From afca685e3dcad5d3849ef86c53a91d52e90b9488 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 6 Apr 2023 14:21:43 +0200 Subject: [PATCH 01/26] 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'); - }); - }); -}); From 3b0f4b8c49ba69bdc584d42aab1e04c097b009cc Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 6 Apr 2023 17:05:25 +0200 Subject: [PATCH 02/26] basic match action without functional behaviour --- package.json | 6 ++ src/app.module.ts | 2 + src/main.ts | 27 ++++----- .../prisma.health-indicator.usecase.spec.ts | 10 ++-- .../adapters/primaries/matcher.controller.ts | 3 +- .../matcher/adapters/primaries/matcher.proto | 7 ++- .../adapters/secondaries/message-broker.ts | 12 ++++ .../matcher/adapters/secondaries/messager.ts | 18 ++++++ .../matcher/domain/usecases/match.usecase.ts | 38 ++++++++++++ src/modules/matcher/matcher.module.ts | 35 +++++++++++ .../matcher/tests/unit/match.usecase.spec.ts | 59 +++++++++++++++++++ .../matcher/tests/unit/messager.spec.ts | 47 +++++++++++++++ .../unit/rpc-validation-pipe.usecase.spec.ts | 20 +++++++ 13 files changed, 262 insertions(+), 22 deletions(-) create mode 100644 src/modules/matcher/adapters/secondaries/message-broker.ts create mode 100644 src/modules/matcher/adapters/secondaries/messager.ts create mode 100644 src/modules/matcher/domain/usecases/match.usecase.ts create mode 100644 src/modules/matcher/matcher.module.ts create mode 100644 src/modules/matcher/tests/unit/match.usecase.spec.ts create mode 100644 src/modules/matcher/tests/unit/messager.spec.ts create mode 100644 src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts diff --git a/package.json b/package.json index 9f0f230..ce6ad16 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,12 @@ "json", "ts" ], + "modulePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + ".request.ts", + "main.ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { diff --git a/src/app.module.ts b/src/app.module.ts index e355295..69bca0e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ConfigurationModule } from './modules/configuration/configuration.module'; import { HealthModule } from './modules/health/health.module'; +import { MatcherModule } from './modules/matcher/matcher.module'; @Module({ imports: [ @@ -11,6 +12,7 @@ import { HealthModule } from './modules/health/health.module'; AutomapperModule.forRoot({ strategyInitializer: classes() }), ConfigurationModule, HealthModule, + MatcherModule, ], controllers: [], providers: [], diff --git a/src/main.ts b/src/main.ts index 2749d0a..0daaf12 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,18 @@ 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'], + 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/tests/unit/prisma.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts index 971a836..7d3cf42 100644 --- a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts +++ b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts @@ -1,10 +1,10 @@ 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'; +import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; -const mockTerritoriesRepository = { +const mockAdRepository = { healthCheck: jest .fn() .mockImplementationOnce(() => { @@ -25,8 +25,8 @@ describe('PrismaHealthIndicatorUseCase', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { - provide: TerritoriesRepository, - useValue: mockTerritoriesRepository, + provide: AdRepository, + useValue: mockAdRepository, }, PrismaHealthIndicatorUseCase, ], diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 2d78ace..a7619fb 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -1,7 +1,7 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Controller, UsePipes } from '@nestjs/common'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; +import { 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'; @@ -19,7 +19,6 @@ import { MatchPresenter } from '../secondaries/match.presenter'; @Controller() export class MatcherController { constructor( - private readonly _commandBus: CommandBus, private readonly _queryBus: QueryBus, @InjectMapper() private readonly _mapper: Mapper, ) {} diff --git a/src/modules/matcher/adapters/primaries/matcher.proto b/src/modules/matcher/adapters/primaries/matcher.proto index 8c18b46..fa642d5 100644 --- a/src/modules/matcher/adapters/primaries/matcher.proto +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -7,7 +7,12 @@ service MatcherService { } message MatchRequest { - string uuid = 1; + repeated Point waypoints = 1; +} + +message Point { + float lon = 1; + float lat = 2; } message Match { diff --git a/src/modules/matcher/adapters/secondaries/message-broker.ts b/src/modules/matcher/adapters/secondaries/message-broker.ts new file mode 100644 index 0000000..594aa43 --- /dev/null +++ b/src/modules/matcher/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/matcher/adapters/secondaries/messager.ts b/src/modules/matcher/adapters/secondaries/messager.ts new file mode 100644 index 0000000..0725261 --- /dev/null +++ b/src/modules/matcher/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/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts new file mode 100644 index 0000000..e925367 --- /dev/null +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -0,0 +1,38 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { QueryHandler } from '@nestjs/cqrs'; +import { Messager } from '../../adapters/secondaries/messager'; +import { MatchQuery } from '../../queries/match.query'; +import { AdRepository } from '../../adapters/secondaries/ad.repository'; +import { Match } from '../entities/match'; +import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; + +@QueryHandler(MatchQuery) +export class MatchUseCase { + constructor( + private readonly _repository: AdRepository, + private readonly _messager: Messager, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + async execute(matchQuery: MatchQuery): Promise> { + try { + const match = new Match(); + match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; + this._messager.publish('matcher.match', 'match !'); + return { + data: [match], + total: 1, + }; + } catch (error) { + this._messager.publish( + 'logging.matcher.match.crit', + JSON.stringify({ + matchQuery, + error, + }), + ); + throw error; + } + } +} diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts new file mode 100644 index 0000000..8071f27 --- /dev/null +++ b/src/modules/matcher/matcher.module.ts @@ -0,0 +1,35 @@ +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { CqrsModule } from '@nestjs/cqrs'; +import { DatabaseModule } from '../database/database.module'; +import { MatcherController } from './adapters/primaries/matcher.controller'; +import { MatchProfile } from './mappers/match.profile'; +import { AdRepository } from './adapters/secondaries/ad.repository'; +import { MatchUseCase } from './domain/usecases/match.usecase'; +import { Messager } from './adapters/secondaries/messager'; + +@Module({ + imports: [ + DatabaseModule, + CqrsModule, + 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], + }), + ], + controllers: [MatcherController], + providers: [MatchProfile, AdRepository, Messager, MatchUseCase], + exports: [], +}) +export class MatcherModule {} diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts new file mode 100644 index 0000000..b0b304f --- /dev/null +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -0,0 +1,59 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Messager } from '../../adapters/secondaries/messager'; +import { MatchUseCase } from '../../domain/usecases/match.usecase'; +import { MatchRequest } from '../../domain/dtos/match.request'; +import { MatchQuery } from '../../queries/match.query'; +import { AdRepository } from '../../adapters/secondaries/ad.repository'; +import { AutomapperModule } from '@automapper/nestjs'; +import { classes } from '@automapper/classes'; + +const mockAdRepository = {}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('MatchUseCase', () => { + let matchUseCase: MatchUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AdRepository, + useValue: mockAdRepository, + }, + { + provide: Messager, + useValue: mockMessager, + }, + MatchUseCase, + ], + }).compile(); + + matchUseCase = module.get(MatchUseCase); + }); + + it('should be defined', () => { + expect(matchUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should return matches', async () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.waypoints = [ + { + lon: 1.093912, + lat: 49.440041, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + const matches = await matchUseCase.execute(new MatchQuery(matchRequest)); + expect(matches.total).toBe(1); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/messager.spec.ts b/src/modules/matcher/tests/unit/messager.spec.ts new file mode 100644 index 0000000..0331332 --- /dev/null +++ b/src/modules/matcher/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/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts b/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts new file mode 100644 index 0000000..911466c --- /dev/null +++ b/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts @@ -0,0 +1,20 @@ +import { ArgumentMetadata } from '@nestjs/common'; +import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe'; +import { MatchRequest } from '../../../matcher/domain/dtos/match.request'; + +describe('RpcValidationPipe', () => { + it('should not validate request', async () => { + const target: RpcValidationPipe = new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }); + const metadata: ArgumentMetadata = { + type: 'body', + metatype: MatchRequest, + data: '', + }; + await target.transform({}, metadata).catch((err) => { + expect(err.message).toEqual('Rpc Exception'); + }); + }); +}); From f3ca813dd6abf7ea79cb13c1b43756593a7ae0a8 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 6 Apr 2023 17:16:08 +0200 Subject: [PATCH 03/26] add cachemodule --- package-lock.json | 13 +++++++++++++ package.json | 1 + src/modules/matcher/matcher.module.ts | 15 +++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/package-lock.json b/package-lock.json index adb8fd8..3512f8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@nestjs/terminus": "^9.2.2", "@prisma/client": "^4.12.0", "cache-manager": "^5.2.0", + "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "ioredis": "^5.3.1", @@ -3413,6 +3414,18 @@ "lru-cache": "~7.18.3" } }, + "node_modules/cache-manager-ioredis-yet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cache-manager-ioredis-yet/-/cache-manager-ioredis-yet-1.1.0.tgz", + "integrity": "sha512-bGBAq8oNzzNkO2dwlYGWBxNXrz4w8FUTpe3nfUydJ6bm1ixKEcSUKYksGokQMaRgqkQjMbIHWFkvb8p+V9ZKqw==", + "dependencies": { + "cache-manager": "^5.1.0", + "ioredis": "^5.2.3" + }, + "engines": { + "node": ">= 16.17.0" + } + }, "node_modules/cache-manager/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", diff --git a/package.json b/package.json index ce6ad16..5919315 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@nestjs/terminus": "^9.2.2", "@prisma/client": "^4.12.0", "cache-manager": "^5.2.0", + "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "ioredis": "^5.3.1", diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index 8071f27..ef74b38 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -8,6 +8,9 @@ import { MatchProfile } from './mappers/match.profile'; import { AdRepository } from './adapters/secondaries/ad.repository'; import { MatchUseCase } from './domain/usecases/match.usecase'; import { Messager } from './adapters/secondaries/messager'; +import { CacheModule } from '@nestjs/cache-manager'; +import { RedisClientOptions } from '@liaoliaots/nestjs-redis'; +import { redisStore } from 'cache-manager-ioredis-yet'; @Module({ imports: [ @@ -27,6 +30,18 @@ import { Messager } from './adapters/secondaries/messager'; }), inject: [ConfigService], }), + CacheModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + store: await redisStore({ + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), + password: configService.get('REDIS_PASSWORD'), + ttl: configService.get('CACHE_TTL'), + }), + }), + inject: [ConfigService], + }), ], controllers: [MatcherController], providers: [MatchProfile, AdRepository, Messager, MatchUseCase], From d6a12e4d0ebc746a179ba992a4f9a1911de96507 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 7 Apr 2023 18:01:57 +0200 Subject: [PATCH 04/26] WIP create query --- .../adapters/primaries/matcher.controller.ts | 29 +++++-- .../matcher/adapters/primaries/matcher.proto | 45 ++++++++++ .../margin-durations.type.ts} | 0 .../matcher/domain/dtos/match.request.ts | 23 ++++-- .../{entities => dtos}/schedule.type.ts | 0 .../matcher/domain/entities/geography.ts | 11 +++ src/modules/matcher/domain/entities/person.ts | 42 ++++++++++ .../matcher/domain/entities/requirement.ts | 4 + .../domain/{dtos => entities}/role.enum.ts | 0 src/modules/matcher/domain/entities/route.ts | 1 + .../matcher/domain/entities/settings.ts | 14 ++++ src/modules/matcher/domain/entities/time.ts | 50 +++++++++++ src/modules/matcher/domain/entities/timing.ts | 14 ++++ .../domain/interfaces/georouter.interface.ts | 3 + .../matcher/exceptions/matcher.exception.ts | 13 +++ src/modules/matcher/queries/match.query.ts | 82 ++++++++++++++++++- 16 files changed, 310 insertions(+), 21 deletions(-) rename src/modules/matcher/domain/{entities/margin_durations.type.ts => dtos/margin-durations.type.ts} (100%) rename src/modules/matcher/domain/{entities => dtos}/schedule.type.ts (100%) create mode 100644 src/modules/matcher/domain/entities/geography.ts create mode 100644 src/modules/matcher/domain/entities/person.ts create mode 100644 src/modules/matcher/domain/entities/requirement.ts rename src/modules/matcher/domain/{dtos => entities}/role.enum.ts (100%) create mode 100644 src/modules/matcher/domain/entities/route.ts create mode 100644 src/modules/matcher/domain/entities/settings.ts create mode 100644 src/modules/matcher/domain/entities/time.ts create mode 100644 src/modules/matcher/domain/entities/timing.ts create mode 100644 src/modules/matcher/domain/interfaces/georouter.interface.ts create mode 100644 src/modules/matcher/exceptions/matcher.exception.ts diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index a7619fb..1a837a9 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -2,17 +2,18 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Controller, UsePipes } from '@nestjs/common'; import { QueryBus } from '@nestjs/cqrs'; -import { GrpcMethod } from '@nestjs/microservices'; +import { GrpcMethod, RpcException } 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'; +import { ConfigService } from '@nestjs/config'; @UsePipes( new RpcValidationPipe({ - whitelist: true, + whitelist: false, forbidUnknownValues: false, }), ) @@ -20,17 +21,27 @@ import { MatchPresenter } from '../secondaries/match.presenter'; export class MatcherController { constructor( private readonly _queryBus: QueryBus, + private readonly _configService: ConfigService, @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, - }); + try { + const matchCollection = await this._queryBus.execute( + new MatchQuery(data, this._configService), + ); + return Promise.resolve({ + data: matchCollection.data.map((match: Match) => + this._mapper.map(match, Match, MatchPresenter), + ), + total: matchCollection.total, + }); + } catch (e) { + throw new RpcException({ + code: e.code, + message: e.message, + }); + } } } diff --git a/src/modules/matcher/adapters/primaries/matcher.proto b/src/modules/matcher/adapters/primaries/matcher.proto index fa642d5..f610b21 100644 --- a/src/modules/matcher/adapters/primaries/matcher.proto +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -8,6 +8,27 @@ service MatcherService { message MatchRequest { repeated Point waypoints = 1; + string departure = 2; + string fromDate = 3; + Schedule schedule = 4; + bool driver = 5; + bool passenger = 6; + string toDate = 7; + int32 marginDuration = 8; + MarginDurations marginDurations = 9; + int32 seatsPassenger = 10; + int32 seatsDriver = 11; + bool strict = 12; + Algorithm algorithm = 13; + int32 remoteness = 14; + bool useProportion = 15; + int32 proportion = 16; + bool useAzimuth = 17; + int32 azimuthMargin = 18; + int32 maxDetourDistanceRatio = 19; + int32 maxDetourDurationRatio = 20; + repeated int32 exclusions = 21; + int32 identifier = 22; } message Point { @@ -15,6 +36,30 @@ message Point { float lat = 2; } +message Schedule { + string mon = 1; + string tue = 2; + string wed = 3; + string thu = 4; + string fri = 5; + string sat = 6; + string sun = 7; +} + +message MarginDurations { + int32 mon = 1; + int32 tue = 2; + int32 wed = 3; + int32 thu = 4; + int32 fri = 5; + int32 sat = 6; + int32 sun = 7; +} + +enum Algorithm { + CLASSIC = 0; +} + message Match { string uuid = 1; } diff --git a/src/modules/matcher/domain/entities/margin_durations.type.ts b/src/modules/matcher/domain/dtos/margin-durations.type.ts similarity index 100% rename from src/modules/matcher/domain/entities/margin_durations.type.ts rename to src/modules/matcher/domain/dtos/margin-durations.type.ts diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 7384689..bbdd274 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -1,18 +1,18 @@ import { IsArray, IsBoolean, - IsDate, IsEnum, IsInt, IsNumber, IsOptional, + IsString, 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 { Schedule } from './schedule.type'; +import { MarginDurations } from './margin-durations.type'; import { Algorithm } from './algorithm.enum'; export class MatchRequest { @@ -20,15 +20,15 @@ export class MatchRequest { @AutoMap() waypoints: Array; - @IsDate() @IsOptional() + @IsString() @AutoMap() - departure: Date; + departure: number; - @IsDate() @IsOptional() + @IsInt() @AutoMap() - fromDate: Date; + fromDate: number; @IsOptional() @AutoMap() @@ -45,9 +45,9 @@ export class MatchRequest { passenger: boolean; @IsOptional() - @IsDate() + @IsInt() @AutoMap() - toDate: Date; + toDate: number; @IsOptional() @IsInt() @@ -123,4 +123,9 @@ export class MatchRequest { @IsOptional() @IsArray() exclusions: Array; + + @IsOptional() + @IsInt() + @AutoMap() + identifier: number; } diff --git a/src/modules/matcher/domain/entities/schedule.type.ts b/src/modules/matcher/domain/dtos/schedule.type.ts similarity index 100% rename from src/modules/matcher/domain/entities/schedule.type.ts rename to src/modules/matcher/domain/dtos/schedule.type.ts diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/geography.ts new file mode 100644 index 0000000..4cc5007 --- /dev/null +++ b/src/modules/matcher/domain/entities/geography.ts @@ -0,0 +1,11 @@ +import { Point } from './point.type'; +import { Route } from './route'; + +export class Geography { + waypoints: Array; + originType: number; + destinationType: number; + timezone: string; + driverRoute: Route; + passengerRoute: Route; +} diff --git a/src/modules/matcher/domain/entities/person.ts b/src/modules/matcher/domain/entities/person.ts new file mode 100644 index 0000000..37b2add --- /dev/null +++ b/src/modules/matcher/domain/entities/person.ts @@ -0,0 +1,42 @@ +import { MatchRequest } from '../dtos/match.request'; + +export class Person { + _matchRequest: MatchRequest; + _defaultIdentifier: number; + _defaultMarginDuration: number; + identifier: number; + marginDurations: Array; + + constructor( + matchRequest: MatchRequest, + defaultIdentifier: number, + defaultMarginDuration: number, + ) { + this._matchRequest = matchRequest; + this._defaultIdentifier = defaultIdentifier; + this._defaultMarginDuration = defaultMarginDuration; + } + + init() { + this.setIdentifier( + this._matchRequest.identifier ?? this._defaultIdentifier, + ); + this.setMarginDurations([ + this._defaultMarginDuration, + this._defaultMarginDuration, + this._defaultMarginDuration, + this._defaultMarginDuration, + this._defaultMarginDuration, + this._defaultMarginDuration, + this._defaultMarginDuration, + ]); + } + + setIdentifier(identifier: number) { + this.identifier = identifier; + } + + setMarginDurations(marginDurations: Array) { + this.marginDurations = marginDurations; + } +} diff --git a/src/modules/matcher/domain/entities/requirement.ts b/src/modules/matcher/domain/entities/requirement.ts new file mode 100644 index 0000000..b9c9b7c --- /dev/null +++ b/src/modules/matcher/domain/entities/requirement.ts @@ -0,0 +1,4 @@ +export class Requirement { + seatsDriver: number; + seatsPassenger: number; +} diff --git a/src/modules/matcher/domain/dtos/role.enum.ts b/src/modules/matcher/domain/entities/role.enum.ts similarity index 100% rename from src/modules/matcher/domain/dtos/role.enum.ts rename to src/modules/matcher/domain/entities/role.enum.ts diff --git a/src/modules/matcher/domain/entities/route.ts b/src/modules/matcher/domain/entities/route.ts new file mode 100644 index 0000000..bf729c9 --- /dev/null +++ b/src/modules/matcher/domain/entities/route.ts @@ -0,0 +1 @@ +export class Route {} diff --git a/src/modules/matcher/domain/entities/settings.ts b/src/modules/matcher/domain/entities/settings.ts new file mode 100644 index 0000000..14d53b8 --- /dev/null +++ b/src/modules/matcher/domain/entities/settings.ts @@ -0,0 +1,14 @@ +import { Georouter } from '../interfaces/georouter.interface'; + +export class Settings { + algorithm: Algorithm; + restrict: boolean; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDurationRatio: number; + maxDetourDistanceRatio: number; + georouter: Georouter; +} diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts new file mode 100644 index 0000000..36ae203 --- /dev/null +++ b/src/modules/matcher/domain/entities/time.ts @@ -0,0 +1,50 @@ +import { MatcherException } from '../../exceptions/matcher.exception'; +import { MatchRequest } from '../dtos/match.request'; +import { TimingFrequency } from './timing'; + +export class Time { + _matchRequest: MatchRequest; + _defaultMarginDuration: number; + _defaultValidityDuration: number; + frequency: TimingFrequency; + fromDate: Date; + toDate: Date; + schedule: Array; + marginDurations: Array; + + constructor( + matchRequest: MatchRequest, + defaultMarginDuration: number, + defaultValidityDuration: number, + ) { + this._matchRequest = matchRequest; + this._defaultMarginDuration = defaultMarginDuration; + this._defaultValidityDuration = defaultValidityDuration; + } + + init() { + this._validatePunctualDate(); + this._validateRecurrentDate(); + } + + _validatePunctualDate() { + if (!this._matchRequest.departure && !this._matchRequest.fromDate) { + throw new MatcherException(3, 'departure or fromDate is Required'); + } + if (this._matchRequest.departure) { + this.fromDate = new Date(this._matchRequest.departure); + if (!this._isDate(this.fromDate)) { + throw new MatcherException(3, 'Wrong departure date'); + } + console.log(this.fromDate); + } + } + + _validateRecurrentDate() { + console.log('validate recurrent date'); + } + + _isDate(date: Date) { + return date instanceof Date && isFinite(+date); + } +} diff --git a/src/modules/matcher/domain/entities/timing.ts b/src/modules/matcher/domain/entities/timing.ts new file mode 100644 index 0000000..2efe5b2 --- /dev/null +++ b/src/modules/matcher/domain/entities/timing.ts @@ -0,0 +1,14 @@ +export enum TimingFrequency { + FREQUENCY_PUNCTUAL = 1, + FREQUENCY_RECURRENT = 2, +} + +export enum TimingDays { + 'mon', + 'tue', + 'wed', + 'thu', + 'fri', + 'sat', + 'sun', +} diff --git a/src/modules/matcher/domain/interfaces/georouter.interface.ts b/src/modules/matcher/domain/interfaces/georouter.interface.ts new file mode 100644 index 0000000..e28617d --- /dev/null +++ b/src/modules/matcher/domain/interfaces/georouter.interface.ts @@ -0,0 +1,3 @@ +export interface Georouter { + type: string; +} diff --git a/src/modules/matcher/exceptions/matcher.exception.ts b/src/modules/matcher/exceptions/matcher.exception.ts new file mode 100644 index 0000000..c72c694 --- /dev/null +++ b/src/modules/matcher/exceptions/matcher.exception.ts @@ -0,0 +1,13 @@ +export class MatcherException implements Error { + name: string; + message: string; + + constructor(private _code: number, private _message: string) { + this.name = 'MatcherException'; + this.message = _message; + } + + get code(): number { + return this._code; + } +} diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 53ac933..4d26d19 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -1,9 +1,85 @@ +import { ConfigService } from '@nestjs/config'; import { MatchRequest } from '../domain/dtos/match.request'; +import { Geography } from '../domain/entities/geography'; +import { Person } from '../domain/entities/person'; +import { Requirement } from '../domain/entities/requirement'; +import { Role } from '../domain/entities/role.enum'; +import { Settings } from '../domain/entities/settings'; +import { Time } from '../domain/entities/time'; +import { MatcherException } from '../exceptions/matcher.exception'; export class MatchQuery { - matchRequest: MatchRequest; + private readonly _matchRequest: MatchRequest; + private readonly _configService: ConfigService; + person: Person; + exclusions: Array; + time: Time; + geography: Geography; + roles: Array; + requirement: Requirement; + settings: Settings; - constructor(matchRequest?: MatchRequest) { - this.matchRequest = matchRequest; + constructor(matchRequest: MatchRequest, configService: ConfigService) { + this._matchRequest = matchRequest; + this._configService = configService; + this._validate(); + this._initialize(); + this._setPerson(); + this._setExclusions(); + this._setRoles(); + this._setTime(); + // console.log(this); + } + + _validate() { + if (!this._matchRequest.departure) { + if (!this._matchRequest.fromDate) + throw new MatcherException(3, 'departure or fromDate is Required'); + if (!this._matchRequest.schedule) + throw new MatcherException(3, 'schedule is Required'); + } + } + + _initialize() { + if ( + this._matchRequest.driver === undefined && + this._matchRequest.passenger === undefined + ) + this._matchRequest.passenger = true; + this.geography = new Geography(); + this.requirement = new Requirement(); + this.settings = new Settings(); + } + + _setPerson() { + this.person = new Person( + this._matchRequest, + this._configService.get('DEFAULT_IDENTIFIER'), + this._configService.get('MARGIN_DURATION'), + ); + this.person.init(); + } + + _setExclusions() { + this.exclusions = []; + if (this._matchRequest.identifier) + this.exclusions.push(this._matchRequest.identifier); + if (this._matchRequest.exclusions) + this.exclusions.push(...this._matchRequest.exclusions); + } + + _setRoles() { + this.roles = []; + if (this._matchRequest.driver) this.roles.push(Role.DRIVER); + if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER); + } + + _setTime() { + this.time = new Time( + this._matchRequest, + this._configService.get('MARGIN_DURATION'), + this._configService.get('VALIDITY_DURATION'), + ); + this.time.init(); } } From 8ab1ddbebc4901aa158c8e111a7df51507bb8987 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 11 Apr 2023 10:13:15 +0200 Subject: [PATCH 05/26] use interfaces for requests --- package.json | 1 + .../adapters/primaries/matcher.controller.ts | 6 +-- .../secondaries/default-params.provider.ts | 16 ++++++++ .../adapters/secondaries/message-broker.ts | 2 +- .../matcher/adapters/secondaries/messager.ts | 4 +- .../matcher/domain/dtos/match.request.ts | 13 +++++-- src/modules/matcher/domain/entities/person.ts | 10 ++--- src/modules/matcher/domain/entities/time.ts | 31 +++++++++------ .../interfaces/default-params.interface.ts | 5 +++ .../interfaces/person-request.interface.ts | 3 ++ .../interfaces/role-request.interface.ts | 4 ++ .../interfaces/time-request.interface.ts | 7 ++++ src/modules/matcher/matcher.module.ts | 9 ++++- src/modules/matcher/queries/match.query.ts | 27 ++++--------- .../unit/default-params.provider.spec.ts | 38 +++++++++++++++++++ .../matcher/tests/unit/match.usecase.spec.ts | 12 +++++- 16 files changed, 141 insertions(+), 47 deletions(-) create mode 100644 src/modules/matcher/adapters/secondaries/default-params.provider.ts create mode 100644 src/modules/matcher/domain/interfaces/default-params.interface.ts create mode 100644 src/modules/matcher/domain/interfaces/person-request.interface.ts create mode 100644 src/modules/matcher/domain/interfaces/role-request.interface.ts create mode 100644 src/modules/matcher/domain/interfaces/time-request.interface.ts create mode 100644 src/modules/matcher/tests/unit/default-params.provider.spec.ts diff --git a/package.json b/package.json index 5919315..7df44c8 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ ".controller.ts", ".module.ts", ".request.ts", + ".presenter.ts", "main.ts" ], "rootDir": "src", diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 1a837a9..7e001d5 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -9,7 +9,7 @@ import { ICollection } from 'src/modules/database/src/interfaces/collection.inte import { Match } from '../../domain/entities/match'; import { MatchQuery } from '../../queries/match.query'; import { MatchPresenter } from '../secondaries/match.presenter'; -import { ConfigService } from '@nestjs/config'; +import { DefaultParamsProvider } from '../secondaries/default-params.provider'; @UsePipes( new RpcValidationPipe({ @@ -21,7 +21,7 @@ import { ConfigService } from '@nestjs/config'; export class MatcherController { constructor( private readonly _queryBus: QueryBus, - private readonly _configService: ConfigService, + private readonly _defaultParamsProvider: DefaultParamsProvider, @InjectMapper() private readonly _mapper: Mapper, ) {} @@ -29,7 +29,7 @@ export class MatcherController { async match(data: MatchRequest): Promise> { try { const matchCollection = await this._queryBus.execute( - new MatchQuery(data, this._configService), + new MatchQuery(data, this._defaultParamsProvider.getParams()), ); return Promise.resolve({ data: matchCollection.data.map((match: Match) => diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts new file mode 100644 index 0000000..d113c4a --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; + +@Injectable() +export class DefaultParamsProvider { + constructor(private readonly configService: ConfigService) {} + + getParams(): IDefaultParams { + return { + DEFAULT_IDENTIFIER: this.configService.get('DEFAULT_IDENTIFIER'), + MARGIN_DURATION: this.configService.get('MARGIN_DURATION'), + VALIDITY_DURATION: this.configService.get('VALIDITY_DURATION'), + }; + } +} diff --git a/src/modules/matcher/adapters/secondaries/message-broker.ts b/src/modules/matcher/adapters/secondaries/message-broker.ts index 594aa43..7b4f4df 100644 --- a/src/modules/matcher/adapters/secondaries/message-broker.ts +++ b/src/modules/matcher/adapters/secondaries/message-broker.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export abstract class IMessageBroker { +export abstract class MessageBroker { exchange: string; constructor(exchange: string) { diff --git a/src/modules/matcher/adapters/secondaries/messager.ts b/src/modules/matcher/adapters/secondaries/messager.ts index 0725261..e808bcf 100644 --- a/src/modules/matcher/adapters/secondaries/messager.ts +++ b/src/modules/matcher/adapters/secondaries/messager.ts @@ -1,10 +1,10 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { IMessageBroker } from './message-broker'; +import { MessageBroker } from './message-broker'; @Injectable() -export class Messager extends IMessageBroker { +export class Messager extends MessageBroker { constructor( private readonly _amqpConnection: AmqpConnection, configService: ConfigService, diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index bbdd274..3052a7e 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -14,8 +14,13 @@ import { Point } from '../entities/point.type'; import { Schedule } from './schedule.type'; import { MarginDurations } from './margin-durations.type'; import { Algorithm } from './algorithm.enum'; +import { IRequestTime } from '../interfaces/time-request.interface'; +import { IRequestRole } from '../interfaces/role-request.interface'; +import { IRequestPerson } from '../interfaces/person-request.interface'; -export class MatchRequest { +export class MatchRequest + implements IRequestTime, IRequestRole, IRequestPerson +{ @IsArray() @AutoMap() waypoints: Array; @@ -23,12 +28,12 @@ export class MatchRequest { @IsOptional() @IsString() @AutoMap() - departure: number; + departure: string; @IsOptional() - @IsInt() + @IsString() @AutoMap() - fromDate: number; + fromDate: string; @IsOptional() @AutoMap() diff --git a/src/modules/matcher/domain/entities/person.ts b/src/modules/matcher/domain/entities/person.ts index 37b2add..40349f1 100644 --- a/src/modules/matcher/domain/entities/person.ts +++ b/src/modules/matcher/domain/entities/person.ts @@ -1,25 +1,25 @@ -import { MatchRequest } from '../dtos/match.request'; +import { IRequestPerson } from '../interfaces/person-request.interface'; export class Person { - _matchRequest: MatchRequest; + _personRequest: IRequestPerson; _defaultIdentifier: number; _defaultMarginDuration: number; identifier: number; marginDurations: Array; constructor( - matchRequest: MatchRequest, + personRequest: IRequestPerson, defaultIdentifier: number, defaultMarginDuration: number, ) { - this._matchRequest = matchRequest; + this._personRequest = personRequest; this._defaultIdentifier = defaultIdentifier; this._defaultMarginDuration = defaultMarginDuration; } init() { this.setIdentifier( - this._matchRequest.identifier ?? this._defaultIdentifier, + this._personRequest.identifier ?? this._defaultIdentifier, ); this.setMarginDurations([ this._defaultMarginDuration, diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index 36ae203..f29c5e0 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -1,9 +1,9 @@ import { MatcherException } from '../../exceptions/matcher.exception'; -import { MatchRequest } from '../dtos/match.request'; +import { IRequestTime } from '../interfaces/time-request.interface'; import { TimingFrequency } from './timing'; export class Time { - _matchRequest: MatchRequest; + _timeRequest: IRequestTime; _defaultMarginDuration: number; _defaultValidityDuration: number; frequency: TimingFrequency; @@ -13,35 +13,44 @@ export class Time { marginDurations: Array; constructor( - matchRequest: MatchRequest, + timeRequest: IRequestTime, defaultMarginDuration: number, defaultValidityDuration: number, ) { - this._matchRequest = matchRequest; + this._timeRequest = timeRequest; this._defaultMarginDuration = defaultMarginDuration; this._defaultValidityDuration = defaultValidityDuration; } init() { + this._validateBaseDate(); this._validatePunctualDate(); this._validateRecurrentDate(); + console.log(this.fromDate); + } + + _validateBaseDate() { + if (!this._timeRequest.departure && !this._timeRequest.fromDate) { + throw new MatcherException(3, 'departure or fromDate is required'); + } } _validatePunctualDate() { - if (!this._matchRequest.departure && !this._matchRequest.fromDate) { - throw new MatcherException(3, 'departure or fromDate is Required'); - } - if (this._matchRequest.departure) { - this.fromDate = new Date(this._matchRequest.departure); + if (this._timeRequest.departure) { + this.fromDate = new Date(this._timeRequest.departure); if (!this._isDate(this.fromDate)) { throw new MatcherException(3, 'Wrong departure date'); } - console.log(this.fromDate); } } _validateRecurrentDate() { - console.log('validate recurrent date'); + if (this._timeRequest.fromDate) { + this.fromDate = new Date(this._timeRequest.fromDate); + if (!this._isDate(this.fromDate)) { + throw new MatcherException(3, 'Wrong fromDate'); + } + } } _isDate(date: Date) { diff --git a/src/modules/matcher/domain/interfaces/default-params.interface.ts b/src/modules/matcher/domain/interfaces/default-params.interface.ts new file mode 100644 index 0000000..e5d5f2d --- /dev/null +++ b/src/modules/matcher/domain/interfaces/default-params.interface.ts @@ -0,0 +1,5 @@ +export interface IDefaultParams { + DEFAULT_IDENTIFIER: number; + MARGIN_DURATION: number; + VALIDITY_DURATION: number; +} diff --git a/src/modules/matcher/domain/interfaces/person-request.interface.ts b/src/modules/matcher/domain/interfaces/person-request.interface.ts new file mode 100644 index 0000000..75211f7 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/person-request.interface.ts @@ -0,0 +1,3 @@ +export interface IRequestPerson { + identifier: number; +} diff --git a/src/modules/matcher/domain/interfaces/role-request.interface.ts b/src/modules/matcher/domain/interfaces/role-request.interface.ts new file mode 100644 index 0000000..cc124b8 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/role-request.interface.ts @@ -0,0 +1,4 @@ +export interface IRequestRole { + driver: boolean; + passenger: boolean; +} diff --git a/src/modules/matcher/domain/interfaces/time-request.interface.ts b/src/modules/matcher/domain/interfaces/time-request.interface.ts new file mode 100644 index 0000000..44ee14e --- /dev/null +++ b/src/modules/matcher/domain/interfaces/time-request.interface.ts @@ -0,0 +1,7 @@ +import { Schedule } from '../dtos/schedule.type'; + +export interface IRequestTime { + departure: string; + fromDate: string; + schedule: Schedule; +} diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index ef74b38..2bb67a3 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -11,6 +11,7 @@ import { Messager } from './adapters/secondaries/messager'; import { CacheModule } from '@nestjs/cache-manager'; import { RedisClientOptions } from '@liaoliaots/nestjs-redis'; import { redisStore } from 'cache-manager-ioredis-yet'; +import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider'; @Module({ imports: [ @@ -44,7 +45,13 @@ import { redisStore } from 'cache-manager-ioredis-yet'; }), ], controllers: [MatcherController], - providers: [MatchProfile, AdRepository, Messager, MatchUseCase], + providers: [ + MatchProfile, + AdRepository, + Messager, + DefaultParamsProvider, + MatchUseCase, + ], exports: [], }) export class MatcherModule {} diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 4d26d19..e957080 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -1,4 +1,3 @@ -import { ConfigService } from '@nestjs/config'; import { MatchRequest } from '../domain/dtos/match.request'; import { Geography } from '../domain/entities/geography'; import { Person } from '../domain/entities/person'; @@ -6,11 +5,11 @@ import { Requirement } from '../domain/entities/requirement'; import { Role } from '../domain/entities/role.enum'; import { Settings } from '../domain/entities/settings'; import { Time } from '../domain/entities/time'; -import { MatcherException } from '../exceptions/matcher.exception'; +import { IDefaultParams } from '../domain/interfaces/default-params.interface'; export class MatchQuery { private readonly _matchRequest: MatchRequest; - private readonly _configService: ConfigService; + private readonly _defaultParams: IDefaultParams; person: Person; exclusions: Array; time: Time; @@ -19,10 +18,9 @@ export class MatchQuery { requirement: Requirement; settings: Settings; - constructor(matchRequest: MatchRequest, configService: ConfigService) { + constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { this._matchRequest = matchRequest; - this._configService = configService; - this._validate(); + this._defaultParams = defaultParams; this._initialize(); this._setPerson(); this._setExclusions(); @@ -31,15 +29,6 @@ export class MatchQuery { // console.log(this); } - _validate() { - if (!this._matchRequest.departure) { - if (!this._matchRequest.fromDate) - throw new MatcherException(3, 'departure or fromDate is Required'); - if (!this._matchRequest.schedule) - throw new MatcherException(3, 'schedule is Required'); - } - } - _initialize() { if ( this._matchRequest.driver === undefined && @@ -54,8 +43,8 @@ export class MatchQuery { _setPerson() { this.person = new Person( this._matchRequest, - this._configService.get('DEFAULT_IDENTIFIER'), - this._configService.get('MARGIN_DURATION'), + this._defaultParams.DEFAULT_IDENTIFIER, + this._defaultParams.MARGIN_DURATION, ); this.person.init(); } @@ -77,8 +66,8 @@ export class MatchQuery { _setTime() { this.time = new Time( this._matchRequest, - this._configService.get('MARGIN_DURATION'), - this._configService.get('VALIDITY_DURATION'), + this._defaultParams.MARGIN_DURATION, + this._defaultParams.VALIDITY_DURATION, ); this.time.init(); } diff --git a/src/modules/matcher/tests/unit/default-params.provider.spec.ts b/src/modules/matcher/tests/unit/default-params.provider.spec.ts new file mode 100644 index 0000000..49cf3ca --- /dev/null +++ b/src/modules/matcher/tests/unit/default-params.provider.spec.ts @@ -0,0 +1,38 @@ +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DefaultParamsProvider } from '../../adapters/secondaries/default-params.provider'; +import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; + +const mockConfigService = { + get: jest.fn().mockImplementationOnce(() => 99), +}; + +describe('DefaultParamsProvider', () => { + let defaultParamsProvider: DefaultParamsProvider; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + DefaultParamsProvider, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + defaultParamsProvider = module.get( + DefaultParamsProvider, + ); + }); + + it('should be defined', () => { + expect(defaultParamsProvider).toBeDefined(); + }); + + it('should provide default params', async () => { + const params: IDefaultParams = defaultParamsProvider.getParams(); + expect(params.DEFAULT_IDENTIFIER).toBe(99); + }); +}); diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index b0b304f..3fa3f92 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -6,6 +6,7 @@ import { MatchQuery } from '../../queries/match.query'; import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; +import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; const mockAdRepository = {}; @@ -13,6 +14,12 @@ const mockMessager = { publish: jest.fn().mockImplementation(), }; +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, +}; + describe('MatchUseCase', () => { let matchUseCase: MatchUseCase; @@ -52,7 +59,10 @@ describe('MatchUseCase', () => { lon: 3.045432, }, ]; - const matches = await matchUseCase.execute(new MatchQuery(matchRequest)); + matchRequest.departure = '2023-04-01 12:23:00'; + const matches = await matchUseCase.execute( + new MatchQuery(matchRequest, defaultParams), + ); expect(matches.total).toBe(1); }); }); From 2ffa40aa53d568155a022065af56178900f2b860 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 11 Apr 2023 10:48:19 +0200 Subject: [PATCH 06/26] test time entity --- src/modules/matcher/domain/entities/time.ts | 1 - .../interfaces/time-request.interface.ts | 6 +- src/modules/matcher/tests/unit/time.spec.ts | 79 +++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/modules/matcher/tests/unit/time.spec.ts diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index f29c5e0..45b5b09 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -26,7 +26,6 @@ export class Time { this._validateBaseDate(); this._validatePunctualDate(); this._validateRecurrentDate(); - console.log(this.fromDate); } _validateBaseDate() { diff --git a/src/modules/matcher/domain/interfaces/time-request.interface.ts b/src/modules/matcher/domain/interfaces/time-request.interface.ts index 44ee14e..6425441 100644 --- a/src/modules/matcher/domain/interfaces/time-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/time-request.interface.ts @@ -1,7 +1,7 @@ import { Schedule } from '../dtos/schedule.type'; export interface IRequestTime { - departure: string; - fromDate: string; - schedule: Schedule; + departure?: string; + fromDate?: string; + schedule?: Schedule; } diff --git a/src/modules/matcher/tests/unit/time.spec.ts b/src/modules/matcher/tests/unit/time.spec.ts new file mode 100644 index 0000000..b19ce8a --- /dev/null +++ b/src/modules/matcher/tests/unit/time.spec.ts @@ -0,0 +1,79 @@ +import { Time } from '../../domain/entities/time'; +import { IRequestTime } from '../../domain/interfaces/time-request.interface'; + +const MARGIN_DURATION = 900; +const VALIDITY_DURATION = 365; + +const punctualTimeRequest: IRequestTime = { + departure: '2023-04-01 12:24:00', +}; + +const invalidPunctualTimeRequest: IRequestTime = { + departure: '2023-15-01 12:24:00', +}; + +const recurrentTimeRequest: IRequestTime = { + fromDate: '2023-04-01', +}; + +const invalidRecurrentTimeRequest: IRequestTime = { + fromDate: '2023-15-01', +}; + +const expectedPunctualFromDate = new Date(punctualTimeRequest.departure); + +describe('Time entity', () => { + it('should be defined', () => { + const time = new Time( + punctualTimeRequest, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(time).toBeDefined(); + }); + + describe('init', () => { + it('should initialize a punctual time request', () => { + const time = new Time( + punctualTimeRequest, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.fromDate.getFullYear()).toBe( + expectedPunctualFromDate.getFullYear(), + ); + }); + it('should initialize a recurrent time request', () => { + const time = new Time( + recurrentTimeRequest, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.fromDate.getFullYear()).toBe( + expectedPunctualFromDate.getFullYear(), + ); + }); + it('should throw an exception if no date is provided', () => { + const time = new Time({}, MARGIN_DURATION, VALIDITY_DURATION); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if punctual date is invalid', () => { + const time = new Time( + invalidPunctualTimeRequest, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if recuurent date is invalid', () => { + const time = new Time( + invalidRecurrentTimeRequest, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + }); +}); From 56bdd11970def121417036338c2912cf4dfddac0 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 11 Apr 2023 15:07:38 +0200 Subject: [PATCH 07/26] time request --- .../secondaries/default-params.provider.ts | 10 +- .../domain/dtos/margin-durations.type.ts | 9 -- .../matcher/domain/dtos/match.request.ts | 13 +- .../matcher/domain/dtos/schedule.type.ts | 9 -- .../domain/entities/margin-durations.type.ts | 9 ++ .../matcher/domain/entities/schedule.type.ts | 9 ++ src/modules/matcher/domain/entities/time.ts | 130 +++++++++++++-- src/modules/matcher/domain/entities/timing.ts | 2 +- ...ms.interface.ts => default-params.type.ts} | 4 +- .../interfaces/person-request.interface.ts | 2 +- .../interfaces/role-request.interface.ts | 4 - .../interfaces/time-request.interface.ts | 6 +- src/modules/matcher/queries/match.query.ts | 35 ++-- .../unit/default-params.provider.spec.ts | 2 +- .../matcher/tests/unit/match.usecase.spec.ts | 2 +- src/modules/matcher/tests/unit/person.spec.ts | 40 +++++ src/modules/matcher/tests/unit/time.spec.ts | 150 ++++++++++++++---- 17 files changed, 338 insertions(+), 98 deletions(-) delete mode 100644 src/modules/matcher/domain/dtos/margin-durations.type.ts delete mode 100644 src/modules/matcher/domain/dtos/schedule.type.ts create mode 100644 src/modules/matcher/domain/entities/margin-durations.type.ts create mode 100644 src/modules/matcher/domain/entities/schedule.type.ts rename src/modules/matcher/domain/interfaces/{default-params.interface.ts => default-params.type.ts} (70%) delete mode 100644 src/modules/matcher/domain/interfaces/role-request.interface.ts create mode 100644 src/modules/matcher/tests/unit/person.spec.ts diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index d113c4a..e8ce169 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../../domain/interfaces/default-params.type'; @Injectable() export class DefaultParamsProvider { @@ -8,9 +8,11 @@ export class DefaultParamsProvider { getParams(): IDefaultParams { return { - DEFAULT_IDENTIFIER: this.configService.get('DEFAULT_IDENTIFIER'), - MARGIN_DURATION: this.configService.get('MARGIN_DURATION'), - VALIDITY_DURATION: this.configService.get('VALIDITY_DURATION'), + DEFAULT_IDENTIFIER: parseInt( + this.configService.get('DEFAULT_IDENTIFIER'), + ), + MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')), + VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')), }; } } diff --git a/src/modules/matcher/domain/dtos/margin-durations.type.ts b/src/modules/matcher/domain/dtos/margin-durations.type.ts deleted file mode 100644 index 720f392..0000000 --- a/src/modules/matcher/domain/dtos/margin-durations.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type MarginDurations = { - mon: number; - tue: number; - wed: number; - thu: number; - fri: number; - sat: number; - sun: number; -}; diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 3052a7e..3a9d736 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -11,16 +11,13 @@ import { } from 'class-validator'; import { AutoMap } from '@automapper/classes'; import { Point } from '../entities/point.type'; -import { Schedule } from './schedule.type'; -import { MarginDurations } from './margin-durations.type'; +import { Schedule } from '../entities/schedule.type'; +import { MarginDurations } from '../entities/margin-durations.type'; import { Algorithm } from './algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; -import { IRequestRole } from '../interfaces/role-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface'; -export class MatchRequest - implements IRequestTime, IRequestRole, IRequestPerson -{ +export class MatchRequest implements IRequestTime, IRequestPerson { @IsArray() @AutoMap() waypoints: Array; @@ -50,9 +47,9 @@ export class MatchRequest passenger: boolean; @IsOptional() - @IsInt() + @IsString() @AutoMap() - toDate: number; + toDate: string; @IsOptional() @IsInt() diff --git a/src/modules/matcher/domain/dtos/schedule.type.ts b/src/modules/matcher/domain/dtos/schedule.type.ts deleted file mode 100644 index 8ee0874..0000000 --- a/src/modules/matcher/domain/dtos/schedule.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Schedule = { - mon: string; - tue: string; - wed: string; - thu: string; - fri: string; - sat: string; - sun: 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..8e09329 --- /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/schedule.type.ts b/src/modules/matcher/domain/entities/schedule.type.ts new file mode 100644 index 0000000..03f8485 --- /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/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index 45b5b09..63f8deb 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -1,6 +1,10 @@ import { MatcherException } from '../../exceptions/matcher.exception'; +import { MarginDurations } from './margin-durations.type'; import { IRequestTime } from '../interfaces/time-request.interface'; -import { TimingFrequency } from './timing'; +import { TimingDays, TimingFrequency } from './timing'; +import { Schedule } from './schedule.type'; + +const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; export class Time { _timeRequest: IRequestTime; @@ -9,8 +13,8 @@ export class Time { frequency: TimingFrequency; fromDate: Date; toDate: Date; - schedule: Array; - marginDurations: Array; + schedule: Schedule; + marginDurations: MarginDurations; constructor( timeRequest: IRequestTime, @@ -20,12 +24,25 @@ export class Time { this._timeRequest = timeRequest; this._defaultMarginDuration = defaultMarginDuration; this._defaultValidityDuration = defaultValidityDuration; + this.schedule = {}; + this.marginDurations = { + mon: defaultMarginDuration, + tue: defaultMarginDuration, + wed: defaultMarginDuration, + thu: defaultMarginDuration, + fri: defaultMarginDuration, + sat: defaultMarginDuration, + sun: defaultMarginDuration, + }; } init() { this._validateBaseDate(); - this._validatePunctualDate(); - this._validateRecurrentDate(); + this._validatePunctualRequest(); + this._validateRecurrentRequest(); + this._setPunctualRequest(); + this._setRecurrentRequest(); + this._setMargindurations(); } _validateBaseDate() { @@ -34,25 +51,118 @@ export class Time { } } - _validatePunctualDate() { + _validatePunctualRequest() { if (this._timeRequest.departure) { - this.fromDate = new Date(this._timeRequest.departure); + this.fromDate = this.toDate = new Date(this._timeRequest.departure); if (!this._isDate(this.fromDate)) { throw new MatcherException(3, 'Wrong departure date'); } } } - _validateRecurrentDate() { + _validateRecurrentRequest() { if (this._timeRequest.fromDate) { this.fromDate = new Date(this._timeRequest.fromDate); if (!this._isDate(this.fromDate)) { throw new MatcherException(3, 'Wrong fromDate'); } } + if (this._timeRequest.toDate) { + this.toDate = new Date(this._timeRequest.toDate); + if (!this._isDate(this.toDate)) { + throw new MatcherException(3, 'Wrong toDate'); + } + } + if (this._timeRequest.fromDate) { + this._validateSchedule(); + } } - _isDate(date: Date) { - return date instanceof Date && isFinite(+date); + _validateSchedule() { + if (!this._timeRequest.schedule) { + throw new MatcherException(3, 'Schedule is required'); + } + if ( + !Object.keys(this._timeRequest.schedule).some((elem) => + days.includes(elem), + ) + ) { + throw new MatcherException(3, 'No valid day in the given schedule'); + } + Object.keys(this._timeRequest.schedule).map((day) => { + const time = new Date('1970-01-01 ' + this._timeRequest.schedule[day]); + if (!this._isDate(time)) { + throw new MatcherException(3, `Wrong time for ${day} in schedule`); + } + }); } + + _setPunctualRequest() { + if (this._timeRequest.departure) { + this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; + this.schedule[TimingDays[this.fromDate.getDay()]] = + this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); + } + } + + _setRecurrentRequest() { + if (this._timeRequest.fromDate) { + this.frequency = TimingFrequency.FREQUENCY_RECURRENT; + if (!this.toDate) { + this.toDate = this._addDays( + this.fromDate, + this._defaultValidityDuration, + ); + } + this._setSchedule(); + } + } + + _setSchedule() { + Object.keys(this._timeRequest.schedule).map((day) => { + this.schedule[day] = this._timeRequest.schedule[day]; + }); + } + + _setMargindurations() { + if (this._timeRequest.marginDuration) { + const duration = Math.abs(this._timeRequest.marginDuration); + this.marginDurations = { + mon: duration, + tue: duration, + wed: duration, + thu: duration, + fri: duration, + sat: duration, + sun: duration, + }; + } + if (this._timeRequest.marginDurations) { + if ( + !Object.keys(this._timeRequest.marginDurations).some((elem) => + days.includes(elem), + ) + ) { + throw new MatcherException( + 3, + 'No valid day in the given margin durations', + ); + } + Object.keys(this._timeRequest.marginDurations).map((day) => { + this.marginDurations[day] = Math.abs( + this._timeRequest.marginDurations[day], + ); + }); + } + } + + _isDate = (date: Date): boolean => { + return date instanceof Date && isFinite(+date); + }; + + _addDays = (date: Date, days: number): Date => { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; + }; } diff --git a/src/modules/matcher/domain/entities/timing.ts b/src/modules/matcher/domain/entities/timing.ts index 2efe5b2..87196af 100644 --- a/src/modules/matcher/domain/entities/timing.ts +++ b/src/modules/matcher/domain/entities/timing.ts @@ -4,11 +4,11 @@ export enum TimingFrequency { } export enum TimingDays { + 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', - 'sun', } diff --git a/src/modules/matcher/domain/interfaces/default-params.interface.ts b/src/modules/matcher/domain/interfaces/default-params.type.ts similarity index 70% rename from src/modules/matcher/domain/interfaces/default-params.interface.ts rename to src/modules/matcher/domain/interfaces/default-params.type.ts index e5d5f2d..acdddd7 100644 --- a/src/modules/matcher/domain/interfaces/default-params.interface.ts +++ b/src/modules/matcher/domain/interfaces/default-params.type.ts @@ -1,5 +1,5 @@ -export interface IDefaultParams { +export type IDefaultParams = { DEFAULT_IDENTIFIER: number; MARGIN_DURATION: number; VALIDITY_DURATION: number; -} +}; diff --git a/src/modules/matcher/domain/interfaces/person-request.interface.ts b/src/modules/matcher/domain/interfaces/person-request.interface.ts index 75211f7..9dd8075 100644 --- a/src/modules/matcher/domain/interfaces/person-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/person-request.interface.ts @@ -1,3 +1,3 @@ export interface IRequestPerson { - identifier: number; + identifier?: number; } diff --git a/src/modules/matcher/domain/interfaces/role-request.interface.ts b/src/modules/matcher/domain/interfaces/role-request.interface.ts deleted file mode 100644 index cc124b8..0000000 --- a/src/modules/matcher/domain/interfaces/role-request.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IRequestRole { - driver: boolean; - passenger: boolean; -} diff --git a/src/modules/matcher/domain/interfaces/time-request.interface.ts b/src/modules/matcher/domain/interfaces/time-request.interface.ts index 6425441..33e902e 100644 --- a/src/modules/matcher/domain/interfaces/time-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/time-request.interface.ts @@ -1,7 +1,11 @@ -import { Schedule } from '../dtos/schedule.type'; +import { MarginDurations } from '../entities/margin-durations.type'; +import { Schedule } from '../entities/schedule.type'; export interface IRequestTime { departure?: string; fromDate?: string; + toDate?: string; schedule?: Schedule; + marginDuration?: number; + marginDurations?: MarginDurations; } diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index e957080..e8d9a44 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -5,36 +5,30 @@ import { Requirement } from '../domain/entities/requirement'; import { Role } from '../domain/entities/role.enum'; import { Settings } from '../domain/entities/settings'; import { Time } from '../domain/entities/time'; -import { IDefaultParams } from '../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../domain/interfaces/default-params.type'; export class MatchQuery { private readonly _matchRequest: MatchRequest; private readonly _defaultParams: IDefaultParams; person: Person; - exclusions: Array; - time: Time; - geography: Geography; roles: Array; + time: Time; + exclusions: Array; + geography: Geography; requirement: Requirement; settings: Settings; constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { this._matchRequest = matchRequest; this._defaultParams = defaultParams; - this._initialize(); this._setPerson(); - this._setExclusions(); this._setRoles(); this._setTime(); - // console.log(this); + this._initialize(); + this._setExclusions(); } _initialize() { - if ( - this._matchRequest.driver === undefined && - this._matchRequest.passenger === undefined - ) - this._matchRequest.passenger = true; this.geography = new Geography(); this.requirement = new Requirement(); this.settings = new Settings(); @@ -49,18 +43,11 @@ export class MatchQuery { this.person.init(); } - _setExclusions() { - this.exclusions = []; - if (this._matchRequest.identifier) - this.exclusions.push(this._matchRequest.identifier); - if (this._matchRequest.exclusions) - this.exclusions.push(...this._matchRequest.exclusions); - } - _setRoles() { this.roles = []; if (this._matchRequest.driver) this.roles.push(Role.DRIVER); if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER); + if (this.roles.length == 0) this.roles.push(Role.PASSENGER); } _setTime() { @@ -71,4 +58,12 @@ export class MatchQuery { ); this.time.init(); } + + _setExclusions() { + this.exclusions = []; + if (this._matchRequest.identifier) + this.exclusions.push(this._matchRequest.identifier); + if (this._matchRequest.exclusions) + this.exclusions.push(...this._matchRequest.exclusions); + } } diff --git a/src/modules/matcher/tests/unit/default-params.provider.spec.ts b/src/modules/matcher/tests/unit/default-params.provider.spec.ts index 49cf3ca..1b3cab7 100644 --- a/src/modules/matcher/tests/unit/default-params.provider.spec.ts +++ b/src/modules/matcher/tests/unit/default-params.provider.spec.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { DefaultParamsProvider } from '../../adapters/secondaries/default-params.provider'; -import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../../domain/interfaces/default-params.type'; const mockConfigService = { get: jest.fn().mockImplementationOnce(() => 99), diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index 3fa3f92..97a12f2 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -6,7 +6,7 @@ import { MatchQuery } from '../../queries/match.query'; import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; -import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../../domain/interfaces/default-params.type'; const mockAdRepository = {}; diff --git a/src/modules/matcher/tests/unit/person.spec.ts b/src/modules/matcher/tests/unit/person.spec.ts new file mode 100644 index 0000000..2ff144f --- /dev/null +++ b/src/modules/matcher/tests/unit/person.spec.ts @@ -0,0 +1,40 @@ +import { Person } from '../../domain/entities/person'; + +const DEFAULT_IDENTIFIER = 0; +const MARGIN_DURATION = 900; + +describe('Person entity', () => { + it('should be defined', () => { + const person = new Person( + { + identifier: 1, + }, + DEFAULT_IDENTIFIER, + MARGIN_DURATION, + ); + expect(person).toBeDefined(); + }); + + describe('init', () => { + it('should initialize a person with an identifier', () => { + const person = new Person( + { + identifier: 1, + }, + DEFAULT_IDENTIFIER, + MARGIN_DURATION, + ); + person.init(); + expect(person.identifier).toBe(1); + expect(person.marginDurations[0]).toBe(900); + expect(person.marginDurations[6]).toBe(900); + }); + it('should initialize a person without an identifier', () => { + const person = new Person({}, DEFAULT_IDENTIFIER, MARGIN_DURATION); + person.init(); + expect(person.identifier).toBe(0); + expect(person.marginDurations[0]).toBe(900); + expect(person.marginDurations[6]).toBe(900); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/time.spec.ts b/src/modules/matcher/tests/unit/time.spec.ts index b19ce8a..4674045 100644 --- a/src/modules/matcher/tests/unit/time.spec.ts +++ b/src/modules/matcher/tests/unit/time.spec.ts @@ -1,31 +1,14 @@ import { Time } from '../../domain/entities/time'; -import { IRequestTime } from '../../domain/interfaces/time-request.interface'; const MARGIN_DURATION = 900; const VALIDITY_DURATION = 365; -const punctualTimeRequest: IRequestTime = { - departure: '2023-04-01 12:24:00', -}; - -const invalidPunctualTimeRequest: IRequestTime = { - departure: '2023-15-01 12:24:00', -}; - -const recurrentTimeRequest: IRequestTime = { - fromDate: '2023-04-01', -}; - -const invalidRecurrentTimeRequest: IRequestTime = { - fromDate: '2023-15-01', -}; - -const expectedPunctualFromDate = new Date(punctualTimeRequest.departure); - describe('Time entity', () => { it('should be defined', () => { const time = new Time( - punctualTimeRequest, + { + departure: '2023-04-01 12:24:00', + }, MARGIN_DURATION, VALIDITY_DURATION, ); @@ -35,24 +18,74 @@ describe('Time entity', () => { describe('init', () => { it('should initialize a punctual time request', () => { const time = new Time( - punctualTimeRequest, + { + departure: '2023-04-01 12:24:00', + }, MARGIN_DURATION, VALIDITY_DURATION, ); time.init(); expect(time.fromDate.getFullYear()).toBe( - expectedPunctualFromDate.getFullYear(), + new Date('2023-04-01 12:24:00').getFullYear(), ); }); + it('should initialize a punctual time request with specific single margin duration', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDuration: 300, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.marginDurations['tue']).toBe(300); + }); + it('should initialize a punctual time request with specific margin durations', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDurations: { + sat: 350, + }, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.marginDurations['tue']).toBe(900); + expect(time.marginDurations['sat']).toBe(350); + }); + it('should initialize a punctual time request with specific single margin duration and margin durations', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDuration: 500, + marginDurations: { + sat: 350, + }, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.marginDurations['tue']).toBe(500); + expect(time.marginDurations['sat']).toBe(350); + }); it('should initialize a recurrent time request', () => { const time = new Time( - recurrentTimeRequest, + { + fromDate: '2023-04-01', + schedule: { + mon: '12:00', + }, + }, MARGIN_DURATION, VALIDITY_DURATION, ); time.init(); expect(time.fromDate.getFullYear()).toBe( - expectedPunctualFromDate.getFullYear(), + new Date('2023-04-01').getFullYear(), ); }); it('should throw an exception if no date is provided', () => { @@ -61,19 +94,82 @@ describe('Time entity', () => { }); it('should throw an exception if punctual date is invalid', () => { const time = new Time( - invalidPunctualTimeRequest, + { + departure: '2023-15-01 12:24:00', + }, MARGIN_DURATION, VALIDITY_DURATION, ); expect(() => time.init()).toThrow(); }); - it('should throw an exception if recuurent date is invalid', () => { + it('should throw an exception if recurrent fromDate is invalid', () => { const time = new Time( - invalidRecurrentTimeRequest, + { + fromDate: '2023-15-01', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if recurrent toDate is invalid', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2023-13-01', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if schedule is missing', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2024-03-31', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if schedule is empty', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2024-03-31', + schedule: {}, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if schedule is invalid', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2024-03-31', + schedule: { + mon: '32:78', + }, + }, MARGIN_DURATION, VALIDITY_DURATION, ); expect(() => time.init()).toThrow(); }); }); + it('should throw an exception if margin durations is provided but empty', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDurations: {}, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); }); From 1e4aa0eadc64f4abca4cd532adf377d5ef40fc64 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 11 Apr 2023 15:17:40 +0200 Subject: [PATCH 08/26] time request --- package.json | 1 + src/modules/matcher/domain/entities/time.ts | 8 +++----- src/modules/matcher/domain/entities/timing.ts | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7df44c8..f8e276e 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ ".module.ts", ".request.ts", ".presenter.ts", + ".exception.ts", "main.ts" ], "rootDir": "src", diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index 63f8deb..ef4b0bf 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -1,11 +1,9 @@ import { MatcherException } from '../../exceptions/matcher.exception'; import { MarginDurations } from './margin-durations.type'; import { IRequestTime } from '../interfaces/time-request.interface'; -import { TimingDays, TimingFrequency } from './timing'; +import { TimingDays, TimingFrequency, Days } from './timing'; import { Schedule } from './schedule.type'; -const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; - export class Time { _timeRequest: IRequestTime; _defaultMarginDuration: number; @@ -84,7 +82,7 @@ export class Time { } if ( !Object.keys(this._timeRequest.schedule).some((elem) => - days.includes(elem), + Days.includes(elem), ) ) { throw new MatcherException(3, 'No valid day in the given schedule'); @@ -140,7 +138,7 @@ export class Time { if (this._timeRequest.marginDurations) { if ( !Object.keys(this._timeRequest.marginDurations).some((elem) => - days.includes(elem), + Days.includes(elem), ) ) { throw new MatcherException( diff --git a/src/modules/matcher/domain/entities/timing.ts b/src/modules/matcher/domain/entities/timing.ts index 87196af..567595a 100644 --- a/src/modules/matcher/domain/entities/timing.ts +++ b/src/modules/matcher/domain/entities/timing.ts @@ -12,3 +12,5 @@ export enum TimingDays { 'fri', 'sat', } + +export const Days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; From b90db67ed060a67b573b02a76a4651e61be90079 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 11 Apr 2023 16:28:22 +0200 Subject: [PATCH 09/26] add geography request --- package-lock.json | 174 +++++++++++++++++- package.json | 9 + .../matcher/domain/dtos/match.request.ts | 5 +- src/modules/matcher/domain/entities/actor.ts | 9 + .../matcher/domain/entities/geography.enum.ts | 7 + .../matcher/domain/entities/geography.ts | 60 +++++- .../matcher/domain/entities/step.enum.ts | 6 + src/modules/matcher/domain/entities/time.ts | 3 + .../matcher/domain/entities/waypoint.ts | 7 + .../interfaces/geography-request.interface.ts | 5 + src/modules/matcher/queries/match.query.ts | 9 +- .../matcher/tests/unit/geography.spec.ts | 85 +++++++++ src/modules/matcher/tests/unit/time.spec.ts | 11 ++ 13 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 src/modules/matcher/domain/entities/actor.ts create mode 100644 src/modules/matcher/domain/entities/geography.enum.ts create mode 100644 src/modules/matcher/domain/entities/step.enum.ts create mode 100644 src/modules/matcher/domain/entities/waypoint.ts create mode 100644 src/modules/matcher/domain/interfaces/geography-request.interface.ts create mode 100644 src/modules/matcher/tests/unit/geography.spec.ts diff --git a/package-lock.json b/package-lock.json index 3512f8a..0d2f171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "geo-tz": "^7.0.7", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -2157,6 +2158,37 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -3005,6 +3037,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/array-source": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz", + "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -4658,6 +4695,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-source": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz", + "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==", + "dependencies": { + "stream-source": "0.3" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4877,6 +4922,62 @@ "node": ">=6.9.0" } }, + "node_modules/geo-tz": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-7.0.7.tgz", + "integrity": "sha512-Aq0sRSO1y4w62D5muRqzDmN4SWfFYnt703BLiqiHAvunlwsJs4qd3Fkl1pKSUa0bwuBmPFxIA8M1E+ilg2PSjw==", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "geobuf": "^3.0.2", + "pbf": "^3.2.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/geobuf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz", + "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==", + "dependencies": { + "concat-stream": "^2.0.0", + "pbf": "^3.2.1", + "shapefile": "~0.6.6" + }, + "bin": { + "geobuf2json": "bin/geobuf2json", + "json2geobuf": "bin/json2geobuf", + "shp2geobuf": "bin/shp2geobuf" + } + }, + "node_modules/geobuf/node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/geobuf/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5088,7 +5189,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -6850,6 +6950,15 @@ "node": ">=12" } }, + "node_modules/path-source": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz", + "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==", + "dependencies": { + "array-source": "0.0", + "file-source": "0.6" + } + }, "node_modules/path-to-regexp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", @@ -6864,6 +6973,18 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -7094,6 +7215,11 @@ "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7353,6 +7479,14 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -7660,6 +7794,28 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shapefile": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz", + "integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==", + "dependencies": { + "array-source": "0.0", + "commander": "2", + "path-source": "0.1", + "slice-source": "0.4", + "stream-source": "0.3", + "text-encoding": "^0.6.4" + }, + "bin": { + "dbf2json": "bin/dbf2json", + "shp2json": "bin/shp2json" + } + }, + "node_modules/shapefile/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7732,6 +7888,11 @@ "node": ">=8" } }, + "node_modules/slice-source": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", + "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==" + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -7800,6 +7961,11 @@ "node": ">= 0.8" } }, + "node_modules/stream-source": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz", + "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -8076,6 +8242,12 @@ "node": ">=8" } }, + "node_modules/text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==", + "deprecated": "no longer maintained" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index f8e276e..a4896ab 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "geo-tz": "^7.0.7", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -101,6 +102,14 @@ "collectCoverageFrom": [ "**/*.(t|j)s" ], + "coveragePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + ".request.ts", + ".presenter.ts", + ".exception.ts", + "main.ts" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" } diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 3a9d736..0a96193 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -16,8 +16,11 @@ import { MarginDurations } from '../entities/margin-durations.type'; import { Algorithm } from './algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface'; +import { IRequestGeography } from '../interfaces/geography-request.interface'; -export class MatchRequest implements IRequestTime, IRequestPerson { +export class MatchRequest + implements IRequestTime, IRequestPerson, IRequestGeography +{ @IsArray() @AutoMap() waypoints: Array; diff --git a/src/modules/matcher/domain/entities/actor.ts b/src/modules/matcher/domain/entities/actor.ts new file mode 100644 index 0000000..09e977b --- /dev/null +++ b/src/modules/matcher/domain/entities/actor.ts @@ -0,0 +1,9 @@ +import { Person } from './person'; +import { Role } from './role.enum'; +import { Step } from './step.enum'; + +export type Actor = { + person: Person; + role: Role; + step: Step; +}; diff --git a/src/modules/matcher/domain/entities/geography.enum.ts b/src/modules/matcher/domain/entities/geography.enum.ts new file mode 100644 index 0000000..e1e57a2 --- /dev/null +++ b/src/modules/matcher/domain/entities/geography.enum.ts @@ -0,0 +1,7 @@ +export enum PointType { + HOUSE_NUMBER = 'HOUSE_NUMBER', + STREET_ADDRESS = 'STREET_ADDRESS', + LOCALITY = 'LOCALITY', + VENUE = 'VENUE', + OTHER = 'OTHER', +} diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/geography.ts index 4cc5007..475f91b 100644 --- a/src/modules/matcher/domain/entities/geography.ts +++ b/src/modules/matcher/domain/entities/geography.ts @@ -1,11 +1,63 @@ +import { MatcherException } from '../../exceptions/matcher.exception'; +import { IRequestGeography } from '../interfaces/geography-request.interface'; +import { PointType } from './geography.enum'; import { Point } from './point.type'; import { Route } from './route'; +import { find } from 'geo-tz'; +import { Waypoint } from './waypoint'; export class Geography { - waypoints: Array; - originType: number; - destinationType: number; - timezone: string; + _geographyRequest: IRequestGeography; + waypoints: Array; + originType: PointType; + destinationType: PointType; + timeZones: Array; driverRoute: Route; passengerRoute: Route; + + constructor(geographyRequest: IRequestGeography) { + this._geographyRequest = geographyRequest; + this.waypoints = []; + this.originType = PointType.OTHER; + this.destinationType = PointType.OTHER; + } + + init() { + this._validateWaypoints(); + this._setTimeZones(); + } + + _validateWaypoints() { + if (this._geographyRequest.waypoints.length < 2) { + throw new MatcherException(3, 'At least 2 waypoints are required'); + } + this._geographyRequest.waypoints.map((point) => { + if (!this._isValidPoint(point)) { + throw new MatcherException( + 3, + `Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`, + ); + } + this.waypoints.push({ + point, + actors: [], + }); + }); + } + + _setTimeZones() { + this.timeZones = find( + this._geographyRequest.waypoints[0].lat, + this._geographyRequest.waypoints[0].lon, + ); + } + + _isValidPoint = (point: Point): boolean => + this._isValidLongitude(point.lon) && this._isValidLatitude(point.lat); + + _isValidLongitude = (longitude: number): boolean => + longitude >= -180 && longitude <= 180; + + _isValidLatitude = (latitude: number): boolean => + latitude >= -90 && latitude <= 90; } diff --git a/src/modules/matcher/domain/entities/step.enum.ts b/src/modules/matcher/domain/entities/step.enum.ts new file mode 100644 index 0000000..4b2fce4 --- /dev/null +++ b/src/modules/matcher/domain/entities/step.enum.ts @@ -0,0 +1,6 @@ +export enum Step { + START = 'start', + INTERMEDIATE = 'intermediate', + NEUTRAL = 'neutral', + FINISH = 'finish', +} diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index ef4b0bf..4669fc8 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -70,6 +70,9 @@ export class Time { if (!this._isDate(this.toDate)) { throw new MatcherException(3, 'Wrong toDate'); } + if (this.toDate < this.fromDate) { + throw new MatcherException(3, 'toDate must be after fromDate'); + } } if (this._timeRequest.fromDate) { this._validateSchedule(); diff --git a/src/modules/matcher/domain/entities/waypoint.ts b/src/modules/matcher/domain/entities/waypoint.ts new file mode 100644 index 0000000..62fe713 --- /dev/null +++ b/src/modules/matcher/domain/entities/waypoint.ts @@ -0,0 +1,7 @@ +import { Actor } from './actor'; +import { Point } from './point.type'; + +export type Waypoint = { + point: Point; + actors: Array; +}; diff --git a/src/modules/matcher/domain/interfaces/geography-request.interface.ts b/src/modules/matcher/domain/interfaces/geography-request.interface.ts new file mode 100644 index 0000000..79a18b3 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/geography-request.interface.ts @@ -0,0 +1,5 @@ +import { Point } from '../entities/point.type'; + +export interface IRequestGeography { + waypoints: Array; +} diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index e8d9a44..6f35f7b 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -13,8 +13,8 @@ export class MatchQuery { person: Person; roles: Array; time: Time; - exclusions: Array; geography: Geography; + exclusions: Array; requirement: Requirement; settings: Settings; @@ -24,12 +24,12 @@ export class MatchQuery { this._setPerson(); this._setRoles(); this._setTime(); + this._setGeography(); this._initialize(); this._setExclusions(); } _initialize() { - this.geography = new Geography(); this.requirement = new Requirement(); this.settings = new Settings(); } @@ -59,6 +59,11 @@ export class MatchQuery { this.time.init(); } + _setGeography() { + this.geography = new Geography(this._matchRequest); + this.geography.init(); + } + _setExclusions() { this.exclusions = []; if (this._matchRequest.identifier) diff --git a/src/modules/matcher/tests/unit/geography.spec.ts b/src/modules/matcher/tests/unit/geography.spec.ts new file mode 100644 index 0000000..3a728e9 --- /dev/null +++ b/src/modules/matcher/tests/unit/geography.spec.ts @@ -0,0 +1,85 @@ +import { Geography } from '../../domain/entities/geography'; + +describe('Geography entity', () => { + it('should be defined', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }); + expect(geography).toBeDefined(); + }); + + describe('init', () => { + it('should initialize a geography request', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }); + geography.init(); + expect(geography.waypoints.length).toBe(2); + }); + it('should throw an exception if waypoints are empty', () => { + const geography = new Geography({ + waypoints: [], + }); + expect(() => geography.init()).toThrow(); + }); + it('should throw an exception if only one waypoint is provided', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + ], + }); + expect(() => geography.init()).toThrow(); + }); + it('should throw an exception if a waypoint has invalid longitude', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 201.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }); + expect(() => geography.init()).toThrow(); + }); + it('should throw an exception if a waypoint has invalid latitude', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 250.630992, + lon: 3.045432, + }, + ], + }); + expect(() => geography.init()).toThrow(); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/time.spec.ts b/src/modules/matcher/tests/unit/time.spec.ts index 4674045..0d8cbdd 100644 --- a/src/modules/matcher/tests/unit/time.spec.ts +++ b/src/modules/matcher/tests/unit/time.spec.ts @@ -123,6 +123,17 @@ describe('Time entity', () => { ); expect(() => time.init()).toThrow(); }); + it('should throw an exception if recurrent toDate is before fromDate', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2023-03-01', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); it('should throw an exception if schedule is missing', () => { const time = new Time( { From c396da77fc17b84fc72992e6865b5486b1214385 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 12 Apr 2023 08:47:38 +0200 Subject: [PATCH 10/26] add default timezone --- .env.dist | 6 + .../secondaries/default-params.provider.ts | 1 + .../matcher/domain/entities/geography.ts | 11 +- .../domain/interfaces/default-params.type.ts | 1 + src/modules/matcher/queries/match.query.ts | 5 +- .../matcher/tests/unit/geography.spec.ts | 136 ++++++++++-------- .../matcher/tests/unit/match.usecase.spec.ts | 1 + 7 files changed, 96 insertions(+), 65 deletions(-) diff --git a/.env.dist b/.env.dist index a249484..c793c22 100644 --- a/.env.dist +++ b/.env.dist @@ -4,6 +4,12 @@ SERVICE_PORT=5005 SERVICE_CONFIGURATION_DOMAIN=MATCHER HEALTH_SERVICE_PORT=6005 +# DEFAULT CONFIGURATION +DEFAULT_IDENTIFIER=0 +MARGIN_DURATION=900 +VALIDITY_DURATION=365 +DEFAULT_TIMEZONE=Europe/Paris + # PRISMA DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index e8ce169..95d5077 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -13,6 +13,7 @@ export class DefaultParamsProvider { ), MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')), VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')), + DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'), }; } } diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/geography.ts index 475f91b..7f41c45 100644 --- a/src/modules/matcher/domain/entities/geography.ts +++ b/src/modules/matcher/domain/entities/geography.ts @@ -11,20 +11,21 @@ export class Geography { waypoints: Array; originType: PointType; destinationType: PointType; - timeZones: Array; + timezones: Array; driverRoute: Route; passengerRoute: Route; - constructor(geographyRequest: IRequestGeography) { + constructor(geographyRequest: IRequestGeography, defaultTimezone: string) { this._geographyRequest = geographyRequest; this.waypoints = []; this.originType = PointType.OTHER; this.destinationType = PointType.OTHER; + this.timezones = [defaultTimezone]; } init() { this._validateWaypoints(); - this._setTimeZones(); + this._setTimezones(); } _validateWaypoints() { @@ -45,8 +46,8 @@ export class Geography { }); } - _setTimeZones() { - this.timeZones = find( + _setTimezones() { + this.timezones = find( this._geographyRequest.waypoints[0].lat, this._geographyRequest.waypoints[0].lon, ); diff --git a/src/modules/matcher/domain/interfaces/default-params.type.ts b/src/modules/matcher/domain/interfaces/default-params.type.ts index acdddd7..abcbe30 100644 --- a/src/modules/matcher/domain/interfaces/default-params.type.ts +++ b/src/modules/matcher/domain/interfaces/default-params.type.ts @@ -2,4 +2,5 @@ export type IDefaultParams = { DEFAULT_IDENTIFIER: number; MARGIN_DURATION: number; VALIDITY_DURATION: number; + DEFAULT_TIMEZONE: string; }; diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 6f35f7b..2d798ea 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -60,7 +60,10 @@ export class MatchQuery { } _setGeography() { - this.geography = new Geography(this._matchRequest); + this.geography = new Geography( + this._matchRequest, + this._defaultParams.DEFAULT_TIMEZONE, + ); this.geography.init(); } diff --git a/src/modules/matcher/tests/unit/geography.spec.ts b/src/modules/matcher/tests/unit/geography.spec.ts index 3a728e9..7c52c8c 100644 --- a/src/modules/matcher/tests/unit/geography.spec.ts +++ b/src/modules/matcher/tests/unit/geography.spec.ts @@ -2,83 +2,101 @@ import { Geography } from '../../domain/entities/geography'; describe('Geography entity', () => { it('should be defined', () => { - const geography = new Geography({ - waypoints: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }); + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + ); expect(geography).toBeDefined(); }); describe('init', () => { it('should initialize a geography request', () => { - const geography = new Geography({ - waypoints: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }); + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + ); geography.init(); expect(geography.waypoints.length).toBe(2); }); it('should throw an exception if waypoints are empty', () => { - const geography = new Geography({ - waypoints: [], - }); + const geography = new Geography( + { + waypoints: [], + }, + 'Europe/Paris', + ); expect(() => geography.init()).toThrow(); }); it('should throw an exception if only one waypoint is provided', () => { - const geography = new Geography({ - waypoints: [ - { - lat: 49.440041, - lon: 1.093912, - }, - ], - }); + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + ], + }, + 'Europe/Paris', + ); expect(() => geography.init()).toThrow(); }); it('should throw an exception if a waypoint has invalid longitude', () => { - const geography = new Geography({ - waypoints: [ - { - lat: 49.440041, - lon: 201.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }); + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 201.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + ); expect(() => geography.init()).toThrow(); }); it('should throw an exception if a waypoint has invalid latitude', () => { - const geography = new Geography({ - waypoints: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 250.630992, - lon: 3.045432, - }, - ], - }); + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 250.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + ); expect(() => geography.init()).toThrow(); }); }); diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index 97a12f2..5304b3d 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -18,6 +18,7 @@ const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', }; describe('MatchUseCase', () => { From b7822cf88f57efe236425df438df6ea77407d991 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 12 Apr 2023 11:47:02 +0200 Subject: [PATCH 11/26] improve tests --- package.json | 2 + .../redis-configuration.repository.spec.ts | 47 ++++++++ .../matcher/tests/unit/match.query.spec.ts | 111 ++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/modules/configuration/tests/unit/redis-configuration.repository.spec.ts create mode 100644 src/modules/matcher/tests/unit/match.query.spec.ts diff --git a/package.json b/package.json index a4896ab..d9801d5 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ ".module.ts", ".request.ts", ".presenter.ts", + ".profile.ts", ".exception.ts", "main.ts" ], @@ -107,6 +108,7 @@ ".module.ts", ".request.ts", ".presenter.ts", + ".profile.ts", ".exception.ts", "main.ts" ], diff --git a/src/modules/configuration/tests/unit/redis-configuration.repository.spec.ts b/src/modules/configuration/tests/unit/redis-configuration.repository.spec.ts new file mode 100644 index 0000000..8efa436 --- /dev/null +++ b/src/modules/configuration/tests/unit/redis-configuration.repository.spec.ts @@ -0,0 +1,47 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository'; +import { getRedisToken } from '@liaoliaots/nestjs-redis'; + +const mockRedis = { + get: jest.fn().mockResolvedValue('myValue'), + set: jest.fn().mockImplementation(), + del: jest.fn().mockImplementation(), +}; + +describe('RedisConfigurationRepository', () => { + let redisConfigurationRepository: RedisConfigurationRepository; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: getRedisToken('default'), + useValue: mockRedis, + }, + RedisConfigurationRepository, + ], + }).compile(); + + redisConfigurationRepository = module.get( + RedisConfigurationRepository, + ); + }); + + it('should be defined', () => { + expect(redisConfigurationRepository).toBeDefined(); + }); + + describe('interact', () => { + it('should get a value', async () => { + expect(await redisConfigurationRepository.get('myKey')).toBe('myValue'); + }); + it('should set a value', async () => { + expect( + await redisConfigurationRepository.set('myKey', 'myValue'), + ).toBeUndefined(); + }); + it('should delete a value', async () => { + expect(await redisConfigurationRepository.del('myKey')).toBeUndefined(); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/match.query.spec.ts b/src/modules/matcher/tests/unit/match.query.spec.ts new file mode 100644 index 0000000..3ad22d3 --- /dev/null +++ b/src/modules/matcher/tests/unit/match.query.spec.ts @@ -0,0 +1,111 @@ +import { MatchRequest } from '../../domain/dtos/match.request'; +import { Role } from '../../domain/entities/role.enum'; +import { IDefaultParams } from '../../domain/interfaces/default-params.type'; +import { MatchQuery } from '../../queries/match.query'; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', +}; + +describe('Match query', () => { + it('should be defined', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery).toBeDefined(); + }); + + describe('init', () => { + it('should create a query with excluded identifiers', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.identifier = 125; + matchRequest.exclusions = [126, 127, 128]; + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + ); + expect(matchQuery.exclusions.length).toBe(4); + }); + }); + + it('should create a query with driver role only', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.driver = true; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.roles).toEqual([Role.DRIVER]); + }); + + it('should create a query with passenger role only', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.passenger = true; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.roles).toEqual([Role.PASSENGER]); + }); + + it('should create a query with driver and passenger roles', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.passenger = true; + matchRequest.driver = true; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.roles.length).toBe(2); + expect(matchQuery.roles).toContain(Role.PASSENGER); + expect(matchQuery.roles).toContain(Role.DRIVER); + }); +}); From dd957763a338c8d576a0c93c038250eb5f7bfd72 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 12 Apr 2023 14:59:25 +0200 Subject: [PATCH 12/26] add algorithm settings --- .env.dist | 33 +++++- .../secondaries/default-params.provider.ts | 18 +++ .../matcher/domain/dtos/match.request.ts | 13 +- .../domain/entities/algorithm-settings.ts | 58 +++++++++ .../matcher/domain/entities/requirement.ts | 9 ++ .../algorithm-settings-request.interface.ts} | 9 +- .../default-algorithm-settings.type.ts | 15 +++ .../domain/interfaces/default-params.type.ts | 4 + .../requirement-request.interface.ts | 4 + src/modules/matcher/queries/match.query.ts | 27 +++-- .../matcher/tests/unit/match.query.spec.ts | 111 ++++++++++++++---- .../matcher/tests/unit/match.usecase.spec.ts | 15 +++ 12 files changed, 278 insertions(+), 38 deletions(-) create mode 100644 src/modules/matcher/domain/entities/algorithm-settings.ts rename src/modules/matcher/domain/{entities/settings.ts => interfaces/algorithm-settings-request.interface.ts} (61%) create mode 100644 src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts create mode 100644 src/modules/matcher/domain/interfaces/requirement-request.interface.ts diff --git a/.env.dist b/.env.dist index c793c22..412486b 100644 --- a/.env.dist +++ b/.env.dist @@ -5,10 +5,39 @@ SERVICE_CONFIGURATION_DOMAIN=MATCHER HEALTH_SERVICE_PORT=6005 # DEFAULT CONFIGURATION + +# default identifier used for match requests DEFAULT_IDENTIFIER=0 -MARGIN_DURATION=900 -VALIDITY_DURATION=365 +# default timezone DEFAULT_TIMEZONE=Europe/Paris +# default number of seats proposed as driver +DEFAULT_SEATS=3 +# algorithm type +ALGORITHM=classic +# strict algorithm (if relevant with the algorithm type) +# if set to true, matches are made so that +# punctual ads match only with punctual ads and +# recurrent ads match only with recurrent ads +STRICT_ALGORITHM=0 +# max distance in metres between driver +# route and passenger pick-up / drop-off +REMOTENESS=15000 +# use passenger proportion +USE_PROPORTION=1 +# minimal driver proportion +PROPORTION=0.3 +# use azimuth calculation +USE_AZIMUTH=1 +# azimuth margin +AZIMUTH_MARGIN=10 +# margin duration in seconds +MARGIN_DURATION=900 +# default validity duration (in days) for recurrent proposals +VALIDITY_DURATION=365 +# max detour ratio +MAX_DETOUR_DISTANCE_RATIO=0.3 +MAX_DETOUR_DURATION_RATIO=0.3 + # PRISMA DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index 95d5077..be1ddeb 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -14,6 +14,24 @@ export class DefaultParamsProvider { MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')), VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')), DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'), + DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')), + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: this.configService.get('ALGORITHM'), + strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')), + remoteness: parseInt(this.configService.get('REMOTENESS')), + useProportion: !!parseInt(this.configService.get('USE_PROPORTION')), + proportion: parseInt(this.configService.get('PROPORTION')), + useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')), + azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')), + maxDetourDistanceRatio: parseFloat( + this.configService.get('MAX_DETOUR_DISTANCE_RATIO'), + ), + maxDetourDurationRatio: parseFloat( + this.configService.get('MAX_DETOUR_DURATION_RATIO'), + ), + georouterType: this.configService.get('GEOROUTER_TYPE'), + georouterUrl: this.configService.get('GEOROUTER_URL'), + }, }; } } diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 0a96193..d3aa3cd 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -17,9 +17,16 @@ import { Algorithm } from './algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface'; import { IRequestGeography } from '../interfaces/geography-request.interface'; +import { IRequestRequirement } from '../interfaces/requirement-request.interface'; +import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface'; export class MatchRequest - implements IRequestTime, IRequestPerson, IRequestGeography + implements + IRequestTime, + IRequestPerson, + IRequestGeography, + IRequestRequirement, + IRequestAlgorithmSettings { @IsArray() @AutoMap() @@ -65,11 +72,15 @@ export class MatchRequest @IsOptional() @IsNumber() + @Min(1) + @Max(10) @AutoMap() seatsPassenger: number; @IsOptional() @IsNumber() + @Min(1) + @Max(10) @AutoMap() seatsDriver: number; diff --git a/src/modules/matcher/domain/entities/algorithm-settings.ts b/src/modules/matcher/domain/entities/algorithm-settings.ts new file mode 100644 index 0000000..278dea3 --- /dev/null +++ b/src/modules/matcher/domain/entities/algorithm-settings.ts @@ -0,0 +1,58 @@ +import { Algorithm } from '../dtos/algorithm.enum'; +import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface'; +import { DefaultAlgorithmSettings } from '../interfaces/default-algorithm-settings.type'; +import { TimingFrequency } from './timing'; + +export class AlgorithmSettings { + _algorithmSettingsRequest: IRequestAlgorithmSettings; + _strict: boolean; + algorithm: Algorithm; + restrict: TimingFrequency; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDurationRatio: number; + maxDetourDistanceRatio: number; + georouterType: string; + georouterUrl: string; + + constructor( + algorithmSettingsRequest: IRequestAlgorithmSettings, + defaultAlgorithmSettings: DefaultAlgorithmSettings, + frequency: TimingFrequency, + ) { + this._algorithmSettingsRequest = algorithmSettingsRequest; + this.algorithm = + algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm; + this._strict = + algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict; + this.remoteness = algorithmSettingsRequest.remoteness + ? Math.abs(algorithmSettingsRequest.remoteness) + : defaultAlgorithmSettings.remoteness; + this.useProportion = + algorithmSettingsRequest.useProportion ?? + defaultAlgorithmSettings.useProportion; + this.proportion = algorithmSettingsRequest.proportion + ? Math.abs(algorithmSettingsRequest.proportion) + : defaultAlgorithmSettings.proportion; + this.useAzimuth = + algorithmSettingsRequest.useAzimuth ?? + defaultAlgorithmSettings.useAzimuth; + this.azimuthMargin = algorithmSettingsRequest.azimuthMargin + ? Math.abs(algorithmSettingsRequest.azimuthMargin) + : defaultAlgorithmSettings.azimuthMargin; + this.maxDetourDistanceRatio = + algorithmSettingsRequest.maxDetourDistanceRatio ?? + defaultAlgorithmSettings.maxDetourDistanceRatio; + this.maxDetourDurationRatio = + algorithmSettingsRequest.maxDetourDurationRatio ?? + defaultAlgorithmSettings.maxDetourDurationRatio; + this.georouterType = defaultAlgorithmSettings.georouterType; + this.georouterUrl = defaultAlgorithmSettings.georouterUrl; + if (this._strict) { + this.restrict = frequency; + } + } +} diff --git a/src/modules/matcher/domain/entities/requirement.ts b/src/modules/matcher/domain/entities/requirement.ts index b9c9b7c..194907f 100644 --- a/src/modules/matcher/domain/entities/requirement.ts +++ b/src/modules/matcher/domain/entities/requirement.ts @@ -1,4 +1,13 @@ +import { IRequestRequirement } from '../interfaces/requirement-request.interface'; + export class Requirement { + _requirementRequest: IRequestRequirement; seatsDriver: number; seatsPassenger: number; + + constructor(requirementRequest: IRequestRequirement, defaultSeats: number) { + this._requirementRequest = requirementRequest; + this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats; + this.seatsPassenger = requirementRequest.seatsPassenger ?? 1; + } } diff --git a/src/modules/matcher/domain/entities/settings.ts b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts similarity index 61% rename from src/modules/matcher/domain/entities/settings.ts rename to src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts index 14d53b8..0b4709f 100644 --- a/src/modules/matcher/domain/entities/settings.ts +++ b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts @@ -1,14 +1,13 @@ -import { Georouter } from '../interfaces/georouter.interface'; +import { Algorithm } from '../dtos/algorithm.enum'; -export class Settings { +export interface IRequestAlgorithmSettings { algorithm: Algorithm; - restrict: boolean; + strict: boolean; remoteness: number; useProportion: boolean; proportion: number; useAzimuth: boolean; azimuthMargin: number; - maxDetourDurationRatio: number; maxDetourDistanceRatio: number; - georouter: Georouter; + maxDetourDurationRatio: number; } diff --git a/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts b/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts new file mode 100644 index 0000000..63eb724 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts @@ -0,0 +1,15 @@ +import { Algorithm } from '../dtos/algorithm.enum'; + +export type DefaultAlgorithmSettings = { + algorithm: Algorithm; + strict: boolean; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDistanceRatio: number; + maxDetourDurationRatio: number; + georouterType: string; + georouterUrl: string; +}; diff --git a/src/modules/matcher/domain/interfaces/default-params.type.ts b/src/modules/matcher/domain/interfaces/default-params.type.ts index abcbe30..f39bd3b 100644 --- a/src/modules/matcher/domain/interfaces/default-params.type.ts +++ b/src/modules/matcher/domain/interfaces/default-params.type.ts @@ -1,6 +1,10 @@ +import { DefaultAlgorithmSettings } from './default-algorithm-settings.type'; + export type IDefaultParams = { DEFAULT_IDENTIFIER: number; MARGIN_DURATION: number; VALIDITY_DURATION: number; DEFAULT_TIMEZONE: string; + DEFAULT_SEATS: number; + DEFAULT_ALGORITHM_SETTINGS: DefaultAlgorithmSettings; }; diff --git a/src/modules/matcher/domain/interfaces/requirement-request.interface.ts b/src/modules/matcher/domain/interfaces/requirement-request.interface.ts new file mode 100644 index 0000000..61e5900 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/requirement-request.interface.ts @@ -0,0 +1,4 @@ +export interface IRequestRequirement { + seatsDriver?: number; + seatsPassenger?: number; +} diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 2d798ea..c873f13 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -3,7 +3,7 @@ import { Geography } from '../domain/entities/geography'; import { Person } from '../domain/entities/person'; import { Requirement } from '../domain/entities/requirement'; import { Role } from '../domain/entities/role.enum'; -import { Settings } from '../domain/entities/settings'; +import { AlgorithmSettings } from '../domain/entities/algorithm-settings'; import { Time } from '../domain/entities/time'; import { IDefaultParams } from '../domain/interfaces/default-params.type'; @@ -16,7 +16,7 @@ export class MatchQuery { geography: Geography; exclusions: Array; requirement: Requirement; - settings: Settings; + algorithmSettings: AlgorithmSettings; constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { this._matchRequest = matchRequest; @@ -25,15 +25,11 @@ export class MatchQuery { this._setRoles(); this._setTime(); this._setGeography(); - this._initialize(); + this._setRequirement(); + this._setAlgorithmSettings(); this._setExclusions(); } - _initialize() { - this.requirement = new Requirement(); - this.settings = new Settings(); - } - _setPerson() { this.person = new Person( this._matchRequest, @@ -67,6 +63,21 @@ export class MatchQuery { this.geography.init(); } + _setRequirement() { + this.requirement = new Requirement( + this._matchRequest, + this._defaultParams.DEFAULT_SEATS, + ); + } + + _setAlgorithmSettings() { + this.algorithmSettings = new AlgorithmSettings( + this._matchRequest, + this._defaultParams.DEFAULT_ALGORITHM_SETTINGS, + this.time.frequency, + ); + } + _setExclusions() { this.exclusions = []; if (this._matchRequest.identifier) diff --git a/src/modules/matcher/tests/unit/match.query.spec.ts b/src/modules/matcher/tests/unit/match.query.spec.ts index 3ad22d3..ef5c883 100644 --- a/src/modules/matcher/tests/unit/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/match.query.spec.ts @@ -1,5 +1,7 @@ +import { Algorithm } from '../../domain/dtos/algorithm.enum'; import { MatchRequest } from '../../domain/dtos/match.request'; import { Role } from '../../domain/entities/role.enum'; +import { TimingFrequency } from '../../domain/entities/timing'; import { IDefaultParams } from '../../domain/interfaces/default-params.type'; import { MatchQuery } from '../../queries/match.query'; @@ -8,6 +10,20 @@ const defaultParams: IDefaultParams = { MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: Algorithm.CLASSIC, + strict: false, + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + georouterType: 'graphhopper', + georouterUrl: 'http://localhost', + }, }; describe('Match query', () => { @@ -28,28 +44,23 @@ describe('Match query', () => { expect(matchQuery).toBeDefined(); }); - describe('init', () => { - it('should create a query with excluded identifiers', () => { - const matchRequest: MatchRequest = new MatchRequest(); - matchRequest.departure = '2023-04-01 12:00'; - matchRequest.waypoints = [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ]; - matchRequest.identifier = 125; - matchRequest.exclusions = [126, 127, 128]; - const matchQuery: MatchQuery = new MatchQuery( - matchRequest, - defaultParams, - ); - expect(matchQuery.exclusions.length).toBe(4); - }); + it('should create a query with excluded identifiers', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.identifier = 125; + matchRequest.exclusions = [126, 127, 128]; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.exclusions.length).toBe(4); }); it('should create a query with driver role only', () => { @@ -108,4 +119,60 @@ describe('Match query', () => { expect(matchQuery.roles).toContain(Role.PASSENGER); expect(matchQuery.roles).toContain(Role.DRIVER); }); + + it('should create a query with number of seats modified', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.seatsDriver = 1; + matchRequest.seatsPassenger = 2; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.requirement.seatsDriver).toBe(1); + expect(matchQuery.requirement.seatsPassenger).toBe(2); + }); + + it('should create a query with modified algorithm settings', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.algorithm = Algorithm.CLASSIC; + matchRequest.strict = true; + matchRequest.useProportion = true; + matchRequest.proportion = 0.45; + matchRequest.useAzimuth = true; + matchRequest.azimuthMargin = 15; + matchRequest.remoteness = 20000; + matchRequest.maxDetourDistanceRatio = 0.41; + matchRequest.maxDetourDurationRatio = 0.42; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC); + expect(matchQuery.algorithmSettings.restrict).toBe( + TimingFrequency.FREQUENCY_PUNCTUAL, + ); + expect(matchQuery.algorithmSettings.useProportion).toBeTruthy(); + expect(matchQuery.algorithmSettings.proportion).toBe(0.45); + expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy(); + expect(matchQuery.algorithmSettings.azimuthMargin).toBe(15); + expect(matchQuery.algorithmSettings.remoteness).toBe(20000); + expect(matchQuery.algorithmSettings.maxDetourDistanceRatio).toBe(0.41); + expect(matchQuery.algorithmSettings.maxDetourDurationRatio).toBe(0.42); + }); }); diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index 5304b3d..c215eb2 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -7,6 +7,7 @@ import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; import { IDefaultParams } from '../../domain/interfaces/default-params.type'; +import { Algorithm } from '../../domain/dtos/algorithm.enum'; const mockAdRepository = {}; @@ -19,6 +20,20 @@ const defaultParams: IDefaultParams = { MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: Algorithm.CLASSIC, + strict: false, + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + georouterType: 'graphhopper', + georouterUrl: 'http://localhost', + }, }; describe('MatchUseCase', () => { From a30b19e2f3235e3bb3efc795205ce9de6aa43492 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 12 Apr 2023 16:03:19 +0200 Subject: [PATCH 13/26] refactor --- .../adapters/secondaries/default-params.provider.ts | 2 +- src/modules/matcher/domain/dtos/match.request.ts | 8 ++++---- src/modules/matcher/domain/entities/algorithm-settings.ts | 6 +++--- src/modules/matcher/domain/entities/geography.ts | 6 +++--- src/modules/matcher/domain/entities/time.ts | 6 +++--- .../interfaces/algorithm-settings-request.interface.ts | 2 +- .../domain/interfaces/geography-request.interface.ts | 2 +- .../matcher/domain/interfaces/time-request.interface.ts | 4 ++-- .../domain/{entities/actor.ts => types/actor.type..ts} | 2 +- .../matcher/domain/{dtos => types}/algorithm.enum.ts | 0 .../default-algorithm-settings.type.ts | 2 +- .../domain/{interfaces => types}/default-params.type.ts | 0 .../matcher/domain/{entities => types}/geography.enum.ts | 0 .../domain/{entities => types}/margin-durations.type.ts | 0 .../matcher/domain/{entities => types}/point.type.ts | 0 .../matcher/domain/{entities => types}/role.enum.ts | 0 .../matcher/domain/{entities => types}/schedule.type.ts | 0 .../matcher/domain/{entities => types}/step.enum.ts | 0 src/modules/matcher/domain/{entities => types}/timing.ts | 0 .../matcher/domain/{entities => types}/waypoint.ts | 2 +- src/modules/matcher/queries/match.query.ts | 4 ++-- .../matcher/tests/unit/default-params.provider.spec.ts | 2 +- src/modules/matcher/tests/unit/match.query.spec.ts | 8 ++++---- src/modules/matcher/tests/unit/match.usecase.spec.ts | 4 ++-- 24 files changed, 30 insertions(+), 30 deletions(-) rename src/modules/matcher/domain/{entities/actor.ts => types/actor.type..ts} (76%) rename src/modules/matcher/domain/{dtos => types}/algorithm.enum.ts (100%) rename src/modules/matcher/domain/{interfaces => types}/default-algorithm-settings.type.ts (86%) rename src/modules/matcher/domain/{interfaces => types}/default-params.type.ts (100%) rename src/modules/matcher/domain/{entities => types}/geography.enum.ts (100%) rename src/modules/matcher/domain/{entities => types}/margin-durations.type.ts (100%) rename src/modules/matcher/domain/{entities => types}/point.type.ts (100%) rename src/modules/matcher/domain/{entities => types}/role.enum.ts (100%) rename src/modules/matcher/domain/{entities => types}/schedule.type.ts (100%) rename src/modules/matcher/domain/{entities => types}/step.enum.ts (100%) rename src/modules/matcher/domain/{entities => types}/timing.ts (100%) rename src/modules/matcher/domain/{entities => types}/waypoint.ts (73%) diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index be1ddeb..9b5bad6 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { IDefaultParams } from '../../domain/interfaces/default-params.type'; +import { IDefaultParams } from '../../domain/types/default-params.type'; @Injectable() export class DefaultParamsProvider { diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index d3aa3cd..14d7339 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -10,10 +10,10 @@ import { 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'; +import { Point } from '../types/point.type'; +import { Schedule } from '../types/schedule.type'; +import { MarginDurations } from '../types/margin-durations.type'; +import { Algorithm } from '../types/algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface'; import { IRequestGeography } from '../interfaces/geography-request.interface'; diff --git a/src/modules/matcher/domain/entities/algorithm-settings.ts b/src/modules/matcher/domain/entities/algorithm-settings.ts index 278dea3..1a79bb5 100644 --- a/src/modules/matcher/domain/entities/algorithm-settings.ts +++ b/src/modules/matcher/domain/entities/algorithm-settings.ts @@ -1,7 +1,7 @@ -import { Algorithm } from '../dtos/algorithm.enum'; import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface'; -import { DefaultAlgorithmSettings } from '../interfaces/default-algorithm-settings.type'; -import { TimingFrequency } from './timing'; +import { DefaultAlgorithmSettings } from '../types/default-algorithm-settings.type'; +import { Algorithm } from '../types/algorithm.enum'; +import { TimingFrequency } from '../types/timing'; export class AlgorithmSettings { _algorithmSettingsRequest: IRequestAlgorithmSettings; diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/geography.ts index 7f41c45..3cf3697 100644 --- a/src/modules/matcher/domain/entities/geography.ts +++ b/src/modules/matcher/domain/entities/geography.ts @@ -1,10 +1,10 @@ import { MatcherException } from '../../exceptions/matcher.exception'; import { IRequestGeography } from '../interfaces/geography-request.interface'; -import { PointType } from './geography.enum'; -import { Point } from './point.type'; +import { PointType } from '../types/geography.enum'; +import { Point } from '../types/point.type'; import { Route } from './route'; import { find } from 'geo-tz'; -import { Waypoint } from './waypoint'; +import { Waypoint } from '../types/waypoint'; export class Geography { _geographyRequest: IRequestGeography; diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index 4669fc8..a6be1f1 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -1,8 +1,8 @@ import { MatcherException } from '../../exceptions/matcher.exception'; -import { MarginDurations } from './margin-durations.type'; +import { MarginDurations } from '../types/margin-durations.type'; import { IRequestTime } from '../interfaces/time-request.interface'; -import { TimingDays, TimingFrequency, Days } from './timing'; -import { Schedule } from './schedule.type'; +import { TimingDays, TimingFrequency, Days } from '../types/timing'; +import { Schedule } from '../types/schedule.type'; export class Time { _timeRequest: IRequestTime; diff --git a/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts index 0b4709f..3ab0de8 100644 --- a/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts @@ -1,4 +1,4 @@ -import { Algorithm } from '../dtos/algorithm.enum'; +import { Algorithm } from '../types/algorithm.enum'; export interface IRequestAlgorithmSettings { algorithm: Algorithm; diff --git a/src/modules/matcher/domain/interfaces/geography-request.interface.ts b/src/modules/matcher/domain/interfaces/geography-request.interface.ts index 79a18b3..d10a6ac 100644 --- a/src/modules/matcher/domain/interfaces/geography-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/geography-request.interface.ts @@ -1,4 +1,4 @@ -import { Point } from '../entities/point.type'; +import { Point } from '../types/point.type'; export interface IRequestGeography { waypoints: Array; diff --git a/src/modules/matcher/domain/interfaces/time-request.interface.ts b/src/modules/matcher/domain/interfaces/time-request.interface.ts index 33e902e..1f8c6a7 100644 --- a/src/modules/matcher/domain/interfaces/time-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/time-request.interface.ts @@ -1,5 +1,5 @@ -import { MarginDurations } from '../entities/margin-durations.type'; -import { Schedule } from '../entities/schedule.type'; +import { MarginDurations } from '../types/margin-durations.type'; +import { Schedule } from '../types/schedule.type'; export interface IRequestTime { departure?: string; diff --git a/src/modules/matcher/domain/entities/actor.ts b/src/modules/matcher/domain/types/actor.type..ts similarity index 76% rename from src/modules/matcher/domain/entities/actor.ts rename to src/modules/matcher/domain/types/actor.type..ts index 09e977b..6edd39a 100644 --- a/src/modules/matcher/domain/entities/actor.ts +++ b/src/modules/matcher/domain/types/actor.type..ts @@ -1,4 +1,4 @@ -import { Person } from './person'; +import { Person } from '../entities/person'; import { Role } from './role.enum'; import { Step } from './step.enum'; diff --git a/src/modules/matcher/domain/dtos/algorithm.enum.ts b/src/modules/matcher/domain/types/algorithm.enum.ts similarity index 100% rename from src/modules/matcher/domain/dtos/algorithm.enum.ts rename to src/modules/matcher/domain/types/algorithm.enum.ts diff --git a/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts b/src/modules/matcher/domain/types/default-algorithm-settings.type.ts similarity index 86% rename from src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts rename to src/modules/matcher/domain/types/default-algorithm-settings.type.ts index 63eb724..89c0c93 100644 --- a/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts +++ b/src/modules/matcher/domain/types/default-algorithm-settings.type.ts @@ -1,4 +1,4 @@ -import { Algorithm } from '../dtos/algorithm.enum'; +import { Algorithm } from './algorithm.enum'; export type DefaultAlgorithmSettings = { algorithm: Algorithm; diff --git a/src/modules/matcher/domain/interfaces/default-params.type.ts b/src/modules/matcher/domain/types/default-params.type.ts similarity index 100% rename from src/modules/matcher/domain/interfaces/default-params.type.ts rename to src/modules/matcher/domain/types/default-params.type.ts diff --git a/src/modules/matcher/domain/entities/geography.enum.ts b/src/modules/matcher/domain/types/geography.enum.ts similarity index 100% rename from src/modules/matcher/domain/entities/geography.enum.ts rename to src/modules/matcher/domain/types/geography.enum.ts diff --git a/src/modules/matcher/domain/entities/margin-durations.type.ts b/src/modules/matcher/domain/types/margin-durations.type.ts similarity index 100% rename from src/modules/matcher/domain/entities/margin-durations.type.ts rename to src/modules/matcher/domain/types/margin-durations.type.ts diff --git a/src/modules/matcher/domain/entities/point.type.ts b/src/modules/matcher/domain/types/point.type.ts similarity index 100% rename from src/modules/matcher/domain/entities/point.type.ts rename to src/modules/matcher/domain/types/point.type.ts diff --git a/src/modules/matcher/domain/entities/role.enum.ts b/src/modules/matcher/domain/types/role.enum.ts similarity index 100% rename from src/modules/matcher/domain/entities/role.enum.ts rename to src/modules/matcher/domain/types/role.enum.ts diff --git a/src/modules/matcher/domain/entities/schedule.type.ts b/src/modules/matcher/domain/types/schedule.type.ts similarity index 100% rename from src/modules/matcher/domain/entities/schedule.type.ts rename to src/modules/matcher/domain/types/schedule.type.ts diff --git a/src/modules/matcher/domain/entities/step.enum.ts b/src/modules/matcher/domain/types/step.enum.ts similarity index 100% rename from src/modules/matcher/domain/entities/step.enum.ts rename to src/modules/matcher/domain/types/step.enum.ts diff --git a/src/modules/matcher/domain/entities/timing.ts b/src/modules/matcher/domain/types/timing.ts similarity index 100% rename from src/modules/matcher/domain/entities/timing.ts rename to src/modules/matcher/domain/types/timing.ts diff --git a/src/modules/matcher/domain/entities/waypoint.ts b/src/modules/matcher/domain/types/waypoint.ts similarity index 73% rename from src/modules/matcher/domain/entities/waypoint.ts rename to src/modules/matcher/domain/types/waypoint.ts index 62fe713..6ee5941 100644 --- a/src/modules/matcher/domain/entities/waypoint.ts +++ b/src/modules/matcher/domain/types/waypoint.ts @@ -1,4 +1,4 @@ -import { Actor } from './actor'; +import { Actor } from './actor.type.'; import { Point } from './point.type'; export type Waypoint = { diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index c873f13..fdec447 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -2,10 +2,10 @@ import { MatchRequest } from '../domain/dtos/match.request'; import { Geography } from '../domain/entities/geography'; import { Person } from '../domain/entities/person'; import { Requirement } from '../domain/entities/requirement'; -import { Role } from '../domain/entities/role.enum'; +import { Role } from '../domain/types/role.enum'; import { AlgorithmSettings } from '../domain/entities/algorithm-settings'; import { Time } from '../domain/entities/time'; -import { IDefaultParams } from '../domain/interfaces/default-params.type'; +import { IDefaultParams } from '../domain/types/default-params.type'; export class MatchQuery { private readonly _matchRequest: MatchRequest; diff --git a/src/modules/matcher/tests/unit/default-params.provider.spec.ts b/src/modules/matcher/tests/unit/default-params.provider.spec.ts index 1b3cab7..a721186 100644 --- a/src/modules/matcher/tests/unit/default-params.provider.spec.ts +++ b/src/modules/matcher/tests/unit/default-params.provider.spec.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { DefaultParamsProvider } from '../../adapters/secondaries/default-params.provider'; -import { IDefaultParams } from '../../domain/interfaces/default-params.type'; +import { IDefaultParams } from '../../domain/types/default-params.type'; const mockConfigService = { get: jest.fn().mockImplementationOnce(() => 99), diff --git a/src/modules/matcher/tests/unit/match.query.spec.ts b/src/modules/matcher/tests/unit/match.query.spec.ts index ef5c883..a48c541 100644 --- a/src/modules/matcher/tests/unit/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/match.query.spec.ts @@ -1,9 +1,9 @@ -import { Algorithm } from '../../domain/dtos/algorithm.enum'; import { MatchRequest } from '../../domain/dtos/match.request'; -import { Role } from '../../domain/entities/role.enum'; -import { TimingFrequency } from '../../domain/entities/timing'; -import { IDefaultParams } from '../../domain/interfaces/default-params.type'; +import { Role } from '../../domain/types/role.enum'; +import { TimingFrequency } from '../../domain/types/timing'; +import { IDefaultParams } from '../../domain/types/default-params.type'; import { MatchQuery } from '../../queries/match.query'; +import { Algorithm } from '../../domain/types/algorithm.enum'; const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index c215eb2..8d59092 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -6,8 +6,8 @@ import { MatchQuery } from '../../queries/match.query'; import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; -import { IDefaultParams } from '../../domain/interfaces/default-params.type'; -import { Algorithm } from '../../domain/dtos/algorithm.enum'; +import { IDefaultParams } from '../../domain/types/default-params.type'; +import { Algorithm } from '../../domain/types/algorithm.enum'; const mockAdRepository = {}; From 4c3195390efbfeaaa499236d6340b61e39425f8f Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 12 Apr 2023 17:09:31 +0200 Subject: [PATCH 14/26] add georouter --- package.json | 6 ++- .../secondaries/prisma-repository.abstract.ts | 45 ++++++++++--------- .../tests/unit/prisma-repository.spec.ts | 24 +++++----- .../adapters/secondaries/georouter-creator.ts | 13 ++++++ .../secondaries/graphhopper-georouter.ts | 19 ++++++++ .../domain/interfaces/georouter.interface.ts | 9 +++- .../tests/unit/georouter-creator.spec.ts | 23 ++++++++++ .../tests/unit/graphhopper-georouter.spec.ts | 16 +++++++ 8 files changed, 119 insertions(+), 36 deletions(-) create mode 100644 src/modules/matcher/adapters/secondaries/georouter-creator.ts create mode 100644 src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts create mode 100644 src/modules/matcher/tests/unit/georouter-creator.spec.ts create mode 100644 src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts diff --git a/package.json b/package.json index d9801d5..0d8ec72 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ ".presenter.ts", ".profile.ts", ".exception.ts", - "main.ts" + "main.ts", + "prisma-service.ts" ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", @@ -110,7 +111,8 @@ ".presenter.ts", ".profile.ts", ".exception.ts", - "main.ts" + "main.ts", + "prisma-service.ts" ], "coverageDirectory": "../coverage", "testEnvironment": "node" diff --git a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts index 2827de9..fa2ba59 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; +import { Prisma } from '@prisma/client'; import { DatabaseException } from '../../exceptions/database.exception'; import { ICollection } from '../../interfaces/collection.interface'; import { IRepository } from '../../interfaces/repository.interface'; @@ -45,9 +45,9 @@ export abstract class PrismaRepository implements IRepository { return entity; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -66,8 +66,11 @@ export abstract class PrismaRepository implements IRepository { return entity; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - throw new DatabaseException(PrismaClientKnownRequestError.name, e.code); + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseException( + Prisma.PrismaClientKnownRequestError.name, + e.code, + ); } else { throw new DatabaseException(); } @@ -85,9 +88,9 @@ export abstract class PrismaRepository implements IRepository { return res; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -105,9 +108,9 @@ export abstract class PrismaRepository implements IRepository { }); return updatedEntity; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -131,9 +134,9 @@ export abstract class PrismaRepository implements IRepository { return updatedEntity; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -151,9 +154,9 @@ export abstract class PrismaRepository implements IRepository { return entity; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -171,9 +174,9 @@ export abstract class PrismaRepository implements IRepository { return entity; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -204,9 +207,9 @@ export abstract class PrismaRepository implements IRepository { )}) VALUES (${Object.values(fields).join(',')})`; return await this._prisma.$executeRawUnsafe(command); } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -225,9 +228,9 @@ export abstract class PrismaRepository implements IRepository { )} WHERE uuid = '${uuid}'`; return await this._prisma.$executeRawUnsafe(command); } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); @@ -242,9 +245,9 @@ export abstract class PrismaRepository implements IRepository { await this._prisma.$queryRaw`SELECT 1`; return true; } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseException( - PrismaClientKnownRequestError.name, + Prisma.PrismaClientKnownRequestError.name, e.code, e.message, ); diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index bc194c4..1b0e1f7 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaService } from '../../src/adapters/secondaries/prisma-service'; import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract'; import { DatabaseException } from '../../src/exceptions/database.exception'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; +import { Prisma } from '@prisma/client'; class FakeEntity { uuid?: string; @@ -66,7 +66,7 @@ const mockPrismaService = { .mockResolvedValueOnce(fakeEntityCreated) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((fields: object) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -78,7 +78,7 @@ const mockPrismaService = { .mockResolvedValueOnce(fakeEntityCreated) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((fields: object) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -90,7 +90,7 @@ const mockPrismaService = { $queryRaw: jest .fn() .mockImplementationOnce(() => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -99,7 +99,7 @@ const mockPrismaService = { return true; }) .mockImplementation(() => { - throw new PrismaClientKnownRequestError('Database unavailable', { + throw new Prisma.PrismaClientKnownRequestError('Database unavailable', { code: 'code', clientVersion: 'version', }); @@ -110,7 +110,7 @@ const mockPrismaService = { .mockResolvedValueOnce(fakeEntityCreated) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -139,7 +139,7 @@ const mockPrismaService = { } if (!entity && params?.where?.uuid == 'unknown') { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -161,7 +161,7 @@ const mockPrismaService = { }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -175,14 +175,14 @@ const mockPrismaService = { .fn() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -212,7 +212,7 @@ const mockPrismaService = { .fn() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); @@ -236,7 +236,7 @@ const mockPrismaService = { .fn() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { - throw new PrismaClientKnownRequestError('unknown request', { + throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); diff --git a/src/modules/matcher/adapters/secondaries/georouter-creator.ts b/src/modules/matcher/adapters/secondaries/georouter-creator.ts new file mode 100644 index 0000000..b82b938 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/georouter-creator.ts @@ -0,0 +1,13 @@ +import { Georouter } from '../../domain/interfaces/georouter.interface'; +import { GraphhopperGeorouter } from './graphhopper-georouter'; + +export class GeorouterCreator { + create(type: string, url: string): Georouter { + switch (type) { + case 'graphhopper': + return new GraphhopperGeorouter(url); + default: + throw new Error('Unknown geocoder'); + } + } +} diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts new file mode 100644 index 0000000..06df223 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -0,0 +1,19 @@ +import { Route } from '../../domain/entities/route'; +import { Georouter } from '../../domain/interfaces/georouter.interface'; + +export class GraphhopperGeorouter implements Georouter { + _url: string; + + constructor(url: string) { + this._url = url + '/route?'; + } + + route( + routesRequested: [], + withPoints: boolean, + withTime: boolean, + withDistance: boolean, + ): Route[] { + throw new Error('Method not implemented.'); + } +} diff --git a/src/modules/matcher/domain/interfaces/georouter.interface.ts b/src/modules/matcher/domain/interfaces/georouter.interface.ts index e28617d..c3046e3 100644 --- a/src/modules/matcher/domain/interfaces/georouter.interface.ts +++ b/src/modules/matcher/domain/interfaces/georouter.interface.ts @@ -1,3 +1,10 @@ +import { Route } from '../entities/route'; + export interface Georouter { - type: string; + route( + routesRequested: [], + withPoints: boolean, + withTime: boolean, + withDistance: boolean, + ): Array; } diff --git a/src/modules/matcher/tests/unit/georouter-creator.spec.ts b/src/modules/matcher/tests/unit/georouter-creator.spec.ts new file mode 100644 index 0000000..cea0732 --- /dev/null +++ b/src/modules/matcher/tests/unit/georouter-creator.spec.ts @@ -0,0 +1,23 @@ +import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; +import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter'; + +describe('Georouter creator', () => { + it('should be defined', () => { + const georouterCreator: GeorouterCreator = new GeorouterCreator(); + expect(georouterCreator).toBeDefined(); + }); + it('should create a graphhopper georouter', () => { + const georouterCreator: GeorouterCreator = new GeorouterCreator(); + const georouter = georouterCreator.create( + 'graphhopper', + 'http://localhost', + ); + expect(georouter).toBeInstanceOf(GraphhopperGeorouter); + }); + it('should throw an exception if georouter type is unknown', () => { + const georouterCreator: GeorouterCreator = new GeorouterCreator(); + expect(() => + georouterCreator.create('unknown', 'http://localhost'), + ).toThrow(); + }); +}); diff --git a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts new file mode 100644 index 0000000..40e3ec6 --- /dev/null +++ b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts @@ -0,0 +1,16 @@ +import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter'; + +describe('Graphhopper Georouter', () => { + it('should be defined', () => { + const graphhopperGeorouter: GraphhopperGeorouter = new GraphhopperGeorouter( + 'http://localhost', + ); + expect(graphhopperGeorouter).toBeDefined(); + }); + it('should throw an exception when calling route', () => { + const graphhopperGeorouter: GraphhopperGeorouter = new GraphhopperGeorouter( + 'http://localhost', + ); + expect(() => graphhopperGeorouter.route([], false, false, false)).toThrow(); + }); +}); From ca03d1769aec708d19b27ba08aab7849edf7e24a Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 13 Apr 2023 11:19:04 +0200 Subject: [PATCH 15/26] add georouter creator injection --- .../adapters/primaries/matcher.controller.ts | 8 +++- .../matcher/adapters/primaries/matcher.proto | 4 +- .../adapters/secondaries/georouter-creator.ts | 9 ++-- .../secondaries/graphhopper-georouter.ts | 12 ++--- .../domain/entities/algorithm-settings.ts | 12 +++-- .../interfaces/georouter-creator.interface.ts | 5 ++ .../domain/interfaces/georouter.interface.ts | 10 ++-- .../domain/types/georouter-settings.type.ts | 5 ++ src/modules/matcher/matcher.module.ts | 2 + src/modules/matcher/queries/match.query.ts | 12 ++++- .../tests/unit/graphhopper-georouter.spec.ts | 8 +++- .../matcher/tests/unit/match.query.spec.ts | 46 ++++++++++++++++--- .../matcher/tests/unit/match.usecase.spec.ts | 6 ++- 13 files changed, 104 insertions(+), 35 deletions(-) create mode 100644 src/modules/matcher/domain/interfaces/georouter-creator.interface.ts create mode 100644 src/modules/matcher/domain/types/georouter-settings.type.ts diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 7e001d5..959910c 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -10,6 +10,7 @@ import { Match } from '../../domain/entities/match'; import { MatchQuery } from '../../queries/match.query'; import { MatchPresenter } from '../secondaries/match.presenter'; import { DefaultParamsProvider } from '../secondaries/default-params.provider'; +import { GeorouterCreator } from '../secondaries/georouter-creator'; @UsePipes( new RpcValidationPipe({ @@ -23,13 +24,18 @@ export class MatcherController { private readonly _queryBus: QueryBus, private readonly _defaultParamsProvider: DefaultParamsProvider, @InjectMapper() private readonly _mapper: Mapper, + private readonly _georouterCreator: GeorouterCreator, ) {} @GrpcMethod('MatcherService', 'Match') async match(data: MatchRequest): Promise> { try { const matchCollection = await this._queryBus.execute( - new MatchQuery(data, this._defaultParamsProvider.getParams()), + new MatchQuery( + data, + this._defaultParamsProvider.getParams(), + this._georouterCreator, + ), ); return Promise.resolve({ data: matchCollection.data.map((match: Match) => diff --git a/src/modules/matcher/adapters/primaries/matcher.proto b/src/modules/matcher/adapters/primaries/matcher.proto index f610b21..af4e083 100644 --- a/src/modules/matcher/adapters/primaries/matcher.proto +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -25,8 +25,8 @@ message MatchRequest { int32 proportion = 16; bool useAzimuth = 17; int32 azimuthMargin = 18; - int32 maxDetourDistanceRatio = 19; - int32 maxDetourDurationRatio = 20; + float maxDetourDistanceRatio = 19; + float maxDetourDurationRatio = 20; repeated int32 exclusions = 21; int32 identifier = 22; } diff --git a/src/modules/matcher/adapters/secondaries/georouter-creator.ts b/src/modules/matcher/adapters/secondaries/georouter-creator.ts index b82b938..005fe12 100644 --- a/src/modules/matcher/adapters/secondaries/georouter-creator.ts +++ b/src/modules/matcher/adapters/secondaries/georouter-creator.ts @@ -1,8 +1,11 @@ -import { Georouter } from '../../domain/interfaces/georouter.interface'; +import { Injectable } from '@nestjs/common'; +import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface'; +import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GraphhopperGeorouter } from './graphhopper-georouter'; -export class GeorouterCreator { - create(type: string, url: string): Georouter { +@Injectable() +export class GeorouterCreator implements ICreateGeorouter { + create(type: string, url: string): IGeorouter { switch (type) { case 'graphhopper': return new GraphhopperGeorouter(url); diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 06df223..edc6c31 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -1,19 +1,15 @@ import { Route } from '../../domain/entities/route'; -import { Georouter } from '../../domain/interfaces/georouter.interface'; +import { IGeorouter } from '../../domain/interfaces/georouter.interface'; +import { GeorouterSettings } from '../../domain/types/georouter-settings.type'; -export class GraphhopperGeorouter implements Georouter { +export class GraphhopperGeorouter implements IGeorouter { _url: string; constructor(url: string) { this._url = url + '/route?'; } - route( - routesRequested: [], - withPoints: boolean, - withTime: boolean, - withDistance: boolean, - ): Route[] { + route(routesRequested: [], settings: GeorouterSettings): Route[] { throw new Error('Method not implemented.'); } } diff --git a/src/modules/matcher/domain/entities/algorithm-settings.ts b/src/modules/matcher/domain/entities/algorithm-settings.ts index 1a79bb5..7378baa 100644 --- a/src/modules/matcher/domain/entities/algorithm-settings.ts +++ b/src/modules/matcher/domain/entities/algorithm-settings.ts @@ -2,6 +2,8 @@ import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-requ import { DefaultAlgorithmSettings } from '../types/default-algorithm-settings.type'; import { Algorithm } from '../types/algorithm.enum'; import { TimingFrequency } from '../types/timing'; +import { ICreateGeorouter } from '../interfaces/georouter-creator.interface'; +import { IGeorouter } from '../interfaces/georouter.interface'; export class AlgorithmSettings { _algorithmSettingsRequest: IRequestAlgorithmSettings; @@ -15,13 +17,13 @@ export class AlgorithmSettings { azimuthMargin: number; maxDetourDurationRatio: number; maxDetourDistanceRatio: number; - georouterType: string; - georouterUrl: string; + georouter: IGeorouter; constructor( algorithmSettingsRequest: IRequestAlgorithmSettings, defaultAlgorithmSettings: DefaultAlgorithmSettings, frequency: TimingFrequency, + georouterCreator: ICreateGeorouter, ) { this._algorithmSettingsRequest = algorithmSettingsRequest; this.algorithm = @@ -49,8 +51,10 @@ export class AlgorithmSettings { this.maxDetourDurationRatio = algorithmSettingsRequest.maxDetourDurationRatio ?? defaultAlgorithmSettings.maxDetourDurationRatio; - this.georouterType = defaultAlgorithmSettings.georouterType; - this.georouterUrl = defaultAlgorithmSettings.georouterUrl; + this.georouter = georouterCreator.create( + defaultAlgorithmSettings.georouterType, + defaultAlgorithmSettings.georouterUrl, + ); if (this._strict) { this.restrict = frequency; } diff --git a/src/modules/matcher/domain/interfaces/georouter-creator.interface.ts b/src/modules/matcher/domain/interfaces/georouter-creator.interface.ts new file mode 100644 index 0000000..7a6bd25 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/georouter-creator.interface.ts @@ -0,0 +1,5 @@ +import { IGeorouter } from './georouter.interface'; + +export interface ICreateGeorouter { + create(type: string, url: string): IGeorouter; +} diff --git a/src/modules/matcher/domain/interfaces/georouter.interface.ts b/src/modules/matcher/domain/interfaces/georouter.interface.ts index c3046e3..05494a4 100644 --- a/src/modules/matcher/domain/interfaces/georouter.interface.ts +++ b/src/modules/matcher/domain/interfaces/georouter.interface.ts @@ -1,10 +1,6 @@ import { Route } from '../entities/route'; +import { GeorouterSettings } from '../types/georouter-settings.type'; -export interface Georouter { - route( - routesRequested: [], - withPoints: boolean, - withTime: boolean, - withDistance: boolean, - ): Array; +export interface IGeorouter { + route(routesRequested: [], settings: GeorouterSettings): Array; } diff --git a/src/modules/matcher/domain/types/georouter-settings.type.ts b/src/modules/matcher/domain/types/georouter-settings.type.ts new file mode 100644 index 0000000..d8f73ae --- /dev/null +++ b/src/modules/matcher/domain/types/georouter-settings.type.ts @@ -0,0 +1,5 @@ +export type GeorouterSettings = { + withPoints: boolean; + withTime: boolean; + withDistance: boolean; +}; diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index 2bb67a3..25fde2c 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -12,6 +12,7 @@ import { CacheModule } from '@nestjs/cache-manager'; import { RedisClientOptions } from '@liaoliaots/nestjs-redis'; import { redisStore } from 'cache-manager-ioredis-yet'; import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider'; +import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; @Module({ imports: [ @@ -51,6 +52,7 @@ import { DefaultParamsProvider } from './adapters/secondaries/default-params.pro Messager, DefaultParamsProvider, MatchUseCase, + GeorouterCreator, ], exports: [], }) diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index fdec447..55381ab 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -6,10 +6,13 @@ import { Role } from '../domain/types/role.enum'; import { AlgorithmSettings } from '../domain/entities/algorithm-settings'; import { Time } from '../domain/entities/time'; import { IDefaultParams } from '../domain/types/default-params.type'; +import { IGeorouter } from '../domain/interfaces/georouter.interface'; +import { ICreateGeorouter } from '../domain/interfaces/georouter-creator.interface'; export class MatchQuery { private readonly _matchRequest: MatchRequest; private readonly _defaultParams: IDefaultParams; + private readonly _georouterCreator: ICreateGeorouter; person: Person; roles: Array; time: Time; @@ -17,10 +20,16 @@ export class MatchQuery { exclusions: Array; requirement: Requirement; algorithmSettings: AlgorithmSettings; + georouter: IGeorouter; - constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { + constructor( + matchRequest: MatchRequest, + defaultParams: IDefaultParams, + georouterCreator: ICreateGeorouter, + ) { this._matchRequest = matchRequest; this._defaultParams = defaultParams; + this._georouterCreator = georouterCreator; this._setPerson(); this._setRoles(); this._setTime(); @@ -75,6 +84,7 @@ export class MatchQuery { this._matchRequest, this._defaultParams.DEFAULT_ALGORITHM_SETTINGS, this.time.frequency, + this._georouterCreator, ); } diff --git a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts index 40e3ec6..fb94d14 100644 --- a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts +++ b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts @@ -11,6 +11,12 @@ describe('Graphhopper Georouter', () => { const graphhopperGeorouter: GraphhopperGeorouter = new GraphhopperGeorouter( 'http://localhost', ); - expect(() => graphhopperGeorouter.route([], false, false, false)).toThrow(); + expect(() => + graphhopperGeorouter.route([], { + withDistance: false, + withPoints: false, + withTime: false, + }), + ).toThrow(); }); }); diff --git a/src/modules/matcher/tests/unit/match.query.spec.ts b/src/modules/matcher/tests/unit/match.query.spec.ts index a48c541..6e9182f 100644 --- a/src/modules/matcher/tests/unit/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/match.query.spec.ts @@ -26,6 +26,10 @@ const defaultParams: IDefaultParams = { }, }; +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + describe('Match query', () => { it('should be defined', () => { const matchRequest: MatchRequest = new MatchRequest(); @@ -40,7 +44,11 @@ describe('Match query', () => { lon: 3.045432, }, ]; - const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + ); expect(matchQuery).toBeDefined(); }); @@ -59,7 +67,11 @@ describe('Match query', () => { ]; matchRequest.identifier = 125; matchRequest.exclusions = [126, 127, 128]; - const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + ); expect(matchQuery.exclusions.length).toBe(4); }); @@ -77,7 +89,11 @@ describe('Match query', () => { }, ]; matchRequest.driver = true; - const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + ); expect(matchQuery.roles).toEqual([Role.DRIVER]); }); @@ -95,7 +111,11 @@ describe('Match query', () => { }, ]; matchRequest.passenger = true; - const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + ); expect(matchQuery.roles).toEqual([Role.PASSENGER]); }); @@ -114,7 +134,11 @@ describe('Match query', () => { ]; matchRequest.passenger = true; matchRequest.driver = true; - const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + ); expect(matchQuery.roles.length).toBe(2); expect(matchQuery.roles).toContain(Role.PASSENGER); expect(matchQuery.roles).toContain(Role.DRIVER); @@ -135,7 +159,11 @@ describe('Match query', () => { ]; matchRequest.seatsDriver = 1; matchRequest.seatsPassenger = 2; - const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + ); expect(matchQuery.requirement.seatsDriver).toBe(1); expect(matchQuery.requirement.seatsPassenger).toBe(2); }); @@ -162,7 +190,11 @@ describe('Match query', () => { matchRequest.remoteness = 20000; matchRequest.maxDetourDistanceRatio = 0.41; matchRequest.maxDetourDurationRatio = 0.42; - const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + ); expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC); expect(matchQuery.algorithmSettings.restrict).toBe( TimingFrequency.FREQUENCY_PUNCTUAL, diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index 8d59092..dace03d 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -15,6 +15,10 @@ const mockMessager = { publish: jest.fn().mockImplementation(), }; +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -77,7 +81,7 @@ describe('MatchUseCase', () => { ]; matchRequest.departure = '2023-04-01 12:23:00'; const matches = await matchUseCase.execute( - new MatchQuery(matchRequest, defaultParams), + new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), ); expect(matches.total).toBe(1); }); From 10a9b9458860fb31071479487ee135db9ad0aef9 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 17 Apr 2023 10:48:09 +0200 Subject: [PATCH 16/26] try to launch thousands of request in parallel --- package-lock.json | 53 ++++++- package.json | 2 + .../adapters/secondaries/georouter-creator.ts | 5 +- .../secondaries/graphhopper-georouter.ts | 126 ++++++++++++++- .../matcher/domain/entities/geography.ts | 2 +- .../matcher/domain/entities/named-route.ts | 6 + .../domain/interfaces/georouter.interface.ts | 8 +- src/modules/matcher/domain/types/path.type.ts | 6 + .../matcher/domain/usecases/match.usecase.ts | 49 +++++- src/modules/matcher/matcher.module.ts | 2 + .../tests/unit/georouter-creator.spec.ts | 24 ++- .../tests/unit/graphhopper-georouter.spec.ts | 147 ++++++++++++++++-- 12 files changed, 392 insertions(+), 38 deletions(-) create mode 100644 src/modules/matcher/domain/entities/named-route.ts create mode 100644 src/modules/matcher/domain/types/path.type.ts diff --git a/package-lock.json b/package-lock.json index 0d2f171..3efcc5e 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/axios": "^2.0.0", "@nestjs/cache-manager": "^1.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.1", @@ -25,6 +26,7 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/terminus": "^9.2.2", "@prisma/client": "^4.12.0", + "axios": "^1.3.5", "cache-manager": "^5.2.0", "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", @@ -1572,6 +1574,17 @@ "node": ">=8" } }, + "node_modules/@nestjs/axios": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-2.0.0.tgz", + "integrity": "sha512-F6oceoQLEn031uun8NiommeMkRIojQqVryxQy/mK7fx0CI0KbgkJL3SloCQcsOD+agoEnqKJKXZpEvL6FNswJg==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "axios": "^1.3.1", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cache-manager": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-1.0.0.tgz", @@ -3060,8 +3073,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", + "integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.5.0", @@ -3757,7 +3779,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3955,7 +3976,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4795,6 +4815,25 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", @@ -4827,7 +4866,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7232,6 +7270,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index 0d8ec72..e304cb9 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/axios": "^2.0.0", "@nestjs/cache-manager": "^1.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.1", @@ -47,6 +48,7 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/terminus": "^9.2.2", "@prisma/client": "^4.12.0", + "axios": "^1.3.5", "cache-manager": "^5.2.0", "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", diff --git a/src/modules/matcher/adapters/secondaries/georouter-creator.ts b/src/modules/matcher/adapters/secondaries/georouter-creator.ts index 005fe12..e87c1fa 100644 --- a/src/modules/matcher/adapters/secondaries/georouter-creator.ts +++ b/src/modules/matcher/adapters/secondaries/georouter-creator.ts @@ -2,13 +2,16 @@ import { Injectable } from '@nestjs/common'; import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface'; import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GraphhopperGeorouter } from './graphhopper-georouter'; +import { HttpService } from '@nestjs/axios'; @Injectable() export class GeorouterCreator implements ICreateGeorouter { + constructor(private readonly httpService: HttpService) {} + create(type: string, url: string): IGeorouter { switch (type) { case 'graphhopper': - return new GraphhopperGeorouter(url); + return new GraphhopperGeorouter(url, this.httpService); default: throw new Error('Unknown geocoder'); } diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index edc6c31..d08583e 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -1,15 +1,133 @@ -import { Route } from '../../domain/entities/route'; +import { HttpService } from '@nestjs/axios'; +import { NamedRoute } from '../../domain/entities/named-route'; import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GeorouterSettings } from '../../domain/types/georouter-settings.type'; +import { Path } from '../../domain/types/path.type'; +import { Injectable } from '@nestjs/common'; +import { + catchError, + defer, + forkJoin, + from, + lastValueFrom, + map, + mergeAll, + mergeMap, + toArray, +} from 'rxjs'; +import { AxiosError } from 'axios'; +@Injectable() export class GraphhopperGeorouter implements IGeorouter { _url: string; + _urlArgs: Array; + _withTime: boolean; + _withPoints: boolean; + _withDistance: boolean; + _paths: Array; + _httpService: HttpService; - constructor(url: string) { + constructor(url: string, httpService: HttpService) { this._url = url + '/route?'; + this._httpService = httpService; } - route(routesRequested: [], settings: GeorouterSettings): Route[] { - throw new Error('Method not implemented.'); + async route( + paths: Array, + settings: GeorouterSettings, + ): Promise> { + this._setDefaultUrlArgs(); + this._setWithTime(settings.withTime); + this._setWithPoints(settings.withPoints); + this._setWithDistance(settings.withDistance); + this._paths = paths; + const routes = await this._getRoutes(); + console.log(routes.length); + return routes; + } + + _setDefaultUrlArgs(): void { + this._urlArgs = [ + 'vehicle=car', + 'weighting=fastest', + 'points_encoded=false', + ]; + } + + _setWithTime(withTime: boolean): void { + this._withTime = withTime; + if (withTime) { + this._urlArgs.push('details=time'); + } + } + + _setWithPoints(withPoints: boolean): void { + this._withPoints = withPoints; + if (withPoints) { + this._urlArgs.push('calc_points=false'); + } + } + + _setWithDistance(withDistance: boolean): void { + this._withDistance = withDistance; + if (withDistance) { + this._urlArgs.push('instructions=true'); + } else { + this._urlArgs.push('instructions=false'); + } + } + + async _getRoutes(): Promise> { + const routes = Promise.all( + this._paths.map(async (path) => { + const url: string = [ + this._getUrl(), + '&point=', + path.points + .map((point) => [point.lat, point.lon].join()) + .join('&point='), + ].join(''); + const res = await lastValueFrom( + this._httpService.get(url).pipe( + map((res) => res.data.paths[0].distance), + catchError((error: AxiosError) => { + throw new Error(error.message); + }), + ), + ); + return { + key: path.key, + route: res, + }; + }), + ); + return routes; + // const date1 = new Date(); + // const urls = this._paths.map((path) => + // defer(() => + // this._httpService + // .get( + // [ + // this._getUrl(), + // '&point=', + // path.points + // .map((point) => [point.lat, point.lon].join()) + // .join('&point='), + // ].join(''), + // ) + // .pipe(map((res) => res.data.paths[0].distance)), + // ), + // ); + // const observables = from(urls); + // const routes = observables.pipe(mergeAll(7), toArray()); + // routes.subscribe(() => { + // const date2 = new Date(); + // console.log(date2.getTime() - date1.getTime()); + // }); + // return []; + } + + _getUrl(): string { + return [this._url, this._urlArgs.join('&')].join(''); } } diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/geography.ts index 3cf3697..506e9b3 100644 --- a/src/modules/matcher/domain/entities/geography.ts +++ b/src/modules/matcher/domain/entities/geography.ts @@ -2,9 +2,9 @@ import { MatcherException } from '../../exceptions/matcher.exception'; import { IRequestGeography } from '../interfaces/geography-request.interface'; import { PointType } from '../types/geography.enum'; import { Point } from '../types/point.type'; -import { Route } from './route'; import { find } from 'geo-tz'; import { Waypoint } from '../types/waypoint'; +import { Route } from './route'; export class Geography { _geographyRequest: IRequestGeography; diff --git a/src/modules/matcher/domain/entities/named-route.ts b/src/modules/matcher/domain/entities/named-route.ts new file mode 100644 index 0000000..c75c4f1 --- /dev/null +++ b/src/modules/matcher/domain/entities/named-route.ts @@ -0,0 +1,6 @@ +import { Route } from './route'; + +export class NamedRoute { + key: string; + route: Route; +} diff --git a/src/modules/matcher/domain/interfaces/georouter.interface.ts b/src/modules/matcher/domain/interfaces/georouter.interface.ts index 05494a4..8f17972 100644 --- a/src/modules/matcher/domain/interfaces/georouter.interface.ts +++ b/src/modules/matcher/domain/interfaces/georouter.interface.ts @@ -1,6 +1,10 @@ -import { Route } from '../entities/route'; +import { NamedRoute } from '../entities/named-route'; import { GeorouterSettings } from '../types/georouter-settings.type'; +import { Path } from '../types/path.type'; export interface IGeorouter { - route(routesRequested: [], settings: GeorouterSettings): Array; + route( + paths: Array, + settings: GeorouterSettings, + ): Promise>; } diff --git a/src/modules/matcher/domain/types/path.type.ts b/src/modules/matcher/domain/types/path.type.ts new file mode 100644 index 0000000..8a1bfe9 --- /dev/null +++ b/src/modules/matcher/domain/types/path.type.ts @@ -0,0 +1,6 @@ +import { Point } from './point.type'; + +export type Path = { + key: string; + points: Array; +}; diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index e925367..f67641b 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -17,21 +17,54 @@ export class MatchUseCase { async execute(matchQuery: MatchQuery): Promise> { try { + const paths = []; + for (let i = 0; i < 2000; i++) { + paths.push({ + key: 'route' + i, + points: [ + { + lat: 48.110899, + lon: -1.68365, + }, + { + lat: 48.131105, + lon: -1.690067, + }, + { + lat: 48.56516, + lon: -1.923553, + }, + { + lat: 48.622813, + lon: -1.997177, + }, + { + lat: 48.67846, + lon: -1.8554, + }, + ], + }); + } + const routes = await matchQuery.algorithmSettings.georouter.route(paths, { + withDistance: true, + withPoints: true, + withTime: false, + }); const match = new Match(); match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; - this._messager.publish('matcher.match', 'match !'); + // this._messager.publish('matcher.match', 'match !'); return { data: [match], total: 1, }; } catch (error) { - this._messager.publish( - 'logging.matcher.match.crit', - JSON.stringify({ - matchQuery, - error, - }), - ); + // this._messager.publish( + // 'logging.matcher.match.crit', + // JSON.stringify({ + // matchQuery, + // error, + // }), + // ); throw error; } } diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index 25fde2c..7854892 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -13,11 +13,13 @@ import { RedisClientOptions } from '@liaoliaots/nestjs-redis'; import { redisStore } from 'cache-manager-ioredis-yet'; import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider'; import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; +import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ DatabaseModule, CqrsModule, + HttpModule, RabbitMQModule.forRootAsync(RabbitMQModule, { imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ diff --git a/src/modules/matcher/tests/unit/georouter-creator.spec.ts b/src/modules/matcher/tests/unit/georouter-creator.spec.ts index cea0732..b7a7d6d 100644 --- a/src/modules/matcher/tests/unit/georouter-creator.spec.ts +++ b/src/modules/matcher/tests/unit/georouter-creator.spec.ts @@ -1,13 +1,32 @@ +import { Test, TestingModule } from '@nestjs/testing'; import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter'; +import { HttpService } from '@nestjs/axios'; + +const mockHttpService = jest.fn(); describe('Georouter creator', () => { + let georouterCreator: GeorouterCreator; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + GeorouterCreator, + { + provide: HttpService, + useValue: mockHttpService, + }, + ], + }).compile(); + + georouterCreator = module.get(GeorouterCreator); + }); + it('should be defined', () => { - const georouterCreator: GeorouterCreator = new GeorouterCreator(); expect(georouterCreator).toBeDefined(); }); it('should create a graphhopper georouter', () => { - const georouterCreator: GeorouterCreator = new GeorouterCreator(); const georouter = georouterCreator.create( 'graphhopper', 'http://localhost', @@ -15,7 +34,6 @@ describe('Georouter creator', () => { expect(georouter).toBeInstanceOf(GraphhopperGeorouter); }); it('should throw an exception if georouter type is unknown', () => { - const georouterCreator: GeorouterCreator = new GeorouterCreator(); expect(() => georouterCreator.create('unknown', 'http://localhost'), ).toThrow(); diff --git a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts index fb94d14..64cc698 100644 --- a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts +++ b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts @@ -1,22 +1,141 @@ -import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter'; +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpService } from '@nestjs/axios'; +import { NamedRoute } from '../../domain/entities/named-route'; +import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; +import { IGeorouter } from '../../domain/interfaces/georouter.interface'; +import { of } from 'rxjs'; +import { AxiosError } from 'axios'; + +const mockHttpService = { + get: jest + .fn() + .mockImplementationOnce(() => { + throw new AxiosError('Axios error !'); + }) + .mockImplementation(() => { + return of({ + status: 200, + data: [new NamedRoute()], + }); + }), +}; describe('Graphhopper Georouter', () => { - it('should be defined', () => { - const graphhopperGeorouter: GraphhopperGeorouter = new GraphhopperGeorouter( + let georouterCreator: GeorouterCreator; + let graphhopperGeorouter: IGeorouter; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + GeorouterCreator, + { + provide: HttpService, + useValue: mockHttpService, + }, + ], + }).compile(); + + georouterCreator = module.get(GeorouterCreator); + graphhopperGeorouter = georouterCreator.create( + 'graphhopper', 'http://localhost', ); + }); + + it('should be defined', () => { expect(graphhopperGeorouter).toBeDefined(); }); - it('should throw an exception when calling route', () => { - const graphhopperGeorouter: GraphhopperGeorouter = new GraphhopperGeorouter( - 'http://localhost', - ); - expect(() => - graphhopperGeorouter.route([], { - withDistance: false, - withPoints: false, - withTime: false, - }), - ).toThrow(); + + describe('route function', () => { + it('should fail on axios error', async () => { + await expect( + graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ), + ).rejects.toBeInstanceOf(Error); + }); + it('should create one route with all settings to false', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ); + expect(routes).toHaveLength(1); + }); + it('should create 2 routes with distance, points and time', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + { + key: 'route2', + points: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + ], + { + withDistance: true, + withPoints: true, + withTime: true, + }, + ); + expect(routes).toHaveLength(2); + }); }); }); From c759e81c23ab367d0e20ef27afae224303cd7b2f Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 19 Apr 2023 17:32:42 +0200 Subject: [PATCH 17/26] WIP graphhopper georouter --- package-lock.json | 266 ++++++++++++++++- package.json | 4 + .../matcher/adapters/secondaries/geodesic.ts | 27 ++ .../adapters/secondaries/georouter-creator.ts | 8 +- .../secondaries/graphhopper-georouter.ts | 274 +++++++++++++++--- src/modules/matcher/domain/entities/actor.ts | 9 + src/modules/matcher/domain/entities/route.ts | 56 +++- .../domain/entities/spacetime-point.ts | 11 + .../matcher/domain/entities/waypoint.ts | 6 + .../domain/interfaces/geodesic.interface.ts | 11 + .../matcher/domain/usecases/match.usecase.ts | 87 +++--- src/modules/matcher/matcher.module.ts | 2 + .../default-params.provider.spec.ts | 4 +- .../adapters/secondaries/geodesic.spec.ts | 14 + .../secondaries}/georouter-creator.spec.ts | 10 +- .../secondaries/graphhopper-georouter.spec.ts | 268 +++++++++++++++++ .../secondaries}/messager.spec.ts | 2 +- .../tests/unit/{ => domain}/geography.spec.ts | 2 +- .../unit/{ => domain}/match.usecase.spec.ts | 14 +- .../tests/unit/{ => domain}/person.spec.ts | 2 +- .../matcher/tests/unit/domain/route.spec.ts | 55 ++++ .../tests/unit/{ => domain}/time.spec.ts | 2 +- .../tests/unit/graphhopper-georouter.spec.ts | 141 --------- .../unit/{ => queries}/match.query.spec.ts | 12 +- 24 files changed, 1033 insertions(+), 254 deletions(-) create mode 100644 src/modules/matcher/adapters/secondaries/geodesic.ts create mode 100644 src/modules/matcher/domain/entities/actor.ts create mode 100644 src/modules/matcher/domain/entities/spacetime-point.ts create mode 100644 src/modules/matcher/domain/entities/waypoint.ts create mode 100644 src/modules/matcher/domain/interfaces/geodesic.interface.ts rename src/modules/matcher/tests/unit/{ => adapters/secondaries}/default-params.provider.spec.ts (84%) create mode 100644 src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts rename src/modules/matcher/tests/unit/{ => adapters/secondaries}/georouter-creator.spec.ts (72%) create mode 100644 src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts rename src/modules/matcher/tests/unit/{ => adapters/secondaries}/messager.spec.ts (94%) rename src/modules/matcher/tests/unit/{ => domain}/geography.spec.ts (97%) rename src/modules/matcher/tests/unit/{ => domain}/match.usecase.spec.ts (80%) rename src/modules/matcher/tests/unit/{ => domain}/person.spec.ts (94%) create mode 100644 src/modules/matcher/tests/unit/domain/route.spec.ts rename src/modules/matcher/tests/unit/{ => domain}/time.spec.ts (98%) delete mode 100644 src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts rename src/modules/matcher/tests/unit/{ => queries}/match.query.spec.ts (93%) diff --git a/package-lock.json b/package-lock.json index 3efcc5e..8a64409 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "geo-tz": "^7.0.7", + "geographiclib-geodesic": "^2.0.0", + "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -2129,6 +2131,17 @@ "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "dev": true }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", @@ -2147,6 +2160,17 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2253,6 +2277,17 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -2326,6 +2361,11 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2366,6 +2406,14 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -2406,6 +2454,14 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -3493,6 +3549,45 @@ "node": ">=12" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3735,6 +3830,17 @@ "node": ">=0.8" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -3939,6 +4045,31 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -3972,6 +4103,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4122,7 +4261,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -5016,6 +5154,11 @@ "node": ">= 6" } }, + "node_modules/geographiclib-geodesic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/geographiclib-geodesic/-/geographiclib-geodesic-2.0.0.tgz", + "integrity": "sha512-qRE11UEF3Zn9VwDFf+Q1ZNn4VW2xwZWeAPiFRrKVSKn2K5lds1jOxhxgFJwbKh5YV58ME6+LGiRtm4A0CjFyiQ==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5131,6 +5274,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5188,6 +5355,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -5203,6 +5375,18 @@ "node": ">= 0.8" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6215,6 +6399,11 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6263,6 +6452,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6387,6 +6584,14 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6559,6 +6764,14 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6725,6 +6938,17 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -6768,7 +6992,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -6853,6 +7076,14 @@ "node": ">=0.10.0" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7279,7 +7510,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7349,6 +7579,17 @@ } ] }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7492,6 +7733,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -7539,6 +7785,17 @@ "node": ">=10" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -8956,8 +9213,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index e304cb9..34f162f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "geo-tz": "^7.0.7", + "geographiclib-geodesic": "^2.0.0", + "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -95,6 +97,7 @@ ".presenter.ts", ".profile.ts", ".exception.ts", + ".enum.ts", "main.ts", "prisma-service.ts" ], @@ -113,6 +116,7 @@ ".presenter.ts", ".profile.ts", ".exception.ts", + ".enum.ts", "main.ts", "prisma-service.ts" ], diff --git a/src/modules/matcher/adapters/secondaries/geodesic.ts b/src/modules/matcher/adapters/secondaries/geodesic.ts new file mode 100644 index 0000000..56423a8 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/geodesic.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; +import { Geodesic, GeodesicClass } from 'geographiclib-geodesic'; + +@Injectable() +export class MatcherGeodesic implements IGeodesic { + _geod: GeodesicClass; + + constructor() { + this._geod = Geodesic.WGS84; + } + + inverse( + lon1: number, + lat1: number, + lon2: number, + lat2: number, + ): { azimuth: number; distance: number } { + const { azi2: azimuth, s12: distance } = this._geod.Inverse( + lat1, + lon1, + lat2, + lon2, + ); + return { azimuth, distance }; + } +} diff --git a/src/modules/matcher/adapters/secondaries/georouter-creator.ts b/src/modules/matcher/adapters/secondaries/georouter-creator.ts index e87c1fa..22e65b3 100644 --- a/src/modules/matcher/adapters/secondaries/georouter-creator.ts +++ b/src/modules/matcher/adapters/secondaries/georouter-creator.ts @@ -3,15 +3,19 @@ import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.inte import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GraphhopperGeorouter } from './graphhopper-georouter'; import { HttpService } from '@nestjs/axios'; +import { MatcherGeodesic } from './geodesic'; @Injectable() export class GeorouterCreator implements ICreateGeorouter { - constructor(private readonly httpService: HttpService) {} + constructor( + private readonly httpService: HttpService, + private readonly geodesic: MatcherGeodesic, + ) {} create(type: string, url: string): IGeorouter { switch (type) { case 'graphhopper': - return new GraphhopperGeorouter(url, this.httpService); + return new GraphhopperGeorouter(url, this.httpService, this.geodesic); default: throw new Error('Unknown geocoder'); } diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index d08583e..87e2129 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -4,18 +4,11 @@ import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GeorouterSettings } from '../../domain/types/georouter-settings.type'; import { Path } from '../../domain/types/path.type'; import { Injectable } from '@nestjs/common'; -import { - catchError, - defer, - forkJoin, - from, - lastValueFrom, - map, - mergeAll, - mergeMap, - toArray, -} from 'rxjs'; -import { AxiosError } from 'axios'; +import { catchError, lastValueFrom, map } from 'rxjs'; +import { AxiosError, AxiosResponse } from 'axios'; +import { Route } from '../../domain/entities/route'; +import { SpacetimePoint } from '../../domain/entities/spacetime-point'; +import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; @Injectable() export class GraphhopperGeorouter implements IGeorouter { @@ -26,10 +19,12 @@ export class GraphhopperGeorouter implements IGeorouter { _withDistance: boolean; _paths: Array; _httpService: HttpService; + _geodesic: IGeodesic; - constructor(url: string, httpService: HttpService) { + constructor(url: string, httpService: HttpService, geodesic: IGeodesic) { this._url = url + '/route?'; this._httpService = httpService; + this._geodesic = geodesic; } async route( @@ -41,9 +36,7 @@ export class GraphhopperGeorouter implements IGeorouter { this._setWithPoints(settings.withPoints); this._setWithDistance(settings.withDistance); this._paths = paths; - const routes = await this._getRoutes(); - console.log(routes.length); - return routes; + return await this._getRoutes(); } _setDefaultUrlArgs(): void { @@ -63,7 +56,7 @@ export class GraphhopperGeorouter implements IGeorouter { _setWithPoints(withPoints: boolean): void { this._withPoints = withPoints; - if (withPoints) { + if (!withPoints) { this._urlArgs.push('calc_points=false'); } } @@ -87,9 +80,11 @@ export class GraphhopperGeorouter implements IGeorouter { .map((point) => [point.lat, point.lon].join()) .join('&point='), ].join(''); - const res = await lastValueFrom( + const route = await lastValueFrom( this._httpService.get(url).pipe( - map((res) => res.data.paths[0].distance), + map((res) => + res.data ? this._createRoute(path.key, res) : undefined, + ), catchError((error: AxiosError) => { throw new Error(error.message); }), @@ -97,37 +92,230 @@ export class GraphhopperGeorouter implements IGeorouter { ); return { key: path.key, - route: res, + route, }; }), ); return routes; - // const date1 = new Date(); - // const urls = this._paths.map((path) => - // defer(() => - // this._httpService - // .get( - // [ - // this._getUrl(), - // '&point=', - // path.points - // .map((point) => [point.lat, point.lon].join()) - // .join('&point='), - // ].join(''), - // ) - // .pipe(map((res) => res.data.paths[0].distance)), - // ), - // ); - // const observables = from(urls); - // const routes = observables.pipe(mergeAll(7), toArray()); - // routes.subscribe(() => { - // const date2 = new Date(); - // console.log(date2.getTime() - date1.getTime()); - // }); - // return []; } _getUrl(): string { return [this._url, this._urlArgs.join('&')].join(''); } + + _createRoute( + key: string, + response: AxiosResponse, + ): Route { + const route = new Route(this._geodesic); + if (response.data.paths && response.data.paths[0]) { + const shortestPath = response.data.paths[0]; + route.distance = shortestPath.distance ?? 0; + route.duration = shortestPath.time ? shortestPath.time / 1000 : 0; + if (shortestPath.points && shortestPath.points.coordinates) { + route.setPoints(shortestPath.points.coordinates); + if ( + shortestPath.details && + shortestPath.details.time && + shortestPath.snapped_waypoints && + shortestPath.snapped_waypoints.coordinates + ) { + let instructions: Array = []; + if (shortestPath.instructions) + instructions = shortestPath.instructions; + route.setSpacetimePoints( + this._generateSpacetimePoints( + shortestPath.points.coordinates, + shortestPath.snapped_waypoints.coordinates, + shortestPath.details.time, + instructions, + ), + ); + } + } + } + return route; + } + + _generateSpacetimePoints( + points: Array>, + snappedWaypoints: Array>, + durations: Array>, + instructions: Array, + ): Array { + const indices = this._getIndices(points, snappedWaypoints); + const times = this._getTimes(durations, indices); + const distances = this._getDistances(instructions, indices); + return indices.map( + (index) => + new SpacetimePoint( + points[index], + times.find((time) => time.index == index)?.duration, + distances.find((distance) => distance.index == index)?.distance, + ), + ); + } + + _getIndices( + points: Array>, + snappedWaypoints: Array>, + ): Array { + const indices = snappedWaypoints.map((waypoint) => + points.findIndex( + (point) => point[0] == waypoint[0] && point[1] == waypoint[1], + ), + ); + if (indices.find((index) => index == -1) === undefined) return indices; + const missedWaypoints = indices + .map( + (value, index) => + < + { + index: number; + originIndex: number; + waypoint: Array; + nearest: number; + distance: number; + } + >{ + index: value, + originIndex: index, + waypoint: snappedWaypoints[index], + nearest: undefined, + distance: 999999999, + }, + ) + .filter((element) => element.index == -1); + for (const index in points) { + for (const missedWaypoint of missedWaypoints) { + const inverse = this._geodesic.inverse( + missedWaypoint.waypoint[0], + missedWaypoint.waypoint[1], + points[index][0], + points[index][1], + ); + if (inverse.distance < missedWaypoint.distance) { + missedWaypoint.distance = inverse.distance; + missedWaypoint.nearest = parseInt(index); + } + } + } + for (const missedWaypoint of missedWaypoints) { + indices[missedWaypoint.originIndex] = missedWaypoint.nearest; + } + return indices; + } + + _getTimes( + durations: Array>, + indices: Array, + ): Array<{ index: number; duration: number }> { + const times: Array<{ index: number; duration: number }> = []; + let duration = 0; + for (const [origin, destination, stepDuration] of durations) { + let indexFound = false; + const indexAsOrigin = indices.find((index) => index == origin); + if ( + indexAsOrigin !== undefined && + times.find((time) => origin == time.index) == undefined + ) { + times.push({ + index: indexAsOrigin, + duration: Math.round(stepDuration / 1000), + }); + indexFound = true; + } + if (!indexFound) { + const indexAsDestination = indices.find( + (index) => index == destination, + ); + if ( + indexAsDestination !== undefined && + times.find((time) => destination == time.index) == undefined + ) { + times.push({ + index: indexAsDestination, + duration: Math.round((duration + stepDuration) / 1000), + }); + indexFound = true; + } + } + if (!indexFound) { + const indexInBetween = indices.find( + (index) => origin < index && index < destination, + ); + if (indexInBetween !== undefined) { + times.push({ + index: indexInBetween, + duration: Math.round((duration + stepDuration / 2) / 1000), + }); + } + } + duration += stepDuration; + } + return times; + } + + _getDistances( + instructions: Array, + indices: Array, + ): Array<{ index: number; distance: number }> { + let distance = 0; + const distances: Array<{ index: number; distance: number }> = [ + { + index: 0, + distance, + }, + ]; + for (const instruction of instructions) { + distance += instruction.distance; + if ( + (instruction.sign == GraphhopperSign.SIGN_WAYPOINT || + instruction.sign == GraphhopperSign.SIGN_FINISH) && + indices.find((index) => index == instruction.interval[0]) !== undefined + ) { + distances.push({ + index: instruction.interval[0], + distance: Math.round(distance), + }); + } + } + return distances; + } +} + +type GraphhopperResponse = { + paths: [ + { + distance: number; + weight: number; + time: number; + points_encoded: boolean; + bbox: Array; + points: GraphhopperCoordinates; + snapped_waypoints: GraphhopperCoordinates; + details: { + time: Array>; + }; + instructions: Array; + }, + ]; +}; + +type GraphhopperCoordinates = { + coordinates: Array>; +}; + +type GraphhopperInstruction = { + distance: number; + heading: number; + sign: GraphhopperSign; + interval: Array; + text: string; +}; + +enum GraphhopperSign { + SIGN_START = 0, + SIGN_FINISH = 4, + SIGN_WAYPOINT = 5, } diff --git a/src/modules/matcher/domain/entities/actor.ts b/src/modules/matcher/domain/entities/actor.ts new file mode 100644 index 0000000..de9448a --- /dev/null +++ b/src/modules/matcher/domain/entities/actor.ts @@ -0,0 +1,9 @@ +import { Role } from '../types/role.enum'; +import { Step } from '../types/step.enum'; +import { Person } from './person'; + +export class Actor { + person: Person; + role: Role; + step: Step; +} diff --git a/src/modules/matcher/domain/entities/route.ts b/src/modules/matcher/domain/entities/route.ts index bf729c9..324abec 100644 --- a/src/modules/matcher/domain/entities/route.ts +++ b/src/modules/matcher/domain/entities/route.ts @@ -1 +1,55 @@ -export class Route {} +import { IGeodesic } from '../interfaces/geodesic.interface'; +import { SpacetimePoint } from './spacetime-point'; +import { Waypoint } from './waypoint'; + +export class Route { + distance: number; + duration: number; + fwdAzimuth: number; + backAzimuth: number; + distanceAzimuth: number; + waypoints: Array; + points: Array>; + spacetimePoints: Array; + _geodesic: IGeodesic; + + constructor(geodesic: IGeodesic) { + this.distance = undefined; + this.duration = undefined; + this.fwdAzimuth = undefined; + this.backAzimuth = undefined; + this.distanceAzimuth = undefined; + this.waypoints = []; + this.points = []; + this.spacetimePoints = []; + this._geodesic = geodesic; + } + + setWaypoints(waypoints: Array): void { + this.waypoints = waypoints; + this._setAzimuth(waypoints.map((waypoint) => waypoint.point)); + } + + setPoints(points: Array>): void { + this.points = points; + this._setAzimuth(points); + } + + setSpacetimePoints(spacetimePoints: Array): void { + this.spacetimePoints = spacetimePoints; + } + + _setAzimuth(points: Array>): void { + const inverse = this._geodesic.inverse( + points[0][0], + points[0][1], + points[points.length - 1][0], + points[points.length - 1][1], + ); + this.fwdAzimuth = + inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth); + this.backAzimuth = + this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180; + this.distanceAzimuth = inverse.distance; + } +} diff --git a/src/modules/matcher/domain/entities/spacetime-point.ts b/src/modules/matcher/domain/entities/spacetime-point.ts new file mode 100644 index 0000000..98fe80f --- /dev/null +++ b/src/modules/matcher/domain/entities/spacetime-point.ts @@ -0,0 +1,11 @@ +export class SpacetimePoint { + point: Array; + duration: number; + distance: number; + + constructor(point: Array, duration: number, distance: number) { + this.point = point; + this.duration = duration; + this.distance = distance; + } +} diff --git a/src/modules/matcher/domain/entities/waypoint.ts b/src/modules/matcher/domain/entities/waypoint.ts new file mode 100644 index 0000000..f44773a --- /dev/null +++ b/src/modules/matcher/domain/entities/waypoint.ts @@ -0,0 +1,6 @@ +import { Actor } from './actor'; + +export class Waypoint { + point: Array; + actors: Array; +} diff --git a/src/modules/matcher/domain/interfaces/geodesic.interface.ts b/src/modules/matcher/domain/interfaces/geodesic.interface.ts new file mode 100644 index 0000000..95680e8 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/geodesic.interface.ts @@ -0,0 +1,11 @@ +export interface IGeodesic { + inverse( + lon1: number, + lat1: number, + lon2: number, + lat2: number, + ): { + azimuth: number; + distance: number; + }; +} diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index f67641b..6d48e63 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -17,54 +17,59 @@ export class MatchUseCase { async execute(matchQuery: MatchQuery): Promise> { try { - const paths = []; - for (let i = 0; i < 2000; i++) { - paths.push({ - key: 'route' + i, - points: [ - { - lat: 48.110899, - lon: -1.68365, - }, - { - lat: 48.131105, - lon: -1.690067, - }, - { - lat: 48.56516, - lon: -1.923553, - }, - { - lat: 48.622813, - lon: -1.997177, - }, - { - lat: 48.67846, - lon: -1.8554, - }, - ], - }); - } - const routes = await matchQuery.algorithmSettings.georouter.route(paths, { - withDistance: true, - withPoints: true, - withTime: false, - }); + // const paths = []; + // for (let i = 0; i < 1; i++) { + // paths.push({ + // key: 'route' + i, + // points: [ + // { + // lat: 48.110899, + // lon: -1.68365, + // }, + // { + // lat: 48.131105, + // lon: -1.690067, + // }, + // { + // lat: 48.534769, + // lon: -1.894032, + // }, + // { + // lat: 48.56516, + // lon: -1.923553, + // }, + // { + // lat: 48.622813, + // lon: -1.997177, + // }, + // { + // lat: 48.67846, + // lon: -1.8554, + // }, + // ], + // }); + // } + // const routes = await matchQuery.algorithmSettings.georouter.route(paths, { + // withDistance: false, + // withPoints: true, + // withTime: true, + // }); + // routes.map((route) => console.log(route.route.spacetimePoints)); const match = new Match(); match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; - // this._messager.publish('matcher.match', 'match !'); + this._messager.publish('matcher.match', 'match !'); return { data: [match], total: 1, }; } catch (error) { - // this._messager.publish( - // 'logging.matcher.match.crit', - // JSON.stringify({ - // matchQuery, - // error, - // }), - // ); + this._messager.publish( + 'logging.matcher.match.crit', + JSON.stringify({ + matchQuery, + error, + }), + ); throw error; } } diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index 7854892..7173746 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -14,6 +14,7 @@ import { redisStore } from 'cache-manager-ioredis-yet'; import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider'; import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; import { HttpModule } from '@nestjs/axios'; +import { MatcherGeodesic } from './adapters/secondaries/geodesic'; @Module({ imports: [ @@ -55,6 +56,7 @@ import { HttpModule } from '@nestjs/axios'; DefaultParamsProvider, MatchUseCase, GeorouterCreator, + MatcherGeodesic, ], exports: [], }) diff --git a/src/modules/matcher/tests/unit/default-params.provider.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts similarity index 84% rename from src/modules/matcher/tests/unit/default-params.provider.spec.ts rename to src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts index a721186..5221c14 100644 --- a/src/modules/matcher/tests/unit/default-params.provider.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultParamsProvider } from '../../adapters/secondaries/default-params.provider'; -import { IDefaultParams } from '../../domain/types/default-params.type'; +import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; const mockConfigService = { get: jest.fn().mockImplementationOnce(() => 99), diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts new file mode 100644 index 0000000..9e08335 --- /dev/null +++ b/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts @@ -0,0 +1,14 @@ +import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic'; + +describe('Matcher geodesic', () => { + it('should be defined', () => { + const geodesic: MatcherGeodesic = new MatcherGeodesic(); + expect(geodesic).toBeDefined(); + }); + it('should get inverse values', () => { + const geodesic: MatcherGeodesic = new MatcherGeodesic(); + const inv = geodesic.inverse(0, 0, 1, 1); + expect(Math.round(inv.azimuth)).toBe(45); + expect(Math.round(inv.distance)).toBe(156900); + }); +}); diff --git a/src/modules/matcher/tests/unit/georouter-creator.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/georouter-creator.spec.ts similarity index 72% rename from src/modules/matcher/tests/unit/georouter-creator.spec.ts rename to src/modules/matcher/tests/unit/adapters/secondaries/georouter-creator.spec.ts index b7a7d6d..543991b 100644 --- a/src/modules/matcher/tests/unit/georouter-creator.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/georouter-creator.spec.ts @@ -1,9 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; -import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter'; +import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator'; +import { GraphhopperGeorouter } from '../../../../adapters/secondaries/graphhopper-georouter'; import { HttpService } from '@nestjs/axios'; +import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic'; const mockHttpService = jest.fn(); +const mockMatcherGeodesic = jest.fn(); describe('Georouter creator', () => { let georouterCreator: GeorouterCreator; @@ -17,6 +19,10 @@ describe('Georouter creator', () => { provide: HttpService, useValue: mockHttpService, }, + { + provide: MatcherGeodesic, + useValue: mockMatcherGeodesic, + }, ], }).compile(); diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts new file mode 100644 index 0000000..71dd199 --- /dev/null +++ b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts @@ -0,0 +1,268 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpService } from '@nestjs/axios'; +import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator'; +import { IGeorouter } from '../../../../domain/interfaces/georouter.interface'; +import { of } from 'rxjs'; +import { AxiosError } from 'axios'; +import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic'; + +const mockHttpService = { + get: jest + .fn() + .mockImplementationOnce(() => { + throw new AxiosError('Axios error !'); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + details: { + time: [ + [0, 1, 180000], + [1, 2, 180000], + [2, 3, 180000], + [3, 4, 180000], + [4, 5, 180000], + [5, 6, 180000], + [6, 7, 180000], + [7, 9, 360000], + [9, 10, 180000], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }), +}; + +const mockMatcherGeodesic = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inverse: jest.fn().mockImplementation(() => ({ + azimuth: 45, + distance: 50000, + })), +}; + +describe('Graphhopper Georouter', () => { + let georouterCreator: GeorouterCreator; + let graphhopperGeorouter: IGeorouter; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + GeorouterCreator, + { + provide: HttpService, + useValue: mockHttpService, + }, + { + provide: MatcherGeodesic, + useValue: mockMatcherGeodesic, + }, + ], + }).compile(); + + georouterCreator = module.get(GeorouterCreator); + graphhopperGeorouter = georouterCreator.create( + 'graphhopper', + 'http://localhost', + ); + }); + + it('should be defined', () => { + expect(graphhopperGeorouter).toBeDefined(); + }); + + describe('route function', () => { + it('should fail on axios error', async () => { + await expect( + graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 1, + lon: 1, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ), + ).rejects.toBeInstanceOf(Error); + }); + it('should create one route with all settings to false', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.distance).toBe(50000); + }); + it('should create one route with points', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: false, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.distance).toBe(50000); + expect(routes[0].route.duration).toBe(1800); + expect(routes[0].route.fwdAzimuth).toBe(45); + expect(routes[0].route.backAzimuth).toBe(225); + expect(routes[0].route.points.length).toBe(11); + }); + it('should create one route with points and time', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: true, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.spacetimePoints.length).toBe(2); + expect(routes[0].route.spacetimePoints[1].duration).toBe(1800); + expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined(); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/messager.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/messager.spec.ts similarity index 94% rename from src/modules/matcher/tests/unit/messager.spec.ts rename to src/modules/matcher/tests/unit/adapters/secondaries/messager.spec.ts index 0331332..0bd23a9 100644 --- a/src/modules/matcher/tests/unit/messager.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/messager.spec.ts @@ -1,7 +1,7 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { Messager } from '../../adapters/secondaries/messager'; +import { Messager } from '../../../../adapters/secondaries/messager'; const mockAmqpConnection = { publish: jest.fn().mockImplementation(), diff --git a/src/modules/matcher/tests/unit/geography.spec.ts b/src/modules/matcher/tests/unit/domain/geography.spec.ts similarity index 97% rename from src/modules/matcher/tests/unit/geography.spec.ts rename to src/modules/matcher/tests/unit/domain/geography.spec.ts index 7c52c8c..e10b773 100644 --- a/src/modules/matcher/tests/unit/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/geography.spec.ts @@ -1,4 +1,4 @@ -import { Geography } from '../../domain/entities/geography'; +import { Geography } from '../../../domain/entities/geography'; describe('Geography entity', () => { it('should be defined', () => { diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts similarity index 80% rename from src/modules/matcher/tests/unit/match.usecase.spec.ts rename to src/modules/matcher/tests/unit/domain/match.usecase.spec.ts index dace03d..6de7ad9 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -1,13 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { Messager } from '../../adapters/secondaries/messager'; -import { MatchUseCase } from '../../domain/usecases/match.usecase'; -import { MatchRequest } from '../../domain/dtos/match.request'; -import { MatchQuery } from '../../queries/match.query'; -import { AdRepository } from '../../adapters/secondaries/ad.repository'; +import { Messager } from '../../../adapters/secondaries/messager'; +import { MatchUseCase } from '../../../domain/usecases/match.usecase'; +import { MatchRequest } from '../../../domain/dtos/match.request'; +import { MatchQuery } from '../../../queries/match.query'; +import { AdRepository } from '../../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; -import { IDefaultParams } from '../../domain/types/default-params.type'; -import { Algorithm } from '../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../domain/types/default-params.type'; +import { Algorithm } from '../../../domain/types/algorithm.enum'; const mockAdRepository = {}; diff --git a/src/modules/matcher/tests/unit/person.spec.ts b/src/modules/matcher/tests/unit/domain/person.spec.ts similarity index 94% rename from src/modules/matcher/tests/unit/person.spec.ts rename to src/modules/matcher/tests/unit/domain/person.spec.ts index 2ff144f..56c2e47 100644 --- a/src/modules/matcher/tests/unit/person.spec.ts +++ b/src/modules/matcher/tests/unit/domain/person.spec.ts @@ -1,4 +1,4 @@ -import { Person } from '../../domain/entities/person'; +import { Person } from '../../../domain/entities/person'; const DEFAULT_IDENTIFIER = 0; const MARGIN_DURATION = 900; diff --git a/src/modules/matcher/tests/unit/domain/route.spec.ts b/src/modules/matcher/tests/unit/domain/route.spec.ts new file mode 100644 index 0000000..d281452 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/route.spec.ts @@ -0,0 +1,55 @@ +import { Route } from '../../../domain/entities/route'; +import { SpacetimePoint } from '../../../domain/entities/spacetime-point'; +import { Waypoint } from '../../../domain/entities/waypoint'; + +const mockGeodesic = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inverse: jest.fn().mockImplementation((lon1, lat1, lon2, lat2) => { + return lon1 == 0 + ? { + azimuth: 45, + distance: 50000, + } + : { + azimuth: -45, + distance: 60000, + }; + }), +}; + +describe('Route entity', () => { + it('should be defined', () => { + const route = new Route(mockGeodesic); + expect(route).toBeDefined(); + }); + it('should set waypoints and geodesic values for a route', () => { + const route = new Route(mockGeodesic); + const waypoint1: Waypoint = new Waypoint(); + waypoint1.point = [0, 0]; + const waypoint2: Waypoint = new Waypoint(); + waypoint2.point = [10, 10]; + route.setWaypoints([waypoint1, waypoint2]); + expect(route.waypoints.length).toBe(2); + expect(route.fwdAzimuth).toBe(45); + expect(route.backAzimuth).toBe(225); + expect(route.distanceAzimuth).toBe(50000); + }); + it('should set points and geodesic values for a route', () => { + const route = new Route(mockGeodesic); + route.setPoints([ + [10, 10], + [20, 20], + ]); + expect(route.points.length).toBe(2); + expect(route.fwdAzimuth).toBe(315); + expect(route.backAzimuth).toBe(135); + expect(route.distanceAzimuth).toBe(60000); + }); + it('should set spacetimePoints for a route', () => { + const route = new Route(mockGeodesic); + const spacetimePoint1 = new SpacetimePoint([0, 0], 0, 0); + const spacetimePoint2 = new SpacetimePoint([10, 10], 500, 5000); + route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]); + expect(route.spacetimePoints.length).toBe(2); + }); +}); diff --git a/src/modules/matcher/tests/unit/time.spec.ts b/src/modules/matcher/tests/unit/domain/time.spec.ts similarity index 98% rename from src/modules/matcher/tests/unit/time.spec.ts rename to src/modules/matcher/tests/unit/domain/time.spec.ts index 0d8cbdd..5cc3929 100644 --- a/src/modules/matcher/tests/unit/time.spec.ts +++ b/src/modules/matcher/tests/unit/domain/time.spec.ts @@ -1,4 +1,4 @@ -import { Time } from '../../domain/entities/time'; +import { Time } from '../../../domain/entities/time'; const MARGIN_DURATION = 900; const VALIDITY_DURATION = 365; diff --git a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts deleted file mode 100644 index 64cc698..0000000 --- a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HttpService } from '@nestjs/axios'; -import { NamedRoute } from '../../domain/entities/named-route'; -import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; -import { IGeorouter } from '../../domain/interfaces/georouter.interface'; -import { of } from 'rxjs'; -import { AxiosError } from 'axios'; - -const mockHttpService = { - get: jest - .fn() - .mockImplementationOnce(() => { - throw new AxiosError('Axios error !'); - }) - .mockImplementation(() => { - return of({ - status: 200, - data: [new NamedRoute()], - }); - }), -}; - -describe('Graphhopper Georouter', () => { - let georouterCreator: GeorouterCreator; - let graphhopperGeorouter: IGeorouter; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [], - providers: [ - GeorouterCreator, - { - provide: HttpService, - useValue: mockHttpService, - }, - ], - }).compile(); - - georouterCreator = module.get(GeorouterCreator); - graphhopperGeorouter = georouterCreator.create( - 'graphhopper', - 'http://localhost', - ); - }); - - it('should be defined', () => { - expect(graphhopperGeorouter).toBeDefined(); - }); - - describe('route function', () => { - it('should fail on axios error', async () => { - await expect( - graphhopperGeorouter.route( - [ - { - key: 'route1', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - ], - { - withDistance: false, - withPoints: false, - withTime: false, - }, - ), - ).rejects.toBeInstanceOf(Error); - }); - it('should create one route with all settings to false', async () => { - const routes = await graphhopperGeorouter.route( - [ - { - key: 'route1', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - ], - { - withDistance: false, - withPoints: false, - withTime: false, - }, - ); - expect(routes).toHaveLength(1); - }); - it('should create 2 routes with distance, points and time', async () => { - const routes = await graphhopperGeorouter.route( - [ - { - key: 'route1', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - { - key: 'route2', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - ], - { - withDistance: true, - withPoints: true, - withTime: true, - }, - ); - expect(routes).toHaveLength(2); - }); - }); -}); diff --git a/src/modules/matcher/tests/unit/match.query.spec.ts b/src/modules/matcher/tests/unit/queries/match.query.spec.ts similarity index 93% rename from src/modules/matcher/tests/unit/match.query.spec.ts rename to src/modules/matcher/tests/unit/queries/match.query.spec.ts index 6e9182f..8ed650b 100644 --- a/src/modules/matcher/tests/unit/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -1,9 +1,9 @@ -import { MatchRequest } from '../../domain/dtos/match.request'; -import { Role } from '../../domain/types/role.enum'; -import { TimingFrequency } from '../../domain/types/timing'; -import { IDefaultParams } from '../../domain/types/default-params.type'; -import { MatchQuery } from '../../queries/match.query'; -import { Algorithm } from '../../domain/types/algorithm.enum'; +import { MatchRequest } from '../../../domain/dtos/match.request'; +import { Role } from '../../../domain/types/role.enum'; +import { TimingFrequency } from '../../../domain/types/timing'; +import { IDefaultParams } from '../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../queries/match.query'; +import { Algorithm } from '../../../domain/types/algorithm.enum'; const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, From 0dec1303963a703cc0607aac9087c6255e799563 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 20 Apr 2023 10:47:54 +0200 Subject: [PATCH 18/26] graphhopper georouter tests --- .../secondaries/graphhopper-georouter.ts | 2 +- .../matcher/domain/usecases/match.usecase.ts | 3 +- .../secondaries/graphhopper-georouter.spec.ts | 189 ++++++++++++++++++ 3 files changed, 192 insertions(+), 2 deletions(-) diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 87e2129..84cb2ca 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -86,7 +86,7 @@ export class GraphhopperGeorouter implements IGeorouter { res.data ? this._createRoute(path.key, res) : undefined, ), catchError((error: AxiosError) => { - throw new Error(error.message); + throw new Error('Georouter unavailable : ' + error.message); }), ), ); diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index 6d48e63..c0ae3d1 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -63,11 +63,12 @@ export class MatchUseCase { total: 1, }; } catch (error) { + const err: Error = error; this._messager.publish( 'logging.matcher.match.crit', JSON.stringify({ matchQuery, - error, + error: err.message, }), ); throw error; diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts index 71dd199..11f5752 100644 --- a/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts @@ -111,6 +111,126 @@ const mockHttpService = { ], }, }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [5, 5], + [10, 10], + ], + }, + details: { + time: [ + [0, 1, 180000], + [1, 2, 180000], + [2, 3, 180000], + [3, 4, 180000], + [4, 7, 540000], + [7, 9, 360000], + [9, 10, 180000], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [5, 5], + [10, 10], + ], + }, + details: { + time: [ + [0, 1, 180000], + [1, 2, 180000], + [2, 3, 180000], + [3, 4, 180000], + [4, 7, 540000], + [7, 9, 360000], + [9, 10, 180000], + ], + }, + instructions: [ + { + distance: 25000, + sign: 0, + interval: [0, 5], + text: 'Some instructions', + time: 900000, + }, + { + distance: 0, + sign: 5, + interval: [5, 5], + text: 'Waypoint 1', + time: 0, + }, + { + distance: 25000, + sign: 2, + interval: [5, 10], + text: 'Some instructions', + time: 900000, + }, + { + distance: 0.0, + sign: 4, + interval: [10, 10], + text: 'Arrive at destination', + time: 0, + }, + ], + }, + ], + }, + }); }), }; @@ -180,6 +300,7 @@ describe('Graphhopper Georouter', () => { ), ).rejects.toBeInstanceOf(Error); }); + it('should create one route with all settings to false', async () => { const routes = await graphhopperGeorouter.route( [ @@ -206,6 +327,7 @@ describe('Graphhopper Georouter', () => { expect(routes).toHaveLength(1); expect(routes[0].route.distance).toBe(50000); }); + it('should create one route with points', async () => { const routes = await graphhopperGeorouter.route( [ @@ -236,6 +358,7 @@ describe('Graphhopper Georouter', () => { expect(routes[0].route.backAzimuth).toBe(225); expect(routes[0].route.points.length).toBe(11); }); + it('should create one route with points and time', async () => { const routes = await graphhopperGeorouter.route( [ @@ -264,5 +387,71 @@ describe('Graphhopper Georouter', () => { expect(routes[0].route.spacetimePoints[1].duration).toBe(1800); expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined(); }); + + it('should create one route with points and missed waypoints extrapolations', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 5, + lon: 5, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: true, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.spacetimePoints.length).toBe(3); + expect(routes[0].route.distance).toBe(50000); + expect(routes[0].route.duration).toBe(1800); + expect(routes[0].route.fwdAzimuth).toBe(45); + expect(routes[0].route.backAzimuth).toBe(225); + expect(routes[0].route.points.length).toBe(9); + }); + + it('should create one route with points, time and distance', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: true, + withPoints: true, + withTime: true, + }, + ); + console.log(routes[0].route.spacetimePoints); + expect(routes).toHaveLength(1); + expect(routes[0].route.spacetimePoints.length).toBe(3); + expect(routes[0].route.spacetimePoints[1].duration).toBe(990); + expect(routes[0].route.spacetimePoints[1].distance).toBe(25000); + }); }); }); From ed99b442eb5b8f94ac2db2f5645c3da079eddd01 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 20 Apr 2023 10:49:35 +0200 Subject: [PATCH 19/26] graphhopper georouter tests --- .../adapters/secondaries/graphhopper-georouter.ts | 9 ++------- .../adapters/secondaries/graphhopper-georouter.spec.ts | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 84cb2ca..2ced628 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -82,9 +82,7 @@ export class GraphhopperGeorouter implements IGeorouter { ].join(''); const route = await lastValueFrom( this._httpService.get(url).pipe( - map((res) => - res.data ? this._createRoute(path.key, res) : undefined, - ), + map((res) => (res.data ? this._createRoute(res) : undefined)), catchError((error: AxiosError) => { throw new Error('Georouter unavailable : ' + error.message); }), @@ -103,10 +101,7 @@ export class GraphhopperGeorouter implements IGeorouter { return [this._url, this._urlArgs.join('&')].join(''); } - _createRoute( - key: string, - response: AxiosResponse, - ): Route { + _createRoute(response: AxiosResponse): Route { const route = new Route(this._geodesic); if (response.data.paths && response.data.paths[0]) { const shortestPath = response.data.paths[0]; diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts index 11f5752..fdfef4b 100644 --- a/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts @@ -447,7 +447,6 @@ describe('Graphhopper Georouter', () => { withTime: true, }, ); - console.log(routes[0].route.spacetimePoints); expect(routes).toHaveLength(1); expect(routes[0].route.spacetimePoints.length).toBe(3); expect(routes[0].route.spacetimePoints[1].duration).toBe(990); From 6dd4837c891c9b951262953cc9fc638b22204b8f Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 20 Apr 2023 15:52:01 +0200 Subject: [PATCH 20/26] geography entity with tests --- .../secondaries/default-params.provider.ts | 4 +- .../matcher/adapters/secondaries/geodesic.ts | 6 +- .../adapters/secondaries/georouter-creator.ts | 4 +- .../secondaries/graphhopper-georouter.ts | 65 ++++--- .../matcher/adapters/secondaries/messager.ts | 4 +- src/modules/matcher/domain/entities/actor.ts | 6 + .../matcher/domain/entities/geography.ts | 155 +++++++++++++-- .../matcher/domain/entities/named-route.ts | 4 +- src/modules/matcher/domain/entities/person.ts | 12 +- src/modules/matcher/domain/entities/route.ts | 27 +-- src/modules/matcher/domain/entities/time.ts | 36 ++-- .../matcher/domain/entities/waypoint.ts | 10 +- .../matcher/domain/types/point.type.ts | 3 + .../matcher/domain/usecases/match.usecase.ts | 4 +- src/modules/matcher/mappers/match.profile.ts | 2 +- src/modules/matcher/queries/match.query.ts | 33 ++-- .../tests/unit/domain/geography.spec.ts | 182 +++++++++++++++++- .../matcher/tests/unit/domain/route.spec.ts | 22 ++- 18 files changed, 458 insertions(+), 121 deletions(-) diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index 9b5bad6..c67dc10 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -6,7 +6,7 @@ import { IDefaultParams } from '../../domain/types/default-params.type'; export class DefaultParamsProvider { constructor(private readonly configService: ConfigService) {} - getParams(): IDefaultParams { + getParams = (): IDefaultParams => { return { DEFAULT_IDENTIFIER: parseInt( this.configService.get('DEFAULT_IDENTIFIER'), @@ -33,5 +33,5 @@ export class DefaultParamsProvider { georouterUrl: this.configService.get('GEOROUTER_URL'), }, }; - } + }; } diff --git a/src/modules/matcher/adapters/secondaries/geodesic.ts b/src/modules/matcher/adapters/secondaries/geodesic.ts index 56423a8..f2a9642 100644 --- a/src/modules/matcher/adapters/secondaries/geodesic.ts +++ b/src/modules/matcher/adapters/secondaries/geodesic.ts @@ -10,12 +10,12 @@ export class MatcherGeodesic implements IGeodesic { this._geod = Geodesic.WGS84; } - inverse( + inverse = ( lon1: number, lat1: number, lon2: number, lat2: number, - ): { azimuth: number; distance: number } { + ): { azimuth: number; distance: number } => { const { azi2: azimuth, s12: distance } = this._geod.Inverse( lat1, lon1, @@ -23,5 +23,5 @@ export class MatcherGeodesic implements IGeodesic { lon2, ); return { azimuth, distance }; - } + }; } diff --git a/src/modules/matcher/adapters/secondaries/georouter-creator.ts b/src/modules/matcher/adapters/secondaries/georouter-creator.ts index 22e65b3..379920a 100644 --- a/src/modules/matcher/adapters/secondaries/georouter-creator.ts +++ b/src/modules/matcher/adapters/secondaries/georouter-creator.ts @@ -12,12 +12,12 @@ export class GeorouterCreator implements ICreateGeorouter { private readonly geodesic: MatcherGeodesic, ) {} - create(type: string, url: string): IGeorouter { + create = (type: string, url: string): IGeorouter => { switch (type) { case 'graphhopper': return new GraphhopperGeorouter(url, this.httpService, this.geodesic); default: throw new Error('Unknown geocoder'); } - } + }; } diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 2ced628..5d1fd84 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -27,50 +27,50 @@ export class GraphhopperGeorouter implements IGeorouter { this._geodesic = geodesic; } - async route( + route = async ( paths: Array, settings: GeorouterSettings, - ): Promise> { + ): Promise> => { this._setDefaultUrlArgs(); this._setWithTime(settings.withTime); this._setWithPoints(settings.withPoints); this._setWithDistance(settings.withDistance); this._paths = paths; return await this._getRoutes(); - } + }; - _setDefaultUrlArgs(): void { + _setDefaultUrlArgs = (): void => { this._urlArgs = [ 'vehicle=car', 'weighting=fastest', 'points_encoded=false', ]; - } + }; - _setWithTime(withTime: boolean): void { + _setWithTime = (withTime: boolean): void => { this._withTime = withTime; if (withTime) { this._urlArgs.push('details=time'); } - } + }; - _setWithPoints(withPoints: boolean): void { + _setWithPoints = (withPoints: boolean): void => { this._withPoints = withPoints; if (!withPoints) { this._urlArgs.push('calc_points=false'); } - } + }; - _setWithDistance(withDistance: boolean): void { + _setWithDistance = (withDistance: boolean): void => { this._withDistance = withDistance; if (withDistance) { this._urlArgs.push('instructions=true'); } else { this._urlArgs.push('instructions=false'); } - } + }; - async _getRoutes(): Promise> { + _getRoutes = async (): Promise> => { const routes = Promise.all( this._paths.map(async (path) => { const url: string = [ @@ -95,20 +95,25 @@ export class GraphhopperGeorouter implements IGeorouter { }), ); return routes; - } + }; - _getUrl(): string { + _getUrl = (): string => { return [this._url, this._urlArgs.join('&')].join(''); - } + }; - _createRoute(response: AxiosResponse): Route { + _createRoute = (response: AxiosResponse): Route => { const route = new Route(this._geodesic); if (response.data.paths && response.data.paths[0]) { const shortestPath = response.data.paths[0]; route.distance = shortestPath.distance ?? 0; route.duration = shortestPath.time ? shortestPath.time / 1000 : 0; if (shortestPath.points && shortestPath.points.coordinates) { - route.setPoints(shortestPath.points.coordinates); + route.setPoints( + shortestPath.points.coordinates.map((coordinate) => ({ + lon: coordinate[0], + lat: coordinate[1], + })), + ); if ( shortestPath.details && shortestPath.details.time && @@ -130,14 +135,14 @@ export class GraphhopperGeorouter implements IGeorouter { } } return route; - } + }; - _generateSpacetimePoints( + _generateSpacetimePoints = ( points: Array>, snappedWaypoints: Array>, durations: Array>, instructions: Array, - ): Array { + ): Array => { const indices = this._getIndices(points, snappedWaypoints); const times = this._getTimes(durations, indices); const distances = this._getDistances(instructions, indices); @@ -149,12 +154,12 @@ export class GraphhopperGeorouter implements IGeorouter { distances.find((distance) => distance.index == index)?.distance, ), ); - } + }; - _getIndices( + _getIndices = ( points: Array>, snappedWaypoints: Array>, - ): Array { + ): Array => { const indices = snappedWaypoints.map((waypoint) => points.findIndex( (point) => point[0] == waypoint[0] && point[1] == waypoint[1], @@ -199,12 +204,12 @@ export class GraphhopperGeorouter implements IGeorouter { indices[missedWaypoint.originIndex] = missedWaypoint.nearest; } return indices; - } + }; - _getTimes( + _getTimes = ( durations: Array>, indices: Array, - ): Array<{ index: number; duration: number }> { + ): Array<{ index: number; duration: number }> => { const times: Array<{ index: number; duration: number }> = []; let duration = 0; for (const [origin, destination, stepDuration] of durations) { @@ -249,12 +254,12 @@ export class GraphhopperGeorouter implements IGeorouter { duration += stepDuration; } return times; - } + }; - _getDistances( + _getDistances = ( instructions: Array, indices: Array, - ): Array<{ index: number; distance: number }> { + ): Array<{ index: number; distance: number }> => { let distance = 0; const distances: Array<{ index: number; distance: number }> = [ { @@ -276,7 +281,7 @@ export class GraphhopperGeorouter implements IGeorouter { } } return distances; - } + }; } type GraphhopperResponse = { diff --git a/src/modules/matcher/adapters/secondaries/messager.ts b/src/modules/matcher/adapters/secondaries/messager.ts index e808bcf..96fa7cc 100644 --- a/src/modules/matcher/adapters/secondaries/messager.ts +++ b/src/modules/matcher/adapters/secondaries/messager.ts @@ -12,7 +12,7 @@ export class Messager extends MessageBroker { super(configService.get('RMQ_EXCHANGE')); } - publish(routingKey: string, message: string): void { + publish = (routingKey: string, message: string): void => { this._amqpConnection.publish(this.exchange, routingKey, message); - } + }; } diff --git a/src/modules/matcher/domain/entities/actor.ts b/src/modules/matcher/domain/entities/actor.ts index de9448a..075b8b4 100644 --- a/src/modules/matcher/domain/entities/actor.ts +++ b/src/modules/matcher/domain/entities/actor.ts @@ -6,4 +6,10 @@ export class Actor { person: Person; role: Role; step: Step; + + constructor(person: Person, role: Role, step: Step) { + this.person = person; + this.role = role; + this.step = step; + } } diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/geography.ts index 506e9b3..2fcfbee 100644 --- a/src/modules/matcher/domain/entities/geography.ts +++ b/src/modules/matcher/domain/entities/geography.ts @@ -3,32 +3,129 @@ import { IRequestGeography } from '../interfaces/geography-request.interface'; import { PointType } from '../types/geography.enum'; import { Point } from '../types/point.type'; import { find } from 'geo-tz'; -import { Waypoint } from '../types/waypoint'; import { Route } from './route'; +import { Role } from '../types/role.enum'; +import { IGeorouter } from '../interfaces/georouter.interface'; +import { Waypoint } from './waypoint'; +import { Actor } from './actor'; +import { Person } from './person'; +import { Step } from '../types/step.enum'; +import { Path } from '../types/path.type'; export class Geography { _geographyRequest: IRequestGeography; - waypoints: Array; + _person: Person; + _points: Array; originType: PointType; destinationType: PointType; timezones: Array; driverRoute: Route; passengerRoute: Route; - constructor(geographyRequest: IRequestGeography, defaultTimezone: string) { + constructor( + geographyRequest: IRequestGeography, + defaultTimezone: string, + person: Person, + ) { this._geographyRequest = geographyRequest; - this.waypoints = []; - this.originType = PointType.OTHER; - this.destinationType = PointType.OTHER; + this._person = person; + this._points = []; + this.originType = undefined; + this.destinationType = undefined; this.timezones = [defaultTimezone]; } - init() { + init = (): void => { this._validateWaypoints(); this._setTimezones(); - } + this._setPointTypes(); + }; - _validateWaypoints() { + createRoutes = async ( + roles: Array, + georouter: IGeorouter, + ): Promise => { + let driverWaypoints: Array = []; + let passengerWaypoints: Array = []; + const paths: Array = []; + if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { + if (this._points.length == 2) { + // 2 points => same route for driver and passenger + const commonPath: Path = { + key: RouteKey.COMMON, + points: this._points, + }; + driverWaypoints = this._createWaypoints(commonPath.points, Role.DRIVER); + passengerWaypoints = this._createWaypoints( + commonPath.points, + Role.PASSENGER, + ); + paths.push(commonPath); + } else { + const driverPath: Path = { + key: RouteKey.DRIVER, + points: this._points, + }; + driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER); + const passengerPath: Path = { + key: RouteKey.PASSENGER, + points: [this._points[0], this._points[this._points.length - 1]], + }; + passengerWaypoints = this._createWaypoints( + passengerPath.points, + Role.PASSENGER, + ); + paths.push(driverPath, passengerPath); + } + } else if (roles.includes(Role.DRIVER)) { + const driverPath: Path = { + key: RouteKey.DRIVER, + points: this._points, + }; + driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER); + paths.push(driverPath); + } else if (roles.includes(Role.PASSENGER)) { + const passengerPath: Path = { + key: RouteKey.PASSENGER, + points: [this._points[0], this._points[this._points.length - 1]], + }; + passengerWaypoints = this._createWaypoints( + passengerPath.points, + Role.PASSENGER, + ); + paths.push(passengerPath); + } + const routes = await georouter.route(paths, { + withDistance: false, + withPoints: true, + withTime: false, + }); + if (routes.some((route) => route.key == RouteKey.COMMON)) { + this.driverRoute = routes.find( + (route) => route.key == RouteKey.COMMON, + ).route; + this.passengerRoute = routes.find( + (route) => route.key == RouteKey.COMMON, + ).route; + this.driverRoute.setWaypoints(driverWaypoints); + this.passengerRoute.setWaypoints(passengerWaypoints); + } else { + if (routes.some((route) => route.key == RouteKey.DRIVER)) { + this.driverRoute = routes.find( + (route) => route.key == RouteKey.DRIVER, + ).route; + this.driverRoute.setWaypoints(driverWaypoints); + } + if (routes.some((route) => route.key == RouteKey.PASSENGER)) { + this.passengerRoute = routes.find( + (route) => route.key == RouteKey.PASSENGER, + ).route; + this.passengerRoute.setWaypoints(passengerWaypoints); + } + } + }; + + _validateWaypoints = (): void => { if (this._geographyRequest.waypoints.length < 2) { throw new MatcherException(3, 'At least 2 waypoints are required'); } @@ -39,19 +136,25 @@ export class Geography { `Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`, ); } - this.waypoints.push({ - point, - actors: [], - }); + this._points.push(point); }); - } + }; - _setTimezones() { + _setTimezones = (): void => { this.timezones = find( this._geographyRequest.waypoints[0].lat, this._geographyRequest.waypoints[0].lon, ); - } + }; + + _setPointTypes = (): void => { + this.originType = + this._geographyRequest.waypoints[0].type ?? PointType.OTHER; + this.destinationType = + this._geographyRequest.waypoints[ + this._geographyRequest.waypoints.length - 1 + ].type ?? PointType.OTHER; + }; _isValidPoint = (point: Point): boolean => this._isValidLongitude(point.lon) && this._isValidLatitude(point.lat); @@ -61,4 +164,24 @@ export class Geography { _isValidLatitude = (latitude: number): boolean => latitude >= -90 && latitude <= 90; + + _createWaypoints = (points: Array, role: Role): Array => { + return points.map((point, index) => { + const waypoint = new Waypoint(point); + if (index == 0) { + waypoint.addActor(new Actor(this._person, role, Step.START)); + } else if (index == points.length - 1) { + waypoint.addActor(new Actor(this._person, role, Step.FINISH)); + } else { + waypoint.addActor(new Actor(this._person, role, Step.INTERMEDIATE)); + } + return waypoint; + }); + }; +} + +export enum RouteKey { + COMMON = 'common', + DRIVER = 'driver', + PASSENGER = 'passenger', } diff --git a/src/modules/matcher/domain/entities/named-route.ts b/src/modules/matcher/domain/entities/named-route.ts index c75c4f1..c57f928 100644 --- a/src/modules/matcher/domain/entities/named-route.ts +++ b/src/modules/matcher/domain/entities/named-route.ts @@ -1,6 +1,6 @@ import { Route } from './route'; -export class NamedRoute { +export type NamedRoute = { key: string; route: Route; -} +}; diff --git a/src/modules/matcher/domain/entities/person.ts b/src/modules/matcher/domain/entities/person.ts index 40349f1..3a1473f 100644 --- a/src/modules/matcher/domain/entities/person.ts +++ b/src/modules/matcher/domain/entities/person.ts @@ -17,7 +17,7 @@ export class Person { this._defaultMarginDuration = defaultMarginDuration; } - init() { + init = (): void => { this.setIdentifier( this._personRequest.identifier ?? this._defaultIdentifier, ); @@ -30,13 +30,13 @@ export class Person { this._defaultMarginDuration, this._defaultMarginDuration, ]); - } + }; - setIdentifier(identifier: number) { + setIdentifier = (identifier: number): void => { this.identifier = identifier; - } + }; - setMarginDurations(marginDurations: Array) { + setMarginDurations = (marginDurations: Array): void => { this.marginDurations = marginDurations; - } + }; } diff --git a/src/modules/matcher/domain/entities/route.ts b/src/modules/matcher/domain/entities/route.ts index 324abec..6712699 100644 --- a/src/modules/matcher/domain/entities/route.ts +++ b/src/modules/matcher/domain/entities/route.ts @@ -1,4 +1,5 @@ import { IGeodesic } from '../interfaces/geodesic.interface'; +import { Point } from '../types/point.type'; import { SpacetimePoint } from './spacetime-point'; import { Waypoint } from './waypoint'; @@ -9,7 +10,7 @@ export class Route { backAzimuth: number; distanceAzimuth: number; waypoints: Array; - points: Array>; + points: Array; spacetimePoints: Array; _geodesic: IGeodesic; @@ -25,31 +26,31 @@ export class Route { this._geodesic = geodesic; } - setWaypoints(waypoints: Array): void { + setWaypoints = (waypoints: Array): void => { this.waypoints = waypoints; this._setAzimuth(waypoints.map((waypoint) => waypoint.point)); - } + }; - setPoints(points: Array>): void { + setPoints = (points: Array): void => { this.points = points; this._setAzimuth(points); - } + }; - setSpacetimePoints(spacetimePoints: Array): void { + setSpacetimePoints = (spacetimePoints: Array): void => { this.spacetimePoints = spacetimePoints; - } + }; - _setAzimuth(points: Array>): void { + _setAzimuth = (points: Array): void => { const inverse = this._geodesic.inverse( - points[0][0], - points[0][1], - points[points.length - 1][0], - points[points.length - 1][1], + points[0].lon, + points[0].lat, + points[points.length - 1].lon, + points[points.length - 1].lat, ); this.fwdAzimuth = inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth); this.backAzimuth = this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180; this.distanceAzimuth = inverse.distance; - } + }; } diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index a6be1f1..b3b2ad8 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -34,31 +34,31 @@ export class Time { }; } - init() { + init = (): void => { this._validateBaseDate(); this._validatePunctualRequest(); this._validateRecurrentRequest(); this._setPunctualRequest(); this._setRecurrentRequest(); this._setMargindurations(); - } + }; - _validateBaseDate() { + _validateBaseDate = (): void => { if (!this._timeRequest.departure && !this._timeRequest.fromDate) { throw new MatcherException(3, 'departure or fromDate is required'); } - } + }; - _validatePunctualRequest() { + _validatePunctualRequest = (): void => { if (this._timeRequest.departure) { this.fromDate = this.toDate = new Date(this._timeRequest.departure); if (!this._isDate(this.fromDate)) { throw new MatcherException(3, 'Wrong departure date'); } } - } + }; - _validateRecurrentRequest() { + _validateRecurrentRequest = (): void => { if (this._timeRequest.fromDate) { this.fromDate = new Date(this._timeRequest.fromDate); if (!this._isDate(this.fromDate)) { @@ -77,9 +77,9 @@ export class Time { if (this._timeRequest.fromDate) { this._validateSchedule(); } - } + }; - _validateSchedule() { + _validateSchedule = (): void => { if (!this._timeRequest.schedule) { throw new MatcherException(3, 'Schedule is required'); } @@ -96,17 +96,17 @@ export class Time { throw new MatcherException(3, `Wrong time for ${day} in schedule`); } }); - } + }; - _setPunctualRequest() { + _setPunctualRequest = (): void => { if (this._timeRequest.departure) { this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; this.schedule[TimingDays[this.fromDate.getDay()]] = this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); } - } + }; - _setRecurrentRequest() { + _setRecurrentRequest = (): void => { if (this._timeRequest.fromDate) { this.frequency = TimingFrequency.FREQUENCY_RECURRENT; if (!this.toDate) { @@ -117,15 +117,15 @@ export class Time { } this._setSchedule(); } - } + }; - _setSchedule() { + _setSchedule = (): void => { Object.keys(this._timeRequest.schedule).map((day) => { this.schedule[day] = this._timeRequest.schedule[day]; }); - } + }; - _setMargindurations() { + _setMargindurations = (): void => { if (this._timeRequest.marginDuration) { const duration = Math.abs(this._timeRequest.marginDuration); this.marginDurations = { @@ -155,7 +155,7 @@ export class Time { ); }); } - } + }; _isDate = (date: Date): boolean => { return date instanceof Date && isFinite(+date); diff --git a/src/modules/matcher/domain/entities/waypoint.ts b/src/modules/matcher/domain/entities/waypoint.ts index f44773a..fb93541 100644 --- a/src/modules/matcher/domain/entities/waypoint.ts +++ b/src/modules/matcher/domain/entities/waypoint.ts @@ -1,6 +1,14 @@ +import { Point } from '../types/point.type'; import { Actor } from './actor'; export class Waypoint { - point: Array; + point: Point; actors: Array; + + constructor(point: Point) { + this.point = point; + this.actors = []; + } + + addActor = (actor: Actor) => this.actors.push(actor); } diff --git a/src/modules/matcher/domain/types/point.type.ts b/src/modules/matcher/domain/types/point.type.ts index 9bb160e..8d32fe0 100644 --- a/src/modules/matcher/domain/types/point.type.ts +++ b/src/modules/matcher/domain/types/point.type.ts @@ -1,4 +1,7 @@ +import { PointType } from './geography.enum'; + export type Point = { lon: number; lat: number; + type?: PointType; }; diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index c0ae3d1..d80f508 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -15,7 +15,7 @@ export class MatchUseCase { @InjectMapper() private readonly _mapper: Mapper, ) {} - async execute(matchQuery: MatchQuery): Promise> { + execute = async (matchQuery: MatchQuery): Promise> => { try { // const paths = []; // for (let i = 0; i < 1; i++) { @@ -73,5 +73,5 @@ export class MatchUseCase { ); throw error; } - } + }; } diff --git a/src/modules/matcher/mappers/match.profile.ts b/src/modules/matcher/mappers/match.profile.ts index 9276c15..c709136 100644 --- a/src/modules/matcher/mappers/match.profile.ts +++ b/src/modules/matcher/mappers/match.profile.ts @@ -11,7 +11,7 @@ export class MatchProfile extends AutomapperProfile { } override get profile() { - return (mapper) => { + return (mapper: Mapper) => { createMap(mapper, Match, MatchPresenter); }; } diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 55381ab..1380c65 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -39,60 +39,65 @@ export class MatchQuery { this._setExclusions(); } - _setPerson() { + createRoutes = (): void => { + this.geography.createRoutes(this.roles, this.algorithmSettings.georouter); + }; + + _setPerson = (): void => { this.person = new Person( this._matchRequest, this._defaultParams.DEFAULT_IDENTIFIER, this._defaultParams.MARGIN_DURATION, ); this.person.init(); - } + }; - _setRoles() { + _setRoles = (): void => { this.roles = []; if (this._matchRequest.driver) this.roles.push(Role.DRIVER); if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER); if (this.roles.length == 0) this.roles.push(Role.PASSENGER); - } + }; - _setTime() { + _setTime = (): void => { this.time = new Time( this._matchRequest, this._defaultParams.MARGIN_DURATION, this._defaultParams.VALIDITY_DURATION, ); this.time.init(); - } + }; - _setGeography() { + _setGeography = (): void => { this.geography = new Geography( this._matchRequest, this._defaultParams.DEFAULT_TIMEZONE, + this.person, ); this.geography.init(); - } + }; - _setRequirement() { + _setRequirement = (): void => { this.requirement = new Requirement( this._matchRequest, this._defaultParams.DEFAULT_SEATS, ); - } + }; - _setAlgorithmSettings() { + _setAlgorithmSettings = (): void => { this.algorithmSettings = new AlgorithmSettings( this._matchRequest, this._defaultParams.DEFAULT_ALGORITHM_SETTINGS, this.time.frequency, this._georouterCreator, ); - } + }; - _setExclusions() { + _setExclusions = (): void => { this.exclusions = []; if (this._matchRequest.identifier) this.exclusions.push(this._matchRequest.identifier); if (this._matchRequest.exclusions) this.exclusions.push(...this._matchRequest.exclusions); - } + }; } diff --git a/src/modules/matcher/tests/unit/domain/geography.spec.ts b/src/modules/matcher/tests/unit/domain/geography.spec.ts index e10b773..f616434 100644 --- a/src/modules/matcher/tests/unit/domain/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/geography.spec.ts @@ -1,4 +1,66 @@ -import { Geography } from '../../../domain/entities/geography'; +import { Person } from '../../../domain/entities/person'; +import { Geography, RouteKey } from '../../../domain/entities/geography'; +import { Role } from '../../../domain/types/role.enum'; +import { NamedRoute } from '../../../domain/entities/named-route'; +import { Route } from '../../../domain/entities/route'; +import { IGeodesic } from '../../../domain/interfaces/geodesic.interface'; +import { PointType } from '../../../domain/types/geography.enum'; + +const person: Person = new Person( + { + identifier: 1, + }, + 0, + 900, +); + +const mockGeodesic: IGeodesic = { + inverse: jest.fn().mockImplementation(() => ({ + azimuth: 45, + distance: 50000, + })), +}; + +const mockGeorouter = { + route: jest + .fn() + .mockImplementationOnce(() => { + return [ + { + key: RouteKey.COMMON, + route: new Route(mockGeodesic), + }, + ]; + }) + .mockImplementationOnce(() => { + return [ + { + key: RouteKey.DRIVER, + route: new Route(mockGeodesic), + }, + { + key: RouteKey.PASSENGER, + route: new Route(mockGeodesic), + }, + ]; + }) + .mockImplementationOnce(() => { + return [ + { + key: RouteKey.DRIVER, + route: new Route(mockGeodesic), + }, + ]; + }) + .mockImplementationOnce(() => { + return [ + { + key: RouteKey.PASSENGER, + route: new Route(mockGeodesic), + }, + ]; + }), +}; describe('Geography entity', () => { it('should be defined', () => { @@ -16,29 +78,35 @@ describe('Geography entity', () => { ], }, 'Europe/Paris', + person, ); expect(geography).toBeDefined(); }); describe('init', () => { - it('should initialize a geography request', () => { + it('should initialize a geography request with point types', () => { const geography = new Geography( { waypoints: [ { lat: 49.440041, lon: 1.093912, + type: PointType.LOCALITY, }, { lat: 50.630992, lon: 3.045432, + type: PointType.LOCALITY, }, ], }, 'Europe/Paris', + person, ); geography.init(); - expect(geography.waypoints.length).toBe(2); + expect(geography._points.length).toBe(2); + expect(geography.originType).toBe(PointType.LOCALITY); + expect(geography.destinationType).toBe(PointType.LOCALITY); }); it('should throw an exception if waypoints are empty', () => { const geography = new Geography( @@ -46,6 +114,7 @@ describe('Geography entity', () => { waypoints: [], }, 'Europe/Paris', + person, ); expect(() => geography.init()).toThrow(); }); @@ -60,6 +129,7 @@ describe('Geography entity', () => { ], }, 'Europe/Paris', + person, ); expect(() => geography.init()).toThrow(); }); @@ -78,6 +148,7 @@ describe('Geography entity', () => { ], }, 'Europe/Paris', + person, ); expect(() => geography.init()).toThrow(); }); @@ -96,8 +167,113 @@ describe('Geography entity', () => { ], }, 'Europe/Paris', + person, ); expect(() => geography.init()).toThrow(); }); }); + + describe('create route', () => { + it('should create routes as driver and passenger', async () => { + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + person, + ); + geography.init(); + await geography.createRoutes( + [Role.DRIVER, Role.PASSENGER], + mockGeorouter, + ); + expect(geography.driverRoute.waypoints.length).toBe(2); + expect(geography.passengerRoute.waypoints.length).toBe(2); + }); + + it('should create routes as driver and passenger with 3 waypoints', async () => { + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 49.781215, + lon: 2.198475, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + person, + ); + geography.init(); + await geography.createRoutes( + [Role.DRIVER, Role.PASSENGER], + mockGeorouter, + ); + expect(geography.driverRoute.waypoints.length).toBe(3); + expect(geography.passengerRoute.waypoints.length).toBe(2); + }); + + it('should create routes as driver', async () => { + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + person, + ); + geography.init(); + await geography.createRoutes([Role.DRIVER], mockGeorouter); + expect(geography.driverRoute.waypoints.length).toBe(2); + expect(geography.passengerRoute).toBeUndefined(); + }); + + it('should create routes as passenger', async () => { + const geography = new Geography( + { + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }, + 'Europe/Paris', + person, + ); + geography.init(); + await geography.createRoutes([Role.PASSENGER], mockGeorouter); + expect(geography.passengerRoute.waypoints.length).toBe(2); + expect(geography.driverRoute).toBeUndefined(); + }); + }); }); diff --git a/src/modules/matcher/tests/unit/domain/route.spec.ts b/src/modules/matcher/tests/unit/domain/route.spec.ts index d281452..35b6214 100644 --- a/src/modules/matcher/tests/unit/domain/route.spec.ts +++ b/src/modules/matcher/tests/unit/domain/route.spec.ts @@ -24,10 +24,14 @@ describe('Route entity', () => { }); it('should set waypoints and geodesic values for a route', () => { const route = new Route(mockGeodesic); - const waypoint1: Waypoint = new Waypoint(); - waypoint1.point = [0, 0]; - const waypoint2: Waypoint = new Waypoint(); - waypoint2.point = [10, 10]; + const waypoint1: Waypoint = new Waypoint({ + lon: 0, + lat: 0, + }); + const waypoint2: Waypoint = new Waypoint({ + lon: 10, + lat: 10, + }); route.setWaypoints([waypoint1, waypoint2]); expect(route.waypoints.length).toBe(2); expect(route.fwdAzimuth).toBe(45); @@ -37,8 +41,14 @@ describe('Route entity', () => { it('should set points and geodesic values for a route', () => { const route = new Route(mockGeodesic); route.setPoints([ - [10, 10], - [20, 20], + { + lon: 10, + lat: 10, + }, + { + lon: 20, + lat: 20, + }, ]); expect(route.points.length).toBe(2); expect(route.fwdAzimuth).toBe(315); From b77c2bf2e167ee95b6889788854ec08d20b35d19 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 20 Apr 2023 16:24:49 +0200 Subject: [PATCH 21/26] pretty pretty --- .prettierrc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.prettierrc b/.prettierrc index dcb7279..3635a40 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { - "singleQuote": true, - "trailingComma": "all" -} \ No newline at end of file + "singleQuote": true, + "trailingComma": "all" +} From 0a4c4bdf5ae0fcb33beb4e5de9b7c8ee7bb92e9a Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 20 Apr 2023 16:29:17 +0200 Subject: [PATCH 22/26] add dotenv cli --- package-lock.json | 16 ++++++++++++++++ package.json | 1 + 2 files changed, 17 insertions(+) diff --git a/package-lock.json b/package-lock.json index 8a64409..b95a8ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "dotenv-cli": "^7.2.1", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", @@ -4213,6 +4214,21 @@ "node": ">=12" } }, + "node_modules/dotenv-cli": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz", + "integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "dotenv": "^16.0.0", + "dotenv-expand": "^10.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, "node_modules/dotenv-expand": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", diff --git a/package.json b/package.json index 34f162f..b2265f2 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "dotenv-cli": "^7.2.1", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", From a54694c5a90acba9d37b48009ce5bfe2d9ffe248 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 21 Apr 2023 10:13:22 +0200 Subject: [PATCH 23/26] divide domain to ecosystem / engine --- .../adapters/primaries/matcher.controller.ts | 2 +- .../adapters/secondaries/ad.repository.ts | 2 +- .../secondaries/graphhopper-georouter.ts | 6 +++--- .../domain/entities/{ => ecosystem}/actor.ts | 4 ++-- .../domain/entities/{ => ecosystem}/ad.ts | 0 .../{ => ecosystem}/algorithm-settings.ts | 12 +++++------ .../entities/{ => ecosystem}/geography.ts | 16 +++++++------- .../domain/entities/{ => ecosystem}/match.ts | 0 .../entities/{ => ecosystem}/named-route.ts | 0 .../domain/entities/{ => ecosystem}/person.ts | 2 +- .../entities/{ => ecosystem}/requirement.ts | 2 +- .../domain/entities/{ => ecosystem}/route.ts | 4 ++-- .../{ => ecosystem}/spacetime-point.ts | 0 .../domain/entities/{ => ecosystem}/time.ts | 10 ++++----- .../entities/{ => ecosystem}/waypoint.ts | 2 +- .../domain/entities/engine/candidate.ts | 5 +++++ .../factory/algorithm-factory.abstract.ts | 15 +++++++++++++ .../domain/entities/engine/factory/classic.ts | 9 ++++++++ .../matcher/domain/entities/engine/matcher.ts | 21 +++++++++++++++++++ .../entities/engine/processor.abstract.ts | 12 +++++++++++ .../classic-waypoint.completer.processor.ts | 8 +++++++ .../processor/completer/completer.abstract.ts | 9 ++++++++ .../matcher/domain/usecases/match.usecase.ts | 4 ++-- src/modules/matcher/queries/match.query.ts | 10 ++++----- .../domain/{ => ecosystem}/geography.spec.ts | 17 ++++++++------- .../domain/{ => ecosystem}/person.spec.ts | 2 +- .../unit/domain/{ => ecosystem}/route.spec.ts | 6 +++--- .../unit/domain/{ => ecosystem}/time.spec.ts | 2 +- 28 files changed, 132 insertions(+), 50 deletions(-) rename src/modules/matcher/domain/entities/{ => ecosystem}/actor.ts (71%) rename src/modules/matcher/domain/entities/{ => ecosystem}/ad.ts (100%) rename src/modules/matcher/domain/entities/{ => ecosystem}/algorithm-settings.ts (82%) rename src/modules/matcher/domain/entities/{ => ecosystem}/geography.ts (92%) rename src/modules/matcher/domain/entities/{ => ecosystem}/match.ts (100%) rename src/modules/matcher/domain/entities/{ => ecosystem}/named-route.ts (100%) rename src/modules/matcher/domain/entities/{ => ecosystem}/person.ts (93%) rename src/modules/matcher/domain/entities/{ => ecosystem}/requirement.ts (82%) rename src/modules/matcher/domain/entities/{ => ecosystem}/route.ts (93%) rename src/modules/matcher/domain/entities/{ => ecosystem}/spacetime-point.ts (100%) rename src/modules/matcher/domain/entities/{ => ecosystem}/time.ts (93%) rename src/modules/matcher/domain/entities/{ => ecosystem}/waypoint.ts (83%) create mode 100644 src/modules/matcher/domain/entities/engine/candidate.ts create mode 100644 src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts create mode 100644 src/modules/matcher/domain/entities/engine/factory/classic.ts create mode 100644 src/modules/matcher/domain/entities/engine/matcher.ts create mode 100644 src/modules/matcher/domain/entities/engine/processor.abstract.ts create mode 100644 src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts create mode 100644 src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts rename src/modules/matcher/tests/unit/domain/{ => ecosystem}/geography.spec.ts (92%) rename src/modules/matcher/tests/unit/domain/{ => ecosystem}/person.spec.ts (93%) rename src/modules/matcher/tests/unit/domain/{ => ecosystem}/route.spec.ts (88%) rename src/modules/matcher/tests/unit/domain/{ => ecosystem}/time.spec.ts (98%) diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 959910c..d1859ff 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -6,11 +6,11 @@ import { GrpcMethod, RpcException } 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'; import { DefaultParamsProvider } from '../secondaries/default-params.provider'; import { GeorouterCreator } from '../secondaries/georouter-creator'; +import { Match } from '../../domain/entities/ecosystem/match'; @UsePipes( new RpcValidationPipe({ diff --git a/src/modules/matcher/adapters/secondaries/ad.repository.ts b/src/modules/matcher/adapters/secondaries/ad.repository.ts index 696bac9..9915f1f 100644 --- a/src/modules/matcher/adapters/secondaries/ad.repository.ts +++ b/src/modules/matcher/adapters/secondaries/ad.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { MatcherRepository } from '../../../database/src/domain/matcher-repository'; -import { Ad } from '../../domain/entities/ad'; +import { Ad } from '../../domain/entities/ecosystem/ad'; @Injectable() export class AdRepository extends MatcherRepository { diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 5d1fd84..26d2e23 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -1,14 +1,14 @@ import { HttpService } from '@nestjs/axios'; -import { NamedRoute } from '../../domain/entities/named-route'; import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GeorouterSettings } from '../../domain/types/georouter-settings.type'; import { Path } from '../../domain/types/path.type'; import { Injectable } from '@nestjs/common'; import { catchError, lastValueFrom, map } from 'rxjs'; import { AxiosError, AxiosResponse } from 'axios'; -import { Route } from '../../domain/entities/route'; -import { SpacetimePoint } from '../../domain/entities/spacetime-point'; import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; +import { NamedRoute } from '../../domain/entities/ecosystem/named-route'; +import { Route } from '../../domain/entities/ecosystem/route'; +import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point'; @Injectable() export class GraphhopperGeorouter implements IGeorouter { diff --git a/src/modules/matcher/domain/entities/actor.ts b/src/modules/matcher/domain/entities/ecosystem/actor.ts similarity index 71% rename from src/modules/matcher/domain/entities/actor.ts rename to src/modules/matcher/domain/entities/ecosystem/actor.ts index 075b8b4..25436e5 100644 --- a/src/modules/matcher/domain/entities/actor.ts +++ b/src/modules/matcher/domain/entities/ecosystem/actor.ts @@ -1,5 +1,5 @@ -import { Role } from '../types/role.enum'; -import { Step } from '../types/step.enum'; +import { Role } from '../../types/role.enum'; +import { Step } from '../../types/step.enum'; import { Person } from './person'; export class Actor { diff --git a/src/modules/matcher/domain/entities/ad.ts b/src/modules/matcher/domain/entities/ecosystem/ad.ts similarity index 100% rename from src/modules/matcher/domain/entities/ad.ts rename to src/modules/matcher/domain/entities/ecosystem/ad.ts diff --git a/src/modules/matcher/domain/entities/algorithm-settings.ts b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts similarity index 82% rename from src/modules/matcher/domain/entities/algorithm-settings.ts rename to src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts index 7378baa..aa12abf 100644 --- a/src/modules/matcher/domain/entities/algorithm-settings.ts +++ b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts @@ -1,9 +1,9 @@ -import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface'; -import { DefaultAlgorithmSettings } from '../types/default-algorithm-settings.type'; -import { Algorithm } from '../types/algorithm.enum'; -import { TimingFrequency } from '../types/timing'; -import { ICreateGeorouter } from '../interfaces/georouter-creator.interface'; -import { IGeorouter } from '../interfaces/georouter.interface'; +import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface'; +import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type'; +import { Algorithm } from '../../types/algorithm.enum'; +import { TimingFrequency } from '../../types/timing'; +import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface'; +import { IGeorouter } from '../../interfaces/georouter.interface'; export class AlgorithmSettings { _algorithmSettingsRequest: IRequestAlgorithmSettings; diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/ecosystem/geography.ts similarity index 92% rename from src/modules/matcher/domain/entities/geography.ts rename to src/modules/matcher/domain/entities/ecosystem/geography.ts index 2fcfbee..592ef31 100644 --- a/src/modules/matcher/domain/entities/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -1,16 +1,16 @@ -import { MatcherException } from '../../exceptions/matcher.exception'; -import { IRequestGeography } from '../interfaces/geography-request.interface'; -import { PointType } from '../types/geography.enum'; -import { Point } from '../types/point.type'; +import { MatcherException } from '../../../exceptions/matcher.exception'; +import { IRequestGeography } from '../../interfaces/geography-request.interface'; +import { PointType } from '../../types/geography.enum'; +import { Point } from '../../types/point.type'; import { find } from 'geo-tz'; import { Route } from './route'; -import { Role } from '../types/role.enum'; -import { IGeorouter } from '../interfaces/georouter.interface'; +import { Role } from '../../types/role.enum'; +import { IGeorouter } from '../../interfaces/georouter.interface'; import { Waypoint } from './waypoint'; import { Actor } from './actor'; import { Person } from './person'; -import { Step } from '../types/step.enum'; -import { Path } from '../types/path.type'; +import { Step } from '../../types/step.enum'; +import { Path } from '../../types/path.type'; export class Geography { _geographyRequest: IRequestGeography; diff --git a/src/modules/matcher/domain/entities/match.ts b/src/modules/matcher/domain/entities/ecosystem/match.ts similarity index 100% rename from src/modules/matcher/domain/entities/match.ts rename to src/modules/matcher/domain/entities/ecosystem/match.ts diff --git a/src/modules/matcher/domain/entities/named-route.ts b/src/modules/matcher/domain/entities/ecosystem/named-route.ts similarity index 100% rename from src/modules/matcher/domain/entities/named-route.ts rename to src/modules/matcher/domain/entities/ecosystem/named-route.ts diff --git a/src/modules/matcher/domain/entities/person.ts b/src/modules/matcher/domain/entities/ecosystem/person.ts similarity index 93% rename from src/modules/matcher/domain/entities/person.ts rename to src/modules/matcher/domain/entities/ecosystem/person.ts index 3a1473f..7340d07 100644 --- a/src/modules/matcher/domain/entities/person.ts +++ b/src/modules/matcher/domain/entities/ecosystem/person.ts @@ -1,4 +1,4 @@ -import { IRequestPerson } from '../interfaces/person-request.interface'; +import { IRequestPerson } from '../../interfaces/person-request.interface'; export class Person { _personRequest: IRequestPerson; diff --git a/src/modules/matcher/domain/entities/requirement.ts b/src/modules/matcher/domain/entities/ecosystem/requirement.ts similarity index 82% rename from src/modules/matcher/domain/entities/requirement.ts rename to src/modules/matcher/domain/entities/ecosystem/requirement.ts index 194907f..40db4c6 100644 --- a/src/modules/matcher/domain/entities/requirement.ts +++ b/src/modules/matcher/domain/entities/ecosystem/requirement.ts @@ -1,4 +1,4 @@ -import { IRequestRequirement } from '../interfaces/requirement-request.interface'; +import { IRequestRequirement } from '../../interfaces/requirement-request.interface'; export class Requirement { _requirementRequest: IRequestRequirement; diff --git a/src/modules/matcher/domain/entities/route.ts b/src/modules/matcher/domain/entities/ecosystem/route.ts similarity index 93% rename from src/modules/matcher/domain/entities/route.ts rename to src/modules/matcher/domain/entities/ecosystem/route.ts index 6712699..d468187 100644 --- a/src/modules/matcher/domain/entities/route.ts +++ b/src/modules/matcher/domain/entities/ecosystem/route.ts @@ -1,5 +1,5 @@ -import { IGeodesic } from '../interfaces/geodesic.interface'; -import { Point } from '../types/point.type'; +import { IGeodesic } from '../../interfaces/geodesic.interface'; +import { Point } from '../../types/point.type'; import { SpacetimePoint } from './spacetime-point'; import { Waypoint } from './waypoint'; diff --git a/src/modules/matcher/domain/entities/spacetime-point.ts b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts similarity index 100% rename from src/modules/matcher/domain/entities/spacetime-point.ts rename to src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/ecosystem/time.ts similarity index 93% rename from src/modules/matcher/domain/entities/time.ts rename to src/modules/matcher/domain/entities/ecosystem/time.ts index b3b2ad8..c4a39c5 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -1,8 +1,8 @@ -import { MatcherException } from '../../exceptions/matcher.exception'; -import { MarginDurations } from '../types/margin-durations.type'; -import { IRequestTime } from '../interfaces/time-request.interface'; -import { TimingDays, TimingFrequency, Days } from '../types/timing'; -import { Schedule } from '../types/schedule.type'; +import { MatcherException } from '../../../exceptions/matcher.exception'; +import { MarginDurations } from '../../types/margin-durations.type'; +import { IRequestTime } from '../../interfaces/time-request.interface'; +import { TimingDays, TimingFrequency, Days } from '../../types/timing'; +import { Schedule } from '../../types/schedule.type'; export class Time { _timeRequest: IRequestTime; diff --git a/src/modules/matcher/domain/entities/waypoint.ts b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts similarity index 83% rename from src/modules/matcher/domain/entities/waypoint.ts rename to src/modules/matcher/domain/entities/ecosystem/waypoint.ts index fb93541..fdcbea0 100644 --- a/src/modules/matcher/domain/entities/waypoint.ts +++ b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts @@ -1,4 +1,4 @@ -import { Point } from '../types/point.type'; +import { Point } from '../../types/point.type'; import { Actor } from './actor'; export class Waypoint { diff --git a/src/modules/matcher/domain/entities/engine/candidate.ts b/src/modules/matcher/domain/entities/engine/candidate.ts new file mode 100644 index 0000000..1a19a59 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/candidate.ts @@ -0,0 +1,5 @@ +import { Person } from '../ecosystem/person'; + +export class Candidate { + person: Person; +} diff --git a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts new file mode 100644 index 0000000..67206e8 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts @@ -0,0 +1,15 @@ +import { MatchQuery } from 'src/modules/matcher/queries/match.query'; +import { Processor } from '../processor.abstract'; +import { Candidate } from '../candidate'; + +export abstract class AlgorithmFactory { + _matchQuery: MatchQuery; + _candidates: Array; + + constructor(matchQuery: MatchQuery) { + this._matchQuery = matchQuery; + this._candidates = []; + } + + abstract createProcessors(): Array; +} diff --git a/src/modules/matcher/domain/entities/engine/factory/classic.ts b/src/modules/matcher/domain/entities/engine/factory/classic.ts new file mode 100644 index 0000000..77a2d04 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/factory/classic.ts @@ -0,0 +1,9 @@ +import { AlgorithmFactory } from './algorithm-factory.abstract'; +import { Processor } from '../processor.abstract'; +import { ClassicWaypointsCompleter } from '../processor/completer/classic-waypoint.completer.processor'; + +export class ClassicAlgorithmFactory extends AlgorithmFactory { + createProcessors(): Array { + return [new ClassicWaypointsCompleter(this._matchQuery)]; + } +} diff --git a/src/modules/matcher/domain/entities/engine/matcher.ts b/src/modules/matcher/domain/entities/engine/matcher.ts new file mode 100644 index 0000000..bc6da9c --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/matcher.ts @@ -0,0 +1,21 @@ +import { MatchQuery } from '../../../queries/match.query'; +import { Algorithm } from '../../types/algorithm.enum'; +import { Match } from '../ecosystem/match'; +import { Candidate } from './candidate'; +import { AlgorithmFactory } from './factory/algorithm-factory.abstract'; +import { ClassicAlgorithmFactory } from './factory/classic'; + +export class Matcher { + match = (matchQuery: MatchQuery): Array => { + let algorithm: AlgorithmFactory; + switch (matchQuery.algorithmSettings.algorithm) { + case Algorithm.CLASSIC: + algorithm = new ClassicAlgorithmFactory(matchQuery); + } + let candidates: Array = []; + for (const processor of algorithm.createProcessors()) { + candidates = processor.execute(candidates); + } + return []; + }; +} diff --git a/src/modules/matcher/domain/entities/engine/processor.abstract.ts b/src/modules/matcher/domain/entities/engine/processor.abstract.ts new file mode 100644 index 0000000..c5df1a6 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor.abstract.ts @@ -0,0 +1,12 @@ +import { MatchQuery } from 'src/modules/matcher/queries/match.query'; +import { Candidate } from './candidate'; + +export abstract class Processor { + _matchQuery: MatchQuery; + + constructor(matchQuery: MatchQuery) { + this._matchQuery = matchQuery; + } + + abstract execute(candidates: Array): Array; +} diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts b/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts new file mode 100644 index 0000000..b55522a --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts @@ -0,0 +1,8 @@ +import { Candidate } from '../../candidate'; +import { Completer } from './completer.abstract'; + +export class ClassicWaypointsCompleter extends Completer { + complete(candidates: Array): Array { + return []; + } +} diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts b/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts new file mode 100644 index 0000000..29f408d --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts @@ -0,0 +1,9 @@ +import { Candidate } from '../../candidate'; +import { Processor } from '../../processor.abstract'; + +export abstract class Completer extends Processor { + execute = (candidates: Array): Array => + this.complete(candidates); + + abstract complete(candidates: Array): Array; +} diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index d80f508..44ce17b 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -4,8 +4,8 @@ import { QueryHandler } from '@nestjs/cqrs'; import { Messager } from '../../adapters/secondaries/messager'; import { MatchQuery } from '../../queries/match.query'; import { AdRepository } from '../../adapters/secondaries/ad.repository'; -import { Match } from '../entities/match'; -import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { Match } from '../entities/ecosystem/match'; +import { ICollection } from '../../../database/src/interfaces/collection.interface'; @QueryHandler(MatchQuery) export class MatchUseCase { diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 1380c65..b5c62fc 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -1,10 +1,10 @@ import { MatchRequest } from '../domain/dtos/match.request'; -import { Geography } from '../domain/entities/geography'; -import { Person } from '../domain/entities/person'; -import { Requirement } from '../domain/entities/requirement'; +import { Geography } from '../domain/entities/ecosystem/geography'; +import { Person } from '../domain/entities/ecosystem/person'; +import { Requirement } from '../domain/entities/ecosystem/requirement'; import { Role } from '../domain/types/role.enum'; -import { AlgorithmSettings } from '../domain/entities/algorithm-settings'; -import { Time } from '../domain/entities/time'; +import { AlgorithmSettings } from '../domain/entities/ecosystem/algorithm-settings'; +import { Time } from '../domain/entities/ecosystem/time'; import { IDefaultParams } from '../domain/types/default-params.type'; import { IGeorouter } from '../domain/interfaces/georouter.interface'; import { ICreateGeorouter } from '../domain/interfaces/georouter-creator.interface'; diff --git a/src/modules/matcher/tests/unit/domain/geography.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts similarity index 92% rename from src/modules/matcher/tests/unit/domain/geography.spec.ts rename to src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts index f616434..e4d20a1 100644 --- a/src/modules/matcher/tests/unit/domain/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -1,10 +1,13 @@ -import { Person } from '../../../domain/entities/person'; -import { Geography, RouteKey } from '../../../domain/entities/geography'; -import { Role } from '../../../domain/types/role.enum'; -import { NamedRoute } from '../../../domain/entities/named-route'; -import { Route } from '../../../domain/entities/route'; -import { IGeodesic } from '../../../domain/interfaces/geodesic.interface'; -import { PointType } from '../../../domain/types/geography.enum'; +import { Person } from '../../../../domain/entities/ecosystem/person'; +import { + Geography, + RouteKey, +} from '../../../../domain/entities/ecosystem/geography'; +import { Role } from '../../../../domain/types/role.enum'; +import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route'; +import { Route } from '../../../../domain/entities/ecosystem/route'; +import { IGeodesic } from '../../../../domain/interfaces/geodesic.interface'; +import { PointType } from '../../../../domain/types/geography.enum'; const person: Person = new Person( { diff --git a/src/modules/matcher/tests/unit/domain/person.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/person.spec.ts similarity index 93% rename from src/modules/matcher/tests/unit/domain/person.spec.ts rename to src/modules/matcher/tests/unit/domain/ecosystem/person.spec.ts index 56c2e47..c9d604c 100644 --- a/src/modules/matcher/tests/unit/domain/person.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/person.spec.ts @@ -1,4 +1,4 @@ -import { Person } from '../../../domain/entities/person'; +import { Person } from '../../../../domain/entities/ecosystem/person'; const DEFAULT_IDENTIFIER = 0; const MARGIN_DURATION = 900; diff --git a/src/modules/matcher/tests/unit/domain/route.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts similarity index 88% rename from src/modules/matcher/tests/unit/domain/route.spec.ts rename to src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts index 35b6214..16d27a3 100644 --- a/src/modules/matcher/tests/unit/domain/route.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts @@ -1,6 +1,6 @@ -import { Route } from '../../../domain/entities/route'; -import { SpacetimePoint } from '../../../domain/entities/spacetime-point'; -import { Waypoint } from '../../../domain/entities/waypoint'; +import { Route } from '../../../../domain/entities/ecosystem/route'; +import { SpacetimePoint } from '../../../../domain/entities/ecosystem/spacetime-point'; +import { Waypoint } from '../../../../domain/entities/ecosystem/waypoint'; const mockGeodesic = { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/modules/matcher/tests/unit/domain/time.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/time.spec.ts similarity index 98% rename from src/modules/matcher/tests/unit/domain/time.spec.ts rename to src/modules/matcher/tests/unit/domain/ecosystem/time.spec.ts index 5cc3929..fa5772e 100644 --- a/src/modules/matcher/tests/unit/domain/time.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/time.spec.ts @@ -1,4 +1,4 @@ -import { Time } from '../../../domain/entities/time'; +import { Time } from '../../../../domain/entities/ecosystem/time'; const MARGIN_DURATION = 900; const VALIDITY_DURATION = 365; From 5edf6895a614dbf0a030d0bfac14f3640f6982d4 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 21 Apr 2023 10:18:47 +0200 Subject: [PATCH 24/26] deactivate integration test --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 15b60fc..dfca5ef 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ test: - docker-compose -f docker-compose.ci.tools.yml -p matcher-tools --env-file ci/.env.ci up -d - sh ci/wait-up.sh - docker-compose -f docker-compose.ci.service.yml -p matcher-service --env-file ci/.env.ci up -d - - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci" + # - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci" coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ rules: - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' From 844f9f34e1a60d4f7459731e68ba6e2b9c70832b Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 21 Apr 2023 10:34:52 +0200 Subject: [PATCH 25/26] fix bad imports --- src/modules/matcher/domain/interfaces/georouter.interface.ts | 2 +- src/modules/matcher/domain/types/actor.type..ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/matcher/domain/interfaces/georouter.interface.ts b/src/modules/matcher/domain/interfaces/georouter.interface.ts index 8f17972..5f09b23 100644 --- a/src/modules/matcher/domain/interfaces/georouter.interface.ts +++ b/src/modules/matcher/domain/interfaces/georouter.interface.ts @@ -1,4 +1,4 @@ -import { NamedRoute } from '../entities/named-route'; +import { NamedRoute } from '../entities/ecosystem/named-route'; import { GeorouterSettings } from '../types/georouter-settings.type'; import { Path } from '../types/path.type'; diff --git a/src/modules/matcher/domain/types/actor.type..ts b/src/modules/matcher/domain/types/actor.type..ts index 6edd39a..aecaa9e 100644 --- a/src/modules/matcher/domain/types/actor.type..ts +++ b/src/modules/matcher/domain/types/actor.type..ts @@ -1,4 +1,4 @@ -import { Person } from '../entities/person'; +import { Person } from '../entities/ecosystem/person'; import { Role } from './role.enum'; import { Step } from './step.enum'; From d4874f3030d96f0b58815a40be6b23f6d026746c Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 21 Apr 2023 10:37:13 +0200 Subject: [PATCH 26/26] fix bad imports --- src/modules/matcher/mappers/match.profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/matcher/mappers/match.profile.ts b/src/modules/matcher/mappers/match.profile.ts index c709136..c44fef8 100644 --- a/src/modules/matcher/mappers/match.profile.ts +++ b/src/modules/matcher/mappers/match.profile.ts @@ -2,7 +2,7 @@ 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'; +import { Match } from '../domain/entities/ecosystem/match'; @Injectable() export class MatchProfile extends AutomapperProfile {