@3sixty/api-server-fastify 0.0.0-stage → 0.20.0-next.0

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.
Files changed (32) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +24 -2
  3. package/dist/es/fastifySocketIo.js +16 -0
  4. package/dist/es/fastifySocketIo.js.map +1 -0
  5. package/dist/es/fastifyWebServer.js +877 -0
  6. package/dist/es/fastifyWebServer.js.map +1 -0
  7. package/dist/es/index.js +6 -0
  8. package/dist/es/index.js.map +1 -0
  9. package/dist/es/models/IFastifyWebServerConfig.js +2 -0
  10. package/dist/es/models/IFastifyWebServerConfig.js.map +1 -0
  11. package/dist/es/models/IFastifyWebServerConstructorOptions.js +2 -0
  12. package/dist/es/models/IFastifyWebServerConstructorOptions.js.map +1 -0
  13. package/dist/es/models/IRestProcessorChains.js +2 -0
  14. package/dist/es/models/IRestProcessorChains.js.map +1 -0
  15. package/dist/es/models/ISocketProcessorChains.js +2 -0
  16. package/dist/es/models/ISocketProcessorChains.js.map +1 -0
  17. package/dist/types/fastifySocketIo.d.ts +8 -0
  18. package/dist/types/fastifyWebServer.d.ts +59 -0
  19. package/dist/types/index.d.ts +3 -0
  20. package/dist/types/models/IFastifyWebServerConfig.d.ts +19 -0
  21. package/dist/types/models/IFastifyWebServerConstructorOptions.d.ts +19 -0
  22. package/dist/types/models/IRestProcessorChains.d.ts +18 -0
  23. package/dist/types/models/ISocketProcessorChains.d.ts +26 -0
  24. package/docs/changelog.md +0 -0
  25. package/docs/examples.md +37 -0
  26. package/docs/reference/classes/FastifyWebServer.md +203 -0
  27. package/docs/reference/index.md +10 -0
  28. package/docs/reference/interfaces/IFastifyWebServerConfig.md +27 -0
  29. package/docs/reference/interfaces/IFastifyWebServerConstructorOptions.md +27 -0
  30. package/locales/.validate-ignore +5 -0
  31. package/locales/en.json +35 -0
  32. package/package.json +57 -5
