@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
@@ -0,0 +1,540 @@
1
+ require('dotenv').config({ quiet: true, debug: false });
2
+ const mqtt = require("mqtt");
3
+ const config = process.env;
4
+ const crypto = require('crypto');
5
+ const mongoose = require('mongoose');
6
+ const bcrypt = require('bcrypt');
7
+ const mongoosePaginate = require('mongoose-paginate-v2');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const JWT = require('jsonwebtoken');
11
+ const { v4: uuidv4 } = require('uuid');
12
+ const redis = require("redis");
13
+ const axios = require("axios");
14
+ const { ValidationError, DatabaseError } = require('./core/errors');
15
+
16
+ /**
17
+ * Base class for Falcon workers that process background tasks and jobs.
18
+ * Workers are typically short-lived processes that handle specific tasks like image processing,
19
+ * data transformation, or batch operations. They can be spawned on-demand or run continuously.
20
+ *
21
+ * @class FalconBaseWorker
22
+ * @example
23
+ * class ImageProcessor extends FalconBaseWorker {
24
+ * constructor() {
25
+ * super('image-processor');
26
+ * }
27
+ *
28
+ * async onMessage(topic, msg) {
29
+ * if (topic === 'worker_image-processor_job') {
30
+ * const job = JSON.parse(msg);
31
+ * await this.processImage(job);
32
+ * }
33
+ * }
34
+ *
35
+ * async run() {
36
+ * const args = this.parseArgs();
37
+ * if (args.jobId) {
38
+ * await this.processJob(args.jobId);
39
+ * process.exit(0); // Exit after single job
40
+ * }
41
+ * // Otherwise wait for MQTT jobs
42
+ * }
43
+ * }
44
+ *
45
+ * const worker = new ImageProcessor();
46
+ * worker.init();
47
+ */
48
+ class FalconBaseWorker {
49
+
50
+ /**
51
+ * Creates a new Falcon worker instance.
52
+ * Automatically loads settings, parses command line arguments, and sets up graceful shutdown.
53
+ *
54
+ * @param {string} workerId - Worker identifier used for MQTT topics and logging
55
+ * @throws {Error} Throws if worker ID is not provided
56
+ *
57
+ * @example
58
+ * class MyWorker extends FalconBaseWorker {
59
+ * constructor() {
60
+ * super('my-worker'); // Creates topics: worker_my-worker, worker_my-worker_job, worker_my-worker_ping
61
+ * }
62
+ * }
63
+ */
64
+ constructor(workerId) {
65
+ if (!workerId || typeof workerId !== 'string') {
66
+ throw new ValidationError('workerId must be a non-empty string');
67
+ }
68
+
69
+ this.workerId = workerId;
70
+ this.models = {};
71
+ this.CONTEXT = { models: {} };
72
+ this.db = null;
73
+ this.mqttClient = null;
74
+ this.redisClient = null;
75
+ this.env = process.env;
76
+ this.args = process.argv.slice(2);
77
+
78
+ // Load settings from CWD
79
+ try {
80
+ const settingsPath = path.join(process.cwd(), 'settings.js');
81
+ if (fs.existsSync(settingsPath)) {
82
+ this.SETTINGS = require(settingsPath).settings;
83
+ } else {
84
+ console.warn("Settings file not found in CWD");
85
+ this.SETTINGS = { models: [] };
86
+ }
87
+ } catch (e) {
88
+ console.error("Error loading settings:", e);
89
+ this.SETTINGS = { models: [] };
90
+ }
91
+
92
+ this.setupGracefulShutdown();
93
+ }
94
+
95
+ /**
96
+ * Checks if running in development mode.
97
+ * @private
98
+ * @returns {boolean}
99
+ */
100
+ #isDev() {
101
+ return process.env.MODE === "DEV";
102
+ }
103
+
104
+ /**
105
+ * Parses command line arguments passed to the worker process.
106
+ * Supports both base64 encoded JSON (preferred) and simple key=value pairs.
107
+ * The main Falcon server uses base64 JSON when spawning workers.
108
+ *
109
+ * @returns {Object} Parsed arguments object
110
+ *
111
+ * @example
112
+ * // Base64 JSON (from Falcon server):
113
+ * // node worker.js eyJqb2JJZCI6MTIzLCJ0eXBlIjoiaW1hZ2UifQ==
114
+ * const args = this.parseArgs(); // { jobId: 123, type: "image" }
115
+ *
116
+ * // Key=value pairs (manual execution):
117
+ * // node worker.js jobId=123 type=image
118
+ * const args = this.parseArgs(); // { jobId: "123", type: "image" }
119
+ *
120
+ * // No arguments:
121
+ * const args = this.parseArgs(); // {}
122
+ */
123
+ parseArgs() {
124
+ if (this.args.length === 0) return {};
125
+
126
+ try {
127
+ // Try to parse as base64 encoded JSON first
128
+ const decoded = Buffer.from(this.args[0], 'base64').toString('utf8');
129
+ return JSON.parse(decoded);
130
+ } catch (e) {
131
+ // Fallback to simple key=value parsing
132
+ const parsed = {};
133
+ this.args.forEach(arg => {
134
+ const [key, value] = arg.split('=');
135
+ if (key && value) {
136
+ parsed[key] = value;
137
+ }
138
+ });
139
+ return parsed;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Loads a model by name from various possible locations.
145
+ * Searches in: models/mongo/, models/, workers/models/
146
+ *
147
+ * @private
148
+ * @param {string} name - Model name without .js extension
149
+ * @throws {Error} Logs error if model loading fails but doesn't throw
150
+ *
151
+ * @example
152
+ * // Searches for job.js in:
153
+ * // - /project/models/mongo/job.js
154
+ * // - /project/models/job.js
155
+ * // - /project/workers/models/job.js
156
+ * await this.#load_model_name('job');
157
+ */
158
+ async #load_model_name(name) {
159
+ const cwd = process.cwd();
160
+ const potentialPaths = [
161
+ path.join(cwd, "models", "mongo", name + ".js"),
162
+ path.join(cwd, "models", name + ".js"),
163
+ path.join(cwd, "workers", "models", name + ".js")
164
+ ];
165
+
166
+ let modelPath = null;
167
+ for (const p of potentialPaths) {
168
+ if (fs.existsSync(p)) {
169
+ modelPath = p;
170
+ break;
171
+ }
172
+ }
173
+
174
+ if (modelPath) {
175
+ try {
176
+ const _temp = await require(modelPath)(mongoose);
177
+ this.models[name] = _temp;
178
+ this.CONTEXT["models"][name] = _temp;
179
+ console.info(`${name} model registered from ${modelPath}`);
180
+ } catch (e) {
181
+ console.error(`Failed to load model ${name} from ${modelPath}:`, e);
182
+ }
183
+ } else {
184
+ console.warn(`Model ${name} not found in any standard location.`);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Connects to MongoDB database.
190
+ * @private
191
+ */
192
+ async #startMongoDB() {
193
+ if (!config.MONGODB_URL) {
194
+ console.warn("MONGODB_URL not configured, skipping database connection");
195
+ return;
196
+ }
197
+
198
+ if (this.#isDev()) {
199
+ mongoose.set("debug", true);
200
+ console.debug("Mongoose ODM is set in Debug Mode");
201
+ }
202
+
203
+ try {
204
+ await mongoose.connect(config.MONGODB_URL, {});
205
+ mongoose.set("debug", (collectionName, method, query, doc) => {
206
+ console.info(`MONGOOSE ==> ${collectionName}.${method}`, JSON.stringify(query), doc);
207
+ });
208
+ this.db = mongoose.connection.db;
209
+ this.CONTEXT["db"] = this.db;
210
+ this.CONTEXT["mongoose"] = mongoose;
211
+ console.info("MongoDB connected");
212
+ } catch (error) {
213
+ console.error("MongoDB connection failed:", error);
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Connects to Redis if enabled.
220
+ * @private
221
+ */
222
+ async #startRedis() {
223
+ if (config.REDIS_ENABLE === "true" && config.REDIS_URL) {
224
+ try {
225
+ this.redis_client = await redis.createClient({ url: config.REDIS_URL }).connect();
226
+ this.CONTEXT["redis"] = this.redis_client;
227
+ console.info("Redis connected");
228
+ } catch (error) {
229
+ console.error("Redis connection failed:", error);
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Connects to MQTT broker.
236
+ * @private
237
+ */
238
+ async #startMQTT() {
239
+ if (!config.MQTT_URL) {
240
+ console.warn("MQTT_URL not configured, skipping MQTT connection");
241
+ return;
242
+ }
243
+
244
+ try {
245
+ this.mqtt_client = mqtt.connect(config.MQTT_URL);
246
+ this.mqtt_client.on("connect", this.onConnect.bind(this));
247
+ this.mqtt_client.on("message", this.#onMessage.bind(this));
248
+ this.mqtt_client.on("error", (err) => {
249
+ console.error("MQTT connection error:", err);
250
+ });
251
+ this.CONTEXT["mqtt_client"] = this.mqtt_client;
252
+ } catch (error) {
253
+ console.error("MQTT connection failed:", error);
254
+ throw error;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Loads all configured models.
260
+ * @private
261
+ */
262
+ async #loadModels() {
263
+ if (!this.SETTINGS.models || this.SETTINGS.models.length === 0) {
264
+ console.info("No models configured");
265
+ return;
266
+ }
267
+
268
+ console.info("-----------------Registering Models----------------");
269
+ for (const model of this.SETTINGS.models) {
270
+ await this.#load_model_name(model);
271
+ }
272
+ console.info("-----------------Models Registered----------------");
273
+ }
274
+
275
+ /**
276
+ * Publishes data to websocket clients via MQTT.
277
+ * @param {*} data - Data to broadcast
278
+ */
279
+ publishToWebsocket(data) {
280
+ if (this.mqttClient) {
281
+ this.mqttClient.publish('websocket_broadcast', JSON.stringify(data));
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Emits data to a specific WebSocket room/group via MQTT.
287
+ *
288
+ * @param {string} room - Room name to emit to
289
+ * @param {string} event - Event name
290
+ * @param {*} payload - Data payload
291
+ */
292
+ emitToGroup(room, event, payload) {
293
+ if (this.mqttClient) {
294
+ this.mqttClient.publish('websocket_emit_group', JSON.stringify({
295
+ room,
296
+ event,
297
+ payload
298
+ }));
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Emits data to a specific WebSocket client by socket ID via MQTT.
304
+ *
305
+ * @param {string} socketId - Socket ID to emit to
306
+ * @param {string} event - Event name
307
+ * @param {*} payload - Data payload
308
+ */
309
+ emitToSocket(socketId, event, payload) {
310
+ if (this.mqttClient) {
311
+ this.mqttClient.publish('websocket_emit_socket', JSON.stringify({
312
+ socketId,
313
+ event,
314
+ payload
315
+ }));
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Publishes data to a specific MQTT topic.
321
+ * Supports both string and object data (objects are JSON stringified).
322
+ *
323
+ * @async
324
+ * @param {string} topic - MQTT topic to publish to
325
+ * @param {*} msg - Message to publish (strings sent as-is, objects JSON stringified)
326
+ *
327
+ * @example
328
+ * // Send job completion status
329
+ * await this.publish('worker_image-processor_complete', {
330
+ * jobId: 123,
331
+ * status: 'completed',
332
+ * result: { processedPath: '/images/processed/123.jpg' }
333
+ * });
334
+ *
335
+ * // Send simple string message
336
+ * await this.publish('worker_status', 'ready');
337
+ */
338
+ async publish(topic, msg) {
339
+ if (this.mqtt_client) {
340
+ const message = typeof msg === 'string' ? msg : JSON.stringify(msg);
341
+ await this.mqtt_client.publish(topic, message);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Called when MQTT connection is established.
347
+ * Automatically subscribes to worker-specific topics for jobs and health checks.
348
+ * Override in subclasses to add custom subscriptions or initialization logic.
349
+ *
350
+ * @async
351
+ * @virtual
352
+ *
353
+ * @example
354
+ * async onConnect() {
355
+ * await super.onConnect(); // Call parent to get default subscriptions
356
+ * await this.mqtt_client.subscribe('worker_image-processor_priority');
357
+ * console.log('Image processor worker connected');
358
+ * }
359
+ */
360
+ async onConnect() {
361
+ console.info(`Worker ${this.service_id} connected to MQTT`);
362
+ // Subscribe to worker-specific topics
363
+ await this.mqtt_client.subscribe(`worker_${this.service_id}`);
364
+ await this.mqtt_client.subscribe(`worker_${this.service_id}_ping`);
365
+ await this.mqtt_client.subscribe(`worker_${this.service_id}_job`);
366
+ }
367
+
368
+ /**
369
+ * Internal MQTT message handler.
370
+ * @private
371
+ */
372
+ async #onMessage(topic, msg) {
373
+ // Handle ping/pong for health checks
374
+ if (topic === `worker_${this.service_id}_ping`) {
375
+ await this.publish(`worker_${this.service_id}_pong`, 'pong');
376
+ return;
377
+ }
378
+
379
+ await this.onMessage(topic, msg);
380
+ }
381
+
382
+ /**
383
+ * Override this method to handle incoming MQTT messages and job requests.
384
+ * Called for all subscribed topics except ping messages (handled automatically).
385
+ *
386
+ * @async
387
+ * @virtual
388
+ * @param {string} topic - MQTT topic that received the message
389
+ * @param {Buffer} msg - Raw message buffer (use msg.toString() to get string)
390
+ *
391
+ * @example
392
+ * async onMessage(topic, msg) {
393
+ * const data = JSON.parse(msg.toString());
394
+ *
395
+ * switch (topic) {
396
+ * case 'worker_image-processor_job':
397
+ * await this.processImage(data);
398
+ * break;
399
+ * case 'worker_image-processor_priority':
400
+ * await this.processPriorityJob(data);
401
+ * break;
402
+ * }
403
+ * }
404
+ */
405
+ async onMessage(topic, msg) {
406
+ // Override in subclasses
407
+ }
408
+
409
+ /**
410
+ * Override this method with your worker's main processing logic.
411
+ * Called after all initialization (database, MQTT, models) is complete.
412
+ * Can handle both one-time jobs (exit after completion) and continuous processing.
413
+ *
414
+ * @async
415
+ * @virtual
416
+ *
417
+ * @example
418
+ * // One-time job worker
419
+ * async run() {
420
+ * const args = this.parseArgs();
421
+ * if (args.jobId) {
422
+ * await this.processJob(args.jobId);
423
+ * process.exit(0); // Exit after processing
424
+ * }
425
+ * }
426
+ *
427
+ * // Continuous worker
428
+ * async run() {
429
+ * console.log('Worker ready for jobs...');
430
+ * // Wait for MQTT messages, process jobs as they come
431
+ * }
432
+ */
433
+ async run() {
434
+ // Override in subclasses
435
+ }
436
+
437
+ /**
438
+ * Initializes the worker with all dependencies in the correct order.
439
+ * Sets up MongoDB, Redis, models, MQTT connection, and then calls run().
440
+ * This method should be called to start the worker.
441
+ *
442
+ * @async
443
+ * @throws {Error} Throws if any initialization step fails
444
+ *
445
+ * @example
446
+ * const imageWorker = new ImageProcessor();
447
+ * await imageWorker.init(); // Worker is now running
448
+ */
449
+ async init() {
450
+ console.info(`Initializing worker: ${this.service_id}`);
451
+ await this.#startMongoDB();
452
+ await this.#startRedis();
453
+ await this.#loadModels();
454
+ await this.#startMQTT();
455
+ await this.run();
456
+ }
457
+
458
+ /**
459
+ * Starts the worker (alias for run).
460
+ */
461
+ async start() {
462
+ await this.run();
463
+ }
464
+
465
+ /**
466
+ * Gracefully cleans up all worker resources.
467
+ * Closes MQTT connection, Redis connection, and MongoDB connection in that order.
468
+ * Called automatically during graceful shutdown or can be called manually.
469
+ *
470
+ * @async
471
+ *
472
+ * @example
473
+ * // Manual cleanup before exit
474
+ * await worker.cleanup();
475
+ * process.exit(0);
476
+ *
477
+ * // Automatic cleanup on process signals (already set up in constructor)
478
+ * process.on('SIGTERM', async () => {
479
+ * await worker.cleanup();
480
+ * process.exit(0);
481
+ * });
482
+ */
483
+ async cleanup() {
484
+ console.info(`Cleaning up worker: ${this.service_id}`);
485
+
486
+ if (this.mqtt_client && this.mqtt_client.connected) {
487
+ await this.mqtt_client.end();
488
+ this.mqtt_client = null;
489
+ }
490
+
491
+ if (this.redis_client && this.redis_client.isOpen) {
492
+ await this.redis_client.quit();
493
+ this.redis_client = null;
494
+ }
495
+
496
+ if (this.db && mongoose.connection.readyState === 1) {
497
+ await mongoose.connection.close();
498
+ this.db = null;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Sets up graceful shutdown handlers for process signals and uncaught exceptions.
504
+ * Automatically handles SIGTERM, SIGINT, uncaughtException, and unhandledRejection.
505
+ * Called automatically in constructor - no need to call manually.
506
+ *
507
+ * @private
508
+ *
509
+ * @example
510
+ * // Automatically handles these scenarios:
511
+ * // - Parent process termination (SIGTERM)
512
+ * // - Ctrl+C in terminal (SIGINT)
513
+ * // - Unhandled promise rejections
514
+ * // - Uncaught exceptions
515
+ *
516
+ * // All trigger cleanup() before process exit
517
+ */
518
+ setupGracefulShutdown() {
519
+ const shutdown = async (signal) => {
520
+ console.log(`Worker ${this.service_id} received ${signal}, shutting down...`);
521
+ await this.cleanup();
522
+ process.exit(0);
523
+ };
524
+
525
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
526
+ process.on('SIGINT', () => shutdown('SIGINT'));
527
+ process.on('uncaughtException', async (err) => {
528
+ console.error('Uncaught Exception:', err);
529
+ await this.cleanup();
530
+ process.exit(1);
531
+ });
532
+ process.on('unhandledRejection', async (reason) => {
533
+ console.error('Unhandled Rejection:', reason);
534
+ await this.cleanup();
535
+ process.exit(1);
536
+ });
537
+ }
538
+ }
539
+
540
+ module.exports = FalconBaseWorker;
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports.FalconServer = require("./falcon");
2
+ module.exports.FalconBaseService = require("./falconBaseService");
3
+ module.exports.FalconBaseWorker = require("./falconBaseWorker");
4
+ module.exports.FalconAuthPlugin = require("./FalconAuthPlugin");