@devopsplaybook.io/common-utils 1.2.0-beta.7.84f3f67 → 1.2.0-beta.7.fd32a5a

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.
@@ -7,6 +7,19 @@ export interface ConfigFieldDef {
7
7
  field: string;
8
8
  /** When `true` the value is masked in log output. */
9
9
  sensitive?: boolean;
10
+ /**
11
+ * Alternative environment variable names to check when the primary field
12
+ * name is not found in `process.env`. Aliases are tried in order and the
13
+ * first match wins. Only checked in the environment layer, never in the
14
+ * config-file layer.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Look for DATABASE_POSTGRES_HOST first, then fall back to POSTGRES_HOST
19
+ * { field: "DATABASE_POSTGRES_HOST", envAliases: ["POSTGRES_HOST"] }
20
+ * ```
21
+ */
22
+ envAliases?: string[];
10
23
  }
11
24
  /**
12
25
  * Database-specific configuration fields shared by every project that
@@ -40,6 +40,48 @@ exports.ConfigBase = void 0;
40
40
  const fse = __importStar(require("fs-extra"));
41
41
  const uuid_1 = require("uuid");
42
42
  const path_1 = __importDefault(require("path"));
43
+ /**
44
+ * Coerce a string value read from an environment variable to match the
45
+ * type of the default value (number → parseFloat, boolean → "true"/"1",
46
+ * array → JSON.parse, etc.). When the default is already a string or
47
+ * there is no default, the original string is returned as-is.
48
+ */
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ function coerceValue(value, defaultValue) {
51
+ if (defaultValue === undefined || defaultValue === null) {
52
+ return value;
53
+ }
54
+ switch (typeof defaultValue) {
55
+ case "number": {
56
+ const parsed = Number(value);
57
+ return Number.isNaN(parsed) ? value : parsed;
58
+ }
59
+ case "boolean": {
60
+ return value === "true" || value === "1" || value === "yes";
61
+ }
62
+ case "object": {
63
+ if (Array.isArray(defaultValue)) {
64
+ try {
65
+ return JSON.parse(value);
66
+ }
67
+ catch {
68
+ return value;
69
+ }
70
+ }
71
+ if (defaultValue !== null) {
72
+ try {
73
+ return JSON.parse(value);
74
+ }
75
+ catch {
76
+ return value;
77
+ }
78
+ }
79
+ return value;
80
+ }
81
+ default:
82
+ return value;
83
+ }
84
+ }
43
85
  /**
44
86
  * Abstract base class for project configuration.
45
87
  *
@@ -94,7 +136,6 @@ class ConfigBase {
94
136
  * Fields registered by subclasses (or the base) that {@link reload}
95
137
  * should process. Common / DB / OTel fields are pre-registered.
96
138
  */
97
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
139
  this._fields = [];
99
140
  this.SERVICE_ID = serviceId;
100
141
  this.CONFIG_FILE = configFile || process.env.CONFIG_FILE || "config.json";
