create dedicated selectors

This commit is contained in:
sbriat 2023-08-31 17:07:34 +02:00
parent 657f8e7a03
commit 8269242d28
10 changed files with 197 additions and 103 deletions

View File

@ -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<AdEntity> & {
getCandidates(query: MatchQuery): Promise<Candidate[]>;
getCandidates(queryString: string): Promise<AdReadModel[]>;
};

View File

@ -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<MatchEntity[]>;
match = async (): Promise<MatchEntity[]> => {
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<Candidate[]>;
}
/**
* A processor processes candidates information
*/
export abstract class Processor {
protected readonly query: MatchQuery;
constructor(query: MatchQuery) {
this.query = query;
}
abstract execute(candidates: Candidate[]): Promise<Candidate[]>;
}

View File

@ -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<Candidate[]> =>
this.complete(candidates);

View File

@ -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<Candidate[]> =>
this.filter(candidates);

View File

@ -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:

View File

@ -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) =>
// <Candidate>{
// ad: {
// id: adEntity.id,
// },
// role: adsRole.role,
// },
// ),
// )
// .flat();
match = async (): Promise<MatchEntity[]> => {
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 }),
);
};
}

View File

@ -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<Candidate[]> => {
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) =>
<Candidate>{
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;
};

View File

@ -4,10 +4,6 @@ export enum AlgorithmType {
PASSENGER_ORIENTED = 'PASSENGER_ORIENTED',
}
export interface Processor {
execute(candidates: Candidate[]): Promise<Candidate[]>;
}
export type Candidate = {
ad: Ad;
role: Role;

View File

@ -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<Candidate[]> => {
// 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<AdReadModel[]> =>
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;
};

View File

@ -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,