@nxgt/openapi-httpyz 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/README.md +590 -0
- package/dist/client/create-openapi-client.d.ts +28 -0
- package/dist/client/create-openapi-client.d.ts.map +1 -0
- package/dist/client/types.d.ts +194 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +296 -0
- package/dist/index.js.map +12 -0
- package/dist/request/check-request.d.ts +12 -0
- package/dist/request/check-request.d.ts.map +1 -0
- package/dist/request/to-request.d.ts +26 -0
- package/dist/request/to-request.d.ts.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
# @nxgt/openapi-httpyz
|
|
2
|
+
|
|
3
|
+
Binds the operations [`@nxgt/openapi-codegen`](https://www.npmjs.com/package/@nxgt/openapi-codegen)
|
|
4
|
+
generates from an OpenAPI spec onto a client of
|
|
5
|
+
[`@nxgt/httpyz`](https://www.npmjs.com/package/@nxgt/httpyz). Every call is
|
|
6
|
+
checked against the spec at compile time: the path, the parameters, the body
|
|
7
|
+
and each reply the operation declares. A reply is a union narrowed on its
|
|
8
|
+
status, so nothing throws for a status the spec declares.
|
|
9
|
+
|
|
10
|
+
The client sends, with its middleware, auth, retries and timeouts. The
|
|
11
|
+
binding adds what the spec knows: how each parameter and body is written,
|
|
12
|
+
the replies each operation declares, and checks by the server's own
|
|
13
|
+
validators.
|
|
14
|
+
|
|
15
|
+
> **0.x.** The API is still settling.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun add @nxgt/httpyz @nxgt/openapi-httpyz zod
|
|
21
|
+
bun add -d @nxgt/openapi-codegen typescript
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Both peers are required: `@nxgt/httpyz`, since the binding uses your client,
|
|
25
|
+
not a copy of its own, and `typescript` 6, for the types. The generated
|
|
26
|
+
`operations.ts` imports `zod`, so the app needs it too.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
Generate the spec's files, into `generated/openapi` unless `-o` says
|
|
31
|
+
otherwise:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bunx nxgt-openapi generate -i openapi.yaml
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then bind them:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createHttpClient } from '@nxgt/httpyz';
|
|
41
|
+
import { createOpenApiClient } from '@nxgt/openapi-httpyz';
|
|
42
|
+
import { operations } from './generated/openapi/operations.js';
|
|
43
|
+
|
|
44
|
+
const http = createHttpClient({
|
|
45
|
+
baseUrl: 'https://api.example.com',
|
|
46
|
+
timeout: 10_000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const api = createOpenApiClient(http, operations);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`operations`, the runtime table of the generated `operations.ts`, holds each
|
|
53
|
+
parameter's location and style, the body's media types, each reply's schema,
|
|
54
|
+
and the server's validators. Its type also carries each operation's input and
|
|
55
|
+
replies, `ClientOperations` of the generated `types.ts`, so the client is
|
|
56
|
+
typed from the table alone:
|
|
57
|
+
|
|
58
|
+
- `api.get` offers the paths that have a GET operation, and takes what the
|
|
59
|
+
operation at the chosen path takes.
|
|
60
|
+
- The client has only the methods the spec has an operation for: no `trace`
|
|
61
|
+
on a spec without a TRACE operation.
|
|
62
|
+
|
|
63
|
+
The types may also be given, as a table generated before
|
|
64
|
+
`@nxgt/openapi-codegen` carried them requires:
|
|
65
|
+
`createOpenApiClient<ClientOperations, OperationsByRoute>(http, operations)`.
|
|
66
|
+
`OperationsByRoute` may be left out: it is worked out of `ClientOperations`.
|
|
67
|
+
|
|
68
|
+
The client keeps that table as `api.operations`, for a package built over it,
|
|
69
|
+
such as [`@nxgt/httpyz-query/openapi`](https://www.npmjs.com/package/@nxgt/httpyz-query).
|
|
70
|
+
|
|
71
|
+
The client's options are the client's own: see
|
|
72
|
+
[`@nxgt/httpyz`](https://www.npmjs.com/package/@nxgt/httpyz#setup). The
|
|
73
|
+
binding's, `validate` and `decode`, are listed under
|
|
74
|
+
[`createOpenApiClient`](#createopenapiclient).
|
|
75
|
+
|
|
76
|
+
## Calls
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// By method and path, QUERY included
|
|
80
|
+
const reply = await api.get('/employees/{id}', { param: { id } });
|
|
81
|
+
|
|
82
|
+
// By operationId
|
|
83
|
+
await api.op('updateEmployee', { param: { id }, json: { name: 'Ada' } });
|
|
84
|
+
|
|
85
|
+
// fetch options, a timeout or a retry come last
|
|
86
|
+
await api.op('listEmployees', { query: { page: 2 } }, { signal, timeout: 2_000 });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A path the spec has no operation for, for that method, does not compile.
|
|
90
|
+
The input holds each part of the request, as `ClientOperations` types it:
|
|
91
|
+
|
|
92
|
+
| Key | Holds | Written |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `param` | path parameters | encoded into the path |
|
|
95
|
+
| `query` | query parameters | a list as a repeated key, or joined with commas when the spec says `explode: false` |
|
|
96
|
+
| `header` | header parameters, by lowercased name | a list joined with commas |
|
|
97
|
+
| `json` | a JSON body | `JSON.stringify`, as the JSON type the spec declares |
|
|
98
|
+
| `form` | a form body | URL-encoded when that is the declared type, else multipart `FormData`; a list as a repeated field |
|
|
99
|
+
| `text` | a text body | as it is, as the text type the spec declares |
|
|
100
|
+
| `body` | a binary body: `Blob`, `ArrayBuffer`, `Uint8Array` | as it is, as the binary type the spec declares |
|
|
101
|
+
|
|
102
|
+
The input may be left out when nothing in it is required, and an operation
|
|
103
|
+
that takes nothing takes no input: `api.op('health')`, or
|
|
104
|
+
`api.op('health', { signal })`.
|
|
105
|
+
|
|
106
|
+
## Cancelling
|
|
107
|
+
|
|
108
|
+
A call's init takes the client's own ways to end it early: its `signal`,
|
|
109
|
+
and `latest`, a key that aborts the call before it with the same key.
|
|
110
|
+
`api.group()` is the same client over the client's `http.group()`, whose
|
|
111
|
+
calls and streams end together:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// Typing ahead: only the last search stays in flight
|
|
115
|
+
await api.get('/employees', { query: { name } }, { latest: 'search' });
|
|
116
|
+
|
|
117
|
+
const page = api.group();
|
|
118
|
+
const employees = await page.op('listEmployees');
|
|
119
|
+
page.cancel(); // on leaving the page: its calls and streams still running abort
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A cancelled call rejects with an `AbortError`, never a `ClientError`: tell
|
|
123
|
+
it from a failure with the client's `isAbortError()`. The group goes on
|
|
124
|
+
after `cancel()`, and `page.signal` aborts on the next one. The details are
|
|
125
|
+
in [the client's README](https://www.npmjs.com/package/@nxgt/httpyz#cancelling).
|
|
126
|
+
|
|
127
|
+
## Replies
|
|
128
|
+
|
|
129
|
+
A call resolves to one of the replies the spec declares:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { unwrap } from '@nxgt/httpyz';
|
|
133
|
+
|
|
134
|
+
const reply = await api.get('/employees/{id}', { param: { id } });
|
|
135
|
+
reply.status; // 200 | 404
|
|
136
|
+
reply.type; // the declared media type: 'application/json'
|
|
137
|
+
reply.data; // Employee when status is 200, ErrorResponse when it is 404
|
|
138
|
+
reply.response; // the Response, for its headers: its body is read
|
|
139
|
+
|
|
140
|
+
const employee = unwrap(reply, 200); // or a ReplyStatusError
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A form reply's `data` is its `FormData`, and a binary one's a `Blob`. A
|
|
144
|
+
status the spec does not declare for the operation throws the client's
|
|
145
|
+
`UndeclaredStatusError`; errors are the client's own, listed in
|
|
146
|
+
[its README](https://www.npmjs.com/package/@nxgt/httpyz#errors).
|
|
147
|
+
|
|
148
|
+
## Streams
|
|
149
|
+
|
|
150
|
+
An operation whose reply the spec describes an item at a time, with OpenAPI
|
|
151
|
+
3.2's `itemSchema`, is read with `stream()`, by its `operationId`. Server-sent
|
|
152
|
+
events come through the client's `events()`, each narrowed on its name, and
|
|
153
|
+
JSON Lines through its `lines()`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const feed = api.stream('watchFeed', { query: { topic: 'news' } });
|
|
157
|
+
for await (const event of feed) {
|
|
158
|
+
if (event.event === 'added') event.data.name; // Item
|
|
159
|
+
else event.data; // 'note': text
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for await (const item of api.stream('exportItems', { json: { limit: 100 } })) {
|
|
163
|
+
item.id;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- It connects when read, and `close()` or a `break` ends it. Events
|
|
168
|
+
reconnect as `EventSource` does, but for POST and PATCH; the init after the
|
|
169
|
+
input takes `reconnect`, `lastEventId` and `onUnknownEvent`, beside the call
|
|
170
|
+
options.
|
|
171
|
+
- It sends the stream's declared media type as `Accept`, and writes the input
|
|
172
|
+
as a call does.
|
|
173
|
+
- `validate` checks the request on the first read, before anything is sent,
|
|
174
|
+
and each item against its schema; `decode` yields what each schema
|
|
175
|
+
outputs.
|
|
176
|
+
- Only an operation with a stream is accepted; the others do not compile.
|
|
177
|
+
|
|
178
|
+
An event the spec does not declare is not yielded: it goes to
|
|
179
|
+
`onUnknownEvent`. The stream API is the client's, described in
|
|
180
|
+
[its README](https://www.npmjs.com/package/@nxgt/httpyz#streams).
|
|
181
|
+
|
|
182
|
+
## Validating and decoding
|
|
183
|
+
|
|
184
|
+
The types hold a call to the spec, but not the values in it: a `page` of `0`
|
|
185
|
+
where the spec says `minimum: 1`, or a reply from a server that drifted.
|
|
186
|
+
`validate` checks both with the schemas in the generated `operations` table:
|
|
187
|
+
|
|
188
|
+
- **The request**, before it is sent, by the validators the server runs, on
|
|
189
|
+
what the server will read: each parameter as the text it travels as, the
|
|
190
|
+
JSON as it parses, a form as its fields. A request the server would refuse
|
|
191
|
+
throws a `ValidationError` with the same issues, and is never sent.
|
|
192
|
+
- **The reply**, before it is returned: a JSON or text reply against its
|
|
193
|
+
schema, as [`@nxgt/openapi-hono`](https://www.npmjs.com/package/@nxgt/openapi-hono)
|
|
194
|
+
checks its own with `validateResponses`.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
createOpenApiClient(http, operations, { validate: { request: true } });
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Unlike a plain call of the client, which checks a declared reply by default,
|
|
201
|
+
the binding checks nothing by default: the types already hold both ends to
|
|
202
|
+
the spec.
|
|
203
|
+
|
|
204
|
+
`decode` returns each reply as its schema outputs it, which differs from what
|
|
205
|
+
JSON carries once the spec is generated with `dates: 'date'`: a date-time is
|
|
206
|
+
a `Date`. The client's replies are typed so:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const api = createOpenApiClient(http, operations, { decode: true });
|
|
210
|
+
const employee = unwrap(await api.get('/employees/{id}', { param: { id } }), 200);
|
|
211
|
+
employee.hiredAt; // Date
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
With the types given, a third type argument says the client decodes:
|
|
215
|
+
`createOpenApiClient<ClientOperations, OperationsByRoute, true>`, which does
|
|
216
|
+
not compile without `decode: true`. Decoding validates the reply, whatever
|
|
217
|
+
`validate` says.
|
|
218
|
+
|
|
219
|
+
## API
|
|
220
|
+
|
|
221
|
+
The examples below use a spec with `getEmployee` at `GET /employees/{id}`,
|
|
222
|
+
`listEmployees` at `GET /employees`, `createEmployee` at `POST /employees`,
|
|
223
|
+
and `watchFeed`, an event stream, at `GET /feed`.
|
|
224
|
+
|
|
225
|
+
### Functions
|
|
226
|
+
|
|
227
|
+
#### createOpenApiClient
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// With decode: true, the replies are typed as their schemas output them
|
|
231
|
+
function createOpenApiClient<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>>(
|
|
232
|
+
http: HttpClient,
|
|
233
|
+
operations: OperationTable<Ops>,
|
|
234
|
+
options: OpenApiOptions & { readonly decode: true },
|
|
235
|
+
): OpenApiClient<Ops, Routes, true>;
|
|
236
|
+
|
|
237
|
+
function createOpenApiClient<
|
|
238
|
+
Ops extends OperationsShape<Ops>,
|
|
239
|
+
Routes = RoutesOf<Ops>,
|
|
240
|
+
Decoded extends boolean = false,
|
|
241
|
+
>(
|
|
242
|
+
http: HttpClient,
|
|
243
|
+
operations: OperationTable<Ops>,
|
|
244
|
+
...options: OpenApiArgs<Decoded>
|
|
245
|
+
): OpenApiClient<Ops, Routes, Decoded>;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Binds the generated `operations` table onto `http`, a client of
|
|
249
|
+
`createHttpClient()` or one of its groups. `Ops` is inferred from the table;
|
|
250
|
+
`Routes` defaults to `RoutesOf<Ops>`; `Decoded` is `true` when `decode: true`
|
|
251
|
+
is passed. See [Setup](#setup) and [Validating and decoding](#validating-and-decoding).
|
|
252
|
+
|
|
253
|
+
| Option | Type | Default | Description |
|
|
254
|
+
| --- | --- | --- | --- |
|
|
255
|
+
| `validate` | `boolean \| { request?: boolean; response?: boolean }` | neither | Checks with the spec's schemas, throwing a `ValidationError`: the request before it is sent, the reply before it is returned. `true` is both |
|
|
256
|
+
| `decode` | `true \| false` | `false` | `true` returns each reply as its schema outputs it, and types it so. It validates the reply, whatever `validate` says. A literal: a `boolean` variable does not compile, since the replies' type depends on it |
|
|
257
|
+
|
|
258
|
+
Returns an [`OpenApiClient`](#openapiclient). It throws nothing itself: an
|
|
259
|
+
unknown `operationId` or path fails the call made with it.
|
|
260
|
+
|
|
261
|
+
### The client
|
|
262
|
+
|
|
263
|
+
#### op
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
api.op<K extends keyof Ops & string>(id: K, ...args: Args<Ops, K>): Promise<WithResponse<OperationReply<Ops, K, Decoded>>>;
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Calls an operation by its `operationId`, with its input, if it takes one,
|
|
270
|
+
then an [`OperationInit`](#operationinit). See [Calls](#calls).
|
|
271
|
+
|
|
272
|
+
Resolves to one of the declared replies, `{ status, type, data, response }`,
|
|
273
|
+
as in [Replies](#replies). Rejects with a `ValidationError` when `validate`
|
|
274
|
+
refuses the request or the reply, with the client's errors, such as
|
|
275
|
+
`UndeclaredStatusError`, and with an `Error` for an `operationId` the table
|
|
276
|
+
does not have.
|
|
277
|
+
|
|
278
|
+
#### Path methods
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
api.get<P extends PathsOf<Routes, 'get'>>(path: P, ...args: Args<Ops, IdOf<Ops, Routes, 'get', P>>): Promise<WithResponse<OperationReply<Ops, IdOf<Ops, Routes, 'get', P>, Decoded>>>;
|
|
282
|
+
// and put, post, delete, options, head, patch, trace, query: each the spec has an operation for
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Calls the operation at a method and a path, as the spec writes it:
|
|
286
|
+
`api.get('/employees/{id}', { param: { id } })`. The client types only the
|
|
287
|
+
methods in [`MethodsOf<Routes>`](#methodsof), each taking only its
|
|
288
|
+
[`PathsOf`](#pathsof). The arguments, reply and errors are those of
|
|
289
|
+
[`op`](#op); a path with no operation for the method rejects with an `Error`.
|
|
290
|
+
|
|
291
|
+
#### stream
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
api.stream<K extends StreamIds<Ops>>(id: K, ...args: StreamArgs<Ops, K>): OperationStreamOf<Ops, K, Decoded>;
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Reads an operation's stream by its `operationId`, an item at a time: an
|
|
298
|
+
`EventStream` of events narrowed on `event`, or a `Stream` of JSON lines. It
|
|
299
|
+
takes the input, then a [`StreamInit`](#streaminit). See [Streams](#streams).
|
|
300
|
+
|
|
301
|
+
It connects when read, and `close()` ends it. It throws an `Error`, where it
|
|
302
|
+
is called, for an operation without a stream. A `ValidationError` from
|
|
303
|
+
`validate`, or a client error, is thrown where the stream is read.
|
|
304
|
+
|
|
305
|
+
#### group
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
api.group(): OpenApiGroup<Ops, Routes, Decoded>;
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The same client, with the same options, over the client's `http.group()`:
|
|
312
|
+
its calls and streams also end on its `cancel()`. See [Cancelling](#cancelling).
|
|
313
|
+
|
|
314
|
+
#### group.cancel
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
page.cancel(reason?: unknown): void;
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Aborts every call and stream of the group still running, with `reason`, or
|
|
321
|
+
an `AbortError`. The group goes on: the calls made after it run.
|
|
322
|
+
|
|
323
|
+
#### group.signal
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
readonly page.signal: AbortSignal;
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Aborts on the next `cancel()`: for work of your own that ends with the
|
|
330
|
+
group's calls. It is a new signal after each `cancel()`.
|
|
331
|
+
|
|
332
|
+
#### operations
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
readonly api.operations: OperationTable<Ops>;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The generated `operations` table the client was bound to, for a package
|
|
339
|
+
built over it that reads the spec as the client does, such as
|
|
340
|
+
[`@nxgt/httpyz-query/openapi`](https://www.npmjs.com/package/@nxgt/httpyz-query).
|
|
341
|
+
|
|
342
|
+
### Types
|
|
343
|
+
|
|
344
|
+
#### OpenApiClient
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
type OpenApiClient<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>, Decoded extends boolean = false> = {
|
|
348
|
+
op: …; // see op
|
|
349
|
+
stream: …; // see stream
|
|
350
|
+
group(): OpenApiGroup<Ops, Routes, Decoded>;
|
|
351
|
+
readonly operations: OperationTable<Ops>;
|
|
352
|
+
} & PathMethods<Ops, Routes, Decoded>;
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
What `createOpenApiClient()` returns: the members are under
|
|
356
|
+
[The client](#the-client).
|
|
357
|
+
|
|
358
|
+
#### OpenApiGroup
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
type OpenApiGroup<Ops, Routes = RoutesOf<Ops>, Decoded extends boolean = false> =
|
|
362
|
+
OpenApiClient<Ops, Routes, Decoded> & {
|
|
363
|
+
cancel(reason?: unknown): void;
|
|
364
|
+
readonly signal: AbortSignal;
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
What [`group()`](#group) returns: the client, with
|
|
369
|
+
[`cancel`](#groupcancel) and [`signal`](#groupsignal).
|
|
370
|
+
|
|
371
|
+
#### PathMethods
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
type PathMethods<Ops, Routes = RoutesOf<Ops>, Decoded extends boolean = false> = {
|
|
375
|
+
readonly [M in MethodsOf<Routes>]: <P extends PathsOf<Routes, M>>(
|
|
376
|
+
path: P,
|
|
377
|
+
...args: Args<Ops, IdOf<Ops, Routes, M, P>>
|
|
378
|
+
) => Promise<WithResponse<OperationReply<Ops, IdOf<Ops, Routes, M, P>, Decoded>>>;
|
|
379
|
+
};
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The [path methods](#path-methods) alone, for a package that offers them on an
|
|
383
|
+
object of its own, as [`@nxgt/datasource-rest`](https://www.npmjs.com/package/@nxgt/datasource-rest)
|
|
384
|
+
does. `PathMethods<ClientOperations>` has `get` and `post` on the example spec.
|
|
385
|
+
|
|
386
|
+
#### OpenApiOptions
|
|
387
|
+
|
|
388
|
+
| Field | Type | Description |
|
|
389
|
+
| --- | --- | --- |
|
|
390
|
+
| `validate` | `boolean \| { readonly request?: boolean; readonly response?: boolean }` | Checks the request, the reply, or both (`true`). Default: neither |
|
|
391
|
+
|
|
392
|
+
The options of [`createOpenApiClient`](#createopenapiclient), beside `decode`.
|
|
393
|
+
|
|
394
|
+
#### OpenApiArgs
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
type OpenApiArgs<Decoded extends boolean> = Decoded extends true
|
|
398
|
+
? [options: OpenApiOptions & { readonly decode: true }]
|
|
399
|
+
: [options?: OpenApiOptions & { readonly decode?: false }];
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The options argument of `createOpenApiClient()`, required with `decode: true`
|
|
403
|
+
when `Decoded` is `true`. `OpenApiArgs<false>` resolves to
|
|
404
|
+
`[options?: OpenApiOptions & { readonly decode?: false }]`.
|
|
405
|
+
|
|
406
|
+
#### OperationInit
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
type OperationInit = Omit<CallOptions, 'operationId'>;
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
What a call takes after its input: the client's call options, the binding
|
|
413
|
+
naming the call itself.
|
|
414
|
+
|
|
415
|
+
| Field | Type | Description |
|
|
416
|
+
| --- | --- | --- |
|
|
417
|
+
| `headers` | `HeadersInit` | Over the client's `headers`; a `Content-Type` here gives way to the one a JSON, text or binary body sets |
|
|
418
|
+
| `timeout` | `number` | This call's timeout, instead of the client's |
|
|
419
|
+
| `retry` | `number \| RetryOptions \| false` | This call's retry, instead of the client's: `false` never retries |
|
|
420
|
+
| `latest` | `string` | Aborts the call before it with the same key, if it still runs |
|
|
421
|
+
| `signal`, `cache`, `credentials`, … | as in `RequestInit` | fetch's own options, but `method`, `body` and `headers` |
|
|
422
|
+
|
|
423
|
+
#### StreamInit
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
interface StreamInit extends OperationInit {
|
|
427
|
+
reconnect?: boolean | ReconnectOptions;
|
|
428
|
+
lastEventId?: string;
|
|
429
|
+
onUnknownEvent?: (event: ServerEvent) => void;
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
What [`stream()`](#stream) takes after its input.
|
|
434
|
+
|
|
435
|
+
| Field | Type | Description |
|
|
436
|
+
| --- | --- | --- |
|
|
437
|
+
| `reconnect` | `boolean \| ReconnectOptions` | Events only: connects again when the connection drops or the stream ends, as `EventSource` does. Default: on, but for POST and PATCH |
|
|
438
|
+
| `lastEventId` | `string` | Events only: sent as `Last-Event-ID` on the first connection |
|
|
439
|
+
| `onUnknownEvent` | `(event: ServerEvent) => void` | Events only: an event the spec does not declare, which is not yielded |
|
|
440
|
+
|
|
441
|
+
#### Args
|
|
442
|
+
|
|
443
|
+
A call's arguments after the `operationId` or the path: the operation's own,
|
|
444
|
+
then an `OperationInit`.
|
|
445
|
+
`Args<ClientOperations, 'getEmployee'>` resolves to
|
|
446
|
+
`[input: { param: { id: number } }, init?: OperationInit]`.
|
|
447
|
+
|
|
448
|
+
#### StreamArgs
|
|
449
|
+
|
|
450
|
+
A stream's arguments after the `operationId`: the operation's own, then a
|
|
451
|
+
`StreamInit`.
|
|
452
|
+
`StreamArgs<ClientOperations, 'watchFeed'>` resolves to
|
|
453
|
+
`[input: { query: { topic: string } }, init?: StreamInit]`.
|
|
454
|
+
|
|
455
|
+
#### OperationReply
|
|
456
|
+
|
|
457
|
+
An operation's replies, decoded or as JSON carries them, as `op()` and the
|
|
458
|
+
path methods resolve to them without `response`.
|
|
459
|
+
`OperationReply<ClientOperations, 'getEmployee', false>` resolves to
|
|
460
|
+
`ClientOperations['getEmployee']['wire']`, and with `true` to its `reply`.
|
|
461
|
+
|
|
462
|
+
#### OperationStreamOf
|
|
463
|
+
|
|
464
|
+
What [`stream()`](#stream) returns: an `EventStream` for server-sent events, a
|
|
465
|
+
`Stream` for JSON lines, of items decoded or as JSON carries them.
|
|
466
|
+
`OperationStreamOf<ClientOperations, 'watchFeed', false>` resolves to
|
|
467
|
+
`EventStream<ClientOperations['watchFeed']['stream']['wire']>`.
|
|
468
|
+
|
|
469
|
+
#### StreamIds
|
|
470
|
+
|
|
471
|
+
The operations that reply with a stream: the only ones `stream()` accepts.
|
|
472
|
+
`StreamIds<ClientOperations>` resolves to `'watchFeed'`.
|
|
473
|
+
|
|
474
|
+
#### RoutesOf
|
|
475
|
+
|
|
476
|
+
`OperationsByRoute`, worked out of `ClientOperations`: each `'method path'`
|
|
477
|
+
to its `operationId`, and the default of `Routes`.
|
|
478
|
+
`RoutesOf<ClientOperations>['get /employees/{id}']` resolves to `'getEmployee'`.
|
|
479
|
+
|
|
480
|
+
#### MethodsOf
|
|
481
|
+
|
|
482
|
+
The methods the spec has an operation for: the only path methods a client
|
|
483
|
+
offers. `MethodsOf<RoutesOf<ClientOperations>>` resolves to `'get' | 'post'`.
|
|
484
|
+
|
|
485
|
+
#### PathsOf
|
|
486
|
+
|
|
487
|
+
The paths with an operation for a method.
|
|
488
|
+
`PathsOf<RoutesOf<ClientOperations>, 'get'>` resolves to
|
|
489
|
+
`'/employees' | '/employees/{id}' | '/feed'`.
|
|
490
|
+
|
|
491
|
+
#### IdOf
|
|
492
|
+
|
|
493
|
+
The `operationId` of the operation at a method and a path.
|
|
494
|
+
`IdOf<ClientOperations, RoutesOf<ClientOperations>, 'get', '/employees/{id}'>`
|
|
495
|
+
resolves to `'getEmployee'`.
|
|
496
|
+
|
|
497
|
+
#### OperationsShape
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
type OperationsShape<Ops> = { [K in keyof Ops]: ClientOperation };
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The constraint on `Ops`: the generated `ClientOperations`, an entry per
|
|
504
|
+
`operationId`.
|
|
505
|
+
|
|
506
|
+
#### ClientOperation
|
|
507
|
+
|
|
508
|
+
| Field | Type | Description |
|
|
509
|
+
| --- | --- | --- |
|
|
510
|
+
| `method` | `Method` | The operation's method, lowercased |
|
|
511
|
+
| `path` | `string` | The path as the spec writes it |
|
|
512
|
+
| `args` | `readonly unknown[]` | What a call takes after the `operationId`: `[input]`, `[input?]` or `[]` |
|
|
513
|
+
| `reply` | `unknown` | Every declared reply, `{ status; type; data }`, decoded |
|
|
514
|
+
| `wire` | `unknown` | The same replies as JSON carries them |
|
|
515
|
+
| `stream` | `OperationStream`, optional | A reply read an item at a time, when the operation has one |
|
|
516
|
+
|
|
517
|
+
What the binding reads of an entry of the generated `ClientOperations`.
|
|
518
|
+
|
|
519
|
+
#### OperationStream
|
|
520
|
+
|
|
521
|
+
| Field | Type | Description |
|
|
522
|
+
| --- | --- | --- |
|
|
523
|
+
| `kind` | `'sse' \| 'jsonl'` | Server-sent events, or JSON lines |
|
|
524
|
+
| `item` | `unknown` | Each item, decoded: an event narrowed on `event`, or a line |
|
|
525
|
+
| `wire` | `unknown` | Each item as JSON carries it |
|
|
526
|
+
|
|
527
|
+
The `stream` of a `ClientOperation`.
|
|
528
|
+
|
|
529
|
+
#### OperationTable
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
type OperationTable<Ops> = {
|
|
533
|
+
readonly [K in keyof Ops]: RuntimeOperation & { readonly '~client'?: Ops[K] };
|
|
534
|
+
};
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
The type of the generated `operations` table, and of `api.operations`. Each
|
|
538
|
+
entry carries its `ClientOperations` entry as `'~client'`, a type that is
|
|
539
|
+
never set, which is how `createOpenApiClient(http, operations)` infers `Ops`.
|
|
540
|
+
|
|
541
|
+
#### RuntimeOperation
|
|
542
|
+
|
|
543
|
+
| Field | Type | Description |
|
|
544
|
+
| --- | --- | --- |
|
|
545
|
+
| `method` | `Method` | The operation's method |
|
|
546
|
+
| `path` | `string` | As the spec writes it: `/employees/{id}` |
|
|
547
|
+
| `parameters` | `readonly RuntimeParameter[]` | How each parameter is written |
|
|
548
|
+
| `param`, `query`, `header` | `StandardSchemaV1`, optional | The server's validators of each location, which read text: for `validate` |
|
|
549
|
+
| `body` | `{ required: boolean; content: { [mediaType]: RuntimeMedia } }`, optional | The request body's media types |
|
|
550
|
+
| `responses` | `{ [status]: { [mediaType]: RuntimeMedia } }` | Each declared reply's media types |
|
|
551
|
+
|
|
552
|
+
What the binding reads of an entry of the `operations` table.
|
|
553
|
+
|
|
554
|
+
#### RuntimeParameter
|
|
555
|
+
|
|
556
|
+
| Field | Type | Description |
|
|
557
|
+
| --- | --- | --- |
|
|
558
|
+
| `name` | `string` | As the spec writes it |
|
|
559
|
+
| `in` | `'path' \| 'query' \| 'header'` | Where it goes |
|
|
560
|
+
| `required` | `boolean` | Whether the spec requires it |
|
|
561
|
+
| `explode` | `boolean` | A query list as `?a=1&a=2` (`true`) or `?a=1,2` (`false`) |
|
|
562
|
+
| `list` | `boolean` | Whether it is validated as a list |
|
|
563
|
+
|
|
564
|
+
An entry of a `RuntimeOperation`'s `parameters`.
|
|
565
|
+
|
|
566
|
+
#### RuntimeMedia
|
|
567
|
+
|
|
568
|
+
| Field | Type | Description |
|
|
569
|
+
| --- | --- | --- |
|
|
570
|
+
| `kind` | `'json' \| 'form' \| 'text' \| 'binary' \| 'sse' \| 'jsonl'` | How the content is read or written |
|
|
571
|
+
| `schema` | `StandardSchemaV1`, optional | The content's schema; absent for binary content and a stream |
|
|
572
|
+
| `events` | `{ [event]: StandardSchemaV1 \| null }`, optional | `sse`: each event's data, by name: a schema for JSON, `null` for text |
|
|
573
|
+
| `item` | `StandardSchemaV1`, optional | `jsonl`: each item |
|
|
574
|
+
|
|
575
|
+
A media type of a `RuntimeOperation`'s `body` or `responses`.
|
|
576
|
+
|
|
577
|
+
## Traps
|
|
578
|
+
|
|
579
|
+
- **Without `decode`, a reply is typed as JSON carries it**: with
|
|
580
|
+
`dates: 'date'`, a date-time is a string.
|
|
581
|
+
- **A request is typed as JSON carries it, whatever `decode` says.** With
|
|
582
|
+
`dates: 'date'`, a date-time in a body or a query is still typed as a
|
|
583
|
+
string: pass `date.toISOString()`.
|
|
584
|
+
- **Give each operation its exact statuses.** A `default` or `4XX` reply is
|
|
585
|
+
not generated, so it throws `UndeclaredStatusError`.
|
|
586
|
+
- **A stream's request is refused where the stream is read**, not where
|
|
587
|
+
`stream()` is called: put the `try` around the `for await`.
|
|
588
|
+
- **The client's `baseUrl` is still required outside a browser**, and its
|
|
589
|
+
options, `auth`, `retry` and `use` included, apply to every call the
|
|
590
|
+
binding makes.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createOpenApiClient`: the operations `@nxgt/openapi-codegen` generates,
|
|
3
|
+
* bound onto a client of `createHttpClient`. The client sends; the binding
|
|
4
|
+
* adds what the spec knows: how each parameter and body is written, the
|
|
5
|
+
* replies each operation declares, and checks by the server's own schemas.
|
|
6
|
+
*/
|
|
7
|
+
import { type HttpClient } from '@nxgt/httpyz';
|
|
8
|
+
import type { OpenApiArgs, OpenApiClient, OpenApiOptions, OperationsShape, OperationTable, RoutesOf } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* A client for a spec, bound to the `operations` table of the generated
|
|
11
|
+
* `operations.ts`, which carries the spec's types: nothing else to import.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const http = createHttpClient({ baseUrl: 'https://api.example.com' });
|
|
15
|
+
* const api = createOpenApiClient(http, operations);
|
|
16
|
+
* const reply = await api.get('/employees/{id}', { param: { id } });
|
|
17
|
+
* if (reply.status === 200) reply.data.name;
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* With `decode: true`, each reply is returned as its schema outputs it, and
|
|
21
|
+
* typed so. The types may also be given: `createOpenApiClient<ClientOperations,
|
|
22
|
+
* OperationsByRoute, true>`, whose `true` then requires `decode: true`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createOpenApiClient<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>>(http: HttpClient, operations: OperationTable<Ops>, options: OpenApiOptions & {
|
|
25
|
+
readonly decode: true;
|
|
26
|
+
}): OpenApiClient<Ops, Routes, true>;
|
|
27
|
+
export declare function createOpenApiClient<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>, Decoded extends boolean = false>(http: HttpClient, operations: OperationTable<Ops>, ...[given]: OpenApiArgs<Decoded>): OpenApiClient<Ops, Routes, Decoded>;
|
|
28
|
+
//# sourceMappingURL=create-openapi-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-openapi-client.d.ts","sourceRoot":"","sources":["../../src/client/create-openapi-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAEN,KAAK,UAAU,EAGf,MAAM,cAAc,CAAC;AAItB,OAAO,KAAK,EACX,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,cAAc,EACd,QAAQ,EAIR,MAAM,SAAS,CAAC;AA6DjB;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAClC,GAAG,SAAS,eAAe,CAAC,GAAG,CAAC,EAChC,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,EAEtB,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,cAAc,CAAC,GAAG,CAAC,EAC/B,OAAO,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CAAE,GACjD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACpC,wBAAgB,mBAAmB,CAClC,GAAG,SAAS,eAAe,CAAC,GAAG,CAAC,EAChC,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,EACtB,OAAO,SAAS,OAAO,GAAG,KAAK,EAE/B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,cAAc,CAAC,GAAG,CAAC,EAC/B,GAAG,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,GAC9B,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types of a client bound to a generated spec. Every lookup is an index
|
|
3
|
+
* into the interfaces the generator wrote (`ClientOperations`,
|
|
4
|
+
* `OperationsByRoute`), so a call costs TypeScript the same whether the spec
|
|
5
|
+
* has five operations or five hundred.
|
|
6
|
+
*/
|
|
7
|
+
import type { CallOptions, EventStream, Method, ReconnectOptions, ServerEvent, StandardSchemaV1, Stream, WithResponse } from '@nxgt/httpyz';
|
|
8
|
+
/** What the binding reads of an entry of the generated `ClientOperations`. */
|
|
9
|
+
export interface ClientOperation {
|
|
10
|
+
readonly method: Method;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** What a call takes after the `operationId`: `[input]`, `[input?]` or `[]`. */
|
|
13
|
+
readonly args: readonly unknown[];
|
|
14
|
+
/** Every declared reply, `{ status; type; data }`, decoded. */
|
|
15
|
+
readonly reply: unknown;
|
|
16
|
+
/** The same replies as JSON carries them. */
|
|
17
|
+
readonly wire: unknown;
|
|
18
|
+
/** A reply read an item at a time, when the operation has one. */
|
|
19
|
+
readonly stream?: OperationStream;
|
|
20
|
+
}
|
|
21
|
+
/** An operation's stream: server-sent events, or JSON lines. */
|
|
22
|
+
export interface OperationStream {
|
|
23
|
+
readonly kind: 'sse' | 'jsonl';
|
|
24
|
+
/** Each item, decoded: an event narrowed on `event`, or a line. */
|
|
25
|
+
readonly item: unknown;
|
|
26
|
+
/** Each item as JSON carries it. */
|
|
27
|
+
readonly wire: unknown;
|
|
28
|
+
}
|
|
29
|
+
/** The generated `ClientOperations`: an entry per `operationId`. */
|
|
30
|
+
export type OperationsShape<Ops> = {
|
|
31
|
+
[K in keyof Ops]: ClientOperation;
|
|
32
|
+
};
|
|
33
|
+
/** How a parameter is written: an entry of `parameters` in `operations.ts`. */
|
|
34
|
+
export interface RuntimeParameter {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly in: 'path' | 'query' | 'header';
|
|
37
|
+
readonly required: boolean;
|
|
38
|
+
/** A query list as `?a=1&a=2` (true) or `?a=1,2` (false). */
|
|
39
|
+
readonly explode: boolean;
|
|
40
|
+
readonly list: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface RuntimeMedia {
|
|
43
|
+
readonly kind: 'json' | 'form' | 'text' | 'binary' | 'sse' | 'jsonl';
|
|
44
|
+
readonly schema?: StandardSchemaV1;
|
|
45
|
+
/** `sse`: each event's data, by name: a schema for JSON, `null` for text. */
|
|
46
|
+
readonly events?: {
|
|
47
|
+
readonly [event: string]: StandardSchemaV1 | null;
|
|
48
|
+
};
|
|
49
|
+
/** `jsonl`: each item. */
|
|
50
|
+
readonly item?: StandardSchemaV1;
|
|
51
|
+
}
|
|
52
|
+
/** What the binding reads of an entry of the generated `operations` table. */
|
|
53
|
+
export interface RuntimeOperation {
|
|
54
|
+
readonly method: Method;
|
|
55
|
+
/** As the spec writes it: `/employees/{id}`. */
|
|
56
|
+
readonly path: string;
|
|
57
|
+
readonly parameters: readonly RuntimeParameter[];
|
|
58
|
+
/** The server's validators of each location, which read text: for `validate`. */
|
|
59
|
+
readonly param?: StandardSchemaV1;
|
|
60
|
+
readonly query?: StandardSchemaV1;
|
|
61
|
+
readonly header?: StandardSchemaV1;
|
|
62
|
+
readonly body?: {
|
|
63
|
+
readonly required: boolean;
|
|
64
|
+
readonly content: {
|
|
65
|
+
readonly [mediaType: string]: RuntimeMedia;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
readonly responses: {
|
|
69
|
+
readonly [status: number]: {
|
|
70
|
+
readonly [mediaType: string]: RuntimeMedia;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The generated `operations` table, keyed like `ClientOperations`. Each entry
|
|
76
|
+
* carries its `ClientOperations` entry as `'~client'`, a type that is never
|
|
77
|
+
* set, which is how `createOpenApiClient(http, operations)` infers `Ops`.
|
|
78
|
+
*/
|
|
79
|
+
export type OperationTable<Ops> = {
|
|
80
|
+
readonly [K in keyof Ops]: RuntimeOperation & {
|
|
81
|
+
readonly '~client'?: Ops[K];
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** `OperationsByRoute`, worked out of `ClientOperations`: `'get /items/{id}'` to its `operationId`. */
|
|
85
|
+
export type RoutesOf<Ops extends OperationsShape<Ops>> = {
|
|
86
|
+
[K in keyof Ops as `${Ops[K]['method']} ${Ops[K]['path']}`]: K;
|
|
87
|
+
};
|
|
88
|
+
export interface OpenApiOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Checks with the spec's schemas, throwing a `ValidationError`: the
|
|
91
|
+
* request before it is sent, the reply before it is returned. `true` is
|
|
92
|
+
* both. Default: neither, since the types already hold both to the spec.
|
|
93
|
+
*/
|
|
94
|
+
validate?: boolean | {
|
|
95
|
+
readonly request?: boolean;
|
|
96
|
+
readonly response?: boolean;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The binding's options, and `decode`, which returns each reply as its schema
|
|
101
|
+
* outputs it: with `dates: 'date'`, a date-time as a `Date`. It changes the
|
|
102
|
+
* replies' types: a client whose types are given says so in its third type
|
|
103
|
+
* argument, `createOpenApiClient<ClientOperations, OperationsByRoute, true>`,
|
|
104
|
+
* which in turn requires `decode: true`. Decoding validates the reply.
|
|
105
|
+
*/
|
|
106
|
+
export type OpenApiArgs<Decoded extends boolean> = Decoded extends true ? [options: OpenApiOptions & {
|
|
107
|
+
readonly decode: true;
|
|
108
|
+
}] : [options?: OpenApiOptions & {
|
|
109
|
+
readonly decode?: false;
|
|
110
|
+
}];
|
|
111
|
+
/** What a call takes after its input: the core client's call options. */
|
|
112
|
+
export type OperationInit = Omit<CallOptions, 'operationId'>;
|
|
113
|
+
/** A call's arguments after the `operationId` or the path. */
|
|
114
|
+
export type Args<Ops extends OperationsShape<Ops>, K extends keyof Ops> = [
|
|
115
|
+
...Ops[K]['args'],
|
|
116
|
+
init?: OperationInit
|
|
117
|
+
];
|
|
118
|
+
/** An operation's replies: decoded, or as JSON carries them. */
|
|
119
|
+
export type OperationReply<Ops extends OperationsShape<Ops>, K extends keyof Ops, Decoded extends boolean> = Decoded extends true ? Ops[K]['reply'] : Ops[K]['wire'];
|
|
120
|
+
/** The operations that reply with a stream. Worked out once per client type. */
|
|
121
|
+
export type StreamIds<Ops> = {
|
|
122
|
+
[K in keyof Ops]: Ops[K] extends {
|
|
123
|
+
readonly stream: OperationStream;
|
|
124
|
+
} ? K : never;
|
|
125
|
+
}[keyof Ops] & string;
|
|
126
|
+
/** What a stream takes after its input: the call options, and for events, how to resume. */
|
|
127
|
+
export interface StreamInit extends OperationInit {
|
|
128
|
+
/**
|
|
129
|
+
* Events only: connects again when the connection drops or the stream
|
|
130
|
+
* ends, as `EventSource` does. Default: on, but for POST and PATCH.
|
|
131
|
+
*/
|
|
132
|
+
reconnect?: boolean | ReconnectOptions;
|
|
133
|
+
/** Events only: sent as `Last-Event-ID` on the first connection. */
|
|
134
|
+
lastEventId?: string;
|
|
135
|
+
/** Events only: an event the spec does not declare, which is not yielded. */
|
|
136
|
+
onUnknownEvent?: (event: ServerEvent) => void;
|
|
137
|
+
}
|
|
138
|
+
/** A stream's arguments after the `operationId`. */
|
|
139
|
+
export type StreamArgs<Ops extends OperationsShape<Ops>, K extends keyof Ops> = [...Ops[K]['args'], init?: StreamInit];
|
|
140
|
+
/** What `stream()` returns: events narrowed on `event`, or lines; decoded, or as JSON carries them. */
|
|
141
|
+
export type OperationStreamOf<Ops extends OperationsShape<Ops>, K extends keyof Ops, Decoded extends boolean> = Ops[K] extends {
|
|
142
|
+
readonly stream: {
|
|
143
|
+
readonly kind: infer Kind;
|
|
144
|
+
readonly item: infer Item;
|
|
145
|
+
readonly wire: infer Wire;
|
|
146
|
+
};
|
|
147
|
+
} ? Kind extends 'sse' ? EventStream<Decoded extends true ? Item : Wire> : Stream<Decoded extends true ? Item : Wire> : never;
|
|
148
|
+
/** A bound client whose calls end together. */
|
|
149
|
+
export type OpenApiGroup<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>, Decoded extends boolean = false> = OpenApiClient<Ops, Routes, Decoded> & {
|
|
150
|
+
/**
|
|
151
|
+
* Aborts every call and stream of the group still running, with `reason`,
|
|
152
|
+
* or an `AbortError`. The group goes on: the calls made after it run.
|
|
153
|
+
*/
|
|
154
|
+
cancel(reason?: unknown): void;
|
|
155
|
+
/** Aborts on the next `cancel()`: for work of your own that ends with the group's calls. */
|
|
156
|
+
readonly signal: AbortSignal;
|
|
157
|
+
};
|
|
158
|
+
/** The paths with an operation for method `M`: `'/items/{id}'` from `'get /items/{id}'`. */
|
|
159
|
+
export type PathsOf<Routes, M extends Method> = keyof Routes extends infer Route ? Route extends `${M} ${infer Path}` ? Path : never : never;
|
|
160
|
+
/** The methods the spec has an operation for: the only ones a client offers. */
|
|
161
|
+
export type MethodsOf<Routes> = keyof Routes extends infer Route ? Route extends `${infer M extends Method} ${string}` ? M : never : never;
|
|
162
|
+
/** The `operationId` of the operation at `M P`. */
|
|
163
|
+
export type IdOf<Ops, Routes, M extends Method, P extends string> = Routes[`${M} ${P}` & keyof Routes] & keyof Ops;
|
|
164
|
+
export type OpenApiClient<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>, Decoded extends boolean = false> = {
|
|
165
|
+
/** Calls an operation by its `operationId`. */
|
|
166
|
+
op<K extends keyof Ops & string>(id: K, ...args: Args<Ops, K>): Promise<WithResponse<OperationReply<Ops, K, Decoded>>>;
|
|
167
|
+
/**
|
|
168
|
+
* Reads an operation's stream, an item at a time, by its `operationId`:
|
|
169
|
+
* its events, each narrowed on `event`, or its JSON lines. It connects when
|
|
170
|
+
* read: `for await (const event of api.stream('watchFeed', input))`.
|
|
171
|
+
*/
|
|
172
|
+
stream<K extends StreamIds<Ops>>(id: K, ...args: StreamArgs<Ops, K>): OperationStreamOf<Ops, K, Decoded>;
|
|
173
|
+
/**
|
|
174
|
+
* The same client over the core client's `group()`: its calls and streams
|
|
175
|
+
* also end on `cancel()`, a page's or a component's together.
|
|
176
|
+
*/
|
|
177
|
+
group(): OpenApiGroup<Ops, Routes, Decoded>;
|
|
178
|
+
/**
|
|
179
|
+
* The generated `operations` the client was bound to: for a package built
|
|
180
|
+
* over it, such as `@nxgt/httpyz-query/openapi`, which reads the spec as
|
|
181
|
+
* the client does.
|
|
182
|
+
*/
|
|
183
|
+
readonly operations: OperationTable<Ops>;
|
|
184
|
+
} & PathMethods<Ops, Routes, Decoded>;
|
|
185
|
+
/**
|
|
186
|
+
* A method per HTTP method the spec has an operation for, which calls the
|
|
187
|
+
* operation at a path: `api.get('/employees/{id}', { param: { id } })`. For
|
|
188
|
+
* a package that offers them on an object of its own, as
|
|
189
|
+
* `@nxgt/datasource-rest` does.
|
|
190
|
+
*/
|
|
191
|
+
export type PathMethods<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>, Decoded extends boolean = false> = {
|
|
192
|
+
readonly [M in MethodsOf<Routes>]: <P extends PathsOf<Routes, M>>(path: P, ...args: Args<Ops, IdOf<Ops, Routes, M, P>>) => Promise<WithResponse<OperationReply<Ops, IdOf<Ops, Routes, M, P>, Decoded>>>;
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createOpenApiClient } from './client/create-openapi-client';
|
|
2
|
+
export type { Args, ClientOperation, IdOf, MethodsOf, OpenApiArgs, OpenApiClient, OpenApiGroup, OpenApiOptions, OperationInit, OperationReply, OperationStream, OperationStreamOf, OperationsShape, OperationTable, PathMethods, PathsOf, RoutesOf, RuntimeMedia, RuntimeOperation, RuntimeParameter, StreamArgs, StreamIds, StreamInit, } from './client/types';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,YAAY,EACX,IAAI,EACJ,eAAe,EACf,IAAI,EACJ,SAAS,EACT,WAAW,EACX,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,WAAW,EACX,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,UAAU,GACV,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// src/client/create-openapi-client.ts
|
|
2
|
+
import {
|
|
3
|
+
ValidationError
|
|
4
|
+
} from "@nxgt/httpyz";
|
|
5
|
+
import { METHODS } from "@nxgt/httpyz/integration";
|
|
6
|
+
|
|
7
|
+
// src/request/check-request.ts
|
|
8
|
+
import { absent, check, fields, text } from "@nxgt/httpyz/integration";
|
|
9
|
+
async function checkRequest(operation, input) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const run = async (target, schema, value) => {
|
|
12
|
+
if (!schema)
|
|
13
|
+
return;
|
|
14
|
+
const result = await check(schema, value, target);
|
|
15
|
+
if (!result.ok)
|
|
16
|
+
issues.push(...result.issues);
|
|
17
|
+
};
|
|
18
|
+
const { param, query, header } = readParameters(operation, input);
|
|
19
|
+
await run("param", operation.param, param);
|
|
20
|
+
await run("query", operation.query, query);
|
|
21
|
+
await run("header", operation.header, header);
|
|
22
|
+
const content = Object.values(operation.body?.content ?? {});
|
|
23
|
+
const schemaOf = (kind) => content.find((media) => media.kind === kind)?.schema;
|
|
24
|
+
if (input?.json !== undefined) {
|
|
25
|
+
const sent = JSON.stringify(input.json);
|
|
26
|
+
await run("json", schemaOf("json"), sent === undefined ? undefined : JSON.parse(sent));
|
|
27
|
+
} else if (input?.form !== undefined) {
|
|
28
|
+
await run("form", schemaOf("form"), formFields(input.form));
|
|
29
|
+
} else if (input?.text !== undefined) {
|
|
30
|
+
await run("body", schemaOf("text"), input.text);
|
|
31
|
+
}
|
|
32
|
+
return issues;
|
|
33
|
+
}
|
|
34
|
+
function readParameters(operation, input) {
|
|
35
|
+
const read = { param: {}, query: {}, header: {} };
|
|
36
|
+
for (const parameter of operation.parameters) {
|
|
37
|
+
const { name, list } = parameter;
|
|
38
|
+
if (parameter.in === "path") {
|
|
39
|
+
const value = input?.param?.[name];
|
|
40
|
+
if (!absent(value))
|
|
41
|
+
read.param[name] = text(value);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const value = parameter.in === "query" ? input?.query?.[name] : input?.header?.[name.toLowerCase()];
|
|
45
|
+
if (absent(value))
|
|
46
|
+
continue;
|
|
47
|
+
const values = Array.isArray(value) ? value.map(text) : [text(value)];
|
|
48
|
+
if (parameter.in === "header") {
|
|
49
|
+
const sent = values.join(",").trim();
|
|
50
|
+
read.header[name.toLowerCase()] = list ? sent.split(",").map((item) => item.trim()) : sent;
|
|
51
|
+
} else if (list) {
|
|
52
|
+
read.query[name] = parameter.explode ? values : values.join(",").split(",");
|
|
53
|
+
} else
|
|
54
|
+
read.query[name] = values.join(",");
|
|
55
|
+
}
|
|
56
|
+
return read;
|
|
57
|
+
}
|
|
58
|
+
function formFields(form) {
|
|
59
|
+
const read = {};
|
|
60
|
+
for (const [name, value] of fields(form)) {
|
|
61
|
+
const item = value instanceof Blob && !(value instanceof File) ? new File([value], "blob", { type: value.type }) : value;
|
|
62
|
+
const held = read[name];
|
|
63
|
+
read[name] = held === undefined ? name.endsWith("[]") ? [item] : item : Array.isArray(held) ? [...held, item] : [held, item];
|
|
64
|
+
}
|
|
65
|
+
return read;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/request/to-request.ts
|
|
69
|
+
import {
|
|
70
|
+
absent as absent2,
|
|
71
|
+
text as text2,
|
|
72
|
+
toFormData,
|
|
73
|
+
toSearchParams
|
|
74
|
+
} from "@nxgt/httpyz/integration";
|
|
75
|
+
var concrete = (type, fallback) => type === undefined || type.includes("*") ? fallback : type;
|
|
76
|
+
function toRequest(operation, input, headers) {
|
|
77
|
+
const query = new URLSearchParams;
|
|
78
|
+
for (const param of operation.parameters) {
|
|
79
|
+
if (param.in === "query") {
|
|
80
|
+
appendQuery(query, param, input?.query?.[param.name]);
|
|
81
|
+
} else if (param.in === "header") {
|
|
82
|
+
const value = input?.header?.[param.name.toLowerCase()];
|
|
83
|
+
if (absent2(value))
|
|
84
|
+
continue;
|
|
85
|
+
headers.set(param.name, Array.isArray(value) ? value.map(text2).join(",") : text2(value));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { query, body: toBody(operation, input, headers) };
|
|
89
|
+
}
|
|
90
|
+
function appendQuery(search, param, value) {
|
|
91
|
+
if (absent2(value))
|
|
92
|
+
return;
|
|
93
|
+
if (!Array.isArray(value))
|
|
94
|
+
search.append(param.name, text2(value));
|
|
95
|
+
else if (param.explode) {
|
|
96
|
+
for (const item of value)
|
|
97
|
+
search.append(param.name, text2(item));
|
|
98
|
+
} else
|
|
99
|
+
search.append(param.name, value.map(text2).join(","));
|
|
100
|
+
}
|
|
101
|
+
function toBody(operation, input, headers) {
|
|
102
|
+
if (input === undefined)
|
|
103
|
+
return {};
|
|
104
|
+
const content = Object.entries(operation.body?.content ?? {});
|
|
105
|
+
const declared = (kind) => content.find(([, media]) => media.kind === kind)?.[0];
|
|
106
|
+
if (input.json !== undefined) {
|
|
107
|
+
headers.set("content-type", concrete(declared("json"), "application/json"));
|
|
108
|
+
return { json: input.json };
|
|
109
|
+
}
|
|
110
|
+
if (input.form !== undefined) {
|
|
111
|
+
return {
|
|
112
|
+
form: declared("form") === "application/x-www-form-urlencoded" ? toSearchParams(input.form) : toFormData(input.form)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (input.text !== undefined) {
|
|
116
|
+
headers.set("content-type", concrete(declared("text"), "text/plain"));
|
|
117
|
+
return { text: input.text };
|
|
118
|
+
}
|
|
119
|
+
if (input.body !== undefined) {
|
|
120
|
+
headers.set("content-type", concrete(declared("binary"), "application/octet-stream"));
|
|
121
|
+
return { body: input.body };
|
|
122
|
+
}
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/client/create-openapi-client.ts
|
|
127
|
+
var toResponses = (operation) => Object.fromEntries(Object.entries(operation.responses).map(([status, content]) => [
|
|
128
|
+
status,
|
|
129
|
+
Object.keys(content).length === 0 ? null : Object.fromEntries(Object.entries(content).map(([type, media]) => [
|
|
130
|
+
type,
|
|
131
|
+
media.schema ?? null
|
|
132
|
+
]))
|
|
133
|
+
]));
|
|
134
|
+
function streamOf(operation) {
|
|
135
|
+
for (const [status, content] of Object.entries(operation.responses)) {
|
|
136
|
+
const code = Number(status);
|
|
137
|
+
if (code < 200 || code > 299)
|
|
138
|
+
continue;
|
|
139
|
+
for (const [type, media] of Object.entries(content)) {
|
|
140
|
+
if (media.kind === "sse" || media.kind === "jsonl")
|
|
141
|
+
return [type, media];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
function checkedFirst(open, check) {
|
|
147
|
+
let inner;
|
|
148
|
+
let closed = false;
|
|
149
|
+
return {
|
|
150
|
+
get lastEventId() {
|
|
151
|
+
return inner?.lastEventId;
|
|
152
|
+
},
|
|
153
|
+
close() {
|
|
154
|
+
closed = true;
|
|
155
|
+
inner?.close();
|
|
156
|
+
},
|
|
157
|
+
async* [Symbol.asyncIterator]() {
|
|
158
|
+
if (!inner) {
|
|
159
|
+
await check();
|
|
160
|
+
if (closed)
|
|
161
|
+
return;
|
|
162
|
+
inner = open();
|
|
163
|
+
}
|
|
164
|
+
yield* inner;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function createOpenApiClient(http, operations, given) {
|
|
169
|
+
return bind(http, operations, given ?? {}, () => {
|
|
170
|
+
return;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
var joined = (given, scope) => scope === undefined ? given : given ? AbortSignal.any([given, scope]) : scope;
|
|
174
|
+
function bind(http, operations, options, scope) {
|
|
175
|
+
const { validate = false } = options;
|
|
176
|
+
const decode = options.decode === true;
|
|
177
|
+
const checks = {
|
|
178
|
+
request: validate === true || typeof validate === "object" && validate.request === true,
|
|
179
|
+
response: decode || validate === true || typeof validate === "object" && validate.response === true
|
|
180
|
+
};
|
|
181
|
+
const table = operations;
|
|
182
|
+
const byRoute = new Map;
|
|
183
|
+
const responses = new Map;
|
|
184
|
+
for (const [id, operation] of Object.entries(table)) {
|
|
185
|
+
byRoute.set(`${operation.method} ${operation.path}`, id);
|
|
186
|
+
responses.set(id, toResponses(operation));
|
|
187
|
+
}
|
|
188
|
+
const read = (id, args) => {
|
|
189
|
+
const operation = table[id];
|
|
190
|
+
if (!operation)
|
|
191
|
+
throw new Error(`${id} is not an operationId of the spec`);
|
|
192
|
+
const takes = operation.parameters.length > 0 || Object.keys(operation.body?.content ?? {}).length > 0;
|
|
193
|
+
const input = takes ? args[0] : undefined;
|
|
194
|
+
const given = (takes ? args[1] : args[0]) ?? {};
|
|
195
|
+
const signal = joined(given.signal, scope());
|
|
196
|
+
const init = signal ? { ...given, signal } : given;
|
|
197
|
+
return { operation, input, init };
|
|
198
|
+
};
|
|
199
|
+
const check = async (id, operation, input) => {
|
|
200
|
+
if (!checks.request)
|
|
201
|
+
return;
|
|
202
|
+
const issues = await checkRequest(operation, input);
|
|
203
|
+
if (issues.length === 0)
|
|
204
|
+
return;
|
|
205
|
+
const context = {
|
|
206
|
+
operationId: id,
|
|
207
|
+
method: operation.method,
|
|
208
|
+
path: operation.path
|
|
209
|
+
};
|
|
210
|
+
throw new ValidationError(context, {
|
|
211
|
+
kind: "request",
|
|
212
|
+
...context,
|
|
213
|
+
issues
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
const written = (id, operation, input, init) => {
|
|
217
|
+
const headers = new Headers(init.headers);
|
|
218
|
+
const { query, body } = toRequest(operation, input, headers);
|
|
219
|
+
return {
|
|
220
|
+
...init,
|
|
221
|
+
...body,
|
|
222
|
+
param: input?.param,
|
|
223
|
+
query,
|
|
224
|
+
headers,
|
|
225
|
+
operationId: id
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
const call = async (id, args) => {
|
|
229
|
+
const { operation, input, init } = read(id, args);
|
|
230
|
+
await check(id, operation, input);
|
|
231
|
+
const request = http.request;
|
|
232
|
+
return request(operation.method, operation.path, {
|
|
233
|
+
...written(id, operation, input, init),
|
|
234
|
+
responses: responses.get(id),
|
|
235
|
+
validate: checks.response,
|
|
236
|
+
decode
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
const stream = (id, args) => {
|
|
240
|
+
const { operation, input, init } = read(id, args);
|
|
241
|
+
const found = streamOf(operation);
|
|
242
|
+
if (!found)
|
|
243
|
+
throw new Error(`${id} does not reply with a stream`);
|
|
244
|
+
const [type, media] = found;
|
|
245
|
+
const { reconnect, lastEventId, onUnknownEvent, ...rest } = init;
|
|
246
|
+
const open = () => {
|
|
247
|
+
const request = written(id, operation, input, rest);
|
|
248
|
+
if (!request.headers.has("accept"))
|
|
249
|
+
request.headers.set("accept", type);
|
|
250
|
+
const common = {
|
|
251
|
+
...request,
|
|
252
|
+
method: operation.method,
|
|
253
|
+
validate: checks.response,
|
|
254
|
+
decode
|
|
255
|
+
};
|
|
256
|
+
const openStream = media.kind === "sse" ? http.events : http.lines;
|
|
257
|
+
return openStream(operation.path, media.kind === "sse" ? {
|
|
258
|
+
...common,
|
|
259
|
+
events: media.events,
|
|
260
|
+
reconnect,
|
|
261
|
+
lastEventId,
|
|
262
|
+
onUnknownEvent
|
|
263
|
+
} : { ...common, item: media.item });
|
|
264
|
+
};
|
|
265
|
+
return checks.request ? checkedFirst(open, () => check(id, operation, input)) : open();
|
|
266
|
+
};
|
|
267
|
+
const client = {
|
|
268
|
+
operations,
|
|
269
|
+
op: (id, ...args) => call(id, args),
|
|
270
|
+
stream: (id, ...args) => stream(id, args),
|
|
271
|
+
group: () => {
|
|
272
|
+
const group = http.group();
|
|
273
|
+
const bound = bind(group, operations, options, () => group.signal);
|
|
274
|
+
return Object.defineProperties(bound, {
|
|
275
|
+
cancel: { enumerable: true, value: group.cancel },
|
|
276
|
+
signal: { enumerable: true, get: () => group.signal }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
for (const method of METHODS) {
|
|
281
|
+
client[method] = (path, ...args) => {
|
|
282
|
+
const id = byRoute.get(`${method} ${path}`);
|
|
283
|
+
if (id === undefined) {
|
|
284
|
+
return Promise.reject(new Error(`The spec has no ${method.toUpperCase()} ${path} operation`));
|
|
285
|
+
}
|
|
286
|
+
return call(id, args);
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return client;
|
|
290
|
+
}
|
|
291
|
+
export {
|
|
292
|
+
createOpenApiClient
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
//# debugId=D3AB5DC616E1E63964756E2164756E21
|
|
296
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/client/create-openapi-client.ts", "../src/request/check-request.ts", "../src/request/to-request.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `createOpenApiClient`: the operations `@nxgt/openapi-codegen` generates,\n * bound onto a client of `createHttpClient`. The client sends; the binding\n * adds what the spec knows: how each parameter and body is written, the\n * replies each operation declares, and checks by the server's own schemas.\n */\nimport {\n\ttype EventStream,\n\ttype HttpClient,\n\ttype Responses,\n\tValidationError,\n} from '@nxgt/httpyz';\nimport { METHODS } from '@nxgt/httpyz/integration';\nimport { checkRequest } from '../request/check-request';\nimport { type Input, toRequest } from '../request/to-request';\nimport type {\n\tOpenApiArgs,\n\tOpenApiClient,\n\tOpenApiOptions,\n\tOperationsShape,\n\tOperationTable,\n\tRoutesOf,\n\tRuntimeMedia,\n\tRuntimeOperation,\n\tStreamInit,\n} from './types';\n\n/** An operation's replies as the core client declares them. */\nconst toResponses = (operation: RuntimeOperation): Responses =>\n\tObject.fromEntries(\n\t\tObject.entries(operation.responses).map(([status, content]) => [\n\t\t\tstatus,\n\t\t\tObject.keys(content).length === 0\n\t\t\t\t? null\n\t\t\t\t: Object.fromEntries(\n\t\t\t\t\t\tObject.entries(content).map(([type, media]) => [\n\t\t\t\t\t\t\ttype,\n\t\t\t\t\t\t\tmedia.schema ?? null,\n\t\t\t\t\t\t]),\n\t\t\t\t\t),\n\t\t]),\n\t);\n\n/** The stream an operation replies with, and its media type: the first of its 2xx replies. */\nfunction streamOf(\n\toperation: RuntimeOperation,\n): [type: string, media: RuntimeMedia] | undefined {\n\tfor (const [status, content] of Object.entries(operation.responses)) {\n\t\tconst code = Number(status);\n\t\tif (code < 200 || code > 299) continue;\n\t\tfor (const [type, media] of Object.entries(content)) {\n\t\t\tif (media.kind === 'sse' || media.kind === 'jsonl') return [type, media];\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * `open()`'s stream, opened once `check` passes on the first read: a request\n * the spec refuses is never sent, and throws where the stream is read.\n */\nfunction checkedFirst<T>(\n\topen: () => EventStream<T>,\n\tcheck: () => Promise<void>,\n): EventStream<T> {\n\tlet inner: EventStream<T> | undefined;\n\tlet closed = false;\n\treturn {\n\t\tget lastEventId() {\n\t\t\treturn inner?.lastEventId;\n\t\t},\n\t\tclose() {\n\t\t\tclosed = true;\n\t\t\tinner?.close();\n\t\t},\n\t\tasync *[Symbol.asyncIterator]() {\n\t\t\tif (!inner) {\n\t\t\t\tawait check();\n\t\t\t\tif (closed) return;\n\t\t\t\tinner = open();\n\t\t\t}\n\t\t\tyield* inner;\n\t\t},\n\t};\n}\n\n/**\n * A client for a spec, bound to the `operations` table of the generated\n * `operations.ts`, which carries the spec's types: nothing else to import.\n *\n * ```ts\n * const http = createHttpClient({ baseUrl: 'https://api.example.com' });\n * const api = createOpenApiClient(http, operations);\n * const reply = await api.get('/employees/{id}', { param: { id } });\n * if (reply.status === 200) reply.data.name;\n * ```\n *\n * With `decode: true`, each reply is returned as its schema outputs it, and\n * typed so. The types may also be given: `createOpenApiClient<ClientOperations,\n * OperationsByRoute, true>`, whose `true` then requires `decode: true`.\n */\nexport function createOpenApiClient<\n\tOps extends OperationsShape<Ops>,\n\tRoutes = RoutesOf<Ops>,\n>(\n\thttp: HttpClient,\n\toperations: OperationTable<Ops>,\n\toptions: OpenApiOptions & { readonly decode: true },\n): OpenApiClient<Ops, Routes, true>;\nexport function createOpenApiClient<\n\tOps extends OperationsShape<Ops>,\n\tRoutes = RoutesOf<Ops>,\n\tDecoded extends boolean = false,\n>(\n\thttp: HttpClient,\n\toperations: OperationTable<Ops>,\n\t...[given]: OpenApiArgs<Decoded>\n): OpenApiClient<Ops, Routes, Decoded>;\nexport function createOpenApiClient<\n\tOps extends OperationsShape<Ops>,\n\tRoutes,\n\tDecoded extends boolean,\n>(\n\thttp: HttpClient,\n\toperations: OperationTable<Ops>,\n\tgiven?: OpenApiOptions & { readonly decode?: boolean },\n): OpenApiClient<Ops, Routes, Decoded> {\n\treturn bind(http, operations, given ?? {}, () => undefined);\n}\n\n/** Both signals: a call ends on either. */\nconst joined = (\n\tgiven: AbortSignal | null | undefined,\n\tscope: AbortSignal | undefined,\n): AbortSignal | null | undefined =>\n\tscope === undefined ? given : given ? AbortSignal.any([given, scope]) : scope;\n\n/**\n * The client, whose calls also end on `scope()`'s signal, read as each call\n * is made: a call checked before it is sent reaches the core client later,\n * after a `cancel()` of its group may already have swapped the signal.\n */\nfunction bind<\n\tOps extends OperationsShape<Ops>,\n\tRoutes,\n\tDecoded extends boolean,\n>(\n\thttp: HttpClient,\n\toperations: OperationTable<Ops>,\n\toptions: OpenApiOptions & { readonly decode?: boolean },\n\tscope: () => AbortSignal | undefined,\n): OpenApiClient<Ops, Routes, Decoded> {\n\tconst { validate = false } = options;\n\tconst decode = options.decode === true;\n\tconst checks = {\n\t\trequest:\n\t\t\tvalidate === true ||\n\t\t\t(typeof validate === 'object' && validate.request === true),\n\t\tresponse:\n\t\t\tdecode ||\n\t\t\tvalidate === true ||\n\t\t\t(typeof validate === 'object' && validate.response === true),\n\t};\n\tconst table = operations as unknown as {\n\t\treadonly [id: string]: RuntimeOperation;\n\t};\n\tconst byRoute = new Map<string, string>();\n\tconst responses = new Map<string, Responses>();\n\tfor (const [id, operation] of Object.entries(table)) {\n\t\tbyRoute.set(`${operation.method} ${operation.path}`, id);\n\t\tresponses.set(id, toResponses(operation));\n\t}\n\n\t/** The operation, and its arguments: the input, then the init. */\n\tconst read = (id: string, args: readonly unknown[]) => {\n\t\tconst operation = table[id];\n\t\tif (!operation) throw new Error(`${id} is not an operationId of the spec`);\n\t\t// An operation that takes nothing has no input: its first argument is the init.\n\t\tconst takes =\n\t\t\toperation.parameters.length > 0 ||\n\t\t\tObject.keys(operation.body?.content ?? {}).length > 0;\n\t\tconst input = (takes ? args[0] : undefined) as Input | undefined;\n\t\tconst given = ((takes ? args[1] : args[0]) ?? {}) as StreamInit;\n\t\tconst signal = joined(given.signal, scope());\n\t\tconst init = signal ? { ...given, signal } : given;\n\t\treturn { operation, input, init };\n\t};\n\n\t/** Throws what the server would refuse, before anything is sent. */\n\tconst check = async (\n\t\tid: string,\n\t\toperation: RuntimeOperation,\n\t\tinput: Input | undefined,\n\t): Promise<void> => {\n\t\tif (!checks.request) return;\n\t\tconst issues = await checkRequest(operation, input);\n\t\tif (issues.length === 0) return;\n\t\tconst context = {\n\t\t\toperationId: id,\n\t\t\tmethod: operation.method,\n\t\t\tpath: operation.path,\n\t\t};\n\t\tthrow new ValidationError(context, {\n\t\t\tkind: 'request',\n\t\t\t...context,\n\t\t\tissues,\n\t\t});\n\t};\n\n\t/** The core client's options for the input, written as the server reads it. */\n\tconst written = (\n\t\tid: string,\n\t\toperation: RuntimeOperation,\n\t\tinput: Input | undefined,\n\t\tinit: object & { headers?: HeadersInit },\n\t) => {\n\t\tconst headers = new Headers(init.headers);\n\t\tconst { query, body } = toRequest(operation, input, headers);\n\t\treturn {\n\t\t\t...init,\n\t\t\t...body,\n\t\t\tparam: input?.param,\n\t\t\tquery,\n\t\t\theaders,\n\t\t\toperationId: id,\n\t\t};\n\t};\n\n\tconst call = async (\n\t\tid: string,\n\t\targs: readonly unknown[],\n\t): Promise<unknown> => {\n\t\tconst { operation, input, init } = read(id, args);\n\t\tawait check(id, operation, input);\n\t\tconst request = http.request as (\n\t\t\tmethod: string,\n\t\t\tpath: string,\n\t\t\toptions: object,\n\t\t) => Promise<unknown>;\n\t\treturn request(operation.method, operation.path, {\n\t\t\t...written(id, operation, input, init),\n\t\t\tresponses: responses.get(id),\n\t\t\tvalidate: checks.response,\n\t\t\tdecode,\n\t\t});\n\t};\n\n\tconst stream = (id: string, args: readonly unknown[]) => {\n\t\tconst { operation, input, init } = read(id, args);\n\t\tconst found = streamOf(operation);\n\t\tif (!found) throw new Error(`${id} does not reply with a stream`);\n\t\tconst [type, media] = found;\n\t\tconst { reconnect, lastEventId, onUnknownEvent, ...rest } = init;\n\t\tconst open = () => {\n\t\t\tconst request = written(id, operation, input, rest);\n\t\t\t// The media type the spec declares, which may not be the core client's default.\n\t\t\tif (!request.headers.has('accept')) request.headers.set('accept', type);\n\t\t\tconst common = {\n\t\t\t\t...request,\n\t\t\t\tmethod: operation.method,\n\t\t\t\tvalidate: checks.response,\n\t\t\t\tdecode,\n\t\t\t};\n\t\t\tconst openStream = (media.kind === 'sse' ? http.events : http.lines) as (\n\t\t\t\tpath: string,\n\t\t\t\toptions: object,\n\t\t\t) => EventStream<unknown>;\n\t\t\treturn openStream(\n\t\t\t\toperation.path,\n\t\t\t\tmedia.kind === 'sse'\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t...common,\n\t\t\t\t\t\t\tevents: media.events,\n\t\t\t\t\t\t\treconnect,\n\t\t\t\t\t\t\tlastEventId,\n\t\t\t\t\t\t\tonUnknownEvent,\n\t\t\t\t\t\t}\n\t\t\t\t\t: { ...common, item: media.item },\n\t\t\t);\n\t\t};\n\t\treturn checks.request\n\t\t\t? checkedFirst(open, () => check(id, operation, input))\n\t\t\t: open();\n\t};\n\n\tconst client: Record<string, unknown> = {\n\t\toperations,\n\t\top: (id: string, ...args: unknown[]) => call(id, args),\n\t\tstream: (id: string, ...args: unknown[]) => stream(id, args),\n\t\tgroup: () => {\n\t\t\tconst group = http.group();\n\t\t\tconst bound = bind<Ops, Routes, Decoded>(\n\t\t\t\tgroup,\n\t\t\t\toperations,\n\t\t\t\toptions,\n\t\t\t\t() => group.signal,\n\t\t\t);\n\t\t\treturn Object.defineProperties(bound, {\n\t\t\t\tcancel: { enumerable: true, value: group.cancel },\n\t\t\t\tsignal: { enumerable: true, get: () => group.signal },\n\t\t\t});\n\t\t},\n\t};\n\tfor (const method of METHODS) {\n\t\tclient[method] = (path: string, ...args: unknown[]) => {\n\t\t\tconst id = byRoute.get(`${method} ${path}`);\n\t\t\tif (id === undefined) {\n\t\t\t\treturn Promise.reject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`The spec has no ${method.toUpperCase()} ${path} operation`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn call(id, args);\n\t\t};\n\t}\n\treturn client as unknown as OpenApiClient<Ops, Routes, Decoded>;\n}\n",
|
|
6
|
+
"/**\n * A request checked before it is sent, by the validators the server runs, on\n * what `@nxgt/openapi-hono` will read from it: each parameter as the\n * text it travels as, the JSON as it parses, a form as its fields. What the\n * server would refuse is refused here with the same issues, and nothing is\n * sent.\n */\nimport type { StandardSchemaV1, ValidationIssue } from '@nxgt/httpyz';\nimport { absent, check, fields, text } from '@nxgt/httpyz/integration';\nimport type { RuntimeMedia, RuntimeOperation } from '../client/types';\nimport type { Input } from './to-request';\n\nexport async function checkRequest(\n\toperation: RuntimeOperation,\n\tinput: Input | undefined,\n): Promise<ValidationIssue[]> {\n\tconst issues: ValidationIssue[] = [];\n\tconst run = async (\n\t\ttarget: ValidationIssue['target'],\n\t\tschema: StandardSchemaV1 | undefined,\n\t\tvalue: unknown,\n\t): Promise<void> => {\n\t\tif (!schema) return;\n\t\tconst result = await check(schema, value, target);\n\t\tif (!result.ok) issues.push(...result.issues);\n\t};\n\tconst { param, query, header } = readParameters(operation, input);\n\t// In the server's order, so the issues come out in the same one.\n\tawait run('param', operation.param, param);\n\tawait run('query', operation.query, query);\n\tawait run('header', operation.header, header);\n\tconst content = Object.values(operation.body?.content ?? {});\n\tconst schemaOf = (kind: RuntimeMedia['kind']) =>\n\t\tcontent.find((media) => media.kind === kind)?.schema;\n\tif (input?.json !== undefined) {\n\t\tconst sent = JSON.stringify(input.json);\n\t\tawait run(\n\t\t\t'json',\n\t\t\tschemaOf('json'),\n\t\t\tsent === undefined ? undefined : JSON.parse(sent),\n\t\t);\n\t} else if (input?.form !== undefined) {\n\t\tawait run('form', schemaOf('form'), formFields(input.form));\n\t} else if (input?.text !== undefined) {\n\t\tawait run('body', schemaOf('text'), input.text);\n\t}\n\treturn issues;\n}\n\n/** The parameters as the engine reads them: strings, a list as every value. */\nfunction readParameters(\n\toperation: RuntimeOperation,\n\tinput: Input | undefined,\n): Record<'param' | 'query' | 'header', Record<string, unknown>> {\n\tconst read = { param: {}, query: {}, header: {} } as Record<\n\t\t'param' | 'query' | 'header',\n\t\tRecord<string, unknown>\n\t>;\n\tfor (const parameter of operation.parameters) {\n\t\tconst { name, list } = parameter;\n\t\tif (parameter.in === 'path') {\n\t\t\tconst value = input?.param?.[name];\n\t\t\tif (!absent(value)) read.param[name] = text(value);\n\t\t\tcontinue;\n\t\t}\n\t\tconst value =\n\t\t\tparameter.in === 'query'\n\t\t\t\t? input?.query?.[name]\n\t\t\t\t: input?.header?.[name.toLowerCase()];\n\t\tif (absent(value)) continue;\n\t\tconst values = Array.isArray(value) ? value.map(text) : [text(value)];\n\t\tif (parameter.in === 'header') {\n\t\t\t// Headers trims what it holds.\n\t\t\tconst sent = values.join(',').trim();\n\t\t\tread.header[name.toLowerCase()] = list\n\t\t\t\t? sent.split(',').map((item) => item.trim())\n\t\t\t\t: sent;\n\t\t} else if (list) {\n\t\t\tread.query[name] = parameter.explode\n\t\t\t\t? values\n\t\t\t\t: values.join(',').split(',');\n\t\t} else read.query[name] = values.join(',');\n\t}\n\treturn read;\n}\n\n/**\n * A form as Hono's `parseBody({ all: true })` reads it: a field sent once is\n * its value, one sent again or named `x[]` a list, and a `Blob` a `File`.\n */\nfunction formFields(\n\tform: Readonly<Record<string, unknown>>,\n): Record<string, unknown> {\n\tconst read: Record<string, unknown> = {};\n\tfor (const [name, value] of fields(form)) {\n\t\tconst item =\n\t\t\tvalue instanceof Blob && !(value instanceof File)\n\t\t\t\t? new File([value], 'blob', { type: value.type })\n\t\t\t\t: value;\n\t\tconst held = read[name];\n\t\tread[name] =\n\t\t\theld === undefined\n\t\t\t\t? name.endsWith('[]')\n\t\t\t\t\t? [item]\n\t\t\t\t\t: item\n\t\t\t\t: Array.isArray(held)\n\t\t\t\t\t? [...held, item]\n\t\t\t\t\t: [held, item];\n\t}\n\treturn read;\n}\n",
|
|
7
|
+
"/**\n * An operation's input as a call of the core client, written the way\n * `@nxgt/openapi-hono` reads it back: a query list as a repeated key,\n * or joined with commas when the spec says `explode: false`; a header list\n * joined with commas; the body as the media type the spec declares for its\n * kind.\n */\nimport type { BodyInput } from '@nxgt/httpyz';\nimport {\n\tabsent,\n\ttext,\n\ttoFormData,\n\ttoSearchParams,\n} from '@nxgt/httpyz/integration';\nimport type {\n\tRuntimeMedia,\n\tRuntimeOperation,\n\tRuntimeParameter,\n} from '../client/types';\n\n/** What an input may hold, as `ClientOperations` types it. */\nexport interface Input {\n\treadonly param?: Readonly<Record<string, unknown>>;\n\treadonly query?: Readonly<Record<string, unknown>>;\n\t/** Keyed by lowercased name. */\n\treadonly header?: Readonly<Record<string, unknown>>;\n\treadonly json?: unknown;\n\treadonly form?: Readonly<Record<string, unknown>>;\n\treadonly text?: string;\n\treadonly body?: Blob | ArrayBuffer | Uint8Array;\n}\n\n/** What to send as `Content-Type` for a declared media type, which may be a range. */\nconst concrete = (type: string | undefined, fallback: string): string =>\n\ttype === undefined || type.includes('*') ? fallback : type;\n\n/** The query and the body; the header parameters and the body's type go onto `headers`. */\nexport function toRequest(\n\toperation: RuntimeOperation,\n\tinput: Input | undefined,\n\theaders: Headers,\n): { query: URLSearchParams; body: BodyInput } {\n\tconst query = new URLSearchParams();\n\tfor (const param of operation.parameters) {\n\t\tif (param.in === 'query') {\n\t\t\tappendQuery(query, param, input?.query?.[param.name]);\n\t\t} else if (param.in === 'header') {\n\t\t\tconst value = input?.header?.[param.name.toLowerCase()];\n\t\t\tif (absent(value)) continue;\n\t\t\theaders.set(\n\t\t\t\tparam.name,\n\t\t\t\tArray.isArray(value) ? value.map(text).join(',') : text(value),\n\t\t\t);\n\t\t}\n\t}\n\treturn { query, body: toBody(operation, input, headers) };\n}\n\nfunction appendQuery(\n\tsearch: URLSearchParams,\n\tparam: RuntimeParameter,\n\tvalue: unknown,\n): void {\n\tif (absent(value)) return;\n\tif (!Array.isArray(value)) search.append(param.name, text(value));\n\telse if (param.explode) {\n\t\tfor (const item of value) search.append(param.name, text(item));\n\t} else search.append(param.name, value.map(text).join(','));\n}\n\nfunction toBody(\n\toperation: RuntimeOperation,\n\tinput: Input | undefined,\n\theaders: Headers,\n): BodyInput {\n\tif (input === undefined) return {};\n\tconst content = Object.entries(operation.body?.content ?? {});\n\tconst declared = (kind: RuntimeMedia['kind']): string | undefined =>\n\t\tcontent.find(([, media]) => media.kind === kind)?.[0];\n\tif (input.json !== undefined) {\n\t\theaders.set('content-type', concrete(declared('json'), 'application/json'));\n\t\treturn { json: input.json };\n\t}\n\tif (input.form !== undefined) {\n\t\treturn {\n\t\t\tform:\n\t\t\t\tdeclared('form') === 'application/x-www-form-urlencoded'\n\t\t\t\t\t? toSearchParams(input.form)\n\t\t\t\t\t: toFormData(input.form),\n\t\t};\n\t}\n\tif (input.text !== undefined) {\n\t\theaders.set('content-type', concrete(declared('text'), 'text/plain'));\n\t\treturn { text: input.text };\n\t}\n\tif (input.body !== undefined) {\n\t\theaders.set(\n\t\t\t'content-type',\n\t\t\tconcrete(declared('binary'), 'application/octet-stream'),\n\t\t);\n\t\treturn { body: input.body };\n\t}\n\treturn {};\n}\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": ";AAMA;AAAA;AAAA;AAMA;;;ACJA;AAIA,eAAsB,YAAY,CACjC,WACA,OAC6B;AAAA,EAC7B,MAAM,SAA4B,CAAC;AAAA,EACnC,MAAM,MAAM,OACX,QACA,QACA,UACmB;AAAA,IACnB,IAAI,CAAC;AAAA,MAAQ;AAAA,IACb,MAAM,SAAS,MAAM,MAAM,QAAQ,OAAO,MAAM;AAAA,IAChD,IAAI,CAAC,OAAO;AAAA,MAAI,OAAO,KAAK,GAAG,OAAO,MAAM;AAAA;AAAA,EAE7C,QAAQ,OAAO,OAAO,WAAW,eAAe,WAAW,KAAK;AAAA,EAEhE,MAAM,IAAI,SAAS,UAAU,OAAO,KAAK;AAAA,EACzC,MAAM,IAAI,SAAS,UAAU,OAAO,KAAK;AAAA,EACzC,MAAM,IAAI,UAAU,UAAU,QAAQ,MAAM;AAAA,EAC5C,MAAM,UAAU,OAAO,OAAO,UAAU,MAAM,WAAW,CAAC,CAAC;AAAA,EAC3D,MAAM,WAAW,CAAC,SACjB,QAAQ,KAAK,CAAC,UAAU,MAAM,SAAS,IAAI,GAAG;AAAA,EAC/C,IAAI,OAAO,SAAS,WAAW;AAAA,IAC9B,MAAM,OAAO,KAAK,UAAU,MAAM,IAAI;AAAA,IACtC,MAAM,IACL,QACA,SAAS,MAAM,GACf,SAAS,YAAY,YAAY,KAAK,MAAM,IAAI,CACjD;AAAA,EACD,EAAO,SAAI,OAAO,SAAS,WAAW;AAAA,IACrC,MAAM,IAAI,QAAQ,SAAS,MAAM,GAAG,WAAW,MAAM,IAAI,CAAC;AAAA,EAC3D,EAAO,SAAI,OAAO,SAAS,WAAW;AAAA,IACrC,MAAM,IAAI,QAAQ,SAAS,MAAM,GAAG,MAAM,IAAI;AAAA,EAC/C;AAAA,EACA,OAAO;AAAA;AAIR,SAAS,cAAc,CACtB,WACA,OACgE;AAAA,EAChE,MAAM,OAAO,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,EAIhD,WAAW,aAAa,UAAU,YAAY;AAAA,IAC7C,QAAQ,MAAM,SAAS;AAAA,IACvB,IAAI,UAAU,OAAO,QAAQ;AAAA,MAC5B,MAAM,QAAQ,OAAO,QAAQ;AAAA,MAC7B,IAAI,CAAC,OAAO,KAAK;AAAA,QAAG,KAAK,MAAM,QAAQ,KAAK,KAAK;AAAA,MACjD;AAAA,IACD;AAAA,IACA,MAAM,QACL,UAAU,OAAO,UACd,OAAO,QAAQ,QACf,OAAO,SAAS,KAAK,YAAY;AAAA,IACrC,IAAI,OAAO,KAAK;AAAA,MAAG;AAAA,IACnB,MAAM,SAAS,MAAM,QAAQ,KAAK,IAAI,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC;AAAA,IACpE,IAAI,UAAU,OAAO,UAAU;AAAA,MAE9B,MAAM,OAAO,OAAO,KAAK,GAAG,EAAE,KAAK;AAAA,MACnC,KAAK,OAAO,KAAK,YAAY,KAAK,OAC/B,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,IACzC;AAAA,IACJ,EAAO,SAAI,MAAM;AAAA,MAChB,KAAK,MAAM,QAAQ,UAAU,UAC1B,SACA,OAAO,KAAK,GAAG,EAAE,MAAM,GAAG;AAAA,IAC9B,EAAO;AAAA,WAAK,MAAM,QAAQ,OAAO,KAAK,GAAG;AAAA,EAC1C;AAAA,EACA,OAAO;AAAA;AAOR,SAAS,UAAU,CAClB,MAC0B;AAAA,EAC1B,MAAM,OAAgC,CAAC;AAAA,EACvC,YAAY,MAAM,UAAU,OAAO,IAAI,GAAG;AAAA,IACzC,MAAM,OACL,iBAAiB,QAAQ,EAAE,iBAAiB,QACzC,IAAI,KAAK,CAAC,KAAK,GAAG,QAAQ,EAAE,MAAM,MAAM,KAAK,CAAC,IAC9C;AAAA,IACJ,MAAM,OAAO,KAAK;AAAA,IAClB,KAAK,QACJ,SAAS,YACN,KAAK,SAAS,IAAI,IACjB,CAAC,IAAI,IACL,OACD,MAAM,QAAQ,IAAI,IACjB,CAAC,GAAG,MAAM,IAAI,IACd,CAAC,MAAM,IAAI;AAAA,EACjB;AAAA,EACA,OAAO;AAAA;;;ACrGR;AAAA,YACC;AAAA,UACA;AAAA;AAAA;AAAA;AAuBD,IAAM,WAAW,CAAC,MAA0B,aAC3C,SAAS,aAAa,KAAK,SAAS,GAAG,IAAI,WAAW;AAGhD,SAAS,SAAS,CACxB,WACA,OACA,SAC8C;AAAA,EAC9C,MAAM,QAAQ,IAAI;AAAA,EAClB,WAAW,SAAS,UAAU,YAAY;AAAA,IACzC,IAAI,MAAM,OAAO,SAAS;AAAA,MACzB,YAAY,OAAO,OAAO,OAAO,QAAQ,MAAM,KAAK;AAAA,IACrD,EAAO,SAAI,MAAM,OAAO,UAAU;AAAA,MACjC,MAAM,QAAQ,OAAO,SAAS,MAAM,KAAK,YAAY;AAAA,MACrD,IAAI,QAAO,KAAK;AAAA,QAAG;AAAA,MACnB,QAAQ,IACP,MAAM,MACN,MAAM,QAAQ,KAAK,IAAI,MAAM,IAAI,KAAI,EAAE,KAAK,GAAG,IAAI,MAAK,KAAK,CAC9D;AAAA,IACD;AAAA,EACD;AAAA,EACA,OAAO,EAAE,OAAO,MAAM,OAAO,WAAW,OAAO,OAAO,EAAE;AAAA;AAGzD,SAAS,WAAW,CACnB,QACA,OACA,OACO;AAAA,EACP,IAAI,QAAO,KAAK;AAAA,IAAG;AAAA,EACnB,IAAI,CAAC,MAAM,QAAQ,KAAK;AAAA,IAAG,OAAO,OAAO,MAAM,MAAM,MAAK,KAAK,CAAC;AAAA,EAC3D,SAAI,MAAM,SAAS;AAAA,IACvB,WAAW,QAAQ;AAAA,MAAO,OAAO,OAAO,MAAM,MAAM,MAAK,IAAI,CAAC;AAAA,EAC/D,EAAO;AAAA,WAAO,OAAO,MAAM,MAAM,MAAM,IAAI,KAAI,EAAE,KAAK,GAAG,CAAC;AAAA;AAG3D,SAAS,MAAM,CACd,WACA,OACA,SACY;AAAA,EACZ,IAAI,UAAU;AAAA,IAAW,OAAO,CAAC;AAAA,EACjC,MAAM,UAAU,OAAO,QAAQ,UAAU,MAAM,WAAW,CAAC,CAAC;AAAA,EAC5D,MAAM,WAAW,CAAC,SACjB,QAAQ,KAAK,IAAI,WAAW,MAAM,SAAS,IAAI,IAAI;AAAA,EACpD,IAAI,MAAM,SAAS,WAAW;AAAA,IAC7B,QAAQ,IAAI,gBAAgB,SAAS,SAAS,MAAM,GAAG,kBAAkB,CAAC;AAAA,IAC1E,OAAO,EAAE,MAAM,MAAM,KAAK;AAAA,EAC3B;AAAA,EACA,IAAI,MAAM,SAAS,WAAW;AAAA,IAC7B,OAAO;AAAA,MACN,MACC,SAAS,MAAM,MAAM,sCAClB,eAAe,MAAM,IAAI,IACzB,WAAW,MAAM,IAAI;AAAA,IAC1B;AAAA,EACD;AAAA,EACA,IAAI,MAAM,SAAS,WAAW;AAAA,IAC7B,QAAQ,IAAI,gBAAgB,SAAS,SAAS,MAAM,GAAG,YAAY,CAAC;AAAA,IACpE,OAAO,EAAE,MAAM,MAAM,KAAK;AAAA,EAC3B;AAAA,EACA,IAAI,MAAM,SAAS,WAAW;AAAA,IAC7B,QAAQ,IACP,gBACA,SAAS,SAAS,QAAQ,GAAG,0BAA0B,CACxD;AAAA,IACA,OAAO,EAAE,MAAM,MAAM,KAAK;AAAA,EAC3B;AAAA,EACA,OAAO,CAAC;AAAA;;;AF1ET,IAAM,cAAc,CAAC,cACpB,OAAO,YACN,OAAO,QAAQ,UAAU,SAAS,EAAE,IAAI,EAAE,QAAQ,aAAa;AAAA,EAC9D;AAAA,EACA,OAAO,KAAK,OAAO,EAAE,WAAW,IAC7B,OACA,OAAO,YACP,OAAO,QAAQ,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW;AAAA,IAC9C;AAAA,IACA,MAAM,UAAU;AAAA,EACjB,CAAC,CACF;AACH,CAAC,CACF;AAGD,SAAS,QAAQ,CAChB,WACkD;AAAA,EAClD,YAAY,QAAQ,YAAY,OAAO,QAAQ,UAAU,SAAS,GAAG;AAAA,IACpE,MAAM,OAAO,OAAO,MAAM;AAAA,IAC1B,IAAI,OAAO,OAAO,OAAO;AAAA,MAAK;AAAA,IAC9B,YAAY,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG;AAAA,MACpD,IAAI,MAAM,SAAS,SAAS,MAAM,SAAS;AAAA,QAAS,OAAO,CAAC,MAAM,KAAK;AAAA,IACxE;AAAA,EACD;AAAA,EACA;AAAA;AAOD,SAAS,YAAe,CACvB,MACA,OACiB;AAAA,EACjB,IAAI;AAAA,EACJ,IAAI,SAAS;AAAA,EACb,OAAO;AAAA,QACF,WAAW,GAAG;AAAA,MACjB,OAAO,OAAO;AAAA;AAAA,IAEf,KAAK,GAAG;AAAA,MACP,SAAS;AAAA,MACT,OAAO,MAAM;AAAA;AAAA,YAEN,OAAO,cAAc,GAAG;AAAA,MAC/B,IAAI,CAAC,OAAO;AAAA,QACX,MAAM,MAAM;AAAA,QACZ,IAAI;AAAA,UAAQ;AAAA,QACZ,QAAQ,KAAK;AAAA,MACd;AAAA,MACA,OAAO;AAAA;AAAA,EAET;AAAA;AAmCM,SAAS,mBAIf,CACA,MACA,YACA,OACsC;AAAA,EACtC,OAAO,KAAK,MAAM,YAAY,SAAS,CAAC,GAAG,MAAG;AAAA,IAAG;AAAA,GAAS;AAAA;AAI3D,IAAM,SAAS,CACd,OACA,UAEA,UAAU,YAAY,QAAQ,QAAQ,YAAY,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI;AAOzE,SAAS,IAIR,CACA,MACA,YACA,SACA,OACsC;AAAA,EACtC,QAAQ,WAAW,UAAU;AAAA,EAC7B,MAAM,SAAS,QAAQ,WAAW;AAAA,EAClC,MAAM,SAAS;AAAA,IACd,SACC,aAAa,QACZ,OAAO,aAAa,YAAY,SAAS,YAAY;AAAA,IACvD,UACC,UACA,aAAa,QACZ,OAAO,aAAa,YAAY,SAAS,aAAa;AAAA,EACzD;AAAA,EACA,MAAM,QAAQ;AAAA,EAGd,MAAM,UAAU,IAAI;AAAA,EACpB,MAAM,YAAY,IAAI;AAAA,EACtB,YAAY,IAAI,cAAc,OAAO,QAAQ,KAAK,GAAG;AAAA,IACpD,QAAQ,IAAI,GAAG,UAAU,UAAU,UAAU,QAAQ,EAAE;AAAA,IACvD,UAAU,IAAI,IAAI,YAAY,SAAS,CAAC;AAAA,EACzC;AAAA,EAGA,MAAM,OAAO,CAAC,IAAY,SAA6B;AAAA,IACtD,MAAM,YAAY,MAAM;AAAA,IACxB,IAAI,CAAC;AAAA,MAAW,MAAM,IAAI,MAAM,GAAG,sCAAsC;AAAA,IAEzE,MAAM,QACL,UAAU,WAAW,SAAS,KAC9B,OAAO,KAAK,UAAU,MAAM,WAAW,CAAC,CAAC,EAAE,SAAS;AAAA,IACrD,MAAM,QAAS,QAAQ,KAAK,KAAK;AAAA,IACjC,MAAM,SAAU,QAAQ,KAAK,KAAK,KAAK,OAAO,CAAC;AAAA,IAC/C,MAAM,SAAS,OAAO,MAAM,QAAQ,MAAM,CAAC;AAAA,IAC3C,MAAM,OAAO,SAAS,KAAK,OAAO,OAAO,IAAI;AAAA,IAC7C,OAAO,EAAE,WAAW,OAAO,KAAK;AAAA;AAAA,EAIjC,MAAM,QAAQ,OACb,IACA,WACA,UACmB;AAAA,IACnB,IAAI,CAAC,OAAO;AAAA,MAAS;AAAA,IACrB,MAAM,SAAS,MAAM,aAAa,WAAW,KAAK;AAAA,IAClD,IAAI,OAAO,WAAW;AAAA,MAAG;AAAA,IACzB,MAAM,UAAU;AAAA,MACf,aAAa;AAAA,MACb,QAAQ,UAAU;AAAA,MAClB,MAAM,UAAU;AAAA,IACjB;AAAA,IACA,MAAM,IAAI,gBAAgB,SAAS;AAAA,MAClC,MAAM;AAAA,SACH;AAAA,MACH;AAAA,IACD,CAAC;AAAA;AAAA,EAIF,MAAM,UAAU,CACf,IACA,WACA,OACA,SACI;AAAA,IACJ,MAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AAAA,IACxC,QAAQ,OAAO,SAAS,UAAU,WAAW,OAAO,OAAO;AAAA,IAC3D,OAAO;AAAA,SACH;AAAA,SACA;AAAA,MACH,OAAO,OAAO;AAAA,MACd;AAAA,MACA;AAAA,MACA,aAAa;AAAA,IACd;AAAA;AAAA,EAGD,MAAM,OAAO,OACZ,IACA,SACsB;AAAA,IACtB,QAAQ,WAAW,OAAO,SAAS,KAAK,IAAI,IAAI;AAAA,IAChD,MAAM,MAAM,IAAI,WAAW,KAAK;AAAA,IAChC,MAAM,UAAU,KAAK;AAAA,IAKrB,OAAO,QAAQ,UAAU,QAAQ,UAAU,MAAM;AAAA,SAC7C,QAAQ,IAAI,WAAW,OAAO,IAAI;AAAA,MACrC,WAAW,UAAU,IAAI,EAAE;AAAA,MAC3B,UAAU,OAAO;AAAA,MACjB;AAAA,IACD,CAAC;AAAA;AAAA,EAGF,MAAM,SAAS,CAAC,IAAY,SAA6B;AAAA,IACxD,QAAQ,WAAW,OAAO,SAAS,KAAK,IAAI,IAAI;AAAA,IAChD,MAAM,QAAQ,SAAS,SAAS;AAAA,IAChC,IAAI,CAAC;AAAA,MAAO,MAAM,IAAI,MAAM,GAAG,iCAAiC;AAAA,IAChE,OAAO,MAAM,SAAS;AAAA,IACtB,QAAQ,WAAW,aAAa,mBAAmB,SAAS;AAAA,IAC5D,MAAM,OAAO,MAAM;AAAA,MAClB,MAAM,UAAU,QAAQ,IAAI,WAAW,OAAO,IAAI;AAAA,MAElD,IAAI,CAAC,QAAQ,QAAQ,IAAI,QAAQ;AAAA,QAAG,QAAQ,QAAQ,IAAI,UAAU,IAAI;AAAA,MACtE,MAAM,SAAS;AAAA,WACX;AAAA,QACH,QAAQ,UAAU;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB;AAAA,MACD;AAAA,MACA,MAAM,aAAc,MAAM,SAAS,QAAQ,KAAK,SAAS,KAAK;AAAA,MAI9D,OAAO,WACN,UAAU,MACV,MAAM,SAAS,QACZ;AAAA,WACG;AAAA,QACH,QAAQ,MAAM;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,MACD,IACC,KAAK,QAAQ,MAAM,MAAM,KAAK,CAClC;AAAA;AAAA,IAED,OAAO,OAAO,UACX,aAAa,MAAM,MAAM,MAAM,IAAI,WAAW,KAAK,CAAC,IACpD,KAAK;AAAA;AAAA,EAGT,MAAM,SAAkC;AAAA,IACvC;AAAA,IACA,IAAI,CAAC,OAAe,SAAoB,KAAK,IAAI,IAAI;AAAA,IACrD,QAAQ,CAAC,OAAe,SAAoB,OAAO,IAAI,IAAI;AAAA,IAC3D,OAAO,MAAM;AAAA,MACZ,MAAM,QAAQ,KAAK,MAAM;AAAA,MACzB,MAAM,QAAQ,KACb,OACA,YACA,SACA,MAAM,MAAM,MACb;AAAA,MACA,OAAO,OAAO,iBAAiB,OAAO;AAAA,QACrC,QAAQ,EAAE,YAAY,MAAM,OAAO,MAAM,OAAO;AAAA,QAChD,QAAQ,EAAE,YAAY,MAAM,KAAK,MAAM,MAAM,OAAO;AAAA,MACrD,CAAC;AAAA;AAAA,EAEH;AAAA,EACA,WAAW,UAAU,SAAS;AAAA,IAC7B,OAAO,UAAU,CAAC,SAAiB,SAAoB;AAAA,MACtD,MAAM,KAAK,QAAQ,IAAI,GAAG,UAAU,MAAM;AAAA,MAC1C,IAAI,OAAO,WAAW;AAAA,QACrB,OAAO,QAAQ,OACd,IAAI,MACH,mBAAmB,OAAO,YAAY,KAAK,gBAC5C,CACD;AAAA,MACD;AAAA,MACA,OAAO,KAAK,IAAI,IAAI;AAAA;AAAA,EAEtB;AAAA,EACA,OAAO;AAAA;",
|
|
10
|
+
"debugId": "D3AB5DC616E1E63964756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A request checked before it is sent, by the validators the server runs, on
|
|
3
|
+
* what `@nxgt/openapi-hono` will read from it: each parameter as the
|
|
4
|
+
* text it travels as, the JSON as it parses, a form as its fields. What the
|
|
5
|
+
* server would refuse is refused here with the same issues, and nothing is
|
|
6
|
+
* sent.
|
|
7
|
+
*/
|
|
8
|
+
import type { ValidationIssue } from '@nxgt/httpyz';
|
|
9
|
+
import type { RuntimeOperation } from '../client/types';
|
|
10
|
+
import type { Input } from './to-request';
|
|
11
|
+
export declare function checkRequest(operation: RuntimeOperation, input: Input | undefined): Promise<ValidationIssue[]>;
|
|
12
|
+
//# sourceMappingURL=check-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-request.d.ts","sourceRoot":"","sources":["../../src/request/check-request.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAoB,eAAe,EAAE,MAAM,cAAc,CAAC;AAEtE,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,wBAAsB,YAAY,CACjC,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,KAAK,GAAG,SAAS,GACtB,OAAO,CAAC,eAAe,EAAE,CAAC,CAgC5B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An operation's input as a call of the core client, written the way
|
|
3
|
+
* `@nxgt/openapi-hono` reads it back: a query list as a repeated key,
|
|
4
|
+
* or joined with commas when the spec says `explode: false`; a header list
|
|
5
|
+
* joined with commas; the body as the media type the spec declares for its
|
|
6
|
+
* kind.
|
|
7
|
+
*/
|
|
8
|
+
import type { BodyInput } from '@nxgt/httpyz';
|
|
9
|
+
import type { RuntimeOperation } from '../client/types';
|
|
10
|
+
/** What an input may hold, as `ClientOperations` types it. */
|
|
11
|
+
export interface Input {
|
|
12
|
+
readonly param?: Readonly<Record<string, unknown>>;
|
|
13
|
+
readonly query?: Readonly<Record<string, unknown>>;
|
|
14
|
+
/** Keyed by lowercased name. */
|
|
15
|
+
readonly header?: Readonly<Record<string, unknown>>;
|
|
16
|
+
readonly json?: unknown;
|
|
17
|
+
readonly form?: Readonly<Record<string, unknown>>;
|
|
18
|
+
readonly text?: string;
|
|
19
|
+
readonly body?: Blob | ArrayBuffer | Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
/** The query and the body; the header parameters and the body's type go onto `headers`. */
|
|
22
|
+
export declare function toRequest(operation: RuntimeOperation, input: Input | undefined, headers: Headers): {
|
|
23
|
+
query: URLSearchParams;
|
|
24
|
+
body: BodyInput;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=to-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-request.d.ts","sourceRoot":"","sources":["../../src/request/to-request.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO9C,OAAO,KAAK,EAEX,gBAAgB,EAEhB,MAAM,iBAAiB,CAAC;AAEzB,8DAA8D;AAC9D,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,gCAAgC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,WAAW,GAAG,UAAU,CAAC;CAChD;AAMD,2FAA2F;AAC3F,wBAAgB,SAAS,CACxB,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,KAAK,GAAG,SAAS,EACxB,OAAO,EAAE,OAAO,GACd;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAe7C"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nxgt/openapi-httpyz",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"package.json"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/softistx/nxgt-http.git",
|
|
24
|
+
"directory": "packages/openapi-httpyz"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"registry": "https://registry.npmjs.org",
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "bun run ../../build.ts",
|
|
32
|
+
"fixtures": "bun run test/generate.ts",
|
|
33
|
+
"test": "bun run fixtures && bun test src",
|
|
34
|
+
"typecheck": "bun run fixtures && tsc --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@nxgt/httpyz": "^0.0.0",
|
|
38
|
+
"@nxgt/openapi-codegen": "^0.1.0",
|
|
39
|
+
"@nxgt/openapi-hono": "^0.0.0",
|
|
40
|
+
"@types/bun": "^1.4.0",
|
|
41
|
+
"hono": "^4.13.4",
|
|
42
|
+
"zod": "^4.5.4"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@nxgt/httpyz": "^0.0.0",
|
|
46
|
+
"typescript": "^6.0.3"
|
|
47
|
+
}
|
|
48
|
+
}
|