@tsed/terminus 8.0.1 → 8.0.2

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@tsed/terminus",
3
3
  "description": "Adds graceful shutdown and Kubernetes readiness / liveness checks for any HTTP applications.",
4
4
  "type": "module",
5
- "version": "8.0.1",
5
+ "version": "8.0.2",
6
6
  "author": "Romain Lenzotti",
7
7
  "source": "./src/index.ts",
8
8
  "main": "./lib/esm/index.js",
@@ -10,6 +10,7 @@
10
10
  "typings": "./lib/types/index.d.ts",
11
11
  "exports": {
12
12
  ".": {
13
+ "@tsed/source": "./src/index.ts",
13
14
  "types": "./lib/types/index.d.ts",
14
15
  "import": "./lib/esm/index.js",
15
16
  "default": "./lib/esm/index.js"
@@ -28,22 +29,22 @@
28
29
  },
29
30
  "devDependencies": {
30
31
  "@godaddy/terminus": "^4.12.1",
31
- "@tsed/barrels": "8.0.1",
32
- "@tsed/core": "8.0.1",
33
- "@tsed/di": "8.0.1",
34
- "@tsed/platform-http": "8.0.1",
35
- "@tsed/schema": "8.0.1",
36
- "@tsed/typescript": "8.0.1",
32
+ "@tsed/barrels": "8.0.2",
33
+ "@tsed/core": "8.0.2",
34
+ "@tsed/di": "8.0.2",
35
+ "@tsed/platform-http": "8.0.2",
36
+ "@tsed/schema": "8.0.2",
37
+ "@tsed/typescript": "8.0.2",
37
38
  "eslint": "9.12.0",
38
39
  "typescript": "5.4.5",
39
40
  "vitest": "2.1.2"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "@godaddy/terminus": "^4.7.1",
43
- "@tsed/core": "8.0.1",
44
- "@tsed/di": "8.0.1",
45
- "@tsed/platform-http": "8.0.1",
46
- "@tsed/schema": "8.0.1"
44
+ "@tsed/core": "8.0.2",
45
+ "@tsed/di": "8.0.2",
46
+ "@tsed/platform-http": "8.0.2",
47
+ "@tsed/schema": "8.0.2"
47
48
  },
48
49
  "peerDependenciesMeta": {
49
50
  "@godaddy/terminus": {
@@ -0,0 +1,91 @@
1
+ import {Injectable} from "@tsed/di";
2
+ import {PlatformTest} from "@tsed/platform-http/testing";
3
+
4
+ import {Health} from "./decorators/health.js";
5
+ import {TerminusModule} from "./TerminusModule.js";
6
+
7
+ @Injectable()
8
+ class MyService {
9
+ @Health("mongo")
10
+ mongo() {
11
+ return Promise.resolve("OK");
12
+ }
13
+
14
+ @Health("/redis/health")
15
+ redis() {
16
+ return Promise.resolve("OK");
17
+ }
18
+
19
+ $beforeShutdown() {}
20
+ }
21
+
22
+ describe("TerminusModule", () => {
23
+ beforeEach(() =>
24
+ PlatformTest.create({
25
+ terminus: {
26
+ path: "/health"
27
+ }
28
+ })
29
+ );
30
+ afterEach(() => PlatformTest.reset());
31
+
32
+ it("should load health providers", async () => {
33
+ const terminusModule = PlatformTest.get<TerminusModule>(TerminusModule);
34
+
35
+ const {logger, ...props} = terminusModule.getConfiguration();
36
+
37
+ expect(props).toEqual({
38
+ beforeShutdown: expect.any(Function),
39
+ healthChecks: {
40
+ "/mongo/health": expect.any(Function),
41
+ "/redis/health": expect.any(Function),
42
+ "/health": expect.any(Function)
43
+ },
44
+ onSendFailureDuringShutdown: expect.any(Function),
45
+ onShutdown: expect.any(Function),
46
+ onSignal: expect.any(Function)
47
+ });
48
+
49
+ const result = await props.healthChecks["/health"]({});
50
+
51
+ expect(result).toEqual([
52
+ {mongo: "OK"},
53
+ {"/redis/health": "OK"} // legacy
54
+ ]);
55
+
56
+ logger("event", {message: "message"});
57
+
58
+ expect(await terminusModule.$logRoutes([])).toEqual([
59
+ {
60
+ method: "GET",
61
+ name: "TerminusModule.dispatch()",
62
+ url: "/health"
63
+ },
64
+ {
65
+ method: "GET",
66
+ name: "MyService.mongo()",
67
+ url: "/mongo/health"
68
+ },
69
+ {
70
+ method: "GET",
71
+ name: "MyService.redis()",
72
+ url: "/redis/health"
73
+ }
74
+ ]);
75
+
76
+ await props.onSignal();
77
+ });
78
+
79
+ it("should emit event", async () => {
80
+ const terminusModule = PlatformTest.get<TerminusModule>(TerminusModule);
81
+ const service = PlatformTest.get<MyService>(MyService);
82
+
83
+ vi.spyOn(service, "$beforeShutdown");
84
+
85
+ const {beforeShutdown} = terminusModule.getConfiguration();
86
+
87
+ await beforeShutdown();
88
+
89
+ expect(service.$beforeShutdown).toHaveBeenCalledWith();
90
+ });
91
+ });
@@ -0,0 +1,146 @@
1
+ import {createTerminus} from "@godaddy/terminus";
2
+ import {Constant, Inject, InjectorService, Module, OnInit, Provider} from "@tsed/di";
3
+ import type {PlatformRouteDetails} from "@tsed/platform-http";
4
+ import {concatPath} from "@tsed/schema";
5
+ import Http from "http";
6
+ import Https from "https";
7
+
8
+ import {TerminusSettings} from "./interfaces/TerminusSettings.js";
9
+
10
+ @Module()
11
+ export class TerminusModule implements OnInit {
12
+ @Constant("terminus", {})
13
+ private settings: TerminusSettings;
14
+
15
+ @Constant("terminus.path", "/health")
16
+ private basePath: string;
17
+
18
+ @Inject()
19
+ private injector: InjectorService;
20
+
21
+ @Inject(Http.Server)
22
+ private httpServer: Http.Server | null;
23
+
24
+ @Inject(Https.Server)
25
+ private httpsServer: Https.Server | null;
26
+
27
+ public $onInit() {
28
+ this.mount();
29
+ }
30
+
31
+ getConfiguration() {
32
+ const {path, ...props} = this.settings;
33
+
34
+ return {
35
+ logger: (event: string, error: any) =>
36
+ this.injector.logger.info({
37
+ event: event.toUpperCase(),
38
+ error_message: error.message
39
+ }),
40
+ healthChecks: this.getHealths(),
41
+ onSignal: this.createEmitter("$onSignal"),
42
+ onShutdown: this.createEmitter("$onShutdown"),
43
+ beforeShutdown: this.createEmitter("$beforeShutdown"),
44
+ onSendFailureDuringShutdown: this.createEmitter("$onSendFailureDuringShutdown"),
45
+ ...props
46
+ };
47
+ }
48
+
49
+ $logRoutes(routes: PlatformRouteDetails[]): Promise<PlatformRouteDetails[]> {
50
+ return Promise.resolve([
51
+ ...routes,
52
+ {
53
+ url: this.basePath,
54
+ method: "GET",
55
+ name: `TerminusModule.dispatch()`
56
+ },
57
+ ...this.getAll<{name: string}>("health").map(({provider, propertyKey, options}) => {
58
+ const path = this.getPath(provider, options.name);
59
+
60
+ return {
61
+ method: "GET",
62
+ name: `${provider.className}.${propertyKey}()`,
63
+ url: path
64
+ } as any;
65
+ })
66
+ ]);
67
+ }
68
+
69
+ private mount() {
70
+ const terminusConfig = this.getConfiguration();
71
+
72
+ if (this.httpServer) {
73
+ createTerminus(this.httpServer, terminusConfig);
74
+ }
75
+
76
+ if (this.httpsServer) {
77
+ createTerminus(this.httpsServer, terminusConfig);
78
+ }
79
+ }
80
+
81
+ private getAll<Opts = any>(
82
+ name: string
83
+ ): {
84
+ provider: Provider;
85
+ propertyKey: string;
86
+ options: Opts;
87
+ }[] {
88
+ return this.injector.getProviders().flatMap((provider) => {
89
+ const metadata = provider.store.get(`terminus:${name}`);
90
+
91
+ if (metadata) {
92
+ return Object.entries(metadata).map(([propertyKey, options]: [string, Opts]) => {
93
+ return {
94
+ provider,
95
+ propertyKey,
96
+ options
97
+ };
98
+ });
99
+ }
100
+ return [];
101
+ });
102
+ }
103
+
104
+ private getHealths() {
105
+ const subHealths = this.getAll<{name: string}>("health").reduce(
106
+ (healths, {provider, propertyKey, options: {name}}) => {
107
+ const instance = this.injector.get<any>(provider.token)!;
108
+ const callback = async (...args: any[]) => {
109
+ const result = await instance[propertyKey](...args);
110
+
111
+ return {[name]: result};
112
+ };
113
+
114
+ let path = this.getPath(provider, name);
115
+
116
+ return {
117
+ ...healths,
118
+ [path]: callback
119
+ };
120
+ },
121
+ {} as Record<string, (state: any) => Promise<any>>
122
+ );
123
+
124
+ const healths: Record<string, any> = {
125
+ ...subHealths,
126
+ [this.basePath]: (state: any) => {
127
+ const promises = Object.entries(subHealths).map(([path, callback]) => callback(state));
128
+
129
+ return Promise.all(promises);
130
+ }
131
+ };
132
+
133
+ return healths;
134
+ }
135
+
136
+ private getPath(provider: Provider<any>, name: string) {
137
+ let path = concatPath(provider.path, name);
138
+ return path.includes("health") ? path : concatPath(path, this.basePath);
139
+ }
140
+
141
+ private createEmitter(name: string) {
142
+ return (...args: any[]) => {
143
+ return this.injector.emit(name, ...args);
144
+ };
145
+ }
146
+ }
@@ -0,0 +1,28 @@
1
+ import {Store} from "@tsed/core";
2
+
3
+ /**
4
+ * Create a readiness / liveness checks.
5
+ *
6
+ * ```ts
7
+ * import { Health } from "@tsed/terminus";
8
+ *
9
+ * @Controller("/mongo")
10
+ * class MongoCtrl {
11
+ * @Health("/health")
12
+ * health() {
13
+ * // Here check the mongo health
14
+ * return Promise.resolve();
15
+ * }
16
+ * }
17
+ *
18
+ * @param name
19
+ * @decorator
20
+ * @terminus
21
+ */
22
+ export function Health(name: string): MethodDecorator {
23
+ return <Function>(target: Object, propertyKey: string) => {
24
+ Store.from(target).merge(`terminus:health`, {
25
+ [propertyKey]: {name}
26
+ });
27
+ };
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file Automatically generated by @tsed/barrels.
3
+ */
4
+ export * from "./decorators/health.js";
5
+ export * from "./interfaces/interfaces.js";
6
+ export * from "./interfaces/TerminusSettings.js";
7
+ export * from "./TerminusModule.js";
@@ -0,0 +1,6 @@
1
+ import {TerminusOptions} from "@godaddy/terminus";
2
+
3
+ export type TerminusSettings = Omit<
4
+ TerminusOptions,
5
+ "healthChecks" | "onSignal" | "onSendFailureDuringShutdown" | "onShutdown" | "beforeShutdown" | "onSigterm"
6
+ > & {path?: string};
@@ -0,0 +1,9 @@
1
+ import {TerminusSettings} from "./TerminusSettings.js";
2
+
3
+ declare global {
4
+ namespace TsED {
5
+ interface Configuration {
6
+ terminus: TerminusSettings;
7
+ }
8
+ }
9
+ }