Skip to content

Commit 684ccf6

Browse files
committed
Import initial structure and code
Signed-off-by: Laurent Broudoux <[email protected]>
1 parent b105225 commit 684ccf6

39 files changed

+1641
-0
lines changed

assets/order-service-architecture.png

88.9 KB
Loading

assets/order-service-ecosystem.png

69.3 KB
Loading

src/.DS_Store

6 KB
Binary file not shown.

src/app.controller.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AppController } from './app.controller';
3+
import { AppService } from './app.service';
4+
5+
describe('AppController', () => {
6+
let appController: AppController;
7+
8+
beforeEach(async () => {
9+
const app: TestingModule = await Test.createTestingModule({
10+
controllers: [AppController],
11+
providers: [AppService],
12+
}).compile();
13+
14+
appController = app.get<AppController>(AppController);
15+
});
16+
17+
describe('root', () => {
18+
it('should return "Hello World!"', () => {
19+
expect(appController.getHello()).toBe('Hello World!');
20+
});
21+
});
22+
});

src/app.controller.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Controller, Get } from '@nestjs/common';
2+
import { AppService } from './app.service';
3+
4+
@Controller()
5+
export class AppController {
6+
constructor(private readonly appService: AppService) {}
7+
8+
@Get()
9+
getHello(): string {
10+
return this.appService.getHello();
11+
}
12+
}

src/app.module.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { AppController } from './app.controller';
4+
import { AppService } from './app.service';
5+
import { OrderModule } from './order/order.module';
6+
import { PastryModule } from './pastry/pastry.module';
7+
8+
@Module({
9+
imports: [OrderModule],
10+
controllers: [AppController],
11+
providers: [AppService],
12+
})
13+
export class AppModule {}

src/app.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class AppService {
5+
getHello(): string {
6+
return 'Hello World!';
7+
}
8+
}

src/main.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { NestFactory } from '@nestjs/core';
2+
import { AppModule } from './app.module';
3+
4+
async function bootstrap() {
5+
const app = await NestFactory.create(AppModule);
6+
await app.listen(3000);
7+
}
8+
bootstrap();

src/order/.DS_Store

6 KB
Binary file not shown.

src/order/dto/order-event.dto.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Order } from "../entities/order.entity"
2+
3+
export class OrderEvent {
4+
timestamp: number
5+
order: Order
6+
changeReason: string
7+
}

