@onlineapps/service-common 1.0.2 → 1.0.4

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.2",
3
+ "version": "1.0.4",
4
4
  "description": "Common utilities for both infrastructure services and business services",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -17,6 +17,21 @@ const {
17
17
  createRedisClient,
18
18
  connectRedis
19
19
  } = require('./redisClient');
20
+ const {
21
+ buildPostgresUrl,
22
+ createPostgresPool,
23
+ connectPostgres
24
+ } = require('./postgresClient');
25
+ const {
26
+ requireEnv,
27
+ optionalEnv,
28
+ optionalNumberEnv,
29
+ getCriticalConfig,
30
+ getCriticalConfigWithFallbacks,
31
+ getOptionalInfraConfig,
32
+ getServiceConfig,
33
+ getInfrastructureHealthConfig
34
+ } = require('./runtime-config');
20
35
 
21
36
  module.exports = {
22
37
  // Infrastructure readiness utilities (used by both infrastructure and business services)
@@ -28,6 +43,21 @@ module.exports = {
28
43
  createRedisClient,
29
44
  connectRedis,
30
45
 
46
+ // PostgreSQL utilities (shared between services and tests)
47
+ buildPostgresUrl,
48
+ createPostgresPool,
49
+ connectPostgres,
50
+
51
+ // Configuration helpers (NO FALLBACKS for critical infrastructure)
52
+ requireEnv,
53
+ optionalEnv,
54
+ optionalNumberEnv,
55
+ getCriticalConfig,
56
+ getCriticalConfigWithFallbacks, // EXCEPTION: Fallbacks allowed ONLY for infrastructure clients
57
+ getOptionalInfraConfig,
58
+ getServiceConfig,
59
+ getInfrastructureHealthConfig,
60
+
31
61
  // Reporting utilities
32
62
  sendMonitoringFailFallbackEmail
33
63
  };
@@ -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,35 +76,24 @@ 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 with timeout
76
- const { createClient } = require('redis');
77
- redis = createClient({
78
- url: redisUrl,
79
- socket: {
80
- connectTimeout: 5000, // 5 seconds timeout for connection
81
- reconnectStrategy: false // Don't auto-reconnect - fail fast
82
- }
83
- });
84
-
85
- redis.on('error', (err) => {
86
- log(`[InfrastructureReady] Redis error: ${err.message}`);
87
- });
88
-
89
- // Add timeout for Redis connection
90
- const CONNECT_TIMEOUT = 10000; // 10 seconds
91
- const connectPromise = redis.connect();
92
- const connectTimeoutPromise = new Promise((_, reject) => {
93
- setTimeout(() => {
94
- reject(new Error(`Redis connection timeout after ${CONNECT_TIMEOUT}ms. Redis may be unavailable at ${redisUrl}`));
95
- }, CONNECT_TIMEOUT);
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' }
96
95
  });
97
-
98
- try {
99
- await Promise.race([connectPromise, connectTimeoutPromise]);
100
- log('[InfrastructureReady] Connected to Redis');
101
- } catch (connectError) {
102
- throw new Error(`[InfrastructureReady] Failed to connect to Redis: ${connectError.message}`);
103
- }
96
+ log('[InfrastructureReady] Connected to Redis');
104
97
 
105
98
  while (Date.now() - startTime < maxWait) {
106
99
  attemptCount++;
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PostgreSQL Client Utilities
5
+ *
6
+ * Provides helper functions for building PostgreSQL connection URLs
7
+ * with fallback support for infrastructure client configuration.
8
+ *
9
+ * EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
10
+ * This is the ONLY place where fallbacks are acceptable.
11
+ */
12
+
13
+ /**
14
+ * Build a PostgreSQL connection URL from environment.
15
+ *
16
+ * EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
17
+ * This is the ONLY place where fallbacks are acceptable.
18
+ *
19
+ * Environment variable precedence (checked in order):
20
+ * 1. POSTGRES_URL (full URL, e.g. postgres://monitoring:monitoring_pass@api_monitoring_postgres:5432/monitoring) - ENV variable name
21
+ * 2. POSTGRES_HOST + POSTGRES_PORT + POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB (if POSTGRES_URL not set)
22
+ * 3. Fallback: postgres://monitoring:monitoring_pass@api_monitoring_postgres:5432/monitoring (docker-compose default)
23
+ *
24
+ * @param {Object} options - Options
25
+ * @param {Object} [options.env=process.env] - Environment variables
26
+ * @param {Object} [options.defaults] - Default host/port/user/password/db overrides
27
+ * @returns {string} PostgreSQL connection URL
28
+ */
29
+ function buildPostgresUrl(options = {}) {
30
+ const env = options.env || process.env || {};
31
+ const defaults = options.defaults || {};
32
+
33
+ // Priority 1: POSTGRES_URL from environment (ENV variable name: POSTGRES_URL)
34
+ if (env.POSTGRES_URL && typeof env.POSTGRES_URL === 'string' && env.POSTGRES_URL.length > 0) {
35
+ return env.POSTGRES_URL;
36
+ }
37
+
38
+ // Priority 2: POSTGRES_HOST + POSTGRES_PORT + POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB (if POSTGRES_URL not set)
39
+ // Fallback for docker-compose environment (defined in library, not in docker-compose.yml)
40
+ const host = env.POSTGRES_HOST || defaults.host || 'api_monitoring_postgres';
41
+ const port = env.POSTGRES_PORT || defaults.port || '5432';
42
+ const user = env.POSTGRES_USER || defaults.user || 'monitoring';
43
+ const password = env.POSTGRES_PASSWORD || defaults.password || 'monitoring_pass';
44
+ const database = env.POSTGRES_DB || defaults.database || 'monitoring';
45
+
46
+ return `postgres://${user}:${password}@${host}:${port}/${database}`;
47
+ }
48
+
49
+ /**
50
+ * Create a PostgreSQL connection pool
51
+ *
52
+ * @param {Object} options - Options
53
+ * @param {Object} [options.env=process.env] - Environment variables
54
+ * @param {Object} [options.defaults] - Default connection parameters
55
+ * @param {Object} [options.poolConfig] - Additional pool configuration (max, idleTimeoutMillis, etc.)
56
+ * @returns {Object} PostgreSQL Pool instance
57
+ */
58
+ function createPostgresPool(options = {}) {
59
+ const { Pool } = require('pg');
60
+
61
+ const connectionString = buildPostgresUrl({
62
+ env: options.env,
63
+ defaults: options.defaults
64
+ });
65
+
66
+ const poolConfig = {
67
+ connectionString,
68
+ max: options.poolConfig?.max || 10,
69
+ idleTimeoutMillis: options.poolConfig?.idleTimeoutMillis || 30000,
70
+ connectionTimeoutMillis: options.poolConfig?.connectionTimeoutMillis || 2000,
71
+ ...options.poolConfig
72
+ };
73
+
74
+ return new Pool(poolConfig);
75
+ }
76
+
77
+ /**
78
+ * Connect to PostgreSQL and test connection
79
+ *
80
+ * @param {Object} pool - PostgreSQL Pool instance
81
+ * @param {Object} logger - Logger object with info/error methods
82
+ * @returns {Promise<void>}
83
+ */
84
+ async function connectPostgres(pool, logger = console) {
85
+ try {
86
+ await pool.query('SELECT NOW()');
87
+ if (logger && logger.info) {
88
+ logger.info('[PostgreSQL] Connected');
89
+ }
90
+ return true;
91
+ } catch (error) {
92
+ if (logger && logger.error) {
93
+ logger.error('[PostgreSQL] Connection failed:', error);
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ module.exports = {
100
+ buildPostgresUrl,
101
+ createPostgresPool,
102
+ connectPostgres
103
+ };
104
+
@@ -17,20 +17,31 @@ const { createClient } = require('redis');
17
17
  /**
18
18
  * Build a Redis connection URL from environment.
19
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
20
+ * EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
21
+ * This is the ONLY place where fallbacks are acceptable.
22
+ *
23
+ * Environment variable precedence (checked in order):
24
+ * 1. REDIS_URL (full URL, e.g. redis://api_node_cache:6379) - ENV variable name
25
+ * 2. REDIS_HOST + REDIS_PORT (if REDIS_URL not set)
26
+ * 3. Fallback: redis://api_node_cache:6379 (docker-compose default)
27
+ *
28
+ * @param {Object} options - Options
29
+ * @param {Object} [options.env=process.env] - Environment variables
30
+ * @param {Object} [options.defaults] - Default host/port overrides
31
+ * @returns {string} Redis connection URL
24
32
  */
25
33
  function buildRedisUrl(options = {}) {
26
34
  const env = options.env || process.env || {};
27
35
  const defaults = options.defaults || {};
28
36
 
37
+ // Priority 1: REDIS_URL from environment (ENV variable name: REDIS_URL)
29
38
  if (env.REDIS_URL && typeof env.REDIS_URL === 'string' && env.REDIS_URL.length > 0) {
30
39
  return env.REDIS_URL;
31
40
  }
32
41
 
33
- const host = env.REDIS_HOST || defaults.host || 'localhost';
42
+ // Priority 2: REDIS_HOST + REDIS_PORT (if REDIS_URL not set)
43
+ // Fallback for docker-compose environment (defined in library, not in docker-compose.yml)
44
+ const host = env.REDIS_HOST || defaults.host || 'api_node_cache';
34
45
  const port = env.REDIS_PORT || defaults.port || '6379';
35
46
 
36
47
  return `redis://${host}:${port}`;
@@ -0,0 +1,167 @@
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
+ *
67
+ * NOTE: This function is DEPRECATED - use getCriticalConfigWithFallbacks() instead.
68
+ * This function requires ENV variables and fails fast (NO FALLBACKS).
69
+ *
70
+ * @returns {Object} Critical infrastructure config
71
+ * @throws {Error} If REDIS_URL or RABBITMQ_URL are not set
72
+ */
73
+ function getCriticalConfig() {
74
+ return {
75
+ REDIS_URL: requireEnv('REDIS_URL', 'Redis connection URL (e.g., redis://api_node_cache:6379)'),
76
+ RABBITMQ_URL: requireEnv('RABBITMQ_URL', 'RabbitMQ connection URL (e.g., amqp://guest:guest@api_services_queuer:5672)')
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Get critical infrastructure configuration with fallbacks
82
+ *
83
+ * EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
84
+ * This is the ONLY place where fallbacks are acceptable.
85
+ *
86
+ * Environment variables (checked in order):
87
+ * - REDIS_URL: Full Redis URL (e.g., redis://api_node_cache:6379)
88
+ * - RABBITMQ_URL: Full RabbitMQ URL (e.g., amqp://guest:guest@api_services_queuer:5672)
89
+ *
90
+ * Fallbacks (used only if ENV is not set):
91
+ * - Redis: redis://api_node_cache:6379 (docker-compose default)
92
+ * - RabbitMQ: amqp://guest:guest@api_services_queuer:5672 (docker-compose default)
93
+ *
94
+ * @returns {Object} Critical infrastructure config with fallbacks
95
+ */
96
+ function getCriticalConfigWithFallbacks() {
97
+ // Fallback URLs for docker-compose environment (defined in library, not in docker-compose.yml)
98
+ // These are used ONLY if ENV variables are not set
99
+ const redisFallback = 'redis://api_node_cache:6379';
100
+ const rabbitmqFallback = 'amqp://guest:guest@api_services_queuer:5672';
101
+
102
+ return {
103
+ REDIS_URL: optionalEnv('REDIS_URL', redisFallback),
104
+ RABBITMQ_URL: optionalEnv('RABBITMQ_URL', rabbitmqFallback)
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Get optional infrastructure configuration
110
+ * These have sensible defaults for development
111
+ * @returns {Object} Optional infrastructure config
112
+ */
113
+ function getOptionalInfraConfig() {
114
+ return {
115
+ POSTGRES_URL: optionalEnv('POSTGRES_URL', null), // Optional - only for services that need it
116
+ RABBITMQ_MGMT_URL: optionalEnv('RABBITMQ_MGMT_URL', null),
117
+ RABBITMQ_DEFAULT_USER: optionalEnv('RABBITMQ_DEFAULT_USER', 'guest'),
118
+ RABBITMQ_DEFAULT_PASS: optionalEnv('RABBITMQ_DEFAULT_PASS', 'guest'),
119
+ RABBITMQ_VHOST: optionalEnv('RABBITMQ_VHOST', '/')
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Get service configuration with defaults
125
+ * @param {Object} options - Options
126
+ * @param {string} [options.defaultPort] - Default port if PORT not set
127
+ * @param {string} [options.defaultLogLevel] - Default log level
128
+ * @returns {Object} Service config
129
+ */
130
+ function getServiceConfig(options = {}) {
131
+ const { defaultPort = 3000, defaultLogLevel = 'info' } = options;
132
+
133
+ return {
134
+ PORT: optionalNumberEnv('PORT', defaultPort),
135
+ LOG_LEVEL: optionalEnv('LOG_LEVEL', defaultLogLevel),
136
+ SERVICE_NAME: optionalEnv('SERVICE_NAME', 'unknown-service'),
137
+ NODE_ENV: optionalEnv('NODE_ENV', 'development')
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Get infrastructure health configuration with defaults
143
+ * @returns {Object} Infrastructure health config
144
+ */
145
+ function getInfrastructureHealthConfig() {
146
+ return {
147
+ queueName: optionalEnv('INFRASTRUCTURE_HEALTH_QUEUE', 'infrastructure.health.checks'),
148
+ publishInterval: optionalNumberEnv('INFRASTRUCTURE_HEALTH_PUBLISH_INTERVAL', 5000),
149
+ waitMaxTime: optionalNumberEnv('INFRASTRUCTURE_HEALTH_WAIT_MAX_TIME', 300000),
150
+ waitCheckInterval: optionalNumberEnv('INFRASTRUCTURE_HEALTH_WAIT_CHECK_INTERVAL', 5000),
151
+ healthCheckTimeout: optionalNumberEnv('INFRASTRUCTURE_HEALTH_TIMEOUT', 15000),
152
+ redisKeyTTL: optionalNumberEnv('INFRASTRUCTURE_HEALTH_REDIS_TTL', 30),
153
+ cleanupInterval: optionalNumberEnv('INFRASTRUCTURE_HEALTH_CLEANUP_INTERVAL', 10000)
154
+ };
155
+ }
156
+
157
+ module.exports = {
158
+ requireEnv,
159
+ optionalEnv,
160
+ optionalNumberEnv,
161
+ getCriticalConfig,
162
+ getCriticalConfigWithFallbacks,
163
+ getOptionalInfraConfig,
164
+ getServiceConfig,
165
+ getInfrastructureHealthConfig
166
+ };
167
+