@prisma/composer-prisma-cloud 0.5.0 → 0.6.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/control.mjs CHANGED
@@ -2,7 +2,7 @@ import { o as paramBindingFor, s as paramName } from "./secret-Doubjztg-CWHCDiuX
2
2
  import { a as encodeParamPointer, i as encode, l as serializeInput, n as configKey, o as paramEntries } from "./serializer-Ch1Tmx-2-D7cNhkEp.mjs";
3
3
  import { a as provisionedEdges, n as SELF_ORIGIN, r as STREAMS_API_KEY, t as RESERVED_PROVIDER_PARAMS } from "./provisioned-edges-COr17uxy-DymAGn_V.mjs";
4
4
  import { n as requiredPackHeadOf } from "./required-pack-head-BrrJarzc-DeP50pBu.mjs";
5
- import { A as packageComputeArtifact, D as EnvironmentVariable, E as Deployment, F as call, I as callVoid, L as fromEnv, M as BucketKey, N as ManagementClient, O as ServiceKey, P as PrismaApiError, R as layer, S as Database, T as ComputeService, _ as resolveTargetRef, a as PgWarmProvider, b as providers, d as collectPreflightNames, f as containerDescriptor, g as resolvePrismaNextConfig, h as prismaCloudContainerOf, i as PgWarm, j as Bucket, k as ServiceKeyProvider, l as S3Credentials, m as packHeadRefHashes, n as GeneratedParamProvider, o as PnMigration, r as PRISMA_CLOUD_EXTENSION_ID, s as PnMigrationProvider, t as GeneratedParam, u as S3CredentialsProvider, w as COMPUTE_REGIONS, x as Connection } from "./s3-credentials-resource-tfirOJBj-79G9R_h8.mjs";
5
+ import { A as packageComputeArtifact, B as layer, D as EnvironmentVariable, E as Deployment, F as ManagementClient, I as PrismaApiError, L as call, M as BucketKey, N as collectPages, O as ServiceKey, P as resolveDefaultBranchId, R as callVoid, S as Database, T as ComputeService, _ as resolveTargetRef, a as PgWarmProvider, b as providers, d as collectPreflightNames, f as containerDescriptor, g as resolvePrismaNextConfig, h as prismaCloudContainerOf, i as PgWarm, j as Bucket, k as ServiceKeyProvider, l as S3Credentials, m as packHeadRefHashes, n as GeneratedParamProvider, o as PnMigration, r as PRISMA_CLOUD_EXTENSION_ID, s as PnMigrationProvider, t as GeneratedParam, u as S3CredentialsProvider, w as COMPUTE_REGIONS, x as Connection, z as fromEnv } from "./s3-credentials-resource-BMsm9nho-C0Bg1909.mjs";
6
6
  import { n as isPnPostgresResourceNode } from "./prisma-next-DmPGOIm2-3V7kdn8T.mjs";
7
7
  import { isParamSource } from "@prisma/composer";
8
8
  import { blindCast } from "@prisma/composer/casts";
@@ -21,62 +21,24 @@ import * as Output from "alchemy/Output";
21
21
  const STATE_DATABASE_NAME = "prisma-composer-state";
22
22
  /** Every connection created against a state database carries this prefix — see `cleanupAgedConnections`. */
23
23
  const CONNECTION_NAME_PREFIX = "prisma-composer-state-";
24
- const listAllBranches = (client, projectId) => Effect.gen(function* () {
25
- const branches = [];
26
- let cursor;
27
- for (;;) {
28
- const query = cursor === void 0 ? {} : { cursor };
29
- const page = yield* call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
30
- path: { projectId },
31
- query
32
- } }));
33
- branches.push(...page.data);
34
- if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
35
- cursor = page.pagination.nextCursor;
36
- }
37
- return branches;
38
- });
39
- /**
40
- * The project's implicit default Branch — every live Project owns exactly
41
- * one (a platform invariant). The list endpoint has no `isDefault` filter, so
42
- * this pages through every Branch and picks it out client-side. Never creates
43
- * one: its absence means the platform's invariant is broken, which is not
44
- * something a deploy can repair.
45
- */
46
- const resolveDefaultBranchId = (client, projectId) => Effect.gen(function* () {
47
- const found = (yield* listAllBranches(client, projectId)).find((b) => b.isDefault);
48
- if (found !== void 0) return found.id;
49
- return yield* Effect.fail(new PrismaApiError({
50
- status: 0,
51
- message: `project ${projectId} has no default Branch — the platform guarantees every live Project owns one; contact support.`
52
- }));
53
- });
54
- /** A named stage carries its `branchId`; production omits it, and its state lives on the Project's default Branch. */
55
- const resolveBranchId = (client, container) => container.branchId !== void 0 ? Effect.succeed(container.branchId) : resolveDefaultBranchId(client, container.projectId);
24
+ /** A named stage carries its `branchId`; production carries `defaultBranchId` (its state lives on the Project's default Branch) re-resolved only when neither is present. */
25
+ const resolveBranchId = (client, container) => {
26
+ const known = container.branchId ?? container.defaultBranchId;
27
+ return known !== void 0 ? Effect.succeed(known) : resolveDefaultBranchId(client, container.projectId);
28
+ };
56
29
  /**
57
- * Every database on this Branch. Uses the flat `GET /v1/databases`, which
58
- * accepts `projectId` and `branchId` together — the project-scoped listing
59
- * has no branch filter at all.
30
+ * Every database on this Branch (bounded — drivePages). Uses the flat
31
+ * `GET /v1/databases`, which accepts `projectId` and `branchId` together —
32
+ * the project-scoped listing has no branch filter at all.
60
33
  */
