@tahminator/sapling 2.0.5-beta.a565b2cc → 2.0.5-beta.aa3623ee

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
@@ -258,6 +258,7 @@ var ParserError = class ParserError extends ResponseStatusError {
258
258
  case "reqbody": return "request body";
259
259
  case "reqparams": return "request params";
260
260
  case "reqquery": return "request query";
261
+ case "resbody": return "response body";
261
262
  }
262
263
  })()}: ${formatted}`;
263
264
  }
@@ -381,134 +382,20 @@ var Sapling = class Sapling {
381
382
  static setSwaggerPath(path) {
382
383
  _settings.doc.swaggerPath = path;
383
384
  }
384
- };
385
- //#endregion
386
- //#region src/annotation/request.ts
387
- const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
388
- /**
389
- * Apply to a route method to have `request.body` be parsed by `schema`.
390
- *
391
- * This annotation will parse `request.body` & then override `request.body`.
392
- * You can then just simply cast `request.body` for your use
393
- *
394
- * @example
395
- * ```ts
396
- * const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
397
- * name: z.string(),
398
- * description: z.string().optional(),
399
- * });
400
- *
401
- * ⠀@Controller({ prefix: "/api/book" })
402
- * class BookController {
403
- * ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
404
- * ⠀@POST()
405
- * public createBook(request: e.Request) {
406
- * const { name, description } = request.body as unknown as z.infer<
407
- * typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
408
- * >;
409
- * }
410
- * }
411
- * ```
412
- */
413
- function RequestBody(schema) {
414
- return (target, propertyKey) => {
415
- const ctor = target.constructor;
416
- const fnName = String(propertyKey);
417
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
418
- };
419
- }
420
- /**
421
- * Apply to a route method to have `request.param` be parsed by `schema`.
422
- *
423
- * This annotation will parse `request.param` & then override `request.param`.
424
- * You can then just simply cast `request.param` for your use
425
- *
426
- * @example
427
- * ```ts
428
- * const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
429
- * bookId: z.string(),
430
- * });
431
- *
432
- * ⠀@Controller({ prefix: "/api/book" })
433
- * class BookController {
434
- * ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
435
- * ⠀@GET("/:bookId")
436
- * public getBook(request: e.Request) {
437
- * const { bookId } = request.param as unknown as z.infer<
438
- * typeof GET_BOOK_REQUEST_PARAM_SCHEMA
439
- * >;
440
- * }
441
- * }
442
- * ```
443
- */
444
- function RequestParam(schema) {
445
- return (target, propertyKey) => {
446
- const ctor = target.constructor;
447
- const fnName = String(propertyKey);
448
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
449
- };
450
- }
451
- /**
452
- * Apply to a route method to have `request.query` be parsed by `schema`.
453
- *
454
- * This annotation will parse `request.query` & then override `request.query`.
455
- * You can then just simply cast `request.query` for your use
456
- *
457
- * @example
458
- * ```ts
459
- * const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
460
- * sort: z.enum(["name", "createdAt"]).optional(),
461
- * q: z.string().optional(),
462
- * });
463
- *
464
- * ⠀@Controller({ prefix: "/api/book" })
465
- * class BookController {
466
- * ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
467
- * ⠀@GET()
468
- * public listBooks(request: e.Request) {
469
- * const { sort, q } = request.query as unknown as z.infer<
470
- * typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
471
- * >;
472
- * }
473
- * }
474
- * ```
475
- */
476
- function RequestQuery(schema) {
477
- return (target, propertyKey) => {
478
- const ctor = target.constructor;
479
- const fnName = String(propertyKey);
480
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
481
- };
482
- }
483
- function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
484
- const byFn = (() => {
485
- const fn = _requestSchemaStore.get(ctor);
486
- if (fn) return fn;
487
- const newFn = /* @__PURE__ */ new Map();
488
- _requestSchemaStore.set(ctor, newFn);
489
- return newFn;
490
- })();
491
- const existing = byFn.get(fnName);
492
- if (existing) return existing;
493
- const created = {};
494
- byFn.set(fnName, created);
495
- return created;
496
- }
497
- function _setOnce(def, key, schema, fnName) {
498
- if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
499
- def[key] = schema;
500
- }
501
- function _getRequestSchemas(ctor, fnName) {
502
- return _requestSchemaStore.get(ctor)?.get(fnName);
503
- }
504
- async function _parseOrThrow(schema, input, kind) {
505
- const result = await schema["~standard"].validate(input);
506
- if (result.issues) {
507
- console.debug(`Failed to parse a schema`);
508
- throw new ParserError(kind, result.issues, schema["~standard"].vendor);
385
+ static chainHandlers(handlers, request, response, next, index = 0) {
386
+ if (index >= handlers.length) {
387
+ next();
388
+ return;
389
+ }
390
+ handlers[index]?.(request, response, (err) => {
391
+ if (err) {
392
+ next(err);
393
+ return;
394
+ }
395
+ Sapling.chainHandlers(handlers, request, response, next, index + 1);
396
+ });
509
397
  }
510
- return result.value;
511
- }
398
+ };
512
399
  //#endregion
513
400
  //#region src/annotation/route.ts
514
401
  const _routeStore = /* @__PURE__ */ new WeakMap();
@@ -591,6 +478,42 @@ function _getRoutes(ctor) {
591
478
  return _routeStore.get(ctor) ?? [];
592
479
  }
593
480
  //#endregion
481
+ //#region src/utils.ts
482
+ function _getOrCreateMap(store, ctor) {
483
+ const existing = store.get(ctor);
484
+ if (existing) return existing;
485
+ const created = /* @__PURE__ */ new Map();
486
+ store.set(ctor, created);
487
+ return created;
488
+ }
489
+ //#endregion
490
+ //#region src/annotation/schema.ts
491
+ const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
492
+ const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
493
+ function ControllerSchema(options) {
494
+ return (target) => {
495
+ _setControllerSchema(target, options);
496
+ };
497
+ }
498
+ function RouteSchema(options) {
499
+ return (target, propertyKey) => {
500
+ const ctor = target.constructor;
501
+ _setRouteSchema(ctor, String(propertyKey), options);
502
+ };
503
+ }
504
+ function _setRouteSchema(ctor, fnName, options) {
505
+ _getOrCreateMap(_routeSchemaStore, ctor).set(fnName, options);
506
+ }
507
+ function _setControllerSchema(ctor, options) {
508
+ _controllerSchemaStore.set(ctor, options);
509
+ }
510
+ function _getRouteSchema(ctor, fnName) {
511
+ return _routeSchemaStore.get(ctor)?.get(fnName);
512
+ }
513
+ function _getControllerSchema(ctor) {
514
+ return _controllerSchemaStore.get(ctor);
515
+ }
516
+ //#endregion
594
517
  //#region src/helper/openapi.ts
595
518
  var OpenAPIGenerator = class {
596
519
  constructor() {
@@ -612,42 +535,78 @@ var OpenAPIGenerator = class {
612
535
  generateSpec() {
613
536
  const config = this.config;
614
537
  const paths = {};
538
+ const tags = [];
615
539
  for (const { class: controllerClass, prefix } of this.controllers) {
616
540
  const routes = _getRoutes(controllerClass);
541
+ const controllerSchema = _getControllerSchema(controllerClass);
542
+ if (controllerSchema?.title) tags.push({
543
+ name: controllerSchema.title,
544
+ description: controllerSchema.description
545
+ });
617
546
  for (const route of routes) {
618
547
  if (route.method === "USE") continue;
619
- const schemas = _getRequestSchemas(controllerClass, route.fnName);
620
- const fullPath = route.path instanceof RegExp ? route.path.source : prefix + route.path;
621
- const openApiPath = typeof fullPath === "string" ? fullPath.replace(/:(\w+)/g, "{$1}") : fullPath;
548
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
549
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
550
+ 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.`);
551
+ const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
622
552
  if (!paths[openApiPath]) paths[openApiPath] = {};
