@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.mjs CHANGED
@@ -23,6 +23,7 @@ const Html404ErrorPage = (error) => `<!DOCTYPE html>
23
23
  * You can either return `new RedirectView(url)` or `RedirectView.redirect(url)` inside of a controller method.
24
24
  */
25
25
  var RedirectView = class RedirectView {
26
+ _url;
26
27
  constructor(url) {
27
28
  this._url = url;
28
29
  }
@@ -118,8 +119,10 @@ let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
118
119
  * @typeParam T - the type of the response body
119
120
  */
120
121
  var ResponseEntity = class {
122
+ _statusCode;
123
+ _headers = {};
124
+ _body;
121
125
  constructor(body, headers = {}, statusCode = 200) {
122
- this._headers = {};
123
126
  this._body = body;
124
127
  this._headers = headers;
125
128
  this._statusCode = statusCode;
@@ -172,8 +175,9 @@ var ResponseEntity = class {
172
175
  * ensuring type safety when constructing the response.
173
176
  */
174
177
  var ResponseEntityBuilder = class {
178
+ _statusCode;
179
+ _headers = {};
175
180
  constructor(statusCode) {
176
- this._headers = {};
177
181
  this._statusCode = statusCode;
178
182
  }
179
183
  /**
@@ -205,6 +209,7 @@ var ResponseEntityBuilder = class {
205
209
  * @see {@link Sapling.loadResponseStatusErrorMiddleware}
206
210
  */
207
211
  var ResponseStatusError = class ResponseStatusError extends Error {
212
+ status;
208
213
  constructor(status, message) {
209
214
  super(message ?? "Something went wrong.");
210
215
  this.status = status;
@@ -216,25 +221,27 @@ var ResponseStatusError = class ResponseStatusError extends Error {
216
221
  //#endregion
217
222
  //#region src/helper/error/parse.ts
218
223
  /**
219
- * This error should be thrown when some data cannot be parsed by a given schema.
224
+ * This error should be thrown when some data cannot be parsed by a given Standard Schema compatible schema.
220
225
  */
221
226
  var ParserError = class ParserError extends ResponseStatusError {
222
- constructor(location, issues, vendor) {
223
- super(400, ParserError.formatMessage(location, issues, vendor));
227
+ constructor(location, issues, vendor, functionName) {
228
+ super(400, ParserError.formatMessage(location, issues, vendor, functionName));
224
229
  Object.setPrototypeOf(this, new.target.prototype);
225
230
  }
226
- static formatMessage(location, issues, vendor) {
231
+ static formatMessage(location, issues, vendor, functionName) {
227
232
  const formatted = issues.map((i) => {
228
233
  const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
229
234
  return path ? `${path}: ${i.message}` : i.message;
230
235
  }).join("; ");
231
- return `${vendor} failed to parse ${(() => {
232
- switch (location) {
233
- case "reqbody": return "request body";
234
- case "reqparams": return "request params";
235
- case "reqquery": return "request query";
236
- }
237
- })()}: ${formatted}`;
236
+ return `Failed to parse ${this.getPrettyLocationString(location)} with ${vendor} on ${functionName}: ${formatted}`;
237
+ }
238
+ static getPrettyLocationString(location) {
239
+ switch (location) {
240
+ case "reqbody": return "request body";
241
+ case "reqparams": return "request params";
242
+ case "reqquery": return "request query";
243
+ case "resbody": return "response body";
244
+ }
238
245
  }
239
246
  };
240
247
  //#endregion
@@ -244,7 +251,11 @@ const _settings = {
244
251
  deserialize: JSON.parse,
245
252
  doc: {
246
253
  openApiPath: "/openapi.json",
247
- swaggerPath: "/swagger.html"
254
+ swaggerPath: "/swagger.html",
255
+ metadata: {
256
+ title: "API",
257
+ version: "1.0.0"
258
+ }
248
259
  }
249
260
  };
250
261
  /**
@@ -350,141 +361,74 @@ var Sapling = class Sapling {
350
361
  static setDeserializeFn(fn) {
351
362
  _settings.deserialize = fn;
352
363
  }
353
- static setOpenApiPath(path) {
354
- _settings.doc.openApiPath = path;
355
- }
356
- static setSwaggerPath(path) {
357
- _settings.doc.swaggerPath = path;
364
+ /**
365
+ * Modify extra settings
366
+ */
367
+ static Extras = {
368
+ /**
369
+ * Modify default settings applied to OpenAPI & Swagger
370
+ */
371
+ swaggerAndOpenApi: {
372
+ /**
373
+ * Set base OpenAPI metadata values.
374
+ *
375
+ * @default { title: "API", version: "1.0.0" }
376
+ */
377
+ setMetadata(metadata) {
378
+ _settings.doc.metadata = metadata;
379
+ },
380
+ /**
381
+ * change default endpoint that will serve OpenAPI spec.
382
+ * Swagger will also load this endpoint on load.
383
+ *
384
+ * @default `/openapi.json`
385
+ */
386
+ setOpenApiPath(path) {
387
+ _settings.doc.openApiPath = path;
388
+ },
389
+ /**
390
+ * change Swagger endpoint.
391
+ *
392
+ * @default `/swagger.html`
393
+ */
394
+ setSwaggerPath(path) {
395
+ _settings.doc.swaggerPath = path;
396
+ }
397
+ } };
398
+ /**
399
+ * This method can be used in a `@MiddlewareClass` to register any libraries
400
+ * that expect you to register multiple registers at once. An example is `swagger-ui-express`
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * ⠀@MiddlewareClass()
405
+ * class Serve {
406
+ * // `swagger.serve` returns multiple Express handlers for all the assets and routes
407
+ * // that will be served
408
+ * private readonly handlers: RequestHandler[] = swagger.serve;
409
+ *
410
+ * ⠀@Middleware(_settings.doc.swaggerPath)
411
+ * handle(request: Request, response: Response, next: NextFunction) {
412
+ * return Sapling.chainHandlers(this.handlers, request, response, next);
413
+ * }
414
+ * }
415
+ * ```
416
+ */
417
+ static chainHandlers(handlers, request, response, next, index = 0) {
418
+ if (index >= handlers.length) {
419
+ next();
420
+ return;
421
+ }
422
+ handlers[index]?.(request, response, (err) => {
423
+ if (err) {
424
+ next(err);
425
+ return;
426
+ }
427
+ Sapling.chainHandlers(handlers, request, response, next, index + 1);
428
+ });
358
429
  }
