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.

210 lines (209 loc) 8.42 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.mappingvalue = exports.mappingvalidation = exports.ensureMappingDefForConstructor = exports.MappingValue = void 0; const canonical_1 = require("@darlean/canonical"); const base_1 = require("./base"); const valueobject_1 = require("./valueobject"); const ELEM_CLASS = 'elem-class'; class MappingValue extends base_1.Value { static required() { return { required: true, clazz: this }; } static optional() { return { required: false, clazz: this }; } /** * Creates a new mapping value from a map of values. */ static from(value) { const options = { value }; return (0, base_1.constructValue)(this, options); } static fromCanonical(value) { const options = { canonical: value }; return Reflect.construct(this, [options]); } constructor(options) { super(options); let v = new Map(); 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 if (options.value instanceof Map) { for (const [name, value] of options.value.entries()) { if (value === undefined) { continue; } v.set(name, value); } } else { for (const [name, value] of Object.entries(options.value)) { if (value === undefined) { continue; } v.set(name, value); } } const msgs = []; const validated = this._validate(v, (msg) => msgs.push(msg)); if (msgs.length > 0) { throw new valueobject_1.ValidationError(msgs.join('; ')); } this._slots = validated ?? v; } _peekCanonicalRepresentation() { if (this._canonical) { return this._canonical; } this._canonical = this._toCanonical(this._checkSlots(), this._logicalTypes); return this._canonical; } equals(other) { if (!(0, canonical_1.isCanonicalLike)(other)) { return false; } return this._peekCanonicalRepresentation().equals(other); } keys() { return this._checkSlots().keys(); } values() { return this._checkSlots().values(); } entries() { return this._checkSlots().entries(); } /** * Extracts the current slots and their values. */ _extractSlots() { const slots = this._checkSlots(); this._slots = undefined; return slots; } get size() { return this._checkSlots().size; } get _logicalTypes() { return (Reflect.getOwnMetadata(base_1.LOGICAL_TYPES, Object.getPrototypeOf(this)) ?? []); } _deriveCanonicalRepresentation() { const slots = this._checkSlots(); return canonical_1.MapCanonical.from(slots, this._logicalTypes); } get(slot) { return this._checkSlots().get(slot); } /** * 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._checkSlots(); this._slots = undefined; return items; } _fromCanonical(canonical, options) { const itemClazz = Reflect.getOwnMetadata(ELEM_CLASS, Object.getPrototypeOf(this)); const result = new Map(); let entry = canonical.firstMappingEntry; while (entry) { const entryCan = (0, canonical_1.toCanonical)(entry.value); const value = (0, base_1.constructValue)((0, base_1.toValueClass)(itemClazz), { canonical: entryCan, cacheCanonical: options?.cacheCanonical }); result.set(entry.key, value); entry = entry.next(); } return result; } _toCanonical(value, logicalTypes) { return canonical_1.MapCanonical.from(value, logicalTypes); } _validate(v, fail) { // First, validate all slots for proper type and presence. const itemClazz = Reflect.getOwnMetadata(ELEM_CLASS, Object.getPrototypeOf(this)); if (!itemClazz) { throw new Error(`Instance of mapping class '${this.constructor.name}' does not have an item type defined, possibly because no '@mappingvalue()' class decorator is present.`); } let ok = true; const expectedLogicalTypes = (0, base_1.toValueClass)(itemClazz).logicalTypes; for (const [name, 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(`Value for attribute '${name}' 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 for attribute '${name}' with logical types '${this._canonical?.logicalTypes.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; } } } } _checkSlots() { if (this._slots === undefined) { throw new Error(`Not allowed to access unfrozen structure`); } return this._slots; } } exports.MappingValue = MappingValue; function ensureMappingDefForConstructor( // 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.ensureMappingDefForConstructor = ensureMappingDefForConstructor; function mappingvalidation(validator, description) { return (0, base_1.validation)(validator, description); } exports.mappingvalidation = mappingvalidation; function mappingvalue(elemClass, logicalName) { // eslint-disable-next-line @typescript-eslint/ban-types return function (constructor) { (0, base_1.valueobject)(logicalName)(constructor); ensureMappingDefForConstructor(constructor, elemClass); }; } exports.mappingvalue = mappingvalue; mappingvalue(base_1.Value, '')(MappingValue);