@prisma/compute-sdk 0.4.0 → 0.6.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.
@@ -14,6 +14,7 @@ import type {
14
14
  DestroyServiceProgress,
15
15
  DestroyVersionInteraction,
16
16
  DestroyVersionProgress,
17
+ PromoteProgress,
17
18
  UpdateEnvProgress,
18
19
  } from "./callbacks.ts";
19
20
  import {
@@ -27,9 +28,11 @@ import {
27
28
  DestroyAggregateError,
28
29
  type DestroyServiceError,
29
30
  type DestroyVersionError,
31
+ InvalidOptionsError,
30
32
  MissingArgumentError,
31
33
  NoExistingVersionError,
32
34
  NoVersionsFoundError,
35
+ type PromoteError,
33
36
  type UpdateEnvError,
34
37
  type VersionOperationError,
35
38
  } from "./errors.ts";
@@ -56,19 +59,21 @@ export interface DeployOptions {
56
59
  serviceId?: string;
57
60
  serviceName?: string;
58
61
  region?: string;
59
- envVars?: Record<string, string>;
62
+ envVars?: Record<string, string | null>;
60
63
  portMapping?: PortMapping;
61
64
  timeoutSeconds?: number;
62
65
  pollIntervalMs?: number;
63
66
  signal?: AbortSignal;
64
67
  interaction?: DeployInteraction;
65
68
  progress?: DeployProgress;
69
+ skipPromote?: boolean;
70
+ destroyOldVersion?: boolean;
66
71
  }
67
72
 
68
73
  export interface UpdateEnvOptions {
69
74
  projectId?: string;
70
75
  serviceId?: string;
71
- envVars?: Record<string, string>;
76
+ envVars?: Record<string, string | null>;
72
77
  portMapping?: PortMapping;
73
78
  timeoutSeconds?: number;
74
79
  pollIntervalMs?: number;
@@ -155,16 +160,37 @@ export interface DeleteVersionOptions {
155
160
  signal?: AbortSignal;
156
161
  }
157
162
 
163
+ export interface PromoteOptions {
164
+ serviceId: string;
165
+ versionId?: string;
166
+ timeoutSeconds?: number;
167
+ pollIntervalMs?: number;
168
+ signal?: AbortSignal;
169
+ interaction?: DestroyVersionInteraction;
170
+ progress?: PromoteProgress;
171
+ }
172
+
158
173
  export interface DeployResult {
159
174
  projectId: string;
160
175
  serviceId: string;
161
176
  serviceName: string;
162
177
  region: string;
163
178
  versionId: string;
164
- deploymentUrl: string;
179
+ versionEndpointDomain: string;
180
+ serviceEndpointDomain: string | null;
181
+ promoted: boolean;
182
+ previousVersionId: string | null;
183
+ previousVersionAction: "stopped" | "destroyed" | "still-active" | null;
165
184
  resolvedConfig: ResolvedConfig;
166
185
  }
167
186
 
187
+ export interface PromoteResult {
188
+ serviceId: string;
189
+ versionId: string;
190
+ serviceEndpointDomain: string;
191
+ versionStarted: boolean;
192
+ }
193
+
168
194
  export interface UpdateEnvResult {
169
195
  projectId: string;
170
196
  serviceId: string;
@@ -199,6 +225,15 @@ export class ComputeClient {
199
225
  async deploy(
200
226
  options: DeployOptions,
201
227
  ): Promise<Result<DeployResult, DeployError>> {
228
+ if (options.skipPromote && options.destroyOldVersion) {
229
+ return Result.err(
230
+ new InvalidOptionsError({
231
+ message:
232
+ "destroyOldVersion cannot be combined with skipPromote — the old version stays active when promotion is skipped",
233
+ }),
234
+ );
235
+ }
236
+
202
237
  const self = this;
203
238
  return Result.gen(async function* () {
204
239
  yield* self.#checkAborted(options.signal);
@@ -284,23 +319,63 @@ export class ComputeClient {
284
319
  }),
285
320
  );
286
321
 
287
- const deploymentUrl = toDeploymentUrl(pollResult.previewDomain);
288
- options.progress?.onRunning?.(deploymentUrl);
322
+ const versionEndpointDomain = toDeploymentUrl(pollResult.previewDomain);
323
+ options.progress?.onRunning?.(versionEndpointDomain);
289
324
 
290
- return Result.ok({
325
+ const service = yield* Result.await(
326
+ self.#api.getService(target.serviceId, options.signal),
327
+ );
328
+ const previousVersionId = service.latestVersionId ?? null;
329
+
330
+ const resolvedConfig: ResolvedConfig = {
291
331
  projectId: target.projectId,
292
332
  serviceId: target.serviceId,
293
333
  serviceName: target.serviceName,
294
334
  region: target.region,
295
- versionId: createResponse.id,
296
- deploymentUrl,
297
- resolvedConfig: {
335
+ portMapping: options.portMapping,
336
+ };
337
+
338
+ if (options.skipPromote) {
339
+ return Result.ok({
298
340
  projectId: target.projectId,
299
341
  serviceId: target.serviceId,
300
342
  serviceName: target.serviceName,
301
343
  region: target.region,
302
- portMapping: options.portMapping,
303
- },
344
+ versionId: createResponse.id,
345
+ versionEndpointDomain,
346
+ serviceEndpointDomain: null,
347
+ promoted: false,
348
+ previousVersionId,
349
+ previousVersionAction: previousVersionId
350
+ ? ("still-active" as const)
351
+ : null,
352
+ resolvedConfig,
353
+ });
354
+ }
355
+
356
+ const switchoverResult = yield* Result.await(
357
+ self.#promoteAndSwitchover(
358
+ target.serviceId,
359
+ createResponse.id,
360
+ previousVersionId,
361
+ options.destroyOldVersion ?? false,
362
+ options.signal,
363
+ options.progress,
364
+ ),
365
+ );
366
+
367
+ return Result.ok({
368
+ projectId: target.projectId,
369
+ serviceId: target.serviceId,
370
+ serviceName: target.serviceName,
371
+ region: target.region,
372
+ versionId: createResponse.id,
373
+ versionEndpointDomain,
374
+ serviceEndpointDomain: switchoverResult.serviceEndpointDomain,
375
+ promoted: true,
376
+ previousVersionId: switchoverResult.previousVersionId,
377
+ previousVersionAction: switchoverResult.previousVersionAction,
378
+ resolvedConfig,
304
379
  });
305
380
  } finally {
306
381
  await artifact.cleanup?.();
@@ -737,6 +812,7 @@ export class ComputeClient {
737
812
  region: s.region.id,
738
813
  projectId: s.projectId,
739
814
  latestVersionId: s.latestVersionId,
815
+ serviceEndpointDomain: s.serviceEndpointDomain,
740
816
  });
741
817
  });
742
818
  }
@@ -848,6 +924,227 @@ export class ComputeClient {
848
924
  });
849
925
  }
850
926
 
927
+ async promote(
928
+ options: PromoteOptions,
929
+ ): Promise<Result<PromoteResult, PromoteError>> {
930
+ const self = this;
931
+ return Result.gen(async function* () {
932
+ yield* self.#checkAborted(options.signal);
933
+
934
+ let versionId = options.versionId;
935
+
936
+ if (!versionId) {
937
+ const versions = yield* Result.await(
938
+ self.#api.listServiceVersions(options.serviceId, options.signal),
939
+ );
940
+ const detailResults = await Promise.all(
941
+ versions.map((v) => self.#api.getVersion(v.id, options.signal)),
942
+ );
943
+ const details: VersionDetailData[] = [];
944
+ for (const result of detailResults) {
945
+ if (result.isErr()) {
946
+ if (ApiError.is(result.error) && result.error.statusCode === 404)
947
+ continue;
948
+ return result;
949
+ }
950
+ details.push(result.value);
951
+ }
952
+
953
+ const versionInfos = details
954
+ .map(
955
+ (v): VersionInfo => ({
956
+ id: v.id,
957
+ status: v.status,
958
+ createdAt: v.createdAt,
959
+ previewDomain: v.previewDomain,
960
+ }),
961
+ )
962
+ .sort(versionSortComparator);
963
+
964
+ if (versionInfos.length === 0) {
965
+ return Result.err(
966
+ new NoVersionsFoundError({ serviceId: options.serviceId }),
967
+ );
968
+ }
969
+
970
+ if (!options.interaction?.selectVersion) {
971
+ return Result.err(new MissingArgumentError({ field: "versionId" }));
972
+ }
973
+
974
+ versionId = await options.interaction.selectVersion(versionInfos);
975
+ }
976
+
977
+ const version = yield* Result.await(
978
+ self.#api.getVersion(versionId, options.signal),
979
+ );
980
+ let versionStarted = false;
981
+
982
+ if (version.status !== "running") {
983
+ options.progress?.onVersionStarting?.(versionId);
984
+
985
+ if (version.status !== "provisioning") {
986
+ options.progress?.onVersionStartRequested?.();
987
+ const startResult = await self.#api.startVersion(
988
+ versionId,
989
+ options.signal,
990
+ );
991
+ if (startResult.isErr()) {
992
+ options.progress?.onPromoteFailed?.(startResult.error);
993
+ return startResult;
994
+ }
995
+ }
996
+
997
+ const pollResult = await pollVersionStatus(self.#api, versionId, {
998
+ targetStatus: "running",
999
+ timeoutSeconds: options.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS,
1000
+ pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
1001
+ signal: options.signal,
1002
+ onStatusChange: options.progress?.onStatusChange,
1003
+ });
1004
+ if (pollResult.isErr()) {
1005
+ options.progress?.onPromoteFailed?.(pollResult.error);
1006
+ return pollResult;
1007
+ }
1008
+ versionStarted = true;
1009
+ options.progress?.onVersionRunning?.();
1010
+ }
1011
+
1012
+ options.progress?.onPromoteStart?.();
1013
+ const promoteResult = await self.#api.promoteService(
1014
+ options.serviceId,
1015
+ { versionId },
1016
+ options.signal,
1017
+ );
1018
+ if (promoteResult.isErr()) {
1019
+ options.progress?.onPromoteFailed?.(promoteResult.error);
1020
+ return promoteResult;
1021
+ }
1022
+ options.progress?.onPromoted?.(promoteResult.value.serviceEndpointDomain);
1023
+
1024
+ return Result.ok({
1025
+ serviceId: options.serviceId,
1026
+ versionId,
1027
+ serviceEndpointDomain: promoteResult.value.serviceEndpointDomain,
1028
+ versionStarted,
1029
+ });
1030
+ });
1031
+ }
1032
+
1033
+ async #promoteAndSwitchover(
1034
+ serviceId: string,
1035
+ newVersionId: string,
1036
+ previousVersionId: string | null,
1037
+ destroyOldVersion: boolean,
1038
+ signal?: AbortSignal,
1039
+ progress?: DeployProgress,
1040
+ ): Promise<
1041
+ Result<
1042
+ {
1043
+ serviceEndpointDomain: string;
1044
+ previousVersionId: string | null;
1045
+ previousVersionAction: "stopped" | "destroyed" | null;
1046
+ },
1047
+ DeployError
1048
+ >
1049
+ > {
1050
+ progress?.onPromoteStart?.();
1051
+ const promoteResult = await this.#api.promoteService(
1052
+ serviceId,
1053
+ { versionId: newVersionId },
1054
+ signal,
1055
+ );
1056
+
1057
+ if (promoteResult.isErr()) {
1058
+ progress?.onPromoteFailed?.(promoteResult.error);
1059
+ progress?.onCleanupDanglingVersion?.(newVersionId);
1060
+ await this.#cleanupDanglingVersion(newVersionId, undefined, progress);
1061
+ return promoteResult;
1062
+ }
1063
+
1064
+ const serviceEndpointDomain = promoteResult.value.serviceEndpointDomain;
1065
+ progress?.onPromoted?.(serviceEndpointDomain);
1066
+
1067
+ let previousVersionAction: "stopped" | "destroyed" | null = null;
1068
+
1069
+ if (previousVersionId && previousVersionId !== newVersionId) {
1070
+ progress?.onOldVersionStopping?.(previousVersionId);
1071
+ const stopResult = await this.#api.stopVersion(previousVersionId, signal);
1072
+
1073
+ if (stopResult.isOk()) {
1074
+ const pollResult = await pollVersionStatus(
1075
+ this.#api,
1076
+ previousVersionId,
1077
+ {
1078
+ targetStatus: "stopped",
1079
+ timeoutSeconds: 30,
1080
+ pollIntervalMs: 1000,
1081
+ signal,
1082
+ },
1083
+ );
1084
+
1085
+ if (pollResult.isOk()) {
1086
+ previousVersionAction = "stopped";
1087
+ progress?.onOldVersionStopped?.(previousVersionId);
1088
+
1089
+ if (destroyOldVersion) {
1090
+ progress?.onOldVersionDeleting?.(previousVersionId);
1091
+ const deleteResult = await this.#api.deleteVersion(
1092
+ previousVersionId,
1093
+ signal,
1094
+ );
1095
+ if (deleteResult.isOk()) {
1096
+ previousVersionAction = "destroyed";
1097
+ progress?.onOldVersionDeleted?.(previousVersionId);
1098
+ } else {
1099
+ progress?.onOldVersionDeleteFailed?.(previousVersionId);
1100
+ }
1101
+ }
1102
+ } else {
1103
+ progress?.onOldVersionStopFailed?.(previousVersionId);
1104
+ }
1105
+ } else {
1106
+ progress?.onOldVersionStopFailed?.(previousVersionId);
1107
+ }
1108
+ }
1109
+
1110
+ return Result.ok({
1111
+ serviceEndpointDomain,
1112
+ previousVersionId,
1113
+ previousVersionAction,
1114
+ });
1115
+ }
1116
+
1117
+ async #cleanupDanglingVersion(
1118
+ versionId: string,
1119
+ signal?: AbortSignal,
1120
+ progress?: DeployProgress,
1121
+ ): Promise<void> {
1122
+ const stopResult = await this.#api.stopVersion(versionId, signal);
1123
+ if (stopResult.isErr()) {
1124
+ progress?.onCleanupDanglingVersionFailed?.(versionId);
1125
+ return;
1126
+ }
1127
+
1128
+ const pollResult = await pollVersionStatus(this.#api, versionId, {
1129
+ targetStatus: "stopped",
1130
+ timeoutSeconds: 30,
1131
+ pollIntervalMs: 1000,
1132
+ signal,
1133
+ });
1134
+ if (pollResult.isErr()) {
1135
+ progress?.onCleanupDanglingVersionFailed?.(versionId);
1136
+ return;
1137
+ }
1138
+
1139
+ const deleteResult = await this.#api.deleteVersion(versionId, signal);
1140
+ if (deleteResult.isErr()) {
1141
+ progress?.onCleanupDanglingVersionFailed?.(versionId);
1142
+ return;
1143
+ }
1144
+
1145
+ progress?.onCleanupDanglingVersionComplete?.(versionId);
1146
+ }
1147
+
851
1148
  #checkAborted(signal?: AbortSignal): Result<void, CancelledError> {
