From afca685e3dcad5d3849ef86c53a91d52e90b9488 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 6 Apr 2023 14:21:43 +0200 Subject: [PATCH] create module --- package-lock.json | 12 ++ package.json | 1 + .../20230406093419_init/migration.sql | 65 +++++++++ prisma/migrations/migration_lock.toml | 3 + src/app.module.ts | 2 + src/main.ts | 28 ++-- .../primaries/health-server.controller.ts | 42 ++++++ .../adapters/primaries/health.controller.ts | 34 +++++ .../health/adapters/primaries/health.proto | 21 +++ .../adapters/secondaries/message-broker.ts | 12 ++ .../health/adapters/secondaries/messager.ts | 18 +++ .../prisma.health-indicator.usecase.ts | 25 ++++ src/modules/health/health.module.ts | 34 +++++ .../health/tests/unit/messager.spec.ts | 47 +++++++ .../prisma.health-indicator.usecase.spec.ts | 58 ++++++++ .../adapters/primaries/matcher.controller.ts | 37 +++++ .../matcher/adapters/primaries/matcher.proto | 20 +++ .../adapters/secondaries/ad.repository.ts | 8 ++ .../adapters/secondaries/match.presenter.ts | 6 + .../matcher/domain/dtos/algorithm.enum.ts | 3 + .../matcher/domain/dtos/match.request.ts | 126 ++++++++++++++++++ src/modules/matcher/domain/dtos/role.enum.ts | 4 + src/modules/matcher/domain/entities/ad.ts | 6 + .../domain/entities/margin_durations.type.ts | 9 ++ src/modules/matcher/domain/entities/match.ts | 6 + .../matcher/domain/entities/point.type.ts | 4 + .../matcher/domain/entities/schedule.type.ts | 9 ++ src/modules/matcher/mappers/match.profile.ts | 18 +++ src/modules/matcher/queries/match.query.ts | 9 ++ .../unit/rpc-validation-pipe.usecase.spec.ts | 22 --- 30 files changed, 653 insertions(+), 36 deletions(-) create mode 100644 prisma/migrations/20230406093419_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/modules/health/adapters/primaries/health-server.controller.ts create mode 100644 src/modules/health/adapters/primaries/health.controller.ts create mode 100644 src/modules/health/adapters/primaries/health.proto create mode 100644 src/modules/health/adapters/secondaries/message-broker.ts create mode 100644 src/modules/health/adapters/secondaries/messager.ts create mode 100644 src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts create mode 100644 src/modules/health/health.module.ts create mode 100644 src/modules/health/tests/unit/messager.spec.ts create mode 100644 src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts create mode 100644 src/modules/matcher/adapters/primaries/matcher.controller.ts create mode 100644 src/modules/matcher/adapters/primaries/matcher.proto create mode 100644 src/modules/matcher/adapters/secondaries/ad.repository.ts create mode 100644 src/modules/matcher/adapters/secondaries/match.presenter.ts create mode 100644 src/modules/matcher/domain/dtos/algorithm.enum.ts create mode 100644 src/modules/matcher/domain/dtos/match.request.ts create mode 100644 src/modules/matcher/domain/dtos/role.enum.ts create mode 100644 src/modules/matcher/domain/entities/ad.ts create mode 100644 src/modules/matcher/domain/entities/margin_durations.type.ts create mode 100644 src/modules/matcher/domain/entities/match.ts create mode 100644 src/modules/matcher/domain/entities/point.type.ts create mode 100644 src/modules/matcher/domain/entities/schedule.type.ts create mode 100644 src/modules/matcher/mappers/match.profile.ts create mode 100644 src/modules/matcher/queries/match.query.ts delete mode 100644 src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts diff --git a/package-lock.json b/package-lock.json index 362fe8c..adb8fd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@grpc/grpc-js": "^1.8.13", "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", + "@nestjs/cache-manager": "^1.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.1", "@nestjs/core": "^9.0.0", @@ -1569,6 +1570,17 @@ "node": ">=8" } }, + "node_modules/@nestjs/cache-manager": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-1.0.0.tgz", + "integrity": "sha512-XMNdgsj3H+Ng/SYwFl13vRGNFA3e5Obk8LNwIuHLVSocnK2exReAWtscxEjQhoBc4FW4jAYOgU/U+mt18Q9T0g==", + "peerDependencies": { + "@nestjs/common": "^9.0.0", + "cache-manager": "<=5", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz", diff --git a/package.json b/package.json index 66c5165..9f0f230 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@grpc/grpc-js": "^1.8.13", "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", + "@nestjs/cache-manager": "^1.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.1", "@nestjs/core": "^9.0.0", diff --git a/prisma/migrations/20230406093419_init/migration.sql b/prisma/migrations/20230406093419_init/migration.sql new file mode 100644 index 0000000..836b706 --- /dev/null +++ b/prisma/migrations/20230406093419_init/migration.sql @@ -0,0 +1,65 @@ +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "postgis"; + +-- Required to use postgis extension : +-- set the search_path to both public and territory (where is postgis) AND the current schema +SET search_path TO matcher, territory, public; + +-- CreateTable +CREATE TABLE "ad" ( + "uuid" UUID NOT NULL, + "driver" BOOLEAN NOT NULL, + "passenger" BOOLEAN NOT NULL, + "frequency" INTEGER NOT NULL, + "from_date" DATE NOT NULL, + "to_date" DATE NOT NULL, + "mon_time" TIMESTAMPTZ NOT NULL, + "tue_time" TIMESTAMPTZ NOT NULL, + "wed_time" TIMESTAMPTZ NOT NULL, + "thu_time" TIMESTAMPTZ NOT NULL, + "fri_time" TIMESTAMPTZ NOT NULL, + "sat_time" TIMESTAMPTZ NOT NULL, + "sun_time" TIMESTAMPTZ NOT NULL, + "mon_margin" INTEGER NOT NULL, + "tue_margin" INTEGER NOT NULL, + "wed_margin" INTEGER NOT NULL, + "thu_margin" INTEGER NOT NULL, + "fri_margin" INTEGER NOT NULL, + "sat_margin" INTEGER NOT NULL, + "sun_margin" INTEGER NOT NULL, + "driver_duration" INTEGER NOT NULL, + "driver_distance" INTEGER NOT NULL, + "passenger_duration" INTEGER NOT NULL, + "passenger_distance" INTEGER NOT NULL, + "origin_type" SMALLINT NOT NULL, + "destination_type" SMALLINT NOT NULL, + "waypoints" geography(LINESTRING) NOT NULL, + "direction" geography(LINESTRING) NOT NULL, + "fwd_azimuth" INTEGER NOT NULL, + "back_azimuth" INTEGER NOT NULL, + "seats_driver" SMALLINT NOT NULL, + "seats_passenger" SMALLINT NOT NULL, + "seats_used" SMALLINT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid") +); + +-- CreateIndex +CREATE INDEX "ad_driver_idx" ON "ad"("driver"); + +-- CreateIndex +CREATE INDEX "ad_passenger_idx" ON "ad"("passenger"); + +-- CreateIndex +CREATE INDEX "ad_from_date_idx" ON "ad"("from_date"); + +-- CreateIndex +CREATE INDEX "ad_to_date_idx" ON "ad"("to_date"); + +-- CreateIndex +CREATE INDEX "ad_fwd_azimuth_idx" ON "ad"("fwd_azimuth"); + +-- CreateIndex +CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 31d35cd..e355295 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,12 +3,14 @@ import { AutomapperModule } from '@automapper/nestjs'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ConfigurationModule } from './modules/configuration/configuration.module'; +import { HealthModule } from './modules/health/health.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), AutomapperModule.forRoot({ strategyInitializer: classes() }), ConfigurationModule, + HealthModule, ], controllers: [], providers: [], diff --git a/src/main.ts b/src/main.ts index 261d761..2749d0a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { join } from 'path'; +// import { join } from 'path'; import { AppModule } from './app.module'; async function bootstrap() { @@ -8,19 +8,19 @@ async function bootstrap() { app.connectMicroservice({ transport: Transport.TCP, }); - app.connectMicroservice({ - transport: Transport.GRPC, - options: { - // package: ['matcher', 'health'], - package: ['health'], - protoPath: [ - // join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'), - join(__dirname, 'modules/health/adapters/primaries/health.proto'), - ], - url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, - loader: { keepCase: true }, - }, - }); + // app.connectMicroservice({ + // transport: Transport.GRPC, + // options: { + // // package: ['matcher', 'health'], + // package: ['health'], + // protoPath: [ + // // join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'), + // join(__dirname, 'modules/health/adapters/primaries/health.proto'), + // ], + // url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, + // loader: { keepCase: true }, + // }, + // }); await app.startAllMicroservices(); await app.listen(process.env.HEALTH_SERVICE_PORT); diff --git a/src/modules/health/adapters/primaries/health-server.controller.ts b/src/modules/health/adapters/primaries/health-server.controller.ts new file mode 100644 index 0000000..b58c761 --- /dev/null +++ b/src/modules/health/adapters/primaries/health-server.controller.ts @@ -0,0 +1,42 @@ +import { Controller } from '@nestjs/common'; +import { GrpcMethod } from '@nestjs/microservices'; +import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; + +enum ServingStatus { + UNKNOWN = 0, + SERVING = 1, + NOT_SERVING = 2, +} + +interface HealthCheckRequest { + service: string; +} + +interface HealthCheckResponse { + status: ServingStatus; +} + +@Controller() +export class HealthServerController { + constructor( + private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, + ) {} + + @GrpcMethod('Health', 'Check') + async check( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + data: HealthCheckRequest, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + metadata: any, + ): Promise { + const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy( + 'prisma', + ); + return { + status: + healthCheck['prisma'].status == 'up' + ? ServingStatus.SERVING + : ServingStatus.NOT_SERVING, + }; + } +} diff --git a/src/modules/health/adapters/primaries/health.controller.ts b/src/modules/health/adapters/primaries/health.controller.ts new file mode 100644 index 0000000..e5c5ac3 --- /dev/null +++ b/src/modules/health/adapters/primaries/health.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get } from '@nestjs/common'; +import { + HealthCheckService, + HealthCheck, + HealthCheckResult, +} from '@nestjs/terminus'; +import { Messager } from '../secondaries/messager'; +import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; + +@Controller('health') +export class HealthController { + constructor( + private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, + private _healthCheckService: HealthCheckService, + private _messager: Messager, + ) {} + + @Get() + @HealthCheck() + async check() { + try { + return await this._healthCheckService.check([ + async () => this._prismaHealthIndicatorUseCase.isHealthy('prisma'), + ]); + } catch (error) { + const healthCheckResult: HealthCheckResult = error.response; + this._messager.publish( + 'logging.matcher.health.crit', + JSON.stringify(healthCheckResult.error), + ); + throw error; + } + } +} diff --git a/src/modules/health/adapters/primaries/health.proto b/src/modules/health/adapters/primaries/health.proto new file mode 100644 index 0000000..74e1a4c --- /dev/null +++ b/src/modules/health/adapters/primaries/health.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package health; + + +service Health { + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); +} + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + } + ServingStatus status = 1; +} diff --git a/src/modules/health/adapters/secondaries/message-broker.ts b/src/modules/health/adapters/secondaries/message-broker.ts new file mode 100644 index 0000000..594aa43 --- /dev/null +++ b/src/modules/health/adapters/secondaries/message-broker.ts @@ -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; +} diff --git a/src/modules/health/adapters/secondaries/messager.ts b/src/modules/health/adapters/secondaries/messager.ts new file mode 100644 index 0000000..0725261 --- /dev/null +++ b/src/modules/health/adapters/secondaries/messager.ts @@ -0,0 +1,18 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IMessageBroker } from './message-broker'; + +@Injectable() +export class Messager extends IMessageBroker { + constructor( + private readonly _amqpConnection: AmqpConnection, + configService: ConfigService, + ) { + super(configService.get('RMQ_EXCHANGE')); + } + + publish(routingKey: string, message: string): void { + this._amqpConnection.publish(this.exchange, routingKey, message); + } +} diff --git a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts new file mode 100644 index 0000000..0b788eb --- /dev/null +++ b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { + HealthCheckError, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; +import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository'; + +@Injectable() +export class PrismaHealthIndicatorUseCase extends HealthIndicator { + constructor(private readonly _repository: AdRepository) { + super(); + } + + async isHealthy(key: string): Promise { + try { + await this._repository.healthCheck(); + return this.getStatus(key, true); + } catch (e) { + throw new HealthCheckError('Prisma', { + prisma: e.message, + }); + } + } +} diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts new file mode 100644 index 0000000..db4980d --- /dev/null +++ b/src/modules/health/health.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { HealthServerController } from './adapters/primaries/health-server.controller'; +import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase'; +import { DatabaseModule } from '../database/database.module'; +import { HealthController } from './adapters/primaries/health.controller'; +import { TerminusModule } from '@nestjs/terminus'; +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { Messager } from './adapters/secondaries/messager'; +import { AdRepository } from '../matcher/adapters/secondaries/ad.repository'; + +@Module({ + imports: [ + TerminusModule, + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + exchanges: [ + { + name: configService.get('RMQ_EXCHANGE'), + type: 'topic', + }, + ], + uri: configService.get('RMQ_URI'), + connectionInitOptions: { wait: false }, + }), + inject: [ConfigService], + }), + DatabaseModule, + ], + controllers: [HealthServerController, HealthController], + providers: [PrismaHealthIndicatorUseCase, AdRepository, Messager], +}) +export class HealthModule {} diff --git a/src/modules/health/tests/unit/messager.spec.ts b/src/modules/health/tests/unit/messager.spec.ts new file mode 100644 index 0000000..0331332 --- /dev/null +++ b/src/modules/health/tests/unit/messager.spec.ts @@ -0,0 +1,47 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Messager } from '../../adapters/secondaries/messager'; + +const mockAmqpConnection = { + publish: jest.fn().mockImplementation(), +}; + +const mockConfigService = { + get: jest.fn().mockResolvedValue({ + RMQ_EXCHANGE: 'mobicoop', + }), +}; + +describe('Messager', () => { + let messager: Messager; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + Messager, + { + provide: AmqpConnection, + useValue: mockAmqpConnection, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + messager = module.get(Messager); + }); + + it('should be defined', () => { + expect(messager).toBeDefined(); + }); + + it('should publish a message', async () => { + jest.spyOn(mockAmqpConnection, 'publish'); + messager.publish('test.create.info', 'my-test'); + expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts new file mode 100644 index 0000000..971a836 --- /dev/null +++ b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; +import { TerritoriesRepository } from '../../../territory/adapters/secondaries/territories.repository'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; +import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus'; + +const mockTerritoriesRepository = { + healthCheck: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve(true); + }) + .mockImplementation(() => { + throw new PrismaClientKnownRequestError('Service unavailable', { + code: 'code', + clientVersion: 'version', + }); + }), +}; + +describe('PrismaHealthIndicatorUseCase', () => { + let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: TerritoriesRepository, + useValue: mockTerritoriesRepository, + }, + PrismaHealthIndicatorUseCase, + ], + }).compile(); + + prismaHealthIndicatorUseCase = module.get( + PrismaHealthIndicatorUseCase, + ); + }); + + it('should be defined', () => { + expect(prismaHealthIndicatorUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should check health successfully', async () => { + const healthIndicatorResult: HealthIndicatorResult = + await prismaHealthIndicatorUseCase.isHealthy('prisma'); + + expect(healthIndicatorResult['prisma'].status).toBe('up'); + }); + + it('should throw an error if database is unavailable', async () => { + await expect( + prismaHealthIndicatorUseCase.isHealthy('prisma'), + ).rejects.toBeInstanceOf(HealthCheckError); + }); + }); +}); diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts new file mode 100644 index 0000000..2d78ace --- /dev/null +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -0,0 +1,37 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, UsePipes } from '@nestjs/common'; +import { CommandBus, QueryBus } from '@nestjs/cqrs'; +import { GrpcMethod } from '@nestjs/microservices'; +import { RpcValidationPipe } from 'src/modules/utils/pipes/rpc.validation-pipe'; +import { MatchRequest } from '../../domain/dtos/match.request'; +import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { Match } from '../../domain/entities/match'; +import { MatchQuery } from '../../queries/match.query'; +import { MatchPresenter } from '../secondaries/match.presenter'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }), +) +@Controller() +export class MatcherController { + constructor( + private readonly _commandBus: CommandBus, + private readonly _queryBus: QueryBus, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + @GrpcMethod('MatcherService', 'Match') + async match(data: MatchRequest): Promise> { + const matchCollection = await this._queryBus.execute(new MatchQuery(data)); + return Promise.resolve({ + data: matchCollection.data.map((match: Match) => + this._mapper.map(match, Match, MatchPresenter), + ), + total: matchCollection.total, + }); + } +} diff --git a/src/modules/matcher/adapters/primaries/matcher.proto b/src/modules/matcher/adapters/primaries/matcher.proto new file mode 100644 index 0000000..8c18b46 --- /dev/null +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package matcher; + +service MatcherService { + rpc Match(MatchRequest) returns (Matches); +} + +message MatchRequest { + string uuid = 1; +} + +message Match { + string uuid = 1; +} + +message Matches { + repeated Match data = 1; + int32 total = 2; +} diff --git a/src/modules/matcher/adapters/secondaries/ad.repository.ts b/src/modules/matcher/adapters/secondaries/ad.repository.ts new file mode 100644 index 0000000..696bac9 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/ad.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; +import { MatcherRepository } from '../../../database/src/domain/matcher-repository'; +import { Ad } from '../../domain/entities/ad'; + +@Injectable() +export class AdRepository extends MatcherRepository { + protected _model = 'ad'; +} diff --git a/src/modules/matcher/adapters/secondaries/match.presenter.ts b/src/modules/matcher/adapters/secondaries/match.presenter.ts new file mode 100644 index 0000000..4d7fd5e --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/match.presenter.ts @@ -0,0 +1,6 @@ +import { AutoMap } from '@automapper/classes'; + +export class MatchPresenter { + @AutoMap() + uuid: string; +} diff --git a/src/modules/matcher/domain/dtos/algorithm.enum.ts b/src/modules/matcher/domain/dtos/algorithm.enum.ts new file mode 100644 index 0000000..0ed0cbc --- /dev/null +++ b/src/modules/matcher/domain/dtos/algorithm.enum.ts @@ -0,0 +1,3 @@ +export enum Algorithm { + CLASSIC = 'CLASSIC', +} diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts new file mode 100644 index 0000000..7384689 --- /dev/null +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -0,0 +1,126 @@ +import { + IsArray, + IsBoolean, + IsDate, + IsEnum, + IsInt, + IsNumber, + IsOptional, + Max, + Min, +} from 'class-validator'; +import { AutoMap } from '@automapper/classes'; +import { Point } from '../entities/point.type'; +import { Schedule } from '../entities/schedule.type'; +import { MarginDurations } from '../entities/margin_durations.type'; +import { Algorithm } from './algorithm.enum'; + +export class MatchRequest { + @IsArray() + @AutoMap() + waypoints: Array; + + @IsDate() + @IsOptional() + @AutoMap() + departure: Date; + + @IsDate() + @IsOptional() + @AutoMap() + fromDate: Date; + + @IsOptional() + @AutoMap() + schedule: Schedule; + + @IsOptional() + @IsBoolean() + @AutoMap() + driver: boolean; + + @IsOptional() + @IsBoolean() + @AutoMap() + passenger: boolean; + + @IsOptional() + @IsDate() + @AutoMap() + toDate: Date; + + @IsOptional() + @IsInt() + @AutoMap() + marginDuration: number; + + @IsOptional() + @AutoMap() + marginDurations: MarginDurations; + + @IsOptional() + @IsNumber() + @AutoMap() + seatsPassenger: number; + + @IsOptional() + @IsNumber() + @AutoMap() + seatsDriver: number; + + @IsOptional() + @AutoMap() + strict: boolean; + + @IsOptional() + @IsEnum(Algorithm) + @AutoMap() + algorithm: Algorithm; + + @IsOptional() + @IsNumber() + @AutoMap() + remoteness: number; + + @IsOptional() + @IsBoolean() + @AutoMap() + useProportion: boolean; + + @IsOptional() + @IsNumber() + @Min(0) + @Max(1) + @AutoMap() + proportion: number; + + @IsOptional() + @IsBoolean() + @AutoMap() + useAzimuth: boolean; + + @IsOptional() + @IsInt() + @Min(0) + @Max(359) + @AutoMap() + azimuthMargin: number; + + @IsOptional() + @IsNumber() + @Min(0) + @Max(1) + @AutoMap() + maxDetourDistanceRatio: number; + + @IsOptional() + @IsNumber() + @Min(0) + @Max(1) + @AutoMap() + maxDetourDurationRatio: number; + + @IsOptional() + @IsArray() + exclusions: Array; +} diff --git a/src/modules/matcher/domain/dtos/role.enum.ts b/src/modules/matcher/domain/dtos/role.enum.ts new file mode 100644 index 0000000..7522f80 --- /dev/null +++ b/src/modules/matcher/domain/dtos/role.enum.ts @@ -0,0 +1,4 @@ +export enum Role { + DRIVER = 'DRIVER', + PASSENGER = 'PASSENGER', +} diff --git a/src/modules/matcher/domain/entities/ad.ts b/src/modules/matcher/domain/entities/ad.ts new file mode 100644 index 0000000..0350f1a --- /dev/null +++ b/src/modules/matcher/domain/entities/ad.ts @@ -0,0 +1,6 @@ +import { AutoMap } from '@automapper/classes'; + +export class Ad { + @AutoMap() + uuid: string; +} diff --git a/src/modules/matcher/domain/entities/margin_durations.type.ts b/src/modules/matcher/domain/entities/margin_durations.type.ts new file mode 100644 index 0000000..720f392 --- /dev/null +++ b/src/modules/matcher/domain/entities/margin_durations.type.ts @@ -0,0 +1,9 @@ +export type MarginDurations = { + mon: number; + tue: number; + wed: number; + thu: number; + fri: number; + sat: number; + sun: number; +}; diff --git a/src/modules/matcher/domain/entities/match.ts b/src/modules/matcher/domain/entities/match.ts new file mode 100644 index 0000000..83c399c --- /dev/null +++ b/src/modules/matcher/domain/entities/match.ts @@ -0,0 +1,6 @@ +import { AutoMap } from '@automapper/classes'; + +export class Match { + @AutoMap() + uuid: string; +} diff --git a/src/modules/matcher/domain/entities/point.type.ts b/src/modules/matcher/domain/entities/point.type.ts new file mode 100644 index 0000000..9bb160e --- /dev/null +++ b/src/modules/matcher/domain/entities/point.type.ts @@ -0,0 +1,4 @@ +export type Point = { + lon: number; + lat: number; +}; diff --git a/src/modules/matcher/domain/entities/schedule.type.ts b/src/modules/matcher/domain/entities/schedule.type.ts new file mode 100644 index 0000000..8ee0874 --- /dev/null +++ b/src/modules/matcher/domain/entities/schedule.type.ts @@ -0,0 +1,9 @@ +export type Schedule = { + mon: string; + tue: string; + wed: string; + thu: string; + fri: string; + sat: string; + sun: string; +}; diff --git a/src/modules/matcher/mappers/match.profile.ts b/src/modules/matcher/mappers/match.profile.ts new file mode 100644 index 0000000..9276c15 --- /dev/null +++ b/src/modules/matcher/mappers/match.profile.ts @@ -0,0 +1,18 @@ +import { createMap, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { MatchPresenter } from '../adapters/secondaries/match.presenter'; +import { Match } from '../domain/entities/match'; + +@Injectable() +export class MatchProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap(mapper, Match, MatchPresenter); + }; + } +} diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts new file mode 100644 index 0000000..53ac933 --- /dev/null +++ b/src/modules/matcher/queries/match.query.ts @@ -0,0 +1,9 @@ +import { MatchRequest } from '../domain/dtos/match.request'; + +export class MatchQuery { + matchRequest: MatchRequest; + + constructor(matchRequest?: MatchRequest) { + this.matchRequest = matchRequest; + } +} diff --git a/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts b/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts deleted file mode 100644 index 5df3e07..0000000 --- a/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ArgumentMetadata } from '@nestjs/common'; -import { UpdateTerritoryRequest } from '../../../modules/territory/domain/dtos/update-territory.request'; -import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe'; - -describe('RpcValidationPipe', () => { - it('should not validate request', async () => { - const target: RpcValidationPipe = new RpcValidationPipe({ - whitelist: true, - forbidUnknownValues: false, - }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: UpdateTerritoryRequest, - data: '', - }; - await target - .transform({}, metadata) - .catch((err) => { - expect(err.message).toEqual('Rpc Exception'); - }); - }); -});