@routar/core 1.6.0 → 1.7.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/dist/index.d.ts CHANGED
@@ -1,3 +1,83 @@
1
+ /**
2
+ * The Standard Schema interface (v1), vendored to keep `@routar/core`
3
+ * dependency-free. Standard Schema is the common validation interface adopted
4
+ * by Zod 3.24+, Valibot, ArkType, and others — exposed via the `~standard`
5
+ * property. The spec is type-only (no runtime), and the authors explicitly
6
+ * permit copying the interface rather than depending on `@standard-schema/spec`.
7
+ *
8
+ * @see https://standardschema.dev
9
+ */
10
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
11
+ /** The Standard Schema properties. */
12
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
13
+ }
14
+ declare namespace StandardSchemaV1 {
15
+ /** The Standard Schema properties interface. */
16
+ interface Props<Input = unknown, Output = Input> {
17
+ /** The version number of the standard. */
18
+ readonly version: 1;
19
+ /** The vendor name of the schema library. */
20
+ readonly vendor: string;
21
+ /** Validates unknown input values. */
22
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
23
+ /** Inferred types associated with the schema. */
24
+ readonly types?: Types<Input, Output> | undefined;
25
+ }
26
+ /** The result interface of the validate function. */
27
+ type Result<Output> = SuccessResult<Output> | FailureResult;
28
+ /** The result interface if validation succeeds. */
29
+ interface SuccessResult<Output> {
30
+ /** The typed output value. */
31
+ readonly value: Output;
32
+ /** The non-existent issues. */
33
+ readonly issues?: undefined;
34
+ }
35
+ /** The result interface if validation fails. */
36
+ interface FailureResult {
37
+ /** The issues of failed validation. */
38
+ readonly issues: ReadonlyArray<Issue>;
39
+ }
40
+ /** The issue interface of the failure output. */
41
+ interface Issue {
42
+ /** The error message of the issue. */
43
+ readonly message: string;
44
+ /** The path of the issue, if any. */
45
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
46
+ }
47
+ /** The path segment interface of the issue. */
48
+ interface PathSegment {
49
+ /** The key representing a path segment. */
50
+ readonly key: PropertyKey;
51
+ }
52
+ /** The Standard Schema types interface. */
53
+ interface Types<Input = unknown, Output = Input> {
54
+ /** The input type of the schema. */
55
+ readonly input: Input;
56
+ /** The output type of the schema. */
57
+ readonly output: Output;
58
+ }
59
+ /** Infers the input type of a Standard Schema. */
60
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
61
+ /** Infers the output type of a Standard Schema. */
62
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
63
+ }
64
+
65
+ declare class ValidationError extends Error {
66
+ readonly cause?: unknown;
67
+ constructor(message: string, cause?: unknown);
68
+ }
69
+ /**
70
+ * Thrown internally when a Standard Schema validator (`~standard.validate`)
71
+ * reports issues. It is wrapped as the `cause` of a {@link ValidationError} by
72
+ * the API client, mirroring how a thrown `.parse()` error becomes the cause —
73
+ * so callers branch on `ValidationError` regardless of the validator library.
74
+ * The structured `issues` array is preserved for drift reporting.
75
+ */
76
+ declare class StandardSchemaError extends Error {
77
+ readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
78
+ constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>);
79
+ }
80
+
1
81
  type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
2
82
  /** Options passed to {@link Executor.execute} on every HTTP call. */
