@twin.org/api-service 0.0.2-next.9 → 0.0.3-next.10

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.
@@ -0,0 +1,8 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ export * from "./informationRoutes.js";
4
+ export * from "./informationService.js";
5
+ export * from "./models/IInformationServiceConfig.js";
6
+ export * from "./models/IInformationServiceConstructorOptions.js";
7
+ export * from "./restEntryPoints.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uCAAuC,CAAC;AACtD,cAAc,mDAAmD,CAAC;AAClE,cAAc,sBAAsB,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./informationRoutes.js\";\nexport * from \"./informationService.js\";\nexport * from \"./models/IInformationServiceConfig.js\";\nexport * from \"./models/IInformationServiceConstructorOptions.js\";\nexport * from \"./restEntryPoints.js\";\n"]}
@@ -1,11 +1,9 @@
1
- import { ComponentFactory, Is, Guards } from '@twin.org/core';
2
- import { MimeTypes, HeaderTypes, HttpStatusCode } from '@twin.org/web';
3
- import { readFile } from 'node:fs/promises';
4
-
1
+ import { ComponentFactory, Is } from "@twin.org/core";
2
+ import { HeaderTypes, HttpStatusCode, MimeTypes } from "@twin.org/web";
5
3
  /**
6
4
  * The tag to associate with the routes.
7
5
  */
