inibase 2.0.1 → 3.1.0
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/README.md +211 -2
- package/dist/expression.d.ts +159 -0
- package/dist/expression.js +495 -0
- package/dist/file.d.ts +20 -1
- package/dist/file.js +211 -16
- package/dist/index.d.ts +177 -1
- package/dist/index.js +1414 -150
- package/dist/journal.d.ts +109 -0
- package/dist/journal.js +263 -0
- package/dist/utils.js +54 -0
- package/package.json +7 -2
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computed-fields expression language (v1 — id-only).
|
|
3
|
+
*
|
|
4
|
+
* Grammar
|
|
5
|
+
* -------
|
|
6
|
+
* ```
|
|
7
|
+
* expression := term (("+" | "-") term)*
|
|
8
|
+
* term := factor (("," | "/" | "%") factor)* // "," = multiply
|
|
9
|
+
* factor := integer-literal | path | function-call | "(" expression ")"
|
|
10
|
+
* path := id ( "." id )* // "." = link/binding hop
|
|
11
|
+
* function := "sum" | "count" | "avg" | "min" | "max" "(" expression ")"
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Every symbol is a numeric field `id` (per-table dense counter, nested
|
|
15
|
+
* children included). There are no decimal literals: `.` is reserved as the
|
|
16
|
+
* path separator, so `3.4` is a link hop (`field 3`, then `field 4` in the
|
|
17
|
+
* table `field 3` links to), never the decimal 3.4. Fractional results are
|
|
18
|
+
* reachable via division (`314 / 100` → 3.14).
|
|
19
|
+
*
|
|
20
|
+
* A bare integer that **matches a field id in the table's schema** is that
|
|
21
|
+
* field's value (ids are locative); a bare integer that matches no field id
|
|
22
|
+
* is an integer literal. This is what makes both `sum(4, 3.4)` (id 4 =
|
|
23
|
+
* quantity) and `314 / 100` (no such ids) usable.
|
|
24
|
+
*
|
|
25
|
+
* Compiled expressions are persisted with the schema as
|
|
26
|
+
* `{ expr: string, ast: CompiledExpressionNode }` so key/table renames never
|
|
27
|
+
* retarget an expression: the AST carries only field ids (+ the id of the
|
|
28
|
+
* array ancestor a helper iterates), and every key path is re-derived from
|
|
29
|
+
* the *current* schema at write time.
|
|
30
|
+
*/
|
|
31
|
+
import { createError, isArrayOfObjects } from "./utils.js";
|
|
32
|
+
/** Cap on the raw `computed` string length (bound work at validation). */
|
|
33
|
+
export const COMPUTED_EXPR_MAX_LENGTH = 512;
|
|
34
|
+
/** Cap on the parsed AST depth (protects the parser/evaluator recursion). */
|
|
35
|
+
export const COMPUTED_EXPR_MAX_DEPTH = 64;
|
|
36
|
+
const FN_NAMES = ["sum", "count", "avg", "min", "max"];
|
|
37
|
+
const syntaxError = (language, fieldKey) => createError(language, "COMPUTED_FIELD_SYNTAX", fieldKey);
|
|
38
|
+
function tokenize(source) {
|
|
39
|
+
const tokens = [];
|
|
40
|
+
let i = 0;
|
|
41
|
+
while (i < source.length) {
|
|
42
|
+
const ch = source[i];
|
|
43
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
44
|
+
i++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch >= "0" && ch <= "9") {
|
|
48
|
+
let j = i;
|
|
49
|
+
while (j < source.length && source[j] >= "0" && source[j] <= "9")
|
|
50
|
+
j++;
|
|
51
|
+
tokens.push({ t: "num", v: Number(source.slice(i, j)) });
|
|
52
|
+
i = j;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z")) {
|
|
56
|
+
let j = i;
|
|
57
|
+
while (j < source.length &&
|
|
58
|
+
((source[j] >= "a" && source[j] <= "z") ||
|
|
59
|
+
(source[j] >= "A" && source[j] <= "Z") ||
|
|
60
|
+
(source[j] >= "0" && source[j] <= "9") ||
|
|
61
|
+
source[j] === "_"))
|
|
62
|
+
j++;
|
|
63
|
+
tokens.push({ t: "ident", v: source.slice(i, j) });
|
|
64
|
+
i = j;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
switch (ch) {
|
|
68
|
+
case ".":
|
|
69
|
+
tokens.push({ t: "dot" });
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
case "(":
|
|
73
|
+
tokens.push({ t: "lp" });
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
case ")":
|
|
77
|
+
tokens.push({ t: "rp" });
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
case "+":
|
|
81
|
+
case "-":
|
|
82
|
+
case ",":
|
|
83
|
+
case "/":
|
|
84
|
+
case "%":
|
|
85
|
+
tokens.push({ t: "op", v: ch });
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
default:
|
|
89
|
+
throw syntaxError("en", "");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
tokens.push({ t: "eof" });
|
|
93
|
+
return tokens;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse a `computed` expression into a raw AST. Throws
|
|
97
|
+
* `COMPUTED_FIELD_SYNTAX` on invalid syntax, oversized input or excessive
|
|
98
|
+
* nesting.
|
|
99
|
+
*/
|
|
100
|
+
export function parseExpression(source, language = "en", fieldKey = "") {
|
|
101
|
+
if (typeof source !== "string" || !source.length)
|
|
102
|
+
throw syntaxError(language, fieldKey);
|
|
103
|
+
if (source.length > COMPUTED_EXPR_MAX_LENGTH)
|
|
104
|
+
throw syntaxError(language, fieldKey);
|
|
105
|
+
const tokens = tokenize(source);
|
|
106
|
+
let pos = 0;
|
|
107
|
+
let depth = 0;
|
|
108
|
+
const peek = () => tokens[pos];
|
|
109
|
+
const next = () => tokens[pos++];
|
|
110
|
+
const fail = () => syntaxError(language, fieldKey);
|
|
111
|
+
const enter = () => {
|
|
112
|
+
if (++depth > COMPUTED_EXPR_MAX_DEPTH)
|
|
113
|
+
throw fail();
|
|
114
|
+
};
|
|
115
|
+
const leave = () => {
|
|
116
|
+
depth--;
|
|
117
|
+
};
|
|
118
|
+
const expression = () => {
|
|
119
|
+
enter();
|
|
120
|
+
let left = term();
|
|
121
|
+
let token = peek();
|
|
122
|
+
while (token.t === "op" && (token.v === "+" || token.v === "-")) {
|
|
123
|
+
const op = next().v;
|
|
124
|
+
left = {
|
|
125
|
+
kind: "bin",
|
|
126
|
+
op: op === "+" ? "add" : "sub",
|
|
127
|
+
left,
|
|
128
|
+
right: term(),
|
|
129
|
+
};
|
|
130
|
+
token = peek();
|
|
131
|
+
}
|
|
132
|
+
leave();
|
|
133
|
+
return left;
|
|
134
|
+
};
|
|
135
|
+
const term = () => {
|
|
136
|
+
enter();
|
|
137
|
+
let left = factor();
|
|
138
|
+
while (peek().t === "op" && [",", "/", "%"].includes(peek().v)) {
|
|
139
|
+
const op = next().v;
|
|
140
|
+
const binOp = op === "," ? "mul" : op === "/" ? "div" : "mod";
|
|
141
|
+
left = { kind: "bin", op: binOp, left, right: factor() };
|
|
142
|
+
}
|
|
143
|
+
leave();
|
|
144
|
+
return left;
|
|
145
|
+
};
|
|
146
|
+
const factor = () => {
|
|
147
|
+
enter();
|
|
148
|
+
const token = next();
|
|
149
|
+
if (token.t === "num") {
|
|
150
|
+
if (peek().t === "dot") {
|
|
151
|
+
const ids = [token.v];
|
|
152
|
+
while (peek().t === "dot") {
|
|
153
|
+
next();
|
|
154
|
+
const seg = next();
|
|
155
|
+
if (seg.t !== "num")
|
|
156
|
+
throw fail();
|
|
157
|
+
ids.push(seg.v);
|
|
158
|
+
}
|
|
159
|
+
leave();
|
|
160
|
+
return { kind: "path", ids };
|
|
161
|
+
}
|
|
162
|
+
leave();
|
|
163
|
+
return { kind: "num", value: token.v };
|
|
164
|
+
}
|
|
165
|
+
if (token.t === "ident") {
|
|
166
|
+
const name = token.v;
|
|
167
|
+
if (!FN_NAMES.includes(name))
|
|
168
|
+
throw fail();
|
|
169
|
+
if (next().t !== "lp")
|
|
170
|
+
throw fail();
|
|
171
|
+
const arg = expression();
|
|
172
|
+
if (next().t !== "rp")
|
|
173
|
+
throw fail();
|
|
174
|
+
leave();
|
|
175
|
+
return { kind: "fn", name: name, arg };
|
|
176
|
+
}
|
|
177
|
+
if (token.t === "lp") {
|
|
178
|
+
const inner = expression();
|
|
179
|
+
if (next().t !== "rp")
|
|
180
|
+
throw fail();
|
|
181
|
+
leave();
|
|
182
|
+
return inner;
|
|
183
|
+
}
|
|
184
|
+
throw fail();
|
|
185
|
+
};
|
|
186
|
+
const root = expression();
|
|
187
|
+
if (next().t !== "eof")
|
|
188
|
+
throw fail();
|
|
189
|
+
return root;
|
|
190
|
+
}
|
|
191
|
+
/* ---------------------------------------------------------------------------
|
|
192
|
+
* Schema index
|
|
193
|
+
* ------------------------------------------------------------------------- */
|
|
194
|
+
/**
|
|
195
|
+
* Build the id → {@link FieldRef} index of a schema (nested children included,
|
|
196
|
+
* using dotted key paths). Container fields (array/object with object
|
|
197
|
+
* children) are indexed too so unknown ids are detected, but they can't be
|
|
198
|
+
* referenced by an expression.
|
|
199
|
+
*/
|
|
200
|
+
export function buildFieldIndex(schema) {
|
|
201
|
+
const index = new Map();
|
|
202
|
+
const isArrayOfObjectsContainer = (field) => Array.isArray(field.children) && isArrayOfObjects(field.children);
|
|
203
|
+
const walk = (fields, prefix, ancestors) => {
|
|
204
|
+
for (const field of fields) {
|
|
205
|
+
const key = prefix ? `${prefix}.${field.key}` : field.key;
|
|
206
|
+
if (field.id !== undefined) {
|
|
207
|
+
let arrayAncestor = null;
|
|
208
|
+
let nestedInArrayOfArrays = false;
|
|
209
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
210
|
+
const ancestor = ancestors[i];
|
|
211
|
+
if (ancestor.field.type === "array" &&
|
|
212
|
+
isArrayOfObjectsContainer(ancestor.field)) {
|
|
213
|
+
arrayAncestor = {
|
|
214
|
+
id: ancestor.field.id,
|
|
215
|
+
key: ancestor.key,
|
|
216
|
+
};
|
|
217
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
218
|
+
const parent = ancestors[j].field;
|
|
219
|
+
if (parent.type === "array" &&
|
|
220
|
+
isArrayOfObjectsContainer(parent)) {
|
|
221
|
+
nestedInArrayOfArrays = true;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
index.set(field.id, {
|
|
229
|
+
key,
|
|
230
|
+
field,
|
|
231
|
+
arrayAncestor,
|
|
232
|
+
nestedInArrayOfArrays,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (isArrayOfObjectsContainer(field))
|
|
236
|
+
walk(field.children, key, [...ancestors, { field, key }]);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
walk(schema, "", []);
|
|
240
|
+
return index;
|
|
241
|
+
}
|
|
242
|
+
const isContainer = (field) => Array.isArray(field.children) && isArrayOfObjects(field.children);
|
|
243
|
+
/** Collect every path node of a (compiled or raw) expression tree. */
|
|
244
|
+
function collectPaths(node) {
|
|
245
|
+
if (node.kind === "path")
|
|
246
|
+
return [node];
|
|
247
|
+
const paths = [];
|
|
248
|
+
if (node.kind === "bin") {
|
|
249
|
+
paths.push(...collectPaths(node.left));
|
|
250
|
+
paths.push(...collectPaths(node.right));
|
|
251
|
+
}
|
|
252
|
+
else if (node.kind === "fn")
|
|
253
|
+
paths.push(...collectPaths(node.arg));
|
|
254
|
+
return paths;
|
|
255
|
+
}
|
|
256
|
+
async function resolvePathNode(node, ctx, inHelper) {
|
|
257
|
+
const ids = node.ids;
|
|
258
|
+
const hop0 = ctx.index.get(ids[0]);
|
|
259
|
+
if (!hop0)
|
|
260
|
+
throw createError(ctx.language, "COMPUTED_FIELD_UNKNOWN_FIELD", [
|
|
261
|
+
ctx.ownKey,
|
|
262
|
+
ids[0],
|
|
263
|
+
]);
|
|
264
|
+
if (isContainer(hop0.field))
|
|
265
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
266
|
+
ctx.ownKey,
|
|
267
|
+
ids[0],
|
|
268
|
+
]);
|
|
269
|
+
const arrayFieldId = hop0.arrayAncestor?.id ?? null;
|
|
270
|
+
// A bare (non-helper) reference may only address fields outside arrays;
|
|
271
|
+
// array contents are reachable exclusively through sum/count/avg/min/max.
|
|
272
|
+
if (!inHelper && arrayFieldId !== null)
|
|
273
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
274
|
+
ctx.ownKey,
|
|
275
|
+
ids[0],
|
|
276
|
+
]);
|
|
277
|
+
if (inHelper && hop0.arrayAncestor !== null && hop0.nestedInArrayOfArrays)
|
|
278
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
279
|
+
ctx.ownKey,
|
|
280
|
+
ids[0],
|
|
281
|
+
]);
|
|
282
|
+
// Link hops: each hop beyond the first must live in the table the previous
|
|
283
|
+
// hop's field links to.
|
|
284
|
+
let currentRef = hop0;
|
|
285
|
+
for (let i = 1; i < ids.length; i++) {
|
|
286
|
+
const linkField = currentRef.field;
|
|
287
|
+
if (linkField.type !== "table" || typeof linkField.table !== "string")
|
|
288
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_LINK", [
|
|
289
|
+
ctx.ownKey,
|
|
290
|
+
ids[i],
|
|
291
|
+
]);
|
|
292
|
+
const targetIndex = await ctx.getTableIndex(linkField.table);
|
|
293
|
+
if (!targetIndex)
|
|
294
|
+
throw createError(ctx.language, "TABLE_NOT_EXISTS", linkField.table);
|
|
295
|
+
const ref = targetIndex.get(ids[i]);
|
|
296
|
+
if (!ref)
|
|
297
|
+
throw createError(ctx.language, "COMPUTED_FIELD_UNKNOWN_FIELD", [
|
|
298
|
+
ctx.ownKey,
|
|
299
|
+
ids[i],
|
|
300
|
+
]);
|
|
301
|
+
if (isContainer(ref.field))
|
|
302
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
303
|
+
ctx.ownKey,
|
|
304
|
+
ids[i],
|
|
305
|
+
]);
|
|
306
|
+
currentRef = ref;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
node: { kind: "path", ids, arrayFieldId },
|
|
310
|
+
deps: new Set([ids[0]]),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
async function resolveNode(node, ctx, inHelper) {
|
|
314
|
+
switch (node.kind) {
|
|
315
|
+
case "num":
|
|
316
|
+
// A bare integer that matches an existing field id is that field
|
|
317
|
+
// (ids are locative, inside helpers included — this is what makes
|
|
318
|
+
// `sum(5, 4.2)` and `sum(5, 6)` usable); any other bare integer is
|
|
319
|
+
// an integer literal (`314 / 100`).
|
|
320
|
+
if (ctx.index.has(node.value))
|
|
321
|
+
return resolvePathNode({ kind: "path", ids: [node.value] }, ctx, inHelper);
|
|
322
|
+
return { node: { kind: "num", value: node.value }, deps: new Set() };
|
|
323
|
+
case "path":
|
|
324
|
+
return resolvePathNode(node, ctx, inHelper);
|
|
325
|
+
case "bin": {
|
|
326
|
+
const left = await resolveNode(node.left, ctx, inHelper);
|
|
327
|
+
const right = await resolveNode(node.right, ctx, inHelper);
|
|
328
|
+
const deps = new Set([...left.deps, ...right.deps]);
|
|
329
|
+
return {
|
|
330
|
+
node: { kind: "bin", op: node.op, left: left.node, right: right.node },
|
|
331
|
+
deps,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
case "fn": {
|
|
335
|
+
if (inHelper)
|
|
336
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
337
|
+
ctx.ownKey,
|
|
338
|
+
]);
|
|
339
|
+
const arg = await resolveNode(node.arg, ctx, true);
|
|
340
|
+
const paths = collectPaths(arg.node);
|
|
341
|
+
if (!paths.length)
|
|
342
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
343
|
+
ctx.ownKey,
|
|
344
|
+
]);
|
|
345
|
+
const arrayFieldIds = new Set(paths.map((path) => path.arrayFieldId ?? null));
|
|
346
|
+
if (arrayFieldIds.size !== 1 || arrayFieldIds.has(null))
|
|
347
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
348
|
+
ctx.ownKey,
|
|
349
|
+
]);
|
|
350
|
+
return {
|
|
351
|
+
node: {
|
|
352
|
+
kind: "fn",
|
|
353
|
+
name: node.name,
|
|
354
|
+
arrayFieldId: arrayFieldIds.values().next().value,
|
|
355
|
+
arg: arg.node,
|
|
356
|
+
},
|
|
357
|
+
deps: arg.deps,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Resolve a raw expression against the schema (and linked tables), returning
|
|
364
|
+
* the compiled node plus the set of field ids it reads from the current
|
|
365
|
+
* table (used for dependency ordering / cycle detection between computed
|
|
366
|
+
* fields).
|
|
367
|
+
*/
|
|
368
|
+
export async function resolveExpression(expression, ctx) {
|
|
369
|
+
const resolved = await resolveNode(expression, ctx, false);
|
|
370
|
+
return { ast: resolved.node, deps: resolved.deps };
|
|
371
|
+
}
|
|
372
|
+
/** Every field id a compiled expression reads from the *current* table. */
|
|
373
|
+
export function collectFieldDeps(node) {
|
|
374
|
+
const deps = new Set();
|
|
375
|
+
const visit = (n) => {
|
|
376
|
+
switch (n.kind) {
|
|
377
|
+
case "num":
|
|
378
|
+
return;
|
|
379
|
+
case "path":
|
|
380
|
+
deps.add(n.ids[0]);
|
|
381
|
+
return;
|
|
382
|
+
case "bin":
|
|
383
|
+
visit(n.left);
|
|
384
|
+
visit(n.right);
|
|
385
|
+
return;
|
|
386
|
+
case "fn":
|
|
387
|
+
visit(n.arg);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
visit(node);
|
|
392
|
+
return deps;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Order computed fields so every dependency is evaluated before its dependents.
|
|
396
|
+
* Throws `COMPUTED_FIELD_CYCLE` when a field (transitively) depends on itself.
|
|
397
|
+
*/
|
|
398
|
+
export function topoSortComputedFields(fields, language) {
|
|
399
|
+
const byId = new Map(fields.map((field) => [field.id, field]));
|
|
400
|
+
const remaining = new Set(fields.map((field) => field.id));
|
|
401
|
+
const ordered = [];
|
|
402
|
+
const blocked = new Map(); // dependent -> dependencies still missing
|
|
403
|
+
const indegree = new Map();
|
|
404
|
+
for (const field of fields) {
|
|
405
|
+
const dependents = [];
|
|
406
|
+
for (const depId of field.deps)
|
|
407
|
+
if (byId.has(depId))
|
|
408
|
+
dependents.push(depId);
|
|
409
|
+
blocked.set(field.id, dependents);
|
|
410
|
+
}
|
|
411
|
+
const ready = [];
|
|
412
|
+
for (const [id, deps] of blocked) {
|
|
413
|
+
indegree.set(id, deps.length);
|
|
414
|
+
if (deps.length === 0)
|
|
415
|
+
ready.push(id);
|
|
416
|
+
}
|
|
417
|
+
while (ready.length) {
|
|
418
|
+
const id = ready.shift();
|
|
419
|
+
const field = byId.get(id);
|
|
420
|
+
if (!field)
|
|
421
|
+
continue;
|
|
422
|
+
ordered.push(field);
|
|
423
|
+
remaining.delete(id);
|
|
424
|
+
for (const [otherId, deps] of blocked) {
|
|
425
|
+
if (!remaining.has(otherId))
|
|
426
|
+
continue;
|
|
427
|
+
if (deps.includes(id)) {
|
|
428
|
+
const nextIndegree = (indegree.get(otherId) ?? 1) - 1;
|
|
429
|
+
indegree.set(otherId, nextIndegree);
|
|
430
|
+
if (nextIndegree === 0)
|
|
431
|
+
ready.push(otherId);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (remaining.size) {
|
|
436
|
+
const keys = fields
|
|
437
|
+
.filter((field) => remaining.has(field.id))
|
|
438
|
+
.map((field) => field.key);
|
|
439
|
+
throw createError(language, "COMPUTED_FIELD_CYCLE", keys.join(", "));
|
|
440
|
+
}
|
|
441
|
+
return ordered;
|
|
442
|
+
}
|
|
443
|
+
/* ---------------------------------------------------------------------------
|
|
444
|
+
* Row flattening (shared by the evaluator)
|
|
445
|
+
* ------------------------------------------------------------------------- */
|
|
446
|
+
/**
|
|
447
|
+
* Flatten a formatted row into a dot-notation record. Objects are flattened
|
|
448
|
+
* (`meta.age`), arrays of objects are combined per child key
|
|
449
|
+
* (`items.quantity` -> array of per-element values) and everything else is
|
|
450
|
+
* stored under its dotted key.
|
|
451
|
+
*/
|
|
452
|
+
export function flattenRecord(obj, prefix = "") {
|
|
453
|
+
const out = {};
|
|
454
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
455
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
456
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v))
|
|
457
|
+
Object.assign(out, flattenRecord(v, key));
|
|
458
|
+
else if (Array.isArray(v) && v.length && isArrayOfObjects(v)) {
|
|
459
|
+
const combined = {};
|
|
460
|
+
for (const element of v)
|
|
461
|
+
for (const [childKey, childValue] of Object.entries(element)) {
|
|
462
|
+
if (!combined[childKey])
|
|
463
|
+
combined[childKey] = [];
|
|
464
|
+
combined[childKey].push(childValue);
|
|
465
|
+
}
|
|
466
|
+
for (const [childKey, values] of Object.entries(combined))
|
|
467
|
+
out[`${key}.${childKey}`] = values;
|
|
468
|
+
}
|
|
469
|
+
else
|
|
470
|
+
out[key] = v;
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Resolve a dotted key path against a structured frame in place — the direct
|
|
476
|
+
* read-path counterpart of `flattenRecord`. Walks `obj` segment by segment
|
|
477
|
+
* and returns `undefined` when any intermediate is null, undefined, a
|
|
478
|
+
* non-object, or an array (arrays resolve numerically only, so they never
|
|
479
|
+
* match a dotted segment), mirroring the leaves `flattenRecord` would have
|
|
480
|
+
* produced without materialising the flattened record.
|
|
481
|
+
*/
|
|
482
|
+
export function resolveFramePath(obj, dottedKey) {
|
|
483
|
+
if (dottedKey.length === 0)
|
|
484
|
+
return obj;
|
|
485
|
+
let cur = obj;
|
|
486
|
+
for (const segment of dottedKey.split(".")) {
|
|
487
|
+
if (cur === null ||
|
|
488
|
+
cur === undefined ||
|
|
489
|
+
typeof cur !== "object" ||
|
|
490
|
+
Array.isArray(cur))
|
|
491
|
+
return undefined;
|
|
492
|
+
cur = cur[segment];
|
|
493
|
+
}
|
|
494
|
+
return cur;
|
|
495
|
+
}
|
package/dist/file.d.ts
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import { type ComparisonOperator, type Field } from "./index.js";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const DURABLE: boolean;
|
|
3
|
+
export declare const lock: (folderPath: string, prefix?: string, ttl?: number) => Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Non-blocking lock acquisition (used by the read path and the open-time
|
|
6
|
+
* recovery sweep). Returns true when the lock was acquired (running crash
|
|
7
|
+
* recovery for prefix-less locks, exactly like `lock`), false when another
|
|
8
|
+
* process holds it. A single stale-lock steal (dead same-host owner, or aged
|
|
9
|
+
* foreign/unknown owner) is attempted so a crashed owner can't wedge readers
|
|
10
|
+
* behind it forever.
|
|
11
|
+
*/
|
|
12
|
+
export declare const tryLock: (folderPath: string, prefix?: string, ttl?: number) => Promise<boolean>;
|
|
3
13
|
export declare const unlock: (folderPath: string, prefix?: string) => Promise<void>;
|
|
4
14
|
export declare const write: (filePath: string, data: any) => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* fsync an existing file. Used to flush temp files (written via streams or
|
|
17
|
+
* shell pipelines) before they are renamed into place.
|
|
18
|
+
*/
|
|
19
|
+
export declare const syncFile: (filePath: string) => Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* fsync a directory so that renames performed inside it are durable.
|
|
22
|
+
*/
|
|
23
|
+
export declare const syncDir: (dirPath: string) => Promise<void>;
|
|
5
24
|
export declare const read: (filePath: string) => Promise<string>;
|
|
6
25
|
export declare function escapeShellPath(filePath: string): string;
|
|
7
26
|
/**
|