@tahminator/sapling 2.1.0-beta.e797371f → 2.1.1

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/README.md CHANGED
@@ -26,6 +26,10 @@ A lightweight Express.js dependency injection & route abstraction library.
26
26
  + [Automatic Spec Generation](#automatic-spec-generation)
27
27
  + [Adding Documentation with Decorators](#adding-documentation-with-decorators)
28
28
  + [Customizing Paths](#customizing-paths)
29
+ * [Health Checks](#health-checks)
30
+ + [Liveness and Readiness Probes](#liveness-and-readiness-probes)
31
+ + [Registering Custom Readiness Checks](#registering-custom-readiness-checks)
32
+ + [Customizing Health Paths](#customizing-health-paths)
29
33
  * [Custom Serialization](#custom-serialization)
30
34
  - [Advanced Setup](#advanced-setup)
31
35
  * [Automatically import controllers](#automatically-import-controllers)
@@ -97,9 +101,10 @@ class UserController {
97
101
  }
98
102
  }
99
103
 
100
- // you still have full access of app to do whatever you want!
101
- const app = express();
102
- Sapling.registerApp(app);
104
+ // registerApp returns the app back to you - use the returned app instance
105
+ // so Sapling can inject some post-startup hooks into the app.
106
+ // BUT, you still have full access of app to do whatever you want!
107
+ const app = Sapling.registerApp(express());
103
108
 
104
109
  // @MiddlewareClass should be registered first before @Controller and should be registered in order
105
110
  // @Injectable classes will automatically be formed into singletons by Sapling behind the scenes!
@@ -581,6 +586,57 @@ Sapling.Extras.swaggerAndOpenApi.setOpenApiPath("/api-spec.json");
581
586
  Sapling.Extras.swaggerAndOpenApi.setSwaggerPath("/api-docs");
582
587
  ```
583
588
 
589
+ ### Health Checks
590
+
591
+ #### Liveness and Readiness Probes
592
+
593
+ Register `DefaultHealthMiddleware` to expose two health endpoints:
594
+
595
+ - `GET /live` — **liveness probe**: returns `{ up: true }` once the server has started listening. Use this to tell your orchestrator the process is alive.
596
+ - `GET /ready` — **readiness probe**: returns `{ up: true }` once the server is live *and* all registered readiness checks pass. Use this to gate traffic until your app is fully initialized.
597
+
598
+ ```typescript
599
+ import { Sapling, DefaultHealthMiddleware } from "@tahminator/sapling";
600
+
601
+ const app = Sapling.registerApp(express());
602
+
603
+ app.use(Sapling.resolve(DefaultHealthMiddleware));
604
+
605
+ app.listen(3000); // _markLive() is called automatically once the server is listening
606
+ ```
607
+
608
+ > [!IMPORTANT]
609
+ > `Sapling.registerApp` returns `express.App` - please use that as your `app` object inside of your server. There is a small post-startup hook applied via a proxy, in which the liveness probe is enabled from such.
610
+
611
+ #### Registering Custom Readiness Checks
612
+
613
+ Inject `HealthRegistrar` into any `@Injectable` or controller to add readiness checks. A check is any function returning `boolean | Promise<boolean>`. If any check returns `false` (or throws), `/ready` reports `{ up: false }`.
614
+
615
+ ```typescript
616
+ import { Injectable, HealthRegistrar } from "@tahminator/sapling";
617
+
618
+ @Injectable([HealthRegistrar])
619
+ class DatabaseService {
620
+ constructor(
621
+ private readonly db: Database,
622
+ private readonly healthRegistrar: HealthRegistrar,
623
+ ) {
624
+ healthRegistrar.add(async () => {
625
+ return this.db.isConnected();
626
+ });
627
+ }
628
+ }
629
+ ```
630
+
631
+ #### Customizing Health Paths
632
+
633
+ The default paths are `/live` and `/ready`. Override them before registering `DefaultHealthMiddleware`:
634
+
635
+ ```typescript
636
+ Sapling.Extras.health.setLivePath("/healthz/live");
637
+ Sapling.Extras.health.setReadyPath("/healthz/ready");
638
+ ```
639
+
584
640
  ### Custom Serialization
585
641
 
586
642
  By default, Sapling uses `JSON.stringify` and `JSON.parse` for serialization. You can override these with custom serializers like [superjson](https://github.com/flightcontrolhq/superjson#readme) to automatically handle Dates, BigInts, and more:
package/dist/index.cjs CHANGED
@@ -401,20 +401,38 @@ function __decorate(decorators, target, key, desc) {
401
401
  //#region src/middleware/health/registrar.ts
402
402
  let HealthRegistrar = class HealthRegistrar {
403
403
  _checks;
404
- _sealed;
404
+ _live;
405
405
  constructor() {
406
406
  this._checks = [];
407
- this._sealed = false;
407
+ this._live = false;
408
408
  }
409
+ /**
410
+ * Add a health check.
411
+ *
412
+ * Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
413
+ */
409
414
  add(healthCheck) {
410
415
  this._checks.push(healthCheck);
411
416
  }
412
- seal() {
413
- this._sealed = true;
417
+ /**
418
+ * @internal used by Sapling library, used to determine once all
419
+ * checks have been registered and server is, at the very least, alive.
420
+ */
421
+ _markLive() {
422
+ this._live = true;
423
+ }
424
+ /**
425
+ * @internal
426
+ */
427
+ async _liveness() {
428
+ return this._live;
414
429
  }
415
- async check() {
416
- if (!this._sealed) return false;
417
- return (await Promise.all(this._checks.map((c) => c()))).every((c) => c === true);
430
+ /**
431
+ * @internal
432
+ */
433
+ async _readiness() {
434
+ if (!this._live) return false;
435
+ return (await Promise.allSettled(this._checks.map((c) => c()))).every((r) => r.status === "fulfilled" && r.value);
418
436
  }
419
437
  };
420
438
  HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
@@ -423,7 +441,10 @@ HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
423
441
  const _settings = {
424
442
  serialize: JSON.stringify,
425
443
  deserialize: JSON.parse,
426
- health: { path: "/up" },
444
+ health: {
445
+ ready: { path: "/readyz" },
446
+ live: { path: "/livez" }
447
+ },
427
448
  doc: {
428
449
  openApiPath: "/openapi.json",
429
450
  swaggerPath: "/swagger.html",
@@ -491,9 +512,8 @@ var Sapling = class Sapling {
491
512
  * import { Sapling } from "@tahminator/sapling";
492
513
  * import express from "express";
493
514
  *
494
- * const app = express();
495
- *
496
- * app.registerApp(app);
515
+ * // returns the exact same `express.App` type back to you!
516
+ * const app = Sapling.registerApp(express());
497
517
  * ```
498
518
  */
499
519
  static registerApp(app) {
@@ -505,7 +525,7 @@ var Sapling = class Sapling {
505
525
  return function(...args) {
506
526
  const server = originalListen.apply(target, args);
507
527
  server.once("listening", () => {
508
- Sapling.onStartup();
528
+ Sapling._onPostStartup();
509
529
  console.log("Sapling successfully initialized post-startup hooks on server start");
510
530
  });
511
531
  return server;
@@ -514,8 +534,12 @@ var Sapling = class Sapling {
514
534
  return Reflect.get(target, prop, receiver);
515
535
  } });
516
536
  }
517
- static onStartup() {
518
- _InjectableRegistry.get(HealthRegistrar)?.seal();
537
+ /**
538
+ * @internal
539
+ * visible for testing
540
+ */
541
+ static _onPostStartup() {
542
+ _InjectableRegistry.get(HealthRegistrar)?._markLive();
519
543
  }
520
544
  /**
521
545
  * Serialize a value into a JSON string.
@@ -556,37 +580,59 @@ var Sapling = class Sapling {
556
580
  /**
557
581
  * Modify extra settings
558
582
  */
559
- static Extras = {
560
- /**
561
- * Modify default settings applied to OpenAPI & Swagger
562
- */
563
- swaggerAndOpenApi: {
583
+ static Extras = {
564
584
  /**
565
- * Set base OpenAPI metadata values.
566
- *
567
- * @default { title: "API", version: "1.0.0" }
585
+ * Modify default settings applied to OpenAPI & Swagger
568
586
  */
569
- setMetadata(metadata) {
570
- _settings.doc.metadata = metadata;
571
- },
572
- /**
573
- * change default endpoint that will serve OpenAPI spec.
574
- * Swagger will also load this endpoint on load.
575
- *
576
- * @default `/openapi.json`
577
- */
578
- setOpenApiPath(path) {
579
- _settings.doc.openApiPath = path;
587
+ swaggerAndOpenApi: {
588
+ /**
589
+ * Set base OpenAPI metadata values.
590
+ *
591
+ * @default { title: "API", version: "1.0.0" }
592
+ */
593
+ setMetadata(metadata) {
594
+ _settings.doc.metadata = metadata;
595
+ },
596
+ /**
597
+ * change default endpoint that will serve OpenAPI spec.
598
+ * Swagger will also load this endpoint on load.
599
+ *
600
+ * @default `/openapi.json`
601
+ */
602
+ setOpenApiPath(path) {
603
+ _settings.doc.openApiPath = path;
604
+ },
605
+ /**
606
+ * change Swagger endpoint.
607
+ *
608
+ * @default `/swagger.html`
609
+ */
610
+ setSwaggerPath(path) {
611
+ _settings.doc.swaggerPath = path;
612
+ }
580
613
  },
581
614
  /**
582
- * change Swagger endpoint.
583
- *
584
- * @default `/swagger.html`
615
+ * Modify default settings applied to health / readiness / liveness.
585
616
  */
586
- setSwaggerPath(path) {
587
- _settings.doc.swaggerPath = path;
617
+ health: {
618
+ /**
619
+ * change default endpoint that ready endpoint will be served on.
620
+ *
621
+ * @default `/readyz`
622
+ */
623
+ setReadyPath(path) {
624
+ _settings.health.ready.path = path;
625
+ },
626
+ /**
627
+ * change default endpoint that live endpoint will be served on.
628
+ *
629
+ * @default `/livez`
630
+ */
631
+ setLivePath(path) {
632
+ _settings.health.live.path = path;
633
+ }
588
634
  }
589
- } };
635
+ };
590
636
  /**
591
637
  * This method can be used in a `@MiddlewareClass` to register any libraries
592
638
  * that expect you to register multiple registers at once. An example is `swagger-ui-express`
@@ -1179,12 +1225,17 @@ let DefaultHealthMiddleware = class DefaultHealthMiddleware {
1179
1225
  constructor(healthRegistrar) {
1180
1226
  this.healthRegistrar = healthRegistrar;
1181
1227
  }
1182
- async serve(_request, _response, _next) {
1183
- const up = await this.healthRegistrar.check();
1228
+ async readiness(_request, _response, _next) {
1229
+ const up = await this.healthRegistrar._readiness();
1230
+ return ResponseEntity.ok().body({ up });
1231
+ }
1232
+ async liveness(_request, _response, _next) {
1233
+ const up = await this.healthRegistrar._liveness();
1184
1234
  return ResponseEntity.ok().body({ up });
1185
1235
  }
1186
1236
  };
1187
- __decorate([GET(_settings.health.path)], DefaultHealthMiddleware.prototype, "serve", null);
1237
+ __decorate([GET(_settings.health.ready.path)], DefaultHealthMiddleware.prototype, "readiness", null);
1238
+ __decorate([GET(_settings.health.live.path)], DefaultHealthMiddleware.prototype, "liveness", null);
1188
1239
  DefaultHealthMiddleware = __decorate([MiddlewareClass({ deps: [HealthRegistrar] })], DefaultHealthMiddleware);
1189
1240
  //#endregion
1190
1241
  exports.Controller = Controller;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import e, { ErrorRequestHandler, NextFunction, Request as Request$1, RequestHandler, Response as Response$1, Router } from "express";
1
+ import e, { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response as Response$1, Router } from "express";
2
2
 
3
3
  //#region src/html/404.d.ts
4
4
  /**
@@ -26,7 +26,7 @@ type RouteDefinition = {
26
26
  };
27
27
  type Class<T> = new (...args: any[]) => T;
28
28
  type HttpHeaders = Record<string, string>;
29
- type ExpressMiddlewareFn = ($1: Request$1, $2: Response$1, $3: NextFunction) => void;
29
+ type ExpressMiddlewareFn = ($1: Request, $2: Response$1, $3: NextFunction) => void;
30
30
  //#endregion
31
31
  //#region src/annotation/controller.d.ts
32
32
  declare const _ControllerRegistry: WeakMap<Function, Router | ErrorRequestHandler>;
@@ -795,7 +795,12 @@ type Settings = {
795
795
  serialize: (value: any) => string;
796
796
  deserialize: (value: string) => any;
797
797
  health: {
798
- path: string;
798
+ ready: {
799
+ path: string;
800
+ };
801
+ live: {
802
+ path: string;
803
+ };
799
804
  };
800
805
  doc: {
801
806
  openApiPath: string;
@@ -843,13 +848,16 @@ declare class Sapling {
843
848
  * import { Sapling } from "@tahminator/sapling";
844
849
  * import express from "express";
845
850
  *
846
- * const app = express();
847
- *
848
- * app.registerApp(app);
851
+ * // returns the exact same `express.App` type back to you!
852
+ * const app = Sapling.registerApp(express());
849
853
  * ```
850
854
  */
851
855
  static registerApp(app: e.Express): e.Express;
852
- static onStartup(): void;
856
+ /**
857
+ * @internal
858
+ * visible for testing
859
+ */
860
+ static _onPostStartup(): void;
853
861
  /**
854
862
  * Serialize a value into a JSON string.
855
863
  *
@@ -906,6 +914,23 @@ declare class Sapling {
906
914
  */
907
915
  setSwaggerPath(this: void, path: string): void;
908
916
  };
917
+ /**
918
+ * Modify default settings applied to health / readiness / liveness.
919
+ */
920
+ health: {
921
+ /**
922
+ * change default endpoint that ready endpoint will be served on.
923
+ *
924
+ * @default `/readyz`
925
+ */
926
+ setReadyPath(this: void, path: string): void;
927
+ /**
928
+ * change default endpoint that live endpoint will be served on.
929
+ *
930
+ * @default `/livez`
931
+ */
932
+ setLivePath(this: void, path: string): void;
933
+ };
909
934
  };
910
935
  /**
911
936
  * This method can be used in a `@MiddlewareClass` to register any libraries
@@ -926,7 +951,7 @@ declare class Sapling {
926
951
  * }
927
952
  * ```
928
953
  */
929
- static chainHandlers(this: void, handlers: RequestHandler[], request: Request$1, response: Response$1, next: NextFunction, index?: number): void;
954
+ static chainHandlers(this: void, handlers: RequestHandler[], request: Request, response: Response$1, next: NextFunction, index?: number): void;
930
955
  }
931
956
  //#endregion
932
957
  //#region src/annotation/validator.d.ts
@@ -982,7 +1007,7 @@ declare function _getControllerSchema(ctor: Function): ControllerSchemaDefinitio
982
1007
  * If the default is not suitable, you may also easily write your own.
983
1008
  */
984
1009
  declare class DefaultBaseErrorMiddleware {
985
- handle(err: unknown, _request: Request$1, _response: Response$1, _next: NextFunction): ResponseEntity<{
1010
+ handle(err: unknown, _request: Request, _response: Response$1, _next: NextFunction): ResponseEntity<{
986
1011
  message: string;
987
1012
  }>;
988
1013
  }
@@ -993,7 +1018,7 @@ declare class DefaultBaseErrorMiddleware {
993
1018
  * If the default is not suitable, you may also easily write your own.
994
1019
  */
995
1020
  declare class DefaultParserErrorMiddleware {
996
- handle(err: unknown, _request: Request$1, _response: Response$1, next: NextFunction): ResponseEntity<{
1021
+ handle(err: unknown, _request: Request, _response: Response$1, next: NextFunction): ResponseEntity<{
997
1022
  message: string;
998
1023
  }> | undefined;
999
1024
  }
@@ -1004,7 +1029,7 @@ declare class DefaultParserErrorMiddleware {
1004
1029
  * If the default is not suitable, you may also easily write your own.
1005
1030
  */
1006
1031
  declare class DefaultResponseStatusErrorMiddleware {
1007
- handle(err: unknown, _request: Request$1, _response: Response$1, next: NextFunction): ResponseEntity<{
1032
+ handle(err: unknown, _request: Request, _response: Response$1, next: NextFunction): ResponseEntity<{
1008
1033
  message: string;
1009
1034
  }> | undefined;
1010
1035
  }
@@ -1027,7 +1052,7 @@ declare class DefaultResponseStatusErrorMiddleware {
1027
1052
  * ```
1028
1053
  */
1029
1054
  declare class DefaultOpenApiMiddleware {
1030
- handle(_request: Request$1, _response: Response$1, _next: NextFunction): ResponseEntity<OpenAPIV3.Document<{}>>;
1055
+ handle(_request: Request, _response: Response$1, _next: NextFunction): ResponseEntity<OpenAPIV3.Document<{}>>;
1031
1056
  }
1032
1057
  //#endregion
1033
1058
  //#region src/middleware/default/swagger/index.d.ts
@@ -1049,7 +1074,7 @@ declare class DefaultOpenApiMiddleware {
1049
1074
  */
1050
1075
  declare class Serve {
1051
1076
  private readonly handlers;
1052
- handle(request: Request$1, response: Response$1, next: NextFunction): void;
1077
+ handle(request: Request, response: Response$1, next: NextFunction): void;
1053
1078
  }
1054
1079
  /**
1055
1080
  * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
@@ -1070,7 +1095,7 @@ declare class Serve {
1070
1095
  declare class Setup {
1071
1096
  private readonly handler;
1072
1097
  constructor();
1073
- handle(request: Request$1, response: Response$1, next: NextFunction): unknown;
1098
+ handle(request: Request, response: Response$1, next: NextFunction): unknown;
1074
1099
  }
1075
1100
  /**
1076
1101
  * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
@@ -1097,18 +1122,42 @@ declare const DefaultSwaggerMiddleware: {
1097
1122
  type HealthCheck = () => boolean | Promise<boolean>;
1098
1123
  declare class HealthRegistrar {
1099
1124
  private _checks;
1100
- private _sealed;
1125
+ private _live;
1101
1126
  constructor();
1127
+ /**
1128
+ * Add a health check.
1129
+ *
1130
+ * Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
1131
+ */
1102
1132
  add(healthCheck: HealthCheck): void;
1103
- seal(): void;
1104
- check(): Promise<boolean>;
1133
+ /**
1134
+ * @internal used by Sapling library, used to determine once all
1135
+ * checks have been registered and server is, at the very least, alive.
1136
+ */
1137
+ _markLive(): void;
1138
+ /**
1139
+ * @internal
1140
+ */
1141
+ _liveness(): Promise<boolean>;
1142
+ /**
1143
+ * @internal
1144
+ */
1145
+ _readiness(): Promise<boolean>;
1105
1146
  }
1106
1147
  //#endregion
1107
1148
  //#region src/middleware/default/health/index.d.ts
1149
+ /**
1150
+ * Enable the serving of `ready` and `live` endpoints.
1151
+ *
1152
+ * Configure any middleware-specific settings with `Sapling.Extras.health`
1153
+ */
1108
1154
  declare class DefaultHealthMiddleware {
1109
1155
  private readonly healthRegistrar;
1110
1156
  constructor(healthRegistrar: HealthRegistrar);
1111
- serve(_request: Request, _response: Response, _next: NextFunction): Promise<ResponseEntity<{
1157
+ readiness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
1158
+ up: boolean;
1159
+ }>>;
1160
+ liveness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
1112
1161
  up: boolean;
1113
1162
  }>>;
1114
1163
  }
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import e, { ErrorRequestHandler, NextFunction, Request as Request$1, RequestHandler, Response as Response$1, Router } from "express";
1
+ import e, { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response as Response$1, Router } from "express";
2
2
 
3
3
  //#region src/html/404.d.ts
4
4
  /**
@@ -26,7 +26,7 @@ type RouteDefinition = {
26
26
  };
27
27
  type Class<T> = new (...args: any[]) => T;
28
28
  type HttpHeaders = Record<string, string>;
29
- type ExpressMiddlewareFn = ($1: Request$1, $2: Response$1, $3: NextFunction) => void;
29
+ type ExpressMiddlewareFn = ($1: Request, $2: Response$1, $3: NextFunction) => void;
30
30
  //#endregion
31
31
  //#region src/annotation/controller.d.ts
32
32
  declare const _ControllerRegistry: WeakMap<Function, Router | ErrorRequestHandler>;
@@ -795,7 +795,12 @@ type Settings = {
795
795
  serialize: (value: any) => string;
796
796
  deserialize: (value: string) => any;
797
797
  health: {
798
- path: string;
798
+ ready: {
799
+ path: string;
800
+ };
801
+ live: {
802
+ path: string;
803
+ };
799
804
  };
800
805
  doc: {
801
806
  openApiPath: string;
@@ -843,13 +848,16 @@ declare class Sapling {
843
848
  * import { Sapling } from "@tahminator/sapling";
844
849
  * import express from "express";
845
850
  *
846
- * const app = express();
847
- *
848
- * app.registerApp(app);
851
+ * // returns the exact same `express.App` type back to you!
852
+ * const app = Sapling.registerApp(express());
849
853
  * ```
850
854
  */
851
855
  static registerApp(app: e.Express): e.Express;
852
- static onStartup(): void;
856
+ /**
857
+ * @internal
858
+ * visible for testing
859
+ */
860
+ static _onPostStartup(): void;
853
861
  /**
854
862
  * Serialize a value into a JSON string.
855
863
  *
@@ -906,6 +914,23 @@ declare class Sapling {
906
914
  */
907
915
  setSwaggerPath(this: void, path: string): void;
908
916
  };
917
+ /**
918
+ * Modify default settings applied to health / readiness / liveness.
919
+ */
920
+ health: {
921
+ /**
922
+ * change default endpoint that ready endpoint will be served on.
923
+ *
924
+ * @default `/readyz`
925
+ */
926
+ setReadyPath(this: void, path: string): void;
927
+ /**
928
+ * change default endpoint that live endpoint will be served on.
929
+ *
930
+ * @default `/livez`
931
+ */
932
+ setLivePath(this: void, path: string): void;
933
+ };
909
934
  };
910
935
  /**
911
936
  * This method can be used in a `@MiddlewareClass` to register any libraries
@@ -926,7 +951,7 @@ declare class Sapling {
926
951
  * }
927
952
  * ```
928
953
  */
929
- static chainHandlers(this: void, handlers: RequestHandler[], request: Request$1, response: Response$1, next: NextFunction, index?: number): void;
954
+ static chainHandlers(this: void, handlers: RequestHandler[], request: Request, response: Response$1, next: NextFunction, index?: number): void;
930
955
  }
931
956
  //#endregion
932
957
  //#region src/annotation/validator.d.ts
@@ -982,7 +1007,7 @@ declare function _getControllerSchema(ctor: Function): ControllerSchemaDefinitio
982
1007
  * If the default is not suitable, you may also easily write your own.
983
1008
  */
984
1009
  declare class DefaultBaseErrorMiddleware {
985
- handle(err: unknown, _request: Request$1, _response: Response$1, _next: NextFunction): ResponseEntity<{
1010
+ handle(err: unknown, _request: Request, _response: Response$1, _next: NextFunction): ResponseEntity<{
986
1011
  message: string;
987
1012
  }>;
988
1013
  }
@@ -993,7 +1018,7 @@ declare class DefaultBaseErrorMiddleware {
993
1018
  * If the default is not suitable, you may also easily write your own.
994
1019
  */
995
1020
  declare class DefaultParserErrorMiddleware {
996
- handle(err: unknown, _request: Request$1, _response: Response$1, next: NextFunction): ResponseEntity<{
1021
+ handle(err: unknown, _request: Request, _response: Response$1, next: NextFunction): ResponseEntity<{
997
1022
  message: string;
998
1023
  }> | undefined;
999
1024
  }
@@ -1004,7 +1029,7 @@ declare class DefaultParserErrorMiddleware {
1004
1029
  * If the default is not suitable, you may also easily write your own.
1005
1030
  */
1006
1031
  declare class DefaultResponseStatusErrorMiddleware {
1007
- handle(err: unknown, _request: Request$1, _response: Response$1, next: NextFunction): ResponseEntity<{
1032
+ handle(err: unknown, _request: Request, _response: Response$1, next: NextFunction): ResponseEntity<{
1008
1033
  message: string;
1009
1034
  }> | undefined;
1010
1035
  }
@@ -1027,7 +1052,7 @@ declare class DefaultResponseStatusErrorMiddleware {
1027
1052
  * ```
1028
1053
  */
1029
1054
  declare class DefaultOpenApiMiddleware {
1030
- handle(_request: Request$1, _response: Response$1, _next: NextFunction): ResponseEntity<OpenAPIV3.Document<{}>>;
1055
+ handle(_request: Request, _response: Response$1, _next: NextFunction): ResponseEntity<OpenAPIV3.Document<{}>>;
1031
1056
  }
1032
1057
  //#endregion
1033
1058
  //#region src/middleware/default/swagger/index.d.ts
@@ -1049,7 +1074,7 @@ declare class DefaultOpenApiMiddleware {
1049
1074
  */
1050
1075
  declare class Serve {
1051
1076
  private readonly handlers;
1052
- handle(request: Request$1, response: Response$1, next: NextFunction): void;
1077
+ handle(request: Request, response: Response$1, next: NextFunction): void;
1053
1078
  }
1054
1079
  /**
1055
1080
  * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
@@ -1070,7 +1095,7 @@ declare class Serve {
1070
1095
  declare class Setup {
1071
1096
  private readonly handler;
1072
1097
  constructor();
1073
- handle(request: Request$1, response: Response$1, next: NextFunction): unknown;
1098
+ handle(request: Request, response: Response$1, next: NextFunction): unknown;
1074
1099
  }
1075
1100
  /**
1076
1101
  * Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
@@ -1097,18 +1122,42 @@ declare const DefaultSwaggerMiddleware: {
1097
1122
  type HealthCheck = () => boolean | Promise<boolean>;
1098
1123
  declare class HealthRegistrar {
1099
1124
  private _checks;
1100
- private _sealed;
1125
+ private _live;
1101
1126
  constructor();
1127
+ /**
1128
+ * Add a health check.
1129
+ *
1130
+ * Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
1131
+ */
1102
1132
  add(healthCheck: HealthCheck): void;
1103
- seal(): void;
1104
- check(): Promise<boolean>;
1133
+ /**
1134
+ * @internal used by Sapling library, used to determine once all
1135
+ * checks have been registered and server is, at the very least, alive.
1136
+ */
1137
+ _markLive(): void;
1138
+ /**
1139
+ * @internal
1140
+ */
1141
+ _liveness(): Promise<boolean>;
1142
+ /**
1143
+ * @internal
1144
+ */
1145
+ _readiness(): Promise<boolean>;
1105
1146
  }
1106
1147
  //#endregion
1107
1148
  //#region src/middleware/default/health/index.d.ts
1149
+ /**
1150
+ * Enable the serving of `ready` and `live` endpoints.
1151
+ *
1152
+ * Configure any middleware-specific settings with `Sapling.Extras.health`
1153
+ */
1108
1154
  declare class DefaultHealthMiddleware {
1109
1155
  private readonly healthRegistrar;
1110
1156
  constructor(healthRegistrar: HealthRegistrar);
1111
- serve(_request: Request, _response: Response, _next: NextFunction): Promise<ResponseEntity<{
1157
+ readiness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
1158
+ up: boolean;
1159
+ }>>;
1160
+ liveness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
1112
1161
  up: boolean;
1113
1162
  }>>;
1114
1163
  }
package/dist/index.mjs CHANGED
@@ -376,20 +376,38 @@ function __decorate(decorators, target, key, desc) {
376
376
  //#region src/middleware/health/registrar.ts
377
377
  let HealthRegistrar = class HealthRegistrar {
378
378
  _checks;
379
- _sealed;
379
+ _live;
380
380
  constructor() {
381
381
  this._checks = [];
382
- this._sealed = false;
382
+ this._live = false;
383
383
  }
384
+ /**
385
+ * Add a health check.
386
+ *
387
+ * Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
388
+ */
384
389
  add(healthCheck) {
385
390
  this._checks.push(healthCheck);
386
391
  }
387
- seal() {
388
- this._sealed = true;
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
+ _markLive() {
397
+ this._live = true;
398
+ }
399
+ /**
400
+ * @internal
401
+ */
402
+ async _liveness() {
403
+ return this._live;
389
404
  }
390
- async check() {
391
- if (!this._sealed) return false;
392
- return (await Promise.all(this._checks.map((c) => c()))).every((c) => c === true);
405
+ /**
406
+ * @internal
407
+ */
408
+ async _readiness() {
409
+ if (!this._live) return false;
410
+ return (await Promise.allSettled(this._checks.map((c) => c()))).every((r) => r.status === "fulfilled" && r.value);
393
411
  }
394
412
  };
395
413
  HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
@@ -398,7 +416,10 @@ HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
398
416
  const _settings = {
399
417
  serialize: JSON.stringify,
400
418
  deserialize: JSON.parse,
401
- health: { path: "/up" },
419
+ health: {
420
+ ready: { path: "/readyz" },
421
+ live: { path: "/livez" }
422
+ },
402
423
  doc: {
403
424
  openApiPath: "/openapi.json",
404
425
  swaggerPath: "/swagger.html",
@@ -466,9 +487,8 @@ var Sapling = class Sapling {
466
487
  * import { Sapling } from "@tahminator/sapling";
467
488
  * import express from "express";
468
489
  *
469
- * const app = express();
470
- *
471
- * app.registerApp(app);
490
+ * // returns the exact same `express.App` type back to you!
491
+ * const app = Sapling.registerApp(express());
472
492
  * ```
473
493
  */
474
494
  static registerApp(app) {
@@ -480,7 +500,7 @@ var Sapling = class Sapling {
480
500
  return function(...args) {
481
501
  const server = originalListen.apply(target, args);
482
502
  server.once("listening", () => {
483
- Sapling.onStartup();
503
+ Sapling._onPostStartup();
484
504
  console.log("Sapling successfully initialized post-startup hooks on server start");
485
505
  });
486
506
  return server;
@@ -489,8 +509,12 @@ var Sapling = class Sapling {
489
509
  return Reflect.get(target, prop, receiver);
490
510
  } });
491
511
  }
492
- static onStartup() {
493
- _InjectableRegistry.get(HealthRegistrar)?.seal();
512
+ /**
513
+ * @internal
514
+ * visible for testing
515
+ */
516
+ static _onPostStartup() {
517
+ _InjectableRegistry.get(HealthRegistrar)?._markLive();
494
518
  }
495
519
  /**
496
520
  * Serialize a value into a JSON string.
@@ -531,37 +555,59 @@ var Sapling = class Sapling {
531
555
  /**
532
556
  * Modify extra settings
533
557
  */
534
- static Extras = {
535
- /**
536
- * Modify default settings applied to OpenAPI & Swagger
537
- */
538
- swaggerAndOpenApi: {
558
+ static Extras = {
539
559
  /**
540
- * Set base OpenAPI metadata values.
541
- *
542
- * @default { title: "API", version: "1.0.0" }
560
+ * Modify default settings applied to OpenAPI & Swagger
543
561
  */
544
- setMetadata(metadata) {
545
- _settings.doc.metadata = metadata;
546
- },
547
- /**
548
- * change default endpoint that will serve OpenAPI spec.
549
- * Swagger will also load this endpoint on load.
550
- *
551
- * @default `/openapi.json`
552
- */
553
- setOpenApiPath(path) {
554
- _settings.doc.openApiPath = path;
562
+ swaggerAndOpenApi: {
563
+ /**
564
+ * Set base OpenAPI metadata values.
565
+ *
566
+ * @default { title: "API", version: "1.0.0" }
567
+ */
568
+ setMetadata(metadata) {
569
+ _settings.doc.metadata = metadata;
570
+ },
571
+ /**
572
+ * change default endpoint that will serve OpenAPI spec.
573
+ * Swagger will also load this endpoint on load.
574
+ *
575
+ * @default `/openapi.json`
576
+ */
577
+ setOpenApiPath(path) {
578
+ _settings.doc.openApiPath = path;
579
+ },
580
+ /**
581
+ * change Swagger endpoint.
582
+ *
583
+ * @default `/swagger.html`
584
+ */
585
+ setSwaggerPath(path) {
586
+ _settings.doc.swaggerPath = path;
587
+ }
555
588
  },
556
589
  /**
557
- * change Swagger endpoint.
558
- *
559
- * @default `/swagger.html`
590
+ * Modify default settings applied to health / readiness / liveness.
560
591
  */
561
- setSwaggerPath(path) {
562
- _settings.doc.swaggerPath = path;
592
+ health: {
593
+ /**
594
+ * change default endpoint that ready endpoint will be served on.
595
+ *
596
+ * @default `/readyz`
597
+ */
598
+ setReadyPath(path) {
599
+ _settings.health.ready.path = path;
600
+ },
601
+ /**
602
+ * change default endpoint that live endpoint will be served on.
603
+ *
604
+ * @default `/livez`
605
+ */
606
+ setLivePath(path) {
607
+ _settings.health.live.path = path;
608
+ }
563
609
  }
564
- } };
610
+ };
565
611
  /**
566
612
  * This method can be used in a `@MiddlewareClass` to register any libraries
567
613
  * that expect you to register multiple registers at once. An example is `swagger-ui-express`
@@ -1154,12 +1200,17 @@ let DefaultHealthMiddleware = class DefaultHealthMiddleware {
1154
1200
  constructor(healthRegistrar) {
1155
1201
  this.healthRegistrar = healthRegistrar;
1156
1202
  }
1157
- async serve(_request, _response, _next) {
1158
- const up = await this.healthRegistrar.check();
1203
+ async readiness(_request, _response, _next) {
1204
+ const up = await this.healthRegistrar._readiness();
1205
+ return ResponseEntity.ok().body({ up });
1206
+ }
1207
+ async liveness(_request, _response, _next) {
1208
+ const up = await this.healthRegistrar._liveness();
1159
1209
  return ResponseEntity.ok().body({ up });
1160
1210
  }
1161
1211
  };
1162
- __decorate([GET(_settings.health.path)], DefaultHealthMiddleware.prototype, "serve", null);
1212
+ __decorate([GET(_settings.health.ready.path)], DefaultHealthMiddleware.prototype, "readiness", null);
1213
+ __decorate([GET(_settings.health.live.path)], DefaultHealthMiddleware.prototype, "liveness", null);
1163
1214
  DefaultHealthMiddleware = __decorate([MiddlewareClass({ deps: [HealthRegistrar] })], DefaultHealthMiddleware);
1164
1215
  //#endregion
1165
1216
  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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tahminator/sapling",
3
- "version": "2.1.0-beta.e797371f",
3
+ "version": "2.1.1",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A library to help you write cleaner Express.js code",
6
6
  "repository": {