@kibibit/configit 2.3.0 → 2.6.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.
@@ -2,22 +2,36 @@
2
2
  import fsExtra from 'fs-extra';
3
3
  import { mockProcessExit } from 'jest-mock-process';
4
4
 
5
- import { ConfigService } from './config.service';
6
- import { PizzaConfig, ToppingEnum } from './pizza.config.model.mock';
5
+ import { ConfigService, EFileFormats, IConfigServiceOptions } from './config.service';
6
+ import { PizzaConfig, ToppingEnum, ToppingsConfig } from './pizza.config.model.mock';
7
7
 
8
8
  class PizzaConfigService extends ConfigService<PizzaConfig> {
9
- constructor(passedConfig?: Partial<PizzaConfig>) {
10
- super(PizzaConfig, passedConfig);
9
+ constructor(passedConfig?: Partial<PizzaConfig>, options?: IConfigServiceOptions) {
10
+ super(PizzaConfig, passedConfig, options);
11
11
  }
12
12
  }
13
13
 
14
14
  describe('Config Service', () => {
15
15
  let configService: PizzaConfigService;
16
+ let mockExit;
16
17
  beforeAll(() => {
17
18
  configService = new PizzaConfigService({
18
19
  NODE_ENV: 'test'
19
20
  });
20
21
  });
22
+
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ mockExit = mockProcessExit();
26
+
27
+ (fsExtra.writeJSONSync as jest.Mock).mockReturnValue(false);
28
+ (fsExtra.writeFileSync as jest.Mock).mockReturnValue(false);
29
+ (fsExtra.pathExistsSync as jest.Mock).mockReturnValue(true);
30
+ });
31
+
32
+ afterEach(() => {
33
+ mockExit.mockRestore();
34
+ });
21
35
  test('Service creation', () => {
22
36
  expect(configService).toBeDefined();
23
37
  });
@@ -38,10 +52,7 @@ describe('Config Service', () => {
38
52
  });
39
53
 
40
54
  test('Service can initialize the config files with saveToFile or init param', () => {
41
- (fsExtra.writeJSONSync as jest.Mock).mockReturnValue(false);
42
55
  (fsExtra.pathExistsSync as jest.Mock).mockReturnValue(false);
43
- const mockExit = mockProcessExit();
44
- jest.clearAllMocks();
45
56
  new PizzaConfigService({
46
57
  saveToFile: true,
47
58
  NODE_ENV: 'test'
@@ -51,14 +62,9 @@ describe('Config Service', () => {
51
62
  expect((fsExtra.writeJSONSync as jest.Mock).mock.calls[0]).toMatchSnapshot();
52
63
  expect((fsExtra.writeJSONSync as jest.Mock).mock.calls[1]).toMatchSnapshot();
53
64
  expect(mockExit).toHaveBeenCalledWith(0);
54
- mockExit.mockRestore();
55
65
  });
56
66
 
57
67
  test('Service can SAVE the config files with saveToFile or init param', () => {
58
- (fsExtra.writeJSONSync as jest.Mock).mockReturnValue(false);
59
- (fsExtra.pathExistsSync as jest.Mock).mockReturnValue(true);
60
- jest.clearAllMocks();
61
- const mockExit = mockProcessExit();
62
68
  new PizzaConfigService({
63
69
  saveToFile: true,
64
70
  NODE_ENV: 'test',
@@ -68,6 +74,234 @@ describe('Config Service', () => {
68
74
  expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(1);
69
75
  expect((fsExtra.writeJSONSync as jest.Mock).mock.calls[0]).toMatchSnapshot();
70
76
  expect(mockExit).toHaveBeenCalledWith(0);
71
- mockExit.mockRestore();
77
+ });
78
+
79
+ test('Service can Save and CONVERT the config files with saveToFile or init param', () => {
80
+ new PizzaConfigService({
81
+ saveToFile: true,
82
+ convert: EFileFormats.yaml,
83
+ NODE_ENV: 'test',
84
+ toppings: [ ToppingEnum.Cheese ]
85
+ });
86
+
87
+ expect(fsExtra.writeFileSync).toHaveBeenCalledTimes(1);
88
+
89
+ const [ filePath, fileContent ] = (fsExtra.writeFileSync as jest.Mock).mock.calls[0];
90
+
91
+ expect(filePath).toMatchSnapshot();
92
+ expect(`\n${ fileContent }\n`).toMatchSnapshot();
93
+ expect(mockExit).toHaveBeenCalledWith(0);
94
+ });
95
+
96
+ describe('wrap variables in attribute', () => {
97
+ test('YAML', () => {
98
+ new PizzaConfigService({
99
+ saveToFile: true,
100
+ convert: EFileFormats.yaml,
101
+ wrapper: 'env_variables',
102
+ NODE_ENV: 'test',
103
+ toppings: [ ToppingEnum.Cheese ]
104
+ });
105
+
106
+ expect(fsExtra.writeFileSync).toHaveBeenCalledTimes(1);
107
+
108
+ const [ filePath, fileContent ] = (fsExtra.writeFileSync as jest.Mock).mock.calls[0];
109
+
110
+ expect(filePath).toMatchSnapshot();
111
+ expect(`\n${ fileContent }\n`).toMatchSnapshot();
112
+ expect(mockExit).toHaveBeenCalledWith(0);
113
+ });
114
+
115
+ test('JSONC', () => {
116
+ new PizzaConfigService({
117
+ saveToFile: true,
118
+ wrapper: 'env_variables',
119
+ NODE_ENV: 'test',
120
+ toppings: [ ToppingEnum.Cheese ]
121
+ }, { fileFormat: EFileFormats.jsonc });
122
+
123
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(1);
124
+
125
+ const [ filePath, fileContent ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
126
+
127
+ expect(filePath).toMatchSnapshot();
128
+ expect(fileContent).toMatchSnapshot();
129
+ expect(mockExit).toHaveBeenCalledWith(0);
130
+ });
131
+
132
+ test('JSON', () => {
133
+ new PizzaConfigService({
134
+ saveToFile: true,
135
+ wrapper: 'env_variables',
136
+ NODE_ENV: 'test',
137
+ toppings: [ ToppingEnum.Cheese ]
138
+ });
139
+
140
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(1);
141
+
142
+ const [ filePath, fileContent ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
143
+
144
+ expect(filePath).toMatchSnapshot();
145
+ expect(fileContent).toMatchSnapshot();
146
+ expect(mockExit).toHaveBeenCalledWith(0);
147
+ });
148
+ });
149
+
150
+ test('Service returns correct empty yaml when config is empty', () => {
151
+ new PizzaConfigService({
152
+ saveToFile: true,
153
+ convert: EFileFormats.yaml
154
+ });
155
+
156
+ expect(fsExtra.writeFileSync).toHaveBeenCalledTimes(1);
157
+
158
+ const [ filePath, fileContent ] = (fsExtra.writeFileSync as jest.Mock).mock.calls[0];
159
+
160
+ expect(filePath).toMatchSnapshot();
161
+ expect(fileContent.trim()).toMatchSnapshot();
162
+ expect(mockExit).toHaveBeenCalledWith(0);
163
+ });
164
+
165
+ test('Service can return a plain object', () => {
166
+ const pizzaConfigInstance = new PizzaConfigService({
167
+ NODE_ENV: 'test',
168
+ toppings: [ ToppingEnum.Cheese ]
169
+ });
170
+
171
+ const plainObjectConfig = pizzaConfigInstance.toPlainObject();
172
+ expect(plainObjectConfig).toMatchSnapshot();
173
+ });
174
+
175
+ test('Service can save yaml without schema', async () => {
176
+ const pizzaConfigInstance = new PizzaConfigService({
177
+ NODE_ENV: 'test',
178
+ toppings: [ ToppingEnum.Cheese ]
179
+ });
180
+
181
+ await pizzaConfigInstance.writeConfigToFile({
182
+ fileFormat: EFileFormats.yaml,
183
+ excludeSchema: true
184
+ });
185
+
186
+ expect(fsExtra.writeFileSync).toHaveBeenCalledTimes(1);
187
+
188
+ const [ filePath, fileContent ] = (fsExtra.writeFileSync as jest.Mock).mock.calls[0];
189
+
190
+ expect(filePath).toMatchSnapshot();
191
+ expect(fileContent).not.toContain('# yaml-language-server: $schema=');
192
+ expect(fileContent).toMatchSnapshot();
193
+ });
194
+
195
+ test('writeConfigToFile YAML', () => {
196
+ const config = new PizzaConfigService({
197
+ toppings: [ ToppingEnum.Cheese ]
198
+ });
199
+
200
+ config.writeConfigToFile({
201
+ fileFormat: EFileFormats.yaml,
202
+ excludeSchema: true,
203
+ objectWrapper: 'env_variables'
204
+ });
205
+
206
+ expect(fsExtra.writeFileSync).toHaveBeenCalledTimes(1);
207
+
208
+ const [ filePath, fileContent ] = (fsExtra.writeFileSync as jest.Mock).mock.calls[0];
209
+
210
+ expect(filePath).toMatchSnapshot();
211
+ expect(fileContent.trim()).toMatchSnapshot();
212
+ });
213
+
214
+ test('writeConfigToFile HJSON', () => {
215
+ const config = new PizzaConfigService({
216
+ toppings: [ ToppingEnum.Cheese ]
217
+ });
218
+
219
+ config.writeConfigToFile({
220
+ fileFormat: EFileFormats.hjson,
221
+ excludeSchema: true,
222
+ objectWrapper: 'env_variables'
223
+ });
224
+
225
+ expect(fsExtra.writeFileSync).toHaveBeenCalledTimes(1);
226
+
227
+ const [ filePath, fileContent ] = (fsExtra.writeFileSync as jest.Mock).mock.calls[0];
228
+
229
+ expect(filePath).toMatchSnapshot();
230
+ expect(fileContent.trim()).toMatchSnapshot();
231
+ });
232
+
233
+ test('writeConfigToFile JSONC', () => {
234
+ const config = new PizzaConfigService({
235
+ toppings: [ ToppingEnum.Cheese ]
236
+ });
237
+
238
+ jest.clearAllMocks();
239
+
240
+ config.writeConfigToFile({
241
+ fileFormat: EFileFormats.jsonc,
242
+ excludeSchema: true,
243
+ objectWrapper: 'env_variables'
244
+ });
245
+
246
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(1);
247
+
248
+ const [ filePath, fileContent ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
249
+
250
+ expect(filePath).toMatchSnapshot();
251
+ expect(fileContent).toMatchSnapshot();
252
+ });
253
+
254
+ test('writeConfigToFile JSON', () => {
255
+ const config = new PizzaConfigService({
256
+ toppings: [ ToppingEnum.Cheese ]
257
+ });
258
+
259
+ jest.clearAllMocks();
260
+
261
+ config.writeConfigToFile({
262
+ fileFormat: EFileFormats.json,
263
+ excludeSchema: true,
264
+ objectWrapper: 'env_variables'
265
+ });
266
+
267
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(1);
268
+
269
+ const [ filePath, fileContent ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
270
+
271
+ expect(filePath).toMatchSnapshot();
272
+ expect(fileContent).toMatchSnapshot();
273
+ });
274
+
275
+ describe('Shared Configurations', () => {
276
+ test('Can define shared config', () => {
277
+ const pizzaConfigInstance = new PizzaConfigService({
278
+ NODE_ENV: 'test',
279
+ toppings: [ ToppingEnum.Cheese ],
280
+ INCLUDE_MEAT: true
281
+ } as any, {
282
+ sharedConfig: [ ToppingsConfig ]
283
+ });
284
+
285
+ pizzaConfigInstance.writeConfigToFile({
286
+ fileFormat: EFileFormats.json
287
+ // excludeSchema: false
288
+ });
289
+
290
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(4);
291
+
292
+ const [ filePath0, fileContent0 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
293
+ const [ filePath1, fileContent1 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[1];
294
+ const [ filePath2, fileContent2 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[2];
295
+ const [ filePath3, fileContent3 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[3];
296
+
297
+ expect(filePath0).toMatchSnapshot();
298
+ expect(fileContent0).toMatchSnapshot();
299
+ expect(filePath1).toMatchSnapshot();
300
+ expect(fileContent1).toMatchSnapshot();
301
+ expect(filePath2).toMatchSnapshot();
302
+ expect(fileContent2).toMatchSnapshot();
303
+ expect(filePath3).toMatchSnapshot();
304
+ expect(fileContent3).toMatchSnapshot();
305
+ });
72
306
  });
73
307
  });
@@ -11,23 +11,32 @@ import {
11
11
  readJSONSync,
12
12
  writeFileSync,
13
13
  writeJSONSync } from 'fs-extra';
14
+ import { stringify as hjsonStringify } from 'hjson';
14
15
  import { camelCase, chain, keys, mapValues, startCase, times } from 'lodash';
15
- import nconf, { IFormats } from 'nconf';
16
+ import nconf, { IFormat, IFormats } from 'nconf';
16
17
  import nconfYamlFormat from 'nconf-yaml';
17
18
  import YAML from 'yaml';
18
19
 
20
+ import * as nconfJsoncFormat from '@kibibit/nconf-jsonc';
21
+
19
22
  import { ConfigValidationError } from './config.errors';
20
23
  import { BaseConfig } from './config.model';
21
24
  import { getEnvironment, setEnvironment } from './environment.service';
22
25
 
23
- type IYamlIncludedFormats = IFormats & { yaml: nconfYamlFormat };
26
+ type INconfKibibitFormats = IFormats & {
27
+ yaml: nconfYamlFormat;
28
+ jsonc: IFormat;
29
+ };
24
30
 
25
- const nconfFomrats = (nconf.formats as IYamlIncludedFormats).yaml = nconfYamlFormat;
31
+ const nconfFormats = nconf.formats as INconfKibibitFormats;
32
+ nconfFormats.yaml = nconfYamlFormat;
33
+ nconfFormats.jsonc = nconfJsoncFormat;
26
34
 
27
35
  export interface IConfigServiceOptions {
28
36
  convertToCamelCase?: boolean;
29
- useYaml?: boolean;
37
+ fileFormat?: EFileFormats;
30
38
  sharedConfig?: TClass<BaseConfig>[];
39
+ skipSchema?: boolean;
31
40
  schemaFolderName?: string;
32
41
  showOverrides?: boolean;
33
42
  configFolderRelativePath?: string;
@@ -37,8 +46,15 @@ export interface IConfigServiceOptions {
37
46
  };
38
47
  }
39
48
 
49
+ export enum EFileFormats {
50
+ json = 'json',
51
+ yaml = 'yaml',
52
+ jsonc = 'jsonc',
53
+ hjson = 'hjson'
54
+ }
55
+
40
56
  export interface IWriteConfigToFileOptions {
41
- useYaml: boolean;
57
+ fileFormat: EFileFormats;
42
58
  excludeSchema?: boolean;
43
59
  objectWrapper?: string;
44
60
  outputFolder?: string;
@@ -56,7 +72,7 @@ type TClass<T> = (new (partial: Partial<T>) => T);
56
72
  * first one.
57
73
  */
58
74
  export class ConfigService<T extends BaseConfig> {
59
- private fileExtension: 'yaml' | 'json';
75
+ private fileExtension: EFileFormats;
60
76
  readonly mode: string = getEnvironment();
61
77
  readonly options: IConfigServiceOptions;
62
78
  readonly config?: T;
@@ -79,15 +95,16 @@ export class ConfigService<T extends BaseConfig> {
79
95
 
80
96
  this.options = {
81
97
  sharedConfig: [],
82
- useYaml: false,
98
+ fileFormat: EFileFormats.json,
83
99
  convertToCamelCase: false,
84
100
  schemaFolderName: '.schemas',
101
+ skipSchema: false,
85
102
  showOverrides: false,
86
103
  ...options
87
104
  };
88
105
  this.appRoot = this.findRoot();
89
106
  this.genericClass = givenClass;
90
- this.fileExtension = this.options.useYaml ? 'yaml' : 'json';
107
+ this.fileExtension = this.options.fileFormat || EFileFormats.json;
91
108
  this.config = this.createConfigInstance(this.genericClass, {}) as T;
92
109
  this.configFileName = this.config.getFileName(this.fileExtension);
93
110
  this.configFileRoot = this.findConfigRoot();
@@ -120,15 +137,17 @@ export class ConfigService<T extends BaseConfig> {
120
137
  if (config.convert) {
121
138
  console.log(cyan('Converting Configuration File'));
122
139
  }
123
- const useYaml = config.convert ? !this.options.useYaml : this.options.useYaml;
140
+ const fileFormat = config.convert ? config.convert : this.fileExtension;
124
141
  const objectWrapper = config.wrapper;
125
- this.writeConfigToFile({ useYaml, objectWrapper, excludeSchema: config.convert });
142
+ this.writeConfigToFile({ fileFormat, objectWrapper });
126
143
  console.log(cyan('EXITING'));
127
144
  process.exit(0);
128
145
  return;
129
146
  }
130
147
 
131
- this.writeSchema();
148
+ if (!this.options.skipSchema) {
149
+ this.writeSchema();
150
+ }
132
151
 
133
152
  configService = this;
134
153
  }
@@ -140,15 +159,15 @@ export class ConfigService<T extends BaseConfig> {
140
159
 
141
160
  writeConfigToFile(
142
161
  {
143
- useYaml,
162
+ fileFormat,
144
163
  excludeSchema,
145
164
  objectWrapper,
146
165
  outputFolder
147
166
  }: IWriteConfigToFileOptions = {
148
- useYaml: this.options.useYaml,
167
+ fileFormat: this.options.fileFormat,
149
168
  excludeSchema: false
150
169
  }) {
151
- const fileExtension = useYaml ? 'yaml' : 'json';
170
+ const fileExtension = fileFormat;
152
171
  const configFileName = this.config.getFileName(fileExtension);
153
172
  const configFileFullPath = join(
154
173
  outputFolder || this.configFileRoot,
@@ -164,7 +183,7 @@ export class ConfigService<T extends BaseConfig> {
164
183
  }
165
184
  const orderedKeys = this.orderObjectKeys(plainConfig);
166
185
 
167
- if (useYaml) {
186
+ if (fileFormat === 'yaml') {
168
187
  const yamlValues = chain(orderedKeys)
169
188
  .omit([ '$schema' ])
170
189
  // eslint-disable-next-line no-undefined
@@ -193,7 +212,16 @@ export class ConfigService<T extends BaseConfig> {
193
212
  { [objectWrapper]: orderedKeys } :
194
213
  orderedKeys;
195
214
 
196
- writeJSONSync(this.configFileFullPath, output, { spaces: 2 });
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 });
197
225
 
198
226
  for (const sharedConfig of this.options.sharedConfig) {
199
227
  this.writeSharedConfigToFile(sharedConfig);
@@ -221,7 +249,7 @@ export class ConfigService<T extends BaseConfig> {
221
249
  });
222
250
 
223
251
  const nconfFileOptions: nconf.IFileOptions = {
224
- format: this.options.useYaml ? nconfFomrats : null
252
+ format: nconfFormats[this.options.fileFormat]
225
253
  };
226
254
 
227
255
  if (this.options.encryptConfig) {
@@ -340,7 +368,7 @@ export class ConfigService<T extends BaseConfig> {
340
368
 
341
369
  const orderedKeys = this.orderObjectKeys(plainConfig);
342
370
 
343
- if (this.options.useYaml) {
371
+ if (this.options.fileFormat === 'yaml') {
344
372
  const yamlValues = chain(orderedKeys)
345
373
  .omit([ '$schema' ])
346
374
  // eslint-disable-next-line no-undefined
@@ -1,4 +1,4 @@
1
- import { IsEnum, IsOptional } from 'class-validator';
1
+ import { IsBoolean, IsEnum, IsOptional } from 'class-validator';
2
2
  import { values } from 'lodash';
3
3
 
4
4
  import { BaseConfig, Configuration, ConfigVariable } from './';
@@ -24,5 +24,10 @@ export class PizzaConfig extends BaseConfig {
24
24
  }
25
25
  }
26
26
 
27
- global.ToppingEnum = ToppingEnum;
28
- global.PizzaConfig = PizzaConfig;
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
+ }