@tahminator/sapling 2.0.5-beta.662f05c0 → 2.0.5-beta.75e5e346

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,79 @@ 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, "output");
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 = resp.schema ? this.toJsonSchema(resp.schema, "output") : void 0;
563
+ responses[String(resp.statusCode)] = {
564
+ description: resp.description ?? responseSchema?.description ?? `Response ${resp.statusCode}`,
565
+ ...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
566
+ };
567
+ }
568
+ const operation = {
569
+ responses,
570
+ summary: routeSchema?.summary,
571
+ description: routeSchema?.description,
572
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
573
+ };
624
574
  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
- });
575
+ if (schemas?.requestParam) {
576
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
577
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
578
+ const parameterSchema = schema;
579
+ parameters.push({
580
+ name,
581
+ in: "path",
582
+ required: true,
583
+ description: parameterSchema.description,
584
+ schema: parameterSchema
585
+ });
586
+ }
633
587
  }
634
- if (schemas?.query) {
635
- const querySchema = this.toJsonSchema(schemas.query);
588
+ if (schemas?.requestQuery) {
589
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
636
590
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
637
591
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
592
+ const parameterSchema = schema;
638
593
  parameters.push({
639
594
  name,
640
595
  in: "query",
641
596
  required: isRequired,
642
- schema
597
+ description: parameterSchema.description,
598
+ schema: parameterSchema
643
599
  });
644
600
  }
645
601
  }
646
602
  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
- };
603
+ if (schemas?.requestBody) {
604
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
605
+ operation.requestBody = {
606
+ required: true,
607
+ description: requestSchema.description,
608
+ content: { "application/json": { schema: requestSchema } }
609
+ };
610
+ }
651
611
  const method = route.method.toLowerCase();
652
612
  paths[openApiPath][method] = operation;
653
613
  }
@@ -659,12 +619,15 @@ var OpenAPIGenerator = class {
659
619
  version: config.version,
660
620
  description: config.description
661
621
  },
622
+ tags: tags.length > 0 ? tags : void 0,
662
623
  paths
663
624
  };
664
625
  }