@@ -117,11 +158,15 @@ class ConfigBase {
117
158
  { field: "JWT_KEY", sensitive: true },
118
159
  { field: "LOG_LEVEL" },
119
160
  { field: "DATABASE_TYPE" },
120
- { field: "DATABASE_POSTGRES_HOST" },
121
- { field: "DATABASE_POSTGRES_PORT" },
122
- { field: "DATABASE_POSTGRES_USER" },
123
- { field: "DATABASE_POSTGRES_PASSWORD", sensitive: true },
124
- { field: "DATABASE_POSTGRES_DATABASE" },
161
+ { field: "DATABASE_POSTGRES_HOST", envAliases: ["POSTGRES_HOST"] },
162
+ { field: "DATABASE_POSTGRES_PORT", envAliases: ["POSTGRES_PORT"] },
163
+ { field: "DATABASE_POSTGRES_USER", envAliases: ["POSTGRES_USER"] },
164
+ {
165
+ field: "DATABASE_POSTGRES_PASSWORD",
166
+ sensitive: true,
167
+ envAliases: ["POSTGRES_PASSWORD"],
168
+ },
169
+ { field: "DATABASE_POSTGRES_DATABASE", envAliases: ["POSTGRES_DB"] },
125
170
  { field: "OPENTELEMETRY_COLLECTOR_HTTP_TRACES" },
126
171
  { field: "OPENTELEMETRY_COLLECTOR_HTTP_METRICS" },
127
172
  { field: "OPENTELEMETRY_COLLECTOR_HTTP_LOGS" },
@@ -146,10 +191,11 @@ class ConfigBase {
146
191
  * Call this in your subclass constructor for every project-specific field.
147
192
  */
148
193
  addConfigField(def) {
149
- var _a;
194
+ var _a, _b;
150
195
  this._fields.push({
151
196
  field: def.field,
152
197
  sensitive: (_a = def.sensitive) !== null && _a !== void 0 ? _a : false,
198
+ envAliases: (_b = def.envAliases) !== null && _b !== void 0 ? _b : [],
153
199
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
200
  defaultValue: this[def.field],
155
201
  });
@@ -175,14 +221,35 @@ class ConfigBase {
175
221
  log(`Configuration Value: CONFIG_FILE: ${this.CONFIG_FILE}`);
176
222
  log(`Configuration Value: SERVICE_ID: ${this.SERVICE_ID}`);
177
223
  log(`Configuration Value: VERSION: ${this.VERSION}`);
178
- for (const { field, sensitive } of this._fields) {
224
+ for (const { field, sensitive, envAliases, defaultValue } of this._fields) {
179
225
  let from = "defaults";
226
+ let foundValue;
227
+ let usedAlias = false;
228
+ // 1. Primary environment variable name
180
229
  if (process.env[field] !== undefined) {
181
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
- this[field] = process.env[field];
230
+ foundValue = process.env[field];
183
231
  from = "environment";
184
232
  }
185
- else if (content[field] !== undefined) {
233
+ // 2. Check aliases if primary env var was not set
234
+ if (foundValue === undefined && envAliases.length > 0) {
235
+ for (const alias of envAliases) {
236
+ if (process.env[alias] !== undefined) {
237
+ foundValue = process.env[alias];
238
+ from = "environment";
239
+ usedAlias = true;
240
+ break;
241
+ }
242
+ }
243
+ }
244
+ // 3. Apply environment value (full name or alias) if found,
245
+ // coercing strings to match the default value type
246
+ if (foundValue !== undefined) {
247
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
248
+ this[field] = coerceValue(foundValue, defaultValue);
249
+ }
250
+ // 4. Config file override (environment always wins, but if neither
251
+ // environment nor alias matched, check config file)
252
+ if (foundValue === undefined && content[field] !== undefined) {
186
253
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
187
254
  this[field] = content[field];
188
255
  from = "config";
@@ -193,7 +260,7 @@ class ConfigBase {
193
260
  else {
194
261
  log(
195
262
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
- `Configuration Value: ${field}: ${this[field]} (from ${from})`);
263
+ `Configuration Value: ${field}: ${this[field]} (from ${from}${usedAlias ? ` via alias` : ""})`);
197
264
  }
198
265
  }
199
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devopsplaybook.io/common-utils",
3
- "version": "1.2.0-beta.7.84f3f67",
3
+ "version": "1.2.0-beta.7.fd32a5a",
4
4
  "description": "Shared utility modules for devopsplaybook.io projects (DB, Config, OTel context, system helpers)",
5
5
  "keywords": [
6
6
  "Open Telemetry",
package/src/ConfigBase.ts CHANGED
@@ -11,6 +11,19 @@ export interface ConfigFieldDef {
11
11
  field: string;
12
12
  /** When `true` the value is masked in log output. */
13
13
  sensitive?: boolean;
14
+ /**
15
+ * Alternative environment variable names to check when the primary field
16
+ * name is not found in `process.env`. Aliases are tried in order and the
17
+ * first match wins. Only checked in the environment layer, never in the
18
+ * config-file layer.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // Look for DATABASE_POSTGRES_HOST first, then fall back to POSTGRES_HOST
23
+ * { field: "DATABASE_POSTGRES_HOST", envAliases: ["POSTGRES_HOST"] }
24
+ * ```
25
+ */
26
+ envAliases?: string[];
14
27
  }
15
28
 
16
29
  /**
@@ -40,6 +53,48 @@ export interface ConfigCommonInterface
40
53
  LOG_LEVEL: string;
41
54
  }
42
55
 
56
+ /**
57
+ * Coerce a string value read from an environment variable to match the
58
+ * type of the default value (number → parseFloat, boolean → "true"/"1",
59
+ * array → JSON.parse, etc.). When the default is already a string or
60
+ * there is no default, the original string is returned as-is.
61
+ */
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ function coerceValue(value: string, defaultValue: any): any {
64
+ if (defaultValue === undefined || defaultValue === null) {
65
+ return value;
66
+ }
67
+
68
+ switch (typeof defaultValue) {
69
+ case "number": {
70
+ const parsed = Number(value);
71
+ return Number.isNaN(parsed) ? value : parsed;
72
+ }
73
+ case "boolean": {
74
+ return value === "true" || value === "1" || value === "yes";
75
+ }
76
+ case "object": {
77
+ if (Array.isArray(defaultValue)) {
78
+ try {
79
+ return JSON.parse(value);
80
+ } catch {
81
+ return value;
82
+ }
83
+ }
84
+ if (defaultValue !== null) {
85
+ try {
86
+ return JSON.parse(value);
87
+ } catch {
88
+ return value;
89
+ }
90
+ }
91
+ return value;
92
+ }
93
+ default:
94
+ return value;
95
+ }
96
+ }
97
+
43
98
  /**
44
99
  * Abstract base class for project configuration.
45
100
  *
@@ -96,9 +151,13 @@ export abstract class ConfigBase implements ConfigCommonInterface {
96
151
  * Fields registered by subclasses (or the base) that {@link reload}
97
152
  * should process. Common / DB / OTel fields are pre-registered.
98
153
  */
99
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
- private _fields: { field: string; sensitive: boolean; defaultValue: any }[] =
101
- [];
154
+ private _fields: {
155
+ field: string;
156
+ sensitive: boolean;
157
+ envAliases: string[];
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
+ defaultValue: any;
160
+ }[] = [];
102
161
 
103
162
  /**
104
163
  * @param serviceId Unique service identifier (e.g. `"cryptotrader-server"`).
@@ -127,11 +186,15 @@ export abstract class ConfigBase implements ConfigCommonInterface {
127
186
  { field: "JWT_KEY", sensitive: true },
128
187
  { field: "LOG_LEVEL" },
129
188
  { field: "DATABASE_TYPE" },
130
- { field: "DATABASE_POSTGRES_HOST" },
131
- { field: "DATABASE_POSTGRES_PORT" },
132
- { field: "DATABASE_POSTGRES_USER" },
133
- { field: "DATABASE_POSTGRES_PASSWORD", sensitive: true },
134
- { field: "DATABASE_POSTGRES_DATABASE" },
189
+ { field: "DATABASE_POSTGRES_HOST", envAliases: ["POSTGRES_HOST"] },
190
+ { field: "DATABASE_POSTGRES_PORT", envAliases: ["POSTGRES_PORT"] },
191
+ { field: "DATABASE_POSTGRES_USER", envAliases: ["POSTGRES_USER"] },
192
+ {
193
+ field: "DATABASE_POSTGRES_PASSWORD",
194
+ sensitive: true,
195
+ envAliases: ["POSTGRES_PASSWORD"],
196
+ },
197
+ { field: "DATABASE_POSTGRES_DATABASE", envAliases: ["POSTGRES_DB"] },
135
198
  { field: "OPENTELEMETRY_COLLECTOR_HTTP_TRACES" },
136
199
  { field: "OPENTELEMETRY_COLLECTOR_HTTP_METRICS" },
137
200
  { field: "OPENTELEMETRY_COLLECTOR_HTTP_LOGS" },
@@ -160,6 +223,7 @@ export abstract class ConfigBase implements ConfigCommonInterface {
160
223
  this._fields.push({
161
224
  field: def.field,
162
225
  sensitive: def.sensitive ?? false,
226
+ envAliases: def.envAliases ?? [],
163
227
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
228
  defaultValue: (this as any)[def.field],
165
229
  });
@@ -187,17 +251,44 @@ export abstract class ConfigBase implements ConfigCommonInterface {
187
251
  log(`Configuration Value: SERVICE_ID: ${this.SERVICE_ID}`);
188
252
  log(`Configuration Value: VERSION: ${this.VERSION}`);
189
253
 
190
- for (const { field, sensitive } of this._fields) {
254
+ for (const { field, sensitive, envAliases, defaultValue } of this._fields) {
191
255
  let from = "defaults";
256
+ let foundValue: string | undefined;
257
+ let usedAlias = false;
258
+
259
+ // 1. Primary environment variable name
192
260
  if (process.env[field] !== undefined) {
193
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
- (this as any)[field] = process.env[field];
261
+ foundValue = process.env[field];
195
262
  from = "environment";
196
- } else if (content[field] !== undefined) {
263
+ }
264
+
265
+ // 2. Check aliases if primary env var was not set
266
+ if (foundValue === undefined && envAliases.length > 0) {
267
+ for (const alias of envAliases) {
268
+ if (process.env[alias] !== undefined) {
269
+ foundValue = process.env[alias];
270
+ from = "environment";
271
+ usedAlias = true;
272
+ break;
273
+ }
274
+ }
275
+ }
276
+
277
+ // 3. Apply environment value (full name or alias) if found,
278
+ // coercing strings to match the default value type
279
+ if (foundValue !== undefined) {
280
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
281
+ (this as any)[field] = coerceValue(foundValue, defaultValue);
282
+ }
283
+
284
+ // 4. Config file override (environment always wins, but if neither
285
+ // environment nor alias matched, check config file)
286
+ if (foundValue === undefined && content[field] !== undefined) {
197
287
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
288
  (this as any)[field] = content[field];
199
289
  from = "config";
200
290
  }
291
+
201
292
  if (sensitive) {
202
293
  log(
203
294
  `Configuration Value: ${field}: ******************** (from ${from})`,
@@ -205,7 +296,7 @@ export abstract class ConfigBase implements ConfigCommonInterface {
205
296
  } else {
206
297
  log(
207
298
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
- `Configuration Value: ${field}: ${(this as any)[field]} (from ${from})`,
299
+ `Configuration Value: ${field}: ${(this as any)[field]} (from ${from}${usedAlias ? ` via alias` : ""})`,
209
300
  );
210
301
  }
211
302
  }