@@ -0,0 +1,877 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { HealthStatus, HttpBodyLimit, HttpContextIdKeys, HttpErrorHelper, HealthCategory } from "@3sixty/api-models";
4
+ import { JsonLdMimeTypeProcessor } from "@3sixty/api-processors";
5
+ import { ContextIdStore } from "@3sixty/context";
6
+ import { BaseError, ComponentFactory, GeneralError, Is, RandomHelper, StringHelper, Url } from "@3sixty/core";
7
+ import { HeaderTypes, HttpMethod, HttpStatusCode, HeaderHelper, MimeTypes } from "@3sixty/web";
8
+ import FastifyCompress from "@fastify/compress";
9
+ import FastifyCors from "@fastify/cors";
10
+ import Fastify from "fastify";
11
+ import FastifySocketIO from "./fastifySocketIo.js";
12
+ /**
13
+ * Implementation of the web server using Fastify.
14
+ */
15
+ export class FastifyWebServer {
16
+ /**
17
+ * Runtime name for the class.
18
+ */
19
+ static CLASS_NAME = "FastifyWebServer";
20
+ /**
21
+ * Default port for running the server.
22
+ * @internal
23
+ */
24
+ static _DEFAULT_PORT = 3000;
25
+ /**
26
+ * Default host for running the server.
27
+ * @internal
28
+ */
29
+ static _DEFAULT_HOST = "localhost";
30
+ /**
31
+ * Default named body size limits for routes.
32
+ * @internal
33
+ */
34
+ static _DEFAULT_BODY_LIMITS = {
35
+ [HttpBodyLimit.Default]: 1048576,
36
+ [HttpBodyLimit.Large]: 26214400
37
+ };
38
+ /**
39
+ * The logging component type.
40
+ * @internal
41
+ */
42
+ _loggingComponentType;
43
+ /**
44
+ * The logging component.
45
+ * @internal
46
+ */
47
+ _logging;
48
+ /**
49
+ * The options for the server.
50
+ * @internal
51
+ */
52
+ _options;
53
+ /**
54
+ * The Fastify instance.
55
+ * @internal
56
+ */
57
+ _fastify;
58
+ /**
59
+ * The options for the socket server.
60
+ * @internal
61
+ */
62
+ _socketConfig;
63
+ /**
64
+ * Whether the server has been started.
65
+ * @internal
66
+ */
67
+ _started;
68
+ /**
69
+ * The mime type processors.
70
+ * @internal
71
+ */
72
+ _mimeTypeProcessors;
73
+ /**
74
+ * Include the stack with errors.
75
+ * @internal
76
+ */
77
+ _includeErrorStack;
78
+ /**
79
+ * The public origin of the server, used for constructing the request URL and for CORS.
80
+ * @internal
81
+ */
82
+ _publicOrigin;
83
+ /**
84
+ * The local origin of the server, used for constructing the request URL and for CORS.
85
+ * @internal
86
+ */
87
+ _localOrigin;
88
+ /**
89
+ * The REST processor chains resolved when the server is built.
90
+ * @internal
91
+ */
92
+ _restChains;
93
+ /**
94
+ * The socket processor chains resolved when the server is built.
95
+ * @internal
96
+ */
97
+ _socketChains;
98
+ /**
99
+ * Create a new instance of FastifyWebServer.
100
+ * @param options The options for the server.
101
+ */
102
+ constructor(options) {
103
+ this._loggingComponentType = options?.loggingComponentType;
104
+ this._logging = ComponentFactory.getIfExists(options?.loggingComponentType);
105
+ this._fastify = Fastify({
106
+ routerOptions: {
107
+ maxParamLength: 2000
108
+ },
109
+ ...options?.config?.web
110
+ // Need this cast for now as maxParamLength has moved in to routerOptions
111
+ // but the TS defs has not been updated yet
112
+ });
113
+ this._socketConfig = {
114
+ path: "/socket",
115
+ ...options?.config?.socket
116
+ };
117
+ this._started = false;
118
+ this._restChains = { pre: [], process: [], post: [] };
119
+ this._socketChains = { connected: [], disconnected: [], pre: [], process: [], post: [] };
120
+ this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
121
+ const hasJsonLd = this._mimeTypeProcessors.some(processor => processor.getTypes().includes(MimeTypes.JsonLd));
122
+ if (!hasJsonLd) {
123
+ this._mimeTypeProcessors.push(new JsonLdMimeTypeProcessor());
124
+ }
125
+ this._includeErrorStack = options?.config?.includeErrorStack ?? false;
126
+ }
127
+ /**
128
+ * Returns the class name of the component.
129
+ * @returns The class name of the component.
130
+ */
131
+ className() {
132
+ return FastifyWebServer.CLASS_NAME;
133
+ }
134
+ /**
135
+ * Get the web server instance.
136
+ * @returns The web server instance.
137
+ */
138
+ getInstance() {
139
+ return this._fastify;
140
+ }
141
+ /**
142
+ * Build the server.
143
+ * @param restRouteProcessors The processors for incoming requests over REST.
144
+ * @param restRoutes The REST routes.
145
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
146
+ * @param socketRoutes The socket routes.
147
+ * @param options Options for building the server.
148
+ * @returns A promise that resolves when the server is fully built and ready to start.
149
+ */
150
+ async build(restRouteProcessors, restRoutes, socketRouteProcessors, socketRoutes, options) {
151
+ if (Is.arrayValue(restRoutes) && !Is.arrayValue(restRouteProcessors)) {
152
+ throw new GeneralError(FastifyWebServer.CLASS_NAME, "noRestProcessors");
153
+ }
154
+ if (Is.arrayValue(socketRoutes) && !Is.arrayValue(socketRouteProcessors)) {
155
+ throw new GeneralError(FastifyWebServer.CLASS_NAME, "noSocketProcessors");
156
+ }
157
+ await this._logging?.log({
158
+ level: "info",
159
+ ts: Date.now(),
160
+ source: FastifyWebServer.CLASS_NAME,
161
+ message: "building"
162
+ });
163
+ let localHost = options?.host ?? "127.0.0.1";
164
+ if (localHost === "0.0.0.0") {
165
+ localHost = "127.0.0.1";
166
+ }
167
+ this._localOrigin = `http://${localHost}:${options?.port ?? 3000}`;
168
+ if (Is.stringValue(options?.publicOrigin)) {
169
+ const publicUrl = Url.tryParseExact(options.publicOrigin);
170
+ if (!Is.empty(publicUrl)) {
171
+ const urlParts = publicUrl.parts();
172
+ this._publicOrigin = `${urlParts.schema}://${urlParts.host}${Is.integer(urlParts.port) ? `:${urlParts.port}` : ""}`;
173
+ }
174
+ else {
175
+ throw new GeneralError(FastifyWebServer.CLASS_NAME, "invalidPublicOrigin", {
176
+ publicOrigin: options.publicOrigin
177
+ });
178
+ }
179
+ }
180
+ this._options = options;
181
+ await this._fastify.register(FastifyCompress);
182
+ if (Is.arrayValue(socketRoutes)) {
183
+ await this._fastify.register(FastifySocketIO, this._socketConfig);
184
+ }
185
+ if (Is.arrayValue(this._mimeTypeProcessors)) {
186
+ for (const contentTypeHandler of this._mimeTypeProcessors) {
187
+ this._fastify.addContentTypeParser(contentTypeHandler.getTypes(), { parseAs: "buffer" }, (request, body, done) => {
188
+ // Fastify does not handle this method correctly if it is async
189
+ // so we have to use the callback method
190
+ contentTypeHandler
191
+ .handle(body)
192
+ // eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
193
+ .then(processed => done(null, processed))
194
+ // eslint-disable-next-line promise/prefer-await-to-then, promise/no-callback-in-promise
195
+ .catch(err => done(BaseError.fromError(err)));
196
+ });
197
+ }
198
+ }
199
+ await this.initCors(options);
200
+ this._fastify.setNotFoundHandler({}, async (request, reply) => this.handleRequestRest(restRouteProcessors ?? [], request, reply));
201
+ this._fastify.setErrorHandler(async (error, request, reply) => {
202
+ // If code property is set this is a fastify error
203
+ // otherwise it's from our framework
204
+ let httpStatusCode;
205
+ let err;
206
+ if (Is.number(error.code) || Is.string(error.code)) {
207
+ err = {
208
+ source: FastifyWebServer.CLASS_NAME,
209
+ name: error.name,
210
+ message: `${error.code}: ${error.message}`
211
+ };
212
+ httpStatusCode = error.statusCode ?? HttpStatusCode.badRequest;
213
+ }
214
+ else {
215
+ const errorAndCode = HttpErrorHelper.processError(error);
216
+ err = errorAndCode.error;
217
+ httpStatusCode = errorAndCode.httpStatusCode;
218
+ }
219
+ await this._logging?.log({
220
+ level: "error",
221
+ ts: Date.now(),
222
+ source: FastifyWebServer.CLASS_NAME,
223
+ message: "badRequest",
224
+ error: err
225
+ });
226
+ return reply.status(httpStatusCode).send({
227
+ error: err
228
+ });
229
+ });
230
+ await this.addRoutesRest(restRouteProcessors, restRoutes);
231
+ await this.addRoutesSocket(socketRouteProcessors, socketRoutes);
232
+ this.buildProcessorChains(restRouteProcessors, socketRouteProcessors);
233
+ }
234
+ /**
235
+ * Start the server.
236
+ * @returns A promise that resolves when the server is listening for connections.
237
+ */
238
+ async start() {
239
+ const host = this._options?.host ?? FastifyWebServer._DEFAULT_HOST;
240
+ const port = this._options?.port ?? FastifyWebServer._DEFAULT_PORT;
241
+ await this._logging?.log({
242
+ level: "info",
243
+ ts: Date.now(),
244
+ source: FastifyWebServer.CLASS_NAME,
245
+ message: "starting",
246
+ data: {
247
+ host,
248
+ port
249
+ }
250
+ });
251
+ if (!this._started) {
252
+ try {
253
+ await this._fastify.listen({ port, host });
254
+ const addresses = this._fastify.addresses();
255
+ const protocol = Is.object(this._fastify.initialConfig.https) ? "https://" : "http://";
256
+ const normalizeAnyHost = (address) => {
257
+ if (address === "0.0.0.0") {
258
+ return "127.0.0.1";
259
+ }
260
+ if (address === "::") {
261
+ return "::1";
262
+ }
263
+ return address;
264
+ };
265
+ const formatAddress = (address, family, forceProtocol) => {
266
+ const displayAddress = normalizeAnyHost(address);
267
+ const isIPv6 = family === "IPv6" || (Is.stringValue(displayAddress) && displayAddress.includes(":"));
268
+ return `${forceProtocol ?? protocol}${isIPv6 ? "[" : ""}${displayAddress}${isIPv6 ? "]" : ""}`;
269
+ };
270
+ const startupAddresses = addresses.map(a => {
271
+ const formatted = formatAddress(a.address, a.family, protocol);
272
+ return `${formatted}:${a.port}`;
273
+ });
274
+ for (const origin of [this._localOrigin, this._publicOrigin]) {
275
+ if (Is.stringValue(origin)) {
276
+ const originUrl = Url.tryParseExact(origin);
277
+ if (!Is.empty(originUrl)) {
278
+ const originParts = originUrl.parts();
279
+ const formatted = formatAddress(originParts.host, undefined, `${originParts.schema}://`);
280
+ startupAddresses.push(`${formatted}${Is.integer(originParts.port) ? `:${originParts.port}` : ""}`);
281
+ }
282
+ else {
283
+ startupAddresses.push(origin);
284
+ }
285
+ }
286
+ }
287
+ const distinctStartupAddresses = [...new Set(startupAddresses)];
288
+ await this._logging?.log({
289
+ level: "info",
290
+ ts: Date.now(),
291
+ source: FastifyWebServer.CLASS_NAME,
292
+ message: "started",
293
+ data: {
294
+ addresses: distinctStartupAddresses.join(", ")
295
+ }
296
+ });
297
+ this._started = true;
298
+ }
299
+ catch (err) {
300
+ await this._logging?.log({
301
+ level: "error",
302
+ ts: Date.now(),
303
+ source: FastifyWebServer.CLASS_NAME,
304
+ message: "startFailed",
305
+ error: BaseError.fromError(err)
306
+ });
307
+ }
308
+ }
309
+ }
310
+ /**
311
+ * Stop the server.
312
+ * @returns A promise that resolves when the server has shut down all connections.
313
+ */
314
+ async stop() {
315
+ if (this._started) {
316
+ this._started = false;
317
+ await this._fastify.close();
318
+ await this._logging?.log({
319
+ level: "info",
320
+ ts: Date.now(),
321
+ source: FastifyWebServer.CLASS_NAME,
322
+ message: "stopped"
323
+ });
324
+ }
325
+ }
326
+ /**
327
+ * Returns the health status of the component.
328
+ * @returns The health status of the component, can return multiple entries for elements within the component.
329
+ */
330
+ async health() {
331
+ let healthCheck;
332
+ if (this._fastify?.server?.listening) {
333
+ healthCheck = {
334
+ source: FastifyWebServer.CLASS_NAME,
335
+ status: HealthStatus.Ok,
336
+ category: HealthCategory.Connectivity,
337
+ description: "healthConnectivityDescription",
338
+ message: "reachable"
339
+ };
340
+ }
341
+ else {
342
+ healthCheck = {
343
+ source: FastifyWebServer.CLASS_NAME,
344
+ status: HealthStatus.Error,
345
+ category: HealthCategory.Connectivity,
346
+ description: "healthConnectivityDescription",
347
+ message: "unreachable"
348
+ };
349
+ }
350
+ return [healthCheck];
351
+ }
352
+ /**
353
+ * Verify the root endpoint is reachable and returns a body by making a real HTTP request.
354
+ * Skipped when GET / is not registered on this server instance.
355
+ * @param callback The callback to invoke when a deferred health result is ready.
356
+ * @returns The application health status of the component.
357
+ */
358
+ async healthApplication(callback) {
359
+ if (!this._fastify.hasRoute({ method: "GET", url: "/" })) {
360
+ return [];
361
+ }
362
+ if (!Is.stringValue(this._localOrigin)) {
363
+ return [
364
+ {
365
+ source: FastifyWebServer.CLASS_NAME,
366
+ status: HealthStatus.Error,
367
+ category: HealthCategory.Application,
368
+ description: "healthApplicationDescription",
369
+ message: "serverNotBuilt"
370
+ }
371
+ ];
372
+ }
373
+ try {
374
+ const response = await fetch(`${this._localOrigin}/`);
375
+ const body = await response.text();
376
+ if (response.ok && Is.stringValue(body)) {
377
+ return [
378
+ {
379
+ source: FastifyWebServer.CLASS_NAME,
380
+ status: HealthStatus.Ok,
381
+ category: HealthCategory.Application,
382
+ description: "healthApplicationDescription",
383
+ message: "rootEndpointReachable"
384
+ }
385
+ ];
386
+ }
387
+ return [
388
+ {
389
+ source: FastifyWebServer.CLASS_NAME,
390
+ status: HealthStatus.Error,
391
+ category: HealthCategory.Application,
392
+ description: "healthApplicationDescription",
393
+ message: "rootEndpointError"
394
+ }
395
+ ];
396
+ }
397
+ catch (error) {
398
+ return [
399
+ {
400
+ source: FastifyWebServer.CLASS_NAME,
401
+ status: HealthStatus.Error,
402
+ category: HealthCategory.Application,
403
+ description: "healthApplicationDescription",
404
+ error: BaseError.fromError(error)
405
+ }
406
+ ];
407
+ }
408
+ }
409
+ /**
410
+ * Add the REST routes to the server.
411
+ * @param restRouteProcessors The processors for the incoming requests.
412
+ * @param restRoutes The REST routes to add.
413
+ * @internal
414
+ */
415
+ async addRoutesRest(restRouteProcessors, restRoutes) {
416
+ if (Is.arrayValue(restRouteProcessors) && Is.arrayValue(restRoutes)) {
417
+ const bodyLimits = this.resolveBodyLimits();
418
+ for (const restRoute of restRoutes) {
419
+ let path = StringHelper.trimTrailingSlashes(restRoute.path);
420
+ if (!path.startsWith("/")) {
421
+ path = `/${path}`;
422
+ }
423
+ await this._logging?.log({
424
+ level: "info",
425
+ ts: Date.now(),
426
+ source: FastifyWebServer.CLASS_NAME,
427
+ message: "restRouteAdded",
428
+ data: { route: path, method: restRoute.method }
429
+ });
430
+ const method = restRoute.method.toLowerCase();
431
+ const bodyLimitKey = Is.stringValue(restRoute.bodyLimit)
432
+ ? restRoute.bodyLimit
433
+ : HttpBodyLimit.Default;
434
+ const bodyLimit = bodyLimits[bodyLimitKey];
435
+ if (Is.empty(bodyLimit)) {
436
+ throw new GeneralError(FastifyWebServer.CLASS_NAME, "unknownBodyLimit", {
437
+ route: path,
438
+ bodyLimit: bodyLimitKey
439
+ });
440
+ }
441
+ this._fastify[method](path, { bodyLimit }, async (request, reply) => this.handleRequestRest(restRouteProcessors, request, reply, restRoute));
442
+ }
443
+ }
444
+ }
445
+ /**
446
+ * Merge the configured body limits over the built-in ones and validate them.
447
+ * @returns The named body limits in bytes.
448
+ * @throws GeneralError If a limit is not a positive integer.
449
+ * @internal
450
+ */
451
+ resolveBodyLimits() {
452
+ const bodyLimits = {
453
+ ...FastifyWebServer._DEFAULT_BODY_LIMITS,
454
+ ...this._options?.bodyLimits
455
+ };
456
+ for (const name of Object.keys(bodyLimits)) {
457
+ const bodyLimit = bodyLimits[name];
458
+ if (!Is.integer(bodyLimit) || bodyLimit <= 0) {
459
+ throw new GeneralError(FastifyWebServer.CLASS_NAME, "invalidBodyLimit", {
460
+ bodyLimit: name,
461
+ value: bodyLimit
462
+ });
463
+ }
464
+ }
465
+ return bodyLimits;
466
+ }
467
+ /**
468
+ * Add the socket routes to the server.
469
+ * @param socketRouteProcessors The processors for the incoming requests.
470
+ * @param socketRoutes The socket routes to add.
471
+ * @internal
472
+ */
473
+ async addRoutesSocket(socketRouteProcessors, socketRoutes) {
474
+ if (Is.arrayValue(socketRouteProcessors) && Is.arrayValue(socketRoutes)) {
475
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
476
+ const io = this._fastify.io;
477
+ for (const socketRoute of socketRoutes) {
478
+ const path = StringHelper.trimLeadingSlashes(StringHelper.trimTrailingSlashes(socketRoute.path));
479
+ const pathParts = path.split("/");
480
+ const namespace = `/${pathParts[0]}`;
481
+ const topic = pathParts.slice(1).join("/");
482
+ await this._logging?.log({
483
+ level: "info",
484
+ ts: Date.now(),
485
+ source: FastifyWebServer.CLASS_NAME,
486
+ message: "socketRouteAdded",
487
+ data: {
488
+ handshakePath: this._socketConfig.path,
489
+ namespace,
490
+ eventName: topic
491
+ }
492
+ });
493
+ const socketNamespace = io.of(namespace);
494
+ socketNamespace.on("connection", async (socket) => {
495
+ const socketServerRequest = {
496
+ method: HttpMethod.GET,
497
+ url: socket.handshake.url,
498
+ query: socket.handshake.query,
499
+ headers: socket.handshake.headers,
500
+ socketId: socket.id
501
+ };
502
+ // Pass the connected information on to any processors
503
+ try {
504
+ for (const socketRouteProcessor of socketRouteProcessors) {
505
+ if (socketRouteProcessor.connected) {
506
+ await socketRouteProcessor.connected(socketServerRequest, socketRoute, this._loggingComponentType);
507
+ }
508
+ }
509
+ }
510
+ catch (err) {
511
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
512
+ const response = {};
513
+ HttpErrorHelper.buildResponse(response, error, httpStatusCode, this._includeErrorStack);
514
+ socket.emit(topic, response);
515
+ }
516
+ socket.on("disconnect", async () => {
517
+ try {
518
+ // The socket disconnected so notify any processors
519
+ for (const socketRouteProcessor of socketRouteProcessors) {
520
+ if (socketRouteProcessor.disconnected) {
521
+ await socketRouteProcessor.disconnected(socketServerRequest, socketRoute, this._loggingComponentType);
522
+ }
523
+ }
524
+ }
525
+ catch {
526
+ // If something fails on a disconnect there is not much we can do with it
527
+ }
528
+ });
529
+ // Handle any incoming messages
530
+ socket.on(topic, async (data) => {
531
+ // The listener is async, so anything escaping it would surface as an unhandled
532
+ // rejection and terminate the process, emit it on the topic instead
533
+ try {
534
+ await this.handleRequestSocket(socketRouteProcessors, socketRoute, socket, `/${pathParts.join("/")}`, topic, data);
535
+ }
536
+ catch (err) {
537
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
538
+ const response = {};
539
+ HttpErrorHelper.buildResponse(response, error, httpStatusCode, this._includeErrorStack);
540
+ socket.emit(topic, response);
541
+ }
542
+ });
543
+ });
544
+ }
545
+ }
546
+ }
547
+ /**
548
+ * Handle the incoming REST request.
549
+ * @param restRouteProcessors The hooks to process the incoming requests.
550
+ * @param request The incoming request.
551
+ * @param reply The outgoing response.
552
+ * @param restRoute The REST route to handle.
553
+ * @returns The Fastify reply with the response.
554
+ * @internal
555
+ */
556
+ async handleRequestRest(restRouteProcessors, request, reply, restRoute) {
557
+ const port = (request.port === 80 && request.protocol === "http") ||
558
+ (request.port === 443 && request.protocol === "https") ||
559
+ !Is.integer(request.port)
560
+ ? ""
561
+ : `:${request.port}`;
562
+ const requestOrigin = `${request.protocol}://${request.hostname}${port}`;
563
+ const httpServerRequest = {
564
+ method: request.method.toUpperCase(),
565
+ url: `${requestOrigin}${request.url}`,
566
+ body: request.body,
567
+ query: request.query,
568
+ pathParams: request.params,
569
+ headers: request.headers
570
+ };
571
+ const httpResponse = {};
572
+ const contextIds = {
573
+ [HttpContextIdKeys.IpAddress]: HeaderHelper.extractClientIps(httpServerRequest.headers).join("|"),
574
+ [HttpContextIdKeys.UserAgent]: HeaderHelper.extractUserAgent(httpServerRequest.headers),
575
+ [HttpContextIdKeys.CorrelationId]: HeaderHelper.extractCorrelationId(httpServerRequest.headers),
576
+ [HttpContextIdKeys.RemoteRequest]: RandomHelper.generateUuidV7("compact"),
577
+ [HttpContextIdKeys.LocalOrigin]: this._localOrigin,
578
+ // This can be overridden by a processor if needed, for example a tenant processor
579
+ [HttpContextIdKeys.PublicOrigin]: this._publicOrigin ?? requestOrigin ?? this._localOrigin
580
+ };
581
+ const processorState = {};
582
+ if (Is.object(httpServerRequest.pathParams)) {
583
+ for (const key of Object.keys(httpServerRequest.pathParams)) {
584
+ httpServerRequest.pathParams[key] = decodeURIComponent(httpServerRequest.pathParams[key]);
585
+ }
586
+ }
587
+ if (Is.object(httpServerRequest.query)) {
588
+ for (const key of Object.keys(httpServerRequest.query)) {
589
+ httpServerRequest.query[key] = decodeURIComponent(httpServerRequest.query[key]);
590
+ }
591
+ }
592
+ await this.runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, contextIds, processorState);
593
+ if (!Is.empty(httpResponse.headers)) {
594
+ for (const header of Object.keys(httpResponse.headers)) {
595
+ reply.header(header, httpResponse.headers[header]);
596
+ }
597
+ }
598
+ return reply.status(httpResponse.statusCode ?? HttpStatusCode.ok).send(httpResponse.body);
599
+ }
600
+ /**
601
+ * Run the REST processors for the route.
602
+ * @param restRouteProcessors The processors to run.
603
+ * @param restRoute The route to process.
604
+ * @param httpServerRequest The incoming request.
605
+ * @param httpResponse The outgoing response.
606
+ * @param contextIds The context IDs of the request.
607
+ * @internal
608
+ */
609
+ async runProcessorsRest(restRouteProcessors, restRoute, httpServerRequest, httpResponse, contextIds, processorState) {
610
+ let hasPreError = false;
611
+ try {
612
+ // Run inside ContextIdStore.run so pre-processors can do tenant-scoped storage lookups.
613
+ await ContextIdStore.run(contextIds, async () => {
614
+ for (const pre of this._restChains.pre) {
615
+ await pre(httpServerRequest, httpResponse, restRoute, contextIds, processorState, {
616
+ loggingComponentType: this._loggingComponentType
617
+ });
618
+ }
619
+ });
620
+ }
621
+ catch (err) {
622
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
623
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode, this._includeErrorStack);
624
+ hasPreError = true;
625
+ }
626
+ // A pre-processor may set an error response without throwing; treat that as halt.
627
+ if (!hasPreError &&
628
+ Is.integer(httpResponse.statusCode) &&
629
+ httpResponse.statusCode >= HttpStatusCode.badRequest) {
630
+ hasPreError = true;
631
+ }
632
+ // Don't run the main processing if there was an error in the pre processing
633
+ // As this is likely to perform tasks such as authentication which may have failed
634
+ if (!hasPreError) {
635
+ try {
636
+ // Run the processors within an async context
637
+ // so that any services can access the context ids
638
+ await ContextIdStore.run(contextIds, async () => {
639
+ for (const process of this._restChains.process) {
640
+ await process(httpServerRequest, httpResponse, restRoute, processorState, {
641
+ loggingComponentType: this._loggingComponentType
642
+ });
643
+ }
644
+ });
645
+ }
646
+ catch (err) {
647
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
648
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode, this._includeErrorStack);
649
+ }
650
+ }
651
+ try {
652
+ // Always run the post processors, even if there was an error earlier
653
+ // as they may perform cleanup tasks, or logging etc
654
+ await ContextIdStore.run(contextIds, async () => {
655
+ for (const post of this._restChains.post) {
656
+ await post(httpServerRequest, httpResponse, restRoute, contextIds, processorState, {
657
+ loggingComponentType: this._loggingComponentType
658
+ });
659
+ }
660
+ });
661
+ }
662
+ catch (err) {
663
+ // Just log post processor errors
664
+ await this._logging?.log({
665
+ level: "error",
666
+ ts: Date.now(),
667
+ source: FastifyWebServer.CLASS_NAME,
668
+ message: "postProcessorError",
669
+ error: BaseError.fromError(err),
670
+ data: {
671
+ route: restRoute?.path ?? ""
672
+ }
673
+ });
674
+ }
675
+ }
676
+ /**
677
+ * Handle the incoming socket request.
678
+ * @param socketRouteProcessors The hooks to process the incoming requests.
679
+ * @param socketRoute The socket route to handle.
680
+ * @param socket The socket to handle.
681
+ * @param fullPath The full path of the socket route.
682
+ * @param emitTopic The topic to emit the response on.
683
+ * @param request The incoming request.
684
+ * @internal
685
+ */
686
+ async handleRequestSocket(socketRouteProcessors, socketRoute, socket, fullPath, emitTopic, request) {
687
+ const socketServerRequest = {
688
+ method: HttpMethod.GET,
689
+ url: fullPath,
690
+ query: socket.handshake.query,
691
+ headers: socket.handshake.headers,
692
+ // The payload arrives straight off the wire, so it can be missing or any shape
693
+ body: Is.object(request) ? request.body : undefined,
694
+ socketId: socket.id
695
+ };
696
+ const httpResponse = {};
697
+ const contextIds = {};
698
+ const processorState = {};
699
+ delete socketServerRequest.query?.EIO;
700
+ delete socketServerRequest.query?.transport;
701
+ await this.runProcessorsSocket(socketRouteProcessors, socketRoute, socketServerRequest, httpResponse, contextIds, processorState, emitTopic, async (topic, response) => {
702
+ socket.emit(topic, response);
703
+ });
704
+ }
705
+ /**
706
+ * Run the socket processors for the route.
707
+ * @param socketRouteProcessors The processors to run.
708
+ * @param socketRoute The route to process.
709
+ * @param socketServerRequest The incoming request.
710
+ * @param httpResponse The outgoing response.
711
+ * @param contextIds The context IDs of the request.
712
+ * @param processorState The state handed through the processors.
713
+ * @param requestTopic The topic of the request.
714
+ * @param responseEmitter The emitter to send the response on.
715
+ * @internal
716
+ */
717
+ async runProcessorsSocket(socketRouteProcessors, socketRoute, socketServerRequest, httpResponse, contextIds, processorState, requestTopic, responseEmitter) {
718
+ // Custom emit method which will also call the post processors
719
+ const postProcessEmit = async (topic, response, responseProcessorState) => {
720
+ await responseEmitter(topic, response);
721
+ try {
722
+ // The post processors are called after the response has been emitted
723
+ for (const post of this._socketChains.post) {
724
+ await post(socketServerRequest, response, socketRoute, contextIds, responseProcessorState, {
725
+ loggingComponentType: this._loggingComponentType
726
+ });
727
+ }
728
+ }
729
+ catch (err) {
730
+ await this._logging?.log({
731
+ level: "error",
732
+ ts: Date.now(),
733
+ source: FastifyWebServer.CLASS_NAME,
734
+ message: "postProcessorError",
735
+ error: BaseError.fromError(err),
736
+ data: {
737
+ route: socketRoute.path
738
+ }
739
+ });
740
+ }
741
+ };
742
+ try {
743
+ for (const pre of this._socketChains.pre) {
744
+ await pre(socketServerRequest, httpResponse, socketRoute, contextIds, processorState, {
745
+ loggingComponentType: this._loggingComponentType
746
+ });
747
+ }
748
+ // We always call all the processors regardless of any response set by a previous processor.
749
+ // But if a pre processor sets a status code, we will emit the response manually, as the pre
750
+ // and post processors do not receive the emit method, they just populate the response object.
751
+ if (!Is.empty(httpResponse.statusCode)) {
752
+ await postProcessEmit(requestTopic, httpResponse, processorState);
753
+ }
754
+ await ContextIdStore.run(contextIds, async () => {
755
+ for (const process of this._socketChains.process) {
756
+ await process(socketServerRequest, httpResponse, socketRoute, processorState, async (topic, processResponse) => {
757
+ await postProcessEmit(topic, processResponse, processorState);
758
+ }, this._loggingComponentType);
759
+ }
760
+ });
761
+ // If the processors set the status to any kind of error then we should emit this manually
762
+ if (Is.integer(httpResponse.statusCode) &&
763
+ httpResponse.statusCode >= HttpStatusCode.badRequest) {
764
+ await postProcessEmit(requestTopic, httpResponse, processorState);
765
+ }
766
+ }
767
+ catch (err) {
768
+ // Emit any unhandled errors manually
769
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
770
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode, this._includeErrorStack);
771
+ await postProcessEmit(requestTopic, httpResponse, processorState);
772
+ }
773
+ }
774
+ /**
775
+ * Initialize the cors options.
776
+ * @param options The web server options.
777
+ * @internal
778
+ */
779
+ async initCors(options) {
780
+ let origins = ["*"];
781
+ if (Is.arrayValue(options?.corsOrigins)) {
782
+ origins = options?.corsOrigins;
783
+ }
784
+ else if (Is.stringValue(options?.corsOrigins)) {
785
+ origins = [options?.corsOrigins];
786
+ }
787
+ const hasWildcardOrigin = origins.includes("*");
788
+ const methods = options?.methods ?? [
789
+ HttpMethod.GET,
790
+ HttpMethod.PUT,
791
+ HttpMethod.POST,
792
+ HttpMethod.PATCH,
793
+ HttpMethod.DELETE,
794
+ HttpMethod.OPTIONS
795
+ ];
796
+ const allowedHeaders = [
797
+ "Access-Control-Allow-Origin",
798
+ "Content-Encoding",
799
+ "Accept-Encoding",
800
+ HeaderTypes.ContentType,
801
+ HeaderTypes.Authorization,
802
+ HeaderTypes.Accept
803
+ ];
804
+ const exposedHeaders = [HeaderTypes.ContentDisposition, HeaderTypes.Location];
805
+ if (Is.arrayValue(options?.allowedHeaders)) {
806
+ allowedHeaders.push(...options.allowedHeaders);
807
+ }
808
+ if (Is.arrayValue(options?.exposedHeaders)) {
809
+ exposedHeaders.push(...options.exposedHeaders);
810
+ }
811
+ await this._fastify.register(FastifyCors, {
812
+ origin: (origin, callback) => {
813
+ callback(null, hasWildcardOrigin ? true : origins.includes(origin));
814
+ },
815
+ methods,
816
+ allowedHeaders,
817
+ exposedHeaders,
818
+ credentials: true
819
+ });
820
+ }
821
+ /**
822
+ * Resolve the route processors in to per phase chains, so that the lookups and bindings
823
+ * are performed once when the server is built instead of on every request.
824
+ * @param restRouteProcessors The processors for the incoming REST requests.
825
+ * @param socketRouteProcessors The processors for the incoming socket requests.
826
+ * @internal
827
+ */
828
+ buildProcessorChains(restRouteProcessors, socketRouteProcessors) {
829
+ const restChains = { pre: [], process: [], post: [] };
830
+ for (const routeProcessor of restRouteProcessors ?? []) {
831
+ const pre = routeProcessor.pre?.bind(routeProcessor);
832
+ if (Is.function(pre)) {
833
+ restChains.pre.push(pre);
834
+ }
835
+ const process = routeProcessor.process?.bind(routeProcessor);
836
+ if (Is.function(process)) {
837
+ restChains.process.push(process);
838
+ }
839
+ const post = routeProcessor.post?.bind(routeProcessor);
840
+ if (Is.function(post)) {
841
+ restChains.post.push(post);
842
+ }
843
+ }
844
+ const socketChains = {
845
+ connected: [],
846
+ disconnected: [],
847
+ pre: [],
848
+ process: [],
849
+ post: []
850
+ };
851
+ for (const routeProcessor of socketRouteProcessors ?? []) {
852
+ const connected = routeProcessor.connected?.bind(routeProcessor);
853
+ if (Is.function(connected)) {
854
+ socketChains.connected.push(connected);
855
+ }
856
+ const disconnected = routeProcessor.disconnected?.bind(routeProcessor);
857
+ if (Is.function(disconnected)) {
858
+ socketChains.disconnected.push(disconnected);
859
+ }
860
+ const pre = routeProcessor.pre?.bind(routeProcessor);
861
+ if (Is.function(pre)) {
862
+ socketChains.pre.push(pre);
863
+ }
864
+ const process = routeProcessor.process?.bind(routeProcessor);
865
+ if (Is.function(process)) {
866
+ socketChains.process.push(process);
867
+ }
868
+ const post = routeProcessor.post?.bind(routeProcessor);
869
+ if (Is.function(post)) {
870
+ socketChains.post.push(post);
871
+ }
872
+ }
873
+ this._restChains = restChains;
874
+ this._socketChains = socketChains;
875
+ }
876
+ }
877
+ //# sourceMappingURL=fastifyWebServer.js.map