@prisma/cli 3.0.0-dev.135.1 → 3.0.0-dev.136.1

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.
@@ -356,7 +356,10 @@ async function requireDatabaseContext(context, flags, commandName) {
356
356
  });
357
357
  if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
358
358
  return {
359
- provider: createManagementDatabaseProvider(client, { formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd) }),
359
+ provider: createManagementDatabaseProvider(client, {
360
+ formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd),
361
+ workspaceId: workspace.id
362
+ }),
360
363
  target: targetResult.value
361
364
  };
362
365
  }
@@ -374,11 +377,14 @@ async function requireDatabaseContext(context, flags, commandName) {
374
377
  };
375
378
  }
376
379
  async function requireDatabaseProviderOnly(context) {
377
- await requireAuthenticatedAuthState(context);
380
+ const authState = await requireAuthenticatedAuthState(context);
378
381
  if (isRealMode(context)) {
379
382
  const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
380
383
  if (!client) throw authRequiredError();
381
- return createManagementDatabaseProvider(client, { formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd) });
384
+ return createManagementDatabaseProvider(client, {
385
+ formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd),
386
+ workspaceId: authState.workspace?.id
387
+ });
382
388
  }
383
389
  return createFixtureDatabaseProvider(context);
384
390
  }
@@ -1,8 +1,17 @@
1
1
  import { formatPrismaCliCommand } from "../../shell/cli-command.js";
2
2
  import { CliError } from "../../shell/errors.js";
3
3
  //#region src/lib/database/provider.ts
4
+ const SUBSCRIPTION_LOOKUP_TIMEOUT_MS = 3e3;
4
5
  function createManagementDatabaseProvider(client, options) {
5
6
  const formatCommand = options?.formatCommand ?? ((args) => formatPrismaCliCommand(args));
7
+ const toDatabaseApiError = (summary, response, error, signal) => databaseApiError({
8
+ client,
9
+ workspaceId: options?.workspaceId,
10
+ summary,
11
+ response,
12
+ error,
13
+ signal
14
+ });
6
15
  return {
7
16
  async listDatabases(options) {
8
17
  const databases = [];
@@ -16,7 +25,7 @@ function createManagementDatabaseProvider(client, options) {
16
25
  } },
17
26
  signal: options.signal
18
27
  });
19
- if (result.error || !result.data) throw databaseApiError("Failed to list databases", result.response, result.error);
28
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to list databases", result.response, result.error, options.signal);
20
29
  databases.push(...result.data.data);
21
30
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
22
31
  cursor = result.data.pagination.nextCursor;
@@ -28,8 +37,8 @@ function createManagementDatabaseProvider(client, options) {
28
37
  params: { path: { databaseId } },
29
38
  signal: options?.signal
30
39
  });
31
- if (result.response?.status === 404) return null;
32
- if (result.error || !result.data) throw databaseApiError("Failed to show database", result.response, result.error);
40
+ if (result.response?.status === 404 && !isPlanLimitApiError(result.error)) return null;
41
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to show database", result.response, result.error, options?.signal);
33
42
  const database = result.data.data;
34
43
  return normalizeDatabase(database, requireDatabaseProjectId(database, options?.projectId));
35
44
  },
@@ -44,7 +53,7 @@ function createManagementDatabaseProvider(client, options) {
44
53
  },
45
54
  signal: options.signal
46
55
  });
47
- if (result.error || !result.data) throw databaseApiError("Failed to create database", result.response, result.error);
56
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to create database", result.response, result.error, options.signal);
48
57
  return normalizeCreatedDatabase(result.data.data, options.projectId);
49
58
  },
50
59
  async removeDatabase(databaseId, options) {
@@ -52,14 +61,14 @@ function createManagementDatabaseProvider(client, options) {
52
61
  params: { path: { databaseId } },
53
62
  signal: options?.signal
54
63
  });
55
- if (result.error) throw databaseApiError("Failed to remove database", result.response, result.error);
64
+ if (result.error) throw await toDatabaseApiError("Failed to remove database", result.response, result.error, options?.signal);
56
65
  },
