config,config-loader: warn when trying to access invisible config values

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-07-30 16:31:44 +02:00
parent 8c6054a587
commit e9d3983eee
16 changed files with 283 additions and 48 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ export function readEnvConfig(env: {
// Warnings were encountered during analysis:
//
// src/lib/schema/types.d.ts:77:5 - (ae-forgotten-export) The symbol "ConfigProcessingOptions" needs to be exported by the entry point index.d.ts
// src/lib/schema/types.d.ts:83:5 - (ae-forgotten-export) The symbol "ConfigProcessingOptions" needs to be exported by the entry point index.d.ts
// src/loader.d.ts:13:5 - (ae-forgotten-export) The symbol "EnvFunc" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
@@ -63,43 +63,115 @@ const visibility = new Map<string, ConfigVisibility>(
describe('filterByVisibility', () => {
test.each<[ConfigVisibility[], JsonObject]>([
[[], {}],
[
[],
{
data: {},
filteredKeys: [
'arr[0]',
'arr[1]',
'arr[2]',
'objArr[0].f',
'objArr[0].b',
'objArr[0].s',
'objArr[1].f',
'objArr[1].b',
'objArr[1].s',
'obj.f',
'obj.b.s',
'arrF[0].never',
'arrB[0].never',
'arrS[0].never',
'objF.never',
'objB.never',
'objS.never',
],
},
],
[
['frontend'],
{
arr: ['f'],
objArr: [{ f: 1 }, { f: 4 }],
obj: { f: 'a' },
arrF: [],
objF: {},
data: {
arr: ['f'],
objArr: [{ f: 1 }, { f: 4 }],
obj: { f: 'a' },
arrF: [],
objF: {},
},
filteredKeys: [
'arr[1]',
'arr[2]',
'objArr[0].b',
'objArr[0].s',
'objArr[1].b',
'objArr[1].s',
'obj.b.s',
'arrF[0].never',
'arrB[0].never',
'arrS[0].never',
'objF.never',
'objB.never',
'objS.never',
],
},
],
[
['backend'],
{
arr: ['b'],
objArr: [{ b: 2 }, { b: 5 }],
obj: { b: {} },
arrF: [{ never: 'here' }],
arrB: [{ never: 'here' }],
arrS: [{ never: 'here' }],
objF: { never: 'here' },
objB: { never: 'here' },
objS: { never: 'here' },
data: {
arr: ['b'],
objArr: [{ b: 2 }, { b: 5 }],
obj: { b: {} },
arrF: [{ never: 'here' }],
arrB: [{ never: 'here' }],
arrS: [{ never: 'here' }],
objF: { never: 'here' },
objB: { never: 'here' },
objS: { never: 'here' },
},
filteredKeys: [
'arr[0]',
'arr[2]',
'objArr[0].f',
'objArr[0].s',
'objArr[1].f',
'objArr[1].s',
'obj.f',
'obj.b.s',
],
},
],
[
['secret'],
{
arr: ['s'],
objArr: [{ s: 3 }, { s: 6 }],
obj: { b: { s: true } },
arrS: [],
objS: {},
data: {
arr: ['s'],
objArr: [{ s: 3 }, { s: 6 }],
obj: { b: { s: true } },
arrS: [],
objS: {},
},
filteredKeys: [
'arr[0]',
'arr[1]',
'objArr[0].f',
'objArr[0].b',
'objArr[1].f',
'objArr[1].b',
'obj.f',
'arrF[0].never',
'arrB[0].never',
'arrS[0].never',
'objF.never',
'objB.never',
'objS.never',
],
},
],
[['frontend', 'backend', 'secret'], data],
[['frontend', 'backend', 'secret'], { data, filteredKeys: [] }],
])('should filter correctly with %p', (filter, expected) => {
expect(filterByVisibility(data, filter, visibility)).toEqual(expected);
expect(
filterByVisibility(data, filter, visibility, undefined, true),
).toEqual(expected);
});
});
@@ -30,9 +30,17 @@ export function filterByVisibility(
includeVisibilities: ConfigVisibility[],
visibilityByPath: Map<string, ConfigVisibility>,
transformFunc?: TransformFunc<number | string | boolean>,
): JsonObject {
function transform(jsonVal: JsonValue, path: string): JsonValue | undefined {
const visibility = visibilityByPath.get(path) ?? DEFAULT_CONFIG_VISIBILITY;
withFilteredKeys?: boolean,
): { data: JsonObject; filteredKeys?: string[] } {
const filteredKeys = new Array<string>();
function transform(
jsonVal: JsonValue,
visibilityPath: string, // Matches the format we get from ajv
filterPath: string, // Matches the format of the ConfigReader
): JsonValue | undefined {
const visibility =
visibilityByPath.get(visibilityPath) ?? DEFAULT_CONFIG_VISIBILITY;
const isVisible = includeVisibilities.includes(visibility);
if (typeof jsonVal !== 'object') {
@@ -42,6 +50,9 @@ export function filterByVisibility(
}
return jsonVal;
}
if (withFilteredKeys) {
filteredKeys.push(filterPath);
}
return undefined;
} else if (jsonVal === null) {
return undefined;
@@ -49,7 +60,11 @@ export function filterByVisibility(
const arr = new Array<JsonValue>();
for (const [index, value] of jsonVal.entries()) {
const out = transform(value, `${path}/${index}`);
const out = transform(
value,
`${visibilityPath}/${index}`,
`${filterPath}[${index}]`,
);
if (out !== undefined) {
arr.push(out);
}
@@ -68,7 +83,11 @@ export function filterByVisibility(
if (value === undefined) {
continue;
}
const out = transform(value, `${path}/${key}`);
const out = transform(
value,
`${visibilityPath}/${key}`,
filterPath ? `${filterPath}.${key}` : key,
);
if (out !== undefined) {
outObj[key] = out;
hasOutput = true;
@@ -81,5 +100,8 @@ export function filterByVisibility(
return undefined;
}
return (transform(data, '') as JsonObject) ?? {};
return {
filteredKeys: withFilteredKeys ? filteredKeys : undefined,
data: (transform(data, '', '') as JsonObject) ?? {},
};
}
@@ -68,13 +68,19 @@ describe('loadConfigSchema', () => {
schema.process(configs, {
visibility: ['frontend'],
valueTransform: () => 'X',
withFilteredKeys: true,
}),
).toEqual([{ data: { key1: 'X' }, context: 'test' }]);
).toEqual([
{ data: { key1: 'X' }, context: 'test', filteredKeys: ['key2'] },
]);
expect(
schema.process(configs, {
valueTransform: () => 'X',
withFilteredKeys: true,
}),
).toEqual([{ data: { key1: 'X', key2: 'X' }, context: 'test' }]);
).toEqual([
{ data: { key1: 'X', key2: 'X' }, context: 'test', filteredKeys: [] },
]);
const serialized = schema.serialize();
@@ -57,7 +57,7 @@ export async function loadConfigSchema(
return {
process(
configs: AppConfig[],
{ visibility, valueTransform } = {},
{ visibility, valueTransform, withFilteredKeys } = {},
): AppConfig[] {
const result = validate(configs);
if (result.errors) {
@@ -73,21 +73,23 @@ export async function loadConfigSchema(
if (visibility) {
processedConfigs = processedConfigs.map(({ data, context }) => ({
context,
data: filterByVisibility(
...filterByVisibility(
data,
visibility,
result.visibilityByPath,
valueTransform,
withFilteredKeys,
),
}));
} else if (valueTransform) {
processedConfigs = processedConfigs.map(({ data, context }) => ({
context,
data: filterByVisibility(
...filterByVisibility(
data,
Array.from(CONFIG_VISIBILITIES),
result.visibilityByPath,
valueTransform,
withFilteredKeys,
),
}));
}
@@ -96,6 +96,13 @@ type ConfigProcessingOptions = {
* will be omitted.
*/
valueTransform?: TransformFunc<any>;
/**
* Whether or not to include the `filteredKeys` property in the output `AppConfig`s.
*
* Default: `false`.
*/
withFilteredKeys?: boolean;
};
/**