get candidates in ad repository

This commit is contained in:
sbriat 2023-09-04 10:15:02 +02:00
parent f4097e96eb
commit 717d047aa8
8 changed files with 227 additions and 92 deletions

View File

@ -4,7 +4,7 @@ import {
AdWriteModel, AdWriteModel,
AdReadModel, AdReadModel,
ScheduleItemModel, ScheduleItemModel,
AdUnsupportedWriteModel, AdWriteExtraModel,
} from './infrastructure/ad.repository'; } from './infrastructure/ad.repository';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
@ -26,7 +26,7 @@ export class AdMapper
AdEntity, AdEntity,
AdReadModel, AdReadModel,
AdWriteModel, AdWriteModel,
AdUnsupportedWriteModel, AdWriteExtraModel,
undefined undefined
> >
{ {
@ -119,7 +119,7 @@ export class AdMapper
return entity; return entity;
}; };
toUnsupportedPersistence = (entity: AdEntity): AdUnsupportedWriteModel => ({ toPersistenceExtra = (entity: AdEntity): AdWriteExtraModel => ({
waypoints: this.directionEncoder.encode(entity.getProps().waypoints), waypoints: this.directionEncoder.encode(entity.getProps().waypoints),
direction: this.directionEncoder.encode(entity.getProps().points), direction: this.directionEncoder.encode(entity.getProps().points),
}); });

View File

