@backstage/config-loader 1.11.1 → 1.11.2

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/CHANGELOG.md CHANGED
@@ -1,10 +1,18 @@
1
1
  # @backstage/config-loader
2
2
 
3
- ## 1.11.1
3
+ ## 1.11.2
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - 60d2b50: Added an `onSchemaError` callback that allows callers to report TypeScript configuration schema errors and continue loading. The callback receives a `ConfigSchemaError` containing the source package and underlying cause. Without a handler, schema errors are thrown.
7
+ - 0c67dbc: Fix issue where `backstage-cli config:check --strict` would incorrectly reject valid configuration for open-ended object schemas, such as plain objects or map-like structures.
8
+ - bb98419: Added an `onSchemaError` callback that allows callers to report TypeScript configuration schema errors and continue loading. The callback receives a `ConfigSchemaError` containing the source package and underlying cause. Without a handler, schema errors are thrown.
9
+
10
+ ## 1.11.2-next.0
11
+
12
+ ### Patch Changes
13
+
14
+ - 0c67dbc: Fix issue where `backstage-cli config:check --strict` would incorrectly reject valid configuration for open-ended object schemas, such as plain objects or map-like structures.
15
+ - bb98419: Added an `onSchemaError` callback that allows callers to report TypeScript configuration schema errors and continue loading. The callback receives a `ConfigSchemaError` containing the source package and underlying cause. Without a handler, schema errors are thrown.
8
16
 
9
17
  ## 1.11.0
10
18
 
@@ -6,12 +6,14 @@ var traverse = require('json-schema-traverse');
6
6
  var config = require('@backstage/config');
7
7
  var types = require('./types.cjs.js');
8
8
  var utils = require('./utils.cjs.js');
9
+ var cloneDeep = require('lodash/cloneDeep');
9
10
 
10
11
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
11
12
 
12
13
  var Ajv__default = /*#__PURE__*/_interopDefaultCompat(Ajv);
13
14
  var mergeAllOf__default = /*#__PURE__*/_interopDefaultCompat(mergeAllOf);
14
15
  var traverse__default = /*#__PURE__*/_interopDefaultCompat(traverse);
16
+ var cloneDeep__default = /*#__PURE__*/_interopDefaultCompat(cloneDeep);
15
17
 
16
18
  const inheritedVisibility = /* @__PURE__ */ Symbol("inherited-visibility");
17
19
  function compileConfigSchemas(schemas, options) {
@@ -89,7 +91,17 @@ function compileConfigSchemas(schemas, options) {
89
91
  throw new Error(`Schema at ${schema.path} is invalid, ${error}`);
90
92
  }
91
93
  }
