@dphonys/nuxt-handler-validation 0.1.1 → 0.2.0

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/README.md CHANGED
@@ -67,7 +67,8 @@ export default defineValidatedEventHandler(
67
67
  - **Mix libraries freely.** zod and valibot can sit in one declaration, or even
68
68
  in one composed tuple. Async schemas are supported; the wrapper awaits them.
69
69
  - **Your return type flows to Nitro's typed routes unchanged.** The wrapper
70
- returns a plain h3 `EventHandler`, so `$fetch('/api/users/1')` infers the
70
+ returns a `ValidatedEventHandler` - still assignable to h3's `EventHandler`,
71
+ carrying the `RequestInput` brand - so `$fetch('/api/users/1')` infers the
71
72
  response exactly as it would with `defineEventHandler`. Nothing to unwrap.
72
73
  - `defineValidatedEventHandler` and `recognizeValidationError` are
73
74
  **auto-imported inside `server/`**, the same ambient position as
@@ -388,15 +389,26 @@ such as `handlerValidation: { channelToken: 'x' }` is a compile error.
388
389
  Runtime, from `@dphonys/nuxt-handler-validation/server`, both auto-imported
389
390
  inside `server/`:
390
391
 
391
- | Export | Role |
392
- | ----------------------------------------------- | --------------------------------------------------------------------------------------- |
393
- | `defineValidatedEventHandler({ validate }, fn)` | The wrapper. One signature. Returns a plain h3 `EventHandler`. |
394
- | `recognizeValidationError(error)` | Observability predicate, process-side only. Returns `ValidationErrorData \| undefined`. |
392
+ | Export | Role |
393
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------------- |
394
+ | `defineValidatedEventHandler({ validate }, fn)` | The wrapper. One signature. Returns a `ValidatedEventHandler`, an h3 `EventHandler` subtype. |
395
+ | `recognizeValidationError(error)` | Observability predicate, process-side only. Returns `ValidationErrorData \| undefined`. |
395
396
 
396
397
  Types, from `@dphonys/nuxt-handler-validation/types` - type-only, safe to
397
398
  import from app code: `ValidationSchemas`, `SourceSchemas`, `ValidationSource`,
398
399
  `ValidatedContext<S>`, `SourceValue<T>`, `MergedOutput<T>`, `OutputOf<S>`,
399
- `ValidationIssue`, `ValidationErrorData`.
400
+ `ValidationIssue`, `ValidationErrorData`, and the request-input family:
401
+
402
+ | Type | Role |
403
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------- |
404
+ | `InputOf<S>` | A schema's input - what the client sends, before transforms. |
405
+ | `MergedInput<T>` | A composed tuple's input: the intersection of its element inputs, flattened to one record. |
406
+ | `SourceInput<T>` | One slot's input - a lone schema's input, or the tuple's intersection. |
407
+ | `RequestInput<S>` | The request input: keys are the declared sources, values what the client sends for each. |
408
+ | `ValidatedEventHandler` | What `defineValidatedEventHandler` returns: an h3 `EventHandler` carrying its `RequestInput` as a brand. |
409
+ | `RequestInputOfHandler<T>` | Reads that brand off a handler type; `never` for `any` and for handlers this package did not produce. |
410
+ | `ValidationSchemasGuard<S>` | The compile-time guard behind the declaration diagnostics above. |
411
+ | `ValidationDeclarationError<Msg>` | The sentence-shaped type those diagnostics surface. |
400
412
 
401
413
  **On the name.** `defineValidatedEventHandler` mirrors the _current_ vanilla
402
414
  `defineEventHandler`, so its role is obvious on sight. It is deliberately not
@@ -416,6 +428,9 @@ pnpm --filter @dphonys/nuxt-handler-validation build
416
428
  pnpm --filter @dphonys/nuxt-handler-validation publint
417
429
  ```
418
430
 
431
+ The `internals/*` entries, consumed only by `@dphonys/nuxt-typed-handler`, are
432
+ documented in [`INTERNALS.md`](./INTERNALS.md).
433
+
419
434
  ## License
420
435
 
421
436
  Licensed under the [MIT License](./LICENSE).
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.5.1 <5.0.0"
6
6
  },
7
- "version": "0.1.1",
7
+ "version": "0.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "unknown"
@@ -0,0 +1,4 @@
1
+ export { sourcePlan, validatedContext } from '../../server/lib/validate.js';
2
+ export type { SourcePlan, ValidatedContextOptions, } from '../../server/lib/validate.js';
3
+ export { raiseValidationError } from '../../server/lib/issues.js';
4
+ export type { OnInvalid } from '../../server/lib/issues.js';
@@ -0,0 +1,2 @@
1
+ export { sourcePlan, validatedContext } from "../../server/lib/validate.js";
2
+ export { raiseValidationError } from "../../server/lib/issues.js";
@@ -0,0 +1 @@
1
+ export { markValidationError, readValidationMarker, VALIDATION_ERROR_KEY, } from '../../shared/error-marker.js';
@@ -0,0 +1,5 @@
1
+ export {
2
+ markValidationError,
3
+ readValidationMarker,
4
+ VALIDATION_ERROR_KEY
5
+ } from "../../shared/error-marker.js";
@@ -1,10 +1,10 @@
1
- import type { EventHandler, EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3';
2
- import type { ValidatedContext, ValidationErrorData, ValidationSchemas } from '../types/index.js';
3
- import type { ValidationSchemasGuard } from '../types/internal.js';
1
+ import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3';
2
+ import type { RequestInput, ValidatedContext, ValidatedEventHandler, ValidationErrorData, ValidationSchemas, ValidationSchemasGuard } from '../types/index.js';
4
3
  /**
5
4
  * Declare what a route validates, and get the validated values eagerly in the
6
5
  * handler's second parameter. Undeclared sources are absent from it rather than
7
- * `unknown`, and the returned handler is an ordinary h3 `EventHandler`.
6
+ * `unknown`, and the returned handler is an ordinary h3 `EventHandler` that
7
+ * additionally carries the computed Request input as a phantom type slot.
8
8
  *
9
9
  * Sources validate in the order `routerParams -> query -> headers -> body`,
10
10
  * fail-fast across sources: the first failure answers `400` and no later source
@@ -14,7 +14,7 @@ import type { ValidationSchemasGuard } from '../types/internal.js';
14
14
  */
15
15
  export declare function defineValidatedEventHandler<const S extends ValidationSchemas, Response extends EventHandlerResponse, Request extends EventHandlerRequest = EventHandlerRequest>(options: {
16
16
  validate: S & ValidationSchemasGuard<S>;
17
- }, handler: (event: H3Event<Request>, validated: ValidatedContext<S>) => Response): EventHandler<Request, Response>;
17
+ }, handler: (event: H3Event<Request>, validated: ValidatedContext<S>) => Response): ValidatedEventHandler<Request, Response, RequestInput<S>>;
18
18
  /**
19
19
  * The issues a validation failure raised, or `undefined` for "not a validation
20
20
  * failure" - for a Nitro `error` hook or a Sentry `beforeSend`.
@@ -1,9 +1,20 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
- import type { ValidationSource } from '../../types/index.js';
2
+ import type { ValidationIssue, ValidationSource } from '../../types/index.js';
3
3
  /**
4
- * The one failure this package answers with: `400`, one fixed shape, identical
5
- * in dev and prod, over one source's projected issues. It is also the only
6
- * raise in the package that marks its error - what reaches here is a client's
7
- * bad input, so an observability hook may skip it.
4
+ * The per-request failure door. Receives projected issues, all from one
5
+ * source, and must not return.
8
6
  */
