151 lines
3.5 KiB
TypeScript
151 lines
3.5 KiB
TypeScript
import {
|
|
ArgumentNotProvidedException,
|
|
ArgumentInvalidException,
|
|
ArgumentOutOfRangeException,
|
|
} from '../exceptions';
|
|
import { Guard } from '../guard';
|
|
import { convertPropsToObject } from '../utils';
|
|
|
|
export type AggregateID = string;
|
|
|
|
export interface BaseEntityProps {
|
|
id: AggregateID;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export interface CreateEntityProps<T> {
|
|
id: AggregateID;
|
|
props: T;
|
|
createdAt?: Date;
|
|
updatedAt?: Date;
|
|
}
|
|
|
|
export abstract class Entity<EntityProps> {
|
|
constructor({
|
|
id,
|
|
createdAt,
|
|
updatedAt,
|
|
props,
|
|
}: CreateEntityProps<EntityProps>) {
|
|
this.setId(id);
|
|
this.validateProps(props);
|
|
const now = new Date();
|
|
this._createdAt = createdAt || now;
|
|
this._updatedAt = updatedAt || now;
|
|
this.props = props;
|
|
this.validate();
|
|
}
|
|
|
|
protected readonly props: EntityProps;
|
|
|
|
/**
|
|
* ID is set in the concrete entity implementation to support
|
|
* different ID types depending on your needs.
|
|
* For example it could be a UUID for aggregate root,
|
|
* and shortid / nanoid for child entities.
|
|
*/
|
|
protected abstract _id: AggregateID;
|
|
|
|
private readonly _createdAt: Date;
|
|
|
|
private _updatedAt: Date;
|
|
|
|
get id(): AggregateID {
|
|
return this._id;
|
|
}
|
|
|
|
private setId(id: AggregateID): void {
|
|
this._id = id;
|
|
}
|
|
|
|
get createdAt(): Date {
|
|
return this._createdAt;
|
|
}
|
|
|
|
get updatedAt(): Date {
|
|
return this._updatedAt;
|
|
}
|
|
|
|
static isEntity(entity: unknown): entity is Entity<unknown> {
|
|
return entity instanceof Entity;
|
|
}
|
|
|
|
/**
|
|
* Checks if two entities are the same Entity by comparing ID field.
|
|
* @param object Entity
|
|
*/
|
|
public equals(object?: Entity<EntityProps>): boolean {
|
|
if (object === null || object === undefined) {
|
|
return false;
|
|
}
|
|
|
|
if (this === object) {
|
|
return true;
|
|
}
|
|
|
|
if (!Entity.isEntity(object)) {
|
|
return false;
|
|
}
|
|
|
|
return this.id ? this.id === object.id : false;
|
|
}
|
|
|
|
/**
|
|
* Returns entity properties.
|
|
* @return {*} {Props & EntityProps}
|
|
* @memberof Entity
|
|
*/
|
|
public getProps(): EntityProps & BaseEntityProps {
|
|
const propsCopy = {
|
|
id: this._id,
|
|
createdAt: this._createdAt,
|
|
updatedAt: this._updatedAt,
|
|
...this.props,
|
|
};
|
|
return Object.freeze(propsCopy);
|
|
}
|
|
|
|
/**
|
|
* Convert an Entity and all sub-entities/Value Objects it
|
|
* contains to a plain object with primitive types. Can be
|
|
* useful when logging an entity during testing/debugging
|
|
*/
|
|
public toObject(): unknown {
|
|
const plainProps = convertPropsToObject(this.props);
|
|
|
|
const result = {
|
|
id: this._id,
|
|
createdAt: this._createdAt,
|
|
updatedAt: this._updatedAt,
|
|
...plainProps,
|
|
};
|
|
return Object.freeze(result);
|
|
}
|
|
|
|
/**
|
|
* There are certain rules that always have to be true (invariants)
|
|
* for each entity. Validate method is called every time before
|
|
* saving an entity to the database to make sure those rules are respected.
|
|
*/
|
|
public abstract validate(): void;
|
|
|
|
private validateProps(props: EntityProps): void {
|
|
const MAX_PROPS = 50;
|
|
|
|
if (Guard.isEmpty(props)) {
|
|
throw new ArgumentNotProvidedException(
|
|
'Entity props should not be empty',
|
|
);
|
|
}
|
|
if (typeof props !== 'object') {
|
|
throw new ArgumentInvalidException('Entity props should be an object');
|
|
}
|
|
if (Object.keys(props as any).length > MAX_PROPS) {
|
|
throw new ArgumentOutOfRangeException(
|
|
`Entity props should not have more than ${MAX_PROPS} properties`,
|
|
);
|
|
}
|
|
}
|
|
}
|