1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 | const _ = require('lodash');
|
8 | const __ = require('@digitalspaces/utils');
|
9 | const Path = require('path');
|
10 | const Fs = require('fs-extra');
|
11 |
|
12 | const validConfigKeys = {
|
13 | 'authToken': true,
|
14 | 'baseUrl': true,
|
15 | 'configPath': true,
|
16 | 'debugRuntimeToken': true,
|
17 | 'userId': true
|
18 | };
|
19 |
|
20 | function ifObjectProxy(value) {
|
21 |
|
22 | switch( __.type(value) ) {
|
23 | case 'undefined':
|
24 | case 'null':
|
25 | case 'boolean':
|
26 | case 'number':
|
27 | case 'string':
|
28 | return value;
|
29 | default:
|
30 | return readOnlyProxy(value);
|
31 | }
|
32 |
|
33 | }
|
34 |
|
35 | function readOnlyProxy(target, handlers) {
|
36 |
|
37 | return new Proxy(target, Object.assign({
|
38 | apply(target, thisArg, args) {
|
39 | Reflect.apply(target, thisArg, args);
|
40 | },
|
41 | get(target, prop, receiver) {
|
42 | return ifObjectProxy(Reflect.get(target, prop, receiver));
|
43 | },
|
44 | getOwnPropertyDescriptor(target, prop) {
|
45 | const result = Reflect.getOwnPropertyDescriptor(target, prop);
|
46 | if( result === undefined )
|
47 | return undefined;
|
48 | result.value = ifObjectProxy(result.value)
|
49 | },
|
50 | defineProperty() {
|
51 | return false;
|
52 | },
|
53 | deleteProperty() {
|
54 | return false;
|
55 | },
|
56 | set(target, prop, value, receiver) {
|
57 | if( receiver !== this )
|
58 | return Reflect.set(target, prop, value, receiver);
|
59 | return false;
|
60 | },
|
61 | setPrototypeOf() {
|
62 | return false;
|
63 | },
|
64 | construct() {
|
65 | throw __.error();
|
66 | }
|
67 | }, handlers));
|
68 |
|
69 | }
|
70 |
|
71 | class Config {
|
72 |
|
73 | constructor(configOverride) {
|
74 |
|
75 |
|
76 | let defaultConfigPath;
|
77 |
|
78 | this._configDefaults = Object.create(null, {
|
79 | baseUrl: {
|
80 | value: 'https://my.digitalspaces.app',
|
81 | writable: true,
|
82 | configurable: true,
|
83 | enumerable: true
|
84 | },
|
85 | configPath: {
|
86 | get: () => {
|
87 | if( defaultConfigPath !== undefined )
|
88 | return defaultConfigPath;
|
89 | defaultConfigPath = Path.resolve(process.cwd(), '.digitalspaces', 'config.json');
|
90 | if( Fs.pathExistsSync(defaultConfigPath) )
|
91 | return defaultConfigPath;
|
92 | return defaultConfigPath = Path.resolve(require('os').homedir(), '.digitalspaces', 'config.json');
|
93 | },
|
94 | configurable: true,
|
95 | enumerable: true
|
96 | }
|
97 | });
|
98 |
|
99 | const self = this;
|
100 |
|
101 | this._configPersist = Object.create(readOnlyProxy(this._configDefaults, {
|
102 | ownKeys(target, receiver) {
|
103 | const keysObj = {};
|
104 | for( let keys = Reflect.ownKeys(self._configDefaults), i = keys.length; i--; )
|
105 | keysObj[keys[i]] = true;
|
106 | for( let keys = Reflect.ownKeys(self._configPersist), i = keys.length; i--; )
|
107 | keysObj[keys[i]] = true;
|
108 | return Reflect.ownKeys(keysObj);
|
109 | },
|
110 | getOwnPropertyDescriptor(target, prop) {
|
111 | const result = Reflect.getOwnPropertyDescriptor(self._configPersist, prop)
|
112 | || Reflect.getOwnPropertyDescriptor(self._configDefaults, prop);
|
113 | if( result !== undefined && result.get === undefined )
|
114 | result.value = ifObjectProxy(result.value);
|
115 | return result;
|
116 | }
|
117 | }));
|
118 |
|
119 | this._configEnv = Object.create(readOnlyProxy(this._configPersist, {
|
120 | ownKeys(target, receiver) {
|
121 | const keysObj = {};
|
122 | for( let keys = Reflect.ownKeys(self._configDefaults), i = keys.length; i--; )
|
123 | keysObj[keys[i]] = true;
|
124 | for( let keys = Reflect.ownKeys(self._configPersist), i = keys.length; i--; )
|
125 | keysObj[keys[i]] = true;
|
126 | for( let keys = Reflect.ownKeys(self._configEnv), i = keys.length; i--; )
|
127 | keysObj[keys[i]] = true;
|
128 | return Reflect.ownKeys(keysObj);
|
129 | },
|
130 | getOwnPropertyDescriptor(target, prop) {
|
131 | const result = Reflect.getOwnPropertyDescriptor(self._configEnv, prop)
|
132 | || Reflect.getOwnPropertyDescriptor(self._configPersist, prop)
|
133 | || Reflect.getOwnPropertyDescriptor(self._configDefaults, prop);
|
134 | if( result !== undefined && result.get === undefined )
|
135 | result.value = ifObjectProxy(result.value);
|
136 | return result;
|
137 | }
|
138 | }));
|
139 |
|
140 | this._configOverride = Object.create(readOnlyProxy(this._configEnv, {
|
141 | ownKeys(target, receiver) {
|
142 | const keysObj = {};
|
143 | for( let keys = Reflect.ownKeys(self._configDefaults), i = keys.length; i--; )
|
144 | keysObj[keys[i]] = true;
|
145 | for( let keys = Reflect.ownKeys(self._configPersist), i = keys.length; i--; )
|
146 | keysObj[keys[i]] = true;
|
147 | for( let keys = Reflect.ownKeys(self._configEnv), i = keys.length; i--; )
|
148 | keysObj[keys[i]] = true;
|
149 | for( let keys = Reflect.ownKeys(self._configOverride), i = keys.length; i--; )
|
150 | keysObj[keys[i]] = true;
|
151 | return Reflect.ownKeys(keysObj);
|
152 | },
|
153 | getOwnPropertyDescriptor(target, prop) {
|
154 | const result = Reflect.getOwnPropertyDescriptor(self._configOverride, prop)
|
155 | || Reflect.getOwnPropertyDescriptor(self._configEnv, prop)
|
156 | || Reflect.getOwnPropertyDescriptor(self._configPersist, prop)
|
157 | || Reflect.getOwnPropertyDescriptor(self._configDefaults, prop);
|
158 | if( result !== undefined && result.get === undefined )
|
159 | result.value = ifObjectProxy(result.value);
|
160 | return result;
|
161 | }
|
162 | }));
|
163 |
|
164 | this._config = readOnlyProxy(this._configOverride, {
|
165 | ownKeys(target, receiver) {
|
166 | const keysObj = {};
|
167 | for( let keys = Reflect.ownKeys(self._configDefaults), i = keys.length; i--; )
|
168 | keysObj[keys[i]] = true;
|
169 | for( let keys = Reflect.ownKeys(self._configPersist), i = keys.length; i--; )
|
170 | keysObj[keys[i]] = true;
|
171 | for( let keys = Reflect.ownKeys(self._configEnv), i = keys.length; i--; )
|
172 | keysObj[keys[i]] = true;
|
173 | for( let keys = Reflect.ownKeys(self._configOverride), i = keys.length; i--; )
|
174 | keysObj[keys[i]] = true;
|
175 | return Reflect.ownKeys(keysObj);
|
176 | },
|
177 | getOwnPropertyDescriptor(target, prop) {
|
178 | const result = Reflect.getOwnPropertyDescriptor(self._configOverride, prop)
|
179 | || Reflect.getOwnPropertyDescriptor(self._configEnv, prop)
|
180 | || Reflect.getOwnPropertyDescriptor(self._configPersist, prop)
|
181 | || Reflect.getOwnPropertyDescriptor(self._configDefaults, prop);
|
182 | if( result !== undefined && result.get === undefined )
|
183 | result.value = ifObjectProxy(result.value);
|
184 | return result;
|
185 | }
|
186 | });
|
187 |
|
188 | this._refreshed = undefined;
|
189 |
|
190 | if( configOverride !== undefined )
|
191 | this.override(configOverride)
|
192 |
|
193 | }
|
194 |
|
195 | async get(key, defaultValue) {
|
196 |
|
197 | await this.refresh();
|
198 |
|
199 | if( key !== undefined ) {
|
200 |
|
201 | if( __.type(key) !== 'string' || key === '' )
|
202 | throw __.error('expect (key) to be a non-empty string if provided.', {key});
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | const result = Reflect.get(this._config, key);
|
208 |
|
209 | if( result === undefined )
|
210 | return defaultValue;
|
211 |
|
212 | return result;
|
213 |
|
214 | }
|
215 | else {
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | return this._config;
|
221 |
|
222 | }
|
223 |
|
224 | }
|
225 |
|
226 | async set(configDetails) {
|
227 |
|
228 | if( __.type(configDetails) !== 'object' || _.isEmpty(configDetails) )
|
229 | throw __.error();
|
230 |
|
231 |
|
232 |
|
233 | await this.override(configDetails);
|
234 |
|
235 | await this._persistUpdate(configDetails);
|
236 |
|
237 | return await this.get();
|
238 | }
|
239 |
|
240 | async wipe(){
|
241 |
|
242 | _.forEach(this._configOverride, (v, k, configOverride) => {
|
243 | delete configOverride[k];
|
244 | });
|
245 |
|
246 | await Promise.all([
|
247 | this._persistWipe(),
|
248 | this._configEnvRefresh()
|
249 | ]);
|
250 |
|
251 | return this._config
|
252 | }
|
253 |
|
254 | |
255 |
|
256 |
|
257 |
|
258 |
|
259 | override(configDetails) {
|
260 |
|
261 | if( __.type(configDetails) !== 'object' || _.isEmpty(configDetails) )
|
262 | throw __.error();
|
263 |
|
264 | _.transform(configDetails, (configOverride, value, key) => {
|
265 |
|
266 | if( ! validConfigKeys[key] )
|
267 | throw __.error({key});
|
268 |
|
269 | if( value === undefined )
|
270 | Reflect.deleteProperty(this._configOverride, key);
|
271 | else
|
272 | Reflect.defineProperty(this._configOverride, key, {
|
273 | value: value,
|
274 | enumerable: true,
|
275 | writable: true,
|
276 | configurable: true
|
277 | });
|
278 |
|
279 | }, this._configOverride);
|
280 |
|
281 | return this._config;
|
282 | }
|
283 |
|
284 | |
285 |
|
286 |
|
287 |
|
288 | async refresh() {
|
289 |
|
290 |
|
291 |
|
292 | await this._configEnvRefresh();
|
293 |
|
294 | await this._persistRefresh();
|
295 |
|
296 | this._refreshed = Date.now();
|
297 |
|
298 | return this._config;
|
299 |
|
300 | }
|
301 |
|
302 | async _configEnvRefresh() {
|
303 |
|
304 | _.forEach(this._configEnv, (v, k) => {
|
305 |
|
306 | const envKey = `DIGITALSPACES_CLI_CONFIG_${_.toUpper(_.snakeCase(k))}`;
|
307 |
|
308 | if( ! Object.hasOwnProperty.call(process.env, envKey) )
|
309 | delete this._configEnv[k];
|
310 |
|
311 | });
|
312 |
|
313 | _.forEach(process.env, (v, envKey) => {
|
314 |
|
315 | const envKeyLower = envKey.toLowerCase();
|
316 |
|
317 | if( ! envKeyLower.startsWith('digitalspaces_cli_config_') )
|
318 | return;
|
319 |
|
320 | const configKey = _.camelCase(envKeyLower.slice(25));
|
321 |
|
322 | if( ! validConfigKeys[configKey] )
|
323 | return;
|
324 |
|
325 | if( Object.hasOwnProperty.call(this._configEnv, configKey) )
|
326 | return;
|
327 |
|
328 | Object.defineProperty(this._configEnv, configKey, {
|
329 | get: () => {
|
330 | switch( process.env[envKey] ) {
|
331 | case '':
|
332 | case 'undefined':
|
333 | return undefined;
|
334 | case 'null':
|
335 | return null;
|
336 | case 'true':
|
337 | return true;
|
338 | case 'false':
|
339 | return false;
|
340 | default:
|
341 | if( _.isFinite(+process.env[envKey]) )
|
342 | return +process.env[envKey];
|
343 | return process.env[envKey];
|
344 | }
|
345 | },
|
346 | configurable: true, enumerable: true
|
347 | });
|
348 |
|
349 | });
|
350 |
|
351 | return this._configEnv;
|
352 | }
|
353 |
|
354 | async _persistWipe(){
|
355 |
|
356 | const persistPath = await this._persistPath();
|
357 |
|
358 | if( ! persistPath )
|
359 | throw __.error('expect (persistPath) to be truthy.', {persistPath});
|
360 |
|
361 | await Fs.unlink(persistPath);
|
362 |
|
363 | return await this._persistRefresh();
|
364 | }
|
365 |
|
366 | |
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 | async _persistUpdate(configDetails) {
|
374 |
|
375 | if( ! __.isPlainObject(configDetails) )
|
376 | throw __.error({configDetails});
|
377 |
|
378 | if( configDetails.configPath !== undefined )
|
379 | configDetails = _.transform(configDetails, (r, v, k) => {
|
380 | k !== 'configPath' && (r[k] = v);
|
381 | }, {});
|
382 |
|
383 | const configPersist = await this._persistRefresh();
|
384 |
|
385 | if( configPersist === undefined )
|
386 | return false;
|
387 | else if( _.isEmpty(configDetails) )
|
388 | return configPersist;
|
389 |
|
390 | let updated;
|
391 |
|
392 | _.transform(configDetails, (configPersist, v, k) => {
|
393 | if( ! _.isEqual(v, configPersist[k]) ) {
|
394 | updated = true;
|
395 | Reflect.defineProperty(configPersist, k, {
|
396 | value: v,
|
397 | enumerable: true,
|
398 | writable: true,
|
399 | configurable: true
|
400 | });
|
401 | }
|
402 |
|
403 | }, configPersist);
|
404 |
|
405 | if( ! updated )
|
406 | return configPersist;
|
407 |
|
408 | await this._persistWrite(configPersist);
|
409 |
|
410 | return configPersist
|
411 | }
|
412 |
|
413 | async _persistRefresh() {
|
414 |
|
415 | const configStored = await this._persistRead();
|
416 |
|
417 | if( configStored === undefined )
|
418 | return undefined;
|
419 |
|
420 | if( ! __.isObject(configStored) )
|
421 | throw __.error({configStored});
|
422 |
|
423 | if( _.isEmpty(configStored) ) {
|
424 |
|
425 | _.forEach(this._configPersist, (v, k, existingPersistedConfig) => {
|
426 | delete existingPersistedConfig[k];
|
427 | });
|
428 |
|
429 | }
|
430 | else {
|
431 |
|
432 |
|
433 | _.forEach(this._configPersist, (v, k, existingPersistedConfig) => {
|
434 | if( ! Object.hasOwnProperty.call(configStored, k) || configStored[k] === undefined )
|
435 | delete existingPersistedConfig[k];
|
436 | });
|
437 |
|
438 | _.transform(configStored, (existingPersistedConfig, v, k) => {
|
439 | existingPersistedConfig[k] = v;
|
440 | }, this._configPersist)
|
441 | }
|
442 |
|
443 | return this._configPersist;
|
444 | }
|
445 |
|
446 | async _persistRead(configPath) {
|
447 |
|
448 | if( configPath === undefined )
|
449 | configPath = await this._persistPath();
|
450 |
|
451 | if( ! configPath )
|
452 | return undefined;
|
453 |
|
454 | try {
|
455 | return await Fs.readJson(configPath);
|
456 | }
|
457 | catch( err ) {
|
458 | return {}
|
459 | }
|
460 | }
|
461 |
|
462 | async _persistWrite(configJson, configPath) {
|
463 |
|
464 | if( __.type(configJson) !== 'object' )
|
465 | throw new Error();
|
466 |
|
467 | if( _.some(configJson, (v, k) => ! validConfigKeys[k]) )
|
468 | throw new Error();
|
469 |
|
470 | if( ! configPath ) {
|
471 | configPath = await this._persistPath();
|
472 | if( ! configPath )
|
473 | throw __.error({configPath});
|
474 | }
|
475 |
|
476 | await Fs.outputJson(configPath, configJson);
|
477 |
|
478 | return configJson;
|
479 | }
|
480 |
|
481 | async _persistPath(configPath) {
|
482 | if( configPath === undefined ){
|
483 | configPath = this._config.configPath;
|
484 | if( ! configPath )
|
485 | return undefined;
|
486 | }
|
487 | await Fs.ensureFile(configPath);
|
488 | return configPath;
|
489 | }
|
490 | }
|
491 |
|
492 | module.exports = Config;
|
493 |
|
494 | module.exports.Config = Config;
|