@atscript/moost-db 0.1.61 → 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
@@ -213,6 +213,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
213
213
  const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
214
214
  const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
215
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
+ /**
216
231
  * Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
217
232
  * read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
218
233
  * fields win), and write it back. `name` is empty until `@DbAction` provides
@@ -235,6 +250,8 @@ function scanParamLevel(params) {
235
250
  let multi = false;
236
251
  let hasRowParam = false;
237
252
  let hasBody = false;
253
+ let inputForm;
254
+ let hasDuplicateInputForm = false;
238
255
  for (const p of params) {
239
256
  const kind = p[MOOST_DB_ACTION_PARAM];
240
257
  if (kind === "id") single = true;
@@ -248,13 +265,18 @@ function scanParamLevel(params) {
248
265
  hasRowParam = true;
249
266
  }
250
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;
251
271
  }
252
272
  return {
253
273
  level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
254
274
  single,
255
275
  multi,
256
276
  hasRowParam,
257
- hasBody
277
+ hasBody,
278
+ inputForm,
279
+ hasDuplicateInputForm
258
280
  };
259
281
  }
260
282
  //#endregion
@@ -271,6 +293,34 @@ const OPTIONAL_FIELDS = [
271
293
  ];
272
294
  const actionsCache = /* @__PURE__ */ new WeakMap();
273
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
+ }
274
324
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
275
325
  function discoverActions(controllerCtor, app, logger) {
276
326
  const cached = actionsCache.get(controllerCtor);
@@ -356,6 +406,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
356
406
  processor: "backend",
357
407
  value: path
358
408
  };
409
+ if (levelInfer.inputForm) {
410
+ if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
411
+ info.inputForm = levelInfer.inputForm.name;
412
+ }
359
413
  emitInfo(info, action.opts);
360
414
  seen.add(action.name);
361
415
  out.push({
@@ -370,10 +424,12 @@ function inferMethodLevel(params, actionName, logger) {
370
424
  logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
371
425
  return null;
372
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.`);
373
428
  return {
374
429
  level: scan.level,
375
430
  bodyConflict: scan.hasBody && scan.level !== "table",
376
- hasRowParam: scan.hasRowParam
431
+ hasRowParam: scan.hasRowParam,
432
+ inputForm: scan.inputForm
377
433
  };
378
434
  }
379
435
  function collectClassActions(ctor, logger, out, seen) {
@@ -484,6 +540,13 @@ function __decorate(decorators, target, key, desc) {
484
540
  return c > 3 && r && Object.defineProperty(target, key, r), r;
485
541
  }
486
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
487
550
  //#region src/as-readable.controller.ts
488
551
  var _ref$4;
489
552
  let AsReadableController = class AsReadableController {
@@ -499,6 +562,8 @@ let AsReadableController = class AsReadableController {
499
562
  _serializedType;
500
563
  /** Cached full meta response (computed lazily on first meta() call). */
501
564
  _metaResponse;
565
+ /** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
566
+ _formSchemas = /* @__PURE__ */ new Map();
502
567
  constructor(boundType, controllerName, app, kindTag = "readable") {
503
568
  this.boundType = boundType;
504
569
  this.controllerName = controllerName;
@@ -641,6 +706,25 @@ let AsReadableController = class AsReadableController {
641
706
  return this.applyMetaOverlay(this._metaResponse);
642
707
  }
643
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
+ /**
644
728
  * Builds the `/meta` payload. Override in subclasses to populate source-specific
645
729
  * fields. Subclasses that fully replace the envelope must call
646
730
  * {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
@@ -694,6 +778,13 @@ __decorate([
694
778
  __decorateMetadata("design:paramtypes", []),
695
779
  __decorateMetadata("design:returntype", Promise)
696
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);
697
788
  AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
698
789
  Object,
699
790
  String,
@@ -856,13 +947,6 @@ const QUERY_CONTROLS = [
856
947
  const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
857
948
  const ONE_CONTROLS = dtoControls(GetOneControlsDto);
858
949
  //#endregion
859
- //#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
860
- function __decorateParam(paramIndex, decorator) {
861
- return function(target, key) {
862
- decorator(target, key, paramIndex);
863
- };
864
- }
865
- //#endregion
866
950
  //#region src/as-db-readable.controller.ts
867
951
  var _ref$3, _ref2$2;
868
952
  let AsDbReadableController = class AsDbReadableController extends AsReadableController {
@@ -1847,6 +1931,29 @@ function readCurrentActionMeta(ctx) {
1847
1931
  return (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
1848
1932
  }
1849
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
1850
1957
  //#region src/actions/id-validation.ts
1851
1958
  const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
1852
1959
  function getSourceCache(source) {
@@ -1964,9 +2071,9 @@ function noTableError(ctx) {
1964
2071
  async function resolveValidatedId(ctx, validate) {
1965
2072
  const table = getActionTable(ctx);
1966
2073
  if (!isIdValidationSource(table)) throw noTableError(ctx);
1967
- const body = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
1968
- validate(body, table);
1969
- return body;
2074
+ const env = await ctx.get(dbActionBodySlot);
2075
+ validate(env.ids, table);
2076
+ return env.ids;
1970
2077
  }
1971
2078
  const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
1972
2079
  const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
@@ -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
@@ -716,6 +726,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
716
726
  /** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
717
727
  declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
718
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
719
761
  //#region src/actions/discover.d.ts
720
762
  /**
721
763
  * Pairs the wire-shaped `info` with the original decorator opts / dict entry,
@@ -726,6 +768,8 @@ interface TDbActionEnvelope {
726
768
  info: TDbActionInfo$1;
727
769
  raw: DbActionOpts | TDbActionsEntry;
728
770
  }
771
+ /** Lookup helper for `AsReadableController.metaForm()`. */
772
+ declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
729
773
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
730
774
  declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
731
775
  //#endregion
@@ -752,6 +796,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
752
796
  load: () => Promise<(Record<string, unknown> | undefined)[]>;
753
797
  }>;
754
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
755
887
  //#region src/actions/action-disabled-error.d.ts
756
888
  /**
757
889
  * Wire-body shape for server-side gate rejections. The `name` discriminator
@@ -812,4 +944,4 @@ declare const QUERY_CONTROLS: readonly string[];
812
944
  declare const PAGES_CONTROLS: readonly string[];
813
945
  declare const ONE_CONTROLS: readonly string[];
814
946
  //#endregion
815
- 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
@@ -716,6 +726,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
716
726
  /** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
717
727
  declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
718
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
719
761
  //#region src/actions/discover.d.ts
720
762
  /**
721
763
  * Pairs the wire-shaped `info` with the original decorator opts / dict entry,
@@ -726,6 +768,8 @@ interface TDbActionEnvelope {
726
768
  info: TDbActionInfo$1;
727
769
  raw: DbActionOpts | TDbActionsEntry;
728
770
  }
771
+ /** Lookup helper for `AsReadableController.metaForm()`. */
772
+ declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
729
773
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
730
774
  declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
731
775
  //#endregion
@@ -752,6 +796,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
752
796
  load: () => Promise<(Record<string, unknown> | undefined)[]>;
753
797
  }>;
754
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
755
887
  //#region src/actions/action-disabled-error.d.ts
756
888
  /**
757
889
  * Wire-body shape for server-side gate rejections. The `name` discriminator
@@ -812,4 +944,4 @@ declare const QUERY_CONTROLS: readonly string[];
812
944
  declare const PAGES_CONTROLS: readonly string[];
813
945
  declare const ONE_CONTROLS: readonly string[];
814
946
  //#endregion
815
- 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
@@ -212,6 +212,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
212
212
  const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
213
213
  const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
214
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
+ /**
215
230
  * Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
216
231
  * read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
217
232
  * fields win), and write it back. `name` is empty until `@DbAction` provides
@@ -234,6 +249,8 @@ function scanParamLevel(params) {
234
249
  let multi = false;
235
250
  let hasRowParam = false;
236
251
  let hasBody = false;
252
+ let inputForm;
253
+ let hasDuplicateInputForm = false;
237
254
  for (const p of params) {
238
255
  const kind = p[MOOST_DB_ACTION_PARAM];
239
256
  if (kind === "id") single = true;
@@ -247,13 +264,18 @@ function scanParamLevel(params) {
247
264
  hasRowParam = true;
248
265
  }
249
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;
250
270
  }
251
271
  return {
252
272
  level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
253
273
  single,
254
274
  multi,
255
275
  hasRowParam,
256
- hasBody
276
+ hasBody,
277
+ inputForm,
278
+ hasDuplicateInputForm
257
279
  };
258
280
  }
259
281
  //#endregion
@@ -270,6 +292,34 @@ const OPTIONAL_FIELDS = [
270
292
  ];
271
293
  const actionsCache = /* @__PURE__ */ new WeakMap();
272
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
+ }
273
323
  /** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
274
324
  function discoverActions(controllerCtor, app, logger) {
275
325
  const cached = actionsCache.get(controllerCtor);
@@ -355,6 +405,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
355
405
  processor: "backend",
356
406
  value: path
357
407
  };
408
+ if (levelInfer.inputForm) {
409
+ if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
410
+ info.inputForm = levelInfer.inputForm.name;
411
+ }
358
412
  emitInfo(info, action.opts);
359
413
  seen.add(action.name);
360
414
  out.push({
@@ -369,10 +423,12 @@ function inferMethodLevel(params, actionName, logger) {
369
423
  logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
370
424
  return null;
371
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.`);
372
427
  return {
373
428
  level: scan.level,
374
429
  bodyConflict: scan.hasBody && scan.level !== "table",
375
- hasRowParam: scan.hasRowParam
430
+ hasRowParam: scan.hasRowParam,
431
+ inputForm: scan.inputForm
376
432
  };
377
433
  }
378
434
  function collectClassActions(ctor, logger, out, seen) {
@@ -483,6 +539,13 @@ function __decorate(decorators, target, key, desc) {
483
539
  return c > 3 && r && Object.defineProperty(target, key, r), r;
484
540
  }
485
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
486
549
  //#region src/as-readable.controller.ts
487
550
  var _ref$4;
488
551
  let AsReadableController = class AsReadableController {
@@ -498,6 +561,8 @@ let AsReadableController = class AsReadableController {
498
561
  _serializedType;
499
562
  /** Cached full meta response (computed lazily on first meta() call). */
500
563
  _metaResponse;
564
+ /** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
565
+ _formSchemas = /* @__PURE__ */ new Map();
501
566
  constructor(boundType, controllerName, app, kindTag = "readable") {
502
567
  this.boundType = boundType;
503
568
  this.controllerName = controllerName;
@@ -640,6 +705,25 @@ let AsReadableController = class AsReadableController {
640
705
  return this.applyMetaOverlay(this._metaResponse);
641
706
  }
642
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
+ /**
643
727
  * Builds the `/meta` payload. Override in subclasses to populate source-specific
644
728
  * fields. Subclasses that fully replace the envelope must call
645
729
  * {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
@@ -693,6 +777,13 @@ __decorate([
693
777
  __decorateMetadata("design:paramtypes", []),
694
778
  __decorateMetadata("design:returntype", Promise)
695
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);
696
787
  AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
697
788
  Object,
698
789
  String,
@@ -855,13 +946,6 @@ const QUERY_CONTROLS = [
855
946
  const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
856
947
  const ONE_CONTROLS = dtoControls(GetOneControlsDto);
857
948
  //#endregion
858
- //#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
859
- function __decorateParam(paramIndex, decorator) {
860
- return function(target, key) {
861
- decorator(target, key, paramIndex);
862
- };
863
- }
864
- //#endregion
865
949
  //#region src/as-db-readable.controller.ts
866
950
  var _ref$3, _ref2$2;
867
951
  let AsDbReadableController = class AsDbReadableController extends AsReadableController {
@@ -1846,6 +1930,29 @@ function readCurrentActionMeta(ctx) {
1846
1930
  return getMoostMate().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
1847
1931
  }
1848
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
1849
1956
  //#region src/actions/id-validation.ts
1850
1957
  const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
1851
1958
  function getSourceCache(source) {
@@ -1963,9 +2070,9 @@ function noTableError(ctx) {
1963
2070
  async function resolveValidatedId(ctx, validate) {
1964
2071
  const table = getActionTable(ctx);
1965
2072
  if (!isIdValidationSource(table)) throw noTableError(ctx);
1966
- const body = await useBody(ctx).parseBody();
1967
- validate(body, table);
1968
- return body;
2073
+ const env = await ctx.get(dbActionBodySlot);
2074
+ validate(env.ids, table);
2075
+ return env.ids;
1969
2076
  }
1970
2077
  const dbActionIdSlot = cached((ctx) => resolveValidatedId(ctx, validateSingleId));
1971
2078
  const dbActionIdsSlot = cached(async (ctx) => {
@@ -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.61",
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.61"
61
+ "@atscript/db": "^0.1.62"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",