NestJS: Microservices với gRPC, API Gateway, và Authentication — Part 2/2
NestJS: Microservices với gRPC, API Gateway, và Authentication — Part 2/2
Authentication Service (grpc-nest-auth-svc)
Cài đặt phụ thuộc
npm i @nestjs/microservices @nestjs/typeorm @nestjs/jwt @nestjs/hộ chiếu hộ chiếu hộ chiếu-jwt typeorm pg class -transformer class -validator bcryptjs
npm i -D @types/node @types/passport-jwt ts-proto
Cấu trúc dự án
Để đơn giản, chúng ta chỉ chọn một mô-đun duy nhất ở đây, mô-đun này sẽ được gọi là auth
Nest g mo auth && Nest g co auth --no-spec
mkdir src/auth/filter && mkdir src/auth/service && mkdir src/auth/strategy
touch src/auth/filter/http-Exception.filter. ts
touch src/auth/service/auth.service.ts
touch src/auth/service/jwt.service.ts
touch src/auth/strategy/jwt.strategy.ts
touch src/auth/auth.dto. ts
touch src/auth/auth.entity.ts
Scripts
Chúng ta cần thêm một số tập lệnh vào package.json để tạo tệp protobuf dựa trên dự án proto được chia sẻ mà chúng ta vừa hoàn thành. Tương tự như những gì chúng ta đã làm trong API Gateway.
Thay thế {{link_github_proto}} bằng link github proto của bạn
proto:install": "npm i {{link_github_proto}}",
"proto:auth": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/auth/ node_modules/grpc-nest-proto/proto/auth.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb"
Chạy lệnh sau:
npm chạy proto:install && npm run proto:auth
Project của chúng ta sẽ như này
HTTP Exception Filter
Vì chúng ta sẽ sử dụng các tệp Data-Transfer-Object để xác thực payload của mình nên chúng ta cần bắt các ngoại lệ HTTP vì gói class-validator mà chúng ta sẽ sử dụng sẽ đưa ra các ngoại lệ HTTP trong trường hợp payload của yêu cầu không hợp lệ. Vì chúng ta không muốn gửi ngoại lệ HTTP từ máy chủ gRPC của mình nên chúng ta sẽ nắm bắt những ngoại lệ này và biến chúng thành phản hồi gRPC bình thường.
Thêm code vào src/auth/filter/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { HttpArgumentsHost } from '@nestjs/common/interfaces';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx: HttpArgumentsHost = host.switchToHttp();
const res: Response = ctx.getResponse<Response>();
const req: Request = ctx.getRequest<Request>();
const status: HttpStatus = exception.getStatus();
if (status === HttpStatus.BAD_REQUEST) {
const res: any = exception.getResponse();
return { status, error: res.message };
}
res.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: req.url,
});
}
}
Xác thực DTO
Thêm một số mã vào src/auth/auth.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
import { LoginRequest, RegisterRequest, ValidateRequest } from './auth.pb';
export class LoginRequestDto implements LoginRequest {
@IsEmail()
public readonly email: string;
@IsString()
public readonly password: string;
}
export class RegisterRequestDto implements RegisterRequest {
@IsEmail()
public readonly email: string;
@IsString()
@MinLength(8)
public readonly password: string;
}
export class ValidateRequestDto implements ValidateRequest {
@IsString()
public readonly token: string;
}
Entity
Thêm một số mã vào src/auth/auth.entity.ts
import { Exclude } from 'class-transformer';
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Auth extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
@Column({ type: 'varchar' })
public email!: string;
@Exclude()
@Column({ type: 'varchar' })
public password!: string;
}
JWT Service
Thêm một số mã vào src/auth/service/jwt.service.ts
mport { Injectable } from '@nestjs/common';
import { JwtService as Jwt } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Auth } from '../auth.entity';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class JwtService {
@InjectRepository(Auth)
private readonly repository: Repository<Auth>;
private readonly jwt: Jwt;
constructor(jwt: Jwt) {
this.jwt = jwt;
}
// Decoding the JWT Token
public async decode(token: string): Promise<unknown> {
return this.jwt.decode(token, null);
}
// Get User by User ID we get from decode()
public async validateUser(decoded: any): Promise<Auth> {
return this.repository.findOne(decoded.id);
}
// Generate JWT Token
public generateToken(auth: Auth): string {
return this.jwt.sign({ id: auth.id, email: auth.email });
}
// Validate User's password
public isPasswordValid(password: string, userPassword: string): boolean {
return bcrypt.compareSync(password, userPassword);
}
// Encode User's password
public encodePassword(password: string): string {
const salt: string = bcrypt.genSaltSync(10);
return bcrypt.hashSync(password, salt);
}
// Validate JWT Token, throw forbidden error if JWT Token is invalid
public async verify(token: string): Promise<any> {
try {
return this.jwt.verify(token);
} catch (err) {}
}
}
JWT Strategy
Thêm một số mã vào src/auth/strategy/jwt.strategy.ts
import { Injectable, Inject } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Auth } from '../auth.entity';
import { JwtService } from '../service/jwt.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
@Inject(JwtService)
private readonly jwtService: JwtService;
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'dev',
ignoreExpiration: true,
});
}
private validate(token: string): Promise<Auth | never> {
return this.jwtService.validateUser(token);
}
}
Auth Service
Thêm một số mã vào src/auth/service/auth.service.ts
import { HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from './jwt.service';
import { RegisterRequestDto, LoginRequestDto, ValidateRequestDto } from '../auth.dto';
import { Auth } from '../auth.entity';
import { LoginResponse, RegisterResponse, ValidateResponse } from '../auth.pb';
@Injectable()
export class AuthService {
@InjectRepository(Auth)
private readonly repository: Repository<Auth>;
@Inject(JwtService)
private readonly jwtService: JwtService;
public async register({ email, password }: RegisterRequestDto): Promise<RegisterResponse> {
let auth: Auth = await this.repository.findOne({ where: { email } });
if (auth) {
return { status: HttpStatus.CONFLICT, error: ['E-Mail already exists'] };
}
auth = new Auth();
auth.email = email;
auth.password = this.jwtService.encodePassword(password);
await this.repository.save(auth);
return { status: HttpStatus.CREATED, error: null };
}
public async login({ email, password }: LoginRequestDto): Promise<LoginResponse> {
const auth: Auth = await this.repository.findOne({ where: { email } });
if (!auth) {
return { status: HttpStatus.NOT_FOUND, error: ['E-Mail not found'], token: null };
}
const isPasswordValid: boolean = this.jwtService.isPasswordValid(password, auth.password);
if (!isPasswordValid) {
return { status: HttpStatus.NOT_FOUND, error: ['Password wrong'], token: null };
}
const token: string = this.jwtService.generateToken(auth);
return { token, status: HttpStatus.OK, error: null };
}
public async validate({ token }: ValidateRequestDto): Promise<ValidateResponse> {
const decoded: Auth = await this.jwtService.verify(token);
if (!decoded) {
return { status: HttpStatus.FORBIDDEN, error: ['Token is invalid'], userId: null };
}
const auth: Auth = await this.jwtService.validateUser(decoded);
if (!auth) {
return { status: HttpStatus.CONFLICT, error: ['User not found'], userId: null };
}
return { status: HttpStatus.OK, error: null, userId: decoded.id };
}
}
Auth Controller
Thay đổi src/auth/auth.controller.ts
import { Controller, Inject } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { LoginRequestDto, RegisterRequestDto, ValidateRequestDto } from './auth.dto';
import { AUTH_SERVICE_NAME, RegisterResponse, LoginResponse, ValidateResponse } from './auth.pb';
import { AuthService } from './service/auth.service';
@Controller()
export class AuthController {
@Inject(AuthService)
private readonly service: AuthService;
@GrpcMethod(AUTH_SERVICE_NAME, 'Register')
private register(payload: RegisterRequestDto): Promise<RegisterResponse> {
return this.service.register(payload);
}
@GrpcMethod(AUTH_SERVICE_NAME, 'Login')
private login(payload: LoginRequestDto): Promise<LoginResponse> {
return this.service.login(payload);
}
@GrpcMethod(AUTH_SERVICE_NAME, 'Validate')
private validate(payload: ValidateRequestDto): Promise<ValidateResponse> {
return this.service.validate(payload);
}
}
Auth Module
Thay đổi src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { Auth } from './auth.entity';
import { AuthService } from './service/auth.service';
import { JwtService } from './service/jwt.service';
import { JwtStrategy } from './strategy/jwt.strategy';
@Module({
imports: [
JwtModule.register({
secret: 'dev',
signOptions: { expiresIn: '365d' },
}),
TypeOrmModule.forFeature([Auth]),
],
controllers: [AuthController],
providers: [AuthService, JwtService, JwtStrategy],
})
export class AuthModule {}
App Module
Thay đổi src/app.module.ts
mport { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
database: 'micro_auth',
username: 'admin',
password: admin,
entities: ['dist/**/*.entity.{ts,js}'],
synchronize: true, // never true in production!
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Main
Thay đổi main.ts
import { INestMicroservice, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './auth/filter/http-exception.filter';
import { protobufPackage } from './auth/auth.pb';
async function bootstrap() {
const app: INestMicroservice = await NestFactory.createMicroservice(AppModule, {
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50051',
package: protobufPackage,
protoPath: join('node_modules/grpc-nest-proto/proto/auth.proto'),
},
});
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen();
}
bootstrap();
Product Service (grpc-nest-product-svc)
Cài đặt phụ thuộc
npm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader @nestjs/typeorm typeorm pg class -transformer class -validator
npm i -D @types/node ts-proto
Cấu trúc dự án
Để đơn giản, chúng ta chỉ chọn một mô-đun duy nhất ở đây, mô-đun này sẽ được gọi là product
Nest g mo sản phẩm && Nest g co sản phẩm --no-spec && Nest gs sản phẩm --no-spec
mkdir src/product/entity
touch src/product/product.dto.ts
touch src/product/entity/ sản phẩm.entity.ts
touch src/product/entity/stock-decrease-log.entity.ts
Scripts
Chúng ta cần thêm một số tập lệnh vào package.json để tạo tệp protobuf dựa trên dự án proto được chia sẻ mà chúng ta vừa hoàn thành. Tương tự như những gì chúng ta đã làm trong API Gateway.
Thay thế {{link_github_proto}} bằng link github proto của bạn
proto:install": "npm i {{link_github_proto}}",
"proto:product": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/product/ node_modules/grpc-nest-proto/proto/product.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb"
Chạy lệnh sau:
npm chạy proto:install && npm run proto:product
Project của chúng ta sẽ như này
Entity
Thêm một số mã vào src/product/entity/product.entity.ts
import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { StockDecreaseLog } from './stock-decrease-log.entity';
@Entity()
export class Product extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
@Column({ type: 'varchar' })
public name!: string;
@Column({ type: 'varchar' })
public sku!: string;
@Column({ type: 'integer' })
public stock!: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
public price!: number;
/*
* One-To-Many Relationships
*/
@OneToMany(() => StockDecreaseLog, (stockDecreaseLog) => stockDecreaseLog.product)
public stockDecreaseLogs: StockDecreaseLog[];
}
StockDecreaseLog Entity
Ngoài ra, chúng ta phải tạo Thực thể StockDecreaseLog của mình. Tại đây, chúng tôi lưu mọi hành động giảm giá được ràng buộc trên ID đơn hàng và ID sản phẩm. Điều này là do chúng tôi muốn ngăn chặn bất kỳ sự giảm giá nhiều lần nào do một lần tạo đơn hàng do các lỗi không thể đoán trước gây ra.
Hãy thêm một số mã vào src/product/entity/stock-decrease-log.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import { Product } from './product.entity';
@Entity()
export class StockDecreaseLog extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
/*
* Relation IDs
*/
@Column({ type: 'integer' })
public orderId!: number;
/*
* Many-To-One Relationships
*/
@ManyToOne(() => Product, (product) => product.stockDecreaseLogs)
public product: Product;
}
Product DTO
Hãy thêm một số mã vào src/product/product.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { CreateProductRequest, DecreaseStockRequest, FindOneRequest } from './product.pb';
export class FindOneRequestDto implements FindOneRequest {
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly id: number;
}
export class CreateProductRequestDto implements CreateProductRequest {
@IsString()
@IsNotEmpty()
public readonly name: string;
@IsString()
@IsNotEmpty()
public readonly sku: string;
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly stock: number;
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly price: number;
}
export class DecreaseStockRequestDto implements DecreaseStockRequest {
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly id: number;
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly orderId: number;
}
Product Service
Hãy thay đổi src/product/product.service.ts
import { HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entity/product.entity';
import { CreateProductRequestDto, DecreaseStockRequestDto, FindOneRequestDto } from './product.dto';
import { CreateProductResponse, DecreaseStockResponse, FindOneResponse } from './product.pb';
import { StockDecreaseLog } from './entity/stock-decrease-log.entity';
@Injectable()
export class ProductService {
@InjectRepository(Product)
private readonly repository: Repository<Product>;
@InjectRepository(StockDecreaseLog)
private readonly decreaseLogRepository: Repository<StockDecreaseLog>;
public async findOne({ id }: FindOneRequestDto): Promise<FindOneResponse> {
const product: Product = await this.repository.findOne({ where: { id } });
if (!product) {
return { data: null, error: ['Product not found'], status: HttpStatus.NOT_FOUND };
}
return { data: product, error: null, status: HttpStatus.OK };
}
public async createProduct(payload: CreateProductRequestDto): Promise<CreateProductResponse> {
const product: Product = new Product();
product.name = payload.name;
product.sku = payload.sku;
product.stock = payload.stock;
product.price = payload.price;
await this.repository.save(product);
return { id: product.id, error: null, status: HttpStatus.OK };
}
public async decreaseStock({ id, orderId }: DecreaseStockRequestDto): Promise<DecreaseStockResponse> {
const product: Product = await this.repository.findOne({ select: ['id', 'stock'], where: { id } });
if (!product) {
return { error: ['Product not found'], status: HttpStatus.NOT_FOUND };
} else if (product.stock <= 0) {
return { error: ['Stock too low'], status: HttpStatus.CONFLICT };
}
const isAlreadyDecreased: number = await this.decreaseLogRepository.count({ where: { orderId } });
if (isAlreadyDecreased) {
// Idempotence
return { error: ['Stock already decreased'], status: HttpStatus.CONFLICT };
}
await this.repository.update(product.id, { stock: product.stock - 1 });
await this.decreaseLogRepository.insert({ product, orderId });
return { error: null, status: HttpStatus.OK };
}
}
Product Controller
Thay đổi src/product/product.controller.ts
import { HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
import { CreateProductRequestDto, DecreaseStockRequestDto, FindOneRequestDto } from './product.dto';
import { CreateProductResponse, DecreaseStockResponse, FindOneResponse } from './product.pb';
@Injectable()
export class ProductService {
@InjectRepository(Product)
private readonly repository: Repository<Product>;
public async findOne({ id }: FindOneRequestDto): Promise<FindOneResponse> {
const product: Product = await this.repository.findOne({ where: { id } });
if (!product) {
return { data: null, error: ['Product not found'], status: HttpStatus.NOT_FOUND };
}
return { data: product, error: null, status: HttpStatus.OK };
}
public async createProduct(payload: CreateProductRequestDto): Promise<CreateProductResponse> {
const product: Product = new Product();
product.name = payload.name;
product.sku = payload.sku;
product.stock = payload.stock;
product.price = payload.price;
await this.repository.save(product);
return { id: product.id, error: null, status: HttpStatus.OK };
}
public async decreaseStock({ id }: DecreaseStockRequestDto): Promise<DecreaseStockResponse> {
const product: Product = await this.repository.findOne({ where: { id } });
if (!product) {
return { error: ['Product not found'], status: HttpStatus.NOT_FOUND };
} else if (product.stock <= 0) {
return { error: ['Stock too low'], status: HttpStatus.CONFLICT };
}
this.repository.update(product.id, { stock: product.stock - 1 });
return { error: null, status: HttpStatus.OK };
}
}
Product Module
Thay đổi src/product/product.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { Product } from './entity/product.entity';
import { StockDecreaseLog } from './entity/stock-decrease-log.entity';
@Module({
imports: [TypeOrmModule.forFeature([Product, StockDecreaseLog])],
controllers: [ProductController],
providers: [ProductService],
})
export class ProductModule {}
App Module
Thay đổi src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductModule } from './product/product.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
database: 'micro_product',
username: 'kevin',
password: null,
entities: ['dist/**/*.entity.{ts,js}'],
synchronize: true, // never true in production!
}),
ProductModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Main
Thay đổi main.ts
mport { INestMicroservice, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
import { protobufPackage } from './product/product.pb';
async function bootstrap() {
const app: INestMicroservice = await NestFactory.createMicroservice(AppModule, {
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50053',
package: protobufPackage,
protoPath: join('node_modules/grpc-nest-proto/proto/product.proto'),
},
});
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen();
}
bootstrap();
Order Service (grpc-nest-order-svc)
Cài đặt phụ thuộc
npm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader @nestjs/typeorm typeorm pg class -transformer class -validator
npm i -D @types/node ts-proto
Cấu trúc dự án
Nest g mo order && Nest g co order --no-spec && Nest gs order --no-spec
mkdir src/order/proto
touch src/order/order.dto.ts
touch src/order/order. thực thể.ts
Scripts
Chúng ta cần thêm một số tập lệnh vào package.json để tạo tệp protobuf dựa trên dự án proto được chia sẻ mà chúng ta vừa hoàn thành. Tương tự như những gì chúng ta đã làm trong API Gateway.
Thay thế {{link_github_proto}} bằng link github proto của bạn
proto:install": "npm i {{link_github_proto}}",
"proto:order": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/order/ node_modules/grpc-nest-proto/proto/order.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb"
Chạy lệnh sau:
npm chạy proto:install && npm run proto:order
Project của chúng ta sẽ như này
Entity
Hãy thêm một số mã vào src/order/order.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Order extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
public price!: number;
/*
* Relation IDs
*/
@Column({ type: 'integer' })
public productId!: number;
@Column({ type: 'integer' })
public userId!: number;
}
Order DTO
Hãy thêm một số mã vào src/order/order.dto.ts
import { IsNumber, Min } from 'class-validator';
import { CreateOrderRequest } from './proto/order.pb';
export class CreateOrderRequestDto implements CreateOrderRequest {
@IsNumber()
public productId: number;
@IsNumber()
@Min(1)
public quantity: number;
@IsNumber()
public userId: number;
}
Order Service
Order Service hơi khác một chút vì đây là lần đầu tiên chúng ta gọi một microservice từ một microservice khác. Chúng ta sẽ gọi Product Microservice hai lần. Đầu tiên, chúng ta sẽ kiểm tra xem sản phẩm có còn tồn tại hay không, sau đó, chúng ta sẽ giảm lượng tồn kho của sản phẩm này để tạo đơn đặt hàng.
Thay đổi src/order/order.service.ts
mport { HttpStatus, Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ClientGrpc } from '@nestjs/microservices';
import { Repository } from 'typeorm';
import { firstValueFrom } from 'rxjs';
import { Order } from './order.entity';
import { FindOneResponse, DecreaseStockResponse, ProductServiceClient, PRODUCT_SERVICE_NAME } from './proto/product.pb';
import { CreateOrderRequest, CreateOrderResponse } from './proto/order.pb';
@Injectable()
export class OrderService implements OnModuleInit {
private productSvc: ProductServiceClient;
@Inject(PRODUCT_SERVICE_NAME)
private readonly client: ClientGrpc;
@InjectRepository(Order)
private readonly repository: Repository<Order>;
public onModuleInit(): void {
this.productSvc = this.client.getService<ProductServiceClient>(PRODUCT_SERVICE_NAME);
}
public async createOrder(data: CreateOrderRequest): Promise<CreateOrderResponse> {
const product: FindOneResponse = await firstValueFrom(this.productSvc.findOne({ id: data.productId }));
if (product.status >= HttpStatus.NOT_FOUND) {
return { id: null, error: ['Product not found'], status: product.status };
} else if (product.data.stock < data.quantity) {
return { id: null, error: ['Stock too less'], status: HttpStatus.CONFLICT };
}
const order: Order = new Order();
order.price = product.data.price;
order.productId = product.data.id;
order.userId = data.userId;
await this.repository.save(order);
const decreasedStockData: DecreaseStockResponse = await firstValueFrom(
this.productSvc.decreaseStock({ id: data.productId, orderId: order.id }),
);
if (decreasedStockData.status === HttpStatus.CONFLICT) {
// deleting order if decreaseStock fails
await this.repository.delete(order);
return { id: null, error: decreasedStockData.error, status: HttpStatus.CONFLICT };
}
return { id: order.id, error: null, status: HttpStatus.OK };
}
}
Order Controller
Thay đổi src/order/order.controller.ts
import { Controller, Inject } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { OrderService } from './order.service';
import { ORDER_SERVICE_NAME, CreateOrderResponse } from './proto/order.pb';
import { CreateOrderRequestDto } from './order.dto';
@Controller('order')
export class OrderController {
@Inject(OrderService)
private readonly service: OrderService;
@GrpcMethod(ORDER_SERVICE_NAME, 'CreateOrder')
private async createOrder(data: CreateOrderRequestDto): Promise<CreateOrderResponse> {
return this.service.createOrder(data);
}
}
Order Module
Thay đổi src/order/order.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrderController } from './order.controller';
import { Order } from './order.entity';
import { OrderService } from './order.service';
import { PRODUCT_SERVICE_NAME, PRODUCT_PACKAGE_NAME } from './proto/product.pb';
@Module({
imports: [
ClientsModule.register([
{
name: PRODUCT_SERVICE_NAME,
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50053',
package: PRODUCT_PACKAGE_NAME,
protoPath: 'node_modules/grpc-nest-proto/proto/product.proto',
},
},
]),
TypeOrmModule.forFeature([Order]),
],
controllers: [OrderController],
providers: [OrderService],
})
export class OrderModule {}
App Module
Thay đổi src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { OrderModule } from './order/order.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
database: 'micro_order',
username: 'kevin',
password: null,
entities: ['dist/**/*.entity.{ts,js}'],
synchronize: true, // never true in production!
}),
OrderModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
main.ts
Thay đổi main.ts
import { INestMicroservice, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
import { protobufPackage } from './order/proto/order.pb';
async function bootstrap() {
const app: INestMicroservice = await NestFactory.createMicroservice(AppModule, {
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50052',
package: protobufPackage,
protoPath: join('node_modules/grpc-nest-proto/proto/order.proto'),
},
});
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen();
}
bootstrap();
Bây giờ mọi thứ đã sẵn sàng, chúng ta có thể chạy các microservice này:
npm run start:dev
All rights reserved