@atscript/moost-db 0.1.60 → 0.1.62

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
@@ -199,9 +199,6 @@ function isAsValueHelpControllerSubclass(ctor) {
199
199
  if (!asValueHelpCtor) return false;
200
200
  return asValueHelpCtor.prototype.isPrototypeOf(ctor.prototype);
201
201
  }
202
- function isAsDbReadableControllerInstance(value) {
203
- return !!asDbReadableCtor && value instanceof asDbReadableCtor;
204
- }
205
202
  //#endregion
206
203
  //#region src/actions/keys.ts
207
204
  /** Log-message prefix for warnings emitted from the actions subsystem. */
@@ -216,6 +213,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
216
213
  const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
217
214
  const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
218
215
  /**
216
+ * Param-level metadata key — written by `@InputForm(FormType)`. Carries the
217
+ * compiled `.as` class plus its `.name` so {@link discoverActions} can both
218
+ * emit `inputForm` on `/meta` and register the type in the controller's form
219
+ * registry for `GET /meta/form/:name`.
220
+ */
221
+ const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
222
+ /**
223
+ * Generic param-level metadata key — written by `@InputForm(FormType)`
224
+ * alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
225
+ * generic atscript-aware Moost pipe (installed globally via
226
+ * `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
227
+ * resolved value without knowing about the moost-db-specific key.
228
+ */
229
+ const MOOST_ATSCRIPT_TYPE = "atscript_type";
230
+ /**
219
231
  * Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
220
232
  * read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
221
233
  * fields win), and write it back. `name` is empty until `@DbAction` provides
@@ -238,6 +250,8 @@ function scanParamLevel(params) {
238
250
  let multi = false;
239
251
  let hasRowParam = false;
240
252
  let hasBody = false;
253
+ let inputForm;
254
+ let hasDuplicateInputForm = false;
241
255
  for (const p of params) {
242
256
  const kind = p[MOOST_DB_ACTION_PARAM];
243
257
  if (kind === "id") single = true;
@@ -251,13 +265,18 @@ function scanParamLevel(params) {
251
265
  hasRowParam = true;
252
266
  }
253
267
  if (p.paramSource === "BODY") hasBody = true;
268
+ const form = p[MOOST_DB_ACTION_INPUT_FORM];
269
+ if (form) if (inputForm) hasDuplicateInputForm = true;
270
+ else inputForm = form;
254
271
  }
255
272
  return {
256
273
  level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
257
274
  single,
258
275
  multi,
259
276
  hasRowParam,
260
- hasBody
277
+ hasBody,
278
+ inputForm,
279
+ hasDuplicateInputForm
261
280
  };
262
281
  }
263
282
  //#endregion
@@ -274,6 +293,34 @@ const OPTIONAL_FIELDS = [
274
293
  ];
275
294
  const actionsCache = /* @__PURE__ */ new WeakMap();
276
295
  const rowLevelActionsCache = /* @__PURE__ */ new WeakMap();
296
+ /**
297
+ * Per-controller registry of form names → compiled `.as` classes, populated
298
+ * during {@link discoverActions} when a method param carries
299
+ * {@link MOOST_DB_ACTION_INPUT_FORM}. Backs `GET /meta/form/:name`.
300
+ *
301
+ * Same name + same type ref across multiple actions is fine (forms can be
302
+ * reused). Same name + *different* type refs is an ambiguity — discovery
303
+ * warns and drops the second action.
304
+ */
305
+ const formRegistry = /* @__PURE__ */ new WeakMap();
306
+ /** Lookup helper for `AsReadableController.metaForm()`. */
307
+ function getControllerFormType(ctor, name) {
308
+ return formRegistry.get(ctor)?.get(name);
309
+ }
310
+ function registerFormType(ctor, meta, actionName, logger) {
311
+ let map = formRegistry.get(ctor);
312
+ if (!map) {
313
+ map = /* @__PURE__ */ new Map();
314
+ formRegistry.set(ctor, map);
315
+ }
316
+ const existing = map.get(meta.name);
317
+ if (existing && existing !== meta.type) {
318
+ logger.warn(`${WARN_PREFIX} action "${actionName}" — form name "${meta.name}" already registered on this controller with a different type. Reusing the same FormType across actions is fine; clashing names are not — dropping`);
319
+ return false;
320
+ }
321
+ if (!existing) map.set(meta.name, meta.type);
322
+ return true;
323
+ }
277
324
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
278
325
  function discoverActions(controllerCtor, app, logger) {
279
326
  const cached = actionsCache.get(controllerCtor);
@@ -359,6 +406,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
359
406
  processor: "backend",
360
407
  value: path
361
408
  };
409
+ if (levelInfer.inputForm) {
410
+ if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
411
+ info.inputForm = levelInfer.inputForm.name;
412
+ }
362
413
  emitInfo(info, action.opts);
363
414
  seen.add(action.name);