623
- const operation = { responses: { "200": { description: "Successful response" } } };
553
+ const responses = {};
554
+ if (schemas?.responseBody) {
555
+ const responseSchema = this.toJsonSchema(schemas.responseBody, "input");
556
+ responses["200"] = {
557
+ description: responseSchema.description ?? "Successful response",
558
+ content: { "application/json": { schema: responseSchema } }
559
+ };
560
+ } else responses["200"] = { description: "Successful response" };
561
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) {
562
+ const responseSchema = this.toJsonSchema(resp.schema, "input");
563
+ responses[String(resp.statusCode)] = {
564
+ description: responseSchema.description ?? `Response ${resp.statusCode}`,
565
+ content: { "application/json": { schema: responseSchema } }
566
+ };
567
+ }
568
+ const operation = {
569
+ responses,
570
+ description: routeSchema?.description,
571
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
572
+ };
624
573
  const parameters = [];
625
- if (schemas?.param) {
626
- const paramSchema = this.toJsonSchema(schemas.param);
627
- if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
628
- name,
629
- in: "path",
630
- required: true,
631
- schema
632
- });
574
+ if (schemas?.requestParam) {
575
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
576
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
577
+ const parameterSchema = schema;
578
+ parameters.push({
579
+ name,
580
+ in: "path",
581
+ required: true,
582
+ description: parameterSchema.description,
583
+ schema: parameterSchema
584
+ });
585
+ }
633
586
  }
634
- if (schemas?.query) {
635
- const querySchema = this.toJsonSchema(schemas.query);
587
+ if (schemas?.requestQuery) {
588
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
636
589
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
637
590
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
591
+ const parameterSchema = schema;
638
592
  parameters.push({
639
593
  name,
640
594
  in: "query",
641
595
  required: isRequired,
642
- schema
596
+ description: parameterSchema.description,
597
+ schema: parameterSchema
643
598
  });
644
599
  }
645
600
  }
646
601
  if (parameters.length > 0) operation.parameters = parameters;
647
- if (schemas?.body) operation.requestBody = {
648
- required: true,
649
- content: { "application/json": { schema: this.toJsonSchema(schemas.body) } }
650
- };
602
+ if (schemas?.requestBody) {
603
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
604
+ operation.requestBody = {
605
+ required: true,
606
+ description: requestSchema.description,
607
+ content: { "application/json": { schema: requestSchema } }
608
+ };
609
+ }
651
610
  const method = route.method.toLowerCase();
652
611
  paths[openApiPath][method] = operation;
653
612
  }
@@ -659,12 +618,15 @@ var OpenAPIGenerator = class {
659
618
  version: config.version,
660
619
  description: config.description
661
620
  },
621
+ tags: tags.length > 0 ? tags : void 0,
662
622
  paths
663
623
  };
664
624
  }