359
430
  };
360
431
  //#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);
484
- }
485
- return result.value;
486
- }
487
- //#endregion
488
432
  //#region src/annotation/route.ts
489
433
  const _routeStore = /* @__PURE__ */ new WeakMap();
490
434
  /**
@@ -566,63 +510,190 @@ function _getRoutes(ctor) {
566
510
  return _routeStore.get(ctor) ?? [];
567
511
  }
568
512
  //#endregion
513
+ //#region src/annotation/schema.ts
514
+ function ControllerSchema(options) {
515
+ return (target) => {
516
+ _setControllerSchema(target, options);
517
+ };
518
+ }
519
+ function RouteSchema(options) {
520
+ return (target, propertyKey) => {
521
+ const ctor = target.constructor;
522
+ _setRouteSchema(ctor, String(propertyKey), options);
523
+ };
524
+ }
525
+ const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
526
+ const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
527
+ function getOrCreateRouteSchemaStore(store, ctor) {
528
+ const existing = store.get(ctor);
529
+ if (existing) return existing;
530
+ const created = /* @__PURE__ */ new Map();
531
+ store.set(ctor, created);
532
+ return created;
533
+ }
534
+ function _setRouteSchema(ctor, fnName, options) {
535
+ getOrCreateRouteSchemaStore(_routeSchemaStore, ctor).set(fnName, options);
536
+ }
537
+ function _setControllerSchema(ctor, options) {
538
+ _controllerSchemaStore.set(ctor, options);
539
+ }
540
+ function _getRouteSchema(ctor, fnName) {
541
+ return _routeSchemaStore.get(ctor)?.get(fnName);
542
+ }
543
+ function _getControllerSchema(ctor) {
544
+ return _controllerSchemaStore.get(ctor);
545
+ }
546
+ //#endregion
547
+ //#region src/annotation/validator.ts
548
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
549
+ function ResponseBody(schema) {
550
+ return (target, propertyKey) => {
551
+ const ctor = target.constructor;
552
+ const fnName = String(propertyKey);
553
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
554
+ };
555
+ }
556
+ function RequestBody(schema) {
557
+ return (target, propertyKey) => {
558
+ const ctor = target.constructor;
559
+ const fnName = String(propertyKey);
560
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
561
+ };
562
+ }
563
+ function RequestParam(schema) {
564
+ return (target, propertyKey) => {
565
+ const ctor = target.constructor;
566
+ const fnName = String(propertyKey);
567
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
568
+ };
569
+ }
570
+ function RequestQuery(schema) {
571
+ return (target, propertyKey) => {
572
+ const ctor = target.constructor;
573
+ const fnName = String(propertyKey);
574
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
575
+ };
576
+ }
577
+ function getOrCreateValidatorSchemaStore(store, ctor) {
578
+ const existing = store.get(ctor);
579
+ if (existing) return existing;
580
+ const created = /* @__PURE__ */ new Map();
581
+ store.set(ctor, created);
582
+ return created;
583
+ }
584
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
585
+ const byFn = getOrCreateValidatorSchemaStore(_validatorSchemaStore, ctor);
586
+ const existing = byFn.get(fnName);
587
+ if (existing) return existing;
588
+ const created = {};
589
+ byFn.set(fnName, created);
590
+ return created;
591
+ }
592
+ async function _parseOrThrow(schema, input, location, fnName) {
593
+ const result = await schema["~standard"].validate(input);
594
+ if (result.issues) throw new ParserError(location, result.issues, schema["~standard"].vendor, fnName);
595
+ return result.value;
596
+ }
597
+ function _saveValidatorSchema(def, key, schema, fnName) {
598
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
599
+ def[key] = schema;
600
+ }
601
+ function _getValidatorSchema(ctor, fnName) {
602
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
603
+ }
604
+ //#endregion
569
605
  //#region src/helper/openapi.ts