src/order/dto/order-info.dto.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ProductQuantity } from "../entities/product-quantity.entity"
2+
3+
export class OrderInfoDto {
4+
customerId: string
5+
productQuantities: ProductQuantity[]
6+
totalPrice: number
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class UnavailableProduct {
2+
productName: string
3+
details: string
4+
5+
constructor(productName: string, details: string) {
6+
this.productName = productName;
7+
this.details = details;
8+
}
9+
}

src/order/entities/order-status.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum OrderStatus {
2+
CREATED = 'CREATED',
3+
VALIDATED = 'VALIDATED',
4+
CANCELED = 'CANCELED',
5+
FAILED = 'FAILED'
6+
}

src/order/entities/order.entity.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { uid } from 'uid';
2+
3+
import { OrderStatus } from "./order-status"
4+
import { ProductQuantity } from "./product-quantity.entity"
5+
6+
export class Order {
7+
id: string = uid()
8+
status: OrderStatus = OrderStatus.CREATED
9+
customerId: string
10+
productQuantities: ProductQuantity[]
11+
totalPrice: number
12+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export class ProductQuantity {
2+
productName: string
3+
quantity: number
4+
}

src/order/order-event.listener.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Controller, Inject, OnApplicationShutdown } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import { ClientKafka, Ctx, EventPattern, KafkaContext, Payload, Transport } from "@nestjs/microservices";
4+
5+
import { OrderService } from './order.service';
6+
import { OrderEvent } from "./dto/order-event.dto";
7+
8+
@Controller('orders-listener')
9+
export class OrderEventListener implements OnApplicationShutdown {
10+
consumer: any;
11+
12+
//constructor(private readonly orderService: OrderService) {}
13+
14+
constructor(configService: ConfigService,
15+
private readonly orderService: OrderService,
16+
@Inject('ORDER_SERVICE') private readonly client: ClientKafka) {
17+
18+
19+
// Add to go through this low-level stuff to get ConfigService topic name.
20+
let kafka = client.createClient();
21+
this.consumer = kafka.consumer({ groupId: 'order-service-consumer' })
22+
this.consumer.subscribe({ topics: [configService.get<string>('order-events-reviewed.topic')], fromBeginning: false });
23+
this.consumer.run({
24+
eachMessage: async ({ topic, partition, message }) => {
25+
console.log(`Received OrderEvent: ${message.value.toString()}`);
26+
this.orderService.updateReviewedOrder(JSON.parse(message.value.toString()) as OrderEvent);
27+
},
28+
})
29+
}
30+
31+
onApplicationShutdown(signal: string) {
32+
console.log('Disconnecting Kafka consumer');
33+
this.consumer.disconnect();
34+
}
35+
36+
// Using @EventPattern is more elegant but cannot find how to use the ConfigService to dynamically
37+
// get the name of the topic to listen to.
38+
/*
39+
@EventPattern('OrderEventsAPI-0.1.0-orders-reviewed', Transport.KAFKA)
40+
handleReviewedOrder(@Payload() message: OrderEvent, @Ctx() context: KafkaContext) {
41+
console.log(`Topic: ${context.getTopic()}`);
42+
}
43+
*/
44+
}

src/order/order-event.publisher.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import { OrderEvent } from './dto/order-event.dto';
3+
import { ClientKafka } from '@nestjs/microservices';
4+
5+
@Injectable()
6+
export class OrderEventPublisher {
7+
8+
constructor(@Inject('ORDER_SERVICE') private readonly client: ClientKafka) {}
9+
10+
publishOrderCreated(event: OrderEvent) {
11+
console.log("Emitting order event");
12+
this.client.emit<any>('orders-created', JSON.stringify(event));
13+
}
14+
}

src/order/order-not-found.error.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class OrderNotFoundException extends Error {
2+
id: string
3+
4+
constructor(id: string) {
5+
super();
6+
this.id = id;
7+
}
8+
}

src/order/order.controller.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
2+
import { Response } from 'express';
3+
4+
import { OrderService } from './order.service';
5+
import { UnavailablePastryError } from './unavailable-pastry.error';
6+
7+
import { OrderInfoDto } from './dto/order-info.dto';
8+
import { UnavailableProduct } from './dto/unavailable-product.dto';
9+
10+
11+
@Controller('orders')
12+
export class OrderController {
13+
constructor(private readonly orderService: OrderService) {}
14+
15+
@Post()
16+
async create(@Body() orderInfo: OrderInfoDto, @Res() res: Response) {
17+
let result = null;
18+
try {
19+
result = await this.orderService.create(orderInfo);
20+
} catch (err) {
21+
if (err instanceof UnavailablePastryError) {
22+
return res.status(HttpStatus.UNPROCESSABLE_ENTITY)
23+
.json(new UnavailableProduct(err.product, err.message))
24+
.send();
25+
} else {
26+
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send();
27+
}
28+
}
29+
30+
return res.status(HttpStatus.CREATED).json(result).send();
31+
}
32+
}

src/order/order.module.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule, ConfigService } from '@nestjs/config';
3+
import { ClientsModule, Transport } from '@nestjs/microservices';
4+
5+
import { OrderService } from './order.service';
6+
import { OrderController } from './order.controller';
7+
import { PastryModule } from '../pastry/pastry.module';
8+
import { OrderEventListener } from './order-event.listener';
9+
import { OrderEventPublisher } from './order-event.publisher';
10+
11+
12+
@Module({
13+
imports: [PastryModule,
14+
ConfigModule.forRoot({
15+
load: [() => ({
16+
'brokers.url': 'localhost:9092',
17+
'order-events-reviewed.topic': 'OrderEventsAPI-0.1.0-orders-reviewed'
18+
})],
19+
}),
20+
ClientsModule.registerAsync([
21+
{
22+
name: 'ORDER_SERVICE',
23+
imports: [ConfigModule],
24+
useFactory:async(configService: ConfigService) => ({
25+
transport: Transport.KAFKA,
26+
options: {
27+
client: {
28+
clientId: 'order-service',
29+
brokers: [configService.get<string>('brokers.url')],
30+
},
31+
consumer: {
32+
groupId: 'order-service'
33+
},
34+
producer: {
35+
allowAutoTopicCreation: true
36+
}
37+
}
38+
}),
39+
inject: [ConfigService],
40+
}
41+
])
42+
43+
/*
44+
ClientsModule.register([
45+
{
46+
name: 'ORDER_SERVICE',
47+
transport: Transport.KAFKA,
48+
options: {
49+
client: {
50+
clientId: 'order-service',
51+
brokers: ['localhost:9092'],
52+
},
53+
consumer: {
54+
groupId: 'order-service'
55+
}
56+
}
57+
},
58+
]),
59+
*/
60+
61+
],
62+
controllers: [OrderController, OrderEventListener],
63+
providers: [OrderService, OrderEventPublisher],
64+
})
65+
export class OrderModule {}

