@prisma/composer-prisma-cloud 0.30.0-dev.2 → 0.30.0-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/pack.mjs +1 -1
- package/dist/auth/pack.mjs.map +1 -1
- package/dist/auth/testing.mjs +1 -1
- package/dist/auth/testing.mjs.map +1 -1
- package/dist/buckets-main.mjs +102 -82
- package/dist/buckets-main.mjs.map +1 -1
- package/dist/compute-main.mjs +243 -231
- package/dist/compute-main.mjs.map +1 -1
- package/dist/control.mjs +3 -3
- package/dist/control.mjs.map +1 -1
- package/dist/{segments-BW2SYdBf-CtmcRzwy.mjs → daemon-D9BV062Q-ZTmWho_N.mjs} +2 -11
- package/dist/daemon-D9BV062Q-ZTmWho_N.mjs.map +1 -0
- package/dist/local-target.mjs +726 -317
- package/dist/local-target.mjs.map +1 -1
- package/dist/postgres-main.mjs +325 -193
- package/dist/postgres-main.mjs.map +1 -1
- package/dist/{s3-credentials-resource-BZjtuopJ-D0fKtjEH.mjs → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs} +50 -10
- package/dist/{s3-credentials-resource-BZjtuopJ-D0fKtjEH.mjs.map → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs.map} +1 -1
- package/package.json +18 -18
- package/dist/segments-BW2SYdBf-CtmcRzwy.mjs.map +0 -1
package/dist/local-target.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as blindCast$1 } from "./casts-DpaahrlC-Ykus3kCE.mjs";
|
|
2
|
-
import { E as
|
|
2
|
+
import { C as Providers, E as ServiceKeyProvider, O as isWithin, S as mintKeyPair, b as serviceAppName, c as PgWarmProvider, d as S3CredentialsProvider, f as collectPreflightNames, g as prismaCloudContainerOf, l as PrismaCloudContainer, m as deserialize, n as GeneratedParamProvider, r as LocalOrmMigrationProvider, y as resourceName } from "./s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { blindCast } from "@prisma/composer/casts";
|
|
5
5
|
import * as Effect from "effect/Effect";
|
|
@@ -1564,7 +1564,7 @@ var require_adapter = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
1564
1564
|
};
|
|
1565
1565
|
}));
|
|
1566
1566
|
//#endregion
|
|
1567
|
-
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/
|
|
1567
|
+
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/daemon-D9BV062Q.mjs
|
|
1568
1568
|
var import_proper_lockfile = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1569
1569
|
const lockfile = require_lockfile();
|
|
1570
1570
|
const { toPromise, toSync, toSyncOptions } = require_adapter();
|
|
@@ -2050,48 +2050,6 @@ async function ensureDaemon(name, entry, opts = {}) {
|
|
|
2050
2050
|
await release();
|
|
2051
2051
|
}
|
|
2052
2052
|
}
|
|
2053
|
-
/**
|
|
2054
|
-
* API hygiene shared by both daemons (local-dev spec § 2): every `<app>`,
|
|
2055
|
-
* `<id>`, and `<name>` path segment must match this shape or the request is
|
|
2056
|
-
* a 400 naming the segment.
|
|
2057
|
-
*/
|
|
2058
|
-
const SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
2059
|
-
function isValidSegment(segment) {
|
|
2060
|
-
return segment.length <= 63 && SEGMENT_RE.test(segment);
|
|
2061
|
-
}
|
|
2062
|
-
//#endregion
|
|
2063
|
-
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/instance-name-BF6J_weL.mjs
|
|
2064
|
-
/**
|
|
2065
|
-
* `pcdev-<app>-<database-id>` instance-name derivation (local-dev spec § 2
|
|
2066
|
-
* `postgres-main.ts`): each half lowercased, every char outside `[a-z0-9]`
|
|
2067
|
-
* replaced by `-`, runs collapsed, the combined name trimmed to 63 chars.
|
|
2068
|
-
*
|
|
2069
|
-
* A separate, side-effect-free module (not defined inline in
|
|
2070
|
-
* `postgres-main.ts`) so it can be imported directly by tests without also
|
|
2071
|
-
* running that file's own `main()` — `postgres-main.ts` is a daemon
|
|
2072
|
-
* entrypoint script, always invoked as a subprocess, and calls `main()`
|
|
2073
|
-
* unconditionally at module load.
|
|
2074
|
-
*/
|
|
2075
|
-
/**
|
|
2076
|
-
* Deliberately linear, no ambiguous quantifiers: a per-character replace
|
|
2077
|
-
* (no `+`, so no run-length backtracking surface), a bounded-quantifier
|
|
2078
|
-
* collapse (`{2,}`, not alternation-with-quantifiers), and plain
|
|
2079
|
-
* index-walking for the leading/trailing trim instead of a regex — a
|
|
2080
|
-
* combined `+`-plus-alternation trim (`/^-+|-+$/g`) is exactly the shape
|
|
2081
|
-
* CodeQL's polynomial-ReDoS check flags, whether or not this particular
|
|
2082
|
-
* instance is provably safe.
|
|
2083
|
-
*/
|
|
2084
|
-
function slug(segment) {
|
|
2085
|
-
const collapsed = segment.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-{2,}/g, "-");
|
|
2086
|
-
let start = 0;
|
|
2087
|
-
let end = collapsed.length;
|
|
2088
|
-
while (start < end && collapsed[start] === "-") start++;
|
|
2089
|
-
while (end > start && collapsed[end - 1] === "-") end--;
|
|
2090
|
-
return collapsed.slice(start, end);
|
|
2091
|
-
}
|
|
2092
|
-
function instanceNameFor(app, id) {
|
|
2093
|
-
return `pcdev-${slug(app)}-${slug(id)}`.slice(0, 63);
|
|
2094
|
-
}
|
|
2095
2053
|
//#endregion
|
|
2096
2054
|
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/index.mjs
|
|
2097
2055
|
/**
|
|
@@ -2124,29 +2082,28 @@ function resolveBaseUrl(name, opts) {
|
|
|
2124
2082
|
if (!isRegistryEntry(parsed) || !isPidAlive(parsed.pid)) throw notRunningError(name);
|
|
2125
2083
|
return `http://127.0.0.1:${parsed.port}`;
|
|
2126
2084
|
}
|
|
2127
|
-
function encodeSegment(segment) {
|
|
2128
|
-
if (!isValidSegment(segment)) throw new Error(`invalid path segment "${segment}"`);
|
|
2129
|
-
return encodeURIComponent(segment);
|
|
2130
|
-
}
|
|
2131
2085
|
const ADMIN_FETCH_ATTEMPTS = 3;
|
|
2132
2086
|
const ADMIN_FETCH_RETRY_DELAY_MS = 150;
|
|
2133
2087
|
/**
|
|
2134
2088
|
* `fetch` for the loopback admin calls, retried on transient socket errors:
|
|
2135
2089
|
* a keep-alive reuse race (the runtime re-uses a pooled connection the
|
|
2136
2090
|
* daemon's HTTP server just closed idle — surfaces as "fetch failed" /
|
|
2137
|
-
* "other side closed") or a briefly overloaded daemon.
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2091
|
+
* "other side closed") or a briefly overloaded daemon. A short blind retry is
|
|
2092
|
+
* safe only for an idempotent call; a call that is not, such as creating an
|
|
2093
|
+
* instance, passes `retry: false`, because the emulator may have handled the
|
|
2094
|
+
* first attempt. A caller-driven abort is never retried. When every attempt
|
|
2095
|
+
* fails to connect, it throws `DEV.EMULATOR_UNREACHABLE`; callers retry that
|
|
2096
|
+
* code and nothing else.
|
|
2141
2097
|
*/
|
|
2142
|
-
async function adminFetch(url, init) {
|
|
2098
|
+
async function adminFetch(url, init, opts = {}) {
|
|
2099
|
+
const attempts = opts.retry === false ? 1 : ADMIN_FETCH_ATTEMPTS;
|
|
2143
2100
|
let lastError;
|
|
2144
|
-
for (let attempt = 1; attempt <=
|
|
2101
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) try {
|
|
2145
2102
|
return await fetch(url, init);
|
|
2146
2103
|
} catch (error) {
|
|
2147
2104
|
if (init?.signal?.aborted === true) throw error;
|
|
2148
2105
|
lastError = error;
|
|
2149
|
-
if (attempt <
|
|
2106
|
+
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, ADMIN_FETCH_RETRY_DELAY_MS));
|
|
2150
2107
|
}
|
|
2151
2108
|
throw new CliStructuredError("DEV.EMULATOR_UNREACHABLE", `Could not connect to the local dev emulator at ${new URL(url).origin}: ${lastError instanceof Error ? lastError.message : String(lastError)}`, {
|
|
2152
2109
|
why: "The emulator is registered and its process is alive, but it did not accept the connection.",
|
|
@@ -2162,23 +2119,33 @@ async function expectOk(res) {
|
|
|
2162
2119
|
}
|
|
2163
2120
|
return res;
|
|
2164
2121
|
}
|
|
2165
|
-
const
|
|
2122
|
+
const INSTANCE_STATUSES = /* @__PURE__ */ new Set([
|
|
2166
2123
|
"running",
|
|
2167
2124
|
"backoff",
|
|
2168
2125
|
"held",
|
|
2169
2126
|
"stopped"
|
|
2170
2127
|
]);
|
|
2171
|
-
function
|
|
2172
|
-
return typeof value === "object" && value !== null &&
|
|
2128
|
+
function isStringRecord$1(value) {
|
|
2129
|
+
return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
|
|
2173
2130
|
}
|
|
2174
|
-
function
|
|
2175
|
-
return
|
|
2131
|
+
function isServiceInstanceInfo(value) {
|
|
2132
|
+
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord$1(value.labels) && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string" && "status" in value && typeof value.status === "string" && INSTANCE_STATUSES.has(value.status) && "logPath" in value && typeof value.logPath === "string";
|
|
2133
|
+
}
|
|
2134
|
+
function isServiceInstanceInfoArray(value) {
|
|
2135
|
+
return Array.isArray(value) && value.every(isServiceInstanceInfo);
|
|
2176
2136
|
}
|
|
2177
2137
|
function isHealthBody(value) {
|
|
2178
2138
|
return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
|
|
2179
2139
|
}
|
|
2180
|
-
function
|
|
2181
|
-
return
|
|
2140
|
+
function instancePath(baseUrl, id) {
|
|
2141
|
+
return `${baseUrl}/instances/${encodeURIComponent(id)}`;
|
|
2142
|
+
}
|
|
2143
|
+
function postJson(body) {
|
|
2144
|
+
return {
|
|
2145
|
+
method: "POST",
|
|
2146
|
+
headers: { "content-type": "application/json" },
|
|
2147
|
+
body: JSON.stringify(body)
|
|
2148
|
+
};
|
|
2182
2149
|
}
|
|
2183
2150
|
function computeClient(opts = {}) {
|
|
2184
2151
|
const baseUrl = resolveBaseUrl("compute", opts);
|
|
@@ -2189,26 +2156,33 @@ function computeClient(opts = {}) {
|
|
|
2189
2156
|
if (!isHealthBody(body)) throw new Error("malformed /health response from the compute emulator");
|
|
2190
2157
|
return body;
|
|
2191
2158
|
},
|
|
2192
|
-
async
|
|
2193
|
-
const body = await (await expectOk(await adminFetch(`${baseUrl}/
|
|
2194
|
-
if (!
|
|
2159
|
+
async createInstance(request) {
|
|
2160
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
|
|
2161
|
+
if (!isServiceInstanceInfo(body)) throw new Error("malformed instance-create response from the compute emulator");
|
|
2162
|
+
return body;
|
|
2163
|
+
},
|
|
2164
|
+
async listInstances() {
|
|
2165
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
|
|
2166
|
+
if (!isServiceInstanceInfoArray(body)) throw new Error("malformed instances listing from the compute emulator");
|
|
2195
2167
|
return body;
|
|
2196
2168
|
},
|
|
2197
|
-
async
|
|
2198
|
-
|
|
2169
|
+
async getInstance(id) {
|
|
2170
|
+
const res = await adminFetch(instancePath(baseUrl, id));
|
|
2171
|
+
if (res.status === 404) return void 0;
|
|
2172
|
+
const body = await (await expectOk(res)).json();
|
|
2173
|
+
if (!isServiceInstanceInfo(body)) throw new Error("malformed instance response from the compute emulator");
|
|
2174
|
+
return body;
|
|
2175
|
+
},
|
|
2176
|
+
async putDeployment(id, deployment) {
|
|
2177
|
+
await expectOk(await adminFetch(`${instancePath(baseUrl, id)}/deployment`, {
|
|
2199
2178
|
method: "PUT",
|
|
2200
2179
|
headers: { "content-type": "application/json" },
|
|
2201
2180
|
body: JSON.stringify(deployment)
|
|
2202
2181
|
}));
|
|
2203
2182
|
},
|
|
2204
|
-
async
|
|
2205
|
-
const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services`))).json();
|
|
2206
|
-
if (!isServiceInfoArray(body)) throw new Error("malformed services listing from the compute emulator");
|
|
2207
|
-
return body;
|
|
2208
|
-
},
|
|
2209
|
-
async *followLogs(app, id, signal, opts) {
|
|
2183
|
+
async *followLogs(id, signal, opts) {
|
|
2210
2184
|
const tail = opts?.tail !== void 0 ? `&tail=${String(Math.max(0, Math.trunc(opts.tail)))}` : "";
|
|
2211
|
-
const body = (await expectOk(await adminFetch(`${baseUrl
|
|
2185
|
+
const body = (await expectOk(await adminFetch(`${instancePath(baseUrl, id)}/logs?follow=1${tail}`, signal ? { signal } : void 0))).body;
|
|
2212
2186
|
if (!body) return;
|
|
2213
2187
|
const reader = body.getReader();
|
|
2214
2188
|
const decoder = new TextDecoder();
|
|
@@ -2222,19 +2196,26 @@ function computeClient(opts = {}) {
|
|
|
2222
2196
|
await reader.cancel().catch(() => void 0);
|
|
2223
2197
|
}
|
|
2224
2198
|
},
|
|
2225
|
-
async
|
|
2226
|
-
await expectOk(await adminFetch(`${baseUrl}/
|
|
2199
|
+
async startInstances(ids) {
|
|
2200
|
+
await expectOk(await adminFetch(`${baseUrl}/instances/start`, postJson({ ids })));
|
|
2227
2201
|
},
|
|
2228
|
-
async
|
|
2229
|
-
await expectOk(await adminFetch(`${baseUrl}/
|
|
2202
|
+
async stopInstances(ids) {
|
|
2203
|
+
await expectOk(await adminFetch(`${baseUrl}/instances/stop`, postJson({ ids })));
|
|
2230
2204
|
},
|
|
2231
|
-
async
|
|
2232
|
-
await expectOk(await adminFetch(
|
|
2205
|
+
async deleteInstance(id) {
|
|
2206
|
+
await expectOk(await adminFetch(instancePath(baseUrl, id), { method: "DELETE" }));
|
|
2233
2207
|
}
|
|
2234
2208
|
};
|
|
2235
2209
|
}
|
|
2210
|
+
function isBucketInstanceInfo(value) {
|
|
2211
|
+
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord$1(value.labels) && "dir" in value && typeof value.dir === "string";
|
|
2212
|
+
}
|
|
2213
|
+
function isBucketInstanceInfoArray(value) {
|
|
2214
|
+
return Array.isArray(value) && value.every(isBucketInstanceInfo);
|
|
2215
|
+
}
|
|
2236
2216
|
function bucketsClient(opts = {}) {
|
|
2237
2217
|
const baseUrl = resolveBaseUrl("buckets", opts);
|
|
2218
|
+
const adminUrl = `${baseUrl}/_pcdev/instances`;
|
|
2238
2219
|
return {
|
|
2239
2220
|
baseUrl,
|
|
2240
2221
|
async health() {
|
|
@@ -2242,15 +2223,25 @@ function bucketsClient(opts = {}) {
|
|
|
2242
2223
|
if (!isHealthBody(body)) throw new Error("malformed /_pcdev/health response from the buckets emulator");
|
|
2243
2224
|
return body;
|
|
2244
2225
|
},
|
|
2245
|
-
async
|
|
2246
|
-
await expectOk(await adminFetch(
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2226
|
+
async createInstance(request) {
|
|
2227
|
+
const body = await (await expectOk(await adminFetch(adminUrl, postJson(request), { retry: false }))).json();
|
|
2228
|
+
if (!isBucketInstanceInfo(body)) throw new Error("malformed instance-create response from the buckets emulator");
|
|
2229
|
+
return body;
|
|
2230
|
+
},
|
|
2231
|
+
async listInstances() {
|
|
2232
|
+
const body = await (await expectOk(await adminFetch(adminUrl))).json();
|
|
2233
|
+
if (!isBucketInstanceInfoArray(body)) throw new Error("malformed instances listing from the buckets emulator");
|
|
2234
|
+
return body;
|
|
2251
2235
|
},
|
|
2252
|
-
async
|
|
2253
|
-
|
|
2236
|
+
async getInstance(id) {
|
|
2237
|
+
const res = await adminFetch(instancePath(`${baseUrl}/_pcdev`, id));
|
|
2238
|
+
if (res.status === 404) return void 0;
|
|
2239
|
+
const body = await (await expectOk(res)).json();
|
|
2240
|
+
if (!isBucketInstanceInfo(body)) throw new Error("malformed instance response from the buckets emulator");
|
|
2241
|
+
return body;
|
|
2242
|
+
},
|
|
2243
|
+
async putCredentials(id, accessKeyId, secretAccessKey) {
|
|
2244
|
+
await expectOk(await adminFetch(`${instancePath(`${baseUrl}/_pcdev`, id)}/credentials`, {
|
|
2254
2245
|
method: "PUT",
|
|
2255
2246
|
headers: { "content-type": "application/json" },
|
|
2256
2247
|
body: JSON.stringify({
|
|
@@ -2259,19 +2250,16 @@ function bucketsClient(opts = {}) {
|
|
|
2259
2250
|
})
|
|
2260
2251
|
}));
|
|
2261
2252
|
},
|
|
2262
|
-
async
|
|
2263
|
-
await expectOk(await adminFetch(`${baseUrl}/_pcdev
|
|
2253
|
+
async deleteInstance(id) {
|
|
2254
|
+
await expectOk(await adminFetch(instancePath(`${baseUrl}/_pcdev`, id), { method: "DELETE" }));
|
|
2264
2255
|
}
|
|
2265
2256
|
};
|
|
2266
2257
|
}
|
|
2267
|
-
function
|
|
2268
|
-
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "
|
|
2269
|
-
}
|
|
2270
|
-
function isDatabaseInfoArray(value) {
|
|
2271
|
-
return Array.isArray(value) && value.every(isDatabaseInfo);
|
|
2258
|
+
function isDatabaseInstanceInfo(value) {
|
|
2259
|
+
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord$1(value.labels) && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string" && "status" in value && (value.status === "running" || value.status === "stopped");
|
|
2272
2260
|
}
|
|
2273
|
-
function
|
|
2274
|
-
return
|
|
2261
|
+
function isDatabaseInstanceInfoArray(value) {
|
|
2262
|
+
return Array.isArray(value) && value.every(isDatabaseInstanceInfo);
|
|
2275
2263
|
}
|
|
2276
2264
|
function postgresClient(opts = {}) {
|
|
2277
2265
|
const baseUrl = resolveBaseUrl("postgres", opts);
|
|
@@ -2282,22 +2270,40 @@ function postgresClient(opts = {}) {
|
|
|
2282
2270
|
if (!isHealthBody(body)) throw new Error("malformed /health response from the postgres emulator");
|
|
2283
2271
|
return body;
|
|
2284
2272
|
},
|
|
2285
|
-
async
|
|
2286
|
-
const body = await (await expectOk(await adminFetch(`${baseUrl}/
|
|
2287
|
-
|
|
2288
|
-
headers: { "content-type": "application/json" },
|
|
2289
|
-
body: JSON.stringify({ prismaDevModulePath })
|
|
2290
|
-
}))).json();
|
|
2291
|
-
if (!isEnsuredDatabase(body)) throw new Error("malformed database-ensure response from the postgres emulator");
|
|
2273
|
+
async createInstance(request) {
|
|
2274
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
|
|
2275
|
+
if (!isDatabaseInstanceInfo(body)) throw new Error("malformed instance-create response from the postgres emulator");
|
|
2292
2276
|
return body;
|
|
2293
2277
|
},
|
|
2294
|
-
async
|
|
2295
|
-
const body = await (await expectOk(await adminFetch(`${baseUrl}/
|
|
2296
|
-
if (!
|
|
2278
|
+
async listInstances() {
|
|
2279
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
|
|
2280
|
+
if (!isDatabaseInstanceInfoArray(body)) throw new Error("malformed instances listing from the postgres emulator");
|
|
2297
2281
|
return body;
|
|
2298
2282
|
},
|
|
2299
|
-
async
|
|
2300
|
-
|
|
2283
|
+
async getInstance(id) {
|
|
2284
|
+
const res = await adminFetch(instancePath(baseUrl, id));
|
|
2285
|
+
if (res.status === 404) return void 0;
|
|
2286
|
+
const body = await (await expectOk(res)).json();
|
|
2287
|
+
if (!isDatabaseInstanceInfo(body)) throw new Error("malformed instance response from the postgres emulator");
|
|
2288
|
+
return body;
|
|
2289
|
+
},
|
|
2290
|
+
async startInstances(ids, prismaDevModulePath) {
|
|
2291
|
+
await expectOk(await adminFetch(`${baseUrl}/instances/start`, postJson({
|
|
2292
|
+
ids,
|
|
2293
|
+
prismaDevModulePath
|
|
2294
|
+
})));
|
|
2295
|
+
},
|
|
2296
|
+
async stopInstances(ids) {
|
|
2297
|
+
await expectOk(await adminFetch(`${baseUrl}/instances/stop`, postJson({ ids })));
|
|
2298
|
+
},
|
|
2299
|
+
async deleteInstance(id, prismaDevModulePath) {
|
|
2300
|
+
await expectOk(await adminFetch(instancePath(baseUrl, id), {
|
|
2301
|
+
method: "DELETE",
|
|
2302
|
+
...prismaDevModulePath !== void 0 ? {
|
|
2303
|
+
headers: { "content-type": "application/json" },
|
|
2304
|
+
body: JSON.stringify({ prismaDevModulePath })
|
|
2305
|
+
} : {}
|
|
2306
|
+
}));
|
|
2301
2307
|
}
|
|
2302
2308
|
};
|
|
2303
2309
|
}
|
|
@@ -5618,6 +5624,386 @@ function appNameOf(container) {
|
|
|
5618
5624
|
return container.input.appName;
|
|
5619
5625
|
}
|
|
5620
5626
|
/**
|
|
5627
|
+
* The shared dev-instance store (local-dev spec § 4): the JSON files under
|
|
5628
|
+
* `<devDir>/` every local provider reads or writes. Every write is
|
|
5629
|
+
* temp-then-rename; reads and writes on the SAME file are additionally
|
|
5630
|
+
* serialized behind one in-process async queue per absolute path, because the
|
|
5631
|
+
* local providers reconcile concurrently inside the one alchemy child and a
|
|
5632
|
+
* naive read-modify-write would drop a concurrent writer's key. Postgres
|
|
5633
|
+
* instance state is NOT here (REVISED — operator review of #162): the
|
|
5634
|
+
* `postgres-main` daemon owns it now.
|
|
5635
|
+
*/
|
|
5636
|
+
function isEnoent(err) {
|
|
5637
|
+
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
5638
|
+
}
|
|
5639
|
+
const queues = /* @__PURE__ */ new Map();
|
|
5640
|
+
function enqueue(filePath, task) {
|
|
5641
|
+
const result = (queues.get(filePath) ?? Promise.resolve()).then(() => void 0, () => void 0).then(task);
|
|
5642
|
+
queues.set(filePath, result.then(() => void 0, () => void 0));
|
|
5643
|
+
return result;
|
|
5644
|
+
}
|
|
5645
|
+
async function readJson(filePath, isValid, empty) {
|
|
5646
|
+
let raw;
|
|
5647
|
+
try {
|
|
5648
|
+
raw = await fs$1$1.readFile(filePath, "utf8");
|
|
5649
|
+
} catch (err) {
|
|
5650
|
+
if (isEnoent(err)) return empty;
|
|
5651
|
+
throw err;
|
|
5652
|
+
}
|
|
5653
|
+
let parsed;
|
|
5654
|
+
try {
|
|
5655
|
+
parsed = JSON.parse(raw);
|
|
5656
|
+
} catch {
|
|
5657
|
+
return empty;
|
|
5658
|
+
}
|
|
5659
|
+
return isValid(parsed) ? parsed : empty;
|
|
5660
|
+
}
|
|
5661
|
+
async function writeJsonAtomic(filePath, value, mode) {
|
|
5662
|
+
const dir = path$1.dirname(filePath);
|
|
5663
|
+
await fs$1$1.mkdir(dir, { recursive: true });
|
|
5664
|
+
const tmpPath = path$1.join(dir, `.${path$1.basename(filePath)}.${crypto$1.randomUUID()}.tmp`);
|
|
5665
|
+
const json = `${JSON.stringify(value, null, 2)}\n`;
|
|
5666
|
+
await fs$1$1.writeFile(tmpPath, json, mode !== void 0 ? { mode } : void 0);
|
|
5667
|
+
await fs$1$1.rename(tmpPath, filePath);
|
|
5668
|
+
if (mode !== void 0) await fs$1$1.chmod(filePath, mode);
|
|
5669
|
+
}
|
|
5670
|
+
function createStore(filePath, isValid, empty, mode) {
|
|
5671
|
+
return {
|
|
5672
|
+
read: () => enqueue(filePath, () => readJson(filePath, isValid, empty)),
|
|
5673
|
+
update: (mutate) => enqueue(filePath, async () => {
|
|
5674
|
+
const next = mutate(await readJson(filePath, isValid, empty));
|
|
5675
|
+
await writeJsonAtomic(filePath, next, mode);
|
|
5676
|
+
return next;
|
|
5677
|
+
})
|
|
5678
|
+
};
|
|
5679
|
+
}
|
|
5680
|
+
function isStringRecord(value) {
|
|
5681
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
5682
|
+
}
|
|
5683
|
+
/** `<devDir>/env.json` — every `EnvironmentVariable` row, key → value. Last write wins. */
|
|
5684
|
+
function envStore(devDir) {
|
|
5685
|
+
return createStore(path$1.join(devDir, "env.json"), isStringRecord, {});
|
|
5686
|
+
}
|
|
5687
|
+
/** `<devDir>/secrets.json` — platform var name → value (shell-sourced or minted placeholder). Mode 0o600. */
|
|
5688
|
+
function secretsStore(devDir) {
|
|
5689
|
+
return createStore(path$1.join(devDir, "secrets.json"), isStringRecord, {}, 384);
|
|
5690
|
+
}
|
|
5691
|
+
/** Where local dev loads `@prisma/dev` from, for the Postgres emulator. */
|
|
5692
|
+
function noPrismaDevError(cause) {
|
|
5693
|
+
return /* @__PURE__ */ new Error(`local dev needs @prisma/dev for its local Postgres emulator — @prisma/composer-prisma-cloud declares it as a dependency, but it did not resolve from Composer's own installation; reinstall your dependencies. (${cause instanceof Error ? cause.message : String(cause)})`);
|
|
5694
|
+
}
|
|
5695
|
+
/**
|
|
5696
|
+
* Composer owns the local Postgres emulator's version: `@prisma/dev` is a
|
|
5697
|
+
* dependency of `@prisma/composer-prisma-cloud` and is resolved from
|
|
5698
|
+
* Composer's own package, never from the app. The app's copy (if any) is
|
|
5699
|
+
* ignored on purpose — an old `@prisma/dev` pulled in transitively (alchemy
|
|
5700
|
+
* pins `^0.20.0`, whose pglite-socket crashes on any message over 64 KiB)
|
|
5701
|
+
* would otherwise silently replace the version Composer tested against.
|
|
5702
|
+
* The daemon imports the returned path dynamically.
|
|
5703
|
+
*/
|
|
5704
|
+
function resolvePrismaDevModulePath() {
|
|
5705
|
+
try {
|
|
5706
|
+
return createRequire(import.meta.url).resolve("@prisma/dev");
|
|
5707
|
+
} catch (cause) {
|
|
5708
|
+
throw noPrismaDevError(cause);
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
/**
|
|
5712
|
+
* Which emulator instance backs each node of each app run from a directory.
|
|
5713
|
+
* `prisma dev` records it in `<directory>/.prisma-composer/dev/`, and every
|
|
5714
|
+
* Composer operation on an emulator instance goes through it. The emulators
|
|
5715
|
+
* know nothing about Composer: the labels written on create are for people
|
|
5716
|
+
* reading an emulator listing, and nothing here reads them back.
|
|
5717
|
+
*/
|
|
5718
|
+
const EMULATOR_INSTANCES_FILE = "emulator-instances.json";
|
|
5719
|
+
function isNodeInstance(value) {
|
|
5720
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5721
|
+
if (!("id" in value) || typeof value.id !== "string" || !("kind" in value)) return false;
|
|
5722
|
+
if (value.kind === "bucket") return true;
|
|
5723
|
+
return (value.kind === "service" || value.kind === "database") && "port" in value && typeof value.port === "number";
|
|
5724
|
+
}
|
|
5725
|
+
function isNodes(value) {
|
|
5726
|
+
return typeof value === "object" && value !== null && Object.values(value).every(isNodeInstance);
|
|
5727
|
+
}
|
|
5728
|
+
function isAppInstances(value) {
|
|
5729
|
+
return typeof value === "object" && value !== null && "nodes" in value && isNodes(value.nodes);
|
|
5730
|
+
}
|
|
5731
|
+
function isEmulatorInstances(value) {
|
|
5732
|
+
return typeof value === "object" && value !== null && "current" in value && typeof value.current === "string" && "apps" in value && typeof value.apps === "object" && value.apps !== null && Object.values(value.apps).every(isAppInstances);
|
|
5733
|
+
}
|
|
5734
|
+
function isEarlierServiceNode(value) {
|
|
5735
|
+
return typeof value === "object" && value !== null && "kind" in value && value.kind === "compute" && "id" in value && typeof value.id === "string" && "port" in value && typeof value.port === "number";
|
|
5736
|
+
}
|
|
5737
|
+
function isSingleAppRecord(value) {
|
|
5738
|
+
return typeof value === "object" && value !== null && "app" in value && typeof value.app === "string" && "nodes" in value && typeof value.nodes === "object" && value.nodes !== null && Object.values(value.nodes).every(isEarlierServiceNode);
|
|
5739
|
+
}
|
|
5740
|
+
function isStoredRecord(value) {
|
|
5741
|
+
return isEmulatorInstances(value) || isSingleAppRecord(value);
|
|
5742
|
+
}
|
|
5743
|
+
/** A single-app record reads as a record holding that one app; the next write rewrites it. */
|
|
5744
|
+
function normalized(stored) {
|
|
5745
|
+
if (stored === void 0 || isEmulatorInstances(stored)) return stored;
|
|
5746
|
+
const nodes = Object.fromEntries(Object.entries(stored.nodes).map(([address, { id, port }]) => [address, {
|
|
5747
|
+
kind: "service",
|
|
5748
|
+
id,
|
|
5749
|
+
port
|
|
5750
|
+
}]));
|
|
5751
|
+
return {
|
|
5752
|
+
current: stored.app,
|
|
5753
|
+
apps: { [stored.app]: { nodes } }
|
|
5754
|
+
};
|
|
5755
|
+
}
|
|
5756
|
+
/**
|
|
5757
|
+
* A database's or bucket's key in the record: its resource `name`, which the
|
|
5758
|
+
* lowering sets from the same helper the emulators hook keys it by. A
|
|
5759
|
+
* resource without a name cannot be matched to the instance the hook
|
|
5760
|
+
* recorded, so it is refused rather than recorded under another key.
|
|
5761
|
+
*/
|
|
5762
|
+
function recordKeyOf(kind, resourceId, name) {
|
|
5763
|
+
if (name === void 0) throw new Error(`local dev: the ${kind} resource "${resourceId}" has no name, so its emulator instance cannot be found in the directory's record.`);
|
|
5764
|
+
return name;
|
|
5765
|
+
}
|
|
5766
|
+
/** The project directory that holds `devDir` (`<directory>/.prisma-composer/dev`). */
|
|
5767
|
+
function directoryOf(devDir) {
|
|
5768
|
+
const normalizedDir = path$1.normalize(devDir);
|
|
5769
|
+
return normalizedDir.endsWith(DEV_DIR) ? normalizedDir.slice(0, -DEV_DIR.length - 1) : normalizedDir;
|
|
5770
|
+
}
|
|
5771
|
+
function store(devDir) {
|
|
5772
|
+
return createStore(path$1.join(devDir, EMULATOR_INSTANCES_FILE), isStoredRecord, void 0);
|
|
5773
|
+
}
|
|
5774
|
+
async function readRecord(devDir) {
|
|
5775
|
+
return normalized(await store(devDir).read());
|
|
5776
|
+
}
|
|
5777
|
+
async function updateRecord(devDir, mutate) {
|
|
5778
|
+
await store(devDir).update((stored) => mutate(normalized(stored)));
|
|
5779
|
+
}
|
|
5780
|
+
async function appNodes$1(scope) {
|
|
5781
|
+
return (await readRecord(scope.devDir))?.apps[scope.app]?.nodes ?? {};
|
|
5782
|
+
}
|
|
5783
|
+
async function updateNodes(scope, mutate) {
|
|
5784
|
+
await updateRecord(scope.devDir, (record) => {
|
|
5785
|
+
const nodes = { ...record?.apps[scope.app]?.nodes ?? {} };
|
|
5786
|
+
mutate(nodes);
|
|
5787
|
+
return {
|
|
5788
|
+
current: record?.current ?? scope.app,
|
|
5789
|
+
apps: {
|
|
5790
|
+
...record?.apps,
|
|
5791
|
+
[scope.app]: { nodes }
|
|
5792
|
+
}
|
|
5793
|
+
};
|
|
5794
|
+
});
|
|
5795
|
+
}
|
|
5796
|
+
function labelsFor(scope, address) {
|
|
5797
|
+
return {
|
|
5798
|
+
"composer.directory": directoryOf(scope.devDir),
|
|
5799
|
+
"composer.app": scope.app,
|
|
5800
|
+
"composer.address": address
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5803
|
+
function idsOf(nodes, kind) {
|
|
5804
|
+
return Object.values(nodes).flatMap((node) => node.kind === kind ? [node.id] : []);
|
|
5805
|
+
}
|
|
5806
|
+
/**
|
|
5807
|
+
* Stops the service processes and database servers of `nodes`, keeping
|
|
5808
|
+
* their data and ports. A kind whose emulator is not running has nothing
|
|
5809
|
+
* running to stop.
|
|
5810
|
+
*/
|
|
5811
|
+
async function stopNodes(nodes) {
|
|
5812
|
+
const services = idsOf(nodes, "service");
|
|
5813
|
+
const databases = idsOf(nodes, "database");
|
|
5814
|
+
const failure = (await Promise.allSettled([services.length > 0 ? whenRunning(() => computeClient().stopInstances(services)) : void 0, databases.length > 0 ? whenRunning(() => postgresClient().stopInstances(databases)) : void 0])).find((result) => result.status === "rejected");
|
|
5815
|
+
if (failure) throw failure.reason;
|
|
5816
|
+
}
|
|
5817
|
+
/** Runs `action` against an emulator, treating "the emulator is not running" as nothing to do. */
|
|
5818
|
+
async function whenRunning(action) {
|
|
5819
|
+
let run;
|
|
5820
|
+
try {
|
|
5821
|
+
run = action();
|
|
5822
|
+
} catch {
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
await run;
|
|
5826
|
+
}
|
|
5827
|
+
/**
|
|
5828
|
+
* Makes `scope.app` the directory's current app. When another app ran here
|
|
5829
|
+
* last, its service processes and database servers are stopped and its
|
|
5830
|
+
* instances stay recorded, so running it again resumes them with their data
|
|
5831
|
+
* and ports. Returns the previous app's name when it changed.
|
|
5832
|
+
*/
|
|
5833
|
+
async function selectApp(scope) {
|
|
5834
|
+
const record = await readRecord(scope.devDir);
|
|
5835
|
+
const previous = record?.current;
|
|
5836
|
+
if (previous !== void 0 && previous !== scope.app) await stopNodes(record?.apps[previous]?.nodes ?? {});
|
|
5837
|
+
await updateRecord(scope.devDir, (latest) => ({
|
|
5838
|
+
current: scope.app,
|
|
5839
|
+
apps: {
|
|
5840
|
+
...latest?.apps,
|
|
5841
|
+
[scope.app]: latest?.apps[scope.app] ?? { nodes: {} }
|
|
5842
|
+
}
|
|
5843
|
+
}));
|
|
5844
|
+
return previous !== void 0 && previous !== scope.app ? previous : void 0;
|
|
5845
|
+
}
|
|
5846
|
+
/** Each node of the app of `kind`, with the instance that backs it. */
|
|
5847
|
+
async function nodeInstancesOf(scope, kind) {
|
|
5848
|
+
return Object.entries(await appNodes$1(scope)).flatMap(([address, instance]) => isOfKind(instance, kind) ? [{
|
|
5849
|
+
address,
|
|
5850
|
+
instance
|
|
5851
|
+
}] : []);
|
|
5852
|
+
}
|
|
5853
|
+
function isOfKind(instance, kind) {
|
|
5854
|
+
return instance.kind === kind;
|
|
5855
|
+
}
|
|
5856
|
+
/** The instance of `kind` recorded for `address`; undefined when there is none. */
|
|
5857
|
+
async function recordedInstance(scope, address, kind) {
|
|
5858
|
+
const recorded = (await appNodes$1(scope))[address];
|
|
5859
|
+
return recorded !== void 0 && isOfKind(recorded, kind) ? recorded : void 0;
|
|
5860
|
+
}
|
|
5861
|
+
/**
|
|
5862
|
+
* The instance that runs the service at `address`. The recorded instance is
|
|
5863
|
+
* reused while the Compute emulator still has it. Otherwise a new one is
|
|
5864
|
+
* created, requesting the recorded port, and recorded.
|
|
5865
|
+
*/
|
|
5866
|
+
async function ensureServiceInstance(scope, address) {
|
|
5867
|
+
const client = computeClient();
|
|
5868
|
+
const recorded = await recordedInstance(scope, address, "service");
|
|
5869
|
+
if (recorded && await client.getInstance(recorded.id)) return { instance: recorded };
|
|
5870
|
+
const created = await client.createInstance({
|
|
5871
|
+
labels: labelsFor(scope, address),
|
|
5872
|
+
...recorded ? { port: recorded.port } : {}
|
|
5873
|
+
});
|
|
5874
|
+
const instance = {
|
|
5875
|
+
kind: "service",
|
|
5876
|
+
id: created.id,
|
|
5877
|
+
port: created.port
|
|
5878
|
+
};
|
|
5879
|
+
await updateNodes(scope, (nodes) => {
|
|
5880
|
+
nodes[address] = instance;
|
|
5881
|
+
});
|
|
5882
|
+
return {
|
|
5883
|
+
instance,
|
|
5884
|
+
...portChangeOf("service", address, recorded?.port, created.port)
|
|
5885
|
+
};
|
|
5886
|
+
}
|
|
5887
|
+
function portChangeOf(kind, address, previous, given) {
|
|
5888
|
+
return previous !== void 0 && previous !== given ? { portChange: {
|
|
5889
|
+
kind,
|
|
5890
|
+
address,
|
|
5891
|
+
previous,
|
|
5892
|
+
given
|
|
5893
|
+
} } : {};
|
|
5894
|
+
}
|
|
5895
|
+
/**
|
|
5896
|
+
* The running database for `address`. A recorded database the Postgres
|
|
5897
|
+
* emulator still has is started if it is stopped. Otherwise a new one is
|
|
5898
|
+
* created, requesting the recorded port, and recorded; it starts empty.
|
|
5899
|
+
*/
|
|
5900
|
+
async function ensureDatabaseInstance(scope, address, prismaDevModulePath) {
|
|
5901
|
+
const client = postgresClient();
|
|
5902
|
+
const recorded = await recordedInstance(scope, address, "database");
|
|
5903
|
+
const existing = recorded ? await client.getInstance(recorded.id) : void 0;
|
|
5904
|
+
if (existing?.status === "running") return { instance: existing };
|
|
5905
|
+
if (existing) {
|
|
5906
|
+
await client.startInstances([existing.id], prismaDevModulePath);
|
|
5907
|
+
return { instance: await client.getInstance(existing.id) ?? existing };
|
|
5908
|
+
}
|
|
5909
|
+
const created = await client.createInstance({
|
|
5910
|
+
labels: labelsFor(scope, address),
|
|
5911
|
+
...recorded ? { port: recorded.port } : {},
|
|
5912
|
+
prismaDevModulePath
|
|
5913
|
+
});
|
|
5914
|
+
await updateNodes(scope, (nodes) => {
|
|
5915
|
+
nodes[address] = {
|
|
5916
|
+
kind: "database",
|
|
5917
|
+
id: created.id,
|
|
5918
|
+
port: created.port
|
|
5919
|
+
};
|
|
5920
|
+
});
|
|
5921
|
+
return {
|
|
5922
|
+
instance: created,
|
|
5923
|
+
...portChangeOf("database", address, recorded?.port, created.port)
|
|
5924
|
+
};
|
|
5925
|
+
}
|
|
5926
|
+
/**
|
|
5927
|
+
* The bucket for `address`. The recorded bucket is reused while the bucket
|
|
5928
|
+
* emulator still has it. Otherwise a new one is created over `dir` and
|
|
5929
|
+
* recorded; the objects already in `dir` are its objects.
|
|
5930
|
+
*/
|
|
5931
|
+
async function ensureBucketInstance(scope, address, dir) {
|
|
5932
|
+
const client = bucketsClient();
|
|
5933
|
+
const recorded = await recordedInstance(scope, address, "bucket");
|
|
5934
|
+
const existing = recorded ? await client.getInstance(recorded.id) : void 0;
|
|
5935
|
+
if (existing) return existing;
|
|
5936
|
+
const created = await client.createInstance({
|
|
5937
|
+
labels: labelsFor(scope, address),
|
|
5938
|
+
dir
|
|
5939
|
+
});
|
|
5940
|
+
await updateNodes(scope, (nodes) => {
|
|
5941
|
+
nodes[address] = {
|
|
5942
|
+
kind: "bucket",
|
|
5943
|
+
id: created.id
|
|
5944
|
+
};
|
|
5945
|
+
});
|
|
5946
|
+
return created;
|
|
5947
|
+
}
|
|
5948
|
+
/** The Compute emulator's view of the instance recorded for `address`; undefined when either is missing. */
|
|
5949
|
+
async function liveServiceInstance(scope, address) {
|
|
5950
|
+
const recorded = await recordedInstance(scope, address, "service");
|
|
5951
|
+
return recorded ? computeClient().getInstance(recorded.id) : void 0;
|
|
5952
|
+
}
|
|
5953
|
+
/** The id recorded for `address`; undefined when there is none. */
|
|
5954
|
+
async function recordedInstanceId(scope, address) {
|
|
5955
|
+
return (await appNodes$1(scope))[address]?.id;
|
|
5956
|
+
}
|
|
5957
|
+
async function deleteInstance(instance) {
|
|
5958
|
+
if (instance.kind === "service") await computeClient().deleteInstance(instance.id);
|
|
5959
|
+
else if (instance.kind === "database") await postgresClient().deleteInstance(instance.id, resolvePrismaDevModulePath());
|
|
5960
|
+
else await bucketsClient().deleteInstance(instance.id);
|
|
5961
|
+
}
|
|
5962
|
+
/** Deletes the instance recorded for `address` and forgets it. */
|
|
5963
|
+
async function deleteNodeInstance(scope, address) {
|
|
5964
|
+
const recorded = (await appNodes$1(scope))[address];
|
|
5965
|
+
if (!recorded) return;
|
|
5966
|
+
await deleteInstance(recorded);
|
|
5967
|
+
await updateNodes(scope, (nodes) => {
|
|
5968
|
+
delete nodes[address];
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5971
|
+
/** The kinds of instance recorded for the app, so a caller can start their emulators first. */
|
|
5972
|
+
async function recordedKinds(scope) {
|
|
5973
|
+
return new Set(Object.values(await appNodes$1(scope)).map((node) => node.kind));
|
|
5974
|
+
}
|
|
5975
|
+
/**
|
|
5976
|
+
* `--fresh`: deletes the app's recorded instances of every kind and its
|
|
5977
|
+
* entry. Other apps' entries are left alone. A bucket's objects live in the
|
|
5978
|
+
* dev directory, which `--fresh` removes separately.
|
|
5979
|
+
*/
|
|
5980
|
+
async function deleteAppInstances(scope) {
|
|
5981
|
+
for (const instance of Object.values(await appNodes$1(scope))) await deleteInstance(instance);
|
|
5982
|
+
await updateRecord(scope.devDir, (record) => {
|
|
5983
|
+
const apps = { ...record?.apps };
|
|
5984
|
+
delete apps[scope.app];
|
|
5985
|
+
return {
|
|
5986
|
+
current: record?.current ?? scope.app,
|
|
5987
|
+
apps
|
|
5988
|
+
};
|
|
5989
|
+
});
|
|
5990
|
+
}
|
|
5991
|
+
/** When `prisma dev` ends: stops the app's service processes and database servers, keeping their data and ports. */
|
|
5992
|
+
async function stopAppInstances(scope) {
|
|
5993
|
+
await stopNodes(await appNodes$1(scope));
|
|
5994
|
+
}
|
|
5995
|
+
/**
|
|
5996
|
+
* The emulators' failure messages name instances by id; `prisma dev` names
|
|
5997
|
+
* services by address. Rewrites a plain `Error`'s message and leaves any
|
|
5998
|
+
* other error, such as a structured one, untouched.
|
|
5999
|
+
*/
|
|
6000
|
+
function withServiceAddresses(error, nodes) {
|
|
6001
|
+
if (!(error instanceof Error) || error.constructor !== Error) return error;
|
|
6002
|
+
let message = error.message.replace("deploy it with listens: false", "declare it with compute({ listens: false })");
|
|
6003
|
+
for (const { address, instance } of nodes) message = message.replaceAll(`instance "${instance.id}"`, `service "${address}"`);
|
|
6004
|
+
return message === error.message ? error : new Error(message, { cause: error });
|
|
6005
|
+
}
|
|
6006
|
+
/**
|
|
5621
6007
|
* Shared helpers for the upstream-attribute records every local provider
|
|
5622
6008
|
* emits — one implementation, so the compute and postgres families cannot
|
|
5623
6009
|
* drift on the `'local'` project fallback or the timestamp.
|
|
@@ -5636,26 +6022,38 @@ function projectIdOfInput(value) {
|
|
|
5636
6022
|
* already up by the time these run, since the extension's
|
|
5637
6023
|
* `localTarget.emulators` hook ensures it before converge.
|
|
5638
6024
|
*/
|
|
5639
|
-
/** Upstream lets `name` default to the resource's logical ID; locally a bucket must have a concrete name to register under. */
|
|
5640
|
-
const bucketNameOf = (news, id) => news.name ?? id;
|
|
5641
6025
|
/** Reads a bucket id from upstream's `bucket` input: a plain string or a resolved `Prisma.Bucket` attributes record. */
|
|
5642
6026
|
function bucketIdOfInput(value) {
|
|
5643
6027
|
if (typeof value === "string") return value;
|
|
5644
6028
|
if (Predicate.isObject(value) && typeof value["bucketId"] === "string") return value["bucketId"];
|
|
5645
6029
|
return "local";
|
|
5646
6030
|
}
|
|
5647
|
-
|
|
6031
|
+
const BUCKETS_DIR = "buckets";
|
|
6032
|
+
/** Where a bucket's objects live: in the project's dev directory, under its app, so `--fresh` removes them with the app. */
|
|
6033
|
+
function bucketDataDir(scope, name) {
|
|
6034
|
+
return path$1.join(scope.devDir, BUCKETS_DIR, scope.app, name);
|
|
6035
|
+
}
|
|
6036
|
+
function appScopeOf$2(input) {
|
|
6037
|
+
return {
|
|
6038
|
+
devDir: input.devDir,
|
|
6039
|
+
app: appNameOf(input.container)
|
|
6040
|
+
};
|
|
6041
|
+
}
|
|
6042
|
+
/**
|
|
6043
|
+
* `Bucket` → the bucket instance the directory's record names for the
|
|
6044
|
+
* resource, created when there is none, over an in-project data root. Its
|
|
6045
|
+
* `bucketId` is the instance id, which is also its S3 bucket name. A Bucket
|
|
6046
|
+
* whose recorded instance changed is updated, so its access key is
|
|
6047
|
+
* registered on the new instance.
|
|
6048
|
+
*/
|
|
5648
6049
|
function LocalBucketProvider(input) {
|
|
5649
6050
|
return Provider.effect(Bucket, Effect.succeed({
|
|
5650
6051
|
list: () => Effect.succeed([]),
|
|
5651
6052
|
reconcile: ({ news, id }) => Effect.tryPromise({
|
|
5652
6053
|
try: async () => {
|
|
5653
|
-
const
|
|
5654
|
-
const name = bucketNameOf(news, id);
|
|
5655
|
-
const dir = path$1.join(input.devDir, "buckets", name);
|
|
5656
|
-
await bucketsClient().putBucket(app, name, dir);
|
|
6054
|
+
const name = recordKeyOf("bucket", id, news.name);
|
|
5657
6055
|
return {
|
|
5658
|
-
bucketId: name,
|
|
6056
|
+
bucketId: (await ensureBucketInstance(appScopeOf$2(input), name, bucketDataDir(appScopeOf$2(input), name))).id,
|
|
5659
6057
|
name,
|
|
5660
6058
|
projectId: projectIdOfInput(news.project),
|
|
5661
6059
|
createdAt: DEV_TIMESTAMP,
|
|
@@ -5664,6 +6062,13 @@ function LocalBucketProvider(input) {
|
|
|
5664
6062
|
},
|
|
5665
6063
|
catch: (cause) => cause
|
|
5666
6064
|
}),
|
|
6065
|
+
diff: ({ output }) => Effect.tryPromise({
|
|
6066
|
+
try: async () => {
|
|
6067
|
+
if (output === void 0) return void 0;
|
|
6068
|
+
return await recordedInstanceId(appScopeOf$2(input), output.name) === output.bucketId ? void 0 : { action: "update" };
|
|
6069
|
+
},
|
|
6070
|
+
catch: (cause) => cause
|
|
6071
|
+
}),
|
|
5667
6072
|
delete: () => Effect.void,
|
|
5668
6073
|
read: ({ output }) => Effect.succeed(output)
|
|
5669
6074
|
}));
|
|
@@ -5675,26 +6080,25 @@ function LocalBucketProvider(input) {
|
|
|
5675
6080
|
* emulator, so a bucket emulator whose own state was wiped self-heals on the
|
|
5676
6081
|
* next converge.
|
|
5677
6082
|
*/
|
|
5678
|
-
function LocalBucketAccessKeyProvider(
|
|
6083
|
+
function LocalBucketAccessKeyProvider(_input) {
|
|
5679
6084
|
return Provider.effect(BucketAccessKey, Effect.succeed({
|
|
5680
6085
|
list: () => Effect.succeed([]),
|
|
5681
6086
|
reconcile: ({ news, output, id }) => Effect.tryPromise({
|
|
5682
6087
|
try: async () => {
|
|
5683
|
-
const app = appNameOf(input.container);
|
|
5684
6088
|
const pair = output !== void 0 ? {
|
|
5685
6089
|
accessKeyId: output.accessKeyId,
|
|
5686
6090
|
secretAccessKey: Redacted.value(output.secretAccessKey)
|
|
5687
6091
|
} : mintKeyPair();
|
|
5688
6092
|
const client = bucketsClient();
|
|
5689
|
-
await client.putCredentials(app, pair.accessKeyId, pair.secretAccessKey);
|
|
5690
6093
|
const bucketId = bucketIdOfInput(news.bucket);
|
|
6094
|
+
await client.putCredentials(bucketId, pair.accessKeyId, pair.secretAccessKey);
|
|
5691
6095
|
return {
|
|
5692
6096
|
bucketAccessKeyId: news.name ?? id,
|
|
5693
6097
|
bucketId,
|
|
5694
6098
|
accessKeyId: pair.accessKeyId,
|
|
5695
6099
|
secretAccessKey: Redacted.make(pair.secretAccessKey),
|
|
5696
6100
|
endpoint: client.baseUrl,
|
|
5697
|
-
bucketName:
|
|
6101
|
+
bucketName: bucketId
|
|
5698
6102
|
};
|
|
5699
6103
|
},
|
|
5700
6104
|
catch: (cause) => cause
|
|
@@ -5704,6 +6108,36 @@ function LocalBucketAccessKeyProvider(input) {
|
|
|
5704
6108
|
}));
|
|
5705
6109
|
}
|
|
5706
6110
|
/**
|
|
6111
|
+
* The emulators hook's half of the directory's record: before Alchemy runs,
|
|
6112
|
+
* every node of the app gets its emulator instance, one at a time in the
|
|
6113
|
+
* order given.
|
|
6114
|
+
*/
|
|
6115
|
+
/**
|
|
6116
|
+
* Ensures each node's instance, in the order given: a recorded instance is
|
|
6117
|
+
* reused (a stopped database is started), a missing one is created again on
|
|
6118
|
+
* its remembered port, and a new node gets a new instance. A new instance
|
|
6119
|
+
* gets the smallest free port, so on a fresh start the ports follow this
|
|
6120
|
+
* order. Returns the nodes whose re-created instance could not get its
|
|
6121
|
+
* remembered port back.
|
|
6122
|
+
*/
|
|
6123
|
+
async function ensureAppInstances(scope, nodes) {
|
|
6124
|
+
const changes = [];
|
|
6125
|
+
for (const { address, kind } of nodes) try {
|
|
6126
|
+
const { portChange } = await ensureNodeInstance(scope, address, kind);
|
|
6127
|
+
if (portChange) changes.push(portChange);
|
|
6128
|
+
} catch (cause) {
|
|
6129
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
6130
|
+
throw new Error(`ensuring the local instance for ${kind} "${address}" failed: ${reason}`, { cause });
|
|
6131
|
+
}
|
|
6132
|
+
return changes;
|
|
6133
|
+
}
|
|
6134
|
+
async function ensureNodeInstance(scope, address, kind) {
|
|
6135
|
+
if (kind === "service") return ensureServiceInstance(scope, address);
|
|
6136
|
+
if (kind === "database") return ensureDatabaseInstance(scope, address, resolvePrismaDevModulePath());
|
|
6137
|
+
await ensureBucketInstance(scope, address, bucketDataDir(scope, address));
|
|
6138
|
+
return {};
|
|
6139
|
+
}
|
|
6140
|
+
/**
|
|
5707
6141
|
* Extracts the tar.gz `@internal/lowering`'s `packageComputeArtifact` writes
|
|
5708
6142
|
* (local-dev spec § 4) — emulation-only code, so it lives here rather than
|
|
5709
6143
|
* beside the writer (production source stays physically separated from
|
|
@@ -5768,71 +6202,6 @@ function extractComputeArtifact(tarGzPath, destDir) {
|
|
|
5768
6202
|
}
|
|
5769
6203
|
}
|
|
5770
6204
|
/**
|
|
5771
|
-
* The shared dev-instance store (local-dev spec § 4): the JSON files under
|
|
5772
|
-
* `<devDir>/` every local provider reads or writes. Every write is
|
|
5773
|
-
* temp-then-rename; reads and writes on the SAME file are additionally
|
|
5774
|
-
* serialized behind one in-process async queue per absolute path, because the
|
|
5775
|
-
* local providers reconcile concurrently inside the one alchemy child and a
|
|
5776
|
-
* naive read-modify-write would drop a concurrent writer's key. Postgres
|
|
5777
|
-
* instance state is NOT here (REVISED — operator review of #162): the
|
|
5778
|
-
* `postgres-main` daemon owns it now.
|
|
5779
|
-
*/
|
|
5780
|
-
function isEnoent(err) {
|
|
5781
|
-
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
5782
|
-
}
|
|
5783
|
-
const queues = /* @__PURE__ */ new Map();
|
|
5784
|
-
function enqueue(filePath, task) {
|
|
5785
|
-
const result = (queues.get(filePath) ?? Promise.resolve()).then(() => void 0, () => void 0).then(task);
|
|
5786
|
-
queues.set(filePath, result.then(() => void 0, () => void 0));
|
|
5787
|
-
return result;
|
|
5788
|
-
}
|
|
5789
|
-
async function readJson(filePath, isValid, empty) {
|
|
5790
|
-
let raw;
|
|
5791
|
-
try {
|
|
5792
|
-
raw = await fs$1$1.readFile(filePath, "utf8");
|
|
5793
|
-
} catch (err) {
|
|
5794
|
-
if (isEnoent(err)) return empty;
|
|
5795
|
-
throw err;
|
|
5796
|
-
}
|
|
5797
|
-
let parsed;
|
|
5798
|
-
try {
|
|
5799
|
-
parsed = JSON.parse(raw);
|
|
5800
|
-
} catch {
|
|
5801
|
-
return empty;
|
|
5802
|
-
}
|
|
5803
|
-
return isValid(parsed) ? parsed : empty;
|
|
5804
|
-
}
|
|
5805
|
-
async function writeJsonAtomic(filePath, value, mode) {
|
|
5806
|
-
const dir = path$1.dirname(filePath);
|
|
5807
|
-
await fs$1$1.mkdir(dir, { recursive: true });
|
|
5808
|
-
const tmpPath = path$1.join(dir, `.${path$1.basename(filePath)}.${crypto$1.randomUUID()}.tmp`);
|
|
5809
|
-
const json = `${JSON.stringify(value, null, 2)}\n`;
|
|
5810
|
-
await fs$1$1.writeFile(tmpPath, json, mode !== void 0 ? { mode } : void 0);
|
|
5811
|
-
await fs$1$1.rename(tmpPath, filePath);
|
|
5812
|
-
if (mode !== void 0) await fs$1$1.chmod(filePath, mode);
|
|
5813
|
-
}
|
|
5814
|
-
function createStore(filePath, isValid, empty, mode) {
|
|
5815
|
-
return {
|
|
5816
|
-
read: () => enqueue(filePath, () => readJson(filePath, isValid, empty)),
|
|
5817
|
-
update: (mutate) => enqueue(filePath, async () => {
|
|
5818
|
-
const next = mutate(await readJson(filePath, isValid, empty));
|
|
5819
|
-
await writeJsonAtomic(filePath, next, mode);
|
|
5820
|
-
return next;
|
|
5821
|
-
})
|
|
5822
|
-
};
|
|
5823
|
-
}
|
|
5824
|
-
function isStringRecord(value) {
|
|
5825
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
5826
|
-
}
|
|
5827
|
-
/** `<devDir>/env.json` — every `EnvironmentVariable` row, key → value. Last write wins. */
|
|
5828
|
-
function envStore(devDir) {
|
|
5829
|
-
return createStore(path$1.join(devDir, "env.json"), isStringRecord, {});
|
|
5830
|
-
}
|
|
5831
|
-
/** `<devDir>/secrets.json` — platform var name → value (shell-sourced or minted placeholder). Mode 0o600. */
|
|
5832
|
-
function secretsStore(devDir) {
|
|
5833
|
-
return createStore(path$1.join(devDir, "secrets.json"), isStringRecord, {}, 384);
|
|
5834
|
-
}
|
|
5835
|
-
/**
|
|
5836
6205
|
* Local compute-cluster providers: upstream alchemy's `Prisma.App` and
|
|
5837
6206
|
* `Prisma.Deployment` become clients of the machine-scoped Compute emulator;
|
|
5838
6207
|
* `Prisma.EnvironmentVariable` becomes a row in the dev env store;
|
|
@@ -5937,16 +6306,6 @@ function readComputeManifest(artifactDir) {
|
|
|
5937
6306
|
listens: parsed.listens !== false
|
|
5938
6307
|
};
|
|
5939
6308
|
}
|
|
5940
|
-
/**
|
|
5941
|
-
* A service address is dot-separated (`"orders.service"`) but the emulator's
|
|
5942
|
-
* `<id>` segment must match `/^[a-z0-9][a-z0-9-]*$/`: disallowed characters
|
|
5943
|
-
* become dashes, runs collapse. The real address still rides the deployment
|
|
5944
|
-
* body's `address` field untouched, so listings show it verbatim.
|
|
5945
|
-
*/
|
|
5946
|
-
function slugServiceId(address) {
|
|
5947
|
-
const slug = address.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5948
|
-
return slug.length > 0 ? slug : "svc";
|
|
5949
|
-
}
|
|
5950
6309
|
async function materializeEnv(devDir, address, port) {
|
|
5951
6310
|
const env = scopedEnvRows(await envStore(devDir).read(), address);
|
|
5952
6311
|
env[servicePortEnvKey(address)] = JSON.stringify(port);
|
|
@@ -5956,21 +6315,30 @@ async function materializeEnv(devDir, address, port) {
|
|
|
5956
6315
|
if (process.env["HOME"] !== void 0) env["HOME"] = process.env["HOME"];
|
|
5957
6316
|
return env;
|
|
5958
6317
|
}
|
|
6318
|
+
function appScopeOf$1(input) {
|
|
6319
|
+
return {
|
|
6320
|
+
devDir: input.devDir,
|
|
6321
|
+
app: appNameOf(input.container)
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
function localUrl(port) {
|
|
6325
|
+
return `http://localhost:${String(port)}`;
|
|
6326
|
+
}
|
|
5959
6327
|
/**
|
|
5960
|
-
* `Prisma.App` → the Compute emulator:
|
|
5961
|
-
*
|
|
5962
|
-
*
|
|
5963
|
-
*
|
|
5964
|
-
*
|
|
6328
|
+
* `Prisma.App` → the Compute emulator: finds or creates the instance that
|
|
6329
|
+
* runs the service, which holds the service's port. The app id here IS the
|
|
6330
|
+
* service's own address. An App whose instance was re-created on another
|
|
6331
|
+
* port is updated, so its dependents get the new URL. Deleting an App (its
|
|
6332
|
+
* service was removed) deletes its instance.
|
|
5965
6333
|
*/
|
|
5966
6334
|
function LocalAppProvider(input) {
|
|
5967
6335
|
return Provider.effect(App, Effect.succeed({
|
|
5968
6336
|
list: () => Effect.succeed([]),
|
|
5969
6337
|
reconcile: ({ id, news }) => Effect.tryPromise({
|
|
5970
6338
|
try: async () => {
|
|
5971
|
-
const appName = appNameOf(input.container);
|
|
5972
6339
|
const name = news.displayName ?? id;
|
|
5973
|
-
const {
|
|
6340
|
+
const { instance } = await ensureServiceInstance(appScopeOf$1(input), name);
|
|
6341
|
+
const url = localUrl(instance.port);
|
|
5974
6342
|
return {
|
|
5975
6343
|
appId: name,
|
|
5976
6344
|
name,
|
|
@@ -5985,26 +6353,21 @@ function LocalAppProvider(input) {
|
|
|
5985
6353
|
},
|
|
5986
6354
|
catch: (cause) => cause
|
|
5987
6355
|
}),
|
|
5988
|
-
|
|
6356
|
+
diff: ({ output }) => Effect.tryPromise({
|
|
6357
|
+
try: async () => {
|
|
6358
|
+
if (output === void 0) return void 0;
|
|
6359
|
+
if ((await liveServiceInstance(appScopeOf$1(input), output.name))?.url === output.appEndpointDomain) return void 0;
|
|
6360
|
+
return { action: "update" };
|
|
6361
|
+
},
|
|
6362
|
+
catch: (cause) => cause
|
|
6363
|
+
}),
|
|
6364
|
+
delete: ({ output }) => Effect.tryPromise({
|
|
6365
|
+
try: () => deleteNodeInstance(appScopeOf$1(input), output.name),
|
|
6366
|
+
catch: (cause) => cause
|
|
6367
|
+
}),
|
|
5989
6368
|
read: ({ output }) => Effect.succeed(output)
|
|
5990
6369
|
}));
|
|
5991
6370
|
}
|
|
5992
|
-
/**
|
|
5993
|
-
* Reserves each service's emulator port one at a time, in the order given,
|
|
5994
|
-
* before Alchemy applies the `App` resources concurrently. A new service gets
|
|
5995
|
-
* the smallest free port, so on a fresh start the ports follow this order; a
|
|
5996
|
-
* service that already has a port keeps it. `serviceAppNames` are the
|
|
5997
|
-
* `App` resources' `displayName`s.
|
|
5998
|
-
*/
|
|
5999
|
-
async function reserveServicePorts(container, serviceAppNames) {
|
|
6000
|
-
const appName = appNameOf(container);
|
|
6001
|
-
for (const service of serviceAppNames) try {
|
|
6002
|
-
await computeClient().ensureService(appName, slugServiceId(service));
|
|
6003
|
-
} catch (cause) {
|
|
6004
|
-
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
6005
|
-
throw new Error(`reserving a local port for service "${service}" failed: ${reason}`, { cause });
|
|
6006
|
-
}
|
|
6007
|
-
}
|
|
6008
6371
|
/** `Prisma.EnvironmentVariable` → a key/value row in `<devDir>/env.json`. Upstream's value is `Redacted`; env.json holds the plain string the child process is given. */
|
|
6009
6372
|
function LocalEnvironmentVariableProvider(input) {
|
|
6010
6373
|
return Provider.effect(EnvironmentVariable, Effect.succeed({
|
|
@@ -6043,8 +6406,8 @@ function LocalEnvironmentVariableProvider(input) {
|
|
|
6043
6406
|
}));
|
|
6044
6407
|
}
|
|
6045
6408
|
/**
|
|
6046
|
-
* `Prisma.Deployment` → unpacks the artifact once per hash,
|
|
6047
|
-
*
|
|
6409
|
+
* `Prisma.Deployment` → unpacks the artifact once per hash, finds the
|
|
6410
|
+
* service's emulator instance and its port, materializes the child's full env (env store +
|
|
6048
6411
|
* secrets + the port override + `PATH`/`HOME`), and puts the deployment — the
|
|
6049
6412
|
* emulator (re)starts the child only when the hash or env actually changed.
|
|
6050
6413
|
* `portMapping` is ignored on purpose: the emulator owns port allocation, and
|
|
@@ -6056,24 +6419,27 @@ function LocalDeploymentProvider(input) {
|
|
|
6056
6419
|
list: () => Effect.succeed([]),
|
|
6057
6420
|
reconcile: ({ news }) => Effect.tryPromise({
|
|
6058
6421
|
try: async () => {
|
|
6059
|
-
const app = appNameOf(input.container);
|
|
6060
6422
|
const appId = appIdOfInput(news.app);
|
|
6061
|
-
const emulatorId = slugServiceId(appId);
|
|
6062
6423
|
if (news.artifactPath === void 0) throw new Error("local Deployment requires an artifactPath — nothing to run.");
|
|
6063
6424
|
const artifactHash = await artifactSha256(news.artifactPath);
|
|
6064
6425
|
const artifactDir = path$1.join(input.devDir, "artifacts", artifactHash);
|
|
6065
6426
|
if (!fs$2.existsSync(artifactDir)) extractComputeArtifact(news.artifactPath, artifactDir);
|
|
6066
6427
|
const { address, listens } = readComputeManifest(artifactDir);
|
|
6067
|
-
const {
|
|
6068
|
-
const env = await materializeEnv(input.devDir, address, port);
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
})
|
|
6428
|
+
const { instance } = await ensureServiceInstance(appScopeOf$1(input), appId);
|
|
6429
|
+
const env = await materializeEnv(input.devDir, address, instance.port);
|
|
6430
|
+
try {
|
|
6431
|
+
await computeClient().putDeployment(instance.id, {
|
|
6432
|
+
artifactDir,
|
|
6433
|
+
artifactHash,
|
|
6434
|
+
env,
|
|
6435
|
+
listens
|
|
6436
|
+
});
|
|
6437
|
+
} catch (error) {
|
|
6438
|
+
throw withServiceAddresses(error, [{
|
|
6439
|
+
address,
|
|
6440
|
+
instance
|
|
6441
|
+
}]);
|
|
6442
|
+
}
|
|
6077
6443
|
return {
|
|
6078
6444
|
deploymentId: artifactHash,
|
|
6079
6445
|
appId,
|
|
@@ -6081,12 +6447,20 @@ function LocalDeploymentProvider(input) {
|
|
|
6081
6447
|
status: "running",
|
|
6082
6448
|
previewDomain: null,
|
|
6083
6449
|
artifactHash,
|
|
6084
|
-
appEndpointDomain:
|
|
6450
|
+
appEndpointDomain: localUrl(instance.port),
|
|
6085
6451
|
createdAt: DEV_TIMESTAMP
|
|
6086
6452
|
};
|
|
6087
6453
|
},
|
|
6088
6454
|
catch: (cause) => cause
|
|
6089
6455
|
}),
|
|
6456
|
+
diff: ({ output }) => Effect.tryPromise({
|
|
6457
|
+
try: async () => {
|
|
6458
|
+
if (output === void 0) return void 0;
|
|
6459
|
+
if ((await liveServiceInstance(appScopeOf$1(input), output.appId))?.artifactHash === output.artifactHash) return void 0;
|
|
6460
|
+
return { action: "update" };
|
|
6461
|
+
},
|
|
6462
|
+
catch: (cause) => cause
|
|
6463
|
+
}),
|
|
6090
6464
|
delete: () => Effect.void
|
|
6091
6465
|
}));
|
|
6092
6466
|
}
|
|
@@ -6116,57 +6490,28 @@ function LocalProjectProvider(_input) {
|
|
|
6116
6490
|
delete: () => Effect.void
|
|
6117
6491
|
}));
|
|
6118
6492
|
}
|
|
6119
|
-
/**
|
|
6120
|
-
* Local postgres-cluster providers: upstream alchemy's `Prisma.Database` and
|
|
6121
|
-
* `Prisma.Connection` become clients of the `postgres-main` emulator daemon
|
|
6122
|
-
* (one named, persistent `@prisma/dev` server per `Database` resource).
|
|
6123
|
-
* `PgWarm`/`OrmMigration` are not here — the hosted ones run against whatever
|
|
6124
|
-
* URL they are handed. Attributes match upstream's shapes; the daemon's
|
|
6125
|
-
* DIRECT connection string maps to `directConnectionString` and
|
|
6126
|
-
* `databaseUrl`, everything else is left absent. Instance names come from
|
|
6127
|
-
* `@internal/dev-emulators`' own `instanceNameFor` — a locally re-derived
|
|
6128
|
-
* slug drifted from the daemon's and broke `Connection`'s lookup.
|
|
6129
|
-
*/
|
|
6130
6493
|
/** Reads a database id from upstream's `database` input: a plain string or a resolved `Prisma.Database` attributes record. */
|
|
6131
6494
|
function databaseIdOfInput(value) {
|
|
6132
6495
|
if (typeof value === "string") return value;
|
|
6133
6496
|
if (Predicate.isObject(value) && typeof value["databaseId"] === "string") return value["databaseId"];
|
|
6134
6497
|
}
|
|
6135
|
-
function noPrismaDevError(cause) {
|
|
6136
|
-
return /* @__PURE__ */ new Error(`local dev needs @prisma/dev for its local Postgres emulator — @prisma/composer-prisma-cloud declares it as a dependency, but it did not resolve from Composer's own installation; reinstall your dependencies. (${cause instanceof Error ? cause.message : String(cause)})`);
|
|
6137
|
-
}
|
|
6138
6498
|
/**
|
|
6139
|
-
*
|
|
6140
|
-
*
|
|
6141
|
-
*
|
|
6142
|
-
*
|
|
6143
|
-
*
|
|
6144
|
-
* would otherwise silently replace the version Composer tested against.
|
|
6145
|
-
* The daemon imports the returned path dynamically.
|
|
6146
|
-
*/
|
|
6147
|
-
function resolvePrismaDevModulePath() {
|
|
6148
|
-
try {
|
|
6149
|
-
return createRequire(import.meta.url).resolve("@prisma/dev");
|
|
6150
|
-
} catch (cause) {
|
|
6151
|
-
throw noPrismaDevError(cause);
|
|
6152
|
-
}
|
|
6153
|
-
}
|
|
6154
|
-
/**
|
|
6155
|
-
* `Prisma.Database` → an ensured `postgres-main` server, one per resource.
|
|
6156
|
-
* Stores the daemon's returned url as the `directConnectionString` attribute.
|
|
6499
|
+
* `Prisma.Database` → the database instance the directory's record names for
|
|
6500
|
+
* the resource, started if it is stopped and created when there is none.
|
|
6501
|
+
* Stores its url as the `directConnectionString` attribute. A Database whose
|
|
6502
|
+
* recorded instance changed (re-created after the emulator lost it) is
|
|
6503
|
+
* updated, so its Connection and dependents get the new instance's URL.
|
|
6157
6504
|
*/
|
|
6158
6505
|
function LocalDatabaseProvider(input) {
|
|
6159
6506
|
return Provider.effect(Prisma.Database, Effect.succeed({
|
|
6160
6507
|
list: () => Effect.succeed([]),
|
|
6161
6508
|
reconcile: ({ id, news }) => Effect.tryPromise({
|
|
6162
6509
|
try: async () => {
|
|
6163
|
-
const
|
|
6164
|
-
const
|
|
6165
|
-
const
|
|
6166
|
-
const { url } = await postgresClient().ensureDatabase(app, slug(name), prismaDevModulePath);
|
|
6167
|
-
const direct = Redacted.make(url);
|
|
6510
|
+
const name = recordKeyOf("database", id, news.name);
|
|
6511
|
+
const { instance } = await ensureDatabaseInstance(appScopeOf(input), name, resolvePrismaDevModulePath());
|
|
6512
|
+
const direct = Redacted.make(instance.url);
|
|
6168
6513
|
return {
|
|
6169
|
-
databaseId:
|
|
6514
|
+
databaseId: instance.id,
|
|
6170
6515
|
databaseName: name,
|
|
6171
6516
|
projectId: projectIdOfInput(news.project),
|
|
6172
6517
|
status: "ready",
|
|
@@ -6186,28 +6531,40 @@ function LocalDatabaseProvider(input) {
|
|
|
6186
6531
|
},
|
|
6187
6532
|
catch: (cause) => cause
|
|
6188
6533
|
}),
|
|
6534
|
+
diff: ({ output }) => Effect.tryPromise({
|
|
6535
|
+
try: async () => {
|
|
6536
|
+
if (output === void 0 || output.databaseName === void 0) return void 0;
|
|
6537
|
+
return await recordedInstanceId(appScopeOf(input), output.databaseName) === output.databaseId ? void 0 : { action: "update" };
|
|
6538
|
+
},
|
|
6539
|
+
catch: (cause) => cause
|
|
6540
|
+
}),
|
|
6189
6541
|
delete: () => Effect.void
|
|
6190
6542
|
}));
|
|
6191
6543
|
}
|
|
6544
|
+
function appScopeOf(input) {
|
|
6545
|
+
return {
|
|
6546
|
+
devDir: input.devDir,
|
|
6547
|
+
app: appNameOf(input.container)
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6192
6550
|
function noRecordedInstanceError(databaseId) {
|
|
6193
6551
|
return /* @__PURE__ */ new Error(`no local Postgres instance recorded for databaseId "${databaseId}" — the Database provider did not run; converge is corrupt (try --fresh).`);
|
|
6194
6552
|
}
|
|
6195
|
-
/** `Prisma.Connection` → the
|
|
6196
|
-
function LocalConnectionProvider(
|
|
6553
|
+
/** `Prisma.Connection` → the database instance the Database attributes' `databaseId` names. */
|
|
6554
|
+
function LocalConnectionProvider(_input) {
|
|
6197
6555
|
return Provider.effect(Prisma.Connection, Effect.succeed({
|
|
6198
6556
|
list: () => Effect.succeed([]),
|
|
6199
6557
|
reconcile: ({ id, news }) => Effect.tryPromise({
|
|
6200
6558
|
try: async () => {
|
|
6201
|
-
const app = appNameOf(input.container);
|
|
6202
6559
|
const databaseId = databaseIdOfInput(news.database);
|
|
6203
6560
|
if (databaseId === void 0) throw noRecordedInstanceError(String(news.database));
|
|
6204
|
-
const found =
|
|
6561
|
+
const found = await postgresClient().getInstance(databaseId);
|
|
6205
6562
|
if (found === void 0) throw noRecordedInstanceError(databaseId);
|
|
6206
6563
|
const direct = Redacted.make(found.url);
|
|
6207
6564
|
return {
|
|
6208
|
-
connectionId: found.
|
|
6565
|
+
connectionId: found.id,
|
|
6209
6566
|
connectionName: news.name ?? id,
|
|
6210
|
-
databaseId: found.
|
|
6567
|
+
databaseId: found.id,
|
|
6211
6568
|
kind: "postgres",
|
|
6212
6569
|
createdAt: DEV_TIMESTAMP,
|
|
6213
6570
|
directConnectionString: direct,
|
|
@@ -6253,10 +6610,8 @@ function resolvePackageEntry(specifier) {
|
|
|
6253
6610
|
* ADR-0041 D12). The target extension's `dev/teardown.ts` orchestrates
|
|
6254
6611
|
* (which app, which emulator clients); this module owns the actual
|
|
6255
6612
|
* filesystem operations, so the extension's own source stays free of
|
|
6256
|
-
* `node:`/`bun:` imports (its invariant 5).
|
|
6257
|
-
*
|
|
6258
|
-
* `postgresClient().deleteApp(app)` call, made directly alongside the
|
|
6259
|
-
* compute/buckets clients — no filesystem or child-process work of its own.
|
|
6613
|
+
* `node:`/`bun:` imports (its invariant 5). Emulator instances are not
|
|
6614
|
+
* removed here: the extension deletes them through the directory's record.
|
|
6260
6615
|
*/
|
|
6261
6616
|
/** `fs.rm(..., { recursive: true, force: true })` for every given path — tolerates absence. */
|
|
6262
6617
|
function removeLocalPaths(paths) {
|
|
@@ -6265,6 +6620,21 @@ function removeLocalPaths(paths) {
|
|
|
6265
6620
|
force: true
|
|
6266
6621
|
});
|
|
6267
6622
|
}
|
|
6623
|
+
/**
|
|
6624
|
+
* Removes the dev state directory's contents for `--fresh`, except what
|
|
6625
|
+
* other apps run from the directory still use: the record of their emulator
|
|
6626
|
+
* instances and their buckets' objects. This app's objects go.
|
|
6627
|
+
*/
|
|
6628
|
+
function removeAppDevState(devDir, app) {
|
|
6629
|
+
let entries;
|
|
6630
|
+
try {
|
|
6631
|
+
entries = fs$2.readdirSync(devDir);
|
|
6632
|
+
} catch {
|
|
6633
|
+
return;
|
|
6634
|
+
}
|
|
6635
|
+
const kept = /* @__PURE__ */ new Set([EMULATOR_INSTANCES_FILE, BUCKETS_DIR]);
|
|
6636
|
+
removeLocalPaths([...entries.filter((entry) => !kept.has(entry)).map((entry) => path$1.join(devDir, entry)), path$1.join(devDir, BUCKETS_DIR, app)]);
|
|
6637
|
+
}
|
|
6268
6638
|
//#endregion
|
|
6269
6639
|
//#region ../../1-prisma-cloud/1-extensions/target/dist/local-target.mjs
|
|
6270
6640
|
const RELIST_INTERVAL_MS = 2e3;
|
|
@@ -6275,7 +6645,7 @@ const RELIST_INTERVAL_MS = 2e3;
|
|
|
6275
6645
|
* dropped (an emulator restart shows a gap in that service's stream, never a
|
|
6276
6646
|
* dead session).
|
|
6277
6647
|
*/
|
|
6278
|
-
async function* mergedLogs(
|
|
6648
|
+
async function* mergedLogs(scope, signal, tail) {
|
|
6279
6649
|
const client = computeClient();
|
|
6280
6650
|
const followed = /* @__PURE__ */ new Set();
|
|
6281
6651
|
const backlogged = /* @__PURE__ */ new Set();
|
|
@@ -6294,7 +6664,7 @@ async function* mergedLogs(app, signal, tail) {
|
|
|
6294
6664
|
(async () => {
|
|
6295
6665
|
try {
|
|
6296
6666
|
let buffer = "";
|
|
6297
|
-
for await (const chunk of client.followLogs(
|
|
6667
|
+
for await (const chunk of client.followLogs(id, signal, { tail: followTail })) {
|
|
6298
6668
|
buffer += chunk;
|
|
6299
6669
|
let newlineAt = buffer.indexOf("\n");
|
|
6300
6670
|
while (newlineAt !== -1) {
|
|
@@ -6312,8 +6682,7 @@ async function* mergedLogs(app, signal, tail) {
|
|
|
6312
6682
|
})();
|
|
6313
6683
|
};
|
|
6314
6684
|
const relist = async () => {
|
|
6315
|
-
const
|
|
6316
|
-
for (const svc of services) follow(svc.id, svc.address);
|
|
6685
|
+
for (const { address, instance } of await nodeInstancesOf(scope, "service")) follow(instance.id, address);
|
|
6317
6686
|
};
|
|
6318
6687
|
await relist();
|
|
6319
6688
|
const timer = setInterval(() => void relist(), RELIST_INTERVAL_MS);
|
|
@@ -6338,18 +6707,28 @@ async function* mergedLogs(app, signal, tail) {
|
|
|
6338
6707
|
}
|
|
6339
6708
|
}
|
|
6340
6709
|
async function devAttach(input) {
|
|
6341
|
-
const
|
|
6710
|
+
const scope = {
|
|
6711
|
+
devDir: input.devDir,
|
|
6712
|
+
app: prismaCloudContainerOf(input.container).input.appName
|
|
6713
|
+
};
|
|
6342
6714
|
const client = computeClient();
|
|
6343
6715
|
return {
|
|
6344
|
-
startServices: () =>
|
|
6716
|
+
startServices: async () => {
|
|
6717
|
+
const nodes = await nodeInstancesOf(scope, "service");
|
|
6718
|
+
try {
|
|
6719
|
+
await client.startInstances(nodes.map(({ instance }) => instance.id));
|
|
6720
|
+
} catch (error) {
|
|
6721
|
+
throw withServiceAddresses(error, nodes);
|
|
6722
|
+
}
|
|
6723
|
+
},
|
|
6345
6724
|
endpoints: async () => {
|
|
6346
|
-
return (await
|
|
6347
|
-
address
|
|
6348
|
-
url:
|
|
6725
|
+
return (await nodeInstancesOf(scope, "service")).map(({ address, instance }) => ({
|
|
6726
|
+
address,
|
|
6727
|
+
url: `http://localhost:${String(instance.port)}`
|
|
6349
6728
|
}));
|
|
6350
6729
|
},
|
|
6351
|
-
logs: (signal, opts) => mergedLogs(
|
|
6352
|
-
|
|
6730
|
+
logs: (signal, opts) => mergedLogs(scope, signal, opts?.tail ?? 0),
|
|
6731
|
+
stopApp: () => stopAppInstances(scope)
|
|
6353
6732
|
};
|
|
6354
6733
|
}
|
|
6355
6734
|
const LOCAL_PROJECT_ID = "local";
|
|
@@ -6371,28 +6750,54 @@ function devContainerDescriptor() {
|
|
|
6371
6750
|
function daemonEntry(name) {
|
|
6372
6751
|
return resolvePackageEntry(`@prisma/composer-prisma-cloud/local-target/${name}-main`);
|
|
6373
6752
|
}
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6753
|
+
const DATABASE_TYPES = /* @__PURE__ */ new Set(["raw-postgres", "postgres"]);
|
|
6754
|
+
const BUCKET_TYPE = "s3";
|
|
6755
|
+
/**
|
|
6756
|
+
* This extension's nodes that run on an emulator, in `graph.nodes` order —
|
|
6757
|
+
* dependencies first, ties in declaration order — keyed by the name their
|
|
6758
|
+
* resource is given: a service by its `App` name, a database and a bucket by
|
|
6759
|
+
* their resource name — the keys the local providers look them up by.
|
|
6760
|
+
*/
|
|
6761
|
+
function appNodes(input) {
|
|
6762
|
+
return input.graph.nodes.flatMap((n) => {
|
|
6763
|
+
if (n.node.kind === "service" && n.node.extension === "@prisma/composer-prisma-cloud") return [{
|
|
6764
|
+
address: serviceAppName(n.id),
|
|
6765
|
+
kind: "service"
|
|
6766
|
+
}];
|
|
6767
|
+
if (n.node.kind === "resource" && DATABASE_TYPES.has(n.node.type)) return [{
|
|
6768
|
+
address: resourceName(n.id),
|
|
6769
|
+
kind: "database"
|
|
6770
|
+
}];
|
|
6771
|
+
if (n.node.kind === "resource" && n.node.type === BUCKET_TYPE) return [{
|
|
6772
|
+
address: resourceName(n.id),
|
|
6773
|
+
kind: "bucket"
|
|
6774
|
+
}];
|
|
6775
|
+
return [];
|
|
6776
|
+
});
|
|
6383
6777
|
}
|
|
6384
6778
|
async function runDevEmulators(input) {
|
|
6779
|
+
const nodes = appNodes(input);
|
|
6385
6780
|
const { url: computeUrl } = await ensureDaemon("compute", daemonEntry("compute"));
|
|
6386
6781
|
console.log(`[dev] compute emulator ready at ${computeUrl}`);
|
|
6387
|
-
|
|
6388
|
-
if (usesBuckets(input)) {
|
|
6782
|
+
if (nodes.some((node) => node.kind === "bucket")) {
|
|
6389
6783
|
const { url: bucketsUrl } = await ensureDaemon("buckets", daemonEntry("buckets"));
|
|
6390
6784
|
console.log(`[dev] buckets emulator ready at ${bucketsUrl}`);
|
|
6391
6785
|
}
|
|
6392
|
-
if (
|
|
6786
|
+
if (nodes.some((node) => node.kind === "database")) {
|
|
6393
6787
|
const { url: postgresUrl } = await ensureDaemon("postgres", daemonEntry("postgres"));
|
|
6394
6788
|
console.log(`[dev] postgres emulator ready at ${postgresUrl}`);
|
|
6395
6789
|
}
|
|
6790
|
+
const scope = {
|
|
6791
|
+
devDir: input.devDir,
|
|
6792
|
+
app: appNameOf(input.container)
|
|
6793
|
+
};
|
|
6794
|
+
const previous = await selectApp(scope);
|
|
6795
|
+
if (previous !== void 0) console.log(`[dev] stopped the local services of "${previous}", the app run here before; its data is kept`);
|
|
6796
|
+
const portChanges = await ensureAppInstances(scope, nodes).catch(async (error) => {
|
|
6797
|
+
await stopAppInstances(scope).catch(() => void 0);
|
|
6798
|
+
throw error;
|
|
6799
|
+
});
|
|
6800
|
+
for (const { kind, address, previous: before, given } of portChanges) console.log(`[dev] ${kind} "${address}" now uses port ${String(given)}: its previous port ${String(before)} is in use`);
|
|
6396
6801
|
}
|
|
6397
6802
|
/** `local-placeholder-<16 lowercase hex>` — Web Crypto only, matching the extension's other local mints (ServiceKey, S3Credentials). */
|
|
6398
6803
|
function mintPlaceholder() {
|
|
@@ -6433,21 +6838,25 @@ async function runDevPreflight(input) {
|
|
|
6433
6838
|
}));
|
|
6434
6839
|
if (missing.length > 0) throw missingEnvParamsError(missing);
|
|
6435
6840
|
}
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
}
|
|
6841
|
+
const DAEMON_OF = {
|
|
6842
|
+
service: "compute",
|
|
6843
|
+
database: "postgres",
|
|
6844
|
+
bucket: "buckets"
|
|
6845
|
+
};
|
|
6441
6846
|
async function runDevTeardown(input) {
|
|
6442
6847
|
const app = prismaCloudContainerOf(input.container).input.appName;
|
|
6443
6848
|
const cwd = process.cwd();
|
|
6444
|
-
|
|
6849
|
+
const devDir = `${cwd}/${DEV_DIR}`;
|
|
6850
|
+
const scope = {
|
|
6851
|
+
devDir,
|
|
6852
|
+
app
|
|
6853
|
+
};
|
|
6445
6854
|
try {
|
|
6446
|
-
await ensureDaemon(
|
|
6447
|
-
await
|
|
6855
|
+
for (const kind of await recordedKinds(scope)) await ensureDaemon(DAEMON_OF[kind], daemonEntry(DAEMON_OF[kind]));
|
|
6856
|
+
await deleteAppInstances(scope);
|
|
6448
6857
|
} finally {
|
|
6449
|
-
|
|
6450
|
-
removeLocalPaths([`${cwd}
|
|
6858
|
+
removeAppDevState(devDir, app);
|
|
6859
|
+
removeLocalPaths([`${cwd}/.alchemy/state/${app}/dev`]);
|
|
6451
6860
|
}
|
|
6452
6861
|
}
|
|
6453
6862
|
/** `localTargetProviders()`'s `ProviderCollection` doesn't structurally unify with Alchemy's inferred providers Layer (a @internal/lowering typings gap); it satisfies it at runtime — mirrors `control/extension.ts`'s own `asProvidersLayer`, duplicated rather than shared so this module imports nothing from `./control`. */
|
|
@@ -6462,7 +6871,7 @@ function asProvidersLayer(layer) {
|
|
|
6462
6871
|
function localTargetDescriptor() {
|
|
6463
6872
|
return {
|
|
6464
6873
|
container: devContainerDescriptor(),
|
|
6465
|
-
providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(),
|
|
6874
|
+
providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(), LocalOrmMigrationProvider(), S3CredentialsProvider(), GeneratedParamProvider(), ServiceKeyProvider())),
|
|
6466
6875
|
preflight: (input) => runDevPreflight(input),
|
|
6467
6876
|
emulators: (input) => runDevEmulators(input),
|
|
6468
6877
|
attach: (input) => devAttach(input),
|