@atscript/moost-db 0.1.58 → 0.1.59

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
@@ -99,7 +99,7 @@ var SelectControlDto = class {
99
99
  (0, _atscript_typescript_utils.throwFeatureDisabled)("JSON Schema", "jsonSchema", "emit.jsonSchema");
100
100
  }
101
101
  };
102
- (0, _atscript_typescript_utils.defineAnnotatedType)("object", QueryControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
102
+ (0, _atscript_typescript_utils.defineAnnotatedType)("object", QueryControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
103
103
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", PagesControlsDto).prop("$page", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").annotate("expect.pattern", {
104
104
  pattern: "^\\d+$",
105
105
  flags: "u",
@@ -108,8 +108,8 @@ var SelectControlDto = class {
108
108
  pattern: "^\\d+$",
109
109
  flags: "u",
110
110
  message: "Expected positive number"
111
- }, true).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
112
- (0, _atscript_typescript_utils.defineAnnotatedType)("object", GetOneControlsDto).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
111
+ }, true).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
112
+ (0, _atscript_typescript_utils.defineAnnotatedType)("object", GetOneControlsDto).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
113
113
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", WithRelationDto).prop("name", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).prop("filter", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).optional().$type).prop("controls", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationControlsDto).optional().$type).prop("insights", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).optional().$type);
114
114
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", WithRelationControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
115
115
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", WithFilterDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("number").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("null").tags("null").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).$type).$type).$type);
@@ -210,7 +210,7 @@ const WARN_PREFIX = "[moost-db actions]";
210
210
  const MOOST_DB_ACTION = "atscript_db_action";
