@backstage/plugin-catalog-backend 4.0.0-next.1 → 4.0.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.
- package/CHANGELOG.md +43 -0
- package/README.md +13 -0
- package/config.schema.json +0 -4
- package/dist/actions/createQueryCatalogEntitiesAction.cjs.js +40 -31
- package/dist/actions/createQueryCatalogEntitiesAction.cjs.js.map +1 -1
- package/dist/database/DefaultCatalogDatabase.cjs.js +2 -2
- package/dist/database/DefaultCatalogDatabase.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/buildEntitySearch.cjs.js +5 -5
- package/dist/database/operations/stitcher/buildEntitySearch.cjs.js.map +1 -1
- package/dist/index.cjs.js +0 -4
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -41
- package/dist/ingestion/CatalogRules.cjs.js +2 -2
- package/dist/ingestion/CatalogRules.cjs.js.map +1 -1
- package/dist/permissions/rules/createPropertyRule.cjs.js +2 -2
- package/dist/permissions/rules/createPropertyRule.cjs.js.map +1 -1
- package/dist/permissions/rules/hasAnnotation.cjs.js +4 -4
- package/dist/permissions/rules/hasAnnotation.cjs.js.map +1 -1
- package/dist/permissions/rules/hasLabel.cjs.js +4 -4
- package/dist/permissions/rules/hasLabel.cjs.js.map +1 -1
- package/dist/permissions/rules/isEntityKind.cjs.js +3 -3
- package/dist/permissions/rules/isEntityKind.cjs.js.map +1 -1
- package/dist/processors/ModelProcessor.cjs.js +7 -7
- package/dist/processors/ModelProcessor.cjs.js.map +1 -1
- package/dist/processors/SchemaValidator.cjs.js +3 -1
- package/dist/processors/SchemaValidator.cjs.js.map +1 -1
- package/dist/providers/DefaultLocationStore.cjs.js +1 -1
- package/dist/providers/DefaultLocationStore.cjs.js.map +1 -1
- package/dist/service/CatalogBuilder.cjs.js +3 -6
- package/dist/service/CatalogBuilder.cjs.js.map +1 -1
- package/dist/service/DefaultEntitiesCatalog.cjs.js +6 -6
- package/dist/service/DefaultEntitiesCatalog.cjs.js.map +1 -1
- package/dist/service/request/applyPredicateEntityFilterToQuery.cjs.js +9 -9
- package/dist/service/request/applyPredicateEntityFilterToQuery.cjs.js.map +1 -1
- package/dist/stitching/DefaultStitcher.cjs.js +1 -3
- package/dist/stitching/DefaultStitcher.cjs.js.map +1 -1
- package/dist/stitching/types.cjs.js +1 -17
- package/dist/stitching/types.cjs.js.map +1 -1
- package/migrations/20260403000000_add_location_entity_ref.js +1 -1
- package/migrations/20260510000000_search_indices_and_dedup.js +50 -48
- package/migrations/20260912000000_refresh_state_maintenance.js +70 -0
- package/package.json +21 -22
- package/dist/processors/AnnotateScmSlugEntityProcessor.cjs.js +0 -77
- package/dist/processors/AnnotateScmSlugEntityProcessor.cjs.js.map +0 -1
- package/dist/processors/CodeOwnersProcessor.cjs.js +0 -56
- package/dist/processors/CodeOwnersProcessor.cjs.js.map +0 -1
- package/dist/processors/codeowners/read.cjs.js +0 -45
- package/dist/processors/codeowners/read.cjs.js.map +0 -1
- package/dist/processors/codeowners/resolve.cjs.js +0 -51
- package/dist/processors/codeowners/resolve.cjs.js.map +0 -1
- package/dist/processors/codeowners/scm.cjs.js +0 -14
- package/dist/processors/codeowners/scm.cjs.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @backstage/plugin-catalog-backend
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- b0d8a53: **BREAKING**: Removed the deprecated `CodeOwnersProcessor` and `AnnotateScmSlugEntityProcessor` exports. To retain their behavior, install and register the corresponding community backend module:
|
|
8
|
+
|
|
9
|
+
- `CodeOwnersProcessor`: [`@backstage-community/plugin-catalog-backend-module-codeowners`](https://github.com/backstage/community-plugins/tree/main/workspaces/catalog/plugins/catalog-backend-module-codeowners)
|
|
10
|
+
- `AnnotateScmSlugEntityProcessor`: [`@backstage-community/plugin-catalog-backend-module-annotate-scm-slug`](https://github.com/backstage/community-plugins/tree/main/workspaces/catalog/plugins/catalog-backend-module-annotate-scm-slug)
|
|
11
|
+
|
|
12
|
+
Add the module to your backend with `backend.add(import('<module-package>'))` and remove the custom registration of the old processor. For explicit processor configuration, follow the replacement module's documentation.
|
|
13
|
+
|
|
14
|
+
- b0d8a53: **BREAKING**: Removed the deprecated `catalog.stitchingStrategy.mode` configuration setting and its startup warnings. Remove this key from your configuration, whether it is set to `immediate` or `deferred`. Stitching continues to run asynchronously via the worker queue. The `catalog.stitchingStrategy.pollingInterval` and `catalog.stitchingStrategy.stitchTimeout` settings remain supported and should be kept if you use them.
|
|
15
|
+
- d2cf99f: **BREAKING**: Removed the deprecated `catalog.enableRelationsCompatibility` config option and its associated compatibility layer. Entity relations are now always returned in the standard format with only `targetRef`. If you were relying on the `target` field in relations, update your code to use `targetRef` instead.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 736d84e: Use locale-insensitive Unicode casing for consistent string handling across environments.
|
|
20
|
+
- f5119cc: Improved PostgreSQL catalog query planning by correcting entity cardinality statistics for the search table.
|
|
21
|
+
- 72c16f5: Use stable PostgreSQL array parameters for `$in` filter predicates.
|
|
22
|
+
- 78656f8: Fixed catalog model relations to inherit the source entity namespace when no default namespace is configured, as documented. Explicit namespaces in entity references and explicitly configured default namespaces continue to take precedence.
|
|
23
|
+
- 7761a50: Improved path validation for catalog entity placeholders.
|
|
24
|
+
- 49b41f6: Fixed a bug where catalog entities could fail to process when catalog model sources are enabled and the catalog mixes `backstage.io/v1alpha1` and `backstage.io/v1beta1` for the same kind.
|
|
25
|
+
- 4b922b3: The `catalog:query-catalog-entities` action now accepts `limit` and `offset` when they are passed as strings, coercing them to numbers before validation. Previously the action failed with a validation error when a client sent these pagination arguments as strings, which is common for MCP/LLM clients.
|
|
26
|
+
- fda0ef1: Correct catalog property permission matching.
|
|
27
|
+
- ff327e0: Apply catalog location analysis permissions consistently when using a custom location analyzer.
|
|
28
|
+
- 05e2005: Fixed PostgreSQL catalog migrations sometimes failing during search index deduplication when using a database connection pool.
|
|
29
|
+
- 7629e5e: Use stable PostgreSQL array parameters for batched refresh state lookups and updates.
|
|
30
|
+
- c26a19b: Improved catalog database performance stability for large PostgreSQL installations with frequent entity updates.
|
|
31
|
+
- e363ae2: Allowed location type restrictions are now applied consistently during catalog processing.
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
- @backstage/integration@2.1.2
|
|
34
|
+
- @backstage/plugin-catalog-common@1.2.0
|
|
35
|
+
- @backstage/backend-openapi-utils@0.7.2
|
|
36
|
+
- @backstage/catalog-client@1.16.2
|
|
37
|
+
- @backstage/catalog-model@1.10.1
|
|
38
|
+
- @backstage/filter-predicates@0.1.5
|
|
39
|
+
- @backstage/config@1.3.9
|
|
40
|
+
- @backstage/plugin-permission-node@0.11.4
|
|
41
|
+
- @backstage/backend-plugin-api@1.10.1
|
|
42
|
+
- @backstage/plugin-catalog-node@2.2.5
|
|
43
|
+
- @backstage/plugin-events-node@0.4.26
|
|
44
|
+
- @backstage/plugin-permission-common@0.9.11
|
|
45
|
+
|
|
3
46
|
## 4.0.0-next.1
|
|
4
47
|
|
|
5
48
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -69,6 +69,19 @@ you will not have any catalog entities loaded. See [Catalog Configuration](https
|
|
|
69
69
|
for how to add locations, or copy the catalog locations from the [create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs)
|
|
70
70
|
to get up and running quickly.
|
|
71
71
|
|
|
72
|
+
## Migrating removed processors
|
|
73
|
+
|
|
74
|
+
`CodeOwnersProcessor` and `AnnotateScmSlugEntityProcessor` are no longer exported
|
|
75
|
+
by this package. Use the corresponding community modules to retain their behavior:
|
|
76
|
+
|
|
77
|
+
- `CodeOwnersProcessor`: [`@backstage-community/plugin-catalog-backend-module-codeowners`](https://github.com/backstage/community-plugins/tree/main/workspaces/catalog/plugins/catalog-backend-module-codeowners)
|
|
78
|
+
- `AnnotateScmSlugEntityProcessor`: [`@backstage-community/plugin-catalog-backend-module-annotate-scm-slug`](https://github.com/backstage/community-plugins/tree/main/workspaces/catalog/plugins/catalog-backend-module-annotate-scm-slug)
|
|
79
|
+
|
|
80
|
+
Install the module you need in your backend package and register it with
|
|
81
|
+
`backend.add(import('<module-package>'))`. Remove your custom registration of the
|
|
82
|
+
old processor to avoid registering it twice. If you need to configure a processor
|
|
83
|
+
explicitly, follow the replacement module's documentation.
|
|
84
|
+
|
|
72
85
|
## Development
|
|
73
86
|
|
|
74
87
|
This backend plugin can be started in a standalone mode from directly in this
|
package/config.schema.json
CHANGED
|
@@ -104,43 +104,52 @@ const createQueryCatalogEntitiesAction = ({
|
|
|
104
104
|
},
|
|
105
105
|
description: useExperimentalCatalogLayersDescriptions ? MODEL_REFERENCE_DESCRIPTION : INLINE_MODEL_DESCRIPTION,
|
|
106
106
|
schema: {
|
|
107
|
-
input: (z) =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
z.
|
|
107
|
+
input: (z) => {
|
|
108
|
+
const numericFromString = (value) => {
|
|
109
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
110
|
+
const parsed = Number(value);
|
|
111
|
+
return Number.isFinite(parsed) ? parsed : value;
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
};
|
|
115
|
+
return z.object({
|
|
116
|
+
query: filterPredicates.createZodV3FilterPredicateSchema(z).optional().describe(
|
|
117
|
+
"Entity predicate query. Supports field matching, $all, $any, $not, $exists, $in, $contains, and $hasPrefix operators."
|
|
118
|
+
),
|
|
119
|
+
fields: z.array(z.string()).optional().describe(
|
|
120
|
+
"Specific fields to include in the response. If not provided, all fields are returned. Each entry is a dot separated path into an entity, e.g. `spec.type`."
|
|
121
|
+
),
|
|
122
|
+
limit: z.preprocess(numericFromString, z.number().int().positive()).optional().describe("Maximum number of entities to return at a time."),
|
|
123
|
+
offset: z.preprocess(numericFromString, z.number().int().min(0)).optional().describe("Number of entities to skip before returning results."),
|
|
124
|
+
orderFields: z.union([
|
|
124
125
|
z.object({
|
|
125
126
|
field: z.string().describe(
|
|
126
127
|
"Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`."
|
|
127
128
|
),
|
|
128
129
|
order: z.enum(["asc", "desc"]).describe("Sort order")
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
}),
|
|
131
|
+
z.array(
|
|
132
|
+
z.object({
|
|
133
|
+
field: z.string().describe(
|
|
134
|
+
"Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`."
|
|
135
|
+
),
|
|
136
|
+
order: z.enum(["asc", "desc"]).describe("Sort order")
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
]).optional().describe(
|
|
140
|
+
"Ordering criteria for the results. Can be a single order directive or an array for multi-field sorting."
|
|
141
|
+
),
|
|
142
|
+
fullTextFilter: z.object({
|
|
143
|
+
term: z.string().describe("Full text search term"),
|
|
144
|
+
fields: z.array(z.string()).optional().describe(
|
|
145
|
+
"Fields to search within. Each entry is a dot separated path into an entity, e.g. `spec.type`."
|
|
146
|
+
)
|
|
147
|
+
}).optional().describe("Full text search criteria"),
|
|
148
|
+
cursor: z.string().optional().describe(
|
|
149
|
+
"Cursor for pagination. This can be used only after the first request with a response containing a cursor. If a cursor is given it takes precedence over `offset`."
|
|
138
150
|
)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
"Cursor for pagination. This can be used only after the first request with a response containing a cursor. If a cursor is given it takes precedence over `offset`."
|
|
142
|
-
)
|
|
143
|
-
}),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
144
153
|
output: (z) => z.object({
|
|
145
154
|
items: z.array(z.object({}).passthrough()).describe("List of entities"),
|
|
146
155
|
totalItems: z.number().describe("Total number of entities"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createQueryCatalogEntitiesAction.cjs.js","sources":["../../src/actions/createQueryCatalogEntitiesAction.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 { createZodV3FilterPredicateSchema } from '@backstage/filter-predicates';\n\nconst QUERY_SYNTAX = `\n## Query Syntax\n\nThe query uses predicate expressions with dot-notation field paths.\n\nSimple matching:\n { query: { kind: \"Component\" } }\n { query: { kind: \"Component\", \"spec.type\": \"service\" } }\n\nValue operators:\n { query: { kind: { \"$in\": [\"API\", \"Component\"] } } }\n { query: { \"metadata.annotations.backstage.io/techdocs-ref\": { \"$exists\": true } } }\n { query: { \"metadata.tags\": { \"$contains\": \"java\" } } }\n { query: { \"metadata.name\": { \"$hasPrefix\": \"team-\" } } }\n\nLogical operators:\n { query: { \"$all\": [{ kind: \"Component\" }, { \"spec.lifecycle\": \"production\" }] } }\n { query: { \"$any\": [{ \"spec.type\": \"service\" }, { \"spec.type\": \"website\" }] } }\n { query: { \"$not\": { kind: \"Group\" } } }\n\nQuerying relations - find all entities owned by a specific group:\n { query: { \"relations.ownedby\": \"group:default/team-alpha\" } }\n\nCombined example - find production services or websites with TechDocs:\n { query: { \"$all\": [\n { kind: \"Component\", \"spec.lifecycle\": \"production\" },\n { \"$any\": [{ \"spec.type\": \"service\" }, { \"spec.type\": \"website\" }] },\n { \"metadata.annotations.backstage.io/techdocs-ref\": { \"$exists\": true } }\n ] } }\n\n## Other Options\n\nLimit returned fields: { fields: [\"kind\", \"metadata.name\", \"metadata.namespace\"] }\nSort results: { orderFields: { field: \"metadata.name\", order: \"asc\" } }\nFull text search: { fullTextFilter: { term: \"auth\", fields: [\"metadata.name\", \"metadata.title\"] } }\nPagination: Use limit (e.g. 20) and the returned nextPageCursor for subsequent requests via cursor.\n`;\n\nconst INLINE_MODEL_DESCRIPTION = `\nQuery entities from the Backstage Software Catalog using predicate filters.\n\n## Catalog Model\n\nThe catalog contains entities of different kinds. Every entity has \"kind\", \"apiVersion\", \"metadata\", and optionally \"spec\" and \"relations\". Fields use dot notation for querying.\n\nCommon metadata fields on all entities: name, namespace (default: \"default\"), title, description, labels, annotations, tags (string array), links.\n\nEntity references use the format \"kind:namespace/name\", e.g. \"component:default/my-service\" or \"user:default/jane.doe\".\n\n### Entity Kinds\n\n**Component** - A piece of software such as a service, website, or library.\n spec fields: type (e.g. \"service\", \"website\", \"library\"), lifecycle (e.g. \"production\", \"experimental\", \"deprecated\"), owner (entity ref), system, subcomponentOf, providesApis, consumesApis, dependsOn, dependencyOf.\n\n**API** - An interface that components expose, such as REST APIs or event streams.\n spec fields: type (e.g. \"openapi\", \"asyncapi\", \"graphql\", \"grpc\"), lifecycle, owner (entity ref), definition (the API spec content), system.\n\n**System** - A collection of components, APIs, and resources that together expose some functionality.\n spec fields: owner (entity ref), domain, type.\n\n**Domain** - A grouping of systems that share terminology, domain models, and business purpose.\n spec fields: owner (entity ref), subdomainOf, type.\n\n**Resource** - Infrastructure required to operate a component, such as databases or storage buckets.\n spec fields: type, owner (entity ref), system, dependsOn, dependencyOf.\n\n**Group** - An organizational entity such as a team or business unit.\n spec fields: type (e.g. \"team\", \"business-unit\"), children (entity refs), parent (entity ref), members (entity refs), profile (displayName, email, picture).\n\n**User** - A person, such as an employee or contractor.\n spec fields: memberOf (entity refs), profile (displayName, email, picture).\n\n**Location** - A marker that references other catalog descriptor files to be ingested.\n spec fields: type, target, targets, presence.\n\n### Relations\n\nEntities have bidirectional relations stored in the \"relations\" array. Common relation types: ownedBy/ownerOf, dependsOn/dependencyOf, providesApi/apiProvidedBy, consumesApi/apiConsumedBy, parentOf/childOf, memberOf/hasMember, partOf/hasPart.\n\nRelations can be queried via \"relations.<type>\" e.g. \"relations.ownedby: user:default/jane-doe\". The value there must always be a valid entity reference.\n\nWhen querying for entity relationships, prefer using relations over spec fields. For example, use \"relations.ownedby\" instead of \"spec.owner\" to find entities owned by a particular group or user.\n${QUERY_SYNTAX}`;\n\nconst MODEL_REFERENCE_DESCRIPTION = `\nQuery entities from the Backstage Software Catalog using predicate filters.\n\nFor a complete list of entity kinds, fields, relations, and other queryable attributes available in the catalog, use \\`get-catalog-model-description\\`.\n${QUERY_SYNTAX}`;\n\nexport const createQueryCatalogEntitiesAction = ({\n catalog,\n actionsRegistry,\n useExperimentalCatalogLayersDescriptions,\n}: {\n catalog: CatalogService;\n actionsRegistry: ActionsRegistryService;\n useExperimentalCatalogLayersDescriptions?: boolean;\n}) => {\n actionsRegistry.register({\n name: 'query-catalog-entities',\n title: 'Query Catalog Entities',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description: useExperimentalCatalogLayersDescriptions\n ? MODEL_REFERENCE_DESCRIPTION\n : INLINE_MODEL_DESCRIPTION,\n schema: {\n input: z =>\n z.object({\n query: createZodV3FilterPredicateSchema(z)\n .optional()\n .describe(\n 'Entity predicate query. Supports field matching, $all, $any, $not, $exists, $in, $contains, and $hasPrefix operators.',\n ),\n fields: z\n .array(z.string())\n .optional()\n .describe(\n 'Specific fields to include in the response. If not provided, all fields are returned. Each entry is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n limit: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('Maximum number of entities to return at a time.'),\n offset: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe('Number of entities to skip before returning results.'),\n orderFields: z\n .union([\n z.object({\n field: z\n .string()\n .describe(\n 'Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n order: z.enum(['asc', 'desc']).describe('Sort order'),\n }),\n z.array(\n z.object({\n field: z\n .string()\n .describe(\n 'Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n order: z.enum(['asc', 'desc']).describe('Sort order'),\n }),\n ),\n ])\n .optional()\n .describe(\n 'Ordering criteria for the results. Can be a single order directive or an array for multi-field sorting.',\n ),\n fullTextFilter: z\n .object({\n term: z.string().describe('Full text search term'),\n fields: z\n .array(z.string())\n .optional()\n .describe(\n 'Fields to search within. Each entry is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n })\n .optional()\n .describe('Full text search criteria'),\n cursor: z\n .string()\n .optional()\n .describe(\n 'Cursor for pagination. This can be used only after the first request with a response containing a cursor. If a cursor is given it takes precedence over `offset`.',\n ),\n }),\n output: z =>\n z.object({\n items: z\n .array(z.object({}).passthrough())\n .describe('List of entities'),\n totalItems: z.number().describe('Total number of entities'),\n hasMoreEntities: z\n .boolean()\n .describe('Whether more entities are available'),\n nextPageCursor: z\n .string()\n .optional()\n .describe('Next page cursor used to fetch next page of entities'),\n }),\n },\n action: async ({ input, credentials }) => {\n const response = await catalog.queryEntities(\n {\n ...input,\n query: input.query,\n },\n { credentials },\n );\n\n return {\n output: {\n items: response.items,\n totalItems: response.totalItems,\n hasMoreEntities: !!response.pageInfo.nextCursor,\n nextPageCursor: response.pageInfo.nextCursor,\n },\n };\n },\n });\n};\n"],"names":["createZodV3FilterPredicateSchema"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createQueryCatalogEntitiesAction.cjs.js","sources":["../../src/actions/createQueryCatalogEntitiesAction.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 { createZodV3FilterPredicateSchema } from '@backstage/filter-predicates';\n\nconst QUERY_SYNTAX = `\n## Query Syntax\n\nThe query uses predicate expressions with dot-notation field paths.\n\nSimple matching:\n { query: { kind: \"Component\" } }\n { query: { kind: \"Component\", \"spec.type\": \"service\" } }\n\nValue operators:\n { query: { kind: { \"$in\": [\"API\", \"Component\"] } } }\n { query: { \"metadata.annotations.backstage.io/techdocs-ref\": { \"$exists\": true } } }\n { query: { \"metadata.tags\": { \"$contains\": \"java\" } } }\n { query: { \"metadata.name\": { \"$hasPrefix\": \"team-\" } } }\n\nLogical operators:\n { query: { \"$all\": [{ kind: \"Component\" }, { \"spec.lifecycle\": \"production\" }] } }\n { query: { \"$any\": [{ \"spec.type\": \"service\" }, { \"spec.type\": \"website\" }] } }\n { query: { \"$not\": { kind: \"Group\" } } }\n\nQuerying relations - find all entities owned by a specific group:\n { query: { \"relations.ownedby\": \"group:default/team-alpha\" } }\n\nCombined example - find production services or websites with TechDocs:\n { query: { \"$all\": [\n { kind: \"Component\", \"spec.lifecycle\": \"production\" },\n { \"$any\": [{ \"spec.type\": \"service\" }, { \"spec.type\": \"website\" }] },\n { \"metadata.annotations.backstage.io/techdocs-ref\": { \"$exists\": true } }\n ] } }\n\n## Other Options\n\nLimit returned fields: { fields: [\"kind\", \"metadata.name\", \"metadata.namespace\"] }\nSort results: { orderFields: { field: \"metadata.name\", order: \"asc\" } }\nFull text search: { fullTextFilter: { term: \"auth\", fields: [\"metadata.name\", \"metadata.title\"] } }\nPagination: Use limit (e.g. 20) and the returned nextPageCursor for subsequent requests via cursor.\n`;\n\nconst INLINE_MODEL_DESCRIPTION = `\nQuery entities from the Backstage Software Catalog using predicate filters.\n\n## Catalog Model\n\nThe catalog contains entities of different kinds. Every entity has \"kind\", \"apiVersion\", \"metadata\", and optionally \"spec\" and \"relations\". Fields use dot notation for querying.\n\nCommon metadata fields on all entities: name, namespace (default: \"default\"), title, description, labels, annotations, tags (string array), links.\n\nEntity references use the format \"kind:namespace/name\", e.g. \"component:default/my-service\" or \"user:default/jane.doe\".\n\n### Entity Kinds\n\n**Component** - A piece of software such as a service, website, or library.\n spec fields: type (e.g. \"service\", \"website\", \"library\"), lifecycle (e.g. \"production\", \"experimental\", \"deprecated\"), owner (entity ref), system, subcomponentOf, providesApis, consumesApis, dependsOn, dependencyOf.\n\n**API** - An interface that components expose, such as REST APIs or event streams.\n spec fields: type (e.g. \"openapi\", \"asyncapi\", \"graphql\", \"grpc\"), lifecycle, owner (entity ref), definition (the API spec content), system.\n\n**System** - A collection of components, APIs, and resources that together expose some functionality.\n spec fields: owner (entity ref), domain, type.\n\n**Domain** - A grouping of systems that share terminology, domain models, and business purpose.\n spec fields: owner (entity ref), subdomainOf, type.\n\n**Resource** - Infrastructure required to operate a component, such as databases or storage buckets.\n spec fields: type, owner (entity ref), system, dependsOn, dependencyOf.\n\n**Group** - An organizational entity such as a team or business unit.\n spec fields: type (e.g. \"team\", \"business-unit\"), children (entity refs), parent (entity ref), members (entity refs), profile (displayName, email, picture).\n\n**User** - A person, such as an employee or contractor.\n spec fields: memberOf (entity refs), profile (displayName, email, picture).\n\n**Location** - A marker that references other catalog descriptor files to be ingested.\n spec fields: type, target, targets, presence.\n\n### Relations\n\nEntities have bidirectional relations stored in the \"relations\" array. Common relation types: ownedBy/ownerOf, dependsOn/dependencyOf, providesApi/apiProvidedBy, consumesApi/apiConsumedBy, parentOf/childOf, memberOf/hasMember, partOf/hasPart.\n\nRelations can be queried via \"relations.<type>\" e.g. \"relations.ownedby: user:default/jane-doe\". The value there must always be a valid entity reference.\n\nWhen querying for entity relationships, prefer using relations over spec fields. For example, use \"relations.ownedby\" instead of \"spec.owner\" to find entities owned by a particular group or user.\n${QUERY_SYNTAX}`;\n\nconst MODEL_REFERENCE_DESCRIPTION = `\nQuery entities from the Backstage Software Catalog using predicate filters.\n\nFor a complete list of entity kinds, fields, relations, and other queryable attributes available in the catalog, use \\`get-catalog-model-description\\`.\n${QUERY_SYNTAX}`;\n\nexport const createQueryCatalogEntitiesAction = ({\n catalog,\n actionsRegistry,\n useExperimentalCatalogLayersDescriptions,\n}: {\n catalog: CatalogService;\n actionsRegistry: ActionsRegistryService;\n useExperimentalCatalogLayersDescriptions?: boolean;\n}) => {\n actionsRegistry.register({\n name: 'query-catalog-entities',\n title: 'Query Catalog Entities',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description: useExperimentalCatalogLayersDescriptions\n ? MODEL_REFERENCE_DESCRIPTION\n : INLINE_MODEL_DESCRIPTION,\n schema: {\n input: z => {\n // MCP/LLM clients often send numeric arguments as strings. Convert numeric\n // strings to numbers, but leave everything else untouched so Zod rejects the\n // original value (e.g. reports \"received string\" for 'abc' rather than\n // \"received NaN\", and doesn't silently turn true/null/'' into 1/0/0).\n const numericFromString = (value: unknown) => {\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : value;\n }\n return value;\n };\n\n return z.object({\n query: createZodV3FilterPredicateSchema(z)\n .optional()\n .describe(\n 'Entity predicate query. Supports field matching, $all, $any, $not, $exists, $in, $contains, and $hasPrefix operators.',\n ),\n fields: z\n .array(z.string())\n .optional()\n .describe(\n 'Specific fields to include in the response. If not provided, all fields are returned. Each entry is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n limit: z\n .preprocess(numericFromString, z.number().int().positive())\n .optional()\n .describe('Maximum number of entities to return at a time.'),\n offset: z\n .preprocess(numericFromString, z.number().int().min(0))\n .optional()\n .describe('Number of entities to skip before returning results.'),\n orderFields: z\n .union([\n z.object({\n field: z\n .string()\n .describe(\n 'Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n order: z.enum(['asc', 'desc']).describe('Sort order'),\n }),\n z.array(\n z.object({\n field: z\n .string()\n .describe(\n 'Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n order: z.enum(['asc', 'desc']).describe('Sort order'),\n }),\n ),\n ])\n .optional()\n .describe(\n 'Ordering criteria for the results. Can be a single order directive or an array for multi-field sorting.',\n ),\n fullTextFilter: z\n .object({\n term: z.string().describe('Full text search term'),\n fields: z\n .array(z.string())\n .optional()\n .describe(\n 'Fields to search within. Each entry is a dot separated path into an entity, e.g. `spec.type`.',\n ),\n })\n .optional()\n .describe('Full text search criteria'),\n cursor: z\n .string()\n .optional()\n .describe(\n 'Cursor for pagination. This can be used only after the first request with a response containing a cursor. If a cursor is given it takes precedence over `offset`.',\n ),\n });\n },\n output: z =>\n z.object({\n items: z\n .array(z.object({}).passthrough())\n .describe('List of entities'),\n totalItems: z.number().describe('Total number of entities'),\n hasMoreEntities: z\n .boolean()\n .describe('Whether more entities are available'),\n nextPageCursor: z\n .string()\n .optional()\n .describe('Next page cursor used to fetch next page of entities'),\n }),\n },\n action: async ({ input, credentials }) => {\n const response = await catalog.queryEntities(\n {\n ...input,\n query: input.query,\n },\n { credentials },\n );\n\n return {\n output: {\n items: response.items,\n totalItems: response.totalItems,\n hasMoreEntities: !!response.pageInfo.nextCursor,\n nextPageCursor: response.pageInfo.nextCursor,\n },\n };\n },\n });\n};\n"],"names":["createZodV3FilterPredicateSchema"],"mappings":"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"}
|
|
@@ -30,7 +30,7 @@ class DefaultCatalogDatabase {
|
|
|
30
30
|
const tx = txOpaque;
|
|
31
31
|
const { entityRef } = options;
|
|
32
32
|
const entityRefs = new Array();
|
|
33
|
-
let currentRef = entityRef.
|
|
33
|
+
let currentRef = entityRef.toLowerCase();
|
|
34
34
|
for (let depth = 1; depth <= MAX_ANCESTOR_DEPTH; depth += 1) {
|
|
35
35
|
const rows = await tx(
|
|
36
36
|
"refresh_state_references"
|
|
@@ -57,7 +57,7 @@ class DefaultCatalogDatabase {
|
|
|
57
57
|
async refresh(txOpaque, options) {
|
|
58
58
|
const tx = txOpaque;
|
|
59
59
|
const { entityRef } = options;
|
|
60
|
-
const updateResult = await tx("refresh_state").where({ entity_ref: entityRef.
|
|
60
|
+
const updateResult = await tx("refresh_state").where({ entity_ref: entityRef.toLowerCase() }).update({ next_update_at: tx.fn.now() });
|
|
61
61
|
if (updateResult === 0) {
|
|
62
62
|
throw new errors.NotFoundError(`Failed to schedule ${entityRef} for refresh`);
|
|
63
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DefaultCatalogDatabase.cjs.js","sources":["../../src/database/DefaultCatalogDatabase.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 { NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport {\n CatalogDatabase,\n ListAncestorsOptions,\n ListAncestorsResult,\n RefreshOptions,\n Transaction,\n} from './types';\nimport { DbRefreshStateReferencesRow, DbRefreshStateRow } from './tables';\nimport { rethrowError } from './conversion';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nconst MAX_ANCESTOR_DEPTH = 32;\n\nexport class DefaultCatalogDatabase implements CatalogDatabase {\n constructor(\n private readonly options: {\n database: Knex;\n logger: LoggerService;\n },\n ) {}\n\n async transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T> {\n try {\n let result: T | undefined = undefined;\n\n await this.options.database.transaction(\n async tx => {\n // We can't return here, as knex swallows the return type in case the transaction is rolled back:\n // https://github.com/knex/knex/blob/e37aeaa31c8ef9c1b07d2e4d3ec6607e557d800d/lib/transaction.js#L136\n result = await fn(tx);\n },\n {\n // If we explicitly trigger a rollback, don't fail.\n doNotRejectOnRollback: true,\n },\n );\n\n return result!;\n } catch (e) {\n this.options.logger.debug(`Error during transaction, ${e}`);\n throw rethrowError(e);\n }\n }\n\n async listAncestors(\n txOpaque: Transaction,\n options: ListAncestorsOptions,\n ): Promise<ListAncestorsResult> {\n const tx = txOpaque as Knex.Transaction;\n const { entityRef } = options;\n const entityRefs = new Array<string>();\n\n let currentRef = entityRef.
|
|
1
|
+
{"version":3,"file":"DefaultCatalogDatabase.cjs.js","sources":["../../src/database/DefaultCatalogDatabase.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 { NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport {\n CatalogDatabase,\n ListAncestorsOptions,\n ListAncestorsResult,\n RefreshOptions,\n Transaction,\n} from './types';\nimport { DbRefreshStateReferencesRow, DbRefreshStateRow } from './tables';\nimport { rethrowError } from './conversion';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nconst MAX_ANCESTOR_DEPTH = 32;\n\nexport class DefaultCatalogDatabase implements CatalogDatabase {\n constructor(\n private readonly options: {\n database: Knex;\n logger: LoggerService;\n },\n ) {}\n\n async transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T> {\n try {\n let result: T | undefined = undefined;\n\n await this.options.database.transaction(\n async tx => {\n // We can't return here, as knex swallows the return type in case the transaction is rolled back:\n // https://github.com/knex/knex/blob/e37aeaa31c8ef9c1b07d2e4d3ec6607e557d800d/lib/transaction.js#L136\n result = await fn(tx);\n },\n {\n // If we explicitly trigger a rollback, don't fail.\n doNotRejectOnRollback: true,\n },\n );\n\n return result!;\n } catch (e) {\n this.options.logger.debug(`Error during transaction, ${e}`);\n throw rethrowError(e);\n }\n }\n\n async listAncestors(\n txOpaque: Transaction,\n options: ListAncestorsOptions,\n ): Promise<ListAncestorsResult> {\n const tx = txOpaque as Knex.Transaction;\n const { entityRef } = options;\n const entityRefs = new Array<string>();\n\n let currentRef = entityRef.toLowerCase();\n for (let depth = 1; depth <= MAX_ANCESTOR_DEPTH; depth += 1) {\n const rows = await tx<DbRefreshStateReferencesRow>(\n 'refresh_state_references',\n )\n .where({ target_entity_ref: currentRef })\n .select();\n\n if (rows.length === 0) {\n if (depth === 1) {\n throw new NotFoundError(`Entity ${currentRef} not found`);\n }\n throw new NotFoundError(\n `Entity ${entityRef} has a broken parent reference chain at ${currentRef}`,\n );\n }\n\n const parentRef = rows.find(r => r.source_entity_ref)?.source_entity_ref;\n if (!parentRef) {\n // We've reached the top of the tree which is the entityProvider.\n // In this case we refresh the entity itself.\n return { entityRefs };\n }\n entityRefs.push(parentRef);\n currentRef = parentRef;\n }\n throw new Error(\n `Unable receive ancestors for ${entityRef}, reached maximum depth of ${MAX_ANCESTOR_DEPTH}`,\n );\n }\n\n async refresh(txOpaque: Transaction, options: RefreshOptions): Promise<void> {\n const tx = txOpaque as Knex.Transaction;\n const { entityRef } = options;\n\n const updateResult = await tx<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef.toLowerCase() })\n .update({ next_update_at: tx.fn.now() });\n if (updateResult === 0) {\n throw new NotFoundError(`Failed to schedule ${entityRef} for refresh`);\n }\n }\n}\n"],"names":["rethrowError","NotFoundError"],"mappings":";;;;;AA6BA,MAAM,kBAAA,GAAqB,EAAA;AAEpB,MAAM,sBAAA,CAAkD;AAAA,EAC7D,YACmB,OAAA,EAIjB;AAJiB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAIhB;AAAA,EAEH,MAAM,YAAe,EAAA,EAAiD;AACpE,IAAA,IAAI;AACF,MAAA,IAAI,MAAA,GAAwB,KAAA,CAAA;AAE5B,MAAA,MAAM,IAAA,CAAK,QAAQ,QAAA,CAAS,WAAA;AAAA,QAC1B,OAAM,EAAA,KAAM;AAGV,UAAA,MAAA,GAAS,MAAM,GAAG,EAAE,CAAA;AAAA,QACtB,CAAA;AAAA,QACA;AAAA;AAAA,UAEE,qBAAA,EAAuB;AAAA;AACzB,OACF;AAEA,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,CAAA,EAAG;AACV,MAAA,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,0BAAA,EAA6B,CAAC,CAAA,CAAE,CAAA;AAC1D,MAAA,MAAMA,wBAAa,CAAC,CAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,aAAA,CACJ,QAAA,EACA,OAAA,EAC8B;AAC9B,IAAA,MAAM,EAAA,GAAK,QAAA;AACX,IAAA,MAAM,EAAE,WAAU,GAAI,OAAA;AACtB,IAAA,MAAM,UAAA,GAAa,IAAI,KAAA,EAAc;AAErC,IAAA,IAAI,UAAA,GAAa,UAAU,WAAA,EAAY;AACvC,IAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,IAAS,kBAAA,EAAoB,SAAS,CAAA,EAAG;AAC3D,MAAA,MAAM,OAAO,MAAM,EAAA;AAAA,QACjB;AAAA,QAEC,KAAA,CAAM,EAAE,mBAAmB,UAAA,EAAY,EACvC,MAAA,EAAO;AAEV,MAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACrB,QAAA,IAAI,UAAU,CAAA,EAAG;AACf,UAAA,MAAM,IAAIC,oBAAA,CAAc,CAAA,OAAA,EAAU,UAAU,CAAA,UAAA,CAAY,CAAA;AAAA,QAC1D;AACA,QAAA,MAAM,IAAIA,oBAAA;AAAA,UACR,CAAA,OAAA,EAAU,SAAS,CAAA,wCAAA,EAA2C,UAAU,CAAA;AAAA,SAC1E;AAAA,MACF;AAEA,MAAA,MAAM,YAAY,IAAA,CAAK,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,iBAAiB,CAAA,EAAG,iBAAA;AACvD,MAAA,IAAI,CAAC,SAAA,EAAW;AAGd,QAAA,OAAO,EAAE,UAAA,EAAW;AAAA,MACtB;AACA,MAAA,UAAA,CAAW,KAAK,SAAS,CAAA;AACzB,MAAA,UAAA,GAAa,SAAA;AAAA,IACf;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,6BAAA,EAAgC,SAAS,CAAA,2BAAA,EAA8B,kBAAkB,CAAA;AAAA,KAC3F;AAAA,EACF;AAAA,EAEA,MAAM,OAAA,CAAQ,QAAA,EAAuB,OAAA,EAAwC;AAC3E,IAAA,MAAM,EAAA,GAAK,QAAA;AACX,IAAA,MAAM,EAAE,WAAU,GAAI,OAAA;AAEtB,IAAA,MAAM,YAAA,GAAe,MAAM,EAAA,CAAsB,eAAe,EAC7D,KAAA,CAAM,EAAE,YAAY,SAAA,CAAU,WAAA,IAAe,CAAA,CAC7C,OAAO,EAAE,cAAA,EAAgB,GAAG,EAAA,CAAG,GAAA,IAAO,CAAA;AACzC,IAAA,IAAI,iBAAiB,CAAA,EAAG;AACtB,MAAA,MAAM,IAAIA,oBAAA,CAAc,CAAA,mBAAA,EAAsB,SAAS,CAAA,YAAA,CAAc,CAAA;AAAA,IACvE;AAAA,EACF;AACF;;;;"}
|
|
@@ -34,7 +34,7 @@ function traverse(root) {
|
|
|
34
34
|
visit(path, item);
|
|
35
35
|
if (typeof item === "string") {
|
|
36
36
|
const pathKey = `${path}.${item}`;
|
|
37
|
-
const lowerKey = pathKey.
|
|
37
|
+
const lowerKey = pathKey.toLowerCase();
|
|
38
38
|
if (!seenPathKeys.has(lowerKey)) {
|
|
39
39
|
seenPathKeys.add(lowerKey);
|
|
40
40
|
output.push({ key: pathKey, value: true });
|
|
@@ -53,7 +53,7 @@ function traverse(root) {
|
|
|
53
53
|
function mapToRows(input, entityId) {
|
|
54
54
|
const result = [];
|
|
55
55
|
for (const { key: rawKey, value: rawValue } of input) {
|
|
56
|
-
const key = rawKey.
|
|
56
|
+
const key = rawKey.toLowerCase();
|
|
57
57
|
if (key.length > MAX_KEY_LENGTH) {
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
@@ -65,7 +65,7 @@ function mapToRows(input, entityId) {
|
|
|
65
65
|
value: null
|
|
66
66
|
});
|
|
67
67
|
} else {
|
|
68
|
-
const value = String(rawValue).
|
|
68
|
+
const value = String(rawValue).toLowerCase();
|
|
69
69
|
if (value.length <= MAX_VALUE_LENGTH) {
|
|
70
70
|
result.push({
|
|
71
71
|
entity_id: entityId,
|
|
@@ -100,11 +100,11 @@ function buildEntitySearch(entityId, entity) {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
const keys = new Set(raw.map((r) => r.key));
|
|
103
|
-
const lowerKeys = new Set(raw.map((r) => r.key.
|
|
103
|
+
const lowerKeys = new Set(raw.map((r) => r.key.toLowerCase()));
|
|
104
104
|
if (keys.size !== lowerKeys.size) {
|
|
105
105
|
const difference = [];
|
|
106
106
|
for (const key of keys) {
|
|
107
|
-
const lower = key.
|
|
107
|
+
const lower = key.toLowerCase();
|
|
108
108
|
if (!lowerKeys.delete(lower)) {
|
|
109
109
|
difference.push(lower);
|
|
110
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildEntitySearch.cjs.js","sources":["../../../../src/database/operations/stitcher/buildEntitySearch.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 { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model';\nimport { InputError } from '@backstage/errors';\nimport { DbSearchRow } from '../../tables';\nimport { NULL_SENTINEL } from './util';\n\n// These are excluded in the generic loop, either because they do not make sense\n// to index, or because they are special-case always inserted whether they are\n// null or not\nconst SPECIAL_KEYS = [\n 'attachments',\n 'relations',\n 'status',\n 'metadata.name',\n 'metadata.namespace',\n 'metadata.uid',\n 'metadata.etag',\n];\n\n// The maximum length allowed for search values. These columns are indexed, and\n// database engines do not like to index on massive values. For example,\n// postgres will balk after 8191 byte line sizes.\nconst MAX_KEY_LENGTH = 200;\nconst MAX_VALUE_LENGTH = 200;\n\ntype Kv = {\n key: string;\n value: unknown;\n};\n\n// Helper for traversing through a nested structure and outputting a list of\n// path->value entries of the leaves.\n//\n// For example, this yaml structure\n//\n// a: 1\n// b:\n// c: null\n// e: [f, g]\n// h:\n// - i: 1\n// j: k\n// - i: 2\n// j: l\n//\n// will result in\n//\n// \"a\", 1\n// \"b.c\", null\n// \"b.e\": \"f\"\n// \"b.e.f\": true\n// \"b.e\": \"g\"\n// \"b.e.g\": true\n// \"h.i\": 1\n// \"h.j\": \"k\"\n// \"h.i\": 2\n// \"h.j\": \"l\"\nexport function traverse(root: unknown): Kv[] {\n const output: Kv[] = [];\n // Use a Set for O(1) case-insensitive duplicate detection of synthetic\n // boolean path keys (e.g. \"metadata.tags.java\"), instead of the previous\n // O(n) Array.some() linear scan which caused O(n²) overall complexity\n // and severe event loop blocking for entities with large arrays.\n const seenPathKeys = new Set<string>();\n\n function visit(path: string, current: unknown) {\n if (SPECIAL_KEYS.includes(path)) {\n return;\n }\n\n // empty or scalar\n if (\n current === undefined ||\n current === null ||\n ['string', 'number', 'boolean'].includes(typeof current)\n ) {\n output.push({ key: path, value: current });\n return;\n }\n\n // unknown\n if (typeof current !== 'object') {\n return;\n }\n\n // array\n if (Array.isArray(current)) {\n for (const item of current) {\n // NOTE(freben): The reason that these are output in two different ways,\n // is to support use cases where you want to express that MORE than one\n // tag is present in a list. Since the EntityFilters structure is a\n // record, you can't have several entries of the same key. Therefore\n // you will have to match on\n //\n // { \"a.b\": [\"true\"], \"a.c\": [\"true\"] }\n //\n // rather than\n //\n // { \"a\": [\"b\", \"c\"] }\n //\n // because the latter means EITHER b or c has to be present.\n visit(path, item);\n if (typeof item === 'string') {\n const pathKey = `${path}.${item}`;\n const lowerKey = pathKey.toLocaleLowerCase('en-US');\n if (!seenPathKeys.has(lowerKey)) {\n seenPathKeys.add(lowerKey);\n output.push({ key: pathKey, value: true });\n }\n }\n }\n return;\n }\n\n // object\n for (const [key, value] of Object.entries(current!)) {\n visit(path ? `${path}.${key}` : key, value);\n }\n }\n\n visit('', root);\n\n return output;\n}\n\n// Translates a number of raw data rows to search table rows\nexport function mapToRows(input: Kv[], entityId: string): DbSearchRow[] {\n const result: DbSearchRow[] = [];\n\n for (const { key: rawKey, value: rawValue } of input) {\n const key = rawKey.toLocaleLowerCase('en-US');\n if (key.length > MAX_KEY_LENGTH) {\n continue;\n }\n if (rawValue === undefined || rawValue === null) {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n } else {\n const value = String(rawValue).toLocaleLowerCase('en-US');\n if (value.length <= MAX_VALUE_LENGTH) {\n result.push({\n entity_id: entityId,\n key,\n original_value: String(rawValue),\n value: value,\n });\n } else {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generates all of the search rows that are relevant for this entity.\n *\n * @param entityId - The uid of the entity\n * @param entity - The entity\n * @returns A list of entity search rows\n */\nexport function buildEntitySearch(\n entityId: string,\n entity: Entity,\n): DbSearchRow[] {\n // Visit the base structure recursively\n const raw = traverse(entity);\n\n // Start with some special keys that are always present because you want to\n // be able to easily search for null specifically\n raw.push({ key: 'metadata.name', value: entity.metadata.name });\n raw.push({ key: 'metadata.namespace', value: entity.metadata.namespace });\n raw.push({ key: 'metadata.uid', value: entity.metadata.uid });\n\n // Namespace not specified has the default value \"default\", so we want to\n // match on that as well\n if (!entity.metadata.namespace) {\n raw.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE });\n }\n\n // Visit relations\n for (const relation of entity.relations ?? []) {\n raw.push({\n key: `relations.${relation.type}`,\n value: relation.targetRef,\n });\n }\n\n // This validates that there are no keys that vary only in casing, such\n // as `spec.foo` and `spec.Foo`.\n const keys = new Set(raw.map(r => r.key));\n const lowerKeys = new Set(raw.map(r => r.key.toLocaleLowerCase('en-US')));\n if (keys.size !== lowerKeys.size) {\n const difference = [];\n for (const key of keys) {\n const lower = key.toLocaleLowerCase('en-US');\n if (!lowerKeys.delete(lower)) {\n difference.push(lower);\n }\n }\n const badKeys = `'${difference.join(\"', '\")}'`;\n throw new InputError(\n `Entity has duplicate keys that vary only in casing, ${badKeys}`,\n );\n }\n\n const rows = mapToRows(raw, entityId);\n\n // Deduplicate by (key, value). Duplicate array values in the entity data\n // (e.g. tags: ['java', 'java']) produce identical search rows which would\n // violate the unique constraint on (entity_id, key, value).\n const seen = new Set<string>();\n return rows.filter(row => {\n const k = `${row.key}\\0${row.value === null ? NULL_SENTINEL : row.value}`;\n if (seen.has(k)) return false;\n seen.add(k);\n return true;\n });\n}\n"],"names":["DEFAULT_NAMESPACE","InputError","NULL_SENTINEL"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"buildEntitySearch.cjs.js","sources":["../../../../src/database/operations/stitcher/buildEntitySearch.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 { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model';\nimport { InputError } from '@backstage/errors';\nimport { DbSearchRow } from '../../tables';\nimport { NULL_SENTINEL } from './util';\n\n// These are excluded in the generic loop, either because they do not make sense\n// to index, or because they are special-case always inserted whether they are\n// null or not\nconst SPECIAL_KEYS = [\n 'attachments',\n 'relations',\n 'status',\n 'metadata.name',\n 'metadata.namespace',\n 'metadata.uid',\n 'metadata.etag',\n];\n\n// The maximum length allowed for search values. These columns are indexed, and\n// database engines do not like to index on massive values. For example,\n// postgres will balk after 8191 byte line sizes.\nconst MAX_KEY_LENGTH = 200;\nconst MAX_VALUE_LENGTH = 200;\n\ntype Kv = {\n key: string;\n value: unknown;\n};\n\n// Helper for traversing through a nested structure and outputting a list of\n// path->value entries of the leaves.\n//\n// For example, this yaml structure\n//\n// a: 1\n// b:\n// c: null\n// e: [f, g]\n// h:\n// - i: 1\n// j: k\n// - i: 2\n// j: l\n//\n// will result in\n//\n// \"a\", 1\n// \"b.c\", null\n// \"b.e\": \"f\"\n// \"b.e.f\": true\n// \"b.e\": \"g\"\n// \"b.e.g\": true\n// \"h.i\": 1\n// \"h.j\": \"k\"\n// \"h.i\": 2\n// \"h.j\": \"l\"\nexport function traverse(root: unknown): Kv[] {\n const output: Kv[] = [];\n // Use a Set for O(1) case-insensitive duplicate detection of synthetic\n // boolean path keys (e.g. \"metadata.tags.java\"), instead of the previous\n // O(n) Array.some() linear scan which caused O(n²) overall complexity\n // and severe event loop blocking for entities with large arrays.\n const seenPathKeys = new Set<string>();\n\n function visit(path: string, current: unknown) {\n if (SPECIAL_KEYS.includes(path)) {\n return;\n }\n\n // empty or scalar\n if (\n current === undefined ||\n current === null ||\n ['string', 'number', 'boolean'].includes(typeof current)\n ) {\n output.push({ key: path, value: current });\n return;\n }\n\n // unknown\n if (typeof current !== 'object') {\n return;\n }\n\n // array\n if (Array.isArray(current)) {\n for (const item of current) {\n // NOTE(freben): The reason that these are output in two different ways,\n // is to support use cases where you want to express that MORE than one\n // tag is present in a list. Since the EntityFilters structure is a\n // record, you can't have several entries of the same key. Therefore\n // you will have to match on\n //\n // { \"a.b\": [\"true\"], \"a.c\": [\"true\"] }\n //\n // rather than\n //\n // { \"a\": [\"b\", \"c\"] }\n //\n // because the latter means EITHER b or c has to be present.\n visit(path, item);\n if (typeof item === 'string') {\n const pathKey = `${path}.${item}`;\n const lowerKey = pathKey.toLowerCase();\n if (!seenPathKeys.has(lowerKey)) {\n seenPathKeys.add(lowerKey);\n output.push({ key: pathKey, value: true });\n }\n }\n }\n return;\n }\n\n // object\n for (const [key, value] of Object.entries(current!)) {\n visit(path ? `${path}.${key}` : key, value);\n }\n }\n\n visit('', root);\n\n return output;\n}\n\n// Translates a number of raw data rows to search table rows\nexport function mapToRows(input: Kv[], entityId: string): DbSearchRow[] {\n const result: DbSearchRow[] = [];\n\n for (const { key: rawKey, value: rawValue } of input) {\n const key = rawKey.toLowerCase();\n if (key.length > MAX_KEY_LENGTH) {\n continue;\n }\n if (rawValue === undefined || rawValue === null) {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n } else {\n const value = String(rawValue).toLowerCase();\n if (value.length <= MAX_VALUE_LENGTH) {\n result.push({\n entity_id: entityId,\n key,\n original_value: String(rawValue),\n value: value,\n });\n } else {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generates all of the search rows that are relevant for this entity.\n *\n * @param entityId - The uid of the entity\n * @param entity - The entity\n * @returns A list of entity search rows\n */\nexport function buildEntitySearch(\n entityId: string,\n entity: Entity,\n): DbSearchRow[] {\n // Visit the base structure recursively\n const raw = traverse(entity);\n\n // Start with some special keys that are always present because you want to\n // be able to easily search for null specifically\n raw.push({ key: 'metadata.name', value: entity.metadata.name });\n raw.push({ key: 'metadata.namespace', value: entity.metadata.namespace });\n raw.push({ key: 'metadata.uid', value: entity.metadata.uid });\n\n // Namespace not specified has the default value \"default\", so we want to\n // match on that as well\n if (!entity.metadata.namespace) {\n raw.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE });\n }\n\n // Visit relations\n for (const relation of entity.relations ?? []) {\n raw.push({\n key: `relations.${relation.type}`,\n value: relation.targetRef,\n });\n }\n\n // This validates that there are no keys that vary only in casing, such\n // as `spec.foo` and `spec.Foo`.\n const keys = new Set(raw.map(r => r.key));\n const lowerKeys = new Set(raw.map(r => r.key.toLowerCase()));\n if (keys.size !== lowerKeys.size) {\n const difference = [];\n for (const key of keys) {\n const lower = key.toLowerCase();\n if (!lowerKeys.delete(lower)) {\n difference.push(lower);\n }\n }\n const badKeys = `'${difference.join(\"', '\")}'`;\n throw new InputError(\n `Entity has duplicate keys that vary only in casing, ${badKeys}`,\n );\n }\n\n const rows = mapToRows(raw, entityId);\n\n // Deduplicate by (key, value). Duplicate array values in the entity data\n // (e.g. tags: ['java', 'java']) produce identical search rows which would\n // violate the unique constraint on (entity_id, key, value).\n const seen = new Set<string>();\n return rows.filter(row => {\n const k = `${row.key}\\0${row.value === null ? NULL_SENTINEL : row.value}`;\n if (seen.has(k)) return false;\n seen.add(k);\n return true;\n });\n}\n"],"names":["DEFAULT_NAMESPACE","InputError","NULL_SENTINEL"],"mappings":"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"}
|
package/dist/index.cjs.js
CHANGED
|
@@ -4,9 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
var CatalogPlugin = require('./service/CatalogPlugin.cjs.js');
|
|
6
6
|
var AnnotateLocationEntityProcessor = require('./processors/AnnotateLocationEntityProcessor.cjs.js');
|
|
7
|
-
var AnnotateScmSlugEntityProcessor = require('./processors/AnnotateScmSlugEntityProcessor.cjs.js');
|
|
8
7
|
var BuiltinKindsEntityProcessor = require('./processors/BuiltinKindsEntityProcessor.cjs.js');
|
|
9
|
-
var CodeOwnersProcessor = require('./processors/CodeOwnersProcessor.cjs.js');
|
|
10
8
|
var FileReaderProcessor = require('./processors/FileReaderProcessor.cjs.js');
|
|
11
9
|
var PlaceholderProcessor = require('./processors/PlaceholderProcessor.cjs.js');
|
|
12
10
|
var UrlReaderProcessor = require('./processors/UrlReaderProcessor.cjs.js');
|
|
@@ -17,9 +15,7 @@ var constants = require('./constants.cjs.js');
|
|
|
17
15
|
|
|
18
16
|
exports.default = CatalogPlugin.catalogPlugin;
|
|
19
17
|
exports.AnnotateLocationEntityProcessor = AnnotateLocationEntityProcessor.AnnotateLocationEntityProcessor;
|
|
20
|
-
exports.AnnotateScmSlugEntityProcessor = AnnotateScmSlugEntityProcessor.AnnotateScmSlugEntityProcessor;
|
|
21
18
|
exports.BuiltinKindsEntityProcessor = BuiltinKindsEntityProcessor.BuiltinKindsEntityProcessor;
|
|
22
|
-
exports.CodeOwnersProcessor = CodeOwnersProcessor.CodeOwnersProcessor;
|
|
23
19
|
exports.FileReaderProcessor = FileReaderProcessor.FileReaderProcessor;
|
|
24
20
|
exports.PlaceholderProcessor = PlaceholderProcessor.PlaceholderProcessor;
|
|
25
21
|
exports.UrlReaderProcessor = UrlReaderProcessor.UrlReaderProcessor;
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
|
-
import {
|
|
2
|
+
import { UrlReaderService, LoggerService } from '@backstage/backend-plugin-api';
|
|
3
3
|
import { Entity, EntityPolicy } from '@backstage/catalog-model';
|
|
4
4
|
import { ScmIntegrationRegistry } from '@backstage/integration';
|
|
5
5
|
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
|
6
6
|
import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, PlaceholderResolver, CatalogProcessorCache } from '@backstage/plugin-catalog-node';
|
|
7
|
-
import { Config } from '@backstage/config';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Catalog plugin
|
|
@@ -22,23 +21,6 @@ declare class AnnotateLocationEntityProcessor implements CatalogProcessor {
|
|
|
22
21
|
preProcessEntity(entity: Entity, location: LocationSpec, _: CatalogProcessorEmit, originLocation: LocationSpec): Promise<Entity>;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
/**
|
|
26
|
-
* @public
|
|
27
|
-
* @deprecated Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead, this will be removed in a future release
|
|
28
|
-
*/
|
|
29
|
-
declare class AnnotateScmSlugEntityProcessor implements CatalogProcessor {
|
|
30
|
-
private readonly opts;
|
|
31
|
-
constructor(opts: {
|
|
32
|
-
scmIntegrationRegistry: ScmIntegrationRegistry;
|
|
33
|
-
kinds?: string[];
|
|
34
|
-
});
|
|
35
|
-
getProcessorName(): string;
|
|
36
|
-
static fromConfig(config: Config, options?: {
|
|
37
|
-
kinds?: string[];
|
|
38
|
-
}): AnnotateScmSlugEntityProcessor;
|
|
39
|
-
preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
24
|
/** @public */
|
|
43
25
|
declare class BuiltinKindsEntityProcessor implements CatalogProcessor {
|
|
44
26
|
private readonly validators;
|
|
@@ -48,27 +30,6 @@ declare class BuiltinKindsEntityProcessor implements CatalogProcessor {
|
|
|
48
30
|
postProcessEntity(entity: Entity, _location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
/**
|
|
52
|
-
* @public
|
|
53
|
-
* @deprecated Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead, this will be removed in a future release
|
|
54
|
-
*/
|
|
55
|
-
declare class CodeOwnersProcessor implements CatalogProcessor {
|
|
56
|
-
private readonly integrations;
|
|
57
|
-
private readonly logger;
|
|
58
|
-
private readonly reader;
|
|
59
|
-
static fromConfig(config: Config, options: {
|
|
60
|
-
logger: LoggerService;
|
|
61
|
-
reader: UrlReaderService;
|
|
62
|
-
}): CodeOwnersProcessor;
|
|
63
|
-
constructor(options: {
|
|
64
|
-
integrations: ScmIntegrationRegistry;
|
|
65
|
-
logger: LoggerService;
|
|
66
|
-
reader: UrlReaderService;
|
|
67
|
-
});
|
|
68
|
-
getProcessorName(): string;
|
|
69
|
-
preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
33
|
/** @public */
|
|
73
34
|
declare class FileReaderProcessor implements CatalogProcessor {
|
|
74
35
|
getProcessorName(): string;
|
|
@@ -119,5 +80,5 @@ declare const CATALOG_CONFLICTS_TOPIC = "experimental.catalog.conflict";
|
|
|
119
80
|
/** @public */
|
|
120
81
|
declare const CATALOG_ERRORS_TOPIC = "experimental.catalog.errors";
|
|
121
82
|
|
|
122
|
-
export { AnnotateLocationEntityProcessor,
|
|
83
|
+
export { AnnotateLocationEntityProcessor, BuiltinKindsEntityProcessor, CATALOG_CONFLICTS_TOPIC, CATALOG_ERRORS_TOPIC, FileReaderProcessor, PlaceholderProcessor, UrlReaderProcessor, catalogPlugin as default, transformLegacyPolicyToProcessor };
|
|
123
84
|
export type { PlaceholderProcessorOptions };
|
|
@@ -150,14 +150,14 @@ class DefaultCatalogRulesEnforcer {
|
|
|
150
150
|
return true;
|
|
151
151
|
}
|
|
152
152
|
for (const matcher of matchers) {
|
|
153
|
-
if (entity.kind?.
|
|
153
|
+
if (entity.kind?.toLowerCase() !== matcher.kind.toLowerCase()) {
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
156
|
if (matcher["spec.type"]) {
|
|
157
157
|
if (typeof entity.spec?.type !== "string") {
|
|
158
158
|
continue;
|
|
159
159
|
}
|
|
160
|
-
if (matcher["spec.type"].
|
|
160
|
+
if (matcher["spec.type"].toLowerCase() !== entity.spec.type.toLowerCase()) {
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogRules.cjs.js","sources":["../../src/ingestion/CatalogRules.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 { Config } from '@backstage/config';\nimport { Entity } from '@backstage/catalog-model';\nimport path from 'node:path';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { minimatch } from 'minimatch';\nimport { z } from 'zod/v3';\n\n/**\n * Rules to apply to catalog entities.\n *\n * An undefined list of matchers means match all, an empty list of matchers means match none.\n */\nexport type CatalogRule = {\n allow: CatalogRuleAllow[];\n locations?: Array<{\n exact?: string;\n type: string;\n pattern?: string;\n }>;\n};\n\ntype CatalogRuleAllow = {\n kind: string;\n 'spec.type'?: string;\n};\n\n/**\n * Decides whether an entity from a given location is allowed to enter the\n * catalog, according to some rule set.\n */\nexport type CatalogRulesEnforcer = {\n isAllowed(entity: Entity, location: LocationSpec): boolean;\n};\n\nconst allowRuleParser = z.array(\n z\n .object({\n kind: z.string(),\n 'spec.type': z.string().optional(),\n })\n .or(z.string())\n .transform(val => (typeof val === 'string' ? { kind: val } : val)),\n);\n\n/**\n * Implements the default catalog rule set, consuming the config keys\n * `catalog.rules` and `catalog.locations.[].rules`.\n */\nexport class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {\n /**\n * Default rules used by the catalog.\n *\n * Denies any location from specifying user or group entities.\n */\n static readonly defaultRules: CatalogRule[] = [\n {\n allow: ['Component', 'API', 'Location'].map(kind => ({ kind })),\n },\n ];\n\n /**\n * Loads catalog rules from config.\n *\n * This reads `catalog.rules` and defaults to the default rules if no value is present.\n * The value of the config should be a list of config objects, each with a single `allow`\n * field which in turn is a list of entity kinds to allow.\n *\n * If there is no matching rule to allow an ingested entity, it will be rejected by the catalog.\n *\n * It also reads in rules from `catalog.locations`, where each location can have a list\n * of rules for that specific location, specified in a `rules` field.\n *\n * For example:\n *\n * ```yaml\n * catalog:\n * rules:\n * - allow: [Component, API]\n * - allow:\n * - kind: Resource\n * 'spec.type': database\n * - allow: [Template]\n * locations:\n * - type: url\n * pattern: https://github.com/org/*\\/blob/master/template.yaml\n * - allow: [Location]\n * locations:\n * - type: url\n * pattern: https://github.com/org/repo/blob/master/location.yaml\n *\n * locations:\n * - type: url\n * target: https://github.com/org/repo/blob/master/users.yaml\n * rules:\n * - allow: [User, Group]\n * - type: url\n * target: https://github.com/org/repo/blob/master/systems.yaml\n * rules:\n * - allow: [System]\n * ```\n */\n static fromConfig(config: Config) {\n const rules = new Array<CatalogRule>();\n\n if (config.has('catalog.rules')) {\n const globalRules = config\n .getConfigArray('catalog.rules')\n .map(ruleConf => ({\n allow: allowRuleParser.parse(ruleConf.get('allow')),\n locations: ruleConf\n .getOptionalConfigArray('locations')\n ?.map(locationConfig => {\n const location = {\n pattern: locationConfig.getOptionalString('pattern'),\n type: locationConfig.getString('type'),\n exact: locationConfig.getOptionalString('exact'),\n };\n if (location.pattern && location.exact) {\n throw new Error(\n 'A catalog rule location cannot have both exact and pattern values',\n );\n }\n return location;\n }),\n }));\n rules.push(...globalRules);\n } else {\n rules.push(...DefaultCatalogRulesEnforcer.defaultRules);\n }\n\n if (config.has('catalog.locations')) {\n const locationRules = config\n .getConfigArray('catalog.locations')\n .flatMap(locConf => {\n if (!locConf.has('rules')) {\n return [];\n }\n const type = locConf.getString('type');\n const exact = resolveTarget(type, locConf.getString('target'));\n\n return locConf.getConfigArray('rules').map(ruleConf => ({\n allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),\n locations: [{ type, exact }],\n }));\n });\n\n rules.push(...locationRules);\n }\n\n return new DefaultCatalogRulesEnforcer(rules);\n }\n\n private readonly rules: CatalogRule[];\n\n constructor(rules: CatalogRule[]) {\n this.rules = rules;\n }\n\n /**\n * Checks whether a specific entity/location combination is allowed\n * according to the configured rules.\n */\n isAllowed(entity: Entity, location: LocationSpec) {\n for (const rule of this.rules) {\n if (!this.matchLocation(location, rule.locations)) {\n continue;\n }\n\n if (this.matchEntity(entity, rule.allow)) {\n return true;\n }\n }\n\n return false;\n }\n\n private matchLocation(\n location: LocationSpec,\n matchers?: { exact?: string; type: string; pattern?: string }[],\n ): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (matcher.type !== location?.type) {\n continue;\n }\n if (matcher.exact && matcher.exact !== location?.target) {\n continue;\n }\n if (\n matcher.pattern &&\n !minimatch(location?.target, matcher.pattern, {\n nocase: true,\n dot: true,\n })\n ) {\n continue;\n }\n return true;\n }\n\n return false;\n }\n\n private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (\n entity.kind?.toLocaleLowerCase('en-US') !==\n matcher.kind.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n\n if (matcher['spec.type']) {\n if (typeof entity.spec?.type !== 'string') {\n continue;\n }\n if (\n matcher['spec.type'].toLocaleLowerCase('en-US') !==\n entity.spec.type.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n }\n\n return true;\n }\n\n return false;\n }\n}\n\nfunction resolveTarget(type: string, target: string): string {\n if (type !== 'file') {\n return target;\n }\n\n return path.resolve(target);\n}\n"],"names":["z","minimatch","path"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CatalogRules.cjs.js","sources":["../../src/ingestion/CatalogRules.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 { Config } from '@backstage/config';\nimport { Entity } from '@backstage/catalog-model';\nimport path from 'node:path';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { minimatch } from 'minimatch';\nimport { z } from 'zod/v3';\n\n/**\n * Rules to apply to catalog entities.\n *\n * An undefined list of matchers means match all, an empty list of matchers means match none.\n */\nexport type CatalogRule = {\n allow: CatalogRuleAllow[];\n locations?: Array<{\n exact?: string;\n type: string;\n pattern?: string;\n }>;\n};\n\ntype CatalogRuleAllow = {\n kind: string;\n 'spec.type'?: string;\n};\n\n/**\n * Decides whether an entity from a given location is allowed to enter the\n * catalog, according to some rule set.\n */\nexport type CatalogRulesEnforcer = {\n isAllowed(entity: Entity, location: LocationSpec): boolean;\n};\n\nconst allowRuleParser = z.array(\n z\n .object({\n kind: z.string(),\n 'spec.type': z.string().optional(),\n })\n .or(z.string())\n .transform(val => (typeof val === 'string' ? { kind: val } : val)),\n);\n\n/**\n * Implements the default catalog rule set, consuming the config keys\n * `catalog.rules` and `catalog.locations.[].rules`.\n */\nexport class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {\n /**\n * Default rules used by the catalog.\n *\n * Denies any location from specifying user or group entities.\n */\n static readonly defaultRules: CatalogRule[] = [\n {\n allow: ['Component', 'API', 'Location'].map(kind => ({ kind })),\n },\n ];\n\n /**\n * Loads catalog rules from config.\n *\n * This reads `catalog.rules` and defaults to the default rules if no value is present.\n * The value of the config should be a list of config objects, each with a single `allow`\n * field which in turn is a list of entity kinds to allow.\n *\n * If there is no matching rule to allow an ingested entity, it will be rejected by the catalog.\n *\n * It also reads in rules from `catalog.locations`, where each location can have a list\n * of rules for that specific location, specified in a `rules` field.\n *\n * For example:\n *\n * ```yaml\n * catalog:\n * rules:\n * - allow: [Component, API]\n * - allow:\n * - kind: Resource\n * 'spec.type': database\n * - allow: [Template]\n * locations:\n * - type: url\n * pattern: https://github.com/org/*\\/blob/master/template.yaml\n * - allow: [Location]\n * locations:\n * - type: url\n * pattern: https://github.com/org/repo/blob/master/location.yaml\n *\n * locations:\n * - type: url\n * target: https://github.com/org/repo/blob/master/users.yaml\n * rules:\n * - allow: [User, Group]\n * - type: url\n * target: https://github.com/org/repo/blob/master/systems.yaml\n * rules:\n * - allow: [System]\n * ```\n */\n static fromConfig(config: Config) {\n const rules = new Array<CatalogRule>();\n\n if (config.has('catalog.rules')) {\n const globalRules = config\n .getConfigArray('catalog.rules')\n .map(ruleConf => ({\n allow: allowRuleParser.parse(ruleConf.get('allow')),\n locations: ruleConf\n .getOptionalConfigArray('locations')\n ?.map(locationConfig => {\n const location = {\n pattern: locationConfig.getOptionalString('pattern'),\n type: locationConfig.getString('type'),\n exact: locationConfig.getOptionalString('exact'),\n };\n if (location.pattern && location.exact) {\n throw new Error(\n 'A catalog rule location cannot have both exact and pattern values',\n );\n }\n return location;\n }),\n }));\n rules.push(...globalRules);\n } else {\n rules.push(...DefaultCatalogRulesEnforcer.defaultRules);\n }\n\n if (config.has('catalog.locations')) {\n const locationRules = config\n .getConfigArray('catalog.locations')\n .flatMap(locConf => {\n if (!locConf.has('rules')) {\n return [];\n }\n const type = locConf.getString('type');\n const exact = resolveTarget(type, locConf.getString('target'));\n\n return locConf.getConfigArray('rules').map(ruleConf => ({\n allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),\n locations: [{ type, exact }],\n }));\n });\n\n rules.push(...locationRules);\n }\n\n return new DefaultCatalogRulesEnforcer(rules);\n }\n\n private readonly rules: CatalogRule[];\n\n constructor(rules: CatalogRule[]) {\n this.rules = rules;\n }\n\n /**\n * Checks whether a specific entity/location combination is allowed\n * according to the configured rules.\n */\n isAllowed(entity: Entity, location: LocationSpec) {\n for (const rule of this.rules) {\n if (!this.matchLocation(location, rule.locations)) {\n continue;\n }\n\n if (this.matchEntity(entity, rule.allow)) {\n return true;\n }\n }\n\n return false;\n }\n\n private matchLocation(\n location: LocationSpec,\n matchers?: { exact?: string; type: string; pattern?: string }[],\n ): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (matcher.type !== location?.type) {\n continue;\n }\n if (matcher.exact && matcher.exact !== location?.target) {\n continue;\n }\n if (\n matcher.pattern &&\n !minimatch(location?.target, matcher.pattern, {\n nocase: true,\n dot: true,\n })\n ) {\n continue;\n }\n return true;\n }\n\n return false;\n }\n\n private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (entity.kind?.toLowerCase() !== matcher.kind.toLowerCase()) {\n continue;\n }\n\n if (matcher['spec.type']) {\n if (typeof entity.spec?.type !== 'string') {\n continue;\n }\n if (\n matcher['spec.type'].toLowerCase() !== entity.spec.type.toLowerCase()\n ) {\n continue;\n }\n }\n\n return true;\n }\n\n return false;\n }\n}\n\nfunction resolveTarget(type: string, target: string): string {\n if (type !== 'file') {\n return target;\n }\n\n return path.resolve(target);\n}\n"],"names":["z","minimatch","path"],"mappings":"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"}
|
|
@@ -14,8 +14,8 @@ const createPropertyRule = (propertyType) => pluginPermissionNode.createPermissi
|
|
|
14
14
|
value: v3.z.string().optional().describe(`Value of the given property to match on`)
|
|
15
15
|
}),
|
|
16
16
|
apply: (resource, { key, value }) => {
|
|
17
|
-
const normalizedKey = `${propertyType}.${key}`.
|
|
18
|
-
const normalizedValue = value?.
|
|
17
|
+
const normalizedKey = `${propertyType}.${key}`.toLowerCase();
|
|
18
|
+
const normalizedValue = value?.toLowerCase();
|
|
19
19
|
return buildEntitySearch.buildEntitySearch("", resource).some(
|
|
20
20
|
(row) => row.key === normalizedKey && (normalizedValue === void 0 || row.value === normalizedValue)
|
|
21
21
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createPropertyRule.cjs.js","sources":["../../../src/permissions/rules/createPropertyRule.ts"],"sourcesContent":["/*\n * Copyright 2022 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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod/v3';\nimport { buildEntitySearch } from '../../database/operations/stitcher/buildEntitySearch';\n\nexport const createPropertyRule = (propertyType: 'metadata' | 'spec') =>\n createPermissionRule({\n name: `HAS_${propertyType.toUpperCase()}`,\n description: `Allow entities with the specified ${propertyType} subfield`,\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n key: z\n .string()\n .describe(`Property within the entities ${propertyType} to match on`),\n value: z\n .string()\n .optional()\n .describe(`Value of the given property to match on`),\n }),\n apply: (resource, { key, value }) => {\n const normalizedKey = `${propertyType}.${key}`.
|
|
1
|
+
{"version":3,"file":"createPropertyRule.cjs.js","sources":["../../../src/permissions/rules/createPropertyRule.ts"],"sourcesContent":["/*\n * Copyright 2022 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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod/v3';\nimport { buildEntitySearch } from '../../database/operations/stitcher/buildEntitySearch';\n\nexport const createPropertyRule = (propertyType: 'metadata' | 'spec') =>\n createPermissionRule({\n name: `HAS_${propertyType.toUpperCase()}`,\n description: `Allow entities with the specified ${propertyType} subfield`,\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n key: z\n .string()\n .describe(`Property within the entities ${propertyType} to match on`),\n value: z\n .string()\n .optional()\n .describe(`Value of the given property to match on`),\n }),\n apply: (resource, { key, value }) => {\n const normalizedKey = `${propertyType}.${key}`.toLowerCase();\n const normalizedValue = value?.toLowerCase();\n\n return buildEntitySearch('', resource).some(\n row =>\n row.key === normalizedKey &&\n (normalizedValue === undefined || row.value === normalizedValue),\n );\n },\n toQuery: ({ key, value }) => ({\n key: `${propertyType}.${key}`,\n ...(value !== undefined && { values: [value] }),\n }),\n });\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z","buildEntitySearch"],"mappings":";;;;;;;AAqBO,MAAM,kBAAA,GAAqB,CAAC,YAAA,KACjCA,yCAAA,CAAqB;AAAA,EACnB,IAAA,EAAM,CAAA,IAAA,EAAO,YAAA,CAAa,WAAA,EAAa,CAAA,CAAA;AAAA,EACvC,WAAA,EAAa,qCAAqC,YAAY,CAAA,SAAA,CAAA;AAAA,EAC9D,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,KAAE,MAAA,CAAO;AAAA,IACrB,KAAKA,IAAA,CACF,MAAA,GACA,QAAA,CAAS,CAAA,6BAAA,EAAgC,YAAY,CAAA,YAAA,CAAc,CAAA;AAAA,IACtE,OAAOA,IAAA,CACJ,MAAA,GACA,QAAA,EAAS,CACT,SAAS,CAAA,uCAAA,CAAyC;AAAA,GACtD,CAAA;AAAA,EACD,OAAO,CAAC,QAAA,EAAU,EAAE,GAAA,EAAK,OAAM,KAAM;AACnC,IAAA,MAAM,gBAAgB,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,GAAG,GAAG,WAAA,EAAY;AAC3D,IAAA,MAAM,eAAA,GAAkB,OAAO,WAAA,EAAY;AAE3C,IAAA,OAAOC,mCAAA,CAAkB,EAAA,EAAI,QAAQ,CAAA,CAAE,IAAA;AAAA,MACrC,SACE,GAAA,CAAI,GAAA,KAAQ,kBACX,eAAA,KAAoB,MAAA,IAAa,IAAI,KAAA,KAAU,eAAA;AAAA,KACpD;AAAA,EACF,CAAA;AAAA,EACA,OAAA,EAAS,CAAC,EAAE,GAAA,EAAK,OAAM,MAAO;AAAA,IAC5B,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAAA,IAC3B,GAAI,KAAA,KAAU,MAAA,IAAa,EAAE,MAAA,EAAQ,CAAC,KAAK,CAAA;AAAE,GAC/C;AACF,CAAC;;;;"}
|