852
1149
  if (signal?.aborted) {
853
1150
  return Result.err(new CancelledError());
package/src/errors.ts CHANGED
@@ -87,7 +87,7 @@ export class NoVersionsFoundError extends TaggedError("NoVersionsFoundError")<{
87
87
  constructor(args: { serviceId: string }) {
88
88
  super({
89
89
  serviceId: args.serviceId,
90
- message: `Service ${args.serviceId} has no versions to destroy`,
90
+ message: `Service ${args.serviceId} has no versions`,
91
91
  });
92
92
  }
93
93
  }
@@ -121,10 +121,15 @@ export class DestroyAggregateError extends TaggedError(
121
121
  }
122
122
  }
123
123
 
124
+ export class InvalidOptionsError extends TaggedError("InvalidOptionsError")<{
125
+ message: string;
126
+ }>() {}
127
+
124
128
  export type DeployError =
125
129
  | AuthenticationError
126
130
  | ApiError
127
131
  | MissingArgumentError
132
+ | InvalidOptionsError
128
133
  | BuildError
129
134
  | ArtifactError
130
135
  | TimeoutError
@@ -155,6 +160,15 @@ export type DestroyServiceError =
155
160
  | ApiError
156
161
  | DestroyAggregateError;
157
162
 
163
+ export type PromoteError =
164
+ | AuthenticationError
165
+ | ApiError
166
+ | MissingArgumentError
167
+ | NoVersionsFoundError
168
+ | TimeoutError
169
+ | VersionFailedError
170
+ | CancelledError;
171
+
158
172
  export type ApiRequestError = CancelledError | AuthenticationError | ApiError;
