From 717d047aa80525e0d7ef7bf2cd986f4bb2f5921b Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 4 Sep 2023 10:15:02 +0200 Subject: [PATCH] get candidates in ad repository --- src/modules/ad/ad.mapper.ts | 6 +- .../commands/create-ad/create-ad.service.ts | 2 +- .../selector/passenger-oriented.selector.ts | 2 - .../ad/infrastructure/ad.repository.ts | 110 +++++----- src/modules/ad/tests/unit/ad.mapper.spec.ts | 5 +- .../tests/unit/core/create-ad.service.spec.ts | 2 +- .../core/passenger-oriented-algorithm.spec.ts | 3 +- .../unit/infrastructure/ad.repository.spec.ts | 189 ++++++++++++++++-- 8 files changed, 227 insertions(+), 92 deletions(-) diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 75df1d5..731bf73 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -4,7 +4,7 @@ import { AdWriteModel, AdReadModel, ScheduleItemModel, - AdUnsupportedWriteModel, + AdWriteExtraModel, } from './infrastructure/ad.repository'; import { v4 } from 'uuid'; import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; @@ -26,7 +26,7 @@ export class AdMapper AdEntity, AdReadModel, AdWriteModel, - AdUnsupportedWriteModel, + AdWriteExtraModel, undefined > { @@ -119,7 +119,7 @@ export class AdMapper return entity; }; - toUnsupportedPersistence = (entity: AdEntity): AdUnsupportedWriteModel => ({ + toPersistenceExtra = (entity: AdEntity): AdWriteExtraModel => ({ waypoints: this.directionEncoder.encode(entity.getProps().waypoints), direction: this.directionEncoder.encode(entity.getProps().points), }); diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 2ec8f5a..2e5b606 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -49,7 +49,7 @@ export class CreateAdService implements ICommandHandler { }); try { - await this.repository.insertWithUnsupportedFields(ad, 'ad'); + await this.repository.insertExtra(ad, 'ad'); return ad.id; } catch (error: any) { if (error instanceof ConflictException) { diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 2f20b57..86ae892 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -62,8 +62,6 @@ export class PassengerOrientedSelector extends Selector { si.day,si.time,si.margin FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" WHERE passenger=True`; - - // await this.repository.getCandidates(this.query); } export type QueryStringRole = { diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index a0d220c..e29cde9 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -9,7 +9,7 @@ import { AdMapper } from '../ad.mapper'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; import { Frequency } from '../core/domain/ad.types'; -export type AdBaseModel = { +export type AdModel = { uuid: string; driver: boolean; passenger: boolean; @@ -29,62 +29,42 @@ export type AdBaseModel = { updatedAt: Date; }; -export type AdReadModel = AdBaseModel & { +export type AdReadModel = AdModel & { waypoints: string; schedule: ScheduleItemModel[]; }; -export type AdWriteModel = AdBaseModel & { +export type AdWriteModel = AdModel & { schedule: { create: ScheduleItemModel[]; }; }; -export type AdUnsupportedWriteModel = { +export type AdWriteExtraModel = { waypoints: string; direction: string; }; -export type ScheduleItemModel = { - uuid: string; +export type ScheduleItem = { day: number; time: Date; margin: number; +}; + +export type ScheduleItemModel = ScheduleItem & { + uuid: string; createdAt: Date; updatedAt: Date; }; -export type RawAdBaseModel = { - uuid: string; - driver: boolean; - passenger: boolean; - frequency: Frequency; - fromDate: Date; - toDate: Date; - seatsProposed: number; - seatsRequested: number; - strict: boolean; - driverDuration?: number; - driverDistance?: number; - passengerDuration?: number; - passengerDistance?: number; - fwdAzimuth: number; - backAzimuth: number; +export type UngroupedAdModel = AdModel & + ScheduleItem & { + waypoints: string; + }; + +export type GroupedAdModel = AdModel & { + schedule: ScheduleItem[]; waypoints: string; - createdAt: Date; - updatedAt: Date; -}; - -export type RawScheduleItemModel = { - day: number; - time: Date; - margin: number; -}; - -export type RawAdModel = RawAdBaseModel & RawScheduleItemModel; - -export type RawAdReadModel = RawAdBaseModel & { - schedule: RawScheduleItemModel[]; }; /** @@ -96,7 +76,7 @@ export class AdRepository AdEntity, AdReadModel, AdWriteModel, - AdUnsupportedWriteModel + AdWriteExtraModel > implements AdRepositoryPort { @@ -121,40 +101,44 @@ export class AdRepository } getCandidates = async (queryString: string): Promise => - this.toReadModels((await this.queryRawUnsafe(queryString)) as RawAdModel[]); + this.toAdReadModels( + (await this.prismaRaw.$queryRawUnsafe(queryString)) as UngroupedAdModel[], + ); - private toReadModels = (rawAds: RawAdModel[]): AdReadModel[] => { - const rawAdReadModels: RawAdReadModel[] = rawAds.map( - (rawAd: RawAdModel) => ({ - uuid: rawAd.uuid, - driver: rawAd.driver, - passenger: rawAd.passenger, - frequency: rawAd.frequency, - fromDate: rawAd.fromDate, - toDate: rawAd.toDate, + private toAdReadModels = ( + ungroupedAds: UngroupedAdModel[], + ): AdReadModel[] => { + const groupedAdModels: GroupedAdModel[] = ungroupedAds.map( + (ungroupedAd: UngroupedAdModel) => ({ + uuid: ungroupedAd.uuid, + driver: ungroupedAd.driver, + passenger: ungroupedAd.passenger, + frequency: ungroupedAd.frequency, + fromDate: ungroupedAd.fromDate, + toDate: ungroupedAd.toDate, schedule: [ { - day: rawAd.day, - time: rawAd.time, - margin: rawAd.margin, + day: ungroupedAd.day, + time: ungroupedAd.time, + margin: ungroupedAd.margin, }, ], - seatsProposed: rawAd.seatsProposed, - seatsRequested: rawAd.seatsRequested, - strict: rawAd.strict, - driverDuration: rawAd.driverDuration, - driverDistance: rawAd.driverDistance, - passengerDuration: rawAd.passengerDuration, - passengerDistance: rawAd.passengerDistance, - fwdAzimuth: rawAd.fwdAzimuth, - backAzimuth: rawAd.backAzimuth, - waypoints: rawAd.waypoints, - createdAt: rawAd.createdAt, - updatedAt: rawAd.updatedAt, + seatsProposed: ungroupedAd.seatsProposed, + seatsRequested: ungroupedAd.seatsRequested, + strict: ungroupedAd.strict, + driverDuration: ungroupedAd.driverDuration, + driverDistance: ungroupedAd.driverDistance, + passengerDuration: ungroupedAd.passengerDuration, + passengerDistance: ungroupedAd.passengerDistance, + fwdAzimuth: ungroupedAd.fwdAzimuth, + backAzimuth: ungroupedAd.backAzimuth, + waypoints: ungroupedAd.waypoints, + createdAt: ungroupedAd.createdAt, + updatedAt: ungroupedAd.updatedAt, }), ); const adReadModels: AdReadModel[] = []; - rawAdReadModels.forEach((adReadModel: AdReadModel) => { + groupedAdModels.forEach((adReadModel: AdReadModel) => { const ad: AdReadModel | undefined = adReadModels.find( (arm: AdReadModel) => arm.uuid == adReadModel.uuid, ); diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index 965ff6a..7e93238 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -4,7 +4,7 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AdReadModel, - AdUnsupportedWriteModel, + AdWriteExtraModel, AdWriteModel, } from '@modules/ad/infrastructure/ad.repository'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; @@ -147,8 +147,7 @@ describe('Ad Mapper', () => { }); it('should map domain entity to unsupported db persistence data', async () => { - const mapped: AdUnsupportedWriteModel = - adMapper.toUnsupportedPersistence(adEntity); + const mapped: AdWriteExtraModel = adMapper.toPersistenceExtra(adEntity); expect(mapped.waypoints).toBe( "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'", ); diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 29f7e2c..c843e08 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -48,7 +48,7 @@ const createAdProps: CreateAdProps = { }; const mockAdRepository = { - insertWithUnsupportedFields: jest + insertExtra: jest .fn() .mockImplementationOnce(() => ({})) .mockImplementationOnce(() => { diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index cc35ac0..5a17a2c 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -42,7 +42,7 @@ const matchQuery = new MatchQuery({ }); const mockMatcherRepository: AdRepositoryPort = { - insertWithUnsupportedFields: jest.fn(), + insertExtra: jest.fn(), findOneById: jest.fn(), findOne: jest.fn(), insert: jest.fn(), @@ -51,7 +51,6 @@ const mockMatcherRepository: AdRepositoryPort = { delete: jest.fn(), count: jest.fn(), healthCheck: jest.fn(), - queryRawUnsafe: jest.fn(), getCandidates: jest.fn().mockImplementation(() => [ { ad: { diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index 789b547..1ba6e92 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -1,13 +1,18 @@ import { AD_DIRECTION_ENCODER, + AD_MESSAGE_PUBLISHER, AD_ROUTE_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; -import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { + AdReadModel, + AdRepository, +} from '@modules/ad/infrastructure/ad.repository'; import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; -import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; +import { EventEmitterModule } from '@nestjs/event-emitter'; import { Test, TestingModule } from '@nestjs/testing'; const mockMessagePublisher = { @@ -23,17 +28,146 @@ const mockRouteProvider: RouteProviderPort = { getBasic: jest.fn(), }; +const mockPrismaService = { + $queryRawUnsafe: jest + .fn() + .mockImplementationOnce(() => { + return [ + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: new Date('2023-06-21'), + toDate: new Date('2023-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 3, + time: new Date('2023-06-21T07:05:00Z'), + margin: 900, + }, + { + uuid: '84af18ff-8779-4cac-9651-1ed5ab0713c4', + driver: true, + passenger: false, + frequency: Frequency.PUNCTUAL, + fromDate: new Date('2023-06-21'), + toDate: new Date('2023-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 349000, + driverDuration: 14300, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-18T14:16:10Z'), + updatedAt: new Date('2023-06-18T14:16:10Z'), + waypoints: 'LINESTRING(6.1765109 48.689455,2.3598 48.8589)', + day: 3, + time: new Date('2023-06-21T07:14:00Z'), + margin: 900, + }, + ]; + }) + .mockImplementationOnce(() => { + return [ + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.RECURRENT, + fromDate: new Date('2023-06-21'), + toDate: new Date('2024-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 3, + time: new Date('2023-06-21T07:05:00Z'), + margin: 900, + }, + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.RECURRENT, + fromDate: new Date('2023-06-21'), + toDate: new Date('2024-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 4, + time: new Date('2023-06-21T07:15:00Z'), + margin: 900, + }, + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.RECURRENT, + fromDate: new Date('2023-06-21'), + toDate: new Date('2024-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 5, + time: new Date('2023-06-21T07:16:00Z'), + margin: 900, + }, + ]; + }) + .mockImplementationOnce(() => { + return []; + }), +}; + describe('Ad repository', () => { - let prismaService: PrismaService; - let adMapper: AdMapper; - let eventEmitter: EventEmitter2; + let adRepository: AdRepository; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [EventEmitterModule.forRoot()], providers: [ - PrismaService, AdMapper, + AdRepository, { provide: AD_DIRECTION_ENCODER, useValue: mockDirectionEncoder, @@ -42,21 +176,42 @@ describe('Ad repository', () => { provide: AD_ROUTE_PROVIDER, useValue: mockRouteProvider, }, + { + provide: AD_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + { + provide: PrismaService, + useValue: mockPrismaService, + }, ], }).compile(); - prismaService = module.get(PrismaService); - adMapper = module.get(AdMapper); - eventEmitter = module.get(EventEmitter2); + adRepository = module.get(AdRepository); }); it('should be defined', () => { - expect( - new AdRepository( - prismaService, - adMapper, - eventEmitter, - mockMessagePublisher, - ), - ).toBeDefined(); + expect(adRepository).toBeDefined(); + }); + + it('should get candidates if query returns punctual Ads', async () => { + const candidates: AdReadModel[] = await adRepository.getCandidates( + 'somePunctualQueryString', + ); + expect(candidates.length).toBe(2); + }); + + it('should get candidates if query returns recurrent Ads', async () => { + const candidates: AdReadModel[] = await adRepository.getCandidates( + 'someRecurrentQueryString', + ); + expect(candidates.length).toBe(1); + expect(candidates[0].schedule.length).toBe(3); + }); + + it('should return an empty array of candidates if query does not return Ads', async () => { + const candidates: AdReadModel[] = await adRepository.getCandidates( + 'someQueryString', + ); + expect(candidates.length).toBe(0); }); });