UNPKG

domain-objects

Version:

A simple, convenient way to represent domain objects, leverage domain knowledge, and add runtime validation in your code base.

120 lines 7.24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.omitReadonly = void 0; const helpful_errors_1 = require("helpful-errors"); const type_fns_1 = require("type-fns"); const assertDomainObjectIsSafeToManipulate_1 = require("../constraints/assertDomainObjectIsSafeToManipulate"); const isOfDomainObject_1 = require("../instantiation/inherit/isOfDomainObject"); const getReadonlyKeys_1 = require("./getReadonlyKeys"); /** * splits a set of readonly keys into * - direct: keys to drop on this object (no dot in the key) * - nested: dot-path groups keyed by their head segment, with the rest of the path as the value * * for example, * - input = ['host', 'network.interface.privateIp', 'network.subnet'] * - direct = ['host'] * - nested = { network: ['interface.privateIp', 'subnet'] } */ const splitReadonlyPaths = (keys) => keys.reduce((summary, key) => { const dotIndex = key.indexOf('.'); // no dot => direct readonly key on this object if (dotIndex === -1) return { ...summary, direct: [...summary.direct, key] }; // dot => nested path; group by head segment, keep the rest (tail) of the path const head = key.slice(0, dotIndex); const tail = key.slice(dotIndex + 1); return { ...summary, nested: { ...summary.nested, [head]: [...(summary.nested[head] ?? []), tail], }, }; }, { direct: [], nested: {} }); /** * applies readonly omission to a single property value * - domain object => recurse, and thread any nested readonly paths declared for this value * - array => recurse on each item (nested readonly applies to every element, per the dot-path contract) * - otherwise => terminal value; if a nested path was expected to descend further, fail-fast (unless the value is absent) */ const recursivelyOmitReadonlyFromObjectValue = (thisValue, nestedPaths) => { // handle a directly nested domain object if ((0, isOfDomainObject_1.isOfDomainObject)(thisValue)) return omitReadonlyDeep(thisValue, nestedPaths); // eslint-disable-line @typescript-eslint/no-use-before-define // handle an array (one level deep); nested readonly applies to every element if (Array.isArray(thisValue)) return thisValue.map((item) => recursivelyOmitReadonlyFromObjectValue(item, nestedPaths)); // no further nested paths => no nested keys to omit, return it if (nestedPaths.length === 0) return thisValue; // a nested path expects to descend further, but the value is null => legitimate absent subtree, return it if (thisValue === null) return thisValue; // a nested path expects to descend further, but the value is absent or not a domain object => misconfiguration, fail-fast // (a nested readonly path must traverse declared `static nested` domain objects so the path can be followed) throw new helpful_errors_1.UnexpectedCodePathError('omitReadonly: a nested readonly path expected a domain object to descend into, but the value is absent or not a domain object. to fix: declare each sub-object on the path in `static nested` (and ensure it is present), e.g. `public static nested = { network: AwsNetwork }`. see the "Nested Readonly Properties" section of the readme for the full pattern.', { thisValue, nestedPaths }); }; /** * omits readonly values from a domain object instance, and passes any inherited nested readonly paths down */ const omitReadonlyDeep = (obj, nestedPathsInherited) => { // make sure that its safe to manipulate (0, assertDomainObjectIsSafeToManipulate_1.assertDomainObjectIsSafeToManipulate)(obj); // combine this object's own readonly keys (metadata + explicit readonly, incl. its own dot-paths) with any inherited nested paths const ownReadonlyKeys = (0, getReadonlyKeys_1.getReadonlyKeys)(obj, { nameOfFunctionNeededFor: 'omitReadonly', }); const { direct, nested } = splitReadonlyPaths([ ...ownReadonlyKeys, ...nestedPathsInherited, ]); // fail-fast if a nested readonly path references a property that is not present on this object (misconfigured path) const headsUnmatched = Object.keys(nested).filter((head) => !Object.keys(obj).includes(head)); if (headsUnmatched.length) throw new helpful_errors_1.UnexpectedCodePathError('omitReadonly: a nested readonly path references a property that is not present on the object. check the dot-path against the object shape and `static nested`.', { headsUnmatched, objectKeys: Object.keys(obj), nestedPaths: nested }); // recurse each property, and pass down any nested readonly paths that apply to that key const objectWithEachValueRecursed = Object.entries(obj).reduce((summary, [thisKey, thisValue]) => ({ ...summary, [thisKey]: recursivelyOmitReadonlyFromObjectValue(thisValue, nested[thisKey] ?? []), }), {}); // drop all of the direct readonly keys const objWithoutReadonlyValues = (0, type_fns_1.omit)(objectWithEachValueRecursed, direct); // reconstruct the instantiated object const Constructor = obj.constructor; // https://stackoverflow.com/a/61444747/3068233 return new Constructor(objWithoutReadonlyValues); }; /** * omits all readonly values on a domain object * * relevance: * - often when submitting user-settable values, readonly values should be omitted * - this provides an easy way to omit both metadata and explicit readonly properties * * features: * - utilizes the `.metadata` property to identify metadata keys (applicable to all domain objects) * - utilizes the `.readonly` property to identify explicit readonly keys (applicable to DomainEntity only) * - supports nested readonly via dot-path keys (e.g. `'network.interface.privateIp'`), declared from the entity grain * - recursive, applies omission deeply * * note: * - both metadata and readonly are set by the persistence layer * - metadata is a special subset of readonly: describes the persistence of the object (not the object itself) * - readonly (non-metadata) describes intrinsic attributes of the object that the persistence layer sets * - readonly (non-metadata) only applies to DomainEntity, due to their nature * - a nested readonly key uses dot-path notation and is declared on the entity (the literal it points into is stateless and reusable, so it cannot self-declare readonly) * - a nested readonly path applies to every element when it traverses an array of nested objects * - for DomainEvent and DomainLiteral, this function behaves identically to omitMetadata */ const omitReadonly = (obj) => { // handle arrays if (Array.isArray(obj)) return recursivelyOmitReadonlyFromObjectValue(obj, []); // make sure its an instance of DomainObject if (!(0, isOfDomainObject_1.isOfDomainObject)(obj)) throw new helpful_errors_1.UnexpectedCodePathError('omitReadonly called on object that is not an instance of a DomainObject. Are you sure you instantiated the object? (Related: see `DomainObject.nested`)', { obj }); // omit readonly values, and pass nested readonly paths through the recursion return omitReadonlyDeep(obj, []); }; exports.omitReadonly = omitReadonly; //# sourceMappingURL=omitReadonly.js.map