@nospt/backstage-plugin-apigee-backend 1.1.0-rc2

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,502 @@
1
+ 'use strict';
2
+
3
+ var entityBuilder = require('./entity-builder.cjs.js');
4
+
5
+ const SPEC_FETCH_CONCURRENCY = 8;
6
+ const MAX_SPEC_CACHE_ENTRIES = 1e3;
7
+ const MAX_SPEC_CACHE_BYTES = 64 * 1024 * 1024;
8
+ function specEmbedBytes(embed) {
9
+ return (embed.definition ? Buffer.byteLength(embed.definition, "utf-8") : 0) + (embed.specUrl ? embed.specUrl.length : 0);
10
+ }
11
+ function setSpecInBoundedCache(cache, currentBytes, key, embed, maxEntries, maxBytes) {
12
+ let bytes = currentBytes;
13
+ const existing = cache.get(key);
14
+ if (existing !== void 0) {
15
+ bytes -= specEmbedBytes(existing);
16
+ cache.delete(key);
17
+ }
18
+ cache.set(key, embed);
19
+ bytes += specEmbedBytes(embed);
20
+ while (cache.size > maxEntries || bytes > maxBytes) {
21
+ const oldest = cache.keys().next().value;
22
+ if (oldest === void 0) break;
23
+ const evicted = cache.get(oldest);
24
+ cache.delete(oldest);
25
+ if (evicted !== void 0) bytes -= specEmbedBytes(evicted);
26
+ }
27
+ return bytes;
28
+ }
29
+ class ApigeeEntityProvider {
30
+ constructor(taskRunner, logger, orgsConfig, apigeeClient, apiHubClient, sharedflowClient, options) {
31
+ this.taskRunner = taskRunner;
32
+ this.logger = logger;
33
+ this.orgsConfig = orgsConfig;
34
+ this.apigeeClient = apigeeClient;
35
+ this.apiHubClient = apiHubClient;
36
+ this.sharedflowClient = sharedflowClient;
37
+ this.options = options;
38
+ }
39
+ taskRunner;
40
+ logger;
41
+ orgsConfig;
42
+ apigeeClient;
43
+ apiHubClient;
44
+ sharedflowClient;
45
+ options;
46
+ connection;
47
+ /**
48
+ * Last successfully-fetched data per org name. Reused when a later sync of
49
+ * that org fails, so a `type: 'full'` mutation never deletes a transiently
50
+ * unavailable org's entities (they survive until that org syncs again).
51
+ */
52
+ lastGoodOrgData = /* @__PURE__ */ new Map();
53
+ /**
54
+ * Last successfully-resolved spec per `${projectId}::${apiId}`. Reused when a
55
+ * later live spec resolution throws (e.g. API Hub is down while an org is
56
+ * served from {@link lastGoodOrgData}), so the entity keeps its embedded spec
57
+ * instead of blinking to `has-spec: false`. Bounded by an LRU policy
58
+ * (MAX_SPEC_CACHE_ENTRIES / MAX_SPEC_CACHE_BYTES).
59
+ */
60
+ lastGoodSpecs = /* @__PURE__ */ new Map();
61
+ /** Running total of bytes retained in {@link lastGoodSpecs} (for LRU eviction). */
62
+ specCacheBytes = 0;
63
+ /** Stable provider name — MUST NOT change; it links entities to this provider. */
64
+ getProviderName() {
65
+ return "apigee-catalog-provider";
66
+ }
67
+ /**
68
+ * Called once by the catalog on startup. Stores the connection and registers
69
+ * the recurring sync task with the scheduler.
70
+ */
71
+ async connect(connection) {
72
+ if (this.connection) {
73
+ throw new Error("ApigeeEntityProvider is already connected");
74
+ }
75
+ this.connection = connection;
76
+ await this.taskRunner.run({
77
+ id: "catalog-backend-module-apigee:sync",
78
+ fn: async () => {
79
+ await this.run();
80
+ }
81
+ });
82
+ }
83
+ /**
84
+ * Executes one full catalog sync:
85
+ * 1. Fetch proxy / sharedflow / API-Hub data for all orgs in parallel.
86
+ * 2. Reuse each failed org's last-known-good data so a `full` mutation
87
+ * never deletes its entities; skip the run entirely if no org succeeds.
88
+ * 3. Deduplicate proxies shared across multiple orgs (D-03).
89
+ * 4. Build entities and call applyMutation exactly once.
90
+ */
91
+ async run() {
92
+ if (!this.connection) {
93
+ throw new Error("ApigeeEntityProvider not initialized");
94
+ }
95
+ this.logger.info(
96
+ `[catalog-module-apigee] Starting catalog sync for ${this.orgsConfig.length} org(s): ${this.orgsConfig.map((o) => o.name).join(", ")}`
97
+ );
98
+ const orgResults = await Promise.allSettled(
99
+ this.orgsConfig.map((orgCfg) => this.fetchOrgData(orgCfg))
100
+ );
101
+ const orgDataList = [];
102
+ let freshCount = 0;
103
+ let reusedCount = 0;
104
+ for (let i = 0; i < orgResults.length; i++) {
105
+ const result = orgResults[i];
106
+ const orgName = this.orgsConfig[i].name;
107
+ if (result.status === "fulfilled") {
108
+ this.lastGoodOrgData.set(orgName, result.value);
109
+ orgDataList.push(result.value);
110
+ freshCount++;
111
+ } else {
112
+ const cached = this.lastGoodOrgData.get(orgName);
113
+ if (cached) {
114
+ reusedCount++;
115
+ orgDataList.push(cached);
116
+ this.logger.warn(
117
+ `[catalog-module-apigee] Org '${orgName}' sync failed: ${result.reason}; reusing last-known-good data to keep its catalog entities`
118
+ );
119
+ } else {
120
+ this.logger.warn(
121
+ `[catalog-module-apigee] Org '${orgName}' sync failed: ${result.reason}; no prior successful sync to fall back on \u2014 its entities are absent this run`
122
+ );
123
+ }
124
+ }
125
+ }
126
+ if (freshCount === 0) {
127
+ this.logger.error(
128
+ `[catalog-module-apigee] All ${this.orgsConfig.length} org(s) failed to sync; skipping catalog mutation to preserve existing entities`
129
+ );
130
+ return;
131
+ }
132
+ const totalProxies = orgDataList.reduce(
133
+ (sum, o) => sum + o.proxyMap.size,
134
+ 0
135
+ );
136
+ const totalSharedflows = orgDataList.reduce(
137
+ (sum, o) => sum + o.sharedflows.length,
138
+ 0
139
+ );
140
+ const totalHubApis = orgDataList.reduce(
141
+ (sum, o) => sum + o.apiHubApis.length,
142
+ 0
143
+ );
144
+ this.logger.info(
145
+ `[catalog-module-apigee] Synced ${freshCount}/${this.orgsConfig.length} org(s) fresh` + (reusedCount > 0 ? `, ${reusedCount} reused from cache` : "") + `: ${totalProxies} proxies, ${totalHubApis} API Hub definitions, ${totalSharedflows} sharedflows`
146
+ );
147
+ const mergedProxyMap = /* @__PURE__ */ new Map();
148
+ for (const orgData of orgDataList) {
149
+ for (const [slug, partial] of orgData.proxyMap) {
150
+ const existing = mergedProxyMap.get(slug);
151
+ if (existing) {
152
+ existing.orgs.push(...partial.orgs);
153
+ for (const bp of partial.basePaths) {
154
+ if (!existing.basePaths.includes(bp)) {
155
+ existing.basePaths.push(bp);
156
+ }
157
+ }
158
+ } else {
159
+ mergedProxyMap.set(slug, {
160
+ proxy: partial.proxy,
161
+ basePaths: [...partial.basePaths],
162
+ orgs: [...partial.orgs]
163
+ });
164
+ }
165
+ }
166
+ }
167
+ const entities = [];
168
+ const apiHubById = /* @__PURE__ */ new Map();
169
+ const mappingOverrides = /* @__PURE__ */ new Map();
170
+ for (const orgData of orgDataList) {
171
+ for (const hubApi of orgData.apiHubApis) {
172
+ const id = entityBuilder.slugify(
173
+ hubApi.displayName ?? hubApi.name.split("/").pop() ?? ""
174
+ );
175
+ if (id && !apiHubById.has(id)) {
176
+ apiHubById.set(id, { api: hubApi, orgData });
177
+ }
178
+ }
179
+ if (orgData.apiHubMappings) {
180
+ for (const [proxyName, hubId] of Object.entries(orgData.apiHubMappings)) {
181
+ if (!mappingOverrides.has(proxyName)) {
182
+ mappingOverrides.set(proxyName, hubId);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ const matchedHubIds = /* @__PURE__ */ new Set();
188
+ const matchedItems = [];
189
+ for (const [slug, merged] of mergedProxyMap.entries()) {
190
+ let apiType;
191
+ for (const orgData of orgDataList) {
192
+ const t = orgData.apiTypeMap.get(slug) ?? orgData.apiTypeMap.get(merged.proxy.name.toLowerCase());
193
+ if (t) {
194
+ apiType = t;
195
+ break;
196
+ }
197
+ }
198
+ const [component, api] = entityBuilder.proxyToEntities(merged, apiType, this.options);
199
+ const candidate = (mappingOverrides.get(merged.proxy.name) ?? slug).toLowerCase();
200
+ const match = apiHubById.get(candidate);
201
+ if (match) {
202
+ matchedHubIds.add(candidate);
203
+ matchedItems.push({
204
+ component,
205
+ api,
206
+ hubApi: match.api,
207
+ githubOrgSlug: match.orgData.githubOrgSlug,
208
+ realApiId: match.api.name.split("/").pop() ?? candidate,
209
+ projectId: match.orgData.projectId
210
+ });
211
+ } else {
212
+ entities.push(component, api);
213
+ }
214
+ }
215
+ const matchedSpecs = await this.mapWithConcurrency(
216
+ matchedItems,
217
+ SPEC_FETCH_CONCURRENCY,
218
+ (item) => this.resolveSpecForApi(item.realApiId, item.projectId)
219
+ );
220
+ matchedItems.forEach((item, i) => {
221
+ const embed = matchedSpecs[i];
222
+ if (embed?.version) {
223
+ item.component.metadata.annotations = {
224
+ ...item.component.metadata.annotations ?? {},
225
+ "apigee.com/version": embed.version
226
+ };
227
+ }
228
+ const department = entityBuilder.departmentLabel(item.hubApi);
229
+ if (department) {
230
+ item.component.metadata.labels = {
231
+ ...item.component.metadata.labels ?? {},
232
+ department
233
+ };
234
+ }
235
+ entities.push(
236
+ item.component,
237
+ entityBuilder.enrichMatchedApiEntity(
238
+ item.api,
239
+ item.hubApi,
240
+ embed,
241
+ item.githubOrgSlug,
242
+ this.options
243
+ )
244
+ );
245
+ });
246
+ const hubOnlyItems = [];
247
+ for (const [id, { api: hubApi, orgData }] of apiHubById.entries()) {
248
+ if (matchedHubIds.has(id)) continue;
249
+ hubOnlyItems.push({
250
+ hubApi,
251
+ orgData,
252
+ realApiId: hubApi.name.split("/").pop() ?? id
253
+ });
254
+ }
255
+ const hubOnlySpecs = await this.mapWithConcurrency(
256
+ hubOnlyItems,
257
+ SPEC_FETCH_CONCURRENCY,
258
+ (item) => this.resolveSpecForApi(item.realApiId, item.orgData.projectId)
259
+ );
260
+ const hubOnlyNames = /* @__PURE__ */ new Set();
261
+ hubOnlyItems.forEach((item, i) => {
262
+ const entity = entityBuilder.hubOnlyToEntity(
263
+ item.hubApi,
264
+ item.orgData.orgName,
265
+ hubOnlySpecs[i],
266
+ item.orgData.githubOrgSlug,
267
+ this.options
268
+ );
269
+ if (hubOnlyNames.has(entity.metadata.name)) return;
270
+ hubOnlyNames.add(entity.metadata.name);
271
+ entities.push(entity);
272
+ });
273
+ for (const orgData of orgDataList) {
274
+ for (const sf of orgData.sharedflows) {
275
+ entities.push(entityBuilder.sharedflowToEntity(sf, orgData.orgName, this.options));
276
+ }
277
+ }
278
+ this.logger.info(
279
+ `[catalog-module-apigee] Catalog sync complete \u2014 emitting ${entities.length} entities`
280
+ );
281
+ await this.connection.applyMutation({
282
+ type: "full",
283
+ entities: entities.map((entity) => ({
284
+ entity,
285
+ locationKey: this.getProviderName()
286
+ }))
287
+ });
288
+ }
289
+ // ── Private helpers ──────────────────────────────────────────────────────
290
+ /**
291
+ * Maps `items` through `fn` with at most `limit` invocations in flight at
292
+ * once, preserving input order in the returned array. Used to parallelise
293
+ * per-API spec resolution without overwhelming the API Hub backend (WR-03).
294
+ */
295
+ async mapWithConcurrency(items, limit, fn) {
296
+ const results = new Array(items.length);
297
+ let next = 0;
298
+ const workerCount = Math.max(1, Math.min(limit, items.length));
299
+ const worker = async () => {
300
+ while (true) {
301
+ const i = next;
302
+ next += 1;
303
+ if (i >= items.length) break;
304
+ results[i] = await fn(items[i]);
305
+ }
306
+ };
307
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
308
+ return results;
309
+ }
310
+ /**
311
+ * Fetches a spec's contents subject to the size guard (D-03).
312
+ *
313
+ * Two-stage guard:
314
+ * 1. Pre-fetch: if the spec metadata reports `sizeBytes > specMaxBytes`, skip
315
+ * the download entirely and return a `specUrl` link. NOTE: the API Hub spec
316
+ * metadata endpoint frequently reports `sizeBytes: 0`/undefined, so this
317
+ * stage often cannot fire and the contents are downloaded anyway.
318
+ * 2. Post-decode: enforce `specMaxBytes` against the actual decoded byte
319
+ * length — this is the guard that reliably bounds the *embedded* size.
320
+ *
321
+ * Because stage 1 is unreliable, `specMaxBytes` bounds the size of the spec
322
+ * that gets embedded, not necessarily the size downloaded into memory.
323
+ */
324
+ async fetchSpecEmbed(apiId, versionId, specId, projectId) {
325
+ const spec = await this.apiHubClient.getSpec(
326
+ apiId,
327
+ versionId,
328
+ specId,
329
+ projectId
330
+ );
331
+ const limit = this.options.specMaxBytes ?? 512e3;
332
+ const size = spec.sizeBytes ?? 0;
333
+ if (size > limit) {
334
+ this.logger.warn(
335
+ `[catalog-module-apigee] Spec '${spec.name}' (${size} bytes) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`
336
+ );
337
+ return { hasSpec: true, specUrl: spec.name };
338
+ }
339
+ const contents = await this.apiHubClient.getSpecContents(
340
+ apiId,
341
+ versionId,
342
+ specId,
343
+ projectId
344
+ );
345
+ const base64 = contents.contents ?? "";
346
+ const definition = Buffer.from(base64, "base64").toString("utf-8");
347
+ const decodedBytes = Buffer.byteLength(definition, "utf-8");
348
+ const version = entityBuilder.extractSpecVersion(definition);
349
+ this.logger.debug(
350
+ `[catalog-module-apigee] Resolved spec '${spec.name}' (mimeType=${contents.mimeType ?? "?"}, ${decodedBytes} bytes decoded, metadata sizeBytes=${spec.sizeBytes ?? "n/a"})`
351
+ );
352
+ if (!base64) {
353
+ this.logger.warn(
354
+ `[catalog-module-apigee] Spec '${spec.name}' returned empty contents; treating as no embeddable spec`
355
+ );
356
+ return { hasSpec: false };
357
+ }
358
+ if (decodedBytes > limit) {
359
+ this.logger.warn(
360
+ `[catalog-module-apigee] Spec '${spec.name}' (${decodedBytes} bytes decoded) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`
361
+ );
362
+ return { hasSpec: true, specUrl: spec.name, version };
363
+ }
364
+ return { hasSpec: true, definition, version };
365
+ }
366
+ /**
367
+ * Resolves the spec for an API: lists versions, deterministically selects one
368
+ * (D-02), enumerates that version's specs (the versions endpoint does not
369
+ * reliably populate specIds), picks the lexicographically smallest spec ID,
370
+ * then fetches it with the size guard.
371
+ *
372
+ * Every successful resolution is cached in {@link lastGoodSpecs}. If a live
373
+ * resolution throws (e.g. API Hub is down while the owning org is served from
374
+ * cache), the last-known-good spec is returned so the entity keeps its
375
+ * embedded spec; with no prior spec, `undefined` is returned. Either way one
376
+ * bad spec never aborts the whole sync.
377
+ */
378
+ async resolveSpecForApi(apiId, projectId) {
379
+ const cacheKey = `${projectId}::${apiId}`;
380
+ try {
381
+ const versions = await this.apiHubClient.listVersions(apiId, projectId);
382
+ const selection = entityBuilder.selectSpec(versions);
383
+ if (!selection) return this.rememberSpec(cacheKey, { hasSpec: false });
384
+ const specs = await this.apiHubClient.listSpecs(
385
+ apiId,
386
+ selection.versionId,
387
+ projectId
388
+ );
389
+ const specId = specs.map((s) => s.name?.split("/").pop() ?? "").filter(Boolean).sort()[0];
390
+ if (!specId) return this.rememberSpec(cacheKey, { hasSpec: false });
391
+ const embed = await this.fetchSpecEmbed(
392
+ apiId,
393
+ selection.versionId,
394
+ specId,
395
+ projectId
396
+ );
397
+ return this.rememberSpec(cacheKey, embed);
398
+ } catch (e) {
399
+ const cached = this.lastGoodSpecs.get(cacheKey);
400
+ if (cached) {
401
+ this.lastGoodSpecs.delete(cacheKey);
402
+ this.lastGoodSpecs.set(cacheKey, cached);
403
+ this.logger.warn(
404
+ `[catalog-module-apigee] spec fetch failed for ${apiId}: ${e}; reusing last-known-good spec`
405
+ );
406
+ return cached;
407
+ }
408
+ this.logger.warn(
409
+ `[catalog-module-apigee] spec fetch failed for ${apiId}: ${e}`
410
+ );
411
+ return void 0;
412
+ }
413
+ }
414
+ /** Records a successfully-resolved spec (bounded LRU) and returns it unchanged. */
415
+ rememberSpec(key, embed) {
416
+ this.specCacheBytes = setSpecInBoundedCache(
417
+ this.lastGoodSpecs,
418
+ this.specCacheBytes,
419
+ key,
420
+ embed,
421
+ MAX_SPEC_CACHE_ENTRIES,
422
+ MAX_SPEC_CACHE_BYTES
423
+ );
424
+ return embed;
425
+ }
426
+ /**
427
+ * Fetches all data for one org: proxies, their deployments + revisions,
428
+ * API Hub type map, and sharedflows.
429
+ */
430
+ async fetchOrgData(orgCfg) {
431
+ const [proxies, apiHubApis, sharedflows] = await Promise.all([
432
+ this.apigeeClient.listProxies(orgCfg.name),
433
+ this.apiHubClient.listApis(orgCfg.projectId),
434
+ this.sharedflowClient.listSharedflows(orgCfg.name)
435
+ ]);
436
+ const apiTypeMap = /* @__PURE__ */ new Map();
437
+ for (const api of apiHubApis) {
438
+ const id = entityBuilder.slugify(api.displayName ?? api.name.split("/").pop() ?? "");
439
+ if (id && api.apiType) {
440
+ apiTypeMap.set(id, api.apiType);
441
+ }
442
+ }
443
+ const proxyMap = /* @__PURE__ */ new Map();
444
+ await Promise.all(
445
+ proxies.map(async (proxy) => {
446
+ try {
447
+ const latestRev = proxy.revision?.length ? proxy.revision[proxy.revision.length - 1] : void 0;
448
+ const [deployments, revision] = await Promise.all([
449
+ this.apigeeClient.listDeployments(orgCfg.name, proxy.name),
450
+ latestRev ? this.apigeeClient.getProxyRevision(
451
+ orgCfg.name,
452
+ proxy.name,
453
+ latestRev
454
+ ) : Promise.resolve({ name: proxy.name, basepaths: [] })
455
+ ]);
456
+ const readyDeps = deployments.filter(
457
+ (d) => d.state === void 0 || d.state === "READY"
458
+ );
459
+ const environments = readyDeps.map((d) => ({
460
+ name: d.environment,
461
+ revision: d.revision ?? ""
462
+ }));
463
+ const envLifecycle = this.options.environmentLifecycle;
464
+ const lifecycle = envLifecycle && Object.keys(envLifecycle).length > 0 ? entityBuilder.resolveLifecycleFromEnvironments(
465
+ environments.map((e) => e.name),
466
+ envLifecycle
467
+ ) : readyDeps.length > 0 ? "production" : "experimental";
468
+ const orgSyncData = {
469
+ slug: entityBuilder.slugify(orgCfg.name),
470
+ name: orgCfg.name,
471
+ projectId: orgCfg.projectId,
472
+ environments,
473
+ lifecycle
474
+ };
475
+ proxyMap.set(entityBuilder.slugify(proxy.name), {
476
+ proxy,
477
+ basePaths: revision.basepaths ?? [],
478
+ orgs: [orgSyncData]
479
+ });
480
+ } catch (e) {
481
+ this.logger.warn(
482
+ `[catalog-module-apigee] Skipping proxy '${proxy.name}' in org '${orgCfg.name}': ${e}`
483
+ );
484
+ }
485
+ })
486
+ );
487
+ return {
488
+ orgName: orgCfg.name,
489
+ proxyMap,
490
+ sharedflows,
491
+ apiTypeMap,
492
+ apiHubApis,
493
+ projectId: orgCfg.projectId,
494
+ githubOrgSlug: orgCfg.githubOrgSlug,
495
+ apiHubMappings: orgCfg.apiHubMappings
496
+ };
497
+ }
498
+ }
499
+
500
+ exports.ApigeeEntityProvider = ApigeeEntityProvider;
501
+ exports.setSpecInBoundedCache = setSpecInBoundedCache;
502
+ //# 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 resolveLifecycleFromEnvironments,\r\n extractSpecVersion,\r\n departmentLabel,\r\n type OrgSyncData,\r\n type EnvDeployment,\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 * Upper bounds on the resolved-spec cache (see {@link ApigeeEntityProvider}).\r\n * Current APIs are re-resolved every run and refreshed to most-recently-used, so\r\n * eviction only removes specs for APIs no longer in the catalogue — capping\r\n * memory as API ids churn across syncs.\r\n */\r\nconst MAX_SPEC_CACHE_ENTRIES = 1000;\r\nconst MAX_SPEC_CACHE_BYTES = 64 * 1024 * 1024; // 64 MB of retained spec text\r\n\r\n/** Approximate retained-byte cost of a cached spec (definition + spec-url). */\r\nfunction specEmbedBytes(embed: SpecEmbed): number {\r\n return (\r\n (embed.definition ? Buffer.byteLength(embed.definition, 'utf-8') : 0) +\r\n (embed.specUrl ? embed.specUrl.length : 0)\r\n );\r\n}\r\n\r\n/**\r\n * Inserts `embed` into `cache` as most-recently-used, then evicts the\r\n * least-recently-used entries until both the entry-count and total-byte bounds\r\n * hold. Returns the updated running byte total. `Map` iteration order is\r\n * insertion order, so the first key is always the least-recently-used.\r\n */\r\nexport function setSpecInBoundedCache(\r\n cache: Map<string, SpecEmbed>,\r\n currentBytes: number,\r\n key: string,\r\n embed: SpecEmbed,\r\n maxEntries: number,\r\n maxBytes: number,\r\n): number {\r\n let bytes = currentBytes;\r\n const existing = cache.get(key);\r\n if (existing !== undefined) {\r\n bytes -= specEmbedBytes(existing);\r\n cache.delete(key); // re-inserted below to mark most-recently-used\r\n }\r\n cache.set(key, embed);\r\n bytes += specEmbedBytes(embed);\r\n while (cache.size > maxEntries || bytes > maxBytes) {\r\n const oldest = cache.keys().next().value as string | undefined;\r\n if (oldest === undefined) break;\r\n const evicted = cache.get(oldest);\r\n cache.delete(oldest);\r\n if (evicted !== undefined) bytes -= specEmbedBytes(evicted);\r\n }\r\n return bytes;\r\n}\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 /**\r\n * Last successfully-fetched data per org name. Reused when a later sync of\r\n * that org fails, so a `type: 'full'` mutation never deletes a transiently\r\n * unavailable org's entities (they survive until that org syncs again).\r\n */\r\n private readonly lastGoodOrgData = new Map<string, OrgData>();\r\n\r\n /**\r\n * Last successfully-resolved spec per `${projectId}::${apiId}`. Reused when a\r\n * later live spec resolution throws (e.g. API Hub is down while an org is\r\n * served from {@link lastGoodOrgData}), so the entity keeps its embedded spec\r\n * instead of blinking to `has-spec: false`. Bounded by an LRU policy\r\n * (MAX_SPEC_CACHE_ENTRIES / MAX_SPEC_CACHE_BYTES).\r\n */\r\n private readonly lastGoodSpecs = new Map<string, SpecEmbed>();\r\n\r\n /** Running total of bytes retained in {@link lastGoodSpecs} (for LRU eviction). */\r\n private specCacheBytes = 0;\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. Reuse each failed org's last-known-good data so a `full` mutation\r\n * never deletes its entities; skip the run entirely if no org succeeds.\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 this.logger.info(\r\n `[catalog-module-apigee] Starting catalog sync for ${\r\n this.orgsConfig.length\r\n } org(s): ${this.orgsConfig.map(o => o.name).join(', ')}`,\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 results; reuse last-known-good on per-org failure ──\r\n // A `type: 'full'` mutation deletes any entity this provider emitted before\r\n // but omits now. So a transiently-failing org would have all its entities\r\n // deleted until its next good run (\"flapping\"). To prevent that, cache each\r\n // org's last successful data and substitute it when that org fails, so its\r\n // entities stay in the emitted set.\r\n const orgDataList: OrgData[] = [];\r\n let freshCount = 0;\r\n let reusedCount = 0;\r\n for (let i = 0; i < orgResults.length; i++) {\r\n const result = orgResults[i];\r\n const orgName = this.orgsConfig[i].name;\r\n if (result.status === 'fulfilled') {\r\n this.lastGoodOrgData.set(orgName, result.value);\r\n orgDataList.push(result.value);\r\n freshCount++;\r\n } else {\r\n const cached = this.lastGoodOrgData.get(orgName);\r\n if (cached) {\r\n reusedCount++;\r\n orgDataList.push(cached);\r\n this.logger.warn(\r\n `[catalog-module-apigee] Org '${orgName}' sync failed: ${result.reason}; ` +\r\n `reusing last-known-good data to keep its catalog entities`,\r\n );\r\n } else {\r\n this.logger.warn(\r\n `[catalog-module-apigee] Org '${orgName}' sync failed: ${result.reason}; ` +\r\n `no prior successful sync to fall back on — its entities are absent this run`,\r\n );\r\n }\r\n }\r\n }\r\n\r\n // ── Step 2b: Never emit a full mutation with no freshly-synced org ─────\r\n // If every org failed this run, skip applyMutation entirely and leave the\r\n // catalog exactly as it is. Emitting here would either wipe the catalog\r\n // (nothing cached) or needlessly re-emit stale data while risking stripped\r\n // spec embeds (specs are re-resolved live in later steps). The catalog\r\n // already holds the last-known-good set, so doing nothing is the safe path.\r\n if (freshCount === 0) {\r\n this.logger.error(\r\n `[catalog-module-apigee] All ${this.orgsConfig.length} org(s) failed to sync; ` +\r\n `skipping catalog mutation to preserve existing entities`,\r\n );\r\n return;\r\n }\r\n\r\n const totalProxies = orgDataList.reduce(\r\n (sum, o) => sum + o.proxyMap.size,\r\n 0,\r\n );\r\n const totalSharedflows = orgDataList.reduce(\r\n (sum, o) => sum + o.sharedflows.length,\r\n 0,\r\n );\r\n const totalHubApis = orgDataList.reduce(\r\n (sum, o) => sum + o.apiHubApis.length,\r\n 0,\r\n );\r\n this.logger.info(\r\n `[catalog-module-apigee] Synced ${freshCount}/${this.orgsConfig.length} org(s) fresh` +\r\n (reusedCount > 0 ? `, ${reusedCount} reused from cache` : '') +\r\n `: ${totalProxies} proxies, ${totalHubApis} API Hub definitions, ${totalSharedflows} sharedflows`,\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 const embed = matchedSpecs[i];\r\n // Spec version lives on the Component too (parity with the API entity).\r\n if (embed?.version) {\r\n item.component.metadata.annotations = {\r\n ...(item.component.metadata.annotations ?? {}),\r\n 'apigee.com/version': embed.version,\r\n };\r\n }\r\n // Department label parity with the API entity (from \"Business unit\").\r\n const department = departmentLabel(item.hubApi);\r\n if (department) {\r\n item.component.metadata.labels = {\r\n ...(item.component.metadata.labels ?? {}),\r\n department,\r\n };\r\n }\r\n entities.push(\r\n item.component,\r\n enrichMatchedApiEntity(\r\n item.api,\r\n item.hubApi,\r\n embed,\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 this.logger.info(\r\n `[catalog-module-apigee] Catalog sync complete — emitting ${entities.length} entities`,\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 const version = extractSpecVersion(definition);\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, version };\r\n }\r\n return { hasSpec: true, definition, version };\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.\r\n *\r\n * Every successful resolution is cached in {@link lastGoodSpecs}. If a live\r\n * resolution throws (e.g. API Hub is down while the owning org is served from\r\n * cache), the last-known-good spec is returned so the entity keeps its\r\n * embedded spec; with no prior spec, `undefined` is returned. Either way one\r\n * bad spec never aborts 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 const cacheKey = `${projectId}::${apiId}`;\r\n try {\r\n const versions = await this.apiHubClient.listVersions(apiId, projectId);\r\n const selection = selectSpec(versions);\r\n if (!selection) return this.rememberSpec(cacheKey, { hasSpec: false });\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 this.rememberSpec(cacheKey, { hasSpec: false });\r\n const embed = await this.fetchSpecEmbed(\r\n apiId,\r\n selection.versionId,\r\n specId,\r\n projectId,\r\n );\r\n return this.rememberSpec(cacheKey, embed);\r\n } catch (e) {\r\n const cached = this.lastGoodSpecs.get(cacheKey);\r\n if (cached) {\r\n // Refresh to most-recently-used so a still-present but persistently\r\n // failing API is not evicted while other specs are re-resolved.\r\n this.lastGoodSpecs.delete(cacheKey);\r\n this.lastGoodSpecs.set(cacheKey, cached);\r\n this.logger.warn(\r\n `[catalog-module-apigee] spec fetch failed for ${apiId}: ${e}; ` +\r\n `reusing last-known-good spec`,\r\n );\r\n return cached;\r\n }\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 /** Records a successfully-resolved spec (bounded LRU) and returns it unchanged. */\r\n private rememberSpec(key: string, embed: SpecEmbed): SpecEmbed {\r\n this.specCacheBytes = setSpecInBoundedCache(\r\n this.lastGoodSpecs,\r\n this.specCacheBytes,\r\n key,\r\n embed,\r\n MAX_SPEC_CACHE_ENTRIES,\r\n MAX_SPEC_CACHE_BYTES,\r\n );\r\n return embed;\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 // The deployments *list* endpoint omits `state` (Apigee only returns it\r\n // when fetching a single deployment), so absent state counts as deployed.\r\n const readyDeps = deployments.filter(\r\n d => d.state === undefined || d.state === 'READY',\r\n );\r\n const environments: EnvDeployment[] = readyDeps.map(d => ({\r\n name: d.environment,\r\n revision: d.revision ?? '',\r\n }));\r\n // Prefer the configured environment→lifecycle map; otherwise any ready\r\n // deployment marks the proxy as production.\r\n const envLifecycle = this.options.environmentLifecycle;\r\n const lifecycle =\r\n envLifecycle && Object.keys(envLifecycle).length > 0\r\n ? resolveLifecycleFromEnvironments(\r\n environments.map(e => e.name),\r\n envLifecycle,\r\n )\r\n : readyDeps.length > 0\r\n ? 'production'\r\n : 'experimental';\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","departmentLabel","enrichMatchedApiEntity","hubOnlyToEntity","sharedflowToEntity","extractSpecVersion","selectSpec","resolveLifecycleFromEnvironments"],"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;;;;;;"}