@prisma/cli 3.0.0-alpha.8 → 3.0.0-beta.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.
@@ -18,9 +18,9 @@ async function runEnvAdd(context, rawAssignment, flags) {
18
18
  requireExplicit: true,
19
19
  command: "add"
20
20
  });
21
- if (!scope) throw usageError(`prisma-cli project env add requires --role`, "Writing without an explicit scope is rejected.", "Pass --role production or --role preview.", [`prisma-cli project env add ${key}=${value} --role production`], "app");
21
+ if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env add ${key}=${value} --role production`], "app");
22
22
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
23
- const resolved = resolveScopeToApi(scope);
23
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: true });
24
24
  if (await findVariableByNaturalKey(client, projectId, key, resolved)) throw new CliError({
25
25
  code: "ENV_VARIABLE_ALREADY_EXISTS",
26
26
  domain: "app",
@@ -28,11 +28,26 @@ async function runEnvAdd(context, rawAssignment, flags) {
28
28
  why: "A variable with this key already exists in the targeted scope.",
29
29
  fix: "Use `prisma-cli project env update` to change an existing variable's value.",
30
30
  exitCode: 1,
31
- nextSteps: [`prisma-cli project env update ${key}=<new-value> --role ${scope.role}`]
31
+ nextSteps: [`prisma-cli project env update ${key}=<new-value> ${formatScopeFlag(scope)}`]
32
32
  });
33
+ const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, key, {
34
+ scope: {
35
+ kind: "role",
36
+ role: "preview"
37
+ },
38
+ descriptor: {
39
+ kind: "role",
40
+ role: "preview"
41
+ },
42
+ apiTarget: {
43
+ class: "preview",
44
+ branchId: null
45
+ }
46
+ }) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
33
47
  const { data, error, response } = await client.POST("/v1/environment-variables", { body: {
34
48
  projectId,
35
49
  class: resolved.apiTarget.class,
50
+ ...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
36
51
  key,
37
52
  value
38
53
  } });
@@ -44,7 +59,7 @@ async function runEnvAdd(context, rawAssignment, flags) {
44
59
  scope: resolved.descriptor,
45
60
  variable: toMetadata(data.data, resolved.descriptor)
46
61
  },
47
- warnings: [],
62
+ warnings,
48
63
  nextSteps: []
49
64
  };
50
65
  }
@@ -54,9 +69,9 @@ async function runEnvUpdate(context, rawAssignment, flags) {
54
69
  requireExplicit: true,
55
70
  command: "update"
56
71
  });
57
- if (!scope) throw usageError(`prisma-cli project env update requires --role`, "Writing without an explicit scope is rejected.", "Pass --role production or --role preview.", [`prisma-cli project env update ${key}=${value} --role production`], "app");
72
+ if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env update ${key}=${value} --role production`], "app");
58
73
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
59
- const resolved = resolveScopeToApi(scope);
74
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
60
75
  const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
61
76
  if (!existing) throw new CliError({
62
77
  code: "ENV_VARIABLE_NOT_FOUND",
@@ -65,7 +80,7 @@ async function runEnvUpdate(context, rawAssignment, flags) {
65
80
  why: "No variable with this key exists in the targeted scope.",
66
81
  fix: "Use `prisma-cli project env add` to create a new variable.",
67
82
  exitCode: 1,
68
- nextSteps: [`prisma-cli project env add ${key}=<value> --role ${scope.role}`]
83
+ nextSteps: [`prisma-cli project env add ${key}=<value> ${formatScopeFlag(scope)}`]
69
84
  });
70
85
  const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
71
86
  params: { path: { envVarId: existing.id } },
@@ -89,7 +104,7 @@ async function runEnvList(context, flags) {
89
104
  command: "list"
90
105
  }) ?? defaultRoleScope();
91
106
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
92
- const resolved = resolveScopeToApi(scope);
107
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
93
108
  const variables = await listVariables(client, projectId, resolved);
94
109
  return {
95
110
  command: "project.env.list",
@@ -99,18 +114,18 @@ async function runEnvList(context, flags) {
99
114
  variables: variables.map((row) => toMetadata(row, resolved.descriptor))
100
115
  },
101
116
  warnings: [],
102
- nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value --role ${scope.role}`] : []
117
+ nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(scope)}`] : []
103
118
  };
104
119
  }
