@twin.org/api-server-fastify 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.
@@ -1,562 +0,0 @@
1
- 'use strict';
2
-
3
- var FastifyCompress = require('@fastify/compress');
4
- var FastifyCors = require('@fastify/cors');
5
- var apiModels = require('@twin.org/api-models');
6
- var apiProcessors = require('@twin.org/api-processors');
7
- var core = require('@twin.org/core');
8
- var web = require('@twin.org/web');
9
- var Fastify = require('fastify');
10
- var fp = require('fastify-plugin');
11
- var socket_io = require('socket.io');
12
-
13
- // This is a clone of fastify-socket.io which runs with recent fastify versions.
14
- const fastifySocketIO = fp(async (fastify, opts) => {
15
- const ioServer = new socket_io.Server(fastify.server, opts);
16
- fastify.decorate("io", ioServer);
17
- fastify.addHook("preClose", done => {
18
- ioServer.disconnectSockets();
19
- ioServer.close();
20
- done();
21
- });
22
- }, { fastify: ">=5.x.x", name: "socket.io" });
23
-
24
- // Copyright 2024 IOTA Stiftung.
25
- // SPDX-License-Identifier: Apache-2.0.
26
- /**
27
- * Implementation of the web server using Fastify.
28
- */
29
- class FastifyWebServer {
30
- /**
31
- * Runtime name for the class in camel case.
32
- * @internal
33
- */
34
- static _CLASS_NAME_CAMEL_CASE = core.StringHelper.camelCase("FastifyWebServer");
35
- /**
36
- * Default port for running the server.
37
- * @internal
38
- */
39
- static _DEFAULT_PORT = 3000;
40
- /**
41
- * Default host for running the server.
42
- * @internal
43
- */
44
- static _DEFAULT_HOST = "localhost";
45
- /**
46
- * Runtime name for the class.
47
- */
48
- CLASS_NAME = "FastifyWebServer";
49
- /**
50
- * The logging component type.
51
- * @internal
52
- */
53
- _loggingComponentType;
54
- /**
55
- * The logging component.
56
- * @internal
57
- */
58
- _logging;
59
- /**
60
- * The options for the server.
61
- * @internal
62
- */
63
- _options;
64
- /**
65
- * The Fastify instance.
66
- * @internal
67
- */
68
- _fastify;
69
- /**
70
- * The options for the socket server.
71
- * @internal
72
- */
73
- _socketConfig;
74
- /**
75
- * Whether the server has been started.
76
- * @internal
77
- */
78
- _started;
79
- /**
80
- * The mime type processors.
81
- * @internal
82
- */
83
- _mimeTypeProcessors;
84
- /**
85
- * Include the stack with errors.
86
- * @internal
87
- */
88
- _includeErrorStack;
89
- /**
90
- * Create a new instance of FastifyWebServer.
91
- * @param options The options for the server.
92
- */
93
- constructor(options) {
94
- this._loggingComponentType = options?.loggingComponentType;
95
- this._logging = core.ComponentFactory.getIfExists(options?.loggingComponentType ?? "logging");
96
- this._fastify = Fastify({
97
- routerOptions: {
98
- maxParamLength: 2000
99
- },
100
- ...options?.config?.web
101
- // Need this cast for now as maxParamLength has moved in to routerOptions
102
- // but the TS defs has not been updated yet
103
- });
104
- this._socketConfig = {
105
- path: "/socket",
106
- ...options?.config?.socket
107
- };
108
- this._started = false;
109
- this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
110
- const hasJsonLd = this._mimeTypeProcessors.find(processor => processor.CLASS_NAME === "json-ld");
111
- if (!hasJsonLd) {
112
- this._mimeTypeProcessors.push(new apiProcessors.JsonLdMimeTypeProcessor());
113
- }
114
- this._includeErrorStack = options?.config?.includeErrorStack ?? false;
115
- }
116
- /**
117
- * Get the web server instance.
118
- * @returns The web server instance.
119
- */
120
- getInstance() {
121
- return this._fastify;
122
- }
123
- /**
124
- * Build the server.
125
- * @param restRouteProcessors The processors for incoming requests over REST.
126
- * @param restRoutes The REST routes.
127
- * @param socketRouteProcessors The processors for incoming requests over Sockets.
128
- * @param socketRoutes The socket routes.
129
- * @param options Options for building the server.
130
- * @returns Nothing.
131
- */
132
- async build(restRouteProcessors, restRoutes, socketRouteProcessors, socketRoutes, options) {
133
- if (core.Is.arrayValue(restRoutes) && !core.Is.arrayValue(restRouteProcessors)) {
134
- throw new core.GeneralError(this.CLASS_NAME, "noRestProcessors");
135
- }
136
- if (core.Is.arrayValue(socketRoutes) && !core.Is.arrayValue(socketRouteProcessors)) {
137
- throw new core.GeneralError(this.CLASS_NAME, "noSocketProcessors");
138
- }
139
- await this._logging?.log({
140
- level: "info",
141
- ts: Date.now(),
142
- source: this.CLASS_NAME,
143
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.building`
144
- });
145
- this._options = options;
146
- await this._fastify.register(FastifyCompress);
147
- if (core.Is.arrayValue(socketRoutes)) {
148
- await this._fastify.register(fastifySocketIO, this._socketConfig);
149
- }
150
- if (core.Is.arrayValue(this._mimeTypeProcessors)) {
151
- for (const contentTypeHandler of this._mimeTypeProcessors) {
152
- this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, (request, body, done) => {
153
- // Fastify does not handle this method correctly if it is async
154
- // so we have to use the callback method
155
- contentTypeHandler
156
- .handle(body)
157
- // eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
158
- .then(processed => done(null, processed))
159
- // eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
160
- .catch(err => done(core.BaseError.fromError(err)));
161
- });
162
- }
163
- }
164
- await this.initCors(options);
165
- this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
166
- this._fastify.setErrorHandler(async (error, request, reply) => {
167
- // If code property is set this is a fastify error
168
- // otherwise it's from our framework
169
- let httpStatusCode;
170
- let err;
171
- if (core.Is.number(error.code)) {
172
- err = {
173
- source: this.CLASS_NAME,
174
- name: error.name,
175
- message: `${error.code}: ${error.message}`
176
- };
177
- httpStatusCode = error.statusCode ?? web.HttpStatusCode.badRequest;
178
- }
179
- else {
180
- const errorAndCode = apiModels.HttpErrorHelper.processError(error);
181
- err = errorAndCode.error;
182
- httpStatusCode = errorAndCode.httpStatusCode;
183
- }
184
- await this._logging?.log({
185
- level: "error",
186
- ts: Date.now(),
187
- source: this.CLASS_NAME,
188
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.badRequest`,
189
- error: err
190
- });
191
- return reply.status(httpStatusCode).send({
192
- error: err
193
- });
194
- });
195
- await this.addRoutesRest(restRouteProcessors, restRoutes);
196
- await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
197
- }
198
- /**
199
- * Start the server.
200
- * @returns Nothing.
201
- */
202
- async start() {
203
- const host = this._options?.host ?? FastifyWebServer._DEFAULT_HOST;
204
- const port = this._options?.port ?? FastifyWebServer._DEFAULT_PORT;
205
- await this._logging?.log({
206
- level: "info",
207
- ts: Date.now(),
208
- source: this.CLASS_NAME,
209
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.starting`,
210
- data: {
211
- host,
212
- port
213
- }
214
- });
215
- if (!this._started) {
216
- try {
217
- await this._fastify.listen({ port, host });
218
- const addresses = this._fastify.addresses();
219
- const protocol = core.Is.object(this._fastify.initialConfig.https) ? "https://" : "http://";
220
- await this._logging?.log({
221
- level: "info",
222
- ts: Date.now(),
223
- source: this.CLASS_NAME,
224
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.started`,
225
- data: {
226
- addresses: addresses
227
- .map(a => `${protocol}${a.family === "IPv6" ? "[" : ""}${a.address}${a.family === "IPv6" ? "]" : ""}:${a.port}`)
228
- .join(", ")
229
- }
230
- });
231
- this._started = true;
232
- }
233
- catch (err) {
234
- await this._logging?.log({
235
- level: "error",
236
- ts: Date.now(),
237
- source: this.CLASS_NAME,
238
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.startFailed`,
239
- error: core.BaseError.fromError(err)
240
- });
241
- }
242
- }
243
- }
244
- /**
245
- * Stop the server.
246
- * @returns Nothing.
247
- */
248
- async stop() {
249
- if (this._started) {
250
- this._started = false;
251
- await this._fastify.close();
252
- await this._logging?.log({
253
- level: "info",
254
- ts: Date.now(),
255
- source: this.CLASS_NAME,
256
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.stopped`
257
- });
258
- }
259
- }
260
- /**
261
- * Add the REST routes to the server.
262
- * @param restRouteProcessors The processors for the incoming requests.
263
- * @param restRoutes The REST routes to add.
264
- * @internal
265
- */
266
- async addRoutesRest(restRouteProcessors, restRoutes) {
267
- if (core.Is.arrayValue(restRouteProcessors) && core.Is.arrayValue(restRoutes)) {
268
- for (const restRoute of restRoutes) {
269
- let path = core.StringHelper.trimTrailingSlashes(restRoute.path);
270
- if (!path.startsWith("/")) {
271
- path = `/${path}`;
272
- }
273
- await this._logging?.log({
274
- level: "info",
275
- ts: Date.now(),
276
- source: this.CLASS_NAME,
277
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
278
- data: { route: path, method: restRoute.method }
279
- });
280
- const method = restRoute.method.toLowerCase();
281
- this._fastify[method](path, async (request, reply) => this.handleRequestRest(restRouteProcessors, request, reply, restRoute));
282
- }
283
- }
284
- }
285
- /**
286
- * Add the socket routes to the server.
287
- * @param socketRouteProcessors The processors for the incoming requests.
288
- * @param socketRoutes The socket routes to add.
289
- * @internal
290
- */
291
- async addRoutesSocket(socketRouteProcessors, socketRoutes) {
292
- if (core.Is.arrayValue(socketRouteProcessors) && core.Is.arrayValue(socketRoutes)) {
293
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
- const io = this._fastify.io;
295
- for (const socketRoute of socketRoutes) {
296
- const path = core.StringHelper.trimLeadingSlashes(core.StringHelper.trimTrailingSlashes(socketRoute.path));
297
- const pathParts = path.split("/");
298
- const namespace = `/${pathParts[0]}`;
299
- const topic = pathParts.slice(1).join("/");
300
- await this._logging?.log({
301
- level: "info",
302
- ts: Date.now(),
303
- source: this.CLASS_NAME,
304
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.socketRouteAdded`,
305
- data: {
306
- handshakePath: this._socketConfig.path,
307
- namespace,
308
- eventName: topic
309
- }
310
- });
311
- const socketNamespace = io.of(namespace);
312
- socketNamespace.on("connection", async (socket) => {
313
- const socketServerRequest = {
314
- method: web.HttpMethod.GET,
315
- url: socket.handshake.url,
316
- query: socket.handshake.query,
317
- headers: socket.handshake.headers,
318
- socketId: socket.id
319
- };
320
- // Pass the connected information on to any processors
321
- try {
322
- for (const socketRouteProcessor of socketRouteProcessors) {
323
- if (socketRouteProcessor.connected) {
324
- await socketRouteProcessor.connected(socketServerRequest, socketRoute, this._loggingComponentType);
325
- }
326
- }
327
- }
328
- catch (err) {
329
- const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
330
- const response = {};
331
- apiModels.HttpErrorHelper.buildResponse(response, error, httpStatusCode);
332
- socket.emit(topic, response);
333
- }
334
- socket.on("disconnect", async () => {
335
- try {
336
- // The socket disconnected so notify any processors
337
- for (const socketRouteProcessor of socketRouteProcessors) {
338
- if (socketRouteProcessor.disconnected) {
339
- await socketRouteProcessor.disconnected(socketServerRequest, socketRoute, this._loggingComponentType);
340
- }
341
- }
342
- }
343
- catch {
344
- // If something fails on a disconnect there is not much we can do with it
345
- }
346
- });
347
- // Handle any incoming messages
348
- socket.on(topic, async (data) => {
349
- await this.handleRequestSocket(socketRouteProcessors, socketRoute, socket, `/${pathParts.join("/")}`, topic, data);
350
- });
351
- });
352
- }
353
- }
354
- }
355
- /**
356
- * Handle the incoming REST request.
357
- * @param restRouteProcessors The hooks to process the incoming requests.
358
- * @param request The incoming request.
359
- * @param reply The outgoing response.
360
- * @param restRoute The REST route to handle.
361
- * @internal
362
- */
363
- async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
364
- const httpServerRequest = {
365
- method: request.method.toUpperCase(),
366
- url: `${request.protocol}://${request.hostname}${request.url}`,
367
- body: request.body,
368
- query: request.query,
369
- pathParams: request.params,
370
- headers: request.headers
371
- };
372
- const httpResponse = {};
373
- const httpRequestIdentity = {};
374
- const processorState = {};
375
- await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState);
376
- if (!core.Is.empty(httpResponse.headers)) {
377
- for (const header of Object.keys(httpResponse.headers)) {
378
- reply.header(header, httpResponse.headers[header]);
379
- }
380
- }
381
- return reply
382
- .status((httpResponse.statusCode ?? web.HttpStatusCode.ok))
383
- .send(httpResponse.body);
384
- }
385
- /**
386
- * Run the REST processors for the route.
387
- * @param restRouteProcessors The processors to run.
388
- * @param restRoute The route to process.
389
- * @param httpServerRequest The incoming request.
390
- * @param httpResponse The outgoing response.
391
- * @param httpRequestIdentity The identity context for the request.
392
- * @internal
393
- */
394
- async runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState) {
395
- try {
396
- for (const routeProcessor of restRouteProcessors) {
397
- if (routeProcessor.pre) {
398
- await routeProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState, this._loggingComponentType);
399
- }
400
- }
401
- for (const routeProcessor of restRouteProcessors) {
402
- if (routeProcessor.process) {
403
- await routeProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState, this._loggingComponentType);
404
- }
405
- }
406
- for (const routeProcessor of restRouteProcessors) {
407
- if (routeProcessor.post) {
408
- await routeProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState, this._loggingComponentType);
409
- }
410
- }
411
- }
412
- catch (err) {
413
- const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
414
- apiModels.HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
415
- }
416
- }
417
- /**
418
- * Handle the incoming socket request.
419
- * @param socketRouteProcessors The hooks to process the incoming requests.
420
- * @param socketRoute The socket route to handle.
421
- * @param socket The socket to handle.
422
- * @param fullPath The full path of the socket route.
423
- * @param emitTopic The topic to emit the response on.
424
- * @param data The incoming data.
425
- * @internal
426
- */
427
- async handleRequestSocket(socketRouteProcessors, socketRoute, socket, fullPath, emitTopic, request) {
428
- const socketServerRequest = {
429
- method: web.HttpMethod.GET,
430
- url: fullPath,
431
- query: socket.handshake.query,
432
- headers: socket.handshake.headers,
433
- body: request.body,
434
- socketId: socket.id
435
- };
436
- const httpResponse = {};
437
- const httpRequestIdentity = {};
438
- const processorState = {};
439
- delete socketServerRequest.query?.EIO;
440
- delete socketServerRequest.query?.transport;
441
- await this.runProcessorsSocket(socketRouteProcessors, socketRoute, socketServerRequest, httpResponse, httpRequestIdentity, processorState, emitTopic, async (topic, response) => {
442
- await socket.emit(topic, response);
443
- });
444
- }
445
- /**
446
- * Run the socket processors for the route.
447
- * @param socketId The id of the socket.
448
- * @param socketRouteProcessors The processors to run.
449
- * @param socketRoute The route to process.
450
- * @param socketServerRequest The incoming request.
451
- * @param httpResponse The outgoing response.
452
- * @param httpRequestIdentity The identity context for the request.
453
- * @param processorState The state handed through the processors.
454
- * @param requestTopic The topic of the request.
455
- * @internal
456
- */
457
- async runProcessorsSocket(socketRouteProcessors, socketRoute, socketServerRequest, httpResponse, httpRequestIdentity, processorState, requestTopic, responseEmitter) {
458
- // Custom emit method which will also call the post processors
459
- const postProcessEmit = async (topic, response, responseProcessorState) => {
460
- await responseEmitter(topic, response);
461
- try {
462
- // The post processors are called after the response has been emitted
463
- for (const postSocketRouteProcessor of socketRouteProcessors) {
464
- if (postSocketRouteProcessor.post) {
465
- await postSocketRouteProcessor.post(socketServerRequest, response, socketRoute, httpRequestIdentity, responseProcessorState, this._loggingComponentType);
466
- }
467
- }
468
- }
469
- catch (err) {
470
- this._logging?.log({
471
- level: "error",
472
- ts: Date.now(),
473
- source: this.CLASS_NAME,
474
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.postProcessorError`,
475
- error: core.BaseError.fromError(err),
476
- data: {
477
- route: socketRoute.path
478
- }
479
- });
480
- }
481
- };
482
- try {
483
- for (const socketRouteProcessor of socketRouteProcessors) {
484
- if (socketRouteProcessor.pre) {
485
- await socketRouteProcessor.pre(socketServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState, this._loggingComponentType);
486
- }
487
- }
488
- // We always call all the processors regardless of any response set by a previous processor.
489
- // But if a pre processor sets a status code, we will emit the response manually, as the pre
490
- // and post processors do not receive the emit method, they just populate the response object.
491
- if (!core.Is.empty(httpResponse.statusCode)) {
492
- await postProcessEmit(requestTopic, httpResponse, processorState);
493
- }
494
- for (const socketRouteProcessor of socketRouteProcessors) {
495
- if (socketRouteProcessor.process) {
496
- await socketRouteProcessor.process(socketServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState, async (topic, processResponse) => {
497
- await postProcessEmit(topic, processResponse, processorState);
498
- }, this._loggingComponentType);
499
- }
500
- }
501
- // If the processors set the status to any kind of error then we should emit this manually
502
- if (core.Is.integer(httpResponse.statusCode) &&
503
- httpResponse.statusCode >= web.HttpStatusCode.badRequest) {
504
- await postProcessEmit(requestTopic, httpResponse, processorState);
505
- }
506
- }
507
- catch (err) {
508
- // Emit any unhandled errors manually
509
- const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
510
- apiModels.HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
511
- await postProcessEmit(requestTopic, httpResponse, processorState);
512
- }
513
- }
514
- /**
515
- * Initialize the cors options.
516
- * @param options The web server options.
517
- * @internal
518
- */
519
- async initCors(options) {
520
- let origins = ["*"];
521
- if (core.Is.arrayValue(options?.corsOrigins)) {
522
- origins = options?.corsOrigins;
523
- }
524
- else if (core.Is.stringValue(options?.corsOrigins)) {
525
- origins = [options?.corsOrigins];
526
- }
527
- const hasWildcardOrigin = origins.includes("*");
528
- const methods = options?.methods ?? [
529
- web.HttpMethod.GET,
530
- web.HttpMethod.PUT,
531
- web.HttpMethod.POST,
532
- web.HttpMethod.DELETE,
533
- web.HttpMethod.OPTIONS
534
- ];
535
- const allowedHeaders = [
536
- "Access-Control-Allow-Origin",
537
- "Content-Encoding",
538
- "Accept-Encoding",
539
- web.HeaderTypes.ContentType,
540
- web.HeaderTypes.Authorization,
541
- web.HeaderTypes.Accept
542
- ];
543
- const exposedHeaders = [web.HeaderTypes.ContentDisposition, web.HeaderTypes.Location];
544
- if (core.Is.arrayValue(options?.allowedHeaders)) {
545
- allowedHeaders.push(...options.allowedHeaders);
546
- }
547
- if (core.Is.arrayValue(options?.exposedHeaders)) {
548
- exposedHeaders.push(...options.exposedHeaders);
549
- }
550
- await this._fastify.register(FastifyCors, {
551
- origin: (origin, callback) => {
552
- callback(null, hasWildcardOrigin ? true : origins.includes(origin));
553
- },
554
- methods,
555
- allowedHeaders,
556
- exposedHeaders,
557
- credentials: true
558
- });
559
- }
560
- }
561
-
562
- exports.FastifyWebServer = FastifyWebServer;