@prisma/compute-sdk 0.20.0 → 0.22.0

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.
@@ -48,6 +48,7 @@ import type {
48
48
  VersionInfo,
49
49
  } from "./types.ts";
50
50
  import { REGIONS } from "./types.ts";
51
+ import { uploadArtifact } from "./upload-artifact.ts";
51
52
 
52
53
  type VersionDetailData = InferOk<
53
54
  Awaited<ReturnType<InternalApiClient["getVersion"]>>
@@ -284,13 +285,19 @@ export class ComputeClient {
284
285
  ? artifact.defaultPortMapping
285
286
  : undefined);
286
287
 
288
+ yield* Result.await(
289
+ self.#applyEnvVars(
290
+ target.projectId,
291
+ target.branchId,
292
+ options.envVars,
293
+ options.signal,
294
+ ),
295
+ );
296
+
287
297
  const createResponse = yield* Result.await(
288
298
  self.#api.createServiceVersion(
289
299
  target.serviceId,
290
- {
291
- envVars: options.envVars,
292
- portMapping,
293
- },
300
+ portMapping ? { portMapping } : {},
294
301
  options.signal,
295
302
  ),
296
303
  );
@@ -309,7 +316,7 @@ export class ComputeClient {
309
316
 
310
317
  options.progress?.onUploadStart?.();
311
318
  yield* Result.await(
312
- self.#uploadArtifact(uploadUrl, archiveBytes, options.signal),
319
+ self.#uploadArtifactResult(uploadUrl, archiveBytes, options.signal),
313
320
  );
314
321
  options.progress?.onUploadComplete?.();
315
322
 
@@ -412,14 +419,25 @@ export class ComputeClient {
412
419
  );
413
420
  }
414
421
 
422
+ yield* self.#checkAborted(options.signal);
423
+ yield* Result.await(
424
+ self.#applyEnvVars(
425
+ target.projectId,
426
+ target.branchId,
427
+ options.envVars,
428
+ options.signal,
429
+ ),
430
+ );
431
+
415
432
  yield* self.#checkAborted(options.signal);
416
433
 
417
434
  const createResponse = yield* Result.await(
418
435
  self.#api.createServiceVersion(
419
436
  target.serviceId,
420
437
  {
421
- envVars: options.envVars,
422
- portMapping: options.portMapping,
438
+ ...(options.portMapping !== undefined
439
+ ? { portMapping: options.portMapping }
440
+ : {}),
423
441
  skipCodeUpload: true,
424
442
  },
425
443
  options.signal,
@@ -1179,9 +1197,14 @@ export class ComputeClient {
1179
1197
  serviceId: string;
1180
1198
  serviceName: string;
1181
1199
  region: string;
1200
+ branchId: string | null;
1182
1201
  isNewService: boolean;
1183
1202
  },
1184
- MissingArgumentError | CancelledError | AuthenticationError | ApiError
1203
+ | MissingArgumentError
1204
+ | CancelledError
1205
+ | AuthenticationError
1206
+ | ApiError
1207
+ | InvalidOptionsError
1185
1208
  >
1186
1209
  > {
1187
1210
  const self = this;
@@ -1190,15 +1213,27 @@ export class ComputeClient {
1190
1213
  const service = yield* Result.await(
1191
1214
  self.#api.getService(options.serviceId, options.signal),
1192
1215
  );
1193
- const projectId = options.projectId ?? service.projectId;
1194
- if (!projectId) {
1216
+ if (!service.projectId) {
1195
1217
  return Result.err(new MissingArgumentError({ field: "projectId" }));
1196
1218
  }
1219
+ const requestedProjectId = options.projectId?.replace(/^proj_/, "");
1220
+ const serviceProjectId = service.projectId.replace(/^proj_/, "");
1221
+ if (
1222
+ requestedProjectId !== undefined &&
1223
+ requestedProjectId !== serviceProjectId
1224
+ ) {
1225
+ return Result.err(
1226
+ new InvalidOptionsError({
1227
+ message: `Service ${service.id} belongs to project ${service.projectId}, but project ${options.projectId} was provided`,
1228
+ }),
1229
+ );
1230
+ }
1197
1231
  return Result.ok({
1198
- projectId,
1232
+ projectId: service.projectId,
1199
1233
  serviceId: service.id,
1200
1234
  serviceName: service.name ?? service.id,
1201
1235
  region: service.region.id,
1236
+ branchId: service.branchId ?? null,
1202
1237
  isNewService: false,
1203
1238
  });
1204
1239
  }
@@ -1239,6 +1274,7 @@ export class ComputeClient {
1239
1274
  serviceId: response.id,
1240
1275
  serviceName: response.name,
1241
1276
  region: response.region.id,
1277
+ branchId: response.branchId ?? null,
1242
1278
  isNewService: true,
1243
1279
  });
1244
1280
  }
@@ -1263,13 +1299,20 @@ export class ComputeClient {
1263
1299
  await options.interaction.selectService(serviceInfos);
1264
1300
 
1265
1301
  if (selectedServiceId !== null) {
1266
- const service = services.find((s) => s.id === selectedServiceId);
1267
- invariant(service, "selectService returned service ID not in the list");
1302
+ const selected = services.find((s) => s.id === selectedServiceId);
1303
+ invariant(
1304
+ selected,
1305
+ "selectService returned service ID not in the list",
1306
+ );
1307
+ const service = yield* Result.await(
1308
+ self.#api.getService(selectedServiceId, options.signal),
1309
+ );
1268
1310
  return Result.ok({
1269
1311
  projectId,
1270
1312
  serviceId: selectedServiceId,
1271
- serviceName: service.name,
1313
+ serviceName: service.name ?? selected.name,
1272
1314
  region: service.region.id,
1315
+ branchId: service.branchId ?? null,
1273
1316
  isNewService: false,
1274
1317
  });
1275
1318
  }
@@ -1306,39 +1349,102 @@ export class ComputeClient {
1306
1349
  serviceId: response.id,
1307
1350
  serviceName: response.name,
1308
1351
  region: response.region.id,
1352
+ branchId: response.branchId ?? null,
1309
1353
  isNewService: true,
1310
1354
  });
1311
1355
  });
