@kizunajs/fetch 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.MD +9 -0
- package/README.md +49 -0
- package/dist/index.cjs +278 -0
- package/dist/index.d.cts +166 -0
- package/dist/index.d.mts +166 -0
- package/dist/index.mjs +263 -0
- package/dist/server.cjs +437 -0
- package/dist/server.d.cts +50 -0
- package/dist/server.d.mts +50 -0
- package/dist/server.mjs +435 -0
- package/package.json +86 -0
package/LICENSE.MD
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sondre Ørland
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @kizunajs/fetch
|
|
2
|
+
|
|
3
|
+
`@kizunajs/fetch` is the typed client for your routes, a wrapper around the native `fetch` API. It runs anywhere `fetch` does, React Native included.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @kizunajs/fetch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Add `fetchClient` to your config and run `kizuna generate`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { fetchClient } from '@kizunajs/fetch/server';
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
adapter: expressAdapter(),
|
|
20
|
+
routes: {
|
|
21
|
+
users,
|
|
22
|
+
},
|
|
23
|
+
clients: [
|
|
24
|
+
fetchClient({
|
|
25
|
+
output: './src/lib/api-client.generated.ts',
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The generated file exports `createClient`:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createClient } from './api-client.generated';
|
|
35
|
+
|
|
36
|
+
const apiClient = createClient({
|
|
37
|
+
baseUrl: 'http://localhost:3000',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const result = await apiClient.users.getUser({
|
|
41
|
+
params: {
|
|
42
|
+
id: '1',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
[Fetch client](https://kizunajs.com/docs/clients/fetch)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let kizunajs = require("kizunajs");
|
|
3
|
+
//#region src/sse.ts
|
|
4
|
+
const parseData = (text) => {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(text);
|
|
7
|
+
} catch {
|
|
8
|
+
return text;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const findLineEnd = (buffer, final) => {
|
|
12
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
13
|
+
const character = buffer[index];
|
|
14
|
+
if (character === "\n") return {
|
|
15
|
+
index,
|
|
16
|
+
length: 1
|
|
17
|
+
};
|
|
18
|
+
if (character === "\r") {
|
|
19
|
+
if (index + 1 < buffer.length) return {
|
|
20
|
+
index,
|
|
21
|
+
length: buffer[index + 1] === "\n" ? 2 : 1
|
|
22
|
+
};
|
|
23
|
+
if (final) return {
|
|
24
|
+
index,
|
|
25
|
+
length: 1
|
|
26
|
+
};
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Parses `text/event-stream` per the WHATWG HTML specification.
|
|
33
|
+
*/
|
|
34
|
+
async function* parseServerSentEvents(body) {
|
|
35
|
+
const reader = body.getReader();
|
|
36
|
+
const decoder = new TextDecoder();
|
|
37
|
+
let buffer = "";
|
|
38
|
+
let first = true;
|
|
39
|
+
let dataLines = [];
|
|
40
|
+
let eventType = "";
|
|
41
|
+
let lastEventId = "";
|
|
42
|
+
let retry;
|
|
43
|
+
const dispatch = () => {
|
|
44
|
+
if (dataLines.length === 0) {
|
|
45
|
+
eventType = "";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const message = { data: parseData(dataLines.join("\n")) };
|
|
49
|
+
if (eventType !== "") message.event = eventType;
|
|
50
|
+
if (lastEventId !== "") message.id = lastEventId;
|
|
51
|
+
if (retry !== void 0) message.retry = retry;
|
|
52
|
+
dataLines = [];
|
|
53
|
+
eventType = "";
|
|
54
|
+
retry = void 0;
|
|
55
|
+
return message;
|
|
56
|
+
};
|
|
57
|
+
const processLine = (line) => {
|
|
58
|
+
if (line === "") return dispatch();
|
|
59
|
+
if (line.startsWith(":")) return void 0;
|
|
60
|
+
const separator = line.indexOf(":");
|
|
61
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
62
|
+
let value = separator === -1 ? "" : line.slice(separator + 1);
|
|
63
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
64
|
+
switch (field) {
|
|
65
|
+
case "event":
|
|
66
|
+
eventType = value;
|
|
67
|
+
break;
|
|
68
|
+
case "data":
|
|
69
|
+
dataLines.push(value);
|
|
70
|
+
break;
|
|
71
|
+
case "id":
|
|
72
|
+
if (!value.includes("\0")) lastEventId = value;
|
|
73
|
+
break;
|
|
74
|
+
case "retry":
|
|
75
|
+
if (/^\d+$/.test(value)) retry = Number(value);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
for (;;) {
|
|
81
|
+
const { done, value } = await reader.read();
|
|
82
|
+
let chunk = done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
83
|
+
if (first && chunk.length > 0) {
|
|
84
|
+
if (chunk.charCodeAt(0) === 65279) chunk = chunk.slice(1);
|
|
85
|
+
first = false;
|
|
86
|
+
}
|
|
87
|
+
buffer += chunk;
|
|
88
|
+
for (;;) {
|
|
89
|
+
const lineEnd = findLineEnd(buffer, done);
|
|
90
|
+
if (lineEnd === void 0) break;
|
|
91
|
+
const line = buffer.slice(0, lineEnd.index);
|
|
92
|
+
buffer = buffer.slice(lineEnd.index + lineEnd.length);
|
|
93
|
+
const message = processLine(line);
|
|
94
|
+
if (message !== void 0) yield message;
|
|
95
|
+
}
|
|
96
|
+
if (done) return;
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
reader.cancel().catch(() => void 0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function* readTextChunks(body) {
|
|
103
|
+
const reader = body.getReader();
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
try {
|
|
106
|
+
for (;;) {
|
|
107
|
+
const { done, value } = await reader.read();
|
|
108
|
+
if (done) {
|
|
109
|
+
const tail = decoder.decode();
|
|
110
|
+
if (tail.length > 0) yield tail;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const text = decoder.decode(value, { stream: true });
|
|
114
|
+
if (text.length > 0) yield text;
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
reader.cancel().catch(() => void 0);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function* readByteChunks(body) {
|
|
121
|
+
const reader = body.getReader();
|
|
122
|
+
try {
|
|
123
|
+
for (;;) {
|
|
124
|
+
const { done, value } = await reader.read();
|
|
125
|
+
if (done) return;
|
|
126
|
+
yield value;
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
reader.cancel().catch(() => void 0);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/client.ts
|
|
134
|
+
const buildFormData = (body) => {
|
|
135
|
+
const formData = new FormData();
|
|
136
|
+
for (const [key, entry] of Object.entries(body)) {
|
|
137
|
+
const values = Array.isArray(entry) ? entry : [entry];
|
|
138
|
+
for (const value of values) if (value instanceof File || value instanceof Blob) formData.append(key, value);
|
|
139
|
+
else if (value !== void 0 && value !== null) formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
140
|
+
}
|
|
141
|
+
return formData;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Serializes a query or path value for the URL. Dates use ISO 8601; everything
|
|
145
|
+
* else uses `String`.
|
|
146
|
+
*/
|
|
147
|
+
const serializeValue = (value) => value instanceof Date ? value.toISOString() : String(value);
|
|
148
|
+
const buildQueryString = (query) => {
|
|
149
|
+
const params = new URLSearchParams();
|
|
150
|
+
for (const [key, value] of Object.entries(query)) {
|
|
151
|
+
if (value === void 0 || value === null) continue;
|
|
152
|
+
if (Array.isArray(value)) for (const item of value) params.append(key, serializeValue(item));
|
|
153
|
+
else params.append(key, serializeValue(value));
|
|
154
|
+
}
|
|
155
|
+
const result = params.toString();
|
|
156
|
+
return result.length > 0 ? `?${result}` : "";
|
|
157
|
+
};
|
|
158
|
+
const CLIENT_ROUTE = Symbol.for("kizuna.client-route");
|
|
159
|
+
/**
|
|
160
|
+
* The route a client method answers, or `undefined` for anything that is not
|
|
161
|
+
* one.
|
|
162
|
+
*/
|
|
163
|
+
const routeOf = (value) => typeof value === "function" ? value[CLIENT_ROUTE] : void 0;
|
|
164
|
+
const buildRouteFn = (route, config) => {
|
|
165
|
+
const call = async (args = {}) => {
|
|
166
|
+
const url = config.baseUrl + (0, kizunajs.buildPath)(route.path, args.params) + (args.query ? buildQueryString(args.query) : "");
|
|
167
|
+
const headers = {
|
|
168
|
+
...config.baseHeaders ?? {},
|
|
169
|
+
...args.headers ?? {}
|
|
170
|
+
};
|
|
171
|
+
let body;
|
|
172
|
+
if (args.body !== void 0) {
|
|
173
|
+
const hasContentTypeHeader = "Content-Type" in headers || "content-type" in headers;
|
|
174
|
+
switch (route.contentType) {
|
|
175
|
+
case "multipart/form-data":
|
|
176
|
+
body = args.body instanceof FormData ? args.body : buildFormData(args.body);
|
|
177
|
+
break;
|
|
178
|
+
case "application/x-www-form-urlencoded":
|
|
179
|
+
if (!hasContentTypeHeader) headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
180
|
+
body = new URLSearchParams(args.body);
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
if (!hasContentTypeHeader) headers["Content-Type"] = "application/json";
|
|
184
|
+
body = JSON.stringify(args.body);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const requestHeaders = new Headers(headers);
|
|
188
|
+
if (config.onRequest) await config.onRequest({
|
|
189
|
+
url,
|
|
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;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
status: res.status,
|
|
220
|
+
body: parsed,
|
|
221
|
+
headers: responseHeaders
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
call[CLIENT_ROUTE] = route;
|
|
225
|
+
return call;
|
|
226
|
+
};
|
|
227
|
+
const readStream = (body, definition) => {
|
|
228
|
+
switch ((0, kizunajs.streamMode)(definition)) {
|
|
229
|
+
case "events": return parseServerSentEvents(body);
|
|
230
|
+
case "text": return readTextChunks(body);
|
|
231
|
+
case "binary": return readByteChunks(body);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const buildClientTree = (router, config) => {
|
|
235
|
+
const result = {};
|
|
236
|
+
for (const key of Object.keys(router)) {
|
|
237
|
+
const node = router[key];
|
|
238
|
+
if ((0, kizunajs.isRouteDefinition)(node)) result[key] = buildRouteFn(node, config);
|
|
239
|
+
else if (node && typeof node === "object") result[key] = buildClientTree(node, config);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Folds the request context headers into `baseHeaders`, so every request
|
|
245
|
+
* carries them. An explicit `baseHeaders` entry wins.
|
|
246
|
+
*/
|
|
247
|
+
const withContextHeaders = (config) => {
|
|
248
|
+
const contextHeaders = config.requestContext;
|
|
249
|
+
if (!contextHeaders) return config;
|
|
250
|
+
return {
|
|
251
|
+
...config,
|
|
252
|
+
baseHeaders: {
|
|
253
|
+
...Object.fromEntries(Object.entries(contextHeaders).filter(([, value]) => value !== void 0)),
|
|
254
|
+
...config.baseHeaders ?? {}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Builds the client tree a generated client exposes. The generated file owns the
|
|
260
|
+
* types; this owns the requests.
|
|
261
|
+
*/
|
|
262
|
+
const createGeneratedClient = (routes, config) => buildClientTree(routes, withContextHeaders(config));
|
|
263
|
+
//#endregion
|
|
264
|
+
exports.CLIENT_ROUTE = CLIENT_ROUTE;
|
|
265
|
+
exports.createGeneratedClient = createGeneratedClient;
|
|
266
|
+
Object.defineProperty(exports, "isProblemDetails", {
|
|
267
|
+
enumerable: true,
|
|
268
|
+
get: function() {
|
|
269
|
+
return kizunajs.isProblemDetails;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
Object.defineProperty(exports, "isValidationError", {
|
|
273
|
+
enumerable: true,
|
|
274
|
+
get: function() {
|
|
275
|
+
return kizunajs.isValidationError;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
exports.routeOf = routeOf;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AUTO_GUARD_BRAND, AUTO_RESPONSES_BRAND, ExtractPathParams, HasPathParams, Method, ProblemDetails, RequestContextHeaderInputs, RouteDefinition, RoutePath, Routes, StreamMessageOf, StreamResponseDefinition, ValidationError, ValidationError as ValidationError$1, ValidationErrorFor, isProblemDetails, isValidationError } from "kizunajs";
|
|
3
|
+
|
|
4
|
+
//#region src/client.d.ts
|
|
5
|
+
type ResponseUnion<R extends RouteDefinition> = { [S in keyof R['responses']]: {
|
|
6
|
+
status: S extends number ? S : never;
|
|
7
|
+
body: R['responses'][S] extends z.ZodType ? z.infer<R['responses'][S]> : R['responses'][S] extends StreamResponseDefinition ? AsyncIterable<StreamMessageOf<R['responses'][S]>> : R['responses'][S] extends {
|
|
8
|
+
body: z.ZodType;
|
|
9
|
+
} ? z.infer<R['responses'][S]['body']> : never;
|
|
10
|
+
headers: R['responses'][S] extends {
|
|
11
|
+
headers: z.ZodType;
|
|
12
|
+
} ? z.infer<R['responses'][S]['headers']> : Record<string, string>;
|
|
13
|
+
} }[keyof R['responses']];
|
|
14
|
+
/**
|
|
15
|
+
* The type a caller passes for a body, query, or headers argument, the schema's
|
|
16
|
+
* input type.
|
|
17
|
+
*/
|
|
18
|
+
type ClientPayload<T extends z.ZodType> = z.input<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Path params are typed from the route's `pathParams` schema output when one is declared,
|
|
21
|
+
* mirroring the server-side `HandlerArgs`. This makes refinements like `.brand()` flow to
|
|
22
|
+
* the caller. Falls back to the path template (`:param` → `string`).
|
|
23
|
+
*/
|
|
24
|
+
type ClientParams<R extends RouteDefinition> = R extends {
|
|
25
|
+
pathParams: z.ZodType;
|
|
26
|
+
} ? z.output<R['pathParams']> : ExtractPathParams<R['path']>;
|
|
27
|
+
/**
|
|
28
|
+
* True when the arg can be omitted: its input accepts `{}` or `undefined`.
|
|
29
|
+
*/
|
|
30
|
+
type IsOmittable<Payload> = {} extends Payload ? true : [undefined] extends [Payload] ? true : false;
|
|
31
|
+
/**
|
|
32
|
+
* Everything a caller passes for one route: `params`, `body`, `query`, `headers`,
|
|
33
|
+
* and `fetchOptions`. Each key is present only when the route declares it, and
|
|
34
|
+
* optional when its input accepts `{}` or `undefined`.
|
|
35
|
+
*/
|
|
36
|
+
type ClientArgs<R extends RouteDefinition> = (HasPathParams<R['path']> extends true ? {
|
|
37
|
+
params: ClientParams<R>;
|
|
38
|
+
} : {}) & (R extends {
|
|
39
|
+
body: z.ZodType;
|
|
40
|
+
} ? (ClientPayload<R['body']> extends void ? {} : {
|
|
41
|
+
body: ClientPayload<R['body']>;
|
|
42
|
+
}) : {}) & (R extends {
|
|
43
|
+
query: z.ZodType;
|
|
44
|
+
} ? IsOmittable<ClientPayload<R['query']>> extends true ? {
|
|
45
|
+
query?: ClientPayload<R['query']>;
|
|
46
|
+
} : {
|
|
47
|
+
query: ClientPayload<R['query']>;
|
|
48
|
+
} : {}) & (R extends {
|
|
49
|
+
headers: z.ZodType;
|
|
50
|
+
} ? IsOmittable<ClientPayload<R['headers']>> extends true ? {
|
|
51
|
+
headers?: ClientPayload<R['headers']>;
|
|
52
|
+
} : {
|
|
53
|
+
headers: ClientPayload<R['headers']>;
|
|
54
|
+
} : {
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
}) & {
|
|
57
|
+
fetchOptions?: RequestInit;
|
|
58
|
+
};
|
|
59
|
+
type ValidationErrorResult<Codes extends string> = {
|
|
60
|
+
status: 400;
|
|
61
|
+
body: [Codes] extends [never] ? ValidationError$1 : ValidationErrorFor<Codes>;
|
|
62
|
+
headers: Record<string, string>;
|
|
63
|
+
};
|
|
64
|
+
type HasValidation<R extends RouteDefinition> = R extends {
|
|
65
|
+
body: z.ZodType;
|
|
66
|
+
} ? true : R extends {
|
|
67
|
+
query: z.ZodType;
|
|
68
|
+
} ? true : false;
|
|
69
|
+
/**
|
|
70
|
+
* The statuses the api's auth map put on a guarded route. A declared `403`
|
|
71
|
+
* sits alongside, as a declared `400` does beside the validation error.
|
|
72
|
+
*/
|
|
73
|
+
type AutoStatuses<R extends RouteDefinition> = Extract<typeof AUTO_RESPONSES_BRAND extends keyof R ? NonNullable<R[typeof AUTO_RESPONSES_BRAND]> : never, number>;
|
|
74
|
+
/**
|
|
75
|
+
* The api's `guardSchema`, or plain Problem Details when it declares none.
|
|
76
|
+
*/
|
|
77
|
+
type AutoBody<R extends RouteDefinition> = typeof AUTO_GUARD_BRAND extends keyof R ? NonNullable<R[typeof AUTO_GUARD_BRAND]> : ProblemDetails;
|
|
78
|
+
type AutoErrorResult<R extends RouteDefinition> = { [Status in AutoStatuses<R>]: {
|
|
79
|
+
status: Status;
|
|
80
|
+
body: AutoBody<R>;
|
|
81
|
+
headers: Record<string, string>;
|
|
82
|
+
} }[AutoStatuses<R>];
|
|
83
|
+
/**
|
|
84
|
+
* Every response one route can produce, as a union discriminated on `status`.
|
|
85
|
+
* Routes with a `body` or `query` schema also carry the automatic `400`, and
|
|
86
|
+
* guarded routes the `401` and `403`.
|
|
87
|
+
*/
|
|
88
|
+
type ClientResponse<R extends RouteDefinition, Codes extends string = never> = (HasValidation<R> extends true ? ResponseUnion<R> | ValidationErrorResult<Codes> : ResponseUnion<R>) | AutoErrorResult<R>;
|
|
89
|
+
type ClientFn<R extends RouteDefinition, Codes extends string> = {} extends ClientArgs<R> ? (args?: ClientArgs<R>) => Promise<ClientResponse<R, Codes>> : (args: ClientArgs<R>) => Promise<ClientResponse<R, Codes>>;
|
|
90
|
+
type Client<T extends Routes, Codes extends string = never> = { [K in keyof T]: T[K] extends RouteDefinition ? ClientFn<T[K], Codes> : T[K] extends Routes ? Client<T[K], Codes> : never };
|
|
91
|
+
type UnionToIntersection<Union> = (Union extends unknown ? (distributed: Union) => void : never) extends ((intersected: infer Intersection) => void) ? Intersection : never;
|
|
92
|
+
/**
|
|
93
|
+
* Every header input the api's request context declares, flattened. Set
|
|
94
|
+
* once on the client under `requestContext` and sent with every request.
|
|
95
|
+
*/
|
|
96
|
+
type ContextHeaderInputs<Declarations> = string extends keyof Declarations ? {} : [keyof Declarations] extends [never] ? {} : UnionToIntersection<{ [Name in keyof Declarations]: RequestContextHeaderInputs<Declarations[Name]> }[keyof Declarations]> extends infer Merged ? { [Key in keyof Merged]: Merged[Key] } : never;
|
|
97
|
+
/**
|
|
98
|
+
* The request `onRequest` is handed, on its way out. Setting a header on
|
|
99
|
+
* `headers` sets it on the request that is sent.
|
|
100
|
+
*/
|
|
101
|
+
interface OutgoingRequest {
|
|
102
|
+
url: string;
|
|
103
|
+
method: string;
|
|
104
|
+
headers: Headers;
|
|
105
|
+
route: RouteDefinition;
|
|
106
|
+
}
|
|
107
|
+
interface ClientConfig {
|
|
108
|
+
baseUrl: string;
|
|
109
|
+
baseHeaders?: Record<string, string>;
|
|
110
|
+
credentials?: RequestCredentials;
|
|
111
|
+
fetch?: typeof fetch;
|
|
112
|
+
onRequest?: (request: OutgoingRequest) => void | Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Registry-global: what a client method was built from. Anything wrapping a
|
|
116
|
+
* client reads the route here rather than being handed the api a second time.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* One method on a generated client: the call itself, carrying the method it
|
|
120
|
+
* sends and whether that response streams. A wrapper reads those off the client
|
|
121
|
+
* rather than being handed the routes a second time.
|
|
122
|
+
*/
|
|
123
|
+
type ClientMethod<Method extends string, Streams extends boolean, Args, Result> = ({} extends Args ? (args?: Args) => Promise<Result> : (args: Args) => Promise<Result>) & {
|
|
124
|
+
readonly [CLIENT_ROUTE]?: {
|
|
125
|
+
readonly method: Method;
|
|
126
|
+
readonly streams: Streams;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
declare const CLIENT_ROUTE: unique symbol;
|
|
130
|
+
/**
|
|
131
|
+
* The route a client method answers, or `undefined` for anything that is not
|
|
132
|
+
* one.
|
|
133
|
+
*/
|
|
134
|
+
declare const routeOf: (value: unknown) => RouteDefinition | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* What a generated client knows about one response: nothing for a body it
|
|
137
|
+
* parses as JSON, the media type for one it streams.
|
|
138
|
+
*/
|
|
139
|
+
type GeneratedResponse = Record<string, never> | {
|
|
140
|
+
stream: {
|
|
141
|
+
contentType?: string;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* One route in a generated client's table: how to reach it, and enough about
|
|
146
|
+
* each response to read the body.
|
|
147
|
+
*/
|
|
148
|
+
interface GeneratedRoute {
|
|
149
|
+
method: Method;
|
|
150
|
+
path: RoutePath;
|
|
151
|
+
contentType?: string;
|
|
152
|
+
responses: Record<number, GeneratedResponse>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The route table a generated client carries, nested the way the client is.
|
|
156
|
+
*/
|
|
157
|
+
interface GeneratedRoutes {
|
|
158
|
+
[key: string]: GeneratedRoutes | GeneratedRoute;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Builds the client tree a generated client exposes. The generated file owns the
|
|
162
|
+
* types; this owns the requests.
|
|
163
|
+
*/
|
|
164
|
+
declare const createGeneratedClient: (routes: GeneratedRoutes, config: ClientConfig) => Record<string, unknown>;
|
|
165
|
+
//#endregion
|
|
166
|
+
export { CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { AUTO_GUARD_BRAND, AUTO_RESPONSES_BRAND, ExtractPathParams, HasPathParams, Method, ProblemDetails, RequestContextHeaderInputs, RouteDefinition, RoutePath, Routes, StreamMessageOf, StreamResponseDefinition, ValidationError, ValidationError as ValidationError$1, ValidationErrorFor, isProblemDetails, isValidationError } from "kizunajs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/client.d.ts
|
|
5
|
+
type ResponseUnion<R extends RouteDefinition> = { [S in keyof R['responses']]: {
|
|
6
|
+
status: S extends number ? S : never;
|
|
7
|
+
body: R['responses'][S] extends z.ZodType ? z.infer<R['responses'][S]> : R['responses'][S] extends StreamResponseDefinition ? AsyncIterable<StreamMessageOf<R['responses'][S]>> : R['responses'][S] extends {
|
|
8
|
+
body: z.ZodType;
|
|
9
|
+
} ? z.infer<R['responses'][S]['body']> : never;
|
|
10
|
+
headers: R['responses'][S] extends {
|
|
11
|
+
headers: z.ZodType;
|
|
12
|
+
} ? z.infer<R['responses'][S]['headers']> : Record<string, string>;
|
|
13
|
+
} }[keyof R['responses']];
|
|
14
|
+
/**
|
|
15
|
+
* The type a caller passes for a body, query, or headers argument, the schema's
|
|
16
|
+
* input type.
|
|
17
|
+
*/
|
|
18
|
+
type ClientPayload<T extends z.ZodType> = z.input<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Path params are typed from the route's `pathParams` schema output when one is declared,
|
|
21
|
+
* mirroring the server-side `HandlerArgs`. This makes refinements like `.brand()` flow to
|
|
22
|
+
* the caller. Falls back to the path template (`:param` → `string`).
|
|
23
|
+
*/
|
|
24
|
+
type ClientParams<R extends RouteDefinition> = R extends {
|
|
25
|
+
pathParams: z.ZodType;
|
|
26
|
+
} ? z.output<R['pathParams']> : ExtractPathParams<R['path']>;
|
|
27
|
+
/**
|
|
28
|
+
* True when the arg can be omitted: its input accepts `{}` or `undefined`.
|
|
29
|
+
*/
|
|
30
|
+
type IsOmittable<Payload> = {} extends Payload ? true : [undefined] extends [Payload] ? true : false;
|
|
31
|
+
/**
|
|
32
|
+
* Everything a caller passes for one route: `params`, `body`, `query`, `headers`,
|
|
33
|
+
* and `fetchOptions`. Each key is present only when the route declares it, and
|
|
34
|
+
* optional when its input accepts `{}` or `undefined`.
|
|
35
|
+
*/
|
|
36
|
+
type ClientArgs<R extends RouteDefinition> = (HasPathParams<R['path']> extends true ? {
|
|
37
|
+
params: ClientParams<R>;
|
|
38
|
+
} : {}) & (R extends {
|
|
39
|
+
body: z.ZodType;
|
|
40
|
+
} ? (ClientPayload<R['body']> extends void ? {} : {
|
|
41
|
+
body: ClientPayload<R['body']>;
|
|
42
|
+
}) : {}) & (R extends {
|
|
43
|
+
query: z.ZodType;
|
|
44
|
+
} ? IsOmittable<ClientPayload<R['query']>> extends true ? {
|
|
45
|
+
query?: ClientPayload<R['query']>;
|
|
46
|
+
} : {
|
|
47
|
+
query: ClientPayload<R['query']>;
|
|
48
|
+
} : {}) & (R extends {
|
|
49
|
+
headers: z.ZodType;
|
|
50
|
+
} ? IsOmittable<ClientPayload<R['headers']>> extends true ? {
|
|
51
|
+
headers?: ClientPayload<R['headers']>;
|
|
52
|
+
} : {
|
|
53
|
+
headers: ClientPayload<R['headers']>;
|
|
54
|
+
} : {
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
}) & {
|
|
57
|
+
fetchOptions?: RequestInit;
|
|
58
|
+
};
|
|
59
|
+
type ValidationErrorResult<Codes extends string> = {
|
|
60
|
+
status: 400;
|
|
61
|
+
body: [Codes] extends [never] ? ValidationError$1 : ValidationErrorFor<Codes>;
|
|
62
|
+
headers: Record<string, string>;
|
|
63
|
+
};
|
|
64
|
+
type HasValidation<R extends RouteDefinition> = R extends {
|
|
65
|
+
body: z.ZodType;
|
|
66
|
+
} ? true : R extends {
|
|
67
|
+
query: z.ZodType;
|
|
68
|
+
} ? true : false;
|
|
69
|
+
/**
|
|
70
|
+
* The statuses the api's auth map put on a guarded route. A declared `403`
|
|
71
|
+
* sits alongside, as a declared `400` does beside the validation error.
|
|
72
|
+
*/
|
|
73
|
+
type AutoStatuses<R extends RouteDefinition> = Extract<typeof AUTO_RESPONSES_BRAND extends keyof R ? NonNullable<R[typeof AUTO_RESPONSES_BRAND]> : never, number>;
|
|
74
|
+
/**
|
|
75
|
+
* The api's `guardSchema`, or plain Problem Details when it declares none.
|
|
76
|
+
*/
|
|
77
|
+
type AutoBody<R extends RouteDefinition> = typeof AUTO_GUARD_BRAND extends keyof R ? NonNullable<R[typeof AUTO_GUARD_BRAND]> : ProblemDetails;
|
|
78
|
+
type AutoErrorResult<R extends RouteDefinition> = { [Status in AutoStatuses<R>]: {
|
|
79
|
+
status: Status;
|
|
80
|
+
body: AutoBody<R>;
|
|
81
|
+
headers: Record<string, string>;
|
|
82
|
+
} }[AutoStatuses<R>];
|
|
83
|
+
/**
|
|
84
|
+
* Every response one route can produce, as a union discriminated on `status`.
|
|
85
|
+
* Routes with a `body` or `query` schema also carry the automatic `400`, and
|
|
86
|
+
* guarded routes the `401` and `403`.
|
|
87
|
+
*/
|
|
88
|
+
type ClientResponse<R extends RouteDefinition, Codes extends string = never> = (HasValidation<R> extends true ? ResponseUnion<R> | ValidationErrorResult<Codes> : ResponseUnion<R>) | AutoErrorResult<R>;
|
|
89
|
+
type ClientFn<R extends RouteDefinition, Codes extends string> = {} extends ClientArgs<R> ? (args?: ClientArgs<R>) => Promise<ClientResponse<R, Codes>> : (args: ClientArgs<R>) => Promise<ClientResponse<R, Codes>>;
|
|
90
|
+
type Client<T extends Routes, Codes extends string = never> = { [K in keyof T]: T[K] extends RouteDefinition ? ClientFn<T[K], Codes> : T[K] extends Routes ? Client<T[K], Codes> : never };
|
|
91
|
+
type UnionToIntersection<Union> = (Union extends unknown ? (distributed: Union) => void : never) extends ((intersected: infer Intersection) => void) ? Intersection : never;
|
|
92
|
+
/**
|
|
93
|
+
* Every header input the api's request context declares, flattened. Set
|
|
94
|
+
* once on the client under `requestContext` and sent with every request.
|
|
95
|
+
*/
|
|
96
|
+
type ContextHeaderInputs<Declarations> = string extends keyof Declarations ? {} : [keyof Declarations] extends [never] ? {} : UnionToIntersection<{ [Name in keyof Declarations]: RequestContextHeaderInputs<Declarations[Name]> }[keyof Declarations]> extends infer Merged ? { [Key in keyof Merged]: Merged[Key] } : never;
|
|
97
|
+
/**
|
|
98
|
+
* The request `onRequest` is handed, on its way out. Setting a header on
|
|
99
|
+
* `headers` sets it on the request that is sent.
|
|
100
|
+
*/
|
|
101
|
+
interface OutgoingRequest {
|
|
102
|
+
url: string;
|
|
103
|
+
method: string;
|
|
104
|
+
headers: Headers;
|
|
105
|
+
route: RouteDefinition;
|
|
106
|
+
}
|
|
107
|
+
interface ClientConfig {
|
|
108
|
+
baseUrl: string;
|
|
109
|
+
baseHeaders?: Record<string, string>;
|
|
110
|
+
credentials?: RequestCredentials;
|
|
111
|
+
fetch?: typeof fetch;
|
|
112
|
+
onRequest?: (request: OutgoingRequest) => void | Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Registry-global: what a client method was built from. Anything wrapping a
|
|
116
|
+
* client reads the route here rather than being handed the api a second time.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* One method on a generated client: the call itself, carrying the method it
|
|
120
|
+
* sends and whether that response streams. A wrapper reads those off the client
|
|
121
|
+
* rather than being handed the routes a second time.
|
|
122
|
+
*/
|
|
123
|
+
type ClientMethod<Method extends string, Streams extends boolean, Args, Result> = ({} extends Args ? (args?: Args) => Promise<Result> : (args: Args) => Promise<Result>) & {
|
|
124
|
+
readonly [CLIENT_ROUTE]?: {
|
|
125
|
+
readonly method: Method;
|
|
126
|
+
readonly streams: Streams;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
declare const CLIENT_ROUTE: unique symbol;
|
|
130
|
+
/**
|
|
131
|
+
* The route a client method answers, or `undefined` for anything that is not
|
|
132
|
+
* one.
|
|
133
|
+
*/
|
|
134
|
+
declare const routeOf: (value: unknown) => RouteDefinition | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* What a generated client knows about one response: nothing for a body it
|
|
137
|
+
* parses as JSON, the media type for one it streams.
|
|
138
|
+
*/
|
|
139
|
+
type GeneratedResponse = Record<string, never> | {
|
|
140
|
+
stream: {
|
|
141
|
+
contentType?: string;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* One route in a generated client's table: how to reach it, and enough about
|
|
146
|
+
* each response to read the body.
|
|
147
|
+
*/
|
|
148
|
+
interface GeneratedRoute {
|
|
149
|
+
method: Method;
|
|
150
|
+
path: RoutePath;
|
|
151
|
+
contentType?: string;
|
|
152
|
+
responses: Record<number, GeneratedResponse>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The route table a generated client carries, nested the way the client is.
|
|
156
|
+
*/
|
|
157
|
+
interface GeneratedRoutes {
|
|
158
|
+
[key: string]: GeneratedRoutes | GeneratedRoute;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Builds the client tree a generated client exposes. The generated file owns the
|
|
162
|
+
* types; this owns the requests.
|
|
163
|
+
*/
|
|
164
|
+
declare const createGeneratedClient: (routes: GeneratedRoutes, config: ClientConfig) => Record<string, unknown>;
|
|
165
|
+
//#endregion
|
|
166
|
+
export { CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };
|