8
- const tagsInformation = [
6
+ export const tagsInformation = [
9
7
  {
10
8
  name: "Info",
11
9
  description: "Information endpoints for the REST server."
@@ -17,14 +15,14 @@ const tagsInformation = [
17
15
  * @param componentName The name of the component to use in the routes stored in the ComponentFactory.
18
16
  * @returns The generated routes.
19
17
  */
20
- function generateRestRoutesInformation(baseRouteName, componentName) {
18
+ export function generateRestRoutesInformation(baseRouteName, componentName) {
21
19
  const rootRoute = {
22
20
  operationId: "serverRoot",
23
21
  summary: "Get the root text page",
24
22
  tag: tagsInformation[0].name,
25
23
  method: "GET",
26
24
  path: `${baseRouteName}/`,
27
- handler: async (httpRequestContext, request) => serverRoot(httpRequestContext, componentName),
25
+ handler: async (httpRequestContext, request) => serverRoot(httpRequestContext, componentName, request),
28
26
  responseType: [
29
27
  {
30
28
  type: "IServerRootResponse",
@@ -34,13 +32,17 @@ function generateRestRoutesInformation(baseRouteName, componentName) {
34
32
  id: "serverRootResponse",
35
33
  description: "The response for the root request.",
36
34
  response: {
35
+ headers: {
36
+ [HeaderTypes.ContentType]: MimeTypes.PlainText
37
+ },
37
38
  body: "API Server - 1.0.0"
38
39
  }
39
40
  }
40
41
  ]
41
42
  }
42
43
  ],
43
- skipAuth: true
44
+ skipAuth: true,
45
+ skipTenant: true
44
46
  };
45
47
  const informationRoute = {
46
48
  operationId: "serverInformation",
@@ -48,7 +50,7 @@ function generateRestRoutesInformation(baseRouteName, componentName) {
48
50
  tag: tagsInformation[0].name,
49
51
  method: "GET",
50
52
  path: `${baseRouteName}/info`,
51
- handler: async (httpRequestContext, request) => serverInfo(httpRequestContext, componentName),
53
+ handler: async (httpRequestContext, request) => serverInfo(httpRequestContext, componentName, request),
52
54
  responseType: [
53
55
  {
54
56
  type: "IServerInfoResponse",
@@ -74,14 +76,53 @@ function generateRestRoutesInformation(baseRouteName, componentName) {
74
76
  tag: tagsInformation[0].name,
75
77
  method: "GET",
76
78
  path: `${baseRouteName}/favicon.ico`,
77
- handler: async (httpRequestContext, request) => serverFavIcon(httpRequestContext, componentName),
79
+ handler: async (httpRequestContext, request) => serverFavIcon(httpRequestContext, componentName, request),
78
80
  responseType: [
79
81
  {
80
82
  type: "IServerFavIconResponse",
81
83
  mimeType: "image/x-icon"
82
84
  }
83
85
  ],
84
- skipAuth: true
86
+ skipAuth: true,
87
+ skipTenant: true
88
+ };
89
+ const livezRoute = {
90
+ operationId: "serverLivez",
91
+ summary: "Get the livez status for the server",
92
+ tag: tagsInformation[0].name,
93
+ method: "GET",
94
+ path: `${baseRouteName}/livez`,
95
+ handler: async (httpRequestContext, request) => serverLivez(httpRequestContext, componentName, request),
96
+ responseType: [
97
+ {
98
+ type: "IServerLivezResponse",
99
+ mimeType: MimeTypes.PlainText,
100
+ examples: [
101
+ {
102
+ id: "livezResponseOK",
103
+ description: "The response for the liveness request.",
104
+ response: {
105
+ headers: {
106
+ [HeaderTypes.ContentType]: MimeTypes.PlainText
107
+ },
108
+ body: "ok"
109
+ }
110
+ },
111
+ {
112
+ id: "livezResponseFailure",
113
+ description: "The response for the liveness request with errors.",
114
+ response: {
115
+ headers: {
116
+ [HeaderTypes.ContentType]: MimeTypes.PlainText
117
+ },
118
+ body: "failed"
119
+ }
120
+ }
121
+ ]
122
+ }
123
+ ],
124
+ skipAuth: true,
125
+ skipTenant: true
85
126
  };
86
127
  const healthRoute = {
87
128
  operationId: "serverHealth",
@@ -89,7 +130,7 @@ function generateRestRoutesInformation(baseRouteName, componentName) {
89
130
  tag: tagsInformation[0].name,
90
131
  method: "GET",
91
132
  path: `${baseRouteName}/health`,
92
- handler: async (httpRequestContext, request) => serverHealth(httpRequestContext, componentName),
133
+ handler: async (httpRequestContext, request) => serverHealth(httpRequestContext, componentName, request),
93
134
  responseType: [
94
135
  {
95
136
  type: "IServerHealthResponse",
@@ -164,7 +205,7 @@ function generateRestRoutesInformation(baseRouteName, componentName) {
164
205
  tag: tagsInformation[0].name,
165
206
  method: "GET",
166
207
  path: `${baseRouteName}/spec`,
167
- handler: async (httpRequestContext, request) => serverSpec(httpRequestContext, componentName),
208
+ handler: async (httpRequestContext, request) => serverSpec(httpRequestContext, componentName, request),
168
209
  responseType: [
169
210
  {
170
211
  type: "IServerSpecResponse",
@@ -185,7 +226,7 @@ function generateRestRoutesInformation(baseRouteName, componentName) {
185
226
  ],
186
227
  skipAuth: true
187
228
  };
188
- return [rootRoute, favIconRoute, informationRoute, healthRoute, specRoute];
229
+ return [rootRoute, favIconRoute, informationRoute, livezRoute, healthRoute, specRoute];
189
230
  }
190
231
  /**
191
232
  * Get the root for the server.
@@ -194,9 +235,12 @@ function generateRestRoutesInformation(baseRouteName, componentName) {
194
235
  * @param request The request.
195
236
  * @returns The response object with additional http response properties.
196
237
  */
197
- async function serverRoot(httpRequestContext, componentName, request) {
238
+ export async function serverRoot(httpRequestContext, componentName, request) {
198
239
  const component = ComponentFactory.get(componentName);
199
240
  return {
241
+ headers: {
242
+ [HeaderTypes.ContentType]: MimeTypes.PlainText
243
+ },
200
244
  body: await component.root()
201
245
  };
202
246
  }
@@ -207,12 +251,28 @@ async function serverRoot(httpRequestContext, componentName, request) {
207
251
  * @param request The request.
208
252
  * @returns The response object with additional http response properties.
209
253
  */
210
- async function serverInfo(httpRequestContext, componentName, request) {
254
+ export async function serverInfo(httpRequestContext, componentName, request) {
211
255
  const component = ComponentFactory.get(componentName);
212
256
  return {
213
257
  body: await component.info()
214
258
  };
215
259
  }
260
+ /**
261
+ * Get the livez for the server.
262
+ * @param httpRequestContext The request context for the API.
263
+ * @param componentName The name of the component to use in the routes.
264
+ * @param request The request.
265
+ * @returns The response object with additional http response properties.
266
+ */
267
+ export async function serverLivez(httpRequestContext, componentName, request) {
268
+ const component = ComponentFactory.get(componentName);
269
+ return {
270
+ headers: {
271
+ [HeaderTypes.ContentType]: MimeTypes.PlainText
272
+ },
273
+ body: (await component.livez()) ? "ok" : "failed"
274
+ };
275
+ }
216
276
  /**
217
277
  * Get the health for the server.
218
278
  * @param httpRequestContext The request context for the API.
@@ -220,7 +280,7 @@ async function serverInfo(httpRequestContext, componentName, request) {
220
280
  * @param request The request.
221
281
  * @returns The response object with additional http response properties.
222
282
  */
223
- async function serverHealth(httpRequestContext, componentName, request) {
283
+ export async function serverHealth(httpRequestContext, componentName, request) {
224
284
  const component = ComponentFactory.get(componentName);
225
285
  return {
226
286
  body: await component.health()
@@ -233,7 +293,7 @@ async function serverHealth(httpRequestContext, componentName, request) {
233
293
  * @param request The request.
234
294
  * @returns The response object with additional http response properties.
235
295
  */
236
- async function serverFavIcon(httpRequestContext, componentName, request) {
296
+ export async function serverFavIcon(httpRequestContext, componentName, request) {
237
297
  const component = ComponentFactory.get(componentName);
238
298
  const favIcon = await component.favicon();
239
299
  if (Is.uint8Array(favIcon)) {
@@ -255,7 +315,7 @@ async function serverFavIcon(httpRequestContext, componentName, request) {
255
315
  * @param request The request.
256
316
  * @returns The response object with additional http response properties.
257
317
  */
258
- async function serverSpec(httpRequestContext, componentName, request) {
318
+ export async function serverSpec(httpRequestContext, componentName, request) {
259
319
  const component = ComponentFactory.get(componentName);
260
320
  const spec = await component.spec();
261
321
  if (Is.objectValue(spec)) {
@@ -267,171 +327,4 @@ async function serverSpec(httpRequestContext, componentName, request) {
267
327
  statusCode: HttpStatusCode.notFound
268
328
  };
269
329
  }
270
-
271
- // Copyright 2024 IOTA Stiftung.
272
- // SPDX-License-Identifier: Apache-2.0.
273
- /**
274
- * The information service for the server.
275
- */
276
- class InformationService {
277
- /**
278
- * Runtime name for the class.
279
- */
280
- CLASS_NAME = "InformationService";
281
- /**
282
- * The server information.
283
- * @internal
284
- */
285
- _serverInfo;
286
- /**
287
- * The server health.
288
- * @internal
289
- */
290
- _healthInfo;
291
- /**
292
- * The path to the favicon Spec.
293
- * @internal
294
- */
295
- _faviconPath;
296
- /**
297
- * The favicon.
298
- * @internal
299
- */
300
- _favicon;
301
- /**
302
- * The path to the OpenAPI Spec.
303
- * @internal
304
- */
305
- _openApiSpecPath;
306
- /**
307
- * The OpenAPI spec.
308
- * @internal
309
- */
310
- _openApiSpec;
311
- /**
312
- * Create a new instance of InformationService.
313
- * @param options The options to create the service.
314
- */
315
- constructor(options) {
316
- Guards.object(this.CLASS_NAME, "options", options);
317
- Guards.object(this.CLASS_NAME, "options.config", options.config);
318
- Guards.object(this.CLASS_NAME, "options.config.serverInfo", options.config.serverInfo);
319
- this._serverInfo = options.config.serverInfo;
320
- this._healthInfo = {
321
- status: "ok"
322
- };
323
- this._faviconPath = options.config.favIconPath;
324
- this._openApiSpecPath = options.config.openApiSpecPath;
325
- }
326
- /**
327
- * The service needs to be started when the application is initialized.
328
- * @returns Nothing.
329
- */
330
- async start() {
331
- const openApiPath = this._openApiSpecPath;
332
- if (Is.stringValue(openApiPath)) {
333
- const contentBuffer = await readFile(openApiPath, "utf8");
334
- this._openApiSpec = JSON.parse(contentBuffer);
335
- }
336
- const favIconPath = this._faviconPath;
337
- if (Is.stringValue(favIconPath)) {
338
- this._favicon = await readFile(favIconPath);
339
- }
340
- }
341
- /**
342
- * Get the root information.
343
- * @returns The root information.
344
- */
345
- async root() {
346
- return `${this._serverInfo.name} - ${this._serverInfo.version}`;
347
- }
348
- /**
349
- * Get the server information.
350
- * @returns The service information.
351
- */
352
- async info() {
353
- return this._serverInfo;
354
- }
355
- /**
356
- * Get the favicon.
357
- * @returns The favicon.
358
- */
359
- async favicon() {
360
- return this._favicon;
361
- }
362
- /**
363
- * Get the OpenAPI spec.
364
- * @returns The OpenAPI spec.
365
- */
366
- async spec() {
367
- return this._openApiSpec;
368
- }
369
- /**
370
- * Get the server health.
371
- * @returns The service health.
372
- */
373
- async health() {
374
- let errorCount = 0;
375
- let warningCount = 0;
376
- if (Is.arrayValue(this._healthInfo.components)) {
377
- errorCount = this._healthInfo.components.filter(c => c.status === "error").length;
378
- warningCount = this._healthInfo.components.filter(c => c.status === "warning").length;
379
- }
380
- if (errorCount > 0) {
381
- this._healthInfo.status = "error";
382
- }
383
- else if (warningCount > 0) {
384
- this._healthInfo.status = "warning";
385
- }
386
- else {
387
- this._healthInfo.status = "ok";
388
- }
389
- return this._healthInfo;
390
- }
391
- /**
392
- * Set the status of a component.
393
- * @param name The component name.
394
- * @param status The status of the component.
395
- * @param details The details for the status.
396
- * @returns Nothing.
397
- */
398
- async setComponentHealth(name, status, details) {
399
- const component = this._healthInfo.components?.find(c => c.name === name);
400
- if (Is.undefined(component)) {
401
- this._healthInfo.components ??= [];
402
- this._healthInfo.components.push({
403
- name,
404
- status,
405
- details
406
- });
407
- }
408
- else {
409
- component.status = status;
410
- component.details = details;
411
- }
412
- }
413
- /**
414
- * Remove the status of a component.
415
- * @param name The component name.
416
- * @returns Nothing.
417
- */
418
- async removeComponentHealth(name) {
419
- if (Is.arrayValue(this._healthInfo.components)) {
420
- const componentIndex = this._healthInfo.components.findIndex(c => c.name === name);
421
- if (componentIndex !== -1) {
422
- this._healthInfo.components.splice(componentIndex, 1);
423
- }
424
- }
425
- }
426
- }
427
-
428
- const restEntryPoints = [
429
- {
430
- name: "information",
431
- defaultBaseRoute: "",
432
- tags: tagsInformation,
433
- generateRoutes: generateRestRoutesInformation
434
- }
435
- ];
436
-
437
- export { InformationService, generateRestRoutesInformation, restEntryPoints, serverFavIcon, serverHealth, serverInfo, serverRoot, serverSpec, tagsInformation };
330
+ //# sourceMappingURL=informationRoutes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"informationRoutes.js","sourceRoot":"","sources":["../../src/informationRoutes.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIHttpRequestContext,\n\tIInformationComponent,\n\tINoContentRequest,\n\tIRestRoute,\n\tIServerFavIconResponse,\n\tIServerHealthResponse,\n\tIServerInfoResponse,\n\tIServerLivezResponse,\n\tIServerRootResponse,\n\tIServerSpecResponse,\n\tITag\n} from \"@twin.org/api-models\";\nimport { ComponentFactory, Is } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { HeaderTypes, HttpStatusCode, MimeTypes } from \"@twin.org/web\";\n\n/**\n * The tag to associate with the routes.\n */\nexport const tagsInformation: ITag[] = [\n\t{\n\t\tname: \"Info\",\n\t\tdescription: \"Information endpoints for the REST server.\"\n\t}\n];\n\n/**\n * The REST routes for server information.\n * @param baseRouteName Prefix to prepend to the paths.\n * @param componentName The name of the component to use in the routes stored in the ComponentFactory.\n * @returns The generated routes.\n */\nexport function generateRestRoutesInformation(\n\tbaseRouteName: string,\n\tcomponentName: string\n): IRestRoute[] {\n\tconst rootRoute: IRestRoute = {\n\t\toperationId: \"serverRoot\",\n\t\tsummary: \"Get the root text page\",\n\t\ttag: tagsInformation[0].name,\n\t\tmethod: \"GET\",\n\t\tpath: `${baseRouteName}/`,\n\t\thandler: async (httpRequestContext, request) =>\n\t\t\tserverRoot(httpRequestContext, componentName, request),\n\t\tresponseType: [\n\t\t\t{\n\t\t\t\ttype: nameof<IServerRootResponse>(),\n\t\t\t\tmimeType: MimeTypes.PlainText,\n\t\t\t\texamples: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"serverRootResponse\",\n\t\t\t\t\t\tdescription: \"The response for the root request.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t[HeaderTypes.ContentType]: MimeTypes.PlainText\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tbody: \"API Server - 1.0.0\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tskipAuth: true,\n\t\tskipTenant: true\n\t};\n\n\tconst informationRoute: IRestRoute<INoContentRequest, IServerInfoResponse> = {\n\t\toperationId: \"serverInformation\",\n\t\tsummary: \"Get the information for the server\",\n\t\ttag: tagsInformation[0].name,\n\t\tmethod: \"GET\",\n\t\tpath: `${baseRouteName}/info`,\n\t\thandler: async (httpRequestContext, request) =>\n\t\t\tserverInfo(httpRequestContext, componentName, request),\n\t\tresponseType: [\n\t\t\t{\n\t\t\t\ttype: nameof<IServerInfoResponse>(),\n\t\t\t\texamples: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"informationResponse\",\n\t\t\t\t\t\tdescription: \"The response for the information request.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tname: \"API Server\",\n\t\t\t\t\t\t\t\tversion: \"1.0.0\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tskipAuth: true\n\t};\n\n\tconst favIconRoute: IRestRoute<INoContentRequest, IServerFavIconResponse> = {\n\t\toperationId: \"serverFavIcon\",\n\t\tsummary: \"Get the favicon for the server\",\n\t\ttag: tagsInformation[0].name,\n\t\tmethod: \"GET\",\n\t\tpath: `${baseRouteName}/favicon.ico`,\n\t\thandler: async (httpRequestContext, request) =>\n\t\t\tserverFavIcon(httpRequestContext, componentName, request),\n\t\tresponseType: [\n\t\t\t{\n\t\t\t\ttype: nameof<IServerFavIconResponse>(),\n\t\t\t\tmimeType: \"image/x-icon\"\n\t\t\t}\n\t\t],\n\t\tskipAuth: true,\n\t\tskipTenant: true\n\t};\n\n\tconst livezRoute: IRestRoute<INoContentRequest, IServerLivezResponse> = {\n\t\toperationId: \"serverLivez\",\n\t\tsummary: \"Get the livez status for the server\",\n\t\ttag: tagsInformation[0].name,\n\t\tmethod: \"GET\",\n\t\tpath: `${baseRouteName}/livez`,\n\t\thandler: async (httpRequestContext, request) =>\n\t\t\tserverLivez(httpRequestContext, componentName, request),\n\t\tresponseType: [\n\t\t\t{\n\t\t\t\ttype: nameof<IServerLivezResponse>(),\n\t\t\t\tmimeType: MimeTypes.PlainText,\n\t\t\t\texamples: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"livezResponseOK\",\n\t\t\t\t\t\tdescription: \"The response for the liveness request.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t[HeaderTypes.ContentType]: MimeTypes.PlainText\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tbody: \"ok\"\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"livezResponseFailure\",\n\t\t\t\t\t\tdescription: \"The response for the liveness request with errors.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t[HeaderTypes.ContentType]: MimeTypes.PlainText\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tbody: \"failed\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tskipAuth: true,\n\t\tskipTenant: true\n\t};\n\n\tconst healthRoute: IRestRoute<INoContentRequest, IServerHealthResponse> = {\n\t\toperationId: \"serverHealth\",\n\t\tsummary: \"Get the health for the server\",\n\t\ttag: tagsInformation[0].name,\n\t\tmethod: \"GET\",\n\t\tpath: `${baseRouteName}/health`,\n\t\thandler: async (httpRequestContext, request) =>\n\t\t\tserverHealth(httpRequestContext, componentName, request),\n\t\tresponseType: [\n\t\t\t{\n\t\t\t\ttype: nameof<IServerHealthResponse>(),\n\t\t\t\texamples: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"healthResponseOK\",\n\t\t\t\t\t\tdescription: \"The response for the health request.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tstatus: \"ok\",\n\t\t\t\t\t\t\t\tcomponents: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Database\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Storage\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"healthResponseWarning\",\n\t\t\t\t\t\tdescription: \"The response for the health request with warnings.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tstatus: \"warning\",\n\t\t\t\t\t\t\t\tcomponents: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Database\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"warning\",\n\t\t\t\t\t\t\t\t\t\tdetails: \"The database is running slow.\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Storage\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"healthResponseError\",\n\t\t\t\t\t\tdescription: \"The response for the health request with errors.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tstatus: \"error\",\n\t\t\t\t\t\t\t\tcomponents: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Database\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Storage\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"error\",\n\t\t\t\t\t\t\t\t\t\tdetails: \"The storage is full.\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tskipAuth: true\n\t};\n\n\tconst specRoute: IRestRoute<INoContentRequest, IServerSpecResponse> = {\n\t\toperationId: \"serverSpec\",\n\t\tsummary: \"Get the OpenAPI specification for the endpoints\",\n\t\ttag: tagsInformation[0].name,\n\t\tmethod: \"GET\",\n\t\tpath: `${baseRouteName}/spec`,\n\t\thandler: async (httpRequestContext, request) =>\n\t\t\tserverSpec(httpRequestContext, componentName, request),\n\t\tresponseType: [\n\t\t\t{\n\t\t\t\ttype: nameof<IServerSpecResponse>(),\n\t\t\t\texamples: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"specResponse\",\n\t\t\t\t\t\tdescription: \"The response for the spec request.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\topenapi: \"3.1.0\",\n\t\t\t\t\t\t\t\tinfo: {},\n\t\t\t\t\t\t\t\tpaths: {}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tskipAuth: true\n\t};\n\n\treturn [rootRoute, favIconRoute, informationRoute, livezRoute, healthRoute, specRoute];\n}\n\n/**\n * Get the root for the server.\n * @param httpRequestContext The request context for the API.\n * @param componentName The name of the component to use in the routes.\n * @param request The request.\n * @returns The response object with additional http response properties.\n */\nexport async function serverRoot(\n\thttpRequestContext: IHttpRequestContext,\n\tcomponentName: string,\n\trequest: INoContentRequest\n): Promise<IServerRootResponse> {\n\tconst component = ComponentFactory.get<IInformationComponent>(componentName);\n\treturn {\n\t\theaders: {\n\t\t\t[HeaderTypes.ContentType]: MimeTypes.PlainText\n\t\t},\n\t\tbody: await component.root()\n\t};\n}\n\n/**\n * Get the information for the server.\n * @param httpRequestContext The request context for the API.\n * @param componentName The name of the component to use in the routes.\n * @param request The request.\n * @returns The response object with additional http response properties.\n */\nexport async function serverInfo(\n\thttpRequestContext: IHttpRequestContext,\n\tcomponentName: string,\n\trequest: INoContentRequest\n): Promise<IServerInfoResponse> {\n\tconst component = ComponentFactory.get<IInformationComponent>(componentName);\n\treturn {\n\t\tbody: await component.info()\n\t};\n}\n\n/**\n * Get the livez for the server.\n * @param httpRequestContext The request context for the API.\n * @param componentName The name of the component to use in the routes.\n * @param request The request.\n * @returns The response object with additional http response properties.\n */\nexport async function serverLivez(\n\thttpRequestContext: IHttpRequestContext,\n\tcomponentName: string,\n\trequest: INoContentRequest\n): Promise<IServerLivezResponse> {\n\tconst component = ComponentFactory.get<IInformationComponent>(componentName);\n\treturn {\n\t\theaders: {\n\t\t\t[HeaderTypes.ContentType]: MimeTypes.PlainText\n\t\t},\n\t\tbody: (await component.livez()) ? \"ok\" : \"failed\"\n\t};\n}\n\n/**\n * Get the health for the server.\n * @param httpRequestContext The request context for the API.\n * @param componentName The name of the component to use in the routes.\n * @param request The request.\n * @returns The response object with additional http response properties.\n */\nexport async function serverHealth(\n\thttpRequestContext: IHttpRequestContext,\n\tcomponentName: string,\n\trequest: INoContentRequest\n): Promise<IServerHealthResponse> {\n\tconst component = ComponentFactory.get<IInformationComponent>(componentName);\n\treturn {\n\t\tbody: await component.health()\n\t};\n}\n\n/**\n * Get the favicon for the server.\n * @param httpRequestContext The request context for the API.\n * @param componentName The name of the component to use in the routes.\n * @param request The request.\n * @returns The response object with additional http response properties.\n */\nexport async function serverFavIcon(\n\thttpRequestContext: IHttpRequestContext,\n\tcomponentName: string,\n\trequest: INoContentRequest\n): Promise<IServerFavIconResponse> {\n\tconst component = ComponentFactory.get<IInformationComponent>(componentName);\n\tconst favIcon = await component.favicon();\n\n\tif (Is.uint8Array(favIcon)) {\n\t\treturn {\n\t\t\theaders: {\n\t\t\t\t[HeaderTypes.ContentType]: \"image/x-icon\"\n\t\t\t},\n\t\t\tbody: favIcon\n\t\t};\n\t}\n\treturn {\n\t\tstatusCode: HttpStatusCode.notFound\n\t};\n}\n\n/**\n * Get the spec for the server.\n * @param httpRequestContext The request context for the API.\n * @param componentName The name of the component to use in the routes.\n * @param request The request.\n * @returns The response object with additional http response properties.\n */\nexport async function serverSpec(\n\thttpRequestContext: IHttpRequestContext,\n\tcomponentName: string,\n\trequest: INoContentRequest\n): Promise<IServerSpecResponse> {\n\tconst component = ComponentFactory.get<IInformationComponent>(componentName);\n\tconst spec = await component.spec();\n\n\tif (Is.objectValue(spec)) {\n\t\treturn {\n\t\t\tbody: spec\n\t\t};\n\t}\n\treturn {\n\t\tstatusCode: HttpStatusCode.notFound\n\t};\n}\n"]}
@@ -0,0 +1,196 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { readFile } from "node:fs/promises";
4
+ import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
5
+ import { Guards, Is } from "@twin.org/core";
6
+ /**
7
+ * The information service for the server.
8
+ */
9
+ export class InformationService {
10
+ /**
11
+ * Runtime name for the class.
12
+ */
13
+ static CLASS_NAME = "InformationService";
14
+ /**
15
+ * The server information.
16
+ * @internal
17
+ */
18
+ _serverInfo;
19
+ /**
20
+ * The server health.
21
+ * @internal
22
+ */
23
+ _healthInfo;
24
+ /**
25
+ * The path to the favicon Spec.
26
+ * @internal
27
+ */
28
+ _faviconPath;
29
+ /**
30
+ * The favicon.
31
+ * @internal
32
+ */
33
+ _favicon;
34
+ /**
35
+ * The path to the OpenAPI Spec.
36
+ * @internal
37
+ */
38
+ _openApiSpecPath;
39
+ /**
40
+ * The OpenAPI spec.
41
+ * @internal
42
+ */
43
+ _openApiSpec;
44
+ /**
45
+ * Create a new instance of InformationService.
46
+ * @param options The options to create the service.
47
+ */
48
+ constructor(options) {
49
+ Guards.object(InformationService.CLASS_NAME, "options", options);
50
+ Guards.object(InformationService.CLASS_NAME, "options.config", options.config);
51
+ Guards.object(InformationService.CLASS_NAME, "options.config.serverInfo", options.config.serverInfo);
52
+ this._serverInfo = options.config.serverInfo;
53
+ this._healthInfo = {
54
+ status: "ok"
55
+ };
56
+ this._faviconPath = options.config.favIconPath;
57
+ this._openApiSpecPath = options.config.openApiSpecPath;
58
+ }
59
+ /**
60
+ * Returns the class name of the component.
61
+ * @returns The class name of the component.
62
+ */
63
+ className() {
64
+ return InformationService.CLASS_NAME;
65
+ }
66
+ /**
67
+ * The service needs to be started when the application is initialized.
68
+ * @returns Nothing.
69
+ */
70
+ async start() {
71
+ const openApiPath = this._openApiSpecPath;
72
+ if (Is.stringValue(openApiPath)) {
73
+ const contentBuffer = await readFile(openApiPath, "utf8");
74
+ this._openApiSpec = JSON.parse(contentBuffer);
75
+ }
76
+ const favIconPath = this._faviconPath;
77
+ if (Is.stringValue(favIconPath)) {
78
+ this._favicon = await readFile(favIconPath);
79
+ }
80
+ }
81
+ /**
82
+ * Get the root information.
83
+ * @returns The root information.
84
+ */
85
+ async root() {
86
+ return `${this._serverInfo.name} - ${this._serverInfo.version}`;
87
+ }
88
+ /**
89
+ * Get the server information.
90
+ * @returns The service information.
91
+ */
92
+ async info() {
93
+ return this._serverInfo;
94
+ }
95
+ /**
96
+ * Get the favicon.
97
+ * @returns The favicon.
98
+ */
99
+ async favicon() {
100
+ return this._favicon;
101
+ }
102
+ /**
103
+ * Get the OpenAPI spec.
104
+ * @returns The OpenAPI spec.
105
+ */
106
+ async spec() {
107
+ return this._openApiSpec;
108
+ }
109
+ /**
110
+ * Is the server live.
111
+ * @returns True if the server is live.
112
+ */
113
+ async livez() {
114
+ let errorCount = 0;
115
+ if (Is.arrayValue(this._healthInfo.components)) {
116
+ errorCount = this._healthInfo.components.filter(c => c.status === "error").length;
117
+ }
118
+ return errorCount === 0;
119
+ }
120
+ /**
121
+ * Get the server health.
122
+ * @returns The service health.
123
+ */
124
+ async health() {
125
+ let errorCount = 0;
126
+ let warningCount = 0;
127
+ const contextIds = await ContextIdStore.getContextIds();
128
+ const tenantId = contextIds?.[ContextIdKeys.Tenant];
129
+ // Filter so we only get components that are not tenant specific or match the tenant id
130
+ const components = this._healthInfo.components?.filter(c => Is.empty(c.tenantId) || c.tenantId === tenantId);
131
+ if (Is.arrayValue(components)) {
132
+ errorCount = components.filter(c => c.status === "error").length;
133
+ warningCount = components.filter(c => c.status === "warning").length;
134
+ }
135
+ if (errorCount > 0) {
136
+ this._healthInfo.status = "error";
137
+ }
138
+ else if (warningCount > 0) {
139
+ this._healthInfo.status = "warning";
140
+ }
141
+ else {
142
+ this._healthInfo.status = "ok";
143
+ }
144
+ return {
145
+ status: this._healthInfo.status,
146
+ components: components?.map(c => ({
147
+ name: c.name,
148
+ status: c.status,
149
+ details: c.details
150
+ }))
151
+ };
152
+ }
153
+ /**
154
+ * Set the status of a component.
155
+ * @param name The component name.
156
+ * @param status The status of the component.
157
+ * @param details The details for the status.
158
+ * @param tenantId The tenant id, optional if the health status is not tenant specific.
159
+ * @returns Nothing.
160
+ */
161
+ async setComponentHealth(name, status, details, tenantId) {
162
+ const component = Is.empty(tenantId)
163
+ ? this._healthInfo.components?.find(c => c.name === name && Is.empty(c.tenantId))
164
+ : this._healthInfo.components?.find(c => c.name === name && c.tenantId === tenantId);
165
+ if (Is.undefined(component)) {
166
+ this._healthInfo.components ??= [];
167
+ this._healthInfo.components.push({
168
+ name,
169
+ status,
170
+ details,
171
+ tenantId
172
+ });
173
+ }
174
+ else {
175
+ component.status = status;
176
+ component.details = details;
177
+ }
178
+ }
179
+ /**
180
+ * Remove the status of a component.
181
+ * @param name The component name.
182
+ * @param tenantId The tenant id, optional if the health status is not tenant specific.
183
+ * @returns Nothing.
184
+ */
185
+ async removeComponentHealth(name, tenantId) {
186
+ if (Is.arrayValue(this._healthInfo.components)) {
187
+ const componentIndex = Is.empty(tenantId)
188
+ ? this._healthInfo.components?.findIndex(c => c.name === name && Is.empty(c.tenantId))
189
+ : this._healthInfo.components?.findIndex(c => c.name === name && c.tenantId === tenantId);
190
+ if (componentIndex !== -1) {
191
+ this._healthInfo.components.splice(componentIndex, 1);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ //# sourceMappingURL=informationService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"informationService.js","sourceRoot":"","sources":["../../src/informationService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { readFile } from \"node:fs/promises\";\nimport type {\n\tHealthStatus,\n\tIHealthComponentInfo,\n\tIHealthInfo,\n\tIInformationComponent,\n\tIServerInfo\n} from \"@twin.org/api-models\";\nimport { ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport { Guards, Is } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IInformationServiceConstructorOptions } from \"./models/IInformationServiceConstructorOptions.js\";\n\n/**\n * The information service for the server.\n */\nexport class InformationService implements IInformationComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<InformationService>();\n\n\t/**\n\t * The server information.\n\t * @internal\n\t */\n\tprivate readonly _serverInfo: IServerInfo;\n\n\t/**\n\t * The server health.\n\t * @internal\n\t */\n\tprivate readonly _healthInfo: {\n\t\tstatus: HealthStatus;\n\t\tcomponents?: (IHealthComponentInfo & { tenantId?: string })[];\n\t};\n\n\t/**\n\t * The path to the favicon Spec.\n\t * @internal\n\t */\n\tprivate readonly _faviconPath?: string;\n\n\t/**\n\t * The favicon.\n\t * @internal\n\t */\n\tprivate _favicon?: Uint8Array;\n\n\t/**\n\t * The path to the OpenAPI Spec.\n\t * @internal\n\t */\n\tprivate readonly _openApiSpecPath?: string;\n\n\t/**\n\t * The OpenAPI spec.\n\t * @internal\n\t */\n\tprivate _openApiSpec?: string;\n\n\t/**\n\t * Create a new instance of InformationService.\n\t * @param options The options to create the service.\n\t */\n\tconstructor(options: IInformationServiceConstructorOptions) {\n\t\tGuards.object(InformationService.CLASS_NAME, nameof(options), options);\n\t\tGuards.object(InformationService.CLASS_NAME, nameof(options.config), options.config);\n\t\tGuards.object(\n\t\t\tInformationService.CLASS_NAME,\n\t\t\tnameof(options.config.serverInfo),\n\t\t\toptions.config.serverInfo\n\t\t);\n\n\t\tthis._serverInfo = options.config.serverInfo;\n\t\tthis._healthInfo = {\n\t\t\tstatus: \"ok\"\n\t\t};\n\t\tthis._faviconPath = options.config.favIconPath;\n\t\tthis._openApiSpecPath = options.config.openApiSpecPath;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn InformationService.CLASS_NAME;\n\t}\n\n\t/**\n\t * The service needs to be started when the application is initialized.\n\t * @returns Nothing.\n\t */\n\tpublic async start(): Promise<void> {\n\t\tconst openApiPath = this._openApiSpecPath;\n\t\tif (Is.stringValue(openApiPath)) {\n\t\t\tconst contentBuffer = await readFile(openApiPath, \"utf8\");\n\t\t\tthis._openApiSpec = JSON.parse(contentBuffer);\n\t\t}\n\n\t\tconst favIconPath = this._faviconPath;\n\t\tif (Is.stringValue(favIconPath)) {\n\t\t\tthis._favicon = await readFile(favIconPath);\n\t\t}\n\t}\n\n\t/**\n\t * Get the root information.\n\t * @returns The root information.\n\t */\n\tpublic async root(): Promise<string> {\n\t\treturn `${this._serverInfo.name} - ${this._serverInfo.version}`;\n\t}\n\n\t/**\n\t * Get the server information.\n\t * @returns The service information.\n\t */\n\tpublic async info(): Promise<IServerInfo> {\n\t\treturn this._serverInfo;\n\t}\n\n\t/**\n\t * Get the favicon.\n\t * @returns The favicon.\n\t */\n\tpublic async favicon(): Promise<Uint8Array | undefined> {\n\t\treturn this._favicon;\n\t}\n\n\t/**\n\t * Get the OpenAPI spec.\n\t * @returns The OpenAPI spec.\n\t */\n\tpublic async spec(): Promise<unknown> {\n\t\treturn this._openApiSpec;\n\t}\n\n\t/**\n\t * Is the server live.\n\t * @returns True if the server is live.\n\t */\n\tpublic async livez(): Promise<boolean> {\n\t\tlet errorCount = 0;\n\n\t\tif (Is.arrayValue(this._healthInfo.components)) {\n\t\t\terrorCount = this._healthInfo.components.filter(c => c.status === \"error\").length;\n\t\t}\n\n\t\treturn errorCount === 0;\n\t}\n\n\t/**\n\t * Get the server health.\n\t * @returns The service health.\n\t */\n\tpublic async health(): Promise<IHealthInfo> {\n\t\tlet errorCount = 0;\n\t\tlet warningCount = 0;\n\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst tenantId = contextIds?.[ContextIdKeys.Tenant];\n\n\t\t// Filter so we only get components that are not tenant specific or match the tenant id\n\t\tconst components = this._healthInfo.components?.filter(\n\t\t\tc => Is.empty(c.tenantId) || c.tenantId === tenantId\n\t\t);\n\n\t\tif (Is.arrayValue(components)) {\n\t\t\terrorCount = components.filter(c => c.status === \"error\").length;\n\t\t\twarningCount = components.filter(c => c.status === \"warning\").length;\n\t\t}\n\n\t\tif (errorCount > 0) {\n\t\t\tthis._healthInfo.status = \"error\";\n\t\t} else if (warningCount > 0) {\n\t\t\tthis._healthInfo.status = \"warning\";\n\t\t} else {\n\t\t\tthis._healthInfo.status = \"ok\";\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: this._healthInfo.status,\n\t\t\tcomponents: components?.map(c => ({\n\t\t\t\tname: c.name,\n\t\t\t\tstatus: c.status,\n\t\t\t\tdetails: c.details\n\t\t\t}))\n\t\t};\n\t}\n\n\t/**\n\t * Set the status of a component.\n\t * @param name The component name.\n\t * @param status The status of the component.\n\t * @param details The details for the status.\n\t * @param tenantId The tenant id, optional if the health status is not tenant specific.\n\t * @returns Nothing.\n\t */\n\tpublic async setComponentHealth(\n\t\tname: string,\n\t\tstatus: HealthStatus,\n\t\tdetails?: string,\n\t\ttenantId?: string\n\t): Promise<void> {\n\t\tconst component = Is.empty(tenantId)\n\t\t\t? this._healthInfo.components?.find(c => c.name === name && Is.empty(c.tenantId))\n\t\t\t: this._healthInfo.components?.find(c => c.name === name && c.tenantId === tenantId);\n\n\t\tif (Is.undefined(component)) {\n\t\t\tthis._healthInfo.components ??= [];\n\t\t\tthis._healthInfo.components.push({\n\t\t\t\tname,\n\t\t\t\tstatus,\n\t\t\t\tdetails,\n\t\t\t\ttenantId\n\t\t\t});\n\t\t} else {\n\t\t\tcomponent.status = status;\n\t\t\tcomponent.details = details;\n\t\t}\n\t}\n\n\t/**\n\t * Remove the status of a component.\n\t * @param name The component name.\n\t * @param tenantId The tenant id, optional if the health status is not tenant specific.\n\t * @returns Nothing.\n\t */\n\tpublic async removeComponentHealth(name: string, tenantId?: string): Promise<void> {\n\t\tif (Is.arrayValue(this._healthInfo.components)) {\n\t\t\tconst componentIndex = Is.empty(tenantId)\n\t\t\t\t? this._healthInfo.components?.findIndex(c => c.name === name && Is.empty(c.tenantId))\n\t\t\t\t: this._healthInfo.components?.findIndex(c => c.name === name && c.tenantId === tenantId);\n\n\t\t\tif (componentIndex !== -1) {\n\t\t\t\tthis._healthInfo.components.splice(componentIndex, 1);\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=IInformationServiceConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IInformationServiceConfig.js","sourceRoot":"","sources":["../../../src/models/IInformationServiceConfig.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IServerInfo } from \"@twin.org/api-models\";\n\n/**\n * Configuration for the information service.\n */\nexport interface IInformationServiceConfig {\n\t/**\n\t * The server information.\n\t */\n\tserverInfo: IServerInfo;\n\n\t/**\n\t * The path to the OpenAPI Spec.\n\t */\n\topenApiSpecPath?: string;\n\n\t/**\n\t * The path to the favicon.\n\t */\n\tfavIconPath?: string;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=IInformationServiceConstructorOptions.js.map