UNPKG

12.5 kBJavaScriptView Raw
1/*
2 * Copyright (C) 2019 Forward Thinking, Inc. - All Rights Reserved
3 * Unauthorized copying of this file, via any medium is strictly prohibited
4 * Proprietary and confidential
5 */
6'use strict';
7const _ = require('lodash');
8const __ = require('@digitalspaces/utils');
9const Path = require('path');
10const Fs = require('fs-extra');
11
12const validConfigKeys = {
13 'authToken': true,
14 'baseUrl': true,
15 'configPath': true,
16 'debugRuntimeToken': true,
17 'userId': true
18};
19
20function 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
35function 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
71class Config {
72
73 constructor(configOverride) {
74
75 //only resolve the defaultConfigPath once per run (keeps the path stable if their were somehow to exist a project level config mid operation
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 //if( ! this._refreshed || this._refreshed < Date.now() - 60000 )
205 // await this.refresh();
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 //if( ! this._refreshed || this._refreshed < Date.now() - 60000 )
218 // await this.refresh();
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 //Everything should be an override first (to override environmental variables, etc).
232 //Then we try to persist the details that make sense
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 * @param configDetails {object<{authToken: string=, baseUrl: string=, configPath: string=, userId: string=}>}
257 * @returns {*}
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 * @returns {object} Config object.
287 */
288 async refresh() {
289
290 //Note: need to do the refreshes in order so that if something in environment overrides configPath it is used in the persistRefresh();
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 * @param configDetails
369 * @returns {Promise<*>}
370 * @private
371 * @TODO Need to do write locking
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 //Delete removed items first
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
492module.exports = Config;
493
494module.exports.Config = Config;