@atscript/moost-db 0.1.144 → 0.1.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +521 -191
- package/dist/index.d.cts +143 -6
- package/dist/index.d.mts +143 -6
- package/dist/index.mjs +520 -192
- package/package.json +11 -11
package/dist/index.cjs
CHANGED
|
@@ -348,7 +348,11 @@ function discoverActions(controllerCtor, app, logger) {
|
|
|
348
348
|
actionsCache.set(controllerCtor, out);
|
|
349
349
|
return out;
|
|
350
350
|
}
|
|
351
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* The `'row'` / `'rows'`-level subset of {@link discoverActions} (since
|
|
353
|
+
* 0.1.145 public) — the actions `$actions` and `GET /meta/actions/:id`
|
|
354
|
+
* consider, in `/meta.actions` order. Memoized per controller class.
|
|
355
|
+
*/
|
|
352
356
|
function discoverRowLevelActions(controllerCtor, app, logger) {
|
|
353
357
|
const cached = rowLevelActionsCache.get(controllerCtor);
|
|
354
358
|
if (cached) return cached;
|
|
@@ -1110,94 +1114,6 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
|
|
|
1110
1114
|
Object
|
|
1111
1115
|
])], AsReadableController);
|
|
1112
1116
|
//#endregion
|
|
1113
|
-
//#region src/actions/verdict.ts
|
|
1114
|
-
/** Assert that a `disabled` predicate returned an array of the expected length; throws HTTP 500 otherwise. */
|
|
1115
|
-
function assertVerdictLength(action, verdicts, expected) {
|
|
1116
|
-
if (!Array.isArray(verdicts) || verdicts.length !== expected) throw new _moostjs_event_http.HttpError(500, `Action "${action}" disabled predicate returned an invalid verdict array`);
|
|
1117
|
-
}
|
|
1118
|
-
/** The reason a verdict carries — a non-empty string; `undefined` for `true` / falsy verdicts. */
|
|
1119
|
-
function verdictReason(verdict) {
|
|
1120
|
-
return typeof verdict === "string" && verdict !== "" ? verdict : void 0;
|
|
1121
|
-
}
|
|
1122
|
-
//#endregion
|
|
1123
|
-
//#region src/actions/list-augmenter.ts
|
|
1124
|
-
const candidateCache = /* @__PURE__ */ new WeakMap();
|
|
1125
|
-
/** WHY: envelopes are immutable post-discovery, so derived `Candidate` shape is cached for the envelope's lifetime; `null` sentinel pins table-level skip. */
|
|
1126
|
-
function getCandidate(e) {
|
|
1127
|
-
const cached = candidateCache.get(e);
|
|
1128
|
-
if (cached !== void 0) return cached;
|
|
1129
|
-
if (e.info.level !== "row" && e.info.level !== "rows") {
|
|
1130
|
-
candidateCache.set(e, null);
|
|
1131
|
-
return null;
|
|
1132
|
-
}
|
|
1133
|
-
const raw = e.raw;
|
|
1134
|
-
const c = {
|
|
1135
|
-
envelope: e,
|
|
1136
|
-
disabledFn: typeof raw.disabled === "function" ? raw.disabled : void 0,
|
|
1137
|
-
requiredFields: Array.isArray(raw.requiredFields) ? raw.requiredFields : []
|
|
1138
|
-
};
|
|
1139
|
-
candidateCache.set(e, c);
|
|
1140
|
-
return c;
|
|
1141
|
-
}
|
|
1142
|
-
function collectCandidates(envelopes) {
|
|
1143
|
-
const out = [];
|
|
1144
|
-
for (const e of envelopes) {
|
|
1145
|
-
const c = getCandidate(e);
|
|
1146
|
-
if (c !== null) out.push(c);
|
|
1147
|
-
}
|
|
1148
|
-
return out;
|
|
1149
|
-
}
|
|
1150
|
-
function computeStripFields(candidates, resolvedProjection) {
|
|
1151
|
-
let userSet = null;
|
|
1152
|
-
let strip = null;
|
|
1153
|
-
for (const c of candidates) for (const f of c.requiredFields) {
|
|
1154
|
-
if (userSet === null) userSet = new Set(resolvedProjection);
|
|
1155
|
-
if (userSet.has(f)) continue;
|
|
1156
|
-
if (strip === null) strip = /* @__PURE__ */ new Set();
|
|
1157
|
-
strip.add(f);
|
|
1158
|
-
}
|
|
1159
|
-
return strip;
|
|
1160
|
-
}
|
|
1161
|
-
/**
|
|
1162
|
-
* Sets `$actions` on every row (plus `$disabledReasons` on rows where a
|
|
1163
|
-
* predicate returned a reason string) and strips the columns fetched only for an
|
|
1164
|
-
* action's `requiredFields` — IN PLACE; returns the same array, typed as
|
|
1165
|
-
* augmented.
|
|
1166
|
-
*/
|
|
1167
|
-
function augmentRowsWithActions(args) {
|
|
1168
|
-
const { envelopes, rows, resolvedProjection } = args;
|
|
1169
|
-
const candidates = collectCandidates(envelopes);
|
|
1170
|
-
if (candidates.length === 0 || rows.length === 0) return rows;
|
|
1171
|
-
const verdicts = candidates.map((c) => {
|
|
1172
|
-
if (!c.disabledFn) return void 0;
|
|
1173
|
-
const out = c.disabledFn(rows);
|
|
1174
|
-
assertVerdictLength(c.envelope.info.name, out, rows.length);
|
|
1175
|
-
return out;
|
|
1176
|
-
});
|
|
1177
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1178
|
-
const row = rows[i];
|
|
1179
|
-
const names = [];
|
|
1180
|
-
let reasons;
|
|
1181
|
-
for (let j = 0; j < candidates.length; j++) {
|
|
1182
|
-
const name = candidates[j].envelope.info.name;
|
|
1183
|
-
const verdict = verdicts[j]?.[i];
|
|
1184
|
-
if (!verdict) {
|
|
1185
|
-
names.push(name);
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
|
-
const reason = verdictReason(verdict);
|
|
1189
|
-
if (reason !== void 0) (reasons ??= {})[name] = reason;
|
|
1190
|
-
}
|
|
1191
|
-
row.$actions = names;
|
|
1192
|
-
if (reasons) row.$disabledReasons = reasons;
|
|
1193
|
-
}
|
|
1194
|
-
if (resolvedProjection !== null) {
|
|
1195
|
-
const stripFields = computeStripFields(candidates, resolvedProjection);
|
|
1196
|
-
if (stripFields !== null) for (const row of rows) for (const f of stripFields) delete row[f];
|
|
1197
|
-
}
|
|
1198
|
-
return rows;
|
|
1199
|
-
}
|
|
1200
|
-
//#endregion
|
|
1201
1117
|
//#region src/actions/current-action.ts
|
|
1202
1118
|
/** Read the current action's `TDbActionMeta` from the wook context. Returns undefined outside a controller (e.g. direct-wook test paths). */
|
|
1203
1119
|
function readCurrentActionMeta(ctx) {
|
|
@@ -1435,6 +1351,13 @@ const useDbActionIds = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: ()
|
|
|
1435
1351
|
//#endregion
|
|
1436
1352
|
//#region src/actions/row-scope.ts
|
|
1437
1353
|
/**
|
|
1354
|
+
* The key of `AsDbReadableController`'s internal action-overlay method —
|
|
1355
|
+
* `rowOverlay()` AND (since 0.1.145) the action's `actionRowScope`. A
|
|
1356
|
+
* registered symbol: not an overridable seam, and still found when
|
|
1357
|
+
* moost-db loads in two module realms (moost-vite SSR).
|
|
1358
|
+
*/
|
|
1359
|
+
const ACTION_OVERLAY = Symbol.for("atscript-db.actionOverlay");
|
|
1360
|
+
/**
|
|
1438
1361
|
* The controller whose hooks govern this action's rows: only when the action
|
|
1439
1362
|
* runs against the controller's OWN readable — an `opts.table` binding on a
|
|
1440
1363
|
* plain controller has no row overlay or visibility hook. Once per event.
|
|
@@ -1451,18 +1374,24 @@ const scopedControllerSlot = (0, _wooksjs_event_core.cached)((ctx) => {
|
|
|
1451
1374
|
return ctrl;
|
|
1452
1375
|
});
|
|
1453
1376
|
/**
|
|
1454
|
-
* The controller's row overlay for action ids / rows — its
|
|
1455
|
-
* the
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
* `
|
|
1377
|
+
* The controller's row overlay for this action's ids / rows — its
|
|
1378
|
+
* `rowOverlay()` (the overlay `/one/:id` ANDs in) AND, since 0.1.145, the
|
|
1379
|
+
* action's `actionRowScope`; `null` when both are empty (no hook call, no
|
|
1380
|
+
* extra query when the controller overrides none of `transformOne`,
|
|
1381
|
+
* `transformFilter`, `actionRowScope`). Evaluated once per request, after
|
|
1382
|
+
* the controller's `prepareRequest` (since 0.1.143).
|
|
1459
1383
|
*/
|
|
1460
1384
|
const dbActionOverlaySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
1461
1385
|
const ctrl = ctx.get(scopedControllerSlot);
|
|
1462
|
-
|
|
1386
|
+
const overlayOf = ctrl?.[ACTION_OVERLAY];
|
|
1387
|
+
if (!overlayOf) return null;
|
|
1463
1388
|
await awaitActionPrepared(ctx);
|
|
1464
|
-
return await
|
|
1389
|
+
return await overlayOf.call(ctrl, readCurrentActionMeta(ctx)?.name) ?? null;
|
|
1465
1390
|
});
|
|
1391
|
+
/** `filter` unless it is absent or `{}`. */
|
|
1392
|
+
function nonEmptyFilter(filter) {
|
|
1393
|
+
return filter && !(0, _atscript_db.isEmptyObject)(filter) ? filter : void 0;
|
|
1394
|
+
}
|
|
1466
1395
|
/** `filter` AND the overlay (identity without one). */
|
|
1467
1396
|
function withOverlay(filter, overlay) {
|
|
1468
1397
|
return overlay ? { $and: [filter, overlay] } : filter;
|
|
@@ -1477,6 +1406,232 @@ function actionFieldVisibility(ctx) {
|
|
|
1477
1406
|
return visibility?.scoped ? visibility.isVisible : void 0;
|
|
1478
1407
|
}
|
|
1479
1408
|
//#endregion
|
|
1409
|
+
//#region src/actions/rows-by-id.ts
|
|
1410
|
+
function stringifyScalar(value) {
|
|
1411
|
+
if (value === null) return "null";
|
|
1412
|
+
if (value === void 0) return "undefined";
|
|
1413
|
+
return String(value);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* The identity of `id`'s values over `fields` (sorted) — equal for equal
|
|
1417
|
+
* keys across driver representations (a number vs its string, an ObjectId
|
|
1418
|
+
* vs its hex). `undefined` when `row` lacks one of the fields.
|
|
1419
|
+
*/
|
|
1420
|
+
function idKey(row, fields) {
|
|
1421
|
+
let key = "";
|
|
1422
|
+
for (const f of fields) {
|
|
1423
|
+
const v = row[f];
|
|
1424
|
+
if (v === void 0) return void 0;
|
|
1425
|
+
key += `${f}\x1f${stringifyScalar(v)}\x1e`;
|
|
1426
|
+
}
|
|
1427
|
+
return key;
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* The rows `ids` address that also match `scope` (none = every row),
|
|
1431
|
+
* aligned with `ids` — `undefined` where nothing matched. One `findMany`
|
|
1432
|
+
* over the deduped ids (`{ $or: ids } AND scope`) selecting `select` plus
|
|
1433
|
+
* every id field; ids of different identification shapes may be mixed.
|
|
1434
|
+
* The action row loader and `$actions` scope check share it.
|
|
1435
|
+
*/
|
|
1436
|
+
async function findRowsByIds(source, ids, scope, select) {
|
|
1437
|
+
if (ids.length === 0) return [];
|
|
1438
|
+
const fields = new Set(select);
|
|
1439
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
1440
|
+
const idKeys = [];
|
|
1441
|
+
const dedupedIds = [];
|
|
1442
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
1443
|
+
for (const id of ids) {
|
|
1444
|
+
const sortedFields = Object.keys(id).toSorted();
|
|
1445
|
+
for (const f of sortedFields) fields.add(f);
|
|
1446
|
+
const sig = sortedFields.join("");
|
|
1447
|
+
if (!shapes.has(sig)) shapes.set(sig, sortedFields);
|
|
1448
|
+
const key = idKey(id, sortedFields);
|
|
1449
|
+
idKeys.push(key);
|
|
1450
|
+
if (key !== void 0 && !seenKeys.has(key)) {
|
|
1451
|
+
seenKeys.add(key);
|
|
1452
|
+
dedupedIds.push(id);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const rows = await source.findMany({
|
|
1456
|
+
filter: withOverlay({ $or: dedupedIds }, scope),
|
|
1457
|
+
controls: { $select: [...fields] }
|
|
1458
|
+
});
|
|
1459
|
+
const rowByKey = /* @__PURE__ */ new Map();
|
|
1460
|
+
for (const row of rows) for (const sortedFields of shapes.values()) {
|
|
1461
|
+
const key = idKey(row, sortedFields);
|
|
1462
|
+
if (key !== void 0 && !rowByKey.has(key)) rowByKey.set(key, row);
|
|
1463
|
+
}
|
|
1464
|
+
return idKeys.map((key) => key === void 0 ? void 0 : rowByKey.get(key));
|
|
1465
|
+
}
|
|
1466
|
+
/** The `requiredFields` of action opts (`@DbAction` opts or a discovered envelope's raw entry). */
|
|
1467
|
+
function requiredFieldsOf(opts) {
|
|
1468
|
+
const fields = opts?.requiredFields;
|
|
1469
|
+
return Array.isArray(fields) ? fields : [];
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Id columns (`preferredId`, else the primary key) plus `requiredFields` —
|
|
1473
|
+
* minus any `isVisible` hides (since 0.1.143; `hasField`, and a derived
|
|
1474
|
+
* field over a hidden source): a hidden column is never loaded, so a
|
|
1475
|
+
* `disabled` predicate sees it as `undefined`. What an action's gate loads.
|
|
1476
|
+
*/
|
|
1477
|
+
function actionRowFields(table, requiredFields, isVisible) {
|
|
1478
|
+
const fields = new Set(table.preferredId ?? table.primaryKeys);
|
|
1479
|
+
for (const f of requiredFields) if (!isVisible || isVisible(f)) fields.add(f);
|
|
1480
|
+
return fields;
|
|
1481
|
+
}
|
|
1482
|
+
/** `true` when `row` carries a top-level key outside `fields` (projecting it would drop something). */
|
|
1483
|
+
function exceedsFields(row, fields) {
|
|
1484
|
+
for (const key in row) if (!fields.has(key)) return true;
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
/** `row` narrowed to `fields` (dot paths copied into fresh nested objects). */
|
|
1488
|
+
function projectRow$2(row, fields) {
|
|
1489
|
+
const out = {};
|
|
1490
|
+
for (const path of fields) {
|
|
1491
|
+
const parts = path.split(".");
|
|
1492
|
+
let src = row;
|
|
1493
|
+
for (const p of parts) src = src?.[p];
|
|
1494
|
+
if (src === void 0) continue;
|
|
1495
|
+
let dst = out;
|
|
1496
|
+
for (let i = 0; i < parts.length - 1; i++) dst = dst[parts[i]] ??= {};
|
|
1497
|
+
dst[parts.at(-1)] = src;
|
|
1498
|
+
}
|
|
1499
|
+
return out;
|
|
1500
|
+
}
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region src/actions/verdict.ts
|
|
1503
|
+
/** Assert that a `disabled` predicate returned an array of the expected length; throws HTTP 500 otherwise. */
|
|
1504
|
+
function assertVerdictLength(action, verdicts, expected) {
|
|
1505
|
+
if (!Array.isArray(verdicts) || verdicts.length !== expected) throw new _moostjs_event_http.HttpError(500, `Action "${action}" disabled predicate returned an invalid verdict array`);
|
|
1506
|
+
}
|
|
1507
|
+
/** Runs `disabled` over `rows` — one verdict per row (a wrong-length answer → HTTP 500). */
|
|
1508
|
+
function judgeRows(action, disabled, rows) {
|
|
1509
|
+
const verdicts = disabled(rows);
|
|
1510
|
+
assertVerdictLength(action, verdicts, rows.length);
|
|
1511
|
+
return verdicts;
|
|
1512
|
+
}
|
|
1513
|
+
/** {@link judgeRows} for one row. */
|
|
1514
|
+
function judgeRow(action, disabled, row) {
|
|
1515
|
+
return judgeRows(action, disabled, [row])[0];
|
|
1516
|
+
}
|
|
1517
|
+
/** The reason a verdict carries — a non-empty string; `undefined` for `true` / falsy verdicts. */
|
|
1518
|
+
function verdictReason(verdict) {
|
|
1519
|
+
return typeof verdict === "string" && verdict !== "" ? verdict : void 0;
|
|
1520
|
+
}
|
|
1521
|
+
//#endregion
|
|
1522
|
+
//#region src/actions/list-augmenter.ts
|
|
1523
|
+
const candidateCache = /* @__PURE__ */ new WeakMap();
|
|
1524
|
+
/** WHY: envelopes are immutable post-discovery, so derived `Candidate` shape is cached for the envelope's lifetime; `null` sentinel pins table-level skip. */
|
|
1525
|
+
function getCandidate(e) {
|
|
1526
|
+
const cached = candidateCache.get(e);
|
|
1527
|
+
if (cached !== void 0) return cached;
|
|
1528
|
+
if (e.info.level !== "row" && e.info.level !== "rows") {
|
|
1529
|
+
candidateCache.set(e, null);
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
const raw = e.raw;
|
|
1533
|
+
const c = {
|
|
1534
|
+
envelope: e,
|
|
1535
|
+
disabledFn: typeof raw.disabled === "function" ? raw.disabled : void 0,
|
|
1536
|
+
requiredFields: requiredFieldsOf(e.raw)
|
|
1537
|
+
};
|
|
1538
|
+
candidateCache.set(e, c);
|
|
1539
|
+
return c;
|
|
1540
|
+
}
|
|
1541
|
+
function collectCandidates(envelopes) {
|
|
1542
|
+
const out = [];
|
|
1543
|
+
for (const e of envelopes) {
|
|
1544
|
+
const c = getCandidate(e);
|
|
1545
|
+
if (c !== null) out.push(c);
|
|
1546
|
+
}
|
|
1547
|
+
return out;
|
|
1548
|
+
}
|
|
1549
|
+
function computeStripFields(candidates, resolvedProjection) {
|
|
1550
|
+
let userSet = null;
|
|
1551
|
+
let strip = null;
|
|
1552
|
+
for (const c of candidates) for (const f of c.requiredFields) {
|
|
1553
|
+
if (userSet === null) userSet = new Set(resolvedProjection);
|
|
1554
|
+
if (userSet.has(f)) continue;
|
|
1555
|
+
if (strip === null) strip = /* @__PURE__ */ new Set();
|
|
1556
|
+
strip.add(f);
|
|
1557
|
+
}
|
|
1558
|
+
return strip;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Sets `$actions` on every row (plus `$disabledReasons` on rows where a
|
|
1562
|
+
* predicate returned a reason string) and strips the columns fetched only for an
|
|
1563
|
+
* action's `requiredFields` — IN PLACE; returns the same array, typed as
|
|
1564
|
+
* augmented.
|
|
1565
|
+
*/
|
|
1566
|
+
function augmentRowsWithActions(args) {
|
|
1567
|
+
const { envelopes, rows, resolvedProjection, outOfScope, gateFields } = args;
|
|
1568
|
+
const candidates = collectCandidates(envelopes);
|
|
1569
|
+
if (candidates.length === 0 || rows.length === 0) return rows;
|
|
1570
|
+
const verdicts = candidates.map((c) => {
|
|
1571
|
+
if (!c.disabledFn) return void 0;
|
|
1572
|
+
const fields = gateFields?.(c.envelope);
|
|
1573
|
+
const input = fields && rows.some((row) => exceedsFields(row, fields)) ? rows.map((row) => projectRow$2(row, fields)) : rows;
|
|
1574
|
+
return judgeRows(c.envelope.info.name, c.disabledFn, input);
|
|
1575
|
+
});
|
|
1576
|
+
const masks = outOfScope && candidates.map((c) => outOfScope.get(c.envelope.info.name));
|
|
1577
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1578
|
+
const row = rows[i];
|
|
1579
|
+
const names = [];
|
|
1580
|
+
let reasons;
|
|
1581
|
+
for (let j = 0; j < candidates.length; j++) {
|
|
1582
|
+
if (masks?.[j]?.[i]) continue;
|
|
1583
|
+
const name = candidates[j].envelope.info.name;
|
|
1584
|
+
const verdict = verdicts[j]?.[i];
|
|
1585
|
+
if (!verdict) {
|
|
1586
|
+
names.push(name);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
const reason = verdictReason(verdict);
|
|
1590
|
+
if (reason !== void 0) (reasons ??= {})[name] = reason;
|
|
1591
|
+
}
|
|
1592
|
+
row.$actions = names;
|
|
1593
|
+
if (reasons) row.$disabledReasons = reasons;
|
|
1594
|
+
}
|
|
1595
|
+
if (resolvedProjection !== null) {
|
|
1596
|
+
const stripFields = computeStripFields(candidates, resolvedProjection);
|
|
1597
|
+
if (stripFields !== null) for (const row of rows) for (const f of stripFields) delete row[f];
|
|
1598
|
+
}
|
|
1599
|
+
return rows;
|
|
1600
|
+
}
|
|
1601
|
+
//#endregion
|
|
1602
|
+
//#region src/db-endpoint.ts
|
|
1603
|
+
/**
|
|
1604
|
+
* Tags a framework handler whose authorization is delegated to the
|
|
1605
|
+
* controller's `prepareRequest` (it runs first with `endpoint`), so a
|
|
1606
|
+
* permission layer can recognise it without knowing its method name.
|
|
1607
|
+
* Internal — moost-db tags its own handlers.
|
|
1608
|
+
*/
|
|
1609
|
+
function DbEndpoint(endpoint) {
|
|
1610
|
+
return getAtscriptDbMate().decorate("atscript_db_endpoint", endpoint);
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* The `prepareRequest` endpoint a controller handler delegates its
|
|
1614
|
+
* authorization to (since 0.1.145) — `undefined` for a handler that is
|
|
1615
|
+
* authorized on its own like any other route. `target` is the controller
|
|
1616
|
+
* instance or class; the tag is inherited through the class chain (a
|
|
1617
|
+
* subclass override of a tagged handler keeps it). Today only
|
|
1618
|
+
* `GET /meta/actions/:id` and `/meta/actions?…` are tagged
|
|
1619
|
+
* (`"availableActions"`).
|
|
1620
|
+
*
|
|
1621
|
+
* ```ts
|
|
1622
|
+
* const endpoint = getDbEndpoint(controller, methodName);
|
|
1623
|
+
* if (endpoint) return; // prepareRequest({ endpoint }) authorizes this call
|
|
1624
|
+
* ```
|
|
1625
|
+
*/
|
|
1626
|
+
function getDbEndpoint(target, method) {
|
|
1627
|
+
const mate = getAtscriptDbMate();
|
|
1628
|
+
let proto = typeof target === "function" ? target.prototype : Object.getPrototypeOf(target);
|
|
1629
|
+
for (; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
|
|
1630
|
+
const endpoint = mate.read(proto, method)?.atscript_db_endpoint;
|
|
1631
|
+
if (endpoint) return endpoint;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
//#endregion
|
|
1480
1635
|
//#region src/decorators.ts
|
|
1481
1636
|
/**
|
|
1482
1637
|
* DI token under which the {@link AtscriptDbReadable} instance
|
|
@@ -1982,6 +2137,8 @@ function writeOnlyError(path, op) {
|
|
|
1982
2137
|
const verdict = writeOnlyVerdict(path, op);
|
|
1983
2138
|
return badRequest(verdict.path, verdict.message);
|
|
1984
2139
|
}
|
|
2140
|
+
/** Controller classes already warned that `actionRowScope` has no row identity to match by. */
|
|
2141
|
+
const warnedNoIdentity = /* @__PURE__ */ new WeakSet();
|
|
1985
2142
|
let AsDbReadableController = _AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
1986
2143
|
/** Reference to the underlying readable (table or view). */
|
|
1987
2144
|
readable;
|
|
@@ -2055,6 +2212,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2055
2212
|
_decorates;
|
|
2056
2213
|
/** `true` when a subclass overrides {@link transformOne} or {@link transformFilter} (a row overlay may exist). */
|
|
2057
2214
|
_hasRowOverlay;
|
|
2215
|
+
/** Per-envelope gate fields, memoized while field visibility is not request-scoped. */
|
|
2216
|
+
_gateFieldsMemo = /* @__PURE__ */ new WeakMap();
|
|
2217
|
+
/** `true` when a subclass overrides {@link allowedActions}. */
|
|
2218
|
+
_hasAllowedActions;
|
|
2219
|
+
/** `true` when a subclass overrides {@link actionRowScope} (the gate, `$actions` and `/meta/actions` apply it). */
|
|
2220
|
+
_hasActionRowScope;
|
|
2058
2221
|
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
2059
2222
|
_quantityRefByPath;
|
|
2060
2223
|
/** `@db.column.searchable` paths — the `$search` fallback when the adapter has no native search. */
|
|
@@ -2082,6 +2245,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2082
2245
|
this._decorates = typeof this.decorateRows === "function";
|
|
2083
2246
|
const proto = _AsDbReadableController.prototype;
|
|
2084
2247
|
this._hasRowOverlay = this.transformOne !== proto.transformOne || this.transformFilter !== proto.transformFilter;
|
|
2248
|
+
this._hasActionRowScope = this.actionRowScope !== proto.actionRowScope;
|
|
2249
|
+
this._hasAllowedActions = this.allowedActions !== proto.allowedActions;
|
|
2250
|
+
if (this._hasActionRowScope && resolved.preferredId.length === 0 && !warnedNoIdentity.has(new.target)) {
|
|
2251
|
+
warnedNoIdentity.add(new.target);
|
|
2252
|
+
this.logger.warn(`actionRowScope() is overridden but "${resolved.tableName}" has no primary key — \`$actions\` withholds every action with a non-empty row scope`);
|
|
2253
|
+
}
|
|
2085
2254
|
const scoped = this.hasField !== proto.hasField;
|
|
2086
2255
|
this._hasFieldOverridden = scoped;
|
|
2087
2256
|
const isVisible = (path) => {
|
|
@@ -2561,6 +2730,50 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2561
2730
|
return this.transformFilter(filter);
|
|
2562
2731
|
}
|
|
2563
2732
|
/**
|
|
2733
|
+
* The subset of the row-level action `names` the caller may run (since
|
|
2734
|
+
* 0.1.145) — what `$actions` and `GET /meta/actions/:id` list from. The
|
|
2735
|
+
* default keeps the names present in the per-request `/meta` envelope
|
|
2736
|
+
* (`applyMetaOverlay` over the cached one), or all of them when
|
|
2737
|
+
* `applyMetaOverlay` is not overridden (no call). Override it to answer
|
|
2738
|
+
* from per-action permission checks without building the whole `/meta`
|
|
2739
|
+
* overlay on every `$actions` read. Runs after {@link prepareRequest}.
|
|
2740
|
+
* Names it returns that are not in `names` are ignored.
|
|
2741
|
+
*
|
|
2742
|
+
* @since 0.1.145
|
|
2743
|
+
*/
|
|
2744
|
+
allowedActions(names) {
|
|
2745
|
+
if (this._overlayIsNoOp) return names;
|
|
2746
|
+
return (async () => {
|
|
2747
|
+
const present = new Set((await this.resolveMeta()).actions.map((a) => a.name));
|
|
2748
|
+
return names.filter((name) => present.has(name));
|
|
2749
|
+
})();
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* The rows the row-level action `actionName` may run on (since 0.1.145),
|
|
2753
|
+
* as an extra row filter; `undefined` or `{}` = no restriction (the
|
|
2754
|
+
* default). Enforced by the action gate — ANDed with the {@link rowOverlay}
|
|
2755
|
+
* the action's ids / rows are loaded under, so an id outside it gets the
|
|
2756
|
+
* same 404 "Row not found for action identifier" as a missing one — and
|
|
2757
|
+
* reflected in `$actions` and `GET /meta/actions`, which list the action
|
|
2758
|
+
* only on rows inside it.
|
|
2759
|
+
*
|
|
2760
|
+
* Runs after {@link prepareRequest}, once per action per request. `$actions`
|
|
2761
|
+
* checks the page's rows with one id-only query per distinct filter
|
|
2762
|
+
* OBJECT (return the same object for several actions to share one query),
|
|
2763
|
+
* straight against the bound readable: the filter may use fields
|
|
2764
|
+
* {@link hasField} hides, and nothing of it reaches the response. Not
|
|
2765
|
+
* overriding it costs nothing.
|
|
2766
|
+
*
|
|
2767
|
+
* ```ts
|
|
2768
|
+
* protected actionRowScope(action: string) {
|
|
2769
|
+
* return action === "approve" ? { ownerId: currentUserId() } : undefined
|
|
2770
|
+
* }
|
|
2771
|
+
* ```
|
|
2772
|
+
*
|
|
2773
|
+
* @since 0.1.145
|
|
2774
|
+
*/
|
|
2775
|
+
actionRowScope(_actionName) {}
|
|
2776
|
+
/**
|
|
2564
2777
|
* Transform projection before querying.
|
|
2565
2778
|
* May return a Promise for async lookups.
|
|
2566
2779
|
*/
|
|
@@ -2688,14 +2901,13 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2688
2901
|
if (excluded.length > 0 && included.length === 0) return this._invertExclusion(new Set(excluded));
|
|
2689
2902
|
throw new _moostjs_event_http.HttpError(500, "[moost-db] mixed inclusion/exclusion projection reached augmenter; widenPreferredIdProjection should have rejected it");
|
|
2690
2903
|
}
|
|
2691
|
-
/**
|
|
2904
|
+
/** Row/rows envelopes narrowed to {@link allowedActions}; no hook call when neither it nor `applyMetaOverlay` is overridden. */
|
|
2692
2905
|
async _resolveAugmentEnvelopes() {
|
|
2693
2906
|
const rowLevelEnvelopes = discoverRowLevelActions(this.constructor, this.app, this.logger);
|
|
2694
2907
|
if (rowLevelEnvelopes.length === 0) return null;
|
|
2695
|
-
if (this._overlayIsNoOp) return rowLevelEnvelopes;
|
|
2696
|
-
const
|
|
2697
|
-
const
|
|
2698
|
-
const filtered = rowLevelEnvelopes.filter((e) => allowedNames.has(e.info.name));
|
|
2908
|
+
if (this._overlayIsNoOp && !this._hasAllowedActions) return rowLevelEnvelopes;
|
|
2909
|
+
const allowed = new Set(await this.allowedActions(rowLevelEnvelopes.map((e) => e.info.name)));
|
|
2910
|
+
const filtered = rowLevelEnvelopes.filter((e) => allowed.has(e.info.name));
|
|
2699
2911
|
return filtered.length === 0 ? null : filtered;
|
|
2700
2912
|
}
|
|
2701
2913
|
/**
|
|
@@ -2707,18 +2919,14 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2707
2919
|
_widenSelectForActions(envelopes, baseSelect) {
|
|
2708
2920
|
let resultSet = null;
|
|
2709
2921
|
let result = null;
|
|
2710
|
-
for (const e of envelopes) {
|
|
2711
|
-
|
|
2712
|
-
if (
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
if (resultSet === null) {
|
|
2716
|
-
resultSet = new Set(baseSelect);
|
|
2717
|
-
result = [...baseSelect];
|
|
2718
|
-
}
|
|
2719
|
-
resultSet.add(f);
|
|
2720
|
-
result.push(f);
|
|
2922
|
+
for (const e of envelopes) for (const f of requiredFieldsOf(e.raw)) {
|
|
2923
|
+
if ((resultSet ? resultSet.has(f) : baseSelect.includes(f)) || !this.fieldVisibility.isVisible(f)) continue;
|
|
2924
|
+
if (resultSet === null) {
|
|
2925
|
+
resultSet = new Set(baseSelect);
|
|
2926
|
+
result = [...baseSelect];
|
|
2721
2927
|
}
|
|
2928
|
+
resultSet.add(f);
|
|
2929
|
+
result.push(f);
|
|
2722
2930
|
}
|
|
2723
2931
|
return result;
|
|
2724
2932
|
}
|
|
@@ -2726,14 +2934,104 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2726
2934
|
if (!controls.$actions) return null;
|
|
2727
2935
|
const envelopes = await this._resolveAugmentEnvelopes();
|
|
2728
2936
|
if (envelopes === null) return null;
|
|
2937
|
+
let scopeGroups = null;
|
|
2938
|
+
if (this._hasActionRowScope) {
|
|
2939
|
+
scopeGroups = this._resolveActionScopeGroups(envelopes);
|
|
2940
|
+
scopeGroups.catch(() => {});
|
|
2941
|
+
}
|
|
2729
2942
|
const resolvedProjection = this._resolveProjectionForAugmenter(select);
|
|
2730
2943
|
return {
|
|
2731
2944
|
envelopes,
|
|
2732
2945
|
resolvedProjection,
|
|
2733
|
-
widenedSelect: resolvedProjection === null ? null : this._widenSelectForActions(envelopes, resolvedProjection)
|
|
2946
|
+
widenedSelect: resolvedProjection === null ? null : this._widenSelectForActions(envelopes, resolvedProjection),
|
|
2947
|
+
scopeGroups
|
|
2734
2948
|
};
|
|
2735
2949
|
}
|
|
2736
2950
|
/**
|
|
2951
|
+
* {@link actionRowScope} of every offered action (in parallel, alongside
|
|
2952
|
+
* `overlay`), grouped by filter object; each group's filter is composed
|
|
2953
|
+
* with the row overlay exactly as the gate composes it.
|
|
2954
|
+
*/
|
|
2955
|
+
async _resolveActionScopeGroups(envelopes, overlay = this.rowOverlay()) {
|
|
2956
|
+
const [rowOverlay, scopes] = await Promise.all([overlay, Promise.all(envelopes.map(async (e) => this._actionScope(e.info.name)))]);
|
|
2957
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2958
|
+
for (let i = 0; i < envelopes.length; i++) {
|
|
2959
|
+
const scope = scopes[i];
|
|
2960
|
+
if (!scope) continue;
|
|
2961
|
+
let group = groups.get(scope);
|
|
2962
|
+
if (!group) {
|
|
2963
|
+
group = {
|
|
2964
|
+
filter: withOverlay(scope, rowOverlay),
|
|
2965
|
+
actions: []
|
|
2966
|
+
};
|
|
2967
|
+
groups.set(scope, group);
|
|
2968
|
+
}
|
|
2969
|
+
group.actions.push(envelopes[i].info.name);
|
|
2970
|
+
}
|
|
2971
|
+
return [...groups.values()];
|
|
2972
|
+
}
|
|
2973
|
+
/** Per scoped action, a per-row mask of rows outside its scope (one id-only read per group). */
|
|
2974
|
+
async _outOfScopeMasks(rows, groups) {
|
|
2975
|
+
const idFields = this.readable.preferredId;
|
|
2976
|
+
const idIndex = [];
|
|
2977
|
+
const ids = [];
|
|
2978
|
+
for (const row of rows) {
|
|
2979
|
+
const id = {};
|
|
2980
|
+
let complete = idFields.length > 0;
|
|
2981
|
+
for (const f of idFields) {
|
|
2982
|
+
const value = row[f];
|
|
2983
|
+
if (value === void 0 || value === null) {
|
|
2984
|
+
complete = false;
|
|
2985
|
+
break;
|
|
2986
|
+
}
|
|
2987
|
+
id[f] = value;
|
|
2988
|
+
}
|
|
2989
|
+
idIndex.push(complete ? ids.push(id) - 1 : -1);
|
|
2990
|
+
}
|
|
2991
|
+
const source = this.readable;
|
|
2992
|
+
const found = await Promise.all(groups.map((group) => findRowsByIds(source, ids, group.filter, [])));
|
|
2993
|
+
const masks = /* @__PURE__ */ new Map();
|
|
2994
|
+
for (let g = 0; g < groups.length; g++) {
|
|
2995
|
+
const mask = idIndex.map((i) => i < 0 || found[g][i] === void 0);
|
|
2996
|
+
for (const name of groups[g].actions) masks.set(name, mask);
|
|
2997
|
+
}
|
|
2998
|
+
return masks;
|
|
2999
|
+
}
|
|
3000
|
+
/**
|
|
3001
|
+
* The row filter {@link actionRowScope} returns for `action` — `undefined`
|
|
3002
|
+
* when empty or when the hook is not overridden (no call). THE per-action
|
|
3003
|
+
* scope rule: the action gate, `$actions` and `/meta/actions` all read it here.
|
|
3004
|
+
*/
|
|
3005
|
+
async _actionScope(action) {
|
|
3006
|
+
if (!this._hasActionRowScope) return void 0;
|
|
3007
|
+
return nonEmptyFilter(await this.actionRowScope(action));
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* The fields `envelope`'s gate loads ({@link actionRowFields} under this
|
|
3011
|
+
* request's field visibility) — memoized unless visibility is request-scoped.
|
|
3012
|
+
*/
|
|
3013
|
+
_gateFields(envelope) {
|
|
3014
|
+
const visibility = this.fieldVisibility;
|
|
3015
|
+
const required = requiredFieldsOf(envelope.raw);
|
|
3016
|
+
if (visibility.scoped) return actionRowFields(this.readable, required, visibility.isVisible);
|
|
3017
|
+
let fields = this._gateFieldsMemo.get(envelope);
|
|
3018
|
+
if (!fields) {
|
|
3019
|
+
fields = actionRowFields(this.readable, required, void 0);
|
|
3020
|
+
this._gateFieldsMemo.set(envelope, fields);
|
|
3021
|
+
}
|
|
3022
|
+
return fields;
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* @internal The action gate's overlay (reached by the `@DbAction`
|
|
3026
|
+
* interceptor through {@link ACTION_OVERLAY}): {@link rowOverlay} AND the
|
|
3027
|
+
* action's {@link actionRowScope} (since 0.1.145) — `undefined` when both
|
|
3028
|
+
* are empty.
|
|
3029
|
+
*/
|
|
3030
|
+
async [ACTION_OVERLAY](action) {
|
|
3031
|
+
const [overlay, scope] = await Promise.all([this.rowOverlay(), action === void 0 ? void 0 : this._actionScope(action)]);
|
|
3032
|
+
return scope ? withOverlay(scope, overlay) : overlay;
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
2737
3035
|
* `@db.column.searchable` paths, minus anything the adapter can't filter
|
|
2738
3036
|
* (JSON storage, encrypted), `@db.writeOnly` fields and navigation
|
|
2739
3037
|
* descendants — physical capability only (manual-mode policy does not
|
|
@@ -2802,7 +3100,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2802
3100
|
if (fields.length === 0) return filter;
|
|
2803
3101
|
const rx = `/${term.replace(/[.*+?^${}()|[\]\\/]/g, String.raw`\$&`)}/i`;
|
|
2804
3102
|
const fragment = { $or: fields.map((f) => ({ [f]: { $regex: rx } })) };
|
|
2805
|
-
|
|
3103
|
+
const base = nonEmptyFilter(filter);
|
|
3104
|
+
return base ? { $and: [base, fragment] } : fragment;
|
|
2806
3105
|
}
|
|
2807
3106
|
/**
|
|
2808
3107
|
* The controls a grouped query hands to the adapter. `$search` has two
|
|
@@ -2848,15 +3147,27 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2848
3147
|
}
|
|
2849
3148
|
/**
|
|
2850
3149
|
* Finishes a read's top-level rows in place: `$actions` augmentation (when
|
|
2851
|
-
* the request asked for it — `prep
|
|
3150
|
+
* the request asked for it — `prep`; minus actions whose
|
|
3151
|
+
* {@link actionRowScope} a row misses), then {@link decorateRows} when a
|
|
2852
3152
|
* subclass implements it. Returns the hook's result — `undefined`, with no
|
|
2853
3153
|
* promise or microtask, when there is no hook or it is synchronous.
|
|
2854
3154
|
*/
|
|
2855
3155
|
_finishRows(rows, prep, ctx) {
|
|
3156
|
+
const groups = prep?.scopeGroups;
|
|
3157
|
+
if (!groups) return this._augmentAndDecorate(rows, prep, ctx);
|
|
3158
|
+
return (async () => {
|
|
3159
|
+
const masks = await this._outOfScopeMasks(rows, await groups);
|
|
3160
|
+
await this._augmentAndDecorate(rows, prep, ctx, masks);
|
|
3161
|
+
})();
|
|
3162
|
+
}
|
|
3163
|
+
/** `$actions` augmentation (when `prep`), then {@link decorateRows} when implemented. */
|
|
3164
|
+
_augmentAndDecorate(rows, prep, ctx, outOfScope) {
|
|
2856
3165
|
if (prep) augmentRowsWithActions({
|
|
2857
3166
|
envelopes: prep.envelopes,
|
|
2858
3167
|
rows,
|
|
2859
|
-
resolvedProjection: prep.resolvedProjection
|
|
3168
|
+
resolvedProjection: prep.resolvedProjection,
|
|
3169
|
+
gateFields: (e) => this._gateFields(e),
|
|
3170
|
+
outOfScope
|
|
2860
3171
|
});
|
|
2861
3172
|
return this._decorates ? this.decorateRows(rows, ctx) : void 0;
|
|
2862
3173
|
}
|
|
@@ -2931,8 +3242,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2931
3242
|
*/
|
|
2932
3243
|
async rowOverlay() {
|
|
2933
3244
|
if (!this._hasRowOverlay) return void 0;
|
|
2934
|
-
|
|
2935
|
-
return overlay && !(0, _atscript_db.isEmptyObject)(overlay) ? overlay : void 0;
|
|
3245
|
+
return nonEmptyFilter(await this.transformOne({}));
|
|
2936
3246
|
}
|
|
2937
3247
|
/**
|
|
2938
3248
|
* Pick the first identification (PK or unique index) whose fields are all
|
|
@@ -3175,6 +3485,71 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
3175
3485
|
return item;
|
|
3176
3486
|
}
|
|
3177
3487
|
/**
|
|
3488
|
+
* **GET /meta/actions/:id** — the row-level actions the caller may run on
|
|
3489
|
+
* the row `id` addresses (the `/one/:id` id forms), as
|
|
3490
|
+
* `{ actions, disabledReasons? }` — no row data (since 0.1.145). Exactly
|
|
3491
|
+
* what calling each action would do: the action survives
|
|
3492
|
+
* {@link applyMetaOverlay}, the row resolves under its gate overlay
|
|
3493
|
+
* ({@link actionOverlay}) and its `disabled` rule passes on the fields the
|
|
3494
|
+
* gate loads. Needs no read grant; an unknown or out-of-scope id answers
|
|
3495
|
+
* `{ actions: [] }`. {@link prepareRequest} runs first with
|
|
3496
|
+
* `endpoint: "availableActions"`.
|
|
3497
|
+
*/
|
|
3498
|
+
async availableActionsById(id) {
|
|
3499
|
+
await this.parseRequest("availableActions");
|
|
3500
|
+
return this._availableActions(id);
|
|
3501
|
+
}
|
|
3502
|
+
/**
|
|
3503
|
+
* **GET /meta/actions?field1=val1&…** — {@link availableActionsById} by
|
|
3504
|
+
* composite key (composite primary key or compound unique index), the
|
|
3505
|
+
* `/one?…` rules.
|
|
3506
|
+
*/
|
|
3507
|
+
async availableActions(query) {
|
|
3508
|
+
await this.parseRequest("availableActions");
|
|
3509
|
+
const idObj = this.extractIdShape(query);
|
|
3510
|
+
if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
|
|
3511
|
+
return this._availableActions(idObj);
|
|
3512
|
+
}
|
|
3513
|
+
/**
|
|
3514
|
+
* The row resolves ONCE, like `/one/:id` under {@link rowOverlay}; scoped
|
|
3515
|
+
* actions are then checked on it exactly like `$actions` rows.
|
|
3516
|
+
*/
|
|
3517
|
+
async _availableActions(id) {
|
|
3518
|
+
const envelopes = await this._resolveAugmentEnvelopes();
|
|
3519
|
+
if (envelopes === null) return { actions: [] };
|
|
3520
|
+
const overlay = this.rowOverlay();
|
|
3521
|
+
const groups = this._hasActionRowScope ? this._resolveActionScopeGroups(envelopes, overlay) : void 0;
|
|
3522
|
+
groups?.catch(() => {});
|
|
3523
|
+
const idKeys = id !== null && typeof id === "object" ? Object.keys(id) : [];
|
|
3524
|
+
const fieldsOf = envelopes.map((e) => {
|
|
3525
|
+
const fields = this._gateFields(e);
|
|
3526
|
+
return idKeys.every((k) => fields.has(k)) ? fields : new Set([...fields, ...idKeys]);
|
|
3527
|
+
});
|
|
3528
|
+
const select = /* @__PURE__ */ new Set();
|
|
3529
|
+
for (const fields of fieldsOf) for (const f of fields) select.add(f);
|
|
3530
|
+
const row = await this._findRow(id, await overlay, { $select: [...select] });
|
|
3531
|
+
if (!row) return { actions: [] };
|
|
3532
|
+
const masks = groups ? await this._outOfScopeMasks([row], await groups) : void 0;
|
|
3533
|
+
const actions = [];
|
|
3534
|
+
let disabledReasons;
|
|
3535
|
+
for (let i = 0; i < envelopes.length; i++) {
|
|
3536
|
+
const name = envelopes[i].info.name;
|
|
3537
|
+
if (masks?.get(name)?.[0]) continue;
|
|
3538
|
+
const disabled = getCandidate(envelopes[i])?.disabledFn;
|
|
3539
|
+
const verdict = disabled ? judgeRow(name, disabled, projectRow$2(row, fieldsOf[i])) : false;
|
|
3540
|
+
if (!verdict) {
|
|
3541
|
+
actions.push(name);
|
|
3542
|
+
continue;
|
|
3543
|
+
}
|
|
3544
|
+
const reason = verdictReason(verdict);
|
|
3545
|
+
if (reason !== void 0) (disabledReasons ??= {})[name] = reason;
|
|
3546
|
+
}
|
|
3547
|
+
return disabledReasons ? {
|
|
3548
|
+
actions,
|
|
3549
|
+
disabledReasons
|
|
3550
|
+
} : { actions };
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3178
3553
|
* **GET /meta** — returns table/view metadata for UI.
|
|
3179
3554
|
*
|
|
3180
3555
|
* Overrides the base's minimal envelope to add relations, searchable flags,
|
|
@@ -3279,6 +3654,22 @@ __decorate([
|
|
|
3279
3654
|
__decorateMetadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record, String]),
|
|
3280
3655
|
__decorateMetadata("design:returntype", Promise)
|
|
3281
3656
|
], AsDbReadableController.prototype, "getOneComposite", null);
|
|
3657
|
+
__decorate([
|
|
3658
|
+
(0, _moostjs_event_http.Get)("meta/actions/:id"),
|
|
3659
|
+
DbEndpoint("availableActions"),
|
|
3660
|
+
__decorateParam(0, (0, moost.Param)("id")),
|
|
3661
|
+
__decorateMetadata("design:type", Function),
|
|
3662
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
3663
|
+
__decorateMetadata("design:returntype", Promise)
|
|
3664
|
+
], AsDbReadableController.prototype, "availableActionsById", null);
|
|
3665
|
+
__decorate([
|
|
3666
|
+
(0, _moostjs_event_http.Get)("meta/actions"),
|
|
3667
|
+
DbEndpoint("availableActions"),
|
|
3668
|
+
__decorateParam(0, (0, _moostjs_event_http.Query)()),
|
|
3669
|
+
__decorateMetadata("design:type", Function),
|
|
3670
|
+
__decorateMetadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
|
|
3671
|
+
__decorateMetadata("design:returntype", Promise)
|
|
3672
|
+
], AsDbReadableController.prototype, "availableActions", null);
|
|
3282
3673
|
AsDbReadableController = _AsDbReadableController = __decorate([
|
|
3283
3674
|
(0, moost.Inherit)(),
|
|
3284
3675
|
__decorateParam(1, (0, moost.Inject)(READABLE_DEF)),
|
|
@@ -4074,33 +4465,13 @@ function asFetchTable(value) {
|
|
|
4074
4465
|
if (Array.isArray(v.primaryKeys) && typeof v.findOne === "function" && typeof v.findMany === "function") return v;
|
|
4075
4466
|
return null;
|
|
4076
4467
|
}
|
|
4077
|
-
function stringifyScalar(value) {
|
|
4078
|
-
if (value === null) return "null";
|
|
4079
|
-
if (value === void 0) return "undefined";
|
|
4080
|
-
return String(value);
|
|
4081
|
-
}
|
|
4082
|
-
/** Returns the action's `requiredFields` or null when called outside a controller context (e.g. direct wook usage in tests). */
|
|
4083
|
-
function readActionFieldSet(ctx) {
|
|
4084
|
-
const action = readCurrentActionMeta(ctx);
|
|
4085
|
-
if (!action) return null;
|
|
4086
|
-
const opts = action.opts;
|
|
4087
|
-
return Array.isArray(opts.requiredFields) ? opts.requiredFields : null;
|
|
4088
|
-
}
|
|
4089
4468
|
/**
|
|
4090
|
-
*
|
|
4091
|
-
*
|
|
4092
|
-
*
|
|
4093
|
-
* predicate sees it as `undefined`.
|
|
4469
|
+
* What this action's gate loads: {@link actionRowFields} of its
|
|
4470
|
+
* `requiredFields` (none outside a controller context, e.g. direct wook
|
|
4471
|
+
* usage in tests) under the controller's field visibility.
|
|
4094
4472
|
*/
|
|
4095
4473
|
function seedActionFields(ctx, table) {
|
|
4096
|
-
|
|
4097
|
-
for (const f of table.preferredId ?? table.primaryKeys) fields.add(f);
|
|
4098
|
-
const action = readActionFieldSet(ctx);
|
|
4099
|
-
if (action) {
|
|
4100
|
-
const visible = actionFieldVisibility(ctx);
|
|
4101
|
-
for (const f of action) if (!visible || visible(f)) fields.add(f);
|
|
4102
|
-
}
|
|
4103
|
-
return fields;
|
|
4474
|
+
return actionRowFields(table, requiredFieldsOf(readCurrentActionMeta(ctx)?.opts), actionFieldVisibility(ctx));
|
|
4104
4475
|
}
|
|
4105
4476
|
/**
|
|
4106
4477
|
* Loaded row / rows are ANDed with the controller's row overlay (see
|
|
@@ -4133,46 +4504,7 @@ async function loadRows(ctx) {
|
|
|
4133
4504
|
const ids = await ctx.get(dbActionIdsSlot);
|
|
4134
4505
|
const table = asFetchTable(getActionTable(ctx));
|
|
4135
4506
|
if (!table) throw noTableError(ctx);
|
|
4136
|
-
|
|
4137
|
-
const fields = seedActionFields(ctx, table);
|
|
4138
|
-
const idKeys = [];
|
|
4139
|
-
const shapes = /* @__PURE__ */ new Map();
|
|
4140
|
-
const dedupedIds = [];
|
|
4141
|
-
const seenKeys = /* @__PURE__ */ new Set();
|
|
4142
|
-
for (const id of ids) {
|
|
4143
|
-
const sortedFields = Object.keys(id).toSorted();
|
|
4144
|
-
const sig = sortedFields.join("");
|
|
4145
|
-
let key = "";
|
|
4146
|
-
for (const f of sortedFields) {
|
|
4147
|
-
fields.add(f);
|
|
4148
|
-
key += `${f}\x1f${stringifyScalar(id[f])}\x1e`;
|
|
4149
|
-
}
|
|
4150
|
-
idKeys.push(key);
|
|
4151
|
-
if (!shapes.has(sig)) shapes.set(sig, sortedFields);
|
|
4152
|
-
if (!seenKeys.has(key)) {
|
|
4153
|
-
seenKeys.add(key);
|
|
4154
|
-
dedupedIds.push(id);
|
|
4155
|
-
}
|
|
4156
|
-
}
|
|
4157
|
-
const rows = await table.findMany({
|
|
4158
|
-
filter: withOverlay({ $or: dedupedIds }, overlay),
|
|
4159
|
-
controls: { $select: [...fields] }
|
|
4160
|
-
});
|
|
4161
|
-
const rowByKey = /* @__PURE__ */ new Map();
|
|
4162
|
-
for (const row of rows) for (const sortedFields of shapes.values()) {
|
|
4163
|
-
let key = "";
|
|
4164
|
-
let ok = true;
|
|
4165
|
-
for (const f of sortedFields) {
|
|
4166
|
-
const v = row[f];
|
|
4167
|
-
if (v === void 0) {
|
|
4168
|
-
ok = false;
|
|
4169
|
-
break;
|
|
4170
|
-
}
|
|
4171
|
-
key += `${f}\x1f${stringifyScalar(v)}\x1e`;
|
|
4172
|
-
}
|
|
4173
|
-
if (ok && !rowByKey.has(key)) rowByKey.set(key, row);
|
|
4174
|
-
}
|
|
4175
|
-
return ids.map((_, i) => rowByKey.get(idKeys[i]));
|
|
4507
|
+
return findRowsByIds(table, ids, overlay, seedActionFields(ctx, table));
|
|
4176
4508
|
}
|
|
4177
4509
|
const dbActionRowSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRow(ctx));
|
|
4178
4510
|
const dbActionRowsSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRows(ctx));
|
|
@@ -4194,9 +4526,8 @@ function buildGateInterceptor(opts) {
|
|
|
4194
4526
|
injectBoundTable(table);
|
|
4195
4527
|
await ctx.get(dbActionOverlaySlot);
|
|
4196
4528
|
if (level === "row") {
|
|
4197
|
-
const
|
|
4198
|
-
|
|
4199
|
-
if (verdicts[0]) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot), void 0, [verdictReason(verdicts[0])]);
|
|
4529
|
+
const verdict = judgeRow(action, disabled, await ctx.get(dbActionRowSlot));
|
|
4530
|
+
if (verdict) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot), void 0, [verdictReason(verdict)]);
|
|
4200
4531
|
return;
|
|
4201
4532
|
}
|
|
4202
4533
|
await gateRows(ctx, action, disabled, onDisabledRows);
|
|
@@ -4214,11 +4545,7 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
4214
4545
|
const rows = await ctx.get(dbActionRowsSlot);
|
|
4215
4546
|
const existingRows = [];
|
|
4216
4547
|
for (const row of rows) if (row !== void 0) existingRows.push(row);
|
|
4217
|
-
|
|
4218
|
-
if (disabled) {
|
|
4219
|
-
verdicts = disabled(existingRows);
|
|
4220
|
-
assertVerdictLength(action, verdicts, existingRows.length);
|
|
4221
|
-
}
|
|
4548
|
+
const verdicts = disabled ? judgeRows(action, disabled, existingRows) : void 0;
|
|
4222
4549
|
const failingIds = [];
|
|
4223
4550
|
const failingReasons = [];
|
|
4224
4551
|
const passingRows = [];
|
|
@@ -4250,7 +4577,8 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
|
|
|
4250
4577
|
* `@DbActionRow*` handler of any other level: bound-table injection only):
|
|
4251
4578
|
* runs the controller's `prepareRequest` (when defined, since 0.1.143),
|
|
4252
4579
|
* injects the bound table and — only when the controller has a row overlay
|
|
4253
|
-
* (`transformOne` / `transformFilter` overridden,
|
|
4580
|
+
* for the action (`transformOne` / `transformFilter` overridden, or — since
|
|
4581
|
+
* 0.1.145 — an `actionRowScope` for it; non-empty) — verifies the
|
|
4254
4582
|
* requested ids against it before the handler runs by loading the row(s)
|
|
4255
4583
|
* the handler would get: `'row'` → the 404 of a missing row; `'rows'` →
|
|
4256
4584
|
* out-of-scope and missing ids fail like disabled rows with no reason
|
|
@@ -4686,10 +5014,12 @@ Object.defineProperty(exports, "collectQueryPaths", {
|
|
|
4686
5014
|
exports.dbActionBodySlot = dbActionBodySlot;
|
|
4687
5015
|
exports.dbActionInputSlot = dbActionInputSlot;
|
|
4688
5016
|
exports.discoverActions = discoverActions;
|
|
5017
|
+
exports.discoverRowLevelActions = discoverRowLevelActions;
|
|
4689
5018
|
exports.errorEnvelope = errorEnvelope;
|
|
4690
5019
|
exports.findReadableBinding = findReadableBinding;
|
|
4691
5020
|
exports.getAtscriptDbMate = getAtscriptDbMate;
|
|
4692
5021
|
exports.getControllerFormType = getControllerFormType;
|
|
5022
|
+
exports.getDbEndpoint = getDbEndpoint;
|
|
4693
5023
|
exports.perRow = perRow;
|
|
4694
5024
|
exports.provideDbSpace = require_db_space_registry.provideDbSpace;
|
|
4695
5025
|
exports.resolveBoundReadable = resolveBoundReadable;
|