t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLBranch.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

691 lines
21 KiB
TypeScript

import { adopt } from "@/AdoptPolicy";
import * as Planetscale from "@/Planetscale";
import * as RemovalPolicy from "@/RemovalPolicy.ts";
import * as Test from "@/Test/Alchemy";
import * as ps from "@distilled.cloud/planetscale";
import { describe, expect } from "alchemy-test";
import { Data, Schedule } from "effect";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import { MinimumLogLevel } from "effect/References";
const { test } = Test.make({ providers: Planetscale.providers() });
const logLevel = Effect.provideService(
MinimumLogLevel,
process.env.DEBUG ? "Debug" : "Info",
);
describe
.skipIf(!process.env.PLANETSCALE_TEST)
.concurrent(
"MySQLBranch",
{ tags: ["provider:planetscale", "provider:planetscale:mysql", "live"] },
() => {
test.provider(
"adopts existing branch when adopt is true",
(stack) =>
Effect.gen(function* () {
const dbName = "alchemy-branch-adopt-true";
const branchName = "adopted";
yield* stack.destroy();
const { database } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
});
return { database };
}),
);
yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
"main",
);
yield* deleteBranchIfExists(
database.organization,
dbName,
branchName,
);
yield* ps.createBranch({
organization: database.organization,
database: dbName,
name: branchName,
parent_branch: "main",
});
yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
branchName,
);
const { branch } = yield* stack
.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase(
"Database",
{
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
},
);
const branch = yield* Planetscale.MySQLBranch(
"AdoptedBranch",
{
name: branchName,
database,
parentBranch: "main",
isProduction: false,
},
);
return { database, branch };
}),
)
.pipe(adopt(true));
expect(branch).toMatchObject({
name: branchName,
organization: database.organization,
database: dbName,
parentBranch: "main",
production: false,
createdAt: expect.any(String),
updatedAt: expect.any(String),
htmlUrl: expect.any(String),
region: { slug: expect.any(String) },
});
yield* stack.destroy();
yield* waitForDatabaseToBeDeleted(dbName, database.organization);
}).pipe(logLevel),
5_000_000,
);
test.provider(
"errors on existing branch when adopt is false",
(stack) =>
Effect.gen(function* () {
const dbName = "alchemy-branch-adopt-false";
const branchName = "existing";
yield* stack.destroy();
const { database } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
});
return { database };
}),
);
yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
"main",
);
yield* deleteBranchIfExists(
database.organization,
dbName,
branchName,
);
yield* ps.createBranch({
organization: database.organization,
database: dbName,
name: branchName,
parent_branch: "main",
});
yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
branchName,
);
const exit = yield* stack
.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase(
"Database",
{
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
},
);
const branch = yield* Planetscale.MySQLBranch(
"ExistingBranch",
{
name: branchName,
database,
parentBranch: "main",
isProduction: false,
},
);
return { database, branch };
}),
)
.pipe(Effect.exit);
yield* deleteBranchIfExists(
database.organization,
dbName,
branchName,
);
yield* stack.destroy();
yield* waitForDatabaseToBeDeleted(dbName, database.organization);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const pretty = Cause.pretty(exit.cause);
expect(pretty).toContain("Cannot adopt resource");
expect(pretty).toContain("Planetscale.MySQLBranch");
}
}).pipe(logLevel),
5_000_000,
);
test.provider(
"can create branch with backup",
(stack) =>
Effect.gen(function* () {
const dbName = "alchemy-branch-backup";
const branchName = "restored";
yield* stack.destroy();
const { database } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
});
return { database };
}),
);
yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
"main",
);
yield* deleteBranchIfExists(
database.organization,
dbName,
branchName,
);
const backup = yield* ps.createBackup({
organization: database.organization,
database: dbName,
branch: "main",
name: "alchemy-branch-backup-source",
retention_unit: "hour",
retention_value: 1,
});
yield* waitForBackupSuccess(
database.organization,
dbName,
"main",
backup.id,
);
const { branch } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
});
const branch = yield* Planetscale.MySQLBranch(
"RestoredBranch",
{
name: branchName,
database,
parentBranch: "main",
isProduction: true,
backupId: backup.id,
clusterSize: "PS_10",
},
);
return { database, branch };
}),
);
expect(branch).toMatchObject({
name: branchName,
database: dbName,
parentBranch: "main",
production: true,
});
const live = yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
branchName,
);
expect(live.name).toEqual(branchName);
expect(live.production).toBe(true);
expect(live.cluster_name).toEqual("PS_10");
yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
});
const branch = yield* Planetscale.MySQLBranch(
"RestoredBranch",
{
name: branchName,
database,
parentBranch: "main",
isProduction: false,
},
);
return { database, branch };
}),
);
yield* stack.destroy();
yield* waitForDatabaseToBeDeleted(dbName, database.organization);
}).pipe(logLevel),
5_000_000,
);
test.provider(
"can enable and disable safe migrations",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const { database, branch } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
clusterSize: "PS_10",
});
const branch = yield* Planetscale.MySQLBranch("Branch", {
database,
parentBranch: "main",
isProduction: true,
clusterSize: "PS_10",
safeMigrations: true,
});
return { database, branch };
}),
);
const enabled = yield* ps.getBranch({
organization: database.organization,
database: database.name,
branch: branch.name,
});
expect(enabled.production).toBe(true);
expect(enabled.safe_migrations).toBe(true);
const { updatedBranch } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
clusterSize: "PS_10",
});
const updatedBranch = yield* Planetscale.MySQLBranch("Branch", {
database,
parentBranch: "main",
isProduction: true,
clusterSize: "PS_10",
safeMigrations: false,
});
return { database, updatedBranch };
}),
);
expect(updatedBranch.name).toEqual(branch.name);
const disabled = yield* ps.getBranch({
organization: database.organization,
database: database.name,
branch: branch.name,
});
expect(disabled.safe_migrations).toBe(false);
yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
clusterSize: "PS_10",
});
const branch = yield* Planetscale.MySQLBranch("Branch", {
database,
parentBranch: "main",
isProduction: false,
});
return { database, branch };
}),
);
yield* stack.destroy();
yield* waitForDatabaseToBeDeleted(
database.name,
database.organization,
);
}).pipe(logLevel),
5_000_000,
);
test.provider(
"can update cluster size",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const { database, branch } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
clusterSize: "PS_10",
});
const branch = yield* Planetscale.MySQLBranch("Branch", {
database,
parentBranch: "main",
isProduction: true,
clusterSize: "PS_10",
});
return { database, branch };
}),
);
const initial = yield* Planetscale.waitForBranchReady(
database.organization,
database.name,
branch.name,
);
expect(initial.cluster_name).toEqual("PS_10");
const { resizedBranch } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
clusterSize: "PS_10",
});
const resizedBranch = yield* Planetscale.MySQLBranch("Branch", {
database,
parentBranch: "main",
isProduction: true,
clusterSize: "PS_20",
});
return { database, resizedBranch };
}),
);
expect(resizedBranch.name).toEqual(branch.name);
const resized = yield* Planetscale.waitForBranchReady(
database.organization,
database.name,
branch.name,
);
expect(resized.cluster_name).toEqual("PS_20");
yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
clusterSize: "PS_10",
});
const branch = yield* Planetscale.MySQLBranch("Branch", {
database,
parentBranch: "main",
isProduction: false,
});
return { database, branch };
}),
);
yield* stack.destroy();
yield* waitForDatabaseToBeDeleted(
database.name,
database.organization,
);
}).pipe(logLevel),
5_000_000,
);
test.provider(
"can create branch with Branch object as parent",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const { database, parent, child } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
clusterSize: "PS_10",
});
const parent = yield* Planetscale.MySQLBranch("ParentBranch", {
database,
parentBranch: "main",
isProduction: false,
});
const child = yield* Planetscale.MySQLBranch("ChildBranch", {
database,
parentBranch: parent,
isProduction: false,
});
return { database, parent, child };
}),
);
expect(child).toMatchObject({
database: database.name,
parentBranch: parent.name,
production: false,
});
const live = yield* Planetscale.waitForBranchReady(
database.organization,
database.name,
child.name,
);
expect(live.parent_branch).toEqual(parent.name);
yield* stack.destroy();
yield* waitForDatabaseToBeDeleted(
database.name,
database.organization,
);
}).pipe(logLevel),
5_000_000,
);
test.provider(
"branch with RemovalPolicy.retain(true) should not be deleted via API",
(stack) =>
Effect.gen(function* () {
const dbName = "alchemy-branch-retain";
const branchName = "retained";
yield* stack.destroy();
const { database } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
}).pipe(RemovalPolicy.retain(true));
return { database };
}),
);
yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
"main",
);
yield* deleteBranchIfExists(
database.organization,
dbName,
branchName,
);
const { branch } = yield* stack.deploy(
Effect.gen(function* () {
const database = yield* Planetscale.MySQLDatabase("Database", {
name: dbName,
region: { slug: "us-east" },
clusterSize: "PS_10",
}).pipe(RemovalPolicy.retain(true));
const branch = yield* Planetscale.MySQLBranch(
"RetainedBranch",
{
name: branchName,
database,
parentBranch: "main",
isProduction: false,
},
).pipe(RemovalPolicy.retain(true));
return { database, branch };
}),
);
expect(branch.name).toEqual(branchName);
yield* stack.destroy();
const live = yield* Planetscale.waitForBranchReady(
database.organization,
dbName,
branchName,
);
expect(live.name).toEqual(branchName);
yield* deleteBranchIfExists(
database.organization,
dbName,
branchName,
);
yield* ps
.deleteDatabase({
organization: database.organization,
database: dbName,
})
.pipe(Effect.catchTag("NotFound", () => Effect.void));
yield* waitForDatabaseToBeDeleted(dbName, database.organization);
}).pipe(logLevel),
5_000_000,
);
},
);
const deleteBranchIfExists = Effect.fn(function* (
organization: string,
database: string,
branch: string,
) {
yield* ps
.deleteBranch({ organization, database, branch })
.pipe(Effect.catchTag("NotFound", () => Effect.void));
yield* waitForBranchToBeDeleted(organization, database, branch);
});
const waitForBranchToBeDeleted = Effect.fn(function* (
organization: string,
database: string,
branch: string,
) {
yield* ps.getBranch({ organization, database, branch }).pipe(
Effect.flatMap(() => Effect.fail(new BranchStillExists())),
Effect.retry({
while: (e): e is BranchStillExists => e instanceof BranchStillExists,
schedule: Schedule.exponential(100),
}),
Effect.catchTag("NotFound", () => Effect.void),
);
});
const waitForDatabaseToBeDeleted = Effect.fn(function* (
database: string,
organization: string,
) {
yield* ps
.getDatabase({
organization,
database,
})
.pipe(
Effect.flatMap(() => Effect.fail(new DatabaseStillExists())),
Effect.retry({
while: (e): e is DatabaseStillExists =>
e instanceof DatabaseStillExists,
schedule: Schedule.exponential(100),
}),
Effect.catchTag("NotFound", () => Effect.void),
);
});
const waitForBackupSuccess = Effect.fn(function* (
organization: string,
database: string,
branch: string,
id: string,
) {
return yield* ps.getBackup({ organization, database, branch, id }).pipe(
Effect.flatMap((backup) => {
switch (backup.state) {
case "success":
return Effect.succeed(backup);
case "failed":
case "canceled":
case "ignored":
return Effect.fail(
new BackupNotReady({ retryable: false, state: backup.state }),
);
default:
return Effect.fail(
new BackupNotReady({ retryable: true, state: backup.state }),
);
}
}),
Effect.retry({
while: (e): e is BackupNotReady =>
e instanceof BackupNotReady && e.retryable,
schedule: Schedule.max([
Schedule.exponential(1_000),
Schedule.recurs(120),
]),
}),
);
});
class BranchStillExists extends Data.TaggedError("BranchStillExists") {}
class DatabaseStillExists extends Data.TaggedError("DatabaseStillExists") {}
class BackupNotReady extends Data.TaggedError("BackupNotReady")<{
retryable: boolean;
state: string;
}> {}