@atscript/db 0.1.148 → 0.1.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/{buckets-DJiYlMXc.d.cts → buckets-24Ac4sIS.d.cts} +80 -5
- package/dist/{buckets-CNdTOnei.d.mts → buckets-XOz0Bw5x.d.mts} +80 -5
- package/dist/{column-diff-CUU4GvYg.cjs → column-diff-CKKBVDvx.cjs} +646 -341
- package/dist/{column-diff-Cp6ZoyRE.mjs → column-diff-CKLDWBbo.mjs} +522 -253
- package/dist/{fk-diff-BQ4krij8.d.cts → fk-diff-CCLiJ9sJ.d.cts} +2 -1
- package/dist/{fk-diff-DsaIijVX.d.mts → fk-diff-R8eU7gq-.d.mts} +2 -1
- package/dist/index.cjs +10 -3
- package/dist/index.d.cts +66 -4
- package/dist/index.d.mts +66 -4
- package/dist/index.mjs +3 -3
- package/dist/plugin.cjs +143 -36
- package/dist/plugin.mjs +138 -31
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/{relation-helpers-kX7jjgME.d.cts → relation-helpers-DjVJN2ng.d.cts} +1 -1
- package/dist/{relation-helpers-Ba0v49sn.d.mts → relation-helpers-H7UEVkHs.d.mts} +1 -1
- package/dist/search-fields-DBIZKwyb.mjs +117 -0
- package/dist/search-fields-DwrsJUC1.cjs +158 -0
- package/dist/shared.cjs +3 -1
- package/dist/shared.d.cts +19 -1
- package/dist/shared.d.mts +19 -1
- package/dist/shared.mjs +2 -2
- package/dist/sync.cjs +1 -1
- package/dist/sync.d.cts +2 -2
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +1 -1
- package/dist/{validation-utils-Dq0uZ7ef.mjs → validation-utils-DqZAH6aT.mjs} +44 -2
- package/dist/{validation-utils-Da2GjobR.cjs → validation-utils-debIYpq0.cjs} +56 -2
- package/package.json +6 -6
- package/dist/derived-rules-0sKn4f5C.mjs +0 -44
- package/dist/derived-rules-YstgIxG-.cjs +0 -67
- package/dist/numeric-operand-B1jKH7x5.mjs +0 -21
- package/dist/numeric-operand-DKfiRLYp.cjs +0 -26
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
const require_db_error = require("./db-error-DRQH4sLY.cjs");
|
|
2
2
|
const require_aggregate_fns = require("./aggregate-fns-C-UJRobm.cjs");
|
|
3
3
|
const require_nested_writer = require("./nested-writer-BUQvk6QT.cjs");
|
|
4
|
-
const
|
|
4
|
+
const require_search_fields = require("./search-fields-DwrsJUC1.cjs");
|
|
5
5
|
const require_object = require("./object-Djg28csK.cjs");
|
|
6
6
|
require("./agg.cjs");
|
|
7
|
-
const require_numeric_operand = require("./numeric-operand-DKfiRLYp.cjs");
|
|
8
7
|
const require_aggregate_rules = require("./aggregate-rules-D_bsCpUI.cjs");
|
|
9
8
|
const require_ops = require("./ops.cjs");
|
|
10
9
|
const require_validator = require("./validator-UcuJxHNT.cjs");
|
|
@@ -396,6 +395,10 @@ function isGeoIndexableType(fieldType) {
|
|
|
396
395
|
}
|
|
397
396
|
return false;
|
|
398
397
|
}
|
|
398
|
+
/** Runtime mirror of the `@db.column.version.exempt` placement errors. */
|
|
399
|
+
function rejectVersionExempt(path, why) {
|
|
400
|
+
throw new Error(`@db.column.version.exempt on "${path}": ${why}`);
|
|
401
|
+
}
|
|
399
402
|
/**
|
|
400
403
|
* Computed metadata for a database table or view.
|
|
401
404
|
*
|
|
@@ -443,6 +446,15 @@ var TableMetadata = class {
|
|
|
443
446
|
measures = [];
|
|
444
447
|
/** Logical field name annotated with `@db.column.version`, if any. */
|
|
445
448
|
versionField;
|
|
449
|
+
/** Paths annotated with `@db.column.version.exempt`, as declared (since 0.1.150). */
|
|
450
|
+
_versionExemptDeclared = /* @__PURE__ */ new Set();
|
|
451
|
+
/**
|
|
452
|
+
* Version-exempt paths (since 0.1.150): the declared ones plus every object
|
|
453
|
+
* whose direct children are all exempt (upward closure). A path is exempt
|
|
454
|
+
* when it, or an ancestor, is in this set — see {@link isVersionExemptPath}.
|
|
455
|
+
* Empty when the table declares no `@db.column.version`.
|
|
456
|
+
*/
|
|
457
|
+
versionExemptPaths = /* @__PURE__ */ new Set();
|
|
446
458
|
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
447
459
|
quantityRefByField = /* @__PURE__ */ new Map();
|
|
448
460
|
/** Logical paths annotated with `@db.encrypted` — stored as one opaque ciphertext column. */
|
|
@@ -599,6 +611,7 @@ var TableMetadata = class {
|
|
|
599
611
|
...this.originalMetaIdFields
|
|
600
612
|
]);
|
|
601
613
|
this._finalizeIndexes();
|
|
614
|
+
this._finalizeVersionExempt();
|
|
602
615
|
this._collateMap.clear();
|
|
603
616
|
this._columnFromMap.clear();
|
|
604
617
|
this.jsonFields.clear();
|
|
@@ -700,6 +713,15 @@ var TableMetadata = class {
|
|
|
700
713
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
701
714
|
this._addIndexField("unique", name, fieldName);
|
|
702
715
|
}
|
|
716
|
+
if (metadata.has("db.writeOnly") && (metadata.has("db.index.fulltext") || metadata.has("db.column.searchable"))) {
|
|
717
|
+
const which = metadata.has("db.index.fulltext") ? "@db.index.fulltext" : "@db.column.searchable";
|
|
718
|
+
throw new Error(`@db.writeOnly cannot coexist with ${which} on "${fieldName}" — search results would reveal the sealed value`);
|
|
719
|
+
}
|
|
720
|
+
const searchKind = require_search_fields.searchMemberKind(fieldType);
|
|
721
|
+
if (typeof searchKind === "object") {
|
|
722
|
+
if (metadata.has("db.index.fulltext")) throw new Error(`@db.index.fulltext on "${fieldName}" ${searchKind.problem} — a fulltext member must be a string or an integer`);
|
|
723
|
+
if (metadata.has("db.column.searchable")) throw new Error(`@db.column.searchable on "${fieldName}" ${searchKind.problem} — a searchable column must be a string or an integer`);
|
|
724
|
+
}
|
|
703
725
|
for (const index of metadata.get("db.index.fulltext") || []) {
|
|
704
726
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
705
727
|
const weight = index !== true && typeof index === "object" ? index?.weight : void 0;
|
|
@@ -727,6 +749,7 @@ var TableMetadata = class {
|
|
|
727
749
|
if (!hasExplicitIndex) this._addIndexField("plain", fieldName, fieldName);
|
|
728
750
|
}
|
|
729
751
|
if (metadata.has("db.column.measure")) this.measures.push(fieldName);
|
|
752
|
+
if (metadata.has("db.column.version.exempt")) this._versionExemptDeclared.add(fieldName);
|
|
730
753
|
if (metadata.has("db.column.version")) if (this.versionField !== void 0) logger.warn(`@db.column.version declared on multiple fields ("${this.versionField}" and "${fieldName}") — only one is allowed; using "${this.versionField}"`);
|
|
731
754
|
else {
|
|
732
755
|
this.versionField = fieldName;
|
|
@@ -737,6 +760,55 @@ var TableMetadata = class {
|
|
|
737
760
|
}
|
|
738
761
|
}
|
|
739
762
|
/**
|
|
763
|
+
* Validates the `@db.column.version.exempt` placements (E1–E5, the runtime
|
|
764
|
+
* mirror of the compile-time check, so pre-compiled models fail fast) and
|
|
765
|
+
* computes {@link versionExemptPaths} with its upward closure. Runs after
|
|
766
|
+
* `_applyOverrides` (the primary keys are final) and before `jsonFields` is
|
|
767
|
+
* released. A table without a version column ignores the annotation.
|
|
768
|
+
*/
|
|
769
|
+
_finalizeVersionExempt() {
|
|
770
|
+
if (this._versionExemptDeclared.size === 0) return;
|
|
771
|
+
for (const path of this._versionExemptDeclared) {
|
|
772
|
+
if (path === this.versionField) rejectVersionExempt(path, "cannot mark the version column itself");
|
|
773
|
+
if (this.primaryKeys.includes(path)) rejectVersionExempt(path, "a primary key identifies the row and is never patched");
|
|
774
|
+
if (this.navFields.has(path)) rejectVersionExempt(path, "a navigation field has no column here");
|
|
775
|
+
let pos = path.length;
|
|
776
|
+
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
777
|
+
const ancestor = path.slice(0, pos);
|
|
778
|
+
const node = this.flatMap.get(ancestor);
|
|
779
|
+
if (node?.metadata.has("db.json")) rejectVersionExempt(path, `mark the @db.json field "${ancestor}" itself — a JSON column is written as one value`);
|
|
780
|
+
if (node?.type.kind === "array") rejectVersionExempt(path, `mark the array field "${ancestor}" itself — array elements are not separate columns`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (this.versionField === void 0) return;
|
|
784
|
+
const exempt = new Set(this._versionExemptDeclared);
|
|
785
|
+
const objects = [...this.flatMap.keys()].filter((p) => {
|
|
786
|
+
const node = this.flatMap.get(p);
|
|
787
|
+
return node.type.kind === "object" && !node.metadata.has("db.json");
|
|
788
|
+
}).toSorted((a, b) => b.split(".").length - a.split(".").length);
|
|
789
|
+
for (const obj of objects) {
|
|
790
|
+
if (exempt.has(obj)) continue;
|
|
791
|
+
const prefix = `${obj}.`;
|
|
792
|
+
let children = 0;
|
|
793
|
+
let all = true;
|
|
794
|
+
for (const key of this.flatMap.keys()) {
|
|
795
|
+
if (!key.startsWith(prefix) || key.indexOf(".", prefix.length) !== -1) continue;
|
|
796
|
+
if (this.navFields.has(key) || this.ignoredFields.has(key)) continue;
|
|
797
|
+
children++;
|
|
798
|
+
if (!exempt.has(key)) {
|
|
799
|
+
all = false;
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (children > 0 && all) exempt.add(obj);
|
|
804
|
+
}
|
|
805
|
+
this.versionExemptPaths = exempt;
|
|
806
|
+
}
|
|
807
|
+
/** Whether `path` — or an ancestor of it — is version-exempt (since 0.1.150). */
|
|
808
|
+
isVersionExemptPath(path) {
|
|
809
|
+
return require_object.selfOrAncestor(path, this.versionExemptPaths) !== void 0;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
740
812
|
* Build-time diagnostics for `@db.encrypted` (§6 of the field-encryption
|
|
741
813
|
* spec). Mirrors the compile-time AnnotationSpec validation so models built
|
|
742
814
|
* from pre-compiled types still fail fast.
|
|
@@ -766,7 +838,7 @@ var TableMetadata = class {
|
|
|
766
838
|
throw new Error(`@db.column.derived on "${fieldName}": ${why}`);
|
|
767
839
|
};
|
|
768
840
|
if (fieldName.includes(".")) reject("only a top-level field of a table can be derived");
|
|
769
|
-
for (const [name, why] of
|
|
841
|
+
for (const [name, why] of require_search_fields.DERIVED_INCOMPATIBLE) if (metadata.has(name)) reject(`cannot coexist with @${name} — ${why}`);
|
|
770
842
|
const ref = fieldType.ref;
|
|
771
843
|
if (!ref?.field) reject("requires a chain reference into a @db.json field of the same table (e.g. `customerId: Order.payload.customer.id`)");
|
|
772
844
|
const target = ref.type();
|
|
@@ -784,7 +856,7 @@ var TableMetadata = class {
|
|
|
784
856
|
}
|
|
785
857
|
if (jsonRoot === void 0 || jsonRoot === sourcePath) reject(`path "${sourcePath}" does not read inside a @db.json field — a flattened or scalar column needs no derived column`);
|
|
786
858
|
const leafType = resolveDesignType(this.flatMap.get(sourcePath));
|
|
787
|
-
if (!
|
|
859
|
+
if (!require_search_fields.isJsonLeafType(leafType)) return reject(`path "${sourcePath}" must end at a string, number or boolean leaf (got "${leafType}")`);
|
|
788
860
|
const source = resolveViewSource(rootType, sourcePath, false);
|
|
789
861
|
return {
|
|
790
862
|
sourcePath,
|
|
@@ -1070,8 +1142,28 @@ var TableMetadata = class {
|
|
|
1070
1142
|
};
|
|
1071
1143
|
}
|
|
1072
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Flags the integer members of fulltext indexes (matched by exact number,
|
|
1147
|
+
* never part of the physical text index) and enforces that each is
|
|
1148
|
+
* index-backed — the equality branch must not scan. Runs while index field
|
|
1149
|
+
* names are still logical.
|
|
1150
|
+
*/
|
|
1151
|
+
_resolveIntegerFulltextMembers() {
|
|
1152
|
+
for (const index of this.indexes.values()) {
|
|
1153
|
+
if (index.type !== "fulltext") continue;
|
|
1154
|
+
for (const field of index.fields) {
|
|
1155
|
+
const ftype = this.flatMap.get(field.name);
|
|
1156
|
+
if (!ftype || require_search_fields.searchMemberKind(ftype) !== "integer") continue;
|
|
1157
|
+
const where = `@db.index.fulltext on the integer field "${field.name}"`;
|
|
1158
|
+
if (require_object.findAncestorInSet(field.name, this.jsonFields) !== void 0) throw new Error(`${where} inside a JSON value is not supported`);
|
|
1159
|
+
if (!(field.name === "_id" || this.originalMetaIdFields[0] === field.name || this.uniqueProps.has(field.name) || [...this.indexes.values()].some((other) => (other.type === "plain" || other.type === "unique") && other.fields[0]?.name === field.name))) throw new Error(`${where} needs an index for its exact-number match — make it the primary key (first @meta.id) or the first field of a @db.index.plain / @db.index.unique`);
|
|
1160
|
+
field.integer = true;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1073
1164
|
_finalizeIndexes() {
|
|
1074
1165
|
for (const index of this.indexes.values()) if (index.type === "unique" && index.fields.length === 1) this.uniqueProps.add(index.fields[0].name);
|
|
1166
|
+
this._resolveIntegerFulltextMembers();
|
|
1075
1167
|
for (const index of this.indexes.values()) for (const field of index.fields) {
|
|
1076
1168
|
const ftype = this.flatMap.get(field.name);
|
|
1077
1169
|
if (ftype) {
|
|
@@ -1154,6 +1246,334 @@ function computedMeta(rootType, path) {
|
|
|
1154
1246
|
} : void 0;
|
|
1155
1247
|
}
|
|
1156
1248
|
//#endregion
|
|
1249
|
+
//#region src/shared/search-term.ts
|
|
1250
|
+
/**
|
|
1251
|
+
* Internal filter operator (since 0.1.150): a regular expression over the
|
|
1252
|
+
* DECIMAL TEXT of an integer column. The core rewrites a caller's `$regex`
|
|
1253
|
+
* (or bare `RegExp`) on an integer field into it at translation time;
|
|
1254
|
+
* callers can never send it (the filter value guard refuses it).
|
|
1255
|
+
*/
|
|
1256
|
+
const INTEGER_REGEX_OP = "$integerRegex";
|
|
1257
|
+
const INTEGER_TERM_RE = /^-?(?:0|[1-9][0-9]*)$/;
|
|
1258
|
+
/**
|
|
1259
|
+
* The whole number a `$search` term denotes, or `undefined` (since 0.1.150).
|
|
1260
|
+
* The trimmed term must be a plain integer literal — ASCII digits with an
|
|
1261
|
+
* optional leading `-`, no `+`, no leading zeros (except `0` itself), no
|
|
1262
|
+
* `-0`, no separators / decimals / exponent — within the safe-integer range.
|
|
1263
|
+
* Only the whole term counts (`"invoice 2946"` has no number).
|
|
1264
|
+
*/
|
|
1265
|
+
function searchTermInteger(term) {
|
|
1266
|
+
const t = term.trim();
|
|
1267
|
+
if (!INTEGER_TERM_RE.test(t) || t === "-0") return void 0;
|
|
1268
|
+
const n = Number(t);
|
|
1269
|
+
return Number.isSafeInteger(n) ? n : void 0;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Splits a fulltext index into the members that belong to the physical text
|
|
1273
|
+
* index and the integer members matched by exact number (since 0.1.150).
|
|
1274
|
+
*/
|
|
1275
|
+
function splitFulltextFields(index) {
|
|
1276
|
+
const text = [];
|
|
1277
|
+
const integer = [];
|
|
1278
|
+
for (const f of index.fields) (f.integer ? integer : text).push(f);
|
|
1279
|
+
return {
|
|
1280
|
+
text,
|
|
1281
|
+
integer
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* `/meta` description of a fulltext index: the adapter's text-index label over
|
|
1286
|
+
* the text members, then the exact-number members (since 0.1.150).
|
|
1287
|
+
*/
|
|
1288
|
+
function describeFulltext(index, textLabel) {
|
|
1289
|
+
const { text, integer } = splitFulltextFields(index);
|
|
1290
|
+
return [text.length > 0 ? textLabel(text.map((f) => f.name).join(", ")) : "", integer.map((f) => `exact number on ${f.name}`).join(" + ")].filter(Boolean).join(" + ");
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* The fulltext index that answers a search naming none (since 0.1.150): the
|
|
1294
|
+
* first one with at least one TEXT member, else the first one (an index of
|
|
1295
|
+
* integer members only answers exact-number terms).
|
|
1296
|
+
*/
|
|
1297
|
+
function defaultFulltextIndex(indexes) {
|
|
1298
|
+
return indexes.find((i) => splitFulltextFields(i).text.length > 0) ?? indexes[0];
|
|
1299
|
+
}
|
|
1300
|
+
//#endregion
|
|
1301
|
+
//#region src/query/filter-values.ts
|
|
1302
|
+
const OPAQUE = {
|
|
1303
|
+
kinds: new Set(["any"]),
|
|
1304
|
+
timestamp: false
|
|
1305
|
+
};
|
|
1306
|
+
const NUMBER = {
|
|
1307
|
+
kinds: new Set(["number"]),
|
|
1308
|
+
timestamp: false
|
|
1309
|
+
};
|
|
1310
|
+
const INTEGER = {
|
|
1311
|
+
kinds: new Set(["integer"]),
|
|
1312
|
+
timestamp: false
|
|
1313
|
+
};
|
|
1314
|
+
const STRING = {
|
|
1315
|
+
kinds: new Set(["string"]),
|
|
1316
|
+
timestamp: false
|
|
1317
|
+
};
|
|
1318
|
+
function collectKinds(type, kinds, out, depth = 0) {
|
|
1319
|
+
const def = type?.type;
|
|
1320
|
+
const metadata = type?.metadata;
|
|
1321
|
+
if (!def || depth > 8) {
|
|
1322
|
+
kinds.add("any");
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
switch (def.kind) {
|
|
1326
|
+
case "": switch (def.designType) {
|
|
1327
|
+
case "string":
|
|
1328
|
+
kinds.add("string");
|
|
1329
|
+
return;
|
|
1330
|
+
case "number": {
|
|
1331
|
+
const timestamp = def.tags?.has("timestamp") === true;
|
|
1332
|
+
if (timestamp) out.timestamp = true;
|
|
1333
|
+
const integer = timestamp || def.tags?.has("int") === true || metadata?.has?.("expect.int") === true;
|
|
1334
|
+
kinds.add(integer ? "integer" : "number");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
case "decimal":
|
|
1338
|
+
kinds.add("decimal");
|
|
1339
|
+
return;
|
|
1340
|
+
case "boolean":
|
|
1341
|
+
kinds.add("boolean");
|
|
1342
|
+
return;
|
|
1343
|
+
case "null":
|
|
1344
|
+
case "undefined":
|
|
1345
|
+
case "never": return;
|
|
1346
|
+
default:
|
|
1347
|
+
kinds.add("any");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
case "union":
|
|
1351
|
+
for (const item of def.items ?? []) collectKinds(item, kinds, out, depth + 1);
|
|
1352
|
+
return;
|
|
1353
|
+
case "array":
|
|
1354
|
+
collectKinds(def.of, kinds, out, depth + 1);
|
|
1355
|
+
return;
|
|
1356
|
+
default: kinds.add("any");
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const typeCache = /* @__PURE__ */ new WeakMap();
|
|
1360
|
+
/**
|
|
1361
|
+
* The value kinds a filter on `fd` accepts (cached per descriptor). A leaf
|
|
1362
|
+
* inside a JSON value (a `@db.json` object or an array, addressable on
|
|
1363
|
+
* nested-object adapters) is opaque: its contents are not schema-enforced.
|
|
1364
|
+
*/
|
|
1365
|
+
function valueTypeOf(meta, fd) {
|
|
1366
|
+
let vt = typeCache.get(fd);
|
|
1367
|
+
if (vt) return vt;
|
|
1368
|
+
if (jsonValueAncestor(fd.path, meta.jsonValueParents) !== void 0 || fd.encrypted || fd.isGeoPoint || fd.designType === "json" || fd.designType === "object" || !fd.type) vt = OPAQUE;
|
|
1369
|
+
else {
|
|
1370
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
1371
|
+
const out = { timestamp: false };
|
|
1372
|
+
collectKinds(fd.type, kinds, out);
|
|
1373
|
+
const metadata = fd.type.metadata;
|
|
1374
|
+
if ((fd.defaultValue?.kind === "fn" && fd.defaultValue.fn !== "uuid" || metadata?.has?.("db.agg.count") === true || metadata?.has?.("db.agg.countDistinct") === true) && kinds.delete("number")) kinds.add("integer");
|
|
1375
|
+
vt = kinds.size === 0 ? OPAQUE : {
|
|
1376
|
+
kinds,
|
|
1377
|
+
timestamp: out.timestamp,
|
|
1378
|
+
integerText: require_search_fields.searchMemberKind(fd.type) === "integer"
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
typeCache.set(fd, vt);
|
|
1382
|
+
return vt;
|
|
1383
|
+
}
|
|
1384
|
+
/** Whether `$regex` on `fd` matches the decimal text of an integer (since 0.1.150). */
|
|
1385
|
+
function isIntegerTextField(meta, fd) {
|
|
1386
|
+
return valueTypeOf(meta, fd).integerText === true;
|
|
1387
|
+
}
|
|
1388
|
+
/** A decimal literal (`5`, `-1.5`, `.5`, `1e3`), surrounding blanks allowed — no hex, no `Infinity`. */
|
|
1389
|
+
const NUMERIC_RE = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?\s*$/i;
|
|
1390
|
+
/** An integer literal (`5`, `-12`), surrounding blanks allowed. */
|
|
1391
|
+
const INTEGER_RE = /^\s*[+-]?\d+\s*$/;
|
|
1392
|
+
function acceptsScalar(kind, value) {
|
|
1393
|
+
switch (kind) {
|
|
1394
|
+
case "any": return true;
|
|
1395
|
+
case "string": return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint";
|
|
1396
|
+
case "number":
|
|
1397
|
+
case "decimal": return typeof value === "number" && Number.isFinite(value) || typeof value === "bigint" || typeof value === "string" && NUMERIC_RE.test(value);
|
|
1398
|
+
case "integer": return typeof value === "number" && Number.isInteger(value) || typeof value === "bigint" || typeof value === "string" && INTEGER_RE.test(value);
|
|
1399
|
+
default: return typeof value === "boolean" || value === 0 || value === 1;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
/** The first element of `value` (itself when not an array) `vt` rejects, or `undefined`. */
|
|
1403
|
+
function rejectedValue(vt, value) {
|
|
1404
|
+
if (vt.kinds.has("any") || value === null || value === void 0) return void 0;
|
|
1405
|
+
if (Array.isArray(value)) {
|
|
1406
|
+
for (const item of value) {
|
|
1407
|
+
const bad = rejectedValue(vt, item);
|
|
1408
|
+
if (bad) return bad;
|
|
1409
|
+
}
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (typeof value === "object" && !require_object.isPlainObject(value)) return void 0;
|
|
1413
|
+
for (const kind of vt.kinds) if (acceptsScalar(kind, value)) return void 0;
|
|
1414
|
+
return { value };
|
|
1415
|
+
}
|
|
1416
|
+
const KIND_LABEL = {
|
|
1417
|
+
string: "a string",
|
|
1418
|
+
number: "a number",
|
|
1419
|
+
integer: "an integer",
|
|
1420
|
+
decimal: "a decimal (number or numeric string)",
|
|
1421
|
+
boolean: "a boolean"
|
|
1422
|
+
};
|
|
1423
|
+
function expectedOf(vt) {
|
|
1424
|
+
const labels = [];
|
|
1425
|
+
for (const kind of vt.kinds) {
|
|
1426
|
+
if (kind === "any") continue;
|
|
1427
|
+
labels.push(kind === "integer" && vt.timestamp ? "an integer (epoch milliseconds)" : KIND_LABEL[kind]);
|
|
1428
|
+
}
|
|
1429
|
+
return labels.join(" or ");
|
|
1430
|
+
}
|
|
1431
|
+
function describeValue(value) {
|
|
1432
|
+
if (typeof value === "string") return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value);
|
|
1433
|
+
if (require_object.isPlainObject(value)) return "an object";
|
|
1434
|
+
return String(value);
|
|
1435
|
+
}
|
|
1436
|
+
function valueError(path, op, message) {
|
|
1437
|
+
return new require_db_error.DbError("INVALID_QUERY", [{
|
|
1438
|
+
path,
|
|
1439
|
+
message: `Invalid filter value for "${path}"${op ? ` (${op})` : ""}: ${message}`
|
|
1440
|
+
}]);
|
|
1441
|
+
}
|
|
1442
|
+
function holdsStrings(vt) {
|
|
1443
|
+
return vt.kinds.has("string") || vt.kinds.has("any") || vt.integerText === true;
|
|
1444
|
+
}
|
|
1445
|
+
function checkRegex(path, vt, op, pattern) {
|
|
1446
|
+
if (!holdsStrings(vt)) throw valueError(path, op, `a pattern match needs a string or integer field, "${path}" holds ${expectedOf(vt)}`);
|
|
1447
|
+
if (typeof pattern !== "string" && !(pattern instanceof RegExp)) throw valueError(path, op, `expected a regular expression, got ${describeValue(pattern)}`);
|
|
1448
|
+
}
|
|
1449
|
+
function checkValue(path, vt, op, value) {
|
|
1450
|
+
if (value instanceof RegExp) {
|
|
1451
|
+
checkRegex(path, vt, op ?? "RegExp", value);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const bad = rejectedValue(vt, value);
|
|
1455
|
+
if (bad) throw valueError(path, op, `expected ${expectedOf(vt)}, got ${describeValue(bad.value)}`);
|
|
1456
|
+
}
|
|
1457
|
+
/** Operators whose operand is compared with the field's values. */
|
|
1458
|
+
const COMPARE_OPS = new Set([
|
|
1459
|
+
"$eq",
|
|
1460
|
+
"$ne",
|
|
1461
|
+
"$gt",
|
|
1462
|
+
"$gte",
|
|
1463
|
+
"$lt",
|
|
1464
|
+
"$lte",
|
|
1465
|
+
"$in",
|
|
1466
|
+
"$nin"
|
|
1467
|
+
]);
|
|
1468
|
+
/** Checks one filter entry's value (a bare value or an operator map) against `vt`. */
|
|
1469
|
+
function checkEntry(path, vt, value) {
|
|
1470
|
+
if (vt.kinds.has("any")) return;
|
|
1471
|
+
if (!require_object.isPlainObject(value)) {
|
|
1472
|
+
checkValue(path, vt, void 0, value);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
for (const [op, operand] of Object.entries(value)) if (op === "$regex") checkRegex(path, vt, op, operand);
|
|
1476
|
+
else if (COMPARE_OPS.has(op)) checkValue(path, vt, op, operand);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Walks `filter` (through `$and` / `$or` / `$not`; relational predicates are
|
|
1480
|
+
* the related table's) and checks every entry whose key `typeOf` knows.
|
|
1481
|
+
*/
|
|
1482
|
+
function walkFilterValues(filter, typeOf) {
|
|
1483
|
+
if (!require_object.isPlainObject(filter)) return;
|
|
1484
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1485
|
+
if (key === "$and" || key === "$or") {
|
|
1486
|
+
if (Array.isArray(value)) for (const child of value) walkFilterValues(child, typeOf);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (key === "$not") {
|
|
1490
|
+
walkFilterValues(value, typeOf);
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (key.startsWith("$") || require_nested_writer.hasRelationOp(value)) continue;
|
|
1494
|
+
if (require_object.isPlainObject(value) && "$integerRegex" in value) throw valueError(key, INTEGER_REGEX_OP, "is internal — use $regex");
|
|
1495
|
+
const vt = typeOf(key);
|
|
1496
|
+
if (vt) checkEntry(key, vt, value);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Rejects (`INVALID_QUERY`, `path` = the field) a filter value that cannot
|
|
1501
|
+
* denote its field's declared type — see the module notes for the accepted
|
|
1502
|
+
* forms. Unknown paths are skipped (the path guard owns them).
|
|
1503
|
+
*/
|
|
1504
|
+
function guardFilterValues(meta, filter) {
|
|
1505
|
+
if (!filter) return;
|
|
1506
|
+
walkFilterValues(filter, (key) => {
|
|
1507
|
+
const fd = meta.descriptorByPath.get(key);
|
|
1508
|
+
return fd ? valueTypeOf(meta, fd) : void 0;
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* `$having` values: an aggregate alias is a number (`count`,
|
|
1513
|
+
* `countDistinct`, `sum`, `avg`, an expression) or its source field's type
|
|
1514
|
+
* (`min` / `max`, `first` / `last`),
|
|
1515
|
+
* a calendar-bucket alias a string label, any other key a `$groupBy`
|
|
1516
|
+
* field's own type.
|
|
1517
|
+
*/
|
|
1518
|
+
function guardHavingValues(meta, controls) {
|
|
1519
|
+
if (!controls?.$having) return;
|
|
1520
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1521
|
+
if (Array.isArray(controls.$select)) {
|
|
1522
|
+
for (const item of controls.$select) if ((0, _uniqu_core.isAggregateOfExpr)(item) || (0, _uniqu_core.isSelectArithExpr)(item)) aliases.set(item.$as, NUMBER);
|
|
1523
|
+
else if ((0, _uniqu_core.isAggregateExpr)(item)) {
|
|
1524
|
+
const fd = require_aggregate_fns.SOURCE_VALUE_FNS.has(item.$fn) ? meta.descriptorByPath.get(item.$field) : void 0;
|
|
1525
|
+
const counts = item.$fn === "count" || item.$fn === "countDistinct";
|
|
1526
|
+
aliases.set((0, _uniqu_core.resolveAlias)(item), fd ? valueTypeOf(meta, fd) : counts ? INTEGER : NUMBER);
|
|
1527
|
+
} else if ((0, _uniqu_core.isBucketExpr)(item)) aliases.set((0, _uniqu_core.resolveAlias)(item), STRING);
|
|
1528
|
+
}
|
|
1529
|
+
walkFilterValues(controls.$having, (key) => {
|
|
1530
|
+
const alias = aliases.get(key);
|
|
1531
|
+
if (alias) return alias;
|
|
1532
|
+
const fd = meta.descriptorByPath.get(key);
|
|
1533
|
+
return fd ? valueTypeOf(meta, fd) : void 0;
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
//#endregion
|
|
1537
|
+
//#region src/query/integer-regex.ts
|
|
1538
|
+
/** Rewrites one field entry; returns the same value when it has no pattern match. */
|
|
1539
|
+
function rewriteEntry(value) {
|
|
1540
|
+
if (value instanceof RegExp) return { [INTEGER_REGEX_OP]: value };
|
|
1541
|
+
if (require_object.isPlainObject(value) && "$regex" in value) {
|
|
1542
|
+
const out = {};
|
|
1543
|
+
for (const [op, operand] of Object.entries(value)) out[op === "$regex" ? INTEGER_REGEX_OP : op] = operand;
|
|
1544
|
+
return out;
|
|
1545
|
+
}
|
|
1546
|
+
return value;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* `$regex` (and a bare `RegExp`) on integer search members →
|
|
1550
|
+
* {@link INTEGER_REGEX_OP}, on the LOGICAL filter. Copy-on-write: the very
|
|
1551
|
+
* same object comes back when nothing changed. Relational predicates are
|
|
1552
|
+
* rewritten by the related table when it translates them.
|
|
1553
|
+
*/
|
|
1554
|
+
function rewriteIntegerRegex(filter, meta) {
|
|
1555
|
+
if (!require_object.isPlainObject(filter)) return filter;
|
|
1556
|
+
let out;
|
|
1557
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1558
|
+
let next = value;
|
|
1559
|
+
if (key === "$and" || key === "$or") {
|
|
1560
|
+
if (Array.isArray(value)) {
|
|
1561
|
+
const mapped = value.map((f) => rewriteIntegerRegex(f, meta));
|
|
1562
|
+
if (mapped.some((m, i) => m !== value[i])) next = mapped;
|
|
1563
|
+
}
|
|
1564
|
+
} else if (key === "$not") next = rewriteIntegerRegex(value, meta);
|
|
1565
|
+
else if (!key.startsWith("$") && !meta.navFields.has(key)) {
|
|
1566
|
+
const fd = meta.descriptorByPath.get(key);
|
|
1567
|
+
if (fd && isIntegerTextField(meta, fd)) next = rewriteEntry(value);
|
|
1568
|
+
}
|
|
1569
|
+
if (next !== value) {
|
|
1570
|
+
out ??= { ...filter };
|
|
1571
|
+
out[key] = next;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return out ?? filter;
|
|
1575
|
+
}
|
|
1576
|
+
//#endregion
|
|
1157
1577
|
//#region src/query/aggregate-expr.ts
|
|
1158
1578
|
/** Most nodes a group-level expression may have once the aliases it names are inlined (SQL repeats them). */
|
|
1159
1579
|
const EXPANDED_MAX_NODES = 4 * _uniqu_core.ARITH_MAX_NODES;
|
|
@@ -1171,7 +1591,7 @@ const QUERY_EXPANDED_MAX_NODES = 1024;
|
|
|
1171
1591
|
function numericOperandProblem(fd) {
|
|
1172
1592
|
if (fd.encrypted) return "is encrypted";
|
|
1173
1593
|
if (isJsonValueField(fd)) return "is not a scalar number";
|
|
1174
|
-
return
|
|
1594
|
+
return require_search_fields.numericTypeProblem({
|
|
1175
1595
|
base: fd.designType,
|
|
1176
1596
|
tags: (fd.type?.type)?.tags
|
|
1177
1597
|
});
|
|
@@ -1787,6 +2207,7 @@ var FieldMappingStrategy = class {
|
|
|
1787
2207
|
* translate predicate operands at deeper levels).
|
|
1788
2208
|
*/
|
|
1789
2209
|
translateFilter(filter, meta, depth = 0) {
|
|
2210
|
+
filter = rewriteIntegerRegex(filter, meta);
|
|
1790
2211
|
const has = require_nested_writer.containsRelationFilter(filter);
|
|
1791
2212
|
const resolved = has ? require_nested_writer.resolveRelationFilterTree(filter, meta, depth) : filter;
|
|
1792
2213
|
return this.noteTranslated(filter, this.translateResolvedFilter(resolved, meta), has);
|
|
@@ -1921,7 +2342,8 @@ var FieldMappingStrategy = class {
|
|
|
1921
2342
|
if (!require_object.isPlainObject(value)) return fmt(value);
|
|
1922
2343
|
const ops = value;
|
|
1923
2344
|
const formatted = {};
|
|
1924
|
-
for (const [op, opVal] of Object.entries(ops)) if (
|
|
2345
|
+
for (const [op, opVal] of Object.entries(ops)) if (op === "$integerRegex") formatted[op] = opVal;
|
|
2346
|
+
else if ((op === "$in" || op === "$nin") && Array.isArray(opVal)) formatted[op] = opVal.map((v) => v === null || v === void 0 ? v : fmt(v));
|
|
1925
2347
|
else if (op.startsWith("$") && opVal !== null && opVal !== void 0) formatted[op] = fmt(opVal);
|
|
1926
2348
|
else formatted[op] = opVal;
|
|
1927
2349
|
return formatted;
|
|
@@ -2118,7 +2540,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2118
2540
|
return result;
|
|
2119
2541
|
}
|
|
2120
2542
|
translateQuery(query, meta) {
|
|
2121
|
-
const logical = query.filter;
|
|
2543
|
+
const logical = rewriteIntegerRegex(query.filter, meta);
|
|
2122
2544
|
const has = require_nested_writer.containsRelationFilter(logical);
|
|
2123
2545
|
const filter = has ? require_nested_writer.resolveRelationFilterTree(logical, meta, 0) : logical;
|
|
2124
2546
|
if (!meta.requiresMappings) {
|
|
@@ -2192,337 +2614,107 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2192
2614
|
for (const key of Object.keys(update)) result[meta.leafByLogical.get(key)?.physicalName ?? key] = update[key];
|
|
2193
2615
|
return result;
|
|
2194
2616
|
}
|
|
2195
|
-
const result = {};
|
|
2196
|
-
const updateKeys = Object.keys(update);
|
|
2197
|
-
for (const key of updateKeys) {
|
|
2198
|
-
const value = update[key];
|
|
2199
|
-
const operatorMatch = key.match(/^(.+?)(\.__\$.+)$/);
|
|
2200
|
-
const basePath = operatorMatch ? operatorMatch[1] : key;
|
|
2201
|
-
const suffix = operatorMatch ? operatorMatch[2] : "";
|
|
2202
|
-
const fd = meta.leafByLogical.get(basePath);
|
|
2203
|
-
const finalKey = (fd?.physicalName ?? basePath) + suffix;
|
|
2204
|
-
if (fd?.storage === "json" && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
|
|
2205
|
-
else result[finalKey] = value;
|
|
2206
|
-
}
|
|
2207
|
-
return this.formatWriteValues(result, meta);
|
|
2208
|
-
}
|
|
2209
|
-
/**
|
|
2210
|
-
* Translates field names in sort and projection controls from
|
|
2211
|
-
* logical dot-paths to physical column names.
|
|
2212
|
-
*/
|
|
2213
|
-
translateControls(controls, meta) {
|
|
2214
|
-
if (!controls) return {};
|
|
2215
|
-
const result = {
|
|
2216
|
-
...controls,
|
|
2217
|
-
$select: void 0,
|
|
2218
|
-
$with: void 0
|
|
2219
|
-
};
|
|
2220
|
-
if (controls.$sort) {
|
|
2221
|
-
const translated = {};
|
|
2222
|
-
const sortObj = controls.$sort;
|
|
2223
|
-
const sortKeys = Object.keys(sortObj);
|
|
2224
|
-
for (const key of sortKeys) {
|
|
2225
|
-
if (meta.flattenedParents.has(key)) continue;
|
|
2226
|
-
const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
|
|
2227
|
-
translated[physical] = sortObj[key];
|
|
2228
|
-
}
|
|
2229
|
-
result.$sort = translated;
|
|
2230
|
-
}
|
|
2231
|
-
if (controls.$select) {
|
|
2232
|
-
let translatedRaw;
|
|
2233
|
-
if (Array.isArray(controls.$select)) {
|
|
2234
|
-
const expanded = [];
|
|
2235
|
-
for (const key of controls.$select) {
|
|
2236
|
-
const expansion = meta.selectExpansion.get(key);
|
|
2237
|
-
if (expansion) expanded.push(...expansion);
|
|
2238
|
-
else expanded.push(meta.leafByLogical.get(key)?.physicalName ?? key);
|
|
2239
|
-
}
|
|
2240
|
-
translatedRaw = expanded;
|
|
2241
|
-
} else {
|
|
2242
|
-
const translated = {};
|
|
2243
|
-
const selectObj = controls.$select;
|
|
2244
|
-
const selectKeys = Object.keys(selectObj);
|
|
2245
|
-
for (const key of selectKeys) {
|
|
2246
|
-
const val = selectObj[key];
|
|
2247
|
-
const expansion = meta.selectExpansion.get(key);
|
|
2248
|
-
if (expansion) for (const leaf of expansion) translated[leaf] = val;
|
|
2249
|
-
else {
|
|
2250
|
-
const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
|
|
2251
|
-
translated[physical] = val;
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
translatedRaw = translated;
|
|
2255
|
-
}
|
|
2256
|
-
result.$select = new UniquSelect(translatedRaw, meta.allPhysicalFields);
|
|
2257
|
-
}
|
|
2258
|
-
return result;
|
|
2259
|
-
}
|
|
2260
|
-
/**
|
|
2261
|
-
* Flattens nested object fields into __-separated keys and
|
|
2262
|
-
* JSON-stringifies @db.json / array fields.
|
|
2263
|
-
*/
|
|
2264
|
-
flattenPayload(data, meta) {
|
|
2265
|
-
const result = {};
|
|
2266
|
-
for (const key of Object.keys(data)) this.writeFlattenedField(key, data[key], result, meta);
|
|
2267
|
-
return result;
|
|
2268
|
-
}
|
|
2269
|
-
/**
|
|
2270
|
-
* Classifies and writes a single field to the result object.
|
|
2271
|
-
* Recurses into nested objects that should be flattened.
|
|
2272
|
-
*/
|
|
2273
|
-
writeFlattenedField(path, value, result, meta) {
|
|
2274
|
-
if (meta.ignoredFields.has(path)) return;
|
|
2275
|
-
if (meta.flattenedParents.has(path)) {
|
|
2276
|
-
if (value === null || value === void 0) this.setFlattenedChildrenNull(path, result, meta);
|
|
2277
|
-
else if (typeof value === "object" && !Array.isArray(value)) {
|
|
2278
|
-
const obj = value;
|
|
2279
|
-
for (const key of Object.keys(obj)) this.writeFlattenedField(`${path}.${key}`, obj[key], result, meta);
|
|
2280
|
-
}
|
|
2281
|
-
} else {
|
|
2282
|
-
const fd = meta.leafByLogical.get(path);
|
|
2283
|
-
const physical = fd?.physicalName ?? path.replace(/\./g, "__");
|
|
2284
|
-
if (fd?.storage === "json") result[physical] = value !== void 0 && value !== null ? JSON.stringify(value) : value;
|
|
2285
|
-
else result[physical] = value;
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
/**
|
|
2289
|
-
* When a parent object is null/undefined, set all its flattened children to null.
|
|
2290
|
-
*/
|
|
2291
|
-
setFlattenedChildrenNull(parentPath, result, meta) {
|
|
2292
|
-
const children = meta.childrenByParent.get(parentPath);
|
|
2293
|
-
if (children) for (const physical of children) result[physical] = null;
|
|
2294
|
-
}
|
|
2295
|
-
};
|
|
2296
|
-
//#endregion
|
|
2297
|
-
//#region src/query/filter-values.ts
|
|
2298
|
-
const OPAQUE = {
|
|
2299
|
-
kinds: new Set(["any"]),
|
|
2300
|
-
timestamp: false
|
|
2301
|
-
};
|
|
2302
|
-
const NUMBER = {
|
|
2303
|
-
kinds: new Set(["number"]),
|
|
2304
|
-
timestamp: false
|
|
2305
|
-
};
|
|
2306
|
-
const INTEGER = {
|
|
2307
|
-
kinds: new Set(["integer"]),
|
|
2308
|
-
timestamp: false
|
|
2309
|
-
};
|
|
2310
|
-
const STRING = {
|
|
2311
|
-
kinds: new Set(["string"]),
|
|
2312
|
-
timestamp: false
|
|
2313
|
-
};
|
|
2314
|
-
function collectKinds(type, kinds, out, depth = 0) {
|
|
2315
|
-
const def = type?.type;
|
|
2316
|
-
const metadata = type?.metadata;
|
|
2317
|
-
if (!def || depth > 8) {
|
|
2318
|
-
kinds.add("any");
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
switch (def.kind) {
|
|
2322
|
-
case "": switch (def.designType) {
|
|
2323
|
-
case "string":
|
|
2324
|
-
kinds.add("string");
|
|
2325
|
-
return;
|
|
2326
|
-
case "number": {
|
|
2327
|
-
const timestamp = def.tags?.has("timestamp") === true;
|
|
2328
|
-
if (timestamp) out.timestamp = true;
|
|
2329
|
-
const integer = timestamp || def.tags?.has("int") === true || metadata?.has?.("expect.int") === true;
|
|
2330
|
-
kinds.add(integer ? "integer" : "number");
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
case "decimal":
|
|
2334
|
-
kinds.add("decimal");
|
|
2335
|
-
return;
|
|
2336
|
-
case "boolean":
|
|
2337
|
-
kinds.add("boolean");
|
|
2338
|
-
return;
|
|
2339
|
-
case "null":
|
|
2340
|
-
case "undefined":
|
|
2341
|
-
case "never": return;
|
|
2342
|
-
default:
|
|
2343
|
-
kinds.add("any");
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
case "union":
|
|
2347
|
-
for (const item of def.items ?? []) collectKinds(item, kinds, out, depth + 1);
|
|
2348
|
-
return;
|
|
2349
|
-
case "array":
|
|
2350
|
-
collectKinds(def.of, kinds, out, depth + 1);
|
|
2351
|
-
return;
|
|
2352
|
-
default: kinds.add("any");
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
const typeCache = /* @__PURE__ */ new WeakMap();
|
|
2356
|
-
/**
|
|
2357
|
-
* The value kinds a filter on `fd` accepts (cached per descriptor). A leaf
|
|
2358
|
-
* inside a JSON value (a `@db.json` object or an array, addressable on
|
|
2359
|
-
* nested-object adapters) is opaque: its contents are not schema-enforced.
|
|
2360
|
-
*/
|
|
2361
|
-
function valueTypeOf(meta, fd) {
|
|
2362
|
-
let vt = typeCache.get(fd);
|
|
2363
|
-
if (vt) return vt;
|
|
2364
|
-
if (jsonValueAncestor(fd.path, meta.jsonValueParents) !== void 0 || fd.encrypted || fd.isGeoPoint || fd.designType === "json" || fd.designType === "object" || !fd.type) vt = OPAQUE;
|
|
2365
|
-
else {
|
|
2366
|
-
const kinds = /* @__PURE__ */ new Set();
|
|
2367
|
-
const out = { timestamp: false };
|
|
2368
|
-
collectKinds(fd.type, kinds, out);
|
|
2369
|
-
const metadata = fd.type.metadata;
|
|
2370
|
-
if ((fd.defaultValue?.kind === "fn" && fd.defaultValue.fn !== "uuid" || metadata?.has?.("db.agg.count") === true || metadata?.has?.("db.agg.countDistinct") === true) && kinds.delete("number")) kinds.add("integer");
|
|
2371
|
-
vt = kinds.size === 0 ? OPAQUE : {
|
|
2372
|
-
kinds,
|
|
2373
|
-
timestamp: out.timestamp
|
|
2374
|
-
};
|
|
2375
|
-
}
|
|
2376
|
-
typeCache.set(fd, vt);
|
|
2377
|
-
return vt;
|
|
2378
|
-
}
|
|
2379
|
-
/** A decimal literal (`5`, `-1.5`, `.5`, `1e3`), surrounding blanks allowed — no hex, no `Infinity`. */
|
|
2380
|
-
const NUMERIC_RE = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?\s*$/i;
|
|
2381
|
-
/** An integer literal (`5`, `-12`), surrounding blanks allowed. */
|
|
2382
|
-
const INTEGER_RE = /^\s*[+-]?\d+\s*$/;
|
|
2383
|
-
function acceptsScalar(kind, value) {
|
|
2384
|
-
switch (kind) {
|
|
2385
|
-
case "any": return true;
|
|
2386
|
-
case "string": return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint";
|
|
2387
|
-
case "number":
|
|
2388
|
-
case "decimal": return typeof value === "number" && Number.isFinite(value) || typeof value === "bigint" || typeof value === "string" && NUMERIC_RE.test(value);
|
|
2389
|
-
case "integer": return typeof value === "number" && Number.isInteger(value) || typeof value === "bigint" || typeof value === "string" && INTEGER_RE.test(value);
|
|
2390
|
-
default: return typeof value === "boolean" || value === 0 || value === 1;
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
/** The first element of `value` (itself when not an array) `vt` rejects, or `undefined`. */
|
|
2394
|
-
function rejectedValue(vt, value) {
|
|
2395
|
-
if (vt.kinds.has("any") || value === null || value === void 0) return void 0;
|
|
2396
|
-
if (Array.isArray(value)) {
|
|
2397
|
-
for (const item of value) {
|
|
2398
|
-
const bad = rejectedValue(vt, item);
|
|
2399
|
-
if (bad) return bad;
|
|
2400
|
-
}
|
|
2401
|
-
return;
|
|
2402
|
-
}
|
|
2403
|
-
if (typeof value === "object" && !require_object.isPlainObject(value)) return void 0;
|
|
2404
|
-
for (const kind of vt.kinds) if (acceptsScalar(kind, value)) return void 0;
|
|
2405
|
-
return { value };
|
|
2406
|
-
}
|
|
2407
|
-
const KIND_LABEL = {
|
|
2408
|
-
string: "a string",
|
|
2409
|
-
number: "a number",
|
|
2410
|
-
integer: "an integer",
|
|
2411
|
-
decimal: "a decimal (number or numeric string)",
|
|
2412
|
-
boolean: "a boolean"
|
|
2413
|
-
};
|
|
2414
|
-
function expectedOf(vt) {
|
|
2415
|
-
const labels = [];
|
|
2416
|
-
for (const kind of vt.kinds) {
|
|
2417
|
-
if (kind === "any") continue;
|
|
2418
|
-
labels.push(kind === "integer" && vt.timestamp ? "an integer (epoch milliseconds)" : KIND_LABEL[kind]);
|
|
2617
|
+
const result = {};
|
|
2618
|
+
const updateKeys = Object.keys(update);
|
|
2619
|
+
for (const key of updateKeys) {
|
|
2620
|
+
const value = update[key];
|
|
2621
|
+
const operatorMatch = key.match(/^(.+?)(\.__\$.+)$/);
|
|
2622
|
+
const basePath = operatorMatch ? operatorMatch[1] : key;
|
|
2623
|
+
const suffix = operatorMatch ? operatorMatch[2] : "";
|
|
2624
|
+
const fd = meta.leafByLogical.get(basePath);
|
|
2625
|
+
const finalKey = (fd?.physicalName ?? basePath) + suffix;
|
|
2626
|
+
if (fd?.storage === "json" && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
|
|
2627
|
+
else result[finalKey] = value;
|
|
2628
|
+
}
|
|
2629
|
+
return this.formatWriteValues(result, meta);
|
|
2419
2630
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2631
|
+
/**
|
|
2632
|
+
* Translates field names in sort and projection controls from
|
|
2633
|
+
* logical dot-paths to physical column names.
|
|
2634
|
+
*/
|
|
2635
|
+
translateControls(controls, meta) {
|
|
2636
|
+
if (!controls) return {};
|
|
2637
|
+
const result = {
|
|
2638
|
+
...controls,
|
|
2639
|
+
$select: void 0,
|
|
2640
|
+
$with: void 0
|
|
2641
|
+
};
|
|
2642
|
+
if (controls.$sort) {
|
|
2643
|
+
const translated = {};
|
|
2644
|
+
const sortObj = controls.$sort;
|
|
2645
|
+
const sortKeys = Object.keys(sortObj);
|
|
2646
|
+
for (const key of sortKeys) {
|
|
2647
|
+
if (meta.flattenedParents.has(key)) continue;
|
|
2648
|
+
const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
|
|
2649
|
+
translated[physical] = sortObj[key];
|
|
2650
|
+
}
|
|
2651
|
+
result.$sort = translated;
|
|
2652
|
+
}
|
|
2653
|
+
if (controls.$select) {
|
|
2654
|
+
let translatedRaw;
|
|
2655
|
+
if (Array.isArray(controls.$select)) {
|
|
2656
|
+
const expanded = [];
|
|
2657
|
+
for (const key of controls.$select) {
|
|
2658
|
+
const expansion = meta.selectExpansion.get(key);
|
|
2659
|
+
if (expansion) expanded.push(...expansion);
|
|
2660
|
+
else expanded.push(meta.leafByLogical.get(key)?.physicalName ?? key);
|
|
2661
|
+
}
|
|
2662
|
+
translatedRaw = expanded;
|
|
2663
|
+
} else {
|
|
2664
|
+
const translated = {};
|
|
2665
|
+
const selectObj = controls.$select;
|
|
2666
|
+
const selectKeys = Object.keys(selectObj);
|
|
2667
|
+
for (const key of selectKeys) {
|
|
2668
|
+
const val = selectObj[key];
|
|
2669
|
+
const expansion = meta.selectExpansion.get(key);
|
|
2670
|
+
if (expansion) for (const leaf of expansion) translated[leaf] = val;
|
|
2671
|
+
else {
|
|
2672
|
+
const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
|
|
2673
|
+
translated[physical] = val;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
translatedRaw = translated;
|
|
2677
|
+
}
|
|
2678
|
+
result.$select = new UniquSelect(translatedRaw, meta.allPhysicalFields);
|
|
2679
|
+
}
|
|
2680
|
+
return result;
|
|
2444
2681
|
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
"$gte",
|
|
2454
|
-
"$lt",
|
|
2455
|
-
"$lte",
|
|
2456
|
-
"$in",
|
|
2457
|
-
"$nin"
|
|
2458
|
-
]);
|
|
2459
|
-
/** Checks one filter entry's value (a bare value or an operator map) against `vt`. */
|
|
2460
|
-
function checkEntry(path, vt, value) {
|
|
2461
|
-
if (vt.kinds.has("any")) return;
|
|
2462
|
-
if (!require_object.isPlainObject(value)) {
|
|
2463
|
-
checkValue(path, vt, void 0, value);
|
|
2464
|
-
return;
|
|
2682
|
+
/**
|
|
2683
|
+
* Flattens nested object fields into __-separated keys and
|
|
2684
|
+
* JSON-stringifies @db.json / array fields.
|
|
2685
|
+
*/
|
|
2686
|
+
flattenPayload(data, meta) {
|
|
2687
|
+
const result = {};
|
|
2688
|
+
for (const key of Object.keys(data)) this.writeFlattenedField(key, data[key], result, meta);
|
|
2689
|
+
return result;
|
|
2465
2690
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2691
|
+
/**
|
|
2692
|
+
* Classifies and writes a single field to the result object.
|
|
2693
|
+
* Recurses into nested objects that should be flattened.
|
|
2694
|
+
*/
|
|
2695
|
+
writeFlattenedField(path, value, result, meta) {
|
|
2696
|
+
if (meta.ignoredFields.has(path)) return;
|
|
2697
|
+
if (meta.flattenedParents.has(path)) {
|
|
2698
|
+
if (value === null || value === void 0) this.setFlattenedChildrenNull(path, result, meta);
|
|
2699
|
+
else if (typeof value === "object" && !Array.isArray(value)) {
|
|
2700
|
+
const obj = value;
|
|
2701
|
+
for (const key of Object.keys(obj)) this.writeFlattenedField(`${path}.${key}`, obj[key], result, meta);
|
|
2702
|
+
}
|
|
2703
|
+
} else {
|
|
2704
|
+
const fd = meta.leafByLogical.get(path);
|
|
2705
|
+
const physical = fd?.physicalName ?? path.replace(/\./g, "__");
|
|
2706
|
+
if (fd?.storage === "json") result[physical] = value !== void 0 && value !== null ? JSON.stringify(value) : value;
|
|
2707
|
+
else result[physical] = value;
|
|
2483
2708
|
}
|
|
2484
|
-
if (key.startsWith("$") || require_nested_writer.hasRelationOp(value)) continue;
|
|
2485
|
-
const vt = typeOf(key);
|
|
2486
|
-
if (vt) checkEntry(key, vt, value);
|
|
2487
2709
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
function guardFilterValues(meta, filter) {
|
|
2495
|
-
if (!filter) return;
|
|
2496
|
-
walkFilterValues(filter, (key) => {
|
|
2497
|
-
const fd = meta.descriptorByPath.get(key);
|
|
2498
|
-
return fd ? valueTypeOf(meta, fd) : void 0;
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
|
-
/**
|
|
2502
|
-
* `$having` values: an aggregate alias is a number (`count`,
|
|
2503
|
-
* `countDistinct`, `sum`, `avg`, an expression) or its source field's type
|
|
2504
|
-
* (`min` / `max`, `first` / `last`),
|
|
2505
|
-
* a calendar-bucket alias a string label, any other key a `$groupBy`
|
|
2506
|
-
* field's own type.
|
|
2507
|
-
*/
|
|
2508
|
-
function guardHavingValues(meta, controls) {
|
|
2509
|
-
if (!controls?.$having) return;
|
|
2510
|
-
const aliases = /* @__PURE__ */ new Map();
|
|
2511
|
-
if (Array.isArray(controls.$select)) {
|
|
2512
|
-
for (const item of controls.$select) if ((0, _uniqu_core.isAggregateOfExpr)(item) || (0, _uniqu_core.isSelectArithExpr)(item)) aliases.set(item.$as, NUMBER);
|
|
2513
|
-
else if ((0, _uniqu_core.isAggregateExpr)(item)) {
|
|
2514
|
-
const fd = require_aggregate_fns.SOURCE_VALUE_FNS.has(item.$fn) ? meta.descriptorByPath.get(item.$field) : void 0;
|
|
2515
|
-
const counts = item.$fn === "count" || item.$fn === "countDistinct";
|
|
2516
|
-
aliases.set((0, _uniqu_core.resolveAlias)(item), fd ? valueTypeOf(meta, fd) : counts ? INTEGER : NUMBER);
|
|
2517
|
-
} else if ((0, _uniqu_core.isBucketExpr)(item)) aliases.set((0, _uniqu_core.resolveAlias)(item), STRING);
|
|
2710
|
+
/**
|
|
2711
|
+
* When a parent object is null/undefined, set all its flattened children to null.
|
|
2712
|
+
*/
|
|
2713
|
+
setFlattenedChildrenNull(parentPath, result, meta) {
|
|
2714
|
+
const children = meta.childrenByParent.get(parentPath);
|
|
2715
|
+
if (children) for (const physical of children) result[physical] = null;
|
|
2518
2716
|
}
|
|
2519
|
-
|
|
2520
|
-
const alias = aliases.get(key);
|
|
2521
|
-
if (alias) return alias;
|
|
2522
|
-
const fd = meta.descriptorByPath.get(key);
|
|
2523
|
-
return fd ? valueTypeOf(meta, fd) : void 0;
|
|
2524
|
-
});
|
|
2525
|
-
}
|
|
2717
|
+
};
|
|
2526
2718
|
//#endregion
|
|
2527
2719
|
//#region src/query/query-guards.ts
|
|
2528
2720
|
/**
|
|
@@ -3595,6 +3787,17 @@ var AtscriptDbReadable = class {
|
|
|
3595
3787
|
if (field === void 0) return void 0;
|
|
3596
3788
|
return this._meta.columnMap.get(field) ?? field;
|
|
3597
3789
|
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Logical paths of the `@db.column.version.exempt` fields, including objects
|
|
3792
|
+
* whose direct children are all exempt. A patch that writes only these (and
|
|
3793
|
+
* no `$cas`) does not bump the version column. Empty when the table has no
|
|
3794
|
+
* `@db.column.version`.
|
|
3795
|
+
* @since 0.1.150
|
|
3796
|
+
*/
|
|
3797
|
+
get versionExemptFields() {
|
|
3798
|
+
this._ensureBuilt();
|
|
3799
|
+
return this._meta.versionExemptPaths;
|
|
3800
|
+
}
|
|
3598
3801
|
/** Dimension fields from `@db.column.dimension`. */
|
|
3599
3802
|
get dimensions() {
|
|
3600
3803
|
this._ensureBuilt();
|
|
@@ -4661,6 +4864,16 @@ var BaseDbAdapter = class {
|
|
|
4661
4864
|
this._verbose = enabled;
|
|
4662
4865
|
}
|
|
4663
4866
|
/**
|
|
4867
|
+
* Physical version column an UPDATE must bump, or `undefined` when it must
|
|
4868
|
+
* not: no `@db.column.version`, or a version-exempt patch
|
|
4869
|
+
* (`opts.keepVersion`, since 0.1.150 — the core never combines it with
|
|
4870
|
+
* `expectedVersion`; a violation throws).
|
|
4871
|
+
*/
|
|
4872
|
+
_versionColumnFor(opts, expectedVersion) {
|
|
4873
|
+
if (!opts?.keepVersion) return this._table.versionColumnPhysical;
|
|
4874
|
+
if (expectedVersion !== void 0) throw new Error("keepVersion cannot combine with expectedVersion");
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4664
4877
|
* Logs a debug message if verbose mode is enabled.
|
|
4665
4878
|
* Adapters call this to log DB operations with zero overhead when disabled.
|
|
4666
4879
|
*/
|
|
@@ -5000,9 +5213,10 @@ var BaseDbAdapter = class {
|
|
|
5000
5213
|
*
|
|
5001
5214
|
* @param filter - Filter identifying the record to patch.
|
|
5002
5215
|
* @param patch - The patch payload with array operations.
|
|
5216
|
+
* @param opts - `keepVersion`: skip the version bump (see {@link TDbUpdateOptions}).
|
|
5003
5217
|
* @returns Update result.
|
|
5004
5218
|
*/
|
|
5005
|
-
async nativePatch(_filter, _patch, _ops, _expectedVersion) {
|
|
5219
|
+
async nativePatch(_filter, _patch, _ops, _expectedVersion, _opts) {
|
|
5006
5220
|
throw new Error("Native patch not supported by this adapter");
|
|
5007
5221
|
}
|
|
5008
5222
|
/**
|
|
@@ -5047,6 +5261,7 @@ var BaseDbAdapter = class {
|
|
|
5047
5261
|
continue;
|
|
5048
5262
|
}
|
|
5049
5263
|
if (opts.shouldSkipType?.(index.type)) continue;
|
|
5264
|
+
if (index.type === "fulltext" && !index.fields.some((f) => !f.integer)) continue;
|
|
5050
5265
|
desiredNames.add(index.key);
|
|
5051
5266
|
if (!existingNames.has(index.key)) {
|
|
5052
5267
|
await attempt(`create index "${index.key}"`, () => opts.createIndex(index));
|
|
@@ -5067,6 +5282,13 @@ var BaseDbAdapter = class {
|
|
|
5067
5282
|
/**
|
|
5068
5283
|
* Returns available search indexes for this adapter.
|
|
5069
5284
|
* UI uses this to show index picker. Override in adapters that support search.
|
|
5285
|
+
*
|
|
5286
|
+
* A fulltext index may carry integer members ({@link TDbIndexField.integer},
|
|
5287
|
+
* since 0.1.150): list them in `fields`, keep them out of the physical text
|
|
5288
|
+
* index, and OR `member = n` into the search predicate when
|
|
5289
|
+
* `searchTermInteger(text)` is defined. An index of integer members only
|
|
5290
|
+
* is still listed (it makes the table natively searchable); a term that is
|
|
5291
|
+
* not a whole number then matches nothing.
|
|
5070
5292
|
*/
|
|
5071
5293
|
getSearchIndexes() {
|
|
5072
5294
|
return [];
|
|
@@ -5112,7 +5334,9 @@ var BaseDbAdapter = class {
|
|
|
5112
5334
|
return false;
|
|
5113
5335
|
}
|
|
5114
5336
|
/**
|
|
5115
|
-
* Full-text search. Override in adapters that support search.
|
|
5337
|
+
* Full-text search. Override in adapters that support search. The text
|
|
5338
|
+
* match is OR'd with exact-number equality on the index's integer members
|
|
5339
|
+
* when the whole term is a whole number (see `searchTermInteger`).
|
|
5116
5340
|
*
|
|
5117
5341
|
* @param text - Search text.
|
|
5118
5342
|
* @param query - Filter, sort, limit, etc.
|
|
@@ -5696,6 +5920,8 @@ function matchByKeys(a, b, keys) {
|
|
|
5696
5920
|
}
|
|
5697
5921
|
//#endregion
|
|
5698
5922
|
//#region src/patch/patch-decomposer.ts
|
|
5923
|
+
/** Metadata tag the flattener puts on top-level array fields (shared with `isVersionExemptPatch`). */
|
|
5924
|
+
const TOP_LEVEL_ARRAY_TAG = "db.__topLevelArray";
|
|
5699
5925
|
/**
|
|
5700
5926
|
* Decomposes a patch payload into a flat update object for adapters
|
|
5701
5927
|
* that don't support native patch operations.
|
|
@@ -5714,7 +5940,7 @@ function matchByKeys(a, b, keys) {
|
|
|
5714
5940
|
*/
|
|
5715
5941
|
function decomposePatch(payload, table) {
|
|
5716
5942
|
const update = {};
|
|
5717
|
-
flattenPatchPayload(payload, "", update, table,
|
|
5943
|
+
flattenPatchPayload(payload, "", update, table, TOP_LEVEL_ARRAY_TAG);
|
|
5718
5944
|
return update;
|
|
5719
5945
|
}
|
|
5720
5946
|
function flattenPatchPayload(payload, prefix, update, table, topLevelArrayTag) {
|
|
@@ -5796,6 +6022,44 @@ function assertNoVersionWrites(data, versionColumn) {
|
|
|
5796
6022
|
}]);
|
|
5797
6023
|
}
|
|
5798
6024
|
//#endregion
|
|
6025
|
+
//#region src/patch/version-exempt.ts
|
|
6026
|
+
/**
|
|
6027
|
+
* Whether a logical (pre-decomposition) patch writes ONLY version-exempt
|
|
6028
|
+
* fields (`@db.column.version.exempt`). `data` has nav fields, filter keys and
|
|
6029
|
+
* derived fields removed and `$cas` separated. Mirrors `flattenPatchPayload`
|
|
6030
|
+
* (keep the two side by side): PK keys are skipped; an exempt path (or one
|
|
6031
|
+
* under an exempt ancestor) is covered whatever its value (scalar, field op,
|
|
6032
|
+
* array ops, object); a non-exempt merge-strategy object recurses (only the
|
|
6033
|
+
* supplied children are written); anything else is a non-exempt write.
|
|
6034
|
+
* True iff at least one key is covered and none is not.
|
|
6035
|
+
* @since 0.1.150
|
|
6036
|
+
*/
|
|
6037
|
+
function isVersionExemptPatch(data, table) {
|
|
6038
|
+
const meta = table.getMetadata();
|
|
6039
|
+
if (meta.versionExemptPaths.size === 0) return false;
|
|
6040
|
+
const walk = (obj, prefix) => {
|
|
6041
|
+
let result = "none";
|
|
6042
|
+
for (const [k, value] of Object.entries(obj)) {
|
|
6043
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
6044
|
+
if (table.primaryKeys.includes(key)) continue;
|
|
6045
|
+
if (meta.isVersionExemptPath(key)) {
|
|
6046
|
+
result = "exempt";
|
|
6047
|
+
continue;
|
|
6048
|
+
}
|
|
6049
|
+
const flatType = table.flatMap.get(key);
|
|
6050
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && !require_ops.isDbFieldOp(value) && !flatType?.metadata?.get("db.__topLevelArray") && !flatType?.metadata?.has("db.json") && flatType?.metadata?.get("db.patch.strategy") === "merge") {
|
|
6051
|
+
const inner = walk(value, key);
|
|
6052
|
+
if (inner === "versioned") return "versioned";
|
|
6053
|
+
if (inner === "exempt") result = "exempt";
|
|
6054
|
+
continue;
|
|
6055
|
+
}
|
|
6056
|
+
return "versioned";
|
|
6057
|
+
}
|
|
6058
|
+
return result;
|
|
6059
|
+
};
|
|
6060
|
+
return walk(data, "") === "exempt";
|
|
6061
|
+
}
|
|
6062
|
+
//#endregion
|
|
5799
6063
|
//#region src/table/db-table.ts
|
|
5800
6064
|
/** Returns true when `value` is a plain object carrying any `$`-prefixed key (an operator object). */
|
|
5801
6065
|
function _hasOperatorKeys(value) {
|
|
@@ -6003,6 +6267,8 @@ function concurrentChange() {
|
|
|
6003
6267
|
message: "The record changed during the write — nothing was written, retry"
|
|
6004
6268
|
}]);
|
|
6005
6269
|
}
|
|
6270
|
+
/** Hand-off for a version-exempt patch (since 0.1.150) — shared, never mutated. */
|
|
6271
|
+
const KEEP_VERSION = Object.freeze({ keepVersion: true });
|
|
6006
6272
|
/** Upper bound of keys per `touchMany` UPDATE statement (parameter-count safety). */
|
|
6007
6273
|
const TOUCH_MANY_CHUNK = 500;
|
|
6008
6274
|
/** `touchMany` input rejection — always `INVALID_QUERY`, path names the key. */
|
|
@@ -6291,12 +6557,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6291
6557
|
if (!matched[i] && _ownedBy?.strict?.[i]) throw concurrentChange();
|
|
6292
6558
|
continue;
|
|
6293
6559
|
}
|
|
6560
|
+
const updateOpts = expectedVersion === void 0 && versionColumn !== void 0 && isVersionExemptPatch(data, this) ? [KEEP_VERSION] : [];
|
|
6294
6561
|
let result;
|
|
6295
6562
|
if (this.adapter.supportsNativePatch()) {
|
|
6296
6563
|
const ops = require_ops.separateFieldOps(data);
|
|
6297
6564
|
const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
|
|
6298
6565
|
const translatedData = this._fieldMapper.translatePatchKeys(data, this._meta);
|
|
6299
|
-
result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps, expectedVersion);
|
|
6566
|
+
result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps, expectedVersion, ...updateOpts);
|
|
6300
6567
|
} else {
|
|
6301
6568
|
const update = decomposePatch(data, this);
|
|
6302
6569
|
const ops = require_ops.separateFieldOps(update);
|
|
@@ -6307,8 +6574,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6307
6574
|
filter: translatedFilter,
|
|
6308
6575
|
controls: {}
|
|
6309
6576
|
}), this);
|
|
6310
|
-
result = await this.adapter.updateOne(translatedFilter, resolved, translatedOps, expectedVersion);
|
|
6311
|
-
} else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps, expectedVersion);
|
|
6577
|
+
result = await this.adapter.updateOne(translatedFilter, resolved, translatedOps, expectedVersion, ...updateOpts);
|
|
6578
|
+
} else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps, expectedVersion, ...updateOpts);
|
|
6312
6579
|
}
|
|
6313
6580
|
modifiedCount += result.modifiedCount;
|
|
6314
6581
|
matched[i] = result.matchedCount > 0;
|
|
@@ -6455,6 +6722,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6455
6722
|
message: "$cas is not supported on updateMany — use bulkUpdate with per-row $cas for version-locked batch updates"
|
|
6456
6723
|
}]);
|
|
6457
6724
|
if (versionColumn !== void 0) assertNoVersionWrites(dataCopy, versionColumn);
|
|
6725
|
+
const updateOpts = versionColumn !== void 0 && isVersionExemptPatch(dataCopy, this) ? [KEEP_VERSION] : [];
|
|
6458
6726
|
await this._encryptItems([dataCopy], "patch");
|
|
6459
6727
|
const update = decomposePatch(dataCopy, this);
|
|
6460
6728
|
const ops = require_ops.separateFieldOps(update);
|
|
@@ -6468,7 +6736,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6468
6736
|
}),
|
|
6469
6737
|
modifiedCount: 0
|
|
6470
6738
|
};
|
|
6471
|
-
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.updateMany(translatedFilter, translatedUpdate, translatedOps));
|
|
6739
|
+
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.updateMany(translatedFilter, translatedUpdate, translatedOps, ...updateOpts));
|
|
6472
6740
|
}
|
|
6473
6741
|
async replaceMany(filter, data) {
|
|
6474
6742
|
this._ensureBuilt();
|
|
@@ -7400,7 +7668,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
7400
7668
|
};
|
|
7401
7669
|
if (joinNullable || source.optional || source.designType === "unknown") mapping.nullable = true;
|
|
7402
7670
|
if (source.jsonPath) {
|
|
7403
|
-
if (!
|
|
7671
|
+
if (!require_search_fields.isJsonLeafType(source.designType)) throw new Error(`View "${this.tableName}" field "${viewPath}": JSON extraction supports string, number and boolean leaves only`);
|
|
7404
7672
|
mapping.json = {
|
|
7405
7673
|
path: source.jsonPath,
|
|
7406
7674
|
type: source.designType
|
|
@@ -7540,7 +7808,8 @@ function computeTableSnapshot(readable, typeMapper, tableOptions) {
|
|
|
7540
7808
|
type: idx.type,
|
|
7541
7809
|
fields: idx.fields.map((f) => ({
|
|
7542
7810
|
name: f.name,
|
|
7543
|
-
sort: f.sort
|
|
7811
|
+
sort: f.sort,
|
|
7812
|
+
...f.integer ? { integer: true } : {}
|
|
7544
7813
|
}))
|
|
7545
7814
|
})).toSorted((a, b) => a.key.localeCompare(b.key));
|
|
7546
7815
|
const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
|
|
@@ -7913,6 +8182,12 @@ Object.defineProperty(exports, "FieldMappingStrategy", {
|
|
|
7913
8182
|
return FieldMappingStrategy;
|
|
7914
8183
|
}
|
|
7915
8184
|
});
|
|
8185
|
+
Object.defineProperty(exports, "INTEGER_REGEX_OP", {
|
|
8186
|
+
enumerable: true,
|
|
8187
|
+
get: function() {
|
|
8188
|
+
return INTEGER_REGEX_OP;
|
|
8189
|
+
}
|
|
8190
|
+
});
|
|
7916
8191
|
Object.defineProperty(exports, "IntegrityStrategy", {
|
|
7917
8192
|
enumerable: true,
|
|
7918
8193
|
get: function() {
|
|
@@ -8057,6 +8332,18 @@ Object.defineProperty(exports, "decomposePatch", {
|
|
|
8057
8332
|
return decomposePatch;
|
|
8058
8333
|
}
|
|
8059
8334
|
});
|
|
8335
|
+
Object.defineProperty(exports, "defaultFulltextIndex", {
|
|
8336
|
+
enumerable: true,
|
|
8337
|
+
get: function() {
|
|
8338
|
+
return defaultFulltextIndex;
|
|
8339
|
+
}
|
|
8340
|
+
});
|
|
8341
|
+
Object.defineProperty(exports, "describeFulltext", {
|
|
8342
|
+
enumerable: true,
|
|
8343
|
+
get: function() {
|
|
8344
|
+
return describeFulltext;
|
|
8345
|
+
}
|
|
8346
|
+
});
|
|
8060
8347
|
Object.defineProperty(exports, "fkColumns", {
|
|
8061
8348
|
enumerable: true,
|
|
8062
8349
|
get: function() {
|
|
@@ -8153,6 +8440,12 @@ Object.defineProperty(exports, "isJsonValueField", {
|
|
|
8153
8440
|
return isJsonValueField;
|
|
8154
8441
|
}
|
|
8155
8442
|
});
|
|
8443
|
+
Object.defineProperty(exports, "isVersionExemptPatch", {
|
|
8444
|
+
enumerable: true,
|
|
8445
|
+
get: function() {
|
|
8446
|
+
return isVersionExemptPatch;
|
|
8447
|
+
}
|
|
8448
|
+
});
|
|
8156
8449
|
Object.defineProperty(exports, "isViewType", {
|
|
8157
8450
|
enumerable: true,
|
|
8158
8451
|
get: function() {
|
|
@@ -8201,6 +8494,12 @@ Object.defineProperty(exports, "searchIndexNotFoundMessage", {
|
|
|
8201
8494
|
return searchIndexNotFoundMessage;
|
|
8202
8495
|
}
|
|
8203
8496
|
});
|
|
8497
|
+
Object.defineProperty(exports, "searchTermInteger", {
|
|
8498
|
+
enumerable: true,
|
|
8499
|
+
get: function() {
|
|
8500
|
+
return searchTermInteger;
|
|
8501
|
+
}
|
|
8502
|
+
});
|
|
8204
8503
|
Object.defineProperty(exports, "snapshotToExistingColumns", {
|
|
8205
8504
|
enumerable: true,
|
|
8206
8505
|
get: function() {
|
|
@@ -8219,6 +8518,12 @@ Object.defineProperty(exports, "sortFieldNames", {
|
|
|
8219
8518
|
return sortFieldNames;
|
|
8220
8519
|
}
|
|
8221
8520
|
});
|
|
8521
|
+
Object.defineProperty(exports, "splitFulltextFields", {
|
|
8522
|
+
enumerable: true,
|
|
8523
|
+
get: function() {
|
|
8524
|
+
return splitFulltextFields;
|
|
8525
|
+
}
|
|
8526
|
+
});
|
|
8222
8527
|
Object.defineProperty(exports, "unsupportedOperatorMessage", {
|
|
8223
8528
|
enumerable: true,
|
|
8224
8529
|
get: function() {
|