665
- toJsonSchema(schema) {
625
+ toJsonSchema(schema, direction = "output") {
666
626
  try {
667
- return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
627
+ const jsonSchema = schema["~standard"].jsonSchema;
628
+ if (direction === "input" && jsonSchema.input) return jsonSchema.input({ target: "openapi-3.0" });
629
+ return jsonSchema.output({ target: "openapi-3.0" });
668
630
  } catch (e) {
669
631
  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`);
670
632
  throw e;
@@ -675,10 +637,10 @@ const openApiGenerator = new OpenAPIGenerator();
675
637
  function _registerControllerClass(controllerClass, prefix) {
676
638
  openApiGenerator.registerController(controllerClass, prefix);
677
639
  }
678
- function _setOpenApiConfig(config) {
640
+ function setOpenApiConfig(config) {
679
641
  openApiGenerator.setConfig(config);
680
642
  }
681
- function _generateOpenApiSpec() {
643
+ function generateOpenApiSpec() {
682
644
  return openApiGenerator.generateSpec();
683
645
  }
684
646
  //#endregion
@@ -814,6 +776,60 @@ function _resolve(ctor) {
814
776
  return _InjectableRegistry.get(ctor);
815
777
  }
816
778
  //#endregion
779
+ //#region src/annotation/validator.ts
780
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
781
+ function ResponseBody(schema) {
782
+ return (target, propertyKey) => {
783
+ const ctor = target.constructor;
784
+ const fnName = String(propertyKey);
785
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
786
+ };
787
+ }
788
+ function RequestBody(schema) {
789
+ return (target, propertyKey) => {
790
+ const ctor = target.constructor;
791
+ const fnName = String(propertyKey);
792
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
793
+ };
794
+ }
795
+ function RequestParam(schema) {
796
+ return (target, propertyKey) => {
797
+ const ctor = target.constructor;
798
+ const fnName = String(propertyKey);
799
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
800
+ };
801
+ }
802
+ function RequestQuery(schema) {
803
+ return (target, propertyKey) => {
804
+ const ctor = target.constructor;
805
+ const fnName = String(propertyKey);
806
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
807
+ };
808
+ }
809
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
810
+ const byFn = _getOrCreateMap(_validatorSchemaStore, ctor);
811
+ const existing = byFn.get(fnName);
812
+ if (existing) return existing;
813
+ const created = {};
814
+ byFn.set(fnName, created);
815
+ return created;
816
+ }
817
+ async function _parseOrThrow(schema, input, kind) {
818
+ const result = await schema["~standard"].validate(input);
819
+ if (result.issues) {
820
+ console.debug(`Failed to parse a schema`);
821
+ throw new ParserError(kind, result.issues, schema["~standard"].vendor);
822
+ }
823
+ return result.value;
824
+ }
825
+ function _getValidatorSchema(ctor, fnName) {
826
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
827
+ }
828
+ function _setOnce(def, key, schema, fnName) {
829
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
830
+ def[key] = schema;
831
+ }
832
+ //#endregion
817
833
  //#region src/annotation/controller.ts
818
834
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
819
835
  /**
@@ -868,12 +884,12 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
868
884
  return;
869
885
  }
870
886
  router[methodName](fp, async (request, response, next) => {
871
- const schemas = _getRequestSchemas(target, fnName);
887
+ const schemas = _getValidatorSchema(target, fnName);
872
888
  if (schemas) {
873
- if (schemas.body) request.body = await _parseOrThrow(schemas.body, request.body, "reqbody");
874
- if (schemas.param) request.params = await _parseOrThrow(schemas.param, request.params, "reqparams");
875
- if (schemas.query) {
876
- const parsedQuery = await _parseOrThrow(schemas.query, request.query, "reqquery");
889
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody");
890
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams");
891
+ if (schemas.requestQuery) {
892
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery");
877
893
  Object.defineProperty(request, "query", {
878
894
  value: parsedQuery,
879
895
  writable: true,
@@ -883,7 +899,8 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
883
899
  }
884
900
  const result = await fn.bind(controllerInstance)(request, response, next);
885
901
  if (result instanceof ResponseEntity) {
886
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
902
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody") : result.getBody();
903
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
887
904
  return;
888
905
  }
889
906
  if (result instanceof RedirectView) {
@@ -950,7 +967,7 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
950
967
  //#region src/middleware/default/openapi/index.ts
951
968
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
952
969
  handle(_request, _response, _next) {
953
- return ResponseEntity.ok().body(_generateOpenApiSpec());
970
+ return ResponseEntity.ok().body(generateOpenApiSpec());
954
971
  }
955
972
  };
956
973
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
@@ -961,21 +978,21 @@ let Serve = class Serve {
961
978
  constructor() {
962
979
  this.handlers = swagger_ui_express.default.serve;
963
980
  }
964
- handle(_request, _response, _next) {
965
- return this.handlers;
981
+ handle(request, response, next) {
982
+ return Sapling.chainHandlers(this.handlers, request, response, next);
966
983
  }
967
984
  };
968
- __decorate([Middleware()], Serve.prototype, "handle", null);
985
+ __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
969
986
  Serve = __decorate([MiddlewareClass()], Serve);
970
987
  let Setup = class Setup {
971
988
  constructor() {
972
- this.handler = swagger_ui_express.default.setup(void 0, { swaggerOptions: { url: _settings.doc.openApiPath } });
989
+ this.handler = swagger_ui_express.default.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
973
990
  }
974
991
  handle(request, response, next) {
975
992
  return this.handler(request, response, next);
976
993
  }
977
994
  };
978
- __decorate([Middleware()], Setup.prototype, "handle", null);
995
+ __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
979
996
  Setup = __decorate([MiddlewareClass()], Setup);
980
997
  const DefaultSwaggerMiddleware = {
981
998
  Serve,
@@ -983,6 +1000,7 @@ const DefaultSwaggerMiddleware = {
983
1000
  };
984
1001
  //#endregion
985
1002
  exports.Controller = Controller;
1003
+ exports.ControllerSchema = ControllerSchema;
986
1004
  exports.DELETE = DELETE;
987
1005
  Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
988
1006
  enumerable: true,
@@ -1025,21 +1043,29 @@ exports.RedirectView = RedirectView;
1025
1043
  exports.RequestBody = RequestBody;
1026
1044
  exports.RequestParam = RequestParam;
1027
1045
  exports.RequestQuery = RequestQuery;
1046
+ exports.ResponseBody = ResponseBody;
1028
1047
  exports.ResponseEntity = ResponseEntity;
1029
1048
  exports.ResponseEntityBuilder = ResponseEntityBuilder;
1030
1049
  exports.ResponseStatusError = ResponseStatusError;
1050
+ exports.RouteSchema = RouteSchema;
1031
1051
  exports.Sapling = Sapling;
1032
1052
  exports._ControllerRegistry = _ControllerRegistry;
1033
1053
  exports._InjectableDeps = _InjectableDeps;
1034
1054
  exports._InjectableRegistry = _InjectableRegistry;
1035
1055
  exports._Route = _Route;
1036
- exports._generateOpenApiSpec = _generateOpenApiSpec;
1037
- exports._getRequestSchemas = _getRequestSchemas;
1056
+ exports._getControllerSchema = _getControllerSchema;
1057
+ exports._getOrCreateSchemaDefinition = _getOrCreateSchemaDefinition;
1058
+ exports._getRouteSchema = _getRouteSchema;
1038
1059
  exports._getRoutes = _getRoutes;
1060
+ exports._getValidatorSchema = _getValidatorSchema;
1039
1061
  exports._parseOrThrow = _parseOrThrow;
1040
1062
  exports._registerControllerClass = _registerControllerClass;
1041
1063
  exports._resolve = _resolve;
1042
- exports._setOpenApiConfig = _setOpenApiConfig;
1064
+ exports._setControllerSchema = _setControllerSchema;
1065
+ exports._setOnce = _setOnce;
1066
+ exports._setRouteSchema = _setRouteSchema;
1043
1067
  exports._settings = _settings;
1068
+ exports.generateOpenApiSpec = generateOpenApiSpec;
1044
1069
  exports.methodResolve = methodResolve;
1045
1070
  exports.openApiGenerator = openApiGenerator;
1071
+ exports.setOpenApiConfig = setOpenApiConfig;
package/dist/index.d.cts CHANGED
@@ -437,7 +437,7 @@ declare class ResponseStatusError extends Error {
437
437
  }
438
438
  //#endregion
439
439
  //#region src/helper/error/parse.d.ts
440
- type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery";
440
+ type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery" | "resbody";
441
441
  /**
442
442
  * This error should be thrown when some data cannot be parsed by a given schema.
443
443
  */
@@ -531,6 +531,7 @@ declare class Sapling {
531
531
  static setDeserializeFn(this: void, fn: (value: string) => any): void;
532
532
  static setOpenApiPath(this: void, path: string): void;
533
533
  static setSwaggerPath(this: void, path: string): void;
534
+ static chainHandlers(this: void, handlers: RequestHandler[], request: Request, response: Response$1, next: NextFunction, index?: number): void;
534
535
  }
535
536
  //#endregion
536
537
  //#region node_modules/.pnpm/openapi-types@12.1.3/node_modules/openapi-types/dist/index.d.ts
@@ -869,94 +870,50 @@ declare class OpenAPIGenerator {
869
870
  }
870
871
  declare const openApiGenerator: OpenAPIGenerator;
871
872
  declare function _registerControllerClass(controllerClass: Function, prefix: string): void;
872
- declare function _setOpenApiConfig(config: OpenAPIConfig): void;
873
- declare function _generateOpenApiSpec(): OpenAPIV3.Document;
873
+ declare function setOpenApiConfig(config: OpenAPIConfig): void;
874
+ declare function generateOpenApiSpec(): OpenAPIV3.Document;
874
875
  //#endregion
875
- //#region src/annotation/request.d.ts
876
- type RequestSchemaDefinition = {
877
- body?: StandardSchemaV1 & StandardJSONSchemaV1;
878
- param?: StandardSchemaV1 & StandardJSONSchemaV1;
879
- query?: StandardSchemaV1 & StandardJSONSchemaV1;
876
+ //#region src/annotation/validator.d.ts
877
+ type ValidatorSchema = {
878
+ requestBody?: StandardSchemaV1 & StandardJSONSchemaV1;
879
+ requestParam?: StandardSchemaV1 & StandardJSONSchemaV1;
880
+ requestQuery?: StandardSchemaV1 & StandardJSONSchemaV1;
881
+ responseBody?: StandardSchemaV1 & StandardJSONSchemaV1;
880
882
  };
881
- /**
882
- * Apply to a route method to have `request.body` be parsed by `schema`.
883
- *
884
- * This annotation will parse `request.body` & then override `request.body`.
885
- * You can then just simply cast `request.body` for your use
886
- *
887
- * @example
888
- * ```ts
889
- * const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
890
- * name: z.string(),
891
- * description: z.string().optional(),
892
- * });
893
- *
894
- * ⠀@Controller({ prefix: "/api/book" })
895
- * class BookController {
896
- * ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
897
- * ⠀@POST()
898
- * public createBook(request: e.Request) {
899
- * const { name, description } = request.body as unknown as z.infer<
900
- * typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
901
- * >;
902
- * }
903
- * }
904
- * ```
905
- */
883
+ declare function ResponseBody(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
906
884
  declare function RequestBody(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
907
- /**
908
- * Apply to a route method to have `request.param` be parsed by `schema`.
909
- *
910
- * This annotation will parse `request.param` & then override `request.param`.
911
- * You can then just simply cast `request.param` for your use
912
- *
913
- * @example
914
- * ```ts
915
- * const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
916
- * bookId: z.string(),
917
- * });
918
- *
919
- * ⠀@Controller({ prefix: "/api/book" })
920
- * class BookController {
921
- * ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
922
- * ⠀@GET("/:bookId")
923
- * public getBook(request: e.Request) {
924
- * const { bookId } = request.param as unknown as z.infer<
925
- * typeof GET_BOOK_REQUEST_PARAM_SCHEMA
926
- * >;
927
- * }
928
- * }
929
- * ```
930
- */
931
885
  declare function RequestParam(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
932
- /**
933
- * Apply to a route method to have `request.query` be parsed by `schema`.
934
- *
935
- * This annotation will parse `request.query` & then override `request.query`.
936
- * You can then just simply cast `request.query` for your use
937
- *
938
- * @example
939
- * ```ts
940
- * const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
941
- * sort: z.enum(["name", "createdAt"]).optional(),
942
- * q: z.string().optional(),
943
- * });
944
- *
945
- * ⠀@Controller({ prefix: "/api/book" })
946
- * class BookController {
947
- * ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
948
- * ⠀@GET()
949
- * public listBooks(request: e.Request) {
950
- * const { sort, q } = request.query as unknown as z.infer<
951
- * typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
952
- * >;
953
- * }
954
- * }
955
- * ```
956
- */
957
886
  declare function RequestQuery(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
958
- declare function _getRequestSchemas(ctor: Function, fnName: string): RequestSchemaDefinition | undefined;
887
+ declare function _getOrCreateSchemaDefinition(ctor: Function, fnName: string): ValidatorSchema;
959
888
  declare function _parseOrThrow<TSchema extends StandardSchemaV1>(schema: TSchema, input: unknown, kind: ParserErrorLocation): Promise<StandardSchemaV1.InferOutput<TSchema>>;
889
+ declare function _getValidatorSchema(ctor: Function, fnName: string): ValidatorSchema | undefined;
890
+ declare function _setOnce(def: ValidatorSchema, key: keyof ValidatorSchema, schema: StandardSchemaV1 & StandardJSONSchemaV1, fnName: string): void;
891
+ //#endregion
892
+ //#region src/annotation/schema.d.ts
893
+ type ResponseSchema = {
894
+ statusCode: HttpStatus;
895
+ schema: StandardSchemaV1 & StandardJSONSchemaV1;
896
+ };
897
+ type RouteSchemaDefinition = {
898
+ description?: string;
899
+ responses?: ResponseSchema[];
900
+ };
901
+ type ControllerSchemaDefinition = {
902
+ title?: string;
903
+ description?: string;
904
+ };
905
+ declare function ControllerSchema(options: {
906
+ title?: string;
907
+ description?: string;
908
+ }): ClassDecorator;
909
+ declare function RouteSchema(options: {
910
+ description?: string;
911
+ responses?: ResponseSchema[];
912
+ }): MethodDecorator;
913
+ declare function _setRouteSchema(ctor: Function, fnName: string, options: RouteSchemaDefinition): void;
914
+ declare function _setControllerSchema(ctor: Function, options: ControllerSchemaDefinition): void;
915
+ declare function _getRouteSchema(ctor: Function, fnName: string): RouteSchemaDefinition | undefined;
916
+ declare function _getControllerSchema(ctor: Function): ControllerSchemaDefinition | undefined;
960
917
  //#endregion
961
918
  //#region src/middleware/default/error/base.d.ts
962
919
  /**
@@ -992,7 +949,7 @@ declare class DefaultOpenApiMiddleware {
992
949
  //#region src/middleware/default/swagger/index.d.ts
993
950
  declare class Serve {
994
951
  private readonly handlers;
995
- handle(_request: Request, _response: Response$1, _next: NextFunction): RequestHandler[];
952
+ handle(request: Request, response: Response$1, next: NextFunction): void;
996
953
  }
997
954
  declare class Setup {
998
955
  private readonly handler;
@@ -1004,4 +961,4 @@ declare const DefaultSwaggerMiddleware: {
1004
961
  Setup: typeof Setup;
1005
962
  };
1006
963
  //#endregion
1007
- export { Class, Controller, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _generateOpenApiSpec, _getRequestSchemas, _getRoutes, _parseOrThrow, _registerControllerClass, _resolve, _setOpenApiConfig, _settings, methodResolve, openApiGenerator };
964
+ export { Class, Controller, ControllerSchema, ControllerSchemaDefinition, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseSchema, ResponseStatusError, RouteDefinition, RouteSchema, RouteSchemaDefinition, Sapling, ValidatorSchema, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerControllerClass, _resolve, _setControllerSchema, _setOnce, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator, setOpenApiConfig };
package/dist/index.d.mts CHANGED
@@ -437,7 +437,7 @@ declare class ResponseStatusError extends Error {
437
437
  }
438
438
  //#endregion
439
439
  //#region src/helper/error/parse.d.ts
440
- type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery";
440
+ type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery" | "resbody";
441
441
  /**
442
442
  * This error should be thrown when some data cannot be parsed by a given schema.
443
443
  */
@@ -531,6 +531,7 @@ declare class Sapling {
531
531
  static setDeserializeFn(this: void, fn: (value: string) => any): void;
532
532
  static setOpenApiPath(this: void, path: string): void;
533
533
  static setSwaggerPath(this: void, path: string): void;
534
+ static chainHandlers(this: void, handlers: RequestHandler[], request: Request, response: Response$1, next: NextFunction, index?: number): void;
534
535
  }
535
536
  //#endregion
536
537
  //#region node_modules/.pnpm/openapi-types@12.1.3/node_modules/openapi-types/dist/index.d.ts
@@ -869,94 +870,50 @@ declare class OpenAPIGenerator {
869
870
  }
870
871
  declare const openApiGenerator: OpenAPIGenerator;
871
872
  declare function _registerControllerClass(controllerClass: Function, prefix: string): void;
872
- declare function _setOpenApiConfig(config: OpenAPIConfig): void;
873
- declare function _generateOpenApiSpec(): OpenAPIV3.Document;
873
+ declare function setOpenApiConfig(config: OpenAPIConfig): void;
874
+ declare function generateOpenApiSpec(): OpenAPIV3.Document;
874
875
  //#endregion
875
- //#region src/annotation/request.d.ts
876
- type RequestSchemaDefinition = {
877
- body?: StandardSchemaV1 & StandardJSONSchemaV1;
878
- param?: StandardSchemaV1 & StandardJSONSchemaV1;
879
- query?: StandardSchemaV1 & StandardJSONSchemaV1;
876
+ //#region src/annotation/validator.d.ts
877
+ type ValidatorSchema = {
878
+ requestBody?: StandardSchemaV1 & StandardJSONSchemaV1;
879
+ requestParam?: StandardSchemaV1 & StandardJSONSchemaV1;
880
+ requestQuery?: StandardSchemaV1 & StandardJSONSchemaV1;
881
+ responseBody?: StandardSchemaV1 & StandardJSONSchemaV1;
880
882
  };
881
- /**
882
- * Apply to a route method to have `request.body` be parsed by `schema`.
883
- *
884
- * This annotation will parse `request.body` & then override `request.body`.
885
- * You can then just simply cast `request.body` for your use
886
- *
887
- * @example
888
- * ```ts
889
- * const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
890
- * name: z.string(),
891
- * description: z.string().optional(),
892
- * });
893
- *
894
- * ⠀@Controller({ prefix: "/api/book" })
895
- * class BookController {
896
- * ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
897
- * ⠀@POST()
898
- * public createBook(request: e.Request) {
899
- * const { name, description } = request.body as unknown as z.infer<
900
- * typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
901
- * >;
902
- * }
903
- * }
904
- * ```
905
- */
883
+ declare function ResponseBody(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
906
884
  declare function RequestBody(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
907
- /**
908
- * Apply to a route method to have `request.param` be parsed by `schema`.
909
- *
910
- * This annotation will parse `request.param` & then override `request.param`.
911
- * You can then just simply cast `request.param` for your use
912
- *
913
- * @example
914
- * ```ts
915
- * const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
916
- * bookId: z.string(),
917
- * });
918
- *
919
- * ⠀@Controller({ prefix: "/api/book" })
920
- * class BookController {
921
- * ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
922
- * ⠀@GET("/:bookId")
923
- * public getBook(request: e.Request) {
924
- * const { bookId } = request.param as unknown as z.infer<
925
- * typeof GET_BOOK_REQUEST_PARAM_SCHEMA
926
- * >;
927
- * }
928
- * }
929
- * ```
930
- */
931
885
  declare function RequestParam(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
932
- /**
933
- * Apply to a route method to have `request.query` be parsed by `schema`.
934
- *
935
- * This annotation will parse `request.query` & then override `request.query`.
936
- * You can then just simply cast `request.query` for your use
937
- *
938
- * @example
939
- * ```ts
940
- * const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
941
- * sort: z.enum(["name", "createdAt"]).optional(),
942
- * q: z.string().optional(),
943
- * });
944
- *
945
- * ⠀@Controller({ prefix: "/api/book" })
946
- * class BookController {
947
- * ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
948
- * ⠀@GET()
949
- * public listBooks(request: e.Request) {
950
- * const { sort, q } = request.query as unknown as z.infer<
951
- * typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
952
- * >;
953
- * }
954
- * }
955
- * ```
956
- */
957
886
  declare function RequestQuery(schema: StandardSchemaV1 & StandardJSONSchemaV1): MethodDecorator;
958
- declare function _getRequestSchemas(ctor: Function, fnName: string): RequestSchemaDefinition | undefined;
887
+ declare function _getOrCreateSchemaDefinition(ctor: Function, fnName: string): ValidatorSchema;
959
888
  declare function _parseOrThrow<TSchema extends StandardSchemaV1>(schema: TSchema, input: unknown, kind: ParserErrorLocation): Promise<StandardSchemaV1.InferOutput<TSchema>>;
889
+ declare function _getValidatorSchema(ctor: Function, fnName: string): ValidatorSchema | undefined;
890
+ declare function _setOnce(def: ValidatorSchema, key: keyof ValidatorSchema, schema: StandardSchemaV1 & StandardJSONSchemaV1, fnName: string): void;
891
+ //#endregion
892
+ //#region src/annotation/schema.d.ts
893
+ type ResponseSchema = {
894
+ statusCode: HttpStatus;
895
+ schema: StandardSchemaV1 & StandardJSONSchemaV1;
896
+ };
897
+ type RouteSchemaDefinition = {
898
+ description?: string;
899
+ responses?: ResponseSchema[];
900
+ };
901
+ type ControllerSchemaDefinition = {
902
+ title?: string;
903
+ description?: string;
904
+ };
905
+ declare function ControllerSchema(options: {
906
+ title?: string;
907
+ description?: string;
908
+ }): ClassDecorator;
909
+ declare function RouteSchema(options: {
910
+ description?: string;
911
+ responses?: ResponseSchema[];
912
+ }): MethodDecorator;
913
+ declare function _setRouteSchema(ctor: Function, fnName: string, options: RouteSchemaDefinition): void;
914
+ declare function _setControllerSchema(ctor: Function, options: ControllerSchemaDefinition): void;
915
+ declare function _getRouteSchema(ctor: Function, fnName: string): RouteSchemaDefinition | undefined;
916
+ declare function _getControllerSchema(ctor: Function): ControllerSchemaDefinition | undefined;
960
917
  //#endregion
961
918
  //#region src/middleware/default/error/base.d.ts
962
919
  /**
@@ -992,7 +949,7 @@ declare class DefaultOpenApiMiddleware {
992
949
  //#region src/middleware/default/swagger/index.d.ts
993
950
  declare class Serve {
994
951
  private readonly handlers;
995
- handle(_request: Request, _response: Response$1, _next: NextFunction): RequestHandler[];
952
+ handle(request: Request, response: Response$1, next: NextFunction): void;
996
953
  }
997
954
  declare class Setup {
998
955
  private readonly handler;
@@ -1004,4 +961,4 @@ declare const DefaultSwaggerMiddleware: {
1004
961
  Setup: typeof Setup;
1005
962
  };
1006
963
  //#endregion
1007
- export { Class, Controller, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _generateOpenApiSpec, _getRequestSchemas, _getRoutes, _parseOrThrow, _registerControllerClass, _resolve, _setOpenApiConfig, _settings, methodResolve, openApiGenerator };
964
+ export { Class, Controller, ControllerSchema, ControllerSchemaDefinition, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseSchema, ResponseStatusError, RouteDefinition, RouteSchema, RouteSchemaDefinition, Sapling, ValidatorSchema, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerControllerClass, _resolve, _setControllerSchema, _setOnce, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator, setOpenApiConfig };
package/dist/index.mjs CHANGED
@@ -233,6 +233,7 @@ var ParserError = class ParserError extends ResponseStatusError {
233
233
  case "reqbody": return "request body";
234
234
  case "reqparams": return "request params";
235
235
  case "reqquery": return "request query";
236
+ case "resbody": return "response body";
236
237
  }
237
238
  })()}: ${formatted}`;
