@prisma/composer-prisma-cloud 0.2.0-dev.13 → 0.2.0-dev.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/local-target.mjs +89 -37
- package/dist/local-target.mjs.map +1 -1
- package/dist/postgres-main.mjs +106 -16
- package/dist/postgres-main.mjs.map +1 -1
- package/package.json +17 -17
package/dist/local-target.mjs
CHANGED
|
@@ -1986,6 +1986,27 @@ function encodeSegment(segment) {
|
|
|
1986
1986
|
if (!isValidSegment(segment)) throw new Error(`invalid path segment "${segment}"`);
|
|
1987
1987
|
return encodeURIComponent(segment);
|
|
1988
1988
|
}
|
|
1989
|
+
const ADMIN_FETCH_ATTEMPTS = 3;
|
|
1990
|
+
const ADMIN_FETCH_RETRY_DELAY_MS = 150;
|
|
1991
|
+
/**
|
|
1992
|
+
* `fetch` for the loopback admin calls, retried on transient socket errors:
|
|
1993
|
+
* a keep-alive reuse race (the runtime re-uses a pooled connection the
|
|
1994
|
+
* daemon's HTTP server just closed idle — surfaces as "fetch failed" /
|
|
1995
|
+
* "other side closed") or a briefly overloaded daemon. Every admin call is
|
|
1996
|
+
* idempotent by design, so a short blind retry is safe. A caller-driven
|
|
1997
|
+
* abort is never retried.
|
|
1998
|
+
*/
|
|
1999
|
+
async function adminFetch(url, init) {
|
|
2000
|
+
let lastError;
|
|
2001
|
+
for (let attempt = 1; attempt <= ADMIN_FETCH_ATTEMPTS; attempt += 1) try {
|
|
2002
|
+
return await fetch(url, init);
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
if (init?.signal?.aborted === true) throw error;
|
|
2005
|
+
lastError = error;
|
|
2006
|
+
if (attempt < ADMIN_FETCH_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, ADMIN_FETCH_RETRY_DELAY_MS));
|
|
2007
|
+
}
|
|
2008
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
2009
|
+
}
|
|
1989
2010
|
async function expectOk(res) {
|
|
1990
2011
|
if (!res.ok) {
|
|
1991
2012
|
const body = await res.text();
|
|
@@ -2016,33 +2037,29 @@ function computeClient(opts = {}) {
|
|
|
2016
2037
|
return {
|
|
2017
2038
|
baseUrl,
|
|
2018
2039
|
async health() {
|
|
2019
|
-
const body = await (await expectOk(await
|
|
2040
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/health`))).json();
|
|
2020
2041
|
if (!isHealthBody(body)) throw new Error("malformed /health response from the compute emulator");
|
|
2021
2042
|
return body;
|
|
2022
2043
|
},
|
|
2023
2044
|
async ensureService(app, id) {
|
|
2024
|
-
const
|
|
2025
|
-
const body = await (await expectOk(await fetch(url, { method: "PUT" }))).json();
|
|
2045
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}`, { method: "PUT" }))).json();
|
|
2026
2046
|
if (!isServiceReservation(body)) throw new Error("malformed service-reservation response from the compute emulator");
|
|
2027
2047
|
return body;
|
|
2028
2048
|
},
|
|
2029
2049
|
async putDeployment(app, id, deployment) {
|
|
2030
|
-
|
|
2031
|
-
await expectOk(await fetch(url, {
|
|
2050
|
+
await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}/deployment`, {
|
|
2032
2051
|
method: "PUT",
|
|
2033
2052
|
headers: { "content-type": "application/json" },
|
|
2034
2053
|
body: JSON.stringify(deployment)
|
|
2035
2054
|
}));
|
|
2036
2055
|
},
|
|
2037
2056
|
async listServices(app) {
|
|
2038
|
-
const
|
|
2039
|
-
const body = await (await expectOk(await fetch(url))).json();
|
|
2057
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services`))).json();
|
|
2040
2058
|
if (!isServiceInfoArray(body)) throw new Error("malformed services listing from the compute emulator");
|
|
2041
2059
|
return body;
|
|
2042
2060
|
},
|
|
2043
2061
|
async *followLogs(app, id, signal) {
|
|
2044
|
-
const
|
|
2045
|
-
const body = (await expectOk(await fetch(url, signal ? { signal } : void 0))).body;
|
|
2062
|
+
const body = (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}/logs?follow=1`, signal ? { signal } : void 0))).body;
|
|
2046
2063
|
if (!body) return;
|
|
2047
2064
|
const reader = body.getReader();
|
|
2048
2065
|
const decoder = new TextDecoder();
|
|
@@ -2057,16 +2074,13 @@ function computeClient(opts = {}) {
|
|
|
2057
2074
|
}
|
|
2058
2075
|
},
|
|
2059
2076
|
async stopApp(app) {
|
|
2060
|
-
|
|
2061
|
-
await expectOk(await fetch(url, { method: "POST" }));
|
|
2077
|
+
await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/stop`, { method: "POST" }));
|
|
2062
2078
|
},
|
|
2063
2079
|
async startApp(app) {
|
|
2064
|
-
|
|
2065
|
-
await expectOk(await fetch(url, { method: "POST" }));
|
|
2080
|
+
await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/start`, { method: "POST" }));
|
|
2066
2081
|
},
|
|
2067
2082
|
async deleteApp(app) {
|
|
2068
|
-
|
|
2069
|
-
await expectOk(await fetch(url, { method: "DELETE" }));
|
|
2083
|
+
await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}`, { method: "DELETE" }));
|
|
2070
2084
|
}
|
|
2071
2085
|
};
|
|
2072
2086
|
}
|
|
@@ -2075,21 +2089,19 @@ function bucketsClient(opts = {}) {
|
|
|
2075
2089
|
return {
|
|
2076
2090
|
baseUrl,
|
|
2077
2091
|
async health() {
|
|
2078
|
-
const body = await (await expectOk(await
|
|
2092
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/_pcdev/health`))).json();
|
|
2079
2093
|
if (!isHealthBody(body)) throw new Error("malformed /_pcdev/health response from the buckets emulator");
|
|
2080
2094
|
return body;
|
|
2081
2095
|
},
|
|
2082
2096
|
async putBucket(app, name, dir) {
|
|
2083
|
-
|
|
2084
|
-
await expectOk(await fetch(url, {
|
|
2097
|
+
await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}/buckets/${encodeSegment(name)}`, {
|
|
2085
2098
|
method: "PUT",
|
|
2086
2099
|
headers: { "content-type": "application/json" },
|
|
2087
2100
|
body: JSON.stringify({ dir })
|
|
2088
2101
|
}));
|
|
2089
2102
|
},
|
|
2090
2103
|
async putCredentials(app, accessKeyId, secretAccessKey) {
|
|
2091
|
-
|
|
2092
|
-
await expectOk(await fetch(url, {
|
|
2104
|
+
await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}/credentials`, {
|
|
2093
2105
|
method: "PUT",
|
|
2094
2106
|
headers: { "content-type": "application/json" },
|
|
2095
2107
|
body: JSON.stringify({
|
|
@@ -2099,8 +2111,7 @@ function bucketsClient(opts = {}) {
|
|
|
2099
2111
|
}));
|
|
2100
2112
|
},
|
|
2101
2113
|
async deleteApp(app) {
|
|
2102
|
-
|
|
2103
|
-
await expectOk(await fetch(url, { method: "DELETE" }));
|
|
2114
|
+
await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}`, { method: "DELETE" }));
|
|
2104
2115
|
}
|
|
2105
2116
|
};
|
|
2106
2117
|
}
|
|
@@ -2118,13 +2129,12 @@ function postgresClient(opts = {}) {
|
|
|
2118
2129
|
return {
|
|
2119
2130
|
baseUrl,
|
|
2120
2131
|
async health() {
|
|
2121
|
-
const body = await (await expectOk(await
|
|
2132
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/health`))).json();
|
|
2122
2133
|
if (!isHealthBody(body)) throw new Error("malformed /health response from the postgres emulator");
|
|
2123
2134
|
return body;
|
|
2124
2135
|
},
|
|
2125
2136
|
async ensureDatabase(app, id, prismaDevModulePath) {
|
|
2126
|
-
const
|
|
2127
|
-
const body = await (await expectOk(await fetch(url, {
|
|
2137
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/databases/${encodeSegment(id)}`, {
|
|
2128
2138
|
method: "PUT",
|
|
2129
2139
|
headers: { "content-type": "application/json" },
|
|
2130
2140
|
body: JSON.stringify({ prismaDevModulePath })
|
|
@@ -2133,14 +2143,12 @@ function postgresClient(opts = {}) {
|
|
|
2133
2143
|
return body;
|
|
2134
2144
|
},
|
|
2135
2145
|
async listDatabases(app) {
|
|
2136
|
-
const
|
|
2137
|
-
const body = await (await expectOk(await fetch(url))).json();
|
|
2146
|
+
const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/databases`))).json();
|
|
2138
2147
|
if (!isDatabaseInfoArray(body)) throw new Error("malformed databases listing from the postgres emulator");
|
|
2139
2148
|
return body;
|
|
2140
2149
|
},
|
|
2141
2150
|
async deleteApp(app) {
|
|
2142
|
-
|
|
2143
|
-
await expectOk(await fetch(url, { method: "DELETE" }));
|
|
2151
|
+
await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}`, { method: "DELETE" }));
|
|
2144
2152
|
}
|
|
2145
2153
|
};
|
|
2146
2154
|
}
|
|
@@ -5668,6 +5676,32 @@ function servicePortEnvKey(address) {
|
|
|
5668
5676
|
"PORT"
|
|
5669
5677
|
].join("_").toUpperCase();
|
|
5670
5678
|
}
|
|
5679
|
+
/** The `COMPOSER_<ADDRESS SEGMENTS>_` prefix every env row this address OWNS carries — `configKey`'s convention (see `servicePortEnvKey`'s doc comment). */
|
|
5680
|
+
function ownEnvKeyPrefix(address) {
|
|
5681
|
+
return `${["COMPOSER", ...address.split(".").filter((s) => s.length > 0)].join("_").toUpperCase()}_`;
|
|
5682
|
+
}
|
|
5683
|
+
const COMPOSER_NAMESPACE_PREFIX = "COMPOSER_";
|
|
5684
|
+
/**
|
|
5685
|
+
* Scopes `env.json` to what THIS service is allowed to see: rows it owns
|
|
5686
|
+
* (`COMPOSER_<its address>_*`) plus every row OUTSIDE the `COMPOSER_`
|
|
5687
|
+
* namespace entirely — the poison `DATABASE_URL(_POOLED)` rows are
|
|
5688
|
+
* deliberately unprefixed and app-wide (local-dev spec § 4's pinned parity
|
|
5689
|
+
* note). The hosted platform materializes the app-wide row set into every
|
|
5690
|
+
* deployment but DIFFS a deployment only on its own referenced rows; an
|
|
5691
|
+
* app-wide LOCAL materialization restart-amplifies instead — an
|
|
5692
|
+
* early-deployed service's snapshot is incomplete on the first converge,
|
|
5693
|
+
* "completes" on the second, and diffs as changed. Scoping the content here
|
|
5694
|
+
* aligns local restart behavior with the platform's diff scope. The dropped
|
|
5695
|
+
* sibling rows have no sanctioned reader: `run()`/`load()` consume only
|
|
5696
|
+
* own-address rows, and ambient sibling reads are exactly what the poison
|
|
5697
|
+
* rows exist to punish.
|
|
5698
|
+
*/
|
|
5699
|
+
function scopedEnvRows(allRows, address) {
|
|
5700
|
+
const ownPrefix = ownEnvKeyPrefix(address);
|
|
5701
|
+
const scoped = {};
|
|
5702
|
+
for (const [key, value] of Object.entries(allRows)) if (key.startsWith(ownPrefix) || !key.startsWith(COMPOSER_NAMESPACE_PREFIX)) scoped[key] = value;
|
|
5703
|
+
return scoped;
|
|
5704
|
+
}
|
|
5671
5705
|
function manifestMissingAddressError() {
|
|
5672
5706
|
return /* @__PURE__ */ new Error("artifact manifest carries no address — repackage with a current @prisma/composer.");
|
|
5673
5707
|
}
|
|
@@ -5692,8 +5726,24 @@ function readManifestAddress(artifactDir) {
|
|
|
5692
5726
|
if (!isComputeManifest(parsed) || parsed.address === void 0) throw manifestMissingAddressError();
|
|
5693
5727
|
return parsed.address;
|
|
5694
5728
|
}
|
|
5729
|
+
/**
|
|
5730
|
+
* The Compute emulator's `<id>` path segment must match
|
|
5731
|
+
* `/^[a-z0-9][a-z0-9-]*$/` (its API hygiene rule, local-dev spec § 2) — but a
|
|
5732
|
+
* service's own address (`news.name`/`news.computeServiceId`) is
|
|
5733
|
+
* hierarchical and dot-separated (e.g. `"orders.service"`, a nested
|
|
5734
|
+
* module's service). This is the seam: every dot (or other disallowed char)
|
|
5735
|
+
* becomes a dash, runs collapse, and the result is what both `ensureService`
|
|
5736
|
+
* and `putDeployment` address the emulator with — the REAL address still
|
|
5737
|
+
* rides the deployment body's `address` field untouched, so the front door
|
|
5738
|
+
* and every listing still show it verbatim (compute-main.ts's `svc.address`
|
|
5739
|
+
* is set from that field, not from the id).
|
|
5740
|
+
*/
|
|
5741
|
+
function slugServiceId(address) {
|
|
5742
|
+
const slug = address.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5743
|
+
return slug.length > 0 ? slug : "svc";
|
|
5744
|
+
}
|
|
5695
5745
|
async function materializeEnv(devDir, address, port) {
|
|
5696
|
-
const env =
|
|
5746
|
+
const env = scopedEnvRows(await envStore(devDir).read(), address);
|
|
5697
5747
|
env[servicePortEnvKey(address)] = JSON.stringify(port);
|
|
5698
5748
|
const secrets = await secretsStore(devDir).read();
|
|
5699
5749
|
for (const [key, value] of Object.entries(secrets)) env[key] = value;
|
|
@@ -5712,7 +5762,7 @@ function LocalComputeServiceProvider(input) {
|
|
|
5712
5762
|
reconcile: ({ news }) => Effect.tryPromise({
|
|
5713
5763
|
try: async () => {
|
|
5714
5764
|
const app = appNameOf(input.container);
|
|
5715
|
-
const { url } = await computeClient().ensureService(app, news.name);
|
|
5765
|
+
const { url } = await computeClient().ensureService(app, slugServiceId(news.name));
|
|
5716
5766
|
return {
|
|
5717
5767
|
id: news.name,
|
|
5718
5768
|
name: news.name,
|
|
@@ -5767,12 +5817,13 @@ function LocalDeploymentProvider(input) {
|
|
|
5767
5817
|
try: async () => {
|
|
5768
5818
|
const app = appNameOf(input.container);
|
|
5769
5819
|
const id = news.computeServiceId;
|
|
5820
|
+
const emulatorId = slugServiceId(id);
|
|
5770
5821
|
const artifactDir = path.join(input.devDir, "artifacts", news.artifactHash);
|
|
5771
5822
|
if (!fs$1.existsSync(artifactDir)) extractComputeArtifact(news.artifactPath, artifactDir);
|
|
5772
5823
|
const address = readManifestAddress(artifactDir);
|
|
5773
|
-
const { port } = await computeClient().ensureService(app,
|
|
5824
|
+
const { port } = await computeClient().ensureService(app, emulatorId);
|
|
5774
5825
|
const env = await materializeEnv(input.devDir, address, port);
|
|
5775
|
-
await computeClient().putDeployment(app,
|
|
5826
|
+
await computeClient().putDeployment(app, emulatorId, {
|
|
5776
5827
|
address,
|
|
5777
5828
|
artifactDir,
|
|
5778
5829
|
artifactHash: news.artifactHash,
|
|
@@ -5860,7 +5911,7 @@ function LocalDatabaseProvider(input) {
|
|
|
5860
5911
|
try: async () => {
|
|
5861
5912
|
const app = appNameOf(input.container);
|
|
5862
5913
|
const prismaDevModulePath = resolvePrismaDevModulePath(process.cwd());
|
|
5863
|
-
const { url } = await postgresClient().ensureDatabase(app, news.name, prismaDevModulePath);
|
|
5914
|
+
const { url } = await postgresClient().ensureDatabase(app, slug(news.name), prismaDevModulePath);
|
|
5864
5915
|
return {
|
|
5865
5916
|
id: instanceNameFor(app, news.name),
|
|
5866
5917
|
name: news.name,
|
|
@@ -5953,7 +6004,7 @@ async function* mergedLogs(app, signal) {
|
|
|
5953
6004
|
wake?.();
|
|
5954
6005
|
wake = void 0;
|
|
5955
6006
|
};
|
|
5956
|
-
const follow = (id) => {
|
|
6007
|
+
const follow = (id, address) => {
|
|
5957
6008
|
if (followed.has(id)) return;
|
|
5958
6009
|
followed.add(id);
|
|
5959
6010
|
(async () => {
|
|
@@ -5964,7 +6015,7 @@ async function* mergedLogs(app, signal) {
|
|
|
5964
6015
|
let newlineAt = buffer.indexOf("\n");
|
|
5965
6016
|
while (newlineAt !== -1) {
|
|
5966
6017
|
push({
|
|
5967
|
-
service:
|
|
6018
|
+
service: address,
|
|
5968
6019
|
line: buffer.slice(0, newlineAt)
|
|
5969
6020
|
});
|
|
5970
6021
|
buffer = buffer.slice(newlineAt + 1);
|
|
@@ -5978,7 +6029,7 @@ async function* mergedLogs(app, signal) {
|
|
|
5978
6029
|
};
|
|
5979
6030
|
const relist = async () => {
|
|
5980
6031
|
const services = await client.listServices(app);
|
|
5981
|
-
for (const svc of services) follow(svc.id);
|
|
6032
|
+
for (const svc of services) follow(svc.id, svc.address);
|
|
5982
6033
|
};
|
|
5983
6034
|
await relist();
|
|
5984
6035
|
const timer = setInterval(() => void relist(), RELIST_INTERVAL_MS);
|
|
@@ -6006,6 +6057,7 @@ async function devAttach(input) {
|
|
|
6006
6057
|
const app = prismaCloudContainerOf(input.container).input.appName;
|
|
6007
6058
|
const client = computeClient();
|
|
6008
6059
|
return {
|
|
6060
|
+
startServices: () => client.startApp(app),
|
|
6009
6061
|
endpoints: async () => {
|
|
6010
6062
|
return (await client.listServices(app)).map((svc) => ({
|
|
6011
6063
|
address: svc.address,
|