159
173
 
160
174
  export type VersionOperationError =
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export type {
18
18
  DestroyServiceProgress,
19
19
  DestroyVersionInteraction,
20
20
  DestroyVersionProgress,
21
+ PromoteProgress,
21
22
  UpdateEnvProgress,
22
23
  } from "./callbacks.ts";
23
24
  export type {
@@ -34,6 +35,8 @@ export type {
34
35
  ListProjectsOptions,
35
36
  ListServicesOptions,
36
37
  ListVersionsOptions,
38
+ PromoteOptions,
39
+ PromoteResult,
37
40
  ShowServiceOptions,
38
41
  ShowVersionOptions,
39
42
  StartVersionOptions,
@@ -48,6 +51,7 @@ export type {
48
51
  DeployError,
49
52
  DestroyServiceError,
50
53
  DestroyVersionError,
54
+ PromoteError,
51
55
  UpdateEnvError,
52
56
  VersionOperationError,
53
57
  } from "./errors.ts";
@@ -59,6 +63,7 @@ export {
59
63
  BuildError,
60
64
  CancelledError,
61
65
  DestroyAggregateError,
66
+ InvalidOptionsError,
62
67
  MissingArgumentError,
63
68
  NoExistingVersionError,
64
69
  NoVersionsFoundError,
package/src/types.ts CHANGED
@@ -33,6 +33,7 @@ export interface ServiceInfo {
33
33
 
34
34
  export interface ServiceDetail extends ServiceInfo {
35
35
  latestVersionId?: string | null;
36
+ serviceEndpointDomain?: string;
36
37
  }
37
38
 
38
39
  export interface VersionInfo {