1import * as apihub from "@distilled.cloud/gcp/apihub_v1";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 ApihubNotResolved,
11 type AttributeValues,
12 type AttributeValuesMap,
13 type Documentation,
14 createOwnership,
15 encodeOwnership,
16 expandParent,
17 hasOwnershipMarker,
18 listApis,
19 listChildResources,
20 listVersions,
21 normalizeLocation,
22 ownedByAlchemy,
23 parseOwnership,
24 parseResourceName,
25 replaceOnIdentity,
26 sameJson,
27 sameStringList,
28 sameText,
29 toPhysicalId,
30 updateMaskOf,
31} from "./internal.ts";
32
33export type ApisVersionProps = {
34 /**
35 * Parent API. Full name
36 * `projects/{project}/locations/{location}/apis/{api}` or the API id
37 * (combined with `location`). Immutable — changing it replaces the
38 * version.
39 */
40 api: string;
41 /**
42 * Version id (the `{version}` segment of
43 * `.../apis/{api}/versions/{version}`). If omitted, a unique id is
44 * generated. Immutable — changing it replaces the version.
45 */
46 versionId?: string;
47 /**
48 * Location used when `api` is a bare id.
49 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
50 */
51 location?: string;
52 /**
53 * Display name. Defaults to the version id.
54 */
55 displayName?: string;
56 /**
57 * Human-readable description. Versions have no labels field, so Alchemy
58 * stamps ownership into a `[alchemy …]` prefix and strips it from
59 * attributes.
60 */
61 description?: string;
62 /**
63 * External documentation.
64 */
65 documentation?: Documentation;
66 /**
67 * Selected deployment resource name.
68 */
69 selectedDeployment?: string;
70 /**
71 * Deployment resource names linked to this version.
72 */
73 deployments?: string[];
74 /**
75 * User-defined attributes keyed by attribute resource name.
76 */
77 attributes?: AttributeValuesMap;
78 /** System attribute `system-lifecycle`. */
79 lifecycle?: AttributeValues;
80 /** System attribute `system-compliance`. */
81 compliance?: AttributeValues;
82 /** System attribute `system-accreditation`. */
83 accreditation?: AttributeValues;
84};
85
86export type ApisVersion = Resource<
87 "GCP.ApiHub.ApisVersion",
88 ApisVersionProps,
89 {
90 /** Full resource name. */
91 name: string;
92 /** Version id (last path segment). */
93 versionId: string;
94 /** Parent API resource name. */
95 api: string;
96 /** Project id. */
97 project: string;
98 /** Location id. */
99 location: string;
100 /** Display name. */
101 displayName: string | undefined;
102 /** User description with the Alchemy ownership prefix stripped. */
103 description: string | undefined;
104 /** Documentation. */
105 documentation: Documentation | undefined;
106 /** Selected deployment resource name. */
107 selectedDeployment: string | undefined;
108 /** Linked deployment resource names. */
109 deployments: string[];
110 /** Spec resource names. */
111 specs: string[];
112 /** Operation resource names. */
113 apiOperations: string[];
114 /** Definition resource names. */
115 definitions: string[];
116 /** User-defined attributes. */
117 attributes: AttributeValuesMap | undefined;
118 /** RFC3339 creation timestamp. */
119 createTime: string | undefined;
120 /** RFC3339 last-update timestamp. */
121 updateTime: string | undefined;
122 },
123 never,
124 Providers
125>;
126
127/**
128 * An API version in API Hub. Specs and operations hang off a version.
129 *
130 * Parent API, location, and id are immutable. Display name, description,
131 * documentation, deployments, and attributes update in place. Versions have
132 * no labels field — Alchemy stamps ownership into the description.
133 *
134 * ### Creating a Version
135 * **Example:** Version under an API
136 * ```typescript
137 * const api = yield* GCP.ApiHub.Api("Pets", { displayName: "pets" });
138 * const version = yield* GCP.ApiHub.ApisVersion("V1", {
139 * api: api.name,
140 * displayName: "v1",
141 * });
142 * ```
143 *
144 * @resource
145 * @category ApiHub
146 */
147export const ApisVersion = Resource<ApisVersion>("GCP.ApiHub.ApisVersion");
148
149const resourceName = (api: string, versionId: string) =>
150 `${api}/versions/${versionId}`;
151
152const parentApi = (value: string, project: string, location: string) =>
153 expandParent(value, project, location, "apis");
154
155const toAttrs = (
156 version: apihub.GoogleCloudApihubV1Version,
157 project: string,
158 region: string,
159): ApisVersion["Attributes"] => {
160 const name = version.name ?? "";
161 const parsed = parseResourceName(name, "versions", region);
162 const description = parseOwnership(version.description).text;
163 return {
164 name,
165 versionId: parsed.id,
166 api: parsed.parent,
167 project: parsed.project || project,
168 location: parsed.location,
169 displayName: version.displayName,
170 description,
171 documentation: version.documentation,
172 selectedDeployment: version.selectedDeployment,
173 deployments: [...(version.deployments ?? [])],
174 specs: [...(version.specs ?? [])],
175 apiOperations: [...(version.apiOperations ?? [])],
176 definitions: [...(version.definitions ?? [])],
177 attributes: version.attributes,
178 createTime: version.createTime,
179 updateTime: version.updateTime,
180 };
181};
182
183const getByName = (name: string) =>
184 name.length === 0
185 ? Effect.succeed(undefined)
186 : apihub
187 .getProjectsLocationsApisVersions({ name })
188 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
189
190const bodyOf = (
191 news: ApisVersionProps,
192 displayName: string,
193 description: string,
194): apihub.GoogleCloudApihubV1Version => ({
195 displayName,
196 description,
197 documentation: news.documentation,
198 selectedDeployment: news.selectedDeployment,
199 deployments: news.deployments,
200 attributes: news.attributes,
201 lifecycle: news.lifecycle,
202 compliance: news.compliance,
203 accreditation: news.accreditation,
204});
205
206export const ApisVersionProvider = () =>
207 Provider.succeed(ApisVersion, {
208 stables: ["name", "versionId", "api", "project", "location", "createTime"],
209
210 diff: Effect.fn(function* ({ news, olds, output }) {
211 if (!isResolved(news)) return undefined;
212 const env = yield* GcpEnvironment.current;
213 const location = normalizeLocation(
214 news.location ?? olds?.location ?? output?.location,
215 env.region,
216 );
217 return replaceOnIdentity({
218 previousId: olds?.versionId ?? output?.versionId,
219 nextId: news.versionId ?? olds?.versionId ?? output?.versionId,
220 previousLocation: normalizeLocation(
221 olds?.location ?? output?.location,
222 env.region,
223 ),
224 nextLocation: location,
225 previousParent: olds?.api ?? output?.api,
226 nextParent: news.api ?? olds?.api ?? output?.api,
227 });
228 }),
229
230 read: Effect.fn(function* ({ id, olds, output }) {
231 const env = yield* GcpEnvironment.current;
232 const location = normalizeLocation(
233 olds?.location ?? output?.location,
234 env.region,
235 );
236 const api = parentApi(
237 olds?.api ?? output?.api ?? "",
238 env.project,
239 location,
240 );
241 const versionId = yield* toPhysicalId(
242 id,
243 olds?.versionId,
244 output?.versionId,
245 );
246 const name = output?.name ?? resourceName(api, versionId);
247 const existing = yield* getByName(name);
248 if (existing === undefined) return undefined;
249 const attrs = toAttrs(existing, env.project, env.region);
250 return (yield* ownedByAlchemy(id, existing.description))
251 ? attrs
252 : Unowned(attrs);
253 }),
254
255 list: () =>
256 Effect.gen(function* () {
257 const env = yield* GcpEnvironment.current;
258 const apis = yield* listApis(
259 `projects/${env.project}/locations/${env.region}`,
260 );
261 const versions = yield* listChildResources(apis, listVersions);
262 return versions
263 .filter((item) => hasOwnershipMarker(item.description))
264 .map((item) => toAttrs(item, env.project, env.region));
265 }),
266
267 reconcile: Effect.fn(function* ({ id, news, output }) {
268 const env = yield* GcpEnvironment.current;
269 const location = normalizeLocation(
270 news.location ?? output?.location,
271 env.region,
272 );
273 const api = parentApi(news.api, env.project, location);
274 const versionId = yield* toPhysicalId(
275 id,
276 news.versionId,
277 output?.versionId,
278 );
279 const name = output?.name ?? resourceName(api, versionId);
280 const ownership = yield* createOwnership(id);
281 const description = encodeOwnership(ownership, news.description);
282 const displayName = news.displayName ?? versionId;
283 const desired = bodyOf(news, displayName, description);
284
285 let current = yield* getByName(name);
286
287 if (current === undefined) {
288 const created = yield* apihub
289 .createProjectsLocationsApisVersions({
290 parent: api,
291 versionId,
292 body: desired,
293 })
294 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
295 current = created ?? undefined;
296 }
297
298 if (current === undefined) {
299 return yield* new ApihubNotResolved({ name });
300 }
301
302 const currentName = current.name ?? name;
303 const displayChanged = !sameText(current.displayName, displayName);
304 const descriptionChanged = !sameText(current.description, description);
305 const docsChanged = !sameJson(current.documentation, news.documentation);
306 const selectedChanged = !sameText(
307 current.selectedDeployment,
308 news.selectedDeployment,
309 );
310 const deploymentsChanged = !sameStringList(
311 current.deployments,
312 news.deployments,
313 );
314 const attributesChanged = !sameJson(current.attributes, news.attributes);
315 const lifecycleChanged = !sameJson(current.lifecycle, news.lifecycle);
316 const complianceChanged = !sameJson(current.compliance, news.compliance);
317 const accreditationChanged = !sameJson(
318 current.accreditation,
319 news.accreditation,
320 );
321
322 if (
323 displayChanged ||
324 descriptionChanged ||
325 docsChanged ||
326 selectedChanged ||
327 deploymentsChanged ||
328 attributesChanged ||
329 lifecycleChanged ||
330 complianceChanged ||
331 accreditationChanged
332 ) {
333 current = yield* apihub.patchProjectsLocationsApisVersions({
334 name: currentName,
335 updateMask: updateMaskOf(
336 displayChanged ? "display_name" : undefined,
337 descriptionChanged ? "description" : undefined,
338 docsChanged ? "documentation" : undefined,
339 selectedChanged ? "selected_deployment" : undefined,
340 deploymentsChanged ? "deployments" : undefined,
341 attributesChanged ? "attributes" : undefined,
342 lifecycleChanged ? "lifecycle" : undefined,
343 complianceChanged ? "compliance" : undefined,
344 accreditationChanged ? "accreditation" : undefined,
345 ),
346 body: { name: currentName, ...desired },
347 });
348 }
349
350 return toAttrs(current, env.project, env.region);
351 }),
352
353 delete: Effect.fn(function* ({ output }) {
354 yield* apihub
355 .deleteProjectsLocationsApisVersions({
356 name: output.name,
357 force: true,
358 })
359 .pipe(Effect.catchTag("NotFound", () => Effect.void));
360 }),
361 });