UNPKG

@darlean/valueobjects

Version:

Library for DDD-like value objects that can be validated, serialized and represented in other programming languages as native objects with native naming.

438 lines (437 loc) 16.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.sequencevalue = exports.sequencevalidation = exports.ensureSequenceDefForConstructor = exports.SequenceValue = void 0; const canonical_1 = require("@darlean/canonical"); const base_1 = require("./base"); const valueobject_1 = require("./valueobject"); const ELEM_CLASS = 'elem-class'; class SequenceValue extends base_1.Value { static required() { return { required: true, clazz: this }; } static optional() { return { required: false, clazz: this }; } /** * Creates a new sequence value from an array of values. */ static from(value) { const options = { value }; return (0, base_1.constructValue)(this, options); } static fromCanonical(value, options) { const valueoptions = { canonical: value, cacheCanonical: options?.cacheCanonical }; return Reflect.construct(this, [valueoptions]); } /** * Creates a new sequence value by copying a template value multiple times. */ static fillFrom(template, repeat) { const items = new Array(repeat).fill(template); const options = { value: items }; return (0, base_1.constructValue)(this, options); } /** * Creates a new sequence value by concatenating multiple arrays. */ static concatenateFrom(...arrays) { const items = []; for (const a of arrays) { if (Array.isArray(a)) { items.push(...a); } else if (a instanceof SequenceValue) { for (const item of a) { items.push(item); } } else { throw new Error('Argument is not an array or SequenceValue'); } } const options = { value: items }; return (0, base_1.constructValue)(this, options); } /** * Creates a new sequence value by mapping existing items. */ static mapFrom(source, mapFunc) { const results = []; let idx = 0; if (source instanceof SequenceValue) { for (const value of source.values()) { results.push(mapFunc(value, idx, source)); idx++; } } else { for (const value of source.values()) { results.push(mapFunc(value, idx, source)); idx++; } } const options = { value: results }; return (0, base_1.constructValue)(this, options); } /** * Creates a new sequence value by sorting the input array. * The provided sort func receives values of the element-type for this sequence value. * To please typescript, you may need to cast them explicitly to this type via `(a: MyType, b: MyType)`. */ static sortFrom(source, sortFunc) { const temp = []; if (source instanceof SequenceValue) { for (const value of source.values()) { temp.push(value); } } else { for (const value of source.values()) { temp.push(value); } } temp.sort(sortFunc); const options = { value: temp }; return (0, base_1.constructValue)(this, options); } /** * Creates a new sequence value by filtering the input array. * The provided sort func receives values of the element-type for this sequence value. * To please typescript, you may need to cast them explicitly to this type via `(value: MyType)`. */ static filterFrom(source, filterFunc) { const temp = []; if (source instanceof SequenceValue) { for (const value of source.values()) { if (filterFunc(value)) { temp.push(value); } } } else { for (const value of source.values()) { if (filterFunc(value)) { temp.push(value); } } } const options = { value: temp }; return (0, base_1.constructValue)(this, options); } /** * Creates a new sequence value by slicing the input array. */ static sliceFrom(source, start, end) { const temp = []; if (source instanceof SequenceValue) { for (const value of source.values()) { temp.push(value); } } else { for (const value of source.values()) { temp.push(value); } } const options = { value: temp.slice(start, end) }; return (0, base_1.constructValue)(this, options); } /** * Creates a new sequence value by reversing the input array. */ static reverseFrom(source) { const temp = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any if (source instanceof SequenceValue) { for (const value of source.values()) { temp.push(value); } } else { for (const value of source.values()) { temp.push(value); } } const options = { value: temp.reverse() }; return (0, base_1.constructValue)(this, options); } /** * Creates a new mapping value instance using the provided value. * @param value */ constructor(options) { super(options); let v = []; if (options.canonical) { const canonical = (0, canonical_1.toCanonical)(options.canonical); const logicalTypes = (0, base_1.checkLogicalTypes)(Object.getPrototypeOf(this)); const canonicalLogicalTypes = canonical.logicalTypes; if (!canonical.is(logicalTypes)) { throw new valueobject_1.ValidationError(`Incoming value of logical types '${canonicalLogicalTypes.join('.')}' is not compatible with '${logicalTypes.join('.')}'`); } if ((0, base_1.shouldCacheCanonical)(canonical, logicalTypes, options?.cacheCanonical)) { this._canonical = canonical; } v = this._fromCanonical(canonical, options); } else { for (const elem of options.value) { v.push(elem); } } const msgs = []; const validated = this._validate(v, (msg) => msgs.push(msg)); if (msgs.length > 0) { throw new valueobject_1.ValidationError(msgs.join('; ')); } this._items = validated ?? v; } get(index) { return this._checkItems()[index]; } values() { return this._checkItems().values(); } _peekCanonicalRepresentation() { if (this._canonical) { return this._canonical; } this._canonical = this._toCanonical(this._checkItems(), this._logicalTypes); return this._canonical; } get _logicalTypes() { return (Reflect.getOwnMetadata(base_1.LOGICAL_TYPES, Object.getPrototypeOf(this)) ?? []); } equals(other) { if (!(0, canonical_1.isCanonicalLike)(other)) { return false; } return this._peekCanonicalRepresentation().equals(other); } get length() { return this._checkItems().length; } *[Symbol.iterator]() { const len = this._checkItems().length; for (let idx = 0; idx < len; idx++) { yield this._checkItems()[idx]; } } /** * Extracts the current elements. After that, the sequence value should not be * used anymore and throws errors when you try to access values. */ extractElements() { const items = this._checkItems(); this._items = undefined; return items; } /** * Maps all items to arbitrary other values by means of a mapping function. * @param func Function that maps an input value to an output value * @returns A native array (not a value object) of mapped items */ map(func) { const results = new Array(this.length); for (let idx = 0; idx < this.length; idx++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const item = this.get(idx); results[idx] = func(item, idx, this); } return results; } /** * Returns a native array with filtered items from this array value. * @param func The function that returns true for values that must be included in the resulting array value. * @returns A new mapped array value. */ filter(func) { const results = []; for (let idx = 0; idx < this.length; idx++) { const item = this.get(idx); if (item && func(item)) { results.push(item); } } return results; } /** * Returns an item based on a match function. * @param func The function that returns true for the value that must be found. * @returns The found value or undefined. */ find(func) { for (let idx = 0; idx < this.length; idx++) { const item = this.get(idx); if (item && func(item)) { return item; } } } /** * Returns the index of an item based on a match function. * @param func The function that returns true for the value that must be found. * @returns The index of the found value or -1. */ findIndex(func) { for (let idx = 0; idx < this.length; idx++) { const item = this.get(idx); if (item && func(item)) { return idx; } } return -1; } /** * Returns the index of value, or -1 when not present. Values are compared using `canonical.equals`. */ indexOf(value) { for (let idx = 0; idx < this.length; idx++) { const item = this.get(idx); if (item && (0, canonical_1.equals)(value, item)) { return idx; } } return -1; } /** * Returns whether value is present. Values are compared using `canonical.equals`. */ includes(value) { for (let idx = 0; idx < this.length; idx++) { const item = this.get(idx); if (item && (0, canonical_1.equals)(value, item)) { return true; } } return false; } /** * Returns a native array with reverted items from this array value. * @returns A new mapped array value. */ reverse() { const len = this.length; const results = new Array(len); for (let idx = 0; idx < len; idx++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion results[len - idx - 1] = this.get(idx); } return results; } /** * Returns a native array with sliced items from this array value. * @param start The start index (inclusive). Default is 0. When negative, it indicates the offset from the end (-1 corresponds to the last element). * @param end The end index (exclusive). Default is input.length. When negative, it indicates the offset from the end (-1 corresponds to the last element). * @returns A new sliced array value. */ slice(start, end) { return this._checkItems().slice(start, end); } reduce(callbackfn, ...initialValue) { if (initialValue.length === 0) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return this._checkItems().reduce(callbackfn); } // eslint-disable-next-line @typescript-eslint/no-explicit-any return this._checkItems().reduce(callbackfn, initialValue[0]); } _fromCanonical(canonical, options) { const itemClazz = Reflect.getOwnMetadata(ELEM_CLASS, Object.getPrototypeOf(this)); const result = []; let item = canonical.firstSequenceItem; while (item) { const itemCan = (0, canonical_1.toCanonical)(item.value); const value = (0, base_1.constructValue)((0, base_1.toValueClass)(itemClazz), { cacheCanonical: options?.cacheCanonical, canonical: itemCan }); result.push(value); item = item.next(); } return result; } _toCanonical(value, logicalTypes) { return canonical_1.ArrayCanonical.from(value.map((x) => x._peekCanonicalRepresentation()), logicalTypes); } _validate(v, fail) { // First, validate all slots for proper type and presence. const itemClazzLike = Reflect.getOwnMetadata(ELEM_CLASS, Object.getPrototypeOf(this)); if (!itemClazzLike) { throw new Error(`Instance of sequence class '${this.constructor.name}' does not have an item type defined, possibly because no '@arrayvalue()' class decorator is present.`); } const itemClazz = (0, base_1.toValueClass)(itemClazzLike); let ok = true; const expectedLogicalTypes = (0, base_1.toValueClass)(itemClazz).logicalTypes; for (const [idx, value] of v.entries()) { // This checks not only checks the proper class types (which may be too strict?), it also catches the case in which // the input is not a Value at all (but, for example, a ICanonical). if (!(value instanceof itemClazz)) { fail(`Item '${idx}' with class '${Object.getPrototypeOf(value).constructor.name}' is not an instance of '${itemClazz.name}'`); ok = false; continue; } const valueLogicalTypes = value._logicalTypes; if (!(0, base_1.aExtendsB)(valueLogicalTypes, expectedLogicalTypes)) { fail(`Value '${idx}' with logical types '${valueLogicalTypes.join('.')}' is not compatible with expected logical types '${expectedLogicalTypes.join('.')}'`); ok = false; continue; } } if (!ok) { return; } // Then, use this prevalidated map as input to custom validators for the struct. It makes little // sense to run such a validator on input that is invalid. That is why we do this after validation // of the individual slots. const validators = Reflect.getOwnMetadata(base_1.VALIDATORS, Object.getPrototypeOf(this)); if (validators) { let failed = false; for (const validator of validators) { validator(v, (msg) => { fail(msg); failed = true; }); if (failed) { break; } } } } _deriveCanonicalRepresentation() { const items = this._checkItems(); return canonical_1.ArrayCanonical.from(items.map((x) => x._peekCanonicalRepresentation()), this._logicalTypes); } _checkItems() { if (this._items === undefined) { throw new Error(`Not allowed to access unfrozen structure`); } return this._items; } } exports.SequenceValue = SequenceValue; function ensureSequenceDefForConstructor( // eslint-disable-next-line @typescript-eslint/ban-types constructor, elemClass) { const prototype = constructor.prototype; let itemClazz = Reflect.getOwnMetadata(ELEM_CLASS, prototype); if (!itemClazz) { const parentItemClazz = Reflect.getMetadata(ELEM_CLASS, prototype); itemClazz = elemClass ?? parentItemClazz; Reflect.defineMetadata(ELEM_CLASS, itemClazz, prototype); } } exports.ensureSequenceDefForConstructor = ensureSequenceDefForConstructor; function sequencevalidation(validator, description) { return (0, base_1.validation)(validator, description); } exports.sequencevalidation = sequencevalidation; function sequencevalue(elemClass, logicalName) { // eslint-disable-next-line @typescript-eslint/ban-types return function (constructor) { (0, base_1.valueobject)(logicalName)(constructor); ensureSequenceDefForConstructor(constructor, elemClass); }; } exports.sequencevalue = sequencevalue; sequencevalue(base_1.Value, '')(SequenceValue);