diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49c1e005..1a6f3dbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,13 @@ jobs: - name: Test env: STATE_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres - run: pnpm test + run: pnpm turbo run test --filter='!@internal/local-target' + # local-target's drift suite talks to the machine-global postgres emulator, the + # same daemon the dev-emulators suite restarts, so it runs alone after the rest. + - name: Test local-target + env: + STATE_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres + run: pnpm turbo run test --filter=@internal/local-target - name: Type-only tests (vitest --typecheck) run: pnpm turbo run test:types - name: Test scripts (cast-ratchet unit tests) @@ -134,7 +140,13 @@ jobs: if: runner.os != 'Windows' env: STATE_TEST_DATABASE_URL: ${{ steps.postgres.outputs.connection-uri }} - run: pnpm test + run: pnpm turbo run test --filter='!@internal/local-target' + # Same daemon-sharing reason as the Linux job: local-target runs alone after the rest. + - name: Test local-target + if: runner.os != 'Windows' + env: + STATE_TEST_DATABASE_URL: ${{ steps.postgres.outputs.connection-uri }} + run: pnpm turbo run test --filter=@internal/local-target # Local dev/log are not supported on Windows. - name: Test changed packages on Windows if: runner.os == 'Windows' diff --git a/.github/workflows/e2e-deploy.yml b/.github/workflows/e2e-deploy.yml index 0a35b869..a684ae9c 100644 --- a/.github/workflows/e2e-deploy.yml +++ b/.github/workflows/e2e-deploy.yml @@ -13,6 +13,10 @@ on: permissions: contents: read +env: + # Every job here creates a fresh per-run Project, and a new Project needs a region. + PRISMA_REGION: us-east-1 + # Fixed group (not per-ref): only one real-cloud deploy runs at a time. # cancel-in-progress stays false so a kill mid-deploy/destroy can't orphan resources. concurrency: diff --git a/packages/1-prisma-cloud/0-lowering/lowering/src/__tests__/container.test.ts b/packages/1-prisma-cloud/0-lowering/lowering/src/__tests__/container.test.ts index 551cc721..24352d04 100644 --- a/packages/1-prisma-cloud/0-lowering/lowering/src/__tests__/container.test.ts +++ b/packages/1-prisma-cloud/0-lowering/lowering/src/__tests__/container.test.ts @@ -6,6 +6,7 @@ import { ContainerNotFoundError, deleteBranch, deleteProject, + type ProjectRegion, resolveContainer, } from '../container.ts'; import { PrismaApiError } from '../http.ts'; @@ -213,7 +214,13 @@ const fakeClient = (state: FakeState): ManagementApiClient => { const run = ( state: FakeState, - opts: { workspaceId: string; appName: string; stage?: string; ensure?: boolean }, + opts: { + workspaceId: string; + appName: string; + stage?: string; + ensure?: boolean; + region?: ProjectRegion; + }, ) => Effect.runPromise( resolveContainer(opts).pipe(Effect.provideService(ManagementClient, fakeClient(state))), @@ -227,7 +234,11 @@ describe('resolveContainer — Project resolution', () => { }); test('no matching project creates one, resolving its default Branch id', async () => { - const result = await run(state, { workspaceId: 'ws-1', appName: 'storefront' }); + const result = await run(state, { + workspaceId: 'ws-1', + appName: 'storefront', + region: 'us-east-1', + }); expect(result.projectId).toBe('proj-1'); expect(result.defaultBranchId).toBe('br-default-proj-1'); @@ -236,11 +247,46 @@ describe('resolveContainer — Project resolution', () => { }); test('project creation opts out of the platform default database', async () => { - await run(state, { workspaceId: 'ws-1', appName: 'storefront' }); + await run(state, { workspaceId: 'ws-1', appName: 'storefront', region: 'us-east-1' }); expect(state.projectCreateBodies[0]?.['createDatabase']).toBe(false); }); + test('project creation sends the configured region to the platform', async () => { + await run(state, { workspaceId: 'ws-1', appName: 'storefront', region: 'ap-southeast-1' }); + + expect(state.projectCreateBodies[0]?.['region']).toBe('ap-southeast-1'); + }); + + test('no region when a new project is needed fails with an actionable error', async () => { + const error: unknown = await run(state, { workspaceId: 'ws-1', appName: 'storefront' }).catch( + (e: unknown) => e, + ); + + expect(error).toBeInstanceOf(PrismaApiError); + expect((error as PrismaApiError).status).toBe(0); + expect((error as PrismaApiError).message).toContain('"storefront"'); + expect((error as PrismaApiError).message).toContain('prismaCloud({ region:'); + expect((error as PrismaApiError).message).toContain('PRISMA_REGION'); + }); + + test('an existing project resolves without a region — region is not required for find', async () => { + state.projects.push({ + id: 'proj-existing', + name: 'storefront', + createdAt: new Date(1).toISOString(), + workspace: { id: 'ws-1' }, + }); + state.branches['proj-existing'] = [ + { id: 'br-default', gitName: 'main', isDefault: true, createdAt: new Date(1).toISOString() }, + ]; + + const result = await run(state, { workspaceId: 'ws-1', appName: 'storefront' }); + + expect(result.projectId).toBe('proj-existing'); + expect(state.projectCreateCalls).toBe(0); + }); + test('adopt-oldest: several projects share the name — the oldest is adopted, none created', async () => { state.projects.push( { @@ -280,7 +326,11 @@ describe('resolveContainer — Project resolution', () => { workspace: { id: 'ws-2' }, }); - const result = await run(state, { workspaceId: 'ws-1', appName: 'storefront' }); + const result = await run(state, { + workspaceId: 'ws-1', + appName: 'storefront', + region: 'us-east-1', + }); expect(result.projectId).toBe('proj-1'); expect(state.projectCreateCalls).toBe(1); @@ -294,7 +344,11 @@ describe('resolveContainer — Project resolution', () => { workspace: { id: 'ws-1' }, }); - const result = await run(state, { workspaceId: 'ws-1', appName: 'storefront' }); + const result = await run(state, { + workspaceId: 'ws-1', + appName: 'storefront', + region: 'us-east-1', + }); expect(result.projectId).toBe('proj-1'); expect(state.projectCreateCalls).toBe(1); @@ -439,7 +493,7 @@ describe('resolveContainer — Project resolution', () => { }); test('project creation sends the module name as the logical id', async () => { - await run(state, { workspaceId: 'ws-1', appName: 'storefront' }); + await run(state, { workspaceId: 'ws-1', appName: 'storefront', region: 'us-east-1' }); expect(state.projectCreateBodies[0]?.['logicalId']).toBe('storefront'); }); @@ -447,9 +501,11 @@ describe('resolveContainer — Project resolution', () => { test('a 409 on project create surfaces a clear name-conflict error', async () => { state.projectCreateConflict = true; - const error: unknown = await run(state, { workspaceId: 'ws-1', appName: 'storefront' }).catch( - (e: unknown) => e, - ); + const error: unknown = await run(state, { + workspaceId: 'ws-1', + appName: 'storefront', + region: 'us-east-1', + }).catch((e: unknown) => e); expect(error).toBeInstanceOf(PrismaApiError); expect((error as PrismaApiError).status).toBe(409); diff --git a/packages/1-prisma-cloud/0-lowering/lowering/src/container.ts b/packages/1-prisma-cloud/0-lowering/lowering/src/container.ts index 755a8761..ee9ea760 100644 --- a/packages/1-prisma-cloud/0-lowering/lowering/src/container.ts +++ b/packages/1-prisma-cloud/0-lowering/lowering/src/container.ts @@ -1,9 +1,15 @@ +import type { paths } from '@prisma/management-api-sdk'; import * as Data from 'effect/Data'; import * as Effect from 'effect/Effect'; import { type ManagementApiClient, ManagementClient } from './client.ts'; import { call, callVoid, PrismaApiError } from './http.ts'; import { collectPages, drivePages } from './pagination.ts'; +/** The region ids the Management API accepts for a new Project — the platform's published contract, not a Composer-owned list. */ +export type ProjectRegion = NonNullable< + NonNullable['content']['application/json']['region'] +>; + export interface ResolveContainerOptions { /** The workspace to resolve the Project in. */ readonly workspaceId: string; @@ -13,6 +19,12 @@ export interface ResolveContainerOptions { readonly stage?: string; /** Create the Project/Branch if absent (default `true`). `false` finds only — used by `destroy`. */ readonly ensure?: boolean; + /** + * The region to stamp on the Project at creation time. Required when creating a new Project + * (`ensure: true` and no matching Project found). Omit for find-only (`ensure: false`) or when + * the Project already exists — the platform's stored default region is used then. + */ + readonly region?: ProjectRegion; } /** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */ @@ -72,6 +84,7 @@ const resolveProject = ( workspaceId: string, appName: string, ensure: boolean, + region: ProjectRegion | undefined, ): Effect.Effect => Effect.gen(function* () { const projects = yield* listAllProjects(client); @@ -91,13 +104,25 @@ const resolveProject = ( if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName })); + if (region === undefined) { + return yield* Effect.fail( + new PrismaApiError({ + status: 0, + message: + `project "${appName}" does not exist yet and no deploy region is configured. ` + + "Set the region via prismaCloud({ region: '' }) in your config, or set the " + + 'PRISMA_REGION environment variable', + }), + ); + } + // createDatabase: false — the platform default database is never used // (composer claims DATABASE_URL with a placeholder at provision), so don't create it. The // API 403s this for user actors, but deploys authenticate as workspace // actors (service tokens), which are allowed. const created = yield* call(() => client.POST('/v1/projects', { - body: { name: appName, workspaceId, createDatabase: false, logicalId: appName }, + body: { name: appName, workspaceId, createDatabase: false, logicalId: appName, region }, }), ).pipe( Effect.catch((err) => @@ -218,7 +243,13 @@ export const resolveContainer = ( Effect.gen(function* () { const client = yield* ManagementClient; const ensure = opts.ensure ?? true; - const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure); + const projectId = yield* resolveProject( + client, + opts.workspaceId, + opts.appName, + ensure, + opts.region, + ); if (opts.stage === undefined) { const defaultBranchId = yield* resolveDefaultBranchId(client, projectId); return { projectId, defaultBranchId }; diff --git a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/container.test.ts b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/container.test.ts index fe62e491..d7532988 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/container.test.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/container.test.ts @@ -25,6 +25,7 @@ interface FakeState { projects: FakeProject[]; branches: Record; projectCreateCalls: number; + projectCreateBodies: Array>; branchCreateCalls: number; deleteBranchCalls: string[]; /** Overrides the DELETE response status — defaults to a 204 success. */ @@ -38,6 +39,7 @@ const newFakeState = (overrides: Partial = {}): FakeState => ({ projects: [], branches: {}, projectCreateCalls: 0, + projectCreateBodies: [], branchCreateCalls: 0, deleteBranchCalls: [], deleteProjectCalls: [], @@ -85,6 +87,7 @@ const fakeClient = (state: FakeState): ManagementApiClient => { ) => { if (path === '/v1/projects') { state.projectCreateCalls++; + state.projectCreateBodies.push(init.body ?? {}); const id = `proj-${state.projectCreateCalls}`; const project: FakeProject = { id, @@ -188,7 +191,10 @@ describe('containerDescriptor().ensure()', () => { const state = newFakeState(); await withEnv(baseEnv, async () => { - const descriptor = containerDescriptor({ client: fakeClient(state) }); + const descriptor = containerDescriptor({ + client: fakeClient(state), + region: () => 'us-east-1', + }); const instance = await descriptor.ensure({ appName: 'storefront', stage: 'staging' }); expect(isPrismaCloudContainer(instance)).toBe(true); @@ -196,6 +202,7 @@ describe('containerDescriptor().ensure()', () => { expect(instance.branchId).toBe('br-proj-1-1'); expect(instance.alchemyStage).toBe('br-proj-1-1'); expect(state.projectCreateCalls).toBe(1); + expect(state.projectCreateBodies[0]?.['region']).toBe('us-east-1'); expect(state.branchCreateCalls).toBe(1); }); }); @@ -204,13 +211,17 @@ describe('containerDescriptor().ensure()', () => { const state = newFakeState(); await withEnv(baseEnv, async () => { - const descriptor = containerDescriptor({ client: fakeClient(state) }); + const descriptor = containerDescriptor({ + client: fakeClient(state), + region: () => 'us-east-1', + }); const instance = await descriptor.ensure({ appName: 'storefront', stage: undefined }); expect(instance.projectId).toBe('proj-1'); expect(instance.branchId).toBeUndefined(); expect(instance.defaultBranchId).toBe('br-default-proj-1'); expect(instance.alchemyStage).toBe('br-default-proj-1'); + expect(state.projectCreateBodies[0]?.['region']).toBe('us-east-1'); expect(state.branchCreateCalls).toBe(0); }); }); diff --git a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-env.test.ts b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-env.test.ts index 133c2598..bf5761fc 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-env.test.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-env.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from 'bun:test'; -import type { LowerContext } from '@internal/core/deploy'; -import * as Effect from 'effect/Effect'; +import type { ManagementApiClient } from '@internal/lowering'; import { prismaCloud } from '../exports/control.ts'; /** Sets env vars for the duration of `fn`, restoring whatever was there before. */ @@ -26,24 +25,6 @@ const SCRUBBED = { PRISMA_SERVICE_TOKEN: undefined, }; -/** Minimal `LowerContext` for driving one node descriptor's `provision` in isolation. */ -function computeCtx(): LowerContext { - return { - id: 'auth', - application: { - projectId: 'shop-project#cloud-id', - branchId: undefined, - defaultBranchId: undefined, - branchless: false, - }, - } as unknown as LowerContext; -} - -/** The throw under test happens before any yield, so no Alchemy context is ever needed — collapse E/R for `runSync` like `control-lowering.test.ts`'s `run` helper does. */ -function runSync(eff: Effect.Effect): A { - return Effect.runSync(eff as Effect.Effect); -} - describe('prismaCloud() — constructs with NO environment present (local-dev spec § 5)', () => { test('succeeds in a fully scrubbed environment — no PRISMA_* var is required at construction', async () => { await withEnv(SCRUBBED, () => { @@ -84,17 +65,47 @@ describe('prismaCloud() — constructs with NO environment present (local-dev sp }); }); +/** A stub client covering only what `ensure` calls for a project that does not exist yet; records every project-create body. */ +const fakeClient = (projectCreateBodies: Array>): ManagementApiClient => { + const page = (data: T[]) => + Promise.resolve({ + data: { data, pagination: { nextCursor: null, hasMore: false } }, + error: undefined, + response: new Response(null, { status: 200 }), + }); + const GET = (path: string) => { + if (path === '/v1/projects') return page([]); + if (path === '/v1/projects/{projectId}/branches') { + return page([{ id: 'br-default', gitName: 'main', isDefault: true }]); + } + throw new Error(`fakeClient: unexpected GET ${path}`); + }; + const POST = (path: string, init: { body?: Record } = {}) => { + if (path !== '/v1/projects') throw new Error(`fakeClient: unexpected POST ${path}`); + projectCreateBodies.push(init.body ?? {}); + return Promise.resolve({ + data: { data: { id: 'proj-1' } }, + error: undefined, + response: new Response(null, { status: 201 }), + }); + }; + // biome-ignore lint/suspicious/noExplicitAny: test stub — see the doc comment above. + return { GET, POST } as any as ManagementApiClient; +}; + describe('prismaCloud() — region resolution is deferred to first lowering use, not construction', () => { - test('a bad PRISMA_REGION does not fail construction — only an actual lowering', async () => { - await withEnv({ PRISMA_WORKSPACE_ID: 'ws-123', PRISMA_REGION: 'mars-1' }, () => { + test('an arbitrary PRISMA_REGION string passes through unchanged — no list to validate against', async () => { + await withEnv({ PRISMA_WORKSPACE_ID: 'ws-123', PRISMA_REGION: 'xx-test-1' }, async () => { expect(() => prismaCloud()).not.toThrow(); - const descriptor = prismaCloud(); - const compute = descriptor.nodes['compute']; - if (compute === undefined || compute.kind !== 'service') { - throw new Error('expected a service descriptor for "compute"'); - } - expect(() => runSync(compute.provision(computeCtx()))).toThrow(/PRISMA_REGION="mars-1"/); + const projectCreateBodies: Array> = []; + const container = prismaCloud().container; + expect(container).toBeDefined(); + await container?.ensure( + { appName: 'storefront', stage: undefined }, + { workspaceId: 'ws-123', client: fakeClient(projectCreateBodies) }, + ); + expect(projectCreateBodies[0]?.['region']).toBe('xx-test-1'); }); }); }); diff --git a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts index 624d125f..be7482bd 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts @@ -511,7 +511,7 @@ describe("prismaCloud().nodes['raw-postgres'] — the resource descriptor", () = expect(recorded.db).toEqual([ [ 'data-db', - { project: 'shop-project#cloud-id', region: 'us-east-1', branchId: 'br_default' }, + { project: 'shop-project#cloud-id', region: 'inherit', branchId: 'br_default' }, ], ]); expect(recorded.conn).toEqual([ @@ -549,7 +549,7 @@ describe("prismaCloud().nodes['raw-postgres'] — the resource descriptor", () = 'data2-db', { project: 'shop-project#cloud-id', - region: 'us-east-1', + region: 'inherit', branchId: 'branch_1', }, ], @@ -595,7 +595,7 @@ describe("prismaCloud().nodes['raw-postgres'] — the resource descriptor", () = run(resourceDescriptorOf(target, 'raw-postgres')(ctx)); expect(recorded.db.slice(before)).toEqual([ - ['data4-db', { project: 'local', name: 'data4', region: 'us-east-1' }], + ['data4-db', { project: 'local', name: 'data4', region: 'inherit' }], ]); }); }); @@ -636,7 +636,7 @@ describe("prismaCloud().nodes['postgres'] — the resource descriptor", () => { expect(recorded.db.slice(before.db)).toEqual([ [ 'pndata-db', - { project: 'shop-project#cloud-id', region: 'us-east-1', branchId: 'br_default' }, + { project: 'shop-project#cloud-id', region: 'inherit', branchId: 'br_default' }, ], ]); const [migrateId, migrateProps] = recorded.pnMigrate[before.migrate] ?? ['', {}]; @@ -749,10 +749,7 @@ describe("prismaCloud().nodes['compute'] — the service descriptor", () => { endpointDomain: 'https://auth-svc.example', }); expect(recorded.svc).toEqual([ - [ - 'auth-svc', - { project: 'shop-project#cloud-id', displayName: 'auth', regionId: 'us-east-1' }, - ], + ['auth-svc', { project: 'shop-project#cloud-id', displayName: 'auth' }], ]); }); }); @@ -779,7 +776,6 @@ describe("prismaCloud().nodes['compute'] — the service descriptor", () => { { project: 'shop-project#cloud-id', displayName: 'auth2', - regionId: 'us-east-1', branchId: 'branch_1', }, ], @@ -1826,7 +1822,7 @@ describe('sharing: one module-provisioned postgres, two compute consumers — th expect(recorded.db.slice(before.db)).toEqual([ [ 'data-db', - { project: 'shop-project#cloud-id', region: 'us-east-1', branchId: 'br_default' }, + { project: 'shop-project#cloud-id', region: 'inherit', branchId: 'br_default' }, ], ]); expect(recorded.conn.slice(before.conn)).toEqual([ diff --git a/packages/1-prisma-cloud/1-extensions/target/src/container.ts b/packages/1-prisma-cloud/1-extensions/target/src/container.ts index ab7e2a99..09231087 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/container.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/container.ts @@ -20,6 +20,7 @@ import { type ManagementApiClient, ManagementClient, managementClientLayer, + type ProjectRegion, resolveContainer, } from '@internal/lowering'; import * as Effect from 'effect/Effect'; @@ -149,6 +150,13 @@ type PrismaCloudCredentials = ContainerCredentials; /** Construction-time injection. Per-call credentials outrank it wherever both are present. */ interface ContainerDeps { readonly client?: ManagementApiClient; + /** + * Getter for the configured deploy region — evaluated at ensure time, not construction time, + * so `prismaCloud()` still constructs with no environment present (local-dev spec § 5). + * When the getter returns undefined and the Project does not exist yet, ensure fails with an + * actionable error asking the user to set the region. + */ + readonly region?: () => ProjectRegion | undefined; } const workspaceRequiredError = (): Error => @@ -205,10 +213,12 @@ async function ensureContainer( // All typed failures are caught and carried as a failure *value*, so // runPromise only rejects on a genuine defect. + const region = deps?.region?.(); const program = resolveContainer({ workspaceId, appName: input.appName, ...(input.stage !== undefined ? { stage: input.stage } : {}), + ...(region !== undefined ? { region } : {}), ensure: true, }).pipe( Effect.map((c) => ({ ok: true as const, container: c })), diff --git a/packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts b/packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts index d4b5cea6..71a46d3a 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts @@ -11,7 +11,6 @@ import * as Prisma from '@internal/lowering'; import { prismaStateLayer } from '@internal/lowering/state'; import { RPC_PEER_KEY } from '@internal/service-rpc'; import * as Output from 'alchemy/Output'; -import * as AlchemyPrisma from 'alchemy/Prisma'; import * as Effect from 'effect/Effect'; import * as Layer from 'effect/Layer'; import { @@ -181,16 +180,7 @@ export interface PrismaCloudOptions { /** Defaults to the PRISMA_WORKSPACE_ID environment variable. */ workspaceId?: string; /** Defaults to the PRISMA_REGION environment variable when set. */ - region?: AlchemyPrisma.Types.PrismaRegionId; -} - -// Upstream's KNOWN_REGION_IDS is the runtime source of truth PrismaRegionId is -// derived from, so this can never fall behind — no hand-maintained list, no -// exhaustiveness gymnastics to keep it honest. -const KNOWN_REGION_SET: ReadonlySet = new Set(AlchemyPrisma.KNOWN_REGION_IDS); - -function isComputeRegion(value: string): value is AlchemyPrisma.Types.PrismaRegionId { - return KNOWN_REGION_SET.has(value); + region?: Prisma.ProjectRegion; } /** Prisma.providers()'s ProviderCollection doesn't structurally unify with Alchemy's inferred providers Layer (a @internal/lowering typings gap); it satisfies it at runtime. */ @@ -282,11 +272,10 @@ export const PROVIDER_PARAMS: ReadonlyMap { const workspaceId = opts.workspaceId ?? process.env['PRISMA_WORKSPACE_ID'] ?? ''; @@ -299,13 +288,14 @@ function resolveOptions(opts: PrismaCloudOptions): Omit(region), + providerParams: PROVIDER_PARAMS, + }; } /** @@ -340,7 +330,7 @@ export const prismaCloud = (opts: PrismaCloudOptions = {}): ExtensionDescriptor return { id: PRISMA_CLOUD_EXTENSION_ID, - container: containerDescriptor(), + container: containerDescriptor({ region: () => o().region }), providers: () => asProvidersLayer( diff --git a/packages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.ts b/packages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.ts index 488d33d8..51194517 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.ts @@ -26,7 +26,6 @@ import { } from '../serializer.ts'; import { cloudApplicationOf, - DEFAULT_REGION, projectIdOf, type ResolvedCloudOptions, validateName, @@ -119,7 +118,7 @@ export function computeDescriptor( const svc = yield* Prisma.App(`${id}-svc`, { project: projectId, displayName: id, - regionId: o().region ?? DEFAULT_REGION, + ...(o().region !== undefined ? { regionId: o().region } : {}), ...(branchId !== undefined ? { branchId } : {}), }); return { serviceId: svc.appId, projectId, endpointDomain: svc.appEndpointDomain }; diff --git a/packages/1-prisma-cloud/1-extensions/target/src/descriptors/shared.ts b/packages/1-prisma-cloud/1-extensions/target/src/descriptors/shared.ts index 442f063b..e9385645 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/descriptors/shared.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/descriptors/shared.ts @@ -1,5 +1,6 @@ /** Helpers shared by the per-node-kind descriptors under `src/descriptors/` and the extension factory in `control.ts`. */ +import type { ProjectRegion } from '@internal/lowering'; import * as Output from 'alchemy/Output'; import * as Prisma from 'alchemy/Prisma'; import * as Effect from 'effect/Effect'; @@ -67,7 +68,7 @@ export interface ServiceProviderParam extends ProviderParamEntry { */ export interface ResolvedCloudOptions { readonly workspaceId: string; - readonly region?: Prisma.Types.PrismaRegionId; + readonly region?: ProjectRegion; /** * This extension's reserved provider params, keyed by need brand — * edge-derived (`ProviderParam`) or service-derived (`ServiceProviderParam`). @@ -89,9 +90,6 @@ export interface ResolvedCloudOptions { readonly pointerUpdatedAt: PointerUpdatedAt; } -/** Where a resource lands when the deploy names no region. */ -export const DEFAULT_REGION: Prisma.Types.PrismaRegionId = 'us-east-1'; - // Prisma's Connection create constrains `name` to 3–65 chars (Management API: // POST /v1/connections); applied here to every id-derived resource name as the // tightest of the API's name-length rules. @@ -184,13 +182,14 @@ export const stageDatabase = ({ }: { readonly id: string; readonly application: unknown; - readonly region: Prisma.Types.PrismaRegionId | undefined; + readonly region: ProjectRegion | undefined; }) => Effect.gen(function* () { const branchId = attachmentBranchIdOf(application, id); + // 'inherit' resolves to the project's default region on the platform. const db = yield* Prisma.Database(`${id}-db`, { project: projectIdOf(application), - region: region ?? DEFAULT_REGION, + region: region ?? 'inherit', ...(branchId !== undefined ? { branchId } : { name: id }), }); const conn = yield* Prisma.Connection(`${id}-conn`, { database: db, name: id });