@tahminator/sapling 2.0.5-beta.23c37926 → 2.0.5-beta.279125eb

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
@@ -48,6 +48,7 @@ const Html404ErrorPage = (error) => `<!DOCTYPE html>
48
48
  * You can either return `new RedirectView(url)` or `RedirectView.redirect(url)` inside of a controller method.
49
49
  */
50
50
  var RedirectView = class RedirectView {
51
+ _url;
51
52
  constructor(url) {
52
53
  this._url = url;
53
54
  }
@@ -143,8 +144,10 @@ let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
143
144
  * @typeParam T - the type of the response body
144
145
  */
145
146
  var ResponseEntity = class {
147
+ _statusCode;
148
+ _headers = {};
149
+ _body;
146
150
  constructor(body, headers = {}, statusCode = 200) {
147
- this._headers = {};
148
151
  this._body = body;
149
152
  this._headers = headers;
150
153
  this._statusCode = statusCode;
@@ -197,8 +200,9 @@ var ResponseEntity = class {
197
200
  * ensuring type safety when constructing the response.
198
201
  */
199
202
  var ResponseEntityBuilder = class {
203
+ _statusCode;
204
+ _headers = {};
200
205
  constructor(statusCode) {
201
- this._headers = {};
202
206
  this._statusCode = statusCode;
203
207
  }
204
208
  /**
@@ -230,6 +234,7 @@ var ResponseEntityBuilder = class {
230
234
  * @see {@link Sapling.loadResponseStatusErrorMiddleware}
231
235
  */
232
236
  var ResponseStatusError = class ResponseStatusError extends Error {
237
+ status;
233
238
  constructor(status, message) {
234
239
  super(message ?? "Something went wrong.");
235
240
  this.status = status;
@@ -241,25 +246,27 @@ var ResponseStatusError = class ResponseStatusError extends Error {
241
246
  //#endregion
242
247
  //#region src/helper/error/parse.ts
243
248
  /**
244
- * This error should be thrown when some data cannot be parsed by a given schema.
249
+ * This error should be thrown when some data cannot be parsed by a given Standard Schema compatible schema.
245
250
  */
246
251
  var ParserError = class ParserError extends ResponseStatusError {
247
- constructor(location, issues, vendor) {
248
- super(400, ParserError.formatMessage(location, issues, vendor));
252
+ constructor(location, issues, vendor, functionName) {
253
+ super(400, ParserError.formatMessage(location, issues, vendor, functionName));
249
254
  Object.setPrototypeOf(this, new.target.prototype);
250
255
  }
251
- static formatMessage(location, issues, vendor) {
256
+ static formatMessage(location, issues, vendor, functionName) {
252
257
  const formatted = issues.map((i) => {
253
258
  const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
254
259
  return path ? `${path}: ${i.message}` : i.message;
255
260
  }).join("; ");
256
- return `${vendor} failed to parse ${(() => {
257
- switch (location) {
258
- case "reqbody": return "request body";
259
- case "reqparams": return "request params";
260
- case "reqquery": return "request query";
261
- }
262
- })()}: ${formatted}`;
261
+ return `Failed to parse ${this.getPrettyLocationString(location)} with ${vendor} on ${functionName}: ${formatted}`;
262
+ }
263
+ static getPrettyLocationString(location) {
264
+ switch (location) {
265
+ case "reqbody": return "request body";
266
+ case "reqparams": return "request params";
267
+ case "reqquery": return "request query";
268
+ case "resbody": return "response body";
269
+ }
263
270
  }
264
271
  };
265
272
  //#endregion
@@ -269,7 +276,11 @@ const _settings = {
269
276
  deserialize: JSON.parse,
270
277
  doc: {
271
278
  openApiPath: "/openapi.json",
272
- swaggerPath: "/swagger.html"
279
+ swaggerPath: "/swagger.html",
280
+ metadata: {
281
+ title: "API",
282
+ version: "1.0.0"
283
+ }
273
284
  }
274
285
  };
275
286
  /**
@@ -375,141 +386,74 @@ var Sapling = class Sapling {
375
386
  static setDeserializeFn(fn) {
376
387
  _settings.deserialize = fn;
377
388
  }
378
- static setOpenApiPath(path) {
379
- _settings.doc.openApiPath = path;
380
- }
381
- static setSwaggerPath(path) {
382
- _settings.doc.swaggerPath = path;
389
+ /**
390
+ * Modify extra settings
391
+ */
392
+ static Extras = {
393
+ /**
394
+ * Modify default settings applied to OpenAPI & Swagger
395
+ */
396
+ swaggerAndOpenApi: {
397
+ /**
398
+ * Set base OpenAPI metadata values.
399
+ *
400
+ * @default { title: "API", version: "1.0.0" }
401
+ */
402
+ setMetadata(metadata) {
403
+ _settings.doc.metadata = metadata;
404
+ },
405
+ /**
406
+ * change default endpoint that will serve OpenAPI spec.
407
+ * Swagger will also load this endpoint on load.
408
+ *
409
+ * @default `/openapi.json`
410
+ */
411
+ setOpenApiPath(path) {
412
+ _settings.doc.openApiPath = path;
413
+ },
414
+ /**
415
+ * change Swagger endpoint.
416
+ *
417
+ * @default `/swagger.html`
418
+ */
419
+ setSwaggerPath(path) {
420
+ _settings.doc.swaggerPath = path;
421
+ }
422
+ } };
423
+ /**
424
+ * This method can be used in a `@MiddlewareClass` to register any libraries
425
+ * that expect you to register multiple registers at once. An example is `swagger-ui-express`
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * ⠀@MiddlewareClass()
430
+ * class Serve {
431
+ * // `swagger.serve` returns multiple Express handlers for all the assets and routes
432
+ * // that will be served
433
+ * private readonly handlers: RequestHandler[] = swagger.serve;
434
+ *
435
+ * ⠀@Middleware(_settings.doc.swaggerPath)
436
+ * handle(request: Request, response: Response, next: NextFunction) {
437
+ * return Sapling.chainHandlers(this.handlers, request, response, next);
438
+ * }
439
+ * }
440
+ * ```
441
+ */
442
+ static chainHandlers(handlers, request, response, next, index = 0) {
443
+ if (index >= handlers.length) {
444
+ next();
445
+ return;
446
+ }
447
+ handlers[index]?.(request, response, (err) => {
448
+ if (err) {
449
+ next(err);
450
+ return;
451
+ }
452
+ Sapling.chainHandlers(handlers, request, response, next, index + 1);
453
+ });
383
454
  }