105
- async function runEnvRm(context, key, flags) {
106
- if (!key) throw usageError("prisma-cli project env rm requires KEY", "No KEY positional argument was supplied.", "Pass the variable name to remove, e.g. STRIPE_KEY.", ["prisma-cli project env rm STRIPE_KEY --role production"], "app");
120
+ async function runEnvRemove(context, key, flags) {
121
+ if (!key) throw usageError("prisma-cli project env remove requires KEY", "No KEY positional argument was supplied.", "Pass the variable name to remove, e.g. STRIPE_KEY.", ["prisma-cli project env remove STRIPE_KEY --role production"], "app");
107
122
  const scope = resolveEnvScope(flags, {
108
123
  requireExplicit: true,
109
- command: "rm"
124
+ command: "remove"
110
125
  });
111
- if (!scope) throw usageError("prisma-cli project env rm requires --role", "Writing without an explicit scope is rejected.", "Pass --role production or --role preview.", [`prisma-cli project env rm ${key} --role production`], "app");
126
+ if (!scope) throw usageError("prisma-cli project env remove requires --role or --branch", "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env remove ${key} --role production`], "app");
112
127
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
113
- const resolved = resolveScopeToApi(scope);
128
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
114
129
  const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
115
130
  if (!existing) throw new CliError({
116
131
  code: "ENV_VARIABLE_NOT_FOUND",
@@ -119,12 +134,12 @@ async function runEnvRm(context, key, flags) {
119
134
  why: "No variable with this key exists in the targeted scope, so there is nothing to remove.",
120
135
  fix: "Run prisma-cli project env list with the same scope to see the available variables.",
121
136
  exitCode: 1,
122
- nextSteps: [`prisma-cli project env list --role ${scope.role}`]
137
+ nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`]
123
138
  });
124
139
  const { error, response } = await client.DELETE("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: existing.id } } });
125
140
  if (error) throw apiCallError(`Failed to remove ${key}`, response, error);
126
141
  return {
127
- command: "project.env.rm",
142
+ command: "project.env.remove",
128
143
  result: {
129
144
  projectId,
130
145
  scope: resolved.descriptor,
@@ -151,8 +166,8 @@ async function requireClientAndProject(context, explicitProject) {
151
166
  })).project.id
152
167
  };
153
168
  }
154
- function resolveScopeToApi(scope) {
155
- return {
169
+ async function resolveScopeToApi(client, projectId, scope, options) {
170
+ if (scope.kind === "role") return {
156
171
  scope,
157
172
  descriptor: {
158
173
  kind: "role",
@@ -163,6 +178,96 @@ function resolveScopeToApi(scope) {
163
178
  branchId: null
164
179
  }
165
180
  };
181
+ const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName) : await resolveExistingBranch(client, projectId, scope.branchName);
182
+ if (branch.isDefault) throw new CliError({
183
+ code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
184
+ domain: "app",
185
+ summary: `Branch "${scope.branchName}" is the default branch`,
186
+ why: "Production variables are project-level only; branch overrides apply to preview branches.",
187
+ fix: "Use --role production for the default branch.",
188
+ exitCode: 1,
189
+ nextSteps: ["prisma-cli project env list --role production"]
190
+ });
191
+ return {
192
+ scope,
193
+ descriptor: {
194
+ kind: "branch",
195
+ branchName: branch.gitName,
196
+ branchId: branch.id
197
+ },
198
+ apiTarget: {
199
+ class: "preview",
200
+ branchId: branch.id
201
+ }
202
+ };
203
+ }
204
+ function formatScopeFlag(scope) {
205
+ if (scope.kind === "role") return `--role ${scope.role}`;
206
+ return `--branch ${scope.branchName}`;
207
+ }
208
+ async function listBranchesByName(client, projectId, branchName) {
209
+ const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", { params: {
210
+ path: { projectId },
211
+ query: { gitName: branchName }
212
+ } });
213
+ if (error || !data) throw apiCallError(`Failed to resolve branch "${branchName}"`, response, error);
214
+ return data.data;
215
+ }
216
+ async function resolveExistingBranch(client, projectId, branchName) {
217
+ const branch = (await listBranchesByName(client, projectId, branchName))[0];
218
+ if (!branch) throw new CliError({
219
+ code: "ENV_BRANCH_NOT_FOUND",
220
+ domain: "app",
221
+ summary: `Branch "${branchName}" not found`,
222
+ why: "Branch update, list, and remove commands only target existing preview branches.",
223
+ fix: "Create the branch by deploying it, or use `project env add --branch` to create its first override.",
224
+ exitCode: 1,
225
+ nextSteps: [`prisma-cli project env add KEY=value --branch ${branchName}`]
226
+ });
227
+ return branch;
228
+ }
229
+ async function resolveOrCreateBranch(client, projectId, branchName) {
230
+ const existing = (await listBranchesByName(client, projectId, branchName))[0];
231
+ if (existing) return existing;
232
+ if (!await projectHasDefaultBranch(client, projectId)) throw new CliError({
233
+ code: "ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH",
234
+ domain: "app",
235
+ summary: `Cannot create branch "${branchName}" from project env`,
236
+ why: "Creating the first branch would make it the project default, but branch overrides are preview-only.",
237
+ fix: "Create or deploy the default branch first, then add the branch override.",
238
+ exitCode: 1,
239
+ nextSteps: ["prisma-cli app deploy --branch main"]
240
+ });
241
+ const { data, error, response } = await client.POST("/v1/projects/{projectId}/branches", {
242
+ params: { path: { projectId } },
243
+ body: {
244
+ gitName: branchName,
245
+ isDefault: false
246
+ }
247
+ });
248
+ if (error || !data) {
249
+ if (response?.status === 409) {
250
+ const raced = (await listBranchesByName(client, projectId, branchName))[0];
251
+ if (raced) return raced;
252
+ }
253
+ throw apiCallError(`Failed to create branch "${branchName}"`, response, error);
254
+ }
255
+ return data.data;
256
+ }
257
+ async function projectHasDefaultBranch(client, projectId) {
258
+ let cursor;
259
+ while (true) {
260
+ const query = {};
261
+ if (cursor !== void 0) query.cursor = cursor;
262
+ const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
263
+ path: { projectId },
264
+ query
265
+ } });
266
+ if (result.error || !result.data) throw apiCallError("Failed to check project default branch", result.response, result.error);
267
+ if (result.data.data.some((branch) => branch.isDefault)) return true;
268
+ if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) return false;
269
+ cursor = result.data.pagination.nextCursor;
270
+ }
166
271
  }
167
272
  async function findVariableByNaturalKey(client, projectId, key, resolved) {
168
273
  const { data, error, response } = await client.GET("/v1/environment-variables", { params: { query: {
@@ -171,7 +276,7 @@ async function findVariableByNaturalKey(client, projectId, key, resolved) {
171
276
  key
172
277
  } } });
173
278
  if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
174
- return data.data.filter((row) => rowMatchesScope(row, resolved))[0] ?? null;
279
+ return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
175
280
  }
176
281
  async function listVariables(client, projectId, resolved) {
177
282
  const collected = [];
@@ -189,20 +294,41 @@ async function listVariables(client, projectId, resolved) {
189
294
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
190
295
  cursor = result.data.pagination.nextCursor;
191
296
  }
192
- return collected;
297
+ return materializeEffectiveRows(collected, resolved);
193
298
  }
194
299
  function rowMatchesScope(row, resolved) {
195
- return row.branchId === null && row.class === resolved.apiTarget.class;
300
+ if (row.class !== resolved.apiTarget.class) return false;
301
+ if (resolved.apiTarget.branchId === null) return row.branchId === null;
302
+ return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
196
303
  }
197
- function toMetadata(row, scope) {
304
+ function rowMatchesExactScope(row, resolved) {
305
+ return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
306
+ }
307
+ function materializeEffectiveRows(rows, resolved) {
308
+ if (resolved.apiTarget.branchId === null) return rows;
309
+ const byKey = /* @__PURE__ */ new Map();
310
+ for (const row of rows) if (row.branchId === null && !byKey.has(row.key)) byKey.set(row.key, row);
311
+ for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
312
+ return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
313
+ }
314
+ function toMetadata(row, requestedScope) {
315
+ const rowScope = row.branchId === null ? {
316
+ kind: "role",
317
+ role: row.class
318
+ } : requestedScope;
198
319
  return {
199
320
  id: row.id,
200
321
  key: row.key,
201
- scope,
322
+ scope: rowScope,
323
+ source: formatDescriptorLabel(rowScope),
202
324
  isManagedBySystem: row.isManagedBySystem,
203
325
  updatedAt: row.updatedAt
204
326
  };
205
327
  }
328
+ function formatDescriptorLabel(scope) {
329
+ if (scope.kind === "role") return scope.role ?? "unknown";
330
+ return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
331
+ }
206
332
  function apiCallError(summary, response, error) {
207
333
  const status = response?.status ?? 0;
208
334
  const apiCode = error?.error?.code;
@@ -220,4 +346,4 @@ function apiCallError(summary, response, error) {
220
346
  });
221
347
  }
222
348
  //#endregion
223
- export { runEnvAdd, runEnvList, runEnvRm, runEnvUpdate };
349
+ export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };