refactor: moved ID generation from EntityBase to an Entity implementation to support multiple ID types

This commit is contained in:
user
2021-09-14 18:53:59 +02:00
parent 357ceb28fa
commit f65c8cdfb6
6 changed files with 53 additions and 33 deletions

View File

@@ -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;

View File

@@ -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;
}

View 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');
}
}
}

View File

@@ -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);

View File

@@ -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 };
}
}

View File

@@ -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,