@nospt/backstage-plugin-apigee-backend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ var fetchUtils = require('./fetch-utils.cjs.js');
4
+
5
+ const APIHUB_BASE_URL = "https://apihub.googleapis.com/v1";
6
+ class ApiHubClient {
7
+ /**
8
+ * @param auth - Shared GoogleAuth instance (D-03: injected by module.ts, not created here)
9
+ * @param projectId - GCP project that hosts the API Hub instance
10
+ * @param location - API Hub location (default: "global" — override if regional instance)
11
+ */
12
+ constructor(auth, projectId = "", location = "global") {
13
+ this.auth = auth;
14
+ this.projectId = projectId;
15
+ this.location = location;
16
+ }
17
+ auth;
18
+ projectId;
19
+ location;
20
+ /** Base path prefix shared by all API Hub resource URLs for this instance. */
21
+ resourcePrefix(projectId) {
22
+ const proj = projectId ?? this.projectId;
23
+ return `${APIHUB_BASE_URL}/projects/${encodeURIComponent(proj)}/locations/${encodeURIComponent(this.location)}`;
24
+ }
25
+ /**
26
+ * Fetches every page of a paginated API Hub list endpoint, following
27
+ * `nextPageToken` until the collection is exhausted, and returns the
28
+ * concatenated items. API Hub caps list responses (commonly at 50 entries per
29
+ * page), so callers MUST page or risk silently truncating the catalogue.
30
+ */
31
+ async fetchAllPages(baseUrl, collection) {
32
+ const items = [];
33
+ let pageToken;
34
+ do {
35
+ const sep = baseUrl.includes("?") ? "&" : "?";
36
+ const url = pageToken ? `${baseUrl}${sep}pageToken=${encodeURIComponent(pageToken)}` : baseUrl;
37
+ const body = await fetchUtils.fetchWithRetry(
38
+ url,
39
+ () => this.auth.getAccessToken()
40
+ );
41
+ const page = body[collection] ?? [];
42
+ items.push(...page);
43
+ pageToken = typeof body.nextPageToken === "string" && body.nextPageToken.length > 0 ? body.nextPageToken : void 0;
44
+ } while (pageToken);
45
+ return items;
46
+ }
47
+ /**
48
+ * Lists all API definitions in this API Hub instance.
49
+ * Calls: GET /projects/{p}/locations/{l}/apis
50
+ *
51
+ * @param projectId - Optional override for the GCP project ID. When provided,
52
+ * overrides the instance-level projectId from the constructor. Allows a single
53
+ * ApiHubClient instance to serve multiple GCP projects (multi-org setups).
54
+ *
55
+ * Pages through all results, following nextPageToken until exhausted (API Hub
56
+ * caps list responses, commonly at 50 entries per page).
57
+ */
58
+ async listApis(projectId) {
59
+ const url = `${this.resourcePrefix(projectId)}/apis`;
60
+ return this.fetchAllPages(url, "apis");
61
+ }
62
+ /**
63
+ * Lists all versions for a given API.
64
+ * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions
65
+ *
66
+ * @param apiId - Short API ID (last segment of the full resource name)
67
+ * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.
68
+ */
69
+ async listVersions(apiId, projectId) {
70
+ const url = `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}/versions`;
71
+ return this.fetchAllPages(url, "versions");
72
+ }
73
+ /**
74
+ * Lists all specs attached to a given API version.
75
+ * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs
76
+ *
77
+ * This is the authoritative source of spec IDs for a version. The versions
78
+ * list endpoint frequently returns version.specIds: null even when specs
79
+ * exist, so callers must enumerate specs here rather than trusting specIds.
80
+ *
81
+ * @param apiId - Short API ID (last segment of the full resource name)
82
+ * @param versionId - Short version ID (last segment of the version resource name)
83
+ * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.
84
+ */
85
+ async listSpecs(apiId, versionId, projectId) {
86
+ const url = `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}/versions/${encodeURIComponent(versionId)}/specs`;
87
+ return this.fetchAllPages(url, "specs");
88
+ }
89
+ /**
90
+ * Fetches the spec metadata for a given API version and spec ID.
91
+ * Calls: GET /projects/{p}/locations/{l}/apis/{apiId}/versions/{versionId}/specs/{specId}
92
+ *
93
+ * To retrieve the actual spec file content (base64), call getSpecContents() instead.
94
+ *
95
+ * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.
96
+ */
97
+ async getSpec(apiId, versionId, specId, projectId) {
98
+ const url = `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}/versions/${encodeURIComponent(versionId)}/specs/${encodeURIComponent(specId)}`;
99
+ return await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
100
+ }
101
+ /**
102
+ * Fetches the raw spec file content for a given spec resource.
103
+ * Calls: GET .../specs/{specId}:contents
104
+ * Returns base64-encoded content in the `contents` field — caller must decode
105
+ * with Buffer.from(contents, 'base64').toString('utf-8').
106
+ *
107
+ * Used in Phase 3 for spec.definition population (with 500 KB size guard).
108
+ *
109
+ * @param projectId - Optional GCP project override for multi-org setups; defaults to the instance projectId.
110
+ */
111
+ async getSpecContents(apiId, versionId, specId, projectId) {
112
+ const url = `${this.resourcePrefix(projectId)}/apis/${encodeURIComponent(apiId)}/versions/${encodeURIComponent(versionId)}/specs/${encodeURIComponent(specId)}:contents`;
113
+ return await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
114
+ }
115
+ }
116
+
117
+ exports.ApiHubClient = ApiHubClient;
118
+ //# sourceMappingURL=api-hub-client.cjs.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ var fetchUtils = require('./fetch-utils.cjs.js');
4
+
5
+ const APIGEE_BASE_URL = "https://apigee.googleapis.com/v1";
6
+ class ApigeeClient {
7
+ constructor(auth) {
8
+ this.auth = auth;
9
+ }
10
+ auth;
11
+ /**
12
+ * Lists all API proxies in the given Apigee organisation.
13
+ * Calls: GET /organizations/{org}/apis
14
+ */
15
+ async listProxies(org) {
16
+ const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis`;
17
+ const body = await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
18
+ return body.proxies ?? [];
19
+ }
20
+ /**
21
+ * Lists all environment deployments for a given proxy.
22
+ * Calls: GET /organizations/{org}/apis/{proxy}/deployments
23
+ */
24
+ async listDeployments(org, proxy) {
25
+ const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis/${encodeURIComponent(proxy)}/deployments`;
26
+ const body = await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
27
+ return body.deployments ?? [];
28
+ }
29
+ /**
30
+ * Fetches full detail for a specific proxy revision.
31
+ * Calls: GET /organizations/{org}/apis/{proxy}/revisions/{rev}
32
+ */
33
+ async getProxyRevision(org, proxy, rev) {
34
+ const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis/${encodeURIComponent(proxy)}/revisions/${encodeURIComponent(rev)}`;
35
+ return await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
36
+ }
37
+ }
38
+
39
+ exports.ApigeeClient = ApigeeClient;
40
+ //# sourceMappingURL=apigee-client.cjs.js.map
@@ -0,0 +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;;;;"}
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ var catalogModel = require('@backstage/catalog-model');
4
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
5
+ var entityBuilder = require('./entity-builder.cjs.js');
6
+
7
+ const ANNOTATION_API_NAME = "nos.pt/apigee-api-name";
8
+ const ANNOTATION_PROJECT_ID = "nos.pt/apigee-project-id";
9
+ class ApigeeStitchingProcessor {
10
+ constructor(logger) {
11
+ this.logger = logger;
12
+ }
13
+ logger;
14
+ getProcessorName() {
15
+ return "ApigeeStitchingProcessor";
16
+ }
17
+ async postProcessEntity(entity, _location, emit) {
18
+ if (entity.kind !== "Component") {
19
+ return entity;
20
+ }
21
+ const annotations = entity.metadata.annotations ?? {};
22
+ const hasAnyApigeeAnnotation = Object.keys(annotations).some(
23
+ (key) => key.startsWith("nos.pt/apigee-")
24
+ );
25
+ if (!hasAnyApigeeAnnotation) {
26
+ return entity;
27
+ }
28
+ const target = this.resolveTarget(annotations, entity);
29
+ if (!target) {
30
+ return entity;
31
+ }
32
+ const componentRef = {
33
+ kind: "Component",
34
+ namespace: entity.metadata.namespace ?? "default",
35
+ name: entity.metadata.name
36
+ };
37
+ emit(
38
+ pluginCatalogNode.processingResult.relation({
39
+ source: componentRef,
40
+ target,
41
+ type: catalogModel.RELATION_PROVIDES_API
42
+ })
43
+ );
44
+ emit(
45
+ pluginCatalogNode.processingResult.relation({
46
+ source: target,
47
+ target: componentRef,
48
+ type: catalogModel.RELATION_API_PROVIDED_BY
49
+ })
50
+ );
51
+ return entity;
52
+ }
53
+ /**
54
+ * Strategy seam. Tries the TargetServer-preferred path first (deferred this
55
+ * phase), then falls back to the active direct-mapping path. First strategy
56
+ * to return a ref wins.
57
+ */
58
+ resolveTarget(annotations, entity) {
59
+ return this.resolveViaTargetServer(annotations, entity) ?? this.resolveViaDirect(annotations, entity);
60
+ }
61
+ /**
62
+ * Deferred: TargetServer-preferred path (STITCH-02). See 04-CONTEXT D-01.
63
+ * No TargetServer data exists in the codebase yet, so this is the clean
64
+ * insertion point for the future branch and intentionally returns undefined.
65
+ */
66
+ resolveViaTargetServer(_annotations, _entity) {
67
+ return void 0;
68
+ }
69
+ /**
70
+ * Direct-mapping path. Requires BOTH the api-name and project-id annotations.
71
+ * The proxy API entity ref is deterministic — `api:default/<slugify(api-name)>` —
72
+ * so no live catalog lookup is needed (D-01). projectId is validated for
73
+ * presence to confirm intent but is not part of the deterministic ref.
74
+ */
75
+ resolveViaDirect(annotations, entity) {
76
+ const apiName = annotations[ANNOTATION_API_NAME]?.trim();
77
+ const projectId = annotations[ANNOTATION_PROJECT_ID]?.trim();
78
+ const hasApiName = Boolean(apiName);
79
+ const hasProjectId = Boolean(projectId);
80
+ if (hasApiName !== hasProjectId) {
81
+ const missing = hasApiName ? ANNOTATION_PROJECT_ID : ANNOTATION_API_NAME;
82
+ this.logger.warn(
83
+ `[ApigeeStitchingProcessor] Component "${entity.metadata.name}" has a partial Apigee direct-mapping annotation set; missing "${missing}". Skipping stitching.`
84
+ );
85
+ return void 0;
86
+ }
87
+ if (!hasApiName) {
88
+ return void 0;
89
+ }
90
+ const slug = entityBuilder.slugify(apiName);
91
+ if (slug === "") {
92
+ this.logger.warn(
93
+ `[ApigeeStitchingProcessor] Component "${entity.metadata.name}" has an unresolvable "${ANNOTATION_API_NAME}" value; nothing to stitch.`
94
+ );
95
+ return void 0;
96
+ }
97
+ return { kind: "API", namespace: "default", name: slug };
98
+ }
99
+ }
100
+
101
+ exports.ANNOTATION_API_NAME = ANNOTATION_API_NAME;
102
+ exports.ANNOTATION_PROJECT_ID = ANNOTATION_PROJECT_ID;
103
+ exports.ApigeeStitchingProcessor = ApigeeStitchingProcessor;
104
+ //# sourceMappingURL=apigee-stitching-processor.cjs.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,253 @@
1
+ 'use strict';
2
+
3
+ var catalogModel = require('@backstage/catalog-model');
4
+
5
+ const LIFECYCLE_RANK = {
6
+ production: 2,
7
+ experimental: 1,
8
+ deprecated: 0
9
+ };
10
+ function resolveHighestLifecycle(orgs) {
11
+ if (orgs.length === 0) return "experimental";
12
+ return orgs.reduce((best, org) => {
13
+ const rank = LIFECYCLE_RANK[org.lifecycle] ?? 1;
14
+ const bestRank = LIFECYCLE_RANK[best] ?? 1;
15
+ return rank > bestRank ? org.lifecycle : best;
16
+ }, orgs[0].lifecycle);
17
+ }
18
+ const APIGEE_LOCATION = "url:https://apigee.com";
19
+ function slugify(name) {
20
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
21
+ }
22
+ function selectSpec(versions) {
23
+ const all = versions ?? [];
24
+ if (all.length === 0) return void 0;
25
+ const active = all.filter((v) => v.lifecycle?.stage === "ACTIVE");
26
+ const candidates = active.length > 0 ? active : all.filter((v) => v.lifecycle?.stage !== "ARCHIVED");
27
+ if (candidates.length === 0) return void 0;
28
+ const sorted = [...candidates].sort((a, b) => {
29
+ const at = a.updateTime ?? "";
30
+ const bt = b.updateTime ?? "";
31
+ if (at !== bt) {
32
+ if (!at) return 1;
33
+ if (!bt) return -1;
34
+ return at < bt ? 1 : -1;
35
+ }
36
+ return (a.name ?? "").localeCompare(b.name ?? "");
37
+ });
38
+ const chosen = sorted[0];
39
+ const versionId = (chosen.name ?? "").split("/").pop() ?? "";
40
+ if (!versionId) return void 0;
41
+ return { versionId };
42
+ }
43
+ function ownerRef(owner, defaultOwner) {
44
+ if (owner?.displayName) {
45
+ return `group:default/${slugify(owner.displayName)}`;
46
+ }
47
+ return defaultOwner ?? "group:default/unknown";
48
+ }
49
+ function categoryTags(categories) {
50
+ const slugs = (categories ?? []).map((c) => slugify(c)).filter((c) => c.length > 0);
51
+ return [...new Set(slugs)].sort();
52
+ }
53
+ function categoryAnnotation(categories) {
54
+ const tags = categoryTags(categories);
55
+ return tags.length > 0 ? tags.join(",") : void 0;
56
+ }
57
+ function resolveGithubLink(input) {
58
+ const httpUrl = /^https?:\/\//i;
59
+ const sources = (input.sourceMetadata ?? []).filter(
60
+ (s) => typeof s.value === "string" && httpUrl.test(s.value)
61
+ );
62
+ if (sources.length > 0) {
63
+ const preferred = sources.find((s) => /repo|git|source/i.test(s.sourceType ?? "")) ?? sources[0];
64
+ return { url: preferred.value, title: "Source repository" };
65
+ }
66
+ if (input.githubOrgSlug) {
67
+ return {
68
+ url: `https://github.com/${input.githubOrgSlug}/${input.entityName}`,
69
+ title: "Source repository"
70
+ };
71
+ }
72
+ return void 0;
73
+ }
74
+ function applyHubMetadata(api, hubApi, spec, githubOrgSlug) {
75
+ const annotations = api.metadata.annotations ?? {};
76
+ const tags = categoryTags(hubApi.categories);
77
+ if (tags.length > 0) {
78
+ api.metadata.tags = tags;
79
+ }
80
+ const catAnno = categoryAnnotation(hubApi.categories);
81
+ if (catAnno) {
82
+ annotations["apigee.com/category"] = catAnno;
83
+ }
84
+ const link = resolveGithubLink({
85
+ sourceMetadata: hubApi.sourceMetadata,
86
+ githubOrgSlug,
87
+ entityName: api.metadata.name
88
+ });
89
+ if (link) {
90
+ const links = api.metadata.links ?? [];
91
+ if (!links.some((l) => l.url === link.url)) {
92
+ links.push(link);
93
+ }
94
+ api.metadata.links = links;
95
+ }
96
+ if (spec?.definition) {
97
+ api.spec.definition = spec.definition;
98
+ annotations["apigee.com/has-spec"] = "true";
99
+ } else if (spec?.specUrl) {
100
+ annotations["apigee.com/spec-url"] = spec.specUrl;
101
+ annotations["apigee.com/has-spec"] = "true";
102
+ } else {
103
+ annotations["apigee.com/has-spec"] = "false";
104
+ }
105
+ api.metadata.annotations = annotations;
106
+ }
107
+ function enrichMatchedApiEntity(baseApi, hubApi, spec, githubOrgSlug, options) {
108
+ const api = {
109
+ ...baseApi,
110
+ metadata: {
111
+ ...baseApi.metadata,
112
+ annotations: { ...baseApi.metadata.annotations ?? {} },
113
+ ...baseApi.metadata.tags ? { tags: [...baseApi.metadata.tags] } : {},
114
+ ...baseApi.metadata.links ? { links: baseApi.metadata.links.map((l) => ({ ...l })) } : {}
115
+ },
116
+ spec: { ...baseApi.spec ?? {} }
117
+ };
118
+ if (hubApi.displayName) {
119
+ api.metadata.title = hubApi.displayName;
120
+ }
121
+ api.metadata.description = hubApi.description ?? "";
122
+ api.spec.owner = ownerRef(
123
+ hubApi.owner,
124
+ options?.defaultOwner
125
+ );
126
+ applyHubMetadata(api, hubApi, spec, githubOrgSlug);
127
+ return api;
128
+ }
129
+ function hubOnlyToEntity(hubApi, orgName, spec, githubOrgSlug, options) {
130
+ const apiId = (hubApi.name ?? "").split("/").pop() ?? "";
131
+ const display = hubApi.displayName ?? apiId;
132
+ const slug = slugify(display);
133
+ const api = {
134
+ apiVersion: "backstage.io/v1alpha1",
135
+ kind: "API",
136
+ metadata: {
137
+ name: slug,
138
+ title: display,
139
+ namespace: "default",
140
+ description: hubApi.description ?? "",
141
+ annotations: {
142
+ [catalogModel.ANNOTATION_LOCATION]: APIGEE_LOCATION,
143
+ [catalogModel.ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,
144
+ // Full API Hub resource name (D-04) — distinguishes hub-only entities.
145
+ "apigee.com/api-hub-id": hubApi.name,
146
+ "apigee.com/org": orgName
147
+ }
148
+ },
149
+ spec: {
150
+ type: hubApi.apiType ?? "other",
151
+ lifecycle: "experimental",
152
+ owner: ownerRef(hubApi.owner, options?.defaultOwner),
153
+ definition: ""
154
+ }
155
+ };
156
+ applyHubMetadata(api, hubApi, spec, githubOrgSlug);
157
+ return api;
158
+ }
159
+ function proxyToEntities(merged, apiType, options) {
160
+ const slug = slugify(merged.proxy.name);
161
+ const lifecycle = resolveHighestLifecycle(merged.orgs);
162
+ const owner = options?.defaultOwner ?? "group:default/unknown";
163
+ const orgsAnnotation = merged.orgs.map((o) => o.name).join(",");
164
+ const perOrgAnnotations = {};
165
+ for (const org of merged.orgs) {
166
+ const safe = slugify(org.slug || org.name);
167
+ perOrgAnnotations[`apigee.com/${safe}/environments`] = org.environments.join(",");
168
+ perOrgAnnotations[`apigee.com/${safe}/lifecycle`] = org.lifecycle;
169
+ }
170
+ const sharedAnnotations = {
171
+ [catalogModel.ANNOTATION_LOCATION]: APIGEE_LOCATION,
172
+ [catalogModel.ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,
173
+ "apigee.com/proxy-name": merged.proxy.name,
174
+ "apigee.com/orgs": orgsAnnotation
175
+ };
176
+ const component = {
177
+ apiVersion: "backstage.io/v1alpha1",
178
+ kind: "Component",
179
+ metadata: {
180
+ name: slug,
181
+ title: merged.proxy.name,
182
+ namespace: "default",
183
+ annotations: {
184
+ ...sharedAnnotations,
185
+ "apigee.com/base-path": merged.basePaths.join(","),
186
+ "apigee.com/has-spec": "false",
187
+ ...perOrgAnnotations
188
+ }
189
+ },
190
+ spec: {
191
+ type: "service",
192
+ lifecycle,
193
+ owner
194
+ }
195
+ };
196
+ const api = {
197
+ apiVersion: "backstage.io/v1alpha1",
198
+ kind: "API",
199
+ metadata: {
200
+ name: slug,
201
+ title: merged.proxy.name,
202
+ namespace: "default",
203
+ annotations: {
204
+ ...sharedAnnotations,
205
+ // Default false; 03-03 enrichment overrides to 'true' when a spec is found (META-05, SC #2).
206
+ "apigee.com/has-spec": "false"
207
+ }
208
+ },
209
+ spec: {
210
+ type: apiType ?? "other",
211
+ lifecycle,
212
+ owner,
213
+ definition: ""
214
+ }
215
+ };
216
+ return [component, api];
217
+ }
218
+ function sharedflowToEntity(sharedflow, orgName, options) {
219
+ const slug = slugify(sharedflow.name);
220
+ const owner = options?.defaultOwner ?? "group:default/unknown";
221
+ return {
222
+ apiVersion: "backstage.io/v1alpha1",
223
+ kind: "Component",
224
+ metadata: {
225
+ name: slug,
226
+ title: sharedflow.name,
227
+ namespace: "apigee-sharedflows",
228
+ annotations: {
229
+ [catalogModel.ANNOTATION_LOCATION]: APIGEE_LOCATION,
230
+ [catalogModel.ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,
231
+ "apigee.com/proxy-name": sharedflow.name,
232
+ "apigee.com/orgs": orgName
233
+ }
234
+ },
235
+ spec: {
236
+ type: "library",
237
+ lifecycle: "production",
238
+ owner
239
+ }
240
+ };
241
+ }
242
+
243
+ exports.categoryAnnotation = categoryAnnotation;
244
+ exports.categoryTags = categoryTags;
245
+ exports.enrichMatchedApiEntity = enrichMatchedApiEntity;
246
+ exports.hubOnlyToEntity = hubOnlyToEntity;
247
+ exports.ownerRef = ownerRef;
248
+ exports.proxyToEntities = proxyToEntities;
249
+ exports.resolveGithubLink = resolveGithubLink;
250
+ exports.selectSpec = selectSpec;
251
+ exports.sharedflowToEntity = sharedflowToEntity;
252
+ exports.slugify = slugify;
253
+ //# sourceMappingURL=entity-builder.cjs.js.map
@@ -0,0 +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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"}