@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
JavaScript
"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);