1312
1356
  }
1313
1357
 
1314
- async #uploadArtifact(
1358
+ async #applyEnvVars(
1359
+ projectId: string,
1360
+ branchId: string | null,
1361
+ envVars: Record<string, string | null> | undefined,
1362
+ signal?: AbortSignal,
1363
+ ): Promise<Result<void, ApiRequestError>> {
1364
+ if (!envVars || Object.keys(envVars).length === 0) {
1365
+ return Result.ok(undefined);
1366
+ }
1367
+
1368
+ const self = this;
1369
+ return Result.gen(async function* () {
1370
+ const scope = yield* Result.await(
1371
+ self.#resolveEnvironmentVariableScope(projectId, branchId, signal),
1372
+ );
1373
+ for (const [key, value] of Object.entries(envVars)) {
1374
+ yield* self.#checkAborted(signal);
1375
+ const existing = yield* Result.await(
1376
+ self.#api.listEnvironmentVariables({ ...scope, key }, signal),
1377
+ );
1378
+ const current = existing[0];
1379
+
1380
+ if (value === null) {
1381
+ if (current) {
1382
+ yield* self.#checkAborted(signal);
1383
+ // Do not pass signal to mutating transport; aborting it can make remote completion ambiguous.
1384
+ yield* Result.await(
1385
+ self.#api.deleteEnvironmentVariable(current.id),
1386
+ );
1387
+ }
1388
+ continue;
1389
+ }
1390
+
1391
+ if (current) {
1392
+ yield* self.#checkAborted(signal);
1393
+ // Do not pass signal to mutating transport; aborting it can make remote completion ambiguous.
1394
+ yield* Result.await(
1395
+ self.#api.updateEnvironmentVariable(current.id, { value }),
1396
+ );
1397
+ continue;
1398
+ }
1399
+
1400
+ yield* self.#checkAborted(signal);
1401
+ // Do not pass signal to mutating transport; aborting it can make remote completion ambiguous.
1402
+ yield* Result.await(
1403
+ self.#api.createEnvironmentVariable({ ...scope, key, value }),
1404
+ );
1405
+ }
1406
+
1407
+ return Result.ok(undefined);
1408
+ });
1409
+ }
1410
+
1411
+ async #resolveEnvironmentVariableScope(
1412
+ projectId: string,
1413
+ branchId: string | null,
1414
+ signal?: AbortSignal,
1415
+ ): Promise<
1416
+ Result<
1417
+ | { projectId: string; class: "production" }
1418
+ | {
1419
+ projectId: string;
1420
+ class: "preview";
1421
+ branchId: string;
1422
+ },
1423
+ ApiRequestError
1424
+ >
1425
+ > {
1426
+ if (!branchId) {
1427
+ return Result.ok({ projectId, class: "production" });
1428
+ }
1429
+
1430
+ const branch = await this.#api.getBranch(branchId, signal);
1431
+ if (branch.isErr()) return branch;
1432
+
1433
+ if (branch.value.role === "preview") {
1434
+ return Result.ok({ projectId, class: "preview", branchId });
1435
+ }
1436
+
1437
+ return Result.ok({ projectId, class: "production" });
1438
+ }
1439
+
1440
+ async #uploadArtifactResult(
1315
1441
  url: string,
