@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,17 @@
1
+ class falconAuthPlugin {
2
+ constructor() {
3
+ this.name = "falconAuthPlugin";
4
+ }
5
+
6
+ init() {
7
+ // Initialize the plugin
8
+ }
9
+
10
+ authenticate(username, password) {
11
+ // Authenticate the user
12
+ }
13
+
14
+ authorize(user, resource, action) {
15
+ // Authorize the user for the given resource and action
16
+ }
17
+ }
@@ -0,0 +1,532 @@
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 services that run as separate processes.
18
+ * Provides MQTT communication, database access, and model loading.
19
+ * Services are long-running processes that handle specific business logic.
20
+ *
21
+ * @class FalconBaseService
22
+ * @example
23
+ * class EmailService extends FalconBaseService {
24
+ * constructor() {
25
+ * super('email');
26
+ * }
27
+ *
28
+ * async onMessage(topic, msg) {
29
+ * if (topic === 'service_email') {
30
+ * await this.sendEmail(JSON.parse(msg));
31
+ * }
32
+ * }
33
+ *
34
+ * async run() {
35
+ * console.log('Email service running...');
36
+ * }
37
+ * }
38
+ *
39
+ * const service = new EmailService();
40
+ * service.init();
41
+ */
42
+ class FalconBaseService {
43
+
44
+ /**
45
+ * Creates a new Falcon service instance.
46
+ * Automatically loads settings, sets up graceful shutdown, and initializes context.
47
+ *
48
+ * @param {string} serviceId - Service identifier used for MQTT topics and logging
49
+ * @throws {Error} Throws if service ID is not provided
50
+ *
51
+ * @example
52
+ * class MyService extends FalconBaseService {
53
+ * constructor() {
54
+ * super('my-service'); // Creates topics: service_my-service, service_my-service_ping
55
+ * }
56
+ * }
57
+ */
58
+ constructor(serviceId) {
59
+ if (!serviceId || typeof serviceId !== 'string') {
60
+ throw new ValidationError('serviceId must be a non-empty string');
61
+ }
62
+
63
+ this.serviceId = serviceId;
64
+ this.models = {};
65
+ this.CONTEXT = { models: {} };
66
+ this.db = null;
67
+ this.mqttClient = null;
68
+ this.redisClient = null;
69
+
70
+ // Load settings from CWD
71
+ try {
72
+ const settingsPath = path.join(process.cwd(), 'settings.js');
73
+ if (fs.existsSync(settingsPath)) {
74
+ this.SETTINGS = require(settingsPath).settings;
75
+ } else {
76
+ console.warn("Settings file not found in CWD");
77
+ this.SETTINGS = { models: [] };
78
+ }
79
+ } catch (e) {
80
+ console.error("Error loading settings:", e);
81
+ this.SETTINGS = { models: [] };
82
+ }
83
+
84
+ this.setupGracefulShutdown();
85
+ }
86
+
87
+ /**
88
+ * Checks if running in development mode.
89
+ * @private
90
+ * @returns {boolean}
91
+ */
92
+ #isDev() {
93
+ return process.env.MODE === "DEV";
94
+ }
95
+
96
+ /**
97
+ * Loads a model by name from various possible locations.
98
+ * Searches in: models/mongo/, models/, services/models/
99
+ *
100
+ * @private
101
+ * @param {string} name - Model name without .js extension
102
+ * @throws {Error} Logs error if model loading fails but doesn't throw
103
+ *
104
+ * @example
105
+ * // Searches for user.js in:
106
+ * // - /project/models/mongo/user.js
107
+ * // - /project/models/user.js
108
+ * // - /project/services/models/user.js
109
+ * await this.#load_model_name('user');
110
+ */
111
+ async #load_model_name(name) {
112
+ const cwd = process.cwd();
113
+ const potentialPaths = [
114
+ path.join(cwd, "models", "mongo", name + ".js"),
115
+ path.join(cwd, "models", name + ".js"),
116
+ path.join(cwd, "services", "models", name + ".js")
117
+ ];
118
+
119
+ let modelPath = null;
120
+ for (const p of potentialPaths) {
121
+ if (fs.existsSync(p)) {
122
+ modelPath = p;
123
+ break;
124
+ }
125
+ }
126
+
127
+ if (modelPath) {
128
+ try {
129
+ const _temp = await require(modelPath)(mongoose);
130
+ this.models[name] = _temp;
131
+ this.CONTEXT["models"][name] = _temp;
132
+ console.info(`${name} model registered from ${modelPath}`);
133
+ } catch (e) {
134
+ console.error(`Failed to load model ${name} from ${modelPath}:`, e);
135
+ }
136
+ } else {
137
+ console.warn(`Model ${name} not found in any standard location.`);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Connects to MongoDB database using MONGODB_URL environment variable.
143
+ * Sets up Mongoose with debug logging in development mode.
144
+ *
145
+ * @private
146
+ * @async
147
+ * @throws {Error} Throws if MongoDB connection fails
148
+ *
149
+ * @example
150
+ * // Requires MONGODB_URL environment variable
151
+ * // mongodb://localhost:27017/mydb
152
+ * await this.#startMongoDB();
153
+ */
154
+ async #startMongoDB() {
155
+ if (!config.MONGODB_URL) {
156
+ console.warn("MONGODB_URL not configured, skipping database connection");
157
+ return;
158
+ }
159
+
160
+ if (this.#isDev()) {
161
+ mongoose.set("debug", true);
162
+ console.debug("Mongoose ODM is set in Debug Mode");
163
+ }
164
+
165
+ try {
166
+ await mongoose.connect(config.MONGODB_URL, {});
167
+ mongoose.set("debug", (collectionName, method, query, doc) => {
168
+ console.info(`MONGOOSE ==> ${collectionName}.${method}`, JSON.stringify(query), doc);
169
+ });
170
+ this.db = mongoose.connection.db;
171
+ this.CONTEXT["db"] = this.db;
172
+ this.CONTEXT["mongoose"] = mongoose;
173
+ console.info("MongoDB connected");
174
+ } catch (error) {
175
+ console.error("MongoDB connection failed:", error);
176
+ throw error;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Connects to Redis if enabled via REDIS_ENABLE=true and REDIS_URL environment variables.
182
+ * Redis connection is optional and service will continue without it if not configured.
183
+ *
184
+ * @private
185
+ * @async
186
+ *
187
+ * @example
188
+ * // Requires environment variables:
189
+ * // REDIS_ENABLE=true
190
+ * // REDIS_URL=redis://localhost:6379
191
+ * await this.#startRedis();
192
+ */
193
+ async #startRedis() {
194
+ if (config.REDIS_ENABLE === "true" && config.REDIS_URL) {
195
+ try {
196
+ this.redis_client = await redis.createClient({ url: config.REDIS_URL }).connect();
197
+ this.CONTEXT["redis"] = this.redis_client;
198
+ console.info("Redis connected");
199
+ } catch (error) {
200
+ console.error("Redis connection failed:", error);
201
+ }
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Connects to MQTT broker using MQTT_URL environment variable.
207
+ * Sets up event handlers for connect, message, and error events.
208
+ *
209
+ * @private
210
+ * @async
211
+ * @throws {Error} Throws if MQTT connection fails
212
+ *
213
+ * @example
214
+ * // Requires MQTT_URL environment variable
215
+ * // mqtt://localhost:1883
216
+ * await this.#startMQTT();
217
+ */
218
+ async #startMQTT() {
219
+ if (!config.MQTT_URL) {
220
+ console.warn("MQTT_URL not configured, skipping MQTT connection");
221
+ return;
222
+ }
223
+
224
+ try {
225
+ this.mqtt_client = mqtt.connect(config.MQTT_URL);
226
+ this.mqtt_client.on("connect", this.onConnect.bind(this));
227
+ this.mqtt_client.on("message", this.#onMessage.bind(this));
228
+ this.mqtt_client.on("error", (err) => {
229
+ console.error("MQTT connection error:", err);
230
+ });
231
+ this.CONTEXT["mqtt_client"] = this.mqtt_client;
232
+ } catch (error) {
233
+ console.error("MQTT connection failed:", error);
234
+ throw error;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Loads all models specified in settings.models array.
240
+ * Models are loaded from various locations and registered in this.models and this.CONTEXT.models.
241
+ *
242
+ * @private
243
+ * @async
244
+ *
245
+ * @example
246
+ * // settings.js: { models: ['user', 'product', 'order'] }
247
+ * await this.#loadModels();
248
+ * // Now available as: this.models.user, this.models.product, etc.
249
+ */
250
+ async #loadModels() {
251
+ if (!this.SETTINGS.models || this.SETTINGS.models.length === 0) {
252
+ console.info("No models configured");
253
+ return;
254
+ }
255
+
256
+ console.info("-----------------Registering Models----------------");
257
+ for (const model of this.SETTINGS.models) {
258
+ await this.#load_model_name(model);
259
+ }
260
+ console.info("-----------------Models Registered----------------");
261
+ }
262
+
263
+ /**
264
+ * Publishes data to websocket clients via the 'websocket_broadcast' MQTT topic.
265
+ * The main Falcon server listens to this topic and forwards to connected websocket clients.
266
+ *
267
+ * @param {*} data - Data to broadcast (will be JSON stringified)
268
+ *
269
+ * @example
270
+ * this.publishToWebsocket({
271
+ * type: 'notification',
272
+ * message: 'Service started',
273
+ * timestamp: new Date()
274
+ * });
275
+ */
276
+ publishToWebsocket(data) {
277
+ if (this.mqttClient) {
278
+ this.mqttClient.publish('websocket_broadcast', JSON.stringify(data));
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Emits data to a specific WebSocket room/group via MQTT.
284
+ *
285
+ * @param {string} room - Room name to emit to
286
+ * @param {string} event - Event name
287
+ * @param {*} payload - Data payload
288
+ *
289
+ * @example
290
+ * this.emitToGroup('user_123', 'notification', { message: 'Hello!' });
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
+ * @example
310
+ * this.emitToSocket('socket_abc123', 'private_message', { text: 'Hello!' });
311
+ */
312
+ emitToSocket(socketId, event, payload) {
313
+ if (this.mqttClient) {
314
+ this.mqttClient.publish('websocket_emit_socket', JSON.stringify({
315
+ socketId,
316
+ event,
317
+ payload
318
+ }));
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Publishes data to a specific MQTT topic.
324
+ * Data is automatically JSON stringified before publishing.
325
+ *
326
+ * @param {*} data - Data to publish (objects will be JSON stringified)
327
+ * @param {string} topic - MQTT topic to publish to
328
+ *
329
+ * @example
330
+ * // Send status update
331
+ * this.publish({ status: 'completed', jobId: 123 }, 'job_status');
332
+ *
333
+ * // Send to another service
334
+ * this.publish({ action: 'process' }, 'service_image-processor');
335
+ */
336
+ publish(data, topic) {
337
+ if (this.mqtt_client) {
338
+ this.mqtt_client.publish(topic, JSON.stringify(data));
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Called when MQTT connection is established.
344
+ * Automatically subscribes to service-specific topics for communication and health checks.
345
+ * Override in subclasses to add custom subscriptions or initialization logic.
346
+ *
347
+ * @async
348
+ * @virtual
349
+ *
350
+ * @example
351
+ * async onConnect() {
352
+ * await super.onConnect(); // Call parent to get default subscriptions
353
+ * await this.mqtt_client.subscribe('custom_topic');
354
+ * console.log('Custom service connected');
355
+ * }
356
+ */
357
+ async onConnect() {
358
+ console.info(`Service ${this.service_id} connected to MQTT`);
359
+ // Subscribe to service-specific topic
360
+ await this.mqtt_client.subscribe(`service_${this.service_id}`);
361
+ await this.mqtt_client.subscribe(`service_${this.service_id}_ping`);
362
+ }
363
+
364
+ /**
365
+ * Internal MQTT message handler.
366
+ * @private
367
+ */
368
+ async #onMessage(topic, msg) {
369
+ // Handle ping/pong for health checks
370
+ if (topic === `service_${this.service_id}_ping`) {
371
+ this.publish('pong', `service_${this.service_id}_pong`);
372
+ return;
373
+ }
374
+
375
+ await this.onMessage(topic, msg);
376
+ }
377
+
378
+ /**
379
+ * Override this method to handle incoming MQTT messages.
380
+ * Called for all subscribed topics except ping messages (handled automatically).
381
+ *
382
+ * @async
383
+ * @virtual
384
+ * @param {string} topic - MQTT topic that received the message
385
+ * @param {Buffer} msg - Raw message buffer (use msg.toString() to get string)
386
+ *
387
+ * @example
388
+ * async onMessage(topic, msg) {
389
+ * const data = JSON.parse(msg.toString());
390
+ *
391
+ * switch (topic) {
392
+ * case 'service_email':
393
+ * await this.sendEmail(data);
394
+ * break;
395
+ * case 'service_email_bulk':
396
+ * await this.sendBulkEmail(data);
397
+ * break;
398
+ * }
399
+ * }
400
+ */
401
+ async onMessage(topic, msg) {
402
+ // Override in subclasses
403
+ }
404
+
405
+ /**
406
+ * Override this method with your service's main business logic.
407
+ * Called after all initialization (database, MQTT, models) is complete.
408
+ * This method should contain the core functionality of your service.
409
+ *
410
+ * @async
411
+ * @virtual
412
+ *
413
+ * @example
414
+ * async run() {
415
+ * console.log('Email service starting...');
416
+ *
417
+ * // Set up periodic tasks
418
+ * setInterval(() => {
419
+ * this.checkPendingEmails();
420
+ * }, 30000);
421
+ *
422
+ * // Service is now ready to handle requests
423
+ * this.publish({ status: 'ready' }, 'service_status');
424
+ * }
425
+ */
426
+ async run() {
427
+ // Override in subclasses
428
+ }
429
+
430
+ /**
431
+ * Initializes the service with all dependencies in the correct order.
432
+ * Sets up MongoDB, Redis, models, MQTT connection, and then calls run().
433
+ * This method should be called to start the service.
434
+ *
435
+ * @async
436
+ * @throws {Error} Throws if any initialization step fails
437
+ *
438
+ * @example
439
+ * const emailService = new EmailService();
440
+ * await emailService.init(); // Service is now running
441
+ */
442
+ async init() {
443
+ console.info(`Initializing service: ${this.service_id}`);
444
+ await this.#startMongoDB();
445
+ await this.#startRedis();
446
+ await this.#loadModels();
447
+ await this.#startMQTT();
448
+ await this.run();
449
+ }
450
+
451
+ /**
452
+ * Starts the service (alias for run).
453
+ */
454
+ async start() {
455
+ await this.run();
456
+ }
457
+
458
+ /**
459
+ * Gracefully cleans up all service resources.
460
+ * Closes MQTT connection, Redis connection, and MongoDB connection in that order.
461
+ * Called automatically during graceful shutdown or can be called manually.
462
+ *
463
+ * @async
464
+ *
465
+ * @example
466
+ * // Manual cleanup
467
+ * await service.cleanup();
468
+ *
469
+ * // Automatic cleanup on process signals (already set up in constructor)
470
+ * process.on('SIGTERM', async () => {
471
+ * await service.cleanup();
472
+ * process.exit(0);
473
+ * });
474
+ */
475
+ async cleanup() {
476
+ console.info(`Cleaning up service: ${this.service_id}`);
477
+
478
+ if (this.mqtt_client && this.mqtt_client.connected) {
479
+ await this.mqtt_client.end();
480
+ this.mqtt_client = null;
481
+ }
482
+
483
+ if (this.redis_client && this.redis_client.isOpen) {
484
+ await this.redis_client.quit();
485
+ this.redis_client = null;
486
+ }
487
+
488
+ if (this.db && mongoose.connection.readyState === 1) {
489
+ await mongoose.connection.close();
490
+ this.db = null;
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Sets up graceful shutdown handlers for process signals and uncaught exceptions.
496
+ * Automatically handles SIGTERM, SIGINT, uncaughtException, and unhandledRejection.
497
+ * Called automatically in constructor - no need to call manually.
498
+ *
499
+ * @private
500
+ *
501
+ * @example
502
+ * // Automatically handles these scenarios:
503
+ * // - Docker container stop (SIGTERM)
504
+ * // - Ctrl+C in terminal (SIGINT)
505
+ * // - Unhandled promise rejections
506
+ * // - Uncaught exceptions
507
+ *
508
+ * // All trigger cleanup() before process exit
509
+ */
510
+ setupGracefulShutdown() {
511
+ const shutdown = async (signal) => {
512
+ console.log(`Service ${this.service_id} received ${signal}, shutting down...`);
513
+ await this.cleanup();
514
+ process.exit(0);
515
+ };
516
+
517
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
518
+ process.on('SIGINT', () => shutdown('SIGINT'));
519
+ process.on('uncaughtException', async (err) => {
520
+ console.error('Uncaught Exception:', err);
521
+ await this.cleanup();
522
+ process.exit(1);
523
+ });
524
+ process.on('unhandledRejection', async (reason) => {
525
+ console.error('Unhandled Rejection:', reason);
526
+ await this.cleanup();
527
+ process.exit(1);
528
+ });
529
+ }
530
+ }
531
+
532
+ module.exports = FalconBaseService;