384
455
  };
385
456
  //#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);
509
- }
510
- return result.value;
511
- }
512
- //#endregion
513
457
  //#region src/annotation/route.ts
514
458
  const _routeStore = /* @__PURE__ */ new WeakMap();
515
459
  /**
@@ -591,63 +535,190 @@ function _getRoutes(ctor) {
591
535
  return _routeStore.get(ctor) ?? [];
592
536
  }
593
537
  //#endregion
538
+ //#region src/annotation/schema.ts
539
+ function ControllerSchema(options) {
540
+ return (target) => {
541
+ _setControllerSchema(target, options);
542
+ };
543
+ }
544
+ function RouteSchema(options) {
545
+ return (target, propertyKey) => {
546
+ const ctor = target.constructor;
547
+ _setRouteSchema(ctor, String(propertyKey), options);
548
+ };
549
+ }
550
+ const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
551
+ const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
552
+ function getOrCreateRouteSchemaStore(store, ctor) {
553
+ const existing = store.get(ctor);
554
+ if (existing) return existing;
555
+ const created = /* @__PURE__ */ new Map();
556
+ store.set(ctor, created);
557
+ return created;
558
+ }
559
+ function _setRouteSchema(ctor, fnName, options) {
560
+ getOrCreateRouteSchemaStore(_routeSchemaStore, ctor).set(fnName, options);
561
+ }
562
+ function _setControllerSchema(ctor, options) {
563
+ _controllerSchemaStore.set(ctor, options);
564
+ }
565
+ function _getRouteSchema(ctor, fnName) {
566
+ return _routeSchemaStore.get(ctor)?.get(fnName);
567
+ }
568
+ function _getControllerSchema(ctor) {
569
+ return _controllerSchemaStore.get(ctor);
570
+ }
571
+ //#endregion
572
+ //#region src/annotation/validator.ts
573
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
574
+ function ResponseBody(schema) {
575
+ return (target, propertyKey) => {
576
+ const ctor = target.constructor;
577
+ const fnName = String(propertyKey);
578
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
579
+ };
580
+ }
581
+ function RequestBody(schema) {
582
+ return (target, propertyKey) => {
583
+ const ctor = target.constructor;
584
+ const fnName = String(propertyKey);
585
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
586
+ };
587
+ }
588
+ function RequestParam(schema) {
589
+ return (target, propertyKey) => {
590
+ const ctor = target.constructor;
591
+ const fnName = String(propertyKey);
592
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
593
+ };
594
+ }
595
+ function RequestQuery(schema) {
596
+ return (target, propertyKey) => {
597
+ const ctor = target.constructor;
598
+ const fnName = String(propertyKey);
599
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
600
+ };
601
+ }
602
+ function getOrCreateValidatorSchemaStore(store, ctor) {
603
+ const existing = store.get(ctor);
604
+ if (existing) return existing;
605
+ const created = /* @__PURE__ */ new Map();
606
+ store.set(ctor, created);
607
+ return created;
608
+ }
609
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
610
+ const byFn = getOrCreateValidatorSchemaStore(_validatorSchemaStore, ctor);
611
+ const existing = byFn.get(fnName);
612
+ if (existing) return existing;
613
+ const created = {};
614
+ byFn.set(fnName, created);
615
+ return created;
616
+ }
617
+ async function _parseOrThrow(schema, input, location, fnName) {
618
+ const result = await schema["~standard"].validate(input);
619
+ if (result.issues) throw new ParserError(location, result.issues, schema["~standard"].vendor, fnName);
620
+ return result.value;
621
+ }
622
+ function _saveValidatorSchema(def, key, schema, fnName) {
623
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
624
+ def[key] = schema;
625
+ }
626
+ function _getValidatorSchema(ctor, fnName) {
627
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
628
+ }
629
+ //#endregion
594
630
  //#region src/helper/openapi.ts
