refactor: moved ID generation from EntityBase to an Entity implementation to support multiple ID types
This commit is contained in:
@@ -17,7 +17,6 @@ export interface BaseEntityProps {
|
||||
export abstract class Entity<EntityProps> {
|
||||
constructor(props: EntityProps) {
|
||||
this.validateProps(props);
|
||||
this._id = ID.generate();
|
||||
const now = DateVO.now();
|
||||
this._createdAt = now;
|
||||
this._updatedAt = now;
|
||||
@@ -26,7 +25,8 @@ export abstract class Entity<EntityProps> {
|
||||
|
||||
protected readonly props: EntityProps;
|
||||
|
||||
private readonly _id: ID;
|
||||
// ID is set in the entity to support different ID types
|
||||
protected abstract readonly _id: ID;
|
||||
|
||||
private readonly _createdAt: DateVO;
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { v4 as uuidV4, validate } from 'uuid';
|
||||
import { ArgumentInvalidException } from '../exceptions';
|
||||
import {
|
||||
DomainPrimitive,
|
||||
ValueObject,
|
||||
} from '../base-classes/value-object.base';
|
||||
|
||||
export class ID extends ValueObject<string> {
|
||||
export abstract class ID extends ValueObject<string> {
|
||||
constructor(value: string) {
|
||||
super({ value });
|
||||
}
|
||||
@@ -14,19 +12,5 @@ export class ID extends ValueObject<string> {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
/**
|
||||
*Returns new ID instance with randomly generated ID value
|
||||
* @static
|
||||
* @return {*} {ID}
|
||||
* @memberof ID
|
||||
*/
|
||||
static generate(): ID {
|
||||
return new ID(uuidV4());
|
||||
}
|
||||
|
||||
protected validate({ value }: DomainPrimitive<string>): void {
|
||||
if (!validate(value)) {
|
||||
throw new ArgumentInvalidException('Incorrect ID format');
|
||||
}
|
||||
}
|
||||
protected abstract validate({ value }: DomainPrimitive<string>): void;
|
||||
}
|
||||
|
||||
22
src/core/value-objects/uuid.value-object.ts
Normal file
22
src/core/value-objects/uuid.value-object.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { v4 as uuidV4, validate } from 'uuid';
|
||||
import { DomainPrimitive } from '../base-classes/value-object.base';
|
||||
import { ArgumentInvalidException } from '../exceptions/argument-invalid.exception';
|
||||
import { ID } from './id.value-object';
|
||||
|
||||
export class UUID extends ID {
|
||||
/**
|
||||
*Returns new ID instance with randomly generated ID value
|
||||
* @static
|
||||
* @return {*} {ID}
|
||||
* @memberof ID
|
||||
*/
|
||||
static generate(): UUID {
|
||||
return new UUID(uuidV4());
|
||||
}
|
||||
|
||||
protected validate({ value }: DomainPrimitive<string>): void {
|
||||
if (!validate(value)) {
|
||||
throw new ArgumentInvalidException('Incorrect UUID format');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,18 @@ export type OrmEntityProps<OrmEntity> = Omit<
|
||||
'id' | 'createdAt' | 'updatedAt'
|
||||
>;
|
||||
|
||||
export interface EntityProps<EntityProps> {
|
||||
id: ID;
|
||||
props: EntityProps;
|
||||
}
|
||||
|
||||
export abstract class OrmMapper<Entity extends BaseEntityProps, OrmEntity> {
|
||||
constructor(
|
||||
private entityConstructor: new (...args: any[]) => Entity,
|
||||
private ormEntityConstructor: new (...args: any[]) => OrmEntity,
|
||||
) {}
|
||||
|
||||
protected abstract toDomainProps(ormEntity: OrmEntity): unknown;
|
||||
protected abstract toDomainProps(ormEntity: OrmEntity): EntityProps<unknown>;
|
||||
|
||||
protected abstract toOrmProps(entity: Entity): OrmEntityProps<OrmEntity>;
|
||||
|
||||
@@ -35,21 +40,24 @@ export abstract class OrmMapper<Entity extends BaseEntityProps, OrmEntity> {
|
||||
});
|
||||
}
|
||||
|
||||
/** Tricking TypeScript to do mapping from OrmEntity to Entity's protected/private properties.
|
||||
* This is done to avoid public setters or accepting all props through constructor.
|
||||
* Public setters may corrupt Entity's state. Accepting every property through constructor may
|
||||
* conflict with some pre-defined business rules that are validated at object creation.
|
||||
* Never use this trick in domain layer. Normally private properties should never be assigned directly.
|
||||
/** Tricking TypeScript to do mapping from OrmEntity to Entity's
|
||||
protected/private properties.
|
||||
This is done to avoid public setters or accepting all props through a
|
||||
constructor. Public setters may corrupt Entity's state. Accepting
|
||||
every property through constructor may conflict with some pre-defined
|
||||
business rules that are validated at object creation.
|
||||
Never use this trick in domain layer. Normally private properties
|
||||
should never be assigned directly.
|
||||
*/
|
||||
private assignPropsToEntity<Props>(
|
||||
entityProps: Props,
|
||||
{ id, props }: EntityProps<Props>,
|
||||
ormEntity: OrmEntity,
|
||||
): Entity {
|
||||
const entityCopy: any = Object.create(this.entityConstructor.prototype);
|
||||
const ormEntityBase: TypeormEntityBase = (ormEntity as unknown) as TypeormEntityBase;
|
||||
|
||||
entityCopy.props = entityProps;
|
||||
entityCopy._id = new ID(ormEntityBase.id);
|
||||
entityCopy.props = props;
|
||||
entityCopy._id = id;
|
||||
entityCopy._createdAt = new DateVO(ormEntityBase.createdAt);
|
||||
entityCopy._updatedAt = new DateVO(ormEntityBase.updatedAt);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { UUID } from 'src/core/value-objects/uuid.value-object';
|
||||
import {
|
||||
EntityProps,
|
||||
OrmEntityProps,
|
||||
OrmMapper,
|
||||
} from 'src/infrastructure/database/base-classes/orm-mapper.base';
|
||||
@@ -20,7 +22,8 @@ export class UserOrmMapper extends OrmMapper<UserEntity, UserOrmEntity> {
|
||||
return ormProps;
|
||||
}
|
||||
|
||||
protected toDomainProps(ormEntity: UserOrmEntity): UserProps {
|
||||
protected toDomainProps(ormEntity: UserOrmEntity): EntityProps<UserProps> {
|
||||
const id = new UUID(ormEntity.id);
|
||||
const props: UserProps = {
|
||||
email: new Email(ormEntity.email),
|
||||
address: new Address({
|
||||
@@ -29,6 +32,6 @@ export class UserOrmMapper extends OrmMapper<UserEntity, UserOrmEntity> {
|
||||
country: ormEntity.country,
|
||||
}),
|
||||
};
|
||||
return props;
|
||||
return { id, props };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AggregateRoot } from 'src/core/base-classes/aggregate-root.base';
|
||||
import { UUID } from 'src/core/value-objects/uuid.value-object';
|
||||
import { UserCreatedDomainEvent } from '../events/user-created.domain-event';
|
||||
import { Address, AddressProps } from '../value-objects/address.value-object';
|
||||
import { Email } from '../value-objects/email.value-object';
|
||||
@@ -15,12 +16,14 @@ export interface UpdateUserAddressProps {
|
||||
}
|
||||
|
||||
export class UserEntity extends AggregateRoot<UserProps> {
|
||||
protected readonly _id: UUID;
|
||||
|
||||
constructor(props: UserProps) {
|
||||
super(props);
|
||||
this._id = UUID.generate();
|
||||
/* adding "UserCreated" Domain Event that will be published
|
||||
eventually so an event handler somewhere may receive it and do an
|
||||
appropriate action, like sending confirmation email, adding user
|
||||
to mailing list, send notification to slack etc */
|
||||
appropriate action */
|
||||
this.addEvent(
|
||||
new UserCreatedDomainEvent({
|
||||
aggregateId: this.id.value,
|
||||
|
||||
Reference in New Issue
Block a user