@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.30
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/README.md +6 -15
- package/dist/adapters/local-state.js +15 -4
- package/dist/adapters/mock-api.js +244 -0
- package/dist/adapters/token-storage.js +335 -34
- package/dist/cli.js +7 -7
- package/dist/cli2.js +24 -5
- package/dist/commands/agent/index.js +60 -0
- package/dist/commands/app/index.js +91 -61
- package/dist/commands/auth/index.js +55 -2
- package/dist/commands/branch/index.js +2 -27
- package/dist/commands/bucket/index.js +123 -0
- package/dist/commands/build/index.js +29 -0
- package/dist/commands/database/index.js +249 -0
- package/dist/commands/env.js +8 -4
- package/dist/commands/feedback/index.js +20 -0
- package/dist/commands/git/index.js +1 -1
- package/dist/commands/init/index.js +33 -0
- package/dist/commands/project/index.js +54 -5
- package/dist/controllers/agent-setup.js +52 -0
- package/dist/controllers/agent.js +228 -0
- package/dist/controllers/app-env-api.js +55 -0
- package/dist/controllers/app-env-file.js +181 -0
- package/dist/controllers/app-env.js +227 -104
- package/dist/controllers/app.js +746 -306
- package/dist/controllers/auth.js +247 -3
- package/dist/controllers/branch.js +78 -48
- package/dist/controllers/bucket.js +278 -0
- package/dist/controllers/build.js +88 -0
- package/dist/controllers/database.js +567 -0
- package/dist/controllers/feedback.js +86 -0
- package/dist/controllers/init.js +753 -0
- package/dist/controllers/project.js +377 -22
- package/dist/controllers/select-prompt-port.js +1 -0
- package/dist/lib/agent/cli-command.js +20 -0
- package/dist/lib/agent/constants.js +12 -0
- package/dist/lib/agent/package-manager.js +99 -0
- package/dist/lib/agent/setup-status.js +83 -0
- package/dist/lib/app/{preview-provider.js → app-provider.js} +137 -88
- package/dist/lib/app/branch-database-api.js +102 -0
- package/dist/lib/app/branch-database-deploy.js +326 -0
- package/dist/lib/app/branch-database.js +216 -0
- package/dist/lib/app/build-settings.js +93 -0
- package/dist/lib/app/build.js +83 -0
- package/dist/lib/app/bun-project.js +3 -4
- package/dist/lib/app/compute-config.js +145 -0
- package/dist/lib/app/deploy-plan.js +59 -0
- package/dist/lib/app/{preview-progress.js → deploy-progress.js} +12 -12
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/env-vars.js +28 -2
- package/dist/lib/app/local-dev.js +3 -60
- package/dist/lib/app/production-deploy-gate.js +162 -0
- package/dist/lib/app/read-branch.js +30 -0
- package/dist/lib/auth/auth-ops.js +10 -4
- package/dist/lib/auth/guard.js +4 -1
- package/dist/lib/auth/login.js +33 -26
- package/dist/lib/auth/recipient.js +42 -0
- package/dist/lib/bucket/provider.js +139 -0
- package/dist/lib/database/provider.js +378 -0
- package/dist/lib/diagnostics.js +15 -0
- package/dist/lib/fs/home-path.js +24 -0
- package/dist/lib/git/local-branch.js +53 -0
- package/dist/lib/git/local-status.js +57 -0
- package/dist/lib/project/interactive-setup.js +5 -4
- package/dist/lib/project/local-pin.js +171 -41
- package/dist/lib/project/provider.js +92 -0
- package/dist/lib/project/resolution.js +199 -48
- package/dist/lib/project/setup.js +67 -20
- package/dist/output/patterns.js +1 -1
- package/dist/presenters/agent.js +74 -0
- package/dist/presenters/app-env.js +149 -14
- package/dist/presenters/app.js +208 -27
- package/dist/presenters/auth.js +99 -2
- package/dist/presenters/branch.js +37 -102
- package/dist/presenters/bucket.js +174 -0
- package/dist/presenters/database.js +448 -0
- package/dist/presenters/feedback.js +26 -0
- package/dist/presenters/init.js +30 -0
- package/dist/presenters/project.js +139 -27
- package/dist/presenters/verbose-context.js +64 -0
- package/dist/shell/cli-command.js +12 -0
- package/dist/shell/command-arguments.js +7 -1
- package/dist/shell/command-meta.js +458 -17
- package/dist/shell/command-runner.js +58 -18
- package/dist/shell/diagnostics-output.js +57 -0
- package/dist/shell/errors.js +56 -1
- package/dist/shell/help.js +31 -20
- package/dist/shell/output.js +72 -1
- package/dist/shell/prompt.js +12 -5
- package/dist/shell/runtime.js +8 -4
- package/dist/shell/ui.js +42 -3
- package/dist/shell/update-check.js +2 -2
- package/dist/use-cases/auth.js +68 -1
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/dist/use-cases/project.js +2 -1
- package/package.json +21 -4
- package/dist/lib/app/preview-build.js +0 -312
- package/dist/lib/app/preview-interaction.js +0 -5
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { resolvePrismaCliPackageCommandFormatterSync } from "../lib/agent/cli-command.js";
|
|
2
|
+
import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
|
|
3
|
+
import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
|
|
4
|
+
import { requireComputeAuth } from "../lib/auth/guard.js";
|
|
5
|
+
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
6
|
+
import { listFixtureWorkspaceProjects, listRealWorkspaceProjects } from "./project.js";
|
|
7
|
+
import { createManagementDatabaseProvider, normalizeConnection, normalizeDatabase } from "../lib/database/provider.js";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
//#region src/controllers/database.ts
|
|
10
|
+
function isRealMode(context) {
|
|
11
|
+
return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
|
|
12
|
+
}
|
|
13
|
+
async function runDatabaseList(context, flags) {
|
|
14
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database list");
|
|
15
|
+
const databases = sortDatabases(await provider.listDatabases({
|
|
16
|
+
projectId: target.project.id,
|
|
17
|
+
branchName: flags.branchName,
|
|
18
|
+
signal: context.runtime.signal
|
|
19
|
+
}));
|
|
20
|
+
return {
|
|
21
|
+
command: "database.list",
|
|
22
|
+
result: {
|
|
23
|
+
projectId: target.project.id,
|
|
24
|
+
projectName: target.project.name,
|
|
25
|
+
branchName: flags.branchName ?? null,
|
|
26
|
+
verboseContext: target,
|
|
27
|
+
databases
|
|
28
|
+
},
|
|
29
|
+
warnings: [],
|
|
30
|
+
nextSteps: []
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function runDatabaseShow(context, databaseRef, flags) {
|
|
34
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database show");
|
|
35
|
+
const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
|
|
36
|
+
const connections = await provider.listConnections(database.id, { signal: context.runtime.signal });
|
|
37
|
+
return {
|
|
38
|
+
command: "database.show",
|
|
39
|
+
result: {
|
|
40
|
+
projectId: target.project.id,
|
|
41
|
+
projectName: target.project.name,
|
|
42
|
+
verboseContext: target,
|
|
43
|
+
database,
|
|
44
|
+
connections
|
|
45
|
+
},
|
|
46
|
+
warnings: [],
|
|
47
|
+
nextSteps: []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function runDatabaseCreate(context, name, flags) {
|
|
51
|
+
const databaseName = name.trim();
|
|
52
|
+
if (!databaseName) throw usageError("Database name required", "Database create needs a non-empty name.", "Pass a database name.", ["prisma-cli database create <name>"], "database");
|
|
53
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database create");
|
|
54
|
+
const created = await provider.createDatabase({
|
|
55
|
+
projectId: target.project.id,
|
|
56
|
+
name: databaseName,
|
|
57
|
+
branchName: flags.branchName,
|
|
58
|
+
region: flags.region,
|
|
59
|
+
signal: context.runtime.signal
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
command: "database.create",
|
|
63
|
+
result: {
|
|
64
|
+
projectId: target.project.id,
|
|
65
|
+
projectName: target.project.name,
|
|
66
|
+
verboseContext: target,
|
|
67
|
+
database: ensureProjectId(created.database, target.project.id),
|
|
68
|
+
connection: created.connection,
|
|
69
|
+
connectionString: created.connectionString
|
|
70
|
+
},
|
|
71
|
+
warnings: [],
|
|
72
|
+
nextSteps: []
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function runDatabaseRemove(context, databaseRef, flags) {
|
|
76
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database remove");
|
|
77
|
+
const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
|
|
78
|
+
requireExactConfirmation({
|
|
79
|
+
resourceName: "database",
|
|
80
|
+
commandName: "database remove",
|
|
81
|
+
id: database.id,
|
|
82
|
+
confirm: flags.confirm
|
|
83
|
+
});
|
|
84
|
+
await provider.removeDatabase(database.id, { signal: context.runtime.signal });
|
|
85
|
+
return {
|
|
86
|
+
command: "database.remove",
|
|
87
|
+
result: {
|
|
88
|
+
projectId: target.project.id,
|
|
89
|
+
projectName: target.project.name,
|
|
90
|
+
verboseContext: target,
|
|
91
|
+
database
|
|
92
|
+
},
|
|
93
|
+
warnings: [],
|
|
94
|
+
nextSteps: []
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function runDatabaseConnectionList(context, databaseRef, flags) {
|
|
98
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database connection list");
|
|
99
|
+
const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
|
|
100
|
+
const connections = await provider.listConnections(database.id, { signal: context.runtime.signal });
|
|
101
|
+
return {
|
|
102
|
+
command: "database.connection.list",
|
|
103
|
+
result: {
|
|
104
|
+
projectId: target.project.id,
|
|
105
|
+
projectName: target.project.name,
|
|
106
|
+
verboseContext: target,
|
|
107
|
+
database,
|
|
108
|
+
connections
|
|
109
|
+
},
|
|
110
|
+
warnings: [],
|
|
111
|
+
nextSteps: []
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function runDatabaseConnectionCreate(context, databaseRef, flags) {
|
|
115
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database connection create");
|
|
116
|
+
const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
|
|
117
|
+
const created = await provider.createConnection({
|
|
118
|
+
databaseId: database.id,
|
|
119
|
+
name: flags.name?.trim() || defaultConnectionName(),
|
|
120
|
+
signal: context.runtime.signal
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
command: "database.connection.create",
|
|
124
|
+
result: {
|
|
125
|
+
projectId: target.project.id,
|
|
126
|
+
projectName: target.project.name,
|
|
127
|
+
verboseContext: target,
|
|
128
|
+
database,
|
|
129
|
+
connection: created.connection,
|
|
130
|
+
connectionString: created.connectionString
|
|
131
|
+
},
|
|
132
|
+
warnings: [],
|
|
133
|
+
nextSteps: []
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function runDatabaseConnectionRemove(context, connectionRef, flags) {
|
|
137
|
+
const connectionId = connectionRef.trim();
|
|
138
|
+
if (!connectionId) throw usageError("Connection id required", "Database connection removal needs a connection id.", "Pass the connection id to remove.", ["prisma-cli database connection remove <connection-id> --confirm <connection-id>"], "database");
|
|
139
|
+
requireExactConfirmation({
|
|
140
|
+
resourceName: "database connection",
|
|
141
|
+
commandName: "database connection remove",
|
|
142
|
+
id: connectionId,
|
|
143
|
+
confirm: flags.confirm
|
|
144
|
+
});
|
|
145
|
+
await (await requireDatabaseProviderOnly(context)).removeConnection(connectionId, { signal: context.runtime.signal });
|
|
146
|
+
return {
|
|
147
|
+
command: "database.connection.remove",
|
|
148
|
+
result: { connection: { id: connectionId } },
|
|
149
|
+
warnings: [],
|
|
150
|
+
nextSteps: []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function runDatabaseUsage(context, databaseRef, flags) {
|
|
154
|
+
const formatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
|
|
155
|
+
const from = parseUsageDate(flags.from, "--from", "start", formatCommand);
|
|
156
|
+
const to = parseUsageDate(flags.to, "--to", "end", formatCommand);
|
|
157
|
+
if (from && to && Date.parse(from) > Date.parse(to)) throw usageError("Invalid usage period", "--from must not be later than --to.", "Pass a --from date that is on or before the --to date.", [formatCommand([
|
|
158
|
+
"database",
|
|
159
|
+
"usage",
|
|
160
|
+
"<database>",
|
|
161
|
+
"--from",
|
|
162
|
+
"2026-06-01",
|
|
163
|
+
"--to",
|
|
164
|
+
"2026-06-30"
|
|
165
|
+
])], "database");
|
|
166
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database usage");
|
|
167
|
+
const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
|
|
168
|
+
const usage = await provider.getUsage(database.id, {
|
|
169
|
+
from,
|
|
170
|
+
to,
|
|
171
|
+
signal: context.runtime.signal
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
command: "database.usage",
|
|
175
|
+
result: {
|
|
176
|
+
projectId: target.project.id,
|
|
177
|
+
projectName: target.project.name,
|
|
178
|
+
verboseContext: target,
|
|
179
|
+
database,
|
|
180
|
+
period: usage.period,
|
|
181
|
+
metrics: usage.metrics,
|
|
182
|
+
generatedAt: usage.generatedAt
|
|
183
|
+
},
|
|
184
|
+
warnings: [],
|
|
185
|
+
nextSteps: []
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function runDatabaseBackupList(context, databaseRef, flags) {
|
|
189
|
+
const limit = parseBackupLimit(flags.limit, resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd));
|
|
190
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database backup list");
|
|
191
|
+
const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
|
|
192
|
+
const backups = await provider.listBackups(database.id, {
|
|
193
|
+
limit,
|
|
194
|
+
signal: context.runtime.signal
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
command: "database.backup.list",
|
|
198
|
+
result: {
|
|
199
|
+
projectId: target.project.id,
|
|
200
|
+
projectName: target.project.name,
|
|
201
|
+
verboseContext: target,
|
|
202
|
+
database,
|
|
203
|
+
backups: backups.backups,
|
|
204
|
+
retentionDays: backups.retentionDays,
|
|
205
|
+
hasMore: backups.hasMore
|
|
206
|
+
},
|
|
207
|
+
warnings: [],
|
|
208
|
+
nextSteps: []
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function runDatabaseRestore(context, databaseRef, flags) {
|
|
212
|
+
const formatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
|
|
213
|
+
const backupId = flags.backupId?.trim();
|
|
214
|
+
if (!backupId) throw usageError("Backup id required", "Database restore needs the backup to restore from.", `Pass --backup <backup-id> from ${formatCommand([
|
|
215
|
+
"database",
|
|
216
|
+
"backup",
|
|
217
|
+
"list",
|
|
218
|
+
"<database>"
|
|
219
|
+
])}.`, [formatCommand([
|
|
220
|
+
"database",
|
|
221
|
+
"backup",
|
|
222
|
+
"list",
|
|
223
|
+
"<database>"
|
|
224
|
+
])], "database");
|
|
225
|
+
const { provider, target } = await requireDatabaseContext(context, flags, "database restore");
|
|
226
|
+
const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
|
|
227
|
+
const sourceDatabase = flags.sourceDatabaseRef ? await resolveDatabase(provider, target, flags.sourceDatabaseRef, flags.branchName, context.runtime.signal) : database;
|
|
228
|
+
const sourceDatabaseArg = sourceDatabase.id === database.id ? "" : ` --source-database ${sourceDatabase.id}`;
|
|
229
|
+
requireExactConfirmation({
|
|
230
|
+
resourceName: "database",
|
|
231
|
+
commandName: "database restore",
|
|
232
|
+
id: database.id,
|
|
233
|
+
confirm: flags.confirm,
|
|
234
|
+
summary: "Confirm database restore",
|
|
235
|
+
why: "Restoring immediately and irreversibly overwrites all data in the target database, so it requires the exact target database id.",
|
|
236
|
+
nextStep: `${formatCommand([
|
|
237
|
+
"database",
|
|
238
|
+
"restore",
|
|
239
|
+
database.id,
|
|
240
|
+
"--backup",
|
|
241
|
+
backupId
|
|
242
|
+
])}${sourceDatabaseArg} --confirm ${database.id}`
|
|
243
|
+
});
|
|
244
|
+
const restored = await provider.restoreDatabase({
|
|
245
|
+
targetDatabaseId: database.id,
|
|
246
|
+
sourceDatabaseId: sourceDatabase.id,
|
|
247
|
+
backupId,
|
|
248
|
+
projectId: target.project.id,
|
|
249
|
+
signal: context.runtime.signal
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
command: "database.restore",
|
|
253
|
+
result: {
|
|
254
|
+
projectId: target.project.id,
|
|
255
|
+
projectName: target.project.name,
|
|
256
|
+
verboseContext: target,
|
|
257
|
+
database: ensureProjectId(restored, target.project.id),
|
|
258
|
+
source: {
|
|
259
|
+
databaseId: sourceDatabase.id,
|
|
260
|
+
backupId
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
warnings: [],
|
|
264
|
+
nextSteps: [formatCommand([
|
|
265
|
+
"database",
|
|
266
|
+
"show",
|
|
267
|
+
database.id
|
|
268
|
+
])]
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function runDatabaseConnectionRotate(context, connectionRef, flags) {
|
|
272
|
+
const formatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
|
|
273
|
+
const connectionId = connectionRef.trim();
|
|
274
|
+
if (!connectionId) throw usageError("Connection id required", "Database connection rotation needs a connection id.", "Pass the connection id to rotate.", [formatCommand([
|
|
275
|
+
"database",
|
|
276
|
+
"connection",
|
|
277
|
+
"rotate",
|
|
278
|
+
"<connection-id>",
|
|
279
|
+
"--confirm",
|
|
280
|
+
"<connection-id>"
|
|
281
|
+
])], "database");
|
|
282
|
+
requireExactConfirmation({
|
|
283
|
+
resourceName: "database connection",
|
|
284
|
+
commandName: "database connection rotate",
|
|
285
|
+
id: connectionId,
|
|
286
|
+
confirm: flags.confirm,
|
|
287
|
+
summary: "Confirm database connection rotation",
|
|
288
|
+
why: "Rotating revokes the previous credentials and breaks clients still using them, so it requires the exact connection id.",
|
|
289
|
+
nextStep: formatCommand([
|
|
290
|
+
"database",
|
|
291
|
+
"connection",
|
|
292
|
+
"rotate",
|
|
293
|
+
connectionId,
|
|
294
|
+
"--confirm",
|
|
295
|
+
connectionId
|
|
296
|
+
])
|
|
297
|
+
});
|
|
298
|
+
const rotated = await (await requireDatabaseProviderOnly(context)).rotateConnection(connectionId, { signal: context.runtime.signal });
|
|
299
|
+
return {
|
|
300
|
+
command: "database.connection.rotate",
|
|
301
|
+
result: {
|
|
302
|
+
connection: rotated.connection,
|
|
303
|
+
database: rotated.database,
|
|
304
|
+
connectionString: rotated.connectionString
|
|
305
|
+
},
|
|
306
|
+
warnings: [],
|
|
307
|
+
nextSteps: []
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const USAGE_DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
311
|
+
const USAGE_DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
312
|
+
function parseUsageDate(value, flagName, dayBoundary, formatCommand) {
|
|
313
|
+
if (value === void 0) return;
|
|
314
|
+
const trimmed = value.trim();
|
|
315
|
+
if (USAGE_DATE_ONLY_PATTERN.test(trimmed) && isValidCalendarDate(trimmed)) return dayBoundary === "start" ? `${trimmed}T00:00:00.000Z` : `${trimmed}T23:59:59.999Z`;
|
|
316
|
+
if (USAGE_DATETIME_PATTERN.test(trimmed) && !Number.isNaN(Date.parse(trimmed)) && isValidCalendarDate(trimmed.slice(0, 10))) return trimmed;
|
|
317
|
+
throw usageError("Invalid usage period", `${flagName} must be an ISO date such as 2026-06-01 or an ISO datetime such as 2026-06-01T12:00:00Z.`, `Pass an ISO date or datetime to ${flagName}.`, [formatCommand([
|
|
318
|
+
"database",
|
|
319
|
+
"usage",
|
|
320
|
+
"<database>",
|
|
321
|
+
"--from",
|
|
322
|
+
"2026-06-01",
|
|
323
|
+
"--to",
|
|
324
|
+
"2026-06-30"
|
|
325
|
+
])], "database");
|
|
326
|
+
}
|
|
327
|
+
function isValidCalendarDate(datePart) {
|
|
328
|
+
const timestamp = Date.parse(`${datePart}T00:00:00.000Z`);
|
|
329
|
+
return !Number.isNaN(timestamp) && new Date(timestamp).toISOString().startsWith(datePart);
|
|
330
|
+
}
|
|
331
|
+
function parseBackupLimit(value, formatCommand) {
|
|
332
|
+
if (value === void 0) return;
|
|
333
|
+
const limit = Number(value.trim());
|
|
334
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw usageError("Invalid backup limit", "--limit must be an integer between 1 and 100.", "Pass a --limit between 1 and 100.", [formatCommand([
|
|
335
|
+
"database",
|
|
336
|
+
"backup",
|
|
337
|
+
"list",
|
|
338
|
+
"<database>",
|
|
339
|
+
"--limit",
|
|
340
|
+
"50"
|
|
341
|
+
])], "database");
|
|
342
|
+
return limit;
|
|
343
|
+
}
|
|
344
|
+
async function requireDatabaseContext(context, flags, commandName) {
|
|
345
|
+
const workspace = (await requireAuthenticatedAuthState(context)).workspace;
|
|
346
|
+
if (!workspace) throw workspaceRequiredError();
|
|
347
|
+
if (isRealMode(context)) {
|
|
348
|
+
const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
|
|
349
|
+
if (!client) throw authRequiredError();
|
|
350
|
+
const targetResult = await resolveProjectTarget({
|
|
351
|
+
context,
|
|
352
|
+
workspace,
|
|
353
|
+
explicitProject: flags.projectRef,
|
|
354
|
+
listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
|
|
355
|
+
commandName
|
|
356
|
+
});
|
|
357
|
+
if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
|
|
358
|
+
return {
|
|
359
|
+
provider: createManagementDatabaseProvider(client, {
|
|
360
|
+
formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd),
|
|
361
|
+
workspaceId: workspace.id
|
|
362
|
+
}),
|
|
363
|
+
target: targetResult.value
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const targetResult = await resolveProjectTarget({
|
|
367
|
+
context,
|
|
368
|
+
workspace,
|
|
369
|
+
explicitProject: flags.projectRef,
|
|
370
|
+
listProjects: async () => listFixtureWorkspaceProjects(context, workspace),
|
|
371
|
+
commandName
|
|
372
|
+
});
|
|
373
|
+
if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
|
|
374
|
+
return {
|
|
375
|
+
provider: createFixtureDatabaseProvider(context),
|
|
376
|
+
target: targetResult.value
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async function requireDatabaseProviderOnly(context) {
|
|
380
|
+
const authState = await requireAuthenticatedAuthState(context);
|
|
381
|
+
if (isRealMode(context)) {
|
|
382
|
+
const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
|
|
383
|
+
if (!client) throw authRequiredError();
|
|
384
|
+
return createManagementDatabaseProvider(client, {
|
|
385
|
+
formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd),
|
|
386
|
+
workspaceId: authState.workspace?.id
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return createFixtureDatabaseProvider(context);
|
|
390
|
+
}
|
|
391
|
+
function createFixtureDatabaseProvider(context) {
|
|
392
|
+
return {
|
|
393
|
+
async listDatabases(options) {
|
|
394
|
+
return context.api.listDatabasesForProject(options.projectId, options.branchName).map((database) => normalizeDatabase(database, database.projectId));
|
|
395
|
+
},
|
|
396
|
+
async showDatabase(databaseId) {
|
|
397
|
+
const database = context.api.getDatabase(databaseId);
|
|
398
|
+
return database ? normalizeDatabase(database, database.projectId) : null;
|
|
399
|
+
},
|
|
400
|
+
async createDatabase(options) {
|
|
401
|
+
const created = context.api.createDatabase(options);
|
|
402
|
+
return {
|
|
403
|
+
database: normalizeDatabase(created.database, created.database.projectId),
|
|
404
|
+
connection: normalizeConnection(created.connection, created.connection.databaseId),
|
|
405
|
+
connectionString: created.connectionString
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
async removeDatabase(databaseId) {
|
|
409
|
+
if (!context.api.removeDatabase(databaseId)) throw databaseNotFoundError(databaseId);
|
|
410
|
+
},
|
|
411
|
+
async listConnections(databaseId) {
|
|
412
|
+
if (!context.api.getDatabase(databaseId)) throw databaseNotFoundError(databaseId);
|
|
413
|
+
return context.api.listDatabaseConnections(databaseId).map((connection) => normalizeConnection(connection, connection.databaseId));
|
|
414
|
+
},
|
|
415
|
+
async createConnection(options) {
|
|
416
|
+
const created = context.api.createDatabaseConnection(options);
|
|
417
|
+
if (!created) throw databaseNotFoundError(options.databaseId);
|
|
418
|
+
return {
|
|
419
|
+
connection: normalizeConnection(created.connection, created.connection.databaseId),
|
|
420
|
+
connectionString: created.connectionString
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
async removeConnection(connectionId) {
|
|
424
|
+
if (!context.api.removeDatabaseConnection(connectionId)) throw connectionNotFoundError(connectionId);
|
|
425
|
+
},
|
|
426
|
+
async getUsage(databaseId, options) {
|
|
427
|
+
if (!context.api.getDatabase(databaseId)) throw databaseNotFoundError(databaseId);
|
|
428
|
+
return context.api.getDatabaseUsage(databaseId, {
|
|
429
|
+
from: options?.from,
|
|
430
|
+
to: options?.to
|
|
431
|
+
});
|
|
432
|
+
},
|
|
433
|
+
async listBackups(databaseId, options) {
|
|
434
|
+
if (!context.api.getDatabase(databaseId)) throw databaseNotFoundError(databaseId);
|
|
435
|
+
return context.api.listDatabaseBackups(databaseId, options?.limit);
|
|
436
|
+
},
|
|
437
|
+
async restoreDatabase(options) {
|
|
438
|
+
const restored = context.api.restoreDatabase({
|
|
439
|
+
targetDatabaseId: options.targetDatabaseId,
|
|
440
|
+
sourceDatabaseId: options.sourceDatabaseId,
|
|
441
|
+
backupId: options.backupId
|
|
442
|
+
});
|
|
443
|
+
if (restored.outcome === "target-not-found") throw databaseNotFoundError(options.targetDatabaseId);
|
|
444
|
+
if (restored.outcome === "backup-not-found") throw backupNotFoundError(options.backupId, options.sourceDatabaseId, resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd));
|
|
445
|
+
return normalizeDatabase(restored.database, options.projectId);
|
|
446
|
+
},
|
|
447
|
+
async rotateConnection(connectionId) {
|
|
448
|
+
const rotated = context.api.rotateDatabaseConnection(connectionId);
|
|
449
|
+
if (!rotated) throw connectionNotFoundError(connectionId);
|
|
450
|
+
const database = context.api.getDatabase(rotated.connection.databaseId);
|
|
451
|
+
return {
|
|
452
|
+
connection: normalizeConnection(rotated.connection, rotated.connection.databaseId),
|
|
453
|
+
database: database ? {
|
|
454
|
+
id: database.id,
|
|
455
|
+
name: database.name
|
|
456
|
+
} : null,
|
|
457
|
+
connectionString: rotated.connectionString
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
async function resolveDatabase(provider, target, databaseRef, branchName, signal) {
|
|
463
|
+
const ref = databaseRef.trim();
|
|
464
|
+
if (!ref) throw usageError("Database id or name required", "This command needs a database id or name.", "Pass a database id or name.", ["prisma-cli database list"], "database");
|
|
465
|
+
const matches = (await provider.listDatabases({
|
|
466
|
+
projectId: target.project.id,
|
|
467
|
+
branchName,
|
|
468
|
+
signal
|
|
469
|
+
})).filter((database) => database.id === ref || database.name === ref);
|
|
470
|
+
if (matches.length === 0) throw databaseNotFoundError(ref, target.project.name, branchName);
|
|
471
|
+
if (matches.length > 1) throw databaseAmbiguousError(ref, matches, branchName);
|
|
472
|
+
const selected = matches[0];
|
|
473
|
+
return ensureProjectId(await provider.showDatabase(selected.id, {
|
|
474
|
+
projectId: target.project.id,
|
|
475
|
+
signal
|
|
476
|
+
}) ?? selected, target.project.id);
|
|
477
|
+
}
|
|
478
|
+
function ensureProjectId(database, projectId) {
|
|
479
|
+
return database.projectId ? database : {
|
|
480
|
+
...database,
|
|
481
|
+
projectId
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function sortDatabases(databases) {
|
|
485
|
+
return databases.slice().sort((left, right) => {
|
|
486
|
+
const branchOrder = (left.branchName ?? "").localeCompare(right.branchName ?? "");
|
|
487
|
+
if (branchOrder !== 0) return branchOrder;
|
|
488
|
+
const nameOrder = left.name.localeCompare(right.name);
|
|
489
|
+
return nameOrder !== 0 ? nameOrder : left.id.localeCompare(right.id);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function requireExactConfirmation(options) {
|
|
493
|
+
if (options.confirm === options.id) return;
|
|
494
|
+
throw new CliError({
|
|
495
|
+
code: "CONFIRMATION_REQUIRED",
|
|
496
|
+
domain: "database",
|
|
497
|
+
summary: options.summary ?? `Confirm ${options.resourceName} removal`,
|
|
498
|
+
why: options.why ?? `Removing this ${options.resourceName} is destructive and requires the exact id.`,
|
|
499
|
+
fix: `Rerun with --confirm ${options.id}.`,
|
|
500
|
+
exitCode: 2,
|
|
501
|
+
nextSteps: [options.nextStep ?? `prisma-cli ${options.commandName} ${options.id} --confirm ${options.id}`],
|
|
502
|
+
meta: {
|
|
503
|
+
expectedConfirm: options.id,
|
|
504
|
+
receivedConfirm: options.confirm ?? null
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function defaultConnectionName() {
|
|
509
|
+
return `cli-${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 17)}-${randomBytes(2).toString("hex")}`;
|
|
510
|
+
}
|
|
511
|
+
function databaseNotFoundError(databaseRef, projectName, branchName) {
|
|
512
|
+
return new CliError({
|
|
513
|
+
code: "DATABASE_NOT_FOUND",
|
|
514
|
+
domain: "database",
|
|
515
|
+
summary: "Database not found",
|
|
516
|
+
why: `No database matched "${databaseRef}"${projectName ? ` in project "${projectName}"${branchName ? ` on branch "${branchName}"` : ""}` : ""}.`,
|
|
517
|
+
fix: "Pass a database id or name from prisma-cli database list.",
|
|
518
|
+
exitCode: 1,
|
|
519
|
+
nextSteps: ["prisma-cli database list"]
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
function databaseAmbiguousError(databaseRef, matches, branchName) {
|
|
523
|
+
return new CliError({
|
|
524
|
+
code: "DATABASE_AMBIGUOUS",
|
|
525
|
+
domain: "database",
|
|
526
|
+
summary: "Database resolution is ambiguous",
|
|
527
|
+
why: branchName ? `Multiple databases matched "${databaseRef}" on branch "${branchName}".` : `Multiple databases matched "${databaseRef}".`,
|
|
528
|
+
fix: "Pass the database id, or pass --branch <git-name> to narrow the match.",
|
|
529
|
+
exitCode: 1,
|
|
530
|
+
nextSteps: ["prisma-cli database list"],
|
|
531
|
+
meta: { matches: matches.map((database) => ({
|
|
532
|
+
id: database.id,
|
|
533
|
+
name: database.name,
|
|
534
|
+
branchName: database.branchName
|
|
535
|
+
})) }
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function backupNotFoundError(backupId, sourceDatabaseId, formatCommand) {
|
|
539
|
+
const listCommand = formatCommand([
|
|
540
|
+
"database",
|
|
541
|
+
"backup",
|
|
542
|
+
"list",
|
|
543
|
+
sourceDatabaseId
|
|
544
|
+
]);
|
|
545
|
+
return new CliError({
|
|
546
|
+
code: "DATABASE_BACKUP_NOT_FOUND",
|
|
547
|
+
domain: "database",
|
|
548
|
+
summary: "Database backup not found",
|
|
549
|
+
why: `No backup matched "${backupId}" for database "${sourceDatabaseId}".`,
|
|
550
|
+
fix: `Pass a backup id from ${listCommand}.`,
|
|
551
|
+
exitCode: 1,
|
|
552
|
+
nextSteps: [listCommand]
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
function connectionNotFoundError(connectionId) {
|
|
556
|
+
return new CliError({
|
|
557
|
+
code: "DATABASE_CONNECTION_NOT_FOUND",
|
|
558
|
+
domain: "database",
|
|
559
|
+
summary: "Database connection not found",
|
|
560
|
+
why: `No database connection matched "${connectionId}".`,
|
|
561
|
+
fix: "Pass a connection id from prisma-cli database connection list <database>.",
|
|
562
|
+
exitCode: 1,
|
|
563
|
+
nextSteps: ["prisma-cli database connection list <database>"]
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
//#endregion
|
|
567
|
+
export { runDatabaseBackupList, runDatabaseConnectionCreate, runDatabaseConnectionList, runDatabaseConnectionRemove, runDatabaseConnectionRotate, runDatabaseCreate, runDatabaseList, runDatabaseRemove, runDatabaseRestore, runDatabaseShow, runDatabaseUsage };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { CliError, usageError } from "../shell/errors.js";
|
|
2
|
+
import { getCliVersion } from "../lib/version.js";
|
|
3
|
+
//#region src/controllers/feedback.ts
|
|
4
|
+
const DEFAULT_FEEDBACK_ENDPOINT = "https://hiieirp2pwqnjvq9axzyg6d0.fra.prisma.build/feedback";
|
|
5
|
+
const FEEDBACK_TIMEOUT_MS = 3e3;
|
|
6
|
+
const MAX_MESSAGE_LENGTH = 4e3;
|
|
7
|
+
const MAX_EMAIL_LENGTH = 320;
|
|
8
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9
|
+
async function runFeedback(context, messageArg, flags) {
|
|
10
|
+
const message = messageArg.trim();
|
|
11
|
+
if (!message) throw usageError("Feedback message required", "The message argument is empty.", "Pass a non-empty message.", ["prisma-cli feedback \"the deploy flow is great\""]);
|
|
12
|
+
if (message.length > MAX_MESSAGE_LENGTH) throw usageError("Feedback message too long", `The message is ${message.length} characters; the limit is ${MAX_MESSAGE_LENGTH}.`, "Shorten the message.");
|
|
13
|
+
const email = flags.email?.trim();
|
|
14
|
+
if (email !== void 0 && (email.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(email))) throw usageError("Invalid email", `"${flags.email}" is not a valid email address of at most ${MAX_EMAIL_LENGTH} characters.`, "Pass a valid address with --email, or drop the flag to stay anonymous.", ["prisma-cli feedback \"please add X\" --email you@example.com"]);
|
|
15
|
+
const feedbackContext = {
|
|
16
|
+
cliVersion: getCliVersion(),
|
|
17
|
+
nodeVersion: process.version,
|
|
18
|
+
platform: process.platform,
|
|
19
|
+
arch: process.arch
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
command: "feedback",
|
|
23
|
+
result: {
|
|
24
|
+
id: await postFeedback(context, context.runtime.env.PRISMA_CLI_FEEDBACK_URL || DEFAULT_FEEDBACK_ENDPOINT, {
|
|
25
|
+
message,
|
|
26
|
+
...email ? { email } : {},
|
|
27
|
+
meta: { ...feedbackContext }
|
|
28
|
+
}),
|
|
29
|
+
email: email ?? null,
|
|
30
|
+
context: feedbackContext
|
|
31
|
+
},
|
|
32
|
+
warnings: [],
|
|
33
|
+
nextSteps: []
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function postFeedback(context, endpoint, body) {
|
|
37
|
+
let response;
|
|
38
|
+
try {
|
|
39
|
+
response = await fetch(endpoint, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"content-type": "application/json",
|
|
43
|
+
"user-agent": `prisma-cli/${getCliVersion()}`
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(body),
|
|
46
|
+
signal: AbortSignal.any([context.runtime.signal, AbortSignal.timeout(FEEDBACK_TIMEOUT_MS)])
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (context.runtime.signal.aborted) throw error;
|
|
50
|
+
throw feedbackSendFailed(error instanceof Error && error.name === "TimeoutError" ? TIMEOUT_DETAIL : `The feedback service could not be reached${error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : ""}.`);
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) throw feedbackSendFailed(`The feedback service responded with HTTP ${response.status}${await readServiceError(context, response)}.`);
|
|
53
|
+
let payload;
|
|
54
|
+
try {
|
|
55
|
+
payload = await response.json();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (context.runtime.signal.aborted) throw error;
|
|
58
|
+
if (!(error instanceof SyntaxError)) throw feedbackSendFailed(error instanceof Error && error.name === "TimeoutError" ? TIMEOUT_DETAIL : "The feedback service response could not be read.");
|
|
59
|
+
payload = null;
|
|
60
|
+
}
|
|
61
|
+
return typeof payload?.id === "string" ? payload.id : null;
|
|
62
|
+
}
|
|
63
|
+
const TIMEOUT_DETAIL = `The feedback service did not answer within ${FEEDBACK_TIMEOUT_MS / 1e3} seconds.`;
|
|
64
|
+
async function readServiceError(context, response) {
|
|
65
|
+
let payload;
|
|
66
|
+
try {
|
|
67
|
+
payload = await response.json();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (context.runtime.signal.aborted) throw error;
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
return typeof payload?.error?.message === "string" ? ` (${payload.error.message})` : "";
|
|
73
|
+
}
|
|
74
|
+
function feedbackSendFailed(detail) {
|
|
75
|
+
return new CliError({
|
|
76
|
+
code: "FEEDBACK_SEND_FAILED",
|
|
77
|
+
domain: "cli",
|
|
78
|
+
summary: "Feedback could not be delivered",
|
|
79
|
+
why: detail,
|
|
80
|
+
fix: "Check your network and rerun the command.",
|
|
81
|
+
exitCode: 1,
|
|
82
|
+
nextSteps: []
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { runFeedback };
|