@onlineapps/service-wrapper 2.1.40 → 2.1.42

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.
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "description": "Runtime defaults for @onlineapps/service-wrapper (non-critical only). Service configs may override.",
4
+ "defaults": {
5
+ "wrapper": {
6
+ "mq": {
7
+ "prefetch": 10,
8
+ "enabled": true
9
+ },
10
+ "registry": {
11
+ "enabled": true,
12
+ "heartbeatInterval": 30000
13
+ },
14
+ "monitoring": {
15
+ "enabled": true,
16
+ "metrics": ["requests", "errors", "duration"]
17
+ },
18
+ "logging": {
19
+ "enabled": true,
20
+ "level": "info",
21
+ "requests": {
22
+ "enabled": true,
23
+ "bodies": true,
24
+ "headers": false
25
+ },
26
+ "responses": {
27
+ "enabled": true,
28
+ "bodies": true
29
+ },
30
+ "excludePaths": ["/health", "/status", "/metrics", "/specification"]
31
+ },
32
+ "health": {
33
+ "enabled": true,
34
+ "endpoint": "/health"
35
+ },
36
+ "validation": {
37
+ "enabled": true
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.1.40",
3
+ "version": "2.1.42",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -22,6 +22,8 @@
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
24
 
25
+ const RUNTIME_DEFAULTS = require('../config/runtime-defaults.json');
26
+
25
27
  /**
26
28
  * CENTRAL REGISTRY: All configuration files used by business services
27
29
  * If you add a new config file, add it here!
@@ -47,7 +49,7 @@ const CONFIG_REGISTRY = {
47
49
  class ConfigLoader {
48
50
  /**
49
51
  * Resolve environment variable substitution
50
- * Supports format: ${VAR_NAME:default_value}
52
+ * Supports format: ${VAR_NAME:default_value} OR ${VAR_NAME}
51
53
  * @param {string} value - Value to resolve
52
54
  * @returns {string} Resolved value
53
55
  */
@@ -68,6 +70,96 @@ class ConfigLoader {
68
70
  return process.env[varName] || defaultValue || '';
69
71
  }
70
72
 
73
+ /**
74
+ * Apply ${VAR} / ${VAR:default} substitutions anywhere within a string.
75
+ * Mirrors ServiceWrapper placeholder support but keeps logic centralized in ConfigLoader.
76
+ *
77
+ * NOTE: This does NOT enforce strict missing-env behavior (that is addressed later).
78
+ *
79
+ * @param {string} value
80
+ * @returns {string}
81
+ */
82
+ static resolveEnvPlaceholdersInString(value) {
83
+ if (typeof value !== 'string') {
84
+ return value;
85
+ }
86
+
87
+ return value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (match, varName, defaultValue) => {
88
+ const envValue = process.env[varName];
89
+
90
+ // Use env value if exists (even if empty string)
91
+ if (envValue !== undefined) {
92
+ return envValue;
93
+ }
94
+
95
+ // Use default if provided
96
+ if (defaultValue !== undefined) {
97
+ return defaultValue;
98
+ }
99
+
100
+ // Fail-fast: placeholder without default requires ENV to be set
101
+ throw new Error(`[ConfigLoader] Missing environment variable - ${varName} is required (config placeholder ${match})`);
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Deeply resolve env placeholders across an object.
107
+ * @param {any} input
108
+ * @param {Object} [options]
109
+ * @param {string} [options.jsonPath] - JSON path prefix for error context
110
+ * @returns {any}
111
+ */
112
+ static resolveEnvPlaceholdersDeep(input, options = {}) {
113
+ const { jsonPath = '$' } = options;
114
+
115
+ if (Array.isArray(input)) {
116
+ return input.map((item, idx) => this.resolveEnvPlaceholdersDeep(item, { jsonPath: `${jsonPath}[${idx}]` }));
117
+ }
118
+
119
+ if (input && typeof input === 'object') {
120
+ const out = {};
121
+ for (const [k, v] of Object.entries(input)) {
122
+ out[k] = this.resolveEnvPlaceholdersDeep(v, { jsonPath: `${jsonPath}.${k}` });
123
+ }
124
+ return out;
125
+ }
126
+
127
+ if (typeof input === 'string') {
128
+ try {
129
+ return this.resolveEnvPlaceholdersInString(input);
130
+ } catch (err) {
131
+ throw new Error(`${err.message} at ${jsonPath}`);
132
+ }
133
+ }
134
+
135
+ return input;
136
+ }
137
+
138
+ /**
139
+ * Deep merge (plain objects only). Arrays are replaced (not merged).
140
+ * @param {Object} base
141
+ * @param {Object} override
142
+ * @returns {Object}
143
+ */
144
+ static deepMerge(base, override) {
145
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
146
+ return override;
147
+ }
148
+ if (!override || typeof override !== 'object' || Array.isArray(override)) {
149
+ return override === undefined ? base : override;
150
+ }
151
+
152
+ const out = { ...base };
153
+ for (const [k, v] of Object.entries(override)) {
154
+ if (v && typeof v === 'object' && !Array.isArray(v) && base[k] && typeof base[k] === 'object' && !Array.isArray(base[k])) {
155
+ out[k] = this.deepMerge(base[k], v);
156
+ } else {
157
+ out[k] = v;
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+
71
163
  /**
72
164
  * Get list of all configuration files
73
165
  * @returns {Object} Configuration registry
@@ -115,7 +207,16 @@ class ConfigLoader {
115
207
  // Always use version from package.json (Single Source of Truth)
116
208
  config.service.version = this.loadPackageVersion(basePath);
117
209
 
118
- return config;
210
+ // Apply repo defaults (non-critical) then allow service config to override
211
+ const defaults = RUNTIME_DEFAULTS?.defaults || {};
212
+ const merged = this.deepMerge(defaults, config);
213
+
214
+ // Resolve env placeholders across the whole config (service + wrapper + custom sections)
215
+ try {
216
+ return this.resolveEnvPlaceholdersDeep(merged, { jsonPath: '$' });
217
+ } catch (err) {
218
+ throw new Error(`[ConfigLoader] Failed to resolve placeholders in ${configPath}. ${err.message}`);
219
+ }
119
220
  } catch (error) {
120
221
  if (error.code === 'ENOENT') {
121
222
  throw new Error(`Service config not found: ${configPath}`);
@@ -195,16 +296,12 @@ class ConfigLoader {
195
296
  const operations = this.loadOperations(basePath);
196
297
  const validationProof = this.loadValidationProof(basePath);
197
298
 
198
- // Resolve environment variables in port and url
199
- const resolvedPort = this.resolveEnvVar(serviceConfig.service.port);
200
- const resolvedUrl = this.resolveEnvVar(serviceConfig.service.url);
201
-
202
299
  return {
203
300
  service: {
204
301
  name: serviceConfig.service.name,
205
302
  version: serviceConfig.service.version,
206
- port: parseInt(resolvedPort, 10),
207
- url: resolvedUrl,
303
+ port: parseInt(serviceConfig.service.port, 10),
304
+ url: serviceConfig.service.url,
208
305
  env: process.env.NODE_ENV || 'development'
209
306
  },
210
307
  wrapper: serviceConfig.wrapper || {},