@atscript/moost-db 0.1.143 → 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 CHANGED
@@ -348,7 +348,11 @@ function discoverActions(controllerCtor, app, logger) {
348
348
  actionsCache.set(controllerCtor, out);
349
349
  return out;
350
350
  }
351
- /** Row/rows-level subset of {@link discoverActions}; memoized per ctor. */
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 `rowOverlay()`,
1455
- * the same overlay `/one/:id` ANDs in (no hook call, no extra query when the
1456
- * controller overrides neither `transformOne` nor `transformFilter`), `null`
1457
- * when there is none. Evaluated once per request, after the controller's
1458
- * `prepareRequest` (since 0.1.143).
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
- if (!ctrl?.rowOverlay) return null;
1386
+ const overlayOf = ctrl?.[ACTION_OVERLAY];
1387
+ if (!overlayOf) return null;
1463
1388
  await awaitActionPrepared(ctx);
1464
- return await ctrl.rowOverlay() ?? null;
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
- /** WHY: filter row/rows envelopes by the per-request `applyMetaOverlay` action set; skip `meta()` when overlay is identity. */
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 overlayMeta = await this.resolveMeta();
2697
- const allowedNames = new Set(overlayMeta.actions.map((a) => a.name));
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
- const raw = e.raw;
2712
- if (!Array.isArray(raw.requiredFields)) continue;
2713
- for (const f of raw.requiredFields) {
2714
- if ((resultSet ? resultSet.has(f) : baseSelect.includes(f)) || !this.fieldVisibility.isVisible(f)) continue;
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
- return filter && !(0, _atscript_db.isEmptyObject)(filter) ? { $and: [filter, fragment] } : fragment;
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`), then {@link decorateRows} when a
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
- const overlay = await this.transformOne({});
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,97 +4465,46 @@ 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
- * Id columns plus the action's `requiredFields` — minus any the controller's
4091
- * field visibility hides (since 0.1.143; `hasField`, and a derived field over
4092
- * a hidden source): a hidden column is never loaded, so a `disabled`
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
- const fields = /* @__PURE__ */ new Set();
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
4107
4478
  * `dbActionOverlaySlot`, since 0.1.143): an out-of-scope id loads nothing —
4108
4479
  * exactly like a missing one (the same 404 on `'row'` actions, so the two
4109
4480
  * can't be told apart).
4481
+ *
4482
+ * The overlay resolves BEFORE the ids — i.e. before the request body is
4483
+ * read. Moost's HTTP adapter releases the event's DI scope when the request
4484
+ * stream ends, so a `transformFilter` / `transformOne` that instantiates a
4485
+ * `FOR_EVENT` dependency (an ARBAC user provider) fails with "scope isn't
4486
+ * registered" once the body has been consumed.
4110
4487
  */
4111
4488
  async function loadRow(ctx) {
4489
+ const overlay = await ctx.get(dbActionOverlaySlot);
4112
4490
  const id = await ctx.get(dbActionIdSlot);
4113
4491
  const table = asFetchTable(getActionTable(ctx));
4114
4492
  if (!table) throw noTableError(ctx);
4115
4493
  const fields = seedActionFields(ctx, table);
4116
4494
  for (const k of Object.keys(id)) fields.add(k);
4117
4495
  const row = await table.findOne({
4118
- filter: withOverlay(id, await ctx.get(dbActionOverlaySlot)),
4496
+ filter: withOverlay(id, overlay),
4119
4497
  controls: { $select: [...fields] }
4120
4498
  });
4121
4499
  if (row == null) throw new _moostjs_event_http.HttpError(404, "Row not found for action identifier");
4122
4500
  return row;
4123
4501
  }
4124
4502
  async function loadRows(ctx) {
4503
+ const overlay = await ctx.get(dbActionOverlaySlot);
4125
4504
  const ids = await ctx.get(dbActionIdsSlot);
4126
4505
  const table = asFetchTable(getActionTable(ctx));
4127
4506
  if (!table) throw noTableError(ctx);
4128
- if (ids.length === 0) return [];
4129
- const fields = seedActionFields(ctx, table);
4130
- const idKeys = [];
4131
- const shapes = /* @__PURE__ */ new Map();
4132
- const dedupedIds = [];
4133
- const seenKeys = /* @__PURE__ */ new Set();
4134
- for (const id of ids) {
4135
- const sortedFields = Object.keys(id).toSorted();
4136
- const sig = sortedFields.join("");
4137
- let key = "";
4138
- for (const f of sortedFields) {
4139
- fields.add(f);
4140
- key += `${f}\x1f${stringifyScalar(id[f])}\x1e`;
4141
- }
4142
- idKeys.push(key);
4143
- if (!shapes.has(sig)) shapes.set(sig, sortedFields);
4144
- if (!seenKeys.has(key)) {
4145
- seenKeys.add(key);
4146
- dedupedIds.push(id);
4147
- }
4148
- }
4149
- const rows = await table.findMany({
4150
- filter: withOverlay({ $or: dedupedIds }, await ctx.get(dbActionOverlaySlot)),
4151
- controls: { $select: [...fields] }
4152
- });
4153
- const rowByKey = /* @__PURE__ */ new Map();
4154
- for (const row of rows) for (const sortedFields of shapes.values()) {
4155
- let key = "";
4156
- let ok = true;
4157
- for (const f of sortedFields) {
4158
- const v = row[f];
4159
- if (v === void 0) {
4160
- ok = false;
4161
- break;
4162
- }
4163
- key += `${f}\x1f${stringifyScalar(v)}\x1e`;
4164
- }
4165
- if (ok && !rowByKey.has(key)) rowByKey.set(key, row);
4166
- }
4167
- return ids.map((_, i) => rowByKey.get(idKeys[i]));
4507
+ return findRowsByIds(table, ids, overlay, seedActionFields(ctx, table));
4168
4508
  }
4169
4509
  const dbActionRowSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRow(ctx));
4170
4510
  const dbActionRowsSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRows(ctx));
@@ -4184,10 +4524,10 @@ function buildGateInterceptor(opts) {
4184
4524
  const ctx = (0, _wooksjs_event_core.current)();
4185
4525
  await awaitActionPrepared(ctx);
4186
4526
  injectBoundTable(table);
4527
+ await ctx.get(dbActionOverlaySlot);
4187
4528
  if (level === "row") {
4188
- const verdicts = disabled([await ctx.get(dbActionRowSlot)]);
4189
- assertVerdictLength(action, verdicts, 1);
4190
- 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)]);
4191
4531
  return;
4192
4532
  }
4193
4533
  await gateRows(ctx, action, disabled, onDisabledRows);
@@ -4205,11 +4545,7 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
4205
4545
  const rows = await ctx.get(dbActionRowsSlot);
4206
4546
  const existingRows = [];
4207
4547
  for (const row of rows) if (row !== void 0) existingRows.push(row);
4208
- let verdicts;
4209
- if (disabled) {
4210
- verdicts = disabled(existingRows);
4211
- assertVerdictLength(action, verdicts, existingRows.length);
4212
- }
4548
+ const verdicts = disabled ? judgeRows(action, disabled, existingRows) : void 0;
4213
4549
  const failingIds = [];
4214
4550
  const failingReasons = [];
4215
4551
  const passingRows = [];
@@ -4241,7 +4577,8 @@ async function gateRows(ctx, action, disabled, onDisabledRows) {
4241
4577
  * `@DbActionRow*` handler of any other level: bound-table injection only):
4242
4578
  * runs the controller's `prepareRequest` (when defined, since 0.1.143),
4243
4579
  * injects the bound table and — only when the controller has a row overlay
4244
- * (`transformOne` / `transformFilter` overridden, non-empty) — verifies the
4580
+ * for the action (`transformOne` / `transformFilter` overridden, or — since
4581
+ * 0.1.145 — an `actionRowScope` for it; non-empty) — verifies the
4245
4582
  * requested ids against it before the handler runs by loading the row(s)
4246
4583
  * the handler would get: `'row'` → the 404 of a missing row; `'rows'` →
4247
4584
  * out-of-scope and missing ids fail like disabled rows with no reason
@@ -4677,10 +5014,12 @@ Object.defineProperty(exports, "collectQueryPaths", {
4677
5014
  exports.dbActionBodySlot = dbActionBodySlot;
4678
5015
  exports.dbActionInputSlot = dbActionInputSlot;
4679
5016
  exports.discoverActions = discoverActions;
5017
+ exports.discoverRowLevelActions = discoverRowLevelActions;
4680
5018
  exports.errorEnvelope = errorEnvelope;
4681
5019
  exports.findReadableBinding = findReadableBinding;
4682
5020
  exports.getAtscriptDbMate = getAtscriptDbMate;
4683
5021
  exports.getControllerFormType = getControllerFormType;
5022
+ exports.getDbEndpoint = getDbEndpoint;
4684
5023
  exports.perRow = perRow;
4685
5024
  exports.provideDbSpace = require_db_space_registry.provideDbSpace;
4686
5025
  exports.resolveBoundReadable = resolveBoundReadable;