570
606
  var OpenAPIGenerator = class {
571
- constructor() {
572
- this.controllers = /* @__PURE__ */ new Set();
573
- this.config = {
574
- title: "API",
575
- version: "1.0.0"
576
- };
577
- }
578
- setConfig(config) {
579
- this.config = config;
580
- }
607
+ controllers = /* @__PURE__ */ new Set();
581
608
  registerController(controllerClass, prefix) {
582
609
  this.controllers.add({
583
610
  class: controllerClass,
584
611
  prefix
585
612
  });
586
613
  }
614
+ get metadata() {
615
+ return _settings.doc.metadata;
616
+ }
587
617
  generateSpec() {
588
- const config = this.config;
618
+ const metadata = this.metadata;
589
619
  const paths = {};
620
+ const tags = [];
590
621
  for (const { class: controllerClass, prefix } of this.controllers) {
591
622
  const routes = _getRoutes(controllerClass);
623
+ const controllerSchema = _getControllerSchema(controllerClass);
624
+ if (controllerSchema?.title) tags.push({
625
+ name: controllerSchema.title,
626
+ description: controllerSchema.description
627
+ });
592
628
  for (const route of routes) {
593
629
  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;
630
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
631
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
632
+ 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.`);
633
+ const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
597
634
  if (!paths[openApiPath]) paths[openApiPath] = {};
598
- const operation = { responses: { "200": { description: "Successful response" } } };
635
+ const responses = {};
636
+ if (schemas?.responseBody) {
637
+ const responseSchema = this.toJsonSchema(schemas.responseBody, "output");
638
+ responses["200"] = {
639
+ description: responseSchema.description ?? "Successful response",
640
+ content: { "application/json": { schema: responseSchema } }
641
+ };
642
+ } else responses["200"] = { description: "Successful response" };
643
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) {
644
+ const statusCode = String(resp.statusCode);
645
+ const existingResponse = responses[statusCode];
646
+ const existingSchema = existingResponse && "content" in existingResponse ? existingResponse.content?.["application/json"]?.schema : void 0;
647
+ const responseSchema = resp.schema ? this.toJsonSchema(resp.schema, "output") : statusCode === "200" ? existingSchema : void 0;
648
+ responses[statusCode] = {
649
+ ...existingResponse,
650
+ description: resp.description ?? responseSchema?.description ?? existingResponse?.description ?? `Response ${resp.statusCode}`,
651
+ ...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
652
+ };
653
+ }
654
+ const operation = {
655
+ responses,
656
+ summary: routeSchema?.summary,
657
+ description: routeSchema?.description,
658
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
659
+ };
599
660
  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
- });
661
+ if (schemas?.requestParam) {
662
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
663
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
664
+ const parameterSchema = schema;
665
+ parameters.push({
666
+ name,
667
+ in: "path",
668
+ required: true,
669
+ description: parameterSchema.description,
670
+ schema: parameterSchema
671
+ });
672
+ }
608
673
  }
609
- if (schemas?.query) {
610
- const querySchema = this.toJsonSchema(schemas.query);
674
+ if (schemas?.requestQuery) {
675
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
611
676
  if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
612
677
  const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
678
+ const parameterSchema = schema;
613
679
  parameters.push({
614
680
  name,
615
681
  in: "query",
616
682
  required: isRequired,
617
- schema
683
+ description: parameterSchema.description,
684
+ schema: parameterSchema
618
685
  });
619
686
  }
620
687
  }
621
688
  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
- };
689
+ if (schemas?.requestBody) {
690
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
691
+ operation.requestBody = {
692
+ required: true,
693
+ description: requestSchema.description,
694
+ content: { "application/json": { schema: requestSchema } }
695
+ };
696
+ }
626
697
  const method = route.method.toLowerCase();
627
698
  paths[openApiPath][method] = operation;
628
699
  }
@@ -630,30 +701,29 @@ var OpenAPIGenerator = class {
630
701
  return {
631
702
  openapi: "3.0.0",
632
703
  info: {
633
- title: config.title,
634
- version: config.version,
635
- description: config.description
704
+ title: metadata.title,
705
+ version: metadata.version,
706
+ description: metadata.description
636
707
  },
708
+ tags: tags.length > 0 ? tags : void 0,
637
709
  paths
638
710
  };
639
711
  }
640
- toJsonSchema(schema) {
712
+ toJsonSchema(schema, direction = "output") {
641
713
  try {
642
- return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
714
+ const jsonSchema = schema["~standard"].jsonSchema;
715
+ return direction === "input" ? jsonSchema.input({ target: "openapi-3.0" }) : jsonSchema.output({ target: "openapi-3.0" });
643
716
  } catch (e) {
644
- 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`);
717
+ 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 });
645
718
  throw e;
646
719
  }
