@kaito-http/core 3.0.2 → 4.0.0-beta.1
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/dist/index.cjs +220 -146
- package/dist/index.d.cts +94 -79
- package/dist/index.d.ts +94 -79
- package/dist/index.js +218 -142
- package/dist/stream/stream.cjs +0 -5
- package/dist/stream/stream.d.cts +0 -1
- package/dist/stream/stream.d.ts +0 -1
- package/dist/stream/stream.js +0 -5
- package/package.json +25 -20
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
// src/router/router.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createDocument } from "zod-openapi";
|
|
4
|
+
|
|
1
5
|
// src/error.ts
|
|
2
6
|
var WrappedError = class _WrappedError extends Error {
|
|
3
|
-
constructor(data) {
|
|
4
|
-
super("Something was thrown, but it was not an instance of Error, so a WrappedError was created.");
|
|
5
|
-
this.data = data;
|
|
6
|
-
}
|
|
7
7
|
static maybe(maybeError) {
|
|
8
8
|
if (maybeError instanceof Error) {
|
|
9
9
|
return maybeError;
|
|
@@ -13,36 +13,62 @@ var WrappedError = class _WrappedError extends Error {
|
|
|
13
13
|
static from(data) {
|
|
14
14
|
return new _WrappedError(data);
|
|
15
15
|
}
|
|
16
|
+
data;
|
|
17
|
+
constructor(data) {
|
|
18
|
+
super("Something was thrown, but it was not an instance of Error, so a WrappedError was created.");
|
|
19
|
+
this.data = data;
|
|
20
|
+
}
|
|
16
21
|
};
|
|
17
22
|
var KaitoError = class extends Error {
|
|
23
|
+
status;
|
|
18
24
|
constructor(status, message) {
|
|
19
25
|
super(message);
|
|
20
26
|
this.status = status;
|
|
21
27
|
}
|
|
22
28
|
};
|
|
23
29
|
|
|
24
|
-
// src/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return result;
|
|
36
|
-
}
|
|
30
|
+
// src/head.ts
|
|
31
|
+
var KaitoHead = class {
|
|
32
|
+
#headers;
|
|
33
|
+
#status;
|
|
34
|
+
constructor() {
|
|
35
|
+
this.#headers = null;
|
|
36
|
+
this.#status = 200;
|
|
37
|
+
}
|
|
38
|
+
get headers() {
|
|
39
|
+
if (this.#headers === null) {
|
|
40
|
+
this.#headers = new Headers();
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
return this.#headers;
|
|
43
|
+
}
|
|
44
|
+
status(status) {
|
|
45
|
+
if (status === void 0) {
|
|
46
|
+
return this.#status;
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
48
|
+
this.#status = status;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Turn this KaitoHead instance into a Response instance
|
|
53
|
+
* @param body The Kaito JSON format to be sent as the response body
|
|
54
|
+
* @returns A Response instance, ready to be sent
|
|
55
|
+
*/
|
|
56
|
+
toResponse(body) {
|
|
57
|
+
const init = {
|
|
58
|
+
status: this.#status
|
|
59
|
+
};
|
|
60
|
+
if (this.#headers) {
|
|
61
|
+
init.headers = this.#headers;
|
|
62
|
+
}
|
|
63
|
+
return Response.json(body, init);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether this KaitoHead instance has been touched/modified
|
|
67
|
+
*/
|
|
68
|
+
get touched() {
|
|
69
|
+
return this.#status !== 200 || this.#headers !== null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
46
72
|
|
|
47
73
|
// src/request.ts
|
|
48
74
|
var KaitoRequest = class {
|
|
@@ -82,91 +108,25 @@ var KaitoRequest = class {
|
|
|
82
108
|
}
|
|
83
109
|
};
|
|
84
110
|
|
|
85
|
-
// src/head.ts
|
|
86
|
-
var KaitoHead = class {
|
|
87
|
-
_headers;
|
|
88
|
-
_status;
|
|
89
|
-
constructor() {
|
|
90
|
-
this._headers = null;
|
|
91
|
-
this._status = 200;
|
|
92
|
-
}
|
|
93
|
-
get headers() {
|
|
94
|
-
if (this._headers === null) {
|
|
95
|
-
this._headers = new Headers();
|
|
96
|
-
}
|
|
97
|
-
return this._headers;
|
|
98
|
-
}
|
|
99
|
-
status(status) {
|
|
100
|
-
if (status === void 0) {
|
|
101
|
-
return this._status;
|
|
102
|
-
}
|
|
103
|
-
this._status = status;
|
|
104
|
-
return this;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Turn this KaitoHead instance into a Response instance
|
|
108
|
-
* @param body The Kaito JSON format to be sent as the response body
|
|
109
|
-
* @returns A Response instance, ready to be sent
|
|
110
|
-
*/
|
|
111
|
-
toResponse(body) {
|
|
112
|
-
const init = {
|
|
113
|
-
status: this._status
|
|
114
|
-
};
|
|
115
|
-
if (this._headers) {
|
|
116
|
-
init.headers = this._headers;
|
|
117
|
-
}
|
|
118
|
-
return Response.json(body, init);
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Whether this KaitoHead instance has been touched/modified
|
|
122
|
-
*/
|
|
123
|
-
get touched() {
|
|
124
|
-
return this._status !== 200 || this._headers !== null;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
111
|
// src/util.ts
|
|
129
112
|
var isNodeLikeDev = typeof process !== "undefined" && typeof process.env !== "undefined" && process.env.NODE_ENV === "development";
|
|
130
|
-
function createUtilities(getContext) {
|
|
131
|
-
return {
|
|
132
|
-
getContext,
|
|
133
|
-
router: () => Router.create()
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function parsable(parse) {
|
|
137
|
-
return {
|
|
138
|
-
parse
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
113
|
|
|
142
114
|
// src/router/router.ts
|
|
143
115
|
var Router = class _Router {
|
|
144
116
|
state;
|
|
145
|
-
static create = () => new _Router({
|
|
117
|
+
static create = (config) => new _Router({
|
|
146
118
|
through: async (context) => context,
|
|
147
|
-
routes: /* @__PURE__ */ new Set()
|
|
119
|
+
routes: /* @__PURE__ */ new Set(),
|
|
120
|
+
config
|
|
148
121
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return {};
|
|
152
|
-
}
|
|
153
|
-
const result = {};
|
|
154
|
-
for (const key in schema) {
|
|
155
|
-
if (!schema.hasOwnProperty(key)) continue;
|
|
156
|
-
const value = url.searchParams.get(key);
|
|
157
|
-
result[key] = schema[key].parse(value);
|
|
158
|
-
}
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
constructor(options) {
|
|
162
|
-
this.state = options;
|
|
122
|
+
constructor(state) {
|
|
123
|
+
this.state = state;
|
|
163
124
|
}
|
|
164
125
|
get routes() {
|
|
165
126
|
return this.state.routes;
|
|
166
127
|
}
|
|
167
128
|
add = (method, path, route) => {
|
|
168
129
|
const merged = {
|
|
169
|
-
// TODO: Ideally fix the typing here, but this will be replaced in Kaito v4 where all routes must return a Response (which we can type)
|
|
170
130
|
...typeof route === "object" ? route : { run: route },
|
|
171
131
|
method,
|
|
172
132
|
path,
|
|
@@ -187,39 +147,54 @@ var Router = class _Router {
|
|
|
187
147
|
routes: /* @__PURE__ */ new Set([...this.state.routes, ...newRoutes])
|
|
188
148
|
});
|
|
189
149
|
};
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
150
|
+
static getFindRoute = (routes) => (method, path) => {
|
|
151
|
+
const params = {};
|
|
152
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
153
|
+
const methodRoutes = routes.get(method);
|
|
154
|
+
if (!methodRoutes) return {};
|
|
155
|
+
for (const [routePath, route] of methodRoutes) {
|
|
156
|
+
const routeParts = routePath.split("/").filter(Boolean);
|
|
157
|
+
if (routeParts.length !== pathParts.length) {
|
|
158
|
+
continue;
|
|
195
159
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
for (let i = 0; i < routeParts.length; i++) {
|
|
206
|
-
const routePart = routeParts[i];
|
|
207
|
-
const pathPart = pathParts[i];
|
|
208
|
-
if (routePart && pathPart && routePart.startsWith(":")) {
|
|
209
|
-
params[routePart.slice(1)] = pathPart;
|
|
210
|
-
} else if (routePart !== pathPart) {
|
|
211
|
-
matches = false;
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (matches) {
|
|
216
|
-
const route = methodHandlers.get(method);
|
|
217
|
-
if (route) return { route, params };
|
|
160
|
+
let matches = true;
|
|
161
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
162
|
+
const routePart = routeParts[i];
|
|
163
|
+
const pathPart = pathParts[i];
|
|
164
|
+
if (routePart && pathPart && routePart.startsWith(":")) {
|
|
165
|
+
params[routePart.slice(1)] = pathPart;
|
|
166
|
+
} else if (routePart !== pathPart) {
|
|
167
|
+
matches = false;
|
|
168
|
+
break;
|
|
218
169
|
}
|
|
219
170
|
}
|
|
220
|
-
return { params };
|
|
221
|
-
}
|
|
222
|
-
return
|
|
171
|
+
if (matches) return { route, params };
|
|
172
|
+
}
|
|
173
|
+
return {};
|
|
174
|
+
};
|
|
175
|
+
static buildQuerySchema = (schema) => {
|
|
176
|
+
const keys = Object.keys(schema);
|
|
177
|
+
return z.instanceof(URLSearchParams).transform((params) => {
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
result[key] = params.get(key);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}).pipe(z.object(schema));
|
|
184
|
+
};
|
|
185
|
+
serve = () => {
|
|
186
|
+
const methodToRoutesMap = /* @__PURE__ */ new Map();
|
|
187
|
+
for (const route of this.state.routes) {
|
|
188
|
+
if (!methodToRoutesMap.has(route.method)) {
|
|
189
|
+
methodToRoutesMap.set(route.method, /* @__PURE__ */ new Map());
|
|
190
|
+
}
|
|
191
|
+
methodToRoutesMap.get(route.method).set(route.path, {
|
|
192
|
+
...route,
|
|
193
|
+
fastQuerySchema: route.query ? _Router.buildQuerySchema(route.query) : void 0
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const findRoute = _Router.getFindRoute(methodToRoutesMap);
|
|
197
|
+
const handle = async (req) => {
|
|
223
198
|
const url = new URL(req.url);
|
|
224
199
|
const method = req.method;
|
|
225
200
|
const { route, params } = findRoute(method, url.pathname);
|
|
@@ -234,10 +209,9 @@ var Router = class _Router {
|
|
|
234
209
|
const request = new KaitoRequest(url, req);
|
|
235
210
|
const head = new KaitoHead();
|
|
236
211
|
try {
|
|
237
|
-
const body = route.body ? await route.body.
|
|
238
|
-
const query =
|
|
239
|
-
const
|
|
240
|
-
const ctx = await route.through(rootCtx);
|
|
212
|
+
const body = route.body ? await route.body.parseAsync(await req.json()) : void 0;
|
|
213
|
+
const query = route.fastQuerySchema ? await route.fastQuerySchema.parseAsync(url.searchParams) : {};
|
|
214
|
+
const ctx = await route.through(await this.state.config.getContext?.(request, head) ?? null);
|
|
241
215
|
const result = await route.run({
|
|
242
216
|
ctx,
|
|
243
217
|
body,
|
|
@@ -260,8 +234,7 @@ var Router = class _Router {
|
|
|
260
234
|
}
|
|
261
235
|
return head.toResponse({
|
|
262
236
|
success: true,
|
|
263
|
-
data: result
|
|
264
|
-
message: "OK"
|
|
237
|
+
data: result
|
|
265
238
|
});
|
|
266
239
|
} catch (e) {
|
|
267
240
|
const error = WrappedError.maybe(e);
|
|
@@ -272,15 +245,115 @@ var Router = class _Router {
|
|
|
272
245
|
message: error.message
|
|
273
246
|
});
|
|
274
247
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
248
|
+
if (!this.state.config.onError) {
|
|
249
|
+
return head.status(500).toResponse({
|
|
250
|
+
success: false,
|
|
251
|
+
data: null,
|
|
252
|
+
message: "Internal Server Error"
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const { status, message } = await this.state.config.onError(error, request);
|
|
257
|
+
return head.status(status).toResponse({
|
|
258
|
+
success: false,
|
|
259
|
+
data: null,
|
|
260
|
+
message
|
|
261
|
+
});
|
|
262
|
+
} catch (e2) {
|
|
263
|
+
console.error("KAITO - Failed to handle error inside `.onError()`, returning 500 and Internal Server Error");
|
|
264
|
+
console.error(e2);
|
|
265
|
+
return head.status(500).toResponse({
|
|
266
|
+
success: false,
|
|
267
|
+
data: null,
|
|
268
|
+
message: "Internal Server Error"
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
return async (request) => {
|
|
274
|
+
if (this.state.config.before) {
|
|
275
|
+
const result = await this.state.config.before(request);
|
|
276
|
+
if (result instanceof Response) {
|
|
277
|
+
if (this.state.config.transform) {
|
|
278
|
+
const transformed = await this.state.config.transform(request, result);
|
|
279
|
+
if (transformed instanceof Response) {
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const response = await handle(request);
|
|
287
|
+
if (this.state.config.transform) {
|
|
288
|
+
const transformed = await this.state.config.transform(request, response);
|
|
289
|
+
if (transformed instanceof Response) {
|
|
290
|
+
return transformed;
|
|
291
|
+
}
|
|
281
292
|
}
|
|
293
|
+
return response;
|
|
282
294
|
};
|
|
283
295
|
};
|
|
296
|
+
openapi = (highLevelSpec) => {
|
|
297
|
+
const OPENAPI_VERSION = "3.0.0";
|
|
298
|
+
const paths = {};
|
|
299
|
+
for (const route of this.state.routes) {
|
|
300
|
+
const path = route.path;
|
|
301
|
+
const pathWithColonParamsReplaceWithCurlyBraces = path.replace(/:(\w+)/g, "{$1}");
|
|
302
|
+
if (!paths[pathWithColonParamsReplaceWithCurlyBraces]) {
|
|
303
|
+
paths[pathWithColonParamsReplaceWithCurlyBraces] = {};
|
|
304
|
+
}
|
|
305
|
+
const item = {
|
|
306
|
+
description: route.openapi?.description ?? "Successful response",
|
|
307
|
+
responses: {
|
|
308
|
+
200: {
|
|
309
|
+
description: route.openapi?.description ?? "Successful response",
|
|
310
|
+
...route.openapi ? {
|
|
311
|
+
content: {
|
|
312
|
+
[{
|
|
313
|
+
json: "application/json",
|
|
314
|
+
sse: "text/event-stream"
|
|
315
|
+
}[route.openapi.body.type]]: { schema: route.openapi?.body.schema }
|
|
316
|
+
}
|
|
317
|
+
} : {}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
if (route.body) {
|
|
322
|
+
item.requestBody = {
|
|
323
|
+
content: {
|
|
324
|
+
"application/json": { schema: route.body }
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const params = {};
|
|
329
|
+
if (route.query) {
|
|
330
|
+
params.query = z.object(route.query);
|
|
331
|
+
}
|
|
332
|
+
const urlParams = path.match(/:(\w+)/g);
|
|
333
|
+
if (urlParams) {
|
|
334
|
+
const pathParams = {};
|
|
335
|
+
for (const param of urlParams) {
|
|
336
|
+
pathParams[param.slice(1)] = z.string();
|
|
337
|
+
}
|
|
338
|
+
params.path = z.object(pathParams);
|
|
339
|
+
}
|
|
340
|
+
item.requestParams = params;
|
|
341
|
+
paths[pathWithColonParamsReplaceWithCurlyBraces][route.method.toLowerCase()] = item;
|
|
342
|
+
}
|
|
343
|
+
const doc = createDocument({
|
|
344
|
+
openapi: OPENAPI_VERSION,
|
|
345
|
+
paths,
|
|
346
|
+
...highLevelSpec,
|
|
347
|
+
servers: Object.entries(highLevelSpec.servers ?? {}).map((entry) => {
|
|
348
|
+
const [url, description] = entry;
|
|
349
|
+
return {
|
|
350
|
+
url,
|
|
351
|
+
description
|
|
352
|
+
};
|
|
353
|
+
})
|
|
354
|
+
});
|
|
355
|
+
return this.add("GET", "/openapi.json", async () => Response.json(doc));
|
|
356
|
+
};
|
|
284
357
|
method = (method) => (path, route) => {
|
|
285
358
|
return this.add(method, path, route);
|
|
286
359
|
};
|
|
@@ -294,17 +367,20 @@ var Router = class _Router {
|
|
|
294
367
|
through = (through) => {
|
|
295
368
|
return new _Router({
|
|
296
369
|
...this.state,
|
|
297
|
-
through: async (context) => through(await this.state.through(context))
|
|
370
|
+
through: async (context) => await through(await this.state.through(context))
|
|
298
371
|
});
|
|
299
372
|
};
|
|
300
373
|
};
|
|
374
|
+
|
|
375
|
+
// src/create.ts
|
|
376
|
+
function create(config = {}) {
|
|
377
|
+
return () => Router.create(config);
|
|
378
|
+
}
|
|
301
379
|
export {
|
|
302
380
|
KaitoError,
|
|
303
381
|
KaitoRequest,
|
|
304
382
|
Router,
|
|
305
383
|
WrappedError,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
isNodeLikeDev,
|
|
309
|
-
parsable
|
|
384
|
+
create,
|
|
385
|
+
isNodeLikeDev
|
|
310
386
|
};
|
package/dist/stream/stream.cjs
CHANGED
package/dist/stream/stream.d.cts
CHANGED
package/dist/stream/stream.d.ts
CHANGED
package/dist/stream/stream.js
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaito-http/core",
|
|
3
|
-
"version": "
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "4.0.0-beta.1",
|
|
5
4
|
"author": "Alistair Smith <hi@alistair.sh>",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
5
|
+
"repository": "https://github.com/kaito-http/kaito",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@arethetypeswrong/cli": "^0.17.2",
|
|
8
|
+
"tsup": "^8.3.5",
|
|
9
|
+
"typescript": "^5.7.3"
|
|
11
10
|
},
|
|
12
11
|
"exports": {
|
|
13
12
|
"./package.json": "./package.json",
|
|
@@ -24,26 +23,32 @@
|
|
|
24
23
|
"require": "./dist/cors/cors.cjs"
|
|
25
24
|
}
|
|
26
25
|
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/kaito-http/kaito/issues"
|
|
28
|
+
},
|
|
29
|
+
"description": "Functional HTTP Framework for TypeScript",
|
|
30
|
+
"files": [
|
|
31
|
+
"package.json",
|
|
32
|
+
"README.md",
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
27
35
|
"homepage": "https://github.com/kaito-http/kaito",
|
|
28
|
-
"repository": "https://github.com/kaito-http/kaito",
|
|
29
36
|
"keywords": [
|
|
30
37
|
"typescript",
|
|
31
38
|
"http",
|
|
32
39
|
"framework"
|
|
33
40
|
],
|
|
34
41
|
"license": "MIT",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"typescript": "^5.7.2"
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"attw": "attw --profile node16 --pack .",
|
|
45
|
+
"test": "node --test --import=tsx ./src/**/*.test.ts"
|
|
40
46
|
},
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"url": "https://github.com/kaito-http/kaito/issues"
|
|
47
|
+
"type": "module",
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"zod": "^3.24.1"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"zod-openapi": "^4.2.3"
|
|
48
53
|
}
|
|
49
54
|
}
|