@tahminator/sapling 2.0.5 → 2.1.0-beta.6ea2cd3e

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
@@ -1,4 +1,5 @@
1
1
  import e, { Router } from "express";
2
+ import swagger from "swagger-ui-express";
2
3
  //#region src/html/404.ts
3
4
  /**
4
5
  * Default Express.js 404 error page, as a string.
@@ -22,6 +23,7 @@ const Html404ErrorPage = (error) => `<!DOCTYPE html>
22
23
  * You can either return `new RedirectView(url)` or `RedirectView.redirect(url)` inside of a controller method.
23
24
  */
24
25
  var RedirectView = class RedirectView {
26
+ _url;
25
27
  constructor(url) {
26
28
  this._url = url;
27
29
  }
@@ -117,8 +119,10 @@ let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
117
119
  * @typeParam T - the type of the response body
118
120
  */
119
121
  var ResponseEntity = class {
122
+ _statusCode;
123
+ _headers = {};
124
+ _body;
120
125
  constructor(body, headers = {}, statusCode = 200) {
121
- this._headers = {};
122
126
  this._body = body;
123
127
  this._headers = headers;
124
128
  this._statusCode = statusCode;
@@ -171,8 +175,9 @@ var ResponseEntity = class {
171
175
  * ensuring type safety when constructing the response.
172
176
  */
173
177
  var ResponseEntityBuilder = class {
178
+ _statusCode;
179
+ _headers = {};
174
180
  constructor(statusCode) {
175
- this._headers = {};
176
181
  this._statusCode = statusCode;
177
182
  }
178
183
  /**
@@ -204,6 +209,7 @@ var ResponseEntityBuilder = class {
204
209
  * @see {@link Sapling.loadResponseStatusErrorMiddleware}
205
210
  */
206
211
  var ResponseStatusError = class ResponseStatusError extends Error {
212
+ status;
207
213
  constructor(status, message) {
208
214
  super(message ?? "Something went wrong.");
209
215
  this.status = status;
@@ -215,150 +221,30 @@ var ResponseStatusError = class ResponseStatusError extends Error {
215
221
  //#endregion
216
222
  //#region src/helper/error/parse.ts
217
223
  /**
218
- * 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.
219
225
  */
220
226
  var ParserError = class ParserError extends ResponseStatusError {
221
- constructor(location, issues, vendor) {
222
- super(400, ParserError.formatMessage(location, issues, vendor));
227
+ constructor(location, issues, vendor, functionName) {
228
+ super(400, ParserError.formatMessage(location, issues, vendor, functionName));
223
229
  Object.setPrototypeOf(this, new.target.prototype);
224
230
  }
225
- static formatMessage(location, issues, vendor) {
231
+ static formatMessage(location, issues, vendor, functionName) {
226
232
  const formatted = issues.map((i) => {
227
233
  const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
228
234
  return path ? `${path}: ${i.message}` : i.message;
229
235
  }).join("; ");
230
- return `${vendor} failed to parse ${(() => {
231
- switch (location) {
232
- case "reqbody": return "request body";
233
- case "reqparams": return "request params";
234
- case "reqquery": return "request query";
235
- }
236
- })()}: ${formatted}`;
237
- }
238
- };
239
- //#endregion
240
- //#region src/helper/sapling.ts
241
- const settings = {
242
- serialize: JSON.stringify,
243
- deserialize: JSON.parse
244
- };
245
- /**
246
- * Collection of utility functions which are essential for Sapling to function.
247
- */
248
- var Sapling = class Sapling {
249
- /**
250
- * If you would prefer to manually resolve your controllers instead, call resolve
251
- * on the controller class.
252
- *
253
- * @example```ts
254
- * import { Sapling } from "@tahminator/sapling";
255
- * import TestController from "./path/to/test.controller";
256
- *
257
- * const app = express();
258
- *
259
- * const router = Sapling.resolve(TestController);
260
- * app.use(router);
261
- * ```
262
- */
263
- static resolve(clazz) {
264
- const router = _ControllerRegistry.get(clazz);
265
- if (!router) throw new Error("Controller cannot be found");
266
- return router;
267
- }
268
- /**
269
- * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
270
- *
271
- * @example```ts
272
- * import { Sapling } from "@tahminator/sapling";
273
- * import express from "express";
274
- *
275
- * const app = express();
276
- *
277
- * app.use(Sapling.json());
278
- * ```
279
- */
280
- static json() {
281
- return (request, _response, next) => {
282
- try {
283
- if (!request.body) return next();
284
- if (request.headers["content-type"] !== "application/json") return next();
285
- if (typeof request.body === "string") request.body = Sapling.deserialize(request.body);
286
- else if (typeof request.body === "object") {
287
- const raw = JSON.stringify(request.body);
288
- request.body = Sapling.deserialize(raw);
289
- }
290
- next();
291
- } catch (err) {
292
- next(err);
293
- }
294
- };
236
+ return `Failed to parse ${this.getPrettyLocationString(location)} with ${vendor} on ${functionName}: ${formatted}`;
295
237
  }
296
- /**
297
- * Register your application with all the necessary middlewares and logics for Sapling to function.
298
- *
299
- * @example```ts
300
- * import { Sapling } from "@tahminator/sapling";
301
- * import express from "express";
302
- *
303
- * const app = express();
304
- *
305
- * app.registerApp(app);
306
- * ```
307
- */
308
- static registerApp(app) {
309
- app.use(e.text({ type: "application/json" }));
310
- app.use(Sapling.json());
311
- }
312
- /**
313
- * Serialize a value into a JSON string.
314
- *
315
- * This function is used in {@link ResponseEntity} to serialize the `body`.
316
- *
317
- * Use `setSerializeFn` to override underlying implementation.
318
- *
319
- * @defaultValue `JSON.stringify`
320
- */
321
- static serialize(value) {
322
- return settings.serialize(value);
323
- }
324
- /**
325
- * Replace the function used for `serialize`.
326
- */
327
- static setSerializeFn(fn) {
328
- settings.serialize = fn;
329
- }
330
- /**
331
- * De-serialize a JSON string back to a JavaScript object.
332
- *
333
- * This function is used to de-serialize a string into a `body`.
334
- *
335
- * Use `setDeserializeFn` to override underlying implementation.
336
- *
337
- * @defaultValue `JSON.parse`
338
- */
339
- static deserialize(value) {
340
- return settings.deserialize(value);
341
- }
342
- /**
343
- * Replace the function used for `deserialize`
344
- */
345
- static setDeserializeFn(fn) {
346
- settings.deserialize = fn;
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
+ }
347
245
  }
348
246
  };
349
247
  //#endregion
350
- //#region src/types.ts
351
- const methodResolve = {
352
- GET: "get",
353
- PUT: "put",
354
- POST: "post",
355
- DELETE: "delete",
356
- OPTIONS: "options",
357
- PATCH: "patch",
358
- HEAD: "head",
359
- USE: "use"
360
- };
361
- //#endregion
362
248
  //#region lib/weakmap.ts
363
249
  /**
364
250
  * WeakMap that is iterable.
@@ -479,132 +365,269 @@ function _resolve(ctor) {
479
365
  return _InjectableRegistry.get(ctor);
480
366
  }
481
367
  //#endregion
482
- //#region src/annotation/request.ts
483
- const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
484
- /**
485
- * Apply to a route method to have `request.body` be parsed by `schema`.
486
- *
487
- * This annotation will parse `request.body` & then override `request.body`.
488
- * You can then just simply cast `request.body` for your use
489
- *
490
- * @example
491
- * ```ts
492
- * const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
493
- * name: z.string(),
494
- * description: z.string().optional(),
495
- * });
496
- *
497
- * ⠀@Controller({ prefix: "/api/book" })
498
- * class BookController {
499
- * ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
500
- * ⠀@POST()
501
- * public createBook(request: e.Request) {
502
- * const { name, description } = request.body as unknown as z.infer<
503
- * typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
504
- * >;
505
- * }
506
- * }
507
- * ```
508
- */
509
- function RequestBody(schema) {
510
- return (target, propertyKey) => {
511
- const ctor = target.constructor;
512
- const fnName = String(propertyKey);
513
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
514
- };
515
- }
516
- /**
517
- * Apply to a route method to have `request.param` be parsed by `schema`.
518
- *
519
- * This annotation will parse `request.param` & then override `request.param`.
520
- * You can then just simply cast `request.param` for your use
521
- *
522
- * @example
523
- * ```ts
524
- * const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
525
- * bookId: z.string(),
526
- * });
527
- *
528
- * ⠀@Controller({ prefix: "/api/book" })
529
- * class BookController {
530
- * ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
531
- * ⠀@GET("/:bookId")
532
- * public getBook(request: e.Request) {
533
- * const { bookId } = request.param as unknown as z.infer<
534
- * typeof GET_BOOK_REQUEST_PARAM_SCHEMA
535
- * >;
536
- * }
537
- * }
538
- * ```
539
- */
540
- function RequestParam(schema) {
541
- return (target, propertyKey) => {
542
- const ctor = target.constructor;
543
- const fnName = String(propertyKey);
544
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
545
- };
368
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
369
+ function __decorate(decorators, target, key, desc) {
370
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
371
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
372
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
373
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
546
374
  }
375
+ //#endregion
376
+ //#region src/middleware/health/registrar.ts
377
+ let HealthRegistrar = class HealthRegistrar {
378
+ _checks;
379
+ _ready;
380
+ constructor() {
381
+ this._checks = [];
382
+ this._ready = false;
383
+ }
384
+ /**
385
+ * Add a health check.
386
+ *
387
+ * Health checks will be used to determine whether service can service traffic or not (a.k.a `liveness`).
388
+ */
389
+ add(healthCheck) {
390
+ this._checks.push(healthCheck);
391
+ }
392
+ /**
393
+ * @internal used by Sapling library, used to determine once all
394
+ * checks have been registered and server is, at the very least, alive.
395
+ */
396
+ _markReady() {
397
+ this._ready = true;
398
+ }
399
+ async check() {
400
+ if (!this._ready) return false;
401
+ return (await Promise.all(this._checks.map((c) => c()))).every((c) => c === true);
402
+ }
403
+ };
404
+ HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
405
+ //#endregion
406
+ //#region src/helper/sapling.ts
407
+ const _settings = {
408
+ serialize: JSON.stringify,
409
+ deserialize: JSON.parse,
410
+ health: {
411
+ ready: { path: "/up" },
412
+ live: { path: "/live" }
413
+ },
414
+ doc: {
415
+ openApiPath: "/openapi.json",
416
+ swaggerPath: "/swagger.html",
417
+ metadata: {
418
+ title: "API",
419
+ version: "1.0.0"
420
+ }
421
+ }
422
+ };
547
423
  /**
548
- * Apply to a route method to have `request.query` be parsed by `schema`.
549
- *
550
- * This annotation will parse `request.query` & then override `request.query`.
551
- * You can then just simply cast `request.query` for your use
552
- *
553
- * @example
554
- * ```ts
555
- * const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
556
- * sort: z.enum(["name", "createdAt"]).optional(),
557
- * q: z.string().optional(),
558
- * });
559
- *
560
- * ⠀@Controller({ prefix: "/api/book" })
561
- * class BookController {
562
- * ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
563
- * ⠀@GET()
564
- * public listBooks(request: e.Request) {
565
- * const { sort, q } = request.query as unknown as z.infer<
566
- * typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
567
- * >;
568
- * }
569
- * }
570
- * ```
424
+ * Collection of utility functions which are essential for Sapling to function.
571
425
  */
572
- function RequestQuery(schema) {
573
- return (target, propertyKey) => {
574
- const ctor = target.constructor;
575
- const fnName = String(propertyKey);
576
- _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
426
+ var Sapling = class Sapling {
427
+ /**
428
+ * If you would prefer to manually resolve your controllers instead, call resolve
429
+ * on the controller class.
430
+ *
431
+ * @example```ts
432
+ * import { Sapling } from "@tahminator/sapling";
433
+ * import TestController from "./path/to/test.controller";
434
+ *
435
+ * const app = express();
436
+ *
437
+ * const router = Sapling.resolve(TestController);
438
+ * app.use(router);
439
+ * ```
440
+ */
441
+ static resolve(clazz) {
442
+ const router = _ControllerRegistry.get(clazz);
443
+ if (!router) throw new Error("Controller cannot be found");
444
+ return router;
445
+ }
446
+ /**
447
+ * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
448
+ *
449
+ * @example```ts
450
+ * import { Sapling } from "@tahminator/sapling";
451
+ * import express from "express";
452
+ *
453
+ * const app = express();
454
+ *
455
+ * app.use(Sapling.json());
456
+ * ```
457
+ */
458
+ static json() {
459
+ return (request, _response, next) => {
460
+ try {
461
+ if (!request.body) return next();
462
+ if (request.headers["content-type"] !== "application/json") return next();
463
+ if (typeof request.body === "string") request.body = Sapling.deserialize(request.body);
464
+ else if (typeof request.body === "object") {
465
+ const raw = JSON.stringify(request.body);
466
+ request.body = Sapling.deserialize(raw);
467
+ }
468
+ next();
469
+ } catch (err) {
470
+ next(err);
471
+ }
472
+ };
473
+ }
474
+ /**
475
+ * Register your application with all the necessary middlewares and logics for Sapling to function.
476
+ *
477
+ * @example```ts
478
+ * import { Sapling } from "@tahminator/sapling";
479
+ * import express from "express";
480
+ *
481
+ * // returns the exact same `express.App` type back to you!
482
+ * const app = Sapling.registerApp(express());
483
+ * ```
484
+ */
485
+ static registerApp(app) {
486
+ app.use(e.text({ type: "application/json" }));
487
+ app.use(Sapling.json());
488
+ return new Proxy(app, { get(target, prop, receiver) {
489
+ if (prop === "listen") {
490
+ const originalListen = target[prop];
491
+ return function(...args) {
492
+ const server = originalListen.apply(target, args);
493
+ server.once("listening", () => {
494
+ Sapling.onPostStartup();
495
+ console.log("Sapling successfully initialized post-startup hooks on server start");
496
+ });
497
+ return server;
498
+ };
499
+ }
500
+ return Reflect.get(target, prop, receiver);
501
+ } });
502
+ }
503
+ static onPostStartup() {
504
+ _InjectableRegistry.get(HealthRegistrar)?._markReady();
505
+ }
506
+ /**
507
+ * Serialize a value into a JSON string.
508
+ *
509
+ * This function is used in {@link ResponseEntity} to serialize the `body`.
510
+ *
511
+ * Use `setSerializeFn` to override underlying implementation.
512
+ *
513
+ * @defaultValue `JSON.stringify`
514
+ */
515
+ static serialize(value) {
516
+ return _settings.serialize(value);
517
+ }
518
+ /**
519
+ * Replace the function used for `serialize`.
520
+ */
521
+ static setSerializeFn(fn) {
522
+ _settings.serialize = fn;
523
+ }
524
+ /**
525
+ * De-serialize a JSON string back to a JavaScript object.
526
+ *
527
+ * This function is used to de-serialize a string into a `body`.
528
+ *
529
+ * Use `setDeserializeFn` to override underlying implementation.
530
+ *
531
+ * @defaultValue `JSON.parse`
532
+ */
533
+ static deserialize(value) {
534
+ return _settings.deserialize(value);
535
+ }
536
+ /**
537
+ * Replace the function used for `deserialize`
538
+ */
539
+ static setDeserializeFn(fn) {
540
+ _settings.deserialize = fn;
541
+ }
542
+ /**
543
+ * Modify extra settings
544
+ */
545
+ static Extras = {
546
+ /**
547
+ * Modify default settings applied to OpenAPI & Swagger
548
+ */
549
+ swaggerAndOpenApi: {
550
+ /**
551
+ * Set base OpenAPI metadata values.
552
+ *
553
+ * @default { title: "API", version: "1.0.0" }
554
+ */
555
+ setMetadata(metadata) {
556
+ _settings.doc.metadata = metadata;
557
+ },
558
+ /**
559
+ * change default endpoint that will serve OpenAPI spec.
560
+ * Swagger will also load this endpoint on load.
561
+ *
562
+ * @default `/openapi.json`
563
+ */
564
+ setOpenApiPath(path) {
565
+ _settings.doc.openApiPath = path;
566
+ },
567
+ /**
568
+ * change Swagger endpoint.
569
+ *
570
+ * @default `/swagger.html`
571
+ */
572
+ setSwaggerPath(path) {
573
+ _settings.doc.swaggerPath = path;
574
+ }
575
+ },
576
+ /**
577
+ * Modify default settings applied to health / readiness / liveness.
578
+ */
579
+ health: {
580
+ /**
581
+ * change default endpoint that ready endpoint will be served on.
582
+ *
583
+ * @default `/up`
584
+ */
585
+ setReadyPath(path) {
586
+ _settings.health.ready.path = path;
587
+ },
588
+ /**
589
+ * change default endpoint that live endpoint will be served on.
590
+ *
591
+ * @default `/live`
592
+ */
593
+ setLivePath(path) {
594
+ _settings.health.live.path = path;
595
+ }
596
+ }
577
597
  };
578
- }
579
- function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
580
- const byFn = (() => {
581
- const fn = _requestSchemaStore.get(ctor);
582
- if (fn) return fn;
583
- const newFn = /* @__PURE__ */ new Map();
584
- _requestSchemaStore.set(ctor, newFn);
585
- return newFn;
586
- })();
587
- const existing = byFn.get(fnName);
588
- if (existing) return existing;
589
- const created = {};
590
- byFn.set(fnName, created);
591
- return created;
592
- }
593
- function _setOnce(def, key, schema, fnName) {
594
- if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
595
- def[key] = schema;
596
- }
597
- function _getRequestSchemas(ctor, fnName) {
598
- return _requestSchemaStore.get(ctor)?.get(fnName);
599
- }
600
- async function _parseOrThrow(schema, input, kind) {
601
- const result = await schema["~standard"].validate(input);
602
- if (result.issues) {
603
- console.debug(`Failed to parse a schema`);
604
- throw new ParserError(kind, result.issues, schema["~standard"].vendor);
598
+ /**
599
+ * This method can be used in a `@MiddlewareClass` to register any libraries
600
+ * that expect you to register multiple registers at once. An example is `swagger-ui-express`
601
+ *
602
+ * @example
603
+ * ```ts
604
+ * ⠀@MiddlewareClass()
605
+ * class Serve {
606
+ * // `swagger.serve` returns multiple Express handlers for all the assets and routes
607
+ * // that will be served
608
+ * private readonly handlers: RequestHandler[] = swagger.serve;
609
+ *
610
+ * ⠀@Middleware(_settings.doc.swaggerPath)
611
+ * handle(request: Request, response: Response, next: NextFunction) {
612
+ * return Sapling.chainHandlers(this.handlers, request, response, next);
613
+ * }
614
+ * }
615
+ * ```
616
+ */
617
+ static chainHandlers(handlers, request, response, next, index = 0) {
618
+ if (index >= handlers.length) {
619
+ next();
620
+ return;
621
+ }
622
+ handlers[index]?.(request, response, (err) => {
623
+ if (err) {
624
+ next(err);
625
+ return;
626
+ }
627
+ Sapling.chainHandlers(handlers, request, response, next, index + 1);
628
+ });
605
629
  }
606
- return result.value;
607
- }
630
+ };
608
631
  //#endregion
609
632
  //#region src/annotation/route.ts
610
633
  const _routeStore = /* @__PURE__ */ new WeakMap();
@@ -687,6 +710,245 @@ function _getRoutes(ctor) {
687
710
  return _routeStore.get(ctor) ?? [];
688
711
  }
689
712
  //#endregion
713
+ //#region src/annotation/schema.ts
714
+ function ControllerSchema(options) {
715
+ return (target) => {
716
+ _setControllerSchema(target, options);
717
+ };
718
+ }
719
+ function RouteSchema(options) {
720
+ return (target, propertyKey) => {
721
+ const ctor = target.constructor;
722
+ _setRouteSchema(ctor, String(propertyKey), options);
723
+ };
724
+ }
725
+ const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
726
+ const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
727
+ function getOrCreateRouteSchemaStore(store, ctor) {
728
+ const existing = store.get(ctor);
729
+ if (existing) return existing;
730
+ const created = /* @__PURE__ */ new Map();
731
+ store.set(ctor, created);
732
+ return created;
733
+ }
734
+ function _setRouteSchema(ctor, fnName, options) {
735
+ getOrCreateRouteSchemaStore(_routeSchemaStore, ctor).set(fnName, options);
736
+ }
737
+ function _setControllerSchema(ctor, options) {
738
+ _controllerSchemaStore.set(ctor, options);
739
+ }
740
+ function _getRouteSchema(ctor, fnName) {
741
+ return _routeSchemaStore.get(ctor)?.get(fnName);
742
+ }
743
+ function _getControllerSchema(ctor) {
744
+ return _controllerSchemaStore.get(ctor);
745
+ }
746
+ //#endregion
747
+ //#region src/annotation/validator.ts
748
+ const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
749
+ function ResponseBody(schema) {
750
+ return (target, propertyKey) => {
751
+ const ctor = target.constructor;
752
+ const fnName = String(propertyKey);
753
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
754
+ };
755
+ }
756
+ function RequestBody(schema) {
757
+ return (target, propertyKey) => {
758
+ const ctor = target.constructor;
759
+ const fnName = String(propertyKey);
760
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
761
+ };
762
+ }
763
+ function RequestParam(schema) {
764
+ return (target, propertyKey) => {
765
+ const ctor = target.constructor;
766
+ const fnName = String(propertyKey);
767
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
768
+ };
769
+ }
770
+ function RequestQuery(schema) {
771
+ return (target, propertyKey) => {
772
+ const ctor = target.constructor;
773
+ const fnName = String(propertyKey);
774
+ _saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
775
+ };
776
+ }
777
+ function getOrCreateValidatorSchemaStore(store, ctor) {
778
+ const existing = store.get(ctor);
779
+ if (existing) return existing;
780
+ const created = /* @__PURE__ */ new Map();
781
+ store.set(ctor, created);
782
+ return created;
783
+ }
784
+ function _getOrCreateSchemaDefinition(ctor, fnName) {
785
+ const byFn = getOrCreateValidatorSchemaStore(_validatorSchemaStore, ctor);
786
+ const existing = byFn.get(fnName);
787
+ if (existing) return existing;
788
+ const created = {};
789
+ byFn.set(fnName, created);
790
+ return created;
791
+ }
792
+ async function _parseOrThrow(schema, input, location, fnName) {
793
+ const result = await schema["~standard"].validate(input);
794
+ if (result.issues) throw new ParserError(location, result.issues, schema["~standard"].vendor, fnName);
795
+ return result.value;
796
+ }
797
+ function _saveValidatorSchema(def, key, schema, fnName) {
798
+ if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
799
+ def[key] = schema;
800
+ }
801
+ function _getValidatorSchema(ctor, fnName) {
802
+ return _validatorSchemaStore.get(ctor)?.get(fnName);
803
+ }
804
+ //#endregion
805
+ //#region src/helper/openapi.ts
806
+ var OpenAPIGenerator = class {
807
+ OPENAPI_VERSION = "3.0.0";
808
+ controllers = /* @__PURE__ */ new Set();
809
+ registerController(controllerClass, prefix) {
810
+ this.controllers.add({
811
+ class: controllerClass,
812
+ prefix
813
+ });
814
+ }
815
+ /**
816
+ * visible for testing
817
+ */
818
+ _clearControllers() {
819
+ this.controllers.clear();
820
+ }
821
+ get metadata() {
822
+ return _settings.doc.metadata;
823
+ }
824
+ generateSpec() {
825
+ const metadata = this.metadata;
826
+ const paths = {};
827
+ const tags = [];
828
+ for (const { class: controllerClass, prefix } of this.controllers) {
829
+ const routes = _getRoutes(controllerClass);
830
+ const controllerSchema = _getControllerSchema(controllerClass);
831
+ if (controllerSchema?.title) tags.push({
832
+ name: controllerSchema.title,
833
+ description: controllerSchema.description
834
+ });
835
+ for (const route of routes) {
836
+ if (route.method === "USE") continue;
837
+ const schemas = _getValidatorSchema(controllerClass, route.fnName);
838
+ const routeSchema = _getRouteSchema(controllerClass, route.fnName);
839
+ 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.`);
840
+ const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
841
+ if (!paths[openApiPath]) paths[openApiPath] = {};
842
+ const responses = {};
843
+ if (schemas?.responseBody) {
844
+ const responseSchema = this.toJsonSchema(schemas.responseBody, "output");
845
+ responses["200"] = {
846
+ description: responseSchema.description ?? "Successful response",
847
+ content: { "application/json": { schema: responseSchema } }
848
+ };
849
+ } else responses["200"] = { description: "Successful response" };
850
+ if (routeSchema?.responses) for (const resp of routeSchema.responses) {
851
+ const statusCode = String(resp.statusCode);
852
+ const existingResponse = responses[statusCode];
853
+ const existingSchema = existingResponse && "content" in existingResponse ? existingResponse.content?.["application/json"]?.schema : void 0;
854
+ const responseSchema = resp.schema ? this.toJsonSchema(resp.schema, "output") : statusCode === "200" ? existingSchema : void 0;
855
+ responses[statusCode] = {
856
+ ...existingResponse,
857
+ description: resp.description ?? responseSchema?.description ?? existingResponse?.description ?? `Response ${resp.statusCode}`,
858
+ ...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
859
+ };
860
+ }
861
+ const operation = {
862
+ responses,
863
+ summary: routeSchema?.summary,
864
+ description: routeSchema?.description,
865
+ tags: controllerSchema?.title ? [controllerSchema.title] : void 0
866
+ };
867
+ const parameters = [];
868
+ if (schemas?.requestParam) {
869
+ const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
870
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
871
+ const parameterSchema = schema;
872
+ parameters.push({
873
+ name,
874
+ in: "path",
875
+ required: true,
876
+ description: parameterSchema.description,
877
+ schema: parameterSchema
878
+ });
879
+ }
880
+ }
881
+ if (schemas?.requestQuery) {
882
+ const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
883
+ if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
884
+ const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
885
+ const parameterSchema = schema;
886
+ parameters.push({
887
+ name,
888
+ in: "query",
889
+ required: isRequired,
890
+ description: parameterSchema.description,
891
+ schema: parameterSchema
892
+ });
893
+ }
894
+ }
895
+ if (parameters.length > 0) operation.parameters = parameters;
896
+ if (schemas?.requestBody) {
897
+ const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
898
+ operation.requestBody = {
899
+ required: true,
900
+ description: requestSchema.description,
901
+ content: { "application/json": { schema: requestSchema } }
902
+ };
903
+ }
904
+ const method = route.method.toLowerCase();
905
+ paths[openApiPath][method] = operation;
906
+ }
907
+ }
908
+ return {
909
+ openapi: this.OPENAPI_VERSION,
910
+ info: {
911
+ title: metadata.title,
912
+ version: metadata.version,
913
+ description: metadata.description
914
+ },
915
+ tags: tags.length > 0 ? tags : void 0,
916
+ paths
917
+ };
918
+ }
919
+ toJsonSchema(schema, direction = "output") {
920
+ try {
921
+ const jsonSchema = schema["~standard"].jsonSchema;
922
+ return direction === "input" ? jsonSchema.input({ target: "openapi-3.0" }) : jsonSchema.output({ target: "openapi-3.0" });
923
+ } catch (e) {
924
+ 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 });
925
+ throw e;
926
+ }
927
+ }
928
+ };
929
+ const openApiGenerator = new OpenAPIGenerator();
930
+ function _registerController(controllerClass, prefix) {
931
+ openApiGenerator.registerController(controllerClass, prefix);
932
+ }
933
+ function generateOpenApiSpec() {
934
+ return openApiGenerator.generateSpec();
935
+ }
936
+ function _clearOpenApiRegistry() {
937
+ openApiGenerator._clearControllers();
938
+ }
939
+ //#endregion
940
+ //#region src/types.ts
941
+ const methodResolve = {
942
+ GET: "get",
943
+ PUT: "put",
944
+ POST: "post",
945
+ DELETE: "delete",
946
+ OPTIONS: "options",
947
+ PATCH: "patch",
948
+ HEAD: "head",
949
+ USE: "use"
950
+ };
951
+ //#endregion
690
952
  //#region src/annotation/controller.ts
691
953
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
692
954
  /**
@@ -698,6 +960,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
698
960
  function Controller({ prefix = "", deps = [] } = {}) {
699
961
  return (target) => {
700
962
  const targetClass = target;
963
+ _registerController(target, prefix);
701
964
  const router = Router();
702
965
  const routes = _getRoutes(target);
703
966
  const usedRoutes = /* @__PURE__ */ new Set();
@@ -722,15 +985,20 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
722
985
  if (method === "USE" && fn.length >= 4) {
723
986
  const middlewareFn = async (err, request, response, next) => {
724
987
  try {
725
- const result = fn.bind(controllerInstance)(err, request, response, next);
726
- if (result instanceof ResponseEntity) {
727
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
728
- return;
729
- }
730
- if (result instanceof RedirectView) {
731
- response.redirect(result.getUrl());
732
- return;
733
- }
988
+ await validate({
989
+ target,
990
+ fnName,
991
+ request
992
+ });
993
+ await handleResult({
994
+ result: fn.bind(controllerInstance)(err, request, response, next),
995
+ response,
996
+ target,
997
+ fnName,
998
+ method,
999
+ path: path instanceof RegExp ? path.source : fp,
1000
+ isErrorMiddleware: true
1001
+ });
734
1002
  } catch (e) {
735
1003
  console.error(e);
736
1004
  next(e);
@@ -740,34 +1008,52 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
740
1008
  return;
741
1009
  }
742
1010
  router[methodName](fp, async (request, response, next) => {
743
- const schemas = _getRequestSchemas(target, fnName);
744
- if (schemas) {
745
- if (schemas.body) request.body = await _parseOrThrow(schemas.body, request.body, "reqbody");
746
- if (schemas.param) request.params = await _parseOrThrow(schemas.param, request.params, "reqparams");
747
- if (schemas.query) {
748
- const parsedQuery = await _parseOrThrow(schemas.query, request.query, "reqquery");
749
- Object.defineProperty(request, "query", {
750
- value: parsedQuery,
751
- writable: true,
752
- configurable: true
753
- });
754
- }
755
- }
756
- const result = await fn.bind(controllerInstance)(request, response, next);
757
- if (result instanceof ResponseEntity) {
758
- response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
759
- return;
760
- }
761
- if (result instanceof RedirectView) {
762
- response.redirect(result.getUrl());
763
- return;
764
- }
765
- if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
1011
+ await validate({
1012
+ target,
1013
+ fnName,
1014
+ request
1015
+ });
1016
+ await handleResult({
1017
+ result: await fn.bind(controllerInstance)(request, response, next),
1018
+ response,
1019
+ target,
1020
+ fnName,
1021
+ method,
1022
+ path: path instanceof RegExp ? path.source : fp
1023
+ });
766
1024
  });
767
1025
  }
768
1026
  _ControllerRegistry.set(targetClass, router);
769
1027
  };
770
1028
  }
1029
+ async function handleResult({ result, target, fnName, response, method, path, isErrorMiddleware = false }) {
1030
+ const schemas = _getValidatorSchema(target, fnName);
1031
+ if (result instanceof ResponseEntity) {
1032
+ const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody", fnName) : result.getBody();
1033
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
1034
+ return;
1035
+ }
1036
+ if (result instanceof RedirectView) {
1037
+ response.redirect(result.getUrl());
1038
+ return;
1039
+ }
1040
+ if (!isErrorMiddleware && method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${method} ${path}`));
1041
+ }
1042
+ async function validate({ target, fnName, request }) {
1043
+ const schemas = _getValidatorSchema(target, fnName);
1044
+ if (schemas) {
1045
+ if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody", fnName);
1046
+ if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams", fnName);
1047
+ if (schemas.requestQuery) {
1048
+ const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery", fnName);
1049
+ Object.defineProperty(request, "query", {
1050
+ value: parsedQuery,
1051
+ writable: true,
1052
+ configurable: true
1053
+ });
1054
+ }
1055
+ }
1056
+ }
771
1057
  //#endregion
772
1058
  //#region src/annotation/middleware.ts
773
1059
  /**
@@ -781,15 +1067,7 @@ function MiddlewareClass(...args) {
781
1067
  return Controller(...args);
782
1068
  }
783
1069
  //#endregion
784
- //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
785
- function __decorate(decorators, target, key, desc) {
786
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
787
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
788
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
789
- return c > 3 && r && Object.defineProperty(target, key, r), r;
790
- }
791
- //#endregion
792
- //#region src/middleware/default/base.ts
1070
+ //#region src/middleware/default/error/base.ts
793
1071
  let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
794
1072
  handle(err, _request, _response, _next) {
795
1073
  console.error("[Error]", err);
@@ -799,7 +1077,20 @@ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
799
1077
  __decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
800
1078
  DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
801
1079
  //#endregion
802
- //#region src/middleware/default/responsestatus.ts
1080
+ //#region src/middleware/default/error/parse.ts
1081
+ let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
1082
+ handle(err, _request, _response, next) {
1083
+ if (err instanceof ParserError) {
1084
+ console.warn(err);
1085
+ return ResponseEntity.status(err.status).body({ message: err.message });
1086
+ }
1087
+ next(err);
1088
+ }
1089
+ };
1090
+ __decorate([Middleware()], DefaultParserErrorMiddleware.prototype, "handle", null);
1091
+ DefaultParserErrorMiddleware = __decorate([MiddlewareClass()], DefaultParserErrorMiddleware);
1092
+ //#endregion
1093
+ //#region src/middleware/default/error/responsestatus.ts
803
1094
  let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
804
1095
  handle(err, _request, _response, next) {
805
1096
  if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
@@ -809,4 +1100,104 @@ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddl
809
1100
  __decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
810
1101
  DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
811
1102
  //#endregion
812
- export { Controller, DELETE, DefaultBaseErrorMiddleware, DefaultResponseStatusErrorMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getRequestSchemas, _getRoutes, _parseOrThrow, _resolve, methodResolve };
1103
+ //#region src/middleware/default/openapi/index.ts
1104
+ let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
1105
+ handle(_request, _response, _next) {
1106
+ return ResponseEntity.ok().body(generateOpenApiSpec());
1107
+ }
1108
+ };
1109
+ __decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
1110
+ DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
1111
+ //#endregion
1112
+ //#region src/middleware/default/swagger/index.ts
1113
+ /**
1114
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1115
+ *
1116
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1117
+ *
1118
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1119
+ *
1120
+ * ```ts
1121
+ * const middlewares = [
1122
+ * DefaultOpenApiMiddleware,
1123
+ * DefaultSwaggerMiddleware.Serve,
1124
+ * DefaultSwaggerMiddleware.Setup,
1125
+ * ];
1126
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1127
+ * ```
1128
+ */
1129
+ let Serve = class Serve {
1130
+ handlers = swagger.serve;
1131
+ handle(request, response, next) {
1132
+ return Sapling.chainHandlers(this.handlers, request, response, next);
1133
+ }
1134
+ };
1135
+ __decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
1136
+ Serve = __decorate([MiddlewareClass()], Serve);
1137
+ /**
1138
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1139
+ *
1140
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1141
+ *
1142
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1143
+ *
1144
+ * ```ts
1145
+ * const middlewares = [
1146
+ * DefaultOpenApiMiddleware,
1147
+ * DefaultSwaggerMiddleware.Serve,
1148
+ * DefaultSwaggerMiddleware.Setup,
1149
+ * ];
1150
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1151
+ * ```
1152
+ */
1153
+ let Setup = class Setup {
1154
+ handler;
1155
+ constructor() {
1156
+ this.handler = swagger.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
1157
+ }
1158
+ handle(request, response, next) {
1159
+ return this.handler(request, response, next);
1160
+ }
1161
+ };
1162
+ __decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
1163
+ Setup = __decorate([MiddlewareClass()], Setup);
1164
+ /**
1165
+ * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
1166
+ *
1167
+ * Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
1168
+ *
1169
+ * You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
1170
+ *
1171
+ * ```ts
1172
+ * const middlewares = [
1173
+ * DefaultOpenApiMiddleware,
1174
+ * DefaultSwaggerMiddleware.Serve,
1175
+ * DefaultSwaggerMiddleware.Setup,
1176
+ * ];
1177
+ * middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
1178
+ * ```
1179
+ */
1180
+ const DefaultSwaggerMiddleware = {
1181
+ Serve,
1182
+ Setup
1183
+ };
1184
+ //#endregion
1185
+ //#region src/middleware/default/health/index.ts
1186
+ let DefaultHealthMiddleware = class DefaultHealthMiddleware {
1187
+ constructor(healthRegistrar) {
1188
+ this.healthRegistrar = healthRegistrar;
1189
+ }
1190
+ async readiness(_request, _response, _next) {
1191
+ const up = await this.healthRegistrar.check();
1192
+ return ResponseEntity.ok().body({ up });
1193
+ }
1194
+ async liveness(_request, _response, _next) {
1195
+ const up = await this.healthRegistrar.check();
1196
+ return ResponseEntity.ok().body({ up });
1197
+ }
1198
+ };
1199
+ __decorate([GET(_settings.health.ready.path)], DefaultHealthMiddleware.prototype, "readiness", null);
1200
+ __decorate([GET(_settings.health.live.path)], DefaultHealthMiddleware.prototype, "liveness", null);
1201
+ DefaultHealthMiddleware = __decorate([MiddlewareClass({ deps: [HealthRegistrar] })], DefaultHealthMiddleware);
1202
+ //#endregion
1203
+ export { Controller, ControllerSchema, DELETE, DefaultBaseErrorMiddleware, DefaultHealthMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, HealthRegistrar, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteSchema, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _clearOpenApiRegistry, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerController, _resolve, _saveValidatorSchema, _setControllerSchema, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator };