@kizunajs/fetch 2.0.0-beta.2 → 2.0.0-beta.21
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 +3 -5
- package/dist/index.cjs +1095 -242
- package/dist/index.d.cts +53 -162
- package/dist/index.d.mts +53 -162
- package/dist/index.mjs +1093 -230
- package/package.json +9 -20
- package/dist/server.cjs +0 -437
- package/dist/server.d.cts +0 -50
- package/dist/server.d.mts +0 -50
- package/dist/server.mjs +0 -435
package/dist/index.cjs
CHANGED
|
@@ -1,278 +1,1131 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
let kizunajs_schemas = require("kizunajs/schemas");
|
|
3
|
+
let kizunajs_generator = require("kizunajs/generator");
|
|
4
|
+
let zod = require("zod");
|
|
5
|
+
//#region src/runtime.ts
|
|
6
|
+
/**
|
|
7
|
+
* The code every generated client runs, written into the file below its route
|
|
8
|
+
* table. It imports nothing, so a client installs nothing, and it always comes
|
|
9
|
+
* from the release that wrote the table it reads.
|
|
10
|
+
*/
|
|
11
|
+
const runtimeSource = String.raw`/**
|
|
12
|
+
* What the client knows about one response: nothing for a body it reads as
|
|
13
|
+
* JSON, the media type for one that streams.
|
|
14
|
+
*/
|
|
15
|
+
export type GeneratedResponse =
|
|
16
|
+
| Record<string, never>
|
|
17
|
+
| {
|
|
18
|
+
stream: true;
|
|
19
|
+
contentType: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One route in the table: how to reach it, and how to read each status it
|
|
24
|
+
* answers with.
|
|
25
|
+
*/
|
|
26
|
+
export interface GeneratedRoute {
|
|
27
|
+
method: string;
|
|
28
|
+
path: string;
|
|
29
|
+
contentType?: string;
|
|
30
|
+
responses: Record<number, GeneratedResponse>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The route table, nested the way the client is.
|
|
35
|
+
*/
|
|
36
|
+
export interface GeneratedRoutes {
|
|
37
|
+
[key: string]: GeneratedRoutes | GeneratedRoute;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The request onRequest receives before it is sent. A header set on headers
|
|
42
|
+
* is sent with it.
|
|
43
|
+
*/
|
|
44
|
+
export interface OutgoingRequest {
|
|
45
|
+
url: string;
|
|
46
|
+
method: string;
|
|
47
|
+
headers: Headers;
|
|
48
|
+
route: GeneratedRoute;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Where the client sends its requests, and what it adds to each one.
|
|
53
|
+
*/
|
|
54
|
+
export interface ClientConfig {
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
baseHeaders?: Record<string, string>;
|
|
57
|
+
credentials?: RequestCredentials;
|
|
58
|
+
fetch?: typeof fetch;
|
|
59
|
+
onRequest?: (request: OutgoingRequest) => void | Promise<void>;
|
|
60
|
+
retry?: RetryOptions;
|
|
61
|
+
reconnect?: ReconnectOptions;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Retries a GET, HEAD, OPTIONS, PUT or DELETE that failed on the network or
|
|
66
|
+
* was answered 429 or 503, waiting what Retry-After asks or backing off.
|
|
67
|
+
*/
|
|
68
|
+
export interface RetryOptions {
|
|
69
|
+
/**
|
|
70
|
+
* Attempts in all, the first included.
|
|
71
|
+
*/
|
|
72
|
+
maximumAttempts?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Milliseconds the first backoff waits at most.
|
|
75
|
+
*/
|
|
76
|
+
initialDelay?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Milliseconds any wait may take.
|
|
79
|
+
*/
|
|
80
|
+
maximumDelay?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its
|
|
85
|
+
* connection drops, sending the id of the last event as Last-Event-ID.
|
|
86
|
+
*/
|
|
87
|
+
export interface ReconnectOptions {
|
|
88
|
+
/**
|
|
89
|
+
* Reconnections in a row before the stream fails. An event received resets the count.
|
|
90
|
+
*/
|
|
91
|
+
maximumAttempts?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Milliseconds to wait before reconnecting, until the server sends a retry field.
|
|
94
|
+
*/
|
|
95
|
+
reconnectionTime?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Milliseconds the wait may grow to after reconnections in a row that fail.
|
|
98
|
+
*/
|
|
99
|
+
maximumDelay?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* One method on the client. Its '~route' names the method it sends and
|
|
104
|
+
* whether its response streams, so a wrapper like @kizunajs/tanstack-query
|
|
105
|
+
* reads both off the client.
|
|
106
|
+
*/
|
|
107
|
+
export type ClientMethod<Method extends string, Streams extends boolean, Args, Result> = ({} extends Args
|
|
108
|
+
? (args?: Args) => Promise<Result>
|
|
109
|
+
: (args: Args) => Promise<Result>) & {
|
|
110
|
+
readonly '~route': GeneratedRoute & {
|
|
111
|
+
readonly method: Method;
|
|
112
|
+
readonly streams: Streams;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
interface CallArgs {
|
|
117
|
+
params?: Record<string, unknown>;
|
|
118
|
+
query?: Record<string, unknown>;
|
|
119
|
+
body?: unknown;
|
|
120
|
+
headers?: Record<string, string | undefined>;
|
|
121
|
+
fetchOptions?: RequestInit;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Dates go on the wire as ISO 8601, everything else as String gives it.
|
|
126
|
+
*/
|
|
127
|
+
const serializeValue = (value: unknown): string => (value instanceof Date ? value.toISOString() : String(value));
|
|
128
|
+
|
|
129
|
+
const buildPath = (path: string, params: Record<string, unknown> = {}): string =>
|
|
130
|
+
path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_, name: string) => {
|
|
131
|
+
const value = params[name];
|
|
132
|
+
if (value === undefined) throw new Error('Missing path parameter: ' + name);
|
|
133
|
+
return encodeURIComponent(serializeValue(value));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* An array repeats its key, and an undefined or null field is left out.
|
|
138
|
+
*/
|
|
139
|
+
const buildSearchParams = (fields: Record<string, unknown>): URLSearchParams => {
|
|
140
|
+
const params = new URLSearchParams();
|
|
141
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
142
|
+
if (value === undefined || value === null) continue;
|
|
143
|
+
for (const item of Array.isArray(value) ? value : [value]) params.append(key, serializeValue(item));
|
|
144
|
+
}
|
|
145
|
+
return params;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const buildFormData = (fields: Record<string, unknown>): FormData => {
|
|
149
|
+
const formData = new FormData();
|
|
150
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
151
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
152
|
+
if (item instanceof Blob) formData.append(key, item);
|
|
153
|
+
else if (item !== undefined && item !== null) formData.append(key, typeof item === 'string' ? item : JSON.stringify(item));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return formData;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A multipart body leaves Content-Type to fetch, which adds the boundary.
|
|
161
|
+
*/
|
|
162
|
+
const encodeBody = (route: GeneratedRoute, body: unknown, headers: Headers): BodyInit => {
|
|
163
|
+
switch (route.contentType) {
|
|
164
|
+
case 'multipart/form-data':
|
|
165
|
+
return body instanceof FormData ? body : buildFormData(body as Record<string, unknown>);
|
|
166
|
+
case 'application/x-www-form-urlencoded':
|
|
167
|
+
if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
|
168
|
+
return buildSearchParams(body as Record<string, unknown>);
|
|
169
|
+
default:
|
|
170
|
+
if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/json');
|
|
171
|
+
return JSON.stringify(body);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const parseJson = (text: string): unknown => {
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(text) as unknown;
|
|
178
|
+
} catch {
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
interface ServerSentEvent {
|
|
184
|
+
event?: string;
|
|
185
|
+
data: unknown;
|
|
186
|
+
id?: string;
|
|
187
|
+
retry?: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A lone carriage return at the end may be half of CRLF, so it waits for more
|
|
192
|
+
* unless the stream is over.
|
|
193
|
+
*/
|
|
194
|
+
const findLineEnd = (buffer: string, final: boolean): { index: number; length: number } | undefined => {
|
|
195
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
196
|
+
const character = buffer[index];
|
|
197
|
+
if (character === '\n') return { index, length: 1 };
|
|
198
|
+
if (character === '\r') {
|
|
199
|
+
if (index + 1 < buffer.length) return { index, length: buffer[index + 1] === '\n' ? 2 : 1 };
|
|
200
|
+
if (final) return { index, length: 1 };
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Reads text/event-stream as the WHATWG HTML specification parses it.
|
|
209
|
+
*/
|
|
210
|
+
interface EventStreamState {
|
|
211
|
+
lastEventId: string;
|
|
212
|
+
reconnectionTime: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function* readEvents(
|
|
216
|
+
body: ReadableStream<Uint8Array>,
|
|
217
|
+
state: EventStreamState = {
|
|
218
|
+
lastEventId: '',
|
|
219
|
+
reconnectionTime: 3000,
|
|
220
|
+
}
|
|
221
|
+
): AsyncGenerator<ServerSentEvent> {
|
|
222
|
+
const reader = body.getReader();
|
|
223
|
+
const decoder = new TextDecoder();
|
|
224
|
+
let buffer = '';
|
|
225
|
+
let first = true;
|
|
226
|
+
let dataLines: string[] = [];
|
|
227
|
+
let eventType = '';
|
|
228
|
+
let lastEventId = state.lastEventId;
|
|
229
|
+
let retry: number | undefined;
|
|
230
|
+
|
|
231
|
+
const dispatch = (): ServerSentEvent | undefined => {
|
|
232
|
+
state.lastEventId = lastEventId;
|
|
233
|
+
if (dataLines.length === 0) {
|
|
234
|
+
eventType = '';
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const message: ServerSentEvent = {
|
|
238
|
+
data: parseJson(dataLines.join('\n')),
|
|
239
|
+
};
|
|
240
|
+
if (eventType !== '') message.event = eventType;
|
|
241
|
+
if (lastEventId !== '') message.id = lastEventId;
|
|
242
|
+
if (retry !== undefined) message.retry = retry;
|
|
243
|
+
dataLines = [];
|
|
244
|
+
eventType = '';
|
|
245
|
+
retry = undefined;
|
|
246
|
+
return message;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const processLine = (line: string): ServerSentEvent | undefined => {
|
|
250
|
+
if (line === '') return dispatch();
|
|
251
|
+
if (line.startsWith(':')) return undefined;
|
|
252
|
+
const separator = line.indexOf(':');
|
|
253
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
254
|
+
let value = separator === -1 ? '' : line.slice(separator + 1);
|
|
255
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
256
|
+
switch (field) {
|
|
257
|
+
case 'event':
|
|
258
|
+
eventType = value;
|
|
259
|
+
break;
|
|
260
|
+
case 'data':
|
|
261
|
+
dataLines.push(value);
|
|
262
|
+
break;
|
|
263
|
+
case 'id':
|
|
264
|
+
if (!value.includes('\0')) lastEventId = value;
|
|
265
|
+
break;
|
|
266
|
+
case 'retry':
|
|
267
|
+
if (/^\d+$/.test(value)) {
|
|
268
|
+
retry = Number(value);
|
|
269
|
+
state.reconnectionTime = Math.min(retry, 3_600_000);
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
for (;;) {
|
|
278
|
+
const { done, value } = await reader.read();
|
|
279
|
+
let chunk = done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
280
|
+
if (first && chunk.length > 0) {
|
|
281
|
+
if (chunk.charCodeAt(0) === 0xfeff) chunk = chunk.slice(1);
|
|
282
|
+
first = false;
|
|
283
|
+
}
|
|
284
|
+
buffer += chunk;
|
|
285
|
+
for (;;) {
|
|
286
|
+
const lineEnd = findLineEnd(buffer, done);
|
|
287
|
+
if (lineEnd === undefined) break;
|
|
288
|
+
const line = buffer.slice(0, lineEnd.index);
|
|
289
|
+
buffer = buffer.slice(lineEnd.index + lineEnd.length);
|
|
290
|
+
const message = processLine(line);
|
|
291
|
+
if (message !== undefined) yield message;
|
|
292
|
+
}
|
|
293
|
+
if (done) return;
|
|
294
|
+
}
|
|
295
|
+
} finally {
|
|
296
|
+
void reader.cancel().catch(() => undefined);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function* reconnectingEvents(
|
|
301
|
+
first: Response,
|
|
302
|
+
reopen: (lastEventId: string) => Promise<Response>,
|
|
303
|
+
options: ReconnectOptions,
|
|
304
|
+
signal: AbortSignal | null | undefined
|
|
305
|
+
): AsyncGenerator<ServerSentEvent> {
|
|
306
|
+
const maximumAttempts = options.maximumAttempts ?? 5;
|
|
307
|
+
const maximumDelay = options.maximumDelay ?? 30_000;
|
|
308
|
+
const state: EventStreamState = {
|
|
309
|
+
lastEventId: '',
|
|
310
|
+
reconnectionTime: options.reconnectionTime ?? 3000,
|
|
311
|
+
};
|
|
312
|
+
let response: Response | undefined = first;
|
|
313
|
+
let attempts = 0;
|
|
314
|
+
let failure: unknown;
|
|
315
|
+
for (;;) {
|
|
316
|
+
if (response?.body) {
|
|
317
|
+
try {
|
|
318
|
+
for await (const message of readEvents(response.body, state)) {
|
|
319
|
+
attempts = 0;
|
|
320
|
+
failure = undefined;
|
|
321
|
+
yield message;
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
failure = error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (signal?.aborted) throw signal.reason;
|
|
328
|
+
if (attempts >= maximumAttempts) {
|
|
329
|
+
if (failure !== undefined) throw failure;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
attempts += 1;
|
|
333
|
+
const ceiling = Math.min(maximumDelay, state.reconnectionTime * 2 ** (attempts - 1));
|
|
334
|
+
await wait(state.reconnectionTime + Math.random() * Math.max(0, ceiling - state.reconnectionTime), signal);
|
|
335
|
+
try {
|
|
336
|
+
response = await reopen(state.lastEventId);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (signal?.aborted) throw error;
|
|
339
|
+
failure = error;
|
|
340
|
+
response = undefined;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (response.status === 204) return;
|
|
344
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
345
|
+
if (response.status !== first.status || !contentType.toLowerCase().startsWith('text/event-stream')) {
|
|
346
|
+
throw new Error('Reconnecting to the event stream failed with status ' + response.status);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function* readText(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {
|
|
352
|
+
const reader = body.getReader();
|
|
353
|
+
const decoder = new TextDecoder();
|
|
354
|
+
try {
|
|
355
|
+
for (;;) {
|
|
356
|
+
const { done, value } = await reader.read();
|
|
357
|
+
if (done) {
|
|
358
|
+
const tail = decoder.decode();
|
|
359
|
+
if (tail.length > 0) yield tail;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const text = decoder.decode(value, { stream: true });
|
|
363
|
+
if (text.length > 0) yield text;
|
|
364
|
+
}
|
|
365
|
+
} finally {
|
|
366
|
+
void reader.cancel().catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function* readBytes(body: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
|
|
371
|
+
const reader = body.getReader();
|
|
372
|
+
try {
|
|
373
|
+
for (;;) {
|
|
374
|
+
const { done, value } = await reader.read();
|
|
375
|
+
if (done) return;
|
|
376
|
+
yield value;
|
|
377
|
+
}
|
|
378
|
+
} finally {
|
|
379
|
+
void reader.cancel().catch(() => undefined);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const readStream = (body: ReadableStream<Uint8Array>, contentType: string): AsyncIterable<unknown> => {
|
|
384
|
+
const essence = (contentType.split(';')[0] ?? '').trim().toLowerCase();
|
|
385
|
+
if (essence === 'text/event-stream') return readEvents(body);
|
|
386
|
+
if (essence.startsWith('text/')) return readText(body);
|
|
387
|
+
return readBytes(body);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
|
|
391
|
+
|
|
392
|
+
const RETRY_STATUSES = new Set([429, 503]);
|
|
393
|
+
|
|
394
|
+
const retryAfter = (header: string | null): number | undefined => {
|
|
395
|
+
if (header === null) return undefined;
|
|
396
|
+
if (/^\d+$/.test(header.trim())) return Number(header.trim()) * 1000;
|
|
397
|
+
const date = Date.parse(header);
|
|
398
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const wait = (milliseconds: number, signal: AbortSignal | null | undefined): Promise<void> =>
|
|
402
|
+
new Promise((resolve, reject) => {
|
|
403
|
+
if (signal?.aborted) {
|
|
404
|
+
reject(signal.reason);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const onAbort = () => {
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
reject(signal?.reason);
|
|
410
|
+
};
|
|
411
|
+
const timer = setTimeout(() => {
|
|
412
|
+
signal?.removeEventListener('abort', onAbort);
|
|
413
|
+
resolve();
|
|
414
|
+
}, milliseconds);
|
|
415
|
+
signal?.addEventListener('abort', onAbort, {
|
|
416
|
+
once: true,
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const send = async (url: string, init: RequestInit, route: GeneratedRoute, config: ClientConfig): Promise<Response> => {
|
|
421
|
+
const fetcher = config.fetch ?? fetch;
|
|
422
|
+
const retry = config.retry;
|
|
423
|
+
if (retry === undefined || !IDEMPOTENT_METHODS.has(route.method)) return fetcher(url, init);
|
|
424
|
+
const maximumAttempts = retry.maximumAttempts ?? 3;
|
|
425
|
+
const initialDelay = retry.initialDelay ?? 500;
|
|
426
|
+
const maximumDelay = retry.maximumDelay ?? 30_000;
|
|
427
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
428
|
+
const backoff = Math.random() * Math.min(maximumDelay, initialDelay * 2 ** (attempt - 1));
|
|
429
|
+
let response: Response;
|
|
430
|
+
try {
|
|
431
|
+
response = await fetcher(url, init);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (attempt >= maximumAttempts || init.signal?.aborted || !(error instanceof TypeError)) throw error;
|
|
434
|
+
await wait(backoff, init.signal);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (attempt >= maximumAttempts || !RETRY_STATUSES.has(response.status)) return response;
|
|
438
|
+
const asked = retryAfter(response.headers.get('retry-after'));
|
|
439
|
+
if (asked !== undefined && asked > maximumDelay) return response;
|
|
440
|
+
await response.body?.cancel();
|
|
441
|
+
await wait(asked ?? backoff, init.signal);
|
|
442
|
+
}
|
|
10
443
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
444
|
+
|
|
445
|
+
const buildMethod = (route: GeneratedRoute, config: ClientConfig) => {
|
|
446
|
+
const call = async (args: CallArgs = {}) => {
|
|
447
|
+
const query = args.query === undefined ? '' : buildSearchParams(args.query).toString();
|
|
448
|
+
const url = config.baseUrl + buildPath(route.path, args.params) + (query.length > 0 ? '?' + query : '');
|
|
449
|
+
const prepare = async (lastEventId = ''): Promise<RequestInit> => {
|
|
450
|
+
const headers = new Headers(config.baseHeaders);
|
|
451
|
+
for (const [name, value] of Object.entries(args.headers ?? {})) {
|
|
452
|
+
if (value !== undefined) headers.set(name, value);
|
|
453
|
+
}
|
|
454
|
+
if (lastEventId !== '') headers.set('Last-Event-ID', lastEventId);
|
|
455
|
+
const init: RequestInit = {
|
|
456
|
+
method: route.method,
|
|
457
|
+
headers,
|
|
458
|
+
};
|
|
459
|
+
if (args.body !== undefined) init.body = encodeBody(route, args.body, headers);
|
|
460
|
+
if (config.credentials !== undefined) init.credentials = config.credentials;
|
|
461
|
+
await config.onRequest?.({
|
|
462
|
+
url,
|
|
463
|
+
method: route.method,
|
|
464
|
+
headers,
|
|
465
|
+
route,
|
|
466
|
+
});
|
|
467
|
+
return {
|
|
468
|
+
...init,
|
|
469
|
+
...args.fetchOptions,
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const response = await send(url, await prepare(), route, config);
|
|
474
|
+
const responseHeaders: Record<string, string> = {};
|
|
475
|
+
response.headers.forEach((value, name) => {
|
|
476
|
+
responseHeaders[name] = value;
|
|
477
|
+
});
|
|
478
|
+
const declared = route.responses[response.status];
|
|
479
|
+
if (declared !== undefined && 'stream' in declared && response.body !== null) {
|
|
480
|
+
const essence = (declared.contentType.split(';')[0] ?? '').trim().toLowerCase();
|
|
481
|
+
const reconnect = essence === 'text/event-stream' && IDEMPOTENT_METHODS.has(route.method) ? config.reconnect : undefined;
|
|
482
|
+
const reopen = async (lastEventId: string) => send(url, await prepare(lastEventId), route, config);
|
|
483
|
+
return {
|
|
484
|
+
status: response.status,
|
|
485
|
+
body: reconnect
|
|
486
|
+
? reconnectingEvents(response, reopen, reconnect, args.fetchOptions?.signal)
|
|
487
|
+
: readStream(response.body, declared.contentType),
|
|
488
|
+
headers: responseHeaders,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const text = await response.text();
|
|
492
|
+
return {
|
|
493
|
+
status: response.status,
|
|
494
|
+
body: text.length > 0 ? parseJson(text) : undefined,
|
|
495
|
+
headers: responseHeaders,
|
|
496
|
+
};
|
|
497
|
+
};
|
|
498
|
+
const streams = Object.values(route.responses).some((response) => 'stream' in response);
|
|
499
|
+
return Object.defineProperty(call, '~route', {
|
|
500
|
+
value: {
|
|
501
|
+
...route,
|
|
502
|
+
streams,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const isRoute = (node: GeneratedRoutes | GeneratedRoute): node is GeneratedRoute => typeof node['method'] === 'string';
|
|
508
|
+
|
|
509
|
+
const buildTree = (routes: GeneratedRoutes, config: ClientConfig): Record<string, unknown> => {
|
|
510
|
+
const tree: Record<string, unknown> = {};
|
|
511
|
+
for (const [key, node] of Object.entries(routes)) {
|
|
512
|
+
tree[key] = isRoute(node) ? buildMethod(node, config) : buildTree(node, config);
|
|
513
|
+
}
|
|
514
|
+
return tree;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Sends the request context headers with every request. An explicit
|
|
519
|
+
* baseHeaders entry wins.
|
|
520
|
+
*/
|
|
521
|
+
const buildClient = (routes: GeneratedRoutes, config: ClientConfig & { requestContext?: object }): Record<string, unknown> => {
|
|
522
|
+
const contextHeaders: Record<string, string> = {};
|
|
523
|
+
for (const [name, value] of Object.entries(config.requestContext ?? {})) {
|
|
524
|
+
if (value !== undefined) contextHeaders[name] = String(value);
|
|
525
|
+
}
|
|
526
|
+
return buildTree(routes, {
|
|
527
|
+
...config,
|
|
528
|
+
baseHeaders: {
|
|
529
|
+
...contextHeaders,
|
|
530
|
+
...config.baseHeaders,
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
`;
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/zod-to-typescript.ts
|
|
537
|
+
/**
|
|
538
|
+
* Collects the named types a walk produces, so a model shared by many routes is
|
|
539
|
+
* declared once.
|
|
540
|
+
*/
|
|
541
|
+
var TypeCollector = class {
|
|
542
|
+
named = /* @__PURE__ */ new Map();
|
|
543
|
+
claimed = /* @__PURE__ */ new Set();
|
|
544
|
+
brands = /* @__PURE__ */ new Map();
|
|
545
|
+
/**
|
|
546
|
+
* Which side of a pipe a schema is read from: the input a request sends, or
|
|
547
|
+
* the output a response carries.
|
|
548
|
+
*/
|
|
549
|
+
direction = "output";
|
|
550
|
+
/**
|
|
551
|
+
* Reads the schemas `read` walks from the side a request sends.
|
|
552
|
+
*/
|
|
553
|
+
readingInput(read) {
|
|
554
|
+
const previous = this.direction;
|
|
555
|
+
this.direction = "input";
|
|
556
|
+
try {
|
|
557
|
+
return read();
|
|
558
|
+
} finally {
|
|
559
|
+
this.direction = previous;
|
|
28
560
|
}
|
|
29
561
|
}
|
|
562
|
+
readsInput() {
|
|
563
|
+
return this.direction === "input";
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Claims a name before its body is walked, so a model that refers to itself
|
|
567
|
+
* resolves to its own name instead of recursing. Answers whether the caller
|
|
568
|
+
* is the one that has to emit it.
|
|
569
|
+
*/
|
|
570
|
+
claim(name) {
|
|
571
|
+
if (this.claimed.has(name)) return false;
|
|
572
|
+
this.claimed.add(name);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
add(type) {
|
|
576
|
+
this.named.set(type.name, type);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Declares a brand as a named type, `UserId = string & KizunaBrand<"UserId">`,
|
|
580
|
+
* and answers the name.
|
|
581
|
+
*/
|
|
582
|
+
addBrand(brand, base, example) {
|
|
583
|
+
const name = (0, kizunajs_generator.sanitizeIdentifier)(brand);
|
|
584
|
+
const existing = this.brands.get(name);
|
|
585
|
+
if (existing !== void 0 && (existing.brand !== brand || existing.base !== base)) throw new Error(`@kizunajs/fetch: brand ${quote(brand)} wraps both ${existing.base} and ${base}. Give each its own name.`);
|
|
586
|
+
if (existing === void 0) this.brands.set(name, {
|
|
587
|
+
brand,
|
|
588
|
+
base,
|
|
589
|
+
example
|
|
590
|
+
});
|
|
591
|
+
return name;
|
|
592
|
+
}
|
|
593
|
+
usesBrand() {
|
|
594
|
+
return this.brands.size > 0;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Each brand's name, the type it wraps, and a value for its `@example`, for
|
|
598
|
+
* the constructors the file exports.
|
|
599
|
+
*/
|
|
600
|
+
allBrands() {
|
|
601
|
+
return [...this.brands].map(([name, { base, example }]) => ({
|
|
602
|
+
name,
|
|
603
|
+
base,
|
|
604
|
+
example
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
all() {
|
|
608
|
+
for (const name of this.brands.keys()) if (this.named.has(name)) throw new Error(`@kizunajs/fetch: brand ${quote(name)} has the same name as a model. Give each its own name.`);
|
|
609
|
+
const brands = [...this.brands].map(([name, { brand, base }]) => ({
|
|
610
|
+
name,
|
|
611
|
+
description: `A branded \`${base}\`. Responses hand one back, and \`to${name}\` makes one from a plain value.`,
|
|
612
|
+
body: `${base} & KizunaBrand<${quote(brand)}>`
|
|
613
|
+
}));
|
|
614
|
+
return [...this.named.values(), ...brands].sort((left, right) => left.name.localeCompare(right.name));
|
|
615
|
+
}
|
|
30
616
|
};
|
|
617
|
+
const quote = (value) => JSON.stringify(value);
|
|
31
618
|
/**
|
|
32
|
-
*
|
|
619
|
+
* What a brand wraps: the scalars, as the JSON carries them.
|
|
33
620
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
621
|
+
const BRANDABLE_BASES = new Set([
|
|
622
|
+
"string",
|
|
623
|
+
"number",
|
|
624
|
+
"boolean"
|
|
625
|
+
]);
|
|
626
|
+
/**
|
|
627
|
+
* A literal as the type the JSON carries, so `z.literal(true)` is `true`.
|
|
628
|
+
*/
|
|
629
|
+
const literalType = (value) => {
|
|
630
|
+
if (typeof value === "string") return quote(value);
|
|
631
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
632
|
+
if (value === void 0) return "undefined";
|
|
633
|
+
return String(value);
|
|
634
|
+
};
|
|
635
|
+
const indent$1 = (text, depth = 1) => text.split("\n").map((line) => line.length > 0 ? " ".repeat(depth) + line : line).join("\n");
|
|
636
|
+
/**
|
|
637
|
+
* A property name, quoted only when it is not a plain identifier.
|
|
638
|
+
*/
|
|
639
|
+
const propertyKey = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : quote(key);
|
|
640
|
+
/**
|
|
641
|
+
* A doc comment over the lines it is given, or nothing when there are none.
|
|
642
|
+
* Always multi-line, the way the rest of this repository writes them.
|
|
643
|
+
*/
|
|
644
|
+
const docComment = (lines) => {
|
|
645
|
+
const body = lines.flatMap((line) => line.split("\n"));
|
|
646
|
+
if (body.length === 0) return "";
|
|
647
|
+
return `/**\n${body.map((line) => ` * ${line}`.trimEnd()).join("\n")}\n */\n`;
|
|
648
|
+
};
|
|
649
|
+
/**
|
|
650
|
+
* Renders a declared example as the TypeScript literal a caller would write.
|
|
651
|
+
*/
|
|
652
|
+
const exampleLiteral = (value) => {
|
|
653
|
+
if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
654
|
+
if (value === void 0) return "undefined";
|
|
655
|
+
return JSON.stringify(value) ?? "undefined";
|
|
656
|
+
};
|
|
657
|
+
const STRING_SAMPLES = {
|
|
658
|
+
email: "ada@example.com",
|
|
659
|
+
url: "https://example.com",
|
|
660
|
+
uuid: "3f1c2d9e-5b6a-4c7d-8e9f-0a1b2c3d4e5f",
|
|
661
|
+
datetime: "2026-01-01T00:00:00Z",
|
|
662
|
+
date: "2026-01-01",
|
|
663
|
+
time: "00:00:00"
|
|
664
|
+
};
|
|
665
|
+
const stringSample = (schema, key) => {
|
|
666
|
+
const def = (0, kizunajs_generator.readDef)(schema);
|
|
667
|
+
const formats = [def.format, ...(def.checks ?? []).map((check) => check.format ?? check.def?.format)];
|
|
668
|
+
for (const format of formats) if (format !== void 0 && format in STRING_SAMPLES) return exampleLiteral(STRING_SAMPLES[format]);
|
|
669
|
+
if (key !== void 0 && /id$/i.test(key)) return exampleLiteral("1");
|
|
670
|
+
return exampleLiteral("string");
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* A value a caller could pass for a schema, used in the `@example` on each
|
|
674
|
+
* client method. A schema that declares an example is taken at its word;
|
|
675
|
+
* anything else gets a value of the right shape.
|
|
676
|
+
*/
|
|
677
|
+
const sampleValue = (schema, depth = 0, key) => {
|
|
678
|
+
const [declared] = (0, kizunajs_generator.readMetaExamples)(schema);
|
|
679
|
+
if (declared !== void 0) return exampleLiteral(declared);
|
|
680
|
+
if ((0, kizunajs_generator.isFileSchema)(schema)) return "new File([], 'upload.txt')";
|
|
681
|
+
if ((0, kizunajs_generator.isBinarySchema)(schema)) return "new Uint8Array()";
|
|
682
|
+
const def = (0, kizunajs_generator.readDef)(schema);
|
|
683
|
+
if (def.type === "default" && def.defaultValue !== void 0) return exampleLiteral(def.defaultValue);
|
|
684
|
+
if (def.type === "pipe") {
|
|
685
|
+
const side = def.in ?? def.out;
|
|
686
|
+
return side ? sampleValue(side, depth, key) : "undefined";
|
|
100
687
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const text = decoder.decode(value, { stream: true });
|
|
114
|
-
if (text.length > 0) yield text;
|
|
688
|
+
if (def.innerType && def.type !== "object") return sampleValue(def.innerType, depth, key);
|
|
689
|
+
switch (def.type) {
|
|
690
|
+
case "number":
|
|
691
|
+
case "int": return "1";
|
|
692
|
+
case "bigint": return exampleLiteral("1");
|
|
693
|
+
case "boolean": return "true";
|
|
694
|
+
case "date": return exampleLiteral("2026-01-01");
|
|
695
|
+
case "null": return "null";
|
|
696
|
+
case "literal": return def.values?.length ? exampleLiteral(def.values[0]) : "undefined";
|
|
697
|
+
case "enum": {
|
|
698
|
+
const [first] = Object.values(def.entries ?? {});
|
|
699
|
+
return first === void 0 ? "undefined" : exampleLiteral(first);
|
|
115
700
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (done) return;
|
|
126
|
-
yield value;
|
|
701
|
+
case "array":
|
|
702
|
+
if (!def.element || depth >= SAMPLE_DEPTH) return "[]";
|
|
703
|
+
return `[\n${indent$1(`${sampleValue(def.element, depth + 1, key)},`)}\n]`;
|
|
704
|
+
case "tuple": return `[${(def.items ?? []).map((item) => sampleValue(item, depth + 1, key)).join(", ")}]`;
|
|
705
|
+
case "object": return sampleObject(schema, depth);
|
|
706
|
+
case "record": return "{}";
|
|
707
|
+
case "union": {
|
|
708
|
+
const options = (0, kizunajs_generator.readDiscriminatedUnion)(schema)?.options ?? def.options ?? [];
|
|
709
|
+
return options[0] ? sampleValue(options[0], depth, key) : "undefined";
|
|
127
710
|
}
|
|
128
|
-
|
|
129
|
-
|
|
711
|
+
case "string": return stringSample(schema, key);
|
|
712
|
+
default: return "undefined";
|
|
130
713
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
714
|
+
};
|
|
715
|
+
const SAMPLE_DEPTH = 2;
|
|
716
|
+
/**
|
|
717
|
+
* The required fields of an object schema, written as the literal a caller
|
|
718
|
+
* would pass. Optional fields are left out, since an example shows the least
|
|
719
|
+
* the route needs.
|
|
720
|
+
*/
|
|
721
|
+
const sampleObject = (schema, depth = 0) => {
|
|
722
|
+
const shape = (0, kizunajs_generator.readObjectShape)(schema);
|
|
723
|
+
if (!shape) return "{}";
|
|
724
|
+
const required = Object.entries(shape).filter(([, field]) => !(0, kizunajs_generator.unwrapOptionalWrappers)(field).optional);
|
|
725
|
+
if (required.length === 0 || depth >= SAMPLE_DEPTH) return "{}";
|
|
726
|
+
return `{\n${indent$1(required.map(([key, field]) => `${propertyKey(key)}: ${sampleValue((0, kizunajs_generator.unwrapOptionalWrappers)(field).inner, depth + 1, key)},`).join("\n"))}\n}`;
|
|
727
|
+
};
|
|
728
|
+
/**
|
|
729
|
+
* What an editor shows above a field: its description, whether it is on its way
|
|
730
|
+
* out, and the values the schema declares as examples.
|
|
731
|
+
*/
|
|
732
|
+
const fieldDoc = (field) => {
|
|
733
|
+
const lines = [];
|
|
734
|
+
const description = (0, kizunajs_generator.readMetaDescription)(field);
|
|
735
|
+
if (description !== void 0) lines.push(description);
|
|
736
|
+
const deprecation = (0, kizunajs_generator.readDeprecation)(field);
|
|
737
|
+
if (deprecation !== void 0) {
|
|
738
|
+
if (lines.length > 0) lines.push("");
|
|
739
|
+
lines.push(`@deprecated ${deprecation.message ?? ""}`.trimEnd());
|
|
740
|
+
}
|
|
741
|
+
for (const example of (0, kizunajs_generator.readMetaExamples)(field)) {
|
|
742
|
+
if (lines.length > 0) lines.push("");
|
|
743
|
+
lines.push("@example", exampleLiteral(example));
|
|
140
744
|
}
|
|
141
|
-
return
|
|
745
|
+
return lines;
|
|
746
|
+
};
|
|
747
|
+
const objectBody = (schema, collector, hint) => {
|
|
748
|
+
const shape = (0, kizunajs_generator.readObjectShape)(schema);
|
|
749
|
+
if (!shape) return "Record<string, unknown>";
|
|
750
|
+
const entries = Object.entries(shape);
|
|
751
|
+
if (entries.length === 0) return "Record<string, never>";
|
|
752
|
+
return `{\n${indent$1(entries.map(([key, field]) => {
|
|
753
|
+
const doc = docComment(fieldDoc(field));
|
|
754
|
+
const mark = (0, kizunajs_generator.unwrapOptionalWrappers)(field).optional ? "?" : "";
|
|
755
|
+
return `${doc}${propertyKey(key)}${mark}: ${typeOf(field, collector, `${hint}${(0, kizunajs_generator.toPascalCase)(key)}`)};`;
|
|
756
|
+
}).join("\n"))}\n}`;
|
|
142
757
|
};
|
|
143
758
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
759
|
+
* The TypeScript type a schema describes. Named models are collected and
|
|
760
|
+
* referenced; anonymous shapes are inlined. A brand is named like a model.
|
|
146
761
|
*/
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
762
|
+
const typeOf = (schema, collector, hint) => {
|
|
763
|
+
const type = unbrandedTypeOf(schema, collector, hint);
|
|
764
|
+
const brand = (0, kizunajs_generator.readMetaBrand)(schema);
|
|
765
|
+
if (brand === void 0 || !BRANDABLE_BASES.has(type)) return type;
|
|
766
|
+
return collector.addBrand(brand, type, sampleValue(schema, 0, brand));
|
|
767
|
+
};
|
|
768
|
+
const unbrandedTypeOf = (schema, collector, hint) => {
|
|
769
|
+
if ((0, kizunajs_generator.isFileSchema)(schema)) return "File | Blob";
|
|
770
|
+
if ((0, kizunajs_generator.isBinarySchema)(schema)) return "Uint8Array";
|
|
771
|
+
const def = (0, kizunajs_generator.readDef)(schema);
|
|
772
|
+
if (def.type === "nullable" && def.innerType) return `${typeOf(def.innerType, collector, hint)} | null`;
|
|
773
|
+
if (def.type === "nullish" && def.innerType) return `${typeOf(def.innerType, collector, hint)} | null`;
|
|
774
|
+
if (def.innerType && def.type !== "object") return typeOf(def.innerType, collector, hint);
|
|
775
|
+
if (def.type === "pipe") {
|
|
776
|
+
const side = collector.readsInput() ? def.in : def.out ?? def.in;
|
|
777
|
+
return side ? typeOf(side, collector, hint) : "unknown";
|
|
154
778
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
779
|
+
const modelName = (0, kizunajs_generator.readMetaId)(schema);
|
|
780
|
+
if (modelName !== void 0) {
|
|
781
|
+
if (collector.claim(modelName)) collector.add({
|
|
782
|
+
name: modelName,
|
|
783
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema),
|
|
784
|
+
body: structuralType(schema, collector, modelName)
|
|
785
|
+
});
|
|
786
|
+
return modelName;
|
|
787
|
+
}
|
|
788
|
+
return structuralType(schema, collector, hint);
|
|
157
789
|
};
|
|
158
|
-
const CLIENT_ROUTE = Symbol.for("kizuna.client-route");
|
|
159
790
|
/**
|
|
160
|
-
* The
|
|
161
|
-
* one.
|
|
791
|
+
* The type a schema describes by its shape, its name already spent by the caller.
|
|
162
792
|
*/
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
break;
|
|
182
|
-
default:
|
|
183
|
-
if (!hasContentTypeHeader) headers["Content-Type"] = "application/json";
|
|
184
|
-
body = JSON.stringify(args.body);
|
|
185
|
-
}
|
|
793
|
+
const structuralType = (schema, collector, hint) => {
|
|
794
|
+
const def = (0, kizunajs_generator.readDef)(schema);
|
|
795
|
+
switch (def.type) {
|
|
796
|
+
case "string": return "string";
|
|
797
|
+
case "number":
|
|
798
|
+
case "int": return "number";
|
|
799
|
+
case "bigint": return "string";
|
|
800
|
+
case "boolean": return "boolean";
|
|
801
|
+
case "date": return "string";
|
|
802
|
+
case "null": return "null";
|
|
803
|
+
case "any":
|
|
804
|
+
case "unknown": return "unknown";
|
|
805
|
+
case "never": return "never";
|
|
806
|
+
case "void":
|
|
807
|
+
case "undefined": return "undefined";
|
|
808
|
+
case "literal": {
|
|
809
|
+
const values = def.values ?? [];
|
|
810
|
+
return values.length > 0 ? values.map(literalType).join(" | ") : "never";
|
|
186
811
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
method: route.method,
|
|
191
|
-
headers: requestHeaders,
|
|
192
|
-
route
|
|
193
|
-
});
|
|
194
|
-
const res = await (config.fetch ?? fetch)(url, {
|
|
195
|
-
method: route.method,
|
|
196
|
-
headers: requestHeaders,
|
|
197
|
-
body,
|
|
198
|
-
credentials: config.credentials,
|
|
199
|
-
...args.fetchOptions
|
|
200
|
-
});
|
|
201
|
-
const responseHeaders = {};
|
|
202
|
-
res.headers.forEach((value, key) => {
|
|
203
|
-
responseHeaders[key] = value;
|
|
204
|
-
});
|
|
205
|
-
const responseSpec = route.responses[res.status];
|
|
206
|
-
if ((0, kizunajs.isStreamResponse)(responseSpec) && res.body !== null) return {
|
|
207
|
-
status: res.status,
|
|
208
|
-
body: readStream(res.body, responseSpec),
|
|
209
|
-
headers: responseHeaders
|
|
210
|
-
};
|
|
211
|
-
const text = await res.text();
|
|
212
|
-
let parsed;
|
|
213
|
-
try {
|
|
214
|
-
parsed = text.length > 0 ? JSON.parse(text) : void 0;
|
|
215
|
-
} catch {
|
|
216
|
-
parsed = text;
|
|
812
|
+
case "enum": {
|
|
813
|
+
const values = Object.values(def.entries ?? {});
|
|
814
|
+
return values.length > 0 ? values.map(literalType).join(" | ") : "never";
|
|
217
815
|
}
|
|
218
|
-
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
816
|
+
case "array": return def.element ? `Array<${typeOf(def.element, collector, `${hint}Item`)}>` : "unknown[]";
|
|
817
|
+
case "tuple": {
|
|
818
|
+
const items = (def.items ?? []).map((item, index) => typeOf(item, collector, `${hint}${index}`));
|
|
819
|
+
const rest = def.rest ? [`...Array<${typeOf(def.rest, collector, `${hint}Rest`)}>`] : [];
|
|
820
|
+
return `[${[...items, ...rest].join(", ")}]`;
|
|
821
|
+
}
|
|
822
|
+
case "object": return objectBody(schema, collector, hint);
|
|
823
|
+
case "record": return `Record<string, ${def.valueType ? typeOf(def.valueType, collector, `${hint}Value`) : "unknown"}>`;
|
|
824
|
+
case "union": {
|
|
825
|
+
const options = (0, kizunajs_generator.readDiscriminatedUnion)(schema)?.options ?? def.options ?? [];
|
|
826
|
+
if (options.length === 0) return "unknown";
|
|
827
|
+
return options.map((option, index) => typeOf(option, collector, `${hint}${index}`)).join(" | ");
|
|
828
|
+
}
|
|
829
|
+
default: return "unknown";
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/generator.ts
|
|
834
|
+
const indent = (text, depth = 1) => text.split("\n").map((line) => line.length > 0 ? " ".repeat(depth) + line : line).join("\n");
|
|
835
|
+
const pathParamNames = (path) => [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1] ?? "");
|
|
836
|
+
/**
|
|
837
|
+
* One server-sent event, typed from what the response declares. A record of
|
|
838
|
+
* event name to schema becomes a union discriminated on `event`; a single
|
|
839
|
+
* schema is the only shape a message takes.
|
|
840
|
+
*/
|
|
841
|
+
const eventUnion = (stream, collector, namespace) => {
|
|
842
|
+
const frame = "id?: string; retry?: number";
|
|
843
|
+
if (stream && typeof stream === "object" && !("_zod" in stream) && !("_def" in stream)) {
|
|
844
|
+
const arms = Object.entries(stream).map(([event, schema]) => `{ event: '${event}'; data: ${typeOf(schema, collector, `${namespace}${(0, kizunajs_generator.toPascalCase)(event)}`)}; ${frame} }`);
|
|
845
|
+
return arms.length > 0 ? arms.join(" | ") : `{ event?: string; data: string; ${frame} }`;
|
|
232
846
|
}
|
|
847
|
+
return `{ event?: string; data: ${typeOf(stream, collector, `${namespace}Message`)}; ${frame} }`;
|
|
848
|
+
};
|
|
849
|
+
const streamBody = (mode, stream, collector, namespace) => {
|
|
850
|
+
if (mode === "binary") return "AsyncIterable<Uint8Array>";
|
|
851
|
+
if (mode === "text") return "AsyncIterable<string>";
|
|
852
|
+
return `AsyncIterable<${eventUnion(stream, collector, namespace)}>`;
|
|
233
853
|
};
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
854
|
+
/**
|
|
855
|
+
* The call a route's `@example` shows: the arguments it cannot be called
|
|
856
|
+
* without, filled with values of the shape each schema declares.
|
|
857
|
+
*/
|
|
858
|
+
const exampleCall = (routeKey, route) => {
|
|
859
|
+
const entries = [];
|
|
860
|
+
const params = pathParamNames(route.path);
|
|
861
|
+
if (params.length > 0) {
|
|
862
|
+
const shape = route.pathParams ? (0, kizunajs_generator.readObjectShape)(route.pathParams) : void 0;
|
|
863
|
+
const fields = params.map((name) => {
|
|
864
|
+
const declared = shape?.[name];
|
|
865
|
+
return `${name}: ${declared ? sampleValue(declared, 0, name) : "'1'"},`;
|
|
866
|
+
});
|
|
867
|
+
entries.push(`params: {\n${indent(fields.join("\n"))}\n},`);
|
|
240
868
|
}
|
|
241
|
-
|
|
869
|
+
if (route.query && requiresArgument(route.query)) entries.push(`query: ${sampleObject(route.query)},`);
|
|
870
|
+
if (route.body && !(0, kizunajs_generator.isVoidSchema)(route.body) && requiresArgument(route.body)) entries.push(`body: ${sampleValue(route.body)},`);
|
|
871
|
+
if (route.headers && requiresArgument(route.headers)) entries.push(`headers: ${sampleObject(route.headers)},`);
|
|
872
|
+
return `const result = await client.${routeKey}(${entries.length > 0 ? `{\n${indent(entries.join("\n"))}\n}` : ""});`;
|
|
242
873
|
};
|
|
243
874
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
875
|
+
* Whether a schema has anything a caller has to pass, so a route whose query is
|
|
876
|
+
* every-field-optional keeps its example to the call itself.
|
|
246
877
|
*/
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
if (!
|
|
878
|
+
const requiresArgument = (schema) => {
|
|
879
|
+
const shape = (0, kizunajs_generator.readObjectShape)(schema);
|
|
880
|
+
if (!shape) return true;
|
|
881
|
+
return Object.values(shape).some((field) => !(0, kizunajs_generator.unwrapOptionalWrappers)(field).optional);
|
|
882
|
+
};
|
|
883
|
+
/**
|
|
884
|
+
* What an editor shows above a client method: what the route is for, whether it
|
|
885
|
+
* is on its way out, and how it is called.
|
|
886
|
+
*/
|
|
887
|
+
const routeDoc = (routeKey, route) => {
|
|
888
|
+
const lines = [];
|
|
889
|
+
if (route.summary) lines.push(route.summary);
|
|
890
|
+
if (route.description) {
|
|
891
|
+
if (lines.length > 0) lines.push("");
|
|
892
|
+
lines.push(route.description);
|
|
893
|
+
}
|
|
894
|
+
if (route.deprecated !== void 0 && route.deprecated !== false) {
|
|
895
|
+
const message = typeof route.deprecated === "string" ? route.deprecated : typeof route.deprecated === "object" ? route.deprecated.message : void 0;
|
|
896
|
+
if (lines.length > 0) lines.push("");
|
|
897
|
+
lines.push(`@deprecated ${message ?? ""}`.trimEnd());
|
|
898
|
+
}
|
|
899
|
+
if (lines.length > 0) lines.push("");
|
|
900
|
+
lines.push("@example", exampleCall(routeKey, route));
|
|
901
|
+
return docComment(lines);
|
|
902
|
+
};
|
|
903
|
+
const emitRoute = (routeKey, key, route, collector, namespaceName) => {
|
|
904
|
+
const namespace = routeKey.split(".").map(kizunajs_generator.toPascalCase).join("");
|
|
905
|
+
const members = [];
|
|
906
|
+
const args = [];
|
|
907
|
+
const params = pathParamNames(route.path);
|
|
908
|
+
const { pathParams, query, body, headers } = route;
|
|
909
|
+
if (params.length > 0) {
|
|
910
|
+
const paramsType = pathParams ? collector.readingInput(() => typeOf(pathParams, collector, `${namespace}Params`)) : `{\n${indent(params.map((name) => `${name}: string;`).join("\n"))}\n}`;
|
|
911
|
+
members.push(`export type Params = ${paramsType};`);
|
|
912
|
+
args.push(`params: ${namespaceName}.${namespace}.Params`);
|
|
913
|
+
}
|
|
914
|
+
if (query) {
|
|
915
|
+
members.push(`export type Query = ${collector.readingInput(() => typeOf(query, collector, `${namespace}Query`))};`);
|
|
916
|
+
args.push(`query${requiresArgument(query) ? "" : "?"}: ${namespaceName}.${namespace}.Query`);
|
|
917
|
+
}
|
|
918
|
+
if (body && !(0, kizunajs_generator.isVoidSchema)(body)) {
|
|
919
|
+
members.push(`export type Body = ${collector.readingInput(() => typeOf(body, collector, `${namespace}Body`))};`);
|
|
920
|
+
args.push(`body: ${namespaceName}.${namespace}.Body`);
|
|
921
|
+
}
|
|
922
|
+
if (headers) {
|
|
923
|
+
members.push(`export type Headers = ${collector.readingInput(() => typeOf(headers, collector, `${namespace}Headers`))};`);
|
|
924
|
+
args.push(`headers${requiresArgument(headers) ? "" : "?"}: ${namespaceName}.${namespace}.Headers`);
|
|
925
|
+
} else args.push("headers?: Record<string, string>");
|
|
926
|
+
args.push("fetchOptions?: RequestInit");
|
|
927
|
+
const validates = route.body !== void 0 || route.query !== void 0;
|
|
928
|
+
const declaredBadRequest = Object.keys(route.responses).includes("400");
|
|
929
|
+
const validationBody = validates ? typeOf(kizunajs_schemas.ValidationErrorSchema, collector, "ValidationError") : void 0;
|
|
930
|
+
const results = Object.entries(route.responses).map(([status, response]) => {
|
|
931
|
+
const headers = (0, kizunajs_generator.resolveResponseHeaders)(response);
|
|
932
|
+
const headerType = headers ? typeOf(headers, collector, `${namespace}${status}Headers`) : "Record<string, string>";
|
|
933
|
+
if ((0, kizunajs_generator.isStreamResponse)(response)) return `{ status: ${status}; body: ${streamBody((0, kizunajs_generator.streamMode)(response), response.stream, collector, `${namespace}${status}`)}; headers: ${headerType} }`;
|
|
934
|
+
const schema = (0, kizunajs_generator.resolveResponseBody)(response);
|
|
935
|
+
const body = schema ? typeOf(schema, collector, `${namespace}${status}`) : "undefined";
|
|
936
|
+
return `{ status: ${status}; body: ${status === "400" && validationBody ? `${body} | ${validationBody}` : body}; headers: ${headerType} }`;
|
|
937
|
+
});
|
|
938
|
+
if (validationBody && !declaredBadRequest) results.push(`{ status: 400; body: ${validationBody}; headers: Record<string, string> }`);
|
|
939
|
+
members.push(`export type Result =\n${indent(results.map((result) => `| ${result}`).join("\n"))};`);
|
|
940
|
+
const argsType = `{\n${indent(args.map((arg) => `${arg};`).join("\n"))}\n}`;
|
|
941
|
+
const tableEntries = [`method: '${route.method}'`, `path: '${route.path}'`];
|
|
942
|
+
if (route.contentType) tableEntries.push(`contentType: '${route.contentType}'`);
|
|
943
|
+
const responses = Object.entries(route.responses).map(([status, response]) => (0, kizunajs_generator.isStreamResponse)(response) ? `${status}: { stream: true, contentType: '${(0, kizunajs_generator.streamContentType)(response)}' }` : `${status}: {}`);
|
|
944
|
+
if (validationBody && !declaredBadRequest) responses.push("400: {}");
|
|
945
|
+
tableEntries.push(`responses: {\n${indent(responses.map((entry) => `${entry},`).join("\n"))}\n}`);
|
|
946
|
+
const streams = Object.values(route.responses).some((response) => (0, kizunajs_generator.isStreamResponse)(response));
|
|
250
947
|
return {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
948
|
+
namespace,
|
|
949
|
+
signature: `${routeDoc(routeKey, route)}${key}: ClientMethod<'${route.method}', ${streams}, ${argsType}, ${namespaceName}.${namespace}.Result>;`,
|
|
950
|
+
declaration: `export namespace ${namespace} {\n${indent(members.join("\n\n"))}\n}`,
|
|
951
|
+
table: `${key}: {\n${indent(tableEntries.map((entry) => `${entry},`).join("\n"))}\n},`
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
const isRoute = (value) => !!value && typeof value === "object" && typeof value.method === "string";
|
|
955
|
+
const emitTree = (routes, prefix, collector, namespaceName) => {
|
|
956
|
+
const signatures = [];
|
|
957
|
+
const tables = [];
|
|
958
|
+
const declarations = [];
|
|
959
|
+
for (const key of Object.keys(routes)) {
|
|
960
|
+
const node = routes[key];
|
|
961
|
+
const routeKey = prefix ? `${prefix}.${key}` : key;
|
|
962
|
+
if (isRoute(node)) {
|
|
963
|
+
const emitted = emitRoute(routeKey, key, node, collector, namespaceName);
|
|
964
|
+
signatures.push(emitted.signature);
|
|
965
|
+
tables.push(emitted.table);
|
|
966
|
+
declarations.push(emitted.declaration);
|
|
967
|
+
continue;
|
|
255
968
|
}
|
|
969
|
+
if (node && typeof node === "object") {
|
|
970
|
+
const group = emitTree(node, routeKey, collector, namespaceName);
|
|
971
|
+
signatures.push(`${key}: {\n${indent(group.signatures)}\n};`);
|
|
972
|
+
tables.push(`${key}: {\n${indent(group.table)}\n},`);
|
|
973
|
+
declarations.push(...group.declarations);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
signatures: signatures.join("\n"),
|
|
978
|
+
table: tables.join("\n"),
|
|
979
|
+
declarations
|
|
256
980
|
};
|
|
257
981
|
};
|
|
258
982
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
983
|
+
* Renders a TypeScript client from an api: its named types, the route table
|
|
984
|
+
* the runtime reads, and a `createClient` typed against both.
|
|
261
985
|
*/
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
986
|
+
/**
|
|
987
|
+
* The headers a request context declares, as one interface the caller fills
|
|
988
|
+
* once. Absent when the API declares no request context that reads headers.
|
|
989
|
+
*/
|
|
990
|
+
const emitRequestContext = (contract, collector) => {
|
|
991
|
+
const declarations = Object.values(contract.requestContext ?? {});
|
|
992
|
+
const fields = [];
|
|
993
|
+
let required = false;
|
|
994
|
+
for (const declaration of declarations) {
|
|
995
|
+
const headers = declaration.headers;
|
|
996
|
+
if (!headers) continue;
|
|
997
|
+
required ||= requiresArgument(headers);
|
|
998
|
+
const inner = collector.readingInput(() => typeOf(headers, collector, "RequestContext")).trim().replace(/^\{/, "").replace(/\}$/, "").split("\n").map((line) => line.trim()).filter((line) => line !== "").join("\n");
|
|
999
|
+
if (inner !== "") fields.push(inner);
|
|
270
1000
|
}
|
|
1001
|
+
if (fields.length === 0) return void 0;
|
|
1002
|
+
return {
|
|
1003
|
+
declaration: `export interface RequestContext {\n${indent(fields.join("\n"))}\n}`,
|
|
1004
|
+
required
|
|
1005
|
+
};
|
|
1006
|
+
};
|
|
1007
|
+
/**
|
|
1008
|
+
* A constructor per brand, so a value from outside the API becomes one with
|
|
1009
|
+
* `toUserId('usr_k7f3q9')` rather than a cast.
|
|
1010
|
+
*/
|
|
1011
|
+
const emitBrandConstructors = (collector, namespaceName) => collector.allBrands().map(({ name, base, example }) => `
|
|
1012
|
+
${docComment([
|
|
1013
|
+
`Brands a value from outside the API, like a URL segment, as \`${namespaceName}.${name}\`. A value from a response is branded already.`,
|
|
1014
|
+
"",
|
|
1015
|
+
"@example",
|
|
1016
|
+
`const id = to${name}(${example});`
|
|
1017
|
+
])}export const to${name} = (value: ${base}): ${namespaceName}.${name} => value as ${namespaceName}.${name};
|
|
1018
|
+
`).join("");
|
|
1019
|
+
/**
|
|
1020
|
+
* Tells kizuna's validation error from a 400 the route declares itself.
|
|
1021
|
+
*/
|
|
1022
|
+
const emitValidationGuard = (namespaceName) => `
|
|
1023
|
+
${docComment([
|
|
1024
|
+
`Whether a \`400\` body is the validation error kizuna sends, rather than a \`400\` the route declares itself.`,
|
|
1025
|
+
"",
|
|
1026
|
+
"@example",
|
|
1027
|
+
"if (result.status === 400 && isValidationError(result.body)) {",
|
|
1028
|
+
" console.log(result.body.errors);",
|
|
1029
|
+
"}"
|
|
1030
|
+
])}export const isValidationError = (body: unknown): body is ${namespaceName}.ValidationError =>
|
|
1031
|
+
typeof body === 'object' && body !== null && 'errors' in body && Array.isArray(body.errors);
|
|
1032
|
+
`;
|
|
1033
|
+
/**
|
|
1034
|
+
* Declared in the file, so a brand needs nothing installed.
|
|
1035
|
+
*/
|
|
1036
|
+
const BRAND_DECLARATION = `declare const kizunaBrand: unique symbol;
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Tells one branded type from another with the same base.
|
|
1040
|
+
*/
|
|
1041
|
+
type KizunaBrand<Name extends string> = {
|
|
1042
|
+
readonly [kizunaBrand]: {
|
|
1043
|
+
readonly [Key in Name]: true;
|
|
1044
|
+
};
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
`;
|
|
1048
|
+
const generateFetchClient = (contract, options = {}) => {
|
|
1049
|
+
const { namespace: namespaceName = "API", regenerateCommand = "kizuna generate", source } = options;
|
|
1050
|
+
const collector = new TypeCollector();
|
|
1051
|
+
const tree = emitTree((0, kizunajs_generator.listedRoutes)(contract), "", collector, namespaceName);
|
|
1052
|
+
const requestContext = emitRequestContext(contract, collector);
|
|
1053
|
+
const api = [...collector.all().map((model) => {
|
|
1054
|
+
return `${model.description ? `/**\n * ${model.description}\n */\n` : ""}export type ${model.name} = ${model.body};`;
|
|
1055
|
+
}), ...tree.declarations].join("\n\n");
|
|
1056
|
+
const brands = emitBrandConstructors(collector, namespaceName);
|
|
1057
|
+
const guards = collector.all().some((model) => model.name === "ValidationError") ? emitValidationGuard(namespaceName) : "";
|
|
1058
|
+
return `/* eslint-disable */
|
|
1059
|
+
/**
|
|
1060
|
+
${[
|
|
1061
|
+
" * Generated by @kizunajs/fetch. Do not edit.",
|
|
1062
|
+
" *",
|
|
1063
|
+
...source === void 0 ? [] : [` * Source: ${source}`],
|
|
1064
|
+
` * Regenerate: ${regenerateCommand}`
|
|
1065
|
+
].join("\n")}
|
|
1066
|
+
*/
|
|
1067
|
+
${collector.usesBrand() ? BRAND_DECLARATION : ""}export namespace ${namespaceName} {
|
|
1068
|
+
${indent(api)}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export interface Client {
|
|
1072
|
+
${indent(tree.signatures)}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const routes: GeneratedRoutes = {
|
|
1076
|
+
${indent(tree.table)}
|
|
1077
|
+
};
|
|
1078
|
+
${brands}${guards}
|
|
1079
|
+
${requestContext === void 0 ? `/**
|
|
1080
|
+
* A client for this API. Pass the base URL and anything else the runtime takes.
|
|
1081
|
+
*/
|
|
1082
|
+
export const createClient = (config: ClientConfig): Client => buildClient(routes, config) as unknown as Client;` : `${requestContext.declaration}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* A client for this API. Pass the base URL, the request context headers this
|
|
1086
|
+
* API declares, and anything else the runtime takes.
|
|
1087
|
+
*/
|
|
1088
|
+
export const createClient = (config: ClientConfig & { requestContext${requestContext.required ? "" : "?"}: RequestContext }): Client =>
|
|
1089
|
+
buildClient(routes, config) as unknown as Client;`}
|
|
1090
|
+
|
|
1091
|
+
${runtimeSource}`;
|
|
1092
|
+
};
|
|
1093
|
+
//#endregion
|
|
1094
|
+
//#region src/client-target.ts
|
|
1095
|
+
const FetchClientOptionsSchema = zod.z.object({
|
|
1096
|
+
/**
|
|
1097
|
+
* Name of the generated namespace holding every model and every route's
|
|
1098
|
+
* `Params`, `Query`, `Body`, `Headers` and `Result`.
|
|
1099
|
+
*
|
|
1100
|
+
* @default 'API'
|
|
1101
|
+
*/
|
|
1102
|
+
namespace: zod.z.string().optional(),
|
|
1103
|
+
/**
|
|
1104
|
+
* Command the file's header tells a reader to run.
|
|
1105
|
+
*
|
|
1106
|
+
* @default 'kizuna generate'
|
|
1107
|
+
*/
|
|
1108
|
+
regenerateCommand: zod.z.string().optional(),
|
|
1109
|
+
/**
|
|
1110
|
+
* Path to the api this was generated from, relative to the generated
|
|
1111
|
+
* file. Named in the header so tooling can tell when the client has fallen
|
|
1112
|
+
* behind, and so a reader knows where the API is declared.
|
|
1113
|
+
*/
|
|
1114
|
+
source: zod.z.string().optional()
|
|
271
1115
|
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
1116
|
+
/**
|
|
1117
|
+
* A TypeScript client for `kizuna.config.ts`.
|
|
1118
|
+
*
|
|
1119
|
+
* @example
|
|
1120
|
+
* fetchClient({
|
|
1121
|
+
* output: './packages/api-client/src/generated.ts',
|
|
1122
|
+
* });
|
|
1123
|
+
*/
|
|
1124
|
+
const fetchClient = (0, kizunajs_generator.defineClient)({
|
|
1125
|
+
target: "fetch",
|
|
1126
|
+
options: FetchClientOptionsSchema,
|
|
1127
|
+
generate: ({ options, api }) => ({ finalize: () => generateFetchClient(api, options) })
|
|
277
1128
|
});
|
|
278
|
-
|
|
1129
|
+
//#endregion
|
|
1130
|
+
exports.fetchClient = fetchClient;
|
|
1131
|
+
exports.generateFetchClient = generateFetchClient;
|