@reimorg/config 1.0.0

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/lib/util.js ADDED
@@ -0,0 +1,1220 @@
1
+ // config.js (c) 2010-2022 Loren West and other contributors
2
+ // May be freely distributed under the MIT license.
3
+ // For further details and documentation:
4
+ // http://lorenwest.github.com/node-config
5
+
6
+ // Dependencies
7
+ const DeferredConfig = require('../defer').DeferredConfig;
8
+ const Path = require('path');
9
+ const FileSystem = require('fs');
10
+ const OS = require("os");
11
+
12
+ const DEFAULT_CONFIG_DIR = Path.join( process.cwd(), 'config');
13
+
14
+ /**
15
+ * A source in the configSources list
16
+ *
17
+ * @typedef {Object} ConfigSource
18
+ * @property {string} name
19
+ * @property {Object} parsed - parsed representation
20
+ * @property {string=} original - unparsed representation of the data
21
+ */
22
+
23
+ /**
24
+ * The data used for a Load operation, mostly derived from environment variables
25
+ *
26
+ * @typedef {Object} LoadOptions
27
+ * @property {string} configDir - config directory location, absolute or relative to cwd()
28
+ * @property {string} nodeEnv - NODE_ENV value or commo-separated list
29
+ * @property {string} hostName - hostName for host-specific loads
30
+ * @property {string=} appInstance - per-process config ID
31
+ * @property {boolean} skipConfigSources - don't track sources
32
+ * @property {boolean} gitCrypt - allow gitcrypt files
33
+ * @property {Parser} parser - alternative parser implementation
34
+ */
35
+
36
+ /** @type {LoadOptions} */
37
+ const DEFAULT_OPTIONS = {
38
+ configDir: DEFAULT_CONFIG_DIR,
39
+ nodeEnv: ['development'],
40
+ hostName: OS.hostname(),
41
+ gitCrypt: true,
42
+ parser: require("../parser.js")
43
+ };
44
+
45
+ /**
46
+ * Callback for converting loaded data.
47
+ *
48
+ * @callback DataConvert
49
+ * @param {Object} input - An object to modify.
50
+ * @returns {Object} - converted object
51
+ */
52
+
53
+
54
+ const DEFAULT_CLONE_DEPTH = 20;
55
+ const GIT_CRYPT_REGEX = /^.GITCRYPT/; // regular expression to test for gitcrypt files.
56
+
57
+ /**
58
+ * Util functions that do not require the singleton in order to run.
59
+ */
60
+ class Util {
61
+
62
+ /**
63
+ * <p>Make a configuration property hidden so it doesn't appear when enumerating
64
+ * elements of the object.</p>
65
+ *
66
+ * <p>
67
+ * The property still exists and can be read from and written to, but it won't
68
+ * show up in for ... in loops, Object.keys(), or JSON.stringify() type methods.
69
+ * </p>
70
+ *
71
+ * <p>
72
+ * If the property already exists, it will be made hidden. Otherwise it will
73
+ * be created as a hidden property with the specified value.
74
+ * </p>
75
+ *
76
+ * <p><i>
77
+ * This method was built for hiding configuration values, but it can be applied
78
+ * to <u>any</u> javascript object.
79
+ * </i></p>
80
+ *
81
+ * <p>Example:</p>
82
+ * <pre>
83
+ * const Util = require('config/lib/util.js');
84
+ * ...
85
+ *
86
+ * // Hide the Amazon S3 credentials
87
+ * Util.makeHidden(CONFIG.amazonS3, 'access_id');
88
+ * Util.makeHidden(CONFIG.amazonS3, 'secret_key');
89
+ * </pre>
90
+ *
91
+ * @method makeHidden
92
+ * @param object {Object} - The object to make a hidden property into.
93
+ * @param property {string} - The name of the property to make hidden.
94
+ * @param value {*} - (optional) Set the property value to this (otherwise leave alone)
95
+ * @return object {Object} - The original object is returned - for chaining.
96
+ */
97
+ static makeHidden(object, property, value) {
98
+ // If the new value isn't specified, just mark the property as hidden
99
+ if (typeof value === 'undefined') {
100
+ Object.defineProperty(object, property, {
101
+ enumerable: false,
102
+ configurable: true
103
+ });
104
+ } else {
105
+ // Otherwise set the value and mark it as hidden
106
+ Object.defineProperty(object, property, {
107
+ value: value,
108
+ enumerable: false,
109
+ configurable: true
110
+ });
111
+ }
112
+
113
+ return object;
114
+ }
115
+
116
+ /**
117
+ * Looks into an options object for a specific attribute
118
+ *
119
+ * <p>
120
+ * This method looks into the options object, and if an attribute is defined, returns it,
121
+ * and if not, returns the default value
122
+ * </p>
123
+ *
124
+ * @method getOption
125
+ * @param options {Object | undefined} the options object
126
+ * @param optionName {string} the attribute name to look for
127
+ * @param defaultValue { any } the default in case the options object is empty, or the attribute does not exist.
128
+ * @return options[optionName] if defined, defaultValue if not.
129
+ */
130
+ static getOption(options, optionName, defaultValue) {
131
+ if (options !== undefined && typeof options[optionName] !== 'undefined') {
132
+ return options[optionName];
133
+ } else {
134
+ return defaultValue;
135
+ }
136
+ }
137
+
138
+
139
+ /**
140
+ * Load the individual file configurations.
141
+ *
142
+ * <p>
143
+ * This method builds a map of filename to the configuration object defined
144
+ * by the file. The search order is:
145
+ * </p>
146
+ *
147
+ * <pre>
148
+ * default.EXT
149
+ * (deployment).EXT
150
+ * (hostname).EXT
151
+ * (hostname)-(deployment).EXT
152
+ * local.EXT
153
+ * local-(deployment).EXT
154
+ * </pre>
155
+ *
156
+ * <p>
157
+ * EXT can be yml, yaml, coffee, iced, json, jsonc, cson or js signifying the file type.
158
+ * yaml (and yml) is in YAML format, coffee is a coffee-script, iced is iced-coffee-script,
159
+ * json is in JSON format, jsonc is in JSONC format, cson is in CSON format, properties is
160
+ * in .properties format (http://en.wikipedia.org/wiki/.properties), and js is a javascript
161
+ * executable file that is require()'d with module.exports being the config object.
162
+ * </p>
163
+ *
164
+ * <p>
165
+ * hostname is the $HOST environment variable (or --HOST command line parameter)
166
+ * if set, otherwise the $HOSTNAME environment variable (or --HOSTNAME command
167
+ * line parameter) if set, otherwise the hostname found from
168
+ * require('os').hostname().
169
+ * </p>
170
+ *
171
+ * <p>
172
+ * Once a hostname is found, everything from the first period ('.') onwards
173
+ * is removed. For example, abc.example.com becomes abc
174
+ * </p>
175
+ *
176
+ * <p>
177
+ * (deployment) is the deployment type, found in the $NODE_ENV environment
178
+ * variable (which can be overridden by using $NODE_CONFIG_ENV
179
+ * environment variable). Defaults to 'development'.
180
+ * </p>
181
+ *
182
+ * <p>
183
+ * If the $NODE_APP_INSTANCE environment variable (or --NODE_APP_INSTANCE
184
+ * command line parameter) is set, then files with this appendage will be loaded.
185
+ * See the Multiple Application Instances section of the main documentation page
186
+ * for more information.
187
+ * </p>
188
+ *
189
+ * @method loadFileConfigs
190
+ * @param opts {LoadOptions | Load} parsing options or Load to update
191
+ * @return loadConfig {LoadConfig}
192
+ */
193
+ static loadFileConfigs(opts) {
194
+ let load;
195
+
196
+ if (opts instanceof Load) {
197
+ load = opts;
198
+ } else {
199
+ load = new Load(opts);
200
+ }
201
+
202
+ let options = load.options;
203
+ let dir = options.configDir;
204
+ dir = _toAbsolutePath(dir);
205
+
206
+ // Read each file in turn
207
+ const baseNames = ['default'].concat(options.nodeEnv);
208
+ const hostName = options.hostName;
209
+
210
+ // #236: Also add full hostname when they are different.
211
+ if (hostName) {
212
+ const firstDomain = hostName.split('.')[0];
213
+
214
+ for (let env of options.nodeEnv) {
215
+ // Backward compatibility
216
+ baseNames.push(firstDomain, firstDomain + '-' + env);
217
+
218
+ // Add full hostname when it is not the same
219
+ if (hostName !== firstDomain) {
220
+ baseNames.push(hostName, hostName + '-' + env);
221
+ }
222
+ }
223
+ }
224
+
225
+ for (let env of options.nodeEnv) {
226
+ baseNames.push('local', 'local-' + env);
227
+ }
228
+
229
+ const allowedFiles = {};
230
+ let resolutionIndex = 1;
231
+ const extNames = options.parser.getFilesOrder();
232
+
233
+ for (let baseName of baseNames) {
234
+ const fileNames = [baseName];
235
+ if (options.appInstance) {
236
+ fileNames.push(baseName + '-' + options.appInstance);
237
+ }
238
+
239
+ for (let fileName of fileNames) {
240
+ for (let extName of extNames) {
241
+ allowedFiles[fileName + '.' + extName] = resolutionIndex++;
242
+ }
243
+ }
244
+ }
245
+
246
+ const locatedFiles = this.locateMatchingFiles(dir, allowedFiles);
247
+ for (let fullFilename of locatedFiles) {
248
+ load.loadFile(fullFilename);
249
+ }
250
+
251
+ return load;
252
+ }
253
+
254
+ /**
255
+ * Return a list of fullFilenames who exists in allowedFiles
256
+ * Ordered according to allowedFiles argument specifications
257
+ *
258
+ * @method locateMatchingFiles
259
+ * @param configDirs {string} the config dir, or multiple dirs separated by a column (:)
260
+ * @param allowedFiles {Object} an object. keys and supported filenames
261
+ * and values are the position in the resolution order
262
+ * @returns {string[]} fullFilenames - path + filename
263
+ */
264
+ static locateMatchingFiles(configDirs, allowedFiles) {
265
+ return configDirs.split(Path.delimiter)
266
+ .filter(Boolean)
267
+ .reduce(function (files, configDir) {
268
+ configDir = _toAbsolutePath(configDir);
269
+ try {
270
+ FileSystem.readdirSync(configDir)
271
+ .filter(file => allowedFiles[file])
272
+ .forEach(function (file) {
273
+ files.push([allowedFiles[file], Path.join(configDir, file)]);
274
+ });
275
+ } catch (e) {
276
+ }
277
+
278
+ return files;
279
+ }, [])
280
+ .sort(function (a, b) {
281
+ return a[0] - b[0];
282
+ })
283
+ .map(function (file) {
284
+ return file[1];
285
+ });
286
+ }
287
+
288
+ /**
289
+ *
290
+ * @param config
291
+ */
292
+ static resolveDeferredConfigs(config) {
293
+ const deferred = [];
294
+
295
+ function _iterate (prop) {
296
+ if (prop == null || prop.constructor === String) {
297
+ return;
298
+ }
299
+
300
+ // We put the properties we are going to look it in an array to keep the order predictable
301
+ const propsToSort = Object.keys(prop).filter((property) => prop[property] != null);
302
+
303
+ // Second step is to iterate of the elements in a predictable (sorted) order
304
+ propsToSort.sort().forEach(function (property) {
305
+ if (prop[property].constructor === Object) {
306
+ _iterate(prop[property]);
307
+ } else if (prop[property].constructor === Array) {
308
+ for (let i = 0; i < prop[property].length; i++) {
309
+ if (prop[property][i] instanceof DeferredConfig) {
310
+ deferred.push(prop[property][i].prepare(config, prop[property], i));
311
+ } else {
312
+ _iterate(prop[property][i]);
313
+ }
314
+ }
315
+ } else {
316
+ if (prop[property] instanceof DeferredConfig) {
317
+ deferred.push(prop[property].prepare(config, prop, property));
318
+ }
319
+ // else: Nothing to do. Keep the property how it is.
320
+ }
321
+ });
322
+ }
323
+
324
+ _iterate(config);
325
+
326
+ deferred.forEach(function (defer) { defer.resolve(); });
327
+ }
328
+
329
+ /**
330
+ * Return a deep copy of the specified object.
331
+ *
332
+ * This returns a new object with all elements copied from the specified
333
+ * object. Deep copies are made of objects and arrays so you can do anything
334
+ * with the returned object without affecting the input object.
335
+ *
336
+ * @method cloneDeep
337
+ * @param parent {Object} The original object to copy from
338
+ * @param [depth=20] {number} Maximum depth (default 20)
339
+ * @return {Object} A new object with the elements copied from the copyFrom object
340
+ *
341
+ * This method is copied from https://github.com/pvorb/node-clone/blob/17eea36140d61d97a9954c53417d0e04a00525d9/clone.js
342
+ *
343
+ * Copyright © 2011-2014 Paul Vorbach and contributors.
344
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
345
+ * of this software and associated documentation files (the “Software”), to deal
346
+ * in the Software without restriction, including without limitation the rights
347
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
348
+ * of the Software, and to permit persons to whom the Software is furnished to do so,
349
+ * subject to the following conditions: The above copyright notice and this permission
350
+ * notice shall be included in all copies or substantial portions of the Software.
351
+ */
352
+ static cloneDeep(parent, depth, circular, prototype) {
353
+ // maintain two arrays for circular references, where corresponding parents
354
+ // and children have the same index
355
+ const allParents = [];
356
+ const allChildren = [];
357
+ const util = this;
358
+
359
+ const useBuffer = typeof Buffer !== 'undefined';
360
+
361
+ if (typeof circular === 'undefined')
362
+ circular = true;
363
+
364
+ if (typeof depth === 'undefined')
365
+ depth = 20;
366
+
367
+ // recurse this function so we don't reset allParents and allChildren
368
+ function _clone(parent, depth) {
369
+ // cloning null always returns null
370
+ if (parent === null)
371
+ return null;
372
+
373
+ if (depth === 0)
374
+ return parent;
375
+
376
+ let child;
377
+ if (typeof parent != 'object') {
378
+ return parent;
379
+ }
380
+
381
+ if (Array.isArray(parent)) {
382
+ child = [];
383
+ } else if (parent instanceof RegExp) {
384
+ child = new RegExp(parent.source, util.getRegExpFlags(parent));
385
+ if (parent.lastIndex) child.lastIndex = parent.lastIndex;
386
+ } else if (parent instanceof Date) {
387
+ child = new Date(parent.getTime());
388
+ } else if (useBuffer && Buffer.isBuffer(parent)) {
389
+ child = Buffer.alloc(parent.length);
390
+ parent.copy(child);
391
+ return child;
392
+ } else {
393
+ if (typeof prototype === 'undefined') child = Object.create(Object.getPrototypeOf(parent));
394
+ else child = Object.create(prototype);
395
+ }
396
+
397
+ if (circular) {
398
+ const index = allParents.indexOf(parent);
399
+
400
+ if (index !== -1) {
401
+ return allChildren[index];
402
+ }
403
+ allParents.push(parent);
404
+ allChildren.push(child);
405
+ }
406
+
407
+ for (const i in parent) {
408
+ const propDescriptor = Object.getOwnPropertyDescriptor(parent, i);
409
+ const hasGetter = ((typeof propDescriptor !== 'undefined') && (typeof propDescriptor.get !== 'undefined'));
410
+
411
+ if (hasGetter) {
412
+ Object.defineProperty(child, i, propDescriptor);
413
+ } else if (util.isPromise(parent[i])) {
414
+ child[i] = parent[i];
415
+ } else {
416
+ child[i] = _clone(parent[i], depth - 1);
417
+ }
418
+ }
419
+
420
+ return child;
421
+ }
422
+
423
+ return _clone(parent, depth);
424
+ }
425
+
426
+ /**
427
+ * Underlying get mechanism
428
+ *
429
+ * @method getPath
430
+ * @param object {Object} - Object to get the property for
431
+ * @param property {string|string[]} - The property name to get (as an array or '.' delimited string)
432
+ * @return value {*} - Property value, including undefined if not defined.
433
+ */
434
+ static getPath(object, property) {
435
+ const path = Array.isArray(property) ? property : property.split('.');
436
+
437
+ let next = object;
438
+ for (let i = 0; i < path.length; i++) {
439
+ const name = path[i];
440
+ const value = next[name];
441
+
442
+ if (i === path.length - 1) {
443
+ return value;
444
+ }
445
+
446
+ // Note that typeof null === 'object'
447
+ if (value === null || typeof value !== 'object') {
448
+ return undefined;
449
+ }
450
+
451
+ next = value;
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Set objects given a path as a string list
457
+ *
458
+ * @method setPath
459
+ * @param object {Object} - Object to set the property on
460
+ * @param property {string|string[]} - The property name to get (as an array or '.' delimited string)
461
+ * @param value {*} - value to set, ignoring null
462
+ * @return {*} - the given value
463
+ */
464
+ static setPath(object, property, value) {
465
+ const path = Array.isArray(property) ? property : property.split('.');
466
+
467
+ if (value === null || path.length === 0) {
468
+ return;
469
+ }
470
+
471
+ let next = object;
472
+
473
+ for (let i = 0; i < path.length; i++) {
474
+ let name = path[i];
475
+
476
+ if (i === path.length - 1) { // no more keys to make, so set the value
477
+ next[name] = value;
478
+ } else if (Object.hasOwnProperty.call(next, name)) {
479
+ next = next[name];
480
+ } else {
481
+ next = next[name] = {};
482
+ }
483
+ }
484
+
485
+ return value;
486
+ }
487
+
488
+ /**
489
+ * Return true if two objects have equal contents.
490
+ *
491
+ * @method equalsDeep
492
+ * @param object1 {Object} The object to compare from
493
+ * @param object2 {Object} The object to compare with
494
+ * @param depth {number} An optional depth to prevent recursion. Default: 20.
495
+ * @return {boolean} True if both objects have equivalent contents
496
+ */
497
+ static equalsDeep(object1, object2, depth) {
498
+ // Recursion detection
499
+ depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth);
500
+ if (depth < 0) {
501
+ return {};
502
+ }
503
+
504
+ // Fast comparisons
505
+ if (!object1 || !object2) {
506
+ return false;
507
+ }
508
+ if (object1 === object2) {
509
+ return true;
510
+ }
511
+ if (typeof (object1) != 'object' || typeof (object2) != 'object') {
512
+ return false;
513
+ }
514
+
515
+ // They must have the same keys. If their length isn't the same
516
+ // then they're not equal. If the keys aren't the same, the value
517
+ // comparisons will fail.
518
+ if (Object.keys(object1).length != Object.keys(object2).length) {
519
+ return false;
520
+ }
521
+
522
+ // Compare the values
523
+ for (const prop in object1) {
524
+
525
+ // Call recursively if an object or array
526
+ if (object1[prop] && typeof (object1[prop]) === 'object') {
527
+ if (!this.equalsDeep(object1[prop], object2[prop], depth - 1)) {
528
+ return false;
529
+ }
530
+ } else {
531
+ if (object1[prop] !== object2[prop]) {
532
+ return false;
533
+ }
534
+ }
535
+ }
536
+
537
+ // Test passed.
538
+ return true;
539
+ }
540
+
541
+ /**
542
+ * Extend an object, and any object it contains.
543
+ *
544
+ * This does not replace deep objects, but dives into them
545
+ * replacing individual elements instead.
546
+ *
547
+ * @method extendDeep
548
+ * @param mergeInto {Object} The object to merge into
549
+ * @param mergeFrom... {Object} - Any number of objects to merge from
550
+ * @param depth {integer} An optional depth to prevent recursion. Default: 20.
551
+ * @return {Object} The altered mergeInto object is returned
552
+ */
553
+ static extendDeep(mergeInto, ...vargs) {
554
+ // Initialize
555
+ let depth = vargs.pop();
556
+ if (typeof (depth) != 'number') {
557
+ vargs.push(depth);
558
+ depth = DEFAULT_CLONE_DEPTH;
559
+ }
560
+
561
+ // Recursion detection
562
+ if (depth < 0) {
563
+ return mergeInto;
564
+ }
565
+
566
+ for (let mergeFrom of vargs) {
567
+ // Cycle through each element of the object to merge from
568
+ for (const prop in mergeFrom) {
569
+ // save original value in deferred elements
570
+ const fromIsDeferredFunc = mergeFrom[prop] instanceof DeferredConfig;
571
+ const isDeferredFunc = mergeInto[prop] instanceof DeferredConfig;
572
+
573
+ if (fromIsDeferredFunc && Object.hasOwnProperty.call(mergeInto, prop)) {
574
+ mergeFrom[prop]._original = isDeferredFunc ? mergeInto[prop]._original : mergeInto[prop];
575
+ }
576
+ // Extend recursively if both elements are objects and target is not really a deferred function
577
+ if (mergeFrom[prop] instanceof Date) {
578
+ mergeInto[prop] = mergeFrom[prop];
579
+ }
580
+ if (mergeFrom[prop] instanceof RegExp) {
581
+ mergeInto[prop] = mergeFrom[prop];
582
+ } else if (Util.isObject(mergeInto[prop]) && Util.isObject(mergeFrom[prop]) && !isDeferredFunc) {
583
+ Util.extendDeep(mergeInto[prop], mergeFrom[prop], depth - 1);
584
+ } else if (Util.isPromise(mergeFrom[prop])) {
585
+ mergeInto[prop] = mergeFrom[prop];
586
+ }
587
+ // Copy recursively if the mergeFrom element is an object (or array or fn)
588
+ else if (mergeFrom[prop] && typeof mergeFrom[prop] === 'object') {
589
+ mergeInto[prop] = Util.cloneDeep(mergeFrom[prop], depth - 1);
590
+ }
591
+ // Copy property descriptor otherwise, preserving accessors
592
+ else if (Object.getOwnPropertyDescriptor(Object(mergeFrom), prop)) {
593
+ Object.defineProperty(mergeInto, prop, Object.getOwnPropertyDescriptor(Object(mergeFrom), prop));
594
+ } else if (mergeInto[prop] !== mergeFrom[prop]) {
595
+ mergeInto[prop] = mergeFrom[prop];
596
+ }
597
+ }
598
+ }
599
+
600
+ // Chain
601
+ return mergeInto;
602
+ }
603
+
604
+ /**
605
+ * Is the specified argument a regular javascript object?
606
+ *
607
+ * The argument is an object if it's a JS object, but not an array.
608
+ *
609
+ * @method isObject
610
+ * @param obj {*} An argument of any type.
611
+ * @return {boolean} TRUE if the arg is an object, FALSE if not
612
+ */
613
+ static isObject(obj) {
614
+ return (obj !== null) && (typeof obj === 'object') && !(Array.isArray(obj));
615
+ }
616
+
617
+ /**
618
+ * Is the specified argument a javascript promise?
619
+ *
620
+ * @method isPromise
621
+ * @param obj {*} An argument of any type.
622
+ * @returns {boolean}
623
+ */
624
+ static isPromise(obj) {
625
+ return Object.prototype.toString.call(obj) === '[object Promise]';
626
+ }
627
+
628
+ /**
629
+ * Returns a string of flags for regular expression `re`.
630
+ *
631
+ * @param {RegExp} re Regular expression
632
+ * @returns {string} Flags
633
+ */
634
+ static getRegExpFlags = function (re) {
635
+ let flags = '';
636
+ re.global && (flags += 'g');
637
+ re.ignoreCase && (flags += 'i');
638
+ re.multiline && (flags += 'm');
639
+
640
+ return flags;
641
+ }
642
+
643
+ /**
644
+ * Returns a new deep copy of the current config object, or any part of the config if provided.
645
+ *
646
+ * @param {Object} config The part of the config to copy and serialize.
647
+ * @returns {Object} The cloned config or part of the config
648
+ */
649
+ static toObject(config) {
650
+ return JSON.parse(JSON.stringify(config));
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Record a set of lookups
656
+ */
657
+ class Env {
658
+ constructor() {
659
+ this.lookups = {};
660
+ }
661
+
662
+ /**
663
+ * <p>Initialize a parameter from the command line or process environment</p>
664
+ *
665
+ * <p>
666
+ * This method looks for the parameter from the command line in the format
667
+ * --PARAMETER=VALUE, then from the process environment, then from the
668
+ * default specified as an argument.
669
+ * </p>
670
+ *
671
+ * @method initParam
672
+ * @param paramName {String} Name of the parameter
673
+ * @param [defaultValue] {*} Default value of the parameter
674
+ * @return {*} The found value, or default value
675
+ */
676
+ initParam(paramName, defaultValue) {
677
+ // Record and return the value
678
+ const value = this.getCmdLineArg(paramName) || process.env[paramName] || defaultValue;
679
+ this.setEnv(paramName, value);
680
+
681
+ return value;
682
+ }
683
+
684
+ /**
685
+ * <p>Get Command Line Arguments</p>
686
+ *
687
+ * <p>
688
+ * This method allows you to retrieve the value of the specified command line argument.
689
+ * </p>
690
+ *
691
+ * <p>
692
+ * The argument is case sensitive, and must be of the form '--ARG_NAME=value'
693
+ * </p>
694
+ *
695
+ * @method getCmdLineArg
696
+ * @param searchFor {String} The argument name to search for
697
+ * @return {*} false if the argument was not found, the argument value if found
698
+ */
699
+ getCmdLineArg(searchFor) {
700
+ const cmdLineArgs = process.argv.slice(2, process.argv.length);
701
+ const argName = '--' + searchFor + '=';
702
+
703
+ for (let argvIt = 0; argvIt < cmdLineArgs.length; argvIt++) {
704
+ if (cmdLineArgs[argvIt].indexOf(argName) === 0) {
705
+ return cmdLineArgs[argvIt].substr(argName.length);
706
+ }
707
+ }
708
+
709
+ return false;
710
+ }
711
+
712
+ /**
713
+ * <p>Get a Config Environment Variable Value</p>
714
+ *
715
+ * <p>
716
+ * This method returns the value of the specified config environment variable,
717
+ * including any defaults or overrides.
718
+ * </p>
719
+ *
720
+ * @method getEnv
721
+ * @param varName {String} The environment variable name
722
+ * @return {String} The value of the environment variable
723
+ */
724
+ getEnv(varName) {
725
+ return this.lookups[varName];
726
+ }
727
+
728
+ /**
729
+ * Set a tracing variable of what was accessed from process.env
730
+ *
731
+ * @see fromEnvironment
732
+ * @param key {string}
733
+ * @param value
734
+ */
735
+ setEnv(key, value) {
736
+ this.lookups[key] = value;
737
+ }
738
+ }
739
+
740
+
741
+ /**
742
+ * The work horse of loading Config data - without the singleton.
743
+ *
744
+ * This class can be used to execute important workflows, such as build-time validations
745
+ * and Module Defaults.
746
+ *
747
+ * @example
748
+ * //load module defaults
749
+ * const config = require("config");
750
+ * const Load = require("config/util.js").Load;
751
+ *
752
+ * let load = Load.fromEnvironment();
753
+ *
754
+ * load.scan();
755
+ *
756
+ * config.setModuleDefaults("my-module", load.config);
757
+ *
758
+ * @example
759
+ * // verify configs
760
+ * const Load = require("config/util.js").Load;
761
+ *
762
+ * for (let environment of ["sandbox", "qa", "qa-hyderabad", "perf", "staging", "prod-east-1", "prod-west-2"] {
763
+ * let load = Load.fromEnvironment(environment);
764
+ *
765
+ * load.scan();
766
+ * }
767
+ *
768
+ *
769
+ * @class Load
770
+ */
771
+ class Load {
772
+ /**
773
+ * @constructor
774
+ * @param options {LoadOptions=} - defaults to reading from environment variables
775
+ */
776
+ constructor(options, env = new Env()) {
777
+ this.env = env;
778
+ this.options = { ...DEFAULT_OPTIONS, ...options };
779
+ this.sources = this.options.skipConfigSources ? undefined : [];
780
+ this.parser = this.options.parser;
781
+ this.config = {};
782
+ this.defaults = undefined;
783
+ this.unmerged = undefined;
784
+ }
785
+
786
+ /**
787
+ * <p>Initialize a parameter from the command line or process environment</p>
788
+ *
789
+ * <p>
790
+ * This method looks for the parameter from the command line in the format
791
+ * --PARAMETER=VALUE, then from the process environment, then from the
792
+ * default specified as an argument.
793
+ * </p>
794
+ *
795
+ * @method initParam
796
+ * @param paramName {String} Name of the parameter
797
+ * @param [defaultValue] {*} Default value of the parameter
798
+ * @return {*} The found value, or default value
799
+ */
800
+ initParam(paramName, defaultValue) {
801
+ return this.env.initParam(paramName, defaultValue);
802
+ }
803
+
804
+ /**
805
+ * <p>Get Command Line Arguments</p>
806
+ *
807
+ * <p>
808
+ * This method allows you to retrieve the value of the specified command line argument.
809
+ * </p>
810
+ *
811
+ * <p>
812
+ * The argument is case sensitive, and must be of the form '--ARG_NAME=value'
813
+ * </p>
814
+ *
815
+ * @method getCmdLineArg
816
+ * @param searchFor {String} The argument name to search for
817
+ * @return {*} false if the argument was not found, the argument value if found
818
+ */
819
+ getCmdLineArg(searchFor) {
820
+ return this.env.getCmdLineArg(searchFor);
821
+ }
822
+
823
+ /**
824
+ * <p>Get a Config Environment Variable Value</p>
825
+ *
826
+ * <p>
827
+ * This method returns the value of the specified config environment variable,
828
+ * including any defaults or overrides.
829
+ * </p>
830
+ *
831
+ * @method getEnv
832
+ * @param varName {String} The environment variable name
833
+ * @return {String} The value of the environment variable
834
+ */
835
+ getEnv(varName) {
836
+ return this.env.getEnv(varName);
837
+ }
838
+
839
+ /**
840
+ * Set a tracing variable of what was accessed from process.env
841
+ *
842
+ * @see fromEnvironment
843
+ * @param key {string}
844
+ * @param value
845
+ */
846
+ setEnv(key, value) {
847
+ return this.env.setEnv(key, value);
848
+ }
849
+
850
+ /**
851
+ * Add a set of configurations and record the source
852
+ *
853
+ * @param name {string=} an entry will be added to sources under this name (if given)
854
+ * @param values {Object} values to merge in
855
+ * @param original {string=} Optional unparsed version of the data
856
+ */
857
+ addConfig(name, values, original) {
858
+ Util.extendDeep(this.config, values);
859
+
860
+ if (name && this.sources) {
861
+ let source = {name, parsed: values};
862
+
863
+ if (original !== undefined) {
864
+ source.original = original;
865
+ }
866
+
867
+ this.sources.push(source);
868
+ }
869
+
870
+ return this;
871
+ }
872
+
873
+
874
+ /**
875
+ * scan and load config files in the same manner that config.js does
876
+ *
877
+ * @param load {Load}
878
+ * @param additional {{name, value}[]=} additional values to populate (usually from NODE_CONFIG
879
+ */
880
+ scan(additional) {
881
+ Util.loadFileConfigs(this);
882
+
883
+ if (additional) {
884
+ for (let {name, config} of additional) {
885
+ this.addConfig(name, config);
886
+ }
887
+ }
888
+
889
+ // Override with environment variables if there is a custom-environment-variables.EXT mapping file
890
+ this.loadCustomEnvVars();
891
+
892
+ Util.resolveDeferredConfigs(this.config);
893
+ }
894
+
895
+ /**
896
+ * Load a file and add it to the configuration
897
+ *
898
+ * @param fullFilename {string} an absolute file path
899
+ * @param convert {DataConvert=}
900
+ * @returns {null}
901
+ */
902
+ loadFile(fullFilename, convert) {
903
+ let configObject = null;
904
+ let fileContent = null;
905
+
906
+ // Note that all methods here are the Sync versions. This is appropriate during
907
+ // module loading (which is a synchronous operation), but not thereafter.
908
+
909
+ try {
910
+ // Try loading the file.
911
+ fileContent = FileSystem.readFileSync(fullFilename, 'utf-8');
912
+ fileContent = fileContent.replace(/^\uFEFF/, '');
913
+ }
914
+ catch (e2) {
915
+ if (e2.code !== 'ENOENT') {
916
+ throw new Error('Config file ' + fullFilename + ' cannot be read. Error code is: '+e2.code
917
+ +'. Error message is: '+e2.message);
918
+ }
919
+ return null; // file doesn't exists
920
+ }
921
+
922
+ // Parse the file based on extension
923
+ try {
924
+
925
+ // skip if it's a gitcrypt file and CONFIG_SKIP_GITCRYPT is true
926
+ if (!this.options.gitCrypt) {
927
+ if (GIT_CRYPT_REGEX.test(fileContent)) {
928
+ console.error('WARNING: ' + fullFilename + ' is a git-crypt file and CONFIG_SKIP_GITCRYPT is set. skipping.');
929
+ return null;
930
+ }
931
+ }
932
+
933
+ configObject = this.parser.parse(fullFilename, fileContent);
934
+ } catch (e3) {
935
+ if (GIT_CRYPT_REGEX.test(fileContent)) {
936
+ console.error('ERROR: ' + fullFilename + ' is a git-crypt file and CONFIG_SKIP_GITCRYPT is not set.');
937
+ }
938
+ throw new Error("Cannot parse config file: '" + fullFilename + "': " + e3);
939
+ }
940
+
941
+ if (convert) {
942
+ configObject = convert(configObject);
943
+ }
944
+
945
+ this.addConfig(fullFilename, configObject, fileContent);
946
+
947
+ return configObject;
948
+ }
949
+
950
+ /**
951
+ * load custom-environment-variables
952
+ *
953
+ * @param extNames {string[]=} extensions
954
+ * @returns {{}}
955
+ */
956
+ loadCustomEnvVars(extNames) {
957
+ let resolutionIndex = 1;
958
+ const allowedFiles = {};
959
+
960
+ extNames = extNames ?? this.parser.getFilesOrder();
961
+
962
+ extNames.forEach(function (extName) {
963
+ allowedFiles['custom-environment-variables' + '.' + extName] = resolutionIndex++;
964
+ });
965
+
966
+ const locatedFiles = Util.locateMatchingFiles(this.options.configDir, allowedFiles);
967
+ locatedFiles.forEach((fullFilename) => {
968
+ this.loadFile(fullFilename, (configObj) => this.substituteDeep(configObj, process.env));
969
+ });
970
+ }
971
+
972
+ /**
973
+ * Return the report of where the sources for this load operation came from
974
+ * @returns {ConfigSource[]}
975
+ */
976
+ getSources() {
977
+ return (this.sources ?? []).slice();
978
+ }
979
+
980
+ /**
981
+ * <p>
982
+ * Set default configurations for a node.js module.
983
+ * </p>
984
+ *
985
+ * <p>
986
+ * This allows module developers to attach their configurations onto the
987
+ * default configuration object so they can be configured by the consumers
988
+ * of the module.
989
+ * </p>
990
+ *
991
+ * <p>Using the function within your module:</p>
992
+ * <pre>
993
+ * load.setModuleDefaults("MyModule", {
994
+ * &nbsp;&nbsp;templateName: "t-50",
995
+ * &nbsp;&nbsp;colorScheme: "green"
996
+ * });
997
+ * <br>
998
+ * // Template name may be overridden by application config files
999
+ * console.log("Template: " + CONFIG.MyModule.templateName);
1000
+ * </pre>
1001
+ *
1002
+ * <p>
1003
+ * The above example results in a "MyModule" element of the configuration
1004
+ * object, containing an object with the specified default values.
1005
+ * </p>
1006
+ *
1007
+ * @method setModuleDefaults
1008
+ * @param moduleName {string} - Name of your module.
1009
+ * @param defaultProperties {Object} - The default module configuration.
1010
+ * @return {Object} - The module level configuration object.
1011
+ */
1012
+ setModuleDefaults(moduleName, defaultProperties) {
1013
+ if (this.defaults === undefined) {
1014
+ this.defaults = {};
1015
+ this.unmerged = {};
1016
+
1017
+ if (this.sources) {
1018
+ this.sources.splice(0, 0, { name: 'Module Defaults', parsed: this.defaults });
1019
+ }
1020
+ }
1021
+
1022
+ const path = moduleName.split('.');
1023
+ const defaults = Util.setPath(this.defaults, path, Util.getPath(this.defaults, path) ?? {});
1024
+
1025
+ Util.extendDeep(defaults, defaultProperties);
1026
+
1027
+ const original =
1028
+ Util.getPath(this.unmerged, path) ??
1029
+ Util.setPath(this.unmerged, path, Util.getPath(this.config, path) ?? {});
1030
+
1031
+ const moduleConfig = Util.extendDeep({}, defaults, original);
1032
+ Util.setPath(this.config, path, moduleConfig);
1033
+ Util.resolveDeferredConfigs(this.config);
1034
+
1035
+ return moduleConfig;
1036
+ }
1037
+
1038
+ /**
1039
+ * Parse and return the specified string with the specified format.
1040
+ *
1041
+ * The format determines the parser to use.
1042
+ *
1043
+ * json = File is parsed using JSON.parse()
1044
+ * yaml (or yml) = Parsed with a YAML parser
1045
+ * toml = Parsed with a TOML parser
1046
+ * cson = Parsed with a CSON parser
1047
+ * hjson = Parsed with a HJSON parser
1048
+ * json5 = Parsed with a JSON5 parser
1049
+ * properties = Parsed with the 'properties' node package
1050
+ * xml = Parsed with a XML parser
1051
+ *
1052
+ * If the file doesn't exist, a null will be returned. If the file can't be
1053
+ * parsed, an exception will be thrown.
1054
+ *
1055
+ * This method performs synchronous file operations, and should not be called
1056
+ * after synchronous module loading.
1057
+ *
1058
+ * @protected
1059
+ * @method parseString
1060
+ * @param content {string} The full content
1061
+ * @param format {string} The format to be parsed
1062
+ * @return {configObject} The configuration object parsed from the string
1063
+ */
1064
+ parseString = function (content, format) {
1065
+ const parser = this.parser.getParser(format);
1066
+
1067
+ if (typeof parser === 'function') {
1068
+ return parser(null, content);
1069
+ } else {
1070
+ //TODO: throw on missing #753
1071
+ }
1072
+ }
1073
+
1074
+ /**
1075
+ * Create a new object patterned after substitutionMap, where:
1076
+ * 1. Terminal string values in substitutionMap are used as keys
1077
+ * 2. To look up values in a key-value store, variables
1078
+ * 3. And parent keys are created as necessary to retain the structure of substitutionMap.
1079
+ *
1080
+ * @protected
1081
+ * @method substituteDeep
1082
+ * @param substitutionMap {Object} - an object whose terminal (non-subobject) values are strings
1083
+ * @param variables {object[string:value]} - usually process.env, a flat object used to transform
1084
+ * terminal values in a copy of substitutionMap.
1085
+ * @returns {Object} - deep copy of substitutionMap with only those paths whose terminal values
1086
+ * corresponded to a key in `variables`
1087
+ */
1088
+ substituteDeep(substitutionMap, variables) {
1089
+ const result = {};
1090
+
1091
+ const _substituteVars = (map, vars, pathTo) => {
1092
+ for (const prop in map) {
1093
+ const value = map[prop];
1094
+
1095
+ if (typeof(value) === 'string') { // We found a leaf variable name
1096
+ if (typeof vars[value] !== 'undefined' && vars[value] !== '') { // if the vars provide a value set the value in the result map
1097
+ Util.setPath(result, pathTo.concat(prop), vars[value]);
1098
+ }
1099
+ } else if (Util.isObject(value)) { // work on the subtree, giving it a clone of the pathTo
1100
+ if ('__name' in value && '__format' in value && typeof vars[value.__name] !== 'undefined' && vars[value.__name] !== '') {
1101
+ let parsedValue;
1102
+ try {
1103
+ parsedValue = this.parseString(vars[value.__name], value.__format);
1104
+ } catch(err) {
1105
+ err.message = '__format parser error in ' + value.__name + ': ' + err.message;
1106
+ throw err;
1107
+ }
1108
+ Util.setPath(result, pathTo.concat(prop), parsedValue);
1109
+ } else {
1110
+ _substituteVars(value, vars, pathTo.concat(prop));
1111
+ }
1112
+ } else {
1113
+ let msg = "Illegal key type for substitution map at " + pathTo.join('.') + ': ' + typeof(value);
1114
+ throw Error(msg);
1115
+ }
1116
+ }
1117
+ };
1118
+
1119
+ _substituteVars(substitutionMap, variables, []);
1120
+ return result;
1121
+ }
1122
+
1123
+ /**
1124
+ * Populate a LoadConfig entirely from environment variables.
1125
+ *
1126
+ * This is the way a base config is normally accomplished, but not for independent loads.
1127
+ *
1128
+ * This function exists in part to reduce the circular dependency of variable initializations
1129
+ * in the config.js file
1130
+ * @param environments {string} the NODE_CONFIG_ENVs you want to load
1131
+ * @private
1132
+ * @returns {Load}
1133
+ */
1134
+ static fromEnvironment(environments) {
1135
+ let env = new Env();
1136
+
1137
+ if (environments !== undefined) {
1138
+ environments = environments.split(',');
1139
+ env.setEnv('nodeEnv', environments.join(','));
1140
+ } else {
1141
+ let nodeConfigEnv = env.initParam('NODE_CONFIG_ENV');
1142
+ let nodeEnv = env.initParam('NODE_ENV');
1143
+
1144
+ if (nodeConfigEnv) {
1145
+ env.setEnv('nodeEnv', 'NODE_CONFIG_ENV');
1146
+ nodeEnv = nodeConfigEnv;
1147
+ } else if (nodeEnv) {
1148
+ env.setEnv('nodeEnv', 'NODE_ENV');
1149
+ env.setEnv('NODE_CONFIG_ENV', nodeEnv); //TODO: This is a bug asserted in the tests
1150
+ } else {
1151
+ nodeEnv = 'development';
1152
+ env.setEnv('nodeEnv', 'default');
1153
+ env.setEnv('NODE_ENV', nodeEnv);
1154
+ env.setEnv('NODE_CONFIG_ENV', nodeEnv); //TODO: This is a bug asserted in the tests
1155
+ }
1156
+
1157
+ environments = nodeEnv.split(',');
1158
+ }
1159
+
1160
+ let configDir = env.initParam('NODE_CONFIG_DIR');
1161
+ configDir = configDir && _toAbsolutePath(configDir);
1162
+
1163
+ let appInstance = env.initParam('NODE_APP_INSTANCE');
1164
+ let gitCrypt = !env.initParam('CONFIG_SKIP_GITCRYPT');
1165
+ let parser = _loadParser(env.initParam('NODE_CONFIG_PARSER'), configDir);
1166
+ let hostName = env.initParam('HOST') || env.initParam('HOSTNAME');
1167
+
1168
+ // Determine the host name from the OS module, $HOST, or $HOSTNAME
1169
+ // Remove any . appendages, and default to null if not set
1170
+ try {
1171
+ if (!hostName) {
1172
+ const OS = require('os');
1173
+ hostName = OS.hostname();
1174
+ }
1175
+ } catch (e) {
1176
+ hostName = '';
1177
+ }
1178
+
1179
+ env.setEnv('HOSTNAME', hostName);
1180
+
1181
+ /** @type {LoadOptions} */
1182
+ let options = {
1183
+ configDir: configDir ?? DEFAULT_CONFIG_DIR,
1184
+ nodeEnv: environments,
1185
+ hostName,
1186
+ parser,
1187
+ appInstance,
1188
+ gitCrypt
1189
+ };
1190
+
1191
+ return new Load(options, env);
1192
+ }
1193
+ }
1194
+
1195
+ // Helper functions shared across object members
1196
+ function _toAbsolutePath (configDir) {
1197
+ if (configDir.indexOf('.') === 0) {
1198
+ return Path.join(process.cwd(), configDir);
1199
+ }
1200
+
1201
+ return configDir;
1202
+ }
1203
+
1204
+ function _loadParser(name, dir) {
1205
+ if (name === undefined) {
1206
+ return require("../parser.js");
1207
+ }
1208
+
1209
+ try {
1210
+ const parserModule = Path.isAbsolute(name) ? name : Path.join(dir, name);
1211
+
1212
+ return require(parserModule);
1213
+ }
1214
+ catch (e) {
1215
+ console.warn(`Failed to load config parser from ${name}`);
1216
+ console.log(e);
1217
+ }
1218
+ }
1219
+
1220
+ module.exports = { Util, Load: Load };