Skip to content

Commit a2f71f6

Browse files
authored
allows plugins to define validation schema for "enabled" flag (#50286) (#50885)
* validation error message gives a hint about error source * allows plugins to define validation schema for "enabled" flag
1 parent a5b3efa commit a2f71f6

File tree

2 files changed

+97
-14
lines changed

2 files changed

+97
-14
lines changed

src/core/server/config/config_service.test.ts

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ test('throws if config at path does not match schema', async () => {
5555
await expect(
5656
configService.setSchema('key', schema.string())
5757
).rejects.toThrowErrorMatchingInlineSnapshot(
58-
`"[key]: expected value of type [string] but got [number]"`
58+
`"[config validation of [key]]: expected value of type [string] but got [number]"`
5959
);
6060
});
6161

@@ -78,11 +78,11 @@ test('re-validate config when updated', async () => {
7878
config$.next(new ObjectToConfigAdapter({ key: 123 }));
7979

8080
await expect(valuesReceived).toMatchInlineSnapshot(`
81-
Array [
82-
"value",
83-
[Error: [key]: expected value of type [string] but got [number]],
84-
]
85-
`);
81+
Array [
82+
"value",
83+
[Error: [config validation of [key]]: expected value of type [string] but got [number]],
84+
]
85+
`);
8686
});
8787

8888
test("returns undefined if fetching optional config at a path that doesn't exist", async () => {
@@ -143,7 +143,7 @@ test("throws error if 'schema' is not defined for a key", async () => {
143143
const configs = configService.atPath('key');
144144

145145
await expect(configs.pipe(first()).toPromise()).rejects.toMatchInlineSnapshot(
146-
`[Error: No validation schema has been defined for key]`
146+
`[Error: No validation schema has been defined for [key]]`
147147
);
148148
});
149149

@@ -153,7 +153,7 @@ test("throws error if 'setSchema' called several times for the same key", async
153153
const addSchema = async () => await configService.setSchema('key', schema.string());
154154
await addSchema();
155155
await expect(addSchema()).rejects.toMatchInlineSnapshot(
156-
`[Error: Validation schema for key was already registered.]`
156+
`[Error: Validation schema for [key] was already registered.]`
157157
);
158158
});
159159

@@ -280,6 +280,33 @@ test('handles disabled path and marks config as used', async () => {
280280
expect(unusedPaths).toEqual([]);
281281
});
282282

283+
test('does not throw if schema does not define "enabled" schema', async () => {
284+
const initialConfig = {
285+
pid: {
286+
file: '/some/file.pid',
287+
},
288+
};
289+
290+
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
291+
const configService = new ConfigService(config$, defaultEnv, logger);
292+
expect(
293+
configService.setSchema(
294+
'pid',
295+
schema.object({
296+
file: schema.string(),
297+
})
298+
)
299+
).resolves.toBeUndefined();
300+
301+
const value$ = configService.atPath('pid');
302+
const value: any = await value$.pipe(first()).toPromise();
303+
expect(value.enabled).toBe(undefined);
304+
305+
const valueOptional$ = configService.optionalAtPath('pid');
306+
const valueOptional: any = await valueOptional$.pipe(first()).toPromise();
307+
expect(valueOptional.enabled).toBe(undefined);
308+
});
309+
283310
test('treats config as enabled if config path is not present in config', async () => {
284311
const initialConfig = {};
285312

@@ -292,3 +319,45 @@ test('treats config as enabled if config path is not present in config', async (
292319
const unusedPaths = await configService.getUnusedPaths();
293320
expect(unusedPaths).toEqual([]);
294321
});
322+
323+
test('read "enabled" even if its schema is not present', async () => {
324+
const initialConfig = {
325+
foo: {
326+
enabled: true,
327+
},
328+
};
329+
330+
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
331+
const configService = new ConfigService(config$, defaultEnv, logger);
332+
333+
const isEnabled = await configService.isEnabledAtPath('foo');
334+
expect(isEnabled).toBe(true);
335+
});
336+
337+
test('allows plugins to specify "enabled" flag via validation schema', async () => {
338+
const initialConfig = {};
339+
340+
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
341+
const configService = new ConfigService(config$, defaultEnv, logger);
342+
343+
await configService.setSchema(
344+
'foo',
345+
schema.object({ enabled: schema.boolean({ defaultValue: false }) })
346+
);
347+
348+
expect(await configService.isEnabledAtPath('foo')).toBe(false);
349+
350+
await configService.setSchema(
351+
'bar',
352+
schema.object({ enabled: schema.boolean({ defaultValue: true }) })
353+
);
354+
355+
expect(await configService.isEnabledAtPath('bar')).toBe(true);
356+
357+
await configService.setSchema(
358+
'baz',
359+
schema.object({ different: schema.boolean({ defaultValue: true }) })
360+
);
361+
362+
expect(await configService.isEnabledAtPath('baz')).toBe(true);
363+
});

src/core/server/config/config_service.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class ConfigService {
5454
public async setSchema(path: ConfigPath, schema: Type<unknown>) {
5555
const namespace = pathToString(path);
5656
if (this.schemas.has(namespace)) {
57-
throw new Error(`Validation schema for ${path} was already registered.`);
57+
throw new Error(`Validation schema for [${path}] was already registered.`);
5858
}
5959

6060
this.schemas.set(namespace, schema);
@@ -98,14 +98,28 @@ export class ConfigService {
9898
}
9999

100100
public async isEnabledAtPath(path: ConfigPath) {
101-
const enabledPath = createPluginEnabledPath(path);
101+
const namespace = pathToString(path);
102+
103+
const validatedConfig = this.schemas.has(namespace)
104+
? await this.atPath<{ enabled?: boolean }>(path)
105+
.pipe(first())
106+
.toPromise()
107+
: undefined;
102108

109+
const enabledPath = createPluginEnabledPath(path);
103110
const config = await this.config$.pipe(first()).toPromise();
104-
if (!config.has(enabledPath)) {
111+
112+
// if plugin hasn't got a config schema, we try to read "enabled" directly
113+
const isEnabled =
114+
validatedConfig && validatedConfig.enabled !== undefined
115+
? validatedConfig.enabled
116+
: config.get(enabledPath);
117+
118+
// not declared. consider that plugin is enabled by default
119+
if (isEnabled === undefined) {
105120
return true;
106121
}
107122

108-
const isEnabled = config.get(enabledPath);
109123
if (isEnabled === false) {
110124
// If the plugin is _not_ enabled, we mark the entire plugin path as
111125
// handled, as it's expected that it won't be used.
@@ -138,7 +152,7 @@ export class ConfigService {
138152
const namespace = pathToString(path);
139153
const schema = this.schemas.get(namespace);
140154
if (!schema) {
141-
throw new Error(`No validation schema has been defined for ${namespace}`);
155+
throw new Error(`No validation schema has been defined for [${namespace}]`);
142156
}
143157
return schema.validate(
144158
config,
@@ -147,7 +161,7 @@ export class ConfigService {
147161
prod: this.env.mode.prod,
148162
...this.env.packageInfo,
149163
},
150-
namespace
164+
`config validation of [${namespace}]`
151165
);
152166
}
153167

0 commit comments

Comments
 (0)