@backstage-community/plugin-catalog-backend-module-apiiro-entity-processor 0.1.0 → 1.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @backstage-community/plugin-catalog-backend-module-apiiro-entity-processor
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - ff337ee: Added support to automatically apply application annotations to system entities.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [ff337ee]
12
+ - @backstage-community/plugin-apiiro-common@1.0.0
13
+
14
+ ## 0.2.0
15
+
16
+ ### Minor Changes
17
+
18
+ - cd2ccac: Backstage version bump to v1.48.2
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [cd2ccac]
23
+ - @backstage-community/plugin-apiiro-common@0.2.0
24
+
3
25
  ## 0.1.0
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -6,12 +6,17 @@ Catalog backend module that automatically adds Apiiro annotations to Backstage e
6
6
 
7
7
  When enabled, this module:
8
8
 
9
- - Derives the Apiiro repository identifier from the entity's source location.
9
+ - Derives the Apiiro repository identifier from the entity's source location (for Component entities).
10
+ - Derives the Apiiro application identifier from the entity's reference and uid (for System entities, when applications view is enabled).
10
11
  - Adds the following annotations when they are missing:
11
- - `apiiro.com/repo-id`
12
+ - `apiiro.com/repo-id` (for Component entities)
13
+ - `apiiro.com/application-id` (for System entities)
12
14
  - `apiiro.com/allow-metrics-view`
13
15
  - **Does not overwrite** existing Apiiro annotations if they are already set on the entity.
14
16
 
17
+ > [!NOTE]
18
+ > Application annotation support requires the Backstage connector to be configured in Apiiro and `enableApplicationsView` to be set to `true` in the configuration.
19
+
15
20
  This helps you avoid manually managing Apiiro annotations on every entity in your catalog.
16
21
 
17
22
  ## Installation
@@ -43,12 +48,20 @@ will be updated with Apiiro annotations when they are missing.
43
48
 
44
49
  The processor works with the following annotations:
45
50
 
51
+ **For Component entities (repositories):**
52
+
46
53
  - `apiiro.com/repo-id`: `<repo-key>`
47
- - `apiiro.com/allow-metrics-view`: `"true"` or `"false"` (controls whether the Metrics view appears in the Apiiro Tab and Apiiro Widget)
54
+ - `apiiro.com/allow-metrics-view`: `"true"` or `"false"` (controls whether the Metrics view appears in the Component Apiiro tab and Apiiro widget)
55
+
56
+ **For System entities (applications):**
57
+
58
+ - `apiiro.com/application-id`: `<application-key>`
59
+ - `apiiro.com/allow-metrics-view`: `"true"` or `"false"` (controls whether the Metrics view and repository list appears in the System Apiiro tab and Apiiro widget)
48
60
 
49
61
  ### Notes
50
62
 
51
- - Annotation values are derived from the value of `backstage.io/source-location`. If `backstage.io/source-location` is not present, Apiiro annotations will not be added.
63
+ - For Component entities, annotation values are derived from the value of `backstage.io/source-location`. If `backstage.io/source-location` is not present, Apiiro annotations will not be added.
64
+ - For System entities, the application identifier is derived from the entity reference and entity uid and matched against applications in Apiiro (requires `enableApplicationsView: true`).
52
65
  - If Apiiro annotations already exist on an entity, they take precedence and will **not** be overwritten.
53
66
 
54
67
  ## Permissions and Metrics View
@@ -61,23 +74,78 @@ widgets. In `app-config.yaml` or `app-config.production.yaml`:
61
74
  apiiro:
62
75
  accessToken: ${APIIRO_TOKEN}
63
76
  defaultAllowMetricsView: true
77
+ enableApplicationsView: false
64
78
  # Optional configuration to allow or disallow metric views for specific entities
65
79
  annotationControl:
66
80
  entityNames:
67
81
  - component:<namespace>/<entity-name>
82
+ - system:<namespace>/<entity-name>
68
83
  exclude: true
69
84
  ```
70
85
 
71
- Where the parameters in the `annotationControl`:
86
+ Where the configuration parameters are:
72
87
 
73
- - `entityNames` is a list of entity references to control the metrics view access.
74
- - `exclude: true` **blocklist mode** (allow all entities except those listed).
75
- - `exclude: false` → **allowlist mode** (deny all entities except those listed).
88
+ - `defaultAllowMetricsView`: Default value for allowing metrics view (default: `true`).
89
+ - `enableApplicationsView`: Enables application annotation processing for System entities (default: `false`). **Note:** Requires Backstage connector configured in Apiiro.
90
+ - `annotationControl`:
91
+ - `entityNames`: List of entity references to control the metrics view access.
92
+ - `exclude: true` → **blocklist mode** (allow all entities except those listed).
93
+ - `exclude: false` → **allowlist mode** (deny all entities except those listed).
76
94
 
77
95
  The `apiiro.com/allow-metrics-view` annotation and the above configuration
78
96
  together determine whether a given entity can display metrics on Apiiro Tab and Apiiro Widget.
79
97
  If you configure this list it will override the `defaultAllowMetricsView` configuration.
80
98
 
99
+ ## High-Level Design
100
+
101
+ ### Architecture Overview
102
+
103
+ The Apiiro Entity Processor is a catalog backend module that automatically enriches Backstage entities with Apiiro-specific annotations. The processor operates during catalog processing cycles and integrates with external Apiiro services to maintain consistent security metadata across your software ecosystem.
104
+
105
+ ### Core Components
106
+
107
+ 1. **ApiiroAnnotationProcessor**: Main catalog processor that handles entity annotation logic
108
+ 2. **CacheManager**: Manages persistent caching of Apiiro data using Backstage's CacheService
109
+ 3. **ApiiroApiClient**: Handles communication with Apiiro's REST APIs
110
+ 4. **Entity Reference Resolution**: Maps Backstage entity references to Apiiro application identifiers
111
+
112
+ ### Caching Strategy
113
+
114
+ #### Why CacheService (Persistent Caching)?
115
+
116
+ This implementation uses **Backstage's CacheService** for persistent data storage across multiple catalog runs. This decision is based on catalog module best practices and performance optimization requirements:
117
+
118
+ **Benefits of CacheService:**
119
+
120
+ - **Performance Optimization**: Reduces expensive external API calls to Apiiro (repository and application data)
121
+ - **Rate Limit Compliance**: Respects Apiiro API rate limits by minimizing requests
122
+ - **System Reliability**: Cached data provides resilience against network failures
123
+ - **Scalability**: Handles growing entity counts without degrading performance
124
+ - **Cross-Run Efficiency**: Multiple catalog runs (every refresh rate) reuse the same cached data
125
+
126
+ **Cached Data Types:**
127
+
128
+ - **Repository Mappings**: URL to Apiiro repository key mappings (1-hour TTL)
129
+ - **Application Mappings**: Entity UID to Apiiro application key mappings (1-hour TTL)
130
+ - **Entity References**: System entity reference to UID mappings (1-hour TTL)
131
+
132
+ ### Data Flow
133
+
134
+ 1. **Catalog Processing Trigger**: Backstage catalog runs every refresh rate (default: 5 minutes)
135
+ 2. **Cache Check**: Processor checks CacheService for existing Apiiro data
136
+ 3. **API Integration**: Fetches fresh data from Apiiro only when cache expires (1-hour TTL)
137
+ 4. **Entity Processing**: Enriches entities with appropriate Apiiro annotations
138
+ 5. **Cache Update**: Refreshes persistent cache with new data when needed
139
+
140
+ ### Integration Points
141
+
142
+ - **Backstage Catalog**: Processes entities during standard catalog refresh cycles
143
+ - **Apiiro Repository API**: Maps source locations to repository identifiers
144
+ - **Apiiro Application API**: Maps system entities to application identifiers
145
+ - **Backstage Catalog API**: Resolves entity references to UIDs for system entity mapping
146
+
147
+ This design follows catalog module best practices by prioritizing performance, reliability, and efficient resource utilization while maintaining data freshness appropriate for security metadata.
148
+
81
149
  ## Development
82
150
 
83
151
  This module is developed as part of the Apiiro Backstage integration.
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ var fetch = require('node-fetch');
4
+ var pluginApiiroCommon = require('@backstage-community/plugin-apiiro-common');
5
+ var types = require('./types.cjs.js');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+
9
+ var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
10
+
11
+ class ApiiroApiClient {
12
+ constructor(accessToken) {
13
+ this.accessToken = accessToken;
14
+ }
15
+ async fetchRepositoriesPage(pageCursor) {
16
+ const baseUrl = pluginApiiroCommon.APIIRO_DEFAULT_BASE_URL;
17
+ const params = new URLSearchParams();
18
+ params.append("pageSize", types.PAGE_LIMIT.toString());
19
+ if (pageCursor) {
20
+ params.append("next", pageCursor);
21
+ }
22
+ const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;
23
+ const response = await fetch__default.default(url, {
24
+ method: "GET",
25
+ headers: {
26
+ Authorization: `Bearer ${this.accessToken}`,
27
+ "Content-Type": "application/json"
28
+ }
29
+ });
30
+ if (!response.ok) {
31
+ const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;
32
+ throw new Error(errorMessage);
33
+ }
34
+ const data = await response.json();
35
+ return {
36
+ items: data.items || [],
37
+ next: data.next || null
38
+ };
39
+ }
40
+ async fetchAllRepositories() {
41
+ const items = [];
42
+ let nextCursor = void 0;
43
+ let pageCount = 0;
44
+ do {
45
+ pageCount++;
46
+ if (pageCount > types.MAX_PAGES) {
47
+ throw new Error(
48
+ `Pagination limit exceeded: Maximum of ${types.MAX_PAGES} pages allowed. This may indicate an infinite loop or an unexpectedly large dataset. Fetched ${items.length} repositories so far.`
49
+ );
50
+ }
51
+ if (!this.accessToken) {
52
+ console.warn(
53
+ "[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config."
54
+ );
55
+ return {
56
+ items: [],
57
+ totalCount: 0
58
+ };
59
+ }
60
+ const page = await this.fetchRepositoriesPage(nextCursor ?? void 0);
61
+ items.push(...page.items);
62
+ nextCursor = page.next;
63
+ } while (nextCursor);
64
+ return { items, totalCount: items.length };
65
+ }
66
+ async fetchApplicationsPage(pageCursor) {
67
+ const baseUrl = pluginApiiroCommon.APIIRO_DEFAULT_BASE_URL;
68
+ const params = new URLSearchParams();
69
+ params.append("pageSize", types.PAGE_LIMIT.toString());
70
+ if (pageCursor) {
71
+ params.append("next", pageCursor);
72
+ }
73
+ const url = `${baseUrl}/rest-api/v1/applications/profiles?${params.toString()}`;
74
+ const response = await fetch__default.default(url, {
75
+ method: "GET",
76
+ headers: {
77
+ Authorization: `Bearer ${this.accessToken}`,
78
+ "Content-Type": "application/json"
79
+ }
80
+ });
81
+ if (!response.ok) {
82
+ const errorMessage = `Failed to fetch applications from Apiiro API. Status: ${response.status} ${response.statusText}`;
83
+ throw new Error(errorMessage);
84
+ }
85
+ const data = await response.json();
86
+ return {
87
+ items: data.items || [],
88
+ next: data.next || null
89
+ };
90
+ }
91
+ async fetchAllApplications() {
92
+ const items = [];
93
+ let nextCursor = void 0;
94
+ let pageCount = 0;
95
+ do {
96
+ pageCount++;
97
+ if (pageCount > types.MAX_PAGES) {
98
+ throw new Error(
99
+ `Pagination limit exceeded: Maximum of ${types.MAX_PAGES} pages allowed. This may indicate an infinite loop or an unexpectedly large dataset. Fetched ${items.length} applications so far.`
100
+ );
101
+ }
102
+ if (!this.accessToken) {
103
+ console.warn(
104
+ "[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config."
105
+ );
106
+ return {
107
+ items: [],
108
+ totalCount: 0
109
+ };
110
+ }
111
+ const page = await this.fetchApplicationsPage(nextCursor ?? void 0);
112
+ items.push(...page.items);
113
+ nextCursor = page.next;
114
+ } while (nextCursor);
115
+ return { items, totalCount: items.length };
116
+ }
117
+ }
118
+
119
+ exports.ApiiroApiClient = ApiiroApiClient;
120
+ //# sourceMappingURL=apiClient.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiClient.cjs.js","sources":["../../src/helpers/apiClient.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 */\nimport fetch from 'node-fetch';\nimport { APIIRO_DEFAULT_BASE_URL } from '@backstage-community/plugin-apiiro-common';\nimport {\n ApiiroRepositoriesResponse,\n ApiiroApplicationsResponse,\n RepositoryItem,\n ApplicationItem,\n PAGE_LIMIT,\n MAX_PAGES,\n} from './types';\n\nexport class ApiiroApiClient {\n constructor(private readonly accessToken: string | undefined) {}\n\n private async fetchRepositoriesPage(\n pageCursor?: string,\n ): Promise<ApiiroRepositoriesResponse> {\n const baseUrl = APIIRO_DEFAULT_BASE_URL;\n\n const params = new URLSearchParams();\n params.append('pageSize', PAGE_LIMIT.toString());\n if (pageCursor) {\n params.append('next', pageCursor);\n }\n\n const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;\n throw new Error(errorMessage);\n }\n\n const data = (await response.json()) as ApiiroRepositoriesResponse;\n return {\n items: data.items || [],\n next: data.next || null,\n };\n }\n\n async fetchAllRepositories(): Promise<{\n items: RepositoryItem[];\n totalCount: number;\n }> {\n const items: RepositoryItem[] = [];\n let nextCursor: string | null | undefined = undefined;\n let pageCount = 0;\n\n do {\n pageCount++;\n\n if (pageCount > MAX_PAGES) {\n throw new Error(\n `Pagination limit exceeded: Maximum of ${MAX_PAGES} pages allowed. ` +\n `This may indicate an infinite loop or an unexpectedly large dataset. ` +\n `Fetched ${items.length} repositories so far.`,\n );\n }\n\n if (!this.accessToken) {\n console.warn(\n '[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config.',\n );\n return {\n items: [],\n totalCount: 0,\n };\n }\n\n const page = await this.fetchRepositoriesPage(nextCursor ?? undefined);\n items.push(...page.items);\n nextCursor = page.next;\n } while (nextCursor);\n\n return { items, totalCount: items.length };\n }\n\n private async fetchApplicationsPage(\n pageCursor?: string,\n ): Promise<ApiiroApplicationsResponse> {\n const baseUrl = APIIRO_DEFAULT_BASE_URL;\n\n const params = new URLSearchParams();\n params.append('pageSize', PAGE_LIMIT.toString());\n if (pageCursor) {\n params.append('next', pageCursor);\n }\n\n const url = `${baseUrl}/rest-api/v1/applications/profiles?${params.toString()}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorMessage = `Failed to fetch applications from Apiiro API. Status: ${response.status} ${response.statusText}`;\n throw new Error(errorMessage);\n }\n\n const data = (await response.json()) as ApiiroApplicationsResponse;\n return {\n items: data.items || [],\n next: data.next || null,\n };\n }\n\n async fetchAllApplications(): Promise<{\n items: ApplicationItem[];\n totalCount: number;\n }> {\n const items: ApplicationItem[] = [];\n let nextCursor: string | null | undefined = undefined;\n let pageCount = 0;\n\n do {\n pageCount++;\n\n if (pageCount > MAX_PAGES) {\n throw new Error(\n `Pagination limit exceeded: Maximum of ${MAX_PAGES} pages allowed. ` +\n `This may indicate an infinite loop or an unexpectedly large dataset. ` +\n `Fetched ${items.length} applications so far.`,\n );\n }\n\n if (!this.accessToken) {\n console.warn(\n '[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config.',\n );\n return {\n items: [],\n totalCount: 0,\n };\n }\n const page = await this.fetchApplicationsPage(nextCursor ?? undefined);\n items.push(...page.items);\n nextCursor = page.next;\n } while (nextCursor);\n return { items, totalCount: items.length };\n }\n}\n"],"names":["APIIRO_DEFAULT_BASE_URL","PAGE_LIMIT","fetch","MAX_PAGES"],"mappings":"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"}
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ var catalogModel = require('@backstage/catalog-model');
4
+ var types = require('./types.cjs.js');
5
+
6
+ class CacheManager {
7
+ constructor(apiClient, cacheService, catalogApi, auth) {
8
+ this.apiClient = apiClient;
9
+ this.cacheService = cacheService;
10
+ this.catalogApi = catalogApi;
11
+ this.auth = auth;
12
+ }
13
+ // Cache keys for Backstage CacheService
14
+ static REPO_CACHE_KEY = "apiiro:repositories";
15
+ static APP_CACHE_KEY = "apiiro:applications";
16
+ static ENTITY_REF_CACHE_KEY = "apiiro:entity-refs";
17
+ static REPO_LOCK_KEY = "apiiro:repositories:lock";
18
+ static APP_LOCK_KEY = "apiiro:applications:lock";
19
+ static ENTITY_REF_LOCK_KEY = "apiiro:entity-refs:lock";
20
+ isCacheExpired(lastFetched) {
21
+ const now = Date.now();
22
+ return now - lastFetched > types.CACHE_TTL_MS;
23
+ }
24
+ buildRepositoryUrlToKeyMap(repositories) {
25
+ const urlToKeyMap = {};
26
+ const tempMap = /* @__PURE__ */ new Map();
27
+ for (const repo of repositories) {
28
+ if (!repo.url || !repo.key) {
29
+ continue;
30
+ }
31
+ const existing = tempMap.get(repo.url);
32
+ if (!existing || repo.isDefaultBranch && !existing.isDefaultBranch) {
33
+ tempMap.set(repo.url, {
34
+ key: repo.key,
35
+ isDefaultBranch: repo.isDefaultBranch || false
36
+ });
37
+ }
38
+ }
39
+ tempMap.forEach((value, key) => {
40
+ urlToKeyMap[key] = value.key;
41
+ });
42
+ return urlToKeyMap;
43
+ }
44
+ async fetchAndBuildRepoMap() {
45
+ try {
46
+ const { items } = await this.apiClient.fetchAllRepositories();
47
+ return this.buildRepositoryUrlToKeyMap(items);
48
+ } catch (error) {
49
+ const errorMessage = error instanceof Error ? error.message : String(error);
50
+ console.error(
51
+ `[CacheManager] Error fetching repositories from Apiiro API: ${errorMessage}`
52
+ );
53
+ return {};
54
+ }
55
+ }
56
+ async refreshRepositoryCacheIfNeeded() {
57
+ const cachedData = await this.cacheService.get(
58
+ CacheManager.REPO_CACHE_KEY
59
+ );
60
+ if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {
61
+ return;
62
+ }
63
+ const refreshLock = await this.cacheService.get(CacheManager.REPO_LOCK_KEY);
64
+ const now = Date.now();
65
+ if (refreshLock && now - refreshLock.timestamp < types.REFRESH_LOCK_TTL_MS) {
66
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
67
+ await this.refreshRepositoryCacheIfNeeded();
68
+ return;
69
+ }
70
+ await this.cacheService.set(CacheManager.REPO_LOCK_KEY, {
71
+ locked: true,
72
+ timestamp: now
73
+ });
74
+ try {
75
+ const urlToKeyMap = await this.fetchAndBuildRepoMap();
76
+ await this.cacheService.set(
77
+ CacheManager.REPO_CACHE_KEY,
78
+ {
79
+ urlToKeyMap,
80
+ lastFetched: Date.now(),
81
+ fetchCompleted: true
82
+ },
83
+ { ttl: types.CACHE_TTL_MS }
84
+ );
85
+ await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);
86
+ } catch (error) {
87
+ await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);
88
+ throw error;
89
+ }
90
+ }
91
+ async getRepoKey(repoUrl) {
92
+ if (!repoUrl) {
93
+ return null;
94
+ }
95
+ await this.refreshRepositoryCacheIfNeeded();
96
+ const cachedData = await this.cacheService.get(
97
+ CacheManager.REPO_CACHE_KEY
98
+ );
99
+ return cachedData?.urlToKeyMap[repoUrl] || null;
100
+ }
101
+ buildApplicationUidToKeyMap(applications) {
102
+ const uidToKeyMap = {};
103
+ for (const app of applications) {
104
+ if (!app.key || !app.externalSources || app.externalSources.length === 0) {
105
+ continue;
106
+ }
107
+ for (const source of app.externalSources) {
108
+ if (source.server.provider === "Backstage") {
109
+ uidToKeyMap[source.id] = app.key;
110
+ }
111
+ }
112
+ }
113
+ return uidToKeyMap;
114
+ }
115
+ async fetchAndBuildAppMap() {
116
+ try {
117
+ const { items } = await this.apiClient.fetchAllApplications();
118
+ return this.buildApplicationUidToKeyMap(items);
119
+ } catch (error) {
120
+ const errorMessage = error instanceof Error ? error.message : String(error);
121
+ console.error(
122
+ `[CacheManager] Error fetching applications from Apiiro API: ${errorMessage}`
123
+ );
124
+ return {};
125
+ }
126
+ }
127
+ async refreshApplicationCacheIfNeeded() {
128
+ const cachedData = await this.cacheService.get(
129
+ CacheManager.APP_CACHE_KEY
130
+ );
131
+ if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {
132
+ return;
133
+ }
134
+ const refreshLock = await this.cacheService.get(CacheManager.APP_LOCK_KEY);
135
+ const now = Date.now();
136
+ if (refreshLock && now - refreshLock.timestamp < types.REFRESH_LOCK_TTL_MS) {
137
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
138
+ await this.refreshApplicationCacheIfNeeded();
139
+ return;
140
+ }
141
+ await this.cacheService.set(CacheManager.APP_LOCK_KEY, {
142
+ locked: true,
143
+ timestamp: now
144
+ });
145
+ try {
146
+ const uidToKeyMap = await this.fetchAndBuildAppMap();
147
+ await this.cacheService.set(
148
+ CacheManager.APP_CACHE_KEY,
149
+ {
150
+ uidToKeyMap,
151
+ lastFetched: Date.now(),
152
+ fetchCompleted: true
153
+ },
154
+ { ttl: types.CACHE_TTL_MS }
155
+ );
156
+ await this.cacheService.delete(CacheManager.APP_LOCK_KEY);
157
+ } catch (error) {
158
+ await this.cacheService.delete(CacheManager.APP_LOCK_KEY);
159
+ throw error;
160
+ }
161
+ }
162
+ async getApplicationId(entityUid) {
163
+ if (!entityUid) {
164
+ return null;
165
+ }
166
+ await this.refreshApplicationCacheIfNeeded();
167
+ const cachedData = await this.cacheService.get(
168
+ CacheManager.APP_CACHE_KEY
169
+ );
170
+ return cachedData?.uidToKeyMap[entityUid] || null;
171
+ }
172
+ async fetchAndBuildEntityRefMap() {
173
+ if (!this.catalogApi || !this.auth) {
174
+ return {};
175
+ }
176
+ try {
177
+ const { token } = await this.auth.getPluginRequestToken({
178
+ onBehalfOf: await this.auth.getOwnServiceCredentials(),
179
+ targetPluginId: "catalog"
180
+ });
181
+ const { items } = await this.catalogApi.getEntities(
182
+ {
183
+ filter: { kind: "System" },
184
+ fields: [
185
+ "kind",
186
+ "metadata.uid",
187
+ "metadata.name",
188
+ "metadata.namespace"
189
+ ]
190
+ },
191
+ { token }
192
+ );
193
+ const refToUidMap = {};
194
+ for (const entity of items) {
195
+ if (entity.metadata.uid) {
196
+ const entityRef = catalogModel.stringifyEntityRef({
197
+ kind: entity.kind,
198
+ name: entity.metadata.name,
199
+ namespace: entity.metadata.namespace
200
+ });
201
+ refToUidMap[entityRef] = entity.metadata.uid;
202
+ }
203
+ }
204
+ return refToUidMap;
205
+ } catch (error) {
206
+ const errorMessage = error instanceof Error ? error.message : String(error);
207
+ console.error(
208
+ `[CacheManager] Error fetching entities from catalog: ${errorMessage}`
209
+ );
210
+ return {};
211
+ }
212
+ }
213
+ async refreshEntityRefCacheIfNeeded() {
214
+ const cachedData = await this.cacheService.get(
215
+ CacheManager.ENTITY_REF_CACHE_KEY
216
+ );
217
+ if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {
218
+ return;
219
+ }
220
+ const refreshLock = await this.cacheService.get(CacheManager.ENTITY_REF_LOCK_KEY);
221
+ const now = Date.now();
222
+ if (refreshLock && now - refreshLock.timestamp < types.REFRESH_LOCK_TTL_MS) {
223
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
224
+ await this.refreshEntityRefCacheIfNeeded();
225
+ return;
226
+ }
227
+ await this.cacheService.set(CacheManager.ENTITY_REF_LOCK_KEY, {
228
+ locked: true,
229
+ timestamp: now
230
+ });
231
+ try {
232
+ const refToUidMap = await this.fetchAndBuildEntityRefMap();
233
+ await this.cacheService.set(
234
+ CacheManager.ENTITY_REF_CACHE_KEY,
235
+ {
236
+ refToUidMap,
237
+ lastFetched: Date.now(),
238
+ fetchCompleted: true
239
+ },
240
+ { ttl: types.CACHE_TTL_MS }
241
+ );
242
+ await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);
243
+ } catch (error) {
244
+ await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);
245
+ throw error;
246
+ }
247
+ }
248
+ async invalidateEntityRefCache() {
249
+ await this.cacheService.set(CacheManager.ENTITY_REF_CACHE_KEY, {
250
+ refToUidMap: {},
251
+ lastFetched: 0,
252
+ fetchCompleted: false
253
+ });
254
+ }
255
+ async getEntityUid(entityRef) {
256
+ if (!entityRef) {
257
+ return null;
258
+ }
259
+ await this.refreshEntityRefCacheIfNeeded();
260
+ const cachedData = await this.cacheService.get(
261
+ CacheManager.ENTITY_REF_CACHE_KEY
262
+ );
263
+ return cachedData?.refToUidMap[entityRef] || null;
264
+ }
265
+ }
266
+
267
+ exports.CacheManager = CacheManager;
268
+ //# sourceMappingURL=cacheManager.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cacheManager.cjs.js","sources":["../../src/helpers/cacheManager.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 */\nimport { CatalogApi } from '@backstage/catalog-client';\nimport { AuthService, CacheService } from '@backstage/backend-plugin-api';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { ApiiroApiClient } from './apiClient';\nimport {\n RepositoryItem,\n ApplicationItem,\n CachedRepositoryData,\n CachedApplicationData,\n CachedEntityRefData,\n CACHE_TTL_MS,\n REFRESH_LOCK_TTL_MS,\n} from './types';\n\nexport class CacheManager {\n // Cache keys for Backstage CacheService\n private static readonly REPO_CACHE_KEY = 'apiiro:repositories';\n private static readonly APP_CACHE_KEY = 'apiiro:applications';\n private static readonly ENTITY_REF_CACHE_KEY = 'apiiro:entity-refs';\n private static readonly REPO_LOCK_KEY = 'apiiro:repositories:lock';\n private static readonly APP_LOCK_KEY = 'apiiro:applications:lock';\n private static readonly ENTITY_REF_LOCK_KEY = 'apiiro:entity-refs:lock';\n\n constructor(\n private readonly apiClient: ApiiroApiClient,\n private readonly cacheService: CacheService,\n private readonly catalogApi?: CatalogApi,\n private readonly auth?: AuthService,\n ) {}\n\n private isCacheExpired(lastFetched: number): boolean {\n const now = Date.now();\n return now - lastFetched > CACHE_TTL_MS;\n }\n\n private buildRepositoryUrlToKeyMap(\n repositories: RepositoryItem[],\n ): Record<string, string> {\n const urlToKeyMap: Record<string, string> = {};\n const tempMap = new Map<\n string,\n { key: string; isDefaultBranch: boolean }\n >();\n\n for (const repo of repositories) {\n if (!repo.url || !repo.key) {\n continue;\n }\n\n const existing = tempMap.get(repo.url);\n if (!existing || (repo.isDefaultBranch && !existing.isDefaultBranch)) {\n tempMap.set(repo.url, {\n key: repo.key,\n isDefaultBranch: repo.isDefaultBranch || false,\n });\n }\n }\n\n tempMap.forEach((value, key) => {\n urlToKeyMap[key] = value.key;\n });\n\n return urlToKeyMap;\n }\n\n private async fetchAndBuildRepoMap(): Promise<Record<string, string>> {\n try {\n const { items } = await this.apiClient.fetchAllRepositories();\n return this.buildRepositoryUrlToKeyMap(items);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[CacheManager] Error fetching repositories from Apiiro API: ${errorMessage}`,\n );\n return {};\n }\n }\n\n async refreshRepositoryCacheIfNeeded(): Promise<void> {\n const cachedData = await this.cacheService.get<CachedRepositoryData>(\n CacheManager.REPO_CACHE_KEY,\n );\n\n if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {\n return;\n }\n\n const refreshLock = await this.cacheService.get<{\n locked: boolean;\n timestamp: number;\n }>(CacheManager.REPO_LOCK_KEY);\n const now = Date.now();\n if (refreshLock && now - refreshLock.timestamp < REFRESH_LOCK_TTL_MS) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n await this.refreshRepositoryCacheIfNeeded();\n return;\n }\n\n await this.cacheService.set(CacheManager.REPO_LOCK_KEY, {\n locked: true,\n timestamp: now,\n });\n\n try {\n const urlToKeyMap = await this.fetchAndBuildRepoMap();\n\n await this.cacheService.set(\n CacheManager.REPO_CACHE_KEY,\n {\n urlToKeyMap,\n lastFetched: Date.now(),\n fetchCompleted: true,\n },\n { ttl: CACHE_TTL_MS },\n );\n\n await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);\n } catch (error) {\n await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);\n throw error;\n }\n }\n\n async getRepoKey(repoUrl: string): Promise<string | null> {\n if (!repoUrl) {\n return null;\n }\n\n await this.refreshRepositoryCacheIfNeeded();\n const cachedData = await this.cacheService.get<CachedRepositoryData>(\n CacheManager.REPO_CACHE_KEY,\n );\n return cachedData?.urlToKeyMap[repoUrl] || null;\n }\n\n private buildApplicationUidToKeyMap(\n applications: ApplicationItem[],\n ): Record<string, string> {\n const uidToKeyMap: Record<string, string> = {};\n\n for (const app of applications) {\n if (\n !app.key ||\n !app.externalSources ||\n app.externalSources.length === 0\n ) {\n continue;\n }\n\n for (const source of app.externalSources) {\n if (source.server.provider === 'Backstage') {\n uidToKeyMap[source.id] = app.key;\n }\n }\n }\n return uidToKeyMap;\n }\n\n private async fetchAndBuildAppMap(): Promise<Record<string, string>> {\n try {\n const { items } = await this.apiClient.fetchAllApplications();\n return this.buildApplicationUidToKeyMap(items);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[CacheManager] Error fetching applications from Apiiro API: ${errorMessage}`,\n );\n return {};\n }\n }\n\n async refreshApplicationCacheIfNeeded(): Promise<void> {\n const cachedData = await this.cacheService.get<CachedApplicationData>(\n CacheManager.APP_CACHE_KEY,\n );\n\n if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {\n return;\n }\n\n const refreshLock = await this.cacheService.get<{\n locked: boolean;\n timestamp: number;\n }>(CacheManager.APP_LOCK_KEY);\n const now = Date.now();\n if (refreshLock && now - refreshLock.timestamp < REFRESH_LOCK_TTL_MS) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n await this.refreshApplicationCacheIfNeeded();\n return;\n }\n\n await this.cacheService.set(CacheManager.APP_LOCK_KEY, {\n locked: true,\n timestamp: now,\n });\n\n try {\n const uidToKeyMap = await this.fetchAndBuildAppMap();\n await this.cacheService.set(\n CacheManager.APP_CACHE_KEY,\n {\n uidToKeyMap,\n lastFetched: Date.now(),\n fetchCompleted: true,\n },\n { ttl: CACHE_TTL_MS },\n );\n\n await this.cacheService.delete(CacheManager.APP_LOCK_KEY);\n } catch (error) {\n await this.cacheService.delete(CacheManager.APP_LOCK_KEY);\n throw error;\n }\n }\n\n async getApplicationId(\n entityUid: string | undefined,\n ): Promise<string | null> {\n if (!entityUid) {\n return null;\n }\n\n await this.refreshApplicationCacheIfNeeded();\n const cachedData = await this.cacheService.get<CachedApplicationData>(\n CacheManager.APP_CACHE_KEY,\n );\n return cachedData?.uidToKeyMap[entityUid] || null;\n }\n\n private async fetchAndBuildEntityRefMap(): Promise<Record<string, string>> {\n if (!this.catalogApi || !this.auth) {\n return {};\n }\n\n try {\n const { token } = await this.auth.getPluginRequestToken({\n onBehalfOf: await this.auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n\n const { items } = await this.catalogApi.getEntities(\n {\n filter: { kind: 'System' },\n fields: [\n 'kind',\n 'metadata.uid',\n 'metadata.name',\n 'metadata.namespace',\n ],\n },\n { token },\n );\n\n const refToUidMap: Record<string, string> = {};\n for (const entity of items) {\n if (entity.metadata.uid) {\n const entityRef = stringifyEntityRef({\n kind: entity.kind,\n name: entity.metadata.name,\n namespace: entity.metadata.namespace,\n });\n refToUidMap[entityRef] = entity.metadata.uid;\n }\n }\n\n return refToUidMap;\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[CacheManager] Error fetching entities from catalog: ${errorMessage}`,\n );\n return {};\n }\n }\n\n async refreshEntityRefCacheIfNeeded(): Promise<void> {\n const cachedData = await this.cacheService.get<CachedEntityRefData>(\n CacheManager.ENTITY_REF_CACHE_KEY,\n );\n\n if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {\n return;\n }\n\n const refreshLock = await this.cacheService.get<{\n locked: boolean;\n timestamp: number;\n }>(CacheManager.ENTITY_REF_LOCK_KEY);\n const now = Date.now();\n if (refreshLock && now - refreshLock.timestamp < REFRESH_LOCK_TTL_MS) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n await this.refreshEntityRefCacheIfNeeded();\n return;\n }\n\n await this.cacheService.set(CacheManager.ENTITY_REF_LOCK_KEY, {\n locked: true,\n timestamp: now,\n });\n\n try {\n const refToUidMap = await this.fetchAndBuildEntityRefMap();\n await this.cacheService.set(\n CacheManager.ENTITY_REF_CACHE_KEY,\n {\n refToUidMap,\n lastFetched: Date.now(),\n fetchCompleted: true,\n },\n { ttl: CACHE_TTL_MS },\n );\n\n await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);\n } catch (error) {\n await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);\n throw error;\n }\n }\n\n async invalidateEntityRefCache(): Promise<void> {\n await this.cacheService.set(CacheManager.ENTITY_REF_CACHE_KEY, {\n refToUidMap: {},\n lastFetched: 0,\n fetchCompleted: false,\n });\n }\n\n async getEntityUid(entityRef: string): Promise<string | null> {\n if (!entityRef) {\n return null;\n }\n\n await this.refreshEntityRefCacheIfNeeded();\n const cachedData = await this.cacheService.get<CachedEntityRefData>(\n CacheManager.ENTITY_REF_CACHE_KEY,\n );\n\n return cachedData?.refToUidMap[entityRef] || null;\n }\n}\n"],"names":["CACHE_TTL_MS","REFRESH_LOCK_TTL_MS","stringifyEntityRef"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const BACKSTAGE_SOURCE_LOCATION_ANNOTATION = "backstage.io/source-location";
4
+ const CACHE_TTL_MS = 60 * 60 * 1e3;
5
+ const REFRESH_LOCK_TTL_MS = 2 * 60 * 1e3;
6
+ const PAGE_LIMIT = 1e3;
7
+ const MAX_PAGES = 1e3;
8
+ const AZURE_HOST_NAME = "dev.azure.com";
9
+
10
+ exports.AZURE_HOST_NAME = AZURE_HOST_NAME;
11
+ exports.BACKSTAGE_SOURCE_LOCATION_ANNOTATION = BACKSTAGE_SOURCE_LOCATION_ANNOTATION;
12
+ exports.CACHE_TTL_MS = CACHE_TTL_MS;
13
+ exports.MAX_PAGES = MAX_PAGES;
14
+ exports.PAGE_LIMIT = PAGE_LIMIT;
15
+ exports.REFRESH_LOCK_TTL_MS = REFRESH_LOCK_TTL_MS;
16
+ //# sourceMappingURL=types.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.cjs.js","sources":["../../src/helpers/types.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 */\nexport interface RepositoryItem {\n name?: string | null;\n key?: string | null;\n url?: string | null;\n isDefaultBranch?: boolean;\n branchName?: string | null;\n [key: string]: unknown;\n}\n\nexport interface ApiiroRepositoriesResponse {\n items: RepositoryItem[];\n next?: string | null;\n}\n\nexport interface ExternalSource {\n id: string;\n server: {\n id: string;\n provider: string;\n tokenExpirationDate: string | null;\n url: string;\n };\n}\n\nexport interface ApplicationItem {\n name?: string | null;\n key?: string | null;\n externalSources?: ExternalSource[];\n [key: string]: unknown;\n}\n\nexport interface ApiiroApplicationsResponse {\n items: ApplicationItem[];\n next?: string | null;\n}\n\nexport interface CachedRepositoryData {\n urlToKeyMap: Record<string, string>;\n lastFetched: number;\n fetchCompleted: boolean;\n [key: string]: any;\n}\n\nexport interface CachedApplicationData {\n uidToKeyMap: Record<string, string>;\n lastFetched: number;\n fetchCompleted: boolean;\n [key: string]: any;\n}\n\nexport interface CachedEntityRefData {\n refToUidMap: Record<string, string>;\n lastFetched: number;\n fetchCompleted: boolean;\n [key: string]: any;\n}\n\nexport const BACKSTAGE_SOURCE_LOCATION_ANNOTATION =\n 'backstage.io/source-location';\n\nexport const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\nexport const REFRESH_LOCK_TTL_MS = 2 * 60 * 1000; // 2 minutes\nexport const PAGE_LIMIT = 1000;\nexport const MAX_PAGES = 1000;\nexport const AZURE_HOST_NAME = 'dev.azure.com';\n"],"names":[],"mappings":";;AAwEO,MAAM,oCAAA,GACX;AAEK,MAAM,YAAA,GAAe,KAAK,EAAA,GAAK;AAC/B,MAAM,mBAAA,GAAsB,IAAI,EAAA,GAAK;AACrC,MAAM,UAAA,GAAa;AACnB,MAAM,SAAA,GAAY;AAClB,MAAM,eAAA,GAAkB;;;;;;;;;"}
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ var types = require('./types.cjs.js');
4
+ var pathToRegexp = require('path-to-regexp');
5
+
6
+ function extractRepoUrlFromSourceLocation(sourceLocation) {
7
+ try {
8
+ if (!sourceLocation) {
9
+ return null;
10
+ }
11
+ const cleanUrl = sourceLocation.replace(/^url:/, "");
12
+ const matches = cleanUrl.match(
13
+ /https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\/.*)?/g
14
+ );
15
+ if (!matches) {
16
+ return null;
17
+ }
18
+ const url = new URL(matches[0]);
19
+ const hostname = url.host.toLowerCase();
20
+ let repoPath = null;
21
+ if (hostname === types.AZURE_HOST_NAME) {
22
+ const matcher = pathToRegexp.match("/:org/:project/_git/:repo", { end: false });
23
+ const extracted = matcher(url.pathname);
24
+ if (extracted) {
25
+ repoPath = `/${extracted.params.org}/${extracted.params.project}/_git/${extracted.params.repo}`;
26
+ }
27
+ } else if (hostname.includes("gitlab")) {
28
+ const cleanPath = url.pathname.replace(/\/-\/(tree|blob)\/[^\/]+.*$/, "");
29
+ const matcher = pathToRegexp.match("/:org/:repo", { end: false });
30
+ const extracted = matcher(cleanPath);
31
+ if (extracted) {
32
+ repoPath = cleanPath;
33
+ }
34
+ } else {
35
+ const matcher = pathToRegexp.match("/:org/:repo", { end: false });
36
+ const extracted = matcher(url.pathname);
37
+ if (extracted) {
38
+ repoPath = `/${extracted.params.org}/${extracted.params.repo}`;
39
+ }
40
+ }
41
+ return repoPath ? `${url.protocol}//${hostname}${repoPath}` : null;
42
+ } catch (error) {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ exports.extractRepoUrlFromSourceLocation = extractRepoUrlFromSourceLocation;
48
+ //# sourceMappingURL=utils.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.cjs.js","sources":["../../src/helpers/utils.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 */\nimport { AZURE_HOST_NAME } from './types';\nimport { match } from 'path-to-regexp';\n\nexport function extractRepoUrlFromSourceLocation(\n sourceLocation?: string,\n): string | null {\n try {\n if (!sourceLocation) {\n return null;\n }\n\n // Extract the base URL (remove \"url:\" prefix if present)\n const cleanUrl = sourceLocation.replace(/^url:/, '');\n const matches = cleanUrl.match(\n /https?:\\/\\/[a-zA-Z0-9\\-\\.]+\\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\\/.*)?/g,\n );\n\n if (!matches) {\n return null;\n }\n const url = new URL(matches[0]);\n const hostname = url.host.toLowerCase();\n\n // Handle different SCM providers using path-to-regexp patterns\n let repoPath: string | null = null;\n if (hostname === AZURE_HOST_NAME) {\n // Azure DevOps format: /org/project/_git/repo\n const matcher = match('/:org/:project/_git/:repo', { end: false });\n const extracted = matcher(url.pathname);\n if (extracted) {\n repoPath = `/${extracted.params.org}/${extracted.params.project}/_git/${extracted.params.repo}`;\n }\n } else if (hostname.includes('gitlab')) {\n // GitLab format: /org/repo or /group/subgroup/.../repo\n // Remove GitLab-specific parts like /-/tree/branch\n const cleanPath = url.pathname.replace(/\\/-\\/(tree|blob)\\/[^\\/]+.*$/, '');\n\n // Use a pattern that matches any number of segments between org and repo\n const matcher = match('/:org/:repo', { end: false });\n const extracted = matcher(cleanPath);\n\n if (extracted) {\n // For GitLab, preserve the full path structure to support subgroups\n repoPath = cleanPath;\n }\n } else {\n // GitHub and Bitbucket format: /org/repo\n const matcher = match('/:org/:repo', { end: false });\n const extracted = matcher(url.pathname);\n if (extracted) {\n repoPath = `/${extracted.params.org}/${extracted.params.repo}`;\n }\n }\n\n return repoPath ? `${url.protocol}//${hostname}${repoPath}` : null;\n } catch (error) {\n return null;\n }\n}\n"],"names":["AZURE_HOST_NAME","match"],"mappings":";;;;;AAkBO,SAAS,iCACd,cAAA,EACe;AACf,EAAA,IAAI;AACF,IAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAA;AACnD,IAAA,MAAM,UAAU,QAAA,CAAS,KAAA;AAAA,MACvB;AAAA,KACF;AAEA,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,CAAC,CAAC,CAAA;AAC9B,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,IAAA,CAAK,WAAA,EAAY;AAGtC,IAAA,IAAI,QAAA,GAA0B,IAAA;AAC9B,IAAA,IAAI,aAAaA,qBAAA,EAAiB;AAEhC,MAAA,MAAM,UAAUC,kBAAA,CAAM,2BAAA,EAA6B,EAAE,GAAA,EAAK,OAAO,CAAA;AACjE,MAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACtC,MAAA,IAAI,SAAA,EAAW;AACb,QAAA,QAAA,GAAW,CAAA,CAAA,EAAI,SAAA,CAAU,MAAA,CAAO,GAAG,CAAA,CAAA,EAAI,SAAA,CAAU,MAAA,CAAO,OAAO,CAAA,MAAA,EAAS,SAAA,CAAU,MAAA,CAAO,IAAI,CAAA,CAAA;AAAA,MAC/F;AAAA,IACF,CAAA,MAAA,IAAW,QAAA,CAAS,QAAA,CAAS,QAAQ,CAAA,EAAG;AAGtC,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,QAAA,CAAS,OAAA,CAAQ,+BAA+B,EAAE,CAAA;AAGxE,MAAA,MAAM,UAAUA,kBAAA,CAAM,aAAA,EAAe,EAAE,GAAA,EAAK,OAAO,CAAA;AACnD,MAAA,MAAM,SAAA,GAAY,QAAQ,SAAS,CAAA;AAEnC,MAAA,IAAI,SAAA,EAAW;AAEb,QAAA,QAAA,GAAW,SAAA;AAAA,MACb;AAAA,IACF,CAAA,MAAO;AAEL,MAAA,MAAM,UAAUA,kBAAA,CAAM,aAAA,EAAe,EAAE,GAAA,EAAK,OAAO,CAAA;AACnD,MAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACtC,MAAA,IAAI,SAAA,EAAW;AACb,QAAA,QAAA,GAAW,IAAI,SAAA,CAAU,MAAA,CAAO,GAAG,CAAA,CAAA,EAAI,SAAA,CAAU,OAAO,IAAI,CAAA,CAAA;AAAA,MAC9D;AAAA,IACF;AAEA,IAAA,OAAO,QAAA,GAAW,GAAG,GAAA,CAAI,QAAQ,KAAK,QAAQ,CAAA,EAAG,QAAQ,CAAA,CAAA,GAAK,IAAA;AAAA,EAChE,SAAS,KAAA,EAAO;AACd,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;;"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
2
 
3
3
  /**
4
+ * Catalog backend module for Apiiro entity annotation processing.
5
+ *
6
+ * Automatically adds Apiiro annotations to Component and System entities
7
+ * based on their source location or name, enabling integration with Apiiro security insights.
8
+ *
4
9
  * @public
5
10
  */
6
11
  declare const catalogModuleApiiroEntityProcessor: _backstage_backend_plugin_api.BackendFeature;
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var backendPluginApi = require('@backstage/backend-plugin-api');
4
- var alpha = require('@backstage/plugin-catalog-node/alpha');
4
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
5
+ var catalogClient = require('@backstage/catalog-client');
5
6
  var ApiiroAnnotationProcessor = require('./processor/ApiiroAnnotationProcessor.cjs.js');
6
7
 
7
8
  const catalogModuleApiiroEntityProcessor = backendPluginApi.createBackendModule({
@@ -10,11 +11,17 @@ const catalogModuleApiiroEntityProcessor = backendPluginApi.createBackendModule(
10
11
  register(reg) {
11
12
  reg.registerInit({
12
13
  deps: {
13
- catalog: alpha.catalogProcessingExtensionPoint,
14
- config: backendPluginApi.coreServices.rootConfig
14
+ catalog: pluginCatalogNode.catalogProcessingExtensionPoint,
15
+ config: backendPluginApi.coreServices.rootConfig,
16
+ discovery: backendPluginApi.coreServices.discovery,
17
+ auth: backendPluginApi.coreServices.auth,
18
+ cache: backendPluginApi.coreServices.cache
15
19
  },
16
- async init({ catalog, config }) {
17
- catalog.addProcessor(new ApiiroAnnotationProcessor.ApiiroAnnotationProcessor(config));
20
+ async init({ catalog, config, discovery, auth, cache }) {
21
+ const catalogApi = new catalogClient.CatalogClient({ discoveryApi: discovery });
22
+ catalog.addProcessor(
23
+ new ApiiroAnnotationProcessor.ApiiroAnnotationProcessor(config, { catalogApi, auth, cache })
24
+ );
18
25
  }
19
26
  });
20
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"module.cjs.js","sources":["../src/module.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 {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';\nimport { ApiiroAnnotationProcessor } from './processor';\n\n/**\n * @public\n */\nexport const catalogModuleApiiroEntityProcessor = createBackendModule({\n pluginId: 'catalog',\n moduleId: 'apiiro-entity-processor',\n register(reg) {\n reg.registerInit({\n deps: {\n catalog: catalogProcessingExtensionPoint,\n config: coreServices.rootConfig,\n },\n async init({ catalog, config }) {\n catalog.addProcessor(new ApiiroAnnotationProcessor(config));\n },\n });\n },\n});\n"],"names":["createBackendModule","catalogProcessingExtensionPoint","coreServices","ApiiroAnnotationProcessor"],"mappings":";;;;;;AA0BO,MAAM,qCAAqCA,oCAAA,CAAoB;AAAA,EACpE,QAAA,EAAU,SAAA;AAAA,EACV,QAAA,EAAU,yBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,OAAA,EAASC,qCAAA;AAAA,QACT,QAAQC,6BAAA,CAAa;AAAA,OACvB;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,OAAA,EAAS,QAAO,EAAG;AAC9B,QAAA,OAAA,CAAQ,YAAA,CAAa,IAAIC,mDAAA,CAA0B,MAAM,CAAC,CAAA;AAAA,MAC5D;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"module.cjs.js","sources":["../src/module.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 {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { ApiiroAnnotationProcessor } from './processor';\n\n/**\n * Catalog backend module for Apiiro entity annotation processing.\n *\n * Automatically adds Apiiro annotations to Component and System entities\n * based on their source location or name, enabling integration with Apiiro security insights.\n *\n * @public\n */\nexport const catalogModuleApiiroEntityProcessor = createBackendModule({\n pluginId: 'catalog',\n moduleId: 'apiiro-entity-processor',\n register(reg) {\n reg.registerInit({\n deps: {\n catalog: catalogProcessingExtensionPoint,\n config: coreServices.rootConfig,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n cache: coreServices.cache,\n },\n async init({ catalog, config, discovery, auth, cache }) {\n const catalogApi = new CatalogClient({ discoveryApi: discovery });\n catalog.addProcessor(\n new ApiiroAnnotationProcessor(config, { catalogApi, auth, cache }),\n );\n },\n });\n },\n});\n"],"names":["createBackendModule","catalogProcessingExtensionPoint","coreServices","CatalogClient","ApiiroAnnotationProcessor"],"mappings":";;;;;;;AAgCO,MAAM,qCAAqCA,oCAAA,CAAoB;AAAA,EACpE,QAAA,EAAU,SAAA;AAAA,EACV,QAAA,EAAU,yBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,OAAA,EAASC,iDAAA;AAAA,QACT,QAAQC,6BAAA,CAAa,UAAA;AAAA,QACrB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,OAAOA,6BAAA,CAAa;AAAA,OACtB;AAAA,MACA,MAAM,KAAK,EAAE,OAAA,EAAS,QAAQ,SAAA,EAAW,IAAA,EAAM,OAAM,EAAG;AACtD,QAAA,MAAM,aAAa,IAAIC,2BAAA,CAAc,EAAE,YAAA,EAAc,WAAW,CAAA;AAChE,QAAA,OAAA,CAAQ,YAAA;AAAA,UACN,IAAIC,mDAAA,CAA0B,MAAA,EAAQ,EAAE,UAAA,EAAY,IAAA,EAAM,OAAO;AAAA,SACnE;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
@@ -1,287 +1,100 @@
1
1
  'use strict';
2
2
 
3
- var fetch = require('node-fetch');
3
+ var catalogModel = require('@backstage/catalog-model');
4
4
  var pluginApiiroCommon = require('@backstage-community/plugin-apiiro-common');
5
+ var apiClient = require('../helpers/apiClient.cjs.js');
6
+ var cacheManager = require('../helpers/cacheManager.cjs.js');
7
+ var utils = require('../helpers/utils.cjs.js');
8
+ var types = require('../helpers/types.cjs.js');
5
9
 
6
- function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
7
-
8
- var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
9
-
10
- const BACKSTAGE_SOURCE_LOCATION_ANNOTATION = "backstage.io/source-location";
11
10
  class ApiiroAnnotationProcessor {
12
- constructor(config) {
11
+ constructor(config, options) {
13
12
  this.config = config;
13
+ const accessToken = this.config.getOptionalString("apiiro.accessToken");
14
+ this.apiClient = new apiClient.ApiiroApiClient(accessToken);
15
+ this.catalogApi = options?.catalogApi;
16
+ this.auth = options?.auth;
17
+ this.cache = options.cache;
18
+ this.cacheManager = new cacheManager.CacheManager(
19
+ this.apiClient,
20
+ this.cache,
21
+ this.catalogApi,
22
+ this.auth
23
+ );
14
24
  }
15
- static CACHE_TTL_MS = 60 * 60 * 1e3;
16
- // 1 hour
17
- static PAGE_LIMIT = 1e3;
18
- static MAX_PAGES = 1e3;
19
- static DEFAULT_NAMESPACE = "default";
20
- repoCache = {
21
- data: /* @__PURE__ */ new Map(),
22
- lastFetched: 0,
23
- isRefreshing: false
24
- };
25
+ apiClient;
26
+ cacheManager;
27
+ catalogApi;
28
+ auth;
29
+ cache;
25
30
  getProcessorName() {
26
31
  return "ApiiroAnnotationProcessor";
27
32
  }
28
- /**
29
- * Determines if an entity should be processed by this processor.
30
- * Only Component entities are processed.
31
- */
32
33
  shouldProcessEntity(entity) {
33
- return entity.kind === "Component";
34
- }
35
- extractRepoUrlFromSourceLocation(sourceLocation) {
36
- try {
37
- if (!sourceLocation) {
38
- return null;
39
- }
40
- const matches = sourceLocation.match(
41
- /https?:\/\/[a-zA-Z0-9\-.]+\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\/.*)?/g
42
- );
43
- if (!matches) {
44
- return null;
45
- }
46
- const url = new URL(matches[0]);
47
- const hostname = url.host.toLowerCase();
48
- let repoPath = null;
49
- if (hostname === "dev.azure.com") {
50
- const pathMatch = url.pathname.match(
51
- /^\/([^/]+)\/([^/]+)\/_git\/([^/]+)(?:\/.*)?$/
52
- );
53
- if (pathMatch) {
54
- repoPath = `/${pathMatch[1]}/${pathMatch[2]}/_git/${pathMatch[3]}`;
55
- }
56
- } else if (hostname.includes("gitlab")) {
57
- const cleanPath = url.pathname.replace(/\/-\/.*$/, "");
58
- const pathMatch = cleanPath.match(/^(\/[^/]+\/[^/]+(?:\/[^/]+)*)$/);
59
- if (pathMatch) {
60
- repoPath = pathMatch[1];
61
- }
62
- } else {
63
- const pathMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)(?:\/.*)?$/);
64
- if (pathMatch) {
65
- repoPath = `/${pathMatch[1]}/${pathMatch[2]}`;
66
- }
67
- }
68
- if (!repoPath) {
69
- return null;
70
- }
71
- return `${url.protocol}//${hostname}${repoPath}`;
72
- } catch (error) {
73
- return null;
34
+ if (entity.kind === "Component") {
35
+ return true;
74
36
  }
75
- }
76
- /**
77
- * Retrieves the Apiiro access token from configuration.
78
- * @throws {Error} If the access token is not configured
79
- */
80
- getAccessToken() {
81
- const accessToken = this.config.getOptionalString("apiiro.accessToken");
82
- return accessToken;
83
- }
84
- /**
85
- * Fetches a single page of repositories from the Apiiro API.
86
- * @param pageCursor - Optional cursor for pagination
87
- * @returns Page of repositories with next cursor
88
- */
89
- async fetchRepositoriesPage(accessToken, pageCursor) {
90
- const baseUrl = pluginApiiroCommon.APIIRO_DEFAULT_BASE_URL;
91
- const params = new URLSearchParams();
92
- params.append("limit", ApiiroAnnotationProcessor.PAGE_LIMIT.toString());
93
- if (pageCursor) {
94
- params.append("next", pageCursor);
95
- }
96
- const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;
97
- const response = await fetch__default.default(url, {
98
- method: "GET",
99
- headers: {
100
- Authorization: `Bearer ${accessToken}`,
101
- "Content-Type": "application/json"
102
- }
103
- });
104
- if (!response.ok) {
105
- const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;
106
- throw new Error(errorMessage);
37
+ if (entity.kind === "System" && this.isApplicationsViewEnabled()) {
38
+ return true;
107
39
  }
108
- const data = await response.json();
109
- return {
110
- items: data.items || [],
111
- next: data.next || null
112
- };
40
+ return false;
113
41
  }
114
- /**
115
- * Fetches all repositories from the Apiiro API with pagination.
116
- * @returns All repositories with total count
117
- * @throws {Error} If pagination limit is exceeded
118
- */
119
- async fetchAllRepositories() {
120
- const items = [];
121
- let nextCursor = void 0;
122
- let pageCount = 0;
123
- do {
124
- pageCount++;
125
- if (pageCount > ApiiroAnnotationProcessor.MAX_PAGES) {
126
- throw new Error(
127
- `Pagination limit exceeded: Maximum of ${ApiiroAnnotationProcessor.MAX_PAGES} pages allowed. This may indicate an infinite loop or an unexpectedly large dataset. Fetched ${items.length} repositories so far.`
128
- );
129
- }
130
- const accessToken = this.getAccessToken();
131
- if (!accessToken) {
132
- console.warn(
133
- "[ApiiroAnnotationProcessor] Apiiro access token not configured. Please set apiiro.accessToken in your app-config."
134
- );
135
- return {
136
- items: [],
137
- totalCount: 0
138
- };
139
- }
140
- const page = await this.fetchRepositoriesPage(
141
- accessToken,
142
- nextCursor ?? void 0
143
- );
144
- items.push(...page.items);
145
- nextCursor = page.next;
146
- } while (nextCursor);
147
- return { items, totalCount: items.length };
42
+ isApplicationsViewEnabled() {
43
+ return this.config.getOptionalBoolean("apiiro.enableApplicationsView") ?? false;
148
44
  }
149
- /**
150
- * Optimized method that combines default branch selection and map building in a single loop.
151
- * Groups repositories by URL, selects the best branch for each, and builds the URL->key mapping.
152
- * @param repositories Array of all repository items
153
- * @returns Map of repository URLs to repository keys
154
- */
155
- buildRepositoryMapWithDefaultBranches(repositories) {
156
- const repoMap = /* @__PURE__ */ new Map();
157
- for (const repo of repositories) {
158
- if (!repo.url) {
159
- continue;
160
- }
161
- if (repoMap.has(repo.url)) {
162
- if (repo.isDefaultBranch) {
163
- repoMap.set(repo.url, {
164
- key: repo.key,
165
- isDefaultBranch: repo.isDefaultBranch
166
- });
167
- }
168
- continue;
169
- } else {
170
- repoMap.set(repo.url, {
171
- key: repo.key,
172
- isDefaultBranch: repo.isDefaultBranch
173
- });
174
- }
175
- }
176
- return repoMap;
177
- }
178
- /**
179
- * Fetches all repositories and builds a name->key mapping.
180
- * Automatically filters to default branches only.
181
- * @returns Map of repository names to repository keys
182
- */
183
- async fetchAllRepos() {
184
- try {
185
- const { items } = await this.fetchAllRepositories();
186
- return this.buildRepositoryMapWithDefaultBranches(items);
187
- } catch (error) {
188
- const errorMessage = error instanceof Error ? error.message : String(error);
189
- console.error(
190
- `[ApiiroAnnotationProcessor] Error fetching repositories from Apiiro API: ${errorMessage}`
191
- );
192
- return /* @__PURE__ */ new Map();
193
- }
194
- }
195
- /**
196
- * Checks if the cache is expired.
197
- */
198
- isCacheExpired() {
199
- const now = Date.now();
200
- return now - this.repoCache.lastFetched > ApiiroAnnotationProcessor.CACHE_TTL_MS || this.repoCache.data.size === 0;
201
- }
202
- /**
203
- * Refreshes the repository cache from the Apiiro API.
204
- * Prevents concurrent refreshes using a flag.
205
- */
206
- async refreshCacheIfNeeded() {
207
- if (!this.isCacheExpired() || this.repoCache.isRefreshing) {
208
- return;
209
- }
210
- this.repoCache.isRefreshing = true;
211
- try {
212
- const newData = await this.fetchAllRepos();
213
- this.repoCache.data = newData;
214
- this.repoCache.lastFetched = Date.now();
215
- } finally {
216
- this.repoCache.isRefreshing = false;
217
- }
218
- }
219
- /**
220
- * Retrieves the repository key for a given entity name.
221
- * Uses a cached map that refreshes automatically when expired.
222
- * @param entityName - The name of the entity/repository
223
- * @returns Repository key or null if not found
224
- */
225
- async getRepoKey(repoUrl) {
226
- await this.refreshCacheIfNeeded();
227
- return this.repoCache.data.get(repoUrl)?.key || null;
228
- }
229
- /**
230
- * Creates an entity reference string in the format: kind:namespace/name
231
- */
232
- createEntityReference(entity) {
233
- const kind = entity.kind?.toLowerCase() || "component";
234
- const namespace = entity.metadata.namespace || ApiiroAnnotationProcessor.DEFAULT_NAMESPACE;
235
- const name = entity.metadata.name;
236
- return `${kind}:${namespace}/${name}`;
237
- }
238
- /**
239
- * Determines if an entity should have the metrics view annotation.
240
- * Based on permission control configuration (exclude/include list).
241
- */
242
45
  shouldAllowMetricsView(entity) {
243
46
  const exclude = this.config.getOptionalBoolean("apiiro.annotationControl.exclude") ?? true;
244
- const entityNames = this.config.getOptionalStringArray(
245
- "apiiro.annotationControl.entityNames"
246
- ) ?? [];
47
+ const entityNames = this.config.getOptionalStringArray("apiiro.annotationControl.entityNames")?.map((name) => name.toLowerCase()) ?? [];
247
48
  if (entityNames.length === 0) {
248
49
  return this.config.getOptionalBoolean("apiiro.defaultAllowMetricsView") ?? true;
249
50
  }
250
- const entityRef = this.createEntityReference(entity);
51
+ const entityRef = catalogModel.stringifyEntityRef(entity);
251
52
  const isInList = entityNames.includes(entityRef);
252
53
  return exclude ? !isInList : isInList;
253
54
  }
254
- /**
255
- * Adds Apiiro annotations to an entity if they don't already exist.
256
- */
257
- addApiiroAnnotations(entity, repoKey, allowMetricsView) {
55
+ addApiiroAnnotations(entity, repoKey, allowMetricsView, applicationId) {
258
56
  const annotations = {
259
57
  ...entity.metadata?.annotations
260
58
  };
261
59
  if (repoKey && !Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_PROJECT_ANNOTATION)) {
262
60
  annotations[pluginApiiroCommon.APIIRO_PROJECT_ANNOTATION] = repoKey;
263
61
  }
264
- if ((repoKey || Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_PROJECT_ANNOTATION)) && !Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_METRICS_VIEW_ANNOTATION)) {
62
+ if (applicationId && !Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_APPLICATION_ANNOTATION)) {
63
+ annotations[pluginApiiroCommon.APIIRO_APPLICATION_ANNOTATION] = applicationId;
64
+ }
65
+ if ((repoKey || Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_PROJECT_ANNOTATION) || applicationId || Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_APPLICATION_ANNOTATION)) && !Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_METRICS_VIEW_ANNOTATION)) {
265
66
  annotations[pluginApiiroCommon.APIIRO_METRICS_VIEW_ANNOTATION] = allowMetricsView ? "true" : "false";
266
67
  }
267
68
  return annotations;
268
69
  }
269
- /**
270
- * Preprocesses an entity to add Apiiro-specific annotations.
271
- * Only processes Component entities.
272
- */
273
70
  async preProcessEntity(entity, _location, _emit, _originLocation, _cache) {
274
71
  if (!this.shouldProcessEntity(entity)) {
275
72
  return entity;
276
73
  }
74
+ let repoKey = null;
75
+ let applicationId = null;
76
+ if (entity.kind === "Component") {
77
+ const sourceLocation = entity.metadata.annotations?.[types.BACKSTAGE_SOURCE_LOCATION_ANNOTATION];
78
+ const repoUrl = utils.extractRepoUrlFromSourceLocation(sourceLocation);
79
+ repoKey = repoUrl ? await this.cacheManager.getRepoKey(repoUrl) : null;
80
+ }
81
+ if (entity.kind === "System" && this.isApplicationsViewEnabled()) {
82
+ const entityRef = catalogModel.stringifyEntityRef(entity);
83
+ let entityUid = await this.cacheManager.getEntityUid(entityRef);
84
+ if (!entityUid) {
85
+ await this.cacheManager.invalidateEntityRefCache();
86
+ entityUid = await this.cacheManager.getEntityUid(entityRef);
87
+ }
88
+ if (entityUid) {
89
+ applicationId = await this.cacheManager.getApplicationId(entityUid);
90
+ }
91
+ }
277
92
  const allowMetricsView = this.shouldAllowMetricsView(entity);
278
- const sourceLocation = entity.metadata.annotations?.[BACKSTAGE_SOURCE_LOCATION_ANNOTATION];
279
- const repoUrl = this.extractRepoUrlFromSourceLocation(sourceLocation);
280
- const repoKey = repoUrl ? await this.getRepoKey(repoUrl) : null;
281
93
  const annotations = this.addApiiroAnnotations(
282
94
  entity,
283
95
  repoKey,
284
- allowMetricsView
96
+ allowMetricsView,
97
+ applicationId
285
98
  );
286
99
  return {
287
100
  ...entity,
@@ -1 +1 @@
1
- {"version":3,"file":"ApiiroAnnotationProcessor.cjs.js","sources":["../../src/processor/ApiiroAnnotationProcessor.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 {\n CatalogProcessor,\n CatalogProcessorEmit,\n CatalogProcessorCache,\n} from '@backstage/plugin-catalog-node';\nimport type { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { Entity } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport fetch from 'node-fetch';\nimport {\n APIIRO_METRICS_VIEW_ANNOTATION,\n APIIRO_PROJECT_ANNOTATION,\n APIIRO_DEFAULT_BASE_URL,\n} from '@backstage-community/plugin-apiiro-common';\n\ninterface RepositoryItem {\n name?: string | null;\n key?: string | null;\n url?: string | null;\n isDefaultBranch?: boolean;\n branchName?: string | null;\n [key: string]: unknown;\n}\n\ninterface ApiiroRepositoriesResponse {\n items: RepositoryItem[];\n next?: string | null;\n}\n\ninterface RepoCache {\n data: Map<string, { key: string; isDefaultBranch: boolean }>;\n lastFetched: number;\n isRefreshing: boolean;\n}\n\nconst BACKSTAGE_SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location';\n\nexport class ApiiroAnnotationProcessor implements CatalogProcessor {\n private static readonly CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\n private static readonly PAGE_LIMIT = 1000;\n private static readonly MAX_PAGES = 1000;\n private static readonly DEFAULT_NAMESPACE = 'default';\n\n private repoCache: RepoCache = {\n data: new Map(),\n lastFetched: 0,\n isRefreshing: false,\n };\n\n constructor(private readonly config: Config) {}\n\n getProcessorName(): string {\n return 'ApiiroAnnotationProcessor';\n }\n\n /**\n * Determines if an entity should be processed by this processor.\n * Only Component entities are processed.\n */\n private shouldProcessEntity(entity: Entity): boolean {\n return entity.kind === 'Component';\n }\n\n private extractRepoUrlFromSourceLocation(\n sourceLocation?: string,\n ): string | null {\n try {\n if (!sourceLocation) {\n return null;\n }\n\n const matches = sourceLocation.match(\n /https?:\\/\\/[a-zA-Z0-9\\-.]+\\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\\/.*)?/g,\n );\n\n if (!matches) {\n return null;\n }\n\n const url = new URL(matches[0]);\n const hostname = url.host.toLowerCase();\n\n let repoPath: string | null = null;\n\n if (hostname === 'dev.azure.com') {\n const pathMatch = url.pathname.match(\n /^\\/([^/]+)\\/([^/]+)\\/_git\\/([^/]+)(?:\\/.*)?$/,\n );\n if (pathMatch) {\n repoPath = `/${pathMatch[1]}/${pathMatch[2]}/_git/${pathMatch[3]}`;\n }\n } else if (hostname.includes('gitlab')) {\n // GitLab can have nested subgroups: /group/subgroup1/subgroup2/.../project\n // Remove /-/... suffix first (GitLab file/blob paths), then extract repo path\n const cleanPath = url.pathname.replace(/\\/-\\/.*$/, '');\n const pathMatch = cleanPath.match(/^(\\/[^/]+\\/[^/]+(?:\\/[^/]+)*)$/);\n if (pathMatch) {\n repoPath = pathMatch[1];\n }\n } else {\n // GitHub and other providers: /org/repo\n const pathMatch = url.pathname.match(/^\\/([^/]+)\\/([^/]+)(?:\\/.*)?$/);\n if (pathMatch) {\n repoPath = `/${pathMatch[1]}/${pathMatch[2]}`;\n }\n }\n\n if (!repoPath) {\n return null;\n }\n\n return `${url.protocol}//${hostname}${repoPath}`;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Retrieves the Apiiro access token from configuration.\n * @throws {Error} If the access token is not configured\n */\n private getAccessToken(): string | undefined {\n const accessToken = this.config.getOptionalString('apiiro.accessToken');\n return accessToken;\n }\n\n /**\n * Fetches a single page of repositories from the Apiiro API.\n * @param pageCursor - Optional cursor for pagination\n * @returns Page of repositories with next cursor\n */\n private async fetchRepositoriesPage(\n accessToken: string,\n pageCursor?: string,\n ): Promise<ApiiroRepositoriesResponse> {\n const baseUrl = APIIRO_DEFAULT_BASE_URL;\n\n const params = new URLSearchParams();\n params.append('limit', ApiiroAnnotationProcessor.PAGE_LIMIT.toString());\n if (pageCursor) {\n params.append('next', pageCursor);\n }\n\n const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;\n throw new Error(errorMessage);\n }\n\n const data = (await response.json()) as ApiiroRepositoriesResponse;\n return {\n items: data.items || [],\n next: data.next || null,\n };\n }\n\n /**\n * Fetches all repositories from the Apiiro API with pagination.\n * @returns All repositories with total count\n * @throws {Error} If pagination limit is exceeded\n */\n private async fetchAllRepositories(): Promise<{\n items: RepositoryItem[];\n totalCount: number;\n }> {\n const items: RepositoryItem[] = [];\n let nextCursor: string | null | undefined = undefined;\n let pageCount = 0;\n\n do {\n pageCount++;\n\n if (pageCount > ApiiroAnnotationProcessor.MAX_PAGES) {\n throw new Error(\n `Pagination limit exceeded: Maximum of ${ApiiroAnnotationProcessor.MAX_PAGES} pages allowed. ` +\n `This may indicate an infinite loop or an unexpectedly large dataset. ` +\n `Fetched ${items.length} repositories so far.`,\n );\n }\n\n const accessToken = this.getAccessToken();\n if (!accessToken) {\n console.warn(\n '[ApiiroAnnotationProcessor] Apiiro access token not configured. Please set apiiro.accessToken in your app-config.',\n );\n return {\n items: [],\n totalCount: 0,\n };\n }\n const page = await this.fetchRepositoriesPage(\n accessToken,\n nextCursor ?? undefined,\n );\n items.push(...page.items);\n nextCursor = page.next;\n } while (nextCursor);\n\n return { items, totalCount: items.length };\n }\n\n /**\n * Optimized method that combines default branch selection and map building in a single loop.\n * Groups repositories by URL, selects the best branch for each, and builds the URL->key mapping.\n * @param repositories Array of all repository items\n * @returns Map of repository URLs to repository keys\n */\n private buildRepositoryMapWithDefaultBranches(\n repositories: RepositoryItem[],\n ): Map<string, { key: string; isDefaultBranch: boolean }> {\n const repoMap = new Map<\n string,\n { key: string; isDefaultBranch: boolean }\n >();\n\n // Single loop: Group repositories by URL\n for (const repo of repositories) {\n if (!repo.url) {\n continue; // Skip items without URL\n }\n if (repoMap.has(repo.url)) {\n if (repo.isDefaultBranch) {\n repoMap.set(repo.url, {\n key: repo.key!,\n isDefaultBranch: repo.isDefaultBranch!,\n });\n }\n continue;\n } else {\n repoMap.set(repo.url, {\n key: repo.key!,\n isDefaultBranch: repo.isDefaultBranch!,\n });\n }\n }\n\n return repoMap;\n }\n\n /**\n * Fetches all repositories and builds a name->key mapping.\n * Automatically filters to default branches only.\n * @returns Map of repository names to repository keys\n */\n private async fetchAllRepos(): Promise<\n Map<string, { key: string; isDefaultBranch: boolean }>\n > {\n try {\n // Fetch all repositories with pagination\n const { items } = await this.fetchAllRepositories();\n\n // Combine default branch selection and map building in a single operation\n return this.buildRepositoryMapWithDefaultBranches(items);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[ApiiroAnnotationProcessor] Error fetching repositories from Apiiro API: ${errorMessage}`,\n );\n return new Map();\n }\n }\n\n /**\n * Checks if the cache is expired.\n */\n private isCacheExpired(): boolean {\n const now = Date.now();\n return (\n now - this.repoCache.lastFetched >\n ApiiroAnnotationProcessor.CACHE_TTL_MS || this.repoCache.data.size === 0\n );\n }\n\n /**\n * Refreshes the repository cache from the Apiiro API.\n * Prevents concurrent refreshes using a flag.\n */\n private async refreshCacheIfNeeded(): Promise<void> {\n if (!this.isCacheExpired() || this.repoCache.isRefreshing) {\n return;\n }\n\n this.repoCache.isRefreshing = true;\n\n try {\n const newData = await this.fetchAllRepos();\n this.repoCache.data = newData;\n this.repoCache.lastFetched = Date.now();\n } finally {\n this.repoCache.isRefreshing = false;\n }\n }\n\n /**\n * Retrieves the repository key for a given entity name.\n * Uses a cached map that refreshes automatically when expired.\n * @param entityName - The name of the entity/repository\n * @returns Repository key or null if not found\n */\n private async getRepoKey(repoUrl: string): Promise<string | null> {\n await this.refreshCacheIfNeeded();\n return this.repoCache.data.get(repoUrl)?.key || null;\n }\n\n /**\n * Creates an entity reference string in the format: kind:namespace/name\n */\n private createEntityReference(entity: Entity): string {\n const kind = entity.kind?.toLowerCase() || 'component';\n const namespace =\n entity.metadata.namespace || ApiiroAnnotationProcessor.DEFAULT_NAMESPACE;\n const name = entity.metadata.name;\n return `${kind}:${namespace}/${name}`;\n }\n\n /**\n * Determines if an entity should have the metrics view annotation.\n * Based on permission control configuration (exclude/include list).\n */\n private shouldAllowMetricsView(entity: Entity): boolean {\n const exclude =\n this.config.getOptionalBoolean('apiiro.annotationControl.exclude') ??\n true;\n const entityNames =\n this.config.getOptionalStringArray(\n 'apiiro.annotationControl.entityNames',\n ) ?? [];\n\n if (entityNames.length === 0) {\n return (\n this.config.getOptionalBoolean('apiiro.defaultAllowMetricsView') ?? true\n );\n }\n\n const entityRef = this.createEntityReference(entity);\n const isInList = entityNames.includes(entityRef);\n\n // If exclude=true: allow all except those in list\n // If exclude=false: allow only those in list\n return exclude ? !isInList : isInList;\n }\n\n /**\n * Adds Apiiro annotations to an entity if they don't already exist.\n */\n private addApiiroAnnotations(\n entity: Entity,\n repoKey: string | null,\n allowMetricsView: boolean,\n ): Record<string, string> {\n const annotations: Record<string, string> = {\n ...entity.metadata?.annotations,\n };\n\n // Add project annotation if repo key exists and annotation not already set\n if (\n repoKey &&\n !Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION)\n ) {\n annotations[APIIRO_PROJECT_ANNOTATION] = repoKey;\n }\n\n // Add metrics view annotation if allowed and not already set\n if (\n (repoKey ||\n Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION)) &&\n !Object.keys(annotations).includes(APIIRO_METRICS_VIEW_ANNOTATION)\n ) {\n annotations[APIIRO_METRICS_VIEW_ANNOTATION] = allowMetricsView\n ? 'true'\n : 'false';\n }\n\n return annotations;\n }\n\n /**\n * Preprocesses an entity to add Apiiro-specific annotations.\n * Only processes Component entities.\n */\n async preProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n _emit: CatalogProcessorEmit,\n _originLocation: LocationSpec,\n _cache: CatalogProcessorCache,\n ): Promise<Entity> {\n if (!this.shouldProcessEntity(entity)) {\n return entity;\n }\n\n // Determine if metrics view should be allowed\n const allowMetricsView = this.shouldAllowMetricsView(entity);\n\n const sourceLocation =\n entity.metadata.annotations?.[BACKSTAGE_SOURCE_LOCATION_ANNOTATION];\n const repoUrl = this.extractRepoUrlFromSourceLocation(sourceLocation);\n\n // Get repository key from cache (refreshes automatically if needed)\n const repoKey = repoUrl ? await this.getRepoKey(repoUrl) : null;\n\n // Add Apiiro annotations\n const annotations = this.addApiiroAnnotations(\n entity,\n repoKey,\n allowMetricsView,\n );\n\n return {\n ...entity,\n metadata: {\n ...entity.metadata,\n annotations,\n },\n };\n }\n}\n"],"names":["APIIRO_DEFAULT_BASE_URL","fetch","APIIRO_PROJECT_ANNOTATION","APIIRO_METRICS_VIEW_ANNOTATION"],"mappings":"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"}
1
+ {"version":3,"file":"ApiiroAnnotationProcessor.cjs.js","sources":["../../src/processor/ApiiroAnnotationProcessor.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 {\n CatalogProcessor,\n CatalogProcessorEmit,\n CatalogProcessorCache,\n} from '@backstage/plugin-catalog-node';\nimport type { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { CatalogApi } from '@backstage/catalog-client';\nimport { Entity, stringifyEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { AuthService, CacheService } from '@backstage/backend-plugin-api';\nimport {\n APIIRO_METRICS_VIEW_ANNOTATION,\n APIIRO_PROJECT_ANNOTATION,\n APIIRO_APPLICATION_ANNOTATION,\n} from '@backstage-community/plugin-apiiro-common';\nimport { ApiiroApiClient } from '../helpers/apiClient';\nimport { CacheManager } from '../helpers/cacheManager';\nimport { extractRepoUrlFromSourceLocation } from '../helpers/utils';\nimport { BACKSTAGE_SOURCE_LOCATION_ANNOTATION } from '../helpers/types';\n\nexport class ApiiroAnnotationProcessor implements CatalogProcessor {\n private readonly apiClient: ApiiroApiClient;\n private readonly cacheManager: CacheManager;\n private readonly catalogApi?: CatalogApi;\n private readonly auth?: AuthService;\n private readonly cache: CacheService;\n\n constructor(\n private readonly config: Config,\n options: {\n catalogApi?: CatalogApi;\n auth?: AuthService;\n cache: CacheService;\n },\n ) {\n const accessToken = this.config.getOptionalString('apiiro.accessToken');\n this.apiClient = new ApiiroApiClient(accessToken);\n this.catalogApi = options?.catalogApi;\n this.auth = options?.auth;\n this.cache = options.cache;\n this.cacheManager = new CacheManager(\n this.apiClient,\n this.cache,\n this.catalogApi,\n this.auth,\n );\n }\n\n getProcessorName(): string {\n return 'ApiiroAnnotationProcessor';\n }\n\n private shouldProcessEntity(entity: Entity): boolean {\n if (entity.kind === 'Component') {\n return true;\n }\n if (entity.kind === 'System' && this.isApplicationsViewEnabled()) {\n return true;\n }\n return false;\n }\n\n private isApplicationsViewEnabled(): boolean {\n return (\n this.config.getOptionalBoolean('apiiro.enableApplicationsView') ?? false\n );\n }\n\n private shouldAllowMetricsView(entity: Entity): boolean {\n const exclude =\n this.config.getOptionalBoolean('apiiro.annotationControl.exclude') ??\n true;\n const entityNames =\n this.config\n .getOptionalStringArray('apiiro.annotationControl.entityNames')\n ?.map(name => name.toLowerCase()) ?? [];\n\n if (entityNames.length === 0) {\n return (\n this.config.getOptionalBoolean('apiiro.defaultAllowMetricsView') ?? true\n );\n }\n\n const entityRef = stringifyEntityRef(entity);\n const isInList = entityNames.includes(entityRef);\n\n return exclude ? !isInList : isInList;\n }\n\n private addApiiroAnnotations(\n entity: Entity,\n repoKey: string | null,\n allowMetricsView: boolean,\n applicationId: string | null,\n ): Record<string, string> {\n const annotations: Record<string, string> = {\n ...entity.metadata?.annotations,\n };\n\n if (\n repoKey &&\n !Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION)\n ) {\n annotations[APIIRO_PROJECT_ANNOTATION] = repoKey;\n }\n\n if (\n applicationId &&\n !Object.keys(annotations).includes(APIIRO_APPLICATION_ANNOTATION)\n ) {\n annotations[APIIRO_APPLICATION_ANNOTATION] = applicationId;\n }\n\n if (\n (repoKey ||\n Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION) ||\n applicationId ||\n Object.keys(annotations).includes(APIIRO_APPLICATION_ANNOTATION)) &&\n !Object.keys(annotations).includes(APIIRO_METRICS_VIEW_ANNOTATION)\n ) {\n annotations[APIIRO_METRICS_VIEW_ANNOTATION] = allowMetricsView\n ? 'true'\n : 'false';\n }\n\n return annotations;\n }\n\n async preProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n _emit: CatalogProcessorEmit,\n _originLocation: LocationSpec,\n _cache: CatalogProcessorCache,\n ): Promise<Entity> {\n if (!this.shouldProcessEntity(entity)) {\n return entity;\n }\n\n let repoKey: string | null = null;\n let applicationId: string | null = null;\n\n if (entity.kind === 'Component') {\n const sourceLocation =\n entity.metadata.annotations?.[BACKSTAGE_SOURCE_LOCATION_ANNOTATION];\n const repoUrl = extractRepoUrlFromSourceLocation(sourceLocation);\n repoKey = repoUrl ? await this.cacheManager.getRepoKey(repoUrl) : null;\n }\n\n if (entity.kind === 'System' && this.isApplicationsViewEnabled()) {\n const entityRef = stringifyEntityRef(entity);\n let entityUid = await this.cacheManager.getEntityUid(entityRef);\n\n if (!entityUid) {\n await this.cacheManager.invalidateEntityRefCache();\n entityUid = await this.cacheManager.getEntityUid(entityRef);\n }\n\n if (entityUid) {\n applicationId = await this.cacheManager.getApplicationId(entityUid);\n }\n }\n\n const allowMetricsView = this.shouldAllowMetricsView(entity);\n\n const annotations = this.addApiiroAnnotations(\n entity,\n repoKey,\n allowMetricsView,\n applicationId,\n );\n\n return {\n ...entity,\n metadata: {\n ...entity.metadata,\n annotations,\n },\n };\n }\n}\n"],"names":["ApiiroApiClient","CacheManager","stringifyEntityRef","APIIRO_PROJECT_ANNOTATION","APIIRO_APPLICATION_ANNOTATION","APIIRO_METRICS_VIEW_ANNOTATION","BACKSTAGE_SOURCE_LOCATION_ANNOTATION","extractRepoUrlFromSourceLocation"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage-community/plugin-catalog-backend-module-apiiro-entity-processor",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The apiiro-entity-processor backend module for the catalog plugin.",
6
6
  "main": "dist/index.cjs.js",
@@ -33,16 +33,18 @@
33
33
  "postpack": "backstage-cli package postpack"
34
34
  },
35
35
  "dependencies": {
36
- "@backstage-community/plugin-apiiro-common": "^0.1.0",
37
- "@backstage/backend-plugin-api": "^1.4.4",
38
- "@backstage/catalog-model": "^1.7.5",
39
- "@backstage/config": "^1.3.5",
40
- "@backstage/plugin-catalog-common": "^1.1.6",
41
- "@backstage/plugin-catalog-node": "^1.19.1",
42
- "node-fetch": "^2.6.7"
36
+ "@backstage-community/plugin-apiiro-common": "^1.0.0",
37
+ "@backstage/backend-plugin-api": "^1.7.0",
38
+ "@backstage/catalog-client": "^1.13.0",
39
+ "@backstage/catalog-model": "^1.7.6",
40
+ "@backstage/config": "^1.3.6",
41
+ "@backstage/plugin-catalog-common": "^1.1.8",
42
+ "@backstage/plugin-catalog-node": "^2.0.0",
43
+ "node-fetch": "^2.6.7",
44
+ "path-to-regexp": "^8.0.0"
43
45
  },
44
46
  "devDependencies": {
45
- "@backstage/cli": "^0.34.4",
47
+ "@backstage/cli": "^0.35.4",
46
48
  "@types/node-fetch": "^2.6.9"
47
49
  },
48
50
  "files": [