From c7b8089079eb85e58f37710db3b584e4dbad6939 Mon Sep 17 00:00:00 2001 From: Kristof Siket Date: Tue, 6 Oct 2026 12:09:09 +0200 Subject: [PATCH 1/3] fix(prisma): inherit database region from the project's default region A database with `region: "inherit"` now takes the project's `defaultRegion` from the Management API. It falls back to the default database's region only when the project has no default region. Before, alchemy used only the default database, so a project without one could not deploy inherited databases, and promoting a default database in another region planned a replacement of every inherited database. Co-Authored-By: Claude Opus 5.5 --- packages/alchemy/src/Prisma/Database.ts | 10 ++- .../test/Prisma/ManagementLifecycle.test.ts | 88 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/alchemy/src/Prisma/Database.ts b/packages/alchemy/src/Prisma/Database.ts index e1b67ec313..3c4307a3ad 100644 --- a/packages/alchemy/src/Prisma/Database.ts +++ b/packages/alchemy/src/Prisma/Database.ts @@ -6,6 +6,7 @@ import { deleteDatabase, getDatabases, getDatabase, + getProject, getProjectBranches, getProjectDatabases, updateDatabase, @@ -136,7 +137,8 @@ export interface DatabaseProps { */ name?: string; /** - * Region for the database. + * Region for the database. `"inherit"` uses the project's default region, + * or the default database's region when the project has none. * * @default "us-east-1" */ @@ -437,12 +439,16 @@ const resolveDatabaseRegion = Effect.fn(function* ( if (region !== "inherit") { return (region ?? "us-east-1") as PrismaRegionId; } + const project = yield* getProject({ id: projectId }); + if (project.data.defaultRegion !== null) { + return project.data.defaultRegion as PrismaRegionId; + } const database = yield* findDefaultDatabase(projectId); const inherited = database?.region?.id; if (inherited === undefined) { return yield* Effect.fail( new Error( - `Cannot resolve Prisma database region 'inherit' because project '${projectId}' has no default database region. Create or promote a default database first, or specify an explicit region.`, + `Cannot resolve Prisma database region 'inherit' because project '${projectId}' has no default region and no default database region. Create or promote a default database first, or specify an explicit region.`, ), ); } diff --git a/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts b/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts index f2c9509ad0..b737c16e3b 100644 --- a/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts +++ b/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts @@ -956,11 +956,16 @@ const apiDatabase = ( const makeDatabaseCloud = () => { const databases = new Map(); const calls: Array<[string, unknown?]> = []; + const project = { defaultRegion: null as string | null }; let nextId = 1; // The same in-memory cloud, served over the wire for the Database resource. const fake = makeFakeManagementApi((request) => { const segments = request.pathname.split("/").filter((s) => s.length > 0); + if (segments.length === 3 && segments[1] === "projects" && request.method === "GET") { + return data(toWireProject(apiProject(segments[2]!, "app", project.defaultRegion))); + } + if (request.pathname === "/v1/databases" && request.method === "GET") { return page(Array.from(databases.values()).map(toWireDatabase)); } @@ -1048,7 +1053,7 @@ const makeDatabaseCloud = () => { return unhandled(request); }); - return { fake, calls, databases }; + return { fake, calls, databases, project }; }; const generatedDatabaseRecoveryCloud = makeDatabaseCloud(); @@ -1258,7 +1263,7 @@ const inheritedRegionCloud = makeDatabaseCloud(); const inheritedRegion = Test.make({ providers: databaseLayer(inheritedRegionCloud.fake) }); inheritedRegion.test.provider( - "Database region inherit is stable and follows the project default region", + "Database region inherit falls back to the default database region", (stack) => Effect.gen(function* () { inheritedRegionCloud.databases.clear(); @@ -1307,6 +1312,85 @@ inheritedRegion.test.provider( { tags: ["unit", "provider:prisma", "provider:prisma:database", "local"] }, ); +const projectRegionCloud = makeDatabaseCloud(); +const projectRegion = Test.make({ providers: databaseLayer(projectRegionCloud.fake) }); + +projectRegion.test.provider( + "Database region inherit uses the project default region over the default database", + (stack) => + Effect.gen(function* () { + projectRegionCloud.databases.clear(); + projectRegionCloud.project.defaultRegion = "eu-central-1"; + yield* stack.destroy(); + + const first = yield* stack.deploy( + PrismaDatabase("Database", { project: "project-1", name: "inherited", region: "inherit" }), + ); + expect(first.region).toBe("eu-central-1"); + expect(projectRegionCloud.calls).toContainEqual([ + "createDatabase", + expect.objectContaining({ region: "eu-central-1" }), + ]); + + projectRegionCloud.databases.set( + "project-default", + apiDatabase("project-default", { + projectId: "project-1", + name: "project-default", + region: "us-east-1", + isDefault: true, + }), + ); + projectRegionCloud.calls.length = 0; + const second = yield* stack.deploy( + PrismaDatabase("Database", { project: "project-1", name: "inherited", region: "inherit" }), + ); + expect(second.databaseId).toBe(first.databaseId); + expect(second.region).toBe("eu-central-1"); + const operations = projectRegionCloud.calls.map(([operation]) => operation); + expect(operations).not.toContain("createDatabase"); + expect(operations).not.toContain("deleteDatabase"); + + projectRegionCloud.databases.delete("project-default"); + yield* stack.destroy(); + projectRegionCloud.databases.clear(); + }), + { tags: ["unit", "provider:prisma", "provider:prisma:database", "local"] }, +); + +const noRegionCloud = makeDatabaseCloud(); +const noRegion = Test.make({ providers: databaseLayer(noRegionCloud.fake) }); + +noRegion.test.provider( + "Database region inherit fails without a project or default database region", + (stack) => + Effect.gen(function* () { + noRegionCloud.databases.clear(); + yield* stack.destroy(); + + const result = yield* stack + .deploy( + PrismaDatabase("Database", { + project: "project-1", + name: "inherited", + region: "inherit", + }), + ) + .pipe(Effect.result); + + expect(Result.isFailure(result)).toBe(true); + if (Result.isFailure(result)) { + expect(String(result.failure)).toContain( + "has no default region and no default database region", + ); + } + expect(noRegionCloud.calls.map(([operation]) => operation)).not.toContain("createDatabase"); + + yield* stack.destroy(); + }), + { tags: ["unit", "provider:prisma", "provider:prisma:database", "local"] }, +); + const environmentVariable = { id: "env-1", type: "environment-variable" as const, From 791ebc0f9305d475fe77f85fabbbdc070ef330cc Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 7 Oct 2026 01:50:56 -0700 Subject: [PATCH 2/3] test(prisma): cover region inherit against the live Management API Replaces the in-memory fake cases with live Database.test.ts cases. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/alchemy/test/Prisma/Database.test.ts | 52 +++++++++++ .../test/Prisma/ManagementLifecycle.test.ts | 88 +------------------ 2 files changed, 54 insertions(+), 86 deletions(-) diff --git a/packages/alchemy/test/Prisma/Database.test.ts b/packages/alchemy/test/Prisma/Database.test.ts index 980cff2d38..f5cb3d31da 100644 --- a/packages/alchemy/test/Prisma/Database.test.ts +++ b/packages/alchemy/test/Prisma/Database.test.ts @@ -365,3 +365,55 @@ test.provider( }), { tags: logicalIdTags, timeout: 180_000 }, ); + +test.provider( + "region inherit uses the project default region when the project has no default database", + Effect.fn(function* (stack: Test.ScratchStack) { + yield* stack.destroy(); + + const resources = Effect.gen(function* () { + const project = yield* Prisma.Project("Project", { + createDatabase: false, + region: "eu-central-1", + }); + const database = yield* Prisma.Database("Inherited", { project, region: "inherit" }); + return { project, database }; + }); + + const initial = yield* stack.deploy(resources); + expect((yield* getProject({ id: initial.project.projectId })).data.defaultRegion).toBe( + "eu-central-1", + ); + expect(initial.database.region).toBe("eu-central-1"); + expect((yield* observeDatabase(initial.database.databaseId)).region?.id).toBe("eu-central-1"); + + const repeated = yield* stack.deploy(resources); + expect(repeated.database.databaseId).toBe(initial.database.databaseId); + + yield* stack.destroy(); + yield* expectDatabaseGone(initial.database.databaseId); + yield* expectProjectGone(initial.project.projectId); + }), + { tags: logicalIdTags, timeout: 180_000 }, +); + +test.provider( + "region inherit fails before creating anything when the project has no region", + Effect.fn(function* (stack: Test.ScratchStack) { + yield* stack.destroy(); + + const failure = yield* failureOf( + stack.deploy( + Effect.gen(function* () { + const project = yield* Prisma.Project("Project", { createDatabase: false }); + const database = yield* Prisma.Database("Inherited", { project, region: "inherit" }); + return { project, database }; + }), + ), + ); + expect(failure.text).toContain("has no default region and no default database region"); + + yield* stack.destroy(); + }), + { tags: logicalIdTags, timeout: 180_000 }, +); diff --git a/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts b/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts index b737c16e3b..f2c9509ad0 100644 --- a/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts +++ b/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts @@ -956,16 +956,11 @@ const apiDatabase = ( const makeDatabaseCloud = () => { const databases = new Map(); const calls: Array<[string, unknown?]> = []; - const project = { defaultRegion: null as string | null }; let nextId = 1; // The same in-memory cloud, served over the wire for the Database resource. const fake = makeFakeManagementApi((request) => { const segments = request.pathname.split("/").filter((s) => s.length > 0); - if (segments.length === 3 && segments[1] === "projects" && request.method === "GET") { - return data(toWireProject(apiProject(segments[2]!, "app", project.defaultRegion))); - } - if (request.pathname === "/v1/databases" && request.method === "GET") { return page(Array.from(databases.values()).map(toWireDatabase)); } @@ -1053,7 +1048,7 @@ const makeDatabaseCloud = () => { return unhandled(request); }); - return { fake, calls, databases, project }; + return { fake, calls, databases }; }; const generatedDatabaseRecoveryCloud = makeDatabaseCloud(); @@ -1263,7 +1258,7 @@ const inheritedRegionCloud = makeDatabaseCloud(); const inheritedRegion = Test.make({ providers: databaseLayer(inheritedRegionCloud.fake) }); inheritedRegion.test.provider( - "Database region inherit falls back to the default database region", + "Database region inherit is stable and follows the project default region", (stack) => Effect.gen(function* () { inheritedRegionCloud.databases.clear(); @@ -1312,85 +1307,6 @@ inheritedRegion.test.provider( { tags: ["unit", "provider:prisma", "provider:prisma:database", "local"] }, ); -const projectRegionCloud = makeDatabaseCloud(); -const projectRegion = Test.make({ providers: databaseLayer(projectRegionCloud.fake) }); - -projectRegion.test.provider( - "Database region inherit uses the project default region over the default database", - (stack) => - Effect.gen(function* () { - projectRegionCloud.databases.clear(); - projectRegionCloud.project.defaultRegion = "eu-central-1"; - yield* stack.destroy(); - - const first = yield* stack.deploy( - PrismaDatabase("Database", { project: "project-1", name: "inherited", region: "inherit" }), - ); - expect(first.region).toBe("eu-central-1"); - expect(projectRegionCloud.calls).toContainEqual([ - "createDatabase", - expect.objectContaining({ region: "eu-central-1" }), - ]); - - projectRegionCloud.databases.set( - "project-default", - apiDatabase("project-default", { - projectId: "project-1", - name: "project-default", - region: "us-east-1", - isDefault: true, - }), - ); - projectRegionCloud.calls.length = 0; - const second = yield* stack.deploy( - PrismaDatabase("Database", { project: "project-1", name: "inherited", region: "inherit" }), - ); - expect(second.databaseId).toBe(first.databaseId); - expect(second.region).toBe("eu-central-1"); - const operations = projectRegionCloud.calls.map(([operation]) => operation); - expect(operations).not.toContain("createDatabase"); - expect(operations).not.toContain("deleteDatabase"); - - projectRegionCloud.databases.delete("project-default"); - yield* stack.destroy(); - projectRegionCloud.databases.clear(); - }), - { tags: ["unit", "provider:prisma", "provider:prisma:database", "local"] }, -); - -const noRegionCloud = makeDatabaseCloud(); -const noRegion = Test.make({ providers: databaseLayer(noRegionCloud.fake) }); - -noRegion.test.provider( - "Database region inherit fails without a project or default database region", - (stack) => - Effect.gen(function* () { - noRegionCloud.databases.clear(); - yield* stack.destroy(); - - const result = yield* stack - .deploy( - PrismaDatabase("Database", { - project: "project-1", - name: "inherited", - region: "inherit", - }), - ) - .pipe(Effect.result); - - expect(Result.isFailure(result)).toBe(true); - if (Result.isFailure(result)) { - expect(String(result.failure)).toContain( - "has no default region and no default database region", - ); - } - expect(noRegionCloud.calls.map(([operation]) => operation)).not.toContain("createDatabase"); - - yield* stack.destroy(); - }), - { tags: ["unit", "provider:prisma", "provider:prisma:database", "local"] }, -); - const environmentVariable = { id: "env-1", type: "environment-variable" as const, From a21a0f34027b540c04bf76f63319a616c48627ff Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 7 Oct 2026 01:53:12 -0700 Subject: [PATCH 3/3] test(prisma): keep the fake Management API serving getProject for the fallback case Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/alchemy/test/Prisma/ManagementLifecycle.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts b/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts index f2c9509ad0..6a82a56bb4 100644 --- a/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts +++ b/packages/alchemy/test/Prisma/ManagementLifecycle.test.ts @@ -956,11 +956,16 @@ const apiDatabase = ( const makeDatabaseCloud = () => { const databases = new Map(); const calls: Array<[string, unknown?]> = []; + const project = { defaultRegion: null as string | null }; let nextId = 1; // The same in-memory cloud, served over the wire for the Database resource. const fake = makeFakeManagementApi((request) => { const segments = request.pathname.split("/").filter((s) => s.length > 0); + if (segments.length === 3 && segments[1] === "projects" && request.method === "GET") { + return data(toWireProject(apiProject(segments[2]!, "app", project.defaultRegion))); + } + if (request.pathname === "/v1/databases" && request.method === "GET") { return page(Array.from(databases.values()).map(toWireDatabase)); } @@ -1048,7 +1053,7 @@ const makeDatabaseCloud = () => { return unhandled(request); }); - return { fake, calls, databases }; + return { fake, calls, databases, project }; }; const generatedDatabaseRecoveryCloud = makeDatabaseCloud(); @@ -1258,7 +1263,7 @@ const inheritedRegionCloud = makeDatabaseCloud(); const inheritedRegion = Test.make({ providers: databaseLayer(inheritedRegionCloud.fake) }); inheritedRegion.test.provider( - "Database region inherit is stable and follows the project default region", + "Database region inherit falls back to the default database region", (stack) => Effect.gen(function* () { inheritedRegionCloud.databases.clear();