@triophore/falconjs 1.0.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 (53) hide show
  1. package/FalconAuthPlugin.js +473 -0
  2. package/LICENSE +21 -0
  3. package/README.md +2 -0
  4. package/core/auth.js +200 -0
  5. package/core/cache/redis_cacher.js +7 -0
  6. package/core/check_collection.js +9 -0
  7. package/core/crypto/encrypt_decrypt.js +19 -0
  8. package/core/errors.js +48 -0
  9. package/core/logger/log4js.js +89 -0
  10. package/core/logo.js +3 -0
  11. package/core/mongo/generateModelfromJsonFile.js +128 -0
  12. package/core/mongo/mongoSchmeFromJson.js +90 -0
  13. package/core/parse_num.js +8 -0
  14. package/core/rannum.js +33 -0
  15. package/core/ranstring.js +33 -0
  16. package/core/recursive-require-call.js +121 -0
  17. package/core/uitls/mongoose_to_joi.js +72 -0
  18. package/core/uitls/return.js +7 -0
  19. package/falcon.js +1644 -0
  20. package/falconAuthPlugin.js +17 -0
  21. package/falconBaseService.js +532 -0
  22. package/falconBaseWorker.js +540 -0
  23. package/index.js +4 -0
  24. package/out/Falcon.html +777 -0
  25. package/out/falcon.js.html +525 -0
  26. package/out/fonts/OpenSans-Bold-webfont.eot +0 -0
  27. package/out/fonts/OpenSans-Bold-webfont.svg +1830 -0
  28. package/out/fonts/OpenSans-Bold-webfont.woff +0 -0
  29. package/out/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  30. package/out/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  31. package/out/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  32. package/out/fonts/OpenSans-Italic-webfont.eot +0 -0
  33. package/out/fonts/OpenSans-Italic-webfont.svg +1830 -0
  34. package/out/fonts/OpenSans-Italic-webfont.woff +0 -0
  35. package/out/fonts/OpenSans-Light-webfont.eot +0 -0
  36. package/out/fonts/OpenSans-Light-webfont.svg +1831 -0
  37. package/out/fonts/OpenSans-Light-webfont.woff +0 -0
  38. package/out/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  39. package/out/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  40. package/out/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  41. package/out/fonts/OpenSans-Regular-webfont.eot +0 -0
  42. package/out/fonts/OpenSans-Regular-webfont.svg +1831 -0
  43. package/out/fonts/OpenSans-Regular-webfont.woff +0 -0
  44. package/out/index.html +65 -0
  45. package/out/scripts/linenumber.js +25 -0
  46. package/out/scripts/prettify/Apache-License-2.0.txt +202 -0
  47. package/out/scripts/prettify/lang-css.js +2 -0
  48. package/out/scripts/prettify/prettify.js +28 -0
  49. package/out/styles/jsdoc-default.css +358 -0
  50. package/out/styles/prettify-jsdoc.css +111 -0
  51. package/out/styles/prettify-tomorrow.css +132 -0
  52. package/package.json +106 -0
  53. package/settings.js +1 -0
