@rosen-bridge/config 0.1.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.
Files changed (50) hide show
  1. package/.eslintignore +1 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +24 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +102 -0
  7. package/dist/config.d.ts +131 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +578 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +2 -0
  13. package/dist/schema/Validators/fieldProperties.d.ts +29 -0
  14. package/dist/schema/Validators/fieldProperties.d.ts.map +1 -0
  15. package/dist/schema/Validators/fieldProperties.js +254 -0
  16. package/dist/schema/types/fields.d.ts +37 -0
  17. package/dist/schema/types/fields.d.ts.map +1 -0
  18. package/dist/schema/types/fields.js +2 -0
  19. package/dist/schema/types/validations.d.ts +46 -0
  20. package/dist/schema/types/validations.d.ts.map +1 -0
  21. package/dist/schema/types/validations.js +2 -0
  22. package/dist/tsconfig.tsbuildinfo +1 -0
  23. package/dist/utils.d.ts +36 -0
  24. package/dist/utils.d.ts.map +1 -0
  25. package/dist/utils.js +59 -0
  26. package/dist/value/validators.d.ts +3 -0
  27. package/dist/value/validators.d.ts.map +1 -0
  28. package/dist/value/validators.js +188 -0
  29. package/lib/cli.ts +113 -0
  30. package/lib/config.ts +664 -0
  31. package/lib/index.ts +1 -0
  32. package/lib/schema/Validators/fieldProperties.ts +273 -0
  33. package/lib/schema/types/fields.ts +46 -0
  34. package/lib/schema/types/validations.ts +63 -0
  35. package/lib/utils.ts +68 -0
  36. package/lib/value/validators.ts +268 -0
  37. package/package.json +48 -0
  38. package/tests/.gitkeep +0 -0
  39. package/tests/config.spec.ts +895 -0
  40. package/tests/configEnvSetup.ts +34 -0
  41. package/tests/configTestData.ts +977 -0
  42. package/tests/configTestFiles/custom-environment-variables.json +5 -0
  43. package/tests/configTestFiles/default.json +12 -0
  44. package/tests/configTestFiles/local.json +5 -0
  45. package/tests/utils.spec.ts +117 -0
  46. package/tests/utilsTestData.ts +26 -0
  47. package/tsconfig.build.json +7 -0
  48. package/tsconfig.build.tsbuildinfo +1 -0
  49. package/tsconfig.json +7 -0
  50. package/vitest.config.ts +11 -0
