1import * as sqladmin from "@distilled.cloud/gcp/sqladmin_v1";
2import type * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import { waitForOperation as waitForGcpOperation } from "../Operation.ts";
5
6const lastSegment = (value: string | undefined) => {
7 const parts = (value ?? "").replace(/\/+$/, "").split("/");
8 return parts[parts.length - 1] ?? "";
9};
10
11const operationNameOf = (operation: sqladmin.Operation) =>
12 lastSegment(operation.name) || lastSegment(operation.selfLink);
13
14/**
15 * Wait for a Cloud SQL admin operation and return the finished operation.
16 *
17 * An operation whose error code (`errors[].code`) says the target already
18 * existed counts as done (a lost create race). With `notFoundOk`, one whose
19 * target was already gone — or whose operation record is gone — counts as
20 * done too (a lost delete race).
21 */
22export const waitForSqlOperation = (
23 project: string,
24 operation: sqladmin.Operation,
25 options: { budget: Duration.Input; notFoundOk?: boolean },
26) =>
27 Effect.gen(function* () {
28 const name = operationNameOf(operation);
29 const get = (operationName: string) =>
30 sqladmin.getOperations({ project, operation: operationName });
31 yield* waitForGcpOperation({ ...operation, name }, get, {
32 budget: options.budget,
33 interval: "10 seconds",
34 }).pipe(
35 Effect.catchTag("GCP.OperationFailed", (error) => {
36 const code = (error.reason ?? "").toUpperCase();
37 return code.includes("ALREADY_EXISTS") ||
38 (options.notFoundOk === true && code.includes("NOT_FOUND"))
39 ? Effect.void
40 : Effect.fail(error);
41 }),
42 Effect.catchTag("NotFound", (error) =>
43 options.notFoundOk === true ? Effect.void : Effect.fail(error),
44 ),
45 );
46 if (operation.status === "DONE" || name.length === 0) return operation;
47 return yield* get(name).pipe(
48 Effect.catchTag("NotFound", (error) =>
49 options.notFoundOk === true
50 ? Effect.succeed(operation)
51 : Effect.fail(error),
52 ),
53 );
54 });
55
56/**
57 * Cloud SQL answers `SqlInstanceNotAuthorized` (403) both for children of an
58 * instance that does not exist and for callers without permission. Recover
59 * with `onMissing` only after `getInstances` confirms the instance is gone
60 * (404); otherwise the original error stands.
61 */
62export const recoverIfInstanceMissing =
63 <B>(project: string, instance: string, onMissing: () => B) =>
64 <A, E extends { readonly _tag: string }, R>(effect: Effect.Effect<A, E, R>) =>
65 effect.pipe(
66 Effect.catchIf(
67 (
68 error,
69 ): error is Extract<E, { readonly _tag: "SqlInstanceNotAuthorized" }> =>
70 error._tag === "SqlInstanceNotAuthorized",
71 (error) =>
72 sqladmin.getInstances({ project, instance }).pipe(
73 Effect.flatMap(() => Effect.fail(error)),
74 Effect.catchTag("NotFound", () => Effect.succeed(onMissing())),
75 ),
76 ),
77 );