@rljson/rljson 0.0.72 → 0.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content/tree.d.ts +52 -0
- package/dist/example/bakery-example.d.ts +2 -0
- package/dist/example.d.ts +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/rljson.d.ts +2 -1
- package/dist/rljson.js +312 -8
- package/dist/src/example.ts +57 -0
- package/dist/tools/time-id.d.ts +10 -9
- package/dist/typedefs.d.ts +1 -1
- package/dist/validate/base-validator.d.ts +3 -0
- package/package.json +14 -14
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Json } from '@rljson/json';
|
|
2
|
+
import { RljsonTable } from '../rljson.ts';
|
|
3
|
+
import { Ref } from '../typedefs.ts';
|
|
4
|
+
import { TableCfg } from './table-cfg.ts';
|
|
5
|
+
/**
|
|
6
|
+
* A TreeRef is a hash pointing to another hash in the tree
|
|
7
|
+
*/
|
|
8
|
+
export type TreeRef = Ref;
|
|
9
|
+
/**
|
|
10
|
+
* A Tree is a hierarchical structure of Trees
|
|
11
|
+
*/
|
|
12
|
+
export interface Tree extends Json {
|
|
13
|
+
/**
|
|
14
|
+
* `id` identifies the tree node, it has to be unique among sibling nodes
|
|
15
|
+
*/
|
|
16
|
+
id?: string;
|
|
17
|
+
/**
|
|
18
|
+
* If `isParent` is true, this node is a parent node and can have children
|
|
19
|
+
*/
|
|
20
|
+
isParent: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Optional meta information about this tree node
|
|
23
|
+
*/
|
|
24
|
+
meta: Json | null;
|
|
25
|
+
/**
|
|
26
|
+
* The children of this tree node
|
|
27
|
+
*/
|
|
28
|
+
children: Array<TreeRef> | null;
|
|
29
|
+
}
|
|
30
|
+
export type TreeWithHash = Tree & {
|
|
31
|
+
_hash: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A table containing trees
|
|
35
|
+
*/
|
|
36
|
+
export type TreesTable = RljsonTable<Tree, 'trees'>;
|
|
37
|
+
/**
|
|
38
|
+
* Creates a TableCfg for Trees tables
|
|
39
|
+
* @param treesTableKey - The table key of the trees table
|
|
40
|
+
* @returns A TableCfg for Trees tables
|
|
41
|
+
*/
|
|
42
|
+
export declare const createTreesTableCfg: (treesTableKey: string) => TableCfg;
|
|
43
|
+
/**
|
|
44
|
+
* Provides an example treesTable for test purposes
|
|
45
|
+
*/
|
|
46
|
+
export declare const exampleTreesTable: () => TreesTable;
|
|
47
|
+
/**
|
|
48
|
+
* Converts a plain object into a tree structure
|
|
49
|
+
* @param obj - The plain object to convert
|
|
50
|
+
* @returns An array of Tree nodes representing the tree structure
|
|
51
|
+
*/
|
|
52
|
+
export declare const treeFromObject: (obj: any) => TreeWithHash[];
|
|
@@ -4,6 +4,7 @@ import { CakesTable } from '../content/cake.ts';
|
|
|
4
4
|
import { ComponentsTable } from '../content/components.ts';
|
|
5
5
|
import { LayersTable } from '../content/layer.ts';
|
|
6
6
|
import { SliceIdsTable } from '../content/slice-ids.ts';
|
|
7
|
+
import { TreesTable } from '../content/tree.ts';
|
|
7
8
|
import { InsertHistoryTable } from '../insertHistory/insertHistory.ts';
|
|
8
9
|
import { Rljson } from '../rljson.ts';
|
|
9
10
|
import { Ref } from '../typedefs.ts';
|
|
@@ -33,5 +34,6 @@ export interface Bakery extends Rljson {
|
|
|
33
34
|
ingredients: ComponentsTable<Ingredient>;
|
|
34
35
|
nutritionalValues: ComponentsTable<NutritionalValues>;
|
|
35
36
|
ingredientsInsertHistory: InsertHistoryTable<'Ingredients'>;
|
|
37
|
+
recipesTreeTable: TreesTable;
|
|
36
38
|
}
|
|
37
39
|
export declare const bakeryExample: () => Bakery;
|
package/dist/example.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class Example {
|
|
|
10
10
|
multiRef: () => Rljson;
|
|
11
11
|
singleSliceIdRef: () => Rljson;
|
|
12
12
|
multiSliceIdRef: () => Rljson;
|
|
13
|
+
tree: () => Rljson;
|
|
13
14
|
complete: () => Rljson;
|
|
14
15
|
};
|
|
15
16
|
static readonly broken: {
|
|
@@ -30,6 +31,12 @@ export declare class Example {
|
|
|
30
31
|
tableCfg: {
|
|
31
32
|
wrongType: () => Rljson;
|
|
32
33
|
};
|
|
34
|
+
trees: {
|
|
35
|
+
missingChildNodes: () => Rljson;
|
|
36
|
+
cyclicTree: () => Rljson;
|
|
37
|
+
duplicateChildNodeIds: () => Rljson;
|
|
38
|
+
nonParentWithChildren: () => Rljson;
|
|
39
|
+
};
|
|
33
40
|
layers: {
|
|
34
41
|
missingBase: () => Rljson;
|
|
35
42
|
missingSliceIdSet: () => Rljson;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './content/layer.ts';
|
|
|
5
5
|
export * from './content/revision.ts';
|
|
6
6
|
export * from './content/slice-ids.ts';
|
|
7
7
|
export * from './content/table-cfg.ts';
|
|
8
|
+
export * from './content/tree.ts';
|
|
8
9
|
export * from './edit/edit-history.ts';
|
|
9
10
|
export * from './edit/edit.ts';
|
|
10
11
|
export * from './edit/multi-edit.ts';
|
package/dist/rljson.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { LayersTable } from './content/layer.ts';
|
|
|
6
6
|
import { RevisionsTable } from './content/revision.ts';
|
|
7
7
|
import { SliceIdsTable } from './content/slice-ids.ts';
|
|
8
8
|
import { TableCfgRef, TablesCfgTable } from './content/table-cfg.ts';
|
|
9
|
+
import { TreesTable } from './content/tree.ts';
|
|
9
10
|
import { EditHistoryTable } from './edit/edit-history.ts';
|
|
10
11
|
import { EditsTable } from './edit/edit.ts';
|
|
11
12
|
import { MultiEditsTable } from './edit/multi-edit.ts';
|
|
@@ -16,7 +17,7 @@ export declare const reservedTableKeys: string[];
|
|
|
16
17
|
/**
|
|
17
18
|
* One of the supported Rljson table types
|
|
18
19
|
*/
|
|
19
|
-
export type TableType = BuffetsTable | ComponentsTable<any> | LayersTable | SliceIdsTable | CakesTable | RevisionsTable | TablesCfgTable | InsertHistoryTable<any> | EditsTable | MultiEditsTable | EditHistoryTable;
|
|
20
|
+
export type TableType = BuffetsTable | ComponentsTable<any> | LayersTable | SliceIdsTable | CakesTable | RevisionsTable | TablesCfgTable | InsertHistoryTable<any> | EditsTable | MultiEditsTable | EditHistoryTable | TreesTable;
|
|
20
21
|
/** The rljson data format */
|
|
21
22
|
export interface Rljson extends Json {
|
|
22
23
|
[tableId: TableKey]: TableType;
|
package/dist/rljson.js
CHANGED
|
@@ -154,7 +154,7 @@ class Route {
|
|
|
154
154
|
* @returns True if the current route is the root route, false otherwise
|
|
155
155
|
*/
|
|
156
156
|
get isRoot() {
|
|
157
|
-
return this._segments.length
|
|
157
|
+
return this._segments.length <= 1;
|
|
158
158
|
}
|
|
159
159
|
// .............................................................................
|
|
160
160
|
/**
|
|
@@ -486,6 +486,23 @@ const bakeryExample = () => {
|
|
|
486
486
|
],
|
|
487
487
|
_hash: ""
|
|
488
488
|
});
|
|
489
|
+
const recipesTreeChildren = hip({
|
|
490
|
+
id: "tastyCake",
|
|
491
|
+
isParent: false,
|
|
492
|
+
meta: { description: "A tasty cake recipe" },
|
|
493
|
+
children: null
|
|
494
|
+
});
|
|
495
|
+
const recipesTreeRoot = hip({
|
|
496
|
+
id: "root",
|
|
497
|
+
isParent: true,
|
|
498
|
+
meta: { description: "Root of the recipes tree" },
|
|
499
|
+
children: [recipesTreeChildren._hash]
|
|
500
|
+
});
|
|
501
|
+
const recipesTreeTable = hip({
|
|
502
|
+
_type: "trees",
|
|
503
|
+
_data: [recipesTreeRoot, recipesTreeChildren],
|
|
504
|
+
_hash: ""
|
|
505
|
+
});
|
|
489
506
|
const result = {
|
|
490
507
|
buffets,
|
|
491
508
|
cakes,
|
|
@@ -496,7 +513,8 @@ const bakeryExample = () => {
|
|
|
496
513
|
recipeIngredients,
|
|
497
514
|
ingredients,
|
|
498
515
|
nutritionalValues,
|
|
499
|
-
ingredientsInsertHistory
|
|
516
|
+
ingredientsInsertHistory,
|
|
517
|
+
recipesTreeTable
|
|
500
518
|
};
|
|
501
519
|
return result;
|
|
502
520
|
};
|
|
@@ -595,6 +613,127 @@ const createSliceIdsTableCfg = (tableKey) => ({
|
|
|
595
613
|
isRoot: false,
|
|
596
614
|
isShared: true
|
|
597
615
|
});
|
|
616
|
+
const createTreesTableCfg = (treesTableKey) => ({
|
|
617
|
+
key: treesTableKey,
|
|
618
|
+
type: "trees",
|
|
619
|
+
columns: [
|
|
620
|
+
{ key: "_hash", type: "string", titleLong: "Hash", titleShort: "Hash" },
|
|
621
|
+
{ key: "id", type: "string", titleLong: "Identifier", titleShort: "Id" },
|
|
622
|
+
{
|
|
623
|
+
key: "isParent",
|
|
624
|
+
type: "boolean",
|
|
625
|
+
titleLong: "Is Parent",
|
|
626
|
+
titleShort: "Is Parent"
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
key: "meta",
|
|
630
|
+
type: "json",
|
|
631
|
+
titleLong: "Meta Information",
|
|
632
|
+
titleShort: "Meta"
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
key: "children",
|
|
636
|
+
type: "jsonArray",
|
|
637
|
+
titleLong: "Children",
|
|
638
|
+
titleShort: "Children"
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
isHead: false,
|
|
642
|
+
isRoot: false,
|
|
643
|
+
isShared: true
|
|
644
|
+
});
|
|
645
|
+
const exampleTreesTable = () => bakeryExample().recipesTreeTable;
|
|
646
|
+
const treeFromObject = (obj) => {
|
|
647
|
+
const result = [];
|
|
648
|
+
const processedIds = /* @__PURE__ */ new Set();
|
|
649
|
+
const idToHashMap = /* @__PURE__ */ new Map();
|
|
650
|
+
const processNode = (value, nodeId) => {
|
|
651
|
+
if (processedIds.has(nodeId)) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
processedIds.add(nodeId);
|
|
655
|
+
const childIds = [];
|
|
656
|
+
if (Array.isArray(value)) {
|
|
657
|
+
const isChildrenArray = value.length === 0 || value.every(
|
|
658
|
+
(item) => item !== null && typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 1
|
|
659
|
+
);
|
|
660
|
+
if (isChildrenArray) {
|
|
661
|
+
for (const item of value) {
|
|
662
|
+
const keys = Object.keys(item);
|
|
663
|
+
const childId = keys[0];
|
|
664
|
+
childIds.push(childId);
|
|
665
|
+
processNode(item[childId], childId);
|
|
666
|
+
}
|
|
667
|
+
const treeNode = {
|
|
668
|
+
id: nodeId,
|
|
669
|
+
isParent: true,
|
|
670
|
+
meta: null,
|
|
671
|
+
children: childIds.length > 0 ? childIds.map((id) => idToHashMap.get(id)) : null
|
|
672
|
+
};
|
|
673
|
+
const hashedNode = hip(treeNode);
|
|
674
|
+
idToHashMap.set(nodeId, hashedNode._hash);
|
|
675
|
+
result.push(hashedNode);
|
|
676
|
+
} else {
|
|
677
|
+
const treeNode = {
|
|
678
|
+
id: nodeId,
|
|
679
|
+
isParent: false,
|
|
680
|
+
meta: { value },
|
|
681
|
+
children: null
|
|
682
|
+
};
|
|
683
|
+
const hashedNode = hip(treeNode);
|
|
684
|
+
idToHashMap.set(nodeId, hashedNode._hash);
|
|
685
|
+
result.push(hashedNode);
|
|
686
|
+
}
|
|
687
|
+
} else if (value !== null && typeof value === "object") {
|
|
688
|
+
const keys = Object.keys(value);
|
|
689
|
+
if (keys.includes("meta") || keys.includes("isParent") || keys.includes("children")) {
|
|
690
|
+
const treeNode = {
|
|
691
|
+
id: nodeId,
|
|
692
|
+
isParent: false,
|
|
693
|
+
meta: value.meta,
|
|
694
|
+
children: null
|
|
695
|
+
};
|
|
696
|
+
const hashedNode = hip(treeNode);
|
|
697
|
+
idToHashMap.set(nodeId, hashedNode._hash);
|
|
698
|
+
result.push(hashedNode);
|
|
699
|
+
} else {
|
|
700
|
+
for (const key in value) {
|
|
701
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
702
|
+
childIds.push(key);
|
|
703
|
+
processNode(value[key], key);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const treeNode = {
|
|
707
|
+
id: nodeId,
|
|
708
|
+
isParent: true,
|
|
709
|
+
meta: null,
|
|
710
|
+
children: childIds.length > 0 ? childIds.map((id) => idToHashMap.get(id)) : null
|
|
711
|
+
};
|
|
712
|
+
const hashedNode = hip(treeNode);
|
|
713
|
+
idToHashMap.set(nodeId, hashedNode._hash);
|
|
714
|
+
result.push(hashedNode);
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
const treeNode = {
|
|
718
|
+
id: nodeId,
|
|
719
|
+
isParent: false,
|
|
720
|
+
meta: { value },
|
|
721
|
+
children: null
|
|
722
|
+
};
|
|
723
|
+
const hashedNode = hip(treeNode);
|
|
724
|
+
idToHashMap.set(nodeId, hashedNode._hash);
|
|
725
|
+
result.push(hashedNode);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
|
|
729
|
+
for (const key in obj) {
|
|
730
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
731
|
+
processNode(obj[key], key);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
};
|
|
598
737
|
class Example {
|
|
599
738
|
static ok = {
|
|
600
739
|
bakery: () => bakeryExample(),
|
|
@@ -936,6 +1075,11 @@ class Example {
|
|
|
936
1075
|
}
|
|
937
1076
|
};
|
|
938
1077
|
},
|
|
1078
|
+
tree: () => {
|
|
1079
|
+
return {
|
|
1080
|
+
recipesTreeTable: exampleTreesTable()
|
|
1081
|
+
};
|
|
1082
|
+
},
|
|
939
1083
|
complete: () => {
|
|
940
1084
|
const sliceIds = hip({
|
|
941
1085
|
_type: "sliceIds",
|
|
@@ -1294,6 +1438,42 @@ class Example {
|
|
|
1294
1438
|
});
|
|
1295
1439
|
}
|
|
1296
1440
|
},
|
|
1441
|
+
trees: {
|
|
1442
|
+
missingChildNodes: () => {
|
|
1443
|
+
const result = Example.ok.tree();
|
|
1444
|
+
const treeTable = result.recipesTreeTable;
|
|
1445
|
+
treeTable._data.pop();
|
|
1446
|
+
return hip(result, {
|
|
1447
|
+
updateExistingHashes: true,
|
|
1448
|
+
throwOnWrongHashes: false
|
|
1449
|
+
});
|
|
1450
|
+
},
|
|
1451
|
+
cyclicTree: () => {
|
|
1452
|
+
const result = Example.ok.tree();
|
|
1453
|
+
const treeTable = result.recipesTreeTable;
|
|
1454
|
+
treeTable._data[0].children = [treeTable._data[0]._hash];
|
|
1455
|
+
return { recipesTreeTable: treeTable };
|
|
1456
|
+
},
|
|
1457
|
+
duplicateChildNodeIds: () => {
|
|
1458
|
+
const result = Example.ok.tree();
|
|
1459
|
+
const treeTable = result.recipesTreeTable;
|
|
1460
|
+
const firstChildHash = treeTable._data[1]._hash;
|
|
1461
|
+
treeTable._data[0].children = [firstChildHash, firstChildHash];
|
|
1462
|
+
return hip(result, {
|
|
1463
|
+
updateExistingHashes: true,
|
|
1464
|
+
throwOnWrongHashes: false
|
|
1465
|
+
});
|
|
1466
|
+
},
|
|
1467
|
+
nonParentWithChildren: () => {
|
|
1468
|
+
const result = Example.ok.tree();
|
|
1469
|
+
const treeTable = result.recipesTreeTable;
|
|
1470
|
+
treeTable._data[0].isParent = false;
|
|
1471
|
+
return hip(result, {
|
|
1472
|
+
updateExistingHashes: true,
|
|
1473
|
+
throwOnWrongHashes: false
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1297
1477
|
layers: {
|
|
1298
1478
|
missingBase: () => {
|
|
1299
1479
|
const result = Example.ok.complete();
|
|
@@ -1509,7 +1689,7 @@ const createEditHistoryTableCfg = (cakeKey) => ({
|
|
|
1509
1689
|
titleLong: "Multi Edit Reference",
|
|
1510
1690
|
titleShort: "Multi Edit Ref",
|
|
1511
1691
|
ref: {
|
|
1512
|
-
tableKey: `${cakeKey}
|
|
1692
|
+
tableKey: `${cakeKey}MultiEdits`
|
|
1513
1693
|
}
|
|
1514
1694
|
},
|
|
1515
1695
|
{
|
|
@@ -1527,7 +1707,10 @@ const createEditHistoryTableCfg = (cakeKey) => ({
|
|
|
1527
1707
|
titleLong: "Previous Values",
|
|
1528
1708
|
titleShort: "Previous"
|
|
1529
1709
|
}
|
|
1530
|
-
]
|
|
1710
|
+
],
|
|
1711
|
+
isHead: false,
|
|
1712
|
+
isRoot: false,
|
|
1713
|
+
isShared: true
|
|
1531
1714
|
});
|
|
1532
1715
|
const createEditTableCfg = (cakeKey) => ({
|
|
1533
1716
|
key: `${cakeKey}Edits`,
|
|
@@ -1551,7 +1734,10 @@ const createEditTableCfg = (cakeKey) => ({
|
|
|
1551
1734
|
titleLong: "Edit Action Data",
|
|
1552
1735
|
titleShort: "Action"
|
|
1553
1736
|
}
|
|
1554
|
-
]
|
|
1737
|
+
],
|
|
1738
|
+
isHead: false,
|
|
1739
|
+
isRoot: false,
|
|
1740
|
+
isShared: true
|
|
1555
1741
|
});
|
|
1556
1742
|
const createMultiEditTableCfg = (cakeKey) => ({
|
|
1557
1743
|
key: `${cakeKey}MultiEdits`,
|
|
@@ -1578,7 +1764,10 @@ const createMultiEditTableCfg = (cakeKey) => ({
|
|
|
1578
1764
|
tableKey: `${cakeKey}Edits`
|
|
1579
1765
|
}
|
|
1580
1766
|
}
|
|
1581
|
-
]
|
|
1767
|
+
],
|
|
1768
|
+
isHead: false,
|
|
1769
|
+
isRoot: false,
|
|
1770
|
+
isShared: true
|
|
1582
1771
|
});
|
|
1583
1772
|
const objectDepth = (o) => Object(o) === o ? 1 + Math.max(-1, ...Object.values(o).map(objectDepth)) : 0;
|
|
1584
1773
|
class InsertValidator {
|
|
@@ -1706,7 +1895,11 @@ const createInsertHistoryTableCfg = (tableCfg) => ({
|
|
|
1706
1895
|
key: `${tableCfg.key}Ref`,
|
|
1707
1896
|
type: "string",
|
|
1708
1897
|
titleLong: "Reference",
|
|
1709
|
-
titleShort: "Ref"
|
|
1898
|
+
titleShort: "Ref",
|
|
1899
|
+
ref: {
|
|
1900
|
+
tableKey: `${tableCfg.key}MultiEdits`,
|
|
1901
|
+
type: tableCfg.type
|
|
1902
|
+
}
|
|
1710
1903
|
},
|
|
1711
1904
|
{ key: "route", type: "string", titleLong: "Route", titleShort: "Route" },
|
|
1712
1905
|
{
|
|
@@ -1809,7 +2002,8 @@ const contentTypes = [
|
|
|
1809
2002
|
"insertHistory",
|
|
1810
2003
|
"edits",
|
|
1811
2004
|
"multiEdits",
|
|
1812
|
-
"editHistory"
|
|
2005
|
+
"editHistory",
|
|
2006
|
+
"trees"
|
|
1813
2007
|
];
|
|
1814
2008
|
const exampleTypedefs = () => {
|
|
1815
2009
|
return {
|
|
@@ -1884,6 +2078,10 @@ class _BaseValidator {
|
|
|
1884
2078
|
() => this._rootOrHeadTableHasNoIdColumn(),
|
|
1885
2079
|
// Check references
|
|
1886
2080
|
() => this._refsNotFound(),
|
|
2081
|
+
// Check trees
|
|
2082
|
+
() => this._treeChildNodesNotFound(),
|
|
2083
|
+
() => this._treeDuplicateNodeIdsAsSibling(),
|
|
2084
|
+
() => this._treeIsNotParentButHasChildren(),
|
|
1887
2085
|
// Check layers
|
|
1888
2086
|
() => this._layerBasesNotFound(),
|
|
1889
2087
|
() => this._layerSliceIdsTableNotFound(),
|
|
@@ -2305,9 +2503,112 @@ class _BaseValidator {
|
|
|
2305
2503
|
}
|
|
2306
2504
|
}
|
|
2307
2505
|
// ...........................................................................
|
|
2506
|
+
_treeChildNodesNotFound() {
|
|
2507
|
+
const brokenTrees = [];
|
|
2508
|
+
iterateTablesSync(this.rljson, (tableKey, table) => {
|
|
2509
|
+
if (table._type !== "trees") {
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const treesTable = table;
|
|
2513
|
+
for (const tree of treesTable._data) {
|
|
2514
|
+
const childIds = tree.children;
|
|
2515
|
+
if (!childIds) {
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
for (const childId of childIds) {
|
|
2519
|
+
const childNode = treesTable._data.find(
|
|
2520
|
+
(n) => n._hash === childId
|
|
2521
|
+
);
|
|
2522
|
+
if (!childNode) {
|
|
2523
|
+
brokenTrees.push({
|
|
2524
|
+
treesTable: tableKey,
|
|
2525
|
+
brokenTree: tree._hash,
|
|
2526
|
+
missingChildNode: childId
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
if (brokenTrees.length > 0) {
|
|
2533
|
+
this.errors.treeChildNodesNotFound = {
|
|
2534
|
+
error: "Child nodes are missing",
|
|
2535
|
+
brokenTrees
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
// ...........................................................................
|
|
2540
|
+
_treeDuplicateNodeIdsAsSibling() {
|
|
2541
|
+
const treesWithDuplicateSiblingIds = [];
|
|
2542
|
+
iterateTablesSync(this.rljson, (tableKey, table) => {
|
|
2543
|
+
if (table._type !== "trees") {
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
const treesTable = table;
|
|
2547
|
+
for (const tree of treesTable._data) {
|
|
2548
|
+
const childIds = tree.children;
|
|
2549
|
+
if (!childIds) {
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2553
|
+
const duplicateIds = /* @__PURE__ */ new Set();
|
|
2554
|
+
for (const childId of childIds) {
|
|
2555
|
+
if (seenIds.has(childId)) {
|
|
2556
|
+
duplicateIds.add(childId);
|
|
2557
|
+
} else {
|
|
2558
|
+
seenIds.add(childId);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (duplicateIds.size > 0) {
|
|
2562
|
+
treesWithDuplicateSiblingIds.push({
|
|
2563
|
+
treesTable: tableKey,
|
|
2564
|
+
tree: tree._hash,
|
|
2565
|
+
duplicateChildIds: Array.from(duplicateIds)
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
if (treesWithDuplicateSiblingIds.length > 0) {
|
|
2571
|
+
this.errors.treeDuplicateNodeIdsAsSibling = {
|
|
2572
|
+
error: "Trees have duplicate sibling node IDs",
|
|
2573
|
+
trees: treesWithDuplicateSiblingIds
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
// ...........................................................................
|
|
2578
|
+
_treeIsNotParentButHasChildren() {
|
|
2579
|
+
const invalidTrees = [];
|
|
2580
|
+
iterateTablesSync(this.rljson, (tableKey, table) => {
|
|
2581
|
+
if (table._type !== "trees") {
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
const treesTable = table;
|
|
2585
|
+
for (const tree of treesTable._data) {
|
|
2586
|
+
const isParent = tree.isParent;
|
|
2587
|
+
const childIds = tree.children;
|
|
2588
|
+
if (!isParent && childIds && childIds.length > 0) {
|
|
2589
|
+
invalidTrees.push({
|
|
2590
|
+
treesTable: tableKey,
|
|
2591
|
+
tree: tree._hash,
|
|
2592
|
+
isParent,
|
|
2593
|
+
children: childIds
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
if (invalidTrees.length > 0) {
|
|
2599
|
+
this.errors.treeIsNotParentButHasChildren = {
|
|
2600
|
+
error: "Trees marked as non-parents have children",
|
|
2601
|
+
trees: invalidTrees
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
// ...........................................................................
|
|
2308
2606
|
_layerBasesNotFound() {
|
|
2309
2607
|
const brokenLayers = [];
|
|
2310
2608
|
iterateTablesSync(this.rljson, (tableKey, table) => {
|
|
2609
|
+
if (table._type !== "layers") {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2311
2612
|
const layersIndexed = this.rljsonIndexed[tableKey];
|
|
2312
2613
|
const layersTable = table;
|
|
2313
2614
|
for (const layer of layersTable._data) {
|
|
@@ -2667,6 +2968,7 @@ export {
|
|
|
2667
2968
|
createLayerTableCfg,
|
|
2668
2969
|
createMultiEditTableCfg,
|
|
2669
2970
|
createSliceIdsTableCfg,
|
|
2971
|
+
createTreesTableCfg,
|
|
2670
2972
|
exampleBuffetsTable,
|
|
2671
2973
|
exampleCakesTable,
|
|
2672
2974
|
exampleComponentsTable,
|
|
@@ -2678,6 +2980,7 @@ export {
|
|
|
2678
2980
|
exampleSliceIdsTable,
|
|
2679
2981
|
exampleTableCfg,
|
|
2680
2982
|
exampleTableCfgTable,
|
|
2983
|
+
exampleTreesTable,
|
|
2681
2984
|
exampleTypedefs,
|
|
2682
2985
|
getTimeIdTimestamp,
|
|
2683
2986
|
getTimeIdUniquePart,
|
|
@@ -2693,6 +2996,7 @@ export {
|
|
|
2693
2996
|
routeSliceIdSeperator,
|
|
2694
2997
|
throwOnInvalidTableCfg,
|
|
2695
2998
|
timeId,
|
|
2999
|
+
treeFromObject,
|
|
2696
3000
|
validateInsert,
|
|
2697
3001
|
validateRljsonAgainstTableCfg
|
|
2698
3002
|
};
|
package/dist/src/example.ts
CHANGED
|
@@ -13,9 +13,11 @@ import { ComponentsTable } from './content/components.ts';
|
|
|
13
13
|
import { Layer, LayersTable } from './content/layer.ts';
|
|
14
14
|
import { SliceIdsTable } from './content/slice-ids.ts';
|
|
15
15
|
import { ColumnCfg, TablesCfgTable } from './content/table-cfg.ts';
|
|
16
|
+
import { exampleTreesTable, TreesTable } from './content/tree.ts';
|
|
16
17
|
import { bakeryExample } from './example/bakery-example.ts';
|
|
17
18
|
import { Rljson } from './rljson.ts';
|
|
18
19
|
|
|
20
|
+
|
|
19
21
|
export class Example {
|
|
20
22
|
static readonly ok = {
|
|
21
23
|
bakery: (): Rljson => bakeryExample(),
|
|
@@ -371,6 +373,11 @@ export class Example {
|
|
|
371
373
|
} as ComponentsTable<Json>,
|
|
372
374
|
};
|
|
373
375
|
},
|
|
376
|
+
tree: (): Rljson => {
|
|
377
|
+
return {
|
|
378
|
+
recipesTreeTable: exampleTreesTable(),
|
|
379
|
+
};
|
|
380
|
+
},
|
|
374
381
|
complete: (): Rljson => {
|
|
375
382
|
const sliceIds = hip<SliceIdsTable>({
|
|
376
383
|
_type: 'sliceIds',
|
|
@@ -751,6 +758,56 @@ export class Example {
|
|
|
751
758
|
},
|
|
752
759
|
},
|
|
753
760
|
|
|
761
|
+
trees: {
|
|
762
|
+
missingChildNodes: (): Rljson => {
|
|
763
|
+
const result = Example.ok.tree();
|
|
764
|
+
const treeTable = result.recipesTreeTable as TreesTable;
|
|
765
|
+
|
|
766
|
+
treeTable._data.pop(); // Remove child node from _data array
|
|
767
|
+
|
|
768
|
+
return hip(result, {
|
|
769
|
+
updateExistingHashes: true,
|
|
770
|
+
throwOnWrongHashes: false,
|
|
771
|
+
});
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
cyclicTree: (): Rljson => {
|
|
775
|
+
const result = Example.ok.tree();
|
|
776
|
+
const treeTable = result.recipesTreeTable as TreesTable;
|
|
777
|
+
|
|
778
|
+
// Introduce a cycle
|
|
779
|
+
treeTable._data[0].children = [treeTable._data[0]._hash as string];
|
|
780
|
+
|
|
781
|
+
return { recipesTreeTable: treeTable } as Rljson;
|
|
782
|
+
},
|
|
783
|
+
|
|
784
|
+
duplicateChildNodeIds: (): Rljson => {
|
|
785
|
+
const result = Example.ok.tree();
|
|
786
|
+
const treeTable = result.recipesTreeTable as TreesTable;
|
|
787
|
+
|
|
788
|
+
// Introduce duplicate child node ids
|
|
789
|
+
const firstChildHash = treeTable._data[1]._hash as string;
|
|
790
|
+
treeTable._data[0].children = [firstChildHash, firstChildHash];
|
|
791
|
+
|
|
792
|
+
return hip(result, {
|
|
793
|
+
updateExistingHashes: true,
|
|
794
|
+
throwOnWrongHashes: false,
|
|
795
|
+
});
|
|
796
|
+
},
|
|
797
|
+
|
|
798
|
+
nonParentWithChildren: (): Rljson => {
|
|
799
|
+
const result = Example.ok.tree();
|
|
800
|
+
const treeTable = result.recipesTreeTable as TreesTable;
|
|
801
|
+
|
|
802
|
+
// Make a non-parent have children
|
|
803
|
+
treeTable._data[0].isParent = false;
|
|
804
|
+
|
|
805
|
+
return hip(result, {
|
|
806
|
+
updateExistingHashes: true,
|
|
807
|
+
throwOnWrongHashes: false,
|
|
808
|
+
});
|
|
809
|
+
},
|
|
810
|
+
},
|
|
754
811
|
layers: {
|
|
755
812
|
missingBase: (): Rljson => {
|
|
756
813
|
const result = Example.ok.complete();
|
package/dist/tools/time-id.d.ts
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
+
export type TimeId = string;
|
|
1
2
|
/**
|
|
2
3
|
* Generates a new TimeId.
|
|
3
|
-
* A TimeId
|
|
4
|
-
* - "xxxx" is a 4-character unique identifier
|
|
4
|
+
* A TimeId has the format "timestamp:xxxx" where:
|
|
5
5
|
* - "timestamp" is the current time in milliseconds since epoch
|
|
6
|
+
* - "xxxx" is a 4-character unique identifier
|
|
6
7
|
* @returns A new TimeId string
|
|
7
8
|
*/
|
|
8
|
-
export declare const timeId: () =>
|
|
9
|
+
export declare const timeId: () => TimeId;
|
|
9
10
|
/**
|
|
10
11
|
* Checks if a given id is a valid TimeId.
|
|
11
|
-
* A
|
|
12
|
-
* - "
|
|
13
|
-
* - "
|
|
12
|
+
* A TimeId has the format "timestamp:xxxx" where:
|
|
13
|
+
* - "timestamp" is the current time in milliseconds since epoch
|
|
14
|
+
* - "xxxx" is a 4-character unique identifier
|
|
14
15
|
* @param id - The id to check
|
|
15
16
|
* @returns True if the id is a valid TimeId, false otherwise
|
|
16
17
|
*/
|
|
17
|
-
export declare const isTimeId: (id:
|
|
18
|
+
export declare const isTimeId: (id: TimeId) => boolean;
|
|
18
19
|
/**
|
|
19
20
|
* Extracts the timestamp from a TimeId.
|
|
20
21
|
* @param id - The TimeId string
|
|
21
22
|
* @returns The timestamp in milliseconds since epoch, or null if the id is not a valid TimeId
|
|
22
23
|
*/
|
|
23
|
-
export declare const getTimeIdTimestamp: (id:
|
|
24
|
+
export declare const getTimeIdTimestamp: (id: TimeId) => number | null;
|
|
24
25
|
/**
|
|
25
26
|
* Extracts the unique part from a TimeId.
|
|
26
27
|
* @param id - The TimeId string
|
|
27
28
|
* @returns The unique identifier part, or null if the id is not a valid TimeId
|
|
28
29
|
*/
|
|
29
|
-
export declare const getTimeIdUniquePart: (id:
|
|
30
|
+
export declare const getTimeIdUniquePart: (id: TimeId) => string | null;
|
package/dist/typedefs.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type ColumnKey = JsonKey;
|
|
|
26
26
|
* - `ids` Tables containing slice ids
|
|
27
27
|
* - `components` Tables containing slice components
|
|
28
28
|
*/
|
|
29
|
-
export declare const contentTypes: readonly ["buffets", "cakes", "layers", "sliceIds", "components", "revisions", "tableCfgs", "insertHistory", "edits", "multiEdits", "editHistory"];
|
|
29
|
+
export declare const contentTypes: readonly ["buffets", "cakes", "layers", "sliceIds", "components", "revisions", "tableCfgs", "insertHistory", "edits", "multiEdits", "editHistory", "trees"];
|
|
30
30
|
export type ContentType = (typeof contentTypes)[number];
|
|
31
31
|
/**
|
|
32
32
|
* An example object using the typedefs
|
|
@@ -18,6 +18,9 @@ export interface BaseErrors extends Errors {
|
|
|
18
18
|
rootOrHeadTableHasNoIdColumn?: Json;
|
|
19
19
|
tableCfgHasRootHeadSharedError?: Json;
|
|
20
20
|
refsNotFound?: Json;
|
|
21
|
+
treeChildNodesNotFound?: Json;
|
|
22
|
+
treeDuplicateNodeIdsAsSibling?: Json;
|
|
23
|
+
treeIsNotParentButHasChildren?: Json;
|
|
21
24
|
layerBasesNotFound?: Json;
|
|
22
25
|
layerSliceIdsGivenButNoTable?: Json;
|
|
23
26
|
layerSliceIdsTableNotFound?: Json;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rljson/rljson",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.74",
|
|
4
4
|
"description": "The RLJSON data format specification",
|
|
5
5
|
"homepage": "https://github.com/rljson/rljson",
|
|
6
6
|
"bugs": "https://github.com/rljson/rljson/issues",
|
|
@@ -20,28 +20,28 @@
|
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
24
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
25
|
-
"@typescript-eslint/parser": "^8.
|
|
26
|
-
"@vitest/coverage-v8": "^4.0.
|
|
23
|
+
"@types/node": "^25.0.9",
|
|
24
|
+
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
25
|
+
"@typescript-eslint/parser": "^8.53.1",
|
|
26
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
27
27
|
"cross-env": "^10.1.0",
|
|
28
|
-
"eslint": "^9.39.
|
|
29
|
-
"eslint-plugin-jsdoc": "^
|
|
28
|
+
"eslint": "^9.39.2",
|
|
29
|
+
"eslint-plugin-jsdoc": "^62.2.0",
|
|
30
30
|
"eslint-plugin-tsdoc": "^0.5.0",
|
|
31
|
-
"globals": "^
|
|
31
|
+
"globals": "^17.0.0",
|
|
32
32
|
"jsdoc": "^4.0.5",
|
|
33
33
|
"read-pkg": "^10.0.0",
|
|
34
34
|
"typescript": "~5.9.3",
|
|
35
|
-
"typescript-eslint": "^8.
|
|
36
|
-
"vite": "^7.
|
|
37
|
-
"vite-node": "^5.
|
|
35
|
+
"typescript-eslint": "^8.53.1",
|
|
36
|
+
"vite": "^7.3.1",
|
|
37
|
+
"vite-node": "^5.3.0",
|
|
38
38
|
"vite-plugin-dts": "^4.5.4",
|
|
39
|
-
"vite-tsconfig-paths": "^
|
|
40
|
-
"vitest": "^4.0.
|
|
39
|
+
"vite-tsconfig-paths": "^6.0.4",
|
|
40
|
+
"vitest": "^4.0.17",
|
|
41
41
|
"vitest-dom": "^0.1.1"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@rljson/hash": "^0.0.
|
|
44
|
+
"@rljson/hash": "^0.0.18",
|
|
45
45
|
"@rljson/json": "^0.0.23",
|
|
46
46
|
"nanoid": "^5.1.6"
|
|
47
47
|
},
|