@atscript/moost-db 0.1.147 → 0.1.148
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-space-registry-C_wft7kl.mjs → db-space-registry-Cdb0FGCn.mjs} +28 -2
- package/dist/{db-space-registry-CKR6G-hi.d.mts → db-space-registry-Cvd8s9ql.d.mts} +17 -1
- package/dist/{db-space-registry-DqKr5Zdk.cjs → db-space-registry-D5gnC_-6.cjs} +32 -0
- package/dist/{db-space-registry-CWpYwZ4R.d.cts → db-space-registry-fXB8LnGe.d.cts} +17 -1
- package/dist/index.cjs +1248 -233
- package/dist/index.d.cts +373 -48
- package/dist/index.d.mts +373 -48
- package/dist/index.mjs +1246 -233
- package/dist/testing.cjs +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +16 -14
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_db_space_registry = require("./db-space-registry-
|
|
2
|
+
const require_db_space_registry = require("./db-space-registry-D5gnC_-6.cjs");
|
|
3
3
|
let _atscript_typescript_utils = require("@atscript/typescript/utils");
|
|
4
4
|
let _moostjs_event_http = require("@moostjs/event-http");
|
|
5
5
|
let moost = require("moost");
|
|
@@ -77,6 +77,7 @@ const dbErrorCodeToStatus = {
|
|
|
77
77
|
CONFLICT: 409,
|
|
78
78
|
CAS_MISMATCH: 409,
|
|
79
79
|
TX_WAIT_TIMEOUT: 503,
|
|
80
|
+
SPACE_CLOSED: 503,
|
|
80
81
|
BUCKET_TZ_UNAVAILABLE: 501
|
|
81
82
|
};
|
|
82
83
|
function transformValidationError(error, reply) {
|
|
@@ -515,6 +516,38 @@ function identityKey(id) {
|
|
|
515
516
|
return idKey(id, Object.keys(id).toSorted());
|
|
516
517
|
}
|
|
517
518
|
/**
|
|
519
|
+
* `resolved` (index-aligned with `requested`) with duplicate identities
|
|
520
|
+
* collapsed to the first, plus the per-request list ({@link TAppliedIds}).
|
|
521
|
+
*/
|
|
522
|
+
function applyResolvedIds(requested, resolved) {
|
|
523
|
+
const ids = [];
|
|
524
|
+
const requests = [];
|
|
525
|
+
const seen = /* @__PURE__ */ new Set();
|
|
526
|
+
let changed = false;
|
|
527
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
528
|
+
const k = identityKey(resolved[i]);
|
|
529
|
+
if (k === void 0) {
|
|
530
|
+
ids.push(resolved[i]);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (k !== identityKey(requested[i])) changed = true;
|
|
534
|
+
requests.push({
|
|
535
|
+
id: requested[i],
|
|
536
|
+
key: k
|
|
537
|
+
});
|
|
538
|
+
if (seen.has(k)) {
|
|
539
|
+
changed = true;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
seen.add(k);
|
|
543
|
+
ids.push(resolved[i]);
|
|
544
|
+
}
|
|
545
|
+
return changed ? {
|
|
546
|
+
ids,
|
|
547
|
+
requests
|
|
548
|
+
} : { ids };
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
518
551
|
* The deduped identities of `rows` over `fields` — a value read by `read`
|
|
519
552
|
* (default: the row's own field) — and, per row, its identity's index in
|
|
520
553
|
* `ids` (`-1`: the row is absent, or a value is missing / null).
|
|
@@ -712,6 +745,10 @@ const ACTION_OVERLAY = Symbol.for("atscript-db.actionOverlay");
|
|
|
712
745
|
const ACTION_SCOPE = Symbol.for("atscript-db.actionScope");
|
|
713
746
|
/** `true` when the controller overrides `actionRowScope` (since 0.1.147). */
|
|
714
747
|
const ACTION_SCOPED = Symbol.for("atscript-db.actionScoped");
|
|
748
|
+
/** The controller's internal `resolveRowIds` call for an action's ids (since 0.1.148). */
|
|
749
|
+
const ROW_RESOLVE_IDS = Symbol.for("atscript-db.resolveRowIds");
|
|
750
|
+
/** `true` when the controller overrides `resolveRowIds` (since 0.1.148). */
|
|
751
|
+
const ROW_RESOLVES = Symbol.for("atscript-db.rowResolves");
|
|
715
752
|
/**
|
|
716
753
|
* The controller whose hooks govern this action's rows: only when the action
|
|
717
754
|
* runs against the controller's OWN readable — an `opts.table` binding on a
|
|
@@ -744,10 +781,6 @@ const dbActionOverlaySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
|
744
781
|
await awaitActionPrepared(ctx);
|
|
745
782
|
return await overlayOf.call(ctrl) ?? null;
|
|
746
783
|
});
|
|
747
|
-
/** `true` when this action's controller overrides `actionRowScope`. */
|
|
748
|
-
function isActionScoped(ctx) {
|
|
749
|
-
return ctx.get(scopedControllerSlot)?.[ACTION_SCOPED] === true;
|
|
750
|
-
}
|
|
751
784
|
/**
|
|
752
785
|
* The loaded `rows` (already inside the row overlay) with every row outside
|
|
753
786
|
* the action's `actionRowScope` replaced by `undefined` (since 0.1.147). The
|
|
@@ -1180,6 +1213,26 @@ function isNonEmptyStringArray(value) {
|
|
|
1180
1213
|
}
|
|
1181
1214
|
//#endregion
|
|
1182
1215
|
//#region src/meta/field-capabilities.ts
|
|
1216
|
+
/** The display-only refusal clause per position. */
|
|
1217
|
+
const DISPLAY_ONLY_POSITION = {
|
|
1218
|
+
filter: "a filter",
|
|
1219
|
+
sort: "$sort",
|
|
1220
|
+
groupedSelect: "a grouped $select",
|
|
1221
|
+
groupBy: "$groupBy",
|
|
1222
|
+
having: "$having",
|
|
1223
|
+
aggregate: "an aggregate",
|
|
1224
|
+
bucket: "a calendar bucket"
|
|
1225
|
+
};
|
|
1226
|
+
/** The capability of a declared decoration: selectable, nothing else. */
|
|
1227
|
+
const DECORATION_CAP = {
|
|
1228
|
+
filterable: false,
|
|
1229
|
+
sortable: false,
|
|
1230
|
+
selectable: true,
|
|
1231
|
+
indexed: false,
|
|
1232
|
+
bucketable: false,
|
|
1233
|
+
groupable: false,
|
|
1234
|
+
numeric: false
|
|
1235
|
+
};
|
|
1183
1236
|
const REASON_ADAPTER_FILTER = `${_atscript_db.ADAPTER_FILTER_REASON}.`;
|
|
1184
1237
|
const REASON_ADAPTER_SORT = "adapter cannot sort on this storage type.";
|
|
1185
1238
|
const REASON_WRITE_ONLY = "field is @db.writeOnly.";
|
|
@@ -1284,6 +1337,8 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1284
1337
|
bucketUnits;
|
|
1285
1338
|
/** Aggregate functions the adapter renders, in canonical `ALL_AGGREGATE_FNS` order (`/meta.aggregateFns`). */
|
|
1286
1339
|
aggregateFns;
|
|
1340
|
+
/** Whether the adapter renders aggregate arithmetic (`/meta.aggregateExpressions`). */
|
|
1341
|
+
aggregateExpressions;
|
|
1287
1342
|
/** The adapter-level capabilities this index was built against — see {@link adapterSignature}. */
|
|
1288
1343
|
signature;
|
|
1289
1344
|
/**
|
|
@@ -1293,9 +1348,24 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1293
1348
|
* the index reads must be added here.
|
|
1294
1349
|
*/
|
|
1295
1350
|
static adapterSignature(source) {
|
|
1296
|
-
return `${source.isGeoSearchable()}|${[...source.calendarBucketUnits()].join(",")}|${[...source.aggregateFns()].join(",")}`;
|
|
1351
|
+
return `${source.isGeoSearchable()}|${[...source.calendarBucketUnits()].join(",")}|${[...source.aggregateFns()].join(",")}|${source.supportsAggregateExpressions()}`;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* The navigation paths of a readable: its `navFields`, else its relation
|
|
1355
|
+
* names (partial readables list only the latter).
|
|
1356
|
+
*/
|
|
1357
|
+
static navPathsOf(source) {
|
|
1358
|
+
const nav = new Set(source.navFields);
|
|
1359
|
+
if (nav.size === 0) for (const name of source.relations.keys()) nav.add(name);
|
|
1360
|
+
return nav;
|
|
1297
1361
|
}
|
|
1298
1362
|
_entries = /* @__PURE__ */ new Map();
|
|
1363
|
+
/**
|
|
1364
|
+
* Declared display-only decorations (`@DbDecorations`, since 0.1.148) —
|
|
1365
|
+
* virtual entries: key → the readable paths it `requires`. Selectable only;
|
|
1366
|
+
* visible while every required path is.
|
|
1367
|
+
*/
|
|
1368
|
+
_decorations;
|
|
1299
1369
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
1300
1370
|
_objectParents = /* @__PURE__ */ new Map();
|
|
1301
1371
|
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
@@ -1308,7 +1378,8 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1308
1378
|
get objectParents() {
|
|
1309
1379
|
return this._objectParents;
|
|
1310
1380
|
}
|
|
1311
|
-
constructor(source, writeOnly) {
|
|
1381
|
+
constructor(source, writeOnly, decorations = /* @__PURE__ */ new Map()) {
|
|
1382
|
+
this._decorations = decorations;
|
|
1312
1383
|
const tableMeta = source.type.metadata;
|
|
1313
1384
|
this.filterableManual = tableMeta.get("db.table.filterable") === "manual";
|
|
1314
1385
|
this.sortableManual = tableMeta.get("db.table.sortable") === "manual";
|
|
@@ -1318,6 +1389,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1318
1389
|
this.bucketUnits = _uniqu_core.BUCKET_UNITS.filter((unit) => units.has(unit));
|
|
1319
1390
|
const fns = source.aggregateFns();
|
|
1320
1391
|
this.aggregateFns = [..._atscript_db.ALL_AGGREGATE_FNS].filter((fn) => fns.has(fn));
|
|
1392
|
+
this.aggregateExpressions = source.supportsAggregateExpressions();
|
|
1321
1393
|
const physicalNames = /* @__PURE__ */ new Set();
|
|
1322
1394
|
const jsonValueParents = /* @__PURE__ */ new Set();
|
|
1323
1395
|
for (const fd of source.fieldDescriptors) {
|
|
@@ -1330,8 +1402,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1330
1402
|
dimensions: source.dimensions,
|
|
1331
1403
|
measures: source.measures
|
|
1332
1404
|
};
|
|
1333
|
-
const nav =
|
|
1334
|
-
if (nav.size === 0) for (const name of source.relations.keys()) nav.add(name);
|
|
1405
|
+
const nav = FieldCapabilityIndex.navPathsOf(source);
|
|
1335
1406
|
this.navFields = nav;
|
|
1336
1407
|
const isNavOrDescendant = (path) => (0, _atscript_db.selfOrAncestor)(path, nav) !== void 0;
|
|
1337
1408
|
const flatMap = source.flatMap;
|
|
@@ -1395,13 +1466,18 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1395
1466
|
if (!sortReason && this.sortableManual && !annotated(fd, "db.column.sortable")) sortReason = REASON_ANNOTATION_SORT;
|
|
1396
1467
|
const bucket = isWriteOnly ? void 0 : (0, _atscript_db.bucketSourceVerdict)(fd, this._bucketTable, source);
|
|
1397
1468
|
const bucketReason = !bucket ? REASON_WRITE_ONLY : bucket.ok ? void 0 : `${bucket.reason}.`;
|
|
1469
|
+
const group = isWriteOnly ? void 0 : (0, _atscript_db.groupSourceVerdict)(fd, this._bucketTable, source);
|
|
1470
|
+
const groupReason = !group ? REASON_WRITE_ONLY : group.ok ? void 0 : `${group.reason}.`;
|
|
1398
1471
|
const cap = {
|
|
1399
1472
|
filterable: filterBy.compare === void 0,
|
|
1400
1473
|
sortable: sortReason === void 0,
|
|
1401
1474
|
selectable: true,
|
|
1402
1475
|
indexed: fd.isIndexed === true,
|
|
1403
|
-
bucketable: bucketReason === void 0
|
|
1476
|
+
bucketable: bucketReason === void 0,
|
|
1477
|
+
groupable: groupReason === void 0,
|
|
1478
|
+
numeric: this.aggregateExpressions && physicalReason === void 0 && (0, _atscript_db.numericOperandProblem)(fd) === void 0
|
|
1404
1479
|
};
|
|
1480
|
+
if (groupReason) cap.groupReason = groupReason;
|
|
1405
1481
|
if (filterOps.length > 0) cap.filterOps = filterOps;
|
|
1406
1482
|
if (filterBy.compare) cap.filterReason = filterBy.compare;
|
|
1407
1483
|
if (sortReason) cap.sortReason = sortReason;
|
|
@@ -1422,6 +1498,18 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1422
1498
|
entry.fd
|
|
1423
1499
|
];
|
|
1424
1500
|
}
|
|
1501
|
+
/** The declared decoration keys, in declaration order. */
|
|
1502
|
+
get decorationKeys() {
|
|
1503
|
+
return this._decorations.keys();
|
|
1504
|
+
}
|
|
1505
|
+
/** The capability of the declared decoration `key` (selectable only), `undefined` when `key` is none. */
|
|
1506
|
+
decorationCap(key) {
|
|
1507
|
+
return this._decorations.has(key) ? DECORATION_CAP : void 0;
|
|
1508
|
+
}
|
|
1509
|
+
/** The decoration `key` is visible: every path it `requires` passes `exists` (the hidden-field hook). */
|
|
1510
|
+
decorationVisible(key, exists) {
|
|
1511
|
+
return this._decorations.get(key)?.every(exists) === true;
|
|
1512
|
+
}
|
|
1425
1513
|
/** Physical filter capability (adapter ∧ ¬writeOnly ∧ ¬encrypted) — ignores the manual-mode policy. */
|
|
1426
1514
|
isPhysicallyFilterable(path) {
|
|
1427
1515
|
return this._entries.get(path)?.physicalFilterable === true;
|
|
@@ -1446,6 +1534,11 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1446
1534
|
* JSON-stored column" — clients pin that wording, so do not "align" it
|
|
1447
1535
|
* with the core backstop's text.
|
|
1448
1536
|
*
|
|
1537
|
+
* A declared decoration (`@DbDecorations`) is a virtual entry: `select` while
|
|
1538
|
+
* every path it requires passes `exists`, any other position a display-only
|
|
1539
|
+
* refusal (`groupedSelect` is a `$select` of an aggregate query), and hidden
|
|
1540
|
+
* sources answer `Unknown field` like a nonexistent path.
|
|
1541
|
+
*
|
|
1449
1542
|
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
1450
1543
|
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
1451
1544
|
*
|
|
@@ -1455,8 +1548,18 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1455
1548
|
* name the prefixed one. A filter on a navigation path names the predicate
|
|
1456
1549
|
* alternative (`ticket=$some(status=…)`).
|
|
1457
1550
|
*/
|
|
1458
|
-
check(local,
|
|
1551
|
+
check(local, gateOp, exists, predicate = "compare", prefix = "") {
|
|
1459
1552
|
const path = prefix + local;
|
|
1553
|
+
const requires = prefix === "" ? this._decorations.get(local) : void 0;
|
|
1554
|
+
if (requires) {
|
|
1555
|
+
if (!requires.every(exists)) return unknownField(path);
|
|
1556
|
+
if (gateOp === "select") return void 0;
|
|
1557
|
+
return {
|
|
1558
|
+
path,
|
|
1559
|
+
message: `Field "${path}" is display-only and cannot be used in ${DISPLAY_ONLY_POSITION[gateOp]}`
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
const op = gateOp === "groupedSelect" ? "select" : gateOp;
|
|
1460
1563
|
if (!exists(local)) return unknownField(path);
|
|
1461
1564
|
const { kind, parent: localParent } = (0, _atscript_db.classifyQueryPath)(this, local);
|
|
1462
1565
|
const parent = localParent === void 0 ? void 0 : prefix + localParent;
|
|
@@ -1486,6 +1589,10 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1486
1589
|
path,
|
|
1487
1590
|
message: `Bucketing field "${path}" is not permitted — ${entry.cap.bucketReason}`
|
|
1488
1591
|
};
|
|
1592
|
+
case "groupBy": return entry.cap.groupable ? void 0 : {
|
|
1593
|
+
path,
|
|
1594
|
+
message: `${OP_SUBJECT[op]} field "${path}" is not permitted — ${entry.cap.groupReason}`
|
|
1595
|
+
};
|
|
1489
1596
|
default: return entry.physicalFilterable ? void 0 : {
|
|
1490
1597
|
path,
|
|
1491
1598
|
message: `${OP_SUBJECT[op]} field "${path}" is not permitted — ${entry.physicalReason}`
|
|
@@ -1852,6 +1959,10 @@ function hasClientPredicate(filter) {
|
|
|
1852
1959
|
* without copying).
|
|
1853
1960
|
*/
|
|
1854
1961
|
function readRequestContext(endpoint, controls, filter) {
|
|
1962
|
+
if (endpoint === "insert") return controls.$onConflict === "ignore" ? {
|
|
1963
|
+
endpoint,
|
|
1964
|
+
onConflict: "ignore"
|
|
1965
|
+
} : { endpoint };
|
|
1855
1966
|
if (!filter || Object.keys(filter).length === 0) return {
|
|
1856
1967
|
endpoint,
|
|
1857
1968
|
controls,
|
|
@@ -2140,7 +2251,7 @@ let AsReadableController = class AsReadableController {
|
|
|
2140
2251
|
async parseRequest(endpoint, url) {
|
|
2141
2252
|
let request;
|
|
2142
2253
|
if (url !== void 0) {
|
|
2143
|
-
const { parsed, hasNonControl } = endpoint === "one" ? this.parseControlsOnlyFromUrl(url) : {
|
|
2254
|
+
const { parsed, hasNonControl } = endpoint === "one" || endpoint === "insert" ? this.parseControlsOnlyFromUrl(url) : {
|
|
2144
2255
|
parsed: this.parseQueryString(url),
|
|
2145
2256
|
hasNonControl: false
|
|
2146
2257
|
};
|
|
@@ -2562,8 +2673,8 @@ function isIdValidationSource(value) {
|
|
|
2562
2673
|
const v = value;
|
|
2563
2674
|
return Array.isArray(v.identifications) && Array.isArray(v.fieldDescriptors);
|
|
2564
2675
|
}
|
|
2565
|
-
function validateSingleId(body, source,
|
|
2566
|
-
const errors = collectIdErrors(body, source,
|
|
2676
|
+
function validateSingleId(body, source, opts = {}) {
|
|
2677
|
+
const errors = collectIdErrors(body, source, opts);
|
|
2567
2678
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
2568
2679
|
return body;
|
|
2569
2680
|
}
|
|
@@ -2580,12 +2691,13 @@ function validateMultiId(body, source, maxIds = Infinity) {
|
|
|
2580
2691
|
details: []
|
|
2581
2692
|
}]);
|
|
2582
2693
|
const errors = [];
|
|
2583
|
-
for (let i = 0; i < body.length; i++) errors.push(...collectIdErrors(body[i], source, `[${i}]`));
|
|
2694
|
+
for (let i = 0; i < body.length; i++) errors.push(...collectIdErrors(body[i], source, { path: `[${i}]` }));
|
|
2584
2695
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
2585
2696
|
return body;
|
|
2586
2697
|
}
|
|
2587
|
-
function collectIdErrors(value, source,
|
|
2588
|
-
|
|
2698
|
+
function collectIdErrors(value, source, opts) {
|
|
2699
|
+
const pathPrefix = opts.path ?? "";
|
|
2700
|
+
if (!isPlainObject$2(value)) return [{
|
|
2589
2701
|
path: pathPrefix,
|
|
2590
2702
|
message: "Expected JSON object for row identifier",
|
|
2591
2703
|
details: []
|
|
@@ -2605,14 +2717,21 @@ function collectIdErrors(value, source, pathPrefix) {
|
|
|
2605
2717
|
const errors = [];
|
|
2606
2718
|
for (const fieldName of match.fields) {
|
|
2607
2719
|
const sub = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
|
|
2608
|
-
const err = checkScalar(value[fieldName], cache.fieldByName.get(fieldName), sub);
|
|
2720
|
+
const err = checkScalar(value[fieldName], sub) ?? (opts.strictTypes === false ? void 0 : checkType(value[fieldName], cache.fieldByName.get(fieldName), sub));
|
|
2609
2721
|
if (err) errors.push(err);
|
|
2610
2722
|
}
|
|
2611
2723
|
return errors;
|
|
2612
2724
|
}
|
|
2613
|
-
|
|
2725
|
+
/**
|
|
2726
|
+
* An identifier value is always a scalar: an object would reach the filter
|
|
2727
|
+
* as an operator expression (`{ $ne: null }`) whatever the field's type.
|
|
2728
|
+
*/
|
|
2729
|
+
function checkScalar(value, path) {
|
|
2730
|
+
return typeof value === "object" && value !== null ? scalarMismatch(path, "a scalar", value) : void 0;
|
|
2731
|
+
}
|
|
2732
|
+
/** The value's type against the field's declared design type (default `string`). */
|
|
2733
|
+
function checkType(value, fd, path) {
|
|
2614
2734
|
const expected = fd?.designType ?? "string";
|
|
2615
|
-
if (typeof value === "object" && value !== null) return scalarMismatch(path, "a scalar", value);
|
|
2616
2735
|
if (expected === "string" && typeof value !== "string") return scalarMismatch(path, expected, value);
|
|
2617
2736
|
if (expected === "number" && typeof value !== "number") return scalarMismatch(path, expected, value);
|
|
2618
2737
|
if (expected === "boolean" && typeof value !== "boolean") return scalarMismatch(path, expected, value);
|
|
@@ -2629,20 +2748,76 @@ function describe(value) {
|
|
|
2629
2748
|
if (Array.isArray(value)) return "array";
|
|
2630
2749
|
return typeof value;
|
|
2631
2750
|
}
|
|
2632
|
-
function isPlainObject$
|
|
2751
|
+
function isPlainObject$2(value) {
|
|
2633
2752
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2634
2753
|
}
|
|
2635
2754
|
//#endregion
|
|
2636
2755
|
//#region src/actions/id-cache.ts
|
|
2637
2756
|
/**
|
|
2757
|
+
* EVERY id the client sent, in request order, with the identity of the id it
|
|
2758
|
+
* resolved to (since 0.1.148) — set only when the controller's
|
|
2759
|
+
* `resolveRowIds` changed or collapsed an id. The single model: refusals,
|
|
2760
|
+
* `reasons`, summaries and counts are judged and reported per request id
|
|
2761
|
+
* through {@link echoRequests}; the resolved (deduped) ids serve only the
|
|
2762
|
+
* row load and the handler.
|
|
2763
|
+
*/
|
|
2764
|
+
const dbActionRequestIdsKey = (0, _wooksjs_event_core.key)("atscript_db_action_request_ids");
|
|
2765
|
+
/** Number of request ids (`fallback` when no id was resolved to another). */
|
|
2766
|
+
function requestCount(ctx, fallback) {
|
|
2767
|
+
return ctx.has(dbActionRequestIdsKey) ? ctx.get(dbActionRequestIdsKey).length : fallback;
|
|
2768
|
+
}
|
|
2769
|
+
/** Number of request ids that resolved to one of `ids` (`ids.length` when none was rewritten). */
|
|
2770
|
+
function requestCountOf(ctx, ids) {
|
|
2771
|
+
if (!ctx.has(dbActionRequestIdsKey)) return ids.length;
|
|
2772
|
+
const keys = new Set(ids.map((id) => identityKey(id)));
|
|
2773
|
+
return ctx.get(dbActionRequestIdsKey).filter((r) => keys.has(r.key)).length;
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* Entries keyed by a resolved id, reported for every REQUEST id that resolved
|
|
2777
|
+
* to it — in request order, each as the client sent it (`id` replaced). Two
|
|
2778
|
+
* aliases of one row come back exactly like two distinct rows (same order,
|
|
2779
|
+
* same count). Entries whose id no request resolved to stay as they are,
|
|
2780
|
+
* after the request ones.
|
|
2781
|
+
*/
|
|
2782
|
+
function echoRequests(ctx, entries) {
|
|
2783
|
+
if (!ctx.has(dbActionRequestIdsKey)) return [...entries];
|
|
2784
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2785
|
+
const rest = [];
|
|
2786
|
+
for (const e of entries) {
|
|
2787
|
+
const k = identityKey(e.id);
|
|
2788
|
+
if (k === void 0) rest.push(e);
|
|
2789
|
+
else if (!byKey.has(k)) byKey.set(k, e);
|
|
2790
|
+
}
|
|
2791
|
+
const out = [];
|
|
2792
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2793
|
+
for (const r of ctx.get(dbActionRequestIdsKey)) {
|
|
2794
|
+
const e = byKey.get(r.key);
|
|
2795
|
+
if (!e) continue;
|
|
2796
|
+
matched.add(r.key);
|
|
2797
|
+
out.push({
|
|
2798
|
+
...e,
|
|
2799
|
+
id: r.id
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
for (const [k, e] of byKey) if (!matched.has(k)) rest.push(e);
|
|
2803
|
+
return [...out, ...rest];
|
|
2804
|
+
}
|
|
2805
|
+
/** The id as the client sent it (the first, for an id several requests resolved to). */
|
|
2806
|
+
function requestIdOf(ctx, id) {
|
|
2807
|
+
return echoRequests(ctx, [{ id }])[0].id;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2638
2810
|
* Validates the body's `ids` against the action table's identifications. For
|
|
2639
2811
|
* the controller's own table that is its `idSource` (since 0.1.134): a unique
|
|
2640
2812
|
* index over a field `hasField` hides neither addresses a row nor appears in
|
|
2641
2813
|
* the "must exactly match one of" message. An `opts.table` binding has no
|
|
2642
2814
|
* visibility hook. The controller's `prepareRequest` (since 0.1.143) runs
|
|
2643
2815
|
* first, so the visibility the ids are validated against is the request's.
|
|
2816
|
+
* Then, when the controller overrides `resolveRowIds` (since 0.1.148), the
|
|
2817
|
+
* validated ids go through it — a one-element array for a `'row'` action —
|
|
2818
|
+
* and the resolved ids (duplicates collapsed) are what every consumer sees.
|
|
2644
2819
|
*/
|
|
2645
|
-
async function resolveValidatedId(ctx, validate) {
|
|
2820
|
+
async function resolveValidatedId(ctx, level, validate) {
|
|
2646
2821
|
await awaitActionPrepared(ctx);
|
|
2647
2822
|
let source = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
|
|
2648
2823
|
if (!source) {
|
|
@@ -2652,9 +2827,21 @@ async function resolveValidatedId(ctx, validate) {
|
|
|
2652
2827
|
if (!isIdValidationSource(source)) throw noTableError(ctx);
|
|
2653
2828
|
const env = await ctx.get(dbActionBodySlot);
|
|
2654
2829
|
validate(env.ids, source);
|
|
2655
|
-
|
|
2830
|
+
const scoped = ctx.get(scopedControllerSlot);
|
|
2831
|
+
const resolve = scoped?.[ROW_RESOLVES] ? scoped[ROW_RESOLVE_IDS] : void 0;
|
|
2832
|
+
if (!resolve) return env.ids;
|
|
2833
|
+
const requested = level === "row" ? [env.ids] : env.ids;
|
|
2834
|
+
const overlay = await ctx.get(dbActionOverlaySlot);
|
|
2835
|
+
const { ids, requests } = await resolve.call(scoped, requested, {
|
|
2836
|
+
purpose: "action",
|
|
2837
|
+
action: readCurrentActionMeta(ctx)?.name,
|
|
2838
|
+
level,
|
|
2839
|
+
overlay: overlay ?? void 0
|
|
2840
|
+
});
|
|
2841
|
+
if (requests) ctx.set(dbActionRequestIdsKey, requests);
|
|
2842
|
+
return level === "row" ? ids[0] : ids;
|
|
2656
2843
|
}
|
|
2657
|
-
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
|
|
2844
|
+
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, "row", validateSingleId));
|
|
2658
2845
|
/**
|
|
2659
2846
|
* The `'rows'` action's identifiers: the body's validated `ids`, or — for a
|
|
2660
2847
|
* query target (`query`, since 0.1.147) — the identities of the rows it
|
|
@@ -2664,7 +2851,7 @@ const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
|
2664
2851
|
await awaitActionPrepared(ctx);
|
|
2665
2852
|
const target = await ctx.get(dbActionQueryTargetSlot);
|
|
2666
2853
|
if (target) return target.ids;
|
|
2667
|
-
return await resolveValidatedId(ctx, (body, src) => validateMultiId(body, src, actionMaxIds(ctx)));
|
|
2854
|
+
return await resolveValidatedId(ctx, "rows", (body, src) => validateMultiId(body, src, actionMaxIds(ctx)));
|
|
2668
2855
|
});
|
|
2669
2856
|
const useDbActionId = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdSlot) }));
|
|
2670
2857
|
const useDbActionIds = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdsSlot) }));
|
|
@@ -2684,29 +2871,70 @@ function asFetchTable(value) {
|
|
|
2684
2871
|
function seedActionFields(ctx, table) {
|
|
2685
2872
|
return actionRowFields(table, requiredFieldsOf(readCurrentActionMeta(ctx)?.opts), actionFieldVisibility(ctx));
|
|
2686
2873
|
}
|
|
2874
|
+
const UNSCOPED = {
|
|
2875
|
+
kind: "resolved",
|
|
2876
|
+
scope: null
|
|
2877
|
+
};
|
|
2878
|
+
const DEFERRED = { kind: "deferred" };
|
|
2879
|
+
function inPreferredShape(ids, preferred) {
|
|
2880
|
+
return ids.every((id) => {
|
|
2881
|
+
return Object.keys(id).length === preferred.length && preferred.every((f) => f in id);
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
async function resolvePreScope(ctx, level) {
|
|
2885
|
+
const ctrl = ctx.get(scopedControllerSlot);
|
|
2886
|
+
const action = readCurrentActionMeta(ctx)?.name;
|
|
2887
|
+
const scopeOf = ctrl?.[ACTION_SCOPED] ? ctrl[ACTION_SCOPE] : void 0;
|
|
2888
|
+
if (!ctrl || !scopeOf || action === void 0) return UNSCOPED;
|
|
2889
|
+
if (await ctx.get(dbActionOverlaySlot)) return DEFERRED;
|
|
2890
|
+
const table = asFetchTable(getActionTable(ctx));
|
|
2891
|
+
const preferred = table?.preferredId;
|
|
2892
|
+
if (!table || !preferred?.length) return DEFERRED;
|
|
2893
|
+
const [target, requested] = await Promise.all([level === "rows" ? ctx.get(dbActionQueryTargetSlot) : void 0, level === "row" ? ctx.get(dbActionIdSlot).then((id) => [id]) : ctx.get(dbActionIdsSlot)]);
|
|
2894
|
+
if (target || !inPreferredShape(requested, preferred)) return DEFERRED;
|
|
2895
|
+
const ids = level === "row" ? requested : dedupeIdentities(requested, preferred).ids;
|
|
2896
|
+
if (ids.length === 0) return UNSCOPED;
|
|
2897
|
+
return {
|
|
2898
|
+
kind: "resolved",
|
|
2899
|
+
scope: nonEmptyFilter(await scopeOf.call(ctrl, action, createScopeContext("execute", ids, table))) ?? null
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* {@link TPreScope} of the current action — once per event. An event runs one
|
|
2904
|
+
* action, so the first caller's `level` (`'row'` / `'rows'`) is the action's.
|
|
2905
|
+
*/
|
|
2906
|
+
const dbActionPreScopeSlot = (0, _wooksjs_event_core.cached)((ctx) => {
|
|
2907
|
+
let pending;
|
|
2908
|
+
return (level) => pending ??= resolvePreScope(ctx, level);
|
|
2909
|
+
});
|
|
2687
2910
|
/**
|
|
2688
2911
|
* Loaded row / rows are ANDed with the controller's row overlay (see
|
|
2689
2912
|
* `dbActionOverlaySlot`, since 0.1.143) and, since 0.1.147, checked against
|
|
2690
|
-
* the action's `actionRowScope
|
|
2691
|
-
*
|
|
2692
|
-
*
|
|
2913
|
+
* the action's `actionRowScope`: an out-of-scope id loads nothing — exactly
|
|
2914
|
+
* like a missing one (the same 404 on `'row'` actions, so the two can't be
|
|
2915
|
+
* told apart).
|
|
2693
2916
|
*
|
|
2694
2917
|
* Order: `prepareRequest` → the row overlay (BEFORE the request body is
|
|
2695
|
-
* read: fail-fast authorization) → the ids (body) →
|
|
2696
|
-
* `actionRowScope` (
|
|
2918
|
+
* read: fail-fast authorization) → the ids (body) → `resolveRowIds` (since
|
|
2919
|
+
* 0.1.148) → `actionRowScope` (pre-load, when there is no overlay and the
|
|
2920
|
+
* ids are in `preferredId` shape — its restriction joins the one row load;
|
|
2921
|
+
* since 0.1.148) → the row load → otherwise `actionRowScope` on the loaded
|
|
2922
|
+
* candidates (it needs them).
|
|
2697
2923
|
*/
|
|
2698
2924
|
async function loadRow(ctx) {
|
|
2699
2925
|
const overlay = await ctx.get(dbActionOverlaySlot);
|
|
2700
2926
|
const id = await ctx.get(dbActionIdSlot);
|
|
2701
2927
|
const table = asFetchTable(getActionTable(ctx));
|
|
2702
2928
|
if (!table) throw noTableError(ctx);
|
|
2929
|
+
const pre = await ctx.get(dbActionPreScopeSlot)("row");
|
|
2703
2930
|
const fields = seedActionFields(ctx, table);
|
|
2704
2931
|
for (const k of Object.keys(id)) fields.add(k);
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2932
|
+
const idFilter = withOverlay(withOverlay(id, overlay), pre.kind === "resolved" ? pre.scope : null);
|
|
2933
|
+
let row = await table.findOne({
|
|
2934
|
+
filter: idFilter,
|
|
2707
2935
|
controls: { $select: [...fields] }
|
|
2708
|
-
});
|
|
2709
|
-
|
|
2936
|
+
}) ?? void 0;
|
|
2937
|
+
if (row !== void 0 && pre.kind === "deferred") [row] = await applyActionScope(ctx, table, [row]);
|
|
2710
2938
|
if (row === void 0) throw new _moostjs_event_http.HttpError(404, "Row not found for action identifier");
|
|
2711
2939
|
return row;
|
|
2712
2940
|
}
|
|
@@ -2717,7 +2945,11 @@ async function loadRows(ctx) {
|
|
|
2717
2945
|
if (!table) throw noTableError(ctx);
|
|
2718
2946
|
const fields = seedActionFields(ctx, table);
|
|
2719
2947
|
const target = await ctx.get(dbActionQueryTargetSlot);
|
|
2720
|
-
if (!target)
|
|
2948
|
+
if (!target) {
|
|
2949
|
+
const pre = await ctx.get(dbActionPreScopeSlot)("rows");
|
|
2950
|
+
if (pre.kind === "resolved") return findRowsByIds(table, ids, pre.scope ? withOverlay(pre.scope, overlay) : overlay, fields);
|
|
2951
|
+
return applyActionScope(ctx, table, await findRowsByIds(table, ids, overlay, fields));
|
|
2952
|
+
}
|
|
2721
2953
|
const rows = await target.load(ids, fields);
|
|
2722
2954
|
const stale = /* @__PURE__ */ new Set();
|
|
2723
2955
|
for (let i = 0; i < rows.length; i++) if (rows[i] === void 0) stale.add(i);
|
|
@@ -2774,25 +3006,52 @@ function DbActionTarget() {
|
|
|
2774
3006
|
var TargetBase = class {
|
|
2775
3007
|
kind;
|
|
2776
3008
|
matched;
|
|
3009
|
+
ctx;
|
|
2777
3010
|
skipped = [];
|
|
2778
3011
|
failed = [];
|
|
2779
3012
|
processed = 0;
|
|
2780
|
-
|
|
3013
|
+
/** Identities of every id handed to the handler so far. */
|
|
3014
|
+
handedKeys = /* @__PURE__ */ new Set();
|
|
3015
|
+
failedKeys = /* @__PURE__ */ new Set();
|
|
3016
|
+
constructor(kind, matched, ctx) {
|
|
2781
3017
|
this.kind = kind;
|
|
2782
3018
|
this.matched = matched;
|
|
3019
|
+
this.ctx = ctx;
|
|
2783
3020
|
}
|
|
3021
|
+
/**
|
|
3022
|
+
* One failure per id, whether or not `resolveRowIds` rewrote any id: a
|
|
3023
|
+
* second `fail()` of the same identity is ignored (the first reason stands),
|
|
3024
|
+
* so `failed` and `processed` agree in both modes.
|
|
3025
|
+
*/
|
|
2784
3026
|
fail(id, reason) {
|
|
3027
|
+
const k = identityKey(id);
|
|
3028
|
+
if (k !== void 0) {
|
|
3029
|
+
if (this.failedKeys.has(k)) return;
|
|
3030
|
+
this.failedKeys.add(k);
|
|
3031
|
+
}
|
|
2785
3032
|
this.failed.push({
|
|
2786
3033
|
id,
|
|
2787
3034
|
reason
|
|
2788
3035
|
});
|
|
2789
3036
|
}
|
|
3037
|
+
/**
|
|
3038
|
+
* A batch handed to the handler, counted per request id (two aliases of one
|
|
3039
|
+
* row count twice, like two distinct rows).
|
|
3040
|
+
*/
|
|
3041
|
+
countProcessed(ids) {
|
|
3042
|
+
for (const id of ids) {
|
|
3043
|
+
const k = identityKey(id);
|
|
3044
|
+
if (k !== void 0) this.handedKeys.add(k);
|
|
3045
|
+
}
|
|
3046
|
+
this.processed += requestCountOf(this.ctx, ids);
|
|
3047
|
+
}
|
|
2790
3048
|
summary() {
|
|
3049
|
+
const failed = echoRequests(this.ctx, this.failed);
|
|
2791
3050
|
return {
|
|
2792
3051
|
matched: this.matched,
|
|
2793
|
-
processed: Math.max(0, this.processed -
|
|
2794
|
-
skipped:
|
|
2795
|
-
failed
|
|
3052
|
+
processed: Math.max(0, this.processed - failed.length),
|
|
3053
|
+
skipped: echoRequests(this.ctx, this.skipped),
|
|
3054
|
+
failed
|
|
2796
3055
|
};
|
|
2797
3056
|
}
|
|
2798
3057
|
};
|
|
@@ -2804,12 +3063,12 @@ var MaterializedTarget = class extends TargetBase {
|
|
|
2804
3063
|
ids;
|
|
2805
3064
|
rows;
|
|
2806
3065
|
iterated = false;
|
|
2807
|
-
constructor(kind, matched, ids, rows, skipped) {
|
|
2808
|
-
super(kind, matched);
|
|
3066
|
+
constructor(kind, matched, ids, rows, skipped, ctx) {
|
|
3067
|
+
super(kind, matched, ctx);
|
|
2809
3068
|
this.ids = ids;
|
|
2810
3069
|
this.rows = rows;
|
|
2811
3070
|
this.skipped.push(...skipped);
|
|
2812
|
-
this.
|
|
3071
|
+
this.countProcessed(ids);
|
|
2813
3072
|
}
|
|
2814
3073
|
async *batches() {
|
|
2815
3074
|
if (this.iterated) throw new Error("[moost-db actions] target.batches() is single-pass");
|
|
@@ -2825,21 +3084,19 @@ async function setMaterializedTarget(ctx) {
|
|
|
2825
3084
|
const target = await resolveActionQueryTarget(ctx, false);
|
|
2826
3085
|
const ids = await ctx.get(dbActionIdsSlot);
|
|
2827
3086
|
const skipped = ctx.has(dbActionSkippedKey) ? ctx.get(dbActionSkippedKey) : [];
|
|
2828
|
-
ctx.set(dbActionTargetKey, new MaterializedTarget(target ? "query" : "ids", target ? target.matched : ids.length + skipped.length, ids, async () => {
|
|
3087
|
+
ctx.set(dbActionTargetKey, new MaterializedTarget(target ? "query" : "ids", target ? target.matched : requestCount(ctx, ids.length + skipped.length), ids, async () => {
|
|
2829
3088
|
return (await ctx.get(dbActionRowsSlot)).filter((r) => r !== void 0);
|
|
2830
|
-
}, skipped));
|
|
3089
|
+
}, skipped, ctx));
|
|
2831
3090
|
}
|
|
2832
3091
|
/** The `@DbActionTarget` surface: the target in batches, each gated when it is reached. */
|
|
2833
3092
|
var StreamedTarget = class extends TargetBase {
|
|
2834
|
-
ctx;
|
|
2835
3093
|
source;
|
|
2836
3094
|
batchSize;
|
|
2837
3095
|
action;
|
|
2838
3096
|
disabled;
|
|
2839
3097
|
iterated = false;
|
|
2840
3098
|
constructor(matched, ctx, source, batchSize, action, disabled) {
|
|
2841
|
-
super(source.kind, matched);
|
|
2842
|
-
this.ctx = ctx;
|
|
3099
|
+
super(source.kind, matched, ctx);
|
|
2843
3100
|
this.source = source;
|
|
2844
3101
|
this.batchSize = batchSize;
|
|
2845
3102
|
this.action = action;
|
|
@@ -2857,7 +3114,7 @@ var StreamedTarget = class extends TargetBase {
|
|
|
2857
3114
|
const batch = await this.gate(ids.slice(start, start + this.batchSize));
|
|
2858
3115
|
this.next = start + this.batchSize;
|
|
2859
3116
|
if (batch.ids.length === 0) continue;
|
|
2860
|
-
this.
|
|
3117
|
+
this.countProcessed(batch.ids);
|
|
2861
3118
|
this.current = batch.ids;
|
|
2862
3119
|
yield batch;
|
|
2863
3120
|
this.current = void 0;
|
|
@@ -2865,30 +3122,66 @@ var StreamedTarget = class extends TargetBase {
|
|
|
2865
3122
|
this.next = ids.length;
|
|
2866
3123
|
}
|
|
2867
3124
|
/**
|
|
2868
|
-
* The summary of a run the handler failed after it received a batch
|
|
2869
|
-
*
|
|
2870
|
-
*
|
|
3125
|
+
* The summary of a run the handler failed after it received a batch, judged
|
|
3126
|
+
* per REQUEST id by its own request position — an alias after the abort
|
|
3127
|
+
* point is "not run" even when its row was judged (run, skipped) earlier,
|
|
3128
|
+
* exactly as the same request of distinct rows would be:
|
|
3129
|
+
* - at or after the position the run stopped at: `failed` as `"not run"`;
|
|
3130
|
+
* - before it, a row the handler failed itself: its reason;
|
|
3131
|
+
* - before it, the batch the handler held (uncertain): the error's message;
|
|
3132
|
+
* - before it, a row the gate skipped: `skipped`; a row that ran: `processed`.
|
|
3133
|
+
* `aborted` is set.
|
|
2871
3134
|
*/
|
|
2872
3135
|
abort(error) {
|
|
2873
3136
|
const message = errorMessage(error);
|
|
2874
|
-
const
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
3137
|
+
const ids = this.source.ids;
|
|
3138
|
+
const requests = this.ctx.has(dbActionRequestIdsKey) ? this.ctx.get(dbActionRequestIdsKey) : ids.map((id, i) => ({
|
|
3139
|
+
id,
|
|
3140
|
+
key: identityKey(id) ?? `#${i}`
|
|
3141
|
+
}));
|
|
3142
|
+
const held = new Set((this.current ?? []).map((id) => identityKey(id)));
|
|
3143
|
+
let cutoff = requests.length;
|
|
3144
|
+
if (this.ctx.has(dbActionRequestIdsKey)) {
|
|
3145
|
+
const firstOf = (key) => requests.findIndex((r) => r.key === key);
|
|
3146
|
+
if (held.size > 0) cutoff = 1 + Math.max(...[...held].map(firstOf));
|
|
3147
|
+
else if (this.next < ids.length) cutoff = firstOf(identityKey(ids[this.next]));
|
|
3148
|
+
if (cutoff < 0) cutoff = requests.length;
|
|
3149
|
+
} else if (this.next < ids.length) cutoff = this.next;
|
|
3150
|
+
const failedBy = new Map(this.failed.map((f) => [identityKey(f.id), f]));
|
|
3151
|
+
const skippedBy = /* @__PURE__ */ new Map();
|
|
3152
|
+
for (const row of this.skipped) {
|
|
3153
|
+
const k = identityKey(row.id);
|
|
3154
|
+
if (!skippedBy.has(k)) skippedBy.set(k, row);
|
|
3155
|
+
}
|
|
3156
|
+
const failed = [];
|
|
3157
|
+
const skipped = [];
|
|
3158
|
+
let processed = 0;
|
|
3159
|
+
requests.forEach((r, i) => {
|
|
3160
|
+
const own = failedBy.get(r.key);
|
|
3161
|
+
const skip = skippedBy.get(r.key);
|
|
3162
|
+
if (i >= cutoff) failed.push({
|
|
3163
|
+
id: r.id,
|
|
3164
|
+
reason: "not run"
|
|
3165
|
+
});
|
|
3166
|
+
else if (own) failed.push({
|
|
3167
|
+
id: r.id,
|
|
3168
|
+
reason: own.reason
|
|
3169
|
+
});
|
|
3170
|
+
else if (held.has(r.key)) failed.push({
|
|
3171
|
+
id: r.id,
|
|
3172
|
+
reason: message
|
|
3173
|
+
});
|
|
3174
|
+
else if (skip) skipped.push({
|
|
3175
|
+
...skip,
|
|
3176
|
+
id: r.id
|
|
3177
|
+
});
|
|
3178
|
+
else if (this.handedKeys.has(r.key)) processed++;
|
|
3179
|
+
});
|
|
2878
3180
|
return {
|
|
2879
|
-
|
|
2880
|
-
processed
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
...uncertain.map((id) => ({
|
|
2884
|
-
id,
|
|
2885
|
-
reason: message
|
|
2886
|
-
})),
|
|
2887
|
-
...this.source.ids.slice(this.next).map((id) => ({
|
|
2888
|
-
id,
|
|
2889
|
-
reason: "not run"
|
|
2890
|
-
}))
|
|
2891
|
-
],
|
|
3181
|
+
matched: this.matched,
|
|
3182
|
+
processed,
|
|
3183
|
+
skipped,
|
|
3184
|
+
failed,
|
|
2892
3185
|
aborted: {
|
|
2893
3186
|
status: errorStatus(error),
|
|
2894
3187
|
message
|
|
@@ -2983,7 +3276,7 @@ async function setStreamedTarget(ctx, action, disabled) {
|
|
|
2983
3276
|
return findRowsByIds(table, batch, overlay, select);
|
|
2984
3277
|
}
|
|
2985
3278
|
};
|
|
2986
|
-
const target = new StreamedTarget(query ? query.matched : source.ids.length, ctx, source, batchSize, action, disabled);
|
|
3279
|
+
const target = new StreamedTarget(query ? query.matched : requestCount(ctx, source.ids.length), ctx, source, batchSize, action, disabled);
|
|
2987
3280
|
ctx.set(dbActionTargetKey, target);
|
|
2988
3281
|
return {
|
|
2989
3282
|
target,
|
|
@@ -3012,6 +3305,8 @@ const ACTION_SLOTS = [
|
|
|
3012
3305
|
dbActionInputSlot,
|
|
3013
3306
|
dbActionIdSlot,
|
|
3014
3307
|
dbActionIdsSlot,
|
|
3308
|
+
dbActionRequestIdsKey,
|
|
3309
|
+
dbActionPreScopeSlot,
|
|
3015
3310
|
dbActionRowSlot,
|
|
3016
3311
|
dbActionRowsSlot,
|
|
3017
3312
|
dbActionOverlaySlot,
|
|
@@ -3319,6 +3614,398 @@ function getDbEndpoint(target, method) {
|
|
|
3319
3614
|
}
|
|
3320
3615
|
}
|
|
3321
3616
|
//#endregion
|
|
3617
|
+
//#region src/decorations/decoration-index.ts
|
|
3618
|
+
/** A decoration key is a valid `$select` URL name: top-level, no `$` prefix, no dots. */
|
|
3619
|
+
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3620
|
+
/**
|
|
3621
|
+
* The own readable paths a `requires` path stands for: itself when it is an
|
|
3622
|
+
* own field, else — a parent object of a flattened (SQL) readable — every own
|
|
3623
|
+
* field below it. Empty when the path is neither.
|
|
3624
|
+
*/
|
|
3625
|
+
function ownLeaves(path, ownPaths) {
|
|
3626
|
+
if (ownPaths.has(path)) return [path];
|
|
3627
|
+
const prefix = `${path}.`;
|
|
3628
|
+
return [...ownPaths].filter((own) => own.startsWith(prefix));
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Validates `@DbDecorations` metadata against the bound readable and indexes
|
|
3632
|
+
* it. Throws `[moost-db]` errors (once per class — the caller memoizes).
|
|
3633
|
+
*/
|
|
3634
|
+
function buildDecorationIndex(controller, meta, source) {
|
|
3635
|
+
const fail = (message) => {
|
|
3636
|
+
throw new Error(`[moost-db] ${controller}: @DbDecorations ${message}`);
|
|
3637
|
+
};
|
|
3638
|
+
const { type } = meta;
|
|
3639
|
+
if (type.type.kind !== "object") fail("expects an object interface");
|
|
3640
|
+
if (type.metadata.has("db.table") || type.metadata.has("db.view")) fail("expects a plain interface — it must not carry @db.table or @db.view");
|
|
3641
|
+
const keys = [...type.type.props.keys()];
|
|
3642
|
+
const fieldPaths = /* @__PURE__ */ new Set();
|
|
3643
|
+
for (const path of source.flatMap?.keys() ?? []) for (let at = path.indexOf(".");; at = path.indexOf(".", at + 1)) {
|
|
3644
|
+
fieldPaths.add(at < 0 ? path : path.slice(0, at));
|
|
3645
|
+
if (at < 0) break;
|
|
3646
|
+
}
|
|
3647
|
+
const isField = (path) => fieldPaths.has(path);
|
|
3648
|
+
for (const key of keys) {
|
|
3649
|
+
if (!KEY_RE.test(key)) fail(`key "${key}" must be a plain top-level identifier (no "$" prefix, no dots)`);
|
|
3650
|
+
if (isField(key) || source.relations?.has(key) || source.navFields.has(key)) fail(`key "${key}" collides with a field or relation of the bound readable`);
|
|
3651
|
+
}
|
|
3652
|
+
const requires = /* @__PURE__ */ new Map();
|
|
3653
|
+
const leavesOf = /* @__PURE__ */ new Map();
|
|
3654
|
+
const nested = (path) => {
|
|
3655
|
+
const prefix = `${path}.`;
|
|
3656
|
+
const below = [...source.flatMap?.keys() ?? []].filter((p) => p.startsWith(prefix));
|
|
3657
|
+
return below.filter((p) => (0, _atscript_db.selfOrAncestor)(p, source.navFields) === void 0 && !below.some((other) => other.startsWith(`${p}.`)));
|
|
3658
|
+
};
|
|
3659
|
+
for (const key of keys) requires.set(key, []);
|
|
3660
|
+
for (const [key, paths] of Object.entries(meta.requires)) {
|
|
3661
|
+
if (!requires.has(key)) fail(`\`requires\` names "${key}", which is not a declared decoration`);
|
|
3662
|
+
const resolved = [];
|
|
3663
|
+
for (const path of paths) {
|
|
3664
|
+
const leaves = ownLeaves(path, source.ownPaths);
|
|
3665
|
+
if (leaves.length === 0) fail(`"${key}" requires "${path}", which is not an own field of the bound readable`);
|
|
3666
|
+
for (const leaf of leaves) if ([...source.writeOnly].some((wo) => wo.startsWith(`${leaf}.`)) || (0, _atscript_db.selfOrAncestor)(leaf, source.writeOnly) !== void 0) fail(`"${key}" requires "${path}", which is @db.writeOnly (a sealed value cannot be read)`);
|
|
3667
|
+
for (const leaf of leaves) {
|
|
3668
|
+
if (leavesOf.has(leaf)) continue;
|
|
3669
|
+
const below = nested(leaf);
|
|
3670
|
+
if (below.length > 0) leavesOf.set(leaf, below);
|
|
3671
|
+
}
|
|
3672
|
+
resolved.push(...leaves);
|
|
3673
|
+
}
|
|
3674
|
+
requires.set(key, [...new Set(resolved)]);
|
|
3675
|
+
}
|
|
3676
|
+
const visibleOn = /* @__PURE__ */ new Map();
|
|
3677
|
+
for (const [key, paths] of requires) {
|
|
3678
|
+
const all = new Set(paths);
|
|
3679
|
+
for (const path of paths) for (const leaf of leavesOf.get(path) ?? []) all.add(leaf);
|
|
3680
|
+
visibleOn.set(key, [...all]);
|
|
3681
|
+
}
|
|
3682
|
+
return {
|
|
3683
|
+
type,
|
|
3684
|
+
keys,
|
|
3685
|
+
keySet: new Set(keys),
|
|
3686
|
+
requires,
|
|
3687
|
+
visibleOn,
|
|
3688
|
+
leavesOf,
|
|
3689
|
+
memo: { meta: /* @__PURE__ */ new WeakMap() }
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
//#endregion
|
|
3693
|
+
//#region src/select-shape.ts
|
|
3694
|
+
/** The included (`1` / `true`) and excluded (`0` / `false`) keys of a `$select` map. */
|
|
3695
|
+
function mapShape(map) {
|
|
3696
|
+
const included = [];
|
|
3697
|
+
const excluded = [];
|
|
3698
|
+
for (const [key, value] of Object.entries(map)) if (value === 1 || value === true) included.push(key);
|
|
3699
|
+
else if (value === 0 || value === false) excluded.push(key);
|
|
3700
|
+
return {
|
|
3701
|
+
included,
|
|
3702
|
+
excluded
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
/** The one splitter every `$select` consumer in the controller reads. */
|
|
3706
|
+
function selectShape(raw) {
|
|
3707
|
+
if (raw === void 0 || raw === null) return { kind: "all" };
|
|
3708
|
+
if (Array.isArray(raw)) return {
|
|
3709
|
+
kind: "list",
|
|
3710
|
+
items: raw
|
|
3711
|
+
};
|
|
3712
|
+
const map = raw;
|
|
3713
|
+
return {
|
|
3714
|
+
kind: "map",
|
|
3715
|
+
map,
|
|
3716
|
+
...mapShape(map)
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
//#endregion
|
|
3720
|
+
//#region src/decorations/decoration-planner.ts
|
|
3721
|
+
const NO_DECORATIONS = /* @__PURE__ */ new Set();
|
|
3722
|
+
/**
|
|
3723
|
+
* The decoration plumbing of one controller (`@DbDecorations`, since 0.1.148):
|
|
3724
|
+
* rewrites a read's `$select` ({@link plan}), decides what the response
|
|
3725
|
+
* carries once the final projection is known ({@link serve}), and builds the
|
|
3726
|
+
* `/meta` view ({@link meta}). The request gate is not here — decorations are
|
|
3727
|
+
* virtual entries of the {@link FieldCapabilityIndex}.
|
|
3728
|
+
*/
|
|
3729
|
+
var DecorationPlanner = class {
|
|
3730
|
+
index;
|
|
3731
|
+
host;
|
|
3732
|
+
constructor(index, host) {
|
|
3733
|
+
this.index = index;
|
|
3734
|
+
this.host = host;
|
|
3735
|
+
}
|
|
3736
|
+
visible(key) {
|
|
3737
|
+
return this.host.capabilities().decorationVisible(key, this.host.isVisible);
|
|
3738
|
+
}
|
|
3739
|
+
requiresOf(keys) {
|
|
3740
|
+
return [...new Set(keys.flatMap((key) => this.index.requires.get(key)))];
|
|
3741
|
+
}
|
|
3742
|
+
/**
|
|
3743
|
+
* Splits the wire `$select` of a read (non-grouped: the gate already
|
|
3744
|
+
* refused a decoration in a grouped one): `requested` is every declared
|
|
3745
|
+
* decoration key whose sources are visible and that the client named — or,
|
|
3746
|
+
* without a `$select`, all of them (an exclusion map: all minus the excluded
|
|
3747
|
+
* ones). The real `$select` is widened by the requested keys' `requires`;
|
|
3748
|
+
* the paths added only for the hook (`requiresOnly`) are stripped again.
|
|
3749
|
+
* The result keeps the representation of `raw` (array or map).
|
|
3750
|
+
*/
|
|
3751
|
+
plan(raw) {
|
|
3752
|
+
const { keySet, keys } = this.index;
|
|
3753
|
+
const shape = selectShape(raw);
|
|
3754
|
+
if (shape.kind === "all") return {
|
|
3755
|
+
select: void 0,
|
|
3756
|
+
requested: keys.filter((k) => this.visible(k)),
|
|
3757
|
+
requiresOnly: []
|
|
3758
|
+
};
|
|
3759
|
+
const passthrough = {
|
|
3760
|
+
select: raw,
|
|
3761
|
+
requested: [],
|
|
3762
|
+
requiresOnly: []
|
|
3763
|
+
};
|
|
3764
|
+
if (shape.kind === "list") {
|
|
3765
|
+
const named = shape.items.filter((item) => typeof item === "string" && keySet.has(item));
|
|
3766
|
+
if (named.length === 0) return passthrough;
|
|
3767
|
+
const real = shape.items.filter((item) => !(typeof item === "string" && keySet.has(item)));
|
|
3768
|
+
const have = new Set(real.filter((item) => typeof item === "string"));
|
|
3769
|
+
return this.inclusion([...new Set(named)], real, have, (list) => list);
|
|
3770
|
+
}
|
|
3771
|
+
const { map, included, excluded } = shape;
|
|
3772
|
+
if (included.length === 0 && excluded.length > 0) {
|
|
3773
|
+
const skip = new Set(excluded);
|
|
3774
|
+
const requested = keys.filter((key) => !skip.has(key) && this.visible(key));
|
|
3775
|
+
const real = Object.fromEntries(Object.entries(map).filter(([k]) => !keySet.has(k)));
|
|
3776
|
+
const requiresOnly = /* @__PURE__ */ new Set();
|
|
3777
|
+
const remaining = new Set(Object.keys(real));
|
|
3778
|
+
const unexclude = (key) => {
|
|
3779
|
+
requiresOnly.add(key);
|
|
3780
|
+
remaining.delete(key);
|
|
3781
|
+
delete real[key];
|
|
3782
|
+
};
|
|
3783
|
+
for (const path of this.requiresOf(requested)) {
|
|
3784
|
+
const hit = (0, _atscript_db.selfOrAncestor)(path, remaining);
|
|
3785
|
+
if (hit !== void 0) {
|
|
3786
|
+
unexclude(hit);
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
const prefix = `${path}.`;
|
|
3790
|
+
for (const key of remaining) if (key.startsWith(prefix)) unexclude(key);
|
|
3791
|
+
}
|
|
3792
|
+
return {
|
|
3793
|
+
select: Object.keys(real).length > 0 ? real : void 0,
|
|
3794
|
+
requested,
|
|
3795
|
+
requiresOnly: [...requiresOnly]
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
const named = included.filter((k) => keySet.has(k));
|
|
3799
|
+
if (named.length === 0) return passthrough;
|
|
3800
|
+
const real = included.filter((k) => !keySet.has(k));
|
|
3801
|
+
return this.inclusion(named, real, new Set(real), (list) => Object.fromEntries(list.map((k) => [k, 1])));
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* The inclusion forms: `real` (the client's own selection) plus the
|
|
3805
|
+
* requested decorations' `requires` that it lacks, re-emitted by `emit`.
|
|
3806
|
+
*/
|
|
3807
|
+
inclusion(named, real, have, emit) {
|
|
3808
|
+
const requested = named.filter((key) => this.visible(key));
|
|
3809
|
+
const extra = this.requiresOf(requested).filter((path) => (0, _atscript_db.selfOrAncestor)(path, have) === void 0);
|
|
3810
|
+
const { list, added } = this.nonEmptyInclusion([...real, ...extra]);
|
|
3811
|
+
return {
|
|
3812
|
+
select: emit(list),
|
|
3813
|
+
requested,
|
|
3814
|
+
requiresOnly: [...extra.filter((path) => !this.host.preferred.has(path)), ...added],
|
|
3815
|
+
selected: [...have]
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
/**
|
|
3819
|
+
* An inclusion list is never empty (an empty one selects everything): a
|
|
3820
|
+
* request naming only decorations with nothing to read selects the
|
|
3821
|
+
* `preferredId` fields (which the response carries anyway) or, without one,
|
|
3822
|
+
* a single visible field that is stripped again (`added`).
|
|
3823
|
+
*/
|
|
3824
|
+
nonEmptyInclusion(list) {
|
|
3825
|
+
if (list.length > 0) return {
|
|
3826
|
+
list,
|
|
3827
|
+
added: []
|
|
3828
|
+
};
|
|
3829
|
+
const { preferred } = this.host;
|
|
3830
|
+
if (preferred.size > 0) return {
|
|
3831
|
+
list: [...preferred],
|
|
3832
|
+
added: []
|
|
3833
|
+
};
|
|
3834
|
+
const field = this.host.firstVisibleField();
|
|
3835
|
+
return field === void 0 ? {
|
|
3836
|
+
list,
|
|
3837
|
+
added: []
|
|
3838
|
+
} : {
|
|
3839
|
+
list: [field],
|
|
3840
|
+
added: [field]
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
/**
|
|
3844
|
+
* The decoration step once the final projection is known: a requested key
|
|
3845
|
+
* is served only if every `requires` path survived `transformProjection`,
|
|
3846
|
+
* the seal and the preferred-id widening (a policy that strips a source
|
|
3847
|
+
* silently drops the decoration, as it drops a field). `kept` is the final
|
|
3848
|
+
* projection's paths (`null` = every field).
|
|
3849
|
+
*/
|
|
3850
|
+
serve(plan, kept) {
|
|
3851
|
+
const { keys, requires, leavesOf } = this.index;
|
|
3852
|
+
const carried = (path, set) => (0, _atscript_db.selfOrAncestor)(path, set) !== void 0 || (leavesOf.get(path)?.every((leaf) => (0, _atscript_db.selfOrAncestor)(leaf, set) !== void 0) ?? false);
|
|
3853
|
+
const dropPaths = plan.requiresOnly.map((path) => path.split("."));
|
|
3854
|
+
const keepPaths = plan.requiresOnly.map((path) => (plan.selected ?? []).filter((sel) => sel.startsWith(`${path}.`)).map((sel) => sel.split(".")));
|
|
3855
|
+
const selectedPaths = plan.selected?.map((sel) => sel.split("."));
|
|
3856
|
+
if (plan.requested.length === 0) return {
|
|
3857
|
+
served: NO_DECORATIONS,
|
|
3858
|
+
dropKeys: keys,
|
|
3859
|
+
dropPaths,
|
|
3860
|
+
keepPaths,
|
|
3861
|
+
selectedPaths
|
|
3862
|
+
};
|
|
3863
|
+
const keptSet = kept === null ? void 0 : new Set(kept);
|
|
3864
|
+
const served = new Set(plan.requested.filter((key) => keptSet === void 0 || requires.get(key).every((path) => carried(path, keptSet))));
|
|
3865
|
+
return {
|
|
3866
|
+
served,
|
|
3867
|
+
dropKeys: keys.filter((key) => !served.has(key)),
|
|
3868
|
+
dropPaths,
|
|
3869
|
+
keepPaths,
|
|
3870
|
+
selectedPaths
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3873
|
+
/**
|
|
3874
|
+
* The `/meta` envelope with the declared decorations: each one still
|
|
3875
|
+
* present in `decorations` (an overlay may delete props to hide a
|
|
3876
|
+
* decoration per principal) whose sources are visible is kept and gets its
|
|
3877
|
+
* `fields[key]` entry (from its virtual capability-index entry); the others
|
|
3878
|
+
* are pruned, and `decorations` is dropped when none is left. Runs after
|
|
3879
|
+
* `applyMetaOverlay` — an overlay never sees the decoration `fields`
|
|
3880
|
+
* entries. Memoized per input while visibility is not request-scoped.
|
|
3881
|
+
*/
|
|
3882
|
+
meta(meta) {
|
|
3883
|
+
if (!meta.decorations) return meta;
|
|
3884
|
+
const { memo, keySet } = this.index;
|
|
3885
|
+
const memoize = !this.host.scoped;
|
|
3886
|
+
const hit = memoize ? memo.meta.get(meta) : void 0;
|
|
3887
|
+
if (hit) return hit;
|
|
3888
|
+
const capabilities = this.host.capabilities();
|
|
3889
|
+
const props = meta.decorations.type.props ?? {};
|
|
3890
|
+
const kept = {};
|
|
3891
|
+
const fields = { ...meta.fields };
|
|
3892
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
3893
|
+
const cap = keySet.has(key) && this.visible(key) ? capabilities.decorationCap(key) : void 0;
|
|
3894
|
+
if (!cap) continue;
|
|
3895
|
+
kept[key] = prop;
|
|
3896
|
+
fields[key] = {
|
|
3897
|
+
sortable: cap.sortable,
|
|
3898
|
+
filterable: cap.filterable,
|
|
3899
|
+
decoration: true
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3902
|
+
let out;
|
|
3903
|
+
if (Object.keys(kept).length === 0) {
|
|
3904
|
+
const { decorations: _dropped, ...rest } = meta;
|
|
3905
|
+
out = rest;
|
|
3906
|
+
} else out = {
|
|
3907
|
+
...meta,
|
|
3908
|
+
fields,
|
|
3909
|
+
decorations: {
|
|
3910
|
+
...meta.decorations,
|
|
3911
|
+
type: {
|
|
3912
|
+
...meta.decorations.type,
|
|
3913
|
+
props: kept
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
};
|
|
3917
|
+
if (memoize) memo.meta.set(meta, out);
|
|
3918
|
+
return out;
|
|
3919
|
+
}
|
|
3920
|
+
/** The declared interface serialized for `/meta.decorations`, once per class. */
|
|
3921
|
+
serialized(serialize) {
|
|
3922
|
+
return this.index.memo.serialized ??= serialize();
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
/** Removes what a read must not carry — the unserved declared keys and the hook-only paths — from `rows`. */
|
|
3926
|
+
function stripDecorations(rows, read) {
|
|
3927
|
+
const { dropKeys, dropPaths, keepPaths, selectedPaths } = read;
|
|
3928
|
+
if (dropKeys.length === 0 && dropPaths.length === 0) return;
|
|
3929
|
+
const owned = (prefix) => selectedPaths !== void 0 && selectedPaths.some((sel) => prefix.every((part, i) => sel[i] === part));
|
|
3930
|
+
for (const row of rows) {
|
|
3931
|
+
for (const key of dropKeys) delete row[key];
|
|
3932
|
+
dropPaths.forEach((parts, i) => {
|
|
3933
|
+
const keep = keepPaths[i] ?? [];
|
|
3934
|
+
if (keep.length === 0) deleteDescending(row, parts, 0, selectedPaths !== void 0, owned);
|
|
3935
|
+
else pruneExcept(row, parts, 0, keep);
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
/** An object without keys, or an array (of any nesting) holding nothing but such values (what a strip left of a parent). */
|
|
3940
|
+
function isHollow(value) {
|
|
3941
|
+
if (Array.isArray(value)) return value.every((el) => isHollow(el));
|
|
3942
|
+
return (0, _atscript_db.isPlainObject)(value) && Object.keys(value).length === 0;
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Deletes the path `parts` below `value`, descending through arrays of objects
|
|
3946
|
+
* (`items.qty` strips every element's `qty`). With `clean` (an inclusion read),
|
|
3947
|
+
* a parent the strip emptied — or a `null` one — goes too, unless the client
|
|
3948
|
+
* selected something at or below it.
|
|
3949
|
+
*/
|
|
3950
|
+
function deleteDescending(value, parts, depth, clean, owned) {
|
|
3951
|
+
if (Array.isArray(value)) {
|
|
3952
|
+
const dropEmptied = clean && depth > 0 && !owned(parts.slice(0, depth));
|
|
3953
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
3954
|
+
const el = value[i];
|
|
3955
|
+
const wasHollow = isHollow(el);
|
|
3956
|
+
deleteDescending(el, parts, depth, clean, owned);
|
|
3957
|
+
if (dropEmptied && !wasHollow && isHollow(el)) value.splice(i, 1);
|
|
3958
|
+
}
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
if (!(0, _atscript_db.isPlainObject)(value)) return;
|
|
3962
|
+
const key = parts[depth];
|
|
3963
|
+
if (depth === parts.length - 1) {
|
|
3964
|
+
delete value[key];
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3967
|
+
const child = value[key];
|
|
3968
|
+
const prefix = parts.slice(0, depth + 1);
|
|
3969
|
+
if (child === null && clean && !owned(prefix)) {
|
|
3970
|
+
delete value[key];
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
deleteDescending(child, parts, depth + 1, clean, owned);
|
|
3974
|
+
if (clean && isHollow(child) && !owned(prefix)) delete value[key];
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Below `parts`, deletes everything except the branches leading to `keep`
|
|
3978
|
+
* (client-selected descendants), through arrays of objects as well.
|
|
3979
|
+
*/
|
|
3980
|
+
function pruneExcept(value, parts, depth, keep) {
|
|
3981
|
+
if (Array.isArray(value)) {
|
|
3982
|
+
for (const el of value) pruneExcept(el, parts, depth, keep);
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
if (!(0, _atscript_db.isPlainObject)(value)) return;
|
|
3986
|
+
if (depth < parts.length) {
|
|
3987
|
+
pruneExcept(value[parts[depth]], parts, depth + 1, keep);
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
prune(value, parts.length, keep.filter((k) => k.length > parts.length));
|
|
3991
|
+
}
|
|
3992
|
+
function prune(value, depth, branches) {
|
|
3993
|
+
if (Array.isArray(value)) {
|
|
3994
|
+
for (const el of value) prune(el, depth, branches);
|
|
3995
|
+
return;
|
|
3996
|
+
}
|
|
3997
|
+
if (!(0, _atscript_db.isPlainObject)(value)) return;
|
|
3998
|
+
for (const key of Object.keys(value)) {
|
|
3999
|
+
const next = branches.filter((b) => b[depth] === key);
|
|
4000
|
+
if (next.length === 0) delete value[key];
|
|
4001
|
+
else if (next.some((b) => b.length > depth + 1)) prune(value[key], depth + 1, next);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
/** `true` when stripping `read` changes nothing — skip the post-hook step. */
|
|
4005
|
+
function stripsNothing(read) {
|
|
4006
|
+
return read === void 0 || read.dropKeys.length === 0 && read.dropPaths.length === 0;
|
|
4007
|
+
}
|
|
4008
|
+
//#endregion
|
|
3322
4009
|
//#region src/decorators.ts
|
|
3323
4010
|
/**
|
|
3324
4011
|
* DI token under which the {@link AtscriptDbReadable} instance
|
|
@@ -3478,7 +4165,8 @@ const QUERY_CONTROLS = [
|
|
|
3478
4165
|
"filter",
|
|
3479
4166
|
"insights",
|
|
3480
4167
|
...dtoControls(QueryControlsDto),
|
|
3481
|
-
"groupBy"
|
|
4168
|
+
"groupBy",
|
|
4169
|
+
"rowOrder"
|
|
3482
4170
|
];
|
|
3483
4171
|
const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
|
|
3484
4172
|
const ONE_CONTROLS = dtoControls(GetOneControlsDto);
|
|
@@ -3509,6 +4197,12 @@ function writeOnlyError(path, op) {
|
|
|
3509
4197
|
const verdict = writeOnlyVerdict(path, op);
|
|
3510
4198
|
return badRequest(verdict.path, verdict.message);
|
|
3511
4199
|
}
|
|
4200
|
+
/**
|
|
4201
|
+
* Validated `@DbDecorations` per readable and controller class — a
|
|
4202
|
+
* `FOR_EVENT` controller is constructed per event, and must not re-validate
|
|
4203
|
+
* (nor re-serialize) its declaration each time.
|
|
4204
|
+
*/
|
|
4205
|
+
const decorationIndexes = /* @__PURE__ */ new WeakMap();
|
|
3512
4206
|
/** Controller classes already warned that `actionRowScope` has no row identity to match by. */
|
|
3513
4207
|
const warnedNoIdentity = /* @__PURE__ */ new WeakSet();
|
|
3514
4208
|
let AsDbReadableController = _AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
@@ -3540,7 +4234,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3540
4234
|
get capabilities() {
|
|
3541
4235
|
const current = this._capabilities;
|
|
3542
4236
|
if (current && current.signature === FieldCapabilityIndex.adapterSignature(this.readable)) return current;
|
|
3543
|
-
const index = new FieldCapabilityIndex(this.readable, this._writeOnlySet);
|
|
4237
|
+
const index = new FieldCapabilityIndex(this.readable, this._writeOnlySet, this._decorations?.visibleOn);
|
|
3544
4238
|
this._capabilities = index;
|
|
3545
4239
|
return index;
|
|
3546
4240
|
}
|
|
@@ -3577,6 +4271,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3577
4271
|
_derivedSource;
|
|
3578
4272
|
/** `@db.writeOnly` paths of `$with` target readables, collected once per target. */
|
|
3579
4273
|
_targetWriteOnly = /* @__PURE__ */ new WeakMap();
|
|
4274
|
+
/** Own leaf paths per readable (bound + `$with` targets), see {@link _leavesOf}. */
|
|
4275
|
+
_targetLeaves = /* @__PURE__ */ new WeakMap();
|
|
3580
4276
|
_indexFieldPathsCache;
|
|
3581
4277
|
/** {@link _nativeSearch} per request, keyed by the request's parsed controls. */
|
|
3582
4278
|
_nativeSearchByRequest = /* @__PURE__ */ new WeakMap();
|
|
@@ -3600,8 +4296,14 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3600
4296
|
_gateFieldsMemo = /* @__PURE__ */ new WeakMap();
|
|
3601
4297
|
/** `true` when a subclass overrides {@link allowedActions}. */
|
|
3602
4298
|
_hasAllowedActions;
|
|
4299
|
+
/** The class's validated `@DbDecorations` (since 0.1.148), `undefined` when none is declared. */
|
|
4300
|
+
_decorations;
|
|
4301
|
+
/** The decoration plumbing of {@link _decorations} — see `DecorationPlanner`. */
|
|
4302
|
+
_planner;
|
|
3603
4303
|
/** `true` when a subclass overrides {@link actionRowScope} (the gate, `$actions` and `/meta/actions` apply it). */
|
|
3604
4304
|
_hasActionRowScope;
|
|
4305
|
+
/** @internal `true` when a subclass overrides {@link resolveRowIds} (every id-addressed endpoint calls it; since 0.1.148). */
|
|
4306
|
+
[ROW_RESOLVES];
|
|
3605
4307
|
/** `true` when the class declares `@DbActionsFrom` (since 0.1.147). */
|
|
3606
4308
|
_hasDelegations;
|
|
3607
4309
|
/** `transformProjection` is overridden (a delegation's id paths are checked against it). */
|
|
@@ -3625,15 +4327,17 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3625
4327
|
this._writeOnlySet = this._writeOnlyOf(resolved);
|
|
3626
4328
|
this._derivedSource = this._derivedSourcesOf(resolved);
|
|
3627
4329
|
this._invertibleFields = this._collectInvertibleFields();
|
|
4330
|
+
this._decorates = typeof this.decorateRows === "function";
|
|
4331
|
+
this._decorations = this._resolveDecorations(new.target);
|
|
3628
4332
|
this._searchFallbackFields = this._collectSearchFallbackFields();
|
|
3629
4333
|
this._preferredIdSet = new Set(resolved.preferredId ?? []);
|
|
3630
4334
|
this._quantityRefByPath = this._collectQuantityRefs();
|
|
3631
4335
|
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
3632
4336
|
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
3633
|
-
this._decorates = typeof this.decorateRows === "function";
|
|
3634
4337
|
const proto = _AsDbReadableController.prototype;
|
|
3635
4338
|
this._hasRowOverlay = this.transformOne !== proto.transformOne || this.transformFilter !== proto.transformFilter;
|
|
3636
4339
|
this._hasActionRowScope = this.actionRowScope !== proto.actionRowScope;
|
|
4340
|
+
this[ROW_RESOLVES] = this.resolveRowIds !== proto.resolveRowIds;
|
|
3637
4341
|
this._hasProjectionHook = this.transformProjection !== proto.transformProjection;
|
|
3638
4342
|
this._hasAllowedActions = this.allowedActions !== proto.allowedActions;
|
|
3639
4343
|
this._hasDelegations = hasActionDelegations(new.target);
|
|
@@ -3650,6 +4354,37 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3650
4354
|
sealedFor: (readable, prefix = "") => this._sealedFor(readable, prefix)
|
|
3651
4355
|
};
|
|
3652
4356
|
this._idOpts = scoped ? { isFieldVisible: isVisible } : void 0;
|
|
4357
|
+
this._planner = this._decorations && new DecorationPlanner(this._decorations, {
|
|
4358
|
+
isVisible,
|
|
4359
|
+
scoped,
|
|
4360
|
+
preferred: this._preferredIdSet,
|
|
4361
|
+
capabilities: () => this.capabilities,
|
|
4362
|
+
firstVisibleField: () => this._invertibleFields.find((path) => isVisible(path))
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
/**
|
|
4366
|
+
* The class's `@DbDecorations`, validated against the bound readable once
|
|
4367
|
+
* per class and readable (a `[moost-db]` error when invalid); warns once
|
|
4368
|
+
* when `decorateRows` is not implemented.
|
|
4369
|
+
*/
|
|
4370
|
+
_resolveDecorations(ctor) {
|
|
4371
|
+
const meta = getAtscriptDbMate().read(ctor)?.atscript_db_decorations;
|
|
4372
|
+
if (!meta) return void 0;
|
|
4373
|
+
let perCtor = decorationIndexes.get(this.readable);
|
|
4374
|
+
if (!perCtor) decorationIndexes.set(this.readable, perCtor = /* @__PURE__ */ new Map());
|
|
4375
|
+
let index = perCtor.get(ctor);
|
|
4376
|
+
if (!index) {
|
|
4377
|
+
index = buildDecorationIndex(ctor.name, meta, {
|
|
4378
|
+
flatMap: this.readable.flatMap,
|
|
4379
|
+
relations: this.readable.relations,
|
|
4380
|
+
navFields: FieldCapabilityIndex.navPathsOf(this.readable),
|
|
4381
|
+
ownPaths: new Set(this._invertibleFields),
|
|
4382
|
+
writeOnly: this._writeOnlySet
|
|
4383
|
+
});
|
|
4384
|
+
perCtor.set(ctor, index);
|
|
4385
|
+
if (!this._decorates) this.logger.warn(`@DbDecorations declares ${index.keys.join(", ")} but ${ctor.name} does not implement decorateRows() — the keys are never filled`);
|
|
4386
|
+
}
|
|
4387
|
+
return index;
|
|
3653
4388
|
}
|
|
3654
4389
|
/**
|
|
3655
4390
|
* The identifications this request may address rows through (since
|
|
@@ -3675,7 +4410,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3675
4410
|
}
|
|
3676
4411
|
_collectInvertibleFields() {
|
|
3677
4412
|
const out = [];
|
|
3678
|
-
const nav = this.
|
|
4413
|
+
const nav = FieldCapabilityIndex.navPathsOf(this.readable);
|
|
3679
4414
|
for (const fd of this.readable.fieldDescriptors) {
|
|
3680
4415
|
if (fd.ignored) continue;
|
|
3681
4416
|
if ((0, _atscript_db.selfOrAncestor)(fd.path, nav) !== void 0) continue;
|
|
@@ -3765,7 +4500,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3765
4500
|
checkCapabilities(parsed) {
|
|
3766
4501
|
const capabilities = this.capabilities;
|
|
3767
4502
|
const isVisible = this.fieldVisibility.isVisible;
|
|
3768
|
-
const
|
|
4503
|
+
const select = parsed.controls?.$select;
|
|
4504
|
+
const refs = (0, _atscript_db.collectQueryPaths)(parsed, Array.isArray(select) && select.some((item) => typeof item !== "string") || void 0);
|
|
3769
4505
|
if (refs.unsupportedOperator !== void 0) return badRequest(refs.unsupportedOperator, (0, _atscript_db.unsupportedOperatorMessage)(refs.unsupportedOperator));
|
|
3770
4506
|
const relState = { nodes: 0 };
|
|
3771
4507
|
for (const ref of refs.filter) {
|
|
@@ -3781,9 +4517,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3781
4517
|
const recorded = parsed.controls ? this._clientWith.get(parsed.controls) : void 0;
|
|
3782
4518
|
const withRelError = this._relationGate().checkWith(recorded === void 0 ? liveWith : recorded?.tree, relState);
|
|
3783
4519
|
if (withRelError) return withRelError;
|
|
3784
|
-
for (const op of PATH_OPS)
|
|
3785
|
-
const
|
|
3786
|
-
|
|
4520
|
+
for (const op of PATH_OPS) {
|
|
4521
|
+
const gateOp = op === "select" && refs.aggregateMode ? "groupedSelect" : op;
|
|
4522
|
+
for (const path of refs[op]) {
|
|
4523
|
+
const verdict = capabilities.check(path, gateOp, isVisible);
|
|
4524
|
+
if (verdict) return badRequest(verdict.path, verdict.message);
|
|
4525
|
+
}
|
|
3787
4526
|
}
|
|
3788
4527
|
const having = (0, _atscript_db.checkHavingKeys)(refs);
|
|
3789
4528
|
if (having) return badRequest(having.path, having.message);
|
|
@@ -3809,7 +4548,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3809
4548
|
navFields: capabilities.navFields
|
|
3810
4549
|
});
|
|
3811
4550
|
} catch (error) {
|
|
3812
|
-
if (!(error instanceof _atscript_db.DbError)) throw error;
|
|
4551
|
+
if (!(error instanceof _atscript_db.DbError) || error.code !== "INVALID_QUERY") throw error;
|
|
3813
4552
|
const [issue] = error.errors;
|
|
3814
4553
|
return badRequest(issue.path, issue.message);
|
|
3815
4554
|
}
|
|
@@ -3987,12 +4726,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3987
4726
|
const sub = nested.$select ?? rel.$select;
|
|
3988
4727
|
return {
|
|
3989
4728
|
...nested,
|
|
3990
|
-
$select: this._sealSelect(sub, sealed)
|
|
4729
|
+
$select: this._sealSelect(sub, sealed, target)
|
|
3991
4730
|
};
|
|
3992
4731
|
});
|
|
3993
4732
|
const out = {
|
|
3994
4733
|
...controls,
|
|
3995
|
-
$select: this._sealSelect(select, vis.sealedFor(this.readable))
|
|
4734
|
+
$select: this._sealSelect(select, vis.sealedFor(this.readable), this.readable)
|
|
3996
4735
|
};
|
|
3997
4736
|
if ($with !== controls.$with) out.$with = $with;
|
|
3998
4737
|
return out;
|
|
@@ -4261,34 +5000,24 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4261
5000
|
* The rows the row-level action `actionName` may run on (since 0.1.145),
|
|
4262
5001
|
* as an extra row filter; `undefined` or `{}` = no restriction (the
|
|
4263
5002
|
* default). Enforced by the action gate — the action's ids / rows are
|
|
4264
|
-
* loaded under {@link rowOverlay}, then checked against this filter
|
|
4265
|
-
*
|
|
5003
|
+
* loaded under {@link rowOverlay}, then checked against this filter (or —
|
|
5004
|
+
* without an overlay — the filter joins the load), so an id outside it gets
|
|
5005
|
+
* the same 404 "Row not found for action identifier"
|
|
4266
5006
|
* as a missing one — and reflected in `$actions` and
|
|
4267
5007
|
* `GET /meta/actions`, which list the action only on rows inside it.
|
|
4268
5008
|
*
|
|
4269
|
-
*
|
|
4270
|
-
*
|
|
4271
|
-
*
|
|
4272
|
-
*
|
|
4273
|
-
*
|
|
4274
|
-
*
|
|
4275
|
-
*
|
|
4276
|
-
*
|
|
4277
|
-
*
|
|
4278
|
-
*
|
|
4279
|
-
*
|
|
4280
|
-
*
|
|
4281
|
-
* one evaluation (memoize on it with a `WeakMap`).
|
|
4282
|
-
* - Candidates are already inside the row overlay — ids that do not exist
|
|
4283
|
-
* or fall outside it never reach the hook.
|
|
4284
|
-
* - The result only restricts (`ids ∧ rowOverlay ∧ scope`); a throw fails
|
|
4285
|
-
* the request — never a silent "allow".
|
|
4286
|
-
* - The filter runs straight against the bound readable: it may use
|
|
4287
|
-
* fields {@link hasField} hides, and nothing of it (nor of
|
|
4288
|
-
* `ctx.loadRows`) reaches the response. Equal filters — the same object,
|
|
4289
|
-
* or structurally equal ones — share one id-only query.
|
|
4290
|
-
* - Runs after {@link prepareRequest} and, on the action route, after the
|
|
4291
|
-
* request body is read (it needs the ids).
|
|
5009
|
+
* The hook receives the candidate rows (`ctx`), so a scope can depend on
|
|
5010
|
+
* them — what `ctx.purpose`, `ctx.ids` and `ctx.loadRows` hold, and when the
|
|
5011
|
+
* hook is asked before any load, is documented on {@link TDbActionScopeContext}.
|
|
5012
|
+
* Called only with at least one candidate, at most once per action per
|
|
5013
|
+
* evaluation. An answer restricting nothing (`undefined`, `null`, `{}`)
|
|
5014
|
+
* makes the gate load no row at all; a restriction is folded into the one
|
|
5015
|
+
* row load. The result only restricts (`ids ∧ rowOverlay ∧ scope`); a throw
|
|
5016
|
+
* fails the request — never a silent "allow". The filter runs straight
|
|
5017
|
+
* against the bound readable: it may use fields {@link hasField} hides, and
|
|
5018
|
+
* nothing of it reaches the response. Runs after {@link prepareRequest} and,
|
|
5019
|
+
* on the action route, after the request body is read and
|
|
5020
|
+
* {@link resolveRowIds}.
|
|
4292
5021
|
*
|
|
4293
5022
|
* Not overriding it costs nothing; a one-parameter override keeps working.
|
|
4294
5023
|
* moost-db always passes `ctx` — it is optional in the signature only so
|
|
@@ -4354,6 +5083,37 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4354
5083
|
transformProjection(projection) {
|
|
4355
5084
|
return projection;
|
|
4356
5085
|
}
|
|
5086
|
+
/**
|
|
5087
|
+
* The shared projection step of `/query`, `/pages`, `/geo` and `/one`:
|
|
5088
|
+
* splits the declared decoration keys out of the wire `$select`
|
|
5089
|
+
* ({@link DecorationPlanner.plan}), runs {@link transformProjection} on the
|
|
5090
|
+
* rest (decoration keys never reach it — a permission layer needs no
|
|
5091
|
+
* change; their `requires` paths are added so the hook's inputs are read),
|
|
5092
|
+
* then seals every projection level. `finish` completes it once the
|
|
5093
|
+
* endpoint is past its own checks: the preferred-id widening (an
|
|
5094
|
+
* `HttpError` for a mixed `$select`) and the decoration step — what every
|
|
5095
|
+
* read endpoint then passes to {@link _runReadWithActions}.
|
|
5096
|
+
*/
|
|
5097
|
+
async _projectRead(controls) {
|
|
5098
|
+
const plan = this._planner?.plan(controls.$select);
|
|
5099
|
+
const transformed = await this.transformProjection(plan ? plan.select : controls.$select);
|
|
5100
|
+
const sealed = this._sealControls(controls, transformed);
|
|
5101
|
+
const finish = () => {
|
|
5102
|
+
const select = this.widenPreferredIdProjection(sealed.$select);
|
|
5103
|
+
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
5104
|
+
let kept;
|
|
5105
|
+
const keptPaths = () => kept === void 0 ? kept = this._resolveProjectionForAugmenter(select) : kept;
|
|
5106
|
+
return {
|
|
5107
|
+
select,
|
|
5108
|
+
kept: keptPaths,
|
|
5109
|
+
read: plan && this._planner.serve(plan, keptPaths())
|
|
5110
|
+
};
|
|
5111
|
+
};
|
|
5112
|
+
return {
|
|
5113
|
+
sealed,
|
|
5114
|
+
finish
|
|
5115
|
+
};
|
|
5116
|
+
}
|
|
4357
5117
|
widenPreferredIdProjection(projection) {
|
|
4358
5118
|
const widened = this.widenQuantityRefProjection(projection);
|
|
4359
5119
|
if (widened instanceof _moostjs_event_http.HttpError) return widened;
|
|
@@ -4375,12 +5135,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4375
5135
|
return out;
|
|
4376
5136
|
}
|
|
4377
5137
|
_widenMapProjection(projection) {
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
const included =
|
|
4381
|
-
const excluded =
|
|
4382
|
-
for (const [k, v] of entries) if (v === 1 || v === true) included.add(k);
|
|
4383
|
-
else if (v === 0 || v === false) excluded.add(k);
|
|
5138
|
+
if (Object.keys(projection).length === 0) return projection;
|
|
5139
|
+
const shape = mapShape(projection);
|
|
5140
|
+
const included = new Set(shape.included);
|
|
5141
|
+
const excluded = new Set(shape.excluded);
|
|
4384
5142
|
if (included.size > 0 && excluded.size > 0) return new _moostjs_event_http.HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
|
|
4385
5143
|
if (excluded.size === 0) {
|
|
4386
5144
|
let allPresent = true;
|
|
@@ -4436,12 +5194,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4436
5194
|
return [...projection, ...toAdd];
|
|
4437
5195
|
}
|
|
4438
5196
|
_widenQuantityMapProjection(projection) {
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
const included =
|
|
4442
|
-
const excluded =
|
|
4443
|
-
for (const [k, v] of entries) if (v === 1 || v === true) included.add(k);
|
|
4444
|
-
else if (v === 0 || v === false) excluded.add(k);
|
|
5197
|
+
if (Object.keys(projection).length === 0) return projection;
|
|
5198
|
+
const shape = mapShape(projection);
|
|
5199
|
+
const included = new Set(shape.included);
|
|
5200
|
+
const excluded = new Set(shape.excluded);
|
|
4445
5201
|
if (included.size > 0 && excluded.size > 0) return new _moostjs_event_http.HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
|
|
4446
5202
|
if (excluded.size === 0) {
|
|
4447
5203
|
const toAdd = [];
|
|
@@ -4456,22 +5212,11 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4456
5212
|
}
|
|
4457
5213
|
/** Normalize a post-`widenPreferredIdProjection` $select into `string[] | null` (`null` = all fields). */
|
|
4458
5214
|
_resolveProjectionForAugmenter(select) {
|
|
4459
|
-
|
|
4460
|
-
if (
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
seen.add(item);
|
|
4465
|
-
out.push(item);
|
|
4466
|
-
}
|
|
4467
|
-
return out;
|
|
4468
|
-
}
|
|
4469
|
-
const obj = select;
|
|
4470
|
-
const included = [];
|
|
4471
|
-
const excluded = [];
|
|
4472
|
-
for (const [k, v] of Object.entries(obj)) if (v === 1 || v === true) included.push(k);
|
|
4473
|
-
else if (v === 0 || v === false) excluded.push(k);
|
|
4474
|
-
if (included.length > 0 && excluded.length === 0) return included;
|
|
5215
|
+
const shape = selectShape(select);
|
|
5216
|
+
if (shape.kind === "all") return null;
|
|
5217
|
+
if (shape.kind === "list") return [...new Set(shape.items.filter((item) => typeof item === "string"))];
|
|
5218
|
+
const { included, excluded } = shape;
|
|
5219
|
+
if (included.length > 0 && excluded.length === 0) return [...included];
|
|
4475
5220
|
if (excluded.length > 0 && included.length === 0) return this._invertExclusion(new Set(excluded));
|
|
4476
5221
|
throw new _moostjs_event_http.HttpError(500, "[moost-db] mixed inclusion/exclusion projection reached augmenter; widenPreferredIdProjection should have rejected it");
|
|
4477
5222
|
}
|
|
@@ -4517,7 +5262,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4517
5262
|
}
|
|
4518
5263
|
return result;
|
|
4519
5264
|
}
|
|
4520
|
-
async _prepareAugmentation(controls,
|
|
5265
|
+
async _prepareAugmentation(controls, projected) {
|
|
4521
5266
|
if (!controls.$actions) return null;
|
|
4522
5267
|
const [own, delegations] = await Promise.all([this._resolveAugmentEnvelopes(), this._activeDelegations()]);
|
|
4523
5268
|
if (own === null && delegations.length === 0) return null;
|
|
@@ -4527,7 +5272,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4527
5272
|
scopeOverlay = this.rowOverlay();
|
|
4528
5273
|
scopeOverlay.catch(() => {});
|
|
4529
5274
|
}
|
|
4530
|
-
let resolvedProjection =
|
|
5275
|
+
let resolvedProjection = projected.kept();
|
|
4531
5276
|
let widenedSelect = resolvedProjection === null ? null : this._widenSelectForActions(envelopes, resolvedProjection);
|
|
4532
5277
|
if (resolvedProjection !== null && delegations.length > 0) {
|
|
4533
5278
|
const base = widenedSelect ?? resolvedProjection;
|
|
@@ -4632,6 +5377,14 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4632
5377
|
[ACTION_OVERLAY]() {
|
|
4633
5378
|
return this.rowOverlay();
|
|
4634
5379
|
}
|
|
5380
|
+
/**
|
|
5381
|
+
* @internal {@link resolveRowIds} for an action's validated ids (since
|
|
5382
|
+
* 0.1.148): the output validated, duplicate identities collapsed, and the
|
|
5383
|
+
* ids as the client sent them kept for the error bodies and summaries.
|
|
5384
|
+
*/
|
|
5385
|
+
async [ROW_RESOLVE_IDS](ids, ctx) {
|
|
5386
|
+
return applyResolvedIds(ids, await this._runResolveRowIds(ids, ctx));
|
|
5387
|
+
}
|
|
4635
5388
|
/** @internal {@link actionRowScope} for the gate's loaded candidates (since 0.1.147). */
|
|
4636
5389
|
[ACTION_SCOPE](action, ctx) {
|
|
4637
5390
|
return this._actionScope(action, ctx);
|
|
@@ -4760,9 +5513,11 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4760
5513
|
/**
|
|
4761
5514
|
* `select` without the `sealed` paths (see {@link _sealControls}); an
|
|
4762
5515
|
* exclusion of them is forced when there is no projection, or when every
|
|
4763
|
-
* requested path was sealed.
|
|
5516
|
+
* requested path was sealed. An inclusion naming a PARENT of a sealed path
|
|
5517
|
+
* (`secret` over a write-only `secret.hash`) is replaced by the parent's
|
|
5518
|
+
* unsealed leaves, so a sealed descendant never rides along with it.
|
|
4764
5519
|
*/
|
|
4765
|
-
_sealSelect(select, writeOnly) {
|
|
5520
|
+
_sealSelect(select, writeOnly, readable) {
|
|
4766
5521
|
if (writeOnly.size === 0) return select;
|
|
4767
5522
|
const exclusion = () => {
|
|
4768
5523
|
const out = {};
|
|
@@ -4770,29 +5525,47 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4770
5525
|
return out;
|
|
4771
5526
|
};
|
|
4772
5527
|
if (select === void 0) return exclusion();
|
|
5528
|
+
const expand = (path) => {
|
|
5529
|
+
if (writeOnly.has(path)) return [];
|
|
5530
|
+
const prefix = `${path}.`;
|
|
5531
|
+
let parent = false;
|
|
5532
|
+
for (const sealed of writeOnly) if (sealed.startsWith(prefix)) {
|
|
5533
|
+
parent = true;
|
|
5534
|
+
break;
|
|
5535
|
+
}
|
|
5536
|
+
if (!parent) return [path];
|
|
5537
|
+
return this._leavesOf(readable).filter((leaf) => leaf.startsWith(prefix) && (0, _atscript_db.selfOrAncestor)(leaf, writeOnly) === void 0);
|
|
5538
|
+
};
|
|
4773
5539
|
if (Array.isArray(select)) {
|
|
4774
|
-
const kept =
|
|
5540
|
+
const kept = [];
|
|
5541
|
+
for (const item of select) if (typeof item === "string") kept.push(...expand(item));
|
|
5542
|
+
else if (!writeOnly.has(item.$field ?? "")) kept.push(item);
|
|
4775
5543
|
return kept.length > 0 ? kept : exclusion();
|
|
4776
5544
|
}
|
|
4777
5545
|
const entries = Object.entries(select);
|
|
4778
5546
|
if (entries.length > 0 && (entries[0][1] === 1 || entries[0][1] === true)) {
|
|
4779
5547
|
const out = {};
|
|
4780
|
-
for (const [k, v] of entries)
|
|
5548
|
+
for (const [k, v] of entries) for (const path of expand(k)) out[path] = v;
|
|
4781
5549
|
return Object.keys(out).length > 0 ? out : exclusion();
|
|
4782
5550
|
}
|
|
4783
5551
|
const out = { ...select };
|
|
4784
5552
|
for (const f of writeOnly) out[f] = 0;
|
|
4785
5553
|
return out;
|
|
4786
5554
|
}
|
|
4787
|
-
/**
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
const
|
|
4794
|
-
|
|
5555
|
+
/** Own leaf field paths (no navigation, no ignored field) of `readable`, once per readable. */
|
|
5556
|
+
_leavesOf(readable) {
|
|
5557
|
+
let leaves = this._targetLeaves.get(readable);
|
|
5558
|
+
if (!leaves) {
|
|
5559
|
+
const paths = [...readable.flatMap?.keys() ?? []].filter((path) => path !== "");
|
|
5560
|
+
const parents = /* @__PURE__ */ new Set();
|
|
5561
|
+
for (const path of paths) for (let at = path.indexOf("."); at >= 0; at = path.indexOf(".", at + 1)) parents.add(path.slice(0, at));
|
|
5562
|
+
const nav = readable.navFields ?? /* @__PURE__ */ new Set();
|
|
5563
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
5564
|
+
for (const fd of readable.fieldDescriptors) if (fd.ignored) ignored.add(fd.path);
|
|
5565
|
+
leaves = paths.filter((path) => !parents.has(path) && (0, _atscript_db.selfOrAncestor)(path, nav) === void 0 && (0, _atscript_db.selfOrAncestor)(path, ignored) === void 0);
|
|
5566
|
+
this._targetLeaves.set(readable, leaves);
|
|
4795
5567
|
}
|
|
5568
|
+
return leaves;
|
|
4796
5569
|
}
|
|
4797
5570
|
/**
|
|
4798
5571
|
* Merges the `$search` fallback into the filter: a case-insensitive literal
|
|
@@ -4862,13 +5635,13 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4862
5635
|
* subclass implements it. Returns the hook's result — `undefined`, with no
|
|
4863
5636
|
* promise or microtask, when there is no hook or it is synchronous.
|
|
4864
5637
|
*/
|
|
4865
|
-
_finishRows(rows, prep, ctx) {
|
|
5638
|
+
_finishRows(rows, prep, ctx, read) {
|
|
4866
5639
|
const overlay = prep?.scopeOverlay;
|
|
4867
|
-
if (!prep || !overlay && prep.delegations.length === 0) return this._augmentAndDecorate(rows, prep, ctx);
|
|
5640
|
+
if (!prep || !overlay && prep.delegations.length === 0) return this._augmentAndDecorate(rows, prep, ctx, read);
|
|
4868
5641
|
return (async () => {
|
|
4869
5642
|
const names = prep.envelopes.map((e) => e.info.name);
|
|
4870
5643
|
const [masks, delegated] = await Promise.all([overlay ? this._scopeMasks(rows, names, "rows", overlay) : void 0, Promise.all(prep.delegations.map((d) => this._delegatedRowVerdicts(rows, d)))]);
|
|
4871
|
-
await this._augmentAndDecorate(rows, prep, ctx, masks, delegated);
|
|
5644
|
+
await this._augmentAndDecorate(rows, prep, ctx, read, masks, delegated);
|
|
4872
5645
|
})();
|
|
4873
5646
|
}
|
|
4874
5647
|
/**
|
|
@@ -4876,7 +5649,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4876
5649
|
* ones (`delegated`: per delegation, per row) — then {@link decorateRows}
|
|
4877
5650
|
* when implemented.
|
|
4878
5651
|
*/
|
|
4879
|
-
_augmentAndDecorate(rows, prep, ctx, outOfScope, delegated) {
|
|
5652
|
+
_augmentAndDecorate(rows, prep, ctx, read, outOfScope, delegated) {
|
|
4880
5653
|
if (prep) {
|
|
4881
5654
|
augmentRowsWithActions({
|
|
4882
5655
|
envelopes: prep.envelopes,
|
|
@@ -4887,7 +5660,11 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4887
5660
|
});
|
|
4888
5661
|
if (prep.delegations.length > 0) mergeDelegatedActions(rows, delegated ?? []);
|
|
4889
5662
|
}
|
|
4890
|
-
|
|
5663
|
+
const decorated = this._decorates ? this.decorateRows(rows, ctx) : void 0;
|
|
5664
|
+
if (!read || stripsNothing(read)) return decorated;
|
|
5665
|
+
const strip = () => stripDecorations(rows, read);
|
|
5666
|
+
if (decorated === void 0) return strip();
|
|
5667
|
+
return Promise.resolve(decorated).then(strip);
|
|
4891
5668
|
}
|
|
4892
5669
|
/**
|
|
4893
5670
|
* The app of the current event, through DI — never the one this
|
|
@@ -4963,9 +5740,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4963
5740
|
*/
|
|
4964
5741
|
resolveMeta() {
|
|
4965
5742
|
const own = super.resolveMeta();
|
|
4966
|
-
if (!this._hasDelegations && !this._hasFieldOverridden) return own;
|
|
5743
|
+
if (!this._hasDelegations && !this._hasFieldOverridden && !this._planner) return own;
|
|
4967
5744
|
return (async () => {
|
|
4968
|
-
const [
|
|
5745
|
+
const [overlaid, delegated] = await Promise.all([own, this._hasDelegations ? this._delegatedInfos() : []]);
|
|
5746
|
+
const meta = this._planner ? this._planner.meta(overlaid) : overlaid;
|
|
4969
5747
|
const visible = this._applyIndexVisibility(meta);
|
|
4970
5748
|
return delegated.length > 0 ? {
|
|
4971
5749
|
...visible,
|
|
@@ -5019,7 +5797,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5019
5797
|
/** @internal Source side of a delegation: `GET /meta/actions` for one id, `names` only. */
|
|
5020
5798
|
async [AVAILABLE_ACTIONS](id, names) {
|
|
5021
5799
|
await this.parseRequest("availableActions");
|
|
5022
|
-
return this.
|
|
5800
|
+
return (await this._availableResolved(id, names)).own;
|
|
5023
5801
|
}
|
|
5024
5802
|
/** @internal Source side of a delegation: the `names` the caller may run (`allowedActions`). */
|
|
5025
5803
|
async [ALLOWED_ACTIONS](names) {
|
|
@@ -5038,8 +5816,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5038
5816
|
* `$actions=true`, then run {@link decorateRows}. Caller dispatches the
|
|
5039
5817
|
* strategy to its read-method family (count vs no-count).
|
|
5040
5818
|
*/
|
|
5041
|
-
async _runReadWithActions(endpoint, queryObj, controls,
|
|
5042
|
-
const [prep, strategy] = await Promise.all([this._prepareAugmentation(controls,
|
|
5819
|
+
async _runReadWithActions(endpoint, queryObj, controls, projected, exec) {
|
|
5820
|
+
const [prep, strategy] = await Promise.all([this._prepareAugmentation(controls, projected), this._resolveReadStrategy(controls)]);
|
|
5043
5821
|
const result = await exec(prep?.widenedSelect ? {
|
|
5044
5822
|
...queryObj,
|
|
5045
5823
|
controls: {
|
|
@@ -5049,19 +5827,120 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5049
5827
|
} : queryObj, strategy);
|
|
5050
5828
|
const pending = this._finishRows(result.data, prep, {
|
|
5051
5829
|
endpoint,
|
|
5052
|
-
projection: select,
|
|
5053
|
-
controls
|
|
5054
|
-
|
|
5830
|
+
projection: projected.select,
|
|
5831
|
+
controls,
|
|
5832
|
+
decorations: projected.read?.served ?? NO_DECORATIONS
|
|
5833
|
+
}, projected.read);
|
|
5055
5834
|
if (pending) await pending;
|
|
5056
5835
|
return result;
|
|
5057
5836
|
}
|
|
5058
5837
|
/**
|
|
5838
|
+
* Maps the ids an id-addressed endpoint received to the rows' current ids
|
|
5839
|
+
* (since 0.1.148) — the seam for stale or alias ids, e.g. a natural key that
|
|
5840
|
+
* was renamed. Called once per request, after {@link prepareRequest} and
|
|
5841
|
+
* after the request's own validation, before anything reads the row, by:
|
|
5842
|
+
*
|
|
5843
|
+
* | `ctx.purpose` | endpoint | `ids` |
|
|
5844
|
+
* | --- | --- | --- |
|
|
5845
|
+
* | `"one"` | `GET /one/:id`, `GET /one?…` | one id: the path string, or the `?`-form identification object |
|
|
5846
|
+
* | `"available"` | `GET /meta/actions/:id`, `?…` (and a view asking its source) | one id, as above |
|
|
5847
|
+
* | `"remove"` | `DELETE /:id`, `DELETE /?…` (`AsDbController`) | one id, as above |
|
|
5848
|
+
* | `"action"` | an action route | the body's validated ids (one for a `'row'` action) |
|
|
5849
|
+
*
|
|
5850
|
+
* Contract:
|
|
5851
|
+
*
|
|
5852
|
+
* - Return one id per input id, index-aligned (anything else is a 500). An
|
|
5853
|
+
* id that already names a row must come back UNCHANGED — the current
|
|
5854
|
+
* holder of a key wins over an alias; so must an id you cannot resolve
|
|
5855
|
+
* (never throw for an unknown alias: a custom error is an oracle — the
|
|
5856
|
+
* endpoint then answers its normal miss).
|
|
5857
|
+
* - The output is validated (a server bug is a 500, never a client 400): a
|
|
5858
|
+
* scalar is resolved like a path scalar (primary key first, then the
|
|
5859
|
+
* visible unique keys, inside the row overlay); an object must be one of
|
|
5860
|
+
* the visible identifications ({@link idSource}); an `"action"` id must
|
|
5861
|
+
* be such an object.
|
|
5862
|
+
* - The resolved id is never trusted for access: the endpoint still reads
|
|
5863
|
+
* or deletes it under {@link rowOverlay} and the visible identifications.
|
|
5864
|
+
* `ctx.overlay` is that overlay — resolve INSIDE it when an alias could
|
|
5865
|
+
* name several rows, so a row the caller can't reach never shadows one
|
|
5866
|
+
* they can. Don't log or return the canonical id in errors.
|
|
5867
|
+
* - Handlers (`@DbActionID()`, `useDbActionId()`), {@link rowOverlay} reads,
|
|
5868
|
+
* {@link actionRowScope} and `onRemove` / `guardRemove` receive the
|
|
5869
|
+
* resolved ids; error bodies and `summary()` echo the ids the client sent.
|
|
5870
|
+
* - Write bodies (`POST` / `PUT` / `PATCH`) are not resolved — use
|
|
5871
|
+
* `onWrite`. Not called by value-help controllers, `$actions` on a read
|
|
5872
|
+
* or a query target. Not overriding it costs nothing.
|
|
5873
|
+
*
|
|
5874
|
+
* It is NOT overridden by {@link resolveRowFilter}, which does not take part
|
|
5875
|
+
* in `/one` for real tables and views.
|
|
5876
|
+
*
|
|
5877
|
+
* ```ts
|
|
5878
|
+
* protected async resolveRowIds(ids: readonly TDbRowIdInput[], ctx: TDbRowIdsContext) {
|
|
5879
|
+
* return Promise.all(ids.map(async (id) => {
|
|
5880
|
+
* const key = typeof id === "object" ? id.code : id
|
|
5881
|
+
* if (typeof key !== "string") return id
|
|
5882
|
+
* // the current holder of the key wins; consult the alias table on a miss
|
|
5883
|
+
* // resolve INSIDE the overlay: a row the caller cannot reach never wins
|
|
5884
|
+
* const inScope = (code: string) =>
|
|
5885
|
+
* this.readable.count({ filter: ctx.overlay ? { $and: [{ code }, ctx.overlay] } : { code } })
|
|
5886
|
+
* if (await inScope(key)) return id
|
|
5887
|
+
* const alias = await aliases.findOne({ filter: { oldCode: key } })
|
|
5888
|
+
* if (!alias || !(await inScope(alias.newCode))) return id
|
|
5889
|
+
* return typeof id === "object" ? { code: alias.newCode } : alias.newCode
|
|
5890
|
+
* }))
|
|
5891
|
+
* }
|
|
5892
|
+
* ```
|
|
5893
|
+
*
|
|
5894
|
+
* @since 0.1.148
|
|
5895
|
+
*/
|
|
5896
|
+
resolveRowIds(ids, _ctx) {
|
|
5897
|
+
return ids;
|
|
5898
|
+
}
|
|
5899
|
+
/** {@link resolveRowIds} with its output validated (a server bug is a 500). */
|
|
5900
|
+
async _runResolveRowIds(ids, ctx) {
|
|
5901
|
+
const out = await this.resolveRowIds(ids, ctx);
|
|
5902
|
+
if (!Array.isArray(out) || out.length !== ids.length) throw new _moostjs_event_http.HttpError(500, "resolveRowIds must return one id per request id");
|
|
5903
|
+
const source = this.idSource;
|
|
5904
|
+
for (const id of out) {
|
|
5905
|
+
if ((typeof id === "string" || typeof id === "number" || typeof id === "boolean") && ctx.purpose !== "action") continue;
|
|
5906
|
+
try {
|
|
5907
|
+
validateSingleId(id, source, { strictTypes: ctx.purpose === "action" });
|
|
5908
|
+
} catch (error) {
|
|
5909
|
+
if (error instanceof _atscript_typescript_utils.ValidatorError) throw new _moostjs_event_http.HttpError(500, "resolveRowIds returned an invalid id");
|
|
5910
|
+
throw error;
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
return out;
|
|
5914
|
+
}
|
|
5915
|
+
/**
|
|
5916
|
+
* @internal The one id of an id-addressed endpoint through
|
|
5917
|
+
* {@link resolveRowIds} (identity, at no cost, when it is not overridden)
|
|
5918
|
+
* and the row overlay it was resolved inside — computed once, for the
|
|
5919
|
+
* endpoint's read or delete under that overlay.
|
|
5920
|
+
*/
|
|
5921
|
+
async _resolveWithOverlay(id, purpose) {
|
|
5922
|
+
const overlay = await this.rowOverlay();
|
|
5923
|
+
if (!this[ROW_RESOLVES]) return {
|
|
5924
|
+
id,
|
|
5925
|
+
overlay
|
|
5926
|
+
};
|
|
5927
|
+
const [resolved] = await this._runResolveRowIds([id], {
|
|
5928
|
+
purpose,
|
|
5929
|
+
overlay
|
|
5930
|
+
});
|
|
5931
|
+
return {
|
|
5932
|
+
id: resolved,
|
|
5933
|
+
overlay
|
|
5934
|
+
};
|
|
5935
|
+
}
|
|
5936
|
+
/**
|
|
5059
5937
|
* The filter addressing exactly the ONE row `id` means — the readable's
|
|
5060
5938
|
* PK-first `resolveRowFilter` (since 0.1.143) under this request's
|
|
5061
5939
|
* identifications (`_idOpts`). `scope` (the row overlay) restricts which
|
|
5062
5940
|
* rows count while the id is pinned, so a row outside it never shadows one
|
|
5063
5941
|
* inside it. Readables without it (partial mocks) fall back to
|
|
5064
|
-
* `resolveIdFilter`.
|
|
5942
|
+
* `resolveIdFilter`. Not an alias seam: `/one` reads through `findOneByRow`
|
|
5943
|
+
* on every real table or view — map stale ids in {@link resolveRowIds}.
|
|
5065
5944
|
*/
|
|
5066
5945
|
resolveRowFilter(id, scope) {
|
|
5067
5946
|
const readable = this.readable;
|
|
@@ -5135,8 +6014,15 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5135
6014
|
if (groupBy?.length && controls.$vector !== void 0) return new _moostjs_event_http.HttpError(400, "Cannot combine $vector and $groupBy in the same query");
|
|
5136
6015
|
const error = this.validateParsed(parsed, "query");
|
|
5137
6016
|
if (error) return error;
|
|
5138
|
-
if (groupBy?.length) {
|
|
5139
|
-
const
|
|
6017
|
+
if (groupBy?.length && this._writeOnlySet.size > 0) {
|
|
6018
|
+
const refs = (0, _atscript_db.collectQueryPaths)(parsed, true);
|
|
6019
|
+
const sealed = [
|
|
6020
|
+
...refs.groupBy,
|
|
6021
|
+
...refs.aggregate,
|
|
6022
|
+
...refs.bucket,
|
|
6023
|
+
...refs.select,
|
|
6024
|
+
...refs.sort
|
|
6025
|
+
].find((path) => this._writeOnlySet.has(path) && this.fieldVisibility.isVisible(path));
|
|
5140
6026
|
if (sealed) return new _moostjs_event_http.HttpError(400, `Field "${sealed}" is @db.writeOnly and cannot be aggregated`);
|
|
5141
6027
|
}
|
|
5142
6028
|
const gateError = this.checkCapabilities(parsed);
|
|
@@ -5150,9 +6036,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5150
6036
|
insights: parsed.insights
|
|
5151
6037
|
});
|
|
5152
6038
|
}
|
|
5153
|
-
const [transformedFilter,
|
|
6039
|
+
const [transformedFilter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
|
|
5154
6040
|
const filter = this.applySearchFallback(transformedFilter, controls);
|
|
5155
|
-
const sealed = this._sealControls(controls, transformedSelect);
|
|
5156
6041
|
if (controls.$count) return this.readable.count({
|
|
5157
6042
|
filter,
|
|
5158
6043
|
controls: {
|
|
@@ -5160,19 +6045,19 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5160
6045
|
$select: sealed.$select
|
|
5161
6046
|
}
|
|
5162
6047
|
});
|
|
5163
|
-
const
|
|
5164
|
-
if (
|
|
6048
|
+
const projected = finish();
|
|
6049
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
5165
6050
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
5166
6051
|
const queryObj = {
|
|
5167
6052
|
filter,
|
|
5168
6053
|
controls: {
|
|
5169
6054
|
...sealed,
|
|
5170
|
-
$select: select,
|
|
6055
|
+
$select: projected.select,
|
|
5171
6056
|
$limit: controls.$limit || 1e3,
|
|
5172
6057
|
$threshold: threshold
|
|
5173
6058
|
}
|
|
5174
6059
|
};
|
|
5175
|
-
return (await this._runReadWithActions("query", queryObj, controls,
|
|
6060
|
+
return (await this._runReadWithActions("query", queryObj, controls, projected, async (q, strategy) => {
|
|
5176
6061
|
switch (strategy.kind) {
|
|
5177
6062
|
case "vector": return { data: await (strategy.vectorField ? this.readable.vectorSearch(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearch(strategy.vector, q)) };
|
|
5178
6063
|
case "search": return { data: await this.readable.search(strategy.term, q, strategy.index) };
|
|
@@ -5193,23 +6078,22 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5193
6078
|
const page = Math.max(Number(controls.$page || 1), 1);
|
|
5194
6079
|
const size = Math.max(Number(controls.$size || 10), 1);
|
|
5195
6080
|
const skip = (page - 1) * size;
|
|
5196
|
-
const [transformedFilter,
|
|
6081
|
+
const [transformedFilter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
|
|
5197
6082
|
const filter = this.applySearchFallback(transformedFilter, controls);
|
|
5198
|
-
const
|
|
5199
|
-
|
|
5200
|
-
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
6083
|
+
const projected = finish();
|
|
6084
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
5201
6085
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
5202
6086
|
const query = {
|
|
5203
6087
|
filter,
|
|
5204
6088
|
controls: {
|
|
5205
6089
|
...sealed,
|
|
5206
|
-
$select: select,
|
|
6090
|
+
$select: projected.select,
|
|
5207
6091
|
$skip: skip,
|
|
5208
6092
|
$limit: size,
|
|
5209
6093
|
$threshold: threshold
|
|
5210
6094
|
}
|
|
5211
6095
|
};
|
|
5212
|
-
const result = await this._runReadWithActions("pages", query, controls,
|
|
6096
|
+
const result = await this._runReadWithActions("pages", query, controls, projected, async (q, strategy) => {
|
|
5213
6097
|
switch (strategy.kind) {
|
|
5214
6098
|
case "vector": return strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q);
|
|
5215
6099
|
case "search": return this.readable.searchWithCount(strategy.term, q, strategy.index);
|
|
@@ -5251,10 +6135,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5251
6135
|
const gateError = this.checkCapabilities(parsed);
|
|
5252
6136
|
if (gateError) return gateError;
|
|
5253
6137
|
const clientFilter = await this._relationOverlay(parsed);
|
|
5254
|
-
const [filter,
|
|
5255
|
-
const
|
|
5256
|
-
|
|
5257
|
-
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
6138
|
+
const [filter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
|
|
6139
|
+
const projected = finish();
|
|
6140
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
5258
6141
|
const paginated = controls.$page !== void 0 || controls.$size !== void 0;
|
|
5259
6142
|
const page = Math.max(Number(controls.$page || 1), 1);
|
|
5260
6143
|
const size = Math.max(Number(controls.$size || 10), 1);
|
|
@@ -5264,7 +6147,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5264
6147
|
...sealed,
|
|
5265
6148
|
$center: void 0,
|
|
5266
6149
|
$index: void 0,
|
|
5267
|
-
$select: select,
|
|
6150
|
+
$select: projected.select,
|
|
5268
6151
|
...paginated ? {
|
|
5269
6152
|
$skip: (page - 1) * size,
|
|
5270
6153
|
$limit: size
|
|
@@ -5272,7 +6155,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5272
6155
|
}
|
|
5273
6156
|
};
|
|
5274
6157
|
if (paginated) {
|
|
5275
|
-
const result = await this._runReadWithActions("geo", queryObj, controls,
|
|
6158
|
+
const result = await this._runReadWithActions("geo", queryObj, controls, projected, async (q) => indexName ? this.readable.geoSearchWithCount(indexName, point, q) : this.readable.geoSearchWithCount(point, q));
|
|
5276
6159
|
return {
|
|
5277
6160
|
data: result.data,
|
|
5278
6161
|
page,
|
|
@@ -5281,7 +6164,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5281
6164
|
count: result.count
|
|
5282
6165
|
};
|
|
5283
6166
|
}
|
|
5284
|
-
return (await this._runReadWithActions("geo", queryObj, controls,
|
|
6167
|
+
return (await this._runReadWithActions("geo", queryObj, controls, projected, async (q) => ({ data: await (indexName ? this.readable.geoSearch(indexName, point, q) : this.readable.geoSearch(point, q)) }))).data;
|
|
5285
6168
|
}
|
|
5286
6169
|
/** Parses the `$center` control: `"lng,lat"` string (or tuple) → `[number, number]`. */
|
|
5287
6170
|
_parseGeoCenter(raw) {
|
|
@@ -5330,21 +6213,22 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5330
6213
|
const error = this.validateParsed(parsed, "getOne") ?? this.checkCapabilities(parsed);
|
|
5331
6214
|
if (error) return error;
|
|
5332
6215
|
await this._relationOverlay(parsed);
|
|
5333
|
-
const sealed =
|
|
5334
|
-
const
|
|
5335
|
-
if (
|
|
5336
|
-
const [prep, overlay] = await Promise.all([this._prepareAugmentation(controls,
|
|
6216
|
+
const { sealed, finish } = await this._projectRead(controls);
|
|
6217
|
+
const projected = finish();
|
|
6218
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
6219
|
+
const [prep, { id: resolvedId, overlay }] = await Promise.all([this._prepareAugmentation(controls, projected), this._resolveWithOverlay(id, "one")]);
|
|
5337
6220
|
const readControls = {
|
|
5338
6221
|
...sealed,
|
|
5339
|
-
$select: prep?.widenedSelect ?? select
|
|
6222
|
+
$select: prep?.widenedSelect ?? projected.select
|
|
5340
6223
|
};
|
|
5341
|
-
const item = await this.returnOne(this._findRow(
|
|
6224
|
+
const item = await this.returnOne(this._findRow(resolvedId, overlay, readControls));
|
|
5342
6225
|
if (item instanceof _moostjs_event_http.HttpError) return item;
|
|
5343
6226
|
const pending = this._finishRows([item], prep, {
|
|
5344
6227
|
endpoint: "one",
|
|
5345
|
-
projection: select,
|
|
5346
|
-
controls
|
|
5347
|
-
|
|
6228
|
+
projection: projected.select,
|
|
6229
|
+
controls,
|
|
6230
|
+
decorations: projected.read?.served ?? NO_DECORATIONS
|
|
6231
|
+
}, projected.read);
|
|
5348
6232
|
if (pending) await pending;
|
|
5349
6233
|
return item;
|
|
5350
6234
|
}
|
|
@@ -5361,10 +6245,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5361
6245
|
*/
|
|
5362
6246
|
async availableActionsById(id) {
|
|
5363
6247
|
await this.parseRequest("availableActions");
|
|
5364
|
-
const own = await this.
|
|
6248
|
+
const { own, id: resolved } = await this._availableResolved(id);
|
|
5365
6249
|
if (!this._hasDelegations) return own;
|
|
5366
|
-
|
|
5367
|
-
return this._delegatedAvailable(own, (d) => preferred.length === 1 && d.paths.every((p) => p === preferred[0]) ? Object.fromEntries(Object.keys(d.idMap).map((f) => [f, id])) : void 0);
|
|
6250
|
+
return this._delegatedAvailable(own, (d) => this._sourceIdOf(d, resolved));
|
|
5368
6251
|
}
|
|
5369
6252
|
/**
|
|
5370
6253
|
* **GET /meta/actions?field1=val1&…** — {@link availableActionsById} by
|
|
@@ -5374,13 +6257,50 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5374
6257
|
async availableActions(query) {
|
|
5375
6258
|
await this.parseRequest("availableActions");
|
|
5376
6259
|
const idObj = this.extractIdShape(query);
|
|
5377
|
-
const sourceIdOf = (d) => d.paths.every((p) => query[p] !== void 0) ? Object.fromEntries(Object.entries(d.idMap).map(([f, p]) => [f, query[p]])) : void 0;
|
|
5378
6260
|
if (idObj instanceof _moostjs_event_http.HttpError) {
|
|
5379
|
-
if (!(await this._activeDelegations()).some((d) =>
|
|
5380
|
-
return this._delegatedAvailable({ actions: [] },
|
|
6261
|
+
if (!(await this._activeDelegations()).some((d) => this._sourceIdOf(d, void 0, query) !== void 0)) return idObj;
|
|
6262
|
+
return this._delegatedAvailable({ actions: [] }, (d) => this._sourceIdOf(d, void 0, query));
|
|
5381
6263
|
}
|
|
5382
|
-
const own = await this.
|
|
5383
|
-
|
|
6264
|
+
const { own, id: resolved } = await this._availableResolved(idObj);
|
|
6265
|
+
if (!this._hasDelegations) return own;
|
|
6266
|
+
return this._delegatedAvailable(own, (d) => this._sourceIdOf(d, resolved, query, this._identificationFields()));
|
|
6267
|
+
}
|
|
6268
|
+
/** Every field of every identification (primary key and unique indexes) the view addresses a row by. */
|
|
6269
|
+
_identificationFields() {
|
|
6270
|
+
return [...new Set(this.idSource.identifications.flatMap((i) => i.fields))];
|
|
6271
|
+
}
|
|
6272
|
+
/**
|
|
6273
|
+
* A delegation's source id: each source id field from the row's mapped path
|
|
6274
|
+
* of the RESOLVED `id`. A path of ANY of the view's identifications
|
|
6275
|
+
* (`consumed` — not just the one the request matched: `?id=1&code=T-OLD` names
|
|
6276
|
+
* `code` too) is NEVER taken from the raw `?` query: that value may be an
|
|
6277
|
+
* alias `resolveRowIds` rewrote, and the source would see (and answer for)
|
|
6278
|
+
* it. Paths outside the identification fall back to `fallback`'s (the raw
|
|
6279
|
+
* query's) value; `undefined` when a path has none.
|
|
6280
|
+
*/
|
|
6281
|
+
_sourceIdOf(d, id, fallback, consumed = []) {
|
|
6282
|
+
const value = (path) => id?.[path] ?? (consumed.includes(path) ? void 0 : fallback?.[path]);
|
|
6283
|
+
return d.paths.every((path) => value(path) !== void 0) ? Object.fromEntries(Object.entries(d.idMap).map(([field, path]) => [field, value(path)])) : void 0;
|
|
6284
|
+
}
|
|
6285
|
+
/**
|
|
6286
|
+
* {@link _availableActions} for a request id (`names`: only those actions):
|
|
6287
|
+
* through {@link resolveRowIds} (`"available"`) first, the row overlay
|
|
6288
|
+
* computed once for both — and the resolved id returned as an object (a
|
|
6289
|
+
* scalar is the single-field `preferredId` value) for the delegated part to
|
|
6290
|
+
* derive its source id from.
|
|
6291
|
+
*/
|
|
6292
|
+
async _availableResolved(id, names) {
|
|
6293
|
+
const { id: resolved, overlay } = await this._resolveWithOverlay(id, "available");
|
|
6294
|
+
const own = await this._availableActions(resolved, overlay, names);
|
|
6295
|
+
if (typeof resolved === "object") return {
|
|
6296
|
+
own,
|
|
6297
|
+
id: resolved
|
|
6298
|
+
};
|
|
6299
|
+
const preferred = this.readable.preferredId;
|
|
6300
|
+
return {
|
|
6301
|
+
own,
|
|
6302
|
+
id: preferred.length === 1 ? { [preferred[0]]: resolved } : void 0
|
|
6303
|
+
};
|
|
5384
6304
|
}
|
|
5385
6305
|
/**
|
|
5386
6306
|
* **POST /delegated-actions/:name** — a query target for a `@DbActionsFrom`
|
|
@@ -5552,8 +6472,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5552
6472
|
* actions are then checked on it (`purpose: "available"`) exactly like
|
|
5553
6473
|
* `$actions` rows.
|
|
5554
6474
|
*/
|
|
5555
|
-
async _availableActions(id, names) {
|
|
5556
|
-
const
|
|
6475
|
+
async _availableActions(id, overlay, names) {
|
|
6476
|
+
const envelopes = await (names ? this._envelopesNamed(names) : this._resolveAugmentEnvelopes());
|
|
5557
6477
|
if (!envelopes?.length) return { actions: [] };
|
|
5558
6478
|
const idKeys = id !== null && typeof id === "object" ? Object.keys(id) : [];
|
|
5559
6479
|
const fieldsOf = envelopes.map((e) => {
|
|
@@ -5634,7 +6554,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5634
6554
|
if (fd.encrypted) entry.encrypted = true;
|
|
5635
6555
|
if (geoIndexedPhysical.has(fd.physicalName)) entry.geo = true;
|
|
5636
6556
|
if (this._writeOnlySet.has(path)) entry.writeOnly = true;
|
|
6557
|
+
if (cap.groupable) entry.groupable = true;
|
|
5637
6558
|
if (cap.bucketable) entry.bucketable = true;
|
|
6559
|
+
if (cap.numeric) entry.numeric = true;
|
|
5638
6560
|
if (fd.derived) entry.derived = true;
|
|
5639
6561
|
if (fd.computed) entry.computed = true;
|
|
5640
6562
|
fields[path] = entry;
|
|
@@ -5649,11 +6571,13 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5649
6571
|
relations,
|
|
5650
6572
|
fields,
|
|
5651
6573
|
type: this.getSerializedType(),
|
|
6574
|
+
...this._decorations && { decorations: this._planner.serialized(() => this.serializeForMeta(this._decorations.type)) },
|
|
5652
6575
|
actions: this.buildActions(),
|
|
5653
6576
|
crud: this.buildCrud(),
|
|
5654
6577
|
versionColumn: this.readable.versionColumn,
|
|
5655
6578
|
...capabilities.bucketUnits.length > 0 && { bucketUnits: [...capabilities.bucketUnits] },
|
|
5656
|
-
aggregateFns: [...capabilities.aggregateFns]
|
|
6579
|
+
aggregateFns: [...capabilities.aggregateFns],
|
|
6580
|
+
aggregateExpressions: capabilities.aggregateExpressions
|
|
5657
6581
|
};
|
|
5658
6582
|
}
|
|
5659
6583
|
buildCrud() {
|
|
@@ -5784,7 +6708,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5784
6708
|
buildCrud() {
|
|
5785
6709
|
return {
|
|
5786
6710
|
...super.buildCrud(),
|
|
5787
|
-
insert: [],
|
|
6711
|
+
insert: this.readable.dbAdapter?.supportsInsertIgnore() ? ["onConflict"] : [],
|
|
5788
6712
|
update: [],
|
|
5789
6713
|
replace: [],
|
|
5790
6714
|
remove: []
|
|
@@ -5804,7 +6728,9 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5804
6728
|
return data;
|
|
5805
6729
|
}
|
|
5806
6730
|
/**
|
|
5807
|
-
* Intercepts delete operations.
|
|
6731
|
+
* Intercepts delete operations. Receives the id {@link resolveRowIds}
|
|
6732
|
+
* resolved (the one the request carried when it is not overridden; since
|
|
6733
|
+
* 0.1.148). Return `undefined` to abort (500 "Not
|
|
5808
6734
|
* deleted"); return an `Error` instance to respond with that error.
|
|
5809
6735
|
* Runs outside any transaction. May be async (e.g. to resolve composite
|
|
5810
6736
|
* ids from external state).
|
|
@@ -5941,8 +6867,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5941
6867
|
* table's transaction and an out-of-scope row is not deleted — a 404,
|
|
5942
6868
|
* exactly like a missing one.
|
|
5943
6869
|
*/
|
|
5944
|
-
async _deleteOrThrow(id) {
|
|
5945
|
-
const scope = await this.rowOverlay();
|
|
6870
|
+
async _deleteOrThrow(id, scope) {
|
|
5946
6871
|
const args = scope ? [{
|
|
5947
6872
|
...this._removeArgs[0],
|
|
5948
6873
|
scope
|
|
@@ -5953,16 +6878,34 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5953
6878
|
}
|
|
5954
6879
|
/**
|
|
5955
6880
|
* **POST /** — inserts one or many records.
|
|
6881
|
+
*
|
|
6882
|
+
* `?$onConflict=ignore` (since 0.1.148) skips rows colliding on the primary
|
|
6883
|
+
* key or a unique index instead of answering 409. The response then is
|
|
6884
|
+
* `{ insertedId?, conflict }` for an object body and
|
|
6885
|
+
* `{ insertedCount, insertedIds, inserted, conflicts }` for an array body.
|
|
6886
|
+
* Any other `$` control on POST answers 400.
|
|
5956
6887
|
*/
|
|
5957
|
-
async insert(payload) {
|
|
5958
|
-
await this.parseRequest("insert");
|
|
6888
|
+
async insert(payload, url) {
|
|
6889
|
+
const { controls } = await this.parseRequest("insert", url ?? "");
|
|
6890
|
+
const onConflict = this._readOnConflict(controls);
|
|
5959
6891
|
assertWriteShape(payload);
|
|
6892
|
+
const args = onConflict ? [{
|
|
6893
|
+
...this._writeArgs[0],
|
|
6894
|
+
onConflict
|
|
6895
|
+
}] : this._writeArgs;
|
|
5960
6896
|
if (Array.isArray(payload)) {
|
|
5961
6897
|
const rows = await this._writeBody("insertMany", payload, true);
|
|
5962
|
-
return this.table.insertMany(rows, ...
|
|
6898
|
+
return this.table.insertMany(rows, ...args);
|
|
5963
6899
|
}
|
|
5964
6900
|
const row = await this._writeBody("insert", payload, false);
|
|
5965
|
-
return this.table.insertOne(row, ...
|
|
6901
|
+
return this.table.insertOne(row, ...args);
|
|
6902
|
+
}
|
|
6903
|
+
/** The only POST control: `$onConflict` (`error` | `ignore`). Anything else `$…` → 400. */
|
|
6904
|
+
_readOnConflict(controls) {
|
|
6905
|
+
for (const key of Object.keys(controls)) if (key !== "$onConflict") throw badRequest("", `Unsupported control "${key}" on insert`);
|
|
6906
|
+
const mode = controls.$onConflict;
|
|
6907
|
+
if (mode !== void 0 && mode !== "error" && mode !== "ignore") throw badRequest("", `$onConflict must be "error" or "ignore", got ${JSON.stringify(mode)}`);
|
|
6908
|
+
return mode === "ignore" ? "ignore" : void 0;
|
|
5966
6909
|
}
|
|
5967
6910
|
/**
|
|
5968
6911
|
* **PUT /** — fully replaces one or many records matched by primary key.
|
|
@@ -6028,7 +6971,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
6028
6971
|
if (typeof table.recordFilter === "function") try {
|
|
6029
6972
|
filter = table.recordFilter(data, this._idOpts);
|
|
6030
6973
|
} catch (error) {
|
|
6031
|
-
if (!(error instanceof _atscript_db.DbError)) throw error;
|
|
6974
|
+
if (!(error instanceof _atscript_db.DbError) || error.code === "SPACE_CLOSED") throw error;
|
|
6032
6975
|
filter = null;
|
|
6033
6976
|
}
|
|
6034
6977
|
else filter = await this.resolveRowFilter(data);
|
|
@@ -6049,8 +6992,9 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
6049
6992
|
*/
|
|
6050
6993
|
async remove(id) {
|
|
6051
6994
|
await this.parseRequest("remove");
|
|
6052
|
-
const
|
|
6053
|
-
|
|
6995
|
+
const { id: resolved, overlay } = await this._resolveWithOverlay(id, "remove");
|
|
6996
|
+
const resolvedId = await this._checkHook(this.onRemove(resolved), "Not deleted");
|
|
6997
|
+
return this._deleteOrThrow(resolvedId, overlay);
|
|
6054
6998
|
}
|
|
6055
6999
|
/**
|
|
6056
7000
|
* **DELETE /?field1=val1&field2=val2** — removes a record by composite key
|
|
@@ -6060,15 +7004,17 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
6060
7004
|
await this.parseRequest("remove");
|
|
6061
7005
|
const idObj = this.extractIdShape(query);
|
|
6062
7006
|
if (idObj instanceof _moostjs_event_http.HttpError) throw idObj;
|
|
6063
|
-
const
|
|
6064
|
-
|
|
7007
|
+
const { id: resolved, overlay } = await this._resolveWithOverlay(idObj, "remove");
|
|
7008
|
+
const resolvedId = await this._checkHook(this.onRemove(resolved), "Not deleted");
|
|
7009
|
+
return this._deleteOrThrow(resolvedId, overlay);
|
|
6065
7010
|
}
|
|
6066
7011
|
};
|
|
6067
7012
|
__decorate([
|
|
6068
7013
|
(0, _moostjs_event_http.Post)(""),
|
|
6069
7014
|
__decorateParam(0, (0, _moostjs_event_http.Body)()),
|
|
7015
|
+
__decorateParam(1, (0, _moostjs_event_http.Url)()),
|
|
6070
7016
|
__decorateMetadata("design:type", Function),
|
|
6071
|
-
__decorateMetadata("design:paramtypes", [Object]),
|
|
7017
|
+
__decorateMetadata("design:paramtypes", [Object, String]),
|
|
6072
7018
|
__decorateMetadata("design:returntype", Promise)
|
|
6073
7019
|
], AsDbController.prototype, "insert", null);
|
|
6074
7020
|
__decorate([
|
|
@@ -6565,7 +7511,7 @@ function buildGateInterceptor(opts) {
|
|
|
6565
7511
|
await ctx.get(dbActionOverlaySlot);
|
|
6566
7512
|
if (level === "row") {
|
|
6567
7513
|
const verdict = judgeRow(action, disabled, await ctx.get(dbActionRowSlot));
|
|
6568
|
-
if (verdict) throw
|
|
7514
|
+
if (verdict) throw rowDisabledError(ctx, action, await ctx.get(dbActionIdSlot), verdictReason(verdict));
|
|
6569
7515
|
return;
|
|
6570
7516
|
}
|
|
6571
7517
|
if (await queryTargetReply(ctx, reply)) return;
|
|
@@ -6589,8 +7535,7 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
6589
7535
|
const existingRows = [];
|
|
6590
7536
|
for (const row of rows) if (row !== void 0) existingRows.push(row);
|
|
6591
7537
|
const verdicts = disabled ? judgeRows(action, disabled, existingRows) : void 0;
|
|
6592
|
-
const
|
|
6593
|
-
const failingReasons = [];
|
|
7538
|
+
const failing = [];
|
|
6594
7539
|
const passingRows = [];
|
|
6595
7540
|
const passingIds = [];
|
|
6596
7541
|
const skipped = [];
|
|
@@ -6601,8 +7546,10 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
6601
7546
|
const verdict = row === void 0 ? void 0 : verdicts?.[verdictIndex++];
|
|
6602
7547
|
if (row === void 0 || verdict) {
|
|
6603
7548
|
const reason = verdictReason(verdict);
|
|
6604
|
-
|
|
6605
|
-
|
|
7549
|
+
failing.push({
|
|
7550
|
+
id: ids[i],
|
|
7551
|
+
reason
|
|
7552
|
+
});
|
|
6606
7553
|
const skipReason = reason ?? (stale?.has(i) ? "stale" : void 0);
|
|
6607
7554
|
skipped.push(skipReason === void 0 ? { id: ids[i] } : {
|
|
6608
7555
|
id: ids[i],
|
|
@@ -6614,27 +7561,47 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
6614
7561
|
}
|
|
6615
7562
|
}
|
|
6616
7563
|
if (onDisabledRows === "skip") {
|
|
6617
|
-
if (passingRows.length === 0) throw
|
|
6618
|
-
if (
|
|
7564
|
+
if (passingRows.length === 0) throw disabledError(ctx, action, failing);
|
|
7565
|
+
if (failing.length > 0) {
|
|
6619
7566
|
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
6620
7567
|
ctx.set(dbActionIdsSlot, Promise.resolve(passingIds));
|
|
6621
7568
|
ctx.set(dbActionSkippedKey, skipped);
|
|
6622
7569
|
}
|
|
6623
7570
|
return;
|
|
6624
7571
|
}
|
|
6625
|
-
if (
|
|
7572
|
+
if (failing.length > 0) throw disabledError(ctx, action, failing);
|
|
7573
|
+
}
|
|
7574
|
+
/**
|
|
7575
|
+
* The 409 of a `'rows'` gate: every failing REQUEST id in request order, each
|
|
7576
|
+
* as the client sent it, with its own reason (`resolveRowIds`, since 0.1.148)
|
|
7577
|
+
* — {@link echoRequests} is the one place that maps resolved ids back.
|
|
7578
|
+
*/
|
|
7579
|
+
function disabledError(ctx, action, failing) {
|
|
7580
|
+
const echoed = echoRequests(ctx, failing);
|
|
7581
|
+
return new ActionDisabledError(action, void 0, echoed.map((f) => f.id), echoed.map((f) => f.reason));
|
|
7582
|
+
}
|
|
7583
|
+
/** The 409 of a `'row'` gate: the id as the client sent it. */
|
|
7584
|
+
function rowDisabledError(ctx, action, id, reason) {
|
|
7585
|
+
return new ActionDisabledError(action, requestIdOf(ctx, id), void 0, [reason]);
|
|
7586
|
+
}
|
|
7587
|
+
/** The action's scope restricts (or needs the loaded rows to decide) — see `TPreScope`. */
|
|
7588
|
+
async function needsScopeLoad(ctx, level) {
|
|
7589
|
+
const pre = await ctx.get(dbActionPreScopeSlot)(level);
|
|
7590
|
+
return pre.kind === "deferred" || pre.scope !== null;
|
|
6626
7591
|
}
|
|
6627
7592
|
/**
|
|
6628
7593
|
* Interceptor for `'row'` / `'rows'` actions without `disabled` (and for a
|
|
6629
7594
|
* `@DbActionRow*` handler of any other level: bound-table injection only):
|
|
6630
7595
|
* runs the controller's `prepareRequest` (when defined, since 0.1.143),
|
|
6631
|
-
* injects the bound table and — only when
|
|
6632
|
-
*
|
|
6633
|
-
* `
|
|
6634
|
-
*
|
|
6635
|
-
*
|
|
6636
|
-
* `
|
|
6637
|
-
*
|
|
7596
|
+
* injects the bound table and — only when there is something to verify —
|
|
7597
|
+
* checks the requested ids before the handler runs by loading the row(s) the
|
|
7598
|
+
* handler would get: `'row'` → the 404 of a missing row; `'rows'` →
|
|
7599
|
+
* out-of-scope and missing ids fail like disabled rows with no reason
|
|
7600
|
+
* (`onDisabledRows`). Something to verify: a row overlay (`transformOne` /
|
|
7601
|
+
* `transformFilter` overridden, non-empty), a non-empty `actionRowScope` for
|
|
7602
|
+
* the action (the hook is asked first, with the request's ids — an override
|
|
7603
|
+
* that restricts nothing verifies nothing; since 0.1.148), or a query target
|
|
7604
|
+
* (since 0.1.147). Nothing to verify → no query.
|
|
6638
7605
|
*/
|
|
6639
7606
|
function buildThinInterceptor(opts) {
|
|
6640
7607
|
const { table, scope } = opts;
|
|
@@ -6645,12 +7612,12 @@ function buildThinInterceptor(opts) {
|
|
|
6645
7612
|
if (!scope) return;
|
|
6646
7613
|
const overlay = await ctx.get(dbActionOverlaySlot);
|
|
6647
7614
|
if (scope.level === "row") {
|
|
6648
|
-
if (overlay ||
|
|
7615
|
+
if (overlay || await needsScopeLoad(ctx, "row")) await ctx.get(dbActionRowSlot);
|
|
6649
7616
|
return;
|
|
6650
7617
|
}
|
|
6651
7618
|
if (await queryTargetReply(ctx, reply)) return;
|
|
6652
7619
|
const target = await ctx.get(dbActionQueryTargetSlot);
|
|
6653
|
-
if (overlay || target ||
|
|
7620
|
+
if (overlay || target || await needsScopeLoad(ctx, "rows")) await gateRows(ctx, scope.action, void 0, scope.onDisabledRows);
|
|
6654
7621
|
await setMaterializedTarget(ctx);
|
|
6655
7622
|
}, ACTION_GATE_PRIORITY);
|
|
6656
7623
|
}
|
|
@@ -7073,6 +8040,52 @@ function DbActionsFrom(source, opts = {}) {
|
|
|
7073
8040
|
*/
|
|
7074
8041
|
const perRow = (fn) => (rows) => rows.map(fn);
|
|
7075
8042
|
//#endregion
|
|
8043
|
+
//#region src/decorations/db-decorations.decorator.ts
|
|
8044
|
+
/**
|
|
8045
|
+
* Declares display-only (decoration) fields of a table or view controller —
|
|
8046
|
+
* values `decorateRows` computes and attaches to rows (since 0.1.148).
|
|
8047
|
+
*
|
|
8048
|
+
* `type` is a plain atscript interface (no `@db.table` / `@db.view`) whose
|
|
8049
|
+
* top-level props are the decorations: their `@meta.label`, `@expect.*` and
|
|
8050
|
+
* `@ui.*` annotations travel in `/meta.decorations`, each key is listed in
|
|
8051
|
+
* `/meta.fields` with `decoration: true`, and a client may name it in
|
|
8052
|
+
* `$select`. A decoration is never filterable, sortable or groupable, and it
|
|
8053
|
+
* is not part of `/meta.type` (forms and write validation never see it).
|
|
8054
|
+
*
|
|
8055
|
+
* ```ts
|
|
8056
|
+
* @TableController(TicketTable)
|
|
8057
|
+
* @DbDecorations(TicketDecorations, { requires: { ownerName: ["ownerId"] } })
|
|
8058
|
+
* export class TicketsController extends AsDbController<typeof TicketTable> {
|
|
8059
|
+
* protected async decorateRows(rows: Record<string, unknown>[], ctx: TDbDecorateContext) {
|
|
8060
|
+
* if (ctx.decorations.has("ownerName")) {
|
|
8061
|
+
* // read rows[i].ownerId, set rows[i].ownerName
|
|
8062
|
+
* }
|
|
8063
|
+
* }
|
|
8064
|
+
* }
|
|
8065
|
+
* ```
|
|
8066
|
+
*
|
|
8067
|
+
* Validated once per class at first use (a `[moost-db]` error): the type is an
|
|
8068
|
+
* object interface; keys are top-level identifiers that collide with no field
|
|
8069
|
+
* or relation of the readable; every `requires` path is an own, readable
|
|
8070
|
+
* (not `@db.writeOnly`) field or a parent object of own fields (on SQL a nested
|
|
8071
|
+
* object is flattened to leaf columns; the hook still gets the whole object). Inherited under `@Inherit()`. Not supported on
|
|
8072
|
+
* value-help controllers.
|
|
8073
|
+
*
|
|
8074
|
+
* @since 0.1.148
|
|
8075
|
+
*/
|
|
8076
|
+
function DbDecorations(type, opts = {}) {
|
|
8077
|
+
if (!(0, _atscript_typescript_utils.isAnnotatedType)(type)) throw new Error("[moost-db] @DbDecorations: expects a compiled atscript interface");
|
|
8078
|
+
const meta = {
|
|
8079
|
+
type,
|
|
8080
|
+
requires: Object.fromEntries(Object.entries(opts.requires ?? {}).map(([key, paths]) => [key, [...paths ?? []]]))
|
|
8081
|
+
};
|
|
8082
|
+
const decorate = getAtscriptDbMate().decorate("atscript_db_decorations", meta);
|
|
8083
|
+
return (target) => {
|
|
8084
|
+
if (isAsValueHelpControllerSubclass(target)) throw new Error(`[moost-db] ${target.name} is a value-help controller — @DbDecorations is not supported there.`);
|
|
8085
|
+
return decorate(target);
|
|
8086
|
+
};
|
|
8087
|
+
}
|
|
8088
|
+
//#endregion
|
|
7076
8089
|
//#region src/permissions/crud-handlers.ts
|
|
7077
8090
|
/**
|
|
7078
8091
|
* The handler method(s) serving each CRUD op on `AsDbReadableController` /
|
|
@@ -7154,6 +8167,7 @@ exports.DbActionRows = DbActionRows;
|
|
|
7154
8167
|
exports.DbActionTarget = DbActionTarget;
|
|
7155
8168
|
exports.DbActions = DbActions;
|
|
7156
8169
|
exports.DbActionsFrom = DbActionsFrom;
|
|
8170
|
+
exports.DbDecorations = DbDecorations;
|
|
7157
8171
|
exports.DbRowActions = DbRowActions;
|
|
7158
8172
|
exports.DbRowsActions = DbRowsActions;
|
|
7159
8173
|
exports.DbTableActions = DbTableActions;
|
|
@@ -7175,6 +8189,7 @@ exports.applyTerminalRefs = applyTerminalRefs;
|
|
|
7175
8189
|
exports.assertExposed = assertExposed;
|
|
7176
8190
|
exports.badRequest = badRequest;
|
|
7177
8191
|
exports.clearDbSpaces = require_db_space_registry.clearDbSpaces;
|
|
8192
|
+
exports.closeDbSpaces = require_db_space_registry.closeDbSpaces;
|
|
7178
8193
|
Object.defineProperty(exports, "collectQueryPaths", {
|
|
7179
8194
|
enumerable: true,
|
|
7180
8195
|
get: function() {
|