@onlineapps/service-common 1.0.1 → 1.0.3

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,9 +1,11 @@
1
1
  {
2
2
  "name": "@onlineapps/service-common",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Common utilities for both infrastructure services and business services",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
+ "prepublishOnly": "../../scripts/pre-publish-compatibility-check.sh",
8
+ "postpublish": "../../scripts/update-manifest-from-npm.sh && ../../scripts/update-all-services.sh",
7
9
  "test": "jest",
8
10
  "test:unit": "jest tests/unit",
9
11
  "test:integration": "jest tests/integration"
@@ -27,4 +29,3 @@
27
29
  "node": ">=14.0.0"
28
30
  }
29
31
  }
30
-
package/src/index.js CHANGED
@@ -2,22 +2,52 @@
2
2
 
3
3
  /**
4
4
  * @onlineapps/service-common
5
- *
5
+ *
6
6
  * Common utilities for both infrastructure services and business services.
7
7
  * Provides shared functionality that is used across the entire system.
8
- *
8
+ *
9
9
  * This library is for ALL services (infrastructure and business).
10
10
  */
11
11
 
12
12
  const { waitForInfrastructureReady } = require('./infrastructure/waitForInfrastructureReady');
13
13
  const { waitForHealthCheckQueueReady } = require('./infrastructure/waitForHealthCheckQueueReady');
14
14
  const { sendMonitoringFailFallbackEmail } = require('./reporting/monitoringFallbackEmail');
15
+ const {
16
+ buildRedisUrl,
17
+ createRedisClient,
18
+ connectRedis
19
+ } = require('./redisClient');
20
+ const {
21
+ requireEnv,
22
+ optionalEnv,
23
+ optionalNumberEnv,
24
+ getCriticalConfig,
25
+ getOptionalInfraConfig,
26
+ getServiceConfig,
27
+ getInfrastructureHealthConfig
28
+ } = require('./runtime-config');
15
29
 
16
30
  module.exports = {
17
31
  // Infrastructure readiness utilities (used by both infrastructure and business services)
18
32
  waitForInfrastructureReady,
19
33
  waitForHealthCheckQueueReady,
34
+
35
+ // Redis utilities (shared between services and tests)
36
+ buildRedisUrl,
37
+ createRedisClient,
38
+ connectRedis,
39
+
40
+ // Configuration helpers (NO FALLBACKS for critical infrastructure)
41
+ requireEnv,
42
+ optionalEnv,
43
+ optionalNumberEnv,
44
+ getCriticalConfig,
45
+ getOptionalInfraConfig,
46
+ getServiceConfig,
47
+ getInfrastructureHealthConfig,
48
+
20
49
  // Reporting utilities
21
50
  sendMonitoringFailFallbackEmail
22
51
  };
23
52
 
53
+
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { connectRedis, buildRedisUrl } = require('../redisClient');
4
+
3
5
  /**
4
6
  * waitForHealthCheckQueueReady.js
5
7
  *
@@ -31,7 +33,9 @@
31
33
  * @throws {Error} - If timeout is reached
32
34
  */
