@prisma/composer-prisma-cloud 0.2.0-dev.12 → 0.2.0-dev.14
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/buckets-main.mjs +795 -0
- package/dist/buckets-main.mjs.map +1 -0
- package/dist/compute-main.mjs +532 -0
- package/dist/compute-main.mjs.map +1 -0
- package/dist/control.mjs +41 -1257
- package/dist/control.mjs.map +1 -1
- package/dist/cron/index.mjs +2 -3
- package/dist/cron/index.mjs.map +1 -1
- package/dist/cron/scheduler-entrypoint.mjs.map +1 -1
- package/dist/cron/scheduler-service.mjs +2 -3
- package/dist/cron/scheduler-service.mjs.map +1 -1
- package/dist/email/email-entrypoint.mjs.map +1 -1
- package/dist/email/email-service.mjs +2 -3
- package/dist/email/email-service.mjs.map +1 -1
- package/dist/email/index.mjs +2 -3
- package/dist/email/index.mjs.map +1 -1
- package/dist/email/testing.mjs.map +1 -1
- package/dist/get-port-BYnpbW5H.mjs +111 -0
- package/dist/get-port-BYnpbW5H.mjs.map +1 -0
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/local-target.d.mts +12 -0
- package/dist/local-target.d.mts.map +1 -0
- package/dist/local-target.mjs +6181 -0
- package/dist/local-target.mjs.map +1 -0
- package/dist/param-BTzY0ne5-CW6ojZ3E.mjs +66 -0
- package/dist/param-BTzY0ne5-CW6ojZ3E.mjs.map +1 -0
- package/dist/pg-connection-CadPZuEK-CF3FsSQu.mjs +79 -0
- package/dist/pg-connection-CadPZuEK-CF3FsSQu.mjs.map +1 -0
- package/dist/postgres-main.mjs +492 -0
- package/dist/postgres-main.mjs.map +1 -0
- package/dist/preflight-names-BPL-qsZF-BfoRgbgb.mjs +1252 -0
- package/dist/preflight-names-BPL-qsZF-BfoRgbgb.mjs.map +1 -0
- package/dist/{prisma-next-BvBwGCIt-CSauMX4l.mjs → prisma-next-BvBwGCIt-DssgnctU.mjs} +3 -78
- package/dist/prisma-next-BvBwGCIt-DssgnctU.mjs.map +1 -0
- package/dist/prisma-next.mjs +1 -1
- package/dist/{provisioned-edges-B_XS1Mz--NFJgcXK7.mjs → provisioned-edges-C1wSO3cx-YC5C19f9.mjs} +5 -65
- package/dist/provisioned-edges-C1wSO3cx-YC5C19f9.mjs.map +1 -0
- package/dist/secret-rz_ZqNeq-_xd97Jd9.mjs +47 -0
- package/dist/secret-rz_ZqNeq-_xd97Jd9.mjs.map +1 -0
- package/dist/segments-NpKN-46R-B01DmCqy.mjs +1441 -0
- package/dist/segments-NpKN-46R-B01DmCqy.mjs.map +1 -0
- package/dist/{serializer-D8_84XWO-DA2B6YbX.mjs → serializer-CJiOhF2d-DJUjQj1i.mjs} +4 -44
- package/dist/serializer-CJiOhF2d-DJUjQj1i.mjs.map +1 -0
- package/dist/storage/index.mjs +2 -3
- package/dist/storage/index.mjs.map +1 -1
- package/dist/storage/storage-entrypoint.mjs.map +1 -1
- package/dist/storage/storage-service.mjs +2 -3
- package/dist/storage/storage-service.mjs.map +1 -1
- package/dist/streams/index.mjs +2 -3
- package/dist/streams/index.mjs.map +1 -1
- package/dist/streams/streams-entrypoint.mjs.map +1 -1
- package/dist/streams/streams-service.mjs +2 -3
- package/dist/streams/streams-service.mjs.map +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +21 -15
- package/src/exports/local-target.ts +1 -0
- package/dist/prisma-next-BvBwGCIt-CSauMX4l.mjs.map +0 -1
- package/dist/provisioned-edges-B_XS1Mz--NFJgcXK7.mjs.map +0 -1
- package/dist/serializer-D8_84XWO-DA2B6YbX.mjs.map +0 -1
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
import { n as secretName } from "./secret-rz_ZqNeq-_xd97Jd9.mjs";
|
|
2
|
+
import { i as paramName, n as isEnvParamSource } from "./param-BTzY0ne5-CW6ojZ3E.mjs";
|
|
3
|
+
import { r as withConnectionRetry, t as normalizeSslMode } from "./pg-connection-CadPZuEK-CF3FsSQu.mjs";
|
|
4
|
+
import { paramManifest, provisionManifest } from "@prisma/composer";
|
|
5
|
+
import { createManagementApiClient } from "@prisma/management-api-sdk";
|
|
6
|
+
import * as Context from "effect/Context";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Layer from "effect/Layer";
|
|
9
|
+
import * as Redacted from "effect/Redacted";
|
|
10
|
+
import * as Config$1 from "effect/Config";
|
|
11
|
+
import * as Data from "effect/Data";
|
|
12
|
+
import * as Provider from "alchemy/Provider";
|
|
13
|
+
import { Resource } from "alchemy";
|
|
14
|
+
import * as Schedule from "effect/Schedule";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as crypto$1 from "node:crypto";
|
|
17
|
+
import "node:crypto";
|
|
18
|
+
import * as os$1 from "node:os";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import * as zlib from "node:zlib";
|
|
21
|
+
import pg from "pg";
|
|
22
|
+
import { readRef } from "@prisma-next/migration-tools/refs";
|
|
23
|
+
import { APP_SPACE_ID, readContractSpaceHeadRef, spaceMigrationDirectory, spaceRefsDirectory } from "@prisma-next/migration-tools/spaces";
|
|
24
|
+
import { createPostgresControlClient } from "@prisma-next/postgres/control";
|
|
25
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CJM0E6rj.mjs
|
|
26
|
+
/**
|
|
27
|
+
* The Prisma service token used to authenticate Management API calls. Kept
|
|
28
|
+
* as a Redacted value so it never lands in logs or error output.
|
|
29
|
+
*/
|
|
30
|
+
var PrismaCredentials = class extends Context.Service()("PrismaCredentials") {};
|
|
31
|
+
/** Resolve the token from the `PRISMA_SERVICE_TOKEN` environment variable. */
|
|
32
|
+
const fromEnv = () => Layer.effect(PrismaCredentials, Effect.gen(function* () {
|
|
33
|
+
return { token: yield* Config$1.redacted("PRISMA_SERVICE_TOKEN") };
|
|
34
|
+
}));
|
|
35
|
+
/**
|
|
36
|
+
* The typed Prisma Management API client, built once from the resolved
|
|
37
|
+
* credentials. Providers yield this in their outer Effect and call it inside
|
|
38
|
+
* `reconcile` / `delete`.
|
|
39
|
+
*/
|
|
40
|
+
var ManagementClient = class extends Context.Service()("PrismaManagementClient") {};
|
|
41
|
+
const layer = () => Layer.effect(ManagementClient, Effect.gen(function* () {
|
|
42
|
+
const { token } = yield* PrismaCredentials;
|
|
43
|
+
return createManagementApiClient({ token: Redacted.value(token) });
|
|
44
|
+
}));
|
|
45
|
+
/** A non-2xx response from the Management API (or a transport failure). */
|
|
46
|
+
var PrismaApiError = class extends Data.TaggedError("PrismaApiError") {};
|
|
47
|
+
const attempt = (f) => Effect.tryPromise({
|
|
48
|
+
try: f,
|
|
49
|
+
catch: (cause) => new PrismaApiError({
|
|
50
|
+
status: 0,
|
|
51
|
+
message: String(cause)
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
const fail = (r) => Effect.fail(new PrismaApiError({
|
|
55
|
+
status: r.response.status,
|
|
56
|
+
message: JSON.stringify(r.error)
|
|
57
|
+
}));
|
|
58
|
+
/** Unwrap `data`, failing on any API error. Preserves the SDK's response type. */
|
|
59
|
+
const call = (f) => attempt(f).pipe(Effect.flatMap((r) => r.error !== void 0 || r.data === void 0 ? fail(r) : Effect.succeed(r.data)));
|
|
60
|
+
/** Unwrap `data`, mapping a 404 to `undefined` (resource gone / not found). */
|
|
61
|
+
const callOptional = (f) => attempt(f).pipe(Effect.flatMap((r) => r.response.status === 404 ? Effect.succeed(void 0) : r.error !== void 0 ? fail(r) : Effect.succeed(r.data)));
|
|
62
|
+
/** Fire-and-forget a call, tolerating a 404 (already deleted). */
|
|
63
|
+
const callVoid = (f) => attempt(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/buckets.mjs
|
|
66
|
+
/** A Prisma **Object Store bucket** inside a project. */
|
|
67
|
+
const Bucket = Resource("Prisma.Bucket");
|
|
68
|
+
const BucketProvider = () => Provider.effect(Bucket, Effect.gen(function* () {
|
|
69
|
+
const client = yield* ManagementClient;
|
|
70
|
+
return {
|
|
71
|
+
stables: ["id"],
|
|
72
|
+
list: () => Effect.succeed([]),
|
|
73
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
74
|
+
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } })) : void 0;
|
|
75
|
+
if (observed) return {
|
|
76
|
+
id: observed.data.id,
|
|
77
|
+
name: observed.data.name
|
|
78
|
+
};
|
|
79
|
+
const created = yield* call(() => client.POST("/v1/buckets", { body: {
|
|
80
|
+
projectId: news.projectId,
|
|
81
|
+
name: news.name,
|
|
82
|
+
...news.branchId !== void 0 ? { branchId: news.branchId } : {}
|
|
83
|
+
} }));
|
|
84
|
+
return {
|
|
85
|
+
id: created.data.id,
|
|
86
|
+
name: created.data.name
|
|
87
|
+
};
|
|
88
|
+
}),
|
|
89
|
+
delete: Effect.fn(function* ({ output }) {
|
|
90
|
+
yield* callVoid(() => client.DELETE("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } }));
|
|
91
|
+
}),
|
|
92
|
+
read: Effect.fn(function* ({ output }) {
|
|
93
|
+
if (!output?.id) return void 0;
|
|
94
|
+
const b = yield* callOptional(() => client.GET("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } }));
|
|
95
|
+
return b ? {
|
|
96
|
+
id: b.data.id,
|
|
97
|
+
name: b.data.name
|
|
98
|
+
} : void 0;
|
|
99
|
+
})
|
|
100
|
+
};
|
|
101
|
+
}));
|
|
102
|
+
/** A **bucket access key** for a Prisma Object Store bucket — yields the S3 credentials. */
|
|
103
|
+
const BucketKey = Resource("Prisma.BucketKey");
|
|
104
|
+
const BucketKeyProvider = () => Provider.effect(BucketKey, Effect.gen(function* () {
|
|
105
|
+
const client = yield* ManagementClient;
|
|
106
|
+
return {
|
|
107
|
+
stables: [
|
|
108
|
+
"id",
|
|
109
|
+
"bucketId",
|
|
110
|
+
"secretAccessKey",
|
|
111
|
+
"accessKeyId",
|
|
112
|
+
"endpoint",
|
|
113
|
+
"bucketName"
|
|
114
|
+
],
|
|
115
|
+
list: () => Effect.succeed([]),
|
|
116
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
117
|
+
if (output?.id) return output;
|
|
118
|
+
const created = yield* call(() => client.POST("/v1/buckets/{bucketId}/keys", {
|
|
119
|
+
params: { path: { bucketId: news.bucketId } },
|
|
120
|
+
body: {
|
|
121
|
+
name: news.name,
|
|
122
|
+
role: news.role
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
id: created.data.id,
|
|
127
|
+
bucketId: news.bucketId,
|
|
128
|
+
accessKeyId: created.data.accessKeyId,
|
|
129
|
+
secretAccessKey: Redacted.make(created.data.secretAccessKey),
|
|
130
|
+
endpoint: created.data.endpoint,
|
|
131
|
+
bucketName: created.data.bucketName
|
|
132
|
+
};
|
|
133
|
+
}),
|
|
134
|
+
delete: Effect.fn(function* ({ output }) {
|
|
135
|
+
yield* callVoid(() => client.DELETE("/v1/buckets/{bucketId}/keys/{keyId}", { params: { path: {
|
|
136
|
+
bucketId: output.bucketId,
|
|
137
|
+
keyId: output.id
|
|
138
|
+
} } }));
|
|
139
|
+
})
|
|
140
|
+
};
|
|
141
|
+
}));
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/compute.mjs
|
|
144
|
+
/**
|
|
145
|
+
* Stopping a deployment before the app that owns it can be
|
|
146
|
+
* deleted is asynchronous on the platform's side: DELETE can 409 with this
|
|
147
|
+
* message while the deployment is still winding down. Retrying blindly on
|
|
148
|
+
* every API error would mask real failures (bad auth, a genuinely conflicting
|
|
149
|
+
* state, etc.), so this only matches the platform's specific "not delete-safe
|
|
150
|
+
* yet" wording — everything else fails immediately, as before.
|
|
151
|
+
*/
|
|
152
|
+
const isDeleteNotSafeYet = (error) => error.message.includes("did not reach a delete-safe state");
|
|
153
|
+
/**
|
|
154
|
+
* Backs off exponentially from 2s, capped at 5 minutes total — long enough
|
|
155
|
+
* for the platform to finish stopping the deployment, short enough to still
|
|
156
|
+
* fail loudly (rather than hang forever) if it never does.
|
|
157
|
+
*/
|
|
158
|
+
const deleteSafeRetrySchedule = Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
|
|
159
|
+
/** Every region Prisma Compute serves — the runtime source of truth; `ComputeRegion` is derived from it so the two can never drift. */
|
|
160
|
+
const COMPUTE_REGIONS = [
|
|
161
|
+
"us-east-1",
|
|
162
|
+
"us-west-1",
|
|
163
|
+
"eu-west-3",
|
|
164
|
+
"eu-central-1",
|
|
165
|
+
"ap-northeast-1",
|
|
166
|
+
"ap-southeast-1"
|
|
167
|
+
];
|
|
168
|
+
/** A Prisma **Compute service** — the stable app identity behind a project. */
|
|
169
|
+
const ComputeService = Resource("Prisma.ComputeService");
|
|
170
|
+
const ComputeServiceProvider = () => Provider.effect(ComputeService, Effect.gen(function* () {
|
|
171
|
+
const client = yield* ManagementClient;
|
|
172
|
+
return {
|
|
173
|
+
stables: ["id"],
|
|
174
|
+
list: () => Effect.succeed([]),
|
|
175
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
176
|
+
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/apps/{appId}", { params: { path: { appId: output.id } } })) : void 0;
|
|
177
|
+
if (observed) return {
|
|
178
|
+
id: observed.data.id,
|
|
179
|
+
name: observed.data.name,
|
|
180
|
+
endpointDomain: observed.data.appEndpointDomain
|
|
181
|
+
};
|
|
182
|
+
const created = yield* call(() => client.POST("/v1/apps", { body: {
|
|
183
|
+
displayName: news.name,
|
|
184
|
+
projectId: news.projectId,
|
|
185
|
+
...news.region && { regionId: news.region },
|
|
186
|
+
...news.branchId !== void 0 && { branchId: news.branchId }
|
|
187
|
+
} }));
|
|
188
|
+
return {
|
|
189
|
+
id: created.data.id,
|
|
190
|
+
name: created.data.name,
|
|
191
|
+
endpointDomain: created.data.appEndpointDomain
|
|
192
|
+
};
|
|
193
|
+
}),
|
|
194
|
+
delete: Effect.fn(function* ({ output }) {
|
|
195
|
+
yield* callVoid(() => client.DELETE("/v1/apps/{appId}", { params: { path: { appId: output.id } } })).pipe(Effect.retry({
|
|
196
|
+
schedule: deleteSafeRetrySchedule,
|
|
197
|
+
while: isDeleteNotSafeYet
|
|
198
|
+
}));
|
|
199
|
+
}),
|
|
200
|
+
read: Effect.fn(function* ({ output }) {
|
|
201
|
+
if (!output?.id) return void 0;
|
|
202
|
+
const s = yield* callOptional(() => client.GET("/v1/apps/{appId}", { params: { path: { appId: output.id } } }));
|
|
203
|
+
return s ? {
|
|
204
|
+
id: s.data.id,
|
|
205
|
+
name: s.data.name,
|
|
206
|
+
endpointDomain: s.data.appEndpointDomain
|
|
207
|
+
} : void 0;
|
|
208
|
+
})
|
|
209
|
+
};
|
|
210
|
+
}));
|
|
211
|
+
/**
|
|
212
|
+
* A **deployment** of a Prisma app — creates a deployment, uploads
|
|
213
|
+
* its artifact, starts the VM, waits for it to run, then promotes it to the
|
|
214
|
+
* app's stable endpoint.
|
|
215
|
+
*/
|
|
216
|
+
const Deployment = Resource("Prisma.Deployment");
|
|
217
|
+
const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
|
|
218
|
+
const client = yield* ManagementClient;
|
|
219
|
+
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({
|
|
220
|
+
status: 409,
|
|
221
|
+
message: `deployment ${deploymentId} is ${v.data.status}, not running`
|
|
222
|
+
}))), Effect.retry(Schedule.both(Schedule.spaced("2 seconds"), Schedule.during("2 minutes"))));
|
|
223
|
+
return {
|
|
224
|
+
stables: [],
|
|
225
|
+
list: () => Effect.succeed([]),
|
|
226
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
227
|
+
const created = yield* call(() => client.POST("/v1/apps/{appId}/deployments", {
|
|
228
|
+
params: { path: { appId: news.computeServiceId } },
|
|
229
|
+
body: news.port !== void 0 ? { portMapping: { http: news.port } } : {}
|
|
230
|
+
}));
|
|
231
|
+
const deploymentId = created.data.id;
|
|
232
|
+
if (created.data.uploadUrl) {
|
|
233
|
+
const uploadUrl = created.data.uploadUrl;
|
|
234
|
+
const artifact = yield* Effect.try({
|
|
235
|
+
try: () => fs.readFileSync(news.artifactPath),
|
|
236
|
+
catch: (cause) => new PrismaApiError({
|
|
237
|
+
status: 0,
|
|
238
|
+
message: `failed to read artifact ${news.artifactPath}: ${String(cause)}`
|
|
239
|
+
})
|
|
240
|
+
});
|
|
241
|
+
yield* Effect.tryPromise({
|
|
242
|
+
try: async () => {
|
|
243
|
+
const res = await fetch(uploadUrl, {
|
|
244
|
+
method: "PUT",
|
|
245
|
+
body: artifact
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) throw new PrismaApiError({
|
|
248
|
+
status: res.status,
|
|
249
|
+
message: `artifact upload failed: ${res.status} ${res.statusText}`
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
catch: (cause) => cause instanceof PrismaApiError ? cause : new PrismaApiError({
|
|
253
|
+
status: 0,
|
|
254
|
+
message: String(cause)
|
|
255
|
+
})
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
yield* call(() => client.POST("/v1/deployments/{deploymentId}/start", { params: { path: { deploymentId } } }));
|
|
259
|
+
yield* waitForRunning(deploymentId);
|
|
260
|
+
const deployedUrl = (yield* call(() => client.POST("/v1/apps/{appId}/promote", {
|
|
261
|
+
params: { path: { appId: news.computeServiceId } },
|
|
262
|
+
body: { deploymentId }
|
|
263
|
+
}))).data.appEndpointDomain;
|
|
264
|
+
return {
|
|
265
|
+
deploymentId,
|
|
266
|
+
...deployedUrl !== void 0 && { deployedUrl }
|
|
267
|
+
};
|
|
268
|
+
}),
|
|
269
|
+
delete: Effect.fn(function* () {}),
|
|
270
|
+
read: Effect.fn(function* ({ output }) {
|
|
271
|
+
if (!output?.deploymentId) return void 0;
|
|
272
|
+
const v = yield* callOptional(() => client.GET("/v1/deployments/{deploymentId}", { params: { path: { deploymentId: output.deploymentId } } }));
|
|
273
|
+
return v ? {
|
|
274
|
+
deploymentId: v.data.id,
|
|
275
|
+
...v.data.previewDomain && { deployedUrl: v.data.previewDomain }
|
|
276
|
+
} : void 0;
|
|
277
|
+
})
|
|
278
|
+
};
|
|
279
|
+
}));
|
|
280
|
+
/**
|
|
281
|
+
* A project-scoped **environment variable** that Compute injects into the
|
|
282
|
+
* project's services from their attached branch (e.g. wiring one module's URL into
|
|
283
|
+
* another).
|
|
284
|
+
*/
|
|
285
|
+
const EnvironmentVariable = Resource("Prisma.EnvironmentVariable");
|
|
286
|
+
const EnvironmentVariableProvider = () => Provider.effect(EnvironmentVariable, Effect.gen(function* () {
|
|
287
|
+
const client = yield* ManagementClient;
|
|
288
|
+
return {
|
|
289
|
+
stables: ["id"],
|
|
290
|
+
list: () => Effect.succeed([]),
|
|
291
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
292
|
+
const cls = news.class ?? "production";
|
|
293
|
+
let id = output?.id;
|
|
294
|
+
if (id !== void 0) {
|
|
295
|
+
const priorId = id;
|
|
296
|
+
if (!(yield* callOptional(() => client.GET("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: priorId } } })))) id = void 0;
|
|
297
|
+
}
|
|
298
|
+
if (id === void 0) {
|
|
299
|
+
const matchId = (yield* call(() => client.GET("/v1/environment-variables", { params: { query: {
|
|
300
|
+
projectId: news.projectId,
|
|
301
|
+
class: cls,
|
|
302
|
+
key: news.key
|
|
303
|
+
} } }))).data?.[0]?.id;
|
|
304
|
+
if (matchId !== void 0) {
|
|
305
|
+
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.`);
|
|
306
|
+
id = matchId;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (id !== void 0) {
|
|
310
|
+
const targetId = id;
|
|
311
|
+
yield* call(() => client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
312
|
+
params: { path: { envVarId: targetId } },
|
|
313
|
+
body: { value: news.value }
|
|
314
|
+
}));
|
|
315
|
+
return {
|
|
316
|
+
id,
|
|
317
|
+
key: news.key
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const created = yield* call(() => client.POST("/v1/environment-variables", { body: {
|
|
321
|
+
projectId: news.projectId,
|
|
322
|
+
class: cls,
|
|
323
|
+
key: news.key,
|
|
324
|
+
value: news.value,
|
|
325
|
+
...news.branchId ? { branchId: news.branchId } : {}
|
|
326
|
+
} }));
|
|
327
|
+
return {
|
|
328
|
+
id: created.data.id,
|
|
329
|
+
key: created.data.key
|
|
330
|
+
};
|
|
331
|
+
}),
|
|
332
|
+
delete: Effect.fn(function* ({ output }) {
|
|
333
|
+
yield* callVoid(() => client.DELETE("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: output.id } } }));
|
|
334
|
+
}),
|
|
335
|
+
read: Effect.fn(function* ({ output }) {
|
|
336
|
+
if (!output?.id) return void 0;
|
|
337
|
+
const v = yield* callOptional(() => client.GET("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: output.id } } }));
|
|
338
|
+
return v ? {
|
|
339
|
+
id: v.data.id,
|
|
340
|
+
key: v.data.key
|
|
341
|
+
} : void 0;
|
|
342
|
+
})
|
|
343
|
+
};
|
|
344
|
+
}));
|
|
345
|
+
/**
|
|
346
|
+
* Assembles a Prisma Compute artifact: the app-built bundle plus the
|
|
347
|
+
* extension-printed bootstrap and manifest, tarred and gzipped deterministically
|
|
348
|
+
* (fixed mtimes, sorted entry order) so an unchanged service noops on
|
|
349
|
+
* redeploy — a rebuild is the only thing that changes the hash. Lives here
|
|
350
|
+
* (not in @prisma/composer-prisma-cloud/control) because it needs node:fs/node:zlib,
|
|
351
|
+
* which the extension's shipped src may never import (invariant 5).
|
|
352
|
+
*/
|
|
353
|
+
const MANIFEST_VERSION = "1";
|
|
354
|
+
/** Finds main.js/main.mjs in a bundle dir when no explicit entry is given. */
|
|
355
|
+
function resolveEntry(bundleDir, entry) {
|
|
356
|
+
if (entry !== void 0) return entry;
|
|
357
|
+
const found = fs.readdirSync(bundleDir).find((f) => /^main\.m?js$/.test(f));
|
|
358
|
+
if (found === void 0) throw new Error(`no main.js/main.mjs found in bundle dir ${bundleDir}`);
|
|
359
|
+
return found;
|
|
360
|
+
}
|
|
361
|
+
/** All files under `dir`, as dir-relative POSIX paths, in sorted order. A
|
|
362
|
+
* symlink is a hard error: deploy bundles must be flat (ADR-0005), and the
|
|
363
|
+
* user's build owns flattening — dereferencing here would relink the tree and
|
|
364
|
+
* risk packaging files from outside it. */
|
|
365
|
+
function walkFiles(dir) {
|
|
366
|
+
const out = [];
|
|
367
|
+
const visit = (sub) => {
|
|
368
|
+
for (const entry of fs.readdirSync(path.join(dir, sub), { withFileTypes: true })) {
|
|
369
|
+
const rel = sub.length > 0 ? `${sub}/${entry.name}` : entry.name;
|
|
370
|
+
if (entry.isSymbolicLink()) throw new Error(`bundle contains a symlink at ${rel} — deploy bundles must be flat; materialize links in your build (e.g. cp -RL) so the tree is self-contained.`);
|
|
371
|
+
if (entry.isDirectory()) visit(rel);
|
|
372
|
+
else out.push(rel);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
visit("");
|
|
376
|
+
return out.sort();
|
|
377
|
+
}
|
|
378
|
+
function octal(value, length) {
|
|
379
|
+
return `${value.toString(8).padStart(length - 1, "0")}\0`;
|
|
380
|
+
}
|
|
381
|
+
/** Splits a path into ustar's name (<=100 bytes) + prefix (<=155 bytes) fields. */
|
|
382
|
+
function splitUstarPath(relPath) {
|
|
383
|
+
if (Buffer.byteLength(relPath, "utf8") <= 100) return {
|
|
384
|
+
name: relPath,
|
|
385
|
+
prefix: ""
|
|
386
|
+
};
|
|
387
|
+
for (let i = relPath.length - 1; i >= 0; i--) {
|
|
388
|
+
if (relPath[i] !== "/") continue;
|
|
389
|
+
const prefix = relPath.slice(0, i);
|
|
390
|
+
const name = relPath.slice(i + 1);
|
|
391
|
+
if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
|
|
392
|
+
name,
|
|
393
|
+
prefix
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`path too long for a ustar tar entry: ${relPath}`);
|
|
397
|
+
}
|
|
398
|
+
function ustarHeader(relPath, size) {
|
|
399
|
+
const { name, prefix } = splitUstarPath(relPath);
|
|
400
|
+
const buf = Buffer.alloc(512);
|
|
401
|
+
buf.write(name, 0, 100, "utf8");
|
|
402
|
+
buf.write(octal(420, 8), 100, 8, "utf8");
|
|
403
|
+
buf.write(octal(0, 8), 108, 8, "utf8");
|
|
404
|
+
buf.write(octal(0, 8), 116, 8, "utf8");
|
|
405
|
+
buf.write(octal(size, 12), 124, 12, "utf8");
|
|
406
|
+
buf.write(octal(0, 12), 136, 12, "utf8");
|
|
407
|
+
buf.write(" ", 148, 8, "utf8");
|
|
408
|
+
buf.write("0", 156, 1, "utf8");
|
|
409
|
+
buf.write("ustar\0", 257, 6, "utf8");
|
|
410
|
+
buf.write("00", 263, 2, "utf8");
|
|
411
|
+
buf.write(prefix, 345, 155, "utf8");
|
|
412
|
+
let sum = 0;
|
|
413
|
+
for (const b of buf) sum += b;
|
|
414
|
+
buf.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "utf8");
|
|
415
|
+
return buf;
|
|
416
|
+
}
|
|
417
|
+
function createDeterministicTarGz(entries) {
|
|
418
|
+
const sorted = [...entries].sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
419
|
+
const chunks = [];
|
|
420
|
+
for (const entry of sorted) {
|
|
421
|
+
chunks.push(ustarHeader(entry.relPath, entry.content.length));
|
|
422
|
+
chunks.push(entry.content);
|
|
423
|
+
const pad = (512 - entry.content.length % 512) % 512;
|
|
424
|
+
if (pad > 0) chunks.push(Buffer.alloc(pad));
|
|
425
|
+
}
|
|
426
|
+
chunks.push(Buffer.alloc(1024));
|
|
427
|
+
return zlib.gzipSync(Buffer.concat(chunks));
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Prints the bootstrap + manifest and tars them with the bundle into a
|
|
431
|
+
* deterministic artifact. If bundleDir doesn't exist (e.g. `alchemy destroy`
|
|
432
|
+
* run before any build), returns a placeholder rather than throwing — the
|
|
433
|
+
* artifact is never read on destroy.
|
|
434
|
+
*/
|
|
435
|
+
function packageComputeArtifact(opts) {
|
|
436
|
+
if (!fs.existsSync(opts.bundleDir)) return {
|
|
437
|
+
path: "",
|
|
438
|
+
sha256: "absent"
|
|
439
|
+
};
|
|
440
|
+
const bootstrap = `import main from "./${resolveEntry(opts.bundleDir, opts.bundleEntry)}";\nawait main.run(${JSON.stringify(opts.address)}, () => import(${JSON.stringify(`./${opts.appEntry}`)}));\n`;
|
|
441
|
+
const manifest = `${JSON.stringify({
|
|
442
|
+
manifestVersion: MANIFEST_VERSION,
|
|
443
|
+
entrypoint: "bootstrap.js",
|
|
444
|
+
address: opts.address
|
|
445
|
+
}, null, 2)}\n`;
|
|
446
|
+
const files = walkFiles(opts.bundleDir).map((relPath) => ({
|
|
447
|
+
relPath,
|
|
448
|
+
content: fs.readFileSync(path.join(opts.bundleDir, relPath))
|
|
449
|
+
}));
|
|
450
|
+
files.push({
|
|
451
|
+
relPath: "bootstrap.js",
|
|
452
|
+
content: Buffer.from(bootstrap, "utf8")
|
|
453
|
+
});
|
|
454
|
+
files.push({
|
|
455
|
+
relPath: "compute.manifest.json",
|
|
456
|
+
content: Buffer.from(manifest, "utf8")
|
|
457
|
+
});
|
|
458
|
+
files.push({
|
|
459
|
+
relPath: "bunfig.toml",
|
|
460
|
+
content: Buffer.from("[install]\nauto = \"disable\"\n", "utf8")
|
|
461
|
+
});
|
|
462
|
+
const gz = createDeterministicTarGz(files);
|
|
463
|
+
const sha256 = crypto$1.createHash("sha256").update(gz).digest("hex");
|
|
464
|
+
const outDir = path.join(os$1.tmpdir(), `prisma-composer-compute-${String(os$1.userInfo().uid)}`, sha256.slice(0, 16));
|
|
465
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
466
|
+
const outPath = path.join(outDir, `${opts.id}.tar.gz`);
|
|
467
|
+
const tmpPath = path.join(outDir, `.${opts.id}.${crypto$1.randomUUID()}.tmp`);
|
|
468
|
+
fs.writeFileSync(tmpPath, gz);
|
|
469
|
+
fs.renameSync(tmpPath, outPath);
|
|
470
|
+
return {
|
|
471
|
+
path: outPath,
|
|
472
|
+
sha256
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* The `ServiceKey` Alchemy resource (ADR-0030) — mints a random 256-bit key
|
|
477
|
+
* ONCE at create and keeps it STABLE across deploys, so an unchanged edge
|
|
478
|
+
* no-ops on redeploy. Same mint-once-stable lifecycle as `S3Credentials`
|
|
479
|
+
* (`packages/1-prisma-cloud/1-extensions/target/src/s3-credentials-resource.ts`):
|
|
480
|
+
* Web Crypto only (`crypto.getRandomValues` — no `node:` import), persisted in
|
|
481
|
+
* Alchemy state; on every later apply the provider returns the persisted
|
|
482
|
+
* attributes (`reconcile`'s `output`) unchanged. One resource per RPC edge;
|
|
483
|
+
* rotation is destroy/recreate.
|
|
484
|
+
*/
|
|
485
|
+
/** The `ServiceKey` resource constructor — `yield* Prisma.ServiceKey(id, {})` in the lowering. */
|
|
486
|
+
const ServiceKey = Resource("PrismaCloud.ServiceKey");
|
|
487
|
+
/** A fresh 256-bit key as 64 lowercase hex chars (Web Crypto — no node import). */
|
|
488
|
+
function mintServiceKey() {
|
|
489
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
|
|
490
|
+
return { value: Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("") };
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The `ServiceKey` provider service. `reconcile` runs for create and update;
|
|
494
|
+
* it returns the persisted `output` when present (a redeploy reuses the
|
|
495
|
+
* stored key — the no-op property) and mints a fresh key only on first
|
|
496
|
+
* create. Nothing to enumerate (`list` → `[]`) or tear down (`delete` →
|
|
497
|
+
* no-op; the key lives only in state).
|
|
498
|
+
*/
|
|
499
|
+
const serviceKeyProviderService = {
|
|
500
|
+
list: () => Effect.succeed([]),
|
|
501
|
+
reconcile: ({ output }) => Effect.sync(() => output ?? mintServiceKey()),
|
|
502
|
+
delete: () => Effect.void
|
|
503
|
+
};
|
|
504
|
+
/** The `ServiceKey` provider layer — merged into the Prisma Cloud extension's `providers()`. */
|
|
505
|
+
const ServiceKeyProvider = () => Provider.effect(ServiceKey, Effect.succeed(serviceKeyProviderService));
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/postgres.mjs
|
|
508
|
+
/** A **connection** to a Prisma Postgres database — yields the connection string. */
|
|
509
|
+
const Connection = Resource("Prisma.Connection");
|
|
510
|
+
const ConnectionProvider = () => Provider.effect(Connection, Effect.gen(function* () {
|
|
511
|
+
const client = yield* ManagementClient;
|
|
512
|
+
return {
|
|
513
|
+
stables: ["id", "connectionString"],
|
|
514
|
+
list: () => Effect.succeed([]),
|
|
515
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
516
|
+
if (output?.id) return output;
|
|
517
|
+
const created = yield* call(() => client.POST("/v1/databases/{databaseId}/connections", {
|
|
518
|
+
params: { path: { databaseId: news.databaseId } },
|
|
519
|
+
body: { name: news.name }
|
|
520
|
+
}));
|
|
521
|
+
const endpoints = created.data.endpoints;
|
|
522
|
+
const dsn = endpoints?.direct?.connectionString ?? endpoints?.pooled?.connectionString;
|
|
523
|
+
if (dsn === void 0) return yield* Effect.fail(new PrismaApiError({
|
|
524
|
+
status: 0,
|
|
525
|
+
message: `connection ${created.data.id} returned no direct/pooled connection string`
|
|
526
|
+
}));
|
|
527
|
+
return {
|
|
528
|
+
id: created.data.id,
|
|
529
|
+
connectionString: Redacted.make(dsn)
|
|
530
|
+
};
|
|
531
|
+
}),
|
|
532
|
+
delete: Effect.fn(function* ({ output }) {
|
|
533
|
+
yield* callVoid(() => client.DELETE("/v1/connections/{id}", { params: { path: { id: output.id } } }));
|
|
534
|
+
})
|
|
535
|
+
};
|
|
536
|
+
}));
|
|
537
|
+
/** A Prisma **Postgres database** inside a project. */
|
|
538
|
+
const Database = Resource("Prisma.Database");
|
|
539
|
+
const DatabaseProvider = () => Provider.effect(Database, Effect.gen(function* () {
|
|
540
|
+
const client = yield* ManagementClient;
|
|
541
|
+
return {
|
|
542
|
+
stables: ["id"],
|
|
543
|
+
list: () => Effect.succeed([]),
|
|
544
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
545
|
+
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/databases/{databaseId}", { params: { path: { databaseId: output.id } } })) : void 0;
|
|
546
|
+
if (!observed) {
|
|
547
|
+
const created = yield* call(() => client.POST("/v1/databases", { body: {
|
|
548
|
+
projectId: news.projectId,
|
|
549
|
+
name: news.name,
|
|
550
|
+
region: news.region,
|
|
551
|
+
...news.isDefault !== void 0 && { isDefault: news.isDefault },
|
|
552
|
+
...news.branchId !== void 0 && { branchId: news.branchId }
|
|
553
|
+
} }));
|
|
554
|
+
return {
|
|
555
|
+
id: created.data.id,
|
|
556
|
+
name: created.data.name
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const result = {
|
|
560
|
+
id: observed.data.id,
|
|
561
|
+
name: observed.data.name
|
|
562
|
+
};
|
|
563
|
+
if (news.branchId !== void 0) {
|
|
564
|
+
const branchId = news.branchId;
|
|
565
|
+
yield* call(() => client.PATCH("/v1/databases/{databaseId}", {
|
|
566
|
+
params: { path: { databaseId: result.id } },
|
|
567
|
+
body: { branchId }
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
return result;
|
|
571
|
+
}),
|
|
572
|
+
delete: Effect.fn(function* ({ output }) {
|
|
573
|
+
yield* callVoid(() => client.DELETE("/v1/databases/{databaseId}", { params: { path: { databaseId: output.id } } }));
|
|
574
|
+
}),
|
|
575
|
+
read: Effect.fn(function* ({ output }) {
|
|
576
|
+
if (!output?.id) return void 0;
|
|
577
|
+
const d = yield* callOptional(() => client.GET("/v1/databases/{databaseId}", { params: { path: { databaseId: output.id } } }));
|
|
578
|
+
return d ? {
|
|
579
|
+
id: d.data.id,
|
|
580
|
+
name: d.data.name
|
|
581
|
+
} : void 0;
|
|
582
|
+
})
|
|
583
|
+
};
|
|
584
|
+
}));
|
|
585
|
+
/** A Prisma Developer Platform **Project** — the container for databases and compute services. */
|
|
586
|
+
const Project = Resource("Prisma.Project");
|
|
587
|
+
const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
588
|
+
const client = yield* ManagementClient;
|
|
589
|
+
return {
|
|
590
|
+
stables: ["id"],
|
|
591
|
+
list: () => Effect.succeed([]),
|
|
592
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
593
|
+
const observed = output?.id ? yield* callOptional(() => client.GET("/v1/projects/{id}", { params: { path: { id: output.id } } })) : void 0;
|
|
594
|
+
if (observed) return {
|
|
595
|
+
id: observed.data.id,
|
|
596
|
+
name: observed.data.name
|
|
597
|
+
};
|
|
598
|
+
const created = yield* call(() => client.POST("/v1/projects", { body: {
|
|
599
|
+
name: news.name,
|
|
600
|
+
workspaceId: news.workspaceId
|
|
601
|
+
} }));
|
|
602
|
+
return {
|
|
603
|
+
id: created.data.id,
|
|
604
|
+
name: created.data.name
|
|
605
|
+
};
|
|
606
|
+
}),
|
|
607
|
+
delete: Effect.fn(function* ({ output }) {
|
|
608
|
+
yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: output.id } } }));
|
|
609
|
+
}),
|
|
610
|
+
read: Effect.fn(function* ({ output }) {
|
|
611
|
+
if (!output?.id) return void 0;
|
|
612
|
+
const p = yield* callOptional(() => client.GET("/v1/projects/{id}", { params: { path: { id: output.id } } }));
|
|
613
|
+
return p ? {
|
|
614
|
+
id: p.data.id,
|
|
615
|
+
name: p.data.name
|
|
616
|
+
} : void 0;
|
|
617
|
+
})
|
|
618
|
+
};
|
|
619
|
+
}));
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
|
|
622
|
+
/** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
|
|
623
|
+
var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
|
|
624
|
+
const listAllProjects = (client) => Effect.gen(function* () {
|
|
625
|
+
const projects = [];
|
|
626
|
+
let cursor;
|
|
627
|
+
for (;;) {
|
|
628
|
+
const query = cursor === void 0 ? {} : { cursor };
|
|
629
|
+
const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
|
|
630
|
+
projects.push(...page.data);
|
|
631
|
+
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
632
|
+
cursor = page.pagination.nextCursor;
|
|
633
|
+
}
|
|
634
|
+
return projects;
|
|
635
|
+
});
|
|
636
|
+
/**
|
|
637
|
+
* Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
|
|
638
|
+
* bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
|
|
639
|
+
* one (the same normalization `state/bootstrap.ts` applies to the same
|
|
640
|
+
* `/v1/projects` listing).
|
|
641
|
+
*/
|
|
642
|
+
const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
|
|
643
|
+
/**
|
|
644
|
+
* Finds the app's Project by name in the workspace — PDP allows duplicate
|
|
645
|
+
* project names, so more than one can match; the oldest wins. Creates one
|
|
646
|
+
* if none match, unless `ensure` is `false` (find-only — `destroy`), in
|
|
647
|
+
* which case an absent Project fails with `ContainerNotFoundError`. No
|
|
648
|
+
* ownership marker and no `--project` override (both deferred — see
|
|
649
|
+
* ADR-0019).
|
|
650
|
+
*/
|
|
651
|
+
const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
|
|
652
|
+
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];
|
|
653
|
+
if (oldest !== void 0) return oldest.id;
|
|
654
|
+
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
|
|
655
|
+
return (yield* call(() => client.POST("/v1/projects", { body: {
|
|
656
|
+
name: appName,
|
|
657
|
+
workspaceId
|
|
658
|
+
} }))).data.id;
|
|
659
|
+
});
|
|
660
|
+
const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
661
|
+
path: { projectId },
|
|
662
|
+
query: { gitName }
|
|
663
|
+
} })).pipe(Effect.map((page) => page.data[0]?.id));
|
|
664
|
+
/**
|
|
665
|
+
* Finds the stage's Branch by its exact `gitName`, creating it if absent
|
|
666
|
+
* unless `ensure` is `false` (find-only — `destroy`), in which case an
|
|
667
|
+
* absent Branch fails with `ContainerNotFoundError`. The Management API has
|
|
668
|
+
* no server-side "create-or-return" idempotency (`POST
|
|
669
|
+
* /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
|
|
670
|
+
* field to make that a no-op), so idempotency is client-side: observe
|
|
671
|
+
* first, and on a racing 409 from create, re-observe rather than fail.
|
|
672
|
+
*/
|
|
673
|
+
const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
|
|
674
|
+
const existing = yield* findBranchId(client, projectId, gitName);
|
|
675
|
+
if (existing !== void 0) return existing;
|
|
676
|
+
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
|
|
677
|
+
appName,
|
|
678
|
+
stage: gitName
|
|
679
|
+
}));
|
|
680
|
+
return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
|
|
681
|
+
params: { path: { projectId } },
|
|
682
|
+
body: { gitName }
|
|
683
|
+
})).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)));
|
|
684
|
+
});
|
|
685
|
+
/**
|
|
686
|
+
* Resolves the two containers a stage's deploy runs into (ADR-0019): the
|
|
687
|
+
* app's **Project**, found-or-created by name, and — for a named stage
|
|
688
|
+
* only — its **Branch**, found-or-created by `gitName`. The default stage
|
|
689
|
+
* (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
|
|
690
|
+
* false` (`destroy`), nothing is created — an absent Project or Branch
|
|
691
|
+
* fails with `ContainerNotFoundError` instead.
|
|
692
|
+
*/
|
|
693
|
+
const resolveContainer = (opts) => Effect.gen(function* () {
|
|
694
|
+
const client = yield* ManagementClient;
|
|
695
|
+
const ensure = opts.ensure ?? true;
|
|
696
|
+
const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
|
|
697
|
+
if (opts.stage === void 0) return { projectId };
|
|
698
|
+
return {
|
|
699
|
+
projectId,
|
|
700
|
+
branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
|
|
701
|
+
};
|
|
702
|
+
});
|
|
703
|
+
/**
|
|
704
|
+
* Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
|
|
705
|
+
* the Branch still has live members or is the production/default Branch —
|
|
706
|
+
* that surfaces as a `PrismaApiError`.
|
|
707
|
+
*/
|
|
708
|
+
const deleteBranch = (branchId) => Effect.gen(function* () {
|
|
709
|
+
const client = yield* ManagementClient;
|
|
710
|
+
yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
|
|
711
|
+
});
|
|
712
|
+
/**
|
|
713
|
+
* Deletes a Project. Tolerates a 404 (already gone). The API refuses with a
|
|
714
|
+
* 400 if the Project still has live dependencies (e.g. another stage's
|
|
715
|
+
* Branch/resources) — that surfaces as a `PrismaApiError`.
|
|
716
|
+
*/
|
|
717
|
+
const deleteProject = (projectId) => Effect.gen(function* () {
|
|
718
|
+
const client = yield* ManagementClient;
|
|
719
|
+
yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: projectId } } }));
|
|
720
|
+
});
|
|
721
|
+
/** The collection of Prisma resource providers. */
|
|
722
|
+
var Providers = class extends Provider.ProviderCollection()("Prisma") {};
|
|
723
|
+
/**
|
|
724
|
+
* The Prisma provider bundle: every resource provider, the Management API
|
|
725
|
+
* client, and env-based credentials. Plug into a stack with
|
|
726
|
+
* `{ providers: Prisma.providers() }`.
|
|
727
|
+
*/
|
|
728
|
+
const providers = () => Layer.effect(Providers, Provider.collection([
|
|
729
|
+
Project,
|
|
730
|
+
Database,
|
|
731
|
+
Connection,
|
|
732
|
+
ComputeService,
|
|
733
|
+
Deployment,
|
|
734
|
+
EnvironmentVariable,
|
|
735
|
+
Bucket,
|
|
736
|
+
BucketKey
|
|
737
|
+
])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DatabaseProvider(), ConnectionProvider(), ComputeServiceProvider(), DeploymentProvider(), EnvironmentVariableProvider(), BucketProvider(), BucketKeyProvider())), Layer.provideMerge(layer()), Layer.provideMerge(fromEnv()), Layer.orDie);
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region ../../1-prisma-cloud/0-lowering/s3-protocol/dist/index.mjs
|
|
740
|
+
/**
|
|
741
|
+
* AWS SigV4 verification for the S3 wire protocol (spec § 2 auth). The payload
|
|
742
|
+
* hash comes from the client — `x-amz-content-sha256` (a real hash or
|
|
743
|
+
* `UNSIGNED-PAYLOAD`) for header auth, `UNSIGNED-PAYLOAD` for presign — and is
|
|
744
|
+
* never re-hashed; the verifier trusts what was signed, like a real S3 endpoint.
|
|
745
|
+
* Runtime engine code (`node:crypto`); not re-exported from the authoring barrel.
|
|
746
|
+
*
|
|
747
|
+
* Also owns `mintKeyPair` (local-dev spec § 1) — the one key-pair generator
|
|
748
|
+
* both the deploy-time `S3Credentials` resource and the local dev bucket
|
|
749
|
+
* emulator's credential provisioning use.
|
|
750
|
+
*/
|
|
751
|
+
function randomBytes$1(n) {
|
|
752
|
+
return crypto.getRandomValues(new Uint8Array(n));
|
|
753
|
+
}
|
|
754
|
+
function toHexUpper(bytes) {
|
|
755
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
756
|
+
}
|
|
757
|
+
/** A fresh SigV4 key pair: an AKIA-prefixed id and a 40-char base64 secret. */
|
|
758
|
+
function mintKeyPair() {
|
|
759
|
+
return {
|
|
760
|
+
accessKeyId: `AKIA${toHexUpper(randomBytes$1(8))}`,
|
|
761
|
+
secretAccessKey: btoa(String.fromCharCode(...randomBytes$1(30)))
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region ../../1-prisma-cloud/1-extensions/target/dist/preflight-names-BPL-qsZF.mjs
|
|
766
|
+
const PRISMA_CLOUD_EXTENSION_ID = "@prisma/composer-prisma-cloud";
|
|
767
|
+
var PrismaCloudContainer = class {
|
|
768
|
+
input;
|
|
769
|
+
projectId;
|
|
770
|
+
branchId;
|
|
771
|
+
constructor(input, projectId, branchId) {
|
|
772
|
+
this.input = input;
|
|
773
|
+
this.projectId = projectId;
|
|
774
|
+
this.branchId = branchId;
|
|
775
|
+
}
|
|
776
|
+
serialize() {
|
|
777
|
+
return JSON.stringify({
|
|
778
|
+
input: this.input,
|
|
779
|
+
projectId: this.projectId,
|
|
780
|
+
...this.branchId !== void 0 ? { branchId: this.branchId } : {}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
/** `instanceof` — parent-side instances and child-side deserialized instances are both constructed by this module. */
|
|
785
|
+
function isPrismaCloudContainer(value) {
|
|
786
|
+
return value instanceof PrismaCloudContainer;
|
|
787
|
+
}
|
|
788
|
+
/** Narrow-or-throw for hook inputs. */
|
|
789
|
+
function prismaCloudContainerOf(value) {
|
|
790
|
+
if (!isPrismaCloudContainer(value)) throw new Error("the Prisma Cloud container was not resolved — the extension's container descriptor did not run.");
|
|
791
|
+
return value;
|
|
792
|
+
}
|
|
793
|
+
function isRecord(value) {
|
|
794
|
+
return typeof value === "object" && value !== null;
|
|
795
|
+
}
|
|
796
|
+
function invalidPayloadError(reason) {
|
|
797
|
+
return /* @__PURE__ */ new Error(`${PRISMA_CLOUD_EXTENSION_ID}: invalid container transport payload — ${reason}.`);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Reconstructs a `PrismaCloudContainer` from `serialize()`'s JSON output —
|
|
801
|
+
* real narrowing, no casts. Exported so `dev/container.ts`'s
|
|
802
|
+
* `devContainerDescriptor` can reuse it verbatim (local-dev spec § 5) — the
|
|
803
|
+
* dev and deploy container descriptors deserialize the identical wire shape.
|
|
804
|
+
*/
|
|
805
|
+
function deserialize(serialized) {
|
|
806
|
+
let parsed;
|
|
807
|
+
try {
|
|
808
|
+
parsed = JSON.parse(serialized);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
throw invalidPayloadError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
811
|
+
}
|
|
812
|
+
if (!isRecord(parsed)) throw invalidPayloadError("not an object");
|
|
813
|
+
const input = parsed["input"];
|
|
814
|
+
if (!isRecord(input)) throw invalidPayloadError("\"input\" is not an object");
|
|
815
|
+
const appName = input["appName"];
|
|
816
|
+
if (typeof appName !== "string") throw invalidPayloadError("\"input.appName\" is not a string");
|
|
817
|
+
const stage = input["stage"];
|
|
818
|
+
if (stage !== void 0 && typeof stage !== "string") throw invalidPayloadError("\"input.stage\" is not a string or absent");
|
|
819
|
+
const projectId = parsed["projectId"];
|
|
820
|
+
if (typeof projectId !== "string") throw invalidPayloadError("\"projectId\" is not a string");
|
|
821
|
+
const branchId = parsed["branchId"];
|
|
822
|
+
if (branchId !== void 0 && typeof branchId !== "string") throw invalidPayloadError("\"branchId\" is not a string or absent");
|
|
823
|
+
return new PrismaCloudContainer({
|
|
824
|
+
appName,
|
|
825
|
+
stage
|
|
826
|
+
}, projectId, branchId);
|
|
827
|
+
}
|
|
828
|
+
const workspaceRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_WORKSPACE_ID is required.");
|
|
829
|
+
const tokenRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required.");
|
|
830
|
+
function requireWorkspaceId() {
|
|
831
|
+
const workspaceId = process.env["PRISMA_WORKSPACE_ID"];
|
|
832
|
+
if (workspaceId === void 0 || workspaceId.length === 0) throw workspaceRequiredError();
|
|
833
|
+
return workspaceId;
|
|
834
|
+
}
|
|
835
|
+
function requireTokenUnlessInjected(deps) {
|
|
836
|
+
if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError();
|
|
837
|
+
}
|
|
838
|
+
async function ensureContainer(input, deps) {
|
|
839
|
+
const workspaceId = requireWorkspaceId();
|
|
840
|
+
requireTokenUnlessInjected(deps);
|
|
841
|
+
const program = resolveContainer({
|
|
842
|
+
workspaceId,
|
|
843
|
+
appName: input.appName,
|
|
844
|
+
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
845
|
+
ensure: true
|
|
846
|
+
}).pipe(Effect.map((c) => ({
|
|
847
|
+
ok: true,
|
|
848
|
+
container: c
|
|
849
|
+
})), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
850
|
+
ok: false,
|
|
851
|
+
message: `Prisma Management API error resolving containers: ${e.message}.`
|
|
852
|
+
})));
|
|
853
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
854
|
+
const outcome = await Effect.runPromise(provided);
|
|
855
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
856
|
+
return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
|
|
857
|
+
}
|
|
858
|
+
async function locateContainer(input, deps) {
|
|
859
|
+
const workspaceId = requireWorkspaceId();
|
|
860
|
+
requireTokenUnlessInjected(deps);
|
|
861
|
+
const program = resolveContainer({
|
|
862
|
+
workspaceId,
|
|
863
|
+
appName: input.appName,
|
|
864
|
+
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
865
|
+
ensure: false
|
|
866
|
+
}).pipe(Effect.map((c) => ({
|
|
867
|
+
ok: true,
|
|
868
|
+
container: c
|
|
869
|
+
})), Effect.catchTag("ContainerNotFoundError", () => Effect.succeed({ ok: false })), Effect.catchTag("PrismaApiError", (e) => Effect.fail(/* @__PURE__ */ new Error(`Prisma Management API error resolving containers: ${e.message}.`))));
|
|
870
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
871
|
+
const outcome = await Effect.runPromise(provided);
|
|
872
|
+
if (!outcome.ok) return void 0;
|
|
873
|
+
return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Soft-deletes a named stage's Branch after a successful `alchemy destroy`
|
|
877
|
+
* has removed its members — the Management API refuses to delete a Branch
|
|
878
|
+
* that still has live members.
|
|
879
|
+
*/
|
|
880
|
+
async function removeStageBranch(branchId, deps) {
|
|
881
|
+
requireTokenUnlessInjected(deps);
|
|
882
|
+
const program = deleteBranch(branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
883
|
+
ok: false,
|
|
884
|
+
message: `Failed to delete the stage Branch: ${e.message}.`
|
|
885
|
+
})));
|
|
886
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
887
|
+
const outcome = await Effect.runPromise(provided);
|
|
888
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Best-effort cleanup after a successful `--production` destroy: removes
|
|
892
|
+
* the app's Project so hand-run stacks don't accumulate as empty Projects
|
|
893
|
+
* (they eventually hit the workspace's plan limit). Unlike `removeStageBranch`,
|
|
894
|
+
* this never throws: the destroy itself already succeeded, and the API's own
|
|
895
|
+
* 400 ("still has dependencies") is the only check that matters — failing
|
|
896
|
+
* the command over a cleanup step would be worse than leaving a Project shell.
|
|
897
|
+
*/
|
|
898
|
+
async function removeAppProject(projectId, deps) {
|
|
899
|
+
if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) {
|
|
900
|
+
console.warn(`Skipped removing the Project (${projectId}): PRISMA_SERVICE_TOKEN is not set.`);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const program = deleteProject(projectId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
904
|
+
ok: false,
|
|
905
|
+
error: e
|
|
906
|
+
})));
|
|
907
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
908
|
+
const outcome = await Effect.runPromise(provided);
|
|
909
|
+
if (outcome.ok) {
|
|
910
|
+
console.log(`Removed the Project (${projectId}) — nothing was left in it.`);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (outcome.error.status === 400) {
|
|
914
|
+
console.log(`Kept the Project (${projectId}) — it still has another stage's resources.`);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
console.warn(`Could not remove the Project (${projectId}) after destroy: ${outcome.error.message}.`);
|
|
918
|
+
}
|
|
919
|
+
function containerDescriptor(deps) {
|
|
920
|
+
return {
|
|
921
|
+
ensure: (input) => ensureContainer(input, deps),
|
|
922
|
+
locate: (input) => locateContainer(input, deps),
|
|
923
|
+
remove: (instance) => instance.input.stage !== void 0 ? removeStageBranch(instance.branchId ?? missingBranchId(instance), deps) : removeAppProject(instance.projectId, deps),
|
|
924
|
+
deserialize
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/** Defensive: a named-stage container always resolves a Branch together with its stage — `ensure`/`locate`/`deserialize` never produce one without the other. */
|
|
928
|
+
function missingBranchId(instance) {
|
|
929
|
+
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.`);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* The `PgWarm` Alchemy resource (slice 3, FT-5226) — warm a freshly-provisioned
|
|
933
|
+
* Prisma Postgres database at apply-time so it is ready by deploy-end, and the
|
|
934
|
+
* first real connection (a service's runtime client, or the migration) doesn't
|
|
935
|
+
* eat the cold-start reject.
|
|
936
|
+
*
|
|
937
|
+
* The DB `url` is a lazy `Output` at lowering time, so warming must be an
|
|
938
|
+
* apply-time tracked resource (same pattern as `PnMigration`): its `reconcile`
|
|
939
|
+
* receives the RESOLVED url and connects with `withConnectionRetry` + `select 1`,
|
|
940
|
+
* riding out the cold-start. Shared by BOTH the bare-`postgres` and the
|
|
941
|
+
* `prisma-next` lowerings; keyed on the connection `url`, so an unchanged
|
|
942
|
+
* redeploy is a no-op (warming is idempotent anyway).
|
|
943
|
+
*
|
|
944
|
+
* Deploy-time only: imports `pg` directly + `alchemy`. Imported by `control.ts`
|
|
945
|
+
* and tests, never by `index.ts` / the `./prisma-next` authoring entry — the
|
|
946
|
+
* isolation invariants hold.
|
|
947
|
+
*/
|
|
948
|
+
/** The `PgWarm` resource constructor — `yield* PgWarm(id, { url })` in a lowering. */
|
|
949
|
+
const PgWarm = Resource("PrismaCloud.PgWarm");
|
|
950
|
+
/** Connect (retrying the cold-start) and run `select 1`, then release the connection. */
|
|
951
|
+
async function warmDatabase(url) {
|
|
952
|
+
await withConnectionRetry(async () => {
|
|
953
|
+
const client = new pg.Client({ connectionString: normalizeSslMode(url) });
|
|
954
|
+
await client.connect();
|
|
955
|
+
try {
|
|
956
|
+
await client.query("select 1");
|
|
957
|
+
} finally {
|
|
958
|
+
await client.end();
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* The `PgWarm` provider service. `reconcile` warms the DB (retrying the
|
|
964
|
+
* cold-start) and echoes the `url` so a downstream resource that reads
|
|
965
|
+
* `warm.url` runs only after the DB is warm. Idempotent — safe on redeploy;
|
|
966
|
+
* nothing to enumerate (`list` → `[]`) or tear down (`delete` → no-op; the DB's
|
|
967
|
+
* own deletion handles teardown). Exported so tests can drive it directly.
|
|
968
|
+
*/
|
|
969
|
+
const pgWarmProviderService = {
|
|
970
|
+
list: () => Effect.succeed([]),
|
|
971
|
+
reconcile: ({ news }) => Effect.tryPromise({
|
|
972
|
+
try: () => warmDatabase(news.url),
|
|
973
|
+
catch: (error) => error
|
|
974
|
+
}).pipe(Effect.map(() => ({ url: news.url }))),
|
|
975
|
+
delete: () => Effect.void
|
|
976
|
+
};
|
|
977
|
+
/** The `PgWarm` provider layer — merged into the extension descriptor's `providers()`. */
|
|
978
|
+
const PgWarmProvider = () => Provider.effect(PgWarm, Effect.succeed(pgWarmProviderService));
|
|
979
|
+
/**
|
|
980
|
+
* The Prisma Next migration step of the deploy lowering (ADR-0022, slice 2) —
|
|
981
|
+
* the safety-critical decision that brings a live database to a target REF
|
|
982
|
+
* using ONLY Prisma Next's authored migrations.
|
|
983
|
+
*
|
|
984
|
+
* Deploy-time only: this module imports `@prisma-next/postgres/control` (which
|
|
985
|
+
* transitively pulls PN's control/migration machinery + `pg`). It is imported
|
|
986
|
+
* by the deploy descriptors and this package's tests, NEVER by `index.ts` / the
|
|
987
|
+
* `./prisma-next` authoring entry — so it never lands in an app runtime bundle
|
|
988
|
+
* (the index-isolation invariant holds).
|
|
989
|
+
*
|
|
990
|
+
* The target is a ref `{ hash, invariants }` — not a bare `storageHash`. A
|
|
991
|
+
* ref's `invariants` are named postconditions established by `data`-class
|
|
992
|
+
* migration steps (e.g. a backfill), recorded monotonically on the live
|
|
993
|
+
* marker. Keying on the hash alone would make a pure data-invariant change an
|
|
994
|
+
* A→A self-edge the deploy wrongly skips. The decision, given the live marker
|
|
995
|
+
* and the target ref (see {@link decideMigrationAction}):
|
|
996
|
+
* - marker at ref.hash AND ref.invariants ⊆ marker.invariants → no-op
|
|
997
|
+
* - no marker (fresh DB) AND no required invariants → `dbInit`
|
|
998
|
+
* - otherwise → `migrate`
|
|
999
|
+
*
|
|
1000
|
+
* `dbInit` is additive-only synthesis — it NEVER runs app-space data steps —
|
|
1001
|
+
* so it is only correct when the ref requires no invariants; a fresh DB whose
|
|
1002
|
+
* target carries invariants goes through `migrate`, which walks the AUTHORED
|
|
1003
|
+
* graph (including the invariant-bearing data migrations) from empty.
|
|
1004
|
+
*
|
|
1005
|
+
* Never `dbUpdate`: synthesized diff-and-apply plans are never run against a
|
|
1006
|
+
* deployed database. A no-authored-path (`MIGRATION_PATH_NOT_FOUND`) or a
|
|
1007
|
+
* runner failure fails the deploy as a typed `PnMigrationError` (not swallowed).
|
|
1008
|
+
* PN applies each migration in its own transaction, so a failed apply is atomic
|
|
1009
|
+
* and resume-safe — the marker and schema are left as the last committed step.
|
|
1010
|
+
*/
|
|
1011
|
+
/** A deploy-failing migration error — surfaced, never swallowed. */
|
|
1012
|
+
var PnMigrationError = class extends Error {
|
|
1013
|
+
code;
|
|
1014
|
+
/** PN's structured explanation, when present. */
|
|
1015
|
+
why;
|
|
1016
|
+
constructor(code, summary, why) {
|
|
1017
|
+
super(`prisma-next migrate (${code}): ${summary}`);
|
|
1018
|
+
this.name = "PnMigrationError";
|
|
1019
|
+
this.code = code;
|
|
1020
|
+
this.why = why;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
/**
|
|
1024
|
+
* The target `storageHash` a contract heads to — `contractJson.storage.storageHash`.
|
|
1025
|
+
* Read defensively: `contractJson` crosses the boundary as `unknown`.
|
|
1026
|
+
*/
|
|
1027
|
+
function targetStorageHash(contractJson) {
|
|
1028
|
+
if (typeof contractJson === "object" && contractJson !== null && "storage" in contractJson) {
|
|
1029
|
+
const storage = contractJson.storage;
|
|
1030
|
+
if (typeof storage === "object" && storage !== null && "storageHash" in storage) {
|
|
1031
|
+
const hash = storage.storageHash;
|
|
1032
|
+
if (typeof hash === "string" && hash.length > 0) return hash;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
throw new PnMigrationError("INIT_FAILED", "the contract has no storage.storageHash — cannot determine the target schema version");
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Resolve the deploy's target ref from the migrations dir.
|
|
1039
|
+
*
|
|
1040
|
+
* - `targetRef` named: read `migrations/app/refs/<name>.json` — fail loudly
|
|
1041
|
+
* (`TARGET_REF_NOT_FOUND`) when the ref doesn't exist or can't be parsed.
|
|
1042
|
+
* - Default: the app space's head. PN synthesizes the app head from the
|
|
1043
|
+
* emitted contract — `{ hash: contract.storage.storageHash, invariants: [] }`
|
|
1044
|
+
* (`contract emit` writes no app-space `refs/head.json` today; extension
|
|
1045
|
+
* spaces have one on disk). When a future PN version does emit one, the
|
|
1046
|
+
* on-disk `head.json` wins — read via `readContractSpaceHeadRef`, exactly
|
|
1047
|
+
* the loader PN's own migrate uses.
|
|
1048
|
+
*/
|
|
1049
|
+
async function resolveTargetRef(migrationsDir, contractJson, targetRef) {
|
|
1050
|
+
if (targetRef !== void 0) {
|
|
1051
|
+
const refsDir = spaceRefsDirectory(spaceMigrationDirectory(migrationsDir, APP_SPACE_ID));
|
|
1052
|
+
try {
|
|
1053
|
+
const ref = await readRef(refsDir, targetRef);
|
|
1054
|
+
return {
|
|
1055
|
+
hash: ref.hash,
|
|
1056
|
+
invariants: ref.invariants
|
|
1057
|
+
};
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
throw new PnMigrationError("TARGET_REF_NOT_FOUND", `targetRef "${targetRef}" could not be read from ${refsDir}`, error instanceof Error ? error.message : String(error));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const head = await readContractSpaceHeadRef(migrationsDir, APP_SPACE_ID);
|
|
1063
|
+
if (head !== null) return {
|
|
1064
|
+
hash: head.hash,
|
|
1065
|
+
invariants: head.invariants
|
|
1066
|
+
};
|
|
1067
|
+
return {
|
|
1068
|
+
hash: targetStorageHash(contractJson),
|
|
1069
|
+
invariants: []
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* The pure migration decision, mirroring PN's own verifier: the database is
|
|
1074
|
+
* AT the target when the marker's hash equals the ref's hash AND every ref
|
|
1075
|
+
* invariant is on the marker (marker invariants are monotonic). `dbInit` is
|
|
1076
|
+
* additive-only synthesis, so it is chosen only for a fresh DB whose
|
|
1077
|
+
* effective required invariants (`ref.invariants − marker.invariants`) are
|
|
1078
|
+
* empty; anything else — different hash, missing invariant (the A→A
|
|
1079
|
+
* data-only self-edge), or a fresh DB with required invariants — walks the
|
|
1080
|
+
* authored graph via `migrate`.
|
|
1081
|
+
*/
|
|
1082
|
+
function decideMigrationAction(marker, ref) {
|
|
1083
|
+
const markerInvariants = new Set(marker?.invariants ?? []);
|
|
1084
|
+
const missing = ref.invariants.filter((id) => !markerInvariants.has(id));
|
|
1085
|
+
if (marker !== null && marker.storageHash === ref.hash && missing.length === 0) return "noop";
|
|
1086
|
+
if (marker === null && missing.length === 0) return "init";
|
|
1087
|
+
return "migrate";
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Bring the database at `url` to the target ref via PN's authored migrations.
|
|
1091
|
+
* Reads the live marker, decides no-op / init / migrate
|
|
1092
|
+
* ({@link decideMigrationAction}), applies, and throws a typed
|
|
1093
|
+
* {@link PnMigrationError} on a no-path or runner failure. `migrationsDir` is
|
|
1094
|
+
* the on-disk migrations root and `ref` the resolved target
|
|
1095
|
+
* ({@link resolveTargetRef} — both resolved by the lowering, which also keys
|
|
1096
|
+
* the PnMigration resource on them). `refName` (the resource's `targetRef`,
|
|
1097
|
+
* when set) is threaded into `migrate` so PN targets the named ref's hash and
|
|
1098
|
+
* plans an invariant-bearing path.
|
|
1099
|
+
*/
|
|
1100
|
+
async function applyPnMigration(opts) {
|
|
1101
|
+
const connection = normalizeSslMode(opts.url);
|
|
1102
|
+
return withConnectionRetry(() => runMigration(connection, opts.contractJson, opts.migrationsDir, opts.ref, opts.refName), { shouldRetry: (error) => !(error instanceof PnMigrationError) });
|
|
1103
|
+
}
|
|
1104
|
+
async function runMigration(connection, contractJson, migrationsDir, ref, refName) {
|
|
1105
|
+
const client = createPostgresControlClient({ connection });
|
|
1106
|
+
await client.connect();
|
|
1107
|
+
try {
|
|
1108
|
+
const marker = await client.readMarker();
|
|
1109
|
+
const markerHashBefore = marker?.storageHash ?? null;
|
|
1110
|
+
const action = decideMigrationAction(marker, ref);
|
|
1111
|
+
if (action === "noop") return {
|
|
1112
|
+
action,
|
|
1113
|
+
targetHash: ref.hash,
|
|
1114
|
+
markerHashBefore
|
|
1115
|
+
};
|
|
1116
|
+
if (action === "init") {
|
|
1117
|
+
const result = await client.dbInit({
|
|
1118
|
+
contract: contractJson,
|
|
1119
|
+
mode: "apply",
|
|
1120
|
+
migrationsDir
|
|
1121
|
+
});
|
|
1122
|
+
if (!result.ok) throw new PnMigrationError("INIT_FAILED", result.failure.summary, result.failure.why);
|
|
1123
|
+
return {
|
|
1124
|
+
action,
|
|
1125
|
+
targetHash: ref.hash,
|
|
1126
|
+
markerHashBefore
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const result = await client.migrate({
|
|
1130
|
+
contract: contractJson,
|
|
1131
|
+
migrationsDir,
|
|
1132
|
+
...refName !== void 0 ? {
|
|
1133
|
+
refHash: ref.hash,
|
|
1134
|
+
refInvariants: ref.invariants,
|
|
1135
|
+
refName
|
|
1136
|
+
} : {}
|
|
1137
|
+
});
|
|
1138
|
+
if (!result.ok) throw new PnMigrationError(result.failure.code === "MIGRATION_PATH_NOT_FOUND" ? "MIGRATION_PATH_NOT_FOUND" : "RUNNER_FAILED", result.failure.summary, result.failure.why);
|
|
1139
|
+
return {
|
|
1140
|
+
action,
|
|
1141
|
+
targetHash: ref.hash,
|
|
1142
|
+
markerHashBefore
|
|
1143
|
+
};
|
|
1144
|
+
} finally {
|
|
1145
|
+
await client.close();
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* The `PnMigration` Alchemy resource (ADR-0022, slice 2 D2) — the migration
|
|
1150
|
+
* step modeled as a tracked resource so it participates in deploy state: keyed
|
|
1151
|
+
* on the target REF identity (`targetHash` + sorted `invariants`), an
|
|
1152
|
+
* unchanged redeploy is an Alchemy-level no-op (on top of the marker read),
|
|
1153
|
+
* and a contract change — or a DATA-ONLY change that adds a ref invariant at
|
|
1154
|
+
* the same hash — re-runs the migration.
|
|
1155
|
+
*
|
|
1156
|
+
* Its provider's `reconcile` receives the RESOLVED props at apply-time — in
|
|
1157
|
+
* particular the concrete DB `url` (a lazy `Output` until the Connection
|
|
1158
|
+
* provisions) — and delegates to the proven `applyPnMigration` decision. The
|
|
1159
|
+
* provider is a standalone `Provider<PnMigration>` layer; the extension
|
|
1160
|
+
* descriptor merges it into its `providers()` (`Layer.merge(Prisma.providers(),
|
|
1161
|
+
* PnMigrationProvider())`), and Alchemy resolves it at apply via a direct
|
|
1162
|
+
* provider-tag lookup (`tryFindProviderByType`) — no change to `@internal/lowering`.
|
|
1163
|
+
*
|
|
1164
|
+
* Deploy-time only: imports `@prisma-next/postgres/control` (via the helper) +
|
|
1165
|
+
* `alchemy`. Imported by `control.ts` and tests, never by `index.ts` / the
|
|
1166
|
+
* `./prisma-next` authoring entry — index isolation holds.
|
|
1167
|
+
*/
|
|
1168
|
+
/** The `PnMigration` resource constructor — `yield* PnMigration(id, props)` in the lowering. */
|
|
1169
|
+
const PnMigration = Resource("PrismaNext.Migration");
|
|
1170
|
+
/**
|
|
1171
|
+
* The `PnMigration` provider service. `reconcile` runs for both create and
|
|
1172
|
+
* update (Alchemy's unified lifecycle); `applyPnMigration` is idempotent via
|
|
1173
|
+
* the live marker read, so it is safe to run for either — the marker decides
|
|
1174
|
+
* no-op / init / migrate. A migration has nothing to enumerate (`list` → `[]`)
|
|
1175
|
+
* and nothing to tear down on its own (`delete` → no-op; the DB's own deletion
|
|
1176
|
+
* handles teardown). Exported so tests can drive `reconcile` directly, without
|
|
1177
|
+
* building an Effect layer.
|
|
1178
|
+
*/
|
|
1179
|
+
const pnMigrationProviderService = {
|
|
1180
|
+
list: () => Effect.succeed([]),
|
|
1181
|
+
reconcile: ({ news }) => Effect.tryPromise({
|
|
1182
|
+
try: () => applyPnMigration({
|
|
1183
|
+
url: news.url,
|
|
1184
|
+
contractJson: news.contractJson,
|
|
1185
|
+
migrationsDir: news.migrationsDir,
|
|
1186
|
+
ref: {
|
|
1187
|
+
hash: news.targetHash,
|
|
1188
|
+
invariants: news.invariants
|
|
1189
|
+
},
|
|
1190
|
+
...news.refName !== void 0 ? { refName: news.refName } : {}
|
|
1191
|
+
}),
|
|
1192
|
+
catch: (error) => error
|
|
1193
|
+
}).pipe(Effect.map((outcome) => ({
|
|
1194
|
+
storageHash: outcome.targetHash,
|
|
1195
|
+
invariants: news.invariants
|
|
1196
|
+
}))),
|
|
1197
|
+
delete: () => Effect.void
|
|
1198
|
+
};
|
|
1199
|
+
/** The `PnMigration` provider layer — merged into the extension descriptor's `providers()`. */
|
|
1200
|
+
const PnMigrationProvider = () => Provider.effect(PnMigration, Effect.succeed(pnMigrationProviderService));
|
|
1201
|
+
/**
|
|
1202
|
+
* The `S3Credentials` Alchemy resource (S5) — mints a random SigV4 key pair
|
|
1203
|
+
* ONCE at create and keeps it STABLE across deploys, so an unchanged module
|
|
1204
|
+
* no-ops on redeploy. The pair is generated by `@internal/s3-protocol`'s
|
|
1205
|
+
* `mintKeyPair` (moved there per the local-dev spec § 1, so deploy and local
|
|
1206
|
+
* dev credential minting share one implementation) and persisted in Alchemy
|
|
1207
|
+
* state; on every later apply the provider returns the persisted attributes
|
|
1208
|
+
* (`reconcile`'s `output`) unchanged — the same way the postgres resource
|
|
1209
|
+
* keeps a Connection stable. Rotation is destroy/recreate (a platform ask,
|
|
1210
|
+
* not solved here).
|
|
1211
|
+
*
|
|
1212
|
+
* Deploy-time only: imports `alchemy`. Imported by `control.ts` and tests,
|
|
1213
|
+
* never by `index.ts` / the authoring entry.
|
|
1214
|
+
*/
|
|
1215
|
+
/** The `S3Credentials` resource constructor — `yield* S3Credentials(id, {})` in the lowering. */
|
|
1216
|
+
const S3Credentials = Resource("PrismaCloud.S3Credentials");
|
|
1217
|
+
/**
|
|
1218
|
+
* The `S3Credentials` provider service. `reconcile` runs for create and update;
|
|
1219
|
+
* it returns the persisted `output` when present (a redeploy reuses the stored
|
|
1220
|
+
* pair — the no-op property) and mints a fresh pair only on first create.
|
|
1221
|
+
* Nothing to enumerate (`list` → `[]`) or tear down (`delete` → no-op; the pair
|
|
1222
|
+
* lives only in state). Exported so tests can drive it directly.
|
|
1223
|
+
*/
|
|
1224
|
+
const s3CredentialsProviderService = {
|
|
1225
|
+
list: () => Effect.succeed([]),
|
|
1226
|
+
reconcile: ({ output }) => Effect.sync(() => output ?? mintKeyPair()),
|
|
1227
|
+
delete: () => Effect.void
|
|
1228
|
+
};
|
|
1229
|
+
/** The `S3Credentials` provider layer — merged into the extension descriptor's `providers()`. */
|
|
1230
|
+
const S3CredentialsProvider = () => Provider.effect(S3Credentials, Effect.succeed(s3CredentialsProviderService));
|
|
1231
|
+
function dedupedNames(entries) {
|
|
1232
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1233
|
+
for (const entry of entries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
1234
|
+
return [...byName.values()];
|
|
1235
|
+
}
|
|
1236
|
+
/** Walks `graph.secrets`/`graph.params` exactly as `runPreflight` did before this extraction. */
|
|
1237
|
+
function collectPreflightNames(graph) {
|
|
1238
|
+
return {
|
|
1239
|
+
secrets: dedupedNames(provisionManifest(graph).map((binding) => ({
|
|
1240
|
+
name: secretName(binding),
|
|
1241
|
+
serviceAddress: binding.serviceAddress
|
|
1242
|
+
}))),
|
|
1243
|
+
envParams: dedupedNames(paramManifest(graph).filter((binding) => isEnvParamSource(binding.binding)).map((binding) => ({
|
|
1244
|
+
name: paramName(binding),
|
|
1245
|
+
serviceAddress: binding.serviceAddress
|
|
1246
|
+
})))
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
//#endregion
|
|
1250
|
+
export { PrismaApiError as A, EnvironmentVariable as C, Bucket as D, packageComputeArtifact as E, callVoid as M, fromEnv as N, BucketKey as O, layer as P, Deployment as S, ServiceKeyProvider as T, Connection as _, PnMigrationProvider as a, COMPUTE_REGIONS as b, S3CredentialsProvider as c, deserialize as d, prismaCloudContainerOf as f, providers as g, Providers as h, PnMigration as i, call as j, ManagementClient as k, collectPreflightNames as l, mintKeyPair as m, PgWarm as n, PrismaCloudContainer as o, resolveTargetRef as p, PgWarmProvider as r, S3Credentials as s, PRISMA_CLOUD_EXTENSION_ID as t, containerDescriptor as u, Database as v, ServiceKey as w, ComputeService as x, Project as y };
|
|
1251
|
+
|
|
1252
|
+
//# sourceMappingURL=preflight-names-BPL-qsZF-BfoRgbgb.mjs.map
|