595
631
  var OpenAPIGenerator = class {
596
- constructor() {
597
- this.controllers = /* @__PURE__ */ new Set();
598
- this.config = {
599
- title: "API",
600
- version: "1.0.0"
601
- };
602
- }
603
- setConfig(config) {
604
- this.config = config;
605
- }
632
+ controllers = /* @__PURE__ */ new Set();
606
633
  registerController(controllerClass, prefix) {
607
634
  this.controllers.add({
608
635
  class: controllerClass,
609
636
  prefix
610
637
  });
611
638
  }
639
+ get metadata() {
640
+ return _settings.doc.metadata;
641
+ }
612
642
  generateSpec() {
613
- const config = this.config;
643
+ const metadata = this.metadata;
614
644
  const paths = {};
645
+ const tags = [];
615
646
  for (const { class: controllerClass, prefix } of this.controllers) {
616
647
  const routes = _getRoutes(controllerClass);
648
+ const controllerSchema = _getControllerSchema(controllerClass);
649
+ if (controllerSchema?.title) tags.push({
650
+ name: controllerSchema.title,
651
+ description: controllerSchema.description
652
+ });
617
653
  for (const route of routes) {
618
654
  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;
655
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
656
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
657
+ 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.`);
658
+ const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
622
659
  if (!paths[openApiPath]) paths[openApiPath] = {};
623
- const operation = { responses: { "200": { description: "Successful response" } } };
660
+ const responses = {};
661
+ if (schemas?.responseBody) {
662
+ const responseSchema = this.toJsonSchema(schemas.responseBody, "output");
663
+ responses["200"] = {
664
+ description: responseSchema.description ?? "Successful response",
665
+ content: { "application/json": { schema: responseSchema } }
666
+ };
667
+ } else responses["200"] = { description: "Successful response" };
668
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) {
669
+ const statusCode = String(resp.statusCode);
670
+ const existingResponse = responses[statusCode];
671
+ const existingSchema = existingResponse && "content" in existingResponse ? existingResponse.content?.["application/json"]?.schema : void 0;
672
+ const responseSchema = resp.schema ? this.toJsonSchema(resp.schema, "output") : statusCode === "200" ? existingSchema : void 0;
673
+ responses[statusCode] = {
674
+ ...existingResponse,
675
+ description: resp.description ?? responseSchema?.description ?? existingResponse?.description ?? `Response ${resp.statusCode}`,
676
+ ...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
677
+ };
678
+ }
679
+ const operation = {
680
+ responses,
681
+ summary: routeSchema?.summary,
682
+ description: routeSchema?.description,
683
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
684
+ };
624
685
  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
- });
686
+ if (schemas?.requestParam) {
687
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
688
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
689
+ const parameterSchema = schema;
690
+ parameters.push({
691
+ name,
692
+ in: "path",
693
+ required: true,
694
+ description: parameterSchema.description,
695
+ schema: parameterSchema
696
+ });
697
+ }
633
698
  }
634
- if (schemas?.query) {
635
- const querySchema = this.toJsonSchema(schemas.query);
699
+ if (schemas?.requestQuery) {
700
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
636
701
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
637
702
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
703
+ const parameterSchema = schema;
638
704
  parameters.push({
639
705
  name,
640
706
  in: "query",
641
707
  required: isRequired,
642
- schema
708
+ description: parameterSchema.description,
709
+ schema: parameterSchema
643
710
  });
644
711
  }
645
712
  }
646
713
  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
- };
714
+ if (schemas?.requestBody) {
715
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
716
+ operation.requestBody = {
717
+ required: true,
718
+ description: requestSchema.description,
719
+ content: { "application/json": { schema: requestSchema } }
720
+ };
721
+ }
651
722
  const method = route.method.toLowerCase();
652
723
  paths[openApiPath][method] = operation;
653
724
  }
@@ -655,30 +726,29 @@ var OpenAPIGenerator = class {
655
726
  return {
656
727
  openapi: "3.0.0",
657
728
  info: {
658
- title: config.title,
659
- version: config.version,
660
- description: config.description
729
+ title: metadata.title,
730
+ version: metadata.version,
731
+ description: metadata.description
661
732
  },
733
+ tags: tags.length > 0 ? tags : void 0,
662
734
  paths
663
735
  };
664
736
  }
665
- toJsonSchema(schema) {
737
+ toJsonSchema(schema, direction = "output") {
666
738
  try {
667
- return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
739
+ const jsonSchema = schema["~standard"].jsonSchema;
740
+ return direction === "input" ? jsonSchema.input({ target: "openapi-3.0" }) : jsonSchema.output({ target: "openapi-3.0" });
668
741
  } catch (e) {
669
- 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`);
742
+ if (e instanceof Error && e.message.includes("Transforms cannot be represented in JSON Schema")) throw new Error(`${e.message}.\nIt appears that you are using z.transform() - it is highly recommended that you use z.codec instead - https://zod.dev/codecs`, { cause: e });
670
743
  throw e;
671
744
  }
672
745
  }
673
746
  };
