@onlineapps/service-common 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-common",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Common utilities for both infrastructure services and business services",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -17,6 +17,7 @@
17
17
  "author": "OA Drive Team",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
+ "nodemailer": "^6.9.8",
20
21
  "redis": "^4.6.0"
21
22
  },
22
23
  "devDependencies": {
package/src/index.js CHANGED
@@ -10,9 +10,14 @@
10
10
  */
11
11
 
12
12
  const { waitForInfrastructureReady } = require('./infrastructure/waitForInfrastructureReady');
13
+ const { waitForHealthCheckQueueReady } = require('./infrastructure/waitForHealthCheckQueueReady');
14
+ const { sendMonitoringFailFallbackEmail } = require('./reporting/monitoringFallbackEmail');
13
15
 
14
16
  module.exports = {
15
17
  // Infrastructure readiness utilities (used by both infrastructure and business services)
16
- waitForInfrastructureReady
18
+ waitForInfrastructureReady,
19
+ waitForHealthCheckQueueReady,
20
+ // Reporting utilities
21
+ sendMonitoringFailFallbackEmail
17
22
  };
18
23
 
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * waitForHealthCheckQueueReady.js
5
+ *
6
+ * Waits for Registry to create the infrastructure.health.checks queue.
7
+ * Used by infrastructure services to know when they can start publishing health checks.
8
+ *
9
+ * This is different from waitForInfrastructureReady - this only checks if the queue exists,
10
+ * not if all services are healthy.
11
+ *
12
+ * **How it works:**
13
+ * 1. Connects to Redis
14
+ * 2. Checks `infrastructure:health:queue:ready` key every 2 seconds
15
+ * 3. If key is `"true"` → queue is ready → return success
16
+ * 4. If key is missing → wait and retry
17
+ * 5. If timeout reached → throw error
18
+ *
19
+ * **Usage:**
20
+ * - Infrastructure services: Wait before starting health check publisher
21
+ */
22
+
23
+ /**
24
+ * Wait for health check queue to be ready
25
+ * @param {Object} options - Options
26
+ * @param {string} [options.redisUrl] - Redis URL (default: REDIS_URL env or redis://api_node_cache:6379)
27
+ * @param {number} [options.maxWait] - Maximum wait time in ms (default: 60000 = 1 minute)
28
+ * @param {number} [options.checkInterval] - Check interval in ms (default: 2000 = 2 seconds)
29
+ * @param {Object} [options.logger] - Logger instance (default: console)
30
+ * @returns {Promise<boolean>} - True if queue is ready
31
+ * @throws {Error} - If timeout is reached
32
+ */
33
+ async function waitForHealthCheckQueueReady(options = {}) {
34
+ const redisUrl = options.redisUrl || process.env.REDIS_URL || 'redis://api_node_cache:6379';
35
+ const maxWait = options.maxWait || parseInt(process.env.INFRASTRUCTURE_HEALTH_QUEUE_WAIT_MAX_TIME) || 60000; // 1 minute
36
+ const checkInterval = options.checkInterval || parseInt(process.env.INFRASTRUCTURE_HEALTH_QUEUE_WAIT_CHECK_INTERVAL) || 2000; // 2 seconds
37
+ const logger = options.logger || console;
38
+
39
+ const log = (msg) => {
40
+ if (logger && typeof logger.info === 'function') {
41
+ logger.info(msg);
42
+ } else if (logger && typeof logger.log === 'function') {
43
+ logger.log(msg);
44
+ } else {
45
+ console.log(msg);
46
+ }
47
+ };
48
+
49
+ let redis = null;
50
+ const startTime = Date.now();
51
+ let attemptCount = 0;
52
+
53
+ log(`[HealthCheckQueueReady] Waiting for health check queue to be ready...`);
54
+ log(`[HealthCheckQueueReady] Redis URL: ${redisUrl}`);
55
+ log(`[HealthCheckQueueReady] Max wait: ${maxWait}ms, Check interval: ${checkInterval}ms`);
56
+
57
+ try {
58
+ // Connect to Redis
59
+ const { createClient } = require('redis');
60
+ redis = createClient({ url: redisUrl });
61
+
62
+ redis.on('error', (err) => {
63
+ log(`[HealthCheckQueueReady] Redis error: ${err.message}`);
64
+ });
65
+
66
+ await redis.connect();
67
+ log('[HealthCheckQueueReady] Connected to Redis');
68
+
69
+ while (Date.now() - startTime < maxWait) {
70
+ attemptCount++;
71
+
72
+ try {
73
+ // Check Redis key: infrastructure:health:queue:ready
74
+ const queueReady = await redis.get('infrastructure:health:queue:ready');
75
+
76
+ if (queueReady === 'true') {
77
+ // Queue is ready, we can proceed
78
+ const elapsed = Date.now() - startTime;
79
+ log(`[HealthCheckQueueReady] ✓ Health check queue is ready (took ${elapsed}ms, ${attemptCount} attempts)`);
80
+ return true;
81
+ }
82
+
83
+ log(`[HealthCheckQueueReady] Attempt ${attemptCount}: Queue not ready yet (current status: ${queueReady || 'unknown'}). Waiting ${checkInterval}ms...`);
84
+
85
+ } catch (error) {
86
+ log(`[HealthCheckQueueReady] Attempt ${attemptCount}: Failed to get queue ready status from Redis (${error.message}). Waiting ${checkInterval}ms...`);
87
+ }
88
+
89
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
90
+ }
91
+
92
+ const elapsed = Date.now() - startTime;
93
+ throw new Error(
94
+ `Health check queue not ready within ${maxWait}ms (${elapsed}ms elapsed, ${attemptCount} attempts). ` +
95
+ `Check Redis key 'infrastructure:health:queue:ready' and ensure Registry has started.`
96
+ );
97
+ } finally {
98
+ if (redis && redis.isReady) {
99
+ await redis.quit();
100
+ log('[HealthCheckQueueReady] Disconnected from Redis.');
101
+ }
102
+ }
103
+ }
104
+
105
+ module.exports = { waitForHealthCheckQueueReady };
106
+
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const nodemailer = require('nodemailer');
4
+
5
+ let transporter = null;
6
+ let cachedConfigSignature = null;
7
+
8
+ function loadConfig() {
9
+ return {
10
+ host: process.env.INFRA_REPORT_SMTP_HOST,
11
+ port: process.env.INFRA_REPORT_SMTP_PORT ? parseInt(process.env.INFRA_REPORT_SMTP_PORT, 10) : undefined,
12
+ secure: process.env.INFRA_REPORT_SMTP_SECURE === 'true',
13
+ user: process.env.INFRA_REPORT_SMTP_USER,
14
+ pass: process.env.INFRA_REPORT_SMTP_PASS,
15
+ from: process.env.INFRA_REPORT_FROM,
16
+ to: process.env.INFRA_REPORT_TO
17
+ };
18
+ }
19
+
20
+ function createTransportIfNeeded(config) {
21
+ const signature = JSON.stringify(config);
22
+ if (transporter && cachedConfigSignature === signature) {
23
+ return transporter;
24
+ }
25
+
26
+ if (!config.host || !config.port || !config.user || !config.pass || !config.from || !config.to) {
27
+ return null;
28
+ }
29
+
30
+ transporter = nodemailer.createTransport({
31
+ host: config.host,
32
+ port: config.port,
33
+ secure: config.secure,
34
+ auth: {
35
+ user: config.user,
36
+ pass: config.pass
37
+ }
38
+ });
39
+ cachedConfigSignature = signature;
40
+ return transporter;
41
+ }
42
+
43
+ async function sendMonitoringFailFallbackEmail(subject, text, html) {
44
+ const config = loadConfig();
45
+ const mailer = createTransportIfNeeded(config);
46
+
47
+ if (!mailer) {
48
+ console.warn('[MonitoringFallbackEmail] SMTP configuration missing, skipping email send');
49
+ return false;
50
+ }
51
+
52
+ const recipients = config.to.split(',').map(addr => addr.trim()).filter(Boolean);
53
+ if (recipients.length === 0) {
54
+ console.warn('[MonitoringFallbackEmail] No recipients configured in INFRA_REPORT_TO');
55
+ return false;
56
+ }
57
+
58
+ try {
59
+ const result = await mailer.sendMail({
60
+ from: config.from,
61
+ to: recipients,
62
+ subject,
63
+ text,
64
+ html
65
+ });
66
+ console.log('[MonitoringFallbackEmail] Email sent successfully', {
67
+ messageId: result.messageId,
68
+ response: result.response,
69
+ accepted: result.accepted,
70
+ rejected: result.rejected
71
+ });
72
+ return true;
73
+ } catch (error) {
74
+ console.error('[MonitoringFallbackEmail] Failed to send email:', {
75
+ message: error.message,
76
+ code: error.code,
77
+ command: error.command,
78
+ response: error.response,
79
+ responseCode: error.responseCode
80
+ });
81
+ return false;
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ sendMonitoringFailFallbackEmail
87
+ };
88
+
89
+
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ jest.mock('nodemailer', () => {
4
+ return {
5
+ createTransport: jest.fn(() => ({
6
+ sendMail: jest.fn().mockResolvedValue(true)
7
+ }))
8
+ };
9
+ });
10
+
11
+ describe('monitoringFallbackEmail', () => {
12
+ const ORIGINAL_ENV = process.env;
13
+
14
+ beforeEach(() => {
15
+ jest.resetModules();
16
+ process.env = { ...ORIGINAL_ENV };
17
+ });
18
+
19
+ afterEach(() => {
20
+ process.env = ORIGINAL_ENV;
21
+ });
22
+
23
+ function loadReporter() {
24
+ return require('../../src/reporting/monitoringFallbackEmail');
25
+ }
26
+
27
+ test('returns false when SMTP config missing', async () => {
28
+ delete process.env.INFRA_REPORT_SMTP_HOST;
29
+ const { sendMonitoringFailFallbackEmail } = loadReporter();
30
+ const result = await sendMonitoringFailFallbackEmail('t', 't', '<p>t</p>');
31
+ expect(result).toBe(false);
32
+ });
33
+
34
+ test('sends email when SMTP config provided', async () => {
35
+ process.env.INFRA_REPORT_SMTP_HOST = 'smtp.example.com';
36
+ process.env.INFRA_REPORT_SMTP_PORT = '587';
37
+ process.env.INFRA_REPORT_SMTP_SECURE = 'false';
38
+ process.env.INFRA_REPORT_SMTP_USER = 'user@example.com';
39
+ process.env.INFRA_REPORT_SMTP_PASS = 'secret';
40
+ process.env.INFRA_REPORT_FROM = 'infra@example.com';
41
+ process.env.INFRA_REPORT_TO = 'ops@example.com';
42
+
43
+ const nodemailer = require('nodemailer');
44
+ const transportMock = {
45
+ sendMail: jest.fn().mockResolvedValue(true)
46
+ };
47
+ nodemailer.createTransport.mockReturnValue(transportMock);
48
+
49
+ const { sendMonitoringFailFallbackEmail } = loadReporter();
50
+ const result = await sendMonitoringFailFallbackEmail('Subject', 'Body', '<p>Body</p>');
51
+
52
+ expect(result).toBe(true);
53
+ expect(transportMock.sendMail).toHaveBeenCalledTimes(1);
54
+ });
55
+ });
56
+
57
+