@opra/common
Version:
Opra common package
346 lines (345 loc) • 12.8 kB
JavaScript
import hashObject from 'object-hash';
import { asMutable } from 'ts-gems';
import { validator, vg } from 'valgen';
import { parseFieldsProjection, ResponsiveMap, } from '../../helpers/index.js';
import { ArrayType } from './array-type.js';
import { DataType } from './data-type.js';
export const FIELD_PATH_PATTERN = /^([+-])?([a-z$_][\w.]*)$/i;
/**
*
* @constructor
*/
export const ComplexTypeBase = function (...args) {
if (!this)
throw new TypeError('"this" should be passed to call class constructor');
// Constructor
const [owner, initArgs, context] = args;
DataType.call(this, owner, initArgs, context);
const _this = asMutable(this);
_this._fields = new ResponsiveMap();
};
/**
*
*/
class ComplexTypeBaseClass extends DataType {
ctor;
additionalFields;
keyField;
fieldCount(scope) {
if (scope === '*')
return this._fields.size;
let count = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const i of this.fields(scope))
count++;
return count;
}
fieldEntries(scope) {
let iterator = this._fields.entries();
if (scope === '*')
return iterator;
let r;
return {
next() {
while (iterator) {
r = iterator.next();
if (r.done)
break;
if (r.value && r.value[1].inScope(scope))
break;
}
if (r.done)
return { done: r.done, value: undefined };
return {
done: r.done,
value: [r.value[0], r.value[1].forScope(scope)],
};
},
return(value) {
iterator = undefined;
return { done: true, value };
},
[Symbol.iterator]() {
return this;
},
};
}
fields(scope) {
let iterator = this.fieldEntries(scope);
let r;
return {
next() {
if (!iterator)
return { done: true, value: undefined };
r = iterator.next();
return { done: r.done, value: r.value?.[1] };
},
return(value) {
iterator = undefined;
return { done: true, value };
},
[Symbol.iterator]() {
return this;
},
};
}
fieldNames(scope) {
if (scope === '*')
return this._fields.keys();
let iterator = this.fieldEntries(scope);
let r;
return {
next() {
if (!iterator)
return { done: true, value: undefined };
r = iterator.next();
return { done: r.done, value: r.value?.[0] };
},
return(value) {
iterator = undefined;
return { done: true, value };
},
[Symbol.iterator]() {
return this;
},
};
}
/**
*
*/
findField(nameOrPath, scope) {
if (nameOrPath.includes('.')) {
const fieldPath = this.parseFieldPath(nameOrPath, { scope });
if (fieldPath.length === 0)
throw new Error(`Field "${nameOrPath}" does not exist in scope "${scope}"`);
const lastItem = fieldPath.pop();
return lastItem?.field;
}
const field = this._fields.get(nameOrPath);
if (field && field.inScope(scope))
return field.forScope(scope);
}
/**
*
*/
getField(nameOrPath, scope) {
const field = this.findField(nameOrPath, '*');
if (field && !field.inScope(scope))
throw new Error(`Field "${nameOrPath}" does not exist in scope "${scope || 'null'}"`);
if (!field) {
throw new Error(`Field (${nameOrPath}) does not exist`);
}
return field.forScope(scope);
}
/**
*
*/
parseFieldPath(fieldPath, options) {
let dataType = this;
let field;
const arr = fieldPath.split('.');
const len = arr.length;
const out = [];
const objectType = this.owner.node.getDataType('object');
const allowSigns = options?.allowSigns;
const getStrPath = () => out.map(x => x.fieldName).join('.');
for (let i = 0; i < len; i++) {
const item = {
fieldName: arr[i],
dataType: objectType,
};
out.push(item);
const m = FIELD_PATH_PATTERN.exec(arr[i]);
if (!m)
throw new TypeError(`Invalid field name (${getStrPath()})`);
if (m[1]) {
if ((i === 0 && allowSigns === 'first') || allowSigns === 'each')
item.sign = m[1];
item.fieldName = m[2];
}
if (dataType) {
if (dataType instanceof ComplexTypeBase) {
field = dataType.findField(item.fieldName, options?.scope);
if (field) {
item.fieldName = field.name;
item.field = field;
item.dataType = field.type;
dataType = field.type;
continue;
}
if (dataType.additionalFields?.[0] === true) {
item.additionalField = true;
item.dataType = objectType;
dataType = undefined;
continue;
}
if (dataType.additionalFields?.[0] === 'type' &&
dataType.additionalFields?.[1] instanceof DataType) {
item.additionalField = true;
item.dataType = dataType.additionalFields[1];
dataType = dataType.additionalFields[1];
continue;
}
throw new Error(`Unknown field (${out.map(x => x.fieldName).join('.')})`);
}
throw new TypeError(`"${out.map(x => x.fieldName).join('.')}" field is not a complex type and has no child fields`);
}
item.additionalField = true;
item.dataType = objectType;
}
return out;
}
/**
*
*/
normalizeFieldPath(fieldPath, options) {
return this.parseFieldPath(fieldPath, options)
.map(x => (x.sign || '') + x.fieldName)
.join('.');
}
/**
*
*/
generateCodec(codec, options) {
const context = options?.cache
? options
: {
...options,
projection: Array.isArray(options?.projection)
? parseFieldsProjection(options.projection)
: options?.projection,
currentPath: '',
};
const schema = this._generateSchema(codec, context);
let additionalFields;
if (this.additionalFields instanceof DataType) {
additionalFields = this.additionalFields.generateCodec(codec, options);
}
else if (typeof this.additionalFields === 'boolean')
additionalFields = this.additionalFields;
else if (Array.isArray(this.additionalFields)) {
if (this.additionalFields.length < 2)
additionalFields = 'error';
else {
const message = additionalFields[1];
additionalFields = validator((input, ctx, _this) => ctx.fail(_this, message, input));
}
}
const fn = vg.isObject(schema, {
ctor: this.name === 'object' ? Object : this.ctor,
additionalFields,
name: this.name,
coerce: true,
caseInSensitive: options?.caseInSensitive,
onFail: options?.onFail,
});
if (context.level === 0 && context.forwardCallbacks?.size) {
for (const cb of context.forwardCallbacks) {
cb();
}
}
return fn;
}
_generateSchema(codec, context) {
context.fieldCache = context.fieldCache || new Map();
context.level = context.level || 0;
context.forwardCallbacks = context.forwardCallbacks || new Set();
const schema = {};
const { currentPath, projection } = context;
const pickList = !!(projection && Object.values(projection).find(p => !p.sign));
// Process fields
let fieldName;
for (const field of this.fields('*')) {
if (
/** Ignore field if required scope(s) do not match field scopes */
!field.inScope(context.scope) ||
(!(context.keepKeyFields && this.keyField) &&
/** Ignore field if readonly and ignoreReadonlyFields option true */
((context.ignoreReadonlyFields && field.readonly) ||
/** Ignore field if writeonly and ignoreWriteonlyFields option true */
(context.ignoreWriteonlyFields && field.writeonly)))) {
schema[field.name] = vg.isUndefined({ coerce: true });
continue;
}
fieldName = field.name;
let p;
if (projection !== '*') {
p = projection?.[fieldName.toLowerCase()];
if (
/** Ignore if field is omitted */
p?.sign === '-' ||
/** Ignore if default fields ignored and field is not in projection */
(pickList && !p) ||
/** Ignore if default fields enabled and fields is exclusive */
(!pickList && field.exclusive && !p)) {
schema[field.name] = vg.isUndefined({ coerce: true });
continue;
}
}
const subProjection = typeof projection === 'object'
? projection[fieldName]?.projection || '*'
: projection;
let cacheItem = context.fieldCache.get(field);
const cacheKey = typeof subProjection === 'string'
? subProjection
: hashObject(subProjection || {});
if (!cacheItem) {
cacheItem = {};
context.fieldCache.set(field, cacheItem);
}
let fn = cacheItem[cacheKey];
/** If in progress (circular) */
if (fn === null) {
// Temporary set any
fn = vg.isAny();
context.forwardCallbacks.add(() => {
fn = cacheItem[cacheKey];
schema[fieldName] =
context.partial || !field.required
? vg.optional(fn)
: vg.required(fn);
});
}
else if (!fn) {
const defaultGenerator = () => {
cacheItem[cacheKey] = null;
const xfn = this._generateFieldCodec(codec, field, {
...context,
partial: context.partial === 'deep' ? context.partial : undefined,
projection: subProjection,
currentPath: currentPath + (currentPath ? '.' : '') + fieldName,
});
cacheItem[cacheKey] = xfn;
return xfn;
};
if (context.fieldHook)
fn = context.fieldHook(field, context.currentPath, defaultGenerator);
else
fn = defaultGenerator();
}
schema[fieldName] =
context.partial || !(field.required || fn.id === 'required')
? vg.optional(fn)
: fn.id === 'required'
? fn
: vg.required(fn);
}
if (context.allowPatchOperators) {
schema._$pull = vg.optional(vg.isAny());
schema._$push = vg.optional(vg.isAny());
}
return schema;
}
_generateFieldCodec(codec, field, context) {
let fn = field.generateCodec(codec, {
...context,
level: context.level + 1,
});
if (field.fixed)
fn = vg.isEqual(field.fixed);
if (field.isArray && !(field.type instanceof ArrayType))
fn = vg.isArray(fn);
return fn;
}
}
ComplexTypeBase.prototype = ComplexTypeBaseClass.prototype;