@rljson/rljson 0.0.61 → 0.0.64

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.
@@ -43,7 +43,7 @@ export type CakesTable = RljsonTable<Cake, 'cakes'>;
43
43
  /**
44
44
  * Sample Table as BoilerPlate for Tests and Examples
45
45
  */
46
- /**
46
+ /**pnpm build
47
47
  * Sample Table as BoilerPlate for Tests and Examples
48
48
  * @param cakeKey - the key of the cake table cfg
49
49
  */
@@ -17,6 +17,23 @@ export interface ColumnCfg extends Json {
17
17
  * The type of the column
18
18
  */
19
19
  type: JsonValueType;
20
+ /**
21
+ * An optional long title of the column
22
+ */
23
+ titleLong: string;
24
+ /**
25
+ * An optional short title of the column
26
+ */
27
+ titleShort: string;
28
+ }
29
+ /**
30
+ * A column configuration with route
31
+ */
32
+ export interface ColumnCfgWithRoute extends ColumnCfg {
33
+ /**
34
+ * An optional route for reference columns
35
+ */
36
+ route: string;
20
37
  }
21
38
  /**
22
39
  * Describes the configuration of a table, i.e. table metadata and columns
@@ -0,0 +1,24 @@
1
+ import { TableCfg } from '../content/table-cfg.ts';
2
+ import { RljsonTable } from '../rljson.ts';
3
+ import { RouteRef } from '../route/route.ts';
4
+ import { Ref } from '../typedefs.ts';
5
+ export type HistoryTimeId = string;
6
+ export type HistoryRow<Str extends string> = {
7
+ [key in Str as `${Uncapitalize<string & key>}Ref`]: string;
8
+ } & {
9
+ timeId: HistoryTimeId;
10
+ route: RouteRef;
11
+ origin?: Ref;
12
+ previous?: HistoryTimeId[];
13
+ };
14
+ export type History<Str extends string> = RljsonTable<HistoryRow<Str>, 'history'>;
15
+ /**
16
+ * Creates a TableCfg for a History table for the given table configuration
17
+ * @param tableCfg - The table configuration to create the History table for
18
+ * @returns The TableCfg for the History table
19
+ */
20
+ export declare const createHistoryTableCfg: (tableCfg: TableCfg) => TableCfg;
21
+ /**
22
+ * Provides an example history table for test purposes
23
+ */
24
+ export declare const exampleHistoryTable: () => History<any>;
@@ -0,0 +1,47 @@
1
+ import { Json } from '@rljson/json';
2
+ import { Errors } from '../validate/validate.ts';
3
+ import { Insert } from './insert.ts';
4
+ export interface InsertErrors extends Errors {
5
+ invalidObject?: Json;
6
+ parameterInvalid?: Json;
7
+ parameterRouteInvalid?: Json;
8
+ parameterCommandInvalid?: Json;
9
+ parameterValueInvalid?: Json;
10
+ parameterOriginInvalid?: Json;
11
+ parameterPreviousInvalid?: Json;
12
+ routeInvalid?: Json;
13
+ dataRouteMismatch?: Json;
14
+ }
15
+ export declare class InsertValidator<T extends Json> {
16
+ private _insert;
17
+ errors: InsertErrors;
18
+ /**
19
+ * Indicates whether there are any errors
20
+ * @returns boolean - True if there are errors, false otherwise
21
+ */
22
+ get hasErrors(): boolean;
23
+ constructor(_insert: Insert<T>);
24
+ /**
25
+ * Validates the Insert object
26
+ * @returns InsertErrors - The errors found during validation
27
+ */
28
+ validate(): InsertErrors;
29
+ /**
30
+ * Recursively checks that all reference keys in the value match the route segments
31
+ * @param route - The current route segment
32
+ * @param value - The current value object
33
+ */
34
+ private _checkMatchingRefs;
35
+ /**
36
+ * Creates an InsertValidator for the given Insert object
37
+ * @param insert - The Insert object to validate
38
+ * @returns InsertValidator - The InsertValidator instance
39
+ */
40
+ static create(insert: Insert<any>): InsertValidator<any>;
41
+ }
42
+ /**
43
+ * Validates the given Insert object
44
+ * @param insert - The Insert object to validate
45
+ * @returns InsertErrors - The errors found during validation
46
+ */
47
+ export declare const validateInsert: (insert: Insert<any>) => InsertErrors;
@@ -0,0 +1,19 @@
1
+ import { Json } from '@rljson/json';
2
+ import { RouteRef } from '../route/route.ts';
3
+ import { Ref } from '../typedefs.ts';
4
+ /**
5
+ * An Insert Object describing inserts on data basically
6
+ */
7
+ export type InsertRef = Ref;
8
+ export type InsertCommand = 'add' | 'remove';
9
+ /**
10
+ * An Insert describes a change to be applied to an Rljson object.
11
+ * @param T - The type of the value being edited, extending Json
12
+ */
13
+ export type Insert<T extends Json> = {
14
+ command: InsertCommand;
15
+ value: T;
16
+ route: RouteRef;
17
+ origin?: Ref;
18
+ acknowledged?: boolean;
19
+ };
@@ -4,7 +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 { EditProtocol } from '../edit/edit.ts';
7
+ import { History } from '../edit/history.ts';
8
8
  import { Rljson } from '../rljson.ts';
