@atscript/moost-db 0.1.147 → 0.1.149
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 +1475 -286
- package/dist/index.d.cts +445 -51
- package/dist/index.d.mts +445 -51
- package/dist/index.mjs +1474 -287
- 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");
|
|
@@ -71,16 +71,25 @@ function insightError(insights, message) {
|
|
|
71
71
|
const entry = insightPaths.get(insights);
|
|
72
72
|
return entry?.message === message ? badRequest(entry.path, message) : new _moostjs_event_http.HttpError(400, message);
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The 400 envelope of a `ValidatorError`; `undefined` for any other error.
|
|
76
|
+
* @internal Not part of the public API (not re-exported from the barrel).
|
|
77
|
+
*/
|
|
78
|
+
function validatorErrorToHttp(error) {
|
|
79
|
+
return error instanceof _atscript_typescript_utils.ValidatorError ? errorEnvelope(400, error.message, error.errors) : void 0;
|
|
80
|
+
}
|
|
74
81
|
//#endregion
|
|
75
82
|
//#region src/validation-interceptor.ts
|
|
76
83
|
const dbErrorCodeToStatus = {
|
|
77
84
|
CONFLICT: 409,
|
|
78
85
|
CAS_MISMATCH: 409,
|
|
79
86
|
TX_WAIT_TIMEOUT: 503,
|
|
87
|
+
SPACE_CLOSED: 503,
|
|
80
88
|
BUCKET_TZ_UNAVAILABLE: 501
|
|
81
89
|
};
|
|
82
90
|
function transformValidationError(error, reply) {
|
|
83
|
-
|
|
91
|
+
const validation = validatorErrorToHttp(error);
|
|
92
|
+
if (validation) reply(validation);
|
|
84
93
|
else if (error instanceof _atscript_db.DbError) reply(errorEnvelope(dbErrorCodeToStatus[error.code] ?? 400, error.message, error.errors));
|
|
85
94
|
}
|
|
86
95
|
const validationErrorTransform = () => (0, moost.defineInterceptor)({ error: transformValidationError }, moost.TInterceptorPriority.BEFORE_ALL);
|
|
@@ -515,6 +524,38 @@ function identityKey(id) {
|
|
|
515
524
|
return idKey(id, Object.keys(id).toSorted());
|
|
516
525
|
}
|
|
517
526
|
/**
|
|
527
|
+
* `resolved` (index-aligned with `requested`) with duplicate identities
|
|
528
|
+
* collapsed to the first, plus the per-request list ({@link TAppliedIds}).
|
|
529
|
+
*/
|
|
530
|
+
function applyResolvedIds(requested, resolved) {
|
|
531
|
+
const ids = [];
|
|
532
|
+
const requests = [];
|
|
533
|
+
const seen = /* @__PURE__ */ new Set();
|
|
534
|
+
let changed = false;
|
|
535
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
536
|
+
const k = identityKey(resolved[i]);
|
|
537
|
+
if (k === void 0) {
|
|
538
|
+
ids.push(resolved[i]);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (k !== identityKey(requested[i])) changed = true;
|
|
542
|
+
requests.push({
|
|
543
|
+
id: requested[i],
|
|
544
|
+
key: k
|
|
545
|
+
});
|
|
546
|
+
if (seen.has(k)) {
|
|
547
|
+
changed = true;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
seen.add(k);
|
|
551
|
+
ids.push(resolved[i]);
|
|
552
|
+
}
|
|
553
|
+
return changed ? {
|
|
554
|
+
ids,
|
|
555
|
+
requests
|
|
556
|
+
} : { ids };
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
518
559
|
* The deduped identities of `rows` over `fields` — a value read by `read`
|
|
519
560
|
* (default: the row's own field) — and, per row, its identity's index in
|
|
520
561
|
* `ids` (`-1`: the row is absent, or a value is missing / null).
|
|
@@ -712,6 +753,10 @@ const ACTION_OVERLAY = Symbol.for("atscript-db.actionOverlay");
|
|
|
712
753
|
const ACTION_SCOPE = Symbol.for("atscript-db.actionScope");
|
|
713
754
|
/** `true` when the controller overrides `actionRowScope` (since 0.1.147). */
|
|
714
755
|
const ACTION_SCOPED = Symbol.for("atscript-db.actionScoped");
|
|
756
|
+
/** The controller's internal `resolveRowIds` call for an action's ids (since 0.1.148). */
|
|
757
|
+
const ROW_RESOLVE_IDS = Symbol.for("atscript-db.resolveRowIds");
|
|
758
|
+
/** `true` when the controller overrides `resolveRowIds` (since 0.1.148). */
|
|
759
|
+
const ROW_RESOLVES = Symbol.for("atscript-db.rowResolves");
|
|
715
760
|
/**
|
|
716
761
|
* The controller whose hooks govern this action's rows: only when the action
|
|
717
762
|
* runs against the controller's OWN readable — an `opts.table` binding on a
|
|
@@ -744,10 +789,6 @@ const dbActionOverlaySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
|
744
789
|
await awaitActionPrepared(ctx);
|
|
745
790
|
return await overlayOf.call(ctrl) ?? null;
|
|
746
791
|
});
|
|
747
|
-
/** `true` when this action's controller overrides `actionRowScope`. */
|
|
748
|
-
function isActionScoped(ctx) {
|
|
749
|
-
return ctx.get(scopedControllerSlot)?.[ACTION_SCOPED] === true;
|
|
750
|
-
}
|
|
751
792
|
/**
|
|
752
793
|
* The loaded `rows` (already inside the row overlay) with every row outside
|
|
753
794
|
* the action's `actionRowScope` replaced by `undefined` (since 0.1.147). The
|
|
@@ -1180,6 +1221,26 @@ function isNonEmptyStringArray(value) {
|
|
|
1180
1221
|
}
|
|
1181
1222
|
//#endregion
|
|
1182
1223
|
//#region src/meta/field-capabilities.ts
|
|
1224
|
+
/** The display-only refusal clause per position. */
|
|
1225
|
+
const DISPLAY_ONLY_POSITION = {
|
|
1226
|
+
filter: "a filter",
|
|
1227
|
+
sort: "$sort",
|
|
1228
|
+
groupedSelect: "a grouped $select",
|
|
1229
|
+
groupBy: "$groupBy",
|
|
1230
|
+
having: "$having",
|
|
1231
|
+
aggregate: "an aggregate",
|
|
1232
|
+
bucket: "a calendar bucket"
|
|
1233
|
+
};
|
|
1234
|
+
/** The capability of a declared decoration: selectable, nothing else. */
|
|
1235
|
+
const DECORATION_CAP = {
|
|
1236
|
+
filterable: false,
|
|
1237
|
+
sortable: false,
|
|
1238
|
+
selectable: true,
|
|
1239
|
+
indexed: false,
|
|
1240
|
+
bucketable: false,
|
|
1241
|
+
groupable: false,
|
|
1242
|
+
numeric: false
|
|
1243
|
+
};
|
|
1183
1244
|
const REASON_ADAPTER_FILTER = `${_atscript_db.ADAPTER_FILTER_REASON}.`;
|
|
1184
1245
|
const REASON_ADAPTER_SORT = "adapter cannot sort on this storage type.";
|
|
1185
1246
|
const REASON_WRITE_ONLY = "field is @db.writeOnly.";
|
|
@@ -1284,6 +1345,8 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1284
1345
|
bucketUnits;
|
|
1285
1346
|
/** Aggregate functions the adapter renders, in canonical `ALL_AGGREGATE_FNS` order (`/meta.aggregateFns`). */
|
|
1286
1347
|
aggregateFns;
|
|
1348
|
+
/** Whether the adapter renders aggregate arithmetic (`/meta.aggregateExpressions`). */
|
|
1349
|
+
aggregateExpressions;
|
|
1287
1350
|
/** The adapter-level capabilities this index was built against — see {@link adapterSignature}. */
|
|
1288
1351
|
signature;
|
|
1289
1352
|
/**
|
|
@@ -1293,9 +1356,24 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1293
1356
|
* the index reads must be added here.
|
|
1294
1357
|
*/
|
|
1295
1358
|
static adapterSignature(source) {
|
|
1296
|
-
return `${source.isGeoSearchable()}|${[...source.calendarBucketUnits()].join(",")}|${[...source.aggregateFns()].join(",")}`;
|
|
1359
|
+
return `${source.isGeoSearchable()}|${[...source.calendarBucketUnits()].join(",")}|${[...source.aggregateFns()].join(",")}|${source.supportsAggregateExpressions()}`;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* The navigation paths of a readable: its `navFields`, else its relation
|
|
1363
|
+
* names (partial readables list only the latter).
|
|
1364
|
+
*/
|
|
1365
|
+
static navPathsOf(source) {
|
|
1366
|
+
const nav = new Set(source.navFields);
|
|
1367
|
+
if (nav.size === 0) for (const name of source.relations.keys()) nav.add(name);
|
|
1368
|
+
return nav;
|
|
1297
1369
|
}
|
|
1298
1370
|
_entries = /* @__PURE__ */ new Map();
|
|
1371
|
+
/**
|
|
1372
|
+
* Declared display-only decorations (`@DbDecorations`, since 0.1.148) —
|
|
1373
|
+
* virtual entries: key → the readable paths it `requires`. Selectable only;
|
|
1374
|
+
* visible while every required path is.
|
|
1375
|
+
*/
|
|
1376
|
+
_decorations;
|
|
1299
1377
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
1300
1378
|
_objectParents = /* @__PURE__ */ new Map();
|
|
1301
1379
|
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
@@ -1308,7 +1386,8 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1308
1386
|
get objectParents() {
|
|
1309
1387
|
return this._objectParents;
|
|
1310
1388
|
}
|
|
1311
|
-
constructor(source, writeOnly) {
|
|
1389
|
+
constructor(source, writeOnly, decorations = /* @__PURE__ */ new Map()) {
|
|
1390
|
+
this._decorations = decorations;
|
|
1312
1391
|
const tableMeta = source.type.metadata;
|
|
1313
1392
|
this.filterableManual = tableMeta.get("db.table.filterable") === "manual";
|
|
1314
1393
|
this.sortableManual = tableMeta.get("db.table.sortable") === "manual";
|
|
@@ -1318,6 +1397,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1318
1397
|
this.bucketUnits = _uniqu_core.BUCKET_UNITS.filter((unit) => units.has(unit));
|
|
1319
1398
|
const fns = source.aggregateFns();
|
|
1320
1399
|
this.aggregateFns = [..._atscript_db.ALL_AGGREGATE_FNS].filter((fn) => fns.has(fn));
|
|
1400
|
+
this.aggregateExpressions = source.supportsAggregateExpressions();
|
|
1321
1401
|
const physicalNames = /* @__PURE__ */ new Set();
|
|
1322
1402
|
const jsonValueParents = /* @__PURE__ */ new Set();
|
|
1323
1403
|
for (const fd of source.fieldDescriptors) {
|
|
@@ -1330,8 +1410,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1330
1410
|
dimensions: source.dimensions,
|
|
1331
1411
|
measures: source.measures
|
|
1332
1412
|
};
|
|
1333
|
-
const nav =
|
|
1334
|
-
if (nav.size === 0) for (const name of source.relations.keys()) nav.add(name);
|
|
1413
|
+
const nav = FieldCapabilityIndex.navPathsOf(source);
|
|
1335
1414
|
this.navFields = nav;
|
|
1336
1415
|
const isNavOrDescendant = (path) => (0, _atscript_db.selfOrAncestor)(path, nav) !== void 0;
|
|
1337
1416
|
const flatMap = source.flatMap;
|
|
@@ -1395,13 +1474,18 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1395
1474
|
if (!sortReason && this.sortableManual && !annotated(fd, "db.column.sortable")) sortReason = REASON_ANNOTATION_SORT;
|
|
1396
1475
|
const bucket = isWriteOnly ? void 0 : (0, _atscript_db.bucketSourceVerdict)(fd, this._bucketTable, source);
|
|
1397
1476
|
const bucketReason = !bucket ? REASON_WRITE_ONLY : bucket.ok ? void 0 : `${bucket.reason}.`;
|
|
1477
|
+
const group = isWriteOnly ? void 0 : (0, _atscript_db.groupSourceVerdict)(fd, this._bucketTable, source);
|
|
1478
|
+
const groupReason = !group ? REASON_WRITE_ONLY : group.ok ? void 0 : `${group.reason}.`;
|
|
1398
1479
|
const cap = {
|
|
1399
1480
|
filterable: filterBy.compare === void 0,
|
|
1400
1481
|
sortable: sortReason === void 0,
|
|
1401
1482
|
selectable: true,
|
|
1402
1483
|
indexed: fd.isIndexed === true,
|
|
1403
|
-
bucketable: bucketReason === void 0
|
|
1484
|
+
bucketable: bucketReason === void 0,
|
|
1485
|
+
groupable: groupReason === void 0,
|
|
1486
|
+
numeric: this.aggregateExpressions && physicalReason === void 0 && (0, _atscript_db.numericOperandProblem)(fd) === void 0
|
|
1404
1487
|
};
|
|
1488
|
+
if (groupReason) cap.groupReason = groupReason;
|
|
1405
1489
|
if (filterOps.length > 0) cap.filterOps = filterOps;
|
|
1406
1490
|
if (filterBy.compare) cap.filterReason = filterBy.compare;
|
|
1407
1491
|
if (sortReason) cap.sortReason = sortReason;
|
|
@@ -1422,6 +1506,18 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1422
1506
|
entry.fd
|
|
1423
1507
|
];
|
|
1424
1508
|
}
|
|
1509
|
+
/** The declared decoration keys, in declaration order. */
|
|
1510
|
+
get decorationKeys() {
|
|
1511
|
+
return this._decorations.keys();
|
|
1512
|
+
}
|
|
1513
|
+
/** The capability of the declared decoration `key` (selectable only), `undefined` when `key` is none. */
|
|
1514
|
+
decorationCap(key) {
|
|
1515
|
+
return this._decorations.has(key) ? DECORATION_CAP : void 0;
|
|
1516
|
+
}
|
|
1517
|
+
/** The decoration `key` is visible: every path it `requires` passes `exists` (the hidden-field hook). */
|
|
1518
|
+
decorationVisible(key, exists) {
|
|
1519
|
+
return this._decorations.get(key)?.every(exists) === true;
|
|
1520
|
+
}
|
|
1425
1521
|
/** Physical filter capability (adapter ∧ ¬writeOnly ∧ ¬encrypted) — ignores the manual-mode policy. */
|
|
1426
1522
|
isPhysicallyFilterable(path) {
|
|
1427
1523
|
return this._entries.get(path)?.physicalFilterable === true;
|
|
@@ -1446,6 +1542,11 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1446
1542
|
* JSON-stored column" — clients pin that wording, so do not "align" it
|
|
1447
1543
|
* with the core backstop's text.
|
|
1448
1544
|
*
|
|
1545
|
+
* A declared decoration (`@DbDecorations`) is a virtual entry: `select` while
|
|
1546
|
+
* every path it requires passes `exists`, any other position a display-only
|
|
1547
|
+
* refusal (`groupedSelect` is a `$select` of an aggregate query), and hidden
|
|
1548
|
+
* sources answer `Unknown field` like a nonexistent path.
|
|
1549
|
+
*
|
|
1449
1550
|
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
1450
1551
|
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
1451
1552
|
*
|
|
@@ -1455,8 +1556,18 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1455
1556
|
* name the prefixed one. A filter on a navigation path names the predicate
|
|
1456
1557
|
* alternative (`ticket=$some(status=…)`).
|
|
1457
1558
|
*/
|
|
1458
|
-
check(local,
|
|
1559
|
+
check(local, gateOp, exists, predicate = "compare", prefix = "") {
|
|
1459
1560
|
const path = prefix + local;
|
|
1561
|
+
const requires = prefix === "" ? this._decorations.get(local) : void 0;
|
|
1562
|
+
if (requires) {
|
|
1563
|
+
if (!requires.every(exists)) return unknownField(path);
|
|
1564
|
+
if (gateOp === "select") return void 0;
|
|
1565
|
+
return {
|
|
1566
|
+
path,
|
|
1567
|
+
message: `Field "${path}" is display-only and cannot be used in ${DISPLAY_ONLY_POSITION[gateOp]}`
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const op = gateOp === "groupedSelect" ? "select" : gateOp;
|
|
1460
1571
|
if (!exists(local)) return unknownField(path);
|
|
1461
1572
|
const { kind, parent: localParent } = (0, _atscript_db.classifyQueryPath)(this, local);
|
|
1462
1573
|
const parent = localParent === void 0 ? void 0 : prefix + localParent;
|
|
@@ -1486,6 +1597,10 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1486
1597
|
path,
|
|
1487
1598
|
message: `Bucketing field "${path}" is not permitted — ${entry.cap.bucketReason}`
|
|
1488
1599
|
};
|
|
1600
|
+
case "groupBy": return entry.cap.groupable ? void 0 : {
|
|
1601
|
+
path,
|
|
1602
|
+
message: `${OP_SUBJECT[op]} field "${path}" is not permitted — ${entry.cap.groupReason}`
|
|
1603
|
+
};
|
|
1489
1604
|
default: return entry.physicalFilterable ? void 0 : {
|
|
1490
1605
|
path,
|
|
1491
1606
|
message: `${OP_SUBJECT[op]} field "${path}" is not permitted — ${entry.physicalReason}`
|
|
@@ -1852,6 +1967,10 @@ function hasClientPredicate(filter) {
|
|
|
1852
1967
|
* without copying).
|
|
1853
1968
|
*/
|
|
1854
1969
|
function readRequestContext(endpoint, controls, filter) {
|
|
1970
|
+
if (endpoint === "insert") return controls.$onConflict === "ignore" ? {
|
|
1971
|
+
endpoint,
|
|
1972
|
+
onConflict: "ignore"
|
|
1973
|
+
} : { endpoint };
|
|
1855
1974
|
if (!filter || Object.keys(filter).length === 0) return {
|
|
1856
1975
|
endpoint,
|
|
1857
1976
|
controls,
|
|
@@ -2140,7 +2259,7 @@ let AsReadableController = class AsReadableController {
|
|
|
2140
2259
|
async parseRequest(endpoint, url) {
|
|
2141
2260
|
let request;
|
|
2142
2261
|
if (url !== void 0) {
|
|
2143
|
-
const { parsed, hasNonControl } = endpoint === "one" ? this.parseControlsOnlyFromUrl(url) : {
|
|
2262
|
+
const { parsed, hasNonControl } = endpoint === "one" || endpoint === "insert" ? this.parseControlsOnlyFromUrl(url) : {
|
|
2144
2263
|
parsed: this.parseQueryString(url),
|
|
2145
2264
|
hasNonControl: false
|
|
2146
2265
|
};
|
|
@@ -2562,8 +2681,8 @@ function isIdValidationSource(value) {
|
|
|
2562
2681
|
const v = value;
|
|
2563
2682
|
return Array.isArray(v.identifications) && Array.isArray(v.fieldDescriptors);
|
|
2564
2683
|
}
|
|
2565
|
-
function validateSingleId(body, source,
|
|
2566
|
-
const errors = collectIdErrors(body, source,
|
|
2684
|
+
function validateSingleId(body, source, opts = {}) {
|
|
2685
|
+
const errors = collectIdErrors(body, source, opts);
|
|
2567
2686
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
2568
2687
|
return body;
|
|
2569
2688
|
}
|
|
@@ -2580,12 +2699,13 @@ function validateMultiId(body, source, maxIds = Infinity) {
|
|
|
2580
2699
|
details: []
|
|
2581
2700
|
}]);
|
|
2582
2701
|
const errors = [];
|
|
2583
|
-
for (let i = 0; i < body.length; i++) errors.push(...collectIdErrors(body[i], source, `[${i}]`));
|
|
2702
|
+
for (let i = 0; i < body.length; i++) errors.push(...collectIdErrors(body[i], source, { path: `[${i}]` }));
|
|
2584
2703
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
2585
2704
|
return body;
|
|
2586
2705
|
}
|
|
2587
|
-
function collectIdErrors(value, source,
|
|
2588
|
-
|
|
2706
|
+
function collectIdErrors(value, source, opts) {
|
|
2707
|
+
const pathPrefix = opts.path ?? "";
|
|
2708
|
+
if (!isPlainObject$2(value)) return [{
|
|
2589
2709
|
path: pathPrefix,
|
|
2590
2710
|
message: "Expected JSON object for row identifier",
|
|
2591
2711
|
details: []
|
|
@@ -2605,14 +2725,21 @@ function collectIdErrors(value, source, pathPrefix) {
|
|
|
2605
2725
|
const errors = [];
|
|
2606
2726
|
for (const fieldName of match.fields) {
|
|
2607
2727
|
const sub = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
|
|
2608
|
-
const err = checkScalar(value[fieldName], cache.fieldByName.get(fieldName), sub);
|
|
2728
|
+
const err = checkScalar(value[fieldName], sub) ?? (opts.strictTypes === false ? void 0 : checkType(value[fieldName], cache.fieldByName.get(fieldName), sub));
|
|
2609
2729
|
if (err) errors.push(err);
|
|
2610
2730
|
}
|
|
2611
2731
|
return errors;
|
|
2612
2732
|
}
|
|
2613
|
-
|
|
2733
|
+
/**
|
|
2734
|
+
* An identifier value is always a scalar: an object would reach the filter
|
|
2735
|
+
* as an operator expression (`{ $ne: null }`) whatever the field's type.
|
|
2736
|
+
*/
|
|
2737
|
+
function checkScalar(value, path) {
|
|
2738
|
+
return typeof value === "object" && value !== null ? scalarMismatch(path, "a scalar", value) : void 0;
|
|
2739
|
+
}
|
|
2740
|
+
/** The value's type against the field's declared design type (default `string`). */
|
|
2741
|
+
function checkType(value, fd, path) {
|
|
2614
2742
|
const expected = fd?.designType ?? "string";
|
|
2615
|
-
if (typeof value === "object" && value !== null) return scalarMismatch(path, "a scalar", value);
|
|
2616
2743
|
if (expected === "string" && typeof value !== "string") return scalarMismatch(path, expected, value);
|
|
2617
2744
|
if (expected === "number" && typeof value !== "number") return scalarMismatch(path, expected, value);
|
|
2618
2745
|
if (expected === "boolean" && typeof value !== "boolean") return scalarMismatch(path, expected, value);
|
|
@@ -2629,20 +2756,76 @@ function describe(value) {
|
|
|
2629
2756
|
if (Array.isArray(value)) return "array";
|
|
2630
2757
|
return typeof value;
|
|
2631
2758
|
}
|
|
2632
|
-
function isPlainObject$
|
|
2759
|
+
function isPlainObject$2(value) {
|
|
2633
2760
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2634
2761
|
}
|
|
2635
2762
|
//#endregion
|
|
2636
2763
|
//#region src/actions/id-cache.ts
|
|
2637
2764
|
/**
|
|
2765
|
+
* EVERY id the client sent, in request order, with the identity of the id it
|
|
2766
|
+
* resolved to (since 0.1.148) — set only when the controller's
|
|
2767
|
+
* `resolveRowIds` changed or collapsed an id. The single model: refusals,
|
|
2768
|
+
* `reasons`, summaries and counts are judged and reported per request id
|
|
2769
|
+
* through {@link echoRequests}; the resolved (deduped) ids serve only the
|
|
2770
|
+
* row load and the handler.
|
|
2771
|
+
*/
|
|
2772
|
+
const dbActionRequestIdsKey = (0, _wooksjs_event_core.key)("atscript_db_action_request_ids");
|
|
2773
|
+
/** Number of request ids (`fallback` when no id was resolved to another). */
|
|
2774
|
+
function requestCount(ctx, fallback) {
|
|
2775
|
+
return ctx.has(dbActionRequestIdsKey) ? ctx.get(dbActionRequestIdsKey).length : fallback;
|
|
2776
|
+
}
|
|
2777
|
+
/** Number of request ids that resolved to one of `ids` (`ids.length` when none was rewritten). */
|
|
2778
|
+
function requestCountOf(ctx, ids) {
|
|
2779
|
+
if (!ctx.has(dbActionRequestIdsKey)) return ids.length;
|
|
2780
|
+
const keys = new Set(ids.map((id) => identityKey(id)));
|
|
2781
|
+
return ctx.get(dbActionRequestIdsKey).filter((r) => keys.has(r.key)).length;
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Entries keyed by a resolved id, reported for every REQUEST id that resolved
|
|
2785
|
+
* to it — in request order, each as the client sent it (`id` replaced). Two
|
|
2786
|
+
* aliases of one row come back exactly like two distinct rows (same order,
|
|
2787
|
+
* same count). Entries whose id no request resolved to stay as they are,
|
|
2788
|
+
* after the request ones.
|
|
2789
|
+
*/
|
|
2790
|
+
function echoRequests(ctx, entries) {
|
|
2791
|
+
if (!ctx.has(dbActionRequestIdsKey)) return [...entries];
|
|
2792
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2793
|
+
const rest = [];
|
|
2794
|
+
for (const e of entries) {
|
|
2795
|
+
const k = identityKey(e.id);
|
|
2796
|
+
if (k === void 0) rest.push(e);
|
|
2797
|
+
else if (!byKey.has(k)) byKey.set(k, e);
|
|
2798
|
+
}
|
|
2799
|
+
const out = [];
|
|
2800
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2801
|
+
for (const r of ctx.get(dbActionRequestIdsKey)) {
|
|
2802
|
+
const e = byKey.get(r.key);
|
|
2803
|
+
if (!e) continue;
|
|
2804
|
+
matched.add(r.key);
|
|
2805
|
+
out.push({
|
|
2806
|
+
...e,
|
|
2807
|
+
id: r.id
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
for (const [k, e] of byKey) if (!matched.has(k)) rest.push(e);
|
|
2811
|
+
return [...out, ...rest];
|
|
2812
|
+
}
|
|
2813
|
+
/** The id as the client sent it (the first, for an id several requests resolved to). */
|
|
2814
|
+
function requestIdOf(ctx, id) {
|
|
2815
|
+
return echoRequests(ctx, [{ id }])[0].id;
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2638
2818
|
* Validates the body's `ids` against the action table's identifications. For
|
|
2639
2819
|
* the controller's own table that is its `idSource` (since 0.1.134): a unique
|
|
2640
2820
|
* index over a field `hasField` hides neither addresses a row nor appears in
|
|
2641
2821
|
* the "must exactly match one of" message. An `opts.table` binding has no
|
|
2642
2822
|
* visibility hook. The controller's `prepareRequest` (since 0.1.143) runs
|
|
2643
2823
|
* first, so the visibility the ids are validated against is the request's.
|
|
2824
|
+
* Then, when the controller overrides `resolveRowIds` (since 0.1.148), the
|
|
2825
|
+
* validated ids go through it — a one-element array for a `'row'` action —
|
|
2826
|
+
* and the resolved ids (duplicates collapsed) are what every consumer sees.
|
|
2644
2827
|
*/
|
|
2645
|
-
async function resolveValidatedId(ctx, validate) {
|
|
2828
|
+
async function resolveValidatedId(ctx, level, validate) {
|
|
2646
2829
|
await awaitActionPrepared(ctx);
|
|
2647
2830
|
let source = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
|
|
2648
2831
|
if (!source) {
|
|
@@ -2652,9 +2835,21 @@ async function resolveValidatedId(ctx, validate) {
|
|
|
2652
2835
|
if (!isIdValidationSource(source)) throw noTableError(ctx);
|
|
2653
2836
|
const env = await ctx.get(dbActionBodySlot);
|
|
2654
2837
|
validate(env.ids, source);
|
|
2655
|
-
|
|
2838
|
+
const scoped = ctx.get(scopedControllerSlot);
|
|
2839
|
+
const resolve = scoped?.[ROW_RESOLVES] ? scoped[ROW_RESOLVE_IDS] : void 0;
|
|
2840
|
+
if (!resolve) return env.ids;
|
|
2841
|
+
const requested = level === "row" ? [env.ids] : env.ids;
|
|
2842
|
+
const overlay = await ctx.get(dbActionOverlaySlot);
|
|
2843
|
+
const { ids, requests } = await resolve.call(scoped, requested, {
|
|
2844
|
+
purpose: "action",
|
|
2845
|
+
action: readCurrentActionMeta(ctx)?.name,
|
|
2846
|
+
level,
|
|
2847
|
+
overlay: overlay ?? void 0
|
|
2848
|
+
});
|
|
2849
|
+
if (requests) ctx.set(dbActionRequestIdsKey, requests);
|
|
2850
|
+
return level === "row" ? ids[0] : ids;
|
|
2656
2851
|
}
|
|
2657
|
-
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
|
|
2852
|
+
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, "row", validateSingleId));
|
|
2658
2853
|
/**
|
|
2659
2854
|
* The `'rows'` action's identifiers: the body's validated `ids`, or — for a
|
|
2660
2855
|
* query target (`query`, since 0.1.147) — the identities of the rows it
|
|
@@ -2664,7 +2859,7 @@ const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
|
2664
2859
|
await awaitActionPrepared(ctx);
|
|
2665
2860
|
const target = await ctx.get(dbActionQueryTargetSlot);
|
|
2666
2861
|
if (target) return target.ids;
|
|
2667
|
-
return await resolveValidatedId(ctx, (body, src) => validateMultiId(body, src, actionMaxIds(ctx)));
|
|
2862
|
+
return await resolveValidatedId(ctx, "rows", (body, src) => validateMultiId(body, src, actionMaxIds(ctx)));
|
|
2668
2863
|
});
|
|
2669
2864
|
const useDbActionId = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdSlot) }));
|
|
2670
2865
|
const useDbActionIds = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdsSlot) }));
|
|
@@ -2684,29 +2879,70 @@ function asFetchTable(value) {
|
|
|
2684
2879
|
function seedActionFields(ctx, table) {
|
|
2685
2880
|
return actionRowFields(table, requiredFieldsOf(readCurrentActionMeta(ctx)?.opts), actionFieldVisibility(ctx));
|
|
2686
2881
|
}
|
|
2882
|
+
const UNSCOPED = {
|
|
2883
|
+
kind: "resolved",
|
|
2884
|
+
scope: null
|
|
2885
|
+
};
|
|
2886
|
+
const DEFERRED = { kind: "deferred" };
|
|
2887
|
+
function inPreferredShape(ids, preferred) {
|
|
2888
|
+
return ids.every((id) => {
|
|
2889
|
+
return Object.keys(id).length === preferred.length && preferred.every((f) => f in id);
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
async function resolvePreScope(ctx, level) {
|
|
2893
|
+
const ctrl = ctx.get(scopedControllerSlot);
|
|
2894
|
+
const action = readCurrentActionMeta(ctx)?.name;
|
|
2895
|
+
const scopeOf = ctrl?.[ACTION_SCOPED] ? ctrl[ACTION_SCOPE] : void 0;
|
|
2896
|
+
if (!ctrl || !scopeOf || action === void 0) return UNSCOPED;
|
|
2897
|
+
if (await ctx.get(dbActionOverlaySlot)) return DEFERRED;
|
|
2898
|
+
const table = asFetchTable(getActionTable(ctx));
|
|
2899
|
+
const preferred = table?.preferredId;
|
|
2900
|
+
if (!table || !preferred?.length) return DEFERRED;
|
|
2901
|
+
const [target, requested] = await Promise.all([level === "rows" ? ctx.get(dbActionQueryTargetSlot) : void 0, level === "row" ? ctx.get(dbActionIdSlot).then((id) => [id]) : ctx.get(dbActionIdsSlot)]);
|
|
2902
|
+
if (target || !inPreferredShape(requested, preferred)) return DEFERRED;
|
|
2903
|
+
const ids = level === "row" ? requested : dedupeIdentities(requested, preferred).ids;
|
|
2904
|
+
if (ids.length === 0) return UNSCOPED;
|
|
2905
|
+
return {
|
|
2906
|
+
kind: "resolved",
|
|
2907
|
+
scope: nonEmptyFilter(await scopeOf.call(ctrl, action, createScopeContext("execute", ids, table))) ?? null
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* {@link TPreScope} of the current action — once per event. An event runs one
|
|
2912
|
+
* action, so the first caller's `level` (`'row'` / `'rows'`) is the action's.
|
|
2913
|
+
*/
|
|
2914
|
+
const dbActionPreScopeSlot = (0, _wooksjs_event_core.cached)((ctx) => {
|
|
2915
|
+
let pending;
|
|
2916
|
+
return (level) => pending ??= resolvePreScope(ctx, level);
|
|
2917
|
+
});
|
|
2687
2918
|
/**
|
|
2688
2919
|
* Loaded row / rows are ANDed with the controller's row overlay (see
|
|
2689
2920
|
* `dbActionOverlaySlot`, since 0.1.143) and, since 0.1.147, checked against
|
|
2690
|
-
* the action's `actionRowScope
|
|
2691
|
-
*
|
|
2692
|
-
*
|
|
2921
|
+
* the action's `actionRowScope`: an out-of-scope id loads nothing — exactly
|
|
2922
|
+
* like a missing one (the same 404 on `'row'` actions, so the two can't be
|
|
2923
|
+
* told apart).
|
|
2693
2924
|
*
|
|
2694
2925
|
* Order: `prepareRequest` → the row overlay (BEFORE the request body is
|
|
2695
|
-
* read: fail-fast authorization) → the ids (body) →
|
|
2696
|
-
* `actionRowScope` (
|
|
2926
|
+
* read: fail-fast authorization) → the ids (body) → `resolveRowIds` (since
|
|
2927
|
+
* 0.1.148) → `actionRowScope` (pre-load, when there is no overlay and the
|
|
2928
|
+
* ids are in `preferredId` shape — its restriction joins the one row load;
|
|
2929
|
+
* since 0.1.148) → the row load → otherwise `actionRowScope` on the loaded
|
|
2930
|
+
* candidates (it needs them).
|
|
2697
2931
|
*/
|
|
2698
2932
|
async function loadRow(ctx) {
|
|
2699
2933
|
const overlay = await ctx.get(dbActionOverlaySlot);
|
|
2700
2934
|
const id = await ctx.get(dbActionIdSlot);
|
|
2701
2935
|
const table = asFetchTable(getActionTable(ctx));
|
|
2702
2936
|
if (!table) throw noTableError(ctx);
|
|
2937
|
+
const pre = await ctx.get(dbActionPreScopeSlot)("row");
|
|
2703
2938
|
const fields = seedActionFields(ctx, table);
|
|
2704
2939
|
for (const k of Object.keys(id)) fields.add(k);
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2940
|
+
const idFilter = withOverlay(withOverlay(id, overlay), pre.kind === "resolved" ? pre.scope : null);
|
|
2941
|
+
let row = await table.findOne({
|
|
2942
|
+
filter: idFilter,
|
|
2707
2943
|
controls: { $select: [...fields] }
|
|
2708
|
-
});
|
|
2709
|
-
|
|
2944
|
+
}) ?? void 0;
|
|
2945
|
+
if (row !== void 0 && pre.kind === "deferred") [row] = await applyActionScope(ctx, table, [row]);
|
|
2710
2946
|
if (row === void 0) throw new _moostjs_event_http.HttpError(404, "Row not found for action identifier");
|
|
2711
2947
|
return row;
|
|
2712
2948
|
}
|
|
@@ -2717,7 +2953,11 @@ async function loadRows(ctx) {
|
|
|
2717
2953
|
if (!table) throw noTableError(ctx);
|
|
2718
2954
|
const fields = seedActionFields(ctx, table);
|
|
2719
2955
|
const target = await ctx.get(dbActionQueryTargetSlot);
|
|
2720
|
-
if (!target)
|
|
2956
|
+
if (!target) {
|
|
2957
|
+
const pre = await ctx.get(dbActionPreScopeSlot)("rows");
|
|
2958
|
+
if (pre.kind === "resolved") return findRowsByIds(table, ids, pre.scope ? withOverlay(pre.scope, overlay) : overlay, fields);
|
|
2959
|
+
return applyActionScope(ctx, table, await findRowsByIds(table, ids, overlay, fields));
|
|
2960
|
+
}
|
|
2721
2961
|
const rows = await target.load(ids, fields);
|
|
2722
2962
|
const stale = /* @__PURE__ */ new Set();
|
|
2723
2963
|
for (let i = 0; i < rows.length; i++) if (rows[i] === void 0) stale.add(i);
|
|
@@ -2774,25 +3014,52 @@ function DbActionTarget() {
|
|
|
2774
3014
|
var TargetBase = class {
|
|
2775
3015
|
kind;
|
|
2776
3016
|
matched;
|
|
3017
|
+
ctx;
|
|
2777
3018
|
skipped = [];
|
|
2778
3019
|
failed = [];
|
|
2779
3020
|
processed = 0;
|
|
2780
|
-
|
|
3021
|
+
/** Identities of every id handed to the handler so far. */
|
|
3022
|
+
handedKeys = /* @__PURE__ */ new Set();
|
|
3023
|
+
failedKeys = /* @__PURE__ */ new Set();
|
|
3024
|
+
constructor(kind, matched, ctx) {
|
|
2781
3025
|
this.kind = kind;
|
|
2782
3026
|
this.matched = matched;
|
|
3027
|
+
this.ctx = ctx;
|
|
2783
3028
|
}
|
|
3029
|
+
/**
|
|
3030
|
+
* One failure per id, whether or not `resolveRowIds` rewrote any id: a
|
|
3031
|
+
* second `fail()` of the same identity is ignored (the first reason stands),
|
|
3032
|
+
* so `failed` and `processed` agree in both modes.
|
|
3033
|
+
*/
|
|
2784
3034
|
fail(id, reason) {
|
|
3035
|
+
const k = identityKey(id);
|
|
3036
|
+
if (k !== void 0) {
|
|
3037
|
+
if (this.failedKeys.has(k)) return;
|
|
3038
|
+
this.failedKeys.add(k);
|
|
3039
|
+
}
|
|
2785
3040
|
this.failed.push({
|
|
2786
3041
|
id,
|
|
2787
3042
|
reason
|
|
2788
3043
|
});
|
|
2789
3044
|
}
|
|
3045
|
+
/**
|
|
3046
|
+
* A batch handed to the handler, counted per request id (two aliases of one
|
|
3047
|
+
* row count twice, like two distinct rows).
|
|
3048
|
+
*/
|
|
3049
|
+
countProcessed(ids) {
|
|
3050
|
+
for (const id of ids) {
|
|
3051
|
+
const k = identityKey(id);
|
|
3052
|
+
if (k !== void 0) this.handedKeys.add(k);
|
|
3053
|
+
}
|
|
3054
|
+
this.processed += requestCountOf(this.ctx, ids);
|
|
3055
|
+
}
|
|
2790
3056
|
summary() {
|
|
3057
|
+
const failed = echoRequests(this.ctx, this.failed);
|
|
2791
3058
|
return {
|
|
2792
3059
|
matched: this.matched,
|
|
2793
|
-
processed: Math.max(0, this.processed -
|
|
2794
|
-
skipped:
|
|
2795
|
-
failed
|
|
3060
|
+
processed: Math.max(0, this.processed - failed.length),
|
|
3061
|
+
skipped: echoRequests(this.ctx, this.skipped),
|
|
3062
|
+
failed
|
|
2796
3063
|
};
|
|
2797
3064
|
}
|
|
2798
3065
|
};
|
|
@@ -2804,12 +3071,12 @@ var MaterializedTarget = class extends TargetBase {
|
|
|
2804
3071
|
ids;
|
|
2805
3072
|
rows;
|
|
2806
3073
|
iterated = false;
|
|
2807
|
-
constructor(kind, matched, ids, rows, skipped) {
|
|
2808
|
-
super(kind, matched);
|
|
3074
|
+
constructor(kind, matched, ids, rows, skipped, ctx) {
|
|
3075
|
+
super(kind, matched, ctx);
|
|
2809
3076
|
this.ids = ids;
|
|
2810
3077
|
this.rows = rows;
|
|
2811
3078
|
this.skipped.push(...skipped);
|
|
2812
|
-
this.
|
|
3079
|
+
this.countProcessed(ids);
|
|
2813
3080
|
}
|
|
2814
3081
|
async *batches() {
|
|
2815
3082
|
if (this.iterated) throw new Error("[moost-db actions] target.batches() is single-pass");
|
|
@@ -2825,21 +3092,19 @@ async function setMaterializedTarget(ctx) {
|
|
|
2825
3092
|
const target = await resolveActionQueryTarget(ctx, false);
|
|
2826
3093
|
const ids = await ctx.get(dbActionIdsSlot);
|
|
2827
3094
|
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 () => {
|
|
3095
|
+
ctx.set(dbActionTargetKey, new MaterializedTarget(target ? "query" : "ids", target ? target.matched : requestCount(ctx, ids.length + skipped.length), ids, async () => {
|
|
2829
3096
|
return (await ctx.get(dbActionRowsSlot)).filter((r) => r !== void 0);
|
|
2830
|
-
}, skipped));
|
|
3097
|
+
}, skipped, ctx));
|
|
2831
3098
|
}
|
|
2832
3099
|
/** The `@DbActionTarget` surface: the target in batches, each gated when it is reached. */
|
|
2833
3100
|
var StreamedTarget = class extends TargetBase {
|
|
2834
|
-
ctx;
|
|
2835
3101
|
source;
|
|
2836
3102
|
batchSize;
|
|
2837
3103
|
action;
|
|
2838
3104
|
disabled;
|
|
2839
3105
|
iterated = false;
|
|
2840
3106
|
constructor(matched, ctx, source, batchSize, action, disabled) {
|
|
2841
|
-
super(source.kind, matched);
|
|
2842
|
-
this.ctx = ctx;
|
|
3107
|
+
super(source.kind, matched, ctx);
|
|
2843
3108
|
this.source = source;
|
|
2844
3109
|
this.batchSize = batchSize;
|
|
2845
3110
|
this.action = action;
|
|
@@ -2857,7 +3122,7 @@ var StreamedTarget = class extends TargetBase {
|
|
|
2857
3122
|
const batch = await this.gate(ids.slice(start, start + this.batchSize));
|
|
2858
3123
|
this.next = start + this.batchSize;
|
|
2859
3124
|
if (batch.ids.length === 0) continue;
|
|
2860
|
-
this.
|
|
3125
|
+
this.countProcessed(batch.ids);
|
|
2861
3126
|
this.current = batch.ids;
|
|
2862
3127
|
yield batch;
|
|
2863
3128
|
this.current = void 0;
|
|
@@ -2865,30 +3130,66 @@ var StreamedTarget = class extends TargetBase {
|
|
|
2865
3130
|
this.next = ids.length;
|
|
2866
3131
|
}
|
|
2867
3132
|
/**
|
|
2868
|
-
* The summary of a run the handler failed after it received a batch
|
|
2869
|
-
*
|
|
2870
|
-
*
|
|
3133
|
+
* The summary of a run the handler failed after it received a batch, judged
|
|
3134
|
+
* per REQUEST id by its own request position — an alias after the abort
|
|
3135
|
+
* point is "not run" even when its row was judged (run, skipped) earlier,
|
|
3136
|
+
* exactly as the same request of distinct rows would be:
|
|
3137
|
+
* - at or after the position the run stopped at: `failed` as `"not run"`;
|
|
3138
|
+
* - before it, a row the handler failed itself: its reason;
|
|
3139
|
+
* - before it, the batch the handler held (uncertain): the error's message;
|
|
3140
|
+
* - before it, a row the gate skipped: `skipped`; a row that ran: `processed`.
|
|
3141
|
+
* `aborted` is set.
|
|
2871
3142
|
*/
|
|
2872
3143
|
abort(error) {
|
|
2873
3144
|
const message = errorMessage(error);
|
|
2874
|
-
const
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
3145
|
+
const ids = this.source.ids;
|
|
3146
|
+
const requests = this.ctx.has(dbActionRequestIdsKey) ? this.ctx.get(dbActionRequestIdsKey) : ids.map((id, i) => ({
|
|
3147
|
+
id,
|
|
3148
|
+
key: identityKey(id) ?? `#${i}`
|
|
3149
|
+
}));
|
|
3150
|
+
const held = new Set((this.current ?? []).map((id) => identityKey(id)));
|
|
3151
|
+
let cutoff = requests.length;
|
|
3152
|
+
if (this.ctx.has(dbActionRequestIdsKey)) {
|
|
3153
|
+
const firstOf = (key) => requests.findIndex((r) => r.key === key);
|
|
3154
|
+
if (held.size > 0) cutoff = 1 + Math.max(...[...held].map(firstOf));
|
|
3155
|
+
else if (this.next < ids.length) cutoff = firstOf(identityKey(ids[this.next]));
|
|
3156
|
+
if (cutoff < 0) cutoff = requests.length;
|
|
3157
|
+
} else if (this.next < ids.length) cutoff = this.next;
|
|
3158
|
+
const failedBy = new Map(this.failed.map((f) => [identityKey(f.id), f]));
|
|
3159
|
+
const skippedBy = /* @__PURE__ */ new Map();
|
|
3160
|
+
for (const row of this.skipped) {
|
|
3161
|
+
const k = identityKey(row.id);
|
|
3162
|
+
if (!skippedBy.has(k)) skippedBy.set(k, row);
|
|
3163
|
+
}
|
|
3164
|
+
const failed = [];
|
|
3165
|
+
const skipped = [];
|
|
3166
|
+
let processed = 0;
|
|
3167
|
+
requests.forEach((r, i) => {
|
|
3168
|
+
const own = failedBy.get(r.key);
|
|
3169
|
+
const skip = skippedBy.get(r.key);
|
|
3170
|
+
if (i >= cutoff) failed.push({
|
|
3171
|
+
id: r.id,
|
|
3172
|
+
reason: "not run"
|
|
3173
|
+
});
|
|
3174
|
+
else if (own) failed.push({
|
|
3175
|
+
id: r.id,
|
|
3176
|
+
reason: own.reason
|
|
3177
|
+
});
|
|
3178
|
+
else if (held.has(r.key)) failed.push({
|
|
3179
|
+
id: r.id,
|
|
3180
|
+
reason: message
|
|
3181
|
+
});
|
|
3182
|
+
else if (skip) skipped.push({
|
|
3183
|
+
...skip,
|
|
3184
|
+
id: r.id
|
|
3185
|
+
});
|
|
3186
|
+
else if (this.handedKeys.has(r.key)) processed++;
|
|
3187
|
+
});
|
|
2878
3188
|
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
|
-
],
|
|
3189
|
+
matched: this.matched,
|
|
3190
|
+
processed,
|
|
3191
|
+
skipped,
|
|
3192
|
+
failed,
|
|
2892
3193
|
aborted: {
|
|
2893
3194
|
status: errorStatus(error),
|
|
2894
3195
|
message
|
|
@@ -2983,7 +3284,7 @@ async function setStreamedTarget(ctx, action, disabled) {
|
|
|
2983
3284
|
return findRowsByIds(table, batch, overlay, select);
|
|
2984
3285
|
}
|
|
2985
3286
|
};
|
|
2986
|
-
const target = new StreamedTarget(query ? query.matched : source.ids.length, ctx, source, batchSize, action, disabled);
|
|
3287
|
+
const target = new StreamedTarget(query ? query.matched : requestCount(ctx, source.ids.length), ctx, source, batchSize, action, disabled);
|
|
2987
3288
|
ctx.set(dbActionTargetKey, target);
|
|
2988
3289
|
return {
|
|
2989
3290
|
target,
|
|
@@ -3012,6 +3313,8 @@ const ACTION_SLOTS = [
|
|
|
3012
3313
|
dbActionInputSlot,
|
|
3013
3314
|
dbActionIdSlot,
|
|
3014
3315
|
dbActionIdsSlot,
|
|
3316
|
+
dbActionRequestIdsKey,
|
|
3317
|
+
dbActionPreScopeSlot,
|
|
3015
3318
|
dbActionRowSlot,
|
|
3016
3319
|
dbActionRowsSlot,
|
|
3017
3320
|
dbActionOverlaySlot,
|
|
@@ -3319,6 +3622,398 @@ function getDbEndpoint(target, method) {
|
|
|
3319
3622
|
}
|
|
3320
3623
|
}
|
|
3321
3624
|
//#endregion
|
|
3625
|
+
//#region src/decorations/decoration-index.ts
|
|
3626
|
+
/** A decoration key is a valid `$select` URL name: top-level, no `$` prefix, no dots. */
|
|
3627
|
+
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3628
|
+
/**
|
|
3629
|
+
* The own readable paths a `requires` path stands for: itself when it is an
|
|
3630
|
+
* own field, else — a parent object of a flattened (SQL) readable — every own
|
|
3631
|
+
* field below it. Empty when the path is neither.
|
|
3632
|
+
*/
|
|
3633
|
+
function ownLeaves(path, ownPaths) {
|
|
3634
|
+
if (ownPaths.has(path)) return [path];
|
|
3635
|
+
const prefix = `${path}.`;
|
|
3636
|
+
return [...ownPaths].filter((own) => own.startsWith(prefix));
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* Validates `@DbDecorations` metadata against the bound readable and indexes
|
|
3640
|
+
* it. Throws `[moost-db]` errors (once per class — the caller memoizes).
|
|
3641
|
+
*/
|
|
3642
|
+
function buildDecorationIndex(controller, meta, source) {
|
|
3643
|
+
const fail = (message) => {
|
|
3644
|
+
throw new Error(`[moost-db] ${controller}: @DbDecorations ${message}`);
|
|
3645
|
+
};
|
|
3646
|
+
const { type } = meta;
|
|
3647
|
+
if (type.type.kind !== "object") fail("expects an object interface");
|
|
3648
|
+
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");
|
|
3649
|
+
const keys = [...type.type.props.keys()];
|
|
3650
|
+
const fieldPaths = /* @__PURE__ */ new Set();
|
|
3651
|
+
for (const path of source.flatMap?.keys() ?? []) for (let at = path.indexOf(".");; at = path.indexOf(".", at + 1)) {
|
|
3652
|
+
fieldPaths.add(at < 0 ? path : path.slice(0, at));
|
|
3653
|
+
if (at < 0) break;
|
|
3654
|
+
}
|
|
3655
|
+
const isField = (path) => fieldPaths.has(path);
|
|
3656
|
+
for (const key of keys) {
|
|
3657
|
+
if (!KEY_RE.test(key)) fail(`key "${key}" must be a plain top-level identifier (no "$" prefix, no dots)`);
|
|
3658
|
+
if (isField(key) || source.relations?.has(key) || source.navFields.has(key)) fail(`key "${key}" collides with a field or relation of the bound readable`);
|
|
3659
|
+
}
|
|
3660
|
+
const requires = /* @__PURE__ */ new Map();
|
|
3661
|
+
const leavesOf = /* @__PURE__ */ new Map();
|
|
3662
|
+
const nested = (path) => {
|
|
3663
|
+
const prefix = `${path}.`;
|
|
3664
|
+
const below = [...source.flatMap?.keys() ?? []].filter((p) => p.startsWith(prefix));
|
|
3665
|
+
return below.filter((p) => (0, _atscript_db.selfOrAncestor)(p, source.navFields) === void 0 && !below.some((other) => other.startsWith(`${p}.`)));
|
|
3666
|
+
};
|
|
3667
|
+
for (const key of keys) requires.set(key, []);
|
|
3668
|
+
for (const [key, paths] of Object.entries(meta.requires)) {
|
|
3669
|
+
if (!requires.has(key)) fail(`\`requires\` names "${key}", which is not a declared decoration`);
|
|
3670
|
+
const resolved = [];
|
|
3671
|
+
for (const path of paths) {
|
|
3672
|
+
const leaves = ownLeaves(path, source.ownPaths);
|
|
3673
|
+
if (leaves.length === 0) fail(`"${key}" requires "${path}", which is not an own field of the bound readable`);
|
|
3674
|
+
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)`);
|
|
3675
|
+
for (const leaf of leaves) {
|
|
3676
|
+
if (leavesOf.has(leaf)) continue;
|
|
3677
|
+
const below = nested(leaf);
|
|
3678
|
+
if (below.length > 0) leavesOf.set(leaf, below);
|
|
3679
|
+
}
|
|
3680
|
+
resolved.push(...leaves);
|
|
3681
|
+
}
|
|
3682
|
+
requires.set(key, [...new Set(resolved)]);
|
|
3683
|
+
}
|
|
3684
|
+
const visibleOn = /* @__PURE__ */ new Map();
|
|
3685
|
+
for (const [key, paths] of requires) {
|
|
3686
|
+
const all = new Set(paths);
|
|
3687
|
+
for (const path of paths) for (const leaf of leavesOf.get(path) ?? []) all.add(leaf);
|
|
3688
|
+
visibleOn.set(key, [...all]);
|
|
3689
|
+
}
|
|
3690
|
+
return {
|
|
3691
|
+
type,
|
|
3692
|
+
keys,
|
|
3693
|
+
keySet: new Set(keys),
|
|
3694
|
+
requires,
|
|
3695
|
+
visibleOn,
|
|
3696
|
+
leavesOf,
|
|
3697
|
+
memo: { meta: /* @__PURE__ */ new WeakMap() }
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
//#endregion
|
|
3701
|
+
//#region src/select-shape.ts
|
|
3702
|
+
/** The included (`1` / `true`) and excluded (`0` / `false`) keys of a `$select` map. */
|
|
3703
|
+
function mapShape(map) {
|
|
3704
|
+
const included = [];
|
|
3705
|
+
const excluded = [];
|
|
3706
|
+
for (const [key, value] of Object.entries(map)) if (value === 1 || value === true) included.push(key);
|
|
3707
|
+
else if (value === 0 || value === false) excluded.push(key);
|
|
3708
|
+
return {
|
|
3709
|
+
included,
|
|
3710
|
+
excluded
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
/** The one splitter every `$select` consumer in the controller reads. */
|
|
3714
|
+
function selectShape(raw) {
|
|
3715
|
+
if (raw === void 0 || raw === null) return { kind: "all" };
|
|
3716
|
+
if (Array.isArray(raw)) return {
|
|
3717
|
+
kind: "list",
|
|
3718
|
+
items: raw
|
|
3719
|
+
};
|
|
3720
|
+
const map = raw;
|
|
3721
|
+
return {
|
|
3722
|
+
kind: "map",
|
|
3723
|
+
map,
|
|
3724
|
+
...mapShape(map)
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
//#endregion
|
|
3728
|
+
//#region src/decorations/decoration-planner.ts
|
|
3729
|
+
const NO_DECORATIONS = /* @__PURE__ */ new Set();
|
|
3730
|
+
/**
|
|
3731
|
+
* The decoration plumbing of one controller (`@DbDecorations`, since 0.1.148):
|
|
3732
|
+
* rewrites a read's `$select` ({@link plan}), decides what the response
|
|
3733
|
+
* carries once the final projection is known ({@link serve}), and builds the
|
|
3734
|
+
* `/meta` view ({@link meta}). The request gate is not here — decorations are
|
|
3735
|
+
* virtual entries of the {@link FieldCapabilityIndex}.
|
|
3736
|
+
*/
|
|
3737
|
+
var DecorationPlanner = class {
|
|
3738
|
+
index;
|
|
3739
|
+
host;
|
|
3740
|
+
constructor(index, host) {
|
|
3741
|
+
this.index = index;
|
|
3742
|
+
this.host = host;
|
|
3743
|
+
}
|
|
3744
|
+
visible(key) {
|
|
3745
|
+
return this.host.capabilities().decorationVisible(key, this.host.isVisible);
|
|
3746
|
+
}
|
|
3747
|
+
requiresOf(keys) {
|
|
3748
|
+
return [...new Set(keys.flatMap((key) => this.index.requires.get(key)))];
|
|
3749
|
+
}
|
|
3750
|
+
/**
|
|
3751
|
+
* Splits the wire `$select` of a read (non-grouped: the gate already
|
|
3752
|
+
* refused a decoration in a grouped one): `requested` is every declared
|
|
3753
|
+
* decoration key whose sources are visible and that the client named — or,
|
|
3754
|
+
* without a `$select`, all of them (an exclusion map: all minus the excluded
|
|
3755
|
+
* ones). The real `$select` is widened by the requested keys' `requires`;
|
|
3756
|
+
* the paths added only for the hook (`requiresOnly`) are stripped again.
|
|
3757
|
+
* The result keeps the representation of `raw` (array or map).
|
|
3758
|
+
*/
|
|
3759
|
+
plan(raw) {
|
|
3760
|
+
const { keySet, keys } = this.index;
|
|
3761
|
+
const shape = selectShape(raw);
|
|
3762
|
+
if (shape.kind === "all") return {
|
|
3763
|
+
select: void 0,
|
|
3764
|
+
requested: keys.filter((k) => this.visible(k)),
|
|
3765
|
+
requiresOnly: []
|
|
3766
|
+
};
|
|
3767
|
+
const passthrough = {
|
|
3768
|
+
select: raw,
|
|
3769
|
+
requested: [],
|
|
3770
|
+
requiresOnly: []
|
|
3771
|
+
};
|
|
3772
|
+
if (shape.kind === "list") {
|
|
3773
|
+
const named = shape.items.filter((item) => typeof item === "string" && keySet.has(item));
|
|
3774
|
+
if (named.length === 0) return passthrough;
|
|
3775
|
+
const real = shape.items.filter((item) => !(typeof item === "string" && keySet.has(item)));
|
|
3776
|
+
const have = new Set(real.filter((item) => typeof item === "string"));
|
|
3777
|
+
return this.inclusion([...new Set(named)], real, have, (list) => list);
|
|
3778
|
+
}
|
|
3779
|
+
const { map, included, excluded } = shape;
|
|
3780
|
+
if (included.length === 0 && excluded.length > 0) {
|
|
3781
|
+
const skip = new Set(excluded);
|
|
3782
|
+
const requested = keys.filter((key) => !skip.has(key) && this.visible(key));
|
|
3783
|
+
const real = Object.fromEntries(Object.entries(map).filter(([k]) => !keySet.has(k)));
|
|
3784
|
+
const requiresOnly = /* @__PURE__ */ new Set();
|
|
3785
|
+
const remaining = new Set(Object.keys(real));
|
|
3786
|
+
const unexclude = (key) => {
|
|
3787
|
+
requiresOnly.add(key);
|
|
3788
|
+
remaining.delete(key);
|
|
3789
|
+
delete real[key];
|
|
3790
|
+
};
|
|
3791
|
+
for (const path of this.requiresOf(requested)) {
|
|
3792
|
+
const hit = (0, _atscript_db.selfOrAncestor)(path, remaining);
|
|
3793
|
+
if (hit !== void 0) {
|
|
3794
|
+
unexclude(hit);
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
const prefix = `${path}.`;
|
|
3798
|
+
for (const key of remaining) if (key.startsWith(prefix)) unexclude(key);
|
|
3799
|
+
}
|
|
3800
|
+
return {
|
|
3801
|
+
select: Object.keys(real).length > 0 ? real : void 0,
|
|
3802
|
+
requested,
|
|
3803
|
+
requiresOnly: [...requiresOnly]
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
const named = included.filter((k) => keySet.has(k));
|
|
3807
|
+
if (named.length === 0) return passthrough;
|
|
3808
|
+
const real = included.filter((k) => !keySet.has(k));
|
|
3809
|
+
return this.inclusion(named, real, new Set(real), (list) => Object.fromEntries(list.map((k) => [k, 1])));
|
|
3810
|
+
}
|
|
3811
|
+
/**
|
|
3812
|
+
* The inclusion forms: `real` (the client's own selection) plus the
|
|
3813
|
+
* requested decorations' `requires` that it lacks, re-emitted by `emit`.
|
|
3814
|
+
*/
|
|
3815
|
+
inclusion(named, real, have, emit) {
|
|
3816
|
+
const requested = named.filter((key) => this.visible(key));
|
|
3817
|
+
const extra = this.requiresOf(requested).filter((path) => (0, _atscript_db.selfOrAncestor)(path, have) === void 0);
|
|
3818
|
+
const { list, added } = this.nonEmptyInclusion([...real, ...extra]);
|
|
3819
|
+
return {
|
|
3820
|
+
select: emit(list),
|
|
3821
|
+
requested,
|
|
3822
|
+
requiresOnly: [...extra.filter((path) => !this.host.preferred.has(path)), ...added],
|
|
3823
|
+
selected: [...have]
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
/**
|
|
3827
|
+
* An inclusion list is never empty (an empty one selects everything): a
|
|
3828
|
+
* request naming only decorations with nothing to read selects the
|
|
3829
|
+
* `preferredId` fields (which the response carries anyway) or, without one,
|
|
3830
|
+
* a single visible field that is stripped again (`added`).
|
|
3831
|
+
*/
|
|
3832
|
+
nonEmptyInclusion(list) {
|
|
3833
|
+
if (list.length > 0) return {
|
|
3834
|
+
list,
|
|
3835
|
+
added: []
|
|
3836
|
+
};
|
|
3837
|
+
const { preferred } = this.host;
|
|
3838
|
+
if (preferred.size > 0) return {
|
|
3839
|
+
list: [...preferred],
|
|
3840
|
+
added: []
|
|
3841
|
+
};
|
|
3842
|
+
const field = this.host.firstVisibleField();
|
|
3843
|
+
return field === void 0 ? {
|
|
3844
|
+
list,
|
|
3845
|
+
added: []
|
|
3846
|
+
} : {
|
|
3847
|
+
list: [field],
|
|
3848
|
+
added: [field]
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
/**
|
|
3852
|
+
* The decoration step once the final projection is known: a requested key
|
|
3853
|
+
* is served only if every `requires` path survived `transformProjection`,
|
|
3854
|
+
* the seal and the preferred-id widening (a policy that strips a source
|
|
3855
|
+
* silently drops the decoration, as it drops a field). `kept` is the final
|
|
3856
|
+
* projection's paths (`null` = every field).
|
|
3857
|
+
*/
|
|
3858
|
+
serve(plan, kept) {
|
|
3859
|
+
const { keys, requires, leavesOf } = this.index;
|
|
3860
|
+
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);
|
|
3861
|
+
const dropPaths = plan.requiresOnly.map((path) => path.split("."));
|
|
3862
|
+
const keepPaths = plan.requiresOnly.map((path) => (plan.selected ?? []).filter((sel) => sel.startsWith(`${path}.`)).map((sel) => sel.split(".")));
|
|
3863
|
+
const selectedPaths = plan.selected?.map((sel) => sel.split("."));
|
|
3864
|
+
if (plan.requested.length === 0) return {
|
|
3865
|
+
served: NO_DECORATIONS,
|
|
3866
|
+
dropKeys: keys,
|
|
3867
|
+
dropPaths,
|
|
3868
|
+
keepPaths,
|
|
3869
|
+
selectedPaths
|
|
3870
|
+
};
|
|
3871
|
+
const keptSet = kept === null ? void 0 : new Set(kept);
|
|
3872
|
+
const served = new Set(plan.requested.filter((key) => keptSet === void 0 || requires.get(key).every((path) => carried(path, keptSet))));
|
|
3873
|
+
return {
|
|
3874
|
+
served,
|
|
3875
|
+
dropKeys: keys.filter((key) => !served.has(key)),
|
|
3876
|
+
dropPaths,
|
|
3877
|
+
keepPaths,
|
|
3878
|
+
selectedPaths
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* The `/meta` envelope with the declared decorations: each one still
|
|
3883
|
+
* present in `decorations` (an overlay may delete props to hide a
|
|
3884
|
+
* decoration per principal) whose sources are visible is kept and gets its
|
|
3885
|
+
* `fields[key]` entry (from its virtual capability-index entry); the others
|
|
3886
|
+
* are pruned, and `decorations` is dropped when none is left. Runs after
|
|
3887
|
+
* `applyMetaOverlay` — an overlay never sees the decoration `fields`
|
|
3888
|
+
* entries. Memoized per input while visibility is not request-scoped.
|
|
3889
|
+
*/
|
|
3890
|
+
meta(meta) {
|
|
3891
|
+
if (!meta.decorations) return meta;
|
|
3892
|
+
const { memo, keySet } = this.index;
|
|
3893
|
+
const memoize = !this.host.scoped;
|
|
3894
|
+
const hit = memoize ? memo.meta.get(meta) : void 0;
|
|
3895
|
+
if (hit) return hit;
|
|
3896
|
+
const capabilities = this.host.capabilities();
|
|
3897
|
+
const props = meta.decorations.type.props ?? {};
|
|
3898
|
+
const kept = {};
|
|
3899
|
+
const fields = { ...meta.fields };
|
|
3900
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
3901
|
+
const cap = keySet.has(key) && this.visible(key) ? capabilities.decorationCap(key) : void 0;
|
|
3902
|
+
if (!cap) continue;
|
|
3903
|
+
kept[key] = prop;
|
|
3904
|
+
fields[key] = {
|
|
3905
|
+
sortable: cap.sortable,
|
|
3906
|
+
filterable: cap.filterable,
|
|
3907
|
+
decoration: true
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
let out;
|
|
3911
|
+
if (Object.keys(kept).length === 0) {
|
|
3912
|
+
const { decorations: _dropped, ...rest } = meta;
|
|
3913
|
+
out = rest;
|
|
3914
|
+
} else out = {
|
|
3915
|
+
...meta,
|
|
3916
|
+
fields,
|
|
3917
|
+
decorations: {
|
|
3918
|
+
...meta.decorations,
|
|
3919
|
+
type: {
|
|
3920
|
+
...meta.decorations.type,
|
|
3921
|
+
props: kept
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
if (memoize) memo.meta.set(meta, out);
|
|
3926
|
+
return out;
|
|
3927
|
+
}
|
|
3928
|
+
/** The declared interface serialized for `/meta.decorations`, once per class. */
|
|
3929
|
+
serialized(serialize) {
|
|
3930
|
+
return this.index.memo.serialized ??= serialize();
|
|
3931
|
+
}
|
|
3932
|
+
};
|
|
3933
|
+
/** Removes what a read must not carry — the unserved declared keys and the hook-only paths — from `rows`. */
|
|
3934
|
+
function stripDecorations(rows, read) {
|
|
3935
|
+
const { dropKeys, dropPaths, keepPaths, selectedPaths } = read;
|
|
3936
|
+
if (dropKeys.length === 0 && dropPaths.length === 0) return;
|
|
3937
|
+
const owned = (prefix) => selectedPaths !== void 0 && selectedPaths.some((sel) => prefix.every((part, i) => sel[i] === part));
|
|
3938
|
+
for (const row of rows) {
|
|
3939
|
+
for (const key of dropKeys) delete row[key];
|
|
3940
|
+
dropPaths.forEach((parts, i) => {
|
|
3941
|
+
const keep = keepPaths[i] ?? [];
|
|
3942
|
+
if (keep.length === 0) deleteDescending(row, parts, 0, selectedPaths !== void 0, owned);
|
|
3943
|
+
else pruneExcept(row, parts, 0, keep);
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
/** An object without keys, or an array (of any nesting) holding nothing but such values (what a strip left of a parent). */
|
|
3948
|
+
function isHollow(value) {
|
|
3949
|
+
if (Array.isArray(value)) return value.every((el) => isHollow(el));
|
|
3950
|
+
return (0, _atscript_db.isPlainObject)(value) && Object.keys(value).length === 0;
|
|
3951
|
+
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Deletes the path `parts` below `value`, descending through arrays of objects
|
|
3954
|
+
* (`items.qty` strips every element's `qty`). With `clean` (an inclusion read),
|
|
3955
|
+
* a parent the strip emptied — or a `null` one — goes too, unless the client
|
|
3956
|
+
* selected something at or below it.
|
|
3957
|
+
*/
|
|
3958
|
+
function deleteDescending(value, parts, depth, clean, owned) {
|
|
3959
|
+
if (Array.isArray(value)) {
|
|
3960
|
+
const dropEmptied = clean && depth > 0 && !owned(parts.slice(0, depth));
|
|
3961
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
3962
|
+
const el = value[i];
|
|
3963
|
+
const wasHollow = isHollow(el);
|
|
3964
|
+
deleteDescending(el, parts, depth, clean, owned);
|
|
3965
|
+
if (dropEmptied && !wasHollow && isHollow(el)) value.splice(i, 1);
|
|
3966
|
+
}
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
if (!(0, _atscript_db.isPlainObject)(value)) return;
|
|
3970
|
+
const key = parts[depth];
|
|
3971
|
+
if (depth === parts.length - 1) {
|
|
3972
|
+
delete value[key];
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
const child = value[key];
|
|
3976
|
+
const prefix = parts.slice(0, depth + 1);
|
|
3977
|
+
if (child === null && clean && !owned(prefix)) {
|
|
3978
|
+
delete value[key];
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
deleteDescending(child, parts, depth + 1, clean, owned);
|
|
3982
|
+
if (clean && isHollow(child) && !owned(prefix)) delete value[key];
|
|
3983
|
+
}
|
|
3984
|
+
/**
|
|
3985
|
+
* Below `parts`, deletes everything except the branches leading to `keep`
|
|
3986
|
+
* (client-selected descendants), through arrays of objects as well.
|
|
3987
|
+
*/
|
|
3988
|
+
function pruneExcept(value, parts, depth, keep) {
|
|
3989
|
+
if (Array.isArray(value)) {
|
|
3990
|
+
for (const el of value) pruneExcept(el, parts, depth, keep);
|
|
3991
|
+
return;
|
|
3992
|
+
}
|
|
3993
|
+
if (!(0, _atscript_db.isPlainObject)(value)) return;
|
|
3994
|
+
if (depth < parts.length) {
|
|
3995
|
+
pruneExcept(value[parts[depth]], parts, depth + 1, keep);
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
prune(value, parts.length, keep.filter((k) => k.length > parts.length));
|
|
3999
|
+
}
|
|
4000
|
+
function prune(value, depth, branches) {
|
|
4001
|
+
if (Array.isArray(value)) {
|
|
4002
|
+
for (const el of value) prune(el, depth, branches);
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
if (!(0, _atscript_db.isPlainObject)(value)) return;
|
|
4006
|
+
for (const key of Object.keys(value)) {
|
|
4007
|
+
const next = branches.filter((b) => b[depth] === key);
|
|
4008
|
+
if (next.length === 0) delete value[key];
|
|
4009
|
+
else if (next.some((b) => b.length > depth + 1)) prune(value[key], depth + 1, next);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
/** `true` when stripping `read` changes nothing — skip the post-hook step. */
|
|
4013
|
+
function stripsNothing(read) {
|
|
4014
|
+
return read === void 0 || read.dropKeys.length === 0 && read.dropPaths.length === 0;
|
|
4015
|
+
}
|
|
4016
|
+
//#endregion
|
|
3322
4017
|
//#region src/decorators.ts
|
|
3323
4018
|
/**
|
|
3324
4019
|
* DI token under which the {@link AtscriptDbReadable} instance
|
|
@@ -3478,7 +4173,8 @@ const QUERY_CONTROLS = [
|
|
|
3478
4173
|
"filter",
|
|
3479
4174
|
"insights",
|
|
3480
4175
|
...dtoControls(QueryControlsDto),
|
|
3481
|
-
"groupBy"
|
|
4176
|
+
"groupBy",
|
|
4177
|
+
"rowOrder"
|
|
3482
4178
|
];
|
|
3483
4179
|
const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
|
|
3484
4180
|
const ONE_CONTROLS = dtoControls(GetOneControlsDto);
|
|
@@ -3504,11 +4200,28 @@ const PATH_OPS = [
|
|
|
3504
4200
|
"aggregate",
|
|
3505
4201
|
"bucket"
|
|
3506
4202
|
];
|
|
4203
|
+
/** The controller of the event that owns the route params (nearest ancestor that set them). */
|
|
4204
|
+
function routedController(ctx) {
|
|
4205
|
+
for (let c = ctx; c; c = c.parent) {
|
|
4206
|
+
try {
|
|
4207
|
+
c.getOwn(_wooksjs_event_core.routeParamsKey);
|
|
4208
|
+
} catch {
|
|
4209
|
+
continue;
|
|
4210
|
+
}
|
|
4211
|
+
return controllerOf(c);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
3507
4214
|
/** The 400 of a filter / sort on a `@db.writeOnly` field. */
|
|
3508
4215
|
function writeOnlyError(path, op) {
|
|
3509
4216
|
const verdict = writeOnlyVerdict(path, op);
|
|
3510
4217
|
return badRequest(verdict.path, verdict.message);
|
|
3511
4218
|
}
|
|
4219
|
+
/**
|
|
4220
|
+
* Validated `@DbDecorations` per readable and controller class — a
|
|
4221
|
+
* `FOR_EVENT` controller is constructed per event, and must not re-validate
|
|
4222
|
+
* (nor re-serialize) its declaration each time.
|
|
4223
|
+
*/
|
|
4224
|
+
const decorationIndexes = /* @__PURE__ */ new WeakMap();
|
|
3512
4225
|
/** Controller classes already warned that `actionRowScope` has no row identity to match by. */
|
|
3513
4226
|
const warnedNoIdentity = /* @__PURE__ */ new WeakSet();
|
|
3514
4227
|
let AsDbReadableController = _AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
@@ -3540,7 +4253,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3540
4253
|
get capabilities() {
|
|
3541
4254
|
const current = this._capabilities;
|
|
3542
4255
|
if (current && current.signature === FieldCapabilityIndex.adapterSignature(this.readable)) return current;
|
|
3543
|
-
const index = new FieldCapabilityIndex(this.readable, this._writeOnlySet);
|
|
4256
|
+
const index = new FieldCapabilityIndex(this.readable, this._writeOnlySet, this._decorations?.visibleOn);
|
|
3544
4257
|
this._capabilities = index;
|
|
3545
4258
|
return index;
|
|
3546
4259
|
}
|
|
@@ -3577,6 +4290,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3577
4290
|
_derivedSource;
|
|
3578
4291
|
/** `@db.writeOnly` paths of `$with` target readables, collected once per target. */
|
|
3579
4292
|
_targetWriteOnly = /* @__PURE__ */ new WeakMap();
|
|
4293
|
+
/** Own leaf paths per readable (bound + `$with` targets), see {@link _leavesOf}. */
|
|
4294
|
+
_targetLeaves = /* @__PURE__ */ new WeakMap();
|
|
3580
4295
|
_indexFieldPathsCache;
|
|
3581
4296
|
/** {@link _nativeSearch} per request, keyed by the request's parsed controls. */
|
|
3582
4297
|
_nativeSearchByRequest = /* @__PURE__ */ new WeakMap();
|
|
@@ -3600,8 +4315,14 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3600
4315
|
_gateFieldsMemo = /* @__PURE__ */ new WeakMap();
|
|
3601
4316
|
/** `true` when a subclass overrides {@link allowedActions}. */
|
|
3602
4317
|
_hasAllowedActions;
|
|
4318
|
+
/** The class's validated `@DbDecorations` (since 0.1.148), `undefined` when none is declared. */
|
|
4319
|
+
_decorations;
|
|
4320
|
+
/** The decoration plumbing of {@link _decorations} — see `DecorationPlanner`. */
|
|
4321
|
+
_planner;
|
|
3603
4322
|
/** `true` when a subclass overrides {@link actionRowScope} (the gate, `$actions` and `/meta/actions` apply it). */
|
|
3604
4323
|
_hasActionRowScope;
|
|
4324
|
+
/** @internal `true` when a subclass overrides {@link resolveRowIds} (every id-addressed endpoint calls it; since 0.1.148). */
|
|
4325
|
+
[ROW_RESOLVES];
|
|
3605
4326
|
/** `true` when the class declares `@DbActionsFrom` (since 0.1.147). */
|
|
3606
4327
|
_hasDelegations;
|
|
3607
4328
|
/** `transformProjection` is overridden (a delegation's id paths are checked against it). */
|
|
@@ -3625,15 +4346,17 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3625
4346
|
this._writeOnlySet = this._writeOnlyOf(resolved);
|
|
3626
4347
|
this._derivedSource = this._derivedSourcesOf(resolved);
|
|
3627
4348
|
this._invertibleFields = this._collectInvertibleFields();
|
|
4349
|
+
this._decorates = typeof this.decorateRows === "function";
|
|
4350
|
+
this._decorations = this._resolveDecorations(new.target);
|
|
3628
4351
|
this._searchFallbackFields = this._collectSearchFallbackFields();
|
|
3629
4352
|
this._preferredIdSet = new Set(resolved.preferredId ?? []);
|
|
3630
4353
|
this._quantityRefByPath = this._collectQuantityRefs();
|
|
3631
4354
|
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
3632
4355
|
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
3633
|
-
this._decorates = typeof this.decorateRows === "function";
|
|
3634
4356
|
const proto = _AsDbReadableController.prototype;
|
|
3635
4357
|
this._hasRowOverlay = this.transformOne !== proto.transformOne || this.transformFilter !== proto.transformFilter;
|
|
3636
4358
|
this._hasActionRowScope = this.actionRowScope !== proto.actionRowScope;
|
|
4359
|
+
this[ROW_RESOLVES] = this.resolveRowIds !== proto.resolveRowIds;
|
|
3637
4360
|
this._hasProjectionHook = this.transformProjection !== proto.transformProjection;
|
|
3638
4361
|
this._hasAllowedActions = this.allowedActions !== proto.allowedActions;
|
|
3639
4362
|
this._hasDelegations = hasActionDelegations(new.target);
|
|
@@ -3650,6 +4373,37 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3650
4373
|
sealedFor: (readable, prefix = "") => this._sealedFor(readable, prefix)
|
|
3651
4374
|
};
|
|
3652
4375
|
this._idOpts = scoped ? { isFieldVisible: isVisible } : void 0;
|
|
4376
|
+
this._planner = this._decorations && new DecorationPlanner(this._decorations, {
|
|
4377
|
+
isVisible,
|
|
4378
|
+
scoped,
|
|
4379
|
+
preferred: this._preferredIdSet,
|
|
4380
|
+
capabilities: () => this.capabilities,
|
|
4381
|
+
firstVisibleField: () => this._invertibleFields.find((path) => isVisible(path))
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4384
|
+
/**
|
|
4385
|
+
* The class's `@DbDecorations`, validated against the bound readable once
|
|
4386
|
+
* per class and readable (a `[moost-db]` error when invalid); warns once
|
|
4387
|
+
* when `decorateRows` is not implemented.
|
|
4388
|
+
*/
|
|
4389
|
+
_resolveDecorations(ctor) {
|
|
4390
|
+
const meta = getAtscriptDbMate().read(ctor)?.atscript_db_decorations;
|
|
4391
|
+
if (!meta) return void 0;
|
|
4392
|
+
let perCtor = decorationIndexes.get(this.readable);
|
|
4393
|
+
if (!perCtor) decorationIndexes.set(this.readable, perCtor = /* @__PURE__ */ new Map());
|
|
4394
|
+
let index = perCtor.get(ctor);
|
|
4395
|
+
if (!index) {
|
|
4396
|
+
index = buildDecorationIndex(ctor.name, meta, {
|
|
4397
|
+
flatMap: this.readable.flatMap,
|
|
4398
|
+
relations: this.readable.relations,
|
|
4399
|
+
navFields: FieldCapabilityIndex.navPathsOf(this.readable),
|
|
4400
|
+
ownPaths: new Set(this._invertibleFields),
|
|
4401
|
+
writeOnly: this._writeOnlySet
|
|
4402
|
+
});
|
|
4403
|
+
perCtor.set(ctor, index);
|
|
4404
|
+
if (!this._decorates) this.logger.warn(`@DbDecorations declares ${index.keys.join(", ")} but ${ctor.name} does not implement decorateRows() — the keys are never filled`);
|
|
4405
|
+
}
|
|
4406
|
+
return index;
|
|
3653
4407
|
}
|
|
3654
4408
|
/**
|
|
3655
4409
|
* The identifications this request may address rows through (since
|
|
@@ -3675,7 +4429,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3675
4429
|
}
|
|
3676
4430
|
_collectInvertibleFields() {
|
|
3677
4431
|
const out = [];
|
|
3678
|
-
const nav = this.
|
|
4432
|
+
const nav = FieldCapabilityIndex.navPathsOf(this.readable);
|
|
3679
4433
|
for (const fd of this.readable.fieldDescriptors) {
|
|
3680
4434
|
if (fd.ignored) continue;
|
|
3681
4435
|
if ((0, _atscript_db.selfOrAncestor)(fd.path, nav) !== void 0) continue;
|
|
@@ -3732,7 +4486,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3732
4486
|
* (`$vector`) or a geo index (`/geo`, `$index`) reading a hidden path
|
|
3733
4487
|
* answers exactly like a nonexistent index (400); a hidden DEFAULT text
|
|
3734
4488
|
* index falls back to the `@db.column.searchable` substring search over
|
|
3735
|
-
* visible fields (or ignores the term when there are none
|
|
4489
|
+
* visible fields (or ignores the term when there are none — on list
|
|
4490
|
+
* endpoints; query targets, delegated targets and {@link resolveQuery}
|
|
4491
|
+
* answer 400 `TARGET_INVALID` instead). A
|
|
3736
4492
|
* `@db.column.derived` field is visible only while its source path is,
|
|
3737
4493
|
* and one whose source is hidden is sealed out of every read projection
|
|
3738
4494
|
* for the request, like a `@db.writeOnly` field. The same holds for a
|
|
@@ -3765,7 +4521,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3765
4521
|
checkCapabilities(parsed) {
|
|
3766
4522
|
const capabilities = this.capabilities;
|
|
3767
4523
|
const isVisible = this.fieldVisibility.isVisible;
|
|
3768
|
-
const
|
|
4524
|
+
const select = parsed.controls?.$select;
|
|
4525
|
+
const refs = (0, _atscript_db.collectQueryPaths)(parsed, Array.isArray(select) && select.some((item) => typeof item !== "string") || void 0);
|
|
3769
4526
|
if (refs.unsupportedOperator !== void 0) return badRequest(refs.unsupportedOperator, (0, _atscript_db.unsupportedOperatorMessage)(refs.unsupportedOperator));
|
|
3770
4527
|
const relState = { nodes: 0 };
|
|
3771
4528
|
for (const ref of refs.filter) {
|
|
@@ -3781,9 +4538,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3781
4538
|
const recorded = parsed.controls ? this._clientWith.get(parsed.controls) : void 0;
|
|
3782
4539
|
const withRelError = this._relationGate().checkWith(recorded === void 0 ? liveWith : recorded?.tree, relState);
|
|
3783
4540
|
if (withRelError) return withRelError;
|
|
3784
|
-
for (const op of PATH_OPS)
|
|
3785
|
-
const
|
|
3786
|
-
|
|
4541
|
+
for (const op of PATH_OPS) {
|
|
4542
|
+
const gateOp = op === "select" && refs.aggregateMode ? "groupedSelect" : op;
|
|
4543
|
+
for (const path of refs[op]) {
|
|
4544
|
+
const verdict = capabilities.check(path, gateOp, isVisible);
|
|
4545
|
+
if (verdict) return badRequest(verdict.path, verdict.message);
|
|
4546
|
+
}
|
|
3787
4547
|
}
|
|
3788
4548
|
const having = (0, _atscript_db.checkHavingKeys)(refs);
|
|
3789
4549
|
if (having) return badRequest(having.path, having.message);
|
|
@@ -3809,7 +4569,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3809
4569
|
navFields: capabilities.navFields
|
|
3810
4570
|
});
|
|
3811
4571
|
} catch (error) {
|
|
3812
|
-
if (!(error instanceof _atscript_db.DbError)) throw error;
|
|
4572
|
+
if (!(error instanceof _atscript_db.DbError) || error.code !== "INVALID_QUERY") throw error;
|
|
3813
4573
|
const [issue] = error.errors;
|
|
3814
4574
|
return badRequest(issue.path, issue.message);
|
|
3815
4575
|
}
|
|
@@ -3987,12 +4747,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3987
4747
|
const sub = nested.$select ?? rel.$select;
|
|
3988
4748
|
return {
|
|
3989
4749
|
...nested,
|
|
3990
|
-
$select: this._sealSelect(sub, sealed)
|
|
4750
|
+
$select: this._sealSelect(sub, sealed, target)
|
|
3991
4751
|
};
|
|
3992
4752
|
});
|
|
3993
4753
|
const out = {
|
|
3994
4754
|
...controls,
|
|
3995
|
-
$select: this._sealSelect(select, vis.sealedFor(this.readable))
|
|
4755
|
+
$select: this._sealSelect(select, vis.sealedFor(this.readable), this.readable)
|
|
3996
4756
|
};
|
|
3997
4757
|
if ($with !== controls.$with) out.$with = $with;
|
|
3998
4758
|
return out;
|
|
@@ -4261,34 +5021,24 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4261
5021
|
* The rows the row-level action `actionName` may run on (since 0.1.145),
|
|
4262
5022
|
* as an extra row filter; `undefined` or `{}` = no restriction (the
|
|
4263
5023
|
* default). Enforced by the action gate — the action's ids / rows are
|
|
4264
|
-
* loaded under {@link rowOverlay}, then checked against this filter
|
|
4265
|
-
*
|
|
5024
|
+
* loaded under {@link rowOverlay}, then checked against this filter (or —
|
|
5025
|
+
* without an overlay — the filter joins the load), so an id outside it gets
|
|
5026
|
+
* the same 404 "Row not found for action identifier"
|
|
4266
5027
|
* as a missing one — and reflected in `$actions` and
|
|
4267
5028
|
* `GET /meta/actions`, which list the action only on rows inside it.
|
|
4268
5029
|
*
|
|
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).
|
|
5030
|
+
* The hook receives the candidate rows (`ctx`), so a scope can depend on
|
|
5031
|
+
* them — what `ctx.purpose`, `ctx.ids` and `ctx.loadRows` hold, and when the
|
|
5032
|
+
* hook is asked before any load, is documented on {@link TDbActionScopeContext}.
|
|
5033
|
+
* Called only with at least one candidate, at most once per action per
|
|
5034
|
+
* evaluation. An answer restricting nothing (`undefined`, `null`, `{}`)
|
|
5035
|
+
* makes the gate load no row at all; a restriction is folded into the one
|
|
5036
|
+
* row load. The result only restricts (`ids ∧ rowOverlay ∧ scope`); a throw
|
|
5037
|
+
* fails the request — never a silent "allow". The filter runs straight
|
|
5038
|
+
* against the bound readable: it may use fields {@link hasField} hides, and
|
|
5039
|
+
* nothing of it reaches the response. Runs after {@link prepareRequest} and,
|
|
5040
|
+
* on the action route, after the request body is read and
|
|
5041
|
+
* {@link resolveRowIds}.
|
|
4292
5042
|
*
|
|
4293
5043
|
* Not overriding it costs nothing; a one-parameter override keeps working.
|
|
4294
5044
|
* moost-db always passes `ctx` — it is optional in the signature only so
|
|
@@ -4341,6 +5091,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4341
5091
|
* {@link checkCapabilities}, {@link hasField}), where the client
|
|
4342
5092
|
* predicates' {@link transformRelationFilter} also runs: a query target
|
|
4343
5093
|
* never filters on, nor counts by, a field the caller can't read.
|
|
5094
|
+
* {@link resolveQuery} does not call it (there is no action of this
|
|
5095
|
+
* controller to scope).
|
|
4344
5096
|
*
|
|
4345
5097
|
* @since 0.1.147
|
|
4346
5098
|
*/
|
|
@@ -4354,6 +5106,37 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4354
5106
|
transformProjection(projection) {
|
|
4355
5107
|
return projection;
|
|
4356
5108
|
}
|
|
5109
|
+
/**
|
|
5110
|
+
* The shared projection step of `/query`, `/pages`, `/geo` and `/one`:
|
|
5111
|
+
* splits the declared decoration keys out of the wire `$select`
|
|
5112
|
+
* ({@link DecorationPlanner.plan}), runs {@link transformProjection} on the
|
|
5113
|
+
* rest (decoration keys never reach it — a permission layer needs no
|
|
5114
|
+
* change; their `requires` paths are added so the hook's inputs are read),
|
|
5115
|
+
* then seals every projection level. `finish` completes it once the
|
|
5116
|
+
* endpoint is past its own checks: the preferred-id widening (an
|
|
5117
|
+
* `HttpError` for a mixed `$select`) and the decoration step — what every
|
|
5118
|
+
* read endpoint then passes to {@link _runReadWithActions}.
|
|
5119
|
+
*/
|
|
5120
|
+
async _projectRead(controls) {
|
|
5121
|
+
const plan = this._planner?.plan(controls.$select);
|
|
5122
|
+
const transformed = await this.transformProjection(plan ? plan.select : controls.$select);
|
|
5123
|
+
const sealed = this._sealControls(controls, transformed);
|
|
5124
|
+
const finish = () => {
|
|
5125
|
+
const select = this.widenPreferredIdProjection(sealed.$select);
|
|
5126
|
+
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
5127
|
+
let kept;
|
|
5128
|
+
const keptPaths = () => kept === void 0 ? kept = this._resolveProjectionForAugmenter(select) : kept;
|
|
5129
|
+
return {
|
|
5130
|
+
select,
|
|
5131
|
+
kept: keptPaths,
|
|
5132
|
+
read: plan && this._planner.serve(plan, keptPaths())
|
|
5133
|
+
};
|
|
5134
|
+
};
|
|
5135
|
+
return {
|
|
5136
|
+
sealed,
|
|
5137
|
+
finish
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
4357
5140
|
widenPreferredIdProjection(projection) {
|
|
4358
5141
|
const widened = this.widenQuantityRefProjection(projection);
|
|
4359
5142
|
if (widened instanceof _moostjs_event_http.HttpError) return widened;
|
|
@@ -4375,12 +5158,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4375
5158
|
return out;
|
|
4376
5159
|
}
|
|
4377
5160
|
_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);
|
|
5161
|
+
if (Object.keys(projection).length === 0) return projection;
|
|
5162
|
+
const shape = mapShape(projection);
|
|
5163
|
+
const included = new Set(shape.included);
|
|
5164
|
+
const excluded = new Set(shape.excluded);
|
|
4384
5165
|
if (included.size > 0 && excluded.size > 0) return new _moostjs_event_http.HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
|
|
4385
5166
|
if (excluded.size === 0) {
|
|
4386
5167
|
let allPresent = true;
|
|
@@ -4436,12 +5217,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4436
5217
|
return [...projection, ...toAdd];
|
|
4437
5218
|
}
|
|
4438
5219
|
_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);
|
|
5220
|
+
if (Object.keys(projection).length === 0) return projection;
|
|
5221
|
+
const shape = mapShape(projection);
|
|
5222
|
+
const included = new Set(shape.included);
|
|
5223
|
+
const excluded = new Set(shape.excluded);
|
|
4445
5224
|
if (included.size > 0 && excluded.size > 0) return new _moostjs_event_http.HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
|
|
4446
5225
|
if (excluded.size === 0) {
|
|
4447
5226
|
const toAdd = [];
|
|
@@ -4456,22 +5235,11 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4456
5235
|
}
|
|
4457
5236
|
/** Normalize a post-`widenPreferredIdProjection` $select into `string[] | null` (`null` = all fields). */
|
|
4458
5237
|
_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;
|
|
5238
|
+
const shape = selectShape(select);
|
|
5239
|
+
if (shape.kind === "all") return null;
|
|
5240
|
+
if (shape.kind === "list") return [...new Set(shape.items.filter((item) => typeof item === "string"))];
|
|
5241
|
+
const { included, excluded } = shape;
|
|
5242
|
+
if (included.length > 0 && excluded.length === 0) return [...included];
|
|
4475
5243
|
if (excluded.length > 0 && included.length === 0) return this._invertExclusion(new Set(excluded));
|
|
4476
5244
|
throw new _moostjs_event_http.HttpError(500, "[moost-db] mixed inclusion/exclusion projection reached augmenter; widenPreferredIdProjection should have rejected it");
|
|
4477
5245
|
}
|
|
@@ -4517,7 +5285,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4517
5285
|
}
|
|
4518
5286
|
return result;
|
|
4519
5287
|
}
|
|
4520
|
-
async _prepareAugmentation(controls,
|
|
5288
|
+
async _prepareAugmentation(controls, projected) {
|
|
4521
5289
|
if (!controls.$actions) return null;
|
|
4522
5290
|
const [own, delegations] = await Promise.all([this._resolveAugmentEnvelopes(), this._activeDelegations()]);
|
|
4523
5291
|
if (own === null && delegations.length === 0) return null;
|
|
@@ -4527,7 +5295,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4527
5295
|
scopeOverlay = this.rowOverlay();
|
|
4528
5296
|
scopeOverlay.catch(() => {});
|
|
4529
5297
|
}
|
|
4530
|
-
let resolvedProjection =
|
|
5298
|
+
let resolvedProjection = projected.kept();
|
|
4531
5299
|
let widenedSelect = resolvedProjection === null ? null : this._widenSelectForActions(envelopes, resolvedProjection);
|
|
4532
5300
|
if (resolvedProjection !== null && delegations.length > 0) {
|
|
4533
5301
|
const base = widenedSelect ?? resolvedProjection;
|
|
@@ -4632,6 +5400,14 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4632
5400
|
[ACTION_OVERLAY]() {
|
|
4633
5401
|
return this.rowOverlay();
|
|
4634
5402
|
}
|
|
5403
|
+
/**
|
|
5404
|
+
* @internal {@link resolveRowIds} for an action's validated ids (since
|
|
5405
|
+
* 0.1.148): the output validated, duplicate identities collapsed, and the
|
|
5406
|
+
* ids as the client sent them kept for the error bodies and summaries.
|
|
5407
|
+
*/
|
|
5408
|
+
async [ROW_RESOLVE_IDS](ids, ctx) {
|
|
5409
|
+
return applyResolvedIds(ids, await this._runResolveRowIds(ids, ctx));
|
|
5410
|
+
}
|
|
4635
5411
|
/** @internal {@link actionRowScope} for the gate's loaded candidates (since 0.1.147). */
|
|
4636
5412
|
[ACTION_SCOPE](action, ctx) {
|
|
4637
5413
|
return this._actionScope(action, ctx);
|
|
@@ -4653,18 +5429,76 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4653
5429
|
async [RESOLVE_TARGET](req) {
|
|
4654
5430
|
const { action } = req;
|
|
4655
5431
|
const body = parseQueryTargetBody(action, req.query);
|
|
5432
|
+
const { rows, filter, findMany, exclude, visibleOf, cap } = await this._resolveMatching({
|
|
5433
|
+
label: action,
|
|
5434
|
+
body,
|
|
5435
|
+
cap: req.cap,
|
|
5436
|
+
maxExclude: req.maxExclude,
|
|
5437
|
+
overlay: req.overlay,
|
|
5438
|
+
scopeByAction: true,
|
|
5439
|
+
select: [...new Set(req.select)],
|
|
5440
|
+
sortBy: req.select,
|
|
5441
|
+
excludeShapes: req.excludeShapes,
|
|
5442
|
+
visibleOf: req.visibleOf
|
|
5443
|
+
});
|
|
5444
|
+
const byIds = (read, ids, scope, select) => findRowsByIds({ findMany: (q) => read({
|
|
5445
|
+
...q,
|
|
5446
|
+
controls: {
|
|
5447
|
+
...q.controls,
|
|
5448
|
+
$limit: Math.max(ids.length, cap + 1)
|
|
5449
|
+
}
|
|
5450
|
+
}) }, ids, scope, select);
|
|
5451
|
+
const snapshotFields = new Set(req.select);
|
|
5452
|
+
let first = true;
|
|
5453
|
+
return {
|
|
5454
|
+
matched: rows.length,
|
|
5455
|
+
rows,
|
|
5456
|
+
dryRun: body.dryRun === true,
|
|
5457
|
+
exclude,
|
|
5458
|
+
visibleOf,
|
|
5459
|
+
load: (ids, select) => {
|
|
5460
|
+
if (!first) return byIds(findMany, ids, filter, select);
|
|
5461
|
+
first = false;
|
|
5462
|
+
const fields = [...select];
|
|
5463
|
+
if (!fields.every((f) => snapshotFields.has(f))) {
|
|
5464
|
+
const plain = (q) => this.readable.findMany(q);
|
|
5465
|
+
return byIds(plain, ids, void 0, fields);
|
|
5466
|
+
}
|
|
5467
|
+
return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$2(row, new Set([...fields, ...Object.keys(ids[i])])) : void 0));
|
|
5468
|
+
}
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
/**
|
|
5472
|
+
* The shared resolver behind query targets and {@link resolveQuery}: the
|
|
5473
|
+
* query body validated, checked and run as a READ of this controller, then
|
|
5474
|
+
* ONE read of `select` ordered by `sort` — `filter (+ $search) ∧ overlay ∧
|
|
5475
|
+
* scope ∧ ¬exclude`, at most `cap + 1` rows. More than `cap` → 400
|
|
5476
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
5477
|
+
* `TARGET_CHANGED`.
|
|
5478
|
+
*
|
|
5479
|
+
* Everything that depends on the read's visibility — the `$search`
|
|
5480
|
+
* fallback, the native-search memo, the un-appliable-term refusal and the
|
|
5481
|
+
* delegated identity — is computed INSIDE the read child, where the
|
|
5482
|
+
* permission layer's per-request state is the read's.
|
|
5483
|
+
*/
|
|
5484
|
+
async _resolveMatching(spec) {
|
|
5485
|
+
const { label, body } = spec;
|
|
5486
|
+
const cap = Math.min(spec.cap, body.maxRows ?? Infinity);
|
|
4656
5487
|
const parsed = this.parseUrlOr400(body.q.startsWith("?") ? body.q.slice(1) : body.q);
|
|
4657
5488
|
const controls = {};
|
|
4658
5489
|
for (const [k, v] of Object.entries(parsed.controls ?? {})) {
|
|
4659
5490
|
if (v === void 0) continue;
|
|
4660
|
-
if (k !== "$search" && k !== "$index") throw targetInvalid(
|
|
4661
|
-
if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(
|
|
5491
|
+
if (k !== "$search" && k !== "$index") throw targetInvalid(label, `A query target takes a filter, $search and $index only — "${k}" is not accepted`);
|
|
5492
|
+
if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(label, "$search must be a search term");
|
|
4662
5493
|
controls[k] = k === "$search" ? `${v}` : v;
|
|
4663
5494
|
}
|
|
4664
|
-
if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(
|
|
5495
|
+
if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(label, "$index must be an index name");
|
|
4665
5496
|
const exclude = body.exclude ?? [];
|
|
4666
|
-
const shapes =
|
|
5497
|
+
const shapes = spec.excludeShapes ?? [];
|
|
5498
|
+
const sealedSet = () => new Set([...this.fieldVisibility.sealedFor(this.readable), ...this._leavesOf(this.readable).filter((leaf) => !this.fieldVisibility.isVisible(leaf))]);
|
|
4667
5499
|
const check = () => {
|
|
5500
|
+
let sealed;
|
|
5501
|
+
const readableLeaf = (leaf) => !(sealed ??= sealedSet()).has(leaf);
|
|
4668
5502
|
const controlsError = this.validateControls(controls, "query");
|
|
4669
5503
|
if (controlsError) throw new _moostjs_event_http.HttpError(400, controlsError);
|
|
4670
5504
|
const gateError = this.checkCapabilities({
|
|
@@ -4672,71 +5506,160 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4672
5506
|
controls
|
|
4673
5507
|
});
|
|
4674
5508
|
if (gateError) throw gateError;
|
|
4675
|
-
if (exclude.length > 0)
|
|
4676
|
-
|
|
4677
|
-
fields
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
5509
|
+
if (exclude.length > 0) {
|
|
5510
|
+
const source = shapes.length === 0 ? this.idSource : {
|
|
5511
|
+
identifications: [...this.idSource.identifications, ...shapes.map((fields) => ({
|
|
5512
|
+
fields,
|
|
5513
|
+
source: "target"
|
|
5514
|
+
}))],
|
|
5515
|
+
fieldDescriptors: this.readable.fieldDescriptors
|
|
5516
|
+
};
|
|
5517
|
+
try {
|
|
5518
|
+
validateMultiId(exclude, source, spec.maxExclude);
|
|
5519
|
+
} catch (error) {
|
|
5520
|
+
throw validatorErrorToHttp(error) ?? error;
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
const nav = FieldCapabilityIndex.navPathsOf(this.readable);
|
|
5524
|
+
for (const path of spec.selectGate ?? []) {
|
|
5525
|
+
if (this.capabilities.decorationCap(path) || (0, _atscript_db.selfOrAncestor)(path, nav) !== void 0) throw badRequest(path, `Unknown field "${path}"`);
|
|
5526
|
+
const verdict = this.capabilities.check(path, "select", this.fieldVisibility.isVisible);
|
|
5527
|
+
if (verdict) throw badRequest(verdict.path, verdict.message);
|
|
5528
|
+
if (this._writeOnlySet.has(path)) throw badRequest(path, `Field "${path}" is @db.writeOnly`);
|
|
5529
|
+
const prefix = `${path}.`;
|
|
5530
|
+
const leaves = this._leavesOf(this.readable).filter((leaf) => leaf.startsWith(prefix));
|
|
5531
|
+
if (leaves.length > 0 && !leaves.some((leaf) => readableLeaf(leaf))) throw badRequest(path, `Unknown field "${path}"`);
|
|
5532
|
+
}
|
|
5533
|
+
for (const path of spec.sortGate ?? []) {
|
|
5534
|
+
const verdict = this.capabilities.check(path, "sort", this.fieldVisibility.isVisible);
|
|
5535
|
+
if (verdict) throw targetInvalid(label, verdict.message);
|
|
5536
|
+
}
|
|
4682
5537
|
};
|
|
4683
|
-
const ownScope =
|
|
4684
|
-
const [
|
|
5538
|
+
const ownScope = spec.scopeByAction === true && (spec.overlay === "action" || this.queryTargetScope !== _AsDbReadableController.prototype.queryTargetScope);
|
|
5539
|
+
const [read, overlay] = await Promise.all([this._asRead(controls, parsed.filter, async () => {
|
|
4685
5540
|
check();
|
|
4686
|
-
const [clientFilter, readScope] = await Promise.all([
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
5541
|
+
const [clientFilter, readScope, strategy] = await Promise.all([
|
|
5542
|
+
this._relationOverlay(parsed),
|
|
5543
|
+
ownScope ? this.queryTargetScope(label) : void 0,
|
|
5544
|
+
this._resolveReadStrategy(controls)
|
|
5545
|
+
]);
|
|
5546
|
+
const base = spec.overlay === "action" ? clientFilter : await this.transformFilter(clientFilter ?? {});
|
|
5547
|
+
const searched = this.applySearchFallback(base, controls);
|
|
5548
|
+
if (controls.$search && strategy.kind !== "search" && searched === base) throw targetInvalid(label, "$search is not available here");
|
|
5549
|
+
const visibleOf = spec.visibleOf?.filter((f) => this.fieldVisibility.isVisible(f));
|
|
5550
|
+
let select = spec.select;
|
|
5551
|
+
if (spec.selectGate) {
|
|
5552
|
+
const sealed = sealedSet();
|
|
5553
|
+
for (const id of spec.identity ?? []) sealed.delete(id);
|
|
5554
|
+
select = this._sealSelect([...spec.select], sealed, this.readable);
|
|
5555
|
+
}
|
|
5556
|
+
return {
|
|
5557
|
+
searched,
|
|
5558
|
+
readScope,
|
|
5559
|
+
strategy,
|
|
5560
|
+
visibleOf,
|
|
5561
|
+
select
|
|
5562
|
+
};
|
|
5563
|
+
}, spec.routeParams), spec.overlay === "action" ? this.rowOverlay() : void 0]);
|
|
5564
|
+
const { strategy } = read;
|
|
5565
|
+
const filter = (0, _atscript_db.andFilters)(read.searched, overlay, read.readScope, spec.scope, exclude.length > 0 ? { $not: { $or: exclude } } : void 0);
|
|
4691
5566
|
const findMany = (q) => strategy.kind === "search" ? this.readable.search(strategy.term, q, strategy.index) : this.readable.findMany(q);
|
|
4692
|
-
const cap = Math.min(req.cap, body.maxRows ?? Infinity);
|
|
4693
|
-
const sort = {};
|
|
4694
|
-
for (const f of req.select) sort[f] = 1;
|
|
4695
5567
|
const rows = await findMany({
|
|
4696
5568
|
filter,
|
|
4697
5569
|
controls: {
|
|
4698
|
-
$select: [...
|
|
4699
|
-
$sort:
|
|
5570
|
+
$select: [...read.select],
|
|
5571
|
+
$sort: Object.fromEntries(spec.sortBy.map((f) => [f, 1])),
|
|
4700
5572
|
$limit: cap + 1
|
|
4701
5573
|
}
|
|
4702
5574
|
});
|
|
4703
|
-
if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE",
|
|
4704
|
-
if (body.expectCount !== void 0 && body.expectCount !== rows.length) throw new ActionTargetError("TARGET_CHANGED",
|
|
4705
|
-
const byIds = (read, ids, scope, select) => findRowsByIds({ findMany: (q) => read({
|
|
4706
|
-
...q,
|
|
4707
|
-
controls: {
|
|
4708
|
-
...q.controls,
|
|
4709
|
-
$limit: Math.max(ids.length, cap + 1)
|
|
4710
|
-
}
|
|
4711
|
-
}) }, ids, scope, select);
|
|
4712
|
-
const snapshotFields = new Set(req.select);
|
|
4713
|
-
let first = true;
|
|
5575
|
+
if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE", label, `The query matches more than ${cap} rows`, { cap });
|
|
5576
|
+
if (body.expectCount !== void 0 && body.expectCount !== rows.length) throw new ActionTargetError("TARGET_CHANGED", label, `The query now matches ${rows.length} rows (expected ${body.expectCount})`, { matched: rows.length });
|
|
4714
5577
|
return {
|
|
4715
|
-
matched: rows.length,
|
|
4716
5578
|
rows,
|
|
4717
|
-
|
|
5579
|
+
filter,
|
|
5580
|
+
findMany,
|
|
4718
5581
|
exclude,
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
const fields = [...select];
|
|
4723
|
-
if (!fields.every((f) => snapshotFields.has(f))) {
|
|
4724
|
-
const plain = (q) => this.readable.findMany(q);
|
|
4725
|
-
return byIds(plain, ids, void 0, fields);
|
|
4726
|
-
}
|
|
4727
|
-
return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$2(row, new Set([...fields, ...Object.keys(ids[i])])) : void 0));
|
|
4728
|
-
}
|
|
5582
|
+
visibleOf: read.visibleOf,
|
|
5583
|
+
cap,
|
|
5584
|
+
select: read.select
|
|
4729
5585
|
};
|
|
4730
5586
|
}
|
|
4731
5587
|
/**
|
|
5588
|
+
* Rows of THIS controller matching `q`, resolved as a READ of it for the
|
|
5589
|
+
* current event's caller (since 0.1.149) — from your own command, e.g. to
|
|
5590
|
+
* act on "every issue matching this search". `q` is a `GET /query` string
|
|
5591
|
+
* (`$search` / `$index` and a filter only) or a query-target envelope
|
|
5592
|
+
* `{ q, exclude?, expectCount?, maxRows? }` (no `dryRun`).
|
|
5593
|
+
*
|
|
5594
|
+
* The read runs under this controller's full read policy
|
|
5595
|
+
* ({@link prepareRequest} with `endpoint: "query"`, {@link hasField},
|
|
5596
|
+
* {@link validateControls}, the capability / index gate and the
|
|
5597
|
+
* {@link transformFilter} overlay) with the current event's identity.
|
|
5598
|
+
* Route interceptors and guards of the `query` route do not run; read
|
|
5599
|
+
* authorization belongs in {@link prepareRequest}. {@link queryTargetScope}
|
|
5600
|
+
* is not called. Hooks see no route params of the caller (only a call from
|
|
5601
|
+
* the routed event's own controller instance keeps its params); pass route-derived
|
|
5602
|
+
* restrictions as `opts.scope`. Joins the caller's open transaction.
|
|
5603
|
+
*
|
|
5604
|
+
* Rows are ordered by identity (`preferredId`, else the primary key) and
|
|
5605
|
+
* carry the identity fields plus `opts.select` (gated like `/query`
|
|
5606
|
+
* `$select`; `transformProjection` is not applied — hide fields with
|
|
5607
|
+
* {@link hasField}; decoration keys and navigation paths are refused). An
|
|
5608
|
+
* identity-less readable is ordered by `select` (each path sortable, else
|
|
5609
|
+
* `TARGET_INVALID`). More than `opts.cap` (default 1000) rows → 400
|
|
5610
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
5611
|
+
* `TARGET_CHANGED`; a `$search` that can't be applied → 400
|
|
5612
|
+
* `TARGET_INVALID`. Must be awaited inside a running event handler.
|
|
5613
|
+
*/
|
|
5614
|
+
async resolveQuery(q, opts = {}) {
|
|
5615
|
+
let caller;
|
|
5616
|
+
try {
|
|
5617
|
+
caller = (0, _wooksjs_event_core.current)();
|
|
5618
|
+
} catch {
|
|
5619
|
+
throw new Error("[moost-db] resolveQuery must be awaited inside an event handler");
|
|
5620
|
+
}
|
|
5621
|
+
const cap = opts.cap ?? 1e3;
|
|
5622
|
+
if (!Number.isInteger(cap) || cap < 1) throw new Error("[moost-db] resolveQuery: `cap` must be a positive integer");
|
|
5623
|
+
const select = opts.select ?? [];
|
|
5624
|
+
if (!Array.isArray(select) || select.some((p) => typeof p !== "string")) throw new Error("[moost-db] resolveQuery: `select` must be an array of field paths");
|
|
5625
|
+
const ids = this.readable.preferredId?.length ? this.readable.preferredId : this.readable.primaryKeys;
|
|
5626
|
+
const order = ids.length > 0 ? ids : select;
|
|
5627
|
+
if (order.length === 0) throw new Error("[moost-db] resolveQuery: this readable has no identity — pass `select` (rows are ordered by it)");
|
|
5628
|
+
const label = readCurrentActionMeta(caller)?.name ?? "";
|
|
5629
|
+
const body = parseQueryTargetBody(label, typeof q === "string" ? { q } : q);
|
|
5630
|
+
if (body.dryRun !== void 0) throw targetInvalid(label, "resolveQuery takes no `dryRun` — read `query.dryRun` yourself");
|
|
5631
|
+
let sameRoute = false;
|
|
5632
|
+
try {
|
|
5633
|
+
sameRoute = controllerOf(caller) === this && routedController(caller) === this;
|
|
5634
|
+
} catch {}
|
|
5635
|
+
const fields = [...new Set([...ids, ...select])];
|
|
5636
|
+
const { rows, select: sealedSelect } = await this._resolveMatching({
|
|
5637
|
+
label,
|
|
5638
|
+
body,
|
|
5639
|
+
cap,
|
|
5640
|
+
maxExclude: DEFAULT_MAX_ACTION_IDS,
|
|
5641
|
+
overlay: "read",
|
|
5642
|
+
scope: opts.scope,
|
|
5643
|
+
select: fields,
|
|
5644
|
+
sortBy: order,
|
|
5645
|
+
selectGate: select,
|
|
5646
|
+
identity: ids,
|
|
5647
|
+
sortGate: ids.length > 0 ? void 0 : order,
|
|
5648
|
+
routeParams: sameRoute ? void 0 : {}
|
|
5649
|
+
});
|
|
5650
|
+
return rows.map((row) => projectRow$2(row, sealedSelect));
|
|
5651
|
+
}
|
|
5652
|
+
/**
|
|
4732
5653
|
* Runs `fn` as a READ of this controller (since 0.1.147): in a child of
|
|
4733
5654
|
* the current event whose controller context is this controller's `query`
|
|
4734
5655
|
* handler, after `prepareRequest({ endpoint: "query", controls, filter })` — the
|
|
4735
5656
|
* request-scoped state a permission layer builds there (read grant, field
|
|
4736
|
-
* visibility) is the read's and stays in the child.
|
|
5657
|
+
* visibility) is the read's and stays in the child. `routeParams` (since
|
|
5658
|
+
* 0.1.149) replaces the route params the child's hooks read.
|
|
4737
5659
|
*/
|
|
4738
|
-
_asRead(controls, filter, fn) {
|
|
5660
|
+
_asRead(controls, filter, fn, routeParams) {
|
|
4739
5661
|
return runAsController(this, "query", async () => {
|
|
5662
|
+
if (routeParams) (0, _wooksjs_event_core.current)().set(_wooksjs_event_core.routeParamsKey, routeParams);
|
|
4740
5663
|
if (typeof this.prepareRequest === "function") await this.prepareRequest(readRequestContext("query", controls, filter));
|
|
4741
5664
|
return fn();
|
|
4742
5665
|
});
|
|
@@ -4760,9 +5683,11 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4760
5683
|
/**
|
|
4761
5684
|
* `select` without the `sealed` paths (see {@link _sealControls}); an
|
|
4762
5685
|
* exclusion of them is forced when there is no projection, or when every
|
|
4763
|
-
* requested path was sealed.
|
|
5686
|
+
* requested path was sealed. An inclusion naming a PARENT of a sealed path
|
|
5687
|
+
* (`secret` over a write-only `secret.hash`) is replaced by the parent's
|
|
5688
|
+
* unsealed leaves, so a sealed descendant never rides along with it.
|
|
4764
5689
|
*/
|
|
4765
|
-
_sealSelect(select, writeOnly) {
|
|
5690
|
+
_sealSelect(select, writeOnly, readable) {
|
|
4766
5691
|
if (writeOnly.size === 0) return select;
|
|
4767
5692
|
const exclusion = () => {
|
|
4768
5693
|
const out = {};
|
|
@@ -4770,29 +5695,47 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4770
5695
|
return out;
|
|
4771
5696
|
};
|
|
4772
5697
|
if (select === void 0) return exclusion();
|
|
5698
|
+
const expand = (path) => {
|
|
5699
|
+
if (writeOnly.has(path)) return [];
|
|
5700
|
+
const prefix = `${path}.`;
|
|
5701
|
+
let parent = false;
|
|
5702
|
+
for (const sealed of writeOnly) if (sealed.startsWith(prefix)) {
|
|
5703
|
+
parent = true;
|
|
5704
|
+
break;
|
|
5705
|
+
}
|
|
5706
|
+
if (!parent) return [path];
|
|
5707
|
+
return this._leavesOf(readable).filter((leaf) => leaf.startsWith(prefix) && (0, _atscript_db.selfOrAncestor)(leaf, writeOnly) === void 0);
|
|
5708
|
+
};
|
|
4773
5709
|
if (Array.isArray(select)) {
|
|
4774
|
-
const kept =
|
|
5710
|
+
const kept = [];
|
|
5711
|
+
for (const item of select) if (typeof item === "string") kept.push(...expand(item));
|
|
5712
|
+
else if (!writeOnly.has(item.$field ?? "")) kept.push(item);
|
|
4775
5713
|
return kept.length > 0 ? kept : exclusion();
|
|
4776
5714
|
}
|
|
4777
5715
|
const entries = Object.entries(select);
|
|
4778
5716
|
if (entries.length > 0 && (entries[0][1] === 1 || entries[0][1] === true)) {
|
|
4779
5717
|
const out = {};
|
|
4780
|
-
for (const [k, v] of entries)
|
|
5718
|
+
for (const [k, v] of entries) for (const path of expand(k)) out[path] = v;
|
|
4781
5719
|
return Object.keys(out).length > 0 ? out : exclusion();
|
|
4782
5720
|
}
|
|
4783
5721
|
const out = { ...select };
|
|
4784
5722
|
for (const f of writeOnly) out[f] = 0;
|
|
4785
5723
|
return out;
|
|
4786
5724
|
}
|
|
4787
|
-
/**
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
const
|
|
4794
|
-
|
|
5725
|
+
/** Own leaf field paths (no navigation, no ignored field) of `readable`, once per readable. */
|
|
5726
|
+
_leavesOf(readable) {
|
|
5727
|
+
let leaves = this._targetLeaves.get(readable);
|
|
5728
|
+
if (!leaves) {
|
|
5729
|
+
const paths = [...readable.flatMap?.keys() ?? []].filter((path) => path !== "");
|
|
5730
|
+
const parents = /* @__PURE__ */ new Set();
|
|
5731
|
+
for (const path of paths) for (let at = path.indexOf("."); at >= 0; at = path.indexOf(".", at + 1)) parents.add(path.slice(0, at));
|
|
5732
|
+
const nav = readable.navFields ?? /* @__PURE__ */ new Set();
|
|
5733
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
5734
|
+
for (const fd of readable.fieldDescriptors) if (fd.ignored) ignored.add(fd.path);
|
|
5735
|
+
leaves = paths.filter((path) => !parents.has(path) && (0, _atscript_db.selfOrAncestor)(path, nav) === void 0 && (0, _atscript_db.selfOrAncestor)(path, ignored) === void 0);
|
|
5736
|
+
this._targetLeaves.set(readable, leaves);
|
|
4795
5737
|
}
|
|
5738
|
+
return leaves;
|
|
4796
5739
|
}
|
|
4797
5740
|
/**
|
|
4798
5741
|
* Merges the `$search` fallback into the filter: a case-insensitive literal
|
|
@@ -4800,7 +5743,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4800
5743
|
* with the existing filter. Applies only when native search does not serve
|
|
4801
5744
|
* the request (no native search, or — since 0.1.143 — its default index
|
|
4802
5745
|
* reads a field {@link hasField} hides) and the request isn't a vector
|
|
4803
|
-
* search (`$vector` consumes the term).
|
|
5746
|
+
* search (`$vector` consumes the term). Lenient on list endpoints: a term
|
|
5747
|
+
* nothing can apply is ignored. Resolvers (query targets, `resolveQuery`)
|
|
5748
|
+
* refuse it — a subclass override that applies the term must return a new
|
|
5749
|
+
* filter object.
|
|
4804
5750
|
*/
|
|
4805
5751
|
applySearchFallback(filter, controls) {
|
|
4806
5752
|
const term = controls.$search;
|
|
@@ -4862,13 +5808,13 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4862
5808
|
* subclass implements it. Returns the hook's result — `undefined`, with no
|
|
4863
5809
|
* promise or microtask, when there is no hook or it is synchronous.
|
|
4864
5810
|
*/
|
|
4865
|
-
_finishRows(rows, prep, ctx) {
|
|
5811
|
+
_finishRows(rows, prep, ctx, read) {
|
|
4866
5812
|
const overlay = prep?.scopeOverlay;
|
|
4867
|
-
if (!prep || !overlay && prep.delegations.length === 0) return this._augmentAndDecorate(rows, prep, ctx);
|
|
5813
|
+
if (!prep || !overlay && prep.delegations.length === 0) return this._augmentAndDecorate(rows, prep, ctx, read);
|
|
4868
5814
|
return (async () => {
|
|
4869
5815
|
const names = prep.envelopes.map((e) => e.info.name);
|
|
4870
5816
|
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);
|
|
5817
|
+
await this._augmentAndDecorate(rows, prep, ctx, read, masks, delegated);
|
|
4872
5818
|
})();
|
|
4873
5819
|
}
|
|
4874
5820
|
/**
|
|
@@ -4876,7 +5822,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4876
5822
|
* ones (`delegated`: per delegation, per row) — then {@link decorateRows}
|
|
4877
5823
|
* when implemented.
|
|
4878
5824
|
*/
|
|
4879
|
-
_augmentAndDecorate(rows, prep, ctx, outOfScope, delegated) {
|
|
5825
|
+
_augmentAndDecorate(rows, prep, ctx, read, outOfScope, delegated) {
|
|
4880
5826
|
if (prep) {
|
|
4881
5827
|
augmentRowsWithActions({
|
|
4882
5828
|
envelopes: prep.envelopes,
|
|
@@ -4887,7 +5833,11 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4887
5833
|
});
|
|
4888
5834
|
if (prep.delegations.length > 0) mergeDelegatedActions(rows, delegated ?? []);
|
|
4889
5835
|
}
|
|
4890
|
-
|
|
5836
|
+
const decorated = this._decorates ? this.decorateRows(rows, ctx) : void 0;
|
|
5837
|
+
if (!read || stripsNothing(read)) return decorated;
|
|
5838
|
+
const strip = () => stripDecorations(rows, read);
|
|
5839
|
+
if (decorated === void 0) return strip();
|
|
5840
|
+
return Promise.resolve(decorated).then(strip);
|
|
4891
5841
|
}
|
|
4892
5842
|
/**
|
|
4893
5843
|
* The app of the current event, through DI — never the one this
|
|
@@ -4963,9 +5913,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4963
5913
|
*/
|
|
4964
5914
|
resolveMeta() {
|
|
4965
5915
|
const own = super.resolveMeta();
|
|
4966
|
-
if (!this._hasDelegations && !this._hasFieldOverridden) return own;
|
|
5916
|
+
if (!this._hasDelegations && !this._hasFieldOverridden && !this._planner) return own;
|
|
4967
5917
|
return (async () => {
|
|
4968
|
-
const [
|
|
5918
|
+
const [overlaid, delegated] = await Promise.all([own, this._hasDelegations ? this._delegatedInfos() : []]);
|
|
5919
|
+
const meta = this._planner ? this._planner.meta(overlaid) : overlaid;
|
|
4969
5920
|
const visible = this._applyIndexVisibility(meta);
|
|
4970
5921
|
return delegated.length > 0 ? {
|
|
4971
5922
|
...visible,
|
|
@@ -5019,7 +5970,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5019
5970
|
/** @internal Source side of a delegation: `GET /meta/actions` for one id, `names` only. */
|
|
5020
5971
|
async [AVAILABLE_ACTIONS](id, names) {
|
|
5021
5972
|
await this.parseRequest("availableActions");
|
|
5022
|
-
return this.
|
|
5973
|
+
return (await this._availableResolved(id, names)).own;
|
|
5023
5974
|
}
|
|
5024
5975
|
/** @internal Source side of a delegation: the `names` the caller may run (`allowedActions`). */
|
|
5025
5976
|
async [ALLOWED_ACTIONS](names) {
|
|
@@ -5038,8 +5989,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5038
5989
|
* `$actions=true`, then run {@link decorateRows}. Caller dispatches the
|
|
5039
5990
|
* strategy to its read-method family (count vs no-count).
|
|
5040
5991
|
*/
|
|
5041
|
-
async _runReadWithActions(endpoint, queryObj, controls,
|
|
5042
|
-
const [prep, strategy] = await Promise.all([this._prepareAugmentation(controls,
|
|
5992
|
+
async _runReadWithActions(endpoint, queryObj, controls, projected, exec) {
|
|
5993
|
+
const [prep, strategy] = await Promise.all([this._prepareAugmentation(controls, projected), this._resolveReadStrategy(controls)]);
|
|
5043
5994
|
const result = await exec(prep?.widenedSelect ? {
|
|
5044
5995
|
...queryObj,
|
|
5045
5996
|
controls: {
|
|
@@ -5049,19 +6000,120 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5049
6000
|
} : queryObj, strategy);
|
|
5050
6001
|
const pending = this._finishRows(result.data, prep, {
|
|
5051
6002
|
endpoint,
|
|
5052
|
-
projection: select,
|
|
5053
|
-
controls
|
|
5054
|
-
|
|
6003
|
+
projection: projected.select,
|
|
6004
|
+
controls,
|
|
6005
|
+
decorations: projected.read?.served ?? NO_DECORATIONS
|
|
6006
|
+
}, projected.read);
|
|
5055
6007
|
if (pending) await pending;
|
|
5056
6008
|
return result;
|
|
5057
6009
|
}
|
|
5058
6010
|
/**
|
|
6011
|
+
* Maps the ids an id-addressed endpoint received to the rows' current ids
|
|
6012
|
+
* (since 0.1.148) — the seam for stale or alias ids, e.g. a natural key that
|
|
6013
|
+
* was renamed. Called once per request, after {@link prepareRequest} and
|
|
6014
|
+
* after the request's own validation, before anything reads the row, by:
|
|
6015
|
+
*
|
|
6016
|
+
* | `ctx.purpose` | endpoint | `ids` |
|
|
6017
|
+
* | --- | --- | --- |
|
|
6018
|
+
* | `"one"` | `GET /one/:id`, `GET /one?…` | one id: the path string, or the `?`-form identification object |
|
|
6019
|
+
* | `"available"` | `GET /meta/actions/:id`, `?…` (and a view asking its source) | one id, as above |
|
|
6020
|
+
* | `"remove"` | `DELETE /:id`, `DELETE /?…` (`AsDbController`) | one id, as above |
|
|
6021
|
+
* | `"action"` | an action route | the body's validated ids (one for a `'row'` action) |
|
|
6022
|
+
*
|
|
6023
|
+
* Contract:
|
|
6024
|
+
*
|
|
6025
|
+
* - Return one id per input id, index-aligned (anything else is a 500). An
|
|
6026
|
+
* id that already names a row must come back UNCHANGED — the current
|
|
6027
|
+
* holder of a key wins over an alias; so must an id you cannot resolve
|
|
6028
|
+
* (never throw for an unknown alias: a custom error is an oracle — the
|
|
6029
|
+
* endpoint then answers its normal miss).
|
|
6030
|
+
* - The output is validated (a server bug is a 500, never a client 400): a
|
|
6031
|
+
* scalar is resolved like a path scalar (primary key first, then the
|
|
6032
|
+
* visible unique keys, inside the row overlay); an object must be one of
|
|
6033
|
+
* the visible identifications ({@link idSource}); an `"action"` id must
|
|
6034
|
+
* be such an object.
|
|
6035
|
+
* - The resolved id is never trusted for access: the endpoint still reads
|
|
6036
|
+
* or deletes it under {@link rowOverlay} and the visible identifications.
|
|
6037
|
+
* `ctx.overlay` is that overlay — resolve INSIDE it when an alias could
|
|
6038
|
+
* name several rows, so a row the caller can't reach never shadows one
|
|
6039
|
+
* they can. Don't log or return the canonical id in errors.
|
|
6040
|
+
* - Handlers (`@DbActionID()`, `useDbActionId()`), {@link rowOverlay} reads,
|
|
6041
|
+
* {@link actionRowScope} and `onRemove` / `guardRemove` receive the
|
|
6042
|
+
* resolved ids; error bodies and `summary()` echo the ids the client sent.
|
|
6043
|
+
* - Write bodies (`POST` / `PUT` / `PATCH`) are not resolved — use
|
|
6044
|
+
* `onWrite`. Not called by value-help controllers, `$actions` on a read
|
|
6045
|
+
* or a query target. Not overriding it costs nothing.
|
|
6046
|
+
*
|
|
6047
|
+
* It is NOT overridden by {@link resolveRowFilter}, which does not take part
|
|
6048
|
+
* in `/one` for real tables and views.
|
|
6049
|
+
*
|
|
6050
|
+
* ```ts
|
|
6051
|
+
* protected async resolveRowIds(ids: readonly TDbRowIdInput[], ctx: TDbRowIdsContext) {
|
|
6052
|
+
* return Promise.all(ids.map(async (id) => {
|
|
6053
|
+
* const key = typeof id === "object" ? id.code : id
|
|
6054
|
+
* if (typeof key !== "string") return id
|
|
6055
|
+
* // the current holder of the key wins; consult the alias table on a miss
|
|
6056
|
+
* // resolve INSIDE the overlay: a row the caller cannot reach never wins
|
|
6057
|
+
* const inScope = (code: string) =>
|
|
6058
|
+
* this.readable.count({ filter: ctx.overlay ? { $and: [{ code }, ctx.overlay] } : { code } })
|
|
6059
|
+
* if (await inScope(key)) return id
|
|
6060
|
+
* const alias = await aliases.findOne({ filter: { oldCode: key } })
|
|
6061
|
+
* if (!alias || !(await inScope(alias.newCode))) return id
|
|
6062
|
+
* return typeof id === "object" ? { code: alias.newCode } : alias.newCode
|
|
6063
|
+
* }))
|
|
6064
|
+
* }
|
|
6065
|
+
* ```
|
|
6066
|
+
*
|
|
6067
|
+
* @since 0.1.148
|
|
6068
|
+
*/
|
|
6069
|
+
resolveRowIds(ids, _ctx) {
|
|
6070
|
+
return ids;
|
|
6071
|
+
}
|
|
6072
|
+
/** {@link resolveRowIds} with its output validated (a server bug is a 500). */
|
|
6073
|
+
async _runResolveRowIds(ids, ctx) {
|
|
6074
|
+
const out = await this.resolveRowIds(ids, ctx);
|
|
6075
|
+
if (!Array.isArray(out) || out.length !== ids.length) throw new _moostjs_event_http.HttpError(500, "resolveRowIds must return one id per request id");
|
|
6076
|
+
const source = this.idSource;
|
|
6077
|
+
for (const id of out) {
|
|
6078
|
+
if ((typeof id === "string" || typeof id === "number" || typeof id === "boolean") && ctx.purpose !== "action") continue;
|
|
6079
|
+
try {
|
|
6080
|
+
validateSingleId(id, source, { strictTypes: ctx.purpose === "action" });
|
|
6081
|
+
} catch (error) {
|
|
6082
|
+
if (error instanceof _atscript_typescript_utils.ValidatorError) throw new _moostjs_event_http.HttpError(500, "resolveRowIds returned an invalid id");
|
|
6083
|
+
throw error;
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
return out;
|
|
6087
|
+
}
|
|
6088
|
+
/**
|
|
6089
|
+
* @internal The one id of an id-addressed endpoint through
|
|
6090
|
+
* {@link resolveRowIds} (identity, at no cost, when it is not overridden)
|
|
6091
|
+
* and the row overlay it was resolved inside — computed once, for the
|
|
6092
|
+
* endpoint's read or delete under that overlay.
|
|
6093
|
+
*/
|
|
6094
|
+
async _resolveWithOverlay(id, purpose) {
|
|
6095
|
+
const overlay = await this.rowOverlay();
|
|
6096
|
+
if (!this[ROW_RESOLVES]) return {
|
|
6097
|
+
id,
|
|
6098
|
+
overlay
|
|
6099
|
+
};
|
|
6100
|
+
const [resolved] = await this._runResolveRowIds([id], {
|
|
6101
|
+
purpose,
|
|
6102
|
+
overlay
|
|
6103
|
+
});
|
|
6104
|
+
return {
|
|
6105
|
+
id: resolved,
|
|
6106
|
+
overlay
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
6109
|
+
/**
|
|
5059
6110
|
* The filter addressing exactly the ONE row `id` means — the readable's
|
|
5060
6111
|
* PK-first `resolveRowFilter` (since 0.1.143) under this request's
|
|
5061
6112
|
* identifications (`_idOpts`). `scope` (the row overlay) restricts which
|
|
5062
6113
|
* rows count while the id is pinned, so a row outside it never shadows one
|
|
5063
6114
|
* inside it. Readables without it (partial mocks) fall back to
|
|
5064
|
-
* `resolveIdFilter`.
|
|
6115
|
+
* `resolveIdFilter`. Not an alias seam: `/one` reads through `findOneByRow`
|
|
6116
|
+
* on every real table or view — map stale ids in {@link resolveRowIds}.
|
|
5065
6117
|
*/
|
|
5066
6118
|
resolveRowFilter(id, scope) {
|
|
5067
6119
|
const readable = this.readable;
|
|
@@ -5135,8 +6187,15 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5135
6187
|
if (groupBy?.length && controls.$vector !== void 0) return new _moostjs_event_http.HttpError(400, "Cannot combine $vector and $groupBy in the same query");
|
|
5136
6188
|
const error = this.validateParsed(parsed, "query");
|
|
5137
6189
|
if (error) return error;
|
|
5138
|
-
if (groupBy?.length) {
|
|
5139
|
-
const
|
|
6190
|
+
if (groupBy?.length && this._writeOnlySet.size > 0) {
|
|
6191
|
+
const refs = (0, _atscript_db.collectQueryPaths)(parsed, true);
|
|
6192
|
+
const sealed = [
|
|
6193
|
+
...refs.groupBy,
|
|
6194
|
+
...refs.aggregate,
|
|
6195
|
+
...refs.bucket,
|
|
6196
|
+
...refs.select,
|
|
6197
|
+
...refs.sort
|
|
6198
|
+
].find((path) => this._writeOnlySet.has(path) && this.fieldVisibility.isVisible(path));
|
|
5140
6199
|
if (sealed) return new _moostjs_event_http.HttpError(400, `Field "${sealed}" is @db.writeOnly and cannot be aggregated`);
|
|
5141
6200
|
}
|
|
5142
6201
|
const gateError = this.checkCapabilities(parsed);
|
|
@@ -5150,9 +6209,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5150
6209
|
insights: parsed.insights
|
|
5151
6210
|
});
|
|
5152
6211
|
}
|
|
5153
|
-
const [transformedFilter,
|
|
6212
|
+
const [transformedFilter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
|
|
5154
6213
|
const filter = this.applySearchFallback(transformedFilter, controls);
|
|
5155
|
-
const sealed = this._sealControls(controls, transformedSelect);
|
|
5156
6214
|
if (controls.$count) return this.readable.count({
|
|
5157
6215
|
filter,
|
|
5158
6216
|
controls: {
|
|
@@ -5160,19 +6218,19 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5160
6218
|
$select: sealed.$select
|
|
5161
6219
|
}
|
|
5162
6220
|
});
|
|
5163
|
-
const
|
|
5164
|
-
if (
|
|
6221
|
+
const projected = finish();
|
|
6222
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
5165
6223
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
5166
6224
|
const queryObj = {
|
|
5167
6225
|
filter,
|
|
5168
6226
|
controls: {
|
|
5169
6227
|
...sealed,
|
|
5170
|
-
$select: select,
|
|
6228
|
+
$select: projected.select,
|
|
5171
6229
|
$limit: controls.$limit || 1e3,
|
|
5172
6230
|
$threshold: threshold
|
|
5173
6231
|
}
|
|
5174
6232
|
};
|
|
5175
|
-
return (await this._runReadWithActions("query", queryObj, controls,
|
|
6233
|
+
return (await this._runReadWithActions("query", queryObj, controls, projected, async (q, strategy) => {
|
|
5176
6234
|
switch (strategy.kind) {
|
|
5177
6235
|
case "vector": return { data: await (strategy.vectorField ? this.readable.vectorSearch(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearch(strategy.vector, q)) };
|
|
5178
6236
|
case "search": return { data: await this.readable.search(strategy.term, q, strategy.index) };
|
|
@@ -5193,23 +6251,22 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5193
6251
|
const page = Math.max(Number(controls.$page || 1), 1);
|
|
5194
6252
|
const size = Math.max(Number(controls.$size || 10), 1);
|
|
5195
6253
|
const skip = (page - 1) * size;
|
|
5196
|
-
const [transformedFilter,
|
|
6254
|
+
const [transformedFilter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
|
|
5197
6255
|
const filter = this.applySearchFallback(transformedFilter, controls);
|
|
5198
|
-
const
|
|
5199
|
-
|
|
5200
|
-
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
6256
|
+
const projected = finish();
|
|
6257
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
5201
6258
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
5202
6259
|
const query = {
|
|
5203
6260
|
filter,
|
|
5204
6261
|
controls: {
|
|
5205
6262
|
...sealed,
|
|
5206
|
-
$select: select,
|
|
6263
|
+
$select: projected.select,
|
|
5207
6264
|
$skip: skip,
|
|
5208
6265
|
$limit: size,
|
|
5209
6266
|
$threshold: threshold
|
|
5210
6267
|
}
|
|
5211
6268
|
};
|
|
5212
|
-
const result = await this._runReadWithActions("pages", query, controls,
|
|
6269
|
+
const result = await this._runReadWithActions("pages", query, controls, projected, async (q, strategy) => {
|
|
5213
6270
|
switch (strategy.kind) {
|
|
5214
6271
|
case "vector": return strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q);
|
|
5215
6272
|
case "search": return this.readable.searchWithCount(strategy.term, q, strategy.index);
|
|
@@ -5251,10 +6308,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5251
6308
|
const gateError = this.checkCapabilities(parsed);
|
|
5252
6309
|
if (gateError) return gateError;
|
|
5253
6310
|
const clientFilter = await this._relationOverlay(parsed);
|
|
5254
|
-
const [filter,
|
|
5255
|
-
const
|
|
5256
|
-
|
|
5257
|
-
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
6311
|
+
const [filter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
|
|
6312
|
+
const projected = finish();
|
|
6313
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
5258
6314
|
const paginated = controls.$page !== void 0 || controls.$size !== void 0;
|
|
5259
6315
|
const page = Math.max(Number(controls.$page || 1), 1);
|
|
5260
6316
|
const size = Math.max(Number(controls.$size || 10), 1);
|
|
@@ -5264,7 +6320,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5264
6320
|
...sealed,
|
|
5265
6321
|
$center: void 0,
|
|
5266
6322
|
$index: void 0,
|
|
5267
|
-
$select: select,
|
|
6323
|
+
$select: projected.select,
|
|
5268
6324
|
...paginated ? {
|
|
5269
6325
|
$skip: (page - 1) * size,
|
|
5270
6326
|
$limit: size
|
|
@@ -5272,7 +6328,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5272
6328
|
}
|
|
5273
6329
|
};
|
|
5274
6330
|
if (paginated) {
|
|
5275
|
-
const result = await this._runReadWithActions("geo", queryObj, controls,
|
|
6331
|
+
const result = await this._runReadWithActions("geo", queryObj, controls, projected, async (q) => indexName ? this.readable.geoSearchWithCount(indexName, point, q) : this.readable.geoSearchWithCount(point, q));
|
|
5276
6332
|
return {
|
|
5277
6333
|
data: result.data,
|
|
5278
6334
|
page,
|
|
@@ -5281,7 +6337,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5281
6337
|
count: result.count
|
|
5282
6338
|
};
|
|
5283
6339
|
}
|
|
5284
|
-
return (await this._runReadWithActions("geo", queryObj, controls,
|
|
6340
|
+
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
6341
|
}
|
|
5286
6342
|
/** Parses the `$center` control: `"lng,lat"` string (or tuple) → `[number, number]`. */
|
|
5287
6343
|
_parseGeoCenter(raw) {
|
|
@@ -5330,21 +6386,22 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5330
6386
|
const error = this.validateParsed(parsed, "getOne") ?? this.checkCapabilities(parsed);
|
|
5331
6387
|
if (error) return error;
|
|
5332
6388
|
await this._relationOverlay(parsed);
|
|
5333
|
-
const sealed =
|
|
5334
|
-
const
|
|
5335
|
-
if (
|
|
5336
|
-
const [prep, overlay] = await Promise.all([this._prepareAugmentation(controls,
|
|
6389
|
+
const { sealed, finish } = await this._projectRead(controls);
|
|
6390
|
+
const projected = finish();
|
|
6391
|
+
if (projected instanceof _moostjs_event_http.HttpError) return projected;
|
|
6392
|
+
const [prep, { id: resolvedId, overlay }] = await Promise.all([this._prepareAugmentation(controls, projected), this._resolveWithOverlay(id, "one")]);
|
|
5337
6393
|
const readControls = {
|
|
5338
6394
|
...sealed,
|
|
5339
|
-
$select: prep?.widenedSelect ?? select
|
|
6395
|
+
$select: prep?.widenedSelect ?? projected.select
|
|
5340
6396
|
};
|
|
5341
|
-
const item = await this.returnOne(this._findRow(
|
|
6397
|
+
const item = await this.returnOne(this._findRow(resolvedId, overlay, readControls));
|
|
5342
6398
|
if (item instanceof _moostjs_event_http.HttpError) return item;
|
|
5343
6399
|
const pending = this._finishRows([item], prep, {
|
|
5344
6400
|
endpoint: "one",
|
|
5345
|
-
projection: select,
|
|
5346
|
-
controls
|
|
5347
|
-
|
|
6401
|
+
projection: projected.select,
|
|
6402
|
+
controls,
|
|
6403
|
+
decorations: projected.read?.served ?? NO_DECORATIONS
|
|
6404
|
+
}, projected.read);
|
|
5348
6405
|
if (pending) await pending;
|
|
5349
6406
|
return item;
|
|
5350
6407
|
}
|
|
@@ -5361,10 +6418,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5361
6418
|
*/
|
|
5362
6419
|
async availableActionsById(id) {
|
|
5363
6420
|
await this.parseRequest("availableActions");
|
|
5364
|
-
const own = await this.
|
|
6421
|
+
const { own, id: resolved } = await this._availableResolved(id);
|
|
5365
6422
|
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);
|
|
6423
|
+
return this._delegatedAvailable(own, (d) => this._sourceIdOf(d, resolved));
|
|
5368
6424
|
}
|
|
5369
6425
|
/**
|
|
5370
6426
|
* **GET /meta/actions?field1=val1&…** — {@link availableActionsById} by
|
|
@@ -5374,13 +6430,50 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5374
6430
|
async availableActions(query) {
|
|
5375
6431
|
await this.parseRequest("availableActions");
|
|
5376
6432
|
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
6433
|
if (idObj instanceof _moostjs_event_http.HttpError) {
|
|
5379
|
-
if (!(await this._activeDelegations()).some((d) =>
|
|
5380
|
-
return this._delegatedAvailable({ actions: [] },
|
|
6434
|
+
if (!(await this._activeDelegations()).some((d) => this._sourceIdOf(d, void 0, query) !== void 0)) return idObj;
|
|
6435
|
+
return this._delegatedAvailable({ actions: [] }, (d) => this._sourceIdOf(d, void 0, query));
|
|
5381
6436
|
}
|
|
5382
|
-
const own = await this.
|
|
5383
|
-
|
|
6437
|
+
const { own, id: resolved } = await this._availableResolved(idObj);
|
|
6438
|
+
if (!this._hasDelegations) return own;
|
|
6439
|
+
return this._delegatedAvailable(own, (d) => this._sourceIdOf(d, resolved, query, this._identificationFields()));
|
|
6440
|
+
}
|
|
6441
|
+
/** Every field of every identification (primary key and unique indexes) the view addresses a row by. */
|
|
6442
|
+
_identificationFields() {
|
|
6443
|
+
return [...new Set(this.idSource.identifications.flatMap((i) => i.fields))];
|
|
6444
|
+
}
|
|
6445
|
+
/**
|
|
6446
|
+
* A delegation's source id: each source id field from the row's mapped path
|
|
6447
|
+
* of the RESOLVED `id`. A path of ANY of the view's identifications
|
|
6448
|
+
* (`consumed` — not just the one the request matched: `?id=1&code=T-OLD` names
|
|
6449
|
+
* `code` too) is NEVER taken from the raw `?` query: that value may be an
|
|
6450
|
+
* alias `resolveRowIds` rewrote, and the source would see (and answer for)
|
|
6451
|
+
* it. Paths outside the identification fall back to `fallback`'s (the raw
|
|
6452
|
+
* query's) value; `undefined` when a path has none.
|
|
6453
|
+
*/
|
|
6454
|
+
_sourceIdOf(d, id, fallback, consumed = []) {
|
|
6455
|
+
const value = (path) => id?.[path] ?? (consumed.includes(path) ? void 0 : fallback?.[path]);
|
|
6456
|
+
return d.paths.every((path) => value(path) !== void 0) ? Object.fromEntries(Object.entries(d.idMap).map(([field, path]) => [field, value(path)])) : void 0;
|
|
6457
|
+
}
|
|
6458
|
+
/**
|
|
6459
|
+
* {@link _availableActions} for a request id (`names`: only those actions):
|
|
6460
|
+
* through {@link resolveRowIds} (`"available"`) first, the row overlay
|
|
6461
|
+
* computed once for both — and the resolved id returned as an object (a
|
|
6462
|
+
* scalar is the single-field `preferredId` value) for the delegated part to
|
|
6463
|
+
* derive its source id from.
|
|
6464
|
+
*/
|
|
6465
|
+
async _availableResolved(id, names) {
|
|
6466
|
+
const { id: resolved, overlay } = await this._resolveWithOverlay(id, "available");
|
|
6467
|
+
const own = await this._availableActions(resolved, overlay, names);
|
|
6468
|
+
if (typeof resolved === "object") return {
|
|
6469
|
+
own,
|
|
6470
|
+
id: resolved
|
|
6471
|
+
};
|
|
6472
|
+
const preferred = this.readable.preferredId;
|
|
6473
|
+
return {
|
|
6474
|
+
own,
|
|
6475
|
+
id: preferred.length === 1 ? { [preferred[0]]: resolved } : void 0
|
|
6476
|
+
};
|
|
5384
6477
|
}
|
|
5385
6478
|
/**
|
|
5386
6479
|
* **POST /delegated-actions/:name** — a query target for a `@DbActionsFrom`
|
|
@@ -5443,7 +6536,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5443
6536
|
maxExclude: limits.maxIds,
|
|
5444
6537
|
overlay: "read",
|
|
5445
6538
|
select: [...new Set([...identity, ...delegation.paths])],
|
|
5446
|
-
excludeShapes: [delegation.paths]
|
|
6539
|
+
excludeShapes: [delegation.paths],
|
|
6540
|
+
visibleOf: identity
|
|
5447
6541
|
});
|
|
5448
6542
|
if (resolved.dryRun) return { matched: resolved.matched };
|
|
5449
6543
|
const summary = {
|
|
@@ -5453,7 +6547,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5453
6547
|
failed: []
|
|
5454
6548
|
};
|
|
5455
6549
|
const { ids, index } = mapToSourceIds(resolved.rows, delegation.idMap);
|
|
5456
|
-
const visibleIdentity =
|
|
6550
|
+
const visibleIdentity = resolved.visibleOf ?? [];
|
|
5457
6551
|
for (let i = 0; i < index.length; i++) {
|
|
5458
6552
|
if (index[i] >= 0) continue;
|
|
5459
6553
|
const row = resolved.rows[i];
|
|
@@ -5552,8 +6646,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5552
6646
|
* actions are then checked on it (`purpose: "available"`) exactly like
|
|
5553
6647
|
* `$actions` rows.
|
|
5554
6648
|
*/
|
|
5555
|
-
async _availableActions(id, names) {
|
|
5556
|
-
const
|
|
6649
|
+
async _availableActions(id, overlay, names) {
|
|
6650
|
+
const envelopes = await (names ? this._envelopesNamed(names) : this._resolveAugmentEnvelopes());
|
|
5557
6651
|
if (!envelopes?.length) return { actions: [] };
|
|
5558
6652
|
const idKeys = id !== null && typeof id === "object" ? Object.keys(id) : [];
|
|
5559
6653
|
const fieldsOf = envelopes.map((e) => {
|
|
@@ -5634,7 +6728,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5634
6728
|
if (fd.encrypted) entry.encrypted = true;
|
|
5635
6729
|
if (geoIndexedPhysical.has(fd.physicalName)) entry.geo = true;
|
|
5636
6730
|
if (this._writeOnlySet.has(path)) entry.writeOnly = true;
|
|
6731
|
+
if (cap.groupable) entry.groupable = true;
|
|
5637
6732
|
if (cap.bucketable) entry.bucketable = true;
|
|
6733
|
+
if (cap.numeric) entry.numeric = true;
|
|
5638
6734
|
if (fd.derived) entry.derived = true;
|
|
5639
6735
|
if (fd.computed) entry.computed = true;
|
|
5640
6736
|
fields[path] = entry;
|
|
@@ -5649,11 +6745,13 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5649
6745
|
relations,
|
|
5650
6746
|
fields,
|
|
5651
6747
|
type: this.getSerializedType(),
|
|
6748
|
+
...this._decorations && { decorations: this._planner.serialized(() => this.serializeForMeta(this._decorations.type)) },
|
|
5652
6749
|
actions: this.buildActions(),
|
|
5653
6750
|
crud: this.buildCrud(),
|
|
5654
6751
|
versionColumn: this.readable.versionColumn,
|
|
5655
6752
|
...capabilities.bucketUnits.length > 0 && { bucketUnits: [...capabilities.bucketUnits] },
|
|
5656
|
-
aggregateFns: [...capabilities.aggregateFns]
|
|
6753
|
+
aggregateFns: [...capabilities.aggregateFns],
|
|
6754
|
+
aggregateExpressions: capabilities.aggregateExpressions
|
|
5657
6755
|
};
|
|
5658
6756
|
}
|
|
5659
6757
|
buildCrud() {
|
|
@@ -5784,7 +6882,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5784
6882
|
buildCrud() {
|
|
5785
6883
|
return {
|
|
5786
6884
|
...super.buildCrud(),
|
|
5787
|
-
insert: [],
|
|
6885
|
+
insert: this.readable.dbAdapter?.supportsInsertIgnore() ? ["onConflict"] : [],
|
|
5788
6886
|
update: [],
|
|
5789
6887
|
replace: [],
|
|
5790
6888
|
remove: []
|
|
@@ -5804,7 +6902,9 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5804
6902
|
return data;
|
|
5805
6903
|
}
|
|
5806
6904
|
/**
|
|
5807
|
-
* Intercepts delete operations.
|
|
6905
|
+
* Intercepts delete operations. Receives the id {@link resolveRowIds}
|
|
6906
|
+
* resolved (the one the request carried when it is not overridden; since
|
|
6907
|
+
* 0.1.148). Return `undefined` to abort (500 "Not
|
|
5808
6908
|
* deleted"); return an `Error` instance to respond with that error.
|
|
5809
6909
|
* Runs outside any transaction. May be async (e.g. to resolve composite
|
|
5810
6910
|
* ids from external state).
|
|
@@ -5941,8 +7041,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5941
7041
|
* table's transaction and an out-of-scope row is not deleted — a 404,
|
|
5942
7042
|
* exactly like a missing one.
|
|
5943
7043
|
*/
|
|
5944
|
-
async _deleteOrThrow(id) {
|
|
5945
|
-
const scope = await this.rowOverlay();
|
|
7044
|
+
async _deleteOrThrow(id, scope) {
|
|
5946
7045
|
const args = scope ? [{
|
|
5947
7046
|
...this._removeArgs[0],
|
|
5948
7047
|
scope
|
|
@@ -5953,16 +7052,34 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
5953
7052
|
}
|
|
5954
7053
|
/**
|
|
5955
7054
|
* **POST /** — inserts one or many records.
|
|
7055
|
+
*
|
|
7056
|
+
* `?$onConflict=ignore` (since 0.1.148) skips rows colliding on the primary
|
|
7057
|
+
* key or a unique index instead of answering 409. The response then is
|
|
7058
|
+
* `{ insertedId?, conflict }` for an object body and
|
|
7059
|
+
* `{ insertedCount, insertedIds, inserted, conflicts }` for an array body.
|
|
7060
|
+
* Any other `$` control on POST answers 400.
|
|
5956
7061
|
*/
|
|
5957
|
-
async insert(payload) {
|
|
5958
|
-
await this.parseRequest("insert");
|
|
7062
|
+
async insert(payload, url) {
|
|
7063
|
+
const { controls } = await this.parseRequest("insert", url ?? "");
|
|
7064
|
+
const onConflict = this._readOnConflict(controls);
|
|
5959
7065
|
assertWriteShape(payload);
|
|
7066
|
+
const args = onConflict ? [{
|
|
7067
|
+
...this._writeArgs[0],
|
|
7068
|
+
onConflict
|
|
7069
|
+
}] : this._writeArgs;
|
|
5960
7070
|
if (Array.isArray(payload)) {
|
|
5961
7071
|
const rows = await this._writeBody("insertMany", payload, true);
|
|
5962
|
-
return this.table.insertMany(rows, ...
|
|
7072
|
+
return this.table.insertMany(rows, ...args);
|
|
5963
7073
|
}
|
|
5964
7074
|
const row = await this._writeBody("insert", payload, false);
|
|
5965
|
-
return this.table.insertOne(row, ...
|
|
7075
|
+
return this.table.insertOne(row, ...args);
|
|
7076
|
+
}
|
|
7077
|
+
/** The only POST control: `$onConflict` (`error` | `ignore`). Anything else `$…` → 400. */
|
|
7078
|
+
_readOnConflict(controls) {
|
|
7079
|
+
for (const key of Object.keys(controls)) if (key !== "$onConflict") throw badRequest("", `Unsupported control "${key}" on insert`);
|
|
7080
|
+
const mode = controls.$onConflict;
|
|
7081
|
+
if (mode !== void 0 && mode !== "error" && mode !== "ignore") throw badRequest("", `$onConflict must be "error" or "ignore", got ${JSON.stringify(mode)}`);
|
|
7082
|
+
return mode === "ignore" ? "ignore" : void 0;
|
|
5966
7083
|
}
|
|
5967
7084
|
/**
|
|
5968
7085
|
* **PUT /** — fully replaces one or many records matched by primary key.
|
|
@@ -6028,7 +7145,7 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
6028
7145
|
if (typeof table.recordFilter === "function") try {
|
|
6029
7146
|
filter = table.recordFilter(data, this._idOpts);
|
|
6030
7147
|
} catch (error) {
|
|
6031
|
-
if (!(error instanceof _atscript_db.DbError)) throw error;
|
|
7148
|
+
if (!(error instanceof _atscript_db.DbError) || error.code === "SPACE_CLOSED") throw error;
|
|
6032
7149
|
filter = null;
|
|
6033
7150
|
}
|
|
6034
7151
|
else filter = await this.resolveRowFilter(data);
|
|
@@ -6049,8 +7166,9 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
6049
7166
|
*/
|
|
6050
7167
|
async remove(id) {
|
|
6051
7168
|
await this.parseRequest("remove");
|
|
6052
|
-
const
|
|
6053
|
-
|
|
7169
|
+
const { id: resolved, overlay } = await this._resolveWithOverlay(id, "remove");
|
|
7170
|
+
const resolvedId = await this._checkHook(this.onRemove(resolved), "Not deleted");
|
|
7171
|
+
return this._deleteOrThrow(resolvedId, overlay);
|
|
6054
7172
|
}
|
|
6055
7173
|
/**
|
|
6056
7174
|
* **DELETE /?field1=val1&field2=val2** — removes a record by composite key
|
|
@@ -6060,15 +7178,17 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
6060
7178
|
await this.parseRequest("remove");
|
|
6061
7179
|
const idObj = this.extractIdShape(query);
|
|
6062
7180
|
if (idObj instanceof _moostjs_event_http.HttpError) throw idObj;
|
|
6063
|
-
const
|
|
6064
|
-
|
|
7181
|
+
const { id: resolved, overlay } = await this._resolveWithOverlay(idObj, "remove");
|
|
7182
|
+
const resolvedId = await this._checkHook(this.onRemove(resolved), "Not deleted");
|
|
7183
|
+
return this._deleteOrThrow(resolvedId, overlay);
|
|
6065
7184
|
}
|
|
6066
7185
|
};
|
|
6067
7186
|
__decorate([
|
|
6068
7187
|
(0, _moostjs_event_http.Post)(""),
|
|
6069
7188
|
__decorateParam(0, (0, _moostjs_event_http.Body)()),
|
|
7189
|
+
__decorateParam(1, (0, _moostjs_event_http.Url)()),
|
|
6070
7190
|
__decorateMetadata("design:type", Function),
|
|
6071
|
-
__decorateMetadata("design:paramtypes", [Object]),
|
|
7191
|
+
__decorateMetadata("design:paramtypes", [Object, String]),
|
|
6072
7192
|
__decorateMetadata("design:returntype", Promise)
|
|
6073
7193
|
], AsDbController.prototype, "insert", null);
|
|
6074
7194
|
__decorate([
|
|
@@ -6565,7 +7685,7 @@ function buildGateInterceptor(opts) {
|
|
|
6565
7685
|
await ctx.get(dbActionOverlaySlot);
|
|
6566
7686
|
if (level === "row") {
|
|
6567
7687
|
const verdict = judgeRow(action, disabled, await ctx.get(dbActionRowSlot));
|
|
6568
|
-
if (verdict) throw
|
|
7688
|
+
if (verdict) throw rowDisabledError(ctx, action, await ctx.get(dbActionIdSlot), verdictReason(verdict));
|
|
6569
7689
|
return;
|
|
6570
7690
|
}
|
|
6571
7691
|
if (await queryTargetReply(ctx, reply)) return;
|
|
@@ -6589,8 +7709,7 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
6589
7709
|
const existingRows = [];
|
|
6590
7710
|
for (const row of rows) if (row !== void 0) existingRows.push(row);
|
|
6591
7711
|
const verdicts = disabled ? judgeRows(action, disabled, existingRows) : void 0;
|
|
6592
|
-
const
|
|
6593
|
-
const failingReasons = [];
|
|
7712
|
+
const failing = [];
|
|
6594
7713
|
const passingRows = [];
|
|
6595
7714
|
const passingIds = [];
|
|
6596
7715
|
const skipped = [];
|
|
@@ -6601,8 +7720,10 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
6601
7720
|
const verdict = row === void 0 ? void 0 : verdicts?.[verdictIndex++];
|
|
6602
7721
|
if (row === void 0 || verdict) {
|
|
6603
7722
|
const reason = verdictReason(verdict);
|
|
6604
|
-
|
|
6605
|
-
|
|
7723
|
+
failing.push({
|
|
7724
|
+
id: ids[i],
|
|
7725
|
+
reason
|
|
7726
|
+
});
|
|
6606
7727
|
const skipReason = reason ?? (stale?.has(i) ? "stale" : void 0);
|
|
6607
7728
|
skipped.push(skipReason === void 0 ? { id: ids[i] } : {
|
|
6608
7729
|
id: ids[i],
|
|
@@ -6614,27 +7735,47 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
6614
7735
|
}
|
|
6615
7736
|
}
|
|
6616
7737
|
if (onDisabledRows === "skip") {
|
|
6617
|
-
if (passingRows.length === 0) throw
|
|
6618
|
-
if (
|
|
7738
|
+
if (passingRows.length === 0) throw disabledError(ctx, action, failing);
|
|
7739
|
+
if (failing.length > 0) {
|
|
6619
7740
|
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
6620
7741
|
ctx.set(dbActionIdsSlot, Promise.resolve(passingIds));
|
|
6621
7742
|
ctx.set(dbActionSkippedKey, skipped);
|
|
6622
7743
|
}
|
|
6623
7744
|
return;
|
|
6624
7745
|
}
|
|
6625
|
-
if (
|
|
7746
|
+
if (failing.length > 0) throw disabledError(ctx, action, failing);
|
|
7747
|
+
}
|
|
7748
|
+
/**
|
|
7749
|
+
* The 409 of a `'rows'` gate: every failing REQUEST id in request order, each
|
|
7750
|
+
* as the client sent it, with its own reason (`resolveRowIds`, since 0.1.148)
|
|
7751
|
+
* — {@link echoRequests} is the one place that maps resolved ids back.
|
|
7752
|
+
*/
|
|
7753
|
+
function disabledError(ctx, action, failing) {
|
|
7754
|
+
const echoed = echoRequests(ctx, failing);
|
|
7755
|
+
return new ActionDisabledError(action, void 0, echoed.map((f) => f.id), echoed.map((f) => f.reason));
|
|
7756
|
+
}
|
|
7757
|
+
/** The 409 of a `'row'` gate: the id as the client sent it. */
|
|
7758
|
+
function rowDisabledError(ctx, action, id, reason) {
|
|
7759
|
+
return new ActionDisabledError(action, requestIdOf(ctx, id), void 0, [reason]);
|
|
7760
|
+
}
|
|
7761
|
+
/** The action's scope restricts (or needs the loaded rows to decide) — see `TPreScope`. */
|
|
7762
|
+
async function needsScopeLoad(ctx, level) {
|
|
7763
|
+
const pre = await ctx.get(dbActionPreScopeSlot)(level);
|
|
7764
|
+
return pre.kind === "deferred" || pre.scope !== null;
|
|
6626
7765
|
}
|
|
6627
7766
|
/**
|
|
6628
7767
|
* Interceptor for `'row'` / `'rows'` actions without `disabled` (and for a
|
|
6629
7768
|
* `@DbActionRow*` handler of any other level: bound-table injection only):
|
|
6630
7769
|
* 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
|
-
*
|
|
7770
|
+
* injects the bound table and — only when there is something to verify —
|
|
7771
|
+
* checks the requested ids before the handler runs by loading the row(s) the
|
|
7772
|
+
* handler would get: `'row'` → the 404 of a missing row; `'rows'` →
|
|
7773
|
+
* out-of-scope and missing ids fail like disabled rows with no reason
|
|
7774
|
+
* (`onDisabledRows`). Something to verify: a row overlay (`transformOne` /
|
|
7775
|
+
* `transformFilter` overridden, non-empty), a non-empty `actionRowScope` for
|
|
7776
|
+
* the action (the hook is asked first, with the request's ids — an override
|
|
7777
|
+
* that restricts nothing verifies nothing; since 0.1.148), or a query target
|
|
7778
|
+
* (since 0.1.147). Nothing to verify → no query.
|
|
6638
7779
|
*/
|
|
6639
7780
|
function buildThinInterceptor(opts) {
|
|
6640
7781
|
const { table, scope } = opts;
|
|
@@ -6645,12 +7786,12 @@ function buildThinInterceptor(opts) {
|
|
|
6645
7786
|
if (!scope) return;
|
|
6646
7787
|
const overlay = await ctx.get(dbActionOverlaySlot);
|
|
6647
7788
|
if (scope.level === "row") {
|
|
6648
|
-
if (overlay ||
|
|
7789
|
+
if (overlay || await needsScopeLoad(ctx, "row")) await ctx.get(dbActionRowSlot);
|
|
6649
7790
|
return;
|
|
6650
7791
|
}
|
|
6651
7792
|
if (await queryTargetReply(ctx, reply)) return;
|
|
6652
7793
|
const target = await ctx.get(dbActionQueryTargetSlot);
|
|
6653
|
-
if (overlay || target ||
|
|
7794
|
+
if (overlay || target || await needsScopeLoad(ctx, "rows")) await gateRows(ctx, scope.action, void 0, scope.onDisabledRows);
|
|
6654
7795
|
await setMaterializedTarget(ctx);
|
|
6655
7796
|
}, ACTION_GATE_PRIORITY);
|
|
6656
7797
|
}
|
|
@@ -7073,6 +8214,52 @@ function DbActionsFrom(source, opts = {}) {
|
|
|
7073
8214
|
*/
|
|
7074
8215
|
const perRow = (fn) => (rows) => rows.map(fn);
|
|
7075
8216
|
//#endregion
|
|
8217
|
+
//#region src/decorations/db-decorations.decorator.ts
|
|
8218
|
+
/**
|
|
8219
|
+
* Declares display-only (decoration) fields of a table or view controller —
|
|
8220
|
+
* values `decorateRows` computes and attaches to rows (since 0.1.148).
|
|
8221
|
+
*
|
|
8222
|
+
* `type` is a plain atscript interface (no `@db.table` / `@db.view`) whose
|
|
8223
|
+
* top-level props are the decorations: their `@meta.label`, `@expect.*` and
|
|
8224
|
+
* `@ui.*` annotations travel in `/meta.decorations`, each key is listed in
|
|
8225
|
+
* `/meta.fields` with `decoration: true`, and a client may name it in
|
|
8226
|
+
* `$select`. A decoration is never filterable, sortable or groupable, and it
|
|
8227
|
+
* is not part of `/meta.type` (forms and write validation never see it).
|
|
8228
|
+
*
|
|
8229
|
+
* ```ts
|
|
8230
|
+
* @TableController(TicketTable)
|
|
8231
|
+
* @DbDecorations(TicketDecorations, { requires: { ownerName: ["ownerId"] } })
|
|
8232
|
+
* export class TicketsController extends AsDbController<typeof TicketTable> {
|
|
8233
|
+
* protected async decorateRows(rows: Record<string, unknown>[], ctx: TDbDecorateContext) {
|
|
8234
|
+
* if (ctx.decorations.has("ownerName")) {
|
|
8235
|
+
* // read rows[i].ownerId, set rows[i].ownerName
|
|
8236
|
+
* }
|
|
8237
|
+
* }
|
|
8238
|
+
* }
|
|
8239
|
+
* ```
|
|
8240
|
+
*
|
|
8241
|
+
* Validated once per class at first use (a `[moost-db]` error): the type is an
|
|
8242
|
+
* object interface; keys are top-level identifiers that collide with no field
|
|
8243
|
+
* or relation of the readable; every `requires` path is an own, readable
|
|
8244
|
+
* (not `@db.writeOnly`) field or a parent object of own fields (on SQL a nested
|
|
8245
|
+
* object is flattened to leaf columns; the hook still gets the whole object). Inherited under `@Inherit()`. Not supported on
|
|
8246
|
+
* value-help controllers.
|
|
8247
|
+
*
|
|
8248
|
+
* @since 0.1.148
|
|
8249
|
+
*/
|
|
8250
|
+
function DbDecorations(type, opts = {}) {
|
|
8251
|
+
if (!(0, _atscript_typescript_utils.isAnnotatedType)(type)) throw new Error("[moost-db] @DbDecorations: expects a compiled atscript interface");
|
|
8252
|
+
const meta = {
|
|
8253
|
+
type,
|
|
8254
|
+
requires: Object.fromEntries(Object.entries(opts.requires ?? {}).map(([key, paths]) => [key, [...paths ?? []]]))
|
|
8255
|
+
};
|
|
8256
|
+
const decorate = getAtscriptDbMate().decorate("atscript_db_decorations", meta);
|
|
8257
|
+
return (target) => {
|
|
8258
|
+
if (isAsValueHelpControllerSubclass(target)) throw new Error(`[moost-db] ${target.name} is a value-help controller — @DbDecorations is not supported there.`);
|
|
8259
|
+
return decorate(target);
|
|
8260
|
+
};
|
|
8261
|
+
}
|
|
8262
|
+
//#endregion
|
|
7076
8263
|
//#region src/permissions/crud-handlers.ts
|
|
7077
8264
|
/**
|
|
7078
8265
|
* The handler method(s) serving each CRUD op on `AsDbReadableController` /
|
|
@@ -7154,6 +8341,7 @@ exports.DbActionRows = DbActionRows;
|
|
|
7154
8341
|
exports.DbActionTarget = DbActionTarget;
|
|
7155
8342
|
exports.DbActions = DbActions;
|
|
7156
8343
|
exports.DbActionsFrom = DbActionsFrom;
|
|
8344
|
+
exports.DbDecorations = DbDecorations;
|
|
7157
8345
|
exports.DbRowActions = DbRowActions;
|
|
7158
8346
|
exports.DbRowsActions = DbRowsActions;
|
|
7159
8347
|
exports.DbTableActions = DbTableActions;
|
|
@@ -7175,6 +8363,7 @@ exports.applyTerminalRefs = applyTerminalRefs;
|
|
|
7175
8363
|
exports.assertExposed = assertExposed;
|
|
7176
8364
|
exports.badRequest = badRequest;
|
|
7177
8365
|
exports.clearDbSpaces = require_db_space_registry.clearDbSpaces;
|
|
8366
|
+
exports.closeDbSpaces = require_db_space_registry.closeDbSpaces;
|
|
7178
8367
|
Object.defineProperty(exports, "collectQueryPaths", {
|
|
7179
8368
|
enumerable: true,
|
|
7180
8369
|
get: function() {
|