UNPKG

sdj-esm

Version:
355 lines (310 loc) 12.4 kB
/* Copyright (c) 2023-2024 Will Rudolph <@willrudolph.com> This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type {CoreSD, DataJI, EntityJI, GenKeyStore, JIValue, NumKeyStore, SdKeyProps} from "../core/interfaces.js"; import {ESDJ_CLASS, ESDJ_LIMIT, GRAPH_ID, SYS_RESERVED} from "../core/statics.js"; import {genSdKeyProps} from "../util/immutables.js"; import {isArrayWithLen} from "../core/validators.js"; import {restrictCoreSD, restrictLimiter, restrictToAllowedKeys} from "../core/restrict.js"; import type {IDescriptionSdj, IEntitySdj, IItemSdj} from "./class-interfaces.js"; import { each, isBoolean, isEmpty, isEqual, isFunction, isNull, isNumber, isObject, isString, isUndefined, uniq } from "lodash-es"; import {validIntArray, validSDKey} from "../core/sdj-types.js"; import type {numArrayOrUnDef} from "../core/internal.js"; /* Entities provide the connective information between the data represented in the SDJ. sdItems* Items that are assigned to entity, unique pos-int values only extendIds* Entities extended by current entity parentIds* Entities that are allowed to contain current entity childIds* Entities that are allowed to be children of current entity limiter 4 Avail: (ESDJ_LIMIT) .REQ Entity is required by parent .REQ_HIDE Entity is required by parent hidden from user (for only one entity) .ONE_NONE Entity is singular on parent or not-present .REQ_ONE At least one Entity is required by parent .KEY_IDX Child Entities of singular type sdProps Optional properties for entities and used by lexicons or manually dataInfo Boolean switch for if Entity stores creation/modification information (sdInfo object) *All pos-int number[] are uniq'd and sorted before operations Extending Rules - - sdProps/limiter and all number[] are considered static during runtime and should be re-validated if changed - auto-inherent all sdItems from lower entities, auto-remove duplicate items - Does not affect childIds or parentIds, which are unique per Entity - sdProps are chained assigned based on entity/order - genJI sdProps will show final values - limiter value matches last extended item if not provided Note: in Sdj "$" functions for deliberate class-only implementations which don't appear on IEntitySdj and should only be used by classes/files that implement "new SdjEntity" in any way. */ export class SdjEntity implements CoreSD, IEntitySdj { private _sdId: number; private _sdKey: string; private _parentIds: number[]; private _sdItems: number[]; private _extendIds: number[] | undefined; private _childIds: number[] | undefined; private _dataInfo: boolean | undefined; private _limiter: ESDJ_LIMIT; private _sdProps: SdKeyProps | undefined; private _description: IDescriptionSdj | undefined; private _childRefs?: NumKeyStore<IEntitySdj>; private _itemRefs?: GenKeyStore<IItemSdj>; constructor(inEnt: EntityJI, description?: IDescriptionSdj | undefined) { this._description = this.confirmDescript(description); if (this._description) { this._description.host.checkClassInst(inEnt, ESDJ_CLASS.ENTITY, false); } SdjEntity.VerifyJI(inEnt); this.graphZeroLockout(inEnt); this._sdId = inEnt.sdId; this._sdKey = inEnt.sdKey; if (isArrayWithLen(inEnt.extendIds)) { this._extendIds = uniq([...<number[]>inEnt.extendIds]); } if (isArrayWithLen(inEnt.parentIds)) { this._parentIds = uniq([...<number[]>inEnt.parentIds]); } else { this._parentIds = [0]; } if (isArrayWithLen(inEnt.childIds)) { this._childIds = uniq([...<number[]>inEnt.childIds]); } if (isArrayWithLen(inEnt.sdItems)) { const inEntItems =[...<number[]>inEnt.sdItems]; this._sdItems = uniq([0, 1].concat(inEntItems)); } else { this._sdItems = [0, 1]; } if (inEnt.dataInfo) { this._dataInfo = inEnt.dataInfo; } if (inEnt.sdProps) { this._sdProps = genSdKeyProps(inEnt.sdProps); } this._limiter = inEnt.limiter || ESDJ_LIMIT.NONE; } get sdKey() { return this._sdKey; } get sdId() { return this._sdId; } get parentIds(): number[] { return this._parentIds; } get sdItems(): number[] { return this._sdItems; } get extendIds(): number[] | undefined { return this._extendIds; } get childIds(): number[] | undefined { return this._childIds; } get childRefs(): NumKeyStore<IEntitySdj> { return (this._childRefs) ? this._childRefs : {}; } get itemRefs(): GenKeyStore<IItemSdj> { return (this._itemRefs) ? this._itemRefs : {}; } get description(): IDescriptionSdj { if (!this._description) { throw new ReferenceError("[SDJ] Entity has no description;"); } return this._description; } get sdProps(): SdKeyProps | undefined { return this._sdProps; } get limiter(): ESDJ_LIMIT { return this._limiter; } get dataInfo(): boolean | undefined{ return this._dataInfo; } set description(inDesc: IDescriptionSdj | undefined) { if (!this._description) { this._description = this.confirmDescript(inDesc); if (this._description) { this.$refreshRefs(); } else { throw new Error("[SDJ] Incorrect or missing description/host;"); } } else { throw new Error("[SDJ] an Entity item is prevented from having its description overwritten;"); } } validStruct(dataSdj: DataJI, parentRef: DataJI | undefined, strict = false): boolean { if (!this._description) { return false; } return this._description.host.searchMgr.validStruct(this, dataSdj, parentRef, strict); } validData(dataSdj: DataJI, strict = false): boolean { if (!this._description) { return false; } return this._description.host.searchMgr.validData(this, dataSdj, strict); } genJI(): EntityJI { const rtnEntJI: EntityJI = { sdId: this._sdId, sdKey: this._sdKey, }; // parents will always be at least [0] and should only be eliminated when === [0] if (this._parentIds.length > 1 || this._parentIds[0] !== 0) { rtnEntJI.parentIds = [...this._parentIds]; } each(this._sdItems, (itemId: number) => { if (itemId !== 0 && itemId !== 1) { if (!rtnEntJI.sdItems) { rtnEntJI.sdItems = []; } rtnEntJI.sdItems.push(itemId); } }); if (isArrayWithLen(this._extendIds)) { rtnEntJI.extendIds = [...<number[]>this._extendIds]; } if (isArrayWithLen(this._childIds)) { rtnEntJI.childIds = [...<number[]>this._childIds]; } if (this._dataInfo) { rtnEntJI.dataInfo = true; } if (this.sdProps) { let rtnProps: SdKeyProps | undefined; if (this._description) { rtnProps = this._description.calcSdKeyProps(this); if (!isEmpty(rtnProps)) { rtnEntJI.sdProps = <SdKeyProps>rtnProps; } } else { rtnEntJI.sdProps = genSdKeyProps(this.sdProps); } } if (this._limiter !== ESDJ_LIMIT.NONE) { rtnEntJI.limiter = this._limiter; } return rtnEntJI; } $refreshRefs() { if (this._description) { this._itemRefs = {}; this._childRefs = {}; each(this._childIds, (num: number) => { const childRef = this._description?.getEntity(num); if (childRef) { this.childRefs[num] = childRef; } else { throw new Error("[SDJ] if this happens something else was missed presence of entity refs should be confirmed;"); } }); each(this._description.getItemsByEntity(this._sdId), (sdItem: IItemSdj) => { this.itemRefs[sdItem.sdKey] = sdItem; }); } } private confirmDescript(inDesc: IDescriptionSdj | undefined): IDescriptionSdj | undefined { let rtnVal = false; if (!isNull(inDesc) && !isUndefined(inDesc)) { if (inDesc.host?.checkClassInst && isFunction(inDesc.host.checkClassInst)) { inDesc.host.checkClassInst(inDesc, ESDJ_CLASS.DESCRIPTION, true); rtnVal = true; } else if (!inDesc.host && isObject(inDesc)) { throw new Error("[SDJ] Improper Entity init attempt use IDescriptionSdj;"); } } return (rtnVal) ? inDesc : undefined; } private graphZeroLockout(inEnt: EntityJI) { if (inEnt.sdId === 0 || inEnt.sdKey === GRAPH_ID) { if (this._description) { // throw nothing for now } else { throw new Error("[SDJ] Entity 0 is reserved for system requires SdjDescription to build;"); } } } static VerifyJI(inEnt: EntityJI) { restrictCoreSD(inEnt); if ((inEnt.sdId === 0 && inEnt?.sdKey !== GRAPH_ID) || inEnt.sdId < 0) { throw new Error(`[SDJ] Entity: '${inEnt.sdKey}' jixId: ${inEnt.sdId} <= 0;`); } else if (inEnt.sdId === 0 && inEnt.sdKey === GRAPH_ID) { return; } SdjEntity.checkArrays(inEnt); if (inEnt.sdProps && !isObject(inEnt.sdProps)) { throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' invalid keyValues;`); } else if (inEnt.sdProps) { each(inEnt.sdProps, (jValue: JIValue, key: string) => { if (SYS_RESERVED.indexOf(key) !== -1 || !validSDKey(key)) { throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' has an illegal keyName: ${key};`); } else if (!(isString(jValue) || isNumber(jValue) || isBoolean(jValue))) { throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' key '${key}' has illegal value, not string|number|boolean;`); } }); } if (inEnt.limiter && !restrictLimiter(inEnt.limiter)) { throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' has unrecognized limiter;`); } else if (inEnt.limiter && inEnt.limiter === ESDJ_LIMIT.KEY_IDX) { if (inEnt.parentIds && !isEqual(inEnt.parentIds, [0])) { throw new TypeError("[SDJ] sdIndexed Entities can only exist on root;"); } if (!inEnt.childIds) { throw new TypeError("[SDJ] sdIndexed Entities are required to have children;"); } } if (inEnt.dataInfo && !isBoolean(inEnt.dataInfo)) { throw new TypeError("[SDJ] Entity dataInfo value boolean only if present;"); } restrictToAllowedKeys("EntityJI: "+ inEnt.sdKey, ["sdKey", "sdId", "sdItems", "extendIds", "parentIds", "childIds", "limiter", "sdProps", "dataInfo"], inEnt); } private static checkArrays(inEnt: EntityJI) { const nameRefs = ["childIds", "parentIds", "extendIds", "childIds", "sdItems"], valueRefArray: numArrayOrUnDef[] = [inEnt.childIds, inEnt.parentIds, inEnt.extendIds, inEnt.childIds, inEnt.sdItems]; each(nameRefs, (keyId, idx) => { const valueRef = valueRefArray[idx]; if (isArrayWithLen(valueRef)) { if (!validIntArray(valueRef)) { throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' invalid '${keyId}' array;`); } else { switch(keyId) { case "childIds": if (valueRef && valueRef.indexOf(0) !== -1) { throw new Error(`[SDJ] Entity: '${inEnt.sdKey}' GraphZero[0] is not allowed to be a child of an entity;`); } break; case "extendIds": if (valueRef && valueRef.indexOf(0) !== -1) { throw new Error(`[SDJ] Entity '${inEnt.sdKey}' cannot extend graphZero [0];`); } else if (valueRef && valueRef.indexOf(inEnt.sdId) !== -1) { throw new Error(`[SDJ] Entity: '${inEnt.sdKey}' cannot extend itself;`); } break; default: // no default; } } } if (keyId === "sdItems" && !inEnt.sdItems && !inEnt.extendIds && !inEnt.childIds) { throw new Error(`[SDJ] Entity '${inEnt.sdKey}' has no items, does not extend, & has no children;`); } }); } }