61
- const listAllDatabasesOnBranch = (client, projectId, branchId) => Effect.gen(function* () {
62
- const databases = [];
63
- let cursor;
64
- for (;;) {
65
- const query = cursor === void 0 ? {
66
- projectId,
67
- branchId
68
- } : {
69
- projectId,
70
- branchId,
71
- cursor
72
- };
73
- const page = yield* call(() => client.GET("/v1/databases", { params: { query } }));
74
- databases.push(...page.data);
75
- if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
76
- cursor = page.pagination.nextCursor;
77
- }
78
- return databases;
79
- });
34
+ const listAllDatabasesOnBranch = (client, projectId, branchId) => collectPages(`databases on branch ${branchId}`, (cursor) => call(() => client.GET("/v1/databases", { params: { query: cursor === void 0 ? {
35
+ projectId,
36
+ branchId
37
+ } : {
38
+ projectId,
39
+ branchId,
40
+ cursor
41
+ } } })));
80
42
  /**
81
43
  * Databases on this Branch named `prisma-composer-state`, oldest first,
82
44
  * excluding the Branch's own default database. The default database is always
@@ -193,11 +155,17 @@ const CONNECTION_MAX_AGE_MS = 1440 * 60 * 1e3;
193
155
  *
194
156
  * `isDefault` is left at the API's `false` default: the state database must
195
157
  * never be the stage's default database, which belongs to the user.
158
+ *
159
+ * The region is explicit: `'inherit'` copies the project default database's
160
+ * region, but composer creates Projects with `createDatabase: false`, so
161
+ * there is nothing to inherit from and the API rejects it. `us-east-1` is
162
+ * what `'inherit'` always resolved to before (composer never set a project
163
+ * region), and matches the target's DEFAULT_REGION.
196
164
  */
