@kibibit/configit 1.0.0-beta.2 → 1.0.0-beta.20
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/README.md +89 -19
- package/lib/config.model.d.ts +14 -3
- package/lib/config.model.d.ts.map +1 -1
- package/lib/config.model.js +70 -22
- package/lib/config.model.js.map +1 -1
- package/lib/config.service.d.ts +37 -7
- package/lib/config.service.d.ts.map +1 -1
- package/lib/config.service.js +271 -41
- package/lib/config.service.js.map +1 -1
- package/lib/environment.service.d.ts +3 -0
- package/lib/environment.service.d.ts.map +1 -0
- package/lib/environment.service.js +15 -0
- package/lib/environment.service.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -1
- package/lib/index.js.map +1 -1
- package/lib/json-schema.validator.d.ts +4 -4
- package/lib/json-schema.validator.d.ts.map +1 -1
- package/lib/json-schema.validator.js +19 -2
- package/lib/json-schema.validator.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +48 -33
- package/src/__snapshots__/config.errors.spec.ts.snap +12 -0
- package/src/__snapshots__/config.model.spec.ts.snap +22 -0
- package/src/__snapshots__/config.service.spec.ts.snap +314 -0
- package/src/config.errors.spec.ts +26 -0
- package/src/config.model.spec.ts +52 -0
- package/src/config.model.ts +85 -23
- package/src/config.service.spec.ts +319 -0
- package/src/config.service.ts +381 -55
- package/src/environment.service.ts +13 -0
- package/src/index.ts +1 -0
- package/src/json-schema.validator.ts +29 -1
- package/src/pizza.config.model.mock.ts +33 -0
package/src/config.service.ts
CHANGED
|
@@ -1,31 +1,69 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
1
|
+
import { join, relative } from 'path';
|
|
2
2
|
|
|
3
3
|
import { classToPlain } from 'class-transformer';
|
|
4
4
|
import { validateSync } from 'class-validator';
|
|
5
|
+
import { cyan, red } from 'colors';
|
|
5
6
|
import findRoot from 'find-root';
|
|
6
7
|
import {
|
|
8
|
+
ensureDirSync,
|
|
7
9
|
pathExistsSync,
|
|
8
10
|
readdirSync,
|
|
9
11
|
readJSONSync,
|
|
10
|
-
|
|
11
|
-
writeJSONSync
|
|
12
|
-
} from '
|
|
13
|
-
import { camelCase, chain,
|
|
14
|
-
import nconf from 'nconf';
|
|
12
|
+
writeFileSync,
|
|
13
|
+
writeJSONSync } from 'fs-extra';
|
|
14
|
+
import { stringify as hjsonStringify } from 'hjson';
|
|
15
|
+
import { camelCase, chain, keys, mapValues, startCase, times } from 'lodash';
|
|
16
|
+
import nconf, { IFormat, IFormats } from 'nconf';
|
|
17
|
+
import nconfYamlFormat from 'nconf-yaml';
|
|
18
|
+
import YAML from 'yaml';
|
|
19
|
+
|
|
20
|
+
import * as nconfJsoncFormat from '@kibibit/nconf-jsonc';
|
|
15
21
|
|
|
16
22
|
import { ConfigValidationError } from './config.errors';
|
|
17
|
-
import {
|
|
23
|
+
import { BaseConfig } from './config.model';
|
|
24
|
+
import { getEnvironment, setEnvironment } from './environment.service';
|
|
25
|
+
|
|
26
|
+
type INconfKibibitFormats = IFormats & {
|
|
27
|
+
yaml: nconfYamlFormat;
|
|
28
|
+
jsonc: IFormat;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const nconfFormats = nconf.formats as INconfKibibitFormats;
|
|
32
|
+
nconfFormats.yaml = nconfYamlFormat;
|
|
33
|
+
nconfFormats.jsonc = nconfJsoncFormat;
|
|
18
34
|
|
|
19
35
|
export interface IConfigServiceOptions {
|
|
20
36
|
convertToCamelCase?: boolean;
|
|
37
|
+
fileFormat?: EFileFormats;
|
|
38
|
+
sharedConfig?: TClass<BaseConfig>[];
|
|
39
|
+
skipSchema?: boolean;
|
|
40
|
+
schemaFolderName?: string;
|
|
41
|
+
showOverrides?: boolean;
|
|
42
|
+
configFolderRelativePath?: string;
|
|
43
|
+
encryptConfig?: {
|
|
44
|
+
algorithm: string;
|
|
45
|
+
secret: string;
|
|
46
|
+
};
|
|
21
47
|
}
|
|
22
48
|
|
|
23
|
-
|
|
49
|
+
export enum EFileFormats {
|
|
50
|
+
json = 'json',
|
|
51
|
+
yaml = 'yaml',
|
|
52
|
+
jsonc = 'jsonc',
|
|
53
|
+
hjson = 'hjson'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface IWriteConfigToFileOptions {
|
|
57
|
+
fileFormat: EFileFormats;
|
|
58
|
+
excludeSchema?: boolean;
|
|
59
|
+
objectWrapper?: string;
|
|
60
|
+
outputFolder?: string;
|
|
61
|
+
}
|
|
24
62
|
|
|
25
63
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
64
|
let configService: ConfigService<any>;
|
|
27
65
|
|
|
28
|
-
type TClass<T> = new (partial: Partial<T>) => T;
|
|
66
|
+
type TClass<T> = (new (partial: Partial<T>) => T);
|
|
29
67
|
|
|
30
68
|
/**
|
|
31
69
|
* This is a **Forced Singleton**.
|
|
@@ -33,16 +71,15 @@ type TClass<T> = new (partial: Partial<T>) => T;
|
|
|
33
71
|
* another ConfigService, you'll always get the
|
|
34
72
|
* first one.
|
|
35
73
|
*/
|
|
36
|
-
export class ConfigService<T extends
|
|
37
|
-
private
|
|
74
|
+
export class ConfigService<T extends BaseConfig> {
|
|
75
|
+
private fileExtension: EFileFormats;
|
|
76
|
+
readonly mode: string = getEnvironment();
|
|
38
77
|
readonly options: IConfigServiceOptions;
|
|
39
78
|
readonly config?: T;
|
|
40
79
|
readonly genericClass?: TClass<T>;
|
|
41
80
|
readonly fileName?: string;
|
|
42
|
-
readonly jsonSchemaFullname?: string;
|
|
43
|
-
readonly defaultConfigFilePath?: string;
|
|
44
81
|
readonly configFileName: string = '';
|
|
45
|
-
readonly
|
|
82
|
+
readonly configFileFullPath?: string;
|
|
46
83
|
readonly configFileRoot?: string;
|
|
47
84
|
readonly appRoot: string;
|
|
48
85
|
|
|
@@ -51,77 +88,305 @@ export class ConfigService<T extends Config> {
|
|
|
51
88
|
passedConfig?: Partial<T>,
|
|
52
89
|
options: IConfigServiceOptions = {}
|
|
53
90
|
) {
|
|
54
|
-
this.options = options;
|
|
55
|
-
this.appRoot = this.findRoot();
|
|
56
91
|
if (!passedConfig && configService) { return configService; }
|
|
57
92
|
|
|
93
|
+
setEnvironment(passedConfig?.NODE_ENV || getEnvironment());
|
|
94
|
+
this.mode = getEnvironment();
|
|
95
|
+
|
|
96
|
+
this.options = {
|
|
97
|
+
sharedConfig: [],
|
|
98
|
+
fileFormat: EFileFormats.json,
|
|
99
|
+
convertToCamelCase: false,
|
|
100
|
+
schemaFolderName: '.schemas',
|
|
101
|
+
skipSchema: false,
|
|
102
|
+
showOverrides: false,
|
|
103
|
+
...options
|
|
104
|
+
};
|
|
105
|
+
this.appRoot = this.findRoot();
|
|
58
106
|
this.genericClass = givenClass;
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
107
|
+
this.fileExtension = this.options.fileFormat || EFileFormats.json;
|
|
108
|
+
this.config = this.createConfigInstance(this.genericClass, {}) as T;
|
|
109
|
+
this.configFileName = this.config.getFileName(this.fileExtension);
|
|
110
|
+
this.configFileRoot = this.findConfigRoot();
|
|
111
|
+
this.configFileFullPath = join(
|
|
112
|
+
this.configFileRoot,
|
|
113
|
+
this.configFileName
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
this.initializeNconf();
|
|
64
117
|
|
|
65
|
-
|
|
118
|
+
const config = passedConfig || nconf.get();
|
|
66
119
|
|
|
67
|
-
|
|
120
|
+
const pathDoesNotExist = pathExistsSync(this.configFileFullPath) === false;
|
|
121
|
+
if (pathDoesNotExist && (config.saveToFile || config.init)) {
|
|
122
|
+
console.log(cyan('Initializing Configuration File'));
|
|
123
|
+
this.config = this.createConfigInstance(this.genericClass, {}) as T;
|
|
124
|
+
this.writeConfigToFile();
|
|
125
|
+
this.writeSchema();
|
|
126
|
+
console.log(cyan('EXITING'));
|
|
127
|
+
process.exit(0);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
68
130
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
131
|
+
if (!this.options.skipSchema) {
|
|
132
|
+
this.writeSchema();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const envConfig = this.validateInput(config);
|
|
136
|
+
if (!envConfig) { return; }
|
|
137
|
+
envConfig.NODE_ENV = this.mode;
|
|
138
|
+
this.config = this.createConfigInstance(this.genericClass, envConfig as T) as T;
|
|
139
|
+
|
|
140
|
+
if (config.saveToFile || config.init) {
|
|
141
|
+
if (config.convert) {
|
|
142
|
+
console.log(cyan('Converting Configuration File'));
|
|
143
|
+
}
|
|
144
|
+
const fileFormat = config.convert ? config.convert : this.fileExtension;
|
|
145
|
+
const objectWrapper = config.wrapper;
|
|
146
|
+
this.writeConfigToFile({ fileFormat, objectWrapper });
|
|
147
|
+
console.log(cyan('EXITING'));
|
|
148
|
+
process.exit(0);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
configService = this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
toPlainObject() {
|
|
156
|
+
// hope this works now!
|
|
157
|
+
return classToPlain(new this.genericClass(this.config));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
writeConfigToFile(
|
|
161
|
+
{
|
|
162
|
+
fileFormat,
|
|
163
|
+
excludeSchema,
|
|
164
|
+
objectWrapper,
|
|
165
|
+
outputFolder
|
|
166
|
+
}: IWriteConfigToFileOptions = {
|
|
167
|
+
fileFormat: this.options.fileFormat,
|
|
168
|
+
excludeSchema: false
|
|
169
|
+
}) {
|
|
170
|
+
const fileExtension = fileFormat;
|
|
171
|
+
const configFileName = this.config.getFileName(fileExtension);
|
|
172
|
+
const configFileFullPath = join(
|
|
173
|
+
outputFolder || this.configFileRoot,
|
|
174
|
+
configFileName
|
|
73
175
|
);
|
|
176
|
+
const plainConfig = classToPlain(this.config);
|
|
177
|
+
const relativePathToSchema = relative(
|
|
178
|
+
outputFolder || this.configFileRoot,
|
|
179
|
+
join(this.appRoot, `/${ this.options.schemaFolderName }/${ this.config.getSchemaFileName() }`)
|
|
180
|
+
);
|
|
181
|
+
if (!excludeSchema) {
|
|
182
|
+
plainConfig.$schema = relativePathToSchema;
|
|
183
|
+
}
|
|
184
|
+
const orderedKeys = this.orderObjectKeys(plainConfig);
|
|
185
|
+
|
|
186
|
+
if (fileFormat === 'yaml') {
|
|
187
|
+
const yamlValues = chain(orderedKeys)
|
|
188
|
+
.omit([ '$schema' ])
|
|
189
|
+
// eslint-disable-next-line no-undefined
|
|
190
|
+
.omitBy((value) => value === undefined)
|
|
191
|
+
.value();
|
|
192
|
+
|
|
193
|
+
const output = objectWrapper ?
|
|
194
|
+
{ [objectWrapper]: yamlValues } :
|
|
195
|
+
yamlValues;
|
|
196
|
+
const yamlString = keys(yamlValues).length > 0 ? YAML.stringify(output) : '';
|
|
197
|
+
writeFileSync(
|
|
198
|
+
configFileFullPath,
|
|
199
|
+
[
|
|
200
|
+
excludeSchema ? '' : [
|
|
201
|
+
'# yaml-language-server: $schema=',
|
|
202
|
+
relativePathToSchema,
|
|
203
|
+
'\n'
|
|
204
|
+
].join(''),
|
|
205
|
+
yamlString
|
|
206
|
+
].join('')
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const output = objectWrapper ?
|
|
212
|
+
{ [objectWrapper]: orderedKeys } :
|
|
213
|
+
orderedKeys;
|
|
214
|
+
|
|
215
|
+
if (fileFormat === 'hjson') {
|
|
216
|
+
writeFileSync(configFileFullPath, hjsonStringify(output, {
|
|
217
|
+
quotes: 'min',
|
|
218
|
+
space: 2
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
writeJSONSync(configFileFullPath, output, { spaces: 2 });
|
|
74
225
|
|
|
226
|
+
for (const sharedConfig of this.options.sharedConfig) {
|
|
227
|
+
this.writeSharedConfigToFile(sharedConfig);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private createConfigInstance(genericClass: TClass<BaseConfig>, data) {
|
|
232
|
+
const configInstance = new genericClass(data);
|
|
233
|
+
configInstance.setName(genericClass);
|
|
234
|
+
|
|
235
|
+
return configInstance;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private initializeNconf() {
|
|
75
239
|
nconf
|
|
76
240
|
.argv({
|
|
77
241
|
parseValues: true
|
|
78
242
|
})
|
|
79
243
|
.env({
|
|
244
|
+
separator: '__',
|
|
80
245
|
parseValues: true,
|
|
81
246
|
transform: this.options.convertToCamelCase ?
|
|
82
247
|
transformToCamelCase :
|
|
83
248
|
null
|
|
84
|
-
})
|
|
85
|
-
.file('defaults', { file: this.defaultConfigFilePath })
|
|
86
|
-
.file('environment', { file: this.configFilePath });
|
|
249
|
+
});
|
|
87
250
|
|
|
88
|
-
const
|
|
89
|
-
|
|
251
|
+
const nconfFileOptions: nconf.IFileOptions = {
|
|
252
|
+
format: nconfFormats[this.options.fileFormat]
|
|
253
|
+
};
|
|
90
254
|
|
|
91
|
-
|
|
255
|
+
if (this.options.encryptConfig) {
|
|
256
|
+
nconfFileOptions.secure = {
|
|
257
|
+
secret: this.options.encryptConfig.secret,
|
|
258
|
+
alg: this.options.encryptConfig.algorithm
|
|
259
|
+
};
|
|
260
|
+
}
|
|
92
261
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}, {})
|
|
104
|
-
// .omitBy((value, key) => key.startsWith('$'))
|
|
105
|
-
.value();
|
|
262
|
+
try {
|
|
263
|
+
nconf
|
|
264
|
+
.file('environment', {
|
|
265
|
+
file: this.configFileFullPath,
|
|
266
|
+
...nconfFileOptions
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(red(error.message));
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
106
272
|
|
|
107
|
-
|
|
273
|
+
for (const sharedConfig of this.options.sharedConfig) {
|
|
274
|
+
const sharedConfigInstance = this.createConfigInstance(sharedConfig, {});
|
|
275
|
+
const sharedConfigFullPath = join(
|
|
276
|
+
this.configFileRoot,
|
|
277
|
+
sharedConfigInstance.getFileName(this.fileExtension, true),
|
|
278
|
+
);
|
|
279
|
+
try {
|
|
280
|
+
nconf.file(sharedConfigInstance.name, {
|
|
281
|
+
file: sharedConfigFullPath,
|
|
282
|
+
...nconfFileOptions
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error(red(error.message));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
108
290
|
|
|
109
|
-
|
|
291
|
+
private writeSchema() {
|
|
292
|
+
ensureDirSync(join(this.configFileRoot, '/', this.options.schemaFolderName));
|
|
293
|
+
const sharedConfigsSchemas = [];
|
|
294
|
+
for (const sharedConfig of this.options.sharedConfig) {
|
|
295
|
+
const sharedConfigSchema = this.writeSharedSchema(sharedConfig);
|
|
296
|
+
sharedConfigsSchemas.push(sharedConfigSchema);
|
|
110
297
|
}
|
|
111
298
|
|
|
112
299
|
const schema = this.config.toJsonSchema();
|
|
113
300
|
const schemaFullPath = join(
|
|
114
301
|
this.configFileRoot,
|
|
115
302
|
'/',
|
|
116
|
-
this.
|
|
303
|
+
this.options.schemaFolderName,
|
|
304
|
+
'/',
|
|
305
|
+
this.config.getSchemaFileName()
|
|
117
306
|
);
|
|
118
|
-
writeJSONSync(schemaFullPath, schema);
|
|
119
307
|
|
|
120
|
-
|
|
308
|
+
let sharedConfigsProperties = {};
|
|
309
|
+
for (const sharedConfigSchema of sharedConfigsSchemas) {
|
|
310
|
+
mapValues(
|
|
311
|
+
sharedConfigSchema.properties,
|
|
312
|
+
(value) => value.description = `(OVERRIDE SHARED CONFIG)\n${ value.description }`
|
|
313
|
+
);
|
|
314
|
+
sharedConfigsProperties = {
|
|
315
|
+
...sharedConfigsProperties,
|
|
316
|
+
...sharedConfigSchema.properties
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (this.options.showOverrides) {
|
|
321
|
+
schema.properties = {
|
|
322
|
+
...this.orderObjectKeys(schema.properties),
|
|
323
|
+
...this.orderObjectKeys(sharedConfigsProperties)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
writeJSONSync(schemaFullPath, schema, { spaces: 2 });
|
|
121
328
|
}
|
|
122
329
|
|
|
123
|
-
|
|
124
|
-
return
|
|
330
|
+
private orderObjectKeys(given: { [key: string]: any }) {
|
|
331
|
+
return chain(given)
|
|
332
|
+
.keys()
|
|
333
|
+
.sort()
|
|
334
|
+
.reduce((obj: { [key: string]: any }, key) => {
|
|
335
|
+
obj[key] = given[key];
|
|
336
|
+
return obj;
|
|
337
|
+
}, {})
|
|
338
|
+
.value();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private writeSharedSchema(configClass: TClass<BaseConfig>) {
|
|
342
|
+
const config = this.createConfigInstance(configClass, {});
|
|
343
|
+
const schema = config.toJsonSchema();
|
|
344
|
+
const schemaFullPath = join(
|
|
345
|
+
this.configFileRoot,
|
|
346
|
+
'/',
|
|
347
|
+
this.options.schemaFolderName,
|
|
348
|
+
'/',
|
|
349
|
+
config.getSchemaFileName()
|
|
350
|
+
);
|
|
351
|
+
writeJSONSync(schemaFullPath, schema, { spaces: 2 });
|
|
352
|
+
|
|
353
|
+
return schema;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private writeSharedConfigToFile(configClass: TClass<BaseConfig>) {
|
|
357
|
+
const config = this.createConfigInstance(configClass, this.config);
|
|
358
|
+
const plainConfig = classToPlain(config);
|
|
359
|
+
const relativePathToSchema = relative(
|
|
360
|
+
this.configFileRoot,
|
|
361
|
+
join(this.appRoot, `/${ this.options.schemaFolderName }/${ config.getSchemaFileName() }`)
|
|
362
|
+
);
|
|
363
|
+
plainConfig.$schema = relativePathToSchema;
|
|
364
|
+
const sharedConfigFullPath = join(
|
|
365
|
+
this.configFileRoot,
|
|
366
|
+
config.getFileName(this.fileExtension, true)
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const orderedKeys = this.orderObjectKeys(plainConfig);
|
|
370
|
+
|
|
371
|
+
if (this.options.fileFormat === 'yaml') {
|
|
372
|
+
const yamlValues = chain(orderedKeys)
|
|
373
|
+
.omit([ '$schema' ])
|
|
374
|
+
// eslint-disable-next-line no-undefined
|
|
375
|
+
.omitBy((value) => value === undefined)
|
|
376
|
+
.value();
|
|
377
|
+
const yamlString = keys(yamlValues).length > 0 ? YAML.stringify(yamlValues) : '';
|
|
378
|
+
writeFileSync(
|
|
379
|
+
sharedConfigFullPath,
|
|
380
|
+
[
|
|
381
|
+
'# yaml-language-server: $schema=',
|
|
382
|
+
relativePathToSchema,
|
|
383
|
+
`\n${ yamlString }`
|
|
384
|
+
].join('')
|
|
385
|
+
);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
writeJSONSync(sharedConfigFullPath, orderedKeys, { spaces: 2 });
|
|
125
390
|
}
|
|
126
391
|
|
|
127
392
|
private findRoot() {
|
|
@@ -141,6 +406,12 @@ export class ConfigService<T extends Config> {
|
|
|
141
406
|
}
|
|
142
407
|
|
|
143
408
|
private findConfigRoot() {
|
|
409
|
+
if (this.options.configFolderRelativePath) {
|
|
410
|
+
const fullPath = join(this.appRoot, this.options.configFolderRelativePath);
|
|
411
|
+
ensureDirSync(fullPath);
|
|
412
|
+
|
|
413
|
+
return fullPath;
|
|
414
|
+
}
|
|
144
415
|
try {
|
|
145
416
|
return findRoot(process.cwd(), (dir) => {
|
|
146
417
|
const fileNames = readdirSync(dir);
|
|
@@ -163,10 +434,65 @@ export class ConfigService<T extends Config> {
|
|
|
163
434
|
const configInstance = new this.genericClass(envConfig);
|
|
164
435
|
const validationErrors = validateSync(configInstance);
|
|
165
436
|
|
|
437
|
+
let fullConfig = {};
|
|
438
|
+
let shouldExitProcess = false;
|
|
439
|
+
for (const sharedConfig of this.options.sharedConfig) {
|
|
440
|
+
const validationResult = this.validateSharedInput(envConfig, sharedConfig);
|
|
441
|
+
shouldExitProcess = shouldExitProcess || validationResult.error;
|
|
442
|
+
fullConfig = {
|
|
443
|
+
...fullConfig,
|
|
444
|
+
...validationResult.configInstance
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (validationErrors.length > 0) {
|
|
449
|
+
const validationError = new ConfigValidationError(validationErrors);
|
|
450
|
+
const errorMessageTitle = `${ startCase(this.config.name) } Configuration Errors`;
|
|
451
|
+
const titleBar = this.generateTerminalTitleBar(errorMessageTitle);
|
|
452
|
+
console.error(titleBar, validationError.message);
|
|
453
|
+
|
|
454
|
+
shouldExitProcess = shouldExitProcess || validationErrors.length > 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (shouldExitProcess) {
|
|
458
|
+
process.exit(1);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
...fullConfig,
|
|
464
|
+
...classToPlain(configInstance) as Partial<T>
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private validateSharedInput(
|
|
469
|
+
envConfig: unknown,
|
|
470
|
+
configClass: TClass<BaseConfig>
|
|
471
|
+
) {
|
|
472
|
+
const configInstance = this.createConfigInstance(configClass, envConfig);
|
|
473
|
+
const validationErrors = validateSync(configInstance);
|
|
474
|
+
|
|
475
|
+
let error = null;
|
|
166
476
|
if (validationErrors.length > 0) {
|
|
167
|
-
|
|
477
|
+
const validationError = new ConfigValidationError(validationErrors);
|
|
478
|
+
const errorMessageTitle = `${ startCase(configInstance.name) } Shared Configuration Errors`;
|
|
479
|
+
const titleBar = this.generateTerminalTitleBar(errorMessageTitle);
|
|
480
|
+
error = { titleBar, message: validationError.message };
|
|
481
|
+
console.error(titleBar, validationError.message);
|
|
168
482
|
}
|
|
169
|
-
return
|
|
483
|
+
return {
|
|
484
|
+
configInstance: classToPlain(configInstance),
|
|
485
|
+
error
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private generateTerminalTitleBar(title: string) {
|
|
490
|
+
const titleBar = red(times(title.length + 4, () => '=').join(''));
|
|
491
|
+
return [
|
|
492
|
+
titleBar,
|
|
493
|
+
red('= ') + title + red(' ='),
|
|
494
|
+
titleBar
|
|
495
|
+
].join('\n');
|
|
170
496
|
}
|
|
171
497
|
}
|
|
172
498
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { get } from 'lodash';
|
|
2
|
+
|
|
3
|
+
const realEnvironment = get(process, 'env.NODE_ENV', 'development');
|
|
4
|
+
|
|
5
|
+
let environment: string;
|
|
6
|
+
|
|
7
|
+
export function setEnvironment(givenEnvironment: string) {
|
|
8
|
+
environment = givenEnvironment;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getEnvironment() {
|
|
12
|
+
return environment || realEnvironment;
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { Exclude, Expose } from 'class-transformer';
|
|
1
2
|
import {
|
|
3
|
+
Validate,
|
|
2
4
|
ValidatorConstraint,
|
|
3
5
|
ValidatorConstraintInterface
|
|
4
6
|
} from 'class-validator';
|
|
5
7
|
|
|
8
|
+
export interface IConfigVariableOptions {
|
|
9
|
+
exclude?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
@ValidatorConstraint({ name: 'JsonSchema', async: false })
|
|
7
|
-
|
|
13
|
+
class JsonSchema implements ValidatorConstraintInterface {
|
|
8
14
|
validate() {
|
|
9
15
|
return true;
|
|
10
16
|
}
|
|
@@ -13,3 +19,25 @@ export class JsonSchema implements ValidatorConstraintInterface {
|
|
|
13
19
|
return '';
|
|
14
20
|
}
|
|
15
21
|
}
|
|
22
|
+
|
|
23
|
+
export function Configuration(): ClassDecorator {
|
|
24
|
+
const exposeFn = Exclude();
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
return function(target: any) {
|
|
27
|
+
exposeFn(target);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ConfigVariable(
|
|
32
|
+
description: string | string[] = '',
|
|
33
|
+
options: IConfigVariableOptions = {}
|
|
34
|
+
): PropertyDecorator {
|
|
35
|
+
description = Array.isArray(description) ? description : [ description ];
|
|
36
|
+
const exposeFn = options.exclude ? Exclude() : Expose();
|
|
37
|
+
const typeFn = Validate(JsonSchema, description);
|
|
38
|
+
|
|
39
|
+
return function(target: unknown, key: string) {
|
|
40
|
+
typeFn(target, key);
|
|
41
|
+
exposeFn(target, key);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IsBoolean, IsEnum, IsOptional } from 'class-validator';
|
|
2
|
+
import { values } from 'lodash';
|
|
3
|
+
|
|
4
|
+
import { BaseConfig, Configuration, ConfigVariable } from './';
|
|
5
|
+
|
|
6
|
+
export enum ToppingEnum {
|
|
7
|
+
Cheese = 'cheese',
|
|
8
|
+
Pepperoni = 'pepperoni',
|
|
9
|
+
Sausage = 'sausage'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Configuration()
|
|
13
|
+
export class PizzaConfig extends BaseConfig {
|
|
14
|
+
@IsOptional()
|
|
15
|
+
@IsEnum(values(ToppingEnum), {
|
|
16
|
+
each: true,
|
|
17
|
+
message: `Topping must be one of: ${ values(ToppingEnum).join(', ') }`
|
|
18
|
+
})
|
|
19
|
+
@ConfigVariable('optional toppings for the pizza')
|
|
20
|
+
public toppings: ToppingEnum[];
|
|
21
|
+
|
|
22
|
+
constructor(partial?: Partial<PizzaConfig>) {
|
|
23
|
+
super(partial);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Configuration()
|
|
28
|
+
export class ToppingsConfig extends BaseConfig {
|
|
29
|
+
@IsOptional()
|
|
30
|
+
@IsBoolean()
|
|
31
|
+
@ConfigVariable('Should meat be included in the toppings options')
|
|
32
|
+
INCLUDE_MEAT;
|
|
33
|
+
}
|