@ -49,7 +49,7 @@ export class CreateAdService implements ICommandHandler {
}); });
try { try {
await this.repository.insertWithUnsupportedFields(ad, 'ad'); await this.repository.insertExtra(ad, 'ad');
return ad.id; return ad.id;
} catch (error: any) { } catch (error: any) {
if (error instanceof ConflictException) { if (error instanceof ConflictException) {

View File

@ -62,8 +62,6 @@ export class PassengerOrientedSelector extends Selector {
si.day,si.time,si.margin si.day,si.time,si.margin
FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid"
WHERE passenger=True`; WHERE passenger=True`;
// await this.repository.getCandidates(this.query);
} }
export type QueryStringRole = { export type QueryStringRole = {

View File

@ -9,7 +9,7 @@ import { AdMapper } from '../ad.mapper';
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
import { Frequency } from '../core/domain/ad.types'; import { Frequency } from '../core/domain/ad.types';
export type AdBaseModel = { export type AdModel = {
uuid: string; uuid: string;
driver: boolean; driver: boolean;
passenger: boolean; passenger: boolean;
@ -29,62 +29,42 @@ export type AdBaseModel = {
updatedAt: Date; updatedAt: Date;
}; };
export type AdReadModel = AdBaseModel & { export type AdReadModel = AdModel & {
waypoints: string; waypoints: string;
schedule: ScheduleItemModel[]; schedule: ScheduleItemModel[];
}; };
export type AdWriteModel = AdBaseModel & { export type AdWriteModel = AdModel & {
schedule: { schedule: {
create: ScheduleItemModel[]; create: ScheduleItemModel[];
}; };
}; };
export type AdUnsupportedWriteModel = { export type AdWriteExtraModel = {
waypoints: string; waypoints: string;
direction: string; direction: string;
}; };
export type ScheduleItemModel = { export type ScheduleItem = {
uuid: string;
day: number; day: number;
time: Date; time: Date;
margin: number; margin: number;
};
export type ScheduleItemModel = ScheduleItem & {
uuid: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
}; };
export type RawAdBaseModel = { export type UngroupedAdModel = AdModel &
uuid: string; ScheduleItem & {
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;
waypoints: string; waypoints: string;
createdAt: Date; };
updatedAt: Date;
};
export type RawScheduleItemModel = { export type GroupedAdModel = AdModel & {
day: number; schedule: ScheduleItem[];
time: Date; waypoints: string;
margin: number;
};
export type RawAdModel = RawAdBaseModel & RawScheduleItemModel;
export type RawAdReadModel = RawAdBaseModel & {
schedule: RawScheduleItemModel[];
}; };
/** /**
@ -96,7 +76,7 @@ export class AdRepository
AdEntity, AdEntity,
AdReadModel, AdReadModel,
AdWriteModel, AdWriteModel,
AdUnsupportedWriteModel AdWriteExtraModel
> >
implements AdRepositoryPort implements AdRepositoryPort
{ {
@ -121,40 +101,44 @@ export class AdRepository
} }
getCandidates = async (queryString: string): Promise<AdReadModel[]> => getCandidates = async (queryString: string): Promise<AdReadModel[]> =>
this.toReadModels((await this.queryRawUnsafe(queryString)) as RawAdModel[]); this.toAdReadModels(
(await this.prismaRaw.$queryRawUnsafe(queryString)) as UngroupedAdModel[],
);
private toReadModels = (rawAds: RawAdModel[]): AdReadModel[] => { private toAdReadModels = (
const rawAdReadModels: RawAdReadModel[] = rawAds.map( ungroupedAds: UngroupedAdModel[],
(rawAd: RawAdModel) => ({ ): AdReadModel[] => {
uuid: rawAd.uuid, const groupedAdModels: GroupedAdModel[] = ungroupedAds.map(
driver: rawAd.driver, (ungroupedAd: UngroupedAdModel) => ({
passenger: rawAd.passenger, uuid: ungroupedAd.uuid,
frequency: rawAd.frequency, driver: ungroupedAd.driver,
fromDate: rawAd.fromDate, passenger: ungroupedAd.passenger,
toDate: rawAd.toDate, frequency: ungroupedAd.frequency,
fromDate: ungroupedAd.fromDate,
toDate: ungroupedAd.toDate,
schedule: [ schedule: [
{ {
day: rawAd.day, day: ungroupedAd.day,
time: rawAd.time, time: ungroupedAd.time,
margin: rawAd.margin, margin: ungroupedAd.margin,
}, },
], ],
seatsProposed: rawAd.seatsProposed, seatsProposed: ungroupedAd.seatsProposed,
seatsRequested: rawAd.seatsRequested, seatsRequested: ungroupedAd.seatsRequested,
strict: rawAd.strict, strict: ungroupedAd.strict,
driverDuration: rawAd.driverDuration, driverDuration: ungroupedAd.driverDuration,
driverDistance: rawAd.driverDistance, driverDistance: ungroupedAd.driverDistance,
passengerDuration: rawAd.passengerDuration, passengerDuration: ungroupedAd.passengerDuration,
passengerDistance: rawAd.passengerDistance, passengerDistance: ungroupedAd.passengerDistance,
fwdAzimuth: rawAd.fwdAzimuth, fwdAzimuth: ungroupedAd.fwdAzimuth,
backAzimuth: rawAd.backAzimuth, backAzimuth: ungroupedAd.backAzimuth,
waypoints: rawAd.waypoints, waypoints: ungroupedAd.waypoints,
createdAt: rawAd.createdAt, createdAt: ungroupedAd.createdAt,
updatedAt: rawAd.updatedAt, updatedAt: ungroupedAd.updatedAt,
}), }),
); );
const adReadModels: AdReadModel[] = []; const adReadModels: AdReadModel[] = [];
rawAdReadModels.forEach((adReadModel: AdReadModel) => { groupedAdModels.forEach((adReadModel: AdReadModel) => {
const ad: AdReadModel | undefined = adReadModels.find( const ad: AdReadModel | undefined = adReadModels.find(
(arm: AdReadModel) => arm.uuid == adReadModel.uuid, (arm: AdReadModel) => arm.uuid == adReadModel.uuid,
); );

View File

@ -4,7 +4,7 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency } from '@modules/ad/core/domain/ad.types'; import { Frequency } from '@modules/ad/core/domain/ad.types';
import { import {
AdReadModel, AdReadModel,
AdUnsupportedWriteModel, AdWriteExtraModel,
AdWriteModel, AdWriteModel,
} from '@modules/ad/infrastructure/ad.repository'; } from '@modules/ad/infrastructure/ad.repository';
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; 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 () => { it('should map domain entity to unsupported db persistence data', async () => {
const mapped: AdUnsupportedWriteModel = const mapped: AdWriteExtraModel = adMapper.toPersistenceExtra(adEntity);
adMapper.toUnsupportedPersistence(adEntity);
expect(mapped.waypoints).toBe( expect(mapped.waypoints).toBe(
"'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'", "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'",
); );

View File

@ -48,7 +48,7 @@ const createAdProps: CreateAdProps = {
}; };
const mockAdRepository = { const mockAdRepository = {
insertWithUnsupportedFields: jest insertExtra: jest
.fn() .fn()
.mockImplementationOnce(() => ({})) .mockImplementationOnce(() => ({}))
.mockImplementationOnce(() => { .mockImplementationOnce(() => {

View File

@ -42,7 +42,7 @@ const matchQuery = new MatchQuery({
}); });
const mockMatcherRepository: AdRepositoryPort = { const mockMatcherRepository: AdRepositoryPort = {
insertWithUnsupportedFields: jest.fn(), insertExtra: jest.fn(),
findOneById: jest.fn(), findOneById: jest.fn(),
findOne: jest.fn(), findOne: jest.fn(),
insert: jest.fn(), insert: jest.fn(),
@ -51,7 +51,6 @@ const mockMatcherRepository: AdRepositoryPort = {
delete: jest.fn(), delete: jest.fn(),
count: jest.fn(), count: jest.fn(),
healthCheck: jest.fn(), healthCheck: jest.fn(),
queryRawUnsafe: jest.fn(),
getCandidates: jest.fn().mockImplementation(() => [ getCandidates: jest.fn().mockImplementation(() => [
{ {
ad: { ad: {

View File

@ -1,13 +1,18 @@
import { import {
AD_DIRECTION_ENCODER, AD_DIRECTION_ENCODER,
AD_MESSAGE_PUBLISHER,
AD_ROUTE_PROVIDER, AD_ROUTE_PROVIDER,
} from '@modules/ad/ad.di-tokens'; } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper'; import { AdMapper } from '@modules/ad/ad.mapper';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; 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 { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; 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'; import { Test, TestingModule } from '@nestjs/testing';
const mockMessagePublisher = { const mockMessagePublisher = {
@ -23,17 +28,146 @@ const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(), 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', () => { describe('Ad repository', () => {
let prismaService: PrismaService; let adRepository: AdRepository;
let adMapper: AdMapper;
let eventEmitter: EventEmitter2;
beforeAll(async () => { beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
imports: [EventEmitterModule.forRoot()], imports: [EventEmitterModule.forRoot()],
providers: [ providers: [
PrismaService,
AdMapper, AdMapper,
AdRepository,
{ {
provide: AD_DIRECTION_ENCODER, provide: AD_DIRECTION_ENCODER,
useValue: mockDirectionEncoder, useValue: mockDirectionEncoder,
@ -42,21 +176,42 @@ describe('Ad repository', () => {
provide: AD_ROUTE_PROVIDER, provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider, useValue: mockRouteProvider,
}, },
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
{
provide: PrismaService,
useValue: mockPrismaService,
},
], ],
}).compile(); }).compile();
prismaService = module.get<PrismaService>(PrismaService); adRepository = module.get<AdRepository>(AdRepository);
adMapper = module.get<AdMapper>(AdMapper);
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
}); });
it('should be defined', () => { it('should be defined', () => {
expect( expect(adRepository).toBeDefined();
new AdRepository( });
prismaService,
adMapper, it('should get candidates if query returns punctual Ads', async () => {
eventEmitter, const candidates: AdReadModel[] = await adRepository.getCandidates(
mockMessagePublisher, 'somePunctualQueryString',
), );
).toBeDefined(); 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);
}); });
}); });