197
165
  const createStateDatabase = (client, projectId, branchId) => call(() => client.POST("/v1/databases", { body: {
198
166
  projectId,
199
167
  name: STATE_DATABASE_NAME,
200
- region: "inherit",
168
+ region: "us-east-1",
201
169
  branchId
202
170
  } })).pipe(Effect.map((created) => ({
203
171
  id: created.data.id,
@@ -341,6 +309,7 @@ const bootstrapStateConnectionWith = (container, verify) => Effect.gen(function*
341
309
  yield* cleanupAgedConnections(client, database.id);
342
310
  return {
343
311
  projectId: container.projectId,
312
+ branchId,
344
313
  databaseId: database.id,
345
314
  connectionString
346
315
  };
@@ -365,6 +334,50 @@ const deleteStateDatabaseWith = (container, verify) => Effect.gen(function* () {
365
334
  console.error(`hosted state: removed state database ${candidate.id} from branch ${branchId}`);
366
335
  }
367
336
  });
337
+ /** Whether the (stack, stage) scope holds any rows in either state table. */
338
+ const scopeOccupied = (sql, stack, stage) => Effect.tryPromise({
339
+ try: async () => {
340
+ return (await sql`
341
+ select
342
+ exists (select 1 from alchemy_resource_state where stack = ${stack} and stage = ${stage})
343
+ or exists (select 1 from alchemy_stack_output where stack = ${stack} and stage = ${stage})
344
+ as occupied
345
+ `)[0]?.occupied === true;
346
+ },
347
+ catch: toStateStoreError
348
+ });
349
+ const listAppsOnBranch = (client, projectId, branchId) => collectPages(`apps on branch ${branchId}`, (cursor) => call(() => client.GET("/v1/apps", { params: { query: cursor === void 0 ? {
350
+ projectId,
351
+ branchId
352
+ } : {
353
+ projectId,
354
+ branchId,
355
+ cursor
356
+ } } })));
357
+ /**
358
+ * The empty-scope-with-live-apps case: the branch-id scope holds no rows
359
+ * while the platform already runs Compute apps on the target Branch. Either
360
+ * this is a pre-branch-id deployment whose rows still sit under a legacy
361
+ * scope (`dev_$USER`, `unknown`, or the old stage name — there is no
362
+ * automatic migration; operator decision TML-3157), or the deploy targets a
363
+ * project that already runs apps. Deploying would recreate every resource
364
+ * and die in per-resource `already_exists` failures — so fail once, up
365
+ * front, naming the empty scope and what exists. Any app on the Branch is
366
+ * this deploy's concern: the Project is app-scoped and the Branch is the
367
+ * deploy's own target. A genuinely fresh deploy sees an empty Branch and
368
+ * passes. Local dev never reaches this — the dev stack pins
369
+ * `state: localState()` (generate-dev-stack.ts).
370
+ */
371
+ const failOnEmptyScopeWithLiveApps = (projectId, branchId, stack, stage) => Effect.gen(function* () {
372
+ const client = yield* ManagementClient;
373
+ const apps = yield* listAppsOnBranch(client, projectId, branchId);
374
+ if (apps.length === 0) return;
375
+ const names = apps.map((app) => `"${app.name}"`).join(", ");
376
+ return yield* Effect.fail(new PrismaApiError({
377
+ status: 0,
378
+ message: `the deploy state scope "${stage}" is empty, but the platform already runs ${String(apps.length)} app(s) on the target branch ${branchId}: ${names}. With no state, a deploy would recreate every resource and fail with already_exists, and a destroy would remove nothing. If those apps are a deployment from before the branch-id state scope, UPDATE the stage column of alchemy_resource_state and alchemy_stack_output to '${stage}' WHERE stack = '${stack}' (the same database can hold other stacks' rows) in this branch's prisma-composer-state database — or delete the apps in the Prisma Console (or via the Management API) and redeploy fresh. If they are another deployment's, remove them or deploy into a different project. Then retry.`
379
+ }));
380
+ });
368
381
  /** Codes for "can't reach the host yet" — DNS/refusal, not a mid-session drop. */
369
382
  const ESTABLISHMENT_CODES = /* @__PURE__ */ new Set([
370
383
  "ECONNREFUSED",
@@ -656,14 +669,15 @@ const guardStateService = (service, checkLive, now = Date.now) => {
656
669
  * state store is `Effect.die` in `Stack.make`).
657
670
  */
658
671
  const prismaStateLayer = (ids) => {
659
- const { projectId, branchId } = ids;
672
+ const { projectId, branchId, defaultBranchId } = ids;
660
673
  return Layer.effect(State, Effect.gen(function* () {
661
674
  const stack = yield* Stack;
662
675
  const container = branchId === void 0 ? projectId : `${projectId}/${branchId}`;
663
676
  const bootstrapError = (step) => (cause) => hostedStateBootstrapError(container, step, cause);
664
- const { connectionString } = yield* bootstrapStateConnection(branchId === void 0 ? { projectId } : {
677
+ const { connectionString, branchId: stateBranchId } = yield* bootstrapStateConnection({
665
678
  projectId,
666
- branchId
679
+ ...branchId !== void 0 ? { branchId } : {},
680
+ ...defaultBranchId !== void 0 ? { defaultBranchId } : {}
667
681
  }).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))), Effect.mapError(bootstrapError("resolving the state database on the stage branch")));
668
682
  const sql = postgres(Redacted.value(connectionString), {
669
683
  max: 5,
@@ -673,6 +687,7 @@ const prismaStateLayer = (ids) => {
673
687
  yield* migratePrismaState(sql).pipe(Effect.retry(Schedule.both(Schedule.spaced("5 seconds"), Schedule.during("2 minutes"))), Effect.mapError(bootstrapError("schema migration")));
674
688
  const lock = yield* acquireStateLock(sql, stack.name, stack.stage).pipe(Effect.mapError(bootstrapError("lock acquisition")));
675
689
  yield* Effect.addFinalizer(() => Effect.promise(() => lock.release()));
690
+ if (!(yield* scopeOccupied(sql, stack.name, stack.stage).pipe(Effect.mapError(bootstrapError("probing the deploy state scope"))))) yield* failOnEmptyScopeWithLiveApps(projectId, stateBranchId, stack.name, stack.stage).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))), Effect.mapError(bootstrapError("checking the empty deploy state scope")));
676
691
  const service = guardStateService(makePrismaStateService(sql), lock.checkLive);
677
692
  return Effect.succeed(service);
678
693
  })).pipe(Layer.orDie);
@@ -1241,11 +1256,12 @@ const selfOriginValue = (provisioned, address) => Output.map(provisioned.endpoin
1241
1256
  const prismaState = () => ({
1242
1257
  extension: PRISMA_CLOUD_EXTENSION_ID,
1243
1258
  create: (container) => {
1244
- const { projectId, branchId } = prismaCloudContainerOf(container);
1245
- return prismaStateLayer(branchId !== void 0 ? {
1259
+ const { projectId, branchId, defaultBranchId } = prismaCloudContainerOf(container);
1260
+ return prismaStateLayer({
1246
1261
  projectId,
1247
- branchId
1248
- } : { projectId });
1262
+ ...branchId !== void 0 ? { branchId } : {},
1263
+ ...defaultBranchId !== void 0 ? { defaultBranchId } : {}
1264
+ });
1249
1265
  }
1250
1266
  });
1251
1267
  const KNOWN_REGION_SET = new Set(COMPUTE_REGIONS);