UNPKG

rljson

Version:

Define and manage relational data structures in JSON

480 lines (479 loc) 14.9 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { JsonHash, ApplyJsonHashConfig } from "gg-json-hash"; // @license class Rljson { // ........................................................................... /// Creates an instance of Rljson. constructor({ data, dataIndexed }) { __publicField(this, "data"); __publicField(this, "dataIndexed"); __publicField(this, "jsonJash", JsonHash.default); this.data = data; this.dataIndexed = dataIndexed; } // ........................................................................... /// Creates an Rljson instance from JSON data. static fromJson(data, options = { validateHashes: false, updateHashes: true }) { const { validateHashes = false } = options; const { updateHashes = true } = options; let result = new Rljson({ data: {}, dataIndexed: {} }); result = result.addData(data, { validateHashes, updateHashes }); return result; } // ........................................................................... /// Creates an empty Rljson instance static empty() { return new Rljson({ data: {}, dataIndexed: {} }); } // ........................................................................... /// Creates a new json containing the given data addData(addedData, options = { validateHashes: false, updateHashes: true }) { const { validateHashes = false, updateHashes = true } = options; this._checkData(addedData); Rljson.checkTableNames(addedData); if (validateHashes) { this.jsonJash.validate(addedData); } addedData = this.jsonJash.apply( addedData, new ApplyJsonHashConfig( false, updateHashes, validateHashes // throwIfOnWrongHashes ) ); const addedDataAsMap = this._toMap(addedData); if (Object.keys(this.data).length === 0) { return new Rljson({ data: addedData, dataIndexed: addedDataAsMap }); } const mergedData = { ...this.data }; const mergedDataIndexed = { ...this.dataIndexed }; if (Object.keys(this.data).length > 0) { for (const table of Object.keys(addedData)) { if (table === "_hash") { continue; } const oldTable = this.data[table]; const newTable = addedData[table]; if (oldTable == null) { mergedData[table] = newTable; mergedDataIndexed[table] = addedDataAsMap[table]; continue; } const oldDataIndexed = this.dataIndexed[table]; const mergedTable = [...oldTable["_data"]]; const mergedTableIndexed = { ...oldDataIndexed }; const newData = newTable["_data"]; for (const item of newData) { const hash = item["_hash"]; const exists = mergedTableIndexed[hash] != null; if (!exists) { mergedTable.push(item); mergedTableIndexed[hash] = item; } } newTable["_data"] = mergedTable; mergedData[table] = newTable; mergedDataIndexed[table] = mergedTableIndexed; } } delete mergedData._hash; this.jsonJash.apply(mergedData, { updateExistingHashes: false, throwIfOnWrongHashes: false, inPlace: true }); this.data = mergedData; this.dataIndexed = mergedDataIndexed; return this; } // ........................................................................... /// Returns the table with the given name. Throws when name is not found. tableIndexed(table) { const tableData = this.dataIndexed[table]; if (tableData == null) { throw new Error(`Table not found: ${table}`); } return tableData; } // ........................................................................... /// Returns the table with the given name. Throws when name is not found. table(table) { const tableData = this.data[table]; if (tableData == null) { throw new Error(`Table not found: ${table}`); } return tableData; } // ........................................................................... hasTable(table) { return this.dataIndexed[table] != null; } // ........................................................................... /// Adds a new table to the data createTable(table) { return this.addData({ [table]: { _data: [] } }); } // ........................................................................... /// Allows to query data from a table items({ table, where }) { const tableData = this.tableIndexed(table); const items = Object.values(tableData).filter(where); return items; } // ........................................................................... /// Allows to query data from the json row(table, hash) { const tableData = this.dataIndexed[table]; if (tableData == null) { throw new Error(`Table not found: ${table}`); } const item = tableData[hash]; if (item == null) { throw new Error(`Item not found with hash "${hash}" in table "${table}"`); } return item; } // ........................................................................... addRow(table, item) { item = this.jsonJash.apply( item, new ApplyJsonHashConfig( false, // inPlace false, // updateExistingHashes true // throwIfOnWrongHashes ) ); let tableData = this.table(table); let tableDataIndexed = this.dataIndexed[table]; const itemExitsts = tableDataIndexed[item._hash] != null; if (itemExitsts) { return; } tableData["_data"].push(item); tableDataIndexed[item._hash] = item; } // ........................................................................... /// Queries a value from data. Throws when table or hash is not found. value({ table, itemHash, followLink }) { if (itemHash.length === 0) { throw new Error("itemHash must not be empty."); } const row = this.row(table, itemHash); if (!(followLink == null ? void 0 : followLink.length)) { return row; } const refKey = followLink[0]; const value = row[refKey]; if (value == null) { throw new Error( `Key "${refKey}" not found in item with hash "${itemHash}" in table "${table}"` ); } if (!refKey.endsWith("Ref")) { const refHash = followLink[1]; if (refHash != null) { throw new Error( `Invalid key "${refHash}". Additional keys are only allowed for links. But key "${refKey}" points to a value.` ); } return value; } const targetTable = refKey.substring(0, refKey.length - 3); const targetHash = value; return this.value({ table: targetTable, itemHash: targetHash, followLink: followLink.slice(1) }); } // ........................................................................... /// Joins multiple tables into one and returns the result /// /// Note: This implementation is not optimized for performance. select(table, columns) { var _a; const sourceRows = (_a = this.data[table]) == null ? void 0 : _a._data; if (!sourceRows) { throw new Error(`Table "${table}" not found.`); } const columnParts = columns.map((column) => column.split("/")); const targetRows = new Array(sourceRows.length); for (let rowNo = 0; rowNo < sourceRows.length; rowNo++) { const sourceRow = sourceRows[rowNo]; const targetRow = targetRows[rowNo] = new Array(columns.length); for (let colNo = 0; colNo < columnParts.length; colNo++) { const parts = columnParts[colNo]; const key = parts[0]; if (!key.endsWith("Ref")) { targetRow[colNo] = sourceRow[key]; continue; } else { const targetTable = key.substring(0, key.length - 3); const targetHash = sourceRow[key]; targetRow[colNo] = this.value({ table: targetTable, itemHash: targetHash, followLink: parts.slice(1) }); } } } return targetRows; } // ........................................................................... /// Returns the hash of the item at the given index in the table hash({ table, index }) { const tableData = this.data[table]; if (tableData == null) { throw new Error(`Table "${table}" not found.`); } const items = tableData["_data"]; if (index >= items.length) { throw new Error(`Index ${index} out of range in table "${table}".`); } const item = items[index]; return item["_hash"]; } // ........................................................................... /// Returns all paths found in data ls() { const result = []; for (const [table, tableData] of Object.entries(this.dataIndexed)) { for (const [hash, item] of Object.entries(tableData)) { for (const key of Object.keys(item)) { if (key === "_hash") { continue; } result.push(`${table}/${hash}/${key}`); } } } return result; } // ........................................................................... /// Throws if a link is not available checkLinks() { for (const table of Object.keys(this.dataIndexed)) { const tableData = this.dataIndexed[table]; for (const entry of Object.entries(tableData)) { const item = entry[1]; for (const key of Object.keys(item)) { if (key === "_hash") continue; if (key.endsWith("Ref")) { const tableName = key.substring(0, key.length - 3); const linkTable = this.dataIndexed[tableName]; const hash = item["_hash"]; if (linkTable == null) { throw new Error( `Table "${table}" has an item "${hash}" which links to not existing table "${key}".` ); } const targetHash = item[key]; const linkedItem = linkTable[targetHash]; if (linkedItem == null) { throw new Error( `Table "${table}" has an item "${hash}" which links to not existing item "${targetHash}" in table "${tableName}".` ); } } } } } } // ........................................................................... /// An example object static get example() { return Rljson.fromJson({ tableA: { _data: [ { keyA0: "a0" }, { keyA1: "a1" } ] }, tableB: { _data: [ { keyB0: "b0" }, { keyB1: "b1" } ] } }); } // ........................................................................... /// An example object static get exampleWithLink() { return Rljson.fromJson({ tableA: { _data: [ { keyA0: "a0" }, { keyA1: "a1" } ] }, linkToTableA: { _data: [ { tableARef: "KFQrf4mEz0UPmUaFHwH4T6" } ] } }); } // ........................................................................... /// An example object static get exampleWithDeepLink() { let rljson = Rljson.fromJson({}); rljson = rljson.addData({ d: { _data: [ { value: "d", details: "details about d" } ] } }); const hashD = rljson.hash({ table: "d", index: 0 }); rljson = rljson.addData({ c: { _data: [ { dRef: hashD, value: "c" } ] } }); const hashC = rljson.hash({ table: "c", index: 0 }); rljson = rljson.addData({ b: { _data: [ { cRef: hashC, value: "b" } ] } }); const hashB = rljson.hash({ table: "b", index: 0 }); rljson = rljson.addData({ a: { _data: [ { bRef: hashB, value: "a" }, { bRef: hashB, value: "a0" } ] } }); JsonHash.default.validate(rljson.data); return rljson; } // ........................................................................... /// Checks if table names are valid static checkTableNames(data) { for (const key of Object.keys(data)) { if (key === "_hash") continue; this.checkTableName(key); } } // ........................................................................... /// Checks if a string is valid table name static checkTableName(str) { if (!/^[a-zA-Z0-9]+$/.test(str)) { throw new Error( `Invalid table name: ${str}. Only letters and numbers are allowed.` ); } if (str.endsWith("Ref")) { throw new Error( `Invalid table name: ${str}. Table names must not end with "Ref".` ); } if (/^[0-9]/.test(str)) { throw new Error( `Invalid table name: ${str}. Table names must not start with a number.` ); } } // ........................................................................... /// Checks if data is valid _checkData(data) { const tablesWithMissingData = []; const tablesWithWrongType = []; for (const table of Object.keys(data)) { if (table === "_hash") continue; const tableData = data[table]; const items = tableData["_data"]; if (items == null) { tablesWithMissingData.push(table); } if (!Array.isArray(items)) { tablesWithWrongType.push(table); } } if (tablesWithMissingData.length > 0) { throw new Error( `_data is missing in table: ${tablesWithMissingData.join(", ")}` ); } if (tablesWithWrongType.length > 0) { throw new Error( `_data must be a list in table: ${tablesWithWrongType.join(", ")}` ); } } // ........................................................................... /// Turns data into a map _toMap(data) { const result = {}; for (const table of Object.keys(data)) { if (table.startsWith("_")) continue; const tableData = {}; result[table] = tableData; const items = data[table]["_data"]; for (const item of items) { const hash = item["_hash"]; tableData[hash] = item; } } return result; } } export { Rljson };