57
66
  async listConnections(databaseId, options) {
58
67
  const result = await client.GET("/v1/databases/{databaseId}/connections", {
59
68
  params: { path: { databaseId } },
60
69
  signal: options?.signal
61
70
  });
62
- if (result.error || !result.data) throw databaseApiError("Failed to list database connections", result.response, result.error);
71
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to list database connections", result.response, result.error, options?.signal);
63
72
  return result.data.data.map((connection) => normalizeConnection(connection, databaseId));
64
73
  },
65
74
  async createConnection(options) {
@@ -68,7 +77,7 @@ function createManagementDatabaseProvider(client, options) {
68
77
  body: { name: options.name },
69
78
  signal: options.signal
70
79
  });
71
- if (result.error || !result.data) throw databaseApiError("Failed to create database connection", result.response, result.error);
80
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to create database connection", result.response, result.error, options.signal);
72
81
  return normalizeCreatedConnection(result.data.data, options.databaseId);
73
82
  },
74
83
  async removeConnection(connectionId, options) {
@@ -76,7 +85,7 @@ function createManagementDatabaseProvider(client, options) {
76
85
  params: { path: { id: connectionId } },
77
86
  signal: options?.signal
78
87
  });
79
- if (result.error) throw databaseApiError("Failed to remove database connection", result.response, result.error);
88
+ if (result.error) throw await toDatabaseApiError("Failed to remove database connection", result.response, result.error, options?.signal);
80
89
  },
81
90
  async getUsage(databaseId, options) {
82
91
  const result = await client.GET("/v1/databases/{databaseId}/usage", {
@@ -89,7 +98,7 @@ function createManagementDatabaseProvider(client, options) {
89
98
  },
90
99
  signal: options?.signal
91
100
  });
92
- if (result.error || !result.data) throw databaseApiError("Failed to fetch database usage", result.response, result.error);
101
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to fetch database usage", result.response, result.error, options?.signal);
93
102
  return normalizeUsage(result.data);
94
103
  },
95
104
  async listBackups(databaseId, options) {
@@ -100,8 +109,8 @@ function createManagementDatabaseProvider(client, options) {
100
109
  },
101
110
  signal: options?.signal
102
111
  });
103
- if (result.response?.status === 422) throw backupsUnsupportedError(databaseId, result.error);
104
- if (result.error || !result.data) throw databaseApiError("Failed to list database backups", result.response, result.error);
112
+ if (result.response?.status === 422 && !isPlanLimitApiError(result.error)) throw backupsUnsupportedError(databaseId, result.error);
113
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to list database backups", result.response, result.error, options?.signal);
105
114
  return normalizeBackupList(result.data);
106
115
  },
107
116
  async restoreDatabase(options) {
@@ -114,9 +123,9 @@ function createManagementDatabaseProvider(client, options) {
114
123
  } },
115
124
  signal: options.signal
116
125
  });
117
- if (result.response?.status === 409) throw restoreConflictError(options.targetDatabaseId, result.error, formatCommand);
118
- if (result.response?.status === 404) throw restoreBackupNotFoundError(options, result.error, formatCommand);
119
- if (result.error || !result.data) throw databaseApiError("Failed to restore database", result.response, result.error);
126
+ if (result.response?.status === 409 && !isPlanLimitApiError(result.error)) throw restoreConflictError(options.targetDatabaseId, result.error, formatCommand);
127
+ if (result.response?.status === 404 && !isPlanLimitApiError(result.error)) throw restoreBackupNotFoundError(options, result.error, formatCommand);
128
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to restore database", result.response, result.error, options.signal);
120
129
  return normalizeDatabase(result.data.data, options.projectId);
121
130
  },
122
131
  async rotateConnection(connectionId, options) {
@@ -124,7 +133,7 @@ function createManagementDatabaseProvider(client, options) {
124
133
  params: { path: { id: connectionId } },
125
134
  signal: options?.signal
126
135
  });
127
- if (result.error || !result.data) throw databaseApiError("Failed to rotate database connection", result.response, result.error);
136
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to rotate database connection", result.response, result.error, options?.signal);
128
137
  return normalizeRotatedConnection(result.data.data);
129
138
  }
130
139
  };
