@kibibit/configit 2.2.1 → 2.5.1

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,215 @@ 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 JSONC', () => {
215
+ const config = new PizzaConfigService({
216
+ toppings: [ ToppingEnum.Cheese ]
217
+ });
218
+
219
+ jest.clearAllMocks();
220
+
221
+ config.writeConfigToFile({
222
+ fileFormat: EFileFormats.jsonc,
223
+ excludeSchema: true,
224
+ objectWrapper: 'env_variables'
225
+ });
226
+
227
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(1);
228
+
229
+ const [ filePath, fileContent ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
230
+
231
+ expect(filePath).toMatchSnapshot();
232
+ expect(fileContent).toMatchSnapshot();
233
+ });
234
+
235
+ test('writeConfigToFile JSON', () => {
236
+ const config = new PizzaConfigService({
237
+ toppings: [ ToppingEnum.Cheese ]
238
+ });
239
+
240
+ jest.clearAllMocks();
241
+
242
+ config.writeConfigToFile({
243
+ fileFormat: EFileFormats.json,
244
+ excludeSchema: true,
245
+ objectWrapper: 'env_variables'
246
+ });
247
+
248
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(1);
249
+
250
+ const [ filePath, fileContent ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
251
+
252
+ expect(filePath).toMatchSnapshot();
253
+ expect(fileContent).toMatchSnapshot();
254
+ });
255
+
256
+ describe('Shared Configurations', () => {
257
+ test('Can define shared config', () => {
258
+ const pizzaConfigInstance = new PizzaConfigService({
259
+ NODE_ENV: 'test',
260
+ toppings: [ ToppingEnum.Cheese ],
261
+ INCLUDE_MEAT: true
262
+ } as any, {
263
+ sharedConfig: [ ToppingsConfig ]
264
+ });
265
+
266
+ pizzaConfigInstance.writeConfigToFile({
267
+ fileFormat: EFileFormats.json
268
+ // excludeSchema: false
269
+ });
270
+
271
+ expect(fsExtra.writeJSONSync).toHaveBeenCalledTimes(4);
272
+
273
+ const [ filePath0, fileContent0 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[0];
274
+ const [ filePath1, fileContent1 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[1];
275
+ const [ filePath2, fileContent2 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[2];
276
+ const [ filePath3, fileContent3 ] = (fsExtra.writeJSONSync as jest.Mock).mock.calls[3];
277
+
278
+ expect(filePath0).toMatchSnapshot();
279
+ expect(fileContent0).toMatchSnapshot();
280
+ expect(filePath1).toMatchSnapshot();
281
+ expect(fileContent1).toMatchSnapshot();
282
+ expect(filePath2).toMatchSnapshot();
283
+ expect(fileContent2).toMatchSnapshot();
284
+ expect(filePath3).toMatchSnapshot();
285
+ expect(fileContent3).toMatchSnapshot();
286
+ });
72
287
  });
73
288
  });
@@ -12,21 +12,28 @@ import {
12
12
  writeFileSync,
13
13
  writeJSONSync } from 'fs-extra';
14
14
  import { camelCase, chain, keys, mapValues, startCase, times } from 'lodash';
15
- import nconf, { IFormats } from 'nconf';
15
+ import nconf, { IFormat, IFormats } from 'nconf';
16
16
  import nconfYamlFormat from 'nconf-yaml';
17
17
  import YAML from 'yaml';
18
18
 
19
+ import * as nconfJsoncFormat from '@kibibit/nconf-jsonc';
20
+
19
21
  import { ConfigValidationError } from './config.errors';
20
22
  import { BaseConfig } from './config.model';
21
23
  import { getEnvironment, setEnvironment } from './environment.service';
22
24
 
23
- type IYamlIncludedFormats = IFormats & { yaml: nconfYamlFormat };
25
+ type INconfKibibitFormats = IFormats & {
26
+ yaml: nconfYamlFormat;
27
+ jsonc: IFormat;
28
+ };
24
29
 
25
- const nconfFomrats = (nconf.formats as IYamlIncludedFormats).yaml = nconfYamlFormat;
30
+ const nconfFormats = nconf.formats as INconfKibibitFormats;
31
+ nconfFormats.yaml = nconfYamlFormat;
32
+ nconfFormats.jsonc = nconfJsoncFormat;
26
33
 
27
34
  export interface IConfigServiceOptions {
28
35
  convertToCamelCase?: boolean;
29
- useYaml?: boolean;
36
+ fileFormat?: EFileFormats;
30
37
  sharedConfig?: TClass<BaseConfig>[];
31
38
  schemaFolderName?: string;
32
39
  showOverrides?: boolean;
@@ -37,6 +44,19 @@ export interface IConfigServiceOptions {
37
44
  };
38
45
  }
39
46
 
47
+ export enum EFileFormats {
48
+ json = 'json',
49
+ yaml = 'yaml',
50
+ jsonc = 'jsonc',
51
+ }
52
+
53
+ export interface IWriteConfigToFileOptions {
54
+ fileFormat: EFileFormats;
55
+ excludeSchema?: boolean;
56
+ objectWrapper?: string;
57
+ outputFolder?: string;
58
+ }
59
+
40
60
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
61
  let configService: ConfigService<any>;
42
62
 
@@ -49,7 +69,7 @@ type TClass<T> = (new (partial: Partial<T>) => T);
49
69
  * first one.
50
70
  */
51
71
  export class ConfigService<T extends BaseConfig> {
52
- private fileExtension: 'yaml' | 'json';
72
+ private fileExtension: EFileFormats;
53
73
  readonly mode: string = getEnvironment();
54
74
  readonly options: IConfigServiceOptions;
55
75
  readonly config?: T;
@@ -72,7 +92,7 @@ export class ConfigService<T extends BaseConfig> {
72
92
 
73
93
  this.options = {
74
94
  sharedConfig: [],
75
- useYaml: false,
95
+ fileFormat: EFileFormats.json,
76
96
  convertToCamelCase: false,
77
97
  schemaFolderName: '.schemas',
78
98
  showOverrides: false,
@@ -80,7 +100,7 @@ export class ConfigService<T extends BaseConfig> {
80
100
  };
81
101
  this.appRoot = this.findRoot();
82
102
  this.genericClass = givenClass;
83
- this.fileExtension = this.options.useYaml ? 'yaml' : 'json';
103
+ this.fileExtension = this.options.fileFormat || EFileFormats.json;
84
104
  this.config = this.createConfigInstance(this.genericClass, {}) as T;
85
105
  this.configFileName = this.config.getFileName(this.fileExtension);
86
106
  this.configFileRoot = this.findConfigRoot();
@@ -113,9 +133,9 @@ export class ConfigService<T extends BaseConfig> {
113
133
  if (config.convert) {
114
134
  console.log(cyan('Converting Configuration File'));
115
135
  }
116
- const useYaml = config.convert ? !this.options.useYaml : this.options.useYaml;
136
+ const fileFormat = config.convert ? config.convert : this.fileExtension;
117
137
  const objectWrapper = config.wrapper;
118
- this.writeConfigToFile(useYaml, objectWrapper, config.convert);
138
+ this.writeConfigToFile({ fileFormat, objectWrapper });
119
139
  console.log(cyan('EXITING'));
120
140
  process.exit(0);
121
141
  return;
@@ -131,16 +151,25 @@ export class ConfigService<T extends BaseConfig> {
131
151
  return classToPlain(new this.genericClass(this.config));
132
152
  }
133
153
 
134
- writeConfigToFile(useYaml = this.options.useYaml, objectWrapper?: string, excludeSchema = false) {
135
- const fileExtension = useYaml ? 'yaml' : 'json';
154
+ writeConfigToFile(
155
+ {
156
+ fileFormat,
157
+ excludeSchema,
158
+ objectWrapper,
159
+ outputFolder
160
+ }: IWriteConfigToFileOptions = {
161
+ fileFormat: this.options.fileFormat,
162
+ excludeSchema: false
163
+ }) {
164
+ const fileExtension = fileFormat;
136
165
  const configFileName = this.config.getFileName(fileExtension);
137
166
  const configFileFullPath = join(
138
- this.configFileRoot,
167
+ outputFolder || this.configFileRoot,
139
168
  configFileName
140
169
  );
141
170
  const plainConfig = classToPlain(this.config);
142
171
  const relativePathToSchema = relative(
143
- this.configFileRoot,
172
+ outputFolder || this.configFileRoot,
144
173
  join(this.appRoot, `/${ this.options.schemaFolderName }/${ this.config.getSchemaFileName() }`)
145
174
  );
146
175
  if (!excludeSchema) {
@@ -148,7 +177,7 @@ export class ConfigService<T extends BaseConfig> {
148
177
  }
149
178
  const orderedKeys = this.orderObjectKeys(plainConfig);
150
179
 
151
- if (useYaml) {
180
+ if (fileFormat === 'yaml') {
152
181
  const yamlValues = chain(orderedKeys)
153
182
  .omit([ '$schema' ])
154
183
  // eslint-disable-next-line no-undefined
@@ -177,7 +206,7 @@ export class ConfigService<T extends BaseConfig> {
177
206
  { [objectWrapper]: orderedKeys } :
178
207
  orderedKeys;
179
208
 
180
- writeJSONSync(this.configFileFullPath, output, { spaces: 2 });
209
+ writeJSONSync(configFileFullPath, output, { spaces: 2 });
181
210
 
182
211
  for (const sharedConfig of this.options.sharedConfig) {
183
212
  this.writeSharedConfigToFile(sharedConfig);
@@ -205,7 +234,7 @@ export class ConfigService<T extends BaseConfig> {
205
234
  });
206
235
 
207
236
  const nconfFileOptions: nconf.IFileOptions = {
208
- format: this.options.useYaml ? nconfFomrats : null
237
+ format: nconfFormats[this.options.fileFormat]
209
238
  };
210
239
 
211
240
  if (this.options.encryptConfig) {
@@ -324,7 +353,7 @@ export class ConfigService<T extends BaseConfig> {
324
353
 
325
354
  const orderedKeys = this.orderObjectKeys(plainConfig);
326
355
 
327
- if (this.options.useYaml) {
356
+ if (this.options.fileFormat === 'yaml') {
328
357
  const yamlValues = chain(orderedKeys)
329
358
  .omit([ '$schema' ])
330
359
  // 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
+ }