3
83
  interface ExecuteOptions {
@@ -13,6 +93,39 @@ interface ExecuteOptions {
13
93
  headers?: Record<string, string>;
14
94
  signal?: AbortSignal;
15
95
  }
96
+ /**
97
+ * Per-call transport options, passed as the optional second argument to any
98
+ * generated endpoint function: `api.create(params, { signal, headers, timeout })`.
99
+ *
100
+ * Passing a bare {@link AbortSignal} as the second argument is still supported
101
+ * (backward compatible) — it is treated as `{ signal }`.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * await api.create({ body }, {
106
+ * signal: controller.signal,
107
+ * headers: { 'Idempotency-Key': key },
108
+ * timeout: 30_000,
109
+ * });
110
+ * ```
111
+ */
112
+ interface EndpointCallOptions {
113
+ /** Aborts the request when the signal fires. */
114
+ signal?: AbortSignal;
115
+ /**
116
+ * Per-call headers. Seeded onto the request before plugin `onRequest` hooks
117
+ * run, and merged over the executor's default headers — so per-call headers
118
+ * win over defaults. A plugin that sets the same header key still wins on
119
+ * collision (plugins are cross-cutting policy such as auth).
120
+ */
121
+ headers?: Record<string, string>;
122
+ /**
123
+ * Per-call timeout in milliseconds. Aborts the request with a `TimeoutError`
124
+ * when exceeded. Applied by the core client (transport-agnostic) and composes
125
+ * with any executor-level timeout (whichever fires first wins).
126
+ */
127
+ timeout?: number;
128
+ }
16
129
  /**
17
130
  * Transport abstraction. Implement this to support any HTTP client.
18
131
  *
@@ -100,8 +213,24 @@ interface CreateExecutorOptions {
100
213
  interface Validator<TOutput> {
101
214
  parse(data: unknown): TOutput;
102
215
  }
103
- /** Extracts the output type of a {@link Validator}. */
104
- type ValidatorOutput<T extends Validator<unknown>> = T extends Validator<infer O> ? O : never;
216
+ /**
217
+ * A validator accepted by routar: either the `.parse()` duck-typed
218
+ * {@link Validator} (Zod, Valibot, Yup, or any object with `.parse()`) **or** a
219
+ * [Standard Schema](https://standardschema.dev) (`~standard` — Zod 3.24+,
220
+ * Valibot, ArkType, …). Both forms are validated at runtime; the `.parse()` path
221
+ * is preferred when present (synchronous), otherwise `~standard.validate` is
222
+ * used (sync or async).
223
+ *
224
+ * @template TOutput Parsed/validated output type.
225
+ */
226
+ type AnyValidator<TOutput = unknown> = Validator<TOutput> | StandardSchemaV1<unknown, TOutput>;
227
+ /**
228
+ * Extracts the output type of an {@link AnyValidator} — the `.parse()` return
229
+ * type for a {@link Validator}, or the Standard Schema output type. The
230
+ * `Validator` branch is checked first so a schema satisfying both (e.g. Zod)
231
+ * resolves through its `.parse()` signature.
232
+ */
233
+ type ValidatorOutput<T> = T extends Validator<infer O> ? O : T extends StandardSchemaV1<unknown, infer O> ? O : never;
105
234
  /**
106
235
  * Shape of an endpoint's request parameters.
107
236
  *
@@ -123,11 +252,11 @@ interface RequestShape {
123
252
  * @template TResponse Validator for the raw response.
124
253
  * @template TAdapter Optional adapter function type.
125
254
  */
126
- interface EndpointSpec<TRequest extends RequestShape = RequestShape, TResponse extends Validator<unknown> = Validator<unknown>, TAdapter extends ((raw: ValidatorOutput<TResponse>) => unknown) | undefined = undefined> {
255
+ interface EndpointSpec<TRequest extends RequestShape = RequestShape, TResponse extends AnyValidator = AnyValidator, TAdapter extends ((raw: ValidatorOutput<TResponse>) => unknown) | undefined = undefined> {
127
256
  method: HttpMethod;
128
257
  path: string;
129
258
  /** Validates and narrows request parameters before the HTTP call. */
130
- request?: Validator<TRequest>;
259
+ request?: AnyValidator<TRequest>;
131
260
  /** Validates the raw server response. */
132
261
  response: TResponse;
133
262
  /**
@@ -176,6 +305,31 @@ type ApiTypes<TApi> = {
176
305
  response: R;
177
306
  } : TApi[K] extends object ? ApiTypes<TApi[K]> : never;
178
307
  };
308
+ /**
309
+ * Per-kind validation mode.
310
+ *
311
+ * - `true` (default) — validate and throw {@link ValidationError} on failure.
312
+ * - `false` — skip validation; raw data passes through untouched.
313
+ * - `'warn'` — attempt validation but, on failure, pass the raw data through
314
+ * and report the error via {@link CreateApiOptions.onValidationError} instead
315
+ * of throwing. The drift-observation mode: surface schema drift without
316
+ * turning it into an outage.
317
+ */
318
+ type ValidationMode = boolean | "warn";
319
+ /**
320
+ * Context passed to {@link CreateApiOptions.onValidationError} describing which
321
+ * call and which phase produced the validation failure.
322
+ */
323
+ interface ValidationErrorContext {
324
+ /** Which phase failed — request params or the server response. */
325
+ kind: "request" | "response";
326
+ /** The endpoint's HTTP method. */
327
+ method: HttpMethod;
328
+ /** The resolved request URL (path is already substituted). */
329
+ url: string;
330
+ /** The raw data that failed validation (raw params or raw response). */
331
+ data: unknown;
332
+ }
179
333
  /**
180
334
  * Options for {@link createApi}.
181
335
  *
@@ -186,6 +340,12 @@ type ApiTypes<TApi> = {
186
340
  *
187
341
  * // Keep request validation (catch call-site bugs), skip response in prod
188
342
  * createApi(executor, router, { validate: { request: true, response: false } });
343
+ *
344
+ * // Observe response drift without breaking production
345
+ * createApi(executor, router, {
346
+ * validate: { request: true, response: 'warn' },
347
+ * onValidationError: (err, ctx) => Sentry.captureException(err, { extra: ctx }),
348
+ * });
189
349
  * ```
190
350
  */
191
351
  interface CreateApiOptions {
@@ -194,18 +354,25 @@ interface CreateApiOptions {
194
354
  *
195
355
  * - `true` (default) — validate both request and response.
196
356
  * - `false` — skip both; raw params and raw response pass through.
197
- * - `{ request?, response? }` enable/disable each independently.
357
+ * - `'warn'` validate both, but pass raw data through and report via
358
+ * {@link CreateApiOptions.onValidationError} instead of throwing.
359
+ * - `{ request?, response? }` — set each independently (each a
360
+ * {@link ValidationMode}).
198
361
  */
199
- validate?: boolean | {
200
- request?: boolean;
201
- response?: boolean;
362
+ validate?: ValidationMode | {
363
+ request?: ValidationMode;
364
+ response?: ValidationMode;
202
365
  };
366
+ /**
367
+ * Called when validation fails under `'warn'` mode (instead of throwing).
368
+ * Use it to report schema drift to your observability stack. Never called
369
+ * when `validate` is `true` (which throws) or `false` (which skips).
370
+ */
371
+ onValidationError?: (error: ValidationError, context: ValidationErrorContext) => void;
203
372
  }
204
373
 
205
374
  /** Callable type for a single endpoint on the generated API client. */
206
- type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = TSpec["request"] extends {
207
- parse: (data: unknown) => infer R;
208
- } ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>> : (params?: RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
375
+ type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = TSpec["request"] extends Validator<infer R> ? (params: R, options?: AbortSignal | EndpointCallOptions) => Promise<InferResponse<TSpec>> : TSpec["request"] extends StandardSchemaV1<unknown, infer O> ? (params: O, options?: AbortSignal | EndpointCallOptions) => Promise<InferResponse<TSpec>> : (params?: RequestShape, options?: AbortSignal | EndpointCallOptions) => Promise<InferResponse<TSpec>>;
209
376
  /**
210
377
  * Fully-typed API client produced by {@link createApi}.
211
378
  * Nested {@link RouterDef} entries become nested sub-client objects.
@@ -444,6 +611,33 @@ type PathParams<TPath extends string> = TPath extends `${string}:${infer Param}/
444
611
  type PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never] ? {} : {
445
612
  path: Record<PathParams<TPath>, unknown>;
446
613
  };
614
+ /**
615
+ * Builds the envelope request output type from the SE-12 separated bucket
616
+ * validators. Each bucket contributes its key only when supplied — the tuple
617
+ * wrapping (`[T] extends [never]`) prevents the `never` default from
618
+ * distributing and collapsing the whole intersection.
619
+ */
620
+ type BucketRequestOutput<TPathParams, TQuery, TBody> = ([
621
+ TPathParams
622
+ ] extends [never] ? {} : {
623
+ path: ValidatorOutput<TPathParams>;
624
+ }) & ([TQuery] extends [never] ? {} : {
625
+ query: ValidatorOutput<TQuery>;
626
+ }) & ([TBody] extends [never] ? {} : {
627
+ body: ValidatorOutput<TBody>;
628
+ });
629
+ /**
630
+ * The `pathParams` field of the separated form: required when `TPath` has
631
+ * dynamic segments, optional otherwise. The validator type is further
632
+ * constrained (at the generic level) to cover every `:param` name.
633
+ */
634
+ type BucketPathField<TPath extends string, TPathParams> = [
635
+ PathParams<TPath>
636
+ ] extends [never] ? {
637
+ pathParams?: TPathParams;
638
+ } : {
639
+ pathParams: TPathParams;
640
+ };
447
641
  /**
448
642
  * Type-safe endpoint definition helper.
449
643
  *
@@ -516,31 +710,31 @@ type PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]
516
710
  * });
517
711
  * ```
518
712
  */
519
- declare function endpoint<TMethod extends HttpMethod, TPath extends string, TRequest extends RequestShape & PathConstraint<TPath>, TResponse extends Validator<unknown>, TOut>(spec: {
713
+ declare function endpoint<TMethod extends HttpMethod, TPath extends string, TReq extends AnyValidator<RequestShape & PathConstraint<TPath>>, TResponse extends AnyValidator, TOut>(spec: {
520
714
  method: TMethod;
521
715
  path: TPath;
522
- request: Validator<TRequest>;
716
+ request: TReq;
523
717
  response: TResponse;
524
718
  adapter: (raw: ValidatorOutput<TResponse>) => TOut;
525
719
  }): {
526
720
  method: TMethod;
527
721
  path: string;
528
- request: Validator<TRequest>;
722
+ request: TReq;
529
723
  response: TResponse;
530
724
  adapter: (raw: ValidatorOutput<TResponse>) => TOut;
531
725
  };
532
- declare function endpoint<TMethod extends HttpMethod, TPath extends string, TRequest extends RequestShape & PathConstraint<TPath>, TResponse extends Validator<unknown>>(spec: {
726
+ declare function endpoint<TMethod extends HttpMethod, TPath extends string, TReq extends AnyValidator<RequestShape & PathConstraint<TPath>>, TResponse extends AnyValidator>(spec: {
533
727
  method: TMethod;
534
728
  path: TPath;
535
- request: Validator<TRequest>;
729
+ request: TReq;
536
730
  response: TResponse;
537
731
  }): {
538
732
  method: TMethod;
539
733
  path: string;
540
- request: Validator<TRequest>;
734
+ request: TReq;
541
735
  response: TResponse;
542
736
  };
543
- declare function endpoint<TMethod extends HttpMethod, TResponse extends Validator<unknown>, TOut>(spec: {
737
+ declare function endpoint<TMethod extends HttpMethod, TResponse extends AnyValidator, TOut>(spec: {
544
738
  method: TMethod;
545
739
  path: string;
546
740
  response: TResponse;
@@ -551,7 +745,7 @@ declare function endpoint<TMethod extends HttpMethod, TResponse extends Validato
551
745
  response: TResponse;
552
746
  adapter: (raw: ValidatorOutput<TResponse>) => TOut;
553
747
  };
554
- declare function endpoint<TMethod extends HttpMethod, TResponse extends Validator<unknown>>(spec: {
748
+ declare function endpoint<TMethod extends HttpMethod, TResponse extends AnyValidator>(spec: {
555
749
  method: TMethod;
556
750
  path: string;
557
751
  response: TResponse;
@@ -560,6 +754,32 @@ declare function endpoint<TMethod extends HttpMethod, TResponse extends Validato
560
754
  path: string;
561
755
  response: TResponse;
562
756
  };
757
+ declare function endpoint<TMethod extends HttpMethod, TPath extends string, TResponse extends AnyValidator, TOut, TPathParams extends AnyValidator<Record<PathParams<TPath>, unknown>> = never, TQuery extends AnyValidator = never, TBody extends AnyValidator = never>(spec: {
758
+ method: TMethod;
759
+ path: TPath;
760
+ query?: TQuery;
761
+ body?: TBody;
762
+ response: TResponse;
763
+ adapter: (raw: ValidatorOutput<TResponse>) => TOut;
764
+ } & BucketPathField<TPath, TPathParams>): {
765
+ method: TMethod;
766
+ path: string;
767
+ request: Validator<BucketRequestOutput<TPathParams, TQuery, TBody>>;
768
+ response: TResponse;
769
+ adapter: (raw: ValidatorOutput<TResponse>) => TOut;
770
+ };
771
+ declare function endpoint<TMethod extends HttpMethod, TPath extends string, TResponse extends AnyValidator, TPathParams extends AnyValidator<Record<PathParams<TPath>, unknown>> = never, TQuery extends AnyValidator = never, TBody extends AnyValidator = never>(spec: {
772
+ method: TMethod;
773
+ path: TPath;
774
+ query?: TQuery;
775
+ body?: TBody;
776
+ response: TResponse;
777
+ } & BucketPathField<TPath, TPathParams>): {
778
+ method: TMethod;
779
+ path: string;
780
+ request: Validator<BucketRequestOutput<TPathParams, TQuery, TBody>>;
781
+ response: TResponse;
782
+ };
563
783
 
564
784
  /**
565
785
  * Groups a set of endpoint specs (and optional nested routers) under a shared
@@ -647,9 +867,17 @@ declare function serializeParams(params: Record<string, unknown>): URLSearchPara
647
867
  declare function joinPaths(...segments: string[]): string;
648
868
  declare function resolvePath(pathTemplate: string, params?: Record<string, unknown>): string;
649
869
 
650
- declare class ValidationError extends Error {
651
- readonly cause?: unknown;
652
- constructor(message: string, cause?: unknown);
653
- }
870
+ /**
871
+ * Validates `data` with an {@link AnyValidator}, returning the parsed value.
872
+ *
873
+ * Prefers the synchronous `.parse()` path when present (Zod, Valibot, Yup, or
874
+ * any object with `.parse()`); a thrown parse error propagates unchanged.
875
+ * Otherwise uses the Standard Schema `~standard.validate` path (sync or async);
876
+ * reported issues are thrown as a {@link StandardSchemaError}.
877
+ *
878
+ * Exported for executor / mock-handler authors who need routar's exact
879
+ * validate-or-throw semantics for both validator styles.
880
+ */
881
+ declare function runValidator(validator: AnyValidator, data: unknown): Promise<unknown>;
654
882
 
655
- export { type ApiClient, type ApiClientWithRouter, type ApiTypes, type CreateApiOptions, type CreateExecutorOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorPlugin, type FetchExecutorOptions, type FetchRetryOption, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, serializeParams };
883
+ export { type AnyValidator, type ApiClient, type ApiClientWithRouter, type ApiTypes, type CreateApiOptions, type CreateExecutorOptions, type EndpointCallOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorPlugin, type FetchExecutorOptions, type FetchRetryOption, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, StandardSchemaError, StandardSchemaV1, TimeoutError, ValidationError, type ValidationErrorContext, type ValidationMode, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, runValidator, serializeParams };