@atscript/moost-db 0.1.132 → 0.1.134
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/index.cjs +177 -81
- package/dist/index.d.cts +97 -40
- package/dist/index.d.mts +97 -40
- package/dist/index.mjs +178 -82
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -1155,16 +1155,12 @@ function resolveBoundReadable(ctor) {
|
|
|
1155
1155
|
}
|
|
1156
1156
|
//#endregion
|
|
1157
1157
|
//#region src/meta/field-capabilities.ts
|
|
1158
|
-
const
|
|
1159
|
-
const REASON_ADAPTER_FILTER = `${ADAPTER_FILTER}.`;
|
|
1158
|
+
const REASON_ADAPTER_FILTER = `${_atscript_db.ADAPTER_FILTER_REASON}.`;
|
|
1160
1159
|
const REASON_ADAPTER_SORT = "adapter cannot sort on this storage type.";
|
|
1161
1160
|
const REASON_WRITE_ONLY = "field is @db.writeOnly.";
|
|
1162
|
-
const REASON_ENCRYPTED =
|
|
1161
|
+
const REASON_ENCRYPTED = `${_atscript_db.ENCRYPTED_REASON}.`;
|
|
1163
1162
|
const REASON_ANNOTATION_FILTER = "add @db.column.filterable to enable.";
|
|
1164
1163
|
const REASON_ANNOTATION_SORT = "add @db.column.sortable to enable.";
|
|
1165
|
-
const REASON_NOT_TIMESTAMP = "not a timestamp field (declare it number.timestamp).";
|
|
1166
|
-
const REASON_NOT_DIMENSION = "not a dimension.";
|
|
1167
|
-
const REASON_NO_BUCKETS = "adapter has no calendar buckets.";
|
|
1168
1164
|
/** Sentence subject per op ("Filtering on field …"). */
|
|
1169
1165
|
const OP_SUBJECT = {
|
|
1170
1166
|
filter: "Filtering on",
|
|
@@ -1185,6 +1181,13 @@ const OP_VERB = {
|
|
|
1185
1181
|
aggregate: "aggregate over",
|
|
1186
1182
|
bucket: "bucket"
|
|
1187
1183
|
};
|
|
1184
|
+
/** The one "nonexistent path" verdict — hidden paths answer with it byte for byte. */
|
|
1185
|
+
function unknownField(path) {
|
|
1186
|
+
return {
|
|
1187
|
+
path,
|
|
1188
|
+
message: `Unknown field "${path}"`
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1188
1191
|
function leafHint(leaves) {
|
|
1189
1192
|
if (leaves.length === 0) return "no leaf fields";
|
|
1190
1193
|
const shown = leaves.slice(0, 5).join(", ");
|
|
@@ -1235,14 +1238,8 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1235
1238
|
_entries = /* @__PURE__ */ new Map();
|
|
1236
1239
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
1237
1240
|
_objectParents = /* @__PURE__ */ new Map();
|
|
1238
|
-
/**
|
|
1239
|
-
|
|
1240
|
-
* measures), else `undefined` — the core rule: a grouping source, a
|
|
1241
|
-
* bucketed field included, must then be a dimension.
|
|
1242
|
-
*/
|
|
1243
|
-
_dimensions;
|
|
1244
|
-
/** Paths of every JSON-value descriptor (`isJsonValueField`) — see `jsonValueAncestor`. */
|
|
1245
|
-
_jsonValueParents;
|
|
1241
|
+
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
1242
|
+
_bucketTable;
|
|
1246
1243
|
/** Listed leaves — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
1247
1244
|
get leaves() {
|
|
1248
1245
|
return this._entries;
|
|
@@ -1259,7 +1256,6 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1259
1256
|
this.signature = FieldCapabilityIndex.adapterSignature(source);
|
|
1260
1257
|
const units = source.calendarBucketUnits();
|
|
1261
1258
|
this.bucketUnits = _uniqu_core.BUCKET_UNITS.filter((unit) => units.has(unit));
|
|
1262
|
-
this._dimensions = source.dimensions.length > 0 || source.measures.length > 0 ? new Set(source.dimensions) : void 0;
|
|
1263
1259
|
const physicalNames = /* @__PURE__ */ new Set();
|
|
1264
1260
|
const jsonValueParents = /* @__PURE__ */ new Set();
|
|
1265
1261
|
for (const fd of source.fieldDescriptors) {
|
|
@@ -1267,7 +1263,11 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1267
1263
|
if ((0, _atscript_db.isJsonValueField)(fd)) jsonValueParents.add(fd.path);
|
|
1268
1264
|
}
|
|
1269
1265
|
this.physicalNames = physicalNames;
|
|
1270
|
-
this.
|
|
1266
|
+
this._bucketTable = {
|
|
1267
|
+
jsonValueParents,
|
|
1268
|
+
dimensions: source.dimensions,
|
|
1269
|
+
measures: source.measures
|
|
1270
|
+
};
|
|
1271
1271
|
const nav = new Set(source.navFields);
|
|
1272
1272
|
if (nav.size === 0) for (const name of source.relations.keys()) nav.add(name);
|
|
1273
1273
|
this.navFields = nav;
|
|
@@ -1324,18 +1324,14 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1324
1324
|
geo: verdict("geo", true)
|
|
1325
1325
|
};
|
|
1326
1326
|
const filterOps = filterBy.compare === REASON_ADAPTER_FILTER && !filterPolicyBlocked ? (0, _atscript_db.narrowerFilterOps)(fd, source) : [];
|
|
1327
|
-
if (filterOps.length > 0) filterBy.compare = `${
|
|
1327
|
+
if (filterOps.length > 0) filterBy.compare = `${_atscript_db.ADAPTER_FILTER_REASON}${(0, _atscript_db.acceptedOperatorsHint)(filterOps)}.`;
|
|
1328
1328
|
const physicalReason = verdict("compare", false);
|
|
1329
1329
|
let sortReason = source.canSortField(fd) ? void 0 : REASON_ADAPTER_SORT;
|
|
1330
1330
|
if (fd.encrypted) sortReason = REASON_ENCRYPTED;
|
|
1331
1331
|
if (isWriteOnly) sortReason = REASON_WRITE_ONLY;
|
|
1332
1332
|
if (!sortReason && this.sortableManual && !annotated(fd, "db.column.sortable")) sortReason = REASON_ANNOTATION_SORT;
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
if (!bucketReason && jsonAncestor !== void 0) bucketReason = `inside JSON-stored column "${jsonAncestor}".`;
|
|
1336
|
-
if (!bucketReason && !(0, _atscript_db.isBucketableField)(fd)) bucketReason = REASON_NOT_TIMESTAMP;
|
|
1337
|
-
if (!bucketReason && this._dimensions && !this._dimensions.has(fd.path)) bucketReason = REASON_NOT_DIMENSION;
|
|
1338
|
-
if (!bucketReason && this.bucketUnits.length === 0) bucketReason = REASON_NO_BUCKETS;
|
|
1333
|
+
const bucket = isWriteOnly ? void 0 : (0, _atscript_db.bucketSourceVerdict)(fd, this._bucketTable, source);
|
|
1334
|
+
const bucketReason = !bucket ? REASON_WRITE_ONLY : bucket.ok ? void 0 : `${bucket.reason}.`;
|
|
1339
1335
|
const cap = {
|
|
1340
1336
|
filterable: filterBy.compare === void 0,
|
|
1341
1337
|
sortable: sortReason === void 0,
|
|
@@ -1369,21 +1365,29 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1369
1365
|
}
|
|
1370
1366
|
/**
|
|
1371
1367
|
* Gate check for one path in one position. Returns `undefined` when the
|
|
1372
|
-
* path is accepted.
|
|
1373
|
-
* the target table but is never a column here), then a listed leaf's
|
|
1374
|
-
* capability (no existence lookup needed — every listed leaf is a real
|
|
1375
|
-
* field), then `exists` (the readable's `isValidFieldPath`) and, for paths
|
|
1376
|
-
* that exist but are not leaves, the storage classification.
|
|
1368
|
+
* path is accepted.
|
|
1377
1369
|
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1370
|
+
* `exists` runs FIRST, for every path (since 0.1.133): it is the
|
|
1371
|
+
* controller's `hasField`, the visibility hook subclasses narrow per
|
|
1372
|
+
* request (e.g. a projection-scoped viewer). A path it rejects answers
|
|
1373
|
+
* `Unknown field "x"` exactly like a nonexistent one — never a capability
|
|
1374
|
+
* or navigation hint, which would reveal the field and let a filter or
|
|
1375
|
+
* sort on it act as a value oracle. Before 0.1.133 listed leaves and
|
|
1376
|
+
* navigation paths skipped it.
|
|
1377
|
+
*
|
|
1378
|
+
* Then: navigation paths (a nav path exists on the target table but is
|
|
1379
|
+
* never a column here), a listed leaf's capability, and for other paths
|
|
1380
|
+
* the storage classification. Existence also runs BEFORE the JSON /
|
|
1381
|
+
* encrypted classification: an untyped descendant of a JSON column
|
|
1382
|
+
* (`address.nope`) is reported as `Unknown field`, not as "inside
|
|
1383
|
+
* JSON-stored column" — clients pin that wording, so do not "align" it
|
|
1384
|
+
* with the core backstop's text.
|
|
1382
1385
|
*
|
|
1383
1386
|
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
1384
1387
|
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
1385
1388
|
*/
|
|
1386
1389
|
check(path, op, exists, predicate = "compare") {
|
|
1390
|
+
if (!exists(path)) return unknownField(path);
|
|
1387
1391
|
const { kind, parent } = (0, _atscript_db.classifyQueryPath)(this, path);
|
|
1388
1392
|
if (kind === "nav") {
|
|
1389
1393
|
if (parent === void 0) return {
|
|
@@ -1423,17 +1427,17 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1423
1427
|
};
|
|
1424
1428
|
}
|
|
1425
1429
|
}
|
|
1426
|
-
if (!exists(path)) return {
|
|
1427
|
-
path,
|
|
1428
|
-
message: `Unknown field "${path}"`
|
|
1429
|
-
};
|
|
1430
1430
|
switch (kind) {
|
|
1431
|
-
case "objectParent":
|
|
1431
|
+
case "objectParent": {
|
|
1432
1432
|
if (op === "select") return void 0;
|
|
1433
|
+
const all = this._objectParents.get(path);
|
|
1434
|
+
const leaves = all.filter(exists);
|
|
1435
|
+
if (leaves.length === 0 && all.length > 0) return unknownField(path);
|
|
1433
1436
|
return {
|
|
1434
1437
|
path,
|
|
1435
|
-
message: `"${path}" is a nested object — filter or sort on one of its leaves (${leafHint(
|
|
1438
|
+
message: `"${path}" is a nested object — filter or sort on one of its leaves (${leafHint(leaves)})`
|
|
1436
1439
|
};
|
|
1440
|
+
}
|
|
1437
1441
|
case "jsonDescendant": return {
|
|
1438
1442
|
path,
|
|
1439
1443
|
message: `"${path}" is inside JSON-stored column "${parent}" — this adapter cannot ${OP_VERB[op]} JSON paths; select "${parent}" and read the value client-side.`
|
|
@@ -1445,10 +1449,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1445
1449
|
path,
|
|
1446
1450
|
message: `Cannot ${OP_VERB[op]} encrypted field "${path}"`
|
|
1447
1451
|
};
|
|
1448
|
-
default: return
|
|
1449
|
-
path,
|
|
1450
|
-
message: `Unknown field "${path}"`
|
|
1451
|
-
};
|
|
1452
|
+
default: return unknownField(path);
|
|
1452
1453
|
}
|
|
1453
1454
|
}
|
|
1454
1455
|
};
|
|
@@ -1490,6 +1491,7 @@ const GEO_CONTROLS = [
|
|
|
1490
1491
|
];
|
|
1491
1492
|
//#endregion
|
|
1492
1493
|
//#region src/as-db-readable.controller.ts
|
|
1494
|
+
var _AsDbReadableController;
|
|
1493
1495
|
/** Gate positions checked after the filter entries, in order; `refs[op]` are their paths. */
|
|
1494
1496
|
const PATH_OPS = [
|
|
1495
1497
|
"sort",
|
|
@@ -1499,7 +1501,7 @@ const PATH_OPS = [
|
|
|
1499
1501
|
"aggregate",
|
|
1500
1502
|
"bucket"
|
|
1501
1503
|
];
|
|
1502
|
-
let AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
1504
|
+
let AsDbReadableController = _AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
1503
1505
|
/** Reference to the underlying readable (table or view). */
|
|
1504
1506
|
readable;
|
|
1505
1507
|
/**
|
|
@@ -1537,8 +1539,17 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1537
1539
|
metaCacheKey() {
|
|
1538
1540
|
return this.capabilities;
|
|
1539
1541
|
}
|
|
1540
|
-
/** Bound once: the
|
|
1542
|
+
/** Bound once: the visibility check ({@link hasField}) the gate hands to `capabilities.check`. */
|
|
1541
1543
|
_exists = (path) => this.hasField(path);
|
|
1544
|
+
/**
|
|
1545
|
+
* Id-resolution options (since 0.1.134): `{ isFieldVisible: hasField }`
|
|
1546
|
+
* when a subclass overrides {@link hasField}, else `undefined` (the default
|
|
1547
|
+
* accepts every real path, so resolution stays unfiltered). A unique index
|
|
1548
|
+
* over a hidden field is never an identification.
|
|
1549
|
+
*/
|
|
1550
|
+
_idOpts;
|
|
1551
|
+
/** Narrowed id sources, one stable object per distinct visible-identification set. */
|
|
1552
|
+
_idSources = /* @__PURE__ */ new Map();
|
|
1542
1553
|
_preferredIdSet;
|
|
1543
1554
|
_overlayIsNoOp;
|
|
1544
1555
|
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
@@ -1564,6 +1575,29 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1564
1575
|
this._quantityRefByPath = this._collectQuantityRefs();
|
|
1565
1576
|
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
1566
1577
|
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
1578
|
+
this._idOpts = this.hasField === _AsDbReadableController.prototype.hasField ? void 0 : { isFieldVisible: this._exists };
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* The identifications this request may address rows through (since
|
|
1582
|
+
* 0.1.134): the readable's own, minus unique indexes over fields
|
|
1583
|
+
* {@link hasField} hides. Used by `/one?…`, `DELETE /?…` and action `ids`.
|
|
1584
|
+
* Stable per distinct outcome, so per-source caches keyed on it hit.
|
|
1585
|
+
*/
|
|
1586
|
+
get idSource() {
|
|
1587
|
+
const opts = this._idOpts;
|
|
1588
|
+
if (!opts) return this.readable;
|
|
1589
|
+
const visible = this.readable.identificationsVisibleTo(opts.isFieldVisible);
|
|
1590
|
+
if (visible.length === this.readable.identifications.length) return this.readable;
|
|
1591
|
+
const key = visible.map((ident) => ident.source).join("");
|
|
1592
|
+
let source = this._idSources.get(key);
|
|
1593
|
+
if (!source) {
|
|
1594
|
+
source = {
|
|
1595
|
+
identifications: visible,
|
|
1596
|
+
fieldDescriptors: this.readable.fieldDescriptors
|
|
1597
|
+
};
|
|
1598
|
+
this._idSources.set(key, source);
|
|
1599
|
+
}
|
|
1600
|
+
return source;
|
|
1567
1601
|
}
|
|
1568
1602
|
_collectInvertibleFields() {
|
|
1569
1603
|
const out = [];
|
|
@@ -1590,6 +1624,24 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1590
1624
|
for (const [path, entry] of this.readable.flatMap) if (entry?.metadata?.has?.(annotation)) out.add(path);
|
|
1591
1625
|
return out;
|
|
1592
1626
|
}
|
|
1627
|
+
/**
|
|
1628
|
+
* THE field-visibility hook: every gated path consults it before any
|
|
1629
|
+
* capability check (since 0.1.133) — filter keys (inside `$and` / `$or` /
|
|
1630
|
+
* `$not`, existence predicates included), `$sort`, `$select`,
|
|
1631
|
+
* `$groupBy`, `$having` keys, aggregate and calendar-bucket `$field`s,
|
|
1632
|
+
* `$with` relation names and sub-query paths, and the `$search` fallback
|
|
1633
|
+
* fields. A path it rejects is answered exactly like a nonexistent one
|
|
1634
|
+
* (`Unknown field "x"` / `Unknown relation "x"`), so override it to hide
|
|
1635
|
+
* fields per request (read scopes). Since 0.1.134 it also governs row
|
|
1636
|
+
* identification — a unique index over a hidden field is not an
|
|
1637
|
+
* identification for `/one/:id`, `/one?…`, `DELETE`, a PK-less `PATCH` or
|
|
1638
|
+
* an action id (primary key and `preferredId` always are) — and the
|
|
1639
|
+
* nested-object 400 hint lists visible leaves only. The default accepts every real path
|
|
1640
|
+
* (`isValidFieldPath`). `/meta` does NOT consult it — prune hidden fields
|
|
1641
|
+
* there with `applyMetaOverlay`. Native text search and vector search
|
|
1642
|
+
* (`$vector` names an index) run inside the engine over its indexes, out of
|
|
1643
|
+
* this hook's reach — keep hidden fields out of those indexes.
|
|
1644
|
+
*/
|
|
1593
1645
|
hasField(path) {
|
|
1594
1646
|
if (typeof this.readable.isValidFieldPath === "function") return this.readable.isValidFieldPath(path);
|
|
1595
1647
|
return this.readable.flatMap.has(path);
|
|
@@ -1677,7 +1729,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1677
1729
|
const withRelations = parsed.controls.$with;
|
|
1678
1730
|
if (withRelations?.length) {
|
|
1679
1731
|
const relations = this.readable.relations;
|
|
1680
|
-
for (const rel of withRelations)
|
|
1732
|
+
for (const rel of withRelations) {
|
|
1733
|
+
const dot = rel.name.indexOf(".");
|
|
1734
|
+
if (!(dot === -1 ? relations.has(rel.name) && this.hasField(rel.name) : this.hasField(rel.name.slice(0, dot)))) {
|
|
1735
|
+
const visible = [...relations.keys()].filter((name) => this.hasField(name));
|
|
1736
|
+
return badRequest("$with", `Unknown relation "${rel.name}"`, `Unknown relation "${rel.name}" in $with. Available relations: ${visible.join(", ") || "(none)"}`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1681
1739
|
}
|
|
1682
1740
|
}
|
|
1683
1741
|
/**
|
|
@@ -1752,11 +1810,26 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1752
1810
|
return widened;
|
|
1753
1811
|
}
|
|
1754
1812
|
const widened = {};
|
|
1755
|
-
for (const path of this.
|
|
1813
|
+
for (const path of this._invertExclusion(excluded)) widened[path] = 1;
|
|
1756
1814
|
for (const field of this._preferredIdSet) widened[field] = 1;
|
|
1757
1815
|
return widened;
|
|
1758
1816
|
}
|
|
1759
1817
|
/**
|
|
1818
|
+
* The logical paths an exclusion keeps. A path goes when it, an ancestor
|
|
1819
|
+
* or a descendant is excluded: excluding an object parent excludes its
|
|
1820
|
+
* whole subtree, and a kept parent would carry an excluded child back
|
|
1821
|
+
* (its other leaves stay listed on their own). Before 0.1.134 only the
|
|
1822
|
+
* exact paths were dropped, so `$select=-a` still returned `a`'s leaves.
|
|
1823
|
+
*/
|
|
1824
|
+
_invertExclusion(excluded) {
|
|
1825
|
+
return this._invertibleFields.filter((path) => {
|
|
1826
|
+
if (excluded.has(path) || (0, _atscript_db.findAncestorInSet)(path, excluded) !== void 0) return false;
|
|
1827
|
+
const prefix = `${path}.`;
|
|
1828
|
+
for (const key of excluded) if (key.startsWith(prefix)) return false;
|
|
1829
|
+
return true;
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1760
1833
|
* Auto-includes the sibling-ref field whenever its `@db.amount.currency.ref`
|
|
1761
1834
|
* / `@db.unit.ref` quantity is selected — UI must never get a value without
|
|
1762
1835
|
* its dimension. No-op when `$select` is undefined (full row covers it).
|
|
@@ -1819,10 +1892,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1819
1892
|
for (const [k, v] of Object.entries(obj)) if (v === 1 || v === true) included.push(k);
|
|
1820
1893
|
else if (v === 0 || v === false) excluded.push(k);
|
|
1821
1894
|
if (included.length > 0 && excluded.length === 0) return included;
|
|
1822
|
-
if (excluded.length > 0 && included.length === 0)
|
|
1823
|
-
const excludedSet = new Set(excluded);
|
|
1824
|
-
return this._invertibleFields.filter((path) => !excludedSet.has(path));
|
|
1825
|
-
}
|
|
1895
|
+
if (excluded.length > 0 && included.length === 0) return this._invertExclusion(new Set(excluded));
|
|
1826
1896
|
throw new _moostjs_event_http.HttpError(500, "[moost-db] mixed inclusion/exclusion projection reached augmenter; widenPreferredIdProjection should have rejected it");
|
|
1827
1897
|
}
|
|
1828
1898
|
/** WHY: filter row/rows envelopes by the per-request `applyMetaOverlay` action set; skip `meta()` when overlay is identity. */
|
|
@@ -1914,10 +1984,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1914
1984
|
/** First `@db.writeOnly` field referenced by `$groupBy` / aggregate `$select`, or undefined. */
|
|
1915
1985
|
_findWriteOnlyInAggregate(groupBy, select) {
|
|
1916
1986
|
if (this._writeOnlySet.size === 0) return void 0;
|
|
1917
|
-
|
|
1987
|
+
const sealed = (f) => this._writeOnlySet.has(f) && this.hasField(f);
|
|
1988
|
+
for (const f of groupBy) if (sealed(f)) return f;
|
|
1918
1989
|
if (Array.isArray(select)) for (const item of select) {
|
|
1919
1990
|
const field = typeof item === "string" ? item : item.$field;
|
|
1920
|
-
if (field &&
|
|
1991
|
+
if (field && sealed(field)) return field;
|
|
1921
1992
|
}
|
|
1922
1993
|
}
|
|
1923
1994
|
/**
|
|
@@ -1929,9 +2000,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1929
2000
|
applySearchFallback(filter, controls) {
|
|
1930
2001
|
const term = controls.$search;
|
|
1931
2002
|
if (!term || controls.$vector !== void 0) return filter;
|
|
1932
|
-
if (this.readable.isSearchable()
|
|
2003
|
+
if (this.readable.isSearchable()) return filter;
|
|
2004
|
+
const fields = this._searchFallbackFields.filter((f) => this.hasField(f));
|
|
2005
|
+
if (fields.length === 0) return filter;
|
|
1933
2006
|
const rx = `/${term.replace(/[.*+?^${}()|[\]\\/]/g, String.raw`\$&`)}/i`;
|
|
1934
|
-
const fragment = { $or:
|
|
2007
|
+
const fragment = { $or: fields.map((f) => ({ [f]: { $regex: rx } })) };
|
|
1935
2008
|
return filter && Object.keys(filter).length > 0 ? { $and: [filter, fragment] } : fragment;
|
|
1936
2009
|
}
|
|
1937
2010
|
/**
|
|
@@ -2000,9 +2073,14 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
2000
2073
|
});
|
|
2001
2074
|
return result;
|
|
2002
2075
|
}
|
|
2003
|
-
/**
|
|
2076
|
+
/**
|
|
2077
|
+
* Pick the first identification (PK or unique index) whose fields are all
|
|
2078
|
+
* present in the query. A unique index over a field {@link hasField} hides
|
|
2079
|
+
* is not a candidate (since 0.1.134) — `?hidden=x` answers exactly like
|
|
2080
|
+
* `?nope=x`, so it cannot probe whether a row with that value exists.
|
|
2081
|
+
*/
|
|
2004
2082
|
extractIdShape(query) {
|
|
2005
|
-
for (const id of this.
|
|
2083
|
+
for (const id of this.idSource.identifications) {
|
|
2006
2084
|
const idObj = {};
|
|
2007
2085
|
let allPresent = true;
|
|
2008
2086
|
for (const field of id.fields) {
|
|
@@ -2239,7 +2317,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
2239
2317
|
...parsedControls,
|
|
2240
2318
|
$select: initialSelect
|
|
2241
2319
|
};
|
|
2242
|
-
const idFilter = this.readable.resolveIdFilter(id);
|
|
2320
|
+
const idFilter = this.readable.resolveIdFilter(id, this._idOpts);
|
|
2243
2321
|
let row = null;
|
|
2244
2322
|
if (idFilter) {
|
|
2245
2323
|
const overlay = await this.transformOne({});
|
|
@@ -2362,7 +2440,7 @@ __decorate([
|
|
|
2362
2440
|
__decorateMetadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record, String]),
|
|
2363
2441
|
__decorateMetadata("design:returntype", Promise)
|
|
2364
2442
|
], AsDbReadableController.prototype, "getOneComposite", null);
|
|
2365
|
-
AsDbReadableController = __decorate([
|
|
2443
|
+
AsDbReadableController = _AsDbReadableController = __decorate([
|
|
2366
2444
|
(0, moost.Inherit)(),
|
|
2367
2445
|
__decorateParam(1, (0, moost.Inject)(READABLE_DEF)),
|
|
2368
2446
|
__decorateParam(1, (0, moost.Optional)()),
|
|
@@ -2394,6 +2472,9 @@ function hasWriteShape(data, many) {
|
|
|
2394
2472
|
let AsDbController = _AsDbController = class AsDbController extends AsDbReadableController {
|
|
2395
2473
|
constructor(app, table) {
|
|
2396
2474
|
super(app, table);
|
|
2475
|
+
const proto = _AsDbController.prototype;
|
|
2476
|
+
this._writeArgs = this._hookArgs(this.guardWrite !== proto.guardWrite ? (ctx) => this.guardWrite(ctx) : void 0);
|
|
2477
|
+
this._removeArgs = this._hookArgs(this.guardRemove !== proto.guardRemove ? (ctx) => this.guardRemove(ctx) : void 0);
|
|
2397
2478
|
}
|
|
2398
2479
|
buildCrud() {
|
|
2399
2480
|
return {
|
|
@@ -2461,19 +2542,20 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
2461
2542
|
withTransaction(fn) {
|
|
2462
2543
|
return this.table.getAdapter().withTransaction(fn);
|
|
2463
2544
|
}
|
|
2545
|
+
/** The table write call's trailing options — see {@link _hookArgs}. */
|
|
2546
|
+
_writeArgs;
|
|
2547
|
+
/** `deleteOne`'s trailing options — see {@link _hookArgs}. */
|
|
2548
|
+
_removeArgs;
|
|
2464
2549
|
/**
|
|
2465
|
-
*
|
|
2466
|
-
*
|
|
2467
|
-
*
|
|
2550
|
+
* A table call's trailing options, built once: `guard` only when the guard
|
|
2551
|
+
* hook is overridden, `isFieldVisible` only when `hasField` is (an id or a
|
|
2552
|
+
* PK-less payload never resolves through a hidden unique key) — else
|
|
2553
|
+
* nothing, so an unmodified controller calls the table exactly as before.
|
|
2468
2554
|
*/
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
/** `deleteOne`'s trailing options: `[{ guard }]` only when `guardRemove` is overridden. */
|
|
2474
|
-
_removeArgs() {
|
|
2475
|
-
if (this.guardRemove === _AsDbController.prototype.guardRemove) return [];
|
|
2476
|
-
return [{ guard: (ctx) => this.guardRemove(ctx) }];
|
|
2555
|
+
_hookArgs(guard) {
|
|
2556
|
+
const opts = { ...this._idOpts };
|
|
2557
|
+
if (guard) opts.guard = guard;
|
|
2558
|
+
return Object.keys(opts).length > 0 ? [opts] : [];
|
|
2477
2559
|
}
|
|
2478
2560
|
/** Resolves a hook result: `undefined` aborts with `abortMessage`, an `Error` is thrown, anything else passes. */
|
|
2479
2561
|
async _checkHook(pending, abortMessage) {
|
|
@@ -2523,7 +2605,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
2523
2605
|
}
|
|
2524
2606
|
/** Deletes by id (guard forwarded when overridden) and maps "nothing deleted" to 404. */
|
|
2525
2607
|
async _deleteOrThrow(id) {
|
|
2526
|
-
const result = await this.table.deleteOne(id, ...this._removeArgs
|
|
2608
|
+
const result = await this.table.deleteOne(id, ...this._removeArgs);
|
|
2527
2609
|
if (result.deletedCount < 1) throw new _moostjs_event_http.HttpError(404);
|
|
2528
2610
|
return result;
|
|
2529
2611
|
}
|
|
@@ -2534,10 +2616,10 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
2534
2616
|
assertWriteShape(payload);
|
|
2535
2617
|
if (Array.isArray(payload)) {
|
|
2536
2618
|
const rows = await this._writeBody("insertMany", payload, true);
|
|
2537
|
-
return this.table.insertMany(rows, ...this._writeArgs
|
|
2619
|
+
return this.table.insertMany(rows, ...this._writeArgs);
|
|
2538
2620
|
}
|
|
2539
2621
|
const row = await this._writeBody("insert", payload, false);
|
|
2540
|
-
return this.table.insertOne(row, ...this._writeArgs
|
|
2622
|
+
return this.table.insertOne(row, ...this._writeArgs);
|
|
2541
2623
|
}
|
|
2542
2624
|
/**
|
|
2543
2625
|
* **PUT /** — fully replaces one or many records matched by primary key.
|
|
@@ -2554,11 +2636,11 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
2554
2636
|
if (Array.isArray(payload)) {
|
|
2555
2637
|
const rows = await this._writeBody("replaceMany", payload, true);
|
|
2556
2638
|
this._resolveBulkCas(rows, versionColumn);
|
|
2557
|
-
return this.table.bulkReplace(rows, ...this._writeArgs
|
|
2639
|
+
return this.table.bulkReplace(rows, ...this._writeArgs);
|
|
2558
2640
|
}
|
|
2559
2641
|
const row = await this._writeBody("replace", payload, false);
|
|
2560
2642
|
const hadCas = this._resolveCas(row, versionColumn);
|
|
2561
|
-
const result = await this.table.replaceOne(row, ...this._writeArgs
|
|
2643
|
+
const result = await this.table.replaceOne(row, ...this._writeArgs);
|
|
2562
2644
|
if (hadCas && result.matchedCount === 0) throw await this._disambiguateMismatch(row, versionColumn);
|
|
2563
2645
|
return result;
|
|
2564
2646
|
}
|
|
@@ -2575,11 +2657,11 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
2575
2657
|
if (Array.isArray(payload)) {
|
|
2576
2658
|
const rows = await this._writeBody("updateMany", payload, true);
|
|
2577
2659
|
this._resolveBulkCas(rows, versionColumn);
|
|
2578
|
-
return this.table.bulkUpdate(rows, ...this._writeArgs
|
|
2660
|
+
return this.table.bulkUpdate(rows, ...this._writeArgs);
|
|
2579
2661
|
}
|
|
2580
2662
|
const row = await this._writeBody("update", payload, false);
|
|
2581
2663
|
const hadCas = this._resolveCas(row, versionColumn);
|
|
2582
|
-
const result = await this.table.updateOne(row, ...this._writeArgs
|
|
2664
|
+
const result = await this.table.updateOne(row, ...this._writeArgs);
|
|
2583
2665
|
if (hadCas && result.matchedCount === 0) throw await this._disambiguateMismatch(row, versionColumn);
|
|
2584
2666
|
return result;
|
|
2585
2667
|
}
|
|
@@ -2590,7 +2672,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
2590
2672
|
* but the supplied version is stale (§6.3). Callers throw the result.
|
|
2591
2673
|
*/
|
|
2592
2674
|
async _disambiguateMismatch(data, versionColumn) {
|
|
2593
|
-
const filter = this.table.resolveIdFilter(data);
|
|
2675
|
+
const filter = this.table.resolveIdFilter(data, this._idOpts);
|
|
2594
2676
|
const row = filter ? await this.table.findOne({
|
|
2595
2677
|
filter,
|
|
2596
2678
|
controls: {}
|
|
@@ -3139,8 +3221,11 @@ function isPlainObject(value) {
|
|
|
3139
3221
|
//#endregion
|
|
3140
3222
|
//#region src/actions/id-cache.ts
|
|
3141
3223
|
const boundTableKey = (0, _wooksjs_event_core.key)("atscript_db_action_bound_table");
|
|
3224
|
+
function controllerOf(ctx) {
|
|
3225
|
+
return (0, moost.useControllerContext)(ctx).getController();
|
|
3226
|
+
}
|
|
3142
3227
|
function controllerTable(ctx) {
|
|
3143
|
-
const ctrl = (
|
|
3228
|
+
const ctrl = controllerOf(ctx);
|
|
3144
3229
|
return ctrl?.readable ?? ctrl?.table ?? null;
|
|
3145
3230
|
}
|
|
3146
3231
|
function getActionTable(ctx) {
|
|
@@ -3161,11 +3246,22 @@ function noTableError(ctx) {
|
|
|
3161
3246
|
code: "ACTION_TABLE_NOT_BOUND"
|
|
3162
3247
|
});
|
|
3163
3248
|
}
|
|
3249
|
+
/**
|
|
3250
|
+
* Validates the body's `ids` against the action table's identifications. For
|
|
3251
|
+
* the controller's own table that is its `idSource` (since 0.1.134): a unique
|
|
3252
|
+
* index over a field `hasField` hides neither addresses a row nor appears in
|
|
3253
|
+
* the "must exactly match one of" message. An `opts.table` binding has no
|
|
3254
|
+
* visibility hook.
|
|
3255
|
+
*/
|
|
3164
3256
|
async function resolveValidatedId(ctx, validate) {
|
|
3165
|
-
|
|
3166
|
-
if (!
|
|
3257
|
+
let source = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
|
|
3258
|
+
if (!source) {
|
|
3259
|
+
const ctrl = controllerOf(ctx);
|
|
3260
|
+
source = ctrl?.idSource ?? ctrl?.readable ?? ctrl?.table ?? null;
|
|
3261
|
+
}
|
|
3262
|
+
if (!isIdValidationSource(source)) throw noTableError(ctx);
|
|
3167
3263
|
const env = await ctx.get(dbActionBodySlot);
|
|
3168
|
-
validate(env.ids,
|
|
3264
|
+
validate(env.ids, source);
|
|
3169
3265
|
return env.ids;
|
|
3170
3266
|
}
|
|
3171
3267
|
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
|