src/order/order.service.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
import { OrderInfoDto } from './dto/order-info.dto';
4+
5+
import { Order } from './entities/order.entity';
6+
import { UnavailablePastryError } from './unavailable-pastry.error'
7+
8+
import { Pastry } from '../pastry/pastry.dto';
9+
import { PastryService } from '../pastry/pastry.service';
10+
import { OrderEventPublisher } from './order-event.publisher';
11+
import { OrderEvent } from './dto/order-event.dto';
12+
import { OrderNotFoundException } from './order-not-found.error';
13+
14+
@Injectable()
15+
export class OrderService {
16+
17+
orderEventsRepository: Map<string, OrderEvent[]> = new Map<string, OrderEvent[]>();
18+
19+
constructor(private readonly pastryService: PastryService,
20+
private readonly orderEventPublisher: OrderEventPublisher) {}
21+
22+
/**
23+
*
24+
* @param orderInfo
25+
* @returns
26+
* @throws {UnavailablePastryError}
27+
*/
28+
async create(orderInfo: OrderInfoDto): Promise<Order> {
29+
let pastryPromises: Promise<Pastry>[] = [];
30+
31+
for (var i=0; i<orderInfo.productQuantities.length; i++) {
32+
let productQuantitiy = orderInfo.productQuantities[i];
33+
pastryPromises.push(this.pastryService.getPastry(productQuantitiy.productName));
34+
}
35+
36+
let pastries: PromiseSettledResult<Pastry>[] = await Promise.allSettled(pastryPromises)
37+
for (var i=0; i<pastries.length; i++) {
38+
let pastry = pastries[i];
39+
if (pastry.status === 'fulfilled') {
40+
if (pastry.value.status != 'available') {
41+
throw new UnavailablePastryError("Pastry " + pastry.value.name + " is not available", pastry.value.name);
42+
}
43+
}
44+
}
45+
46+
let result = new Order();
47+
result.customerId = orderInfo.customerId;
48+
result.productQuantities = orderInfo.productQuantities;
49+
result.totalPrice = orderInfo.totalPrice;
50+
51+
// Persist and publish creation event.
52+
let orderEvent = new OrderEvent();
53+
orderEvent.timestamp = Date.now();
54+
orderEvent.order = result;
55+
orderEvent.changeReason = 'Creation';
56+
this.persistOrderEvent(orderEvent);
57+
this.orderEventPublisher.publishOrderCreated(orderEvent)
58+
59+
return result;
60+
}
61+
62+
/**
63+
*
64+
* @param id
65+
* @returns
66+
* @throws {OrderNotFoundException}
67+
*/
68+
getOrder(id: string): Order {
69+
let orderEvents = this.orderEventsRepository.get(id);
70+
if (orderEvents != undefined) {
71+
return orderEvents[orderEvents.length - 1].order;
72+
}
73+
throw new OrderNotFoundException(id);
74+
}
75+
76+
updateReviewedOrder(event: OrderEvent): void {
77+
this.persistOrderEvent(event);
78+
}
79+
80+
private persistOrderEvent(event: OrderEvent): void {
81+
let orderEvents = this.orderEventsRepository.get(event.order.id);
82+
if (orderEvents == undefined) {
83+
orderEvents = [];
84+
}
85+
orderEvents.push(event);
86+
this.orderEventsRepository.set(event.order.id, orderEvents);
87+
}
88+
}

src/order/unavailable-pastry.error.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class UnavailablePastryError extends Error {
2+
product: string
3+
4+
constructor(message?: string, product?: string) {
5+
super(message);
6+
this.product = product;
7+
}
8+
}

0 commit comments

Comments
 (0)