package/lib/config.ts ADDED
@@ -0,0 +1,664 @@
1
+ import { IConfig, IConfigSource } from 'config';
2
+ import * as fs from 'fs';
3
+ import * as yaml from 'js-yaml';
4
+ import JsonBigIntFactory from 'json-bigint';
5
+ import path from 'path';
6
+ import {
7
+ propertyValidators,
8
+ supportedTypes,
9
+ } from './schema/Validators/fieldProperties';
10
+ import { ConfigField, ConfigSchema } from './schema/types/fields';
11
+ import { When } from './schema/types/validations';
12
+ import { getSourceName, getValueFromConfigSources } from './utils';
13
+ import { valueValidations, valueValidators } from './value/validators';
14
+
15
+ export class ConfigValidator {
16
+ constructor(private schema: ConfigSchema) {
17
+ this.validateSchema();
18
+ }
19
+
20
+ /**
21
+ * validates the passed config against the instance's schema
22
+ *
23
+ * @param {Record<string, any>} config
24
+ */
25
+ public validateConfig(config: Record<string, any>) {
26
+ const errorPreamble = (path: Array<string>) =>
27
+ `config validation failed for "${path.join('.')}" field`;
28
+
29
+ const stack: Array<{
30
+ subSchema: ConfigSchema;
31
+ subConfig: Record<string, any> | undefined;
32
+ parentPath: Array<string>;
33
+ }> = [
34
+ {
35
+ subSchema: this.schema,
36
+ subConfig: config,
37
+ parentPath: [],
38
+ },
39
+ ];
40
+
41
+ this.validateValue(
42
+ config,
43
+ { type: 'object', children: this.schema },
44
+ config
45
+ );
46
+
47
+ // Traverses the schema object tree depth first in coordination with config
48
+ // object tree and validate config using the schema
49
+ while (stack.length > 0) {
50
+ const { subSchema, subConfig, parentPath } = stack.pop()!;
51
+ // Process children of current field
52
+ for (const name of Object.keys(subSchema)) {
53
+ const path = parentPath.concat([name]);
54
+ try {
55
+ const field = subSchema[name];
56
+ let value = undefined;
57
+ if (subConfig != undefined && Object.hasOwn(subConfig, name)) {
58
+ value = subConfig[name];
59
+ }
60
+
61
+ this.validateValue(value, field, config);
62
+
63
+ // if a node/field is of type object and thus is a subtree, add it to
64
+ // the stack to be traversed later
65
+ if (field.type === 'object') {
66
+ stack.push({
67
+ subSchema: field.children,
68
+ subConfig: value,
69
+ parentPath: path,
70
+ });
71
+ }
72
+ } catch (error: any) {
73
+ throw new Error(`${errorPreamble(path)}: ${error.message}`);
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * validates a value in config object
81
+ *
82
+ * @private
83
+ * @param {*} value
84
+ * @param {ConfigField} field the field specification in schema
85
+ * @param {Record<string, any>} config the config object
86
+ */
87
+ private validateValue = (
88
+ value: any,
89
+ field: ConfigField,
90
+ config: Record<string, any>
91
+ ) => {
92
+ if (value != undefined) {
93
+ if (field.type === 'bigint') {
94
+ value = BigInt(value);
95
+ } else if (field.type === 'number' && value && !isNaN(value)) {
96
+ value = Number(value);
97
+ }
98
+ valueValidators[field.type](value, field);
99
+ }
100
+
101
+ if (field.type != 'object' && field.validations) {
102
+ for (const validation of field.validations) {
103
+ const name = Object.keys(validation).filter(
104
+ (key) => key !== 'when' && key !== 'error'
105
+ )[0];
106
+ if (Object.hasOwn(valueValidations[field.type], name)) {
107
+ try {
108
+ valueValidations[field.type][name](value, validation, config, this);
109
+ } catch (error: any) {
110
+ if (validation.error != undefined) {
111
+ throw new Error(validation.error);
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ };
119
+
120
+ /**
121
+ * determines if a when clause in validations section of a schema field is
122
+ * satisfied
123
+ *
124
+ * @param {When} when
125
+ * @param {Record<string, any>} config
126
+ * @return {boolean}
127
+ */
128
+ public isWhenTrue = (when: When, config: Record<string, any>): boolean => {
129
+ const pathParts = when.path.split('.');
130
+ const value = ConfigValidator.valueAt(config, pathParts);
131
+ return value != undefined && value === when.value;
132
+ };
133
+
134
+ /**
135
+ * returns the value at specified path in config object
136
+ *
137
+ * @static
138
+ * @param {Record<string, any>} config
139
+ * @param {string[]} path
140
+ * @return {*}
141
+ */
142
+ static valueAt = (config: Record<string, any>, path: string[]) => {
143
+ let value: any = config;
144
+ for (const key of path) {
145
+ if (value != undefined && Object.hasOwn(value, key)) {
146
+ value = value[key];
147
+ } else {
148
+ return undefined;
149
+ }
150
+ }
151
+
152
+ return value;
153
+ };
154
+
155
+ /**
156
+ * validates this.schema and throws exception if any errors found
157
+ */
158
+ private validateSchema = () => {
159
+ const errorPreamble = (path: Array<string>) =>
160
+ `Schema validation failed for "${path.join('.')}" field`;
161
+
162
+ const stack: Array<{
163
+ subSchema: ConfigSchema;
164
+ parentPath: Array<string>;
165
+ }> = [
166
+ {
167
+ subSchema: this.schema,
168
+ parentPath: [],
169
+ },
170
+ ];
171
+
172
+ // Traverses the schema object tree depth first and validate fields
173
+ while (stack.length > 0) {
174
+ const { subSchema, parentPath } = stack.pop()!;
175
+
176
+ // process children of current object field
177
+ for (const name of Object.keys(subSchema).reverse()) {
178
+ const path = parentPath.concat([name]);
179
+ try {
180
+ this.validateConfigName(name);
181
+ const field = subSchema[name];
182
+
183
+ if (!Object.hasOwn(field, 'type') || typeof field.type !== 'string') {
184
+ throw new Error(
185
+ `every schema field must have a "type" property of type "string"`
186
+ );
187
+ }
188
+
189
+ if (!supportedTypes.includes(field.type)) {
190
+ throw new Error(`unsupported field type "${field.type}"`);
191
+ }
192
+
193
+ this.validateSchemaField(field);
194
+
195
+ // if the child is an object field itself add it to stack for
196
+ // processing
197
+ if (field.type === 'object') {
198
+ stack.push({
199
+ subSchema: field.children,
200
+ parentPath: path,
201
+ });
202
+ }
203
+ } catch (error: any) {
204
+ throw new Error(`${errorPreamble(path)}: ${error.message}`);
205
+ }
206
+ }
207
+ }
208
+ };
209
+
210
+ /**
211
+ * validates config key name
212
+ *
213
+ * @param {string} name
214
+ */
215
+ private validateConfigName = (name: string) => {
216
+ if (name.includes('.')) {
217
+ throw new Error(`config key name can not contain the '.' character`);
218
+ }
219
+ };
220
+
221
+ /**
222
+ * validates passed schema field structure
223
+ *
224
+ * @param {ConfigField} field
225
+ */
226
+ private validateSchemaField = (field: ConfigField) => {
227
+ for (const key of Object.keys(field)) {
228
+ if (
229
+ !Object.hasOwn(propertyValidators.all, key) &&
230
+ !(
231
+ field.type !== 'object' &&
232
+ Object.hasOwn(propertyValidators.primitive, key)
233
+ ) &&
234
+ !Object.hasOwn(propertyValidators[field.type], key)
235
+ ) {
236
+ throw new Error(`schema field has unknown property "${key}"`);
237
+ }
238
+ }
239
+
240
+ for (const validator of Object.values(propertyValidators.all)) {
241
+ validator(field, this);
242
+ }
243
+
244
+ if (field.type !== 'object') {
245
+ for (const validator of Object.values(propertyValidators.primitive)) {
246
+ validator(field, this);
247
+ }
248
+ }
249
+
250
+ for (const validator of Object.values(propertyValidators[field.type])) {
251
+ validator(field, this);
252
+ }
253
+ };
254
+
255
+ /**
256
+ * returns a field corresponding to a path in schema tree
257
+ *
258
+ * @param {string[]} path
259
+ * @return {(ConfigField | undefined)} returns undefined if field is not found
260
+ */
261
+ getSchemaField = (path: string[]): ConfigField | undefined => {
262
+ let subTree: ConfigSchema | undefined = this.schema;
263
+ let field: ConfigField | undefined = undefined;
264
+ for (const part of path) {
265
+ if (subTree != undefined && Object.hasOwn(subTree, part)) {
266
+ field = subTree[part];
267
+ subTree = 'children' in field ? field.children : undefined;
268
+ } else {
269
+ return undefined;
270
+ }
271
+ }
272
+ return field;
273
+ };
274
+
275
+ /**
276
+ * extracts default values from a schema
277
+ *
278
+ * @return {Record<string, any>} object of default values
279
+ */
280
+ generateDefault = (): Record<string, any> => {
281
+ const valueTree: Record<string, any> = Object.create(null);
282
+
283
+ const stack: {
284
+ schema: ConfigSchema;
285
+ parentValue: Record<string, any> | undefined;
286
+ fieldName: string;
287
+ children: string[];
288
+ }[] = [
289
+ {
290
+ schema: this.schema,
291
+ parentValue: undefined,
292
+ fieldName: '',
293
+ children: Object.keys(this.schema).reverse(),
294
+ },
295
+ ];
296
+
297
+ // Traverses the schema object tree depth first
298
+ while (stack.length > 0) {
299
+ const { schema, parentValue, fieldName, children } = stack.at(-1)!;
300
+
301
+ // if a subtree's processing is finished go to the previous level
302
+ if (children.length === 0) {
303
+ // if a subtree is empty (has no values) remove it from the result
304
+ if (
305
+ parentValue != undefined &&
306
+ Object.keys(parentValue[fieldName]).length === 0
307
+ ) {
308
+ delete parentValue[fieldName];
309
+ }
310
+ stack.pop();
311
+ continue;
312
+ }
313
+
314
+ const childName = children.pop()!;
315
+ const value =
316
+ parentValue != undefined ? parentValue[fieldName] : valueTree;
317
+ const field = schema[childName];
318
+ // if a node/field is of type object and thus is a subtree, add it both to
319
+ // value tree and to the stack to be traversed later. Otherwise it's a
320
+ // leaf and needs no traversal, so add it only to the value tree.
321
+ if (field.type === 'object') {
322
+ value[childName] = Object.create(null);
323
+ stack.push({
324
+ schema: field.children,
325
+ parentValue: value,
326
+ fieldName: childName,
327
+ children: Object.keys(field.children).reverse(),
328
+ });
329
+ } else if (field.default != undefined) {
330
+ value[childName] = field.default;
331
+ }
332
+ }
333
+
334
+ return valueTree;
335
+ };
336
+
337
+ /**
338
+ * generates compatible TypeScript interface for this instance's schema
339
+ *
340
+ * @param {string} name the name of root type
341
+ * @return {string}
342
+ */
343
+ generateTSTypes = (name: string): string => {
344
+ const errorPreamble = (path: Array<string>) =>
345
+ `TypeScript type generation failed for "${path.join('.')}" field`;
346
+
347
+ const types: Array<string> = [];
348
+
349
+ const typeNames: Map<string, bigint> = new Map<string, bigint>();
350
+ typeNames.set(name, 1n);
351
+
352
+ const stack: Array<{
353
+ subSchema: ConfigSchema;
354
+ children: string[];
355
+ parentPath: Array<string>;
356
+ typeName: string;
357
+ attributes: Array<[string, string]>;
358
+ }> = [
359
+ {
360
+ subSchema: this.schema,
361
+ children: Object.keys(this.schema),
362
+ parentPath: [],
363
+ typeName: name,
364
+ attributes: [],
365
+ },
366
+ ];
367
+
368
+ // Traverses the schema object tree depth first
369
+ while (stack.length > 0) {
370
+ const { subSchema, children, parentPath, typeName, attributes } =
371
+ stack.at(-1)!;
372
+ const path = parentPath.concat([name]);
373
+ try {
374
+ // if a subtree's processing is finished go to the previous level
375
+ if (children.length == 0) {
376
+ types.push(this.genTSInterface(typeName, attributes));
377
+ stack.pop();
378
+ continue;
379
+ }
380
+
381
+ const childName = children.pop()!;
382
+ const field = subSchema[childName];
383
+
384
+ // if a node/field is of type object and thus is a subtree, add it to
385
+ // the stack to be traversed later. Otherwise it's a leaf and needs no
386
+ // traversal.
387
+ if (field.type === 'object') {
388
+ let childTypeName = `${childName[0].toUpperCase()}${childName.substring(
389
+ 1
390
+ )}`;
391
+ const typeNameCount = typeNames.get(childTypeName);
392
+ typeNames.set(childTypeName, (typeNames.get(childName) || 0n) + 1n);
393
+ if (typeNameCount) {
394
+ childTypeName += typeNameCount.toString();
395
+ }
396
+
397
+ stack.push({
398
+ subSchema: field.children,
399
+ children: Object.keys(field.children).reverse(),
400
+ parentPath: path,
401
+ typeName: childTypeName,
402
+ attributes: [],
403
+ });
404
+
405
+ attributes.push([childName, childTypeName]);
406
+ } else {
407
+ attributes.push([childName, field.type]);
408
+ }
409
+ } catch (error: any) {
410
+ throw new Error(`${errorPreamble(path)}: ${error.message}`);
411
+ }
412
+ }
413
+
414
+ return types.reverse().join('\n\n') + '\n';
415
+ };
416
+
417
+ /**
418
+ * generates a TypeScript interface definition for passed name and attributes
419
+ *
420
+ * @param {string} name
421
+ * @param {Array<[string, string]>} attributes
422
+ * @return {string}
423
+ */
424
+ private genTSInterface = (
425
+ name: string,
426
+ attributes: Array<[string, string]>
427
+ ): string => {
428
+ return `interface ${name} {
429
+ ${attributes.map((attr) => `${attr[0]}: ${attr[1]};`).join('\n ')}
430
+ }`;
431
+ };
432
+
433
+ /**
434
+ * returns a characteristic object for values at a specific node config level
435
+ *
436
+ * @param {IConfig} config
437
+ * @param {string} level
438
+ * @return {Record<string, any>}
439
+ */
440
+ getConfigForLevel(config: IConfig, level: string): Record<string, any> {
441
+ const confLevels = ConfigValidator.getNodeConfigLevels(config);
442
+ const levelIndex = confLevels.indexOf(level);
443
+ if (levelIndex === -1) {
444
+ throw new Error(
445
+ `The "${level}" level not found in the current system configuration levels`
446
+ );
447
+ }
448
+ const higherLevelSources = config.util
449
+ .getConfigSources()
450
+ .filter(
451
+ (source) => confLevels.indexOf(getSourceName(source)) > levelIndex
452
+ );
453
+ const currentLevelSource = config.util
454
+ .getConfigSources()
455
+ .filter((source) => getSourceName(source) === level)
456
+ .at(0);
457
+ const lowerLevelSources = config.util
458
+ .getConfigSources()
459
+ .filter(
460
+ (source) => confLevels.indexOf(getSourceName(source)) < levelIndex
461
+ );
462
+
463
+ // Traverses the schema object tree depth first
464
+ const valueTree = ConfigValidator.processConfigForLevelNode(
465
+ this.schema,
466
+ [],
467
+ higherLevelSources,
468
+ currentLevelSource,
469
+ lowerLevelSources
470
+ );
471
+
472
+ return valueTree;
473
+ }
474
+
475
+ /**
476
+ *traverses the config schema depth first to produce characteristic object
477
+ *
478
+ * @private
479
+ * @static
480
+ * @param {ConfigSchema} schema
481
+ * @param {string[]} path
482
+ * @param {IConfigSource[]} higherLevelSources
483
+ * @param {(IConfigSource | undefined)} currentLevelSource
484
+ * @param {IConfigSource[]} lowerLevelSources
485
+ * @return {Record<string, any>}
486
+ * @memberof ConfigValidator
487
+ */
488
+ private static processConfigForLevelNode(
489
+ schema: ConfigSchema,
490
+ path: string[],
491
+ higherLevelSources: IConfigSource[],
492
+ currentLevelSource: IConfigSource | undefined,
493
+ lowerLevelSources: IConfigSource[]
494
+ ): Record<string, any> {
495
+ const value = Object.create(null);
496
+ for (const childName of Object.keys(schema).reverse()) {
497
+ const childPath = path.concat([childName]);
498
+ const field = schema[childName];
499
+ // if a field is of type object and thus is a subtree, recurse on it.
500
+ // Otherwise it's a leaf and needs no traversal.
501
+ value[childName] = Object.create(null);
502
+ if (field.type === 'object') {
503
+ value[childName] = ConfigValidator.processConfigForLevelNode(
504
+ field.children,
505
+ childPath,
506
+ higherLevelSources,
507
+ currentLevelSource,
508
+ lowerLevelSources
509
+ );
510
+ } else {
511
+ value[childName]['label'] =
512
+ field.label != undefined ? field.label : null;
513
+ value[childName]['description'] =
514
+ field.description != undefined ? field.description : null;
515
+ value[childName]['default'] = getValueFromConfigSources(
516
+ lowerLevelSources,
517
+ childPath
518
+ );
519
+ value[childName]['value'] = getValueFromConfigSources(
520
+ [...(currentLevelSource != undefined ? [currentLevelSource] : [])],
521
+ childPath
522
+ );
523
+ value[childName]['override'] = getValueFromConfigSources(
524
+ higherLevelSources,
525
+ childPath
526
+ );
527
+ }
528
+ }
529
+
530
+ return value;
531
+ }
532
+
533
+ /**
534
+ * returns a list of config sources used by node config package, ordered from
535
+ * the lowest to the highest priority
536
+ *
537
+ * @static
538
+ * @param {IConfig} config
539
+ * @return {string[]}
540
+ */
541
+ private static getNodeConfigLevels = (config: IConfig): string[] => {
542
+ const instance = config.util.getEnv('NODE_APP_INSTANCE');
543
+ let deployment = config.util.getEnv('NODE_ENV');
544
+ deployment = config.util.getEnv('NODE_CONFIG_ENV');
545
+ const fullHostname = config.util.getEnv('HOSTNAME');
546
+ const shortHostname =
547
+ fullHostname != undefined ? fullHostname.split('.')[0] : undefined;
548
+
549
+ const configLevels = [
550
+ 'default',
551
+ ...(instance != undefined ? [`default-${instance}`] : []),
552
+ ...(deployment != undefined ? [`${deployment}`] : []),
553
+ ...(instance != undefined && deployment != undefined
554
+ ? [`${deployment}-${instance}`]
555
+ : []),
556
+ ...(shortHostname != undefined ? [`${shortHostname}`] : []),
557
+ ...(shortHostname != undefined && instance != undefined
558
+ ? [`${shortHostname}-${instance}`]
559
+ : []),
560
+ ...(shortHostname != undefined && deployment != undefined
561
+ ? [`${shortHostname}-${deployment}`]
562
+ : []),
563
+ ...(shortHostname != undefined &&
564
+ deployment != undefined &&
565
+ instance != undefined
566
+ ? [`${shortHostname}-${deployment}-${instance}`]
567
+ : []),
568
+ ...(fullHostname != undefined ? [`${fullHostname}`] : []),
569
+ ...(fullHostname != undefined && instance != undefined
570
+ ? [`${fullHostname}-${instance}`]
571
+ : []),
572
+ ...(fullHostname != undefined && deployment != undefined
573
+ ? [`${fullHostname}-${deployment}`]
574
+ : []),
575
+ ...(fullHostname != undefined &&
576
+ deployment != undefined &&
577
+ instance != undefined
578
+ ? [`${fullHostname}-${deployment}-${instance}`]
579
+ : []),
580
+ `local`,
581
+ ...(instance != undefined ? [`local-${instance}`] : []),
582
+ ...(deployment != undefined ? [`local-${deployment}`] : []),
583
+ ...(deployment != undefined && instance != undefined
584
+ ? [`local-${deployment}-${instance}`]
585
+ : []),
586
+ '$NODE_CONFIG',
587
+ 'custom-environment-variables',
588
+ ];
589
+
590
+ return configLevels;
591
+ };
592
+
593
+ /**
594
+ * validates a config object and writes it to the node-config file
595
+ * corresponding to the passed level
596
+ *
597
+ * @param {Record<string, any>} configObj
598
+ * @param {IConfig} config
599
+ * @param {string} level output node-config file level
600
+ * @param {string} format the format of the output file
601
+ */
602
+ validateAndWriteConfig = (
603
+ configObj: Record<string, any>,
604
+ config: IConfig,
605
+ level: string,
606
+ format: string
607
+ ) => {
608
+ const confLevels = ConfigValidator.getNodeConfigLevels(config).filter(
609
+ (l) => l !== 'custom-environment-variables'
610
+ );
611
+ const levelIndex = confLevels.indexOf(level);
612
+ if (levelIndex === -1) {
613
+ throw new Error(
614
+ `The [${level}] level not found in the current system's configuration levels`
615
+ );
616
+ }
617
+
618
+ const configDir =
619
+ process.env['NODE_CONFIG_DIR'] != undefined
620
+ ? process.env['NODE_CONFIG_DIR']
621
+ : './config';
622
+ let output = '';
623
+ let ext = '';
624
+ switch (format) {
625
+ case 'json': {
626
+ const JsonBigInt = JsonBigIntFactory({
627
+ alwaysParseAsBig: false,
628
+ useNativeBigInt: true,
629
+ });
630
+ output = JsonBigInt.stringify(configObj);
631
+ ext = 'json';
632
+ break;
633
+ }
634
+ case 'yaml': {
635
+ output = yaml.dump(configObj);
636
+ ext = 'yaml';
637
+ break;
638
+ }
639
+ default:
640
+ throw Error(`Invalid format: ${format}`);
641
+ }
642
+
643
+ const outputPath = path.join(configDir, `${level}.${ext}`);
644
+ const backupPath = path.join(configDir, `${level}-backup.${ext}`);
645
+ const confFileExists = fs.existsSync(outputPath);
646
+ if (confFileExists) {
647
+ fs.renameSync(outputPath, backupPath);
648
+ }
649
+ fs.writeFileSync(outputPath, output);
650
+
651
+ const updatedConfObj = config.util.loadFileConfigs();
652
+
653
+ try {
654
+ this.validateConfig(updatedConfObj);
655
+ fs.unlinkSync(backupPath);
656
+ } catch (error) {
657
+ fs.unlinkSync(outputPath);
658
+ if (confFileExists) {
659
+ fs.renameSync(backupPath, outputPath);
660
+ }
661
+ throw error;
662
+ }
663
+ };
664
+ }
package/lib/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './config';