refactor; create ad request validation

This commit is contained in:
sbriat 2023-04-26 12:10:22 +02:00
parent aeead7fb62
commit 5865464c53
28 changed files with 173 additions and 86 deletions

View File

@ -5,28 +5,44 @@ import { InjectMapper } from '@automapper/nestjs';
import { Mapper } from '@automapper/core'; import { Mapper } from '@automapper/core';
import { CommandBus } from '@nestjs/cqrs'; import { CommandBus } from '@nestjs/cqrs';
import { CreateAdCommand } from '../../commands/create-ad.command'; import { CreateAdCommand } from '../../commands/create-ad.command';
import { AdPresenter } from './ad.presenter';
import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; import { CreateAdRequest } from '../../domain/dtos/create-ad.request';
import { ValidationError, validateOrReject } from 'class-validator';
@Controller() @Controller()
export class AdMessagerController { export class AdMessagerController {
constructor( constructor(
private readonly _commandBus: CommandBus, private readonly commandBus: CommandBus,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
) {} ) {}
@RabbitSubscribe({ @RabbitSubscribe({
name: 'adCreated', name: 'adCreated',
}) })
async adCreatedHandler(message: string): Promise<AdPresenter> { async adCreatedHandler(message: string): Promise<void> {
try { try {
const createAdRequest: CreateAdRequest = JSON.parse(message); // parse message to conform to CreateAdRequest (not a real instance yet)
const ad: Ad = await this._commandBus.execute( const parsedMessage: CreateAdRequest = JSON.parse(message);
// create a real instance of CreateAdRequest from parsed message
const createAdRequest: CreateAdRequest = this.mapper.map(
parsedMessage,
CreateAdRequest,
CreateAdRequest,
);
console.log(createAdRequest);
// validate instance
await validateOrReject(createAdRequest);
const ad: Ad = await this.commandBus.execute(
new CreateAdCommand(createAdRequest), new CreateAdCommand(createAdRequest),
); );
return this._mapper.map(ad, Ad, AdPresenter); console.log(ad);
} catch (e) { } catch (e) {
console.log('error', e); if (Array.isArray(e)) {
e.forEach((error) =>
error instanceof ValidationError
? console.log(error.constraints)
: console.log(error),
);
}
} }
} }
} }

View File

