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

BC-7772 introduce ddd objects #5167

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion apps/server/src/shared/common/guards/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { TypeGuard } from './type.guard';
export { TypeGuard, PrimitiveType, PrimitiveTypeArray, ObjectType } from './type.guard';

// Guards at different places exists insdide the modules, as validation as utils.
// Please consolidate it and make it explicit as guard.
54 changes: 54 additions & 0 deletions apps/server/src/shared/common/guards/type.guard.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { shallowEqual, shallowEqualArrays, shallowEqualObjects } from 'shallow-equal';

export type PrimitiveType = number | string | boolean | undefined | symbol | bigint;

/** Note that undefined is also added to match our usage. */
export type ObjectType = Record<string, unknown> | unknown[] | null | undefined;

export type PrimitiveTypeArray = PrimitiveType[];

export class TypeGuard {
static isError(value: unknown): value is Error {
const isError = value instanceof Error;
Expand Down Expand Up @@ -150,4 +159,49 @@ export class TypeGuard {

return value;
}

static isBoolean(value: unknown): boolean {
const isBoolean = typeof value === 'boolean';

return isBoolean;
}

static isBigInt(value: unknown): boolean {
const isBigInt = typeof value === 'bigint';

return isBigInt;
}

static isSymbol(value: unknown): boolean {
const isSymbol = typeof value === 'symbol';

return isSymbol;
}

static isPrimitiveType(value: unknown): boolean {
const isPrimitiveType =
TypeGuard.isNumber(value) ||
TypeGuard.isString(value) ||
TypeGuard.isBoolean(value) ||
TypeGuard.isUndefined(value) ||
TypeGuard.isSymbol(value) ||
TypeGuard.isBigInt(value);

return isPrimitiveType;
}

static isSameClassTyp(value1: unknown, value2: unknown): boolean {
const isSameClassTyp =
TypeGuard.isDefinedObject(value1) &&
TypeGuard.isDefinedObject(value2) &&
value1.constructor.name === value2.constructor.name;

return isSameClassTyp;
}

static isShallowEqualArray = shallowEqualArrays;

static isShallowEqualObject = shallowEqualObjects;

static shallowEqualObjectOrArray = shallowEqual;
}
187 changes: 187 additions & 0 deletions apps/server/src/shared/domain/value-object.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { TypeGuard } from '@shared/common';
import { ValueObject } from './value-object';

describe('ValueObject', () => {
describe('By passing valid values', () => {
const setup = () => {
class Name extends ValueObject<string> {}

const nameThor1 = new Name('Thor');
const nameThor2 = new Name('Thor');

return { nameThor1, nameThor2 };
};

it('should be return true by same reference', () => {
const { nameThor1 } = setup();

expect(nameThor1.equals(nameThor1)).toBe(true);
});

it('should be return true by different references', () => {
const { nameThor1, nameThor2 } = setup();

expect(nameThor1.equals(nameThor2)).toBe(true);
});
});

describe('By usage of primitive array as value object', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"of array with primitive values"

const setup = () => {
class Names extends ValueObject<string[]> {}

const nameThors1 = new Names(['Thor1', 'Thor2', 'Thor3']);
const nameThors2 = new Names(['Thor1', 'Thor2', 'Thor3']);
const nameThorsDifferent = new Names(['Thor1', 'Thor2', 'Thor4']);
const nameThorsDifferentCount = new Names(['Thor1', 'Thor2']);

return { nameThors1, nameThors2, nameThorsDifferent, nameThorsDifferentCount };
};

it('should be return true by same reference', () => {
const { nameThors1 } = setup();

expect(nameThors1.equals(nameThors1)).toBe(true);
});

it('should be return true by different references', () => {
const { nameThors1, nameThors2 } = setup();

expect(nameThors1.equals(nameThors2)).toBe(true);
});

it('should be return false by different values inside the array', () => {
const { nameThors1, nameThorsDifferentCount } = setup();

expect(nameThors1.equals(nameThorsDifferentCount)).toBe(false);
});

it('should be return false by different values inside the array', () => {
const { nameThors1, nameThorsDifferent } = setup();

expect(nameThors1.equals(nameThorsDifferent)).toBe(false);
});
});

describe('When value object with validation is created.', () => {
const setup = () => {
class Name extends ValueObject<string> {
protected validation(value: unknown): boolean {
let isValid = false;

if (TypeGuard.isString(value) && this.isValidLength(value)) {
isValid = true;
}

return isValid;
}

private isValidLength(value: string): boolean {
let isValid = false;

if (value.length > 0 && value.length <= 5) {
isValid = true;
}

return isValid;
}
}

return { Name };
};

it('validation should be execute and throw if not valid', () => {
const { Name } = setup();

const expectedError = new Error('ValueObject Name validation is failed for input ""');
expect(() => new Name('')).toThrowError(expectedError);
});

it('should execute but not throw if is valid value is passsed', () => {
const { Name } = setup();

expect(new Name('Thor')).toBeDefined();
});
/*
it('should execute but not throw if is valid value is passsed', () => {
const { Name } = setup();

const myName = new Name('Thor');

expect(myName.isValidValue('123456')).toBe(false);
expect(myName.isValidValue('')).toBe(false);
expect(myName.isValidValue('ABC')).toBe(true);
});
*/
});

describe('When value object with modification is created.', () => {
const setup = () => {
class Name extends ValueObject<string> {
protected modified(value: string): string {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing the boilerplate with array of functions that can be execute will improve the usability
modifications: [this.removeWithspacesAtBeginning, this.truncatedToLength4]
validations: [TypeGuard.isString, this.isValidLength]

In this case it is also easy to use same hooks for different value objects. But it is harder to implement with ts. We should look togehter into it.

let modifedValue = value;

modifedValue = this.removeWithspacesAtBeginning(value);
modifedValue = this.truncatedToLength4(modifedValue);

return modifedValue;
}

private removeWithspacesAtBeginning(value: string): string {
return value.trimStart();
}

private truncatedToLength4(value: string): string {
return value.substring(0, 4);
}
}

return { Name };
};

it('should be execute all modifications by creating a new instance', () => {
const { Name } = setup();

const myName = new Name(' Thor the God of Thunder!');

expect(myName.value).toEqual('Thor');
});
});

describe('By passing invalid values', () => {
const setup = () => {
class Name extends ValueObject<string> {}
class OtherString extends ValueObject<string> {}
// class InvalidType extends ValueObject<{}> {} --> object and functions do not work
// TODO: Test for array

const nameThor1 = new Name('Thor');
const nameUnhappy = new Name('Unhappy');
const otherThor = new OtherString('Thor');
const fakeThor = {
value: 'Thor',
equals: (vo: ValueObject<string>): boolean => vo === this,
};

return { nameThor1, nameUnhappy, otherThor, fakeThor };
};

it('should be return false by different value', () => {
const { nameThor1, nameUnhappy } = setup();

expect(nameThor1.equals(nameUnhappy)).toBe(false);
});

it('should be return false if value object is passed ', () => {
const { nameThor1, fakeThor } = setup();

// @ts-expect-error Test case
expect(nameThor1.equals(fakeThor)).toBe(false);
});

it('should be return false by different ValueObjects and same value', () => {
const { nameThor1, otherThor } = setup();

expect(nameThor1.equals(otherThor)).toBe(false);
});
});
});
73 changes: 73 additions & 0 deletions apps/server/src/shared/domain/value-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { TypeGuard, PrimitiveType, PrimitiveTypeArray } from '@shared/common';

type ValueObjectTyp = PrimitiveType | PrimitiveTypeArray;

export abstract class ValueObject<T extends ValueObjectTyp> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ergibt es überhaupt Sinn eine abstrakte Klasse für Value Objects einzuführen? Geht uns dadurch nicht ganz viel Fachlichkeit verloren, wenn zB der Wert immer Value heißt? Was an der abstrakten Klasse bietet wirklich einen Mehrwert beim entwickeln?

public readonly value: T;

constructor(value: T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:

// TODO: No Test for the execution order exists for now, but we must clarify if we want first the modifcation, or first the validation
// For operations with truncat before make more sense. Adding before/after modifications are also possible, but it can be overload the interface
const modifiedValue = this.modified(value);
this.checkValue(modifiedValue);
this.value = Object.freeze(modifiedValue);
}

/** Use this method with override for add validations. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected validation(value: unknown): boolean {
return true;
}

/** Use this method with override for add modifications, before execute the validation. */
protected modified(value: T): T {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:

// TODO: Why eslint think that T is from type any is unlear for me.
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return value;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:

// TODO: make this method sense if we not can add this as static MyValueObject.isValidValue() ?
// Should it be optional by adding in extended class, or as abstract that it must be implemented?
/*
public isValidValue(value: unknown): boolean {
return this.validation(value);
}
*/
// TODO: Same questions make it sense without static to set it public?
// If not than we can change it to private for a less overloaded interface at the value object
private checkValue(value: unknown): void {
if (!this.validation(value)) {
throw new Error(`ValueObject ${this.constructor.name} validation is failed for input ${JSON.stringify(value)}`);
}
}

/** The equal methode of ValueObjects check the value is equal not the reference. */
public equals(vo: ValueObject<T>): boolean {
if (!TypeGuard.isSameClassTyp(this, vo)) {
return false;
}

if (TypeGuard.isPrimitiveType(vo.value)) {
return vo.value === this.value;
}

if (TypeGuard.isArray(vo.value) && TypeGuard.isArray(this.value)) {
return TypeGuard.isShallowEqualArray(this.value, vo.value);
}

return false;

/*
VS


let isEqual = false;
if (TypeGuard.isPrimitiveType(vo.value)) {
isEqual = vo.value === this.value;
} else if (TypeGuard.isArray(vo.value) && TypeGuard.isArray(this.value)) {
isEqual = TypeGuard.isShallowEqualArray(this.value, vo.value);
}

return isEqual; */
}
}
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"sanitize-html": "^2.13.0",
"serve-favicon": "^2.3.2",
"service": "^0.1.4",
"shallow-equal": "^3.1.0",
"socket.io": "^4.7.5",
"socketio-file-upload": "^0.7.0",
"source-map-support": "^0.5.19",
Expand Down
Loading