@twin.org/api-server-fastify 0.0.1-next.12 → 0.0.1-next.13

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.
@@ -7,6 +7,19 @@ var core = require('@twin.org/core');
7
7
  var loggingModels = require('@twin.org/logging-models');
8
8
  var web = require('@twin.org/web');
9
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" });
10
23
 
11
24
  // Copyright 2024 IOTA Stiftung.
12
25
  // SPDX-License-Identifier: Apache-2.0.
@@ -48,6 +61,11 @@ class FastifyWebServer {
48
61
  * @internal
49
62
  */
50
63
  _fastify;
64
+ /**
65
+ * The options for the socket server.
66
+ * @internal
67
+ */
68
+ _socketConfig;
51
69
  /**
52
70
  * Whether the server has been started.
53
71
  * @internal
@@ -58,20 +76,33 @@ class FastifyWebServer {
58
76
  * @internal
59
77
  */
60
78
  _mimeTypeProcessors;
79
+ /**
80
+ * Include the stack with errors.
81
+ * @internal
82
+ */
83
+ _includeErrorStack;
61
84
  /**
62
85
  * Create a new instance of FastifyWebServer.
63
86
  * @param options The options for the server.
64
87
  * @param options.loggingConnectorType The type of the logging connector to use, if undefined, no logging will happen.
65
- * @param options.config Additional options for the Fastify server.
66
88
  * @param options.mimeTypeProcessors Additional MIME type processors.
89
+ * @param options.config Additional configuration for the server.
67
90
  */
68
91
  constructor(options) {
69
92
  this._loggingConnector = core.Is.stringValue(options?.loggingConnectorType)
70
93
  ? loggingModels.LoggingConnectorFactory.get(options.loggingConnectorType)
71
94
  : undefined;
72
- this._fastify = Fastify({ maxParamLength: 2000, ...options?.config });
95
+ this._fastify = Fastify({
96
+ maxParamLength: 2000,
97
+ ...options?.config?.web
98
+ });
99
+ this._socketConfig = {
100
+ path: "/socket",
101
+ ...options?.config?.socket
102
+ };
73
103
  this._started = false;
74
104
  this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
105
+ this._includeErrorStack = options?.config?.includeErrorStack ?? false;
75
106
  }
76
107
  /**
77
108
  * Get the web server instance.
@@ -82,14 +113,19 @@ class FastifyWebServer {
82
113
  }
83
114
  /**
84
115
  * Build the server.
85
- * @param restRouteProcessors The hooks to process the incoming requests.
116
+ * @param restRouteProcessors The processors for incoming requests over REST.
86
117
  * @param restRoutes The REST routes.
118
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
119
+ * @param socketRoutes The socket routes.
87
120
  * @param options Options for building the server.
88
121
  * @returns Nothing.
89
122
  */
90
- async build(restRouteProcessors, restRoutes, options) {
91
- if (!core.Is.arrayValue(restRouteProcessors)) {
92
- throw new core.GeneralError(this.CLASS_NAME, "noProcessors");
123
+ async build(restRouteProcessors, restRoutes, socketRouteProcessors, socketRoutes, options) {
124
+ if (core.Is.arrayValue(restRoutes) && !core.Is.arrayValue(restRouteProcessors)) {
125
+ throw new core.GeneralError(this.CLASS_NAME, "noRestProcessors");
126
+ }
127
+ if (core.Is.arrayValue(socketRoutes) && !core.Is.arrayValue(socketRouteProcessors)) {
128
+ throw new core.GeneralError(this.CLASS_NAME, "noSocketProcessors");
93
129
  }
94
130
  await this._loggingConnector?.log({
95
131
  level: "info",
@@ -99,6 +135,9 @@ class FastifyWebServer {
99
135
  });
100
136
  this._options = options;
101
137
  await this._fastify.register(FastifyCompress);
138
+ if (core.Is.arrayValue(socketRoutes)) {
139
+ await this._fastify.register(fastifySocketIO, this._socketConfig);
140
+ }
102
141
  if (core.Is.arrayValue(this._mimeTypeProcessors)) {
103
142
  for (const contentTypeHandler of this._mimeTypeProcessors) {
104
143
  this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, async (request, body, done) => {
@@ -113,7 +152,7 @@ class FastifyWebServer {
113
152
  }
114
153
  }
115
154
  await this.initCors(options);
116
- this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply));
155
+ this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
117
156
  this._fastify.setErrorHandler(async (error, request, reply) => {
118
157
  // If code property is set this is a fastify error
119
158
  // otherwise it's from our framework
@@ -143,22 +182,8 @@ class FastifyWebServer {
143
182
  error: err
144
183
  });
145
184
  });
146
- // Add the routes to the server.
147
- for (const restRoute of restRoutes) {
148
- let path = core.StringHelper.trimTrailingSlashes(restRoute.path);
149
- if (!path.startsWith("/")) {
150
- path = `/${path}`;
151
- }
152
- await this._loggingConnector?.log({
153
- level: "info",
154
- ts: Date.now(),
155
- source: this.CLASS_NAME,
156
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
157
- data: { route: path, method: restRoute.method }
158
- });
159
- const method = restRoute.method.toLowerCase();
160
- this._fastify[method](path, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply, restRoute));
161
- }
185
+ await this.addRoutesRest(restRouteProcessors, restRoutes);
186
+ await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
162
187
  }
163
188
  /**
164
189
  * Start the server.
@@ -223,14 +248,109 @@ class FastifyWebServer {
223
248
  }
224
249
  }
225
250
  /**
226
- * Handle the incoming request.
251
+ * Add the REST routes to the server.
252
+ * @param restRouteProcessors The processors for the incoming requests.
253
+ * @param restRoutes The REST routes to add.
254
+ * @internal
255
+ */
256
+ async addRoutesRest(restRouteProcessors, restRoutes) {
257
+ if (core.Is.arrayValue(restRouteProcessors) && core.Is.arrayValue(restRoutes)) {
258
+ for (const restRoute of restRoutes) {
259
+ let path = core.StringHelper.trimTrailingSlashes(restRoute.path);
260
+ if (!path.startsWith("/")) {
261
+ path = `/${path}`;
262
+ }
263
+ await this._loggingConnector?.log({
264
+ level: "info",
265
+ ts: Date.now(),
266
+ source: this.CLASS_NAME,
267
+ message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
268
+ data: { route: path, method: restRoute.method }
269
+ });
270
+ const method = restRoute.method.toLowerCase();
271
+ this._fastify[method](path, async (request, reply) => this.handleRequestRest(restRouteProcessors, request, reply, restRoute));
272
+ }
273
+ }
274
+ }
275
+ /**
276
+ * Add the socket routes to the server.
277
+ * @param socketRouteProcessors The processors for the incoming requests.
278
+ * @param socketRoutes The socket routes to add.
279
+ * @internal
280
+ */
281
+ async addRoutesSocket(socketRouteProcessors, socketRoutes) {
282
+ if (core.Is.arrayValue(socketRouteProcessors) && core.Is.arrayValue(socketRoutes)) {
283
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
284
+ const io = this._fastify.io;
285
+ for (const socketRoute of socketRoutes) {
286
+ const path = core.StringHelper.trimLeadingSlashes(core.StringHelper.trimTrailingSlashes(socketRoute.path));
287
+ const pathParts = path.split("/");
288
+ await this._loggingConnector?.log({
289
+ level: "info",
290
+ ts: Date.now(),
291
+ source: this.CLASS_NAME,
292
+ message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.socketRouteAdded`,
293
+ data: { route: path }
294
+ });
295
+ const socketNamespace = io.of(`/${pathParts[0]}`);
296
+ const topic = pathParts.slice(1).join("/");
297
+ socketNamespace.on("connection", async (socket) => {
298
+ const httpServerRequest = {
299
+ method: web.HttpMethod.GET,
300
+ url: socket.handshake.url,
301
+ query: socket.handshake.query,
302
+ headers: socket.handshake.headers
303
+ };
304
+ // Pass the connected information on to any processors
305
+ try {
306
+ const processorState = {
307
+ socketId: socket.id
308
+ };
309
+ for (const socketRouteProcessor of socketRouteProcessors) {
310
+ if (core.Is.function(socketRouteProcessor.connected)) {
311
+ await socketRouteProcessor.connected(httpServerRequest, socketRoute, processorState);
312
+ }
313
+ }
314
+ }
315
+ catch (err) {
316
+ const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
317
+ const response = {};
318
+ apiModels.HttpErrorHelper.buildResponse(response, error, httpStatusCode);
319
+ socket.emit(topic, response);
320
+ }
321
+ socket.on("disconnect", async () => {
322
+ try {
323
+ const processorState = {
324
+ socketId: socket.id
325
+ };
326
+ // The socket disconnected so notify any processors
327
+ for (const socketRouteProcessor of socketRouteProcessors) {
328
+ if (core.Is.function(socketRouteProcessor.disconnected)) {
329
+ await socketRouteProcessor.disconnected(httpServerRequest, socketRoute, processorState);
330
+ }
331
+ }
332
+ }
333
+ catch {
334
+ // If something fails on a disconnect there is not much we can do with it
335
+ }
336
+ });
337
+ // Handle any incoming messages
338
+ socket.on(topic, async (data) => {
339
+ await this.handleRequestSocket(socketRouteProcessors, socketRoute, socket, `/${pathParts.join("/")}`, topic, data);
340
+ });
341
+ });
342
+ }
343
+ }
344
+ }
345
+ /**
346
+ * Handle the incoming REST request.
227
347
  * @param restRouteProcessors The hooks to process the incoming requests.
228
348
  * @param request The incoming request.
229
349
  * @param reply The outgoing response.
230
350
  * @param restRoute The REST route to handle.
231
351
  * @internal
232
352
  */
233
- async handleRequest(restRouteProcessors, request, reply, restRoute) {
353
+ async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
234
354
  const httpServerRequest = {
235
355
  method: request.method.toUpperCase(),
236
356
  url: `${request.protocol}://${request.hostname}${request.url}`,
@@ -242,21 +362,7 @@ class FastifyWebServer {
242
362
  const httpResponse = {};
243
363
  const httpRequestIdentity = {};
244
364
  const processorState = {};
245
- for (const restRouteProcessor of restRouteProcessors) {
246
- if (core.Is.function(restRouteProcessor.pre)) {
247
- await restRouteProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
248
- }
249
- }
250
- for (const restRouteProcessor of restRouteProcessors) {
251
- if (core.Is.function(restRouteProcessor.process)) {
252
- await restRouteProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
253
- }
254
- }
255
- for (const restRouteProcessor of restRouteProcessors) {
256
- if (core.Is.function(restRouteProcessor.post)) {
257
- await restRouteProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
258
- }
259
- }
365
+ await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState);
260
366
  if (!core.Is.empty(httpResponse.headers)) {
261
367
  for (const header of Object.keys(httpResponse.headers)) {
262
368
  reply.header(header, httpResponse.headers[header]);
@@ -266,6 +372,119 @@ class FastifyWebServer {
266
372
  .status((httpResponse.statusCode ?? web.HttpStatusCode.ok))
267
373
  .send(httpResponse.body);
268
374
  }
375
+ /**
376
+ * Run the REST processors for the route.
377
+ * @param restRouteProcessors The processors to run.
378
+ * @param restRoute The route to process.
379
+ * @param httpServerRequest The incoming request.
380
+ * @param httpResponse The outgoing response.
381
+ * @param httpRequestIdentity The identity context for the request.
382
+ * @internal
383
+ */
384
+ async runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState) {
385
+ try {
386
+ for (const routeProcessor of restRouteProcessors) {
387
+ if (core.Is.function(routeProcessor.pre)) {
388
+ await routeProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
389
+ }
390
+ }
391
+ for (const routeProcessor of restRouteProcessors) {
392
+ if (core.Is.function(routeProcessor.process)) {
393
+ await routeProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
394
+ }
395
+ }
396
+ for (const routeProcessor of restRouteProcessors) {
397
+ if (core.Is.function(routeProcessor.post)) {
398
+ await routeProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
399
+ }
400
+ }
401
+ }
402
+ catch (err) {
403
+ const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
404
+ apiModels.HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
405
+ }
406
+ }
407
+ /**
408
+ * Handle the incoming socket request.
409
+ * @param socketRouteProcessors The hooks to process the incoming requests.
410
+ * @param socketRoute The socket route to handle.
411
+ * @param socket The socket to handle.
412
+ * @param fullPath The full path of the socket route.
413
+ * @param emitTopic The topic to emit the response on.
414
+ * @param data The incoming data.
415
+ * @internal
416
+ */
417
+ async handleRequestSocket(socketRouteProcessors, socketRoute, socket, fullPath, emitTopic, data) {
418
+ const httpServerRequest = {
419
+ method: web.HttpMethod.GET,
420
+ url: fullPath,
421
+ query: socket.handshake.query,
422
+ headers: socket.handshake.headers
423
+ };
424
+ const httpResponse = {};
425
+ const httpRequestIdentity = {};
426
+ const processorState = {
427
+ socketId: socket.id
428
+ };
429
+ delete httpServerRequest.query?.EIO;
430
+ delete httpServerRequest.query?.transport;
431
+ httpServerRequest.body = data;
432
+ await this.runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, async () => {
433
+ await socket.emit(emitTopic, httpResponse);
434
+ });
435
+ }
436
+ /**
437
+ * Run the socket processors for the route.
438
+ * @param socketRouteProcessors The processors to run.
439
+ * @param socketRoute The route to process.
440
+ * @param httpServerRequest The incoming request.
441
+ * @param httpResponse The outgoing response.
442
+ * @param httpRequestIdentity The identity context for the request.
443
+ * @internal
444
+ */
445
+ async runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, responseEmitter) {
446
+ // Custom emit method which will also call the post processors
447
+ const postProcessEmit = async (response, responseProcessorState) => {
448
+ await responseEmitter(response);
449
+ // The post processors are called after the response has been emitted
450
+ for (const postSocketRouteProcessor of socketRouteProcessors) {
451
+ if (core.Is.function(postSocketRouteProcessor.post)) {
452
+ await postSocketRouteProcessor.post(httpServerRequest, response, socketRoute, httpRequestIdentity, responseProcessorState);
453
+ }
454
+ }
455
+ };
456
+ try {
457
+ for (const socketRouteProcessor of socketRouteProcessors) {
458
+ if (core.Is.function(socketRouteProcessor.pre)) {
459
+ await socketRouteProcessor.pre(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState);
460
+ }
461
+ }
462
+ // We always call all the processors regardless of any response set by a previous processor.
463
+ // But if a pre processor sets a status code, we will emit the response manually, as the pre
464
+ // and post processors do not receive the emit method, they just populate the response object.
465
+ if (!core.Is.empty(httpResponse.statusCode)) {
466
+ await postProcessEmit(httpResponse, processorState);
467
+ }
468
+ for (const socketRouteProcessor of socketRouteProcessors) {
469
+ if (core.Is.function(socketRouteProcessor.process)) {
470
+ await socketRouteProcessor.process(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState, async (processResponse) => {
471
+ await postProcessEmit(processResponse, processorState);
472
+ });
473
+ }
474
+ }
475
+ // If the processors set the status to any kind of error then we should emit this manually
476
+ if (core.Is.integer(httpResponse.statusCode) &&
477
+ httpResponse.statusCode >= web.HttpStatusCode.badRequest) {
478
+ await postProcessEmit(httpResponse, processorState);
479
+ }
480
+ }
481
+ catch (err) {
482
+ // Emit any unhandled errors manually
483
+ const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
484
+ apiModels.HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
485
+ await postProcessEmit(httpResponse, processorState);
486
+ }
487
+ }
269
488
  /**
270
489
  * Initialize the cors options.
271
490
  * @param options The web server options.
@@ -5,6 +5,19 @@ import { StringHelper, Is, GeneralError, BaseError } from '@twin.org/core';
5
5
  import { LoggingConnectorFactory } from '@twin.org/logging-models';
6
6
  import { HttpStatusCode, HttpMethod, HeaderTypes } from '@twin.org/web';
7
7
  import Fastify from 'fastify';
8
+ import fp from 'fastify-plugin';
9
+ import { Server } from 'socket.io';
10
+
11
+ // This is a clone of fastify-socket.io which runs with recent fastify versions.
12
+ const fastifySocketIO = fp(async (fastify, opts) => {
13
+ const ioServer = new Server(fastify.server, opts);
14
+ fastify.decorate("io", ioServer);
15
+ fastify.addHook("preClose", done => {
16
+ ioServer.disconnectSockets();
17
+ ioServer.close();
18
+ done();
19
+ });
20
+ }, { fastify: ">=5.x.x", name: "socket.io" });
8
21
 
9
22
  // Copyright 2024 IOTA Stiftung.
10
23
  // SPDX-License-Identifier: Apache-2.0.
@@ -46,6 +59,11 @@ class FastifyWebServer {
46
59
  * @internal
47
60
  */
48
61
  _fastify;
62
+ /**
63
+ * The options for the socket server.
64
+ * @internal
65
+ */
66
+ _socketConfig;
49
67
  /**
50
68
  * Whether the server has been started.
51
69
  * @internal
@@ -56,20 +74,33 @@ class FastifyWebServer {
56
74
  * @internal
57
75
  */
58
76
  _mimeTypeProcessors;
77
+ /**
78
+ * Include the stack with errors.
79
+ * @internal
80
+ */
81
+ _includeErrorStack;
59
82
  /**
60
83
  * Create a new instance of FastifyWebServer.
61
84
  * @param options The options for the server.
62
85
  * @param options.loggingConnectorType The type of the logging connector to use, if undefined, no logging will happen.
63
- * @param options.config Additional options for the Fastify server.
64
86
  * @param options.mimeTypeProcessors Additional MIME type processors.
87
+ * @param options.config Additional configuration for the server.
65
88
  */
66
89
  constructor(options) {
67
90
  this._loggingConnector = Is.stringValue(options?.loggingConnectorType)
68
91
  ? LoggingConnectorFactory.get(options.loggingConnectorType)
69
92
  : undefined;
70
- this._fastify = Fastify({ maxParamLength: 2000, ...options?.config });
93
+ this._fastify = Fastify({
94
+ maxParamLength: 2000,
95
+ ...options?.config?.web
96
+ });
97
+ this._socketConfig = {
98
+ path: "/socket",
99
+ ...options?.config?.socket
100
+ };
71
101
  this._started = false;
72
102
  this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
103
+ this._includeErrorStack = options?.config?.includeErrorStack ?? false;
73
104
  }
74
105
  /**
75
106
  * Get the web server instance.
@@ -80,14 +111,19 @@ class FastifyWebServer {
80
111
  }
81
112
  /**
82
113
  * Build the server.
83
- * @param restRouteProcessors The hooks to process the incoming requests.
114
+ * @param restRouteProcessors The processors for incoming requests over REST.
84
115
  * @param restRoutes The REST routes.
116
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
117
+ * @param socketRoutes The socket routes.
85
118
  * @param options Options for building the server.
86
119
  * @returns Nothing.
87
120
  */
88
- async build(restRouteProcessors, restRoutes, options) {
89
- if (!Is.arrayValue(restRouteProcessors)) {
90
- throw new GeneralError(this.CLASS_NAME, "noProcessors");
121
+ async build(restRouteProcessors, restRoutes, socketRouteProcessors, socketRoutes, options) {
122
+ if (Is.arrayValue(restRoutes) && !Is.arrayValue(restRouteProcessors)) {
123
+ throw new GeneralError(this.CLASS_NAME, "noRestProcessors");
124
+ }
125
+ if (Is.arrayValue(socketRoutes) && !Is.arrayValue(socketRouteProcessors)) {
126
+ throw new GeneralError(this.CLASS_NAME, "noSocketProcessors");
91
127
  }
92
128
  await this._loggingConnector?.log({
93
129
  level: "info",
@@ -97,6 +133,9 @@ class FastifyWebServer {
97
133
  });
98
134
  this._options = options;
99
135
  await this._fastify.register(FastifyCompress);
136
+ if (Is.arrayValue(socketRoutes)) {
137
+ await this._fastify.register(fastifySocketIO, this._socketConfig);
138
+ }
100
139
  if (Is.arrayValue(this._mimeTypeProcessors)) {
101
140
  for (const contentTypeHandler of this._mimeTypeProcessors) {
102
141
  this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, async (request, body, done) => {
@@ -111,7 +150,7 @@ class FastifyWebServer {
111
150
  }
112
151
  }
113
152
  await this.initCors(options);
114
- this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply));
153
+ this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
115
154
  this._fastify.setErrorHandler(async (error, request, reply) => {
116
155
  // If code property is set this is a fastify error
117
156
  // otherwise it's from our framework
@@ -141,22 +180,8 @@ class FastifyWebServer {
141
180
  error: err
142
181
  });
143
182
  });
144
- // Add the routes to the server.
145
- for (const restRoute of restRoutes) {
146
- let path = StringHelper.trimTrailingSlashes(restRoute.path);
147
- if (!path.startsWith("/")) {
148
- path = `/${path}`;
149
- }
150
- await this._loggingConnector?.log({
151
- level: "info",
152
- ts: Date.now(),
153
- source: this.CLASS_NAME,
154
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
155
- data: { route: path, method: restRoute.method }
156
- });
157
- const method = restRoute.method.toLowerCase();
158
- this._fastify[method](path, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply, restRoute));
159
- }
183
+ await this.addRoutesRest(restRouteProcessors, restRoutes);
184
+ await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
160
185
  }
161
186
  /**
162
187
  * Start the server.
@@ -221,14 +246,109 @@ class FastifyWebServer {
221
246
  }
222
247
  }
223
248
  /**
224
- * Handle the incoming request.
249
+ * Add the REST routes to the server.
250
+ * @param restRouteProcessors The processors for the incoming requests.
251
+ * @param restRoutes The REST routes to add.
252
+ * @internal
253
+ */
254
+ async addRoutesRest(restRouteProcessors, restRoutes) {
255
+ if (Is.arrayValue(restRouteProcessors) && Is.arrayValue(restRoutes)) {
256
+ for (const restRoute of restRoutes) {
257
+ let path = StringHelper.trimTrailingSlashes(restRoute.path);
258
+ if (!path.startsWith("/")) {
259
+ path = `/${path}`;
260
+ }
261
+ await this._loggingConnector?.log({
262
+ level: "info",
263
+ ts: Date.now(),
264
+ source: this.CLASS_NAME,
265
+ message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
266
+ data: { route: path, method: restRoute.method }
267
+ });
268
+ const method = restRoute.method.toLowerCase();
269
+ this._fastify[method](path, async (request, reply) => this.handleRequestRest(restRouteProcessors, request, reply, restRoute));
270
+ }
271
+ }
272
+ }
273
+ /**
274
+ * Add the socket routes to the server.
275
+ * @param socketRouteProcessors The processors for the incoming requests.
276
+ * @param socketRoutes The socket routes to add.
277
+ * @internal
278
+ */
279
+ async addRoutesSocket(socketRouteProcessors, socketRoutes) {
280
+ if (Is.arrayValue(socketRouteProcessors) && Is.arrayValue(socketRoutes)) {
281
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
282
+ const io = this._fastify.io;
283
+ for (const socketRoute of socketRoutes) {
284
+ const path = StringHelper.trimLeadingSlashes(StringHelper.trimTrailingSlashes(socketRoute.path));
285
+ const pathParts = path.split("/");
286
+ await this._loggingConnector?.log({
287
+ level: "info",
288
+ ts: Date.now(),
289
+ source: this.CLASS_NAME,
290
+ message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.socketRouteAdded`,
291
+ data: { route: path }
292
+ });
293
+ const socketNamespace = io.of(`/${pathParts[0]}`);
294
+ const topic = pathParts.slice(1).join("/");
295
+ socketNamespace.on("connection", async (socket) => {
296
+ const httpServerRequest = {
297
+ method: HttpMethod.GET,
298
+ url: socket.handshake.url,
299
+ query: socket.handshake.query,
300
+ headers: socket.handshake.headers
301
+ };
302
+ // Pass the connected information on to any processors
303
+ try {
304
+ const processorState = {
305
+ socketId: socket.id
306
+ };
307
+ for (const socketRouteProcessor of socketRouteProcessors) {
308
+ if (Is.function(socketRouteProcessor.connected)) {
309
+ await socketRouteProcessor.connected(httpServerRequest, socketRoute, processorState);
310
+ }
311
+ }
312
+ }
313
+ catch (err) {
314
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
315
+ const response = {};
316
+ HttpErrorHelper.buildResponse(response, error, httpStatusCode);
317
+ socket.emit(topic, response);
318
+ }
319
+ socket.on("disconnect", async () => {
320
+ try {
321
+ const processorState = {
322
+ socketId: socket.id
323
+ };
324
+ // The socket disconnected so notify any processors
325
+ for (const socketRouteProcessor of socketRouteProcessors) {
326
+ if (Is.function(socketRouteProcessor.disconnected)) {
327
+ await socketRouteProcessor.disconnected(httpServerRequest, socketRoute, processorState);
328
+ }
329
+ }
330
+ }
331
+ catch {
332
+ // If something fails on a disconnect there is not much we can do with it
333
+ }
334
+ });
335
+ // Handle any incoming messages
336
+ socket.on(topic, async (data) => {
337
+ await this.handleRequestSocket(socketRouteProcessors, socketRoute, socket, `/${pathParts.join("/")}`, topic, data);
338
+ });
339
+ });
340
+ }
341
+ }
342
+ }
343
+ /**
344
+ * Handle the incoming REST request.
225
345
  * @param restRouteProcessors The hooks to process the incoming requests.
226
346
  * @param request The incoming request.
227
347
  * @param reply The outgoing response.
228
348
  * @param restRoute The REST route to handle.
229
349
  * @internal
230
350
  */
231
- async handleRequest(restRouteProcessors, request, reply, restRoute) {
351
+ async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
232
352
  const httpServerRequest = {
233
353
  method: request.method.toUpperCase(),
234
354
  url: `${request.protocol}://${request.hostname}${request.url}`,
@@ -240,21 +360,7 @@ class FastifyWebServer {
240
360
  const httpResponse = {};
241
361
  const httpRequestIdentity = {};
242
362
  const processorState = {};
243
- for (const restRouteProcessor of restRouteProcessors) {
244
- if (Is.function(restRouteProcessor.pre)) {
245
- await restRouteProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
246
- }
247
- }
248
- for (const restRouteProcessor of restRouteProcessors) {
249
- if (Is.function(restRouteProcessor.process)) {
250
- await restRouteProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
251
- }
252
- }
253
- for (const restRouteProcessor of restRouteProcessors) {
254
- if (Is.function(restRouteProcessor.post)) {
255
- await restRouteProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
256
- }
257
- }
363
+ await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState);
258
364
  if (!Is.empty(httpResponse.headers)) {
259
365
  for (const header of Object.keys(httpResponse.headers)) {
260
366
  reply.header(header, httpResponse.headers[header]);
@@ -264,6 +370,119 @@ class FastifyWebServer {
264
370
  .status((httpResponse.statusCode ?? HttpStatusCode.ok))
265
371
  .send(httpResponse.body);
266
372
  }
373
+ /**
374
+ * Run the REST processors for the route.
375
+ * @param restRouteProcessors The processors to run.
376
+ * @param restRoute The route to process.
377
+ * @param httpServerRequest The incoming request.
378
+ * @param httpResponse The outgoing response.
379
+ * @param httpRequestIdentity The identity context for the request.
380
+ * @internal
381
+ */
382
+ async runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState) {
383
+ try {
384
+ for (const routeProcessor of restRouteProcessors) {
385
+ if (Is.function(routeProcessor.pre)) {
386
+ await routeProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
387
+ }
388
+ }
389
+ for (const routeProcessor of restRouteProcessors) {
390
+ if (Is.function(routeProcessor.process)) {
391
+ await routeProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
392
+ }
393
+ }
394
+ for (const routeProcessor of restRouteProcessors) {
395
+ if (Is.function(routeProcessor.post)) {
396
+ await routeProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
397
+ }
398
+ }
399
+ }
400
+ catch (err) {
401
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
402
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
403
+ }
404
+ }
405
+ /**
406
+ * Handle the incoming socket request.
407
+ * @param socketRouteProcessors The hooks to process the incoming requests.
408
+ * @param socketRoute The socket route to handle.
409
+ * @param socket The socket to handle.
410
+ * @param fullPath The full path of the socket route.
411
+ * @param emitTopic The topic to emit the response on.
412
+ * @param data The incoming data.
413
+ * @internal
414
+ */
415
+ async handleRequestSocket(socketRouteProcessors, socketRoute, socket, fullPath, emitTopic, data) {
416
+ const httpServerRequest = {
417
+ method: HttpMethod.GET,
418
+ url: fullPath,
419
+ query: socket.handshake.query,
420
+ headers: socket.handshake.headers
421
+ };
422
+ const httpResponse = {};
423
+ const httpRequestIdentity = {};
424
+ const processorState = {
425
+ socketId: socket.id
426
+ };
427
+ delete httpServerRequest.query?.EIO;
428
+ delete httpServerRequest.query?.transport;
429
+ httpServerRequest.body = data;
430
+ await this.runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, async () => {
431
+ await socket.emit(emitTopic, httpResponse);
432
+ });
433
+ }
434
+ /**
435
+ * Run the socket processors for the route.
436
+ * @param socketRouteProcessors The processors to run.
437
+ * @param socketRoute The route to process.
438
+ * @param httpServerRequest The incoming request.
439
+ * @param httpResponse The outgoing response.
440
+ * @param httpRequestIdentity The identity context for the request.
441
+ * @internal
442
+ */
443
+ async runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, responseEmitter) {
444
+ // Custom emit method which will also call the post processors
445
+ const postProcessEmit = async (response, responseProcessorState) => {
446
+ await responseEmitter(response);
447
+ // The post processors are called after the response has been emitted
448
+ for (const postSocketRouteProcessor of socketRouteProcessors) {
449
+ if (Is.function(postSocketRouteProcessor.post)) {
450
+ await postSocketRouteProcessor.post(httpServerRequest, response, socketRoute, httpRequestIdentity, responseProcessorState);
451
+ }
452
+ }
453
+ };
454
+ try {
455
+ for (const socketRouteProcessor of socketRouteProcessors) {
456
+ if (Is.function(socketRouteProcessor.pre)) {
457
+ await socketRouteProcessor.pre(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState);
458
+ }
459
+ }
460
+ // We always call all the processors regardless of any response set by a previous processor.
461
+ // But if a pre processor sets a status code, we will emit the response manually, as the pre
462
+ // and post processors do not receive the emit method, they just populate the response object.
463
+ if (!Is.empty(httpResponse.statusCode)) {
464
+ await postProcessEmit(httpResponse, processorState);
465
+ }
466
+ for (const socketRouteProcessor of socketRouteProcessors) {
467
+ if (Is.function(socketRouteProcessor.process)) {
468
+ await socketRouteProcessor.process(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState, async (processResponse) => {
469
+ await postProcessEmit(processResponse, processorState);
470
+ });
471
+ }
472
+ }
473
+ // If the processors set the status to any kind of error then we should emit this manually
474
+ if (Is.integer(httpResponse.statusCode) &&
475
+ httpResponse.statusCode >= HttpStatusCode.badRequest) {
476
+ await postProcessEmit(httpResponse, processorState);
477
+ }
478
+ }
479
+ catch (err) {
480
+ // Emit any unhandled errors manually
481
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
482
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
483
+ await postProcessEmit(httpResponse, processorState);
484
+ }
485
+ }
267
486
  /**
268
487
  * Initialize the cors options.
269
488
  * @param options The web server options.
@@ -0,0 +1,4 @@
1
+ import type { FastifyPluginAsync } from "fastify";
2
+ import { type ServerOptions } from "socket.io";
3
+ declare const fastifySocketIO: FastifyPluginAsync<Partial<ServerOptions>>;
4
+ export default fastifySocketIO;
@@ -1,5 +1,6 @@
1
- import { type IHttpRestRouteProcessor, type IMimeTypeProcessor, type IRestRoute, type IWebServer, type IWebServerOptions } from "@twin.org/api-models";
2
- import { type FastifyInstance, type FastifyServerOptions } from "fastify";
1
+ import { type IMimeTypeProcessor, type IRestRoute, type IRestRouteProcessor, type ISocketRoute, type ISocketRouteProcessor, type IWebServer, type IWebServerOptions } from "@twin.org/api-models";
2
+ import { type FastifyInstance } from "fastify";
3
+ import type { IFastifyWebServerConfig } from "./models/IFastifyWebServerConfig";
3
4
  /**
4
5
  * Implementation of the web server using Fastify.
5
6
  */
@@ -12,12 +13,12 @@ export declare class FastifyWebServer implements IWebServer<FastifyInstance> {
12
13
  * Create a new instance of FastifyWebServer.
13
14
  * @param options The options for the server.
14
15
  * @param options.loggingConnectorType The type of the logging connector to use, if undefined, no logging will happen.
15
- * @param options.config Additional options for the Fastify server.
16
16
  * @param options.mimeTypeProcessors Additional MIME type processors.
17
+ * @param options.config Additional configuration for the server.
17
18
  */
18
19
  constructor(options?: {
19
20
  loggingConnectorType?: string;
20
- config?: FastifyServerOptions;
21
+ config?: IFastifyWebServerConfig;
21
22
  mimeTypeProcessors?: IMimeTypeProcessor[];
22
23
  });
23
24
  /**
@@ -27,12 +28,14 @@ export declare class FastifyWebServer implements IWebServer<FastifyInstance> {
27
28
  getInstance(): FastifyInstance;
28
29
  /**
29
30
  * Build the server.
30
- * @param restRouteProcessors The hooks to process the incoming requests.
31
+ * @param restRouteProcessors The processors for incoming requests over REST.
31
32
  * @param restRoutes The REST routes.
33
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
34
+ * @param socketRoutes The socket routes.
32
35
  * @param options Options for building the server.
33
36
  * @returns Nothing.
34
37
  */
35
- build(restRouteProcessors: IHttpRestRouteProcessor[], restRoutes: IRestRoute[], options?: IWebServerOptions): Promise<void>;
38
+ build(restRouteProcessors?: IRestRouteProcessor[], restRoutes?: IRestRoute[], socketRouteProcessors?: ISocketRouteProcessor[], socketRoutes?: ISocketRoute[], options?: IWebServerOptions): Promise<void>;
36
39
  /**
37
40
  * Start the server.
38
41
  * @returns Nothing.
@@ -1 +1,2 @@
1
1
  export * from "./fastifyWebServer";
2
+ export * from "./models/IFastifyWebServerConfig";
@@ -0,0 +1,19 @@
1
+ import type { FastifyServerOptions } from "fastify";
2
+ import type { ServerOptions } from "socket.io";
3
+ /**
4
+ * The configuration for the Fastify web server.
5
+ */
6
+ export interface IFastifyWebServerConfig {
7
+ /**
8
+ * The web server options.
9
+ */
10
+ web?: Partial<FastifyServerOptions>;
11
+ /**
12
+ * The socket server options.
13
+ */
14
+ socket?: Partial<ServerOptions>;
15
+ /**
16
+ * Include the stack with errors.
17
+ */
18
+ includeErrorStack?: boolean;
19
+ }
package/docs/changelog.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # @twin.org/api-server-fastify - Changelog
2
2
 
3
- ## v0.0.1-next.12
3
+ ## v0.0.1-next.13
4
4
 
5
5
  - Initial Release
@@ -24,9 +24,9 @@ The options for the server.
24
24
 
25
25
  The type of the logging connector to use, if undefined, no logging will happen.
26
26
 
27
- • **options.config?**: `FastifyServerOptions`
27
+ • **options.config?**: [`IFastifyWebServerConfig`](../interfaces/IFastifyWebServerConfig.md)
28
28
 
29
- Additional options for the Fastify server.
29
+ Additional configuration for the server.
30
30
 
31
31
  • **options.mimeTypeProcessors?**: `IMimeTypeProcessor`[]
32
32
 
@@ -66,20 +66,28 @@ The web server instance.
66
66
 
67
67
  ### build()
68
68
 
69
- > **build**(`restRouteProcessors`, `restRoutes`, `options`?): `Promise`\<`void`\>
69
+ > **build**(`restRouteProcessors`?, `restRoutes`?, `socketRouteProcessors`?, `socketRoutes`?, `options`?): `Promise`\<`void`\>
70
70
 
71
71
  Build the server.
72
72
 
73
73
  #### Parameters
74
74
 
75
- • **restRouteProcessors**: `IHttpRestRouteProcessor`[]
75
+ • **restRouteProcessors?**: `IRestRouteProcessor`[]
76
76
 
77
- The hooks to process the incoming requests.
77
+ The processors for incoming requests over REST.
78
78
 
79
- • **restRoutes**: `IRestRoute`\<`any`, `any`\>[]
79
+ • **restRoutes?**: `IRestRoute`\<`any`, `any`\>[]
80
80
 
81
81
  The REST routes.
82
82
 
83
+ • **socketRouteProcessors?**: `ISocketRouteProcessor`[]
84
+
85
+ The processors for incoming requests over Sockets.
86
+
87
+ • **socketRoutes?**: `ISocketRoute`\<`any`, `any`\>[]
88
+
89
+ The socket routes.
90
+
83
91
  • **options?**: `IWebServerOptions`
84
92
 
85
93
  Options for building the server.
@@ -3,3 +3,7 @@
3
3
  ## Classes
4
4
 
5
5
  - [FastifyWebServer](classes/FastifyWebServer.md)
6
+
7
+ ## Interfaces
8
+
9
+ - [IFastifyWebServerConfig](interfaces/IFastifyWebServerConfig.md)
@@ -0,0 +1,27 @@
1
+ # Interface: IFastifyWebServerConfig
2
+
3
+ The configuration for the Fastify web server.
4
+
5
+ ## Properties
6
+
7
+ ### web?
8
+
9
+ > `optional` **web**: `Partial`\<`FastifyServerOptions`\>
10
+
11
+ The web server options.
12
+
13
+ ***
14
+
15
+ ### socket?
16
+
17
+ > `optional` **socket**: `Partial`\<`ServerOptions`\>
18
+
19
+ The socket server options.
20
+
21
+ ***
22
+
23
+ ### includeErrorStack?
24
+
25
+ > `optional` **includeErrorStack**: `boolean`
26
+
27
+ Include the stack with errors.
package/locales/en.json CHANGED
@@ -7,6 +7,8 @@
7
7
  "stopped": "The Web Server was stopped",
8
8
  "badRequest": "The web server could not handle the request",
9
9
  "restRouteAdded": "Added REST route \"{route}\" \"{method}\"",
10
- "noProcessors": "You must configure at least one processor"
10
+ "socketRouteAdded": "Added socket route \"{route}\"",
11
+ "noRestProcessors": "You must configure at least one REST processor",
12
+ "noSocketProcessors": "You must configure at least one socket processor"
11
13
  }
12
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/api-server-fastify",
3
- "version": "0.0.1-next.12",
3
+ "version": "0.0.1-next.13",
4
4
  "description": "Use Fastify as the core web server for APIs",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,13 +16,14 @@
16
16
  "dependencies": {
17
17
  "@fastify/compress": "8.0.1",
18
18
  "@fastify/cors": "10.0.1",
19
- "@twin.org/api-core": "0.0.1-next.12",
20
- "@twin.org/api-models": "0.0.1-next.12",
19
+ "@twin.org/api-core": "0.0.1-next.13",
20
+ "@twin.org/api-models": "0.0.1-next.13",
21
21
  "@twin.org/core": "next",
22
22
  "@twin.org/logging-models": "next",
23
23
  "@twin.org/nameof": "next",
24
24
  "@twin.org/web": "next",
25
- "fastify": "5.0.0"
25
+ "fastify": "5.1.0",
26
+ "socket.io": "4.8.1"
26
27
  },
27
28
  "main": "./dist/cjs/index.cjs",
28
29
  "module": "./dist/esm/index.mjs",