@backstage/plugin-catalog-backend 3.6.0-next.2 → 3.6.1-next.0

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/actions/createGetCatalogModelDescriptionAction.cjs.js +149 -0
  3. package/dist/actions/createGetCatalogModelDescriptionAction.cjs.js.map +1 -0
  4. package/dist/actions/index.cjs.js +2 -0
  5. package/dist/actions/index.cjs.js.map +1 -1
  6. package/dist/model/ModelHolder.cjs.js +53 -0
  7. package/dist/model/ModelHolder.cjs.js.map +1 -0
  8. package/dist/processors/ModelProcessor.cjs.js +113 -0
  9. package/dist/processors/ModelProcessor.cjs.js.map +1 -0
  10. package/dist/processors/SchemaValidator.cjs.js +77 -0
  11. package/dist/processors/SchemaValidator.cjs.js.map +1 -0
  12. package/dist/providers/DefaultLocationStore.cjs.js +74 -13
  13. package/dist/providers/DefaultLocationStore.cjs.js.map +1 -1
  14. package/dist/schema/openapi/generated/router.cjs.js +51 -1
  15. package/dist/schema/openapi/generated/router.cjs.js.map +1 -1
  16. package/dist/service/AuthorizedLocationService.cjs.js +10 -0
  17. package/dist/service/AuthorizedLocationService.cjs.js.map +1 -1
  18. package/dist/service/CatalogBuilder.cjs.js +5 -1
  19. package/dist/service/CatalogBuilder.cjs.js.map +1 -1
  20. package/dist/service/CatalogPlugin.cjs.js +18 -3
  21. package/dist/service/CatalogPlugin.cjs.js.map +1 -1
  22. package/dist/service/DefaultEntitiesCatalog.cjs.js +4 -2
  23. package/dist/service/DefaultEntitiesCatalog.cjs.js.map +1 -1
  24. package/dist/service/DefaultLocationService.cjs.js +15 -1
  25. package/dist/service/DefaultLocationService.cjs.js.map +1 -1
  26. package/dist/service/createRouter.cjs.js +24 -0
  27. package/dist/service/createRouter.cjs.js.map +1 -1
  28. package/dist/service/request/common.cjs.js.map +1 -1
  29. package/migrations/20200702153613_entities.js +1 -1
  30. package/migrations/20201005122705_add_entity_full_name.js +2 -2
  31. package/migrations/20210302150147_refresh_state.js +2 -25
  32. package/migrations/20220116144621_remove_legacy.js +86 -1
  33. package/migrations/20241003170511_alter_target_in_locations.js +2 -2
  34. package/package.json +22 -20
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 3.6.1-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - b33f845: Fixed several database migration `down` functions that were not properly reversible, causing the SQL report to show warnings:
8
+
9
+ - `20241003170511_alter_target_in_locations.js`: both `up` and `down` now include `.notNullable()` when altering the `locations.target` column, preventing the `NOT NULL` constraint from being accidentally dropped when widening the column type from `varchar(255)` to `text`.
10
+ - `20220116144621_remove_legacy.js`: the `down` function now properly recreates the three dropped legacy tables (`entities`, `entities_search`, `entities_relations`) with correct columns and indices.
11
+ - `20210302150147_refresh_state.js`: the `down` function now drops dependent tables in the correct order (avoiding a FK constraint violation) and fixes a typo where the table was referred to as `references` instead of `refresh_state_references`.
12
+ - `20201005122705_add_entity_full_name.js`: the `down` function now drops the `full_name` column from `entities` (not `entities_search`), and restores the `entities_unique_name` index with the correct column order `(kind, name, namespace)`.
13
+ - `20200702153613_entities.js`: the `down` function now uses `table.integer('generation')` instead of `table.string('generation')`, restoring the correct column type.
14
+
15
+ - cf195de: Fixed a performance regression in the `/entity-facets` endpoint when filters or permission conditions are applied, by routing the EXISTS-based filter through `final_entities` instead of correlating against the much larger `search` table.
16
+ - 744fa1f: Removed duplicated entries that appeared in both `dependencies` and `devDependencies`.
17
+ - Updated dependencies
18
+ - @backstage/errors@1.3.1-next.0
19
+ - @backstage/integration@2.0.2-next.0
20
+ - @backstage/backend-openapi-utils@0.6.9-next.0
21
+ - @backstage/backend-plugin-api@1.9.1-next.0
22
+ - @backstage/catalog-client@1.15.1-next.0
23
+ - @backstage/catalog-model@1.8.1-next.0
24
+ - @backstage/config@1.3.8-next.0
25
+ - @backstage/filter-predicates@0.1.3-next.0
26
+ - @backstage/plugin-catalog-node@2.2.1-next.0
27
+ - @backstage/plugin-events-node@0.4.22-next.0
28
+ - @backstage/plugin-permission-common@0.9.9-next.0
29
+ - @backstage/plugin-permission-node@0.10.13-next.0
30
+ - @backstage/types@1.2.2
31
+ - @backstage/plugin-catalog-common@1.1.10-next.0
32
+
33
+ ## 3.6.0
34
+
35
+ ### Minor Changes
36
+
37
+ - d16311f: Added a `location_entity_ref` column to the `locations` database table that stores the full entity ref of the corresponding `kind: Location` catalog entity for each registered location row. The value is pre-computed and persisted so that it no longer needs to be recomputed from the location's type and target on every read.
38
+ - e5fcfcb: Added `ModelProcessor` that validates catalog entities against the compiled catalog model schemas, and integrated it into the `CatalogBuilder` and `CatalogPlugin`. This processor is only activated if you explicitly add catalog model sources to your backend; there is no functional change for regular catalog usage.
39
+ - c384fff: Location responses now include an `entityRef` field with the stable entity reference for each location. The `entityRef` field is also filterable via `POST /locations/by-query`. Added `PUT /locations/:id` endpoint for updating the type and target of an existing location.
40
+
41
+ ### Patch Changes
42
+
43
+ - 2e5c5f8: Bumped `glob` dependency from v7/v8/v11 to v13 to address security vulnerabilities in older versions. Bumped `rollup` from v4.27 to v4.59+ to fix a high severity path traversal vulnerability (GHSA-mw96-cpmx-2vgc).
44
+ - 7e63730: Removed deprecated `PermissionAuthorizer` support and the `createPermissionIntegrationRouter` fallback path from `CatalogBuilder`. The `permissionsRegistry` service is now required, and `permissions` is always a `PermissionsService`.
45
+ - 056e18e: Removed the internal `addPermissions` and `addPermissionRules` methods from `CatalogBuilder`, and removed the `catalogPermissionExtensionPoint` wiring from `CatalogPlugin`. Custom permission rules and permissions should be registered via `coreServices.permissionsRegistry` directly.
46
+ - 6884814: Improved catalog entity filter query performance by switching from `IN (subquery)` to `EXISTS (correlated subquery)` patterns. This enables PostgreSQL semi-join optimizations and fixes `NOT IN` NULL-semantics pitfalls by using `NOT EXISTS` instead.
47
+ - 9da73bf: Reduced search table write churn during stitching by syncing only changed rows instead of doing a full delete and re-insert. On Postgres this uses a single writable CTE, on MySQL a temporary table merge with deadlock retry, and on SQLite the previous bulk replace.
48
+ - 482ceed: Migrated from `assertError` to `toError` for error handling.
49
+ - 375b546: Fixed a deadlock in the catalog processing loop that occurred when running multiple replicas. The `getProcessableEntities` method used `SELECT ... FOR UPDATE SKIP LOCKED` to prevent concurrent processors from claiming the same rows, but the call was not wrapped in a transaction, so the row locks were released before the subsequent `UPDATE` executed. This allowed multiple replicas to select and update overlapping rows, causing PostgreSQL deadlock errors (code 40P01).
50
+ - 79453c0: Updated dependency `wait-for-expect` to `^4.0.0`.
51
+ - Updated dependencies
52
+ - @backstage/backend-plugin-api@1.9.0
53
+ - @backstage/errors@1.3.0
54
+ - @backstage/catalog-model@1.8.0
55
+ - @backstage/plugin-catalog-node@2.2.0
56
+ - @backstage/filter-predicates@0.1.2
57
+ - @backstage/catalog-client@1.15.0
58
+ - @backstage/backend-openapi-utils@0.6.8
59
+ - @backstage/integration@2.0.1
60
+ - @backstage/plugin-permission-node@0.10.12
61
+ - @backstage/config@1.3.7
62
+ - @backstage/plugin-catalog-common@1.1.9
63
+ - @backstage/plugin-events-node@0.4.21
64
+ - @backstage/plugin-permission-common@0.9.8
65
+
3
66
  ## 3.6.0-next.2
