@tahminator/sapling 2.0.5 → 2.1.0-beta.a2de2fb9

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
@@ -23,6 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  let express = require("express");
25
25
  express = __toESM(express);
26
+ let swagger_ui_express = require("swagger-ui-express");
27
+ swagger_ui_express = __toESM(swagger_ui_express);
26
28
  //#region src/html/404.ts
27
29
  /**
28
30
  * Default Express.js 404 error page, as a string.
@@ -46,6 +48,7 @@ const Html404ErrorPage = (error) => `<!DOCTYPE html>
46
48
  * You can either return `new RedirectView(url)` or `RedirectView.redirect(url)` inside of a controller method.
47
49
  */
48
50
  var RedirectView = class RedirectView {
51
+ _url;
49
52
  constructor(url) {
50
53
  this._url = url;
51
54
  }
@@ -141,8 +144,10 @@ let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
141
144
  * @typeParam T - the type of the response body
142
145
  */
143
146
  var ResponseEntity = class {
147
+ _statusCode;
148
+ _headers = {};
149
+ _body;
144
150
  constructor(body, headers = {}, statusCode = 200) {
145
- this._headers = {};
146
151
  this._body = body;
147
152
  this._headers = headers;
148
153
  this._statusCode = statusCode;
@@ -195,8 +200,9 @@ var ResponseEntity = class {
195
200
  * ensuring type safety when constructing the response.
196
201
  */
197
202
  var ResponseEntityBuilder = class {
203
+ _statusCode;
204
+ _headers = {};
198
205
  constructor(statusCode) {
199
- this._headers = {};
200
206
  this._statusCode = statusCode;
201
207
  }
202
208
  /**
@@ -228,6 +234,7 @@ var ResponseEntityBuilder = class {
228
234
  * @see {@link Sapling.loadResponseStatusErrorMiddleware}
229
235
  */
230
236
  var ResponseStatusError = class ResponseStatusError extends Error {
237
+ status;
231
238
  constructor(status, message) {
232
239
  super(message ?? "Something went wrong.");
233
240
  this.status = status;
@@ -239,150 +246,30 @@ var ResponseStatusError = class ResponseStatusError extends Error {
239
246
  //#endregion
240
247
  //#region src/helper/error/parse.ts
241
248
  /**
242
- * This error should be thrown when some data cannot be parsed by a given schema.
249
+ * This error should be thrown when some data cannot be parsed by a given Standard Schema compatible schema.
243
250
  */
244
251
  var ParserError = class ParserError extends ResponseStatusError {
245
- constructor(location, issues, vendor) {
246
- super(400, ParserError.formatMessage(location, issues, vendor));
252
+ constructor(location, issues, vendor, functionName) {
253
+ super(400, ParserError.formatMessage(location, issues, vendor, functionName));
247
254
  Object.setPrototypeOf(this, new.target.prototype);
248
255
  }
249
- static formatMessage(location, issues, vendor) {
256
+ static formatMessage(location, issues, vendor, functionName) {
250
257
  const formatted = issues.map((i) => {
251
258
  const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
252
259
  return path ? `${path}: ${i.message}` : i.message;
253
260
  }).join("; ");
254
- return `${vendor} failed to parse ${(() => {
255
- switch (location) {
256
- case "reqbody": return "request body";
257
- case "reqparams": return "request params";
258
- case "reqquery": return "request query";
259
- }
260
- })()}: ${formatted}`;
261
- }
262
- };
263
- //#endregion
264
- //#region src/helper/sapling.ts
265
- const settings = {
266
- serialize: JSON.stringify,
267
- deserialize: JSON.parse
268
- };
269
- /**
270
- * Collection of utility functions which are essential for Sapling to function.
271
- */
272
- var Sapling = class Sapling {
273
- /**
274
- * If you would prefer to manually resolve your controllers instead, call resolve
275
- * on the controller class.
276
- *
277
- * @example```ts
278
- * import { Sapling } from "@tahminator/sapling";
279
- * import TestController from "./path/to/test.controller";
280
- *
281
- * const app = express();
282
- *
283
- * const router = Sapling.resolve(TestController);
284
- * app.use(router);
285
- * ```
286
- */
287
- static resolve(clazz) {
288
- const router = _ControllerRegistry.get(clazz);
289
- if (!router) throw new Error("Controller cannot be found");
290
- return router;
291
- }
292
- /**
293
- * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
294
- *
295
- * @example```ts
296
- * import { Sapling } from "@tahminator/sapling";
297
- * import express from "express";
298
- *
299
- * const app = express();
300
- *
301
- * app.use(Sapling.json());
302
- * ```
303
- */
304
- static json() {
305
- return (request, _response, next) => {
306
- try {
307
- if (!request.body) return next();
308
- if (request.headers["content-type"] !== "application/json") return next();
309
- if (typeof request.body === "string") request.body = Sapling.deserialize(request.body);
310
- else if (typeof request.body === "object") {
311
- const raw = JSON.stringify(request.body);
312
- request.body = Sapling.deserialize(raw);
313
- }
314
- next();
315
- } catch (err) {
316
- next(err);
317
- }
318
- };
261
+ return `Failed to parse ${this.getPrettyLocationString(location)} with ${vendor} on ${functionName}: ${formatted}`;
319
262
  }
320
- /**
321
- * Register your application with all the necessary middlewares and logics for Sapling to function.
322
- *
323
- * @example```ts
324
- * import { Sapling } from "@tahminator/sapling";
325
- * import express from "express";
326
- *
327
- * const app = express();
328
- *
329
- * app.registerApp(app);
330
- * ```
331
- */
332
- static registerApp(app) {
333
- app.use(express.default.text({ type: "application/json" }));
334
- app.use(Sapling.json());
335
- }
336
- /**
337
- * Serialize a value into a JSON string.
338
- *
339
- * This function is used in {@link ResponseEntity} to serialize the `body`.
340
- *
341
- * Use `setSerializeFn` to override underlying implementation.
342
- *
343
- * @defaultValue `JSON.stringify`
344
- */
345
- static serialize(value) {
346
- return settings.serialize(value);
347
- }
348
- /**
349
- * Replace the function used for `serialize`.
350
- */
351
- static setSerializeFn(fn) {
352
- settings.serialize = fn;
353
- }
354
- /**
355
- * De-serialize a JSON string back to a JavaScript object.
356
- *
357
- * This function is used to de-serialize a string into a `body`.
358
- *
359
- * Use `setDeserializeFn` to override underlying implementation.
360
- *
361
- * @defaultValue `JSON.parse`
362
- */
363
- static deserialize(value) {
364
- return settings.deserialize(value);
365
- }
366
- /**
367
- * Replace the function used for `deserialize`
368
- */
369
- static setDeserializeFn(fn) {
370
- settings.deserialize = fn;
263
+ static getPrettyLocationString(location) {
264
+ switch (location) {
265
+ case "reqbody": return "request body";
266
+ case "reqparams": return "request params";
267
+ case "reqquery": return "request query";
268
+ case "resbody": return "response body";
269
+ }
371
270
  }
372
271
  };
373
272
  //#endregion
374
- //#region src/types.ts
375
- const methodResolve = {
376
- GET: "get",
377
- PUT: "put",
378
- POST: "post",
379
- DELETE: "delete",
380
- OPTIONS: "options",
381
- PATCH: "patch",
382
- HEAD: "head",
383
- USE: "use"
384
- };
385
- //#endregion
386
273
  //#region lib/weakmap.ts
387
274
  /**
388
275
  * WeakMap that is iterable.
@@ -503,132 +390,239 @@ function _resolve(ctor) {
503
390
  return _InjectableRegistry.get(ctor);
504
391
  }
505
392
  //#endregion
506
- //#region src/annotation/request.ts
507
- const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
508
- /**
509
- * Apply to a route method to have `request.body` be parsed by `schema`.
510
- *
511
- * This annotation will parse `request.body` & then override `request.body`.
512
- * You can then just simply cast `request.body` for your use
513
- *
514
- * @example
515
- * ```ts
516
- * const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
517
- * name: z.string(),
518
- * description: z.string().optional(),
519
- * });
520
- *
521
- * ⠀@Controller({ prefix: "/api/book" })
522
- * class BookController {
523
- * ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
524
- * ⠀@POST()
525
- * public createBook(request: e.Request) {
526
- * const { name, description } = request.body as unknown as z.infer<
527
- * typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
528
- * >;
529
- * }
530
- * }
531
- * ```
532
- */
533
- function RequestBody(schema) {
534
- return (target, propertyKey) => {
535
- const ctor = target.constructor;
536
- const fnName = String(propertyKey);
537
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
538
- };
539
- }
540
- /**
541
- * Apply to a route method to have `request.param` be parsed by `schema`.
542
- *
543
- * This annotation will parse `request.param` & then override `request.param`.
544
- * You can then just simply cast `request.param` for your use
545
- *
546
- * @example
547
- * ```ts
548
- * const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
549
- * bookId: z.string(),
550
- * });
551
- *
552
- * ⠀@Controller({ prefix: "/api/book" })
553
- * class BookController {
554
- * ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
555
- * ⠀@GET("/:bookId")
556
- * public getBook(request: e.Request) {
557
- * const { bookId } = request.param as unknown as z.infer<
558
- * typeof GET_BOOK_REQUEST_PARAM_SCHEMA
559
- * >;
560
- * }
561
- * }
562
- * ```
563
- */
564
- function RequestParam(schema) {
565
- return (target, propertyKey) => {
566
- const ctor = target.constructor;
567
- const fnName = String(propertyKey);
568
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
569
- };
393
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
394
+ function __decorate(decorators, target, key, desc) {
395
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
396
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
397
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
398
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
570
399
  }
400
+ //#endregion
401
+ //#region src/middleware/health/registrar.ts
402
+ let HealthRegistrar = class HealthRegistrar {
403
+ _checks;
404
+ _sealed;
405
+ constructor() {
406
+ this._checks = [];
407
+ this._sealed = false;
408
+ }
409
+ add(healthCheck) {
410
+ this._checks.push(healthCheck);
411
+ }
412
+ /**
413
+ * @internal used by Sapling library
414
+ */
415
+ _seal() {
416
+ this._sealed = true;
417
+ }
418
+ async check() {
419
+ if (!this._sealed) return false;
420
+ return (await Promise.all(this._checks.map((c) => c()))).every((c) => c === true);
421
+ }
422
+ };
423
+ HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
424
+ //#endregion
425
+ //#region src/helper/sapling.ts
426
+ const _settings = {
427
+ serialize: JSON.stringify,
428
+ deserialize: JSON.parse,
429
+ health: { path: "/up" },
430
+ doc: {
431
+ openApiPath: "/openapi.json",
432
+ swaggerPath: "/swagger.html",
433
+ metadata: {
434
+ title: "API",
435
+ version: "1.0.0"
436
+ }
437
+ }
438
+ };
571
439
  /**
572
- * Apply to a route method to have `request.query` be parsed by `schema`.
573
- *
574
- * This annotation will parse `request.query` & then override `request.query`.
575
- * You can then just simply cast `request.query` for your use
576
- *
577
- * @example
578
- * ```ts
579
- * const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
580
- * sort: z.enum(["name", "createdAt"]).optional(),
581
- * q: z.string().optional(),
582
- * });
583
- *
584
- * ⠀@Controller({ prefix: "/api/book" })
585
- * class BookController {
586
- * ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
587
- * ⠀@GET()
588
- * public listBooks(request: e.Request) {
589
- * const { sort, q } = request.query as unknown as z.infer<
590
- * typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
591
- * >;
592
- * }
593
- * }
594
- * ```
440
+ * Collection of utility functions which are essential for Sapling to function.
595
441
  */
596
- function RequestQuery(schema) {
597
- return (target, propertyKey) => {
598
- const ctor = target.constructor;
599
- const fnName = String(propertyKey);
600
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
601
- };
602
- }
603
- function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
604
- const byFn = (() => {
605
- const fn = _requestSchemaStore.get(ctor);
606
- if (fn) return fn;
607
- const newFn = /* @__PURE__ */ new Map();
608
- _requestSchemaStore.set(ctor, newFn);
609
- return newFn;
610
- })();
611
- const existing = byFn.get(fnName);
612
- if (existing) return existing;
613
- const created = {};
614
- byFn.set(fnName, created);
615
- return created;
616
- }
617
- function _setOnce(def, key, schema, fnName) {
618
- if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
619
- def[key] = schema;
620
- }
621
- function _getRequestSchemas(ctor, fnName) {
622
- return _requestSchemaStore.get(ctor)?.get(fnName);
623
- }
624
- async function _parseOrThrow(schema, input, kind) {
625
- const result = await schema["~standard"].validate(input);
626
- if (result.issues) {
627
- console.debug(`Failed to parse a schema`);
628
- throw new ParserError(kind, result.issues, schema["~standard"].vendor);
442
+ var Sapling = class Sapling {
443
+ /**
444
+ * If you would prefer to manually resolve your controllers instead, call resolve
445
+ * on the controller class.
446
+ *
447
+ * @example```ts
448
+ * import { Sapling } from "@tahminator/sapling";
449
+ * import TestController from "./path/to/test.controller";
450
+ *
451
+ * const app = express();
452
+ *
453
+ * const router = Sapling.resolve(TestController);
454
+ * app.use(router);
455
+ * ```
456
+ */
457
+ static resolve(clazz) {
458
+ const router = _ControllerRegistry.get(clazz);
459
+ if (!router) throw new Error("Controller cannot be found");
460
+ return router;
629
461
  }
630
- return result.value;
631
- }
462
+ /**
463
+ * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
464
+ *
465
+ * @example```ts
466
+ * import { Sapling } from "@tahminator/sapling";
467
+ * import express from "express";
468
+ *
469
+ * const app = express();
470
+ *
471
+ * app.use(Sapling.json());
472
+ * ```
473
+ */
474
+ static json() {
475
+ return (request, _response, next) => {
476
+ try {
477
+ if (!request.body) return next();
478
+ if (request.headers["content-type"] !== "application/json") return next();
479
+ if (typeof request.body === "string") request.body = Sapling.deserialize(request.body);
480
+ else if (typeof request.body === "object") {
481
+ const raw = JSON.stringify(request.body);
482
+ request.body = Sapling.deserialize(raw);
483
+ }
484
+ next();
485
+ } catch (err) {
486
+ next(err);
487
+ }
488
+ };
489
+ }
490
+ /**
491
+ * Register your application with all the necessary middlewares and logics for Sapling to function.
492
+ *
493
+ * @example```ts
494
+ * import { Sapling } from "@tahminator/sapling";
495
+ * import express from "express";
496
+ *
497
+ * const app = express();
498
+ *
499
+ * app.registerApp(app);
500
+ * ```
501
+ */
502
+ static registerApp(app) {
503
+ app.use(express.default.text({ type: "application/json" }));
504
+ app.use(Sapling.json());
505
+ return new Proxy(app, { get(target, prop, receiver) {
506
+ if (prop === "listen") {
507
+ const originalListen = target[prop];
508
+ return function(...args) {
509
+ const server = originalListen.apply(target, args);
510
+ server.once("listening", () => {
511
+ Sapling.onStartup();
512
+ console.log("Sapling successfully initialized post-startup hooks on server start");
513
+ });
514
+ return server;
515
+ };
516
+ }
517
+ return Reflect.get(target, prop, receiver);
518
+ } });
519
+ }
520
+ static onStartup() {
521
+ _InjectableRegistry.get(HealthRegistrar)?.seal();
522
+ }
523
+ /**
524
+ * Serialize a value into a JSON string.
525
+ *
526
+ * This function is used in {@link ResponseEntity} to serialize the `body`.
527
+ *
528
+ * Use `setSerializeFn` to override underlying implementation.
529
+ *
530
+ * @defaultValue `JSON.stringify`
531
+ */
532
+ static serialize(value) {
533
+ return _settings.serialize(value);
534
+ }
535
+ /**
536
+ * Replace the function used for `serialize`.
537
+ */
538
+ static setSerializeFn(fn) {
539
+ _settings.serialize = fn;
540
+ }
541
+ /**
542
+ * De-serialize a JSON string back to a JavaScript object.
543
+ *
544
+ * This function is used to de-serialize a string into a `body`.
545
+ *
546
+ * Use `setDeserializeFn` to override underlying implementation.
547
+ *
548
+ * @defaultValue `JSON.parse`
549
+ */
550
+ static deserialize(value) {
551
+ return _settings.deserialize(value);
552
+ }
553
+ /**
554
+ * Replace the function used for `deserialize`
555
+ */
556
+ static setDeserializeFn(fn) {
557
+ _settings.deserialize = fn;
558
+ }
559
+ /**
560
+ * Modify extra settings
561
+ */
562
+ static Extras = {
563
+ /**
564
+ * Modify default settings applied to OpenAPI & Swagger
565
+ */
566
+ swaggerAndOpenApi: {
567
+ /**
568
+ * Set base OpenAPI metadata values.
569
+ *
570
+ * @default { title: "API", version: "1.0.0" }
571
+ */
572
+ setMetadata(metadata) {
573
+ _settings.doc.metadata = metadata;
574
+ },
575
+ /**
576
+ * change default endpoint that will serve OpenAPI spec.
577
+ * Swagger will also load this endpoint on load.
578
+ *
579
+ * @default `/openapi.json`
580
+ */
581
+ setOpenApiPath(path) {
582
+ _settings.doc.openApiPath = path;
583
+ },
584
+ /**
585
+ * change Swagger endpoint.
586
+ *
587
+ * @default `/swagger.html`
588
+ */
589
+ setSwaggerPath(path) {
590
+ _settings.doc.swaggerPath = path;
591
+ }
592
+ } };
593
+ /**
594
+ * This method can be used in a `@MiddlewareClass` to register any libraries
595
+ * that expect you to register multiple registers at once. An example is `swagger-ui-express`
596
+ *
597
+ * @example
598
+ * ```ts
599
+ * ⠀@MiddlewareClass()
600
+ * class Serve {
601
+ * // `swagger.serve` returns multiple Express handlers for all the assets and routes
602
+ * // that will be served
603
+ * private readonly handlers: RequestHandler[] = swagger.serve;
604
+ *
605
+ * ⠀@Middleware(_settings.doc.swaggerPath)
606
+ * handle(request: Request, response: Response, next: NextFunction) {
607
+ * return Sapling.chainHandlers(this.handlers, request, response, next);
608
+ * }
609
+ * }
610
+ * ```
611
+ */
612
+ static chainHandlers(handlers, request, response, next, index = 0) {
613
+ if (index >= handlers.length) {
614
+ next();
615
+ return;
616
+ }
617
+ handlers[index]?.(request, response, (err) => {
618
+ if (err) {
619
+ next(err);
620
+ return;
621
+ }
622
+ Sapling.chainHandlers(handlers, request, response, next, index + 1);
623
+ });
624
+ }
625
+ };
632
626
  //#endregion
633
627
  //#region src/annotation/route.ts
634
628
  const _routeStore = /* @__PURE__ */ new WeakMap();
@@ -711,6 +705,245 @@ function _getRoutes(ctor) {
711
705
  return _routeStore.get(ctor) ?? [];
712
706
  }
713
707
  //#endregion
708
+ //#region src/annotation/schema.ts
709
+ function ControllerSchema(options) {
710
+ return (target) => {
711
+ _setControllerSchema(target, options);
712
+ };
713
+ }
714
+ function RouteSchema(options) {
715
+ return (target, propertyKey) => {
716
+ const ctor = target.constructor;
717
+ _setRouteSchema(ctor, String(propertyKey), options);
718
+ };
719
+ }
720
+ const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
721
+ const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
722
+ function getOrCreateRouteSchemaStore(store, ctor) {
723
+ const existing = store.get(ctor);
724
+ if (existing) return existing;
725
+ const created = /* @__PURE__ */ new Map();
726
+ store.set(ctor, created);
727
+ return created;
728
+ }
729
+ function _setRouteSchema(ctor, fnName, options) {
730
+ getOrCreateRouteSchemaStore(_routeSchemaStore, ctor).set(fnName, options);
731
+ }
732
+ function _setControllerSchema(ctor, options) {
733
+ _controllerSchemaStore.set(ctor, options);
734
+ }
735
+ function _getRouteSchema(ctor, fnName) {
736
+ return _routeSchemaStore.get(ctor)?.get(fnName);
737
+ }
738
+ function _getControllerSchema(ctor) {
739
+ return _controllerSchemaStore.get(ctor);
740
+ }
741
+ //#endregion
742
+ //#region src/annotation/validator.ts
743
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
744
+ function ResponseBody(schema) {
745
+ return (target, propertyKey) => {
746
+ const ctor = target.constructor;
747
+ const fnName = String(propertyKey);
748
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
749
+ };
750
+ }
751
+ function RequestBody(schema) {
752
+ return (target, propertyKey) => {
753
+ const ctor = target.constructor;
754
+ const fnName = String(propertyKey);
755
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
756
+ };
757
+ }
758
+ function RequestParam(schema) {
759
+ return (target, propertyKey) => {
760
+ const ctor = target.constructor;
761
+ const fnName = String(propertyKey);
762
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
763
+ };
764
+ }
765
+ function RequestQuery(schema) {
766
+ return (target, propertyKey) => {
767
+ const ctor = target.constructor;
768
+ const fnName = String(propertyKey);
769
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
770
+ };
771
+ }
772
+ function getOrCreateValidatorSchemaStore(store, ctor) {
773
+ const existing = store.get(ctor);
774
+ if (existing) return existing;
775
+ const created = /* @__PURE__ */ new Map();
776
+ store.set(ctor, created);
777
+ return created;
778
+ }
779
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
780
+ const byFn = getOrCreateValidatorSchemaStore(_validatorSchemaStore, ctor);
781
+ const existing = byFn.get(fnName);
782
+ if (existing) return existing;
783
+ const created = {};
784
+ byFn.set(fnName, created);
785
+ return created;
786
+ }
787
+ async function _parseOrThrow(schema, input, location, fnName) {
788
+ const result = await schema["~standard"].validate(input);
789
+ if (result.issues) throw new ParserError(location, result.issues, schema["~standard"].vendor, fnName);
790
+ return result.value;
791
+ }
792
+ function _saveValidatorSchema(def, key, schema, fnName) {
793
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
794
+ def[key] = schema;
795
+ }
796
+ function _getValidatorSchema(ctor, fnName) {
797
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
798
+ }
799
+ //#endregion
800
+ //#region src/helper/openapi.ts
801
+ var OpenAPIGenerator = class {
802
+ OPENAPI_VERSION = "3.0.0";
803
+ controllers = /* @__PURE__ */ new Set();
804
+ registerController(controllerClass, prefix) {
805
+ this.controllers.add({
806
+ class: controllerClass,
807
+ prefix
808
+ });
809
+ }
810
+ /**
811
+ * visible for testing
812
+ */
813
+ _clearControllers() {
814
+ this.controllers.clear();
815
+ }
816
+ get metadata() {
817
+ return _settings.doc.metadata;
818
+ }
819
+ generateSpec() {
820
+ const metadata = this.metadata;
821
+ const paths = {};
822
+ const tags = [];
823
+ for (const { class: controllerClass, prefix } of this.controllers) {
824
+ const routes = _getRoutes(controllerClass);
825
+ const controllerSchema = _getControllerSchema(controllerClass);
826
+ if (controllerSchema?.title) tags.push({
827
+ name: controllerSchema.title,
828
+ description: controllerSchema.description
829
+ });
830
+ for (const route of routes) {
831
+ if (route.method === "USE") continue;
832
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
833
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
834
+ if (route.path instanceof RegExp) throw new Error(`You have a route with a regex path of ${route.path.source}. This is not compatible with OpenAPI.`);
835
+ const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
836
+ if (!paths[openApiPath]) paths[openApiPath] = {};
837
+ const responses = {};
838
+ if (schemas?.responseBody) {
839
+ const responseSchema = this.toJsonSchema(schemas.responseBody, "output");
840
+ responses["200"] = {
841
+ description: responseSchema.description ?? "Successful response",
842
+ content: { "application/json": { schema: responseSchema } }
843
+ };
844
+ } else responses["200"] = { description: "Successful response" };
845
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) {
846
+ const statusCode = String(resp.statusCode);
847
+ const existingResponse = responses[statusCode];
848
+ const existingSchema = existingResponse && "content" in existingResponse ? existingResponse.content?.["application/json"]?.schema : void 0;
849
+ const responseSchema = resp.schema ? this.toJsonSchema(resp.schema, "output") : statusCode === "200" ? existingSchema : void 0;
850
+ responses[statusCode] = {
851
+ ...existingResponse,
852
+ description: resp.description ?? responseSchema?.description ?? existingResponse?.description ?? `Response ${resp.statusCode}`,
853
+ ...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
854
+ };
855
+ }
856
+ const operation = {
857
+ responses,
858
+ summary: routeSchema?.summary,
859
+ description: routeSchema?.description,
860
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
861
+ };
862
+ const parameters = [];
863
+ if (schemas?.requestParam) {
864
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
865
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
866
+ const parameterSchema = schema;
867
+ parameters.push({
868
+ name,
869
+ in: "path",
870
+ required: true,
871
+ description: parameterSchema.description,
872
+ schema: parameterSchema
873
+ });
874
+ }
875
+ }
876
+ if (schemas?.requestQuery) {
877
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
878
+ if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
879
+ const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
880
+ const parameterSchema = schema;
881
+ parameters.push({
882
+ name,
883
+ in: "query",
884
+ required: isRequired,
885
+ description: parameterSchema.description,
886
+ schema: parameterSchema
887
+ });
888
+ }
889
+ }
890
+ if (parameters.length > 0) operation.parameters = parameters;
891
+ if (schemas?.requestBody) {
892
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
893
+ operation.requestBody = {
894
+ required: true,
895
+ description: requestSchema.description,
896
+ content: { "application/json": { schema: requestSchema } }
897
+ };
898
+ }
899
+ const method = route.method.toLowerCase();
900
+ paths[openApiPath][method] = operation;
901
+ }
902
+ }
903
+ return {
904
+ openapi: this.OPENAPI_VERSION,
905
+ info: {
906
+ title: metadata.title,
907
+ version: metadata.version,
908
+ description: metadata.description
909
+ },
910
+ tags: tags.length > 0 ? tags : void 0,
911
+ paths
912
+ };
913
+ }
914
+ toJsonSchema(schema, direction = "output") {
915
+ try {
916
+ const jsonSchema = schema["~standard"].jsonSchema;
917
+ return direction === "input" ? jsonSchema.input({ target: "openapi-3.0" }) : jsonSchema.output({ target: "openapi-3.0" });
918
+ } catch (e) {
919
+ if (e instanceof Error && e.message.includes("Transforms cannot be represented in JSON Schema")) throw new Error(`${e.message}.\nIt appears that you are using z.transform() - it is highly recommended that you use z.codec instead - https://zod.dev/codecs`, { cause: e });
920
+ throw e;
921
+ }
922
+ }
923
+ };
924
+ const openApiGenerator = new OpenAPIGenerator();
925
+ function _registerController(controllerClass, prefix) {
926
+ openApiGenerator.registerController(controllerClass, prefix);
927
+ }
928
+ function generateOpenApiSpec() {
929
+ return openApiGenerator.generateSpec();
930
+ }
931
+ function _clearOpenApiRegistry() {
932
+ openApiGenerator._clearControllers();
933
+ }
934
+ //#endregion
935
+ //#region src/types.ts
936
+ const methodResolve = {
937
+ GET: "get",
938
+ PUT: "put",
939
+ POST: "post",
940
+ DELETE: "delete",
941
+ OPTIONS: "options",
942
+ PATCH: "patch",
943
+ HEAD: "head",
944
+ USE: "use"
945
+ };
946
+ //#endregion
714
947
  //#region src/annotation/controller.ts
715
948
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
716
949
  /**
@@ -722,6 +955,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
722
955
  function Controller({ prefix = "", deps = [] } = {}) {
723
956
  return (target) => {
724
957
  const targetClass = target;
958
+ _registerController(target, prefix);
725
959
  const router = (0, express.Router)();
726
960
  const routes = _getRoutes(target);
727
961
  const usedRoutes = /* @__PURE__ */ new Set();
@@ -746,15 +980,20 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
746
980
  if (method === "USE" && fn.length >= 4) {
747
981
  const middlewareFn = async (err, request, response, next) => {
748
982
  try {
749
- const result = fn.bind(controllerInstance)(err, request, response, next);
750
- if (result instanceof ResponseEntity) {
751
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
752
- return;
753
- }
754
- if (result instanceof RedirectView) {
755
- response.redirect(result.getUrl());
756
- return;
757
- }
983
+ await validate({
984
+ target,
985
+ fnName,
986
+ request
987
+ });
988
+ await handleResult({
989
+ result: fn.bind(controllerInstance)(err, request, response, next),
990
+ response,
991
+ target,
992
+ fnName,
993
+ method,
994
+ path: path instanceof RegExp ? path.source : fp,
995
+ isErrorMiddleware: true
996
+ });
758
997
  } catch (e) {
759
998
  console.error(e);
760
999
  next(e);
@@ -764,34 +1003,52 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
764
1003
  return;
765
1004
  }
766
1005
  router[methodName](fp, async (request, response, next) => {
767
- const schemas = _getRequestSchemas(target, fnName);
768
- if (schemas) {
769
- if (schemas.body) request.body = await _parseOrThrow(schemas.body, request.body, "reqbody");
770
- if (schemas.param) request.params = await _parseOrThrow(schemas.param, request.params, "reqparams");
771
- if (schemas.query) {
772
- const parsedQuery = await _parseOrThrow(schemas.query, request.query, "reqquery");
773
- Object.defineProperty(request, "query", {
774
- value: parsedQuery,
775
- writable: true,
776
- configurable: true
777
- });
778
- }
779
- }
780
- const result = await fn.bind(controllerInstance)(request, response, next);
781
- if (result instanceof ResponseEntity) {
782
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
783
- return;
784
- }
785
- if (result instanceof RedirectView) {
786
- response.redirect(result.getUrl());
787
- return;
788
- }
789
- if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
1006
+ await validate({
1007
+ target,
1008
+ fnName,
1009
+ request
1010
+ });
1011
+ await handleResult({
1012
+ result: await fn.bind(controllerInstance)(request, response, next),
1013
+ response,
1014
+ target,
1015
+ fnName,
1016
+ method,
1017
+ path: path instanceof RegExp ? path.source : fp
1018
+ });
790
1019
  });
791
1020
  }
792
1021
  _ControllerRegistry.set(targetClass, router);
793
1022
  };
794
1023
  }
1024
+ async function handleResult({ result, target, fnName, response, method, path, isErrorMiddleware = false }) {
1025
+ const schemas = _getValidatorSchema(target, fnName);
1026
+ if (result instanceof ResponseEntity) {
1027
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody", fnName) : result.getBody();
1028
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
1029
+ return;
1030
+ }
1031
+ if (result instanceof RedirectView) {
1032
+ response.redirect(result.getUrl());
1033
+ return;
1034
+ }
1035
+ if (!isErrorMiddleware && method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${method} ${path}`));
1036
+ }
1037
+ async function validate({ target, fnName, request }) {
1038
+ const schemas = _getValidatorSchema(target, fnName);
1039
+ if (schemas) {
1040
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody", fnName);
1041
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams", fnName);
1042
+ if (schemas.requestQuery) {
1043
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery", fnName);
1044
+ Object.defineProperty(request, "query", {
1045
+ value: parsedQuery,
1046
+ writable: true,
1047
+ configurable: true
1048
+ });
1049
+ }
1050
+ }
1051
+ }
795
1052
  //#endregion
796
1053
  //#region src/annotation/middleware.ts
797
1054
  /**
@@ -805,15 +1062,7 @@ function MiddlewareClass(...args) {
805
1062
  return Controller(...args);
806
1063
  }
807
1064
  //#endregion
808
- //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
809
- function __decorate(decorators, target, key, desc) {
810
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
811
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
812
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
813
- return c > 3 && r && Object.defineProperty(target, key, r), r;
814
- }
815
- //#endregion
816
- //#region src/middleware/default/base.ts
1065
+ //#region src/middleware/default/error/base.ts
817
1066
  let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
818
1067
  handle(err, _request, _response, _next) {
819
1068
  console.error("[Error]", err);
@@ -823,7 +1072,20 @@ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
823
1072
  __decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
824
1073
  DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
825
1074
  //#endregion
826
- //#region src/middleware/default/responsestatus.ts
1075
+ //#region src/middleware/default/error/parse.ts
1076
+ let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
1077
+ handle(err, _request, _response, next) {
1078
+ if (err instanceof ParserError) {
1079
+ console.warn(err);
1080
+ return ResponseEntity.status(err.status).body({ message: err.message });
1081
+ }
1082
+ next(err);
1083
+ }
1084
+ };
1085
+ __decorate([Middleware()], DefaultParserErrorMiddleware.prototype, "handle", null);
1086
+ DefaultParserErrorMiddleware = __decorate([MiddlewareClass()], DefaultParserErrorMiddleware);
1087
+ //#endregion
1088
+ //#region src/middleware/default/error/responsestatus.ts
827
1089
  let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
828
1090
  handle(err, _request, _response, next) {
829
1091
  if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
@@ -833,7 +1095,103 @@ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddl
833
1095
  __decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
834
1096
  DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
835
1097
  //#endregion
1098
+ //#region src/middleware/default/openapi/index.ts
1099
+ let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
1100
+ handle(_request, _response, _next) {
1101
+ return ResponseEntity.ok().body(generateOpenApiSpec());
1102
+ }
1103
+ };
1104
+ __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
1105
+ DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
1106
+ //#endregion
1107
+ //#region src/middleware/default/swagger/index.ts
1108
+ /**
1109
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1110
+ *
1111
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1112
+ *
1113
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1114
+ *
1115
+ * ```ts
1116
+ * const middlewares = [
1117
+ * DefaultOpenApiMiddleware,
1118
+ * DefaultSwaggerMiddleware.Serve,
1119
+ * DefaultSwaggerMiddleware.Setup,
1120
+ * ];
1121
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1122
+ * ```
1123
+ */
1124
+ let Serve = class Serve {
1125
+ handlers = swagger_ui_express.default.serve;
1126
+ handle(request, response, next) {
1127
+ return Sapling.chainHandlers(this.handlers, request, response, next);
1128
+ }
1129
+ };
1130
+ __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
1131
+ Serve = __decorate([MiddlewareClass()], Serve);
1132
+ /**
1133
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1134
+ *
1135
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1136
+ *
1137
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1138
+ *
1139
+ * ```ts
1140
+ * const middlewares = [
1141
+ * DefaultOpenApiMiddleware,
1142
+ * DefaultSwaggerMiddleware.Serve,
1143
+ * DefaultSwaggerMiddleware.Setup,
1144
+ * ];
1145
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1146
+ * ```
1147
+ */
1148
+ let Setup = class Setup {
1149
+ handler;
1150
+ constructor() {
1151
+ this.handler = swagger_ui_express.default.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
1152
+ }
1153
+ handle(request, response, next) {
1154
+ return this.handler(request, response, next);
1155
+ }
1156
+ };
1157
+ __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
1158
+ Setup = __decorate([MiddlewareClass()], Setup);
1159
+ /**
1160
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1161
+ *
1162
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1163
+ *
1164
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1165
+ *
1166
+ * ```ts
1167
+ * const middlewares = [
1168
+ * DefaultOpenApiMiddleware,
1169
+ * DefaultSwaggerMiddleware.Serve,
1170
+ * DefaultSwaggerMiddleware.Setup,
1171
+ * ];
1172
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1173
+ * ```
1174
+ */
1175
+ const DefaultSwaggerMiddleware = {
1176
+ Serve,
1177
+ Setup
1178
+ };
1179
+ //#endregion
1180
+ //#region src/middleware/default/health/index.ts
1181
+ let DefaultHealthMiddleware = class DefaultHealthMiddleware {
1182
+ constructor(healthRegistrar) {
1183
+ this.healthRegistrar = healthRegistrar;
1184
+ }
1185
+ async serve(_request, _response, _next) {
1186
+ const up = await this.healthRegistrar.check();
1187
+ return ResponseEntity.ok().body({ up });
1188
+ }
1189
+ };
1190
+ __decorate([GET(_settings.health.path)], DefaultHealthMiddleware.prototype, "serve", null);
1191
+ DefaultHealthMiddleware = __decorate([MiddlewareClass({ deps: [HealthRegistrar] })], DefaultHealthMiddleware);
1192
+ //#endregion
836
1193
  exports.Controller = Controller;
1194
+ exports.ControllerSchema = ControllerSchema;
837
1195
  exports.DELETE = DELETE;
838
1196
  Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
839
1197
  enumerable: true,
@@ -841,14 +1199,39 @@ Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
841
1199
  return DefaultBaseErrorMiddleware;
842
1200
  }
843
1201
  });
1202
+ Object.defineProperty(exports, "DefaultHealthMiddleware", {
1203
+ enumerable: true,
1204
+ get: function() {
1205
+ return DefaultHealthMiddleware;
1206
+ }
1207
+ });
1208
+ Object.defineProperty(exports, "DefaultOpenApiMiddleware", {
1209
+ enumerable: true,
1210
+ get: function() {
1211
+ return DefaultOpenApiMiddleware;
1212
+ }
1213
+ });
1214
+ Object.defineProperty(exports, "DefaultParserErrorMiddleware", {
1215
+ enumerable: true,
1216
+ get: function() {
1217
+ return DefaultParserErrorMiddleware;
1218
+ }
1219
+ });
844
1220
  Object.defineProperty(exports, "DefaultResponseStatusErrorMiddleware", {
845
1221
  enumerable: true,
846
1222
  get: function() {
847
1223
  return DefaultResponseStatusErrorMiddleware;
848
1224
  }
849
1225
  });
1226
+ exports.DefaultSwaggerMiddleware = DefaultSwaggerMiddleware;
850
1227
  exports.GET = GET;
851
1228
  exports.HEAD = HEAD;
1229
+ Object.defineProperty(exports, "HealthRegistrar", {
1230
+ enumerable: true,
1231
+ get: function() {
1232
+ return HealthRegistrar;
1233
+ }
1234
+ });
852
1235
  exports.Html404ErrorPage = Html404ErrorPage;
853
1236
  exports.HttpStatus = HttpStatus;
854
1237
  exports.Injectable = Injectable;
@@ -863,16 +1246,29 @@ exports.RedirectView = RedirectView;
863
1246
  exports.RequestBody = RequestBody;
864
1247
  exports.RequestParam = RequestParam;
865
1248
  exports.RequestQuery = RequestQuery;
1249
+ exports.ResponseBody = ResponseBody;
866
1250
  exports.ResponseEntity = ResponseEntity;
867
1251
  exports.ResponseEntityBuilder = ResponseEntityBuilder;
868
1252
  exports.ResponseStatusError = ResponseStatusError;
1253
+ exports.RouteSchema = RouteSchema;
869
1254
  exports.Sapling = Sapling;
870
1255
  exports._ControllerRegistry = _ControllerRegistry;
871
1256
  exports._InjectableDeps = _InjectableDeps;
872
1257
  exports._InjectableRegistry = _InjectableRegistry;
873
1258
  exports._Route = _Route;
874
- exports._getRequestSchemas = _getRequestSchemas;
1259
+ exports._clearOpenApiRegistry = _clearOpenApiRegistry;
1260
+ exports._getControllerSchema = _getControllerSchema;
1261
+ exports._getOrCreateSchemaDefinition = _getOrCreateSchemaDefinition;
1262
+ exports._getRouteSchema = _getRouteSchema;
875
1263
  exports._getRoutes = _getRoutes;
1264
+ exports._getValidatorSchema = _getValidatorSchema;
876
1265
  exports._parseOrThrow = _parseOrThrow;
1266
+ exports._registerController = _registerController;
877
1267
  exports._resolve = _resolve;
1268
+ exports._saveValidatorSchema = _saveValidatorSchema;
1269
+ exports._setControllerSchema = _setControllerSchema;
1270
+ exports._setRouteSchema = _setRouteSchema;
1271
+ exports._settings = _settings;
1272
+ exports.generateOpenApiSpec = generateOpenApiSpec;
878
1273
  exports.methodResolve = methodResolve;
1274
+ exports.openApiGenerator = openApiGenerator;