92
- const merged = mergeConfigSchemas(schemas.map((_) => _.value));
94
+ const merged = mergeConfigSchemas(
95
+ schemas.map((_) => {
96
+ const copy = cloneDeep__default.default(_.value);
97
+ traverse__default.default(copy, (s) => {
98
+ if (typeof s.additionalProperties === "object" && s.additionalProperties !== null && Object.keys(s.additionalProperties).length === 0) {
99
+ s.additionalProperties = true;
100
+ }
101
+ });
102
+ return copy;
103
+ })
104
+ );
93
105
  traverse__default.default(
94
106
  merged,
95
107
  (schema, jsonPtr, _1, _2, _3, parentSchema) => {
@@ -109,7 +121,8 @@ function compileConfigSchemas(schemas, options) {
109
121
  }
110
122
  }
111
123
  if (options?.noUndeclaredProperties) {
112
- if (schema?.type === "object") {
124
+ const hasPropertyWithKey = (key) => typeof schema[key] === "object" && schema[key] !== null && Object.keys(schema[key]).length > 0;
125
+ if (schema?.type === "object" && (hasPropertyWithKey("properties") || hasPropertyWithKey("patternProperties"))) {
113
126
  schema.additionalProperties ||= false;
114
127
  }
115
128
  }
@@ -1 +1 @@
1
- {"version":3,"file":"compile.cjs.js","sources":["../../src/schema/compile.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Ajv from 'ajv';\nimport type { JSONSchema7 as JSONSchema } from 'json-schema';\nimport mergeAllOf, { Resolvers } from 'json-schema-merge-allof';\nimport traverse from 'json-schema-traverse';\nimport { ConfigReader } from '@backstage/config';\nimport {\n ConfigSchemaPackageEntry,\n ValidationFunc,\n CONFIG_VISIBILITIES,\n ConfigVisibility,\n} from './types';\nimport { SchemaObject } from 'json-schema-traverse';\nimport { normalizeAjvPath } from './utils';\n\n// Used to keep track of the internal deepVisibility inherited through the schema.\nconst inheritedVisibility = Symbol('inherited-visibility');\n\n/**\n * This takes a collection of Backstage configuration schemas from various\n * sources and compiles them down into a single schema validation function.\n *\n * It also handles the implementation of the custom \"visibility\" keyword used\n * to specify the scope of different config paths.\n */\nexport function compileConfigSchemas(\n schemas: ConfigSchemaPackageEntry[],\n options?: {\n noUndeclaredProperties?: boolean;\n },\n): ValidationFunc {\n // The ajv instance below is stateful and doesn't really allow for additional\n // output during validation. We work around this by having this extra piece\n // of state that we reset before each validation.\n const visibilityByDataPath = new Map<string, ConfigVisibility>();\n const deepVisibilityByDataPath = new Map<string, ConfigVisibility>();\n const deprecationByDataPath = new Map<string, string>();\n\n const ajv = new Ajv({\n allErrors: true,\n allowUnionTypes: true,\n coerceTypes: true,\n schemas: {\n 'https://backstage.io/schema/config-v1': true,\n },\n })\n .addKeyword({\n keyword: 'visibility',\n metaSchema: {\n type: 'string',\n enum: CONFIG_VISIBILITIES,\n },\n compile(visibility: ConfigVisibility) {\n return (_data, context) => {\n if (context?.instancePath === undefined) {\n return false;\n }\n if (visibility && visibility !== 'backend') {\n const normalizedPath = normalizeAjvPath(context.instancePath);\n visibilityByDataPath.set(normalizedPath, visibility);\n }\n return true;\n };\n },\n })\n .addKeyword({\n keyword: 'deepVisibility',\n metaSchema: {\n type: 'string',\n /**\n * Disallow 'backend' deepVisibility to prevent cases of permission escaping.\n *\n * Something like:\n * - deepVisibility secret -> backend -> frontend.\n * - deepVisibility secret -> backend -> visibility frontend.\n */\n enum: ['frontend', 'secret'],\n },\n compile(visibility: 'frontend' | 'secret') {\n return (_data, context) => {\n if (context?.instancePath === undefined) {\n return false;\n }\n if (visibility) {\n const normalizedPath = normalizeAjvPath(context.instancePath);\n deepVisibilityByDataPath.set(normalizedPath, visibility);\n }\n return true;\n };\n },\n })\n .removeKeyword('deprecated') // remove `deprecated` keyword so that we can implement our own compiler\n .addKeyword({\n keyword: 'deprecated',\n metaSchema: { type: 'string' },\n compile(deprecationDescription: string) {\n return (_data, context) => {\n if (context?.instancePath === undefined) {\n return false;\n }\n const normalizedPath = normalizeAjvPath(context.instancePath);\n // create mapping of deprecation description and data path of property\n deprecationByDataPath.set(normalizedPath, deprecationDescription);\n return true;\n };\n },\n });\n\n for (const schema of schemas) {\n try {\n ajv.compile(schema.value);\n } catch (error) {\n throw new Error(`Schema at ${schema.path} is invalid, ${error}`);\n }\n }\n\n const merged = mergeConfigSchemas(schemas.map(_ => _.value));\n\n traverse(\n merged,\n (\n schema: SchemaObject & { [inheritedVisibility]?: ConfigVisibility },\n jsonPtr: string,\n _1: any,\n _2: any,\n _3?: any,\n parentSchema?: SchemaObject & {\n [inheritedVisibility]?: ConfigVisibility;\n },\n ) => {\n // Inherit parent deepVisibility if we don't define one ourselves.\n // This is used to detect situations where conflicting deep visibilities are set.\n schema[inheritedVisibility] ??=\n schema?.deepVisibility ?? parentSchema?.[inheritedVisibility];\n\n if (schema[inheritedVisibility]) {\n // Validate that we're not trying to set a conflicting visibility. This can be done\n // either by setting a conflicting visibility directly or deep visibility\n const values = [\n schema.visibility,\n schema[inheritedVisibility],\n parentSchema?.[inheritedVisibility],\n ];\n const hasFrontend = values.some(e => e === 'frontend');\n const hasSecret = values.some(e => e === 'secret');\n if (hasFrontend && hasSecret) {\n throw new Error(\n `Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}`,\n );\n }\n }\n\n if (options?.noUndeclaredProperties) {\n /**\n * The `additionalProperties` key can only be applied to `type: object` in the JSON\n * schema.\n */\n if (schema?.type === 'object') {\n schema.additionalProperties ||= false;\n }\n }\n },\n );\n\n const validate = ajv.compile(merged);\n\n const visibilityBySchemaPath = new Map<string, ConfigVisibility>();\n traverse(merged, (schema, path) => {\n if (schema.visibility && schema.visibility !== 'backend') {\n visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility);\n }\n if (schema.deepVisibility) {\n visibilityBySchemaPath.set(normalizeAjvPath(path), schema.deepVisibility);\n }\n });\n\n return configs => {\n const config = ConfigReader.fromConfigs(configs).getOptional();\n\n visibilityByDataPath.clear();\n deepVisibilityByDataPath.clear();\n\n const valid = validate(config);\n\n if (!valid) {\n return {\n errors: validate.errors ?? [],\n visibilityByDataPath: new Map(visibilityByDataPath),\n deepVisibilityByDataPath: new Map(deepVisibilityByDataPath),\n visibilityBySchemaPath,\n deprecationByDataPath,\n };\n }\n\n return {\n visibilityByDataPath: new Map(visibilityByDataPath),\n deepVisibilityByDataPath: new Map(deepVisibilityByDataPath),\n visibilityBySchemaPath,\n deprecationByDataPath,\n };\n };\n}\n\n/**\n * Given a list of configuration schemas from packages, merge them\n * into a single json schema.\n *\n * @public\n */\nexport function mergeConfigSchemas(schemas: JSONSchema[]): JSONSchema {\n const merged = mergeAllOf(\n { allOf: schemas },\n {\n // JSONSchema is typically subtractive, as in it always reduces the set of allowed\n // inputs through constraints. This changes the object property merging to be additive\n // rather than subtractive.\n ignoreAdditionalProperties: true,\n resolvers: {\n // This ensures that the visibilities across different schemas are sound, and\n // selects the most specific visibility for each path.\n visibility(values: string[], path: string[]) {\n const hasFrontend = values.some(_ => _ === 'frontend');\n const hasSecret = values.some(_ => _ === 'secret');\n if (hasFrontend && hasSecret) {\n throw new Error(\n `Config schema visibility is both 'frontend' and 'secret' for ${path.join(\n '/',\n )}`,\n );\n } else if (hasFrontend) {\n return 'frontend';\n } else if (hasSecret) {\n return 'secret';\n }\n\n return 'backend';\n },\n } as Partial<Resolvers<JSONSchema>>,\n },\n );\n return merged;\n}\n"],"names":["Ajv","CONFIG_VISIBILITIES","normalizeAjvPath","traverse","config","ConfigReader","mergeAllOf"],"mappings":"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"}
1
+ {"version":3,"file":"compile.cjs.js","sources":["../../src/schema/compile.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Ajv from 'ajv';\nimport type { JSONSchema7 as JSONSchema } from 'json-schema';\nimport mergeAllOf, { Resolvers } from 'json-schema-merge-allof';\nimport traverse from 'json-schema-traverse';\nimport { ConfigReader } from '@backstage/config';\nimport {\n ConfigSchemaPackageEntry,\n ValidationFunc,\n CONFIG_VISIBILITIES,\n ConfigVisibility,\n} from './types';\nimport { SchemaObject } from 'json-schema-traverse';\nimport { normalizeAjvPath } from './utils';\nimport cloneDeep from 'lodash/cloneDeep';\n\n// Used to keep track of the internal deepVisibility inherited through the schema.\nconst inheritedVisibility = Symbol('inherited-visibility');\n\n/**\n * This takes a collection of Backstage configuration schemas from various\n * sources and compiles them down into a single schema validation function.\n *\n * It also handles the implementation of the custom \"visibility\" keyword used\n * to specify the scope of different config paths.\n */\nexport function compileConfigSchemas(\n schemas: ConfigSchemaPackageEntry[],\n options?: {\n noUndeclaredProperties?: boolean;\n },\n): ValidationFunc {\n // The ajv instance below is stateful and doesn't really allow for additional\n // output during validation. We work around this by having this extra piece\n // of state that we reset before each validation.\n const visibilityByDataPath = new Map<string, ConfigVisibility>();\n const deepVisibilityByDataPath = new Map<string, ConfigVisibility>();\n const deprecationByDataPath = new Map<string, string>();\n\n const ajv = new Ajv({\n allErrors: true,\n allowUnionTypes: true,\n coerceTypes: true,\n schemas: {\n 'https://backstage.io/schema/config-v1': true,\n },\n })\n .addKeyword({\n keyword: 'visibility',\n metaSchema: {\n type: 'string',\n enum: CONFIG_VISIBILITIES,\n },\n compile(visibility: ConfigVisibility) {\n return (_data, context) => {\n if (context?.instancePath === undefined) {\n return false;\n }\n if (visibility && visibility !== 'backend') {\n const normalizedPath = normalizeAjvPath(context.instancePath);\n visibilityByDataPath.set(normalizedPath, visibility);\n }\n return true;\n };\n },\n })\n .addKeyword({\n keyword: 'deepVisibility',\n metaSchema: {\n type: 'string',\n /**\n * Disallow 'backend' deepVisibility to prevent cases of permission escaping.\n *\n * Something like:\n * - deepVisibility secret -> backend -> frontend.\n * - deepVisibility secret -> backend -> visibility frontend.\n */\n enum: ['frontend', 'secret'],\n },\n compile(visibility: 'frontend' | 'secret') {\n return (_data, context) => {\n if (context?.instancePath === undefined) {\n return false;\n }\n if (visibility) {\n const normalizedPath = normalizeAjvPath(context.instancePath);\n deepVisibilityByDataPath.set(normalizedPath, visibility);\n }\n return true;\n };\n },\n })\n .removeKeyword('deprecated') // remove `deprecated` keyword so that we can implement our own compiler\n .addKeyword({\n keyword: 'deprecated',\n metaSchema: { type: 'string' },\n compile(deprecationDescription: string) {\n return (_data, context) => {\n if (context?.instancePath === undefined) {\n return false;\n }\n const normalizedPath = normalizeAjvPath(context.instancePath);\n // create mapping of deprecation description and data path of property\n deprecationByDataPath.set(normalizedPath, deprecationDescription);\n return true;\n };\n },\n });\n\n for (const schema of schemas) {\n try {\n ajv.compile(schema.value);\n } catch (error) {\n throw new Error(`Schema at ${schema.path} is invalid, ${error}`);\n }\n }\n\n const merged = mergeConfigSchemas(\n schemas.map(_ => {\n const copy = cloneDeep(_.value as JSONSchema);\n traverse(copy, s => {\n // We convert `additionalProperties: {}` to `additionalProperties: true`\n // because `mergeAllOf` drops `additionalProperties: {}` during merging.\n // According to JSON Schema, these two forms are equivalent. Without this\n // conversion, the dropped value is treated as unset, which causes\n // `noUndeclaredProperties` to incorrectly add `additionalProperties: false`\n // and reject valid configuration.\n if (\n typeof s.additionalProperties === 'object' &&\n s.additionalProperties !== null &&\n Object.keys(s.additionalProperties).length === 0\n ) {\n s.additionalProperties = true;\n }\n });\n return copy;\n }),\n );\n\n traverse(\n merged,\n (\n schema: SchemaObject & { [inheritedVisibility]?: ConfigVisibility },\n jsonPtr: string,\n _1: any,\n _2: any,\n _3?: any,\n parentSchema?: SchemaObject & {\n [inheritedVisibility]?: ConfigVisibility;\n },\n ) => {\n // Inherit parent deepVisibility if we don't define one ourselves.\n // This is used to detect situations where conflicting deep visibilities are set.\n schema[inheritedVisibility] ??=\n schema?.deepVisibility ?? parentSchema?.[inheritedVisibility];\n\n if (schema[inheritedVisibility]) {\n // Validate that we're not trying to set a conflicting visibility. This can be done\n // either by setting a conflicting visibility directly or deep visibility\n const values = [\n schema.visibility,\n schema[inheritedVisibility],\n parentSchema?.[inheritedVisibility],\n ];\n const hasFrontend = values.some(e => e === 'frontend');\n const hasSecret = values.some(e => e === 'secret');\n if (hasFrontend && hasSecret) {\n throw new Error(\n `Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}`,\n );\n }\n }\n\n if (options?.noUndeclaredProperties) {\n const hasPropertyWithKey = (key: string) =>\n typeof schema[key] === 'object' &&\n schema[key] !== null &&\n Object.keys(schema[key]).length > 0;\n /**\n * The `additionalProperties` key can only be applied to `type: object`\n * in the JSON schema. It's only applied to objects that specify\n * properties. Otherwise, we end up with nonsensical schemas that don't\n * allow any properties at all.\n */\n if (\n schema?.type === 'object' &&\n (hasPropertyWithKey('properties') ||\n hasPropertyWithKey('patternProperties'))\n ) {\n schema.additionalProperties ||= false;\n }\n }\n },\n );\n\n const validate = ajv.compile(merged);\n\n const visibilityBySchemaPath = new Map<string, ConfigVisibility>();\n traverse(merged, (schema, path) => {\n if (schema.visibility && schema.visibility !== 'backend') {\n visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility);\n }\n if (schema.deepVisibility) {\n visibilityBySchemaPath.set(normalizeAjvPath(path), schema.deepVisibility);\n }\n });\n\n return configs => {\n const config = ConfigReader.fromConfigs(configs).getOptional();\n\n visibilityByDataPath.clear();\n deepVisibilityByDataPath.clear();\n\n const valid = validate(config);\n\n if (!valid) {\n return {\n errors: validate.errors ?? [],\n visibilityByDataPath: new Map(visibilityByDataPath),\n deepVisibilityByDataPath: new Map(deepVisibilityByDataPath),\n visibilityBySchemaPath,\n deprecationByDataPath,\n };\n }\n\n return {\n visibilityByDataPath: new Map(visibilityByDataPath),\n deepVisibilityByDataPath: new Map(deepVisibilityByDataPath),\n visibilityBySchemaPath,\n deprecationByDataPath,\n };\n };\n}\n\n/**\n * Given a list of configuration schemas from packages, merge them\n * into a single json schema.\n *\n * @public\n */\nexport function mergeConfigSchemas(schemas: JSONSchema[]): JSONSchema {\n const merged = mergeAllOf(\n { allOf: schemas },\n {\n // JSONSchema is typically subtractive, as in it always reduces the set of allowed\n // inputs through constraints. This changes the object property merging to be additive\n // rather than subtractive.\n ignoreAdditionalProperties: true,\n resolvers: {\n // This ensures that the visibilities across different schemas are sound, and\n // selects the most specific visibility for each path.\n visibility(values: string[], path: string[]) {\n const hasFrontend = values.some(_ => _ === 'frontend');\n const hasSecret = values.some(_ => _ === 'secret');\n if (hasFrontend && hasSecret) {\n throw new Error(\n `Config schema visibility is both 'frontend' and 'secret' for ${path.join(\n '/',\n )}`,\n );\n } else if (hasFrontend) {\n return 'frontend';\n } else if (hasSecret) {\n return 'secret';\n }\n\n return 'backend';\n },\n } as Partial<Resolvers<JSONSchema>>,\n },\n );\n return merged;\n}\n"],"names":["Ajv","CONFIG_VISIBILITIES","normalizeAjvPath","cloneDeep","traverse","config","ConfigReader","mergeAllOf"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/config-loader",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "description": "Config loading functionality used by Backstage backend, and CLI",
5
5
  "backstage": {
6
6
  "role": "node-library"
@@ -53,8 +53,8 @@
53
53
  "yaml": "^2.0.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@backstage/backend-test-utils": "^1.11.5",
57
- "@backstage/cli": "^0.36.4",
56
+ "@backstage/backend-test-utils": "^1.11.6",
57
+ "@backstage/cli": "^0.36.5",
58
58
  "@types/json-schema-merge-allof": "^0.6.0",
59
59
  "@types/minimist": "^1.2.5",
60
60
  "msw": "^2.0.0",