1316
1442
  body: Uint8Array,
1317
1443
  signal?: AbortSignal,
1318
1444
  ): Promise<Result<void, ArtifactError | CancelledError>> {
1319
1445
  return Result.tryPromise({
1320
1446
  try: async () => {
1321
- signal?.throwIfAborted();
1322
- // Intentionally do not pass AbortSignal to this mutating upload request.
1323
- // Aborting transport can leave completion ambiguous server-side.
1324
- const res = await fetch(url, {
1325
- method: "PUT",
1326
- headers: { "content-type": "application/gzip" },
1327
- body,
1328
- });
1329
-
1330
- if (!res.ok) {
1331
- let message = `Artifact upload failed with HTTP ${res.status}`;
1332
- try {
1333
- const json = (await res.json()) as { error?: unknown };
1334
- if (typeof json.error === "string" && json.error.length > 0) {
1335
- message = `Artifact upload failed: ${json.error}`;
1336
- }
1337
- } catch {
1338
- // ignore body parse failures
1339
- }
1340
- throw new Error(message);
1341
- }
1447
+ await uploadArtifact(url, body, signal);
1342
1448
  },
1343
1449
  catch: (e) => {
1344
1450
  if (signal?.aborted) return new CancelledError();
package/src/errors.ts CHANGED
@@ -145,6 +145,7 @@ export type UpdateEnvError =
145
145
  | AuthenticationError
146
146
  | ApiError
147
147
  | MissingArgumentError
148
+ | InvalidOptionsError
148
149
  | NoExistingVersionError
149
150
  | TimeoutError
150
151
  | VersionFailedError
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export {
7
7
  type Result,
8
8
  TaggedError,
9
9
  } from "better-result";
10
+ export { createArchive } from "./archive.ts";
11
+ export { normalizeArtifactSymlinks } from "./artifact-postprocess.ts";
10
12
  export { AstroBuild } from "./astro-build.ts";
11
13
  export { AutoBuild } from "./auto-build.ts";
12
14
  export type { BuildArtifact, BuildStrategy } from "./build-strategy.ts";
@@ -94,3 +96,4 @@ export type {
94
96
  VersionInfo,
95
97
  } from "./types.ts";
96
98
  export { KNOWN_REGION_IDS, REGIONS } from "./types.ts";
99
+ export { uploadArtifact } from "./upload-artifact.ts";
@@ -1,6 +1,7 @@
1
1
  import { cp, mkdtemp, readdir, rm, stat } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { normalizeArtifactSymlinks } from "./artifact-postprocess.ts";
4
5
  import type { BuildArtifact, BuildStrategy } from "./build-strategy.ts";
5
6
  import {
6
7
  hasPackageDependency,
@@ -95,6 +96,8 @@ export class NextjsBuild implements BuildStrategy {
95
96
  ? "server.js"
96
97
  : path.posix.join(serverSubpath, "server.js");
97
98
 
99
+ await normalizeArtifactSymlinks(artifactDir, this.#appPath, signal);
100
+
98
101
  return {
99
102
  directory: artifactDir,
100
103
  entrypoint,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * PUT a gzipped compute artifact to a Foundry-minted presigned upload URL.
3
+ */
4
+ export async function uploadArtifact(
5
+ uploadUrl: string,
6
+ body: Uint8Array,
7
+ signal?: AbortSignal,
8
+ ): Promise<void> {
9
+ signal?.throwIfAborted();
10
+ // Intentionally do not pass AbortSignal to this mutating upload request.
11
+ // Aborting transport can leave completion ambiguous server-side.
12
+ const response = await fetch(uploadUrl, {
13
+ method: "PUT",
14
+ headers: { "content-type": "application/gzip" },
15
+ body,
16
+ });
17
+
18
+ if (!response.ok) {
19
+ let message = `Artifact upload failed with HTTP ${response.status}`;
20
+ try {
21
+ const json = (await response.json()) as { error?: unknown };
22
+ if (typeof json.error === "string" && json.error.length > 0) {
23
+ message = `Artifact upload failed: ${json.error}`;
24
+ }
25
+ } catch {
26
+ // ignore body parse failures
27
+ }
28
+ throw new Error(message);
29
+ }
30
+ }