UNPKG

@fjell/core

Version:

Core Item and Key Framework for Fjell

856 lines (846 loc) 25.2 kB
// src/logger.ts import Logging from "@fjell/logging"; var LibLogger = Logging.getLogger("@fjell/core"); var logger_default = LibLogger; // src/dictionary.ts var logger = logger_default.get("Dictionary"); var Dictionary = class _Dictionary { map = {}; hashFunction = (key) => JSON.stringify(key); constructor(map, hashFunction) { if (hashFunction) { this.hashFunction = hashFunction; } if (map) { Object.entries(map).forEach(([hashedKey, value]) => { try { const originalKey = JSON.parse(hashedKey); this.map[hashedKey] = { originalKey, value }; } catch { logger.warning("Cannot recover original key from legacy map entry", { hashedKey }); } }); } } set(key, item) { logger.trace("set", { key, item }); const hashedKey = this.hashFunction(key); this.map[hashedKey] = { originalKey: key, value: item }; } get(key) { logger.trace("get", { key }); const hashedKey = this.hashFunction(key); const entry = this.map[hashedKey]; return entry && this.keysEqual(entry.originalKey, key) ? entry.value : null; } keysEqual(key1, key2) { return key1 === key2; } delete(key) { logger.trace("delete", { key }); const hashedKey = this.hashFunction(key); delete this.map[hashedKey]; } keys() { return Object.values(this.map).map((entry) => entry.originalKey); } values() { return Object.values(this.map).map((entry) => entry.value); } includesKey(key) { const hashedKey = this.hashFunction(key); const entry = this.map[hashedKey]; return entry ? this.keysEqual(entry.originalKey, key) : false; } clone() { const clonedMap = {}; Object.entries(this.map).forEach(([hashedKey, entry]) => { clonedMap[hashedKey] = entry.value; }); const clone = new _Dictionary(clonedMap, this.hashFunction); clone.map = Object.assign({}, this.map); return clone; } }; // src/item/IFactory.ts import deepmerge from "deepmerge"; // src/key/KUtils.ts var logger2 = logger_default.get("KUtils"); var normalizeKeyValue = (value) => { return String(value); }; var createNormalizedHashFunction = () => { return (key) => { if (typeof key === "object" && key !== null) { const normalizedKey = JSON.parse(JSON.stringify(key)); if ("pk" in normalizedKey && (normalizedKey.pk !== void 0 && normalizedKey.pk !== null)) { normalizedKey.pk = normalizeKeyValue(normalizedKey.pk); } if ("lk" in normalizedKey && (normalizedKey.lk !== void 0 && normalizedKey.lk !== null)) { normalizedKey.lk = normalizeKeyValue(normalizedKey.lk); } if ("loc" in normalizedKey && Array.isArray(normalizedKey.loc)) { normalizedKey.loc = normalizedKey.loc.map((locItem) => { if (locItem && "lk" in locItem && (locItem.lk !== void 0 && locItem.lk !== null)) { return { ...locItem, lk: normalizeKeyValue(locItem.lk) }; } return locItem; }); } return JSON.stringify(normalizedKey); } return JSON.stringify(key); }; }; var isPriKeyEqualNormalized = (a, b) => { logger2.trace("isPriKeyEqualNormalized", { a, b }); return a && b && normalizeKeyValue(a.pk) === normalizeKeyValue(b.pk) && a.kt === b.kt; }; var isLocKeyEqualNormalized = (a, b) => { logger2.trace("isLocKeyEqualNormalized", { a, b }); return a && b && normalizeKeyValue(a.lk) === normalizeKeyValue(b.lk) && a.kt === b.kt; }; var isComKeyEqualNormalized = (a, b) => { logger2.trace("isComKeyEqualNormalized", { a, b }); if (a && b && isPriKeyEqualNormalized({ kt: a.kt, pk: a.pk }, { kt: b.kt, pk: b.pk })) { if (a.loc.length === b.loc.length) { for (let i = 0; i < a.loc.length; i++) { if (!isLocKeyEqualNormalized(a.loc[i], b.loc[i])) { return false; } } return true; } else { return false; } } else { return false; } }; var isItemKeyEqualNormalized = (a, b) => { logger2.trace("isItemKeyEqualNormalized", { a, b }); if (isComKey(a) && isComKey(b)) { return isComKeyEqualNormalized(a, b); } else if (isPriKey(a) && isPriKey(b)) { if (isComKey(a) || isComKey(b)) { return false; } else { return isPriKeyEqualNormalized(a, b); } } else { return false; } }; var isItemKeyEqual = (a, b) => { logger2.trace("isKeyEqual", { a, b }); if (isComKey(a) && isComKey(b)) { return isComKeyEqual(a, b); } else if (isPriKey(a) && isPriKey(b)) { if (isComKey(a) || isComKey(b)) { return false; } else { return isPriKeyEqual(a, b); } } else { return false; } }; var isPriKeyEqual = (a, b) => { logger2.trace("isPriKeyEqual", { a, b }); return a && b && a.pk === b.pk && a.kt === b.kt; }; var isLocKeyEqual = (a, b) => { logger2.trace("isLocKeyEqual", { a, b }); return a && b && a.lk === b.lk && a.kt === b.kt; }; var isComKeyEqual = (a, b) => { logger2.trace("isComKeyEqual", { a, b }); if (a && b && isPriKeyEqual({ kt: a.kt, pk: a.pk }, { kt: b.kt, pk: b.pk })) { if (a.loc.length === b.loc.length) { for (let i = 0; i < a.loc.length; i++) { if (!isLocKeyEqual(a.loc[i], b.loc[i])) { return false; } } return true; } else { return false; } } else { return false; } }; var isItemKey = (key) => { logger2.trace("isItemKey", { key }); return key !== void 0 && (isComKey(key) || isPriKey(key)); }; var isComKey = (key) => { logger2.trace("isComKey", { key }); return key !== void 0 && (key.pk !== void 0 && key.kt !== void 0) && (key.loc !== void 0 && key.loc.length > 0); }; var isPriKey = (key) => { logger2.trace("isPriKey", { key }); return key !== void 0 && (key.pk !== void 0 && key.kt !== void 0) && (key.loc === void 0 || key.loc.length === 0); }; var isLocKey = (key) => { logger2.trace("isLocKey", { key }); return key !== void 0 && (key.lk !== void 0 && key.kt !== void 0); }; var generateKeyArray = (key) => { logger2.trace("generateKeyArray", { key }); const keys = []; if (isComKey(key) || isPriKey(key)) { if (isComKey(key)) { const comKey = key; keys.push({ pk: comKey.pk, kt: comKey.kt }); for (let i = 0; i < comKey.loc.length; i++) { keys.push(comKey.loc[i]); } } else { keys.push(key); } } else { const locKeys = key; for (let i = 0; i < locKeys.length; i++) { keys.push(locKeys[i]); } } return keys; }; var constructPriKey = (pk, kt) => { logger2.trace("constructPriKey", { pk, kt }); let pri; if (typeof pk === "string" || typeof pk === "number") { pri = { kt, pk }; } else { pri = pk; } return pri; }; var cPK = constructPriKey; var toKeyTypeArray = (ik) => { logger2.trace("toKeyTypeArray", { ik }); if (isComKey(ik)) { const ck = ik; return [ck.kt, ...ck.loc.map((l) => l.kt)]; } else { return [ik.kt]; } }; var abbrevIK = (ik) => { logger2.trace("abbrevIK", { ik }); if (ik) { if (isComKey(ik)) { const ck = ik; return `${ck.kt}:${ck.pk}:${ck.loc.map((l) => `${l.kt}:${l.lk}`).join(",")}`; } else { return `${ik.kt}:${ik.pk}`; } } else { return "null IK"; } }; var abbrevLKA = (keyArray) => { logger2.trace("abbrevLKA", { keyArray }); if (keyArray === void 0 || keyArray === null) { return "null LKA"; } else { return keyArray.map((key) => { if (key) { return `${key.kt}:${key.lk}`; } else { return key; } }).join(","); } }; var primaryType = (ik) => { logger2.trace("primaryType", { ik }); if (isComKey(ik)) { return ik.kt; } else { return ik.kt; } }; var itemKeyToLocKeyArray = (ik) => { logger2.trace("itemKeyToLocKeyArray", { ik: abbrevIK(ik) }); let lka = []; if (isComKey(ik)) { const ck = ik; lka = [{ kt: ck.kt, lk: ck.pk }, ...ck.loc]; } else { const pk = ik; lka = [{ kt: pk.kt, lk: pk.pk }]; } logger2.trace("itemKeyToLocKeyArray Results", { ik: abbrevIK(ik), lka: abbrevLKA(lka) }); return lka; }; var ikToLKA = itemKeyToLocKeyArray; var locKeyArrayToItemKey = (lka) => { logger2.trace("locKeyArrayToItemKey", { lka: abbrevLKA(lka) }); if (lka && lka.length === 1) { const priKey = cPK(lka[0].lk, lka[0].kt); return priKey; } else if (lka && lka.length > 1 && lka[0] !== void 0) { const locs = lka.slice(1); const priKey = cPK(lka[0].lk, lka[0].kt); const comKey = { kt: priKey.kt, pk: priKey.pk, loc: locs }; return comKey; } else { throw new Error("locKeyArrayToItemKey: lka is undefined or empty"); } }; var isValidPriKey = (key) => { const valid = key !== void 0 && key !== null && (key.pk !== void 0 && key.pk !== null && key.pk !== "" && key.pk !== "null") && (key.kt !== void 0 && key.kt !== null && key.kt !== "" && key.kt !== "null"); return valid; }; var isValidLocKey = (key) => { const valid = key !== void 0 && key !== null && (key.lk !== void 0 && key.lk !== null && key.lk !== "" && key.lk !== "null") && (key.kt !== void 0 && key.kt !== null && key.kt !== "" && key.kt !== "null"); return valid; }; var isValidLocKeyArray = (keyArray) => { return keyArray !== void 0 && keyArray !== null && keyArray.every(isValidLocKey); }; var isValidComKey = (key) => { return key !== void 0 && key !== null && isValidPriKey(key) && isValidLocKeyArray(key.loc); }; var isValidItemKey = (key) => { return isComKey(key) && isValidComKey(key) || isPriKey(key) && isValidPriKey(key); }; var lkaToIK = locKeyArrayToItemKey; // src/item/IFactory.ts var IFactory = class _IFactory { item = {}; constructor(props = {}) { this.item = deepmerge(this.item, props); } addRef(i, name) { const ik = i.key; const refName = name || primaryType(ik); if (!this.item.refs) { this.item.refs = {}; } this.item.refs[refName] = ik; return this; } static addRef(i, name) { return new _IFactory().addRef(i, name); } addDefaultEvents() { if (!this.item.events) { this.item.events = {}; } const now = /* @__PURE__ */ new Date(); if (!this.item.events.created) { this.item.events.created = { at: now }; } if (!this.item.events.updated) { this.item.events.updated = { at: now }; } if (!this.item.events.deleted) { this.item.events.deleted = { at: null }; } return this; } addEvent(name, at, by) { if (!this.item.events) { this.item.events = {}; } this.item.events[name] = { at, by }; return this; } static addEvent(name, at, by) { return new _IFactory().addEvent(name, at, by); } addProp(name, value) { this.item[name] = value; return this; } static addProp(name, value) { return new _IFactory().addProp(name, value); } addProps(props) { this.item = deepmerge(this.item, props); return this; } static addProps(props) { return new _IFactory().addProps(props); } toItem() { return this.item; } }; // src/AItemService.ts var AItemService = class { pkType; parentService = null; constructor(pkType, parentService) { this.pkType = pkType; if (parentService) { this.parentService = parentService; } } getPkType = () => { return this.pkType; }; getKeyTypes = () => { let keyTypes = [this.getPkType()]; if (this.parentService) { keyTypes = keyTypes.concat(this.parentService.getKeyTypes()); } return keyTypes; }; }; // src/item/ItemQuery.ts var isCondition = (condition) => { return (typeof condition.column === "string" && (Array.isArray(condition.value) && condition.value.every((item) => typeof item === "string")) || Array.isArray(condition.value) && condition.value.every((item) => typeof item === "number") || typeof condition.value === "string" || typeof condition.value === "number" || typeof condition.value === "boolean" || condition.value instanceof Date) && (condition.operator ? typeof condition.operator === "string" : true); }; // src/item/IQFactory.ts var IQFactory = class _IQFactory { query = {}; constructor(query = {}) { this.query = query; } orderBy(field, direction = "asc") { if (!this.query.orderBy) { this.query.orderBy = []; } this.query.orderBy.push({ field, direction }); return this; } agg(name, query) { if (!this.query.aggs) { this.query.aggs = {}; } this.query.aggs[name] = query; return this; } event(name, query) { if (!this.query.events) { this.query.events = {}; } this.query.events[name] = query; return this; } conditions(conditions, compoundType = "AND") { for (const condition of conditions) { if (!isCondition(condition)) { throw new Error(`Invalid condition: ${JSON.stringify(condition)}`); } } if (!this.query.compoundCondition) { this.query.compoundCondition = { compoundType, conditions }; } else { const compoundCondition = { compoundType, conditions }; this.query.compoundCondition.conditions.push(compoundCondition); } return this; } limit(limit) { this.query.limit = limit; return this; } offset(offset) { this.query.offset = offset; return this; } // TODO: right now, we're only supporting PK refs for queries. Should add support for CKs pk(kt, pk, name) { if (!this.query.refs) { this.query.refs = {}; } const refName = name || kt; this.query.refs[refName] = cPK(pk, kt); return this; } condition(column, value, operator = "==") { const condition = { column, value, operator }; if (isCondition(condition)) { if (!this.query.compoundCondition) { this.query.compoundCondition = { compoundType: "AND", conditions: [] }; } this.query.compoundCondition.conditions.push(condition); return this; } else { throw new Error(`Invalid condition: ${JSON.stringify(condition)}`); } } static all() { const iqFactory = new _IQFactory(); return iqFactory; } static orderBy(field, direction = "asc") { const iqFactory = new _IQFactory(); return iqFactory.orderBy(field, direction); } static agg(name, query) { const iqFactory = new _IQFactory(); return iqFactory.agg(name, query); } static event(name, query) { const iqFactory = new _IQFactory(); return iqFactory.event(name, query); } static limit(limit) { const iqFactory = new _IQFactory(); return iqFactory.limit(limit); } static offset(offset) { const iqFactory = new _IQFactory(); return iqFactory.offset(offset); } static pk(kt, pk, name) { const iqFactory = new _IQFactory(); return iqFactory.pk(kt, pk, name); } static condition(column, value, operator = "==") { const iqFactory = new _IQFactory(); return iqFactory.condition(column, value, operator); } static conditions(conditions, compoundType = "AND") { const iqFactory = new _IQFactory(); return iqFactory.conditions(conditions, compoundType); } toQuery() { return this.query; } }; // src/item/IQUtils.ts import * as luxon from "luxon"; var logger3 = logger_default.get("IQUtils"); var queryToParams = (query) => { const params = {}; if (query.compoundCondition) { params.compoundCondition = JSON.stringify(query.compoundCondition); } if (query.refs) { params.refs = JSON.stringify(query.refs); } if (query.limit) { params.limit = query.limit; } if (query.offset) { params.offset = query.offset; } if (query.aggs) { params.aggs = JSON.stringify(query.aggs); } if (query.events) { params.events = JSON.stringify(query.events); } return params; }; var dateTimeReviver = function(key, value) { if (typeof value === "string") { const parsedDate = luxon.DateTime.fromISO(value); if (parsedDate.isValid) { return parsedDate.toJSDate(); ; } } return value; }; var paramsToQuery = (params) => { const query = {}; if (params.compoundCondition) { query.compoundCondition = JSON.parse(params.compoundCondition); } if (params.refs) { query.refs = JSON.parse(params.refs); } if (params.limit) { query.limit = Number(params.limit); } if (params.offset) { query.offset = Number(params.offset); } if (params.aggs) { query.aggs = JSON.parse(params.aggs); } if (params.events) { query.events = JSON.parse(params.events, dateTimeReviver); } return query; }; var isRefQueryMatch = (refKey, queryRef, references) => { logger3.trace("doesRefMatch", { queryRef, references }); logger3.debug("Comparing Ref", { refKey, itemRef: references[refKey], queryRef }); return isItemKeyEqual(queryRef, references[refKey]); }; var isCompoundConditionQueryMatch = (queryCondition, item) => { if (queryCondition.compoundType === "AND") { return queryCondition.conditions.every( (condition) => isCondition(condition) ? isConditionQueryMatch(condition, item) : isCompoundConditionQueryMatch(condition, item) ); } else { return queryCondition.conditions.some( (condition) => isCondition(condition) ? isConditionQueryMatch(condition, item) : isCompoundConditionQueryMatch(condition, item) ); } }; var isConditionQueryMatch = (queryCondition, item) => { const propKey = queryCondition.column; logger3.trace("doesConditionMatch", { propKey, queryCondition, item }); if (item[propKey] === void 0) { logger3.debug("Item does not contain prop under key", { propKey, item }); return false; } logger3.debug("Comparing Condition", { propKey, itemProp: item[propKey], queryCondition }); let result = false; switch (queryCondition.operator) { case "==": result = item[propKey] === queryCondition.value; break; case "!=": result = item[propKey] !== queryCondition.value; break; case ">": result = item[propKey] > queryCondition.value; break; case ">=": result = item[propKey] >= queryCondition.value; break; case "<": result = item[propKey] < queryCondition.value; break; case "<=": result = item[propKey] <= queryCondition.value; break; case "in": result = queryCondition.value.includes(item[propKey]); break; case "not-in": result = !queryCondition.value.includes(item[propKey]); break; case "array-contains": result = item[propKey].includes(queryCondition.value); break; case "array-contains-any": result = queryCondition.value.some((value) => item[propKey].includes(value)); break; } return result; }; var isAggQueryMatch = (aggKey, aggQuery, agg) => { const aggItem = agg.item; logger3.debug("Comparing Agg", { aggKey, aggItem, aggQuery }); return isQueryMatch(aggItem, aggQuery); }; var isEventQueryMatch = (eventKey, eventQuery, item) => { if (!item.events[eventKey]) { logger3.debug("Item does not contain event under key", { eventKey, events: item.events }); return false; } else { const itemEvent = item.events[eventKey]; if (itemEvent.at !== null) { if (eventQuery.start && !(eventQuery.start.getTime() <= itemEvent.at.getTime())) { logger3.debug("Item date before event start query", { eventQuery, itemEvent }); return false; } if (eventQuery.end && !(eventQuery.end.getTime() > itemEvent.at.getTime())) { logger3.debug("Item date after event end query", { eventQuery, itemEvent }); return false; } } else { logger3.debug("Item event does contains a null at", { itemEvent }); return false; } return true; } }; var isQueryMatch = (item, query) => { logger3.trace("isMatch", { item, query }); if (query.refs && item.refs) { for (const key in query.refs) { const queryRef = query.refs[key]; if (!isRefQueryMatch(key, queryRef, item.refs)) return false; } } else if (query.refs && !item.refs) { logger3.debug("Query contains refs but item does not have refs", { query, item }); return false; } if (query.compoundCondition && item) { if (!isCompoundConditionQueryMatch(query.compoundCondition, item)) return false; } if (query.events && item.events) { for (const key in query.events) { const queryEvent = query.events[key]; if (!isEventQueryMatch(key, queryEvent, item)) return false; } return true; } if (query.aggs && item.aggs) { for (const key in query.aggs) { const aggQuery = query.aggs[key]; if (item.aggs[key] && !isAggQueryMatch(key, aggQuery, item.aggs[key])) return false; } } if (query.aggs && !item.aggs) { logger3.debug("Query contains aggs but item does not have aggs", { query, item }); return false; } return true; }; var abbrevQuery = (query) => { const abbrev = ["IQ"]; if (query) { if (query.refs) { for (const key in query.refs) { const ref = abbrevRef(key, query.refs[key]); abbrev.push(ref); } } if (query.compoundCondition) { const props = abbrevCompoundCondition(query.compoundCondition); abbrev.push(props); } if (query.aggs) { for (const key in query.aggs) { const agg = abbrevAgg(key, query.aggs[key]); abbrev.push(agg); } } if (query.events) { const events = `(E${Object.keys(query.events).join(",")})`; abbrev.push(events); } if (query.limit) { abbrev.push(`L${query.limit}`); } if (query.offset) { abbrev.push(`O${query.offset}`); } } else { abbrev.push("(empty)"); } return abbrev.join(" "); }; var abbrevRef = (key, ref) => { if (isPriKey(ref)) { const priKey = ref; return `R(${key},${priKey.kt},${priKey.pk})`; } else { const comKey = ref; return `R(${key},${JSON.stringify(comKey)})`; } }; var abbrevAgg = (key, agg) => { return `A(${key},${abbrevQuery(agg)})`; }; var abbrevCompoundCondition = (compoundCondition) => { return `CC(${compoundCondition.compoundType},${compoundCondition.conditions ? compoundCondition.conditions.map(abbrevCondition).join(",") : "No Conditions"})`; }; var abbrevCondition = (condition) => { if (isCondition(condition)) { return `(${condition.column},${condition.value},${condition.operator})`; } else { return abbrevCompoundCondition(condition); } }; // src/item/IUtils.ts var logger4 = logger_default.get("IUtils"); var validatePKForItem = (item, pkType) => { if (!item) { logger4.error("Validating PK, Item is undefined", { item }); throw new Error("Validating PK, Item is undefined"); } if (!item.key) { logger4.error("Validating PK, Item does not have a key", { item }); throw new Error("Validating PK, Item does not have a key"); } const keyTypeArray = toKeyTypeArray(item.key); if (keyTypeArray[0] !== pkType) { logger4.error("Key Type Array Mismatch", { keyTypeArray, pkType }); throw new Error(`Item does not have the correct primary key type. Expected ${pkType}, got ${keyTypeArray[0]}`); } return item; }; var validatePK = (input, pkType) => { logger4.trace("Checking Return Type", { input }); if (Array.isArray(input)) { return input.map((item) => validatePKForItem(item, pkType)); } return validatePKForItem(input, pkType); }; var validateKeys = (item, keyTypes) => { logger4.trace("Checking Return Type", { item }); if (!item) { throw new Error("validating keys, item is undefined"); } if (!item.key) { throw new Error("validating keys, item does not have a key: " + JSON.stringify(item)); } const keyTypeArray = toKeyTypeArray(item.key); if (keyTypeArray.length !== keyTypes.length) { throw new Error(`Item does not have the correct number of keys. Expected ${keyTypes.length}, but got ${keyTypeArray.length}`); } const match = JSON.stringify(keyTypeArray) === JSON.stringify(keyTypes); if (!match) { logger4.error("Key Type Array Mismatch", { keyTypeArray, thisKeyTypes: keyTypes }); throw new Error(`Item does not have the correct key types. Expected [${keyTypes.join(", ")}], but got [${keyTypeArray.join(", ")}]`); } return item; }; var isPriItem = (item) => { return !!(item && item.key && isPriKey(item.key)); }; var isComItem = (item) => { return !!(item && item.key && isComKey(item.key)); }; export { AItemService, Dictionary, IFactory, IQFactory, abbrevAgg, abbrevCompoundCondition, abbrevCondition, abbrevIK, abbrevLKA, abbrevQuery, abbrevRef, cPK, constructPriKey, createNormalizedHashFunction, generateKeyArray, ikToLKA, isComItem, isComKey, isComKeyEqual, isComKeyEqualNormalized, isCondition, isItemKey, isItemKeyEqual, isItemKeyEqualNormalized, isLocKey, isLocKeyEqual, isLocKeyEqualNormalized, isPriItem, isPriKey, isPriKeyEqual, isPriKeyEqualNormalized, isQueryMatch, isValidComKey, isValidItemKey, isValidLocKey, isValidLocKeyArray, isValidPriKey, itemKeyToLocKeyArray, lkaToIK, locKeyArrayToItemKey, paramsToQuery, primaryType, queryToParams, toKeyTypeArray, validateKeys, validatePK };