364
415
  out.push({
@@ -373,10 +424,12 @@ function inferMethodLevel(params, actionName, logger) {
373
424
  logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
374
425
  return null;
375
426
  }
427
+ if (scan.hasDuplicateInputForm) logger.warn(`${WARN_PREFIX} action "${actionName}" has more than one @InputForm() param — only the first is honored. Compose multiple inputs into a single form interface.`);
376
428
  return {
377
429
  level: scan.level,
378
430
  bodyConflict: scan.hasBody && scan.level !== "table",
379
- hasRowParam: scan.hasRowParam
431
+ hasRowParam: scan.hasRowParam,
432
+ inputForm: scan.inputForm
380
433
  };
381
434
  }
382
435
  function collectClassActions(ctor, logger, out, seen) {
@@ -487,6 +540,13 @@ function __decorate(decorators, target, key, desc) {
487
540
  return c > 3 && r && Object.defineProperty(target, key, r), r;
488
541
  }
489
542
  //#endregion
543
+ //#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
544
+ function __decorateParam(paramIndex, decorator) {
545
+ return function(target, key) {
546
+ decorator(target, key, paramIndex);
547
+ };
548
+ }
549
+ //#endregion
490
550
  //#region src/as-readable.controller.ts
491
551
  var _ref$4;
492
552
  let AsReadableController = class AsReadableController {
@@ -502,6 +562,8 @@ let AsReadableController = class AsReadableController {
502
562
  _serializedType;
503
563
  /** Cached full meta response (computed lazily on first meta() call). */
504
564
  _metaResponse;
565
+ /** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
566
+ _formSchemas = /* @__PURE__ */ new Map();
505
567
  constructor(boundType, controllerName, app, kindTag = "readable") {
506
568
  this.boundType = boundType;
507
569
  this.controllerName = controllerName;
@@ -644,6 +706,25 @@ let AsReadableController = class AsReadableController {
644
706
  return this.applyMetaOverlay(this._metaResponse);
645
707
  }
646
708
  /**
709
+ * **GET /meta/form/:name** — returns the serialized schema of a form
710
+ * referenced by an action's `inputForm` field. The form name is the
711
+ * compiled `.as` class's `.name`, registered when an action's parameter is
712
+ * decorated with `@InputForm(FormType)`. Schemas are serialized once and
713
+ * cached per controller; the response uses the same annotation-allowlist
714
+ * policy as {@link getSerializeOptions}.
715
+ */
716
+ async metaForm(name) {
717
+ discoverActions(this.constructor, this.app, this.logger);
718
+ const formType = getControllerFormType(this.constructor, name);
719
+ if (!formType) throw new _moostjs_event_http.HttpError(404, `Unknown form "${name}"`);
720
+ let cached = this._formSchemas.get(name);
721
+ if (!cached) {
722
+ cached = (0, _atscript_typescript_utils.serializeAnnotatedType)(formType, this.getSerializeOptions());
723
+ this._formSchemas.set(name, cached);
724
+ }
725
+ return cached;
726
+ }
727
+ /**
647
728
  * Builds the `/meta` payload. Override in subclasses to populate source-specific
648
729
  * fields. Subclasses that fully replace the envelope must call
649
730
  * {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
@@ -697,6 +778,13 @@ __decorate([
697
778
  __decorateMetadata("design:paramtypes", []),
698
779
  __decorateMetadata("design:returntype", Promise)
699
780
  ], AsReadableController.prototype, "meta", null);
781
+ __decorate([
782
+ (0, _moostjs_event_http.Get)("meta/form/:name"),
783
+ __decorateParam(0, (0, moost.Param)("name")),
784
+ __decorateMetadata("design:type", Function),
785
+ __decorateMetadata("design:paramtypes", [String]),
786
+ __decorateMetadata("design:returntype", Promise)
787
+ ], AsReadableController.prototype, "metaForm", null);
700
788
  AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
701
789
  Object,
702
790
  String,
@@ -859,13 +947,6 @@ const QUERY_CONTROLS = [
859
947
  const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
860
948
  const ONE_CONTROLS = dtoControls(GetOneControlsDto);
861
949
  //#endregion
862
- //#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
863
- function __decorateParam(paramIndex, decorator) {
864
- return function(target, key) {
865
- decorator(target, key, paramIndex);
866
- };
867
- }
868
- //#endregion
869
950
  //#region src/as-db-readable.controller.ts
870
951
  var _ref$3, _ref2$2;
871
952
  let AsDbReadableController = class AsDbReadableController extends AsReadableController {
@@ -873,14 +954,12 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
873
954
  readable;
874
955
  _gates;
875
956
  _preferredIdSet;
876
- _compositeIdShapes;
877
957
  _overlayIsNoOp;
878
958
  constructor(readable, app) {
879
959
  super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
880
960
  this.readable = readable;
881
961
  this._gates = this._buildGates();
882
962
  this._preferredIdSet = new Set(readable.preferredId ?? []);
883
- this._compositeIdShapes = (readable.identifications ?? []).filter((id) => id.fields.length >= 2);
884
963
  const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
885
964
  this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
886
965
  }
@@ -1104,12 +1183,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1104
1183
  });
1105
1184
  return result;
1106
1185
  }
1107
- /**
1108
- * Extracts a composite identifier object from query params.
1109
- * Tries composite primary key first, then compound unique indexes.
1110
- */
1111
- extractCompositeId(query) {
1112
- for (const id of this._compositeIdShapes) {
1186
+ /** Pick the first identification (PK or unique index) whose fields are all present in the query. */
1187
+ extractIdShape(query) {
1188
+ for (const id of this.readable.identifications) {
1113
1189
  const idObj = {};
1114
1190
  let allPresent = true;
1115
1191
  for (const field of id.fields) {
@@ -1121,7 +1197,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1121
1197
  }
1122
1198
  if (allPresent) return idObj;
1123
1199
  }
1124
- return new _moostjs_event_http.HttpError(400, "Query params do not match any composite primary key or compound unique index");
1200
+ return new _moostjs_event_http.HttpError(400, "Query params do not match any primary key or unique index");
1125
1201
  }
1126
1202
  /**
1127
1203
  * **GET /query** — returns an array of records or a count.
@@ -1237,7 +1313,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1237
1313
  * (composite primary key or compound unique index).
1238
1314
  */
1239
1315
  async getOneComposite(query, url) {
1240
- const idObj = this.extractCompositeId(query);
1316
+ const idObj = this.extractIdShape(query);
1241
1317
  if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
1242
1318
  const parsed = this.parseQueryString(url);
1243
1319
  this._coerceActionsControl(parsed.controls);
@@ -1437,7 +1513,7 @@ let AsDbController = class AsDbController extends AsDbReadableController {
1437
1513
  * (composite primary key or compound unique index).
1438
1514
  */
1439
1515
  async removeComposite(query) {
1440
- const idObj = this.extractCompositeId(query);
1516
+ const idObj = this.extractIdShape(query);
1441
1517
  if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
1442
1518
  const resolvedId = await this.onRemove(idObj);
1443
1519
  if (resolvedId === void 0) return new _moostjs_event_http.HttpError(500, "Not deleted");
@@ -1855,12 +1931,35 @@ function readCurrentActionMeta(ctx) {
1855
1931
  return (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
1856
1932
  }
1857
1933
  //#endregion
1934
+ //#region src/actions/input-form-cache.ts
1935
+ /**
1936
+ * Cached parse of the action request body. Centralises the shape check so
1937
+ * every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
1938
+ * reads through the same gate. An array or scalar root is rejected with the
1939
+ * same `ValidatorError` envelope as today's strict-shape ID failures.
1940
+ */
1941
+ const dbActionBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
1942
+ const raw = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
1943
+ if (raw == null) return {};
1944
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new _atscript_typescript_utils.ValidatorError([{
1945
+ path: "",
1946
+ message: "Action body must be an object of shape { ids?, input? }"
1947
+ }]);
1948
+ return raw;
1949
+ });
1950
+ /** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
1951
+ const dbActionInputSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
1952
+ return (await ctx.get(dbActionBodySlot)).input;
1953
+ });
1954
+ /** Composable for in-handler reads of the form input. */
1955
+ const useDbActionInput = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionInputSlot) }));
1956
+ //#endregion
1858
1957
  //#region src/actions/id-validation.ts
1859
1958
  const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
1860
1959
  function getSourceCache(source) {
1861
1960
  let cache = SOURCE_CACHE.get(source);
1862
1961
  if (cache) return cache;
1863
- const identifications = source.getIdentifications();
1962
+ const identifications = source.identifications;
1864
1963
  const byKeySig = /* @__PURE__ */ new Map();
1865
1964
  for (const ident of identifications) byKeySig.set(fieldsSig(ident.fields), ident);
1866
1965
  const fieldByName = /* @__PURE__ */ new Map();
@@ -1879,7 +1978,7 @@ function fieldsSig(fields) {
1879
1978
  function isIdValidationSource(value) {
1880
1979
  if (!value || typeof value !== "object") return false;
1881
1980
  const v = value;
1882
- return typeof v.getIdentifications === "function" && Array.isArray(v.fieldDescriptors);
1981
+ return Array.isArray(v.identifications) && Array.isArray(v.fieldDescriptors);
1883
1982
  }
1884
1983
  function validateSingleId(body, source, path = "") {
1885
1984
  const errors = collectIdErrors(body, source, path);
@@ -1947,22 +2046,34 @@ function isPlainObject(value) {
1947
2046
  //#endregion
1948
2047
  //#region src/actions/id-cache.ts
1949
2048
  const boundTableKey = (0, _wooksjs_event_core.key)("atscript_db_action_bound_table");
1950
- function getActionTable(ctx) {
1951
- const fromSlot = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
1952
- if (fromSlot) return fromSlot;
2049
+ function controllerTable(ctx) {
1953
2050
  const ctrl = (0, moost.useControllerContext)(ctx).getController();
1954
2051
  return ctrl?.readable ?? ctrl?.table ?? null;
1955
2052
  }
2053
+ function getActionTable(ctx) {
2054
+ return (ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0) ?? controllerTable(ctx);
2055
+ }
2056
+ const warnedTags = /* @__PURE__ */ new Set();
1956
2057
  function noTableError(ctx) {
1957
2058
  const actionName = readCurrentActionMeta(ctx)?.name;
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.`);
2059
+ const tag = actionName ? `"${actionName}"` : "<unknown>";
2060
+ if (!warnedTags.has(tag)) {
2061
+ warnedTags.add(tag);
2062
+ console.warn(`${WARN_PREFIX} ${tag}: 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.`);
2063
+ }
2064
+ return new _moostjs_event_http.HttpError(500, {
2065
+ statusCode: 500,
2066
+ error: "Internal Server Error",
2067
+ message: "Internal server error",
2068
+ code: "ACTION_TABLE_NOT_BOUND"
2069
+ });
1959
2070
  }
1960
2071
  async function resolveValidatedId(ctx, validate) {
1961
2072
  const table = getActionTable(ctx);
1962
2073
  if (!isIdValidationSource(table)) throw noTableError(ctx);
1963
- const body = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
1964
- validate(body, table);
1965
- return body;
2074
+ const env = await ctx.get(dbActionBodySlot);
2075
+ validate(env.ids, table);
2076
+ return env.ids;
1966
2077
  }
1967
2078
  const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
1968
2079
  const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
@@ -2062,15 +2173,11 @@ const useDbActionRows = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: ()
2062
2173
  //#endregion
2063
2174
  //#region src/actions/gate-interceptor.ts
2064
2175
  const GATE_PRIORITY = moost.TInterceptorPriority.AFTER_GUARD;
2065
- function injectBoundTable(table) {
2176
+ function injectBoundTable(fallback) {
2066
2177
  const ctx = (0, _wooksjs_event_core.current)();
2067
2178
  if (ctx.has(boundTableKey)) return;
2068
- const controller = (0, moost.useControllerContext)(ctx).getController();
2069
- if (isAsDbReadableControllerInstance(controller)) {
2070
- ctx.set(boundTableKey, controller.readable);
2071
- return;
2072
- }
2073
- if (table != null) ctx.set(boundTableKey, table);
2179
+ const t = controllerTable(ctx) ?? fallback;
2180
+ if (t != null) ctx.set(boundTableKey, t);
2074
2181
  }
2075
2182
  function buildGateInterceptor(opts) {
2076
2183
  const { action, level, disabled, onDisabledRows, table } = opts;
@@ -2325,6 +2432,43 @@ function classLevelActions(dict, forcedLevel) {
2325
2432
  });
2326
2433
  }
2327
2434
  //#endregion
2435
+ //#region src/actions/db-action-input-form.decorator.ts
2436
+ /**
2437
+ * Parameter decorator that injects the `input` field of the action request
2438
+ * envelope (`{ ids?, input? }`) into the handler.
2439
+ *
2440
+ * Pairs the resolved value with two pieces of param-level metadata:
2441
+ *
2442
+ * 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
2443
+ * name, consumed by {@link discoverActions} to:
2444
+ * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
2445
+ * - register the type in the controller's form registry so
2446
+ * `GET /meta/form/:name` can serve the serialized schema.
2447
+ * 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
2448
+ * hook any atscript-aware Moost pipe can read without knowing about the
2449
+ * moost-db-specific key.
2450
+ *
2451
+ * Validation is intentionally *not* performed here. To validate `input`
2452
+ * against `FormType`, install an atscript validator pipe globally
2453
+ * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
2454
+ * `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
2455
+ *
2456
+ * Only one `@InputForm()` per action is supported. To collect multiple
2457
+ * structured inputs, compose them into a single `.as` interface and pass an
2458
+ * array form on the field whose user-facing intent is "list of items".
2459
+ *
2460
+ * @param formType A compiled `.as` interface class (carries `.validator()`,
2461
+ * `.metadata`, etc.).
2462
+ */
2463
+ function InputForm(formType) {
2464
+ const mate = (0, moost.getMoostMate)();
2465
+ const meta = {
2466
+ type: formType,
2467
+ name: formType.name
2468
+ };
2469
+ return (0, moost.ApplyDecorators)(mate.decorate(MOOST_DB_ACTION_INPUT_FORM, meta), mate.decorate(MOOST_ATSCRIPT_TYPE, formType), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(dbActionInputSlot), "dbActionInputForm"));
2470
+ }
2471
+ //#endregion
2328
2472
  //#region src/actions/per-row.ts
2329
2473
  /**
2330
2474
  * Lift a per-row predicate into the batch shape required by
@@ -2381,6 +2525,9 @@ exports.DbActions = DbActions;
2381
2525
  exports.DbRowActions = DbRowActions;
2382
2526
  exports.DbRowsActions = DbRowsActions;
2383
2527
  exports.DbTableActions = DbTableActions;
2528
+ exports.InputForm = InputForm;
2529
+ exports.MOOST_ATSCRIPT_TYPE = MOOST_ATSCRIPT_TYPE;
2530
+ exports.MOOST_DB_ACTION_INPUT_FORM = MOOST_DB_ACTION_INPUT_FORM;
2384
2531
  exports.ONE_CONTROLS = ONE_CONTROLS;
2385
2532
  exports.PAGES_CONTROLS = PAGES_CONTROLS;
2386
2533
  exports.QUERY_CONTROLS = QUERY_CONTROLS;
@@ -2390,10 +2537,14 @@ exports.TABLE_DEF = TABLE_DEF;
2390
2537
  exports.TableController = TableController;
2391
2538
  exports.UseValidationErrorTransform = UseValidationErrorTransform;
2392
2539
  exports.ViewController = ViewController;
2540
+ exports.dbActionBodySlot = dbActionBodySlot;
2541
+ exports.dbActionInputSlot = dbActionInputSlot;
2393
2542
  exports.discoverActions = discoverActions;
2543
+ exports.getControllerFormType = getControllerFormType;
2394
2544
  exports.perRow = perRow;
2395
2545
  exports.useDbActionId = useDbActionId;
2396
2546
  exports.useDbActionIds = useDbActionIds;
2547
+ exports.useDbActionInput = useDbActionInput;
2397
2548
  exports.useDbActionRow = useDbActionRow;
2398
2549
  exports.useDbActionRows = useDbActionRows;
2399
2550
  exports.validationErrorTransform = validationErrorTransform;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,5 @@
1
- import * as _atscript_typescript_utils0 from "@atscript/typescript/utils";
2
- import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, Validator } from "@atscript/typescript/utils";
3
1
  import * as _uniqu_url0 from "@uniqu/url";
2
+ import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
4
3
  import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TIdentification, TMetaResponse, Uniquery, UniqueryControls } from "@atscript/db";
5
4
  import { HttpError } from "@moostjs/event-http";
6
5
  import * as moost from "moost";
@@ -61,13 +60,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
61
60
  private _serializedType?;
62
61
  /** Cached full meta response (computed lazily on first meta() call). */
63
62
  private _metaResponse?;
63
+ /** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
64
+ private _formSchemas;
64
65
  constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
65
66
  /** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
66
67
  protected abstract hasField(path: string): boolean;
67
68
  /** Sets @db.http.path on the type metadata from the controller's computed prefix. */
68
69
  private _resolveHttpPath;
69
70
  /** Lazily serializes the bound type (after all controllers have set @db.http.path). */
70
- protected getSerializedType(): _atscript_typescript_utils0.TSerializedAnnotatedType;
71
+ protected getSerializedType(): TSerializedAnnotatedType;
71
72
  /**
72
73
  * One-time initialization hook. Override to seed data, register watchers, etc.
73
74
  */
@@ -112,6 +113,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
112
113
  * subclasses can prune the response by principal.
113
114
  */
114
115
  meta(): Promise<TMetaResponse>;
116
+ /**
117
+ * **GET /meta/form/:name** — returns the serialized schema of a form
118
+ * referenced by an action's `inputForm` field. The form name is the
119
+ * compiled `.as` class's `.name`, registered when an action's parameter is
120
+ * decorated with `@InputForm(FormType)`. Schemas are serialized once and
121
+ * cached per controller; the response uses the same annotation-allowlist
122
+ * policy as {@link getSerializeOptions}.
123
+ */
124
+ metaForm(name: string): Promise<TSerializedAnnotatedType>;
115
125
  /**
116
126
  * Builds the `/meta` payload. Override in subclasses to populate source-specific
117
127
  * fields. Subclasses that fully replace the envelope must call
@@ -155,7 +165,6 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
155
165
  protected readable: AtscriptDbReadable<T>;
156
166
  private readonly _gates;
157
167
  private readonly _preferredIdSet;
158
- private readonly _compositeIdShapes;
159
168
  private readonly _overlayIsNoOp;
160
169
  constructor(readable: AtscriptDbReadable<T>, app: Moost);
161
170
  private _buildGates;
@@ -200,11 +209,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
200
209
  * family (count vs no-count).
201
210
  */
202
211
  private _runReadWithActions;
203
- /**
204
- * Extracts a composite identifier object from query params.
205
- * Tries composite primary key first, then compound unique indexes.
206
- */
207
- protected extractCompositeId(query: Record<string, string>): Record<string, unknown> | HttpError;
212
+ /** Pick the first identification (PK or unique index) whose fields are all present in the query. */
213
+ protected extractIdShape(query: Record<string, string>): Record<string, unknown> | HttpError;
208
214
  /**
209
215
  * **GET /query** — returns an array of records or a count.
210
216
  */
@@ -720,6 +726,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
720
726
  /** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
721
727
  declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
722
728
  //#endregion
729
+ //#region src/actions/db-action-input-form.decorator.d.ts
730
+ /**
731
+ * Parameter decorator that injects the `input` field of the action request
732
+ * envelope (`{ ids?, input? }`) into the handler.
733
+ *
734
+ * Pairs the resolved value with two pieces of param-level metadata:
735
+ *
736
+ * 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
737
+ * name, consumed by {@link discoverActions} to:
738
+ * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
739
+ * - register the type in the controller's form registry so
740
+ * `GET /meta/form/:name` can serve the serialized schema.
741
+ * 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
742
+ * hook any atscript-aware Moost pipe can read without knowing about the
743
+ * moost-db-specific key.
744
+ *
745
+ * Validation is intentionally *not* performed here. To validate `input`
746
+ * against `FormType`, install an atscript validator pipe globally
747
+ * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
748
+ * `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
749
+ *
750
+ * Only one `@InputForm()` per action is supported. To collect multiple
751
+ * structured inputs, compose them into a single `.as` interface and pass an
752
+ * array form on the field whose user-facing intent is "list of items".
753
+ *
754
+ * @param formType A compiled `.as` interface class (carries `.validator()`,
755
+ * `.metadata`, etc.).
756
+ */
757
+ declare function InputForm<T extends TAtscriptAnnotatedType & {
758
+ readonly name: string;
759
+ }>(formType: T): ParameterDecorator;
760
+ //#endregion
723
761
  //#region src/actions/discover.d.ts
724
762
  /**
725
763
  * Pairs the wire-shaped `info` with the original decorator opts / dict entry,
@@ -730,13 +768,16 @@ interface TDbActionEnvelope {
730
768
  info: TDbActionInfo$1;
731
769
  raw: DbActionOpts | TDbActionsEntry;
732
770
  }
771
+ /** Lookup helper for `AsReadableController.metaForm()`. */
772
+ declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
733
773
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
734
774
  declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
735
775
  //#endregion
736
776
  //#region src/actions/id-validation.d.ts
777
+ /** Duck-typed shape; matches `AtscriptDbReadable`'s public surface. */
737
778
  interface IdValidationSource {
738
- getIdentifications(): readonly TIdentification[];
739
- fieldDescriptors: readonly TDbFieldMeta[];
779
+ readonly identifications: readonly TIdentification[];
780
+ readonly fieldDescriptors: readonly TDbFieldMeta[];
740
781
  }
741
782
  //#endregion
742
783
  //#region src/actions/id-cache.d.ts
@@ -755,6 +796,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
755
796
  load: () => Promise<(Record<string, unknown> | undefined)[]>;
756
797
  }>;
757
798
  //#endregion
799
+ //#region src/actions/input-form-cache.d.ts
800
+ /**
801
+ * Wire-shape of an action request body. Both fields are optional:
802
+ *
803
+ * - `ids` — what previously sat at the body root: a single identifier object
804
+ * (`'row'`-level), an array of identifier objects (`'rows'`-level), or
805
+ * absent (`'table'`-level).
806
+ * - `input` — present only when the action declares an `@InputForm()`
807
+ * parameter; carries the form payload the user filled out.
808
+ */
809
+ interface DbActionEnvelope {
810
+ ids?: unknown;
811
+ input?: unknown;
812
+ }
813
+ /**
814
+ * Cached parse of the action request body. Centralises the shape check so
815
+ * every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
816
+ * reads through the same gate. An array or scalar root is rejected with the
817
+ * same `ValidatorError` envelope as today's strict-shape ID failures.
818
+ */
819
+ declare const dbActionBodySlot: moost.Cached<Promise<DbActionEnvelope>>;
820
+ /** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
821
+ declare const dbActionInputSlot: moost.Cached<Promise<unknown>>;
822
+ /** Composable for in-handler reads of the form input. */
823
+ declare const useDbActionInput: _wooksjs_event_core0.WookComposable<{
824
+ load: () => Promise<unknown>;
825
+ }>;
826
+ //#endregion
827
+ //#region src/actions/keys.d.ts
828
+ /** Method-level metadata key — written by `@DbAction(name, opts)`. */
829
+ declare const MOOST_DB_ACTION = "atscript_db_action";
830
+ /** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
831
+ declare const MOOST_DB_ACTIONS = "atscript_db_actions";
832
+ /** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
833
+ declare const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
834
+ /** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
835
+ declare const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
836
+ declare const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
837
+ /**
838
+ * Param-level metadata key — written by `@InputForm(FormType)`. Carries the
839
+ * compiled `.as` class plus its `.name` so {@link discoverActions} can both
840
+ * emit `inputForm` on `/meta` and register the type in the controller's form
841
+ * registry for `GET /meta/form/:name`.
842
+ */
843
+ declare const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
844
+ /**
845
+ * Generic param-level metadata key — written by `@InputForm(FormType)`
846
+ * alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
847
+ * generic atscript-aware Moost pipe (installed globally via
848
+ * `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
849
+ * resolved value without knowing about the moost-db-specific key.
850
+ */
851
+ declare const MOOST_ATSCRIPT_TYPE = "atscript_type";
852
+ type TDbActionRowMarker = true;
853
+ /** Stamped by `@InputForm(FormType)` — the compiled `.as` class + the wire name (`FormType.name`). */
854
+ interface TDbActionInputFormMeta {
855
+ type: TAtscriptAnnotatedType;
856
+ name: string;
857
+ }
858
+ /** Method-level action metadata written by `@DbAction(name, opts)`. */
859
+ interface TDbActionMeta {
860
+ name: string;
861
+ opts: DbActionOpts;
862
+ }
863
+ /** Class-level entry — a `TDbActionsEntry` plus its dictionary key. */
864
+ interface TDbClassActionMeta {
865
+ name: string;
866
+ entry: TDbActionsEntry;
867
+ }
868
+ /** Param marker kind — informs level inference and ID-resolution shape. */
869
+ type TDbActionParamKind = "id" | "ids";
870
+ declare module "moost" {
871
+ interface TMoostMetadata {
872
+ [MOOST_DB_ACTION]?: TDbActionMeta;
873
+ [MOOST_DB_ACTIONS]?: TDbClassActionMeta[];
874
+ [MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
875
+ [MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
876
+ [MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
877
+ }
878
+ interface TMoostParamsMetadata {
879
+ [MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
880
+ [MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
881
+ [MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
882
+ [MOOST_DB_ACTION_INPUT_FORM]?: TDbActionInputFormMeta;
883
+ [MOOST_ATSCRIPT_TYPE]?: TAtscriptAnnotatedType;
884
+ }
885
+ }
886
+ //#endregion
758
887
  //#region src/actions/action-disabled-error.d.ts
759
888
  /**
760
889
  * Wire-body shape for server-side gate rejections. The `name` discriminator
@@ -815,4 +944,4 @@ declare const QUERY_CONTROLS: readonly string[];
815
944
  declare const PAGES_CONTROLS: readonly string[];
816
945
  declare const ONE_CONTROLS: readonly string[];
817
946
  //#endregion
818
- export { ActionDisabledError, ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, IdValidationSource, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, TDbActionsEntry, TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, discoverActions, perRow, useDbActionId, useDbActionIds, useDbActionRow, useDbActionRows, validationErrorTransform };
947
+ export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, type IdValidationSource, InputForm, MOOST_ATSCRIPT_TYPE, MOOST_DB_ACTION_INPUT_FORM, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, dbActionBodySlot, dbActionInputSlot, discoverActions, getControllerFormType, perRow, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import * as _atscript_typescript_utils0 from "@atscript/typescript/utils";
2
- import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, Validator } from "@atscript/typescript/utils";
1
+ import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
3
2
  import { HttpError } from "@moostjs/event-http";
4
3
  import * as moost from "moost";
5
4
  import { Moost, TConsoleBase } from "moost";
@@ -61,13 +60,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
61
60
  private _serializedType?;
62
61
  /** Cached full meta response (computed lazily on first meta() call). */
63
62
  private _metaResponse?;
63
+ /** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
64
+ private _formSchemas;
64
65
  constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
65
66
  /** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
66
67
  protected abstract hasField(path: string): boolean;
67
68
  /** Sets @db.http.path on the type metadata from the controller's computed prefix. */
68
69
  private _resolveHttpPath;
69
70
  /** Lazily serializes the bound type (after all controllers have set @db.http.path). */
70
- protected getSerializedType(): _atscript_typescript_utils0.TSerializedAnnotatedType;
71
+ protected getSerializedType(): TSerializedAnnotatedType;
71
72
  /**
72
73
  * One-time initialization hook. Override to seed data, register watchers, etc.
73
74
  */
@@ -112,6 +113,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
112
113
  * subclasses can prune the response by principal.
113
114
  */
114
115
  meta(): Promise<TMetaResponse>;
116
+ /**
117
+ * **GET /meta/form/:name** — returns the serialized schema of a form
118
+ * referenced by an action's `inputForm` field. The form name is the
119
+ * compiled `.as` class's `.name`, registered when an action's parameter is
120
+ * decorated with `@InputForm(FormType)`. Schemas are serialized once and
121
+ * cached per controller; the response uses the same annotation-allowlist
122
+ * policy as {@link getSerializeOptions}.
123
+ */
124
+ metaForm(name: string): Promise<TSerializedAnnotatedType>;
115
125
  /**
116
126
  * Builds the `/meta` payload. Override in subclasses to populate source-specific
117
127
  * fields. Subclasses that fully replace the envelope must call
@@ -155,7 +165,6 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
155
165
  protected readable: AtscriptDbReadable<T>;
156
166
  private readonly _gates;
157
167
  private readonly _preferredIdSet;
158
- private readonly _compositeIdShapes;
159
168
  private readonly _overlayIsNoOp;
160
169
  constructor(readable: AtscriptDbReadable<T>, app: Moost);
161
170
  private _buildGates;
@@ -200,11 +209,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
200
209
  * family (count vs no-count).
201
210
  */
202
211
  private _runReadWithActions;
203
- /**
204
- * Extracts a composite identifier object from query params.
205
- * Tries composite primary key first, then compound unique indexes.
206
- */
207
- protected extractCompositeId(query: Record<string, string>): Record<string, unknown> | HttpError;
212
+ /** Pick the first identification (PK or unique index) whose fields are all present in the query. */
213
+ protected extractIdShape(query: Record<string, string>): Record<string, unknown> | HttpError;
208
214
  /**
209
215
  * **GET /query** — returns an array of records or a count.
210
216
  */
@@ -720,6 +726,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
720
726
  /** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
721
727
  declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
722
728
  //#endregion
729
+ //#region src/actions/db-action-input-form.decorator.d.ts
730
+ /**
731
+ * Parameter decorator that injects the `input` field of the action request
732
+ * envelope (`{ ids?, input? }`) into the handler.
733
+ *
734
+ * Pairs the resolved value with two pieces of param-level metadata:
735
+ *
736
+ * 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
737
+ * name, consumed by {@link discoverActions} to:
738
+ * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
739
+ * - register the type in the controller's form registry so
740
+ * `GET /meta/form/:name` can serve the serialized schema.
741
+ * 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
742
+ * hook any atscript-aware Moost pipe can read without knowing about the
743
+ * moost-db-specific key.
744
+ *
745
+ * Validation is intentionally *not* performed here. To validate `input`
746
+ * against `FormType`, install an atscript validator pipe globally
747
+ * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
748
+ * `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
749
+ *
750
+ * Only one `@InputForm()` per action is supported. To collect multiple
751
+ * structured inputs, compose them into a single `.as` interface and pass an
752
+ * array form on the field whose user-facing intent is "list of items".
753
+ *
754
+ * @param formType A compiled `.as` interface class (carries `.validator()`,
755
+ * `.metadata`, etc.).
756
+ */
757
+ declare function InputForm<T extends TAtscriptAnnotatedType & {
758
+ readonly name: string;
759
+ }>(formType: T): ParameterDecorator;
760
+ //#endregion
723
761
  //#region src/actions/discover.d.ts
724
762
  /**
725
763
  * Pairs the wire-shaped `info` with the original decorator opts / dict entry,
@@ -730,13 +768,16 @@ interface TDbActionEnvelope {
730
768
  info: TDbActionInfo$1;
731
769
  raw: DbActionOpts | TDbActionsEntry;
732
770
  }
771
+ /** Lookup helper for `AsReadableController.metaForm()`. */
772
+ declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
733
773
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
734
774
  declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
735
775
  //#endregion
736
776
  //#region src/actions/id-validation.d.ts
777
+ /** Duck-typed shape; matches `AtscriptDbReadable`'s public surface. */
737
778
  interface IdValidationSource {
738
- getIdentifications(): readonly TIdentification[];
739
- fieldDescriptors: readonly TDbFieldMeta[];
779
+ readonly identifications: readonly TIdentification[];
780
+ readonly fieldDescriptors: readonly TDbFieldMeta[];
740
781
  }
741
782
  //#endregion
742
783
  //#region src/actions/id-cache.d.ts
@@ -755,6 +796,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
755
796
  load: () => Promise<(Record<string, unknown> | undefined)[]>;
756
797
  }>;
757
798
  //#endregion
799
+ //#region src/actions/input-form-cache.d.ts
800
+ /**
801
+ * Wire-shape of an action request body. Both fields are optional:
802
+ *
803
+ * - `ids` — what previously sat at the body root: a single identifier object
804
+ * (`'row'`-level), an array of identifier objects (`'rows'`-level), or
805
+ * absent (`'table'`-level).
806
+ * - `input` — present only when the action declares an `@InputForm()`
807
+ * parameter; carries the form payload the user filled out.
808
+ */
809
+ interface DbActionEnvelope {
810
+ ids?: unknown;
811
+ input?: unknown;
812
+ }
813
+ /**
814
+ * Cached parse of the action request body. Centralises the shape check so
815
+ * every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
816
+ * reads through the same gate. An array or scalar root is rejected with the
817
+ * same `ValidatorError` envelope as today's strict-shape ID failures.
818
+ */
819
+ declare const dbActionBodySlot: moost.Cached<Promise<DbActionEnvelope>>;
820
+ /** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
821
+ declare const dbActionInputSlot: moost.Cached<Promise<unknown>>;
822
+ /** Composable for in-handler reads of the form input. */
823
+ declare const useDbActionInput: _wooksjs_event_core0.WookComposable<{
824
+ load: () => Promise<unknown>;
825
+ }>;
826
+ //#endregion
827
+ //#region src/actions/keys.d.ts
828
+ /** Method-level metadata key — written by `@DbAction(name, opts)`. */
829
+ declare const MOOST_DB_ACTION = "atscript_db_action";
830
+ /** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
831
+ declare const MOOST_DB_ACTIONS = "atscript_db_actions";
832
+ /** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
833
+ declare const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
834
+ /** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
835
+ declare const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
836
+ declare const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
837
+ /**
838
+ * Param-level metadata key — written by `@InputForm(FormType)`. Carries the
839
+ * compiled `.as` class plus its `.name` so {@link discoverActions} can both
840
+ * emit `inputForm` on `/meta` and register the type in the controller's form
841
+ * registry for `GET /meta/form/:name`.
842
+ */
843
+ declare const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
844
+ /**
845
+ * Generic param-level metadata key — written by `@InputForm(FormType)`
846
+ * alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
847
+ * generic atscript-aware Moost pipe (installed globally via
848
+ * `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
849
+ * resolved value without knowing about the moost-db-specific key.
850
+ */
851
+ declare const MOOST_ATSCRIPT_TYPE = "atscript_type";
852
+ type TDbActionRowMarker = true;
853
+ /** Stamped by `@InputForm(FormType)` — the compiled `.as` class + the wire name (`FormType.name`). */
854
+ interface TDbActionInputFormMeta {
855
+ type: TAtscriptAnnotatedType;
856
+ name: string;
857
+ }
858
+ /** Method-level action metadata written by `@DbAction(name, opts)`. */
859
+ interface TDbActionMeta {
860
+ name: string;
861
+ opts: DbActionOpts;
862
+ }
863
+ /** Class-level entry — a `TDbActionsEntry` plus its dictionary key. */
864
+ interface TDbClassActionMeta {
865
+ name: string;
866
+ entry: TDbActionsEntry;
867
+ }
868
+ /** Param marker kind — informs level inference and ID-resolution shape. */
869
+ type TDbActionParamKind = "id" | "ids";
870
+ declare module "moost" {
871
+ interface TMoostMetadata {
872
+ [MOOST_DB_ACTION]?: TDbActionMeta;
873
+ [MOOST_DB_ACTIONS]?: TDbClassActionMeta[];
874
+ [MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
875
+ [MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
876
+ [MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
877
+ }
878
+ interface TMoostParamsMetadata {
879
+ [MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
880
+ [MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
881
+ [MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
882
+ [MOOST_DB_ACTION_INPUT_FORM]?: TDbActionInputFormMeta;
883
+ [MOOST_ATSCRIPT_TYPE]?: TAtscriptAnnotatedType;
884
+ }
885
+ }
886
+ //#endregion
758
887
  //#region src/actions/action-disabled-error.d.ts
759
888
  /**
760
889
  * Wire-body shape for server-side gate rejections. The `name` discriminator
@@ -815,4 +944,4 @@ declare const QUERY_CONTROLS: readonly string[];
815
944
  declare const PAGES_CONTROLS: readonly string[];
816
945
  declare const ONE_CONTROLS: readonly string[];
817
946
  //#endregion
818
- export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, type IdValidationSource, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, discoverActions, perRow, useDbActionId, useDbActionIds, useDbActionRow, useDbActionRows, validationErrorTransform };
947
+ export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, type IdValidationSource, InputForm, MOOST_ATSCRIPT_TYPE, MOOST_DB_ACTION_INPUT_FORM, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, dbActionBodySlot, dbActionInputSlot, discoverActions, getControllerFormType, perRow, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
package/dist/index.mjs CHANGED
@@ -198,9 +198,6 @@ function isAsValueHelpControllerSubclass(ctor) {
198
198
  if (!asValueHelpCtor) return false;
199
199
  return asValueHelpCtor.prototype.isPrototypeOf(ctor.prototype);
200
200
  }
201
- function isAsDbReadableControllerInstance(value) {
202
- return !!asDbReadableCtor && value instanceof asDbReadableCtor;
203
- }
204
201
  //#endregion
205
202
  //#region src/actions/keys.ts
206
203
  /** Log-message prefix for warnings emitted from the actions subsystem. */
@@ -215,6 +212,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
215
212
  const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
216
213
  const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
217
214
  /**
215
+ * Param-level metadata key — written by `@InputForm(FormType)`. Carries the
216
+ * compiled `.as` class plus its `.name` so {@link discoverActions} can both
217
+ * emit `inputForm` on `/meta` and register the type in the controller's form
218
+ * registry for `GET /meta/form/:name`.
219
+ */
220
+ const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
221
+ /**
222
+ * Generic param-level metadata key — written by `@InputForm(FormType)`
223
+ * alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
224
+ * generic atscript-aware Moost pipe (installed globally via
225
+ * `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
226
+ * resolved value without knowing about the moost-db-specific key.
227
+ */
228
+ const MOOST_ATSCRIPT_TYPE = "atscript_type";
229
+ /**
218
230
  * Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
219
231
  * read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
220
232
  * fields win), and write it back. `name` is empty until `@DbAction` provides
@@ -237,6 +249,8 @@ function scanParamLevel(params) {
237
249
  let multi = false;
238
250
  let hasRowParam = false;
239
251
  let hasBody = false;
252
+ let inputForm;
253
+ let hasDuplicateInputForm = false;
240
254
  for (const p of params) {
241
255
  const kind = p[MOOST_DB_ACTION_PARAM];
242
256
  if (kind === "id") single = true;
@@ -250,13 +264,18 @@ function scanParamLevel(params) {
250
264
  hasRowParam = true;
251
265
  }
252
266
  if (p.paramSource === "BODY") hasBody = true;
267
+ const form = p[MOOST_DB_ACTION_INPUT_FORM];
268
+ if (form) if (inputForm) hasDuplicateInputForm = true;
269
+ else inputForm = form;
253
270
  }
254
271
  return {
255
272
  level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
256
273
  single,
257
274
  multi,
258
275
  hasRowParam,
259
- hasBody
276
+ hasBody,
277
+ inputForm,
278
+ hasDuplicateInputForm
260
279
  };
261
280
  }
262
281
  //#endregion
@@ -273,6 +292,34 @@ const OPTIONAL_FIELDS = [
273
292
  ];
274
293
  const actionsCache = /* @__PURE__ */ new WeakMap();
275
294
  const rowLevelActionsCache = /* @__PURE__ */ new WeakMap();
295
+ /**
296
+ * Per-controller registry of form names → compiled `.as` classes, populated
297
+ * during {@link discoverActions} when a method param carries
298
+ * {@link MOOST_DB_ACTION_INPUT_FORM}. Backs `GET /meta/form/:name`.
299
+ *
300
+ * Same name + same type ref across multiple actions is fine (forms can be
301
+ * reused). Same name + *different* type refs is an ambiguity — discovery
302
+ * warns and drops the second action.
303
+ */
304
+ const formRegistry = /* @__PURE__ */ new WeakMap();
305
+ /** Lookup helper for `AsReadableController.metaForm()`. */
306
+ function getControllerFormType(ctor, name) {
307
+ return formRegistry.get(ctor)?.get(name);
308
+ }
309
+ function registerFormType(ctor, meta, actionName, logger) {
310
+ let map = formRegistry.get(ctor);
311
+ if (!map) {
312
+ map = /* @__PURE__ */ new Map();
313
+ formRegistry.set(ctor, map);
314
+ }
315
+ const existing = map.get(meta.name);
316
+ if (existing && existing !== meta.type) {
317
+ logger.warn(`${WARN_PREFIX} action "${actionName}" — form name "${meta.name}" already registered on this controller with a different type. Reusing the same FormType across actions is fine; clashing names are not — dropping`);
318
+ return false;
319
+ }
320
+ if (!existing) map.set(meta.name, meta.type);
321
+ return true;
322
+ }
276
323
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
277
324
  function discoverActions(controllerCtor, app, logger) {
278
325
  const cached = actionsCache.get(controllerCtor);
@@ -358,6 +405,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
358
405
  processor: "backend",
359
406
  value: path
360
407
  };
408
+ if (levelInfer.inputForm) {
409
+ if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
410
+ info.inputForm = levelInfer.inputForm.name;
411
+ }
361
412
  emitInfo(info, action.opts);
362
413
  seen.add(action.name);
363
414
  out.push({
@@ -372,10 +423,12 @@ function inferMethodLevel(params, actionName, logger) {
372
423
  logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
373
424
  return null;
374
425
  }
426
+ if (scan.hasDuplicateInputForm) logger.warn(`${WARN_PREFIX} action "${actionName}" has more than one @InputForm() param — only the first is honored. Compose multiple inputs into a single form interface.`);
375
427
  return {
376
428
  level: scan.level,
377
429
  bodyConflict: scan.hasBody && scan.level !== "table",
378
- hasRowParam: scan.hasRowParam
430
+ hasRowParam: scan.hasRowParam,
431
+ inputForm: scan.inputForm
379
432
  };
380
433
  }
381
434
  function collectClassActions(ctor, logger, out, seen) {
@@ -486,6 +539,13 @@ function __decorate(decorators, target, key, desc) {
486
539
  return c > 3 && r && Object.defineProperty(target, key, r), r;
487
540
  }
488
541
  //#endregion
542
+ //#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
543
+ function __decorateParam(paramIndex, decorator) {
544
+ return function(target, key) {
545
+ decorator(target, key, paramIndex);
546
+ };
547
+ }
548
+ //#endregion
489
549
  //#region src/as-readable.controller.ts
490
550
  var _ref$4;
491
551
  let AsReadableController = class AsReadableController {
@@ -501,6 +561,8 @@ let AsReadableController = class AsReadableController {
501
561
  _serializedType;
502
562
  /** Cached full meta response (computed lazily on first meta() call). */
503
563
  _metaResponse;
564
+ /** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
565
+ _formSchemas = /* @__PURE__ */ new Map();
504
566
  constructor(boundType, controllerName, app, kindTag = "readable") {
505
567
  this.boundType = boundType;
506
568
  this.controllerName = controllerName;
@@ -643,6 +705,25 @@ let AsReadableController = class AsReadableController {
643
705
  return this.applyMetaOverlay(this._metaResponse);
644
706
  }
645
707
  /**
708
+ * **GET /meta/form/:name** — returns the serialized schema of a form
709
+ * referenced by an action's `inputForm` field. The form name is the
710
+ * compiled `.as` class's `.name`, registered when an action's parameter is
711
+ * decorated with `@InputForm(FormType)`. Schemas are serialized once and
712
+ * cached per controller; the response uses the same annotation-allowlist
713
+ * policy as {@link getSerializeOptions}.
714
+ */
715
+ async metaForm(name) {
716
+ discoverActions(this.constructor, this.app, this.logger);
717
+ const formType = getControllerFormType(this.constructor, name);
718
+ if (!formType) throw new HttpError(404, `Unknown form "${name}"`);
719
+ let cached = this._formSchemas.get(name);
720
+ if (!cached) {
721
+ cached = serializeAnnotatedType(formType, this.getSerializeOptions());
722
+ this._formSchemas.set(name, cached);
723
+ }
724
+ return cached;
725
+ }
726
+ /**
646
727
  * Builds the `/meta` payload. Override in subclasses to populate source-specific
647
728
  * fields. Subclasses that fully replace the envelope must call
648
729
  * {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
@@ -696,6 +777,13 @@ __decorate([
696
777
  __decorateMetadata("design:paramtypes", []),
697
778
  __decorateMetadata("design:returntype", Promise)
698
779
  ], AsReadableController.prototype, "meta", null);
780
+ __decorate([
781
+ Get("meta/form/:name"),
782
+ __decorateParam(0, Param("name")),
783
+ __decorateMetadata("design:type", Function),
784
+ __decorateMetadata("design:paramtypes", [String]),
785
+ __decorateMetadata("design:returntype", Promise)
786
+ ], AsReadableController.prototype, "metaForm", null);
699
787
  AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
700
788
  Object,
701
789
  String,
@@ -858,13 +946,6 @@ const QUERY_CONTROLS = [
858
946
  const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
859
947
  const ONE_CONTROLS = dtoControls(GetOneControlsDto);
860
948
  //#endregion
861
- //#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
862
- function __decorateParam(paramIndex, decorator) {
863
- return function(target, key) {
864
- decorator(target, key, paramIndex);
865
- };
866
- }
867
- //#endregion
868
949
  //#region src/as-db-readable.controller.ts
869
950
  var _ref$3, _ref2$2;
870
951
  let AsDbReadableController = class AsDbReadableController extends AsReadableController {
@@ -872,14 +953,12 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
872
953
  readable;
873
954
  _gates;
874
955
  _preferredIdSet;
875
- _compositeIdShapes;
876
956
  _overlayIsNoOp;
877
957
  constructor(readable, app) {
878
958
  super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
879
959
  this.readable = readable;
880
960
  this._gates = this._buildGates();
881
961
  this._preferredIdSet = new Set(readable.preferredId ?? []);
882
- this._compositeIdShapes = (readable.identifications ?? []).filter((id) => id.fields.length >= 2);
883
962
  const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
884
963
  this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
885
964
  }
@@ -1103,12 +1182,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1103
1182
  });
1104
1183
  return result;
1105
1184
  }
1106
- /**
1107
- * Extracts a composite identifier object from query params.
1108
- * Tries composite primary key first, then compound unique indexes.
1109
- */
1110
- extractCompositeId(query) {
1111
- for (const id of this._compositeIdShapes) {
1185
+ /** Pick the first identification (PK or unique index) whose fields are all present in the query. */
1186
+ extractIdShape(query) {
1187
+ for (const id of this.readable.identifications) {
1112
1188
  const idObj = {};
1113
1189
  let allPresent = true;
1114
1190
  for (const field of id.fields) {
@@ -1120,7 +1196,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1120
1196
  }
1121
1197
  if (allPresent) return idObj;
1122
1198
  }
1123
- return new HttpError(400, "Query params do not match any composite primary key or compound unique index");
1199
+ return new HttpError(400, "Query params do not match any primary key or unique index");
1124
1200
  }
1125
1201
  /**
1126
1202
  * **GET /query** — returns an array of records or a count.
@@ -1236,7 +1312,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1236
1312
  * (composite primary key or compound unique index).
1237
1313
  */
1238
1314
  async getOneComposite(query, url) {
1239
- const idObj = this.extractCompositeId(query);
1315
+ const idObj = this.extractIdShape(query);
1240
1316
  if (idObj instanceof HttpError) return idObj;
1241
1317
  const parsed = this.parseQueryString(url);
1242
1318
  this._coerceActionsControl(parsed.controls);
@@ -1436,7 +1512,7 @@ let AsDbController = class AsDbController extends AsDbReadableController {
1436
1512
  * (composite primary key or compound unique index).
1437
1513
  */
1438
1514
  async removeComposite(query) {
1439
- const idObj = this.extractCompositeId(query);
1515
+ const idObj = this.extractIdShape(query);
1440
1516
  if (idObj instanceof HttpError) return idObj;
1441
1517
  const resolvedId = await this.onRemove(idObj);
1442
1518
  if (resolvedId === void 0) return new HttpError(500, "Not deleted");
@@ -1854,12 +1930,35 @@ function readCurrentActionMeta(ctx) {
1854
1930
  return getMoostMate().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
1855
1931
  }
1856
1932
  //#endregion
1933
+ //#region src/actions/input-form-cache.ts
1934
+ /**
1935
+ * Cached parse of the action request body. Centralises the shape check so
1936
+ * every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
1937
+ * reads through the same gate. An array or scalar root is rejected with the
1938
+ * same `ValidatorError` envelope as today's strict-shape ID failures.
1939
+ */
1940
+ const dbActionBodySlot = cached(async (ctx) => {
1941
+ const raw = await useBody(ctx).parseBody();
1942
+ if (raw == null) return {};
1943
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new ValidatorError([{
1944
+ path: "",
1945
+ message: "Action body must be an object of shape { ids?, input? }"
1946
+ }]);
1947
+ return raw;
1948
+ });
1949
+ /** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
1950
+ const dbActionInputSlot = cached(async (ctx) => {
1951
+ return (await ctx.get(dbActionBodySlot)).input;
1952
+ });
1953
+ /** Composable for in-handler reads of the form input. */
1954
+ const useDbActionInput = defineWook((ctx) => ({ load: () => ctx.get(dbActionInputSlot) }));
1955
+ //#endregion
1857
1956
  //#region src/actions/id-validation.ts
1858
1957
  const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
1859
1958
  function getSourceCache(source) {
1860
1959
  let cache = SOURCE_CACHE.get(source);
1861
1960
  if (cache) return cache;
1862
- const identifications = source.getIdentifications();
1961
+ const identifications = source.identifications;
1863
1962
  const byKeySig = /* @__PURE__ */ new Map();
1864
1963
  for (const ident of identifications) byKeySig.set(fieldsSig(ident.fields), ident);
1865
1964
  const fieldByName = /* @__PURE__ */ new Map();
@@ -1878,7 +1977,7 @@ function fieldsSig(fields) {
1878
1977
  function isIdValidationSource(value) {
1879
1978
  if (!value || typeof value !== "object") return false;
1880
1979
  const v = value;
1881
- return typeof v.getIdentifications === "function" && Array.isArray(v.fieldDescriptors);
1980
+ return Array.isArray(v.identifications) && Array.isArray(v.fieldDescriptors);
1882
1981
  }
1883
1982
  function validateSingleId(body, source, path = "") {
1884
1983
  const errors = collectIdErrors(body, source, path);
@@ -1946,22 +2045,34 @@ function isPlainObject(value) {
1946
2045
  //#endregion
1947
2046
  //#region src/actions/id-cache.ts
1948
2047
  const boundTableKey = key("atscript_db_action_bound_table");
1949
- function getActionTable(ctx) {
1950
- const fromSlot = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
1951
- if (fromSlot) return fromSlot;
2048
+ function controllerTable(ctx) {
1952
2049
  const ctrl = useControllerContext(ctx).getController();
1953
2050
  return ctrl?.readable ?? ctrl?.table ?? null;
1954
2051
  }
2052
+ function getActionTable(ctx) {
2053
+ return (ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0) ?? controllerTable(ctx);
2054
+ }
2055
+ const warnedTags = /* @__PURE__ */ new Set();
1955
2056
  function noTableError(ctx) {
1956
2057
  const actionName = readCurrentActionMeta(ctx)?.name;
1957
- return new 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.`);
2058
+ const tag = actionName ? `"${actionName}"` : "<unknown>";
2059
+ if (!warnedTags.has(tag)) {
2060
+ warnedTags.add(tag);
2061
+ console.warn(`${WARN_PREFIX} ${tag}: 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.`);
2062
+ }
2063
+ return new HttpError(500, {
2064
+ statusCode: 500,
2065
+ error: "Internal Server Error",
2066
+ message: "Internal server error",
2067
+ code: "ACTION_TABLE_NOT_BOUND"
2068
+ });
1958
2069
  }
1959
2070
  async function resolveValidatedId(ctx, validate) {
1960
2071
  const table = getActionTable(ctx);
1961
2072
  if (!isIdValidationSource(table)) throw noTableError(ctx);
1962
- const body = await useBody(ctx).parseBody();
1963
- validate(body, table);
1964
- return body;
2073
+ const env = await ctx.get(dbActionBodySlot);
2074
+ validate(env.ids, table);
2075
+ return env.ids;
1965
2076
  }
1966
2077
  const dbActionIdSlot = cached((ctx) => resolveValidatedId(ctx, validateSingleId));
1967
2078
  const dbActionIdsSlot = cached(async (ctx) => {
@@ -2061,15 +2172,11 @@ const useDbActionRows = defineWook((ctx) => ({ load: () => ctx.get(dbActionRowsS
2061
2172
  //#endregion
2062
2173
  //#region src/actions/gate-interceptor.ts
2063
2174
  const GATE_PRIORITY = TInterceptorPriority.AFTER_GUARD;
2064
- function injectBoundTable(table) {
2175
+ function injectBoundTable(fallback) {
2065
2176
  const ctx = current();
2066
2177
  if (ctx.has(boundTableKey)) return;
2067
- const controller = useControllerContext(ctx).getController();
2068
- if (isAsDbReadableControllerInstance(controller)) {
2069
- ctx.set(boundTableKey, controller.readable);
2070
- return;
2071
- }
2072
- if (table != null) ctx.set(boundTableKey, table);
2178
+ const t = controllerTable(ctx) ?? fallback;
2179
+ if (t != null) ctx.set(boundTableKey, t);
2073
2180
  }
2074
2181
  function buildGateInterceptor(opts) {
2075
2182
  const { action, level, disabled, onDisabledRows, table } = opts;
@@ -2324,6 +2431,43 @@ function classLevelActions(dict, forcedLevel) {
2324
2431
  });
2325
2432
  }
2326
2433
  //#endregion
2434
+ //#region src/actions/db-action-input-form.decorator.ts
2435
+ /**
2436
+ * Parameter decorator that injects the `input` field of the action request
2437
+ * envelope (`{ ids?, input? }`) into the handler.
2438
+ *
2439
+ * Pairs the resolved value with two pieces of param-level metadata:
2440
+ *
2441
+ * 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
2442
+ * name, consumed by {@link discoverActions} to:
2443
+ * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
2444
+ * - register the type in the controller's form registry so
2445
+ * `GET /meta/form/:name` can serve the serialized schema.
2446
+ * 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
2447
+ * hook any atscript-aware Moost pipe can read without knowing about the
2448
+ * moost-db-specific key.
2449
+ *
2450
+ * Validation is intentionally *not* performed here. To validate `input`
2451
+ * against `FormType`, install an atscript validator pipe globally
2452
+ * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
2453
+ * `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
2454
+ *
2455
+ * Only one `@InputForm()` per action is supported. To collect multiple
2456
+ * structured inputs, compose them into a single `.as` interface and pass an
2457
+ * array form on the field whose user-facing intent is "list of items".
2458
+ *
2459
+ * @param formType A compiled `.as` interface class (carries `.validator()`,
2460
+ * `.metadata`, etc.).
2461
+ */
2462
+ function InputForm(formType) {
2463
+ const mate = getMoostMate();
2464
+ const meta = {
2465
+ type: formType,
2466
+ name: formType.name
2467
+ };
2468
+ return ApplyDecorators(mate.decorate(MOOST_DB_ACTION_INPUT_FORM, meta), mate.decorate(MOOST_ATSCRIPT_TYPE, formType), Resolve(async () => current().get(dbActionInputSlot), "dbActionInputForm"));
2469
+ }
2470
+ //#endregion
2327
2471
  //#region src/actions/per-row.ts
2328
2472
  /**
2329
2473
  * Lift a per-row predicate into the batch shape required by
@@ -2339,4 +2483,4 @@ function classLevelActions(dict, forcedLevel) {
2339
2483
  */
2340
2484
  const perRow = (fn) => (rows) => rows.map(fn);
2341
2485
  //#endregion
2342
- export { ActionDisabledError, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, discoverActions, perRow, useDbActionId, useDbActionIds, useDbActionRow, useDbActionRows, validationErrorTransform };
2486
+ export { ActionDisabledError, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, InputForm, MOOST_ATSCRIPT_TYPE, MOOST_DB_ACTION_INPUT_FORM, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, dbActionBodySlot, dbActionInputSlot, discoverActions, getControllerFormType, perRow, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -58,7 +58,7 @@
58
58
  "@wooksjs/event-core": "^0.7.10",
59
59
  "@wooksjs/http-body": "^0.7.10",
60
60
  "moost": "^0.6.8",
61
- "@atscript/db": "^0.1.60"
61
+ "@atscript/db": "^0.1.62"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",