@rosen-bridge/config 0.4.0 → 1.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.
package/lib/config.ts DELETED
@@ -1,733 +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 = (options?: { validate?: boolean }): Record<string, any> => {
329
- const valueTree = this.buildDefaultsForSchema(this.schema);
330
- if (options?.validate) {
331
- this.validateConfig(valueTree);
332
- }
333
- return valueTree;
334
- };
335
-
336
- // Builds default values for a schema subtree (objects and arrays), recursively
337
- private buildDefaultsForSchema = (
338
- schema: ConfigSchema
339
- ): Record<string, any> => {
340
- const defaults: Record<string, any> = Object.create(null);
341
- for (const key of Object.keys(schema)) {
342
- const field = schema[key];
343
- if (field.type === 'object') {
344
- const childDefaults = this.buildDefaultsForSchema(field.children);
345
- if (Object.keys(childDefaults).length > 0) {
346
- defaults[key] = childDefaults;
347
- }
348
- } else if (field.type === 'array') {
349
- if ((field as any).default != undefined) {
350
- if (field.items.type === 'object') {
351
- const itemDefaults = this.buildDefaultsForSchema(
352
- field.items.children
353
- );
354
- defaults[key] = (field as any).default.map((elem: any) => {
355
- if (
356
- elem != null &&
357
- typeof elem === 'object' &&
358
- !Array.isArray(elem)
359
- ) {
360
- return { ...itemDefaults, ...elem };
361
- }
362
- return elem;
363
- });
364
- } else {
365
- defaults[key] = (field as any).default;
366
- }
367
- }
368
- } else {
369
- if ((field as any).default != undefined) {
370
- defaults[key] = (field as any).default;
371
- }
372
- }
373
- }
374
- return defaults;
375
- };
376
-
377
- /**
378
- * generates compatible TypeScript interface for this instance's schema
379
- *
380
- * @param {string} name the name of root type
381
- * @return {string}
382
- */
383
- generateTSTypes = (name: string): string => {
384
- const errorPreamble = (path: Array<string>) =>
385
- `TypeScript type generation failed for "${path.join('.')}" field`;
386
-
387
- const types: Array<string> = [];
388
- const emittedTypeNames: Set<string> = new Set<string>();
389
-
390
- const stack: Array<{
391
- subSchema: ConfigSchema;
392
- children: string[];
393
- parentPath: Array<string>;
394
- typeName: string;
395
- attributes: Array<[string, string]>;
396
- }> = [
397
- {
398
- subSchema: this.schema,
399
- children: Object.keys(this.schema),
400
- parentPath: [],
401
- typeName: name,
402
- attributes: [],
403
- },
404
- ];
405
-
406
- // Traverses the schema object tree depth first
407
- while (stack.length > 0) {
408
- const { subSchema, children, parentPath, typeName, attributes } =
409
- stack.at(-1)!;
410
- let path: string[] = parentPath;
411
- try {
412
- // if a subtree's processing is finished go to the previous level
413
- if (children.length == 0) {
414
- if (!emittedTypeNames.has(typeName)) {
415
- types.push(this.genTSInterface(typeName, attributes));
416
- emittedTypeNames.add(typeName);
417
- }
418
- stack.pop();
419
- continue;
420
- }
421
-
422
- const childName = children.pop()!;
423
- path = parentPath.concat([childName]);
424
- const field = subSchema[childName];
425
-
426
- // if a node/field is of type object and thus is a subtree, add it to
427
- // the stack to be traversed later. Otherwise it's a leaf and needs no
428
- // traversal.
429
- if (
430
- field.type === 'object' ||
431
- (field.type === 'array' && field.items.type === 'object')
432
- ) {
433
- // Create unique type name from schema path excluding root interface name
434
- const pathParts = path;
435
- const childTypeName = pathParts
436
- .map((part) => part[0].toUpperCase() + part.substring(1))
437
- .join('');
438
-
439
- const children =
440
- field.type === 'array' && field.items.type === 'object'
441
- ? field.items.children
442
- : field.type === 'object'
443
- ? field.children
444
- : {};
445
-
446
- stack.push({
447
- subSchema: children,
448
- children: Object.keys(children).reverse(),
449
- parentPath: path,
450
- typeName: childTypeName,
451
- attributes: [],
452
- });
453
-
454
- attributes.push([
455
- childName,
456
- field.type === 'array' ? `${childTypeName}[]` : childTypeName,
457
- ]);
458
- } else {
459
- let fieldType: string =
460
- field.type === 'array' ? field.items.type : field.type;
461
- let isOptional = true;
462
- if (field.type !== 'array' && field.validations != undefined) {
463
- for (const validation of field.validations) {
464
- if (field.type === 'string' && 'choices' in validation) {
465
- fieldType = validation.choices.map((c) => `'${c}'`).join(' | ');
466
- }
467
-
468
- if ('required' in validation && !('when' in validation)) {
469
- isOptional = false;
470
- }
471
- }
472
- }
473
- attributes.push([
474
- isOptional ? `${childName}?` : childName,
475
- field.type === 'array' ? `${fieldType}[]` : fieldType,
476
- ]);
477
- }
478
- } catch (error: any) {
479
- throw new Error(`${errorPreamble(path)}: ${error.message}`);
480
- }
481
- }
482
-
483
- return types.reverse().join('\n\n') + '\n';
484
- };
485
-
486
- /**
487
- * generates a TypeScript interface definition for passed name and attributes
488
- *
489
- * @param {string} name
490
- * @param {Array<[string, string]>} attributes
491
- * @return {string}
492
- */
493
- private genTSInterface = (
494
- name: string,
495
- attributes: Array<[string, string]>
496
- ): string => {
497
- return `export interface ${name} {
498
- ${attributes.map((attr) => `${attr[0]}: ${attr[1]};`).join('\n ')}
499
- }`;
500
- };
501
-
502
- /**
503
- * returns a characteristic object for values at a specific node config level
504
- *
505
- * @param {IConfig} config
506
- * @param {string} level
507
- * @return {Record<string, any>}
508
- */
509
- getConfigForLevel(config: IConfig, level: string): Record<string, any> {
510
- const confLevels = ConfigValidator.getNodeConfigLevels(config);
511
- const levelIndex = confLevels.indexOf(level);
512
- if (levelIndex === -1) {
513
- throw new Error(
514
- `The "${level}" level not found in the current system configuration levels`
515
- );
516
- }
517
- const higherLevelSources = config.util
518
- .getConfigSources()
519
- .filter(
520
- (source) => confLevels.indexOf(getSourceName(source)) > levelIndex
521
- );
522
- const currentLevelSource = config.util
523
- .getConfigSources()
524
- .filter((source) => getSourceName(source) === level)
525
- .at(0);
526
- const lowerLevelSources = config.util
527
- .getConfigSources()
528
- .filter(
529
- (source) => confLevels.indexOf(getSourceName(source)) < levelIndex
530
- );
531
-
532
- // Traverses the schema object tree depth first
533
- const valueTree = ConfigValidator.processConfigForLevelNode(
534
- this.schema,
535
- [],
536
- higherLevelSources,
537
- currentLevelSource,
538
- lowerLevelSources
539
- );
540
-
541
- return valueTree;
542
- }
543
-
544
- /**
545
- *traverses the config schema depth first to produce characteristic object
546
- *
547
- * @private
548
- * @static
549
- * @param {ConfigSchema} schema
550
- * @param {string[]} path
551
- * @param {IConfigSource[]} higherLevelSources
552
- * @param {(IConfigSource | undefined)} currentLevelSource
553
- * @param {IConfigSource[]} lowerLevelSources
554
- * @return {Record<string, any>}
555
- * @memberof ConfigValidator
556
- */
557
- private static processConfigForLevelNode(
558
- schema: ConfigSchema,
559
- path: string[],
560
- higherLevelSources: IConfigSource[],
561
- currentLevelSource: IConfigSource | undefined,
562
- lowerLevelSources: IConfigSource[]
563
- ): Record<string, any> {
564
- const value = Object.create(null);
565
- for (const childName of Object.keys(schema).reverse()) {
566
- const childPath = path.concat([childName]);
567
- const field = schema[childName];
568
- // if a field is of type object and thus is a subtree, recurse on it.
569
- // Otherwise it's a leaf and needs no traversal.
570
- value[childName] = Object.create(null);
571
- if (field.type === 'object') {
572
- value[childName] = ConfigValidator.processConfigForLevelNode(
573
- field.children,
574
- childPath,
575
- higherLevelSources,
576
- currentLevelSource,
577
- lowerLevelSources
578
- );
579
- } else {
580
- value[childName]['label'] =
581
- field.label != undefined ? field.label : null;
582
- value[childName]['description'] =
583
- field.description != undefined ? field.description : null;
584
- value[childName]['default'] = getValueFromConfigSources(
585
- lowerLevelSources,
586
- childPath
587
- );
588
- value[childName]['value'] = getValueFromConfigSources(
589
- [...(currentLevelSource != undefined ? [currentLevelSource] : [])],
590
- childPath
591
- );
592
- value[childName]['override'] = getValueFromConfigSources(
593
- higherLevelSources,
594
- childPath
595
- );
596
- }
597
- }
598
-
599
- return value;
600
- }
601
-
602
- /**
603
- * returns a list of config sources used by node config package, ordered from
604
- * the lowest to the highest priority
605
- *
606
- * @static
607
- * @param {IConfig} config
608
- * @return {string[]}
609
- */
610
- private static getNodeConfigLevels = (config: IConfig): string[] => {
611
- const instance = config.util.getEnv('NODE_APP_INSTANCE');
612
- let deployment = config.util.getEnv('NODE_ENV');
613
- deployment = config.util.getEnv('NODE_CONFIG_ENV');
614
- const fullHostname = config.util.getEnv('HOSTNAME');
615
- const shortHostname =
616
- fullHostname != undefined ? fullHostname.split('.')[0] : undefined;
617
-
618
- const configLevels = [
619
- 'default',
620
- ...(instance != undefined ? [`default-${instance}`] : []),
621
- ...(deployment != undefined ? [`${deployment}`] : []),
622
- ...(instance != undefined && deployment != undefined
623
- ? [`${deployment}-${instance}`]
624
- : []),
625
- ...(shortHostname != undefined ? [`${shortHostname}`] : []),
626
- ...(shortHostname != undefined && instance != undefined
627
- ? [`${shortHostname}-${instance}`]
628
- : []),
629
- ...(shortHostname != undefined && deployment != undefined
630
- ? [`${shortHostname}-${deployment}`]
631
- : []),
632
- ...(shortHostname != undefined &&
633
- deployment != undefined &&
634
- instance != undefined
635
- ? [`${shortHostname}-${deployment}-${instance}`]
636
- : []),
637
- ...(fullHostname != undefined ? [`${fullHostname}`] : []),
638
- ...(fullHostname != undefined && instance != undefined
639
- ? [`${fullHostname}-${instance}`]
640
- : []),
641
- ...(fullHostname != undefined && deployment != undefined
642
- ? [`${fullHostname}-${deployment}`]
643
- : []),
644
- ...(fullHostname != undefined &&
645
- deployment != undefined &&
646
- instance != undefined
647
- ? [`${fullHostname}-${deployment}-${instance}`]
648
- : []),
649
- `local`,
650
- ...(instance != undefined ? [`local-${instance}`] : []),
651
- ...(deployment != undefined ? [`local-${deployment}`] : []),
652
- ...(deployment != undefined && instance != undefined
653
- ? [`local-${deployment}-${instance}`]
654
- : []),
655
- '$NODE_CONFIG',
656
- 'custom-environment-variables',
657
- ];
658
-
659
- return configLevels;
660
- };
661
-
662
- /**
663
- * validates a config object and writes it to the node-config file
664
- * corresponding to the passed level
665
- *
666
- * @param {Record<string, any>} configObj
667
- * @param {IConfig} config
668
- * @param {string} level output node-config file level
669
- * @param {string} format the format of the output file
670
- */
671
- validateAndWriteConfig = (
672
- configObj: Record<string, any>,
673
- config: IConfig,
674
- level: string,
675
- format: string
676
- ) => {
677
- const confLevels = ConfigValidator.getNodeConfigLevels(config).filter(
678
- (l) => l !== 'custom-environment-variables'
679
- );
680
- const levelIndex = confLevels.indexOf(level);
681
- if (levelIndex === -1) {
682
- throw new Error(
683
- `The [${level}] level not found in the current system's configuration levels`
684
- );
685
- }
686
-
687
- const configDir =
688
- process.env['NODE_CONFIG_DIR'] != undefined
689
- ? process.env['NODE_CONFIG_DIR']
690
- : './config';
691
- let output = '';
692
- let ext = '';
693
- switch (format) {
694
- case 'json': {
695
- const JsonBigInt = JsonBigIntFactory({
696
- alwaysParseAsBig: false,
697
- useNativeBigInt: true,
698
- });
699
- output = JsonBigInt.stringify(configObj);
700
- ext = 'json';
701
- break;
702
- }
703
- case 'yaml': {
704
- output = yaml.dump(configObj);
705
- ext = 'yaml';
706
- break;
707
- }
708
- default:
709
- throw Error(`Invalid format: ${format}`);
710
- }
711
-
712
- const outputPath = path.join(configDir, `${level}.${ext}`);
713
- const backupPath = path.join(configDir, `${level}-backup.${ext}`);
714
- const confFileExists = fs.existsSync(outputPath);
715
- if (confFileExists) {
716
- fs.renameSync(outputPath, backupPath);
717
- }
718
- fs.writeFileSync(outputPath, output);
719
-
720
- const updatedConfObj = config.util.loadFileConfigs();
721
-
722
- try {
723
- this.validateConfig(updatedConfObj);
724
- fs.unlinkSync(backupPath);
725
- } catch (error) {
726
- fs.unlinkSync(outputPath);
727
- if (confFileExists) {
728
- fs.renameSync(backupPath, outputPath);
729
- }
730
- throw error;
731
- }
732
- };
733
- }