Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value Object Enhancements and Bug Fixes #154

Merged
merged 8 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ All notable changes to this project will be documented in this file.
---


### [1.23.1] - 2024-05-02

#### Fix

- Fix: Improved return typings in the `get` method of the value object.
- Fix: Bug fix #152 when cloning an instance of a value object.
- Fix: Ensure that properties of an entity or aggregate will always be an object.
- Fix: Improved validations performed in the `isEqual` method of the value object.

---


### [1.23.0] - 2024-04-28

#### Changes
Expand Down
7 changes: 4 additions & 3 deletions lib/core/auto-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ export class AutoMapper<Props> implements IAutoMapper<Props> {
* @returns an object or a value object value.
*/
valueObjectToObj(valueObject: IValueObject<Props>): AutoMapperSerializer<Props> {

// internal state
if (valueObject === null) return null as any;

if (this.validator.isSymbol(valueObject)) return (valueObject as unknown as Symbol).description as any;
if (this.validator.isID(valueObject)) return (valueObject as any)?.value();

let props = {} as { [key in keyof Props]: any };
Expand All @@ -27,7 +26,6 @@ export class AutoMapper<Props> implements IAutoMapper<Props> {
this.validator.isString(valueObject) ||
this.validator.isObject(valueObject) || // primitive object
this.validator.isDate(valueObject);

if (isSimpleValue) return valueObject as AutoMapperSerializer<Props>

const isID = this.validator.isID(valueObject);
Expand All @@ -46,6 +44,8 @@ export class AutoMapper<Props> implements IAutoMapper<Props> {

if (isSimp) return voProps;

if (this.validator.isSymbol(voProps)) return (voProps as unknown as Symbol).description as any;

const keys: Array<keyof Props> = Object.keys(voProps) as Array<keyof Props>;

const values = keys.map((key) => {
Expand Down Expand Up @@ -161,6 +161,7 @@ export class AutoMapper<Props> implements IAutoMapper<Props> {
this.validator.isString(props?.[key as any]) ||
this.validator.isObject(props?.[key as any]) ||
this.validator.isDate(props?.[key as any]) ||
this.validator.isSymbol(props) ||
props?.[key] === null;

const isEntity = this.validator.isEntity(props?.[key as any]);
Expand Down
20 changes: 19 additions & 1 deletion lib/core/base-getters-and-setters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ICreateManyDomain, IBaseGettersAndSetters, ISettings, UID, IVoSettings } from "../types";
import { BuiltIns } from "../types-util";
import util, { Utils } from "../utils/util";
import validator, { Validator } from "../utils/validator";
import createManyDomainInstances from "./create-many-domain-instance";
Expand Down Expand Up @@ -56,7 +57,22 @@ export class BaseGettersAndSetters<Props> implements IBaseGettersAndSetters<Prop
* @param key the property key you want to get
* @returns the value of property
*/
get<Key extends keyof Props>(key: Props extends object ? (Props extends { [k in Key]: Date } ? Key : 'value') : 'value'): Readonly<Props extends { [k in keyof Props]: Props[k] } ? Readonly<Props[Key]> : Readonly<Props>> {
get<Key extends keyof Props>(
key: Props extends BuiltIns ?
'value' :
Props extends Symbol ?
'value' :
Props extends any[] ?
'value' :
Key
): Props extends BuiltIns ?
Props :
Props extends Symbol ?
string :
Props extends any[] ?
Readonly<Props> :
Props extends {} ?
Readonly<Props[Key]> : Props {
if (this.config.disableGetters) {
const instance = Reflect.getPrototypeOf(this);
throw new Error(`Trying to get key: "${String(key)}" but the getters are deactivated on ${instance?.constructor.name}`);
Expand All @@ -69,6 +85,8 @@ export class BaseGettersAndSetters<Props> implements IBaseGettersAndSetters<Prop
this.validator.isNumber(this.props) ||
this.validator.isString(this.props)

if (this.validator.isSymbol(this.props)) return (this.props as Symbol).description as any;

if (isSimpleValue) return this.props as any;

const isID = this.validator.isID(this.props);
Expand Down
21 changes: 13 additions & 8 deletions lib/core/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export class Entity<Props extends EntityProps> extends GettersAndSetters<Props>
protected autoMapper: AutoMapper<Props>;
constructor(props: Props, config?: ISettings) {
super(Object.assign({}, { createdAt: new Date(), updatedAt: new Date() }, { ...props }), 'Entity', config);
if (typeof props !== 'object' || (props instanceof Date) || Array.isArray(props)) {
throw new Error(`Props must be an 'object' for entities, but received: '${typeof props}' as props on Class '${this.constructor.name}'`);
};
const isID = this.validator.isID(props?.['id']);
const isStringOrNumber = this.validator.isString(props?.['id']) || this.validator.isNumber(props?.['id']);
this._id = isStringOrNumber ? ID.create(props?.['id']) : isID ? props?.['id'] : ID.create();
Expand All @@ -27,16 +30,19 @@ export class Entity<Props extends EntityProps> extends GettersAndSetters<Props>
* @returns true if props is equal and false if not.
*/
isEqual(other: this): boolean {
const currentProps = Object.assign({}, {}, { ...this?.props });
const providedProps = Object.assign({}, {}, { ...other?.props });
const currentProps = { ...this?.props };
const providedProps = { ...other?.props };

delete currentProps?.['createdAt'];
delete currentProps?.['updatedAt'];
delete providedProps?.['createdAt'];
delete providedProps?.['updatedAt'];
const equalId = this.id.equal(other?.id);

const equalId = this.id.isEqual(other?.id);
const serializedA = JSON.stringify(currentProps);
const serializedB = JSON.stringify(providedProps);
const equalSerialized = serializedA === serializedB;

return equalId && equalSerialized;
}

Expand All @@ -47,7 +53,7 @@ export class Entity<Props extends EntityProps> extends GettersAndSetters<Props>
toObject<T>(adapter?: IAdapter<this, T>)
: T extends {}
? T & EntityMapperPayload
: ReadonlyDeep<AutoMapperSerializer<Props> & EntityMapperPayload> {
: ReadonlyDeep<AutoMapperSerializer<Props> & EntityMapperPayload> {
if (adapter && typeof adapter?.build === 'function') return adapter.build(this).value() as any;

const serializedObject = this.autoMapper.entityToObj(this) as ReadonlyDeep<AutoMapperSerializer<Props>>;
Expand Down Expand Up @@ -91,12 +97,11 @@ export class Entity<Props extends EntityProps> extends GettersAndSetters<Props>
* @param props as optional Entity Props.
* @returns new Entity instance.
*/
clone(props?: Partial<Props>): this {
const _props = props ? { ...this.props, ...props } : { ...this.props };
clone(props?: Props extends object ? Partial<Props> : never): this {
const instance = Reflect.getPrototypeOf(this);
const _props = props ? { ...this.props, ...props } : { ...this.props };
const args = [_props, this.config];
const entity = Reflect.construct(instance!.constructor, args);
return entity
return Reflect.construct(instance!.constructor, args);
}

/**
Expand Down
69 changes: 53 additions & 16 deletions lib/core/value-object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AutoMapperSerializer, IAdapter, IResult, IValueObject, IVoSettings } from "../types";
import { AutoMapperSerializer, IAdapter, IResult, IValueObject, IVoSettings, UID } from "../types";
import { ReadonlyDeep } from "../types-util";
import { deepFreeze } from "../utils/deep-freeze.util";
import AutoMapper from "./auto-mapper";
Expand All @@ -22,39 +22,76 @@ export class ValueObject<Props> extends BaseGettersAndSetters<Props> implements
* @returns true if props is equal and false if not.
*/
isEqual(other: this): boolean {
const currentProps = Object.assign({}, {}, { ...this?.props});
const providedProps = Object.assign({}, {}, { ...other?.props});
delete currentProps?.['createdAt'];
delete currentProps?.['updatedAt'];
delete providedProps?.['createdAt'];
delete providedProps?.['updatedAt'];
return JSON.stringify(currentProps) === JSON.stringify(providedProps);
const props = this.props;
const otherProps = other?.props;

const stringifyAndOmit = (obj: any): string => {
if (!obj) return '';
const { createdAt, updatedAt, ...cleanedProps } = obj;
return JSON.stringify(cleanedProps);
};

if (this.validator.isString(props)) {
return this.validator.string(props as string).isEqual(otherProps as string);
}

if (this.validator.isDate(props)) {
return (props as Date).getTime() === (otherProps as Date)?.getTime();
}

if (this.validator.isArray(props) || this.validator.isFunction(props)) {
return JSON.stringify(props) === JSON.stringify(otherProps);
}

if (this.validator.isBoolean(props)) {
return props === otherProps;
}

if (this.validator.isID(props)) {
return (props as UID).value() === (otherProps as UID)?.value();
}

if (this.validator.isNumber(props) || typeof props === 'bigint') {
return this.validator.number(props as number).isEqualTo(otherProps as number);
}

if (this.validator.isUndefined(props) || this.validator.isNull(props)) {
return props === otherProps;
}

if (this.validator.isSymbol(props)) {
return (props as Symbol).description === (otherProps as Symbol)?.description;
}

return stringifyAndOmit(props) === stringifyAndOmit(otherProps);
}

/**
* @description Get an instance copy.
* @returns a new instance of value object.
*/
clone(props?: Partial<Props>): ValueObject<Props> {
const _props = props ? { ...this.props, ...props } : { ...this.props };
clone(props?: Props extends object ? Partial<Props> : never): this {
const instance = Reflect.getPrototypeOf(this);
const args = [_props, this.config];
const obj = Reflect.construct(instance!.constructor, args);
return obj;
if (typeof this.props === 'object' && !(this.props instanceof Date) && !(Array.isArray(this.props))) {
const _props = props ? { ...this.props, ...props } : { ...this.props };
const args = [_props, this.config];
return Reflect.construct(instance!.constructor, args);
}
const args = [this.props, this.config];
return Reflect.construct(instance!.constructor, args);
}

/**
* @description Get value from value object.
* @returns value as string, number or any type defined.
*/
toObject<T>(adapter? :IAdapter<this, T>)
toObject<T>(adapter?: IAdapter<this, T>)
: T extends {}
? T
: ReadonlyDeep<AutoMapperSerializer<Props>> {
if (adapter && typeof adapter?.build === 'function') return adapter.build(this).value() as any

const serializedObject = this.autoMapper.valueObjectToObj(this) as ReadonlyDeep<AutoMapperSerializer<Props>>;
const frozenObject = deepFreeze<any>(serializedObject);
const frozenObject = deepFreeze<any>(serializedObject);
return frozenObject;
}

Expand Down
5 changes: 3 additions & 2 deletions lib/types-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ type ReadonlyObjectDeep<ObjectType extends object> = {
readonly [KeyType in keyof ObjectType]: ReadonlyDeep<ObjectType[KeyType]>
};

type Primitive =
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint;
type BuiltIns = Primitive | void | Date | RegExp;
export type BuiltIns = Primitive | void | Date | RegExp;

/**
* @description Deeply readonly object.
* @link https://github.com/sindresorhus/type-fest/blob/main/source/readonly-deep.d.ts
Expand Down
33 changes: 24 additions & 9 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Entity, ValueObject } from "./core";
import { ReadonlyDeep } from "./types-util";
import { BuiltIns, ReadonlyDeep } from "./types-util";

export type Event = { detail: any[] };

Expand Down Expand Up @@ -225,7 +225,22 @@ export interface IEntityGettersAndSetters<Props> {
}

export interface IBaseGettersAndSetters<Props> {
get<Key extends keyof Props>(key: Props extends object ? (Props extends { [k in Key]: Date } ? Key : 'value'): 'value'): Readonly<Props extends { [k in keyof Props]: Props[k] } ? Readonly<Props[Key]> : Readonly<Props>>
get<Key extends keyof Props>(
key: Props extends BuiltIns ?
'value' :
Props extends Symbol ?
'value' :
Props extends any[] ?
'value' :
Key
): Props extends BuiltIns ?
Props :
Props extends Symbol ?
string :
Props extends any[] ?
Readonly<Props> :
Props extends {} ?
Readonly<Props[Key]> : Props
getRaw(): Props;
}

Expand Down Expand Up @@ -256,13 +271,13 @@ export type AutoMapperSerializer<Props> = {
? AutoMapperSerializer<SerializerEntityReturnType<Props[key]>> & EntityMapperPayload
: Props[key] extends Array<any>
? Array<
AutoMapperSerializer<ReturnType<Props[key][0]['getRaw']>>
& (
Props[key][0] extends Entity<any>
? EntityMapperPayload
: {}
)
>
AutoMapperSerializer<ReturnType<Props[key][0]['getRaw']>>
& (
Props[key][0] extends Entity<any>
? EntityMapperPayload
: {}
)
>
: Props[key]
}
export interface IAutoMapper<Props> {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rich-domain",
"version": "1.23.0",
"version": "1.23.1",
"description": "This package provide utils file and interfaces to assistant build a complex application with domain driving design",
"main": "index.js",
"types": "index.d.ts",
Expand Down
Loading
Loading