diff --git a/src/modules/ad/core/application/ports/ad.repository.port.ts b/src/modules/ad/core/application/ports/ad.repository.port.ts index 8e9674b..a123015 100644 --- a/src/modules/ad/core/application/ports/ad.repository.port.ts +++ b/src/modules/ad/core/application/ports/ad.repository.port.ts @@ -1,8 +1,7 @@ import { ExtendedRepositoryPort } from '@mobicoop/ddd-library'; import { AdEntity } from '../../domain/ad.entity'; -import { Candidate } from '../types/algorithm.types'; -import { MatchQuery } from '../queries/match/match.query'; +import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; export type AdRepositoryPort = ExtendedRepositoryPort & { - getCandidates(query: MatchQuery): Promise; + getCandidates(queryString: string): Promise; }; diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index 3bd3f82..c5df2d4 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -1,10 +1,11 @@ import { MatchEntity } from '../../../domain/match.entity'; -import { Candidate, Processor } from '../../types/algorithm.types'; +import { Candidate } from '../../types/algorithm.types'; import { MatchQuery } from './match.query'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; export abstract class Algorithm { protected candidates: Candidate[]; + protected selector: Selector; protected processors: Processor[]; constructor( protected readonly query: MatchQuery, @@ -14,5 +15,37 @@ export abstract class Algorithm { /** * Filter candidates that matches the query */ - abstract match(): Promise; + match = async (): Promise => { + this.candidates = await this.selector.select(); + for (const processor of this.processors) { + this.candidates = await processor.execute(this.candidates); + } + return this.candidates.map((candidate: Candidate) => + MatchEntity.create({ adId: candidate.ad.id }), + ); + }; +} + +/** + * A selector queries potential candidates in a repository + */ +export abstract class Selector { + protected readonly query: MatchQuery; + protected readonly repository: AdRepositoryPort; + constructor(query: MatchQuery, repository: AdRepositoryPort) { + this.query = query; + this.repository = repository; + } + abstract select(): Promise; +} + +/** + * A processor processes candidates information + */ +export abstract class Processor { + protected readonly query: MatchQuery; + constructor(query: MatchQuery) { + this.query = query; + } + abstract execute(candidates: Candidate[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts index 9034bcc..883edd0 100644 --- a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts +++ b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts @@ -1,6 +1,7 @@ -import { Candidate, Processor } from '../../../types/algorithm.types'; +import { Candidate } from '../../../types/algorithm.types'; +import { Processor } from '../algorithm.abstract'; -export abstract class Completer implements Processor { +export abstract class Completer extends Processor { execute = async (candidates: Candidate[]): Promise => this.complete(candidates); diff --git a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts index 5461f33..8262592 100644 --- a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts +++ b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts @@ -1,6 +1,7 @@ -import { Candidate, Processor } from '../../../types/algorithm.types'; +import { Candidate } from '../../../types/algorithm.types'; +import { Processor } from '../algorithm.abstract'; -export abstract class Filter implements Processor { +export abstract class Filter extends Processor { execute = async (candidates: Candidate[]): Promise => this.filter(candidates); diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 23cac57..86a3b17 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -51,8 +51,6 @@ export class MatchQueryHandler implements IQueryHandler { }) .setDatesAndSchedule(this.datetimeTransformer); - console.log(query); - let algorithm: Algorithm; switch (query.algorithmType) { case AlgorithmType.PASSENGER_ORIENTED: diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index 16ac2e7..8ca7147 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -3,9 +3,7 @@ import { MatchQuery } from './match.query'; import { PassengerOrientedWaypointsCompleter } from './completer/passenger-oriented-waypoints.completer'; import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; -import { Role } from '@modules/ad/core/domain/ad.types'; -import { MatchEntity } from '@modules/ad/core/domain/match.entity'; -import { Candidate } from '../../types/algorithm.types'; +import { PassengerOrientedSelector } from './selector/passenger-oriented.selector'; export class PassengerOrientedAlgorithm extends Algorithm { constructor( @@ -13,52 +11,10 @@ export class PassengerOrientedAlgorithm extends Algorithm { protected readonly repository: AdRepositoryPort, ) { super(query, repository); + this.selector = new PassengerOrientedSelector(query, repository); this.processors = [ - new PassengerOrientedWaypointsCompleter(), - new PassengerOrientedGeoFilter(), - ]; - this.candidates = [ - { - ad: { - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - }, - role: Role.DRIVER, - }, + new PassengerOrientedWaypointsCompleter(query), + new PassengerOrientedGeoFilter(query), ]; } - // this.candidates = ( - // await Promise.all( - // sqlQueries.map( - // async (queryRole: QueryRole) => - // ({ - // ads: (await this.repository.queryRawUnsafe( - // queryRole.query, - // )) as AdEntity[], - // role: queryRole.role, - // } as AdsRole), - // ), - // ) - // ) - // .map((adsRole: AdsRole) => - // adsRole.ads.map( - // (adEntity: AdEntity) => - // { - // ad: { - // id: adEntity.id, - // }, - // role: adsRole.role, - // }, - // ), - // ) - // .flat(); - - match = async (): Promise => { - this.candidates = await this.repository.getCandidates(this.query); - for (const processor of this.processors) { - this.candidates = await processor.execute(this.candidates); - } - return this.candidates.map((candidate: Candidate) => - MatchEntity.create({ adId: candidate.ad.id }), - ); - }; } 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 new file mode 100644 index 0000000..2f20b57 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -0,0 +1,72 @@ +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Candidate } from '../../../types/algorithm.types'; +import { Selector } from '../algorithm.abstract'; +import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; + +export class PassengerOrientedSelector extends Selector { + select = async (): Promise => { + const queryStringRoles: QueryStringRole[] = []; + if (this.query.driver) + queryStringRoles.push({ + query: this.asDriverQueryString(), + role: Role.DRIVER, + }); + if (this.query.passenger) + queryStringRoles.push({ + query: this.asPassengerQueryString(), + role: Role.PASSENGER, + }); + + return ( + await Promise.all( + queryStringRoles.map(async (queryStringRole: QueryStringRole) => ({ + ads: await this.repository.getCandidates(queryStringRole.query), + role: queryStringRole.role, + })), + ) + ) + .map((adsRole) => + adsRole.ads.map( + (adReadModel: AdReadModel) => + { + ad: { + id: adReadModel.uuid, + }, + role: adsRole.role, + }, + ), + ) + .flat(); + }; + + private asPassengerQueryString = (): string => `SELECT + ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, + "fromDate","toDate", + "seatsProposed","seatsRequested", + strict, + "driverDuration","driverDistance", + "passengerDuration","passengerDistance", + "fwdAzimuth","backAzimuth", + si.day,si.time,si.margin + FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" + WHERE driver=True`; + + private asDriverQueryString = (): string => `SELECT + ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, + "fromDate","toDate", + "seatsProposed","seatsRequested", + strict, + "driverDuration","driverDistance", + "passengerDuration","passengerDistance", + "fwdAzimuth","backAzimuth", + 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 = { + query: string; + role: Role; +}; diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts index 017f6a4..a3e75a8 100644 --- a/src/modules/ad/core/application/types/algorithm.types.ts +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -4,10 +4,6 @@ export enum AlgorithmType { PASSENGER_ORIENTED = 'PASSENGER_ORIENTED', } -export interface Processor { - execute(candidates: Candidate[]): Promise; -} - export type Candidate = { ad: Ad; role: Role; diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 551579e..a0d220c 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -7,10 +7,7 @@ import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; import { AdEntity } from '../core/domain/ad.entity'; import { AdMapper } from '../ad.mapper'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; -import { Frequency, Role } from '../core/domain/ad.types'; -import { Candidate } from '../core/application/types/algorithm.types'; -import { AdSelector } from './ad.selector'; -import { MatchQuery } from '../core/application/queries/match/match.query'; +import { Frequency } from '../core/domain/ad.types'; export type AdBaseModel = { uuid: string; @@ -34,7 +31,6 @@ export type AdBaseModel = { export type AdReadModel = AdBaseModel & { waypoints: string; - direction: string; schedule: ScheduleItemModel[]; }; @@ -58,6 +54,39 @@ export type ScheduleItemModel = { 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; + 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[]; +}; + /** * Repository is used for retrieving/saving domain entities * */ @@ -91,39 +120,50 @@ export class AdRepository ); } - getCandidates = async (query: MatchQuery): Promise => { - // let candidates: Candidate[] = []; - const sqlQueries: QueryRole[] = []; - if (query.driver) - sqlQueries.push({ - query: AdSelector.select(Role.DRIVER, query), - role: Role.DRIVER, - }); - if (query.passenger) - sqlQueries.push({ - query: AdSelector.select(Role.PASSENGER, query), - role: Role.PASSENGER, - }); - const results = await Promise.all( - sqlQueries.map( - async (queryRole: QueryRole) => - ({ - ads: (await this.queryRawUnsafe(queryRole.query)) as AdEntity[], - role: queryRole.role, - } as AdsRole), - ), + getCandidates = async (queryString: string): Promise => + this.toReadModels((await this.queryRawUnsafe(queryString)) as RawAdModel[]); + + 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, + schedule: [ + { + day: rawAd.day, + time: rawAd.time, + margin: rawAd.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, + }), ); - // console.log(results[0].ads); - return []; + const adReadModels: AdReadModel[] = []; + rawAdReadModels.forEach((adReadModel: AdReadModel) => { + const ad: AdReadModel | undefined = adReadModels.find( + (arm: AdReadModel) => arm.uuid == adReadModel.uuid, + ); + if (ad) { + ad.schedule.push(...adReadModel.schedule); + } else { + adReadModels.push(adReadModel); + } + }); + return adReadModels; }; } - -type QueryRole = { - query: string; - role: Role; -}; - -type AdsRole = { - ads: AdEntity[]; - role: Role; -}; diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index 925961a..965ff6a 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -84,8 +84,6 @@ const adReadModel: AdReadModel = { }, ], waypoints: "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'", - direction: - "'LINESTRING(6.1765102 48.689445,5.12345 48.76543,2.3522 48.8566)'", driverDistance: 350000, driverDuration: 14400, passengerDistance: 350000,