UNPKG

sdj-esm

Version:
439 lines (399 loc) 16.3 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 {ISdjHost, ISdjSearch} from "./global-interfaces.js"; import type {DataJI, EntitySearch, FuncStrNumVoid, ItemSearch, JIValue, NumKeyStore} from "../core/interfaces.js"; import type {IDataSdj, IDescriptionSdj, IEntitySdj, IItemSdj, IJsonSdj} from "../classes/class-interfaces.js"; import {each, find, isEqual, isNull, isString, isUndefined} from "lodash-es"; import {validIntArray} from "../core/sdj-types.js"; import {ESDJ_LIMIT, SD_IDX} from "../core/statics.js"; import {SdjDescription} from "../classes/description.js"; import {getFromCoreArray} from "../util/func.std.js"; export class SdjSearch implements ISdjSearch { log: FuncStrNumVoid; private _host: ISdjHost; constructor(private host: ISdjHost) { this._host = host; this.log = this._host.getLogFunc("SdjSearch"); } changeDataInfo(sdJson: IJsonSdj, newValue: boolean) { this.log(sdJson.sdInfo.name + ":" + newValue); } // Excepts either / or . // clears or ignores first /. dataByPath(sdJson: IJsonSdj, dataPath: string): IDataSdj | undefined { if (!dataPath || !isString(dataPath) || dataPath === "." || dataPath === "/") { return undefined; } let routePath = (dataPath.indexOf(".") !== -1) ? dataPath.replace(/\./g, "/"): dataPath, routeSplit: string[] = routePath.split("/"), parentPath: string; while(routeSplit[0] === "" || routeSplit[0] === "/") { routeSplit.shift(); } if (routeSplit.length === 0) { this.log(`Error finding sdKey: ${dataPath}`, 3); return undefined; } let rtnData: IDataSdj | undefined = find(sdJson.data, {sdKey: String(routeSplit[0])}); if (!rtnData) { this.log(`Error finding sdKey:${routeSplit[0]}`, 3); return undefined; } else if (rtnData && routeSplit.length === 1) { return rtnData; } parentPath = String(routeSplit[0]); routeSplit.shift(); while (routeSplit.length > 0) { if (rtnData) { rtnData = find(rtnData.sdChildren, {sdKey: String(routeSplit[0])}); } if (rtnData) { parentPath += ("." + routeSplit[0]); routeSplit.shift(); } else { this.log(`On ${parentPath}; Error finding ${routeSplit[0]}`, 3); return undefined; } } return rtnData; } /* searchDescriptions(search: DescriptionSearch): IDescriptionSdj[] { let rtnArray: IDescriptionSdj[] = [], singleDesc: IDescriptionSdj | undefined; // TODO: Make more robust if (search.name && isString(search.name)) { singleDesc = find(this._host.descriptions, {name: search.name}); if (singleDesc) { rtnArray.push(singleDesc); } } return rtnArray; }*/ // Both of these searches are single prop search term/item cut-out searches // Adding multiple terms will be ignored after the first is found in below order // Unclear if multiple item search should be supported // but that could be achieved by external methodologies searchEntities(sdjDescript: IDescriptionSdj, searchEnt: EntitySearch): IEntitySdj[] { // TODO: Condense/split function const asDescSdj: SdjDescription = <SdjDescription>sdjDescript, rtnAry: IEntitySdj[] = [], pushIfAvail = (ent: IEntitySdj | undefined) => { if (ent) { rtnAry.push(ent); } }; if (searchEnt) { if (searchEnt.sdId) { pushIfAvail(<IEntitySdj>getFromCoreArray(searchEnt.sdId, asDescSdj.graph)); } else if (searchEnt.sdKey) { pushIfAvail(<IEntitySdj>getFromCoreArray(searchEnt.sdKey, asDescSdj.graph)); } else if (searchEnt.limiter) { each(asDescSdj.graph, (sdjEnt: IEntitySdj) => { if (sdjEnt.limiter === searchEnt.limiter) { rtnAry.push(sdjEnt); } }); } else if (validIntArray(searchEnt.parentIds)) { each(searchEnt.parentIds, (sdId: number) => { each(asDescSdj.graph, (entRef: IEntitySdj) => { if (entRef.parentIds.indexOf(sdId) !== -1) { rtnAry.push(entRef); } }); }); } else if (validIntArray(searchEnt.extendIds)) { each(searchEnt.extendIds, (sdId: number) => { each(asDescSdj.graph, (entRef: IEntitySdj) => { if (entRef.extendIds && entRef.extendIds.indexOf(sdId) !== -1) { rtnAry.push(entRef); } }); }); } else if (validIntArray(searchEnt.sdItems)) { each(searchEnt.sdItems, (sdId: number) => { each(asDescSdj.graph, (entRef: IEntitySdj) => { if (entRef.sdItems.indexOf(sdId) !== -1) { rtnAry.push(entRef); } }); }); } else if (validIntArray(searchEnt.childIds)) { each(searchEnt.childIds, (sdId: number) => { each(asDescSdj.graph, (entRef: IEntitySdj) => { if (entRef.sdId === 0) { return; } if (entRef.childIds && entRef.childIds?.indexOf(sdId) !== -1) { rtnAry.push(entRef); } }); }); } else if (searchEnt.sdProps) { each(asDescSdj.graph, (entRef: IEntitySdj) => { if (entRef.sdId === 0) { return; } if (searchEnt.checkData) { if (entRef.sdProps && isEqual(entRef.sdProps, searchEnt.sdProps)) { rtnAry.push(entRef); } } else { let allValid = true; each(searchEnt.sdProps, (propValue, key) => { if (entRef.sdProps && entRef.sdProps[key] !== propValue) { allValid = false; return false; } return true; }); if (allValid) { rtnAry.push(entRef); } } }); } } return rtnAry; } searchItems(sdjDescript: IDescriptionSdj, searchItem: ItemSearch): IItemSdj[] { const asDescript: SdjDescription = <SdjDescription>sdjDescript, rtnAry: IItemSdj[] = [], pushIfAvail = (item: IItemSdj | undefined) => { if (item) { rtnAry.push(item); } }; if (searchItem) { if (searchItem.sdId) { pushIfAvail(<IItemSdj>getFromCoreArray(searchItem.sdId, asDescript.items)); } else if (searchItem.sdKey) { pushIfAvail(<IItemSdj>getFromCoreArray(searchItem.sdKey, asDescript.items)); } else if (searchItem.type) { each(asDescript.items, (sdjItem: IItemSdj) => { if (sdjItem.type === searchItem.type) { rtnAry.push(sdjItem); } }); } else if (searchItem.limiter) { each(asDescript.items, (sdjItem: IItemSdj) => { if (sdjItem.limiter === searchItem.limiter) { rtnAry.push(sdjItem); } }); } } return rtnAry; } dataByEntity(sdJson: IJsonSdj, searchEnt: EntitySearch, dataPath?: string): IDataSdj[] { const dataPathItem: IDataSdj | undefined = (dataPath) ? sdJson.dataByPath(dataPath) : undefined, data: IDataSdj[] = (dataPath && dataPathItem) ? [dataPathItem] : sdJson.data, foundEnts: IEntitySdj[] = sdJson.description.searchEntities(searchEnt), rtnAry: IDataSdj[] = [], subChildSearch = (children: IDataSdj[]) => { each(children, (dataObj: IDataSdj) => { const matchedEnt = find(foundEnts, {sdId: dataObj.sdId}); if (dataObj.entity && matchedEnt) { if (!searchEnt.checkData) { rtnAry.push(dataObj); } else if (searchEnt.checkData && this.checkEntityData(searchEnt, dataObj, matchedEnt)){ rtnAry.push(dataObj); } } if (dataObj.sdChildren && dataObj.sdChildren?.length > 0) { subChildSearch(dataObj.sdChildren); } }); }; if (data.length > 0) { subChildSearch(data); this.log("dataByEntity;" + foundEnts.length); } return rtnAry; } dataByItem(sdJson: IJsonSdj, searchItem: ItemSearch, dataPath?: string): IDataSdj[] { const dataPathItem: IDataSdj | undefined = (dataPath) ? sdJson.dataByPath(dataPath) : undefined, data: IDataSdj[] = (dataPath && dataPathItem) ? [dataPathItem] : sdJson.data, foundItems: IItemSdj[] = sdJson.description.searchItems(searchItem), rtnAry: IDataSdj[] = [], subChildSearch = (children: IDataSdj[]) => { each(children, (dataObj: IDataSdj) => { each(foundItems, (item: IItemSdj): boolean => { if (dataObj.entity && dataObj.entity.itemRefs[item.sdKey]) { rtnAry.push(dataObj); return false; } else { return true; } }); if (dataObj.sdChildren && dataObj.sdChildren?.length > 0) { subChildSearch(dataObj.sdChildren); } }); }; if (data.length > 0) { subChildSearch(data); this.log("dataByEntity;" + foundItems.length); } return rtnAry; } validData(entity: IEntitySdj, dataSdj: DataJI, strict = false): boolean { let rtnVal = true, errorResults: string[] = this.findDataErrors(entity, dataSdj), splits: string[]; each(errorResults, (errSptStr) => { splits = errSptStr.split(","); this.log( `SdjEntity '${entity.sdKey}' struct '${splits[0]}' is '${splits[1]}' should be type '${splits[2]}'` , 3); rtnVal = false; }); if (strict && errorResults?.length > 0) { const singleErr = errorResults.shift(); if (singleErr) { splits = singleErr.split(","); throw new Error( `SdjData key '${entity.sdKey}' validate item '${splits[0]}' is '${splits[1]}' should be type '${splits[2]}'`); } } return rtnVal; } validStruct(entity: IEntitySdj, dataSdj: DataJI, parentRef: DataJI | undefined, strict = false): boolean { let rtnVal = true, errorResults: string[] = this.findStructErrors(entity, dataSdj, parentRef), splits: string[]; each(errorResults, (errSptStr) => { splits = errSptStr.split(","); this.log( `Entity '${entity.sdKey}' with data '${splits[0]}' error '${splits[1]}' with data/ent '${splits[2]}'` , 3); rtnVal = false; }); if (strict && errorResults?.length > 0) { const singleErr = errorResults.shift(); if (singleErr) { splits = singleErr.split(","); throw new Error( `[SDJ] Entity '${entity.sdKey}' with data '${splits[0]}' error '${splits[1]}' with data/ent '${splits[2]}'`); } } return rtnVal; } /* getRefByType<Type>(sdjDescript: IDescriptionSdj, idOrKey: number | string, type: string): Type | undefined { if (isUndefined(idOrKey) || (!isString(idOrKey) && !isNumber(idOrKey))) return undefined; const searchGroup = (type === "entity") ? sdjDescript.graph : sdjDescript.items; return (isNumber(idOrKey)) ? <Type>find(searchGroup, {sdId: idOrKey}) : <Type>find(searchGroup, {sdKey: idOrKey}); }*/ private findDataErrors(entity: IEntitySdj, dataJI: DataJI): string[] { let rtnErrStrs: string[] = []; each(entity.itemRefs, (itemRef: IItemSdj) => { const key = itemRef.sdKey, possibleValue = dataJI[key], isPresent: boolean = (!isNull(possibleValue) && !isUndefined(possibleValue)), actualValue: JIValue = isPresent ? <JIValue>possibleValue : false, isRequired = (itemRef.limiter === ESDJ_LIMIT.SYS_REQ || itemRef.limiter === ESDJ_LIMIT.REQ || itemRef.limiter === ESDJ_LIMIT.REQ_HIDE), isValid = (isPresent) ? itemRef.validator.valid(actualValue) : false; if (isRequired && !isPresent) { rtnErrStrs.push(key + ",required," + itemRef.type); } else if (isRequired && isPresent && !isValid) { rtnErrStrs.push(key + ",invalid," + itemRef.type); } else if (!isRequired && isPresent && !isValid) { rtnErrStrs.push(key + ",invalid," + itemRef.type); } }); return rtnErrStrs; } private findStructErrors(entity: IEntitySdj, dataJI: DataJI, parentData: DataJI | undefined): string[] { // TODO: Condense split function if (!entity.description) { return [entity.sdKey + ",description,not available"]; } let rtnErrs: string[] = [], childPresent: NumKeyStore<number> = {}, parentEnt: IEntitySdj | undefined = (parentData) ? entity.description.getEntity(parentData.sdId) : undefined, checkIndexing: boolean = (parentEnt?.limiter === ESDJ_LIMIT.KEY_IDX); each(entity.childRefs, (childRef) => { childPresent[childRef.sdId] = 0; }); if (!parentData && entity.parentIds.indexOf(0) === -1) { rtnErrs.push(entity.sdKey + ",requires parent,parent undefined"); } else if (parentEnt && entity.parentIds.indexOf(parentEnt.sdId) === -1) { rtnErrs.push(entity.sdKey + ",entity does not allow parent," + parentEnt.sdKey); } else if (parentEnt && (!parentEnt.childIds || parentEnt.childIds.indexOf(entity.sdId) === -1)) { rtnErrs.push(parentEnt.sdKey + ",parent does not allow child," + entity.sdKey); } each(dataJI.sdChildren, (childItem: DataJI, idx: number) => { const entRef = entity.childRefs[childItem.sdId]; if (entity.childIds?.indexOf(childItem.sdId) === -1) { rtnErrs.push(entity.sdKey +",doesn't allow child," + childItem.sdId); } if (entRef) { if (entRef.limiter !== ESDJ_LIMIT.NONE) { childPresent[childItem.sdId] += 1; } } else { rtnErrs.push(entity.sdKey + ",unknown child entity," + childItem.sdKey); } if (checkIndexing && childItem.sdKey !== (SD_IDX + idx)) { rtnErrs.push(dataJI.sdKey + ",KEY_IDX out of order or wrong," + childItem.sdKey + "!=" + SD_IDX + idx); } }); each(childPresent, (totals, sdId) => { const childEnt = entity.childRefs[sdId]; if (childEnt) { switch(childEnt.limiter) { case ESDJ_LIMIT.REQ: if (totals === 0) { rtnErrs.push(entity.sdKey + ",missing req'd," + childEnt.sdKey); } break; case ESDJ_LIMIT.REQ_HIDE: if (totals !== 1) { rtnErrs.push(entity.sdKey + `,single req hidden 1 != '${totals}',` + childEnt.sdKey); } break; case ESDJ_LIMIT.ONE_NONE: if (totals > 1) { rtnErrs.push(entity.sdKey + ",can only have one or none," + childEnt.sdKey); } break; case ESDJ_LIMIT.REQ_ONE: if (totals > 1 || totals === 0) { rtnErrs.push(entity.sdKey + ",requires one single," + childEnt.sdKey); } break; default: // default would be NONE/SD_IDX } } }); return rtnErrs; } private checkEntityData(searchEnt: EntitySearch, inData: IDataSdj, checkEnt: IEntitySdj): boolean { let rtnVal = false; // Assume single value array for now if (searchEnt.childIds) { if (find(inData.sdChildren, {sdId: checkEnt.sdId})) { rtnVal = true; } } else if (searchEnt.parentIds) { if (inData.parentRef && inData.parentRef.sdId === checkEnt.sdId) { rtnVal = true; } } else if (searchEnt.sdItems) { const checksdId = searchEnt.sdItems[0], foundItem = (checksdId) ? find(checkEnt.itemRefs, {sdId: checksdId}) : undefined; if (foundItem) { rtnVal = Boolean(!isUndefined(inData.getDataKey(foundItem.sdKey))); } } else { rtnVal = true; } return rtnVal; } }