Skip to content

Commit 5e4aa7c

Browse files
committedSep 19, 2021
refactor: abstracted UnitOfWork initiation
1 parent 16db79b commit 5e4aa7c

File tree

12 files changed

+95
-46
lines changed

12 files changed

+95
-46
lines changed
 

‎src/infrastructure/database/unit-of-work.ts

-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ import { WalletRepository } from '@modules/wallet/database/wallet.repository';
77
export class UnitOfWork extends UnitOfWorkOrm {
88
// Add new repositories below to use this generic UnitOfWork
99

10-
init(): string {
11-
return UnitOfWork.init();
12-
}
13-
1410
// Convert TypeOrm Repository to a Domain Repository
1511
getUserRepository(correlationId: string): UserRepository {
1612
return new UserRepository(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { UnitOfWorkOrm } from '../../infrastructure/database/base-classes/unit-of-work-orm';
2+
import { ID } from '../value-objects/id.value-object';
3+
import { Command } from './command.base';
4+
5+
export abstract class CommandHandler<UnitOfWork extends UnitOfWorkOrm> {
6+
constructor(protected readonly unitOfWork: UnitOfWork) {}
7+
8+
protected abstract execute(command: Command): Promise<ID>;
9+
10+
async executeUnitOfWork(command: Command): Promise<ID> {
11+
UnitOfWorkOrm.init(command.correlationId);
12+
return UnitOfWorkOrm.execute(command.correlationId, async () =>
13+
this.execute(command),
14+
);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { nanoid } from 'nanoid';
2+
import { ArgumentNotProvidedException } from '../../../exceptions';
3+
import { Guard } from '../guard';
4+
5+
export type CommandProps<T> = Omit<T, 'correlationId'> & Partial<Command>;
6+
7+
export class Command {
8+
public readonly correlationId: string;
9+
10+
constructor(props: CommandProps<unknown>) {
11+
if (Guard.isEmpty(props)) {
12+
throw new ArgumentNotProvidedException(
13+
'Command props should not be empty',
14+
);
15+
}
16+
this.correlationId = props.correlationId || nanoid(8);
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
1+
import { ArgumentNotProvidedException } from '../../../exceptions';
2+
import { Guard } from '../guard';
3+
4+
export type DomainEventProps<T> = Omit<T, 'correlationId' | 'dateOccurred'> &
5+
Omit<DomainEvent, 'correlationId' | 'dateOccurred'> & {
6+
correlationId?: string;
7+
dateOccurred?: number;
8+
};
9+
110
export abstract class DomainEvent {
2-
constructor(
3-
public readonly aggregateId: string,
4-
public readonly dateOccurred: number,
5-
public correlationId?: string,
6-
) {}
11+
/** Aggregate ID where domain event occurred */
12+
public readonly aggregateId: string;
13+
14+
/** Date when this domain event occurred */
15+
public readonly dateOccurred: number;
16+
17+
/** ID for correlation purposes (for UnitOfWork, Integration Events,logs correlation etc).
18+
* This ID is set automatically in a publisher.
19+
*/
20+
public correlationId: string;
21+
22+
constructor(props: DomainEventProps<unknown>) {
23+
if (Guard.isEmpty(props)) {
24+
throw new ArgumentNotProvidedException(
25+
'DomainEvent props should not be empty',
26+
);
27+
}
28+
this.aggregateId = props.aggregateId;
29+
this.dateOccurred = props.dateOccurred || Date.now();
30+
if (props.correlationId) this.correlationId = props.correlationId;
31+
}
732
}

‎src/libs/ddd/infrastructure/database/base-classes/unit-of-work-orm.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { Logger } from '@nestjs/common';
22
import { EntityTarget, getConnection, QueryRunner, Repository } from 'typeorm';
3-
import { nanoid } from 'nanoid';
43

54
export class UnitOfWorkOrm {
65
private static queryRunners: Map<string, QueryRunner> = new Map();
76

87
/**
9-
* Create a connection pool and get its ID.
10-
* ID is used for correlation purposes (to use a correct query runner, correlate logs etc)
8+
* Creates a connection pool with a specified ID.
119
*/
12-
static init(): string {
10+
static init(correlationId: string): void {
11+
if (!correlationId) {
12+
throw new Error('Correlation ID must be provided');
13+
}
1314
const queryRunner = getConnection().createQueryRunner();
14-
const correlationId = nanoid(8);
1515
this.queryRunners.set(correlationId, queryRunner);
16-
return correlationId;
1716
}
1817

1918
static getQueryRunner(correlationId: string): QueryRunner {
@@ -47,7 +46,7 @@ export class UnitOfWorkOrm {
4746
const logger = new Logger(`${this.name}:${correlationId}`);
4847
logger.debug(`[Starting transaction]`);
4948
const queryRunner = this.getQueryRunner(correlationId);
50-
await queryRunner.startTransaction();
49+
await queryRunner.startTransaction('SERIALIZABLE');
5150
let result: T;
5251
try {
5352
result = await callback();

‎src/modules/user/commands/create-user/create-user.cli.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class CreateUserCliController {
3838
street,
3939
});
4040

41-
const id = await this.service.execute(command);
41+
const id = await this.service.executeUnitOfWork(command);
4242

4343
this.logger.log('User created:', id.value);
4444
}

‎src/modules/user/commands/create-user/create-user.command.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import {
2+
Command,
3+
CommandProps,
4+
} from '@src/libs/ddd/domain/base-classes/command.base';
5+
16
// Command is a plain object with properties
2-
export class CreateUserCommand {
3-
constructor(props: CreateUserCommand) {
7+
export class CreateUserCommand extends Command {
8+
constructor(props: CommandProps<CreateUserCommand>) {
9+
super(props);
410
this.email = props.email;
511
this.country = props.country;
612
this.postalCode = props.postalCode;

‎src/modules/user/commands/create-user/create-user.http.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class CreateUserHttpController {
3030
async create(@Body() body: CreateUserHttpRequest): Promise<IdResponse> {
3131
const command = new CreateUserCommand(body);
3232

33-
const id = await this.service.execute(command);
33+
const id = await this.service.executeUnitOfWork(command);
3434

3535
return new IdResponse(id.value);
3636
}

‎src/modules/user/commands/create-user/create-user.message.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class CreateUserMessageController {
1616
async create(message: CreateUserMessageRequest): Promise<IdResponse> {
1717
const command = new CreateUserCommand(message);
1818

19-
const id = await this.service.execute(command);
19+
const id = await this.service.executeUnitOfWork(command);
2020

2121
return new IdResponse(id.value);
2222
}

‎src/modules/user/commands/create-user/create-user.service.ts

+6-18
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ import { ConflictException } from '@libs/exceptions';
44
import { Address } from '@modules/user/domain/value-objects/address.value-object';
55
import { Email } from '@modules/user/domain/value-objects/email.value-object';
66
import { UnitOfWork } from '@src/infrastructure/database/unit-of-work';
7+
import { CommandHandler } from '@src/libs/ddd/domain/base-classes/command-handler.base';
78
import { CreateUserCommand } from './create-user.command';
89
import { UserEntity } from '../../domain/entities/user.entity';
910

10-
export class CreateUserService {
11-
constructor(private readonly unitOfWork: UnitOfWork) {}
12-
13-
async create(
14-
command: CreateUserCommand,
15-
userRepo: UserRepositoryPort,
16-
): Promise<ID> {
11+
export class CreateUserService extends CommandHandler<UnitOfWork> {
12+
protected async execute(command: CreateUserCommand): Promise<ID> {
13+
const userRepo: UserRepositoryPort = this.unitOfWork.getUserRepository(
14+
command.correlationId,
15+
);
1716
// user uniqueness guard
1817
if (await userRepo.exists(command.email)) {
1918
throw new ConflictException('User already exists');
@@ -34,15 +33,4 @@ export class CreateUserService {
3433

3534
return created.id;
3635
}
37-
38-
async execute(command: CreateUserCommand): Promise<ID> {
39-
const correlationId = this.unitOfWork.init();
40-
const userRepo: UserRepositoryPort = this.unitOfWork.getUserRepository(
41-
correlationId,
42-
);
43-
// Wrapping user creation in a UnitOfWork so events get included in a transaction
44-
return UnitOfWork.execute(correlationId, async () =>
45-
this.create(command, userRepo),
46-
);
47-
}
4836
}

‎src/modules/user/domain/entities/user.entity.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ export interface UserProps extends CreateUserProps {
1919
export class UserEntity extends AggregateRoot<UserProps> {
2020
protected readonly _id: UUID;
2121

22-
static create(createUser: CreateUserProps): UserEntity {
22+
static create(create: CreateUserProps): UserEntity {
2323
const id = UUID.generate();
24-
// Setting a default role since it is not accepted during creation
25-
const props: UserProps = { ...createUser, role: UserRoles.guest };
24+
/* Setting a default role since we are not accepting it during creation. */
25+
const props: UserProps = { ...create, role: UserRoles.guest };
2626
const user = new UserEntity({ id, props });
2727
/* adding "UserCreated" Domain Event that will be published
2828
eventually so an event handler somewhere may receive it and do an
@@ -32,7 +32,6 @@ export class UserEntity extends AggregateRoot<UserProps> {
3232
aggregateId: id.value,
3333
email: props.email.getRawProps(),
3434
...props.address.getRawProps(),
35-
dateOccurred: Date.now(),
3635
}),
3736
);
3837
return user;
@@ -63,6 +62,8 @@ export class UserEntity extends AggregateRoot<UserProps> {
6362
...this.props.address,
6463
...props,
6564
} as AddressProps);
65+
66+
// Note: AddressUpdatedDomainEvent can be emitted here if needed.
6667
}
6768

6869
someBusinessLogic(): void {

‎src/modules/user/domain/events/user-created.domain-event.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { DomainEvent } from '@libs/ddd/domain/domain-events';
1+
import { DomainEvent, DomainEventProps } from '@libs/ddd/domain/domain-events';
22

33
// DomainEvent is a plain object with properties
44
export class UserCreatedDomainEvent extends DomainEvent {
5-
constructor(props: UserCreatedDomainEvent) {
6-
super(props.aggregateId, props.dateOccurred);
5+
constructor(props: DomainEventProps<UserCreatedDomainEvent>) {
6+
super(props);
77
this.email = props.email;
88
this.country = props.country;
99
this.postalCode = props.postalCode;

0 commit comments

Comments
 (0)
Failed to load comments.