@prisma/composer-prisma-cloud 0.5.0 → 0.6.0-dev.1
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/auth/pack.mjs +1 -1
- package/dist/auth/pack.mjs.map +1 -1
- package/dist/auth/testing.mjs +1 -1
- package/dist/auth/testing.mjs.map +1 -1
- package/dist/control.mjs +79 -63
- package/dist/control.mjs.map +1 -1
- package/dist/local-target.mjs +1 -1
- package/dist/local-target.mjs.map +1 -1
- package/dist/{s3-credentials-resource-tfirOJBj-79G9R_h8.mjs → s3-credentials-resource-BMsm9nho-C0Bg1909.mjs} +186 -107
- package/dist/s3-credentials-resource-BMsm9nho-C0Bg1909.mjs.map +1 -0
- package/package.json +21 -20
- package/dist/s3-credentials-resource-tfirOJBj-79G9R_h8.mjs.map +0 -1
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
58
|
-
* accepts `projectId` and `branchId` together —
|
|
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) =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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: "
|
|
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(
|
|
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(
|
|
1259
|
+
const { projectId, branchId, defaultBranchId } = prismaCloudContainerOf(container);
|
|
1260
|
+
return prismaStateLayer({
|
|
1246
1261
|
projectId,
|
|
1247
|
-
branchId
|
|
1248
|
-
|
|
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);
|