@znight/openapi 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +572 -2
- package/dist/build-BTUC96mY.d.mts +174 -0
- package/dist/build-DXunOzlI.mjs +733 -0
- package/dist/build-DXunOzlI.mjs.map +1 -0
- package/dist/compiler.d.mts +16 -0
- package/dist/compiler.mjs +2639 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/emit-child.d.mts +1 -0
- package/dist/emit-child.mjs +124 -0
- package/dist/emit-child.mjs.map +1 -0
- package/dist/emit.d.mts +2 -0
- package/dist/emit.mjs +252 -0
- package/dist/emit.mjs.map +1 -0
- package/dist/index-CqQ6IRnN.d.mts +60 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.mjs +777 -0
- package/dist/index.mjs.map +1 -0
- package/dist/openapi-CQk5dHqc.mjs +39 -0
- package/dist/openapi-CQk5dHqc.mjs.map +1 -0
- package/dist/shared-BZxtyaB0.mjs +147 -0
- package/dist/shared-BZxtyaB0.mjs.map +1 -0
- package/dist/testing.d.mts +39 -0
- package/dist/testing.mjs +155 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-VzBexMKj.d.mts +213 -0
- package/dist/vite.d.mts +24 -0
- package/dist/vite.mjs +64 -0
- package/dist/vite.mjs.map +1 -0
- package/package.json +115 -4
- package/src/build.ts +898 -0
- package/src/compiler/analyze.ts +81 -0
- package/src/compiler/document.ts +612 -0
- package/src/compiler/generate.ts +79 -0
- package/src/compiler/index.ts +47 -0
- package/src/compiler/model.ts +141 -0
- package/src/compiler/operations.ts +1475 -0
- package/src/compiler/schemas.ts +706 -0
- package/src/compiler/validate.ts +102 -0
- package/src/compiler/values.ts +81 -0
- package/src/converters.ts +137 -0
- package/src/decorators.ts +187 -0
- package/src/emit/child.ts +144 -0
- package/src/emit/index.ts +377 -0
- package/src/emit/protocol.ts +35 -0
- package/src/index.ts +94 -0
- package/src/metadata.ts +112 -0
- package/src/openapi.ts +55 -0
- package/src/plugin.ts +810 -0
- package/src/shared.ts +154 -0
- package/src/testing.ts +224 -0
- package/src/types.ts +240 -0
- package/src/ui.ts +215 -0
- package/src/vite.ts +126 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ugho STEPHAN
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,573 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @znight/openapi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenAPI 3.1 for [Znight](../../README.md) HTTP applications, generated at build time from your
|
|
4
|
+
controllers, without describing the API twice.
|
|
5
|
+
|
|
6
|
+
Paths, parameters, coercion types, defaults, request and response bodies, uploads, statuses,
|
|
7
|
+
headers and access decisions come from the code that already runs: the route table of
|
|
8
|
+
`@znight/http`, the handler signatures, their **annotated return types**, their JSDoc and your
|
|
9
|
+
Standard Schemas (Zod, Valibot, ArkType...). A few `@Api*` decorators add what cannot be inferred.
|
|
10
|
+
|
|
11
|
+
- **Built at compile time.** `@znight/openapi/compiler` builds every operation and the JSON Schema
|
|
12
|
+
of every TypeScript type, and checks them: a response that cannot be documented, a `bigint` in a
|
|
13
|
+
JSON response (a guaranteed 500), duplicate operation ids or an undeclared OAuth2 scope fail the
|
|
14
|
+
build with a location and a hint (ZNIGHT030–036).
|
|
15
|
+
- **One function, one truth.** `buildOpenApiDocument(metadata)` converts the Standard Schemas with
|
|
16
|
+
their own library and assembles the document. The same pure function runs at boot, at build time
|
|
17
|
+
and in tests: the served document and the committed `openapi.json` are byte-identical.
|
|
18
|
+
- **Served everywhere.** `OpenApiPlugin` serves `/openapi.json` (precomputed bytes, strong ETag),
|
|
19
|
+
an optional YAML twin and an API reference UI (Scalar, or Swagger UI) with a strict CSP, the same
|
|
20
|
+
way on the node, Fastify, Express and Hono adapters, on Node.js and Bun.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
# bun
|
|
24
|
+
bun add @znight/core @znight/http @znight/openapi
|
|
25
|
+
bun add -d @znight/compiler vite
|
|
26
|
+
bun add @scalar/api-reference # optional: serve the UI from node_modules instead of the pinned CDN
|
|
27
|
+
bun add yaml # optional: YAML output
|
|
28
|
+
bun add @valibot/to-json-schema # optional: Valibot schemas
|
|
29
|
+
bun add -d @cfworker/json-schema # optional: validateResponse() in tests
|
|
30
|
+
|
|
31
|
+
# npm
|
|
32
|
+
npm install @znight/core @znight/http @znight/openapi
|
|
33
|
+
npm install -D @znight/compiler vite
|
|
34
|
+
npm install @scalar/api-reference # optional: serve the UI from node_modules instead of the pinned CDN
|
|
35
|
+
npm install yaml # optional: YAML output
|
|
36
|
+
npm install @valibot/to-json-schema # optional: Valibot schemas
|
|
37
|
+
npm install -D @cfworker/json-schema # optional: validateResponse() in tests
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Runs on Node.js >= 24 and Bun >= 1.3 (ESM only). `@znight/http` is a required peer dependency; the
|
|
41
|
+
others are optional (`swagger-ui-dist` serves Swagger UI from `node_modules`, `vite` is needed by
|
|
42
|
+
the `openapi()` Vite plugin).
|
|
43
|
+
|
|
44
|
+
**Documentation:** [`@znight/openapi`](https://github.com/znight-org/znight/blob/main/docs/packages/openapi.md).
|
|
45
|
+
**Example:** [`storefront-api`](https://github.com/znight-org/znight/tree/main/examples/storefront-api)
|
|
46
|
+
(Zod and Valibot schemas, a committed `openapi.json` checked in CI, a typed client generated from it).
|
|
47
|
+
|
|
48
|
+
- [Quick start](#quick-start)
|
|
49
|
+
- [The document](#the-document)
|
|
50
|
+
- [Documenting operations](#documenting-operations)
|
|
51
|
+
- [What is inferred](#what-is-inferred)
|
|
52
|
+
- [Security](#security)
|
|
53
|
+
- [Plugin options](#plugin-options)
|
|
54
|
+
- [Injection](#injection)
|
|
55
|
+
- [Emitting openapi.json at build time](#emitting-openapijson-at-build-time)
|
|
56
|
+
- [Testing](#testing)
|
|
57
|
+
- [Diagnostics](#diagnostics)
|
|
58
|
+
- [Compiler contract](#compiler-contract)
|
|
59
|
+
- [Limitations](#limitations)
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// src/main.ts
|
|
65
|
+
import { Application } from '@znight/core'
|
|
66
|
+
import { HttpPlugin } from '@znight/http'
|
|
67
|
+
import { OpenApiPlugin } from '@znight/openapi'
|
|
68
|
+
import { __ZNIGHT_METADATA__ } from './.znight/container.generated.js'
|
|
69
|
+
|
|
70
|
+
const app = await Application.create({
|
|
71
|
+
metadata: __ZNIGHT_METADATA__,
|
|
72
|
+
plugins: [HttpPlugin.configure({ port: 3000 }), OpenApiPlugin.configure()],
|
|
73
|
+
})
|
|
74
|
+
await app.start() // GET /openapi.json, GET /docs
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Nothing else is needed: the title, version, description and license come from `package.json`,
|
|
78
|
+
parameters from the signatures, request schemas from `@Body(S)`, responses from the annotated
|
|
79
|
+
return types and summaries from JSDoc. The compiler extension is discovered because a scanned file
|
|
80
|
+
(`main.ts`) imports `@znight/openapi`.
|
|
81
|
+
|
|
82
|
+
## The document
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// src/api.ts
|
|
86
|
+
import { OpenApiDocument, securityScheme } from '@znight/openapi'
|
|
87
|
+
import { Public } from './auth/auth.decorators.js'
|
|
88
|
+
|
|
89
|
+
/** The `x-api-key` header checked by AuthGuard. */
|
|
90
|
+
export const ApiKey = securityScheme('apiKey', { type: 'apiKey', in: 'header', name: 'x-api-key' })
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Bookshelf API
|
|
94
|
+
*
|
|
95
|
+
* A shelf of books with a cover each. Every route needs an API key, except the public ones.
|
|
96
|
+
*/
|
|
97
|
+
@OpenApiDocument({
|
|
98
|
+
info: { contact: { email: 'api@example.com' } }, // title and description: the JSDoc above
|
|
99
|
+
servers: [{ url: '/api' }],
|
|
100
|
+
security: [ApiKey], // default of every operation
|
|
101
|
+
decisions: [{ when: Public, security: [] }], // @Public() routes need nothing
|
|
102
|
+
tags: [{ name: 'Books', description: 'The shelf' }],
|
|
103
|
+
})
|
|
104
|
+
export class BookshelfApi {}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- One `@OpenApiDocument` per application, optional. The class is never instantiated.
|
|
108
|
+
- `info` precedence: the options, then the JSDoc of the class (first paragraph: `title`, the rest:
|
|
109
|
+
`description`), then `package.json` (`name`, `version`, `description`, an SPDX `license`), then
|
|
110
|
+
`API` / `0.0.0`.
|
|
111
|
+
- `securityScheme(name, definition)` declares an OpenAPI Security Scheme Object (`apiKey`, `http`,
|
|
112
|
+
`oauth2`, `openIdConnect`, `mutualTLS`). The compiler reads it statically; a scheme appears in
|
|
113
|
+
`components.securitySchemes` as soon as something references it.
|
|
114
|
+
|
|
115
|
+
| Option | Default | Description |
|
|
116
|
+
| -------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
|
|
117
|
+
| `info` | — | Title, version, description, contact, license, terms of service |
|
|
118
|
+
| `servers` | — | Servers of the document (else the HTTP prefix, see below) |
|
|
119
|
+
| `tags` | — | Declared tags first, in this order; the used tags are added, sorted |
|
|
120
|
+
| `externalDocs` | — | External documentation of the API |
|
|
121
|
+
| `security` | none | Default requirements of every operation (entries are alternatives) |
|
|
122
|
+
| `decisions` | `[]` | Security derived from route decorators (`createRouteDecorator`), see [Security](#security) |
|
|
123
|
+
| `operationIds` | `'controller.handler'` | `'controller.handler'` (`book.list`), `'handler'` (`list`) or `'Controller_handler'` |
|
|
124
|
+
| `errors` | `true` | Standard error responses; `false`, or `{ schema, contentType? }` for your own envelope |
|
|
125
|
+
| `strict` | `false` | ZNIGHT031 and ZNIGHT032 become errors |
|
|
126
|
+
| `jsdoc` | `true` | Read the JSDoc of controllers, handlers and types |
|
|
127
|
+
| `extensions` | — | `x-*` fields of the root object |
|
|
128
|
+
|
|
129
|
+
## Documenting operations
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Body, Controller, Get, HttpResponse, Param, Post, Query } from '@znight/http'
|
|
133
|
+
import { ApiResponse, ApiTags } from '@znight/openapi'
|
|
134
|
+
|
|
135
|
+
/** Books of the shelf. */
|
|
136
|
+
@Controller('/books')
|
|
137
|
+
@ApiTags('Books')
|
|
138
|
+
export class BookController {
|
|
139
|
+
/**
|
|
140
|
+
* List books
|
|
141
|
+
*
|
|
142
|
+
* Sorted and paginated.
|
|
143
|
+
* @param search Case-insensitive search in titles and authors.
|
|
144
|
+
*/
|
|
145
|
+
@Get()
|
|
146
|
+
async list(
|
|
147
|
+
@Query('q') search?: string,
|
|
148
|
+
@Query('sort') sort: BookSort = 'title',
|
|
149
|
+
@Query('limit') limit = 20,
|
|
150
|
+
): Promise<Page<BookResponse>> {
|
|
151
|
+
/* ... */
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Get a book */
|
|
155
|
+
@Get('/:id')
|
|
156
|
+
@ApiResponse(404, { description: 'No book with this id' })
|
|
157
|
+
async get(@Param('id') id: number): Promise<BookResponse> {
|
|
158
|
+
/* ... */
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add a book
|
|
163
|
+
* @returns The new book. Its URL is in the `location` header.
|
|
164
|
+
*/
|
|
165
|
+
@Post()
|
|
166
|
+
@ApiResponse(201, { headers: { location: { description: 'URL of the new book' } } })
|
|
167
|
+
@ApiResponse(409, { description: 'A book with this ISBN is already on the shelf' })
|
|
168
|
+
async create(@Body(CreateBook) input: CreateBook): Promise<HttpResponse<BookResponse>> {
|
|
169
|
+
/* ... */
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Report of the shelf */
|
|
173
|
+
@Get('/report')
|
|
174
|
+
@ApiResponse<ShelfReport>(200) // documents a Web Response, whose body type the compiler cannot see
|
|
175
|
+
report(): Response {
|
|
176
|
+
/* ... */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Decorator | Target | Effect |
|
|
182
|
+
| -------------------------------------------------------------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| `@OpenApiDocument(options)` | class | Options of the document |
|
|
184
|
+
| `@ApiOperation({ summary?, description?, operationId?, deprecated?, externalDocs?, extensions? })` | method | Overrides the JSDoc and the defaults |
|
|
185
|
+
| `@ApiTags(...names)` | class, method | Tags (class tags first). Without any: the controller name without its `Controller` suffix |
|
|
186
|
+
| `@ApiResponse<T>(status, { description?, schema?, contentType?, headers?, example? })` | method, repeatable | Declares or overrides a response (`404`, `'4XX'`, `'default'`). The body type comes from `T` or from a Standard Schema in `schema` |
|
|
187
|
+
| `@ApiBody<T>({ description?, required?, contentType?, schema? })` | method | The body of routes that read it themselves (`ctx.parseBody()`, `ctx.bodyStream()`) |
|
|
188
|
+
| `@ApiQuery<T>(name, { description?, required?, deprecated?, example? })` | method | A query parameter read outside the signature |
|
|
189
|
+
| `@ApiHeader<T>(name, { ... })` | class, method | A header read by a guard or an interceptor (`x-tenant`) |
|
|
190
|
+
| `@ApiSecurity(...requirements)` | class, method | Security of the operations; no argument means public |
|
|
191
|
+
| `@ApiExclude()` | class, method | Not documented |
|
|
192
|
+
|
|
193
|
+
Every decorator is a no-op marker stripped from the bundle. Type arguments (`@ApiResponse<Book[]>`)
|
|
194
|
+
are erased at runtime, but the compiler reads them.
|
|
195
|
+
|
|
196
|
+
**JSDoc is the first source of text** (decorators override it):
|
|
197
|
+
|
|
198
|
+
| JSDoc | OpenAPI |
|
|
199
|
+
| ------------------------------------ | -------------------------------------------------------------------------------- |
|
|
200
|
+
| First paragraph of a handler comment | `summary` |
|
|
201
|
+
| The rest | `description` |
|
|
202
|
+
| `@deprecated` on a handler | `deprecated: true` |
|
|
203
|
+
| `@param search Text` | `description` of the parameter (or body) the TypeScript parameter `search` feeds |
|
|
204
|
+
| `@returns Text` | `description` of the success response (default: the reason phrase) |
|
|
205
|
+
| Comment of a controller | `description` of its default tag (when it is not declared in `@OpenApiDocument`) |
|
|
206
|
+
| Comment of a type or property | `description`, plus the tags below |
|
|
207
|
+
|
|
208
|
+
Tags on types and properties: `@deprecated`, `@example`, `@default`, `@format` (`int32` / `int64`
|
|
209
|
+
make a `number` an `integer`), `@minimum`, `@maximum`, `@exclusiveMinimum`, `@exclusiveMaximum`,
|
|
210
|
+
`@multipleOf`, `@minLength`, `@maxLength`, `@pattern`, `@minItems`, `@maxItems`, `@uniqueItems`.
|
|
211
|
+
Values are JSON (`@example {"id": 1}`) or text.
|
|
212
|
+
|
|
213
|
+
## What is inferred
|
|
214
|
+
|
|
215
|
+
**Operations.** One per route and method; `:id` becomes `{id}`, a trailing `*` becomes
|
|
216
|
+
`{wildcard}` (`x-znight-wildcard: true`; `{wildcard_1}` when a parameter is named `wildcard`).
|
|
217
|
+
Routes of one path that name its parameters differently (`GET /books/:id`,
|
|
218
|
+
`DELETE /books/:bookId`) are documented under one template, with a ZNIGHT035 warning: OpenAPI
|
|
219
|
+
forbids two templates of one path. Automatic `HEAD` / `OPTIONS` are not documented, explicit
|
|
220
|
+
ones are (without content: a `HEAD` response has no body). `@All` is documented for GET, POST, PUT, PATCH and DELETE when no explicit route has the
|
|
221
|
+
same path (ids end with the method: `proxy.forward.get`). Paths never contain the HTTP prefix: it
|
|
222
|
+
belongs to `servers` (plugin `servers`, then `@OpenApiDocument({ servers })`, then
|
|
223
|
+
`[{ url: prefix }]`).
|
|
224
|
+
|
|
225
|
+
**Parameters.**
|
|
226
|
+
|
|
227
|
+
| Handler parameter | OpenAPI |
|
|
228
|
+
| -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
229
|
+
| `@Param('id') id: number` | `{ name: 'id', in: 'path', required: true, schema: { type: 'number' } }` |
|
|
230
|
+
| `@Query('limit') limit = 20` | `{ in: 'query', schema: { type: 'number', default: 20 } }` |
|
|
231
|
+
| `@Query('tag') tags: string[] = []` | `style: 'form'`, `explode: true`, `schema: { type: 'array', items: { type: 'string' } }` |
|
|
232
|
+
| `@Query('sort') sort: BookSort = 'title'` | `schema: { type: 'string', enum: ['author', 'title', 'year'], default: 'title' }` |
|
|
233
|
+
| `@Headers('x-tenant') tenant: string` | `{ in: 'header', required: true }` |
|
|
234
|
+
| `@Cookie('theme') theme?: 'light' \| 'dark'` | `{ in: 'cookie', schema: { type: 'string', enum: ['dark', 'light'] } }` (also `@SignedCookie`) |
|
|
235
|
+
| `@Param('id', IdSchema)`, `@Query('q', S)` | `schema: { $ref }` to the **input** JSON Schema of the schema |
|
|
236
|
+
| `@Query(SearchQuery)` (whole object) | One parameter per property of the input JSON Schema (ZNIGHT037 for a non-object schema)¹ |
|
|
237
|
+
| `@Query() q: SearchParams` (TypeScript type) | One parameter per property of the type |
|
|
238
|
+
|
|
239
|
+
¹ A property that another parameter of the operation already documents (`@Query('page')`,
|
|
240
|
+
`@ApiQuery('page')`, another whole-object schema) is dropped, with a ZNIGHT035 assembly warning:
|
|
241
|
+
OpenAPI forbids two parameters with one name and location.
|
|
242
|
+
|
|
243
|
+
Coercion plans give `string`, `number`, `integer`, `boolean`, `bigint` (`string` with a pattern),
|
|
244
|
+
`date` (`date-time`), enums and arrays. `Accept`, `Content-Type` and `Authorization` are never
|
|
245
|
+
parameters (describe credentials with a security scheme).
|
|
246
|
+
|
|
247
|
+
**Request bodies.** `@Body(S)` / `@Validate(S)`: `application/json` with the **input** schema;
|
|
248
|
+
`@Body() input: T`: the shape of `T` (`string`: `text/plain`, bytes: `application/octet-stream`).
|
|
249
|
+
Upload routes get a `multipart/form-data` object of their file fields
|
|
250
|
+
(`contentMediaType: application/octet-stream`, `maxItems`, `At most 2 MiB` and
|
|
251
|
+
`x-znight-max-size`, `encoding.contentType` from `mimeTypes`), merged with the text fields of
|
|
252
|
+
`@Body(Form)` (`allOf`).
|
|
253
|
+
|
|
254
|
+
**Responses.** The success status is `@HttpCode`, else 201 for POST, else 200 (204 when the
|
|
255
|
+
handler returns `void`; `T | undefined` also documents the empty 204). Its content comes from the
|
|
256
|
+
annotated return type, `Promise` unwrapped, with the media types of the serialiser:
|
|
257
|
+
|
|
258
|
+
| Return type | Success response |
|
|
259
|
+
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
260
|
+
| Interfaces, aliases, classes, records, arrays, unions... | `application/json` with the schema of the type |
|
|
261
|
+
| `HttpResponse<T>`, `HttpResponse<A> \| HttpResponse<B> \| C` | As `T` (`A \| B \| C`); the status is chosen at runtime: other 2XX may be declared |
|
|
262
|
+
| `HttpResponse<SseBody>` (`HttpResponse.sse()`) | `text/event-stream`, status 200 even on POST (unless `@HttpCode`) |
|
|
263
|
+
| `z.output<typeof S>`, `v.InferOutput<typeof S>`, ArkType `S.infer` | The **output** JSON Schema of `S` |
|
|
264
|
+
| `string`, string literals, string enums | `text/plain` (`string \| null`: `text/plain` and `application/json` for the rest) |
|
|
265
|
+
| Bytes, streams, `Blob`, `HttpResponse<FileBody>` | `application/octet-stream` (set the media type with `@ApiResponse(s, { contentType })`) |
|
|
266
|
+
| `@Sse` routes (`AsyncIterable<SseEvent<T>>`, `AsyncGenerator`...) | `text/event-stream`, the schema of `T` in `x-znight-event-schema` |
|
|
267
|
+
| `void`, `undefined` | No content |
|
|
268
|
+
| A body with `@HttpCode(204)` / `304` | ZNIGHT035 error: the serialiser drops the body of bodyless statuses |
|
|
269
|
+
| No annotation, `unknown`, bare `HttpResponse`, `Response`, `@Res()` | ZNIGHT031, unless `@ApiResponse` documents the success status |
|
|
270
|
+
|
|
271
|
+
`@Header` values are documented on the success response. **Standard errors** (`errors: true`):
|
|
272
|
+
`400` (`#/components/responses/BadRequest`) on routes that coerce or validate an input, `413` on
|
|
273
|
+
routes that read a body, `415` on upload routes, and `HttpErrorBody` for every declared `4XX` /
|
|
274
|
+
`5XX` response without schema. A custom envelope (RFC 9457 problem details) is declared once:
|
|
275
|
+
`errors: { schema: ProblemDetails, contentType: 'application/problem+json' }`.
|
|
276
|
+
|
|
277
|
+
**Types → JSON Schema.** Interfaces (flattened `extends`), classes (instance fields, `toJSON()`),
|
|
278
|
+
aliases, enums, generics (`Page<Book>` → component `Page_Book`; an anonymous type argument adds a
|
|
279
|
+
short hash of the instantiation: `Page<{ id: string }>` → `Page_object_1a2b3c4d`), records, tuples,
|
|
280
|
+
`Partial`, `Pick`, `Omit`... `T | null` gives `type: [T, 'null']`, unions of literals give `enum`,
|
|
281
|
+
unions of objects with a required literal property give `oneOf`, with a `discriminator` and its
|
|
282
|
+
`mapping` when every member is a named declaration. `Date` is a `date-time` string and `URL` a
|
|
283
|
+
`uri` string. Named declarations become components, named after the declaration. Standard Schemas
|
|
284
|
+
are named after their export, with an `XInput` variant when their input and output JSON Schemas
|
|
285
|
+
differ; the same name for a declaration and a schema is ZNIGHT034 at compile time, except an
|
|
286
|
+
interface merged with the schema it is inferred from
|
|
287
|
+
(`export interface User extends z.infer<typeof User> {}`), documented by the schema.
|
|
288
|
+
|
|
289
|
+
Converted schemas: `$defs` are hoisted into components. A definition with an id
|
|
290
|
+
(`.meta({ id: 'Address' })`) is named after it, and gets an `AddressInput` variant when it differs
|
|
291
|
+
between requests and responses; a root `.meta({ id })` equal to the export name is the component
|
|
292
|
+
itself (another id gives an alias); anonymous definitions (Zod `__schema0` of a recursive schema)
|
|
293
|
+
are scoped to their schema (`Tree_schema0`). Dates are `date-time` strings in responses (Zod and
|
|
294
|
+
Valibot) and for coerced dates in requests (`z.coerce.date()`).
|
|
295
|
+
|
|
296
|
+
## Security
|
|
297
|
+
|
|
298
|
+
For each operation, the first rule that applies wins: `@ApiSecurity` on the route, on the
|
|
299
|
+
controller, then the first `decisions` entry whose route decorator is in the route's effective
|
|
300
|
+
metadata (route entries first), else the document `security`.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
export const OAuth = securityScheme('oauth2', {
|
|
304
|
+
type: 'oauth2',
|
|
305
|
+
flows: {
|
|
306
|
+
clientCredentials: {
|
|
307
|
+
tokenUrl: 'https://id.example.com/oauth2/token',
|
|
308
|
+
scopes: { reader: 'Read books', admin: 'Manage books' },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
@OpenApiDocument({
|
|
314
|
+
security: [OAuth],
|
|
315
|
+
decisions: [
|
|
316
|
+
{ when: Public, security: [] },
|
|
317
|
+
{ when: Roles, security: [OAuth], scopes: 'value' }, // @Roles(['admin']) → security: [{ oauth2: ['admin'] }]
|
|
318
|
+
],
|
|
319
|
+
})
|
|
320
|
+
export class BookshelfApi {}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Requirements: `ApiKey` (one scheme), `[Bearer, Tenant]` (both), `{ scheme: OAuth, scopes: [...] }`;
|
|
324
|
+
several arguments of `@ApiSecurity` are alternatives, and `@ApiSecurity()` means public. Scopes
|
|
325
|
+
must be declared by a flow of the scheme and are only allowed on `oauth2` and `openIdConnect`
|
|
326
|
+
schemes (ZNIGHT036).
|
|
327
|
+
|
|
328
|
+
## Plugin options
|
|
329
|
+
|
|
330
|
+
`OpenApiPlugin.configure(options | (container) => options)` (name `openapi`):
|
|
331
|
+
|
|
332
|
+
| Option | Default | Description |
|
|
333
|
+
| ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
334
|
+
| `enabled` | `true` | `false` serves nothing and builds nothing (the `OpenApi` external still works) |
|
|
335
|
+
| `path` | `'/openapi.json'` | JSON document, relative to the HTTP prefix |
|
|
336
|
+
| `yaml` | `false` | A path (`'/openapi.yaml'`) serves YAML (`application/yaml`, needs `yaml`) |
|
|
337
|
+
| `ui` | `'scalar'` | `'scalar'`, `'swagger'`, `false`, or `{ scalar: config }` / `{ swagger: config }` (JSON configuration) |
|
|
338
|
+
| `uiPath` | `'/docs'` | The UI page; local assets are under `${uiPath}/_assets/` |
|
|
339
|
+
| `assets` | `'auto'` | `'local'` (from `node_modules`), `'cdn'` (jsDelivr, pinned with SRI), `'auto'` (local when installed) |
|
|
340
|
+
| `document` | `'build'` | `{ file }` serves a prebuilt `openapi.json` (no conversion at boot; a stale file is a ZNIGHT030 warning)² |
|
|
341
|
+
| `build` | `'boot'` | `'boot'`: a failure fails `Application.create`; `'first-request'`: built lazily, one shared promise, retried |
|
|
342
|
+
| `info`, `servers` | — | Runtime overrides (`info` merged, `servers` replaced) |
|
|
343
|
+
| `filter` | — | `(operation: OpenApiOperationView) => boolean`; unreferenced components are pruned |
|
|
344
|
+
| `transform` | — | `(document) => document`, last-resort escape hatch |
|
|
345
|
+
| `converters` | `[]` | Extra `SchemaConverter`s, tried before the built-in ones |
|
|
346
|
+
| `unrepresentable` | `'error'` | `'warn'`: a schema that cannot be converted becomes `{}` with a ZNIGHT037 warning |
|
|
347
|
+
| `meta` | `[]` | Route metadata of the docs routes (`[Public.meta()]`), seen by guards and by the route decision check |
|
|
348
|
+
| `guards` | `[]` | Extra guards of the docs routes (global guards still run first) |
|
|
349
|
+
| `cacheControl` | `'no-cache'` | `cache-control` of the document (revalidated with the ETag) |
|
|
350
|
+
|
|
351
|
+
² `info`, `servers`, `filter` and `transform` apply to the file as to a build. Under an HTTP
|
|
352
|
+
prefix, the servers of the file are checked (ZNIGHT030 warnings), so that clients generated from
|
|
353
|
+
the file call the right paths:
|
|
354
|
+
|
|
355
|
+
- a file without `servers` is served with `[{ url: prefix }]`: emit it with the prefix
|
|
356
|
+
(`znight openapi --prefix /api`, `znight build --openapi --openapi-prefix /api`, or
|
|
357
|
+
`openapi({ prefix: '/api' })` in `vite.config`);
|
|
358
|
+
- a relative server that does not end with the prefix (`/v1` under `/api`) is reported;
|
|
359
|
+
- so is a file where no server ends with the prefix: `servers: []` (served as is), or only
|
|
360
|
+
absolute servers of another path (`https://api.example.com`). A relative server, an absolute one
|
|
361
|
+
whose path ends with the prefix (`https://api.example.com/api`) or one with variables is enough.
|
|
362
|
+
Behind a gateway that rewrites the paths, give the plugin its own `servers`: they replace those
|
|
363
|
+
of the file and only they are checked.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
OpenApiPlugin.configure(container => ({
|
|
367
|
+
enabled: container.get(ServerConfig).docs, // DOCS=false hides the documentation
|
|
368
|
+
meta: [Public.meta()], // the access group of @znight/http route decisions is required: the docs are public
|
|
369
|
+
ui: { scalar: { theme: 'purple' } },
|
|
370
|
+
document: process.env['NODE_ENV'] === 'production' ? { file: new URL('./openapi.json', import.meta.url) } : 'build',
|
|
371
|
+
}))
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The docs routes are **plugin routes** of `@znight/http`: they go through the same pipeline as the
|
|
375
|
+
controllers (`http:request` hook, request scope, global guards, then `guards`), are listed in
|
|
376
|
+
`HttpPlugin.routes` with `source: 'openapi'`, and a collision with a controller route is ZNIGHT007.
|
|
377
|
+
The document is served from precomputed bytes with a strong ETag (`if-none-match` → 304, `HEAD`
|
|
378
|
+
included). The UI page has its own `content-security-policy` (`default-src 'none'`, the inline
|
|
379
|
+
script allowed by hash, `connect-src` limited to the document's servers, in every build mode: the
|
|
380
|
+
first request of the page builds the document with `build: 'first-request'`); Scalar runs without its
|
|
381
|
+
fonts, telemetry nor proxy, Swagger UI without its validator. With local assets nothing is loaded
|
|
382
|
+
from a third party; otherwise only the pinned jsDelivr files, with Subresource Integrity. The
|
|
383
|
+
installed UI package is found the same way on Node.js and Bun, even when its exports map does not
|
|
384
|
+
export its `package.json` (`@scalar/api-reference`).
|
|
385
|
+
|
|
386
|
+
Logs: `OpenAPI document built` (debug: operations, schemas, ms), each assembly warning once,
|
|
387
|
+
`API reference: <address>/docs` when the server listens. Exclude the docs from request logs with
|
|
388
|
+
`logging.requests.ignore: e => e.route?.startsWith('/docs')`.
|
|
389
|
+
|
|
390
|
+
## Injection
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { Injectable, type OnReady } from '@znight/core'
|
|
394
|
+
import { OpenApi } from '@znight/openapi'
|
|
395
|
+
|
|
396
|
+
/** Publishes the contract to the API gateway once the service is ready. */
|
|
397
|
+
@Injectable()
|
|
398
|
+
export class GatewayRegistration implements OnReady {
|
|
399
|
+
constructor(
|
|
400
|
+
private readonly openapi: OpenApi,
|
|
401
|
+
private readonly gateway: GatewayClient,
|
|
402
|
+
) {}
|
|
403
|
+
|
|
404
|
+
async onReady(): Promise<void> {
|
|
405
|
+
await this.gateway.publish(await this.openapi.json())
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`OpenApi` (singleton external, token `@znight/openapi#OpenApi`): `document()`, `json()`, `yaml()`
|
|
411
|
+
and `operations` (`OpenApiOperationView`: method, path, operation id, tags, controller, handler,
|
|
412
|
+
effective route metadata).
|
|
413
|
+
|
|
414
|
+
## Emitting openapi.json at build time
|
|
415
|
+
|
|
416
|
+
```sh
|
|
417
|
+
znight openapi # writes openapi.json at the project root
|
|
418
|
+
znight openapi -o docs/openapi.yaml # YAML, chosen by the extension (needs yaml)
|
|
419
|
+
znight openapi --check # CI: exit 1 when the file is missing or stale; never writes
|
|
420
|
+
znight openapi --prefix /api # servers default when @OpenApiDocument declares none
|
|
421
|
+
znight openapi --runtime bun # load the modules with Bun (default: the runtime running the CLI)
|
|
422
|
+
znight build --openapi # after the bundle: dist/openapi.json, next to dist/main.js
|
|
423
|
+
znight build --openapi --openapi-prefix /api # the same, under HttpPlugin.configure({ prefix: '/api' })
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Pass the `prefix` of `HttpPlugin.configure()` to every command that emits the file served with
|
|
427
|
+
`document: { file }`: `--prefix` (`znight openapi`), `--openapi-prefix` (`znight build`, it needs
|
|
428
|
+
`--openapi`: a usage error otherwise), or `prefix` (the `openapi()` Vite plugin). It becomes the
|
|
429
|
+
`servers` of the document when `@OpenApiDocument` declares none, so the file and a build at boot
|
|
430
|
+
serve the same document; without it, the plugin warns (ZNIGHT030) and serves the file with
|
|
431
|
+
`[{ url: prefix }]`, but clients generated from the file call the wrong paths. When the Vite config
|
|
432
|
+
registers its own `openapi()`, `znight build --openapi=<path>` and `--openapi-prefix` do not apply
|
|
433
|
+
(the CLI warns): set `openapi({ output, prefix })` there.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
// vite.config.ts
|
|
437
|
+
import { znight } from '@znight/compiler'
|
|
438
|
+
import { openapi } from '@znight/openapi/vite'
|
|
439
|
+
import { defineConfig } from 'vite'
|
|
440
|
+
import options from './znight.config.ts'
|
|
441
|
+
|
|
442
|
+
export default defineConfig({
|
|
443
|
+
plugins: [
|
|
444
|
+
znight(options),
|
|
445
|
+
// after the bundle is written (and after each good rebuild of znight dev); default output: dist/openapi.json
|
|
446
|
+
// prefix: the prefix of HttpPlugin.configure() (servers default when @OpenApiDocument declares none)
|
|
447
|
+
// check (CI): openapi.json must be up to date (never written); dist/openapi.json is always written
|
|
448
|
+
openapi({ output: ['openapi.json', 'dist/openapi.json'], prefix: '/api', check: process.env['CI'] === 'true' }),
|
|
449
|
+
],
|
|
450
|
+
})
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
// programmatic API, used by the CLI and the Vite plugin
|
|
455
|
+
import { emitOpenApi } from '@znight/openapi/emit'
|
|
456
|
+
|
|
457
|
+
const result = await emitOpenApi({ root: process.cwd(), output: 'openapi.json', check: false, runtime: 'auto' })
|
|
458
|
+
// { ok, written, stale, staleFiles, document, diagnostics, warnings, outputs, runtime }
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Emission compiles the application like `znight generate` (`znight.config.*`, else the `znight()`
|
|
462
|
+
plugin of the Vite config; compile errors stop it before any module runs), then loads the
|
|
463
|
+
generated module in a **child process** (Bun: the compiler preload; Node.js: a Vite server in
|
|
464
|
+
middleware mode), so module side effects never reach the CLI or Vite process. Nothing is
|
|
465
|
+
instantiated: no container, no plugin, no port; `NODE_ENV` defaults to `production`. Outputs are
|
|
466
|
+
written atomically, only when they changed; the file is byte-identical on Node.js and Bun.
|
|
467
|
+
|
|
468
|
+
Custom converters are runtime functions: emission loads them from a module whose default export is
|
|
469
|
+
a `SchemaConverter[]` (`--converters src/openapi.converters.ts`, `openapi({ converters })`).
|
|
470
|
+
|
|
471
|
+
**Commit `openapi.json` and check it in CI** (`znight openapi --check`): an API change shows up in
|
|
472
|
+
the pull request diff, and clients can be generated from the file without starting anything.
|
|
473
|
+
|
|
474
|
+
## Testing
|
|
475
|
+
|
|
476
|
+
`@znight/testing` needs nothing special: `TestApp` boots the plugin, `app.get(OpenApi)` reaches the
|
|
477
|
+
document.
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
import { OpenApi } from '@znight/openapi'
|
|
481
|
+
import { validateResponse } from '@znight/openapi/testing'
|
|
482
|
+
|
|
483
|
+
test('GET /books/:id answers what its documentation says', async () => {
|
|
484
|
+
await using app = await createTestApp() // the plugins of main.ts, with OpenApiPlugin.configure({ ui: false })
|
|
485
|
+
const document = await app.get(OpenApi).document()
|
|
486
|
+
const response = await app.inject({ method: 'GET', url: '/books/1', headers: { 'x-api-key': READER_KEY } })
|
|
487
|
+
expect(validateResponse(document, { method: 'GET', path: '/books/{id}', response })).toEqual([])
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
import { buildOpenApiDocument } from '@znight/openapi'
|
|
493
|
+
|
|
494
|
+
test('the committed openapi.json is up to date', async () => {
|
|
495
|
+
const { json } = await buildOpenApiDocument(__ZNIGHT_METADATA__)
|
|
496
|
+
expect(json).toBe(await Bun.file(new URL('../openapi.json', import.meta.url)).text())
|
|
497
|
+
})
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`validateResponse(document, { method, path, response })` returns `{ path, message, keyword }[]`
|
|
501
|
+
issues: the status must be documented (exact code, then `NXX`, then `default`), the media type
|
|
502
|
+
documented (ranges included), documented `required` headers present and a JSON body valid against
|
|
503
|
+
its schema (JSON Schema 2020-12 with the optional peer `@cfworker/json-schema`: no code generation,
|
|
504
|
+
so it runs on Bun, Node.js and edge runtimes). `response` is an `InjectResponse` or a
|
|
505
|
+
`{ status, headers, body }` object.
|
|
506
|
+
|
|
507
|
+
## Diagnostics
|
|
508
|
+
|
|
509
|
+
| Code | Name | When | Severity |
|
|
510
|
+
| --------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
511
|
+
| ZNIGHT030 | `InvalidOpenApiDefinition` | Invalid `@OpenApiDocument` options or a second one, `@Api*` outside a controller or route handler, invalid static values, a malformed `securityScheme()`, the reserved `$znight` key. At runtime: invalid plugin options, a missing OpenAPI section, a stale prebuilt file, servers not reflecting the prefix (warnings) | error / warning |
|
|
512
|
+
| ZNIGHT031 | `UndocumentedResponse` | The success response cannot be documented and no `@ApiResponse` covers it | warning (error with `strict`) |
|
|
513
|
+
| ZNIGHT032 | `UnrepresentableType` | A documented type has no JSON Schema or does not serialise as declared; a `bigint` in a JSON response is always an error | warning / error |
|
|
514
|
+
| ZNIGHT033 | `DuplicateOperationId` | Two operations with the same id | error |
|
|
515
|
+
| ZNIGHT034 | `SchemaNameConflict` | Two declarations or schemas for one component name (at compile time, or at assembly for hoisted `$defs` ids and `XInput` names) | error |
|
|
516
|
+
| ZNIGHT035 | `InconsistentOperation` | The documentation contradicts the route: a body on 204/304 (`@ApiResponse` or `@HttpCode`), another success status than the route's, `@ApiQuery('x')` next to `@Query('x')`, a documented `Accept` / `Content-Type` / `Authorization` header, `@ApiBody` next to a body schema. Warnings: `@ApiBody` on GET/HEAD, `@ApiExclude` with other `@Api*`, two templates of one path, a whole-object parameter documented twice (assembly) | error / warning |
|
|
517
|
+
| ZNIGHT036 | `InvalidSecurityRequirement` | A requirement that is not a `securityScheme()`, misplaced or undeclared scopes, one scheme name for two definitions, an invalid decision, a decision never applied (warning) | error / warning |
|
|
518
|
+
| ZNIGHT037 | `SchemaConversionFailed` | At assembly: no converter for a schema, a converter that throws or returns something else than an object, a whole-object query schema that is not an object | error (warning with `unrepresentable: 'warn'`) |
|
|
519
|
+
|
|
520
|
+
```text
|
|
521
|
+
✗ ZNIGHT032: ShelfStats.total is a bigint: JSON.stringify throws on bigint, so GET /books/stats (BookController.stats) would answer 500
|
|
522
|
+
--> src/books/book.ts:58:19
|
|
523
|
+
= hint: Return a number or a string (String(total)), or give the type a toJSON() method
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Compiler contract
|
|
527
|
+
|
|
528
|
+
`@znight/openapi/compiler` reads the data stored by `@znight/http/compiler` (`graph.extensions`,
|
|
529
|
+
see "Compiler contract for other extensions" in the `@znight/http` README) and the shapes of the
|
|
530
|
+
compiler (`ctx.types.shape()`), and generates `metadata.extensions.openapi` (types exported from
|
|
531
|
+
`@znight/openapi`):
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
interface OpenApiMetadata {
|
|
535
|
+
readonly version: 1
|
|
536
|
+
/** SHA-256 (base64url) of the section, schema values replaced by their names (stale-file detection). */
|
|
537
|
+
readonly fingerprint: string
|
|
538
|
+
/** openapi ('3.1.1'), info, servers?, tags?, externalDocs?, security?, x-*. */
|
|
539
|
+
readonly document: OpenApiDocumentTemplate
|
|
540
|
+
/** Sorted by path, then method; `operation` holds schema templates. */
|
|
541
|
+
readonly operations: readonly OpenApiOperationDefinition[]
|
|
542
|
+
/** JSON Schemas of TypeScript declarations, by component name (sorted). */
|
|
543
|
+
readonly components: Readonly<Record<string, SchemaTemplate>>
|
|
544
|
+
readonly securitySchemes: Readonly<Record<string, SecuritySchemeObject>>
|
|
545
|
+
/** Standard Schemas (imported values) converted at assembly, sorted by name. */
|
|
546
|
+
readonly schemas: readonly OpenApiSchemaSlot[] // { name, schema, io, source }
|
|
547
|
+
readonly errors?: { readonly contentType: string; readonly schema?: string }
|
|
548
|
+
/** Descriptions of the default tags (JSDoc of the controllers). */
|
|
549
|
+
readonly tagDescriptions?: Readonly<Record<string, string>>
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Where a schema is expected, the compiler writes JSON Schema or a placeholder
|
|
554
|
+
`{ $znight: 'schema', name, io }`; in `parameters`, `{ $znight: 'parameters', in, name, io: 'input' }`
|
|
555
|
+
expands to one parameter per property. The `$znight` key is reserved. The manifest declares the
|
|
556
|
+
extension and the `OpenApi` external:
|
|
557
|
+
|
|
558
|
+
```jsonc
|
|
559
|
+
"znight": {
|
|
560
|
+
"apiVersion": 1,
|
|
561
|
+
"compiler": "@znight/openapi/compiler",
|
|
562
|
+
"provides": [{ "export": "OpenApi", "plugin": "OpenApiPlugin", "scope": "singleton" }]
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Limitations
|
|
567
|
+
|
|
568
|
+
- OpenAPI 3.1.1 only (3.2 with `itemSchema` for streams is planned behind an option).
|
|
569
|
+
- Return types must be annotated: the compiler does not run the TypeScript checker.
|
|
570
|
+
- Controllers of compiled provider libraries are documented from their `.d.ts` signatures; their
|
|
571
|
+
`@Api*` decorators are not read yet.
|
|
572
|
+
- One document per application (use `filter` to hide operations).
|
|
573
|
+
- Error responses are not inferred from `throw` statements: declare them with `@ApiResponse`.
|