@twin.org/api-server-fastify 0.0.1-next.11 → 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,23 +61,48 @@ 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
54
72
  */
55
73
  _started;
74
+ /**
75
+ * The mime type processors.
76
+ * @internal
77
+ */
78
+ _mimeTypeProcessors;
79
+ /**
80
+ * Include the stack with errors.
81
+ * @internal
82
+ */
83
+ _includeErrorStack;
56
84
  /**
57
85
  * Create a new instance of FastifyWebServer.
58
86
  * @param options The options for the server.
59
87
  * @param options.loggingConnectorType The type of the logging connector to use, if undefined, no logging will happen.
60
- * @param options.config Additional options for the Fastify server.
88
+ * @param options.mimeTypeProcessors Additional MIME type processors.
89
+ * @param options.config Additional configuration for the server.
61
90
  */
62
91
  constructor(options) {
63
92
  this._loggingConnector = core.Is.stringValue(options?.loggingConnectorType)
64
93
  ? loggingModels.LoggingConnectorFactory.get(options.loggingConnectorType)
65
94
  : undefined;
66
- 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
+ };
67
103
  this._started = false;
104
+ this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
105
+ this._includeErrorStack = options?.config?.includeErrorStack ?? false;
68
106
  }
69
107
  /**
70
108
  * Get the web server instance.
@@ -75,14 +113,19 @@ class FastifyWebServer {
75
113
  }
76
114
  /**
77
115
  * Build the server.
78
- * @param restRouteProcessors The hooks to process the incoming requests.
116
+ * @param restRouteProcessors The processors for incoming requests over REST.
79
117
  * @param restRoutes The REST routes.
118
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
119
+ * @param socketRoutes The socket routes.
80
120
  * @param options Options for building the server.
81
121
  * @returns Nothing.
82
122
  */
83
- async build(restRouteProcessors, restRoutes, options) {
84
- if (!core.Is.arrayValue(restRouteProcessors)) {
85
- 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");
86
129
  }
87
130
  await this._loggingConnector?.log({
88
131
  level: "info",
@@ -92,8 +135,24 @@ class FastifyWebServer {
92
135
  });
93
136
  this._options = options;
94
137
  await this._fastify.register(FastifyCompress);
138
+ if (core.Is.arrayValue(socketRoutes)) {
139
+ await this._fastify.register(fastifySocketIO, this._socketConfig);
140
+ }
141
+ if (core.Is.arrayValue(this._mimeTypeProcessors)) {
142
+ for (const contentTypeHandler of this._mimeTypeProcessors) {
143
+ this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, async (request, body, done) => {
144
+ try {
145
+ const processed = await contentTypeHandler.handle(body);
146
+ done(null, processed);
147
+ }
148
+ catch (err) {
149
+ done(core.BaseError.fromError(err));
150
+ }
151
+ });
152
+ }
153
+ }
95
154
  await this.initCors(options);
96
- this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply));
155
+ this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
97
156
  this._fastify.setErrorHandler(async (error, request, reply) => {
98
157
  // If code property is set this is a fastify error
99
158
  // otherwise it's from our framework
@@ -123,22 +182,8 @@ class FastifyWebServer {
123
182
  error: err
124
183
  });
125
184
  });
126
- // Add the routes to the server.
127
- for (const restRoute of restRoutes) {
128
- let path = core.StringHelper.trimTrailingSlashes(restRoute.path);
129
- if (!path.startsWith("/")) {
130
- path = `/${path}`;
131
- }
132
- await this._loggingConnector?.log({
133
- level: "info",
134
- ts: Date.now(),
135
- source: this.CLASS_NAME,
136
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
137
- data: { route: path, method: restRoute.method }
138
- });
139
- const method = restRoute.method.toLowerCase();
140
- this._fastify[method](path, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply, restRoute));
141
- }
185
+ await this.addRoutesRest(restRouteProcessors, restRoutes);
186
+ await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
142
187
  }
143
188
  /**
144
189
  * Start the server.
@@ -203,14 +248,109 @@ class FastifyWebServer {
203
248
  }
204
249
  }
205
250
  /**
206
- * 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.
207
347
  * @param restRouteProcessors The hooks to process the incoming requests.
208
348
  * @param request The incoming request.
209
349
  * @param reply The outgoing response.
210
350
  * @param restRoute The REST route to handle.
211
351
  * @internal
212
352
  */
213
- async handleRequest(restRouteProcessors, request, reply, restRoute) {
353
+ async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
214
354
  const httpServerRequest = {
215
355
  method: request.method.toUpperCase(),
216
356
  url: `${request.protocol}://${request.hostname}${request.url}`,
@@ -222,21 +362,7 @@ class FastifyWebServer {
222
362
  const httpResponse = {};
223
363
  const httpRequestIdentity = {};
224
364
  const processorState = {};
225
- for (const restRouteProcessor of restRouteProcessors) {
226
- if (core.Is.function(restRouteProcessor.pre)) {
227
- await restRouteProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
228
- }
229
- }
230
- for (const restRouteProcessor of restRouteProcessors) {
231
- if (core.Is.function(restRouteProcessor.process)) {
232
- await restRouteProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
233
- }
234
- }
235
- for (const restRouteProcessor of restRouteProcessors) {
236
- if (core.Is.function(restRouteProcessor.post)) {
237
- await restRouteProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
238
- }
239
- }
365
+ await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState);
240
366
  if (!core.Is.empty(httpResponse.headers)) {
241
367
  for (const header of Object.keys(httpResponse.headers)) {
242
368
  reply.header(header, httpResponse.headers[header]);
@@ -246,6 +372,119 @@ class FastifyWebServer {
246
372
  .status((httpResponse.statusCode ?? web.HttpStatusCode.ok))
247
373
  .send(httpResponse.body);
248
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
+ }
249
488
  /**
250
489
  * Initialize the cors options.
251
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,23 +59,48 @@ 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
52
70
  */
53
71
  _started;
72
+ /**
73
+ * The mime type processors.
74
+ * @internal
75
+ */
76
+ _mimeTypeProcessors;
77
+ /**
78
+ * Include the stack with errors.
79
+ * @internal
80
+ */
81
+ _includeErrorStack;
54
82
  /**
55
83
  * Create a new instance of FastifyWebServer.
56
84
  * @param options The options for the server.
57
85
  * @param options.loggingConnectorType The type of the logging connector to use, if undefined, no logging will happen.
58
- * @param options.config Additional options for the Fastify server.
86
+ * @param options.mimeTypeProcessors Additional MIME type processors.
87
+ * @param options.config Additional configuration for the server.
59
88
  */
60
89
  constructor(options) {
61
90
  this._loggingConnector = Is.stringValue(options?.loggingConnectorType)
62
91
  ? LoggingConnectorFactory.get(options.loggingConnectorType)
63
92
  : undefined;
64
- 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
+ };
65
101
  this._started = false;
102
+ this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
103
+ this._includeErrorStack = options?.config?.includeErrorStack ?? false;
66
104
  }
67
105
  /**
68
106
  * Get the web server instance.
@@ -73,14 +111,19 @@ class FastifyWebServer {
73
111
  }
74
112
  /**
75
113
  * Build the server.
76
- * @param restRouteProcessors The hooks to process the incoming requests.
114
+ * @param restRouteProcessors The processors for incoming requests over REST.
77
115
  * @param restRoutes The REST routes.
116
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
117
+ * @param socketRoutes The socket routes.
78
118
  * @param options Options for building the server.
79
119
  * @returns Nothing.
80
120
  */
81
- async build(restRouteProcessors, restRoutes, options) {
82
- if (!Is.arrayValue(restRouteProcessors)) {
83
- 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");
84
127
  }
85
128
  await this._loggingConnector?.log({
86
129
  level: "info",
@@ -90,8 +133,24 @@ class FastifyWebServer {
90
133
  });
91
134
  this._options = options;
92
135
  await this._fastify.register(FastifyCompress);
136
+ if (Is.arrayValue(socketRoutes)) {
137
+ await this._fastify.register(fastifySocketIO, this._socketConfig);
138
+ }
139
+ if (Is.arrayValue(this._mimeTypeProcessors)) {
140
+ for (const contentTypeHandler of this._mimeTypeProcessors) {
141
+ this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, async (request, body, done) => {
142
+ try {
143
+ const processed = await contentTypeHandler.handle(body);
144
+ done(null, processed);
145
+ }
146
+ catch (err) {
147
+ done(BaseError.fromError(err));
148
+ }
149
+ });
150
+ }
151
+ }
93
152
  await this.initCors(options);
94
- this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply));
153
+ this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
95
154
  this._fastify.setErrorHandler(async (error, request, reply) => {
96
155
  // If code property is set this is a fastify error
97
156
  // otherwise it's from our framework
@@ -121,22 +180,8 @@ class FastifyWebServer {
121
180
  error: err
122
181
  });
123
182
  });
124
- // Add the routes to the server.
125
- for (const restRoute of restRoutes) {
126
- let path = StringHelper.trimTrailingSlashes(restRoute.path);
127
- if (!path.startsWith("/")) {
128
- path = `/${path}`;
129
- }
130
- await this._loggingConnector?.log({
131
- level: "info",
132
- ts: Date.now(),
133
- source: this.CLASS_NAME,
134
- message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
135
- data: { route: path, method: restRoute.method }
136
- });
137
- const method = restRoute.method.toLowerCase();
138
- this._fastify[method](path, async (request, reply) => this.handleRequest(restRouteProcessors, request, reply, restRoute));
139
- }
183
+ await this.addRoutesRest(restRouteProcessors, restRoutes);
184
+ await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
140
185
  }
141
186
  /**
142
187
  * Start the server.
@@ -201,14 +246,109 @@ class FastifyWebServer {
201
246
  }
202
247
  }
203
248
  /**
204
- * 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.
205
345
  * @param restRouteProcessors The hooks to process the incoming requests.
206
346
  * @param request The incoming request.
207
347
  * @param reply The outgoing response.
208
348
  * @param restRoute The REST route to handle.
209
349
  * @internal
210
350
  */
211
- async handleRequest(restRouteProcessors, request, reply, restRoute) {
351
+ async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
212
352
  const httpServerRequest = {
213
353
  method: request.method.toUpperCase(),
214
354
  url: `${request.protocol}://${request.hostname}${request.url}`,
@@ -220,21 +360,7 @@ class FastifyWebServer {
220
360
  const httpResponse = {};
221
361
  const httpRequestIdentity = {};
222
362
  const processorState = {};
223
- for (const restRouteProcessor of restRouteProcessors) {
224
- if (Is.function(restRouteProcessor.pre)) {
225
- await restRouteProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
226
- }
227
- }
228
- for (const restRouteProcessor of restRouteProcessors) {
229
- if (Is.function(restRouteProcessor.process)) {
230
- await restRouteProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
231
- }
232
- }
233
- for (const restRouteProcessor of restRouteProcessors) {
234
- if (Is.function(restRouteProcessor.post)) {
235
- await restRouteProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
236
- }
237
- }
363
+ await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState);
238
364
  if (!Is.empty(httpResponse.headers)) {
239
365
  for (const header of Object.keys(httpResponse.headers)) {
240
366
  reply.header(header, httpResponse.headers[header]);
@@ -244,6 +370,119 @@ class FastifyWebServer {
244
370
  .status((httpResponse.statusCode ?? HttpStatusCode.ok))
245
371
  .send(httpResponse.body);
246
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
+ }
247
486
  /**
248
487
  * Initialize the cors options.
249
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 IRestRoute, type IWebServer, type IWebServerOptions } from "@twin.org/api-models";
2
- import { type FastifyServerOptions, type FastifyInstance } 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,11 +13,13 @@ 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
+ * @param options.mimeTypeProcessors Additional MIME type processors.
17
+ * @param options.config Additional configuration for the server.
16
18
  */
17
19
  constructor(options?: {
18
20
  loggingConnectorType?: string;
19
- config?: FastifyServerOptions;
21
+ config?: IFastifyWebServerConfig;
22
+ mimeTypeProcessors?: IMimeTypeProcessor[];
20
23
  });
21
24
  /**
22
25
  * Get the web server instance.
@@ -25,12 +28,14 @@ export declare class FastifyWebServer implements IWebServer<FastifyInstance> {
25
28
  getInstance(): FastifyInstance;
26
29
  /**
27
30
  * Build the server.
28
- * @param restRouteProcessors The hooks to process the incoming requests.
31
+ * @param restRouteProcessors The processors for incoming requests over REST.
29
32
  * @param restRoutes The REST routes.
33
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
34
+ * @param socketRoutes The socket routes.
30
35
  * @param options Options for building the server.
31
36
  * @returns Nothing.
32
37
  */
33
- build(restRouteProcessors: IHttpRestRouteProcessor[], restRoutes: IRestRoute[], options?: IWebServerOptions): Promise<void>;
38
+ build(restRouteProcessors?: IRestRouteProcessor[], restRoutes?: IRestRoute[], socketRouteProcessors?: ISocketRouteProcessor[], socketRoutes?: ISocketRoute[], options?: IWebServerOptions): Promise<void>;
34
39
  /**
35
40
  * Start the server.
36
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.11
3
+ ## v0.0.1-next.13
4
4
 
5
5
  - Initial Release
@@ -24,9 +24,13 @@ 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
+
31
+ • **options.mimeTypeProcessors?**: `IMimeTypeProcessor`[]
32
+
33
+ Additional MIME type processors.
30
34
 
31
35
  #### Returns
32
36
 
@@ -62,20 +66,28 @@ The web server instance.
62
66
 
63
67
  ### build()
64
68
 
65
- > **build**(`restRouteProcessors`, `restRoutes`, `options`?): `Promise`\<`void`\>
69
+ > **build**(`restRouteProcessors`?, `restRoutes`?, `socketRouteProcessors`?, `socketRoutes`?, `options`?): `Promise`\<`void`\>
66
70
 
67
71
  Build the server.
68
72
 
69
73
  #### Parameters
70
74
 
71
- • **restRouteProcessors**: `IHttpRestRouteProcessor`[]
75
+ • **restRouteProcessors?**: `IRestRouteProcessor`[]
72
76
 
73
- The hooks to process the incoming requests.
77
+ The processors for incoming requests over REST.
74
78
 
75
- • **restRoutes**: `IRestRoute`\<`any`, `any`\>[]
79
+ • **restRoutes?**: `IRestRoute`\<`any`, `any`\>[]
76
80
 
77
81
  The REST routes.
78
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
+
79
91
  • **options?**: `IWebServerOptions`
80
92
 
81
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.11",
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.11",
20
- "@twin.org/api-models": "0.0.1-next.11",
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",