@tetsujs/openapi 0.6.0 → 0.6.2
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 +11 -445
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,465 +1,31 @@
|
|
|
1
1
|
# @tetsujs/openapi
|
|
2
2
|
|
|
3
|
-
An OpenAPI 3.1 document and
|
|
4
|
-
already declared.
|
|
3
|
+
An OpenAPI 3.1 document and docs page generated from [Tetsu](https://tetsujs.com) routes.
|
|
5
4
|
|
|
6
5
|
```bash
|
|
7
6
|
bun add @tetsujs/openapi
|
|
8
7
|
```
|
|
9
8
|
|
|
10
|
-
## Usage
|
|
11
|
-
|
|
12
|
-
Mount the `docs()` controller next to your own:
|
|
13
|
-
|
|
14
9
|
```ts
|
|
15
10
|
import { docs } from "@tetsujs/openapi";
|
|
16
11
|
|
|
17
12
|
createApp({
|
|
18
13
|
routes: [
|
|
19
|
-
group("/api", { children: [usersController(
|
|
14
|
+
group("/api", { children: [usersController()] }),
|
|
20
15
|
docs({ info: { title: "Users API", version: "1.0.0" } }),
|
|
21
16
|
],
|
|
22
17
|
});
|
|
23
18
|
```
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
is built once, at startup, so anything that cannot be described is
|
|
27
|
-
reported before the first request.
|
|
28
|
-
|
|
29
|
-
`docs()` is an ordinary controller: put it in a group to move it under a
|
|
30
|
-
prefix, guard it with hooks, or leave it out in production. Its own two
|
|
31
|
-
routes are left out of the document.
|
|
32
|
-
|
|
33
|
-
### The page runs someone else's code on your origin
|
|
34
|
-
|
|
35
|
-
The page loads its renderer — Scalar, Swagger UI or Redoc — from
|
|
36
|
-
jsDelivr and runs it on the application's origin, with that origin's
|
|
37
|
-
cookies. If a session cookie lives there, a renderer that is not what it
|
|
38
|
-
should be acts as the signed-in user. The defaults are pinned to an exact
|
|
39
|
-
version with a Subresource Integrity hash, so a file the CDN changes is
|
|
40
|
-
refused by the browser; that does not make the renderer yours, and what it
|
|
41
|
-
loads in turn is not covered.
|
|
42
|
-
|
|
43
|
-
Where the origin carries a session, serve the document alone:
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
docs({ info, ui: false });
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
`/openapi.json` is served and there is no page. For a page anyway, host
|
|
50
|
-
the renderer yourself or pin your own copy, with its hash:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
docs({
|
|
54
|
-
info,
|
|
55
|
-
assets: {
|
|
56
|
-
script: "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.72.1/dist/browser/standalone.js",
|
|
57
|
-
integrity: { script: "sha384-…" },
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
`curl -s <url> | openssl dgst -sha384 -binary | openssl base64 -A` prints
|
|
63
|
-
the hash, to be prefixed with `sha384-`.
|
|
64
|
-
|
|
65
|
-
## Options
|
|
66
|
-
|
|
67
|
-
| Option | Default | |
|
|
68
|
-
| --- | --- | --- |
|
|
69
|
-
| `info` | — | the document's `info`: title, version, description |
|
|
70
|
-
| `servers` | — | where the API is reachable |
|
|
71
|
-
| `path` | `/openapi.json` | where the document is served |
|
|
72
|
-
| `uiPath` | `/docs` | where the page is served |
|
|
73
|
-
| `ui` | `"scalar"` | `"scalar"`, `"swagger-ui"`, `"redoc"`, or `false` for no page |
|
|
74
|
-
| `title` | the document's title | the page's title |
|
|
75
|
-
| `assets` | the renderer on jsDelivr, pinned | your own renderer URLs, with `integrity` hashes |
|
|
76
|
-
| `documentSelf` | `false` | include `/openapi.json` and `/docs` in the document |
|
|
77
|
-
| `onWarning` | `console.warn` | receives what could not be described |
|
|
78
|
-
| `errors` | the framework's envelope | an error format of your own — see [below](#an-error-format-of-your-own) |
|
|
79
|
-
| `tags` | — | what each tag is, in the order the sidebar lists them — see [below](#tags) |
|
|
80
|
-
|
|
81
|
-
## How a route becomes an operation
|
|
82
|
-
|
|
83
|
-
Every part of an operation comes from somewhere the application already
|
|
84
|
-
wrote, by a rule:
|
|
85
|
-
|
|
86
|
-
| In the document | Comes from | Rule |
|
|
87
|
-
| --- | --- | --- |
|
|
88
|
-
| the path | the route's `path`, under its groups' prefixes | `/users/:id` → `/users/{id}` |
|
|
89
|
-
| `operationId` | `docs.operationId`, or the controller's name and the field | `setAvatar` in `controller("Users", …)` → `usersSetAvatar` — see [Operation ids](#operation-ids) |
|
|
90
|
-
| `summary`, `description`, `tags`, `deprecated` | the route's `docs` | as written |
|
|
91
|
-
| parameters | `schema.params`, `query`, `headers`, `cookies` | required as the schema says |
|
|
92
|
-
| the request body | `schema.body` and `bodyType` | media types from `bodyType`; required unless `text` or `stream` |
|
|
93
|
-
| `security` | `secured()` hooks, of every level the route runs under | every hook required together; `anyOf` inside one hook is the alternatives |
|
|
94
|
-
| the statuses | the route's `schema.response`, the hooks' `documented()` and `secured()`, the framework | one schema is `200`, a map its statuses, `null` or an entry without `body` a status without a body; with no `schema.response` at all, `200`; the framework's `422`, `400`, `413`, `500` where they can happen — see [Responses the framework adds](#responses-the-framework-adds) |
|
|
95
|
-
| a status's body | everything answering with that status | one flat `anyOf`, the route's own first, the same body once |
|
|
96
|
-
| an error envelope | any of them | one definition per status and code in `components`, named after the code; the route's own kept |
|
|
97
|
-
| `discriminator` | the envelopes of a status | on `error`, or the format's field, when every alternative is an envelope |
|
|
98
|
-
| a status's description | the schemas' `description`, the hooks', the framework's | one is the description, several a list led by code, none the reason phrase |
|
|
99
|
-
| a status's headers | the route's entry — `{ body, headers, cookies }` — and the hooks' `documented()` headers | the route's required as its schema says, a property's `description` the header's; its cookies one `set-cookie` header listing them; a hook's possible, not required; the route's wins a name both give |
|
|
100
|
-
| the document's `tags` | the `tags` option | in its order, then the tags routes use and it leaves out |
|
|
101
|
-
|
|
102
|
-
`docs: { hidden: true }` leaves a route out of the document; it is served
|
|
103
|
-
as before. A route with no `schema.response` is documented as `200` — its
|
|
104
|
-
handler may still answer `204` by returning nothing, which only a declared
|
|
105
|
-
`204: null` says.
|
|
106
|
-
|
|
107
|
-
## Tags
|
|
108
|
-
|
|
109
|
-
A route names its tags in `docs: { tags }`; the document says what each
|
|
110
|
-
one is:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
docs({
|
|
114
|
-
info,
|
|
115
|
-
tags: {
|
|
116
|
-
"sign-in": "Signing in with a code sent by email, and signing out",
|
|
117
|
-
me: "The signed-in user",
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
The order of the keys is the order of the sections a renderer lists. A tag
|
|
123
|
-
the routes use and `tags` leaves out is listed after them, in the order
|
|
124
|
-
the routes first use it, and reported — it is usually one tag spelled two
|
|
125
|
-
ways, `sign_in` next to `sign-in`. So is a tag described and used by no
|
|
126
|
-
operation. Without `tags` the document lists none, and nothing is
|
|
127
|
-
reported.
|
|
128
|
-
|
|
129
|
-
A `docs()` documents the application it is mounted in, all of it. Several
|
|
130
|
-
surfaces with a document each — a public API and an admin one — are
|
|
131
|
-
several applications, each with its own `docs()` and only the tags of its
|
|
132
|
-
own routes:
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
const api = Bun.serve({
|
|
136
|
-
...createApp({ routes: [orders, docs({ info, tags: { orders: "Orders" } })] }),
|
|
137
|
-
port: 3000,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const admin = Bun.serve({
|
|
141
|
-
...createApp({ routes: [users, docs({ info: adminInfo, tags: { users: "Users" } })] }),
|
|
142
|
-
port: 3001,
|
|
143
|
-
});
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Operation ids
|
|
147
|
-
|
|
148
|
-
A generated client names its methods after the `operationId`s, so they are
|
|
149
|
-
a contract, and every one comes from a name you wrote:
|
|
150
|
-
|
|
151
|
-
| Route | `operationId` |
|
|
152
|
-
| --- | --- |
|
|
153
|
-
| with `docs: { operationId }` | as written |
|
|
154
|
-
| in `controller("Users", …)` as `setAvatar` | `usersSetAvatar` |
|
|
155
|
-
| in a class `UsersController` as `setAvatar` | `usersSetAvatar` |
|
|
156
|
-
| in an object literal as `setAvatar` | `setAvatar` |
|
|
157
|
-
| mounted on its own, `POST /auth/code` | `postAuthCode` |
|
|
158
|
-
|
|
159
|
-
Two routes arriving at the same id stop the application at startup, naming
|
|
160
|
-
both — nothing is renamed behind your back, which would change a method in
|
|
161
|
-
someone's SDK the day a route is added. Give one of them
|
|
162
|
-
`docs.operationId`, or its controller a name of its own. On a public API,
|
|
163
|
-
state the id on every route: it is then visible, and a change to it is a
|
|
164
|
-
change a reviewer sees.
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
route({
|
|
168
|
-
method: "POST",
|
|
169
|
-
path: "/internal/drain",
|
|
170
|
-
docs: { summary: "Drain the queue", hidden: true },
|
|
171
|
-
handler: () => queue.drain(),
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Schemas are described through Standard Schema's JSON Schema support: Zod,
|
|
176
|
-
Valibot, ArkType and [`@tetsujs/typebox`](../typebox) provide it.
|
|
177
|
-
|
|
178
|
-
## Responses the framework adds
|
|
179
|
-
|
|
180
|
-
Failures the framework answers by itself are documented on every route
|
|
181
|
-
where they can happen, in the same error envelope the server sends:
|
|
182
|
-
|
|
183
|
-
| Status | `error` | When |
|
|
184
|
-
| --- | --- | --- |
|
|
185
|
-
| `422` (or the configured validation status) | `VALIDATION_FAILED` | a request part failed its schema |
|
|
186
|
-
| `400` | `MALFORMED_JSON` / `MALFORMED_FORM` | the body could not be parsed |
|
|
187
|
-
| `413` | `BODY_TOO_LARGE` | the body exceeded `maxBodySize` |
|
|
188
|
-
| `500` | `INTERNAL_SERVER_ERROR` | any unhandled failure |
|
|
189
|
-
|
|
190
|
-
The `error` code is a constant in each schema, so a generated client can
|
|
191
|
-
tell failures apart by it. A status the route declares itself is kept, and
|
|
192
|
-
the framework's failures for the same status are listed next to it.
|
|
193
|
-
|
|
194
|
-
Every envelope — the framework's, a hook's, or one the route declares
|
|
195
|
-
itself — is one definition in `components` per status and code, named
|
|
196
|
-
after the code (`ITEM_NOT_FOUND` is `ItemNotFound`), so a generated client
|
|
197
|
-
gets one type per failure. A schema is recognized as an envelope when its
|
|
198
|
-
`error` is required and a single string. When the route and a hook both
|
|
199
|
-
describe one code, the route's definition is kept; if the other one has
|
|
200
|
-
different fields, the generator warns. A union the route declares joins
|
|
201
|
-
the other failures of its status as one flat `anyOf`.
|
|
202
|
-
|
|
203
|
-
When every alternative of a status is an envelope, the union carries a
|
|
204
|
-
`discriminator` on `error`, with the mapping from each code to its
|
|
205
|
-
definition, and a generated client narrows on the code.
|
|
206
|
-
|
|
207
|
-
A status is described by what answers with it. The route's part is the
|
|
208
|
-
`description` of the schema it declares — `.describe()` in Zod and
|
|
209
|
-
ArkType, `v.description()` in Valibot, `{ description }` in TypeBox — for
|
|
210
|
-
any status, a `200` as much as a `404`; a union described as a whole is
|
|
211
|
-
one description, one described branch by branch is one per branch. A
|
|
212
|
-
hook's part is its `documented()` description, the framework's its own.
|
|
213
|
-
One description is the status's description; several are a list, each
|
|
214
|
-
led by its code:
|
|
215
|
-
|
|
216
|
-
```md
|
|
217
|
-
- `ACCOUNT_DISABLED`: this account is disabled
|
|
218
|
-
- `CAPTCHA_FAILED`: the captcha token is missing or did not pass
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
A status nothing describes keeps its reason phrase — `Not Found`,
|
|
222
|
-
`Successful response`.
|
|
223
|
-
|
|
224
|
-
## An error format of your own
|
|
225
|
-
|
|
226
|
-
Every failure reaches the application's `onError` hooks — a thrown
|
|
227
|
-
`HttpError`, a validation or body failure, a `404`, a `405`, a rate limit's
|
|
228
|
-
refusal, and an error nothing expected — so one hook answers all of them in
|
|
229
|
-
your format:
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
import type { ErrorBody } from "@tetsujs/core";
|
|
233
|
-
import { hook, HttpError, reportFailure } from "@tetsujs/core";
|
|
234
|
-
|
|
235
|
-
const inOurFormat = hook.onError((ctx) => {
|
|
236
|
-
const { error } = ctx;
|
|
237
|
-
|
|
238
|
-
if (error instanceof HttpError) {
|
|
239
|
-
const { status, error: code, ...rest } = error.body as ErrorBody;
|
|
240
|
-
|
|
241
|
-
return Response.json({ code, ...rest }, { status });
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
reportFailure(ctx, "unhandled", error);
|
|
245
|
-
|
|
246
|
-
return Response.json(
|
|
247
|
-
{ code: "INTERNAL_SERVER_ERROR", message: "Internal Server Error" },
|
|
248
|
-
{ status: 500 },
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
An error the hook answers is one nothing else reports: `reportError` hears
|
|
254
|
-
of a failure only when no `onError` hook answered it. So the hook that
|
|
255
|
-
answers the unexpected ones — a database gone, a `TypeError` — reports them
|
|
256
|
-
itself; left out, they answer in your format and never reach the error
|
|
257
|
-
tracker.
|
|
258
|
-
|
|
259
|
-
The document is generated from the routes, not from that hook, so it is
|
|
260
|
-
told the same format with `errors`:
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
const errors: ErrorFormat = {
|
|
264
|
-
schema: ({ error, message, fields }) => ({
|
|
265
|
-
type: "object",
|
|
266
|
-
required: ["code", "message", ...Object.keys(fields)],
|
|
267
|
-
properties: {
|
|
268
|
-
code: error ? { type: "string", const: error } : { type: "string" },
|
|
269
|
-
message: { type: "string", ...(message ? { examples: [message] } : {}) },
|
|
270
|
-
...fields,
|
|
271
|
-
},
|
|
272
|
-
}),
|
|
273
|
-
discriminator: "code",
|
|
274
|
-
};
|
|
20
|
+
## Documentation
|
|
275
21
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`schema` describes one failure from what the generator knows of it: its
|
|
283
|
-
status, its code when known, an example message, and in `fields` what it
|
|
284
|
-
carries besides — `issues` for a validation failure, `retryAfter` for a
|
|
285
|
-
rate limit, a hook's own `fields`. It is used for the framework's failures
|
|
286
|
-
and the hooks' refusals alike.
|
|
287
|
-
|
|
288
|
-
`discriminator` names the top-level field that holds the code. Statuses of
|
|
289
|
-
envelopes are then discriminated on it, and a route's own envelope is
|
|
290
|
-
recognized by it — a `{ code: "ITEM_NOT_FOUND", … }` the route declares is
|
|
291
|
-
the same definition as a hook's. A format that nests its code has no
|
|
292
|
-
top-level field to name; it gives `code` instead, reading the code from a
|
|
293
|
-
route's schema:
|
|
294
|
-
|
|
295
|
-
```ts
|
|
296
|
-
code: (schema) => {
|
|
297
|
-
const error = schema.properties?.error;
|
|
298
|
-
const code = typeof error === "object" ? error.properties?.code : undefined;
|
|
299
|
-
|
|
300
|
-
return typeof code === "object" && typeof code.const === "string" ? code.const : undefined;
|
|
301
|
-
},
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
The hook and `errors` describe one format in two places — the core knows
|
|
305
|
-
nothing about documents, and a function cannot be read for the shape it
|
|
306
|
-
returns — so keep a test that holds them together: provoke the failures,
|
|
307
|
-
the unexpected one included, and check each against the document with
|
|
308
|
-
[`assertDescribed`](#testing-against-the-document):
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
const boom = route({ method: "GET", path: "/boom", handler: () => { throw new Error("boom"); } });
|
|
312
|
-
const app = createApp({ hooks: { onError: [inOurFormat] }, routes: [api, boom] });
|
|
313
|
-
const request = serve(app);
|
|
314
|
-
const { document } = openapi(app, { info, errors });
|
|
315
|
-
|
|
316
|
-
test("failures are what the document says", async () => {
|
|
317
|
-
await assertDescribed(document, "POST /items", await request("/items", { method: "POST", body: "{}" }));
|
|
318
|
-
await assertDescribed(document, "GET /boom", await request("/boom"));
|
|
319
|
-
});
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
## Documenting hooks
|
|
323
|
-
|
|
324
|
-
A hook that answers by itself — an auth check, a limiter — can say so, and
|
|
325
|
-
every route it guards is documented accordingly. `secured()` adds a
|
|
326
|
-
security scheme, `documented()` adds responses:
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
import { documented, secured } from "@tetsujs/openapi";
|
|
330
|
-
|
|
331
|
-
export const auth = secured(
|
|
332
|
-
hook.beforeParse((ctx) => {
|
|
333
|
-
const user = verify(ctx.req.headers.get("authorization"));
|
|
334
|
-
|
|
335
|
-
if (!user) throw new HttpError(401);
|
|
336
|
-
|
|
337
|
-
return { user };
|
|
338
|
-
}),
|
|
339
|
-
{ name: "bearerAuth", scheme: { type: "http", scheme: "bearer", bearerFormat: "JWT" } },
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
export const guard = documented(hook.beforeParse(check), {
|
|
343
|
-
responses: [
|
|
344
|
-
{
|
|
345
|
-
status: 429,
|
|
346
|
-
description: "Rate limit exceeded",
|
|
347
|
-
error: "RATE_LIMITED",
|
|
348
|
-
fields: { retryAfter: { type: "integer", minimum: 0 } },
|
|
349
|
-
headers: { "retry-after": { schema: { type: "integer", minimum: 0 } } },
|
|
350
|
-
},
|
|
351
|
-
],
|
|
352
|
-
});
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
A response with an `error` code is the framework's envelope, defined once
|
|
356
|
-
in `components` and named after the code. `fields` adds what the hook puts
|
|
357
|
-
next to `status`, `message` and `error`, each always present; `headers`,
|
|
358
|
-
what it sets on the response. Both are JSON Schema written by hand, typed
|
|
359
|
-
keyword by keyword (`JsonSchema`), so a misspelled keyword does not
|
|
360
|
-
compile. A hook whose body is not the envelope passes a `schema` instead.
|
|
361
|
-
|
|
362
|
-
Hooks from `@tetsujs/rate-limit` are already documented this way. A scheme
|
|
363
|
-
is identified by its `name`: the same one mounted twice is one
|
|
364
|
-
requirement, and two different schemes under one name are reported.
|
|
365
|
-
|
|
366
|
-
Every hook of a route runs, so every scheme its hooks carry is required
|
|
367
|
-
together: a route behind a CSRF check and a captcha is documented as
|
|
368
|
-
needing both, one entry of `security` — in OpenAPI, separate entries mean
|
|
369
|
-
any one of them will do. Two hooks of one scheme require the scopes of
|
|
370
|
-
both.
|
|
371
|
-
|
|
372
|
-
"Either" lives inside one hook: a hook that accepts a session cookie or a
|
|
373
|
-
bearer token says so with `anyOf`, and the document lists every
|
|
374
|
-
combination a client may bring:
|
|
375
|
-
|
|
376
|
-
```ts
|
|
377
|
-
export const caller = secured(hook.beforeParse(sessionOrToken), {
|
|
378
|
-
anyOf: [cookieSession, bearerToken],
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// with a CSRF check on the same route:
|
|
382
|
-
// security: [{ session: [], csrf: [] }, { bearer: [], csrf: [] }]
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
## Using the document directly
|
|
386
|
-
|
|
387
|
-
When the document itself is the output — written to a file in CI, fed to a
|
|
388
|
-
client generator — call the generator:
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
import { openapi } from "@tetsujs/openapi";
|
|
392
|
-
|
|
393
|
-
const { document, warnings } = openapi(app, {
|
|
394
|
-
info: { title: "Users API", version: "1.0.0" },
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
await Bun.write("openapi.json", JSON.stringify(document, null, 2));
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
`docsPage()` renders the page on its own, for serving it elsewhere.
|
|
401
|
-
|
|
402
|
-
## Testing against the document
|
|
403
|
-
|
|
404
|
-
`@tetsujs/openapi/testing` checks a response a test provoked against the
|
|
405
|
-
operation the document describes:
|
|
406
|
-
|
|
407
|
-
```ts
|
|
408
|
-
import { assertDescribed } from "@tetsujs/openapi/testing";
|
|
409
|
-
|
|
410
|
-
const { document } = openapi(app, { info });
|
|
411
|
-
|
|
412
|
-
test("a wrong code is what the document says", async () => {
|
|
413
|
-
const res = await request("/session", { method: "POST", body });
|
|
414
|
-
|
|
415
|
-
await assertDescribed(document, "POST /session", res);
|
|
416
|
-
});
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
It throws unless the status is declared and the body fits one of the
|
|
420
|
-
alternatives its status describes, listing every problem at once:
|
|
421
|
-
|
|
422
|
-
```
|
|
423
|
-
POST /session answered 403, which the document does not describe:
|
|
424
|
-
- error "CAPTCHA_FAILED", which its 403 does not list: ACCOUNT_DISABLED, CSRF_HEADER_REQUIRED
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
The operation is named as the test called it, a method and the path it
|
|
428
|
-
requested, matched against the document's templates. Without a validator
|
|
429
|
-
only the top level of the body is compared — required fields, and fields
|
|
430
|
-
that are a `const`, which is where an envelope keeps its code. `validate`
|
|
431
|
-
checks the whole body with the JSON Schema validator of your choice, given
|
|
432
|
-
a schema that stands on its own; `headers` names headers the status must
|
|
433
|
-
declare when the response carries them:
|
|
434
|
-
|
|
435
|
-
```ts
|
|
436
|
-
await assertDescribed(document, "POST /session", res, {
|
|
437
|
-
validate: (schema, body) => ajv.validate(schema, body) || ajv.errorsText(),
|
|
438
|
-
headers: ["retry-after"],
|
|
439
|
-
});
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
The core does not check a thrown error against the route's response map:
|
|
443
|
-
a guard's refusal would be reported on every route it runs on. A test asks
|
|
444
|
-
about the one response it provoked.
|
|
445
|
-
|
|
446
|
-
## Warnings
|
|
447
|
-
|
|
448
|
-
A route is still documented when part of it cannot be described, and the
|
|
449
|
-
gap is reported instead of failing the startup:
|
|
450
|
-
|
|
451
|
-
```
|
|
452
|
-
[openapi] POST /api/users: the body schema does not emit JSON Schema
|
|
453
|
-
```
|
|
22
|
+
- [tetsujs.com/docs/packages/openapi](https://tetsujs.com/docs/packages/openapi/): options, details
|
|
23
|
+
and recipes, also [as Markdown](https://tetsujs.com/docs/packages/openapi.md)
|
|
24
|
+
- [llms-full.txt](https://tetsujs.com/llms-full.txt): the whole documentation in one
|
|
25
|
+
file, for tools and AI assistants
|
|
26
|
+
- every export and option is also documented in the package's type
|
|
27
|
+
definitions, which editors and tools read from `node_modules`
|
|
454
28
|
|
|
455
|
-
|
|
456
|
-
whose conversion throws, and two routes that map to the same OpenAPI path
|
|
457
|
-
(`/files/*` and `/files/:wildcard` are both `/files/{wildcard}`).
|
|
29
|
+
## License
|
|
458
30
|
|
|
459
|
-
|
|
460
|
-
the validator emits it, and the references inside it — `#` for its own
|
|
461
|
-
root, `#/$defs/…` for a definition next to it — resolve against the
|
|
462
|
-
document's root once embedded, where they lead nowhere. Zod's recursive
|
|
463
|
-
getters and `.meta({ id })`, ArkType's scopes and Valibot's `lazy` emit
|
|
464
|
-
them; TypeBox's `Type.Cyclic` names itself with `$id` and is described as
|
|
465
|
-
it is.
|
|
31
|
+
[MIT](https://github.com/tetsujs/tetsu/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tetsujs/openapi",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "An OpenAPI 3.1 document and docs page generated from Tetsu routes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"author": "tetsuodev",
|
|
13
13
|
"license": "MIT",
|
|
14
|
-
"homepage": "https://
|
|
14
|
+
"homepage": "https://tetsujs.com/docs/packages/openapi/",
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/tetsujs/tetsu/issues"
|
|
17
17
|
},
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@tetsujs/core": "^0.6.
|
|
41
|
+
"@tetsujs/core": "^0.6.2"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@tetsujs/core": "0.6.
|
|
44
|
+
"@tetsujs/core": "0.6.2"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|