@twin.org/api-server-fastify 0.0.1-next.8 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +294 -41
- package/dist/esm/index.mjs +294 -41
- package/dist/types/fastifySocketIo.d.ts +4 -0
- package/dist/types/fastifyWebServer.d.ts +8 -10
- package/dist/types/index.d.ts +2 -0
- package/dist/types/models/IFastifyWebServerConfig.d.ts +19 -0
- package/dist/types/models/IFastifyWebServerConstructorOptions.d.ts +19 -0
- package/docs/changelog.md +103 -1
- package/docs/reference/classes/FastifyWebServer.md +31 -19
- package/docs/reference/index.md +5 -0
- package/docs/reference/interfaces/IFastifyWebServerConfig.md +27 -0
- package/docs/reference/interfaces/IFastifyWebServerConstructorOptions.md +27 -0
- package/locales/en.json +4 -1
- package/package.json +13 -12
package/dist/cjs/index.cjs
CHANGED
|
@@ -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,45 @@ 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
|
-
* @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.
|
|
61
87
|
*/
|
|
62
88
|
constructor(options) {
|
|
63
89
|
this._loggingConnector = core.Is.stringValue(options?.loggingConnectorType)
|
|
64
90
|
? loggingModels.LoggingConnectorFactory.get(options.loggingConnectorType)
|
|
65
91
|
: undefined;
|
|
66
|
-
this._fastify = Fastify({
|
|
92
|
+
this._fastify = Fastify({
|
|
93
|
+
maxParamLength: 2000,
|
|
94
|
+
...options?.config?.web
|
|
95
|
+
});
|
|
96
|
+
this._socketConfig = {
|
|
97
|
+
path: "/socket",
|
|
98
|
+
...options?.config?.socket
|
|
99
|
+
};
|
|
67
100
|
this._started = false;
|
|
101
|
+
this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
|
|
102
|
+
this._includeErrorStack = options?.config?.includeErrorStack ?? false;
|
|
68
103
|
}
|
|
69
104
|
/**
|
|
70
105
|
* Get the web server instance.
|
|
@@ -75,14 +110,19 @@ class FastifyWebServer {
|
|
|
75
110
|
}
|
|
76
111
|
/**
|
|
77
112
|
* Build the server.
|
|
78
|
-
* @param restRouteProcessors The
|
|
113
|
+
* @param restRouteProcessors The processors for incoming requests over REST.
|
|
79
114
|
* @param restRoutes The REST routes.
|
|
115
|
+
* @param socketRouteProcessors The processors for incoming requests over Sockets.
|
|
116
|
+
* @param socketRoutes The socket routes.
|
|
80
117
|
* @param options Options for building the server.
|
|
81
118
|
* @returns Nothing.
|
|
82
119
|
*/
|
|
83
|
-
async build(restRouteProcessors, restRoutes, options) {
|
|
84
|
-
if (!core.Is.arrayValue(restRouteProcessors)) {
|
|
85
|
-
throw new core.GeneralError(this.CLASS_NAME, "
|
|
120
|
+
async build(restRouteProcessors, restRoutes, socketRouteProcessors, socketRoutes, options) {
|
|
121
|
+
if (core.Is.arrayValue(restRoutes) && !core.Is.arrayValue(restRouteProcessors)) {
|
|
122
|
+
throw new core.GeneralError(this.CLASS_NAME, "noRestProcessors");
|
|
123
|
+
}
|
|
124
|
+
if (core.Is.arrayValue(socketRoutes) && !core.Is.arrayValue(socketRouteProcessors)) {
|
|
125
|
+
throw new core.GeneralError(this.CLASS_NAME, "noSocketProcessors");
|
|
86
126
|
}
|
|
87
127
|
await this._loggingConnector?.log({
|
|
88
128
|
level: "info",
|
|
@@ -92,8 +132,25 @@ class FastifyWebServer {
|
|
|
92
132
|
});
|
|
93
133
|
this._options = options;
|
|
94
134
|
await this._fastify.register(FastifyCompress);
|
|
135
|
+
if (core.Is.arrayValue(socketRoutes)) {
|
|
136
|
+
await this._fastify.register(fastifySocketIO, this._socketConfig);
|
|
137
|
+
}
|
|
138
|
+
if (core.Is.arrayValue(this._mimeTypeProcessors)) {
|
|
139
|
+
for (const contentTypeHandler of this._mimeTypeProcessors) {
|
|
140
|
+
this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, (request, body, done) => {
|
|
141
|
+
// Fastify does not handle this method correctly if it is async
|
|
142
|
+
// so we have to use the callback method
|
|
143
|
+
contentTypeHandler
|
|
144
|
+
.handle(body)
|
|
145
|
+
// eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
|
|
146
|
+
.then(processed => done(null, processed))
|
|
147
|
+
// eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
|
|
148
|
+
.catch(err => done(core.BaseError.fromError(err)));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
95
152
|
await this.initCors(options);
|
|
96
|
-
this._fastify.setNotFoundHandler({}, async (request, reply) => this.
|
|
153
|
+
this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
|
|
97
154
|
this._fastify.setErrorHandler(async (error, request, reply) => {
|
|
98
155
|
// If code property is set this is a fastify error
|
|
99
156
|
// otherwise it's from our framework
|
|
@@ -123,22 +180,8 @@ class FastifyWebServer {
|
|
|
123
180
|
error: err
|
|
124
181
|
});
|
|
125
182
|
});
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
}
|
|
183
|
+
await this.addRoutesRest(restRouteProcessors, restRoutes);
|
|
184
|
+
await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
|
|
142
185
|
}
|
|
143
186
|
/**
|
|
144
187
|
* Start the server.
|
|
@@ -203,14 +246,109 @@ class FastifyWebServer {
|
|
|
203
246
|
}
|
|
204
247
|
}
|
|
205
248
|
/**
|
|
206
|
-
*
|
|
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 (core.Is.arrayValue(restRouteProcessors) && core.Is.arrayValue(restRoutes)) {
|
|
256
|
+
for (const restRoute of restRoutes) {
|
|
257
|
+
let path = core.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 (core.Is.arrayValue(socketRouteProcessors) && core.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 = core.StringHelper.trimLeadingSlashes(core.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: web.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 (core.Is.function(socketRouteProcessor.connected)) {
|
|
309
|
+
await socketRouteProcessor.connected(httpServerRequest, socketRoute, processorState);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
|
|
315
|
+
const response = {};
|
|
316
|
+
apiModels.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 (core.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.
|
|
207
345
|
* @param restRouteProcessors The hooks to process the incoming requests.
|
|
208
346
|
* @param request The incoming request.
|
|
209
347
|
* @param reply The outgoing response.
|
|
210
348
|
* @param restRoute The REST route to handle.
|
|
211
349
|
* @internal
|
|
212
350
|
*/
|
|
213
|
-
async
|
|
351
|
+
async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
|
|
214
352
|
const httpServerRequest = {
|
|
215
353
|
method: request.method.toUpperCase(),
|
|
216
354
|
url: `${request.protocol}://${request.hostname}${request.url}`,
|
|
@@ -222,21 +360,7 @@ class FastifyWebServer {
|
|
|
222
360
|
const httpResponse = {};
|
|
223
361
|
const httpRequestIdentity = {};
|
|
224
362
|
const processorState = {};
|
|
225
|
-
|
|
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
|
-
}
|
|
363
|
+
await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState);
|
|
240
364
|
if (!core.Is.empty(httpResponse.headers)) {
|
|
241
365
|
for (const header of Object.keys(httpResponse.headers)) {
|
|
242
366
|
reply.header(header, httpResponse.headers[header]);
|
|
@@ -246,6 +370,135 @@ class FastifyWebServer {
|
|
|
246
370
|
.status((httpResponse.statusCode ?? web.HttpStatusCode.ok))
|
|
247
371
|
.send(httpResponse.body);
|
|
248
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 (core.Is.function(routeProcessor.pre)) {
|
|
386
|
+
await routeProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const routeProcessor of restRouteProcessors) {
|
|
390
|
+
if (core.Is.function(routeProcessor.process)) {
|
|
391
|
+
await routeProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const routeProcessor of restRouteProcessors) {
|
|
395
|
+
if (core.Is.function(routeProcessor.post)) {
|
|
396
|
+
await routeProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
|
|
402
|
+
apiModels.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, request) {
|
|
416
|
+
const httpServerRequest = {
|
|
417
|
+
method: web.HttpMethod.GET,
|
|
418
|
+
url: fullPath,
|
|
419
|
+
query: socket.handshake.query,
|
|
420
|
+
headers: socket.handshake.headers,
|
|
421
|
+
body: request.body
|
|
422
|
+
};
|
|
423
|
+
const httpResponse = {};
|
|
424
|
+
const httpRequestIdentity = {};
|
|
425
|
+
const processorState = {
|
|
426
|
+
socketId: socket.id
|
|
427
|
+
};
|
|
428
|
+
delete httpServerRequest.query?.EIO;
|
|
429
|
+
delete httpServerRequest.query?.transport;
|
|
430
|
+
await this.runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, emitTopic, async (topic, response) => {
|
|
431
|
+
await socket.emit(topic, response);
|
|
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
|
+
* @param processorState The state handed through the processors.
|
|
442
|
+
* @param requestTopic The topic of the request.
|
|
443
|
+
* @internal
|
|
444
|
+
*/
|
|
445
|
+
async runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, requestTopic, responseEmitter) {
|
|
446
|
+
// Custom emit method which will also call the post processors
|
|
447
|
+
const postProcessEmit = async (topic, response, responseProcessorState) => {
|
|
448
|
+
await responseEmitter(topic, response);
|
|
449
|
+
try {
|
|
450
|
+
// The post processors are called after the response has been emitted
|
|
451
|
+
for (const postSocketRouteProcessor of socketRouteProcessors) {
|
|
452
|
+
if (core.Is.function(postSocketRouteProcessor.post)) {
|
|
453
|
+
await postSocketRouteProcessor.post(httpServerRequest, response, socketRoute, httpRequestIdentity, responseProcessorState);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
this._loggingConnector?.log({
|
|
459
|
+
level: "error",
|
|
460
|
+
ts: Date.now(),
|
|
461
|
+
source: this.CLASS_NAME,
|
|
462
|
+
message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.postProcessorError`,
|
|
463
|
+
error: core.BaseError.fromError(err),
|
|
464
|
+
data: {
|
|
465
|
+
route: socketRoute.path
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
try {
|
|
471
|
+
for (const socketRouteProcessor of socketRouteProcessors) {
|
|
472
|
+
if (core.Is.function(socketRouteProcessor.pre)) {
|
|
473
|
+
await socketRouteProcessor.pre(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// We always call all the processors regardless of any response set by a previous processor.
|
|
477
|
+
// But if a pre processor sets a status code, we will emit the response manually, as the pre
|
|
478
|
+
// and post processors do not receive the emit method, they just populate the response object.
|
|
479
|
+
if (!core.Is.empty(httpResponse.statusCode)) {
|
|
480
|
+
await postProcessEmit(requestTopic, httpResponse, processorState);
|
|
481
|
+
}
|
|
482
|
+
for (const socketRouteProcessor of socketRouteProcessors) {
|
|
483
|
+
if (core.Is.function(socketRouteProcessor.process)) {
|
|
484
|
+
await socketRouteProcessor.process(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState, async (topic, processResponse) => {
|
|
485
|
+
await postProcessEmit(topic, processResponse, processorState);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// If the processors set the status to any kind of error then we should emit this manually
|
|
490
|
+
if (core.Is.integer(httpResponse.statusCode) &&
|
|
491
|
+
httpResponse.statusCode >= web.HttpStatusCode.badRequest) {
|
|
492
|
+
await postProcessEmit(requestTopic, httpResponse, processorState);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
// Emit any unhandled errors manually
|
|
497
|
+
const { error, httpStatusCode } = apiModels.HttpErrorHelper.processError(err, this._includeErrorStack);
|
|
498
|
+
apiModels.HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
|
|
499
|
+
await postProcessEmit(requestTopic, httpResponse, processorState);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
249
502
|
/**
|
|
250
503
|
* Initialize the cors options.
|
|
251
504
|
* @param options The web server options.
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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,45 @@ 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
|
-
* @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.
|
|
59
85
|
*/
|
|
60
86
|
constructor(options) {
|
|
61
87
|
this._loggingConnector = Is.stringValue(options?.loggingConnectorType)
|
|
62
88
|
? LoggingConnectorFactory.get(options.loggingConnectorType)
|
|
63
89
|
: undefined;
|
|
64
|
-
this._fastify = Fastify({
|
|
90
|
+
this._fastify = Fastify({
|
|
91
|
+
maxParamLength: 2000,
|
|
92
|
+
...options?.config?.web
|
|
93
|
+
});
|
|
94
|
+
this._socketConfig = {
|
|
95
|
+
path: "/socket",
|
|
96
|
+
...options?.config?.socket
|
|
97
|
+
};
|
|
65
98
|
this._started = false;
|
|
99
|
+
this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
|
|
100
|
+
this._includeErrorStack = options?.config?.includeErrorStack ?? false;
|
|
66
101
|
}
|
|
67
102
|
/**
|
|
68
103
|
* Get the web server instance.
|
|
@@ -73,14 +108,19 @@ class FastifyWebServer {
|
|
|
73
108
|
}
|
|
74
109
|
/**
|
|
75
110
|
* Build the server.
|
|
76
|
-
* @param restRouteProcessors The
|
|
111
|
+
* @param restRouteProcessors The processors for incoming requests over REST.
|
|
77
112
|
* @param restRoutes The REST routes.
|
|
113
|
+
* @param socketRouteProcessors The processors for incoming requests over Sockets.
|
|
114
|
+
* @param socketRoutes The socket routes.
|
|
78
115
|
* @param options Options for building the server.
|
|
79
116
|
* @returns Nothing.
|
|
80
117
|
*/
|
|
81
|
-
async build(restRouteProcessors, restRoutes, options) {
|
|
82
|
-
if (!Is.arrayValue(restRouteProcessors)) {
|
|
83
|
-
throw new GeneralError(this.CLASS_NAME, "
|
|
118
|
+
async build(restRouteProcessors, restRoutes, socketRouteProcessors, socketRoutes, options) {
|
|
119
|
+
if (Is.arrayValue(restRoutes) && !Is.arrayValue(restRouteProcessors)) {
|
|
120
|
+
throw new GeneralError(this.CLASS_NAME, "noRestProcessors");
|
|
121
|
+
}
|
|
122
|
+
if (Is.arrayValue(socketRoutes) && !Is.arrayValue(socketRouteProcessors)) {
|
|
123
|
+
throw new GeneralError(this.CLASS_NAME, "noSocketProcessors");
|
|
84
124
|
}
|
|
85
125
|
await this._loggingConnector?.log({
|
|
86
126
|
level: "info",
|
|
@@ -90,8 +130,25 @@ class FastifyWebServer {
|
|
|
90
130
|
});
|
|
91
131
|
this._options = options;
|
|
92
132
|
await this._fastify.register(FastifyCompress);
|
|
133
|
+
if (Is.arrayValue(socketRoutes)) {
|
|
134
|
+
await this._fastify.register(fastifySocketIO, this._socketConfig);
|
|
135
|
+
}
|
|
136
|
+
if (Is.arrayValue(this._mimeTypeProcessors)) {
|
|
137
|
+
for (const contentTypeHandler of this._mimeTypeProcessors) {
|
|
138
|
+
this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, (request, body, done) => {
|
|
139
|
+
// Fastify does not handle this method correctly if it is async
|
|
140
|
+
// so we have to use the callback method
|
|
141
|
+
contentTypeHandler
|
|
142
|
+
.handle(body)
|
|
143
|
+
// eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
|
|
144
|
+
.then(processed => done(null, processed))
|
|
145
|
+
// eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
|
|
146
|
+
.catch(err => done(BaseError.fromError(err)));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
93
150
|
await this.initCors(options);
|
|
94
|
-
this._fastify.setNotFoundHandler({}, async (request, reply) => this.
|
|
151
|
+
this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
|
|
95
152
|
this._fastify.setErrorHandler(async (error, request, reply) => {
|
|
96
153
|
// If code property is set this is a fastify error
|
|
97
154
|
// otherwise it's from our framework
|
|
@@ -121,22 +178,8 @@ class FastifyWebServer {
|
|
|
121
178
|
error: err
|
|
122
179
|
});
|
|
123
180
|
});
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
}
|
|
181
|
+
await this.addRoutesRest(restRouteProcessors, restRoutes);
|
|
182
|
+
await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
|
|
140
183
|
}
|
|
141
184
|
/**
|
|
142
185
|
* Start the server.
|
|
@@ -201,14 +244,109 @@ class FastifyWebServer {
|
|
|
201
244
|
}
|
|
202
245
|
}
|
|
203
246
|
/**
|
|
204
|
-
*
|
|
247
|
+
* Add the REST routes to the server.
|
|
248
|
+
* @param restRouteProcessors The processors for the incoming requests.
|
|
249
|
+
* @param restRoutes The REST routes to add.
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
async addRoutesRest(restRouteProcessors, restRoutes) {
|
|
253
|
+
if (Is.arrayValue(restRouteProcessors) && Is.arrayValue(restRoutes)) {
|
|
254
|
+
for (const restRoute of restRoutes) {
|
|
255
|
+
let path = StringHelper.trimTrailingSlashes(restRoute.path);
|
|
256
|
+
if (!path.startsWith("/")) {
|
|
257
|
+
path = `/${path}`;
|
|
258
|
+
}
|
|
259
|
+
await this._loggingConnector?.log({
|
|
260
|
+
level: "info",
|
|
261
|
+
ts: Date.now(),
|
|
262
|
+
source: this.CLASS_NAME,
|
|
263
|
+
message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.restRouteAdded`,
|
|
264
|
+
data: { route: path, method: restRoute.method }
|
|
265
|
+
});
|
|
266
|
+
const method = restRoute.method.toLowerCase();
|
|
267
|
+
this._fastify[method](path, async (request, reply) => this.handleRequestRest(restRouteProcessors, request, reply, restRoute));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Add the socket routes to the server.
|
|
273
|
+
* @param socketRouteProcessors The processors for the incoming requests.
|
|
274
|
+
* @param socketRoutes The socket routes to add.
|
|
275
|
+
* @internal
|
|
276
|
+
*/
|
|
277
|
+
async addRoutesSocket(socketRouteProcessors, socketRoutes) {
|
|
278
|
+
if (Is.arrayValue(socketRouteProcessors) && Is.arrayValue(socketRoutes)) {
|
|
279
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
280
|
+
const io = this._fastify.io;
|
|
281
|
+
for (const socketRoute of socketRoutes) {
|
|
282
|
+
const path = StringHelper.trimLeadingSlashes(StringHelper.trimTrailingSlashes(socketRoute.path));
|
|
283
|
+
const pathParts = path.split("/");
|
|
284
|
+
await this._loggingConnector?.log({
|
|
285
|
+
level: "info",
|
|
286
|
+
ts: Date.now(),
|
|
287
|
+
source: this.CLASS_NAME,
|
|
288
|
+
message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.socketRouteAdded`,
|
|
289
|
+
data: { route: `/${path}` }
|
|
290
|
+
});
|
|
291
|
+
const socketNamespace = io.of(`/${pathParts[0]}`);
|
|
292
|
+
const topic = pathParts.slice(1).join("/");
|
|
293
|
+
socketNamespace.on("connection", async (socket) => {
|
|
294
|
+
const httpServerRequest = {
|
|
295
|
+
method: HttpMethod.GET,
|
|
296
|
+
url: socket.handshake.url,
|
|
297
|
+
query: socket.handshake.query,
|
|
298
|
+
headers: socket.handshake.headers
|
|
299
|
+
};
|
|
300
|
+
// Pass the connected information on to any processors
|
|
301
|
+
try {
|
|
302
|
+
const processorState = {
|
|
303
|
+
socketId: socket.id
|
|
304
|
+
};
|
|
305
|
+
for (const socketRouteProcessor of socketRouteProcessors) {
|
|
306
|
+
if (Is.function(socketRouteProcessor.connected)) {
|
|
307
|
+
await socketRouteProcessor.connected(httpServerRequest, socketRoute, processorState);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
|
|
313
|
+
const response = {};
|
|
314
|
+
HttpErrorHelper.buildResponse(response, error, httpStatusCode);
|
|
315
|
+
socket.emit(topic, response);
|
|
316
|
+
}
|
|
317
|
+
socket.on("disconnect", async () => {
|
|
318
|
+
try {
|
|
319
|
+
const processorState = {
|
|
320
|
+
socketId: socket.id
|
|
321
|
+
};
|
|
322
|
+
// The socket disconnected so notify any processors
|
|
323
|
+
for (const socketRouteProcessor of socketRouteProcessors) {
|
|
324
|
+
if (Is.function(socketRouteProcessor.disconnected)) {
|
|
325
|
+
await socketRouteProcessor.disconnected(httpServerRequest, socketRoute, processorState);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// If something fails on a disconnect there is not much we can do with it
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
// Handle any incoming messages
|
|
334
|
+
socket.on(topic, async (data) => {
|
|
335
|
+
await this.handleRequestSocket(socketRouteProcessors, socketRoute, socket, `/${pathParts.join("/")}`, topic, data);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Handle the incoming REST request.
|
|
205
343
|
* @param restRouteProcessors The hooks to process the incoming requests.
|
|
206
344
|
* @param request The incoming request.
|
|
207
345
|
* @param reply The outgoing response.
|
|
208
346
|
* @param restRoute The REST route to handle.
|
|
209
347
|
* @internal
|
|
210
348
|
*/
|
|
211
|
-
async
|
|
349
|
+
async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
|
|
212
350
|
const httpServerRequest = {
|
|
213
351
|
method: request.method.toUpperCase(),
|
|
214
352
|
url: `${request.protocol}://${request.hostname}${request.url}`,
|
|
@@ -220,21 +358,7 @@ class FastifyWebServer {
|
|
|
220
358
|
const httpResponse = {};
|
|
221
359
|
const httpRequestIdentity = {};
|
|
222
360
|
const processorState = {};
|
|
223
|
-
|
|
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
|
-
}
|
|
361
|
+
await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState);
|
|
238
362
|
if (!Is.empty(httpResponse.headers)) {
|
|
239
363
|
for (const header of Object.keys(httpResponse.headers)) {
|
|
240
364
|
reply.header(header, httpResponse.headers[header]);
|
|
@@ -244,6 +368,135 @@ class FastifyWebServer {
|
|
|
244
368
|
.status((httpResponse.statusCode ?? HttpStatusCode.ok))
|
|
245
369
|
.send(httpResponse.body);
|
|
246
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Run the REST processors for the route.
|
|
373
|
+
* @param restRouteProcessors The processors to run.
|
|
374
|
+
* @param restRoute The route to process.
|
|
375
|
+
* @param httpServerRequest The incoming request.
|
|
376
|
+
* @param httpResponse The outgoing response.
|
|
377
|
+
* @param httpRequestIdentity The identity context for the request.
|
|
378
|
+
* @internal
|
|
379
|
+
*/
|
|
380
|
+
async runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState) {
|
|
381
|
+
try {
|
|
382
|
+
for (const routeProcessor of restRouteProcessors) {
|
|
383
|
+
if (Is.function(routeProcessor.pre)) {
|
|
384
|
+
await routeProcessor.pre(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const routeProcessor of restRouteProcessors) {
|
|
388
|
+
if (Is.function(routeProcessor.process)) {
|
|
389
|
+
await routeProcessor.process(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const routeProcessor of restRouteProcessors) {
|
|
393
|
+
if (Is.function(routeProcessor.post)) {
|
|
394
|
+
await routeProcessor.post(httpServerRequest, httpResponse, restRoute, httpRequestIdentity, processorState);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
|
|
400
|
+
HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Handle the incoming socket request.
|
|
405
|
+
* @param socketRouteProcessors The hooks to process the incoming requests.
|
|
406
|
+
* @param socketRoute The socket route to handle.
|
|
407
|
+
* @param socket The socket to handle.
|
|
408
|
+
* @param fullPath The full path of the socket route.
|
|
409
|
+
* @param emitTopic The topic to emit the response on.
|
|
410
|
+
* @param data The incoming data.
|
|
411
|
+
* @internal
|
|
412
|
+
*/
|
|
413
|
+
async handleRequestSocket(socketRouteProcessors, socketRoute, socket, fullPath, emitTopic, request) {
|
|
414
|
+
const httpServerRequest = {
|
|
415
|
+
method: HttpMethod.GET,
|
|
416
|
+
url: fullPath,
|
|
417
|
+
query: socket.handshake.query,
|
|
418
|
+
headers: socket.handshake.headers,
|
|
419
|
+
body: request.body
|
|
420
|
+
};
|
|
421
|
+
const httpResponse = {};
|
|
422
|
+
const httpRequestIdentity = {};
|
|
423
|
+
const processorState = {
|
|
424
|
+
socketId: socket.id
|
|
425
|
+
};
|
|
426
|
+
delete httpServerRequest.query?.EIO;
|
|
427
|
+
delete httpServerRequest.query?.transport;
|
|
428
|
+
await this.runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, emitTopic, async (topic, response) => {
|
|
429
|
+
await socket.emit(topic, response);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Run the socket processors for the route.
|
|
434
|
+
* @param socketRouteProcessors The processors to run.
|
|
435
|
+
* @param socketRoute The route to process.
|
|
436
|
+
* @param httpServerRequest The incoming request.
|
|
437
|
+
* @param httpResponse The outgoing response.
|
|
438
|
+
* @param httpRequestIdentity The identity context for the request.
|
|
439
|
+
* @param processorState The state handed through the processors.
|
|
440
|
+
* @param requestTopic The topic of the request.
|
|
441
|
+
* @internal
|
|
442
|
+
*/
|
|
443
|
+
async runProcessorsSocket(socketRouteProcessors, socketRoute, httpServerRequest, httpResponse, httpRequestIdentity, processorState, requestTopic, responseEmitter) {
|
|
444
|
+
// Custom emit method which will also call the post processors
|
|
445
|
+
const postProcessEmit = async (topic, response, responseProcessorState) => {
|
|
446
|
+
await responseEmitter(topic, response);
|
|
447
|
+
try {
|
|
448
|
+
// The post processors are called after the response has been emitted
|
|
449
|
+
for (const postSocketRouteProcessor of socketRouteProcessors) {
|
|
450
|
+
if (Is.function(postSocketRouteProcessor.post)) {
|
|
451
|
+
await postSocketRouteProcessor.post(httpServerRequest, response, socketRoute, httpRequestIdentity, responseProcessorState);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
this._loggingConnector?.log({
|
|
457
|
+
level: "error",
|
|
458
|
+
ts: Date.now(),
|
|
459
|
+
source: this.CLASS_NAME,
|
|
460
|
+
message: `${FastifyWebServer._CLASS_NAME_CAMEL_CASE}.postProcessorError`,
|
|
461
|
+
error: BaseError.fromError(err),
|
|
462
|
+
data: {
|
|
463
|
+
route: socketRoute.path
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
for (const socketRouteProcessor of socketRouteProcessors) {
|
|
470
|
+
if (Is.function(socketRouteProcessor.pre)) {
|
|
471
|
+
await socketRouteProcessor.pre(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// We always call all the processors regardless of any response set by a previous processor.
|
|
475
|
+
// But if a pre processor sets a status code, we will emit the response manually, as the pre
|
|
476
|
+
// and post processors do not receive the emit method, they just populate the response object.
|
|
477
|
+
if (!Is.empty(httpResponse.statusCode)) {
|
|
478
|
+
await postProcessEmit(requestTopic, httpResponse, processorState);
|
|
479
|
+
}
|
|
480
|
+
for (const socketRouteProcessor of socketRouteProcessors) {
|
|
481
|
+
if (Is.function(socketRouteProcessor.process)) {
|
|
482
|
+
await socketRouteProcessor.process(httpServerRequest, httpResponse, socketRoute, httpRequestIdentity, processorState, async (topic, processResponse) => {
|
|
483
|
+
await postProcessEmit(topic, processResponse, processorState);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// If the processors set the status to any kind of error then we should emit this manually
|
|
488
|
+
if (Is.integer(httpResponse.statusCode) &&
|
|
489
|
+
httpResponse.statusCode >= HttpStatusCode.badRequest) {
|
|
490
|
+
await postProcessEmit(requestTopic, httpResponse, processorState);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
// Emit any unhandled errors manually
|
|
495
|
+
const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
|
|
496
|
+
HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode);
|
|
497
|
+
await postProcessEmit(requestTopic, httpResponse, processorState);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
247
500
|
/**
|
|
248
501
|
* Initialize the cors options.
|
|
249
502
|
* @param options The web server options.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
1
|
+
import { 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 { IFastifyWebServerConstructorOptions } from "./models/IFastifyWebServerConstructorOptions";
|
|
3
4
|
/**
|
|
4
5
|
* Implementation of the web server using Fastify.
|
|
5
6
|
*/
|
|
@@ -11,13 +12,8 @@ export declare class FastifyWebServer implements IWebServer<FastifyInstance> {
|
|
|
11
12
|
/**
|
|
12
13
|
* Create a new instance of FastifyWebServer.
|
|
13
14
|
* @param options The options for the server.
|
|
14
|
-
* @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
15
|
*/
|
|
17
|
-
constructor(options?:
|
|
18
|
-
loggingConnectorType?: string;
|
|
19
|
-
config?: FastifyServerOptions;
|
|
20
|
-
});
|
|
16
|
+
constructor(options?: IFastifyWebServerConstructorOptions);
|
|
21
17
|
/**
|
|
22
18
|
* Get the web server instance.
|
|
23
19
|
* @returns The web server instance.
|
|
@@ -25,12 +21,14 @@ export declare class FastifyWebServer implements IWebServer<FastifyInstance> {
|
|
|
25
21
|
getInstance(): FastifyInstance;
|
|
26
22
|
/**
|
|
27
23
|
* Build the server.
|
|
28
|
-
* @param restRouteProcessors The
|
|
24
|
+
* @param restRouteProcessors The processors for incoming requests over REST.
|
|
29
25
|
* @param restRoutes The REST routes.
|
|
26
|
+
* @param socketRouteProcessors The processors for incoming requests over Sockets.
|
|
27
|
+
* @param socketRoutes The socket routes.
|
|
30
28
|
* @param options Options for building the server.
|
|
31
29
|
* @returns Nothing.
|
|
32
30
|
*/
|
|
33
|
-
build(restRouteProcessors
|
|
31
|
+
build(restRouteProcessors?: IRestRouteProcessor[], restRoutes?: IRestRoute[], socketRouteProcessors?: ISocketRouteProcessor[], socketRoutes?: ISocketRoute[], options?: IWebServerOptions): Promise<void>;
|
|
34
32
|
/**
|
|
35
33
|
* Start the server.
|
|
36
34
|
* @returns Nothing.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IMimeTypeProcessor } from "@twin.org/api-models";
|
|
2
|
+
import type { IFastifyWebServerConfig } from "./IFastifyWebServerConfig";
|
|
3
|
+
/**
|
|
4
|
+
* The options for the Fastify web server constructor.
|
|
5
|
+
*/
|
|
6
|
+
export interface IFastifyWebServerConstructorOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The type of the logging connector to use, if undefined, no logging will happen.
|
|
9
|
+
*/
|
|
10
|
+
loggingConnectorType?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Additional configuration for the server.
|
|
13
|
+
*/
|
|
14
|
+
config?: IFastifyWebServerConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Additional MIME type processors.
|
|
17
|
+
*/
|
|
18
|
+
mimeTypeProcessors?: IMimeTypeProcessor[];
|
|
19
|
+
}
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,107 @@
|
|
|
1
1
|
# @twin.org/api-server-fastify - Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.0.1 (2025-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* release to production ([70ee2d5](https://github.com/twinfoundation/api/commit/70ee2d56a1dc9537d7c9c154d4cb78a235678a3a))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/api-models bumped from ^0.0.0 to ^0.0.1
|
|
16
|
+
* @twin.org/api-core bumped from ^0.0.0 to ^0.0.1
|
|
17
|
+
* devDependencies
|
|
18
|
+
* @twin.org/api-processors bumped from ^0.0.0 to ^0.0.1
|
|
19
|
+
|
|
20
|
+
## [0.0.1-next.36](https://github.com/twinfoundation/api/compare/api-server-fastify-v0.0.1-next.35...api-server-fastify-v0.0.1-next.36) (2025-06-17)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Miscellaneous Chores
|
|
24
|
+
|
|
25
|
+
* **api-server-fastify:** Synchronize repo versions
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Dependencies
|
|
29
|
+
|
|
30
|
+
* The following workspace dependencies were updated
|
|
31
|
+
* dependencies
|
|
32
|
+
* @twin.org/api-models bumped from 0.0.1-next.35 to 0.0.1-next.36
|
|
33
|
+
* @twin.org/api-core bumped from 0.0.1-next.35 to 0.0.1-next.36
|
|
34
|
+
* devDependencies
|
|
35
|
+
* @twin.org/api-processors bumped from 0.0.1-next.35 to 0.0.1-next.36
|
|
36
|
+
|
|
37
|
+
## [0.0.1-next.35](https://github.com/twinfoundation/api/compare/api-server-fastify-v0.0.1-next.34...api-server-fastify-v0.0.1-next.35) (2025-06-11)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Features
|
|
41
|
+
|
|
42
|
+
* update dependencies ([1171dc4](https://github.com/twinfoundation/api/commit/1171dc416a9481737f6a640e3cf30145768f37e9))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Dependencies
|
|
46
|
+
|
|
47
|
+
* The following workspace dependencies were updated
|
|
48
|
+
* dependencies
|
|
49
|
+
* @twin.org/api-models bumped from 0.0.1-next.34 to 0.0.1-next.35
|
|
50
|
+
* @twin.org/api-core bumped from 0.0.1-next.34 to 0.0.1-next.35
|
|
51
|
+
* devDependencies
|
|
52
|
+
* @twin.org/api-processors bumped from 0.0.1-next.34 to 0.0.1-next.35
|
|
53
|
+
|
|
54
|
+
## [0.0.1-next.34](https://github.com/twinfoundation/api/compare/api-server-fastify-v0.0.1-next.33...api-server-fastify-v0.0.1-next.34) (2025-05-27)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### Miscellaneous Chores
|
|
58
|
+
|
|
59
|
+
* **api-server-fastify:** Synchronize repo versions
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Dependencies
|
|
63
|
+
|
|
64
|
+
* The following workspace dependencies were updated
|
|
65
|
+
* dependencies
|
|
66
|
+
* @twin.org/api-models bumped from 0.0.1-next.33 to 0.0.1-next.34
|
|
67
|
+
* @twin.org/api-core bumped from 0.0.1-next.33 to 0.0.1-next.34
|
|
68
|
+
* devDependencies
|
|
69
|
+
* @twin.org/api-processors bumped from 0.0.1-next.33 to 0.0.1-next.34
|
|
70
|
+
|
|
71
|
+
## [0.0.1-next.33](https://github.com/twinfoundation/api/compare/api-server-fastify-v0.0.1-next.32...api-server-fastify-v0.0.1-next.33) (2025-04-17)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
### Features
|
|
75
|
+
|
|
76
|
+
* use shared store mechanism ([#19](https://github.com/twinfoundation/api/issues/19)) ([32116df](https://github.com/twinfoundation/api/commit/32116df3b4380a30137f5056f242a5c99afa2df9))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
### Dependencies
|
|
80
|
+
|
|
81
|
+
* The following workspace dependencies were updated
|
|
82
|
+
* dependencies
|
|
83
|
+
* @twin.org/api-models bumped from 0.0.1-next.32 to 0.0.1-next.33
|
|
84
|
+
* @twin.org/api-core bumped from 0.0.1-next.32 to 0.0.1-next.33
|
|
85
|
+
* devDependencies
|
|
86
|
+
* @twin.org/api-processors bumped from 0.0.1-next.32 to 0.0.1-next.33
|
|
87
|
+
|
|
88
|
+
## [0.0.1-next.32](https://github.com/twinfoundation/api/compare/api-server-fastify-v0.0.1-next.31...api-server-fastify-v0.0.1-next.32) (2025-03-28)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
### Miscellaneous Chores
|
|
92
|
+
|
|
93
|
+
* **api-server-fastify:** Synchronize repo versions
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
### Dependencies
|
|
97
|
+
|
|
98
|
+
* The following workspace dependencies were updated
|
|
99
|
+
* dependencies
|
|
100
|
+
* @twin.org/api-models bumped from 0.0.1-next.31 to 0.0.1-next.32
|
|
101
|
+
* @twin.org/api-core bumped from 0.0.1-next.31 to 0.0.1-next.32
|
|
102
|
+
* devDependencies
|
|
103
|
+
* @twin.org/api-processors bumped from next to 0.0.1-next.32
|
|
104
|
+
|
|
105
|
+
## v0.0.1-next.31
|
|
4
106
|
|
|
5
107
|
- Initial Release
|
|
@@ -8,29 +8,23 @@ Implementation of the web server using Fastify.
|
|
|
8
8
|
|
|
9
9
|
## Constructors
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### Constructor
|
|
12
12
|
|
|
13
|
-
> **new FastifyWebServer**(`options
|
|
13
|
+
> **new FastifyWebServer**(`options?`): `FastifyWebServer`
|
|
14
14
|
|
|
15
15
|
Create a new instance of FastifyWebServer.
|
|
16
16
|
|
|
17
17
|
#### Parameters
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
##### options?
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
• **options.loggingConnectorType?**: `string`
|
|
24
|
-
|
|
25
|
-
The type of the logging connector to use, if undefined, no logging will happen.
|
|
26
|
-
|
|
27
|
-
• **options.config?**: `FastifyServerOptions`
|
|
21
|
+
[`IFastifyWebServerConstructorOptions`](../interfaces/IFastifyWebServerConstructorOptions.md)
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
The options for the server.
|
|
30
24
|
|
|
31
25
|
#### Returns
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
`FastifyWebServer`
|
|
34
28
|
|
|
35
29
|
## Properties
|
|
36
30
|
|
|
@@ -44,13 +38,13 @@ Runtime name for the class.
|
|
|
44
38
|
|
|
45
39
|
### getInstance()
|
|
46
40
|
|
|
47
|
-
> **getInstance**(): `FastifyInstance
|
|
41
|
+
> **getInstance**(): `FastifyInstance`
|
|
48
42
|
|
|
49
43
|
Get the web server instance.
|
|
50
44
|
|
|
51
45
|
#### Returns
|
|
52
46
|
|
|
53
|
-
`FastifyInstance
|
|
47
|
+
`FastifyInstance`
|
|
54
48
|
|
|
55
49
|
The web server instance.
|
|
56
50
|
|
|
@@ -62,21 +56,39 @@ The web server instance.
|
|
|
62
56
|
|
|
63
57
|
### build()
|
|
64
58
|
|
|
65
|
-
> **build**(`restRouteProcessors
|
|
59
|
+
> **build**(`restRouteProcessors?`, `restRoutes?`, `socketRouteProcessors?`, `socketRoutes?`, `options?`): `Promise`\<`void`\>
|
|
66
60
|
|
|
67
61
|
Build the server.
|
|
68
62
|
|
|
69
63
|
#### Parameters
|
|
70
64
|
|
|
71
|
-
|
|
65
|
+
##### restRouteProcessors?
|
|
66
|
+
|
|
67
|
+
`IRestRouteProcessor`[]
|
|
68
|
+
|
|
69
|
+
The processors for incoming requests over REST.
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
##### restRoutes?
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
`IRestRoute`\<`any`, `any`\>[]
|
|
76
74
|
|
|
77
75
|
The REST routes.
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
##### socketRouteProcessors?
|
|
78
|
+
|
|
79
|
+
`ISocketRouteProcessor`[]
|
|
80
|
+
|
|
81
|
+
The processors for incoming requests over Sockets.
|
|
82
|
+
|
|
83
|
+
##### socketRoutes?
|
|
84
|
+
|
|
85
|
+
`ISocketRoute`\<`any`, `any`\>[]
|
|
86
|
+
|
|
87
|
+
The socket routes.
|
|
88
|
+
|
|
89
|
+
##### options?
|
|
90
|
+
|
|
91
|
+
`IWebServerOptions`
|
|
80
92
|
|
|
81
93
|
Options for building the server.
|
|
82
94
|
|
package/docs/reference/index.md
CHANGED
|
@@ -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.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Interface: IFastifyWebServerConstructorOptions
|
|
2
|
+
|
|
3
|
+
The options for the Fastify web server constructor.
|
|
4
|
+
|
|
5
|
+
## Properties
|
|
6
|
+
|
|
7
|
+
### loggingConnectorType?
|
|
8
|
+
|
|
9
|
+
> `optional` **loggingConnectorType**: `string`
|
|
10
|
+
|
|
11
|
+
The type of the logging connector to use, if undefined, no logging will happen.
|
|
12
|
+
|
|
13
|
+
***
|
|
14
|
+
|
|
15
|
+
### config?
|
|
16
|
+
|
|
17
|
+
> `optional` **config**: [`IFastifyWebServerConfig`](IFastifyWebServerConfig.md)
|
|
18
|
+
|
|
19
|
+
Additional configuration for the server.
|
|
20
|
+
|
|
21
|
+
***
|
|
22
|
+
|
|
23
|
+
### mimeTypeProcessors?
|
|
24
|
+
|
|
25
|
+
> `optional` **mimeTypeProcessors**: `IMimeTypeProcessor`[]
|
|
26
|
+
|
|
27
|
+
Additional MIME type processors.
|
package/locales/en.json
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
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
|
-
"
|
|
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",
|
|
13
|
+
"postProcessorError": "There was a failure after in a post processor for route \"{route}\""
|
|
11
14
|
}
|
|
12
15
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/api-server-fastify",
|
|
3
|
-
"version": "0.0.1
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "Use Fastify as the core web server for APIs",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,24 +14,25 @@
|
|
|
14
14
|
"node": ">=20.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@fastify/compress": "8.0.
|
|
18
|
-
"@fastify/cors": "
|
|
19
|
-
"@twin.org/api-core": "0.0.1
|
|
20
|
-
"@twin.org/api-models": "0.0.1
|
|
21
|
-
"@twin.org/core": "
|
|
22
|
-
"@twin.org/logging-models": "next",
|
|
23
|
-
"@twin.org/nameof": "
|
|
24
|
-
"@twin.org/web": "
|
|
25
|
-
"fastify": "5.
|
|
17
|
+
"@fastify/compress": "8.0.3",
|
|
18
|
+
"@fastify/cors": "11.0.1",
|
|
19
|
+
"@twin.org/api-core": "^0.0.1",
|
|
20
|
+
"@twin.org/api-models": "^0.0.1",
|
|
21
|
+
"@twin.org/core": "^0.0.1",
|
|
22
|
+
"@twin.org/logging-models": "^0.0.1-next.2",
|
|
23
|
+
"@twin.org/nameof": "^0.0.1",
|
|
24
|
+
"@twin.org/web": "^0.0.1",
|
|
25
|
+
"fastify": "5.3.3",
|
|
26
|
+
"socket.io": "4.8.1"
|
|
26
27
|
},
|
|
27
28
|
"main": "./dist/cjs/index.cjs",
|
|
28
29
|
"module": "./dist/esm/index.mjs",
|
|
29
30
|
"types": "./dist/types/index.d.ts",
|
|
30
31
|
"exports": {
|
|
31
32
|
".": {
|
|
33
|
+
"types": "./dist/types/index.d.ts",
|
|
32
34
|
"require": "./dist/cjs/index.cjs",
|
|
33
|
-
"import": "./dist/esm/index.mjs"
|
|
34
|
-
"types": "./dist/types/index.d.ts"
|
|
35
|
+
"import": "./dist/esm/index.mjs"
|
|
35
36
|
}
|
|
36
37
|
},
|
|
37
38
|
"files": [
|