@ -1,5 +1,18 @@
import { AutoMap } from '@automapper/classes'; import { AutoMap } from '@automapper/classes';
import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; import {
ArrayMinSize,
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { PointType } from '../../../geography/domain/types/point-type.enum';
import { Frequency } from '../types/frequency.enum';
import { Point } from '../../../geography/domain/types/point.type';
export class CreateAdRequest { export class CreateAdRequest {
@IsString() @IsString()
@ -15,9 +28,10 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
passenger: boolean; passenger: boolean;
@IsNumber() @IsNotEmpty()
@IsEnum(Frequency)
@AutoMap() @AutoMap()
frequency: number; frequency: Frequency;
@IsString() @IsString()
@AutoMap() @AutoMap()
@ -27,33 +41,40 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
toDate: string; toDate: string;
@IsOptional()
@IsString() @IsString()
@AutoMap() @AutoMap()
monTime: string; monTime: string | null;
@IsOptional()
@IsString() @IsString()
@AutoMap() @AutoMap()
tueTime: string; tueTime: string | null;
@IsOptional()
@IsString() @IsString()
@AutoMap() @AutoMap()
wedTime: string; wedTime: string | null;
@IsOptional()
@IsString() @IsString()
@AutoMap() @AutoMap()
thuTime: string; thuTime!: string | null;
@IsOptional()
@IsString() @IsString()
@AutoMap() @AutoMap()
friTime: string; friTime: string | null;
@IsOptional()
@IsString() @IsString()
@AutoMap() @AutoMap()
satTime: string; satTime: string | null;
@IsOptional()
@IsString() @IsString()
@AutoMap() @AutoMap()
sunTime: string; sunTime: string | null;
@IsNumber() @IsNumber()
@AutoMap() @AutoMap()
@ -83,16 +104,19 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
sunMargin: number; sunMargin: number;
@IsNumber() @IsEnum(PointType)
@AutoMap() @AutoMap()
originType: number; originType: PointType;
@IsNumber() @IsEnum(PointType)
@AutoMap() @AutoMap()
destinationType: number; destinationType: PointType;
@IsArray()
@ArrayMinSize(2)
@ValidateNested({ each: true })
@AutoMap() @AutoMap()
waypoints: []; waypoints: Array<Point>;
@IsNumber() @IsNumber()
@AutoMap() @AutoMap()
@ -102,6 +126,7 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
seatsPassenger: number; seatsPassenger: number;
@IsOptional()
@IsNumber() @IsNumber()
@AutoMap() @AutoMap()
seatsUsed: number; seatsUsed: number;

View File

@ -1,4 +1,7 @@
import { AutoMap } from '@automapper/classes'; import { AutoMap } from '@automapper/classes';
import { ArrayMinSize, IsArray, IsEnum, ValidateNested } from 'class-validator';
import { PointType } from '../../../geography/domain/types/point-type.enum';
import { Point } from '../../../geography/domain/types/point.type';
export class Ad { export class Ad {
@AutoMap() @AutoMap()
@ -14,10 +17,10 @@ export class Ad {
frequency: number; frequency: number;
@AutoMap() @AutoMap()
fromDate: string; fromDate: Date;
@AutoMap() @AutoMap()
toDate: string; toDate: Date;
@AutoMap() @AutoMap()
monTime: string; monTime: string;
@ -73,14 +76,19 @@ export class Ad {
@AutoMap() @AutoMap()
passengerDistance: number; passengerDistance: number;
@IsEnum(PointType)
@AutoMap() @AutoMap()
originType: number; originType: PointType;
@IsEnum(PointType)
@AutoMap() @AutoMap()
destinationType: number; destinationType: PointType;
@IsArray()
@ArrayMinSize(2)
@ValidateNested({ each: true })
@AutoMap() @AutoMap()
waypoints: []; waypoints: Array<Point>;
@AutoMap() @AutoMap()
direction: string; direction: string;
@ -101,8 +109,8 @@ export class Ad {
seatsUsed: number; seatsUsed: number;
@AutoMap() @AutoMap()
createdAt: string; createdAt: Date;
@AutoMap() @AutoMap()
updatedAt: string; updatedAt: Date;
} }

View File

@ -0,0 +1,4 @@
export enum Frequency {
PUNCTUAL = 1,
RECURRENT = 2,
}

View File

@ -2,14 +2,26 @@ import { CommandHandler } from '@nestjs/cqrs';
import { CreateAdCommand } from '../../commands/create-ad.command'; import { CreateAdCommand } from '../../commands/create-ad.command';
import { Ad } from '../entities/ad'; import { Ad } from '../entities/ad';
import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AdRepository } from '../../adapters/secondaries/ad.repository';
import { InjectMapper } from '@automapper/nestjs';
import { Mapper } from '@automapper/core';
import { CreateAdRequest } from '../dtos/create-ad.request';
@CommandHandler(CreateAdCommand) @CommandHandler(CreateAdCommand)
export class CreateAdUseCase { export class CreateAdUseCase {
constructor(private readonly adRepository: AdRepository) {} constructor(
@InjectMapper() private readonly mapper: Mapper,
private readonly adRepository: AdRepository,
) {}
async execute(command: CreateAdCommand): Promise<Ad> { async execute(command: CreateAdCommand): Promise<Ad> {
try { try {
return await this.adRepository.createAd(command.createAdRequest); const adToCreate: Ad = this.mapper.map(
command.createAdRequest,
CreateAdRequest,
Ad,
);
return adToCreate;
// return await this.adRepository.createAd(adToCreate);
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -1,8 +1,9 @@
import { createMap, Mapper } from '@automapper/core'; import { createMap, forMember, mapFrom, Mapper } from '@automapper/core';
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Ad } from '../domain/entities/ad'; import { Ad } from '../domain/entities/ad';
import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { AdPresenter } from '../adapters/primaries/ad.presenter';
import { CreateAdRequest } from '../domain/dtos/create-ad.request';
@Injectable() @Injectable()
export class AdProfile extends AutomapperProfile { export class AdProfile extends AutomapperProfile {
@ -13,6 +14,28 @@ export class AdProfile extends AutomapperProfile {
override get profile() { override get profile() {
return (mapper: any) => { return (mapper: any) => {
createMap(mapper, Ad, AdPresenter); createMap(mapper, Ad, AdPresenter);
createMap(mapper, CreateAdRequest, CreateAdRequest);
createMap(
mapper,
CreateAdRequest,
Ad,
forMember(
(dest) => dest.fromDate,
mapFrom((source) => new Date(source.fromDate)),
),
forMember(
(dest) => dest.toDate,
mapFrom((source) => new Date(source.toDate)),
),
forMember(
(dest) => dest.createdAt,
mapFrom((source) => new Date(source.createdAt)),
),
forMember(
(dest) => dest.updatedAt,
mapFrom((source) => new Date(source.updatedAt)),
),
);
}; };
} }
} }

View File

@ -0,0 +1,4 @@
export type Coordinates = {
lon: number;
lat: number;
};

View File

@ -0,0 +1,6 @@
import { PointType } from './point-type.enum';
import { Coordinates } from './coordinates.type';
export type Point = Coordinates & {
type?: PointType;
};

View File

@ -154,7 +154,7 @@ export class GraphhopperGeorouter implements IGeorouter {
return indices.map( return indices.map(
(index) => (index) =>
new SpacetimePoint( new SpacetimePoint(
points[index], { lon: points[index][1], lat: points[index][0] },
times.find((time) => time.index == index)?.duration, times.find((time) => time.index == index)?.duration,
distances.find((distance) => distance.index == index)?.distance, distances.find((distance) => distance.index == index)?.distance,
), ),

View File

@ -10,7 +10,7 @@ import {
Min, Min,
} from 'class-validator'; } from 'class-validator';
import { AutoMap } from '@automapper/classes'; import { AutoMap } from '@automapper/classes';
import { Point } from '../types/point.type'; import { Point } from '../../../geography/domain/types/point.type';
import { Schedule } from '../types/schedule.type'; import { Schedule } from '../types/schedule.type';
import { MarginDurations } from '../types/margin-durations.type'; import { MarginDurations } from '../types/margin-durations.type';
import { AlgorithmType } from '../types/algorithm.enum'; import { AlgorithmType } from '../types/algorithm.enum';

View File

@ -1,15 +1,15 @@
import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface'; import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface';
import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type'; import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type';
import { AlgorithmType } from '../../types/algorithm.enum'; import { AlgorithmType } from '../../types/algorithm.enum';
import { TimingFrequency } from '../../types/timing';
import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface'; import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface';
import { IGeorouter } from '../../interfaces/georouter.interface'; import { IGeorouter } from '../../interfaces/georouter.interface';
import { Frequency } from '../../../../ad/domain/types/frequency.enum';
export class AlgorithmSettings { export class AlgorithmSettings {
private algorithmSettingsRequest: IRequestAlgorithmSettings; private algorithmSettingsRequest: IRequestAlgorithmSettings;
private strict: boolean; private strict: boolean;
algorithmType: AlgorithmType; algorithmType: AlgorithmType;
restrict: TimingFrequency; restrict: Frequency;
remoteness: number; remoteness: number;
useProportion: boolean; useProportion: boolean;
proportion: number; proportion: number;
@ -22,7 +22,7 @@ export class AlgorithmSettings {
constructor( constructor(
algorithmSettingsRequest: IRequestAlgorithmSettings, algorithmSettingsRequest: IRequestAlgorithmSettings,
defaultAlgorithmSettings: DefaultAlgorithmSettings, defaultAlgorithmSettings: DefaultAlgorithmSettings,
frequency: TimingFrequency, frequency: Frequency,
georouterCreator: ICreateGeorouter, georouterCreator: ICreateGeorouter,
) { ) {
this.algorithmSettingsRequest = algorithmSettingsRequest; this.algorithmSettingsRequest = algorithmSettingsRequest;

View File

@ -3,8 +3,8 @@ import {
MatcherExceptionCode, MatcherExceptionCode,
} from '../../../exceptions/matcher.exception'; } from '../../../exceptions/matcher.exception';
import { IRequestGeography } from '../../interfaces/geography-request.interface'; import { IRequestGeography } from '../../interfaces/geography-request.interface';
import { PointType } from '../../types/geography.enum'; import { PointType } from '../../../../geography/domain/types/point-type.enum';
import { Point } from '../../types/point.type'; import { Point } from '../../../../geography/domain/types/point.type';
import { Route } from './route'; import { Route } from './route';
import { Role } from '../../types/role.enum'; import { Role } from '../../types/role.enum';
import { IGeorouter } from '../../interfaces/georouter.interface'; import { IGeorouter } from '../../interfaces/georouter.interface';

View File

@ -1,5 +1,5 @@
import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface'; import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface';
import { Point } from '../../types/point.type'; import { Point } from '../../../../geography/domain/types/point.type';
import { SpacetimePoint } from './spacetime-point'; import { SpacetimePoint } from './spacetime-point';
import { Waypoint } from './waypoint'; import { Waypoint } from './waypoint';

View File

@ -1,10 +1,12 @@
import { Coordinates } from 'src/modules/geography/domain/types/coordinates.type';
export class SpacetimePoint { export class SpacetimePoint {
point: Array<number>; coordinates: Coordinates;
duration: number; duration: number;
distance: number; distance: number;
constructor(point: Array<number>, duration: number, distance: number) { constructor(coordinates: Coordinates, duration: number, distance: number) {
this.point = point; this.coordinates = coordinates;
this.duration = duration; this.duration = duration;
this.distance = distance; this.distance = distance;
} }

View File

@ -4,14 +4,16 @@ import {
} from '../../../exceptions/matcher.exception'; } from '../../../exceptions/matcher.exception';
import { MarginDurations } from '../../types/margin-durations.type'; import { MarginDurations } from '../../types/margin-durations.type';
import { IRequestTime } from '../../interfaces/time-request.interface'; import { IRequestTime } from '../../interfaces/time-request.interface';
import { TimingDays, TimingFrequency, Days } from '../../types/timing'; import { DAYS } from '../../types/days.const';
import { Schedule } from '../../types/schedule.type'; import { Schedule } from '../../types/schedule.type';
import { Frequency } from '../../../../ad/domain/types/frequency.enum';
import { Day } from '../../types/day.type';
export class Time { export class Time {
private timeRequest: IRequestTime; private timeRequest: IRequestTime;
private defaultMarginDuration: number; private defaultMarginDuration: number;
private defaultValidityDuration: number; private defaultValidityDuration: number;
frequency: TimingFrequency; frequency: Frequency;
fromDate: Date; fromDate: Date;
toDate: Date; toDate: Date;
schedule: Schedule; schedule: Schedule;
@ -106,7 +108,7 @@ export class Time {
} }
if ( if (
!Object.keys(this.timeRequest.schedule).some((elem) => !Object.keys(this.timeRequest.schedule).some((elem) =>
Days.includes(elem), DAYS.includes(elem),
) )
) { ) {
throw new MatcherException( throw new MatcherException(
@ -127,15 +129,15 @@ export class Time {
private setPunctualRequest = (): void => { private setPunctualRequest = (): void => {
if (this.timeRequest.departure) { if (this.timeRequest.departure) {
this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; this.frequency = Frequency.PUNCTUAL;
this.schedule[TimingDays[this.fromDate.getDay()]] = this.schedule[Day[this.fromDate.getDay()]] =
this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); this.fromDate.getHours() + ':' + this.fromDate.getMinutes();
} }
}; };
private setRecurrentRequest = (): void => { private setRecurrentRequest = (): void => {
if (this.timeRequest.fromDate) { if (this.timeRequest.fromDate) {
this.frequency = TimingFrequency.FREQUENCY_RECURRENT; this.frequency = Frequency.RECURRENT;
if (!this.toDate) { if (!this.toDate) {
this.toDate = this.addDays(this.fromDate, this.defaultValidityDuration); this.toDate = this.addDays(this.fromDate, this.defaultValidityDuration);
} }
@ -165,7 +167,7 @@ export class Time {
if (this.timeRequest.marginDurations) { if (this.timeRequest.marginDurations) {
if ( if (
!Object.keys(this.timeRequest.marginDurations).some((elem) => !Object.keys(this.timeRequest.marginDurations).some((elem) =>
Days.includes(elem), DAYS.includes(elem),
) )
) { ) {
throw new MatcherException( throw new MatcherException(

View File

@ -1,4 +1,4 @@
import { Point } from '../../types/point.type'; import { Point } from '../../../../geography/domain/types/point.type';
import { Actor } from './actor'; import { Actor } from './actor';
export class Waypoint { export class Waypoint {

View File

@ -1,4 +1,4 @@
import { Point } from '../types/point.type'; import { Point } from '../../../geography/domain/types/point.type';
export interface IRequestGeography { export interface IRequestGeography {
waypoints: Array<Point>; waypoints: Array<Point>;

View File

@ -0,0 +1,9 @@
export enum Day {
'sun',
'mon',
'tue',
'wed',
'thu',
'fri',
'sat',
}

View File

@ -0,0 +1 @@
export const DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];

View File

@ -1,4 +1,4 @@
import { Point } from './point.type'; import { Point } from '../../../geography/domain/types/point.type';
export type Path = { export type Path = {
key: string; key: string;

View File

@ -1,7 +0,0 @@
import { PointType } from './geography.enum';
export type Point = {
lon: number;
lat: number;
type?: PointType;
};

View File

@ -1,16 +0,0 @@
export enum TimingFrequency {
FREQUENCY_PUNCTUAL = 1,
FREQUENCY_RECURRENT = 2,
}
export enum TimingDays {
'sun',
'mon',
'tue',
'wed',
'thu',
'fri',
'sat',
}
export const Days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];

View File

@ -1,5 +1,5 @@
import { Actor } from './actor.type.'; import { Actor } from './actor.type.';
import { Point } from './point.type'; import { Point } from '../../../geography/domain/types/point.type';
export type Waypoint = { export type Waypoint = {
point: Point; point: Point;

View File

@ -6,7 +6,7 @@ const mockGeoTimezoneFinder = {
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
}; };
describe('Geo TZ Finder', () => { describe('Timezone Finder', () => {
let timezoneFinder: TimezoneFinder; let timezoneFinder: TimezoneFinder;
beforeAll(async () => { beforeAll(async () => {

View File

@ -7,7 +7,7 @@ import { Role } from '../../../../domain/types/role.enum';
import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route'; import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route';
import { Route } from '../../../../domain/entities/ecosystem/route'; import { Route } from '../../../../domain/entities/ecosystem/route';
import { IGeodesic } from '../../../../../geography/domain/interfaces/geodesic.interface'; import { IGeodesic } from '../../../../../geography/domain/interfaces/geodesic.interface';
import { PointType } from '../../../../domain/types/geography.enum'; import { PointType } from '../../../../../geography/domain/types/point-type.enum';
const person: Person = new Person( const person: Person = new Person(
{ {

View File

@ -57,8 +57,8 @@ describe('Route entity', () => {
}); });
it('should set spacetimePoints for a route', () => { it('should set spacetimePoints for a route', () => {
const route = new Route(mockGeodesic); const route = new Route(mockGeodesic);
const spacetimePoint1 = new SpacetimePoint([0, 0], 0, 0); const spacetimePoint1 = new SpacetimePoint({ lon: 0, lat: 0 }, 0, 0);
const spacetimePoint2 = new SpacetimePoint([10, 10], 500, 5000); const spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000);
route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]); route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]);
expect(route.spacetimePoints.length).toBe(2); expect(route.spacetimePoints.length).toBe(2);
}); });

View File

@ -1,9 +1,9 @@
import { MatchRequest } from '../../../domain/dtos/match.request'; import { MatchRequest } from '../../../domain/dtos/match.request';
import { Role } from '../../../domain/types/role.enum'; import { Role } from '../../../domain/types/role.enum';
import { TimingFrequency } from '../../../domain/types/timing';
import { IDefaultParams } from '../../../domain/types/default-params.type'; import { IDefaultParams } from '../../../domain/types/default-params.type';
import { MatchQuery } from '../../../queries/match.query'; import { MatchQuery } from '../../../queries/match.query';
import { AlgorithmType } from '../../../domain/types/algorithm.enum'; import { AlgorithmType } from '../../../domain/types/algorithm.enum';
import { Frequency } from '../../../../ad/domain/types/frequency.enum';
const defaultParams: IDefaultParams = { const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0, DEFAULT_IDENTIFIER: 0,
@ -209,9 +209,7 @@ describe('Match query', () => {
expect(matchQuery.algorithmSettings.algorithmType).toBe( expect(matchQuery.algorithmSettings.algorithmType).toBe(
AlgorithmType.CLASSIC, AlgorithmType.CLASSIC,
); );
expect(matchQuery.algorithmSettings.restrict).toBe( expect(matchQuery.algorithmSettings.restrict).toBe(Frequency.PUNCTUAL);
TimingFrequency.FREQUENCY_PUNCTUAL,
);
expect(matchQuery.algorithmSettings.useProportion).toBeTruthy(); expect(matchQuery.algorithmSettings.useProportion).toBeTruthy();
expect(matchQuery.algorithmSettings.proportion).toBe(0.45); expect(matchQuery.algorithmSettings.proportion).toBe(0.45);
expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy(); expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy();