@pryv/boiler 1.0.8

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/src/config.js ADDED
@@ -0,0 +1,441 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/pryv-boiler/blob/master/LICENSE)
4
+ */
5
+
6
+ /**
7
+ * Load configuration in the following order (1st prevails)
8
+ *
9
+ * .0 'memory' -> empty, use when doing 'config.set()'
10
+ * .1 'override-file' -> Loaded at from override-config.yml (if present)
11
+ * .2 'test' -> empty, used by test to override any other config parameter
12
+ * .3 'argv' -> Loaded from arguments
13
+ * .4 'env' -> Loaded from environement variables
14
+ * .5 'base' -> Loaded from ${process.env.NODE_ENV}-config.yml (if present) or --config parameter
15
+ * .6 and next -> Loaded from extras
16
+ * .end
17
+ * . 'default-file' -> Loaded from ${baseDir}/default-config.yml
18
+ * . 'defaults' -> Hard coded defaults for logger
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const nconf = require('nconf');
25
+ nconf.formats.yaml = require('./lib/nconf-yaml');
26
+
27
+ const superagent = require('superagent');
28
+
29
+ /**
30
+ * Default values for Logger
31
+ */
32
+ const defaults = {
33
+ logs: {
34
+ console: {
35
+ active: true,
36
+ level: 'info',
37
+ format: {
38
+ color: true,
39
+ time: true,
40
+ aligned: true
41
+ }
42
+ },
43
+ file: {
44
+ active: true,
45
+ path: 'application.log',
46
+ rotation: {
47
+ isActive: false
48
+ }
49
+ },
50
+ airbrake: {
51
+ active: false
52
+ }
53
+ }
54
+ };
55
+
56
+
57
+
58
+ /**
59
+ * Config manager
60
+ */
61
+ class Config {
62
+ store;
63
+ logger;
64
+ extraAsync;
65
+ baseConfigDir;
66
+ learnDirectoryAndFilename;
67
+
68
+ constructor() {
69
+ this.extraAsync = [];
70
+ }
71
+
72
+ /**
73
+ * @private
74
+ * Init Config with Files should be called just once when starting an APP
75
+ * @param {Object} options
76
+ * @param {string} appName
77
+ * @param {string} [learnDirectory] - (optional) if set, all .get() calls will be tracked in this files in this directory
78
+ * @param {string} [options.baseConfigDir] - (optional) directory to use to look for configs (default, env)
79
+ * @param {Array<ConfigFile|ConfigPlugin|ConfigData|ConfigRemoteURL|ConfigRemoteURLFromKey>} [options.extras] - (optional) and array of extra files or plugins to load (synchronously or async)
80
+ * @param {Object} logging
81
+ * @returns {Config} this
82
+ */
83
+ initSync(options, logging) {
84
+ this.appName = options.appName;
85
+ this.learnDirectoryAndFilename = getLearnFilename(options.appName, options.learnDirectory);
86
+
87
+ const logger = this.logger = logging.getLogger('config');
88
+ const store = this.store = new nconf.Provider();
89
+
90
+ const baseConfigDir = this.baseConfigDir = options.baseConfigDir || process.cwd();
91
+ logger.debug('Init with baseConfigDir: ' + baseConfigDir);
92
+
93
+ // 0. memory at top
94
+ store.use('memory');
95
+
96
+ // 1. eventual ovverride-config.yml
97
+ loadFile('override-file', path.resolve(baseConfigDir, 'override-config.yml'));
98
+
99
+ // 2. put a 'test' store up in the list that could be overwitten afterward and override other options
100
+ // override 'test' store with store.add('test', {type: 'literal', store: {....}});
101
+ store.use('test', { type: 'literal', store: {} });
102
+
103
+ // get config from arguments and env variables
104
+ // memory must come first for config.set() to work without loading config files
105
+ // 3. `process.env`
106
+ // 4. `process.argv`
107
+ store.argv({parseValues: true}).env({parseValues: true, separator: '__'});
108
+
109
+ // 5. Values in `${NODE_ENV}-config.yml` or from --config parameter
110
+ let configFile;
111
+ if (store.get('config')) {
112
+ configFile = store.get('config')
113
+ } else if (store.get('NODE_ENV')) {
114
+ configFile = path.resolve(baseConfigDir, store.get('NODE_ENV') + '-config.yml');
115
+ }
116
+ if (configFile) {
117
+ loadFile('base', configFile);
118
+ } else {
119
+ // book 'base' slot
120
+ store.use('base', { type: 'literal', store: {} });
121
+ logger.debug('Booked [base] empty as no --config or NODE_ENV was set');
122
+ }
123
+
124
+ // load extra config files & plugins
125
+ if (options.extras) {
126
+ for (let extra of options.extras) {
127
+ if (extra.file) {
128
+ loadFile(extra.scope, extra.file);
129
+ continue;
130
+ }
131
+ if (extra.plugin) {
132
+ const name = extra.plugin.load(this);
133
+ logger.debug('Loaded plugin: ' + name + ' ' + extra.plugin.load.then);
134
+ continue;
135
+ }
136
+ if (extra.data) {
137
+ const conf = extra.key ? {[extra.key]: extra.data} : extra.data;
138
+ store.use(extra.scope, { type: 'literal', store: conf });
139
+ logger.debug('Loaded [' + extra.scope + '] from DATA: ' + (extra.key ? ' under [' + extra.key + ']' : ''));
140
+ continue;
141
+ }
142
+ if (extra.url || extra.urlFromKey || extra.fileAsync) {
143
+ // register scope in the chain to keep order of configs
144
+ store.use(extra.scope, { type: 'literal', store: {} });
145
+ logger.debug('Booked [' + extra.scope +'] for async Loading ');
146
+ this.extraAsync.push(extra);
147
+ continue;
148
+ }
149
+ if (extra.pluginAsync) {
150
+ logger.debug('Added 1 plugin for async Loading ');
151
+ this.extraAsync.push(extra);
152
+ continue;
153
+ }
154
+ logger.warn('Unkown extra in config init', extra);
155
+ }
156
+ }
157
+
158
+
159
+ // .end-1 load default and custom config from configs/default-config.json
160
+ loadFile('default-file', path.resolve(baseConfigDir, 'default-config.yml'));
161
+
162
+ // .end load hard coded defaults
163
+ store.defaults(defaults);
164
+
165
+ // init Logger
166
+ logging.initLoggerWithConfig(this);
167
+ return this;
168
+
169
+ // --- helpers --/
170
+
171
+ function loadFile(scope, filePath) {
172
+
173
+ if (fs.existsSync(filePath)) {
174
+
175
+ if (filePath.endsWith('.js')) { // JS file
176
+ const conf = require(filePath);
177
+ store.use(scope, { type: 'literal', store: conf });
178
+ } else {  // JSON or YAML
179
+ const options = { file: filePath }
180
+ if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) { options.format = nconf.formats.yaml }
181
+ store.file(scope, options);
182
+ }
183
+
184
+ logger.debug('Loaded [' + scope + '] from file: ' + filePath)
185
+ } else {
186
+ logger.debug('Cannot find file: ' + filePath + ' for scope [' + scope + ']');
187
+ }
188
+ }
189
+ }
190
+
191
+ async initASync() {
192
+ const store = this.store;
193
+ const logger = this.logger;
194
+ const baseConfigDir = this.baseConfigDir;
195
+
196
+ async function loadUrl(scope, key, url) {
197
+ if (typeof url === 'undefined' || url === null) {
198
+ logger.warn('Null or Undefined Url for [' + scope +']');
199
+ return;
200
+ }
201
+
202
+ let res = null;
203
+ if (isFileUrl(url)) {
204
+ res = loadFromFile(url);
205
+ } else {
206
+ res = await loadFromUrl(url);
207
+ }
208
+ const conf = key ? {[key]: res} : res;
209
+ store.add(scope, { type: 'literal', store: conf });
210
+ logger.debug('Loaded [' + scope + '] from URL: ' + url + (key ? ' under [' + key + ']' : ''));
211
+ }
212
+
213
+ // load remote config files
214
+ for (let extra of this.extraAsync) {
215
+ if (extra.url) {
216
+ await loadUrl(extra.scope, extra.key, extra.url);
217
+ continue;
218
+ }
219
+ if (extra.urlFromKey) {
220
+ const url = store.get(extra.urlFromKey);
221
+ await loadUrl(extra.scope, extra.key, url);
222
+ continue;
223
+ }
224
+
225
+ if (extra.pluginAsync) {
226
+ const name = await extra.pluginAsync.load(this);
227
+ logger.debug('Loaded async plugin: ' + name);
228
+ continue;
229
+ }
230
+
231
+ if (extra.fileAsync) {
232
+ const filePath = path.resolve(baseConfigDir, extra.fileAsync);
233
+
234
+ if (! fs.existsSync(filePath)) {
235
+ logger.warn('Cannot find file: ' + filePath + ' for scope [' + extra.scope + ']');
236
+ continue;
237
+ }
238
+ if (! filePath.endsWith('.js')) {
239
+ logger.warn('Cannot only load .js file: ' + filePath + ' for scope [' + extra.scope + ']');
240
+ continue;
241
+ }
242
+
243
+ const conf = await require(filePath)();
244
+ store.add(extra.scope, { type: 'literal', store: conf });
245
+
246
+ logger.debug('Loaded in scope [' + extra.scope + ']async .js file: ' + filePath);
247
+ }
248
+ }
249
+
250
+ logger.debug('Config fully Loaded');
251
+ saveConfig(this.learnDirectoryAndFilename, this.store);
252
+ return this;
253
+ }
254
+
255
+ /**
256
+ * Return true if key as value
257
+ * @param {string} key
258
+ * @returns {boolean}
259
+ */
260
+ has(key) {
261
+ if (! this.store) { throw(new Error('Config not yet initialized'))}
262
+ const value = this.store.get(key);
263
+ return (typeof value !== 'undefined');
264
+ }
265
+
266
+ /**
267
+ * Retreive value
268
+ * @param {string} key
269
+ */
270
+ get(key) {
271
+ if (! this.store) { throw(new Error('Config not yet initialized'))}
272
+ learn(this.learnDirectoryAndFilename, key);
273
+ const value = this.store.get(key);
274
+ if (typeof value === 'undefined') this.logger.debug('get: [' + key +'] is undefined');
275
+ return value;
276
+ }
277
+
278
+ /**
279
+ * Retreive value and store info that applies
280
+ * @param {string} key
281
+ */
282
+ getScopeAndValue(key) {
283
+ if (! this.store) { throw(new Error('Config not yet initialized'))};
284
+ for (let scopeName of Object.keys(this.store.stores)) {
285
+ const store = this.store.stores[scopeName];
286
+ const value = store.get(key);
287
+ if (typeof value !== 'undefined') {
288
+ const res = {
289
+ value: value,
290
+ scope: scopeName
291
+ }
292
+ if (store.type === 'file') {
293
+ res.info = 'From file: ' + store.file
294
+ } else {
295
+ info = 'Type: ' + store.type
296
+ }
297
+ return res;
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+
303
+ /**
304
+ * Set value
305
+ * @param {string} key
306
+ * @param {Object} value
307
+ */
308
+ set(key, value) {
309
+ if (! this.store) { throw(new Error('Config not yet initialized'))}
310
+ this.store.set(key, value);
311
+ }
312
+
313
+ /**
314
+ * Inject Test Config and override any other option
315
+ * @param {Object} configObject;
316
+ */
317
+ injectTestConfig(configObject) {
318
+ this.replaceScopeConfig('test', configObject);
319
+ }
320
+
321
+ /**
322
+ * Replace a scope config set
323
+ * @param {string} scope;
324
+ * @param {Object} configObject;
325
+ */
326
+ replaceScopeConfig(scope, configObject) {
327
+ if (! this.store) { throw(new Error('Config not yet initialized'))}
328
+ this.logger.debug('Replace ['+ scope + '] with: ', configObject);
329
+ this.store.add(scope, {type: 'literal', store: configObject});
330
+ }
331
+
332
+ }
333
+
334
+ module.exports = Config;
335
+
336
+ // --- remote and local json ressource loader ---- //
337
+
338
+ const FILE_PROTOCOL = 'file://';
339
+ const FILE_PROTOCOL_LENGTH = FILE_PROTOCOL.length;
340
+
341
+ async function loadFromUrl(serviceInfoUrl ) {
342
+ const res = await superagent.get(serviceInfoUrl);
343
+ return res.body;
344
+ }
345
+
346
+ function loadFromFile(fileUrl ) {
347
+ const filePath = stripFileProtocol(fileUrl);
348
+
349
+ if (isRelativePath(filePath)) {
350
+ const serviceCorePath = path.resolve(__dirname, '../../../../../'); // assuming /node_modules/@pryv/boiler/src/
351
+ fileUrl = path.resolve(serviceCorePath, filePath);
352
+ fileUrl = 'file://' + fileUrl;
353
+ } else {
354
+ // absolute path, do nothing.
355
+ }
356
+ const res = JSON.parse(
357
+ fs.readFileSync(stripFileProtocol(fileUrl), 'utf8')
358
+ );
359
+ return res;
360
+ }
361
+
362
+
363
+ function isFileUrl(filePath) {
364
+ return filePath.startsWith(FILE_PROTOCOL);
365
+ }
366
+
367
+ function isRelativePath(filePath) {
368
+ return !path.isAbsolute(filePath);
369
+ }
370
+
371
+ function stripFileProtocol(filePath) {
372
+ return filePath.substring(FILE_PROTOCOL_LENGTH);
373
+ }
374
+
375
+
376
+ // -------- learning mode ------- //
377
+
378
+ function getLearnFilename(appName, learnDirectory) {
379
+ if (! learnDirectory) return;
380
+ let i = 0;
381
+ let res;
382
+ do {
383
+ res = path.join(learnDirectory, appName + i );
384
+ i++;
385
+ } while(fs.existsSync(res + '-config.json'));
386
+ return res;
387
+ }
388
+
389
+ function learn(learnDirectoryAndFilename, key) {
390
+ if (learnDirectoryAndFilename) {
391
+ const caller_line = (new Error()).stack.split('\n')[3]; // get callee name and line
392
+ const index = caller_line.indexOf("at ");
393
+ str = key + ';' + caller_line.slice(index+3, caller_line.length) + '\n';
394
+ fs.appendFileSync(learnDirectoryAndFilename + '-calls.csv', str);
395
+ }
396
+ }
397
+
398
+ function saveConfig(learnDirectoryAndFilename, store) {
399
+ if (learnDirectoryAndFilename) {
400
+ let i = 0;
401
+ let filename;
402
+ do {
403
+ filename =learnDirectoryAndFilename + '-config.json';
404
+ i++;
405
+ } while(fs.existsSync(filename));
406
+ fs.writeFileSync(filename, JSON.stringify({stores: store.stores, config: store.get()}, null, 2));
407
+ }
408
+ }
409
+
410
+
411
+ /**
412
+ * @typedef ConfigFile
413
+ * @property {string} scope - scope for nconf hierachical load
414
+ * @property {string} file - the config file (.yml, .json, .js)
415
+ */
416
+
417
+ /**
418
+ * @typedef ConfigPlugin
419
+ * @property {Object} plugin
420
+ * @property {Function} plugin.load - a function that takes the "nconf store" as argument and returns the "name" of the plugin
421
+ */
422
+
423
+ /**
424
+ * @typedef ConfigData
425
+ * @property {string} scope - scope for nconf hierachical load
426
+ * @property {string} [key] - (optional) key to load result of url. If null loaded at root of the config
427
+ * @property {object} data - the data to load
428
+
429
+
430
+ /**
431
+ * @typedef ConfigRemoteURL
432
+ * @property {string} scope - scope for nconf hierachical load
433
+ * @property {string} [key] - (optional) key to load result of url. If null loaded at root of the config
434
+ * @property {string} url - the url to the config definition
435
+ */
436
+ /**
437
+ * @typedef ConfigRemoteURLFromKey
438
+ * @property {string} scope - scope for nconf hierachical load
439
+ * @property {string} [key] - (optional) key to load result of url. If null override
440
+ * @property {string} urlFromKey - retrieve url from config matching this key
441
+ */
package/src/index.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/pryv-boiler/blob/master/LICENSE)
4
+ */
5
+
6
+ /**
7
+ * Pryv Boiler module.
8
+ * @module boiler
9
+ */
10
+
11
+ const Config = require('./config');
12
+ const logging = require('./logging');
13
+ const airbrake = require('./airbrake');
14
+
15
+ const config = new Config();
16
+
17
+ const boiler = {
18
+ /**
19
+ * notify Airbrake.
20
+ * If initalize, arguments will be passed to airbrake.notify()
21
+ */
22
+ notifyAirbrake: airbrake.notifyAirbrake,
23
+
24
+ /**
25
+ * get a Logger
26
+ * @param {string} name
27
+ * @returns {Logger}
28
+ */
29
+ getLogger: logging.getLogger,
30
+ /**
31
+ * Prefered way to get the configuration
32
+ * @returns {Promise}
33
+ */
34
+ getConfig: getConfig,
35
+ /**
36
+ * get the configuration.
37
+ * If the configuration is not fully initialized throw an error
38
+ * @param {boolean} warnOnly - Only warns about potential misuse of config
39
+ * @returns {Config}
40
+ */
41
+ getConfigUnsafe: getConfigUnsafe,
42
+
43
+ /**
44
+ * Init Boiler, should be called just once when starting an APP
45
+ * @param {Object} options
46
+ * @param {string} options.appName - the name of the Application used by Logger and debug
47
+ * @param {string} [options.baseConfigDir] - (optional) directory to use to look for configs
48
+ * @param {Array<ConfigFile|ConfigRemoteURL|ConfigRemoteURLFromKey|ConfigPlugin>} [options.extraConfigs] - (optional) and array of extra files to load
49
+ * @param {Function} [fullyLoadedCallback] - (optional) called when the config is fully loaded
50
+ */
51
+ init: init
52
+ };
53
+
54
+ let logger;
55
+ let configInitialized = false;
56
+ let configInitCalledWithName = null;
57
+
58
+ function init (options, fullyLoadedCallback) {
59
+ if (configInitCalledWithName) {
60
+ logger.warn('Skipping initalization! boiler is already initialized with appName: ' + configInitCalledWithName);
61
+ return boiler;
62
+ }
63
+
64
+ // append the value of process.env.PRYV_BOILER_SUFFIX if present
65
+ options.appNameWithoutPostfix = options.appName;
66
+ if (process.env.PRYV_BOILER_SUFFIX) options.appName += process.env.PRYV_BOILER_SUFFIX;
67
+
68
+ logging.setGlobalName(options.appName);
69
+ configInitCalledWithName = options.appName;
70
+ config.initSync({
71
+ baseConfigDir: options.baseConfigDir,
72
+ extras: options.extraConfigs,
73
+ appName: options.appNameWithoutPostfix,
74
+ learnDirectory: process.env.CONFIG_LEARN_DIR
75
+ }, logging);
76
+
77
+ logger = logging.getLogger('boiler');
78
+ airbrake.setUpAirbrakeIfNeeded(config, logger);
79
+
80
+ config.initASync().then((config) => {
81
+ configInitialized = true;
82
+ // airbrake config might come from async settings, so we try twice.
83
+ airbrake.setUpAirbrakeIfNeeded(config, logger);
84
+ if (fullyLoadedCallback) fullyLoadedCallback(config);
85
+ });
86
+
87
+ return boiler;
88
+ }
89
+
90
+ async function getConfig () {
91
+ if (!configInitCalledWithName) {
92
+ throw (new Error('boiler must be initialized with init() before using getConfig()'));
93
+ }
94
+ /* eslint-disable-next-line no-unmodified-loop-condition */
95
+ while (!configInitialized) {
96
+ await new Promise(resolve => setTimeout(resolve, 100)); // wait 100ms
97
+ }
98
+ return config;
99
+ }
100
+
101
+ function getConfigUnsafe (warnOnly) {
102
+ if (!configInitCalledWithName) {
103
+ throw (new Error('boiler must be initialized with init() before using getConfigUnsafe()'));
104
+ }
105
+ if (!configInitialized) {
106
+ if (warnOnly) {
107
+ logger.warn('Warning! config loaded before being fully initialized');
108
+ } else {
109
+ throw (new Error('Config loaded before being fully initialized'));
110
+ }
111
+ }
112
+ return config;
113
+ }
114
+
115
+ module.exports = boiler;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/pryv-boiler/blob/master/LICENSE)
4
+ */
5
+ const yaml = require('js-yaml');
6
+
7
+ exports.stringify = function (obj, options) {
8
+ return yaml.dump(obj, options);
9
+ };
10
+
11
+ exports.parse = function (obj, options) {
12
+ return yaml.load(obj, options);
13
+ };