@@ -298,17 +307,72 @@ function restoreConflictError(targetDatabaseId, error, formatCommand) {
298
307
  ])]
299
308
  });
300
309
  }
301
- function databaseApiError(summary, response, error) {
302
- const status = response?.status ?? 0;
310
+ async function databaseApiError(options) {
311
+ if (isPlanLimitApiError(options.error)) {
312
+ const subscription = options.workspaceId ? await readWorkspaceSubscription(options.client, options.workspaceId, options.signal) : null;
313
+ const workspaceLine = options.workspaceId ? `Workspace: ${options.workspaceId}` : "Workspace: unavailable";
314
+ const planName = subscription?.planName || null;
315
+ const usageBlocked = subscription?.usageBlocked ?? null;
316
+ const upgradeUrl = subscription?.upgradeUrl || null;
317
+ const recoveryLines = [...planName ? [`Current plan: ${planName}`] : [], upgradeUrl ? `Upgrade: ${upgradeUrl}` : "Upgrade: Open Prisma Console and upgrade the affected workspace plan."];
318
+ return new CliError({
319
+ code: "PLAN_LIMIT_REACHED",
320
+ domain: "database",
321
+ summary: "Workspace plan limit reached",
322
+ why: "Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.",
323
+ fix: upgradeUrl ? `Upgrade the workspace plan at ${upgradeUrl}.` : "Open Prisma Console and upgrade the affected workspace plan.",
324
+ meta: {
325
+ workspaceId: options.workspaceId ?? null,
326
+ blockedFeature: null,
327
+ planName,
328
+ usageBlocked,
329
+ upgradeUrl
330
+ },
331
+ exitCode: 1,
332
+ nextSteps: [],
333
+ humanLines: [
334
+ "Workspace plan limit reached [PLAN_LIMIT_REACHED]",
335
+ "",
336
+ "Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.",
337
+ "",
338
+ workspaceLine,
339
+ ...recoveryLines
340
+ ]
341
+ });
342
+ }
343
+ const status = options.response?.status ?? 0;
303
344
  return new CliError({
304
- code: error?.error?.code ?? "DATABASE_API_ERROR",
345
+ code: options.error?.error?.code ?? "DATABASE_API_ERROR",
305
346
  domain: "database",
306
- summary,
307
- why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
308
- fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
347
+ summary: options.summary,
348
+ why: options.error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
349
+ fix: options.error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
309
350
  exitCode: 1,
310
351
  nextSteps: []
311
352
  });
312
353
  }
354
+ function isPlanLimitApiError(error) {
355
+ return error?.error?.code === "planLimitReached";
356
+ }
357
+ async function readWorkspaceSubscription(client, workspaceId, signal) {
358
+ signal?.throwIfAborted();
359
+ const timeoutController = new AbortController();
360
+ const timeout = setTimeout(() => timeoutController.abort(), SUBSCRIPTION_LOOKUP_TIMEOUT_MS);
361
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutController.signal]) : timeoutController.signal;
362
+ try {
363
+ const result = await client.GET("/v1/workspaces/{id}/subscription", {
364
+ params: { path: { id: workspaceId } },
365
+ signal: requestSignal
366
+ });
367
+ signal?.throwIfAborted();
368
+ if (result.error) return null;
369
+ return result.data?.data ?? null;
370
+ } catch {
371
+ signal?.throwIfAborted();
372
+ return null;
373
+ } finally {
374
+ clearTimeout(timeout);
375
+ }
376
+ }
313
377
  //#endregion
314
378
  export { createManagementDatabaseProvider, normalizeConnection, normalizeDatabase };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.135.1",
3
+ "version": "3.0.0-dev.136.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,7 @@
46
46
  "@clack/prompts": "^1.5.0",
47
47
  "@prisma/compute-sdk": "0.34.0",
48
48
  "@prisma/credentials-store": "^7.8.0",
49
- "@prisma/management-api-sdk": "^1.50.0",
49
+ "@prisma/management-api-sdk": "^1.55.0",
50
50
  "better-result": "^2.9.2",
51
51
  "colorette": "^2.0.20",
52
52
  "commander": "^14.0.3",