inibase 3.1.1 → 3.2.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 +84 -4
- package/dist/expression.d.ts +9 -0
- package/dist/expression.js +18 -5
- package/dist/index.d.ts +28 -0
- package/dist/index.js +391 -61
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -798,6 +798,20 @@ await db.sum("user", "age");
|
|
|
798
798
|
|
|
799
799
|
// get the sum of column "age" by criteria (where "isActive" is equal to "false") in "user" table
|
|
800
800
|
await db.sum("user", ["age", ...], { isActive: false });
|
|
801
|
+
|
|
802
|
+
// aggregate over the elements of an array-of-objects column: `items.quantity`
|
|
803
|
+
// counts every element of every row's `items` (single array path per call;
|
|
804
|
+
// `product` here is an *element* predicate, matching elements whose linked
|
|
805
|
+
// product is the given row)
|
|
806
|
+
await db.sum("orders", "items.quantity", { product: "id-of-widget" }, { nested: true });
|
|
807
|
+
|
|
808
|
+
// multiple columns share one predicate and return a record
|
|
809
|
+
await db.sum(
|
|
810
|
+
"orders",
|
|
811
|
+
["items.quantity", "items.lineTotal"],
|
|
812
|
+
{ product: "id-of-widget" },
|
|
813
|
+
{ nested: true },
|
|
814
|
+
); // => { "items.quantity": 5, "items.lineTotal": 1295 }
|
|
801
815
|
```
|
|
802
816
|
|
|
803
817
|
</blockquote>
|
|
@@ -910,6 +924,68 @@ const posted = await db.post(
|
|
|
910
924
|
// posted.totalCentsLive === 847 (2*price(widget) + 1*price(gadget))
|
|
911
925
|
```
|
|
912
926
|
|
|
927
|
+
A child of an array-of-objects column can itself be computed. It is evaluated
|
|
928
|
+
once **per element** and stored in its own element cell, so it works like any
|
|
929
|
+
other element child in queries — including `sum` with `{ nested: true }`:
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
// ids are assigned per table in schema order: status=1, items=2, product=3,
|
|
933
|
+
// quantity=4, lineTotal=5
|
|
934
|
+
await db.createTable("orders", [
|
|
935
|
+
{ key: "status", type: "number" },
|
|
936
|
+
{
|
|
937
|
+
key: "items",
|
|
938
|
+
type: "array",
|
|
939
|
+
children: [
|
|
940
|
+
{ key: "product", type: "table", table: "product" },
|
|
941
|
+
{ key: "quantity", type: "number" },
|
|
942
|
+
// lineTotal = quantity × product.price ("4 * 3.2": id 4 = quantity,
|
|
943
|
+
// hop to the linked product's price, id 2 in `product`)
|
|
944
|
+
{ key: "lineTotal", type: "number", computed: "4 * 3.2" },
|
|
945
|
+
],
|
|
946
|
+
},
|
|
947
|
+
]);
|
|
948
|
+
|
|
949
|
+
const posted = await db.post(
|
|
950
|
+
"orders",
|
|
951
|
+
{
|
|
952
|
+
status: 1,
|
|
953
|
+
items: [
|
|
954
|
+
{ product: "id-of-widget", quantity: 2 },
|
|
955
|
+
{ product: "id-of-gadget", quantity: 1 },
|
|
956
|
+
],
|
|
957
|
+
},
|
|
958
|
+
undefined,
|
|
959
|
+
true,
|
|
960
|
+
);
|
|
961
|
+
// posted.items[0].lineTotal === 2 * price(widget)
|
|
962
|
+
// posted.items[1].lineTotal === 1 * price(gadget)
|
|
963
|
+
|
|
964
|
+
// per-product revenue without touching the line items yourself:
|
|
965
|
+
await db.sum("orders", "items.lineTotal", { product: "id-of-widget" }, { nested: true });
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
A top-level computed may read a computed child through a helper
|
|
969
|
+
(`{ key: "totalCents", type: "number", computed: "sum(5)" }`) — the children
|
|
970
|
+
are derived first, in dependency order.
|
|
971
|
+
|
|
972
|
+
**Element expressions (child computeds).**
|
|
973
|
+
|
|
974
|
+
- Every reference must be a sibling child of the same array root — top-level
|
|
975
|
+
columns and siblings of a *different* array are rejected
|
|
976
|
+
(`COMPUTED_FIELD_INVALID_TARGET`), and helpers (`sum`/`avg`/...) are not
|
|
977
|
+
allowed inside element expressions.
|
|
978
|
+
- Link hops are supported (`4 * 3.2` = sibling `quantity` × linked
|
|
979
|
+
`product.price`).
|
|
980
|
+
- The element field must be `type: "number"`.
|
|
981
|
+
- A **missing/null** operand in an element evaluates to `0` for that element
|
|
982
|
+
(it never aborts the write and never stores null). A genuinely dangling link
|
|
983
|
+
(a link to a missing row) still raises `COMPUTED_FIELD_DANGLING_LINK`, same
|
|
984
|
+
as top-level computeds.
|
|
985
|
+
- `updateTable` backfills element cells when a child computed is added or its
|
|
986
|
+
expression changes; the backfill resolves links from the persisted element
|
|
987
|
+
values and follows the same missing-operand → `0` rule.
|
|
988
|
+
|
|
913
989
|
**Expression language (v1, integer-only).**
|
|
914
990
|
|
|
915
991
|
- Operators: `+` `-` `*` `/` `%`; `( )` for grouping. Multiplication
|
|
@@ -941,10 +1017,14 @@ const posted = await db.post(
|
|
|
941
1017
|
`avg`/`min`/`max` over no values throw `COMPUTED_FIELD_ARITHMETIC`.
|
|
942
1018
|
- Non-numeric operands, division/modulo by zero, and arithmetic over missing
|
|
943
1019
|
(null) values throw `COMPUTED_FIELD_ARITHMETIC`; a link to a missing row
|
|
944
|
-
throws `COMPUTED_FIELD_DANGLING_LINK`.
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1020
|
+
throws `COMPUTED_FIELD_DANGLING_LINK`. (Element expressions are the one
|
|
1021
|
+
exception: a missing/null operand there evaluates that element to `0` — see
|
|
1022
|
+
above.)
|
|
1023
|
+
- Child computeds (see above) extend the v1 rules to element fields of
|
|
1024
|
+
array-of-objects columns; array contents stay reachable from top-level
|
|
1025
|
+
helpers. Only one level of nesting is computed.
|
|
1026
|
+
- Missing link values make a single bare path evaluate to null (stored
|
|
1027
|
+
empty).
|
|
948
1028
|
|
|
949
1029
|
</blockquote>
|
|
950
1030
|
</details>
|
package/dist/expression.d.ts
CHANGED
|
@@ -118,6 +118,15 @@ export interface ResolveContext {
|
|
|
118
118
|
index: Map<number, FieldRef>;
|
|
119
119
|
/** Fetch (and cache) the id index of another table, or undefined. */
|
|
120
120
|
getTableIndex: (tableName: string) => Promise<Map<number, FieldRef> | undefined>;
|
|
121
|
+
/**
|
|
122
|
+
* When set, the expression being compiled belongs to an *element* of the
|
|
123
|
+
* array-of-objects field with this id (a child computed field). Every
|
|
124
|
+
* referenced field must be a sibling child of that same array; top-level
|
|
125
|
+
* fields and helpers are rejected.
|
|
126
|
+
*/
|
|
127
|
+
elementContext?: {
|
|
128
|
+
arrayRootId: number;
|
|
129
|
+
} | null;
|
|
121
130
|
}
|
|
122
131
|
/**
|
|
123
132
|
* Resolve a raw expression against the schema (and linked tables), returning
|
package/dist/expression.js
CHANGED
|
@@ -267,14 +267,27 @@ async function resolvePathNode(node, ctx, inHelper) {
|
|
|
267
267
|
ids[0],
|
|
268
268
|
]);
|
|
269
269
|
const arrayFieldId = hop0.arrayAncestor?.id ?? null;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
if (ctx.elementContext) {
|
|
271
|
+
// Child computed field: every reference must be a sibling child of the
|
|
272
|
+
// computed field's own array root (helpers that recurse into other
|
|
273
|
+
// arrays and reads of top-level columns are invalid here).
|
|
274
|
+
if (hop0.arrayAncestor?.id !== ctx.elementContext.arrayRootId)
|
|
275
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
276
|
+
ctx.ownKey,
|
|
277
|
+
ids[0],
|
|
278
|
+
]);
|
|
279
|
+
if (hop0.nestedInArrayOfArrays)
|
|
280
|
+
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
281
|
+
ctx.ownKey,
|
|
282
|
+
ids[0],
|
|
283
|
+
]);
|
|
284
|
+
}
|
|
285
|
+
else if (!inHelper && arrayFieldId !== null)
|
|
273
286
|
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
274
287
|
ctx.ownKey,
|
|
275
288
|
ids[0],
|
|
276
289
|
]);
|
|
277
|
-
if (inHelper && hop0.arrayAncestor !== null && hop0.nestedInArrayOfArrays)
|
|
290
|
+
else if (inHelper && hop0.arrayAncestor !== null && hop0.nestedInArrayOfArrays)
|
|
278
291
|
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
279
292
|
ctx.ownKey,
|
|
280
293
|
ids[0],
|
|
@@ -332,7 +345,7 @@ async function resolveNode(node, ctx, inHelper) {
|
|
|
332
345
|
};
|
|
333
346
|
}
|
|
334
347
|
case "fn": {
|
|
335
|
-
if (inHelper)
|
|
348
|
+
if (inHelper || ctx.elementContext)
|
|
336
349
|
throw createError(ctx.language, "COMPUTED_FIELD_INVALID_TARGET", [
|
|
337
350
|
ctx.ownKey,
|
|
338
351
|
]);
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export interface Options {
|
|
|
29
29
|
columns?: string[] | string;
|
|
30
30
|
sort?: Record<string, 1 | -1 | "asc" | "ASC" | "desc" | "DESC"> | string[] | string;
|
|
31
31
|
}
|
|
32
|
+
/** Options for the aggregate actions (`sum`, and later avg/min/max). */
|
|
33
|
+
export interface AggregateOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Aggregate over the *elements* of an array-of-objects column instead of
|
|
36
|
+
* whole rows. The summed columns must be single-level child paths of the
|
|
37
|
+
* same array root (e.g. `"items.quantity"`); `where` keys that resolve as
|
|
38
|
+
* children of that root become per-element predicates.
|
|
39
|
+
*/
|
|
40
|
+
nested?: boolean;
|
|
41
|
+
}
|
|
32
42
|
export interface TableConfig {
|
|
33
43
|
compression?: boolean;
|
|
34
44
|
cache?: boolean;
|
|
@@ -183,6 +193,12 @@ export default class Inibase {
|
|
|
183
193
|
* persisted `{ expr, ast }` spec).
|
|
184
194
|
*/
|
|
185
195
|
private computedExprOf;
|
|
196
|
+
/**
|
|
197
|
+
* Every computed field of a schema as dotted keys (computed children of an
|
|
198
|
+
* array-of-objects column use their full path, e.g. `items.lineTotal`),
|
|
199
|
+
* with its raw expression — used for updateTable change detection.
|
|
200
|
+
*/
|
|
201
|
+
private computedEntries;
|
|
186
202
|
/**
|
|
187
203
|
* Compile every `computed` expression of a schema (which must already have
|
|
188
204
|
* ids assigned) into its persisted `{ expr, ast }` form, in dependency
|
|
@@ -229,6 +245,9 @@ export default class Inibase {
|
|
|
229
245
|
/** Merge evaluated per-line computed values into a `pathesContents` map
|
|
230
246
|
* as line-numbered replace records (encoded cells). */
|
|
231
247
|
private mergeComputedLineRecords;
|
|
248
|
+
/** Compare a freshly evaluated cell value against a stored (already
|
|
249
|
+
* decoded) one — both are scalar or index-aligned element arrays. */
|
|
250
|
+
private sameCellValue;
|
|
232
251
|
private isSimpleField;
|
|
233
252
|
private processSimpleField;
|
|
234
253
|
/**
|
|
@@ -407,6 +426,15 @@ export default class Inibase {
|
|
|
407
426
|
*/
|
|
408
427
|
sum(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise<number>;
|
|
409
428
|
sum(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria): Promise<Record<string, number>>;
|
|
429
|
+
/**
|
|
430
|
+
* Element-wise aggregation over an array-of-objects column (`nested` mode).
|
|
431
|
+
*
|
|
432
|
+
* `where` keys that resolve as children of the array root become per-element
|
|
433
|
+
* predicates (AND); everything else is a row-level filter. Only elements
|
|
434
|
+
* passing every element predicate, inside rows passing the row filters, are
|
|
435
|
+
* accumulated.
|
|
436
|
+
*/
|
|
437
|
+
private sumNested;
|
|
410
438
|
/**
|
|
411
439
|
* Generate average of column(s) in a table
|
|
412
440
|
*
|
package/dist/index.js
CHANGED
|
@@ -364,14 +364,14 @@ export default class Inibase {
|
|
|
364
364
|
// row. Evaluation happens BEFORE the schema is rewritten, so a
|
|
365
365
|
// failed migration (dangling link, arithmetic error, unknown
|
|
366
366
|
// field, ...) leaves the old schema and column values untouched.
|
|
367
|
-
const oldComputed = new Map((table.schema ?? [])
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
const oldComputed = new Map(this.computedEntries(table.schema ?? []).map(({ key, expr }) => [
|
|
368
|
+
key,
|
|
369
|
+
expr,
|
|
370
|
+
]));
|
|
370
371
|
const changedComputedKeys = new Set(totalLines > 0
|
|
371
|
-
? schema
|
|
372
|
-
.filter((
|
|
373
|
-
|
|
374
|
-
.map((field) => field.key)
|
|
372
|
+
? this.computedEntries(schema)
|
|
373
|
+
.filter(({ key, expr }) => oldComputed.get(key) !== expr)
|
|
374
|
+
.map(({ key }) => key)
|
|
375
375
|
: []);
|
|
376
376
|
const backfillReplacers = changedComputedKeys.size
|
|
377
377
|
? await (async () => {
|
|
@@ -1026,6 +1026,25 @@ export default class Inibase {
|
|
|
1026
1026
|
const computed = field.computed;
|
|
1027
1027
|
return typeof computed === "string" ? computed : (computed?.expr ?? "");
|
|
1028
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Every computed field of a schema as dotted keys (computed children of an
|
|
1031
|
+
* array-of-objects column use their full path, e.g. `items.lineTotal`),
|
|
1032
|
+
* with its raw expression — used for updateTable change detection.
|
|
1033
|
+
*/
|
|
1034
|
+
computedEntries(schema) {
|
|
1035
|
+
const out = [];
|
|
1036
|
+
const walk = (fields, prefix) => {
|
|
1037
|
+
for (const field of fields) {
|
|
1038
|
+
const key = prefix ? `${prefix}.${field.key}` : field.key;
|
|
1039
|
+
if (typeof field.computed !== "undefined")
|
|
1040
|
+
out.push({ key, expr: this.computedExprOf(field) });
|
|
1041
|
+
if (field.children && Utils.isArrayOfObjects(field.children))
|
|
1042
|
+
walk(field.children, key);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
walk(schema, "");
|
|
1046
|
+
return out;
|
|
1047
|
+
}
|
|
1029
1048
|
/**
|
|
1030
1049
|
* Compile every `computed` expression of a schema (which must already have
|
|
1031
1050
|
* ids assigned) into its persisted `{ expr, ast }` form, in dependency
|
|
@@ -1033,27 +1052,67 @@ export default class Inibase {
|
|
|
1033
1052
|
* on-disk schema always carries compiled ASTs.
|
|
1034
1053
|
*/
|
|
1035
1054
|
async compileComputedFields(schema) {
|
|
1036
|
-
const
|
|
1037
|
-
if (!computedFields.length)
|
|
1038
|
-
return schema;
|
|
1055
|
+
const entries = [];
|
|
1039
1056
|
const index = buildFieldIndex(schema);
|
|
1057
|
+
const walk = (fields, prefix) => {
|
|
1058
|
+
for (const field of fields) {
|
|
1059
|
+
const key = prefix ? `${prefix}.${field.key}` : field.key;
|
|
1060
|
+
if (typeof field.computed !== "undefined") {
|
|
1061
|
+
const ref = index.get(field.id);
|
|
1062
|
+
entries.push({
|
|
1063
|
+
field,
|
|
1064
|
+
key,
|
|
1065
|
+
elementRoot: ref?.arrayAncestor
|
|
1066
|
+
? { id: ref.arrayAncestor.id, key: ref.arrayAncestor.key }
|
|
1067
|
+
: null,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (field.children && Utils.isArrayOfObjects(field.children))
|
|
1071
|
+
walk(field.children, key);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
walk(schema, "");
|
|
1075
|
+
if (!entries.length)
|
|
1076
|
+
return schema;
|
|
1040
1077
|
const ctx = {
|
|
1041
1078
|
language: this.language,
|
|
1042
1079
|
ownKey: "",
|
|
1043
1080
|
index,
|
|
1044
1081
|
getTableIndex: async (target) => this.tableFieldIndex(target),
|
|
1082
|
+
elementContext: null,
|
|
1045
1083
|
};
|
|
1046
1084
|
const resolved = [];
|
|
1047
|
-
for (const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1085
|
+
for (const entry of entries) {
|
|
1086
|
+
const ref = index.get(entry.field.id);
|
|
1087
|
+
if (!ref)
|
|
1088
|
+
throw this.createError("COMPUTED_FIELD_INVALID_TARGET", entry.key);
|
|
1089
|
+
if (entry.elementRoot) {
|
|
1090
|
+
// Element computed field (a child of an array-of-objects
|
|
1091
|
+
// column): must hold a plain value (never a container), and v1
|
|
1092
|
+
// keeps them numeric so writes are always summable.
|
|
1093
|
+
if ((ref.nestedInArrayOfArrays && ref.arrayAncestor) ||
|
|
1094
|
+
entry.field.type !== "number")
|
|
1095
|
+
throw this.createError("COMPUTED_FIELD_INVALID_TARGET", entry.key);
|
|
1096
|
+
ctx.elementContext = {
|
|
1097
|
+
arrayRootId: entry.elementRoot.id,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
// Top-level computed field: cannot live inside an array.
|
|
1102
|
+
if (ref.arrayAncestor)
|
|
1103
|
+
throw this.createError("COMPUTED_FIELD_INVALID_TARGET", entry.key);
|
|
1104
|
+
ctx.elementContext = null;
|
|
1105
|
+
}
|
|
1106
|
+
ctx.ownKey = entry.key;
|
|
1107
|
+
const raw = parseExpression(this.computedExprOf(entry.field), this.language, entry.key);
|
|
1055
1108
|
const { ast, deps } = await resolveExpression(raw, ctx);
|
|
1056
|
-
resolved.push({
|
|
1109
|
+
resolved.push({
|
|
1110
|
+
field: entry.field,
|
|
1111
|
+
key: entry.key,
|
|
1112
|
+
elementRoot: entry.elementRoot,
|
|
1113
|
+
ast,
|
|
1114
|
+
deps,
|
|
1115
|
+
});
|
|
1057
1116
|
}
|
|
1058
1117
|
const ordered = topoSortComputedFields(resolved.map(({ field, deps }) => ({
|
|
1059
1118
|
id: field.id,
|
|
@@ -1065,14 +1124,27 @@ export default class Inibase {
|
|
|
1065
1124
|
const entry = resolved.find(({ field }) => field.id === meta.id);
|
|
1066
1125
|
if (!entry)
|
|
1067
1126
|
continue;
|
|
1068
|
-
specByKey.set(
|
|
1127
|
+
specByKey.set(entry.key, {
|
|
1069
1128
|
expr: this.computedExprOf(entry.field),
|
|
1070
1129
|
ast: entry.ast,
|
|
1071
1130
|
});
|
|
1072
1131
|
}
|
|
1073
|
-
|
|
1074
|
-
? {
|
|
1075
|
-
|
|
1132
|
+
const applySpecs = (fields, prefix) => fields.map((field) => {
|
|
1133
|
+
const key = prefix ? `${prefix}.${field.key}` : field.key;
|
|
1134
|
+
let next = field;
|
|
1135
|
+
if (typeof field.computed !== "undefined") {
|
|
1136
|
+
const spec = specByKey.get(key);
|
|
1137
|
+
if (spec)
|
|
1138
|
+
next = { ...field, computed: spec };
|
|
1139
|
+
}
|
|
1140
|
+
if (field.children && Utils.isArrayOfObjects(field.children))
|
|
1141
|
+
next = {
|
|
1142
|
+
...next,
|
|
1143
|
+
children: applySpecs(field.children, key),
|
|
1144
|
+
};
|
|
1145
|
+
return next;
|
|
1146
|
+
});
|
|
1147
|
+
return applySpecs(schema, "");
|
|
1076
1148
|
}
|
|
1077
1149
|
/**
|
|
1078
1150
|
* Build the evaluation plan (topological order + id index) for a table's
|
|
@@ -1098,6 +1170,7 @@ export default class Inibase {
|
|
|
1098
1170
|
language: this.language,
|
|
1099
1171
|
ownKey: "",
|
|
1100
1172
|
index,
|
|
1173
|
+
elementContext: null,
|
|
1101
1174
|
getTableIndex: async (target) => {
|
|
1102
1175
|
let cached = indexCache.get(target);
|
|
1103
1176
|
if (!cached) {
|
|
@@ -1108,29 +1181,46 @@ export default class Inibase {
|
|
|
1108
1181
|
return cached;
|
|
1109
1182
|
},
|
|
1110
1183
|
};
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1184
|
+
// Collect every computed field: top-level ones and, when a schema
|
|
1185
|
+
// carries them, the computed children of array-of-objects columns.
|
|
1186
|
+
const walk = async (fieldsSchema, prefix) => {
|
|
1187
|
+
for (const field of fieldsSchema) {
|
|
1188
|
+
const key = prefix ? `${prefix}.${field.key}` : field.key;
|
|
1189
|
+
if (typeof field.computed !== "undefined") {
|
|
1190
|
+
const ref = index.get(field.id);
|
|
1191
|
+
const elementRoot = ref?.arrayAncestor
|
|
1192
|
+
? { id: ref.arrayAncestor.id, key: ref.arrayAncestor.key }
|
|
1193
|
+
: null;
|
|
1194
|
+
const computed = field.computed;
|
|
1195
|
+
if (typeof computed === "string") {
|
|
1196
|
+
ctx.ownKey = key;
|
|
1197
|
+
ctx.elementContext = elementRoot
|
|
1198
|
+
? { arrayRootId: elementRoot.id }
|
|
1199
|
+
: null;
|
|
1200
|
+
const raw = parseExpression(computed, this.language, key);
|
|
1201
|
+
const { ast, deps } = await resolveExpression(raw, ctx);
|
|
1202
|
+
fields.push({
|
|
1203
|
+
id: field.id,
|
|
1204
|
+
key,
|
|
1205
|
+
ast,
|
|
1206
|
+
deps,
|
|
1207
|
+
elementRoot,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
else
|
|
1211
|
+
fields.push({
|
|
1212
|
+
id: field.id,
|
|
1213
|
+
key,
|
|
1214
|
+
ast: computed.ast,
|
|
1215
|
+
deps: collectFieldDeps(computed.ast),
|
|
1216
|
+
elementRoot,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
if (field.children && Utils.isArrayOfObjects(field.children))
|
|
1220
|
+
await walk(field.children, key);
|
|
1125
1221
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
id: field.id,
|
|
1129
|
-
key: field.key,
|
|
1130
|
-
ast: computed.ast,
|
|
1131
|
-
deps: collectFieldDeps(computed.ast),
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1222
|
+
};
|
|
1223
|
+
await walk(s, "");
|
|
1134
1224
|
if (!fields.length) {
|
|
1135
1225
|
if (schema === undefined)
|
|
1136
1226
|
this.computedPlanCache.set(tableName, null);
|
|
@@ -1206,6 +1296,42 @@ export default class Inibase {
|
|
|
1206
1296
|
collect,
|
|
1207
1297
|
};
|
|
1208
1298
|
for (const field of plan.fields) {
|
|
1299
|
+
if (field.elementRoot) {
|
|
1300
|
+
// Element computed field (child of an array-of-objects column):
|
|
1301
|
+
// evaluate once per element (frame = element, strip =
|
|
1302
|
+
// `${rootKey}.`), write back into the element so other computed
|
|
1303
|
+
// fields and the flatten pass see it, and report the
|
|
1304
|
+
// per-element array under the dotted column key. Missing
|
|
1305
|
+
// operands read as 0 (`env.nullAsZero`), never an error.
|
|
1306
|
+
const rootKey = field.elementRoot.key;
|
|
1307
|
+
const childKey = field.key.slice(rootKey.length + 1);
|
|
1308
|
+
const elements = Array.isArray(env.row[rootKey])
|
|
1309
|
+
? env.row[rootKey]
|
|
1310
|
+
: [];
|
|
1311
|
+
const savedFrame = env.frame;
|
|
1312
|
+
const savedStrip = env.strip;
|
|
1313
|
+
const savedNullAsZero = env.nullAsZero;
|
|
1314
|
+
env.strip = `${rootKey}.`;
|
|
1315
|
+
env.ownKey = field.key;
|
|
1316
|
+
env.nullAsZero = true;
|
|
1317
|
+
const values = [];
|
|
1318
|
+
for (const element of elements) {
|
|
1319
|
+
if (!Utils.isObject(element)) {
|
|
1320
|
+
values.push(0);
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
env.frame = element;
|
|
1324
|
+
const value = await this.evaluateNode(field.ast, env);
|
|
1325
|
+
const written = value === null || value === undefined ? 0 : value;
|
|
1326
|
+
element[childKey] = written;
|
|
1327
|
+
values.push(written);
|
|
1328
|
+
}
|
|
1329
|
+
env.frame = savedFrame;
|
|
1330
|
+
env.strip = savedStrip;
|
|
1331
|
+
env.nullAsZero = savedNullAsZero;
|
|
1332
|
+
out[field.key] = values;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1209
1335
|
env.strip = "";
|
|
1210
1336
|
env.ownKey = field.key;
|
|
1211
1337
|
const value = await this.evaluateNode(field.ast, env);
|
|
@@ -1223,7 +1349,7 @@ export default class Inibase {
|
|
|
1223
1349
|
case "bin": {
|
|
1224
1350
|
const a = await this.evaluateNode(node.left, env);
|
|
1225
1351
|
const b = await this.evaluateNode(node.right, env);
|
|
1226
|
-
return this.applyBinaryOp(node.op, a, b, env.ownKey, env.collect);
|
|
1352
|
+
return this.applyBinaryOp(node.op, a, b, env.ownKey, env.collect, env.nullAsZero);
|
|
1227
1353
|
}
|
|
1228
1354
|
case "fn": {
|
|
1229
1355
|
const arrayRef = env.index.get(node.arrayFieldId);
|
|
@@ -1283,7 +1409,12 @@ export default class Inibase {
|
|
|
1283
1409
|
strip = "";
|
|
1284
1410
|
}
|
|
1285
1411
|
else {
|
|
1286
|
-
// `value` is the id of the row this hop lives in.
|
|
1412
|
+
// `value` is the id of the row this hop lives in. Rows that
|
|
1413
|
+
// came off disk are eagerly resolved by processSchemaData, so
|
|
1414
|
+
// table children may already be linked objects — unwrap those
|
|
1415
|
+
// back to their id before the hop lookup. Nothing else hops.
|
|
1416
|
+
if (typeof value === "object" && value !== null)
|
|
1417
|
+
value = value.id;
|
|
1287
1418
|
if (value === undefined ||
|
|
1288
1419
|
value === null ||
|
|
1289
1420
|
value === "" ||
|
|
@@ -1302,8 +1433,7 @@ export default class Inibase {
|
|
|
1302
1433
|
? (env.links.record(table, ref.key, value), null)
|
|
1303
1434
|
: await env.links.read(table, ref.key, value)
|
|
1304
1435
|
: await this.readLinkedRow(table, value, ref.key);
|
|
1305
|
-
if (!env.collect &&
|
|
1306
|
-
(row === undefined || row === null))
|
|
1436
|
+
if (!env.collect && (row === undefined || row === null))
|
|
1307
1437
|
throw this.createError("COMPUTED_FIELD_DANGLING_LINK", [
|
|
1308
1438
|
env.ownKey,
|
|
1309
1439
|
table,
|
|
@@ -1352,18 +1482,28 @@ export default class Inibase {
|
|
|
1352
1482
|
]);
|
|
1353
1483
|
return num;
|
|
1354
1484
|
}
|
|
1355
|
-
applyBinaryOp(op, a, b, ownKey, collect = false) {
|
|
1485
|
+
applyBinaryOp(op, a, b, ownKey, collect = false, nullAsZero = false) {
|
|
1356
1486
|
if (a === null || a === undefined || b === null || b === undefined) {
|
|
1357
1487
|
// Collection passes must tolerate unresolved (null) link hops:
|
|
1358
1488
|
// the pass only discovers which links are needed, so a neutral
|
|
1359
1489
|
// result is fine. Real evaluation still throws.
|
|
1360
1490
|
if (collect)
|
|
1361
1491
|
return 0;
|
|
1492
|
+
// Element computed evaluation treats a missing operand as the
|
|
1493
|
+
// value 0 (per the element-sum feature), so a single missing
|
|
1494
|
+
// child never aborts the write.
|
|
1495
|
+
if (nullAsZero) {
|
|
1496
|
+
if (a === null || a === undefined)
|
|
1497
|
+
a = 0;
|
|
1498
|
+
if (b === null || b === undefined)
|
|
1499
|
+
b = 0;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
if (a === null || a === undefined || b === null || b === undefined)
|
|
1362
1503
|
throw this.createError("COMPUTED_FIELD_ARITHMETIC", [
|
|
1363
1504
|
ownKey,
|
|
1364
1505
|
"missing or null operand",
|
|
1365
1506
|
]);
|
|
1366
|
-
}
|
|
1367
1507
|
const an = this.coerceNumber(a, ownKey);
|
|
1368
1508
|
const bn = this.coerceNumber(b, ownKey);
|
|
1369
1509
|
switch (op) {
|
|
@@ -1447,9 +1587,11 @@ export default class Inibase {
|
|
|
1447
1587
|
if (existing) {
|
|
1448
1588
|
let matches = true;
|
|
1449
1589
|
for (const [line, values] of entries) {
|
|
1590
|
+
if (!Object.hasOwn(values, field.key))
|
|
1591
|
+
continue;
|
|
1592
|
+
const lineValue = values[field.key];
|
|
1450
1593
|
const old = existing[Number(line)]?.[field.key];
|
|
1451
|
-
|
|
1452
|
-
if (old === undefined || File.encode(old) !== fresh) {
|
|
1594
|
+
if (old === undefined || !this.sameCellValue(lineValue, old)) {
|
|
1453
1595
|
matches = false;
|
|
1454
1596
|
break;
|
|
1455
1597
|
}
|
|
@@ -1459,10 +1601,26 @@ export default class Inibase {
|
|
|
1459
1601
|
}
|
|
1460
1602
|
const content = pathesContents[path] ?? {};
|
|
1461
1603
|
for (const [line, values] of entries)
|
|
1462
|
-
|
|
1604
|
+
if (Object.hasOwn(values, field.key) && values[field.key] !== undefined)
|
|
1605
|
+
content[Number(line)] = File.encode(values[field.key]);
|
|
1463
1606
|
pathesContents[path] = content;
|
|
1464
1607
|
}
|
|
1465
1608
|
}
|
|
1609
|
+
/** Compare a freshly evaluated cell value against a stored (already
|
|
1610
|
+
* decoded) one — both are scalar or index-aligned element arrays. */
|
|
1611
|
+
sameCellValue(fresh, stored) {
|
|
1612
|
+
if (fresh === stored)
|
|
1613
|
+
return true;
|
|
1614
|
+
if (Array.isArray(fresh) &&
|
|
1615
|
+
Array.isArray(stored) &&
|
|
1616
|
+
fresh.length === stored.length) {
|
|
1617
|
+
for (let i = 0; i < fresh.length; i++)
|
|
1618
|
+
if (fresh[i] !== stored[i])
|
|
1619
|
+
return false;
|
|
1620
|
+
return true;
|
|
1621
|
+
}
|
|
1622
|
+
return File.encode(fresh) === File.encode(stored);
|
|
1623
|
+
}
|
|
1466
1624
|
// Helper function to determine if a field is simple
|
|
1467
1625
|
isSimpleField(fieldType) {
|
|
1468
1626
|
const complexTypes = ["array", "object", "table"];
|
|
@@ -3214,20 +3372,30 @@ export default class Inibase {
|
|
|
3214
3372
|
}
|
|
3215
3373
|
}
|
|
3216
3374
|
}
|
|
3217
|
-
async sum(tableName, columns, where) {
|
|
3375
|
+
async sum(tableName, columns, where, options) {
|
|
3218
3376
|
this.validateName(tableName);
|
|
3219
3377
|
if (!Array.isArray(columns))
|
|
3220
3378
|
columns = [columns];
|
|
3221
|
-
for (const column of columns)
|
|
3222
|
-
|
|
3379
|
+
for (const column of columns) {
|
|
3380
|
+
// Nested columns are dotted child paths (validated structurally by
|
|
3381
|
+
// `sumNested`); everything else must be a plain safe name.
|
|
3382
|
+
if (!options?.nested)
|
|
3383
|
+
this.validateName(column);
|
|
3384
|
+
}
|
|
3223
3385
|
await this.throwErrorIfTableEmpty(tableName);
|
|
3224
3386
|
const RETURN = {};
|
|
3225
3387
|
const tablePath = join(this.databasePath, tableName);
|
|
3388
|
+
if (options?.nested) {
|
|
3389
|
+
const nested = await this.sumNested(tableName, columns, where, tablePath);
|
|
3390
|
+
return columns.length > 1 ? nested : Object.values(nested)[0];
|
|
3391
|
+
}
|
|
3226
3392
|
for await (const column of columns) {
|
|
3227
3393
|
const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
|
|
3228
3394
|
if (await File.isExists(columnPath)) {
|
|
3229
3395
|
if (where) {
|
|
3230
|
-
|
|
3396
|
+
// `{ perPage: -1 }` so criteria matching is never capped by
|
|
3397
|
+
// the default page size (15): every matching row counts.
|
|
3398
|
+
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3231
3399
|
RETURN[column] = lineNumbers
|
|
3232
3400
|
? await File.sum(columnPath, lineNumbers)
|
|
3233
3401
|
: 0;
|
|
@@ -3238,6 +3406,168 @@ export default class Inibase {
|
|
|
3238
3406
|
}
|
|
3239
3407
|
return columns.length > 1 ? RETURN : Object.values(RETURN)[0];
|
|
3240
3408
|
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Element-wise aggregation over an array-of-objects column (`nested` mode).
|
|
3411
|
+
*
|
|
3412
|
+
* `where` keys that resolve as children of the array root become per-element
|
|
3413
|
+
* predicates (AND); everything else is a row-level filter. Only elements
|
|
3414
|
+
* passing every element predicate, inside rows passing the row filters, are
|
|
3415
|
+
* accumulated.
|
|
3416
|
+
*/
|
|
3417
|
+
async sumNested(tableName, columns, where, tablePath) {
|
|
3418
|
+
const tablePathSafe = tablePath ?? join(this.databasePath, tableName);
|
|
3419
|
+
const schema = globalConfig[this.databasePath].tables?.get(tableName)?.schema ?? [];
|
|
3420
|
+
const RETURN = {};
|
|
3421
|
+
// Resolve the array-of-objects root from the summed columns (v1: exactly
|
|
3422
|
+
// one nesting level, e.g. "items.quantity" -> root "items").
|
|
3423
|
+
let rootKey = null;
|
|
3424
|
+
let rootField = null;
|
|
3425
|
+
const targetFields = new Map();
|
|
3426
|
+
for (const column of columns) {
|
|
3427
|
+
const dot = column.indexOf(".");
|
|
3428
|
+
if (dot <= 0 || dot === column.length - 1 || column.indexOf(".", dot + 1) !== -1)
|
|
3429
|
+
throw this.createError("INVALID_PARAMETERS", [
|
|
3430
|
+
`sum nested: '${column}' must be a single-level child path of an array-of-objects column`,
|
|
3431
|
+
]);
|
|
3432
|
+
const rk = column.slice(0, dot);
|
|
3433
|
+
if (rootKey !== null && rk !== rootKey)
|
|
3434
|
+
throw this.createError("INVALID_PARAMETERS", [
|
|
3435
|
+
"sum nested: all summed columns must share the same array root",
|
|
3436
|
+
]);
|
|
3437
|
+
rootKey = rk;
|
|
3438
|
+
// NB: `getField` on a single segment that resolves to an
|
|
3439
|
+
// array-of-objects field returns its *first child*, so resolve the
|
|
3440
|
+
// root field directly from the top-level schema.
|
|
3441
|
+
rootField = schema.find((f) => f.key === rk) ?? null;
|
|
3442
|
+
const okRoot = rootField &&
|
|
3443
|
+
rootField.type === "array" &&
|
|
3444
|
+
Array.isArray(rootField.children) &&
|
|
3445
|
+
Utils.isArrayOfObjects(rootField.children);
|
|
3446
|
+
if (!okRoot)
|
|
3447
|
+
throw this.createError("INVALID_PARAMETERS", [
|
|
3448
|
+
`sum nested: '${rk}' is not an array-of-objects column`,
|
|
3449
|
+
]);
|
|
3450
|
+
const child = Utils.getField(column, schema);
|
|
3451
|
+
if (!child || child.type === "array" || child.type === "object")
|
|
3452
|
+
throw this.createError("INVALID_PARAMETERS", [
|
|
3453
|
+
`sum nested: '${column}' does not resolve to a scalar child`,
|
|
3454
|
+
]);
|
|
3455
|
+
targetFields.set(column, child);
|
|
3456
|
+
}
|
|
3457
|
+
if (!rootKey)
|
|
3458
|
+
return RETURN;
|
|
3459
|
+
// Split `where` into element predicates (children of the root) and a
|
|
3460
|
+
// row-level filter (everything else, incl. `and`/`or` groups).
|
|
3461
|
+
const elementPredicates = [];
|
|
3462
|
+
const rowWhere = {};
|
|
3463
|
+
let idWhere;
|
|
3464
|
+
if (where && Utils.isObject(where)) {
|
|
3465
|
+
for (const [key, value] of Object.entries(where)) {
|
|
3466
|
+
if (key === "and" || key === "or") {
|
|
3467
|
+
rowWhere[key] = value;
|
|
3468
|
+
continue;
|
|
3469
|
+
}
|
|
3470
|
+
const candidate = key.startsWith(`${rootKey}.`)
|
|
3471
|
+
? key
|
|
3472
|
+
: `${rootKey}.${key}`;
|
|
3473
|
+
const child = Utils.getField(candidate, schema);
|
|
3474
|
+
if (child) {
|
|
3475
|
+
if (Utils.isObject(value))
|
|
3476
|
+
throw this.createError("INVALID_PARAMETERS", [
|
|
3477
|
+
`sum nested: element predicate '${key}' must be a scalar, not an object`,
|
|
3478
|
+
]);
|
|
3479
|
+
const [operator, comparedValue] = typeof value === "string"
|
|
3480
|
+
? Utils.FormatObjectCriteriaValue(value)
|
|
3481
|
+
: ["=", value];
|
|
3482
|
+
if (operator === "[]" || operator === "![]")
|
|
3483
|
+
throw this.createError("INVALID_PARAMETERS", [
|
|
3484
|
+
`sum nested: operator '${operator}' is not supported on element predicates`,
|
|
3485
|
+
]);
|
|
3486
|
+
elementPredicates.push({
|
|
3487
|
+
field: child,
|
|
3488
|
+
operator,
|
|
3489
|
+
comparedValue,
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
else {
|
|
3493
|
+
if (!Utils.getField(key, schema))
|
|
3494
|
+
throw this.createError("INVALID_PARAMETERS", [
|
|
3495
|
+
`sum nested: criteria key '${key}' matches neither the array root nor a row column`,
|
|
3496
|
+
]);
|
|
3497
|
+
rowWhere[key] = value;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
else if (typeof where === "number" ||
|
|
3502
|
+
typeof where === "string" ||
|
|
3503
|
+
Array.isArray(where)) {
|
|
3504
|
+
idWhere = where;
|
|
3505
|
+
}
|
|
3506
|
+
// Row-level narrowing (ids or criteria). `undefined` lines = whole file.
|
|
3507
|
+
let lines;
|
|
3508
|
+
if (idWhere !== undefined)
|
|
3509
|
+
lines = (await this.get(tableName, idWhere, { perPage: -1 }, undefined, true));
|
|
3510
|
+
else if (Object.keys(rowWhere).length)
|
|
3511
|
+
lines = (await this.get(tableName, rowWhere, { perPage: -1 }, undefined, true));
|
|
3512
|
+
if (lines === null) {
|
|
3513
|
+
for (const column of columns)
|
|
3514
|
+
RETURN[column] = 0;
|
|
3515
|
+
return RETURN;
|
|
3516
|
+
}
|
|
3517
|
+
const fieldOpt = (field) => ({
|
|
3518
|
+
...field,
|
|
3519
|
+
databasePath: this.databasePath,
|
|
3520
|
+
});
|
|
3521
|
+
const ext = this.getFileExtension(tableName);
|
|
3522
|
+
// Element predicate cells, read once for every summed column.
|
|
3523
|
+
const predCells = new Map();
|
|
3524
|
+
for (const p of elementPredicates) {
|
|
3525
|
+
const path = join(tablePathSafe, `${rootKey}.${p.field.key}${ext}`);
|
|
3526
|
+
if (!(await File.isExists(path)))
|
|
3527
|
+
continue;
|
|
3528
|
+
const cell = (await File.get(path, lines, fieldOpt(p.field)));
|
|
3529
|
+
if (cell)
|
|
3530
|
+
predCells.set(p.field.key, cell);
|
|
3531
|
+
}
|
|
3532
|
+
for (const [column, child] of targetFields) {
|
|
3533
|
+
const path = join(tablePathSafe, `${column}${ext}`);
|
|
3534
|
+
if (!(await File.isExists(path))) {
|
|
3535
|
+
RETURN[column] = 0;
|
|
3536
|
+
continue;
|
|
3537
|
+
}
|
|
3538
|
+
const cell = (await File.get(path, lines, fieldOpt(child)));
|
|
3539
|
+
if (!cell) {
|
|
3540
|
+
RETURN[column] = 0;
|
|
3541
|
+
continue;
|
|
3542
|
+
}
|
|
3543
|
+
let sum = 0;
|
|
3544
|
+
for (const [lineStr, values] of Object.entries(cell)) {
|
|
3545
|
+
if (!Array.isArray(values))
|
|
3546
|
+
continue;
|
|
3547
|
+
for (let i = 0; i < values.length; i++) {
|
|
3548
|
+
const v = values[i];
|
|
3549
|
+
if (v === null || v === undefined)
|
|
3550
|
+
continue;
|
|
3551
|
+
let matches = true;
|
|
3552
|
+
for (const p of elementPredicates) {
|
|
3553
|
+
const pv = predCells.get(p.field.key)?.[Number(lineStr)]?.[i];
|
|
3554
|
+
if (pv === undefined || !UtilsServer.compare(p.operator, pv, p.comparedValue, p.field.type)) {
|
|
3555
|
+
matches = false;
|
|
3556
|
+
break;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
if (!matches)
|
|
3560
|
+
continue;
|
|
3561
|
+
const num = Number(v);
|
|
3562
|
+
if (Number.isNaN(num))
|
|
3563
|
+
continue;
|
|
3564
|
+
sum += num;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
RETURN[column] = sum;
|
|
3568
|
+
}
|
|
3569
|
+
return RETURN;
|
|
3570
|
+
}
|
|
3241
3571
|
async avg(tableName, columns, where) {
|
|
3242
3572
|
this.validateName(tableName);
|
|
3243
3573
|
if (!Array.isArray(columns))
|
|
@@ -3251,7 +3581,7 @@ export default class Inibase {
|
|
|
3251
3581
|
const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
|
|
3252
3582
|
if (await File.isExists(columnPath)) {
|
|
3253
3583
|
if (where) {
|
|
3254
|
-
const lineNumbers = await this.get(tableName, where,
|
|
3584
|
+
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3255
3585
|
RETURN[column] = lineNumbers
|
|
3256
3586
|
? await File.avg(columnPath, lineNumbers)
|
|
3257
3587
|
: 0;
|
|
@@ -3275,7 +3605,7 @@ export default class Inibase {
|
|
|
3275
3605
|
const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
|
|
3276
3606
|
if (await File.isExists(columnPath)) {
|
|
3277
3607
|
if (where) {
|
|
3278
|
-
const lineNumbers = await this.get(tableName, where,
|
|
3608
|
+
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3279
3609
|
RETURN[column] = lineNumbers
|
|
3280
3610
|
? await File.max(columnPath, lineNumbers)
|
|
3281
3611
|
: 0;
|
|
@@ -3299,7 +3629,7 @@ export default class Inibase {
|
|
|
3299
3629
|
const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
|
|
3300
3630
|
if (await File.isExists(columnPath)) {
|
|
3301
3631
|
if (where) {
|
|
3302
|
-
const lineNumbers = await this.get(tableName, where,
|
|
3632
|
+
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3303
3633
|
RETURN[column] = lineNumbers
|
|
3304
3634
|
? await File.min(columnPath, lineNumbers)
|
|
3305
3635
|
: 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inibase",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Karim Amahtil",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"benchmark": "./benchmark/run.js",
|
|
89
89
|
"benchmark:durability": "tsx ./benchmark/durability.ts",
|
|
90
90
|
"benchmark:computed": "tsx ./benchmark/computed.ts",
|
|
91
|
-
"test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts",
|
|
91
|
+
"test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts && tsx ./tests/sum.nested.test.ts && tsx ./tests/computed.child.test.ts",
|
|
92
92
|
"test:durability": "tsx ./tests/durability.test.ts",
|
|
93
93
|
"test:transaction": "tsx ./tests/transaction.test.ts",
|
|
94
94
|
"test:utils": "tsx ./tests/utils.test.ts",
|