@kaito-http/core 3.0.3 → 4.0.0-beta.10

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,12 @@
1
+ // src/router/router.ts
2
+ import { z } from "zod";
3
+ import {
4
+ createDocument
5
+ } from "zod-openapi";
6
+ import "zod-openapi/extend";
7
+
1
8
  // src/error.ts
2
9
  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
10
  static maybe(maybeError) {
8
11
  if (maybeError instanceof Error) {
9
12
  return maybeError;
@@ -13,56 +16,39 @@ var WrappedError = class _WrappedError extends Error {
13
16
  static from(data) {
14
17
  return new _WrappedError(data);
15
18
  }
19
+ data;
20
+ constructor(data) {
21
+ super("Something was thrown, but it was not an instance of Error, so a WrappedError was created.");
22
+ this.data = data;
23
+ }
16
24
  };
17
25
  var KaitoError = class extends Error {
26
+ status;
18
27
  constructor(status, message) {
19
28
  super(message);
20
29
  this.status = status;
21
30
  }
22
31
  };
23
32
 
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
- }
37
- }
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
- }
43
- return response;
44
- };
45
- }
46
-
47
33
  // src/head.ts
48
34
  var KaitoHead = class {
49
- _headers;
50
- _status;
35
+ #headers;
36
+ #status;
51
37
  constructor() {
52
- this._headers = null;
53
- this._status = 200;
38
+ this.#headers = null;
39
+ this.#status = 200;
54
40
  }
55
41
  get headers() {
56
- if (this._headers === null) {
57
- this._headers = new Headers();
42
+ if (this.#headers === null) {
43
+ this.#headers = new Headers();
58
44
  }
59
- return this._headers;
45
+ return this.#headers;
60
46
  }
61
47
  status(status) {
62
48
  if (status === void 0) {
63
- return this._status;
49
+ return this.#status;
64
50
  }
65
- this._status = status;
51
+ this.#status = status;
66
52
  return this;
67
53
  }
68
54
  /**
@@ -72,10 +58,10 @@ var KaitoHead = class {
72
58
  */
73
59
  toResponse(body) {
74
60
  const init = {
75
- status: this._status
61
+ status: this.#status
76
62
  };
77
- if (this._headers) {
78
- init.headers = this._headers;
63
+ if (this.#headers) {
64
+ init.headers = this.#headers;
79
65
  }
80
66
  return Response.json(body, init);
81
67
  }
@@ -83,7 +69,7 @@ var KaitoHead = class {
83
69
  * Whether this KaitoHead instance has been touched/modified
84
70
  */
85
71
  get touched() {
86
- return this._status !== 200 || this._headers !== null;
72
+ return this.#status !== 200 || this.#headers !== null;
87
73
  }
88
74
  };
89
75
 
@@ -127,102 +113,102 @@ var KaitoRequest = class {
127
113
 
128
114
  // src/util.ts
129
115
  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
116
 
142
117
  // src/router/router.ts
143
118
  var Router = class _Router {
144
119
  state;
145
- static create = () => new _Router({
120
+ static create = (config) => new _Router({
146
121
  through: async (context) => context,
147
- routes: /* @__PURE__ */ new Set()
122
+ routes: /* @__PURE__ */ new Set(),
123
+ config,
124
+ paramsSchema: null
148
125
  });
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;
126
+ constructor(state) {
127
+ this.state = state;
163
128
  }
164
129
  get routes() {
165
130
  return this.state.routes;
166
131
  }
167
132
  add = (method, path, route) => {
168
133
  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
134
  ...typeof route === "object" ? route : { run: route },
171
135
  method,
172
136
  path,
173
- through: this.state.through
137
+ router: this
174
138
  };
175
139
  return new _Router({
176
140
  ...this.state,
177
141
  routes: /* @__PURE__ */ new Set([...this.state.routes, merged])
178
142
  });
179
143
  };
144
+ params = (spec) => new _Router({
145
+ ...this.state,
146
+ paramsSchema: z.object(spec)
147
+ });
180
148
  merge = (pathPrefix, other) => {
181
149
  const newRoutes = [...other.state.routes].map((route) => ({
182
150
  ...route,
183
- path: `${pathPrefix}${route.path}`
151
+ // handle pathPrefix = / & route.path = / case causing //
152
+ // we intentionally are replacing on the joining path and not the pathPrefix, in case of
153
+ // /named -> merged to -> / causing /named/ not /named
154
+ path: `${pathPrefix}${route.path === "/" ? "" : route.path}`
184
155
  }));
185
156
  return new _Router({
186
157
  ...this.state,
187
158
  routes: /* @__PURE__ */ new Set([...this.state.routes, ...newRoutes])
188
159
  });
189
160
  };
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());
161
+ static getFindRoute = (routes) => (method, path) => {
162
+ const params = {};
163
+ const pathParts = path.split("/").filter(Boolean);
164
+ const methodRoutes = routes.get(method);
165
+ if (!methodRoutes) return {};
166
+ for (const [routePath, route] of methodRoutes) {
167
+ const routeParts = routePath.split("/").filter(Boolean);
168
+ if (routeParts.length !== pathParts.length) {
169
+ continue;
195
170
  }
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 };
171
+ let matches = true;
172
+ for (let i = 0; i < routeParts.length; i++) {
173
+ const routePart = routeParts[i];
174
+ const pathPart = pathParts[i];
175
+ if (routePart && pathPart && routePart.startsWith(":")) {
176
+ params[routePart.slice(1)] = pathPart;
177
+ } else if (routePart !== pathPart) {
178
+ matches = false;
179
+ break;
218
180
  }
219
181
  }
220
- return { params };
221
- };
222
- return async (req) => {
182
+ if (matches) return { route, params };
183
+ }
184
+ return {};
185
+ };
186
+ static buildQuerySchema = (schema) => {
187
+ const keys = Object.keys(schema);
188
+ return z.instanceof(URLSearchParams).transform((params) => {
189
+ const result = {};
190
+ for (const key of keys) {
191
+ result[key] = params.get(key);
192
+ }
193
+ return result;
194
+ }).pipe(z.object(schema));
195
+ };
196
+ serve = () => {
197
+ const methodToRoutesMap = /* @__PURE__ */ new Map();
198
+ for (const route of this.state.routes) {
199
+ if (!methodToRoutesMap.has(route.method)) {
200
+ methodToRoutesMap.set(route.method, /* @__PURE__ */ new Map());
201
+ }
202
+ methodToRoutesMap.get(route.method).set(route.path, {
203
+ ...route,
204
+ fastQuerySchema: route.query ? _Router.buildQuerySchema(route.query) : void 0
205
+ });
206
+ }
207
+ const findRoute = _Router.getFindRoute(methodToRoutesMap);
208
+ const handle = async (req) => {
223
209
  const url = new URL(req.url);
224
210
  const method = req.method;
225
- const { route, params } = findRoute(method, url.pathname);
211
+ const { route, params: rawParams } = findRoute(method, url.pathname);
226
212
  if (!route) {
227
213
  const body = {
228
214
  success: false,
@@ -234,10 +220,13 @@ var Router = class _Router {
234
220
  const request = new KaitoRequest(url, req);
235
221
  const head = new KaitoHead();
236
222
  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);
223
+ const body = route.body ? await route.body.parseAsync(await req.json()) : void 0;
224
+ const query = route.fastQuerySchema ? await route.fastQuerySchema.parseAsync(url.searchParams) : {};
225
+ const params = route.router.state.paramsSchema ? route.router.state.paramsSchema.parse(rawParams) : rawParams;
226
+ const ctx = await route.router.state.through(
227
+ await this.state.config.getContext?.(request, head) ?? null,
228
+ params
229
+ );
241
230
  const result = await route.run({
242
231
  ctx,
243
232
  body,
@@ -260,8 +249,7 @@ var Router = class _Router {
260
249
  }
261
250
  return head.toResponse({
262
251
  success: true,
263
- data: result,
264
- message: "OK"
252
+ data: result
265
253
  });
266
254
  } catch (e) {
267
255
  const error = WrappedError.maybe(e);
@@ -272,17 +260,129 @@ var Router = class _Router {
272
260
  message: error.message
273
261
  });
274
262
  }
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
- });
263
+ if (!this.state.config.onError) {
264
+ return head.status(500).toResponse({
265
+ success: false,
266
+ data: null,
267
+ message: "Internal Server Error"
268
+ });
269
+ }
270
+ try {
271
+ const { status, message } = await this.state.config.onError(error, request);
272
+ return head.status(status).toResponse({
273
+ success: false,
274
+ data: null,
275
+ message
276
+ });
277
+ } catch (e2) {
278
+ console.error("KAITO - Failed to handle error inside `.onError()`, returning 500 and Internal Server Error");
279
+ console.error(e2);
280
+ return head.status(500).toResponse({
281
+ success: false,
282
+ data: null,
283
+ message: "Internal Server Error"
284
+ });
285
+ }
286
+ }
287
+ };
288
+ return async (request) => {
289
+ if (this.state.config.before) {
290
+ const result = await this.state.config.before(request);
291
+ if (result instanceof Response) {
292
+ if (this.state.config.transform) {
293
+ const transformed = await this.state.config.transform(request, result);
294
+ if (transformed instanceof Response) {
295
+ return result;
296
+ }
297
+ }
298
+ return result;
299
+ }
300
+ }
301
+ const response = await handle(request);
302
+ if (this.state.config.transform) {
303
+ const transformed = await this.state.config.transform(request, response);
304
+ if (transformed instanceof Response) {
305
+ return transformed;
306
+ }
281
307
  }
308
+ return response;
282
309
  };
283
310
  };
284
- method = (method) => (path, route) => {
285
- return this.add(method, path, route);
311
+ openapi = (highLevelSpec) => {
312
+ const OPENAPI_VERSION = "3.0.0";
313
+ const paths = {};
314
+ for (const route of this.state.routes) {
315
+ const path = route.path;
316
+ if (!route.openapi) {
317
+ continue;
318
+ }
319
+ const pathWithColonParamsReplaceWithCurlyBraces = path.replace(/:(\w+)/g, "{$1}");
320
+ if (!paths[pathWithColonParamsReplaceWithCurlyBraces]) {
321
+ paths[pathWithColonParamsReplaceWithCurlyBraces] = {};
322
+ }
323
+ const content = route.openapi.body.type === "json" ? {
324
+ "application/json": {
325
+ schema: z.object({
326
+ success: z.literal(true).openapi({
327
+ type: "boolean",
328
+ enum: [true]
329
+ // Need this as zod-openapi doesn't properly work with literals
330
+ }),
331
+ data: route.openapi.body.schema
332
+ })
333
+ }
334
+ } : {
335
+ "text/event-stream": {
336
+ schema: route.openapi.body.schema
337
+ }
338
+ };
339
+ const item = {
340
+ description: route.openapi?.description ?? "Successful response",
341
+ responses: {
342
+ 200: {
343
+ description: route.openapi?.description ?? "Successful response",
344
+ content
345
+ }
346
+ }
347
+ };
348
+ if (route.body) {
349
+ item.requestBody = {
350
+ content: {
351
+ "application/json": { schema: route.body }
352
+ }
353
+ };
354
+ }
355
+ const params = {};
356
+ if (route.query) {
357
+ params.query = z.object(route.query);
358
+ }
359
+ const urlParams = path.match(/:(\w+)/g);
360
+ if (urlParams) {
361
+ const pathParams = {};
362
+ for (const param of urlParams) {
363
+ pathParams[param.slice(1)] = z.string();
364
+ }
365
+ params.path = z.object(pathParams);
366
+ }
367
+ item.requestParams = params;
368
+ paths[pathWithColonParamsReplaceWithCurlyBraces][route.method.toLowerCase()] = item;
369
+ }
370
+ const doc = createDocument({
371
+ openapi: OPENAPI_VERSION,
372
+ paths,
373
+ ...highLevelSpec,
374
+ servers: Object.entries(highLevelSpec.servers ?? {}).map((entry) => {
375
+ const [url, description] = entry;
376
+ return {
377
+ url,
378
+ description
379
+ };
380
+ })
381
+ });
382
+ return this.get("/openapi.json", () => Response.json(doc));
383
+ };
384
+ method = (method) => {
385
+ return (path, route) => this.add(method, path, route);
286
386
  };
287
387
  get = this.method("GET");
288
388
  post = this.method("POST");
@@ -294,18 +394,21 @@ var Router = class _Router {
294
394
  through = (through) => {
295
395
  return new _Router({
296
396
  ...this.state,
297
- through: async (context) => through(await this.state.through(context))
397
+ through: async (context, params) => await through(await this.state.through(context, params), params)
298
398
  });
299
399
  };
300
400
  };
401
+
402
+ // src/create.ts
403
+ function create(config = {}) {
404
+ return Router.create(config);
405
+ }
301
406
  export {
302
407
  KaitoError,
303
408
  KaitoHead,
304
409
  KaitoRequest,
305
410
  Router,
306
411
  WrappedError,
307
- createKaitoHandler,
308
- createUtilities,
309
- isNodeLikeDev,
310
- parsable
412
+ create,
413
+ isNodeLikeDev
311
414
  };
@@ -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.3",
4
- "type": "module",
3
+ "version": "4.0.0-beta.10",
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
  }