@atscript/db 0.1.127 → 0.1.129
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/{db-error-DXwEzmYJ.cjs → db-error-C4JuLcvb.cjs} +27 -0
- package/dist/{db-error-BHPXOKzc.mjs → db-error-COrO58t5.mjs} +22 -1
- package/dist/{db-readable-BkAGccv9.d.mts → db-readable-B7eYWS5q.d.cts} +299 -17
- package/dist/{db-readable-C0nDKX8A.d.cts → db-readable-Bn1bV_eC.d.mts} +299 -17
- package/dist/{db-space-B_ASuDaR.d.mts → db-space-C2UCnGHd.d.cts} +108 -30
- package/dist/{db-space-CSntT6yS.d.cts → db-space-DdIPYD0Q.d.mts} +108 -30
- package/dist/{db-view-BP0Qbeux.cjs → db-view-CRBgkEp0.cjs} +786 -100
- package/dist/{db-view-C8rZM5_N.mjs → db-view-Dl0aDTiT.mjs} +733 -101
- package/dist/index.cjs +48 -3
- package/dist/index.d.cts +162 -37
- package/dist/index.d.mts +162 -37
- package/dist/index.mjs +37 -5
- package/dist/{nested-writer-DI-HeTky.mjs → nested-writer-CkDo-ZfH.mjs} +1 -1
- package/dist/{nested-writer-DoDhl3X3.cjs → nested-writer-DxPhmWFz.cjs} +1 -1
- package/dist/{ops-DJRnNTVo.d.cts → ops-AqhV7s9o.d.cts} +24 -1
- package/dist/{ops-DJRnNTVo.d.mts → ops-AqhV7s9o.d.mts} +24 -1
- package/dist/ops.cjs +44 -1
- package/dist/ops.d.cts +2 -2
- package/dist/ops.d.mts +2 -2
- package/dist/ops.mjs +44 -2
- package/dist/plugin.cjs +12 -5
- package/dist/plugin.mjs +12 -5
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +1 -1
- package/dist/rel.d.mts +1 -1
- package/dist/rel.mjs +2 -2
- package/dist/{relation-loader-BnUgJsUG.cjs → relation-loader-C8GOpNYJ.cjs} +1 -1
- package/dist/{relation-loader-BmeOMj0b.mjs → relation-loader-CUGcxJ18.mjs} +1 -1
- package/dist/sync.cjs +1270 -398
- package/dist/sync.d.cts +255 -18
- package/dist/sync.d.mts +255 -18
- package/dist/sync.mjs +1269 -399
- package/dist/{validator-0vRXN51D.mjs → validator-CeD_fqyW.mjs} +21 -4
- package/dist/{validator-CSGug4vg.cjs → validator-lkCJKuoo.cjs} +32 -3
- package/dist/{validator-BcBtg8yW.d.cts → validator-wBARmD68.d.cts} +57 -1
- package/dist/{validator-BcBtg8yW.d.mts → validator-wBARmD68.d.mts} +57 -1
- package/dist/validator.cjs +7 -1
- package/dist/validator.d.cts +3 -3
- package/dist/validator.d.mts +3 -3
- package/dist/validator.mjs +4 -3
- package/package.json +8 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const require_db_error = require("./db-error-
|
|
1
|
+
const require_db_error = require("./db-error-C4JuLcvb.cjs");
|
|
2
|
+
const require_validator = require("./validator-lkCJKuoo.cjs");
|
|
2
3
|
const require_agg = require("./agg.cjs");
|
|
3
|
-
const require_nested_writer = require("./nested-writer-
|
|
4
|
+
const require_nested_writer = require("./nested-writer-DxPhmWFz.cjs");
|
|
4
5
|
const require_ops = require("./ops.cjs");
|
|
5
|
-
const require_validator = require("./validator-CSGug4vg.cjs");
|
|
6
6
|
let _atscript_typescript_utils = require("@atscript/typescript/utils");
|
|
7
7
|
let node_async_hooks = require("node:async_hooks");
|
|
8
8
|
//#region src/logger.ts
|
|
@@ -108,6 +108,22 @@ var TableMetadata = class {
|
|
|
108
108
|
leafByPhysical = /* @__PURE__ */ new Map();
|
|
109
109
|
/** Leaf field descriptors indexed by logical path (write/patch/filter paths). */
|
|
110
110
|
leafByLogical = /* @__PURE__ */ new Map();
|
|
111
|
+
/**
|
|
112
|
+
* Non-ignored field descriptors keyed by logical path, excluding navigation
|
|
113
|
+
* relations and their descendants. Unlike `leafByLogical` (relational
|
|
114
|
+
* adapters only) this is built for every adapter, so the core path guard
|
|
115
|
+
* (`guardPaths`) can answer "does this path have physical storage here?"
|
|
116
|
+
* on nested-object adapters too.
|
|
117
|
+
*/
|
|
118
|
+
descriptorByPath = /* @__PURE__ */ new Map();
|
|
119
|
+
/**
|
|
120
|
+
* Logical paths stored as a single JSON column (`storage === 'json'`,
|
|
121
|
+
* non-ignored descriptors). Retained after build — unlike the build-time
|
|
122
|
+
* `jsonFields` set — so the path guard can classify JSON descendants on
|
|
123
|
+
* relational adapters. Empty on nested-object adapters (they keep native
|
|
124
|
+
* dotted paths as descriptors).
|
|
125
|
+
*/
|
|
126
|
+
jsonParents = /* @__PURE__ */ new Set();
|
|
111
127
|
_built = false;
|
|
112
128
|
_identifications;
|
|
113
129
|
_collateMap = /* @__PURE__ */ new Map();
|
|
@@ -160,6 +176,7 @@ var TableMetadata = class {
|
|
|
160
176
|
const overrides = adapter.getMetadataOverrides?.(this);
|
|
161
177
|
if (overrides) this._applyOverrides(overrides);
|
|
162
178
|
this._buildFieldDescriptors(adapter);
|
|
179
|
+
this._buildGuardIndexes();
|
|
163
180
|
if (!this.nestedObjects) this._buildLeafIndexes();
|
|
164
181
|
this._buildIdentifications();
|
|
165
182
|
this._resolvePreferredId(type);
|
|
@@ -399,6 +416,23 @@ var TableMetadata = class {
|
|
|
399
416
|
const lastDot = path.lastIndexOf(".");
|
|
400
417
|
return lastDot >= 0 ? `${path.slice(0, lastDot).replace(/\./g, "__")}__` : "";
|
|
401
418
|
}
|
|
419
|
+
/** Nearest `@db.encrypted` ancestor of `path` (exclusive), or `undefined`. */
|
|
420
|
+
/**
|
|
421
|
+
* Indexes non-ignored descriptors by logical path and retains the JSON-parent
|
|
422
|
+
* set. Navigation relations and their descendants are skipped even when the
|
|
423
|
+
* adapter keeps them as descriptors (nested-object adapters do) — they are
|
|
424
|
+
* loaded with `$with`, never addressed as columns of this table.
|
|
425
|
+
*/
|
|
426
|
+
_buildGuardIndexes() {
|
|
427
|
+
const jsonParents = /* @__PURE__ */ new Set();
|
|
428
|
+
for (const fd of this.fieldDescriptors) {
|
|
429
|
+
if (fd.ignored) continue;
|
|
430
|
+
if (this.navFields.has(fd.path) || findAncestorInSet(fd.path, this.navFields) !== void 0) continue;
|
|
431
|
+
this.descriptorByPath.set(fd.path, fd);
|
|
432
|
+
if (fd.storage === "json") jsonParents.add(fd.path);
|
|
433
|
+
}
|
|
434
|
+
this.jsonParents = jsonParents;
|
|
435
|
+
}
|
|
402
436
|
/**
|
|
403
437
|
* Indexes `fieldDescriptors` into two lookup maps for unified
|
|
404
438
|
* read/write field classification in the RelationalFieldMapper.
|
|
@@ -821,8 +855,7 @@ var FieldMappingStrategy = class {
|
|
|
821
855
|
if (!fmt) return value;
|
|
822
856
|
if (value === null || value === void 0) return value;
|
|
823
857
|
if (typeof value !== "object" || Array.isArray(value)) return fmt(value);
|
|
824
|
-
|
|
825
|
-
if (proto !== Object.prototype && proto !== null) return fmt(value);
|
|
858
|
+
if (!require_validator.isPlainObject(value)) return fmt(value);
|
|
826
859
|
const ops = value;
|
|
827
860
|
const formatted = {};
|
|
828
861
|
for (const [op, opVal] of Object.entries(ops)) if ((op === "$in" || op === "$nin") && Array.isArray(opVal)) formatted[op] = opVal.map((v) => v === null || v === void 0 ? v : fmt(v));
|
|
@@ -1165,6 +1198,11 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1165
1198
|
* - `$geoWithin` on a non-geoPoint field → `FILTER_TYPE_MISMATCH`
|
|
1166
1199
|
* - `$geoWithin` with a malformed circle → `INVALID_QUERY`
|
|
1167
1200
|
* - `$geoWithin` on an adapter without geo support → `GEO_NOT_SUPPORTED`
|
|
1201
|
+
* - every filter / `$sort` / `$select` / `$groupBy` / `$having` / aggregate
|
|
1202
|
+
* path must resolve to physical storage on THIS adapter and pass the
|
|
1203
|
+
* adapter's `canFilterField` / `canSortField` → `INVALID_QUERY`
|
|
1204
|
+
* (see {@link guardPaths}). Runs after the checks above so `ENC_*` codes
|
|
1205
|
+
* keep firing first for encrypted subtrees.
|
|
1168
1206
|
*/
|
|
1169
1207
|
/** Validates a `[lng, lat]` tuple (GeoJSON coordinate order). */
|
|
1170
1208
|
function assertGeoPoint(point, path) {
|
|
@@ -1231,24 +1269,274 @@ function guardSort(meta, sort) {
|
|
|
1231
1269
|
if (!sort || typeof sort !== "object" || meta.encryptedFields.size === 0) return;
|
|
1232
1270
|
for (const key of Object.keys(sort)) if (isEncryptedRef(meta, key)) throw encryptedRefError("ENC_FIELD_SORT", key, "sort by");
|
|
1233
1271
|
}
|
|
1234
|
-
|
|
1272
|
+
const OP_VERB = {
|
|
1273
|
+
filter: "filter on",
|
|
1274
|
+
sort: "sort by",
|
|
1275
|
+
select: "select",
|
|
1276
|
+
groupBy: "group by",
|
|
1277
|
+
having: "filter ($having) on",
|
|
1278
|
+
aggregate: "aggregate over"
|
|
1279
|
+
};
|
|
1280
|
+
function pathError(path, message) {
|
|
1281
|
+
return new require_db_error.DbError("INVALID_QUERY", [{
|
|
1282
|
+
path,
|
|
1283
|
+
message
|
|
1284
|
+
}]);
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* The one wording for a filter-node `$`-key that is not `$and` / `$or` /
|
|
1288
|
+
* `$not` (uniqu's walker would treat it as a field named `$…`). The core
|
|
1289
|
+
* guard and the HTTP gate both answer with it; `path` is the operator itself.
|
|
1290
|
+
*/
|
|
1291
|
+
function unsupportedOperatorMessage(op) {
|
|
1292
|
+
return `Unsupported filter operator "${op}" — use $and, $or or $not`;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Normalizes every accepted `$sort` form (`"a,-b"`, `["a", "-b"]`,
|
|
1296
|
+
* `[{ a: 1 }]`, `{ a: 1 }`) into its field names. Mirrors the HTTP layer's
|
|
1297
|
+
* walker so programmatic callers get the same acceptance set.
|
|
1298
|
+
*/
|
|
1299
|
+
function sortFieldNames(sort) {
|
|
1300
|
+
if (!sort) return [];
|
|
1301
|
+
if (typeof sort === "string") {
|
|
1302
|
+
const out = [];
|
|
1303
|
+
for (const part of sort.split(",")) {
|
|
1304
|
+
const name = part.trim().replace(/^[-+]/, "").split(":")[0];
|
|
1305
|
+
if (name) out.push(name);
|
|
1306
|
+
}
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
1309
|
+
if (Array.isArray(sort)) {
|
|
1310
|
+
const out = [];
|
|
1311
|
+
for (const entry of sort) if (typeof entry === "string") out.push(entry.replace(/^[-+]/, ""));
|
|
1312
|
+
else if (entry && typeof entry === "object") out.push(...Object.keys(entry));
|
|
1313
|
+
return out;
|
|
1314
|
+
}
|
|
1315
|
+
if (typeof sort === "object") return Object.keys(sort);
|
|
1316
|
+
return [];
|
|
1317
|
+
}
|
|
1318
|
+
function collectFilterKeys(filter, out, geo, skip, refs) {
|
|
1319
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter)) return;
|
|
1320
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1321
|
+
if (refs.unsupportedOperator) return;
|
|
1322
|
+
if (key === "$and" || key === "$or") {
|
|
1323
|
+
if (Array.isArray(value)) for (const child of value) collectFilterKeys(child, out, geo, skip, refs);
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (key === "$not") {
|
|
1327
|
+
collectFilterKeys(value, out, geo, skip, refs);
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (key.startsWith("$")) {
|
|
1331
|
+
refs.unsupportedOperator = key;
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (skip?.has(key)) continue;
|
|
1335
|
+
(geo !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value) && "$geoWithin" in value ? geo : out).push(key);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Walks the PARSED query structure (not the flattened insights map, which
|
|
1340
|
+
* cannot tell `$select=assignee.name` from `$with=assignee($select=name)`)
|
|
1341
|
+
* and returns the root paths per position. `$with` sub-trees are not
|
|
1342
|
+
* visited — they are validated against their target relation separately.
|
|
1343
|
+
*
|
|
1344
|
+
* Aggregate mode is `aggregate` when given, else the presence of `$groupBy`.
|
|
1345
|
+
* In aggregate mode `$select` entries are aggregate expressions whose
|
|
1346
|
+
* `$field` is collected and whose alias (`$as`, else `fn_field` — the core's
|
|
1347
|
+
* `resolveAlias`) is exempted from `$sort` / `$having`; outside it non-string
|
|
1348
|
+
* `$select` entries are ignored (the projection seal handles them).
|
|
1349
|
+
*/
|
|
1350
|
+
function collectQueryPaths(query, aggregate) {
|
|
1351
|
+
const refs = {
|
|
1352
|
+
filter: [],
|
|
1353
|
+
geoFilter: [],
|
|
1354
|
+
sort: [],
|
|
1355
|
+
select: [],
|
|
1356
|
+
groupBy: [],
|
|
1357
|
+
having: [],
|
|
1358
|
+
aggregate: [],
|
|
1359
|
+
aggregateMode: false
|
|
1360
|
+
};
|
|
1361
|
+
collectFilterKeys(query.filter, refs.filter, refs.geoFilter, void 0, refs);
|
|
1362
|
+
const controls = query.controls ?? {};
|
|
1363
|
+
const rawGroupBy = controls.$groupBy;
|
|
1364
|
+
const groupBy = Array.isArray(rawGroupBy) ? rawGroupBy.filter((f) => typeof f === "string") : typeof rawGroupBy === "string" ? [rawGroupBy] : [];
|
|
1365
|
+
refs.aggregateMode = aggregate ?? groupBy.length > 0;
|
|
1366
|
+
refs.groupBy = groupBy;
|
|
1367
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1368
|
+
const select = controls.$select;
|
|
1369
|
+
if (Array.isArray(select)) {
|
|
1370
|
+
for (const item of select) if (typeof item === "string") refs.select.push(item);
|
|
1371
|
+
else if (refs.aggregateMode && item && typeof item === "object" && "$field" in item) {
|
|
1372
|
+
const expr = item;
|
|
1373
|
+
aliases.add(require_agg.resolveAlias(expr));
|
|
1374
|
+
if (expr.$field !== "*") refs.aggregate.push(expr.$field);
|
|
1375
|
+
}
|
|
1376
|
+
} else if (select && typeof select === "object") refs.select.push(...Object.keys(select));
|
|
1377
|
+
for (const name of sortFieldNames(controls.$sort)) if (!aliases.has(name)) refs.sort.push(name);
|
|
1378
|
+
if (refs.aggregateMode) collectFilterKeys(controls.$having, refs.having, void 0, aliases, refs);
|
|
1379
|
+
return refs;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Classifies one logical path. The rules exist once, in this order, for the
|
|
1383
|
+
* core backstop ({@link guardPath}) and the HTTP capability gate alike:
|
|
1384
|
+
*
|
|
1385
|
+
* 1. navigation relations (and anything under them) — `parent` is the nav
|
|
1386
|
+
* head when the path is a descendant;
|
|
1387
|
+
* 2. a stored leaf;
|
|
1388
|
+
* 3. a nested-object parent;
|
|
1389
|
+
* 4. a descendant of a JSON-stored column — `parent` names the column;
|
|
1390
|
+
* 5. a descendant of an `@db.encrypted` field — `parent` names the field;
|
|
1391
|
+
* 6. unknown.
|
|
1392
|
+
*/
|
|
1393
|
+
function classifyQueryPath(source, path) {
|
|
1394
|
+
if (source.navFields.has(path)) return { kind: "nav" };
|
|
1395
|
+
const navHead = findAncestorInSet(path, source.navFields);
|
|
1396
|
+
if (navHead !== void 0) return {
|
|
1397
|
+
kind: "nav",
|
|
1398
|
+
parent: navHead
|
|
1399
|
+
};
|
|
1400
|
+
if (source.leaves.has(path)) return { kind: "leaf" };
|
|
1401
|
+
if (source.objectParents.has(path)) return { kind: "objectParent" };
|
|
1402
|
+
const jsonParent = findAncestorInSet(path, source.jsonParents);
|
|
1403
|
+
if (jsonParent !== void 0) return {
|
|
1404
|
+
kind: "jsonDescendant",
|
|
1405
|
+
parent: jsonParent
|
|
1406
|
+
};
|
|
1407
|
+
const encryptedParent = findAncestorInSet(path, source.encryptedFields);
|
|
1408
|
+
if (encryptedParent !== void 0) return {
|
|
1409
|
+
kind: "encryptedDescendant",
|
|
1410
|
+
parent: encryptedParent
|
|
1411
|
+
};
|
|
1412
|
+
return { kind: "unknown" };
|
|
1413
|
+
}
|
|
1414
|
+
/** `TableMetadata` as a {@link TQueryPathSource} (descriptors are the leaves, flattened parents the object parents). */
|
|
1415
|
+
function pathSourceOf(meta) {
|
|
1416
|
+
return {
|
|
1417
|
+
navFields: meta.navFields,
|
|
1418
|
+
leaves: meta.descriptorByPath,
|
|
1419
|
+
objectParents: meta.flattenedParents,
|
|
1420
|
+
jsonParents: meta.jsonParents,
|
|
1421
|
+
encryptedFields: meta.encryptedFields
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Validates ONE logical path for ONE query position against this table's
|
|
1426
|
+
* metadata and adapter capability — the classification of
|
|
1427
|
+
* {@link classifyQueryPath} plus the position's physical requirement:
|
|
1428
|
+
*
|
|
1429
|
+
* - a leaf → physical capability (`canFilterField` / `canSortField`;
|
|
1430
|
+
* `$select` always passes);
|
|
1431
|
+
* - a nested-object parent → only `$select`, and only when it expands to
|
|
1432
|
+
* leaf columns (`selectExpansion`);
|
|
1433
|
+
* - everything else is rejected.
|
|
1434
|
+
*
|
|
1435
|
+
* `geoPredicate` marks a filter entry whose operator is `$geoWithin`: its
|
|
1436
|
+
* shape and index support were already validated by {@link guardFilter}, so
|
|
1437
|
+
* the adapter's scalar `canFilterField` veto does not apply.
|
|
1438
|
+
*
|
|
1439
|
+
* Messages are the short programmatic forms; the HTTP wording (moost-db's
|
|
1440
|
+
* `FieldCapabilityIndex`, with `$with` hints and leaf lists) is what clients
|
|
1441
|
+
* see and is authoritative — the HTTP gate always answers first.
|
|
1442
|
+
*/
|
|
1443
|
+
function guardPath(meta, adapter, path, op, geoPredicate = false) {
|
|
1444
|
+
const verb = OP_VERB[op];
|
|
1445
|
+
const { kind, parent } = classifyQueryPath(pathSourceOf(meta), path);
|
|
1446
|
+
switch (kind) {
|
|
1447
|
+
case "nav": throw pathError(path, `Cannot ${verb} "${path}" — navigation path`);
|
|
1448
|
+
case "leaf": {
|
|
1449
|
+
if (op === "select") return;
|
|
1450
|
+
const fd = meta.descriptorByPath.get(path);
|
|
1451
|
+
if (op === "sort") {
|
|
1452
|
+
if (!adapter.canSortField(fd)) throw pathError(path, `Cannot sort by "${path}" — adapter cannot sort on this storage type`);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (geoPredicate) return;
|
|
1456
|
+
if (!adapter.canFilterField(fd)) throw pathError(path, `Cannot ${verb} "${path}" — adapter cannot filter on this storage type`);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
case "objectParent":
|
|
1460
|
+
if (op === "select" && meta.selectExpansion.has(path)) return;
|
|
1461
|
+
throw pathError(path, `Cannot ${verb} "${path}" — nested object; use one of its leaf fields`);
|
|
1462
|
+
case "jsonDescendant": throw pathError(path, `Cannot ${verb} "${path}" — inside JSON-stored column "${parent}"`);
|
|
1463
|
+
case "encryptedDescendant": throw pathError(path, op === "select" ? `Cannot select "${path}" — inside encrypted field "${parent}"; select "${parent}" instead` : `Cannot ${verb} encrypted field "${path}"`);
|
|
1464
|
+
default: throw pathError(path, `Unknown field "${path}"`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Core backstop for every read / aggregate / mutation-filter entry point:
|
|
1469
|
+
* each referenced path (see {@link collectQueryPaths}) must exist on THIS
|
|
1470
|
+
* adapter with the physical capability the position needs (see
|
|
1471
|
+
* {@link guardPath}). Adapters may therefore assume every path they receive
|
|
1472
|
+
* is physical.
|
|
1473
|
+
*
|
|
1474
|
+
* In aggregate mode (`aggregate = true`) `$select` entries are aggregate
|
|
1475
|
+
* expressions whose `$field` is checked, `$groupBy` fields are checked, and
|
|
1476
|
+
* aggregate aliases (`$as` or `fn_field`) are exempt in `$sort` / `$having`.
|
|
1477
|
+
*
|
|
1478
|
+
* Returns the collected refs so callers can run further structural rules
|
|
1479
|
+
* (see {@link checkHavingKeys}) without walking the query again.
|
|
1480
|
+
*/
|
|
1481
|
+
function guardPaths(meta, adapter, query, aggregate = false) {
|
|
1482
|
+
if (!query) return;
|
|
1483
|
+
const refs = collectQueryPaths(query, aggregate);
|
|
1484
|
+
if (refs.unsupportedOperator !== void 0) throw pathError(refs.unsupportedOperator, unsupportedOperatorMessage(refs.unsupportedOperator));
|
|
1485
|
+
for (const path of refs.filter) guardPath(meta, adapter, path, "filter");
|
|
1486
|
+
for (const path of refs.geoFilter) guardPath(meta, adapter, path, "filter", true);
|
|
1487
|
+
for (const path of refs.sort) guardPath(meta, adapter, path, "sort");
|
|
1488
|
+
for (const path of refs.select) guardPath(meta, adapter, path, "select");
|
|
1489
|
+
for (const path of refs.aggregate) guardPath(meta, adapter, path, "aggregate");
|
|
1490
|
+
for (const path of refs.groupBy) guardPath(meta, adapter, path, "groupBy");
|
|
1491
|
+
for (const path of refs.having) guardPath(meta, adapter, path, "having");
|
|
1492
|
+
return refs;
|
|
1493
|
+
}
|
|
1494
|
+
/** Shared read-path guard: filter + $sort encryption checks, then the path guard. */
|
|
1235
1495
|
function guardQuery(meta, adapter, query) {
|
|
1236
1496
|
if (!query) return;
|
|
1237
1497
|
guardFilter(meta, adapter, query.filter);
|
|
1238
1498
|
guardSort(meta, query.controls?.$sort);
|
|
1499
|
+
guardPaths(meta, adapter, query);
|
|
1239
1500
|
}
|
|
1240
|
-
/**
|
|
1501
|
+
/**
|
|
1502
|
+
* `$having` is a post-aggregation filter, so a key is either an aggregate
|
|
1503
|
+
* alias (`$as`, else `fn_field` — already exempt in {@link collectQueryPaths})
|
|
1504
|
+
* or a `$groupBy` field (exact logical-path match: `metadata.clicks` grouped
|
|
1505
|
+
* stays valid). Any other key — a real but non-grouped column included — is
|
|
1506
|
+
* rejected here, once, for SDK and HTTP callers alike, instead of by the
|
|
1507
|
+
* engine (PostgreSQL / MySQL error, SQLite tolerance, Mongo `[]`). Returns
|
|
1508
|
+
* the first offending key as an error entry (`path` = the bare key, as the
|
|
1509
|
+
* `Unknown field` rejection uses); `undefined` when every key is valid.
|
|
1510
|
+
*/
|
|
1511
|
+
function checkHavingKeys(refs) {
|
|
1512
|
+
if (refs.having.length === 0) return;
|
|
1513
|
+
const grouped = new Set(refs.groupBy);
|
|
1514
|
+
for (const key of refs.having) if (!grouped.has(key)) return {
|
|
1515
|
+
path: key,
|
|
1516
|
+
message: `$having key "${key}" must be an aggregate alias or a $groupBy field`
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Aggregate-path guard: $groupBy / $select / $having encryption refs + filter
|
|
1521
|
+
* + $sort, then the path guard, then the `$having` key rule
|
|
1522
|
+
* ({@link checkHavingKeys} — after the path guard so an unknown key still
|
|
1523
|
+
* reads `Unknown field`).
|
|
1524
|
+
*/
|
|
1241
1525
|
function guardAggregate(meta, adapter, query) {
|
|
1242
1526
|
guardFilter(meta, adapter, query.filter);
|
|
1243
|
-
if (meta.encryptedFields.size === 0) return;
|
|
1244
1527
|
const controls = query.controls;
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1528
|
+
if (meta.encryptedFields.size > 0) {
|
|
1529
|
+
for (const field of controls.$groupBy ?? []) if (isEncryptedRef(meta, field)) throw encryptedRefError("ENC_FIELD_AGG", field, "group by");
|
|
1530
|
+
if (controls.$select) for (const item of controls.$select) {
|
|
1531
|
+
const field = typeof item === "string" ? item : item.$field;
|
|
1532
|
+
if (field !== "*" && isEncryptedRef(meta, field)) throw encryptedRefError("ENC_FIELD_AGG", field, "aggregate over");
|
|
1533
|
+
}
|
|
1534
|
+
if (controls.$having) guardFilter(meta, adapter, controls.$having, "ENC_FIELD_AGG");
|
|
1535
|
+
guardSort(meta, controls.$sort);
|
|
1249
1536
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1537
|
+
const refs = guardPaths(meta, adapter, query, true);
|
|
1538
|
+
const having = refs ? checkHavingKeys(refs) : void 0;
|
|
1539
|
+
if (having) throw new require_db_error.DbError("INVALID_QUERY", [having]);
|
|
1252
1540
|
}
|
|
1253
1541
|
//#endregion
|
|
1254
1542
|
//#region src/table/db-readable.ts
|
|
@@ -1761,15 +2049,7 @@ var AtscriptDbReadable = class {
|
|
|
1761
2049
|
}
|
|
1762
2050
|
guardAggregate(this._meta, this.adapter, query);
|
|
1763
2051
|
const dbQuery = this._fieldMapper.translateAggregateQuery(query, this._meta);
|
|
1764
|
-
return (await this.adapter.aggregate(dbQuery)).map((row) =>
|
|
1765
|
-
const mapped = {};
|
|
1766
|
-
for (const [key, value] of Object.entries(row)) {
|
|
1767
|
-
const logical = this._meta.physicalToPath.get(key) ?? key;
|
|
1768
|
-
const fmt = this._meta.fromStorageFormatters?.get(key);
|
|
1769
|
-
mapped[logical] = fmt && value !== null && value !== void 0 ? fmt(value) : value;
|
|
1770
|
-
}
|
|
1771
|
-
return mapped;
|
|
1772
|
-
});
|
|
2052
|
+
return (await this.adapter.aggregate(dbQuery)).map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
|
|
1773
2053
|
}
|
|
1774
2054
|
/** Whether the underlying adapter supports text search. */
|
|
1775
2055
|
isSearchable() {
|
|
@@ -2065,7 +2345,7 @@ var AtscriptDbReadable = class {
|
|
|
2065
2345
|
* Public entry point for relation loading. Used by adapters for nested $with delegation.
|
|
2066
2346
|
*/
|
|
2067
2347
|
async loadRelations(rows, withRelations) {
|
|
2068
|
-
const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-
|
|
2348
|
+
const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-C8GOpNYJ.cjs")).then((n) => n.relation_loader_exports);
|
|
2069
2349
|
return loadRelationsImpl(rows, withRelations, this);
|
|
2070
2350
|
}
|
|
2071
2351
|
/**
|
|
@@ -2131,6 +2411,10 @@ function createFailureCollector(what) {
|
|
|
2131
2411
|
//#region src/base-adapter.ts
|
|
2132
2412
|
const EMPTY_DEFAULT_FNS = /* @__PURE__ */ new Set();
|
|
2133
2413
|
const txStorage = new node_async_hooks.AsyncLocalStorage();
|
|
2414
|
+
/** The innermost open transaction of `owner` in the current async chain. */
|
|
2415
|
+
function findTxContext(owner) {
|
|
2416
|
+
for (let ctx = txStorage.getStore(); ctx; ctx = ctx.parent) if (ctx.owner === owner) return ctx;
|
|
2417
|
+
}
|
|
2134
2418
|
/**
|
|
2135
2419
|
* Abstract base class for database adapters.
|
|
2136
2420
|
*
|
|
@@ -2155,6 +2439,15 @@ const txStorage = new node_async_hooks.AsyncLocalStorage();
|
|
|
2155
2439
|
* - `this._table.isView` — whether this is a view (vs a table)
|
|
2156
2440
|
*/
|
|
2157
2441
|
var BaseDbAdapter = class {
|
|
2442
|
+
/**
|
|
2443
|
+
* The readable this adapter serves. UNSET on an administrative adapter:
|
|
2444
|
+
* `DbSpace` creates one from the factory without a readable for the
|
|
2445
|
+
* name-taking schema-sync primitives (`dropTableByName`, `dropViewByName`,
|
|
2446
|
+
* `dropTablesByName`, `getReferencingForeignKeys`, `getObjectKind`,
|
|
2447
|
+
* `getExistingColumnsForTable`, `hasRows(tableName)`), so those must derive
|
|
2448
|
+
* everything — the schema included — from the driver/connection, never from
|
|
2449
|
+
* `this._table`.
|
|
2450
|
+
*/
|
|
2158
2451
|
_table;
|
|
2159
2452
|
/**
|
|
2160
2453
|
* Resolves the correct insertedId: prefers the user-supplied PK value
|
|
@@ -2194,15 +2487,22 @@ var BaseDbAdapter = class {
|
|
|
2194
2487
|
}
|
|
2195
2488
|
/**
|
|
2196
2489
|
* Runs `fn` inside a database transaction. Nested calls (from related tables
|
|
2197
|
-
* within the same async chain) reuse the existing transaction automatically
|
|
2490
|
+
* within the same async chain) reuse the existing transaction automatically
|
|
2491
|
+
* — "existing" meaning a transaction of the same {@link _transactionOwner};
|
|
2492
|
+
* inside another adapter family's transaction this opens its own.
|
|
2198
2493
|
*
|
|
2199
2494
|
* The generic layer handles nesting detection via `AsyncLocalStorage`.
|
|
2200
2495
|
* Adapters override `_beginTransaction`, `_commitTransaction`, and
|
|
2201
2496
|
* `_rollbackTransaction` to provide raw DB-specific transaction primitives.
|
|
2202
2497
|
*/
|
|
2203
2498
|
async withTransaction(fn) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2499
|
+
const owner = this._transactionOwner();
|
|
2500
|
+
if (findTxContext(owner)) return fn();
|
|
2501
|
+
const ctx = {
|
|
2502
|
+
owner,
|
|
2503
|
+
state: void 0,
|
|
2504
|
+
parent: txStorage.getStore()
|
|
2505
|
+
};
|
|
2206
2506
|
ctx.state = await this._beginTransaction();
|
|
2207
2507
|
return txStorage.run(ctx, async () => {
|
|
2208
2508
|
try {
|
|
@@ -2218,22 +2518,39 @@ var BaseDbAdapter = class {
|
|
|
2218
2518
|
});
|
|
2219
2519
|
}
|
|
2220
2520
|
/**
|
|
2221
|
-
*
|
|
2521
|
+
* The object a transaction state is branded with (since 0.1.128). Every
|
|
2522
|
+
* adapter instance that returns the same owner shares one transaction —
|
|
2523
|
+
* override to return the driver / pool / client the adapter was constructed
|
|
2524
|
+
* with, so all tables of a space join it. The default (the adapter class)
|
|
2525
|
+
* suits adapters without a connection object (in-memory, mocks).
|
|
2526
|
+
*/
|
|
2527
|
+
_transactionOwner() {
|
|
2528
|
+
return this.constructor;
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Returns the opaque transaction state of THIS adapter's owner from the
|
|
2532
|
+
* current async context — `undefined` when no transaction is open or only
|
|
2533
|
+
* another adapter family's transaction is (its state is never handed out).
|
|
2222
2534
|
* Adapters use this to retrieve DB-specific state (e.g., MongoDB `ClientSession`).
|
|
2223
2535
|
*/
|
|
2224
2536
|
_getTransactionState() {
|
|
2225
|
-
return
|
|
2537
|
+
return findTxContext(this._transactionOwner())?.state;
|
|
2226
2538
|
}
|
|
2227
2539
|
/**
|
|
2228
2540
|
* Runs `fn` inside the transaction ALS context with the given state.
|
|
2229
2541
|
* Adapters that override `withTransaction` (e.g., to use MongoDB's
|
|
2230
2542
|
* `session.withTransaction()` Convenient API) use this to set up the
|
|
2231
2543
|
* shared context so that nested adapters see the same session.
|
|
2232
|
-
* If a context already exists (nesting), it's reused.
|
|
2544
|
+
* If a context of the same owner already exists (nesting), it's reused.
|
|
2233
2545
|
*/
|
|
2234
2546
|
_runInTransactionContext(state, fn) {
|
|
2235
|
-
|
|
2236
|
-
|
|
2547
|
+
const owner = this._transactionOwner();
|
|
2548
|
+
if (findTxContext(owner)) return fn();
|
|
2549
|
+
return txStorage.run({
|
|
2550
|
+
owner,
|
|
2551
|
+
state,
|
|
2552
|
+
parent: txStorage.getStore()
|
|
2553
|
+
}, fn);
|
|
2237
2554
|
}
|
|
2238
2555
|
/**
|
|
2239
2556
|
* Starts a raw transaction. Returns opaque state stored in the async context.
|
|
@@ -2282,11 +2599,14 @@ var BaseDbAdapter = class {
|
|
|
2282
2599
|
return false;
|
|
2283
2600
|
}
|
|
2284
2601
|
/**
|
|
2285
|
-
* Whether the DB engine
|
|
2286
|
-
*
|
|
2287
|
-
*
|
|
2288
|
-
*
|
|
2289
|
-
*
|
|
2602
|
+
* Whether the DB engine carries static `@db.default "value"` defaults in
|
|
2603
|
+
* its DDL (`DEFAULT` clauses in `CREATE TABLE`).
|
|
2604
|
+
*
|
|
2605
|
+
* @deprecated since 0.1.128 — no longer consulted: the table layer fills
|
|
2606
|
+
* static value defaults SDK-side on every adapter before validation, and the
|
|
2607
|
+
* SQL adapters emit their DDL `DEFAULT` clauses regardless of this flag.
|
|
2608
|
+
* Kept as a capability hint for tooling; nothing in the generic layer
|
|
2609
|
+
* branches on it.
|
|
2290
2610
|
*/
|
|
2291
2611
|
supportsNativeValueDefaults() {
|
|
2292
2612
|
return false;
|
|
@@ -2332,12 +2652,16 @@ var BaseDbAdapter = class {
|
|
|
2332
2652
|
}
|
|
2333
2653
|
/**
|
|
2334
2654
|
* Whether this adapter can sort by a given field.
|
|
2335
|
-
* Default: scalar columns yes
|
|
2336
|
-
* (min/max element) is a footgun for generic
|
|
2337
|
-
* stays conservative even for adapters that
|
|
2655
|
+
* Default: scalar columns yes; JSON-stored columns, `@db.json` objects and
|
|
2656
|
+
* arrays no. Mongo's array sort (min/max element) is a footgun for generic
|
|
2657
|
+
* UI sort headers, so the default stays conservative even for adapters that
|
|
2658
|
+
* technically support it — the veto keys on `designType` as well as
|
|
2659
|
+
* `storage` because nested-object adapters keep arrays / `@db.json` values
|
|
2660
|
+
* inline as `storage: 'column'` (since 0.1.128).
|
|
2338
2661
|
*/
|
|
2339
2662
|
canSortField(fd) {
|
|
2340
2663
|
if (fd.encrypted || fd.isGeoPoint) return false;
|
|
2664
|
+
if (fd.designType === "json" || fd.designType === "array") return false;
|
|
2341
2665
|
return fd.storage !== "json";
|
|
2342
2666
|
}
|
|
2343
2667
|
/**
|
|
@@ -2387,6 +2711,7 @@ var BaseDbAdapter = class {
|
|
|
2387
2711
|
* @param includeSchema - Whether to prepend `schema.` prefix (default: true).
|
|
2388
2712
|
*/
|
|
2389
2713
|
resolveTableName(includeSchema = true) {
|
|
2714
|
+
if (!this._table) throw new Error("Adapter has no registered readable: table-scoped operations need a table/view; on an administrative adapter use the name-taking primitives (dropTableByName, hasRows(tableName), …)");
|
|
2390
2715
|
const schema = this._table.schema;
|
|
2391
2716
|
const name = this._table.tableName;
|
|
2392
2717
|
return includeSchema && schema ? `${schema}.${name}` : name;
|
|
@@ -2428,7 +2753,7 @@ var BaseDbAdapter = class {
|
|
|
2428
2753
|
}
|
|
2429
2754
|
if (index.type === "plain" || index.type === "unique") {
|
|
2430
2755
|
const liveColumns = existingColumns.get(index.key);
|
|
2431
|
-
const desiredColumns = index.fields.map((f) => f.name);
|
|
2756
|
+
const desiredColumns = index.fields.map((f) => opts.renderDesiredColumn?.(index, f) ?? f.name);
|
|
2432
2757
|
if (liveColumns && (liveColumns.length !== desiredColumns.length || liveColumns.some((c, i) => c !== desiredColumns[i]))) await attempt(`rebuild index "${index.key}"`, async () => {
|
|
2433
2758
|
await opts.dropIndex(index.key);
|
|
2434
2759
|
await opts.createIndex(index);
|
|
@@ -2576,6 +2901,39 @@ var BaseDbAdapter = class {
|
|
|
2576
2901
|
* instead of requiring `@db.sync.method "recreate"` or `"drop"`.
|
|
2577
2902
|
*/
|
|
2578
2903
|
supportsColumnModify;
|
|
2904
|
+
/**
|
|
2905
|
+
* Drops several tables that reference each other (a foreign-key cycle) as
|
|
2906
|
+
* one operation. Schema sync only calls this for cycles whose members are
|
|
2907
|
+
* ALL being removed. Default: {@link dropTableByName} in the given order —
|
|
2908
|
+
* enough for engines that tolerate it (SQLite with FK checks off, MySQL with
|
|
2909
|
+
* FOREIGN_KEY_CHECKS=0); PostgreSQL overrides it with one multi-table
|
|
2910
|
+
* `DROP TABLE a, b` statement.
|
|
2911
|
+
* @since 0.1.128
|
|
2912
|
+
*/
|
|
2913
|
+
async dropTablesByName(tableNames) {
|
|
2914
|
+
for (const name of tableNames) await this.dropTableByName?.(name);
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Whether the table has at least one row. Schema sync uses it in the
|
|
2918
|
+
* pre-flight phase to refuse a primary-key change on a populated table.
|
|
2919
|
+
* Override with an EXISTS/LIMIT 1 probe — this default is `count() > 0`,
|
|
2920
|
+
* a full scan on some engines, and it can only answer for the adapter's
|
|
2921
|
+
* OWN table: for another `tableName` (or on an administrative adapter
|
|
2922
|
+
* without a readable) it returns `undefined` ("cannot tell"), which schema
|
|
2923
|
+
* sync treats as a refusal.
|
|
2924
|
+
*
|
|
2925
|
+
* @param tableName - Check this table instead of the adapter's own (schema
|
|
2926
|
+
* sync passes the OLD name of a table that is about to be renamed).
|
|
2927
|
+
* @returns `true`/`false`, or `undefined` when the adapter cannot tell.
|
|
2928
|
+
* @since 0.1.128
|
|
2929
|
+
*/
|
|
2930
|
+
async hasRows(tableName) {
|
|
2931
|
+
if (tableName !== void 0 && tableName !== this._table?.tableName) return;
|
|
2932
|
+
return await this.count({
|
|
2933
|
+
filter: {},
|
|
2934
|
+
controls: {}
|
|
2935
|
+
}) > 0;
|
|
2936
|
+
}
|
|
2579
2937
|
};
|
|
2580
2938
|
//#endregion
|
|
2581
2939
|
//#region src/strategies/application-integrity.ts
|
|
@@ -3035,11 +3393,104 @@ function _hasOperatorKeys(value) {
|
|
|
3035
3393
|
* @typeParam T - The Atscript annotated type for this table.
|
|
3036
3394
|
* @typeParam DataType - The inferred data shape from the annotated type.
|
|
3037
3395
|
*/
|
|
3038
|
-
/**
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3396
|
+
/**
|
|
3397
|
+
* Clones a write payload while dropping every own key whose value is
|
|
3398
|
+
* `=== undefined` (`undefined` ≡ absent; `null` stays an explicit NULL), so
|
|
3399
|
+
* that defaults, validation, encryption and decomposition never see an
|
|
3400
|
+
* `undefined` prop.
|
|
3401
|
+
*
|
|
3402
|
+
* Recurses into plain objects and into arrays at any depth (plain-object
|
|
3403
|
+
* elements are cloned, elements are never dropped or reordered) and never
|
|
3404
|
+
* into class instances (`Date`, `Uint8Array`/`Buffer`, `ObjectId`, …), which
|
|
3405
|
+
* are kept by reference. Arrays without plain-object elements anywhere below
|
|
3406
|
+
* them are kept by reference too. The caller's payload tree is never mutated.
|
|
3407
|
+
* @internal exported for the core spec only — not part of the package surface.
|
|
3408
|
+
*/
|
|
3409
|
+
function _cloneWritePayload(source) {
|
|
3410
|
+
if (typeof source !== "object" || source === null) return { ...source };
|
|
3411
|
+
const out = {};
|
|
3412
|
+
for (const key of Object.keys(source)) {
|
|
3413
|
+
const value = source[key];
|
|
3414
|
+
if (value === void 0) continue;
|
|
3415
|
+
out[key] = _cloneWriteValue(value);
|
|
3416
|
+
}
|
|
3417
|
+
return out;
|
|
3042
3418
|
}
|
|
3419
|
+
function _cloneWriteValue(value) {
|
|
3420
|
+
if (Array.isArray(value)) {
|
|
3421
|
+
let cloned;
|
|
3422
|
+
for (let i = 0; i < value.length; i++) {
|
|
3423
|
+
const el = value[i];
|
|
3424
|
+
if (require_validator.isPlainObject(el) || Array.isArray(el)) {
|
|
3425
|
+
const c = _cloneWriteValue(el);
|
|
3426
|
+
if (c !== el) {
|
|
3427
|
+
cloned ??= value.slice();
|
|
3428
|
+
cloned[i] = c;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
return cloned ?? value;
|
|
3433
|
+
}
|
|
3434
|
+
return require_validator.isPlainObject(value) ? _cloneWritePayload(value) : value;
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* The clone for a nested re-entry (`_depth > 0`) and for `preValidateItems`:
|
|
3438
|
+
* the root call already deep-pruned the whole tree, so only the row's own
|
|
3439
|
+
* keys are (re-)pruned — no recursion, no per-level re-cloning of subtrees.
|
|
3440
|
+
*/
|
|
3441
|
+
function _shallowPrunedClone(source) {
|
|
3442
|
+
const out = {};
|
|
3443
|
+
for (const key in source) {
|
|
3444
|
+
const value = source[key];
|
|
3445
|
+
if (value !== void 0) out[key] = value;
|
|
3446
|
+
}
|
|
3447
|
+
return out;
|
|
3448
|
+
}
|
|
3449
|
+
/**
|
|
3450
|
+
* {@link TDbWriteGuardContext} handed to a write guard: a sparse per-index
|
|
3451
|
+
* cache of pre-image reads, allocated only when `current(i)` is first used.
|
|
3452
|
+
*/
|
|
3453
|
+
var WriteGuardContext = class {
|
|
3454
|
+
action;
|
|
3455
|
+
rows;
|
|
3456
|
+
expectedVersions;
|
|
3457
|
+
_table;
|
|
3458
|
+
_pending;
|
|
3459
|
+
constructor(action, rows, expectedVersions, _table) {
|
|
3460
|
+
this.action = action;
|
|
3461
|
+
this.rows = rows;
|
|
3462
|
+
this.expectedVersions = expectedVersions;
|
|
3463
|
+
this._table = _table;
|
|
3464
|
+
}
|
|
3465
|
+
current(i) {
|
|
3466
|
+
const cache = this._pending ??= [];
|
|
3467
|
+
let pending = cache[i];
|
|
3468
|
+
if (!pending) {
|
|
3469
|
+
pending = this._table._readPreImage(this.rows[i]);
|
|
3470
|
+
cache[i] = pending;
|
|
3471
|
+
}
|
|
3472
|
+
return pending;
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
/** {@link TDbRemoveGuardContext} handed to a delete guard (one memoised pre-image read). */
|
|
3476
|
+
var RemoveGuardContext = class {
|
|
3477
|
+
id;
|
|
3478
|
+
filter;
|
|
3479
|
+
_table;
|
|
3480
|
+
_pending;
|
|
3481
|
+
constructor(id, filter, _table) {
|
|
3482
|
+
this.id = id;
|
|
3483
|
+
this.filter = filter;
|
|
3484
|
+
this._table = _table;
|
|
3485
|
+
}
|
|
3486
|
+
current() {
|
|
3487
|
+
this._pending ??= this._table.findOne({
|
|
3488
|
+
filter: this.filter,
|
|
3489
|
+
controls: {}
|
|
3490
|
+
});
|
|
3491
|
+
return this._pending;
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3043
3494
|
/** Translates a single ops record from logical to physical column names. */
|
|
3044
3495
|
function _translateOpsRecord(rec, meta) {
|
|
3045
3496
|
const out = {};
|
|
@@ -3053,6 +3504,15 @@ function _translateOpsKeys(ops, meta) {
|
|
|
3053
3504
|
mul: ops.mul ? _translateOpsRecord(ops.mul, meta) : void 0
|
|
3054
3505
|
};
|
|
3055
3506
|
}
|
|
3507
|
+
/** Upper bound of keys per `touchMany` UPDATE statement (parameter-count safety). */
|
|
3508
|
+
const TOUCH_MANY_CHUNK = 500;
|
|
3509
|
+
/** `touchMany` input rejection — always `INVALID_QUERY`, path names the key. */
|
|
3510
|
+
function invalidTouchKey(path, message) {
|
|
3511
|
+
return new require_db_error.DbError("INVALID_QUERY", [{
|
|
3512
|
+
path,
|
|
3513
|
+
message
|
|
3514
|
+
}]);
|
|
3515
|
+
}
|
|
3056
3516
|
var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
3057
3517
|
_cascadeResolver;
|
|
3058
3518
|
_fkLookupResolver;
|
|
@@ -3097,7 +3557,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3097
3557
|
* nested creation support.
|
|
3098
3558
|
*/
|
|
3099
3559
|
async insertOne(payload, opts) {
|
|
3100
|
-
return { insertedId: (await this.insertMany([payload],
|
|
3560
|
+
return { insertedId: (await this.insertMany([payload], {
|
|
3561
|
+
...opts,
|
|
3562
|
+
_action: "insert"
|
|
3563
|
+
})).insertedIds[0] };
|
|
3101
3564
|
}
|
|
3102
3565
|
/**
|
|
3103
3566
|
* Inserts multiple records with batch-optimized nested creation.
|
|
@@ -3109,16 +3572,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3109
3572
|
* (they receive our PKs as their FKs). Fully recursive — nested records
|
|
3110
3573
|
* with their own nav data trigger further batch inserts at each level.
|
|
3111
3574
|
* Recursive up to `maxDepth` (default 3).
|
|
3575
|
+
*
|
|
3576
|
+
* `opts.guard` (since 0.1.128) runs once inside the transaction, after
|
|
3577
|
+
* defaults + validation, with the prepared rows — see {@link TWriteOptions}.
|
|
3112
3578
|
*/
|
|
3113
3579
|
async insertMany(payloads, opts) {
|
|
3114
3580
|
this._ensureBuilt();
|
|
3115
|
-
const { _depth, maxDepth: userMax } = opts ?? {};
|
|
3581
|
+
const { _depth, _action, maxDepth: userMax, guard } = opts ?? {};
|
|
3116
3582
|
const maxDepth = userMax ?? 3;
|
|
3117
3583
|
const depth = _depth ?? 0;
|
|
3118
3584
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
3119
3585
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
3120
3586
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
3121
|
-
const
|
|
3587
|
+
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
3588
|
+
const items = payloads.map((p) => this._applyDefaults(clone(p)));
|
|
3589
|
+
const originals = canNest ? items.map((item) => ({ ...item })) : [];
|
|
3122
3590
|
const validator = this.getValidator("insert");
|
|
3123
3591
|
const ctx = {
|
|
3124
3592
|
mode: "insert",
|
|
@@ -3126,6 +3594,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3126
3594
|
};
|
|
3127
3595
|
this._applyDepthCtx(ctx, depth);
|
|
3128
3596
|
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3597
|
+
if (guard) {
|
|
3598
|
+
await guard(new WriteGuardContext(_action ?? "insertMany", items, Array.from({ length: items.length }), this));
|
|
3599
|
+
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3600
|
+
}
|
|
3129
3601
|
await this._encryptItems(items, "write");
|
|
3130
3602
|
const host = this;
|
|
3131
3603
|
if (canNest) await require_nested_writer.batchInsertNestedTo(host, items, maxDepth, depth);
|
|
@@ -3135,10 +3607,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3135
3607
|
prepared.push(this._fieldMapper.prepareForWrite(data, this._meta, this.adapter));
|
|
3136
3608
|
}
|
|
3137
3609
|
await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver);
|
|
3138
|
-
if (canNest) await require_nested_writer.preValidateNestedFrom(host,
|
|
3610
|
+
if (canNest) await require_nested_writer.preValidateNestedFrom(host, originals);
|
|
3139
3611
|
const result = await this.adapter.insertMany(prepared);
|
|
3140
|
-
if (canNest) await require_nested_writer.batchInsertNestedFrom(host,
|
|
3141
|
-
if (canNest) await require_nested_writer.batchInsertNestedVia(host,
|
|
3612
|
+
if (canNest) await require_nested_writer.batchInsertNestedFrom(host, originals, result.insertedIds, maxDepth, depth);
|
|
3613
|
+
if (canNest) await require_nested_writer.batchInsertNestedVia(host, originals, result.insertedIds, maxDepth, depth);
|
|
3142
3614
|
return result;
|
|
3143
3615
|
}));
|
|
3144
3616
|
}
|
|
@@ -3147,7 +3619,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3147
3619
|
* Delegates to {@link bulkReplace} for unified nested relation support.
|
|
3148
3620
|
*/
|
|
3149
3621
|
async replaceOne(payload, opts) {
|
|
3150
|
-
return this.bulkReplace([payload],
|
|
3622
|
+
return this.bulkReplace([payload], {
|
|
3623
|
+
...opts,
|
|
3624
|
+
_action: "replace"
|
|
3625
|
+
});
|
|
3151
3626
|
}
|
|
3152
3627
|
/**
|
|
3153
3628
|
* Replaces multiple records with deep nested relation support.
|
|
@@ -3156,22 +3631,27 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3156
3631
|
* replaced first (their PKs become our FKs), FROM dependents are replaced
|
|
3157
3632
|
* after (they receive our PKs as their FKs), VIA relations clear and
|
|
3158
3633
|
* re-create junction rows. Fully recursive up to `maxDepth` (default 3).
|
|
3634
|
+
*
|
|
3635
|
+
* `opts.guard` (since 0.1.128) runs once inside the transaction, after
|
|
3636
|
+
* `$cas` extraction, defaults + validation — see {@link TWriteOptions}.
|
|
3159
3637
|
*/
|
|
3160
3638
|
async bulkReplace(payloads, opts) {
|
|
3161
3639
|
this._ensureBuilt();
|
|
3162
|
-
const maxDepth = opts
|
|
3163
|
-
const
|
|
3640
|
+
const { _depth, _action, maxDepth: userMax, guard } = opts ?? {};
|
|
3641
|
+
const maxDepth = userMax ?? 3;
|
|
3642
|
+
const depth = _depth ?? 0;
|
|
3164
3643
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
3165
3644
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
3166
3645
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
3167
3646
|
const versionColumn = this.versionColumn;
|
|
3168
3647
|
const expectedVersions = Array.from({ length: payloads.length });
|
|
3648
|
+
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
3169
3649
|
const items = payloads.map((p, i) => {
|
|
3170
|
-
const
|
|
3171
|
-
expectedVersions[i] = require_ops.separateCas(
|
|
3172
|
-
return this._applyDefaults(
|
|
3650
|
+
const c = clone(p);
|
|
3651
|
+
expectedVersions[i] = require_ops.separateCas(c, versionColumn);
|
|
3652
|
+
return this._applyDefaults(c);
|
|
3173
3653
|
});
|
|
3174
|
-
const originals = canNest ?
|
|
3654
|
+
const originals = canNest ? items.map((item) => ({ ...item })) : [];
|
|
3175
3655
|
const validator = this.getValidator("bulkReplace");
|
|
3176
3656
|
const ctx = {
|
|
3177
3657
|
mode: "replace",
|
|
@@ -3179,6 +3659,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3179
3659
|
};
|
|
3180
3660
|
this._applyDepthCtx(ctx, depth);
|
|
3181
3661
|
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3662
|
+
if (guard) {
|
|
3663
|
+
await guard(new WriteGuardContext(_action ?? "replaceMany", items, expectedVersions, this));
|
|
3664
|
+
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3665
|
+
}
|
|
3182
3666
|
await this._encryptItems(items, "write");
|
|
3183
3667
|
const host = this;
|
|
3184
3668
|
if (canNest) await require_nested_writer.batchReplaceNestedTo(host, items, maxDepth, depth);
|
|
@@ -3209,7 +3693,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3209
3693
|
* Delegates to {@link bulkUpdate} for unified nested relation support.
|
|
3210
3694
|
*/
|
|
3211
3695
|
async updateOne(payload, opts) {
|
|
3212
|
-
return this.bulkUpdate([payload],
|
|
3696
|
+
return this.bulkUpdate([payload], {
|
|
3697
|
+
...opts,
|
|
3698
|
+
_action: "update"
|
|
3699
|
+
});
|
|
3213
3700
|
}
|
|
3214
3701
|
/**
|
|
3215
3702
|
* Partially updates multiple records with deep nested relation support.
|
|
@@ -3217,18 +3704,24 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3217
3704
|
* Only TO relations (1:1, N:1) are supported for patching. FROM/VIA
|
|
3218
3705
|
* relations will error — use {@link bulkReplace} for those.
|
|
3219
3706
|
* Recursive up to `maxDepth` (default 3).
|
|
3707
|
+
*
|
|
3708
|
+
* `opts.guard` (since 0.1.128) runs once inside the transaction, after
|
|
3709
|
+
* `$cas` extraction and validation, with the patches (identifying fields
|
|
3710
|
+
* present, `$cas` removed) — see {@link TWriteOptions}.
|
|
3220
3711
|
*/
|
|
3221
3712
|
async bulkUpdate(payloads, opts) {
|
|
3222
3713
|
this._ensureBuilt();
|
|
3223
|
-
const maxDepth = opts
|
|
3224
|
-
const
|
|
3714
|
+
const { _depth, _action, maxDepth: userMax, guard } = opts ?? {};
|
|
3715
|
+
const maxDepth = userMax ?? 3;
|
|
3716
|
+
const depth = _depth ?? 0;
|
|
3225
3717
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
3226
3718
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
3227
3719
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
3228
3720
|
const versionColumn = this.versionColumn;
|
|
3229
3721
|
const expectedVersions = Array.from({ length: payloads.length });
|
|
3722
|
+
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
3230
3723
|
const cloned = payloads.map((p, i) => {
|
|
3231
|
-
const c =
|
|
3724
|
+
const c = clone(p);
|
|
3232
3725
|
expectedVersions[i] = require_ops.separateCas(c, versionColumn);
|
|
3233
3726
|
return c;
|
|
3234
3727
|
});
|
|
@@ -3240,6 +3733,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3240
3733
|
};
|
|
3241
3734
|
this._applyDepthCtx(ctx, depth);
|
|
3242
3735
|
require_nested_writer.validateBatch(validator, cloned, ctx);
|
|
3736
|
+
if (guard) {
|
|
3737
|
+
await guard(new WriteGuardContext(_action ?? "updateMany", cloned, expectedVersions, this));
|
|
3738
|
+
require_nested_writer.validateBatch(validator, cloned, ctx);
|
|
3739
|
+
}
|
|
3243
3740
|
const originals = canNest ? cloned.map((p) => ({ ...p })) : [];
|
|
3244
3741
|
await this._encryptItems(cloned, "patch");
|
|
3245
3742
|
const host = this;
|
|
@@ -3255,13 +3752,16 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3255
3752
|
const filter = this._extractRecordFilter(data);
|
|
3256
3753
|
for (const key of Object.keys(filter)) delete data[key];
|
|
3257
3754
|
if (versionColumn !== void 0) assertNoVersionWrites(data, versionColumn);
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3755
|
+
const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3756
|
+
if (require_validator.isEmptyObject(data) && expectedVersion === void 0) {
|
|
3757
|
+
const exists = await this.adapter.count({
|
|
3758
|
+
filter: translatedFilter,
|
|
3759
|
+
controls: {}
|
|
3760
|
+
});
|
|
3761
|
+
matchedCount += exists > 0 ? 1 : 0;
|
|
3261
3762
|
continue;
|
|
3262
3763
|
}
|
|
3263
3764
|
let result;
|
|
3264
|
-
const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3265
3765
|
if (this.adapter.supportsNativePatch()) {
|
|
3266
3766
|
const ops = require_ops.separateFieldOps(data);
|
|
3267
3767
|
const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
|
|
@@ -3292,27 +3792,114 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3292
3792
|
}));
|
|
3293
3793
|
}
|
|
3294
3794
|
/**
|
|
3795
|
+
* Batch versioned touch (since 0.1.129): bumps the version of every listed
|
|
3796
|
+
* row by exactly one, each row guarded by its own expected version. This is
|
|
3797
|
+
* the batch fence `updateMany(orFilter, {})` used to be before 0.1.128 (an
|
|
3798
|
+
* empty patch is a no-op since then and takes no lock).
|
|
3799
|
+
*
|
|
3800
|
+
* Each key carries the primary key field(s) (composite supported) plus the
|
|
3801
|
+
* version column and NOTHING else — a touch has no payload. Unique indexes
|
|
3802
|
+
* do not identify a touch key. `undefined`-valued properties are ignored,
|
|
3803
|
+
* like in every write payload. Empty `keys` → `{ 0, 0 }` without a statement.
|
|
3804
|
+
*
|
|
3805
|
+
* `require: 'all'` (default): one count over the whole key set runs FIRST;
|
|
3806
|
+
* a stale or missing row throws {@link CasMismatchError} before any write.
|
|
3807
|
+
* The bumps then run as `updateMany(orFilter, {})` chunks of at most
|
|
3808
|
+
* {@link TOUCH_MANY_CHUNK} keys inside one adapter transaction; a summed
|
|
3809
|
+
* `matchedCount` short of `keys.length` (a row moved between the count and
|
|
3810
|
+
* the bump) throws the same error — SQL engines roll every bump back. The
|
|
3811
|
+
* pre-count is therefore a deliberate double check on SQL: it is what makes
|
|
3812
|
+
* the guarantee hold on adapters whose `withTransaction` is a passthrough
|
|
3813
|
+
* (the memory adapter, a Mongo standalone topology) — there it covers the
|
|
3814
|
+
* common stale case and the residual race window is accepted.
|
|
3815
|
+
* `require: 'any'`: no pre-count, the honest summed result is returned.
|
|
3816
|
+
*
|
|
3817
|
+
* No `guard`, no `onWrite`; not exposed over HTTP.
|
|
3818
|
+
*/
|
|
3819
|
+
async touchMany(keys, opts) {
|
|
3820
|
+
this._ensureBuilt();
|
|
3821
|
+
const versionField = this._meta.versionField;
|
|
3822
|
+
if (versionField === void 0) throw invalidTouchKey("", "touchMany requires @db.column.version");
|
|
3823
|
+
if (keys.length === 0) return {
|
|
3824
|
+
matchedCount: 0,
|
|
3825
|
+
modifiedCount: 0
|
|
3826
|
+
};
|
|
3827
|
+
const pkFields = this.primaryKeys;
|
|
3828
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3829
|
+
const pairs = [];
|
|
3830
|
+
for (const [i, key] of keys.entries()) {
|
|
3831
|
+
const pair = {};
|
|
3832
|
+
for (const pk of pkFields) {
|
|
3833
|
+
if (key[pk] === void 0) throw invalidTouchKey(`[${i}].${pk}`, `touchMany: each key must carry its "${pk}"`);
|
|
3834
|
+
pair[pk] = key[pk];
|
|
3835
|
+
}
|
|
3836
|
+
const version = key[versionField];
|
|
3837
|
+
if (typeof version !== "number" || !Number.isFinite(version)) throw invalidTouchKey(`[${i}].${versionField}`, `touchMany: each key must carry its expected "${versionField}" (number)`);
|
|
3838
|
+
for (const [prop, value] of Object.entries(key)) if (value !== void 0 && prop !== versionField && !(prop in pair)) throw invalidTouchKey(`[${i}].${prop}`, `touchMany: a touch carries no payload — keys hold the primary key and "${versionField}" only, got "${prop}"`);
|
|
3839
|
+
const identity = JSON.stringify(pair);
|
|
3840
|
+
if (seen.has(identity)) throw invalidTouchKey(`[${i}]`, `touchMany: duplicate key ${identity}`);
|
|
3841
|
+
seen.add(identity);
|
|
3842
|
+
pair[versionField] = version;
|
|
3843
|
+
pairs.push(pair);
|
|
3844
|
+
}
|
|
3845
|
+
const orFilter = { $or: pairs };
|
|
3846
|
+
this._guardMutationFilter(orFilter);
|
|
3847
|
+
const translated = this._fieldMapper.translateFilter(orFilter, this._meta).$or;
|
|
3848
|
+
const requireAll = (opts?.require ?? "all") === "all";
|
|
3849
|
+
if (requireAll) {
|
|
3850
|
+
const matched = await this.adapter.count({
|
|
3851
|
+
filter: { $or: translated },
|
|
3852
|
+
controls: {}
|
|
3853
|
+
});
|
|
3854
|
+
if (matched < keys.length) throw new require_db_error.CasMismatchError(matched, keys.length);
|
|
3855
|
+
}
|
|
3856
|
+
return this.adapter.withTransaction(async () => {
|
|
3857
|
+
let matchedCount = 0;
|
|
3858
|
+
let modifiedCount = 0;
|
|
3859
|
+
for (let start = 0; start < translated.length; start += TOUCH_MANY_CHUNK) {
|
|
3860
|
+
const chunk = translated.slice(start, start + TOUCH_MANY_CHUNK);
|
|
3861
|
+
const result = await this.adapter.updateMany({ $or: chunk }, {}, void 0);
|
|
3862
|
+
matchedCount += result.matchedCount;
|
|
3863
|
+
modifiedCount += result.modifiedCount;
|
|
3864
|
+
}
|
|
3865
|
+
if (requireAll && matchedCount !== keys.length) throw new require_db_error.CasMismatchError(matchedCount, keys.length);
|
|
3866
|
+
return {
|
|
3867
|
+
matchedCount,
|
|
3868
|
+
modifiedCount
|
|
3869
|
+
};
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3295
3873
|
* Deletes a single record by any type-compatible identifier — primary key
|
|
3296
3874
|
* or single-field unique index. Uses the same resolution logic as `findById`.
|
|
3297
3875
|
*
|
|
3298
3876
|
* When the adapter does not support native foreign keys (e.g. MongoDB),
|
|
3299
3877
|
* cascade and setNull actions are applied before the delete.
|
|
3878
|
+
*
|
|
3879
|
+
* `opts.guard` (since 0.1.128) runs inside the transaction once the id has
|
|
3880
|
+
* resolved to a filter, before cascade / delete — see {@link TDeleteOptions}.
|
|
3881
|
+
* An id that resolves to no filter answers `{ deletedCount: 0 }` without
|
|
3882
|
+
* calling the guard.
|
|
3300
3883
|
*/
|
|
3301
|
-
async deleteOne(id) {
|
|
3884
|
+
async deleteOne(id, opts) {
|
|
3302
3885
|
this._ensureBuilt();
|
|
3303
3886
|
const filter = this._resolveIdFilter(id);
|
|
3304
3887
|
if (!filter) return { deletedCount: 0 };
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3888
|
+
const guard = opts?.guard;
|
|
3889
|
+
const needsCascade = this._integrity.needsCascade(this._cascadeResolver);
|
|
3890
|
+
const translated = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3891
|
+
const run = async () => {
|
|
3892
|
+
if (guard) await guard(new RemoveGuardContext(id, filter, this));
|
|
3893
|
+
if (needsCascade) await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
|
|
3894
|
+
return this.adapter.deleteOne(translated);
|
|
3895
|
+
};
|
|
3896
|
+
return require_nested_writer.remapDeleteFkViolation(this.tableName, () => guard || needsCascade ? this.adapter.withTransaction(run) : run());
|
|
3310
3897
|
}
|
|
3311
3898
|
async updateMany(filter, data) {
|
|
3312
3899
|
this._ensureBuilt();
|
|
3313
3900
|
this._guardMutationFilter(filter);
|
|
3314
3901
|
await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
|
|
3315
|
-
const dataCopy =
|
|
3902
|
+
const dataCopy = _cloneWritePayload(data);
|
|
3316
3903
|
const versionColumn = this.versionColumn;
|
|
3317
3904
|
if ("$cas" in dataCopy) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
3318
3905
|
path: "$cas",
|
|
@@ -3324,13 +3911,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3324
3911
|
const ops = require_ops.separateFieldOps(update);
|
|
3325
3912
|
const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
|
|
3326
3913
|
const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
|
|
3327
|
-
|
|
3914
|
+
const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3915
|
+
if (translatedOps === void 0 && require_validator.isEmptyObject(translatedUpdate)) return {
|
|
3916
|
+
matchedCount: await this.adapter.count({
|
|
3917
|
+
filter: translatedFilter,
|
|
3918
|
+
controls: {}
|
|
3919
|
+
}),
|
|
3920
|
+
modifiedCount: 0
|
|
3921
|
+
};
|
|
3922
|
+
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.updateMany(translatedFilter, translatedUpdate, translatedOps));
|
|
3328
3923
|
}
|
|
3329
3924
|
async replaceMany(filter, data) {
|
|
3330
3925
|
this._ensureBuilt();
|
|
3331
3926
|
this._guardMutationFilter(filter);
|
|
3332
3927
|
await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver);
|
|
3333
|
-
const dataCopy =
|
|
3928
|
+
const dataCopy = _cloneWritePayload(data);
|
|
3334
3929
|
await this._encryptItems([dataCopy], "write");
|
|
3335
3930
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.replaceMany(this._fieldMapper.translateFilter(filter, this._meta), this._fieldMapper.prepareForWrite(dataCopy, this._meta, this.adapter)));
|
|
3336
3931
|
}
|
|
@@ -3360,6 +3955,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3360
3955
|
/** Engine-agnostic guard for user-supplied mutation filters (updateMany/deleteMany/…). */
|
|
3361
3956
|
_guardMutationFilter(filter) {
|
|
3362
3957
|
guardFilter(this._meta, this.adapter, filter);
|
|
3958
|
+
guardPaths(this._meta, this.adapter, { filter });
|
|
3363
3959
|
}
|
|
3364
3960
|
/**
|
|
3365
3961
|
* Encrypts `@db.encrypted` field values in place on (already validated)
|
|
@@ -3389,21 +3985,43 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3389
3985
|
}
|
|
3390
3986
|
}
|
|
3391
3987
|
/**
|
|
3392
|
-
*
|
|
3393
|
-
*
|
|
3394
|
-
*
|
|
3988
|
+
* Lazy pre-image read for a guard's `current(i)`: `null` when the row has
|
|
3989
|
+
* no identifying key (e.g. an auto-increment insert) or the key cannot be
|
|
3990
|
+
* resolved — never throws for a missing key.
|
|
3991
|
+
* @internal
|
|
3992
|
+
*/
|
|
3993
|
+
async _readPreImage(row) {
|
|
3994
|
+
if (!row) return null;
|
|
3995
|
+
let filter;
|
|
3996
|
+
try {
|
|
3997
|
+
filter = this._resolveIdFilter(row);
|
|
3998
|
+
} catch {
|
|
3999
|
+
return null;
|
|
4000
|
+
}
|
|
4001
|
+
if (!filter) return null;
|
|
4002
|
+
return await this.findOne({
|
|
4003
|
+
filter,
|
|
4004
|
+
controls: {}
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
/**
|
|
4008
|
+
* Applies `@db.default` values in place to a row's absent fields — the
|
|
4009
|
+
* defaults pass every insert / replace path runs before validation.
|
|
4010
|
+
* Static value defaults (`@db.default 'x'`) are filled on EVERY adapter
|
|
4011
|
+
* (since 0.1.128 — writing the column's own default explicitly is
|
|
4012
|
+
* equivalent to leaving it to the DDL `DEFAULT`, and write guards see the
|
|
4013
|
+
* full row). Function defaults (`now` / `uuid` / `increment` / custom) the
|
|
4014
|
+
* adapter handles natively are NOT filled — the field stays absent so the
|
|
4015
|
+
* engine's own default applies. The version column is never touched.
|
|
3395
4016
|
*/
|
|
3396
4017
|
_applyDefaults(data) {
|
|
3397
|
-
const nativeValues = this.adapter.supportsNativeValueDefaults();
|
|
3398
4018
|
const nativeFns = this.adapter.nativeDefaultFns();
|
|
3399
4019
|
const versionField = this._meta.versionField;
|
|
3400
4020
|
for (const [field, def] of this._meta.defaults.entries()) {
|
|
3401
4021
|
if (field === versionField) continue;
|
|
3402
4022
|
if (data[field] === void 0) {
|
|
3403
|
-
if (def.kind === "value"
|
|
3404
|
-
|
|
3405
|
-
data[field] = (fieldType?.type.kind === "" && fieldType.type.designType) === "string" ? def.value : JSON.parse(def.value);
|
|
3406
|
-
} else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
|
|
4023
|
+
if (def.kind === "value") data[field] = this._parseValueDefault(field, def.value);
|
|
4024
|
+
else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
|
|
3407
4025
|
case "now":
|
|
3408
4026
|
data[field] = Date.now();
|
|
3409
4027
|
break;
|
|
@@ -3416,6 +4034,22 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3416
4034
|
return data;
|
|
3417
4035
|
}
|
|
3418
4036
|
/**
|
|
4037
|
+
* The JS value for a `@db.default 'literal'`: strings (including unions of
|
|
4038
|
+
* string literals) are used as-is, every other design type is parsed as
|
|
4039
|
+
* JSON — the same value the SQL adapters put into the DDL `DEFAULT` clause.
|
|
4040
|
+
* A literal that is not valid JSON falls back to the raw string so the
|
|
4041
|
+
* validator reports it against the field instead of a bare `SyntaxError`.
|
|
4042
|
+
*/
|
|
4043
|
+
_parseValueDefault(field, literal) {
|
|
4044
|
+
const fieldType = this._meta.flatMap?.get(field);
|
|
4045
|
+
if ((fieldType ? resolveDesignType(fieldType) : "string") === "string") return literal;
|
|
4046
|
+
try {
|
|
4047
|
+
return JSON.parse(literal);
|
|
4048
|
+
} catch {
|
|
4049
|
+
return literal;
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
/**
|
|
3419
4053
|
* Extracts a record-identifying filter from a payload.
|
|
3420
4054
|
*
|
|
3421
4055
|
* Resolution order:
|
|
@@ -3521,7 +4155,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3521
4155
|
mode: "insert",
|
|
3522
4156
|
navFields: this._meta.navFields
|
|
3523
4157
|
};
|
|
3524
|
-
require_nested_writer.validateBatch(validator, items.map((raw) => this._applyDefaults(
|
|
4158
|
+
require_nested_writer.validateBatch(validator, items.map((raw) => this._applyDefaults(_shallowPrunedClone(raw))), ctx);
|
|
3525
4159
|
await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver, false, opts?.excludeFkTargetTable);
|
|
3526
4160
|
}
|
|
3527
4161
|
/**
|
|
@@ -3535,22 +4169,6 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3535
4169
|
const adapterPlugins = this.adapter.getValidatorPlugins();
|
|
3536
4170
|
if (purpose === "insert" || purpose === "patch" || purpose === "bulkReplace") {
|
|
3537
4171
|
const mode = purpose === "bulkReplace" ? "replace" : purpose;
|
|
3538
|
-
const versionField = this._meta.versionField;
|
|
3539
|
-
if (versionField !== void 0) {
|
|
3540
|
-
const plugins = adapterPlugins.length ? [...adapterPlugins, require_validator.dbPlugin] : [require_validator.dbPlugin];
|
|
3541
|
-
return this.createValidator({
|
|
3542
|
-
plugins,
|
|
3543
|
-
partial: mode === "patch" ? require_validator.buildPatchPartial(this._meta.navFields) : false,
|
|
3544
|
-
replace: (def, path) => {
|
|
3545
|
-
const transformed = require_validator.forceNavNonOptional(def);
|
|
3546
|
-
if (path === versionField && !transformed.optional) return {
|
|
3547
|
-
...transformed,
|
|
3548
|
-
optional: true
|
|
3549
|
-
};
|
|
3550
|
-
return transformed;
|
|
3551
|
-
}
|
|
3552
|
-
});
|
|
3553
|
-
}
|
|
3554
4172
|
return require_validator.buildDbValidator(this.type, mode, adapterPlugins);
|
|
3555
4173
|
}
|
|
3556
4174
|
if (purpose === "bulkUpdate") {
|
|
@@ -3664,7 +4282,9 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
3664
4282
|
"db.agg.min",
|
|
3665
4283
|
"db.agg.max"
|
|
3666
4284
|
];
|
|
4285
|
+
const ignored = this.ignoredFields;
|
|
3667
4286
|
for (const [fieldName, fieldType] of this._type.type.props.entries()) {
|
|
4287
|
+
if (ignored.has(fieldName)) continue;
|
|
3668
4288
|
let aggFn;
|
|
3669
4289
|
let aggField;
|
|
3670
4290
|
for (const key of aggKeys) {
|
|
@@ -3700,6 +4320,18 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
3700
4320
|
return mappings;
|
|
3701
4321
|
}
|
|
3702
4322
|
};
|
|
4323
|
+
/**
|
|
4324
|
+
* Structural type guard for views: `true` when the readable reports
|
|
4325
|
+
* `isView`, whether or not it is an `AtscriptDbView` instance of THIS copy
|
|
4326
|
+
* of `@atscript/db`. Adapters must use this (or `readable.isView`) instead of
|
|
4327
|
+
* `instanceof AtscriptDbView` — in a bundle that carries two copies of the
|
|
4328
|
+
* core (app bundle + external adapter), `instanceof` is false and the adapter
|
|
4329
|
+
* would create an empty physical table under the view's name.
|
|
4330
|
+
* @since 0.1.128
|
|
4331
|
+
*/
|
|
4332
|
+
function isAtscriptDbView(readable) {
|
|
4333
|
+
return readable.isView;
|
|
4334
|
+
}
|
|
3703
4335
|
//#endregion
|
|
3704
4336
|
Object.defineProperty(exports, "ApplicationIntegrity", {
|
|
3705
4337
|
enumerable: true,
|
|
@@ -3791,6 +4423,24 @@ Object.defineProperty(exports, "assertNoVersionWrites", {
|
|
|
3791
4423
|
return assertNoVersionWrites;
|
|
3792
4424
|
}
|
|
3793
4425
|
});
|
|
4426
|
+
Object.defineProperty(exports, "checkHavingKeys", {
|
|
4427
|
+
enumerable: true,
|
|
4428
|
+
get: function() {
|
|
4429
|
+
return checkHavingKeys;
|
|
4430
|
+
}
|
|
4431
|
+
});
|
|
4432
|
+
Object.defineProperty(exports, "classifyQueryPath", {
|
|
4433
|
+
enumerable: true,
|
|
4434
|
+
get: function() {
|
|
4435
|
+
return classifyQueryPath;
|
|
4436
|
+
}
|
|
4437
|
+
});
|
|
4438
|
+
Object.defineProperty(exports, "collectQueryPaths", {
|
|
4439
|
+
enumerable: true,
|
|
4440
|
+
get: function() {
|
|
4441
|
+
return collectQueryPaths;
|
|
4442
|
+
}
|
|
4443
|
+
});
|
|
3794
4444
|
Object.defineProperty(exports, "createFailureCollector", {
|
|
3795
4445
|
enumerable: true,
|
|
3796
4446
|
get: function() {
|
|
@@ -3803,6 +4453,12 @@ Object.defineProperty(exports, "decomposePatch", {
|
|
|
3803
4453
|
return decomposePatch;
|
|
3804
4454
|
}
|
|
3805
4455
|
});
|
|
4456
|
+
Object.defineProperty(exports, "findAncestorInSet", {
|
|
4457
|
+
enumerable: true,
|
|
4458
|
+
get: function() {
|
|
4459
|
+
return findAncestorInSet;
|
|
4460
|
+
}
|
|
4461
|
+
});
|
|
3806
4462
|
Object.defineProperty(exports, "guardAggregate", {
|
|
3807
4463
|
enumerable: true,
|
|
3808
4464
|
get: function() {
|
|
@@ -3815,12 +4471,30 @@ Object.defineProperty(exports, "guardFilter", {
|
|
|
3815
4471
|
return guardFilter;
|
|
3816
4472
|
}
|
|
3817
4473
|
});
|
|
4474
|
+
Object.defineProperty(exports, "guardPath", {
|
|
4475
|
+
enumerable: true,
|
|
4476
|
+
get: function() {
|
|
4477
|
+
return guardPath;
|
|
4478
|
+
}
|
|
4479
|
+
});
|
|
4480
|
+
Object.defineProperty(exports, "guardPaths", {
|
|
4481
|
+
enumerable: true,
|
|
4482
|
+
get: function() {
|
|
4483
|
+
return guardPaths;
|
|
4484
|
+
}
|
|
4485
|
+
});
|
|
3818
4486
|
Object.defineProperty(exports, "guardQuery", {
|
|
3819
4487
|
enumerable: true,
|
|
3820
4488
|
get: function() {
|
|
3821
4489
|
return guardQuery;
|
|
3822
4490
|
}
|
|
3823
4491
|
});
|
|
4492
|
+
Object.defineProperty(exports, "isAtscriptDbView", {
|
|
4493
|
+
enumerable: true,
|
|
4494
|
+
get: function() {
|
|
4495
|
+
return isAtscriptDbView;
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
3824
4498
|
Object.defineProperty(exports, "isGeoIndexableType", {
|
|
3825
4499
|
enumerable: true,
|
|
3826
4500
|
get: function() {
|
|
@@ -3839,3 +4513,15 @@ Object.defineProperty(exports, "resolveDesignType", {
|
|
|
3839
4513
|
return resolveDesignType;
|
|
3840
4514
|
}
|
|
3841
4515
|
});
|
|
4516
|
+
Object.defineProperty(exports, "sortFieldNames", {
|
|
4517
|
+
enumerable: true,
|
|
4518
|
+
get: function() {
|
|
4519
|
+
return sortFieldNames;
|
|
4520
|
+
}
|
|
4521
|
+
});
|
|
4522
|
+
Object.defineProperty(exports, "unsupportedOperatorMessage", {
|
|
4523
|
+
enumerable: true,
|
|
4524
|
+
get: function() {
|
|
4525
|
+
return unsupportedOperatorMessage;
|
|
4526
|
+
}
|
|
4527
|
+
});
|