1 |
|
|
'use strict'; |
2 |
|
|
|
3 |
|
1 |
const Hoek = require('hoek'); |
4 |
|
1 |
const Joi = require('joi'); |
5 |
|
1 |
const Async = require('async'); |
6 |
|
1 |
const _ = require('lodash'); |
7 |
|
1 |
const Promise = require('bluebird'); |
8 |
|
|
|
9 |
|
|
/** |
10 |
|
|
* Plugin Internals |
11 |
|
|
* @type {Object} |
12 |
|
|
*/ |
13 |
|
1 |
const internals = {}; |
14 |
|
|
|
15 |
|
|
// TODO: maybe options |
16 |
|
1 |
internals.Configue = module.exports = function Configue(options) { |
17 |
|
|
if (!(this instanceof Configue)) { |
18 |
|
25 |
return new Configue(options); |
19 |
|
|
} |
20 |
|
|
|
21 |
|
|
// load fresh instance of nconf |
22 |
|
30 |
delete require.cache[require.resolve('nconf')]; |
23 |
|
|
|
24 |
|
30 |
const nconf = internals.nconf = this.nconf = require('nconf'); |
25 |
|
30 |
const settings = this.settings = Hoek.cloneWithShallow(options, 'provider') || {}; |
26 |
|
30 |
this.resolved = false; |
27 |
|
|
|
28 |
|
30 |
const results = Joi.validate(settings, internals.schema); |
29 |
|
31 |
if (results.error) throw results.error; |
30 |
|
|
|
31 |
|
29 |
nconf.use('memory'); |
32 |
|
29 |
nconf.clear(); |
33 |
|
|
} |
34 |
|
|
|
35 |
|
|
// Fluent builder |
36 |
|
1 |
const Configue = internals.Configue; |
37 |
|
1 |
Configue._options = {}; |
38 |
|
1 |
['files', 'defaults', 'disable', 'env', 'argv', 'customWorkflow'].forEach(option => { |
39 |
|
3 |
Configue[option] = (opt) => { Configue._options[option] = opt; return Configue; }; |
40 |
|
|
}); |
41 |
|
1 |
['overrides', 'argv', 'env', 'files', 'defaults'].forEach(hook => { |
42 |
|
2 |
Configue[hook + 'Hook'] = (opt) => { _.set(Configue._options, `postHooks.${hook}`, opt); return Configue; }; |
43 |
|
|
}); |
44 |
|
11 |
Configue.get = () => { const c = new Configue(Configue._options); Configue._options = {} ; return c; }; |
45 |
|
|
|
46 |
|
|
|
47 |
|
1 |
internals.Configue.prototype.resolve = function (callback) { |
48 |
|
|
if (callback === undefined) { |
49 |
|
3 |
return Promise.fromCallback(callback => _resolve(this, callback)) |
50 |
|
|
} |
51 |
|
22 |
_resolve(this, callback) |
52 |
|
|
} |
53 |
|
|
|
54 |
|
1 |
const _resolve = (self, callback) => { |
55 |
|
1 |
if (self.resolved) callback() |
56 |
|
|
|
57 |
|
25 |
const markAsDone = (cb) => (err, res) => { |
58 |
|
|
self.resolved = true; |
59 |
|
25 |
cb(err, res) |
60 |
|
|
} |
61 |
|
25 |
if (self.settings.customWorkflow) |
62 |
|
2 |
self.settings.customWorkflow(self.nconf, markAsDone(callback)); |
63 |
|
23 |
else internals.applyDefaultWorkflow(self.nconf, self.settings, markAsDone(callback)); |
64 |
|
|
} |
65 |
|
|
|
66 |
|
1 |
internals.Configue.prototype.get = function get(key, defaultValue) { |
67 |
|
|
const result = this.nconf.get(key); |
68 |
|
27 |
return result === undefined ? defaultValue : result; |
69 |
|
|
} |
70 |
|
|
|
71 |
|
|
/** |
72 |
|
|
* Register the <tt>Configue</tt> plugin and process the various steps and hooks |
73 |
|
|
* @param server - Hapi server to configure |
74 |
|
|
* @param options - options of the Configue Plugin |
75 |
|
|
* @param next - plugin continuation |
76 |
|
|
*/ |
77 |
|
1 |
const hapiPlugin = internals.Configue.prototype.plugin = function () { |
78 |
|
|
const configue = this; |
79 |
|
4 |
const plugin = function plugin(server, options, next) { |
80 |
|
|
const configure = (err) => { |
81 |
|
1 |
if(err) return next(err); |
82 |
|
3 |
server.log(['plugin', 'info'], "Registering the configue has decoration"); |
83 |
|
3 |
const configGetter = configue.get.bind(configue); |
84 |
|
3 |
server.decorate('server', 'configue', configGetter); |
85 |
|
3 |
server.decorate('request', 'configue', configGetter); |
86 |
|
3 |
next() |
87 |
|
|
} |
88 |
|
6 |
if(configue.resolved) configure() |
89 |
|
2 |
else configue.resolve(configure) |
90 |
|
|
|
91 |
|
|
|
92 |
|
|
} |
93 |
|
4 |
plugin.attributes = hapiPlugin.attributes; |
94 |
|
4 |
return plugin; |
95 |
|
|
}; |
96 |
|
|
|
97 |
|
1 |
hapiPlugin.attributes = { |
98 |
|
|
pkg: require('../package.json') |
99 |
|
|
}; |
100 |
|
|
|
101 |
|
|
|
102 |
|
|
/** |
103 |
|
|
* Joi options schema |
104 |
|
|
*/ |
105 |
|
1 |
internals.schema = [ |
106 |
|
|
Joi.object({customWorkflow: Joi.func()}), |
107 |
|
|
Joi.object().keys({ |
108 |
|
|
disable: Joi.object({ |
109 |
|
|
argv: Joi.boolean(), |
110 |
|
|
env: Joi.boolean() |
111 |
|
|
}), |
112 |
|
|
argv: [Joi.object()], |
113 |
|
|
env: [Joi.object(), Joi.array().items(Joi.string()), Joi.string()], |
114 |
|
|
|
115 |
|
|
files: [Joi.string(), |
116 |
|
|
Joi.array().items(Joi.object({ |
117 |
|
|
file: Joi.string().required(), |
118 |
|
|
format: Joi.object({stringify: Joi.func(), parse: Joi.func()}) |
119 |
|
|
})), |
120 |
|
|
Joi.array().items(Joi.string())], |
121 |
|
|
defaults: [Joi.object(), |
122 |
|
|
Joi.array().items(Joi.object())], |
123 |
|
|
postHooks: Joi.object({ |
124 |
|
|
overrides: Joi.func(), |
125 |
|
|
argv: Joi.func(), |
126 |
|
|
env: Joi.func(), |
127 |
|
|
files: Joi.func(), |
128 |
|
|
defaults: Joi.func() |
129 |
|
|
}) |
130 |
|
|
})]; |
131 |
|
|
|
132 |
|
|
/** |
133 |
|
|
* Apply the Default Configuration Workflow |
134 |
|
|
* @param nconf - the nconf object |
135 |
|
|
* @param settings - plugin config |
136 |
|
|
* @param next - callback |
137 |
|
|
*/ |
138 |
|
1 |
internals.applyDefaultWorkflow = function applyDefaultWorkflow(nconf, settings, next) { |
139 |
|
|
const hooks = settings.postHooks; |
140 |
|
|
|
141 |
|
|
// Load eventual overrides values and then iterates over the different steps (in order: argv, env, files, default) |
142 |
|
23 |
return Async.series([ |
143 |
|
|
this.processHook(hooks, 'overrides'), |
144 |
|
|
this.iterateSteps(this.steps, settings) |
145 |
|
|
], next); |
146 |
|
|
}; |
147 |
|
|
|
148 |
|
|
/** |
149 |
|
|
* Iterate asynchronously over the various steps |
150 |
|
|
* @param steps - list of configuration steps |
151 |
|
|
* @param settings - project settings |
152 |
|
|
* @returns {Function} |
153 |
|
|
*/ |
154 |
|
1 |
internals.iterateSteps = function iterateSteps(steps, settings) { |
155 |
|
|
const hooks = settings.postHooks; |
156 |
|
23 |
return (next) => { |
157 |
|
|
return Async.eachOfSeries(steps, (stepName, key, done) => { |
158 |
|
|
this.stepActions[stepName](settings); |
159 |
|
89 |
if (hooks && hooks[stepName]) |
160 |
|
3 |
this.executePostHook(hooks[stepName], done); |
161 |
|
86 |
else done(); |
162 |
|
|
}, next) |
163 |
|
|
}; |
164 |
|
|
}; |
165 |
|
|
|
166 |
|
|
/** |
167 |
|
|
* Ordered list of configuration steps |
168 |
|
|
* @type {string[]} |
169 |
|
|
*/ |
170 |
|
1 |
internals.steps = ['argv', 'env', 'files', 'defaults']; |
171 |
|
|
// Definition of associated actions below |
172 |
|
|
|
173 |
|
|
/** |
174 |
|
|
* Closure that takes the name of a nconf resource as a parameters and |
175 |
|
|
* loads it if it is not disabled in the plugin options |
176 |
|
|
* |
177 |
|
|
* @param resource - the ressource to be loaded. (argv, env, files...) |
178 |
|
|
* @returns {Function} |
179 |
|
|
*/ |
180 |
|
1 |
internals.load = function load(resource) { |
181 |
|
|
return function onResource(options) { |
182 |
|
|
if (!options.disable || ! options.disable[resource]) { |
183 |
|
44 |
return internals.nconf[resource](options[resource]); |
184 |
|
|
} |
185 |
|
|
} |
186 |
|
|
}; |
187 |
|
|
|
188 |
|
|
/** |
189 |
|
|
* Process the hook for a given step |
190 |
|
|
* @param hooks |
191 |
|
|
* @param stepName |
192 |
|
|
* @returns {Function} |
193 |
|
|
*/ |
194 |
|
1 |
internals.processHook = function processHook(hooks, stepName) { |
195 |
|
|
return (done) => { |
196 |
|
|
if (hooks && hooks[stepName]) |
197 |
|
1 |
this.executePostHook(hooks[stepName], done); |
198 |
|
22 |
else done(); |
199 |
|
|
}; |
200 |
|
|
}; |
201 |
|
|
|
202 |
|
|
/** |
203 |
|
|
* Execute a hook |
204 |
|
|
* @param hook - a post step hook |
205 |
|
|
* @param done - callback |
206 |
|
|
*/ |
207 |
|
1 |
internals.executePostHook = function executeHook(hook, done) { |
208 |
|
|
return hook(internals.nconf, done); |
209 |
|
|
}; |
210 |
|
|
|
211 |
|
|
/** |
212 |
|
|
* Load the files in options using <tt>nconf.file</tt> |
213 |
|
|
* @param options - plugin options |
214 |
|
|
*/ |
215 |
|
1 |
internals.loadFiles = function loadFiles(options) { |
216 |
|
|
const files = options.files; |
217 |
|
22 |
if (Array.isArray(files) && files.length) { |
218 |
|
4 |
files.forEach((file) => internals.nconf.file((typeof files[0] === 'string') ? file : file.file, file)); |
219 |
|
|
// file(.file) is used as namespace for nconf |
220 |
|
18 |
} else if (typeof files === 'string' && files.length) |
221 |
|
1 |
internals.nconf.file(files); |
222 |
|
|
}; |
223 |
|
|
|
224 |
|
|
/** |
225 |
|
|
* Load the defaults in options using <tt>nconf.defaults</tt> |
226 |
|
|
* @param options - plugin options |
227 |
|
|
*/ |
228 |
|
1 |
internals.loadDefaults = function loadDefaults(options) { |
229 |
|
|
const defaults = options.defaults; |
230 |
|
22 |
internals.nconf.defaults(Array.isArray(defaults) ? _.defaults.apply({}, defaults) : defaults); |
231 |
|
|
}; |
232 |
|
|
|
233 |
|
|
/** |
234 |
|
|
* Steps and their associated action function |
235 |
|
|
* @type {{argv: Function, env: Function, files: loadFiles}} |
236 |
|
|
*/ |
237 |
|
1 |
internals.stepActions = { |
238 |
|
|
argv: internals.load('argv'), |
239 |
|
|
env: internals.load('env'), |
240 |
|
|
files: internals.loadFiles, |
241 |
|
|
defaults: internals.loadDefaults |
242 |
|
|
}; |