674
747
  const openApiGenerator = new OpenAPIGenerator();
675
- function _registerControllerClass(controllerClass, prefix) {
748
+ function _registerController(controllerClass, prefix) {
676
749
  openApiGenerator.registerController(controllerClass, prefix);
677
750
  }
678
- function _setOpenApiConfig(config) {
679
- openApiGenerator.setConfig(config);
680
- }
681
- function _generateOpenApiSpec() {
751
+ function generateOpenApiSpec() {
682
752
  return openApiGenerator.generateSpec();
683
753
  }
684
754
  //#endregion
@@ -825,7 +895,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
825
895
  function Controller({ prefix = "", deps = [] } = {}) {
826
896
  return (target) => {
827
897
  const targetClass = target;
828
- _registerControllerClass(target, prefix);
898
+ _registerController(target, prefix);
829
899
  const router = (0, express.Router)();
830
900
  const routes = _getRoutes(target);
831
901
  const usedRoutes = /* @__PURE__ */ new Set();
@@ -850,15 +920,20 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
850
920
  if (method === "USE" && fn.length >= 4) {
851
921
  const middlewareFn = async (err, request, response, next) => {
852
922
  try {
853
- const result = fn.bind(controllerInstance)(err, request, response, next);
854
- if (result instanceof ResponseEntity) {
855
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
856
- return;
857
- }
858
- if (result instanceof RedirectView) {
859
- response.redirect(result.getUrl());
860
- return;
861
- }
923
+ await validate({
924
+ target,
925
+ fnName,
926
+ request
927
+ });
928
+ await handleResult({
929
+ result: fn.bind(controllerInstance)(err, request, response, next),
930
+ response,
931
+ target,
932
+ fnName,
933
+ method,
934
+ path: path instanceof RegExp ? path.source : fp,
935
+ isErrorMiddleware: true
936
+ });
862
937
  } catch (e) {
863
938
  console.error(e);
864
939
  next(e);
@@ -868,34 +943,52 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
868
943
  return;
869
944
  }
870
945
  router[methodName](fp, async (request, response, next) => {
871
- const schemas = _getRequestSchemas(target, fnName);
872
- 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");
877
- Object.defineProperty(request, "query", {
878
- value: parsedQuery,
879
- writable: true,
880
- configurable: true
881
- });
882
- }
883
- }
884
- const result = await fn.bind(controllerInstance)(request, response, next);
885
- if (result instanceof ResponseEntity) {
886
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
887
- return;
888
- }
889
- if (result instanceof RedirectView) {
890
- response.redirect(result.getUrl());
891
- return;
892
- }
893
- if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
946
+ await validate({
947
+ target,
948
+ fnName,
949
+ request
950
+ });
951
+ await handleResult({
952
+ result: await fn.bind(controllerInstance)(request, response, next),
953
+ response,
954
+ target,
955
+ fnName,
956
+ method,
957
+ path: path instanceof RegExp ? path.source : fp
958
+ });
894
959
  });
895
960
  }
896
961
  _ControllerRegistry.set(targetClass, router);
897
962
  };
898
963
  }
964
+ async function handleResult({ result, target, fnName, response, method, path, isErrorMiddleware = false }) {
965
+ const schemas = _getValidatorSchema(target, fnName);
966
+ if (result instanceof ResponseEntity) {
967
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody", fnName) : result.getBody();
968
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
969
+ return;
970
+ }
971
+ if (result instanceof RedirectView) {
972
+ response.redirect(result.getUrl());
973
+ return;
974
+ }
975
+ if (!isErrorMiddleware && method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${method} ${path}`));
976
+ }
977
+ async function validate({ target, fnName, request }) {
978
+ const schemas = _getValidatorSchema(target, fnName);
979
+ if (schemas) {
980
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody", fnName);
981
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams", fnName);
982
+ if (schemas.requestQuery) {
983
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery", fnName);
984
+ Object.defineProperty(request, "query", {
985
+ value: parsedQuery,
986
+ writable: true,
987
+ configurable: true
988
+ });
989
+ }
990
+ }
991
+ }
899
992
  //#endregion
900
993
  //#region src/annotation/middleware.ts
901
994
  /**
@@ -930,7 +1023,10 @@ DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMid
930
1023
  //#region src/middleware/default/error/parse.ts
931
1024
  let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
932
1025
  handle(err, _request, _response, next) {
933
- if (err instanceof ParserError) return ResponseEntity.status(err.status).body({ message: err.message });
1026
+ if (err instanceof ParserError) {
1027
+ console.warn(err);
1028
+ return ResponseEntity.status(err.status).body({ message: err.message });
1029
+ }
934
1030
  next(err);
935
1031
  }
936
1032
  };
@@ -950,26 +1046,57 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
950
1046
  //#region src/middleware/default/openapi/index.ts
951
1047
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
952
1048
  handle(_request, _response, _next) {
953
- return ResponseEntity.ok().body(_generateOpenApiSpec());
1049
+ return ResponseEntity.ok().body(generateOpenApiSpec());
954
1050
  }
955
1051
  };
956
1052
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
957
1053
  DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
958
1054
  //#endregion
959
1055
  //#region src/middleware/default/swagger/index.ts
1056
+ /**
1057
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1058
+ *
1059
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1060
+ *
1061
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1062
+ *
1063
+ * ```ts
1064
+ * const middlewares = [
1065
+ * DefaultOpenApiMiddleware,
1066
+ * DefaultSwaggerMiddleware.Serve,
1067
+ * DefaultSwaggerMiddleware.Setup,
1068
+ * ];
1069
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1070
+ * ```
1071
+ */
960
1072
  let Serve = class Serve {
961
- constructor() {
962
- this.handlers = swagger_ui_express.default.serve;
963
- }
964
- handle(_request, _response, _next) {
965
- return this.handlers;
1073
+ handlers = swagger_ui_express.default.serve;
1074
+ handle(request, response, next) {
1075
+ return Sapling.chainHandlers(this.handlers, request, response, next);
966
1076
  }
967
1077
  };
968
1078
  __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
969
1079
  Serve = __decorate([MiddlewareClass()], Serve);
1080
+ /**
1081
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1082
+ *
1083
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1084
+ *
1085
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1086
+ *
1087
+ * ```ts
1088
+ * const middlewares = [
1089
+ * DefaultOpenApiMiddleware,
1090
+ * DefaultSwaggerMiddleware.Serve,
1091
+ * DefaultSwaggerMiddleware.Setup,
1092
+ * ];
1093
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1094
+ * ```
1095
+ */
970
1096
  let Setup = class Setup {
1097
+ handler;
971
1098
  constructor() {
972
- this.handler = swagger_ui_express.default.setup(void 0, { swaggerOptions: { url: _settings.doc.openApiPath } });
1099
+ this.handler = swagger_ui_express.default.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
973
1100
  }
974
1101
  handle(request, response, next) {
975
1102
  return this.handler(request, response, next);
@@ -977,12 +1104,29 @@ let Setup = class Setup {
977
1104
  };
978
1105
  __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
979
1106
  Setup = __decorate([MiddlewareClass()], Setup);
1107
+ /**
1108
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1109
+ *
1110
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1111
+ *
1112
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1113
+ *
1114
+ * ```ts
1115
+ * const middlewares = [
1116
+ * DefaultOpenApiMiddleware,
1117
+ * DefaultSwaggerMiddleware.Serve,
1118
+ * DefaultSwaggerMiddleware.Setup,
1119
+ * ];
1120
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1121
+ * ```
1122
+ */
980
1123
  const DefaultSwaggerMiddleware = {
981
1124
  Serve,
982
1125
  Setup
983
1126
  };
984
1127
  //#endregion
985
1128
  exports.Controller = Controller;
1129
+ exports.ControllerSchema = ControllerSchema;
986
1130
  exports.DELETE = DELETE;
987
1131
  Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
988
1132
  enumerable: true,
@@ -1025,21 +1169,28 @@ exports.RedirectView = RedirectView;
1025
1169
  exports.RequestBody = RequestBody;
1026
1170
  exports.RequestParam = RequestParam;
1027
1171
  exports.RequestQuery = RequestQuery;
1172
+ exports.ResponseBody = ResponseBody;
1028
1173
  exports.ResponseEntity = ResponseEntity;
1029
1174
  exports.ResponseEntityBuilder = ResponseEntityBuilder;
1030
1175
  exports.ResponseStatusError = ResponseStatusError;
1176
+ exports.RouteSchema = RouteSchema;
1031
1177
  exports.Sapling = Sapling;
1032
1178
  exports._ControllerRegistry = _ControllerRegistry;
1033
1179
  exports._InjectableDeps = _InjectableDeps;
1034
1180
  exports._InjectableRegistry = _InjectableRegistry;
1035
1181
  exports._Route = _Route;
1036
- exports._generateOpenApiSpec = _generateOpenApiSpec;
1037
- exports._getRequestSchemas = _getRequestSchemas;
1182
+ exports._getControllerSchema = _getControllerSchema;
1183
+ exports._getOrCreateSchemaDefinition = _getOrCreateSchemaDefinition;
1184
+ exports._getRouteSchema = _getRouteSchema;
1038
1185
  exports._getRoutes = _getRoutes;
1186
+ exports._getValidatorSchema = _getValidatorSchema;
1039
1187
  exports._parseOrThrow = _parseOrThrow;
1040
- exports._registerControllerClass = _registerControllerClass;
1188
+ exports._registerController = _registerController;
1041
1189
  exports._resolve = _resolve;
1042
- exports._setOpenApiConfig = _setOpenApiConfig;
1190
+ exports._saveValidatorSchema = _saveValidatorSchema;
1191
+ exports._setControllerSchema = _setControllerSchema;
1192
+ exports._setRouteSchema = _setRouteSchema;
1043
1193
  exports._settings = _settings;
1194
+ exports.generateOpenApiSpec = generateOpenApiSpec;
1044
1195
  exports.methodResolve = methodResolve;
1045
1196
  exports.openApiGenerator = openApiGenerator;