@grest-ts/config 0.0.6 → 0.0.8
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/LICENSE +21 -21
- package/README.md +57 -57
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/GGConfig.ts +69 -69
- package/src/GGConfigKey.ts +106 -106
- package/src/GGConfigLocator.ts +112 -112
- package/src/GGConfigStore.ts +100 -100
- package/src/GG_CONFIG.ts +4 -4
- package/src/assureValidConfigPath.spec.ts +140 -140
- package/src/assureValidConfigPath.ts +33 -33
- package/src/index-node.ts +10 -10
- package/src/keys/GGResource.ts +20 -20
- package/src/keys/GGSecret.ts +20 -20
- package/src/keys/GGSetting.ts +28 -28
- package/src/stores/GGConfigStoreFile.ts +69 -69
- package/src/stores/GGConfigStoreLocal.ts +109 -109
package/src/GGConfigStore.ts
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
import {GGConfigKey} from "./GGConfigKey";
|
|
2
|
-
import {GG_CONFIG} from "./GG_CONFIG";
|
|
3
|
-
|
|
4
|
-
export type ConfigUpdateCallback = (value: any) => void | Promise<void>;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Important when extending: Use resolveValue() to resolve and validate config values.
|
|
8
|
-
*/
|
|
9
|
-
export abstract class GGConfigStore<Key extends GGConfigKey> {
|
|
10
|
-
|
|
11
|
-
#keys: Key[] = []
|
|
12
|
-
public get keys(): readonly Key[] { return this.#keys }
|
|
13
|
-
readonly #watchers: Map<GGConfigKey, ConfigUpdateCallback[]> = new Map()
|
|
14
|
-
|
|
15
|
-
protected isStarted = false;
|
|
16
|
-
|
|
17
|
-
public get started(): boolean {
|
|
18
|
-
return this.isStarted;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
public setKeys(keys: readonly Key[]): void {
|
|
22
|
-
if (this.isStarted) {
|
|
23
|
-
throw new Error(`Cannot set keys after store is started: ${this.constructor.name}`);
|
|
24
|
-
}
|
|
25
|
-
this.#keys.push(...keys);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public abstract getValue<T>(key: GGConfigKey<T>): T
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolves the final value for a config key: uses storeValue if present, falls back to key.getDefault(),
|
|
32
|
-
* then validates against schema. If the resolved value is undefined and the schema doesn't allow it,
|
|
33
|
-
* validation will fail — this is how missing required keys are caught.
|
|
34
|
-
*/
|
|
35
|
-
protected resolveValue<T>(key: GGConfigKey<T>, storeValue: unknown, isInitialLoad: boolean = false): T {
|
|
36
|
-
const val = storeValue !== undefined ? storeValue : key.getDefault();
|
|
37
|
-
const check = key.schema.safeParse(val);
|
|
38
|
-
if (check.success === false) {
|
|
39
|
-
const errors = JSON.stringify(check.issues.toJSON(), null, 2).replaceAll("\n", "\n\t\t")
|
|
40
|
-
throw new Error(`Config validation failed for "${key.name}" during ${isInitialLoad ? "initial-load" : "reload"}.\n\tDefined at:\n\t\t${key.definedAt}\n\tIssues:\n\t\t${errors}`);
|
|
41
|
-
} else {
|
|
42
|
-
return check.value as T;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public async start(): Promise<void> {
|
|
47
|
-
if (this.isStarted === true) {
|
|
48
|
-
throw new Error(`Config store already started: ${this.constructor.name}`);
|
|
49
|
-
}
|
|
50
|
-
this.isStarted = true;
|
|
51
|
-
Object.freeze(this.#keys);
|
|
52
|
-
|
|
53
|
-
this.keys.forEach(key => {
|
|
54
|
-
this.getValue(key); // Get every value so that config values are definitely initialized (or they would throw)
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public async teardown(): Promise<void> {
|
|
59
|
-
this.#watchers.clear();
|
|
60
|
-
this.isStarted = false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public watch(key: GGConfigKey, callback: ConfigUpdateCallback): () => void {
|
|
64
|
-
if (!this.#watchers.has(key)) {
|
|
65
|
-
this.#watchers.set(key, []);
|
|
66
|
-
}
|
|
67
|
-
this.#watchers.get(key)!.push(callback);
|
|
68
|
-
return () => {
|
|
69
|
-
const list = this.#watchers.get(key);
|
|
70
|
-
if (!list) return;
|
|
71
|
-
const index = list.indexOf(callback);
|
|
72
|
-
if (index >= 0) list.splice(index, 1);
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
public async notify(key: GGConfigKey) {
|
|
77
|
-
if (this.#watchers.has(key)) {
|
|
78
|
-
const promises: Promise<void>[] = [];
|
|
79
|
-
const newValue = this.getValue(key);
|
|
80
|
-
this.#watchers.get(key).forEach(callback => {
|
|
81
|
-
try {
|
|
82
|
-
const result = callback(newValue);
|
|
83
|
-
if (result instanceof Promise) {
|
|
84
|
-
promises.push(result);
|
|
85
|
-
}
|
|
86
|
-
} catch (err) {
|
|
87
|
-
GG_CONFIG.get().onNotifyError(err);
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
if (promises.length > 0) {
|
|
91
|
-
const results = await Promise.allSettled(promises);
|
|
92
|
-
for (const result of results) {
|
|
93
|
-
if (result.status === 'rejected') {
|
|
94
|
-
GG_CONFIG.get().onNotifyError(result.reason);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
1
|
+
import {GGConfigKey} from "./GGConfigKey";
|
|
2
|
+
import {GG_CONFIG} from "./GG_CONFIG";
|
|
3
|
+
|
|
4
|
+
export type ConfigUpdateCallback = (value: any) => void | Promise<void>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Important when extending: Use resolveValue() to resolve and validate config values.
|
|
8
|
+
*/
|
|
9
|
+
export abstract class GGConfigStore<Key extends GGConfigKey> {
|
|
10
|
+
|
|
11
|
+
#keys: Key[] = []
|
|
12
|
+
public get keys(): readonly Key[] { return this.#keys }
|
|
13
|
+
readonly #watchers: Map<GGConfigKey, ConfigUpdateCallback[]> = new Map()
|
|
14
|
+
|
|
15
|
+
protected isStarted = false;
|
|
16
|
+
|
|
17
|
+
public get started(): boolean {
|
|
18
|
+
return this.isStarted;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public setKeys(keys: readonly Key[]): void {
|
|
22
|
+
if (this.isStarted) {
|
|
23
|
+
throw new Error(`Cannot set keys after store is started: ${this.constructor.name}`);
|
|
24
|
+
}
|
|
25
|
+
this.#keys.push(...keys);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public abstract getValue<T>(key: GGConfigKey<T>): T
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the final value for a config key: uses storeValue if present, falls back to key.getDefault(),
|
|
32
|
+
* then validates against schema. If the resolved value is undefined and the schema doesn't allow it,
|
|
33
|
+
* validation will fail — this is how missing required keys are caught.
|
|
34
|
+
*/
|
|
35
|
+
protected resolveValue<T>(key: GGConfigKey<T>, storeValue: unknown, isInitialLoad: boolean = false): T {
|
|
36
|
+
const val = storeValue !== undefined ? storeValue : key.getDefault();
|
|
37
|
+
const check = key.schema.safeParse(val);
|
|
38
|
+
if (check.success === false) {
|
|
39
|
+
const errors = JSON.stringify(check.issues.toJSON(), null, 2).replaceAll("\n", "\n\t\t")
|
|
40
|
+
throw new Error(`Config validation failed for "${key.name}" during ${isInitialLoad ? "initial-load" : "reload"}.\n\tDefined at:\n\t\t${key.definedAt}\n\tIssues:\n\t\t${errors}`);
|
|
41
|
+
} else {
|
|
42
|
+
return check.value as T;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async start(): Promise<void> {
|
|
47
|
+
if (this.isStarted === true) {
|
|
48
|
+
throw new Error(`Config store already started: ${this.constructor.name}`);
|
|
49
|
+
}
|
|
50
|
+
this.isStarted = true;
|
|
51
|
+
Object.freeze(this.#keys);
|
|
52
|
+
|
|
53
|
+
this.keys.forEach(key => {
|
|
54
|
+
this.getValue(key); // Get every value so that config values are definitely initialized (or they would throw)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public async teardown(): Promise<void> {
|
|
59
|
+
this.#watchers.clear();
|
|
60
|
+
this.isStarted = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public watch(key: GGConfigKey, callback: ConfigUpdateCallback): () => void {
|
|
64
|
+
if (!this.#watchers.has(key)) {
|
|
65
|
+
this.#watchers.set(key, []);
|
|
66
|
+
}
|
|
67
|
+
this.#watchers.get(key)!.push(callback);
|
|
68
|
+
return () => {
|
|
69
|
+
const list = this.#watchers.get(key);
|
|
70
|
+
if (!list) return;
|
|
71
|
+
const index = list.indexOf(callback);
|
|
72
|
+
if (index >= 0) list.splice(index, 1);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async notify(key: GGConfigKey) {
|
|
77
|
+
if (this.#watchers.has(key)) {
|
|
78
|
+
const promises: Promise<void>[] = [];
|
|
79
|
+
const newValue = this.getValue(key);
|
|
80
|
+
this.#watchers.get(key).forEach(callback => {
|
|
81
|
+
try {
|
|
82
|
+
const result = callback(newValue);
|
|
83
|
+
if (result instanceof Promise) {
|
|
84
|
+
promises.push(result);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
GG_CONFIG.get().onNotifyError(err);
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
if (promises.length > 0) {
|
|
91
|
+
const results = await Promise.allSettled(promises);
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
if (result.status === 'rejected') {
|
|
94
|
+
GG_CONFIG.get().onNotifyError(result.reason);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/GG_CONFIG.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {GGLocatorKey} from "@grest-ts/locator";
|
|
2
|
-
import type {GGConfigLocator} from "./GGConfigLocator";
|
|
3
|
-
|
|
4
|
-
export const GG_CONFIG = new GGLocatorKey<GGConfigLocator<any>>("GGConfig");
|
|
1
|
+
import {GGLocatorKey} from "@grest-ts/locator";
|
|
2
|
+
import type {GGConfigLocator} from "./GGConfigLocator";
|
|
3
|
+
|
|
4
|
+
export const GG_CONFIG = new GGLocatorKey<GGConfigLocator<any>>("GGConfig");
|
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
import {describe, it, expect} from 'vitest';
|
|
2
|
-
import {assureValidConfigPath} from './assureValidConfigPath';
|
|
3
|
-
|
|
4
|
-
describe('assureValidConfigPath', () => {
|
|
5
|
-
|
|
6
|
-
describe('valid paths', () => {
|
|
7
|
-
it('accepts simple path', () => {
|
|
8
|
-
expect(() => assureValidConfigPath('/app/setting')).not.toThrow();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('accepts path with multiple segments', () => {
|
|
12
|
-
expect(() => assureValidConfigPath('/app/db/host')).not.toThrow();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('accepts path with underscores', () => {
|
|
16
|
-
expect(() => assureValidConfigPath('/my_app/my_setting')).not.toThrow();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('accepts path with numbers after letter', () => {
|
|
20
|
-
expect(() => assureValidConfigPath('/app1/setting2')).not.toThrow();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('accepts mixed valid characters', () => {
|
|
24
|
-
expect(() => assureValidConfigPath('/my_app_v2/db_connection_pool_size')).not.toThrow();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('accepts uppercase letters', () => {
|
|
28
|
-
expect(() => assureValidConfigPath('/MyApp/DbHost')).not.toThrow();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('accepts single segment', () => {
|
|
32
|
-
expect(() => assureValidConfigPath('/setting')).not.toThrow();
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('must start with slash', () => {
|
|
37
|
-
it('rejects path without leading slash', () => {
|
|
38
|
-
expect(() => assureValidConfigPath('app/setting')).toThrow("must start with '/'");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('rejects single word without slash', () => {
|
|
42
|
-
expect(() => assureValidConfigPath('setting')).toThrow("must start with '/'");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('cannot end with slash', () => {
|
|
47
|
-
it('rejects path with trailing slash', () => {
|
|
48
|
-
expect(() => assureValidConfigPath('/app/setting/')).toThrow("cannot end with '/'");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('rejects root path (just slash)', () => {
|
|
52
|
-
expect(() => assureValidConfigPath('/')).toThrow("cannot end with '/'");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('no double slashes', () => {
|
|
57
|
-
it('rejects double slash in middle', () => {
|
|
58
|
-
expect(() => assureValidConfigPath('/app//setting')).toThrow("cannot contain '//'");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('rejects double slash at start', () => {
|
|
62
|
-
expect(() => assureValidConfigPath('//app/setting')).toThrow("cannot contain '//'");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('rejects multiple double slashes', () => {
|
|
66
|
-
expect(() => assureValidConfigPath('/app//db//host')).toThrow("cannot contain '//'");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('invalid characters', () => {
|
|
71
|
-
it('rejects dots', () => {
|
|
72
|
-
expect(() => assureValidConfigPath('/app/db.host')).toThrow('invalid characters');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('rejects hyphens', () => {
|
|
76
|
-
expect(() => assureValidConfigPath('/my-app/setting')).toThrow('invalid characters');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('rejects spaces', () => {
|
|
80
|
-
expect(() => assureValidConfigPath('/app/my setting')).toThrow('invalid characters');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('rejects special characters @', () => {
|
|
84
|
-
expect(() => assureValidConfigPath('/app@host/setting')).toThrow('invalid characters');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('rejects special characters #', () => {
|
|
88
|
-
expect(() => assureValidConfigPath('/app#1/setting')).toThrow('invalid characters');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('rejects special characters $', () => {
|
|
92
|
-
expect(() => assureValidConfigPath('/app/$setting')).toThrow('invalid characters');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('rejects special characters %', () => {
|
|
96
|
-
expect(() => assureValidConfigPath('/app/100%')).toThrow('invalid characters');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('rejects colon', () => {
|
|
100
|
-
expect(() => assureValidConfigPath('/app:setting')).toThrow('invalid characters');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('rejects backslash', () => {
|
|
104
|
-
expect(() => assureValidConfigPath('/app\\setting')).toThrow('invalid characters');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe('segments must start with letter', () => {
|
|
109
|
-
it('rejects segment starting with number', () => {
|
|
110
|
-
expect(() => assureValidConfigPath('/app/1setting')).toThrow('must start with a letter');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('rejects segment starting with underscore', () => {
|
|
114
|
-
expect(() => assureValidConfigPath('/app/_setting')).toThrow('must start with a letter');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('rejects first segment starting with number', () => {
|
|
118
|
-
expect(() => assureValidConfigPath('/1app/setting')).toThrow('must start with a letter');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('accepts segment with numbers after first letter', () => {
|
|
122
|
-
expect(() => assureValidConfigPath('/app/s1e2t3')).not.toThrow();
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('max length', () => {
|
|
127
|
-
it('accepts path at exactly 2048 characters', () => {
|
|
128
|
-
const path = '/' + 'a'.repeat(2047);
|
|
129
|
-
expect(path.length).toBe(2048);
|
|
130
|
-
expect(() => assureValidConfigPath(path)).not.toThrow();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('rejects path exceeding 2048 characters', () => {
|
|
134
|
-
const path = '/' + 'a'.repeat(2048);
|
|
135
|
-
expect(path.length).toBe(2049);
|
|
136
|
-
expect(() => assureValidConfigPath(path)).toThrow('exceeds 2048 characters');
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
});
|
|
1
|
+
import {describe, it, expect} from 'vitest';
|
|
2
|
+
import {assureValidConfigPath} from './assureValidConfigPath';
|
|
3
|
+
|
|
4
|
+
describe('assureValidConfigPath', () => {
|
|
5
|
+
|
|
6
|
+
describe('valid paths', () => {
|
|
7
|
+
it('accepts simple path', () => {
|
|
8
|
+
expect(() => assureValidConfigPath('/app/setting')).not.toThrow();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('accepts path with multiple segments', () => {
|
|
12
|
+
expect(() => assureValidConfigPath('/app/db/host')).not.toThrow();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('accepts path with underscores', () => {
|
|
16
|
+
expect(() => assureValidConfigPath('/my_app/my_setting')).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('accepts path with numbers after letter', () => {
|
|
20
|
+
expect(() => assureValidConfigPath('/app1/setting2')).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('accepts mixed valid characters', () => {
|
|
24
|
+
expect(() => assureValidConfigPath('/my_app_v2/db_connection_pool_size')).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('accepts uppercase letters', () => {
|
|
28
|
+
expect(() => assureValidConfigPath('/MyApp/DbHost')).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('accepts single segment', () => {
|
|
32
|
+
expect(() => assureValidConfigPath('/setting')).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('must start with slash', () => {
|
|
37
|
+
it('rejects path without leading slash', () => {
|
|
38
|
+
expect(() => assureValidConfigPath('app/setting')).toThrow("must start with '/'");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects single word without slash', () => {
|
|
42
|
+
expect(() => assureValidConfigPath('setting')).toThrow("must start with '/'");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('cannot end with slash', () => {
|
|
47
|
+
it('rejects path with trailing slash', () => {
|
|
48
|
+
expect(() => assureValidConfigPath('/app/setting/')).toThrow("cannot end with '/'");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects root path (just slash)', () => {
|
|
52
|
+
expect(() => assureValidConfigPath('/')).toThrow("cannot end with '/'");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('no double slashes', () => {
|
|
57
|
+
it('rejects double slash in middle', () => {
|
|
58
|
+
expect(() => assureValidConfigPath('/app//setting')).toThrow("cannot contain '//'");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects double slash at start', () => {
|
|
62
|
+
expect(() => assureValidConfigPath('//app/setting')).toThrow("cannot contain '//'");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('rejects multiple double slashes', () => {
|
|
66
|
+
expect(() => assureValidConfigPath('/app//db//host')).toThrow("cannot contain '//'");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('invalid characters', () => {
|
|
71
|
+
it('rejects dots', () => {
|
|
72
|
+
expect(() => assureValidConfigPath('/app/db.host')).toThrow('invalid characters');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects hyphens', () => {
|
|
76
|
+
expect(() => assureValidConfigPath('/my-app/setting')).toThrow('invalid characters');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects spaces', () => {
|
|
80
|
+
expect(() => assureValidConfigPath('/app/my setting')).toThrow('invalid characters');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects special characters @', () => {
|
|
84
|
+
expect(() => assureValidConfigPath('/app@host/setting')).toThrow('invalid characters');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('rejects special characters #', () => {
|
|
88
|
+
expect(() => assureValidConfigPath('/app#1/setting')).toThrow('invalid characters');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('rejects special characters $', () => {
|
|
92
|
+
expect(() => assureValidConfigPath('/app/$setting')).toThrow('invalid characters');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('rejects special characters %', () => {
|
|
96
|
+
expect(() => assureValidConfigPath('/app/100%')).toThrow('invalid characters');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('rejects colon', () => {
|
|
100
|
+
expect(() => assureValidConfigPath('/app:setting')).toThrow('invalid characters');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects backslash', () => {
|
|
104
|
+
expect(() => assureValidConfigPath('/app\\setting')).toThrow('invalid characters');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('segments must start with letter', () => {
|
|
109
|
+
it('rejects segment starting with number', () => {
|
|
110
|
+
expect(() => assureValidConfigPath('/app/1setting')).toThrow('must start with a letter');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('rejects segment starting with underscore', () => {
|
|
114
|
+
expect(() => assureValidConfigPath('/app/_setting')).toThrow('must start with a letter');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('rejects first segment starting with number', () => {
|
|
118
|
+
expect(() => assureValidConfigPath('/1app/setting')).toThrow('must start with a letter');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('accepts segment with numbers after first letter', () => {
|
|
122
|
+
expect(() => assureValidConfigPath('/app/s1e2t3')).not.toThrow();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('max length', () => {
|
|
127
|
+
it('accepts path at exactly 2048 characters', () => {
|
|
128
|
+
const path = '/' + 'a'.repeat(2047);
|
|
129
|
+
expect(path.length).toBe(2048);
|
|
130
|
+
expect(() => assureValidConfigPath(path)).not.toThrow();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('rejects path exceeding 2048 characters', () => {
|
|
134
|
+
const path = '/' + 'a'.repeat(2048);
|
|
135
|
+
expect(path.length).toBe(2049);
|
|
136
|
+
expect(() => assureValidConfigPath(path)).toThrow('exceeds 2048 characters');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
});
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates config key name for compatibility with external providers.
|
|
3
|
-
* Rules:
|
|
4
|
-
* - Must start with forward slash
|
|
5
|
-
* - Only letters, numbers, underscore, forward slash allowed
|
|
6
|
-
* - Each segment (word) must start with a letter
|
|
7
|
-
* - No double slashes
|
|
8
|
-
* - Cannot end with slash
|
|
9
|
-
* - Max 2048 characters (AWS limit)
|
|
10
|
-
*/
|
|
11
|
-
export function assureValidConfigPath(path: string) {
|
|
12
|
-
if (path.length > 2048) {
|
|
13
|
-
throw new Error(`Config key name exceeds 2048 characters: ${path}`);
|
|
14
|
-
}
|
|
15
|
-
if (!path.startsWith('/')) {
|
|
16
|
-
throw new Error(`Config key name must start with '/': ${path}`);
|
|
17
|
-
}
|
|
18
|
-
if (path.endsWith('/')) {
|
|
19
|
-
throw new Error(`Config key name cannot end with '/': ${path}`);
|
|
20
|
-
}
|
|
21
|
-
if (path.includes('//')) {
|
|
22
|
-
throw new Error(`Config key name cannot contain '//': ${path}`);
|
|
23
|
-
}
|
|
24
|
-
if (!/^[a-zA-Z0-9_\/]+$/.test(path)) {
|
|
25
|
-
throw new Error(`Config key name contains invalid characters (allowed: a-z, A-Z, 0-9, _, /): ${path}`);
|
|
26
|
-
}
|
|
27
|
-
// Each segment must start with a letter
|
|
28
|
-
const segments = path.split('/').filter(s => s.length > 0);
|
|
29
|
-
for (const segment of segments) {
|
|
30
|
-
if (!/^[a-zA-Z]/.test(segment)) {
|
|
31
|
-
throw new Error(`Each path segment must start with a letter: ${path}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Validates config key name for compatibility with external providers.
|
|
3
|
+
* Rules:
|
|
4
|
+
* - Must start with forward slash
|
|
5
|
+
* - Only letters, numbers, underscore, forward slash allowed
|
|
6
|
+
* - Each segment (word) must start with a letter
|
|
7
|
+
* - No double slashes
|
|
8
|
+
* - Cannot end with slash
|
|
9
|
+
* - Max 2048 characters (AWS limit)
|
|
10
|
+
*/
|
|
11
|
+
export function assureValidConfigPath(path: string) {
|
|
12
|
+
if (path.length > 2048) {
|
|
13
|
+
throw new Error(`Config key name exceeds 2048 characters: ${path}`);
|
|
14
|
+
}
|
|
15
|
+
if (!path.startsWith('/')) {
|
|
16
|
+
throw new Error(`Config key name must start with '/': ${path}`);
|
|
17
|
+
}
|
|
18
|
+
if (path.endsWith('/')) {
|
|
19
|
+
throw new Error(`Config key name cannot end with '/': ${path}`);
|
|
20
|
+
}
|
|
21
|
+
if (path.includes('//')) {
|
|
22
|
+
throw new Error(`Config key name cannot contain '//': ${path}`);
|
|
23
|
+
}
|
|
24
|
+
if (!/^[a-zA-Z0-9_\/]+$/.test(path)) {
|
|
25
|
+
throw new Error(`Config key name contains invalid characters (allowed: a-z, A-Z, 0-9, _, /): ${path}`);
|
|
26
|
+
}
|
|
27
|
+
// Each segment must start with a letter
|
|
28
|
+
const segments = path.split('/').filter(s => s.length > 0);
|
|
29
|
+
for (const segment of segments) {
|
|
30
|
+
if (!/^[a-zA-Z]/.test(segment)) {
|
|
31
|
+
throw new Error(`Each path segment must start with a letter: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
34
|
}
|
package/src/index-node.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export * from './GGConfig';
|
|
2
|
-
export * from './GG_CONFIG';
|
|
3
|
-
export * from './GGConfigLocator';
|
|
4
|
-
export * from "./GGConfigKey";
|
|
5
|
-
export * from "./GGConfigStore";
|
|
6
|
-
export * from "./stores/GGConfigStoreFile";
|
|
7
|
-
export * from "./stores/GGConfigStoreLocal";
|
|
8
|
-
export * from "./keys/GGSetting";
|
|
9
|
-
export * from "./keys/GGSecret";
|
|
10
|
-
export * from "./keys/GGResource";
|
|
1
|
+
export * from './GGConfig';
|
|
2
|
+
export * from './GG_CONFIG';
|
|
3
|
+
export * from './GGConfigLocator';
|
|
4
|
+
export * from "./GGConfigKey";
|
|
5
|
+
export * from "./GGConfigStore";
|
|
6
|
+
export * from "./stores/GGConfigStoreFile";
|
|
7
|
+
export * from "./stores/GGConfigStoreLocal";
|
|
8
|
+
export * from "./keys/GGSetting";
|
|
9
|
+
export * from "./keys/GGSecret";
|
|
10
|
+
export * from "./keys/GGResource";
|
|
11
11
|
export {assureValidConfigPath} from "./assureValidConfigPath";
|
package/src/keys/GGResource.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {GGConfigKey} from "../GGConfigKey";
|
|
2
|
-
import {GGValidator} from "@grest-ts/schema";
|
|
3
|
-
|
|
4
|
-
export class GGResource<T> extends GGConfigKey<T> {
|
|
5
|
-
|
|
6
|
-
public static readonly NAME = "[GGResource]";
|
|
7
|
-
|
|
8
|
-
constructor(name: string, schema: GGValidator<T>, description: string) {
|
|
9
|
-
super(name, schema, description);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
public getStoreKey(): string {
|
|
13
|
-
return GGResource.NAME;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
public get(): T {
|
|
17
|
-
return this.getValue();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
}
|
|
1
|
+
import {GGConfigKey} from "../GGConfigKey";
|
|
2
|
+
import {GGValidator} from "@grest-ts/schema";
|
|
3
|
+
|
|
4
|
+
export class GGResource<T> extends GGConfigKey<T> {
|
|
5
|
+
|
|
6
|
+
public static readonly NAME = "[GGResource]";
|
|
7
|
+
|
|
8
|
+
constructor(name: string, schema: GGValidator<T>, description: string) {
|
|
9
|
+
super(name, schema, description);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public getStoreKey(): string {
|
|
13
|
+
return GGResource.NAME;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public get(): T {
|
|
17
|
+
return this.getValue();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|