1import * as oracle from "@distilled.cloud/gcp/oracledatabase_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 { tagRecord } from "../../Tags.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import { createInternalLabels, hasAlchemyLabels, toLabels } from "../Labels.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 OracleDatabaseNotResolved,
13 collectPages,
14 hasAlchemyLabelMap,
15 listAtLocation,
16 normalizeLocation,
17 parentOf,
18 parseName,
19 replaceOnIdentity,
20 resourceNameOf,
21 retryConflict,
22 retryQuota,
23 toPhysicalId,
24 userLabels,
25 waitUntilExists,
26 waitUntilGone,
27 waitUntilReady,
28} from "./internal.ts";
29import { waitForOperation } from "./operations.ts";
30
31const COLLECTION = "goldengateConnections";
32const FALLBACK_ID = "ggconn";
33
34export type GoldengateConnectionProps = {
35 /**
36 * Connection id. If omitted, a unique RFC1035 name is generated.
37 * Immutable.
38 */
39 goldengateConnectionId?: string;
40 /**
41 * Region. Immutable.
42 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
43 */
44 location?: string;
45 /**
46 * GCP Oracle zone. Immutable.
47 */
48 gcpOracleZone?: string;
49 /**
50 * ODB Network. Immutable.
51 */
52 odbNetwork?: string;
53 /**
54 * ODB Subnet. Immutable.
55 */
56 odbSubnet?: string;
57 /**
58 * User labels. Alchemy ownership labels are merged in automatically.
59 */
60 labels?: Record<string, string>;
61 /**
62 * Connection properties. `connectionType` and `displayName` are
63 * required on create. Type-specific nested objects (Oracle, Kafka,
64 * PostgreSQL, …) are passed through to the API.
65 */
66 properties?: oracle.GoldengateConnectionProperties;
67 /** Connection type. Convenience alias for `properties.connectionType`. */
68 connectionType?:
69 | oracle.GoldengateConnectionPropertiesConnectionTypeEnum
70 | (string & {});
71 /** Display name. Convenience alias for `properties.displayName`. */
72 displayName?: string;
73 /** Description. Convenience alias for `properties.description`. */
74 description?: string;
75 /** Routing method. Convenience alias for `properties.routingMethod`. */
76 routingMethod?:
77 | oracle.GoldengateConnectionPropertiesRoutingMethodEnum
78 | (string & {});
79};
80
81export type GoldengateConnection = Resource<
82 "GCP.OracleDatabase.GoldengateConnection",
83 GoldengateConnectionProps,
84 {
85 /** Full resource name. */
86 name: string;
87 /** Connection id. */
88 goldengateConnectionId: string;
89 /** Project id. */
90 project: string;
91 /** Location id. */
92 location: string;
93 /** GCP Oracle zone. */
94 gcpOracleZone: string | undefined;
95 /** ODB Network. */
96 odbNetwork: string | undefined;
97 /** ODB Subnet. */
98 odbSubnet: string | undefined;
99 /** User labels (Alchemy ownership labels stripped). */
100 labels: Record<string, string>;
101 /** Entitlement id. */
102 entitlementId: string | undefined;
103 /** Connection type. */
104 connectionType: string | undefined;
105 /** Display name. */
106 displayName: string | undefined;
107 /** Lifecycle state. */
108 lifecycleState: string | undefined;
109 /** OCID. */
110 ocid: string | undefined;
111 /** Ingress IPs. */
112 ingressIpAddresses: string[];
113 /** RFC3339 creation timestamp. */
114 createTime: string | undefined;
115 },
116 never,
117 Providers
118>;
119
120/**
121 * An Oracle GoldenGate connection on Google Cloud.
122 *
123 * Changing `goldengateConnectionId`, `location`, `connectionType`, or
124 * ODB identity replaces the connection. There is no patch API in the
125 * distilled SDK, so labels are applied at create.
126 *
127 * ### Creating a GoldenGate connection
128 * **Example:** Generic connection
129 * ```typescript
130 * const conn = yield* GCP.OracleDatabase.GoldengateConnection("Src", {
131 * connectionType: "GENERIC",
132 * displayName: "src",
133 * properties: {
134 * genericConnectionProperties: {
135 * host: "db.example.com",
136 * technologyType: "GENERIC",
137 * },
138 * },
139 * });
140 * ```
141 *
142 * @resource
143 * @category OracleDatabase
144 */
145export const GoldengateConnection = Resource<GoldengateConnection>(
146 "GCP.OracleDatabase.GoldengateConnection",
147);
148
149const mergedProperties = (
150 news: GoldengateConnectionProps,
151): oracle.GoldengateConnectionProperties => ({
152 ...(news.properties ?? {}),
153 connectionType: news.connectionType ?? news.properties?.connectionType,
154 displayName: news.displayName ?? news.properties?.displayName,
155 description: news.description ?? news.properties?.description,
156 routingMethod: news.routingMethod ?? news.properties?.routingMethod,
157});
158
159const toCreateBody = (
160 news: GoldengateConnectionProps,
161 desiredLabels: Record<string, string>,
162): oracle.GoldengateConnection => {
163 const body: oracle.GoldengateConnection = {
164 labels: desiredLabels,
165 properties: mergedProperties(news),
166 };
167 if (news.gcpOracleZone !== undefined) body.gcpOracleZone = news.gcpOracleZone;
168 if (news.odbNetwork !== undefined) body.odbNetwork = news.odbNetwork;
169 if (news.odbSubnet !== undefined) body.odbSubnet = news.odbSubnet;
170 return body;
171};
172
173const toAttrs = (connection: oracle.GoldengateConnection, project: string) => {
174 const name = connection.name ?? "";
175 const parsed = parseName(name, COLLECTION);
176 return {
177 name,
178 goldengateConnectionId: parsed.id,
179 project: parsed.project || project,
180 location: parsed.location,
181 gcpOracleZone: connection.gcpOracleZone,
182 odbNetwork: connection.odbNetwork,
183 odbSubnet: connection.odbSubnet,
184 labels: userLabels(connection.labels),
185 entitlementId: connection.entitlementId,
186 connectionType: connection.properties?.connectionType,
187 displayName: connection.properties?.displayName,
188 lifecycleState: connection.properties?.lifecycleState,
189 ocid: connection.properties?.ocid,
190 ingressIpAddresses: [...(connection.properties?.ingressIpAddresses ?? [])],
191 createTime: connection.createTime,
192 };
193};
194
195const getByName = (name: string) =>
196 retryQuota(oracle.getProjectsLocationsGoldengateConnections({ name })).pipe(
197 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
198 );
199
200const listConnections = (project: string, region: string) => {
201 const collect = (parent: string) =>
202 collectPages(
203 oracle.listProjectsLocationsGoldengateConnections.pages({
204 parent,
205 pageSize: 1000,
206 }),
207 (page) => page.goldengateConnections,
208 ).pipe(
209 Effect.map((items) =>
210 items.filter((item) => hasAlchemyLabelMap(item.labels)),
211 ),
212 );
213 return listAtLocation(project, region, collect).pipe(
214 Effect.catchTag("NotFound", () => Effect.succeed([])),
215 );
216};
217
218export const GoldengateConnectionProvider = () =>
219 Provider.succeed(GoldengateConnection, {
220 stables: [
221 "name",
222 "goldengateConnectionId",
223 "project",
224 "location",
225 "createTime",
226 "ocid",
227 ],
228
229 diff: Effect.fn(function* ({ news, olds, output }) {
230 if (!isResolved(news)) return undefined;
231 const env = yield* GcpEnvironment.current;
232 const previousType =
233 olds?.connectionType ??
234 olds?.properties?.connectionType ??
235 output?.connectionType ??
236 "";
237 const nextType =
238 news.connectionType ?? news.properties?.connectionType ?? previousType;
239 const previousOdb = olds?.odbSubnet ?? output?.odbSubnet ?? "";
240 const nextOdb = news.odbSubnet ?? previousOdb;
241 return replaceOnIdentity({
242 previousId:
243 olds?.goldengateConnectionId ?? output?.goldengateConnectionId,
244 nextId:
245 news.goldengateConnectionId ??
246 olds?.goldengateConnectionId ??
247 output?.goldengateConnectionId,
248 previousLocation: normalizeLocation(
249 olds?.location ?? output?.location ?? env.region,
250 ),
251 nextLocation: normalizeLocation(
252 news.location ?? olds?.location ?? output?.location ?? env.region,
253 ),
254 extra: nextType !== previousType || nextOdb !== previousOdb,
255 });
256 }),
257
258 read: Effect.fn(function* ({ id, olds, output }) {
259 const env = yield* GcpEnvironment.current;
260 const goldengateConnectionId = yield* toPhysicalId(
261 id,
262 olds?.goldengateConnectionId,
263 output?.goldengateConnectionId,
264 FALLBACK_ID,
265 );
266 const location = normalizeLocation(
267 olds?.location ?? output?.location ?? env.region,
268 );
269 const name =
270 output?.name ??
271 resourceNameOf(
272 env.project,
273 location,
274 COLLECTION,
275 goldengateConnectionId,
276 );
277 const existing = yield* getByName(name);
278 if (existing === undefined) return undefined;
279 const attrs = toAttrs(existing, env.project);
280 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
281 ? attrs
282 : Unowned(attrs);
283 }),
284
285 list: () =>
286 Effect.gen(function* () {
287 const env = yield* GcpEnvironment.current;
288 const items = yield* listConnections(env.project, env.region);
289 return items.map((item) => toAttrs(item, env.project));
290 }),
291
292 reconcile: Effect.fn(function* ({ id, news, output }) {
293 const env = yield* GcpEnvironment.current;
294 const goldengateConnectionId = yield* toPhysicalId(
295 id,
296 news.goldengateConnectionId,
297 output?.goldengateConnectionId,
298 FALLBACK_ID,
299 );
300 const location = normalizeLocation(
301 news.location ?? output?.location ?? env.region,
302 );
303 const name = resourceNameOf(
304 env.project,
305 location,
306 COLLECTION,
307 goldengateConnectionId,
308 );
309 const desiredLabels = {
310 ...toLabels(news.labels),
311 ...(yield* createInternalLabels(id)),
312 };
313
314 let current = yield* getByName(output?.name ?? name);
315
316 if (current === undefined) {
317 const created = yield* oracle
318 .createProjectsLocationsGoldengateConnections({
319 parent: parentOf(env.project, location),
320 goldengateConnectionId,
321 body: toCreateBody(news, desiredLabels),
322 })
323 .pipe(
324 retryQuota,
325 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
326 );
327 if (created !== undefined) {
328 yield* waitForOperation(created);
329 }
330 current = yield* waitUntilExists(getByName(name), name);
331 }
332
333 if (current === undefined) {
334 return yield* new OracleDatabaseNotResolved({ name });
335 }
336
337 const ready = yield* waitUntilReady(
338 getByName(current.name ?? name),
339 current.name ?? name,
340 (value) => value.properties?.lifecycleState,
341 );
342
343 return toAttrs(ready, env.project);
344 }),
345
346 delete: Effect.fn(function* ({ output }) {
347 const operation = yield* oracle
348 .deleteProjectsLocationsGoldengateConnections({
349 name: output.name,
350 })
351 .pipe(
352 retryConflict,
353 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
354 );
355 if (operation !== undefined) {
356 yield* waitForOperation(operation, { notFoundOk: true });
357 }
358 yield* waitUntilGone(getByName(output.name), output.name);
359 }),
360 });