Implement update ad command

This commit is contained in:
Romain Thouvenin 2024-04-29 17:06:18 +02:00
parent 3d4ff00066
commit 7a84bff260
7 changed files with 216 additions and 119 deletions

View File

@ -11,7 +11,9 @@ import {
AdReadModel,
AdWriteModel,
ScheduleItemModel,
ScheduleWriteModel,
WaypointModel,
WaypointWriteModel,
} from './infrastructure/ad.repository';
import { AdResponseDto } from './interface/dtos/ad.response.dto';
@ -31,9 +33,8 @@ export class AdMapper
private readonly outputDatetimeTransformer: DateTimeTransformerPort,
) {}
toPersistence = (entity: AdEntity): AdWriteModel => {
toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
const copy = entity.getProps();
const now = new Date();
const record: AdWriteModel = {
uuid: copy.id,
userUuid: copy.userId,
@ -43,50 +44,80 @@ export class AdMapper
frequency: copy.frequency,
fromDate: new Date(copy.fromDate),
toDate: new Date(copy.toDate),
schedule: copy.schedule
? {
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
uuid: v4(),
day: scheduleItem.day,
time: new Date(
1970,
0,
1,
parseInt(scheduleItem.time.split(':')[0]),
parseInt(scheduleItem.time.split(':')[1]),
),
margin: scheduleItem.margin,
createdAt: now,
updatedAt: now,
})),
}
: undefined,
schedule: this.toScheduleItemWriteModel(copy.schedule, update),
seatsProposed: copy.seatsProposed as number,
seatsRequested: copy.seatsRequested as number,
strict: copy.strict as boolean,
waypoints: copy.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,
})),
}
: undefined,
waypoints: this.toWaypointWriteModel(copy.waypoints, update),
comment: copy.comment,
};
return record;
};
toScheduleItemWriteModel = (
schedule: ScheduleItemProps[],
update?: boolean,
): ScheduleWriteModel | undefined => {
if (!schedule) {
return undefined;
}
const now = new Date();
const record: ScheduleWriteModel = {
create: schedule.map((scheduleItem: ScheduleItemProps) => ({
uuid: v4(),
day: scheduleItem.day,
time: new Date(
1970,
0,
1,
parseInt(scheduleItem.time.split(':')[0]),
parseInt(scheduleItem.time.split(':')[1]),
),
margin: scheduleItem.margin,
createdAt: now,
updatedAt: now,
})),
};
if (update) {
record.deleteMany = {
createdAt: { lt: now },
};
}
return record;
};
toWaypointWriteModel = (
waypoints: WaypointProps[],
update?: boolean,
): WaypointWriteModel | undefined => {
if (!waypoints) {
return undefined;
}
const now = new Date();
const record: WaypointWriteModel = {
create: 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,
})),
};
if (update) {
record.deleteMany = {
createdAt: { lt: now },
};
}
return record;
};
toDomain = (record: AdReadModel): AdEntity => {
const entity = new AdEntity({
id: record.uuid,

View File

@ -14,6 +14,7 @@ import { CreateAdService } from './core/application/commands/create-ad/create-ad
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service';
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service';
import { PublishMessageWhenAdIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-deleted.domain-event-handler';
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
@ -58,6 +59,7 @@ const eventHandlers: Provider[] = [
const commandHandlers: Provider[] = [
CreateAdService,
UpdateAdService,
DeleteAdService,
DeleteUserAdsService,
ValidateAdService,

View File

@ -13,6 +13,87 @@ import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
import { Waypoint } from '../../types/waypoint';
import { CreateAdCommand } from './create-ad.command';
export function createPropsFromCommand(
command: CreateAdCommand,
datetimeTransformer: DateTimeTransformerPort,
) {
return {
userId: command.userId,
driver: command.driver,
passenger: command.passenger,
frequency: command.frequency,
//TODO Shouldn't that kind of logic be in the domain layer?
fromDate: datetimeTransformer.fromDate(
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
toDate: datetimeTransformer.toDate(
command.toDate,
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({
day: datetimeTransformer.day(
scheduleItem.day,
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
time: datetimeTransformer.time(
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
margin: scheduleItem.margin,
})),
seatsProposed: command.seatsProposed ?? 0,
seatsRequested: command.seatsRequested ?? 0,
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,
},
},
})),
comment: command.comment,
};
}
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
constructor(
@ -23,80 +104,9 @@ export class CreateAdService implements ICommandHandler {
) {}
async execute(command: CreateAdCommand): Promise<AggregateID> {
const ad = AdEntity.create({
userId: command.userId,
driver: command.driver,
passenger: command.passenger,
frequency: command.frequency,
fromDate: this.datetimeTransformer.fromDate(
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
toDate: this.datetimeTransformer.toDate(
command.toDate,
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({
day: this.datetimeTransformer.day(
scheduleItem.day,
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
time: this.datetimeTransformer.time(
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
margin: scheduleItem.margin,
})),
seatsProposed: command.seatsProposed ?? 0,
seatsRequested: command.seatsRequested ?? 0,
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,
},
},
})),
comment: command.comment,
});
const ad = AdEntity.create(
createPropsFromCommand(command, this.datetimeTransformer),
);
try {
await this.repository.insert(ad);

View File

@ -0,0 +1,16 @@
import { CommandProps } from '@mobicoop/ddd-library';
import { CreateAdCommand } from '../create-ad/create-ad.command';
/**
* Ad updates follow the PUT semantics: they replace the entire object.
* Therefore the update command extends the create command to inherit the same properties
* and re-use the data transformation logic.
*/
export class UpdateAdCommand extends CreateAdCommand {
public adId: string;
constructor(props: CommandProps<UpdateAdCommand>) {
super(props);
this.adId = props.adId;
}
}

View File

@ -0,0 +1,29 @@
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
} from '@modules/ad/ad.di-tokens';
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
import { createPropsFromCommand } from '../create-ad/create-ad.service';
import { UpdateAdCommand } from './update-ad.command';
@CommandHandler(UpdateAdCommand)
export class UpdateAdService implements ICommandHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {}
async execute(command: UpdateAdCommand): Promise<void> {
const ad = await this.repository.findOneById(command.adId, {
waypoints: true,
schedule: true,
});
ad.update(createPropsFromCommand(command, this.datetimeTransformer));
await this.repository.update(ad.id, ad);
}
}

View File

@ -1,5 +1,6 @@
import { Address } from './address';
//TODO Why not use the Waypoint value-object from the domain?
export type Waypoint = {
position: number;
} & Address;

View File

@ -1,16 +1,16 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AdEntity } from '../core/domain/ad.entity';
import { AdMapper } from '../ad.mapper';
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
import {
LoggerBase,
MessagePublisherPort,
PrismaRepositoryBase,
} from '@mobicoop/ddd-library';
import { PrismaService } from './prisma.service';
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { SERVICE_NAME } from '@src/app.constants';
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
import { AdMapper } from '../ad.mapper';
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
import { AdEntity } from '../core/domain/ad.entity';
import { PrismaService } from './prisma.service';
export type AdBaseModel = {
uuid: string;
@ -40,13 +40,21 @@ export type AdWriteModel = AdBaseModel & {
};
export type ScheduleWriteModel = {
deleteMany?: PastCreatedFilter;
create: ScheduleItemModel[];
};
export type WaypointWriteModel = {
deleteMany?: PastCreatedFilter;
create: WaypointModel[];
};
// used to delete records created in the past,
// because the order of `create` and `deleteMany` is not guaranteed
export type PastCreatedFilter = {
createdAt: { lt: Date };
};
export type ScheduleItemModel = {
uuid: string;
day: number;