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