basic requirements

This commit is contained in:
sbriat
2023-02-06 13:50:07 +01:00
parent bb5cd96bd9
commit a743fefe94
68 changed files with 18879 additions and 73 deletions

View File

@@ -0,0 +1,46 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import {
CacheInterceptor,
CacheKey,
Controller,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod } from '@nestjs/microservices';
import { Territory } from '../../domain/entities/territory';
import { FindForPointQuery } from '../../queries/find-for-point.query';
import { TerritoryPresenter } from './territory.presenter';
import { ICollection } from '../../../database/src/interfaces/collection.interface';
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
import { FindForPointRequest } from '../../domain/dtos/find-for-point.request';
@UsePipes(
new RpcValidationPipe({
whitelist: true,
forbidUnknownValues: false,
}),
)
@Controller()
export class TerritoriesController {
constructor(
private readonly _queryBus: QueryBus,
@InjectMapper() private readonly _mapper: Mapper,
) {}
@GrpcMethod('TerritoriesService', 'FindForPoint')
@UseInterceptors(CacheInterceptor)
@CacheKey('TerritoriesServiceFindForPoint')
async findAll(data: FindForPointRequest): Promise<ICollection<Territory>> {
const territoryCollection = await this._queryBus.execute(
new FindForPointQuery(data),
);
return Promise.resolve({
data: territoryCollection.data.map((territory: Territory) =>
this._mapper.map(territory, Territory, TerritoryPresenter),
),
total: territoryCollection.total,
});
}
}

View File

@@ -0,0 +1,9 @@
import { AutoMap } from '@automapper/classes';
export class TerritoryPresenter {
@AutoMap()
uuid: string;
@AutoMap()
name: string;
}

View File

@@ -0,0 +1,20 @@
syntax = "proto3";
package territory;
service TerritoriesService {
rpc FindForPoint(Point) returns (Territories);
}
message Point {
float lon = 1;
float lat = 2;
}
message Territories {
repeated Territory data = 1;
}
message Territory {
string name = 1;
}

View File

