Merge branch 'refactorToBetterHexagon' into 'main'
Refactor to better hexagon See merge request v3/service/ad!10
This commit is contained in:
commit
c03edba904
|
@ -22,9 +22,12 @@ DEPARTURE_MARGIN=900
|
||||||
# DEFAULT ROLE
|
# DEFAULT ROLE
|
||||||
ROLE=passenger
|
ROLE=passenger
|
||||||
|
|
||||||
# SEATS PROVIDED AS DRIVER / REQUESTED AS PASSENGER
|
# SEATS PROPOSED AS DRIVER / REQUESTED AS PASSENGER
|
||||||
SEATS_PROVIDED=3
|
SEATS_PROPOSED=3
|
||||||
SEATS_REQUESTED=1
|
SEATS_REQUESTED=1
|
||||||
|
|
||||||
# ACCEPT ONLY SAME FREQUENCY REQUESTS
|
# ACCEPT ONLY SAME FREQUENCY REQUESTS
|
||||||
STRICT_FREQUENCY=false
|
STRICT_FREQUENCY=false
|
||||||
|
|
||||||
|
# default timezone
|
||||||
|
DEFAULT_TIMEZONE=Europe/Paris
|
||||||
|
|
65
README.md
65
README.md
|
@ -48,30 +48,34 @@ npm run migrate
|
||||||
|
|
||||||
The app exposes the following [gRPC](https://grpc.io/) services :
|
The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
|
|
||||||
- **FindByUuid** : find an ad by its uuid
|
- **FindById** : find an ad by its id
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"uuid": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
"id": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Create** : create an ad (note that uuid is optional, a uuid will be automatically attributed if it is not provided)
|
- **Create** : create an ad (note that id is optional, an id (as a uuid) will be automatically attributed if it is not provided)
|
||||||
|
|
||||||
Punctual driver ad :
|
Punctual driver ad :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"userUuid": "80c9bb02-0931-4a1d-bea6-22d358992245",
|
"userId": "80c9bb02-0931-4a1d-bea6-22d358992245",
|
||||||
"driver": true,
|
"driver": true,
|
||||||
"seatsDriver": 3,
|
"seatsProposed": 3,
|
||||||
"frequency": "PUNCTUAL",
|
"frequency": "PUNCTUAL",
|
||||||
"departureDateTime": "2023-01-15 09:00",
|
"fromDate": "2023-01-15",
|
||||||
"addresses": [
|
"toDate": "2023-01-15",
|
||||||
|
"schedule": {
|
||||||
|
"thu": "09:00"
|
||||||
|
},
|
||||||
|
"waypoints": [
|
||||||
{
|
{
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"lon": 48.68944505415954,
|
"lon": 48.689445,
|
||||||
"lat": 6.176510296462267,
|
"lat": 6.17651,
|
||||||
"houseNumber": "5",
|
"houseNumber": "5",
|
||||||
"street": "Avenue Foch",
|
"street": "Avenue Foch",
|
||||||
"locality": "Nancy",
|
"locality": "Nancy",
|
||||||
|
@ -94,18 +98,22 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"userUuid": "80c9bb02-0931-4a1d-bea6-22d358992245",
|
"userId": "80c9bb02-0931-4a1d-bea6-22d358992245",
|
||||||
"driver": true,
|
"driver": true,
|
||||||
"pasenger": true,
|
"pasenger": true,
|
||||||
"seatsDriver": 3,
|
"seatsProposed": 3,
|
||||||
"seatsPassenger": 1,
|
"seatsRequested": 1,
|
||||||
"frequency": "PUNCTUAL",
|
"frequency": "PUNCTUAL",
|
||||||
"departureDateTime": "2023-01-15 09:00",
|
"fromDate": "2023-01-15",
|
||||||
"addresses": [
|
"toDate": "2023-01-15",
|
||||||
|
"schedule": {
|
||||||
|
"thu": "09:00"
|
||||||
|
},
|
||||||
|
"waypoints": [
|
||||||
{
|
{
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"lon": 48.68944505415954,
|
"lon": 48.689445,
|
||||||
"lat": 6.176510296462267,
|
"lat": 6.17651,
|
||||||
"houseNumber": "5",
|
"houseNumber": "5",
|
||||||
"street": "Avenue Foch",
|
"street": "Avenue Foch",
|
||||||
"locality": "Nancy",
|
"locality": "Nancy",
|
||||||
|
@ -128,7 +136,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"userUuid": "80c9bb02-0931-4a1d-bea6-22d358992245",
|
"userId": "80c9bb02-0931-4a1d-bea6-22d358992245",
|
||||||
"passenger": true,
|
"passenger": true,
|
||||||
"seatsPassenger": 1,
|
"seatsPassenger": 1,
|
||||||
"frequency": "RECURRRENT",
|
"frequency": "RECURRRENT",
|
||||||
|
@ -139,11 +147,11 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
"tue": "07:05",
|
"tue": "07:05",
|
||||||
"fri": "07:10"
|
"fri": "07:10"
|
||||||
},
|
},
|
||||||
"addresses": [
|
"waypoints": [
|
||||||
{
|
{
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"lon": 48.68944505415954,
|
"lon": 48.689445,
|
||||||
"lat": 6.176510296462267,
|
"lat": 6.17651,
|
||||||
"houseNumber": "5",
|
"houseNumber": "5",
|
||||||
"street": "Avenue Foch",
|
"street": "Avenue Foch",
|
||||||
"locality": "Nancy",
|
"locality": "Nancy",
|
||||||
|
@ -164,15 +172,14 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
|
|
||||||
The list of possible options when creating an ad :
|
The list of possible options when creating an ad :
|
||||||
|
|
||||||
- uuid (optional): the uuid of the ad
|
- id (optional): the id of the ad (as a uuid)
|
||||||
- userUuid: the user uuid
|
- userId: the user id (as a uuid)
|
||||||
- driver (boolean, optional): if the ad is a driver ad
|
- driver (boolean, optional): if the ad is a driver ad
|
||||||
- passenger (boolean, optional): if the ad is a passenger ad
|
- passenger (boolean, optional): if the ad is a passenger ad
|
||||||
- frequency: `PUNCTUAL` or `RECURRENT`
|
- frequency: `PUNCTUAL` or `RECURRENT`
|
||||||
- departureDateTime (required if punctual): departure date and hour/minute for a punctual ad
|
- fromDate: start date for recurrent ad, carpool date for punctual ad
|
||||||
- fromDate (required if recurrent): start date for recurrent ad
|
- toDate: end date for recurrent ad, same as fromDate for punctual ad
|
||||||
- toDate (required if recurrent): end date for recurrent ad
|
- schedule: an object with the departure time for each carpooled day in the week (only the carpooled day for punctual ad)
|
||||||
- schedule (required if recurrent): an object with the departure time for each carpooled day in the week
|
|
||||||
- marginDurations (optional): an object with the margin duration (in seconds) for each carpooled day in the week, eg:
|
- marginDurations (optional): an object with the margin duration (in seconds) for each carpooled day in the week, eg:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -181,10 +188,10 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
"fri": 950
|
"fri": 950
|
||||||
}
|
}
|
||||||
|
|
||||||
- seatsDriver (optional): number of seats proposed as driver;
|
- seatsProposed (optional): number of seats proposed as driver
|
||||||
- seatsPassenger (optional): number of seats requested as passenger;
|
- seatsRequested (optional): number of seats requested as passenger
|
||||||
- strict (boolean, optional): if set to true, allow matching only with similar frequency ads
|
- strict (boolean, optional): if set to true, allow matching only with similar frequency ads
|
||||||
- addresses: an array of adresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads)
|
- waypoints: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
|
||||||
|
|
||||||
Default values must be set in `.env` file.
|
Default values must be set in `.env` file.
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/ad",
|
"name": "@mobicoop/ad",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "Mobicoop V3 Ad",
|
"description": "Mobicoop V3 Ad",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -34,9 +34,6 @@
|
||||||
"migrate:deploy": "npx prisma migrate deploy"
|
"migrate:deploy": "npx prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automapper/classes": "^8.7.7",
|
|
||||||
"@automapper/core": "^8.7.7",
|
|
||||||
"@automapper/nestjs": "^8.7.7",
|
|
||||||
"@grpc/grpc-js": "^1.8.14",
|
"@grpc/grpc-js": "^1.8.14",
|
||||||
"@grpc/proto-loader": "^0.7.6",
|
"@grpc/proto-loader": "^0.7.6",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
|
@ -46,15 +43,19 @@
|
||||||
"@nestjs/config": "^2.3.1",
|
"@nestjs/config": "^2.3.1",
|
||||||
"@nestjs/core": "^9.0.0",
|
"@nestjs/core": "^9.0.0",
|
||||||
"@nestjs/cqrs": "^9.0.3",
|
"@nestjs/cqrs": "^9.0.3",
|
||||||
|
"@nestjs/event-emitter": "^1.4.2",
|
||||||
"@nestjs/microservices": "^9.4.0",
|
"@nestjs/microservices": "^9.4.0",
|
||||||
"@nestjs/platform-express": "^9.0.0",
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
"@nestjs/terminus": "^9.2.2",
|
"@nestjs/terminus": "^9.2.2",
|
||||||
"@prisma/client": "^4.13.0",
|
"@prisma/client": "^4.13.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"geo-tz": "^7.0.7",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
"nestjs-request-context": "^2.1.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.2.0"
|
"rxjs": "^7.2.0",
|
||||||
|
"timezonecomplete": "^5.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
"@types/jest": "29.5.0",
|
"@types/jest": "29.5.0",
|
||||||
"@types/node": "18.15.11",
|
"@types/node": "18.15.11",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"dotenv-cli": "^7.2.1",
|
"dotenv-cli": "^7.2.1",
|
||||||
|
@ -88,13 +90,16 @@
|
||||||
"ts"
|
"ts"
|
||||||
],
|
],
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
".controller.ts",
|
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
|
||||||
".profile.ts",
|
|
||||||
".exception.ts",
|
|
||||||
".constants.ts",
|
".constants.ts",
|
||||||
|
".response.ts",
|
||||||
|
".response.base.ts",
|
||||||
|
".port.ts",
|
||||||
|
"libs/exceptions",
|
||||||
|
"libs/types",
|
||||||
|
"prisma.service.ts",
|
||||||
|
"convert-props-to-object.util.ts",
|
||||||
"main.ts"
|
"main.ts"
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
@ -106,18 +111,24 @@
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
".validator.ts",
|
|
||||||
".controller.ts",
|
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
|
||||||
".profile.ts",
|
|
||||||
".exception.ts",
|
|
||||||
".constants.ts",
|
".constants.ts",
|
||||||
".interfaces.ts",
|
".response.ts",
|
||||||
|
".response.base.ts",
|
||||||
|
".port.ts",
|
||||||
|
"libs/exceptions",
|
||||||
|
"libs/types",
|
||||||
|
"prisma.service.ts",
|
||||||
|
"convert-props-to-object.util.ts",
|
||||||
"main.ts"
|
"main.ts"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@libs(.*)": "<rootDir>/libs/$1",
|
||||||
|
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||||
|
"^@src(.*)": "<rootDir>$1"
|
||||||
|
},
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,13 @@ CREATE TABLE "ad" (
|
||||||
"frequency" "Frequency" NOT NULL,
|
"frequency" "Frequency" NOT NULL,
|
||||||
"fromDate" DATE NOT NULL,
|
"fromDate" DATE NOT NULL,
|
||||||
"toDate" DATE NOT NULL,
|
"toDate" DATE NOT NULL,
|
||||||
"monTime" TEXT,
|
"monTime" TIMESTAMPTZ,
|
||||||
"tueTime" TEXT,
|
"tueTime" TIMESTAMPTZ,
|
||||||
"wedTime" TEXT,
|
"wedTime" TIMESTAMPTZ,
|
||||||
"thuTime" TEXT,
|
"thuTime" TIMESTAMPTZ,
|
||||||
"friTime" TEXT,
|
"friTime" TIMESTAMPTZ,
|
||||||
"satTime" TEXT,
|
"satTime" TIMESTAMPTZ,
|
||||||
"sunTime" TEXT,
|
"sunTime" TIMESTAMPTZ,
|
||||||
"monMargin" INTEGER NOT NULL,
|
"monMargin" INTEGER NOT NULL,
|
||||||
"tueMargin" INTEGER NOT NULL,
|
"tueMargin" INTEGER NOT NULL,
|
||||||
"wedMargin" INTEGER NOT NULL,
|
"wedMargin" INTEGER NOT NULL,
|
||||||
|
@ -24,8 +24,8 @@ CREATE TABLE "ad" (
|
||||||
"friMargin" INTEGER NOT NULL,
|
"friMargin" INTEGER NOT NULL,
|
||||||
"satMargin" INTEGER NOT NULL,
|
"satMargin" INTEGER NOT NULL,
|
||||||
"sunMargin" INTEGER NOT NULL,
|
"sunMargin" INTEGER NOT NULL,
|
||||||
"seatsDriver" SMALLINT NOT NULL,
|
"seatsProposed" SMALLINT NOT NULL,
|
||||||
"seatsPassenger" SMALLINT NOT NULL,
|
"seatsRequested" SMALLINT NOT NULL,
|
||||||
"strict" BOOLEAN NOT NULL,
|
"strict" BOOLEAN NOT NULL,
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
@ -34,12 +34,12 @@ CREATE TABLE "ad" (
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "address" (
|
CREATE TABLE "waypoint" (
|
||||||
"uuid" UUID NOT NULL,
|
"uuid" UUID NOT NULL,
|
||||||
"adUuid" UUID NOT NULL,
|
"adUuid" UUID NOT NULL,
|
||||||
"position" SMALLINT NOT NULL,
|
"position" SMALLINT NOT NULL,
|
||||||
"lon" DOUBLE PRECISION NOT NULL,
|
"lon" DECIMAL(9,6) NOT NULL,
|
||||||
"lat" DOUBLE PRECISION NOT NULL,
|
"lat" DECIMAL(8,6) NOT NULL,
|
||||||
"name" TEXT,
|
"name" TEXT,
|
||||||
"houseNumber" TEXT,
|
"houseNumber" TEXT,
|
||||||
"street" TEXT,
|
"street" TEXT,
|
||||||
|
@ -49,8 +49,8 @@ CREATE TABLE "address" (
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
CONSTRAINT "address_pkey" PRIMARY KEY ("uuid")
|
CONSTRAINT "waypoint_pkey" PRIMARY KEY ("uuid")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "address" ADD CONSTRAINT "address_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "waypoint" ADD CONSTRAINT "waypoint_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -12,20 +12,20 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Ad {
|
model Ad {
|
||||||
uuid String @id @default(uuid()) @db.Uuid
|
uuid String @id @default(uuid()) @db.Uuid
|
||||||
userUuid String @db.Uuid
|
userUuid String @db.Uuid
|
||||||
driver Boolean
|
driver Boolean
|
||||||
passenger Boolean
|
passenger Boolean
|
||||||
frequency Frequency
|
frequency Frequency
|
||||||
fromDate DateTime @db.Date
|
fromDate DateTime @db.Date
|
||||||
toDate DateTime @db.Date
|
toDate DateTime @db.Date
|
||||||
monTime String?
|
monTime DateTime? @db.Timestamptz()
|
||||||
tueTime String?
|
tueTime DateTime? @db.Timestamptz()
|
||||||
wedTime String?
|
wedTime DateTime? @db.Timestamptz()
|
||||||
thuTime String?
|
thuTime DateTime? @db.Timestamptz()
|
||||||
friTime String?
|
friTime DateTime? @db.Timestamptz()
|
||||||
satTime String?
|
satTime DateTime? @db.Timestamptz()
|
||||||
sunTime String?
|
sunTime DateTime? @db.Timestamptz()
|
||||||
monMargin Int
|
monMargin Int
|
||||||
tueMargin Int
|
tueMargin Int
|
||||||
wedMargin Int
|
wedMargin Int
|
||||||
|
@ -33,22 +33,22 @@ model Ad {
|
||||||
friMargin Int
|
friMargin Int
|
||||||
satMargin Int
|
satMargin Int
|
||||||
sunMargin Int
|
sunMargin Int
|
||||||
seatsDriver Int @db.SmallInt
|
seatsProposed Int @db.SmallInt
|
||||||
seatsPassenger Int @db.SmallInt
|
seatsRequested Int @db.SmallInt
|
||||||
strict Boolean
|
strict Boolean
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
addresses Address[]
|
waypoints Waypoint[]
|
||||||
|
|
||||||
@@map("ad")
|
@@map("ad")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Address {
|
model Waypoint {
|
||||||
uuid String @id @default(uuid()) @db.Uuid
|
uuid String @id @default(uuid()) @db.Uuid
|
||||||
adUuid String @db.Uuid
|
adUuid String @db.Uuid
|
||||||
position Int @db.SmallInt
|
position Int @db.SmallInt
|
||||||
lon Float
|
lon Decimal @db.Decimal(9, 6)
|
||||||
lat Float
|
lat Decimal @db.Decimal(8, 6)
|
||||||
name String?
|
name String?
|
||||||
houseNumber String?
|
houseNumber String?
|
||||||
street String?
|
street String?
|
||||||
|
@ -59,7 +59,7 @@ model Address {
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
|
Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
|
||||||
|
|
||||||
@@map("address")
|
@@map("waypoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Frequency {
|
enum Frequency {
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
|
||||||
import { AdModule } from './modules/ad/ad.module';
|
import { AdModule } from './modules/ad/ad.module';
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import {
|
import {
|
||||||
MessageBrokerModule,
|
MessageBrokerModule,
|
||||||
MessageBrokerModuleOptions,
|
MessageBrokerModuleOptions,
|
||||||
|
@ -12,11 +9,15 @@ import {
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
ConfigurationModuleOptions,
|
ConfigurationModuleOptions,
|
||||||
} from '@mobicoop/configuration-module';
|
} from '@mobicoop/configuration-module';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { RequestContextModule } from 'nestjs-request-context';
|
||||||
|
import { HealthModule } from '@modules/health/health.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
EventEmitterModule.forRoot(),
|
||||||
|
RequestContextModule,
|
||||||
MessageBrokerModule.forRootAsync({
|
MessageBrokerModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
@ -44,8 +45,6 @@ import {
|
||||||
password: configService.get<string>('REDIS_PASSWORD'),
|
password: configService.get<string>('REDIS_PASSWORD'),
|
||||||
port: configService.get<number>('REDIS_PORT'),
|
port: configService.get<number>('REDIS_PORT'),
|
||||||
},
|
},
|
||||||
|
|
||||||
propagateConfigurationRoutingKey: 'configuration.propagate',
|
|
||||||
setConfigurationBrokerQueue: 'ad-configuration-create-update',
|
setConfigurationBrokerQueue: 'ad-configuration-create-update',
|
||||||
deleteConfigurationQueue: 'ad-configuration-delete',
|
deleteConfigurationQueue: 'ad-configuration-delete',
|
||||||
propagateConfigurationQueue: 'ad-configuration-propagate',
|
propagateConfigurationQueue: 'ad-configuration-propagate',
|
||||||
|
@ -54,7 +53,5 @@ import {
|
||||||
HealthModule,
|
HealthModule,
|
||||||
AdModule,
|
AdModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
|
||||||
providers: [],
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
export class ApiErrorResponse {
|
||||||
|
readonly statusCode: number;
|
||||||
|
|
||||||
|
readonly message: string;
|
||||||
|
|
||||||
|
readonly error: string;
|
||||||
|
|
||||||
|
readonly correlationId: string;
|
||||||
|
|
||||||
|
readonly subErrors?: string[];
|
||||||
|
|
||||||
|
constructor(body: ApiErrorResponse) {
|
||||||
|
this.statusCode = body.statusCode;
|
||||||
|
this.message = body.message;
|
||||||
|
this.error = body.error;
|
||||||
|
this.correlationId = body.correlationId;
|
||||||
|
this.subErrors = body.subErrors;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export class IdResponse {
|
||||||
|
constructor(id: string) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly id: string;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Paginated } from '../ddd';
|
||||||
|
|
||||||
|
export abstract class PaginatedResponseDto<T> extends Paginated<T> {
|
||||||
|
readonly total: number;
|
||||||
|
readonly perPage: number;
|
||||||
|
readonly page: number;
|
||||||
|
abstract readonly data: readonly T[];
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { IdResponse } from './id.response.dto';
|
||||||
|
|
||||||
|
export interface BaseResponseProps {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most of our response objects will have properties like
|
||||||
|
* id, createdAt and updatedAt so we can move them to a
|
||||||
|
* separate class and extend it to avoid duplication.
|
||||||
|
*/
|
||||||
|
export class ResponseBase extends IdResponse {
|
||||||
|
constructor(props: BaseResponseProps) {
|
||||||
|
super(props.id);
|
||||||
|
this.createdAt = new Date(props.createdAt).toISOString();
|
||||||
|
this.updatedAt = new Date(props.updatedAt).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly updatedAt: string;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { AggregateRoot, Mapper, RepositoryPort } from '../ddd';
|
||||||
|
import { ObjectLiteral } from '../types';
|
||||||
|
import { LoggerPort } from '../ports/logger.port';
|
||||||
|
import {
|
||||||
|
PrismaRawRepositoryPort,
|
||||||
|
PrismaRepositoryPort,
|
||||||
|
} from '../ports/prisma-repository.port';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import {
|
||||||
|
ConflictException,
|
||||||
|
DatabaseErrorException,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@libs/exceptions';
|
||||||
|
|
||||||
|
export abstract class PrismaRepositoryBase<
|
||||||
|
Aggregate extends AggregateRoot<any>,
|
||||||
|
DbReadModel extends ObjectLiteral,
|
||||||
|
DbWriteModel extends ObjectLiteral,
|
||||||
|
> implements RepositoryPort<Aggregate>
|
||||||
|
{
|
||||||
|
protected constructor(
|
||||||
|
protected readonly prisma: PrismaRepositoryPort<Aggregate> | any,
|
||||||
|
protected readonly prismaRaw: PrismaRawRepositoryPort,
|
||||||
|
protected readonly mapper: Mapper<Aggregate, DbReadModel, DbWriteModel>,
|
||||||
|
protected readonly eventEmitter: EventEmitter2,
|
||||||
|
protected readonly logger: LoggerPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findOneById(id: string, include?: any): Promise<Aggregate> {
|
||||||
|
const entity = await this.prisma.findUnique({
|
||||||
|
where: { uuid: id },
|
||||||
|
include,
|
||||||
|
});
|
||||||
|
if (entity) return this.mapper.toDomain(entity);
|
||||||
|
throw new NotFoundException('Record not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(entity: Aggregate): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.prisma.create({
|
||||||
|
data: this.mapper.toPersistence(entity),
|
||||||
|
});
|
||||||
|
entity.publishEvents(this.logger, this.eventEmitter);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.message.includes('Already exists')) {
|
||||||
|
throw new ConflictException('Record already exists', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async healthCheck(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.prismaRaw.$queryRaw`SELECT 1`;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
throw new DatabaseErrorException(e.message);
|
||||||
|
}
|
||||||
|
throw new DatabaseErrorException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Entity } from './entity.base';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { LoggerPort } from '@libs/ports/logger.port';
|
||||||
|
import { DomainEvent } from './domain-event.base';
|
||||||
|
|
||||||
|
export abstract class AggregateRoot<EntityProps> extends Entity<EntityProps> {
|
||||||
|
private _domainEvents: DomainEvent[] = [];
|
||||||
|
|
||||||
|
get domainEvents(): DomainEvent[] {
|
||||||
|
return this._domainEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addEvent(domainEvent: DomainEvent): void {
|
||||||
|
this._domainEvents.push(domainEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearEvents(): void {
|
||||||
|
this._domainEvents = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async publishEvents(
|
||||||
|
logger: LoggerPort,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
this.domainEvents.map(async (event) => {
|
||||||
|
logger.debug(
|
||||||
|
`"${event.constructor.name}" event published for aggregate ${this.constructor.name} : ${this.id}`,
|
||||||
|
);
|
||||||
|
return eventEmitter.emitAsync(event.constructor.name, event);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.clearEvents();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { ArgumentNotProvidedException } from '../exceptions';
|
||||||
|
import { Guard } from '../guard';
|
||||||
|
|
||||||
|
export type CommandProps<T> = Omit<T, 'id' | 'metadata'> & Partial<Command>;
|
||||||
|
|
||||||
|
type CommandMetadata = {
|
||||||
|
/** ID for correlation purposes (for commands that
|
||||||
|
* arrive from other microservices,logs correlation, etc). */
|
||||||
|
readonly correlationId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Causation id to reconstruct execution order if needed
|
||||||
|
*/
|
||||||
|
readonly causationId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of a user who invoker the command. Can be useful for
|
||||||
|
* logging and tracking execution of commands and events
|
||||||
|
*/
|
||||||
|
readonly userId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time when the command occurred. Mostly for tracing purposes
|
||||||
|
*/
|
||||||
|
readonly timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Command {
|
||||||
|
/**
|
||||||
|
* Command id, in case if we want to save it
|
||||||
|
* for auditing purposes and create a correlation/causation chain
|
||||||
|
*/
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
|
readonly metadata: CommandMetadata;
|
||||||
|
|
||||||
|
constructor(props: CommandProps<unknown>) {
|
||||||
|
if (Guard.isEmpty(props)) {
|
||||||
|
throw new ArgumentNotProvidedException(
|
||||||
|
'Command props should not be empty',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.id = props.id || v4();
|
||||||
|
this.metadata = {
|
||||||
|
correlationId: props?.metadata?.correlationId,
|
||||||
|
causationId: props?.metadata?.causationId,
|
||||||
|
timestamp: props?.metadata?.timestamp || Date.now(),
|
||||||
|
userId: props?.metadata?.userId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { ArgumentNotProvidedException } from '../exceptions';
|
||||||
|
import { Guard } from '../guard';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
type DomainEventMetadata = {
|
||||||
|
/** Timestamp when this domain event occurred */
|
||||||
|
readonly timestamp: number;
|
||||||
|
|
||||||
|
/** ID for correlation purposes (for Integration Events,logs correlation, etc).
|
||||||
|
*/
|
||||||
|
readonly correlationId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Causation id used to reconstruct execution order if needed
|
||||||
|
*/
|
||||||
|
readonly causationId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User ID for debugging and logging purposes
|
||||||
|
*/
|
||||||
|
readonly userId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DomainEventProps<T> = Omit<T, 'id' | 'metadata'> & {
|
||||||
|
aggregateId: string;
|
||||||
|
metadata?: DomainEventMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class DomainEvent {
|
||||||
|
public readonly id: string;
|
||||||
|
|
||||||
|
/** Aggregate ID where domain event occurred */
|
||||||
|
public readonly aggregateId: string;
|
||||||
|
|
||||||
|
public readonly metadata: DomainEventMetadata;
|
||||||
|
|
||||||
|
constructor(props: DomainEventProps<unknown>) {
|
||||||
|
if (Guard.isEmpty(props)) {
|
||||||
|
throw new ArgumentNotProvidedException(
|
||||||
|
'DomainEvent props should not be empty',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.id = v4();
|
||||||
|
this.aggregateId = props.aggregateId;
|
||||||
|
this.metadata = {
|
||||||
|
correlationId: props?.metadata?.correlationId,
|
||||||
|
causationId: props?.metadata?.causationId,
|
||||||
|
timestamp: props?.metadata?.timestamp || Date.now(),
|
||||||
|
userId: props?.metadata?.userId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
import {
|
||||||
|
ArgumentNotProvidedException,
|
||||||
|
ArgumentInvalidException,
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
} from '../exceptions';
|
||||||
|
import { Guard } from '../guard';
|
||||||
|
import { convertPropsToObject } from '../utils';
|
||||||
|
|
||||||
|
export type AggregateID = string;
|
||||||
|
|
||||||
|
export interface BaseEntityProps {
|
||||||
|
id: AggregateID;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEntityProps<T> {
|
||||||
|
id: AggregateID;
|
||||||
|
props: T;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Entity<EntityProps> {
|
||||||
|
constructor({
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
props,
|
||||||
|
}: CreateEntityProps<EntityProps>) {
|
||||||
|
this.setId(id);
|
||||||
|
this.validateProps(props);
|
||||||
|
const now = new Date();
|
||||||
|
this._createdAt = createdAt || now;
|
||||||
|
this._updatedAt = updatedAt || now;
|
||||||
|
this.props = props;
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly props: EntityProps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID is set in the concrete entity implementation to support
|
||||||
|
* different ID types depending on your needs.
|
||||||
|
* For example it could be a UUID for aggregate root,
|
||||||
|
* and shortid / nanoid for child entities.
|
||||||
|
*/
|
||||||
|
protected abstract _id: AggregateID;
|
||||||
|
|
||||||
|
private readonly _createdAt: Date;
|
||||||
|
|
||||||
|
private _updatedAt: Date;
|
||||||
|
|
||||||
|
get id(): AggregateID {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setId(id: AggregateID): void {
|
||||||
|
this._id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get createdAt(): Date {
|
||||||
|
return this._createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get updatedAt(): Date {
|
||||||
|
return this._updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isEntity(entity: unknown): entity is Entity<unknown> {
|
||||||
|
return entity instanceof Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if two entities are the same Entity by comparing ID field.
|
||||||
|
* @param object Entity
|
||||||
|
*/
|
||||||
|
public equals(object?: Entity<EntityProps>): boolean {
|
||||||
|
if (object === null || object === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this === object) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Entity.isEntity(object)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.id ? this.id === object.id : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns entity properties.
|
||||||
|
* @return {*} {Props & EntityProps}
|
||||||
|
* @memberof Entity
|
||||||
|
*/
|
||||||
|
public getProps(): EntityProps & BaseEntityProps {
|
||||||
|
const propsCopy = {
|
||||||
|
id: this._id,
|
||||||
|
createdAt: this._createdAt,
|
||||||
|
updatedAt: this._updatedAt,
|
||||||
|
...this.props,
|
||||||
|
};
|
||||||
|
return Object.freeze(propsCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an Entity and all sub-entities/Value Objects it
|
||||||
|
* contains to a plain object with primitive types. Can be
|
||||||
|
* useful when logging an entity during testing/debugging
|
||||||
|
*/
|
||||||
|
public toObject(): unknown {
|
||||||
|
const plainProps = convertPropsToObject(this.props);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
id: this._id,
|
||||||
|
createdAt: this._createdAt,
|
||||||
|
updatedAt: this._updatedAt,
|
||||||
|
...plainProps,
|
||||||
|
};
|
||||||
|
return Object.freeze(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are certain rules that always have to be true (invariants)
|
||||||
|
* for each entity. Validate method is called every time before
|
||||||
|
* saving an entity to the database to make sure those rules are respected.
|
||||||
|
*/
|
||||||
|
public abstract validate(): void;
|
||||||
|
|
||||||
|
private validateProps(props: EntityProps): void {
|
||||||
|
const MAX_PROPS = 50;
|
||||||
|
|
||||||
|
if (Guard.isEmpty(props)) {
|
||||||
|
throw new ArgumentNotProvidedException(
|
||||||
|
'Entity props should not be empty',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof props !== 'object') {
|
||||||
|
throw new ArgumentInvalidException('Entity props should be an object');
|
||||||
|
}
|
||||||
|
if (Object.keys(props as any).length > MAX_PROPS) {
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
`Entity props should not have more than ${MAX_PROPS} properties`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export * from './aggregate-root.base';
|
||||||
|
export * from './command.base';
|
||||||
|
export * from './domain-event.base';
|
||||||
|
export * from './entity.base';
|
||||||
|
export * from './mapper.interface';
|
||||||
|
export * from './repository.port';
|
||||||
|
export * from './value-object.base';
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Entity } from './entity.base';
|
||||||
|
|
||||||
|
export interface Mapper<
|
||||||
|
DomainEntity extends Entity<any>,
|
||||||
|
DbReadRecord,
|
||||||
|
DbWriteRecord,
|
||||||
|
Response = any,
|
||||||
|
> {
|
||||||
|
toPersistence(entity: DomainEntity): DbWriteRecord;
|
||||||
|
toDomain(record: DbReadRecord): DomainEntity;
|
||||||
|
toResponse(entity: DomainEntity): Response;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { OrderBy, PaginatedQueryParams } from './repository.port';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for regular queries
|
||||||
|
*/
|
||||||
|
export abstract class QueryBase {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for paginated queries
|
||||||
|
*/
|
||||||
|
export abstract class PaginatedQueryBase extends QueryBase {
|
||||||
|
perPage: number;
|
||||||
|
offset: number;
|
||||||
|
orderBy: OrderBy;
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
constructor(props: PaginatedParams<PaginatedQueryBase>) {
|
||||||
|
super();
|
||||||
|
this.perPage = props.perPage || 10;
|
||||||
|
this.offset = props.page ? props.page * this.perPage : 0;
|
||||||
|
this.page = props.page || 0;
|
||||||
|
this.orderBy = props.orderBy || { field: true, param: 'desc' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginated query parameters
|
||||||
|
export type PaginatedParams<T> = Omit<
|
||||||
|
T,
|
||||||
|
'perPage' | 'offset' | 'orderBy' | 'page'
|
||||||
|
> &
|
||||||
|
Partial<Omit<PaginatedQueryParams, 'offset'>>;
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* Most of repositories will probably need generic
|
||||||
|
save/find/delete operations, so it's easier
|
||||||
|
to have some shared interfaces.
|
||||||
|
More specific queries should be defined
|
||||||
|
in a respective repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Paginated<T> {
|
||||||
|
readonly total: number;
|
||||||
|
readonly perPage: number;
|
||||||
|
readonly page: number;
|
||||||
|
readonly data: readonly T[];
|
||||||
|
|
||||||
|
constructor(props: Paginated<T>) {
|
||||||
|
this.total = props.total;
|
||||||
|
this.perPage = props.perPage;
|
||||||
|
this.page = props.page;
|
||||||
|
this.data = props.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderBy = { field: string | true; param: 'asc' | 'desc' };
|
||||||
|
|
||||||
|
export type PaginatedQueryParams = {
|
||||||
|
perPage: number;
|
||||||
|
page: number;
|
||||||
|
offset: number;
|
||||||
|
orderBy: OrderBy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RepositoryPort<Entity> {
|
||||||
|
insert(entity: Entity | Entity[]): Promise<void>;
|
||||||
|
findOneById(id: string, include?: any): Promise<Entity>;
|
||||||
|
healthCheck(): Promise<boolean>;
|
||||||
|
// findAll(): Promise<Entity[]>;
|
||||||
|
// findAllPaginated(params: PaginatedQueryParams): Promise<Paginated<Entity>>;
|
||||||
|
// delete(entity: Entity): Promise<boolean>;
|
||||||
|
|
||||||
|
// transaction<T>(handler: () => Promise<T>): Promise<T>;
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { ArgumentNotProvidedException } from '../exceptions';
|
||||||
|
import { Guard } from '../guard';
|
||||||
|
import { convertPropsToObject } from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain Primitive is an object that contains only a single value
|
||||||
|
*/
|
||||||
|
export type Primitives = string | number | boolean;
|
||||||
|
export interface DomainPrimitive<T extends Primitives | Date> {
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueObjectProps<T> = T extends Primitives | Date ? DomainPrimitive<T> : T;
|
||||||
|
|
||||||
|
export abstract class ValueObject<T> {
|
||||||
|
protected readonly props: ValueObjectProps<T>;
|
||||||
|
|
||||||
|
constructor(props: ValueObjectProps<T>) {
|
||||||
|
this.checkIfEmpty(props);
|
||||||
|
this.validate(props);
|
||||||
|
this.props = props;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract validate(props: ValueObjectProps<T>): void;
|
||||||
|
|
||||||
|
static isValueObject(obj: unknown): obj is ValueObject<unknown> {
|
||||||
|
return obj instanceof ValueObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if two Value Objects are equal. Checks structural equality.
|
||||||
|
* @param vo ValueObject
|
||||||
|
*/
|
||||||
|
public equals(vo?: ValueObject<T>): boolean {
|
||||||
|
if (vo === null || vo === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return JSON.stringify(this) === JSON.stringify(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpack a value object to get its raw properties
|
||||||
|
*/
|
||||||
|
public unpack(): T {
|
||||||
|
if (this.isDomainPrimitive(this.props)) {
|
||||||
|
return this.props.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsCopy = convertPropsToObject(this.props);
|
||||||
|
|
||||||
|
return Object.freeze(propsCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkIfEmpty(props: ValueObjectProps<T>): void {
|
||||||
|
if (
|
||||||
|
Guard.isEmpty(props) ||
|
||||||
|
(this.isDomainPrimitive(props) && Guard.isEmpty(props.value))
|
||||||
|
) {
|
||||||
|
throw new ArgumentNotProvidedException('Property cannot be empty');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDomainPrimitive(
|
||||||
|
obj: unknown,
|
||||||
|
): obj is DomainPrimitive<T & (Primitives | Date)> {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, 'value')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
export interface SerializedException {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
correlationId: string;
|
||||||
|
stack?: string;
|
||||||
|
cause?: string;
|
||||||
|
metadata?: unknown;
|
||||||
|
/**
|
||||||
|
* ^ Consider adding optional `metadata` object to
|
||||||
|
* exceptions (if language doesn't support anything
|
||||||
|
* similar by default) and pass some useful technical
|
||||||
|
* information about the exception when throwing.
|
||||||
|
* This will make debugging easier.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for custom exceptions.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @class ExceptionBase
|
||||||
|
* @extends {Error}
|
||||||
|
*/
|
||||||
|
export abstract class ExceptionBase extends Error {
|
||||||
|
abstract code: string;
|
||||||
|
|
||||||
|
public readonly correlationId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @param {ObjectLiteral} [metadata={}]
|
||||||
|
* **BE CAREFUL** not to include sensitive info in 'metadata'
|
||||||
|
* to prevent leaks since all exception's data will end up
|
||||||
|
* in application's log files. Only include non-sensitive
|
||||||
|
* info that may help with debugging.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly message: string,
|
||||||
|
readonly cause?: Error,
|
||||||
|
readonly metadata?: unknown,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default in NodeJS Error objects are not
|
||||||
|
* serialized properly when sending plain objects
|
||||||
|
* to external processes. This method is a workaround.
|
||||||
|
* Keep in mind not to return a stack trace to user when in production.
|
||||||
|
* https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||||
|
*/
|
||||||
|
toJSON(): SerializedException {
|
||||||
|
return {
|
||||||
|
message: this.message,
|
||||||
|
code: this.code,
|
||||||
|
stack: this.stack,
|
||||||
|
correlationId: this.correlationId,
|
||||||
|
cause: JSON.stringify(this.cause),
|
||||||
|
metadata: this.metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Adding a `code` string with a custom status code for every
|
||||||
|
* exception is a good practice, since when that exception
|
||||||
|
* is transferred to another process `instanceof` check
|
||||||
|
* cannot be performed anymore so a `code` string is used instead.
|
||||||
|
* code constants can be stored in a separate file so they
|
||||||
|
* can be shared and reused on a receiving side (code sharing is
|
||||||
|
* useful when developing fullstack apps or microservices)
|
||||||
|
*/
|
||||||
|
export const ARGUMENT_INVALID = 'GENERIC.ARGUMENT_INVALID';
|
||||||
|
export const ARGUMENT_OUT_OF_RANGE = 'GENERIC.ARGUMENT_OUT_OF_RANGE';
|
||||||
|
export const ARGUMENT_NOT_PROVIDED = 'GENERIC.ARGUMENT_NOT_PROVIDED';
|
||||||
|
export const NOT_FOUND = 'GENERIC.NOT_FOUND';
|
||||||
|
export const CONFLICT = 'GENERIC.CONFLICT';
|
||||||
|
export const INTERNAL_SERVER_ERROR = 'GENERIC.INTERNAL_SERVER_ERROR';
|
||||||
|
export const DATABASE_ERROR = 'GENERIC.DATABASE_ERROR';
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {
|
||||||
|
ARGUMENT_INVALID,
|
||||||
|
ARGUMENT_NOT_PROVIDED,
|
||||||
|
ARGUMENT_OUT_OF_RANGE,
|
||||||
|
CONFLICT,
|
||||||
|
DATABASE_ERROR,
|
||||||
|
INTERNAL_SERVER_ERROR,
|
||||||
|
NOT_FOUND,
|
||||||
|
} from '.';
|
||||||
|
import { ExceptionBase } from './exception.base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate that an incorrect argument was provided to a method/function/class constructor
|
||||||
|
*
|
||||||
|
* @class ArgumentInvalidException
|
||||||
|
* @extends {ExceptionBase}
|
||||||
|
*/
|
||||||
|
export class ArgumentInvalidException extends ExceptionBase {
|
||||||
|
readonly code = ARGUMENT_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate that an argument was not provided (is empty object/array, null of undefined).
|
||||||
|
*
|
||||||
|
* @class ArgumentNotProvidedException
|
||||||
|
* @extends {ExceptionBase}
|
||||||
|
*/
|
||||||
|
export class ArgumentNotProvidedException extends ExceptionBase {
|
||||||
|
readonly code = ARGUMENT_NOT_PROVIDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate that an argument is out of allowed range
|
||||||
|
* (for example: incorrect string/array length, number not in allowed min/max range etc)
|
||||||
|
*
|
||||||
|
* @class ArgumentOutOfRangeException
|
||||||
|
* @extends {ExceptionBase}
|
||||||
|
*/
|
||||||
|
export class ArgumentOutOfRangeException extends ExceptionBase {
|
||||||
|
readonly code = ARGUMENT_OUT_OF_RANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate conflicting entities (usually in the database)
|
||||||
|
*
|
||||||
|
* @class ConflictException
|
||||||
|
* @extends {ExceptionBase}
|
||||||
|
*/
|
||||||
|
export class ConflictException extends ExceptionBase {
|
||||||
|
readonly code = CONFLICT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate that entity is not found
|
||||||
|
*
|
||||||
|
* @class NotFoundException
|
||||||
|
* @extends {ExceptionBase}
|
||||||
|
*/
|
||||||
|
export class NotFoundException extends ExceptionBase {
|
||||||
|
static readonly message = 'Not found';
|
||||||
|
|
||||||
|
constructor(message = NotFoundException.message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly code = NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate an internal server error that does not fall under all other errors
|
||||||
|
*
|
||||||
|
* @class InternalServerErrorException
|
||||||
|
* @extends {ExceptionBase}
|
||||||
|
*/
|
||||||
|
export class InternalServerErrorException extends ExceptionBase {
|
||||||
|
static readonly message = 'Internal server error';
|
||||||
|
|
||||||
|
constructor(message = InternalServerErrorException.message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly code = INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate a database error
|
||||||
|
*
|
||||||
|
* @class DatabaseErrorException
|
||||||
|
* @extends {ExceptionBase}
|
||||||
|
*/
|
||||||
|
export class DatabaseErrorException extends ExceptionBase {
|
||||||
|
static readonly message = 'Database error';
|
||||||
|
|
||||||
|
constructor(message = DatabaseErrorException.message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly code = DATABASE_ERROR;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './exception.base';
|
||||||
|
export * from './exception.codes';
|
||||||
|
export * from './exceptions';
|
||||||
|
export * from './rpc-exception.codes.enum';
|
|
@ -0,0 +1,19 @@
|
||||||
|
export enum RpcExceptionCode {
|
||||||
|
OK = 0,
|
||||||
|
CANCELLED = 1,
|
||||||
|
UNKNOWN = 2,
|
||||||
|
INVALID_ARGUMENT = 3,
|
||||||
|
DEADLINE_EXCEEDED = 4,
|
||||||
|
NOT_FOUND = 5,
|
||||||
|
ALREADY_EXISTS = 6,
|
||||||
|
PERMISSION_DENIED = 7,
|
||||||
|
RESOURCE_EXHAUSTED = 8,
|
||||||
|
FAILED_PRECONDITION = 9,
|
||||||
|
ABORTED = 10,
|
||||||
|
OUT_OF_RANGE = 11,
|
||||||
|
UNIMPLEMENTED = 12,
|
||||||
|
INTERNAL = 13,
|
||||||
|
UNAVAILABLE = 14,
|
||||||
|
DATA_LOSS = 15,
|
||||||
|
UNAUTHENTICATED = 16,
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
export class Guard {
|
||||||
|
/**
|
||||||
|
* Checks if value is empty. Accepts strings, numbers, booleans, objects and arrays.
|
||||||
|
*/
|
||||||
|
static isEmpty(value: unknown): boolean {
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof value === 'undefined' || value === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value instanceof Object && !Object.keys(value).length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value.every((item) => Guard.isEmpty(item))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks length range of a provided number/string/array
|
||||||
|
*/
|
||||||
|
static lengthIsBetween(
|
||||||
|
value: number | string | Array<unknown>,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
): boolean {
|
||||||
|
if (Guard.isEmpty(value)) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot check length of a value. Provided value is empty',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const valueLength =
|
||||||
|
typeof value === 'number'
|
||||||
|
? Number(value).toString().length
|
||||||
|
: value.length;
|
||||||
|
if (valueLength >= min && valueLength <= max) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface LoggerPort {
|
||||||
|
log(message: string, ...meta: unknown[]): void;
|
||||||
|
error(message: string, trace?: unknown, ...meta: unknown[]): void;
|
||||||
|
warn(message: string, ...meta: unknown[]): void;
|
||||||
|
debug(message: string, ...meta: unknown[]): void;
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
export interface IPublishMessage {
|
export interface MessagePublisherPort {
|
||||||
publish(routingKey: string, message: string): void;
|
publish(routingKey: string, message: string): void;
|
||||||
}
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export interface PrismaRepositoryPort<Entity> {
|
||||||
|
findUnique(options: any): Promise<Entity>;
|
||||||
|
create(entity: any): Promise<Entity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrismaRawRepositoryPort {
|
||||||
|
$queryRaw<T = unknown>(
|
||||||
|
query: TemplateStringsArray,
|
||||||
|
...values: any[]
|
||||||
|
): Promise<T>;
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
import { ResponseBase } from '@libs/api/response.base';
|
||||||
|
import { PrismaRepositoryBase } from '@libs/db/prisma-repository.base';
|
||||||
|
import { PrismaService } from '@libs/db/prisma.service';
|
||||||
|
import { AggregateID, AggregateRoot, Mapper, RepositoryPort } from '@libs/ddd';
|
||||||
|
import {
|
||||||
|
ConflictException,
|
||||||
|
DatabaseErrorException,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@libs/exceptions';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
interface FakeProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateFakeProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeEntity extends AggregateRoot<FakeProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = (create: CreateFakeProps): FakeEntity => {
|
||||||
|
const id = v4();
|
||||||
|
const props: FakeProps = { ...create };
|
||||||
|
const fake = new FakeEntity({ id, props });
|
||||||
|
return fake;
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeModel = {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FakeRepositoryPort = RepositoryPort<FakeEntity>;
|
||||||
|
|
||||||
|
class FakeResponseDto extends ResponseBase {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fakeRecord: FakeModel = {
|
||||||
|
uuid: 'd567ea3b-4981-43c9-9449-a409b5fa9fed',
|
||||||
|
name: 'fakeName',
|
||||||
|
createdAt: new Date('2023-06-28T16:02:00Z'),
|
||||||
|
updatedAt: new Date('2023-06-28T16:02:00Z'),
|
||||||
|
};
|
||||||
|
|
||||||
|
let recordId = 2;
|
||||||
|
const recordUuid = 'uuid-';
|
||||||
|
const recordName = 'fakeName-';
|
||||||
|
|
||||||
|
const createRandomRecord = (): FakeModel => {
|
||||||
|
const fakeRecord: FakeModel = {
|
||||||
|
uuid: `${recordUuid}${recordId}`,
|
||||||
|
name: `${recordName}${recordId}`,
|
||||||
|
createdAt: new Date('2023-06-30T08:00:00Z'),
|
||||||
|
updatedAt: new Date('2023-06-30T08:00:00Z'),
|
||||||
|
};
|
||||||
|
|
||||||
|
recordId++;
|
||||||
|
|
||||||
|
return fakeRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeRecords: FakeModel[] = [];
|
||||||
|
Array.from({ length: 10 }).forEach(() => {
|
||||||
|
fakeRecords.push(createRandomRecord());
|
||||||
|
});
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
class FakeMapper
|
||||||
|
implements Mapper<FakeEntity, FakeModel, FakeModel, FakeResponseDto>
|
||||||
|
{
|
||||||
|
toPersistence = (entity: FakeEntity): FakeModel => {
|
||||||
|
const copy = entity.getProps();
|
||||||
|
const record: FakeModel = {
|
||||||
|
uuid: copy.id,
|
||||||
|
name: copy.name,
|
||||||
|
createdAt: copy.createdAt,
|
||||||
|
updatedAt: copy.updatedAt,
|
||||||
|
};
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
toDomain = (record: FakeModel): FakeEntity => {
|
||||||
|
const entity = new FakeEntity({
|
||||||
|
id: record.uuid,
|
||||||
|
createdAt: new Date(record.createdAt),
|
||||||
|
updatedAt: new Date(record.updatedAt),
|
||||||
|
props: {
|
||||||
|
name: record.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return entity;
|
||||||
|
};
|
||||||
|
|
||||||
|
toResponse = (entity: FakeEntity): FakeResponseDto => {
|
||||||
|
const props = entity.getProps();
|
||||||
|
const response = new FakeResponseDto(entity);
|
||||||
|
response.name = props.name;
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
class FakePrismaService extends PrismaService {
|
||||||
|
fake: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockPrismaService = {
|
||||||
|
$queryRaw: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.mockImplementation(() => {
|
||||||
|
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
||||||
|
code: 'code',
|
||||||
|
clientVersion: 'version',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
}),
|
||||||
|
fake: {
|
||||||
|
create: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(fakeRecord)
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Prisma.PrismaClientKnownRequestError('Already exists', {
|
||||||
|
code: 'code',
|
||||||
|
clientVersion: 'version',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error('An unknown error');
|
||||||
|
}),
|
||||||
|
|
||||||
|
findUnique: jest.fn().mockImplementation(async (params?: any) => {
|
||||||
|
let record: FakeModel;
|
||||||
|
|
||||||
|
if (params?.where?.uuid) {
|
||||||
|
record = fakeRecords.find(
|
||||||
|
(record) => record.uuid === params?.where?.uuid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!record && params?.where?.uuid == 'uuid-triggering-error') {
|
||||||
|
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||||
|
code: 'code',
|
||||||
|
clientVersion: 'version',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
class FakeRepository
|
||||||
|
extends PrismaRepositoryBase<FakeEntity, FakeModel, FakeModel>
|
||||||
|
implements FakeRepositoryPort
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
prisma: FakePrismaService,
|
||||||
|
mapper: FakeMapper,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
prisma.fake,
|
||||||
|
prisma,
|
||||||
|
mapper,
|
||||||
|
eventEmitter,
|
||||||
|
new Logger(FakeRepository.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PrismaRepositoryBase', () => {
|
||||||
|
let fakeRepository: FakeRepository;
|
||||||
|
let prisma: FakePrismaService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
EventEmitter2,
|
||||||
|
FakeRepository,
|
||||||
|
FakeMapper,
|
||||||
|
{
|
||||||
|
provide: FakePrismaService,
|
||||||
|
useValue: mockPrismaService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
fakeRepository = module.get<FakeRepository>(FakeRepository);
|
||||||
|
prisma = module.get<FakePrismaService>(FakePrismaService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(fakeRepository).toBeDefined();
|
||||||
|
expect(prisma).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insert', () => {
|
||||||
|
it('should create a record', async () => {
|
||||||
|
jest.spyOn(prisma.fake, 'create');
|
||||||
|
|
||||||
|
await fakeRepository.insert(
|
||||||
|
FakeEntity.create({
|
||||||
|
name: 'someFakeName',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(prisma.fake.create).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a ConflictException if record already exists', async () => {
|
||||||
|
await expect(
|
||||||
|
fakeRepository.insert(
|
||||||
|
FakeEntity.create({
|
||||||
|
name: 'someFakeName',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toBeInstanceOf(ConflictException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an Error if an error occurs', async () => {
|
||||||
|
await expect(
|
||||||
|
fakeRepository.insert(
|
||||||
|
FakeEntity.create({
|
||||||
|
name: 'someFakeName',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findOneById', () => {
|
||||||
|
it('should find a record by its id', async () => {
|
||||||
|
const record = await fakeRepository.findOneById('uuid-3');
|
||||||
|
expect(record.getProps().name).toBe('fakeName-3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an Error for client error', async () => {
|
||||||
|
await expect(
|
||||||
|
fakeRepository.findOneById('uuid-triggering-error'),
|
||||||
|
).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a NotFoundException if id is not found', async () => {
|
||||||
|
await expect(
|
||||||
|
fakeRepository.findOneById('wrong-id'),
|
||||||
|
).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('healthCheck', () => {
|
||||||
|
it('should return a healthy result', async () => {
|
||||||
|
const res = await fakeRepository.healthCheck();
|
||||||
|
expect(res).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if database is not available', async () => {
|
||||||
|
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
||||||
|
DatabaseErrorException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a DatabaseErrorException if an error occurs', async () => {
|
||||||
|
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
||||||
|
DatabaseErrorException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
AggregateID,
|
||||||
|
AggregateRoot,
|
||||||
|
DomainEvent,
|
||||||
|
DomainEventProps,
|
||||||
|
} from '@libs/ddd';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
interface FakeProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateFakeProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeRecordCreatedDomainEvent extends DomainEvent {
|
||||||
|
readonly name: string;
|
||||||
|
|
||||||
|
constructor(props: DomainEventProps<FakeRecordCreatedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
this.name = props.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeEntity extends AggregateRoot<FakeProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = (create: CreateFakeProps): FakeEntity => {
|
||||||
|
const id = v4();
|
||||||
|
const props: FakeProps = { ...create };
|
||||||
|
const fake = new FakeEntity({ id, props });
|
||||||
|
fake.addEvent(
|
||||||
|
new FakeRecordCreatedDomainEvent({
|
||||||
|
aggregateId: id,
|
||||||
|
name: props.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return fake;
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockLogger = {
|
||||||
|
debug: jest.fn(),
|
||||||
|
log: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AggregateRoot Base', () => {
|
||||||
|
it('should define an aggregate root based object instance', () => {
|
||||||
|
const fakeInstance = FakeEntity.create({
|
||||||
|
name: 'someFakeName',
|
||||||
|
});
|
||||||
|
expect(fakeInstance).toBeDefined();
|
||||||
|
expect(fakeInstance.domainEvents.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should publish domain events', async () => {
|
||||||
|
jest.spyOn(mockLogger, 'debug');
|
||||||
|
const eventEmitter = new EventEmitter2();
|
||||||
|
jest.spyOn(eventEmitter, 'emitAsync');
|
||||||
|
const fakeInstance = FakeEntity.create({
|
||||||
|
name: 'someFakeName',
|
||||||
|
});
|
||||||
|
await fakeInstance.publishEvents(mockLogger, eventEmitter);
|
||||||
|
expect(mockLogger.debug).toHaveBeenCalledTimes(1);
|
||||||
|
expect(eventEmitter.emitAsync).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fakeInstance.domainEvents.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Command, CommandProps } from '@libs/ddd';
|
||||||
|
import { ArgumentNotProvidedException } from '@libs/exceptions';
|
||||||
|
|
||||||
|
class FakeCommand extends Command {
|
||||||
|
readonly name: string;
|
||||||
|
|
||||||
|
constructor(props: CommandProps<FakeCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.name = props.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BadFakeCommand extends Command {
|
||||||
|
constructor(props: CommandProps<BadFakeCommand>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Command Base', () => {
|
||||||
|
it('should define a command based object instance', () => {
|
||||||
|
const fakeCommand = new FakeCommand({ name: 'fakeName' });
|
||||||
|
expect(fakeCommand).toBeDefined();
|
||||||
|
expect(fakeCommand.id.length).toBe(36);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should define a command based object instance with a provided id', () => {
|
||||||
|
const fakeCommand = new FakeCommand({ id: 'some-id', name: 'fakeName' });
|
||||||
|
expect(fakeCommand.id).toBe('some-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should define a command based object instance with metadata', () => {
|
||||||
|
const fakeCommand = new FakeCommand({
|
||||||
|
name: 'fakeName',
|
||||||
|
metadata: {
|
||||||
|
correlationId: 'some-correlation-id',
|
||||||
|
causationId: 'some-causation-id',
|
||||||
|
userId: 'some-user-id',
|
||||||
|
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(fakeCommand.metadata.timestamp).toBe(1687928400000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if props are empty', () => {
|
||||||
|
expect(() => new BadFakeCommand({})).toThrow(ArgumentNotProvidedException);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@libs/ddd';
|
||||||
|
import { ArgumentNotProvidedException } from '@libs/exceptions';
|
||||||
|
|
||||||
|
class FakeDomainEvent extends DomainEvent {
|
||||||
|
readonly name: string;
|
||||||
|
|
||||||
|
constructor(props: DomainEventProps<FakeDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
this.name = props.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DomainEvent Base', () => {
|
||||||
|
it('should define a domain event based object instance', () => {
|
||||||
|
const fakeDomainEvent = new FakeDomainEvent({
|
||||||
|
aggregateId: 'some-id',
|
||||||
|
name: 'some-name',
|
||||||
|
});
|
||||||
|
expect(fakeDomainEvent).toBeDefined();
|
||||||
|
expect(fakeDomainEvent.id.length).toBe(36);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should define a domain event based object instance with metadata', () => {
|
||||||
|
const fakeDomainEvent = new FakeDomainEvent({
|
||||||
|
aggregateId: 'some-id',
|
||||||
|
name: 'some-name',
|
||||||
|
metadata: {
|
||||||
|
correlationId: 'some-correlation-id',
|
||||||
|
causationId: 'some-causation-id',
|
||||||
|
userId: 'some-user-id',
|
||||||
|
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(fakeDomainEvent.metadata.timestamp).toBe(1687928400000);
|
||||||
|
});
|
||||||
|
it('should throw an exception if props are empty', () => {
|
||||||
|
const emptyProps: DomainEventProps<FakeDomainEvent> = undefined;
|
||||||
|
expect(() => new FakeDomainEvent(emptyProps)).toThrow(
|
||||||
|
ArgumentNotProvidedException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { Entity } from '@libs/ddd';
|
||||||
|
import { ArgumentOutOfRangeException } from '@libs/exceptions';
|
||||||
|
|
||||||
|
interface FakeProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeEntity extends Entity<FakeProps> {
|
||||||
|
protected _id: string;
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Entity Base', () => {
|
||||||
|
it('should define an entity based object instance', () => {
|
||||||
|
const fakeInstance = new FakeEntity({
|
||||||
|
id: 'some-id',
|
||||||
|
props: {
|
||||||
|
name: 'some-name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(fakeInstance).toBeDefined();
|
||||||
|
expect(fakeInstance.id).toBe('some-id');
|
||||||
|
expect(fakeInstance.createdAt).toBeInstanceOf(Date);
|
||||||
|
expect(fakeInstance.updatedAt).toBeInstanceOf(Date);
|
||||||
|
expect(FakeEntity.isEntity(fakeInstance)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should define an entity with given created and updated dates', () => {
|
||||||
|
const fakeInstance = new FakeEntity({
|
||||||
|
id: 'some-id',
|
||||||
|
createdAt: new Date('2023-06-28T05:00:00Z'),
|
||||||
|
updatedAt: new Date('2023-06-28T06:00:00Z'),
|
||||||
|
props: {
|
||||||
|
name: 'some-name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(fakeInstance.createdAt.getUTCHours()).toBe(5);
|
||||||
|
expect(fakeInstance.updatedAt.getUTCHours()).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compare entities', () => {
|
||||||
|
const fakeInstance = new FakeEntity({
|
||||||
|
id: 'some-id',
|
||||||
|
props: {
|
||||||
|
name: 'some-name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const fakeInstanceClone = new FakeEntity({
|
||||||
|
id: 'some-id',
|
||||||
|
props: {
|
||||||
|
name: 'some-name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const fakeInstanceNotReallyClone = new FakeEntity({
|
||||||
|
id: 'some-slightly-different-id',
|
||||||
|
props: {
|
||||||
|
name: 'some-name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const undefinedFakeInstance: FakeEntity = undefined;
|
||||||
|
expect(fakeInstance.equals(undefinedFakeInstance)).toBeFalsy();
|
||||||
|
expect(fakeInstance.equals(fakeInstance)).toBeTruthy();
|
||||||
|
expect(fakeInstance.equals(fakeInstanceClone)).toBeTruthy();
|
||||||
|
expect(fakeInstance.equals(fakeInstanceNotReallyClone)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert entity to plain object', () => {
|
||||||
|
const fakeInstance = new FakeEntity({
|
||||||
|
id: 'some-id',
|
||||||
|
createdAt: new Date('2023-06-28T05:00:00Z'),
|
||||||
|
updatedAt: new Date('2023-06-28T06:00:00Z'),
|
||||||
|
props: {
|
||||||
|
name: 'some-name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(fakeInstance.toObject()).toEqual({
|
||||||
|
id: 'some-id',
|
||||||
|
createdAt: new Date('2023-06-28T05:00:00.000Z'),
|
||||||
|
updatedAt: new Date('2023-06-28T06:00:00.000Z'),
|
||||||
|
name: 'some-name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if props number is too high', () => {
|
||||||
|
interface BigFakeProps {
|
||||||
|
prop1: string;
|
||||||
|
prop2: string;
|
||||||
|
prop3: string;
|
||||||
|
prop4: string;
|
||||||
|
prop5: string;
|
||||||
|
prop6: string;
|
||||||
|
prop7: string;
|
||||||
|
prop8: string;
|
||||||
|
prop9: string;
|
||||||
|
prop10: string;
|
||||||
|
prop11: string;
|
||||||
|
prop12: string;
|
||||||
|
prop13: string;
|
||||||
|
prop14: string;
|
||||||
|
prop15: string;
|
||||||
|
prop16: string;
|
||||||
|
prop17: string;
|
||||||
|
prop18: string;
|
||||||
|
prop19: string;
|
||||||
|
prop20: string;
|
||||||
|
prop21: string;
|
||||||
|
prop22: string;
|
||||||
|
prop23: string;
|
||||||
|
prop24: string;
|
||||||
|
prop25: string;
|
||||||
|
prop26: string;
|
||||||
|
prop27: string;
|
||||||
|
prop28: string;
|
||||||
|
prop29: string;
|
||||||
|
prop30: string;
|
||||||
|
prop31: string;
|
||||||
|
prop32: string;
|
||||||
|
prop33: string;
|
||||||
|
prop34: string;
|
||||||
|
prop35: string;
|
||||||
|
prop36: string;
|
||||||
|
prop37: string;
|
||||||
|
prop38: string;
|
||||||
|
prop39: string;
|
||||||
|
prop40: string;
|
||||||
|
prop41: string;
|
||||||
|
prop42: string;
|
||||||
|
prop43: string;
|
||||||
|
prop44: string;
|
||||||
|
prop45: string;
|
||||||
|
prop46: string;
|
||||||
|
prop47: string;
|
||||||
|
prop48: string;
|
||||||
|
prop49: string;
|
||||||
|
prop50: string;
|
||||||
|
prop51: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BigFakeEntity extends Entity<BigFakeProps> {
|
||||||
|
protected _id: string;
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
new BigFakeEntity({
|
||||||
|
id: 'some-id',
|
||||||
|
props: {
|
||||||
|
prop1: 'some-name',
|
||||||
|
prop2: 'some-name',
|
||||||
|
prop3: 'some-name',
|
||||||
|
prop4: 'some-name',
|
||||||
|
prop5: 'some-name',
|
||||||
|
prop6: 'some-name',
|
||||||
|
prop7: 'some-name',
|
||||||
|
prop8: 'some-name',
|
||||||
|
prop9: 'some-name',
|
||||||
|
prop10: 'some-name',
|
||||||
|
prop11: 'some-name',
|
||||||
|
prop12: 'some-name',
|
||||||
|
prop13: 'some-name',
|
||||||
|
prop14: 'some-name',
|
||||||
|
prop15: 'some-name',
|
||||||
|
prop16: 'some-name',
|
||||||
|
prop17: 'some-name',
|
||||||
|
prop18: 'some-name',
|
||||||
|
prop19: 'some-name',
|
||||||
|
prop20: 'some-name',
|
||||||
|
prop21: 'some-name',
|
||||||
|
prop22: 'some-name',
|
||||||
|
prop23: 'some-name',
|
||||||
|
prop24: 'some-name',
|
||||||
|
prop25: 'some-name',
|
||||||
|
prop26: 'some-name',
|
||||||
|
prop27: 'some-name',
|
||||||
|
prop28: 'some-name',
|
||||||
|
prop29: 'some-name',
|
||||||
|
prop30: 'some-name',
|
||||||
|
prop31: 'some-name',
|
||||||
|
prop32: 'some-name',
|
||||||
|
prop33: 'some-name',
|
||||||
|
prop34: 'some-name',
|
||||||
|
prop35: 'some-name',
|
||||||
|
prop36: 'some-name',
|
||||||
|
prop37: 'some-name',
|
||||||
|
prop38: 'some-name',
|
||||||
|
prop39: 'some-name',
|
||||||
|
prop40: 'some-name',
|
||||||
|
prop41: 'some-name',
|
||||||
|
prop42: 'some-name',
|
||||||
|
prop43: 'some-name',
|
||||||
|
prop44: 'some-name',
|
||||||
|
prop45: 'some-name',
|
||||||
|
prop46: 'some-name',
|
||||||
|
prop47: 'some-name',
|
||||||
|
prop48: 'some-name',
|
||||||
|
prop49: 'some-name',
|
||||||
|
prop50: 'some-name',
|
||||||
|
prop51: 'some-name',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow(ArgumentOutOfRangeException);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
PaginatedParams,
|
||||||
|
PaginatedQueryBase,
|
||||||
|
QueryBase,
|
||||||
|
} from '@libs/ddd/query.base';
|
||||||
|
|
||||||
|
class FakeQuery extends QueryBase {
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
|
constructor(id: string) {
|
||||||
|
super();
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Query Base', () => {
|
||||||
|
it('should define a query based object instance', () => {
|
||||||
|
const fakeQuery = new FakeQuery('some-id');
|
||||||
|
expect(fakeQuery).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class FakePaginatedQuery extends PaginatedQueryBase {
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
|
constructor(props: PaginatedParams<FakePaginatedQuery>) {
|
||||||
|
super(props);
|
||||||
|
this.id = props.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Paginated Query Base', () => {
|
||||||
|
it('should define a paginated query based object instance', () => {
|
||||||
|
const fakePaginatedQuery = new FakePaginatedQuery({
|
||||||
|
id: 'some-id',
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
expect(fakePaginatedQuery).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { ValueObject } from '@libs/ddd';
|
||||||
|
|
||||||
|
interface FakeProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeValueObject extends ValueObject<FakeProps> {
|
||||||
|
get name(): string {
|
||||||
|
return this.props.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: FakeProps): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Value Object Base', () => {
|
||||||
|
it('should create a base value object', () => {
|
||||||
|
const fakeValueObject = new FakeValueObject({ name: 'fakeName' });
|
||||||
|
expect(fakeValueObject).toBeDefined();
|
||||||
|
expect(ValueObject.isValueObject(fakeValueObject)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compare value objects', () => {
|
||||||
|
const fakeValueObject = new FakeValueObject({ name: 'fakeName' });
|
||||||
|
const fakeValueObjectClone = new FakeValueObject({ name: 'fakeName' });
|
||||||
|
const undefinedFakeValueObject: FakeValueObject = undefined;
|
||||||
|
const nullFakeValueObject: FakeValueObject = null;
|
||||||
|
expect(fakeValueObject.equals(undefinedFakeValueObject)).toBeFalsy();
|
||||||
|
expect(fakeValueObject.equals(nullFakeValueObject)).toBeFalsy();
|
||||||
|
expect(fakeValueObject.equals(fakeValueObject)).toBeTruthy();
|
||||||
|
expect(fakeValueObject.equals(fakeValueObjectClone)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unpack value object props', () => {
|
||||||
|
const fakeValueObject = new FakeValueObject({ name: 'fakeName' });
|
||||||
|
expect(fakeValueObject.unpack()).toEqual({
|
||||||
|
name: 'fakeName',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Guard } from '@libs/guard';
|
||||||
|
|
||||||
|
describe('Guard', () => {
|
||||||
|
describe('isEmpty', () => {
|
||||||
|
it('should return false for a number', () => {
|
||||||
|
expect(Guard.isEmpty(1)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return false for a falsy boolean', () => {
|
||||||
|
expect(Guard.isEmpty(false)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return false for a truthy boolean', () => {
|
||||||
|
expect(Guard.isEmpty(true)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return true for undefined', () => {
|
||||||
|
expect(Guard.isEmpty(undefined)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for null', () => {
|
||||||
|
expect(Guard.isEmpty(null)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return false for a Date', () => {
|
||||||
|
expect(Guard.isEmpty(new Date('2023-06-28'))).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return false for an object with keys', () => {
|
||||||
|
expect(Guard.isEmpty({ key: 'value' })).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return true for an object without keys', () => {
|
||||||
|
expect(Guard.isEmpty({})).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for an array without values', () => {
|
||||||
|
expect(Guard.isEmpty([])).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for an array with only empty values', () => {
|
||||||
|
expect(Guard.isEmpty([null, undefined])).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return false for an array with some empty values', () => {
|
||||||
|
expect(Guard.isEmpty([1, null, undefined])).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return true for an empty string', () => {
|
||||||
|
expect(Guard.isEmpty('')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('lengthIsBetween', () => {
|
||||||
|
it('should return true for a string in the range', () => {
|
||||||
|
expect(Guard.lengthIsBetween('test', 0, 4)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for a number in the range', () => {
|
||||||
|
expect(Guard.lengthIsBetween(2, 0, 4)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for an array with number of elements in the range', () => {
|
||||||
|
expect(Guard.lengthIsBetween([1, 2, 3], 0, 4)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return false for a string not in the range', () => {
|
||||||
|
expect(Guard.lengthIsBetween('test', 5, 9)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return false for a number not in the range', () => {
|
||||||
|
expect(Guard.lengthIsBetween(2, 3, 6)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return false for an array with number of elements not in the range', () => {
|
||||||
|
expect(Guard.lengthIsBetween([1, 2, 3], 10, 12)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should throw an exception if value is empty', () => {
|
||||||
|
expect(() => Guard.lengthIsBetween(undefined, 0, 4)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
import { ArgumentMetadata } from '@nestjs/common';
|
import { ArgumentMetadata } from '@nestjs/common';
|
||||||
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
|
import { FindAdByIdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/find-ad-by-id.request.dto';
|
||||||
import { FindAdByUuidRequest } from '../../../modules/ad/domain/dtos/find-ad-by-uuid.request';
|
import { RpcValidationPipe } from '@libs/utils/pipes/rpc.validation-pipe';
|
||||||
|
|
||||||
describe('RpcValidationPipe', () => {
|
describe('RpcValidationPipe', () => {
|
||||||
it('should not validate request', async () => {
|
it('should not validate request', async () => {
|
||||||
|
@ -10,10 +10,10 @@ describe('RpcValidationPipe', () => {
|
||||||
});
|
});
|
||||||
const metadata: ArgumentMetadata = {
|
const metadata: ArgumentMetadata = {
|
||||||
type: 'body',
|
type: 'body',
|
||||||
metatype: FindAdByUuidRequest,
|
metatype: FindAdByIdRequestDto,
|
||||||
data: '',
|
data: '',
|
||||||
};
|
};
|
||||||
await target.transform(<FindAdByUuidRequest>{}, metadata).catch((err) => {
|
await target.transform(<FindAdByIdRequestDto>{}, metadata).catch((err) => {
|
||||||
expect(err.message).toEqual('Rpc Exception');
|
expect(err.message).toEqual('Rpc Exception');
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
/** Consider creating a bunch of shared custom utility
|
||||||
|
* types for different situations.
|
||||||
|
* Alternatively you can use a library like
|
||||||
|
* https://github.com/andnp/SimplyTyped
|
||||||
|
*/
|
||||||
|
export * from './object-literal.type';
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Interface of the simple literal object with any string keys.
|
||||||
|
*/
|
||||||
|
export interface ObjectLiteral {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { Entity } from '../ddd/entity.base';
|
||||||
|
import { ValueObject } from '../ddd/value-object.base';
|
||||||
|
|
||||||
|
function isEntity(obj: unknown): obj is Entity<unknown> {
|
||||||
|
/**
|
||||||
|
* 'instanceof Entity' causes error here for some reason.
|
||||||
|
* Probably creates some circular dependency. This is a workaround
|
||||||
|
* until I find a solution :)
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
Object.prototype.hasOwnProperty.call(obj, 'toObject') &&
|
||||||
|
Object.prototype.hasOwnProperty.call(obj, 'id') &&
|
||||||
|
ValueObject.isValueObject((obj as Entity<unknown>).id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToPlainObject(item: any): any {
|
||||||
|
if (ValueObject.isValueObject(item)) {
|
||||||
|
return item.unpack();
|
||||||
|
}
|
||||||
|
if (isEntity(item)) {
|
||||||
|
return item.toObject();
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Entity/Value Objects props to a plain object.
|
||||||
|
* Useful for testing and debugging.
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export function convertPropsToObject(props: any): any {
|
||||||
|
const propsCopy = structuredClone(props);
|
||||||
|
|
||||||
|
// eslint-disable-next-line guard-for-in
|
||||||
|
for (const prop in propsCopy) {
|
||||||
|
if (Array.isArray(propsCopy[prop])) {
|
||||||
|
propsCopy[prop] = (propsCopy[prop] as Array<unknown>).map((item) => {
|
||||||
|
return convertToPlainObject(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
propsCopy[prop] = convertToPlainObject(propsCopy[prop]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return propsCopy;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './convert-props-to-object.util';
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { RpcExceptionCode } from '@libs/exceptions/rpc-exception.codes.enum';
|
||||||
import { Injectable, ValidationPipe } from '@nestjs/common';
|
import { Injectable, ValidationPipe } from '@nestjs/common';
|
||||||
import { RpcException } from '@nestjs/microservices';
|
import { RpcException } from '@nestjs/microservices';
|
||||||
|
|
||||||
|
@ -6,7 +7,7 @@ export class RpcValidationPipe extends ValidationPipe {
|
||||||
createExceptionFactory() {
|
createExceptionFactory() {
|
||||||
return (validationErrors = []) => {
|
return (validationErrors = []) => {
|
||||||
return new RpcException({
|
return new RpcException({
|
||||||
code: 3,
|
code: RpcExceptionCode.INVALID_ARGUMENT,
|
||||||
message: this.flattenValidationErrors(validationErrors),
|
message: this.flattenValidationErrors(validationErrors),
|
||||||
});
|
});
|
||||||
};
|
};
|
|
@ -13,10 +13,13 @@ async function bootstrap() {
|
||||||
options: {
|
options: {
|
||||||
package: ['ad', 'health'],
|
package: ['ad', 'health'],
|
||||||
protoPath: [
|
protoPath: [
|
||||||
join(__dirname, 'modules/ad/adapters/primaries/ad.proto'),
|
join(__dirname, 'modules/ad/interface/grpc-controllers/ad.proto'),
|
||||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
join(
|
||||||
|
__dirname,
|
||||||
|
'modules/health/interface/grpc-controllers/health.proto',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
url: `${process.env.SERVICE_URL}:${process.env.SERVICE_PORT}`,
|
||||||
loader: { keepCase: true },
|
loader: { keepCase: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export const PARAMS_PROVIDER = Symbol();
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
|
||||||
|
export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER');
|
||||||
|
export const TIME_CONVERTER = Symbol('TIME_CONVERTER');
|
||||||
|
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');
|
|
@ -0,0 +1,247 @@
|
||||||
|
import { Mapper } from '@libs/ddd';
|
||||||
|
import { AdResponseDto } from './interface/dtos/ad.response.dto';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { AdEntity } from './core/ad.entity';
|
||||||
|
import {
|
||||||
|
AdWriteModel,
|
||||||
|
AdReadModel,
|
||||||
|
WaypointModel,
|
||||||
|
} from './infrastructure/ad.repository';
|
||||||
|
import { Frequency } from './core/ad.types';
|
||||||
|
import { WaypointProps } from './core/value-objects/waypoint.value-object';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import {
|
||||||
|
PARAMS_PROVIDER,
|
||||||
|
TIMEZONE_FINDER,
|
||||||
|
TIME_CONVERTER,
|
||||||
|
} from './ad.di-tokens';
|
||||||
|
import { TimezoneFinderPort } from './core/ports/timezone-finder.port';
|
||||||
|
import { DefaultParamsProviderPort } from './core/ports/default-params-provider.port';
|
||||||
|
import { DefaultParams } from './core/ports/default-params.type';
|
||||||
|
import { TimeConverterPort } from './core/ports/time-converter.port';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper constructs objects that are used in different layers:
|
||||||
|
* Record is an object that is stored in a database,
|
||||||
|
* Entity is an object that is used in application domain layer,
|
||||||
|
* and a ResponseDTO is an object returned to a user (usually as json).
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdMapper
|
||||||
|
implements Mapper<AdEntity, AdReadModel, AdWriteModel, AdResponseDto>
|
||||||
|
{
|
||||||
|
private readonly _defaultParams: DefaultParams;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PARAMS_PROVIDER)
|
||||||
|
private readonly defaultParamsProvider: DefaultParamsProviderPort,
|
||||||
|
@Inject(TIMEZONE_FINDER)
|
||||||
|
private readonly timezoneFinder: TimezoneFinderPort,
|
||||||
|
@Inject(TIME_CONVERTER)
|
||||||
|
private readonly timeConverter: TimeConverterPort,
|
||||||
|
) {
|
||||||
|
this._defaultParams = defaultParamsProvider.getParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
toPersistence = (entity: AdEntity): AdWriteModel => {
|
||||||
|
const copy = entity.getProps();
|
||||||
|
const { lon, lat } = copy.waypoints[0].address.coordinates;
|
||||||
|
const timezone = this.timezoneFinder.timezones(
|
||||||
|
lon,
|
||||||
|
lat,
|
||||||
|
this._defaultParams.DEFAULT_TIMEZONE,
|
||||||
|
)[0];
|
||||||
|
const now = new Date();
|
||||||
|
const record: AdWriteModel = {
|
||||||
|
uuid: copy.id,
|
||||||
|
userUuid: copy.userId,
|
||||||
|
driver: copy.driver,
|
||||||
|
passenger: copy.passenger,
|
||||||
|
frequency: copy.frequency,
|
||||||
|
fromDate: new Date(copy.fromDate),
|
||||||
|
toDate: new Date(copy.toDate),
|
||||||
|
monTime: copy.schedule.mon
|
||||||
|
? this.timeConverter.localDateTimeToUtc(
|
||||||
|
copy.fromDate,
|
||||||
|
copy.schedule.mon,
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
tueTime: copy.schedule.tue
|
||||||
|
? this.timeConverter.localDateTimeToUtc(
|
||||||
|
copy.fromDate,
|
||||||
|
copy.schedule.tue,
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
wedTime: copy.schedule.wed
|
||||||
|
? this.timeConverter.localDateTimeToUtc(
|
||||||
|
copy.fromDate,
|
||||||
|
copy.schedule.wed,
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
thuTime: copy.schedule.thu
|
||||||
|
? this.timeConverter.localDateTimeToUtc(
|
||||||
|
copy.fromDate,
|
||||||
|
copy.schedule.thu,
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
friTime: copy.schedule.fri
|
||||||
|
? this.timeConverter.localDateTimeToUtc(
|
||||||
|
copy.fromDate,
|
||||||
|
copy.schedule.fri,
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
satTime: copy.schedule.sat
|
||||||
|
? this.timeConverter.localDateTimeToUtc(
|
||||||
|
copy.fromDate,
|
||||||
|
copy.schedule.sat,
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
sunTime: copy.schedule.sun
|
||||||
|
? this.timeConverter.localDateTimeToUtc(
|
||||||
|
copy.fromDate,
|
||||||
|
copy.schedule.sun,
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
monMargin: copy.marginDurations.mon,
|
||||||
|
tueMargin: copy.marginDurations.tue,
|
||||||
|
wedMargin: copy.marginDurations.wed,
|
||||||
|
thuMargin: copy.marginDurations.thu,
|
||||||
|
friMargin: copy.marginDurations.fri,
|
||||||
|
satMargin: copy.marginDurations.sat,
|
||||||
|
sunMargin: copy.marginDurations.sun,
|
||||||
|
seatsProposed: copy.seatsProposed,
|
||||||
|
seatsRequested: copy.seatsRequested,
|
||||||
|
strict: copy.strict,
|
||||||
|
waypoints: {
|
||||||
|
create: copy.waypoints.map((waypoint: WaypointProps) => ({
|
||||||
|
uuid: v4(),
|
||||||
|
position: waypoint.position,
|
||||||
|
name: waypoint.address.name,
|
||||||
|
houseNumber: waypoint.address.houseNumber,
|
||||||
|
street: waypoint.address.street,
|
||||||
|
locality: waypoint.address.locality,
|
||||||
|
postalCode: waypoint.address.postalCode,
|
||||||
|
country: waypoint.address.country,
|
||||||
|
lon: waypoint.address.coordinates.lon,
|
||||||
|
lat: waypoint.address.coordinates.lat,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
createdAt: copy.createdAt,
|
||||||
|
updatedAt: copy.updatedAt,
|
||||||
|
};
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
toDomain = (record: AdReadModel): AdEntity => {
|
||||||
|
const timezone = this.timezoneFinder.timezones(
|
||||||
|
record.waypoints[0].lon,
|
||||||
|
record.waypoints[0].lat,
|
||||||
|
this._defaultParams.DEFAULT_TIMEZONE,
|
||||||
|
)[0];
|
||||||
|
const entity = new AdEntity({
|
||||||
|
id: record.uuid,
|
||||||
|
createdAt: new Date(record.createdAt),
|
||||||
|
updatedAt: new Date(record.updatedAt),
|
||||||
|
props: {
|
||||||
|
userId: record.userUuid,
|
||||||
|
driver: record.driver,
|
||||||
|
passenger: record.passenger,
|
||||||
|
frequency: Frequency[record.frequency],
|
||||||
|
fromDate: record.fromDate.toISOString().split('T')[0],
|
||||||
|
toDate: record.toDate.toISOString().split('T')[0],
|
||||||
|
schedule: {
|
||||||
|
mon: record.monTime?.toISOString(),
|
||||||
|
tue: record.tueTime?.toISOString(),
|
||||||
|
wed: record.wedTime
|
||||||
|
? this.timeConverter.utcDatetimeToLocalTime(
|
||||||
|
record.wedTime.toISOString(),
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
thu: record.thuTime
|
||||||
|
? this.timeConverter.utcDatetimeToLocalTime(
|
||||||
|
record.thuTime.toISOString(),
|
||||||
|
timezone,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
fri: record.friTime?.toISOString(),
|
||||||
|
sat: record.satTime?.toISOString(),
|
||||||
|
sun: record.sunTime?.toISOString(),
|
||||||
|
},
|
||||||
|
marginDurations: {
|
||||||
|
mon: record.monMargin,
|
||||||
|
tue: record.tueMargin,
|
||||||
|
wed: record.wedMargin,
|
||||||
|
thu: record.thuMargin,
|
||||||
|
fri: record.friMargin,
|
||||||
|
sat: record.satMargin,
|
||||||
|
sun: record.sunMargin,
|
||||||
|
},
|
||||||
|
seatsProposed: record.seatsProposed,
|
||||||
|
seatsRequested: record.seatsRequested,
|
||||||
|
strict: record.strict,
|
||||||
|
waypoints: record.waypoints.map((waypoint: WaypointModel) => ({
|
||||||
|
position: waypoint.position,
|
||||||
|
address: {
|
||||||
|
name: waypoint.name,
|
||||||
|
houseNumber: waypoint.houseNumber,
|
||||||
|
street: waypoint.street,
|
||||||
|
postalCode: waypoint.postalCode,
|
||||||
|
locality: waypoint.locality,
|
||||||
|
country: waypoint.country,
|
||||||
|
coordinates: {
|
||||||
|
lon: waypoint.lon,
|
||||||
|
lat: waypoint.lat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return entity;
|
||||||
|
};
|
||||||
|
|
||||||
|
toResponse = (entity: AdEntity): AdResponseDto => {
|
||||||
|
const props = entity.getProps();
|
||||||
|
const response = new AdResponseDto(entity);
|
||||||
|
response.userId = props.userId;
|
||||||
|
response.driver = props.driver;
|
||||||
|
response.passenger = props.passenger;
|
||||||
|
response.frequency = props.frequency;
|
||||||
|
response.fromDate = props.fromDate;
|
||||||
|
response.toDate = props.toDate;
|
||||||
|
response.schedule = { ...props.schedule };
|
||||||
|
response.marginDurations = { ...props.marginDurations };
|
||||||
|
response.seatsProposed = props.seatsProposed;
|
||||||
|
response.seatsRequested = props.seatsRequested;
|
||||||
|
response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({
|
||||||
|
position: waypoint.position,
|
||||||
|
name: waypoint.address.name,
|
||||||
|
houseNumber: waypoint.address.houseNumber,
|
||||||
|
street: waypoint.address.street,
|
||||||
|
postalCode: waypoint.address.postalCode,
|
||||||
|
locality: waypoint.address.locality,
|
||||||
|
country: waypoint.address.country,
|
||||||
|
lon: waypoint.address.coordinates.lon,
|
||||||
|
lat: waypoint.address.coordinates.lat,
|
||||||
|
}));
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ^ Data returned to the user is whitelisted to avoid leaks.
|
||||||
|
If a new property is added, like password or a
|
||||||
|
credit card number, it won't be returned
|
||||||
|
unless you specifically allow this.
|
||||||
|
(avoid blacklisting, which will return everything
|
||||||
|
but blacklisted items, which can lead to a data leak).
|
||||||
|
*/
|
||||||
|
}
|
|
@ -1,37 +1,97 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { AdController } from './adapters/primaries/ad.controller';
|
import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
|
||||||
import { DatabaseModule } from '../database/database.module';
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { AdProfile } from './mappers/ad.profile';
|
import {
|
||||||
import { AdsRepository } from './adapters/secondaries/ads.repository';
|
AD_REPOSITORY,
|
||||||
import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase';
|
PARAMS_PROVIDER,
|
||||||
import { CreateAdUseCase } from './domain/usecases/create-ad.usecase';
|
TIMEZONE_FINDER,
|
||||||
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
|
TIME_CONVERTER,
|
||||||
import { PARAMS_PROVIDER } from './ad.constants';
|
} from './ad.di-tokens';
|
||||||
import { MESSAGE_BROKER_PUBLISHER, MESSAGE_PUBLISHER } from 'src/app.constants';
|
import {
|
||||||
|
MESSAGE_BROKER_PUBLISHER,
|
||||||
|
MESSAGE_PUBLISHER,
|
||||||
|
} from '@src/app.constants';
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
import { MessagePublisher } from './adapters/secondaries/message-publisher';
|
import { AdRepository } from './infrastructure/ad.repository';
|
||||||
|
import { DefaultParamsProvider } from './infrastructure/default-params-provider';
|
||||||
|
import { MessagePublisher } from './infrastructure/message-publisher';
|
||||||
|
import { AdMapper } from './ad.mapper';
|
||||||
|
import { CreateAdService } from './core/commands/create-ad/create-ad.service';
|
||||||
|
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||||
|
import { PrismaService } from '@libs/db/prisma.service';
|
||||||
|
import { TimeConverter } from './infrastructure/time-converter';
|
||||||
|
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
||||||
|
import { FindAdByIdQueryHandler } from './core/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
||||||
|
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
|
||||||
|
import { PublishLogMessageWhenAdIsCreatedDomainEventHandler } from './core/event-handlers/publish-log-message-when-ad-is-created.domain-event-handler';
|
||||||
|
|
||||||
|
const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
|
||||||
|
|
||||||
|
const eventHandlers: Provider[] = [
|
||||||
|
PublishMessageWhenAdIsCreatedDomainEventHandler,
|
||||||
|
PublishLogMessageWhenAdIsCreatedDomainEventHandler,
|
||||||
|
];
|
||||||
|
|
||||||
|
const commandHandlers: Provider[] = [CreateAdService];
|
||||||
|
|
||||||
|
const queryHandlers: Provider[] = [FindAdByIdQueryHandler];
|
||||||
|
|
||||||
|
const mappers: Provider[] = [AdMapper];
|
||||||
|
|
||||||
|
const repositories: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: AD_REPOSITORY,
|
||||||
|
useClass: AdRepository,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const messageBrokers: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: MESSAGE_BROKER_PUBLISHER,
|
||||||
|
useClass: MessageBrokerPublisher,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MESSAGE_PUBLISHER,
|
||||||
|
useClass: MessagePublisher,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const orms: Provider[] = [PrismaService];
|
||||||
|
|
||||||
|
const utilities: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: PARAMS_PROVIDER,
|
||||||
|
useClass: DefaultParamsProvider,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TIMEZONE_FINDER,
|
||||||
|
useClass: TimezoneFinder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TIME_CONVERTER,
|
||||||
|
useClass: TimeConverter,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule, CqrsModule],
|
imports: [CqrsModule],
|
||||||
controllers: [AdController],
|
controllers: [...grpcControllers],
|
||||||
providers: [
|
providers: [
|
||||||
AdProfile,
|
...eventHandlers,
|
||||||
AdsRepository,
|
...commandHandlers,
|
||||||
FindAdByUuidUseCase,
|
...queryHandlers,
|
||||||
CreateAdUseCase,
|
...mappers,
|
||||||
{
|
...repositories,
|
||||||
provide: PARAMS_PROVIDER,
|
...messageBrokers,
|
||||||
useClass: DefaultParamsProvider,
|
...orms,
|
||||||
},
|
...utilities,
|
||||||
{
|
],
|
||||||
provide: MESSAGE_BROKER_PUBLISHER,
|
exports: [
|
||||||
useClass: MessageBrokerPublisher,
|
PrismaService,
|
||||||
},
|
AdMapper,
|
||||||
{
|
AD_REPOSITORY,
|
||||||
provide: MESSAGE_PUBLISHER,
|
PARAMS_PROVIDER,
|
||||||
useClass: MessagePublisher,
|
TIMEZONE_FINDER,
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AdModule {}
|
export class AdModule {}
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Controller, UsePipes } from '@nestjs/common';
|
|
||||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
|
||||||
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
|
|
||||||
import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request';
|
|
||||||
import { AdPresenter } from './ad.presenter';
|
|
||||||
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
|
|
||||||
import { Ad } from '../../domain/entities/ad';
|
|
||||||
import { CreateAdRequest } from '../../domain/dtos/create-ad.request';
|
|
||||||
import { CreateAdCommand } from '../../commands/create-ad.command';
|
|
||||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
|
||||||
|
|
||||||
@UsePipes(
|
|
||||||
new RpcValidationPipe({
|
|
||||||
whitelist: false,
|
|
||||||
forbidUnknownValues: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@Controller()
|
|
||||||
export class AdController {
|
|
||||||
constructor(
|
|
||||||
private readonly commandBus: CommandBus,
|
|
||||||
private readonly queryBus: QueryBus,
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@GrpcMethod('AdsService', 'FindOneByUuid')
|
|
||||||
async findOnebyUuid(data: FindAdByUuidRequest): Promise<AdPresenter> {
|
|
||||||
try {
|
|
||||||
const ad = await this.queryBus.execute(new FindAdByUuidQuery(data));
|
|
||||||
return this.mapper.map(ad, Ad, AdPresenter);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: e.code,
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AdsService', 'Create')
|
|
||||||
async createAd(data: CreateAdRequest): Promise<AdPresenter> {
|
|
||||||
try {
|
|
||||||
const ad = await this.commandBus.execute(new CreateAdCommand(data));
|
|
||||||
return this.mapper.map(ad, Ad, AdPresenter);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 6,
|
|
||||||
message: 'Ad already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RpcException({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class AdPresenter {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package ad;
|
|
||||||
|
|
||||||
service AdsService {
|
|
||||||
rpc FindOneByUuid(AdByUuid) returns (Ad);
|
|
||||||
rpc FindAll(AdFilter) returns (Ads);
|
|
||||||
rpc Create(Ad) returns (AdByUuid);
|
|
||||||
rpc Update(Ad) returns (Ad);
|
|
||||||
rpc Delete(AdByUuid) returns (Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
message AdByUuid {
|
|
||||||
string uuid = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Ad {
|
|
||||||
string uuid = 1;
|
|
||||||
string userUuid = 2;
|
|
||||||
bool driver = 3;
|
|
||||||
bool passenger = 4;
|
|
||||||
Frequency frequency = 5;
|
|
||||||
optional string departureDateTime = 6;
|
|
||||||
string fromDate = 7;
|
|
||||||
string toDate = 8;
|
|
||||||
Schedule schedule = 9;
|
|
||||||
MarginDurations marginDurations = 10;
|
|
||||||
int32 seatsPassenger = 11;
|
|
||||||
int32 seatsDriver = 12;
|
|
||||||
bool strict = 13;
|
|
||||||
repeated Address addresses = 14;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Schedule {
|
|
||||||
optional string mon = 1;
|
|
||||||
optional string tue = 2;
|
|
||||||
optional string wed = 3;
|
|
||||||
optional string thu = 4;
|
|
||||||
optional string fri = 5;
|
|
||||||
optional string sat = 6;
|
|
||||||
optional string sun = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
message MarginDurations {
|
|
||||||
int32 mon = 1;
|
|
||||||
int32 tue = 2;
|
|
||||||
int32 wed = 3;
|
|
||||||
int32 thu = 4;
|
|
||||||
int32 fri = 5;
|
|
||||||
int32 sat = 6;
|
|
||||||
int32 sun = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Address {
|
|
||||||
string uuid = 1;
|
|
||||||
int32 position = 2;
|
|
||||||
float lon = 3;
|
|
||||||
float lat = 4;
|
|
||||||
optional string name = 5;
|
|
||||||
optional string houseNumber = 6;
|
|
||||||
optional string street = 7;
|
|
||||||
optional string locality = 8;
|
|
||||||
optional string postalCode = 9;
|
|
||||||
string country = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
enum Frequency {
|
|
||||||
PUNCTUAL = 1;
|
|
||||||
RECURRENT = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AdFilter {
|
|
||||||
int32 page = 1;
|
|
||||||
int32 perPage = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Ads {
|
|
||||||
repeated Ad data = 1;
|
|
||||||
int32 total = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Empty {}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AdRepository } from '../../../database/domain/ad-repository';
|
|
||||||
import { Ad } from '../../domain/entities/ad';
|
|
||||||
//TODO : properly implement mutate operation to prisma
|
|
||||||
@Injectable()
|
|
||||||
export class AdsRepository extends AdRepository<Ad> {
|
|
||||||
protected model = 'ad';
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { DefaultParams } from '../../domain/types/default-params.type';
|
|
||||||
import { IProvideParams } from '../../domain/interfaces/param-provider.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DefaultParamsProvider implements IProvideParams {
|
|
||||||
constructor(private readonly configService: ConfigService) {}
|
|
||||||
getParams = (): DefaultParams => ({
|
|
||||||
MON_MARGIN: parseInt(this.configService.get('DEPARTURE_MARGIN')),
|
|
||||||
TUE_MARGIN: parseInt(this.configService.get('DEPARTURE_MARGIN')),
|
|
||||||
WED_MARGIN: parseInt(this.configService.get('DEPARTURE_MARGIN')),
|
|
||||||
THU_MARGIN: parseInt(this.configService.get('DEPARTURE_MARGIN')),
|
|
||||||
FRI_MARGIN: parseInt(this.configService.get('DEPARTURE_MARGIN')),
|
|
||||||
SAT_MARGIN: parseInt(this.configService.get('DEPARTURE_MARGIN')),
|
|
||||||
SUN_MARGIN: parseInt(this.configService.get('DEPARTURE_MARGIN')),
|
|
||||||
DRIVER: this.configService.get('ROLE') == 'driver',
|
|
||||||
SEATS_PROVIDED: parseInt(this.configService.get('SEATS_PROVIDED')),
|
|
||||||
PASSENGER: this.configService.get('ROLE') == 'passenger',
|
|
||||||
SEATS_REQUESTED: parseInt(this.configService.get('SEATS_REQUESTED')),
|
|
||||||
STRICT: this.configService.get('STRICT_FREQUENCY') == 'true',
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { CreateAdRequest } from '../domain/dtos/create-ad.request';
|
|
||||||
|
|
||||||
export class CreateAdCommand {
|
|
||||||
readonly createAdRequest: CreateAdRequest;
|
|
||||||
|
|
||||||
constructor(request: CreateAdRequest) {
|
|
||||||
this.createAdRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { AggregateRoot, AggregateID } from '@libs/ddd';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
|
||||||
|
import { AdProps, CreateAdProps, DefaultAdProps } from './ad.types';
|
||||||
|
import { Waypoint } from './value-objects/waypoint.value-object';
|
||||||
|
import { MarginDurationsProps } from './value-objects/margin-durations.value-object';
|
||||||
|
|
||||||
|
export class AdEntity extends AggregateRoot<AdProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = (
|
||||||
|
create: CreateAdProps,
|
||||||
|
defaultAdProps: DefaultAdProps,
|
||||||
|
): AdEntity => {
|
||||||
|
const id = v4();
|
||||||
|
const props: AdProps = { ...create };
|
||||||
|
const ad = new AdEntity({ id, props })
|
||||||
|
.setMissingMarginDurations(defaultAdProps.marginDurations)
|
||||||
|
.setMissingStrict(defaultAdProps.strict)
|
||||||
|
.setDefaultDriverAndPassengerParameters({
|
||||||
|
driver: defaultAdProps.driver,
|
||||||
|
passenger: defaultAdProps.passenger,
|
||||||
|
seatsProposed: defaultAdProps.seatsProposed,
|
||||||
|
seatsRequested: defaultAdProps.seatsRequested,
|
||||||
|
})
|
||||||
|
.setMissingWaypointsPosition();
|
||||||
|
ad.addEvent(
|
||||||
|
new AdCreatedDomainEvent({
|
||||||
|
aggregateId: id,
|
||||||
|
userId: props.userId,
|
||||||
|
driver: props.driver,
|
||||||
|
passenger: props.passenger,
|
||||||
|
frequency: props.frequency,
|
||||||
|
fromDate: props.fromDate,
|
||||||
|
toDate: props.toDate,
|
||||||
|
monTime: props.schedule.mon,
|
||||||
|
tueTime: props.schedule.tue,
|
||||||
|
wedTime: props.schedule.wed,
|
||||||
|
thuTime: props.schedule.thu,
|
||||||
|
friTime: props.schedule.fri,
|
||||||
|
satTime: props.schedule.sat,
|
||||||
|
sunTime: props.schedule.sun,
|
||||||
|
monMarginDuration: props.marginDurations.mon,
|
||||||
|
tueMarginDuration: props.marginDurations.tue,
|
||||||
|
wedMarginDuration: props.marginDurations.wed,
|
||||||
|
thuMarginDuration: props.marginDurations.thu,
|
||||||
|
friMarginDuration: props.marginDurations.fri,
|
||||||
|
satMarginDuration: props.marginDurations.sat,
|
||||||
|
sunMarginDuration: props.marginDurations.sun,
|
||||||
|
seatsProposed: props.seatsProposed,
|
||||||
|
seatsRequested: props.seatsRequested,
|
||||||
|
strict: props.strict,
|
||||||
|
waypoints: props.waypoints.map((waypoint: Waypoint) => ({
|
||||||
|
position: waypoint.position,
|
||||||
|
name: waypoint.address.name,
|
||||||
|
houseNumber: waypoint.address.houseNumber,
|
||||||
|
street: waypoint.address.street,
|
||||||
|
postalCode: waypoint.address.postalCode,
|
||||||
|
locality: waypoint.address.locality,
|
||||||
|
country: waypoint.address.postalCode,
|
||||||
|
lon: waypoint.address.coordinates.lon,
|
||||||
|
lat: waypoint.address.coordinates.lat,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return ad;
|
||||||
|
};
|
||||||
|
|
||||||
|
private setMissingMarginDurations = (
|
||||||
|
defaultMarginDurations: MarginDurationsProps,
|
||||||
|
): AdEntity => {
|
||||||
|
if (!this.props.marginDurations) this.props.marginDurations = {};
|
||||||
|
if (!this.props.marginDurations.mon)
|
||||||
|
this.props.marginDurations.mon = defaultMarginDurations.mon;
|
||||||
|
if (!this.props.marginDurations.tue)
|
||||||
|
this.props.marginDurations.tue = defaultMarginDurations.tue;
|
||||||
|
if (!this.props.marginDurations.wed)
|
||||||
|
this.props.marginDurations.wed = defaultMarginDurations.wed;
|
||||||
|
if (!this.props.marginDurations.thu)
|
||||||
|
this.props.marginDurations.thu = defaultMarginDurations.thu;
|
||||||
|
if (!this.props.marginDurations.fri)
|
||||||
|
this.props.marginDurations.fri = defaultMarginDurations.fri;
|
||||||
|
if (!this.props.marginDurations.sat)
|
||||||
|
this.props.marginDurations.sat = defaultMarginDurations.sat;
|
||||||
|
if (!this.props.marginDurations.sun)
|
||||||
|
this.props.marginDurations.sun = defaultMarginDurations.sun;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
private setMissingStrict = (strict: boolean): AdEntity => {
|
||||||
|
if (this.props.strict === undefined) this.props.strict = strict;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
private setDefaultDriverAndPassengerParameters = (
|
||||||
|
defaultDriverAndPassengerParameters: DefaultDriverAndPassengerParameters,
|
||||||
|
): AdEntity => {
|
||||||
|
this.props.driver = !!this.props.driver;
|
||||||
|
this.props.passenger = !!this.props.passenger;
|
||||||
|
if (!this.props.driver && !this.props.passenger) {
|
||||||
|
this.props.driver = defaultDriverAndPassengerParameters.driver;
|
||||||
|
this.props.seatsProposed =
|
||||||
|
defaultDriverAndPassengerParameters.seatsProposed;
|
||||||
|
this.props.passenger = defaultDriverAndPassengerParameters.passenger;
|
||||||
|
this.props.seatsRequested =
|
||||||
|
defaultDriverAndPassengerParameters.seatsRequested;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (!this.props.seatsProposed || this.props.seatsProposed <= 0)
|
||||||
|
this.props.seatsProposed =
|
||||||
|
defaultDriverAndPassengerParameters.seatsProposed;
|
||||||
|
if (!this.props.seatsRequested || this.props.seatsRequested <= 0)
|
||||||
|
this.props.seatsRequested =
|
||||||
|
defaultDriverAndPassengerParameters.seatsRequested;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
private setMissingWaypointsPosition = (): AdEntity => {
|
||||||
|
if (this.props.waypoints[0].position === undefined) {
|
||||||
|
for (let i = 0; i < this.props.waypoints.length; i++) {
|
||||||
|
this.props.waypoints[i].position = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DefaultDriverAndPassengerParameters {
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { ExceptionBase } from '@libs/exceptions';
|
||||||
|
|
||||||
|
export class AdAlreadyExistsException extends ExceptionBase {
|
||||||
|
static readonly message = 'Ad already exists';
|
||||||
|
|
||||||
|
public readonly code = 'AD.ALREADY_EXISTS';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(AdAlreadyExistsException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { MarginDurationsProps } from './value-objects/margin-durations.value-object';
|
||||||
|
import { ScheduleProps } from './value-objects/schedule.value-object';
|
||||||
|
import { WaypointProps } from './value-objects/waypoint.value-object';
|
||||||
|
|
||||||
|
// All properties that an Ad has
|
||||||
|
export interface AdProps {
|
||||||
|
userId: string;
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: Frequency;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
schedule: ScheduleProps;
|
||||||
|
marginDurations: MarginDurationsProps;
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
waypoints: WaypointProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties that are needed for an Ad creation
|
||||||
|
export interface CreateAdProps {
|
||||||
|
userId: string;
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: Frequency;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
schedule: ScheduleProps;
|
||||||
|
marginDurations: MarginDurationsProps;
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
waypoints: WaypointProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultAdProps {
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
marginDurations: MarginDurationsProps;
|
||||||
|
strict: boolean;
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Frequency {
|
||||||
|
PUNCTUAL = 'PUNCTUAL',
|
||||||
|
RECURRENT = 'RECURRENT',
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Command, CommandProps } from '@libs/ddd';
|
||||||
|
import { Frequency } from '@modules/ad/core/ad.types';
|
||||||
|
import { Schedule } from '../../types/schedule';
|
||||||
|
import { MarginDurations } from '../../types/margin-durations';
|
||||||
|
import { Waypoint } from '../../types/waypoint';
|
||||||
|
|
||||||
|
export class CreateAdCommand extends Command {
|
||||||
|
readonly userId: string;
|
||||||
|
readonly driver?: boolean;
|
||||||
|
readonly passenger?: boolean;
|
||||||
|
readonly frequency?: Frequency;
|
||||||
|
readonly fromDate: string;
|
||||||
|
readonly toDate: string;
|
||||||
|
readonly schedule: Schedule;
|
||||||
|
readonly marginDurations?: MarginDurations;
|
||||||
|
readonly seatsProposed?: number;
|
||||||
|
readonly seatsRequested?: number;
|
||||||
|
readonly strict?: boolean;
|
||||||
|
readonly waypoints: Waypoint[];
|
||||||
|
|
||||||
|
constructor(props: CommandProps<CreateAdCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.userId = props.userId;
|
||||||
|
this.driver = props.driver;
|
||||||
|
this.passenger = props.passenger;
|
||||||
|
this.frequency = props.frequency;
|
||||||
|
this.fromDate = props.fromDate;
|
||||||
|
this.toDate = props.toDate;
|
||||||
|
this.schedule = props.schedule;
|
||||||
|
this.marginDurations = props.marginDurations;
|
||||||
|
this.seatsProposed = props.seatsProposed;
|
||||||
|
this.seatsRequested = props.seatsRequested;
|
||||||
|
this.strict = props.strict;
|
||||||
|
this.waypoints = props.waypoints;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { CreateAdCommand } from './create-ad.command';
|
||||||
|
import { DefaultParams } from '@modules/ad/core/ports/default-params.type';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AdRepositoryPort } from '@modules/ad/core/ports/ad.repository.port';
|
||||||
|
import { DefaultParamsProviderPort } from '@modules/ad/core/ports/default-params-provider.port';
|
||||||
|
import { AggregateID } from '@libs/ddd';
|
||||||
|
import { AdAlreadyExistsException } from '@modules/ad/core/ad.errors';
|
||||||
|
import { AdEntity } from '@modules/ad/core/ad.entity';
|
||||||
|
import { ConflictException } from '@libs/exceptions';
|
||||||
|
import { Waypoint } from '../../types/waypoint';
|
||||||
|
|
||||||
|
@CommandHandler(CreateAdCommand)
|
||||||
|
export class CreateAdService implements ICommandHandler {
|
||||||
|
private readonly _defaultParams: DefaultParams;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_REPOSITORY)
|
||||||
|
private readonly repository: AdRepositoryPort,
|
||||||
|
@Inject(PARAMS_PROVIDER)
|
||||||
|
private readonly defaultParamsProvider: DefaultParamsProviderPort,
|
||||||
|
) {
|
||||||
|
this._defaultParams = defaultParamsProvider.getParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||||
|
const ad = AdEntity.create(
|
||||||
|
{
|
||||||
|
userId: command.userId,
|
||||||
|
driver: command.driver,
|
||||||
|
passenger: command.passenger,
|
||||||
|
frequency: command.frequency,
|
||||||
|
fromDate: command.fromDate,
|
||||||
|
toDate: command.toDate,
|
||||||
|
schedule: command.schedule,
|
||||||
|
marginDurations: command.marginDurations,
|
||||||
|
seatsProposed: command.seatsProposed,
|
||||||
|
seatsRequested: command.seatsRequested,
|
||||||
|
strict: command.strict,
|
||||||
|
waypoints: command.waypoints.map((waypoint: Waypoint) => ({
|
||||||
|
position: waypoint.position,
|
||||||
|
address: {
|
||||||
|
name: waypoint.name,
|
||||||
|
houseNumber: waypoint.houseNumber,
|
||||||
|
street: waypoint.street,
|
||||||
|
postalCode: waypoint.postalCode,
|
||||||
|
locality: waypoint.locality,
|
||||||
|
country: waypoint.country,
|
||||||
|
coordinates: {
|
||||||
|
lon: waypoint.lon,
|
||||||
|
lat: waypoint.lat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
driver: this._defaultParams.DRIVER,
|
||||||
|
passenger: this._defaultParams.PASSENGER,
|
||||||
|
marginDurations: {
|
||||||
|
mon: this._defaultParams.MON_MARGIN,
|
||||||
|
tue: this._defaultParams.TUE_MARGIN,
|
||||||
|
wed: this._defaultParams.WED_MARGIN,
|
||||||
|
thu: this._defaultParams.THU_MARGIN,
|
||||||
|
fri: this._defaultParams.FRI_MARGIN,
|
||||||
|
sat: this._defaultParams.SAT_MARGIN,
|
||||||
|
sun: this._defaultParams.SUN_MARGIN,
|
||||||
|
},
|
||||||
|
strict: this._defaultParams.STRICT,
|
||||||
|
seatsProposed: this._defaultParams.SEATS_PROPOSED,
|
||||||
|
seatsRequested: this._defaultParams.SEATS_REQUESTED,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.repository.insert(ad);
|
||||||
|
return ad.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new AdAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { MessagePublisherPort } from '@libs/ports/message-publisher.port';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
||||||
|
import { AdCreatedDomainEvent } from '../events/ad-created.domain-events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PublishLogMessageWhenAdIsCreatedDomainEventHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(MESSAGE_PUBLISHER)
|
||||||
|
private readonly messagePublisher: MessagePublisherPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent(AdCreatedDomainEvent.name, { async: true, promisify: true })
|
||||||
|
async handle(event: AdCreatedDomainEvent): Promise<any> {
|
||||||
|
this.messagePublisher.publish(
|
||||||
|
'logging.ad.created.info',
|
||||||
|
JSON.stringify(event),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { MessagePublisherPort } from '@libs/ports/message-publisher.port';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
||||||
|
import { AdCreatedDomainEvent } from '../events/ad-created.domain-events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PublishMessageWhenAdIsCreatedDomainEventHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(MESSAGE_PUBLISHER)
|
||||||
|
private readonly messagePublisher: MessagePublisherPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent(AdCreatedDomainEvent.name, { async: true, promisify: true })
|
||||||
|
async handle(event: AdCreatedDomainEvent): Promise<any> {
|
||||||
|
this.messagePublisher.publish('ad.created', JSON.stringify(event));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@libs/ddd';
|
||||||
|
|
||||||
|
export class AdCreatedDomainEvent extends DomainEvent {
|
||||||
|
readonly userId: string;
|
||||||
|
readonly driver: boolean;
|
||||||
|
readonly passenger: boolean;
|
||||||
|
readonly frequency: string;
|
||||||
|
readonly fromDate: string;
|
||||||
|
readonly toDate: string;
|
||||||
|
readonly monTime: string;
|
||||||
|
readonly tueTime: string;
|
||||||
|
readonly wedTime: string;
|
||||||
|
readonly thuTime: string;
|
||||||
|
readonly friTime: string;
|
||||||
|
readonly satTime: string;
|
||||||
|
readonly sunTime: string;
|
||||||
|
readonly monMarginDuration: number;
|
||||||
|
readonly tueMarginDuration: number;
|
||||||
|
readonly wedMarginDuration: number;
|
||||||
|
readonly thuMarginDuration: number;
|
||||||
|
readonly friMarginDuration: number;
|
||||||
|
readonly satMarginDuration: number;
|
||||||
|
readonly sunMarginDuration: number;
|
||||||
|
readonly seatsProposed: number;
|
||||||
|
readonly seatsRequested: number;
|
||||||
|
readonly strict: boolean;
|
||||||
|
readonly waypoints: Waypoint[];
|
||||||
|
|
||||||
|
constructor(props: DomainEventProps<AdCreatedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
this.userId = props.userId;
|
||||||
|
this.driver = props.driver;
|
||||||
|
this.passenger = props.passenger;
|
||||||
|
this.frequency = props.frequency;
|
||||||
|
this.fromDate = props.fromDate;
|
||||||
|
this.toDate = props.toDate;
|
||||||
|
this.monTime = props.monTime;
|
||||||
|
this.tueTime = props.tueTime;
|
||||||
|
this.wedTime = props.wedTime;
|
||||||
|
this.thuTime = props.thuTime;
|
||||||
|
this.friTime = props.friTime;
|
||||||
|
this.satTime = props.satTime;
|
||||||
|
this.sunTime = props.sunTime;
|
||||||
|
this.monMarginDuration = props.monMarginDuration;
|
||||||
|
this.tueMarginDuration = props.tueMarginDuration;
|
||||||
|
this.wedMarginDuration = props.wedMarginDuration;
|
||||||
|
this.thuMarginDuration = props.thuMarginDuration;
|
||||||
|
this.friMarginDuration = props.friMarginDuration;
|
||||||
|
this.satMarginDuration = props.satMarginDuration;
|
||||||
|
this.sunMarginDuration = props.sunMarginDuration;
|
||||||
|
this.seatsProposed = props.seatsProposed;
|
||||||
|
this.seatsRequested = props.seatsRequested;
|
||||||
|
this.strict = props.strict;
|
||||||
|
this.waypoints = props.waypoints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Waypoint {
|
||||||
|
position: number;
|
||||||
|
name?: string;
|
||||||
|
houseNumber?: string;
|
||||||
|
street?: string;
|
||||||
|
locality?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country: string;
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { RepositoryPort } from '@libs/ddd';
|
||||||
|
import { AdEntity } from '../ad.entity';
|
||||||
|
|
||||||
|
export type AdRepositoryPort = RepositoryPort<AdEntity>;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { DefaultParams } from './default-params.type';
|
||||||
|
|
||||||
|
export interface DefaultParamsProviderPort {
|
||||||
|
getParams(): DefaultParams;
|
||||||
|
}
|
|
@ -7,8 +7,9 @@ export type DefaultParams = {
|
||||||
SAT_MARGIN: number;
|
SAT_MARGIN: number;
|
||||||
SUN_MARGIN: number;
|
SUN_MARGIN: number;
|
||||||
DRIVER: boolean;
|
DRIVER: boolean;
|
||||||
SEATS_PROVIDED: number;
|
SEATS_PROPOSED: number;
|
||||||
PASSENGER: boolean;
|
PASSENGER: boolean;
|
||||||
SEATS_REQUESTED: number;
|
SEATS_REQUESTED: number;
|
||||||
STRICT: boolean;
|
STRICT: boolean;
|
||||||
|
DEFAULT_TIMEZONE: string;
|
||||||
};
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
export interface TimeConverterPort {
|
||||||
|
localDateTimeToUtc(
|
||||||
|
date: string,
|
||||||
|
time: string,
|
||||||
|
timezone: string,
|
||||||
|
dst?: boolean,
|
||||||
|
): Date;
|
||||||
|
utcDatetimeToLocalTime(isoString: string, timezone: string): string;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface TimezoneFinderPort {
|
||||||
|
timezones(lon: number, lat: number, defaultTimezone?: string): string[];
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
|
import { FindAdByIdQuery } from './find-ad-by-id.query';
|
||||||
|
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { AdEntity } from '../../ad.entity';
|
||||||
|
|
||||||
|
@QueryHandler(FindAdByIdQuery)
|
||||||
|
export class FindAdByIdQueryHandler implements IQueryHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_REPOSITORY)
|
||||||
|
private readonly repository: AdRepositoryPort,
|
||||||
|
) {}
|
||||||
|
async execute(query: FindAdByIdQuery): Promise<AdEntity> {
|
||||||
|
return await this.repository.findOneById(query.id, { waypoints: true });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { QueryBase } from '@libs/ddd/query.base';
|
||||||
|
|
||||||
|
export class FindAdByIdQuery extends QueryBase {
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
|
constructor(id: string) {
|
||||||
|
super();
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Coordinates } from './coordinates';
|
||||||
|
|
||||||
|
export type Address = {
|
||||||
|
name?: string;
|
||||||
|
houseNumber?: string;
|
||||||
|
street?: string;
|
||||||
|
locality?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country: string;
|
||||||
|
} & Coordinates;
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type Coordinates = {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
export type MarginDurations = {
|
||||||
|
mon?: number;
|
||||||
|
tue?: number;
|
||||||
|
wed?: number;
|
||||||
|
thu?: number;
|
||||||
|
fri?: number;
|
||||||
|
sat?: number;
|
||||||
|
sun?: number;
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
export type Schedule = {
|
||||||
|
mon?: string;
|
||||||
|
tue?: string;
|
||||||
|
wed?: string;
|
||||||
|
thu?: string;
|
||||||
|
fri?: string;
|
||||||
|
sat?: string;
|
||||||
|
sun?: string;
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Address } from './address';
|
||||||
|
|
||||||
|
export type Waypoint = {
|
||||||
|
position?: number;
|
||||||
|
} & Address;
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { ValueObject } from '@libs/ddd';
|
||||||
|
import { CoordinatesProps } from './coordinates.value-object';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface AddressProps {
|
||||||
|
name?: string;
|
||||||
|
houseNumber?: string;
|
||||||
|
street?: string;
|
||||||
|
locality?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country: string;
|
||||||
|
coordinates: CoordinatesProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Address extends ValueObject<AddressProps> {
|
||||||
|
get name(): string {
|
||||||
|
return this.props.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get houseNumber(): string {
|
||||||
|
return this.props.houseNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
get street(): string {
|
||||||
|
return this.props.street;
|
||||||
|
}
|
||||||
|
|
||||||
|
get locality(): string {
|
||||||
|
return this.props.locality;
|
||||||
|
}
|
||||||
|
|
||||||
|
get postalCode(): string {
|
||||||
|
return this.props.postalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get country(): string {
|
||||||
|
return this.props.country;
|
||||||
|
}
|
||||||
|
|
||||||
|
get coordinates(): CoordinatesProps {
|
||||||
|
return this.props.coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: AddressProps): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { ValueObject } from '@libs/ddd';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface CoordinatesProps {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Coordinates extends ValueObject<CoordinatesProps> {
|
||||||
|
get lon(): number {
|
||||||
|
return this.props.lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat(): number {
|
||||||
|
return this.props.lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: CoordinatesProps): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { ValueObject } from '@libs/ddd';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface MarginDurationsProps {
|
||||||
|
mon?: number;
|
||||||
|
tue?: number;
|
||||||
|
wed?: number;
|
||||||
|
thu?: number;
|
||||||
|
fri?: number;
|
||||||
|
sat?: number;
|
||||||
|
sun?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MarginDurations extends ValueObject<MarginDurationsProps> {
|
||||||
|
get mon(): number {
|
||||||
|
return this.props.mon;
|
||||||
|
}
|
||||||
|
|
||||||
|
set mon(margin: number) {
|
||||||
|
this.props.mon = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tue(): number {
|
||||||
|
return this.props.tue;
|
||||||
|
}
|
||||||
|
|
||||||
|
set tue(margin: number) {
|
||||||
|
this.props.tue = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get wed(): number {
|
||||||
|
return this.props.wed;
|
||||||
|
}
|
||||||
|
|
||||||
|
set wed(margin: number) {
|
||||||
|
this.props.wed = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get thu(): number {
|
||||||
|
return this.props.thu;
|
||||||
|
}
|
||||||
|
|
||||||
|
set thu(margin: number) {
|
||||||
|
this.props.thu = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fri(): number {
|
||||||
|
return this.props.fri;
|
||||||
|
}
|
||||||
|
|
||||||
|
set fri(margin: number) {
|
||||||
|
this.props.fri = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sat(): number {
|
||||||
|
return this.props.sat;
|
||||||
|
}
|
||||||
|
|
||||||
|
set sat(margin: number) {
|
||||||
|
this.props.sat = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sun(): number {
|
||||||
|
return this.props.sun;
|
||||||
|
}
|
||||||
|
|
||||||
|
set sun(margin: number) {
|
||||||
|
this.props.sun = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: MarginDurationsProps): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { ValueObject } from '@libs/ddd';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface ScheduleProps {
|
||||||
|
mon?: string;
|
||||||
|
tue?: string;
|
||||||
|
wed?: string;
|
||||||
|
thu?: string;
|
||||||
|
fri?: string;
|
||||||
|
sat?: string;
|
||||||
|
sun?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Schedule extends ValueObject<ScheduleProps> {
|
||||||
|
get mon(): string | undefined {
|
||||||
|
return this.props.mon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tue(): string | undefined {
|
||||||
|
return this.props.tue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get wed(): string | undefined {
|
||||||
|
return this.props.wed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get thu(): string | undefined {
|
||||||
|
return this.props.thu;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fri(): string | undefined {
|
||||||
|
return this.props.fri;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sat(): string | undefined {
|
||||||
|
return this.props.sat;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sun(): string | undefined {
|
||||||
|
return this.props.sun;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: ScheduleProps): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { ValueObject } from '@libs/ddd';
|
||||||
|
import { AddressProps } from './address.value-object';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface WaypointProps {
|
||||||
|
position: number;
|
||||||
|
address: AddressProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Waypoint extends ValueObject<WaypointProps> {
|
||||||
|
get position(): number {
|
||||||
|
return this.props.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
get address(): AddressProps {
|
||||||
|
return this.props.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: WaypointProps): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { Frequency } from '../types/frequency.enum';
|
|
||||||
import { Address } from '../entities/address';
|
|
||||||
|
|
||||||
export class AdCreation {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
userUuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
driver: boolean;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
passenger: boolean;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
frequency: Frequency;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
fromDate: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
toDate: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
monTime?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
tueTime?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
wedTime?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
thuTime?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
friTime?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
satTime?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
sunTime?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
monMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
tueMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
wedMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
thuMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
friMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
satMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
sunMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
seatsDriver: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
seatsPassenger: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
strict: boolean;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
updatedAt?: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
addresses: { create: Address[] };
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import {
|
|
||||||
IsInt,
|
|
||||||
IsLatitude,
|
|
||||||
IsLongitude,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
IsUUID,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
export class AddressDTO {
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
uuid?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
adUuid?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
position?: number;
|
|
||||||
|
|
||||||
@IsLongitude()
|
|
||||||
@AutoMap()
|
|
||||||
lon: number;
|
|
||||||
|
|
||||||
@IsLatitude()
|
|
||||||
@AutoMap()
|
|
||||||
lat: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@AutoMap()
|
|
||||||
name?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
houseNumber?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
street?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
locality?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
postalCode?: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
country: string;
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import {
|
|
||||||
IsOptional,
|
|
||||||
IsBoolean,
|
|
||||||
IsDate,
|
|
||||||
IsInt,
|
|
||||||
IsEnum,
|
|
||||||
ValidateNested,
|
|
||||||
ArrayMinSize,
|
|
||||||
IsUUID,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { Frequency } from '../types/frequency.enum';
|
|
||||||
import { Transform, Type } from 'class-transformer';
|
|
||||||
import { intToFrequency } from './validators/frequency.mapping';
|
|
||||||
import { MarginDTO } from './margin.dto';
|
|
||||||
import { ScheduleDTO } from './schedule.dto';
|
|
||||||
import { AddressDTO } from './address.dto';
|
|
||||||
import { IsPunctualOrRecurrent } from './validators/decorators/is-punctual-or-recurrent.validator';
|
|
||||||
import { HasProperDriverSeats } from './validators/decorators/has-driver-seats.validator';
|
|
||||||
import { HasProperPassengerSeats } from './validators/decorators/has-passenger-seats.validator';
|
|
||||||
import { HasProperPositionIndexes } from './validators/decorators/address-position.validator';
|
|
||||||
|
|
||||||
export class CreateAdRequest {
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
uuid?: string;
|
|
||||||
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
userUuid: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
driver?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
passenger?: boolean;
|
|
||||||
|
|
||||||
@Transform(({ value }) => intToFrequency(value), {
|
|
||||||
toClassOnly: true,
|
|
||||||
})
|
|
||||||
@IsEnum(Frequency)
|
|
||||||
@AutoMap()
|
|
||||||
frequency: Frequency;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsPunctualOrRecurrent()
|
|
||||||
@Type(() => Date)
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
departureDateTime?: Date;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsPunctualOrRecurrent()
|
|
||||||
@Type(() => Date)
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
fromDate?: Date;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsPunctualOrRecurrent()
|
|
||||||
@Type(() => Date)
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
toDate?: Date;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => ScheduleDTO)
|
|
||||||
@IsPunctualOrRecurrent()
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@AutoMap()
|
|
||||||
schedule: ScheduleDTO = {};
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => MarginDTO)
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@AutoMap()
|
|
||||||
marginDurations?: MarginDTO;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@HasProperDriverSeats()
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
seatsDriver?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@HasProperPassengerSeats()
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
seatsPassenger?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
strict?: boolean;
|
|
||||||
|
|
||||||
@ArrayMinSize(2)
|
|
||||||
@Type(() => AddressDTO)
|
|
||||||
@HasProperPositionIndexes()
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@AutoMap()
|
|
||||||
addresses: AddressDTO[];
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { AddressDTO } from '../address.dto';
|
|
||||||
|
|
||||||
export const hasProperPositionIndexes = (value: AddressDTO[]): boolean => {
|
|
||||||
if (value.every((address) => address.position === undefined)) return true;
|
|
||||||
if (value.every((address) => typeof address.position === 'number')) {
|
|
||||||
value.sort((a, b) => a.position - b.position);
|
|
||||||
for (let i = 1; i < value.length; i++) {
|
|
||||||
if (value[i - 1].position >= value[i].position) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { ValidateBy, ValidationOptions, buildMessage } from 'class-validator';
|
|
||||||
import { AddressDTO } from '../../address.dto';
|
|
||||||
import { hasProperPositionIndexes } from '../address-position';
|
|
||||||
|
|
||||||
export const HasProperPositionIndexes = (
|
|
||||||
validationOptions?: ValidationOptions,
|
|
||||||
): PropertyDecorator =>
|
|
||||||
ValidateBy(
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
constraints: [],
|
|
||||||
validator: {
|
|
||||||
validate: (value: AddressDTO[]): boolean =>
|
|
||||||
hasProperPositionIndexes(value),
|
|
||||||
|
|
||||||
defaultMessage: buildMessage(
|
|
||||||
() =>
|
|
||||||
`indexes position incorrect, please provide a complete list of indexes or ordened list of adresses from start to end of journey`,
|
|
||||||
validationOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
validationOptions,
|
|
||||||
);
|
|
|
@ -1,26 +0,0 @@
|
||||||
import {
|
|
||||||
ValidateBy,
|
|
||||||
ValidationArguments,
|
|
||||||
ValidationOptions,
|
|
||||||
buildMessage,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { hasProperDriverSeats } from '../has-driver-seats';
|
|
||||||
|
|
||||||
export const HasProperDriverSeats = (
|
|
||||||
validationOptions?: ValidationOptions,
|
|
||||||
): PropertyDecorator =>
|
|
||||||
ValidateBy(
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
constraints: [],
|
|
||||||
validator: {
|
|
||||||
validate: (value: any, args: ValidationArguments): boolean =>
|
|
||||||
hasProperDriverSeats(args),
|
|
||||||
defaultMessage: buildMessage(
|
|
||||||
() => `driver and driver seats are not correct`,
|
|
||||||
validationOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
validationOptions,
|
|
||||||
);
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {
|
|
||||||
ValidateBy,
|
|
||||||
ValidationArguments,
|
|
||||||
ValidationOptions,
|
|
||||||
buildMessage,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { isPunctualOrRecurrent } from '../is-punctual-or-recurrent';
|
|
||||||
|
|
||||||
export const IsPunctualOrRecurrent = (
|
|
||||||
validationOptions?: ValidationOptions,
|
|
||||||
): PropertyDecorator =>
|
|
||||||
ValidateBy(
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
constraints: [],
|
|
||||||
validator: {
|
|
||||||
validate: (value, args: ValidationArguments): boolean =>
|
|
||||||
isPunctualOrRecurrent(args),
|
|
||||||
defaultMessage: buildMessage(
|
|
||||||
() =>
|
|
||||||
`the departure Date and time , from date, to date and schedule must be properly set on recurrent or punctual ad`,
|
|
||||||
validationOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
validationOptions,
|
|
||||||
);
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { Frequency } from '../../types/frequency.enum';
|
|
||||||
|
|
||||||
export const intToFrequency = (index: number): Frequency => {
|
|
||||||
if (index == 1) return Frequency.PUNCTUAL;
|
|
||||||
if (index == 2) return Frequency.RECURRENT;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { ValidationArguments } from 'class-validator';
|
|
||||||
|
|
||||||
export const hasProperDriverSeats = (args: ValidationArguments): boolean => {
|
|
||||||
if (
|
|
||||||
args.object['driver'] === true &&
|
|
||||||
typeof args.object['seatsDriver'] === 'number'
|
|
||||||
)
|
|
||||||
return args.object['seatsDriver'] > 0;
|
|
||||||
if (
|
|
||||||
(args.object['driver'] === false ||
|
|
||||||
args.object['driver'] === null ||
|
|
||||||
args.object['driver'] === undefined) &&
|
|
||||||
(args.object['seatsDriver'] === 0 ||
|
|
||||||
args.object['seatsDriver'] === null ||
|
|
||||||
args.object['seatsDriver'] === undefined)
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
};
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { ValidationArguments } from 'class-validator';
|
|
||||||
|
|
||||||
export const hasProperPassengerSeats = (args: ValidationArguments): boolean => {
|
|
||||||
if (
|
|
||||||
args.object['passenger'] === true &&
|
|
||||||
typeof args.object['seatsPassenger'] === 'number'
|
|
||||||
)
|
|
||||||
return args.object['seatsPassenger'] > 0;
|
|
||||||
if (
|
|
||||||
(args.object['passenger'] === false ||
|
|
||||||
args.object['passenger'] === null ||
|
|
||||||
args.object['passenger'] === undefined) &&
|
|
||||||
(args.object['seatsPassenger'] === 0 ||
|
|
||||||
args.object['seatsPassenger'] === null ||
|
|
||||||
args.object['seatsPassenger'] === undefined)
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
};
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { ValidationArguments } from 'class-validator';
|
|
||||||
import { Frequency } from '../../types/frequency.enum';
|
|
||||||
|
|
||||||
const isPunctual = (args: ValidationArguments): boolean =>
|
|
||||||
args.object['frequency'] === Frequency.PUNCTUAL &&
|
|
||||||
args.object['departureDateTime'] instanceof Date &&
|
|
||||||
!Object.keys(args.object['schedule']).length;
|
|
||||||
|
|
||||||
const isRecurrent = (args: ValidationArguments): boolean =>
|
|
||||||
args.object['frequency'] === Frequency.RECURRENT &&
|
|
||||||
args.object['fromDate'] instanceof Date &&
|
|
||||||
args.object['toDate'] instanceof Date &&
|
|
||||||
Object.keys(args.object['schedule']).length > 0;
|
|
||||||
|
|
||||||
export const isPunctualOrRecurrent = (args: ValidationArguments): boolean =>
|
|
||||||
isPunctual(args) || isRecurrent(args);
|
|
|
@ -1,132 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import {
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
IsBoolean,
|
|
||||||
IsDate,
|
|
||||||
IsInt,
|
|
||||||
IsEnum,
|
|
||||||
ValidateNested,
|
|
||||||
ArrayMinSize,
|
|
||||||
IsUUID,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { Address } from '../entities/address';
|
|
||||||
import { Frequency } from '../types/frequency.enum';
|
|
||||||
|
|
||||||
export class Ad {
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
userUuid: string;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
driver: boolean;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
passenger: boolean;
|
|
||||||
|
|
||||||
@IsEnum(Frequency)
|
|
||||||
@AutoMap()
|
|
||||||
frequency: Frequency;
|
|
||||||
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
fromDate: Date;
|
|
||||||
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
toDate: Date;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
monTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
tueTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
wedTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
thuTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
friTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
satTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@AutoMap()
|
|
||||||
sunTime?: string;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
monMargin: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
tueMargin: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
wedMargin: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
thuMargin: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
friMargin: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
satMargin: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
sunMargin: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
seatsDriver: number;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
seatsPassenger: number;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
strict: boolean;
|
|
||||||
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
updatedAt?: Date;
|
|
||||||
|
|
||||||
@ArrayMinSize(2)
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@AutoMap(() => [Address])
|
|
||||||
addresses: Address[];
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsInt, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export class Address {
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsUUID(4)
|
|
||||||
@AutoMap()
|
|
||||||
adUuid: string;
|
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@AutoMap()
|
|
||||||
position: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
lon: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
lat: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
name?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
houseNumber?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
street?: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
locality: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
postalCode: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
country: string;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { CreateAdRequest } from '../dtos/create-ad.request';
|
|
||||||
import { Day } from '../types/day.enum';
|
|
||||||
|
|
||||||
import { Frequency } from '../types/frequency.enum';
|
|
||||||
|
|
||||||
export class FrequencyNormaliser {
|
|
||||||
fromDateResolver(createAdRequest: CreateAdRequest): Date {
|
|
||||||
if (createAdRequest.frequency === Frequency.PUNCTUAL)
|
|
||||||
return createAdRequest.departureDateTime;
|
|
||||||
return createAdRequest.fromDate;
|
|
||||||
}
|
|
||||||
toDateResolver(createAdRequest: CreateAdRequest): Date {
|
|
||||||
if (createAdRequest.frequency === Frequency.PUNCTUAL)
|
|
||||||
return createAdRequest.departureDateTime;
|
|
||||||
return createAdRequest.toDate;
|
|
||||||
}
|
|
||||||
scheduleResolver = (
|
|
||||||
createAdRequest: CreateAdRequest,
|
|
||||||
day: number,
|
|
||||||
): string => {
|
|
||||||
if (
|
|
||||||
Object.keys(createAdRequest.schedule).length === 0 &&
|
|
||||||
createAdRequest.frequency == Frequency.PUNCTUAL &&
|
|
||||||
createAdRequest.departureDateTime.getDay() === day
|
|
||||||
)
|
|
||||||
return `${createAdRequest.departureDateTime
|
|
||||||
.getHours()
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}:${createAdRequest.departureDateTime
|
|
||||||
.getMinutes()
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}`;
|
|
||||||
return createAdRequest.schedule[Day[day]];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { DefaultParams } from '../types/default-params.type';
|
|
||||||
export interface IProvideParams {
|
|
||||||
getParams(): DefaultParams;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
export enum Day {
|
|
||||||
sun = 0,
|
|
||||||
mon,
|
|
||||||
tue,
|
|
||||||
wed,
|
|
||||||
thu,
|
|
||||||
fri,
|
|
||||||
sat,
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum Frequency {
|
|
||||||
PUNCTUAL = 'PUNCTUAL',
|
|
||||||
RECURRENT = 'RECURRENT',
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Inject } from '@nestjs/common';
|
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { AdsRepository } from '../../adapters/secondaries/ads.repository';
|
|
||||||
import { CreateAdCommand } from '../../commands/create-ad.command';
|
|
||||||
import { CreateAdRequest } from '../dtos/create-ad.request';
|
|
||||||
import { IProvideParams } from '../interfaces/param-provider.interface';
|
|
||||||
import { DefaultParams } from '../types/default-params.type';
|
|
||||||
import { AdCreation } from '../dtos/ad.creation';
|
|
||||||
import { Ad } from '../entities/ad';
|
|
||||||
import { PARAMS_PROVIDER } from '../../ad.constants';
|
|
||||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
|
|
||||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
|
||||||
|
|
||||||
@CommandHandler(CreateAdCommand)
|
|
||||||
export class CreateAdUseCase {
|
|
||||||
private readonly defaultParams: DefaultParams;
|
|
||||||
private ad: AdCreation;
|
|
||||||
constructor(
|
|
||||||
private readonly repository: AdsRepository,
|
|
||||||
@Inject(MESSAGE_PUBLISHER)
|
|
||||||
private readonly messagePublisher: IPublishMessage,
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
@Inject(PARAMS_PROVIDER)
|
|
||||||
private readonly defaultParamsProvider: IProvideParams,
|
|
||||||
) {
|
|
||||||
this.defaultParams = defaultParamsProvider.getParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(command: CreateAdCommand): Promise<Ad> {
|
|
||||||
this.ad = this.mapper.map(
|
|
||||||
command.createAdRequest,
|
|
||||||
CreateAdRequest,
|
|
||||||
AdCreation,
|
|
||||||
);
|
|
||||||
this.setDefaultMarginDurations();
|
|
||||||
this.setDefaultAddressesPosition();
|
|
||||||
this.setDefaultDriverAndPassengerParameters();
|
|
||||||
this.setDefaultStrict();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const adCreated: Ad = await this.repository.create(this.ad);
|
|
||||||
this.messagePublisher.publish('ad.create', JSON.stringify(adCreated));
|
|
||||||
this.messagePublisher.publish(
|
|
||||||
'logging.ad.create.info',
|
|
||||||
JSON.stringify(adCreated),
|
|
||||||
);
|
|
||||||
return adCreated;
|
|
||||||
} catch (error) {
|
|
||||||
let key = 'logging.ad.create.crit';
|
|
||||||
if (error.message.includes('Already exists')) {
|
|
||||||
key = 'logging.ad.create.warning';
|
|
||||||
}
|
|
||||||
this.messagePublisher.publish(
|
|
||||||
key,
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setDefaultMarginDurations = (): void => {
|
|
||||||
if (this.ad.monMargin === undefined)
|
|
||||||
this.ad.monMargin = this.defaultParams.MON_MARGIN;
|
|
||||||
if (this.ad.tueMargin === undefined)
|
|
||||||
this.ad.tueMargin = this.defaultParams.TUE_MARGIN;
|
|
||||||
if (this.ad.wedMargin === undefined)
|
|
||||||
this.ad.wedMargin = this.defaultParams.WED_MARGIN;
|
|
||||||
if (this.ad.thuMargin === undefined)
|
|
||||||
this.ad.thuMargin = this.defaultParams.THU_MARGIN;
|
|
||||||
if (this.ad.friMargin === undefined)
|
|
||||||
this.ad.friMargin = this.defaultParams.FRI_MARGIN;
|
|
||||||
if (this.ad.satMargin === undefined)
|
|
||||||
this.ad.satMargin = this.defaultParams.SAT_MARGIN;
|
|
||||||
if (this.ad.sunMargin === undefined)
|
|
||||||
this.ad.sunMargin = this.defaultParams.SUN_MARGIN;
|
|
||||||
};
|
|
||||||
|
|
||||||
private setDefaultStrict = (): void => {
|
|
||||||
if (this.ad.strict === undefined)
|
|
||||||
this.ad.strict = this.defaultParams.STRICT;
|
|
||||||
};
|
|
||||||
|
|
||||||
private setDefaultDriverAndPassengerParameters = (): void => {
|
|
||||||
this.ad.driver = !!this.ad.driver;
|
|
||||||
this.ad.passenger = !!this.ad.passenger;
|
|
||||||
if (!this.ad.driver && !this.ad.passenger) {
|
|
||||||
this.ad.driver = this.defaultParams.DRIVER;
|
|
||||||
this.ad.seatsDriver = this.defaultParams.SEATS_PROVIDED;
|
|
||||||
this.ad.passenger = this.defaultParams.PASSENGER;
|
|
||||||
this.ad.seatsPassenger = this.defaultParams.SEATS_REQUESTED;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.ad.seatsDriver || this.ad.seatsDriver <= 0)
|
|
||||||
this.ad.seatsDriver = this.defaultParams.SEATS_PROVIDED;
|
|
||||||
if (!this.ad.seatsPassenger || this.ad.seatsPassenger <= 0)
|
|
||||||
this.ad.seatsPassenger = this.defaultParams.SEATS_REQUESTED;
|
|
||||||
};
|
|
||||||
|
|
||||||
private setDefaultAddressesPosition = (): void => {
|
|
||||||
if (this.ad.addresses.create[0].position === undefined) {
|
|
||||||
for (let i = 0; i < this.ad.addresses.create.length; i++) {
|
|
||||||
this.ad.addresses.create[i].position = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { Inject, NotFoundException } from '@nestjs/common';
|
|
||||||
import { QueryHandler } from '@nestjs/cqrs';
|
|
||||||
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
|
|
||||||
import { AdsRepository } from '../../adapters/secondaries/ads.repository';
|
|
||||||
import { Ad } from '../entities/ad';
|
|
||||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
|
||||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
|
|
||||||
|
|
||||||
@QueryHandler(FindAdByUuidQuery)
|
|
||||||
export class FindAdByUuidUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly repository: AdsRepository,
|
|
||||||
@Inject(MESSAGE_PUBLISHER)
|
|
||||||
private readonly messagePublisher: IPublishMessage,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async execute(findAdByUuid: FindAdByUuidQuery): Promise<Ad> {
|
|
||||||
try {
|
|
||||||
const ad = await this.repository.findOneByUuid(findAdByUuid.uuid);
|
|
||||||
if (!ad) throw new NotFoundException();
|
|
||||||
return ad;
|
|
||||||
} catch (error) {
|
|
||||||
this.messagePublisher.publish(
|
|
||||||
'logging.ad.read.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
query: findAdByUuid,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue