@atscript/moost-db 0.1.119 → 0.1.121

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
@@ -922,7 +922,7 @@ function normalizeOptions(prefixOrOptions) {
922
922
  * Builds the shared binding metadata for a decorator invocation: classifies
923
923
  * the binding form, computes the static route prefix, and packages a uniform
924
924
  * `resolve()` used by both the DI provide factory and the base controller's
925
- * `super(undefined, app)` fallback.
925
+ * `super(app)` fallback.
926
926
  */
927
927
  function buildBinding(binding, options, decoratorName) {
928
928
  if ((0, _atscript_typescript_utils.isAnnotatedType)(binding)) {
@@ -1041,7 +1041,7 @@ function findReadableBinding(ctor) {
1041
1041
  *
1042
1042
  * Used by {@link AsDbReadableController}'s constructor when `readable` is
1043
1043
  * `undefined` — i.e. a subclass with its own constructor called
1044
- * `super(undefined, app)` instead of forwarding an injected instance.
1044
+ * `super(app)` instead of forwarding an injected instance.
1045
1045
  */
1046
1046
  function resolveBoundReadable(ctor) {
1047
1047
  const binding = findReadableBinding(ctor);
@@ -1096,7 +1096,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1096
1096
  _quantityRefByPath;
1097
1097
  /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
1098
1098
  _adapterNonFilterable;
1099
- constructor(readable, app) {
1099
+ constructor(app, readable) {
1100
1100
  const resolved = readable ?? resolveBoundReadable(new.target);
1101
1101
  super(resolved.type, resolved.tableName, app, resolved.isView ? "view" : "table");
1102
1102
  this.readable = resolved;
@@ -1758,9 +1758,9 @@ __decorate([
1758
1758
  ], AsDbReadableController.prototype, "getOneComposite", null);
1759
1759
  AsDbReadableController = __decorate([
1760
1760
  (0, moost.Inherit)(),
1761
- __decorateParam(0, (0, moost.Inject)(READABLE_DEF)),
1762
- __decorateParam(0, (0, moost.Optional)()),
1763
- __decorateMetadata("design:paramtypes", [Object, typeof moost.Moost === "undefined" ? Object : moost.Moost])
1761
+ __decorateParam(1, (0, moost.Inject)(READABLE_DEF)),
1762
+ __decorateParam(1, (0, moost.Optional)()),
1763
+ __decorateMetadata("design:paramtypes", [typeof moost.Moost === "undefined" ? Object : moost.Moost, Object])
1764
1764
  ], AsDbReadableController);
1765
1765
  registerAsDbReadableController(AsDbReadableController);
1766
1766
  //#endregion
@@ -1793,8 +1793,8 @@ let AsDbController = class AsDbController extends AsDbReadableController {
1793
1793
  get table() {
1794
1794
  return this.readable;
1795
1795
  }
1796
- constructor(table, app) {
1797
- super(table, app);
1796
+ constructor(app, table) {
1797
+ super(app, table);
1798
1798
  }
1799
1799
  buildCrud() {
1800
1800
  return {
@@ -1956,9 +1956,9 @@ __decorate([
1956
1956
  ], AsDbController.prototype, "removeComposite", null);
1957
1957
  AsDbController = __decorate([
1958
1958
  (0, moost.Inherit)(),
1959
- __decorateParam(0, (0, moost.Inject)(TABLE_DEF)),
1960
- __decorateParam(0, (0, moost.Optional)()),
1961
- __decorateMetadata("design:paramtypes", [Object, typeof moost.Moost === "undefined" ? Object : moost.Moost])
1959
+ __decorateParam(1, (0, moost.Inject)(TABLE_DEF)),
1960
+ __decorateParam(1, (0, moost.Optional)()),
1961
+ __decorateMetadata("design:paramtypes", [typeof moost.Moost === "undefined" ? Object : moost.Moost, Object])
1962
1962
  ], AsDbController);
1963
1963
  //#endregion
1964
1964
  //#region src/as-value-help.controller.ts
@@ -2802,40 +2802,82 @@ function classLevelActions(dict, forcedLevel) {
2802
2802
  }
2803
2803
  //#endregion
2804
2804
  //#region src/actions/db-action-input-form.decorator.ts
2805
+ /** Human-readable description of a rejected form candidate for the error hint. */
2806
+ function describeCandidate(candidate) {
2807
+ if (candidate === void 0) return "undefined — no reflected type (emitDecoratorMetadata off, or a circular import)";
2808
+ if (candidate === Object) return "Object — reflection lost the type (interface/union annotation, or the class was imported with `import type`)";
2809
+ if (typeof candidate === "function") return `class ${candidate.name ?? "<anonymous>"} — not a compiled .as interface`;
2810
+ return typeof candidate;
2811
+ }
2805
2812
  /**
2806
2813
  * Parameter decorator that injects the `input` field of the action request
2807
- * envelope (`{ ids?, input? }`) into the handler.
2814
+ * envelope (`{ ids?, input? }`) into the handler, **validated** against the
2815
+ * action's form.
2816
+ *
2817
+ * The form type may be passed explicitly — `@InputForm(MyForm)` — or inferred
2818
+ * from the parameter's reflected design type when omitted:
2819
+ *
2820
+ * ```ts
2821
+ * @Post("actions/comment")
2822
+ * @DbAction("comment", { label: "Comment" })
2823
+ * async comment(@InputForm() input: CommentForm) { ... }
2824
+ * ```
2808
2825
  *
2809
- * Pairs the resolved value with two pieces of param-level metadata:
2826
+ * Inference reads `design:paramtypes`, so it only works when the parameter is
2827
+ * annotated with the compiled `.as` class through a VALUE import — an
2828
+ * `import type` elides the class and reflection yields `Object`. When the
2829
+ * reflected type is unusable, decoration **throws** (fail-loud, at import
2830
+ * time) instead of silently serving an action without a form. The explicit
2831
+ * argument sidesteps reflection entirely and always wins over the annotation.
2810
2832
  *
2811
- * 1. `atscript_db_action_input_form` — the compiled `.as` class plus its
2812
- * name, consumed by `discoverActions` to:
2813
- * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
2814
- * - register the type in the controller's form registry so
2815
- * `GET /meta/form/:name` can serve the serialized schema.
2816
- * 2. `atscript_type` — just the type ref, providing a generic hook any
2817
- * atscript-aware Moost pipe can read without knowing about the
2818
- * moost-db-specific key.
2833
+ * The resolved form drives three things:
2819
2834
  *
2820
- * Validation is intentionally *not* performed here. To validate `input`
2821
- * against `FormType`, install an atscript validator pipe globally
2822
- * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
2823
- * `atscript_type` off the param and runs `FormType.validator()`.
2835
+ * 1. `atscript_db_action_input_form` — `{ type, name }` param metadata,
2836
+ * consumed by `discoverActions` to emit `inputForm: <name>` on the
2837
+ * action's `/meta` entry and to register the schema for
2838
+ * `GET /meta/form/:name`.
2839
+ * 2. `atscript_type` — a generic hook any atscript-aware Moost pipe can read
2840
+ * without knowing the moost-db-specific key.
2841
+ * 3. **Request-time validation** — the resolver runs
2842
+ * `FormType.validator(opts).validate(input ?? {})` before the handler
2843
+ * fires. A failure throws `ValidatorError`, which the controllers' own
2844
+ * `validationErrorTransform` shapes into the same structured `400`
2845
+ * envelope as strict-`ids` failures. Absent `input` is validated as `{}`
2846
+ * (all-optional forms pass; required fields produce per-field errors) and
2847
+ * the handler always receives an object, never `undefined`. Pass
2848
+ * `validatorOpts` to tune validation; an app-level `validatorPipe()` may
2849
+ * re-validate the same value harmlessly.
2824
2850
  *
2825
2851
  * Only one `@InputForm()` per action is supported. To collect multiple
2826
2852
  * structured inputs, compose them into a single `.as` interface and pass an
2827
2853
  * array form on the field whose user-facing intent is "list of items".
2828
2854
  *
2829
2855
  * @param formType A compiled `.as` interface class (carries `.validator()`,
2830
- * `.metadata`, etc.).
2856
+ * `.metadata`, etc.). Optional — inferred from the param's
2857
+ * reflected type when omitted.
2858
+ * @param validatorOpts Options forwarded to `formType.validator()`.
2831
2859
  */
2832
- function InputForm(formType) {
2860
+ function InputForm(formType, validatorOpts) {
2833
2861
  const mate = (0, moost.getMoostMate)();
2834
- const meta = {
2835
- type: formType,
2836
- name: formType.name
2837
- };
2838
- return (0, moost.ApplyDecorators)(mate.decorate("atscript_db_action_input_form", meta), mate.decorate("atscript_type", formType), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(dbActionInputSlot), "dbActionInputForm"));
2862
+ let resolved;
2863
+ return (0, moost.ApplyDecorators)(mate.decorate((paramMeta) => {
2864
+ const candidate = formType ?? paramMeta.type;
2865
+ if (!(0, _atscript_typescript_utils.isAnnotatedType)(candidate) || typeof candidate.name !== "string" || typeof candidate.validator !== "function") throw new Error(`@InputForm(${formType ? "…" : ""}) could not resolve the form type: expected a compiled .as interface, got ${describeCandidate(candidate)}. Annotate the parameter with the compiled .as class via a VALUE import, or pass the form explicitly: @InputForm(MyForm).`);
2866
+ resolved = candidate;
2867
+ const meta = {
2868
+ type: resolved,
2869
+ name: resolved.name
2870
+ };
2871
+ return {
2872
+ ...paramMeta,
2873
+ atscript_db_action_input_form: meta,
2874
+ atscript_type: resolved
2875
+ };
2876
+ }), (0, moost.Resolve)(async () => {
2877
+ const input = await (0, _wooksjs_event_core.current)().get(dbActionInputSlot) ?? {};
2878
+ if (resolved) resolved.validator(validatorOpts).validate(input);
2879
+ return input;
2880
+ }, "dbActionInputForm"));
2839
2881
  }
2840
2882
  //#endregion
2841
2883
  //#region src/actions/per-row.ts
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as resolveDbSpace, n as clearDbSpaces, r as provideDbSpace, t as DEFAULT_DB_SPACE } from "./db-space-registry-CWpYwZ4R.cjs";
2
- import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
2
+ import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, TValidatorOptions, Validator } from "@atscript/typescript/utils";
3
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";
4
4
  import { HttpError } from "@moostjs/event-http";
5
5
  import { Mate, Moost, TConsoleBase, TMateParamMeta, TMoostMetadata } from "moost";
@@ -181,7 +181,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
181
181
  private readonly _quantityRefByPath;
182
182
  /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
183
183
  private readonly _adapterNonFilterable;
184
- constructor(readable: AtscriptDbReadable<T> | undefined, app: Moost);
184
+ constructor(app: Moost, readable?: AtscriptDbReadable<T>);
185
185
  private _collectAdapterNonFilterable;
186
186
  private _collectQuantityRefs;
187
187
  private _buildGates;
@@ -323,7 +323,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
323
323
  declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsDbReadableController<T, DataType> {
324
324
  /** Reference to the underlying table (typed for write access). */
325
325
  protected get table(): AtscriptDbTable<T>;
326
- constructor(table: AtscriptDbTable<T> | undefined, app: Moost);
326
+ constructor(app: Moost, table?: AtscriptDbTable<T>);
327
327
  protected buildCrud(): TCrudPermissions$1;
328
328
  /**
329
329
  * Intercepts write operations. Return `undefined` to abort.
@@ -699,7 +699,7 @@ declare module "moost" {
699
699
  * Class-level readable-binding descriptor written by `@TableController` /
700
700
  * `@ReadableController` / `@ViewController`. One uniform `resolve()` backs
701
701
  * both the DI provide factory and the base controller's
702
- * `super(undefined, app)` fallback; `model` additionally feeds
702
+ * `super(app)` fallback; `model` additionally feeds
703
703
  * `assertExposed()`.
704
704
  */
705
705
  interface TReadableBindingMeta {
@@ -903,7 +903,7 @@ declare function findReadableBinding(ctor: Function | undefined): TReadableBindi
903
903
  *
904
904
  * Used by {@link AsDbReadableController}'s constructor when `readable` is
905
905
  * `undefined` — i.e. a subclass with its own constructor called
906
- * `super(undefined, app)` instead of forwarding an injected instance.
906
+ * `super(app)` instead of forwarding an injected instance.
907
907
  */
908
908
  declare function resolveBoundReadable(ctor: Function | undefined): AtscriptDbReadable<any>;
909
909
  //#endregion
@@ -1056,34 +1056,55 @@ declare function DbRowsActions<TRow = unknown, const D extends Record<string, un
1056
1056
  //#region src/actions/db-action-input-form.decorator.d.ts
1057
1057
  /**
1058
1058
  * Parameter decorator that injects the `input` field of the action request
1059
- * envelope (`{ ids?, input? }`) into the handler.
1059
+ * envelope (`{ ids?, input? }`) into the handler, **validated** against the
1060
+ * action's form.
1060
1061
  *
1061
- * Pairs the resolved value with two pieces of param-level metadata:
1062
+ * The form type may be passed explicitly — `@InputForm(MyForm)` — or inferred
1063
+ * from the parameter's reflected design type when omitted:
1062
1064
  *
1063
- * 1. `atscript_db_action_input_form` — the compiled `.as` class plus its
1064
- * name, consumed by `discoverActions` to:
1065
- * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
1066
- * - register the type in the controller's form registry so
1067
- * `GET /meta/form/:name` can serve the serialized schema.
1068
- * 2. `atscript_type` — just the type ref, providing a generic hook any
1069
- * atscript-aware Moost pipe can read without knowing about the
1070
- * moost-db-specific key.
1065
+ * ```ts
1066
+ * @Post("actions/comment")
1067
+ * @DbAction("comment", { label: "Comment" })
1068
+ * async comment(@InputForm() input: CommentForm) { ... }
1069
+ * ```
1070
+ *
1071
+ * Inference reads `design:paramtypes`, so it only works when the parameter is
1072
+ * annotated with the compiled `.as` class through a VALUE import — an
1073
+ * `import type` elides the class and reflection yields `Object`. When the
1074
+ * reflected type is unusable, decoration **throws** (fail-loud, at import
1075
+ * time) instead of silently serving an action without a form. The explicit
1076
+ * argument sidesteps reflection entirely and always wins over the annotation.
1077
+ *
1078
+ * The resolved form drives three things:
1071
1079
  *
1072
- * Validation is intentionally *not* performed here. To validate `input`
1073
- * against `FormType`, install an atscript validator pipe globally
1074
- * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
1075
- * `atscript_type` off the param and runs `FormType.validator()`.
1080
+ * 1. `atscript_db_action_input_form` — `{ type, name }` param metadata,
1081
+ * consumed by `discoverActions` to emit `inputForm: <name>` on the
1082
+ * action's `/meta` entry and to register the schema for
1083
+ * `GET /meta/form/:name`.
1084
+ * 2. `atscript_type` — a generic hook any atscript-aware Moost pipe can read
1085
+ * without knowing the moost-db-specific key.
1086
+ * 3. **Request-time validation** — the resolver runs
1087
+ * `FormType.validator(opts).validate(input ?? {})` before the handler
1088
+ * fires. A failure throws `ValidatorError`, which the controllers' own
1089
+ * `validationErrorTransform` shapes into the same structured `400`
1090
+ * envelope as strict-`ids` failures. Absent `input` is validated as `{}`
1091
+ * (all-optional forms pass; required fields produce per-field errors) and
1092
+ * the handler always receives an object, never `undefined`. Pass
1093
+ * `validatorOpts` to tune validation; an app-level `validatorPipe()` may
1094
+ * re-validate the same value harmlessly.
1076
1095
  *
1077
1096
  * Only one `@InputForm()` per action is supported. To collect multiple
1078
1097
  * structured inputs, compose them into a single `.as` interface and pass an
1079
1098
  * array form on the field whose user-facing intent is "list of items".
1080
1099
  *
1081
1100
  * @param formType A compiled `.as` interface class (carries `.validator()`,
1082
- * `.metadata`, etc.).
1101
+ * `.metadata`, etc.). Optional — inferred from the param's
1102
+ * reflected type when omitted.
1103
+ * @param validatorOpts Options forwarded to `formType.validator()`.
1083
1104
  */
1084
1105
  declare function InputForm<T extends TAtscriptAnnotatedType & {
1085
1106
  readonly name: string;
1086
- }>(formType: T): ParameterDecorator;
1107
+ }>(formType?: T, validatorOpts?: Partial<TValidatorOptions>): ParameterDecorator;
1087
1108
  //#endregion
1088
1109
  //#region src/actions/discover.d.ts
1089
1110
  /**
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as resolveDbSpace, n as clearDbSpaces, r as provideDbSpace, t as DEFAULT_DB_SPACE } from "./db-space-registry-CKR6G-hi.mjs";
2
- import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
2
+ import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, TValidatorOptions, Validator } from "@atscript/typescript/utils";
3
3
  import { HttpError } from "@moostjs/event-http";
4
4
  import { Mate, Moost, TConsoleBase, TMateParamMeta, TMoostMetadata } from "moost";
5
5
  import { parseUrl } from "@uniqu/url";
@@ -181,7 +181,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
181
181
  private readonly _quantityRefByPath;
182
182
  /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
183
183
  private readonly _adapterNonFilterable;
184
- constructor(readable: AtscriptDbReadable<T> | undefined, app: Moost);
184
+ constructor(app: Moost, readable?: AtscriptDbReadable<T>);
185
185
  private _collectAdapterNonFilterable;
186
186
  private _collectQuantityRefs;
187
187
  private _buildGates;
@@ -323,7 +323,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
323
323
  declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsDbReadableController<T, DataType> {
324
324
  /** Reference to the underlying table (typed for write access). */
325
325
  protected get table(): AtscriptDbTable<T>;
326
- constructor(table: AtscriptDbTable<T> | undefined, app: Moost);
326
+ constructor(app: Moost, table?: AtscriptDbTable<T>);
327
327
  protected buildCrud(): TCrudPermissions$1;
328
328
  /**
329
329
  * Intercepts write operations. Return `undefined` to abort.
@@ -699,7 +699,7 @@ declare module "moost" {
699
699
  * Class-level readable-binding descriptor written by `@TableController` /
700
700
  * `@ReadableController` / `@ViewController`. One uniform `resolve()` backs
701
701
  * both the DI provide factory and the base controller's
702
- * `super(undefined, app)` fallback; `model` additionally feeds
702
+ * `super(app)` fallback; `model` additionally feeds
703
703
  * `assertExposed()`.
704
704
  */
705
705
  interface TReadableBindingMeta {
@@ -903,7 +903,7 @@ declare function findReadableBinding(ctor: Function | undefined): TReadableBindi
903
903
  *
904
904
  * Used by {@link AsDbReadableController}'s constructor when `readable` is
905
905
  * `undefined` — i.e. a subclass with its own constructor called
906
- * `super(undefined, app)` instead of forwarding an injected instance.
906
+ * `super(app)` instead of forwarding an injected instance.
907
907
  */
908
908
  declare function resolveBoundReadable(ctor: Function | undefined): AtscriptDbReadable<any>;
909
909
  //#endregion
@@ -1056,34 +1056,55 @@ declare function DbRowsActions<TRow = unknown, const D extends Record<string, un
1056
1056
  //#region src/actions/db-action-input-form.decorator.d.ts
1057
1057
  /**
1058
1058
  * Parameter decorator that injects the `input` field of the action request
1059
- * envelope (`{ ids?, input? }`) into the handler.
1059
+ * envelope (`{ ids?, input? }`) into the handler, **validated** against the
1060
+ * action's form.
1060
1061
  *
1061
- * Pairs the resolved value with two pieces of param-level metadata:
1062
+ * The form type may be passed explicitly — `@InputForm(MyForm)` — or inferred
1063
+ * from the parameter's reflected design type when omitted:
1062
1064
  *
1063
- * 1. `atscript_db_action_input_form` — the compiled `.as` class plus its
1064
- * name, consumed by `discoverActions` to:
1065
- * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
1066
- * - register the type in the controller's form registry so
1067
- * `GET /meta/form/:name` can serve the serialized schema.
1068
- * 2. `atscript_type` — just the type ref, providing a generic hook any
1069
- * atscript-aware Moost pipe can read without knowing about the
1070
- * moost-db-specific key.
1065
+ * ```ts
1066
+ * @Post("actions/comment")
1067
+ * @DbAction("comment", { label: "Comment" })
1068
+ * async comment(@InputForm() input: CommentForm) { ... }
1069
+ * ```
1070
+ *
1071
+ * Inference reads `design:paramtypes`, so it only works when the parameter is
1072
+ * annotated with the compiled `.as` class through a VALUE import — an
1073
+ * `import type` elides the class and reflection yields `Object`. When the
1074
+ * reflected type is unusable, decoration **throws** (fail-loud, at import
1075
+ * time) instead of silently serving an action without a form. The explicit
1076
+ * argument sidesteps reflection entirely and always wins over the annotation.
1077
+ *
1078
+ * The resolved form drives three things:
1071
1079
  *
1072
- * Validation is intentionally *not* performed here. To validate `input`
1073
- * against `FormType`, install an atscript validator pipe globally
1074
- * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
1075
- * `atscript_type` off the param and runs `FormType.validator()`.
1080
+ * 1. `atscript_db_action_input_form` — `{ type, name }` param metadata,
1081
+ * consumed by `discoverActions` to emit `inputForm: <name>` on the
1082
+ * action's `/meta` entry and to register the schema for
1083
+ * `GET /meta/form/:name`.
1084
+ * 2. `atscript_type` — a generic hook any atscript-aware Moost pipe can read
1085
+ * without knowing the moost-db-specific key.
1086
+ * 3. **Request-time validation** — the resolver runs
1087
+ * `FormType.validator(opts).validate(input ?? {})` before the handler
1088
+ * fires. A failure throws `ValidatorError`, which the controllers' own
1089
+ * `validationErrorTransform` shapes into the same structured `400`
1090
+ * envelope as strict-`ids` failures. Absent `input` is validated as `{}`
1091
+ * (all-optional forms pass; required fields produce per-field errors) and
1092
+ * the handler always receives an object, never `undefined`. Pass
1093
+ * `validatorOpts` to tune validation; an app-level `validatorPipe()` may
1094
+ * re-validate the same value harmlessly.
1076
1095
  *
1077
1096
  * Only one `@InputForm()` per action is supported. To collect multiple
1078
1097
  * structured inputs, compose them into a single `.as` interface and pass an
1079
1098
  * array form on the field whose user-facing intent is "list of items".
1080
1099
  *
1081
1100
  * @param formType A compiled `.as` interface class (carries `.validator()`,
1082
- * `.metadata`, etc.).
1101
+ * `.metadata`, etc.). Optional — inferred from the param's
1102
+ * reflected type when omitted.
1103
+ * @param validatorOpts Options forwarded to `formType.validator()`.
1083
1104
  */
1084
1105
  declare function InputForm<T extends TAtscriptAnnotatedType & {
1085
1106
  readonly name: string;
1086
- }>(formType: T): ParameterDecorator;
1107
+ }>(formType?: T, validatorOpts?: Partial<TValidatorOptions>): ParameterDecorator;
1087
1108
  //#endregion
1088
1109
  //#region src/actions/discover.d.ts
1089
1110
  /**
package/dist/index.mjs CHANGED
@@ -921,7 +921,7 @@ function normalizeOptions(prefixOrOptions) {
921
921
  * Builds the shared binding metadata for a decorator invocation: classifies
922
922
  * the binding form, computes the static route prefix, and packages a uniform
923
923
  * `resolve()` used by both the DI provide factory and the base controller's
924
- * `super(undefined, app)` fallback.
924
+ * `super(app)` fallback.
925
925
  */
926
926
  function buildBinding(binding, options, decoratorName) {
927
927
  if (isAnnotatedType(binding)) {
@@ -1040,7 +1040,7 @@ function findReadableBinding(ctor) {
1040
1040
  *
1041
1041
  * Used by {@link AsDbReadableController}'s constructor when `readable` is
1042
1042
  * `undefined` — i.e. a subclass with its own constructor called
1043
- * `super(undefined, app)` instead of forwarding an injected instance.
1043
+ * `super(app)` instead of forwarding an injected instance.
1044
1044
  */
1045
1045
  function resolveBoundReadable(ctor) {
1046
1046
  const binding = findReadableBinding(ctor);
@@ -1095,7 +1095,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1095
1095
  _quantityRefByPath;
1096
1096
  /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
1097
1097
  _adapterNonFilterable;
1098
- constructor(readable, app) {
1098
+ constructor(app, readable) {
1099
1099
  const resolved = readable ?? resolveBoundReadable(new.target);
1100
1100
  super(resolved.type, resolved.tableName, app, resolved.isView ? "view" : "table");
1101
1101
  this.readable = resolved;
@@ -1757,9 +1757,9 @@ __decorate([
1757
1757
  ], AsDbReadableController.prototype, "getOneComposite", null);
1758
1758
  AsDbReadableController = __decorate([
1759
1759
  Inherit(),
1760
- __decorateParam(0, Inject(READABLE_DEF)),
1761
- __decorateParam(0, Optional()),
1762
- __decorateMetadata("design:paramtypes", [Object, typeof Moost === "undefined" ? Object : Moost])
1760
+ __decorateParam(1, Inject(READABLE_DEF)),
1761
+ __decorateParam(1, Optional()),
1762
+ __decorateMetadata("design:paramtypes", [typeof Moost === "undefined" ? Object : Moost, Object])
1763
1763
  ], AsDbReadableController);
1764
1764
  registerAsDbReadableController(AsDbReadableController);
1765
1765
  //#endregion
@@ -1792,8 +1792,8 @@ let AsDbController = class AsDbController extends AsDbReadableController {
1792
1792
  get table() {
1793
1793
  return this.readable;
1794
1794
  }
1795
- constructor(table, app) {
1796
- super(table, app);
1795
+ constructor(app, table) {
1796
+ super(app, table);
1797
1797
  }
1798
1798
  buildCrud() {
1799
1799
  return {
@@ -1955,9 +1955,9 @@ __decorate([
1955
1955
  ], AsDbController.prototype, "removeComposite", null);
1956
1956
  AsDbController = __decorate([
1957
1957
  Inherit(),
1958
- __decorateParam(0, Inject(TABLE_DEF)),
1959
- __decorateParam(0, Optional()),
1960
- __decorateMetadata("design:paramtypes", [Object, typeof Moost === "undefined" ? Object : Moost])
1958
+ __decorateParam(1, Inject(TABLE_DEF)),
1959
+ __decorateParam(1, Optional()),
1960
+ __decorateMetadata("design:paramtypes", [typeof Moost === "undefined" ? Object : Moost, Object])
1961
1961
  ], AsDbController);
1962
1962
  //#endregion
1963
1963
  //#region src/as-value-help.controller.ts
@@ -2801,40 +2801,82 @@ function classLevelActions(dict, forcedLevel) {
2801
2801
  }
2802
2802
  //#endregion
2803
2803
  //#region src/actions/db-action-input-form.decorator.ts
2804
+ /** Human-readable description of a rejected form candidate for the error hint. */
2805
+ function describeCandidate(candidate) {
2806
+ if (candidate === void 0) return "undefined — no reflected type (emitDecoratorMetadata off, or a circular import)";
2807
+ if (candidate === Object) return "Object — reflection lost the type (interface/union annotation, or the class was imported with `import type`)";
2808
+ if (typeof candidate === "function") return `class ${candidate.name ?? "<anonymous>"} — not a compiled .as interface`;
2809
+ return typeof candidate;
2810
+ }
2804
2811
  /**
2805
2812
  * Parameter decorator that injects the `input` field of the action request
2806
- * envelope (`{ ids?, input? }`) into the handler.
2813
+ * envelope (`{ ids?, input? }`) into the handler, **validated** against the
2814
+ * action's form.
2815
+ *
2816
+ * The form type may be passed explicitly — `@InputForm(MyForm)` — or inferred
2817
+ * from the parameter's reflected design type when omitted:
2818
+ *
2819
+ * ```ts
2820
+ * @Post("actions/comment")
2821
+ * @DbAction("comment", { label: "Comment" })
2822
+ * async comment(@InputForm() input: CommentForm) { ... }
2823
+ * ```
2807
2824
  *
2808
- * Pairs the resolved value with two pieces of param-level metadata:
2825
+ * Inference reads `design:paramtypes`, so it only works when the parameter is
2826
+ * annotated with the compiled `.as` class through a VALUE import — an
2827
+ * `import type` elides the class and reflection yields `Object`. When the
2828
+ * reflected type is unusable, decoration **throws** (fail-loud, at import
2829
+ * time) instead of silently serving an action without a form. The explicit
2830
+ * argument sidesteps reflection entirely and always wins over the annotation.
2809
2831
  *
2810
- * 1. `atscript_db_action_input_form` — the compiled `.as` class plus its
2811
- * name, consumed by `discoverActions` to:
2812
- * - emit `inputForm: FormType.name` on the action's `/meta` entry, and
2813
- * - register the type in the controller's form registry so
2814
- * `GET /meta/form/:name` can serve the serialized schema.
2815
- * 2. `atscript_type` — just the type ref, providing a generic hook any
2816
- * atscript-aware Moost pipe can read without knowing about the
2817
- * moost-db-specific key.
2832
+ * The resolved form drives three things:
2818
2833
  *
2819
- * Validation is intentionally *not* performed here. To validate `input`
2820
- * against `FormType`, install an atscript validator pipe globally
2821
- * (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
2822
- * `atscript_type` off the param and runs `FormType.validator()`.
2834
+ * 1. `atscript_db_action_input_form` — `{ type, name }` param metadata,
2835
+ * consumed by `discoverActions` to emit `inputForm: <name>` on the
2836
+ * action's `/meta` entry and to register the schema for
2837
+ * `GET /meta/form/:name`.
2838
+ * 2. `atscript_type` — a generic hook any atscript-aware Moost pipe can read
2839
+ * without knowing the moost-db-specific key.
2840
+ * 3. **Request-time validation** — the resolver runs
2841
+ * `FormType.validator(opts).validate(input ?? {})` before the handler
2842
+ * fires. A failure throws `ValidatorError`, which the controllers' own
2843
+ * `validationErrorTransform` shapes into the same structured `400`
2844
+ * envelope as strict-`ids` failures. Absent `input` is validated as `{}`
2845
+ * (all-optional forms pass; required fields produce per-field errors) and
2846
+ * the handler always receives an object, never `undefined`. Pass
2847
+ * `validatorOpts` to tune validation; an app-level `validatorPipe()` may
2848
+ * re-validate the same value harmlessly.
2823
2849
  *
2824
2850
  * Only one `@InputForm()` per action is supported. To collect multiple
2825
2851
  * structured inputs, compose them into a single `.as` interface and pass an
2826
2852
  * array form on the field whose user-facing intent is "list of items".
2827
2853
  *
2828
2854
  * @param formType A compiled `.as` interface class (carries `.validator()`,
2829
- * `.metadata`, etc.).
2855
+ * `.metadata`, etc.). Optional — inferred from the param's
2856
+ * reflected type when omitted.
2857
+ * @param validatorOpts Options forwarded to `formType.validator()`.
2830
2858
  */
2831
- function InputForm(formType) {
2859
+ function InputForm(formType, validatorOpts) {
2832
2860
  const mate = getMoostMate();
2833
- const meta = {
2834
- type: formType,
2835
- name: formType.name
2836
- };
2837
- return ApplyDecorators(mate.decorate("atscript_db_action_input_form", meta), mate.decorate("atscript_type", formType), Resolve(async () => current().get(dbActionInputSlot), "dbActionInputForm"));
2861
+ let resolved;
2862
+ return ApplyDecorators(mate.decorate((paramMeta) => {
2863
+ const candidate = formType ?? paramMeta.type;
2864
+ if (!isAnnotatedType(candidate) || typeof candidate.name !== "string" || typeof candidate.validator !== "function") throw new Error(`@InputForm(${formType ? "…" : ""}) could not resolve the form type: expected a compiled .as interface, got ${describeCandidate(candidate)}. Annotate the parameter with the compiled .as class via a VALUE import, or pass the form explicitly: @InputForm(MyForm).`);
2865
+ resolved = candidate;
2866
+ const meta = {
2867
+ type: resolved,
2868
+ name: resolved.name
2869
+ };
2870
+ return {
2871
+ ...paramMeta,
2872
+ atscript_db_action_input_form: meta,
2873
+ atscript_type: resolved
2874
+ };
2875
+ }), Resolve(async () => {
2876
+ const input = await current().get(dbActionInputSlot) ?? {};
2877
+ if (resolved) resolved.validator(validatorOpts).validate(input);
2878
+ return input;
2879
+ }, "dbActionInputForm"));
2838
2880
  }
2839
2881
  //#endregion
2840
2882
  //#region src/actions/per-row.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.119",
3
+ "version": "0.1.121",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -44,27 +44,27 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@uniqu/url": "^0.1.7",
47
- "@atscript/db-memory": "^0.1.119"
47
+ "@atscript/db-memory": "^0.1.121"
48
48
  },
49
49
  "devDependencies": {
50
- "@atscript/core": "^0.1.84",
51
- "@atscript/typescript": "^0.1.84",
52
- "@moostjs/event-http": "^0.6.32",
50
+ "@atscript/core": "^0.1.85",
51
+ "@atscript/typescript": "^0.1.85",
52
+ "@moostjs/event-http": "^0.6.33",
53
53
  "@uniqu/core": "^0.1.7",
54
- "@wooksjs/event-core": "^0.7.20",
55
- "@wooksjs/event-http": "^0.7.20",
56
- "@wooksjs/http-body": "^0.7.20",
57
- "moost": "^0.6.32",
58
- "unplugin-atscript": "^0.1.84"
54
+ "@wooksjs/event-core": "^0.7.21",
55
+ "@wooksjs/event-http": "^0.7.21",
56
+ "@wooksjs/http-body": "^0.7.21",
57
+ "moost": "^0.6.33",
58
+ "unplugin-atscript": "^0.1.85"
59
59
  },
60
60
  "peerDependencies": {
61
- "@atscript/typescript": "^0.1.84",
62
- "@moostjs/event-http": "^0.6.32",
61
+ "@atscript/typescript": "^0.1.85",
62
+ "@moostjs/event-http": "^0.6.33",
63
63
  "@uniqu/core": "^0.1.7",
64
- "@wooksjs/event-core": "^0.7.20",
65
- "@wooksjs/http-body": "^0.7.20",
66
- "moost": "^0.6.32",
67
- "@atscript/db": "^0.1.119"
64
+ "@wooksjs/event-core": "^0.7.21",
65
+ "@wooksjs/http-body": "^0.7.21",
66
+ "moost": "^0.6.33",
67
+ "@atscript/db": "^0.1.121"
68
68
  },
69
69
  "scripts": {
70
70
  "postinstall": "asc -f dts",