diff --git a/package-lock.json b/package-lock.json index b95a8ba..e565461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,8 @@ "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "timezonecomplete": "^5.12.4" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -8576,6 +8577,14 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/timezonecomplete": { + "version": "5.12.4", + "resolved": "https://registry.npmjs.org/timezonecomplete/-/timezonecomplete-5.12.4.tgz", + "integrity": "sha512-K+ocagBAl5wu9Ifh5oHKhRRLb0wP7j0VjAzjboZsT6bnVmtJNRe3Wnk2IPp0C4Uc8HpLly3gbfUrTlJ3M7vCPA==", + "dependencies": { + "tzdata": "^1.0.25" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -8867,6 +8876,11 @@ "node": ">=4.2.0" } }, + "node_modules/tzdata": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/tzdata/-/tzdata-1.0.38.tgz", + "integrity": "sha512-KIgVvZTLt+DWzr3MOENNLCLdsNB+usedRYYHCVfVbA7TDewj8mfjlWmj3Mv6FfdrvfeE6Oprt+qE47YiL90duQ==" + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", diff --git a/package.json b/package.json index 43e918b..35861ee 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "timezonecomplete": "^5.12.4" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/src/modules/ad/ad.modules.ts b/src/modules/ad/ad.modules.ts index e59f7fd..91717d5 100644 --- a/src/modules/ad/ad.modules.ts +++ b/src/modules/ad/ad.modules.ts @@ -7,6 +7,9 @@ import { CreateAdUseCase } from './domain/usecases/create-ad.usecase'; import { AdRepository } from './adapters/secondaries/ad.repository'; import { DatabaseModule } from '../database/database.module'; import { CqrsModule } from '@nestjs/cqrs'; +import { Messager } from './adapters/secondaries/messager'; +import { TimezoneFinder } from './adapters/secondaries/timezone-finder'; +import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder'; @Module({ imports: [ @@ -36,7 +39,14 @@ import { CqrsModule } from '@nestjs/cqrs'; }), ], controllers: [AdMessagerController], - providers: [AdProfile, AdRepository, CreateAdUseCase], + providers: [ + AdProfile, + Messager, + AdRepository, + TimezoneFinder, + GeoTimezoneFinder, + CreateAdUseCase, + ], exports: [], }) export class AdModule {} diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts index bbc4685..4028b69 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -6,51 +6,59 @@ import { Mapper } from '@automapper/core'; import { CommandBus } from '@nestjs/cqrs'; import { CreateAdCommand } from '../../commands/create-ad.command'; import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; -import { ValidationError, validateOrReject } from 'class-validator'; +import { validateOrReject } from 'class-validator'; +import { Messager } from '../secondaries/messager'; +import { GeoTimezoneFinder } from 'src/modules/geography/adapters/secondaries/geo-timezone-finder'; @Controller() export class AdMessagerController { constructor( + private readonly messager: Messager, private readonly commandBus: CommandBus, @InjectMapper() private readonly mapper: Mapper, + private readonly timezoneFinder: GeoTimezoneFinder, ) {} @RabbitSubscribe({ name: 'adCreated', }) async adCreatedHandler(message: string): Promise { + let createAdRequest: CreateAdRequest; try { // parse message to conform to CreateAdRequest (not a real instance yet) const parsedMessage: CreateAdRequest = JSON.parse(message); - console.log(parsedMessage); // create a real instance of CreateAdRequest from parsed message - // const createAdRequest: CreateAdRequest = this.mapper.map( - // parsedMessage, - // CreateAdRequest, - // CreateAdRequest, - // ); - const createAdRequest = new CreateAdRequest(); - createAdRequest.originType = parsedMessage.originType; - createAdRequest.destinationType = parsedMessage.destinationType; - createAdRequest.waypoints = parsedMessage.waypoints.map((waypoint) => ({ - lon: waypoint.lon, - lat: waypoint.lat, - })); - console.log(createAdRequest); + createAdRequest = this.mapper.map( + parsedMessage, + CreateAdRequest, + CreateAdRequest, + ); // validate instance - await validateOrReject(createAdRequest.waypoints[0]); + await validateOrReject(createAdRequest); + // validate nested objects (fixes direct nested validation bug) + for (const waypoint of createAdRequest.waypoints) { + try { + await validateOrReject(waypoint); + } catch (e) { + throw e; + } + } + createAdRequest.timezone = this.timezoneFinder.timezones( + createAdRequest.waypoints[0].lon, + createAdRequest.waypoints[0].lat, + )[0]; const ad: Ad = await this.commandBus.execute( new CreateAdCommand(createAdRequest), ); console.log(ad); } catch (e) { - if (Array.isArray(e)) { - e.forEach((error) => - error instanceof ValidationError - ? console.log(error.constraints) - : console.log(error), - ); - } + this.messager.publish( + 'logging.matcher.ad.crit', + JSON.stringify({ + createAdRequest, + error: e, + }), + ); } } } diff --git a/src/modules/ad/adapters/secondaries/message-broker.ts b/src/modules/ad/adapters/secondaries/message-broker.ts new file mode 100644 index 0000000..7b4f4df --- /dev/null +++ b/src/modules/ad/adapters/secondaries/message-broker.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export abstract class MessageBroker { + exchange: string; + + constructor(exchange: string) { + this.exchange = exchange; + } + + abstract publish(routingKey: string, message: string): void; +} diff --git a/src/modules/ad/adapters/secondaries/messager.ts b/src/modules/ad/adapters/secondaries/messager.ts new file mode 100644 index 0000000..96fa7cc --- /dev/null +++ b/src/modules/ad/adapters/secondaries/messager.ts @@ -0,0 +1,18 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { MessageBroker } from './message-broker'; + +@Injectable() +export class Messager extends MessageBroker { + constructor( + private readonly _amqpConnection: AmqpConnection, + configService: ConfigService, + ) { + super(configService.get('RMQ_EXCHANGE')); + } + + publish = (routingKey: string, message: string): void => { + this._amqpConnection.publish(this.exchange, routingKey, message); + }; +} diff --git a/src/modules/ad/adapters/secondaries/timezone-finder.ts b/src/modules/ad/adapters/secondaries/timezone-finder.ts new file mode 100644 index 0000000..8459661 --- /dev/null +++ b/src/modules/ad/adapters/secondaries/timezone-finder.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder'; +import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface'; + +@Injectable() +export class TimezoneFinder implements IFindTimezone { + constructor(private readonly geoTimezoneFinder: GeoTimezoneFinder) {} + + timezones = (lon: number, lat: number): string[] => + this.geoTimezoneFinder.timezones(lon, lat); +} diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index 0e71c3d..02fcd41 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -8,12 +8,10 @@ import { IsNumber, IsOptional, IsString, - ValidateNested, } from 'class-validator'; import { PointType } from '../../../geography/domain/types/point-type.enum'; import { Frequency } from '../types/frequency.enum'; -import { Coordinates } from '../../../geography/domain/types/coordinates.type'; -import { Type } from 'class-transformer'; +import { Coordinates } from '../../../geography/domain/entities/coordinates'; export class CreateAdRequest { @IsString() @@ -114,9 +112,7 @@ export class CreateAdRequest { destinationType: PointType; @IsArray() - @ValidateNested({ each: true }) @ArrayMinSize(2) - @Type(() => Coordinates) @AutoMap(() => [Coordinates]) waypoints: Coordinates[]; @@ -140,4 +136,6 @@ export class CreateAdRequest { @IsString() @AutoMap() updatedAt: string; + + timezone: string; } diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index 98771fb..f19b5e0 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { PointType } from '../../../geography/domain/types/point-type.enum'; -import { Coordinates } from '../../../geography/domain/types/coordinates.type'; +import { Coordinates } from '../../../geography/domain/entities/coordinates'; export class Ad { @AutoMap() @@ -22,7 +22,7 @@ export class Ad { toDate: Date; @AutoMap() - monTime: string; + monTime: Date; @AutoMap() tueTime: string; diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index 460068c..3bd2eed 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -4,7 +4,8 @@ import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; -import { Coordinates } from 'src/modules/geography/domain/types/coordinates.type'; +import { Coordinates } from '../../geography/domain/entities/coordinates'; +import moment from 'moment-timezone'; @Injectable() export class AdProfile extends AutomapperProfile { @@ -21,16 +22,14 @@ export class AdProfile extends AutomapperProfile { CreateAdRequest, forMember( (dest) => dest.waypoints, - mapFrom( - (source) => - source.waypoints.map( - (waypoint) => - new Coordinates( - waypoint.lon ?? undefined, - waypoint.lat ?? undefined, - ), - ), - // .filter((waypoint) => waypoint), + mapFrom((source) => + source.waypoints.map( + (waypoint) => + new Coordinates( + waypoint.lon ?? undefined, + waypoint.lat ?? undefined, + ), + ), ), ), ); @@ -54,6 +53,18 @@ export class AdProfile extends AutomapperProfile { (dest) => dest.updatedAt, mapFrom((source) => new Date(source.updatedAt)), ), + // forMember( + // (dest) => dest.monTime, + // mapFrom((source) => + // source.monTime + // ? new Date( + // moment + // .tz(`${source.fromDate} ${source.monTime}`, source.timezone) + // .format(), + // ) + // : undefined, + // ), + // ), ); }; } diff --git a/src/modules/ad/mappers/coordinates.profile.ts b/src/modules/ad/mappers/coordinates.profile.ts deleted file mode 100644 index 69a4a47..0000000 --- a/src/modules/ad/mappers/coordinates.profile.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createMap, Mapper } from '@automapper/core'; -import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; -import { Injectable } from '@nestjs/common'; -import { Coordinates } from '../../geography/domain/types/coordinates.type'; - -@Injectable() -export class CoordinatesProfile extends AutomapperProfile { - constructor(@InjectMapper() mapper: Mapper) { - super(mapper); - } - - override get profile() { - return (mapper: any) => { - createMap(mapper, Coordinates, Coordinates); - }; - } -} diff --git a/src/modules/geography/domain/types/coordinates.type.ts b/src/modules/geography/domain/entities/coordinates.ts similarity index 100% rename from src/modules/geography/domain/types/coordinates.type.ts rename to src/modules/geography/domain/entities/coordinates.ts diff --git a/src/modules/geography/domain/types/point.type.ts b/src/modules/geography/domain/types/point.type.ts index 9285d70..b3733a8 100644 --- a/src/modules/geography/domain/types/point.type.ts +++ b/src/modules/geography/domain/types/point.type.ts @@ -1,5 +1,5 @@ import { PointType } from './point-type.enum'; -import { Coordinates } from './coordinates.type'; +import { Coordinates } from '../entities/coordinates'; export type Point = Coordinates & { type?: PointType; diff --git a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts index 57e21d6..91dd7dc 100644 --- a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts +++ b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts @@ -1,4 +1,4 @@ -import { Coordinates } from 'src/modules/geography/domain/types/coordinates.type'; +import { Coordinates } from '../../../../geography/domain/entities/coordinates'; export class SpacetimePoint { coordinates: Coordinates; diff --git a/src/modules/matcher/domain/entities/ecosystem/time.ts b/src/modules/matcher/domain/entities/ecosystem/time.ts index 417cd22..6059784 100644 --- a/src/modules/matcher/domain/entities/ecosystem/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -11,7 +11,6 @@ import { Day } from '../../types/day.type'; export class Time { private timeRequest: IRequestTime; - private defaultMarginDuration: number; private defaultValidityDuration: number; frequency: Frequency; fromDate: Date; @@ -25,7 +24,6 @@ export class Time { defaultValidityDuration: number, ) { this.timeRequest = timeRequest; - this.defaultMarginDuration = defaultMarginDuration; this.defaultValidityDuration = defaultValidityDuration; this.schedule = {}; this.marginDurations = {