@minisylar/express-typed-router 1.9.7 → 1.9.9
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 +250 -2
- package/dist/schema-router.cjs +5 -5
- package/dist/schema-router.d.cts +164 -21
- package/dist/schema-router.d.mts +164 -21
- package/dist/schema-router.mjs +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,182 @@ router.get("/static/*", handler); // { "0": string }
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
+
## Validation failure hooks
|
|
131
|
+
|
|
132
|
+
`bodySchema`/`querySchema`/`paramsSchema` already reject a bad request with a 400. Hooks let you react to that failure, and for three of them, change what happens next. There are two kinds:
|
|
133
|
+
|
|
134
|
+
- **`onValidationFailure`** (route or global) — a side effect only: logging, metrics, alerts. It never changes the response.
|
|
135
|
+
- **`onBodyValidationFailure`, `onQueryValidationFailure`, `onParamsValidationFailure`** — one per schema. These *can* change the response: keep the default 400, send something else, or let the request through anyway. Covered below.
|
|
136
|
+
|
|
137
|
+
### The `info` object
|
|
138
|
+
|
|
139
|
+
Every hook is called with one object:
|
|
140
|
+
|
|
141
|
+
| field | type | what it is |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| `source` | `"body" \| "query" \| "params"` | which schema failed — only on `onValidationFailure`; the schema-specific hooks already know from their own name |
|
|
144
|
+
| `error` | `string` | short error message |
|
|
145
|
+
| `details` | that schema's own issue shape once narrowed | the validation issues themselves. On the route's `onValidationFailure`, narrow on `source` first (it's a discriminated union). On the global `onValidationFailure`, always `any[]`, it isn't tied to one route's schemas. |
|
|
146
|
+
| `method` | `string` | the route's HTTP method, e.g. `"post"` |
|
|
147
|
+
| `path` | `string` | the route's path, readable even for a `RegExp` route |
|
|
148
|
+
| `req` | Express `Request` | the request; also sees this route's `middleware`-added properties (route-level hooks) or the router's `useMiddleware()`-added properties (global), see below |
|
|
149
|
+
| `res` | Express `Response` | the response — schema-specific hooks only, that's how they change it |
|
|
150
|
+
| `next` | Express `NextFunction` | continues the request — schema-specific hooks only |
|
|
151
|
+
|
|
152
|
+
### Just reacting to a failure
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
router.post(
|
|
156
|
+
"/users",
|
|
157
|
+
{
|
|
158
|
+
bodySchema: UserSchema,
|
|
159
|
+
querySchema: QuerySchema,
|
|
160
|
+
hooks: {
|
|
161
|
+
onValidationFailure: (info) => {
|
|
162
|
+
if (info.source === "body") {
|
|
163
|
+
logger.warn({ issues: info.details }, "user body validation failed"); // UserSchema's own issue shape
|
|
164
|
+
} else if (info.source === "query") {
|
|
165
|
+
logger.warn({ issues: info.details }, "user query validation failed"); // QuerySchema's own issue shape
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
handler,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Or globally, for every route on the router — details is always any[] here,
|
|
174
|
+
// a route's own onValidationFailure above is the one that narrows per schema.
|
|
175
|
+
createTypedRouter().onValidationFailure(({ source, method, path }) => metrics.increment(source));
|
|
176
|
+
createTypedRouterWithConfig({ hooks: { onValidationFailure: (info) => metrics.increment(info.source) } });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Narrowing on `info.source` is what gives you the precise `details` type per branch; unnarrowed, it's whatever's common to all three. The 400 is sent regardless of what this hook does. It can be `async`; a throw or rejection is swallowed, the response never waits on it. `router.onValidationFailure()` can be called any time, read fresh per request, and picks up whatever `.useMiddleware()` already added by that point in the chain, no cast needed:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const router = createTypedRouter()
|
|
183
|
+
.useMiddleware(requireAuth) // adds req.userId
|
|
184
|
+
.onValidationFailure((info) => {
|
|
185
|
+
metrics.increment(`validation_failure.${info.source}`, { userId: info.req.userId });
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`createTypedRouterWithConfig<Req>({ hooks: { onValidationFailure } })` works the same way, `Req` just has to be spelled out explicitly since there's no `.useMiddleware()` call for it to infer from:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const router = createTypedRouterWithConfig<{ userId: string }>({
|
|
193
|
+
hooks: { onValidationFailure: (info) => metrics.increment(info.source, { userId: info.req.userId }) },
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The global hook doesn't inherit into a `.mount()`ed sub-router, pass the same function to each router if you want it everywhere.
|
|
198
|
+
|
|
199
|
+
The route's own `onValidationFailure` gets this too, same as its schema-specific siblings, from that route's `middleware` option:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
router.post(
|
|
203
|
+
"/orders",
|
|
204
|
+
{
|
|
205
|
+
bodySchema: OrderSchema,
|
|
206
|
+
middleware: [requireAuth], // adds req.userId
|
|
207
|
+
hooks: {
|
|
208
|
+
onValidationFailure: (info) => logger.warn({ userId: info.req.userId, source: info.source }, "validation failed"),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
handler,
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Changing the response
|
|
216
|
+
|
|
217
|
+
`onBodyValidationFailure`, `onQueryValidationFailure`, `onParamsValidationFailure` are typed to that one schema (`details` is its own issue shape, not a blanket `any[]`), and the router waits for them before responding:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { defaultValidationHandler } from "@minisylar/express-typed-router";
|
|
221
|
+
// ^ the library's own default 400 response, exported so a hook can run a
|
|
222
|
+
// side effect and still fall back to it, instead of re-implementing it.
|
|
223
|
+
|
|
224
|
+
router.post(
|
|
225
|
+
"/orders",
|
|
226
|
+
{
|
|
227
|
+
bodySchema: OrderSchema,
|
|
228
|
+
hooks: {
|
|
229
|
+
onBodyValidationFailure: (info) => {
|
|
230
|
+
logger.warn({ issues: info.details }, "order validation failed");
|
|
231
|
+
return defaultValidationHandler(info); // side effect above, still the normal 400
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
handler,
|
|
236
|
+
);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
What a hook does decides the outcome:
|
|
240
|
+
|
|
241
|
+
| does this | result |
|
|
242
|
+
| --- | --- |
|
|
243
|
+
| nothing | default 400, same as if there were no hook |
|
|
244
|
+
| `defaultValidationHandler(info)` | default 400, after whatever the hook did first |
|
|
245
|
+
| `info.res.status(422).json(...)` | that response instead |
|
|
246
|
+
| `info.next()` | request continues unvalidated; `req.body` stays typed as `OrderSchema`'s output either way, you're trusting it yourself |
|
|
247
|
+
|
|
248
|
+
### Order
|
|
249
|
+
|
|
250
|
+
A request only ever fails one schema, whichever is checked first (body, then query, then params). So on the `/users` route above, a request with both an invalid body *and* an invalid query string still only fails on `body`, query is never even checked:
|
|
251
|
+
|
|
252
|
+
- `onBodyValidationFailure` fires (if set)
|
|
253
|
+
- `onValidationFailure` fires once, with `source: "body"` — not twice, and `onQueryValidationFailure` does not fire
|
|
254
|
+
- the global `onValidationFailure` fires once, same `source: "body"`
|
|
255
|
+
|
|
256
|
+
Fix the body and resend, and *then* you'd see the query failure. Whichever hooks apply to that one failure run in this order: the schema-specific one, then the route's `onValidationFailure`, then the global one.
|
|
257
|
+
|
|
258
|
+
### Reading middleware-added properties
|
|
259
|
+
|
|
260
|
+
A route's own `middleware` (see [Middleware typing](#middleware-typing) below) runs before validation, and what it adds to `req` is visible, typed, inside a schema-specific hook too, no cast needed, as long as the hook is written inline:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
router.post(
|
|
264
|
+
"/orders",
|
|
265
|
+
{
|
|
266
|
+
bodySchema: OrderSchema,
|
|
267
|
+
middleware: [requireAuth], // adds req.userId
|
|
268
|
+
hooks: {
|
|
269
|
+
onBodyValidationFailure: (info) => {
|
|
270
|
+
if (info.req.userId === "trusted-internal-service") return info.next();
|
|
271
|
+
info.res.status(422).json({ error: "Invalid order payload" });
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
handler,
|
|
276
|
+
);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A hook pulled out into a standalone `const` can't know which route's `middleware` it'll be attached to, so it falls back to plain `req` there, see reuse below.
|
|
280
|
+
|
|
281
|
+
### Reuse outside the route
|
|
282
|
+
|
|
283
|
+
`SchemaValidationFailureHook<S>` / `ValidationFailureHook` are exported, so a hook doesn't have to be written inline:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const logUserBodyFailure: SchemaValidationFailureHook<typeof UserSchema> = ({ details }) => {
|
|
287
|
+
logger.warn({ issues: details }, "user body validation failed");
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
router.post("/users", { bodySchema: UserSchema, hooks: { onBodyValidationFailure: logUserBodyFailure } }, handler);
|
|
291
|
+
router.put("/users/:id", { bodySchema: UserSchema, hooks: { onBodyValidationFailure: logUserBodyFailure } }, handler);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### At a glance
|
|
295
|
+
|
|
296
|
+
| hook | fires when | can change the response | `details` |
|
|
297
|
+
| --- | --- | --- | --- |
|
|
298
|
+
| `onBodyValidationFailure` | `bodySchema` rejects | yes | `bodySchema`'s own issue shape |
|
|
299
|
+
| `onQueryValidationFailure` | `querySchema` rejects | yes | `querySchema`'s own issue shape |
|
|
300
|
+
| `onParamsValidationFailure` | `paramsSchema` rejects | yes | `paramsSchema`'s own issue shape |
|
|
301
|
+
| `onValidationFailure` (route option) | any of the three, on this route | no | matching schema's issue shape, once narrowed on `source` |
|
|
302
|
+
| `onValidationFailure` (`router.onValidationFailure()` / `createTypedRouterWithConfig`) | any of the three, on any route on this router | no | `any[]` |
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
130
306
|
## Middleware typing
|
|
131
307
|
|
|
132
308
|
Declare what a middleware adds to `req`, and that type flows into every handler that uses it.
|
|
@@ -329,7 +505,32 @@ app.use(
|
|
|
329
505
|
|
|
330
506
|
**How it persists:** schemas fill in as traffic flows. They're held in memory and, when `specOutputPath` is set, written to the file (debounced) as new shapes are observed. On startup the library **reloads** the existing file, so a restart doesn't reset what was already learned — the file is the durable store.
|
|
331
507
|
|
|
332
|
-
|
|
508
|
+
### Gotchas
|
|
509
|
+
|
|
510
|
+
- **Keep middleware variables as tuples** - inline middleware arrays preserve
|
|
511
|
+
their tuple type automatically, but assigning the array to a variable widens
|
|
512
|
+
it to `TypedMiddleware[]`. The simplest option is to pass the tuple directly
|
|
513
|
+
in the route's `middleware` object:
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
router.get("/something", { middleware: [a, b, c] }, handler);
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
If you need to reuse the middleware variable with `InferSchemaHandler`, wrap
|
|
520
|
+
it in `defineMiddleware`. It keeps each middleware's specific type instead
|
|
521
|
+
of widening:
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
const middleware = defineMiddleware(a, b, c);
|
|
525
|
+
type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`as const` works too, if you'd rather not import a helper:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
const middleware = [a, b, c] as const;
|
|
532
|
+
type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
|
|
533
|
+
```
|
|
333
534
|
|
|
334
535
|
- **Reset accumulated schemas** — inference is merge-only, so a field you _remove_ from a response lingers in the docs. To clear it, delete `openapi.json` and let it rebuild from current traffic.
|
|
335
536
|
- **`responseSchema` beats inference** — declare it on routes you want guaranteed-correct (and leak-proof); it overrides whatever traffic suggests.
|
|
@@ -413,6 +614,46 @@ Edit a route, save, and your client types update on their own.
|
|
|
413
614
|
|
|
414
615
|
> ⚠️ **Avoid a restart loop.** Write the generated `api.d.ts` **outside** the path your server watcher restarts on (or add it to the watcher's ignore list). If your server watches `*.ts` in `src/` and you output the types _into_ `src/`, you get: type-gen writes `api.d.ts` → server restarts → spec rewrites → type-gen runs again → ♻️. Putting it in a separate folder (e.g. `shared/`, `generated/`) avoids this.
|
|
415
616
|
|
|
617
|
+
### Generate the spec without running a server
|
|
618
|
+
|
|
619
|
+
`specOutputPath` above writes the file as a side effect of starting your app. That's fine for local dev, but in CI you don't want to start a server just to get a file. `generateOpenApiSpec` builds the spec object directly: no Express app, no `app.listen()`, no HTTP request.
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
import { createTypedRouter, generateOpenApiSpec } from "@minisylar/express-typed-router";
|
|
623
|
+
import { writeFile } from "node:fs/promises";
|
|
624
|
+
|
|
625
|
+
const router = createTypedRouter();
|
|
626
|
+
router.get("/users/:id", handler);
|
|
627
|
+
// ... define the rest of your routes
|
|
628
|
+
|
|
629
|
+
const spec = await generateOpenApiSpec(router, { title: "My API", version: "1.0.0" });
|
|
630
|
+
await writeFile("./openapi.json", JSON.stringify(spec, null, 2));
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Run that as a script in CI and feed the output to whatever reads a static spec: `openapi-typescript`, a docs site generator, a linter, anything that takes a `.json` file.
|
|
634
|
+
|
|
635
|
+
It accepts the same router shapes as [`createDocs`](#per-feature-routers-one-doc-endpoint) — a single router, a single prefixed router, or an array mixing either:
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
// Single router, no options
|
|
639
|
+
await generateOpenApiSpec(usersRouter);
|
|
640
|
+
|
|
641
|
+
// Single router with a prefix
|
|
642
|
+
await generateOpenApiSpec({ prefix: "/api/users", router: usersRouter });
|
|
643
|
+
|
|
644
|
+
// Multiple routers, all prefixed
|
|
645
|
+
await generateOpenApiSpec([
|
|
646
|
+
{ prefix: "/api/users", router: usersRouter },
|
|
647
|
+
{ prefix: "/api/orders", router: ordersRouter },
|
|
648
|
+
]);
|
|
649
|
+
|
|
650
|
+
// Mixed — some prefixed, some not
|
|
651
|
+
await generateOpenApiSpec([
|
|
652
|
+
usersRouter,
|
|
653
|
+
{ prefix: "/api/orders", router: ordersRouter },
|
|
654
|
+
]);
|
|
655
|
+
```
|
|
656
|
+
|
|
416
657
|
### Use with `openapi-fetch`
|
|
417
658
|
|
|
418
659
|
```ts
|
|
@@ -596,12 +837,19 @@ Use a schema that actually parses the text:
|
|
|
596
837
|
| ---------------------------------------- | ------------------------------------------------ |
|
|
597
838
|
| `createTypedRouter()` | Create a router |
|
|
598
839
|
| `createTypedRouterWithMiddleware(...mw)` | Create a router pre-configured with middleware |
|
|
599
|
-
| `createTypedRouterWithConfig(config)` | Create a router with
|
|
840
|
+
| `createTypedRouterWithConfig(config)` | Create a router with an error handler and/or global config |
|
|
600
841
|
| `router.useMiddleware(mw)` | Add typed global middleware (returns new router) |
|
|
842
|
+
| `router.onValidationFailure(hook)` | Set the global validation failure hook (returns same router) |
|
|
601
843
|
| `router.use(prefix, subRouter)` | Mount a sub-router |
|
|
602
844
|
| `router.getRouter()` | Get the underlying Express router |
|
|
603
845
|
| `router.docs(options)` | Get the docs + OpenAPI spec router |
|
|
846
|
+
| `generateOpenApiSpec(routers, options)` | Build the spec object with no server involved |
|
|
604
847
|
| `TypedMiddleware<T>` | Type helper for middleware that extends `req` |
|
|
848
|
+
| `defineMiddleware(...mw)` | Keep a middleware array's tuple type when reused |
|
|
849
|
+
| `SchemaValidationFailureHook<S>` | Type a reusable `onBodyValidationFailure`/`onQueryValidationFailure`/`onParamsValidationFailure` outside the route |
|
|
850
|
+
| `RouteValidationFailureHook<Body, Query, Params>` | Type a reusable route-level `onValidationFailure` outside the route, `details` still narrows on `source` |
|
|
851
|
+
| `ValidationFailureHook` | Type a reusable global `onValidationFailure` outside the route |
|
|
852
|
+
| `defaultValidationHandler(info)` | The library's own default 400 response, callable from a schema-specific hook |
|
|
605
853
|
|
|
606
854
|
---
|
|
607
855
|
|
package/dist/schema-router.cjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("express");c=s(c,1);let l=require("@standard-schema/utils");function u(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`){let e=n[`~standard`].validate(t);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new l.SchemaError(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function d(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function f(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("express");c=s(c,1);let l=require("@standard-schema/utils");function u(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`){let e=n[`~standard`].validate(t);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new l.SchemaError(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function d(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function f(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}function p(...e){return e}function m(e){e.res.status(400).json({error:`Validation failed`,details:e.details})}function h(e,t){if(e)try{e(t)?.catch?.(()=>{})}catch{}}const g=Function(`m`,`return import(m)`);let _,v;async function y(e){_??=await g(`module`),v??=await g(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(v.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(v.pathToFileURL(r+`/`).href);for(let n of t)try{let t=_.createRequire(n).resolve(e);return await g(v.pathToFileURL(t).href)}catch{}return g(e)}const b=new WeakMap,x=/\(\?<[^>]+>/;function S(e){return typeof e==`string`&&x.test(e)?new RegExp(e):e}function C(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function w(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function T(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function E(e){let t=0;return T(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function D(e){if(typeof e.path==`string`||e.pathExample)return[C(e)];let t=w(e.path.source);return t.length===1?[C(e)]:t.map(E)}function O(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/\*([A-Za-z0-9_]+)/g,`{$1}`).replace(/^\^|\$$/g,``),n=0;return t.replace(/\*/g,()=>`{${n++}}`).replace(/\/{2,}/g,`/`)}function k(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/^\^|\$$/g,``),n=[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g),...t.matchAll(/\*([A-Za-z0-9_]+)/g)].map(e=>e[1]),r=(t.replace(/\*[A-Za-z0-9_]+/g,``).match(/\*/g)??[]).length;for(let e=0;e<r;e++)n.push(String(e));return n}function A(e){return e.startsWith(`:`)||e.includes(`*`)||e.includes(`(?<`)}function j(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!A(e))??`default`}function M(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!A(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function N(e){let t=b.get(e);if(t)return t;let n=await P(e);return b.set(e,n),n}async function P(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await y(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await y(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await y(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await y(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function F(e){return I(e,0,new WeakSet)}function I(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return I(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=I(e[0],t+1,n);for(let a=1;a<r;a++)i=z(i,I(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=I(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function L(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function R(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function z(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...L(e),...L(t)]),r={},i=R(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=z(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=z(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function B(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}const V=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function H(e,t,n){return`<!doctype html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
-
<title>${
|
|
4
|
+
<title>${B(e)}</title>
|
|
5
5
|
<meta charset="utf-8" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
|
-
<script id="api-reference" data-url="${
|
|
10
|
-
<script src="${
|
|
9
|
+
<script id="api-reference" data-url="${B(t)}"><\/script>
|
|
10
|
+
<script src="${B(n)}"><\/script>
|
|
11
11
|
</body>
|
|
12
|
-
</html>`}async function B(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of w(t)){let r=T(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await A(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=E(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await A(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??k(t.method,e),tags:t.tags??[O(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await A(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await A(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const V=new WeakMap;var H=class e{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=c.default.Router(),V.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(t,...n){let r=typeof t==`string`,i=r?t:``,a=(r?n:[t,...n]).map(t=>{if(t instanceof e)return this.trackMounted(i,t),t.getRouter();let n=V.get(t);return n&&this.trackMounted(i,n),t});return r?this.router.use(t,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+b(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of w(e)){let i=r[T(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(e={}){let t=c.default.Router(),n;if(e.specOutputPath){let t=e.specOutputPath,r=!1,i=!1,a=async()=>{if(r){i=!0;return}r=!0;try{let n=await B(this.getRouteMetadata(),e),r=await p(`fs/promises`),i=t.replace(/[/\\][^/\\]*$/,``);i&&i!==t&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${t}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,t)}catch{}finally{r=!1,i&&(i=!1,a())}},o;n=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let e=await(await p(`fs/promises`)).readFile(t,`utf8`).catch(()=>null);if(e)try{this.hydrateResponses(JSON.parse(e))}catch{}}catch{}await a()})}return e.sampleResponses!==!1&&this.enableSampling(e.sampleResponses===`live`?`live`:`redacted`,n),t.get(`/openapi.json`,async(t,n)=>{try{let t=await B(this.getRouteMetadata(),e);n.json(t)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),t.get(`/`,(t,n)=>{let r=`${t.baseUrl}/openapi.json`,i=e.cdnUrl??R;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(z(e.title??`API`,r,i))}),t}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.paramsSchema=e.paramsSchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,a.pathExample=e.pathExample,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),e.paramsSchema&&i.push(this.createParamsValidationMiddleware(e.paramsSchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=M(t),i=a.responseSamples.get(n),o=i?I(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](y(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.body),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}t.body=a&&`value`in a?a.value:a,r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}createParamsValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.params),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}t.params=a&&`value`in a?a.value:a,r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}createQueryValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.query),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}let o=a&&`value`in a?a.value:a;Object.defineProperty(t,"query",{value:o,writable:!1,enumerable:!0,configurable:!0}),r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}};function U(){return new H}function W(e){let t=new H;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function G(...e){let t=new H;for(let n of e)t=t.useMiddleware(n);return t}function K(e,t={}){let n=(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(t.sampleResponses!==!1){let e=t.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of n)t.enableSampling(e)}let r=c.default.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await B(n.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+b(t)}}))),t);r.json(e)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),r.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??R;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(z(t.title??`API`,r,i))}),r}exports.TypedRouter=H,exports.createDocs=K,exports.createTypedRouter=U,exports.createTypedRouterWithConfig=W,exports.createTypedRouterWithMiddleware=G,exports.inferJsonSchema=M,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
|
|
12
|
+
</html>`}async function U(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of D(t)){let r=O(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await N(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=k(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await N(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??M(t.method,e),tags:t.tags??[j(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await N(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await N(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const W=new WeakMap;var G=class e{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;globalValidationFailureHook;constructor(){this.router=c.default.Router(),W.set(this.router,this)}onValidationFailure(e){return this.globalValidationFailureHook=e,this}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(t,...n){let r=typeof t==`string`,i=r?t:``,a=(r?n:[t,...n]).map(t=>{if(t instanceof e)return this.trackMounted(i,t),t.getRouter();let n=W.get(t);return n&&this.trackMounted(i,n),t});return r?this.router.use(t,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+C(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of D(e)){let i=r[O(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(e={}){let t=c.default.Router(),n;if(e.specOutputPath){let t=e.specOutputPath,r=!1,i=!1,a=async()=>{if(r){i=!0;return}r=!0;try{let n=await U(this.getRouteMetadata(),e),r=await g(`fs/promises`),i=t.replace(/[/\\][^/\\]*$/,``);i&&i!==t&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${t}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,t)}catch{}finally{r=!1,i&&(i=!1,a())}},o;n=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let e=await(await g(`fs/promises`)).readFile(t,`utf8`).catch(()=>null);if(e)try{this.hydrateResponses(JSON.parse(e))}catch{}}catch{}await a()})}return e.sampleResponses!==!1&&this.enableSampling(e.sampleResponses===`live`?`live`:`redacted`,n),t.get(`/openapi.json`,async(t,n)=>{try{let t=await U(this.getRouteMetadata(),e);n.json(t)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),t.get(`/`,(t,n)=>{let r=`${t.baseUrl}/openapi.json`,i=e.cdnUrl??V;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(H(e.title??`API`,r,i))}),t}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let o=n;a.bodySchema=o.bodySchema,a.querySchema=o.querySchema,a.paramsSchema=o.paramsSchema,a.tags=o.tags,a.description=o.description,a.summary=o.summary,a.deprecated=o.deprecated,a.responseSchema=o.responseSchema,a.hidden=o.hidden,a.pathExample=o.pathExample,o.middleware&&i.push(...o.middleware);let s=o.hooks?.onValidationFailure,c=o.hooks?.onBodyValidationFailure,l=o.hooks?.onQueryValidationFailure,u=o.hooks?.onParamsValidationFailure;o.bodySchema&&i.push(this.createBodyValidationMiddleware(o.bodySchema,e,t,o.pathExample,s,c)),o.querySchema&&i.push(this.createQueryValidationMiddleware(o.querySchema,e,t,o.pathExample,s,l)),o.paramsSchema&&i.push(this.createParamsValidationMiddleware(o.paramsSchema,e,t,o.pathExample,s,u)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=F(t),i=a.responseSamples.get(n),o=i?z(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](S(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}notifyValidationFailure(e,t,n,r,i,a,o){if(!o&&!this.globalValidationFailureHook)return;let s={source:e,error:t,details:n,method:r,path:i,req:a};h(o,s),h(this.globalValidationFailureHook,s)}async runSchemaValidationHook(e,t,n,r,i,a,o,s){if(!e)return!1;let c=!1,l={error:t,details:n,method:r,path:i,req:a,res:o,next:((...e)=>{c=!0,s(...e)})};try{await e(l)}catch{}return c||o.headersSent}createBodyValidationMiddleware(e,t,n,r,i,a){return async(o,s,c)=>{try{let l=d(e,o.body),u=l&&typeof l.then==`function`?await l:l;if(u&&`issues`in u&&u.issues){let e=u.errors||u.issues,l=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,e,t,l,o,s,c);this.notifyValidationFailure(`body`,`Validation failed`,e,t,l,o,i),d||m({res:s,details:e});return}o.body=u&&`value`in u?u.value:u,c()}catch(e){if(f(e)){let l=e.errors||e.issues,u=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,l,t,u,o,s,c);this.notifyValidationFailure(`body`,`Validation failed`,l,t,u,o,i),d||m({res:s,details:l})}else c(e)}}}createParamsValidationMiddleware(e,t,n,r,i,a){return async(o,s,c)=>{try{let l=d(e,o.params),u=l&&typeof l.then==`function`?await l:l;if(u&&`issues`in u&&u.issues){let e=u.errors||u.issues,l=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,e,t,l,o,s,c);this.notifyValidationFailure(`params`,`Validation failed`,e,t,l,o,i),d||m({res:s,details:e});return}o.params=u&&`value`in u?u.value:u,c()}catch(e){if(f(e)){let l=e.errors||e.issues,u=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,l,t,u,o,s,c);this.notifyValidationFailure(`params`,`Validation failed`,l,t,u,o,i),d||m({res:s,details:l})}else c(e)}}}createQueryValidationMiddleware(e,t,n,r,i,a){return async(o,s,c)=>{try{let l=d(e,o.query),u=l&&typeof l.then==`function`?await l:l;if(u&&`issues`in u&&u.issues){let e=u.errors||u.issues,l=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,e,t,l,o,s,c);this.notifyValidationFailure(`query`,`Validation failed`,e,t,l,o,i),d||m({res:s,details:e});return}let f=u&&`value`in u?u.value:u;Object.defineProperty(o,"query",{value:f,writable:!1,enumerable:!0,configurable:!0}),c()}catch(e){if(f(e)){let l=e.errors||e.issues,u=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,l,t,u,o,s,c);this.notifyValidationFailure(`query`,`Validation failed`,l,t,u,o,i),d||m({res:s,details:l})}else c(e)}}}};function K(){return new G}function q(e){let t=new G;return e?.errorHandler&&t.getRouter().use(e.errorHandler),e?.hooks?.onValidationFailure&&t.onValidationFailure(e.hooks.onValidationFailure),t}function J(...e){let t=new G;for(let n of e)t=t.useMiddleware(n);return t}function Y(e){return(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e}).flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+C(t)}})))}async function X(e,t={}){return U(Y(e),t)}function Z(e,t={}){if(t.sampleResponses!==!1){let n=t.sampleResponses===`live`?`live`:`redacted`,r=Array.isArray(e)?e:[e];for(let e of r)(`prefix`in e?e.router:e).enableSampling(n)}let n=c.default.Router();return n.get(`/openapi.json`,async(n,r)=>{try{let n=await X(e,t);r.json(n)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??V;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(H(t.title??`API`,r,i))}),n}exports.TypedRouter=G,exports.createDocs=Z,exports.createTypedRouter=K,exports.createTypedRouterWithConfig=q,exports.createTypedRouterWithMiddleware=J,exports.defaultValidationHandler=m,exports.defineMiddleware=p,exports.generateOpenApiSpec=X,exports.inferJsonSchema=F,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
|
package/dist/schema-router.d.cts
CHANGED
|
@@ -112,6 +112,18 @@ type RequestOnlyMiddleware<TReq extends Record<string, any>> = TypedMiddleware<T
|
|
|
112
112
|
* Simplified TypedMiddleware for response locals-only extensions
|
|
113
113
|
*/
|
|
114
114
|
type LocalsOnlyMiddleware<TLocals extends Record<string, any>> = TypedMiddleware<{}, TLocals>;
|
|
115
|
+
/**
|
|
116
|
+
* Assigning a middleware array to a variable widens it to
|
|
117
|
+
* TypedMiddleware<any, any>[], losing the per-middleware types that
|
|
118
|
+
* InferSchemaHandler needs. The usual fix is `as const` on the array;
|
|
119
|
+
* defineMiddleware does the same thing without it, since the `const` type
|
|
120
|
+
* parameter keeps each argument's specific type instead of widening.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const middleware = defineMiddleware(auth, logging);
|
|
124
|
+
* type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
|
|
125
|
+
*/
|
|
126
|
+
declare function defineMiddleware<const M extends readonly TypedMiddleware<any, any>[]>(...mw: M): [...M];
|
|
115
127
|
type InferMiddlewareProps<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<infer FirstReq, any> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstReq & InferMiddlewareProps<Rest> : FirstReq : {} : {};
|
|
116
128
|
type InferMiddlewareLocals<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<any, infer FirstLocals> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstLocals & InferMiddlewareLocals<Rest> : FirstLocals : {} : {};
|
|
117
129
|
type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = Omit<Request, "params" | "query" | "body"> & {
|
|
@@ -131,6 +143,112 @@ type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaL
|
|
|
131
143
|
* @property paramsSchema - Optional schema for validating route params.
|
|
132
144
|
* @property middleware - Optional array of TypedMiddleware for this route.
|
|
133
145
|
*/
|
|
146
|
+
/** Which validated part of the request failed. */
|
|
147
|
+
type ValidationFailureSource = "body" | "query" | "params";
|
|
148
|
+
/**
|
|
149
|
+
* Passed to a validation failure hook when bodySchema/querySchema/paramsSchema
|
|
150
|
+
* rejects a request. `req` includes whatever `.useMiddleware()` added, when
|
|
151
|
+
* set via `router.onValidationFailure()` (that method's `Req` type parameter
|
|
152
|
+
* is the router's own, already widened by any `.useMiddleware()` calls
|
|
153
|
+
* before it in the chain). Not the case for `createTypedRouterWithConfig`'s
|
|
154
|
+
* `hooks.onValidationFailure`, called before any middleware exists.
|
|
155
|
+
*/
|
|
156
|
+
interface ValidationFailureInfo<MiddlewareProps extends Record<string, any> = {}> {
|
|
157
|
+
source: ValidationFailureSource;
|
|
158
|
+
error: string;
|
|
159
|
+
details: any[];
|
|
160
|
+
method: HttpMethod;
|
|
161
|
+
path: string;
|
|
162
|
+
req: Request & MiddlewareProps;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Called when validation fails, in addition to the 400 response that's
|
|
166
|
+
* already sent, for side effects like metrics/logging/alerting, not for
|
|
167
|
+
* changing the response (see the schema-specific hooks in RouteHooks for
|
|
168
|
+
* that). May be async (e.g. to log to an external service); the response is sent
|
|
169
|
+
* without waiting for it either way. Both a synchronous throw and a rejected
|
|
170
|
+
* promise are caught and ignored, so a broken hook can't take down request
|
|
171
|
+
* handling or surface as an unhandled rejection.
|
|
172
|
+
*/
|
|
173
|
+
type ValidationFailureHook<MiddlewareProps extends Record<string, any> = {}> = (info: ValidationFailureInfo<MiddlewareProps>) => void | Promise<void>;
|
|
174
|
+
type SchemaValidationDetails<S> = S extends AnyStandardSchema ? readonly StandardSchemaV1.Issue[] : any[];
|
|
175
|
+
/**
|
|
176
|
+
* Passed to a route's onValidationFailure. A discriminated union on `source`
|
|
177
|
+
* (unlike {@link ValidationFailureInfo}, which is only ever `any[]`): narrow
|
|
178
|
+
* on `source` and `details` is that schema's own issue shape.
|
|
179
|
+
*/
|
|
180
|
+
type RouteValidationFailureInfo<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = {
|
|
181
|
+
source: "body";
|
|
182
|
+
error: string;
|
|
183
|
+
details: SchemaValidationDetails<BodySchema>;
|
|
184
|
+
method: HttpMethod;
|
|
185
|
+
path: string;
|
|
186
|
+
req: Request & MiddlewareProps;
|
|
187
|
+
} | {
|
|
188
|
+
source: "query";
|
|
189
|
+
error: string;
|
|
190
|
+
details: SchemaValidationDetails<QuerySchema>;
|
|
191
|
+
method: HttpMethod;
|
|
192
|
+
path: string;
|
|
193
|
+
req: Request & MiddlewareProps;
|
|
194
|
+
} | {
|
|
195
|
+
source: "params";
|
|
196
|
+
error: string;
|
|
197
|
+
details: SchemaValidationDetails<ParamsSchema>;
|
|
198
|
+
method: HttpMethod;
|
|
199
|
+
path: string;
|
|
200
|
+
req: Request & MiddlewareProps;
|
|
201
|
+
};
|
|
202
|
+
type RouteValidationFailureHook<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = (info: RouteValidationFailureInfo<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>) => void | Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Passed to a schema-specific validation failure hook. Do nothing and the
|
|
205
|
+
* default 400 still fires. Call `next()` to continue with the raw,
|
|
206
|
+
* unvalidated value, or respond via `res` yourself (e.g. a custom status).
|
|
207
|
+
* `req` includes whatever the route's own `middleware` option adds, same as
|
|
208
|
+
* the handler's `req`.
|
|
209
|
+
*/
|
|
210
|
+
interface SchemaValidationFailureInfo<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> {
|
|
211
|
+
error: string;
|
|
212
|
+
details: SchemaValidationDetails<S>;
|
|
213
|
+
method: HttpMethod;
|
|
214
|
+
path: string;
|
|
215
|
+
req: Request & MiddlewareProps;
|
|
216
|
+
res: Response;
|
|
217
|
+
next: NextFunction;
|
|
218
|
+
}
|
|
219
|
+
type SchemaValidationFailureHook<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> = (info: SchemaValidationFailureInfo<S, MiddlewareProps>) => void | Promise<void>;
|
|
220
|
+
/** The default 400 response for a validation failure. Call it from a schema-specific hook to log first and still fall back to the normal response. */
|
|
221
|
+
declare function defaultValidationHandler(info: {
|
|
222
|
+
res: Response;
|
|
223
|
+
details: unknown;
|
|
224
|
+
}): void;
|
|
225
|
+
/**
|
|
226
|
+
* Router-wide validation failure hook. Schema-specific hooks aren't
|
|
227
|
+
* available here: a router spans many routes, each with its own (possibly
|
|
228
|
+
* different) bodySchema/querySchema/paramsSchema, so there's no single
|
|
229
|
+
* schema to type this against.
|
|
230
|
+
*/
|
|
231
|
+
interface RouterHooks<Req extends Record<string, any> = {}> {
|
|
232
|
+
/** Runs for every route on this router, after any per-route hooks below. */
|
|
233
|
+
onValidationFailure?: ValidationFailureHook<Req>;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Per-route lifecycle hooks, grouped under one `hooks` option so adding a
|
|
237
|
+
* future hook doesn't grow the flat option list. `onBodyValidationFailure`/
|
|
238
|
+
* `onQueryValidationFailure`/`onParamsValidationFailure` are typed to that
|
|
239
|
+
* route's own schema; `onValidationFailure` covers all three sources with
|
|
240
|
+
* `details: any[]`, for narrowing on `source` yourself instead.
|
|
241
|
+
*/
|
|
242
|
+
interface RouteHooks<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> {
|
|
243
|
+
/** Runs when bodySchema, querySchema, or paramsSchema rejects the request. Fires after the schema-specific hook for that one, before the router's global hook. Can't change the response, use the schema-specific hooks for that; good for side effects (metrics, logging, alerts) across all three sources at once. `details` is typed to the matching schema once you narrow on `source`; `req` includes this route's `middleware` props, same as the schema-specific hooks below. */
|
|
244
|
+
onValidationFailure?: RouteValidationFailureHook<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>;
|
|
245
|
+
/** Runs when bodySchema rejects. `details` typed to bodySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw body, or respond via `res` yourself. */
|
|
246
|
+
onBodyValidationFailure?: SchemaValidationFailureHook<BodySchema, MiddlewareProps>;
|
|
247
|
+
/** Runs when querySchema rejects. `details` typed to querySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw query, or respond via `res` yourself. */
|
|
248
|
+
onQueryValidationFailure?: SchemaValidationFailureHook<QuerySchema, MiddlewareProps>;
|
|
249
|
+
/** Runs when paramsSchema rejects. `details` typed to paramsSchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw params, or respond via `res` yourself. */
|
|
250
|
+
onParamsValidationFailure?: SchemaValidationFailureHook<ParamsSchema, MiddlewareProps>;
|
|
251
|
+
}
|
|
134
252
|
interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined> {
|
|
135
253
|
bodySchema?: BodySchema;
|
|
136
254
|
/**
|
|
@@ -147,6 +265,8 @@ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, Qu
|
|
|
147
265
|
* string-arrival caveat as `querySchema` applies; see its docs above.
|
|
148
266
|
*/
|
|
149
267
|
paramsSchema?: ParamsSchema;
|
|
268
|
+
/** hooks.onValidationFailure runs in addition to the router's global hook (set via createTypedRouterWithConfig or router.onValidationFailure()), if both are set. */
|
|
269
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema>;
|
|
150
270
|
middleware?: TypedMiddleware<any, any>[];
|
|
151
271
|
tags?: string[];
|
|
152
272
|
description?: string;
|
|
@@ -297,7 +417,21 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
297
417
|
private mountedRouters;
|
|
298
418
|
private sampleMode;
|
|
299
419
|
private scheduleSpecWrite?;
|
|
420
|
+
private globalValidationFailureHook?;
|
|
300
421
|
constructor();
|
|
422
|
+
/**
|
|
423
|
+
* Set the global validation failure hook. Called for every route
|
|
424
|
+
* registered directly on this router when bodySchema/querySchema/
|
|
425
|
+
* paramsSchema rejects a request. Works the same whether the router came
|
|
426
|
+
* from createTypedRouter() or createTypedRouterWithConfig({ onValidationFailure }),
|
|
427
|
+
* so you're not locked into the config-taking factory just to add this later.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* const router = createTypedRouter().onValidationFailure((info) => {
|
|
431
|
+
* logger.warn(info, 'request validation failed');
|
|
432
|
+
* });
|
|
433
|
+
*/
|
|
434
|
+
onValidationFailure(hook: ValidationFailureHook<Req>): TypedRouter<Req, Locals>;
|
|
301
435
|
/**
|
|
302
436
|
* Add typed middleware that extends the request with additional properties
|
|
303
437
|
* and/or adds properties to response.locals
|
|
@@ -391,6 +525,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
391
525
|
bodySchema?: BodySchema;
|
|
392
526
|
querySchema?: QuerySchema;
|
|
393
527
|
paramsSchema?: ParamsSchema;
|
|
528
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
394
529
|
middleware?: [...M];
|
|
395
530
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
396
531
|
get(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -398,6 +533,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
398
533
|
bodySchema?: BodySchema;
|
|
399
534
|
querySchema?: QuerySchema;
|
|
400
535
|
paramsSchema?: ParamsSchema;
|
|
536
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
401
537
|
middleware?: [...M];
|
|
402
538
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
403
539
|
post<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -405,6 +541,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
405
541
|
bodySchema?: BodySchema;
|
|
406
542
|
querySchema?: QuerySchema;
|
|
407
543
|
paramsSchema?: ParamsSchema;
|
|
544
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
408
545
|
middleware?: [...M];
|
|
409
546
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
410
547
|
post(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -412,6 +549,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
412
549
|
bodySchema?: BodySchema;
|
|
413
550
|
querySchema?: QuerySchema;
|
|
414
551
|
paramsSchema?: ParamsSchema;
|
|
552
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
415
553
|
middleware?: [...M];
|
|
416
554
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
417
555
|
put<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -419,6 +557,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
419
557
|
bodySchema?: BodySchema;
|
|
420
558
|
querySchema?: QuerySchema;
|
|
421
559
|
paramsSchema?: ParamsSchema;
|
|
560
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
422
561
|
middleware?: [...M];
|
|
423
562
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
424
563
|
put(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -426,6 +565,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
426
565
|
bodySchema?: BodySchema;
|
|
427
566
|
querySchema?: QuerySchema;
|
|
428
567
|
paramsSchema?: ParamsSchema;
|
|
568
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
429
569
|
middleware?: [...M];
|
|
430
570
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
431
571
|
patch<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -433,6 +573,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
433
573
|
bodySchema?: BodySchema;
|
|
434
574
|
querySchema?: QuerySchema;
|
|
435
575
|
paramsSchema?: ParamsSchema;
|
|
576
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
436
577
|
middleware?: [...M];
|
|
437
578
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
438
579
|
patch(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -440,42 +581,49 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
440
581
|
bodySchema?: BodySchema;
|
|
441
582
|
querySchema?: QuerySchema;
|
|
442
583
|
paramsSchema?: ParamsSchema;
|
|
584
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
443
585
|
middleware?: [...M];
|
|
444
586
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
445
587
|
delete<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
446
588
|
delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
|
|
447
589
|
querySchema?: QuerySchema;
|
|
448
590
|
paramsSchema?: ParamsSchema;
|
|
591
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
449
592
|
middleware?: [...M];
|
|
450
593
|
}, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
451
594
|
delete(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
452
595
|
delete<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
|
|
453
596
|
querySchema?: QuerySchema;
|
|
454
597
|
paramsSchema?: ParamsSchema;
|
|
598
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
455
599
|
middleware?: [...M];
|
|
456
600
|
}, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
457
601
|
options<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
458
602
|
options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
|
|
459
603
|
querySchema?: QuerySchema;
|
|
460
604
|
paramsSchema?: ParamsSchema;
|
|
605
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
461
606
|
middleware?: [...M];
|
|
462
607
|
}, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
463
608
|
options(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
464
609
|
options<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
|
|
465
610
|
querySchema?: QuerySchema;
|
|
466
611
|
paramsSchema?: ParamsSchema;
|
|
612
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
467
613
|
middleware?: [...M];
|
|
468
614
|
}, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
469
615
|
head<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
470
616
|
head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
|
|
471
617
|
querySchema?: QuerySchema;
|
|
472
618
|
paramsSchema?: ParamsSchema;
|
|
619
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
473
620
|
middleware?: [...M];
|
|
474
621
|
}, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
475
622
|
head(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
476
623
|
head<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
|
|
477
624
|
querySchema?: QuerySchema;
|
|
478
625
|
paramsSchema?: ParamsSchema;
|
|
626
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
479
627
|
middleware?: [...M];
|
|
480
628
|
}, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
481
629
|
all<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -483,6 +631,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
483
631
|
bodySchema?: BodySchema;
|
|
484
632
|
querySchema?: QuerySchema;
|
|
485
633
|
paramsSchema?: ParamsSchema;
|
|
634
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
486
635
|
middleware?: [...M];
|
|
487
636
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
488
637
|
all(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -490,9 +639,12 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
490
639
|
bodySchema?: BodySchema;
|
|
491
640
|
querySchema?: QuerySchema;
|
|
492
641
|
paramsSchema?: ParamsSchema;
|
|
642
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
493
643
|
middleware?: [...M];
|
|
494
644
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
495
645
|
private registerRoute;
|
|
646
|
+
private notifyValidationFailure;
|
|
647
|
+
private runSchemaValidationHook;
|
|
496
648
|
private createBodyValidationMiddleware;
|
|
497
649
|
private createParamsValidationMiddleware;
|
|
498
650
|
private createQueryValidationMiddleware;
|
|
@@ -523,10 +675,12 @@ declare function createTypedRouter<Req extends Record<string, any> = AdditionalR
|
|
|
523
675
|
*
|
|
524
676
|
* @property validateInput - (Future) Whether to enable global input validation.
|
|
525
677
|
* @property errorHandler - Optional global error handler middleware for the router.
|
|
678
|
+
* @property hooks.onValidationFailure - Called for every validation failure on this router, in addition to any per-route hook.
|
|
526
679
|
*/
|
|
527
|
-
interface RouterConfig {
|
|
680
|
+
interface RouterConfig<Req extends Record<string, any> = {}> {
|
|
528
681
|
validateInput?: boolean;
|
|
529
682
|
errorHandler?: (error: any, req: Request, res: Response, next: NextFunction) => void;
|
|
683
|
+
hooks?: RouterHooks<Req>;
|
|
530
684
|
}
|
|
531
685
|
/**
|
|
532
686
|
* Create a new typed router with optional configuration.
|
|
@@ -545,7 +699,7 @@ interface RouterConfig {
|
|
|
545
699
|
* }
|
|
546
700
|
* });
|
|
547
701
|
*/
|
|
548
|
-
declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig): TypedRouter<Req, Locals>;
|
|
702
|
+
declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig<Req>): TypedRouter<Req, Locals>;
|
|
549
703
|
/**
|
|
550
704
|
* Create a new typed router with pre-configured middleware.
|
|
551
705
|
*
|
|
@@ -576,27 +730,16 @@ type RouterDocEntry = TypedRouter<any, any> | {
|
|
|
576
730
|
router: TypedRouter<any, any>;
|
|
577
731
|
};
|
|
578
732
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
733
|
+
* Build the OpenAPI spec object directly, without mounting an Express router
|
|
734
|
+
* or making an HTTP request. For generating openapi.json at build/CI time,
|
|
735
|
+
* separately from running the app. Accepts the same router(s) shape as
|
|
736
|
+
* createDocs.
|
|
581
737
|
*
|
|
582
738
|
* @example
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* // auth.router.ts — routes like /login, /logout
|
|
587
|
-
* export const authRouter = createTypedRouter();
|
|
588
|
-
*
|
|
589
|
-
* // app.ts
|
|
590
|
-
* app.use('/api', usersRouter.getRouter());
|
|
591
|
-
* app.use('/api', authRouter.getRouter());
|
|
592
|
-
* app.use('/docs', createDocs(
|
|
593
|
-
* [
|
|
594
|
-
* { prefix: '/api', router: usersRouter },
|
|
595
|
-
* { prefix: '/api', router: authRouter },
|
|
596
|
-
* ],
|
|
597
|
-
* { title: 'My API', version: '1.0.0' }
|
|
598
|
-
* ));
|
|
739
|
+
* const spec = await generateOpenApiSpec(router, { title: 'My API' });
|
|
740
|
+
* await fs.writeFile('./openapi.json', JSON.stringify(spec, null, 2));
|
|
599
741
|
*/
|
|
742
|
+
declare function generateOpenApiSpec(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): Promise<Record<string, any>>;
|
|
600
743
|
declare function createDocs(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): express.Router & express.RequestHandler;
|
|
601
744
|
//#endregion
|
|
602
|
-
export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
|
|
745
|
+
export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteHooks, RouteOptions, RouteValidationFailureHook, RouteValidationFailureInfo, RouterConfig, RouterDocEntry, RouterHooks, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, SchemaValidationFailureHook, SchemaValidationFailureInfo, TypedMiddleware, TypedRouter, ValidationFailureHook, ValidationFailureInfo, ValidationFailureSource, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, defaultValidationHandler, defineMiddleware, generateOpenApiSpec, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
|
package/dist/schema-router.d.mts
CHANGED
|
@@ -112,6 +112,18 @@ type RequestOnlyMiddleware<TReq extends Record<string, any>> = TypedMiddleware<T
|
|
|
112
112
|
* Simplified TypedMiddleware for response locals-only extensions
|
|
113
113
|
*/
|
|
114
114
|
type LocalsOnlyMiddleware<TLocals extends Record<string, any>> = TypedMiddleware<{}, TLocals>;
|
|
115
|
+
/**
|
|
116
|
+
* Assigning a middleware array to a variable widens it to
|
|
117
|
+
* TypedMiddleware<any, any>[], losing the per-middleware types that
|
|
118
|
+
* InferSchemaHandler needs. The usual fix is `as const` on the array;
|
|
119
|
+
* defineMiddleware does the same thing without it, since the `const` type
|
|
120
|
+
* parameter keeps each argument's specific type instead of widening.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const middleware = defineMiddleware(auth, logging);
|
|
124
|
+
* type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
|
|
125
|
+
*/
|
|
126
|
+
declare function defineMiddleware<const M extends readonly TypedMiddleware<any, any>[]>(...mw: M): [...M];
|
|
115
127
|
type InferMiddlewareProps<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<infer FirstReq, any> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstReq & InferMiddlewareProps<Rest> : FirstReq : {} : {};
|
|
116
128
|
type InferMiddlewareLocals<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<any, infer FirstLocals> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstLocals & InferMiddlewareLocals<Rest> : FirstLocals : {} : {};
|
|
117
129
|
type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = Omit<Request, "params" | "query" | "body"> & {
|
|
@@ -131,6 +143,112 @@ type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaL
|
|
|
131
143
|
* @property paramsSchema - Optional schema for validating route params.
|
|
132
144
|
* @property middleware - Optional array of TypedMiddleware for this route.
|
|
133
145
|
*/
|
|
146
|
+
/** Which validated part of the request failed. */
|
|
147
|
+
type ValidationFailureSource = "body" | "query" | "params";
|
|
148
|
+
/**
|
|
149
|
+
* Passed to a validation failure hook when bodySchema/querySchema/paramsSchema
|
|
150
|
+
* rejects a request. `req` includes whatever `.useMiddleware()` added, when
|
|
151
|
+
* set via `router.onValidationFailure()` (that method's `Req` type parameter
|
|
152
|
+
* is the router's own, already widened by any `.useMiddleware()` calls
|
|
153
|
+
* before it in the chain). Not the case for `createTypedRouterWithConfig`'s
|
|
154
|
+
* `hooks.onValidationFailure`, called before any middleware exists.
|
|
155
|
+
*/
|
|
156
|
+
interface ValidationFailureInfo<MiddlewareProps extends Record<string, any> = {}> {
|
|
157
|
+
source: ValidationFailureSource;
|
|
158
|
+
error: string;
|
|
159
|
+
details: any[];
|
|
160
|
+
method: HttpMethod;
|
|
161
|
+
path: string;
|
|
162
|
+
req: Request & MiddlewareProps;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Called when validation fails, in addition to the 400 response that's
|
|
166
|
+
* already sent, for side effects like metrics/logging/alerting, not for
|
|
167
|
+
* changing the response (see the schema-specific hooks in RouteHooks for
|
|
168
|
+
* that). May be async (e.g. to log to an external service); the response is sent
|
|
169
|
+
* without waiting for it either way. Both a synchronous throw and a rejected
|
|
170
|
+
* promise are caught and ignored, so a broken hook can't take down request
|
|
171
|
+
* handling or surface as an unhandled rejection.
|
|
172
|
+
*/
|
|
173
|
+
type ValidationFailureHook<MiddlewareProps extends Record<string, any> = {}> = (info: ValidationFailureInfo<MiddlewareProps>) => void | Promise<void>;
|
|
174
|
+
type SchemaValidationDetails<S> = S extends AnyStandardSchema ? readonly StandardSchemaV1.Issue[] : any[];
|
|
175
|
+
/**
|
|
176
|
+
* Passed to a route's onValidationFailure. A discriminated union on `source`
|
|
177
|
+
* (unlike {@link ValidationFailureInfo}, which is only ever `any[]`): narrow
|
|
178
|
+
* on `source` and `details` is that schema's own issue shape.
|
|
179
|
+
*/
|
|
180
|
+
type RouteValidationFailureInfo<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = {
|
|
181
|
+
source: "body";
|
|
182
|
+
error: string;
|
|
183
|
+
details: SchemaValidationDetails<BodySchema>;
|
|
184
|
+
method: HttpMethod;
|
|
185
|
+
path: string;
|
|
186
|
+
req: Request & MiddlewareProps;
|
|
187
|
+
} | {
|
|
188
|
+
source: "query";
|
|
189
|
+
error: string;
|
|
190
|
+
details: SchemaValidationDetails<QuerySchema>;
|
|
191
|
+
method: HttpMethod;
|
|
192
|
+
path: string;
|
|
193
|
+
req: Request & MiddlewareProps;
|
|
194
|
+
} | {
|
|
195
|
+
source: "params";
|
|
196
|
+
error: string;
|
|
197
|
+
details: SchemaValidationDetails<ParamsSchema>;
|
|
198
|
+
method: HttpMethod;
|
|
199
|
+
path: string;
|
|
200
|
+
req: Request & MiddlewareProps;
|
|
201
|
+
};
|
|
202
|
+
type RouteValidationFailureHook<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = (info: RouteValidationFailureInfo<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>) => void | Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Passed to a schema-specific validation failure hook. Do nothing and the
|
|
205
|
+
* default 400 still fires. Call `next()` to continue with the raw,
|
|
206
|
+
* unvalidated value, or respond via `res` yourself (e.g. a custom status).
|
|
207
|
+
* `req` includes whatever the route's own `middleware` option adds, same as
|
|
208
|
+
* the handler's `req`.
|
|
209
|
+
*/
|
|
210
|
+
interface SchemaValidationFailureInfo<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> {
|
|
211
|
+
error: string;
|
|
212
|
+
details: SchemaValidationDetails<S>;
|
|
213
|
+
method: HttpMethod;
|
|
214
|
+
path: string;
|
|
215
|
+
req: Request & MiddlewareProps;
|
|
216
|
+
res: Response;
|
|
217
|
+
next: NextFunction;
|
|
218
|
+
}
|
|
219
|
+
type SchemaValidationFailureHook<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> = (info: SchemaValidationFailureInfo<S, MiddlewareProps>) => void | Promise<void>;
|
|
220
|
+
/** The default 400 response for a validation failure. Call it from a schema-specific hook to log first and still fall back to the normal response. */
|
|
221
|
+
declare function defaultValidationHandler(info: {
|
|
222
|
+
res: Response;
|
|
223
|
+
details: unknown;
|
|
224
|
+
}): void;
|
|
225
|
+
/**
|
|
226
|
+
* Router-wide validation failure hook. Schema-specific hooks aren't
|
|
227
|
+
* available here: a router spans many routes, each with its own (possibly
|
|
228
|
+
* different) bodySchema/querySchema/paramsSchema, so there's no single
|
|
229
|
+
* schema to type this against.
|
|
230
|
+
*/
|
|
231
|
+
interface RouterHooks<Req extends Record<string, any> = {}> {
|
|
232
|
+
/** Runs for every route on this router, after any per-route hooks below. */
|
|
233
|
+
onValidationFailure?: ValidationFailureHook<Req>;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Per-route lifecycle hooks, grouped under one `hooks` option so adding a
|
|
237
|
+
* future hook doesn't grow the flat option list. `onBodyValidationFailure`/
|
|
238
|
+
* `onQueryValidationFailure`/`onParamsValidationFailure` are typed to that
|
|
239
|
+
* route's own schema; `onValidationFailure` covers all three sources with
|
|
240
|
+
* `details: any[]`, for narrowing on `source` yourself instead.
|
|
241
|
+
*/
|
|
242
|
+
interface RouteHooks<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> {
|
|
243
|
+
/** Runs when bodySchema, querySchema, or paramsSchema rejects the request. Fires after the schema-specific hook for that one, before the router's global hook. Can't change the response, use the schema-specific hooks for that; good for side effects (metrics, logging, alerts) across all three sources at once. `details` is typed to the matching schema once you narrow on `source`; `req` includes this route's `middleware` props, same as the schema-specific hooks below. */
|
|
244
|
+
onValidationFailure?: RouteValidationFailureHook<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>;
|
|
245
|
+
/** Runs when bodySchema rejects. `details` typed to bodySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw body, or respond via `res` yourself. */
|
|
246
|
+
onBodyValidationFailure?: SchemaValidationFailureHook<BodySchema, MiddlewareProps>;
|
|
247
|
+
/** Runs when querySchema rejects. `details` typed to querySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw query, or respond via `res` yourself. */
|
|
248
|
+
onQueryValidationFailure?: SchemaValidationFailureHook<QuerySchema, MiddlewareProps>;
|
|
249
|
+
/** Runs when paramsSchema rejects. `details` typed to paramsSchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw params, or respond via `res` yourself. */
|
|
250
|
+
onParamsValidationFailure?: SchemaValidationFailureHook<ParamsSchema, MiddlewareProps>;
|
|
251
|
+
}
|
|
134
252
|
interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined> {
|
|
135
253
|
bodySchema?: BodySchema;
|
|
136
254
|
/**
|
|
@@ -147,6 +265,8 @@ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, Qu
|
|
|
147
265
|
* string-arrival caveat as `querySchema` applies; see its docs above.
|
|
148
266
|
*/
|
|
149
267
|
paramsSchema?: ParamsSchema;
|
|
268
|
+
/** hooks.onValidationFailure runs in addition to the router's global hook (set via createTypedRouterWithConfig or router.onValidationFailure()), if both are set. */
|
|
269
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema>;
|
|
150
270
|
middleware?: TypedMiddleware<any, any>[];
|
|
151
271
|
tags?: string[];
|
|
152
272
|
description?: string;
|
|
@@ -297,7 +417,21 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
297
417
|
private mountedRouters;
|
|
298
418
|
private sampleMode;
|
|
299
419
|
private scheduleSpecWrite?;
|
|
420
|
+
private globalValidationFailureHook?;
|
|
300
421
|
constructor();
|
|
422
|
+
/**
|
|
423
|
+
* Set the global validation failure hook. Called for every route
|
|
424
|
+
* registered directly on this router when bodySchema/querySchema/
|
|
425
|
+
* paramsSchema rejects a request. Works the same whether the router came
|
|
426
|
+
* from createTypedRouter() or createTypedRouterWithConfig({ onValidationFailure }),
|
|
427
|
+
* so you're not locked into the config-taking factory just to add this later.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* const router = createTypedRouter().onValidationFailure((info) => {
|
|
431
|
+
* logger.warn(info, 'request validation failed');
|
|
432
|
+
* });
|
|
433
|
+
*/
|
|
434
|
+
onValidationFailure(hook: ValidationFailureHook<Req>): TypedRouter<Req, Locals>;
|
|
301
435
|
/**
|
|
302
436
|
* Add typed middleware that extends the request with additional properties
|
|
303
437
|
* and/or adds properties to response.locals
|
|
@@ -391,6 +525,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
391
525
|
bodySchema?: BodySchema;
|
|
392
526
|
querySchema?: QuerySchema;
|
|
393
527
|
paramsSchema?: ParamsSchema;
|
|
528
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
394
529
|
middleware?: [...M];
|
|
395
530
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
396
531
|
get(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -398,6 +533,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
398
533
|
bodySchema?: BodySchema;
|
|
399
534
|
querySchema?: QuerySchema;
|
|
400
535
|
paramsSchema?: ParamsSchema;
|
|
536
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
401
537
|
middleware?: [...M];
|
|
402
538
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
403
539
|
post<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -405,6 +541,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
405
541
|
bodySchema?: BodySchema;
|
|
406
542
|
querySchema?: QuerySchema;
|
|
407
543
|
paramsSchema?: ParamsSchema;
|
|
544
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
408
545
|
middleware?: [...M];
|
|
409
546
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
410
547
|
post(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -412,6 +549,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
412
549
|
bodySchema?: BodySchema;
|
|
413
550
|
querySchema?: QuerySchema;
|
|
414
551
|
paramsSchema?: ParamsSchema;
|
|
552
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
415
553
|
middleware?: [...M];
|
|
416
554
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
417
555
|
put<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -419,6 +557,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
419
557
|
bodySchema?: BodySchema;
|
|
420
558
|
querySchema?: QuerySchema;
|
|
421
559
|
paramsSchema?: ParamsSchema;
|
|
560
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
422
561
|
middleware?: [...M];
|
|
423
562
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
424
563
|
put(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -426,6 +565,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
426
565
|
bodySchema?: BodySchema;
|
|
427
566
|
querySchema?: QuerySchema;
|
|
428
567
|
paramsSchema?: ParamsSchema;
|
|
568
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
429
569
|
middleware?: [...M];
|
|
430
570
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
431
571
|
patch<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -433,6 +573,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
433
573
|
bodySchema?: BodySchema;
|
|
434
574
|
querySchema?: QuerySchema;
|
|
435
575
|
paramsSchema?: ParamsSchema;
|
|
576
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
436
577
|
middleware?: [...M];
|
|
437
578
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
438
579
|
patch(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -440,42 +581,49 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
440
581
|
bodySchema?: BodySchema;
|
|
441
582
|
querySchema?: QuerySchema;
|
|
442
583
|
paramsSchema?: ParamsSchema;
|
|
584
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
443
585
|
middleware?: [...M];
|
|
444
586
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
445
587
|
delete<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
446
588
|
delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
|
|
447
589
|
querySchema?: QuerySchema;
|
|
448
590
|
paramsSchema?: ParamsSchema;
|
|
591
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
449
592
|
middleware?: [...M];
|
|
450
593
|
}, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
451
594
|
delete(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
452
595
|
delete<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
|
|
453
596
|
querySchema?: QuerySchema;
|
|
454
597
|
paramsSchema?: ParamsSchema;
|
|
598
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
455
599
|
middleware?: [...M];
|
|
456
600
|
}, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
457
601
|
options<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
458
602
|
options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
|
|
459
603
|
querySchema?: QuerySchema;
|
|
460
604
|
paramsSchema?: ParamsSchema;
|
|
605
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
461
606
|
middleware?: [...M];
|
|
462
607
|
}, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
463
608
|
options(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
464
609
|
options<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
|
|
465
610
|
querySchema?: QuerySchema;
|
|
466
611
|
paramsSchema?: ParamsSchema;
|
|
612
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
467
613
|
middleware?: [...M];
|
|
468
614
|
}, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
469
615
|
head<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
470
616
|
head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
|
|
471
617
|
querySchema?: QuerySchema;
|
|
472
618
|
paramsSchema?: ParamsSchema;
|
|
619
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
473
620
|
middleware?: [...M];
|
|
474
621
|
}, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
475
622
|
head(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
476
623
|
head<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
|
|
477
624
|
querySchema?: QuerySchema;
|
|
478
625
|
paramsSchema?: ParamsSchema;
|
|
626
|
+
hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
479
627
|
middleware?: [...M];
|
|
480
628
|
}, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
481
629
|
all<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -483,6 +631,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
483
631
|
bodySchema?: BodySchema;
|
|
484
632
|
querySchema?: QuerySchema;
|
|
485
633
|
paramsSchema?: ParamsSchema;
|
|
634
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
486
635
|
middleware?: [...M];
|
|
487
636
|
}, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
488
637
|
all(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
|
|
@@ -490,9 +639,12 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
490
639
|
bodySchema?: BodySchema;
|
|
491
640
|
querySchema?: QuerySchema;
|
|
492
641
|
paramsSchema?: ParamsSchema;
|
|
642
|
+
hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
|
|
493
643
|
middleware?: [...M];
|
|
494
644
|
}, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
|
|
495
645
|
private registerRoute;
|
|
646
|
+
private notifyValidationFailure;
|
|
647
|
+
private runSchemaValidationHook;
|
|
496
648
|
private createBodyValidationMiddleware;
|
|
497
649
|
private createParamsValidationMiddleware;
|
|
498
650
|
private createQueryValidationMiddleware;
|
|
@@ -523,10 +675,12 @@ declare function createTypedRouter<Req extends Record<string, any> = AdditionalR
|
|
|
523
675
|
*
|
|
524
676
|
* @property validateInput - (Future) Whether to enable global input validation.
|
|
525
677
|
* @property errorHandler - Optional global error handler middleware for the router.
|
|
678
|
+
* @property hooks.onValidationFailure - Called for every validation failure on this router, in addition to any per-route hook.
|
|
526
679
|
*/
|
|
527
|
-
interface RouterConfig {
|
|
680
|
+
interface RouterConfig<Req extends Record<string, any> = {}> {
|
|
528
681
|
validateInput?: boolean;
|
|
529
682
|
errorHandler?: (error: any, req: Request, res: Response, next: NextFunction) => void;
|
|
683
|
+
hooks?: RouterHooks<Req>;
|
|
530
684
|
}
|
|
531
685
|
/**
|
|
532
686
|
* Create a new typed router with optional configuration.
|
|
@@ -545,7 +699,7 @@ interface RouterConfig {
|
|
|
545
699
|
* }
|
|
546
700
|
* });
|
|
547
701
|
*/
|
|
548
|
-
declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig): TypedRouter<Req, Locals>;
|
|
702
|
+
declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig<Req>): TypedRouter<Req, Locals>;
|
|
549
703
|
/**
|
|
550
704
|
* Create a new typed router with pre-configured middleware.
|
|
551
705
|
*
|
|
@@ -576,27 +730,16 @@ type RouterDocEntry = TypedRouter<any, any> | {
|
|
|
576
730
|
router: TypedRouter<any, any>;
|
|
577
731
|
};
|
|
578
732
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
733
|
+
* Build the OpenAPI spec object directly, without mounting an Express router
|
|
734
|
+
* or making an HTTP request. For generating openapi.json at build/CI time,
|
|
735
|
+
* separately from running the app. Accepts the same router(s) shape as
|
|
736
|
+
* createDocs.
|
|
581
737
|
*
|
|
582
738
|
* @example
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* // auth.router.ts — routes like /login, /logout
|
|
587
|
-
* export const authRouter = createTypedRouter();
|
|
588
|
-
*
|
|
589
|
-
* // app.ts
|
|
590
|
-
* app.use('/api', usersRouter.getRouter());
|
|
591
|
-
* app.use('/api', authRouter.getRouter());
|
|
592
|
-
* app.use('/docs', createDocs(
|
|
593
|
-
* [
|
|
594
|
-
* { prefix: '/api', router: usersRouter },
|
|
595
|
-
* { prefix: '/api', router: authRouter },
|
|
596
|
-
* ],
|
|
597
|
-
* { title: 'My API', version: '1.0.0' }
|
|
598
|
-
* ));
|
|
739
|
+
* const spec = await generateOpenApiSpec(router, { title: 'My API' });
|
|
740
|
+
* await fs.writeFile('./openapi.json', JSON.stringify(spec, null, 2));
|
|
599
741
|
*/
|
|
742
|
+
declare function generateOpenApiSpec(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): Promise<Record<string, any>>;
|
|
600
743
|
declare function createDocs(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): express.Router & express.RequestHandler;
|
|
601
744
|
//#endregion
|
|
602
|
-
export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
|
|
745
|
+
export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteHooks, RouteOptions, RouteValidationFailureHook, RouteValidationFailureInfo, RouterConfig, RouterDocEntry, RouterHooks, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, SchemaValidationFailureHook, SchemaValidationFailureInfo, TypedMiddleware, TypedRouter, ValidationFailureHook, ValidationFailureInfo, ValidationFailureSource, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, defaultValidationHandler, defineMiddleware, generateOpenApiSpec, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
|
package/dist/schema-router.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import e from"express";import{SchemaError as t}from"@standard-schema/utils";function n(e,n){let r=e;if(r&&r[`~standard`]&&typeof r[`~standard`].validate==`function`){let e=r[`~standard`].validate(n);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new t(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function r(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function i(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const
|
|
1
|
+
import e from"express";import{SchemaError as t}from"@standard-schema/utils";function n(e,n){let r=e;if(r&&r[`~standard`]&&typeof r[`~standard`].validate==`function`){let e=r[`~standard`].validate(n);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new t(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function r(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function i(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}function a(...e){return e}function o(e){e.res.status(400).json({error:`Validation failed`,details:e.details})}function s(e,t){if(e)try{e(t)?.catch?.(()=>{})}catch{}}const c=Function(`m`,`return import(m)`);let l,u;async function d(e){l??=await c(`module`),u??=await c(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(u.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(u.pathToFileURL(r+`/`).href);for(let n of t)try{let t=l.createRequire(n).resolve(e);return await c(u.pathToFileURL(t).href)}catch{}return c(e)}const f=new WeakMap,p=/\(\?<[^>]+>/;function m(e){return typeof e==`string`&&p.test(e)?new RegExp(e):e}function h(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function g(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function _(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function v(e){let t=0;return _(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function y(e){if(typeof e.path==`string`||e.pathExample)return[h(e)];let t=g(e.path.source);return t.length===1?[h(e)]:t.map(v)}function b(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/\*([A-Za-z0-9_]+)/g,`{$1}`).replace(/^\^|\$$/g,``),n=0;return t.replace(/\*/g,()=>`{${n++}}`).replace(/\/{2,}/g,`/`)}function x(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/^\^|\$$/g,``),n=[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g),...t.matchAll(/\*([A-Za-z0-9_]+)/g)].map(e=>e[1]),r=(t.replace(/\*[A-Za-z0-9_]+/g,``).match(/\*/g)??[]).length;for(let e=0;e<r;e++)n.push(String(e));return n}function S(e){return e.startsWith(`:`)||e.includes(`*`)||e.includes(`(?<`)}function C(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!S(e))??`default`}function w(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!S(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function T(e){let t=f.get(e);if(t)return t;let n=await E(e);return f.set(e,n),n}async function E(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await d(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await d(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await d(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await d(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function D(e){return O(e,0,new WeakSet)}function O(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return O(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=O(e[0],t+1,n);for(let a=1;a<r;a++)i=j(i,O(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=O(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function k(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function A(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function j(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...k(e),...k(t)]),r={},i=A(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=j(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=j(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function M(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}const N=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function P(e,t,n){return`<!doctype html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
-
<title>${
|
|
4
|
+
<title>${M(e)}</title>
|
|
5
5
|
<meta charset="utf-8" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
|
-
<script id="api-reference" data-url="${
|
|
10
|
-
<script src="${
|
|
9
|
+
<script id="api-reference" data-url="${M(t)}"><\/script>
|
|
10
|
+
<script src="${M(n)}"><\/script>
|
|
11
11
|
</body>
|
|
12
|
-
</html>`}async function M(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of g(t)){let r=_(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await S(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=v(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await S(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??x(t.method,e),tags:t.tags??[b(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await S(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await S(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const N=new WeakMap;var P=class t{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=e.Router(),N.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(e,...n){let r=typeof e==`string`,i=r?e:``,a=(r?n:[e,...n]).map(e=>{if(e instanceof t)return this.trackMounted(i,e),e.getRouter();let n=N.get(e);return n&&this.trackMounted(i,n),e});return r?this.router.use(e,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+f(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of g(e)){let i=r[_(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(t={}){let n=e.Router(),r;if(t.specOutputPath){let e=t.specOutputPath,n=!1,i=!1,o=async()=>{if(n){i=!0;return}n=!0;try{let n=await M(this.getRouteMetadata(),t),r=await a(`fs/promises`),i=e.replace(/[/\\][^/\\]*$/,``);i&&i!==e&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let o=globalThis.process?.pid??`0`,s=`${e}.${o}.tmp`;await r.writeFile(s,JSON.stringify(n,null,2),`utf8`),await r.rename(s,e)}catch{}finally{n=!1,i&&(i=!1,o())}},s;r=()=>{s&&clearTimeout(s),s=setTimeout(o,300),s.unref?.()},setImmediate(async()=>{try{let t=await(await a(`fs/promises`)).readFile(e,`utf8`).catch(()=>null);if(t)try{this.hydrateResponses(JSON.parse(t))}catch{}}catch{}await o()})}return t.sampleResponses!==!1&&this.enableSampling(t.sampleResponses===`live`?`live`:`redacted`,r),n.get(`/openapi.json`,async(e,n)=>{try{let e=await M(this.getRouteMetadata(),t);n.json(e)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??A;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(j(t.title??`API`,r,i))}),n}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.paramsSchema=e.paramsSchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,a.pathExample=e.pathExample,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),e.paramsSchema&&i.push(this.createParamsValidationMiddleware(e.paramsSchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=w(t),i=a.responseSamples.get(n),o=i?O(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](d(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.body),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}t.body=o&&`value`in o?o.value:o,a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}createParamsValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.params),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}t.params=o&&`value`in o?o.value:o,a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}createQueryValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.query),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}let s=o&&`value`in o?o.value:o;Object.defineProperty(t,"query",{value:s,writable:!1,enumerable:!0,configurable:!0}),a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}};function F(){return new P}function I(e){let t=new P;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function L(...e){let t=new P;for(let n of e)t=t.useMiddleware(n);return t}function R(t,n={}){let r=(Array.isArray(t)?t:[t]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(n.sampleResponses!==!1){let e=n.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of r)t.enableSampling(e)}let i=e.Router();return i.get(`/openapi.json`,async(e,t)=>{try{let e=await M(r.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+f(t)}}))),n);t.json(e)}catch(e){t.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),i.get(`/`,(e,t)=>{let r=`${e.baseUrl}/openapi.json`,i=n.cdnUrl??A;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(j(n.title??`API`,r,i))}),i}export{P as TypedRouter,R as createDocs,F as createTypedRouter,I as createTypedRouterWithConfig,L as createTypedRouterWithMiddleware,w as inferJsonSchema,i as isSchemaError,n as parseSchema,r as safeParseSchema};
|
|
12
|
+
</html>`}async function F(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of y(t)){let r=b(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await T(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=x(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await T(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??w(t.method,e),tags:t.tags??[C(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await T(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await T(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const I=new WeakMap;var L=class t{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;globalValidationFailureHook;constructor(){this.router=e.Router(),I.set(this.router,this)}onValidationFailure(e){return this.globalValidationFailureHook=e,this}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(e,...n){let r=typeof e==`string`,i=r?e:``,a=(r?n:[e,...n]).map(e=>{if(e instanceof t)return this.trackMounted(i,e),e.getRouter();let n=I.get(e);return n&&this.trackMounted(i,n),e});return r?this.router.use(e,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+h(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of y(e)){let i=r[b(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(t={}){let n=e.Router(),r;if(t.specOutputPath){let e=t.specOutputPath,n=!1,i=!1,a=async()=>{if(n){i=!0;return}n=!0;try{let n=await F(this.getRouteMetadata(),t),r=await c(`fs/promises`),i=e.replace(/[/\\][^/\\]*$/,``);i&&i!==e&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${e}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,e)}catch{}finally{n=!1,i&&(i=!1,a())}},o;r=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let t=await(await c(`fs/promises`)).readFile(e,`utf8`).catch(()=>null);if(t)try{this.hydrateResponses(JSON.parse(t))}catch{}}catch{}await a()})}return t.sampleResponses!==!1&&this.enableSampling(t.sampleResponses===`live`?`live`:`redacted`,r),n.get(`/openapi.json`,async(e,n)=>{try{let e=await F(this.getRouteMetadata(),t);n.json(e)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??N;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(P(t.title??`API`,r,i))}),n}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let o=n;a.bodySchema=o.bodySchema,a.querySchema=o.querySchema,a.paramsSchema=o.paramsSchema,a.tags=o.tags,a.description=o.description,a.summary=o.summary,a.deprecated=o.deprecated,a.responseSchema=o.responseSchema,a.hidden=o.hidden,a.pathExample=o.pathExample,o.middleware&&i.push(...o.middleware);let s=o.hooks?.onValidationFailure,c=o.hooks?.onBodyValidationFailure,l=o.hooks?.onQueryValidationFailure,u=o.hooks?.onParamsValidationFailure;o.bodySchema&&i.push(this.createBodyValidationMiddleware(o.bodySchema,e,t,o.pathExample,s,c)),o.querySchema&&i.push(this.createQueryValidationMiddleware(o.querySchema,e,t,o.pathExample,s,l)),o.paramsSchema&&i.push(this.createParamsValidationMiddleware(o.paramsSchema,e,t,o.pathExample,s,u)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=D(t),i=a.responseSamples.get(n),o=i?j(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](m(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}notifyValidationFailure(e,t,n,r,i,a,o){if(!o&&!this.globalValidationFailureHook)return;let c={source:e,error:t,details:n,method:r,path:i,req:a};s(o,c),s(this.globalValidationFailureHook,c)}async runSchemaValidationHook(e,t,n,r,i,a,o,s){if(!e)return!1;let c=!1,l={error:t,details:n,method:r,path:i,req:a,res:o,next:((...e)=>{c=!0,s(...e)})};try{await e(l)}catch{}return c||o.headersSent}createBodyValidationMiddleware(e,t,n,a,s,c){return async(l,u,d)=>{try{let i=r(e,l.body),f=i&&typeof i.then==`function`?await i:i;if(f&&`issues`in f&&f.issues){let e=f.errors||f.issues,r=h({path:n,pathExample:a}),i=await this.runSchemaValidationHook(c,`Validation failed`,e,t,r,l,u,d);this.notifyValidationFailure(`body`,`Validation failed`,e,t,r,l,s),i||o({res:u,details:e});return}l.body=f&&`value`in f?f.value:f,d()}catch(e){if(i(e)){let r=e.errors||e.issues,i=h({path:n,pathExample:a}),f=await this.runSchemaValidationHook(c,`Validation failed`,r,t,i,l,u,d);this.notifyValidationFailure(`body`,`Validation failed`,r,t,i,l,s),f||o({res:u,details:r})}else d(e)}}}createParamsValidationMiddleware(e,t,n,a,s,c){return async(l,u,d)=>{try{let i=r(e,l.params),f=i&&typeof i.then==`function`?await i:i;if(f&&`issues`in f&&f.issues){let e=f.errors||f.issues,r=h({path:n,pathExample:a}),i=await this.runSchemaValidationHook(c,`Validation failed`,e,t,r,l,u,d);this.notifyValidationFailure(`params`,`Validation failed`,e,t,r,l,s),i||o({res:u,details:e});return}l.params=f&&`value`in f?f.value:f,d()}catch(e){if(i(e)){let r=e.errors||e.issues,i=h({path:n,pathExample:a}),f=await this.runSchemaValidationHook(c,`Validation failed`,r,t,i,l,u,d);this.notifyValidationFailure(`params`,`Validation failed`,r,t,i,l,s),f||o({res:u,details:r})}else d(e)}}}createQueryValidationMiddleware(e,t,n,a,s,c){return async(l,u,d)=>{try{let i=r(e,l.query),f=i&&typeof i.then==`function`?await i:i;if(f&&`issues`in f&&f.issues){let e=f.errors||f.issues,r=h({path:n,pathExample:a}),i=await this.runSchemaValidationHook(c,`Validation failed`,e,t,r,l,u,d);this.notifyValidationFailure(`query`,`Validation failed`,e,t,r,l,s),i||o({res:u,details:e});return}let p=f&&`value`in f?f.value:f;Object.defineProperty(l,"query",{value:p,writable:!1,enumerable:!0,configurable:!0}),d()}catch(e){if(i(e)){let r=e.errors||e.issues,i=h({path:n,pathExample:a}),f=await this.runSchemaValidationHook(c,`Validation failed`,r,t,i,l,u,d);this.notifyValidationFailure(`query`,`Validation failed`,r,t,i,l,s),f||o({res:u,details:r})}else d(e)}}}};function R(){return new L}function z(e){let t=new L;return e?.errorHandler&&t.getRouter().use(e.errorHandler),e?.hooks?.onValidationFailure&&t.onValidationFailure(e.hooks.onValidationFailure),t}function B(...e){let t=new L;for(let n of e)t=t.useMiddleware(n);return t}function V(e){return(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e}).flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+h(t)}})))}async function H(e,t={}){return F(V(e),t)}function U(t,n={}){if(n.sampleResponses!==!1){let e=n.sampleResponses===`live`?`live`:`redacted`,r=Array.isArray(t)?t:[t];for(let t of r)(`prefix`in t?t.router:t).enableSampling(e)}let r=e.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await H(t,n);r.json(e)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),r.get(`/`,(e,t)=>{let r=`${e.baseUrl}/openapi.json`,i=n.cdnUrl??N;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(P(n.title??`API`,r,i))}),r}export{L as TypedRouter,U as createDocs,R as createTypedRouter,z as createTypedRouterWithConfig,B as createTypedRouterWithMiddleware,o as defaultValidationHandler,a as defineMiddleware,H as generateOpenApiSpec,D as inferJsonSchema,i as isSchemaError,n as parseSchema,r as safeParseSchema};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minisylar/express-typed-router",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.9",
|
|
4
4
|
"description": "A strongly-typed Express router with Zod validation and automatic type inference for params, body, query, and middleware",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/schema-router.cjs",
|