@croutonian/with-openapi 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -44
- package/dist/index.d.ts +292 -29
- package/dist/index.js +34 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
[](https://github.com/croutonian/with-openapi/actions/workflows/ci.yml)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
|
|
9
|
-
OpenAPI middleware for
|
|
9
|
+
OpenAPI middleware for
|
|
10
|
+
[`@supabase/middleware`](https://github.com/supabase/middleware).
|
|
10
11
|
|
|
11
12
|
An OpenAPI document already says what your API accepts. This makes it say it at
|
|
12
13
|
runtime: every request is matched to an Operation Object, optionally refused if
|
|
@@ -24,7 +25,8 @@ export default {
|
|
|
24
25
|
[withOpenApi({ document, reference: true })],
|
|
25
26
|
async (_req, ctx) => {
|
|
26
27
|
if (!ctx.openapi.matched) return new Response(null, { status: 404 })
|
|
27
|
-
// Already validated
|
|
28
|
+
// Already validated and coerced, so this holds the number 10 — though
|
|
29
|
+
// its static type is `unknown`; see Parameters.
|
|
28
30
|
const { limit } = ctx.openapi.params.query
|
|
29
31
|
return Response.json({ operation: ctx.openapi.operationId, limit })
|
|
30
32
|
},
|
|
@@ -68,17 +70,35 @@ A discriminated union on `matched`:
|
|
|
68
70
|
```ts
|
|
69
71
|
if (ctx.openapi.matched) {
|
|
70
72
|
ctx.openapi.route // '/users/{id}' — the path template, not the pathname
|
|
71
|
-
ctx.openapi.method // 'get'
|
|
72
73
|
ctx.openapi.operation // the Operation Object, `$ref` already followed
|
|
73
74
|
ctx.openapi.operationId // 'getUser'
|
|
74
75
|
ctx.openapi.security // the operation's, falling back to the document's
|
|
75
76
|
ctx.openapi.params // { path, query, header, cookie }, deserialized + coerced
|
|
76
|
-
ctx.openapi.
|
|
77
|
-
ctx.openapi.mediaType // the `content` key that matched
|
|
78
|
-
ctx.openapi.validated // false when `validate: false`
|
|
77
|
+
ctx.openapi.mediaType // which `content` key matched
|
|
79
78
|
}
|
|
80
79
|
```
|
|
81
80
|
|
|
81
|
+
`params` is `unknown` here. Give the middleware a document whose type survived
|
|
82
|
+
and it narrows to that operation's own parameters — see
|
|
83
|
+
[Typed against your document](#typed-against-your-document).
|
|
84
|
+
|
|
85
|
+
Everything here is something only this middleware knows. What you already have,
|
|
86
|
+
it does not hand back: not the `document` you passed in, not the method
|
|
87
|
+
(`req.method`), not whether you configured `validate: false`, and not the parsed
|
|
88
|
+
body — `req.json()` is one call and reading it here does not consume it.
|
|
89
|
+
|
|
90
|
+
The parsed body is **not** here. Reading it in the middleware does not consume
|
|
91
|
+
it — the framework hands every layer a buffered request — so the handler calls
|
|
92
|
+
`req.json()` and gets the very bytes this middleware validated, without a second
|
|
93
|
+
copy on `ctx` to keep in step with it.
|
|
94
|
+
|
|
95
|
+
`mediaType` is contributed because it is the reverse case: it names which
|
|
96
|
+
`content` key matched, and so which schema ran, and repeating that takes
|
|
97
|
+
resolving a `$ref` on `requestBody` and reimplementing the exact / `type/*` /
|
|
98
|
+
`*/*` precedence. It is **not** the request's content type — a `*/*` range
|
|
99
|
+
matches anything — so a handler deciding how to parse should read the header,
|
|
100
|
+
which is what this middleware does too.
|
|
101
|
+
|
|
82
102
|
With the defaults, the handler only ever sees `matched: true` — anything else
|
|
83
103
|
was already answered with a `404` or a `405`. The narrowing matters once you set
|
|
84
104
|
`onUnknownRoute` or `onUnknownMethod` to `'pass'`, or pass a `skip`; then the
|
|
@@ -129,10 +149,10 @@ callback before that response is built:
|
|
|
129
149
|
| `unsupported_media_type` | 415 | The body's content type is not in the operation's `content`. |
|
|
130
150
|
| `validation_failed` | 400 | A parameter or body failed its schema. |
|
|
131
151
|
|
|
132
|
-
`route_not_found` says which of its two causes it was, because they are the
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
152
|
+
`route_not_found` says which of its two causes it was, because they are the same
|
|
153
|
+
status and very different mistakes — a `basePath` nothing starts with turns
|
|
154
|
+
every route into a 404, and blaming the document sends you looking for a path
|
|
155
|
+
that is already in it:
|
|
136
156
|
|
|
137
157
|
```
|
|
138
158
|
no operation in the API description matches "/nope"
|
|
@@ -164,10 +184,10 @@ path to the property that failed.
|
|
|
164
184
|
|
|
165
185
|
### Descriptions
|
|
166
186
|
|
|
167
|
-
`message` is the validator's, and says what is mechanically wrong.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
187
|
+
`message` is the validator's, and says what is mechanically wrong. `description`
|
|
188
|
+
is the **document's own prose** for whatever failed, and is usually the half a
|
|
189
|
+
caller can act on. You wrote it once; there is no reason for an error response
|
|
190
|
+
to throw it away.
|
|
171
191
|
|
|
172
192
|
It is resolved from the most specific place that has it:
|
|
173
193
|
|
|
@@ -192,14 +212,14 @@ body Instance does not have required property "name".
|
|
|
192
212
|
That third row is the one worth pointing at: `required` fails against the
|
|
193
213
|
_object_, so the obvious implementation describes the object — "A person with
|
|
194
214
|
access to the workspace" — which says nothing about what is missing. The
|
|
195
|
-
property is named only inside the validator's message, so it is read from
|
|
196
|
-
|
|
215
|
+
property is named only inside the validator's message, so it is read from there,
|
|
216
|
+
and falls back to the container's prose if that wording ever changes.
|
|
197
217
|
|
|
198
218
|
A field with nothing written about it simply has no `description`. Set
|
|
199
|
-
`validate: { describe: false }` to leave them all off — descriptions are
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
`validate: { describe: false }` to leave them all off — descriptions are written
|
|
220
|
+
for a document's consumers, who are the same people reading these errors, but
|
|
221
|
+
turn it off if yours carries notes you would rather not return in a response
|
|
222
|
+
body.
|
|
203
223
|
|
|
204
224
|
To answer in your own error envelope:
|
|
205
225
|
|
|
@@ -261,8 +281,8 @@ reference: {
|
|
|
261
281
|
}
|
|
262
282
|
```
|
|
263
283
|
|
|
264
|
-
A path you give explicitly is taken **literally** — `basePath` is not applied
|
|
265
|
-
|
|
284
|
+
A path you give explicitly is taken **literally** — `basePath` is not applied to
|
|
285
|
+
it, so an API under `/api/v1` can still put its docs at `/docs`:
|
|
266
286
|
|
|
267
287
|
```ts
|
|
268
288
|
withOpenApi({ document, basePath: '/api/v1', reference: { path: '/docs' } })
|
|
@@ -299,6 +319,13 @@ withOpenApi({
|
|
|
299
319
|
|
|
300
320
|
## Parameters
|
|
301
321
|
|
|
322
|
+
Every parameter is typed `unknown`, whatever its schema says. The document is
|
|
323
|
+
data this middleware reads at runtime, and turning a schema into a TypeScript
|
|
324
|
+
type needs code generation, which is not something this package does. So the
|
|
325
|
+
_value_ is coerced and checked — `params.query.limit` holds the number `10` —
|
|
326
|
+
while the _type_ you hover is `unknown`, and narrowing it is yours. Anything
|
|
327
|
+
stronger would be a type that lies when a document changes without a rebuild.
|
|
328
|
+
|
|
302
329
|
`style` and `explode` are honored, so the document decides how a value is
|
|
303
330
|
spelled:
|
|
304
331
|
|
|
@@ -337,18 +364,121 @@ the request says it is:
|
|
|
337
364
|
| `multipart/form-data` | object, with parts left as `File` | no |
|
|
338
365
|
| anything else | not read at all | no |
|
|
339
366
|
|
|
340
|
-
Multipart parts are `File` objects, which no JSON Schema describes, so
|
|
341
|
-
is parsed
|
|
342
|
-
|
|
343
|
-
is
|
|
367
|
+
Multipart parts are `File` objects, which no JSON Schema describes, so a
|
|
368
|
+
multipart body is parsed — enough to reject a malformed one and to enforce
|
|
369
|
+
`required` — but never schema-checked. Binary media types are never buffered:
|
|
370
|
+
there is no shape to check, and reading a large upload to ignore it is pure
|
|
371
|
+
cost.
|
|
372
|
+
|
|
373
|
+
Two consequences of the body not being contributed, both worth knowing: reading
|
|
374
|
+
it here does not consume it, so `req.json()` in the handler returns the same
|
|
375
|
+
value that was validated; and the coercion in the urlencoded row is applied for
|
|
376
|
+
validation and then dropped, so a handler reading that body itself sees the
|
|
377
|
+
original text.
|
|
378
|
+
|
|
379
|
+
## Typed against your document
|
|
380
|
+
|
|
381
|
+
`ctx.openapi.params` is `unknown` by default, because the middleware reads
|
|
382
|
+
whatever document it is handed at runtime. Hand it a document whose type
|
|
383
|
+
survived, though, and it narrows to that document's own operations:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import { pipeline } from '@supabase/middleware'
|
|
387
|
+
import { withOpenApi, defineDocument } from '@croutonian/with-openapi'
|
|
388
|
+
|
|
389
|
+
const document = defineDocument({
|
|
390
|
+
openapi: '3.1.0',
|
|
391
|
+
info: { title: 'Acme', version: '1' },
|
|
392
|
+
paths: {
|
|
393
|
+
'/users/{id}': {
|
|
394
|
+
get: {
|
|
395
|
+
operationId: 'getUser',
|
|
396
|
+
parameters: [
|
|
397
|
+
{
|
|
398
|
+
name: 'id',
|
|
399
|
+
in: 'path',
|
|
400
|
+
required: true,
|
|
401
|
+
schema: { type: 'integer' },
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
responses: { '200': { description: 'ok' } },
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
'/users': {
|
|
408
|
+
get: {
|
|
409
|
+
operationId: 'listUsers',
|
|
410
|
+
parameters: [
|
|
411
|
+
{ name: 'limit', in: 'query', schema: { type: 'integer' } },
|
|
412
|
+
],
|
|
413
|
+
responses: { '200': { description: 'ok' } },
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
export default {
|
|
420
|
+
fetch: pipeline([withOpenApi({ document })], async (_req, ctx) => {
|
|
421
|
+
if (!ctx.openapi.matched) return new Response(null, { status: 404 })
|
|
422
|
+
|
|
423
|
+
if (ctx.openapi.operationId === 'getUser') {
|
|
424
|
+
ctx.openapi.params.path.id // number
|
|
425
|
+
}
|
|
426
|
+
if (ctx.openapi.operationId === 'listUsers') {
|
|
427
|
+
ctx.openapi.params.query.limit // number | undefined
|
|
428
|
+
}
|
|
429
|
+
return Response.json({})
|
|
430
|
+
}),
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
`ctx.openapi` becomes one branch per declared operation, discriminated by
|
|
435
|
+
`operationId` (or `route`). Narrowing is all it takes — no cast, no route
|
|
436
|
+
argument, no runtime guard, and the compiler knows when you have handled every
|
|
437
|
+
operation.
|
|
438
|
+
|
|
439
|
+
### The document has to keep its type
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
withOpenApi({ document: { /* literal inline */ } }) // ✅
|
|
443
|
+
const document = defineDocument({ ... }) // ✅ in its own module
|
|
444
|
+
const document = { ... } satisfies OpenAPIObject // ⚠️ see below
|
|
445
|
+
const document: OpenAPIObject = { ... } // ❌ falls back to unknown
|
|
446
|
+
import document from './openapi.json' // ❌ fails to compile
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`defineDocument` is an identity function whose only job is its `const` type
|
|
450
|
+
parameter. `satisfies` gets close — route names and schemas survive it — but it
|
|
451
|
+
still lets leaf values widen where the target type says `string`, so
|
|
452
|
+
`servers[0].url` comes back as `string` rather than the URL you wrote.
|
|
453
|
+
|
|
454
|
+
An annotation is the one that costs you silently: `paths` widens to an index
|
|
455
|
+
signature, and the contribution **falls back to the unspecialized shape**, so
|
|
456
|
+
everything keeps working and `params` stays `unknown`. That fallback is
|
|
457
|
+
deliberate — it is what makes this additive rather than breaking. A `.json`
|
|
458
|
+
import fails earlier and louder: its values widen too, so `in: string` no
|
|
459
|
+
longer narrows to a `ParameterLocation` and the document fails the constraint.
|
|
460
|
+
|
|
461
|
+
### What the projections cover
|
|
462
|
+
|
|
463
|
+
A deliberate subset of JSON Schema, not an implementation of it — the runtime
|
|
464
|
+
validator remains the authority. Covered: `$ref` into `#/components/schemas`
|
|
465
|
+
and `#/components/parameters`, `const`, `enum`, `string` / `integer` /
|
|
466
|
+
`number` / `boolean` / `null`, arrays via `items`, objects via `properties`
|
|
467
|
+
with `required` driving optionality, and Path Item parameters merged into every
|
|
468
|
+
operation beneath them. `$ref` chains are followed eight levels deep, so a
|
|
469
|
+
recursive schema terminates.
|
|
470
|
+
|
|
471
|
+
Not interpreted: `allOf`, `oneOf`, `anyOf`. Those resolve to `unknown`, which
|
|
472
|
+
is what the untyped path gives you anyway — guessing at them is how a type
|
|
473
|
+
starts disagreeing with the validator that actually runs.
|
|
344
474
|
|
|
345
|
-
|
|
346
|
-
|
|
475
|
+
`RoutesOf`, `MethodsOf`, `OperationIdsOf`, `OperationOf`, `ParamsFor` and
|
|
476
|
+
`FromSchema` are exported for reading the same document yourself.
|
|
347
477
|
|
|
348
478
|
## CORS
|
|
349
479
|
|
|
350
|
-
An OpenAPI document already knows most of a CORS policy. `cors` derives it,
|
|
351
|
-
|
|
480
|
+
An OpenAPI document already knows most of a CORS policy. `cors` derives it, per
|
|
481
|
+
route:
|
|
352
482
|
|
|
353
483
|
```ts
|
|
354
484
|
withOpenApi({
|
|
@@ -376,15 +506,15 @@ comes from.
|
|
|
376
506
|
Three things worth knowing:
|
|
377
507
|
|
|
378
508
|
- **`origin` is required and never derived.** A document says where an API
|
|
379
|
-
lives, not who may call it — `servers` is not an allowlist, and treating it
|
|
380
|
-
|
|
509
|
+
lives, not who may call it — `servers` is not an allowlist, and treating it as
|
|
510
|
+
one would be a security decision made from the wrong data. Same for
|
|
381
511
|
`credentials`.
|
|
382
512
|
- **Rejections are stamped too.** An unstamped `400` reaches a browser as an
|
|
383
513
|
opaque CORS error rather than the violations it is carrying.
|
|
384
514
|
- **The document is the source of truth for headers.** A request header the API
|
|
385
515
|
reads but the document does not declare will be refused by the browser. That
|
|
386
|
-
is usually the document being wrong; `allowedHeaders` is the escape hatch
|
|
387
|
-
|
|
516
|
+
is usually the document being wrong; `allowedHeaders` is the escape hatch when
|
|
517
|
+
it genuinely is not.
|
|
388
518
|
|
|
389
519
|
Preflights are answered after the route match but before the method lookup —
|
|
390
520
|
otherwise the `OPTIONS` no document declares an operation for would come back
|
|
@@ -421,11 +551,11 @@ Worth knowing before you wire this into something:
|
|
|
421
551
|
translating it. A 3.0 document falls back to draft 4, which gets
|
|
422
552
|
`exclusiveMinimum` and `required` right but does **not** translate `nullable`.
|
|
423
553
|
Convert to 3.1 for full fidelity.
|
|
424
|
-
- **Local `$ref`s only.** External and remote references are not fetched.
|
|
425
|
-
|
|
554
|
+
- **Local `$ref`s only.** External and remote references are not fetched. Bundle
|
|
555
|
+
the document first.
|
|
426
556
|
- **Requests only.** Responses are not validated. That is the framework's model,
|
|
427
|
-
not an omission: a middleware runs before the handler, and response shape
|
|
428
|
-
under the handler's ownership.
|
|
557
|
+
not an omission: a middleware runs before the handler, and response shape
|
|
558
|
+
stays under the handler's ownership.
|
|
429
559
|
- **`deepObject` is one level deep**, matching what the specification defines.
|
|
430
560
|
- **Trailing slashes are normalized**, so `/users` and `/users/` are one route.
|
|
431
561
|
- Indexing the document for validation stamps each node with its own absolute
|
|
@@ -444,8 +574,8 @@ Three, and each is load-bearing:
|
|
|
444
574
|
- **`@cfworker/json-schema`** — the validator. Zero dependencies, and it
|
|
445
575
|
_interprets_ schemas rather than compiling them to JavaScript, which is what
|
|
446
576
|
lets it run on Cloudflare Workers and anywhere else `new Function` is
|
|
447
|
-
unavailable. The document is walked once at construction and every subschema
|
|
448
|
-
it validated against that one index, so `$ref` — recursive ones included —
|
|
577
|
+
unavailable. The document is walked once at construction and every subschema
|
|
578
|
+
in it validated against that one index, so `$ref` — recursive ones included —
|
|
449
579
|
resolves without inlining anything.
|
|
450
580
|
- **`@supabase/middleware`** — the composition engine.
|
|
451
581
|
|
|
@@ -518,10 +648,10 @@ resolves that by publishing from your machine — your npm login, your 2FA, no
|
|
|
518
648
|
token created and none stored. CI takes over from the next release.
|
|
519
649
|
|
|
520
650
|
`release.yml` also accepts an `NPM_TOKEN` secret, but it is not a general
|
|
521
|
-
answer: a token cannot answer a one-time password, and npm asks for one on
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
651
|
+
answer: a token cannot answer a one-time password, and npm asks for one on every
|
|
652
|
+
write unless the account's two-factor setting is _Authorization only_. Where 2FA
|
|
653
|
+
covers writes, a CI publish fails with `EOTP` and the first version has to come
|
|
654
|
+
from a human.
|
|
525
655
|
|
|
526
656
|
Between releases, every branch push and pull request publishes an installable
|
|
527
657
|
preview to [pkg.pr.new](https://pkg.pr.new):
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FetchHandler, Middleware } from "@supabase/middleware";
|
|
1
|
+
import { FetchHandler, Middleware, SingleKeyEntry } from "@supabase/middleware";
|
|
2
2
|
import { SchemaDraft } from "@cfworker/json-schema";
|
|
3
3
|
import { OpenAPIObject, OperationObject, ParameterLocation, ParameterStyle, ReferenceObject, SchemaObject, SecurityRequirementObject } from "openapi3-ts/oas31";
|
|
4
4
|
|
|
@@ -198,7 +198,16 @@ interface ScalarReferenceOptions {
|
|
|
198
198
|
//#region src/types.d.ts
|
|
199
199
|
/** Where a parameter was declared. */
|
|
200
200
|
type ParameterIn = 'path' | 'query' | 'header' | 'cookie';
|
|
201
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Deserialized, coerced parameters, grouped by where they came from.
|
|
203
|
+
*
|
|
204
|
+
* `unknown` rather than the schema's type: the document is read at runtime, and
|
|
205
|
+
* lifting a schema into a TypeScript type needs code generation, which this
|
|
206
|
+
* package does not do. The value is coerced and checked; narrowing the type is
|
|
207
|
+
* the consumer's. Pinned by `N5` in `type-tests/negative.ts`, so widening this
|
|
208
|
+
* to `any` — which would compile and look safer while checking nothing — fails
|
|
209
|
+
* the build.
|
|
210
|
+
*/
|
|
202
211
|
interface OpenApiParams {
|
|
203
212
|
readonly path: Readonly<Record<string, unknown>>;
|
|
204
213
|
readonly query: Readonly<Record<string, unknown>>;
|
|
@@ -267,7 +276,7 @@ interface OpenApiValidateOptions {
|
|
|
267
276
|
cookie?: boolean;
|
|
268
277
|
/**
|
|
269
278
|
* Check — and therefore read and parse — the request body. With this off,
|
|
270
|
-
*
|
|
279
|
+
* the body is never read here and reaches the handler unexamined.
|
|
271
280
|
*
|
|
272
281
|
* @defaultValue `true`
|
|
273
282
|
*/
|
|
@@ -392,54 +401,119 @@ interface WithOpenApiConfig {
|
|
|
392
401
|
*/
|
|
393
402
|
reject?: (rejection: OpenApiRejection, req: Request) => Response | undefined | Promise<Response | undefined>;
|
|
394
403
|
}
|
|
395
|
-
/**
|
|
404
|
+
/**
|
|
405
|
+
* `ctx.openapi` when an operation in the document describes the request.
|
|
406
|
+
*
|
|
407
|
+
* Every field here is something a consumer would otherwise have to derive from
|
|
408
|
+
* the document itself. What the caller already holds — the document, the
|
|
409
|
+
* method, the validate config, the request body — is deliberately absent.
|
|
410
|
+
*/
|
|
396
411
|
interface OpenApiMatched {
|
|
397
412
|
readonly matched: true;
|
|
398
|
-
/**
|
|
399
|
-
|
|
400
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Path template that matched, e.g. `'/users/{id}'`.
|
|
415
|
+
*
|
|
416
|
+
* The label to group a request under. A pathname cannot be used for that —
|
|
417
|
+
* `/users/1` and `/users/2` are separate series — so this is what metrics,
|
|
418
|
+
* traces, rate-limit buckets and audit logs key on.
|
|
419
|
+
*/
|
|
401
420
|
readonly route: string;
|
|
402
|
-
/**
|
|
403
|
-
|
|
404
|
-
|
|
421
|
+
/**
|
|
422
|
+
* The Operation Object, with its own `$ref` (if any) already followed.
|
|
423
|
+
*
|
|
424
|
+
* How a consumer drives behaviour off the document: `x-` extensions
|
|
425
|
+
* (`operation['x-rate-limit']`, `x-required-scope`), `deprecated` to stamp a
|
|
426
|
+
* sunset header, `tags` to attribute a request to the team that owns it.
|
|
427
|
+
*
|
|
428
|
+
* Here rather than left to a `document.paths` lookup because a Path Item can
|
|
429
|
+
* itself be a `$ref`, so that lookup is not reliably an Operation Object.
|
|
430
|
+
*/
|
|
405
431
|
readonly operation: OperationObject;
|
|
432
|
+
/**
|
|
433
|
+
* A name for this operation that survives the path changing, which `route`
|
|
434
|
+
* does not — so it is the stable key for a permission check, a handler
|
|
435
|
+
* dispatch table, or a log field you intend to query next year.
|
|
436
|
+
*
|
|
437
|
+
* Read through from {@link operation}, so it adds no information; it is here
|
|
438
|
+
* because it is the field consumers reach for most.
|
|
439
|
+
*/
|
|
406
440
|
readonly operationId: string | undefined;
|
|
407
441
|
/**
|
|
408
442
|
* Security requirements in force — the operation's, falling back to the
|
|
409
|
-
* document's.
|
|
410
|
-
*
|
|
443
|
+
* document's. Never enforced here; contributed so a downstream auth layer
|
|
444
|
+
* can enforce it without a route table of its own, and tell a public
|
|
445
|
+
* operation (`[]`) from one that needs a credential.
|
|
446
|
+
*
|
|
447
|
+
* The fallback is the reason this is not left to the consumer: reading
|
|
448
|
+
* `operation.security` alone treats a document-secured operation as public,
|
|
449
|
+
* which fails open.
|
|
411
450
|
*/
|
|
412
451
|
readonly security: SecurityRequirementObject[] | undefined;
|
|
413
|
-
/**
|
|
452
|
+
/**
|
|
453
|
+
* Deserialized and (unless turned off) coerced parameters, keyed by
|
|
454
|
+
* location.
|
|
455
|
+
*
|
|
456
|
+
* The one that saves real work: `params.query.limit` holds a number, already
|
|
457
|
+
* checked against its `maximum`, with `style`/`explode` honoured, so
|
|
458
|
+
* `?ids=1,2,3` arrives as an array and `?filter[a]=b` as an object. Without
|
|
459
|
+
* it every handler re-reads `URLSearchParams` and re-coerces by hand.
|
|
460
|
+
*
|
|
461
|
+
* Values only — every entry is typed `unknown`. See {@link OpenApiParams}.
|
|
462
|
+
*/
|
|
414
463
|
readonly params: OpenApiParams;
|
|
415
464
|
/**
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
465
|
+
* Which `content` key matched, and so which Media Type Object's schema the
|
|
466
|
+
* body was checked against.
|
|
467
|
+
*
|
|
468
|
+
* Worth having when an operation declares more than one: it tells a handler
|
|
469
|
+
* which contract it is serving (`application/json` against
|
|
470
|
+
* `application/vnd.acme.v2+json`, say), and tells telemetry which declared
|
|
471
|
+
* variant clients actually send, which is what you need before deprecating
|
|
472
|
+
* one.
|
|
473
|
+
*
|
|
474
|
+
* Not a substitute for the request's own `Content-Type`: a catch-all range
|
|
475
|
+
* matches anything, so this says which *declaration* applied, not what was
|
|
476
|
+
* actually sent. A handler choosing how to parse wants the header — which is
|
|
477
|
+
* what this middleware reads too.
|
|
478
|
+
*
|
|
479
|
+
* Contributed because it is the one decision made here that a consumer
|
|
480
|
+
* cannot cheaply repeat: it takes resolving a `$ref` on `requestBody` and
|
|
481
|
+
* reimplementing the exact / type-wildcard / catch-all precedence, and a
|
|
482
|
+
* reimplementation that drifted would disagree about which schema ran. The
|
|
483
|
+
* parsed body is not contributed for the opposite reason — reading it here
|
|
484
|
+
* does not consume it, so `req.json()` in the handler is one call away and
|
|
485
|
+
* returns the same value this middleware validated.
|
|
419
486
|
*/
|
|
420
|
-
readonly body: unknown;
|
|
421
|
-
/** The `content` key that matched the request's content type. */
|
|
422
487
|
readonly mediaType: string | undefined;
|
|
423
|
-
/** `false` when `validate: false` — the request was matched, not checked. */
|
|
424
|
-
readonly validated: boolean;
|
|
425
488
|
}
|
|
426
|
-
/**
|
|
489
|
+
/**
|
|
490
|
+
* `ctx.openapi` when the document does not describe the request.
|
|
491
|
+
*
|
|
492
|
+
* Only reachable with `onUnknownRoute`/`onUnknownMethod` set to `'pass'`, or a
|
|
493
|
+
* `skip` — which is how you put this middleware in front of an existing API
|
|
494
|
+
* and watch what it *would* have refused before letting it refuse anything.
|
|
495
|
+
*/
|
|
427
496
|
interface OpenApiUnmatched {
|
|
428
497
|
readonly matched: false;
|
|
429
|
-
/**
|
|
498
|
+
/**
|
|
499
|
+
* Which kind of miss it was, so an observer can tell them apart: `no_route`
|
|
500
|
+
* is a path the document does not describe (an undocumented endpoint, or
|
|
501
|
+
* someone probing), `no_operation` is a path it does describe reached with a
|
|
502
|
+
* verb it does not, and `skipped` is the consumer's own `skip` firing. Three
|
|
503
|
+
* different follow-up actions, and nothing else records which happened.
|
|
504
|
+
*/
|
|
430
505
|
readonly reason: 'skipped' | 'no_route' | 'no_operation';
|
|
431
|
-
readonly document: OpenAPIObject;
|
|
432
506
|
/** Set when the path matched but the method was not declared under it. */
|
|
433
507
|
readonly route: string | undefined;
|
|
434
|
-
/** The request's method, as sent. */
|
|
435
|
-
readonly method: string;
|
|
436
508
|
readonly operation: undefined;
|
|
437
509
|
readonly operationId: undefined;
|
|
438
510
|
readonly security: undefined;
|
|
511
|
+
/**
|
|
512
|
+
* Always empty here. Kept uniform across both branches so the field every
|
|
513
|
+
* consumer reaches for does not need a `matched` guard.
|
|
514
|
+
*/
|
|
439
515
|
readonly params: OpenApiParams;
|
|
440
|
-
readonly body: undefined;
|
|
441
516
|
readonly mediaType: undefined;
|
|
442
|
-
readonly validated: false;
|
|
443
517
|
}
|
|
444
518
|
/**
|
|
445
519
|
* What lands at `ctx.openapi`.
|
|
@@ -451,7 +525,172 @@ interface OpenApiUnmatched {
|
|
|
451
525
|
*/
|
|
452
526
|
type OpenApiContribution = OpenApiMatched | OpenApiUnmatched;
|
|
453
527
|
//#endregion
|
|
528
|
+
//#region src/document-types.d.ts
|
|
529
|
+
/**
|
|
530
|
+
* Collapse an intersection of mapped types into one object type.
|
|
531
|
+
*
|
|
532
|
+
* Splitting `properties` by `required` produces `{ a: A } & { b?: B }`, which
|
|
533
|
+
* is assignable both ways but is not the *same* type — so an exact-equality
|
|
534
|
+
* test fails, and hovering it shows the intersection rather than the shape.
|
|
535
|
+
* Both matter here: the point of these projections is what the editor tells
|
|
536
|
+
* you.
|
|
537
|
+
*/
|
|
538
|
+
type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
539
|
+
/** How deep `$ref` chains and nested schemas are followed before giving up. */
|
|
540
|
+
type MaxDepth = 8;
|
|
541
|
+
type Deeper<D extends unknown[]> = [...D, unknown];
|
|
542
|
+
type LocalRef<Document, Ref extends string, Bucket extends string> = Ref extends `#/components/${Bucket}/${infer Name}` ? Document extends {
|
|
543
|
+
components: { [K in Bucket]: Record<Name, infer Target> };
|
|
544
|
+
} ? Target : unknown : unknown;
|
|
545
|
+
/**
|
|
546
|
+
* A Schema Object as a TypeScript type.
|
|
547
|
+
*
|
|
548
|
+
* Covers `$ref` into `#/components/schemas`, `const`, `enum`, the primitive
|
|
549
|
+
* types, arrays, and objects with `required` driving optionality. Composition
|
|
550
|
+
* keywords (`allOf`, `oneOf`, `anyOf`) are not interpreted — they resolve to
|
|
551
|
+
* `unknown`, because guessing at them is how a type starts disagreeing with
|
|
552
|
+
* the validator.
|
|
553
|
+
*/
|
|
554
|
+
type FromSchema<Schema, Document, Depth extends unknown[] = []> = Depth['length'] extends MaxDepth ? unknown : Schema extends {
|
|
555
|
+
$ref: infer Ref extends string;
|
|
556
|
+
} ? FromSchema<LocalRef<Document, Ref, 'schemas'>, Document, Deeper<Depth>> : Schema extends {
|
|
557
|
+
const: infer Value;
|
|
558
|
+
} ? Value : Schema extends {
|
|
559
|
+
enum: ReadonlyArray<infer Member>;
|
|
560
|
+
} ? Member : Schema extends {
|
|
561
|
+
type: 'string';
|
|
562
|
+
} ? string : Schema extends {
|
|
563
|
+
type: 'integer' | 'number';
|
|
564
|
+
} ? number : Schema extends {
|
|
565
|
+
type: 'boolean';
|
|
566
|
+
} ? boolean : Schema extends {
|
|
567
|
+
type: 'null';
|
|
568
|
+
} ? null : Schema extends {
|
|
569
|
+
type: 'array';
|
|
570
|
+
} ? FromArraySchema<Schema, Document, Depth> : Schema extends {
|
|
571
|
+
type: 'object';
|
|
572
|
+
} ? FromObjectSchema<Schema, Document, Depth> : unknown;
|
|
573
|
+
type FromArraySchema<Schema, Document, Depth extends unknown[]> = Schema extends {
|
|
574
|
+
items: infer Items;
|
|
575
|
+
} ? Array<FromSchema<Items, Document, Deeper<Depth>>> : unknown[];
|
|
576
|
+
type FromObjectSchema<Schema, Document, Depth extends unknown[]> = Schema extends {
|
|
577
|
+
properties: infer Props;
|
|
578
|
+
} ? Schema extends {
|
|
579
|
+
required: ReadonlyArray<infer Required extends string>;
|
|
580
|
+
} ? Optionalize<Props, Required, Document, Depth> : Optionalize<Props, never, Document, Depth> : Record<string, unknown>;
|
|
581
|
+
/** Split `properties` by whether `required` names them. */
|
|
582
|
+
type Optionalize<Props, Required extends string, Document, Depth extends unknown[]> = Simplify<{ [K in keyof Props as K extends Required ? K : never]: FromSchema<Props[K], Document, Deeper<Depth>> } & { [K in keyof Props as K extends Required ? never : K]?: FromSchema<Props[K], Document, Deeper<Depth>> }>;
|
|
583
|
+
/** The path templates the document declares. */
|
|
584
|
+
type RoutesOf<Document> = Document extends {
|
|
585
|
+
paths: infer Paths;
|
|
586
|
+
} ? Extract<keyof Paths, string> : never;
|
|
587
|
+
type PathItemOf<Document, Route extends string> = Document extends {
|
|
588
|
+
paths: Record<Route, infer Item>;
|
|
589
|
+
} ? Item : never;
|
|
590
|
+
/** HTTP methods the document declares under `Route`. */
|
|
591
|
+
type MethodsOf<Document, Route extends string> = Extract<keyof PathItemOf<Document, Route>, 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace'>;
|
|
592
|
+
/** The Operation Object at `Route` + `Method`. */
|
|
593
|
+
type OperationOf<Document, Route extends string, Method extends string> = PathItemOf<Document, Route> extends Record<Method, infer Operation> ? Operation : never;
|
|
594
|
+
/** Every `operationId` the document declares, as a union. */
|
|
595
|
+
type OperationIdsOf<Document> = Document extends {
|
|
596
|
+
paths: infer Paths;
|
|
597
|
+
} ? { [R in keyof Paths]: { [M in keyof Paths[R]]: Paths[R][M] extends {
|
|
598
|
+
operationId: infer Id;
|
|
599
|
+
} ? Id : never }[keyof Paths[R]] }[keyof Paths] : never;
|
|
600
|
+
/**
|
|
601
|
+
* Parameter Objects in force for an operation: the Path Item's, then the
|
|
602
|
+
* operation's own, with `$ref`s into `#/components/parameters` followed.
|
|
603
|
+
*/
|
|
604
|
+
type ParametersOf<Document, Route extends string, Method extends string> = ResolveParameter<Document, PathItemOf<Document, Route> extends {
|
|
605
|
+
parameters: ReadonlyArray<infer Parameter>;
|
|
606
|
+
} ? Parameter : never> | ResolveParameter<Document, OperationOf<Document, Route, Method> extends {
|
|
607
|
+
parameters: ReadonlyArray<infer Parameter>;
|
|
608
|
+
} ? Parameter : never>;
|
|
609
|
+
type ResolveParameter<Document, Parameter> = Parameter extends {
|
|
610
|
+
$ref: infer Ref extends string;
|
|
611
|
+
} ? LocalRef<Document, Ref, 'parameters'> : Parameter;
|
|
612
|
+
type SchemaOfParameter<Document, Parameter> = Parameter extends {
|
|
613
|
+
schema: infer Schema;
|
|
614
|
+
} ? FromSchema<Schema, Document> : unknown;
|
|
615
|
+
/** Parameters declared `in: Location`, keyed by name, optional unless required. */
|
|
616
|
+
type ParametersIn<Document, Route extends string, Method extends string, Location extends string> = Extract<ParametersOf<Document, Route, Method>, {
|
|
617
|
+
name: string;
|
|
618
|
+
in: Location;
|
|
619
|
+
}> extends infer Declared ? Simplify<{ [P in Extract<Declared, {
|
|
620
|
+
name: string;
|
|
621
|
+
}> as P extends {
|
|
622
|
+
required: true;
|
|
623
|
+
} ? P['name'] : never]: SchemaOfParameter<Document, P> } & { [P in Extract<Declared, {
|
|
624
|
+
name: string;
|
|
625
|
+
}> as P extends {
|
|
626
|
+
required: true;
|
|
627
|
+
} ? never : P['name']]?: SchemaOfParameter<Document, P> }> : never;
|
|
628
|
+
/**
|
|
629
|
+
* `ctx.openapi.params` for one operation, with each value typed by its schema
|
|
630
|
+
* instead of `unknown`.
|
|
631
|
+
*/
|
|
632
|
+
type ParamsFor<Document, Route extends string, Method extends string> = {
|
|
633
|
+
readonly path: ParametersIn<Document, Route, Method, 'path'>;
|
|
634
|
+
readonly query: ParametersIn<Document, Route, Method, 'query'>;
|
|
635
|
+
readonly header: ParametersIn<Document, Route, Method, 'header'>;
|
|
636
|
+
readonly cookie: ParametersIn<Document, Route, Method, 'cookie'>;
|
|
637
|
+
};
|
|
638
|
+
/**
|
|
639
|
+
* The `matched` branch for one operation, with `route`, `operation` and
|
|
640
|
+
* `params` specialized to it.
|
|
641
|
+
*
|
|
642
|
+
* `security` and `mediaType` keep their general types: they are not what the
|
|
643
|
+
* narrowing is for, and pinning them would add conditional depth for no gain.
|
|
644
|
+
*/
|
|
645
|
+
type MatchedOperation<Document, Route extends string, Method extends string> = {
|
|
646
|
+
readonly matched: true;
|
|
647
|
+
readonly route: Route;
|
|
648
|
+
readonly operation: OperationOf<Document, Route, Method>;
|
|
649
|
+
readonly operationId: OperationOf<Document, Route, Method> extends {
|
|
650
|
+
operationId: infer Id;
|
|
651
|
+
} ? Id : undefined;
|
|
652
|
+
readonly security: OpenApiMatched['security'];
|
|
653
|
+
readonly params: ParamsFor<Document, Route, Method>;
|
|
654
|
+
readonly mediaType: OpenApiMatched['mediaType'];
|
|
655
|
+
};
|
|
656
|
+
/**
|
|
657
|
+
* Every operation the document declares, as a union — one branch each, so
|
|
658
|
+
* narrowing on `operationId` (or `route`) inside a handler reaches that
|
|
659
|
+
* operation's own parameter types with no cast.
|
|
660
|
+
*/
|
|
661
|
+
type MatchedFor<Document> = { [Route in RoutesOf<Document>]: { [Method in MethodsOf<Document, Route>]: MatchedOperation<Document, Route, Method> }[MethodsOf<Document, Route>] }[RoutesOf<Document>];
|
|
662
|
+
/**
|
|
663
|
+
* What lands at `ctx.openapi` for a given document.
|
|
664
|
+
*
|
|
665
|
+
* Falls back to the unspecialized {@link OpenApiContribution} when the
|
|
666
|
+
* document arrived without its literal type — annotated `: OpenAPIObject`,
|
|
667
|
+
* say. `RoutesOf` is `never` there, which would otherwise leave a union with
|
|
668
|
+
* no `matched: true` branch at all and break every existing consumer.
|
|
669
|
+
* Degrading to today's shape is what keeps this change additive.
|
|
670
|
+
*/
|
|
671
|
+
type ContributionFor<Document> = [RoutesOf<Document>] extends [never] ? OpenApiContribution : MatchedFor<Document> | OpenApiUnmatched;
|
|
672
|
+
//#endregion
|
|
454
673
|
//#region src/with-openapi.d.ts
|
|
674
|
+
/**
|
|
675
|
+
* The document-aware call signature, intersected *ahead* of the one
|
|
676
|
+
* `defineMiddleware` produces so it is tried first.
|
|
677
|
+
*
|
|
678
|
+
* Only the config-only form is specialized — the form that goes in a
|
|
679
|
+
* `pipeline` array, which is how this is mounted. `pipeline` then carries the
|
|
680
|
+
* document-specific contribution through to the handler on its own. The two
|
|
681
|
+
* handler-taking forms fall through to the general signatures below it, where
|
|
682
|
+
* `ctx.openapi` keeps its unspecialized shape.
|
|
683
|
+
*
|
|
684
|
+
* The runtime is untouched: this re-describes what `defineMiddleware` already
|
|
685
|
+
* returns. That makes the description an assertion we own — if `ParamsFor`
|
|
686
|
+
* ever disagrees with what the middleware actually deserializes, the types
|
|
687
|
+
* are what lie, and nothing here would catch it.
|
|
688
|
+
*/
|
|
689
|
+
interface TypedByDocument {
|
|
690
|
+
<const Document extends OpenAPIObject>(config: Omit<WithOpenApiConfig, 'document'> & {
|
|
691
|
+
document: Document;
|
|
692
|
+
}): SingleKeyEntry<'openapi', Record<never, never>, ContributionFor<Document>>;
|
|
693
|
+
}
|
|
455
694
|
/**
|
|
456
695
|
* Middleware that holds an API to its own description.
|
|
457
696
|
*
|
|
@@ -484,6 +723,30 @@ type OpenApiContribution = OpenApiMatched | OpenApiUnmatched;
|
|
|
484
723
|
*
|
|
485
724
|
* @category Middleware
|
|
486
725
|
*/
|
|
487
|
-
declare const withOpenApi: Middleware<'openapi', WithOpenApiConfig, Record<never, never>, OpenApiContribution>;
|
|
726
|
+
declare const withOpenApi: TypedByDocument & Middleware<'openapi', WithOpenApiConfig, Record<never, never>, OpenApiContribution>;
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/define-document.d.ts
|
|
729
|
+
/**
|
|
730
|
+
* Check a document against `OpenAPIObject` without widening it.
|
|
731
|
+
*
|
|
732
|
+
* Returns its argument; the whole function is the type parameter.
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```ts
|
|
736
|
+
* // document.ts
|
|
737
|
+
* export const document = defineDocument({
|
|
738
|
+
* openapi: '3.1.0',
|
|
739
|
+
* info: { title: 'Acme', version: '1' },
|
|
740
|
+
* servers: [{ url: '/api' }],
|
|
741
|
+
* paths: { ... },
|
|
742
|
+
* })
|
|
743
|
+
*
|
|
744
|
+
* // server.ts — `ctx.openapi` is typed against these operations
|
|
745
|
+
* pipeline([withOpenApi({ document })], handler)
|
|
746
|
+
*
|
|
747
|
+
* document.servers[0].url // '/api', not string
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
declare function defineDocument<const Document extends OpenAPIObject>(document: Document): Document;
|
|
488
751
|
//#endregion
|
|
489
|
-
export { type CorsOrigin, type CorsPolicy, type FetchHandler, type HttpMethod, type OpenApiContribution, type OpenApiCorsOptions, type OpenApiMatched, type OpenApiParams, type OpenApiRejection, type OpenApiRejectionKind, type OpenApiUnmatched, type OpenApiValidateOptions, type OpenApiViolation, type ParameterIn, SCALAR_CDN_URL, type ScalarHtmlInput, type ScalarReferenceOptions, type SchemaDraft, type WithOpenApiConfig, withOpenApi };
|
|
752
|
+
export { type ContributionFor, type CorsOrigin, type CorsPolicy, type FetchHandler, type FromSchema, type HttpMethod, type MatchedFor, type MethodsOf, type OpenApiContribution, type OpenApiCorsOptions, type OpenApiMatched, type OpenApiParams, type OpenApiRejection, type OpenApiRejectionKind, type OpenApiUnmatched, type OpenApiValidateOptions, type OpenApiViolation, type OperationIdsOf, type OperationOf, type ParameterIn, type ParamsFor, type RoutesOf, SCALAR_CDN_URL, type ScalarHtmlInput, type ScalarReferenceOptions, type SchemaDraft, type WithOpenApiConfig, defineDocument, withOpenApi };
|
package/dist/index.js
CHANGED
|
@@ -1162,20 +1162,16 @@ function defaultRejectionResponse(rejection) {
|
|
|
1162
1162
|
headers
|
|
1163
1163
|
});
|
|
1164
1164
|
}
|
|
1165
|
-
function unmatched(
|
|
1165
|
+
function unmatched(reason, route) {
|
|
1166
1166
|
return { openapi: {
|
|
1167
1167
|
matched: false,
|
|
1168
1168
|
reason,
|
|
1169
|
-
document,
|
|
1170
1169
|
route,
|
|
1171
|
-
method,
|
|
1172
1170
|
operation: void 0,
|
|
1173
1171
|
operationId: void 0,
|
|
1174
1172
|
security: void 0,
|
|
1175
1173
|
params: EMPTY_PARAMS,
|
|
1176
|
-
|
|
1177
|
-
mediaType: void 0,
|
|
1178
|
-
validated: false
|
|
1174
|
+
mediaType: void 0
|
|
1179
1175
|
} };
|
|
1180
1176
|
}
|
|
1181
1177
|
/**
|
|
@@ -1266,7 +1262,7 @@ const withOpenApi = defineMiddleware({
|
|
|
1266
1262
|
route: void 0
|
|
1267
1263
|
});
|
|
1268
1264
|
if (config.skip?.(req) === true) return {
|
|
1269
|
-
result: unmatched(
|
|
1265
|
+
result: unmatched("skipped", void 0),
|
|
1270
1266
|
route: void 0
|
|
1271
1267
|
};
|
|
1272
1268
|
const url = new URL(req.url);
|
|
@@ -1292,7 +1288,7 @@ const withOpenApi = defineMiddleware({
|
|
|
1292
1288
|
const match = pathname === void 0 ? void 0 : router.match(pathname);
|
|
1293
1289
|
if (match === void 0) {
|
|
1294
1290
|
if (onUnknownRoute === "pass") return {
|
|
1295
|
-
result: unmatched(
|
|
1291
|
+
result: unmatched("no_route", void 0),
|
|
1296
1292
|
route: void 0
|
|
1297
1293
|
};
|
|
1298
1294
|
return respond({
|
|
@@ -1311,7 +1307,7 @@ const withOpenApi = defineMiddleware({
|
|
|
1311
1307
|
const operation = isHttpMethod(method) ? match.route.operations.get(method) : void 0;
|
|
1312
1308
|
if (operation === void 0) {
|
|
1313
1309
|
if (onUnknownMethod === "pass") return {
|
|
1314
|
-
result: unmatched(
|
|
1310
|
+
result: unmatched("no_operation", match.route.template),
|
|
1315
1311
|
route: match.route
|
|
1316
1312
|
};
|
|
1317
1313
|
return respond({
|
|
@@ -1346,7 +1342,6 @@ const withOpenApi = defineMiddleware({
|
|
|
1346
1342
|
message: `query parameter "${name}" is not declared by this operation`
|
|
1347
1343
|
});
|
|
1348
1344
|
}
|
|
1349
|
-
let body;
|
|
1350
1345
|
let mediaType;
|
|
1351
1346
|
const requestBody = operation.requestBody;
|
|
1352
1347
|
if (requestBody !== void 0 && options?.body === true) {
|
|
@@ -1377,7 +1372,6 @@ const withOpenApi = defineMiddleware({
|
|
|
1377
1372
|
});
|
|
1378
1373
|
} else {
|
|
1379
1374
|
mediaType = read.content.mediaType;
|
|
1380
|
-
body = read.value;
|
|
1381
1375
|
if (read.validatable && read.content.schema !== void 0 && validateSchema) {
|
|
1382
1376
|
const schema = read.content.schema;
|
|
1383
1377
|
for (const unit of validateSchema(read.value, schema)) violations.push(toViolation("body", void 0, unit, options?.describe === true ? describeFailure(document, schema, unit) : void 0));
|
|
@@ -1395,16 +1389,12 @@ const withOpenApi = defineMiddleware({
|
|
|
1395
1389
|
return {
|
|
1396
1390
|
result: { openapi: {
|
|
1397
1391
|
matched: true,
|
|
1398
|
-
document,
|
|
1399
1392
|
route: operation.route,
|
|
1400
|
-
method: operation.method,
|
|
1401
1393
|
operation: operation.operation,
|
|
1402
1394
|
operationId: operation.operationId,
|
|
1403
1395
|
security: operation.security,
|
|
1404
1396
|
params,
|
|
1405
|
-
|
|
1406
|
-
mediaType,
|
|
1407
|
-
validated: options !== void 0
|
|
1397
|
+
mediaType
|
|
1408
1398
|
} },
|
|
1409
1399
|
route: match.route
|
|
1410
1400
|
};
|
|
@@ -1420,4 +1410,31 @@ const withOpenApi = defineMiddleware({
|
|
|
1420
1410
|
});
|
|
1421
1411
|
|
|
1422
1412
|
//#endregion
|
|
1423
|
-
|
|
1413
|
+
//#region src/define-document.ts
|
|
1414
|
+
/**
|
|
1415
|
+
* Check a document against `OpenAPIObject` without widening it.
|
|
1416
|
+
*
|
|
1417
|
+
* Returns its argument; the whole function is the type parameter.
|
|
1418
|
+
*
|
|
1419
|
+
* @example
|
|
1420
|
+
* ```ts
|
|
1421
|
+
* // document.ts
|
|
1422
|
+
* export const document = defineDocument({
|
|
1423
|
+
* openapi: '3.1.0',
|
|
1424
|
+
* info: { title: 'Acme', version: '1' },
|
|
1425
|
+
* servers: [{ url: '/api' }],
|
|
1426
|
+
* paths: { ... },
|
|
1427
|
+
* })
|
|
1428
|
+
*
|
|
1429
|
+
* // server.ts — `ctx.openapi` is typed against these operations
|
|
1430
|
+
* pipeline([withOpenApi({ document })], handler)
|
|
1431
|
+
*
|
|
1432
|
+
* document.servers[0].url // '/api', not string
|
|
1433
|
+
* ```
|
|
1434
|
+
*/
|
|
1435
|
+
function defineDocument(document) {
|
|
1436
|
+
return document;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
//#endregion
|
|
1440
|
+
export { SCALAR_CDN_URL, defineDocument, withOpenApi };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@croutonian/with-openapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "OpenAPI middleware for @supabase/middleware. Matches each request against an OpenAPI 3.1 document, optionally rejects the ones it does not describe, contributes the matched operation and validated params to ctx, and optionally serves a Scalar API reference.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openapi",
|