238
239
  }
@@ -356,134 +357,20 @@ var Sapling = class Sapling {
356
357
  static setSwaggerPath(path) {
357
358
  _settings.doc.swaggerPath = path;
358
359
  }
359
- };
360
- //#endregion
361
- //#region src/annotation/request.ts
362
- const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
363
- /**
364
- * Apply to a route method to have `request.body` be parsed by `schema`.
365
- *
366
- * This annotation will parse `request.body` & then override `request.body`.
367
- * You can then just simply cast `request.body` for your use
368
- *
369
- * @example
370
- * ```ts
371
- * const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
372
- * name: z.string(),
373
- * description: z.string().optional(),
374
- * });
375
- *
376
- * ⠀@Controller({ prefix: "/api/book" })
377
- * class BookController {
378
- * ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
379
- * ⠀@POST()
380
- * public createBook(request: e.Request) {
381
- * const { name, description } = request.body as unknown as z.infer<
382
- * typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
383
- * >;
384
- * }
385
- * }
386
- * ```
387
- */
388
- function RequestBody(schema) {
389
- return (target, propertyKey) => {
390
- const ctor = target.constructor;
391
- const fnName = String(propertyKey);
392
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
393
- };
394
- }
395
- /**
396
- * Apply to a route method to have `request.param` be parsed by `schema`.
397
- *
398
- * This annotation will parse `request.param` & then override `request.param`.
399
- * You can then just simply cast `request.param` for your use
400
- *
401
- * @example
402
- * ```ts
403
- * const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
404
- * bookId: z.string(),
405
- * });
406
- *
407
- * ⠀@Controller({ prefix: "/api/book" })
408
- * class BookController {
409
- * ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
410
- * ⠀@GET("/:bookId")
411
- * public getBook(request: e.Request) {
412
- * const { bookId } = request.param as unknown as z.infer<
413
- * typeof GET_BOOK_REQUEST_PARAM_SCHEMA
414
- * >;
415
- * }
416
- * }
417
- * ```
418
- */
419
- function RequestParam(schema) {
420
- return (target, propertyKey) => {
421
- const ctor = target.constructor;
422
- const fnName = String(propertyKey);
423
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
424
- };
425
- }
426
- /**
427
- * Apply to a route method to have `request.query` be parsed by `schema`.
428
- *
429
- * This annotation will parse `request.query` & then override `request.query`.
430
- * You can then just simply cast `request.query` for your use
431
- *
432
- * @example
433
- * ```ts
434
- * const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
435
- * sort: z.enum(["name", "createdAt"]).optional(),
436
- * q: z.string().optional(),
437
- * });
438
- *
439
- * ⠀@Controller({ prefix: "/api/book" })
440
- * class BookController {
441
- * ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
442
- * ⠀@GET()
443
- * public listBooks(request: e.Request) {
444
- * const { sort, q } = request.query as unknown as z.infer<
445
- * typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
446
- * >;
447
- * }
448
- * }
449
- * ```
450
- */
451
- function RequestQuery(schema) {
452
- return (target, propertyKey) => {
453
- const ctor = target.constructor;
454
- const fnName = String(propertyKey);
455
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
456
- };
457
- }
458
- function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
459
- const byFn = (() => {
460
- const fn = _requestSchemaStore.get(ctor);
461
- if (fn) return fn;
462
- const newFn = /* @__PURE__ */ new Map();
463
- _requestSchemaStore.set(ctor, newFn);
464
- return newFn;
465
- })();
466
- const existing = byFn.get(fnName);
467
- if (existing) return existing;
468
- const created = {};
469
- byFn.set(fnName, created);
470
- return created;
471
- }
472
- function _setOnce(def, key, schema, fnName) {
473
- if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
474
- def[key] = schema;
475
- }
476
- function _getRequestSchemas(ctor, fnName) {
477
- return _requestSchemaStore.get(ctor)?.get(fnName);
478
- }
479
- async function _parseOrThrow(schema, input, kind) {
480
- const result = await schema["~standard"].validate(input);
481
- if (result.issues) {
482
- console.debug(`Failed to parse a schema`);
483
- throw new ParserError(kind, result.issues, schema["~standard"].vendor);
360
+ static chainHandlers(handlers, request, response, next, index = 0) {
361
+ if (index >= handlers.length) {
362
+ next();
363
+ return;
364
+ }
365
+ handlers[index]?.(request, response, (err) => {
366
+ if (err) {
367
+ next(err);
368
+ return;
369
+ }
370
+ Sapling.chainHandlers(handlers, request, response, next, index + 1);
371
+ });
484
372
  }
485
- return result.value;
486
- }
373
+ };
487
374
  //#endregion