647
720
  }
648
721
  };
649
722
  const openApiGenerator = new OpenAPIGenerator();
650
- function _registerControllerClass(controllerClass, prefix) {
723
+ function _registerController(controllerClass, prefix) {
651
724
  openApiGenerator.registerController(controllerClass, prefix);
652
725
  }
653
- function _setOpenApiConfig(config) {
654
- openApiGenerator.setConfig(config);
655
- }
656
- function _generateOpenApiSpec() {
726
+ function generateOpenApiSpec() {
657
727
  return openApiGenerator.generateSpec();
658
728
  }
659
729
  //#endregion
@@ -800,7 +870,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
800
870
  function Controller({ prefix = "", deps = [] } = {}) {
801
871
  return (target) => {
802
872
  const targetClass = target;
803
- _registerControllerClass(target, prefix);
873
+ _registerController(target, prefix);
804
874
  const router = Router();
805
875
  const routes = _getRoutes(target);
806
876
  const usedRoutes = /* @__PURE__ */ new Set();
@@ -825,15 +895,20 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
825
895
  if (method === "USE" && fn.length >= 4) {
826
896
  const middlewareFn = async (err, request, response, next) => {
827
897
  try {
828
- const result = fn.bind(controllerInstance)(err, request, response, next);
829
- if (result instanceof ResponseEntity) {
830
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
831
- return;
832
- }
833
- if (result instanceof RedirectView) {
834
- response.redirect(result.getUrl());
835
- return;
836
- }
898
+ await validate({
899
+ target,
900
+ fnName,
901
+ request
902
+ });
903
+ await handleResult({
904
+ result: fn.bind(controllerInstance)(err, request, response, next),
905
+ response,
906
+ target,
907
+ fnName,
908
+ method,
909
+ path: path instanceof RegExp ? path.source : fp,
910
+ isErrorMiddleware: true
911
+ });
837
912
  } catch (e) {
838
913
  console.error(e);
839
914
  next(e);
@@ -843,34 +918,52 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
843
918
  return;
844
919
  }
845
920
  router[methodName](fp, async (request, response, next) => {
846
- const schemas = _getRequestSchemas(target, fnName);
847
- 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");
852
- Object.defineProperty(request, "query", {
853
- value: parsedQuery,
854
- writable: true,
855
- configurable: true
856
- });
857
- }
858
- }
859
- const result = await fn.bind(controllerInstance)(request, response, next);
860
- if (result instanceof ResponseEntity) {
861
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
862
- return;
863
- }
864
- if (result instanceof RedirectView) {
865
- response.redirect(result.getUrl());
866
- return;
867
- }
868
- if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
921
+ await validate({
922
+ target,
923
+ fnName,
924
+ request
925
+ });
926
+ await handleResult({
927
+ result: await fn.bind(controllerInstance)(request, response, next),
928
+ response,
929
+ target,
930
+ fnName,
931
+ method,
932
+ path: path instanceof RegExp ? path.source : fp
933
+ });
869
934
  });
870
935
  }
871
936
  _ControllerRegistry.set(targetClass, router);
872
937
  };
873
938
  }
939
+ async function handleResult({ result, target, fnName, response, method, path, isErrorMiddleware = false }) {
940
+ const schemas = _getValidatorSchema(target, fnName);
941
+ if (result instanceof ResponseEntity) {
942
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody", fnName) : result.getBody();
943
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
944
+ return;
945
+ }
946
+ if (result instanceof RedirectView) {
947
+ response.redirect(result.getUrl());
948
+ return;
949
+ }
950
+ if (!isErrorMiddleware && method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${method} ${path}`));
951
+ }
952
+ async function validate({ target, fnName, request }) {
953
+ const schemas = _getValidatorSchema(target, fnName);
954
+ if (schemas) {
955
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody", fnName);
956
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams", fnName);
957
+ if (schemas.requestQuery) {
958
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery", fnName);
959
+ Object.defineProperty(request, "query", {
960
+ value: parsedQuery,
961
+ writable: true,
962
+ configurable: true
963
+ });
964
+ }
965
+ }
966
+ }
874
967
  //#endregion
875
968
  //#region src/annotation/middleware.ts
876
969
  /**
@@ -905,7 +998,10 @@ DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMid
905
998
  //#region src/middleware/default/error/parse.ts
906
999
  let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
907
1000
  handle(err, _request, _response, next) {
908
- if (err instanceof ParserError) return ResponseEntity.status(err.status).body({ message: err.message });
1001
+ if (err instanceof ParserError) {
1002
+ console.warn(err);
1003
+ return ResponseEntity.status(err.status).body({ message: err.message });
1004
+ }
909
1005
  next(err);
910
1006
  }
911
1007
  };
@@ -925,26 +1021,57 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
925
1021
  //#region src/middleware/default/openapi/index.ts
926
1022
  let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
927
1023
  handle(_request, _response, _next) {
928
- return ResponseEntity.ok().body(_generateOpenApiSpec());
1024
+ return ResponseEntity.ok().body(generateOpenApiSpec());
929
1025
  }
930
1026
  };
931
1027
  __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
932
1028
  DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
933
1029
  //#endregion
934
1030
  //#region src/middleware/default/swagger/index.ts
1031
+ /**
1032
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1033
+ *
1034
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1035
+ *
1036
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1037
+ *
1038
+ * ```ts
1039
+ * const middlewares = [
1040
+ * DefaultOpenApiMiddleware,
1041
+ * DefaultSwaggerMiddleware.Serve,
1042
+ * DefaultSwaggerMiddleware.Setup,
1043
+ * ];
1044
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1045
+ * ```
1046
+ */
935
1047
  let Serve = class Serve {
936
- constructor() {
937
- this.handlers = swagger.serve;
938
- }
939
- handle(_request, _response, _next) {
940
- return this.handlers;
1048
+ handlers = swagger.serve;
1049
+ handle(request, response, next) {
1050
+ return Sapling.chainHandlers(this.handlers, request, response, next);
941
1051
  }
942
1052
  };
943
1053
  __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
944
1054
  Serve = __decorate([MiddlewareClass()], Serve);
1055
+ /**
1056
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1057
+ *
1058
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1059
+ *
1060
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1061
+ *
1062
+ * ```ts
1063
+ * const middlewares = [
1064
+ * DefaultOpenApiMiddleware,
1065
+ * DefaultSwaggerMiddleware.Serve,
1066
+ * DefaultSwaggerMiddleware.Setup,
1067
+ * ];
1068
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1069
+ * ```
1070
+ */
945
1071
  let Setup = class Setup {
1072
+ handler;
946
1073
  constructor() {
947
- this.handler = swagger.setup(void 0, { swaggerOptions: { url: _settings.doc.openApiPath } });
1074
+ this.handler = swagger.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
948
1075
  }
949
1076
  handle(request, response, next) {
950
1077
  return this.handler(request, response, next);
@@ -952,9 +1079,25 @@ let Setup = class Setup {
952
1079
  };
953
1080
  __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
954
1081
  Setup = __decorate([MiddlewareClass()], Setup);
1082
+ /**
1083
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1084
+ *
1085
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1086
+ *
1087
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1088
+ *
1089
+ * ```ts
1090
+ * const middlewares = [
1091
+ * DefaultOpenApiMiddleware,
1092
+ * DefaultSwaggerMiddleware.Serve,
1093
+ * DefaultSwaggerMiddleware.Setup,
1094
+ * ];
1095
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1096
+ * ```
1097
+ */
955
1098
  const DefaultSwaggerMiddleware = {
956
1099
  Serve,
957
1100
  Setup
958
1101
  };
959
1102
  //#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 };
1103
+ 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, _registerController, _resolve, _saveValidatorSchema, _setControllerSchema, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator };