@nospt/backstage-plugin-apigee-backend 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,91 +1,91 @@
1
- # @nospt/backstage-plugin-apigee-backend
2
-
3
- Backend package for the bootstrap stage of the Backstage Apigee plugin.
4
-
5
- ## Included
6
-
7
- - New Backend System plugin registration
8
- - Minimal router mounted at `/api/apigee`
9
- - Placeholder health endpoint at `/api/apigee/health`
10
-
11
- ## Development
12
-
13
- ```bash
14
- cd plugins/apigee-backend
15
- yarn start
16
- yarn test
17
- yarn lint
18
- yarn build
19
- ```
20
-
21
- ## Repository Stitching
22
-
23
- A Backstage `CatalogProcessor` (`ApigeeStitchingProcessor`) links a backend
24
- `Component` to its proxy `API` entity by emitting the native `providesApi` /
25
- `apiProvidedBy` relations. This surfaces the standard **"Provided APIs"** card on
26
- the component page and the **"Providers"** card on the API page — with zero custom
27
- UI. A repository opts in by adding `nos.pt/apigee-*` annotations to its
28
- `catalog-info.yaml`.
29
-
30
- ### Annotation styles
31
-
32
- **Direct mapping (active in this release):**
33
-
34
- ```yaml
35
- apiVersion: backstage.io/v1alpha1
36
- kind: Component
37
- metadata:
38
- name: orders-service
39
- annotations:
40
- nos.pt/apigee-api-name: my-proxy
41
- nos.pt/apigee-project-id: apigee-x-public-p-123
42
- spec:
43
- type: service
44
- owner: team-orders
45
- lifecycle: production
46
- ```
47
-
48
- **TargetServer-based mapping (documented; resolution planned, not yet active):**
49
-
50
- ```yaml
51
- metadata:
52
- annotations:
53
- nos.pt/apigee-targetserver-hostname: backend.nos.pt
54
- nos.pt/apigee-targetserver-port: "443"
55
- ```
56
-
57
- ### Precedence rule
58
-
59
- When both styles are present, the **TargetServer-based mapping is the preferred**
60
- resolution path (STITCH-02). The direct API-name mapping is the **fallback**, used
61
- when no TargetServer annotation is present or when TargetServer resolution is
62
- ambiguous (for example, a generic load balancer shared by many proxies).
63
-
64
- > **In this release only the direct-mapping path is implemented.** TargetServer
65
- > resolution is planned but not yet active — no TargetServer data is captured by the
66
- > entity provider yet (see `04-CONTEXT` D-01). The processor is structured around a
67
- > resolution-strategy seam so the TargetServer branch slots in without rework.
68
-
69
- ### Apigee → Backstage mapping
70
-
71
- | Apigee concept | Backstage entity |
72
- |----------------|------------------|
73
- | API proxy (implementation) | `kind: Component`, `spec.type: api-proxy` |
74
- | API proxy (contract) | `kind: API`, `spec.type` from API Hub (default `other`) |
75
- | Sharedflow | `kind: Component`, `spec.type: library` |
76
- | Backend repo component ↔ proxy API | `providesApi` / `apiProvidedBy` relation |
77
-
78
- ### Resolution detail
79
-
80
- The target API entity ref is deterministic:
81
- `api:default/<slugify(nos.pt/apigee-api-name)>`. The processor reads the annotations,
82
- synthesizes the ref, and emits the relation pair — no live catalog lookup is needed.
83
- A partial annotation set (e.g. `nos.pt/apigee-api-name` without
84
- `nos.pt/apigee-project-id`) or an empty/unresolvable value is skipped with a warning
85
- (warn-and-skip), so no malformed relations are emitted.
86
-
87
- ### Example fixture
88
-
89
- See [`examples/components.yaml`](./examples/components.yaml) for ready-to-register
90
- sample `Component` entities exercising both annotation styles — useful for local
91
- smoke testing of the stitching processor.
1
+ # @nospt/backstage-plugin-apigee-backend
2
+
3
+ Backend package for the bootstrap stage of the Backstage Apigee plugin.
4
+
5
+ ## Included
6
+
7
+ - New Backend System plugin registration
8
+ - Minimal router mounted at `/api/apigee`
9
+ - Placeholder health endpoint at `/api/apigee/health`
10
+
11
+ ## Development
12
+
13
+ ```bash
14
+ cd plugins/apigee-backend
15
+ yarn start
16
+ yarn test
17
+ yarn lint
18
+ yarn build
19
+ ```
20
+
21
+ ## Repository Stitching
22
+
23
+ A Backstage `CatalogProcessor` (`ApigeeStitchingProcessor`) links a backend
24
+ `Component` to its proxy `API` entity by emitting the native `providesApi` /
25
+ `apiProvidedBy` relations. This surfaces the standard **"Provided APIs"** card on
26
+ the component page and the **"Providers"** card on the API page — with zero custom
27
+ UI. A repository opts in by adding `nos.pt/apigee-*` annotations to its
28
+ `catalog-info.yaml`.
29
+
30
+ ### Annotation styles
31
+
32
+ **Direct mapping (active in this release):**
33
+
34
+ ```yaml
35
+ apiVersion: backstage.io/v1alpha1
36
+ kind: Component
37
+ metadata:
38
+ name: orders-service
39
+ annotations:
40
+ nos.pt/apigee-api-name: my-proxy
41
+ nos.pt/apigee-project-id: apigee-x-public-p-123
42
+ spec:
43
+ type: service
44
+ owner: team-orders
45
+ lifecycle: production
46
+ ```
47
+
48
+ **TargetServer-based mapping (documented; resolution planned, not yet active):**
49
+
50
+ ```yaml
51
+ metadata:
52
+ annotations:
53
+ nos.pt/apigee-targetserver-hostname: backend.nos.pt
54
+ nos.pt/apigee-targetserver-port: "443"
55
+ ```
56
+
57
+ ### Precedence rule
58
+
59
+ When both styles are present, the **TargetServer-based mapping is the preferred**
60
+ resolution path (STITCH-02). The direct API-name mapping is the **fallback**, used
61
+ when no TargetServer annotation is present or when TargetServer resolution is
62
+ ambiguous (for example, a generic load balancer shared by many proxies).
63
+
64
+ > **In this release only the direct-mapping path is implemented.** TargetServer
65
+ > resolution is planned but not yet active — no TargetServer data is captured by the
66
+ > entity provider yet (see `04-CONTEXT` D-01). The processor is structured around a
67
+ > resolution-strategy seam so the TargetServer branch slots in without rework.
68
+
69
+ ### Apigee → Backstage mapping
70
+
71
+ | Apigee concept | Backstage entity |
72
+ |----------------|------------------|
73
+ | API proxy (implementation) | `kind: Component`, `spec.type: api-proxy` |
74
+ | API proxy (contract) | `kind: API`, `spec.type` from API Hub (default `other`) |
75
+ | Sharedflow | `kind: Component`, `spec.type: library` |
76
+ | Backend repo component ↔ proxy API | `providesApi` / `apiProvidedBy` relation |
77
+
78
+ ### Resolution detail
79
+
80
+ The target API entity ref is deterministic:
81
+ `api:default/<slugify(nos.pt/apigee-api-name)>`. The processor reads the annotations,
82
+ synthesizes the ref, and emits the relation pair — no live catalog lookup is needed.
83
+ A partial annotation set (e.g. `nos.pt/apigee-api-name` without
84
+ `nos.pt/apigee-project-id`) or an empty/unresolvable value is skipped with a warning
85
+ (warn-and-skip), so no malformed relations are emitted.
86
+
87
+ ### Example fixture
88
+
89
+ See [`examples/components.yaml`](./examples/components.yaml) for ready-to-register
90
+ sample `Component` entities exercising both annotation styles — useful for local
91
+ smoke testing of the stitching processor.
package/config.d.ts CHANGED
@@ -1,75 +1,75 @@
1
- export interface Config {
2
- /** Apigee catalog module configuration */
3
- apigee?: {
4
- /**
5
- * List of Apigee X organisations to sync.
6
- * At least one entry is required; the module fails fast at startup if missing.
7
- * @visibility backend
8
- */
9
- orgs: Array<{
10
- /** Human-readable label for this org */
11
- name: string;
12
- /** GCP project ID that owns this Apigee X organisation */
13
- projectId: string;
14
- /**
15
- * Optional list of environments to include (e.g. ["prod", "staging"]).
16
- * If omitted, all environments are synced.
17
- * @visibility backend
18
- */
19
- environments?: string[];
20
- /**
21
- * Optional GitHub organisation slug used to derive GitHub repo links
22
- * when API Hub does not supply a repositoryUri (FR17).
23
- * @visibility backend
24
- */
25
- githubOrgSlug?: string;
26
- /**
27
- * Explicit proxy-name -> API Hub apiId overrides for proxies whose slug
28
- * does not match their API Hub apiId (D-01).
29
- * @visibility backend
30
- */
31
- apiHubMappings?: { [proxyName: string]: string };
32
- }>;
33
- /**
34
- * Catalog sync interval in minutes. Default: 60.
35
- * @visibility backend
36
- */
37
- syncIntervalMinutes?: number;
38
- /**
39
- * Default spec.owner for API entities whose API Hub ownerTeam is absent.
40
- * Must be a Backstage entity reference, e.g. "group:default/platform-team".
41
- * @visibility backend
42
- */
43
- defaultOwner?: string;
44
- /**
45
- * Maximum inline spec size in bytes; specs larger than this are linked via
46
- * apigee.com/spec-url instead of embedded. Default 512000 (500 KB).
47
- * @visibility backend
48
- */
49
- specMaxBytes?: number;
50
- /**
51
- * API Hub location (GCP region) to query. Defaults to "global".
52
- * Use this when your API Hub instance is regional, e.g. "europe-west1".
53
- * @visibility backend
54
- */
55
- apiHubLocation?: string;
56
- /**
57
- * Optional custom schedule for the catalog sync task.
58
- * When provided, overrides syncIntervalMinutes.
59
- * @visibility backend
60
- */
61
- schedule?: {
62
- /** How often to run the sync. */
63
- frequency?: {
64
- cron?: string;
65
- minutes?: number;
66
- hours?: number;
67
- seconds?: number;
68
- };
69
- /** Maximum time allowed for a single sync run. */
70
- timeout?: { minutes?: number; seconds?: number };
71
- /** Delay before the first run after startup. */
72
- initialDelay?: { minutes?: number; seconds?: number };
73
- };
74
- };
75
- }
1
+ export interface Config {
2
+ /** Apigee catalog module configuration */
3
+ apigee?: {
4
+ /**
5
+ * List of Apigee X organisations to sync.
6
+ * At least one entry is required; the module fails fast at startup if missing.
7
+ * @visibility backend
8
+ */
9
+ orgs: Array<{
10
+ /** Human-readable label for this org */
11
+ name: string;
12
+ /** GCP project ID that owns this Apigee X organisation */
13
+ projectId: string;
14
+ /**
15
+ * Optional list of environments to include (e.g. ["prod", "staging"]).
16
+ * If omitted, all environments are synced.
17
+ * @visibility backend
18
+ */
19
+ environments?: string[];
20
+ /**
21
+ * Optional GitHub organisation slug used to derive GitHub repo links
22
+ * when API Hub does not supply a repositoryUri (FR17).
23
+ * @visibility backend
24
+ */
25
+ githubOrgSlug?: string;
26
+ /**
27
+ * Explicit proxy-name -> API Hub apiId overrides for proxies whose slug
28
+ * does not match their API Hub apiId (D-01).
29
+ * @visibility backend
30
+ */
31
+ apiHubMappings?: { [proxyName: string]: string };
32
+ }>;
33
+ /**
34
+ * Catalog sync interval in minutes. Default: 60.
35
+ * @visibility backend
36
+ */
37
+ syncIntervalMinutes?: number;
38
+ /**
39
+ * Default spec.owner for API entities whose API Hub ownerTeam is absent.
40
+ * Must be a Backstage entity reference, e.g. "group:default/platform-team".
41
+ * @visibility backend
42
+ */
43
+ defaultOwner?: string;
44
+ /**
45
+ * Maximum inline spec size in bytes; specs larger than this are linked via
46
+ * apigee.com/spec-url instead of embedded. Default 512000 (500 KB).
47
+ * @visibility backend
48
+ */
49
+ specMaxBytes?: number;
50
+ /**
51
+ * API Hub location (GCP region) to query. Defaults to "global".
52
+ * Use this when your API Hub instance is regional, e.g. "europe-west1".
53
+ * @visibility backend
54
+ */
55
+ apiHubLocation?: string;
56
+ /**
57
+ * Optional custom schedule for the catalog sync task.
58
+ * When provided, overrides syncIntervalMinutes.
59
+ * @visibility backend
60
+ */
61
+ schedule?: {
62
+ /** How often to run the sync. */
63
+ frequency?: {
64
+ cron?: string;
65
+ minutes?: number;
66
+ hours?: number;
67
+ seconds?: number;
68
+ };
69
+ /** Maximum time allowed for a single sync run. */
70
+ timeout?: { minutes?: number; seconds?: number };
71
+ /** Delay before the first run after startup. */
72
+ initialDelay?: { minutes?: number; seconds?: number };
73
+ };
74
+ };
75
+ }
package/dist/index.cjs.js CHANGED
File without changes
File without changes
package/dist/index.d.ts CHANGED
File without changes
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"api-hub-client.cjs.js","sources":["../../src/lib/api-hub-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 response types — only the fields consumed in Phases 1-3.\r\n// Full Cloud API Hub REST v1 reference:\r\n// https://cloud.google.com/apigee/docs/apihub/reference/rest/v1/projects.locations.apis\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Represents a single API definition from Cloud API Hub.\r\n * GET /projects/{p}/locations/{l}/apis\r\n */\r\nexport interface ApiHubApi {\r\n /** Full resource name: projects/{p}/locations/{l}/apis/{id} */\r\n name: string;\r\n /** Human-readable display name */\r\n displayName?: string;\r\n /** Description from API Hub */\r\n description?: string;\r\n /** API Hub lifecycle state: DRAFT | ACTIVE | DEPRECATED | ARCHIVED */\r\n businessUnit?: string;\r\n /** Owner team name (maps to spec.owner in Backstage) */\r\n owner?: { displayName?: string; email?: string };\r\n /**\r\n * GitHub / source repo URI from API Hub metadata.\r\n * Maps to metadata.links in Backstage (FR17 / META-10).\r\n */\r\n sourceMetadata?: Array<{ sourceType?: string; value?: string }>;\r\n /** Array of category/tag IDs applied to this API in API Hub (META-09) */\r\n categories?: string[];\r\n /** API Hub catalog type (e.g. \"openapi\", \"mcp\") — maps to spec.type in Backstage */\r\n apiType?: string;\r\n /** ISO 8601 create/update timestamps */\r\n createTime?: string;\r\n updateTime?: string;\r\n}\r\n\r\n/**\r\n * Represents one version of an API Hub API.\r\n * GET /projects/{p}/locations/{l}/apis/{apiId}/versions\r\n */\r\nexport interface ApiHubVersion {\r\n /** Full resource name: .../apis/{id}/versions/{vid} */\r\n name: string;\r\n displayName?: string;\r\n description?: string;\r\n /** Version state: DRAFT | ACTIVE | DEPRECATED | ARCHIVED */\r\n lifecycle?: { stage?: string };\r\n /** Spec IDs attached to this version */\r\n specIds?: string[];\r\n createTime?: string;\r\n updateTime?: string;\r\n}\r\n\r\n/**\r\n * Represents a spec (OpenAPI document) attached to a version.\r\n * GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs/{specId}\r\n */\r\nexport interface ApiHubSpec {\r\n /** Full resource name */\r\n name: string;\r\n /** MIME type (e.g. \"application/json\", \"application/yaml\") */\r\n mimeType?: string;\r\n /** File name (e.g. \"openapi.yaml\") */\r\n filename?: string;\r\n /** Hash of spec contents for deduplication */\r\n hash?: string;\r\n /** Size in bytes — used by spec size guard in Phase 3 */\r\n sizeBytes?: number;\r\n createTime?: string;\r\n updateTime?: string;\r\n}\r\n\r\n/**\r\n * Raw spec contents response from the :contents sub-resource.\r\n * `contents` is base64-encoded (Cloud API Hub ApiSpecContents resource).\r\n */\r\nexport interface ApiHubSpecContents {\r\n mimeType?: string;\r\n /** Base64-encoded spec file content */\r\n contents?: string;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Client\r\n// ---------------------------------------------------------------------------\r\n\r\nconst APIHUB_BASE_URL = 'https://apihub.googleapis.com/v1';\r\n\r\n/**\r\n * Read-only HTTP client for the Cloud API Hub REST v1 API.\r\n *\r\n * Mirrors ApigeeClient exactly (D-04): same constructor pattern, same\r\n * fetchWithRetry delegation, same encodeURIComponent usage.\r\n *\r\n * Auth: uses the same GCP Application Default Credentials GoogleAuth instance\r\n * that is shared with ApigeeClient (D-03).\r\n *\r\n * Usage:\r\n * ```typescript\r\n * const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });\r\n * const client = new ApiHubClient(auth, 'my-gcp-project');\r\n * const apis = await client.listApis();\r\n * ```\r\n */\r\nexport class ApiHubClient {\r\n /**\r\n * @param auth - Shared GoogleAuth instance (D-03: injected by module.ts, not created here)\r\n * @param projectId - GCP project that hosts the API Hub instance\r\n * @param location - API Hub location (default: \"global\" — override if regional instance)\r\n */\r\n constructor(\r\n private readonly auth: GoogleAuth,\r\n private readonly projectId: string = '',\r\n private readonly location: string = 'global',\r\n ) {}\r\n\r\n /** Base path prefix shared by all API Hub resource URLs for this instance. */\r\n private resourcePrefix(projectId?: string): string {\r\n const proj = projectId ?? this.projectId;\r\n return (\r\n `${APIHUB_BASE_URL}/projects/${encodeURIComponent(proj)}` +\r\n `/locations/${encodeURIComponent(this.location)}`\r\n );\r\n }\r\n\r\n /**\r\n * Fetches every page of a paginated API Hub list endpoint, following\r\n * `nextPageToken` until the collection is exhausted, and returns the\r\n * concatenated items. API Hub caps list responses (commonly at 50 entries per\r\n * page), so callers MUST page or risk silently truncating the catalogue.\r\n */\r\n private async fetchAllPages<T>(\r\n baseUrl: string,\r\n collection: 'apis' | 'versions' | 'specs',\r\n ): Promise<T[]> {\r\n const items: T[] = [];\r\n let pageToken: string | undefined;\r\n do {\r\n const sep = baseUrl.includes('?') ? '&' : '?';\r\n const url = pageToken\r\n ? `${baseUrl}${sep}pageToken=${encodeURIComponent(pageToken)}`\r\n : baseUrl;\r\n const body = (await fetchWithRetry(url, () =>\r\n this.auth.getAccessToken(),\r\n )) as Record<string, unknown>;\r\n const page = (body[collection] as T[] | undefined) ?? [];\r\n items.push(...page);\r\n pageToken =\r\n typeof body.nextPageToken === 'string' && body.nextPageToken.length > 0\r\n ? body.nextPageToken\r\n : undefined;\r\n } while (pageToken);\r\n return items;\r\n }\r\n\r\n /**\r\n * Lists all API definitions in this API Hub instance.\r\n * Calls: GET /projects/{p}/locations/{l}/apis\r\n *\r\n * @param projectId - Optional override for the GCP project ID. When provided,\r\n * overrides the instance-level projectId from the constructor. Allows a single\r\n * ApiHubClient instance to serve multiple GCP projects (multi-org setups).\r\n *\r\n * Pages through all results, following nextPageToken until exhausted (API Hub\r\n * caps list responses, commonly at 50 entries per page).\r\n */\r\n async listApis(projectId?: string): Promise<ApiHubApi[]> {\r\n const url = `${this.resourcePrefix(projectId)}/apis`;\r\n return this.fetchAllPages<ApiHubApi>(url, 'apis');\r\n }\r\n\r\n /**\r\n * Lists all versions for a given API.\r\n * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions\r\n *\r\n * @param apiId - Short API ID (last segment of the full resource name)\r\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\r\n */\r\n async listVersions(apiId: string, projectId?: string): Promise<ApiHubVersion[]> {\r\n const url = `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}/versions`;\r\n return this.fetchAllPages<ApiHubVersion>(url, 'versions');\r\n }\r\n\r\n /**\r\n * Lists all specs attached to a given API version.\r\n * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs\r\n *\r\n * This is the authoritative source of spec IDs for a version. The versions\r\n * list endpoint frequently returns version.specIds: null even when specs\r\n * exist, so callers must enumerate specs here rather than trusting specIds.\r\n *\r\n * @param apiId - Short API ID (last segment of the full resource name)\r\n * @param versionId - Short version ID (last segment of the version resource name)\r\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\r\n */\r\n async listSpecs(\r\n apiId: string,\r\n versionId: string,\r\n projectId?: string,\r\n ): Promise<ApiHubSpec[]> {\r\n const url =\r\n `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}` +\r\n `/versions/${encodeURIComponent(versionId)}/specs`;\r\n return this.fetchAllPages<ApiHubSpec>(url, 'specs');\r\n }\r\n\r\n /**\r\n * Fetches the spec metadata for a given API version and spec ID.\r\n * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs/{specId}\r\n *\r\n * To retrieve the actual spec file content (base64), call getSpecContents() instead.\r\n *\r\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\r\n */\r\n async getSpec(\r\n apiId: string,\r\n versionId: string,\r\n specId: string,\r\n projectId?: string,\r\n ): Promise<ApiHubSpec> {\r\n const url =\r\n `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}` +\r\n `/versions/${encodeURIComponent(versionId)}` +\r\n `/specs/${encodeURIComponent(specId)}`;\r\n return (await fetchWithRetry(url, () => this.auth.getAccessToken())) as ApiHubSpec;\r\n }\r\n\r\n /**\r\n * Fetches the raw spec file content for a given spec resource.\r\n * Calls: GET .../specs/{specId}:contents\r\n * Returns base64-encoded content in the `contents` field — caller must decode\r\n * with Buffer.from(contents, 'base64').toString('utf-8').\r\n *\r\n * Used in Phase 3 for spec.definition population (with 500 KB size guard).\r\n *\r\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\r\n */\r\n async getSpecContents(\r\n apiId: string,\r\n versionId: string,\r\n specId: string,\r\n projectId?: string,\r\n ): Promise<ApiHubSpecContents> {\r\n const url =\r\n `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}` +\r\n `/versions/${encodeURIComponent(versionId)}` +\r\n `/specs/${encodeURIComponent(specId)}:contents`;\r\n return (await fetchWithRetry(url, () => this.auth.getAccessToken())) as ApiHubSpecContents;\r\n }\r\n}\r\n"],"names":["fetchWithRetry"],"mappings":"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"}
1
+ {"version":3,"file":"api-hub-client.cjs.js","sources":["../../src/lib/api-hub-client.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { GoogleAuth } from 'google-auth-library';\nimport { fetchWithRetry } from './fetch-utils';\n\n// ---------------------------------------------------------------------------\n// Minimal response types — only the fields consumed in Phases 1-3.\n// Full Cloud API Hub REST v1 reference:\n// https://cloud.google.com/apigee/docs/apihub/reference/rest/v1/projects.locations.apis\n// ---------------------------------------------------------------------------\n\n/**\n * Represents a single API definition from Cloud API Hub.\n * GET /projects/{p}/locations/{l}/apis\n */\nexport interface ApiHubApi {\n /** Full resource name: projects/{p}/locations/{l}/apis/{id} */\n name: string;\n /** Human-readable display name */\n displayName?: string;\n /** Description from API Hub */\n description?: string;\n /** API Hub lifecycle state: DRAFT | ACTIVE | DEPRECATED | ARCHIVED */\n businessUnit?: string;\n /** Owner team name (maps to spec.owner in Backstage) */\n owner?: { displayName?: string; email?: string };\n /**\n * GitHub / source repo URI from API Hub metadata.\n * Maps to metadata.links in Backstage (FR17 / META-10).\n */\n sourceMetadata?: Array<{ sourceType?: string; value?: string }>;\n /** Array of category/tag IDs applied to this API in API Hub (META-09) */\n categories?: string[];\n /** API Hub catalog type (e.g. \"openapi\", \"mcp\") — maps to spec.type in Backstage */\n apiType?: string;\n /** ISO 8601 create/update timestamps */\n createTime?: string;\n updateTime?: string;\n}\n\n/**\n * Represents one version of an API Hub API.\n * GET /projects/{p}/locations/{l}/apis/{apiId}/versions\n */\nexport interface ApiHubVersion {\n /** Full resource name: .../apis/{id}/versions/{vid} */\n name: string;\n displayName?: string;\n description?: string;\n /** Version state: DRAFT | ACTIVE | DEPRECATED | ARCHIVED */\n lifecycle?: { stage?: string };\n /** Spec IDs attached to this version */\n specIds?: string[];\n createTime?: string;\n updateTime?: string;\n}\n\n/**\n * Represents a spec (OpenAPI document) attached to a version.\n * GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs/{specId}\n */\nexport interface ApiHubSpec {\n /** Full resource name */\n name: string;\n /** MIME type (e.g. \"application/json\", \"application/yaml\") */\n mimeType?: string;\n /** File name (e.g. \"openapi.yaml\") */\n filename?: string;\n /** Hash of spec contents for deduplication */\n hash?: string;\n /** Size in bytes — used by spec size guard in Phase 3 */\n sizeBytes?: number;\n createTime?: string;\n updateTime?: string;\n}\n\n/**\n * Raw spec contents response from the :contents sub-resource.\n * `contents` is base64-encoded (Cloud API Hub ApiSpecContents resource).\n */\nexport interface ApiHubSpecContents {\n mimeType?: string;\n /** Base64-encoded spec file content */\n contents?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\nconst APIHUB_BASE_URL = 'https://apihub.googleapis.com/v1';\n\n/**\n * Read-only HTTP client for the Cloud API Hub REST v1 API.\n *\n * Mirrors ApigeeClient exactly (D-04): same constructor pattern, same\n * fetchWithRetry delegation, same encodeURIComponent usage.\n *\n * Auth: uses the same GCP Application Default Credentials GoogleAuth instance\n * that is shared with ApigeeClient (D-03).\n *\n * Usage:\n * ```typescript\n * const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });\n * const client = new ApiHubClient(auth, 'my-gcp-project');\n * const apis = await client.listApis();\n * ```\n */\nexport class ApiHubClient {\n /**\n * @param auth - Shared GoogleAuth instance (D-03: injected by module.ts, not created here)\n * @param projectId - GCP project that hosts the API Hub instance\n * @param location - API Hub location (default: \"global\" — override if regional instance)\n */\n constructor(\n private readonly auth: GoogleAuth,\n private readonly projectId: string = '',\n private readonly location: string = 'global',\n ) {}\n\n /** Base path prefix shared by all API Hub resource URLs for this instance. */\n private resourcePrefix(projectId?: string): string {\n const proj = projectId ?? this.projectId;\n return (\n `${APIHUB_BASE_URL}/projects/${encodeURIComponent(proj)}` +\n `/locations/${encodeURIComponent(this.location)}`\n );\n }\n\n /**\n * Fetches every page of a paginated API Hub list endpoint, following\n * `nextPageToken` until the collection is exhausted, and returns the\n * concatenated items. API Hub caps list responses (commonly at 50 entries per\n * page), so callers MUST page or risk silently truncating the catalogue.\n */\n private async fetchAllPages<T>(\n baseUrl: string,\n collection: 'apis' | 'versions' | 'specs',\n ): Promise<T[]> {\n const items: T[] = [];\n let pageToken: string | undefined;\n do {\n const sep = baseUrl.includes('?') ? '&' : '?';\n const url = pageToken\n ? `${baseUrl}${sep}pageToken=${encodeURIComponent(pageToken)}`\n : baseUrl;\n const body = (await fetchWithRetry(url, () =>\n this.auth.getAccessToken(),\n )) as Record<string, unknown>;\n const page = (body[collection] as T[] | undefined) ?? [];\n items.push(...page);\n pageToken =\n typeof body.nextPageToken === 'string' && body.nextPageToken.length > 0\n ? body.nextPageToken\n : undefined;\n } while (pageToken);\n return items;\n }\n\n /**\n * Lists all API definitions in this API Hub instance.\n * Calls: GET /projects/{p}/locations/{l}/apis\n *\n * @param projectId - Optional override for the GCP project ID. When provided,\n * overrides the instance-level projectId from the constructor. Allows a single\n * ApiHubClient instance to serve multiple GCP projects (multi-org setups).\n *\n * Pages through all results, following nextPageToken until exhausted (API Hub\n * caps list responses, commonly at 50 entries per page).\n */\n async listApis(projectId?: string): Promise<ApiHubApi[]> {\n const url = `${this.resourcePrefix(projectId)}/apis`;\n return this.fetchAllPages<ApiHubApi>(url, 'apis');\n }\n\n /**\n * Lists all versions for a given API.\n * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions\n *\n * @param apiId - Short API ID (last segment of the full resource name)\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\n */\n async listVersions(apiId: string, projectId?: string): Promise<ApiHubVersion[]> {\n const url = `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}/versions`;\n return this.fetchAllPages<ApiHubVersion>(url, 'versions');\n }\n\n /**\n * Lists all specs attached to a given API version.\n * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs\n *\n * This is the authoritative source of spec IDs for a version. The versions\n * list endpoint frequently returns version.specIds: null even when specs\n * exist, so callers must enumerate specs here rather than trusting specIds.\n *\n * @param apiId - Short API ID (last segment of the full resource name)\n * @param versionId - Short version ID (last segment of the version resource name)\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\n */\n async listSpecs(\n apiId: string,\n versionId: string,\n projectId?: string,\n ): Promise<ApiHubSpec[]> {\n const url =\n `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}` +\n `/versions/${encodeURIComponent(versionId)}/specs`;\n return this.fetchAllPages<ApiHubSpec>(url, 'specs');\n }\n\n /**\n * Fetches the spec metadata for a given API version and spec ID.\n * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs/{specId}\n *\n * To retrieve the actual spec file content (base64), call getSpecContents() instead.\n *\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\n */\n async getSpec(\n apiId: string,\n versionId: string,\n specId: string,\n projectId?: string,\n ): Promise<ApiHubSpec> {\n const url =\n `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}` +\n `/versions/${encodeURIComponent(versionId)}` +\n `/specs/${encodeURIComponent(specId)}`;\n return (await fetchWithRetry(url, () => this.auth.getAccessToken())) as ApiHubSpec;\n }\n\n /**\n * Fetches the raw spec file content for a given spec resource.\n * Calls: GET .../specs/{specId}:contents\n * Returns base64-encoded content in the `contents` field — caller must decode\n * with Buffer.from(contents, 'base64').toString('utf-8').\n *\n * Used in Phase 3 for spec.definition population (with 500 KB size guard).\n *\n * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.\n */\n async getSpecContents(\n apiId: string,\n versionId: string,\n specId: string,\n projectId?: string,\n ): Promise<ApiHubSpecContents> {\n const url =\n `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}` +\n `/versions/${encodeURIComponent(versionId)}` +\n `/specs/${encodeURIComponent(specId)}:contents`;\n return (await fetchWithRetry(url, () => this.auth.getAccessToken())) as ApiHubSpecContents;\n }\n}\n"],"names":["fetchWithRetry"],"mappings":"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"}
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"apigee-client.cjs.js","sources":["../../src/lib/apigee-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 response types — only the fields consumed in Phases 1-3.\r\n// Full Apigee Management API response shapes are at:\r\n// https://cloud.google.com/apigee/docs/reference/apis/apigee/rest/v1/organizations.apis\r\n// ---------------------------------------------------------------------------\r\n\r\n/** Represents a single Apigee API proxy entry from GET /organizations/{org}/apis */\r\nexport interface ApigeeProxy {\r\n /** Proxy name as registered in Apigee (may contain dots/underscores — slugify before use as metadata.name) */\r\n name: string;\r\n /** Array of revision numbers (strings), most recent last */\r\n revision?: string[];\r\n /** ISO 8601 creation timestamp */\r\n createdAt?: string;\r\n /** ISO 8601 last-modified timestamp */\r\n lastModifiedAt?: string;\r\n}\r\n\r\n/** Represents one environment deployment from GET /organizations/{org}/apis/{proxy}/deployments */\r\nexport interface ApigeeDeployment {\r\n /** Environment name (e.g. \"prod\", \"staging\") */\r\n environment: string;\r\n /** Deployed revision number */\r\n apiRevision?: string;\r\n /** Deployment state: READY | PROGRESSING | ERROR */\r\n deploymentStatus?: string;\r\n}\r\n\r\n/**\r\n * Represents a single proxy revision detail from\r\n * GET /organizations/{org}/apis/{proxy}/revisions/{rev}\r\n */\r\nexport interface ApigeeProxyRevision {\r\n name: string;\r\n revision?: string;\r\n /** Base paths declared in the proxy bundle (used for apigee.com/base-path annotation) */\r\n basepaths?: string[];\r\n description?: string;\r\n /** Resource files attached to this revision */\r\n resourceFiles?: { resourceFile?: Array<{ name: string; type: string }> };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Client\r\n// ---------------------------------------------------------------------------\r\n\r\nconst APIGEE_BASE_URL = 'https://apigee.googleapis.com/v1';\r\n\r\n/**\r\n * Read-only HTTP client for the Apigee Management API v1.\r\n *\r\n * Auth: uses GCP Application Default Credentials via the injected GoogleAuth\r\n * instance. All calls are GET-only (AUTH-02: read-only IAM roles).\r\n *\r\n * Usage:\r\n * ```typescript\r\n * const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });\r\n * const client = new ApigeeClient(auth);\r\n * const proxies = await client.listProxies('my-apigee-org');\r\n * ```\r\n */\r\nexport class ApigeeClient {\r\n constructor(private readonly auth: GoogleAuth) {}\r\n\r\n /**\r\n * Lists all API proxies in the given Apigee organisation.\r\n * Calls: GET /organizations/{org}/apis\r\n */\r\n async listProxies(org: string): Promise<ApigeeProxy[]> {\r\n const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis`;\r\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\r\n return (body as { proxies?: ApigeeProxy[] }).proxies ?? [];\r\n }\r\n\r\n /**\r\n * Lists all environment deployments for a given proxy.\r\n * Calls: GET /organizations/{org}/apis/{proxy}/deployments\r\n */\r\n async listDeployments(org: string, proxy: string): Promise<ApigeeDeployment[]> {\r\n const url =\r\n `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}` +\r\n `/apis/${encodeURIComponent(proxy)}/deployments`;\r\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\r\n return (body as { deployments?: ApigeeDeployment[] }).deployments ?? [];\r\n }\r\n\r\n /**\r\n * Fetches full detail for a specific proxy revision.\r\n * Calls: GET /organizations/{org}/apis/{proxy}/revisions/{rev}\r\n */\r\n async getProxyRevision(\r\n org: string,\r\n proxy: string,\r\n rev: string,\r\n ): Promise<ApigeeProxyRevision> {\r\n const url =\r\n `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}` +\r\n `/apis/${encodeURIComponent(proxy)}/revisions/${encodeURIComponent(rev)}`;\r\n return (await fetchWithRetry(url, () => this.auth.getAccessToken())) as ApigeeProxyRevision;\r\n }\r\n}\r\n"],"names":["fetchWithRetry"],"mappings":";;;;AAkDA,MAAM,eAAA,GAAkB,kCAAA;AAejB,MAAM,YAAA,CAAa;AAAA,EACxB,YAA6B,IAAA,EAAkB;AAAlB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAmB;AAAA,EAAnB,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B,MAAM,YAAY,GAAA,EAAqC;AACrD,IAAA,MAAM,MAAM,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,kBAAA,CAAmB,GAAG,CAAC,CAAA,KAAA,CAAA;AACvE,IAAA,MAAM,IAAA,GAAO,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AACvE,IAAA,OAAQ,IAAA,CAAqC,WAAW,EAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAA,CAAgB,GAAA,EAAa,KAAA,EAA4C;AAC7E,IAAA,MAAM,GAAA,GACJ,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,kBAAA,CAAmB,GAAG,CAAC,CAAA,MAAA,EAClD,kBAAA,CAAmB,KAAK,CAAC,CAAA,YAAA,CAAA;AACpC,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;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAA,CACJ,GAAA,EACA,KAAA,EACA,GAAA,EAC8B;AAC9B,IAAA,MAAM,GAAA,GACJ,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,mBAAmB,GAAG,CAAC,CAAA,MAAA,EAClD,kBAAA,CAAmB,KAAK,CAAC,CAAA,WAAA,EAAc,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAA;AACzE,IAAA,OAAQ,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AAAA,EACpE;AACF;;;;"}
1
+ {"version":3,"file":"apigee-client.cjs.js","sources":["../../src/lib/apigee-client.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { GoogleAuth } from 'google-auth-library';\nimport { fetchWithRetry } from './fetch-utils';\n\n// ---------------------------------------------------------------------------\n// Minimal response types — only the fields consumed in Phases 1-3.\n// Full Apigee Management API response shapes are at:\n// https://cloud.google.com/apigee/docs/reference/apis/apigee/rest/v1/organizations.apis\n// ---------------------------------------------------------------------------\n\n/** Represents a single Apigee API proxy entry from GET /organizations/{org}/apis */\nexport interface ApigeeProxy {\n /** Proxy name as registered in Apigee (may contain dots/underscores — slugify before use as metadata.name) */\n name: string;\n /** Array of revision numbers (strings), most recent last */\n revision?: string[];\n /** ISO 8601 creation timestamp */\n createdAt?: string;\n /** ISO 8601 last-modified timestamp */\n lastModifiedAt?: string;\n}\n\n/** Represents one environment deployment from GET /organizations/{org}/apis/{proxy}/deployments */\nexport interface ApigeeDeployment {\n /** Environment name (e.g. \"prod\", \"staging\") */\n environment: string;\n /** Deployed revision number */\n apiRevision?: string;\n /** Deployment state: READY | PROGRESSING | ERROR */\n deploymentStatus?: string;\n}\n\n/**\n * Represents a single proxy revision detail from\n * GET /organizations/{org}/apis/{proxy}/revisions/{rev}\n */\nexport interface ApigeeProxyRevision {\n name: string;\n revision?: string;\n /** Base paths declared in the proxy bundle (used for apigee.com/base-path annotation) */\n basepaths?: string[];\n description?: string;\n /** Resource files attached to this revision */\n resourceFiles?: { resourceFile?: Array<{ name: string; type: string }> };\n}\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\nconst APIGEE_BASE_URL = 'https://apigee.googleapis.com/v1';\n\n/**\n * Read-only HTTP client for the Apigee Management API v1.\n *\n * Auth: uses GCP Application Default Credentials via the injected GoogleAuth\n * instance. All calls are GET-only (AUTH-02: read-only IAM roles).\n *\n * Usage:\n * ```typescript\n * const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });\n * const client = new ApigeeClient(auth);\n * const proxies = await client.listProxies('my-apigee-org');\n * ```\n */\nexport class ApigeeClient {\n constructor(private readonly auth: GoogleAuth) {}\n\n /**\n * Lists all API proxies in the given Apigee organisation.\n * Calls: GET /organizations/{org}/apis\n */\n async listProxies(org: string): Promise<ApigeeProxy[]> {\n const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis`;\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\n return (body as { proxies?: ApigeeProxy[] }).proxies ?? [];\n }\n\n /**\n * Lists all environment deployments for a given proxy.\n * Calls: GET /organizations/{org}/apis/{proxy}/deployments\n */\n async listDeployments(org: string, proxy: string): Promise<ApigeeDeployment[]> {\n const url =\n `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}` +\n `/apis/${encodeURIComponent(proxy)}/deployments`;\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\n return (body as { deployments?: ApigeeDeployment[] }).deployments ?? [];\n }\n\n /**\n * Fetches full detail for a specific proxy revision.\n * Calls: GET /organizations/{org}/apis/{proxy}/revisions/{rev}\n */\n async getProxyRevision(\n org: string,\n proxy: string,\n rev: string,\n ): Promise<ApigeeProxyRevision> {\n const url =\n `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}` +\n `/apis/${encodeURIComponent(proxy)}/revisions/${encodeURIComponent(rev)}`;\n return (await fetchWithRetry(url, () => this.auth.getAccessToken())) as ApigeeProxyRevision;\n }\n}\n"],"names":["fetchWithRetry"],"mappings":";;;;AAkDA,MAAM,eAAA,GAAkB,kCAAA;AAejB,MAAM,YAAA,CAAa;AAAA,EACxB,YAA6B,IAAA,EAAkB;AAAlB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAmB;AAAA,EAAnB,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B,MAAM,YAAY,GAAA,EAAqC;AACrD,IAAA,MAAM,MAAM,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,kBAAA,CAAmB,GAAG,CAAC,CAAA,KAAA,CAAA;AACvE,IAAA,MAAM,IAAA,GAAO,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AACvE,IAAA,OAAQ,IAAA,CAAqC,WAAW,EAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAA,CAAgB,GAAA,EAAa,KAAA,EAA4C;AAC7E,IAAA,MAAM,GAAA,GACJ,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,kBAAA,CAAmB,GAAG,CAAC,CAAA,MAAA,EAClD,kBAAA,CAAmB,KAAK,CAAC,CAAA,YAAA,CAAA;AACpC,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;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAA,CACJ,GAAA,EACA,KAAA,EACA,GAAA,EAC8B;AAC9B,IAAA,MAAM,GAAA,GACJ,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,mBAAmB,GAAG,CAAC,CAAA,MAAA,EAClD,kBAAA,CAAmB,KAAK,CAAC,CAAA,WAAA,EAAc,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAA;AACzE,IAAA,OAAQ,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AAAA,EACpE;AACF;;;;"}
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"apigee-stitching-processor.cjs.js","sources":["../../src/lib/apigee-stitching-processor.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n RELATION_PROVIDES_API,\r\n RELATION_API_PROVIDED_BY,\r\n type Entity,\r\n} from '@backstage/catalog-model';\r\nimport {\r\n processingResult,\r\n type CatalogProcessor,\r\n type CatalogProcessorEmit,\r\n type LocationSpec,\r\n} from '@backstage/plugin-catalog-node';\r\nimport type { LoggerService } from '@backstage/backend-plugin-api';\r\nimport { slugify } from './entity-builder';\r\n\r\n/**\r\n * Annotation that names the Apigee proxy / API this Component provides.\r\n * Used by the direct-mapping resolution path.\r\n */\r\nexport const ANNOTATION_API_NAME = 'nos.pt/apigee-api-name';\r\n\r\n/**\r\n * Annotation carrying the owning GCP/Apigee project id. Required (alongside\r\n * {@link ANNOTATION_API_NAME}) for the direct-mapping path to disambiguate intent.\r\n */\r\nexport const ANNOTATION_PROJECT_ID = 'nos.pt/apigee-project-id';\r\n\r\n/**\r\n * Annotation carrying the TargetServer hostname. Reserved for the deferred\r\n * TargetServer-preferred resolution path (STITCH-02); not resolved this phase.\r\n */\r\nexport const ANNOTATION_TARGETSERVER_HOST = 'nos.pt/apigee-targetserver-hostname';\r\n\r\n/** Compound ref to a Backstage entity. */\r\ninterface EntityRef {\r\n kind: string;\r\n namespace: string;\r\n name: string;\r\n}\r\n\r\n/**\r\n * A Backstage {@link CatalogProcessor} that links a backend `Component` to its\r\n * proxy `API` entity by emitting the native `providesApi` / `apiProvidedBy`\r\n * relation pair, surfacing the standard \"Provided APIs\" / \"Providers\" cards\r\n * with zero custom UI (STITCH-01).\r\n *\r\n * Resolution dispatches through a strategy seam: a TargetServer-first path\r\n * (deferred — STITCH-02, see 04-CONTEXT D-01) followed by the active\r\n * direct-mapping path. The first strategy to return a target ref wins.\r\n */\r\nexport class ApigeeStitchingProcessor implements CatalogProcessor {\r\n constructor(private readonly logger: LoggerService) {}\r\n\r\n getProcessorName(): string {\r\n return 'ApigeeStitchingProcessor';\r\n }\r\n\r\n async postProcessEntity(\r\n entity: Entity,\r\n _location: LocationSpec,\r\n emit: CatalogProcessorEmit,\r\n ): Promise<Entity> {\r\n // The processor only stitches Components.\r\n if (entity.kind !== 'Component') {\r\n return entity;\r\n }\r\n\r\n const annotations = entity.metadata.annotations ?? {};\r\n\r\n // Untouched component: no nos.pt/apigee-* annotations at all → no-op, no warning.\r\n const hasAnyApigeeAnnotation = Object.keys(annotations).some(key =>\r\n key.startsWith('nos.pt/apigee-'),\r\n );\r\n if (!hasAnyApigeeAnnotation) {\r\n return entity;\r\n }\r\n\r\n const target = this.resolveTarget(annotations, entity);\r\n if (!target) {\r\n // A warning (if applicable) was already logged by the resolution strategy.\r\n return entity;\r\n }\r\n\r\n const componentRef: EntityRef = {\r\n kind: 'Component',\r\n namespace: entity.metadata.namespace ?? 'default',\r\n name: entity.metadata.name,\r\n };\r\n\r\n emit(\r\n processingResult.relation({\r\n source: componentRef,\r\n target,\r\n type: RELATION_PROVIDES_API,\r\n }),\r\n );\r\n emit(\r\n processingResult.relation({\r\n source: target,\r\n target: componentRef,\r\n type: RELATION_API_PROVIDED_BY,\r\n }),\r\n );\r\n\r\n // Relation-only work: never mutate the entity.\r\n return entity;\r\n }\r\n\r\n /**\r\n * Strategy seam. Tries the TargetServer-preferred path first (deferred this\r\n * phase), then falls back to the active direct-mapping path. First strategy\r\n * to return a ref wins.\r\n */\r\n private resolveTarget(\r\n annotations: Record<string, string>,\r\n entity: Entity,\r\n ): EntityRef | undefined {\r\n return (\r\n this.resolveViaTargetServer(annotations, entity) ??\r\n this.resolveViaDirect(annotations, entity)\r\n );\r\n }\r\n\r\n /**\r\n * Deferred: TargetServer-preferred path (STITCH-02). See 04-CONTEXT D-01.\r\n * No TargetServer data exists in the codebase yet, so this is the clean\r\n * insertion point for the future branch and intentionally returns undefined.\r\n */\r\n private resolveViaTargetServer(\r\n _annotations: Record<string, string>,\r\n _entity: Entity,\r\n ): undefined {\r\n return undefined;\r\n }\r\n\r\n /**\r\n * Direct-mapping path. Requires BOTH the api-name and project-id annotations.\r\n * The proxy API entity ref is deterministic — `api:default/<slugify(api-name)>` —\r\n * so no live catalog lookup is needed (D-01). projectId is validated for\r\n * presence to confirm intent but is not part of the deterministic ref.\r\n */\r\n private resolveViaDirect(\r\n annotations: Record<string, string>,\r\n entity: Entity,\r\n ): EntityRef | undefined {\r\n const apiName = annotations[ANNOTATION_API_NAME]?.trim();\r\n const projectId = annotations[ANNOTATION_PROJECT_ID]?.trim();\r\n\r\n const hasApiName = Boolean(apiName);\r\n const hasProjectId = Boolean(projectId);\r\n\r\n if (hasApiName !== hasProjectId) {\r\n const missing = hasApiName ? ANNOTATION_PROJECT_ID : ANNOTATION_API_NAME;\r\n this.logger.warn(\r\n `[ApigeeStitchingProcessor] Component \"${entity.metadata.name}\" has a partial ` +\r\n `Apigee direct-mapping annotation set; missing \"${missing}\". Skipping stitching.`,\r\n );\r\n return undefined;\r\n }\r\n\r\n if (!hasApiName) {\r\n // Neither direct annotation present — defer silently (e.g. TargetServer-only\r\n // annotations were supplied, handled by the deferred path).\r\n return undefined;\r\n }\r\n\r\n const slug = slugify(apiName as string);\r\n if (slug === '') {\r\n this.logger.warn(\r\n `[ApigeeStitchingProcessor] Component \"${entity.metadata.name}\" has an ` +\r\n `unresolvable \"${ANNOTATION_API_NAME}\" value; nothing to stitch.`,\r\n );\r\n return undefined;\r\n }\r\n\r\n return { kind: 'API', namespace: 'default', name: slug };\r\n }\r\n}\r\n"],"names":["processingResult","RELATION_PROVIDES_API","RELATION_API_PROVIDED_BY","slugify"],"mappings":"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"}
1
+ {"version":3,"file":"apigee-stitching-processor.cjs.js","sources":["../../src/lib/apigee-stitching-processor.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n RELATION_PROVIDES_API,\n RELATION_API_PROVIDED_BY,\n type Entity,\n} from '@backstage/catalog-model';\nimport {\n processingResult,\n type CatalogProcessor,\n type CatalogProcessorEmit,\n type LocationSpec,\n} from '@backstage/plugin-catalog-node';\nimport type { LoggerService } from '@backstage/backend-plugin-api';\nimport { slugify } from './entity-builder';\n\n/**\n * Annotation that names the Apigee proxy / API this Component provides.\n * Used by the direct-mapping resolution path.\n */\nexport const ANNOTATION_API_NAME = 'nos.pt/apigee-api-name';\n\n/**\n * Annotation carrying the owning GCP/Apigee project id. Required (alongside\n * {@link ANNOTATION_API_NAME}) for the direct-mapping path to disambiguate intent.\n */\nexport const ANNOTATION_PROJECT_ID = 'nos.pt/apigee-project-id';\n\n/**\n * Annotation carrying the TargetServer hostname. Reserved for the deferred\n * TargetServer-preferred resolution path (STITCH-02); not resolved this phase.\n */\nexport const ANNOTATION_TARGETSERVER_HOST = 'nos.pt/apigee-targetserver-hostname';\n\n/** Compound ref to a Backstage entity. */\ninterface EntityRef {\n kind: string;\n namespace: string;\n name: string;\n}\n\n/**\n * A Backstage {@link CatalogProcessor} that links a backend `Component` to its\n * proxy `API` entity by emitting the native `providesApi` / `apiProvidedBy`\n * relation pair, surfacing the standard \"Provided APIs\" / \"Providers\" cards\n * with zero custom UI (STITCH-01).\n *\n * Resolution dispatches through a strategy seam: a TargetServer-first path\n * (deferred — STITCH-02, see 04-CONTEXT D-01) followed by the active\n * direct-mapping path. The first strategy to return a target ref wins.\n */\nexport class ApigeeStitchingProcessor implements CatalogProcessor {\n constructor(private readonly logger: LoggerService) {}\n\n getProcessorName(): string {\n return 'ApigeeStitchingProcessor';\n }\n\n async postProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n emit: CatalogProcessorEmit,\n ): Promise<Entity> {\n // The processor only stitches Components.\n if (entity.kind !== 'Component') {\n return entity;\n }\n\n const annotations = entity.metadata.annotations ?? {};\n\n // Untouched component: no nos.pt/apigee-* annotations at all → no-op, no warning.\n const hasAnyApigeeAnnotation = Object.keys(annotations).some(key =>\n key.startsWith('nos.pt/apigee-'),\n );\n if (!hasAnyApigeeAnnotation) {\n return entity;\n }\n\n const target = this.resolveTarget(annotations, entity);\n if (!target) {\n // A warning (if applicable) was already logged by the resolution strategy.\n return entity;\n }\n\n const componentRef: EntityRef = {\n kind: 'Component',\n namespace: entity.metadata.namespace ?? 'default',\n name: entity.metadata.name,\n };\n\n emit(\n processingResult.relation({\n source: componentRef,\n target,\n type: RELATION_PROVIDES_API,\n }),\n );\n emit(\n processingResult.relation({\n source: target,\n target: componentRef,\n type: RELATION_API_PROVIDED_BY,\n }),\n );\n\n // Relation-only work: never mutate the entity.\n return entity;\n }\n\n /**\n * Strategy seam. Tries the TargetServer-preferred path first (deferred this\n * phase), then falls back to the active direct-mapping path. First strategy\n * to return a ref wins.\n */\n private resolveTarget(\n annotations: Record<string, string>,\n entity: Entity,\n ): EntityRef | undefined {\n return (\n this.resolveViaTargetServer(annotations, entity) ??\n this.resolveViaDirect(annotations, entity)\n );\n }\n\n /**\n * Deferred: TargetServer-preferred path (STITCH-02). See 04-CONTEXT D-01.\n * No TargetServer data exists in the codebase yet, so this is the clean\n * insertion point for the future branch and intentionally returns undefined.\n */\n private resolveViaTargetServer(\n _annotations: Record<string, string>,\n _entity: Entity,\n ): undefined {\n return undefined;\n }\n\n /**\n * Direct-mapping path. Requires BOTH the api-name and project-id annotations.\n * The proxy API entity ref is deterministic — `api:default/<slugify(api-name)>` —\n * so no live catalog lookup is needed (D-01). projectId is validated for\n * presence to confirm intent but is not part of the deterministic ref.\n */\n private resolveViaDirect(\n annotations: Record<string, string>,\n entity: Entity,\n ): EntityRef | undefined {\n const apiName = annotations[ANNOTATION_API_NAME]?.trim();\n const projectId = annotations[ANNOTATION_PROJECT_ID]?.trim();\n\n const hasApiName = Boolean(apiName);\n const hasProjectId = Boolean(projectId);\n\n if (hasApiName !== hasProjectId) {\n const missing = hasApiName ? ANNOTATION_PROJECT_ID : ANNOTATION_API_NAME;\n this.logger.warn(\n `[ApigeeStitchingProcessor] Component \"${entity.metadata.name}\" has a partial ` +\n `Apigee direct-mapping annotation set; missing \"${missing}\". Skipping stitching.`,\n );\n return undefined;\n }\n\n if (!hasApiName) {\n // Neither direct annotation present — defer silently (e.g. TargetServer-only\n // annotations were supplied, handled by the deferred path).\n return undefined;\n }\n\n const slug = slugify(apiName as string);\n if (slug === '') {\n this.logger.warn(\n `[ApigeeStitchingProcessor] Component \"${entity.metadata.name}\" has an ` +\n `unresolvable \"${ANNOTATION_API_NAME}\" value; nothing to stitch.`,\n );\n return undefined;\n }\n\n return { kind: 'API', namespace: 'default', name: slug };\n }\n}\n"],"names":["processingResult","RELATION_PROVIDES_API","RELATION_API_PROVIDED_BY","slugify"],"mappings":"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"}
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"entity-builder.cjs.js","sources":["../../src/lib/entity-builder.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n ANNOTATION_LOCATION,\r\n ANNOTATION_ORIGIN_LOCATION,\r\n type Entity,\r\n} from '@backstage/catalog-model';\r\n\r\nimport type { ApigeeProxy } from './apigee-client';\r\nimport type { ApigeeSharedflow } from './sharedflow-client';\r\nimport type { ApiHubApi, ApiHubVersion } from './api-hub-client';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Public types\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Per-organisation data gathered during a sync run.\r\n * Populated by ApigeeEntityProvider.run() before calling proxyToEntities().\r\n */\r\nexport interface OrgSyncData {\r\n /** Slugified org identifier (used as annotation key segment) */\r\n slug: string;\r\n /** Raw org name from config (e.g. \"my-apigee-org\") */\r\n name: string;\r\n /** GCP project ID owning this Apigee organisation */\r\n projectId: string;\r\n /** Names of environments this proxy is deployed to in this org */\r\n environments: string[];\r\n /** Resolved Backstage lifecycle for this org: production | experimental | deprecated */\r\n lifecycle: string;\r\n}\r\n\r\n/**\r\n * A proxy that has been deduplicated across organisations (D-03).\r\n * Multiple orgs may share the same proxy name — they are merged into one entity.\r\n */\r\nexport interface MergedProxy {\r\n /** Proxy data from the first org that declared this proxy */\r\n proxy: ApigeeProxy;\r\n /** Union of all base paths declared across all org revisions */\r\n basePaths: string[];\r\n /** All orgs that contain this proxy */\r\n orgs: OrgSyncData[];\r\n}\r\n\r\n/** Options forwarded from plugin configuration. */\r\nexport interface EntityBuildOptions {\r\n /** Default spec.owner when not provided by API Hub. Must be a Backstage entity ref. */\r\n defaultOwner?: string;\r\n /** Inline spec size limit in bytes; default 512000 applied by the provider (D-03). */\r\n specMaxBytes?: number;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nconst LIFECYCLE_RANK: Record<string, number> = {\r\n production: 2,\r\n experimental: 1,\r\n deprecated: 0,\r\n};\r\n\r\n/**\r\n * Returns the highest-priority lifecycle value across all org sync data.\r\n * Precedence: production > experimental > deprecated.\r\n */\r\nfunction resolveHighestLifecycle(orgs: OrgSyncData[]): string {\r\n if (orgs.length === 0) return 'experimental';\r\n return orgs.reduce<string>((best, org) => {\r\n const rank = LIFECYCLE_RANK[org.lifecycle] ?? 1;\r\n const bestRank = LIFECYCLE_RANK[best] ?? 1;\r\n return rank > bestRank ? org.lifecycle : best;\r\n }, orgs[0].lifecycle);\r\n}\r\n\r\n/** Shared location annotation value for all Apigee-managed entities. */\r\nconst APIGEE_LOCATION = 'url:https://apigee.com';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Public functions\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Converts an arbitrary string into a valid Backstage metadata.name slug.\r\n * Lowercases, replaces non-alphanumeric runs with hyphens, trims leading/trailing hyphens.\r\n */\r\nexport function slugify(name: string): string {\r\n return name\r\n .toLowerCase()\r\n .replace(/[^a-z0-9]+/g, '-')\r\n .replace(/-{2,}/g, '-')\r\n .replace(/^-+|-+$/g, '');\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// API Hub enrichment helpers (pure, side-effect-free) — D-02, D-05, D-06\r\n// ---------------------------------------------------------------------------\r\n\r\n/** A deterministic version selection. The spec ID is resolved separately by\r\n * the provider via ApiHubClient.listSpecs(), because the API Hub versions list\r\n * does not reliably populate version.specIds (it is often null even when the\r\n * version has specs). */\r\nexport interface SpecSelection {\r\n versionId: string;\r\n}\r\n\r\n/** Input for resolving a GitHub source-repository link for an entity. */\r\nexport interface GithubLinkInput {\r\n sourceMetadata?: Array<{ sourceType?: string; value?: string }>;\r\n githubOrgSlug?: string;\r\n entityName: string;\r\n}\r\n\r\n/**\r\n * Deterministically selects one version from an API Hub version list (D-02).\r\n *\r\n * Selection rules:\r\n * - Prefer versions whose lifecycle.stage === 'ACTIVE'; if none are ACTIVE,\r\n * use candidates whose stage !== 'ARCHIVED'.\r\n * - Sort by updateTime descending (missing updateTime sorts last),\r\n * tie-break by version resource name ascending.\r\n *\r\n * Note: this intentionally does NOT filter on version.specIds. The API Hub\r\n * versions endpoint frequently returns specIds: null even when the version has\r\n * specs; the actual spec list is resolved separately via ApiHubClient.listSpecs().\r\n *\r\n * @returns the selected version, or undefined when there is no usable version.\r\n */\r\nexport function selectSpec(\r\n versions: ApiHubVersion[],\r\n): SpecSelection | undefined {\r\n const all = versions ?? [];\r\n if (all.length === 0) return undefined;\r\n\r\n const active = all.filter(v => v.lifecycle?.stage === 'ACTIVE');\r\n const candidates =\r\n active.length > 0\r\n ? active\r\n : all.filter(v => v.lifecycle?.stage !== 'ARCHIVED');\r\n if (candidates.length === 0) return undefined;\r\n\r\n const sorted = [...candidates].sort((a, b) => {\r\n // updateTime descending; missing sorts last\r\n const at = a.updateTime ?? '';\r\n const bt = b.updateTime ?? '';\r\n if (at !== bt) {\r\n if (!at) return 1;\r\n if (!bt) return -1;\r\n return at < bt ? 1 : -1;\r\n }\r\n // tie-break by name ascending\r\n return (a.name ?? '').localeCompare(b.name ?? '');\r\n });\r\n\r\n const chosen = sorted[0];\r\n const versionId = (chosen.name ?? '').split('/').pop() ?? '';\r\n if (!versionId) return undefined;\r\n return { versionId };\r\n}\r\n\r\n/**\r\n * Maps an API Hub owner to a Backstage owner entity reference (D-06).\r\n * owner.displayName => group:default/<slug>; otherwise defaultOwner, else\r\n * group:default/unknown.\r\n */\r\nexport function ownerRef(\r\n owner: { displayName?: string; email?: string } | undefined,\r\n defaultOwner?: string,\r\n): string {\r\n if (owner?.displayName) {\r\n return `group:default/${slugify(owner.displayName)}`;\r\n }\r\n return defaultOwner ?? 'group:default/unknown';\r\n}\r\n\r\n/**\r\n * Derives slugified, deduped, sorted tag values from raw API Hub category ids (D-05).\r\n * No extra API Hub call — operates only on the raw ids already present on the API.\r\n */\r\nexport function categoryTags(categories: string[] | undefined): string[] {\r\n const slugs = (categories ?? [])\r\n .map(c => slugify(c))\r\n .filter(c => c.length > 0);\r\n return [...new Set(slugs)].sort();\r\n}\r\n\r\n/**\r\n * Comma-joined category annotation value (D-05), or undefined when there are\r\n * no categories.\r\n */\r\nexport function categoryAnnotation(\r\n categories: string[] | undefined,\r\n): string | undefined {\r\n const tags = categoryTags(categories);\r\n return tags.length > 0 ? tags.join(',') : undefined;\r\n}\r\n\r\n/**\r\n * Resolves a GitHub source-repository link for an entity (D-05).\r\n *\r\n * SECURITY: only http(s) URLs are ever returned. A sourceMetadata value that\r\n * does not match ^https?:// is rejected to prevent javascript:/data: link\r\n * injection into rendered entity pages.\r\n *\r\n * Resolution order:\r\n * 1. First sourceMetadata entry whose value is an http(s) URL (entries whose\r\n * sourceType matches /repo|git|source/i are preferred when several qualify).\r\n * 2. githubOrgSlug convention: https://github.com/<slug>/<entityName>.\r\n * 3. undefined.\r\n */\r\nexport function resolveGithubLink(\r\n input: GithubLinkInput,\r\n): { url: string; title: string } | undefined {\r\n const httpUrl = /^https?:\\/\\//i;\r\n const sources = (input.sourceMetadata ?? []).filter(\r\n s => typeof s.value === 'string' && httpUrl.test(s.value),\r\n );\r\n if (sources.length > 0) {\r\n const preferred =\r\n sources.find(s => /repo|git|source/i.test(s.sourceType ?? '')) ??\r\n sources[0];\r\n return { url: preferred.value as string, title: 'Source repository' };\r\n }\r\n if (input.githubOrgSlug) {\r\n return {\r\n url: `https://github.com/${input.githubOrgSlug}/${input.entityName}`,\r\n title: 'Source repository',\r\n };\r\n }\r\n return undefined;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// API Hub entity composition (pure) — D-03, D-04, D-05, D-07\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The outcome of resolving a spec for an API (computed by the provider, plan 03-04).\r\n * Either embedded inline (`definition`) or linked when over the size limit (`specUrl`).\r\n */\r\nexport interface SpecEmbed {\r\n /** true when a spec exists (embedded OR linked). */\r\n hasSpec: boolean;\r\n /** Decoded UTF-8 spec text when embedded inline. */\r\n definition?: string;\r\n /** API Hub spec resource name when the spec exceeds specMaxBytes (D-03). */\r\n specUrl?: string;\r\n}\r\n\r\n/**\r\n * Applies the shared API Hub metadata (tags, category annotation, GitHub link,\r\n * has-spec + spec.definition/spec-url) to an API entity in place.\r\n *\r\n * Used by both enrichMatchedApiEntity and hubOnlyToEntity to avoid divergence.\r\n * Callers pass a cloned/fresh entity — this never reads from a network.\r\n */\r\nfunction applyHubMetadata(\r\n api: Entity,\r\n hubApi: ApiHubApi,\r\n spec: SpecEmbed | undefined,\r\n githubOrgSlug: string | undefined,\r\n): void {\r\n const annotations = api.metadata.annotations ?? {};\r\n\r\n // Tags + category annotation (D-05, META-09)\r\n const tags = categoryTags(hubApi.categories);\r\n if (tags.length > 0) {\r\n api.metadata.tags = tags;\r\n }\r\n const catAnno = categoryAnnotation(hubApi.categories);\r\n if (catAnno) {\r\n annotations['apigee.com/category'] = catAnno;\r\n }\r\n\r\n // GitHub source link (D-05, META-10) — only ever http(s) per resolveGithubLink\r\n const link = resolveGithubLink({\r\n sourceMetadata: hubApi.sourceMetadata,\r\n githubOrgSlug,\r\n entityName: api.metadata.name,\r\n });\r\n if (link) {\r\n const links = api.metadata.links ?? [];\r\n if (!links.some(l => l.url === link.url)) {\r\n links.push(link);\r\n }\r\n api.metadata.links = links;\r\n }\r\n\r\n // Spec embed-or-url (D-03, META-05/08)\r\n if (spec?.definition) {\r\n (api.spec as Record<string, unknown>).definition = spec.definition;\r\n annotations['apigee.com/has-spec'] = 'true';\r\n } else if (spec?.specUrl) {\r\n annotations['apigee.com/spec-url'] = spec.specUrl;\r\n annotations['apigee.com/has-spec'] = 'true';\r\n } else {\r\n annotations['apigee.com/has-spec'] = 'false';\r\n }\r\n\r\n api.metadata.annotations = annotations;\r\n}\r\n\r\n/**\r\n * Enriches a proxy-derived API entity with matched API Hub metadata (D-03/D-05/D-07).\r\n *\r\n * PURE: deep-copies `baseApi` and returns the copy; never mutates the input, so\r\n * repeated runs with identical inputs are idempotent (ROADMAP SC #4).\r\n *\r\n * @param baseApi - The API entity produced by proxyToEntities.\r\n * @param hubApi - The matched API Hub definition.\r\n * @param spec - Resolved spec embed/url (undefined when none).\r\n * @param githubOrgSlug - Org GitHub slug for link fallback.\r\n * @param options - Build-time options (defaultOwner).\r\n */\r\nexport function enrichMatchedApiEntity(\r\n baseApi: Entity,\r\n hubApi: ApiHubApi,\r\n spec: SpecEmbed | undefined,\r\n githubOrgSlug: string | undefined,\r\n options?: EntityBuildOptions,\r\n): Entity {\r\n const api: Entity = {\r\n ...baseApi,\r\n metadata: {\r\n ...baseApi.metadata,\r\n annotations: { ...(baseApi.metadata.annotations ?? {}) },\r\n ...(baseApi.metadata.tags ? { tags: [...baseApi.metadata.tags] } : {}),\r\n ...(baseApi.metadata.links\r\n ? { links: baseApi.metadata.links.map(l => ({ ...l })) }\r\n : {}),\r\n },\r\n spec: { ...(baseApi.spec ?? {}) },\r\n };\r\n\r\n // Metadata backfill (D-07, META-02/03/07)\r\n if (hubApi.displayName) {\r\n api.metadata.title = hubApi.displayName;\r\n }\r\n api.metadata.description = hubApi.description ?? '';\r\n (api.spec as Record<string, unknown>).owner = ownerRef(\r\n hubApi.owner,\r\n options?.defaultOwner,\r\n );\r\n\r\n applyHubMetadata(api, hubApi, spec, githubOrgSlug);\r\n return api;\r\n}\r\n\r\n/**\r\n * Builds a standalone API entity for an API Hub definition with no matching\r\n * Apigee proxy (D-04). Lifecycle is always 'experimental'; the full API Hub\r\n * resource name is recorded in `apigee.com/api-hub-id`.\r\n *\r\n * PURE: derives output solely from inputs (idempotent).\r\n *\r\n * @param hubApi - The unmatched API Hub definition.\r\n * @param orgName - Raw org name this definition was discovered under.\r\n * @param spec - Resolved spec embed/url (undefined when none).\r\n * @param githubOrgSlug - Org GitHub slug for link fallback.\r\n * @param options - Build-time options (defaultOwner).\r\n */\r\nexport function hubOnlyToEntity(\r\n hubApi: ApiHubApi,\r\n orgName: string,\r\n spec: SpecEmbed | undefined,\r\n githubOrgSlug: string | undefined,\r\n options?: EntityBuildOptions,\r\n): Entity {\r\n const apiId = (hubApi.name ?? '').split('/').pop() ?? '';\r\n const display = hubApi.displayName ?? apiId;\r\n const slug = slugify(display);\r\n\r\n const api: Entity = {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'API',\r\n metadata: {\r\n name: slug,\r\n title: display,\r\n namespace: 'default',\r\n description: hubApi.description ?? '',\r\n annotations: {\r\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\r\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\r\n // Full API Hub resource name (D-04) — distinguishes hub-only entities.\r\n 'apigee.com/api-hub-id': hubApi.name,\r\n 'apigee.com/org': orgName,\r\n },\r\n },\r\n spec: {\r\n type: hubApi.apiType ?? 'other',\r\n lifecycle: 'experimental',\r\n owner: ownerRef(hubApi.owner, options?.defaultOwner),\r\n definition: '',\r\n },\r\n };\r\n\r\n applyHubMetadata(api, hubApi, spec, githubOrgSlug);\r\n return api;\r\n}\r\n\r\n/**\r\n * Builds a [Component, API] entity pair from a merged proxy (D-01, D-03).\r\n *\r\n * @param merged - Deduplicated proxy with per-org data.\r\n * @param apiType - API type from API Hub (D-01); defaults to 'other'.\r\n * @param options - Build-time options (defaultOwner, etc.)\r\n * @returns Tuple [componentEntity, apiEntity]\r\n */\r\nexport function proxyToEntities(\r\n merged: MergedProxy,\r\n apiType: string | undefined,\r\n options?: EntityBuildOptions,\r\n): [Entity, Entity] {\r\n const slug = slugify(merged.proxy.name);\r\n const lifecycle = resolveHighestLifecycle(merged.orgs);\r\n const owner = options?.defaultOwner ?? 'group:default/unknown';\r\n const orgsAnnotation = merged.orgs.map(o => o.name).join(',');\r\n\r\n // Per-org annotations (D-03): apigee.com/<org-slug>/environments + /lifecycle\r\n const perOrgAnnotations: Record<string, string> = {};\r\n for (const org of merged.orgs) {\r\n const safe = slugify(org.slug || org.name);\r\n perOrgAnnotations[`apigee.com/${safe}/environments`] =\r\n org.environments.join(',');\r\n perOrgAnnotations[`apigee.com/${safe}/lifecycle`] = org.lifecycle;\r\n }\r\n\r\n const sharedAnnotations: Record<string, string> = {\r\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\r\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\r\n 'apigee.com/proxy-name': merged.proxy.name,\r\n 'apigee.com/orgs': orgsAnnotation,\r\n };\r\n\r\n const component: Entity = {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'Component',\r\n metadata: {\r\n name: slug,\r\n title: merged.proxy.name,\r\n namespace: 'default',\r\n annotations: {\r\n ...sharedAnnotations,\r\n 'apigee.com/base-path': merged.basePaths.join(','),\r\n 'apigee.com/has-spec': 'false',\r\n ...perOrgAnnotations,\r\n },\r\n },\r\n spec: {\r\n type: 'service',\r\n lifecycle,\r\n owner,\r\n },\r\n };\r\n\r\n const api: Entity = {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'API',\r\n metadata: {\r\n name: slug,\r\n title: merged.proxy.name,\r\n namespace: 'default',\r\n annotations: {\r\n ...sharedAnnotations,\r\n // Default false; 03-03 enrichment overrides to 'true' when a spec is found (META-05, SC #2).\r\n 'apigee.com/has-spec': 'false',\r\n },\r\n },\r\n spec: {\r\n type: apiType ?? 'other',\r\n lifecycle,\r\n owner,\r\n definition: '',\r\n },\r\n };\r\n\r\n return [component, api];\r\n}\r\n\r\n/**\r\n * Builds a Component entity from an Apigee sharedflow (D-04).\r\n * Placed in the 'apigee-sharedflows' namespace with spec.type 'library'.\r\n *\r\n * @param sharedflow - Sharedflow data from SharedflowClient.\r\n * @param orgName - Raw org name that owns this sharedflow.\r\n * @param options - Build-time options.\r\n */\r\nexport function sharedflowToEntity(\r\n sharedflow: ApigeeSharedflow,\r\n orgName: string,\r\n options?: EntityBuildOptions,\r\n): Entity {\r\n const slug = slugify(sharedflow.name);\r\n const owner = options?.defaultOwner ?? 'group:default/unknown';\r\n\r\n return {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'Component',\r\n metadata: {\r\n name: slug,\r\n title: sharedflow.name,\r\n namespace: 'apigee-sharedflows',\r\n annotations: {\r\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\r\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\r\n 'apigee.com/proxy-name': sharedflow.name,\r\n 'apigee.com/orgs': orgName,\r\n },\r\n },\r\n spec: {\r\n type: 'library',\r\n lifecycle: 'production',\r\n owner,\r\n },\r\n };\r\n}\r\n"],"names":["ANNOTATION_LOCATION","ANNOTATION_ORIGIN_LOCATION"],"mappings":"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"}
1
+ {"version":3,"file":"entity-builder.cjs.js","sources":["../../src/lib/entity-builder.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n ANNOTATION_LOCATION,\n ANNOTATION_ORIGIN_LOCATION,\n type Entity,\n} from '@backstage/catalog-model';\n\nimport type { ApigeeProxy } from './apigee-client';\nimport type { ApigeeSharedflow } from './sharedflow-client';\nimport type { ApiHubApi, ApiHubVersion } from './api-hub-client';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Per-organisation data gathered during a sync run.\n * Populated by ApigeeEntityProvider.run() before calling proxyToEntities().\n */\nexport interface OrgSyncData {\n /** Slugified org identifier (used as annotation key segment) */\n slug: string;\n /** Raw org name from config (e.g. \"my-apigee-org\") */\n name: string;\n /** GCP project ID owning this Apigee organisation */\n projectId: string;\n /** Names of environments this proxy is deployed to in this org */\n environments: string[];\n /** Resolved Backstage lifecycle for this org: production | experimental | deprecated */\n lifecycle: string;\n}\n\n/**\n * A proxy that has been deduplicated across organisations (D-03).\n * Multiple orgs may share the same proxy name — they are merged into one entity.\n */\nexport interface MergedProxy {\n /** Proxy data from the first org that declared this proxy */\n proxy: ApigeeProxy;\n /** Union of all base paths declared across all org revisions */\n basePaths: string[];\n /** All orgs that contain this proxy */\n orgs: OrgSyncData[];\n}\n\n/** Options forwarded from plugin configuration. */\nexport interface EntityBuildOptions {\n /** Default spec.owner when not provided by API Hub. Must be a Backstage entity ref. */\n defaultOwner?: string;\n /** Inline spec size limit in bytes; default 512000 applied by the provider (D-03). */\n specMaxBytes?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst LIFECYCLE_RANK: Record<string, number> = {\n production: 2,\n experimental: 1,\n deprecated: 0,\n};\n\n/**\n * Returns the highest-priority lifecycle value across all org sync data.\n * Precedence: production > experimental > deprecated.\n */\nfunction resolveHighestLifecycle(orgs: OrgSyncData[]): string {\n if (orgs.length === 0) return 'experimental';\n return orgs.reduce<string>((best, org) => {\n const rank = LIFECYCLE_RANK[org.lifecycle] ?? 1;\n const bestRank = LIFECYCLE_RANK[best] ?? 1;\n return rank > bestRank ? org.lifecycle : best;\n }, orgs[0].lifecycle);\n}\n\n/** Shared location annotation value for all Apigee-managed entities. */\nconst APIGEE_LOCATION = 'url:https://apigee.com';\n\n// ---------------------------------------------------------------------------\n// Public functions\n// ---------------------------------------------------------------------------\n\n/**\n * Converts an arbitrary string into a valid Backstage metadata.name slug.\n * Lowercases, replaces non-alphanumeric runs with hyphens, trims leading/trailing hyphens.\n */\nexport function slugify(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n// ---------------------------------------------------------------------------\n// API Hub enrichment helpers (pure, side-effect-free) — D-02, D-05, D-06\n// ---------------------------------------------------------------------------\n\n/** A deterministic version selection. The spec ID is resolved separately by\n * the provider via ApiHubClient.listSpecs(), because the API Hub versions list\n * does not reliably populate version.specIds (it is often null even when the\n * version has specs). */\nexport interface SpecSelection {\n versionId: string;\n}\n\n/** Input for resolving a GitHub source-repository link for an entity. */\nexport interface GithubLinkInput {\n sourceMetadata?: Array<{ sourceType?: string; value?: string }>;\n githubOrgSlug?: string;\n entityName: string;\n}\n\n/**\n * Deterministically selects one version from an API Hub version list (D-02).\n *\n * Selection rules:\n * - Prefer versions whose lifecycle.stage === 'ACTIVE'; if none are ACTIVE,\n * use candidates whose stage !== 'ARCHIVED'.\n * - Sort by updateTime descending (missing updateTime sorts last),\n * tie-break by version resource name ascending.\n *\n * Note: this intentionally does NOT filter on version.specIds. The API Hub\n * versions endpoint frequently returns specIds: null even when the version has\n * specs; the actual spec list is resolved separately via ApiHubClient.listSpecs().\n *\n * @returns the selected version, or undefined when there is no usable version.\n */\nexport function selectSpec(\n versions: ApiHubVersion[],\n): SpecSelection | undefined {\n const all = versions ?? [];\n if (all.length === 0) return undefined;\n\n const active = all.filter(v => v.lifecycle?.stage === 'ACTIVE');\n const candidates =\n active.length > 0\n ? active\n : all.filter(v => v.lifecycle?.stage !== 'ARCHIVED');\n if (candidates.length === 0) return undefined;\n\n const sorted = [...candidates].sort((a, b) => {\n // updateTime descending; missing sorts last\n const at = a.updateTime ?? '';\n const bt = b.updateTime ?? '';\n if (at !== bt) {\n if (!at) return 1;\n if (!bt) return -1;\n return at < bt ? 1 : -1;\n }\n // tie-break by name ascending\n return (a.name ?? '').localeCompare(b.name ?? '');\n });\n\n const chosen = sorted[0];\n const versionId = (chosen.name ?? '').split('/').pop() ?? '';\n if (!versionId) return undefined;\n return { versionId };\n}\n\n/**\n * Maps an API Hub owner to a Backstage owner entity reference (D-06).\n * owner.displayName => group:default/<slug>; otherwise defaultOwner, else\n * group:default/unknown.\n */\nexport function ownerRef(\n owner: { displayName?: string; email?: string } | undefined,\n defaultOwner?: string,\n): string {\n if (owner?.displayName) {\n return `group:default/${slugify(owner.displayName)}`;\n }\n return defaultOwner ?? 'group:default/unknown';\n}\n\n/**\n * Derives slugified, deduped, sorted tag values from raw API Hub category ids (D-05).\n * No extra API Hub call — operates only on the raw ids already present on the API.\n */\nexport function categoryTags(categories: string[] | undefined): string[] {\n const slugs = (categories ?? [])\n .map(c => slugify(c))\n .filter(c => c.length > 0);\n return [...new Set(slugs)].sort();\n}\n\n/**\n * Comma-joined category annotation value (D-05), or undefined when there are\n * no categories.\n */\nexport function categoryAnnotation(\n categories: string[] | undefined,\n): string | undefined {\n const tags = categoryTags(categories);\n return tags.length > 0 ? tags.join(',') : undefined;\n}\n\n/**\n * Resolves a GitHub source-repository link for an entity (D-05).\n *\n * SECURITY: only http(s) URLs are ever returned. A sourceMetadata value that\n * does not match ^https?:// is rejected to prevent javascript:/data: link\n * injection into rendered entity pages.\n *\n * Resolution order:\n * 1. First sourceMetadata entry whose value is an http(s) URL (entries whose\n * sourceType matches /repo|git|source/i are preferred when several qualify).\n * 2. githubOrgSlug convention: https://github.com/<slug>/<entityName>.\n * 3. undefined.\n */\nexport function resolveGithubLink(\n input: GithubLinkInput,\n): { url: string; title: string } | undefined {\n const httpUrl = /^https?:\\/\\//i;\n const sources = (input.sourceMetadata ?? []).filter(\n s => typeof s.value === 'string' && httpUrl.test(s.value),\n );\n if (sources.length > 0) {\n const preferred =\n sources.find(s => /repo|git|source/i.test(s.sourceType ?? '')) ??\n sources[0];\n return { url: preferred.value as string, title: 'Source repository' };\n }\n if (input.githubOrgSlug) {\n return {\n url: `https://github.com/${input.githubOrgSlug}/${input.entityName}`,\n title: 'Source repository',\n };\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// API Hub entity composition (pure) — D-03, D-04, D-05, D-07\n// ---------------------------------------------------------------------------\n\n/**\n * The outcome of resolving a spec for an API (computed by the provider, plan 03-04).\n * Either embedded inline (`definition`) or linked when over the size limit (`specUrl`).\n */\nexport interface SpecEmbed {\n /** true when a spec exists (embedded OR linked). */\n hasSpec: boolean;\n /** Decoded UTF-8 spec text when embedded inline. */\n definition?: string;\n /** API Hub spec resource name when the spec exceeds specMaxBytes (D-03). */\n specUrl?: string;\n}\n\n/**\n * Applies the shared API Hub metadata (tags, category annotation, GitHub link,\n * has-spec + spec.definition/spec-url) to an API entity in place.\n *\n * Used by both enrichMatchedApiEntity and hubOnlyToEntity to avoid divergence.\n * Callers pass a cloned/fresh entity — this never reads from a network.\n */\nfunction applyHubMetadata(\n api: Entity,\n hubApi: ApiHubApi,\n spec: SpecEmbed | undefined,\n githubOrgSlug: string | undefined,\n): void {\n const annotations = api.metadata.annotations ?? {};\n\n // Tags + category annotation (D-05, META-09)\n const tags = categoryTags(hubApi.categories);\n if (tags.length > 0) {\n api.metadata.tags = tags;\n }\n const catAnno = categoryAnnotation(hubApi.categories);\n if (catAnno) {\n annotations['apigee.com/category'] = catAnno;\n }\n\n // GitHub source link (D-05, META-10) — only ever http(s) per resolveGithubLink\n const link = resolveGithubLink({\n sourceMetadata: hubApi.sourceMetadata,\n githubOrgSlug,\n entityName: api.metadata.name,\n });\n if (link) {\n const links = api.metadata.links ?? [];\n if (!links.some(l => l.url === link.url)) {\n links.push(link);\n }\n api.metadata.links = links;\n }\n\n // Spec embed-or-url (D-03, META-05/08)\n if (spec?.definition) {\n (api.spec as Record<string, unknown>).definition = spec.definition;\n annotations['apigee.com/has-spec'] = 'true';\n } else if (spec?.specUrl) {\n annotations['apigee.com/spec-url'] = spec.specUrl;\n annotations['apigee.com/has-spec'] = 'true';\n } else {\n annotations['apigee.com/has-spec'] = 'false';\n }\n\n api.metadata.annotations = annotations;\n}\n\n/**\n * Enriches a proxy-derived API entity with matched API Hub metadata (D-03/D-05/D-07).\n *\n * PURE: deep-copies `baseApi` and returns the copy; never mutates the input, so\n * repeated runs with identical inputs are idempotent (ROADMAP SC #4).\n *\n * @param baseApi - The API entity produced by proxyToEntities.\n * @param hubApi - The matched API Hub definition.\n * @param spec - Resolved spec embed/url (undefined when none).\n * @param githubOrgSlug - Org GitHub slug for link fallback.\n * @param options - Build-time options (defaultOwner).\n */\nexport function enrichMatchedApiEntity(\n baseApi: Entity,\n hubApi: ApiHubApi,\n spec: SpecEmbed | undefined,\n githubOrgSlug: string | undefined,\n options?: EntityBuildOptions,\n): Entity {\n const api: Entity = {\n ...baseApi,\n metadata: {\n ...baseApi.metadata,\n annotations: { ...(baseApi.metadata.annotations ?? {}) },\n ...(baseApi.metadata.tags ? { tags: [...baseApi.metadata.tags] } : {}),\n ...(baseApi.metadata.links\n ? { links: baseApi.metadata.links.map(l => ({ ...l })) }\n : {}),\n },\n spec: { ...(baseApi.spec ?? {}) },\n };\n\n // Metadata backfill (D-07, META-02/03/07)\n if (hubApi.displayName) {\n api.metadata.title = hubApi.displayName;\n }\n api.metadata.description = hubApi.description ?? '';\n (api.spec as Record<string, unknown>).owner = ownerRef(\n hubApi.owner,\n options?.defaultOwner,\n );\n\n applyHubMetadata(api, hubApi, spec, githubOrgSlug);\n return api;\n}\n\n/**\n * Builds a standalone API entity for an API Hub definition with no matching\n * Apigee proxy (D-04). Lifecycle is always 'experimental'; the full API Hub\n * resource name is recorded in `apigee.com/api-hub-id`.\n *\n * PURE: derives output solely from inputs (idempotent).\n *\n * @param hubApi - The unmatched API Hub definition.\n * @param orgName - Raw org name this definition was discovered under.\n * @param spec - Resolved spec embed/url (undefined when none).\n * @param githubOrgSlug - Org GitHub slug for link fallback.\n * @param options - Build-time options (defaultOwner).\n */\nexport function hubOnlyToEntity(\n hubApi: ApiHubApi,\n orgName: string,\n spec: SpecEmbed | undefined,\n githubOrgSlug: string | undefined,\n options?: EntityBuildOptions,\n): Entity {\n const apiId = (hubApi.name ?? '').split('/').pop() ?? '';\n const display = hubApi.displayName ?? apiId;\n const slug = slugify(display);\n\n const api: Entity = {\n apiVersion: 'backstage.io/v1alpha1',\n kind: 'API',\n metadata: {\n name: slug,\n title: display,\n namespace: 'default',\n description: hubApi.description ?? '',\n annotations: {\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\n // Full API Hub resource name (D-04) — distinguishes hub-only entities.\n 'apigee.com/api-hub-id': hubApi.name,\n 'apigee.com/org': orgName,\n },\n },\n spec: {\n type: hubApi.apiType ?? 'other',\n lifecycle: 'experimental',\n owner: ownerRef(hubApi.owner, options?.defaultOwner),\n definition: '',\n },\n };\n\n applyHubMetadata(api, hubApi, spec, githubOrgSlug);\n return api;\n}\n\n/**\n * Builds a [Component, API] entity pair from a merged proxy (D-01, D-03).\n *\n * @param merged - Deduplicated proxy with per-org data.\n * @param apiType - API type from API Hub (D-01); defaults to 'other'.\n * @param options - Build-time options (defaultOwner, etc.)\n * @returns Tuple [componentEntity, apiEntity]\n */\nexport function proxyToEntities(\n merged: MergedProxy,\n apiType: string | undefined,\n options?: EntityBuildOptions,\n): [Entity, Entity] {\n const slug = slugify(merged.proxy.name);\n const lifecycle = resolveHighestLifecycle(merged.orgs);\n const owner = options?.defaultOwner ?? 'group:default/unknown';\n const orgsAnnotation = merged.orgs.map(o => o.name).join(',');\n\n // Per-org annotations (D-03): apigee.com/<org-slug>/environments + /lifecycle\n const perOrgAnnotations: Record<string, string> = {};\n for (const org of merged.orgs) {\n const safe = slugify(org.slug || org.name);\n perOrgAnnotations[`apigee.com/${safe}/environments`] =\n org.environments.join(',');\n perOrgAnnotations[`apigee.com/${safe}/lifecycle`] = org.lifecycle;\n }\n\n const sharedAnnotations: Record<string, string> = {\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\n 'apigee.com/proxy-name': merged.proxy.name,\n 'apigee.com/orgs': orgsAnnotation,\n };\n\n const component: Entity = {\n apiVersion: 'backstage.io/v1alpha1',\n kind: 'Component',\n metadata: {\n name: slug,\n title: merged.proxy.name,\n namespace: 'default',\n annotations: {\n ...sharedAnnotations,\n 'apigee.com/base-path': merged.basePaths.join(','),\n 'apigee.com/has-spec': 'false',\n ...perOrgAnnotations,\n },\n },\n spec: {\n type: 'service',\n lifecycle,\n owner,\n },\n };\n\n const api: Entity = {\n apiVersion: 'backstage.io/v1alpha1',\n kind: 'API',\n metadata: {\n name: slug,\n title: merged.proxy.name,\n namespace: 'default',\n annotations: {\n ...sharedAnnotations,\n // Default false; 03-03 enrichment overrides to 'true' when a spec is found (META-05, SC #2).\n 'apigee.com/has-spec': 'false',\n },\n },\n spec: {\n type: apiType ?? 'other',\n lifecycle,\n owner,\n definition: '',\n },\n };\n\n return [component, api];\n}\n\n/**\n * Builds a Component entity from an Apigee sharedflow (D-04).\n * Placed in the 'apigee-sharedflows' namespace with spec.type 'library'.\n *\n * @param sharedflow - Sharedflow data from SharedflowClient.\n * @param orgName - Raw org name that owns this sharedflow.\n * @param options - Build-time options.\n */\nexport function sharedflowToEntity(\n sharedflow: ApigeeSharedflow,\n orgName: string,\n options?: EntityBuildOptions,\n): Entity {\n const slug = slugify(sharedflow.name);\n const owner = options?.defaultOwner ?? 'group:default/unknown';\n\n return {\n apiVersion: 'backstage.io/v1alpha1',\n kind: 'Component',\n metadata: {\n name: slug,\n title: sharedflow.name,\n namespace: 'apigee-sharedflows',\n annotations: {\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\n 'apigee.com/proxy-name': sharedflow.name,\n 'apigee.com/orgs': orgName,\n },\n },\n spec: {\n type: 'library',\n lifecycle: 'production',\n owner,\n },\n };\n}\n"],"names":["ANNOTATION_LOCATION","ANNOTATION_ORIGIN_LOCATION"],"mappings":"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"}
@@ -52,6 +52,9 @@ class ApigeeEntityProvider {
52
52
  if (!this.connection) {
53
53
  throw new Error("ApigeeEntityProvider not initialized");
54
54
  }
55
+ this.logger.info(
56
+ `[catalog-module-apigee] Starting catalog sync for ${this.orgsConfig.length} org(s): ${this.orgsConfig.map((o) => o.name).join(", ")}`
57
+ );
55
58
  const orgResults = await Promise.allSettled(
56
59
  this.orgsConfig.map((orgCfg) => this.fetchOrgData(orgCfg))
57
60
  );
@@ -66,6 +69,21 @@ class ApigeeEntityProvider {
66
69
  orgDataList.push(result.value);
67
70
  }
68
71
  }
72
+ const totalProxies = orgDataList.reduce(
73
+ (sum, o) => sum + o.proxyMap.size,
74
+ 0
75
+ );
76
+ const totalSharedflows = orgDataList.reduce(
77
+ (sum, o) => sum + o.sharedflows.length,
78
+ 0
79
+ );
80
+ const totalHubApis = orgDataList.reduce(
81
+ (sum, o) => sum + o.apiHubApis.length,
82
+ 0
83
+ );
84
+ this.logger.info(
85
+ `[catalog-module-apigee] Fetched ${orgDataList.length}/${this.orgsConfig.length} org(s): ${totalProxies} proxies, ${totalHubApis} API Hub definitions, ${totalSharedflows} sharedflows`
86
+ );
69
87
  const mergedProxyMap = /* @__PURE__ */ new Map();
70
88
  for (const orgData of orgDataList) {
71
89
  for (const [slug, partial] of orgData.proxyMap) {
@@ -183,6 +201,9 @@ class ApigeeEntityProvider {
183
201
  entities.push(entityBuilder.sharedflowToEntity(sf, orgData.orgName, this.options));
184
202
  }
185
203
  }
204
+ this.logger.info(
205
+ `[catalog-module-apigee] Catalog sync complete \u2014 emitting ${entities.length} entities`
206
+ );
186
207
  await this.connection.applyMutation({
187
208
  type: "full",
188
209
  entities: entities.map((entity) => ({
@@ -1 +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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"}
1
+ {"version":3,"file":"entity-provider.cjs.js","sources":["../../src/lib/entity-provider.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n type LoggerService,\n type SchedulerServiceTaskRunner,\n} from '@backstage/backend-plugin-api';\nimport type { Entity } from '@backstage/catalog-model';\nimport type {\n EntityProvider,\n EntityProviderConnection,\n} from '@backstage/plugin-catalog-node';\n\nimport type { ApigeeClient } from './apigee-client';\nimport type { ApiHubClient, ApiHubApi } from './api-hub-client';\nimport type { SharedflowClient, ApigeeSharedflow } from './sharedflow-client';\nimport {\n slugify,\n proxyToEntities,\n sharedflowToEntity,\n selectSpec,\n enrichMatchedApiEntity,\n hubOnlyToEntity,\n type OrgSyncData,\n type MergedProxy,\n type EntityBuildOptions,\n type SpecEmbed,\n} from './entity-builder';\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\n/** Org configuration shape (matches config.d.ts apigee.orgs[n]) */\ninterface OrgConfig {\n name: string;\n projectId: string;\n environments?: string[];\n githubOrgSlug?: string;\n /** proxyName → API Hub apiId override (D-01); set by module.ts from config. */\n apiHubMappings?: Record<string, string>;\n}\n\n/** Data collected for a single org during one sync run. */\ninterface OrgData {\n orgName: string;\n proxyMap: Map<string, MergedProxy>; // slug → MergedProxy (single-org orgs array)\n sharedflows: ApigeeSharedflow[];\n /** proxyName.toLowerCase() → apiType from API Hub */\n apiTypeMap: Map<string, string>;\n /** All API Hub definitions discovered for this org (retained for enrichment). */\n apiHubApis: ApiHubApi[];\n /** GCP project for this org's API Hub instance (D-01 spec fetch). */\n projectId: string;\n /** Org GitHub slug for source-link fallback (D-05). */\n githubOrgSlug?: string;\n /** proxyName → API Hub apiId override (D-01). */\n apiHubMappings?: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\n/**\n * Maximum number of spec resolutions (each ~3-4 API Hub round-trips) to run in\n * parallel during a sync. Bounds concurrency so a large catalogue does not open\n * dozens of simultaneous connections while still avoiding fully serial latency.\n */\nconst SPEC_FETCH_CONCURRENCY = 8;\n\n/**\n * Entity provider that discovers Apigee API proxies and sharedflows across\n * one or more organisations and registers them as Backstage entities.\n *\n * Registered with the catalog via `catalogProcessingExtensionPoint.addEntityProvider`.\n * Scheduled via the `SchedulerServiceTaskRunner` injected at construction time.\n */\nexport class ApigeeEntityProvider implements EntityProvider {\n private connection?: EntityProviderConnection;\n\n constructor(\n private readonly taskRunner: SchedulerServiceTaskRunner,\n private readonly logger: LoggerService,\n private readonly orgsConfig: OrgConfig[],\n private readonly apigeeClient: ApigeeClient,\n private readonly apiHubClient: ApiHubClient,\n private readonly sharedflowClient: SharedflowClient,\n private readonly options: EntityBuildOptions,\n ) {}\n\n /** Stable provider name — MUST NOT change; it links entities to this provider. */\n getProviderName(): string {\n return 'apigee-catalog-provider';\n }\n\n /**\n * Called once by the catalog on startup. Stores the connection and registers\n * the recurring sync task with the scheduler.\n */\n async connect(connection: EntityProviderConnection): Promise<void> {\n if (this.connection) {\n throw new Error('ApigeeEntityProvider is already connected');\n }\n this.connection = connection;\n await this.taskRunner.run({\n id: 'catalog-backend-module-apigee:sync',\n fn: async () => {\n await this.run();\n },\n });\n }\n\n /**\n * Executes one full catalog sync:\n * 1. Fetch proxy / sharedflow / API-Hub data for all orgs in parallel.\n * 2. Warn and skip any org that fails (error isolation per org).\n * 3. Deduplicate proxies shared across multiple orgs (D-03).\n * 4. Build entities and call applyMutation exactly once.\n */\n async run(): Promise<void> {\n if (!this.connection) {\n throw new Error('ApigeeEntityProvider not initialized');\n }\n\n this.logger.info(\n `[catalog-module-apigee] Starting catalog sync for ${\n this.orgsConfig.length\n } org(s): ${this.orgsConfig.map(o => o.name).join(', ')}`,\n );\n\n // ── Step 1: Parallel fetch ────────────────────────────────────────────\n const orgResults = await Promise.allSettled(\n this.orgsConfig.map(orgCfg => this.fetchOrgData(orgCfg)),\n );\n\n // ── Step 2: Collect successful results; warn on failures ──────────────\n const orgDataList: OrgData[] = [];\n for (let i = 0; i < orgResults.length; i++) {\n const result = orgResults[i];\n if (result.status === 'rejected') {\n this.logger.warn(\n `[catalog-module-apigee] Org '${this.orgsConfig[i].name}' sync failed: ${result.reason}`,\n );\n } else {\n orgDataList.push(result.value);\n }\n }\n\n const totalProxies = orgDataList.reduce(\n (sum, o) => sum + o.proxyMap.size,\n 0,\n );\n const totalSharedflows = orgDataList.reduce(\n (sum, o) => sum + o.sharedflows.length,\n 0,\n );\n const totalHubApis = orgDataList.reduce(\n (sum, o) => sum + o.apiHubApis.length,\n 0,\n );\n this.logger.info(\n `[catalog-module-apigee] Fetched ${orgDataList.length}/${this.orgsConfig.length} org(s): ` +\n `${totalProxies} proxies, ${totalHubApis} API Hub definitions, ${totalSharedflows} sharedflows`,\n );\n\n // ── Step 3: Deduplicate proxies by slug across orgs (D-03) ────────────\n const mergedProxyMap = new Map<string, MergedProxy>();\n for (const orgData of orgDataList) {\n for (const [slug, partial] of orgData.proxyMap) {\n const existing = mergedProxyMap.get(slug);\n if (existing) {\n // Same proxy in multiple orgs: merge orgs array\n existing.orgs.push(...partial.orgs);\n // Merge base paths (deduplicate)\n for (const bp of partial.basePaths) {\n if (!existing.basePaths.includes(bp)) {\n existing.basePaths.push(bp);\n }\n }\n } else {\n mergedProxyMap.set(slug, {\n proxy: partial.proxy,\n basePaths: [...partial.basePaths],\n orgs: [...partial.orgs],\n });\n }\n }\n }\n\n // ── Step 4: Build entities ─────────────────────────────────────────────\n const entities: Entity[] = [];\n\n // Combined API Hub lookup across orgs, keyed by slugified displayName\n // (the human identifier; the resource name's last segment is an opaque\n // UUID and never matches a proxy slug, D-01). First org wins on collision.\n const apiHubById = new Map<string, { api: ApiHubApi; orgData: OrgData }>();\n // proxyName → API Hub apiId overrides collected across orgs (D-01).\n const mappingOverrides = new Map<string, string>();\n for (const orgData of orgDataList) {\n for (const hubApi of orgData.apiHubApis) {\n const id = slugify(\n hubApi.displayName ?? hubApi.name.split('/').pop() ?? '',\n );\n if (id && !apiHubById.has(id)) {\n apiHubById.set(id, { api: hubApi, orgData });\n }\n }\n if (orgData.apiHubMappings) {\n for (const [proxyName, hubId] of Object.entries(orgData.apiHubMappings)) {\n if (!mappingOverrides.has(proxyName)) {\n mappingOverrides.set(proxyName, hubId);\n }\n }\n }\n }\n\n const matchedHubIds = new Set<string>();\n\n // First pass: build entity pairs and split into matched (need a spec fetch)\n // vs. unmatched (ready to emit). Spec fetches are deferred so they can run\n // with bounded concurrency instead of one-at-a-time (WR-03).\n interface MatchedItem {\n component: Entity;\n api: Entity;\n hubApi: ApiHubApi;\n githubOrgSlug?: string;\n realApiId: string;\n projectId: string;\n }\n const matchedItems: MatchedItem[] = [];\n\n for (const [slug, merged] of mergedProxyMap.entries()) {\n // Resolve API type: try each org's apiTypeMap (D-01)\n let apiType: string | undefined;\n for (const orgData of orgDataList) {\n const t =\n orgData.apiTypeMap.get(slug) ??\n orgData.apiTypeMap.get(merged.proxy.name.toLowerCase());\n if (t) {\n apiType = t;\n break;\n }\n }\n const [component, api] = proxyToEntities(merged, apiType, this.options);\n\n // Match proxy → API Hub definition: apiHubMappings override else\n // slug == lowercased apiId (D-01).\n const candidate = (\n mappingOverrides.get(merged.proxy.name) ?? slug\n ).toLowerCase();\n const match = apiHubById.get(candidate);\n if (match) {\n matchedHubIds.add(candidate);\n matchedItems.push({\n component,\n api,\n hubApi: match.api,\n githubOrgSlug: match.orgData.githubOrgSlug,\n realApiId: match.api.name.split('/').pop() ?? candidate,\n projectId: match.orgData.projectId,\n });\n } else {\n // No API Hub match: API keeps has-spec:'false' from proxyToEntities.\n entities.push(component, api);\n }\n }\n\n // D-02/D-03: resolve specs with bounded concurrency, then assemble entities\n // in original order. Per-spec try/catch isolation lives in resolveSpecForApi.\n const matchedSpecs = await this.mapWithConcurrency(\n matchedItems,\n SPEC_FETCH_CONCURRENCY,\n item => this.resolveSpecForApi(item.realApiId, item.projectId),\n );\n matchedItems.forEach((item, i) => {\n entities.push(\n item.component,\n enrichMatchedApiEntity(\n item.api,\n item.hubApi,\n matchedSpecs[i],\n item.githubOrgSlug,\n this.options,\n ),\n );\n });\n\n // Append hub-only API entities for API Hub definitions with no matching\n // proxy (D-04, CAT-05). De-dupe by entity name for hub apis shared across orgs.\n const hubOnlyItems: Array<{\n hubApi: ApiHubApi;\n orgData: OrgData;\n realApiId: string;\n }> = [];\n for (const [id, { api: hubApi, orgData }] of apiHubById.entries()) {\n if (matchedHubIds.has(id)) continue;\n hubOnlyItems.push({\n hubApi,\n orgData,\n realApiId: hubApi.name.split('/').pop() ?? id,\n });\n }\n const hubOnlySpecs = await this.mapWithConcurrency(\n hubOnlyItems,\n SPEC_FETCH_CONCURRENCY,\n item => this.resolveSpecForApi(item.realApiId, item.orgData.projectId),\n );\n const hubOnlyNames = new Set<string>();\n hubOnlyItems.forEach((item, i) => {\n const entity = hubOnlyToEntity(\n item.hubApi,\n item.orgData.orgName,\n hubOnlySpecs[i],\n item.orgData.githubOrgSlug,\n this.options,\n );\n if (hubOnlyNames.has(entity.metadata.name)) return;\n hubOnlyNames.add(entity.metadata.name);\n entities.push(entity);\n });\n\n // Add sharedflow entities\n for (const orgData of orgDataList) {\n for (const sf of orgData.sharedflows) {\n entities.push(sharedflowToEntity(sf, orgData.orgName, this.options));\n }\n }\n\n this.logger.info(\n `[catalog-module-apigee] Catalog sync complete — emitting ${entities.length} entities`,\n );\n\n // ── Step 5: Single full mutation ──────────────────────────────────────\n await this.connection.applyMutation({\n type: 'full',\n entities: entities.map(entity => ({\n entity,\n locationKey: this.getProviderName(),\n })),\n });\n }\n\n // ── Private helpers ──────────────────────────────────────────────────────\n\n /**\n * Maps `items` through `fn` with at most `limit` invocations in flight at\n * once, preserving input order in the returned array. Used to parallelise\n * per-API spec resolution without overwhelming the API Hub backend (WR-03).\n */\n private async mapWithConcurrency<T, R>(\n items: T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n ): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const workerCount = Math.max(1, Math.min(limit, items.length));\n const worker = async (): Promise<void> => {\n while (true) {\n const i = next;\n next += 1;\n if (i >= items.length) break;\n results[i] = await fn(items[i]);\n }\n };\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n }\n\n /**\n * Fetches a spec's contents subject to the size guard (D-03).\n *\n * Two-stage guard:\n * 1. Pre-fetch: if the spec metadata reports `sizeBytes > specMaxBytes`, skip\n * the download entirely and return a `specUrl` link. NOTE: the API Hub spec\n * metadata endpoint frequently reports `sizeBytes: 0`/undefined, so this\n * stage often cannot fire and the contents are downloaded anyway.\n * 2. Post-decode: enforce `specMaxBytes` against the actual decoded byte\n * length — this is the guard that reliably bounds the *embedded* size.\n *\n * Because stage 1 is unreliable, `specMaxBytes` bounds the size of the spec\n * that gets embedded, not necessarily the size downloaded into memory.\n */\n private async fetchSpecEmbed(\n apiId: string,\n versionId: string,\n specId: string,\n projectId: string,\n ): Promise<SpecEmbed> {\n const spec = await this.apiHubClient.getSpec(\n apiId,\n versionId,\n specId,\n projectId,\n );\n const limit = this.options.specMaxBytes ?? 512000;\n const size = spec.sizeBytes ?? 0;\n if (size > limit) {\n // D-03 stage 1: over the size limit by metadata — link instead of embedding.\n this.logger.warn(\n `[catalog-module-apigee] Spec '${spec.name}' (${size} bytes) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`,\n );\n return { hasSpec: true, specUrl: spec.name };\n }\n const contents = await this.apiHubClient.getSpecContents(\n apiId,\n versionId,\n specId,\n projectId,\n );\n const base64 = contents.contents ?? '';\n const definition = Buffer.from(base64, 'base64').toString('utf-8');\n const decodedBytes = Buffer.byteLength(definition, 'utf-8');\n // Trace decode results so spec-shape regressions (e.g. a renamed contents\n // field, or malformed base64 decoding to garbage) are easy to diagnose.\n this.logger.debug(\n `[catalog-module-apigee] Resolved spec '${spec.name}' (mimeType=${\n contents.mimeType ?? '?'\n }, ${decodedBytes} bytes decoded, metadata sizeBytes=${\n spec.sizeBytes ?? 'n/a'\n })`,\n );\n if (!base64) {\n // Empty payload — record as \"no spec\" rather than embedding an empty string.\n this.logger.warn(\n `[catalog-module-apigee] Spec '${spec.name}' returned empty contents; treating as no embeddable spec`,\n );\n return { hasSpec: false };\n }\n // D-03 stage 2: the pre-fetch guard can be bypassed when sizeBytes is 0,\n // so enforce the limit against the actual decoded byte length.\n if (decodedBytes > limit) {\n this.logger.warn(\n `[catalog-module-apigee] Spec '${spec.name}' (${decodedBytes} bytes decoded) exceeds specMaxBytes (${limit}); linking via spec-url instead of embedding`,\n );\n return { hasSpec: true, specUrl: spec.name };\n }\n return { hasSpec: true, definition };\n }\n\n /**\n * Resolves the spec for an API: lists versions, deterministically selects one\n * (D-02), enumerates that version's specs (the versions endpoint does not\n * reliably populate specIds), picks the lexicographically smallest spec ID,\n * then fetches it with the size guard. Per-spec failures are isolated: any\n * error is logged and `undefined` is returned so one bad spec never aborts\n * the whole sync.\n */\n private async resolveSpecForApi(\n apiId: string,\n projectId: string,\n ): Promise<SpecEmbed | undefined> {\n try {\n const versions = await this.apiHubClient.listVersions(apiId, projectId);\n const selection = selectSpec(versions);\n if (!selection) return undefined;\n const specs = await this.apiHubClient.listSpecs(\n apiId,\n selection.versionId,\n projectId,\n );\n const specId = specs\n .map(s => s.name?.split('/').pop() ?? '')\n .filter(Boolean)\n .sort()[0];\n if (!specId) return undefined;\n return await this.fetchSpecEmbed(\n apiId,\n selection.versionId,\n specId,\n projectId,\n );\n } catch (e) {\n this.logger.warn(\n `[catalog-module-apigee] spec fetch failed for ${apiId}: ${e}`,\n );\n return undefined;\n }\n }\n\n /**\n * Fetches all data for one org: proxies, their deployments + revisions,\n * API Hub type map, and sharedflows.\n */\n private async fetchOrgData(orgCfg: OrgConfig): Promise<OrgData> {\n const [proxies, apiHubApis, sharedflows] = await Promise.all([\n this.apigeeClient.listProxies(orgCfg.name),\n this.apiHubClient.listApis(orgCfg.projectId),\n this.sharedflowClient.listSharedflows(orgCfg.name),\n ]);\n\n // Build API Hub type map: slugified displayName → apiType\n const apiTypeMap = new Map<string, string>();\n for (const api of apiHubApis) {\n const id = slugify(api.displayName ?? api.name.split('/').pop() ?? '');\n if (id && api.apiType) {\n apiTypeMap.set(id, api.apiType);\n }\n }\n\n // Fetch deployments + latest revision for all proxies in parallel\n const proxyMap = new Map<string, MergedProxy>();\n await Promise.all(\n proxies.map(async proxy => {\n try {\n const latestRev = proxy.revision?.length\n ? proxy.revision[proxy.revision.length - 1]\n : undefined;\n\n const [deployments, revision] = await Promise.all([\n this.apigeeClient.listDeployments(orgCfg.name, proxy.name),\n latestRev\n ? this.apigeeClient.getProxyRevision(\n orgCfg.name,\n proxy.name,\n latestRev,\n )\n : Promise.resolve({ name: proxy.name, basepaths: [] as string[] }),\n ]);\n\n // Derive lifecycle and environment list from ready deployments\n const readyDeps = deployments.filter(\n d => d.deploymentStatus === 'READY',\n );\n const lifecycle =\n readyDeps.length > 0 ? 'production' : 'experimental';\n const environments = readyDeps.map(d => d.environment);\n\n const orgSyncData: OrgSyncData = {\n slug: slugify(orgCfg.name),\n name: orgCfg.name,\n projectId: orgCfg.projectId,\n environments,\n lifecycle,\n };\n\n proxyMap.set(slugify(proxy.name), {\n proxy,\n basePaths: revision.basepaths ?? [],\n orgs: [orgSyncData],\n });\n } catch (e) {\n this.logger.warn(\n `[catalog-module-apigee] Skipping proxy '${proxy.name}' in org '${orgCfg.name}': ${e}`,\n );\n }\n }),\n );\n\n return {\n orgName: orgCfg.name,\n proxyMap,\n sharedflows,\n apiTypeMap,\n apiHubApis,\n projectId: orgCfg.projectId,\n githubOrgSlug: orgCfg.githubOrgSlug,\n apiHubMappings: orgCfg.apiHubMappings,\n };\n }\n}\n"],"names":["slugify","proxyToEntities","enrichMatchedApiEntity","hubOnlyToEntity","sharedflowToEntity","selectSpec"],"mappings":"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"}
File without changes
@@ -1 +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;;;;;;"}
1
+ {"version":3,"file":"fetch-utils.cjs.js","sources":["../../src/lib/fetch-utils.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport fetch from 'node-fetch';\n\n/**\n * HTTP status codes that are NOT worth retrying — fail immediately.\n * 429 (rate limit) IS retried (not in this set).\n */\nexport const NON_RETRYABLE_STATUS = new Set([400, 401, 403, 404]);\n\n/** Maximum retry attempts (not counting the initial attempt). */\nexport const MAX_RETRIES = 3;\n\n/** Base delays in milliseconds for attempts 0, 1, 2. */\nconst BASE_DELAYS_MS = [1000, 2000, 4000];\n\n/**\n * Fetches a URL with Bearer auth token injection and exponential back-off retry.\n *\n * @param url - Full HTTPS URL to fetch (GET)\n * @param getToken - Async function that returns a fresh access token string.\n * Pass `() => auth.getAccessToken()` from google-auth-library GoogleAuth instance.\n * @returns Parsed JSON response body\n * @throws Error with HTTP status + URL for non-retryable errors or exhausted retries\n */\nexport async function fetchWithRetry(\n url: string,\n getToken: () => Promise<string | null | undefined>,\n): Promise<unknown> {\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n const token = await getToken();\n if (!token) {\n throw new Error(`fetchWithRetry: unable to obtain access token for ${url}`);\n }\n\n const res = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: 'application/json',\n },\n });\n\n if (res.ok) {\n return res.json();\n }\n\n const errMessage = `HTTP ${res.status} ${res.statusText} for GET ${url}`;\n\n if (NON_RETRYABLE_STATUS.has(res.status)) {\n throw new Error(errMessage);\n }\n\n lastError = new Error(errMessage);\n\n if (attempt < MAX_RETRIES) {\n const baseDelay = BASE_DELAYS_MS[attempt] ?? 4000;\n // Jitter: ±20% of the base delay\n const jitter = baseDelay * 0.2 * (Math.random() * 2 - 1);\n await sleep(baseDelay + jitter);\n }\n }\n\n throw lastError ?? new Error(`fetchWithRetry: exhausted retries for ${url}`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, Math.max(0, ms)));\n}\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;;;;;;"}
File without changes
@@ -1 +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;;;;"}
1
+ {"version":3,"file":"sharedflow-client.cjs.js","sources":["../../src/lib/sharedflow-client.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { GoogleAuth } from 'google-auth-library';\nimport { fetchWithRetry } from './fetch-utils';\n\n/**\n * Minimal representation of an Apigee sharedflow as returned by\n * GET /v1/organizations/{org}/sharedflows\n */\nexport interface ApigeeSharedflow {\n name: string;\n latestRevisionId?: string;\n metaData?: {\n createdAt?: string;\n lastModifiedAt?: string;\n };\n}\n\n/**\n * Client for the Apigee Management API's sharedflow endpoints.\n */\nexport class SharedflowClient {\n constructor(private readonly auth: GoogleAuth) {}\n\n /**\n * Lists all sharedflows in the given Apigee organisation.\n */\n async listSharedflows(org: string): Promise<ApigeeSharedflow[]> {\n const url = `https://apigee.googleapis.com/v1/organizations/${encodeURIComponent(org)}/sharedflows`;\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\n return (body as { sharedFlows?: ApigeeSharedflow[] }).sharedFlows ?? [];\n }\n}\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;;;;"}
File without changes
@@ -1 +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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"}
1
+ {"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n coreServices,\n createBackendModule,\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n} from '@backstage/backend-plugin-api';\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';\nimport { GoogleAuth } from 'google-auth-library';\nimport { ApigeeClient } from './lib/apigee-client';\nimport { ApiHubClient } from './lib/api-hub-client';\nimport { SharedflowClient } from './lib/sharedflow-client';\nimport { ApigeeEntityProvider } from './lib/entity-provider';\nimport { ApigeeStitchingProcessor } from './lib/apigee-stitching-processor';\n\n/**\n * Backstage catalog backend module for Apigee API discovery.\n * Plugs into the catalog backend and registers Apigee proxies + API Hub\n * definitions as Backstage API entities.\n */\nexport const catalogModuleApigee = createBackendModule({\n pluginId: 'catalog',\n moduleId: 'apigee-catalog',\n register(env) {\n env.registerInit({\n deps: {\n catalog: catalogProcessingExtensionPoint,\n scheduler: coreServices.scheduler,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n },\n async init({ catalog, scheduler, logger, config }) {\n // AUTH-03: Fail fast if apigee.orgs is missing or empty.\n let orgs: ReturnType<typeof config.getConfigArray>;\n try {\n orgs = config.getConfigArray('apigee.orgs');\n } catch {\n throw new Error(\n 'Apigee catalog module misconfigured: \"apigee.orgs\" is required in app-config.yaml.\\n' +\n 'Example:\\n' +\n ' apigee:\\n' +\n ' orgs:\\n' +\n ' - name: my-org\\n' +\n ' projectId: my-gcp-project',\n );\n }\n if (orgs.length === 0) {\n throw new Error(\n 'Apigee catalog module misconfigured: \"apigee.orgs\" must contain at least one entry.',\n );\n }\n logger.info(\n `[catalog-module-apigee] Initialized with ${orgs.length} org(s): ` +\n orgs.map(o => o.getString('name')).join(', '),\n );\n\n // Build task schedule — prefer apigee.schedule config, fall back to 60-min default\n const scheduleConfig = config.has('apigee.schedule')\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n config.getConfig('apigee.schedule'),\n )\n : {\n frequency: { minutes: 60 },\n timeout: { minutes: 10 },\n initialDelay: { seconds: 30 },\n };\n const taskRunner = scheduler.createScheduledTaskRunner(scheduleConfig);\n\n // Shared GoogleAuth instance (AUTH-01: GCP Application Default Credentials)\n const auth = new GoogleAuth({\n scopes: ['https://www.googleapis.com/auth/cloud-platform'],\n });\n\n // Instantiate clients (ApiHubClient: no projectId needed; provided per-call by entity provider)\n const apigeeClient = new ApigeeClient(auth);\n const apiHubClient = new ApiHubClient(\n auth,\n '',\n config.getOptionalString('apigee.apiHubLocation') ?? 'global',\n );\n const sharedflowClient = new SharedflowClient(auth);\n\n // Build orgsConfig from config\n const orgsConfig = orgs.map(orgCfg => ({\n name: orgCfg.getString('name'),\n projectId: orgCfg.getString('projectId'),\n environments: orgCfg.has('environments')\n ? orgCfg.getStringArray('environments')\n : undefined,\n githubOrgSlug: orgCfg.getOptionalString('githubOrgSlug'),\n // D-01: explicit proxy-name -> API Hub apiId overrides per org\n apiHubMappings: orgCfg.has('apiHubMappings')\n ? (orgCfg.getConfig('apiHubMappings').get() as Record<string, string>)\n : undefined,\n }));\n\n // Register the entity provider with the catalog\n const provider = new ApigeeEntityProvider(\n taskRunner,\n logger,\n orgsConfig,\n apigeeClient,\n apiHubClient,\n sharedflowClient,\n {\n defaultOwner: config.getOptionalString('apigee.defaultOwner'),\n // D-03: inline spec size guard (bytes); provider falls back to 512000\n specMaxBytes: config.getOptionalNumber('apigee.specMaxBytes'),\n },\n );\n catalog.addEntityProvider(provider);\n\n // Phase 4 (STITCH-01): register the repository-stitching processor so\n // annotated backend Components are linked to their proxy API entities.\n catalog.addProcessor(new ApigeeStitchingProcessor(logger));\n logger.info(\n '[catalog-module-apigee] Registered Apigee stitching processor',\n );\n },\n });\n },\n});\n\n"],"names":["createBackendModule","catalogProcessingExtensionPoint","coreServices","readSchedulerServiceTaskScheduleDefinitionFromConfig","GoogleAuth","apigeeClient","ApigeeClient","apiHubClient","ApiHubClient","sharedflowClient","SharedflowClient","ApigeeEntityProvider","ApigeeStitchingProcessor"],"mappings":"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"}
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n coreServices,\r\n createBackendPlugin,\r\n} from '@backstage/backend-plugin-api';\r\nimport { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';\r\nimport { createRouter } from './router';\r\n\r\nexport const apigeePlugin = createBackendPlugin({\r\n pluginId: 'apigee',\r\n register(env) {\r\n env.registerInit({\r\n deps: {\r\n httpRouter: coreServices.httpRouter,\r\n logger: coreServices.logger,\r\n config: coreServices.rootConfig,\r\n },\r\n async init({ httpRouter, logger, config }) {\r\n const router = await createRouter({ logger });\r\n const middleware = MiddlewareFactory.create({ config, logger });\r\n\r\n router.use(middleware.notFound());\r\n router.use(middleware.error);\r\n\r\n httpRouter.use(router);\r\n },\r\n });\r\n },\r\n});\r\n"],"names":["createBackendPlugin","coreServices","router","createRouter","MiddlewareFactory"],"mappings":";;;;;;AAQO,MAAM,eAAeA,oCAAA,CAAoB;AAAA,EAC9C,QAAA,EAAU,QAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa;AAAA,OACvB;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,UAAA,EAAY,MAAA,EAAQ,QAAO,EAAG;AACzC,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa,EAAE,QAAQ,CAAA;AAC5C,QAAA,MAAM,aAAaC,gCAAA,CAAkB,MAAA,CAAO,EAAE,MAAA,EAAQ,QAAQ,CAAA;AAE9D,QAAAF,QAAA,CAAO,GAAA,CAAI,UAAA,CAAW,QAAA,EAAU,CAAA;AAChC,QAAAA,QAAA,CAAO,GAAA,CAAI,WAAW,KAAK,CAAA;AAE3B,QAAA,UAAA,CAAW,IAAIA,QAAM,CAAA;AAAA,MACvB;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';\nimport { createRouter } from './router';\n\nexport const apigeePlugin = createBackendPlugin({\n pluginId: 'apigee',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n },\n async init({ httpRouter, logger, config }) {\n const router = await createRouter({ logger });\n const middleware = MiddlewareFactory.create({ config, logger });\n\n router.use(middleware.notFound());\n router.use(middleware.error);\n\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","router","createRouter","MiddlewareFactory"],"mappings":";;;;;;AAQO,MAAM,eAAeA,oCAAA,CAAoB;AAAA,EAC9C,QAAA,EAAU,QAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa;AAAA,OACvB;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,UAAA,EAAY,MAAA,EAAQ,QAAO,EAAG;AACzC,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa,EAAE,QAAQ,CAAA;AAC5C,QAAA,MAAM,aAAaC,gCAAA,CAAkB,MAAA,CAAO,EAAE,MAAA,EAAQ,QAAQ,CAAA;AAE9D,QAAAF,QAAA,CAAO,GAAA,CAAI,UAAA,CAAW,QAAA,EAAU,CAAA;AAChC,QAAAA,QAAA,CAAO,GAAA,CAAI,WAAW,KAAK,CAAA;AAE3B,QAAA,UAAA,CAAW,IAAIA,QAAM,CAAA;AAAA,MACvB;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { LoggerService } from '@backstage/backend-plugin-api';\r\nimport express from 'express';\r\n\r\ntype RouterOptions = {\r\n logger: LoggerService;\r\n};\r\n\r\nexport async function createRouter({\r\n logger,\r\n}: RouterOptions): Promise<express.Router> {\r\n const router = express.Router();\r\n\r\n router.use(express.json());\r\n\r\n router.get('/health', (_req, res) => {\r\n logger.debug('Serving Apigee bootstrap health response');\r\n res.json({ status: 'ok', pluginId: 'apigee' });\r\n });\r\n\r\n router.get('/hello', (_req, res) => {\r\n logger.info('Serving Apigee hello response');\r\n res.json({ message: 'Hello from the Apigee backend!', timestamp: new Date().toISOString() });\r\n });\r\n\r\n return router;\r\n}\r\n"],"names":["express"],"mappings":";;;;;;;;AAQA,eAAsB,YAAA,CAAa;AAAA,EACjC;AACF,CAAA,EAA2C;AACzC,EAAA,MAAM,MAAA,GAASA,yBAAQ,MAAA,EAAO;AAE9B,EAAA,MAAA,CAAO,GAAA,CAAIA,wBAAA,CAAQ,IAAA,EAAM,CAAA;AAEzB,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,CAAC,IAAA,EAAM,GAAA,KAAQ;AACnC,IAAA,MAAA,CAAO,MAAM,0CAA0C,CAAA;AACvD,IAAA,GAAA,CAAI,KAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU,UAAU,CAAA;AAAA,EAC/C,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,QAAA,EAAU,CAAC,IAAA,EAAM,GAAA,KAAQ;AAClC,IAAA,MAAA,CAAO,KAAK,+BAA+B,CAAA;AAC3C,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,OAAA,EAAS,gCAAA,EAAkC,SAAA,EAAA,qBAAe,IAAA,EAAK,EAAE,WAAA,EAAY,EAAG,CAAA;AAAA,EAC7F,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport express from 'express';\n\ntype RouterOptions = {\n logger: LoggerService;\n};\n\nexport async function createRouter({\n logger,\n}: RouterOptions): Promise<express.Router> {\n const router = express.Router();\n\n router.use(express.json());\n\n router.get('/health', (_req, res) => {\n logger.debug('Serving Apigee bootstrap health response');\n res.json({ status: 'ok', pluginId: 'apigee' });\n });\n\n router.get('/hello', (_req, res) => {\n logger.info('Serving Apigee hello response');\n res.json({ message: 'Hello from the Apigee backend!', timestamp: new Date().toISOString() });\n });\n\n return router;\n}\n"],"names":["express"],"mappings":";;;;;;;;AAQA,eAAsB,YAAA,CAAa;AAAA,EACjC;AACF,CAAA,EAA2C;AACzC,EAAA,MAAM,MAAA,GAASA,yBAAQ,MAAA,EAAO;AAE9B,EAAA,MAAA,CAAO,GAAA,CAAIA,wBAAA,CAAQ,IAAA,EAAM,CAAA;AAEzB,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,CAAC,IAAA,EAAM,GAAA,KAAQ;AACnC,IAAA,MAAA,CAAO,MAAM,0CAA0C,CAAA;AACvD,IAAA,GAAA,CAAI,KAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU,UAAU,CAAA;AAAA,EAC/C,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,QAAA,EAAU,CAAC,IAAA,EAAM,GAAA,KAAQ;AAClC,IAAA,MAAA,CAAO,KAAK,+BAA+B,CAAA;AAC3C,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,OAAA,EAAS,gCAAA,EAAkC,SAAA,EAAA,qBAAe,IAAA,EAAK,EAAE,WAAA,EAAY,EAAG,CAAA;AAAA,EAC7F,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nospt/backstage-plugin-apigee-backend",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Backstage backend plugin bootstrap for Apigee integration",
6
6
  "author": "NOS SGPS, S.A.",
@@ -48,7 +48,7 @@
48
48
  "@backstage/backend-plugin-api": "^1.8.0",
49
49
  "@backstage/catalog-model": "^1.7.3",
50
50
  "@backstage/plugin-catalog-node": "^1.13.0",
51
- "@nospt/backstage-plugin-apigee-common": "workspace:^",
51
+ "@nospt/backstage-plugin-apigee-common": "^0.1.2",
52
52
  "express": "^4.17.1",
53
53
  "google-auth-library": "^9.15.1",
54
54
  "node-fetch": "^2.7.0"
@@ -75,4 +75,4 @@
75
75
  ]
76
76
  }
77
77
  }
78
- }
78
+ }