488
375
  //#region src/annotation/route.ts
489
376
  const _routeStore = /* @__PURE__ */ new WeakMap();
@@ -566,6 +453,42 @@ function _getRoutes(ctor) {
566
453
  return _routeStore.get(ctor) ?? [];
567
454
  }
568
455
  //#endregion
456
+ //#region src/utils.ts
457
+ function _getOrCreateMap(store, ctor) {
458
+ const existing = store.get(ctor);
459
+ if (existing) return existing;
460
+ const created = /* @__PURE__ */ new Map();
461
+ store.set(ctor, created);
462
+ return created;
463
+ }
464
+ //#endregion
465
+ //#region src/annotation/schema.ts
466
+ const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
467
+ const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
468
+ function ControllerSchema(options) {
469
+ return (target) => {
470
+ _setControllerSchema(target, options);
471
+ };
472
+ }
473
+ function RouteSchema(options) {
474
+ return (target, propertyKey) => {
475
+ const ctor = target.constructor;
476
+ _setRouteSchema(ctor, String(propertyKey), options);
477
+ };
478
+ }
479
+ function _setRouteSchema(ctor, fnName, options) {
480
+ _getOrCreateMap(_routeSchemaStore, ctor).set(fnName, options);
481
+ }
482
+ function _setControllerSchema(ctor, options) {
483
+ _controllerSchemaStore.set(ctor, options);
484
+ }
485
+ function _getRouteSchema(ctor, fnName) {
486
+ return _routeSchemaStore.get(ctor)?.get(fnName);
487
+ }
488
+ function _getControllerSchema(ctor) {
489
+ return _controllerSchemaStore.get(ctor);
490
+ }
491
+ //#endregion
569
492
  //#region src/helper/openapi.ts
