@openmrs/esm-config 4.0.0-pre.0 → 4.0.0-pre.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.
- package/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +9 -8
- package/.turbo/turbo-typescript.log +2 -2
- package/dist/__mocks__/openmrs-esm-state.mock.d.ts +12 -0
- package/dist/openmrs-esm-module-config.js +1 -1
- package/dist/openmrs-esm-module-config.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/module-config/module-config.d.ts +57 -0
- package/dist/src/module-config/module-config.test.d.ts +1 -0
- package/dist/src/module-config/state.d.ts +109 -0
- package/dist/src/navigation/interpolate-string.d.ts +34 -0
- package/dist/src/navigation/interpolate-string.test.d.ts +1 -0
- package/dist/src/navigation/navigate.d.ts +26 -0
- package/dist/src/navigation/navigate.test.d.ts +1 -0
- package/dist/src/public.d.ts +7 -0
- package/dist/src/types.d.ts +49 -0
- package/dist/src/validators/type-validators.d.ts +6 -0
- package/dist/src/validators/type-validators.test.d.ts +1 -0
- package/dist/src/validators/validator.d.ts +24 -0
- package/dist/src/validators/validators.d.ts +32 -0
- package/dist/src/validators/validators.test.d.ts +1 -0
- package/package.json +4 -4
- package/src/module-config/module-config.test.ts +63 -5
- package/src/module-config/module-config.ts +100 -42
- package/src/navigation/interpolate-string.test.ts +3 -3
- package/src/navigation/interpolate-string.ts +10 -2
- package/src/navigation/navigate.ts +10 -4
- package/src/validators/validator.ts +27 -2
- package/src/validators/validators.test.ts +18 -1
- package/src/validators/validators.ts +13 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Config, ConfigObject, ConfigSchema, ExtensionSlotConfigObject, ProvidedConfig } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Internal store
|
|
4
|
+
* A store of the inputs and internal state
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface ConfigInternalStore {
|
|
8
|
+
/** Configs added using the `provide` function */
|
|
9
|
+
providedConfigs: Array<ProvidedConfig>;
|
|
10
|
+
/** An object with module names for keys and schemas for values */
|
|
11
|
+
schemas: Record<string, ConfigSchema>;
|
|
12
|
+
/** Whether to use dev defaults or not */
|
|
13
|
+
devDefaultsAreOn: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare const configInternalStore: import("unistore").Store<ConfigInternalStore>;
|
|
19
|
+
/**
|
|
20
|
+
* Temporary config
|
|
21
|
+
* LocalStorage-based config used by the implementer tools
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export interface TemporaryConfigStore {
|
|
25
|
+
config: Config;
|
|
26
|
+
}
|
|
27
|
+
/** @internal */
|
|
28
|
+
export declare const temporaryConfigStore: import("unistore").Store<TemporaryConfigStore>;
|
|
29
|
+
/**
|
|
30
|
+
* Config-side extension store
|
|
31
|
+
* Just what esm-config needs to know about extension state. This
|
|
32
|
+
* is to avoid having esm-config depend on esm-extensions, which would
|
|
33
|
+
* create a circular dependency.
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export interface ConfigExtensionStore {
|
|
37
|
+
mountedExtensions: Array<ConfigExtensionStoreElement>;
|
|
38
|
+
}
|
|
39
|
+
/** @internal */
|
|
40
|
+
export interface ConfigExtensionStoreElement {
|
|
41
|
+
slotModuleName: string;
|
|
42
|
+
extensionModuleName: string;
|
|
43
|
+
slotName: string;
|
|
44
|
+
extensionId: string;
|
|
45
|
+
}
|
|
46
|
+
/** @internal */
|
|
47
|
+
export declare const configExtensionStore: import("unistore").Store<ConfigExtensionStore>;
|
|
48
|
+
/**
|
|
49
|
+
* Output configs
|
|
50
|
+
*
|
|
51
|
+
* Each module has its own stores for its config and its extension slots' configs.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export interface ConfigStore {
|
|
55
|
+
config: ConfigObject | null;
|
|
56
|
+
loaded: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** @internal */
|
|
59
|
+
export declare function getConfigStore(moduleName: string): import("unistore").Store<ConfigStore>;
|
|
60
|
+
/**
|
|
61
|
+
* Configuration for all the specific extension slots
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export interface ExtensionSlotsConfigStore {
|
|
65
|
+
slots: {
|
|
66
|
+
[slotName: string]: {
|
|
67
|
+
config: ExtensionSlotConfigObject;
|
|
68
|
+
loaded: boolean;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** @internal */
|
|
73
|
+
export declare function getExtensionSlotsConfigStore(): import("unistore").Store<ExtensionSlotsConfigStore>;
|
|
74
|
+
/** @internal */
|
|
75
|
+
export declare function getExtensionSlotConfig(slotName: string): {
|
|
76
|
+
config: ExtensionSlotConfigObject;
|
|
77
|
+
loaded: boolean;
|
|
78
|
+
};
|
|
79
|
+
/** @internal */
|
|
80
|
+
export declare function getExtensionSlotConfigFromStore(state: ExtensionSlotsConfigStore, slotName: string): {
|
|
81
|
+
config: ExtensionSlotConfigObject;
|
|
82
|
+
loaded: boolean;
|
|
83
|
+
};
|
|
84
|
+
/** @internal */
|
|
85
|
+
export interface ExtensionsConfigStore {
|
|
86
|
+
configs: {
|
|
87
|
+
[slotName: string]: {
|
|
88
|
+
[extensionId: string]: ConfigStore;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One store for all the extensions
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
export declare function getExtensionsConfigStore(): import("unistore").Store<ExtensionsConfigStore>;
|
|
97
|
+
/** @internal */
|
|
98
|
+
export declare function getExtensionConfig(slotName: string, extensionId: string): ConfigStore;
|
|
99
|
+
/** @internal */
|
|
100
|
+
export declare function getExtensionConfigFromStore(state: ExtensionsConfigStore, slotName: string, extensionId: string): ConfigStore;
|
|
101
|
+
/**
|
|
102
|
+
* A store of the implementer tools output config
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
export interface ImplementerToolsConfigStore {
|
|
106
|
+
config: Config;
|
|
107
|
+
}
|
|
108
|
+
/** @internal */
|
|
109
|
+
export declare const implementerToolsConfigStore: import("unistore").Store<ImplementerToolsConfigStore>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @module @category Navigation */
|
|
2
|
+
/**
|
|
3
|
+
* Interpolates a string with openmrsBase and openmrsSpaBase.
|
|
4
|
+
*
|
|
5
|
+
* Useful for accepting `${openmrsBase}` or `${openmrsSpaBase}` template
|
|
6
|
+
* parameters in configurable URLs.
|
|
7
|
+
*
|
|
8
|
+
* @param template A string to interpolate
|
|
9
|
+
* @param additionalParams Additional values to interpolate into the string template
|
|
10
|
+
*/
|
|
11
|
+
export declare function interpolateUrl(template: string, additionalParams?: {
|
|
12
|
+
[key: string]: string;
|
|
13
|
+
}): string;
|
|
14
|
+
/**
|
|
15
|
+
* Interpolates values of `params` into the `template` string.
|
|
16
|
+
*
|
|
17
|
+
* Useful for additional template parameters in URLs.
|
|
18
|
+
*
|
|
19
|
+
* Example usage:
|
|
20
|
+
* ```js
|
|
21
|
+
* navigate({
|
|
22
|
+
* to: interpolateString(
|
|
23
|
+
* config.links.patientChart,
|
|
24
|
+
* { patientUuid: patient.uuid }
|
|
25
|
+
* )
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @param template With optional params wrapped in `${ }`
|
|
30
|
+
* @param params Values to interpolate into the string template
|
|
31
|
+
*/
|
|
32
|
+
export declare function interpolateString(template: string, params: {
|
|
33
|
+
[key: string]: string;
|
|
34
|
+
}): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare type TemplateParams = {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
};
|
|
4
|
+
export interface NavigateOptions {
|
|
5
|
+
to: string;
|
|
6
|
+
templateParams?: TemplateParams;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Calls `location.assign` for non-SPA paths and [navigateToUrl](https://single-spa.js.org/docs/api/#navigatetourl) for SPA paths
|
|
10
|
+
*
|
|
11
|
+
* Example usage:
|
|
12
|
+
* ```js
|
|
13
|
+
* const config = useConfig();
|
|
14
|
+
* const submitHandler = () => {
|
|
15
|
+
* navigate({ to: config.links.submitSuccess });
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @param to The target path or URL. Supports templating with 'openmrsBase', 'openmrsSpaBase',
|
|
20
|
+
* and any additional template parameters defined in `templateParams`.
|
|
21
|
+
* For example, `${openmrsSpaBase}/home` will resolve to `/openmrs/spa/home`
|
|
22
|
+
* for implementations using the standard OpenMRS and SPA base paths.
|
|
23
|
+
* If `templateParams` contains `{ foo: "bar" }`, then the URL `${openmrsBase}/${foo}`
|
|
24
|
+
* will become `/openmrs/bar`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function navigate({ to, templateParams }: NavigateOptions): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./navigation/interpolate-string";
|
|
3
|
+
export * from "./navigation/navigate";
|
|
4
|
+
export * from "./validators/validator";
|
|
5
|
+
export * from "./validators/validators";
|
|
6
|
+
export { defineConfigSchema, defineExtensionConfigSchema, provide, getConfig, } from "./module-config/module-config";
|
|
7
|
+
export { getConfigStore, ConfigStore } from "./module-config/state";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export declare enum Type {
|
|
2
|
+
Array = "Array",
|
|
3
|
+
Boolean = "Boolean",
|
|
4
|
+
ConceptUuid = "ConceptUuid",
|
|
5
|
+
Number = "Number",
|
|
6
|
+
Object = "Object",
|
|
7
|
+
String = "String",
|
|
8
|
+
UUID = "UUID",
|
|
9
|
+
PersonAttributeTypeUuid = "PersonAttributeTypeUuid",
|
|
10
|
+
PatientIdentifierTypeUuid = "PatientIdentifierTypeUuid"
|
|
11
|
+
}
|
|
12
|
+
export interface ConfigSchema {
|
|
13
|
+
[key: string]: ConfigSchema | ConfigValue;
|
|
14
|
+
_type?: Type;
|
|
15
|
+
_validators?: Array<Validator>;
|
|
16
|
+
_elements?: ConfigSchema;
|
|
17
|
+
}
|
|
18
|
+
export interface Config extends Object {
|
|
19
|
+
[moduleName: string]: {
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface ConfigObject extends Object {
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
export declare type ConfigValue = string | number | boolean | void | Array<any> | object;
|
|
27
|
+
export interface ExtensionSlotConfig {
|
|
28
|
+
add?: Array<string>;
|
|
29
|
+
remove?: Array<string>;
|
|
30
|
+
order?: Array<string>;
|
|
31
|
+
configure?: ExtensionSlotConfigureValueObject;
|
|
32
|
+
}
|
|
33
|
+
export interface ExtensionSlotConfigureValueObject {
|
|
34
|
+
[key: string]: object;
|
|
35
|
+
}
|
|
36
|
+
export interface ExtensionSlotConfigObject {
|
|
37
|
+
/** Additional extension IDs to assign to this slot, in addition to those `attach`ed in code. */
|
|
38
|
+
add?: Array<string>;
|
|
39
|
+
/** Extension IDs which were `attach`ed to the slot but which should not be assigned. */
|
|
40
|
+
remove?: Array<string>;
|
|
41
|
+
/** Overrides the default ordering of extensions. */
|
|
42
|
+
order?: Array<string>;
|
|
43
|
+
}
|
|
44
|
+
export declare type ProvidedConfig = {
|
|
45
|
+
source: string;
|
|
46
|
+
config: Config;
|
|
47
|
+
};
|
|
48
|
+
export declare type ValidatorFunction = (value: any) => boolean;
|
|
49
|
+
export declare type Validator = (value: any) => void | string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const isArray: import("..").Validator;
|
|
2
|
+
export declare const isBoolean: import("..").Validator;
|
|
3
|
+
export declare const isNumber: import("..").Validator;
|
|
4
|
+
export declare const isString: import("..").Validator;
|
|
5
|
+
export declare const isObject: import("..").Validator;
|
|
6
|
+
export declare const isUuid: import("..").Validator;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** @module @category Config Validation */
|
|
2
|
+
import { Validator, ValidatorFunction } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Constructs a custom validator.
|
|
5
|
+
*
|
|
6
|
+
* ### Example
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* {
|
|
10
|
+
* foo: {
|
|
11
|
+
* _default: 0,
|
|
12
|
+
* _validators: [
|
|
13
|
+
* validator(val => val >= 0, "Must not be negative.")
|
|
14
|
+
* ]
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
* @param validationFunction Takes the configured value as input. Returns true
|
|
19
|
+
* if it is valid, false otherwise.
|
|
20
|
+
* @param message A string message that explains why the value is invalid. Can
|
|
21
|
+
* also be a function that takes the value as input and returns a string.
|
|
22
|
+
* @returns A validator ready for use in a config schema
|
|
23
|
+
*/
|
|
24
|
+
export declare function validator(validationFunction: ValidatorFunction, message: string | ((value: any) => string)): Validator;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies that the value is between the provided minimum and maximum
|
|
3
|
+
*
|
|
4
|
+
* @param min Minimum acceptable value
|
|
5
|
+
* @param max Maximum acceptable value
|
|
6
|
+
*/
|
|
7
|
+
export declare const inRange: (min: number, max: number) => import("..").Validator;
|
|
8
|
+
/**
|
|
9
|
+
* Verifies that a string contains only the default URL template
|
|
10
|
+
* parameters, plus any specified in `allowedTemplateParameters`.
|
|
11
|
+
*
|
|
12
|
+
* @param allowedTemplateParameters To be added to `openmrsBase` and `openmrsSpaBase`
|
|
13
|
+
* @category Navigation
|
|
14
|
+
*/
|
|
15
|
+
export declare const isUrlWithTemplateParameters: (allowedTemplateParameters: string[]) => import("..").Validator;
|
|
16
|
+
/**
|
|
17
|
+
* Verifies that a string contains only the default URL template parameters.
|
|
18
|
+
*
|
|
19
|
+
* @category Navigation
|
|
20
|
+
*/
|
|
21
|
+
export declare const isUrl: import("..").Validator;
|
|
22
|
+
/**
|
|
23
|
+
* Verifies that the value is one of the allowed options.
|
|
24
|
+
* @param allowedValues The list of allowable values
|
|
25
|
+
*/
|
|
26
|
+
export declare const oneOf: (allowedValues: Array<any>) => import("..").Validator;
|
|
27
|
+
export declare const validators: {
|
|
28
|
+
inRange: (min: number, max: number) => import("..").Validator;
|
|
29
|
+
isUrl: import("..").Validator;
|
|
30
|
+
isUrlWithTemplateParameters: (allowedTemplateParameters: string[]) => import("..").Validator;
|
|
31
|
+
oneOf: (allowedValues: Array<any>) => import("..").Validator;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-config",
|
|
3
|
-
"version": "4.0.0-pre.
|
|
3
|
+
"version": "4.0.0-pre.1",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "A configuration library for the OpenMRS Single-Spa framework.",
|
|
6
6
|
"browser": "dist/openmrs-esm-module-config.js",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"systemjs": "6.x"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@openmrs/esm-globals": "^4.0.0-pre.
|
|
49
|
-
"@openmrs/esm-state": "^4.0.0-pre.
|
|
48
|
+
"@openmrs/esm-globals": "^4.0.0-pre.1",
|
|
49
|
+
"@openmrs/esm-state": "^4.0.0-pre.1",
|
|
50
50
|
"@types/ramda": "^0.26.44",
|
|
51
51
|
"@types/systemjs": "^6.1.0",
|
|
52
52
|
"babel-plugin-ramda": "^2.0.0",
|
|
53
53
|
"single-spa": "^5.9.2"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "9f58b801f07526083a9edc1cc5a1e58660d71eb0"
|
|
56
56
|
}
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
ConfigInternalStore,
|
|
13
13
|
configInternalStore,
|
|
14
14
|
getExtensionConfig,
|
|
15
|
-
getExtensionsConfigStore,
|
|
16
15
|
ImplementerToolsConfigStore,
|
|
17
16
|
implementerToolsConfigStore,
|
|
18
17
|
temporaryConfigStore,
|
|
@@ -101,6 +100,33 @@ describe("defineConfigSchema", () => {
|
|
|
101
100
|
expect(console.error).not.toHaveBeenCalled();
|
|
102
101
|
});
|
|
103
102
|
|
|
103
|
+
it("runs top-level validators on filled-in config", () => {
|
|
104
|
+
const schema = {
|
|
105
|
+
foo: {
|
|
106
|
+
_type: Type.String,
|
|
107
|
+
_default: "bar-value",
|
|
108
|
+
},
|
|
109
|
+
fooStart: {
|
|
110
|
+
_type: Type.String,
|
|
111
|
+
_default: "bar",
|
|
112
|
+
},
|
|
113
|
+
_validators: [
|
|
114
|
+
validator(
|
|
115
|
+
(v) => v.foo.startsWith(v.fooStart),
|
|
116
|
+
(v) =>
|
|
117
|
+
`The value of \`foo\` must start with the value of \`fooStart\` (which is ${v.fooStart}).`
|
|
118
|
+
),
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
Config.defineConfigSchema("foo-module", schema);
|
|
122
|
+
Config.provide({ "foo-module": { foo: "different" } });
|
|
123
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
124
|
+
expect.stringMatching(
|
|
125
|
+
/The value of `foo` must start with the value of `fooStart`.*bar/
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
104
130
|
it("logs an error if a non-function validator is provided", () => {
|
|
105
131
|
const schema = {
|
|
106
132
|
bar: { _default: [], _validators: [false] },
|
|
@@ -264,7 +290,7 @@ describe("getConfig", () => {
|
|
|
264
290
|
await Config.getConfig("foo-module");
|
|
265
291
|
expect(console.error).toHaveBeenCalledWith(
|
|
266
292
|
expect.stringMatching(
|
|
267
|
-
/
|
|
293
|
+
/Invalid configuration for foo-module.bar.*c must equal a\.b \+ diff/
|
|
268
294
|
)
|
|
269
295
|
);
|
|
270
296
|
|
|
@@ -360,6 +386,19 @@ describe("getConfig", () => {
|
|
|
360
386
|
);
|
|
361
387
|
});
|
|
362
388
|
|
|
389
|
+
it("tolerates defaults that don't pass validation", async () => {
|
|
390
|
+
Config.defineConfigSchema("foo-module", {
|
|
391
|
+
foo: {
|
|
392
|
+
_default: null,
|
|
393
|
+
_validators: [
|
|
394
|
+
validator((val) => val.startsWith("thi"), "must start with 'thi'"),
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
await Config.getConfig("foo-module");
|
|
399
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
400
|
+
});
|
|
401
|
+
|
|
363
402
|
it("validators pass", async () => {
|
|
364
403
|
Config.defineConfigSchema("foo-module", {
|
|
365
404
|
foo: {
|
|
@@ -505,6 +544,27 @@ describe("getConfig", () => {
|
|
|
505
544
|
expect(console.error).not.toHaveBeenCalled();
|
|
506
545
|
});
|
|
507
546
|
|
|
547
|
+
it("supports array of string elements", async () => {
|
|
548
|
+
Config.defineConfigSchema("foo-module", {
|
|
549
|
+
foo: {
|
|
550
|
+
_type: Type.Array,
|
|
551
|
+
_default: ["bar"],
|
|
552
|
+
_elements: {
|
|
553
|
+
_type: Type.String,
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
const testConfig = {
|
|
558
|
+
"foo-module": {
|
|
559
|
+
foo: ["bar", "baz", "qux"],
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
Config.provide(testConfig);
|
|
563
|
+
const config = await Config.getConfig("foo-module");
|
|
564
|
+
expect(config.foo).toStrictEqual(["bar", "baz", "qux"]);
|
|
565
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
566
|
+
});
|
|
567
|
+
|
|
508
568
|
it("supports validation of array elements", async () => {
|
|
509
569
|
Config.defineConfigSchema("foo-module", {
|
|
510
570
|
foo: {
|
|
@@ -638,9 +698,7 @@ describe("getConfig", () => {
|
|
|
638
698
|
Config.provide(badConfig);
|
|
639
699
|
await Config.getConfig("foo-module");
|
|
640
700
|
expect(console.error).toHaveBeenCalledWith(
|
|
641
|
-
expect.stringMatching(
|
|
642
|
-
/value.*{\"a\":{\"b\":1},\"c\":3}.*foo-module.bar\[1\].*c must equal a\.b \+ 1/
|
|
643
|
-
)
|
|
701
|
+
expect.stringMatching(/foo-module.bar\[1\].*c must equal a\.b \+ 1/i)
|
|
644
702
|
);
|
|
645
703
|
|
|
646
704
|
await resetAll();
|
|
@@ -280,8 +280,9 @@ export function processConfig(
|
|
|
280
280
|
keyPathContext: string,
|
|
281
281
|
devDefaultsAreOn: boolean = false
|
|
282
282
|
) {
|
|
283
|
-
|
|
283
|
+
validateStructure(schema, providedConfig, keyPathContext);
|
|
284
284
|
const config = setDefaults(schema, providedConfig, devDefaultsAreOn);
|
|
285
|
+
runAllValidatorsInConfigTree(schema, config, keyPathContext);
|
|
285
286
|
return config;
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -326,12 +327,13 @@ function computeExtensionConfig(
|
|
|
326
327
|
// TODO: validate that a schema exists for the module
|
|
327
328
|
const schema =
|
|
328
329
|
extensionConfigSchema ?? configState.schemas[extensionModuleName];
|
|
329
|
-
|
|
330
|
+
validateStructure(schema, combinedConfig, nameOfSchemaSource);
|
|
330
331
|
const config = setDefaults(
|
|
331
332
|
schema,
|
|
332
333
|
combinedConfig,
|
|
333
334
|
configState.devDefaultsAreOn
|
|
334
335
|
);
|
|
336
|
+
runAllValidatorsInConfigTree(schema, config, nameOfSchemaSource);
|
|
335
337
|
delete config.extensionSlots;
|
|
336
338
|
return config;
|
|
337
339
|
}
|
|
@@ -571,8 +573,9 @@ function getConfigForModule(
|
|
|
571
573
|
moduleName,
|
|
572
574
|
getProvidedConfigs(configState, tempConfigState)
|
|
573
575
|
);
|
|
574
|
-
|
|
576
|
+
validateStructure(schema, inputConfig, moduleName);
|
|
575
577
|
const config = setDefaults(schema, inputConfig, configState.devDefaultsAreOn);
|
|
578
|
+
runAllValidatorsInConfigTree(schema, config, moduleName);
|
|
576
579
|
delete config.extensionSlots;
|
|
577
580
|
return config;
|
|
578
581
|
}
|
|
@@ -593,21 +596,25 @@ function mergeConfigs(configs: Array<Config>) {
|
|
|
593
596
|
return mergeDeepAll({}, configs) as Config;
|
|
594
597
|
}
|
|
595
598
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Recursively check the provided config tree to make sure that all
|
|
601
|
+
* of the provided properties exist in the schema, and that types are
|
|
602
|
+
* correct. Does not run validators yet, since those will be run on
|
|
603
|
+
* the config with the defaults filled in.
|
|
604
|
+
*/
|
|
605
|
+
function validateStructure(
|
|
600
606
|
schema: ConfigSchema,
|
|
601
607
|
config: ConfigObject,
|
|
602
608
|
keyPath = ""
|
|
603
|
-
)
|
|
609
|
+
) {
|
|
610
|
+
// validate each constituent element
|
|
604
611
|
for (const key of Object.keys(config)) {
|
|
605
612
|
const value = config[key];
|
|
606
613
|
const thisKeyPath = keyPath + "." + key;
|
|
607
614
|
const schemaPart = schema[key] as ConfigSchema;
|
|
608
615
|
|
|
609
616
|
if (!schema.hasOwnProperty(key)) {
|
|
610
|
-
if (key
|
|
617
|
+
if (!(key === "extensionSlots" && keyPath !== "")) {
|
|
611
618
|
console.error(
|
|
612
619
|
`Unknown config key '${thisKeyPath}' provided. Ignoring.`
|
|
613
620
|
);
|
|
@@ -616,27 +623,34 @@ const validateConfig = (
|
|
|
616
623
|
continue;
|
|
617
624
|
}
|
|
618
625
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
626
|
+
validateBranchStructure(schemaPart, value, thisKeyPath);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function validateBranchStructure(
|
|
631
|
+
schemaPart: ConfigSchema,
|
|
632
|
+
value: any,
|
|
633
|
+
keyPath: string
|
|
634
|
+
) {
|
|
635
|
+
checkType(keyPath, schemaPart._type, value);
|
|
636
|
+
|
|
637
|
+
if (isOrdinaryObject(value)) {
|
|
638
|
+
// structurally validate only if there's elements specified
|
|
639
|
+
// or there's no `_default` value (which would indicate a freeform object)
|
|
640
|
+
if (schemaPart._type === Type.Object) {
|
|
641
|
+
validateDictionaryStructure(schemaPart, value, keyPath);
|
|
642
|
+
} else if (!schemaPart.hasOwnProperty("_default")) {
|
|
643
|
+
// recurse to validate nested object structure
|
|
644
|
+
validateStructure(schemaPart, value, keyPath);
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
if (schemaPart._type === Type.Array) {
|
|
648
|
+
validateArrayStructure(schemaPart, value, keyPath);
|
|
635
649
|
}
|
|
636
650
|
}
|
|
637
|
-
}
|
|
651
|
+
}
|
|
638
652
|
|
|
639
|
-
function
|
|
653
|
+
function validateDictionaryStructure(
|
|
640
654
|
dictionarySchema: ConfigSchema,
|
|
641
655
|
config: ConfigObject,
|
|
642
656
|
keyPath: string
|
|
@@ -644,12 +658,12 @@ function validateDictionary(
|
|
|
644
658
|
if (dictionarySchema._elements) {
|
|
645
659
|
for (const key of Object.keys(config)) {
|
|
646
660
|
const value = config[key];
|
|
647
|
-
|
|
661
|
+
validateStructure(dictionarySchema._elements, value, `${keyPath}.${key}`);
|
|
648
662
|
}
|
|
649
663
|
}
|
|
650
664
|
}
|
|
651
665
|
|
|
652
|
-
function
|
|
666
|
+
function validateArrayStructure(
|
|
653
667
|
arraySchema: ConfigSchema,
|
|
654
668
|
value: ConfigObject,
|
|
655
669
|
keyPath: string
|
|
@@ -657,17 +671,55 @@ function validateArray(
|
|
|
657
671
|
// if there is an array element object schema, verify that elements match it
|
|
658
672
|
if (hasObjectSchema(arraySchema._elements)) {
|
|
659
673
|
for (let i = 0; i < value.length; i++) {
|
|
660
|
-
|
|
674
|
+
validateBranchStructure(
|
|
675
|
+
arraySchema._elements,
|
|
676
|
+
value[i],
|
|
677
|
+
`${keyPath}[${i}]`
|
|
678
|
+
);
|
|
661
679
|
}
|
|
662
680
|
}
|
|
663
681
|
|
|
664
682
|
for (let i = 0; i < value.length; i++) {
|
|
665
683
|
checkType(`${keyPath}[${i}]`, arraySchema._elements?._type, value[i]);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Run all the validators in the config tree. This should be run
|
|
689
|
+
* on the config object after it has been filled in with all the defaults, since
|
|
690
|
+
* higher-level validators may refer to default values.
|
|
691
|
+
*/
|
|
692
|
+
function runAllValidatorsInConfigTree(
|
|
693
|
+
schema: ConfigSchema,
|
|
694
|
+
config: ConfigObject,
|
|
695
|
+
keyPath = ""
|
|
696
|
+
) {
|
|
697
|
+
// If `!schema`, there should have been a structural validation error printed already.
|
|
698
|
+
if (schema) {
|
|
699
|
+
if (config !== schema._default) {
|
|
700
|
+
runValidators(keyPath, schema._validators, config);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (isOrdinaryObject(config)) {
|
|
704
|
+
for (const key of Object.keys(config)) {
|
|
705
|
+
const value = config[key];
|
|
706
|
+
const thisKeyPath = keyPath + "." + key;
|
|
707
|
+
const schemaPart = schema[key] as ConfigSchema;
|
|
708
|
+
if (schema._type === Type.Object && schema._elements) {
|
|
709
|
+
runAllValidatorsInConfigTree(schema._elements, value, thisKeyPath);
|
|
710
|
+
} else {
|
|
711
|
+
runAllValidatorsInConfigTree(schemaPart, value, thisKeyPath);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} else if (Array.isArray(config) && schema._elements) {
|
|
715
|
+
for (let i = 0; i < config.length; i++) {
|
|
716
|
+
runAllValidatorsInConfigTree(
|
|
717
|
+
schema._elements,
|
|
718
|
+
config[i],
|
|
719
|
+
`${keyPath}[${i}]`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
671
723
|
}
|
|
672
724
|
}
|
|
673
725
|
|
|
@@ -699,15 +751,21 @@ function runValidators(
|
|
|
699
751
|
const validatorResult = validator(value);
|
|
700
752
|
|
|
701
753
|
if (typeof validatorResult === "string") {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
754
|
+
if (typeof value === "object") {
|
|
755
|
+
console.error(
|
|
756
|
+
`Invalid configuration for ${keyPath}: ${validatorResult}`
|
|
757
|
+
);
|
|
758
|
+
} else {
|
|
759
|
+
console.error(
|
|
760
|
+
`Invalid configuration value ${value} for ${keyPath}: ${validatorResult}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
707
763
|
}
|
|
708
764
|
}
|
|
709
765
|
} catch (e) {
|
|
710
|
-
console.error(
|
|
766
|
+
console.error(
|
|
767
|
+
`Skipping invalid validator at "${keyPath}". Encountered error\n\t${e}`
|
|
768
|
+
);
|
|
711
769
|
}
|
|
712
770
|
}
|
|
713
771
|
}
|
|
@@ -748,7 +806,7 @@ const setDefaults = (
|
|
|
748
806
|
const elements = schemaPart._elements;
|
|
749
807
|
|
|
750
808
|
if (configPart && hasObjectSchema(elements)) {
|
|
751
|
-
if (schemaPart._type === Type.Array) {
|
|
809
|
+
if (schemaPart._type === Type.Array && Array.isArray(configPart)) {
|
|
752
810
|
const configWithDefaults = configPart.map((conf: Config) =>
|
|
753
811
|
setDefaults(elements, conf, devDefaultsAreOn)
|
|
754
812
|
);
|