rljson
Version:
Define and manage relational data structures in JSON
480 lines (479 loc) • 14.9 kB
JavaScript
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
};