570
493
  var OpenAPIGenerator = class {
571
494
  constructor() {
@@ -587,42 +510,78 @@ var OpenAPIGenerator = class {
587
510
  generateSpec() {
588
511
  const config = this.config;
589
512
  const paths = {};
513
+ const tags = [];
590
514
  for (const { class: controllerClass, prefix } of this.controllers) {
591
515
  const routes = _getRoutes(controllerClass);
516
+ const controllerSchema = _getControllerSchema(controllerClass);
517
+ if (controllerSchema?.title) tags.push({
518
+ name: controllerSchema.title,
519
+ description: controllerSchema.description
520
+ });
592
521
  for (const route of routes) {
593
522
  if (route.method === "USE") continue;
594
- const schemas = _getRequestSchemas(controllerClass, route.fnName);
595
- const fullPath = route.path instanceof RegExp ? route.path.source : prefix + route.path;
596
- const openApiPath = typeof fullPath === "string" ? fullPath.replace(/:(\w+)/g, "{$1}") : fullPath;
523
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
524
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
525
+ 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.`);
526
+ const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
597
527
  if (!paths[openApiPath]) paths[openApiPath] = {};
598
- const operation = { responses: { "200": { description: "Successful response" } } };
528
+ const responses = {};
529
+ if (schemas?.responseBody) {
530
+ const responseSchema = this.toJsonSchema(schemas.responseBody, "input");
531
+ responses["200"] = {
532
+ description: responseSchema.description ?? "Successful response",
533
+ content: { "application/json": { schema: responseSchema } }
534
+ };
535
+ } else responses["200"] = { description: "Successful response" };
536
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) {
537
+ const responseSchema = this.toJsonSchema(resp.schema, "input");
538
+ responses[String(resp.statusCode)] = {
539
+ description: responseSchema.description ?? `Response ${resp.statusCode}`,
540
+ content: { "application/json": { schema: responseSchema } }
541
+ };
542
+ }
543
+ const operation = {
544
+ responses,
545
+ description: routeSchema?.description,
546
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
547
+ };
599
548
  const parameters = [];
600
- if (schemas?.param) {
601
- const paramSchema = this.toJsonSchema(schemas.param);
602
- if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
603
- name,
604
- in: "path",
605
- required: true,
606
- schema
607
- });
549
+ if (schemas?.requestParam) {
550
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
551
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
552
+ const parameterSchema = schema;
553
+ parameters.push({
554
+ name,
555
+ in: "path",
556
+ required: true,
557
+ description: parameterSchema.description,
558
+ schema: parameterSchema
559
+ });
560
+ }
608
561
  }
609
- if (schemas?.query) {
610
- const querySchema = this.toJsonSchema(schemas.query);
562
+ if (schemas?.requestQuery) {
563
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
611
564
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
612
565
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
566
+ const parameterSchema = schema;
613
567
  parameters.push({
614
568
  name,
615
569
  in: "query",
616
570
  required: isRequired,
617
- schema
571
+ description: parameterSchema.description,
572
+ schema: parameterSchema
618
573
  });
619
574
  }
620
575
  }
621
576
  if (parameters.length > 0) operation.parameters = parameters;
622
- if (schemas?.body) operation.requestBody = {
623
- required: true,
624
- content: { "application/json": { schema: this.toJsonSchema(schemas.body) } }
625
- };
577
+ if (schemas?.requestBody) {
578
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
579
+ operation.requestBody = {
580
+ required: true,
581
+ description: requestSchema.description,
582
+ content: { "application/json": { schema: requestSchema } }
583
+ };
584
+ }
626
585
  const method = route.method.toLowerCase();
627
586
  paths[openApiPath][method] = operation;
628
587
  }
@@ -634,12 +593,15 @@ var OpenAPIGenerator = class {
634
593
  version: config.version,
635
594
  description: config.description
636
595
  },
596
+ tags: tags.length > 0 ? tags : void 0,
637
597
  paths
638
598
  };
639
599
  }
640
- toJsonSchema(schema) {
600
+ toJsonSchema(schema, direction = "output") {
641
601
  try {
642
- return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
602
+ const jsonSchema = schema["~standard"].jsonSchema;
603
+ if (direction === "input" && jsonSchema.input) return jsonSchema.input({ target: "openapi-3.0" });
604
+ return jsonSchema.output({ target: "openapi-3.0" });
643
605
  } catch (e) {
644
606
  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`);
645
607
  throw e;
@@ -650,10 +612,10 @@ const openApiGenerator = new OpenAPIGenerator();
650
612
  function _registerControllerClass(controllerClass, prefix) {
651
613
  openApiGenerator.registerController(controllerClass, prefix);
652
614
  }
653
- function _setOpenApiConfig(config) {
615
+ function setOpenApiConfig(config) {
654
616
  openApiGenerator.setConfig(config);
655
617
  }
656
- function _generateOpenApiSpec() {
618
+ function generateOpenApiSpec() {
657
619
  return openApiGenerator.generateSpec();
658
620
  }
659
621
  //#endregion
@@ -789,6 +751,60 @@ function _resolve(ctor) {
789
751
  return _InjectableRegistry.get(ctor);
790
752
  }
791
753
  //#endregion
754
+ //#region src/annotation/validator.ts
755
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
756
+ function ResponseBody(schema) {
757
+ return (target, propertyKey) => {
758
+ const ctor = target.constructor;
759
+ const fnName = String(propertyKey);
760
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
761
+ };
762
+ }
763
+ function RequestBody(schema) {
764
+ return (target, propertyKey) => {
765
+ const ctor = target.constructor;
766
+ const fnName = String(propertyKey);
767
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
768
+ };
769
+ }
770
+ function RequestParam(schema) {
771
+ return (target, propertyKey) => {
772
+ const ctor = target.constructor;
773
+ const fnName = String(propertyKey);
774
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
775
+ };
776
+ }
777
+ function RequestQuery(schema) {
778
+ return (target, propertyKey) => {
779
+ const ctor = target.constructor;
780
+ const fnName = String(propertyKey);
781
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
782
+ };
783
+ }
784
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
785
+ const byFn = _getOrCreateMap(_validatorSchemaStore, ctor);
786
+ const existing = byFn.get(fnName);
787
+ if (existing) return existing;
788
+ const created = {};
789
+ byFn.set(fnName, created);
790
+ return created;
791
+ }
792
+ async function _parseOrThrow(schema, input, kind) {
793
+ const result = await schema["~standard"].validate(input);
794
+ if (result.issues) {
795
+ console.debug(`Failed to parse a schema`);
796
+ throw new ParserError(kind, result.issues, schema["~standard"].vendor);
797
+ }
798
+ return result.value;
799
+ }
800
+ function _getValidatorSchema(ctor, fnName) {
801
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
802
+ }
803
+ function _setOnce(def, key, schema, fnName) {
804
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
805
+ def[key] = schema;
806
+ }
807
+ //#endregion
792
808
  //#region src/annotation/controller.ts
793
809
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
794
810
  /**
@@ -843,12 +859,12 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
843
859
  return;
844
860
  }
845
861
  router[methodName](fp, async (request, response, next) => {
846
- const schemas = _getRequestSchemas(target, fnName);
862
+ const schemas = _getValidatorSchema(target, fnName);
847
863
  if (schemas) {
848
- if (schemas.body) request.body = await _parseOrThrow(schemas.body, request.body, "reqbody");
849
- if (schemas.param) request.params = await _parseOrThrow(schemas.param, request.params, "reqparams");
850
- if (schemas.query) {
851
- const parsedQuery = await _parseOrThrow(schemas.query, request.query, "reqquery");
864
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody");
865
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams");
866
+ if (schemas.requestQuery) {
867
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery");
852
868
  Object.defineProperty(request, "query", {
853
869
  value: parsedQuery,
854
870
  writable: true,
@@ -858,7 +874,8 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
858
874
  }
859
875
  const result = await fn.bind(controllerInstance)(request, response, next);
860
876
  if (result instanceof ResponseEntity) {
861
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
877
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody") : result.getBody();
878
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
862
879
  return;
863
880
  }
864
881
  if (result instanceof RedirectView) {
@@ -925,7 +942,7 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
925
942
  //#region src/middleware/default/openapi/index.ts
926
943
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
927
944
  handle(_request, _response, _next) {
928
- return ResponseEntity.ok().body(_generateOpenApiSpec());
945
+ return ResponseEntity.ok().body(generateOpenApiSpec());
929
946
  }
930
947
  };
931
948
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
@@ -936,25 +953,25 @@ let Serve = class Serve {
936
953
  constructor() {
937
954
  this.handlers = swagger.serve;
938
955
  }
939
- handle(_request, _response, _next) {
940
- return this.handlers;
956
+ handle(request, response, next) {
957
+ return Sapling.chainHandlers(this.handlers, request, response, next);
941
958
  }
942
959
  };
943
- __decorate([Middleware()], Serve.prototype, "handle", null);
960
+ __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
944
961
  Serve = __decorate([MiddlewareClass()], Serve);
945
962
  let Setup = class Setup {
946
963
  constructor() {
947
- this.handler = swagger.setup(void 0, { swaggerOptions: { url: _settings.doc.openApiPath } });
964
+ this.handler = swagger.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
948
965
  }
949
966
  handle(request, response, next) {
950
967
  return this.handler(request, response, next);
951
968
  }
952
969
  };
953
- __decorate([Middleware()], Setup.prototype, "handle", null);
970
+ __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
954
971
  Setup = __decorate([MiddlewareClass()], Setup);
955
972
  const DefaultSwaggerMiddleware = {
956
973
  Serve,
957
974
  Setup
958
975
  };
959
976
  //#endregion
960
- export { Controller, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _generateOpenApiSpec, _getRequestSchemas, _getRoutes, _parseOrThrow, _registerControllerClass, _resolve, _setOpenApiConfig, _settings, methodResolve, openApiGenerator };
977
+ export { Controller, ControllerSchema, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteSchema, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerControllerClass, _resolve, _setControllerSchema, _setOnce, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator, setOpenApiConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tahminator/sapling",
3
- "version": "2.0.5-beta.a565b2cc",
3
+ "version": "2.0.5-beta.aa3623ee",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A library to help you write cleaner Express.js code",
6
6
  "repository": {