@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.cjs CHANGED
@@ -25,19 +25,18 @@ __export(index_exports, {
25
25
  KaitoRequest: () => KaitoRequest,
26
26
  Router: () => Router,
27
27
  WrappedError: () => WrappedError,
28
- createKaitoHandler: () => createKaitoHandler,
29
- createUtilities: () => createUtilities,
30
- isNodeLikeDev: () => isNodeLikeDev,
31
- parsable: () => parsable
28
+ create: () => create,
29
+ isNodeLikeDev: () => isNodeLikeDev
32
30
  });
33
31
  module.exports = __toCommonJS(index_exports);
34
32
 
33
+ // src/router/router.ts
34
+ var import_zod = require("zod");
35
+ var import_zod_openapi = require("zod-openapi");
36
+ var import_extend = require("zod-openapi/extend");
37
+
35
38
  // src/error.ts
36
39
  var WrappedError = class _WrappedError extends Error {
37
- constructor(data) {
38
- super("Something was thrown, but it was not an instance of Error, so a WrappedError was created.");
39
- this.data = data;
40
- }
41
40
  static maybe(maybeError) {
42
41
  if (maybeError instanceof Error) {
43
42
  return maybeError;
@@ -47,56 +46,39 @@ var WrappedError = class _WrappedError extends Error {
47
46
  static from(data) {
48
47
  return new _WrappedError(data);
49
48
  }
49
+ data;
50
+ constructor(data) {
51
+ super("Something was thrown, but it was not an instance of Error, so a WrappedError was created.");
52
+ this.data = data;
53
+ }
50
54
  };
51
55
  var KaitoError = class extends Error {
56
+ status;
52
57
  constructor(status, message) {
53
58
  super(message);
54
59
  this.status = status;
55
60
  }
56
61
  };
57
62
 
58
- // src/handler.ts
59
- function createKaitoHandler(config) {
60
- const handle = config.router.freeze(config);
61
- return async (request) => {
62
- if (config.before) {
63
- const result = await config.before(request);
64
- if (result instanceof Response) {
65
- if (config.transform) {
66
- const result2 = await config.transform(request, result);
67
- if (result2 instanceof Response) return result;
68
- }
69
- return result;
70
- }
71
- }
72
- const response = await handle(request);
73
- if (config.transform) {
74
- const result = await config.transform(request, response);
75
- if (result instanceof Response) return result;
76
- }
77
- return response;
78
- };
79
- }
80
-
81
63
  // src/head.ts
82
64
  var KaitoHead = class {
83
- _headers;
84
- _status;
65
+ #headers;
66
+ #status;
85
67
  constructor() {
86
- this._headers = null;
87
- this._status = 200;
68
+ this.#headers = null;
69
+ this.#status = 200;
88
70
  }
89
71
  get headers() {
90
- if (this._headers === null) {
91
- this._headers = new Headers();
72
+ if (this.#headers === null) {
73
+ this.#headers = new Headers();
92
74
  }
93
- return this._headers;
75
+ return this.#headers;
94
76
  }
95
77
  status(status) {
96
78
  if (status === void 0) {
97
- return this._status;
79
+ return this.#status;
98
80
  }
99
- this._status = status;
81
+ this.#status = status;
100
82
  return this;
101
83
  }
102
84
  /**
@@ -106,10 +88,10 @@ var KaitoHead = class {
106
88
  */
107
89
  toResponse(body) {
108
90
  const init = {
109
- status: this._status
91
+ status: this.#status
110
92
  };
111
- if (this._headers) {
112
- init.headers = this._headers;
93
+ if (this.#headers) {
94
+ init.headers = this.#headers;
113
95
  }
114
96
  return Response.json(body, init);
115
97
  }
@@ -117,7 +99,7 @@ var KaitoHead = class {
117
99
  * Whether this KaitoHead instance has been touched/modified
118
100
  */
119
101
  get touched() {
120
- return this._status !== 200 || this._headers !== null;
102
+ return this.#status !== 200 || this.#headers !== null;
121
103
  }
122
104
  };
123
105
 
@@ -161,102 +143,102 @@ var KaitoRequest = class {
161
143
 
162
144
  // src/util.ts
163
145
  var isNodeLikeDev = typeof process !== "undefined" && typeof process.env !== "undefined" && process.env.NODE_ENV === "development";
164
- function createUtilities(getContext) {
165
- return {
166
- getContext,
167
- router: () => Router.create()
168
- };
169
- }
170
- function parsable(parse) {
171
- return {
172
- parse
173
- };
174
- }
175
146
 
176
147
  // src/router/router.ts
177
148
  var Router = class _Router {
178
149
  state;
179
- static create = () => new _Router({
150
+ static create = (config) => new _Router({
180
151
  through: async (context) => context,
181
- routes: /* @__PURE__ */ new Set()
152
+ routes: /* @__PURE__ */ new Set(),
153
+ config,
154
+ paramsSchema: null
182
155
  });
183
- static parseQuery(schema, url) {
184
- if (!schema) {
185
- return {};
186
- }
187
- const result = {};
188
- for (const key in schema) {
189
- if (!schema.hasOwnProperty(key)) continue;
190
- const value = url.searchParams.get(key);
191
- result[key] = schema[key].parse(value);
192
- }
193
- return result;
194
- }
195
- constructor(options) {
196
- this.state = options;
156
+ constructor(state) {
157
+ this.state = state;
197
158
  }
198
159
  get routes() {
199
160
  return this.state.routes;
200
161
  }
201
162
  add = (method, path, route) => {
202
163
  const merged = {
203
- // 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)
204
164
  ...typeof route === "object" ? route : { run: route },
205
165
  method,
206
166
  path,
207
- through: this.state.through
167
+ router: this
208
168
  };
209
169
  return new _Router({
210
170
  ...this.state,
211
171
  routes: /* @__PURE__ */ new Set([...this.state.routes, merged])
212
172
  });
213
173
  };
174
+ params = (spec) => new _Router({
175
+ ...this.state,
176
+ paramsSchema: import_zod.z.object(spec)
177
+ });
214
178
  merge = (pathPrefix, other) => {
215
179
  const newRoutes = [...other.state.routes].map((route) => ({
216
180
  ...route,
217
- path: `${pathPrefix}${route.path}`
181
+ // handle pathPrefix = / & route.path = / case causing //
182
+ // we intentionally are replacing on the joining path and not the pathPrefix, in case of
183
+ // /named -> merged to -> / causing /named/ not /named
184
+ path: `${pathPrefix}${route.path === "/" ? "" : route.path}`
218
185
  }));
219
186
  return new _Router({
220
187
  ...this.state,
221
188
  routes: /* @__PURE__ */ new Set([...this.state.routes, ...newRoutes])
222
189
  });
223
190
  };
224
- freeze = (server) => {
225
- const routes = /* @__PURE__ */ new Map();
226
- for (const route of this.state.routes) {
227
- if (!routes.has(route.path)) {
228
- routes.set(route.path, /* @__PURE__ */ new Map());
191
+ static getFindRoute = (routes) => (method, path) => {
192
+ const params = {};
193
+ const pathParts = path.split("/").filter(Boolean);
194
+ const methodRoutes = routes.get(method);
195
+ if (!methodRoutes) return {};
196
+ for (const [routePath, route] of methodRoutes) {
197
+ const routeParts = routePath.split("/").filter(Boolean);
198
+ if (routeParts.length !== pathParts.length) {
199
+ continue;
229
200
  }
230
- routes.get(route.path).set(route.method, route);
231
- }
232
- const findRoute = (method, path) => {
233
- const params = {};
234
- const pathParts = path.split("/").filter(Boolean);
235
- for (const [routePath, methodHandlers] of routes) {
236
- const routeParts = routePath.split("/").filter(Boolean);
237
- if (routeParts.length !== pathParts.length) continue;
238
- let matches = true;
239
- for (let i = 0; i < routeParts.length; i++) {
240
- const routePart = routeParts[i];
241
- const pathPart = pathParts[i];
242
- if (routePart && pathPart && routePart.startsWith(":")) {
243
- params[routePart.slice(1)] = pathPart;
244
- } else if (routePart !== pathPart) {
245
- matches = false;
246
- break;
247
- }
248
- }
249
- if (matches) {
250
- const route = methodHandlers.get(method);
251
- if (route) return { route, params };
201
+ let matches = true;
202
+ for (let i = 0; i < routeParts.length; i++) {
203
+ const routePart = routeParts[i];
204
+ const pathPart = pathParts[i];
205
+ if (routePart && pathPart && routePart.startsWith(":")) {
206
+ params[routePart.slice(1)] = pathPart;
207
+ } else if (routePart !== pathPart) {
208
+ matches = false;
209
+ break;
252
210
  }
253
211
  }
254
- return { params };
255
- };
256
- return async (req) => {
212
+ if (matches) return { route, params };
213
+ }
214
+ return {};
215
+ };
216
+ static buildQuerySchema = (schema) => {
217
+ const keys = Object.keys(schema);
218
+ return import_zod.z.instanceof(URLSearchParams).transform((params) => {
219
+ const result = {};
220
+ for (const key of keys) {
221
+ result[key] = params.get(key);
222
+ }
223
+ return result;
224
+ }).pipe(import_zod.z.object(schema));
225
+ };
226
+ serve = () => {
227
+ const methodToRoutesMap = /* @__PURE__ */ new Map();
228
+ for (const route of this.state.routes) {
229
+ if (!methodToRoutesMap.has(route.method)) {
230
+ methodToRoutesMap.set(route.method, /* @__PURE__ */ new Map());
231
+ }
232
+ methodToRoutesMap.get(route.method).set(route.path, {
233
+ ...route,
234
+ fastQuerySchema: route.query ? _Router.buildQuerySchema(route.query) : void 0
235
+ });
236
+ }
237
+ const findRoute = _Router.getFindRoute(methodToRoutesMap);
238
+ const handle = async (req) => {
257
239
  const url = new URL(req.url);
258
240
  const method = req.method;
259
- const { route, params } = findRoute(method, url.pathname);
241
+ const { route, params: rawParams } = findRoute(method, url.pathname);
260
242
  if (!route) {
261
243
  const body = {
262
244
  success: false,
@@ -268,10 +250,13 @@ var Router = class _Router {
268
250
  const request = new KaitoRequest(url, req);
269
251
  const head = new KaitoHead();
270
252
  try {
271
- const body = route.body ? await route.body.parse(await req.json()) : void 0;
272
- const query = _Router.parseQuery(route.query, url);
273
- const rootCtx = await server.getContext(request, head);
274
- const ctx = await route.through(rootCtx);
253
+ const body = route.body ? await route.body.parseAsync(await req.json()) : void 0;
254
+ const query = route.fastQuerySchema ? await route.fastQuerySchema.parseAsync(url.searchParams) : {};
255
+ const params = route.router.state.paramsSchema ? route.router.state.paramsSchema.parse(rawParams) : rawParams;
256
+ const ctx = await route.router.state.through(
257
+ await this.state.config.getContext?.(request, head) ?? null,
258
+ params
259
+ );
275
260
  const result = await route.run({
276
261
  ctx,
277
262
  body,
@@ -294,8 +279,7 @@ var Router = class _Router {
294
279
  }
295
280
  return head.toResponse({
296
281
  success: true,
297
- data: result,
298
- message: "OK"
282
+ data: result
299
283
  });
300
284
  } catch (e) {
301
285
  const error = WrappedError.maybe(e);
@@ -306,17 +290,129 @@ var Router = class _Router {
306
290
  message: error.message
307
291
  });
308
292
  }
309
- const { status, message } = await server.onError({ error, req: request }).catch(() => ({ status: 500, message: "Internal Server Error" }));
310
- return head.status(status).toResponse({
311
- success: false,
312
- data: null,
313
- message
314
- });
293
+ if (!this.state.config.onError) {
294
+ return head.status(500).toResponse({
295
+ success: false,
296
+ data: null,
297
+ message: "Internal Server Error"
298
+ });
299
+ }
300
+ try {
301
+ const { status, message } = await this.state.config.onError(error, request);
302
+ return head.status(status).toResponse({
303
+ success: false,
304
+ data: null,
305
+ message
306
+ });
307
+ } catch (e2) {
308
+ console.error("KAITO - Failed to handle error inside `.onError()`, returning 500 and Internal Server Error");
309
+ console.error(e2);
310
+ return head.status(500).toResponse({
311
+ success: false,
312
+ data: null,
313
+ message: "Internal Server Error"
314
+ });
315
+ }
316
+ }
317
+ };
318
+ return async (request) => {
319
+ if (this.state.config.before) {
320
+ const result = await this.state.config.before(request);
321
+ if (result instanceof Response) {
322
+ if (this.state.config.transform) {
323
+ const transformed = await this.state.config.transform(request, result);
324
+ if (transformed instanceof Response) {
325
+ return result;
326
+ }
327
+ }
328
+ return result;
329
+ }
330
+ }
331
+ const response = await handle(request);
332
+ if (this.state.config.transform) {
333
+ const transformed = await this.state.config.transform(request, response);
334
+ if (transformed instanceof Response) {
335
+ return transformed;
336
+ }
315
337
  }
338
+ return response;
316
339
  };
317
340
  };
318
- method = (method) => (path, route) => {
319
- return this.add(method, path, route);
341
+ openapi = (highLevelSpec) => {
342
+ const OPENAPI_VERSION = "3.0.0";
343
+ const paths = {};
344
+ for (const route of this.state.routes) {
345
+ const path = route.path;
346
+ if (!route.openapi) {
347
+ continue;
348
+ }
349
+ const pathWithColonParamsReplaceWithCurlyBraces = path.replace(/:(\w+)/g, "{$1}");
350
+ if (!paths[pathWithColonParamsReplaceWithCurlyBraces]) {
351
+ paths[pathWithColonParamsReplaceWithCurlyBraces] = {};
352
+ }
353
+ const content = route.openapi.body.type === "json" ? {
354
+ "application/json": {
355
+ schema: import_zod.z.object({
356
+ success: import_zod.z.literal(true).openapi({
357
+ type: "boolean",
358
+ enum: [true]
359
+ // Need this as zod-openapi doesn't properly work with literals
360
+ }),
361
+ data: route.openapi.body.schema
362
+ })
363
+ }
364
+ } : {
365
+ "text/event-stream": {
366
+ schema: route.openapi.body.schema
367
+ }
368
+ };
369
+ const item = {
370
+ description: route.openapi?.description ?? "Successful response",
371
+ responses: {
372
+ 200: {
373
+ description: route.openapi?.description ?? "Successful response",
374
+ content
375
+ }
376
+ }
377
+ };
378
+ if (route.body) {
379
+ item.requestBody = {
380
+ content: {
381
+ "application/json": { schema: route.body }
382
+ }
383
+ };
384
+ }
385
+ const params = {};
386
+ if (route.query) {
387
+ params.query = import_zod.z.object(route.query);
388
+ }
389
+ const urlParams = path.match(/:(\w+)/g);
390
+ if (urlParams) {
391
+ const pathParams = {};
392
+ for (const param of urlParams) {
393
+ pathParams[param.slice(1)] = import_zod.z.string();
394
+ }
395
+ params.path = import_zod.z.object(pathParams);
396
+ }
397
+ item.requestParams = params;
398
+ paths[pathWithColonParamsReplaceWithCurlyBraces][route.method.toLowerCase()] = item;
399
+ }
400
+ const doc = (0, import_zod_openapi.createDocument)({
401
+ openapi: OPENAPI_VERSION,
402
+ paths,
403
+ ...highLevelSpec,
404
+ servers: Object.entries(highLevelSpec.servers ?? {}).map((entry) => {
405
+ const [url, description] = entry;
406
+ return {
407
+ url,
408
+ description
409
+ };
410
+ })
411
+ });
412
+ return this.get("/openapi.json", () => Response.json(doc));
413
+ };
414
+ method = (method) => {
415
+ return (path, route) => this.add(method, path, route);
320
416
  };
321
417
  get = this.method("GET");
322
418
  post = this.method("POST");
@@ -328,10 +424,15 @@ var Router = class _Router {
328
424
  through = (through) => {
329
425
  return new _Router({
330
426
  ...this.state,
331
- through: async (context) => through(await this.state.through(context))
427
+ through: async (context, params) => await through(await this.state.through(context, params), params)
332
428
  });
333
429
  };
334
430
  };
431
+
432
+ // src/create.ts
433
+ function create(config = {}) {
434
+ return Router.create(config);
435
+ }
335
436
  // Annotate the CommonJS export names for ESM import in node:
336
437
  0 && (module.exports = {
337
438
  KaitoError,
@@ -339,8 +440,6 @@ var Router = class _Router {
339
440
  KaitoRequest,
340
441
  Router,
341
442
  WrappedError,
342
- createKaitoHandler,
343
- createUtilities,
344
- isNodeLikeDev,
345
- parsable
443
+ create,
444
+ isNodeLikeDev
346
445
  });