@@ -0,0 +1,14 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { IMessageBroker } from '../../domain/interfaces/message-broker';
@Injectable()
export class LoggingMessager extends IMessageBroker {
constructor(private readonly _amqpConnection: AmqpConnection) {
super('logging');
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
import { TerritoryRepository } from '../../../database/src/domain/territory-repository';
import { Territory } from '../../domain/entities/territory';
@Injectable()
export class TerritoriesRepository extends TerritoryRepository<Territory> {
protected _model = 'territory';
}

View File

@@ -0,0 +1,14 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { IMessageBroker } from '../../domain/interfaces/message-broker';
@Injectable()
export class TerritoryMessager extends IMessageBroker {
constructor(private readonly _amqpConnection: AmqpConnection) {
super('territory');
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@@ -0,0 +1,11 @@
import { IsNotEmpty, IsNumber } from 'class-validator';
export class FindForPointRequest {
@IsNumber()
@IsNotEmpty()
lon: number;
@IsNumber()
@IsNotEmpty()
lat: number;
}

View File

@@ -0,0 +1,9 @@
import { AutoMap } from '@automapper/classes';
export class Territory {
@AutoMap()
uuid: string;
@AutoMap()
name: string;
}

View File

@@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class IMessageBroker {
exchange: string;
constructor(exchange: string) {
this.exchange = exchange;
}
abstract publish(routingKey: string, message: string): void;
}

View File

@@ -0,0 +1,19 @@
import { QueryHandler } from '@nestjs/cqrs';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository';
import { FindForPointQuery } from '../../queries/find-for-point.query';
import { Territory } from '../entities/territory';
@QueryHandler(FindForPointQuery)
export class FindForPointUseCase {
constructor(private readonly _repository: TerritoriesRepository) {}
async execute(
findForPointQuery: FindForPointQuery,
): Promise<ICollection<Territory>> {
return this._repository.findAll(1, 999999, {
lon: findForPointQuery.lon,
lat: findForPointQuery.lat,
});
}
}

View File

@@ -0,0 +1,18 @@
import { createMap, Mapper } from '@automapper/core';
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import { Injectable } from '@nestjs/common';
import { TerritoryPresenter } from '../adapters/primaries/territory.presenter';
import { Territory } from '../domain/entities/territory';
@Injectable()
export class TerritoryProfile extends AutomapperProfile {
constructor(@InjectMapper() mapper: Mapper) {
super(mapper);
}
override get profile() {
return (mapper) => {
createMap(mapper, Territory, TerritoryPresenter);
};
}
}

View File

@@ -0,0 +1,11 @@
import { FindForPointRequest } from '../domain/dtos/find-for-point.request';
export class FindForPointQuery {
lon: number;
lat: number;
constructor(findForPointRequest?: FindForPointRequest) {
this.lon = findForPointRequest.lon;
this.lat = findForPointRequest.lat;
}
}

View File

@@ -0,0 +1,59 @@
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { RedisClientOptions } from '@liaoliaots/nestjs-redis';
import { CacheModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { redisStore } from 'cache-manager-ioredis-yet';
import { DatabaseModule } from '../database/database.module';
import { TerritoriesController } from './adapters/primaries/territories.controller';
import { LoggingMessager } from './adapters/secondaries/logging.messager';
import { TerritoriesRepository } from './adapters/secondaries/territories.repository';
import { TerritoryMessager } from './adapters/secondaries/territory.messager';
import { FindForPointUseCase } from './domain/usecases/find-for-point.usecase';
import { TerritoryProfile } from './mappers/territory.profile';
@Module({
imports: [
DatabaseModule,
CqrsModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: 'territory',
type: 'topic',
},
{
name: 'logging',
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
CacheModule.registerAsync<RedisClientOptions>({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
store: await redisStore({
host: configService.get<string>('REDIS_HOST'),
port: configService.get<number>('REDIS_PORT'),
ttl: configService.get('CACHE_TTL'),
}),
}),
inject: [ConfigService],
}),
],
controllers: [TerritoriesController],
providers: [
TerritoryProfile,
TerritoriesRepository,
TerritoryMessager,
LoggingMessager,
FindForPointUseCase,
],
exports: [],
})
export class TerritoriesModule {}

View File

@@ -0,0 +1,61 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository';
import { FindForPointRequest } from '../../domain/dtos/find-for-point.request';
import { FindForPointUseCase } from '../../domain/usecases/find-for-point.usecase';
import { FindForPointQuery } from '../../queries/find-for-point.query';
const findForPointRequest: FindForPointRequest = new FindForPointRequest();
findForPointRequest.lon = 6.181455;
findForPointRequest.lat = 48.685689;
const findforPointQuery: FindForPointQuery = new FindForPointQuery(
findForPointRequest,
);
const mockTerritories = [
{
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
name: 'Nancy',
},
{
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
name: 'Meurthe-et-Moselle',
},
];
const mockTerritoriesRepository = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
findAll: jest.fn().mockImplementation((query?: FindForPointQuery) => {
return Promise.resolve(mockTerritories);
}),
};
describe('FindforPointUseCase', () => {
let findForPointUseCase: FindForPointUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: TerritoriesRepository,
useValue: mockTerritoriesRepository,
},
FindForPointUseCase,
],
}).compile();
findForPointUseCase = module.get<FindForPointUseCase>(FindForPointUseCase);
});
it('should be defined', () => {
expect(findForPointUseCase).toBeDefined();
});
describe('execute', () => {
it('should return an array filled with territories', async () => {
const territories = await findForPointUseCase.execute(findforPointQuery);
expect(territories).toBe(mockTerritories);
});
});
});

View File

@@ -0,0 +1,36 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Test, TestingModule } from '@nestjs/testing';
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
describe('LoggingMessager', () => {
let loggingMessager: LoggingMessager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
LoggingMessager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
],
}).compile();
loggingMessager = module.get<LoggingMessager>(LoggingMessager);
});
it('should be defined', () => {
expect(LoggingMessager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
await loggingMessager.publish('territory.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,36 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Test, TestingModule } from '@nestjs/testing';
import { TerritoryMessager } from '../../adapters/secondaries/territory.messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
describe('TerritoryMessager', () => {
let territoryMessager: TerritoryMessager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
TerritoryMessager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
],
}).compile();
territoryMessager = module.get<TerritoryMessager>(TerritoryMessager);
});
it('should be defined', () => {
expect(territoryMessager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
await territoryMessager.publish('territory.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});