4
67
 
5
68
  ### Minor Changes
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ var alpha = require('@backstage/catalog-model/alpha');
4
+
5
+ const createGetCatalogModelDescriptionAction = ({
6
+ modelHolder,
7
+ actionsRegistry
8
+ }) => {
9
+ actionsRegistry.register({
10
+ name: "get-catalog-model-description",
11
+ title: "Get a Catalog Model Description",
12
+ description: "Returns a markdown formatted description of the current catalog model, including all registered entity kinds, annotations, labels, tags, and relations.",
13
+ attributes: {
14
+ destructive: false,
15
+ readOnly: true,
16
+ idempotent: true
17
+ },
18
+ schema: {
19
+ input: (z) => z.object({}),
20
+ output: (z) => z.object({
21
+ description: z.string().describe(
22
+ "Markdown description of the catalog model including entity kinds, spec fields, and relations."
23
+ )
24
+ })
25
+ },
26
+ action: async () => {
27
+ return {
28
+ output: {
29
+ description: describeCatalogModel(modelHolder?.model)
30
+ }
31
+ };
32
+ }
33
+ });
34
+ };
35
+ let defaultModelDescription;
36
+ function describeCatalogModel(model) {
37
+ if (!model) {
38
+ if (!defaultModelDescription) {
39
+ defaultModelDescription = describeModel(
40
+ alpha.compileCatalogModel([alpha.defaultCatalogEntityModel])
41
+ );
42
+ }
43
+ return defaultModelDescription;
44
+ }
45
+ return describeModel(model);
46
+ }
47
+ function describeModel(model) {
48
+ return `# Catalog Model
49
+
50
+ The software catalog contains a few key concepts:
51
+
52
+ * Entities
53
+ * rich objects with various kinds, with different schemas
54
+ * can have relations between each other
55
+ * Locations
56
+ * registered as type/target pairs
57
+ * typically in the form of URLs that the catalog has been tasked with keeping track of
58
+
59
+ ## Entities
60
+
61
+ The catalog contains entities of different kinds. Every entity is an object with
62
+ fields "kind", "apiVersion", "metadata", and optionally "spec" and "relations".
63
+
64
+ When querying the catalog you use dot path notation to address fields, e.g. "metadata.name".
65
+ When querying for entity relationships, prefer using relations over spec fields, e.g. the
66
+ special syntax "relations.ownedBy" instead of "spec.owner".
67
+
68
+ The unique identifying key for an entity is its so called entity reference (or entity ref
69
+ for short), on the form of "kind:namespace/name", e.g. "component:default/my-service".
70
+ These are always uniformly lowercased in the "en-US" locale.
71
+
72
+ ## Entity Metadata field
73
+
74
+ The "metadata" object field on all entities has the same static schema. Some common fields
75
+ there are:
76
+
77
+ * "name": The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair. This value is part of the technical identifier of the entity, and as such it will appear in URLs, database tables, entity references, and similar. It is subject to restrictions regarding what characters are allowed. If you want to use a different, more human readable string with fewer restrictions on it in user interfaces, see the "title" field below.
78
+ * "namespace" (default value: "default"): The namespace that the entity belongs to.
79
+ * "uid": A globally unique ID for the entity. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics.
80
+ * "etag": An opaque string that changes for each update operation to any part of the entity, including metadata. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, and the server will then reject the operation if it does not match the current stored value.
81
+ * "title": A display name of the entity, to be presented in user interfaces instead of the name property, when available. This field is sometimes useful when the name is cumbersome or ends up being perceived as overly technical. The title generally does not have as stringent format requirements on it, so it may contain special characters and be more explanatory. Do keep it very short though, and avoid situations where a title can be confused with the name of another entity, or where two entities share a title. Note that this is only for display purposes, and may be ignored by some parts of the code. Entity references still always make use of the name property, not the title.
82
+ * "description": A short (typically relatively few words, on one line) description of the entity.
83
+ * "annotations": A map of annotations on the entity.
84
+ * "labels": A map of labels on the entity.
85
+ * "tags": A list of tags on the entity.
86
+ * "links": A list of links on the entity.
87
+
88
+ ${model.getMetadata().annotations.map(describeAnnotation).join("\n")}
89
+ ${model.getMetadata().labels.map(describeLabel).join("\n")}
90
+ ${model.getMetadata().tags.map(describeTag).join("\n")}
91
+
92
+ ## Entity Kinds
93
+
94
+ The model contains the following entity kinds:
95
+
96
+ ${model.listKinds().map(describeKind).join("\n")}
97
+
98
+ ## Entity Relations
99
+
100
+ The model contains the following entity relations:
101
+
102
+ ${model.listRelations().map(describeRelation).join("\n")}
103
+ `;
104
+ }
105
+ function describeKind(kind) {
106
+ return `### Entity Kind "${kind.names.kind}"
107
+
108
+ * Singular in text: "${kind.names.singular}"
109
+ * Plural in text: "${kind.names.plural}"
110
+ * Versions:
111
+ ${kind.versions.map(
112
+ (version) => ` * apiVersion: "${version.apiVersion}"${version.specType ? ` (spec.type: "${version.specType}")` : ""}`
113
+ ).join("\n")}
114
+
115
+ ${kind.description}
116
+ `;
117
+ }
118
+ function describeAnnotation(annotation) {
119
+ const title = annotation.title ? `: ${annotation.title}` : "";
120
+ return `### Annotation "${annotation.name}"${title}
121
+
122
+ ${annotation.description}
123
+ `;
124
+ }
125
+ function describeLabel(label) {
126
+ const title = label.title ? `: ${label.title}` : "";
127
+ return `### Label "${label.name}"${title}
128
+
129
+ ${label.description}
130
+ `;
131
+ }
132
+ function describeTag(tag) {
133
+ const title = tag.title ? `: ${tag.title}` : "";
134
+ return `### Tag "${tag.name}"${title}
135
+
136
+ ${tag.description}
137
+ `;
138
+ }
139
+ function describeRelation(relation) {
140
+ return `### Relation "${relation.forward.type}": ${relation.forward.title}
141
+
142
+ * Reverse type: "${relation.reverse.type}": ${relation.reverse.title}
143
+
144
+ ${relation.description}
145
+ `;
146
+ }
147
+
148
+ exports.createGetCatalogModelDescriptionAction = createGetCatalogModelDescriptionAction;
149
+ //# sourceMappingURL=createGetCatalogModelDescriptionAction.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createGetCatalogModelDescriptionAction.cjs.js","sources":["../../src/actions/createGetCatalogModelDescriptionAction.ts"],"sourcesContent":["/*\n * Copyright 2026 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport {\n CatalogModel,\n CatalogModelAnnotationSummary,\n CatalogModelKindSummary,\n CatalogModelLabelSummary,\n CatalogModelRelationSummary,\n CatalogModelTagSummary,\n compileCatalogModel,\n defaultCatalogEntityModel,\n} from '@backstage/catalog-model/alpha';\nimport { ModelHolder } from '../model/ModelHolder';\n\n/**\n * Lets users fetch a markdown formatted description of the catalog model. This\n * is useful for informing LLMs how to properly work with it.\n */\nexport const createGetCatalogModelDescriptionAction = ({\n modelHolder,\n actionsRegistry,\n}: {\n modelHolder: ModelHolder | undefined;\n actionsRegistry: ActionsRegistryService;\n}) => {\n actionsRegistry.register({\n name: 'get-catalog-model-description',\n title: 'Get a Catalog Model Description',\n description:\n 'Returns a markdown formatted description of the current catalog model, including all registered entity kinds, annotations, labels, tags, and relations.',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n schema: {\n input: z => z.object({}),\n output: z =>\n z.object({\n description: z\n .string()\n .describe(\n 'Markdown description of the catalog model including entity kinds, spec fields, and relations.',\n ),\n }),\n },\n action: async () => {\n return {\n output: {\n description: describeCatalogModel(modelHolder?.model),\n },\n };\n },\n });\n};\n\n// Compute the default description once (if needed) and cache it\nlet defaultModelDescription: string | undefined;\n\nfunction describeCatalogModel(model: CatalogModel | undefined): string {\n if (!model) {\n if (!defaultModelDescription) {\n defaultModelDescription = describeModel(\n compileCatalogModel([defaultCatalogEntityModel]),\n );\n }\n return defaultModelDescription;\n }\n\n return describeModel(model);\n}\n\nfunction describeModel(model: CatalogModel): string {\n return `# Catalog Model\n\nThe software catalog contains a few key concepts:\n\n* Entities\n * rich objects with various kinds, with different schemas\n * can have relations between each other\n* Locations\n * registered as type/target pairs\n * typically in the form of URLs that the catalog has been tasked with keeping track of\n\n## Entities\n\nThe catalog contains entities of different kinds. Every entity is an object with\nfields \"kind\", \"apiVersion\", \"metadata\", and optionally \"spec\" and \"relations\".\n\nWhen querying the catalog you use dot path notation to address fields, e.g. \"metadata.name\".\nWhen querying for entity relationships, prefer using relations over spec fields, e.g. the\nspecial syntax \"relations.ownedBy\" instead of \"spec.owner\".\n\nThe unique identifying key for an entity is its so called entity reference (or entity ref\nfor short), on the form of \"kind:namespace/name\", e.g. \"component:default/my-service\".\nThese are always uniformly lowercased in the \"en-US\" locale.\n\n## Entity Metadata field\n\nThe \"metadata\" object field on all entities has the same static schema. Some common fields\nthere are:\n\n* \"name\": The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair. This value is part of the technical identifier of the entity, and as such it will appear in URLs, database tables, entity references, and similar. It is subject to restrictions regarding what characters are allowed. If you want to use a different, more human readable string with fewer restrictions on it in user interfaces, see the \"title\" field below.\n* \"namespace\" (default value: \"default\"): The namespace that the entity belongs to.\n* \"uid\": A globally unique ID for the entity. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics.\n* \"etag\": An opaque string that changes for each update operation to any part of the entity, including metadata. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, and the server will then reject the operation if it does not match the current stored value.\n* \"title\": A display name of the entity, to be presented in user interfaces instead of the name property, when available. This field is sometimes useful when the name is cumbersome or ends up being perceived as overly technical. The title generally does not have as stringent format requirements on it, so it may contain special characters and be more explanatory. Do keep it very short though, and avoid situations where a title can be confused with the name of another entity, or where two entities share a title. Note that this is only for display purposes, and may be ignored by some parts of the code. Entity references still always make use of the name property, not the title.\n* \"description\": A short (typically relatively few words, on one line) description of the entity.\n* \"annotations\": A map of annotations on the entity.\n* \"labels\": A map of labels on the entity.\n* \"tags\": A list of tags on the entity.\n* \"links\": A list of links on the entity.\n\n${model.getMetadata().annotations.map(describeAnnotation).join('\\n')}\n${model.getMetadata().labels.map(describeLabel).join('\\n')}\n${model.getMetadata().tags.map(describeTag).join('\\n')}\n\n## Entity Kinds\n\nThe model contains the following entity kinds:\n\n${model.listKinds().map(describeKind).join('\\n')}\n\n## Entity Relations\n\nThe model contains the following entity relations:\n\n${model.listRelations().map(describeRelation).join('\\n')}\n`;\n}\n\nfunction describeKind(kind: CatalogModelKindSummary): string {\n return `### Entity Kind \"${kind.names.kind}\"\n\n* Singular in text: \"${kind.names.singular}\"\n* Plural in text: \"${kind.names.plural}\"\n* Versions:\n${kind.versions\n .map(\n version =>\n ` * apiVersion: \"${version.apiVersion}\"${\n version.specType ? ` (spec.type: \"${version.specType}\")` : ''\n }`,\n )\n .join('\\n')}\n\n${kind.description}\n`;\n}\n\nfunction describeAnnotation(annotation: CatalogModelAnnotationSummary): string {\n const title = annotation.title ? `: ${annotation.title}` : '';\n return `### Annotation \"${annotation.name}\"${title}\n\n${annotation.description}\n`;\n}\n\nfunction describeLabel(label: CatalogModelLabelSummary): string {\n const title = label.title ? `: ${label.title}` : '';\n return `### Label \"${label.name}\"${title}\n\n${label.description}\n`;\n}\n\nfunction describeTag(tag: CatalogModelTagSummary): string {\n const title = tag.title ? `: ${tag.title}` : '';\n return `### Tag \"${tag.name}\"${title}\n\n${tag.description}\n`;\n}\n\nfunction describeRelation(relation: CatalogModelRelationSummary): string {\n return `### Relation \"${relation.forward.type}\": ${relation.forward.title}\n\n* Reverse type: \"${relation.reverse.type}\": ${relation.reverse.title}\n\n${relation.description}\n`;\n}\n"],"names":["compileCatalogModel","defaultCatalogEntityModel"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var createGetCatalogModelDescriptionAction = require('./createGetCatalogModelDescriptionAction.cjs.js');
3
4
  var createGetCatalogEntityAction = require('./createGetCatalogEntityAction.cjs.js');
