mirror of
https://gitlab.com/mobicoop/v3/service/territory.git
synced 2026-03-22 16:55:50 +00:00
basic requirements
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class TerritoryPresenter {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
name: string;
|
||||
}
|
||||
20
src/modules/territories/adapters/primaries/territory.proto
Normal file
20
src/modules/territories/adapters/primaries/territory.proto
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { IsNotEmpty, IsNumber } from 'class-validator';
|
||||
|
||||
export class FindForPointRequest {
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
lon: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
lat: number;
|
||||
}
|
||||
9
src/modules/territories/domain/entities/territory.ts
Normal file
9
src/modules/territories/domain/entities/territory.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class Territory {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
name: string;
|
||||
}
|
||||
12
src/modules/territories/domain/interfaces/message-broker.ts
Normal file
12
src/modules/territories/domain/interfaces/message-broker.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
18
src/modules/territories/mappers/territory.profile.ts
Normal file
18
src/modules/territories/mappers/territory.profile.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
11
src/modules/territories/queries/find-for-point.query.ts
Normal file
11
src/modules/territories/queries/find-for-point.query.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
59
src/modules/territories/territories.module.ts
Normal file
59
src/modules/territories/territories.module.ts
Normal 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 {}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user