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

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,25 @@ 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 _getRouteSchema(ctor, fnName) {
494
+ return _routeSchemaStore.get(ctor)?.get(fnName);
495
+ }
496
+ function _getControllerSchema(ctor) {
497
+ return _controllerSchemaStore.get(ctor);
498
+ }
499
+ //#endregion
594
500
  //#region src/helper/openapi.ts
595
501
  var OpenAPIGenerator = class {
596
502
  constructor() {
@@ -612,18 +518,39 @@ var OpenAPIGenerator = class {
612
518
  generateSpec() {
613
519
  const config = this.config;
614
520
  const paths = {};
521
+ const tags = [];
615
522
  for (const { class: controllerClass, prefix } of this.controllers) {
616
523
  const routes = _getRoutes(controllerClass);
524
+ const controllerSchema = _getControllerSchema(controllerClass);
525
+ if (controllerSchema?.title) tags.push({
526
+ name: controllerSchema.title,
527
+ description: controllerSchema.description
528
+ });
617
529
  for (const route of routes) {
618
530
  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;
531
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
532
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
533
+ 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.`);
534
+ const openApiPath = prefix + route.path;
622
535
  if (!paths[openApiPath]) paths[openApiPath] = {};
623
- const operation = { responses: { "200": { description: "Successful response" } } };
536
+ const responses = {};
537
+ if (schemas?.responseBody) responses["200"] = {
538
+ description: "Successful response",
539
+ content: { "application/json": { schema: this.toJsonSchema(schemas.responseBody) } }
540
+ };
541
+ else responses["200"] = { description: "Successful response" };
542
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) responses[String(resp.statusCode)] = {
543
+ description: `Response ${resp.statusCode}`,
544
+ content: { "application/json": { schema: this.toJsonSchema(resp.schema) } }
545
+ };
546
+ const operation = {
547
+ responses,
548
+ description: routeSchema?.description,
549
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
550
+ };
624
551
  const parameters = [];
625
- if (schemas?.param) {
626
- const paramSchema = this.toJsonSchema(schemas.param);
552
+ if (schemas?.requestParam) {
553
+ const paramSchema = this.toJsonSchema(schemas.requestParam);
627
554
  if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
628
555
  name,
629
556
  in: "path",
@@ -631,8 +558,8 @@ var OpenAPIGenerator = class {
631
558
  schema
632
559
  });
633
560
  }
634
- if (schemas?.query) {
635
- const querySchema = this.toJsonSchema(schemas.query);
561
+ if (schemas?.requestQuery) {
562
+ const querySchema = this.toJsonSchema(schemas.requestQuery);
636
563
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
637
564
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
638
565
  parameters.push({
@@ -644,9 +571,9 @@ var OpenAPIGenerator = class {
644
571
  }
645
572
  }
646
573
  if (parameters.length > 0) operation.parameters = parameters;
647
- if (schemas?.body) operation.requestBody = {
574
+ if (schemas?.requestBody) operation.requestBody = {
648
575
  required: true,
649
- content: { "application/json": { schema: this.toJsonSchema(schemas.body) } }
576
+ content: { "application/json": { schema: this.toJsonSchema(schemas.requestBody) } }
650
577
  };
651
578
  const method = route.method.toLowerCase();
652
579
  paths[openApiPath][method] = operation;
@@ -659,6 +586,7 @@ var OpenAPIGenerator = class {
659
586
  version: config.version,
660
587
  description: config.description
661
588
  },
589
+ tags: tags.length > 0 ? tags : void 0,
662
590
  paths
663
591
  };
664
592
  }
@@ -675,10 +603,10 @@ const openApiGenerator = new OpenAPIGenerator();
675
603
  function _registerControllerClass(controllerClass, prefix) {
676
604
  openApiGenerator.registerController(controllerClass, prefix);
677
605
  }
678
- function _setOpenApiConfig(config) {
606
+ function setOpenApiConfig(config) {
679
607
  openApiGenerator.setConfig(config);
680
608
  }
681
- function _generateOpenApiSpec() {
609
+ function generateOpenApiSpec() {
682
610
  return openApiGenerator.generateSpec();
683
611
  }
684
612
  //#endregion
@@ -814,6 +742,60 @@ function _resolve(ctor) {
814
742
  return _InjectableRegistry.get(ctor);
815
743
  }
816
744
  //#endregion
745
+ //#region src/annotation/validator.ts
746
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
747
+ function ResponseBody(schema) {
748
+ return (target, propertyKey) => {
749
+ const ctor = target.constructor;
750
+ const fnName = String(propertyKey);
751
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
752
+ };
753
+ }
754
+ function RequestBody(schema) {
755
+ return (target, propertyKey) => {
756
+ const ctor = target.constructor;
757
+ const fnName = String(propertyKey);
758
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
759
+ };
760
+ }
761
+ function RequestParam(schema) {
762
+ return (target, propertyKey) => {
763
+ const ctor = target.constructor;
764
+ const fnName = String(propertyKey);
765
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
766
+ };
767
+ }
768
+ function RequestQuery(schema) {
769
+ return (target, propertyKey) => {
770
+ const ctor = target.constructor;
771
+ const fnName = String(propertyKey);
772
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
773
+ };
774
+ }
775
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
776
+ const byFn = _getOrCreateMap(_validatorSchemaStore, ctor);
777
+ const existing = byFn.get(fnName);
778
+ if (existing) return existing;
779
+ const created = {};
780
+ byFn.set(fnName, created);
781
+ return created;
782
+ }
783
+ async function _parseOrThrow(schema, input, kind) {
784
+ const result = await schema["~standard"].validate(input);
785
+ if (result.issues) {
786
+ console.debug(`Failed to parse a schema`);
787
+ throw new ParserError(kind, result.issues, schema["~standard"].vendor);
788
+ }
789
+ return result.value;
790
+ }
791
+ function _getValidatorSchema(ctor, fnName) {
792
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
793
+ }
794
+ function _setOnce(def, key, schema, fnName) {
795
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
796
+ def[key] = schema;
797
+ }
798
+ //#endregion
817
799
  //#region src/annotation/controller.ts
818
800
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
819
801
  /**
@@ -868,12 +850,12 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
868
850
  return;
869
851
  }
870
852
  router[methodName](fp, async (request, response, next) => {
871
- const schemas = _getRequestSchemas(target, fnName);
853
+ const schemas = _getValidatorSchema(target, fnName);
872
854
  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");
855
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody");
856
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams");
857
+ if (schemas.requestQuery) {
858
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery");
877
859
  Object.defineProperty(request, "query", {
878
860
  value: parsedQuery,
879
861
  writable: true,
@@ -883,7 +865,8 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
883
865
  }
884
866
  const result = await fn.bind(controllerInstance)(request, response, next);
885
867
  if (result instanceof ResponseEntity) {
886
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
868
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody") : result.getBody();
869
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
887
870
  return;
888
871
  }
889
872
  if (result instanceof RedirectView) {
@@ -950,7 +933,7 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
950
933
  //#region src/middleware/default/openapi/index.ts
951
934
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
952
935
  handle(_request, _response, _next) {
953
- return ResponseEntity.ok().body(_generateOpenApiSpec());
936
+ return ResponseEntity.ok().body(generateOpenApiSpec());
954
937
  }
955
938
  };
956
939
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
@@ -961,21 +944,21 @@ let Serve = class Serve {
961
944
  constructor() {
962
945
  this.handlers = swagger_ui_express.default.serve;
963
946
  }
964
- handle(_request, _response, _next) {
965
- return this.handlers;
947
+ handle(request, response, next) {
948
+ return Sapling.chainHandlers(this.handlers, request, response, next);
966
949
  }
967
950
  };
968
- __decorate([Middleware()], Serve.prototype, "handle", null);
951
+ __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
969
952
  Serve = __decorate([MiddlewareClass()], Serve);
970
953
  let Setup = class Setup {
971
954
  constructor() {
972
- this.handler = swagger_ui_express.default.setup(void 0, { swaggerOptions: { url: _settings.doc.openApiPath } });
955
+ this.handler = swagger_ui_express.default.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
973
956
  }
974
957
  handle(request, response, next) {
975
958
  return this.handler(request, response, next);
976
959
  }
977
960
  };
978
- __decorate([Middleware()], Setup.prototype, "handle", null);
961
+ __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
979
962
  Setup = __decorate([MiddlewareClass()], Setup);
980
963
  const DefaultSwaggerMiddleware = {
981
964
  Serve,
@@ -1025,6 +1008,7 @@ exports.RedirectView = RedirectView;
1025
1008
  exports.RequestBody = RequestBody;
1026
1009
  exports.RequestParam = RequestParam;
1027
1010
  exports.RequestQuery = RequestQuery;
1011
+ exports.ResponseBody = ResponseBody;
1028
1012
  exports.ResponseEntity = ResponseEntity;
1029
1013
  exports.ResponseEntityBuilder = ResponseEntityBuilder;
1030
1014
  exports.ResponseStatusError = ResponseStatusError;
@@ -1033,13 +1017,15 @@ exports._ControllerRegistry = _ControllerRegistry;
1033
1017
  exports._InjectableDeps = _InjectableDeps;
1034
1018
  exports._InjectableRegistry = _InjectableRegistry;
1035
1019
  exports._Route = _Route;
1036
- exports._generateOpenApiSpec = _generateOpenApiSpec;
1037
- exports._getRequestSchemas = _getRequestSchemas;
1020
+ exports._getOrCreateSchemaDefinition = _getOrCreateSchemaDefinition;
1038
1021
  exports._getRoutes = _getRoutes;
1022
+ exports._getValidatorSchema = _getValidatorSchema;
1039
1023
  exports._parseOrThrow = _parseOrThrow;
1040
1024
  exports._registerControllerClass = _registerControllerClass;
1041
1025
  exports._resolve = _resolve;
1042
- exports._setOpenApiConfig = _setOpenApiConfig;
1026
+ exports._setOnce = _setOnce;
1043
1027
  exports._settings = _settings;
1028
+ exports.generateOpenApiSpec = generateOpenApiSpec;
1044
1029
  exports.methodResolve = methodResolve;
1045
1030
  exports.openApiGenerator = openApiGenerator;
1031
+ 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,24 @@ 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;
960
891
  //#endregion
961
892
  //#region src/middleware/default/error/base.d.ts
962
893
  /**
@@ -992,7 +923,7 @@ declare class DefaultOpenApiMiddleware {
992
923
  //#region src/middleware/default/swagger/index.d.ts
993
924
  declare class Serve {
994
925
  private readonly handlers;
995
- handle(_request: Request, _response: Response$1, _next: NextFunction): RequestHandler[];
926
+ handle(request: Request, response: Response$1, next: NextFunction): void;
996
927
  }
997
928
  declare class Setup {
998
929
  private readonly handler;
@@ -1004,4 +935,4 @@ declare const DefaultSwaggerMiddleware: {
1004
935
  Setup: typeof Setup;
1005
936
  };
1006
937
  //#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 };
938
+ 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, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, ValidatorSchema, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getOrCreateSchemaDefinition, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerControllerClass, _resolve, _setOnce, _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,24 @@ 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;
960
891
  //#endregion
961
892
  //#region src/middleware/default/error/base.d.ts
962
893
  /**
@@ -992,7 +923,7 @@ declare class DefaultOpenApiMiddleware {
992
923
  //#region src/middleware/default/swagger/index.d.ts
993
924
  declare class Serve {
994
925
  private readonly handlers;
995
- handle(_request: Request, _response: Response$1, _next: NextFunction): RequestHandler[];
926
+ handle(request: Request, response: Response$1, next: NextFunction): void;
996
927
  }
997
928
  declare class Setup {
998
929
  private readonly handler;
@@ -1004,4 +935,4 @@ declare const DefaultSwaggerMiddleware: {
1004
935
  Setup: typeof Setup;
1005
936
  };
1006
937
  //#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 };
938
+ 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, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, ValidatorSchema, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getOrCreateSchemaDefinition, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerControllerClass, _resolve, _setOnce, _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,25 @@ 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 _getRouteSchema(ctor, fnName) {
469
+ return _routeSchemaStore.get(ctor)?.get(fnName);
470
+ }
471
+ function _getControllerSchema(ctor) {
472
+ return _controllerSchemaStore.get(ctor);
473
+ }
474
+ //#endregion
569
475
  //#region src/helper/openapi.ts
570
476
  var OpenAPIGenerator = class {
571
477
  constructor() {
@@ -587,18 +493,39 @@ var OpenAPIGenerator = class {
587
493
  generateSpec() {
588
494
  const config = this.config;
589
495
  const paths = {};
496
+ const tags = [];
590
497
  for (const { class: controllerClass, prefix } of this.controllers) {
591
498
  const routes = _getRoutes(controllerClass);
499
+ const controllerSchema = _getControllerSchema(controllerClass);
500
+ if (controllerSchema?.title) tags.push({
501
+ name: controllerSchema.title,
502
+ description: controllerSchema.description
503
+ });
592
504
  for (const route of routes) {
593
505
  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;
506
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
507
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
508
+ 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.`);
509
+ const openApiPath = prefix + route.path;
597
510
  if (!paths[openApiPath]) paths[openApiPath] = {};
598
- const operation = { responses: { "200": { description: "Successful response" } } };
511
+ const responses = {};
512
+ if (schemas?.responseBody) responses["200"] = {
513
+ description: "Successful response",
514
+ content: { "application/json": { schema: this.toJsonSchema(schemas.responseBody) } }
515
+ };
516
+ else responses["200"] = { description: "Successful response" };
517
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) responses[String(resp.statusCode)] = {
518
+ description: `Response ${resp.statusCode}`,
519
+ content: { "application/json": { schema: this.toJsonSchema(resp.schema) } }
520
+ };
521
+ const operation = {
522
+ responses,
523
+ description: routeSchema?.description,
524
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
525
+ };
599
526
  const parameters = [];
600
- if (schemas?.param) {
601
- const paramSchema = this.toJsonSchema(schemas.param);
527
+ if (schemas?.requestParam) {
528
+ const paramSchema = this.toJsonSchema(schemas.requestParam);
602
529
  if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
603
530
  name,
604
531
  in: "path",
@@ -606,8 +533,8 @@ var OpenAPIGenerator = class {
606
533
  schema
607
534
  });
608
535
  }
609
- if (schemas?.query) {
610
- const querySchema = this.toJsonSchema(schemas.query);
536
+ if (schemas?.requestQuery) {
537
+ const querySchema = this.toJsonSchema(schemas.requestQuery);
611
538
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
612
539
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
613
540
  parameters.push({
@@ -619,9 +546,9 @@ var OpenAPIGenerator = class {
619
546
  }
620
547
  }
621
548
  if (parameters.length > 0) operation.parameters = parameters;
622
- if (schemas?.body) operation.requestBody = {
549
+ if (schemas?.requestBody) operation.requestBody = {
623
550
  required: true,
624
- content: { "application/json": { schema: this.toJsonSchema(schemas.body) } }
551
+ content: { "application/json": { schema: this.toJsonSchema(schemas.requestBody) } }
625
552
  };
626
553
  const method = route.method.toLowerCase();
627
554
  paths[openApiPath][method] = operation;
@@ -634,6 +561,7 @@ var OpenAPIGenerator = class {
634
561
  version: config.version,
635
562
  description: config.description
636
563
  },
564
+ tags: tags.length > 0 ? tags : void 0,
637
565
  paths
638
566
  };
639
567
  }
@@ -650,10 +578,10 @@ const openApiGenerator = new OpenAPIGenerator();
650
578
  function _registerControllerClass(controllerClass, prefix) {
651
579
  openApiGenerator.registerController(controllerClass, prefix);
652
580
  }
653
- function _setOpenApiConfig(config) {
581
+ function setOpenApiConfig(config) {
654
582
  openApiGenerator.setConfig(config);
655
583
  }
656
- function _generateOpenApiSpec() {
584
+ function generateOpenApiSpec() {
657
585
  return openApiGenerator.generateSpec();
658
586
  }
659
587
  //#endregion
@@ -789,6 +717,60 @@ function _resolve(ctor) {
789
717
  return _InjectableRegistry.get(ctor);
790
718
  }
791
719
  //#endregion
720
+ //#region src/annotation/validator.ts
721
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
722
+ function ResponseBody(schema) {
723
+ return (target, propertyKey) => {
724
+ const ctor = target.constructor;
725
+ const fnName = String(propertyKey);
726
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
727
+ };
728
+ }
729
+ function RequestBody(schema) {
730
+ return (target, propertyKey) => {
731
+ const ctor = target.constructor;
732
+ const fnName = String(propertyKey);
733
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
734
+ };
735
+ }
736
+ function RequestParam(schema) {
737
+ return (target, propertyKey) => {
738
+ const ctor = target.constructor;
739
+ const fnName = String(propertyKey);
740
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
741
+ };
742
+ }
743
+ function RequestQuery(schema) {
744
+ return (target, propertyKey) => {
745
+ const ctor = target.constructor;
746
+ const fnName = String(propertyKey);
747
+ _setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
748
+ };
749
+ }
750
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
751
+ const byFn = _getOrCreateMap(_validatorSchemaStore, ctor);
752
+ const existing = byFn.get(fnName);
753
+ if (existing) return existing;
754
+ const created = {};
755
+ byFn.set(fnName, created);
756
+ return created;
757
+ }
758
+ async function _parseOrThrow(schema, input, kind) {
759
+ const result = await schema["~standard"].validate(input);
760
+ if (result.issues) {
761
+ console.debug(`Failed to parse a schema`);
762
+ throw new ParserError(kind, result.issues, schema["~standard"].vendor);
763
+ }
764
+ return result.value;
765
+ }
766
+ function _getValidatorSchema(ctor, fnName) {
767
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
768
+ }
769
+ function _setOnce(def, key, schema, fnName) {
770
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
771
+ def[key] = schema;
772
+ }
773
+ //#endregion
792
774
  //#region src/annotation/controller.ts
793
775
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
794
776
  /**
@@ -843,12 +825,12 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
843
825
  return;
844
826
  }
845
827
  router[methodName](fp, async (request, response, next) => {
846
- const schemas = _getRequestSchemas(target, fnName);
828
+ const schemas = _getValidatorSchema(target, fnName);
847
829
  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");
830
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody");
831
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams");
832
+ if (schemas.requestQuery) {
833
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery");
852
834
  Object.defineProperty(request, "query", {
853
835
  value: parsedQuery,
854
836
  writable: true,
@@ -858,7 +840,8 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
858
840
  }
859
841
  const result = await fn.bind(controllerInstance)(request, response, next);
860
842
  if (result instanceof ResponseEntity) {
861
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
843
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody") : result.getBody();
844
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
862
845
  return;
863
846
  }
864
847
  if (result instanceof RedirectView) {
@@ -925,7 +908,7 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
925
908
  //#region src/middleware/default/openapi/index.ts
926
909
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
927
910
  handle(_request, _response, _next) {
928
- return ResponseEntity.ok().body(_generateOpenApiSpec());
911
+ return ResponseEntity.ok().body(generateOpenApiSpec());
929
912
  }
930
913
  };
931
914
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
@@ -936,25 +919,25 @@ let Serve = class Serve {
936
919
  constructor() {
937
920
  this.handlers = swagger.serve;
938
921
  }
939
- handle(_request, _response, _next) {
940
- return this.handlers;
922
+ handle(request, response, next) {
923
+ return Sapling.chainHandlers(this.handlers, request, response, next);
941
924
  }
942
925
  };
943
- __decorate([Middleware()], Serve.prototype, "handle", null);
926
+ __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
944
927
  Serve = __decorate([MiddlewareClass()], Serve);
945
928
  let Setup = class Setup {
946
929
  constructor() {
947
- this.handler = swagger.setup(void 0, { swaggerOptions: { url: _settings.doc.openApiPath } });
930
+ this.handler = swagger.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
948
931
  }
949
932
  handle(request, response, next) {
950
933
  return this.handler(request, response, next);
951
934
  }
952
935
  };
953
- __decorate([Middleware()], Setup.prototype, "handle", null);
936
+ __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
954
937
  Setup = __decorate([MiddlewareClass()], Setup);
955
938
  const DefaultSwaggerMiddleware = {
956
939
  Serve,
957
940
  Setup
958
941
  };
959
942
  //#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 };
943
+ export { Controller, 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, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getOrCreateSchemaDefinition, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerControllerClass, _resolve, _setOnce, _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.c70dc62b",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A library to help you write cleaner Express.js code",
6
6
  "repository": {