@prisma/composer-prisma-cloud 0.1.0 → 0.2.0-dev.10
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.d.mts +79 -36
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +1074 -330
- package/dist/control.mjs.map +1 -1
- package/dist/cron/index.d.mts +63 -10
- package/dist/cron/index.d.mts.map +1 -0
- package/dist/cron/index.mjs +255 -42
- package/dist/cron/index.mjs.map +1 -1
- package/dist/cron/scheduler-entrypoint.mjs +578 -300
- package/dist/cron/scheduler-entrypoint.mjs.map +1 -1
- package/dist/cron/scheduler-service.mjs +255 -42
- package/dist/cron/scheduler-service.mjs.map +1 -1
- package/dist/email/email-entrypoint.mjs +19738 -0
- package/dist/email/email-entrypoint.mjs.map +1 -0
- package/dist/email/email-service.mjs +566 -0
- package/dist/email/email-service.mjs.map +1 -0
- package/dist/email/index.d.mts +282 -0
- package/dist/email/index.d.mts.map +1 -0
- package/dist/email/index.mjs +653 -0
- package/dist/email/index.mjs.map +1 -0
- package/dist/email/testing.d.mts +22 -0
- package/dist/email/testing.d.mts.map +1 -0
- package/dist/email/testing.mjs +1321 -0
- package/dist/email/testing.mjs.map +1 -0
- package/dist/index.d.mts +152 -26
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +108 -43
- package/dist/index.mjs.map +1 -1
- package/dist/{prisma-next-COrwlg3N.mjs → prisma-next-BvBwGCIt-CSauMX4l.mjs} +20 -4
- package/dist/prisma-next-BvBwGCIt-CSauMX4l.mjs.map +1 -0
- package/dist/prisma-next.d.mts +14 -6
- package/dist/prisma-next.d.mts.map +1 -0
- package/dist/prisma-next.mjs +1 -1
- package/dist/provisioned-edges-B_XS1Mz--NFJgcXK7.mjs +211 -0
- package/dist/provisioned-edges-B_XS1Mz--NFJgcXK7.mjs.map +1 -0
- package/dist/serializer-Cx5slrV4-xJfH6EWS.d.mts +36 -0
- package/dist/serializer-Cx5slrV4-xJfH6EWS.d.mts.map +1 -0
- package/dist/{serializer-C2CsA7xm-29Eg2Tjl.mjs → serializer-D8_84XWO-DA2B6YbX.mjs} +91 -7
- package/dist/serializer-D8_84XWO-DA2B6YbX.mjs.map +1 -0
- package/dist/storage/index.d.mts +56 -20
- package/dist/storage/index.d.mts.map +1 -0
- package/dist/storage/index.mjs +267 -44
- package/dist/storage/index.mjs.map +1 -1
- package/dist/storage/storage-entrypoint.mjs +7501 -155
- package/dist/storage/storage-entrypoint.mjs.map +1 -1
- package/dist/storage/storage-service.mjs +267 -44
- package/dist/storage/storage-service.mjs.map +1 -1
- package/dist/storage/testing.d.mts +7 -4
- package/dist/storage/testing.d.mts.map +1 -0
- package/dist/storage/testing.mjs.map +1 -1
- package/dist/streams/index.d.mts +222 -0
- package/dist/streams/index.d.mts.map +1 -0
- package/dist/streams/index.mjs +4170 -0
- package/dist/streams/index.mjs.map +1 -0
- package/dist/streams/streams-entrypoint.mjs +48211 -0
- package/dist/streams/streams-entrypoint.mjs.map +1 -0
- package/dist/streams/streams-service.mjs +923 -0
- package/dist/streams/streams-service.mjs.map +1 -0
- package/dist/streams/testing.d.mts +33 -0
- package/dist/streams/testing.d.mts.map +1 -0
- package/dist/streams/testing.mjs +31335 -0
- package/dist/streams/testing.mjs.map +1 -0
- package/dist/testing.d.mts +1 -2
- package/dist/testing.d.mts.map +1 -0
- package/dist/testing.mjs +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +35 -23
- package/src/exports/control.ts +1 -0
- package/src/exports/cron.ts +1 -0
- package/src/exports/email-testing.ts +1 -0
- package/src/exports/email.ts +1 -0
- package/src/exports/index.ts +1 -0
- package/src/exports/prisma-next.ts +1 -0
- package/src/exports/storage-testing.ts +1 -0
- package/src/exports/storage.ts +1 -0
- package/src/exports/streams-testing.ts +1 -0
- package/src/exports/streams.ts +1 -0
- package/src/exports/testing.ts +1 -0
- package/dist/prisma-next-COrwlg3N.mjs.map +0 -1
- package/dist/serializer-C2CsA7xm-29Eg2Tjl.mjs.map +0 -1
package/dist/control.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { a as encode, c as paramEntries, d as secretPointerRows, n as configKey, o as encodeParamPointer, u as secretName } from "./serializer-D8_84XWO-DA2B6YbX.mjs";
|
|
2
|
+
import { c as paramName, l as provisionedEdges, n as SELF_ORIGIN, o as isEnvParamSource, r as STREAMS_API_KEY, s as paramBindingFor, t as RESERVED_PROVIDER_PARAMS } from "./provisioned-edges-B_XS1Mz--NFJgcXK7.mjs";
|
|
3
|
+
import { a as normalizeSslMode, n as isPnPostgresResourceNode, o as withConnectionRetry } from "./prisma-next-BvBwGCIt-CSauMX4l.mjs";
|
|
4
|
+
import { isParamSource, paramManifest, provisionManifest } from "@prisma/composer";
|
|
4
5
|
import { blindCast } from "@prisma/composer/casts";
|
|
6
|
+
import { RPC_PEER_KEY } from "@prisma/composer/service-rpc";
|
|
5
7
|
import pg from "pg";
|
|
6
|
-
import * as Layer from "effect/Layer";
|
|
7
8
|
import { createManagementApiClient } from "@prisma/management-api-sdk";
|
|
8
9
|
import * as Context from "effect/Context";
|
|
9
10
|
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
10
12
|
import * as Redacted from "effect/Redacted";
|
|
11
13
|
import * as Config$1 from "effect/Config";
|
|
12
14
|
import * as Data from "effect/Data";
|
|
@@ -18,15 +20,15 @@ import * as crypto$1 from "node:crypto";
|
|
|
18
20
|
import * as os from "node:os";
|
|
19
21
|
import * as path from "node:path";
|
|
20
22
|
import * as zlib from "node:zlib";
|
|
21
|
-
import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveStateRecursive } from "alchemy/State";
|
|
22
23
|
import postgres from "postgres";
|
|
24
|
+
import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveStateRecursive } from "alchemy/State";
|
|
23
25
|
import * as Output from "alchemy/Output";
|
|
24
|
-
import { loadConfig } from "@prisma-next/
|
|
26
|
+
import { loadConfig } from "@prisma-next/config-loader";
|
|
25
27
|
import { resolve } from "pathe";
|
|
26
28
|
import { readRef } from "@prisma-next/migration-tools/refs";
|
|
27
29
|
import { APP_SPACE_ID, readContractSpaceHeadRef, spaceMigrationDirectory, spaceRefsDirectory } from "@prisma-next/migration-tools/spaces";
|
|
28
30
|
import { createPostgresControlClient } from "@prisma-next/postgres/control";
|
|
29
|
-
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-
|
|
31
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CJM0E6rj.mjs
|
|
30
32
|
/**
|
|
31
33
|
* The Prisma service token used to authenticate Management API calls. Kept
|
|
32
34
|
* as a Redacted value so it never lands in logs or error output.
|
|
@@ -66,9 +68,87 @@ const callOptional = (f) => attempt$1(f).pipe(Effect.flatMap((r) => r.response.s
|
|
|
66
68
|
/** Fire-and-forget a call, tolerating a 404 (already deleted). */
|
|
67
69
|
const callVoid = (f) => attempt$1(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
|
|
68
70
|
//#endregion
|
|
69
|
-
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/
|
|
71
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/buckets.mjs
|
|
72
|
+
/** A Prisma **Object Store bucket** inside a project. */
|
|
73
|
+
const Bucket = Resource("Prisma.Bucket");
|
|
74
|
+
const BucketProvider = () => Provider.effect(Bucket, Effect.gen(function* () {
|
|
75
|
+
const client = yield* ManagementClient;
|
|
76
|
+
return {
|
|
77
|
+
stables: ["id"],
|
|
78
|
+
list: () => Effect.succeed([]),
|
|
79
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
80
|
+
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } })) : void 0;
|
|
81
|
+
if (observed) return {
|
|
82
|
+
id: observed.data.id,
|
|
83
|
+
name: observed.data.name
|
|
84
|
+
};
|
|
85
|
+
const created = yield* call(() => client.POST("/v1/buckets", { body: {
|
|
86
|
+
projectId: news.projectId,
|
|
87
|
+
name: news.name,
|
|
88
|
+
...news.branchId !== void 0 ? { branchId: news.branchId } : {}
|
|
89
|
+
} }));
|
|
90
|
+
return {
|
|
91
|
+
id: created.data.id,
|
|
92
|
+
name: created.data.name
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
delete: Effect.fn(function* ({ output }) {
|
|
96
|
+
yield* callVoid(() => client.DELETE("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } }));
|
|
97
|
+
}),
|
|
98
|
+
read: Effect.fn(function* ({ output }) {
|
|
99
|
+
if (!output?.id) return void 0;
|
|
100
|
+
const b = yield* callOptional(() => client.GET("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } }));
|
|
101
|
+
return b ? {
|
|
102
|
+
id: b.data.id,
|
|
103
|
+
name: b.data.name
|
|
104
|
+
} : void 0;
|
|
105
|
+
})
|
|
106
|
+
};
|
|
107
|
+
}));
|
|
108
|
+
/** A **bucket access key** for a Prisma Object Store bucket — yields the S3 credentials. */
|
|
109
|
+
const BucketKey = Resource("Prisma.BucketKey");
|
|
110
|
+
const BucketKeyProvider = () => Provider.effect(BucketKey, Effect.gen(function* () {
|
|
111
|
+
const client = yield* ManagementClient;
|
|
112
|
+
return {
|
|
113
|
+
stables: [
|
|
114
|
+
"id",
|
|
115
|
+
"bucketId",
|
|
116
|
+
"secretAccessKey",
|
|
117
|
+
"accessKeyId",
|
|
118
|
+
"endpoint",
|
|
119
|
+
"bucketName"
|
|
120
|
+
],
|
|
121
|
+
list: () => Effect.succeed([]),
|
|
122
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
123
|
+
if (output?.id) return output;
|
|
124
|
+
const created = yield* call(() => client.POST("/v1/buckets/{bucketId}/keys", {
|
|
125
|
+
params: { path: { bucketId: news.bucketId } },
|
|
126
|
+
body: {
|
|
127
|
+
name: news.name,
|
|
128
|
+
role: news.role
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
return {
|
|
132
|
+
id: created.data.id,
|
|
133
|
+
bucketId: news.bucketId,
|
|
134
|
+
accessKeyId: created.data.accessKeyId,
|
|
135
|
+
secretAccessKey: Redacted.make(created.data.secretAccessKey),
|
|
136
|
+
endpoint: created.data.endpoint,
|
|
137
|
+
bucketName: created.data.bucketName
|
|
138
|
+
};
|
|
139
|
+
}),
|
|
140
|
+
delete: Effect.fn(function* ({ output }) {
|
|
141
|
+
yield* callVoid(() => client.DELETE("/v1/buckets/{bucketId}/keys/{keyId}", { params: { path: {
|
|
142
|
+
bucketId: output.bucketId,
|
|
143
|
+
keyId: output.id
|
|
144
|
+
} } }));
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
}));
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/compute.mjs
|
|
70
150
|
/**
|
|
71
|
-
* Stopping a deployment before the
|
|
151
|
+
* Stopping a deployment before the app that owns it can be
|
|
72
152
|
* deleted is asynchronous on the platform's side: DELETE can 409 with this
|
|
73
153
|
* message while the deployment is still winding down. Retrying blindly on
|
|
74
154
|
* every API error would mask real failures (bad auth, a genuinely conflicting
|
|
@@ -99,64 +179,62 @@ const ComputeServiceProvider = () => Provider.effect(ComputeService, Effect.gen(
|
|
|
99
179
|
stables: ["id"],
|
|
100
180
|
list: () => Effect.succeed([]),
|
|
101
181
|
reconcile: Effect.fn(function* ({ news, output }) {
|
|
102
|
-
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/
|
|
182
|
+
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/apps/{appId}", { params: { path: { appId: output.id } } })) : void 0;
|
|
103
183
|
if (observed) return {
|
|
104
184
|
id: observed.data.id,
|
|
105
185
|
name: observed.data.name,
|
|
106
|
-
endpointDomain: observed.data.
|
|
186
|
+
endpointDomain: observed.data.appEndpointDomain
|
|
107
187
|
};
|
|
108
|
-
const created = yield* call(() => client.POST("/v1/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
}));
|
|
188
|
+
const created = yield* call(() => client.POST("/v1/apps", { body: {
|
|
189
|
+
displayName: news.name,
|
|
190
|
+
projectId: news.projectId,
|
|
191
|
+
...news.region && { regionId: news.region },
|
|
192
|
+
...news.branchId !== void 0 && { branchId: news.branchId }
|
|
193
|
+
} }));
|
|
116
194
|
return {
|
|
117
195
|
id: created.data.id,
|
|
118
196
|
name: created.data.name,
|
|
119
|
-
endpointDomain: created.data.
|
|
197
|
+
endpointDomain: created.data.appEndpointDomain
|
|
120
198
|
};
|
|
121
199
|
}),
|
|
122
200
|
delete: Effect.fn(function* ({ output }) {
|
|
123
|
-
yield* callVoid(() => client.DELETE("/v1/
|
|
201
|
+
yield* callVoid(() => client.DELETE("/v1/apps/{appId}", { params: { path: { appId: output.id } } })).pipe(Effect.retry({
|
|
124
202
|
schedule: deleteSafeRetrySchedule,
|
|
125
203
|
while: isDeleteNotSafeYet
|
|
126
204
|
}));
|
|
127
205
|
}),
|
|
128
206
|
read: Effect.fn(function* ({ output }) {
|
|
129
207
|
if (!output?.id) return void 0;
|
|
130
|
-
const s = yield* callOptional(() => client.GET("/v1/
|
|
208
|
+
const s = yield* callOptional(() => client.GET("/v1/apps/{appId}", { params: { path: { appId: output.id } } }));
|
|
131
209
|
return s ? {
|
|
132
210
|
id: s.data.id,
|
|
133
211
|
name: s.data.name,
|
|
134
|
-
endpointDomain: s.data.
|
|
212
|
+
endpointDomain: s.data.appEndpointDomain
|
|
135
213
|
} : void 0;
|
|
136
214
|
})
|
|
137
215
|
};
|
|
138
216
|
}));
|
|
139
217
|
/**
|
|
140
|
-
* A **deployment** of a Prisma
|
|
218
|
+
* A **deployment** of a Prisma app — creates a deployment, uploads
|
|
141
219
|
* its artifact, starts the VM, waits for it to run, then promotes it to the
|
|
142
|
-
*
|
|
220
|
+
* app's stable endpoint.
|
|
143
221
|
*/
|
|
144
222
|
const Deployment = Resource("Prisma.Deployment");
|
|
145
223
|
const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
|
|
146
224
|
const client = yield* ManagementClient;
|
|
147
|
-
const waitForRunning = (
|
|
225
|
+
const waitForRunning = (deploymentId) => call(() => client.GET("/v1/deployments/{deploymentId}", { params: { path: { deploymentId } } })).pipe(Effect.flatMap((v) => v.data.status === "running" ? Effect.void : Effect.fail(new PrismaApiError({
|
|
148
226
|
status: 409,
|
|
149
|
-
message: `
|
|
227
|
+
message: `deployment ${deploymentId} is ${v.data.status}, not running`
|
|
150
228
|
}))), Effect.retry(Schedule.both(Schedule.spaced("2 seconds"), Schedule.during("2 minutes"))));
|
|
151
229
|
return {
|
|
152
230
|
stables: [],
|
|
153
231
|
list: () => Effect.succeed([]),
|
|
154
232
|
reconcile: Effect.fn(function* ({ news }) {
|
|
155
|
-
const created = yield* call(() => client.POST("/v1/
|
|
156
|
-
params: { path: {
|
|
233
|
+
const created = yield* call(() => client.POST("/v1/apps/{appId}/deployments", {
|
|
234
|
+
params: { path: { appId: news.computeServiceId } },
|
|
157
235
|
body: news.port !== void 0 ? { portMapping: { http: news.port } } : {}
|
|
158
236
|
}));
|
|
159
|
-
const
|
|
237
|
+
const deploymentId = created.data.id;
|
|
160
238
|
if (created.data.uploadUrl) {
|
|
161
239
|
const uploadUrl = created.data.uploadUrl;
|
|
162
240
|
const artifact = yield* Effect.try({
|
|
@@ -183,24 +261,23 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
183
261
|
})
|
|
184
262
|
});
|
|
185
263
|
}
|
|
186
|
-
yield* call(() => client.POST("/v1/
|
|
187
|
-
yield* waitForRunning(
|
|
188
|
-
yield* call(() => client.POST("/v1/
|
|
189
|
-
params: { path: {
|
|
190
|
-
body: {
|
|
191
|
-
}));
|
|
192
|
-
const deployedUrl = (yield* call(() => client.GET("/v1/compute-services/{computeServiceId}", { params: { path: { computeServiceId: news.computeServiceId } } }))).data.serviceEndpointDomain;
|
|
264
|
+
yield* call(() => client.POST("/v1/deployments/{deploymentId}/start", { params: { path: { deploymentId } } }));
|
|
265
|
+
yield* waitForRunning(deploymentId);
|
|
266
|
+
const deployedUrl = (yield* call(() => client.POST("/v1/apps/{appId}/promote", {
|
|
267
|
+
params: { path: { appId: news.computeServiceId } },
|
|
268
|
+
body: { deploymentId }
|
|
269
|
+
}))).data.appEndpointDomain;
|
|
193
270
|
return {
|
|
194
|
-
|
|
271
|
+
deploymentId,
|
|
195
272
|
...deployedUrl !== void 0 && { deployedUrl }
|
|
196
273
|
};
|
|
197
274
|
}),
|
|
198
275
|
delete: Effect.fn(function* () {}),
|
|
199
276
|
read: Effect.fn(function* ({ output }) {
|
|
200
|
-
if (!output?.
|
|
201
|
-
const v = yield* callOptional(() => client.GET("/v1/
|
|
277
|
+
if (!output?.deploymentId) return void 0;
|
|
278
|
+
const v = yield* callOptional(() => client.GET("/v1/deployments/{deploymentId}", { params: { path: { deploymentId: output.deploymentId } } }));
|
|
202
279
|
return v ? {
|
|
203
|
-
|
|
280
|
+
deploymentId: v.data.id,
|
|
204
281
|
...v.data.previewDomain && { deployedUrl: v.data.previewDomain }
|
|
205
282
|
} : void 0;
|
|
206
283
|
})
|
|
@@ -231,7 +308,7 @@ const EnvironmentVariableProvider = () => Provider.effect(EnvironmentVariable, E
|
|
|
231
308
|
key: news.key
|
|
232
309
|
} } }))).data?.[0]?.id;
|
|
233
310
|
if (matchId !== void 0) {
|
|
234
|
-
if (!(news.key === "DATABASE_URL" || news.key === "DATABASE_URL_POOLED")) throw new Error(`EnvironmentVariable "${news.key}" (project "${news.projectId}", class "${cls}") exists but is untracked in this deploy state — refusing to overwrite a reserved
|
|
311
|
+
if (!(news.key === "DATABASE_URL" || news.key === "DATABASE_URL_POOLED")) throw new Error(`EnvironmentVariable "${news.key}" (project "${news.projectId}", class "${cls}") exists but is untracked in this deploy state — refusing to overwrite a reserved COMPOSER_ key. Restore this deploy's hosted state, or remove the variable to let this deploy recreate it.`);
|
|
235
312
|
id = matchId;
|
|
236
313
|
}
|
|
237
314
|
}
|
|
@@ -400,8 +477,39 @@ function packageComputeArtifact(opts) {
|
|
|
400
477
|
sha256
|
|
401
478
|
};
|
|
402
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* The `ServiceKey` Alchemy resource (ADR-0030) — mints a random 256-bit key
|
|
482
|
+
* ONCE at create and keeps it STABLE across deploys, so an unchanged edge
|
|
483
|
+
* no-ops on redeploy. Same mint-once-stable lifecycle as `S3Credentials`
|
|
484
|
+
* (`packages/1-prisma-cloud/1-extensions/target/src/s3-credentials-resource.ts`):
|
|
485
|
+
* Web Crypto only (`crypto.getRandomValues` — no `node:` import), persisted in
|
|
486
|
+
* Alchemy state; on every later apply the provider returns the persisted
|
|
487
|
+
* attributes (`reconcile`'s `output`) unchanged. One resource per RPC edge;
|
|
488
|
+
* rotation is destroy/recreate.
|
|
489
|
+
*/
|
|
490
|
+
/** The `ServiceKey` resource constructor — `yield* Prisma.ServiceKey(id, {})` in the lowering. */
|
|
491
|
+
const ServiceKey = Resource("PrismaCloud.ServiceKey");
|
|
492
|
+
/** A fresh 256-bit key as 64 lowercase hex chars (Web Crypto — no node import). */
|
|
493
|
+
function mintServiceKey() {
|
|
494
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
|
|
495
|
+
return { value: Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("") };
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* The `ServiceKey` provider service. `reconcile` runs for create and update;
|
|
499
|
+
* it returns the persisted `output` when present (a redeploy reuses the
|
|
500
|
+
* stored key — the no-op property) and mints a fresh key only on first
|
|
501
|
+
* create. Nothing to enumerate (`list` → `[]`) or tear down (`delete` →
|
|
502
|
+
* no-op; the key lives only in state).
|
|
503
|
+
*/
|
|
504
|
+
const serviceKeyProviderService = {
|
|
505
|
+
list: () => Effect.succeed([]),
|
|
506
|
+
reconcile: ({ output }) => Effect.sync(() => output ?? mintServiceKey()),
|
|
507
|
+
delete: () => Effect.void
|
|
508
|
+
};
|
|
509
|
+
/** The `ServiceKey` provider layer — merged into the Prisma Cloud extension's `providers()`. */
|
|
510
|
+
const ServiceKeyProvider = () => Provider.effect(ServiceKey, Effect.succeed(serviceKeyProviderService));
|
|
403
511
|
//#endregion
|
|
404
|
-
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/postgres
|
|
512
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/postgres.mjs
|
|
405
513
|
/** A **connection** to a Prisma Postgres database — yields the connection string. */
|
|
406
514
|
const Connection = Resource("Prisma.Connection");
|
|
407
515
|
const ConnectionProvider = () => Provider.effect(Connection, Effect.gen(function* () {
|
|
@@ -440,25 +548,23 @@ const DatabaseProvider = () => Provider.effect(Database, Effect.gen(function* ()
|
|
|
440
548
|
list: () => Effect.succeed([]),
|
|
441
549
|
reconcile: Effect.fn(function* ({ news, output }) {
|
|
442
550
|
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/databases/{databaseId}", { params: { path: { databaseId: output.id } } })) : void 0;
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
name: news.name,
|
|
453
|
-
region: news.region,
|
|
454
|
-
...news.isDefault !== void 0 && { isDefault: news.isDefault }
|
|
455
|
-
}
|
|
456
|
-
}));
|
|
457
|
-
result = {
|
|
551
|
+
if (!observed) {
|
|
552
|
+
const created = yield* call(() => client.POST("/v1/databases", { body: {
|
|
553
|
+
projectId: news.projectId,
|
|
554
|
+
name: news.name,
|
|
555
|
+
region: news.region,
|
|
556
|
+
...news.isDefault !== void 0 && { isDefault: news.isDefault },
|
|
557
|
+
...news.branchId !== void 0 && { branchId: news.branchId }
|
|
558
|
+
} }));
|
|
559
|
+
return {
|
|
458
560
|
id: created.data.id,
|
|
459
561
|
name: created.data.name
|
|
460
562
|
};
|
|
461
563
|
}
|
|
564
|
+
const result = {
|
|
565
|
+
id: observed.data.id,
|
|
566
|
+
name: observed.data.name
|
|
567
|
+
};
|
|
462
568
|
if (news.branchId !== void 0) {
|
|
463
569
|
const branchId = news.branchId;
|
|
464
570
|
yield* call(() => client.PATCH("/v1/databases/{databaseId}", {
|
|
@@ -516,7 +622,107 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
516
622
|
})
|
|
517
623
|
};
|
|
518
624
|
}));
|
|
519
|
-
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
|
|
627
|
+
/** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
|
|
628
|
+
var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
|
|
629
|
+
const listAllProjects = (client) => Effect.gen(function* () {
|
|
630
|
+
const projects = [];
|
|
631
|
+
let cursor;
|
|
632
|
+
for (;;) {
|
|
633
|
+
const query = cursor === void 0 ? {} : { cursor };
|
|
634
|
+
const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
|
|
635
|
+
projects.push(...page.data);
|
|
636
|
+
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
637
|
+
cursor = page.pagination.nextCursor;
|
|
638
|
+
}
|
|
639
|
+
return projects;
|
|
640
|
+
});
|
|
641
|
+
/**
|
|
642
|
+
* Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
|
|
643
|
+
* bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
|
|
644
|
+
* one (the same normalization `state/bootstrap.ts` applies to the same
|
|
645
|
+
* `/v1/projects` listing).
|
|
646
|
+
*/
|
|
647
|
+
const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
|
|
648
|
+
/**
|
|
649
|
+
* Finds the app's Project by name in the workspace — PDP allows duplicate
|
|
650
|
+
* project names, so more than one can match; the oldest wins. Creates one
|
|
651
|
+
* if none match, unless `ensure` is `false` (find-only — `destroy`), in
|
|
652
|
+
* which case an absent Project fails with `ContainerNotFoundError`. No
|
|
653
|
+
* ownership marker and no `--project` override (both deferred — see
|
|
654
|
+
* ADR-0019).
|
|
655
|
+
*/
|
|
656
|
+
const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
|
|
657
|
+
const oldest = (yield* listAllProjects(client)).filter((p) => bareWorkspaceId(p.workspace.id) === bareWorkspaceId(workspaceId) && p.name === appName).sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
658
|
+
if (oldest !== void 0) return oldest.id;
|
|
659
|
+
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
|
|
660
|
+
return (yield* call(() => client.POST("/v1/projects", { body: {
|
|
661
|
+
name: appName,
|
|
662
|
+
workspaceId
|
|
663
|
+
} }))).data.id;
|
|
664
|
+
});
|
|
665
|
+
const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
666
|
+
path: { projectId },
|
|
667
|
+
query: { gitName }
|
|
668
|
+
} })).pipe(Effect.map((page) => page.data[0]?.id));
|
|
669
|
+
/**
|
|
670
|
+
* Finds the stage's Branch by its exact `gitName`, creating it if absent
|
|
671
|
+
* unless `ensure` is `false` (find-only — `destroy`), in which case an
|
|
672
|
+
* absent Branch fails with `ContainerNotFoundError`. The Management API has
|
|
673
|
+
* no server-side "create-or-return" idempotency (`POST
|
|
674
|
+
* /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
|
|
675
|
+
* field to make that a no-op), so idempotency is client-side: observe
|
|
676
|
+
* first, and on a racing 409 from create, re-observe rather than fail.
|
|
677
|
+
*/
|
|
678
|
+
const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
|
|
679
|
+
const existing = yield* findBranchId(client, projectId, gitName);
|
|
680
|
+
if (existing !== void 0) return existing;
|
|
681
|
+
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
|
|
682
|
+
appName,
|
|
683
|
+
stage: gitName
|
|
684
|
+
}));
|
|
685
|
+
return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
|
|
686
|
+
params: { path: { projectId } },
|
|
687
|
+
body: { gitName }
|
|
688
|
+
})).pipe(Effect.map((r) => r.data.id), Effect.catch((err) => err.status === 409 ? findBranchId(client, projectId, gitName).pipe(Effect.flatMap((id) => id === void 0 ? Effect.fail(err) : Effect.succeed(id))) : Effect.fail(err)));
|
|
689
|
+
});
|
|
690
|
+
/**
|
|
691
|
+
* Resolves the two containers a stage's deploy runs into (ADR-0019): the
|
|
692
|
+
* app's **Project**, found-or-created by name, and — for a named stage
|
|
693
|
+
* only — its **Branch**, found-or-created by `gitName`. The default stage
|
|
694
|
+
* (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
|
|
695
|
+
* false` (`destroy`), nothing is created — an absent Project or Branch
|
|
696
|
+
* fails with `ContainerNotFoundError` instead.
|
|
697
|
+
*/
|
|
698
|
+
const resolveContainer = (opts) => Effect.gen(function* () {
|
|
699
|
+
const client = yield* ManagementClient;
|
|
700
|
+
const ensure = opts.ensure ?? true;
|
|
701
|
+
const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
|
|
702
|
+
if (opts.stage === void 0) return { projectId };
|
|
703
|
+
return {
|
|
704
|
+
projectId,
|
|
705
|
+
branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
|
|
706
|
+
};
|
|
707
|
+
});
|
|
708
|
+
/**
|
|
709
|
+
* Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
|
|
710
|
+
* the Branch still has live members or is the production/default Branch —
|
|
711
|
+
* that surfaces as a `PrismaApiError`.
|
|
712
|
+
*/
|
|
713
|
+
const deleteBranch = (branchId) => Effect.gen(function* () {
|
|
714
|
+
const client = yield* ManagementClient;
|
|
715
|
+
yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
|
|
716
|
+
});
|
|
717
|
+
/**
|
|
718
|
+
* Deletes a Project. Tolerates a 404 (already gone). The API refuses with a
|
|
719
|
+
* 400 if the Project still has live dependencies (e.g. another stage's
|
|
720
|
+
* Branch/resources) — that surfaces as a `PrismaApiError`.
|
|
721
|
+
*/
|
|
722
|
+
const deleteProject = (projectId) => Effect.gen(function* () {
|
|
723
|
+
const client = yield* ManagementClient;
|
|
724
|
+
yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: projectId } } }));
|
|
725
|
+
});
|
|
520
726
|
/** The collection of Prisma resource providers. */
|
|
521
727
|
var Providers = class extends Provider.ProviderCollection()("Prisma") {};
|
|
522
728
|
/**
|
|
@@ -530,10 +736,101 @@ const providers = () => Layer.effect(Providers, Provider.collection([
|
|
|
530
736
|
Connection,
|
|
531
737
|
ComputeService,
|
|
532
738
|
Deployment,
|
|
533
|
-
EnvironmentVariable
|
|
534
|
-
|
|
739
|
+
EnvironmentVariable,
|
|
740
|
+
Bucket,
|
|
741
|
+
BucketKey
|
|
742
|
+
])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DatabaseProvider(), ConnectionProvider(), ComputeServiceProvider(), DeploymentProvider(), EnvironmentVariableProvider(), BucketProvider(), BucketKeyProvider())), Layer.provideMerge(layer()), Layer.provideMerge(fromEnv()), Layer.orDie);
|
|
535
743
|
//#endregion
|
|
536
|
-
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/state
|
|
744
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/state.mjs
|
|
745
|
+
/** The framework-owned database a stage's deploy state lives in — a child of that stage's Branch (ADR-0034). */
|
|
746
|
+
const STATE_DATABASE_NAME = "prisma-composer-state";
|
|
747
|
+
/** Every connection created against a state database carries this prefix — see `cleanupAgedConnections`. */
|
|
748
|
+
const CONNECTION_NAME_PREFIX = "prisma-composer-state-";
|
|
749
|
+
const listAllBranches = (client, projectId) => Effect.gen(function* () {
|
|
750
|
+
const branches = [];
|
|
751
|
+
let cursor;
|
|
752
|
+
for (;;) {
|
|
753
|
+
const query = cursor === void 0 ? {} : { cursor };
|
|
754
|
+
const page = yield* call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
755
|
+
path: { projectId },
|
|
756
|
+
query
|
|
757
|
+
} }));
|
|
758
|
+
branches.push(...page.data);
|
|
759
|
+
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
760
|
+
cursor = page.pagination.nextCursor;
|
|
761
|
+
}
|
|
762
|
+
return branches;
|
|
763
|
+
});
|
|
764
|
+
/**
|
|
765
|
+
* The project's implicit default Branch — every live Project owns exactly
|
|
766
|
+
* one (a platform invariant). The list endpoint has no `isDefault` filter, so
|
|
767
|
+
* this pages through every Branch and picks it out client-side. Never creates
|
|
768
|
+
* one: its absence means the platform's invariant is broken, which is not
|
|
769
|
+
* something a deploy can repair.
|
|
770
|
+
*/
|
|
771
|
+
const resolveDefaultBranchId = (client, projectId) => Effect.gen(function* () {
|
|
772
|
+
const found = (yield* listAllBranches(client, projectId)).find((b) => b.isDefault);
|
|
773
|
+
if (found !== void 0) return found.id;
|
|
774
|
+
return yield* Effect.fail(new PrismaApiError({
|
|
775
|
+
status: 0,
|
|
776
|
+
message: `project ${projectId} has no default Branch — the platform guarantees every live Project owns one; contact support.`
|
|
777
|
+
}));
|
|
778
|
+
});
|
|
779
|
+
/** A named stage carries its `branchId`; production omits it, and its state lives on the Project's default Branch. */
|
|
780
|
+
const resolveBranchId = (client, container) => container.branchId !== void 0 ? Effect.succeed(container.branchId) : resolveDefaultBranchId(client, container.projectId);
|
|
781
|
+
/**
|
|
782
|
+
* Every database on this Branch. Uses the flat `GET /v1/databases`, which
|
|
783
|
+
* accepts `projectId` and `branchId` together — the project-scoped listing
|
|
784
|
+
* has no branch filter at all.
|
|
785
|
+
*/
|
|
786
|
+
const listAllDatabasesOnBranch = (client, projectId, branchId) => Effect.gen(function* () {
|
|
787
|
+
const databases = [];
|
|
788
|
+
let cursor;
|
|
789
|
+
for (;;) {
|
|
790
|
+
const query = cursor === void 0 ? {
|
|
791
|
+
projectId,
|
|
792
|
+
branchId
|
|
793
|
+
} : {
|
|
794
|
+
projectId,
|
|
795
|
+
branchId,
|
|
796
|
+
cursor
|
|
797
|
+
};
|
|
798
|
+
const page = yield* call(() => client.GET("/v1/databases", { params: { query } }));
|
|
799
|
+
databases.push(...page.data);
|
|
800
|
+
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
801
|
+
cursor = page.pagination.nextCursor;
|
|
802
|
+
}
|
|
803
|
+
return databases;
|
|
804
|
+
});
|
|
805
|
+
/**
|
|
806
|
+
* Databases on this Branch named `prisma-composer-state`, oldest first,
|
|
807
|
+
* excluding the Branch's own default database. The default database is always
|
|
808
|
+
* the user's, never ours to adopt or delete. A name match alone proves
|
|
809
|
+
* nothing — the platform allows duplicate names — so every caller must still
|
|
810
|
+
* verify ownership before acting on a candidate.
|
|
811
|
+
*/
|
|
812
|
+
const listStateDatabaseCandidates = (client, projectId, branchId) => listAllDatabasesOnBranch(client, projectId, branchId).pipe(Effect.map((databases) => databases.filter((d) => d.name === "prisma-composer-state" && !d.isDefault).sort((a, b) => a.createdAt.localeCompare(b.createdAt))));
|
|
813
|
+
/**
|
|
814
|
+
* Creates a fresh Postgres connection and reads its connection string. Reads
|
|
815
|
+
* `endpoints.direct.connectionString` only — never `endpoints.pooled`, and
|
|
816
|
+
* never the deprecated top-level `connectionString`/`url` (PRO-212), neither
|
|
817
|
+
* of which the platform guarantees.
|
|
818
|
+
*
|
|
819
|
+
* The connection string is returned only when the connection is created and
|
|
820
|
+
* cannot be read back afterwards, which is why every run creates a fresh
|
|
821
|
+
* connection instead of reusing one.
|
|
822
|
+
*/
|
|
823
|
+
const createConnection = (client, databaseId) => call(() => client.POST("/v1/databases/{databaseId}/connections", {
|
|
824
|
+
params: { path: { databaseId } },
|
|
825
|
+
body: { name: `${CONNECTION_NAME_PREFIX}${Date.now()}` }
|
|
826
|
+
})).pipe(Effect.flatMap((r) => {
|
|
827
|
+
const created = r.data;
|
|
828
|
+
const dsn = created.endpoints.direct?.connectionString;
|
|
829
|
+
return dsn === void 0 ? Effect.fail(new PrismaApiError({
|
|
830
|
+
status: 0,
|
|
831
|
+
message: `connection ${created.id} returned no endpoints.direct.connectionString (PRO-212)`
|
|
832
|
+
})) : Effect.succeed(Redacted.make(dsn));
|
|
833
|
+
}));
|
|
537
834
|
/** Collapses any thrown value (postgres.js failures included) into a {@link StateStoreError}. */
|
|
538
835
|
const toStateStoreError = (cause) => cause instanceof Error ? new StateStoreError({
|
|
539
836
|
message: cause.message,
|
|
@@ -541,13 +838,13 @@ const toStateStoreError = (cause) => cause instanceof Error ? new StateStoreErro
|
|
|
541
838
|
}) : new StateStoreError({ message: String(cause) });
|
|
542
839
|
/**
|
|
543
840
|
* An operator-facing failure from the hosted-state bootstrap pipeline
|
|
544
|
-
* (
|
|
841
|
+
* (branch/database discovery, connection creation, schema migration, or lock
|
|
545
842
|
* acquisition) — what a deployer actually sees, instead of a raw Effect
|
|
546
843
|
* defect.
|
|
547
844
|
*/
|
|
548
845
|
var HostedStateBootstrapError = class extends Data.TaggedError("HostedStateBootstrapError") {
|
|
549
846
|
get message() {
|
|
550
|
-
return `hosted-state bootstrap failed
|
|
847
|
+
return `hosted-state bootstrap failed in ${this.container}: ${this.step} — ${this.reason}`;
|
|
551
848
|
}
|
|
552
849
|
};
|
|
553
850
|
/**
|
|
@@ -557,8 +854,8 @@ var HostedStateBootstrapError = class extends Data.TaggedError("HostedStateBoots
|
|
|
557
854
|
* omit the DSN or credentials, so only the extracted message text survives
|
|
558
855
|
* into the operator-facing error.
|
|
559
856
|
*/
|
|
560
|
-
const hostedStateBootstrapError = (
|
|
561
|
-
|
|
857
|
+
const hostedStateBootstrapError = (container, step, cause) => new HostedStateBootstrapError({
|
|
858
|
+
container,
|
|
562
859
|
step,
|
|
563
860
|
reason: cause instanceof Error ? cause.message : String(cause)
|
|
564
861
|
});
|
|
@@ -609,85 +906,30 @@ const migratePrismaState = (sql) => Effect.tryPromise({
|
|
|
609
906
|
},
|
|
610
907
|
catch: toStateStoreError
|
|
611
908
|
});
|
|
612
|
-
/**
|
|
613
|
-
* The workspace's dedicated project for hosted deploy state. A project is
|
|
614
|
-
* the closest expressible stand-in for "ambient platform infrastructure" —
|
|
615
|
-
* PDP has no workspace-level database, and the app's own project is
|
|
616
|
-
* circular (it doesn't exist before the first apply, and is itself tracked
|
|
617
|
-
* in the state it would have to host).
|
|
618
|
-
*/
|
|
619
|
-
const STATE_PROJECT_NAME = "prisma-composer-state";
|
|
620
|
-
/** Every connection this bootstrap mints carries this prefix — see `cleanupAgedConnections`. */
|
|
621
|
-
const CONNECTION_NAME_PREFIX = "prisma-composer-state-";
|
|
622
909
|
const CONNECTION_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
623
|
-
const DEFAULT_DATABASE_POLL_ATTEMPTS = 5;
|
|
624
|
-
const DEFAULT_DATABASE_POLL_DELAY = "500 millis";
|
|
625
|
-
const listAllProjects = (client) => Effect.gen(function* () {
|
|
626
|
-
const projects = [];
|
|
627
|
-
let cursor;
|
|
628
|
-
for (;;) {
|
|
629
|
-
const query = cursor === void 0 ? {} : { cursor };
|
|
630
|
-
const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
|
|
631
|
-
projects.push(...page.data);
|
|
632
|
-
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
633
|
-
cursor = page.pagination.nextCursor;
|
|
634
|
-
}
|
|
635
|
-
return projects;
|
|
636
|
-
});
|
|
637
910
|
/**
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
911
|
+
* Creates the state database on the stage's Branch, using the flat endpoint
|
|
912
|
+
* because it is the only one that accepts a `branchId`. The platform still
|
|
913
|
+
* creates the row on the project's default Branch and attaches it afterwards,
|
|
914
|
+
* so a failed attach can leave a database behind on the default Branch. It
|
|
915
|
+
* checks the Branch exists before creating, and this is one request instead of
|
|
916
|
+
* two, so that window is much narrower than attaching from here — but it is
|
|
917
|
+
* narrowed, not closed.
|
|
918
|
+
*
|
|
919
|
+
* `isDefault` is left at the API's `false` default: the state database must
|
|
920
|
+
* never be the stage's default database, which belongs to the user.
|
|
643
921
|
*/
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
} })).pipe(Effect.map((r) => r.data));
|
|
656
|
-
const listAllDatabases = (client, projectId) => Effect.gen(function* () {
|
|
657
|
-
const databases = [];
|
|
658
|
-
let cursor;
|
|
659
|
-
for (;;) {
|
|
660
|
-
const query = cursor === void 0 ? {} : { cursor };
|
|
661
|
-
const page = yield* call(() => client.GET("/v1/projects/{projectId}/databases", { params: {
|
|
662
|
-
path: { projectId },
|
|
663
|
-
query
|
|
664
|
-
} }));
|
|
665
|
-
databases.push(...page.data);
|
|
666
|
-
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
667
|
-
cursor = page.pagination.nextCursor;
|
|
668
|
-
}
|
|
669
|
-
return databases;
|
|
670
|
-
});
|
|
671
|
-
/**
|
|
672
|
-
* The project's default database — auto-provisioned at project creation.
|
|
673
|
-
* Never create a database here: a project already has exactly one default,
|
|
674
|
-
* and creating another 409s (FT-5220). Whether the default is listable in
|
|
675
|
-
* the same tick as the project-create response is not a documented
|
|
676
|
-
* contract, so a fresh project polls a few times with a short backoff
|
|
677
|
-
* before giving up — the observed live behaviour is synchronous, but this
|
|
678
|
-
* does not assume that holds on every run.
|
|
679
|
-
*/
|
|
680
|
-
const findDefaultDatabase = (client, projectId) => Effect.gen(function* () {
|
|
681
|
-
for (let attempt = 1; attempt <= DEFAULT_DATABASE_POLL_ATTEMPTS; attempt++) {
|
|
682
|
-
const found = (yield* listAllDatabases(client, projectId)).find((d) => d.isDefault);
|
|
683
|
-
if (found !== void 0) return found;
|
|
684
|
-
if (attempt < DEFAULT_DATABASE_POLL_ATTEMPTS) yield* Effect.sleep(DEFAULT_DATABASE_POLL_DELAY);
|
|
685
|
-
}
|
|
686
|
-
return yield* Effect.fail(new PrismaApiError({
|
|
687
|
-
status: 0,
|
|
688
|
-
message: `project ${projectId} (${STATE_PROJECT_NAME}) has no default database after ${DEFAULT_DATABASE_POLL_ATTEMPTS} attempts — it may still be provisioning; re-run the deploy.`
|
|
689
|
-
}));
|
|
690
|
-
});
|
|
922
|
+
const createStateDatabase = (client, projectId, branchId) => call(() => client.POST("/v1/databases", { body: {
|
|
923
|
+
projectId,
|
|
924
|
+
name: STATE_DATABASE_NAME,
|
|
925
|
+
region: "inherit",
|
|
926
|
+
branchId
|
|
927
|
+
} })).pipe(Effect.map((created) => ({
|
|
928
|
+
id: created.data.id,
|
|
929
|
+
name: created.data.name,
|
|
930
|
+
isDefault: created.data.isDefault,
|
|
931
|
+
createdAt: created.data.createdAt
|
|
932
|
+
})));
|
|
691
933
|
const listAllConnections = (client, databaseId) => Effect.gen(function* () {
|
|
692
934
|
const connections = [];
|
|
693
935
|
let cursor;
|
|
@@ -705,43 +947,24 @@ const listAllConnections = (client, databaseId) => Effect.gen(function* () {
|
|
|
705
947
|
});
|
|
706
948
|
const deleteConnection = (client, connectionId) => callVoid(() => client.DELETE("/v1/connections/{id}", { params: { path: { id: connectionId } } }));
|
|
707
949
|
/**
|
|
708
|
-
* Every deploy
|
|
709
|
-
* closes it, so the
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
* after).
|
|
950
|
+
* Every deploy creates a fresh connection (`createConnection`) and nothing ever
|
|
951
|
+
* closes it, so the state database otherwise accumulates one connection
|
|
952
|
+
* resource per run without bound. Best-effort, never blocks bootstrap: lists
|
|
953
|
+
* this database's connections, deletes the ones matching our naming pattern
|
|
954
|
+
* older than the age threshold, and swallows any failure (a transient API
|
|
955
|
+
* error here must never fail the deploy it's cleaning up after).
|
|
715
956
|
*/
|
|
716
957
|
const cleanupAgedConnections = (client, databaseId) => Effect.gen(function* () {
|
|
717
958
|
const connections = yield* listAllConnections(client, databaseId);
|
|
718
959
|
const cutoff = Date.now() - CONNECTION_MAX_AGE_MS;
|
|
719
|
-
const aged = connections.filter((c) => c.name.startsWith(
|
|
960
|
+
const aged = connections.filter((c) => c.name.startsWith("prisma-composer-state-") && Date.parse(c.createdAt) < cutoff);
|
|
720
961
|
yield* Effect.forEach(aged, (c) => deleteConnection(client, c.id), { discard: true });
|
|
721
962
|
}).pipe(Effect.ignore);
|
|
722
963
|
/**
|
|
723
|
-
*
|
|
724
|
-
* Never `endpoints.pooled`, the deprecated top-level `connectionString`/`url`
|
|
725
|
-
* (PRO-212) — those are not guaranteed by the platform. The DSN is
|
|
726
|
-
* write-only on read (a stored connection can't be re-read later), which is
|
|
727
|
-
* exactly why a fresh connection is minted every run instead of reusing one.
|
|
728
|
-
*/
|
|
729
|
-
const mintConnection = (client, databaseId) => call(() => client.POST("/v1/databases/{databaseId}/connections", {
|
|
730
|
-
params: { path: { databaseId } },
|
|
731
|
-
body: { name: `${CONNECTION_NAME_PREFIX}${Date.now()}` }
|
|
732
|
-
})).pipe(Effect.flatMap((r) => {
|
|
733
|
-
const created = r.data;
|
|
734
|
-
const dsn = created.endpoints.direct?.connectionString;
|
|
735
|
-
return dsn === void 0 ? Effect.fail(new PrismaApiError({
|
|
736
|
-
status: 0,
|
|
737
|
-
message: `connection ${created.id} returned no endpoints.direct.connectionString (PRO-212)`
|
|
738
|
-
})) : Effect.succeed(Redacted.make(dsn));
|
|
739
|
-
}));
|
|
740
|
-
/**
|
|
741
|
-
* PDP allows duplicate project names, so a project named `prisma-composer-state`
|
|
964
|
+
* PDP allows duplicate database names, so a database named `prisma-composer-state`
|
|
742
965
|
* found by listing is not proof it's ours — it could be an unrelated
|
|
743
|
-
*
|
|
744
|
-
* Connects to the candidate
|
|
966
|
+
* database that happens to share the name (a squatter, deliberate or not).
|
|
967
|
+
* Connects to the candidate and inspects its tables:
|
|
745
968
|
*
|
|
746
969
|
* - our marker table with our marker row present → `ours`, adopt outright.
|
|
747
970
|
* - our state tables (`alchemy_resource_state`/`alchemy_stack_output`) but no
|
|
@@ -749,7 +972,7 @@ const mintConnection = (client, databaseId) => call(() => client.POST("/v1/datab
|
|
|
749
972
|
* The real, currently-in-use workspace state is in this shape today, so it
|
|
750
973
|
* must keep working — adopt it, and `migratePrismaState` (idempotent)
|
|
751
974
|
* writes the marker on the way in.
|
|
752
|
-
* - no tables at all → `empty`, a freshly-created
|
|
975
|
+
* - no tables at all → `empty`, a freshly-created database — adopt.
|
|
753
976
|
* - anything else → `squatter`: foreign data occupies the name; refuse it.
|
|
754
977
|
*/
|
|
755
978
|
const verifyOwnership = (connectionString) => Effect.tryPromise({
|
|
@@ -784,76 +1007,124 @@ const verifyOwnership = (connectionString) => Effect.tryPromise({
|
|
|
784
1007
|
})
|
|
785
1008
|
});
|
|
786
1009
|
/**
|
|
787
|
-
* Finds the
|
|
788
|
-
* than trusting the name alone (PDP allows duplicate names — see
|
|
789
|
-
* `verifyOwnership`).
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const resolveStateProject = (client, workspaceId, verify) => Effect.gen(function* () {
|
|
800
|
-
const candidates = yield* listStateProjects(client, workspaceId);
|
|
1010
|
+
* Finds the Branch's `prisma-composer-state` database, verifying ownership
|
|
1011
|
+
* rather than trusting the name alone (PDP allows duplicate names — see
|
|
1012
|
+
* `verifyOwnership`). Finding none creates one, with nothing to verify: only
|
|
1013
|
+
* this run can have touched a database it just created.
|
|
1014
|
+
*
|
|
1015
|
+
* Candidates are tried oldest first, so repeated runs pick the same one. The
|
|
1016
|
+
* first that verifies as ours is used. A candidate that fails verification is
|
|
1017
|
+
* skipped. If every candidate fails, bootstrap fails and names each rejected
|
|
1018
|
+
* database id, so an operator knows which to rename or remove.
|
|
1019
|
+
*/
|
|
1020
|
+
const resolveStateDatabase = (client, projectId, branchId, verify) => Effect.gen(function* () {
|
|
1021
|
+
const candidates = yield* listStateDatabaseCandidates(client, projectId, branchId);
|
|
801
1022
|
if (candidates.length === 0) {
|
|
802
|
-
const
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
console.error(`hosted state: provisioned new state project ${project.id} (db ${database.id}) in workspace ${workspaceId}`);
|
|
1023
|
+
const database = yield* createStateDatabase(client, projectId, branchId);
|
|
1024
|
+
const connectionString = yield* createConnection(client, database.id);
|
|
1025
|
+
console.error(`hosted state: provisioned state database ${database.id} on branch ${branchId} (project ${projectId})`);
|
|
806
1026
|
return {
|
|
807
|
-
project,
|
|
808
1027
|
database,
|
|
809
1028
|
connectionString
|
|
810
1029
|
};
|
|
811
1030
|
}
|
|
812
|
-
const sorted = [...candidates].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
813
1031
|
const rejected = [];
|
|
814
|
-
for (const candidate of
|
|
815
|
-
const
|
|
816
|
-
const connectionString = yield* mintConnection(client, database.id);
|
|
1032
|
+
for (const candidate of candidates) {
|
|
1033
|
+
const connectionString = yield* createConnection(client, candidate.id);
|
|
817
1034
|
const verdict = yield* verify(connectionString);
|
|
818
1035
|
if (verdict.kind === "squatter") {
|
|
819
|
-
rejected.push(`${candidate.id} (foreign tables: ${verdict.tables.join(", ")
|
|
1036
|
+
rejected.push(`${candidate.id} (foreign tables: ${verdict.tables.join(", ")})`);
|
|
820
1037
|
continue;
|
|
821
1038
|
}
|
|
822
|
-
console.error(`hosted state: using state
|
|
1039
|
+
console.error(`hosted state: using state database ${candidate.id} on branch ${branchId} (${verdict.kind}) — ${candidates.length} candidate(s) named ${STATE_DATABASE_NAME}`);
|
|
823
1040
|
return {
|
|
824
|
-
|
|
825
|
-
database,
|
|
1041
|
+
database: candidate,
|
|
826
1042
|
connectionString
|
|
827
1043
|
};
|
|
828
1044
|
}
|
|
829
1045
|
return yield* Effect.fail(new PrismaApiError({
|
|
830
1046
|
status: 0,
|
|
831
|
-
message: `found ${
|
|
1047
|
+
message: `found ${candidates.length} database(s) named "${STATE_DATABASE_NAME}" on branch ${branchId}, but none verified as Composer's state store: ${rejected.join("; ")}. Rename or remove the offending database(s).`
|
|
832
1048
|
}));
|
|
833
1049
|
});
|
|
834
1050
|
/**
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
1051
|
+
* Resolves the stage's Branch, find-or-creates its `prisma-composer-state`
|
|
1052
|
+
* database, and creates a fresh connection — the automatic bootstrap every
|
|
1053
|
+
* deploy runs once, needing nothing beyond the service token and the
|
|
1054
|
+
* Project/Branch ids the CLI already resolved.
|
|
839
1055
|
*/
|
|
840
|
-
const bootstrapStateConnection = (
|
|
1056
|
+
const bootstrapStateConnection = (container) => bootstrapStateConnectionWith(container, verifyOwnership);
|
|
841
1057
|
/**
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* connection to them.
|
|
1058
|
+
* Identical to {@link bootstrapStateConnection}, except the ownership check is
|
|
1059
|
+
* a parameter so `bootstrap.test.ts` can supply a fake instead of opening a
|
|
1060
|
+
* real Postgres connection.
|
|
846
1061
|
*/
|
|
847
|
-
const bootstrapStateConnectionWith = (
|
|
1062
|
+
const bootstrapStateConnectionWith = (container, verify) => Effect.gen(function* () {
|
|
848
1063
|
const client = yield* ManagementClient;
|
|
849
|
-
const
|
|
1064
|
+
const branchId = yield* resolveBranchId(client, container);
|
|
1065
|
+
const { database, connectionString } = yield* resolveStateDatabase(client, container.projectId, branchId, verify);
|
|
850
1066
|
yield* cleanupAgedConnections(client, database.id);
|
|
851
1067
|
return {
|
|
852
|
-
projectId:
|
|
1068
|
+
projectId: container.projectId,
|
|
853
1069
|
databaseId: database.id,
|
|
854
1070
|
connectionString
|
|
855
1071
|
};
|
|
856
1072
|
});
|
|
1073
|
+
const deleteDatabase = (client, databaseId) => callVoid(() => client.DELETE("/v1/databases/{databaseId}", { params: { path: { databaseId } } }));
|
|
1074
|
+
/**
|
|
1075
|
+
* Identical to {@link deleteStateDatabase}, except the ownership check is a
|
|
1076
|
+
* parameter so tests can supply a fake instead of opening a real Postgres
|
|
1077
|
+
* connection.
|
|
1078
|
+
*/
|
|
1079
|
+
const deleteStateDatabaseWith = (container, verify) => Effect.gen(function* () {
|
|
1080
|
+
const client = yield* ManagementClient;
|
|
1081
|
+
const branchId = yield* resolveBranchId(client, container);
|
|
1082
|
+
const candidates = yield* listStateDatabaseCandidates(client, container.projectId, branchId);
|
|
1083
|
+
for (const candidate of candidates) {
|
|
1084
|
+
const verdict = yield* verify(yield* createConnection(client, candidate.id));
|
|
1085
|
+
if (verdict.kind === "squatter") {
|
|
1086
|
+
console.warn(`hosted state: left database ${candidate.id} on branch ${branchId} alone — it is named ${STATE_DATABASE_NAME} but holds unrelated data (tables: ${verdict.tables.join(", ")}).`);
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
yield* deleteDatabase(client, candidate.id);
|
|
1090
|
+
console.error(`hosted state: removed state database ${candidate.id} from branch ${branchId}`);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
/** Codes for "can't reach the host yet" — DNS/refusal, not a mid-session drop. */
|
|
1094
|
+
const ESTABLISHMENT_CODES = /* @__PURE__ */ new Set([
|
|
1095
|
+
"ECONNREFUSED",
|
|
1096
|
+
"ENOTFOUND",
|
|
1097
|
+
"EAI_AGAIN"
|
|
1098
|
+
]);
|
|
1099
|
+
/** Connection-establishment failure messages (no useful `err.code`). `upstream database` is PPg's edge proxy while a cold/idle upstream warms up. */
|
|
1100
|
+
const ESTABLISHMENT_MESSAGE_FRAGMENTS = ["upstream database", "connection refused"];
|
|
1101
|
+
/**
|
|
1102
|
+
* Whether a failure is a connection-establishment error against a warming
|
|
1103
|
+
* upstream (retry), as opposed to a lost lease, a mid-session drop, or a real
|
|
1104
|
+
* query error (all of which must surface at once). Unwraps a
|
|
1105
|
+
* {@link StateStoreError} to its driver `cause` so a code-only error still
|
|
1106
|
+
* classifies.
|
|
1107
|
+
*/
|
|
1108
|
+
const isColdStartConnectError = (error) => {
|
|
1109
|
+
if (typeof error !== "object" || error === null) return false;
|
|
1110
|
+
const code = "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
1111
|
+
if (code !== void 0 && ESTABLISHMENT_CODES.has(code)) return true;
|
|
1112
|
+
const message = "message" in error && typeof error.message === "string" ? error.message.toLowerCase() : "";
|
|
1113
|
+
if (ESTABLISHMENT_MESSAGE_FRAGMENTS.some((fragment) => message.includes(fragment))) return true;
|
|
1114
|
+
const cause = "cause" in error ? error.cause : void 0;
|
|
1115
|
+
return cause !== void 0 && cause !== error && isColdStartConnectError(cause);
|
|
1116
|
+
};
|
|
1117
|
+
/** The same ~2-minute budget the bootstrap migration uses (`layer.ts`): retry every 5s, up to 2 minutes. */
|
|
1118
|
+
const COLD_START_SCHEDULE = Schedule.both(Schedule.spaced("5 seconds"), Schedule.during("2 minutes"));
|
|
1119
|
+
/**
|
|
1120
|
+
* Retries a state operation past a cold-start connection rejection only; every
|
|
1121
|
+
* other failure surfaces immediately. `schedule` is injectable so tests drive
|
|
1122
|
+
* it without real delay.
|
|
1123
|
+
*/
|
|
1124
|
+
const retryColdStart = (operation, schedule = COLD_START_SCHEDULE) => Effect.retry(operation, {
|
|
1125
|
+
while: isColdStartConnectError,
|
|
1126
|
+
schedule
|
|
1127
|
+
});
|
|
857
1128
|
/** Another deploy already holds the lock for this stack/stage. Never queued — fails immediately. */
|
|
858
1129
|
var StateLockContentionError = class extends Data.TaggedError("StateLockContentionError") {
|
|
859
1130
|
get message() {
|
|
@@ -894,22 +1165,22 @@ const acquireStateLock = (sql, stack, stage) => Effect.gen(function* () {
|
|
|
894
1165
|
}));
|
|
895
1166
|
}
|
|
896
1167
|
const lockPid = acquired.pid;
|
|
897
|
-
const checkLive = Effect.tryPromise({
|
|
1168
|
+
const checkLive = retryColdStart(Effect.tryPromise({
|
|
898
1169
|
try: async () => {
|
|
899
1170
|
return (await sql`
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1171
|
+
select exists (
|
|
1172
|
+
select 1 from pg_locks
|
|
1173
|
+
where locktype = 'advisory'
|
|
1174
|
+
and pid = ${lockPid}
|
|
1175
|
+
and objsubid = 1
|
|
1176
|
+
and granted
|
|
1177
|
+
and ((classid::bigint << 32) | (objid::bigint & 4294967295))
|
|
1178
|
+
= hashtextextended(${key}, 0)
|
|
1179
|
+
) as live
|
|
1180
|
+
`)[0]?.live ?? false;
|
|
910
1181
|
},
|
|
911
1182
|
catch: toStateStoreError
|
|
912
|
-
}).pipe(Effect.flatMap((live) => live ? Effect.void : Effect.fail(new StateStoreError({ message: `the state lock for ${stack}/${stage} was lost mid-run; refusing to continue unlocked` }))));
|
|
1183
|
+
}).pipe(Effect.flatMap((live) => live ? Effect.void : Effect.fail(new StateStoreError({ message: `the state lock for ${stack}/${stage} was lost mid-run; refusing to continue unlocked` })))));
|
|
913
1184
|
const release = async () => {
|
|
914
1185
|
try {
|
|
915
1186
|
await reserved`select pg_advisory_unlock(hashtextextended(${key}, 0))`;
|
|
@@ -922,10 +1193,10 @@ const acquireStateLock = (sql, stack, stage) => Effect.gen(function* () {
|
|
|
922
1193
|
release
|
|
923
1194
|
};
|
|
924
1195
|
});
|
|
925
|
-
const attempt = (f) => Effect.tryPromise({
|
|
1196
|
+
const attempt = (f) => retryColdStart(Effect.tryPromise({
|
|
926
1197
|
try: f,
|
|
927
1198
|
catch: toStateStoreError
|
|
928
|
-
});
|
|
1199
|
+
}));
|
|
929
1200
|
/**
|
|
930
1201
|
* Wraps an already-`encodeState`d value as a jsonb-typed bind parameter.
|
|
931
1202
|
* Must go through `sql.json(...)` (not `JSON.stringify(...)::jsonb`) —
|
|
@@ -1093,29 +1364,32 @@ const guardStateService = (service, checkLive, now = Date.now) => {
|
|
|
1093
1364
|
};
|
|
1094
1365
|
/**
|
|
1095
1366
|
* The hosted Alchemy state store. On layer init (scoped, once per stack
|
|
1096
|
-
* run): find-or-create
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1367
|
+
* run): resolve the stage's Branch, find-or-create its `prisma-composer-state`
|
|
1368
|
+
* database, create a fresh connection, migrate the schema, and acquire the
|
|
1369
|
+
* (stack, stage) advisory lock — see `bootstrap.ts` and `lock.ts`. The
|
|
1370
|
+
* Management API plumbing (`ManagementClient`, `PrismaCredentials`) is
|
|
1371
|
+
* provided internally, so the returned layer's only requirements are the
|
|
1372
|
+
* ones alchemy itself already provides to every state store
|
|
1373
|
+
* (`StackServices`).
|
|
1103
1374
|
*
|
|
1104
1375
|
* Any bootstrap/lock/migration failure is wrapped into an operator-facing
|
|
1105
|
-
* `HostedStateBootstrapError` (naming the
|
|
1376
|
+
* `HostedStateBootstrapError` (naming the Project/Branch and the step that
|
|
1106
1377
|
* failed, never the raw driver/API error — see `errors.ts`) before dying the
|
|
1107
1378
|
* layer (loud, immediate, unrecoverable) rather than surfacing as a typed
|
|
1108
1379
|
* error — matching core's `LowerOptions.state: Layer.Layer<State, never,
|
|
1109
1380
|
* StackServices>` contract and alchemy's own convention (e.g. a missing
|
|
1110
1381
|
* state store is `Effect.die` in `Stack.make`).
|
|
1111
1382
|
*/
|
|
1112
|
-
const
|
|
1113
|
-
const
|
|
1114
|
-
if (workspaceId === void 0 || workspaceId.length === 0) throw new Error("prismaState(): environment variable PRISMA_WORKSPACE_ID is required.");
|
|
1383
|
+
const prismaStateLayer = (ids) => {
|
|
1384
|
+
const { projectId, branchId } = ids;
|
|
1115
1385
|
return Layer.effect(State, Effect.gen(function* () {
|
|
1116
1386
|
const stack = yield* Stack;
|
|
1117
|
-
const
|
|
1118
|
-
const
|
|
1387
|
+
const container = branchId === void 0 ? projectId : `${projectId}/${branchId}`;
|
|
1388
|
+
const bootstrapError = (step) => (cause) => hostedStateBootstrapError(container, step, cause);
|
|
1389
|
+
const { connectionString } = yield* bootstrapStateConnection(branchId === void 0 ? { projectId } : {
|
|
1390
|
+
projectId,
|
|
1391
|
+
branchId
|
|
1392
|
+
}).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))), Effect.mapError(bootstrapError("resolving the state database on the stage branch")));
|
|
1119
1393
|
const sql = postgres(Redacted.value(connectionString), {
|
|
1120
1394
|
max: 5,
|
|
1121
1395
|
onnotice: () => {}
|
|
@@ -1130,41 +1404,262 @@ const prismaState = (opts = {}) => {
|
|
|
1130
1404
|
};
|
|
1131
1405
|
//#endregion
|
|
1132
1406
|
//#region ../../1-prisma-cloud/1-extensions/target/dist/control.mjs
|
|
1407
|
+
const PRISMA_CLOUD_EXTENSION_ID = "@prisma/composer-prisma-cloud";
|
|
1408
|
+
var PrismaCloudContainer = class {
|
|
1409
|
+
input;
|
|
1410
|
+
projectId;
|
|
1411
|
+
branchId;
|
|
1412
|
+
constructor(input, projectId, branchId) {
|
|
1413
|
+
this.input = input;
|
|
1414
|
+
this.projectId = projectId;
|
|
1415
|
+
this.branchId = branchId;
|
|
1416
|
+
}
|
|
1417
|
+
serialize() {
|
|
1418
|
+
return JSON.stringify({
|
|
1419
|
+
input: this.input,
|
|
1420
|
+
projectId: this.projectId,
|
|
1421
|
+
...this.branchId !== void 0 ? { branchId: this.branchId } : {}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
/** `instanceof` — parent-side instances and child-side deserialized instances are both constructed by this module. */
|
|
1426
|
+
function isPrismaCloudContainer(value) {
|
|
1427
|
+
return value instanceof PrismaCloudContainer;
|
|
1428
|
+
}
|
|
1429
|
+
/** Narrow-or-throw for hook inputs. */
|
|
1430
|
+
function prismaCloudContainerOf(value) {
|
|
1431
|
+
if (!isPrismaCloudContainer(value)) throw new Error("the Prisma Cloud container was not resolved — the extension's container descriptor did not run.");
|
|
1432
|
+
return value;
|
|
1433
|
+
}
|
|
1434
|
+
function isRecord(value) {
|
|
1435
|
+
return typeof value === "object" && value !== null;
|
|
1436
|
+
}
|
|
1437
|
+
function invalidPayloadError(reason) {
|
|
1438
|
+
return /* @__PURE__ */ new Error(`${PRISMA_CLOUD_EXTENSION_ID}: invalid container transport payload — ${reason}.`);
|
|
1439
|
+
}
|
|
1440
|
+
/** Reconstructs a `PrismaCloudContainer` from `serialize()`'s JSON output — real narrowing, no casts. */
|
|
1441
|
+
function deserialize(serialized) {
|
|
1442
|
+
let parsed;
|
|
1443
|
+
try {
|
|
1444
|
+
parsed = JSON.parse(serialized);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
throw invalidPayloadError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
1447
|
+
}
|
|
1448
|
+
if (!isRecord(parsed)) throw invalidPayloadError("not an object");
|
|
1449
|
+
const input = parsed["input"];
|
|
1450
|
+
if (!isRecord(input)) throw invalidPayloadError("\"input\" is not an object");
|
|
1451
|
+
const appName = input["appName"];
|
|
1452
|
+
if (typeof appName !== "string") throw invalidPayloadError("\"input.appName\" is not a string");
|
|
1453
|
+
const stage = input["stage"];
|
|
1454
|
+
if (stage !== void 0 && typeof stage !== "string") throw invalidPayloadError("\"input.stage\" is not a string or absent");
|
|
1455
|
+
const projectId = parsed["projectId"];
|
|
1456
|
+
if (typeof projectId !== "string") throw invalidPayloadError("\"projectId\" is not a string");
|
|
1457
|
+
const branchId = parsed["branchId"];
|
|
1458
|
+
if (branchId !== void 0 && typeof branchId !== "string") throw invalidPayloadError("\"branchId\" is not a string or absent");
|
|
1459
|
+
return new PrismaCloudContainer({
|
|
1460
|
+
appName,
|
|
1461
|
+
stage
|
|
1462
|
+
}, projectId, branchId);
|
|
1463
|
+
}
|
|
1464
|
+
const workspaceRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_WORKSPACE_ID is required.");
|
|
1465
|
+
const tokenRequiredError$2 = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required.");
|
|
1466
|
+
function requireWorkspaceId() {
|
|
1467
|
+
const workspaceId = process.env["PRISMA_WORKSPACE_ID"];
|
|
1468
|
+
if (workspaceId === void 0 || workspaceId.length === 0) throw workspaceRequiredError();
|
|
1469
|
+
return workspaceId;
|
|
1470
|
+
}
|
|
1471
|
+
function requireTokenUnlessInjected(deps) {
|
|
1472
|
+
if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError$2();
|
|
1473
|
+
}
|
|
1474
|
+
async function ensureContainer(input, deps) {
|
|
1475
|
+
const workspaceId = requireWorkspaceId();
|
|
1476
|
+
requireTokenUnlessInjected(deps);
|
|
1477
|
+
const program = resolveContainer({
|
|
1478
|
+
workspaceId,
|
|
1479
|
+
appName: input.appName,
|
|
1480
|
+
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
1481
|
+
ensure: true
|
|
1482
|
+
}).pipe(Effect.map((c) => ({
|
|
1483
|
+
ok: true,
|
|
1484
|
+
container: c
|
|
1485
|
+
})), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
1486
|
+
ok: false,
|
|
1487
|
+
message: `Prisma Management API error resolving containers: ${e.message}.`
|
|
1488
|
+
})));
|
|
1489
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1490
|
+
const outcome = await Effect.runPromise(provided);
|
|
1491
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
1492
|
+
return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
|
|
1493
|
+
}
|
|
1494
|
+
async function locateContainer(input, deps) {
|
|
1495
|
+
const workspaceId = requireWorkspaceId();
|
|
1496
|
+
requireTokenUnlessInjected(deps);
|
|
1497
|
+
const program = resolveContainer({
|
|
1498
|
+
workspaceId,
|
|
1499
|
+
appName: input.appName,
|
|
1500
|
+
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
1501
|
+
ensure: false
|
|
1502
|
+
}).pipe(Effect.map((c) => ({
|
|
1503
|
+
ok: true,
|
|
1504
|
+
container: c
|
|
1505
|
+
})), Effect.catchTag("ContainerNotFoundError", () => Effect.succeed({ ok: false })), Effect.catchTag("PrismaApiError", (e) => Effect.fail(/* @__PURE__ */ new Error(`Prisma Management API error resolving containers: ${e.message}.`))));
|
|
1506
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1507
|
+
const outcome = await Effect.runPromise(provided);
|
|
1508
|
+
if (!outcome.ok) return void 0;
|
|
1509
|
+
return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Soft-deletes a named stage's Branch after a successful `alchemy destroy`
|
|
1513
|
+
* has removed its members — the Management API refuses to delete a Branch
|
|
1514
|
+
* that still has live members.
|
|
1515
|
+
*/
|
|
1516
|
+
async function removeStageBranch(branchId, deps) {
|
|
1517
|
+
requireTokenUnlessInjected(deps);
|
|
1518
|
+
const program = deleteBranch(branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
1519
|
+
ok: false,
|
|
1520
|
+
message: `Failed to delete the stage Branch: ${e.message}.`
|
|
1521
|
+
})));
|
|
1522
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1523
|
+
const outcome = await Effect.runPromise(provided);
|
|
1524
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Best-effort cleanup after a successful `--production` destroy: removes
|
|
1528
|
+
* the app's Project so hand-run stacks don't accumulate as empty Projects
|
|
1529
|
+
* (they eventually hit the workspace's plan limit). Unlike `removeStageBranch`,
|
|
1530
|
+
* this never throws: the destroy itself already succeeded, and the API's own
|
|
1531
|
+
* 400 ("still has dependencies") is the only check that matters — failing
|
|
1532
|
+
* the command over a cleanup step would be worse than leaving a Project shell.
|
|
1533
|
+
*/
|
|
1534
|
+
async function removeAppProject(projectId, deps) {
|
|
1535
|
+
if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) {
|
|
1536
|
+
console.warn(`Skipped removing the Project (${projectId}): PRISMA_SERVICE_TOKEN is not set.`);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const program = deleteProject(projectId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
1540
|
+
ok: false,
|
|
1541
|
+
error: e
|
|
1542
|
+
})));
|
|
1543
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1544
|
+
const outcome = await Effect.runPromise(provided);
|
|
1545
|
+
if (outcome.ok) {
|
|
1546
|
+
console.log(`Removed the Project (${projectId}) — nothing was left in it.`);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (outcome.error.status === 400) {
|
|
1550
|
+
console.log(`Kept the Project (${projectId}) — it still has another stage's resources.`);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
console.warn(`Could not remove the Project (${projectId}) after destroy: ${outcome.error.message}.`);
|
|
1554
|
+
}
|
|
1555
|
+
function containerDescriptor(deps) {
|
|
1556
|
+
return {
|
|
1557
|
+
ensure: (input) => ensureContainer(input, deps),
|
|
1558
|
+
locate: (input) => locateContainer(input, deps),
|
|
1559
|
+
remove: (instance) => instance.input.stage !== void 0 ? removeStageBranch(instance.branchId ?? missingBranchId(instance), deps) : removeAppProject(instance.projectId, deps),
|
|
1560
|
+
deserialize
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
/** Defensive: a named-stage container always resolves a Branch together with its stage — `ensure`/`locate`/`deserialize` never produce one without the other. */
|
|
1564
|
+
function missingBranchId(instance) {
|
|
1565
|
+
throw new Error(`${PRISMA_CLOUD_EXTENSION_ID}: a named-stage ("${instance.input.stage}") container instance is missing its branchId — this is a bug in ensure/locate/deserialize.`);
|
|
1566
|
+
}
|
|
1133
1567
|
const PRISMA_NAME_MIN = 3;
|
|
1134
1568
|
const PRISMA_NAME_MAX = 65;
|
|
1135
1569
|
function validateName(value, source) {
|
|
1136
1570
|
if (value.length < PRISMA_NAME_MIN || value.length > PRISMA_NAME_MAX) throw new Error(`prisma-cloud: ${source} "${value}" (${value.length} characters) is not a valid Prisma resource name — Prisma requires ${PRISMA_NAME_MIN}–${PRISMA_NAME_MAX} characters. Rename the provision id (or the deploy --name) to fit.`);
|
|
1137
1571
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1572
|
+
function isCloudApplication(value) {
|
|
1573
|
+
return typeof value === "object" && value !== null && "projectId" in value && typeof value.projectId === "string" && "branchId" in value && (value.branchId === void 0 || typeof value.branchId === "string");
|
|
1574
|
+
}
|
|
1575
|
+
/** Narrows `ctx.application`, which core hands over as `unknown`, to this extension's own product; throws naming the hook when it hasn't run. */
|
|
1576
|
+
function cloudApplicationOf(application) {
|
|
1577
|
+
if (!isCloudApplication(application)) throw new Error("prisma-cloud: ctx.application is not this extension's application product — the prismaCloud() application hook must run before any node lowers.");
|
|
1578
|
+
return application;
|
|
1579
|
+
}
|
|
1580
|
+
function projectIdOf(application) {
|
|
1581
|
+
return cloudApplicationOf(application).projectId;
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* One Bucket per module-provisioned bucket resource — `id` is the module
|
|
1585
|
+
* provision id, so a resource shared by several consumers is created exactly
|
|
1586
|
+
* once. A BucketKey is minted for the bucket: it is the reveal-once credential
|
|
1587
|
+
* carrier, and its attributes (endpoint, bucketName, accessKeyId,
|
|
1588
|
+
* secretAccessKey) become the four S3Config outputs consumers resolve by name.
|
|
1589
|
+
*/
|
|
1590
|
+
function bucketDescriptor(_o) {
|
|
1591
|
+
const lowering = ({ id, application }) => Effect.gen(function* () {
|
|
1592
|
+
validateName(id, "resource name (from provision id)");
|
|
1593
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1594
|
+
const bkt = yield* Bucket(`${id}-bucket`, {
|
|
1595
|
+
projectId: projectIdOf(application),
|
|
1596
|
+
name: id,
|
|
1597
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1598
|
+
});
|
|
1599
|
+
const key = yield* BucketKey(`${id}-key`, {
|
|
1600
|
+
bucketId: bkt.id,
|
|
1601
|
+
name: id,
|
|
1602
|
+
role: "read_write"
|
|
1603
|
+
});
|
|
1604
|
+
const secretAccessKey = Output.map(key.secretAccessKey, (v) => Redacted.value(v));
|
|
1605
|
+
return {
|
|
1606
|
+
outputs: {
|
|
1607
|
+
url: key.endpoint,
|
|
1608
|
+
bucket: key.bucketName,
|
|
1609
|
+
accessKeyId: key.accessKeyId,
|
|
1610
|
+
secretAccessKey
|
|
1611
|
+
},
|
|
1612
|
+
entities: [{
|
|
1613
|
+
kind: "bucket",
|
|
1614
|
+
id: bkt.id
|
|
1615
|
+
}]
|
|
1616
|
+
};
|
|
1617
|
+
});
|
|
1618
|
+
return Object.assign(lowering, { kind: "resource" });
|
|
1619
|
+
}
|
|
1620
|
+
/** The `compute` node kind's descriptor: the four service hooks — provision, serialize, package, deploy. */
|
|
1621
|
+
/**
|
|
1622
|
+
* Returns the PRECISE descriptor type, not the erased `NodeDescriptor`: the
|
|
1623
|
+
* registry in control.ts erases it on assignment anyway (method bivariance),
|
|
1624
|
+
* but s3-store composes over these hooks and needs their P/S to stay visible.
|
|
1625
|
+
* Annotating this `NodeDescriptor` would force s3-store to cast them back.
|
|
1626
|
+
*/
|
|
1140
1627
|
function computeDescriptor(o) {
|
|
1141
1628
|
return {
|
|
1142
1629
|
kind: "service",
|
|
1143
1630
|
provision: ({ id, application }) => Effect.gen(function* () {
|
|
1144
1631
|
validateName(id, "service name (from provision id)");
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1632
|
+
const projectId = projectIdOf(application);
|
|
1633
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1634
|
+
const svc = yield* ComputeService(`${id}-svc`, {
|
|
1635
|
+
projectId,
|
|
1636
|
+
name: id,
|
|
1637
|
+
region: o.region ?? "us-east-1",
|
|
1638
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1639
|
+
});
|
|
1640
|
+
return {
|
|
1641
|
+
serviceId: svc.id,
|
|
1642
|
+
projectId,
|
|
1643
|
+
endpointDomain: svc.endpointDomain
|
|
1644
|
+
};
|
|
1154
1645
|
}),
|
|
1155
|
-
serialize: (
|
|
1156
|
-
const
|
|
1157
|
-
const
|
|
1158
|
-
const
|
|
1646
|
+
serialize: (ctx, provisioned, config) => Effect.gen(function* () {
|
|
1647
|
+
const { address, node, graph } = ctx;
|
|
1648
|
+
const branchId = cloudApplicationOf(ctx.application).branchId;
|
|
1649
|
+
const cls = branchId ? "preview" : "production";
|
|
1650
|
+
const branch = branchId !== void 0 ? { branchId } : {};
|
|
1651
|
+
const projectId = provisioned.projectId;
|
|
1159
1652
|
const svc = node;
|
|
1160
1653
|
const records = [];
|
|
1161
1654
|
for (const d of paramEntries(svc)) {
|
|
1162
1655
|
const value = d.owner === "service" ? config.service[d.name] : config.inputs[d.owner.input]?.[d.name];
|
|
1656
|
+
if (value === void 0) continue;
|
|
1163
1657
|
const key = configKey(address, d);
|
|
1658
|
+
const rowValue = d.owner === "service" && isParamSource(value) ? encodeParamPointer(paramName(paramBindingFor(graph.params, address, d.name))) : encode(d.owner, value);
|
|
1164
1659
|
records.push(yield* EnvironmentVariable(`${key}-var`, {
|
|
1165
1660
|
projectId,
|
|
1166
1661
|
key,
|
|
1167
|
-
value:
|
|
1662
|
+
value: rowValue,
|
|
1168
1663
|
class: cls,
|
|
1169
1664
|
...branch
|
|
1170
1665
|
}));
|
|
@@ -1176,10 +1671,36 @@ function computeDescriptor(o) {
|
|
|
1176
1671
|
class: cls,
|
|
1177
1672
|
...branch
|
|
1178
1673
|
}));
|
|
1179
|
-
|
|
1674
|
+
const exposes = svc.expose !== void 0 && Object.keys(svc.expose).length > 0;
|
|
1675
|
+
const refsByBrand = /* @__PURE__ */ new Map();
|
|
1676
|
+
if (exposes) for (const edge of provisionedEdges(graph)) {
|
|
1677
|
+
if (edge.providerAddress !== address) continue;
|
|
1678
|
+
const ref = ctx.provisioned.get(edge.edgeId);
|
|
1679
|
+
if (ref === void 0) continue;
|
|
1680
|
+
const refs = refsByBrand.get(edge.brand) ?? [];
|
|
1681
|
+
refs.push(ref);
|
|
1682
|
+
refsByBrand.set(edge.brand, refs);
|
|
1683
|
+
}
|
|
1684
|
+
for (const [brand, entry] of o.providerParams) {
|
|
1685
|
+
const raw = "valueForService" in entry ? entry.valueForService(provisioned, address) : exposes ? entry.value(refsByBrand.get(brand) ?? []) : void 0;
|
|
1686
|
+
if (raw === void 0) continue;
|
|
1687
|
+
const key = configKey(address, {
|
|
1688
|
+
owner: "service",
|
|
1689
|
+
name: entry.name
|
|
1690
|
+
});
|
|
1691
|
+
const value = Output.isOutput(raw) ? Output.map(raw, (v) => encode("service", v)) : encode("service", raw);
|
|
1692
|
+
records.push(yield* EnvironmentVariable(`${key}-var`, {
|
|
1693
|
+
projectId,
|
|
1694
|
+
key,
|
|
1695
|
+
value,
|
|
1696
|
+
class: cls,
|
|
1697
|
+
...branch
|
|
1698
|
+
}));
|
|
1699
|
+
}
|
|
1700
|
+
return {
|
|
1180
1701
|
environment: records,
|
|
1181
1702
|
port: typeof config.service["port"] === "number" ? config.service["port"] : 3e3
|
|
1182
|
-
}
|
|
1703
|
+
};
|
|
1183
1704
|
}),
|
|
1184
1705
|
package: ({ id }, { assembled, address }) => Effect.try(() => packageComputeArtifact({
|
|
1185
1706
|
id,
|
|
@@ -1188,16 +1709,24 @@ function computeDescriptor(o) {
|
|
|
1188
1709
|
address
|
|
1189
1710
|
})),
|
|
1190
1711
|
deploy: ({ id }, provisioned, artifact, serialized) => Effect.gen(function* () {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1712
|
+
const deployment = yield* Deployment(`${id}-deploy`, {
|
|
1713
|
+
computeServiceId: provisioned.serviceId,
|
|
1714
|
+
artifactPath: artifact.path,
|
|
1715
|
+
artifactHash: artifact.sha256,
|
|
1716
|
+
environment: serialized.environment,
|
|
1717
|
+
port: serialized.port
|
|
1718
|
+
});
|
|
1719
|
+
return {
|
|
1720
|
+
outputs: {
|
|
1721
|
+
url: deployment.deployedUrl,
|
|
1722
|
+
projectId: provisioned.projectId
|
|
1723
|
+
},
|
|
1724
|
+
entities: [{
|
|
1725
|
+
kind: "compute-service",
|
|
1726
|
+
id: provisioned.serviceId,
|
|
1727
|
+
url: deployment.deployedUrl
|
|
1728
|
+
}]
|
|
1729
|
+
};
|
|
1201
1730
|
})
|
|
1202
1731
|
};
|
|
1203
1732
|
}
|
|
@@ -1257,18 +1786,25 @@ const PgWarmProvider = () => Provider.effect(PgWarm, Effect.succeed(pgWarmProvid
|
|
|
1257
1786
|
function postgresDescriptor(o) {
|
|
1258
1787
|
const lowering = ({ id, application }) => Effect.gen(function* () {
|
|
1259
1788
|
validateName(id, "resource name (from provision id)");
|
|
1789
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1260
1790
|
const db = yield* Database(`${id}-db`, {
|
|
1261
1791
|
projectId: projectIdOf(application),
|
|
1262
1792
|
name: id,
|
|
1263
1793
|
region: o.region ?? "us-east-1",
|
|
1264
|
-
...
|
|
1794
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1265
1795
|
});
|
|
1266
1796
|
const conn = yield* Connection(`${id}-conn`, {
|
|
1267
1797
|
databaseId: db.id,
|
|
1268
1798
|
name: id
|
|
1269
1799
|
});
|
|
1270
1800
|
const url = Output.map(conn.connectionString, (value) => Redacted.value(value));
|
|
1271
|
-
return {
|
|
1801
|
+
return {
|
|
1802
|
+
outputs: { url: (yield* PgWarm(`${id}-warm`, { url })).url },
|
|
1803
|
+
entities: [{
|
|
1804
|
+
kind: "postgres-database",
|
|
1805
|
+
id: db.id
|
|
1806
|
+
}]
|
|
1807
|
+
};
|
|
1272
1808
|
});
|
|
1273
1809
|
return Object.assign(lowering, { kind: "resource" });
|
|
1274
1810
|
}
|
|
@@ -1519,11 +2055,12 @@ const PnMigrationProvider = () => Provider.effect(PnMigration, Effect.succeed(pn
|
|
|
1519
2055
|
function prismaNextDescriptor(o) {
|
|
1520
2056
|
const lowering = ({ id, node, application }) => Effect.gen(function* () {
|
|
1521
2057
|
validateName(id, "resource name (from provision id)");
|
|
2058
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1522
2059
|
const db = yield* Database(`${id}-db`, {
|
|
1523
2060
|
projectId: projectIdOf(application),
|
|
1524
2061
|
name: id,
|
|
1525
2062
|
region: o.region ?? "us-east-1",
|
|
1526
|
-
...
|
|
2063
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1527
2064
|
});
|
|
1528
2065
|
const conn = yield* Connection(`${id}-conn`, {
|
|
1529
2066
|
databaseId: db.id,
|
|
@@ -1543,7 +2080,13 @@ function prismaNextDescriptor(o) {
|
|
|
1543
2080
|
invariants: [...ref.invariants].sort(),
|
|
1544
2081
|
...node.targetRef !== void 0 ? { refName: node.targetRef } : {}
|
|
1545
2082
|
});
|
|
1546
|
-
return {
|
|
2083
|
+
return {
|
|
2084
|
+
outputs: { url: warm.url },
|
|
2085
|
+
entities: [{
|
|
2086
|
+
kind: "postgres-database",
|
|
2087
|
+
id: db.id
|
|
2088
|
+
}]
|
|
2089
|
+
};
|
|
1547
2090
|
});
|
|
1548
2091
|
return Object.assign(lowering, { kind: "resource" });
|
|
1549
2092
|
}
|
|
@@ -1599,16 +2142,18 @@ const S3CredentialsProvider = () => Provider.effect(S3Credentials, Effect.succee
|
|
|
1599
2142
|
function s3CredentialsDescriptor(_o) {
|
|
1600
2143
|
const lowering = ({ id }) => Effect.gen(function* () {
|
|
1601
2144
|
const creds = yield* S3Credentials(`${id}-creds`, {});
|
|
1602
|
-
return {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2145
|
+
return {
|
|
2146
|
+
outputs: {
|
|
2147
|
+
accessKeyId: creds.accessKeyId,
|
|
2148
|
+
secretAccessKey: creds.secretAccessKey
|
|
2149
|
+
},
|
|
2150
|
+
entities: []
|
|
2151
|
+
};
|
|
1606
2152
|
});
|
|
1607
2153
|
return Object.assign(lowering, { kind: "resource" });
|
|
1608
2154
|
}
|
|
1609
2155
|
function s3StoreDescriptor(o) {
|
|
1610
2156
|
const base = computeDescriptor(o);
|
|
1611
|
-
if (base.kind !== "service") throw new Error("computeDescriptor must be a service descriptor");
|
|
1612
2157
|
return {
|
|
1613
2158
|
kind: "service",
|
|
1614
2159
|
provision: base.provision,
|
|
@@ -1618,27 +2163,33 @@ function s3StoreDescriptor(o) {
|
|
|
1618
2163
|
const credentials = config.inputs["credentials"] ?? {};
|
|
1619
2164
|
const bucket = config.service["bucket"];
|
|
1620
2165
|
if (credentials["accessKeyId"] === void 0 || credentials["secretAccessKey"] === void 0 || bucket === void 0) throw new Error("s3-store service must wire a 'credentials' dependency and a 'bucket' param");
|
|
1621
|
-
return {
|
|
1622
|
-
...serialized
|
|
2166
|
+
return {
|
|
2167
|
+
...serialized,
|
|
1623
2168
|
bucket,
|
|
1624
2169
|
accessKeyId: credentials["accessKeyId"],
|
|
1625
2170
|
secretAccessKey: credentials["secretAccessKey"]
|
|
1626
|
-
}
|
|
2171
|
+
};
|
|
1627
2172
|
}),
|
|
1628
2173
|
deploy: (ctx, provisioned, artifact, serialized) => Effect.gen(function* () {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
2174
|
+
const deployed = yield* base.deploy(ctx, provisioned, artifact, serialized);
|
|
2175
|
+
return {
|
|
2176
|
+
...deployed,
|
|
2177
|
+
outputs: {
|
|
2178
|
+
...deployed.outputs,
|
|
2179
|
+
bucket: serialized.bucket,
|
|
2180
|
+
accessKeyId: serialized.accessKeyId,
|
|
2181
|
+
secretAccessKey: serialized.secretAccessKey
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
1635
2184
|
})
|
|
1636
2185
|
};
|
|
1637
2186
|
}
|
|
1638
2187
|
/**
|
|
1639
|
-
* Deploy preflight (ADR-0029): before Alchemy
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
2188
|
+
* Deploy preflight (ADR-0029, extended for env-sourced params): before Alchemy
|
|
2189
|
+
* runs, verify every secret binding AND every env-sourced param binding in the
|
|
2190
|
+
* app's manifests (each a platform env-var NAME) exists on Prisma Cloud for
|
|
2191
|
+
* the target stage. A literal-bound param never touches the platform — only
|
|
2192
|
+
* `envParam(...)` sources are checked here.
|
|
1642
2193
|
* A name absent on the platform but present in the deploy shell is provisioned
|
|
1643
2194
|
* via a direct Management API POST — NEVER an Alchemy resource, so the value
|
|
1644
2195
|
* never lands in hosted deploy state. A name absent from both fails the deploy,
|
|
@@ -1646,7 +2197,7 @@ function s3StoreDescriptor(o) {
|
|
|
1646
2197
|
*
|
|
1647
2198
|
* Control-plane only (imported by control.ts → prisma-composer.config.ts); runs
|
|
1648
2199
|
* in the CLI parent, so it builds its own Management API client from env — the
|
|
1649
|
-
* same credential path `
|
|
2200
|
+
* same credential path `container.ts`'s `ensure`/`locate` use.
|
|
1650
2201
|
*/
|
|
1651
2202
|
/** production for the default stage; preview for a named stage — matching how the pack writes config rows. */
|
|
1652
2203
|
const classFor = (branchId) => branchId === void 0 ? "production" : "preview";
|
|
@@ -1689,59 +2240,202 @@ async function existsOnPlatform(client, projectId, branchId, key) {
|
|
|
1689
2240
|
* EnvironmentVariable.ts). A 409 means a concurrent deploy already provisioned
|
|
1690
2241
|
* it — tolerated. The value is never logged.
|
|
1691
2242
|
*/
|
|
1692
|
-
async function fillMissing(client,
|
|
2243
|
+
async function fillMissing(client, projectId, branchId, key, value) {
|
|
1693
2244
|
const res = await client.POST("/v1/environment-variables", { body: {
|
|
1694
|
-
projectId
|
|
1695
|
-
class: classFor(
|
|
2245
|
+
projectId,
|
|
2246
|
+
class: classFor(branchId),
|
|
1696
2247
|
key,
|
|
1697
2248
|
value,
|
|
1698
|
-
...
|
|
2249
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1699
2250
|
} });
|
|
1700
2251
|
if (res.error !== void 0 && res.response.status !== 409) throw fillFailedError(key, res.error);
|
|
1701
2252
|
}
|
|
1702
|
-
const tokenRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for deploy preflight.");
|
|
2253
|
+
const tokenRequiredError$1 = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for deploy preflight.");
|
|
1703
2254
|
const listFailedError = (key, error) => /* @__PURE__ */ new Error(`deploy preflight: Prisma Management API error listing "${key}": ${JSON.stringify(error)}.`);
|
|
1704
2255
|
const fillFailedError = (key, error) => /* @__PURE__ */ new Error(`deploy preflight: failed to provision "${key}" from the deploy shell: ${JSON.stringify(error)}.`);
|
|
1705
|
-
function missingError(missing,
|
|
1706
|
-
const scope =
|
|
2256
|
+
function missingError(missing, branchId, stage) {
|
|
2257
|
+
const scope = branchId === void 0 ? "the production class (project-level template)" : `the preview class of stage "${stage ?? branchId}" (branch override or template)`;
|
|
1707
2258
|
const lines = missing.map((m) => ` - ${m.name} (required by service "${m.serviceAddress}")`);
|
|
1708
|
-
return /* @__PURE__ */ new Error(`Deploy preflight failed — ${missing.length}
|
|
2259
|
+
return /* @__PURE__ */ new Error(`Deploy preflight failed — ${missing.length} env var(s) (secret or env-sourced param) are not provisioned on Prisma Cloud for ${scope}, and are absent from the deploy shell:\n${lines.join("\n")}\n\nSet each in the deploy shell environment (the CLI will provision it on deploy), or create it on the platform (Prisma Console or the Management API) in ${scope}.`);
|
|
1709
2260
|
}
|
|
1710
|
-
async function managementClient() {
|
|
1711
|
-
if ((process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError();
|
|
2261
|
+
async function managementClient$1() {
|
|
2262
|
+
if ((process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError$1();
|
|
1712
2263
|
return Effect.runPromise(Effect.gen(function* () {
|
|
1713
2264
|
return yield* ManagementClient;
|
|
1714
2265
|
}).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv())))));
|
|
1715
2266
|
}
|
|
1716
2267
|
/**
|
|
1717
2268
|
* The Prisma Cloud extension's `preflight`. Aggregates the target-agnostic
|
|
1718
|
-
*
|
|
2269
|
+
* manifests (core's `provisionManifest` for secrets, `paramManifest` filtered
|
|
2270
|
+
* to env-sourced bindings for params), checks each pointer name against the
|
|
1719
2271
|
* platform, fills from the shell where possible, and fails loudly on anything
|
|
1720
2272
|
* absent from both. Accepts an injected client for tests; otherwise builds one
|
|
1721
2273
|
* from env.
|
|
1722
2274
|
*/
|
|
1723
2275
|
async function runPreflight(input, deps) {
|
|
1724
|
-
const
|
|
1725
|
-
if (manifest.length === 0) return;
|
|
2276
|
+
const { projectId, branchId } = prismaCloudContainerOf(input.container);
|
|
1726
2277
|
const names = /* @__PURE__ */ new Map();
|
|
1727
|
-
for (const binding of
|
|
1728
|
-
name
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2278
|
+
for (const binding of provisionManifest(input.graph)) {
|
|
2279
|
+
const name = secretName(binding);
|
|
2280
|
+
if (!names.has(name)) names.set(name, {
|
|
2281
|
+
name,
|
|
2282
|
+
serviceAddress: binding.serviceAddress
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
for (const binding of paramManifest(input.graph)) {
|
|
2286
|
+
if (!isEnvParamSource(binding.binding)) continue;
|
|
2287
|
+
const name = paramName(binding);
|
|
2288
|
+
if (!names.has(name)) names.set(name, {
|
|
2289
|
+
name,
|
|
2290
|
+
serviceAddress: binding.serviceAddress
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
if (names.size === 0) return;
|
|
2294
|
+
const client = deps?.client ?? await managementClient$1();
|
|
1732
2295
|
const missing = [];
|
|
1733
2296
|
for (const meta of names.values()) {
|
|
1734
|
-
if (await existsOnPlatform(client,
|
|
2297
|
+
if (await existsOnPlatform(client, projectId, branchId, meta.name)) continue;
|
|
1735
2298
|
const shellValue = process.env[meta.name];
|
|
1736
2299
|
if (shellValue !== void 0 && shellValue.length > 0) {
|
|
1737
|
-
await fillMissing(client,
|
|
2300
|
+
await fillMissing(client, projectId, branchId, meta.name, shellValue);
|
|
1738
2301
|
continue;
|
|
1739
2302
|
}
|
|
1740
2303
|
missing.push(meta);
|
|
1741
2304
|
}
|
|
1742
|
-
if (missing.length > 0) throw missingError(missing, input);
|
|
2305
|
+
if (missing.length > 0) throw missingError(missing, branchId, input.stage);
|
|
2306
|
+
}
|
|
2307
|
+
const tokenRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for destroy teardown.");
|
|
2308
|
+
async function managementClient() {
|
|
2309
|
+
if ((process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError();
|
|
2310
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
2311
|
+
return yield* ManagementClient;
|
|
2312
|
+
}).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv())))));
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* The Prisma Cloud extension's `teardown`. Removes the stage's state database,
|
|
2316
|
+
* with failure handling that differs by stage because the consequences do:
|
|
2317
|
+
*
|
|
2318
|
+
* - **Named stage: throw.** The Branch delete that follows would fail anyway —
|
|
2319
|
+
* the platform refuses a Branch that still has a database attached — so
|
|
2320
|
+
* failing here names the actual cause instead of a confusing symptom.
|
|
2321
|
+
* - **Production: warn and continue.** Nothing blocks the Project delete on
|
|
2322
|
+
* this; removing the database only stops production's state outliving
|
|
2323
|
+
* production and holding a quota slot. That is a cleanup step, and a cleanup
|
|
2324
|
+
* step must not fail the command.
|
|
2325
|
+
*
|
|
2326
|
+
* Accepts an injected client and ownership verifier for tests; otherwise
|
|
2327
|
+
* builds a client from env and verifies against the real database.
|
|
2328
|
+
*/
|
|
2329
|
+
async function runTeardown(input, deps) {
|
|
2330
|
+
const { projectId, branchId } = prismaCloudContainerOf(input.container);
|
|
2331
|
+
const isNamedStage = branchId !== void 0;
|
|
2332
|
+
try {
|
|
2333
|
+
const client = deps?.client ?? await managementClient();
|
|
2334
|
+
await Effect.runPromise(deleteStateDatabaseWith({
|
|
2335
|
+
projectId,
|
|
2336
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
2337
|
+
}, deps?.verify ?? verifyOwnership).pipe(Effect.provideService(ManagementClient, client)));
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2340
|
+
if (isNamedStage) throw new Error(`Failed to delete the deploy-state database: ${reason}`);
|
|
2341
|
+
console.warn(`Could not remove production's deploy-state database: ${reason}`);
|
|
2342
|
+
}
|
|
1743
2343
|
}
|
|
1744
2344
|
/** The Prisma Cloud–hosted deploy state store; its implementation lives in @internal/lowering. */
|
|
2345
|
+
/**
|
|
2346
|
+
* ADR-0031's registered provisioner for RPC_PEER_KEY: mints one `ServiceKey`
|
|
2347
|
+
* resource per edge (ADR-0030) and forwards its value as the opaque ref core
|
|
2348
|
+
* writes into the consumer's `serviceKey` param. The resource id keeps the
|
|
2349
|
+
* `servicekey-${edgeId}` scheme byte-identical to slice 2's, so an existing
|
|
2350
|
+
* deploy's keys are found, not re-minted. Defined here, not in service-keys.ts:
|
|
2351
|
+
* that module is also reachable from the runtime/authoring side, which must
|
|
2352
|
+
* never import `@internal/lowering` or `effect`.
|
|
2353
|
+
*/
|
|
2354
|
+
const serviceKeyProvisioner = { provision: (edge) => Effect.gen(function* () {
|
|
2355
|
+
return (yield* ServiceKey(`servicekey-${edge.edgeId}`, {})).value;
|
|
2356
|
+
}) };
|
|
2357
|
+
/**
|
|
2358
|
+
* `ctx.provisioned`'s refs are typed `unknown` — core forwards a provisioner's
|
|
2359
|
+
* ref without inspecting it. Each provider param below is the sole reader of
|
|
2360
|
+
* its own provisioner's output, so the shape is asserted here, once, rather
|
|
2361
|
+
* than checked.
|
|
2362
|
+
*/
|
|
2363
|
+
const asKeyOutputs = (refs) => refs.map((ref) => blindCast(ref));
|
|
2364
|
+
/**
|
|
2365
|
+
* RPC's deploy-side `value(refs)` (ADR-0030): the provider stores a SET — one
|
|
2366
|
+
* key per inbound edge — into the accepted-keys var `serve()` reads. Paired
|
|
2367
|
+
* with the provisioner above: mint per edge, aggregate every edge.
|
|
2368
|
+
*
|
|
2369
|
+
* Zero consumers still emits, and that is the whole point of #100: an ABSENT
|
|
2370
|
+
* var means "never provisioned" and passes every caller through, so a deployed
|
|
2371
|
+
* provider nobody wired must say "[]" — deny everything — rather than say
|
|
2372
|
+
* nothing. Written as a literal because `Output.all()` with no arguments has
|
|
2373
|
+
* nothing to resolve.
|
|
2374
|
+
*/
|
|
2375
|
+
const rpcAcceptedKeysValue = (refs) => refs.length > 0 ? Output.all(...asKeyOutputs(refs)) : [];
|
|
2376
|
+
/**
|
|
2377
|
+
* ADR-0031's registered provisioner for STREAMS_API_KEY — the same `ServiceKey`
|
|
2378
|
+
* mint, keyed PER PROVIDER instead of per edge: the resource id is the
|
|
2379
|
+
* provider's address, so every consumer edge of one streams module resolves to
|
|
2380
|
+
* the same resource and therefore the same stable value. That is what
|
|
2381
|
+
* `@prisma/streams-server` requires (it authenticates a single `API_KEY`), and
|
|
2382
|
+
* cardinality is exactly what ADR-0031 leaves to the provisioner. Making it
|
|
2383
|
+
* per-edge later is this id's shape plus an accepted-set provider param — no
|
|
2384
|
+
* new resource, no core change.
|
|
2385
|
+
*/
|
|
2386
|
+
const streamsApiKeyProvisioner = { provision: (edge) => Effect.gen(function* () {
|
|
2387
|
+
return (yield* ServiceKey(`streamskey-${edge.providerAddress}`, {})).value;
|
|
2388
|
+
}) };
|
|
2389
|
+
/**
|
|
2390
|
+
* Streams' deploy-side `value(refs)`: ONE value, not a set —
|
|
2391
|
+
* `@prisma/streams-server` authenticates a single `API_KEY`, which is why the
|
|
2392
|
+
* provisioner above mints per provider. That pairing is the invariant this
|
|
2393
|
+
* param depends on, so it asserts rather than trusts it: a future per-edge
|
|
2394
|
+
* flip without a paired accepted-set param here would otherwise ship
|
|
2395
|
+
* whichever key came first and leave every other consumer 401ing, silently.
|
|
2396
|
+
* The refs are lazy Outputs (not comparable at serialize time); inside
|
|
2397
|
+
* `Output.map` they are resolved strings, the same seam RPC's set aggregates
|
|
2398
|
+
* on.
|
|
2399
|
+
*
|
|
2400
|
+
* Zero consumers emits NOTHING — the streams counterpart to RPC's "[]".
|
|
2401
|
+
* `@prisma/streams-server` has no deny-all mode: it either authenticates a key
|
|
2402
|
+
* or runs --no-auth, so there is no value here that means "refuse everyone".
|
|
2403
|
+
* Writing no key is what fails closed — the entrypoint refuses to boot with
|
|
2404
|
+
* a named error rather than serve unauthenticated.
|
|
2405
|
+
*/
|
|
2406
|
+
const streamsApiKeyValue = (refs) => {
|
|
2407
|
+
if (refs.length === 0) return void 0;
|
|
2408
|
+
return Output.map(Output.all(...asKeyOutputs(refs)), (vals) => {
|
|
2409
|
+
const distinct = [...new Set(vals)];
|
|
2410
|
+
if (distinct.length > 1) throw new Error(`a streams provider was provisioned ${distinct.length} distinct keys across its ${refs.length} inbound bindings, but it can only be given one (@prisma/streams-server authenticates a single API_KEY). Its provisioner must mint per provider, not per edge — or this param must store an accepted-key set, once the server accepts one.`);
|
|
2411
|
+
return distinct[0] ?? "";
|
|
2412
|
+
});
|
|
2413
|
+
};
|
|
2414
|
+
/**
|
|
2415
|
+
* Origin's deploy-side value function (service-derived): the provisioned
|
|
2416
|
+
* service's own `endpointDomain`, verbatim — `https://…`, no trailing slash,
|
|
2417
|
+
* no normalization. Every compute service gets this row, exposing or not.
|
|
2418
|
+
* The undefined guard is a deploy-time invariant check, not a policy: the
|
|
2419
|
+
* Management API always reports an endpoint domain post-PRO-200, so a missing
|
|
2420
|
+
* one means the platform predates the fix — fail the deploy loudly rather
|
|
2421
|
+
* than write a row origin() would trust. The raw string is JSON-encoded by
|
|
2422
|
+
* the descriptor's generic loop, like every other reserved provider param.
|
|
2423
|
+
*/
|
|
2424
|
+
const selfOriginValue = (provisioned, address) => Output.map(provisioned.endpointDomain, (v) => {
|
|
2425
|
+
if (v === void 0) throw new Error(`ComputeService for "${address}" reported no endpointDomain at provision — cannot resolve the service's own origin (Management API predates the PRO-200 fix?)`);
|
|
2426
|
+
return v;
|
|
2427
|
+
});
|
|
2428
|
+
/** The user-facing state descriptor: `state: prismaState()` in `prisma-composer.config.ts` (ADR-0017). */
|
|
2429
|
+
const prismaState = () => ({
|
|
2430
|
+
extension: PRISMA_CLOUD_EXTENSION_ID,
|
|
2431
|
+
create: (container) => {
|
|
2432
|
+
const { projectId, branchId } = prismaCloudContainerOf(container);
|
|
2433
|
+
return prismaStateLayer(branchId !== void 0 ? {
|
|
2434
|
+
projectId,
|
|
2435
|
+
branchId
|
|
2436
|
+
} : { projectId });
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
1745
2439
|
const KNOWN_REGION_SET = new Set(COMPUTE_REGIONS);
|
|
1746
2440
|
function isComputeRegion(value) {
|
|
1747
2441
|
return KNOWN_REGION_SET.has(value);
|
|
@@ -1751,64 +2445,114 @@ function asProvidersLayer(layer) {
|
|
|
1751
2445
|
return layer;
|
|
1752
2446
|
}
|
|
1753
2447
|
/**
|
|
2448
|
+
* This extension's brands, each with the two halves ADR-0031 splits: the
|
|
2449
|
+
* PROVISIONER core resolves a mint through, and the reserved PROVIDER PARAM
|
|
2450
|
+
* that stores the minted values on the provider. So this file stays the only
|
|
2451
|
+
* place a brand is named — `descriptors/compute.ts` just looks a provider
|
|
2452
|
+
* param up by brand.
|
|
2453
|
+
*
|
|
2454
|
+
* `__tests__/provider-params.test.ts` asserts this map's brands are exactly
|
|
2455
|
+
* `PROVIDER_PARAMS`'s edge-derived brands (a service-derived param like the
|
|
2456
|
+
* origin mints nothing, so it has no provisioner): a brand minted here with no provider param
|
|
2457
|
+
* below would leave the value it mints written to consumers while no
|
|
2458
|
+
* provider ever stores an accepted-keys row for it — `serve()` (or the
|
|
2459
|
+
* equivalent runtime reader) then sees an absent var and passes every caller
|
|
2460
|
+
* through.
|
|
2461
|
+
*/
|
|
2462
|
+
const PROVISIONERS = /* @__PURE__ */ new Map([[RPC_PEER_KEY, serviceKeyProvisioner], [STREAMS_API_KEY, streamsApiKeyProvisioner]]);
|
|
2463
|
+
/**
|
|
2464
|
+
* Every brand's deploy-side value function — the only per-brand thing this
|
|
2465
|
+
* file still holds directly. `PROVIDER_PARAMS` below is built by mapping
|
|
2466
|
+
* `RESERVED_PROVIDER_PARAMS` (`provider-params.ts`, the boot-side list) onto
|
|
2467
|
+
* this map, so a param can exist on the deploy side only if it already
|
|
2468
|
+
* exists on the boot side: `RESERVED_PROVIDER_PARAMS` is the single source of
|
|
2469
|
+
* which reserved provider params exist at all, closing the drift the old
|
|
2470
|
+
* name-comparison test only detected after the fact.
|
|
2471
|
+
*/
|
|
2472
|
+
const PROVIDER_PARAM_VALUES = /* @__PURE__ */ new Map([
|
|
2473
|
+
[RPC_PEER_KEY, { value: rpcAcceptedKeysValue }],
|
|
2474
|
+
[STREAMS_API_KEY, { value: streamsApiKeyValue }],
|
|
2475
|
+
[SELF_ORIGIN, { valueForService: selfOriginValue }]
|
|
2476
|
+
]);
|
|
2477
|
+
/**
|
|
2478
|
+
* Builds the deploy-side registry from the boot-side list, keyed by brand —
|
|
2479
|
+
* throws if a boot-side entry has no registered deploy-side value function,
|
|
2480
|
+
* so `RESERVED_PROVIDER_PARAMS` stays the single source of which reserved
|
|
2481
|
+
* provider params exist: deploy can no longer write a row boot never
|
|
2482
|
+
* stashes. Exported (rather than inlined into `PROVIDER_PARAMS` below) so
|
|
2483
|
+
* `__tests__/provider-params.test.ts` can drive it directly with a
|
|
2484
|
+
* deliberately incomplete value map and watch it throw.
|
|
2485
|
+
*/
|
|
2486
|
+
function buildProviderParams(entries, values) {
|
|
2487
|
+
return new Map(entries.map((entry) => {
|
|
2488
|
+
const value = values.get(entry.brand);
|
|
2489
|
+
if (value === void 0) throw new Error(`prisma-cloud: reserved provider param "${entry.name}" (provider-params.ts) has no registered deploy-side value() in control.ts's PROVIDER_PARAM_VALUES — every param in RESERVED_PROVIDER_PARAMS must have one.`);
|
|
2490
|
+
return [entry.brand, {
|
|
2491
|
+
...entry,
|
|
2492
|
+
...value
|
|
2493
|
+
}];
|
|
2494
|
+
}));
|
|
2495
|
+
}
|
|
2496
|
+
const PROVIDER_PARAMS = buildProviderParams(RESERVED_PROVIDER_PARAMS, PROVIDER_PARAM_VALUES);
|
|
2497
|
+
/**
|
|
1754
2498
|
* Resolves the factory's env-or-option inputs, failing fast with the exact
|
|
1755
|
-
* variable name.
|
|
1756
|
-
* also runs in the CLI parent, before they're set; the required check lives in `application.provision`.
|
|
2499
|
+
* variable name.
|
|
1757
2500
|
*/
|
|
1758
2501
|
function resolveOptions(opts) {
|
|
1759
2502
|
const workspaceId = opts.workspaceId ?? process.env["PRISMA_WORKSPACE_ID"];
|
|
1760
2503
|
if (workspaceId === void 0 || workspaceId.length === 0) throw new Error("prismaCloud(): environment variable PRISMA_WORKSPACE_ID is required.");
|
|
1761
|
-
const projectId = process.env["PRISMA_PROJECT_ID"] || void 0;
|
|
1762
|
-
const branchId = process.env["PRISMA_BRANCH_ID"] || void 0;
|
|
1763
2504
|
if (opts.region !== void 0) return {
|
|
1764
2505
|
workspaceId,
|
|
1765
2506
|
region: opts.region,
|
|
1766
|
-
|
|
1767
|
-
branchId
|
|
2507
|
+
providerParams: PROVIDER_PARAMS
|
|
1768
2508
|
};
|
|
1769
2509
|
const region = process.env["PRISMA_REGION"];
|
|
1770
2510
|
if (region === void 0 || region.length === 0) return {
|
|
1771
2511
|
workspaceId,
|
|
1772
|
-
|
|
1773
|
-
branchId
|
|
2512
|
+
providerParams: PROVIDER_PARAMS
|
|
1774
2513
|
};
|
|
1775
2514
|
if (!isComputeRegion(region)) throw new Error(`prismaCloud(): environment variable PRISMA_REGION="${region}" is not a known region (expected one of: ${COMPUTE_REGIONS.join(", ")}).`);
|
|
1776
2515
|
return {
|
|
1777
2516
|
workspaceId,
|
|
1778
2517
|
region,
|
|
1779
|
-
|
|
1780
|
-
branchId
|
|
2518
|
+
providerParams: PROVIDER_PARAMS
|
|
1781
2519
|
};
|
|
1782
2520
|
}
|
|
1783
2521
|
/** The Prisma Cloud extension descriptor — `prisma-composer.config.ts` lists it under `extensions`. */
|
|
1784
2522
|
const prismaCloud = (opts = {}) => {
|
|
1785
2523
|
const o = resolveOptions(opts);
|
|
1786
2524
|
return {
|
|
1787
|
-
id:
|
|
1788
|
-
|
|
2525
|
+
id: PRISMA_CLOUD_EXTENSION_ID,
|
|
2526
|
+
container: containerDescriptor(),
|
|
2527
|
+
providers: () => asProvidersLayer(Layer.mergeAll(providers(), PgWarmProvider(), PnMigrationProvider(), S3CredentialsProvider(), ServiceKeyProvider())),
|
|
1789
2528
|
preflight: (input) => runPreflight(input),
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2529
|
+
teardown: (input) => runTeardown(input),
|
|
2530
|
+
application: { provision: (ctx) => Effect.gen(function* () {
|
|
2531
|
+
const { projectId, branchId } = prismaCloudContainerOf(ctx.container);
|
|
1793
2532
|
for (const key of ["DATABASE_URL", "DATABASE_URL_POOLED"]) yield* EnvironmentVariable(`${key}-poison`, {
|
|
1794
2533
|
projectId,
|
|
1795
2534
|
key,
|
|
1796
2535
|
value: "-",
|
|
1797
|
-
class:
|
|
1798
|
-
...
|
|
2536
|
+
class: branchId ? "preview" : "production",
|
|
2537
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1799
2538
|
});
|
|
1800
|
-
return {
|
|
2539
|
+
return {
|
|
2540
|
+
projectId,
|
|
2541
|
+
branchId
|
|
2542
|
+
};
|
|
1801
2543
|
}) },
|
|
2544
|
+
provisions: PROVISIONERS,
|
|
1802
2545
|
nodes: {
|
|
1803
2546
|
postgres: postgresDescriptor(o),
|
|
1804
2547
|
"prisma-next": prismaNextDescriptor(o),
|
|
1805
2548
|
compute: computeDescriptor(o),
|
|
1806
2549
|
credentials: s3CredentialsDescriptor(o),
|
|
1807
|
-
"s3-store": s3StoreDescriptor(o)
|
|
2550
|
+
"s3-store": s3StoreDescriptor(o),
|
|
2551
|
+
s3: bucketDescriptor(o)
|
|
1808
2552
|
}
|
|
1809
2553
|
};
|
|
1810
2554
|
};
|
|
1811
2555
|
//#endregion
|
|
1812
|
-
export { prismaCloud, prismaState };
|
|
2556
|
+
export { PROVIDER_PARAMS, buildProviderParams, prismaCloud, prismaState };
|
|
1813
2557
|
|
|
1814
2558
|
//# sourceMappingURL=control.mjs.map
|