@prisma/composer-prisma-cloud 0.30.0-dev.3 → 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 +101 -82
- package/dist/buckets-main.mjs.map +1 -1
- package/dist/control.mjs +3 -3
- package/dist/control.mjs.map +1 -1
- package/dist/local-target.mjs +614 -412
- package/dist/local-target.mjs.map +1 -1
- package/dist/postgres-main.mjs +324 -193
- package/dist/postgres-main.mjs.map +1 -1
- package/dist/{s3-credentials-resource-BZjtuopJ-azxxffwQ.mjs → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs} +50 -10
- package/dist/{s3-credentials-resource-BZjtuopJ-azxxffwQ.mjs.map → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs.map} +1 -1
- package/package.json +18 -18
- package/dist/segments-CA7TOJ1U-CoICbb95.mjs +0 -14
- package/dist/segments-CA7TOJ1U-CoICbb95.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";
|
|
@@ -2051,50 +2051,6 @@ async function ensureDaemon(name, entry, opts = {}) {
|
|
|
2051
2051
|
}
|
|
2052
2052
|
}
|
|
2053
2053
|
//#endregion
|
|
2054
|
-
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/segments-CA7TOJ1U.mjs
|
|
2055
|
-
/**
|
|
2056
|
-
* API hygiene shared by both daemons (local-dev spec § 2): every `<app>`,
|
|
2057
|
-
* `<id>`, and `<name>` path segment must match this shape or the request is
|
|
2058
|
-
* a 400 naming the segment.
|
|
2059
|
-
*/
|
|
2060
|
-
const SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
2061
|
-
function isValidSegment(segment) {
|
|
2062
|
-
return segment.length <= 63 && SEGMENT_RE.test(segment);
|
|
2063
|
-
}
|
|
2064
|
-
//#endregion
|
|
2065
|
-
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/instance-name-BF6J_weL.mjs
|
|
2066
|
-
/**
|
|
2067
|
-
* `pcdev-<app>-<database-id>` instance-name derivation (local-dev spec § 2
|
|
2068
|
-
* `postgres-main.ts`): each half lowercased, every char outside `[a-z0-9]`
|
|
2069
|
-
* replaced by `-`, runs collapsed, the combined name trimmed to 63 chars.
|
|
2070
|
-
*
|
|
2071
|
-
* A separate, side-effect-free module (not defined inline in
|
|
2072
|
-
* `postgres-main.ts`) so it can be imported directly by tests without also
|
|
2073
|
-
* running that file's own `main()` — `postgres-main.ts` is a daemon
|
|
2074
|
-
* entrypoint script, always invoked as a subprocess, and calls `main()`
|
|
2075
|
-
* unconditionally at module load.
|
|
2076
|
-
*/
|
|
2077
|
-
/**
|
|
2078
|
-
* Deliberately linear, no ambiguous quantifiers: a per-character replace
|
|
2079
|
-
* (no `+`, so no run-length backtracking surface), a bounded-quantifier
|
|
2080
|
-
* collapse (`{2,}`, not alternation-with-quantifiers), and plain
|
|
2081
|
-
* index-walking for the leading/trailing trim instead of a regex — a
|
|
2082
|
-
* combined `+`-plus-alternation trim (`/^-+|-+$/g`) is exactly the shape
|
|
2083
|
-
* CodeQL's polynomial-ReDoS check flags, whether or not this particular
|
|
2084
|
-
* instance is provably safe.
|
|
2085
|
-
*/
|
|
2086
|
-
function slug(segment) {
|
|
2087
|
-
const collapsed = segment.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-{2,}/g, "-");
|
|
2088
|
-
let start = 0;
|
|
2089
|
-
let end = collapsed.length;
|
|
2090
|
-
while (start < end && collapsed[start] === "-") start++;
|
|
2091
|
-
while (end > start && collapsed[end - 1] === "-") end--;
|
|
2092
|
-
return collapsed.slice(start, end);
|
|
2093
|
-
}
|
|
2094
|
-
function instanceNameFor(app, id) {
|
|
2095
|
-
return `pcdev-${slug(app)}-${slug(id)}`.slice(0, 63);
|
|
2096
|
-
}
|
|
2097
|
-
//#endregion
|
|
2098
2054
|
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/index.mjs
|
|
2099
2055
|
/**
|
|
2100
2056
|
* Typed loopback clients for both emulator daemons (local-dev spec § 2),
|
|
@@ -2126,10 +2082,6 @@ function resolveBaseUrl(name, opts) {
|
|
|
2126
2082
|
if (!isRegistryEntry(parsed) || !isPidAlive(parsed.pid)) throw notRunningError(name);
|
|
2127
2083
|
return `http://127.0.0.1:${parsed.port}`;
|
|
2128
2084
|
}
|
|
2129
|
-
function encodeSegment(segment) {
|
|
2130
|
-
if (!isValidSegment(segment)) throw new Error(`invalid path segment "${segment}"`);
|
|
2131
|
-
return encodeURIComponent(segment);
|
|
2132
|
-
}
|
|
2133
2085
|
const ADMIN_FETCH_ATTEMPTS = 3;
|
|
2134
2086
|
const ADMIN_FETCH_RETRY_DELAY_MS = 150;
|
|
2135
2087
|
/**
|
|
@@ -2176,11 +2128,11 @@ const INSTANCE_STATUSES = /* @__PURE__ */ new Set([
|
|
|
2176
2128
|
function isStringRecord$1(value) {
|
|
2177
2129
|
return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
|
|
2178
2130
|
}
|
|
2179
|
-
function
|
|
2131
|
+
function isServiceInstanceInfo(value) {
|
|
2180
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";
|
|
2181
2133
|
}
|
|
2182
|
-
function
|
|
2183
|
-
return Array.isArray(value) && value.every(
|
|
2134
|
+
function isServiceInstanceInfoArray(value) {
|
|
2135
|
+
return Array.isArray(value) && value.every(isServiceInstanceInfo);
|
|
2184
2136
|
}
|
|
2185
2137
|
function isHealthBody(value) {
|
|
2186
2138
|
return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
|
|
@@ -2206,19 +2158,19 @@ function computeClient(opts = {}) {
|
|
|
2206
2158
|
},
|
|
2207
2159
|
async createInstance(request) {
|
|
2208
2160
|
const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
|
|
2209
|
-
if (!
|
|
2161
|
+
if (!isServiceInstanceInfo(body)) throw new Error("malformed instance-create response from the compute emulator");
|
|
2210
2162
|
return body;
|
|
2211
2163
|
},
|
|
2212
2164
|
async listInstances() {
|
|
2213
2165
|
const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
|
|
2214
|
-
if (!
|
|
2166
|
+
if (!isServiceInstanceInfoArray(body)) throw new Error("malformed instances listing from the compute emulator");
|
|
2215
2167
|
return body;
|
|
2216
2168
|
},
|
|
2217
2169
|
async getInstance(id) {
|
|
2218
2170
|
const res = await adminFetch(instancePath(baseUrl, id));
|
|
2219
2171
|
if (res.status === 404) return void 0;
|
|
2220
2172
|
const body = await (await expectOk(res)).json();
|
|
2221
|
-
if (!
|
|
2173
|
+
if (!isServiceInstanceInfo(body)) throw new Error("malformed instance response from the compute emulator");
|
|
2222
2174
|
return body;
|
|
2223
2175
|
},
|
|
2224
2176
|
async putDeployment(id, deployment) {
|
|
@@ -2255,8 +2207,15 @@ function computeClient(opts = {}) {
|
|
|
2255
2207
|
}
|
|
2256
2208
|
};
|
|
2257
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
|
+
}
|
|
2258
2216
|
function bucketsClient(opts = {}) {
|
|
2259
2217
|
const baseUrl = resolveBaseUrl("buckets", opts);
|
|
2218
|
+
const adminUrl = `${baseUrl}/_pcdev/instances`;
|
|
2260
2219
|
return {
|
|
2261
2220
|
baseUrl,
|
|
2262
2221
|
async health() {
|
|
@@ -2264,15 +2223,25 @@ function bucketsClient(opts = {}) {
|
|
|
2264
2223
|
if (!isHealthBody(body)) throw new Error("malformed /_pcdev/health response from the buckets emulator");
|
|
2265
2224
|
return body;
|
|
2266
2225
|
},
|
|
2267
|
-
async
|
|
2268
|
-
await expectOk(await adminFetch(
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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;
|
|
2235
|
+
},
|
|
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;
|
|
2273
2242
|
},
|
|
2274
|
-
async putCredentials(
|
|
2275
|
-
await expectOk(await adminFetch(`${baseUrl}/_pcdev
|
|
2243
|
+
async putCredentials(id, accessKeyId, secretAccessKey) {
|
|
2244
|
+
await expectOk(await adminFetch(`${instancePath(`${baseUrl}/_pcdev`, id)}/credentials`, {
|
|
2276
2245
|
method: "PUT",
|
|
2277
2246
|
headers: { "content-type": "application/json" },
|
|
2278
2247
|
body: JSON.stringify({
|
|
@@ -2281,19 +2250,16 @@ function bucketsClient(opts = {}) {
|
|
|
2281
2250
|
})
|
|
2282
2251
|
}));
|
|
2283
2252
|
},
|
|
2284
|
-
async
|
|
2285
|
-
await expectOk(await adminFetch(`${baseUrl}/_pcdev
|
|
2253
|
+
async deleteInstance(id) {
|
|
2254
|
+
await expectOk(await adminFetch(instancePath(`${baseUrl}/_pcdev`, id), { method: "DELETE" }));
|
|
2286
2255
|
}
|
|
2287
2256
|
};
|
|
2288
2257
|
}
|
|
2289
|
-
function
|
|
2290
|
-
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "
|
|
2291
|
-
}
|
|
2292
|
-
function isDatabaseInfoArray(value) {
|
|
2293
|
-
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");
|
|
2294
2260
|
}
|
|
2295
|
-
function
|
|
2296
|
-
return
|
|
2261
|
+
function isDatabaseInstanceInfoArray(value) {
|
|
2262
|
+
return Array.isArray(value) && value.every(isDatabaseInstanceInfo);
|
|
2297
2263
|
}
|
|
2298
2264
|
function postgresClient(opts = {}) {
|
|
2299
2265
|
const baseUrl = resolveBaseUrl("postgres", opts);
|
|
@@ -2304,22 +2270,40 @@ function postgresClient(opts = {}) {
|
|
|
2304
2270
|
if (!isHealthBody(body)) throw new Error("malformed /health response from the postgres emulator");
|
|
2305
2271
|
return body;
|
|
2306
2272
|
},
|
|
2307
|
-
async
|
|
2308
|
-
const body = await (await expectOk(await adminFetch(`${baseUrl}/
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
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");
|
|
2276
|
+
return body;
|
|
2277
|
+
},
|
|
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");
|
|
2314
2281
|
return body;
|
|
2315
2282
|
},
|
|
2316
|
-
async
|
|
2317
|
-
const
|
|
2318
|
-
if (
|
|
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");
|
|
2319
2288
|
return body;
|
|
2320
2289
|
},
|
|
2321
|
-
async
|
|
2322
|
-
await expectOk(await adminFetch(`${baseUrl}/
|
|
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
|
+
}));
|
|
2323
2307
|
}
|
|
2324
2308
|
};
|
|
2325
2309
|
}
|
|
@@ -5640,6 +5624,386 @@ function appNameOf(container) {
|
|
|
5640
5624
|
return container.input.appName;
|
|
5641
5625
|
}
|
|
5642
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
|
+
/**
|
|
5643
6007
|
* Shared helpers for the upstream-attribute records every local provider
|
|
5644
6008
|
* emits — one implementation, so the compute and postgres families cannot
|
|
5645
6009
|
* drift on the `'local'` project fallback or the timestamp.
|
|
@@ -5658,26 +6022,38 @@ function projectIdOfInput(value) {
|
|
|
5658
6022
|
* already up by the time these run, since the extension's
|
|
5659
6023
|
* `localTarget.emulators` hook ensures it before converge.
|
|
5660
6024
|
*/
|
|
5661
|
-
/** Upstream lets `name` default to the resource's logical ID; locally a bucket must have a concrete name to register under. */
|
|
5662
|
-
const bucketNameOf = (news, id) => news.name ?? id;
|
|
5663
6025
|
/** Reads a bucket id from upstream's `bucket` input: a plain string or a resolved `Prisma.Bucket` attributes record. */
|
|
5664
6026
|
function bucketIdOfInput(value) {
|
|
5665
6027
|
if (typeof value === "string") return value;
|
|
5666
6028
|
if (Predicate.isObject(value) && typeof value["bucketId"] === "string") return value["bucketId"];
|
|
5667
6029
|
return "local";
|
|
5668
6030
|
}
|
|
5669
|
-
|
|
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
|
+
*/
|
|
5670
6049
|
function LocalBucketProvider(input) {
|
|
5671
6050
|
return Provider.effect(Bucket, Effect.succeed({
|
|
5672
6051
|
list: () => Effect.succeed([]),
|
|
5673
6052
|
reconcile: ({ news, id }) => Effect.tryPromise({
|
|
5674
6053
|
try: async () => {
|
|
5675
|
-
const
|
|
5676
|
-
const name = bucketNameOf(news, id);
|
|
5677
|
-
const dir = path$1.join(input.devDir, "buckets", name);
|
|
5678
|
-
await bucketsClient().putBucket(app, name, dir);
|
|
6054
|
+
const name = recordKeyOf("bucket", id, news.name);
|
|
5679
6055
|
return {
|
|
5680
|
-
bucketId: name,
|
|
6056
|
+
bucketId: (await ensureBucketInstance(appScopeOf$2(input), name, bucketDataDir(appScopeOf$2(input), name))).id,
|
|
5681
6057
|
name,
|
|
5682
6058
|
projectId: projectIdOfInput(news.project),
|
|
5683
6059
|
createdAt: DEV_TIMESTAMP,
|
|
@@ -5686,6 +6062,13 @@ function LocalBucketProvider(input) {
|
|
|
5686
6062
|
},
|
|
5687
6063
|
catch: (cause) => cause
|
|
5688
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
|
+
}),
|
|
5689
6072
|
delete: () => Effect.void,
|
|
5690
6073
|
read: ({ output }) => Effect.succeed(output)
|
|
5691
6074
|
}));
|
|
@@ -5697,26 +6080,25 @@ function LocalBucketProvider(input) {
|
|
|
5697
6080
|
* emulator, so a bucket emulator whose own state was wiped self-heals on the
|
|
5698
6081
|
* next converge.
|
|
5699
6082
|
*/
|
|
5700
|
-
function LocalBucketAccessKeyProvider(
|
|
6083
|
+
function LocalBucketAccessKeyProvider(_input) {
|
|
5701
6084
|
return Provider.effect(BucketAccessKey, Effect.succeed({
|
|
5702
6085
|
list: () => Effect.succeed([]),
|
|
5703
6086
|
reconcile: ({ news, output, id }) => Effect.tryPromise({
|
|
5704
6087
|
try: async () => {
|
|
5705
|
-
const app = appNameOf(input.container);
|
|
5706
6088
|
const pair = output !== void 0 ? {
|
|
5707
6089
|
accessKeyId: output.accessKeyId,
|
|
5708
6090
|
secretAccessKey: Redacted.value(output.secretAccessKey)
|
|
5709
6091
|
} : mintKeyPair();
|
|
5710
6092
|
const client = bucketsClient();
|
|
5711
|
-
await client.putCredentials(app, pair.accessKeyId, pair.secretAccessKey);
|
|
5712
6093
|
const bucketId = bucketIdOfInput(news.bucket);
|
|
6094
|
+
await client.putCredentials(bucketId, pair.accessKeyId, pair.secretAccessKey);
|
|
5713
6095
|
return {
|
|
5714
6096
|
bucketAccessKeyId: news.name ?? id,
|
|
5715
6097
|
bucketId,
|
|
5716
6098
|
accessKeyId: pair.accessKeyId,
|
|
5717
6099
|
secretAccessKey: Redacted.make(pair.secretAccessKey),
|
|
5718
6100
|
endpoint: client.baseUrl,
|
|
5719
|
-
bucketName:
|
|
6101
|
+
bucketName: bucketId
|
|
5720
6102
|
};
|
|
5721
6103
|
},
|
|
5722
6104
|
catch: (cause) => cause
|
|
@@ -5726,6 +6108,36 @@ function LocalBucketAccessKeyProvider(input) {
|
|
|
5726
6108
|
}));
|
|
5727
6109
|
}
|
|
5728
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
|
+
/**
|
|
5729
6141
|
* Extracts the tar.gz `@internal/lowering`'s `packageComputeArtifact` writes
|
|
5730
6142
|
* (local-dev spec § 4) — emulation-only code, so it lives here rather than
|
|
5731
6143
|
* beside the writer (production source stays physically separated from
|
|
@@ -5790,209 +6202,6 @@ function extractComputeArtifact(tarGzPath, destDir) {
|
|
|
5790
6202
|
}
|
|
5791
6203
|
}
|
|
5792
6204
|
/**
|
|
5793
|
-
* The shared dev-instance store (local-dev spec § 4): the JSON files under
|
|
5794
|
-
* `<devDir>/` every local provider reads or writes. Every write is
|
|
5795
|
-
* temp-then-rename; reads and writes on the SAME file are additionally
|
|
5796
|
-
* serialized behind one in-process async queue per absolute path, because the
|
|
5797
|
-
* local providers reconcile concurrently inside the one alchemy child and a
|
|
5798
|
-
* naive read-modify-write would drop a concurrent writer's key. Postgres
|
|
5799
|
-
* instance state is NOT here (REVISED — operator review of #162): the
|
|
5800
|
-
* `postgres-main` daemon owns it now.
|
|
5801
|
-
*/
|
|
5802
|
-
function isEnoent(err) {
|
|
5803
|
-
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
5804
|
-
}
|
|
5805
|
-
const queues = /* @__PURE__ */ new Map();
|
|
5806
|
-
function enqueue(filePath, task) {
|
|
5807
|
-
const result = (queues.get(filePath) ?? Promise.resolve()).then(() => void 0, () => void 0).then(task);
|
|
5808
|
-
queues.set(filePath, result.then(() => void 0, () => void 0));
|
|
5809
|
-
return result;
|
|
5810
|
-
}
|
|
5811
|
-
async function readJson(filePath, isValid, empty) {
|
|
5812
|
-
let raw;
|
|
5813
|
-
try {
|
|
5814
|
-
raw = await fs$1$1.readFile(filePath, "utf8");
|
|
5815
|
-
} catch (err) {
|
|
5816
|
-
if (isEnoent(err)) return empty;
|
|
5817
|
-
throw err;
|
|
5818
|
-
}
|
|
5819
|
-
let parsed;
|
|
5820
|
-
try {
|
|
5821
|
-
parsed = JSON.parse(raw);
|
|
5822
|
-
} catch {
|
|
5823
|
-
return empty;
|
|
5824
|
-
}
|
|
5825
|
-
return isValid(parsed) ? parsed : empty;
|
|
5826
|
-
}
|
|
5827
|
-
async function writeJsonAtomic(filePath, value, mode) {
|
|
5828
|
-
const dir = path$1.dirname(filePath);
|
|
5829
|
-
await fs$1$1.mkdir(dir, { recursive: true });
|
|
5830
|
-
const tmpPath = path$1.join(dir, `.${path$1.basename(filePath)}.${crypto$1.randomUUID()}.tmp`);
|
|
5831
|
-
const json = `${JSON.stringify(value, null, 2)}\n`;
|
|
5832
|
-
await fs$1$1.writeFile(tmpPath, json, mode !== void 0 ? { mode } : void 0);
|
|
5833
|
-
await fs$1$1.rename(tmpPath, filePath);
|
|
5834
|
-
if (mode !== void 0) await fs$1$1.chmod(filePath, mode);
|
|
5835
|
-
}
|
|
5836
|
-
function createStore(filePath, isValid, empty, mode) {
|
|
5837
|
-
return {
|
|
5838
|
-
read: () => enqueue(filePath, () => readJson(filePath, isValid, empty)),
|
|
5839
|
-
update: (mutate) => enqueue(filePath, async () => {
|
|
5840
|
-
const next = mutate(await readJson(filePath, isValid, empty));
|
|
5841
|
-
await writeJsonAtomic(filePath, next, mode);
|
|
5842
|
-
return next;
|
|
5843
|
-
})
|
|
5844
|
-
};
|
|
5845
|
-
}
|
|
5846
|
-
function isStringRecord(value) {
|
|
5847
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
5848
|
-
}
|
|
5849
|
-
/** `<devDir>/env.json` — every `EnvironmentVariable` row, key → value. Last write wins. */
|
|
5850
|
-
function envStore(devDir) {
|
|
5851
|
-
return createStore(path$1.join(devDir, "env.json"), isStringRecord, {});
|
|
5852
|
-
}
|
|
5853
|
-
/** `<devDir>/secrets.json` — platform var name → value (shell-sourced or minted placeholder). Mode 0o600. */
|
|
5854
|
-
function secretsStore(devDir) {
|
|
5855
|
-
return createStore(path$1.join(devDir, "secrets.json"), isStringRecord, {}, 384);
|
|
5856
|
-
}
|
|
5857
|
-
/**
|
|
5858
|
-
* Which emulator instance backs each node of the app run from a directory.
|
|
5859
|
-
* `prisma dev` records it in `<directory>/.prisma-composer/dev/`, and every
|
|
5860
|
-
* Composer operation on an emulator instance goes through it. The emulators
|
|
5861
|
-
* know nothing about Composer: the labels written on create are for people
|
|
5862
|
-
* reading an emulator listing, and nothing here reads them back.
|
|
5863
|
-
*/
|
|
5864
|
-
const EMULATOR_INSTANCES_FILE = "emulator-instances.json";
|
|
5865
|
-
function isNodeInstance(value) {
|
|
5866
|
-
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";
|
|
5867
|
-
}
|
|
5868
|
-
function isEmulatorInstances(value) {
|
|
5869
|
-
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(isNodeInstance);
|
|
5870
|
-
}
|
|
5871
|
-
function directoryOf(devDir) {
|
|
5872
|
-
const normalized = path$1.normalize(devDir);
|
|
5873
|
-
return normalized.endsWith(DEV_DIR) ? normalized.slice(0, -DEV_DIR.length - 1) : normalized;
|
|
5874
|
-
}
|
|
5875
|
-
function store(devDir) {
|
|
5876
|
-
return createStore(path$1.join(devDir, EMULATOR_INSTANCES_FILE), isEmulatorInstances, void 0);
|
|
5877
|
-
}
|
|
5878
|
-
/** This app's nodes. A record left by another app run from the same directory counts as empty. */
|
|
5879
|
-
async function appNodes(scope) {
|
|
5880
|
-
const current = await store(scope.devDir).read();
|
|
5881
|
-
return current?.app === scope.app ? current.nodes : {};
|
|
5882
|
-
}
|
|
5883
|
-
async function updateNodes(scope, mutate) {
|
|
5884
|
-
await store(scope.devDir).update((current) => {
|
|
5885
|
-
const nodes = { ...current?.app === scope.app ? current.nodes : {} };
|
|
5886
|
-
mutate(nodes);
|
|
5887
|
-
return {
|
|
5888
|
-
app: scope.app,
|
|
5889
|
-
nodes
|
|
5890
|
-
};
|
|
5891
|
-
});
|
|
5892
|
-
}
|
|
5893
|
-
/**
|
|
5894
|
-
* A directory has one dev instance. When the record names another app, that
|
|
5895
|
-
* app's compute instances are deleted and the record starts over for this
|
|
5896
|
-
* one. Returns the previous app's name when it was retired.
|
|
5897
|
-
*/
|
|
5898
|
-
async function retirePreviousApp(scope) {
|
|
5899
|
-
const current = await store(scope.devDir).read();
|
|
5900
|
-
if (current === void 0 || current.app === scope.app) return void 0;
|
|
5901
|
-
const client = computeClient();
|
|
5902
|
-
for (const instance of Object.values(current.nodes)) await client.deleteInstance(instance.id);
|
|
5903
|
-
await store(scope.devDir).update(() => ({
|
|
5904
|
-
app: scope.app,
|
|
5905
|
-
nodes: {}
|
|
5906
|
-
}));
|
|
5907
|
-
return current.app;
|
|
5908
|
-
}
|
|
5909
|
-
/** Each compute node of the app, with the instance that backs it. */
|
|
5910
|
-
async function computeNodeInstances(scope) {
|
|
5911
|
-
return Object.entries(await appNodes(scope)).map(([address, instance]) => ({
|
|
5912
|
-
address,
|
|
5913
|
-
instance
|
|
5914
|
-
}));
|
|
5915
|
-
}
|
|
5916
|
-
/** The compute instance recorded for `address`; undefined when there is none. */
|
|
5917
|
-
async function mappedComputeInstance(scope, address) {
|
|
5918
|
-
return (await appNodes(scope))[address];
|
|
5919
|
-
}
|
|
5920
|
-
/**
|
|
5921
|
-
* The instance that runs the service at `address`. The recorded instance is
|
|
5922
|
-
* reused while the Compute emulator still has it. Otherwise a new one is
|
|
5923
|
-
* created, requesting the recorded port, and recorded.
|
|
5924
|
-
*/
|
|
5925
|
-
async function ensureComputeInstance(scope, address) {
|
|
5926
|
-
const client = computeClient();
|
|
5927
|
-
const recorded = await mappedComputeInstance(scope, address);
|
|
5928
|
-
if (recorded && await client.getInstance(recorded.id)) return { instance: recorded };
|
|
5929
|
-
const created = await client.createInstance({
|
|
5930
|
-
labels: {
|
|
5931
|
-
"composer.directory": directoryOf(scope.devDir),
|
|
5932
|
-
"composer.app": scope.app,
|
|
5933
|
-
"composer.address": address
|
|
5934
|
-
},
|
|
5935
|
-
...recorded ? { port: recorded.port } : {}
|
|
5936
|
-
});
|
|
5937
|
-
const instance = {
|
|
5938
|
-
kind: "compute",
|
|
5939
|
-
id: created.id,
|
|
5940
|
-
port: created.port
|
|
5941
|
-
};
|
|
5942
|
-
await updateNodes(scope, (nodes) => {
|
|
5943
|
-
nodes[address] = instance;
|
|
5944
|
-
});
|
|
5945
|
-
if (recorded && recorded.port !== created.port) return {
|
|
5946
|
-
instance,
|
|
5947
|
-
portChange: {
|
|
5948
|
-
address,
|
|
5949
|
-
previous: recorded.port,
|
|
5950
|
-
given: created.port
|
|
5951
|
-
}
|
|
5952
|
-
};
|
|
5953
|
-
return { instance };
|
|
5954
|
-
}
|
|
5955
|
-
/** The emulator's view of the instance recorded for `address`; undefined when either is missing. */
|
|
5956
|
-
async function liveComputeInstance(scope, address) {
|
|
5957
|
-
const recorded = await mappedComputeInstance(scope, address);
|
|
5958
|
-
return recorded ? computeClient().getInstance(recorded.id) : void 0;
|
|
5959
|
-
}
|
|
5960
|
-
/** Deletes the instance recorded for `address` and forgets it. */
|
|
5961
|
-
async function deleteComputeInstance(scope, address) {
|
|
5962
|
-
const recorded = await mappedComputeInstance(scope, address);
|
|
5963
|
-
if (!recorded) return;
|
|
5964
|
-
await computeClient().deleteInstance(recorded.id);
|
|
5965
|
-
await updateNodes(scope, (nodes) => {
|
|
5966
|
-
delete nodes[address];
|
|
5967
|
-
});
|
|
5968
|
-
}
|
|
5969
|
-
/**
|
|
5970
|
-
* `--fresh`: deletes every instance the directory's record names, whichever
|
|
5971
|
-
* app wrote it, then empties the record. Teardown runs before the emulators
|
|
5972
|
-
* hook, so the record may still name the app run here before.
|
|
5973
|
-
*/
|
|
5974
|
-
async function deleteRecordedInstances(devDir) {
|
|
5975
|
-
const current = await store(devDir).read();
|
|
5976
|
-
if (current === void 0) return;
|
|
5977
|
-
const client = computeClient();
|
|
5978
|
-
for (const instance of Object.values(current.nodes)) await client.deleteInstance(instance.id);
|
|
5979
|
-
await store(devDir).update(() => ({
|
|
5980
|
-
app: current.app,
|
|
5981
|
-
nodes: {}
|
|
5982
|
-
}));
|
|
5983
|
-
}
|
|
5984
|
-
/**
|
|
5985
|
-
* The Compute emulator's failure messages name instances by id; `prisma dev`
|
|
5986
|
-
* names services by address. Rewrites a plain `Error`'s message and leaves
|
|
5987
|
-
* any other error, such as a structured one, untouched.
|
|
5988
|
-
*/
|
|
5989
|
-
function withServiceAddresses(error, nodes) {
|
|
5990
|
-
if (!(error instanceof Error) || error.constructor !== Error) return error;
|
|
5991
|
-
let message = error.message.replace("deploy it with listens: false", "declare it with compute({ listens: false })");
|
|
5992
|
-
for (const { address, instance } of nodes) message = message.replaceAll(`instance "${instance.id}"`, `service "${address}"`);
|
|
5993
|
-
return message === error.message ? error : new Error(message, { cause: error });
|
|
5994
|
-
}
|
|
5995
|
-
/**
|
|
5996
6205
|
* Local compute-cluster providers: upstream alchemy's `Prisma.App` and
|
|
5997
6206
|
* `Prisma.Deployment` become clients of the machine-scoped Compute emulator;
|
|
5998
6207
|
* `Prisma.EnvironmentVariable` becomes a row in the dev env store;
|
|
@@ -6106,7 +6315,7 @@ async function materializeEnv(devDir, address, port) {
|
|
|
6106
6315
|
if (process.env["HOME"] !== void 0) env["HOME"] = process.env["HOME"];
|
|
6107
6316
|
return env;
|
|
6108
6317
|
}
|
|
6109
|
-
function appScopeOf(input) {
|
|
6318
|
+
function appScopeOf$1(input) {
|
|
6110
6319
|
return {
|
|
6111
6320
|
devDir: input.devDir,
|
|
6112
6321
|
app: appNameOf(input.container)
|
|
@@ -6128,7 +6337,7 @@ function LocalAppProvider(input) {
|
|
|
6128
6337
|
reconcile: ({ id, news }) => Effect.tryPromise({
|
|
6129
6338
|
try: async () => {
|
|
6130
6339
|
const name = news.displayName ?? id;
|
|
6131
|
-
const { instance } = await
|
|
6340
|
+
const { instance } = await ensureServiceInstance(appScopeOf$1(input), name);
|
|
6132
6341
|
const url = localUrl(instance.port);
|
|
6133
6342
|
return {
|
|
6134
6343
|
appId: name,
|
|
@@ -6147,41 +6356,18 @@ function LocalAppProvider(input) {
|
|
|
6147
6356
|
diff: ({ output }) => Effect.tryPromise({
|
|
6148
6357
|
try: async () => {
|
|
6149
6358
|
if (output === void 0) return void 0;
|
|
6150
|
-
if ((await
|
|
6359
|
+
if ((await liveServiceInstance(appScopeOf$1(input), output.name))?.url === output.appEndpointDomain) return void 0;
|
|
6151
6360
|
return { action: "update" };
|
|
6152
6361
|
},
|
|
6153
6362
|
catch: (cause) => cause
|
|
6154
6363
|
}),
|
|
6155
6364
|
delete: ({ output }) => Effect.tryPromise({
|
|
6156
|
-
try: () =>
|
|
6365
|
+
try: () => deleteNodeInstance(appScopeOf$1(input), output.name),
|
|
6157
6366
|
catch: (cause) => cause
|
|
6158
6367
|
}),
|
|
6159
6368
|
read: ({ output }) => Effect.succeed(output)
|
|
6160
6369
|
}));
|
|
6161
6370
|
}
|
|
6162
|
-
/**
|
|
6163
|
-
* Ensures each service's emulator instance one at a time, in the order given,
|
|
6164
|
-
* before Alchemy applies the `App` resources concurrently. A new instance gets
|
|
6165
|
-
* the smallest free port, so on a fresh start the ports follow this order; a
|
|
6166
|
-
* service that already has an instance keeps its port. `serviceAppNames` are
|
|
6167
|
-
* the `App` resources' `displayName`s. Returns the services whose instance had
|
|
6168
|
-
* to be re-created and could not get its remembered port back.
|
|
6169
|
-
*/
|
|
6170
|
-
async function ensureServiceInstances(container, devDir, serviceAppNames) {
|
|
6171
|
-
const scope = {
|
|
6172
|
-
devDir,
|
|
6173
|
-
app: appNameOf(container)
|
|
6174
|
-
};
|
|
6175
|
-
const changes = [];
|
|
6176
|
-
for (const service of serviceAppNames) try {
|
|
6177
|
-
const { portChange } = await ensureComputeInstance(scope, service);
|
|
6178
|
-
if (portChange) changes.push(portChange);
|
|
6179
|
-
} catch (cause) {
|
|
6180
|
-
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
6181
|
-
throw new Error(`ensuring the local service for service "${service}" failed: ${reason}`, { cause });
|
|
6182
|
-
}
|
|
6183
|
-
return changes;
|
|
6184
|
-
}
|
|
6185
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. */
|
|
6186
6372
|
function LocalEnvironmentVariableProvider(input) {
|
|
6187
6373
|
return Provider.effect(EnvironmentVariable, Effect.succeed({
|
|
@@ -6239,7 +6425,7 @@ function LocalDeploymentProvider(input) {
|
|
|
6239
6425
|
const artifactDir = path$1.join(input.devDir, "artifacts", artifactHash);
|
|
6240
6426
|
if (!fs$2.existsSync(artifactDir)) extractComputeArtifact(news.artifactPath, artifactDir);
|
|
6241
6427
|
const { address, listens } = readComputeManifest(artifactDir);
|
|
6242
|
-
const { instance } = await
|
|
6428
|
+
const { instance } = await ensureServiceInstance(appScopeOf$1(input), appId);
|
|
6243
6429
|
const env = await materializeEnv(input.devDir, address, instance.port);
|
|
6244
6430
|
try {
|
|
6245
6431
|
await computeClient().putDeployment(instance.id, {
|
|
@@ -6270,7 +6456,7 @@ function LocalDeploymentProvider(input) {
|
|
|
6270
6456
|
diff: ({ output }) => Effect.tryPromise({
|
|
6271
6457
|
try: async () => {
|
|
6272
6458
|
if (output === void 0) return void 0;
|
|
6273
|
-
if ((await
|
|
6459
|
+
if ((await liveServiceInstance(appScopeOf$1(input), output.appId))?.artifactHash === output.artifactHash) return void 0;
|
|
6274
6460
|
return { action: "update" };
|
|
6275
6461
|
},
|
|
6276
6462
|
catch: (cause) => cause
|
|
@@ -6304,57 +6490,28 @@ function LocalProjectProvider(_input) {
|
|
|
6304
6490
|
delete: () => Effect.void
|
|
6305
6491
|
}));
|
|
6306
6492
|
}
|
|
6307
|
-
/**
|
|
6308
|
-
* Local postgres-cluster providers: upstream alchemy's `Prisma.Database` and
|
|
6309
|
-
* `Prisma.Connection` become clients of the `postgres-main` emulator daemon
|
|
6310
|
-
* (one named, persistent `@prisma/dev` server per `Database` resource).
|
|
6311
|
-
* `PgWarm`/`OrmMigration` are not here — the hosted ones run against whatever
|
|
6312
|
-
* URL they are handed. Attributes match upstream's shapes; the daemon's
|
|
6313
|
-
* DIRECT connection string maps to `directConnectionString` and
|
|
6314
|
-
* `databaseUrl`, everything else is left absent. Instance names come from
|
|
6315
|
-
* `@internal/dev-emulators`' own `instanceNameFor` — a locally re-derived
|
|
6316
|
-
* slug drifted from the daemon's and broke `Connection`'s lookup.
|
|
6317
|
-
*/
|
|
6318
6493
|
/** Reads a database id from upstream's `database` input: a plain string or a resolved `Prisma.Database` attributes record. */
|
|
6319
6494
|
function databaseIdOfInput(value) {
|
|
6320
6495
|
if (typeof value === "string") return value;
|
|
6321
6496
|
if (Predicate.isObject(value) && typeof value["databaseId"] === "string") return value["databaseId"];
|
|
6322
6497
|
}
|
|
6323
|
-
function noPrismaDevError(cause) {
|
|
6324
|
-
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)})`);
|
|
6325
|
-
}
|
|
6326
6498
|
/**
|
|
6327
|
-
*
|
|
6328
|
-
*
|
|
6329
|
-
*
|
|
6330
|
-
*
|
|
6331
|
-
*
|
|
6332
|
-
* would otherwise silently replace the version Composer tested against.
|
|
6333
|
-
* The daemon imports the returned path dynamically.
|
|
6334
|
-
*/
|
|
6335
|
-
function resolvePrismaDevModulePath() {
|
|
6336
|
-
try {
|
|
6337
|
-
return createRequire(import.meta.url).resolve("@prisma/dev");
|
|
6338
|
-
} catch (cause) {
|
|
6339
|
-
throw noPrismaDevError(cause);
|
|
6340
|
-
}
|
|
6341
|
-
}
|
|
6342
|
-
/**
|
|
6343
|
-
* `Prisma.Database` → an ensured `postgres-main` server, one per resource.
|
|
6344
|
-
* 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.
|
|
6345
6504
|
*/
|
|
6346
6505
|
function LocalDatabaseProvider(input) {
|
|
6347
6506
|
return Provider.effect(Prisma.Database, Effect.succeed({
|
|
6348
6507
|
list: () => Effect.succeed([]),
|
|
6349
6508
|
reconcile: ({ id, news }) => Effect.tryPromise({
|
|
6350
6509
|
try: async () => {
|
|
6351
|
-
const
|
|
6352
|
-
const
|
|
6353
|
-
const
|
|
6354
|
-
const { url } = await postgresClient().ensureDatabase(app, slug(name), prismaDevModulePath);
|
|
6355
|
-
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);
|
|
6356
6513
|
return {
|
|
6357
|
-
databaseId:
|
|
6514
|
+
databaseId: instance.id,
|
|
6358
6515
|
databaseName: name,
|
|
6359
6516
|
projectId: projectIdOfInput(news.project),
|
|
6360
6517
|
status: "ready",
|
|
@@ -6374,28 +6531,40 @@ function LocalDatabaseProvider(input) {
|
|
|
6374
6531
|
},
|
|
6375
6532
|
catch: (cause) => cause
|
|
6376
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
|
+
}),
|
|
6377
6541
|
delete: () => Effect.void
|
|
6378
6542
|
}));
|
|
6379
6543
|
}
|
|
6544
|
+
function appScopeOf(input) {
|
|
6545
|
+
return {
|
|
6546
|
+
devDir: input.devDir,
|
|
6547
|
+
app: appNameOf(input.container)
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6380
6550
|
function noRecordedInstanceError(databaseId) {
|
|
6381
6551
|
return /* @__PURE__ */ new Error(`no local Postgres instance recorded for databaseId "${databaseId}" — the Database provider did not run; converge is corrupt (try --fresh).`);
|
|
6382
6552
|
}
|
|
6383
|
-
/** `Prisma.Connection` → the
|
|
6384
|
-
function LocalConnectionProvider(
|
|
6553
|
+
/** `Prisma.Connection` → the database instance the Database attributes' `databaseId` names. */
|
|
6554
|
+
function LocalConnectionProvider(_input) {
|
|
6385
6555
|
return Provider.effect(Prisma.Connection, Effect.succeed({
|
|
6386
6556
|
list: () => Effect.succeed([]),
|
|
6387
6557
|
reconcile: ({ id, news }) => Effect.tryPromise({
|
|
6388
6558
|
try: async () => {
|
|
6389
|
-
const app = appNameOf(input.container);
|
|
6390
6559
|
const databaseId = databaseIdOfInput(news.database);
|
|
6391
6560
|
if (databaseId === void 0) throw noRecordedInstanceError(String(news.database));
|
|
6392
|
-
const found =
|
|
6561
|
+
const found = await postgresClient().getInstance(databaseId);
|
|
6393
6562
|
if (found === void 0) throw noRecordedInstanceError(databaseId);
|
|
6394
6563
|
const direct = Redacted.make(found.url);
|
|
6395
6564
|
return {
|
|
6396
|
-
connectionId: found.
|
|
6565
|
+
connectionId: found.id,
|
|
6397
6566
|
connectionName: news.name ?? id,
|
|
6398
|
-
databaseId: found.
|
|
6567
|
+
databaseId: found.id,
|
|
6399
6568
|
kind: "postgres",
|
|
6400
6569
|
createdAt: DEV_TIMESTAMP,
|
|
6401
6570
|
directConnectionString: direct,
|
|
@@ -6441,10 +6610,8 @@ function resolvePackageEntry(specifier) {
|
|
|
6441
6610
|
* ADR-0041 D12). The target extension's `dev/teardown.ts` orchestrates
|
|
6442
6611
|
* (which app, which emulator clients); this module owns the actual
|
|
6443
6612
|
* filesystem operations, so the extension's own source stays free of
|
|
6444
|
-
* `node:`/`bun:` imports (its invariant 5).
|
|
6445
|
-
*
|
|
6446
|
-
* `postgresClient().deleteApp(app)` call, made directly alongside the
|
|
6447
|
-
* 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.
|
|
6448
6615
|
*/
|
|
6449
6616
|
/** `fs.rm(..., { recursive: true, force: true })` for every given path — tolerates absence. */
|
|
6450
6617
|
function removeLocalPaths(paths) {
|
|
@@ -6453,6 +6620,21 @@ function removeLocalPaths(paths) {
|
|
|
6453
6620
|
force: true
|
|
6454
6621
|
});
|
|
6455
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
|
+
}
|
|
6456
6638
|
//#endregion
|
|
6457
6639
|
//#region ../../1-prisma-cloud/1-extensions/target/dist/local-target.mjs
|
|
6458
6640
|
const RELIST_INTERVAL_MS = 2e3;
|
|
@@ -6500,7 +6682,7 @@ async function* mergedLogs(scope, signal, tail) {
|
|
|
6500
6682
|
})();
|
|
6501
6683
|
};
|
|
6502
6684
|
const relist = async () => {
|
|
6503
|
-
for (const { address, instance } of await
|
|
6685
|
+
for (const { address, instance } of await nodeInstancesOf(scope, "service")) follow(instance.id, address);
|
|
6504
6686
|
};
|
|
6505
6687
|
await relist();
|
|
6506
6688
|
const timer = setInterval(() => void relist(), RELIST_INTERVAL_MS);
|
|
@@ -6532,7 +6714,7 @@ async function devAttach(input) {
|
|
|
6532
6714
|
const client = computeClient();
|
|
6533
6715
|
return {
|
|
6534
6716
|
startServices: async () => {
|
|
6535
|
-
const nodes = await
|
|
6717
|
+
const nodes = await nodeInstancesOf(scope, "service");
|
|
6536
6718
|
try {
|
|
6537
6719
|
await client.startInstances(nodes.map(({ instance }) => instance.id));
|
|
6538
6720
|
} catch (error) {
|
|
@@ -6540,16 +6722,13 @@ async function devAttach(input) {
|
|
|
6540
6722
|
}
|
|
6541
6723
|
},
|
|
6542
6724
|
endpoints: async () => {
|
|
6543
|
-
return (await
|
|
6725
|
+
return (await nodeInstancesOf(scope, "service")).map(({ address, instance }) => ({
|
|
6544
6726
|
address,
|
|
6545
6727
|
url: `http://localhost:${String(instance.port)}`
|
|
6546
6728
|
}));
|
|
6547
6729
|
},
|
|
6548
6730
|
logs: (signal, opts) => mergedLogs(scope, signal, opts?.tail ?? 0),
|
|
6549
|
-
|
|
6550
|
-
const nodes = await computeNodeInstances(scope);
|
|
6551
|
-
await client.stopInstances(nodes.map(({ instance }) => instance.id));
|
|
6552
|
-
}
|
|
6731
|
+
stopApp: () => stopAppInstances(scope)
|
|
6553
6732
|
};
|
|
6554
6733
|
}
|
|
6555
6734
|
const LOCAL_PROJECT_ID = "local";
|
|
@@ -6571,34 +6750,54 @@ function devContainerDescriptor() {
|
|
|
6571
6750
|
function daemonEntry(name) {
|
|
6572
6751
|
return resolvePackageEntry(`@prisma/composer-prisma-cloud/local-target/${name}-main`);
|
|
6573
6752
|
}
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
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
|
+
});
|
|
6583
6777
|
}
|
|
6584
6778
|
async function runDevEmulators(input) {
|
|
6779
|
+
const nodes = appNodes(input);
|
|
6585
6780
|
const { url: computeUrl } = await ensureDaemon("compute", daemonEntry("compute"));
|
|
6586
6781
|
console.log(`[dev] compute emulator ready at ${computeUrl}`);
|
|
6587
|
-
|
|
6588
|
-
devDir: input.devDir,
|
|
6589
|
-
app: appNameOf(input.container)
|
|
6590
|
-
});
|
|
6591
|
-
if (retired !== void 0) console.log(`[dev] removed the local services of "${retired}", the app run here before`);
|
|
6592
|
-
const portChanges = await ensureServiceInstances(input.container, input.devDir, serviceAppNames(input));
|
|
6593
|
-
for (const { address, previous, given } of portChanges) console.log(`[dev] service "${address}" now uses port ${String(given)}: its previous port ${String(previous)} is in use`);
|
|
6594
|
-
if (usesBuckets(input)) {
|
|
6782
|
+
if (nodes.some((node) => node.kind === "bucket")) {
|
|
6595
6783
|
const { url: bucketsUrl } = await ensureDaemon("buckets", daemonEntry("buckets"));
|
|
6596
6784
|
console.log(`[dev] buckets emulator ready at ${bucketsUrl}`);
|
|
6597
6785
|
}
|
|
6598
|
-
if (
|
|
6786
|
+
if (nodes.some((node) => node.kind === "database")) {
|
|
6599
6787
|
const { url: postgresUrl } = await ensureDaemon("postgres", daemonEntry("postgres"));
|
|
6600
6788
|
console.log(`[dev] postgres emulator ready at ${postgresUrl}`);
|
|
6601
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`);
|
|
6602
6801
|
}
|
|
6603
6802
|
/** `local-placeholder-<16 lowercase hex>` — Web Crypto only, matching the extension's other local mints (ServiceKey, S3Credentials). */
|
|
6604
6803
|
function mintPlaceholder() {
|
|
@@ -6639,22 +6838,25 @@ async function runDevPreflight(input) {
|
|
|
6639
6838
|
}));
|
|
6640
6839
|
if (missing.length > 0) throw missingEnvParamsError(missing);
|
|
6641
6840
|
}
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
}
|
|
6841
|
+
const DAEMON_OF = {
|
|
6842
|
+
service: "compute",
|
|
6843
|
+
database: "postgres",
|
|
6844
|
+
bucket: "buckets"
|
|
6845
|
+
};
|
|
6647
6846
|
async function runDevTeardown(input) {
|
|
6648
6847
|
const app = prismaCloudContainerOf(input.container).input.appName;
|
|
6649
6848
|
const cwd = process.cwd();
|
|
6650
6849
|
const devDir = `${cwd}/${DEV_DIR}`;
|
|
6651
|
-
|
|
6850
|
+
const scope = {
|
|
6851
|
+
devDir,
|
|
6852
|
+
app
|
|
6853
|
+
};
|
|
6652
6854
|
try {
|
|
6653
|
-
await ensureDaemon(
|
|
6654
|
-
await
|
|
6855
|
+
for (const kind of await recordedKinds(scope)) await ensureDaemon(DAEMON_OF[kind], daemonEntry(DAEMON_OF[kind]));
|
|
6856
|
+
await deleteAppInstances(scope);
|
|
6655
6857
|
} finally {
|
|
6656
|
-
|
|
6657
|
-
removeLocalPaths([
|
|
6858
|
+
removeAppDevState(devDir, app);
|
|
6859
|
+
removeLocalPaths([`${cwd}/.alchemy/state/${app}/dev`]);
|
|
6658
6860
|
}
|
|
6659
6861
|
}
|
|
6660
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`. */
|
|
@@ -6669,7 +6871,7 @@ function asProvidersLayer(layer) {
|
|
|
6669
6871
|
function localTargetDescriptor() {
|
|
6670
6872
|
return {
|
|
6671
6873
|
container: devContainerDescriptor(),
|
|
6672
|
-
providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(),
|
|
6874
|
+
providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(), LocalOrmMigrationProvider(), S3CredentialsProvider(), GeneratedParamProvider(), ServiceKeyProvider())),
|
|
6673
6875
|
preflight: (input) => runDevPreflight(input),
|
|
6674
6876
|
emulators: (input) => runDevEmulators(input),
|
|
6675
6877
|
attach: (input) => devAttach(input),
|