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