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

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 { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.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 { 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 { 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 { 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'; 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[] = [ const commandHandlers: Provider[] = [
CreateAdService, CreateAdService,
UpdateAdService,
DeleteAdService, DeleteAdService,
DeleteUserAdsService, DeleteUserAdsService,
ValidateAdService, ValidateAdService,

View File

@ -13,22 +13,17 @@ import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
import { Waypoint } from '../../types/waypoint'; import { Waypoint } from '../../types/waypoint';
import { CreateAdCommand } from './create-ad.command'; import { CreateAdCommand } from './create-ad.command';
@CommandHandler(CreateAdCommand) export function createPropsFromCommand(
export class CreateAdService implements ICommandHandler { command: CreateAdCommand,
constructor( datetimeTransformer: DateTimeTransformerPort,
@Inject(AD_REPOSITORY) ) {
private readonly repository: AdRepositoryPort, return {
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {}
async execute(command: CreateAdCommand): Promise<AggregateID> {
const ad = AdEntity.create({
userId: command.userId, userId: command.userId,
driver: command.driver, driver: command.driver,
passenger: command.passenger, passenger: command.passenger,
frequency: command.frequency, frequency: command.frequency,
fromDate: this.datetimeTransformer.fromDate( //TODO Shouldn't that kind of logic be in the domain layer?
fromDate: datetimeTransformer.fromDate(
{ {
date: command.fromDate, date: command.fromDate,
time: command.schedule[0].time, time: command.schedule[0].time,
@ -39,7 +34,7 @@ export class CreateAdService implements ICommandHandler {
}, },
command.frequency, command.frequency,
), ),
toDate: this.datetimeTransformer.toDate( toDate: datetimeTransformer.toDate(
command.toDate, command.toDate,
{ {
date: command.fromDate, date: command.fromDate,
@ -52,7 +47,7 @@ export class CreateAdService implements ICommandHandler {
command.frequency, command.frequency,
), ),
schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({ schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({
day: this.datetimeTransformer.day( day: datetimeTransformer.day(
scheduleItem.day, scheduleItem.day,
{ {
date: command.fromDate, date: command.fromDate,
@ -64,7 +59,7 @@ export class CreateAdService implements ICommandHandler {
}, },
command.frequency, command.frequency,
), ),
time: this.datetimeTransformer.time( time: datetimeTransformer.time(
{ {
date: command.fromDate, date: command.fromDate,
time: scheduleItem.time, time: scheduleItem.time,
@ -96,7 +91,22 @@ export class CreateAdService implements ICommandHandler {
}, },
})), })),
comment: command.comment, comment: command.comment,
}); };
}
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {}
async execute(command: CreateAdCommand): Promise<AggregateID> {
const ad = AdEntity.create(
createPropsFromCommand(command, this.datetimeTransformer),
);
try { try {
await this.repository.insert(ad); 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'; import { Address } from './address';
//TODO Why not use the Waypoint value-object from the domain?
export type Waypoint = { export type Waypoint = {
position: number; position: number;
} & Address; } & 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 { import {
LoggerBase, LoggerBase,
MessagePublisherPort, MessagePublisherPort,
PrismaRepositoryBase, PrismaRepositoryBase,
} from '@mobicoop/ddd-library'; } from '@mobicoop/ddd-library';
import { PrismaService } from './prisma.service'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { SERVICE_NAME } from '@src/app.constants'; 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 = { export type AdBaseModel = {
uuid: string; uuid: string;
@ -40,13 +40,21 @@ export type AdWriteModel = AdBaseModel & {
}; };
export type ScheduleWriteModel = { export type ScheduleWriteModel = {
deleteMany?: PastCreatedFilter;
create: ScheduleItemModel[]; create: ScheduleItemModel[];
}; };
export type WaypointWriteModel = { export type WaypointWriteModel = {
deleteMany?: PastCreatedFilter;
create: WaypointModel[]; 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 = { export type ScheduleItemModel = {
uuid: string; uuid: string;
day: number; day: number;