211
211
  /** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
212
212
  const MOOST_DB_ACTIONS = "atscript_db_actions";
213
- /** Param-level metadata key — written by `@DbActionPK()` / `@DbActionPKs()`. Drives level inference. */
213
+ /** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
214
214
  const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
215
215
  /** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
216
216
  const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
@@ -240,8 +240,8 @@ function scanParamLevel(params) {
240
240
  let hasBody = false;
241
241
  for (const p of params) {
242
242
  const kind = p[MOOST_DB_ACTION_PARAM];
243
- if (kind === "pk") single = true;
244
- else if (kind === "pks") multi = true;
243
+ if (kind === "id") single = true;
244
+ else if (kind === "ids") multi = true;
245
245
  if (p["atscript_db_action_row"]) {
246
246
  single = true;
247
247
  hasRowParam = true;
@@ -262,11 +262,7 @@ function scanParamLevel(params) {
262
262
  }
263
263
  //#endregion
264
264
  //#region src/actions/discover.ts
265
- /**
266
- * Pure structural-copy fields. `disabled` and `requiredFields` are handled
267
- * as special cases in {@link emitInfo} so the function-to-string transform
268
- * stays out of the copy loop.
269
- */
265
+ /** Structural-copy fields; `disabled` is handled separately in {@link emitInfo} (function-to-string transform). */
270
266
  const OPTIONAL_FIELDS = [
271
267
  "icon",
272
268
  "intent",
@@ -277,27 +273,29 @@ const OPTIONAL_FIELDS = [
277
273
  "shortcut"
278
274
  ];
279
275
  const actionsCache = /* @__PURE__ */ new WeakMap();
280
- /**
281
- * Discover all actions declared on a controller and produce the `/meta` array.
282
- * Reads class + method metadata via `getMoostMate()` and resolves bound POST
283
- * paths through the Moost controller overview.
284
- *
285
- * Result is memoized per controller constructor — discovery walks every
286
- * handler entry and reads decorator metadata, which is wasted work to repeat
287
- * across instances.
288
- */
276
+ const rowLevelActionsCache = /* @__PURE__ */ new WeakMap();
277
+ /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
289
278
  function discoverActions(controllerCtor, app, logger) {
290
279
  const cached = actionsCache.get(controllerCtor);
291
280
  if (cached) return cached;
292
281
  const overview = app.getControllersOverview?.()?.find((o) => o.type === controllerCtor);
293
282
  const out = [];
294
- collectMethodActions(controllerCtor, overview, logger, out);
295
- collectClassActions(controllerCtor, logger, out);
283
+ const seen = /* @__PURE__ */ new Set();
284
+ collectMethodActions(controllerCtor, overview, logger, out, seen);
285
+ collectClassActions(controllerCtor, logger, out, seen);
296
286
  applyDefaultPerLevel(out, logger);
297
287
  actionsCache.set(controllerCtor, out);
298
288
  return out;
299
289
  }
300
- function collectMethodActions(ctor, overview, logger, out) {
290
+ /** Row/rows-level subset of {@link discoverActions}; memoized per ctor. */
291
+ function discoverRowLevelActions(controllerCtor, app, logger) {
292
+ const cached = rowLevelActionsCache.get(controllerCtor);
293
+ if (cached) return cached;
294
+ const filtered = discoverActions(controllerCtor, app, logger).filter((e) => e.info.level === "row" || e.info.level === "rows");
295
+ rowLevelActionsCache.set(controllerCtor, filtered);
296
+ return filtered;
297
+ }
298
+ function collectMethodActions(ctor, overview, logger, out, seen) {
301
299
  if (!overview) return;
302
300
  const byMethod = /* @__PURE__ */ new Map();
303
301
  for (const h of overview.handlers) {
@@ -316,13 +314,17 @@ function collectMethodActions(ctor, overview, logger, out) {
316
314
  const levelInfer = inferMethodLevel(methodMeta.params ?? [], action.name, logger);
317
315
  if (!levelInfer) continue;
318
316
  if (levelInfer.bodyConflict) {
319
- logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionPK*/@DbActionPKs/@DbActionRow*/@DbActionRows with @Body() — dropping`);
317
+ logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionID*/@DbActionIDs/@DbActionRow*/@DbActionRows with @Body() — dropping`);
320
318
  continue;
321
319
  }
322
320
  if (levelInfer.level === "table" && action.opts.disabled !== void 0) {
323
321
  logger.warn(`${WARN_PREFIX} action "${action.name}" — \`disabled\` is not allowed at the 'table' level; row-state predicates are not meaningful when no row is in scope. Use @Authenticate / arbac for table-level access — dropping`);
324
322
  continue;
325
323
  }
324
+ if (action.opts.disabled !== void 0 && !isNonEmptyStringArray(action.opts.requiredFields)) {
325
+ logger.warn(`${WARN_PREFIX} action "${action.name}" — \`disabled\` requires a non-empty \`requiredFields\` array (the predicate's field dependencies must be declared explicitly) — dropping`);
326
+ continue;
327
+ }
326
328
  if (action.opts.disabled !== void 0 || levelInfer.hasRowParam) {
327
329
  const extendsReadable = isAsDbReadableControllerSubclass(ctor);
328
330
  const hasOptsTable = action.opts.table != null;
@@ -346,6 +348,10 @@ function collectMethodActions(ctor, overview, logger, out) {
346
348
  logger.warn(`${WARN_PREFIX} action "${action.name}" requires a label (opts.label or @Label) — dropping`);
347
349
  continue;
348
350
  }
351
+ if (seen.has(action.name)) {
352
+ logger.warn(`${WARN_PREFIX} duplicate action name "${action.name}" within controller — dropping the second declaration`);
353
+ continue;
354
+ }
349
355
  const info = {
350
356
  name: action.name,
351
357
  label,
@@ -353,14 +359,18 @@ function collectMethodActions(ctor, overview, logger, out) {
353
359
  processor: "backend",
354
360
  value: path
355
361
  };
356
- emitInfo(info, action.opts, action.name, logger);
357
- out.push(info);
362
+ emitInfo(info, action.opts);
363
+ seen.add(action.name);
364
+ out.push({
365
+ info,
366
+ raw: action.opts
367
+ });
358
368
  }
359
369
  }
360
370
  function inferMethodLevel(params, actionName, logger) {
361
371
  const scan = scanParamLevel(params);
362
372
  if (scan.single && scan.multi) {
363
- logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionPK / @DbActionRow vs @DbActionPKs / @DbActionRows) — dropping`);
373
+ logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
364
374
  return null;
365
375
  }
366
376
  return {
@@ -369,12 +379,22 @@ function inferMethodLevel(params, actionName, logger) {
369
379
  hasRowParam: scan.hasRowParam
370
380
  };
371
381
  }
372
- function collectClassActions(ctor, logger, out) {
382
+ function collectClassActions(ctor, logger, out, seen) {
373
383
  const list = (0, moost.getMoostMate)().read(ctor)?.[MOOST_DB_ACTIONS];
374
384
  if (!list) return;
375
385
  for (const { name, entry } of list) {
386
+ if (seen.has(name)) {
387
+ logger.warn(`${WARN_PREFIX} duplicate action name "${name}" within controller — dropping the second declaration`);
388
+ continue;
389
+ }
376
390
  const built = buildClassEntry(name, entry, logger);
377
- if (built) out.push(built);
391
+ if (built) {
392
+ seen.add(name);
393
+ out.push({
394
+ info: built,
395
+ raw: entry
396
+ });
397
+ }
378
398
  }
379
399
  }
380
400
  function buildClassEntry(name, entry, logger) {
@@ -391,6 +411,10 @@ function buildClassEntry(name, entry, logger) {
391
411
  logger.warn(`${WARN_PREFIX} class-level action "${name}" — \`disabled\` is not allowed at the 'table' level — dropping`);
392
412
  return null;
393
413
  }
414
+ if (entry.disabled !== void 0 && !isNonEmptyStringArray(entry.requiredFields)) {
415
+ logger.warn(`${WARN_PREFIX} class-level action "${name}" — \`disabled\` requires a non-empty \`requiredFields\` array (the predicate's field dependencies must be declared explicitly) — dropping`);
416
+ return null;
417
+ }
394
418
  const processor = entry.processor;
395
419
  let value;
396
420
  if (processor === "navigate" || processor === "backend") {
@@ -418,39 +442,27 @@ function buildClassEntry(name, entry, logger) {
418
442
  processor,
419
443
  value
420
444
  };
421
- emitInfo(info, entry, name, logger);
445
+ emitInfo(info, entry);
422
446
  return info;
423
447
  }
424
- function applyDefaultPerLevel(actions, logger) {
448
+ function applyDefaultPerLevel(envelopes, logger) {
425
449
  const winners = /* @__PURE__ */ new Map();
426
- for (const a of actions) {
427
- if (!a.default) continue;
428
- const existing = winners.get(a.level);
450
+ for (const { info } of envelopes) {
451
+ if (!info.default) continue;
452
+ const existing = winners.get(info.level);
429
453
  if (existing) {
430
- a.default = false;
431
- logger.warn(`${WARN_PREFIX} duplicate default action at level "${a.level}": "${existing}" wins, "${a.name}" demoted`);
432
- } else winners.set(a.level, a.name);
454
+ info.default = false;
455
+ logger.warn(`${WARN_PREFIX} duplicate default action at level "${info.level}": "${existing}" wins, "${info.name}" demoted`);
456
+ } else winners.set(info.level, info.name);
433
457
  }
434
458
  }
435
- /**
436
- * Emit structural-copy fields plus `disabled` (stringified) and
437
- * `requiredFields` (forwarded verbatim — server doesn't auto-derive).
438
- * `requiredFields` without `disabled` is dropped with a warning before the
439
- * structural copy runs, so method-decorator and class-level-dict origins
440
- * stay symmetric.
441
- */
442
- function emitInfo(info, source, name, logger) {
459
+ /** Emit structural-copy fields plus stringified `disabled`. `requiredFields` is server-internal (never on the wire). */
460
+ function emitInfo(info, source) {
443
461
  const disabled = source.disabled;
444
462
  const hasDisabled = typeof disabled === "function";
445
- let requiredFields = source.requiredFields;
446
- if (!hasDisabled && requiredFields !== void 0) {
447
- logger.warn(`${WARN_PREFIX} action "${name}" has \`requiredFields\` without \`disabled\` — \`requiredFields\` is purely a UI hint and meaningless without a predicate. Dropping \`requiredFields\` from /meta.`);
448
- requiredFields = void 0;
449
- }
450
463
  copyOptionalFields(info, source);
451
464
  if (Array.isArray(info.promptText)) info.promptText = info.promptText.slice();
452
465
  if (hasDisabled) info.disabled = disabled.toString();
453
- if (Array.isArray(requiredFields)) info.requiredFields = requiredFields.slice();
454
466
  }
455
467
  function copyOptionalFields(info, source) {
456
468
  for (const key of OPTIONAL_FIELDS) {
@@ -458,6 +470,9 @@ function copyOptionalFields(info, source) {
458
470
  if (value !== void 0) info[key] = value;
459
471
  }
460
472
  }
473
+ function isNonEmptyStringArray(value) {
474
+ return Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string");
475
+ }
461
476
  //#endregion
462
477
  //#region \0@oxc-project+runtime@0.120.0/helpers/decorateMetadata.js
463
478
  function __decorateMetadata(k, v) {
@@ -640,6 +655,7 @@ let AsReadableController = class AsReadableController {
640
655
  vectorSearchable: false,
641
656
  searchIndexes: [],
642
657
  primaryKeys: [],
658
+ preferredId: [],
643
659
  relations: [],
644
660
  fields: {},
645
661
  type: this.getSerializedType(),
@@ -653,7 +669,7 @@ let AsReadableController = class AsReadableController {
653
669
  * controllers — see {@link AsValueHelpController#buildMetaResponse}.
654
670
  */
655
671
  buildActions() {
656
- return discoverActions(this.constructor, this.app, this.logger);
672
+ return discoverActions(this.constructor, this.app, this.logger).map((e) => e.info);
657
673
  }
658
674
  /**
659
675
  * Declares the built-in CRUD operations this controller exposes. Subclasses
@@ -688,6 +704,80 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
688
704
  Object
689
705
  ])], AsReadableController);
690
706
  //#endregion
707
+ //#region src/actions/verdict.ts
708
+ /** Assert that a `disabled` predicate returned a `boolean[]` of the expected length; throws HTTP 500 otherwise. */
709
+ function assertVerdictLength(action, verdicts, expected) {
710
+ if (!Array.isArray(verdicts) || verdicts.length !== expected) throw new _moostjs_event_http.HttpError(500, `Action "${action}" disabled predicate returned an invalid verdict array`);
711
+ }
712
+ //#endregion
713
+ //#region src/actions/list-augmenter.ts
714
+ const candidateCache = /* @__PURE__ */ new WeakMap();
715
+ /** WHY: envelopes are immutable post-discovery, so derived `Candidate` shape is cached for the envelope's lifetime; `null` sentinel pins table-level skip. */
716
+ function getCandidate(e) {
717
+ const cached = candidateCache.get(e);
718
+ if (cached !== void 0) return cached;
719
+ if (e.info.level !== "row" && e.info.level !== "rows") {
720
+ candidateCache.set(e, null);
721
+ return null;
722
+ }
723
+ const raw = e.raw;
724
+ const c = {
725
+ envelope: e,
726
+ disabledFn: typeof raw.disabled === "function" ? raw.disabled : void 0,
727
+ requiredFields: Array.isArray(raw.requiredFields) ? raw.requiredFields : []
728
+ };
729
+ candidateCache.set(e, c);
730
+ return c;
731
+ }
732
+ function collectCandidates(envelopes) {
733
+ const out = [];
734
+ for (const e of envelopes) {
735
+ const c = getCandidate(e);
736
+ if (c !== null) out.push(c);
737
+ }
738
+ return out;
739
+ }
740
+ function computeStripFields(candidates, resolvedProjection) {
741
+ let userSet = null;
742
+ let strip = null;
743
+ for (const c of candidates) for (const f of c.requiredFields) {
744
+ if (userSet === null) userSet = new Set(resolvedProjection);
745
+ if (userSet.has(f)) continue;
746
+ if (strip === null) strip = /* @__PURE__ */ new Set();
747
+ strip.add(f);
748
+ }
749
+ return strip;
750
+ }
751
+ function augmentRowsWithActions(args) {
752
+ const { envelopes, rows, resolvedProjection } = args;
753
+ const candidates = collectCandidates(envelopes);
754
+ if (candidates.length === 0 || rows.length === 0) return rows;
755
+ const verdicts = candidates.map((c) => {
756
+ if (!c.disabledFn) return void 0;
757
+ const out = c.disabledFn(rows);
758
+ assertVerdictLength(c.envelope.info.name, out, rows.length);
759
+ return out;
760
+ });
761
+ for (let i = 0; i < rows.length; i++) {
762
+ const row = rows[i];
763
+ const names = [];
764
+ for (let j = 0; j < candidates.length; j++) {
765
+ const v = verdicts[j];
766
+ if (v === void 0) {
767
+ names.push(candidates[j].envelope.info.name);
768
+ continue;
769
+ }
770
+ if (!v[i]) names.push(candidates[j].envelope.info.name);
771
+ }
772
+ row.$actions = names;
773
+ }
774
+ if (resolvedProjection !== null) {
775
+ const stripFields = computeStripFields(candidates, resolvedProjection);
776
+ if (stripFields !== null) for (const row of rows) for (const f of stripFields) delete row[f];
777
+ }
778
+ return rows;
779
+ }
780
+ //#endregion
691
781
  //#region src/decorators.ts
692
782
  /**
693
783
  * DI token under which the {@link AtscriptDbReadable} instance
@@ -782,10 +872,17 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
782
872
  /** Reference to the underlying readable (table or view). */
783
873
  readable;
784
874
  _gates;
875
+ _preferredIdSet;
876
+ _compositeIdShapes;
877
+ _overlayIsNoOp;
785
878
  constructor(readable, app) {
786
879
  super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
787
880
  this.readable = readable;
788
881
  this._gates = this._buildGates();
882
+ this._preferredIdSet = new Set(readable.preferredId ?? []);
883
+ this._compositeIdShapes = (readable.identifications ?? []).filter((id) => id.fields.length >= 2);
884
+ const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
885
+ this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
789
886
  }
790
887
  _buildGates() {
791
888
  const meta = this.readable.type.metadata;
@@ -853,16 +950,169 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
853
950
  transformProjection(projection) {
854
951
  return projection;
855
952
  }
953
+ widenPreferredIdProjection(projection) {
954
+ if (this._preferredIdSet.size === 0 || projection === void 0) return projection;
955
+ if (Array.isArray(projection)) return this._widenArrayProjection(projection);
956
+ return this._widenMapProjection(projection);
957
+ }
958
+ _widenArrayProjection(projection) {
959
+ const stringItems = /* @__PURE__ */ new Set();
960
+ for (const item of projection) if (typeof item === "string") stringItems.add(item);
961
+ let allPresent = true;
962
+ for (const field of this._preferredIdSet) if (!stringItems.has(field)) {
963
+ allPresent = false;
964
+ break;
965
+ }
966
+ if (allPresent) return projection;
967
+ const out = [...projection];
968
+ for (const field of this._preferredIdSet) if (!stringItems.has(field)) out.push(field);
969
+ return out;
970
+ }
971
+ _widenMapProjection(projection) {
972
+ const entries = Object.entries(projection);
973
+ if (entries.length === 0) return projection;
974
+ const included = /* @__PURE__ */ new Set();
975
+ const excluded = /* @__PURE__ */ new Set();
976
+ for (const [k, v] of entries) if (v === 1 || v === true) included.add(k);
977
+ else if (v === 0 || v === false) excluded.add(k);
978
+ if (included.size > 0 && excluded.size > 0) return new _moostjs_event_http.HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
979
+ if (excluded.size === 0) {
980
+ let allPresent = true;
981
+ for (const field of this._preferredIdSet) if (!included.has(field)) {
982
+ allPresent = false;
983
+ break;
984
+ }
985
+ if (allPresent) return projection;
986
+ const widened = {};
987
+ for (const k of included) widened[k] = 1;
988
+ for (const field of this._preferredIdSet) widened[field] = 1;
989
+ return widened;
990
+ }
991
+ const widened = {};
992
+ for (const fd of this.readable.fieldDescriptors) if (!fd.ignored && !excluded.has(fd.path)) widened[fd.path] = 1;
993
+ for (const field of this._preferredIdSet) widened[field] = 1;
994
+ return widened;
995
+ }
996
+ /** WHY: the URL parser only auto-coerces `$count`; every other boolean control reaches us as `"true"`/`"1"` and would fail DTO validation. */
997
+ _coerceActionsControl(controls) {
998
+ const v = controls.$actions;
999
+ if (typeof v === "string") controls.$actions = v === "true" || v === "1" || v === "";
1000
+ }
1001
+ /** Normalize a post-`widenPreferredIdProjection` $select into `string[] | null` (`null` = all fields). */
1002
+ _resolveProjectionForAugmenter(select) {
1003
+ if (select === void 0) return null;
1004
+ if (Array.isArray(select)) {
1005
+ const out = [];
1006
+ const seen = /* @__PURE__ */ new Set();
1007
+ for (const item of select) if (typeof item === "string" && !seen.has(item)) {
1008
+ seen.add(item);
1009
+ out.push(item);
1010
+ }
1011
+ return out;
1012
+ }
1013
+ const obj = select;
1014
+ const included = [];
1015
+ const excluded = [];
1016
+ for (const [k, v] of Object.entries(obj)) if (v === 1 || v === true) included.push(k);
1017
+ else if (v === 0 || v === false) excluded.push(k);
1018
+ if (included.length > 0 && excluded.length === 0) return included;
1019
+ if (excluded.length > 0 && included.length === 0) {
1020
+ const excludedSet = new Set(excluded);
1021
+ const out = [];
1022
+ for (const fd of this.readable.fieldDescriptors) if (!fd.ignored && !excludedSet.has(fd.path)) out.push(fd.path);
1023
+ return out;
1024
+ }
1025
+ throw new _moostjs_event_http.HttpError(500, "[moost-db] mixed inclusion/exclusion projection reached augmenter; widenPreferredIdProjection should have rejected it");
1026
+ }
1027
+ /** WHY: filter row/rows envelopes by the per-request `applyMetaOverlay` action set; skip `meta()` when overlay is identity. */
1028
+ async _resolveAugmentEnvelopes() {
1029
+ const rowLevelEnvelopes = discoverRowLevelActions(this.constructor, this.app, this.logger);
1030
+ if (rowLevelEnvelopes.length === 0) return null;
1031
+ if (this._overlayIsNoOp) return rowLevelEnvelopes;
1032
+ const overlayMeta = await this.meta();
1033
+ const allowedNames = new Set(overlayMeta.actions.map((a) => a.name));
1034
+ const filtered = rowLevelEnvelopes.filter((e) => allowedNames.has(e.info.name));
1035
+ return filtered.length === 0 ? null : filtered;
1036
+ }
1037
+ /** Returns a widened `$select` only when at least one `requiredFields` entry is missing; `null` means "no widening needed". */
1038
+ _widenSelectForActions(envelopes, baseSelect) {
1039
+ let resultSet = null;
1040
+ let result = null;
1041
+ for (const e of envelopes) {
1042
+ const raw = e.raw;
1043
+ if (!Array.isArray(raw.requiredFields)) continue;
1044
+ for (const f of raw.requiredFields) {
1045
+ if (resultSet ? resultSet.has(f) : baseSelect.includes(f)) continue;
1046
+ if (resultSet === null) {
1047
+ resultSet = new Set(baseSelect);
1048
+ result = [...baseSelect];
1049
+ }
1050
+ resultSet.add(f);
1051
+ result.push(f);
1052
+ }
1053
+ }
1054
+ return result;
1055
+ }
1056
+ async _prepareAugmentation(controls, select) {
1057
+ if (!controls.$actions) return null;
1058
+ const envelopes = await this._resolveAugmentEnvelopes();
1059
+ if (envelopes === null) return null;
1060
+ const resolvedProjection = this._resolveProjectionForAugmenter(select);
1061
+ return {
1062
+ envelopes,
1063
+ resolvedProjection,
1064
+ widenedSelect: resolvedProjection === null ? null : this._widenSelectForActions(envelopes, resolvedProjection)
1065
+ };
1066
+ }
1067
+ async _resolveReadStrategy(controls) {
1068
+ const searchTerm = controls.$search;
1069
+ const indexName = controls.$index;
1070
+ const vectorField = controls.$vector;
1071
+ if (vectorField !== void 0 && searchTerm) return {
1072
+ kind: "vector",
1073
+ vector: await this.computeEmbedding(searchTerm, vectorField || void 0),
1074
+ vectorField
1075
+ };
1076
+ if (searchTerm && this.readable.isSearchable()) return {
1077
+ kind: "search",
1078
+ term: searchTerm,
1079
+ index: indexName
1080
+ };
1081
+ return { kind: "plain" };
1082
+ }
1083
+ /**
1084
+ * Shared `query` / `pages` pipeline: prepare actions augmentation + read
1085
+ * strategy in parallel, pre-widen $select for `requiredFields`, run
1086
+ * `exec`, and augment `result.data` with `$actions` when the request set
1087
+ * `$actions=true`. Caller dispatches the strategy to its read-method
1088
+ * family (count vs no-count).
1089
+ */
1090
+ async _runReadWithActions(queryObj, controls, select, exec) {
1091
+ const [prep, strategy] = await Promise.all([this._prepareAugmentation(controls, select), this._resolveReadStrategy(controls)]);
1092
+ const result = await exec(prep?.widenedSelect ? {
1093
+ ...queryObj,
1094
+ controls: {
1095
+ ...queryObj.controls,
1096
+ $select: prep.widenedSelect
1097
+ }
1098
+ } : queryObj, strategy);
1099
+ if (!prep) return result;
1100
+ result.data = augmentRowsWithActions({
1101
+ envelopes: prep.envelopes,
1102
+ rows: result.data,
1103
+ resolvedProjection: prep.resolvedProjection
1104
+ });
1105
+ return result;
1106
+ }
856
1107
  /**
857
1108
  * Extracts a composite identifier object from query params.
858
1109
  * Tries composite primary key first, then compound unique indexes.
859
1110
  */
860
1111
  extractCompositeId(query) {
861
- const pkFields = this.readable.primaryKeys;
862
- if (pkFields.length > 1) {
1112
+ for (const id of this._compositeIdShapes) {
863
1113
  const idObj = {};
864
1114
  let allPresent = true;
865
- for (const field of pkFields) {
1115
+ for (const field of id.fields) {
866
1116
  if (query[field] === void 0) {
867
1117
  allPresent = false;
868
1118
  break;
@@ -871,19 +1121,6 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
871
1121
  }
872
1122
  if (allPresent) return idObj;
873
1123
  }
874
- for (const index of this.readable.indexes.values()) {
875
- if (index.type !== "unique" || index.fields.length < 2) continue;
876
- const idObj = {};
877
- let allPresent = true;
878
- for (const indexField of index.fields) {
879
- if (query[indexField.name] === void 0) {
880
- allPresent = false;
881
- break;
882
- }
883
- idObj[indexField.name] = query[indexField.name];
884
- }
885
- if (allPresent) return idObj;
886
- }
887
1124
  return new _moostjs_event_http.HttpError(400, "Query params do not match any composite primary key or compound unique index");
888
1125
  }
889
1126
  /**
@@ -892,6 +1129,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
892
1129
  async query(url) {
893
1130
  const parsed = this.parseQueryString(url);
894
1131
  const controls = parsed.controls;
1132
+ this._coerceActionsControl(controls);
895
1133
  if (controls.$groupBy?.length) {
896
1134
  if (controls.$with?.length) return new _moostjs_event_http.HttpError(400, "Cannot combine $with and $groupBy in the same query");
897
1135
  if (parsed.insights) {
@@ -909,17 +1147,16 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
909
1147
  if (error) return error;
910
1148
  const gateError = this.checkGates(parsed.filter, controls, this._gates);
911
1149
  if (gateError) return gateError;
912
- const [filter, select] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
1150
+ const [filter, rawSelect] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
913
1151
  if (controls.$count) return this.readable.count({
914
1152
  filter,
915
1153
  controls: {
916
1154
  ...controls,
917
- $select: select
1155
+ $select: rawSelect
918
1156
  }
919
1157
  });
920
- const searchTerm = controls.$search;
921
- const indexName = controls.$index;
922
- const vectorField = controls.$vector;
1158
+ const select = this.widenPreferredIdProjection(rawSelect);
1159
+ if (select instanceof _moostjs_event_http.HttpError) return select;
923
1160
  const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
924
1161
  const queryObj = {
925
1162
  filter,
@@ -930,19 +1167,20 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
930
1167
  $threshold: threshold
931
1168
  }
932
1169
  };
933
- if (vectorField !== void 0 && searchTerm) {
934
- const vector = await this.computeEmbedding(searchTerm, vectorField || void 0);
935
- if (vectorField) return this.readable.vectorSearch(vectorField, vector, queryObj);
936
- return this.readable.vectorSearch(vector, queryObj);
937
- }
938
- if (searchTerm && this.readable.isSearchable()) return this.readable.search(searchTerm, queryObj, indexName);
939
- return this.readable.findMany(queryObj);
1170
+ return (await this._runReadWithActions(queryObj, controls, select, async (q, strategy) => {
1171
+ switch (strategy.kind) {
1172
+ case "vector": return { data: await (strategy.vectorField ? this.readable.vectorSearch(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearch(strategy.vector, q)) };
1173
+ case "search": return { data: await this.readable.search(strategy.term, q, strategy.index) };
1174
+ case "plain": return { data: await this.readable.findMany(q) };
1175
+ }
1176
+ })).data;
940
1177
  }
941
1178
  /**
942
1179
  * **GET /pages** — returns paginated records with metadata.
943
1180
  */
944
1181
  async pages(url) {
945
1182
  const parsed = this.parseQueryString(url);
1183
+ this._coerceActionsControl(parsed.controls);
946
1184
  const error = this.validateParsed(parsed, "pages");
947
1185
  if (error) return error;
948
1186
  const controls = parsed.controls;
@@ -951,10 +1189,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
951
1189
  const page = Math.max(Number(controls.$page || 1), 1);
952
1190
  const size = Math.max(Number(controls.$size || 10), 1);
953
1191
  const skip = (page - 1) * size;
954
- const [filter, select] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
955
- const searchTerm = controls.$search;
956
- const indexName = controls.$index;
957
- const vectorField = controls.$vector;
1192
+ const [filter, rawSelect] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
1193
+ const select = this.widenPreferredIdProjection(rawSelect);
1194
+ if (select instanceof _moostjs_event_http.HttpError) return select;
958
1195
  const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
959
1196
  const query = {
960
1197
  filter,
@@ -966,13 +1203,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
966
1203
  $threshold: threshold
967
1204
  }
968
1205
  };
969
- let result;
970
- if (vectorField !== void 0 && searchTerm) {
971
- const vector = await this.computeEmbedding(searchTerm, vectorField || void 0);
972
- if (vectorField) result = await this.readable.vectorSearchWithCount(vectorField, vector, query);
973
- else result = await this.readable.vectorSearchWithCount(vector, query);
974
- } else if (searchTerm && this.readable.isSearchable()) result = await this.readable.searchWithCount(searchTerm, query, indexName);
975
- else result = await this.readable.findManyWithCount(query);
1206
+ const result = await this._runReadWithActions(query, controls, select, async (q, strategy) => {
1207
+ switch (strategy.kind) {
1208
+ case "vector": return strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q);
1209
+ case "search": return this.readable.searchWithCount(strategy.term, q, strategy.index);
1210
+ case "plain": return this.readable.findManyWithCount(q);
1211
+ }
1212
+ });
976
1213
  return {
977
1214
  data: result.data,
978
1215
  page,
@@ -986,15 +1223,14 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
986
1223
  */
987
1224
  async getOne(id, url) {
988
1225
  const parsed = this.parseQueryString(url);
1226
+ this._coerceActionsControl(parsed.controls);
989
1227
  if (Object.keys(parsed.filter).length > 0) return new _moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
990
1228
  const error = this.validateParsed(parsed, "getOne");
991
1229
  if (error) return error;
992
- const select = await this.transformProjection(parsed.controls.$select);
993
- const controls = {
994
- ...parsed.controls,
995
- $select: select
996
- };
997
- return this.returnOne(this.readable.findById(id, { controls }));
1230
+ const rawSelect = await this.transformProjection(parsed.controls.$select);
1231
+ const select = this.widenPreferredIdProjection(rawSelect);
1232
+ if (select instanceof _moostjs_event_http.HttpError) return select;
1233
+ return this._findByIdAndAugment(id, parsed.controls, select);
998
1234
  }
999
1235
  /**
1000
1236
  * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
@@ -1004,12 +1240,29 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1004
1240
  const idObj = this.extractCompositeId(query);
1005
1241
  if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
1006
1242
  const parsed = this.parseQueryString(url);
1007
- const select = await this.transformProjection(parsed.controls.$select);
1243
+ this._coerceActionsControl(parsed.controls);
1244
+ const rawSelect = await this.transformProjection(parsed.controls.$select);
1245
+ const select = this.widenPreferredIdProjection(rawSelect);
1246
+ if (select instanceof _moostjs_event_http.HttpError) return select;
1247
+ return this._findByIdAndAugment(idObj, parsed.controls, select);
1248
+ }
1249
+ async _findByIdAndAugment(id, parsedControls, select) {
1250
+ const prep = await this._prepareAugmentation(parsedControls, select);
1251
+ const initialSelect = prep?.widenedSelect ?? select;
1008
1252
  const controls = {
1009
- ...parsed.controls,
1010
- $select: select
1253
+ ...parsedControls,
1254
+ $select: initialSelect
1011
1255
  };
1012
- return this.returnOne(this.readable.findById(idObj, { controls }));
1256
+ const row = await this.readable.findById(id, { controls });
1257
+ const item = await this.returnOne(Promise.resolve(row));
1258
+ if (item instanceof _moostjs_event_http.HttpError) return item;
1259
+ if (!prep) return item;
1260
+ const [augmented] = augmentRowsWithActions({
1261
+ envelopes: prep.envelopes,
1262
+ rows: [item],
1263
+ resolvedProjection: prep.resolvedProjection
1264
+ });
1265
+ return augmented;
1013
1266
  }
1014
1267
  /**
1015
1268
  * **GET /meta** — returns table/view metadata for UI.
@@ -1043,6 +1296,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1043
1296
  vectorSearchable: this.readable.isVectorSearchable(),
1044
1297
  searchIndexes: this.readable.getSearchIndexes(),
1045
1298
  primaryKeys: [...this.readable.primaryKeys],
1299
+ preferredId: [...this.readable.preferredId],
1046
1300
  relations,
1047
1301
  fields,
1048
1302
  type: this.getSerializedType(),
@@ -1341,6 +1595,7 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
1341
1595
  vectorSearchable: false,
1342
1596
  searchIndexes: [],
1343
1597
  primaryKeys: this.primaryKey ? [this.primaryKey] : [],
1598
+ preferredId: this.primaryKey ? [this.primaryKey] : [],
1344
1599
  relations: [],
1345
1600
  fields,
1346
1601
  type: this.getSerializedType(),
@@ -1555,8 +1810,8 @@ function applySelect(rows, select) {
1555
1810
  }
1556
1811
  //#endregion
1557
1812
  //#region src/actions/action-disabled-error.ts
1558
- function buildMessage(action, pks) {
1559
- if (pks !== void 0) return `Action "${action}" is disabled for ${pks.length} of the selected rows`;
1813
+ function buildMessage(action, ids) {
1814
+ if (ids !== void 0) return `Action "${action}" is disabled for ${ids.length} of the selected rows`;
1560
1815
  return `Action "${action}" is disabled for this row`;
1561
1816
  }
1562
1817
  /**
@@ -1564,95 +1819,110 @@ function buildMessage(action, pks) {
1564
1819
  * with Moost's existing error mapper to produce HTTP 409 with the wire body
1565
1820
  * defined by {@link ActionDisabledErrorBody}.
1566
1821
  *
1567
- * - `'row'`-level rejection: pass `(action, pk)` — the body emits `pk`.
1568
- * - `'rows'`-level rejection: pass `(action, undefined, pks)` — the body
1569
- * emits `pks` (the FULL list of failing PKs in reject mode; the FULL list
1570
- * of request PKs in skip mode with zero survivors).
1822
+ * - `'row'`-level rejection: pass `(action, id)` — the body emits `id`.
1823
+ * - `'rows'`-level rejection: pass `(action, undefined, ids)` — the body
1824
+ * emits `ids` (the FULL list of failing IDs in reject mode; the FULL list
1825
+ * of request IDs in skip mode with zero survivors).
1571
1826
  */
1572
1827
  var ActionDisabledError = class extends _moostjs_event_http.HttpError {
1573
1828
  name = "ActionDisabledError";
1574
- constructor(action, pk, pks) {
1829
+ constructor(action, id, ids) {
1575
1830
  const body = {
1576
1831
  name: "ActionDisabledError",
1577
- message: buildMessage(action, pks),
1832
+ message: buildMessage(action, ids),
1578
1833
  statusCode: 409,
1579
1834
  action
1580
1835
  };
1581
- if (pks !== void 0) body.pks = pks;
1582
- else if (pk !== void 0) body.pk = pk;
1836
+ if (ids !== void 0) body.ids = ids;
1837
+ else if (id !== void 0) body.id = id;
1583
1838
  super(409, body);
1584
1839
  }
1585
1840
  };
1586
1841
  //#endregion
1587
- //#region src/actions/pk-validation.ts
1588
- function isPkValidationSource(value) {
1842
+ //#region src/actions/current-action.ts
1843
+ /** Read the current action's `TDbActionMeta` from the wook context. Returns undefined outside a controller (e.g. direct-wook test paths). */
1844
+ function readCurrentActionMeta(ctx) {
1845
+ let ctrl;
1846
+ let methodName;
1847
+ try {
1848
+ const cc = (0, moost.useControllerContext)(ctx);
1849
+ ctrl = cc.getController();
1850
+ methodName = cc.getMethod();
1851
+ } catch {
1852
+ return;
1853
+ }
1854
+ if (!ctrl || !methodName) return void 0;
1855
+ return (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
1856
+ }
1857
+ //#endregion
1858
+ //#region src/actions/id-validation.ts
1859
+ const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
1860
+ function getSourceCache(source) {
1861
+ let cache = SOURCE_CACHE.get(source);
1862
+ if (cache) return cache;
1863
+ const identifications = source.getIdentifications();
1864
+ const byKeySig = /* @__PURE__ */ new Map();
1865
+ for (const ident of identifications) byKeySig.set(fieldsSig(ident.fields), ident);
1866
+ const fieldByName = /* @__PURE__ */ new Map();
1867
+ for (const fd of source.fieldDescriptors) fieldByName.set(fd.path, fd);
1868
+ cache = {
1869
+ byKeySig,
1870
+ fieldByName,
1871
+ formatted: identifications.map((id) => `[${id.fields.join(", ")}]`).join(", ")
1872
+ };
1873
+ SOURCE_CACHE.set(source, cache);
1874
+ return cache;
1875
+ }
1876
+ function fieldsSig(fields) {
1877
+ return fields.toSorted().join("");
1878
+ }
1879
+ function isIdValidationSource(value) {
1589
1880
  if (!value || typeof value !== "object") return false;
1590
1881
  const v = value;
1591
- return Array.isArray(v.primaryKeys) && Array.isArray(v.fieldDescriptors);
1882
+ return typeof v.getIdentifications === "function" && Array.isArray(v.fieldDescriptors);
1592
1883
  }
1593
- /**
1594
- * Validate a JSON-decoded body against a single-row PK shape (scalar or
1595
- * composite). Throws {@link ValidatorError} with structured `errors` so the
1596
- * existing validation interceptor returns HTTP 400.
1597
- */
1598
- function validateSinglePk(body, source, path = "") {
1599
- const errors = collectPkErrors(body, source, path);
1884
+ function validateSingleId(body, source, path = "") {
1885
+ const errors = collectIdErrors(body, source, path);
1600
1886
  if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
1887
+ return body;
1601
1888
  }
1602
- /**
1603
- * Validate a JSON-decoded body against an array of PK shapes (`@DbActionPKs`).
1604
- * The body MUST be an array; each element is validated against the PK schema.
1605
- */
1606
- function validateMultiPk(body, source) {
1889
+ function validateMultiId(body, source) {
1607
1890
  if (!Array.isArray(body)) throw new _atscript_typescript_utils.ValidatorError([{
1608
1891
  path: "",
1609
- message: "Expected JSON array of primary keys",
1892
+ message: "Expected JSON array of identifier objects",
1610
1893
  details: []
1611
1894
  }]);
1612
1895
  const errors = [];
1613
- for (let i = 0; i < body.length; i++) errors.push(...collectPkErrors(body[i], source, `[${i}]`));
1896
+ for (let i = 0; i < body.length; i++) errors.push(...collectIdErrors(body[i], source, `[${i}]`));
1614
1897
  if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
1898
+ return body;
1615
1899
  }
1616
- function collectPkErrors(value, source, pathPrefix) {
1617
- const pkFields = source.primaryKeys;
1618
- if (pkFields.length === 0) return [{
1900
+ function collectIdErrors(value, source, pathPrefix) {
1901
+ if (!isPlainObject(value)) return [{
1902
+ path: pathPrefix,
1903
+ message: "Expected JSON object for row identifier",
1904
+ details: []
1905
+ }];
1906
+ const cache = getSourceCache(source);
1907
+ if (cache.byKeySig.size === 0) return [{
1908
+ path: pathPrefix,
1909
+ message: "Table has no identifier configured",
1910
+ details: []
1911
+ }];
1912
+ const match = cache.byKeySig.get(fieldsSig(Object.keys(value)));
1913
+ if (!match) return [{
1619
1914
  path: pathPrefix,
1620
- message: "Table has no primary key configured",
1915
+ message: `Identifier fields must exactly match one of: ${cache.formatted}`,
1621
1916
  details: []
1622
1917
  }];
1623
1918
  const errors = [];
1624
- if (pkFields.length === 1) {
1625
- const err = checkScalar(value, findFieldDescriptor(source, pkFields[0]), pathPrefix);
1626
- if (err) errors.push(err);
1627
- return errors;
1628
- }
1629
- if (!isPlainObject(value)) {
1630
- errors.push({
1631
- path: pathPrefix,
1632
- message: "Expected JSON object for composite primary key",
1633
- details: []
1634
- });
1635
- return errors;
1636
- }
1637
- for (const fieldName of pkFields) {
1919
+ for (const fieldName of match.fields) {
1638
1920
  const sub = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
1639
- if (!(fieldName in value)) {
1640
- errors.push({
1641
- path: sub,
1642
- message: `Missing primary-key field "${fieldName}"`,
1643
- details: []
1644
- });
1645
- continue;
1646
- }
1647
- const fd = findFieldDescriptor(source, fieldName);
1648
- const err = checkScalar(value[fieldName], fd, sub);
1921
+ const err = checkScalar(value[fieldName], cache.fieldByName.get(fieldName), sub);
1649
1922
  if (err) errors.push(err);
1650
1923
  }
1651
1924
  return errors;
1652
1925
  }
1653
- function findFieldDescriptor(source, name) {
1654
- for (const fd of source.fieldDescriptors) if (fd.path === name) return fd;
1655
- }
1656
1926
  function checkScalar(value, fd, path) {
1657
1927
  const expected = fd?.designType ?? "string";
1658
1928
  if (expected === "string" && typeof value !== "string") return scalarMismatch(path, expected, value);
@@ -1662,7 +1932,7 @@ function checkScalar(value, fd, path) {
1662
1932
  function scalarMismatch(path, expected, value) {
1663
1933
  return {
1664
1934
  path,
1665
- message: `Expected primary-key value to be ${expected}, got ${describe(value)}`,
1935
+ message: `Expected identifier value to be ${expected}, got ${describe(value)}`,
1666
1936
  details: []
1667
1937
  };
1668
1938
  }
@@ -1675,105 +1945,115 @@ function isPlainObject(value) {
1675
1945
  return typeof value === "object" && value !== null && !Array.isArray(value);
1676
1946
  }
1677
1947
  //#endregion
1678
- //#region src/actions/pk-cache.ts
1948
+ //#region src/actions/id-cache.ts
1679
1949
  const boundTableKey = (0, _wooksjs_event_core.key)("atscript_db_action_bound_table");
1680
1950
  function getActionTable(ctx) {
1681
- if (ctx.has(boundTableKey)) {
1682
- const fromSlot = ctx.get(boundTableKey);
1683
- if (fromSlot) return fromSlot;
1684
- }
1951
+ const fromSlot = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
1952
+ if (fromSlot) return fromSlot;
1685
1953
  const ctrl = (0, moost.useControllerContext)(ctx).getController();
1686
- if (ctrl) {
1687
- const t = ctrl.readable ?? ctrl.table;
1688
- if (t) return t;
1689
- }
1690
- return null;
1954
+ return ctrl?.readable ?? ctrl?.table ?? null;
1691
1955
  }
1692
1956
  function noTableError(ctx) {
1693
- const cc = (0, moost.useControllerContext)(ctx);
1694
- const ctrl = cc.getController();
1695
- const methodName = cc.getMethod();
1696
- let actionName;
1697
- if (ctrl && methodName) actionName = (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION]?.name;
1957
+ const actionName = readCurrentActionMeta(ctx)?.name;
1698
1958
  return new _moostjs_event_http.HttpError(500, `${WARN_PREFIX} ${actionName ? `"${actionName}"` : "<unknown>"}: controller has no readable/table property and the action declares no opts.table. Either expose readable/table on the controller, extend AsDbReadableController, or pass opts.table on @DbAction.`);
1699
1959
  }
1700
- async function resolveValidatedPk(ctx, validate) {
1960
+ async function resolveValidatedId(ctx, validate) {
1701
1961
  const table = getActionTable(ctx);
1702
- if (!isPkValidationSource(table)) throw noTableError(ctx);
1962
+ if (!isIdValidationSource(table)) throw noTableError(ctx);
1703
1963
  const body = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
1704
1964
  validate(body, table);
1705
1965
  return body;
1706
1966
  }
1707
- const dbActionPkSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedPk(ctx, validateSinglePk));
1708
- const dbActionPksSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
1709
- return await resolveValidatedPk(ctx, validateMultiPk);
1967
+ const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
1968
+ const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
1969
+ return await resolveValidatedId(ctx, validateMultiId);
1710
1970
  });
1711
- const useDbActionPk = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionPkSlot) }));
1712
- const useDbActionPks = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionPksSlot) }));
1971
+ const useDbActionId = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdSlot) }));
1972
+ const useDbActionIds = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdsSlot) }));
1713
1973
  //#endregion
1714
1974
  //#region src/actions/row-cache.ts
1715
1975
  function asFetchTable(value) {
1716
1976
  if (!value || typeof value !== "object") return null;
1717
1977
  const v = value;
1718
- return Array.isArray(v.primaryKeys) && typeof v.findById === "function" && typeof v.findMany === "function" ? v : null;
1978
+ if (Array.isArray(v.primaryKeys) && typeof v.findOne === "function" && typeof v.findMany === "function") return v;
1979
+ return null;
1980
+ }
1981
+ function stringifyScalar(value) {
1982
+ if (value === null) return "null";
1983
+ if (value === void 0) return "undefined";
1984
+ return String(value);
1985
+ }
1986
+ /** Returns the action's `requiredFields` or null when called outside a controller context (e.g. direct wook usage in tests). */
1987
+ function readActionFieldSet(ctx) {
1988
+ const action = readCurrentActionMeta(ctx);
1989
+ if (!action) return null;
1990
+ const opts = action.opts;
1991
+ return Array.isArray(opts.requiredFields) ? opts.requiredFields : null;
1719
1992
  }
1720
- function noTable() {
1721
- throw new _moostjs_event_http.HttpError(500, `${WARN_PREFIX} cached row wook: no bound table`);
1993
+ function seedActionFields(ctx, table) {
1994
+ const fields = /* @__PURE__ */ new Set();
1995
+ for (const f of table.preferredId ?? table.primaryKeys) fields.add(f);
1996
+ const action = readActionFieldSet(ctx);
1997
+ if (action) for (const f of action) fields.add(f);
1998
+ return fields;
1722
1999
  }
1723
2000
  async function loadRow(ctx) {
1724
- const pk = await ctx.get(dbActionPkSlot);
1725
- const row = await (asFetchTable(getActionTable(ctx)) ?? noTable()).findById(pk);
1726
- if (row == null) throw new _moostjs_event_http.HttpError(404, "Row not found for action PK");
2001
+ const id = await ctx.get(dbActionIdSlot);
2002
+ const table = asFetchTable(getActionTable(ctx));
2003
+ if (!table) throw noTableError(ctx);
2004
+ const fields = seedActionFields(ctx, table);
2005
+ for (const k of Object.keys(id)) fields.add(k);
2006
+ const row = await table.findOne({
2007
+ filter: id,
2008
+ controls: { $select: [...fields] }
2009
+ });
2010
+ if (row == null) throw new _moostjs_event_http.HttpError(404, "Row not found for action identifier");
1727
2011
  return row;
1728
2012
  }
1729
2013
  async function loadRows(ctx) {
1730
- const pks = await ctx.get(dbActionPksSlot);
1731
- const table = asFetchTable(getActionTable(ctx)) ?? noTable();
1732
- if (pks.length === 0) return [];
1733
- const { primaryKeys } = table;
1734
- const rows = await table.findMany({ filter: buildPksFilter(pks, primaryKeys) });
1735
- if (primaryKeys.length === 1) {
1736
- const field = primaryKeys[0];
1737
- const byKey = /* @__PURE__ */ new Map();
1738
- for (const row of rows) byKey.set(row[field], row);
1739
- const ordered = [];
1740
- for (const pk of pks) {
1741
- const found = byKey.get(pk);
1742
- if (found !== void 0) ordered.push(found);
2014
+ const ids = await ctx.get(dbActionIdsSlot);
2015
+ const table = asFetchTable(getActionTable(ctx));
2016
+ if (!table) throw noTableError(ctx);
2017
+ if (ids.length === 0) return [];
2018
+ const fields = seedActionFields(ctx, table);
2019
+ const idKeys = [];
2020
+ const shapes = /* @__PURE__ */ new Map();
2021
+ const dedupedIds = [];
2022
+ const seenKeys = /* @__PURE__ */ new Set();
2023
+ for (const id of ids) {
2024
+ const sortedFields = Object.keys(id).toSorted();
2025
+ const sig = sortedFields.join("");
2026
+ let key = "";
2027
+ for (const f of sortedFields) {
2028
+ fields.add(f);
2029
+ key += `${f}\x1f${stringifyScalar(id[f])}\x1e`;
2030
+ }
2031
+ idKeys.push(key);
2032
+ if (!shapes.has(sig)) shapes.set(sig, sortedFields);
2033
+ if (!seenKeys.has(key)) {
2034
+ seenKeys.add(key);
2035
+ dedupedIds.push(id);
1743
2036
  }
1744
- return ordered;
1745
- }
1746
- const byKey = /* @__PURE__ */ new Map();
1747
- for (const row of rows) byKey.set(compositeKey(row, primaryKeys), row);
1748
- const ordered = [];
1749
- for (const pk of pks) {
1750
- const found = byKey.get(compositeKey(pk, primaryKeys));
1751
- if (found !== void 0) ordered.push(found);
1752
- }
1753
- return ordered;
1754
- }
1755
- function buildPksFilter(pks, primaryKeys) {
1756
- if (primaryKeys.length === 1) return { [primaryKeys[0]]: { $in: pks } };
1757
- return { $or: pks.map((pk) => {
1758
- const obj = pk;
1759
- const clause = {};
1760
- for (const field of primaryKeys) clause[field] = obj[field];
1761
- return clause;
1762
- }) };
1763
- }
1764
- function compositeKey(obj, primaryKeys) {
1765
- let out = "";
1766
- for (const f of primaryKeys) {
1767
- if (out !== "") out += "\0";
1768
- const v = obj[f];
1769
- if (v === null) out += "n";
1770
- else if (v === void 0) out += "u";
1771
- else if (typeof v === "string") out += `s\x02${v}`;
1772
- else if (typeof v === "number") out += `n\x02${v}`;
1773
- else if (typeof v === "boolean") out += `b\x02${v}`;
1774
- else out += `j\x02${JSON.stringify(v)}`;
1775
2037
  }
1776
- return out;
2038
+ const rows = await table.findMany({
2039
+ filter: { $or: dedupedIds },
2040
+ controls: { $select: [...fields] }
2041
+ });
2042
+ const rowByKey = /* @__PURE__ */ new Map();
2043
+ for (const row of rows) for (const sortedFields of shapes.values()) {
2044
+ let key = "";
2045
+ let ok = true;
2046
+ for (const f of sortedFields) {
2047
+ const v = row[f];
2048
+ if (v === void 0) {
2049
+ ok = false;
2050
+ break;
2051
+ }
2052
+ key += `${f}\x1f${stringifyScalar(v)}\x1e`;
2053
+ }
2054
+ if (ok && !rowByKey.has(key)) rowByKey.set(key, row);
2055
+ }
2056
+ return ids.map((_, i) => rowByKey.get(idKeys[i]));
1777
2057
  }
1778
2058
  const dbActionRowSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRow(ctx));
1779
2059
  const dbActionRowsSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRows(ctx));
@@ -1798,28 +2078,38 @@ function buildGateInterceptor(opts) {
1798
2078
  injectBoundTable(table);
1799
2079
  const ctx = (0, _wooksjs_event_core.current)();
1800
2080
  if (level === "row") {
1801
- if (disabled(await ctx.get(dbActionRowSlot))) throw new ActionDisabledError(action, await ctx.get(dbActionPkSlot));
2081
+ const verdicts = disabled([await ctx.get(dbActionRowSlot)]);
2082
+ assertVerdictLength(action, verdicts, 1);
2083
+ if (verdicts[0]) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot));
1802
2084
  return;
1803
2085
  }
1804
- const pks = await ctx.get(dbActionPksSlot);
2086
+ const ids = await ctx.get(dbActionIdsSlot);
1805
2087
  const rows = await ctx.get(dbActionRowsSlot);
1806
- const failingPks = [];
2088
+ const existingRows = [];
2089
+ for (const row of rows) if (row !== void 0) existingRows.push(row);
2090
+ const verdicts = disabled(existingRows);
2091
+ assertVerdictLength(action, verdicts, existingRows.length);
2092
+ const failingIds = [];
1807
2093
  const passingRows = [];
1808
- const passingPks = [];
1809
- for (let i = 0; i < rows.length; i++) if (disabled(rows[i])) failingPks.push(pks[i]);
1810
- else {
1811
- passingRows.push(rows[i]);
1812
- passingPks.push(pks[i]);
2094
+ const passingIds = [];
2095
+ let verdictIndex = 0;
2096
+ for (let i = 0; i < ids.length; i++) {
2097
+ const row = rows[i];
2098
+ if (row === void 0 || verdicts[verdictIndex++]) failingIds.push(ids[i]);
2099
+ else {
2100
+ passingRows.push(row);
2101
+ passingIds.push(ids[i]);
2102
+ }
1813
2103
  }
1814
2104
  if (onDisabledRows === "skip") {
1815
- if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...pks]);
1816
- if (failingPks.length > 0) {
2105
+ if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...ids]);
2106
+ if (failingIds.length > 0) {
1817
2107
  ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
1818
- ctx.set(dbActionPksSlot, Promise.resolve(passingPks));
2108
+ ctx.set(dbActionIdsSlot, Promise.resolve(passingIds));
1819
2109
  }
1820
2110
  return;
1821
2111
  }
1822
- if (failingPks.length > 0) throw new ActionDisabledError(action, void 0, failingPks);
2112
+ if (failingIds.length > 0) throw new ActionDisabledError(action, void 0, failingIds);
1823
2113
  }, GATE_PRIORITY);
1824
2114
  }
1825
2115
  /** Thin interceptor for `@DbActionRow*` without `disabled` — injects only the bound table. */
@@ -1837,6 +2127,11 @@ function buildThinInterceptor(opts) {
1837
2127
  * (gate when `disabled` is set, thin bound-table injector when only
1838
2128
  * `@DbActionRow*` is present). Stacking two `@DbAction` on the same method
1839
2129
  * is undefined and emits a warning.
2130
+ *
2131
+ * Generic over `TRow` (annotate at the call site: `@DbAction<Order>(...)`)
2132
+ * and `R` (the literal `requiredFields` tuple, inferred via `const R`).
2133
+ * The `disabled` predicate's `rows` argument is type-narrowed to
2134
+ * `Pick<FlatOf<TRow>, R[number]>[]`.
1840
2135
  */
1841
2136
  function DbAction(name, opts = {}) {
1842
2137
  const mate = (0, moost.getMoostMate)();
@@ -1855,14 +2150,15 @@ function DbAction(name, opts = {}) {
1855
2150
  })(target, propertyKey, descriptor);
1856
2151
  if (isAsValueHelpControllerSubclass(typeof target === "function" ? target : target.constructor)) return descriptor;
1857
2152
  const scan = scanParamLevel(mate.read(target, propertyKey)?.params ?? []);
1858
- if (!!opts.disabled && (scan.level === "row" || scan.level === "rows")) (0, moost.Intercept)(buildGateInterceptor({
2153
+ const rawOpts = opts;
2154
+ if (typeof rawOpts.disabled === "function" && (scan.level === "row" || scan.level === "rows")) (0, moost.Intercept)(buildGateInterceptor({
1859
2155
  action: name,
1860
2156
  level: scan.level,
1861
- disabled: opts.disabled,
1862
- onDisabledRows: opts.onDisabledRows ?? "reject",
1863
- table: opts.table
2157
+ disabled: rawOpts.disabled,
2158
+ onDisabledRows: rawOpts.onDisabledRows ?? "reject",
2159
+ table: rawOpts.table
1864
2160
  }))(target, propertyKey, descriptor);
1865
- else if (scan.hasRowParam) (0, moost.Intercept)(buildThinInterceptor({ table: opts.table }))(target, propertyKey, descriptor);
2161
+ else if (scan.hasRowParam) (0, moost.Intercept)(buildThinInterceptor({ table: rawOpts.table }))(target, propertyKey, descriptor);
1866
2162
  return descriptor;
1867
2163
  });
1868
2164
  }
@@ -1882,62 +2178,70 @@ function DbActionDefault() {
1882
2178
  });
1883
2179
  }
1884
2180
  //#endregion
1885
- //#region src/actions/pk-source.ts
2181
+ //#region src/actions/id-source.ts
1886
2182
  /**
1887
- * Build a parameter decorator that reads its value from the cached PK wook
2183
+ * Build a parameter decorator that reads its value from the cached ID wook
1888
2184
  * (single or multi). Validation runs inside the wook factory exactly once
1889
- * per request, regardless of how many readers consume the value (`@DbActionPK*`
2185
+ * per request, regardless of how many readers consume the value (`@DbActionID*`
1890
2186
  * resolver, gate interceptor, cached row wook, in-handler composables).
1891
2187
  *
1892
2188
  * Marks the param so {@link discoverActions} can infer the action's `level`.
1893
2189
  */
1894
- function createPkParamDecorator(kind) {
2190
+ function createIdParamDecorator(kind) {
1895
2191
  const mate = (0, moost.getMoostMate)();
1896
- const slot = kind === "pk" ? dbActionPkSlot : dbActionPksSlot;
1897
- const resolverName = kind === "pk" ? "dbActionPk" : "dbActionPks";
1898
- return (0, moost.ApplyDecorators)(mate.decorate(MOOST_DB_ACTION_PARAM, kind), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(slot), resolverName));
2192
+ const resolverName = kind === "id" ? "dbActionId" : "dbActionIds";
2193
+ const resolver = kind === "id" ? async () => (0, _wooksjs_event_core.current)().get(dbActionIdSlot) : async () => (0, _wooksjs_event_core.current)().get(dbActionIdsSlot);
2194
+ return (0, moost.ApplyDecorators)(mate.decorate(MOOST_DB_ACTION_PARAM, kind), (0, moost.Resolve)(resolver, resolverName));
1899
2195
  }
1900
2196
  //#endregion
1901
- //#region src/actions/db-action-pk.decorator.ts
2197
+ //#region src/actions/db-action-id.decorator.ts
1902
2198
  /**
1903
- * Parameter resolver that reads the primary key from the JSON request body
1904
- * and validates it against the bound table's PK schema.
2199
+ * Parameter resolver that reads a row identifier from the JSON request body
2200
+ * and validates it against the bound table's legitimate identifiers.
2201
+ *
2202
+ * Body shape is always a JSON object — no scalar form. The object's key set
2203
+ * MUST exactly match one of the table's legitimate identifications:
1905
2204
  *
1906
- * - Single-field PK → JSON-encoded scalar (`"abc"`, `42`, `true`).
1907
- * - Composite PK → JSON object with all PK fields.
2205
+ * - Single-field PK → `{ id: "abc" }` (or whatever the PK prop is named).
2206
+ * - Composite PK → `{ tenantId: "...", userId: "..." }`.
2207
+ * - Single-field unique index → `{ slug: "alpha" }`.
2208
+ * - Compound unique index → `{ tenantId: "...", slug: "..." }`.
1908
2209
  *
1909
- * Validation is strict no type coercion. Mismatches throw a
2210
+ * Strict unknown fields are rejected, no type coercion. Mismatches throw a
1910
2211
  * `ValidatorError` which the existing validation interceptor surfaces as
1911
2212
  * HTTP 400 with the same envelope as DTO failures.
1912
2213
  *
1913
2214
  * Marks the param so {@link discoverActions} can infer the action's `level`
1914
2215
  * as `'row'`.
1915
2216
  *
1916
- * Implementation note: the resolver is a thin reader of the cached PK wook
2217
+ * Implementation note: the resolver is a thin reader of the cached ID wook
1917
2218
  * — validation logic lives in the wook factory, which runs once per request
1918
2219
  * regardless of how many readers consume the value.
1919
2220
  */
1920
- function DbActionPK() {
1921
- return createPkParamDecorator("pk");
2221
+ function DbActionID() {
2222
+ return createIdParamDecorator("id");
1922
2223
  }
1923
2224
  //#endregion
1924
- //#region src/actions/db-action-pks.decorator.ts
2225
+ //#region src/actions/db-action-ids.decorator.ts
1925
2226
  /**
1926
- * Parameter resolver that reads a JSON array of primary keys from the request
1927
- * body and validates each entry against the bound table's PK schema.
2227
+ * Parameter resolver that reads a JSON array of row identifiers from the
2228
+ * request body and validates each entry against the bound table.
1928
2229
  *
1929
- * - Scalar PK JSON array of scalars (`["a","b","c"]`).
1930
- * - Composite PK JSON array of objects.
2230
+ * Body shape is always a JSON array of objects — no scalar form. Each
2231
+ * element's key set MUST exactly match one of the table's legitimate
2232
+ * identifications (PK or any unique index). Elements MAY mix shapes:
2233
+ * `[{ id: "1" }, { slug: "alpha" }]` is valid when both `id` is the PK
2234
+ * and `slug` is a unique index.
1931
2235
  *
1932
- * Validation is strict no type coercion. Marks the param so
1933
- * {@link discoverActions} can infer the action's `level` as `'rows'`.
2236
+ * Strict unknown fields are rejected, no type coercion. Marks the param
2237
+ * so {@link discoverActions} can infer the action's `level` as `'rows'`.
1934
2238
  *
1935
2239
  * In `'rows'` skip mode the resolved value reflects the gate interceptor's
1936
- * filtered subset (the cached PK slot is overwritten in place); see
1937
- * {@link dbActionPksSlot} for precedence details.
2240
+ * filtered subset (the cached ID slot is overwritten in place); see
2241
+ * {@link dbActionIdsSlot} for precedence details.
1938
2242
  */
1939
- function DbActionPKs() {
1940
- return createPkParamDecorator("pks");
2243
+ function DbActionIDs() {
2244
+ return createIdParamDecorator("ids");
1941
2245
  }
1942
2246
  //#endregion
1943
2247
  //#region src/actions/db-action-row.decorator.ts
@@ -1945,9 +2249,8 @@ function createRowParamDecorator(metaKey, slot, resolverName) {
1945
2249
  return (0, moost.ApplyDecorators)((0, moost.getMoostMate)().decorate(metaKey, true), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(slot), resolverName));
1946
2250
  }
1947
2251
  /**
1948
- * Parameter decorator that injects the row whose PK was supplied in the
1949
- * request body. Reads from the cached row wook (fetched once per request,
1950
- * shared with the gate interceptor when `disabled` is set).
2252
+ * Parameter decorator that injects the row whose identifier was supplied in
2253
+ * the request body.
1951
2254
  *
1952
2255
  * Marks the param so {@link discoverActions} infers the action's `level` as
1953
2256
  * `'row'`. Co-occurrence with `@DbActionRows()` (or any multi-cardinality
@@ -1960,8 +2263,8 @@ function DbActionRow() {
1960
2263
  return createRowParamDecorator(MOOST_DB_ACTION_ROW, dbActionRowSlot, "dbActionRow");
1961
2264
  }
1962
2265
  /**
1963
- * Parameter decorator that injects the rows-array fetched by primary keys
1964
- * from the request body. Reads from the cached row-array wook.
2266
+ * Parameter decorator that injects the rows fetched by the identifiers
2267
+ * supplied in the request body.
1965
2268
  *
1966
2269
  * Marks the param so {@link discoverActions} infers the action's `level` as
1967
2270
  * `'rows'`. In `'rows'` + `'skip'` mode the resolved value contains only the
@@ -1979,10 +2282,9 @@ function DbActionRows() {
1979
2282
  * the level-pinned shortcuts (`@DbTableActions`, `@DbRowActions`,
1980
2283
  * `@DbRowsActions`) to avoid repeating `level`.
1981
2284
  *
1982
- * The dictionary key serves as the action `name`. Entries do NOT bind any
1983
- * HTTP route the meta builder surfaces them in `/meta` only. For
1984
- * `processor: 'backend'`, the dev-supplied `value` MUST point to a real
1985
- * `@Post`-bound endpoint accepting the level-determined body shape.
2285
+ * Generic over `TRow` (annotate at the call site: `@DbActions<Order>(...)`)
2286
+ * and `D` (the literal dict, captured via `const D`). Each entry's
2287
+ * `disabled` predicate is type-narrowed by its own `requiredFields` literal.
1986
2288
  *
1987
2289
  * Multiple `@DbActions` (and shortcut) decorators on the same class
1988
2290
  * accumulate.
@@ -2023,6 +2325,21 @@ function classLevelActions(dict, forcedLevel) {
2023
2325
  });
2024
2326
  }
2025
2327
  //#endregion
2328
+ //#region src/actions/per-row.ts
2329
+ /**
2330
+ * Lift a per-row predicate into the batch shape required by
2331
+ * `@DbAction` opts.`disabled` and class-level dict `disabled`. Polarity is
2332
+ * preserved — `true` from `fn` means the action is disabled for that row.
2333
+ *
2334
+ * ```ts
2335
+ * @DbAction<Order>('archive', {
2336
+ * requiredFields: ['status'],
2337
+ * disabled: perRow(r => r.status === 'archived'),
2338
+ * })
2339
+ * ```
2340
+ */
2341
+ const perRow = (fn) => (rows) => rows.map(fn);
2342
+ //#endregion
2026
2343
  exports.ActionDisabledError = ActionDisabledError;
2027
2344
  Object.defineProperty(exports, "AsDbController", {
2028
2345
  enumerable: true,
@@ -2056,8 +2373,8 @@ Object.defineProperty(exports, "AsValueHelpController", {
2056
2373
  });
2057
2374
  exports.DbAction = DbAction;
2058
2375
  exports.DbActionDefault = DbActionDefault;
2059
- exports.DbActionPK = DbActionPK;
2060
- exports.DbActionPKs = DbActionPKs;
2376
+ exports.DbActionID = DbActionID;
2377
+ exports.DbActionIDs = DbActionIDs;
2061
2378
  exports.DbActionRow = DbActionRow;
2062
2379
  exports.DbActionRows = DbActionRows;
2063
2380
  exports.DbActions = DbActions;
@@ -2074,8 +2391,9 @@ exports.TableController = TableController;
2074
2391
  exports.UseValidationErrorTransform = UseValidationErrorTransform;
2075
2392
  exports.ViewController = ViewController;
2076
2393
  exports.discoverActions = discoverActions;
2077
- exports.useDbActionPk = useDbActionPk;
2078
- exports.useDbActionPks = useDbActionPks;
2394
+ exports.perRow = perRow;
2395
+ exports.useDbActionId = useDbActionId;
2396
+ exports.useDbActionIds = useDbActionIds;
2079
2397
  exports.useDbActionRow = useDbActionRow;
2080
2398
  exports.useDbActionRows = useDbActionRows;
2081
2399
  exports.validationErrorTransform = validationErrorTransform;