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