@atscript/db 0.1.127 → 0.1.128
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-readable-BkAGccv9.d.mts → db-readable-BTl4NzNN.d.mts} +289 -16
- package/dist/{db-readable-C0nDKX8A.d.cts → db-readable-CkbZn_z-.d.cts} +289 -16
- package/dist/{db-space-B_ASuDaR.d.mts → db-space-BEB5Jl8M.d.mts} +81 -29
- package/dist/{db-space-CSntT6yS.d.cts → db-space-BevOyVrC.d.cts} +81 -29
- package/dist/{db-view-BP0Qbeux.cjs → db-view-C8-MsjND.cjs} +696 -97
- package/dist/{db-view-C8rZM5_N.mjs → db-view-Ck0I-PMI.mjs} +643 -98
- package/dist/index.cjs +46 -2
- package/dist/index.d.cts +162 -37
- package/dist/index.d.mts +162 -37
- package/dist/index.mjs +36 -4
- 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 +43 -0
- package/dist/ops.d.cts +2 -2
- package/dist/ops.d.mts +2 -2
- package/dist/ops.mjs +43 -1
- package/dist/plugin.cjs +12 -5
- package/dist/plugin.mjs +12 -5
- package/dist/rel.d.cts +1 -1
- package/dist/rel.d.mts +1 -1
- package/dist/sync.cjs +1096 -361
- package/dist/sync.d.cts +234 -15
- package/dist/sync.d.mts +234 -15
- package/dist/sync.mjs +1095 -362
- package/dist/{validator-CSGug4vg.cjs → validator-BNUHCXIE.cjs} +31 -2
- package/dist/{validator-BcBtg8yW.d.cts → validator-DuAmWc8L.d.cts} +38 -1
- package/dist/{validator-BcBtg8yW.d.mts → validator-DuAmWc8L.d.mts} +38 -1
- package/dist/{validator-0vRXN51D.mjs → validator-eKYWf3xf.mjs} +20 -3
- 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
1
|
const require_db_error = require("./db-error-DXwEzmYJ.cjs");
|
|
2
|
+
const require_validator = require("./validator-BNUHCXIE.cjs");
|
|
2
3
|
const require_agg = require("./agg.cjs");
|
|
3
4
|
const require_nested_writer = require("./nested-writer-DoDhl3X3.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);
|
|
1500
|
+
}
|
|
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
|
+
};
|
|
1239
1518
|
}
|
|
1240
|
-
/**
|
|
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() {
|
|
@@ -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 = {};
|
|
@@ -3097,7 +3548,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3097
3548
|
* nested creation support.
|
|
3098
3549
|
*/
|
|
3099
3550
|
async insertOne(payload, opts) {
|
|
3100
|
-
return { insertedId: (await this.insertMany([payload],
|
|
3551
|
+
return { insertedId: (await this.insertMany([payload], {
|
|
3552
|
+
...opts,
|
|
3553
|
+
_action: "insert"
|
|
3554
|
+
})).insertedIds[0] };
|
|
3101
3555
|
}
|
|
3102
3556
|
/**
|
|
3103
3557
|
* Inserts multiple records with batch-optimized nested creation.
|
|
@@ -3109,16 +3563,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3109
3563
|
* (they receive our PKs as their FKs). Fully recursive — nested records
|
|
3110
3564
|
* with their own nav data trigger further batch inserts at each level.
|
|
3111
3565
|
* Recursive up to `maxDepth` (default 3).
|
|
3566
|
+
*
|
|
3567
|
+
* `opts.guard` (since 0.1.128) runs once inside the transaction, after
|
|
3568
|
+
* defaults + validation, with the prepared rows — see {@link TWriteOptions}.
|
|
3112
3569
|
*/
|
|
3113
3570
|
async insertMany(payloads, opts) {
|
|
3114
3571
|
this._ensureBuilt();
|
|
3115
|
-
const { _depth, maxDepth: userMax } = opts ?? {};
|
|
3572
|
+
const { _depth, _action, maxDepth: userMax, guard } = opts ?? {};
|
|
3116
3573
|
const maxDepth = userMax ?? 3;
|
|
3117
3574
|
const depth = _depth ?? 0;
|
|
3118
3575
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
3119
3576
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
3120
3577
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
3121
|
-
const
|
|
3578
|
+
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
3579
|
+
const items = payloads.map((p) => this._applyDefaults(clone(p)));
|
|
3580
|
+
const originals = canNest ? items.map((item) => ({ ...item })) : [];
|
|
3122
3581
|
const validator = this.getValidator("insert");
|
|
3123
3582
|
const ctx = {
|
|
3124
3583
|
mode: "insert",
|
|
@@ -3126,6 +3585,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3126
3585
|
};
|
|
3127
3586
|
this._applyDepthCtx(ctx, depth);
|
|
3128
3587
|
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3588
|
+
if (guard) {
|
|
3589
|
+
await guard(new WriteGuardContext(_action ?? "insertMany", items, Array.from({ length: items.length }), this));
|
|
3590
|
+
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3591
|
+
}
|
|
3129
3592
|
await this._encryptItems(items, "write");
|
|
3130
3593
|
const host = this;
|
|
3131
3594
|
if (canNest) await require_nested_writer.batchInsertNestedTo(host, items, maxDepth, depth);
|
|
@@ -3135,10 +3598,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3135
3598
|
prepared.push(this._fieldMapper.prepareForWrite(data, this._meta, this.adapter));
|
|
3136
3599
|
}
|
|
3137
3600
|
await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver);
|
|
3138
|
-
if (canNest) await require_nested_writer.preValidateNestedFrom(host,
|
|
3601
|
+
if (canNest) await require_nested_writer.preValidateNestedFrom(host, originals);
|
|
3139
3602
|
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,
|
|
3603
|
+
if (canNest) await require_nested_writer.batchInsertNestedFrom(host, originals, result.insertedIds, maxDepth, depth);
|
|
3604
|
+
if (canNest) await require_nested_writer.batchInsertNestedVia(host, originals, result.insertedIds, maxDepth, depth);
|
|
3142
3605
|
return result;
|
|
3143
3606
|
}));
|
|
3144
3607
|
}
|
|
@@ -3147,7 +3610,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3147
3610
|
* Delegates to {@link bulkReplace} for unified nested relation support.
|
|
3148
3611
|
*/
|
|
3149
3612
|
async replaceOne(payload, opts) {
|
|
3150
|
-
return this.bulkReplace([payload],
|
|
3613
|
+
return this.bulkReplace([payload], {
|
|
3614
|
+
...opts,
|
|
3615
|
+
_action: "replace"
|
|
3616
|
+
});
|
|
3151
3617
|
}
|
|
3152
3618
|
/**
|
|
3153
3619
|
* Replaces multiple records with deep nested relation support.
|
|
@@ -3156,22 +3622,27 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3156
3622
|
* replaced first (their PKs become our FKs), FROM dependents are replaced
|
|
3157
3623
|
* after (they receive our PKs as their FKs), VIA relations clear and
|
|
3158
3624
|
* re-create junction rows. Fully recursive up to `maxDepth` (default 3).
|
|
3625
|
+
*
|
|
3626
|
+
* `opts.guard` (since 0.1.128) runs once inside the transaction, after
|
|
3627
|
+
* `$cas` extraction, defaults + validation — see {@link TWriteOptions}.
|
|
3159
3628
|
*/
|
|
3160
3629
|
async bulkReplace(payloads, opts) {
|
|
3161
3630
|
this._ensureBuilt();
|
|
3162
|
-
const maxDepth = opts
|
|
3163
|
-
const
|
|
3631
|
+
const { _depth, _action, maxDepth: userMax, guard } = opts ?? {};
|
|
3632
|
+
const maxDepth = userMax ?? 3;
|
|
3633
|
+
const depth = _depth ?? 0;
|
|
3164
3634
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
3165
3635
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
3166
3636
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
3167
3637
|
const versionColumn = this.versionColumn;
|
|
3168
3638
|
const expectedVersions = Array.from({ length: payloads.length });
|
|
3639
|
+
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
3169
3640
|
const items = payloads.map((p, i) => {
|
|
3170
|
-
const
|
|
3171
|
-
expectedVersions[i] = require_ops.separateCas(
|
|
3172
|
-
return this._applyDefaults(
|
|
3641
|
+
const c = clone(p);
|
|
3642
|
+
expectedVersions[i] = require_ops.separateCas(c, versionColumn);
|
|
3643
|
+
return this._applyDefaults(c);
|
|
3173
3644
|
});
|
|
3174
|
-
const originals = canNest ?
|
|
3645
|
+
const originals = canNest ? items.map((item) => ({ ...item })) : [];
|
|
3175
3646
|
const validator = this.getValidator("bulkReplace");
|
|
3176
3647
|
const ctx = {
|
|
3177
3648
|
mode: "replace",
|
|
@@ -3179,6 +3650,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3179
3650
|
};
|
|
3180
3651
|
this._applyDepthCtx(ctx, depth);
|
|
3181
3652
|
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3653
|
+
if (guard) {
|
|
3654
|
+
await guard(new WriteGuardContext(_action ?? "replaceMany", items, expectedVersions, this));
|
|
3655
|
+
require_nested_writer.validateBatch(validator, items, ctx);
|
|
3656
|
+
}
|
|
3182
3657
|
await this._encryptItems(items, "write");
|
|
3183
3658
|
const host = this;
|
|
3184
3659
|
if (canNest) await require_nested_writer.batchReplaceNestedTo(host, items, maxDepth, depth);
|
|
@@ -3209,7 +3684,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3209
3684
|
* Delegates to {@link bulkUpdate} for unified nested relation support.
|
|
3210
3685
|
*/
|
|
3211
3686
|
async updateOne(payload, opts) {
|
|
3212
|
-
return this.bulkUpdate([payload],
|
|
3687
|
+
return this.bulkUpdate([payload], {
|
|
3688
|
+
...opts,
|
|
3689
|
+
_action: "update"
|
|
3690
|
+
});
|
|
3213
3691
|
}
|
|
3214
3692
|
/**
|
|
3215
3693
|
* Partially updates multiple records with deep nested relation support.
|
|
@@ -3217,18 +3695,24 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3217
3695
|
* Only TO relations (1:1, N:1) are supported for patching. FROM/VIA
|
|
3218
3696
|
* relations will error — use {@link bulkReplace} for those.
|
|
3219
3697
|
* Recursive up to `maxDepth` (default 3).
|
|
3698
|
+
*
|
|
3699
|
+
* `opts.guard` (since 0.1.128) runs once inside the transaction, after
|
|
3700
|
+
* `$cas` extraction and validation, with the patches (identifying fields
|
|
3701
|
+
* present, `$cas` removed) — see {@link TWriteOptions}.
|
|
3220
3702
|
*/
|
|
3221
3703
|
async bulkUpdate(payloads, opts) {
|
|
3222
3704
|
this._ensureBuilt();
|
|
3223
|
-
const maxDepth = opts
|
|
3224
|
-
const
|
|
3705
|
+
const { _depth, _action, maxDepth: userMax, guard } = opts ?? {};
|
|
3706
|
+
const maxDepth = userMax ?? 3;
|
|
3707
|
+
const depth = _depth ?? 0;
|
|
3225
3708
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
3226
3709
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
3227
3710
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
3228
3711
|
const versionColumn = this.versionColumn;
|
|
3229
3712
|
const expectedVersions = Array.from({ length: payloads.length });
|
|
3713
|
+
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
3230
3714
|
const cloned = payloads.map((p, i) => {
|
|
3231
|
-
const c =
|
|
3715
|
+
const c = clone(p);
|
|
3232
3716
|
expectedVersions[i] = require_ops.separateCas(c, versionColumn);
|
|
3233
3717
|
return c;
|
|
3234
3718
|
});
|
|
@@ -3240,6 +3724,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3240
3724
|
};
|
|
3241
3725
|
this._applyDepthCtx(ctx, depth);
|
|
3242
3726
|
require_nested_writer.validateBatch(validator, cloned, ctx);
|
|
3727
|
+
if (guard) {
|
|
3728
|
+
await guard(new WriteGuardContext(_action ?? "updateMany", cloned, expectedVersions, this));
|
|
3729
|
+
require_nested_writer.validateBatch(validator, cloned, ctx);
|
|
3730
|
+
}
|
|
3243
3731
|
const originals = canNest ? cloned.map((p) => ({ ...p })) : [];
|
|
3244
3732
|
await this._encryptItems(cloned, "patch");
|
|
3245
3733
|
const host = this;
|
|
@@ -3255,13 +3743,16 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3255
3743
|
const filter = this._extractRecordFilter(data);
|
|
3256
3744
|
for (const key of Object.keys(filter)) delete data[key];
|
|
3257
3745
|
if (versionColumn !== void 0) assertNoVersionWrites(data, versionColumn);
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3746
|
+
const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3747
|
+
if (require_validator.isEmptyObject(data) && expectedVersion === void 0) {
|
|
3748
|
+
const exists = await this.adapter.count({
|
|
3749
|
+
filter: translatedFilter,
|
|
3750
|
+
controls: {}
|
|
3751
|
+
});
|
|
3752
|
+
matchedCount += exists > 0 ? 1 : 0;
|
|
3261
3753
|
continue;
|
|
3262
3754
|
}
|
|
3263
3755
|
let result;
|
|
3264
|
-
const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3265
3756
|
if (this.adapter.supportsNativePatch()) {
|
|
3266
3757
|
const ops = require_ops.separateFieldOps(data);
|
|
3267
3758
|
const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
|
|
@@ -3297,22 +3788,31 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3297
3788
|
*
|
|
3298
3789
|
* When the adapter does not support native foreign keys (e.g. MongoDB),
|
|
3299
3790
|
* cascade and setNull actions are applied before the delete.
|
|
3791
|
+
*
|
|
3792
|
+
* `opts.guard` (since 0.1.128) runs inside the transaction once the id has
|
|
3793
|
+
* resolved to a filter, before cascade / delete — see {@link TDeleteOptions}.
|
|
3794
|
+
* An id that resolves to no filter answers `{ deletedCount: 0 }` without
|
|
3795
|
+
* calling the guard.
|
|
3300
3796
|
*/
|
|
3301
|
-
async deleteOne(id) {
|
|
3797
|
+
async deleteOne(id, opts) {
|
|
3302
3798
|
this._ensureBuilt();
|
|
3303
3799
|
const filter = this._resolveIdFilter(id);
|
|
3304
3800
|
if (!filter) return { deletedCount: 0 };
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3801
|
+
const guard = opts?.guard;
|
|
3802
|
+
const needsCascade = this._integrity.needsCascade(this._cascadeResolver);
|
|
3803
|
+
const translated = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3804
|
+
const run = async () => {
|
|
3805
|
+
if (guard) await guard(new RemoveGuardContext(id, filter, this));
|
|
3806
|
+
if (needsCascade) await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
|
|
3807
|
+
return this.adapter.deleteOne(translated);
|
|
3808
|
+
};
|
|
3809
|
+
return require_nested_writer.remapDeleteFkViolation(this.tableName, () => guard || needsCascade ? this.adapter.withTransaction(run) : run());
|
|
3310
3810
|
}
|
|
3311
3811
|
async updateMany(filter, data) {
|
|
3312
3812
|
this._ensureBuilt();
|
|
3313
3813
|
this._guardMutationFilter(filter);
|
|
3314
3814
|
await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
|
|
3315
|
-
const dataCopy =
|
|
3815
|
+
const dataCopy = _cloneWritePayload(data);
|
|
3316
3816
|
const versionColumn = this.versionColumn;
|
|
3317
3817
|
if ("$cas" in dataCopy) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
3318
3818
|
path: "$cas",
|
|
@@ -3324,13 +3824,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3324
3824
|
const ops = require_ops.separateFieldOps(update);
|
|
3325
3825
|
const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
|
|
3326
3826
|
const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
|
|
3327
|
-
|
|
3827
|
+
const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
|
|
3828
|
+
if (translatedOps === void 0 && require_validator.isEmptyObject(translatedUpdate)) return {
|
|
3829
|
+
matchedCount: await this.adapter.count({
|
|
3830
|
+
filter: translatedFilter,
|
|
3831
|
+
controls: {}
|
|
3832
|
+
}),
|
|
3833
|
+
modifiedCount: 0
|
|
3834
|
+
};
|
|
3835
|
+
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.updateMany(translatedFilter, translatedUpdate, translatedOps));
|
|
3328
3836
|
}
|
|
3329
3837
|
async replaceMany(filter, data) {
|
|
3330
3838
|
this._ensureBuilt();
|
|
3331
3839
|
this._guardMutationFilter(filter);
|
|
3332
3840
|
await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver);
|
|
3333
|
-
const dataCopy =
|
|
3841
|
+
const dataCopy = _cloneWritePayload(data);
|
|
3334
3842
|
await this._encryptItems([dataCopy], "write");
|
|
3335
3843
|
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
3844
|
}
|
|
@@ -3360,6 +3868,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3360
3868
|
/** Engine-agnostic guard for user-supplied mutation filters (updateMany/deleteMany/…). */
|
|
3361
3869
|
_guardMutationFilter(filter) {
|
|
3362
3870
|
guardFilter(this._meta, this.adapter, filter);
|
|
3871
|
+
guardPaths(this._meta, this.adapter, { filter });
|
|
3363
3872
|
}
|
|
3364
3873
|
/**
|
|
3365
3874
|
* Encrypts `@db.encrypted` field values in place on (already validated)
|
|
@@ -3389,21 +3898,43 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3389
3898
|
}
|
|
3390
3899
|
}
|
|
3391
3900
|
/**
|
|
3392
|
-
*
|
|
3393
|
-
*
|
|
3394
|
-
*
|
|
3901
|
+
* Lazy pre-image read for a guard's `current(i)`: `null` when the row has
|
|
3902
|
+
* no identifying key (e.g. an auto-increment insert) or the key cannot be
|
|
3903
|
+
* resolved — never throws for a missing key.
|
|
3904
|
+
* @internal
|
|
3905
|
+
*/
|
|
3906
|
+
async _readPreImage(row) {
|
|
3907
|
+
if (!row) return null;
|
|
3908
|
+
let filter;
|
|
3909
|
+
try {
|
|
3910
|
+
filter = this._resolveIdFilter(row);
|
|
3911
|
+
} catch {
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
if (!filter) return null;
|
|
3915
|
+
return await this.findOne({
|
|
3916
|
+
filter,
|
|
3917
|
+
controls: {}
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Applies `@db.default` values in place to a row's absent fields — the
|
|
3922
|
+
* defaults pass every insert / replace path runs before validation.
|
|
3923
|
+
* Static value defaults (`@db.default 'x'`) are filled on EVERY adapter
|
|
3924
|
+
* (since 0.1.128 — writing the column's own default explicitly is
|
|
3925
|
+
* equivalent to leaving it to the DDL `DEFAULT`, and write guards see the
|
|
3926
|
+
* full row). Function defaults (`now` / `uuid` / `increment` / custom) the
|
|
3927
|
+
* adapter handles natively are NOT filled — the field stays absent so the
|
|
3928
|
+
* engine's own default applies. The version column is never touched.
|
|
3395
3929
|
*/
|
|
3396
3930
|
_applyDefaults(data) {
|
|
3397
|
-
const nativeValues = this.adapter.supportsNativeValueDefaults();
|
|
3398
3931
|
const nativeFns = this.adapter.nativeDefaultFns();
|
|
3399
3932
|
const versionField = this._meta.versionField;
|
|
3400
3933
|
for (const [field, def] of this._meta.defaults.entries()) {
|
|
3401
3934
|
if (field === versionField) continue;
|
|
3402
3935
|
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) {
|
|
3936
|
+
if (def.kind === "value") data[field] = this._parseValueDefault(field, def.value);
|
|
3937
|
+
else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
|
|
3407
3938
|
case "now":
|
|
3408
3939
|
data[field] = Date.now();
|
|
3409
3940
|
break;
|
|
@@ -3416,6 +3947,22 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3416
3947
|
return data;
|
|
3417
3948
|
}
|
|
3418
3949
|
/**
|
|
3950
|
+
* The JS value for a `@db.default 'literal'`: strings (including unions of
|
|
3951
|
+
* string literals) are used as-is, every other design type is parsed as
|
|
3952
|
+
* JSON — the same value the SQL adapters put into the DDL `DEFAULT` clause.
|
|
3953
|
+
* A literal that is not valid JSON falls back to the raw string so the
|
|
3954
|
+
* validator reports it against the field instead of a bare `SyntaxError`.
|
|
3955
|
+
*/
|
|
3956
|
+
_parseValueDefault(field, literal) {
|
|
3957
|
+
const fieldType = this._meta.flatMap?.get(field);
|
|
3958
|
+
if ((fieldType ? resolveDesignType(fieldType) : "string") === "string") return literal;
|
|
3959
|
+
try {
|
|
3960
|
+
return JSON.parse(literal);
|
|
3961
|
+
} catch {
|
|
3962
|
+
return literal;
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
/**
|
|
3419
3966
|
* Extracts a record-identifying filter from a payload.
|
|
3420
3967
|
*
|
|
3421
3968
|
* Resolution order:
|
|
@@ -3521,7 +4068,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3521
4068
|
mode: "insert",
|
|
3522
4069
|
navFields: this._meta.navFields
|
|
3523
4070
|
};
|
|
3524
|
-
require_nested_writer.validateBatch(validator, items.map((raw) => this._applyDefaults(
|
|
4071
|
+
require_nested_writer.validateBatch(validator, items.map((raw) => this._applyDefaults(_shallowPrunedClone(raw))), ctx);
|
|
3525
4072
|
await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver, false, opts?.excludeFkTargetTable);
|
|
3526
4073
|
}
|
|
3527
4074
|
/**
|
|
@@ -3535,22 +4082,6 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
3535
4082
|
const adapterPlugins = this.adapter.getValidatorPlugins();
|
|
3536
4083
|
if (purpose === "insert" || purpose === "patch" || purpose === "bulkReplace") {
|
|
3537
4084
|
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
4085
|
return require_validator.buildDbValidator(this.type, mode, adapterPlugins);
|
|
3555
4086
|
}
|
|
3556
4087
|
if (purpose === "bulkUpdate") {
|
|
@@ -3664,7 +4195,9 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
3664
4195
|
"db.agg.min",
|
|
3665
4196
|
"db.agg.max"
|
|
3666
4197
|
];
|
|
4198
|
+
const ignored = this.ignoredFields;
|
|
3667
4199
|
for (const [fieldName, fieldType] of this._type.type.props.entries()) {
|
|
4200
|
+
if (ignored.has(fieldName)) continue;
|
|
3668
4201
|
let aggFn;
|
|
3669
4202
|
let aggField;
|
|
3670
4203
|
for (const key of aggKeys) {
|
|
@@ -3700,6 +4233,18 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
3700
4233
|
return mappings;
|
|
3701
4234
|
}
|
|
3702
4235
|
};
|
|
4236
|
+
/**
|
|
4237
|
+
* Structural type guard for views: `true` when the readable reports
|
|
4238
|
+
* `isView`, whether or not it is an `AtscriptDbView` instance of THIS copy
|
|
4239
|
+
* of `@atscript/db`. Adapters must use this (or `readable.isView`) instead of
|
|
4240
|
+
* `instanceof AtscriptDbView` — in a bundle that carries two copies of the
|
|
4241
|
+
* core (app bundle + external adapter), `instanceof` is false and the adapter
|
|
4242
|
+
* would create an empty physical table under the view's name.
|
|
4243
|
+
* @since 0.1.128
|
|
4244
|
+
*/
|
|
4245
|
+
function isAtscriptDbView(readable) {
|
|
4246
|
+
return readable.isView;
|
|
4247
|
+
}
|
|
3703
4248
|
//#endregion
|
|
3704
4249
|
Object.defineProperty(exports, "ApplicationIntegrity", {
|
|
3705
4250
|
enumerable: true,
|
|
@@ -3791,6 +4336,24 @@ Object.defineProperty(exports, "assertNoVersionWrites", {
|
|
|
3791
4336
|
return assertNoVersionWrites;
|
|
3792
4337
|
}
|
|
3793
4338
|
});
|
|
4339
|
+
Object.defineProperty(exports, "checkHavingKeys", {
|
|
4340
|
+
enumerable: true,
|
|
4341
|
+
get: function() {
|
|
4342
|
+
return checkHavingKeys;
|
|
4343
|
+
}
|
|
4344
|
+
});
|
|
4345
|
+
Object.defineProperty(exports, "classifyQueryPath", {
|
|
4346
|
+
enumerable: true,
|
|
4347
|
+
get: function() {
|
|
4348
|
+
return classifyQueryPath;
|
|
4349
|
+
}
|
|
4350
|
+
});
|
|
4351
|
+
Object.defineProperty(exports, "collectQueryPaths", {
|
|
4352
|
+
enumerable: true,
|
|
4353
|
+
get: function() {
|
|
4354
|
+
return collectQueryPaths;
|
|
4355
|
+
}
|
|
4356
|
+
});
|
|
3794
4357
|
Object.defineProperty(exports, "createFailureCollector", {
|
|
3795
4358
|
enumerable: true,
|
|
3796
4359
|
get: function() {
|
|
@@ -3803,6 +4366,12 @@ Object.defineProperty(exports, "decomposePatch", {
|
|
|
3803
4366
|
return decomposePatch;
|
|
3804
4367
|
}
|
|
3805
4368
|
});
|
|
4369
|
+
Object.defineProperty(exports, "findAncestorInSet", {
|
|
4370
|
+
enumerable: true,
|
|
4371
|
+
get: function() {
|
|
4372
|
+
return findAncestorInSet;
|
|
4373
|
+
}
|
|
4374
|
+
});
|
|
3806
4375
|
Object.defineProperty(exports, "guardAggregate", {
|
|
3807
4376
|
enumerable: true,
|
|
3808
4377
|
get: function() {
|
|
@@ -3815,12 +4384,30 @@ Object.defineProperty(exports, "guardFilter", {
|
|
|
3815
4384
|
return guardFilter;
|
|
3816
4385
|
}
|
|
3817
4386
|
});
|
|
4387
|
+
Object.defineProperty(exports, "guardPath", {
|
|
4388
|
+
enumerable: true,
|
|
4389
|
+
get: function() {
|
|
4390
|
+
return guardPath;
|
|
4391
|
+
}
|
|
4392
|
+
});
|
|
4393
|
+
Object.defineProperty(exports, "guardPaths", {
|
|
4394
|
+
enumerable: true,
|
|
4395
|
+
get: function() {
|
|
4396
|
+
return guardPaths;
|
|
4397
|
+
}
|
|
4398
|
+
});
|
|
3818
4399
|
Object.defineProperty(exports, "guardQuery", {
|
|
3819
4400
|
enumerable: true,
|
|
3820
4401
|
get: function() {
|
|
3821
4402
|
return guardQuery;
|
|
3822
4403
|
}
|
|
3823
4404
|
});
|
|
4405
|
+
Object.defineProperty(exports, "isAtscriptDbView", {
|
|
4406
|
+
enumerable: true,
|
|
4407
|
+
get: function() {
|
|
4408
|
+
return isAtscriptDbView;
|
|
4409
|
+
}
|
|
4410
|
+
});
|
|
3824
4411
|
Object.defineProperty(exports, "isGeoIndexableType", {
|
|
3825
4412
|
enumerable: true,
|
|
3826
4413
|
get: function() {
|
|
@@ -3839,3 +4426,15 @@ Object.defineProperty(exports, "resolveDesignType", {
|
|
|
3839
4426
|
return resolveDesignType;
|
|
3840
4427
|
}
|
|
3841
4428
|
});
|
|
4429
|
+
Object.defineProperty(exports, "sortFieldNames", {
|
|
4430
|
+
enumerable: true,
|
|
4431
|
+
get: function() {
|
|
4432
|
+
return sortFieldNames;
|
|
4433
|
+
}
|
|
4434
|
+
});
|
|
4435
|
+
Object.defineProperty(exports, "unsupportedOperatorMessage", {
|
|
4436
|
+
enumerable: true,
|
|
4437
|
+
get: function() {
|
|
4438
|
+
return unsupportedOperatorMessage;
|
|
4439
|
+
}
|
|
4440
|
+
});
|