frontend-app-api: extract extension config reading into separate module

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-15 13:03:23 +02:00
parent 776d2c3fca
commit 6c6f392c74
5 changed files with 471 additions and 438 deletions
@@ -35,7 +35,6 @@ import {
import {
ExtensionInstanceParameters,
mergeExtensionParameters,
readAppExtensionParameters,
} from './parameters';
import {
AnyApiFactory,
@@ -96,6 +95,7 @@ import { AppRouteBinder } from '../routing';
import { RoutingProvider } from '../routing/RoutingProvider';
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
import { collectRouteIds } from '../routing/collectRouteIds';
import { readAppExtensionsConfig } from './graph/readAppExtensionsConfig';
/** @public */
export interface ExtensionTreeNode {
@@ -200,7 +200,7 @@ export function createInstances(options: {
const extensionParams = mergeExtensionParameters({
features: options.features,
builtinExtensions,
parameters: readAppExtensionParameters(options.config),
parameters: readAppExtensionsConfig(options.config),
});
// TODO: validate the config of all extension instances
@@ -0,0 +1,268 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import {
expandShorthandExtensionParameters,
readAppExtensionsConfig,
} from './readAppExtensionsConfig';
describe('readAppExtensionsConfig', () => {
it('should disable extension with shorthand notation', () => {
expect(
readAppExtensionsConfig(
new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: true,
},
]);
expect(
readAppExtensionsConfig(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: true } }] },
}),
),
).toEqual([
{
at: undefined,
config: undefined,
disabled: true,
id: 'core.router',
},
]);
});
it('should enable extension with shorthand notation', () => {
expect(
readAppExtensionsConfig(
new ConfigReader({ app: { extensions: ['core.router'] } }),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
expect(
readAppExtensionsConfig(
new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
expect(
readAppExtensionsConfig(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: false } }] },
}),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
});
it('should not allow string keys', () => {
expect(() =>
readAppExtensionsConfig(
new ConfigReader({
app: {
extensions: [{ 'core.router': 'some-string' }],
},
}),
),
).toThrow(
'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object',
);
});
it('should not allow invalid keys', () => {
expect(() =>
readAppExtensionsConfig(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
extension: 'example-package#MyPage',
config: { foo: 'bar' },
},
},
],
},
}),
),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[0], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
);
});
});
describe('expandShorthandExtensionParameters', () => {
const run = (value: JsonValue) => {
return expandShorthandExtensionParameters(value, 1);
};
it('rejects unknown keys', () => {
expect(() => run(null)).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
expect(() => run(1)).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
expect(() => run([])).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
});
it('rejects the wrong number of keys', () => {
expect(() => run({})).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must have exactly one key, got none"`,
);
expect(() => run({ a: {}, b: {} })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must have exactly one key, got 'a', 'b'"`,
);
});
it('rejects unknown values', () => {
expect(() => run({ a: 1 })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`,
);
expect(() => run({ a: [] })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`,
);
});
it('supports string key', () => {
expect(run('core.router')).toEqual({
id: 'core.router',
disabled: false,
});
expect(() => run('')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`,
);
expect(() => run(' a')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`,
);
expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
);
});
it('supports null value', () => {
// this is the result of typing:
// - core.router:
// The missing value is interpreted as null by the yaml parser so we deal with that
expect(run({ 'core.router': null })).toEqual({
id: 'core.router',
disabled: false,
});
});
it('supports boolean value', () => {
expect(run({ 'core.router': true })).toEqual({
id: 'core.router',
disabled: false,
});
expect(run({ 'core.router': false })).toEqual({
id: 'core.router',
disabled: true,
});
});
it('should not support string values', () => {
expect(() =>
run({ 'core.router': 'example-package#MyRouter' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router], value must be a boolean or object"`,
);
});
it('supports object id only in the key', () => {
expect(() =>
run({ 'core.router': { id: 'some.id' } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
it('supports object attachTo', () => {
expect(
run({
'core.router': { attachTo: { id: 'other.root', input: 'inputs' } },
}),
).toEqual({
id: 'core.router',
attachTo: { id: 'other.root', input: 'inputs' },
});
expect(() =>
run({
'core.router': {
id: 'other-id',
},
}),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
it('supports object disabled', () => {
expect(run({ 'core.router': { disabled: true } })).toEqual({
id: 'core.router',
disabled: true,
});
expect(run({ 'core.router': { disabled: false } })).toEqual({
id: 'core.router',
disabled: false,
});
expect(() =>
run({ 'core.router': { disabled: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`,
);
});
it('supports object config', () => {
expect(
run({ 'core.router': { config: { disableRedirects: true } } }),
).toEqual({
id: 'core.router',
config: { disableRedirects: true },
});
expect(() =>
run({ 'core.router': { config: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`,
);
});
it('rejects unknown object keys', () => {
expect(() =>
run({ 'core.router': { foo: { settings: true } } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
});
@@ -0,0 +1,199 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { JsonValue } from '@backstage/types';
export interface ExtensionParameters {
id: string;
attachTo?: { id: string; input: string };
disabled?: boolean;
config?: unknown;
}
const knownExtensionParameters = ['attachTo', 'disabled', 'config'];
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
// as the Config interface makes it quite hard for us otherwise.
/** @internal */
export function readAppExtensionsConfig(
rootConfig: Config,
): ExtensionParameters[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
if (arr === undefined) {
return [];
}
// This will throw, and show which part of config had the wrong type
rootConfig.getConfigArray('app.extensions');
return [];
}
return arr.map((arrayEntry, arrayIndex) =>
expandShorthandExtensionParameters(arrayEntry, arrayIndex),
);
}
/** @internal */
export function expandShorthandExtensionParameters(
arrayEntry: JsonValue,
arrayIndex: number,
): ExtensionParameters {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${arrayIndex}]${
key ? `[${key}]` : ''
}${prop ? `.${prop}` : ''}, ${msg}`;
}
// NOTE(freben): This check is intentionally not complete and doesn't check
// whether letters and digits are used, etc. It's not up to the config reading
// logic to decide what constitutes a valid extension ID; that should be
// decided by the logic that loads and instantiates the extensions. This check
// is just here to catch real mistakes or truly conceptually wrong input.
function assertValidId(id: string) {
if (!id || id !== id.trim()) {
throw new Error(
errorMsg('extension ID must not be empty or contain whitespace'),
);
}
if (id.includes('/')) {
let message = `extension ID must not contain slashes; got '${id}'`;
const good = id.split('/')[0];
if (good) {
message += `, did you mean '${good}'?`;
}
throw new Error(errorMsg(message));
}
}
// Example YAML:
// - entity.card.about
if (typeof arrayEntry === 'string') {
assertValidId(arrayEntry);
return {
id: arrayEntry,
disabled: false,
};
}
// All remaining cases are single-key objects
if (
typeof arrayEntry !== 'object' ||
arrayEntry === null ||
Array.isArray(arrayEntry)
) {
throw new Error(errorMsg('must be a string or an object'));
}
const keys = Object.keys(arrayEntry);
if (keys.length !== 1) {
const joinedKeys = keys.length ? `'${keys.join("', '")}'` : 'none';
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
const id = String(keys[0]);
const value = arrayEntry[id];
assertValidId(id);
// This example covers a potentially common mistake in the syntax
// Example YAML:
// - entity.card.about:
if (value === null) {
return {
id,
disabled: false,
};
}
// Example YAML:
// - catalog.page.cicd: false
if (typeof value === 'boolean') {
return {
id,
disabled: !value,
};
}
// The remaining case is the generic object. Example YAML:
// - tech-radar.page:
// at: core.router/routes
// disabled: false
// config:
// path: /tech-radar
// width: 1500
// height: 800
if (typeof value !== 'object' || Array.isArray(value)) {
// We don't mention null here - we don't want people to explicitly enter
// - entity.card.about: null
throw new Error(errorMsg('value must be a boolean or object', id));
}
const attachTo = value.attachTo as { id: string; input: string } | undefined;
const disabled = value.disabled;
const config = value.config;
if (attachTo !== undefined) {
if (
attachTo === null ||
typeof attachTo !== 'object' ||
Array.isArray(attachTo)
) {
throw new Error(errorMsg('must be an object', id, 'attachTo'));
}
if (typeof attachTo.id !== 'string' || attachTo.id === '') {
throw new Error(
errorMsg('must be a non-empty string', id, 'attachTo.id'),
);
}
if (typeof attachTo.input !== 'string' || attachTo.input === '') {
throw new Error(
errorMsg('must be a non-empty string', id, 'attachTo.input'),
);
}
}
if (disabled !== undefined && typeof disabled !== 'boolean') {
throw new Error(errorMsg('must be a boolean', id, 'disabled'));
}
if (
config !== undefined &&
(typeof config !== 'object' || config === null || Array.isArray(config))
) {
throw new Error(errorMsg('must be an object', id, 'config'));
}
const unknownKeys = Object.keys(value).filter(
k => !knownExtensionParameters.includes(k),
);
if (unknownKeys.length > 0) {
throw new Error(
errorMsg(
`unknown parameter; expected one of '${knownExtensionParameters.join(
"', '",
)}'`,
id,
unknownKeys.join(', '),
),
);
}
return {
id,
attachTo,
disabled,
config,
};
}
@@ -14,18 +14,12 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import {
createExtensionOverrides,
createPlugin,
Extension,
} from '@backstage/frontend-plugin-api';
import { JsonValue } from '@backstage/types';
import {
expandShorthandExtensionParameters,
mergeExtensionParameters,
readAppExtensionParameters,
} from './parameters';
import { mergeExtensionParameters } from './parameters';
function makeExt(
id: string,
@@ -208,249 +202,3 @@ describe('mergeExtensionParameters', () => {
expect(result.map(r => r.extension.id)).toEqual(['b', 'c', 'a']);
});
});
describe('readAppExtensionParameters', () => {
it('should disable extension with shorthand notation', () => {
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: true,
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: true } }] },
}),
),
).toEqual([
{
at: undefined,
config: undefined,
disabled: true,
id: 'core.router',
},
]);
});
it('should enable extension with shorthand notation', () => {
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: ['core.router'] } }),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: false } }] },
}),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
});
it('should not allow string keys', () => {
expect(() =>
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [{ 'core.router': 'some-string' }],
},
}),
),
).toThrow(
'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object',
);
});
it('should not allow invalid keys', () => {
expect(() =>
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
extension: 'example-package#MyPage',
config: { foo: 'bar' },
},
},
],
},
}),
),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[0], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
);
});
});
describe('expandShorthandExtensionParameters', () => {
const run = (value: JsonValue) => {
return expandShorthandExtensionParameters(value, 1);
};
it('rejects unknown keys', () => {
expect(() => run(null)).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
expect(() => run(1)).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
expect(() => run([])).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
});
it('rejects the wrong number of keys', () => {
expect(() => run({})).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must have exactly one key, got none"`,
);
expect(() => run({ a: {}, b: {} })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must have exactly one key, got 'a', 'b'"`,
);
});
it('rejects unknown values', () => {
expect(() => run({ a: 1 })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`,
);
expect(() => run({ a: [] })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`,
);
});
it('supports string key', () => {
expect(run('core.router')).toEqual({
id: 'core.router',
disabled: false,
});
expect(() => run('')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`,
);
expect(() => run(' a')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`,
);
expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
);
});
it('supports null value', () => {
// this is the result of typing:
// - core.router:
// The missing value is interpreted as null by the yaml parser so we deal with that
expect(run({ 'core.router': null })).toEqual({
id: 'core.router',
disabled: false,
});
});
it('supports boolean value', () => {
expect(run({ 'core.router': true })).toEqual({
id: 'core.router',
disabled: false,
});
expect(run({ 'core.router': false })).toEqual({
id: 'core.router',
disabled: true,
});
});
it('should not support string values', () => {
expect(() =>
run({ 'core.router': 'example-package#MyRouter' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router], value must be a boolean or object"`,
);
});
it('supports object id only in the key', () => {
expect(() =>
run({ 'core.router': { id: 'some.id' } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
it('supports object attachTo', () => {
expect(
run({
'core.router': { attachTo: { id: 'other.root', input: 'inputs' } },
}),
).toEqual({
id: 'core.router',
attachTo: { id: 'other.root', input: 'inputs' },
});
expect(() =>
run({
'core.router': {
id: 'other-id',
},
}),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
it('supports object disabled', () => {
expect(run({ 'core.router': { disabled: true } })).toEqual({
id: 'core.router',
disabled: true,
});
expect(run({ 'core.router': { disabled: false } })).toEqual({
id: 'core.router',
disabled: false,
});
expect(() =>
run({ 'core.router': { disabled: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`,
);
});
it('supports object config', () => {
expect(
run({ 'core.router': { config: { disableRedirects: true } } }),
).toEqual({
id: 'core.router',
config: { disableRedirects: true },
});
expect(() =>
run({ 'core.router': { config: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`,
);
});
it('rejects unknown object keys', () => {
expect(() =>
run({ 'core.router': { foo: { settings: true } } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
});
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Config } from '@backstage/config';
import {
BackstagePlugin,
Extension,
@@ -22,188 +21,7 @@ import {
} from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
import { JsonValue } from '@backstage/types';
export interface ExtensionParameters {
id: string;
attachTo?: { id: string; input: string };
disabled?: boolean;
config?: unknown;
}
const knownExtensionParameters = ['attachTo', 'disabled', 'config'];
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
// as the Config interface makes it quite hard for us otherwise.
/** @internal */
export function readAppExtensionParameters(
rootConfig: Config,
): ExtensionParameters[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
if (arr === undefined) {
return [];
}
// This will throw, and show which part of config had the wrong type
rootConfig.getConfigArray('app.extensions');
return [];
}
return arr.map((arrayEntry, arrayIndex) =>
expandShorthandExtensionParameters(arrayEntry, arrayIndex),
);
}
/** @internal */
export function expandShorthandExtensionParameters(
arrayEntry: JsonValue,
arrayIndex: number,
): ExtensionParameters {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${arrayIndex}]${
key ? `[${key}]` : ''
}${prop ? `.${prop}` : ''}, ${msg}`;
}
// NOTE(freben): This check is intentionally not complete and doesn't check
// whether letters and digits are used, etc. It's not up to the config reading
// logic to decide what constitutes a valid extension ID; that should be
// decided by the logic that loads and instantiates the extensions. This check
// is just here to catch real mistakes or truly conceptually wrong input.
function assertValidId(id: string) {
if (!id || id !== id.trim()) {
throw new Error(
errorMsg('extension ID must not be empty or contain whitespace'),
);
}
if (id.includes('/')) {
let message = `extension ID must not contain slashes; got '${id}'`;
const good = id.split('/')[0];
if (good) {
message += `, did you mean '${good}'?`;
}
throw new Error(errorMsg(message));
}
}
// Example YAML:
// - entity.card.about
if (typeof arrayEntry === 'string') {
assertValidId(arrayEntry);
return {
id: arrayEntry,
disabled: false,
};
}
// All remaining cases are single-key objects
if (
typeof arrayEntry !== 'object' ||
arrayEntry === null ||
Array.isArray(arrayEntry)
) {
throw new Error(errorMsg('must be a string or an object'));
}
const keys = Object.keys(arrayEntry);
if (keys.length !== 1) {
const joinedKeys = keys.length ? `'${keys.join("', '")}'` : 'none';
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
const id = String(keys[0]);
const value = arrayEntry[id];
assertValidId(id);
// This example covers a potentially common mistake in the syntax
// Example YAML:
// - entity.card.about:
if (value === null) {
return {
id,
disabled: false,
};
}
// Example YAML:
// - catalog.page.cicd: false
if (typeof value === 'boolean') {
return {
id,
disabled: !value,
};
}
// The remaining case is the generic object. Example YAML:
// - tech-radar.page:
// at: core.router/routes
// disabled: false
// config:
// path: /tech-radar
// width: 1500
// height: 800
if (typeof value !== 'object' || Array.isArray(value)) {
// We don't mention null here - we don't want people to explicitly enter
// - entity.card.about: null
throw new Error(errorMsg('value must be a boolean or object', id));
}
const attachTo = value.attachTo as { id: string; input: string } | undefined;
const disabled = value.disabled;
const config = value.config;
if (attachTo !== undefined) {
if (
attachTo === null ||
typeof attachTo !== 'object' ||
Array.isArray(attachTo)
) {
throw new Error(errorMsg('must be an object', id, 'attachTo'));
}
if (typeof attachTo.id !== 'string' || attachTo.id === '') {
throw new Error(
errorMsg('must be a non-empty string', id, 'attachTo.id'),
);
}
if (typeof attachTo.input !== 'string' || attachTo.input === '') {
throw new Error(
errorMsg('must be a non-empty string', id, 'attachTo.input'),
);
}
}
if (disabled !== undefined && typeof disabled !== 'boolean') {
throw new Error(errorMsg('must be a boolean', id, 'disabled'));
}
if (
config !== undefined &&
(typeof config !== 'object' || config === null || Array.isArray(config))
) {
throw new Error(errorMsg('must be an object', id, 'config'));
}
const unknownKeys = Object.keys(value).filter(
k => !knownExtensionParameters.includes(k),
);
if (unknownKeys.length > 0) {
throw new Error(
errorMsg(
`unknown parameter; expected one of '${knownExtensionParameters.join(
"', '",
)}'`,
id,
unknownKeys.join(', '),
),
);
}
return {
id,
attachTo,
disabled,
config,
};
}
import { ExtensionParameters } from './graph/readAppExtensionsConfig';
export interface ExtensionInstanceParameters {
extension: Extension<unknown>;