package/falcon.js ADDED
@@ -0,0 +1,1644 @@
1
+ require("dotenv").config({ quiet: true, debug: false });
2
+ const config = process.env;
3
+ const crypto = require("crypto");
4
+ const mongoose = require("mongoose");
5
+ const bcrypt = require("bcrypt");
6
+ const Hapi = require("@hapi/hapi");
7
+ const Blipp = require("blipp");
8
+ const Inert = require("@hapi/inert");
9
+ const path = require("path");
10
+ const fs = require("fs");
11
+ const fsp = require("fs/promises");
12
+ const Crumb = require("@hapi/crumb");
13
+ const JWT = require("jsonwebtoken");
14
+ const { v4: uuidv4 } = require("uuid");
15
+ const { Server } = require("socket.io");
16
+ const redis = require("redis");
17
+ const axios = require("axios");
18
+ const spawn = require("child_process").spawn;
19
+ const customParser = require("socket.io-msgpack-parser");
20
+ const encrypt = require("./core/crypto/encrypt_decrypt").encrypt;
21
+ const decrypt = require("./core/crypto/encrypt_decrypt").decrypt;
22
+ const mqtt = require("mqtt");
23
+ const Scooter = require("@hapi/scooter");
24
+ const { createServer } = require("node:http");
25
+ const logo = require("./core/logo").logo;
26
+ const Joi = require("joi");
27
+ const Boom = require("@hapi/boom");
28
+ const { ConfigurationError, ValidationError } = require("./core/errors");
29
+ const { RateLimiterMemory } = require("rate-limiter-flexible");
30
+ const mongooseToJoi = require("./core/uitls/mongoose_to_joi");
31
+
32
+ /**
33
+ * Represents the Falcon Server Core.
34
+ * Handles initialization of Database, Redis, MQTT, Workers, and Hapi.js HTTP Server.
35
+ *
36
+ * @class
37
+ * @example
38
+ * const app = new Falcon(__dirname);
39
+ * app.setAuthStrategy(async (server, config) => {
40
+ * // Custom auth logic
41
+ * });
42
+ * await app.init();
43
+ */
44
+ class Falcon {
45
+ // #models = {}
46
+
47
+ /**
48
+ * Creates an instance of the Falcon Server.
49
+ * @constructor
50
+ * @param {String} basePath - The absolute path to the application root (usually __dirname).
51
+ * @throws {Error} Will throw an error if the 'settings.js' file is missing in the basePath.
52
+ */
53
+ constructor(basePath) {
54
+ // Validate input
55
+ if (!basePath || typeof basePath !== 'string') {
56
+ throw new ValidationError('basePath must be a valid string');
57
+ }
58
+
59
+ // Check if settings file exists
60
+ const settingsPath = path.join(basePath, "settings.js");
61
+ if (!fs.existsSync(settingsPath)) {
62
+ throw new ConfigurationError(`Settings file not found at: ${settingsPath}`);
63
+ }
64
+
65
+ this.SETTINGS = require(settingsPath).settings;
66
+ this.db = null;
67
+ this.redisClient = null;
68
+ this.httpServer = null;
69
+ this.socketIoConfig = this.SETTINGS || null;
70
+ this.models = {};
71
+ this.CONTEXT = {};
72
+ this.CONTEXT["models"] = {};
73
+ this.CONTEXT["env"] = config;
74
+ this.CONTEXT["services"] = [];
75
+ this.CONTEXT["workers"] = {};
76
+ this.aedes = null;
77
+ this.mqttClient = null;
78
+ this.mqttServer = null;
79
+ this.mqttPort = null;
80
+ this.CONTEXT["mqtt"] = null;
81
+ this.CONTEXT["settings"] = this.SETTINGS;
82
+
83
+ if (this.SETTINGS.log) {
84
+ this.CONTEXT["logger"] = require("./core/logger/log4js").getLogger(
85
+ this.SETTINGS.log,
86
+ );
87
+ }
88
+
89
+ this.config = config;
90
+ this.basePath = basePath;
91
+ this.CONTEXT["custom"] = {};
92
+ this.customAuthHandler = null;
93
+ }
94
+
95
+ /**
96
+ * Adds a custom context to the Falcon instance that can be accessed throughout the application.
97
+ * The context is stored with special delimiters to avoid naming conflicts.
98
+ *
99
+ * @param {string} customContextName - The name identifier for the custom context
100
+ * @param {*} customContext - The context data/object to store (can be any type)
101
+ * @returns {Falcon} The current Falcon instance for method chaining
102
+ *
103
+ * @example
104
+ * const app = new Falcon(__dirname);
105
+ * app.addContext('database', { host: 'localhost', port: 5432 });
106
+ * app.addContext('cache', redisClient);
107
+ */
108
+ addContext(customContextName, customContext) {
109
+ if (!customContextName || typeof customContextName !== 'string') {
110
+ throw new ValidationError('customContextName must be a non-empty string');
111
+ }
112
+ this.CONTEXT["custom"]["$$" + customContextName + "$$"] = customContext;
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Retrieves a previously stored custom context by name.
118
+ * Note: This method has a bug - it doesn't return the context value.
119
+ *
120
+ * @param {string} custom_context_name - The name identifier of the context to retrieve
121
+ * @returns {Falcon} The current Falcon instance (should return the context value)
122
+ *
123
+ * @example
124
+ * const dbConfig = app.getContext('database');
125
+ *
126
+ * @todo Fix return statement to actually return the context value
127
+ */
128
+ getContext(custom_context_name) {
129
+ return this.CONTEXT["custom"]["$$" + custom_context_name + "$$"];
130
+ }
131
+
132
+ /**
133
+ * Removes a custom context from the Falcon instance if it exists.
134
+ *
135
+ * @param {string} custom_context_name - The name identifier of the context to remove
136
+ * @returns {Falcon} The current Falcon instance for method chaining
137
+ *
138
+ * @example
139
+ * app.removeContext('database');
140
+ */
141
+ removeContext(custom_context_name) {
142
+ if (
143
+ this.CONTEXT["custom"].hasOwnProperty("$$" + custom_context_name + "$$")
144
+ ) {
145
+ delete this.CONTEXT["custom"]["$$" + custom_context_name + "$$"];
146
+ }
147
+ return this;
148
+ }
149
+
150
+ /**
151
+ * Validates environment variables using Joi schema.
152
+ * Ensures required environment variables are present and have valid values.
153
+ *
154
+ * @private
155
+ * @throws {Error} Throws error if environment validation fails
156
+ */
157
+ async #validateEnv() {
158
+ const schema = Joi.object({
159
+ MODE: Joi.string().valid('DEV', 'PROD').default('DEV'),
160
+ MQTT_PORT: Joi.number().optional(),
161
+ MQTT_URL: Joi.string().optional(),
162
+ REDIS_ENABLE: Joi.string().valid('true', 'false').default('false'),
163
+ REDIS_URL: Joi.string().when('REDIS_ENABLE', { is: 'true', then: Joi.required() })
164
+ }).unknown();
165
+
166
+ const { error, value } = schema.validate(process.env);
167
+ if (error) {
168
+ throw new Error(`Config validation error: ${error.message}`);
169
+ }
170
+ this.config = value; // Update config with defaults
171
+ this.CONTEXT["env"] = this.config;
172
+
173
+ if (!process.env.MODE) {
174
+ this.CONTEXT["logger"]?.warn("MODE not set in env, defaulting to PROD");
175
+ }
176
+ }
177
+
178
+ #isDev() {
179
+ return process.env.MODE == "DEV" ? true : false;
180
+ }
181
+
182
+ async #mqtt_aedes() {
183
+ this.aedes = require("aedes")();
184
+ this.mqtt_client = false;
185
+ this.mqtt_server = require("net").createServer(this.aedes.handle);
186
+ this.mqtt_port = process.env.MQTT_PORT;
187
+ this.mqtt_server.listen(this.mqtt_port, function () {
188
+ this.CONTEXT["logger"].info(
189
+ "MQTT server started and listening on port ",
190
+ this.mqtt_port,
191
+ );
192
+ });
193
+ this.CONTEXT["mqtt"] = this.mqtt_server;
194
+ }
195
+ async #mqtt_external() {
196
+ this.CONTEXT["mqtt"] = null;
197
+ }
198
+
199
+ async #mqtt() {
200
+ if (this.SETTINGS.mqtt) {
201
+ if (this.SETTINGS.mqtt.internal) {
202
+ await this.#mqtt_aedes();
203
+ } else {
204
+ if (this.SETTINGS.mqtt.external) {
205
+ await this.#mqtt_external();
206
+ } else {
207
+ await this.#mqtt_aedes();
208
+ }
209
+ }
210
+ } else {
211
+ await this.#mqtt_aedes();
212
+ }
213
+ }
214
+
215
+ async #Prep() {
216
+ this.CONTEXT["logger"].info("");
217
+ this.CONTEXT["logger"].info("");
218
+ this.CONTEXT["logger"].info(
219
+ "......................................................................",
220
+ );
221
+ this.CONTEXT["logger"].info("TRIOPHORE SERVER");
222
+ this.CONTEXT["logger"].info("Developed and maintained by");
223
+ this.CONTEXT["logger"].info(logo);
224
+ this.CONTEXT["logger"].info("Triophore");
225
+ this.CONTEXT["logger"].info("visit us on : https://triophore.com");
226
+ this.CONTEXT["logger"].info(
227
+ "......................................................................",
228
+ );
229
+ this.CONTEXT["logger"].info(
230
+ "......................................................................",
231
+ );
232
+ this.CONTEXT["logger"].info("Starting server");
233
+ this.CONTEXT["logger"].info("Date :: " + Date.now());
234
+ this.CONTEXT["logger"].info(
235
+ "......................................................................",
236
+ );
237
+ this.CONTEXT["logger"].info("");
238
+ this.CONTEXT["logger"].info(
239
+ "......................................................................",
240
+ );
241
+ this.CONTEXT["logger"].info("Starting bind for services");
242
+ this.CONTEXT["logger"].info("Date :: " + Date.now());
243
+ this.CONTEXT["logger"].info("Binding complete");
244
+ this.CONTEXT["logger"].info(
245
+ "......................................................................",
246
+ );
247
+ this.CONTEXT["logger"].info("");
248
+ this.CONTEXT["logger"].info("MQTT Service details");
249
+ this.CONTEXT["logger"].info("launching worker process....");
250
+ }
251
+
252
+ /**
253
+ * Initializes the Falcon server with all configured components.
254
+ * This method sets up graceful shutdown handlers, database connections, Redis,
255
+ * models, HTTP server, MQTT client, Socket.IO, and runs post-initialization hooks.
256
+ *
257
+ * @async
258
+ * @returns {Promise<Falcon>} The current Falcon instance after successful initialization
259
+ * @throws {Error} Throws error if any initialization step fails
260
+ *
261
+ * @example
262
+ * const app = new Falcon(__dirname);
263
+ * await app.init();
264
+ * await app.runServer();
265
+ */
266
+ async init() {
267
+ await this.#validateEnv();
268
+ this.setupGracefulShutdown();
269
+ await this.#Prep();
270
+ await this.#startService();
271
+ await this.#startServiceDB();
272
+ await this.#startRedis();
273
+ // this.CONTEXT["return"] = require("./core/uitls/return").return_obj;
274
+ await this.#loadModels();
275
+ await this.#startHttpServer();
276
+ await this.#loadRoutes();
277
+ await this.#startMqttClient();
278
+ await this.#startSocketIO();
279
+ await this.#generateSwaggerSchemas(); // Generate schemas before routes
280
+ await this.#startCrudRoutes(); // Added CRUD generation
281
+
282
+ await this.#Postinit();
283
+
284
+ // Final immutable context
285
+ const deepFreeze = (obj) => {
286
+ if (typeof obj !== "object" || obj === null) return obj;
287
+ Object.freeze(obj);
288
+ Object.keys(obj).forEach(key => {
289
+ const value = obj[key];
290
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
291
+ deepFreeze(value);
292
+ }
293
+ });
294
+ return obj;
295
+ };
296
+
297
+ // Deep freeze the most critical parts
298
+ const immutableModels = deepFreeze({ ...this.CONTEXT.models });
299
+ const immutableValidators = deepFreeze({ ...this.CONTEXT.validators });
300
+
301
+ // Create final frozen context
302
+ this.CONTEXT = Object.freeze({
303
+ ...this.CONTEXT,
304
+ models: immutableModels,
305
+ validators: immutableValidators,
306
+ // Allow custom to remain mutable (for flexibility)
307
+ custom: this.CONTEXT.custom || {},
308
+ });
309
+
310
+ this.CONTEXT.logger.info("Falcon ready — CONTEXT is now immutable and protected!");
311
+ return this;
312
+ }
313
+
314
+ async #startService() {
315
+ if (this.SETTINGS.hasOwnProperty("services")) {
316
+ if (this.SETTINGS.services.length > 0) {
317
+ this.CONTEXT["logger"].info(
318
+ "-----------------Spawning Services----------------",
319
+ );
320
+ this.CONTEXT["logger"].info(
321
+ "......................................................................",
322
+ );
323
+ this._spawnServices(this.SETTINGS.services);
324
+ this.CONTEXT["logger"].info(
325
+ "......................................................................",
326
+ );
327
+ this.CONTEXT["logger"].info(
328
+ "-----------------Spawning Services----------------",
329
+ );
330
+ } else {
331
+ this.CONTEXT["logger"].info(
332
+ "-----------------Spawning Services----------------",
333
+ );
334
+ this.CONTEXT["logger"].info(
335
+ "......................................................................",
336
+ );
337
+ this.CONTEXT["logger"].info("No services registered..");
338
+ this.CONTEXT["logger"].info(
339
+ "......................................................................",
340
+ );
341
+ this.CONTEXT["logger"].info(
342
+ "-----------------Spawning Services----------------",
343
+ );
344
+ }
345
+ }
346
+ }
347
+
348
+ async #Postinit() {
349
+ if (this.SETTINGS.postInit) {
350
+ let post_file_path = path.join(
351
+ this.basePath,
352
+ "init",
353
+ this.SETTINGS.postInit + ".js",
354
+ );
355
+ if (fs.existsSync(post_file_path)) {
356
+ await require(post_file_path).run(this.CONTEXT);
357
+ }
358
+ }
359
+ }
360
+
361
+ async #startServiceDB() {
362
+ if (this.SETTINGS.database) {
363
+ if (this.SETTINGS.database.mongodb) {
364
+ if (this.#isDev()) {
365
+ mongoose.set("debug", true);
366
+ this.CONTEXT["logger"].debug("Mongoose ODM is set in Debug Mode");
367
+ this.CONTEXT["logger"].info("Mongoose Log will be collected");
368
+ }
369
+ await mongoose.connect(this.SETTINGS.database.mongodb.database, {}); //useNewUrlParser: true, useUnifiedTopology: true
370
+ mongoose.set("debug", (collectionName, method, query, doc) => {
371
+ this.CONTEXT["logger"].info(
372
+ `MONGOOSE ==> ${collectionName}.${method}`,
373
+ JSON.stringify(query),
374
+ doc,
375
+ );
376
+ });
377
+ this.db = mongoose.connection.db;
378
+ this.CONTEXT["logger"].info("MongoDB connected");
379
+ this.CONTEXT["db"] = this.db;
380
+ this.CONTEXT["mongoose"] = mongoose;
381
+ }
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Registers a plugin with the Falcon server. Plugins can extend server functionality
387
+ * for different components like server middleware, Socket.IO handlers, or Mongoose plugins.
388
+ *
389
+ * @async
390
+ * @param {Object} pluginObj - The plugin configuration object
391
+ * @param {string} pluginObj.type - Plugin type: 'server', 'socketio', 'mongoose', or 'falcon'
392
+ * @param {*} pluginObj.plugin - The actual plugin implementation
393
+ * @returns {Promise<Falcon>} The current Falcon instance for method chaining
394
+ * @throws {Error} Throws error if plugin type is invalid
395
+ *
396
+ * @example
397
+ * await app.AddPlugin({
398
+ * type: 'server',
399
+ * plugin: myHapiPlugin
400
+ * });
401
+ */
402
+ async AddPlugin(pluginObj) {
403
+ this.CONTEXT["logger"].info("Plugin added");
404
+ await this.#regsiterPlugin(pluginObj);
405
+ return this;
406
+ }
407
+
408
+ async #regsiterPlugin(pluginObj) {
409
+ switch (pluginObj.type) {
410
+ case "server":
411
+ // this.middleware.push(pluginObj);
412
+ break;
413
+ case "socketio":
414
+ // this.controllers.push(pluginObj);
415
+ break;
416
+ case "mongoose":
417
+ break;
418
+ case "falcon":
419
+ break;
420
+ default:
421
+ throw new Error(`Invalid plugin type: ${pluginObj.type}`);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Asynchronously checks if a given path exists and is a directory.
427
+ * This utility method provides safe directory existence checking with proper error handling.
428
+ *
429
+ * @async
430
+ * @param {string} directoryPath - The file system path to check
431
+ * @returns {Promise<boolean>} True if path exists and is a directory, false if path doesn't exist
432
+ * @throws {Error} Throws error for permission issues or other file system errors (not ENOENT)
433
+ *
434
+ * @example
435
+ * const exists = await app.checkDirectoryAsync('./models');
436
+ * if (exists) {
437
+ * console.log('Models directory found');
438
+ * }
439
+ *
440
+ * @example
441
+ * try {
442
+ * const isDir = await app.checkDirectoryAsync('/restricted/path');
443
+ * } catch (error) {
444
+ * console.error('Permission denied or other error:', error.message);
445
+ * }
446
+ */
447
+ async checkDirectoryAsync(directoryPath) {
448
+ try {
449
+ // Use fs.promises.stat() to get file statistics
450
+ const stats = await fs.promises.stat(directoryPath);
451
+ // Check if the path exists AND if it's a directory
452
+ return stats.isDirectory();
453
+ } catch (error) {
454
+ // Check if the error is "No such file or directory" (ENOENT)
455
+ if (error.code === "ENOENT") {
456
+ return false;
457
+ }
458
+ // For other errors (permissions, etc.), re-throw the error
459
+ throw error;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Recursively finds all JavaScript files in a directory.
465
+ * Skips hidden directories and test files for efficiency.
466
+ *
467
+ * @private
468
+ * @param {string} dir - Directory to search
469
+ * @param {string[]} files - Array to collect file paths (used for recursion)
470
+ * @returns {Promise<string[]>} Array of JavaScript file paths
471
+ */
472
+ async #findJsFilesRecursively(dir, files = []) {
473
+ const items = await fs.promises.readdir(dir, { withFileTypes: true });
474
+
475
+ for (const item of items) {
476
+ const fullPath = path.join(dir, item.name);
477
+
478
+ if (item.isDirectory() && !item.name.startsWith('.')) {
479
+ await this.#findJsFilesRecursively(fullPath, files);
480
+ }
481
+ else if (item.isFile() && item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
482
+ files.push(fullPath);
483
+ }
484
+ }
485
+
486
+ return files;
487
+ }
488
+
489
+ /**
490
+ * Builds a clean route path from file system path.
491
+ * Converts folder structure to REST-style paths with parameter support.
492
+ *
493
+ * @private
494
+ * @param {string} relativePath - Relative path from routes directory
495
+ * @returns {string} Clean route path
496
+ */
497
+ #buildRoutePath(relativePath) {
498
+ let routePath = '/' + path.dirname(relativePath)
499
+ .replace(/\\/g, '/')
500
+ .replace(/\/index$/gi, '')
501
+ .replace(/\/_/g, '/:')
502
+ .replace(/_/g, '-')
503
+ .replace(/(^|\/)\[([^\]]+)\]/g, '$1:$2');
504
+
505
+ const baseName = path.basename(relativePath, '.js');
506
+ if (baseName !== 'index') {
507
+ let segment = baseName
508
+ .replace(/^_/, ':')
509
+ .replace(/_/g, '-')
510
+ .replace(/^\[([^\]]+)\]$/, ':$1');
511
+ routePath = path.posix.join(routePath, segment);
512
+ }
513
+
514
+ return routePath.replace(/\/+$/, '') || '/';
515
+ }
516
+
517
+ /**
518
+ * Finds the best matching validator for a route.
519
+ *
520
+ * @private
521
+ * @param {string} baseName - Base filename without extension
522
+ * @param {string} relativePath - Relative path from routes directory
523
+ * @param {Object} validators - Available validators
524
+ * @returns {string|null} Validator name or null if not found
525
+ */
526
+ #findBestValidatorName(baseName, relativePath, validators) {
527
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
528
+
529
+ const candidates = [
530
+ baseName,
531
+ baseName + 'Payload',
532
+ path.dirname(relativePath).split(path.sep).pop() + baseName,
533
+ capitalize(baseName),
534
+ baseName.replace(/Handler$/i, ''),
535
+ ];
536
+ for (const c of candidates) {
537
+ if (validators[c]) return c;
538
+ }
539
+ return null;
540
+ }
541
+
542
+ async #startRedis() {
543
+ if (this.config.REDIS_ENABLE === "true") {
544
+ this.CONTEXT["logger"].info("Redis enabled!");
545
+ this.redis_client = await redis
546
+ .createClient({
547
+ url: this.config.REDIS_URL,
548
+ })
549
+ .on("error", (err) =>
550
+ this.CONTEXT["logger"].error(`Error connecting to Redis ${err}`),
551
+ )
552
+ .on("ready", () =>
553
+ this.CONTEXT["logger"].info(`Connected to Redis ${config.REDIS_URL}`),
554
+ )
555
+ .connect();
556
+ this.CONTEXT["redis"] = this.redis_client;
557
+ } else {
558
+ this.CONTEXT["logger"].warn("Redis disabled");
559
+ this.CONTEXT["redis"] = false;
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Dynamically adds a route to the Hapi.js server after initialization.
565
+ * This allows for runtime route registration beyond the initial setup.
566
+ *
567
+ * @async
568
+ * @param {Object} route - Hapi.js route configuration object
569
+ * @param {string} route.method - HTTP method (GET, POST, PUT, DELETE, etc.)
570
+ * @param {string} route.path - Route path with optional parameters
571
+ * @param {Function} route.handler - Route handler function
572
+ * @param {Object} [route.options] - Additional route options (auth, validation, etc.)
573
+ * @returns {Promise<Falcon>} The current Falcon instance for method chaining
574
+ *
575
+ * @example
576
+ * await app.addRoute({
577
+ * method: 'GET',
578
+ * path: '/api/health',
579
+ * handler: (request, h) => ({ status: 'ok' })
580
+ * });
581
+ */
582
+ async addRoute(route) {
583
+ await this.httpServer.route(route);
584
+ return this;
585
+ }
586
+
587
+ /**
588
+ * Loads routes recursively from the routes directory with automatic validation and Swagger documentation.
589
+ * Supports nested folder structures and automatic validator matching.
590
+ *
591
+ * @private
592
+ * @async
593
+ */
594
+ async #loadRoutes() {
595
+ const routesDir = path.join(this.basePath, "routes");
596
+ const validatorsDir = path.join(this.basePath, "validators");
597
+
598
+ if (!fs.existsSync(routesDir)) {
599
+ this.CONTEXT["logger"]?.info("No routes/ folder — skipping");
600
+ return;
601
+ }
602
+
603
+ // Load validators (for validation + Swagger schemas)
604
+ const validators = {};
605
+ if (fs.existsSync(validatorsDir)) {
606
+ const files = await this.#findJsFilesRecursively(validatorsDir);
607
+ for (const file of files) {
608
+ const name = path.basename(file, '.js');
609
+ try {
610
+ const schema = require(file);
611
+ if (schema && typeof schema.describe === 'function') {
612
+ validators[name] = schema;
613
+ }
614
+ } catch (e) { /* ignore */ }
615
+ }
616
+ }
617
+ this.CONTEXT.validators = validators;
618
+
619
+ // Register Swagger first (needs to be before routes)
620
+ await this.#registerSwagger();
621
+
622
+ this.CONTEXT["logger"].info("Loading routes with validation + Swagger docs...");
623
+
624
+ const routeFiles = await this.#findJsFilesRecursively(routesDir);
625
+
626
+ for (const filePath of routeFiles) {
627
+ const relativePath = path.relative(routesDir, filePath);
628
+ const baseName = path.basename(filePath, '.js');
629
+ let routePath = this.#buildRoutePath(relativePath);
630
+ this.CONTEXT["logger"].info("Route Name : " + routePath);
631
+
632
+ try {
633
+ const routeModule = require(filePath);
634
+ const register = typeof routeModule === 'function' ? routeModule : routeModule.route?.bind(routeModule);
635
+
636
+ if (typeof register !== 'function') continue;
637
+
638
+ // Find validator + extract schema
639
+ const validatorName = this.#findBestValidatorName(baseName, relativePath, validators);
640
+ const validator = validatorName ? validators[validatorName] : null;
641
+
642
+ // Auto-generate tag from folder
643
+ const tag = path.dirname(relativePath).split(path.sep)[0] || 'api';
644
+
645
+ const wrappedRoute = async (server, ctx) => {
646
+ // Add auto-generated route info to context
647
+ ctx.route = {
648
+ path: routePath,
649
+ tag: tag,
650
+ validator: validator,
651
+ validatorName: validatorName
652
+ };
653
+
654
+ await register(ctx); // Only pass context, server is already in ctx.server
655
+ };
656
+
657
+ await wrappedRoute(this.httpServer, this.CONTEXT);
658
+
659
+ const valMark = validator ? 'Validated + Documented' : 'Documented';
660
+ this.CONTEXT["logger"].info(`${valMark} ${routePath} ← ${relativePath}`);
661
+ } catch (err) {
662
+ this.CONTEXT["logger"].error(`Route failed: ${filePath}`, err.message);
663
+ }
664
+ }
665
+
666
+ this.CONTEXT["logger"].info("API fully documented at /documentation");
667
+ }
668
+
669
+ /**
670
+ * Registers Swagger UI for API documentation if enabled in settings.
671
+ *
672
+ * @private
673
+ * @async
674
+ */
675
+ async #registerSwagger() {
676
+ if (!this.SETTINGS.swagger?.enabled) return;
677
+
678
+ const Inert = require('@hapi/inert');
679
+ const Vision = require('@hapi/vision');
680
+ const HapiSwagger = require('hapi-swagger');
681
+
682
+ const swaggerOptions = {
683
+ info: {
684
+ title: this.SETTINGS.name || 'Falcon API',
685
+ version: require(path.join(this.basePath, 'package.json')).version || '1.0.0',
686
+ },
687
+ schemes: ['http', 'https'],
688
+ grouping: 'tags',
689
+ tags: [
690
+ { name: 'api', description: 'General API' },
691
+ { name: 'auth', description: 'Authentication' },
692
+ { name: 'users', description: 'User operations' },
693
+ ],
694
+ documentationPath: this.SETTINGS.swagger.path || '/documentation',
695
+
696
+ validatorUrl: null,
697
+ reuseDefinitions: true,
698
+ };
699
+
700
+ await this.httpServer.register([
701
+ Inert,
702
+ Vision,
703
+ {
704
+ plugin: HapiSwagger,
705
+ options: swaggerOptions
706
+ }
707
+ ]);
708
+
709
+ this.CONTEXT["logger"].info(`Swagger UI → http://localhost:${this.SETTINGS.http.port}${swaggerOptions.documentationPath}`);
710
+ }
711
+
712
+ async #loadModels() {
713
+ /*MODELS*/
714
+ if (this.CONTEXT["mongoose"]) {
715
+ let files = await fsp.readdir(
716
+ path.join(this.basePath, "models", "mongo"),
717
+ );
718
+ const jsFiles = files.filter((file) => path.extname(file) === ".js");
719
+ this.CONTEXT["logger"].info(
720
+ "-----------------Registering Model----------------",
721
+ );
722
+ for (let i = 0; i < jsFiles.length; i++) {
723
+ console.log(path.join(this.basePath, "models", "mongo", jsFiles[i]));
724
+ await this._load_model_name(
725
+ path.join(this.basePath, "models", "mongo", jsFiles[i]),
726
+ );
727
+ }
728
+ // for (let mindex = 0; mindex < _models.length; mindex++) {
729
+ // const model = _models[mindex];
730
+ // if (await fsp.exists(path.join(this.basePath, "models", model + ".js"))) {
731
+ // await this.#load_model_name(model);
732
+ // } else {
733
+ // this.CONTEXT["logger"].error("Model file not found :: " + model)
734
+ // }
735
+ // }
736
+ this.CONTEXT["logger"].info(
737
+ "-----------------Registering Model----------------",
738
+ );
739
+ }
740
+
741
+ /*MODELS*/
742
+ // this.CONTEXT["models"] = models;
743
+ }
744
+
745
+ async #startHttpServer() {
746
+ //creating server object
747
+ this.httpServer = null;
748
+ this.httpServer = Hapi.server({
749
+ port: this.SETTINGS.http.port,
750
+ host: this.SETTINGS.http.host,
751
+ routes: {
752
+ cors: true,
753
+ },
754
+ debug: { request: ['error'] } // Enable better error logging
755
+ });
756
+
757
+ // Set Joi as the validator for the server
758
+ this.httpServer.validator(Joi);
759
+ this.httpServer.events.on("log", (event, tags) => {
760
+ this.CONTEXT["logger"].error(
761
+ `Server error: ${event.error ? event.error.message : "unknown"} -- ${tags}`,
762
+ );
763
+ });
764
+ this.httpServer.events.on("request", (request, event, tags) => {
765
+ this.CONTEXT["logger"].info(`Server request: ${request} ${event}`);
766
+ });
767
+
768
+ //Enable Blipp
769
+ await this.httpServer.register({
770
+ plugin: Blipp,
771
+ options: { showAuth: true },
772
+ });
773
+
774
+ // Health Checks
775
+ await this.httpServer.register({
776
+ plugin: require('hapi-alive'),
777
+ options: {
778
+ path: '/health',
779
+ tags: ['health', 'monitor'],
780
+ responses: {
781
+ healthy: { message: 'I am healthy' },
782
+ unhealthy: { message: 'I am unhealthy' }
783
+ }
784
+ }
785
+ });
786
+
787
+ // Rate Limiter
788
+ const rateLimiter = new RateLimiterMemory({
789
+ points: 10, // 10 points
790
+ duration: 1, // per second
791
+ });
792
+
793
+ this.httpServer.ext('onPreHandler', async (request, h) => {
794
+ try {
795
+ await rateLimiter.consume(request.info.remoteAddress);
796
+ return h.continue;
797
+ } catch (rejRes) {
798
+ throw Boom.tooManyRequests('Rate limit exceeded');
799
+ }
800
+ });
801
+
802
+ // Security Headers
803
+ this.httpServer.ext('onPreResponse', (request, h) => {
804
+ const response = request.response;
805
+ if (response.isBoom) {
806
+ return h.continue;
807
+ }
808
+ response.header('X-Frame-Options', 'DENY');
809
+ response.header('X-Content-Type-Options', 'nosniff');
810
+ response.header('X-XSS-Protection', '1; mode=block');
811
+ response.header('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
812
+ return h.continue;
813
+ });
814
+
815
+ await this.#registerAuth();
816
+
817
+ //hapi scooter
818
+ await this.httpServer.register(Scooter);
819
+ this.CONTEXT["logger"].info(await this.httpServer.plugins.blipp.info());
820
+
821
+ if (this.#isDev()) {
822
+ this.CONTEXT["logger"].info("-----------------DEV MODE----------------");
823
+ this.CONTEXT["logger"].info("-----------------DEV MODE----------------");
824
+ this.CONTEXT["logger"].info("-----------------DEV MODE----------------");
825
+ }
826
+
827
+ this.CONTEXT["server"] = this.httpServer;
828
+
829
+ this.httpServer.events.on("response", (request) => {
830
+ this.CONTEXT["logger"].info(
831
+ request.info.remoteAddress +
832
+ ": " +
833
+ request.method.toUpperCase() +
834
+ " " +
835
+ request.path +
836
+ " --> " +
837
+ request.response.statusCode,
838
+ );
839
+ });
840
+ this.CONTEXT["logger"].info(
841
+ "-----------------Registered Routes----------------",
842
+ );
843
+ await this.httpServer
844
+ .table()
845
+ .forEach((route) =>
846
+ this.CONTEXT["logger"].info(`${route.method}\t${route.path}`),
847
+ );
848
+ this.CONTEXT["logger"].info(
849
+ "-----------------Registered Routes----------------",
850
+ );
851
+ }
852
+
853
+ /**
854
+ * Registers a custom authentication strategy for the Hapi.js server.
855
+ * This method allows complete control over authentication setup and MUST be called before init().
856
+ * The handler function receives the Falcon context including the server instance.
857
+ *
858
+ * @param {Function} handler - Async function that configures authentication
859
+ * @param {Object} handler.context - Falcon context object containing server, logger, etc.
860
+ * @param {Object} handler.context.server - Hapi.js server instance
861
+ * @param {Object} handler.context.logger - Logger instance
862
+ * @returns {Falcon} The current Falcon instance for method chaining
863
+ *
864
+ * @example
865
+ * // Custom JWT authentication
866
+ * falcon.setAuthStrategy(async (context) => {
867
+ * await context.server.register(require('hapi-auth-jwt2'));
868
+ * context.server.auth.strategy('jwt', 'jwt', {
869
+ * key: process.env.JWT_SECRET,
870
+ * validate: async (decoded) => {
871
+ * const user = await getUserById(decoded.id);
872
+ * return { isValid: !!user, credentials: user };
873
+ * }
874
+ * });
875
+ * context.server.auth.default('jwt');
876
+ * });
877
+ */
878
+ setAuthStrategy(handler) {
879
+ this.customAuthHandler = handler;
880
+ return this;
881
+ }
882
+
883
+ async #registerAuth() {
884
+ // 1. Check if the user provided their own logic
885
+ if (this.customAuthHandler) {
886
+ this.CONTEXT["logger"].info("Registering CUSTOM Auth Strategy...");
887
+
888
+ // Execute the user's function, passing the Hapi server instance
889
+ await this.customAuthHandler(this.CONTEXT);
890
+
891
+ return this;
892
+ }
893
+
894
+ // 2. Register the default Falcon auth plugin if auth is configured in settings
895
+ if (this.SETTINGS.auth && Object.keys(this.SETTINGS.auth).length > 0) {
896
+ this.CONTEXT["logger"].info("Registering Falcon Auth Plugin...");
897
+
898
+ // Pass context to server app for auth plugin access
899
+ this.httpServer.app.falconContext = this.CONTEXT;
900
+
901
+ const { plugin } = require('./FalconAuthPlugin');
902
+ await this.httpServer.register({
903
+ plugin: plugin,
904
+ options: this.SETTINGS.auth
905
+ });
906
+
907
+ // Store auth instance for Socket.IO use
908
+ this.CONTEXT["auth"] = this.httpServer.app.falconAuth;
909
+ } else {
910
+ this.CONTEXT["logger"].info("No auth configuration found - skipping auth setup");
911
+ }
912
+
913
+ return this;
914
+ }
915
+
916
+ /**
917
+ * Starts the Hapi.js HTTP server and begins listening for incoming requests.
918
+ * This method should be called after init() to actually start serving traffic.
919
+ * The server will listen on the host and port specified in settings.
920
+ *
921
+ * @async
922
+ * @returns {Promise<void>} Resolves when the server has started successfully
923
+ * @throws {Error} Throws error if server fails to start
924
+ *
925
+ * @example
926
+ * const app = new Falcon(__dirname);
927
+ * await app.init();
928
+ * await app.runServer();
929
+ * console.log('Server running on', app.CONTEXT.server.info.uri);
930
+ */
931
+ async runServer() {
932
+ await this.CONTEXT["server"].start();
933
+ }
934
+
935
+ async #startSocketIO() {
936
+ if (this.socketIoConfig) {
937
+ const io = new Server(this.httpServer.listener, this.socketIoConfig);
938
+
939
+ // Add authentication middleware if auth is configured
940
+ if (this.CONTEXT["auth"]) {
941
+ const authMiddleware = this.CONTEXT["auth"].createSocketIOMiddleware();
942
+ io.use(authMiddleware);
943
+ this.CONTEXT["logger"].info("Socket.IO authentication middleware enabled");
944
+ }
945
+
946
+ // Store Socket.IO instance in context
947
+ this.CONTEXT["io"] = io;
948
+
949
+ // Load socket handlers from socket/ directory
950
+ await this.#loadSocketHandlers(io);
951
+
952
+ // Basic connection handling
953
+ io.on('connection', (socket) => {
954
+ this.CONTEXT["logger"].info(`Socket.IO client connected: ${socket.id}`);
955
+
956
+ if (socket.authenticated) {
957
+ this.CONTEXT["logger"].info(`Authenticated user: ${socket.user?.id || 'unknown'}`);
958
+ }
959
+
960
+ socket.on('disconnect', () => {
961
+ this.CONTEXT["logger"].info(`Socket.IO client disconnected: ${socket.id}`);
962
+ });
963
+ });
964
+
965
+ } else {
966
+ this.CONTEXT["logger"].info("Socket.IO config not found!");
967
+ }
968
+ }
969
+
970
+ async #loadSocketHandlers(io) {
971
+ const socketDir = path.join(this.basePath, 'sockets');
972
+
973
+ if (!fs.existsSync(socketDir)) {
974
+ this.CONTEXT["logger"].info("Socket handlers directory not found, skipping");
975
+ return;
976
+ }
977
+
978
+ const files = fs.readdirSync(socketDir).filter(file => file.endsWith('.js'));
979
+
980
+ for (const file of files) {
981
+ const filePath = path.join(socketDir, file);
982
+ const handlerName = path.basename(file, '.js');
983
+
984
+ try {
985
+ const handlerModule = require(filePath);
986
+ const handler = typeof handlerModule === 'function' ? handlerModule : handlerModule.handler;
987
+
988
+ if (typeof handler === 'function') {
989
+ await handler(io, this.CONTEXT);
990
+ this.CONTEXT["logger"].info(`Socket handler loaded: ${handlerName}`);
991
+ }
992
+ } catch (err) {
993
+ this.CONTEXT["logger"].error(`Failed to load socket handler ${handlerName}:`, err.message);
994
+ }
995
+ }
996
+ }
997
+
998
+ async #startInternalMqttClient(urlObj) {
999
+ const mqttUrl = this.SETTINGS.mqtt.internal;
1000
+ this.mqttClient = mqtt.connect(`${config.MQTT_URL}`);
1001
+ this.CONTEXT["mqttClient"] = this.mqttClient;
1002
+ this.mqttClient.on("connect", () => {
1003
+ this.mqttClient.subscribe("service_utils", (err) => {
1004
+ this.CONTEXT["logger"].info(
1005
+ "mqtt connected and subscribed to utility service",
1006
+ );
1007
+ });
1008
+
1009
+ // Subscribe to WebSocket topics
1010
+ this.mqttClient.subscribe("websocket_broadcast", (err) => {
1011
+ this.CONTEXT["logger"].info("mqtt subscribed to websocket_broadcast");
1012
+ });
1013
+ this.mqttClient.subscribe("websocket_emit_group", (err) => {
1014
+ this.CONTEXT["logger"].info("mqtt subscribed to websocket_emit_group");
1015
+ });
1016
+ this.mqttClient.subscribe("websocket_emit_socket", (err) => {
1017
+ this.CONTEXT["logger"].info("mqtt subscribed to websocket_emit_socket");
1018
+ });
1019
+ });
1020
+
1021
+ // Handle MQTT messages for WebSocket bridging
1022
+ this.mqttClient.on("message", (topic, message) => {
1023
+ this.#handleMqttToWebSocket(topic, message);
1024
+ });
1025
+ }
1026
+
1027
+ async #startExternalMqttClient() {
1028
+ this.mqttClient = mqtt.connect(`${config.MQTT_URL}`);
1029
+ this.CONTEXT["mqttClient"] = this.mqttClient;
1030
+ this.mqttClient.on("connect", () => {
1031
+ this.mqttClient.subscribe("service_utils", (err) => {
1032
+ this.CONTEXT["logger"].info(
1033
+ "mqtt connected and subscribed to utility service",
1034
+ );
1035
+ });
1036
+
1037
+ // Subscribe to WebSocket topics
1038
+ this.mqttClient.subscribe("websocket_broadcast", (err) => {
1039
+ this.CONTEXT["logger"].info("mqtt subscribed to websocket_broadcast");
1040
+ });
1041
+ this.mqttClient.subscribe("websocket_emit_group", (err) => {
1042
+ this.CONTEXT["logger"].info("mqtt subscribed to websocket_emit_group");
1043
+ });
1044
+ this.mqttClient.subscribe("websocket_emit_socket", (err) => {
1045
+ this.CONTEXT["logger"].info("mqtt subscribed to websocket_emit_socket");
1046
+ });
1047
+ });
1048
+
1049
+ // Handle MQTT messages for WebSocket bridging
1050
+ this.mqttClient.on("message", (topic, message) => {
1051
+ this.#handleMqttToWebSocket(topic, message);
1052
+ });
1053
+ }
1054
+
1055
+ #handleMqttToWebSocket(topic, message) {
1056
+ if (!this.CONTEXT["io"]) return;
1057
+
1058
+ try {
1059
+ const data = JSON.parse(message.toString());
1060
+ const io = this.CONTEXT["io"];
1061
+
1062
+ switch (topic) {
1063
+ case "websocket_broadcast":
1064
+ io.emit(data.event || 'message', data.payload || data);
1065
+ this.CONTEXT["logger"].debug(`WebSocket broadcast: ${data.event}`);
1066
+ break;
1067
+
1068
+ case "websocket_emit_group":
1069
+ if (data.room) {
1070
+ io.to(data.room).emit(data.event || 'message', data.payload || data);
1071
+ this.CONTEXT["logger"].debug(`WebSocket emit to room ${data.room}: ${data.event}`);
1072
+ }
1073
+ break;
1074
+
1075
+ case "websocket_emit_socket":
1076
+ if (data.socketId) {
1077
+ io.to(data.socketId).emit(data.event || 'message', data.payload || data);
1078
+ this.CONTEXT["logger"].debug(`WebSocket emit to socket ${data.socketId}: ${data.event}`);
1079
+ }
1080
+ break;
1081
+ }
1082
+ } catch (err) {
1083
+ this.CONTEXT["logger"].error(`Failed to handle MQTT to WebSocket message:`, err.message);
1084
+ }
1085
+ }
1086
+
1087
+ async #startMqttClient() {
1088
+ if (this.SETTINGS.mqtt) {
1089
+ if (this.SETTINGS.mqtt.internal) {
1090
+ await this.#startInternalMqttClient(this.SETTINGS.mqtt.internal);
1091
+ } else {
1092
+ if (this.SETTINGS.mqtt.external) {
1093
+ await this.#startExternalMqttClient(this.SETTINGS.mqtt.external);
1094
+ } else {
1095
+ await this.#startInternalMqttClient(this.SETTINGS.mqtt.internal);
1096
+ }
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ /**
1102
+ *
1103
+ * Get Socket IO middle ware
1104
+ * @returns Falcon instance
1105
+ */
1106
+ registerSocketIOMiddleware() {
1107
+ return this;
1108
+ }
1109
+
1110
+
1111
+ async #validate(decoded, request, h) {
1112
+ await this.validateAuth(decoded, request, h, this.CONTEXT);
1113
+ }
1114
+
1115
+
1116
+
1117
+ // --- LOADER FUNCTION --- //
1118
+ async _load_model_name(name) {
1119
+ let _temp = await require(name)(mongoose);
1120
+ this.models[path.basename(name).trim()] = _temp;
1121
+ this.CONTEXT["models"][path.basename(name).trim()] = _temp;
1122
+ this.CONTEXT["logger"].info(name + " model registered");
1123
+ }
1124
+ async #load_route_name(name, server) {
1125
+ await require(path.join(this.basePath, "routes", name)).route(
1126
+ server,
1127
+ models,
1128
+ logger,
1129
+ );
1130
+ this.CONTEXT["logger"].info(name + " route registered");
1131
+ }
1132
+ // --- LOADER FUNCTION --- //
1133
+
1134
+ /**
1135
+ * Starts a worker process with the given arguments.
1136
+ * Workers are spawned as separate Node.js processes.
1137
+ *
1138
+ * @param {string} worker_name - Name of the worker file (without .js extension)
1139
+ * @param {Object} args - Arguments to pass to the worker
1140
+ * @returns {Promise<Object>} Worker process information
1141
+ */
1142
+ async startWorker(worker_name, args = {}) {
1143
+ const worker_file = path.join(this.basePath, "workers", worker_name + ".js");
1144
+
1145
+ if (!fs.existsSync(worker_file)) {
1146
+ throw new Error(`Worker file not found: ${worker_file}`);
1147
+ }
1148
+
1149
+ const args_json = JSON.stringify(args);
1150
+ const base64Args = Buffer.from(args_json, "utf8").toString("base64");
1151
+
1152
+ const worker_process = spawn(process.execPath, [worker_file, base64Args], {
1153
+ env: { ...process.env },
1154
+ stdio: ['pipe', 'pipe', 'pipe']
1155
+ });
1156
+
1157
+ worker_process.stdout.setEncoding("utf8");
1158
+ worker_process.stderr.setEncoding("utf8");
1159
+
1160
+ worker_process.stdout.on("data", (data) => {
1161
+ this.CONTEXT["logger"].info(`[WORKER-${worker_name}] ${data}`);
1162
+ });
1163
+
1164
+ worker_process.stderr.on("data", (data) => {
1165
+ this.CONTEXT["logger"].error(`[WORKER-${worker_name}] ERROR: ${data}`);
1166
+ });
1167
+
1168
+ worker_process.on("close", (code) => {
1169
+ this.CONTEXT["logger"].info(`[WORKER-${worker_name}] Exited with code ${code}`);
1170
+ });
1171
+
1172
+ worker_process.on("error", (err) => {
1173
+ this.CONTEXT["logger"].error(`[WORKER-${worker_name}] Failed to start:`, err);
1174
+ });
1175
+
1176
+ // Store worker reference
1177
+ if (!this.CONTEXT["workers"]) {
1178
+ this.CONTEXT["workers"] = {};
1179
+ }
1180
+
1181
+ this.CONTEXT["workers"][worker_name] = {
1182
+ pid: worker_process.pid,
1183
+ process: worker_process,
1184
+ started: new Date(),
1185
+ args: args
1186
+ };
1187
+
1188
+ this.CONTEXT["logger"].info(`Worker ${worker_name} started with PID ${worker_process.pid}`);
1189
+ return this.CONTEXT["workers"][worker_name];
1190
+ }
1191
+
1192
+ /**
1193
+ * Sends a message to a worker via MQTT.
1194
+ *
1195
+ * @param {string} worker_name - Name of the worker
1196
+ * @param {*} message - Message to send
1197
+ * @param {string} [action='job'] - Action type (job, ping, etc.)
1198
+ */
1199
+ sendToWorker(worker_name, message, action = 'job') {
1200
+ if (this.mqtt_client) {
1201
+ const topic = `worker_${worker_name}_${action}`;
1202
+ const payload = typeof message === 'string' ? message : JSON.stringify(message);
1203
+ this.mqtt_client.publish(topic, payload);
1204
+ this.CONTEXT["logger"].info(`Message sent to worker ${worker_name} on topic ${topic}`);
1205
+ } else {
1206
+ this.CONTEXT["logger"].warn("MQTT client not available, cannot send message to worker");
1207
+ }
1208
+ }
1209
+
1210
+ /**
1211
+ * Sends a message to a service via MQTT.
1212
+ *
1213
+ * @param {string} service_name - Name of the service
1214
+ * @param {*} message - Message to send
1215
+ */
1216
+ sendToService(service_name, message) {
1217
+ if (this.mqtt_client) {
1218
+ const topic = `service_${service_name}`;
1219
+ const payload = typeof message === 'string' ? message : JSON.stringify(message);
1220
+ this.mqtt_client.publish(topic, payload);
1221
+ this.CONTEXT["logger"].info(`Message sent to service ${service_name}`);
1222
+ } else {
1223
+ this.CONTEXT["logger"].warn("MQTT client not available, cannot send message to service");
1224
+ }
1225
+ }
1226
+
1227
+ /**
1228
+ * Broadcasts a message to all websocket clients.
1229
+ *
1230
+ * @param {*} data - Data to broadcast
1231
+ */
1232
+ broadcastToWebsockets(data) {
1233
+ if (this.mqtt_client) {
1234
+ this.mqtt_client.publish('websocket_broadcast', JSON.stringify(data));
1235
+ this.CONTEXT["logger"].info("Message broadcasted to websockets");
1236
+ }
1237
+ }
1238
+
1239
+ #workers() {
1240
+ // Legacy method - keeping for backward compatibility
1241
+ this.CONTEXT.startWorker = this.startWorker.bind(this);
1242
+ }
1243
+
1244
+ _spawnServices(filesToRun) {
1245
+ filesToRun.forEach((file) => {
1246
+ const spawnOptions = {
1247
+ stdio: ["pipe", "pipe", "pipe"], // Pipe stdout and stderr to our custom logging function
1248
+ env: { ...process.env, LOG4JS_LOGGER_LEVEL: "DEBUG" }, // Set the Log4js logger level for each process
1249
+ };
1250
+
1251
+ const s_path = path.join(this.basePath, "services", file + ".js");
1252
+
1253
+ // Use process.execPath to ensure we use the same Node binary
1254
+ const cprocess = spawn(process.execPath, [s_path], spawnOptions);
1255
+
1256
+ cprocess.stdout.on("data", (data) => {
1257
+ this.CONTEXT["logger"].info(
1258
+ `------------------------SERVICE - ${file}----------------`,
1259
+ );
1260
+ this.CONTEXT["logger"].info(data.toString());
1261
+ this.CONTEXT["logger"].info(
1262
+ `------------------------SERVICE - ${file}----------------`,
1263
+ );
1264
+ });
1265
+ cprocess.stderr.on("data", (data) => {
1266
+ this.CONTEXT["logger"].error(
1267
+ `------------------------SERVICE - ${file}----------------`,
1268
+ );
1269
+ this.CONTEXT["logger"].error(data.toString());
1270
+ this.CONTEXT["logger"].error(
1271
+ `------------------------SERVICE - ${file}----------------`,
1272
+ );
1273
+ });
1274
+
1275
+ cprocess.on('error', (err) => {
1276
+ this.CONTEXT["logger"].error(`Failed to spawn service ${file}:`, err);
1277
+ });
1278
+
1279
+ cprocess.on('exit', (code, signal) => {
1280
+ if (code !== 0) {
1281
+ this.CONTEXT["logger"].warn(`Service ${file} exited with code ${code} and signal ${signal}`);
1282
+ } else {
1283
+ this.CONTEXT["logger"].info(`Service ${file} exited gracefully.`);
1284
+ }
1285
+ });
1286
+
1287
+ this.CONTEXT["services"].push({
1288
+ pid: cprocess.pid,
1289
+ stdout: cprocess.stdout,
1290
+ stderr: cprocess.stderr,
1291
+ });
1292
+ });
1293
+ }
1294
+
1295
+ /**
1296
+ * Gracefully closes all active connections and cleans up resources.
1297
+ * This method safely shuts down HTTP server, database connections, Redis client,
1298
+ * MQTT connections, and terminates spawned service processes.
1299
+ *
1300
+ * @async
1301
+ * @returns {Promise<void>} Resolves when all cleanup operations complete
1302
+ *
1303
+ * @example
1304
+ * // Manual cleanup
1305
+ * await app.cleanup();
1306
+ *
1307
+ * // Automatic cleanup on process signals (handled by setupGracefulShutdown)
1308
+ * process.on('SIGTERM', async () => {
1309
+ * await app.cleanup();
1310
+ * process.exit(0);
1311
+ * });
1312
+ */
1313
+ async cleanup() {
1314
+ if (this.CONTEXT["logger"]) {
1315
+ this.CONTEXT["logger"].info("Starting cleanup process...");
1316
+ }
1317
+
1318
+ // Close HTTP server
1319
+ if (this.httpServer) {
1320
+ await this.httpServer.stop();
1321
+ this.httpServer = null;
1322
+ }
1323
+
1324
+ // Close MongoDB connection
1325
+ if (this.db && mongoose.connection.readyState === 1) {
1326
+ await mongoose.connection.close();
1327
+ this.db = null;
1328
+ }
1329
+
1330
+ // Close Redis connection
1331
+ if (this.redis_client && this.redis_client.isOpen) {
1332
+ await this.redis_client.quit();
1333
+ this.redis_client = null;
1334
+ }
1335
+
1336
+ // Close MQTT connections
1337
+ if (this.mqtt_client && this.mqtt_client.connected) {
1338
+ await this.mqtt_client.end();
1339
+ this.mqtt_client = null;
1340
+ }
1341
+
1342
+ if (this.mqtt_server) {
1343
+ this.mqtt_server.close();
1344
+ this.mqtt_server = null;
1345
+ }
1346
+
1347
+ // Kill spawned services
1348
+ if (this.CONTEXT["services"] && this.CONTEXT["services"].length > 0) {
1349
+ this.CONTEXT["services"].forEach(service => {
1350
+ if (service.pid) {
1351
+ process.kill(service.pid, 'SIGTERM');
1352
+ }
1353
+ });
1354
+ }
1355
+
1356
+ if (this.CONTEXT["logger"]) {
1357
+ this.CONTEXT["logger"].info("Cleanup completed");
1358
+ }
1359
+ }
1360
+
1361
+ /**
1362
+ * Sets up graceful shutdown handlers for process signals and uncaught exceptions.
1363
+ * Automatically registers handlers for SIGTERM, SIGINT, uncaughtException, and unhandledRejection.
1364
+ * When any of these events occur, the cleanup method is called before process termination.
1365
+ *
1366
+ * @returns {void}
1367
+ *
1368
+ * @example
1369
+ * // Called automatically in init(), but can be called manually if needed
1370
+ * app.setupGracefulShutdown();
1371
+ *
1372
+ * // The following signals will trigger graceful shutdown:
1373
+ * // - SIGTERM (termination request)
1374
+ * // - SIGINT (interrupt signal, Ctrl+C)
1375
+ * // - uncaughtException (unhandled errors)
1376
+ * // - unhandledRejection (unhandled promise rejections)
1377
+ */
1378
+ setupGracefulShutdown() {
1379
+ const signals = ['SIGINT', 'SIGTERM'];
1380
+ const logger = this.CONTEXT["logger"] || console;
1381
+
1382
+ signals.forEach((signal) => {
1383
+ process.on(signal, async () => {
1384
+ logger.info(`Received ${signal}. Starting graceful shutdown...`);
1385
+
1386
+ try {
1387
+ if (this.httpServer) {
1388
+ await this.httpServer.stop({ timeout: 10000 });
1389
+ logger.info('HTTP server stopped');
1390
+ this.httpServer = null;
1391
+ }
1392
+
1393
+ if (this.db && mongoose.connection.readyState === 1) {
1394
+ await mongoose.connection.close();
1395
+ logger.info('MongoDB connection closed');
1396
+ this.db = null;
1397
+ }
1398
+
1399
+ if (this.redis_client && this.redis_client.isOpen) {
1400
+ await this.redis_client.quit();
1401
+ logger.info('Redis connection closed');
1402
+ this.redis_client = null;
1403
+ }
1404
+
1405
+ if (this.mqtt_client && this.mqtt_client.connected) {
1406
+ this.mqtt_client.end();
1407
+ logger.info('MQTT client disconnected');
1408
+ this.mqtt_client = null;
1409
+ }
1410
+
1411
+ if (this.mqtt_server) {
1412
+ this.mqtt_server.close();
1413
+ logger.info('MQTT server closed');
1414
+ this.mqtt_server = null;
1415
+ }
1416
+
1417
+ // Kill spawned services
1418
+ if (this.CONTEXT["services"] && this.CONTEXT["services"].length > 0) {
1419
+ this.CONTEXT["services"].forEach(service => {
1420
+ if (service.pid) {
1421
+ process.kill(service.pid, 'SIGTERM');
1422
+ }
1423
+ });
1424
+ logger.info('Services terminated');
1425
+ }
1426
+
1427
+ logger.info('Graceful shutdown complete. Exiting.');
1428
+ process.exit(0);
1429
+ } catch (err) {
1430
+ logger.error('Error during graceful shutdown:', err);
1431
+ process.exit(1);
1432
+ }
1433
+ });
1434
+ });
1435
+
1436
+ process.on('uncaughtException', async (err) => {
1437
+ logger.error('Uncaught Exception:', err);
1438
+ await this.cleanup();
1439
+ process.exit(1);
1440
+ });
1441
+
1442
+ process.on('unhandledRejection', async (reason) => {
1443
+ console.log(reason)
1444
+ logger.error('Unhandled Rejection:', reason);
1445
+ await this.cleanup();
1446
+ process.exit(1);
1447
+ });
1448
+ }
1449
+
1450
+
1451
+ async #generateSwaggerSchemas() {
1452
+ this.CONTEXT["swagger_schema"] = {};
1453
+ if (this.CONTEXT["models"]) {
1454
+ for (const [name, model] of Object.entries(this.CONTEXT["models"])) {
1455
+ try {
1456
+ if (model.schema) {
1457
+ this.CONTEXT["swagger_schema"][name] = mongooseToJoi(model.schema).label(model.modelName || name);
1458
+ }
1459
+ } catch (e) {
1460
+ this.CONTEXT["logger"].error(`Failed to generate swagger schema for ${name}`, e);
1461
+ }
1462
+ }
1463
+ }
1464
+ }
1465
+
1466
+
1467
+
1468
+ async #startCrudRoutes() {
1469
+ if (!this.SETTINGS.crud || Object.keys(this.SETTINGS.crud).length === 0) return;
1470
+
1471
+ this.CONTEXT["logger"].info("Starting Auto-Generated CRUD Routes...");
1472
+
1473
+ const allowedOps = ['create', 'read', 'update', 'delete', 'paginate'];
1474
+
1475
+ for (const [modelName, config] of Object.entries(this.SETTINGS.crud)) {
1476
+ // 1. Validate Model Exists
1477
+ // Models are stored in context with .js extension usually, wait, loaded models keys might vary
1478
+ // Let's check against loaded models. The loader uses filenames as keys usually.
1479
+ // We need to find the matching model key.
1480
+ const modelKey = Object.keys(this.CONTEXT.models).find(k => k.replace(/\.js$/, '') === modelName);
1481
+
1482
+ if (!modelKey) {
1483
+ this.CONTEXT["logger"].warn(`CRUD Gen: Model "${modelName}" not found in loaded models. Skipping.`);
1484
+ continue;
1485
+ }
1486
+
1487
+ const Model = this.CONTEXT.models[modelKey];
1488
+ let ops = [];
1489
+
1490
+ // 2. Parse Config
1491
+ if (config === 'all') {
1492
+ ops = allowedOps;
1493
+ } else if (Array.isArray(config)) {
1494
+ // Validate ops
1495
+ const invalidOps = config.filter(op => !allowedOps.includes(op));
1496
+ if (invalidOps.length > 0) {
1497
+ this.CONTEXT["logger"].warn(`CRUD Gen: Invalid operations [${invalidOps.join(', ')}] config for model "${modelName}". Ignoring ENTIRE model.`);
1498
+ continue;
1499
+ }
1500
+ ops = config;
1501
+ } else {
1502
+ this.CONTEXT["logger"].warn(`CRUD Gen: Invalid config format for model "${modelName}". Expected "all" or array. Skipping.`);
1503
+ continue;
1504
+ }
1505
+
1506
+ this.CONTEXT["logger"].info(`CRUD Gen: Generating [${ops.join(', ')}] for ${modelName}`);
1507
+
1508
+ const basePath = `/crud/${modelName}`;
1509
+
1510
+ // Retrieve Joi Schema from Context
1511
+ const joiSchema = this.CONTEXT["swagger_schema"][modelName] || this.CONTEXT["swagger_schema"][modelKey];
1512
+
1513
+ // 3. Generate Routes
1514
+ if (ops.includes('create')) {
1515
+ this.httpServer.route({
1516
+ method: 'POST',
1517
+ path: basePath,
1518
+ options: {
1519
+ tags: ['api'],
1520
+ description: `Create a new ${modelName}`,
1521
+ notes: `Auto-generated create route for ${modelName}`,
1522
+ validate: {
1523
+ payload: joiSchema
1524
+ }
1525
+ },
1526
+ handler: async (request, h) => {
1527
+ try {
1528
+ const doc = await Model.create(request.payload);
1529
+ return h.response(doc).code(201);
1530
+ } catch (err) {
1531
+ return h.response({ error: err.message }).code(500);
1532
+ }
1533
+ }
1534
+ });
1535
+ }
1536
+
1537
+ if (ops.includes('read')) { // Get By ID
1538
+ this.httpServer.route({
1539
+ method: 'GET',
1540
+ path: `${basePath}/{id}`,
1541
+ options: {
1542
+ tags: ['api'],
1543
+ description: `Get ${modelName} by ID`,
1544
+ notes: `Auto-generated get by id route for ${modelName}`
1545
+ },
1546
+ handler: async (request, h) => {
1547
+ try {
1548
+ const doc = await Model.findById(request.params.id);
1549
+ if (!doc) return h.response({ error: "Not Found" }).code(404);
1550
+ return doc;
1551
+ } catch (err) {
1552
+ return h.response({ error: err.message }).code(500);
1553
+ }
1554
+ }
1555
+ });
1556
+ }
1557
+
1558
+ if (ops.includes('update')) {
1559
+ this.httpServer.route({
1560
+ method: 'PUT',
1561
+ path: `${basePath}/{id}`,
1562
+ options: {
1563
+ tags: ['api'],
1564
+ description: `Update ${modelName} by ID`,
1565
+ notes: `Auto-generated update route for ${modelName}`,
1566
+ validate: {
1567
+ payload: joiSchema
1568
+ }
1569
+ },
1570
+ handler: async (request, h) => {
1571
+ try {
1572
+ const doc = await Model.findByIdAndUpdate(request.params.id, request.payload, { new: true });
1573
+ if (!doc) return h.response({ error: "Not Found" }).code(404);
1574
+ return doc;
1575
+ } catch (err) {
1576
+ return h.response({ error: err.message }).code(500);
1577
+ }
1578
+ }
1579
+ });
1580
+ }
1581
+
1582
+ if (ops.includes('delete')) {
1583
+ this.httpServer.route({
1584
+ method: 'DELETE',
1585
+ path: `${basePath}/{id}`,
1586
+ options: {
1587
+ tags: ['api'],
1588
+ description: `Delete ${modelName} by ID`,
1589
+ notes: `Auto-generated delete route for ${modelName}`
1590
+ },
1591
+ handler: async (request, h) => {
1592
+ try {
1593
+ const doc = await Model.findByIdAndDelete(request.params.id);
1594
+ if (!doc) return h.response({ error: "Not Found" }).code(404);
1595
+ return { message: "Deleted successfully", id: doc._id };
1596
+ } catch (err) {
1597
+ return h.response({ error: err.message }).code(500);
1598
+ }
1599
+ }
1600
+ });
1601
+ }
1602
+
1603
+ if (ops.includes('paginate')) {
1604
+ this.httpServer.route({
1605
+ method: 'GET',
1606
+ path: basePath,
1607
+ options: {
1608
+ tags: ['api'],
1609
+ description: `Paginate ${modelName}`,
1610
+ notes: `Auto-generated pagination route for ${modelName}`
1611
+ },
1612
+ handler: async (request, h) => {
1613
+ try {
1614
+ const page = parseInt(request.query.page) || 1;
1615
+ const limit = parseInt(request.query.limit) || 10;
1616
+ const skip = (page - 1) * limit;
1617
+
1618
+ // Basic filtering support can be added here if needed
1619
+ const docs = await Model.find({}).skip(skip).limit(limit);
1620
+ const total = await Model.countDocuments({});
1621
+
1622
+ return {
1623
+ data: docs,
1624
+ pagination: {
1625
+ page,
1626
+ limit,
1627
+ total,
1628
+ pages: Math.ceil(total / limit)
1629
+ }
1630
+ };
1631
+ } catch (err) {
1632
+ return h.response({ error: err.message }).code(500);
1633
+ }
1634
+ }
1635
+ });
1636
+ }
1637
+ }
1638
+ }
1639
+
1640
+
1641
+ }
1642
+
1643
+ module.exports = Falcon;
1644
+