33
35
  async function waitForHealthCheckQueueReady(options = {}) {
34
- const redisUrl = options.redisUrl || process.env.REDIS_URL || 'redis://api_node_cache:6379';
36
+ const redisUrl =
37
+ options.redisUrl ||
38
+ buildRedisUrl({ env: process.env, defaults: { host: 'api_node_cache', port: '6379' } });
35
39
  const maxWait = options.maxWait || parseInt(process.env.INFRASTRUCTURE_HEALTH_QUEUE_WAIT_MAX_TIME) || 60000; // 1 minute
36
40
  const checkInterval = options.checkInterval || parseInt(process.env.INFRASTRUCTURE_HEALTH_QUEUE_WAIT_CHECK_INTERVAL) || 2000; // 2 seconds
37
41
  const logger = options.logger || console;
@@ -55,15 +59,23 @@ async function waitForHealthCheckQueueReady(options = {}) {
55
59
  log(`[HealthCheckQueueReady] Max wait: ${maxWait}ms, Check interval: ${checkInterval}ms`);
56
60
 
57
61
  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}`);
62
+ // Connect to Redis using shared helper (consistent config + fail-fast behaviour)
63
+ redis = await connectRedis({
64
+ purpose: 'wait-for-health-check-queue-ready',
65
+ logger: {
66
+ info: (message, meta = {}) => {
67
+ const suffix = meta && meta.url ? ` (${meta.url})` : '';
68
+ log(`[HealthCheckQueueReady] ${message}${suffix}`);
69
+ },
70
+ error: (message, meta = {}) => {
71
+ const detail = meta && meta.error ? ` ${meta.error}` : '';
72
+ log(`[HealthCheckQueueReady] ${message}${detail}`);
73
+ }
74
+ },
75
+ timeoutMs: 10000,
76
+ env: process.env,
77
+ defaults: { host: 'api_node_cache', port: '6379' }
64
78
  });
65
-
66
- await redis.connect();
67
79
  log('[HealthCheckQueueReady] Connected to Redis');
68
80
 
69
81
  while (Date.now() - startTime < maxWait) {
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { connectRedis, buildRedisUrl } = require('../redisClient');
4
+
3
5
  /**
4
6
  * waitForInfrastructureReady.js
5
7
  *
@@ -42,7 +44,9 @@
42
44
  * Registry config (config.infrastructureHealth) is the source of truth for these defaults.
43
45
  */
44
46
  async function waitForInfrastructureReady(options = {}) {
45
- const redisUrl = options.redisUrl || process.env.REDIS_URL || 'redis://api_node_cache:6379';
47
+ const redisUrl =
48
+ options.redisUrl ||
49
+ buildRedisUrl({ env: process.env, defaults: { host: 'api_node_cache', port: '6379' } });
46
50
  const maxWait = options.maxWait || parseInt(process.env.INFRASTRUCTURE_HEALTH_WAIT_MAX_TIME) || 300000; // 5 minutes
47
51
  const checkInterval = options.checkInterval || parseInt(process.env.INFRASTRUCTURE_HEALTH_WAIT_CHECK_INTERVAL) || 5000; // 5 seconds
48
52
  const logger = options.logger || console;
@@ -72,23 +76,39 @@ async function waitForInfrastructureReady(options = {}) {
72
76
  log(`[InfrastructureReady] Max wait: ${maxWait}ms, Check interval: ${checkInterval}ms`);
73
77
 
74
78
  try {
75
- // Connect to Redis
76
- const { createClient } = require('redis');
77
- redis = createClient({ url: redisUrl });
78
-
79
- redis.on('error', (err) => {
80
- log(`[InfrastructureReady] Redis error: ${err.message}`);
79
+ // Connect to Redis with timeout using shared helper (fail-fast, no infinite reconnect loops)
80
+ redis = await connectRedis({
81
+ purpose: 'wait-for-infrastructure-ready',
82
+ logger: {
83
+ error: (message, meta = {}) => {
84
+ const msg = meta && meta.error ? `${message} ${meta.error}` : message;
85
+ log(`[InfrastructureReady] ${msg}`);
86
+ },
87
+ info: (message, meta = {}) => {
88
+ const suffix = meta && meta.url ? ` (${meta.url})` : '';
89
+ log(`[InfrastructureReady] ${message}${suffix}`);
90
+ }
91
+ },
92
+ timeoutMs: 10000,
93
+ env: process.env,
94
+ defaults: { host: 'api_node_cache', port: '6379' }
81
95
  });
82
-
83
- await redis.connect();
84
96
  log('[InfrastructureReady] Connected to Redis');
85
97
 
86
98
  while (Date.now() - startTime < maxWait) {
87
99
  attemptCount++;
88
100
 
89
101
  try {
90
- // Check Redis key: infrastructure:health:all
91
- const allHealthy = await redis.get('infrastructure:health:all');
102
+ // Check Redis key: infrastructure:health:all with timeout to prevent hanging
103
+ const REDIS_OPERATION_TIMEOUT = 5000; // 5 seconds per operation
104
+ const getPromise = redis.get('infrastructure:health:all');
105
+ const getTimeoutPromise = new Promise((_, reject) => {
106
+ setTimeout(() => {
107
+ reject(new Error(`Redis get() operation timeout after ${REDIS_OPERATION_TIMEOUT}ms`));
108
+ }, REDIS_OPERATION_TIMEOUT);
109
+ });
110
+
111
+ const allHealthy = await Promise.race([getPromise, getTimeoutPromise]);
92
112
 
93
113
  if (allHealthy === 'true') {
94
114
  // All services are UP, we can proceed
@@ -100,9 +120,18 @@ async function waitForInfrastructureReady(options = {}) {
100
120
  const serviceKeys = ['gateway', 'registry', 'validator', 'delivery', 'monitoring'];
101
121
  const statuses = {};
102
122
  for (const serviceName of serviceKeys) {
103
- const status = await redis.get(`infrastructure:health:${serviceName}`);
104
- if (status) {
105
- statuses[serviceName] = JSON.parse(status);
123
+ try {
124
+ const statusPromise = redis.get(`infrastructure:health:${serviceName}`);
125
+ const statusTimeoutPromise = new Promise((_, reject) => {
126
+ setTimeout(() => reject(new Error('timeout')), REDIS_OPERATION_TIMEOUT);
127
+ });
128
+ const status = await Promise.race([statusPromise, statusTimeoutPromise]);
129
+ if (status) {
130
+ statuses[serviceName] = JSON.parse(status);
131
+ }
132
+ } catch (statusError) {
133
+ // Ignore individual service status errors
134
+ log(`[InfrastructureReady] Could not get status for ${serviceName}: ${statusError.message}`);
106
135
  }
107
136
  }
108
137
  if (Object.keys(statuses).length > 0) {
@@ -115,7 +144,13 @@ async function waitForInfrastructureReady(options = {}) {
115
144
  log(`[InfrastructureReady] Attempt ${attemptCount}: Not all services ready (current status: ${allHealthy || 'unknown'}). Waiting ${checkInterval}ms...`);
116
145
 
117
146
  } catch (error) {
118
- log(`[InfrastructureReady] Attempt ${attemptCount}: Failed to get infrastructure health from Redis (${error.message}). Waiting ${checkInterval}ms...`);
147
+ const errorMsg = error.message || String(error);
148
+ log(`[InfrastructureReady] Attempt ${attemptCount}: Failed to get infrastructure health from Redis (${errorMsg}). Waiting ${checkInterval}ms...`);
149
+
150
+ // If Redis connection is lost, throw error immediately instead of retrying
151
+ if (errorMsg.includes('timeout') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND')) {
152
+ throw new Error(`[InfrastructureReady] Redis connection lost: ${errorMsg}. Cannot continue waiting for infrastructure ready.`);
153
+ }
119
154
  }
120
155
 
121
156
  await new Promise(resolve => setTimeout(resolve, checkInterval));
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared Redis client utilities for services and tests.
5
+ *
6
+ * Responsibility:
7
+ * - Build Redis connection URL from environment in a consistent way
8
+ * - Create a configured redis client with unified error logging
9
+ * - (Optionally) connect with a hard timeout for tests / tools
10
+ *
11
+ * This module is intentionally small and framework-agnostic so it can be used
12
+ * both in infrastructure services, business services and in test utilities.
13
+ */
14
+
15
+ const { createClient } = require('redis');
16
+
17
+ /**
18
+ * Build a Redis connection URL from environment.
19
+ *
20
+ * Precedence:
21
+ * 1. REDIS_URL (full URL, e.g. redis://host:port/db)
22
+ * 2. REDIS_HOST + REDIS_PORT
23
+ * 3. Defaults (host/port) or localhost:6379
24
+ */
25
+ function buildRedisUrl(options = {}) {
26
+ const env = options.env || process.env || {};
27
+ const defaults = options.defaults || {};
28
+
29
+ if (env.REDIS_URL && typeof env.REDIS_URL === 'string' && env.REDIS_URL.length > 0) {
30
+ return env.REDIS_URL;
31
+ }
32
+
33
+ const host = env.REDIS_HOST || defaults.host || 'localhost';
34
+ const port = env.REDIS_PORT || defaults.port || '6379';
35
+
36
+ return `redis://${host}:${port}`;
37
+ }
38
+
39
+ /**
40
+ * Create a Redis client with unified error logging and optional test settings.
41
+ *
42
+ * @param {Object} options
43
+ * @param {string} [options.purpose] - Logical purpose (for log context)
44
+ * @param {boolean} [options.forTests=false] - If true, disables automatic reconnect loops
45
+ * @param {Object} [options.logger=console] - Logger with .error/.info methods
46
+ * @param {Object} [options.env=process.env] - Optional env override (for tests)
47
+ * @param {Object} [options.defaults] - Optional default host/port overrides
48
+ * @returns {{ client: import('redis').RedisClientType, url: string }}
49
+ */
50
+ function createRedisClient(options = {}) {
51
+ const {
52
+ purpose = 'default',
53
+ forTests = false,
54
+ logger = console,
55
+ env,
56
+ defaults
57
+ } = options;
58
+
59
+ const url = buildRedisUrl({ env, defaults });
60
+
61
+ const socketOptions = forTests
62
+ ? {
63
+ // In tests we typically want fail-fast behaviour, not infinite reconnect loops
64
+ reconnectStrategy: false
65
+ }
66
+ : {};
67
+
68
+ const client = createClient({
69
+ url,
70
+ socket: socketOptions
71
+ });
72
+
73
+ const log = logger && typeof logger.error === 'function' ? logger : console;
74
+
75
+ client.on('error', (err) => {
76
+ try {
77
+ log.error('[Redis] Error', {
78
+ purpose,
79
+ url,
80
+ message: err && err.message ? err.message : String(err)
81
+ });
82
+ } catch {
83
+ // Last-resort fallback to avoid throwing in error handler
84
+ // eslint-disable-next-line no-console
85
+ console.error('[Redis] Error', err);
86
+ }
87
+ });
88
+
89
+ return { client, url };
90
+ }
91
+
92
+ /**
93
+ * Create and connect a Redis client with a hard timeout (useful for tests/tools).
94
+ *
95
+ * @param {Object} options - Same as createRedisClient plus:
96
+ * @param {number} [options.timeoutMs=15000] - Max time to wait for connect()
97
+ * @returns {Promise<import('redis').RedisClientType>}
98
+ */
99
+ async function connectRedis(options = {}) {
100
+ const { timeoutMs = 15000, logger = console } = options;
101
+ const { client, url } = createRedisClient(options);
102
+
103
+ if (!timeoutMs || timeoutMs <= 0) {
104
+ await client.connect();
105
+ if (logger && typeof logger.info === 'function') {
106
+ logger.info('[Redis] Connected', { url });
107
+ }
108
+ return client;
109
+ }
110
+
111
+ const connectPromise = client.connect();
112
+ const timeoutPromise = new Promise((_, reject) => {
113
+ setTimeout(
114
+ () => reject(new Error(`Redis connection timeout after ${timeoutMs}ms to ${url}`)),
115
+ timeoutMs
116
+ );
117
+ });
118
+
119
+ await Promise.race([connectPromise, timeoutPromise]);
120
+
121
+ if (logger && typeof logger.info === 'function') {
122
+ logger.info('[Redis] Connected', { url });
123
+ }
124
+
125
+ return client;
126
+ }
127
+
128
+ module.exports = {
129
+ buildRedisUrl,
130
+ createRedisClient,
131
+ connectRedis
132
+ };
133
+
134
+
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Runtime Configuration Helper
5
+ *
6
+ * NO FALLBACKS for critical infrastructure connections.
7
+ * Services must fail fast if critical config is missing.
8
+ *
9
+ * This ensures:
10
+ * - Services don't silently connect to wrong infrastructure
11
+ * - Configuration problems are detected immediately
12
+ * - No hidden dependencies on hardcoded values
13
+ */
14
+
15
+ /**
16
+ * Get required environment variable or throw error
17
+ * @param {string} name - Environment variable name
18
+ * @param {string} description - Description for error message
19
+ * @returns {string} Environment variable value
20
+ * @throws {Error} If variable is not set
21
+ */
22
+ function requireEnv(name, description) {
23
+ const value = process.env[name];
24
+ if (!value || value.trim() === '') {
25
+ throw new Error(
26
+ `Service configuration error: ${name} is required but not set. ${description || ''}\n` +
27
+ `Please set ${name} environment variable in .env file or environment.\n` +
28
+ `Example: ${name}=value`
29
+ );
30
+ }
31
+ return value;
32
+ }
33
+
34
+ /**
35
+ * Get optional environment variable with default
36
+ * Use ONLY for non-critical values (ports, timeouts, log levels)
37
+ * @param {string} name - Environment variable name
38
+ * @param {any} defaultValue - Default value if not set
39
+ * @returns {any} Environment variable value or default
40
+ */
41
+ function optionalEnv(name, defaultValue) {
42
+ const value = process.env[name];
43
+ if (value === undefined || value === null || value === '') {
44
+ return defaultValue;
45
+ }
46
+ return value;
47
+ }
48
+
49
+ /**
50
+ * Get optional number from environment with default
51
+ * @param {string} name - Environment variable name
52
+ * @param {number} defaultValue - Default value if not set or invalid
53
+ * @returns {number} Parsed number or default
54
+ */
55
+ function optionalNumberEnv(name, defaultValue) {
56
+ const value = process.env[name];
57
+ if (!value || value.trim() === '') {
58
+ return defaultValue;
59
+ }
60
+ const parsed = parseInt(value, 10);
61
+ return Number.isFinite(parsed) ? parsed : defaultValue;
62
+ }
63
+
64
+ /**
65
+ * Get critical infrastructure configuration
66
+ * These MUST be set - no fallbacks allowed
67
+ * @returns {Object} Critical infrastructure config
68
+ */
69
+ function getCriticalConfig() {
70
+ return {
71
+ REDIS_URL: requireEnv('REDIS_URL', 'Redis connection URL (e.g., redis://api_node_cache:6379)'),
72
+ RABBITMQ_URL: requireEnv('RABBITMQ_URL', 'RabbitMQ connection URL (e.g., amqp://guest:guest@api_services_queuer:5672)')
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Get optional infrastructure configuration
78
+ * These have sensible defaults for development
79
+ * @returns {Object} Optional infrastructure config
80
+ */
81
+ function getOptionalInfraConfig() {
82
+ return {
83
+ POSTGRES_URL: optionalEnv('POSTGRES_URL', null), // Optional - only for services that need it
84
+ RABBITMQ_MGMT_URL: optionalEnv('RABBITMQ_MGMT_URL', null),
85
+ RABBITMQ_DEFAULT_USER: optionalEnv('RABBITMQ_DEFAULT_USER', 'guest'),
86
+ RABBITMQ_DEFAULT_PASS: optionalEnv('RABBITMQ_DEFAULT_PASS', 'guest'),
87
+ RABBITMQ_VHOST: optionalEnv('RABBITMQ_VHOST', '/')
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Get service configuration with defaults
93
+ * @param {Object} options - Options
94
+ * @param {string} [options.defaultPort] - Default port if PORT not set
95
+ * @param {string} [options.defaultLogLevel] - Default log level
96
+ * @returns {Object} Service config
97
+ */
98
+ function getServiceConfig(options = {}) {
99
+ const { defaultPort = 3000, defaultLogLevel = 'info' } = options;
100
+
101
+ return {
102
+ PORT: optionalNumberEnv('PORT', defaultPort),
103
+ LOG_LEVEL: optionalEnv('LOG_LEVEL', defaultLogLevel),
104
+ SERVICE_NAME: optionalEnv('SERVICE_NAME', 'unknown-service'),
105
+ NODE_ENV: optionalEnv('NODE_ENV', 'development')
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Get infrastructure health configuration with defaults
111
+ * @returns {Object} Infrastructure health config
112
+ */
113
+ function getInfrastructureHealthConfig() {
114
+ return {
115
+ queueName: optionalEnv('INFRASTRUCTURE_HEALTH_QUEUE', 'infrastructure.health.checks'),
116
+ publishInterval: optionalNumberEnv('INFRASTRUCTURE_HEALTH_PUBLISH_INTERVAL', 5000),
117
+ waitMaxTime: optionalNumberEnv('INFRASTRUCTURE_HEALTH_WAIT_MAX_TIME', 300000),
118
+ waitCheckInterval: optionalNumberEnv('INFRASTRUCTURE_HEALTH_WAIT_CHECK_INTERVAL', 5000),
119
+ healthCheckTimeout: optionalNumberEnv('INFRASTRUCTURE_HEALTH_TIMEOUT', 15000),
120
+ redisKeyTTL: optionalNumberEnv('INFRASTRUCTURE_HEALTH_REDIS_TTL', 30),
121
+ cleanupInterval: optionalNumberEnv('INFRASTRUCTURE_HEALTH_CLEANUP_INTERVAL', 10000)
122
+ };
123
+ }
124
+
125
+ module.exports = {
126
+ requireEnv,
127
+ optionalEnv,
128
+ optionalNumberEnv,
129
+ getCriticalConfig,
130
+ getOptionalInfraConfig,
131
+ getServiceConfig,
132
+ getInfrastructureHealthConfig
133
+ };
134
+