4
5
  var createValidateEntityAction = require('./createValidateEntityAction.cjs.js');
5
6
  var createRegisterCatalogEntitiesAction = require('./createRegisterCatalogEntitiesAction.cjs.js');
@@ -7,6 +8,7 @@ var createUnregisterCatalogEntitiesAction = require('./createUnregisterCatalogEn
7
8
  var createQueryCatalogEntitiesAction = require('./createQueryCatalogEntitiesAction.cjs.js');
8
9
 
9
10
  const createCatalogActions = (options) => {
11
+ createGetCatalogModelDescriptionAction.createGetCatalogModelDescriptionAction(options);
10
12
  createGetCatalogEntityAction.createGetCatalogEntityAction(options);
11
13
  createValidateEntityAction.createValidateEntityAction(options);
12
14
  createRegisterCatalogEntitiesAction.createRegisterCatalogEntitiesAction(options);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../../src/actions/index.ts"],"sourcesContent":["/*\n * Copyright 2025 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 */\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { createGetCatalogEntityAction } from './createGetCatalogEntityAction.ts';\nimport { createValidateEntityAction } from './createValidateEntityAction.ts';\nimport { createRegisterCatalogEntitiesAction } from './createRegisterCatalogEntitiesAction.ts';\nimport { createUnregisterCatalogEntitiesAction } from './createUnregisterCatalogEntitiesAction.ts';\nimport { createQueryCatalogEntitiesAction } from './createQueryCatalogEntitiesAction.ts';\n\nexport const createCatalogActions = (options: {\n actionsRegistry: ActionsRegistryService;\n catalog: CatalogService;\n}) => {\n createGetCatalogEntityAction(options);\n createValidateEntityAction(options);\n createRegisterCatalogEntitiesAction(options);\n createUnregisterCatalogEntitiesAction(options);\n createQueryCatalogEntitiesAction(options);\n};\n"],"names":["createGetCatalogEntityAction","createValidateEntityAction","createRegisterCatalogEntitiesAction","createUnregisterCatalogEntitiesAction","createQueryCatalogEntitiesAction"],"mappings":";;;;;;;;AAuBO,MAAM,oBAAA,GAAuB,CAAC,OAAA,KAG/B;AACJ,EAAAA,yDAAA,CAA6B,OAAO,CAAA;AACpC,EAAAC,qDAAA,CAA2B,OAAO,CAAA;AAClC,EAAAC,uEAAA,CAAoC,OAAO,CAAA;AAC3C,EAAAC,2EAAA,CAAsC,OAAO,CAAA;AAC7C,EAAAC,iEAAA,CAAiC,OAAO,CAAA;AAC1C;;;;"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../../src/actions/index.ts"],"sourcesContent":["/*\n * Copyright 2025 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { ModelHolder } from '../model/ModelHolder';\nimport { createGetCatalogModelDescriptionAction } from './createGetCatalogModelDescriptionAction.ts';\nimport { createGetCatalogEntityAction } from './createGetCatalogEntityAction.ts';\nimport { createValidateEntityAction } from './createValidateEntityAction.ts';\nimport { createRegisterCatalogEntitiesAction } from './createRegisterCatalogEntitiesAction.ts';\nimport { createUnregisterCatalogEntitiesAction } from './createUnregisterCatalogEntitiesAction.ts';\nimport { createQueryCatalogEntitiesAction } from './createQueryCatalogEntitiesAction.ts';\n\nexport const createCatalogActions = (options: {\n actionsRegistry: ActionsRegistryService;\n catalog: CatalogService;\n modelHolder: ModelHolder | undefined;\n}) => {\n createGetCatalogModelDescriptionAction(options);\n createGetCatalogEntityAction(options);\n createValidateEntityAction(options);\n createRegisterCatalogEntitiesAction(options);\n createUnregisterCatalogEntitiesAction(options);\n createQueryCatalogEntitiesAction(options);\n};\n"],"names":["createGetCatalogModelDescriptionAction","createGetCatalogEntityAction","createValidateEntityAction","createRegisterCatalogEntitiesAction","createUnregisterCatalogEntitiesAction","createQueryCatalogEntitiesAction"],"mappings":";;;;;;;;;AA0BO,MAAM,oBAAA,GAAuB,CAAC,OAAA,KAI/B;AACJ,EAAAA,6EAAA,CAAuC,OAAO,CAAA;AAC9C,EAAAC,yDAAA,CAA6B,OAAO,CAAA;AACpC,EAAAC,qDAAA,CAA2B,OAAO,CAAA;AAClC,EAAAC,uEAAA,CAAoC,OAAO,CAAA;AAC3C,EAAAC,2EAAA,CAAsC,OAAO,CAAA;AAC7C,EAAAC,iEAAA,CAAiC,OAAO,CAAA;AAC1C;;;;"}
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ var alpha = require('@backstage/catalog-model/alpha');
4
+
5
+ class ModelHolder {
6
+ #model;
7
+ static modelPassthroughForTest(model) {
8
+ return new ModelHolder(model);
9
+ }
10
+ static async create(options) {
11
+ const { sources, logger, lifecycle } = options;
12
+ const shutdownController = new AbortController();
13
+ lifecycle.addShutdownHook(() => shutdownController.abort());
14
+ logger.info(`Reading ${sources.length} catalog model sources`);
15
+ let readyCount = 0;
16
+ const logInterval = setInterval(() => {
17
+ const remaining = sources.length - readyCount;
18
+ logger.warn(
19
+ `Waiting for ${remaining}/${sources.length} catalog model sources to be ready`
20
+ );
21
+ }, 3e3);
22
+ try {
23
+ const layers = await Promise.all(
24
+ sources.map(async (source) => {
25
+ const iter = source.read({ signal: shutdownController.signal });
26
+ try {
27
+ const result = await iter.next();
28
+ readyCount += 1;
29
+ const entries = result.value?.data ?? [];
30
+ for (const entry of entries) {
31
+ logger.info(`Loaded catalog model layer: ${entry.layer.layerId}`);
32
+ }
33
+ return entries.map((entry) => entry.layer);
34
+ } finally {
35
+ await iter.return(void 0);
36
+ }
37
+ })
38
+ );
39
+ return new ModelHolder(alpha.compileCatalogModel(layers.flat()));
40
+ } finally {
41
+ clearInterval(logInterval);
42
+ }
43
+ }
44
+ get model() {
45
+ return this.#model;
46
+ }
47
+ constructor(model) {
48
+ this.#model = model;
49
+ }
50
+ }
51
+
52
+ exports.ModelHolder = ModelHolder;
53
+ //# sourceMappingURL=ModelHolder.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelHolder.cjs.js","sources":["../../src/model/ModelHolder.ts"],"sourcesContent":["/*\n * Copyright 2026 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 { LifecycleService, LoggerService } from '@backstage/backend-plugin-api';\nimport {\n CatalogModel,\n CatalogModelSource,\n compileCatalogModel,\n} from '@backstage/catalog-model/alpha';\n\n/**\n * Wraps the concern of maintaining a compiled catalog model based on sources.\n *\n * @internal\n */\nexport class ModelHolder {\n #model: CatalogModel;\n\n static modelPassthroughForTest(model: CatalogModel): ModelHolder {\n return new ModelHolder(model);\n }\n\n static async create(options: {\n sources: CatalogModelSource[];\n logger: LoggerService;\n lifecycle: LifecycleService;\n }): Promise<ModelHolder> {\n const { sources, logger, lifecycle } = options;\n\n const shutdownController = new AbortController();\n lifecycle.addShutdownHook(() => shutdownController.abort());\n\n logger.info(`Reading ${sources.length} catalog model sources`);\n let readyCount = 0;\n\n const logInterval = setInterval(() => {\n const remaining = sources.length - readyCount;\n logger.warn(\n `Waiting for ${remaining}/${sources.length} catalog model sources to be ready`,\n );\n }, 3000);\n\n // TODO(freben): Obviously this needs to be extended to support dynamic\n // model source events during the lifetime of the plugin.\n try {\n const layers = await Promise.all(\n sources.map(async source => {\n const iter = source.read({ signal: shutdownController.signal });\n try {\n const result = await iter.next();\n readyCount += 1;\n const entries = result.value?.data ?? [];\n for (const entry of entries) {\n logger.info(`Loaded catalog model layer: ${entry.layer.layerId}`);\n }\n return entries.map(entry => entry.layer);\n } finally {\n await iter.return(undefined);\n }\n }),\n );\n return new ModelHolder(compileCatalogModel(layers.flat()));\n } finally {\n clearInterval(logInterval);\n }\n }\n\n get model(): CatalogModel {\n return this.#model;\n }\n\n private constructor(model: CatalogModel) {\n this.#model = model;\n }\n}\n"],"names":["compileCatalogModel"],"mappings":";;;;AA4BO,MAAM,WAAA,CAAY;AAAA,EACvB,MAAA;AAAA,EAEA,OAAO,wBAAwB,KAAA,EAAkC;AAC/D,IAAA,OAAO,IAAI,YAAY,KAAK,CAAA;AAAA,EAC9B;AAAA,EAEA,aAAa,OAAO,OAAA,EAIK;AACvB,IAAA,MAAM,EAAE,OAAA,EAAS,MAAA,EAAQ,SAAA,EAAU,GAAI,OAAA;AAEvC,IAAA,MAAM,kBAAA,GAAqB,IAAI,eAAA,EAAgB;AAC/C,IAAA,SAAA,CAAU,eAAA,CAAgB,MAAM,kBAAA,CAAmB,KAAA,EAAO,CAAA;AAE1D,IAAA,MAAA,CAAO,IAAA,CAAK,CAAA,QAAA,EAAW,OAAA,CAAQ,MAAM,CAAA,sBAAA,CAAwB,CAAA;AAC7D,IAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,IAAA,MAAM,WAAA,GAAc,YAAY,MAAM;AACpC,MAAA,MAAM,SAAA,GAAY,QAAQ,MAAA,GAAS,UAAA;AACnC,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,CAAA,YAAA,EAAe,SAAS,CAAA,CAAA,EAAI,OAAA,CAAQ,MAAM,CAAA,kCAAA;AAAA,OAC5C;AAAA,IACF,GAAG,GAAI,CAAA;AAIP,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,GAAA;AAAA,QAC3B,OAAA,CAAQ,GAAA,CAAI,OAAM,MAAA,KAAU;AAC1B,UAAA,MAAM,OAAO,MAAA,CAAO,IAAA,CAAK,EAAE,MAAA,EAAQ,kBAAA,CAAmB,QAAQ,CAAA;AAC9D,UAAA,IAAI;AACF,YAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,IAAA,EAAK;AAC/B,YAAA,UAAA,IAAc,CAAA;AACd,YAAA,MAAM,OAAA,GAAU,MAAA,CAAO,KAAA,EAAO,IAAA,IAAQ,EAAC;AACvC,YAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,cAAA,MAAA,CAAO,IAAA,CAAK,CAAA,4BAAA,EAA+B,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,YAClE;AACA,YAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,CAAA,KAAA,KAAS,KAAA,CAAM,KAAK,CAAA;AAAA,UACzC,CAAA,SAAE;AACA,YAAA,MAAM,IAAA,CAAK,OAAO,KAAA,CAAS,CAAA;AAAA,UAC7B;AAAA,QACF,CAAC;AAAA,OACH;AACA,MAAA,OAAO,IAAI,WAAA,CAAYA,yBAAA,CAAoB,MAAA,CAAO,IAAA,EAAM,CAAC,CAAA;AAAA,IAC3D,CAAA,SAAE;AACA,MAAA,aAAA,CAAc,WAAW,CAAA;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,IAAI,KAAA,GAAsB;AACxB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA,EAEQ,YAAY,KAAA,EAAqB;AACvC,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA;AAAA,EAChB;AACF;;;;"}
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ var catalogModel = require('@backstage/catalog-model');
4
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
5
+ var lodash = require('lodash');
6
+ var SchemaValidator = require('./SchemaValidator.cjs.js');
7
+
8
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
9
+
10
+ var lodash__default = /*#__PURE__*/_interopDefaultCompat(lodash);
11
+
12
+ class ModelProcessor {
13
+ #modelHolder;
14
+ #schemaValidator = new SchemaValidator.SchemaValidator();
15
+ constructor(modelHolder) {
16
+ this.#modelHolder = modelHolder;
17
+ }
18
+ getProcessorName() {
19
+ return "ModelProcessor";
20
+ }
21
+ /**
22
+ * For all fields in the entity that the model says are relations: if it's an
23
+ * array of strings, sort that array. Since relations are unordered, this cuts
24
+ * down on unnecessary processing and stitching for sources that don't have a
25
+ * stable order for its output.
26
+ */
27
+ async preProcessEntity(entity) {
28
+ const kind = this.#modelHolder.model.getKind(entity);
29
+ if (kind) {
30
+ for (const fieldModel of kind.relationFields) {
31
+ const value = lodash__default.default.get(entity, fieldModel.path);
32
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
33
+ value.sort();
34
+ }
35
+ }
36
+ }
37
+ return entity;
38
+ }
39
+ /**
40
+ * If the model knows how to handle this entity, validate it against its
41
+ * schema and then return true. Otherwise return false.
42
+ */
43
+ async validateEntityKind(entity) {
44
+ const kind = this.#modelHolder.model.getKind(entity);
45
+ if (!kind) {
46
+ return false;
47
+ }
48
+ const errors = this.#schemaValidator.validate(kind.jsonSchema, entity);
49
+ if (errors.length) {
50
+ throw new TypeError(
51
+ `Validation of ${entity.kind} entity failed: ${errors.join("; ")}`
52
+ );
53
+ }
54
+ return true;
55
+ }
56
+ /**
57
+ * For all fields in the entity that the model says are relations: if the
58
+ * field is a string or an array of strings, emit both the forward and reverse
59
+ * relations that the model says apply for it.
60
+ */
61
+ async postProcessEntity(entity, _location, emit) {
62
+ const kind = this.#modelHolder.model.getKind(entity);
63
+ if (!kind) {
64
+ return entity;
65
+ }
66
+ const modelRelations = this.#modelHolder.model.getRelations({ kind: entity.kind }) ?? [];
67
+ const selfRef = catalogModel.getCompoundEntityRef(entity);
68
+ const selfNamespace = entity.metadata.namespace ?? catalogModel.DEFAULT_NAMESPACE;
69
+ for (const fieldModel of kind.relationFields) {
70
+ const fieldValue = lodash__default.default.get(entity, fieldModel.path);
71
+ if (!fieldValue) {
72
+ continue;
73
+ }
74
+ const shorthandRefs = (Array.isArray(fieldValue) ? fieldValue : [fieldValue]).filter((x) => x && typeof x === "string");
75
+ for (const shorthandRef of shorthandRefs) {
76
+ const targetRef = catalogModel.parseEntityRef(shorthandRef, {
77
+ defaultKind: fieldModel.defaultKind,
78
+ defaultNamespace: fieldModel.defaultNamespace === "inherit" ? selfNamespace : catalogModel.DEFAULT_NAMESPACE
79
+ });
80
+ const targetKind = targetRef.kind.toLocaleLowerCase("en-US");
81
+ if (fieldModel.allowedKinds && !fieldModel.allowedKinds.some(
82
+ (k) => k.toLocaleLowerCase("en-US") === targetKind
83
+ )) {
84
+ continue;
85
+ }
86
+ emit(
87
+ pluginCatalogNode.processingResult.relation({
88
+ source: selfRef,
89
+ type: fieldModel.relation,
90
+ target: targetRef
91
+ })
92
+ );
93
+ const selfKind = entity.kind.toLocaleLowerCase("en-US");
94
+ const relation = modelRelations.find(
95
+ (r) => r.forward.type === fieldModel.relation && r.fromKind.some((k) => k.toLocaleLowerCase("en-US") === selfKind) && r.toKind.some((k) => k.toLocaleLowerCase("en-US") === targetKind)
96
+ );
97
+ if (relation) {
98
+ emit(
99
+ pluginCatalogNode.processingResult.relation({
100
+ source: targetRef,
101
+ type: relation.reverse.type,
102
+ target: selfRef
103
+ })
104
+ );
105
+ }
106
+ }
107
+ }
108
+ return entity;
109
+ }
110
+ }
111
+
112
+ exports.ModelProcessor = ModelProcessor;
113
+ //# sourceMappingURL=ModelProcessor.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelProcessor.cjs.js","sources":["../../src/processors/ModelProcessor.ts"],"sourcesContent":["/*\n * Copyright 2026 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 {\n DEFAULT_NAMESPACE,\n Entity,\n getCompoundEntityRef,\n parseEntityRef,\n} from '@backstage/catalog-model';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport {\n CatalogProcessor,\n CatalogProcessorEmit,\n processingResult,\n} from '@backstage/plugin-catalog-node';\nimport lodash from 'lodash';\nimport { ModelHolder } from '../model/ModelHolder';\nimport { SchemaValidator } from './SchemaValidator';\n\nexport class ModelProcessor implements CatalogProcessor {\n readonly #modelHolder: ModelHolder;\n readonly #schemaValidator = new SchemaValidator();\n\n constructor(modelHolder: ModelHolder) {\n this.#modelHolder = modelHolder;\n }\n\n getProcessorName(): string {\n return 'ModelProcessor';\n }\n\n /**\n * For all fields in the entity that the model says are relations: if it's an\n * array of strings, sort that array. Since relations are unordered, this cuts\n * down on unnecessary processing and stitching for sources that don't have a\n * stable order for its output.\n */\n async preProcessEntity(entity: Entity): Promise<Entity> {\n const kind = this.#modelHolder.model.getKind(entity);\n if (kind) {\n for (const fieldModel of kind.relationFields) {\n const value = lodash.get(entity, fieldModel.path);\n if (Array.isArray(value) && value.every(v => typeof v === 'string')) {\n value.sort();\n }\n }\n }\n\n return entity;\n }\n\n /**\n * If the model knows how to handle this entity, validate it against its\n * schema and then return true. Otherwise return false.\n */\n async validateEntityKind(entity: Entity): Promise<boolean> {\n const kind = this.#modelHolder.model.getKind(entity);\n if (!kind) {\n return false;\n }\n\n const errors = this.#schemaValidator.validate(kind.jsonSchema, entity);\n if (errors.length) {\n throw new TypeError(\n `Validation of ${entity.kind} entity failed: ${errors.join('; ')}`,\n );\n }\n\n return true;\n }\n\n /**\n * For all fields in the entity that the model says are relations: if the\n * field is a string or an array of strings, emit both the forward and reverse\n * relations that the model says apply for it.\n */\n async postProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n emit: CatalogProcessorEmit,\n ): Promise<Entity> {\n const kind = this.#modelHolder.model.getKind(entity);\n if (!kind) {\n return entity;\n }\n\n const modelRelations =\n this.#modelHolder.model.getRelations({ kind: entity.kind }) ?? [];\n const selfRef = getCompoundEntityRef(entity);\n const selfNamespace = entity.metadata.namespace ?? DEFAULT_NAMESPACE;\n\n for (const fieldModel of kind.relationFields) {\n const fieldValue = lodash.get(entity, fieldModel.path);\n if (!fieldValue) {\n continue;\n }\n\n const shorthandRefs = (\n Array.isArray(fieldValue) ? fieldValue : [fieldValue]\n ).filter((x): x is string => x && typeof x === 'string');\n\n for (const shorthandRef of shorthandRefs) {\n const targetRef = parseEntityRef(shorthandRef, {\n defaultKind: fieldModel.defaultKind,\n defaultNamespace:\n fieldModel.defaultNamespace === 'inherit'\n ? selfNamespace\n : DEFAULT_NAMESPACE,\n });\n\n const targetKind = targetRef.kind.toLocaleLowerCase('en-US');\n\n if (\n fieldModel.allowedKinds &&\n !fieldModel.allowedKinds.some(\n k => k.toLocaleLowerCase('en-US') === targetKind,\n )\n ) {\n // TODO: Make this more visible. We should probably not use logging,\n // but if we added admonition support on entities, this would be a\n // good time to emit one.\n continue;\n }\n\n // Emit the forward relation\n emit(\n processingResult.relation({\n source: selfRef,\n type: fieldModel.relation,\n target: targetRef,\n }),\n );\n\n // Emit the reverse relation if the model knows about it\n const selfKind = entity.kind.toLocaleLowerCase('en-US');\n const relation = modelRelations.find(\n r =>\n r.forward.type === fieldModel.relation &&\n r.fromKind.some(k => k.toLocaleLowerCase('en-US') === selfKind) &&\n r.toKind.some(k => k.toLocaleLowerCase('en-US') === targetKind),\n );\n if (relation) {\n emit(\n processingResult.relation({\n source: targetRef,\n type: relation.reverse.type,\n target: selfRef,\n }),\n );\n }\n }\n }\n\n return entity;\n }\n}\n"],"names":["SchemaValidator","lodash","getCompoundEntityRef","DEFAULT_NAMESPACE","parseEntityRef","processingResult"],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ var Ajv = require('ajv');
4
+ var ajvErrors = require('ajv-errors');
5
+
6
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
7
+
8
+ var Ajv__default = /*#__PURE__*/_interopDefaultCompat(Ajv);
9
+ var ajvErrors__default = /*#__PURE__*/_interopDefaultCompat(ajvErrors);
10
+
11
+ class ExpiryMap extends Map {
12
+ #ttlMs;
13
+ #timestamps = /* @__PURE__ */ new Map();
14
+ constructor(ttlMs) {
15
+ super();
16
+ this.#ttlMs = ttlMs;
17
+ }
18
+ set(key, value) {
19
+ this.#timestamps.set(key, Date.now());
20
+ return super.set(key, value);
21
+ }
22
+ get(key) {
23
+ const timestamp = this.#timestamps.get(key);
24
+ if (timestamp !== void 0 && Date.now() - timestamp > this.#ttlMs) {
25
+ this.delete(key);
26
+ return void 0;
27
+ }
28
+ return super.get(key);
29
+ }
30
+ delete(key) {
31
+ this.#timestamps.delete(key);
32
+ return super.delete(key);
33
+ }
34
+ clear() {
35
+ this.#timestamps.clear();
36
+ return super.clear();
37
+ }
38
+ }
39
+ class SchemaValidator {
40
+ #ajv;
41
+ #cache;
42
+ constructor(options) {
43
+ this.#cache = new ExpiryMap(options?.ttlMs ?? 60 * 60 * 1e3);
44
+ this.#ajv = new Ajv__default.default({
45
+ allowUnionTypes: true,
46
+ allErrors: true,
47
+ validateSchema: true
48
+ });
49
+ ajvErrors__default.default(this.#ajv);
50
+ }
51
+ /**
52
+ * Validates the given data against the provided JSON schema. Returns an
53
+ * array of human-readable error strings, or an empty array if valid.
54
+ */
55
+ validate(schema, data) {
56
+ const validator = this.#getOrCompile(schema);
57
+ const valid = validator(data);
58
+ if (valid) {
59
+ return [];
60
+ }
61
+ return (validator.errors ?? []).map(
62
+ (e) => `${e.instancePath || "<root>"} ${e.message}${e.params ? ` - ${Object.entries(e.params).map(([key, val]) => `${key}: ${val}`).join(", ")}` : ""}`
63
+ );
64
+ }
65
+ #getOrCompile(schema) {
66
+ const cached = this.#cache.get(schema);
67
+ if (cached) {
68
+ return cached;
69
+ }
70
+ const validator = this.#ajv.compile(schema);
71
+ this.#cache.set(schema, validator);
72
+ return validator;
73
+ }
74
+ }
75
+
76
+ exports.SchemaValidator = SchemaValidator;
77
+ //# sourceMappingURL=SchemaValidator.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchemaValidator.cjs.js","sources":["../../src/processors/SchemaValidator.ts"],"sourcesContent":["/*\n * Copyright 2026 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 { JsonObject } from '@backstage/types';\nimport Ajv, { ValidateFunction } from 'ajv';\nimport ajvErrors from 'ajv-errors';\n\nclass ExpiryMap<K, V> extends Map<K, V> {\n readonly #ttlMs: number;\n readonly #timestamps = new Map<K, number>();\n\n constructor(ttlMs: number) {\n super();\n this.#ttlMs = ttlMs;\n }\n\n set(key: K, value: V) {\n this.#timestamps.set(key, Date.now());\n return super.set(key, value);\n }\n\n get(key: K) {\n const timestamp = this.#timestamps.get(key);\n if (timestamp !== undefined && Date.now() - timestamp > this.#ttlMs) {\n this.delete(key);\n return undefined;\n }\n return super.get(key);\n }\n\n delete(key: K) {\n this.#timestamps.delete(key);\n return super.delete(key);\n }\n\n clear() {\n this.#timestamps.clear();\n return super.clear();\n }\n}\n\n/**\n * A helper that lazily compiles and caches AJV validators for JSON schemas,\n * with a time-based expiry to avoid holding on to stale entries indefinitely.\n */\nexport class SchemaValidator {\n readonly #ajv: Ajv;\n readonly #cache: ExpiryMap<JsonObject, ValidateFunction>;\n\n constructor(options?: { ttlMs?: number }) {\n this.#cache = new ExpiryMap(options?.ttlMs ?? 60 * 60 * 1000); // 1 hour\n this.#ajv = new Ajv({\n allowUnionTypes: true,\n allErrors: true,\n validateSchema: true,\n });\n ajvErrors(this.#ajv);\n }\n\n /**\n * Validates the given data against the provided JSON schema. Returns an\n * array of human-readable error strings, or an empty array if valid.\n */\n validate(schema: JsonObject, data: unknown): string[] {\n const validator = this.#getOrCompile(schema);\n const valid = validator(data);\n if (valid) {\n return [];\n }\n return (validator.errors ?? []).map(\n e =>\n `${e.instancePath || '<root>'} ${e.message}${\n e.params\n ? ` - ${Object.entries(e.params)\n .map(([key, val]) => `${key}: ${val}`)\n .join(', ')}`\n : ''\n }`,\n );\n }\n\n #getOrCompile(schema: JsonObject): ValidateFunction {\n const cached = this.#cache.get(schema);\n if (cached) {\n return cached;\n }\n\n const validator = this.#ajv.compile(schema);\n this.#cache.set(schema, validator);\n return validator;\n }\n}\n"],"names":["Ajv","ajvErrors"],"mappings":"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"}