@kaito-http/core 3.0.2 → 4.0.0-beta.2

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.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/handler.ts
25
- function createKaitoHandler(config) {
26
- const handle = config.router.freeze(config);
27
- return async (request) => {
28
- if (config.before) {
29
- const result = await config.before(request);
30
- if (result instanceof Response) {
31
- if (config.transform) {
32
- const result2 = await config.transform(request, result);
33
- if (result2 instanceof Response) return result;
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
- const response = await handle(request);
39
- if (config.transform) {
40
- const result = await config.transform(request, response);
41
- if (result instanceof Response) return result;
42
+ return this.#headers;
43
+ }
44
+ status(status) {
45
+ if (status === void 0) {
46
+ return this.#status;
42
47
  }
43
- return response;
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
- static parseQuery(schema, url) {
150
- if (!schema) {
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
- freeze = (server) => {
191
- const routes = /* @__PURE__ */ new Map();
192
- for (const route of this.state.routes) {
193
- if (!routes.has(route.path)) {
194
- routes.set(route.path, /* @__PURE__ */ new Map());
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
- routes.get(route.path).set(route.method, route);
197
- }
198
- const findRoute = (method, path) => {
199
- const params = {};
200
- const pathParts = path.split("/").filter(Boolean);
201
- for (const [routePath, methodHandlers] of routes) {
202
- const routeParts = routePath.split("/").filter(Boolean);
203
- if (routeParts.length !== pathParts.length) continue;
204
- let matches = true;
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 async (req) => {
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.parse(await req.json()) : void 0;
238
- const query = _Router.parseQuery(route.query, url);
239
- const rootCtx = await server.getContext(request, head);
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,17 +245,117 @@ var Router = class _Router {
272
245
  message: error.message
273
246
  });
274
247
  }
275
- const { status, message } = await server.onError({ error, req: request }).catch(() => ({ status: 500, message: "Internal Server Error" }));
276
- return head.status(status).toResponse({
277
- success: false,
278
- data: null,
279
- message
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
  };
284
- method = (method) => (path, route) => {
285
- return this.add(method, path, route);
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
+ };
357
+ method = (method) => {
358
+ return (path, route) => this.add(method, path, route);
286
359
  };
287
360
  get = this.method("GET");
288
361
  post = this.method("POST");
@@ -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
- createKaitoHandler,
307
- createUtilities,
308
- isNodeLikeDev,
309
- parsable
384
+ create,
385
+ isNodeLikeDev
310
386
  };
@@ -38,10 +38,8 @@ var KaitoSSEResponse = class extends Response {
38
38
  headers
39
39
  });
40
40
  }
41
- async *[Symbol.asyncIterator]() {
42
- for await (const chunk of this.body) {
43
- yield chunk;
44
- }
41
+ get [Symbol.toStringTag]() {
42
+ return "KaitoSSEResponse";
45
43
  }
46
44
  };
47
45
  function sseEventToString(event) {
@@ -1,6 +1,6 @@
1
1
  declare class KaitoSSEResponse<_T> extends Response {
2
2
  constructor(body: ReadableStream<string>, init?: ResponseInit);
3
- [Symbol.asyncIterator](): AsyncGenerator<Uint8Array<ArrayBufferLike>, void, unknown>;
3
+ get [Symbol.toStringTag](): string;
4
4
  }
5
5
  type SSEEvent<T, E extends string> = ({
6
6
  data: T;
@@ -1,6 +1,6 @@
1
1
  declare class KaitoSSEResponse<_T> extends Response {
2
2
  constructor(body: ReadableStream<string>, init?: ResponseInit);
3
- [Symbol.asyncIterator](): AsyncGenerator<Uint8Array<ArrayBufferLike>, void, unknown>;
3
+ get [Symbol.toStringTag](): string;
4
4
  }
5
5
  type SSEEvent<T, E extends string> = ({
6
6
  data: T;
@@ -10,10 +10,8 @@ var KaitoSSEResponse = class extends Response {
10
10
  headers
11
11
  });
12
12
  }
13
- async *[Symbol.asyncIterator]() {
14
- for await (const chunk of this.body) {
15
- yield chunk;
16
- }
13
+ get [Symbol.toStringTag]() {
14
+ return "KaitoSSEResponse";
17
15
  }
18
16
  };
19
17
  function sseEventToString(event) {
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@kaito-http/core",
3
- "version": "3.0.2",
4
- "type": "module",
3
+ "version": "4.0.0-beta.2",
5
4
  "author": "Alistair Smith <hi@alistair.sh>",
6
- "description": "Functional HTTP Framework for TypeScript",
7
- "scripts": {
8
- "build": "tsup",
9
- "attw": "attw --profile node16 --pack .",
10
- "test": "node --test --import=tsx ./src/**/*.test.ts"
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
- "devDependencies": {
36
- "@arethetypeswrong/cli": "^0.17.2",
37
- "@types/node": "^22.10.2",
38
- "tsup": "^8.3.5",
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
- "files": [
42
- "package.json",
43
- "README.md",
44
- "dist"
45
- ],
46
- "bugs": {
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
  }