665
- toJsonSchema(schema) {
626
+ toJsonSchema(schema, direction = "output") {
666
627
  try {
667
- return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
628
+ const jsonSchema = schema["~standard"].jsonSchema;
629
+ if (direction === "input" && jsonSchema.input) return jsonSchema.input({ target: "openapi-3.0" });
630
+ return jsonSchema.output({ target: "openapi-3.0" });
668
631
  } catch (e) {
669
632
  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
633
  throw e;
@@ -675,10 +638,10 @@ const openApiGenerator = new OpenAPIGenerator();
675
638
  function _registerControllerClass(controllerClass, prefix) {
676
639
  openApiGenerator.registerController(controllerClass, prefix);
677
640
  }
678
- function _setOpenApiConfig(config) {
641
+ function setOpenApiConfig(config) {
679
642
  openApiGenerator.setConfig(config);
680
643
  }
681
- function _generateOpenApiSpec() {
644
+ function generateOpenApiSpec() {
682
645
  return openApiGenerator.generateSpec();
683
646
  }
684
647
  //#endregion
@@ -814,6 +777,60 @@ function _resolve(ctor) {
814
777
  return _InjectableRegistry.get(ctor);
815
778
  }
816
779
  //#endregion
780
+ //#region src/annotation/validator.ts
781
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
782
+ function ResponseBody(schema) {
783
+ return (target, propertyKey) => {
784
+ const ctor = target.constructor;
785
+ const fnName = String(propertyKey);
786
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
787
+ };
788
+ }
789
+ function RequestBody(schema) {
790
+ return (target, propertyKey) => {
791
+ const ctor = target.constructor;
792
+ const fnName = String(propertyKey);
793
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
794
+ };
795
+ }
796
+ function RequestParam(schema) {
797
+ return (target, propertyKey) => {
798
+ const ctor = target.constructor;
799
+ const fnName = String(propertyKey);
800
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
801
+ };
802
+ }
803
+ function RequestQuery(schema) {
804
+ return (target, propertyKey) => {
805
+ const ctor = target.constructor;
806
+ const fnName = String(propertyKey);
807
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
808
+ };
809
+ }
810
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
811
+ const byFn = _getOrCreateMap(_validatorSchemaStore, ctor);
812
+ const existing = byFn.get(fnName);
813
+ if (existing) return existing;
814
+ const created = {};
815
+ byFn.set(fnName, created);
816
+ return created;
817
+ }
818
+ async function _parseOrThrow(schema, input, kind) {
819
+ const result = await schema["~standard"].validate(input);
820
+ if (result.issues) {
821
+ console.debug(`Failed to parse ${schema["~standard"].vendor} schema\nissues: ${result.issues}`);
822
+ throw new ParserError(kind, result.issues, schema["~standard"].vendor);
823
+ }
824
+ return result.value;
825
+ }
826
+ function _getValidatorSchema(ctor, fnName) {
827
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
828
+ }
829
+ function _setOnce(def, key, schema, fnName) {
830
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
831
+ def[key] = schema;
832
+ }
833
+ //#endregion
817
834
  //#region src/annotation/controller.ts
818
835
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
819
836
  /**
@@ -868,12 +885,12 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
868
885
  return;
869
886
  }
870
887
  router[methodName](fp, async (request, response, next) => {
871
- const schemas = _getRequestSchemas(target, fnName);
888
+ const schemas = _getValidatorSchema(target, fnName);
872
889
  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");
890
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody");
891
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams");
892
+ if (schemas.requestQuery) {
893
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery");
877
894
  Object.defineProperty(request, "query", {
878
895
  value: parsedQuery,
879
896
  writable: true,
@@ -883,7 +900,8 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
883
900
  }
884
901
  const result = await fn.bind(controllerInstance)(request, response, next);
885
902
  if (result instanceof ResponseEntity) {
886
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
903
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody") : result.getBody();
904
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
887
905
  return;
888
906
  }
889
907
  if (result instanceof RedirectView) {
@@ -950,7 +968,7 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
950
968
  //#region src/middleware/default/openapi/index.ts
951
969
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
952
970
  handle(_request, _response, _next) {
953
- return ResponseEntity.ok().body(_generateOpenApiSpec());
971
+ return ResponseEntity.ok().body(generateOpenApiSpec());
954
972
  }
955
973
  };
956
974
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
@@ -962,20 +980,7 @@ let Serve = class Serve {
962
980
  this.handlers = swagger_ui_express.default.serve;
963
981
  }
964
982
  handle(request, response, next) {
965
- this.runNext(0, request, response, next);
966
- }
967
- runNext(i, req, res, next) {
968
- if (i >= this.handlers.length) {
969
- next();
970
- return;
971
- }
972
- this.handlers[i]?.(req, res, (err) => {
973
- if (err) {
974
- next(err);
975
- return;
976
- }
977
- this.runNext(i + 1, req, res, next);
978
- });
983
+ return Sapling.chainHandlers(this.handlers, request, response, next);
979
984
  }
980
985
  };
981
986
  __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
@@ -996,6 +1001,7 @@ const DefaultSwaggerMiddleware = {
996
1001
  };
997
1002
  //#endregion
998
1003
  exports.Controller = Controller;
1004
+ exports.ControllerSchema = ControllerSchema;
999
1005
  exports.DELETE = DELETE;
1000
1006
  Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
1001
1007
  enumerable: true,
@@ -1038,21 +1044,29 @@ exports.RedirectView = RedirectView;
1038
1044
  exports.RequestBody = RequestBody;
1039
1045
  exports.RequestParam = RequestParam;
1040
1046
  exports.RequestQuery = RequestQuery;
1047
+ exports.ResponseBody = ResponseBody;
1041
1048
  exports.ResponseEntity = ResponseEntity;
1042
1049
  exports.ResponseEntityBuilder = ResponseEntityBuilder;
1043
1050
  exports.ResponseStatusError = ResponseStatusError;
1051
+ exports.RouteSchema = RouteSchema;
1044
1052
  exports.Sapling = Sapling;
1045
1053
  exports._ControllerRegistry = _ControllerRegistry;
1046
1054
  exports._InjectableDeps = _InjectableDeps;
1047
1055
  exports._InjectableRegistry = _InjectableRegistry;
1048
1056
  exports._Route = _Route;
1049
- exports._generateOpenApiSpec = _generateOpenApiSpec;
1050
- exports._getRequestSchemas = _getRequestSchemas;
1057
+ exports._getControllerSchema = _getControllerSchema;
1058
+ exports._getOrCreateSchemaDefinition = _getOrCreateSchemaDefinition;
1059
+ exports._getRouteSchema = _getRouteSchema;
1051
1060
  exports._getRoutes = _getRoutes;
1061
+ exports._getValidatorSchema = _getValidatorSchema;
1052
1062
  exports._parseOrThrow = _parseOrThrow;
1053
1063
  exports._registerControllerClass = _registerControllerClass;
1054
1064
  exports._resolve = _resolve;
1055
- exports._setOpenApiConfig = _setOpenApiConfig;
1065
+ exports._setControllerSchema = _setControllerSchema;
1066
+ exports._setOnce = _setOnce;
1067
+ exports._setRouteSchema = _setRouteSchema;
1056
1068
  exports._settings = _settings;
1069
+ exports.generateOpenApiSpec = generateOpenApiSpec;
1057
1070
  exports.methodResolve = methodResolve;
1058
1071
  exports.openApiGenerator = openApiGenerator;
1072
+ exports.setOpenApiConfig = setOpenApiConfig;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import e, { ErrorRequestHandler, NextFunction, Request, Response as Response$1, Router } from "express";
1
+ import e, { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response as Response$1, Router } from "express";
2
2
 
3
3
  //#region src/html/404.d.ts
4
4
  /**
@@ -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,53 @@ 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
+ description?: string;
896
+ schema?: StandardSchemaV1 & StandardJSONSchemaV1;
897
+ };
898
+ type RouteSchemaDefinition = {
899
+ summary?: string;
900
+ description?: string;
901
+ responses?: ResponseSchema[];
902
+ };
903
+ type ControllerSchemaDefinition = {
904
+ title?: string;
905
+ description?: string;
906
+ };
907
+ declare function ControllerSchema(options: {
908
+ title?: string;
909
+ description?: string;
910
+ }): ClassDecorator;
911
+ declare function RouteSchema(options: {
912
+ summary?: string;
913
+ description?: string;
914
+ responses?: ResponseSchema[];
915
+ }): MethodDecorator;
916
+ declare function _setRouteSchema(ctor: Function, fnName: string, options: RouteSchemaDefinition): void;
917
+ declare function _setControllerSchema(ctor: Function, options: ControllerSchemaDefinition): void;
918
+ declare function _getRouteSchema(ctor: Function, fnName: string): RouteSchemaDefinition | undefined;
919
+ declare function _getControllerSchema(ctor: Function): ControllerSchemaDefinition | undefined;
960
920
  //#endregion
961
921
  //#region src/middleware/default/error/base.d.ts
962
922
  /**
@@ -993,7 +953,6 @@ declare class DefaultOpenApiMiddleware {
993
953
  declare class Serve {
994
954
  private readonly handlers;
995
955
  handle(request: Request, response: Response$1, next: NextFunction): void;
996
- private runNext;
997
956
  }
998
957
  declare class Setup {
999
958
  private readonly handler;
@@ -1005,4 +964,4 @@ declare const DefaultSwaggerMiddleware: {
1005
964
  Setup: typeof Setup;
1006
965
  };
1007
966
  //#endregion
1008
- 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 };
967
+ 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
@@ -1,4 +1,4 @@
1
- import e, { ErrorRequestHandler, NextFunction, Request, Response as Response$1, Router } from "express";
1
+ import e, { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response as Response$1, Router } from "express";
2
2
 
3
3
  //#region src/html/404.d.ts
4
4
  /**
@@ -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,53 @@ 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
+ description?: string;
896
+ schema?: StandardSchemaV1 & StandardJSONSchemaV1;
897
+ };
898
+ type RouteSchemaDefinition = {
899
+ summary?: string;
900
+ description?: string;
901
+ responses?: ResponseSchema[];
902
+ };
903
+ type ControllerSchemaDefinition = {
904
+ title?: string;
905
+ description?: string;
906
+ };
907
+ declare function ControllerSchema(options: {
908
+ title?: string;
909
+ description?: string;
910
+ }): ClassDecorator;
911
+ declare function RouteSchema(options: {
912
+ summary?: string;
913
+ description?: string;
914
+ responses?: ResponseSchema[];
915
+ }): MethodDecorator;
916
+ declare function _setRouteSchema(ctor: Function, fnName: string, options: RouteSchemaDefinition): void;
917
+ declare function _setControllerSchema(ctor: Function, options: ControllerSchemaDefinition): void;
918
+ declare function _getRouteSchema(ctor: Function, fnName: string): RouteSchemaDefinition | undefined;
919
+ declare function _getControllerSchema(ctor: Function): ControllerSchemaDefinition | undefined;
960
920
  //#endregion
961
921
  //#region src/middleware/default/error/base.d.ts
962
922
  /**
@@ -993,7 +953,6 @@ declare class DefaultOpenApiMiddleware {
993
953
  declare class Serve {
994
954
  private readonly handlers;
995
955
  handle(request: Request, response: Response$1, next: NextFunction): void;
996
- private runNext;
997
956
  }
998
957
  declare class Setup {
999
958
  private readonly handler;
@@ -1005,4 +964,4 @@ declare const DefaultSwaggerMiddleware: {
1005
964
  Setup: typeof Setup;
1006
965
  };
1007
966
  //#endregion
1008
- 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 };
967
+ 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,79 @@ 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, "output");
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 = resp.schema ? this.toJsonSchema(resp.schema, "output") : void 0;
538
+ responses[String(resp.statusCode)] = {
539
+ description: resp.description ?? responseSchema?.description ?? `Response ${resp.statusCode}`,
540
+ ...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
541
+ };
542
+ }
543
+ const operation = {
544
+ responses,
545
+ summary: routeSchema?.summary,
546
+ description: routeSchema?.description,
547
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
548
+ };
599
549
  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
- });
550
+ if (schemas?.requestParam) {
551
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
552
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
553
+ const parameterSchema = schema;
554
+ parameters.push({
555
+ name,
556
+ in: "path",
557
+ required: true,
558
+ description: parameterSchema.description,
559
+ schema: parameterSchema
560
+ });
561
+ }
608
562
  }
609
- if (schemas?.query) {
610
- const querySchema = this.toJsonSchema(schemas.query);
563
+ if (schemas?.requestQuery) {
564
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
611
565
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
612
566
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
567
+ const parameterSchema = schema;
613
568
  parameters.push({
614
569
  name,
615
570
  in: "query",
616
571
  required: isRequired,
617
- schema
572
+ description: parameterSchema.description,
573
+ schema: parameterSchema
618
574
  });
619
575
  }
620
576
  }
621
577
  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
- };
578
+ if (schemas?.requestBody) {
579
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
580
+ operation.requestBody = {
581
+ required: true,
582
+ description: requestSchema.description,
583
+ content: { "application/json": { schema: requestSchema } }
584
+ };
585
+ }
626
586
  const method = route.method.toLowerCase();
627
587
  paths[openApiPath][method] = operation;
628
588
  }
@@ -634,12 +594,15 @@ var OpenAPIGenerator = class {
634
594
  version: config.version,
635
595
  description: config.description
636
596
  },
597
+ tags: tags.length > 0 ? tags : void 0,
637
598
  paths
638
599
  };
639
600
  }
640
- toJsonSchema(schema) {
601
+ toJsonSchema(schema, direction = "output") {
641
602
  try {
642
- return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
603
+ const jsonSchema = schema["~standard"].jsonSchema;
604
+ if (direction === "input" && jsonSchema.input) return jsonSchema.input({ target: "openapi-3.0" });
605
+ return jsonSchema.output({ target: "openapi-3.0" });
643
606
  } catch (e) {
644
607
  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
608
  throw e;
@@ -650,10 +613,10 @@ const openApiGenerator = new OpenAPIGenerator();
650
613
  function _registerControllerClass(controllerClass, prefix) {
651
614
  openApiGenerator.registerController(controllerClass, prefix);
652
615
  }
653
- function _setOpenApiConfig(config) {
616
+ function setOpenApiConfig(config) {
654
617
  openApiGenerator.setConfig(config);
655
618
  }
656
- function _generateOpenApiSpec() {
619
+ function generateOpenApiSpec() {
657
620
  return openApiGenerator.generateSpec();
658
621
  }
659
622
  //#endregion
@@ -789,6 +752,60 @@ function _resolve(ctor) {
789
752
  return _InjectableRegistry.get(ctor);
790
753
  }
791
754
  //#endregion
755
+ //#region src/annotation/validator.ts
756
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
757
+ function ResponseBody(schema) {
758
+ return (target, propertyKey) => {
759
+ const ctor = target.constructor;
760
+ const fnName = String(propertyKey);
761
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
762
+ };
763
+ }
764
+ function RequestBody(schema) {
765
+ return (target, propertyKey) => {
766
+ const ctor = target.constructor;
767
+ const fnName = String(propertyKey);
768
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
769
+ };
770
+ }
771
+ function RequestParam(schema) {
772
+ return (target, propertyKey) => {
773
+ const ctor = target.constructor;
774
+ const fnName = String(propertyKey);
775
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
776
+ };
777
+ }
778
+ function RequestQuery(schema) {
779
+ return (target, propertyKey) => {
780
+ const ctor = target.constructor;
781
+ const fnName = String(propertyKey);
782
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
783
+ };
784
+ }
785
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
786
+ const byFn = _getOrCreateMap(_validatorSchemaStore, ctor);
787
+ const existing = byFn.get(fnName);
788
+ if (existing) return existing;
789
+ const created = {};
790
+ byFn.set(fnName, created);
791
+ return created;
792
+ }
793
+ async function _parseOrThrow(schema, input, kind) {
794
+ const result = await schema["~standard"].validate(input);
795
+ if (result.issues) {
796
+ console.debug(`Failed to parse ${schema["~standard"].vendor} schema\nissues: ${result.issues}`);
797
+ throw new ParserError(kind, result.issues, schema["~standard"].vendor);
798
+ }
799
+ return result.value;
800
+ }
801
+ function _getValidatorSchema(ctor, fnName) {
802
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
803
+ }
804
+ function _setOnce(def, key, schema, fnName) {
805
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
806
+ def[key] = schema;
807
+ }
808
+ //#endregion
792
809
  //#region src/annotation/controller.ts
793
810
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
794
811
  /**
@@ -843,12 +860,12 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
843
860
  return;
844
861
  }
845
862
  router[methodName](fp, async (request, response, next) => {
846
- const schemas = _getRequestSchemas(target, fnName);
863
+ const schemas = _getValidatorSchema(target, fnName);
847
864
  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");
865
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody");
866
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams");
867
+ if (schemas.requestQuery) {
868
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery");
852
869
  Object.defineProperty(request, "query", {
853
870
  value: parsedQuery,
854
871
  writable: true,
@@ -858,7 +875,8 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
858
875
  }
859
876
  const result = await fn.bind(controllerInstance)(request, response, next);
860
877
  if (result instanceof ResponseEntity) {
861
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
878
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody") : result.getBody();
879
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
862
880
  return;
863
881
  }
864
882
  if (result instanceof RedirectView) {
@@ -925,7 +943,7 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
925
943
  //#region src/middleware/default/openapi/index.ts
926
944
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
927
945
  handle(_request, _response, _next) {
928
- return ResponseEntity.ok().body(_generateOpenApiSpec());
946
+ return ResponseEntity.ok().body(generateOpenApiSpec());
929
947
  }
930
948
  };
931
949
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
@@ -937,20 +955,7 @@ let Serve = class Serve {
937
955
  this.handlers = swagger.serve;
938
956
  }
939
957
  handle(request, response, next) {
940
- this.runNext(0, request, response, next);
941
- }
942
- runNext(i, req, res, next) {
943
- if (i >= this.handlers.length) {
944
- next();
945
- return;
946
- }
947
- this.handlers[i]?.(req, res, (err) => {
948
- if (err) {
949
- next(err);
950
- return;
951
- }
952
- this.runNext(i + 1, req, res, next);
953
- });
958
+ return Sapling.chainHandlers(this.handlers, request, response, next);
954
959
  }
955
960
  };
956
961
  __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
@@ -970,4 +975,4 @@ const DefaultSwaggerMiddleware = {
970
975
  Setup
971
976
  };
972
977
  //#endregion
973
- 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 };
978
+ 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.662f05c0",
3
+ "version": "2.0.5-beta.75e5e346",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A library to help you write cleaner Express.js code",
6
6
  "repository": {