9
- export declare function raiseValidationError(source: ValidationSource, issues: readonly StandardSchemaV1.Issue[]): never;
7
+ export type OnInvalid = (source: ValidationSource, issues: readonly ValidationIssue[]) => never;
8
+ /**
9
+ * Projection by construction, never by filtering: nothing is copied across but
10
+ * the message and the normalized path, so no vendor extra reaches a client or
11
+ * an `onInvalid` hook. Module-level for `validate.ts`; no entry re-exports it.
12
+ */
13
+ export declare function projectIssues(source: ValidationSource, issues: readonly StandardSchemaV1.Issue[]): ValidationIssue[];
14
+ /**
15
+ * The default `onInvalid`: `400`, one fixed shape, identical in dev and prod,
16
+ * over one source's projected issues. It is also the only raise in the package
17
+ * that marks its error - what reaches here is a client's bad input, so an
18
+ * observability hook may skip it.
19
+ */
20
+ export declare function raiseValidationError(source: ValidationSource, issues: readonly ValidationIssue[]): never;
@@ -1,6 +1,6 @@
1
1
  import { createError } from "h3";
2
2
  import { markValidationError } from "../../shared/error-marker.js";
3
- function projectIssues(source, issues) {
3
+ export function projectIssues(source, issues) {
4
4
  return issues.map((issue) => ({
5
5
  source,
6
6
  message: issue.message,
@@ -15,7 +15,7 @@ function projectPath(path) {
15
15
  });
16
16
  }
17
17
  export function raiseValidationError(source, issues) {
18
- const data = { issues: projectIssues(source, issues) };
18
+ const data = { issues: [...issues] };
19
19
  const error = createError({
20
20
  statusCode: 400,
21
21
  statusMessage: "Validation Error",
@@ -1,7 +1,11 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import type { ValidationSource } from '../../types/index.js';
3
- /** How one source is taken off the event. */
4
- export type SourceReader = (event: H3Event) => unknown;
3
+ import type { OnInvalid } from './issues.js';
4
+ /**
5
+ * How one source is taken off the event. A read that already knows the input
6
+ * is bad reports through `onInvalid`, the same door a rejecting schema uses.
7
+ */
8
+ export type SourceReader = (event: H3Event, onInvalid: OnInvalid) => unknown;
5
9
  type SourceWalk<Remaining extends ValidationSource = ValidationSource> = [
6
10
  Remaining
7
11
  ] extends [never] ? readonly [] : {
@@ -1,5 +1,4 @@
1
1
  import { getQuery, getRequestHeaders, getRouterParams, readBody } from "h3";
2
- import { raiseValidationError } from "./issues.js";
3
2
  const PAYLOAD_METHODS = /* @__PURE__ */ new Set([
4
3
  "PATCH",
5
4
  "POST",
@@ -14,13 +13,15 @@ function isClientError(error) {
14
13
  const { statusCode } = error;
15
14
  return typeof statusCode === "number" && statusCode >= 400 && statusCode < 500;
16
15
  }
17
- async function readBodyForValidation(event) {
16
+ async function readBodyForValidation(event, onInvalid) {
18
17
  if (!PAYLOAD_METHODS.has(event.method)) return void 0;
19
18
  try {
20
19
  return await readBody(event, { strict: true });
21
20
  } catch (error) {
22
21
  if (!isClientError(error)) throw error;
23
- raiseValidationError("body", [{ message: UNPARSEABLE_BODY_MESSAGE }]);
22
+ onInvalid("body", [
23
+ { source: "body", message: UNPARSEABLE_BODY_MESSAGE, path: [] }
24
+ ]);
24
25
  }
25
26
  }
26
27
  export const SOURCE_WALK = [
@@ -1,8 +1,14 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import type { H3Event } from 'h3';
3
3
  import type { ValidationSchemas, ValidationSource } from '../../types/index.js';
4
+ import type { OnInvalid } from './issues.js';
4
5
  import type { SourceReader } from './sources.js';
5
- interface SourcePlan {
6
+ /**
7
+ * One resolved source slot: its reader and its schema list. The schemas are
8
+ * always a list - a bare slot is its own one-element list, settled here so
9
+ * nothing downstream asks which shape the author wrote.
10
+ */
11
+ export interface SourcePlan {
6
12
  readonly source: ValidationSource;
7
13
  readonly read: SourceReader;
8
14
  readonly schemas: readonly StandardSchemaV1[];
@@ -13,6 +19,14 @@ interface SourcePlan {
13
19
  * fail-fast order the package's promise instead of the author's key order.
14
20
  */
15
21
  export declare function sourcePlan(schemas: ValidationSchemas): readonly SourcePlan[];
16
- /** Run one request through the plan, in the plan's order. */
17
- export declare function validatedContext(event: H3Event, plan: readonly SourcePlan[]): Promise<Record<string, unknown>>;
18
- export {};
22
+ /** What a caller may swap in per request; the parent passes nothing. */
23
+ export interface ValidatedContextOptions {
24
+ /** Defaults to `raiseValidationError`. */
25
+ readonly onInvalid?: OnInvalid;
26
+ }
27
+ /**
28
+ * Run one request through the plan, in the plan's order. Every client-input
29
+ * rejection - a rejecting schema and an unparseable body alike - goes through
30
+ * `onInvalid`; the developer-mistake `500`s never do.
31
+ */
32
+ export declare function validatedContext(event: H3Event, plan: readonly SourcePlan[], options?: ValidatedContextOptions): Promise<Record<string, unknown>>;
@@ -1,5 +1,5 @@
1
1
  import { createError } from "h3";
2
- import { raiseValidationError } from "./issues.js";
2
+ import { projectIssues, raiseValidationError } from "./issues.js";
3
3
  import { SOURCE_WALK } from "./sources.js";
4
4
  export function sourcePlan(schemas) {
5
5
  const plan = [];
@@ -25,18 +25,20 @@ function raiseUnschemaedSource(source, position) {
25
25
  `[nuxt-handler-validation] cannot validate ${source}: the value at index ${position} is not a Standard Schema. A source slot holds a schema or a non-empty tuple of them - every element must carry a '~standard' property.`
26
26
  );
27
27
  }
28
- export async function validatedContext(event, plan) {
28
+ export async function validatedContext(event, plan, options = {}) {
29
+ const onInvalid = options.onInvalid ?? raiseValidationError;
29
30
  const validated = {};
30
31
  for (const { source, read, schemas } of plan) {
31
32
  validated[source] = await validatedValueFor(
32
33
  source,
33
34
  schemas,
34
- await read(event)
35
+ await read(event, onInvalid),
36
+ onInvalid
35
37
  );
36
38
  }
37
39
  return validated;
38
40
  }
39
- async function validatedValueFor(source, schemas, raw) {
41
+ async function validatedValueFor(source, schemas, raw, onInvalid) {
40
42
  const issues = [];
41
43
  const outputs = [];
42
44
  let unreportedAt;
@@ -49,7 +51,7 @@ async function validatedValueFor(source, schemas, raw) {
49
51
  if (result.issues.length === 0) unreportedAt ??= position;
50
52
  else issues.push(...result.issues);
51
53
  }
52
- if (issues.length > 0) raiseValidationError(source, issues);
54
+ if (issues.length > 0) onInvalid(source, projectIssues(source, issues));
53
55
  if (unreportedAt !== void 0) raiseUnreportedFailure(source, unreportedAt);
54
56
  return mergeOutputs(source, outputs);
55
57
  }
@@ -1,4 +1,7 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { EventHandler, EventHandlerRequest, EventHandlerResponse } from 'h3';
3
+ import type { IsAny } from './internal.js';
4
+ export type { ValidationDeclarationError, ValidationSchemasGuard, } from './internal.js';
2
5
  /** The four sources, in the settled fail-fast order. Every key is optional. */
3
6
  export interface ValidationSchemas {
4
7
  routerParams?: SourceSchemas;
@@ -17,6 +20,8 @@ export type ValidationSource = keyof ValidationSchemas;
17
20
  export type SourceSchemas = StandardSchemaV1 | readonly [StandardSchemaV1, ...StandardSchemaV1[]];
18
21
  /** A schema's output (`InferOutput`), so transforms land already applied. */
19
22
  export type OutputOf<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<Schema> : never;
23
+ /** A schema's input (`InferInput`) - what the client sends, before transforms. */
24
+ export type InputOf<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema> : never;
20
25
  type Flattened<T> = string extends keyof T ? T : number extends keyof T ? T : symbol extends keyof T ? T : {
21
26
  [K in keyof T]: T[K];
22
27
  };
@@ -35,6 +40,20 @@ export type SourceValue<T> = T extends readonly [
35
40
  StandardSchemaV1,
36
41
  ...StandardSchemaV1[]
37
42
  ] ? MergedOutput<T> : OutputOf<T>;
43
+ /**
44
+ * A composed tuple's input: the intersection of element inputs, flattened to
45
+ * one record - every element parses the whole raw source, so the wire must
46
+ * satisfy all of them. Outputs merge later-wins; inputs intersect.
47
+ */
48
+ export type MergedInput<T> = T extends readonly [
49
+ infer Head extends StandardSchemaV1,
50
+ ...infer Rest
51
+ ] ? Rest extends readonly [StandardSchemaV1, ...StandardSchemaV1[]] ? Flattened<InputOf<Head> & MergedInput<Rest>> : InputOf<Head> : never;
52
+ /** One slot's input: a lone schema's input, or the tuple's intersection. */
53
+ export type SourceInput<T> = T extends readonly [
54
+ StandardSchemaV1,
55
+ ...StandardSchemaV1[]
56
+ ] ? MergedInput<T> : InputOf<T>;
38
57
  /**
39
58
  * The handler's second parameter: exactly the sources the declaration
40
59
  * guarantees, each typed as its slot's delivered value. A key is guaranteed
@@ -45,6 +64,32 @@ export type SourceValue<T> = T extends readonly [
45
64
  export type ValidatedContext<S extends ValidationSchemas> = {
46
65
  [K in Extract<keyof S, ValidationSource> as undefined extends S[K] ? never : K]: SourceValue<S[K]>;
47
66
  };
67
+ /**
68
+ * The Request input: keys = declared sources, values = what the client sends.
69
+ * Same key rule as `ValidatedContext` - a slot typed `| undefined` declares
70
+ * nothing and contributes no key.
71
+ */
72
+ export type RequestInput<S extends ValidationSchemas> = {
73
+ [K in Extract<keyof S, ValidationSource> as undefined extends S[K] ? never : K]: SourceInput<S[K]>;
74
+ };
75
+ declare const validatedRequestInput: unique symbol;
76
+ /**
77
+ * The handler `defineValidatedEventHandler` returns: an ordinary h3
78
+ * `EventHandler` carrying the computed Request input in a phantom slot. The
79
+ * slot is never assigned at runtime; it exists so a typed client can read what
80
+ * the route expects from `RequestInputOfHandler`.
81
+ */
82
+ export interface ValidatedEventHandler<Request extends EventHandlerRequest = EventHandlerRequest, Response extends EventHandlerResponse = EventHandlerResponse, Input = never> extends EventHandler<Request, Response> {
83
+ [validatedRequestInput]?: Input;
84
+ }
85
+ /**
86
+ * The Request input a branded handler carries, or `never` for `any` and for
87
+ * any handler this package did not produce. `never` rather than `{}` on
88
+ * purpose: a reader guards `[Input] extends [never]` before keying on it.
89
+ */
90
+ export type RequestInputOfHandler<T> = IsAny<T> extends true ? never : T extends {
91
+ [validatedRequestInput]?: infer I;
92
+ } ? Exclude<I, undefined> : never;
48
93
  /**
49
94
  * One projected issue - the whole of what a client is told about a rejected
50
95
  * value. Raw Standard Schema issues never reach it.
@@ -62,4 +107,3 @@ export interface ValidationIssue {
62
107
  export interface ValidationErrorData {
63
108
  issues: ValidationIssue[];
64
109
  }
65
- export {};
@@ -11,7 +11,7 @@ export interface ValidationDeclarationError<Msg extends string> {
11
11
  type KeysOfUnion<T> = T extends unknown ? keyof T : never;
12
12
  type NamedKey<K> = string extends K ? never : number extends K ? never : symbol extends K ? never : K;
13
13
  type NamedKeys<T> = NamedKey<KeysOfUnion<T>>;
14
- type IsAny<T> = 0 extends 1 & T ? true : false;
14
+ export type IsAny<T> = 0 extends 1 & T ? true : false;
15
15
  type ElementOutputs<T extends readonly StandardSchemaV1[]> = {
16
16
  [I in keyof T]: OutputOf<T[I]>;
17
17
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dphonys/nuxt-handler-validation",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Declare a Nitro handler's request schemas once and receive the validated, fully-typed values in the handler's second parameter.",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -29,6 +29,12 @@
29
29
  ],
30
30
  "server": [
31
31
  "./dist/runtime/server/index.d.ts"
32
+ ],
33
+ "internals/server": [
34
+ "./dist/runtime/internals/server/index.d.ts"
35
+ ],
36
+ "internals/shared": [
37
+ "./dist/runtime/internals/shared/index.d.ts"
32
38
  ]
33
39
  }
34
40
  },
@@ -44,7 +50,16 @@
44
50
  "./server": {
45
51
  "types": "./dist/runtime/server/index.d.ts",
46
52
  "import": "./dist/runtime/server/index.js"
47
- }
53
+ },
54
+ "./internals/server": {
55
+ "types": "./dist/runtime/internals/server/index.d.ts",
56
+ "import": "./dist/runtime/internals/server/index.js"
57
+ },
58
+ "./internals/shared": {
59
+ "types": "./dist/runtime/internals/shared/index.d.ts",
60
+ "import": "./dist/runtime/internals/shared/index.js"
61
+ },
62
+ "./package.json": "./package.json"
48
63
  },
49
64
  "publishConfig": {
50
65
  "access": "public"