@nospt/backstage-plugin-apigee-backend 0.1.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.
@@ -0,0 +1,371 @@
1
+ 'use strict';
2
+
3
+ var entityBuilder = require('./entity-builder.cjs.js');
4
+
5
+ const SPEC_FETCH_CONCURRENCY = 8;
6
+ class ApigeeEntityProvider {
7
+ constructor(taskRunner, logger, orgsConfig, apigeeClient, apiHubClient, sharedflowClient, options) {
8
+ this.taskRunner = taskRunner;
9
+ this.logger = logger;
10
+ this.orgsConfig = orgsConfig;
11
+ this.apigeeClient = apigeeClient;
12
+ this.apiHubClient = apiHubClient;
13
+ this.sharedflowClient = sharedflowClient;
14
+ this.options = options;
15
+ }
16
+ taskRunner;
17
+ logger;
18
+ orgsConfig;
19
+ apigeeClient;
20
+ apiHubClient;
21
+ sharedflowClient;
22
+ options;
23
+ connection;
24
+ /** Stable provider name — MUST NOT change; it links entities to this provider. */
25
+ getProviderName() {
26
+ return "apigee-catalog-provider";
27
+ }
28
+ /**
29
+ * Called once by the catalog on startup. Stores the connection and registers
30
+ * the recurring sync task with the scheduler.
31
+ */
32
+ async connect(connection) {
33
+ if (this.connection) {
34
+ throw new Error("ApigeeEntityProvider is already connected");
35
+ }
36
+ this.connection = connection;
37
+ await this.taskRunner.run({
38
+ id: "catalog-backend-module-apigee:sync",
39
+ fn: async () => {
40
+ await this.run();
41
+ }
42
+ });
43
+ }
44
+ /**
45
+ * Executes one full catalog sync:
46
+ * 1. Fetch proxy / sharedflow / API-Hub data for all orgs in parallel.
47
+ * 2. Warn and skip any org that fails (error isolation per org).
48
+ * 3. Deduplicate proxies shared across multiple orgs (D-03).
49
+ * 4. Build entities and call applyMutation exactly once.
50
+ */
51
+ async run() {
52
+ if (!this.connection) {
53
+ throw new Error("ApigeeEntityProvider not initialized");
54
+ }
55
+ const orgResults = await Promise.allSettled(
56
+ this.orgsConfig.map((orgCfg) => this.fetchOrgData(orgCfg))
57
+ );
58
+ const orgDataList = [];
59
+ for (let i = 0; i < orgResults.length; i++) {
60
+ const result = orgResults[i];
61
+ if (result.status === "rejected") {
62
+ this.logger.warn(
63
+ `[catalog-module-apigee] Org '${this.orgsConfig[i].name}' sync failed: ${result.reason}`
64
+ );
65
+ } else {
66
+ orgDataList.push(result.value);
67
+ }
68
+ }
69
+ const mergedProxyMap = /* @__PURE__ */ new Map();
70
+ for (const orgData of orgDataList) {
71
+ for (const [slug, partial] of orgData.proxyMap) {
72
+ const existing = mergedProxyMap.get(slug);
73
+ if (existing) {
74
+ existing.orgs.push(...partial.orgs);
75
+ for (const bp of partial.basePaths) {
76
+ if (!existing.basePaths.includes(bp)) {
77
+ existing.basePaths.push(bp);
78
+ }
79
+ }
80
+ } else {
81
+ mergedProxyMap.set(slug, {
82
+ proxy: partial.proxy,
83
+ basePaths: [...partial.basePaths],
84
+ orgs: [...partial.orgs]
85
+ });
86
+ }
87
+ }
88
+ }
89
+ const entities = [];
90
+ const apiHubById = /* @__PURE__ */ new Map();
91
+ const mappingOverrides = /* @__PURE__ */ new Map();
92
+ for (const orgData of orgDataList) {
93
+ for (const hubApi of orgData.apiHubApis) {
94
+ const id = entityBuilder.slugify(
95
+ hubApi.displayName ?? hubApi.name.split("/").pop() ?? ""
96
+ );
97
+ if (id && !apiHubById.has(id)) {
98
+ apiHubById.set(id, { api: hubApi, orgData });
99
+ }
100
+ }
101
+ if (orgData.apiHubMappings) {
102
+ for (const [proxyName, hubId] of Object.entries(orgData.apiHubMappings)) {
103
+ if (!mappingOverrides.has(proxyName)) {
104
+ mappingOverrides.set(proxyName, hubId);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ const matchedHubIds = /* @__PURE__ */ new Set();
110
+ const matchedItems = [];
111
+ for (const [slug, merged] of mergedProxyMap.entries()) {
112
+ let apiType;
113
+ for (const orgData of orgDataList) {
114
+ const t = orgData.apiTypeMap.get(slug) ?? orgData.apiTypeMap.get(merged.proxy.name.toLowerCase());
115
+ if (t) {
116
+ apiType = t;
117
+ break;
118
+ }
119
+ }
120
+ const [component, api] = entityBuilder.proxyToEntities(merged, apiType, this.options);
121
+ const candidate = (mappingOverrides.get(merged.proxy.name) ?? slug).toLowerCase();
122
+ const match = apiHubById.get(candidate);
123
+ if (match) {
124
+ matchedHubIds.add(candidate);
125
+ matchedItems.push({
126
+ component,
127
+ api,
128
+ hubApi: match.api,
129
+ githubOrgSlug: match.orgData.githubOrgSlug,
130
+ realApiId: match.api.name.split("/").pop() ?? candidate,
131
+ projectId: match.orgData.projectId
132
+ });
133
+ } else {
134
+ entities.push(component, api);
135
+ }
136
+ }
137
+ const matchedSpecs = await this.mapWithConcurrency(
138
+ matchedItems,
139
+ SPEC_FETCH_CONCURRENCY,
140
+ (item) => this.resolveSpecForApi(item.realApiId, item.projectId)
141
+ );
142
+ matchedItems.forEach((item, i) => {
143
+ entities.push(
144
+ item.component,
145
+ entityBuilder.enrichMatchedApiEntity(
146
+ item.api,
147
+ item.hubApi,
148
+ matchedSpecs[i],
149
+ item.githubOrgSlug,
150
+ this.options
151
+ )
152
+ );
153
+ });
154
+ const hubOnlyItems = [];
155
+ for (const [id, { api: hubApi, orgData }] of apiHubById.entries()) {
156
+ if (matchedHubIds.has(id)) continue;
157
+ hubOnlyItems.push({
158
+ hubApi,
159
+ orgData,
160
+ realApiId: hubApi.name.split("/").pop() ?? id
161
+ });
162
+ }
163
+ const hubOnlySpecs = await this.mapWithConcurrency(
164
+ hubOnlyItems,
165
+ SPEC_FETCH_CONCURRENCY,
166
+ (item) => this.resolveSpecForApi(item.realApiId, item.orgData.projectId)
167
+ );
168
+ const hubOnlyNames = /* @__PURE__ */ new Set();
169
+ hubOnlyItems.forEach((item, i) => {
170
+ const entity = entityBuilder.hubOnlyToEntity(
171
+ item.hubApi,
172
+ item.orgData.orgName,
173
+ hubOnlySpecs[i],
174
+ item.orgData.githubOrgSlug,
175
+ this.options
176
+ );
177
+ if (hubOnlyNames.has(entity.metadata.name)) return;
178
+ hubOnlyNames.add(entity.metadata.name);
179
+ entities.push(entity);
180
+ });
181
+ for (const orgData of orgDataList) {
182
+ for (const sf of orgData.sharedflows) {
183
+ entities.push(entityBuilder.sharedflowToEntity(sf, orgData.orgName, this.options));
184
+ }
185
+ }
186
+ await this.connection.applyMutation({
187
+ type: "full",
188
+ entities: entities.map((entity) => ({
189
+ entity,
190
+ locationKey: this.getProviderName()
191
+ }))
192
+ });
193
+ }
194
+ // ── Private helpers ──────────────────────────────────────────────────────
195
+ /**
196
+ * Maps `items` through `fn` with at most `limit` invocations in flight at
197
+ * once, preserving input order in the returned array. Used to parallelise
198
+ * per-API spec resolution without overwhelming the API Hub backend (WR-03).
199
+ */
200
+ async mapWithConcurrency(items, limit, fn) {
201
+ const results = new Array(items.length);
202
+ let next = 0;
203
+ const workerCount = Math.max(1, Math.min(limit, items.length));
204
+ const worker = async () => {
205
+ while (true) {
206
+ const i = next;
207
+ next += 1;
208
+ if (i >= items.length) break;
209
+ results[i] = await fn(items[i]);
210
+ }
211
+ };
212
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
213
+ return results;
214
+ }
215
+ /**
216
+ * Fetches a spec's contents subject to the size guard (D-03).
217
+ *
218
+ * Two-stage guard:
219
+ * 1. Pre-fetch: if the spec metadata reports `sizeBytes > specMaxBytes`, skip
220
+ * the download entirely and return a `specUrl` link. NOTE: the API Hub spec
221
+ * metadata endpoint frequently reports `sizeBytes: 0`/undefined, so this
222
+ * stage often cannot fire and the contents are downloaded anyway.
223
+ * 2. Post-decode: enforce `specMaxBytes` against the actual decoded byte
224
+ * length — this is the guard that reliably bounds the *embedded* size.
225
+ *
226
+ * Because stage 1 is unreliable, `specMaxBytes` bounds the size of the spec
227
+ * that gets embedded, not necessarily the size downloaded into memory.
228
+ */
229
+ async fetchSpecEmbed(apiId, versionId, specId, projectId) {
230
+ const spec = await this.apiHubClient.getSpec(
231
+ apiId,
232
+ versionId,
233
+ specId,
234
+ projectId
235
+ );
236
+ const limit = this.options.specMaxBytes ?? 512e3;
237
+ const size = spec.sizeBytes ?? 0;
238
+ if (size > limit) {
239
+ this.logger.warn(
240
+ `[catalog-module-apigee] Spec '${spec.name}' (${size} bytes) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`
241
+ );
242
+ return { hasSpec: true, specUrl: spec.name };
243
+ }
244
+ const contents = await this.apiHubClient.getSpecContents(
245
+ apiId,
246
+ versionId,
247
+ specId,
248
+ projectId
249
+ );
250
+ const base64 = contents.contents ?? "";
251
+ const definition = Buffer.from(base64, "base64").toString("utf-8");
252
+ const decodedBytes = Buffer.byteLength(definition, "utf-8");
253
+ this.logger.debug(
254
+ `[catalog-module-apigee] Resolved spec '${spec.name}' (mimeType=${contents.mimeType ?? "?"}, ${decodedBytes} bytes decoded, metadata sizeBytes=${spec.sizeBytes ?? "n/a"})`
255
+ );
256
+ if (!base64) {
257
+ this.logger.warn(
258
+ `[catalog-module-apigee] Spec '${spec.name}' returned empty contents; treating as no embeddable spec`
259
+ );
260
+ return { hasSpec: false };
261
+ }
262
+ if (decodedBytes > limit) {
263
+ this.logger.warn(
264
+ `[catalog-module-apigee] Spec '${spec.name}' (${decodedBytes} bytes decoded) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`
265
+ );
266
+ return { hasSpec: true, specUrl: spec.name };
267
+ }
268
+ return { hasSpec: true, definition };
269
+ }
270
+ /**
271
+ * Resolves the spec for an API: lists versions, deterministically selects one
272
+ * (D-02), enumerates that version's specs (the versions endpoint does not
273
+ * reliably populate specIds), picks the lexicographically smallest spec ID,
274
+ * then fetches it with the size guard. Per-spec failures are isolated: any
275
+ * error is logged and `undefined` is returned so one bad spec never aborts
276
+ * the whole sync.
277
+ */
278
+ async resolveSpecForApi(apiId, projectId) {
279
+ try {
280
+ const versions = await this.apiHubClient.listVersions(apiId, projectId);
281
+ const selection = entityBuilder.selectSpec(versions);
282
+ if (!selection) return void 0;
283
+ const specs = await this.apiHubClient.listSpecs(
284
+ apiId,
285
+ selection.versionId,
286
+ projectId
287
+ );
288
+ const specId = specs.map((s) => s.name?.split("/").pop() ?? "").filter(Boolean).sort()[0];
289
+ if (!specId) return void 0;
290
+ return await this.fetchSpecEmbed(
291
+ apiId,
292
+ selection.versionId,
293
+ specId,
294
+ projectId
295
+ );
296
+ } catch (e) {
297
+ this.logger.warn(
298
+ `[catalog-module-apigee] spec fetch failed for ${apiId}: ${e}`
299
+ );
300
+ return void 0;
301
+ }
302
+ }
303
+ /**
304
+ * Fetches all data for one org: proxies, their deployments + revisions,
305
+ * API Hub type map, and sharedflows.
306
+ */
307
+ async fetchOrgData(orgCfg) {
308
+ const [proxies, apiHubApis, sharedflows] = await Promise.all([
309
+ this.apigeeClient.listProxies(orgCfg.name),
310
+ this.apiHubClient.listApis(orgCfg.projectId),
311
+ this.sharedflowClient.listSharedflows(orgCfg.name)
312
+ ]);
313
+ const apiTypeMap = /* @__PURE__ */ new Map();
314
+ for (const api of apiHubApis) {
315
+ const id = entityBuilder.slugify(api.displayName ?? api.name.split("/").pop() ?? "");
316
+ if (id && api.apiType) {
317
+ apiTypeMap.set(id, api.apiType);
318
+ }
319
+ }
320
+ const proxyMap = /* @__PURE__ */ new Map();
321
+ await Promise.all(
322
+ proxies.map(async (proxy) => {
323
+ try {
324
+ const latestRev = proxy.revision?.length ? proxy.revision[proxy.revision.length - 1] : void 0;
325
+ const [deployments, revision] = await Promise.all([
326
+ this.apigeeClient.listDeployments(orgCfg.name, proxy.name),
327
+ latestRev ? this.apigeeClient.getProxyRevision(
328
+ orgCfg.name,
329
+ proxy.name,
330
+ latestRev
331
+ ) : Promise.resolve({ name: proxy.name, basepaths: [] })
332
+ ]);
333
+ const readyDeps = deployments.filter(
334
+ (d) => d.deploymentStatus === "READY"
335
+ );
336
+ const lifecycle = readyDeps.length > 0 ? "production" : "experimental";
337
+ const environments = readyDeps.map((d) => d.environment);
338
+ const orgSyncData = {
339
+ slug: entityBuilder.slugify(orgCfg.name),
340
+ name: orgCfg.name,
341
+ projectId: orgCfg.projectId,
342
+ environments,
343
+ lifecycle
344
+ };
345
+ proxyMap.set(entityBuilder.slugify(proxy.name), {
346
+ proxy,
347
+ basePaths: revision.basepaths ?? [],
348
+ orgs: [orgSyncData]
349
+ });
350
+ } catch (e) {
351
+ this.logger.warn(
352
+ `[catalog-module-apigee] Skipping proxy '${proxy.name}' in org '${orgCfg.name}': ${e}`
353
+ );
354
+ }
355
+ })
356
+ );
357
+ return {
358
+ orgName: orgCfg.name,
359
+ proxyMap,
360
+ sharedflows,
361
+ apiTypeMap,
362
+ apiHubApis,
363
+ projectId: orgCfg.projectId,
364
+ githubOrgSlug: orgCfg.githubOrgSlug,
365
+ apiHubMappings: orgCfg.apiHubMappings
366
+ };
367
+ }
368
+ }
369
+
370
+ exports.ApigeeEntityProvider = ApigeeEntityProvider;
371
+ //# sourceMappingURL=entity-provider.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-provider.cjs.js","sources":["../../src/lib/entity-provider.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n type LoggerService,\r\n type SchedulerServiceTaskRunner,\r\n} from '@backstage/backend-plugin-api';\r\nimport type { Entity } from '@backstage/catalog-model';\r\nimport type {\r\n EntityProvider,\r\n EntityProviderConnection,\r\n} from '@backstage/plugin-catalog-node';\r\n\r\nimport type { ApigeeClient } from './apigee-client';\r\nimport type { ApiHubClient, ApiHubApi } from './api-hub-client';\r\nimport type { SharedflowClient, ApigeeSharedflow } from './sharedflow-client';\r\nimport {\r\n slugify,\r\n proxyToEntities,\r\n sharedflowToEntity,\r\n selectSpec,\r\n enrichMatchedApiEntity,\r\n hubOnlyToEntity,\r\n type OrgSyncData,\r\n type MergedProxy,\r\n type EntityBuildOptions,\r\n type SpecEmbed,\r\n} from './entity-builder';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal types\r\n// ---------------------------------------------------------------------------\r\n\r\n/** Org configuration shape (matches config.d.ts apigee.orgs[n]) */\r\ninterface OrgConfig {\r\n name: string;\r\n projectId: string;\r\n environments?: string[];\r\n githubOrgSlug?: string;\r\n /** proxyName → API Hub apiId override (D-01); set by module.ts from config. */\r\n apiHubMappings?: Record<string, string>;\r\n}\r\n\r\n/** Data collected for a single org during one sync run. */\r\ninterface OrgData {\r\n orgName: string;\r\n proxyMap: Map<string, MergedProxy>; // slug → MergedProxy (single-org orgs array)\r\n sharedflows: ApigeeSharedflow[];\r\n /** proxyName.toLowerCase() → apiType from API Hub */\r\n apiTypeMap: Map<string, string>;\r\n /** All API Hub definitions discovered for this org (retained for enrichment). */\r\n apiHubApis: ApiHubApi[];\r\n /** GCP project for this org's API Hub instance (D-01 spec fetch). */\r\n projectId: string;\r\n /** Org GitHub slug for source-link fallback (D-05). */\r\n githubOrgSlug?: string;\r\n /** proxyName → API Hub apiId override (D-01). */\r\n apiHubMappings?: Record<string, string>;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Provider\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Maximum number of spec resolutions (each ~3-4 API Hub round-trips) to run in\r\n * parallel during a sync. Bounds concurrency so a large catalogue does not open\r\n * dozens of simultaneous connections while still avoiding fully serial latency.\r\n */\r\nconst SPEC_FETCH_CONCURRENCY = 8;\r\n\r\n/**\r\n * Entity provider that discovers Apigee API proxies and sharedflows across\r\n * one or more organisations and registers them as Backstage entities.\r\n *\r\n * Registered with the catalog via `catalogProcessingExtensionPoint.addEntityProvider`.\r\n * Scheduled via the `SchedulerServiceTaskRunner` injected at construction time.\r\n */\r\nexport class ApigeeEntityProvider implements EntityProvider {\r\n private connection?: EntityProviderConnection;\r\n\r\n constructor(\r\n private readonly taskRunner: SchedulerServiceTaskRunner,\r\n private readonly logger: LoggerService,\r\n private readonly orgsConfig: OrgConfig[],\r\n private readonly apigeeClient: ApigeeClient,\r\n private readonly apiHubClient: ApiHubClient,\r\n private readonly sharedflowClient: SharedflowClient,\r\n private readonly options: EntityBuildOptions,\r\n ) {}\r\n\r\n /** Stable provider name — MUST NOT change; it links entities to this provider. */\r\n getProviderName(): string {\r\n return 'apigee-catalog-provider';\r\n }\r\n\r\n /**\r\n * Called once by the catalog on startup. Stores the connection and registers\r\n * the recurring sync task with the scheduler.\r\n */\r\n async connect(connection: EntityProviderConnection): Promise<void> {\r\n if (this.connection) {\r\n throw new Error('ApigeeEntityProvider is already connected');\r\n }\r\n this.connection = connection;\r\n await this.taskRunner.run({\r\n id: 'catalog-backend-module-apigee:sync',\r\n fn: async () => {\r\n await this.run();\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Executes one full catalog sync:\r\n * 1. Fetch proxy / sharedflow / API-Hub data for all orgs in parallel.\r\n * 2. Warn and skip any org that fails (error isolation per org).\r\n * 3. Deduplicate proxies shared across multiple orgs (D-03).\r\n * 4. Build entities and call applyMutation exactly once.\r\n */\r\n async run(): Promise<void> {\r\n if (!this.connection) {\r\n throw new Error('ApigeeEntityProvider not initialized');\r\n }\r\n\r\n // ── Step 1: Parallel fetch ────────────────────────────────────────────\r\n const orgResults = await Promise.allSettled(\r\n this.orgsConfig.map(orgCfg => this.fetchOrgData(orgCfg)),\r\n );\r\n\r\n // ── Step 2: Collect successful results; warn on failures ──────────────\r\n const orgDataList: OrgData[] = [];\r\n for (let i = 0; i < orgResults.length; i++) {\r\n const result = orgResults[i];\r\n if (result.status === 'rejected') {\r\n this.logger.warn(\r\n `[catalog-module-apigee] Org '${this.orgsConfig[i].name}' sync failed: ${result.reason}`,\r\n );\r\n } else {\r\n orgDataList.push(result.value);\r\n }\r\n }\r\n\r\n // ── Step 3: Deduplicate proxies by slug across orgs (D-03) ────────────\r\n const mergedProxyMap = new Map<string, MergedProxy>();\r\n for (const orgData of orgDataList) {\r\n for (const [slug, partial] of orgData.proxyMap) {\r\n const existing = mergedProxyMap.get(slug);\r\n if (existing) {\r\n // Same proxy in multiple orgs: merge orgs array\r\n existing.orgs.push(...partial.orgs);\r\n // Merge base paths (deduplicate)\r\n for (const bp of partial.basePaths) {\r\n if (!existing.basePaths.includes(bp)) {\r\n existing.basePaths.push(bp);\r\n }\r\n }\r\n } else {\r\n mergedProxyMap.set(slug, {\r\n proxy: partial.proxy,\r\n basePaths: [...partial.basePaths],\r\n orgs: [...partial.orgs],\r\n });\r\n }\r\n }\r\n }\r\n\r\n // ── Step 4: Build entities ─────────────────────────────────────────────\r\n const entities: Entity[] = [];\r\n\r\n // Combined API Hub lookup across orgs, keyed by slugified displayName\r\n // (the human identifier; the resource name's last segment is an opaque\r\n // UUID and never matches a proxy slug, D-01). First org wins on collision.\r\n const apiHubById = new Map<string, { api: ApiHubApi; orgData: OrgData }>();\r\n // proxyName → API Hub apiId overrides collected across orgs (D-01).\r\n const mappingOverrides = new Map<string, string>();\r\n for (const orgData of orgDataList) {\r\n for (const hubApi of orgData.apiHubApis) {\r\n const id = slugify(\r\n hubApi.displayName ?? hubApi.name.split('/').pop() ?? '',\r\n );\r\n if (id && !apiHubById.has(id)) {\r\n apiHubById.set(id, { api: hubApi, orgData });\r\n }\r\n }\r\n if (orgData.apiHubMappings) {\r\n for (const [proxyName, hubId] of Object.entries(orgData.apiHubMappings)) {\r\n if (!mappingOverrides.has(proxyName)) {\r\n mappingOverrides.set(proxyName, hubId);\r\n }\r\n }\r\n }\r\n }\r\n\r\n const matchedHubIds = new Set<string>();\r\n\r\n // First pass: build entity pairs and split into matched (need a spec fetch)\r\n // vs. unmatched (ready to emit). Spec fetches are deferred so they can run\r\n // with bounded concurrency instead of one-at-a-time (WR-03).\r\n interface MatchedItem {\r\n component: Entity;\r\n api: Entity;\r\n hubApi: ApiHubApi;\r\n githubOrgSlug?: string;\r\n realApiId: string;\r\n projectId: string;\r\n }\r\n const matchedItems: MatchedItem[] = [];\r\n\r\n for (const [slug, merged] of mergedProxyMap.entries()) {\r\n // Resolve API type: try each org's apiTypeMap (D-01)\r\n let apiType: string | undefined;\r\n for (const orgData of orgDataList) {\r\n const t =\r\n orgData.apiTypeMap.get(slug) ??\r\n orgData.apiTypeMap.get(merged.proxy.name.toLowerCase());\r\n if (t) {\r\n apiType = t;\r\n break;\r\n }\r\n }\r\n const [component, api] = proxyToEntities(merged, apiType, this.options);\r\n\r\n // Match proxy → API Hub definition: apiHubMappings override else\r\n // slug == lowercased apiId (D-01).\r\n const candidate = (\r\n mappingOverrides.get(merged.proxy.name) ?? slug\r\n ).toLowerCase();\r\n const match = apiHubById.get(candidate);\r\n if (match) {\r\n matchedHubIds.add(candidate);\r\n matchedItems.push({\r\n component,\r\n api,\r\n hubApi: match.api,\r\n githubOrgSlug: match.orgData.githubOrgSlug,\r\n realApiId: match.api.name.split('/').pop() ?? candidate,\r\n projectId: match.orgData.projectId,\r\n });\r\n } else {\r\n // No API Hub match: API keeps has-spec:'false' from proxyToEntities.\r\n entities.push(component, api);\r\n }\r\n }\r\n\r\n // D-02/D-03: resolve specs with bounded concurrency, then assemble entities\r\n // in original order. Per-spec try/catch isolation lives in resolveSpecForApi.\r\n const matchedSpecs = await this.mapWithConcurrency(\r\n matchedItems,\r\n SPEC_FETCH_CONCURRENCY,\r\n item => this.resolveSpecForApi(item.realApiId, item.projectId),\r\n );\r\n matchedItems.forEach((item, i) => {\r\n entities.push(\r\n item.component,\r\n enrichMatchedApiEntity(\r\n item.api,\r\n item.hubApi,\r\n matchedSpecs[i],\r\n item.githubOrgSlug,\r\n this.options,\r\n ),\r\n );\r\n });\r\n\r\n // Append hub-only API entities for API Hub definitions with no matching\r\n // proxy (D-04, CAT-05). De-dupe by entity name for hub apis shared across orgs.\r\n const hubOnlyItems: Array<{\r\n hubApi: ApiHubApi;\r\n orgData: OrgData;\r\n realApiId: string;\r\n }> = [];\r\n for (const [id, { api: hubApi, orgData }] of apiHubById.entries()) {\r\n if (matchedHubIds.has(id)) continue;\r\n hubOnlyItems.push({\r\n hubApi,\r\n orgData,\r\n realApiId: hubApi.name.split('/').pop() ?? id,\r\n });\r\n }\r\n const hubOnlySpecs = await this.mapWithConcurrency(\r\n hubOnlyItems,\r\n SPEC_FETCH_CONCURRENCY,\r\n item => this.resolveSpecForApi(item.realApiId, item.orgData.projectId),\r\n );\r\n const hubOnlyNames = new Set<string>();\r\n hubOnlyItems.forEach((item, i) => {\r\n const entity = hubOnlyToEntity(\r\n item.hubApi,\r\n item.orgData.orgName,\r\n hubOnlySpecs[i],\r\n item.orgData.githubOrgSlug,\r\n this.options,\r\n );\r\n if (hubOnlyNames.has(entity.metadata.name)) return;\r\n hubOnlyNames.add(entity.metadata.name);\r\n entities.push(entity);\r\n });\r\n\r\n // Add sharedflow entities\r\n for (const orgData of orgDataList) {\r\n for (const sf of orgData.sharedflows) {\r\n entities.push(sharedflowToEntity(sf, orgData.orgName, this.options));\r\n }\r\n }\r\n\r\n // ── Step 5: Single full mutation ──────────────────────────────────────\r\n await this.connection.applyMutation({\r\n type: 'full',\r\n entities: entities.map(entity => ({\r\n entity,\r\n locationKey: this.getProviderName(),\r\n })),\r\n });\r\n }\r\n\r\n // ── Private helpers ──────────────────────────────────────────────────────\r\n\r\n /**\r\n * Maps `items` through `fn` with at most `limit` invocations in flight at\r\n * once, preserving input order in the returned array. Used to parallelise\r\n * per-API spec resolution without overwhelming the API Hub backend (WR-03).\r\n */\r\n private async mapWithConcurrency<T, R>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T) => Promise<R>,\r\n ): Promise<R[]> {\r\n const results = new Array<R>(items.length);\r\n let next = 0;\r\n const workerCount = Math.max(1, Math.min(limit, items.length));\r\n const worker = async (): Promise<void> => {\r\n while (true) {\r\n const i = next;\r\n next += 1;\r\n if (i >= items.length) break;\r\n results[i] = await fn(items[i]);\r\n }\r\n };\r\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\r\n return results;\r\n }\r\n\r\n /**\r\n * Fetches a spec's contents subject to the size guard (D-03).\r\n *\r\n * Two-stage guard:\r\n * 1. Pre-fetch: if the spec metadata reports `sizeBytes > specMaxBytes`, skip\r\n * the download entirely and return a `specUrl` link. NOTE: the API Hub spec\r\n * metadata endpoint frequently reports `sizeBytes: 0`/undefined, so this\r\n * stage often cannot fire and the contents are downloaded anyway.\r\n * 2. Post-decode: enforce `specMaxBytes` against the actual decoded byte\r\n * length — this is the guard that reliably bounds the *embedded* size.\r\n *\r\n * Because stage 1 is unreliable, `specMaxBytes` bounds the size of the spec\r\n * that gets embedded, not necessarily the size downloaded into memory.\r\n */\r\n private async fetchSpecEmbed(\r\n apiId: string,\r\n versionId: string,\r\n specId: string,\r\n projectId: string,\r\n ): Promise<SpecEmbed> {\r\n const spec = await this.apiHubClient.getSpec(\r\n apiId,\r\n versionId,\r\n specId,\r\n projectId,\r\n );\r\n const limit = this.options.specMaxBytes ?? 512000;\r\n const size = spec.sizeBytes ?? 0;\r\n if (size > limit) {\r\n // D-03 stage 1: over the size limit by metadata — link instead of embedding.\r\n this.logger.warn(\r\n `[catalog-module-apigee] Spec '${spec.name}' (${size} bytes) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`,\r\n );\r\n return { hasSpec: true, specUrl: spec.name };\r\n }\r\n const contents = await this.apiHubClient.getSpecContents(\r\n apiId,\r\n versionId,\r\n specId,\r\n projectId,\r\n );\r\n const base64 = contents.contents ?? '';\r\n const definition = Buffer.from(base64, 'base64').toString('utf-8');\r\n const decodedBytes = Buffer.byteLength(definition, 'utf-8');\r\n // Trace decode results so spec-shape regressions (e.g. a renamed contents\r\n // field, or malformed base64 decoding to garbage) are easy to diagnose.\r\n this.logger.debug(\r\n `[catalog-module-apigee] Resolved spec '${spec.name}' (mimeType=${\r\n contents.mimeType ?? '?'\r\n }, ${decodedBytes} bytes decoded, metadata sizeBytes=${\r\n spec.sizeBytes ?? 'n/a'\r\n })`,\r\n );\r\n if (!base64) {\r\n // Empty payload — record as \"no spec\" rather than embedding an empty string.\r\n this.logger.warn(\r\n `[catalog-module-apigee] Spec '${spec.name}' returned empty contents; treating as no embeddable spec`,\r\n );\r\n return { hasSpec: false };\r\n }\r\n // D-03 stage 2: the pre-fetch guard can be bypassed when sizeBytes is 0,\r\n // so enforce the limit against the actual decoded byte length.\r\n if (decodedBytes > limit) {\r\n this.logger.warn(\r\n `[catalog-module-apigee] Spec '${spec.name}' (${decodedBytes} bytes decoded) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`,\r\n );\r\n return { hasSpec: true, specUrl: spec.name };\r\n }\r\n return { hasSpec: true, definition };\r\n }\r\n\r\n /**\r\n * Resolves the spec for an API: lists versions, deterministically selects one\r\n * (D-02), enumerates that version's specs (the versions endpoint does not\r\n * reliably populate specIds), picks the lexicographically smallest spec ID,\r\n * then fetches it with the size guard. Per-spec failures are isolated: any\r\n * error is logged and `undefined` is returned so one bad spec never aborts\r\n * the whole sync.\r\n */\r\n private async resolveSpecForApi(\r\n apiId: string,\r\n projectId: string,\r\n ): Promise<SpecEmbed | undefined> {\r\n try {\r\n const versions = await this.apiHubClient.listVersions(apiId, projectId);\r\n const selection = selectSpec(versions);\r\n if (!selection) return undefined;\r\n const specs = await this.apiHubClient.listSpecs(\r\n apiId,\r\n selection.versionId,\r\n projectId,\r\n );\r\n const specId = specs\r\n .map(s => s.name?.split('/').pop() ?? '')\r\n .filter(Boolean)\r\n .sort()[0];\r\n if (!specId) return undefined;\r\n return await this.fetchSpecEmbed(\r\n apiId,\r\n selection.versionId,\r\n specId,\r\n projectId,\r\n );\r\n } catch (e) {\r\n this.logger.warn(\r\n `[catalog-module-apigee] spec fetch failed for ${apiId}: ${e}`,\r\n );\r\n return undefined;\r\n }\r\n }\r\n\r\n /**\r\n * Fetches all data for one org: proxies, their deployments + revisions,\r\n * API Hub type map, and sharedflows.\r\n */\r\n private async fetchOrgData(orgCfg: OrgConfig): Promise<OrgData> {\r\n const [proxies, apiHubApis, sharedflows] = await Promise.all([\r\n this.apigeeClient.listProxies(orgCfg.name),\r\n this.apiHubClient.listApis(orgCfg.projectId),\r\n this.sharedflowClient.listSharedflows(orgCfg.name),\r\n ]);\r\n\r\n // Build API Hub type map: slugified displayName → apiType\r\n const apiTypeMap = new Map<string, string>();\r\n for (const api of apiHubApis) {\r\n const id = slugify(api.displayName ?? api.name.split('/').pop() ?? '');\r\n if (id && api.apiType) {\r\n apiTypeMap.set(id, api.apiType);\r\n }\r\n }\r\n\r\n // Fetch deployments + latest revision for all proxies in parallel\r\n const proxyMap = new Map<string, MergedProxy>();\r\n await Promise.all(\r\n proxies.map(async proxy => {\r\n try {\r\n const latestRev = proxy.revision?.length\r\n ? proxy.revision[proxy.revision.length - 1]\r\n : undefined;\r\n\r\n const [deployments, revision] = await Promise.all([\r\n this.apigeeClient.listDeployments(orgCfg.name, proxy.name),\r\n latestRev\r\n ? this.apigeeClient.getProxyRevision(\r\n orgCfg.name,\r\n proxy.name,\r\n latestRev,\r\n )\r\n : Promise.resolve({ name: proxy.name, basepaths: [] as string[] }),\r\n ]);\r\n\r\n // Derive lifecycle and environment list from ready deployments\r\n const readyDeps = deployments.filter(\r\n d => d.deploymentStatus === 'READY',\r\n );\r\n const lifecycle =\r\n readyDeps.length > 0 ? 'production' : 'experimental';\r\n const environments = readyDeps.map(d => d.environment);\r\n\r\n const orgSyncData: OrgSyncData = {\r\n slug: slugify(orgCfg.name),\r\n name: orgCfg.name,\r\n projectId: orgCfg.projectId,\r\n environments,\r\n lifecycle,\r\n };\r\n\r\n proxyMap.set(slugify(proxy.name), {\r\n proxy,\r\n basePaths: revision.basepaths ?? [],\r\n orgs: [orgSyncData],\r\n });\r\n } catch (e) {\r\n this.logger.warn(\r\n `[catalog-module-apigee] Skipping proxy '${proxy.name}' in org '${orgCfg.name}': ${e}`,\r\n );\r\n }\r\n }),\r\n );\r\n\r\n return {\r\n orgName: orgCfg.name,\r\n proxyMap,\r\n sharedflows,\r\n apiTypeMap,\r\n apiHubApis,\r\n projectId: orgCfg.projectId,\r\n githubOrgSlug: orgCfg.githubOrgSlug,\r\n apiHubMappings: orgCfg.apiHubMappings,\r\n };\r\n }\r\n}\r\n"],"names":["slugify","proxyToEntities","enrichMatchedApiEntity","hubOnlyToEntity","sharedflowToEntity","selectSpec"],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ var fetch = require('node-fetch');
4
+
5
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
6
+
7
+ var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
8
+
9
+ const NON_RETRYABLE_STATUS = /* @__PURE__ */ new Set([400, 401, 403, 404]);
10
+ const MAX_RETRIES = 3;
11
+ const BASE_DELAYS_MS = [1e3, 2e3, 4e3];
12
+ async function fetchWithRetry(url, getToken) {
13
+ let lastError;
14
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
15
+ const token = await getToken();
16
+ if (!token) {
17
+ throw new Error(`fetchWithRetry: unable to obtain access token for ${url}`);
18
+ }
19
+ const res = await fetch__default.default(url, {
20
+ method: "GET",
21
+ headers: {
22
+ Authorization: `Bearer ${token}`,
23
+ Accept: "application/json"
24
+ }
25
+ });
26
+ if (res.ok) {
27
+ return res.json();
28
+ }
29
+ const errMessage = `HTTP ${res.status} ${res.statusText} for GET ${url}`;
30
+ if (NON_RETRYABLE_STATUS.has(res.status)) {
31
+ throw new Error(errMessage);
32
+ }
33
+ lastError = new Error(errMessage);
34
+ if (attempt < MAX_RETRIES) {
35
+ const baseDelay = BASE_DELAYS_MS[attempt] ?? 4e3;
36
+ const jitter = baseDelay * 0.2 * (Math.random() * 2 - 1);
37
+ await sleep(baseDelay + jitter);
38
+ }
39
+ }
40
+ throw lastError ?? new Error(`fetchWithRetry: exhausted retries for ${url}`);
41
+ }
42
+ function sleep(ms) {
43
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
44
+ }
45
+
46
+ exports.MAX_RETRIES = MAX_RETRIES;
47
+ exports.NON_RETRYABLE_STATUS = NON_RETRYABLE_STATUS;
48
+ exports.fetchWithRetry = fetchWithRetry;
49
+ //# sourceMappingURL=fetch-utils.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-utils.cjs.js","sources":["../../src/lib/fetch-utils.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport fetch from 'node-fetch';\r\n\r\n/**\r\n * HTTP status codes that are NOT worth retrying — fail immediately.\r\n * 429 (rate limit) IS retried (not in this set).\r\n */\r\nexport const NON_RETRYABLE_STATUS = new Set([400, 401, 403, 404]);\r\n\r\n/** Maximum retry attempts (not counting the initial attempt). */\r\nexport const MAX_RETRIES = 3;\r\n\r\n/** Base delays in milliseconds for attempts 0, 1, 2. */\r\nconst BASE_DELAYS_MS = [1000, 2000, 4000];\r\n\r\n/**\r\n * Fetches a URL with Bearer auth token injection and exponential back-off retry.\r\n *\r\n * @param url - Full HTTPS URL to fetch (GET)\r\n * @param getToken - Async function that returns a fresh access token string.\r\n * Pass `() => auth.getAccessToken()` from google-auth-library GoogleAuth instance.\r\n * @returns Parsed JSON response body\r\n * @throws Error with HTTP status + URL for non-retryable errors or exhausted retries\r\n */\r\nexport async function fetchWithRetry(\r\n url: string,\r\n getToken: () => Promise<string | null | undefined>,\r\n): Promise<unknown> {\r\n let lastError: Error | undefined;\r\n\r\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\r\n const token = await getToken();\r\n if (!token) {\r\n throw new Error(`fetchWithRetry: unable to obtain access token for ${url}`);\r\n }\r\n\r\n const res = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n Authorization: `Bearer ${token}`,\r\n Accept: 'application/json',\r\n },\r\n });\r\n\r\n if (res.ok) {\r\n return res.json();\r\n }\r\n\r\n const errMessage = `HTTP ${res.status} ${res.statusText} for GET ${url}`;\r\n\r\n if (NON_RETRYABLE_STATUS.has(res.status)) {\r\n throw new Error(errMessage);\r\n }\r\n\r\n lastError = new Error(errMessage);\r\n\r\n if (attempt < MAX_RETRIES) {\r\n const baseDelay = BASE_DELAYS_MS[attempt] ?? 4000;\r\n // Jitter: ±20% of the base delay\r\n const jitter = baseDelay * 0.2 * (Math.random() * 2 - 1);\r\n await sleep(baseDelay + jitter);\r\n }\r\n }\r\n\r\n throw lastError ?? new Error(`fetchWithRetry: exhausted retries for ${url}`);\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise(resolve => setTimeout(resolve, Math.max(0, ms)));\r\n}\r\n"],"names":["fetch"],"mappings":";;;;;;;;AAOO,MAAM,oBAAA,uBAA2B,GAAA,CAAI,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC;AAGzD,MAAM,WAAA,GAAc;AAG3B,MAAM,cAAA,GAAiB,CAAC,GAAA,EAAM,GAAA,EAAM,GAAI,CAAA;AAWxC,eAAsB,cAAA,CACpB,KACA,QAAA,EACkB;AAClB,EAAA,IAAI,SAAA;AAEJ,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,WAAA,EAAa,OAAA,EAAA,EAAW;AACvD,IAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,EAAS;AAC7B,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kDAAA,EAAqD,GAAG,CAAA,CAAE,CAAA;AAAA,IAC5E;AAEA,IAAA,MAAM,GAAA,GAAM,MAAMA,sBAAA,CAAM,GAAA,EAAK;AAAA,MAC3B,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,UAAU,KAAK,CAAA,CAAA;AAAA,QAC9B,MAAA,EAAQ;AAAA;AACV,KACD,CAAA;AAED,IAAA,IAAI,IAAI,EAAA,EAAI;AACV,MAAA,OAAO,IAAI,IAAA,EAAK;AAAA,IAClB;AAEA,IAAA,MAAM,UAAA,GAAa,QAAQ,GAAA,CAAI,MAAM,IAAI,GAAA,CAAI,UAAU,YAAY,GAAG,CAAA,CAAA;AAEtE,IAAA,IAAI,oBAAA,CAAqB,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA,EAAG;AACxC,MAAA,MAAM,IAAI,MAAM,UAAU,CAAA;AAAA,IAC5B;AAEA,IAAA,SAAA,GAAY,IAAI,MAAM,UAAU,CAAA;AAEhC,IAAA,IAAI,UAAU,WAAA,EAAa;AACzB,MAAA,MAAM,SAAA,GAAY,cAAA,CAAe,OAAO,CAAA,IAAK,GAAA;AAE7C,MAAA,MAAM,SAAS,SAAA,GAAY,GAAA,IAAO,IAAA,CAAK,MAAA,KAAW,CAAA,GAAI,CAAA,CAAA;AACtD,MAAA,MAAM,KAAA,CAAM,YAAY,MAAM,CAAA;AAAA,IAChC;AAAA,EACF;AAEA,EAAA,MAAM,SAAA,IAAa,IAAI,KAAA,CAAM,CAAA,sCAAA,EAAyC,GAAG,CAAA,CAAE,CAAA;AAC7E;AAEA,SAAS,MAAM,EAAA,EAA2B;AACxC,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAA,OAAA,KAAW,UAAA,CAAW,OAAA,EAAS,KAAK,GAAA,CAAI,CAAA,EAAG,EAAE,CAAC,CAAC,CAAA;AACpE;;;;;;"}
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ var fetchUtils = require('./fetch-utils.cjs.js');
4
+
5
+ class SharedflowClient {
6
+ constructor(auth) {
7
+ this.auth = auth;
8
+ }
9
+ auth;
10
+ /**
11
+ * Lists all sharedflows in the given Apigee organisation.
12
+ */
13
+ async listSharedflows(org) {
14
+ const url = `https://apigee.googleapis.com/v1/organizations/${encodeURIComponent(org)}/sharedflows`;
15
+ const body = await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
16
+ return body.sharedFlows ?? [];
17
+ }
18
+ }
19
+
20
+ exports.SharedflowClient = SharedflowClient;
21
+ //# sourceMappingURL=sharedflow-client.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedflow-client.cjs.js","sources":["../../src/lib/sharedflow-client.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { GoogleAuth } from 'google-auth-library';\r\nimport { fetchWithRetry } from './fetch-utils';\r\n\r\n/**\r\n * Minimal representation of an Apigee sharedflow as returned by\r\n * GET /v1/organizations/{org}/sharedflows\r\n */\r\nexport interface ApigeeSharedflow {\r\n name: string;\r\n latestRevisionId?: string;\r\n metaData?: {\r\n createdAt?: string;\r\n lastModifiedAt?: string;\r\n };\r\n}\r\n\r\n/**\r\n * Client for the Apigee Management API's sharedflow endpoints.\r\n */\r\nexport class SharedflowClient {\r\n constructor(private readonly auth: GoogleAuth) {}\r\n\r\n /**\r\n * Lists all sharedflows in the given Apigee organisation.\r\n */\r\n async listSharedflows(org: string): Promise<ApigeeSharedflow[]> {\r\n const url = `https://apigee.googleapis.com/v1/organizations/${encodeURIComponent(org)}/sharedflows`;\r\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\r\n return (body as { sharedFlows?: ApigeeSharedflow[] }).sharedFlows ?? [];\r\n }\r\n}\r\n"],"names":["fetchWithRetry"],"mappings":";;;;AAoBO,MAAM,gBAAA,CAAiB;AAAA,EAC5B,YAA6B,IAAA,EAAkB;AAAlB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAmB;AAAA,EAAnB,IAAA;AAAA;AAAA;AAAA;AAAA,EAK7B,MAAM,gBAAgB,GAAA,EAA0C;AAC9D,IAAA,MAAM,GAAA,GAAM,CAAA,+CAAA,EAAkD,kBAAA,CAAmB,GAAG,CAAC,CAAA,YAAA,CAAA;AACrF,IAAA,MAAM,IAAA,GAAO,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AACvE,IAAA,OAAQ,IAAA,CAA8C,eAAe,EAAC;AAAA,EACxE;AACF;;;;"}
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var alpha = require('@backstage/plugin-catalog-node/alpha');
5
+ var googleAuthLibrary = require('google-auth-library');
6
+ var apigeeClient = require('./lib/apigee-client.cjs.js');
7
+ var apiHubClient = require('./lib/api-hub-client.cjs.js');
8
+ var sharedflowClient = require('./lib/sharedflow-client.cjs.js');
9
+ var entityProvider = require('./lib/entity-provider.cjs.js');
10
+ var apigeeStitchingProcessor = require('./lib/apigee-stitching-processor.cjs.js');
11
+
12
+ const catalogModuleApigee = backendPluginApi.createBackendModule({
13
+ pluginId: "catalog",
14
+ moduleId: "apigee-catalog",
15
+ register(env) {
16
+ env.registerInit({
17
+ deps: {
18
+ catalog: alpha.catalogProcessingExtensionPoint,
19
+ scheduler: backendPluginApi.coreServices.scheduler,
20
+ logger: backendPluginApi.coreServices.logger,
21
+ config: backendPluginApi.coreServices.rootConfig
22
+ },
23
+ async init({ catalog, scheduler, logger, config }) {
24
+ let orgs;
25
+ try {
26
+ orgs = config.getConfigArray("apigee.orgs");
27
+ } catch {
28
+ throw new Error(
29
+ 'Apigee catalog module misconfigured: "apigee.orgs" is required in app-config.yaml.\nExample:\n apigee:\n orgs:\n - name: my-org\n projectId: my-gcp-project'
30
+ );
31
+ }
32
+ if (orgs.length === 0) {
33
+ throw new Error(
34
+ 'Apigee catalog module misconfigured: "apigee.orgs" must contain at least one entry.'
35
+ );
36
+ }
37
+ logger.info(
38
+ `[catalog-module-apigee] Initialized with ${orgs.length} org(s): ` + orgs.map((o) => o.getString("name")).join(", ")
39
+ );
40
+ const scheduleConfig = config.has("apigee.schedule") ? backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(
41
+ config.getConfig("apigee.schedule")
42
+ ) : {
43
+ frequency: { minutes: 60 },
44
+ timeout: { minutes: 10 },
45
+ initialDelay: { seconds: 30 }
46
+ };
47
+ const taskRunner = scheduler.createScheduledTaskRunner(scheduleConfig);
48
+ const auth = new googleAuthLibrary.GoogleAuth({
49
+ scopes: ["https://www.googleapis.com/auth/cloud-platform"]
50
+ });
51
+ const apigeeClient$1 = new apigeeClient.ApigeeClient(auth);
52
+ const apiHubClient$1 = new apiHubClient.ApiHubClient(
53
+ auth,
54
+ "",
55
+ config.getOptionalString("apigee.apiHubLocation") ?? "global"
56
+ );
57
+ const sharedflowClient$1 = new sharedflowClient.SharedflowClient(auth);
58
+ const orgsConfig = orgs.map((orgCfg) => ({
59
+ name: orgCfg.getString("name"),
60
+ projectId: orgCfg.getString("projectId"),
61
+ environments: orgCfg.has("environments") ? orgCfg.getStringArray("environments") : void 0,
62
+ githubOrgSlug: orgCfg.getOptionalString("githubOrgSlug"),
63
+ // D-01: explicit proxy-name -> API Hub apiId overrides per org
64
+ apiHubMappings: orgCfg.has("apiHubMappings") ? orgCfg.getConfig("apiHubMappings").get() : void 0
65
+ }));
66
+ const provider = new entityProvider.ApigeeEntityProvider(
67
+ taskRunner,
68
+ logger,
69
+ orgsConfig,
70
+ apigeeClient$1,
71
+ apiHubClient$1,
72
+ sharedflowClient$1,
73
+ {
74
+ defaultOwner: config.getOptionalString("apigee.defaultOwner"),
75
+ // D-03: inline spec size guard (bytes); provider falls back to 512000
76
+ specMaxBytes: config.getOptionalNumber("apigee.specMaxBytes")
77
+ }
78
+ );
79
+ catalog.addEntityProvider(provider);
80
+ catalog.addProcessor(new apigeeStitchingProcessor.ApigeeStitchingProcessor(logger));
81
+ logger.info(
82
+ "[catalog-module-apigee] Registered Apigee stitching processor"
83
+ );
84
+ }
85
+ });
86
+ }
87
+ });
88
+
89
+ exports.catalogModuleApigee = catalogModuleApigee;
90
+ //# sourceMappingURL=module.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n coreServices,\r\n createBackendModule,\r\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\r\n} from '@backstage/backend-plugin-api';\r\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';\r\nimport { GoogleAuth } from 'google-auth-library';\r\nimport { ApigeeClient } from './lib/apigee-client';\r\nimport { ApiHubClient } from './lib/api-hub-client';\r\nimport { SharedflowClient } from './lib/sharedflow-client';\r\nimport { ApigeeEntityProvider } from './lib/entity-provider';\r\nimport { ApigeeStitchingProcessor } from './lib/apigee-stitching-processor';\r\n\r\n/**\r\n * Backstage catalog backend module for Apigee API discovery.\r\n * Plugs into the catalog backend and registers Apigee proxies + API Hub\r\n * definitions as Backstage API entities.\r\n */\r\nexport const catalogModuleApigee = createBackendModule({\r\n pluginId: 'catalog',\r\n moduleId: 'apigee-catalog',\r\n register(env) {\r\n env.registerInit({\r\n deps: {\r\n catalog: catalogProcessingExtensionPoint,\r\n scheduler: coreServices.scheduler,\r\n logger: coreServices.logger,\r\n config: coreServices.rootConfig,\r\n },\r\n async init({ catalog, scheduler, logger, config }) {\r\n // AUTH-03: Fail fast if apigee.orgs is missing or empty.\r\n let orgs: ReturnType<typeof config.getConfigArray>;\r\n try {\r\n orgs = config.getConfigArray('apigee.orgs');\r\n } catch {\r\n throw new Error(\r\n 'Apigee catalog module misconfigured: \"apigee.orgs\" is required in app-config.yaml.\\n' +\r\n 'Example:\\n' +\r\n ' apigee:\\n' +\r\n ' orgs:\\n' +\r\n ' - name: my-org\\n' +\r\n ' projectId: my-gcp-project',\r\n );\r\n }\r\n if (orgs.length === 0) {\r\n throw new Error(\r\n 'Apigee catalog module misconfigured: \"apigee.orgs\" must contain at least one entry.',\r\n );\r\n }\r\n logger.info(\r\n `[catalog-module-apigee] Initialized with ${orgs.length} org(s): ` +\r\n orgs.map(o => o.getString('name')).join(', '),\r\n );\r\n\r\n // Build task schedule — prefer apigee.schedule config, fall back to 60-min default\r\n const scheduleConfig = config.has('apigee.schedule')\r\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\r\n config.getConfig('apigee.schedule'),\r\n )\r\n : {\r\n frequency: { minutes: 60 },\r\n timeout: { minutes: 10 },\r\n initialDelay: { seconds: 30 },\r\n };\r\n const taskRunner = scheduler.createScheduledTaskRunner(scheduleConfig);\r\n\r\n // Shared GoogleAuth instance (AUTH-01: GCP Application Default Credentials)\r\n const auth = new GoogleAuth({\r\n scopes: ['https://www.googleapis.com/auth/cloud-platform'],\r\n });\r\n\r\n // Instantiate clients (ApiHubClient: no projectId needed; provided per-call by entity provider)\r\n const apigeeClient = new ApigeeClient(auth);\r\n const apiHubClient = new ApiHubClient(\r\n auth,\r\n '',\r\n config.getOptionalString('apigee.apiHubLocation') ?? 'global',\r\n );\r\n const sharedflowClient = new SharedflowClient(auth);\r\n\r\n // Build orgsConfig from config\r\n const orgsConfig = orgs.map(orgCfg => ({\r\n name: orgCfg.getString('name'),\r\n projectId: orgCfg.getString('projectId'),\r\n environments: orgCfg.has('environments')\r\n ? orgCfg.getStringArray('environments')\r\n : undefined,\r\n githubOrgSlug: orgCfg.getOptionalString('githubOrgSlug'),\r\n // D-01: explicit proxy-name -> API Hub apiId overrides per org\r\n apiHubMappings: orgCfg.has('apiHubMappings')\r\n ? (orgCfg.getConfig('apiHubMappings').get() as Record<string, string>)\r\n : undefined,\r\n }));\r\n\r\n // Register the entity provider with the catalog\r\n const provider = new ApigeeEntityProvider(\r\n taskRunner,\r\n logger,\r\n orgsConfig,\r\n apigeeClient,\r\n apiHubClient,\r\n sharedflowClient,\r\n {\r\n defaultOwner: config.getOptionalString('apigee.defaultOwner'),\r\n // D-03: inline spec size guard (bytes); provider falls back to 512000\r\n specMaxBytes: config.getOptionalNumber('apigee.specMaxBytes'),\r\n },\r\n );\r\n catalog.addEntityProvider(provider);\r\n\r\n // Phase 4 (STITCH-01): register the repository-stitching processor so\r\n // annotated backend Components are linked to their proxy API entities.\r\n catalog.addProcessor(new ApigeeStitchingProcessor(logger));\r\n logger.info(\r\n '[catalog-module-apigee] Registered Apigee stitching processor',\r\n );\r\n },\r\n });\r\n },\r\n});\r\n\r\n"],"names":["createBackendModule","catalogProcessingExtensionPoint","coreServices","readSchedulerServiceTaskScheduleDefinitionFromConfig","GoogleAuth","apigeeClient","ApigeeClient","apiHubClient","ApiHubClient","sharedflowClient","SharedflowClient","ApigeeEntityProvider","ApigeeStitchingProcessor"],"mappings":"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"}