@hg-ts/config-loader 0.5.17 → 0.5.19
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/package.json +10 -10
- package/src/config-loader.ts +5 -0
- package/src/config.module.ts +30 -0
- package/src/exceptions/index.ts +1 -0
- package/src/exceptions/no-base-config.exception.ts +7 -0
- package/src/file.config-loader.ts +127 -0
- package/src/index.ts +4 -0
- package/src/mock.config-loader.ts +16 -0
- package/src/path-builder.ts +158 -0
- package/src/tests/config-loader.test.ts +202 -0
- package/src/tests/path-builder.test.ts +133 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hg-ts/config-loader",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"test:dev": "vitest watch"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@hg-ts-config/typescript": "0.5.
|
|
22
|
-
"@hg-ts/exception": "0.5.
|
|
23
|
-
"@hg-ts/execution-mode": "0.5.
|
|
24
|
-
"@hg-ts/linter": "0.5.
|
|
25
|
-
"@hg-ts/tests": "0.5.
|
|
26
|
-
"@hg-ts/types": "0.5.
|
|
21
|
+
"@hg-ts-config/typescript": "0.5.19",
|
|
22
|
+
"@hg-ts/exception": "0.5.19",
|
|
23
|
+
"@hg-ts/execution-mode": "0.5.19",
|
|
24
|
+
"@hg-ts/linter": "0.5.19",
|
|
25
|
+
"@hg-ts/tests": "0.5.19",
|
|
26
|
+
"@hg-ts/types": "0.5.19",
|
|
27
27
|
"@nestjs/common": "11.1.0",
|
|
28
28
|
"@types/node": "22.19.1",
|
|
29
29
|
"@vitest/coverage-v8": "4.0.14",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"vitest": "4.0.14"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@hg-ts/exception": "0.5.
|
|
41
|
-
"@hg-ts/execution-mode": "0.5.
|
|
40
|
+
"@hg-ts/exception": "0.5.19",
|
|
41
|
+
"@hg-ts/execution-mode": "0.5.19",
|
|
42
42
|
"@nestjs/common": "*",
|
|
43
43
|
"reflect-metadata": "*",
|
|
44
44
|
"rxjs": "*",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"vitest": "*"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@hg-ts/validation": "0.5.
|
|
49
|
+
"@hg-ts/validation": "0.5.19"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExecutionMode,
|
|
3
|
+
ExecutionModeModule,
|
|
4
|
+
} from '@hg-ts/execution-mode';
|
|
5
|
+
import {
|
|
6
|
+
Global,
|
|
7
|
+
Module,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
import { ConfigLoader } from './config-loader.js';
|
|
10
|
+
import { FileConfigLoader } from './file.config-loader.js';
|
|
11
|
+
|
|
12
|
+
@Global()
|
|
13
|
+
@Module({
|
|
14
|
+
imports: [ExecutionModeModule],
|
|
15
|
+
providers: [
|
|
16
|
+
{
|
|
17
|
+
provide: ConfigLoader,
|
|
18
|
+
useFactory(env: ExecutionMode): ConfigLoader {
|
|
19
|
+
return new FileConfigLoader({
|
|
20
|
+
envBuilder: true,
|
|
21
|
+
configDir: 'config',
|
|
22
|
+
overrideEnv: env,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
inject: [ExecutionMode],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
exports: [ConfigLoader],
|
|
29
|
+
})
|
|
30
|
+
export class ConfigModule {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './no-base-config.exception.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ZodDto,
|
|
3
|
+
ZodType,
|
|
4
|
+
} from '@hg-ts/validation';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import { ConfigLoader } from './config-loader.js';
|
|
8
|
+
|
|
9
|
+
import { NoBaseConfigException } from './exceptions/index.js';
|
|
10
|
+
import {
|
|
11
|
+
PathBuilder,
|
|
12
|
+
PathBuilderOptions,
|
|
13
|
+
} from './path-builder.js';
|
|
14
|
+
|
|
15
|
+
export type ConfigLoaderOptions = PathBuilderOptions & {
|
|
16
|
+
recursive?: boolean;
|
|
17
|
+
cache?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type CacheItem = {
|
|
21
|
+
schema: ZodType<object>;
|
|
22
|
+
config: object;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class FileConfigLoader implements ConfigLoader {
|
|
26
|
+
private readonly pathBuilder: PathBuilder;
|
|
27
|
+
private readonly options: ConfigLoaderOptions;
|
|
28
|
+
private readonly cacheMap = new Map<string, CacheItem>();
|
|
29
|
+
|
|
30
|
+
public constructor(options: ConfigLoaderOptions = {}) {
|
|
31
|
+
this.pathBuilder = new PathBuilder(options);
|
|
32
|
+
this.options = this.pathBuilder.options;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async load<ConfigType extends object>(dto: ZodDto<ConfigType>, name: string): Promise<ConfigType> {
|
|
36
|
+
const { schema } = dto;
|
|
37
|
+
if (this.cacheMap.has(name)) {
|
|
38
|
+
const cacheItem = this.cacheMap.get(name)!;
|
|
39
|
+
|
|
40
|
+
assert.ok(cacheItem.schema === schema, `cached instance of config "${name}" has another schema`);
|
|
41
|
+
|
|
42
|
+
return cacheItem.config as ConfigType;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rawConfig = await this.loadRawConfig(name);
|
|
46
|
+
|
|
47
|
+
const config = await this.validate(schema, rawConfig);
|
|
48
|
+
|
|
49
|
+
if (this.cacheEnabled) {
|
|
50
|
+
this.cacheMap.set(name, { schema, config });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected async loadRawConfig(name: string): Promise<unknown> {
|
|
57
|
+
const paths = this.pathBuilder.build(name);
|
|
58
|
+
|
|
59
|
+
const configs = await Promise.all(paths.map(async path => this.loadConfigFile(path)));
|
|
60
|
+
|
|
61
|
+
if (!configs[0]) {
|
|
62
|
+
throw new NoBaseConfigException(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.mergeConfigs(configs);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
*
|
|
70
|
+
* @todo Add options to add custom loaders with own ext names (for example yaml)
|
|
71
|
+
* @param {string} path
|
|
72
|
+
* @protected
|
|
73
|
+
*/
|
|
74
|
+
protected async loadConfigFile(path: string): Promise<unknown> {
|
|
75
|
+
try {
|
|
76
|
+
const content = await fs.readFile(path, { encoding: 'utf-8' });
|
|
77
|
+
|
|
78
|
+
return JSON.parse(content) as unknown;
|
|
79
|
+
} catch (error: unknown) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected mergeConfigs(configs: unknown[]): Record<string, unknown> {
|
|
85
|
+
return configs
|
|
86
|
+
.filter(config => config !== null)
|
|
87
|
+
.filter((config): config is Record<string, unknown> => typeof config === 'object')
|
|
88
|
+
.reduce((merged, next) => {
|
|
89
|
+
if (this.isRecursive && merged[this.recursiveRootKey] === true) {
|
|
90
|
+
return merged;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...merged,
|
|
94
|
+
...next,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected async validate<ConfigType>(
|
|
100
|
+
schema: ZodType<ConfigType>,
|
|
101
|
+
config: unknown,
|
|
102
|
+
): Promise<ConfigType> {
|
|
103
|
+
return schema.parseAsync(config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected get cacheEnabled(): boolean {
|
|
107
|
+
if (this.options.cache) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected get isRecursive(): boolean {
|
|
115
|
+
if (this.options.recursive) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// TODO: Get field name from options
|
|
123
|
+
// eslint-disable-next-line @typescript/class-literal-property-style
|
|
124
|
+
protected get recursiveRootKey(): string {
|
|
125
|
+
return 'root';
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ZodDto } from '@hg-ts/validation';
|
|
2
|
+
import { ConfigLoader } from './config-loader.js';
|
|
3
|
+
|
|
4
|
+
export class MockConfigLoader implements ConfigLoader {
|
|
5
|
+
private readonly configMap: Map<string, unknown>;
|
|
6
|
+
|
|
7
|
+
public constructor(configMap: Map<string, unknown>) {
|
|
8
|
+
this.configMap = configMap;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public async load<ConfigType extends object>(dto: ZodDto<ConfigType>, name: string): Promise<ConfigType> {
|
|
12
|
+
const config = this.configMap.get(name);
|
|
13
|
+
|
|
14
|
+
return dto.schema.parse(config);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ExecutionMode } from '@hg-ts/execution-mode';
|
|
2
|
+
import {
|
|
3
|
+
dirname,
|
|
4
|
+
resolve,
|
|
5
|
+
} from 'path';
|
|
6
|
+
|
|
7
|
+
export type PathBuilderOptions = {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* Override default app path
|
|
12
|
+
* @default process.cwd()
|
|
13
|
+
*/
|
|
14
|
+
appPath?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* Additional directory to appPath
|
|
19
|
+
* @default null
|
|
20
|
+
*/
|
|
21
|
+
configDir?: Nullable<string>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* Use environment variable for build config.
|
|
26
|
+
* Get base config, apply ${env} config and, after, apply local config
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
envBuilder?: boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
* Override injected env
|
|
34
|
+
* Uses only with envBuilder: true
|
|
35
|
+
*/
|
|
36
|
+
overrideEnv?: Nullable<ExecutionMode>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
* Used for loading root package config. Useful with overridePostfix option
|
|
41
|
+
* @default null
|
|
42
|
+
* @todo Implement
|
|
43
|
+
*/
|
|
44
|
+
basePostfix?: Nullable<string>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* Useful with basePostfix.
|
|
49
|
+
* For example:
|
|
50
|
+
* You can specify configName to 'hg', basePostfix to 'config' and overridePostfix to 'override'.
|
|
51
|
+
* In this case will be loaded 'bg.config.{ext}' and overrode by 'hg.override.{ext}'.
|
|
52
|
+
* Similar scheme used by many tools like 'docker-compose'.
|
|
53
|
+
* @default null
|
|
54
|
+
* @todo Implement
|
|
55
|
+
*/
|
|
56
|
+
overridePostfix?: Nullable<string>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Apply all configs from `appDir` to `/` until `root` field won't be specified
|
|
60
|
+
* @default false
|
|
61
|
+
* @todo Implement
|
|
62
|
+
*/
|
|
63
|
+
recursive?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export class PathBuilder {
|
|
67
|
+
public readonly options: Required<PathBuilderOptions>;
|
|
68
|
+
private paths: string[];
|
|
69
|
+
|
|
70
|
+
public constructor(options: PathBuilderOptions = {}) {
|
|
71
|
+
this.options = {
|
|
72
|
+
appPath: process.cwd(),
|
|
73
|
+
configDir: null,
|
|
74
|
+
envBuilder: false,
|
|
75
|
+
overrideEnv: null,
|
|
76
|
+
overridePostfix: null,
|
|
77
|
+
basePostfix: null,
|
|
78
|
+
recursive: false,
|
|
79
|
+
...options,
|
|
80
|
+
};
|
|
81
|
+
this.paths = [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public build(name: string): string[] {
|
|
85
|
+
return this.prepareBasePaths()
|
|
86
|
+
.addEnvFolders()
|
|
87
|
+
.addConfigName(name)
|
|
88
|
+
.paths;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
protected prepareBasePaths(): this {
|
|
92
|
+
const { appPath, configDir, recursive } = this.options;
|
|
93
|
+
if (recursive) {
|
|
94
|
+
let nextPath = appPath;
|
|
95
|
+
const basePaths: string[] = [appPath];
|
|
96
|
+
|
|
97
|
+
while (nextPath !== '/') {
|
|
98
|
+
nextPath = dirname(nextPath);
|
|
99
|
+
basePaths.push(nextPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.paths = basePaths;
|
|
103
|
+
|
|
104
|
+
if (configDir) {
|
|
105
|
+
this.mergePaths([configDir]);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
this.paths = [this.buildPathToConfig([appPath, configDir])];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected addEnvFolders(): this {
|
|
115
|
+
const { envBuilder, overrideEnv } = this.options;
|
|
116
|
+
const envFolders: string[] = [];
|
|
117
|
+
|
|
118
|
+
if (!envBuilder) {
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
envFolders.push('base');
|
|
123
|
+
|
|
124
|
+
if (overrideEnv) {
|
|
125
|
+
envFolders.push(overrideEnv.getValue());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
envFolders.push('local');
|
|
129
|
+
|
|
130
|
+
return this.mergePaths(envFolders);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
protected addConfigName(configName: string): this {
|
|
134
|
+
const { overridePostfix, basePostfix } = this.options;
|
|
135
|
+
const baseName = basePostfix ? `${configName}.${basePostfix}.json` : `${configName}.json`;
|
|
136
|
+
const overrideName = overridePostfix ? `${configName}.${overridePostfix}.json` : null;
|
|
137
|
+
|
|
138
|
+
const names = [baseName];
|
|
139
|
+
|
|
140
|
+
if (overrideName) {
|
|
141
|
+
names.push(overrideName);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return this.mergePaths(names);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private mergePaths(additionalParts: string[]): this {
|
|
148
|
+
this.paths = this.paths.flatMap(path => additionalParts.map(part => this.buildPathToConfig([path, part])));
|
|
149
|
+
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private buildPathToConfig(parts: Nullable<string>[]): string {
|
|
154
|
+
const filteredParts = parts.filter((part): part is string => part !== null);
|
|
155
|
+
|
|
156
|
+
return resolve(...filteredParts);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Describe,
|
|
3
|
+
expect,
|
|
4
|
+
ExpectException,
|
|
5
|
+
Suite,
|
|
6
|
+
Test,
|
|
7
|
+
} from '@hg-ts/tests';
|
|
8
|
+
import zod, { z } from '@hg-ts/validation';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import { vi } from 'vitest';
|
|
11
|
+
import { NoBaseConfigException } from '../exceptions/index.js';
|
|
12
|
+
|
|
13
|
+
import { FileConfigLoader } from '../file.config-loader.js';
|
|
14
|
+
|
|
15
|
+
@Describe()
|
|
16
|
+
export class ConfigLoaderTest extends Suite {
|
|
17
|
+
@Test()
|
|
18
|
+
public async simple(): Promise<void> {
|
|
19
|
+
const loader = new FileConfigLoader();
|
|
20
|
+
|
|
21
|
+
const merged = loader['mergeConfigs']([
|
|
22
|
+
{
|
|
23
|
+
a: 'a',
|
|
24
|
+
b: 'b',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
a: 'aa',
|
|
28
|
+
c: 'c',
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
expect(merged['a']).toBe('aa');
|
|
33
|
+
expect(merged['b']).toBe('b');
|
|
34
|
+
expect(merged['c']).toBe('c');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test()
|
|
38
|
+
public async recursive(): Promise<void> {
|
|
39
|
+
const loader = new FileConfigLoader({ recursive: true });
|
|
40
|
+
|
|
41
|
+
const merged = loader['mergeConfigs']([
|
|
42
|
+
{
|
|
43
|
+
a: 'a',
|
|
44
|
+
b: 'b',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
a: 'aa',
|
|
48
|
+
c: 'c',
|
|
49
|
+
root: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
a: 'aaa',
|
|
53
|
+
b: 'bbb',
|
|
54
|
+
c: 'ccc',
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
expect(merged['a']).toBe('aa');
|
|
59
|
+
expect(merged['b']).toBe('b');
|
|
60
|
+
expect(merged['c']).toBe('c');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test()
|
|
64
|
+
public async enforceEnv(): Promise<void> {
|
|
65
|
+
const loader = new FileConfigLoader();
|
|
66
|
+
const configSchema = zod.object({
|
|
67
|
+
number: zod.union([
|
|
68
|
+
zod.number(),
|
|
69
|
+
zod.string(),
|
|
70
|
+
])
|
|
71
|
+
.enforceEnv('SOME_NUMERIC_ENV')
|
|
72
|
+
.transform(value => Number(value))
|
|
73
|
+
.pipe(zod.number()),
|
|
74
|
+
bool: zod.union([
|
|
75
|
+
zod.string(),
|
|
76
|
+
zod.boolean(),
|
|
77
|
+
])
|
|
78
|
+
.enforceEnv('SOME_BOOLEAN_ENV')
|
|
79
|
+
.transformBooleanString(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
process.env['SOME_NUMERIC_ENV'] = '10';
|
|
83
|
+
process.env['SOME_BOOLEAN_ENV'] = 'false';
|
|
84
|
+
|
|
85
|
+
const config = { number: 1, bool: true };
|
|
86
|
+
|
|
87
|
+
const transformed = await loader['validate'](configSchema, config);
|
|
88
|
+
|
|
89
|
+
expect(transformed.number).toBe(10);
|
|
90
|
+
expect(transformed.bool).toBe(false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test()
|
|
94
|
+
public async fullLoadSuccessWithCache(): Promise<void> {
|
|
95
|
+
const schema = z.object({
|
|
96
|
+
a: z.string(),
|
|
97
|
+
b: z.string(),
|
|
98
|
+
c: z.string(),
|
|
99
|
+
d: z.string(),
|
|
100
|
+
});
|
|
101
|
+
const loader = new FileConfigLoader({ recursive: true, cache: true });
|
|
102
|
+
|
|
103
|
+
const loadFileSpy = vi.spyOn(fs, 'readFile');
|
|
104
|
+
const reults = [
|
|
105
|
+
{
|
|
106
|
+
a: 'a',
|
|
107
|
+
b: 'b',
|
|
108
|
+
c: 'c',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
a: 'a',
|
|
112
|
+
b: 'd',
|
|
113
|
+
d: 'b',
|
|
114
|
+
},
|
|
115
|
+
'sdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdf',
|
|
116
|
+
];
|
|
117
|
+
loadFileSpy.mockImplementation(async() => JSON.stringify(reults.shift()!));
|
|
118
|
+
|
|
119
|
+
const result = await loader.load(schema.toClass(), 'test');
|
|
120
|
+
|
|
121
|
+
expect(result).toMatchObject({
|
|
122
|
+
a: 'a',
|
|
123
|
+
b: 'd',
|
|
124
|
+
c: 'c',
|
|
125
|
+
d: 'b',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const cached = await loader.load(schema.toClass(), 'test');
|
|
129
|
+
|
|
130
|
+
expect(cached).toMatchObject(result);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@Test()
|
|
135
|
+
public async fullLoadSuccessWithoutCache(): Promise<void> {
|
|
136
|
+
const schema = z.object({
|
|
137
|
+
a: z.string(),
|
|
138
|
+
b: z.string(),
|
|
139
|
+
c: z.string(),
|
|
140
|
+
d: z.string(),
|
|
141
|
+
});
|
|
142
|
+
const loader = new FileConfigLoader({ recursive: true, cache: false });
|
|
143
|
+
|
|
144
|
+
const loadFileSpy = vi.spyOn(fs, 'readFile');
|
|
145
|
+
const expects: any[] = [
|
|
146
|
+
{
|
|
147
|
+
a: 'a',
|
|
148
|
+
b: 'b',
|
|
149
|
+
c: 'c',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
a: 'a',
|
|
153
|
+
b: 'd',
|
|
154
|
+
d: 'b',
|
|
155
|
+
},
|
|
156
|
+
'sdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdf',
|
|
157
|
+
];
|
|
158
|
+
loadFileSpy.mockImplementation(async() => JSON.stringify(expects.shift()));
|
|
159
|
+
|
|
160
|
+
const result = await loader.load(schema.toClass(), 'test');
|
|
161
|
+
|
|
162
|
+
expects.push({
|
|
163
|
+
a: 'a',
|
|
164
|
+
b: 'b',
|
|
165
|
+
c: 'c',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
a: 'a',
|
|
169
|
+
d: 'd',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result).toMatchObject({
|
|
173
|
+
a: 'a',
|
|
174
|
+
b: 'd',
|
|
175
|
+
c: 'c',
|
|
176
|
+
d: 'b',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const cached = await loader.load(schema.toClass(), 'test');
|
|
180
|
+
|
|
181
|
+
expect(cached).not.toMatchObject(result);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@Test()
|
|
185
|
+
@ExpectException(NoBaseConfigException)
|
|
186
|
+
public async failOnNoBaseConfig(): Promise<void> {
|
|
187
|
+
const schema = z.object({
|
|
188
|
+
a: z.string(),
|
|
189
|
+
b: z.string(),
|
|
190
|
+
c: z.string(),
|
|
191
|
+
d: z.string(),
|
|
192
|
+
});
|
|
193
|
+
const loader = new FileConfigLoader({ recursive: true, cache: true });
|
|
194
|
+
|
|
195
|
+
const loadFileSpy = vi.spyOn(fs, 'readFile');
|
|
196
|
+
loadFileSpy.mockImplementation(() => {
|
|
197
|
+
throw new Error('file not found');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await loader.load(schema.toClass(), 'test');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExecutionModeVariants,
|
|
3
|
+
MockExecutionMode,
|
|
4
|
+
} from '@hg-ts/execution-mode';
|
|
5
|
+
import {
|
|
6
|
+
Describe,
|
|
7
|
+
expect,
|
|
8
|
+
Suite,
|
|
9
|
+
Test,
|
|
10
|
+
} from '@hg-ts/tests';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
PathBuilder,
|
|
14
|
+
PathBuilderOptions,
|
|
15
|
+
} from '../path-builder.js';
|
|
16
|
+
|
|
17
|
+
@Describe()
|
|
18
|
+
export class PathBuilderTest extends Suite {
|
|
19
|
+
@Test()
|
|
20
|
+
public async simple(): Promise<void> {
|
|
21
|
+
const paths = this.getPaths();
|
|
22
|
+
|
|
23
|
+
expect(paths).toHaveLength(1);
|
|
24
|
+
expect(paths[0]).toBe('/tmp/example.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test()
|
|
28
|
+
public async recursive(): Promise<void> {
|
|
29
|
+
const paths = this.getPaths({ recursive: true });
|
|
30
|
+
|
|
31
|
+
expect(paths).toHaveLength(2);
|
|
32
|
+
expect(paths[0]).toBe('/tmp/example.json');
|
|
33
|
+
expect(paths[1]).toBe('/example.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test()
|
|
37
|
+
public async full(): Promise<void> {
|
|
38
|
+
const paths = this.getPaths({
|
|
39
|
+
recursive: true,
|
|
40
|
+
overridePostfix: 'override',
|
|
41
|
+
basePostfix: 'config',
|
|
42
|
+
envBuilder: true,
|
|
43
|
+
overrideEnv: new MockExecutionMode(ExecutionModeVariants.DEMO),
|
|
44
|
+
configDir: 'config',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let index = 0;
|
|
48
|
+
|
|
49
|
+
expect(paths).toHaveLength(12);
|
|
50
|
+
expect(paths[index++]).toBe('/tmp/config/base/example.config.json');
|
|
51
|
+
expect(paths[index++]).toBe('/tmp/config/base/example.override.json');
|
|
52
|
+
expect(paths[index++]).toBe('/tmp/config/demo/example.config.json');
|
|
53
|
+
expect(paths[index++]).toBe('/tmp/config/demo/example.override.json');
|
|
54
|
+
expect(paths[index++]).toBe('/tmp/config/local/example.config.json');
|
|
55
|
+
expect(paths[index++]).toBe('/tmp/config/local/example.override.json');
|
|
56
|
+
|
|
57
|
+
expect(paths[index++]).toBe('/config/base/example.config.json');
|
|
58
|
+
expect(paths[index++]).toBe('/config/base/example.override.json');
|
|
59
|
+
expect(paths[index++]).toBe('/config/demo/example.config.json');
|
|
60
|
+
expect(paths[index++]).toBe('/config/demo/example.override.json');
|
|
61
|
+
expect(paths[index++]).toBe('/config/local/example.config.json');
|
|
62
|
+
expect(paths[index++]).toBe('/config/local/example.override.json');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test()
|
|
66
|
+
public async configDir(): Promise<void> {
|
|
67
|
+
const paths = this.getPaths({ configDir: 'config' });
|
|
68
|
+
|
|
69
|
+
expect(paths).toHaveLength(1);
|
|
70
|
+
expect(paths[0]).toBe('/tmp/config/example.json');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test()
|
|
74
|
+
public async envBuilder(): Promise<void> {
|
|
75
|
+
const paths = this.getPaths({ envBuilder: true });
|
|
76
|
+
|
|
77
|
+
expect(paths).toHaveLength(2);
|
|
78
|
+
expect(paths[0]).toBe('/tmp/base/example.json');
|
|
79
|
+
expect(paths[1]).toBe('/tmp/local/example.json');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test()
|
|
83
|
+
public async envBuilderWithOverrideProd(): Promise<void> {
|
|
84
|
+
const paths = this.getPaths({ envBuilder: true, overrideEnv: new MockExecutionMode(ExecutionModeVariants.PROD) });
|
|
85
|
+
|
|
86
|
+
expect(paths).toHaveLength(3);
|
|
87
|
+
expect(paths[0]).toBe('/tmp/base/example.json');
|
|
88
|
+
expect(paths[1]).toBe('/tmp/prod/example.json');
|
|
89
|
+
expect(paths[2]).toBe('/tmp/local/example.json');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test()
|
|
93
|
+
public async envBuilderWithOverrideDev(): Promise<void> {
|
|
94
|
+
const paths = this.getPaths({ envBuilder: true, overrideEnv: new MockExecutionMode(ExecutionModeVariants.DEV) });
|
|
95
|
+
|
|
96
|
+
expect(paths).toHaveLength(3);
|
|
97
|
+
expect(paths[0]).toBe('/tmp/base/example.json');
|
|
98
|
+
expect(paths[1]).toBe('/tmp/dev/example.json');
|
|
99
|
+
expect(paths[2]).toBe('/tmp/local/example.json');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test()
|
|
103
|
+
public async basePostfix(): Promise<void> {
|
|
104
|
+
const paths = this.getPaths({ basePostfix: 'config' });
|
|
105
|
+
|
|
106
|
+
expect(paths).toHaveLength(1);
|
|
107
|
+
expect(paths[0]).toBe('/tmp/example.config.json');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Test()
|
|
111
|
+
public async basePostfixWithOverridePostfix(): Promise<void> {
|
|
112
|
+
const paths = this.getPaths({ basePostfix: 'config', overridePostfix: 'override' });
|
|
113
|
+
|
|
114
|
+
expect(paths).toHaveLength(2);
|
|
115
|
+
expect(paths[0]).toBe('/tmp/example.config.json');
|
|
116
|
+
expect(paths[1]).toBe('/tmp/example.override.json');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test()
|
|
120
|
+
public async overridePostfix(): Promise<void> {
|
|
121
|
+
const paths = this.getPaths({ overridePostfix: 'override' });
|
|
122
|
+
|
|
123
|
+
expect(paths).toHaveLength(2);
|
|
124
|
+
expect(paths[0]).toBe('/tmp/example.json');
|
|
125
|
+
expect(paths[1]).toBe('/tmp/example.override.json');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private getPaths(options: Omit<PathBuilderOptions, 'appPath'> = {}): string[] {
|
|
129
|
+
const builder = new PathBuilder({ appPath: '/tmp', ...options });
|
|
130
|
+
|
|
131
|
+
return builder.build('example');
|
|
132
|
+
}
|
|
133
|
+
}
|