9
9
  import { Ref } from '../typedefs.ts';
10
10
  export interface Ingredient extends Json {
@@ -32,6 +32,6 @@ export interface Bakery extends Rljson {
32
32
  recipeIngredients: ComponentsTable<RecipIngredient>;
33
33
  ingredients: ComponentsTable<Ingredient>;
34
34
  nutritionalValues: ComponentsTable<NutritionalValues>;
35
- ingredientsEdits: EditProtocol<'Ingredients'>;
35
+ ingredientsHistory: History<'Ingredients'>;
36
36
  }
37
37
  export declare const bakeryExample: () => Bakery;
package/dist/index.d.ts CHANGED
@@ -5,11 +5,13 @@ 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 './edit/edit.ts';
9
- export * from './edit/route.ts';
8
+ export * from './edit/history.ts';
9
+ export * from './edit/insert-validator.ts';
10
+ export * from './edit/insert.ts';
10
11
  export * from './example.ts';
11
12
  export * from './example/bakery-example.ts';
12
13
  export * from './rljson.ts';
14
+ export * from './route/route.ts';
13
15
  export * from './tools/remove-duplicates.ts';
14
16
  export * from './tools/time-id.ts';
15
17
  export * from './typedefs.ts';
package/dist/rljson.d.ts CHANGED
@@ -6,14 +6,14 @@ 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 { EditProtocol } from './edit/edit.ts';
9
+ import { History } from './edit/history.ts';
10
10
  import { ContentType, Ref, TableKey } from './typedefs.ts';
11
11
  export declare const reservedFieldNames: string[];
12
12
  export declare const reservedTableKeys: string[];
13
13
  /**
14
14
  * One of the supported Rljson table types
15
15
  */
16
- export type TableType = BuffetsTable | ComponentsTable<any> | LayersTable | SliceIdsTable | CakesTable | RevisionsTable | TablesCfgTable | EditProtocol<any>;
16
+ export type TableType = BuffetsTable | ComponentsTable<any> | LayersTable | SliceIdsTable | CakesTable | RevisionsTable | TablesCfgTable | History<any>;
17
17
  /** The rljson data format */
18
18
  export interface Rljson extends Json {
19
19
  [tableId: TableKey]: TableType;
package/dist/rljson.js CHANGED
@@ -3,36 +3,166 @@ import { exampleJsonObject, jsonValueTypes, jsonValueMatchesType, jsonValueType
3
3
  import { nanoid } from "nanoid";
4
4
  // @license
5
5
  class Route {
6
- constructor(segments) {
7
- this.segments = segments;
6
+ constructor(_segments) {
7
+ this._segments = _segments;
8
8
  }
9
+ // .............................................................................
10
+ /**
11
+ * Creates a Route from a flat string representation.
12
+ * @param route - The flat string representation of the route (e.g. "/a/b/c")
13
+ * @returns A Route object
14
+ */
9
15
  static fromFlat(route) {
10
- return new Route(route.split("/").filter(Boolean));
16
+ const segmentsFlat = route.split("/").filter((s) => s.length > 0);
17
+ const segments = [];
18
+ for (const segmentFlat of segmentsFlat) {
19
+ const [tableKey, refFlat] = segmentFlat.split("@");
20
+ const ref = !!refFlat ? refFlat.split(":").length == 2 ? { [tableKey + "HistoryRef"]: refFlat } : { [tableKey + "Ref"]: refFlat } : {};
21
+ const segment = {
22
+ tableKey,
23
+ ...ref
24
+ };
25
+ segments.push(segment);
26
+ }
27
+ return new Route(segments);
11
28
  }
29
+ // .............................................................................
30
+ /**
31
+ * Returns the segment at the given index or the last segment if no index is provided.
32
+ * @param index - The index of the segment to return (optional)
33
+ * @returns The segment at the given index or the last segment
34
+ */
12
35
  segment(index) {
13
36
  if (index === void 0) {
14
- return this.segments[this.segments.length - 1];
37
+ return this._segments[this._segments.length - 1];
15
38
  }
16
- return this.segments[index];
39
+ return this._segments[index];
17
40
  }
41
+ // .............................................................................
42
+ /**
43
+ * Returns a new Route that is one level deeper than the current route.
44
+ * If steps is provided, it returns a new Route that is 'steps' levels deeper.
45
+ * @param steps - The number of levels to go deeper (optional)
46
+ * @returns A new Route that is one level deeper or 'steps' levels deeper
47
+ */
18
48
  deeper(steps) {
19
49
  if (steps === void 0) {
20
- return new Route(this.segments.slice(1, this.segments.length));
50
+ return new Route(this._segments.slice(1, this._segments.length));
21
51
  } else {
22
52
  if (steps < 1) {
23
53
  throw new Error("Steps must be greater than 0");
24
54
  }
25
- if (steps >= this.segments.length) {
55
+ if (steps >= this._segments.length) {
26
56
  throw new Error("Cannot go deeper than the root");
27
57
  }
28
- return new Route(this.segments.slice(steps, this.segments.length));
58
+ return new Route(this._segments.slice(steps, this._segments.length));
29
59
  }
30
60
  }
61
+ // .............................................................................
62
+ /**
63
+ * Checks if the current route is the root route.
64
+ * @returns True if the current route is the root route, false otherwise
65
+ */
31
66
  get isRoot() {
32
- return this.segments.length === 1;
67
+ return this._segments.length === 1;
33
68
  }
69
+ // .............................................................................
70
+ /**
71
+ * Returns the flat string representation of the route.
72
+ * @returns The flat string representation of the route (e.g. "/a/b/c")
73
+ */
34
74
  get flat() {
35
- return "/" + this.segments.join("/");
75
+ let flat = "";
76
+ for (const segment of this._segments) {
77
+ const tableKey = segment.tableKey;
78
+ const ref = Object.keys(segment).filter((k) => k !== "tableKey")[0];
79
+ flat += `/${tableKey}${ref ? `@${segment[ref]}` : ""}`;
80
+ }
81
+ return flat;
82
+ }
83
+ // .............................................................................
84
+ /**
85
+ * Returns the segments of the route as an array of strings.
86
+ * @returns The segments of the route as an array of strings
87
+ */
88
+ get segments() {
89
+ return this._segments;
90
+ }
91
+ // .............................................................................
92
+ /**
93
+ * Returns the root segment of the route.
94
+ * @returns The root segment of the route
95
+ */
96
+ get root() {
97
+ return this.segment();
98
+ }
99
+ // .............................................................................
100
+ /**
101
+ * Returns the top level segment of the route.
102
+ * @returns The top level segment of the route
103
+ */
104
+ get top() {
105
+ return this.segment(0);
106
+ }
107
+ // .............................................................................
108
+ /**
109
+ * Returns the next segment of the route if it exists.
110
+ * @returns The next segment of the route or undefined if it doesn't exist
111
+ */
112
+ get next() {
113
+ return this._segments.length > 1 ? this._segments[1] : void 0;
114
+ }
115
+ // .............................................................................
116
+ /**
117
+ * Checks if the route is valid (i.e. has at least one segment and no empty segments).
118
+ * @returns True if the route is valid, false otherwise
119
+ */
120
+ get isValid() {
121
+ return this._segments.length > 0 && this._segments.every((s) => s.tableKey.length > 0);
122
+ }
123
+ // .............................................................................
124
+ /**
125
+ * Returns the reference of a segment if it exists.
126
+ * @param segment - The segment to get the reference from
127
+ * @returns The reference of the segment or undefined if it doesn't exist
128
+ */
129
+ static segmentRef(segment) {
130
+ if (this.segmentHasRef(segment)) {
131
+ const refKey = Object.keys(segment).find(
132
+ (k) => k.endsWith("Ref") && k !== "tableKey"
133
+ );
134
+ if (refKey) {
135
+ return segment[refKey];
136
+ }
137
+ }
138
+ return void 0;
139
+ }
140
+ // .............................................................................
141
+ /**
142
+ * Checks if a segment has any reference (either default or history).
143
+ * @param segment - The segment to check
144
+ * @returns True if the segment has any reference, false otherwise
145
+ */
146
+ static segmentHasRef(segment) {
147
+ return Object.keys(segment).some((k) => k.endsWith("Ref"));
148
+ }
149
+ // .............................................................................
150
+ /**
151
+ * Checks if a segment has a default reference (i.e. not a history reference).
152
+ * @param segment - The segment to check
153
+ * @returns True if the segment has a default reference, false otherwise
154
+ */
155
+ static segmentHasDefaultRef(segment) {
156
+ return this.segmentHasRef(segment) && !this.segmentHasHistoryRef(segment);
157
+ }
158
+ // .............................................................................
159
+ /**
160
+ * Checks if a segment has a history reference (i.e. an HistoryRef).
161
+ * @param segment - The segment to check
162
+ * @returns True if the segment has a history reference, false otherwise
163
+ */
164
+ static segmentHasHistoryRef(segment) {
165
+ return this.segmentHasRef(segment) && Object.keys(segment).some((k) => k.endsWith("HistoryRef"));
36
166
  }
37
167
  }
38
168
  // @license
@@ -163,8 +293,8 @@ const bakeryExample = () => {
163
293
  }
164
294
  ]
165
295
  });
166
- const ingredientsEdits = hip({
167
- _type: "edits",
296
+ const ingredientsHistory = hip({
297
+ _type: "history",
168
298
  _data: [
169
299
  {
170
300
  timeId: "de72:1759123957292",
@@ -193,7 +323,7 @@ const bakeryExample = () => {
193
323
  recipeIngredients,
194
324
  ingredients,
195
325
  nutritionalValues,
196
- ingredientsEdits
326
+ ingredientsHistory
197
327
  };
198
328
  return result;
199
329
  };
@@ -204,10 +334,21 @@ const createCakeTableCfg = (cakeKey) => ({
204
334
  key: cakeKey,
205
335
  type: "cakes",
206
336
  columns: [
207
- { key: "sliceIdsTable", type: "string" },
208
- { key: "sliceIdsRow", type: "string" },
209
- { key: "layers", type: "jsonArray" },
210
- { key: "id", type: "string" }
337
+ { key: "_hash", type: "string", titleLong: "Hash", titleShort: "Hash" },
338
+ {
339
+ key: "sliceIdsTable",
340
+ type: "string",
341
+ titleLong: "Slice Ids Table",
342
+ titleShort: "Slice Ids Table"
343
+ },
344
+ {
345
+ key: "sliceIdsRow",
346
+ type: "string",
347
+ titleLong: "Slice Ids Row",
348
+ titleShort: "Slice Ids Row"
349
+ },
350
+ { key: "layers", type: "json", titleLong: "Layers", titleShort: "Layers" },
351
+ { key: "id", type: "string", titleLong: "ID", titleShort: "ID" }
211
352
  ],
212
353
  isHead: false,
213
354
  isRoot: false,
@@ -221,12 +362,28 @@ const createLayerTableCfg = (layerKey) => ({
221
362
  key: layerKey,
222
363
  type: "layers",
223
364
  columns: [
224
- { key: "base", type: "string" },
225
- { key: "sliceIdsTable", type: "string" },
226
- { key: "sliceIdsTableRow", type: "string" },
227
- { key: "componentsTable", type: "string" },
228
- { key: "add", type: "jsonArray" },
229
- { key: "remove", type: "jsonArray" }
365
+ { key: "_hash", type: "string", titleLong: "Hash", titleShort: "Hash" },
366
+ { key: "base", type: "string", titleLong: "Base", titleShort: "Base" },
367
+ {
368
+ key: "sliceIdsTable",
369
+ type: "string",
370
+ titleLong: "Slice Ids Table",
371
+ titleShort: "Slice Ids Table"
372
+ },
373
+ {
374
+ key: "sliceIdsTableRow",
375
+ type: "string",
376
+ titleLong: "Slice Ids Table Row",
377
+ titleShort: "Slice Ids Table Row"
378
+ },
379
+ {
380
+ key: "componentsTable",
381
+ type: "string",
382
+ titleLong: "Components Table",
383
+ titleShort: "Components Table"
384
+ },
385
+ { key: "add", type: "json", titleLong: "Add", titleShort: "Add" },
386
+ { key: "remove", type: "json", titleLong: "Remove", titleShort: "Remove" }
230
387
  ],
231
388
  isHead: false,
232
389
  isRoot: false,
@@ -279,39 +436,57 @@ class Example {
279
436
  columns: [
280
437
  {
281
438
  key: "_hash",
282
- type: "string"
439
+ type: "string",
440
+ titleLong: "Hash",
441
+ titleShort: "Hash"
283
442
  },
284
443
  {
285
444
  key: "int",
286
- type: "number"
445
+ type: "number",
446
+ titleLong: "Integer",
447
+ titleShort: "Int"
287
448
  },
288
449
  {
289
450
  key: "double",
290
- type: "number"
451
+ type: "number",
452
+ titleLong: "Double",
453
+ titleShort: "Double"
291
454
  },
292
455
  {
293
456
  key: "string",
294
- type: "string"
457
+ type: "string",
458
+ titleLong: "String",
459
+ titleShort: "String"
295
460
  },
296
461
  {
297
462
  key: "boolean",
298
- type: "boolean"
463
+ type: "boolean",
464
+ titleLong: "Boolean",
465
+ titleShort: "Boolean"
299
466
  },
300
467
  {
301
468
  key: "null",
302
- type: "string"
469
+ type: "string",
470
+ titleLong: "Null",
471
+ titleShort: "Null"
303
472
  },
304
473
  {
305
474
  key: "jsonArray",
306
- type: "jsonArray"
475
+ type: "jsonArray",
476
+ titleLong: "JSON Array",
477
+ titleShort: "JSONArray"
307
478
  },
308
479
  {
309
480
  key: "json",
310
- type: "json"
481
+ type: "json",
482
+ titleLong: "JSON Object",
483
+ titleShort: "JSONObject"
311
484
  },
312
485
  {
313
486
  key: "jsonValue",
314
- type: "jsonValue"
487
+ type: "jsonValue",
488
+ titleLong: "JSON Value",
489
+ titleShort: "JSONValue"
315
490
  }
316
491
  ]
317
492
  }
@@ -900,14 +1075,20 @@ const exampleTableCfg = (tableCfg = void 0) => {
900
1075
  columns: tableCfg?.columns ?? [
901
1076
  {
902
1077
  key: "_hash",
1078
+ titleLong: "Hash",
1079
+ titleShort: "Hash",
903
1080
  type: "string"
904
1081
  },
905
1082
  {
906
1083
  key: "a",
1084
+ titleLong: "Column A",
1085
+ titleShort: "A",
907
1086
  type: "string"
908
1087
  },
909
1088
  {
910
1089
  key: "b",
1090
+ titleLong: "Column B",
1091
+ titleShort: "B",
911
1092
  type: "number"
912
1093
  }
913
1094
  ],
@@ -918,7 +1099,150 @@ const exampleTableCfg = (tableCfg = void 0) => {
918
1099
  };
919
1100
  };
920
1101
  // @license
921
- const exampleEditsTable = () => bakeryExample().ingredientsEdits;
1102
+ const createHistoryTableCfg = (tableCfg) => ({
1103
+ key: `${tableCfg.key}History`,
1104
+ type: "history",
1105
+ columns: [
1106
+ { key: "_hash", type: "string", titleLong: "Hash", titleShort: "Hash" },
1107
+ {
1108
+ key: "timeId",
1109
+ type: "string",
1110
+ titleLong: "Time ID",
1111
+ titleShort: "Time ID"
1112
+ },
1113
+ {
1114
+ key: `${tableCfg.key}Ref`,
1115
+ type: "string",
1116
+ titleLong: "Reference",
1117
+ titleShort: "Ref"
1118
+ },
1119
+ { key: "route", type: "string", titleLong: "Route", titleShort: "Route" },
1120
+ {
1121
+ key: "origin",
1122
+ type: "string",
1123
+ titleLong: "Origin",
1124
+ titleShort: "Origin"
1125
+ },
1126
+ {
1127
+ key: "previous",
1128
+ type: "jsonArray",
1129
+ titleLong: "Previous",
1130
+ titleShort: "Previous"
1131
+ }
1132
+ ],
1133
+ isHead: false,
1134
+ isRoot: false,
1135
+ isShared: false
1136
+ });
1137
+ const exampleHistoryTable = () => bakeryExample().ingredientsHistory;
1138
+ // @license
1139
+ const objectDepth = (o) => Object(o) === o ? 1 + Math.max(-1, ...Object.values(o).map(objectDepth)) : 0;
1140
+ // @license
1141
+ class InsertValidator {
1142
+ constructor(_insert) {
1143
+ this._insert = _insert;
1144
+ }
1145
+ errors = { hasErrors: false };
1146
+ // ............................................................................
1147
+ /**
1148
+ * Indicates whether there are any errors
1149
+ * @returns boolean - True if there are errors, false otherwise
1150
+ */
1151
+ get hasErrors() {
1152
+ return Object.keys(this.errors).length > 1;
1153
+ }
1154
+ // ............................................................................
1155
+ /**
1156
+ * Validates the Insert object
1157
+ * @returns InsertErrors - The errors found during validation
1158
+ */
1159
+ validate() {
1160
+ if (typeof this._insert.route !== "string" || this._insert.route.length === 0) {
1161
+ this.errors.parameterRouteInvalid = {
1162
+ error: "Insert route must be a non-empty string.",
1163
+ parameter: this._insert.route
1164
+ };
1165
+ }
1166
+ if (!this._insert.command.startsWith("add") && !this._insert.command.startsWith("remove")) {
1167
+ this.errors.parameterCommandInvalid = {
1168
+ error: "Insert command must be starting with either 'add' or 'remove'.",
1169
+ parameter: this._insert.command
1170
+ };
1171
+ }
1172
+ if (typeof this._insert.value === "undefined" || typeof this._insert.value !== "object" || this._insert.value === null) {
1173
+ this.errors.parameterValueInvalid = {
1174
+ error: "Insert value must be a non-null object.",
1175
+ parameter: this._insert.value
1176
+ };
1177
+ }
1178
+ if (typeof this._insert.origin !== "undefined" && typeof this._insert.origin !== "string") {
1179
+ this.errors.parameterOriginInvalid = {
1180
+ error: "Insert origin must be a string if defined.",
1181
+ parameter: this._insert.origin
1182
+ };
1183
+ }
1184
+ const route = Route.fromFlat(this._insert.route);
1185
+ if (!route.isValid) {
1186
+ this.errors.routeInvalid = {
1187
+ error: "Insert route is not valid.",
1188
+ route: this._insert.route
1189
+ };
1190
+ }
1191
+ if (route.isValid) {
1192
+ const routeDepth = route.segments.length;
1193
+ const valueDepth = objectDepth(this._insert.value);
1194
+ if (routeDepth !== valueDepth) {
1195
+ this.errors.dataRouteMismatch = {
1196
+ error: "Insert route depth does not match value depth. Route depth must match the depth of the value object.",
1197
+ route: this._insert.route,
1198
+ routeDepth,
1199
+ valueDepth
1200
+ };
1201
+ }
1202
+ }
1203
+ if (typeof this._insert.value === "object" && this._insert.value !== null) {
1204
+ this._checkMatchingRefs(route, this._insert.value);
1205
+ }
1206
+ this.errors.hasErrors = this.hasErrors;
1207
+ return this.errors;
1208
+ }
1209
+ // ............................................................................
1210
+ /**
1211
+ * Recursively checks that all reference keys in the value match the route segments
1212
+ * @param route - The current route segment
1213
+ * @param value - The current value object
1214
+ */
1215
+ _checkMatchingRefs(route, value) {
1216
+ const next = route.next;
1217
+ const keys = Object.keys(value);
1218
+ for (const key of keys) {
1219
+ if (key.endsWith("Ref") && value[key] !== null && value[key] !== void 0 && typeof value[key] !== "string") {
1220
+ const refObj = value[key];
1221
+ if (!next || next.tableKey !== key.slice(0, -3)) {
1222
+ this.errors.parameterInvalid = {
1223
+ error: `Insert value has a reference key "${key}" that does not match the next route segment.`,
1224
+ parameter: this._insert.value
1225
+ };
1226
+ break;
1227
+ }
1228
+ if (route.deeper().next)
1229
+ this._checkMatchingRefs(route.deeper(), refObj);
1230
+ }
1231
+ }
1232
+ }
1233
+ // ............................................................................
1234
+ /**
1235
+ * Creates an InsertValidator for the given Insert object
1236
+ * @param insert - The Insert object to validate
1237
+ * @returns InsertValidator - The InsertValidator instance
1238
+ */
1239
+ static create(insert) {
1240
+ return new InsertValidator(insert);
1241
+ }
1242
+ }
1243
+ const validateInsert = (insert) => {
1244
+ return InsertValidator.create(insert).validate();
1245
+ };
922
1246
  // @license
923
1247
  const reservedFieldNames = ["_data"];
924
1248
  const reservedTableKeys = [
@@ -977,6 +1301,12 @@ const removeDuplicates = (rljson) => {
977
1301
  const timeId = () => {
978
1302
  return nanoid(4) + ":" + Date.now();
979
1303
  };
1304
+ const isTimeId = (id) => {
1305
+ const parts = id.split(":");
1306
+ if (parts.length !== 2) return false;
1307
+ if (isNaN(Number(parts[1]))) return false;
1308
+ return parts[0].length === 4;
1309
+ };
980
1310
  // @license
981
1311
  const contentTypes = [
982
1312
  "buffets",
@@ -986,7 +1316,7 @@ const contentTypes = [
986
1316
  "components",
987
1317
  "revisions",
988
1318
  "tableCfgs",
989
- "edits"
1319
+ "history"
990
1320
  ];
991
1321
  const exampleTypedefs = () => {
992
1322
  return {
@@ -1876,17 +2206,19 @@ class Validate {
1876
2206
  export {
1877
2207
  BaseValidator,
1878
2208
  Example,
2209
+ InsertValidator,
1879
2210
  Route,
1880
2211
  Validate,
1881
2212
  addColumnsToTableCfg,
1882
2213
  bakeryExample,
1883
2214
  contentTypes,
1884
2215
  createCakeTableCfg,
2216
+ createHistoryTableCfg,
1885
2217
  createLayerTableCfg,
1886
2218
  exampleBuffetsTable,
1887
2219
  exampleCakesTable,
1888
2220
  exampleComponentsTable,
1889
- exampleEditsTable,
2221
+ exampleHistoryTable,
1890
2222
  exampleLayersTable,
1891
2223
  exampleRevision,
1892
2224
  exampleRljson,
@@ -1894,6 +2226,7 @@ export {
1894
2226
  exampleTableCfg,
1895
2227
  exampleTableCfgTable,
1896
2228
  exampleTypedefs,
2229
+ isTimeId,
1897
2230
  isValidFieldName,
1898
2231
  iterateTables,
1899
2232
  iterateTablesSync,
@@ -1902,5 +2235,6 @@ export {
1902
2235
  reservedTableKeys,
1903
2236
  throwOnInvalidTableCfg,
1904
2237
  timeId,
2238
+ validateInsert,
1905
2239
  validateRljsonAgainstTableCfg
1906
2240
  };
@@ -0,0 +1,93 @@
1
+ import { TableKey } from '../typedefs.ts';
2
+ export type RouteRef = string;
3
+ export type RouteSegment<Str extends string> = {
4
+ [key in Str as `${Uncapitalize<string & key>}Ref`]: string;
5
+ } & {
6
+ tableKey: TableKey;
7
+ };
8
+ export type RouteSegmentFlat<N extends string> = `${N}` | `${N}@${string}` | `${N}@${string}:${string}`;
9
+ /**
10
+ * A class to handle routes in an Rljson object.
11
+ */
12
+ export declare class Route {
13
+ private readonly _segments;
14
+ constructor(_segments: RouteSegment<any>[]);
15
+ /**
16
+ * Creates a Route from a flat string representation.
17
+ * @param route - The flat string representation of the route (e.g. "/a/b/c")
18
+ * @returns A Route object
19
+ */
20
+ static fromFlat(route: string): Route;
21
+ /**
22
+ * Returns the segment at the given index or the last segment if no index is provided.
23
+ * @param index - The index of the segment to return (optional)
24
+ * @returns The segment at the given index or the last segment
25
+ */
26
+ segment(index?: number): RouteSegment<any>;
27
+ /**
28
+ * Returns a new Route that is one level deeper than the current route.
29
+ * If steps is provided, it returns a new Route that is 'steps' levels deeper.
30
+ * @param steps - The number of levels to go deeper (optional)
31
+ * @returns A new Route that is one level deeper or 'steps' levels deeper
32
+ */
33
+ deeper(steps?: number): Route;
34
+ /**
35
+ * Checks if the current route is the root route.
36
+ * @returns True if the current route is the root route, false otherwise
37
+ */
38
+ get isRoot(): boolean;
39
+ /**
40
+ * Returns the flat string representation of the route.
41
+ * @returns The flat string representation of the route (e.g. "/a/b/c")
42
+ */
43
+ get flat(): string;
44
+ /**
45
+ * Returns the segments of the route as an array of strings.
46
+ * @returns The segments of the route as an array of strings
47
+ */
48
+ get segments(): RouteSegment<any>[];
49
+ /**
50
+ * Returns the root segment of the route.
51
+ * @returns The root segment of the route
52
+ */
53
+ get root(): RouteSegment<any>;
54
+ /**
55
+ * Returns the top level segment of the route.
56
+ * @returns The top level segment of the route
57
+ */
58
+ get top(): RouteSegment<any>;
59
+ /**
60
+ * Returns the next segment of the route if it exists.
61
+ * @returns The next segment of the route or undefined if it doesn't exist
62
+ */
63
+ get next(): RouteSegment<any> | undefined;
64
+ /**
65
+ * Checks if the route is valid (i.e. has at least one segment and no empty segments).
66
+ * @returns True if the route is valid, false otherwise
67
+ */
68
+ get isValid(): boolean;
69
+ /**
70
+ * Returns the reference of a segment if it exists.
71
+ * @param segment - The segment to get the reference from
72
+ * @returns The reference of the segment or undefined if it doesn't exist
73
+ */
74
+ static segmentRef(segment: RouteSegment<any>): string | undefined;
75
+ /**
76
+ * Checks if a segment has any reference (either default or history).
77
+ * @param segment - The segment to check
78
+ * @returns True if the segment has any reference, false otherwise
79
+ */
80
+ static segmentHasRef(segment: RouteSegment<any>): boolean;
81
+ /**
82
+ * Checks if a segment has a default reference (i.e. not a history reference).
83
+ * @param segment - The segment to check
84
+ * @returns True if the segment has a default reference, false otherwise
85
+ */
86
+ static segmentHasDefaultRef(segment: RouteSegment<any>): boolean;
87
+ /**
88
+ * Checks if a segment has a history reference (i.e. an HistoryRef).
89
+ * @param segment - The segment to check
90
+ * @returns True if the segment has a history reference, false otherwise
91
+ */
92
+ static segmentHasHistoryRef(segment: RouteSegment<any>): boolean;
93
+ }
@@ -16,6 +16,7 @@ import { ColumnCfg, TablesCfgTable } from './content/table-cfg.ts';
16
16
  import { bakeryExample } from './example/bakery-example.ts';
17
17
  import { Rljson } from './rljson.ts';
18
18
 
19
+
19
20
  export class Example {
20
21
  static readonly ok = {
21
22
  bakery: (): Rljson => bakeryExample(),
@@ -55,38 +56,56 @@ export class Example {
55
56
  {
56
57
  key: '_hash',
57
58
  type: 'string',
59
+ titleLong: 'Hash',
60
+ titleShort: 'Hash',
58
61
  },
59
62
  {
60
63
  key: 'int',
61
64
  type: 'number',
65
+ titleLong: 'Integer',
66
+ titleShort: 'Int',
62
67
  },
63
68
  {
64
69
  key: 'double',
65
70
  type: 'number',
71
+ titleLong: 'Double',
72
+ titleShort: 'Double',
66
73
  },
67
74
  {
68
75
  key: 'string',
69
76
  type: 'string',
77
+ titleLong: 'String',
78
+ titleShort: 'String',
70
79
  },
71
80
  {
72
81
  key: 'boolean',
73
82
  type: 'boolean',
83
+ titleLong: 'Boolean',
84
+ titleShort: 'Boolean',
74
85
  },
75
86
  {
76
87
  key: 'null',
77
88
  type: 'string',
89
+ titleLong: 'Null',
90
+ titleShort: 'Null',
78
91
  },
79
92
  {
80
93
  key: 'jsonArray',
81
94
  type: 'jsonArray',
95
+ titleLong: 'JSON Array',
96
+ titleShort: 'JSONArray',
82
97
  },
83
98
  {
84
99
  key: 'json',
85
100
  type: 'json',
101
+ titleLong: 'JSON Object',
102
+ titleShort: 'JSONObject',
86
103
  },
87
104
  {
88
105
  key: 'jsonValue',
89
106
  type: 'jsonValue',
107
+ titleLong: 'JSON Value',
108
+ titleShort: 'JSONValue',
90
109
  },
91
110
  ],
92
111
  },
@@ -0,0 +1 @@
1
+ export declare const objectDepth: (o: object) => number;
@@ -1 +1,17 @@
1
+ /**
2
+ * Generates a new TimeId.
3
+ * A TimeId is a string in the format "xxxx:timestamp" where:
4
+ * - "xxxx" is a 4-character unique identifier
5
+ * - "timestamp" is the current time in milliseconds since epoch
6
+ * @returns A new TimeId string
7
+ */
1
8
  export declare const timeId: () => string;
9
+ /**
10
+ * Checks if a given id is a valid TimeId.
11
+ * A valid TimeId has the format "xxxx:timestamp" where:
12
+ * - "xxxx" is a 4-character string
13
+ * - "timestamp" is a valid number representing milliseconds since epoch
14
+ * @param id - The id to check
15
+ * @returns True if the id is a valid TimeId, false otherwise
16
+ */
17
+ export declare const isTimeId: (id: string) => boolean;
@@ -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", "edits"];
29
+ export declare const contentTypes: readonly ["buffets", "cakes", "layers", "sliceIds", "components", "revisions", "tableCfgs", "history"];
30
30
  export type ContentType = (typeof contentTypes)[number];
31
31
  /**
32
32
  * An example object using the typedefs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/rljson",
3
- "version": "0.0.61",
3
+ "version": "0.0.64",
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,20 +20,20 @@
20
20
  ],
21
21
  "type": "module",
22
22
  "devDependencies": {
23
- "@types/node": "^24.6.1",
24
- "@typescript-eslint/eslint-plugin": "^8.45.0",
25
- "@typescript-eslint/parser": "^8.45.0",
23
+ "@types/node": "^24.9.1",
24
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
25
+ "@typescript-eslint/parser": "^8.46.2",
26
26
  "@vitest/coverage-v8": "^3.2.4",
27
27
  "cross-env": "^10.1.0",
28
- "eslint": "^9.36.0",
29
- "eslint-plugin-jsdoc": "^60.7.0",
28
+ "eslint": "^9.38.0",
29
+ "eslint-plugin-jsdoc": "^61.1.5",
30
30
  "eslint-plugin-tsdoc": "^0.4.0",
31
31
  "globals": "^16.4.0",
32
- "jsdoc": "^4.0.4",
32
+ "jsdoc": "^4.0.5",
33
33
  "read-pkg": "^9.0.1",
34
34
  "typescript": "~5.9.3",
35
- "typescript-eslint": "^8.45.0",
36
- "vite": "^7.1.7",
35
+ "typescript-eslint": "^8.46.2",
36
+ "vite": "^7.1.11",
37
37
  "vite-node": "^3.2.4",
38
38
  "vite-plugin-dts": "^4.5.4",
39
39
  "vite-tsconfig-paths": "^5.1.4",
@@ -1,29 +0,0 @@
1
- import { Json, JsonH, JsonValueH } from '@rljson/json';
2
- import { Ref } from '../typedefs.ts';
3
- import { RouteRef } from './route.ts';
4
- import { RljsonTable } from '../rljson.ts';
5
- /**
6
- * An Edit Object describing edits on data basically
7
- */
8
- export type EditRef = Ref;
9
- export type Edit<T extends Json> = {
10
- value: T & JsonValueH;
11
- route: RouteRef;
12
- origin?: Ref;
13
- previous?: EditProtocolTimeId[];
14
- acknowledged?: boolean;
15
- } & JsonH;
16
- export type EditProtocolTimeId = string;
17
- export type EditProtocolRow<Str extends string> = {
18
- [key in Str as `${Uncapitalize<string & key>}Ref`]: string;
19
- } & {
20
- timeId: EditProtocolTimeId;
21
- route: RouteRef;
22
- origin?: Ref;
23
- previous?: EditProtocolTimeId[];
24
- };
25
- export type EditProtocol<Str extends string> = RljsonTable<EditProtocolRow<Str>, 'edits'>;
26
- /**
27
- * Provides an example Edits table for test purposes
28
- */
29
- export declare const exampleEditsTable: () => EditProtocol<any>;
@@ -1,13 +0,0 @@
1
- export type RouteRef = string;
2
- /**
3
- * A class to handle routes in an Rljson object.
4
- */
5
- export declare class Route {
6
- private segments;
7
- constructor(segments: string[]);
8
- static fromFlat(route: string): Route;
9
- segment(index?: number): string;
10
- deeper(steps?: number): Route;
11
- get isRoot(): boolean;
12
- get flat(): string;
13
- }