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