@prisma/cli 3.0.0-dev.110.1 → 3.0.0-dev.114.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.
- package/dist/adapters/mock-api.js +63 -0
- package/dist/commands/database/index.js +92 -2
- package/dist/controllers/app-env-api.js +2 -1
- package/dist/controllers/database.js +249 -6
- package/dist/lib/database/provider.js +148 -1
- package/dist/presenters/database.js +175 -1
- package/dist/shell/command-meta.js +52 -0
- package/package.json +1 -1
|
@@ -131,6 +131,69 @@ var MockApi = class MockApi {
|
|
|
131
131
|
this.data.databaseConnections = this.data.databaseConnections.filter((candidate) => candidate.id !== connectionId);
|
|
132
132
|
return connection;
|
|
133
133
|
}
|
|
134
|
+
getDatabaseUsage(databaseId, period) {
|
|
135
|
+
const defaults = (this.data.databaseUsage ?? []).find((record) => record.databaseId === databaseId) ?? {
|
|
136
|
+
databaseId,
|
|
137
|
+
period: {
|
|
138
|
+
start: "2026-06-01T00:00:00.000Z",
|
|
139
|
+
end: "2026-06-30T23:59:59.999Z"
|
|
140
|
+
},
|
|
141
|
+
metrics: {
|
|
142
|
+
operations: {
|
|
143
|
+
used: 0,
|
|
144
|
+
unit: "ops"
|
|
145
|
+
},
|
|
146
|
+
storage: {
|
|
147
|
+
used: 0,
|
|
148
|
+
unit: "GiB"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
generatedAt: "2026-07-01T00:00:00.000Z"
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
period: {
|
|
155
|
+
start: period?.from ?? defaults.period.start,
|
|
156
|
+
end: period?.to ?? defaults.period.end
|
|
157
|
+
},
|
|
158
|
+
metrics: defaults.metrics,
|
|
159
|
+
generatedAt: defaults.generatedAt
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
listDatabaseBackups(databaseId, limit) {
|
|
163
|
+
const backups = (this.data.databaseBackups ?? []).filter((backup) => backup.databaseId === databaseId);
|
|
164
|
+
const limited = limit === void 0 ? backups : backups.slice(0, limit);
|
|
165
|
+
return {
|
|
166
|
+
backups: limited.map((backup) => ({
|
|
167
|
+
id: backup.id,
|
|
168
|
+
backupType: backup.backupType,
|
|
169
|
+
status: backup.status,
|
|
170
|
+
size: backup.size ?? null,
|
|
171
|
+
createdAt: backup.createdAt
|
|
172
|
+
})),
|
|
173
|
+
retentionDays: this.data.databaseBackupRetentionDays ?? null,
|
|
174
|
+
hasMore: limited.length < backups.length
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
restoreDatabase(input) {
|
|
178
|
+
const target = this.getDatabase(input.targetDatabaseId);
|
|
179
|
+
if (!target) return { outcome: "target-not-found" };
|
|
180
|
+
if (!(this.data.databaseBackups ?? []).find((candidate) => candidate.id === input.backupId && candidate.databaseId === input.sourceDatabaseId)) return { outcome: "backup-not-found" };
|
|
181
|
+
target.status = "recovering";
|
|
182
|
+
return {
|
|
183
|
+
outcome: "restored",
|
|
184
|
+
database: target
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
rotateDatabaseConnection(connectionId) {
|
|
188
|
+
const connection = this.getDatabaseConnection(connectionId);
|
|
189
|
+
if (!connection) return;
|
|
190
|
+
const connectionString = `postgresql://rotated-${connection.databaseId}-${connection.id}.example.prisma.io/postgres`;
|
|
191
|
+
connection.connectionString = connectionString;
|
|
192
|
+
return {
|
|
193
|
+
connection,
|
|
194
|
+
connectionString
|
|
195
|
+
};
|
|
196
|
+
}
|
|
134
197
|
};
|
|
135
198
|
//#endregion
|
|
136
199
|
export { MockApi };
|
|
@@ -2,8 +2,8 @@ import { attachCommandDescriptor } from "../../shell/command-meta.js";
|
|
|
2
2
|
import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
|
|
3
3
|
import { configureRuntimeCommand } from "../../shell/runtime.js";
|
|
4
4
|
import { runCommand } from "../../shell/command-runner.js";
|
|
5
|
-
import { runDatabaseConnectionCreate, runDatabaseConnectionList, runDatabaseConnectionRemove, runDatabaseCreate, runDatabaseList, runDatabaseRemove, runDatabaseShow } from "../../controllers/database.js";
|
|
6
|
-
import { renderDatabaseConnectionCreate, renderDatabaseConnectionCreateStdout, renderDatabaseConnectionList, renderDatabaseConnectionRemove, renderDatabaseCreate, renderDatabaseCreateStdout, renderDatabaseList, renderDatabaseRemove, renderDatabaseShow, serializeDatabaseConnectionCreate, serializeDatabaseConnectionList, serializeDatabaseConnectionRemove, serializeDatabaseCreate, serializeDatabaseList, serializeDatabaseRemove, serializeDatabaseShow } from "../../presenters/database.js";
|
|
5
|
+
import { runDatabaseBackupList, runDatabaseConnectionCreate, runDatabaseConnectionList, runDatabaseConnectionRemove, runDatabaseConnectionRotate, runDatabaseCreate, runDatabaseList, runDatabaseRemove, runDatabaseRestore, runDatabaseShow, runDatabaseUsage } from "../../controllers/database.js";
|
|
6
|
+
import { renderDatabaseBackupList, renderDatabaseConnectionCreate, renderDatabaseConnectionCreateStdout, renderDatabaseConnectionList, renderDatabaseConnectionRemove, renderDatabaseConnectionRotate, renderDatabaseConnectionRotateStdout, renderDatabaseCreate, renderDatabaseCreateStdout, renderDatabaseList, renderDatabaseRemove, renderDatabaseRestore, renderDatabaseShow, renderDatabaseUsage, serializeDatabaseBackupList, serializeDatabaseConnectionCreate, serializeDatabaseConnectionList, serializeDatabaseConnectionRemove, serializeDatabaseConnectionRotate, serializeDatabaseCreate, serializeDatabaseList, serializeDatabaseRemove, serializeDatabaseRestore, serializeDatabaseShow, serializeDatabaseUsage } from "../../presenters/database.js";
|
|
7
7
|
import { Command, Option } from "commander";
|
|
8
8
|
//#region src/commands/database/index.ts
|
|
9
9
|
function createDatabaseCommand(runtime) {
|
|
@@ -12,7 +12,10 @@ function createDatabaseCommand(runtime) {
|
|
|
12
12
|
database.addCommand(createDatabaseListCommand(runtime));
|
|
13
13
|
database.addCommand(createDatabaseShowCommand(runtime));
|
|
14
14
|
database.addCommand(createDatabaseCreateCommand(runtime));
|
|
15
|
+
database.addCommand(createDatabaseUsageCommand(runtime));
|
|
16
|
+
database.addCommand(createDatabaseRestoreCommand(runtime));
|
|
15
17
|
database.addCommand(createDatabaseRemoveCommand(runtime));
|
|
18
|
+
database.addCommand(createDatabaseBackupCommand(runtime));
|
|
16
19
|
database.addCommand(createDatabaseConnectionCommand(runtime));
|
|
17
20
|
return database;
|
|
18
21
|
}
|
|
@@ -75,6 +78,78 @@ function createDatabaseCreateCommand(runtime) {
|
|
|
75
78
|
});
|
|
76
79
|
return command;
|
|
77
80
|
}
|
|
81
|
+
function createDatabaseUsageCommand(runtime) {
|
|
82
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("usage"), runtime), "database.usage");
|
|
83
|
+
command.argument("<database>", "Database id or name").addOption(new Option("--from <iso-date>", "Start of the usage period")).addOption(new Option("--to <iso-date>", "End of the usage period"));
|
|
84
|
+
addProjectAndBranchOptions(command);
|
|
85
|
+
addGlobalFlags(command);
|
|
86
|
+
command.action(async (databaseRef, options) => {
|
|
87
|
+
const projectRef = options.project;
|
|
88
|
+
const branchName = options.branch;
|
|
89
|
+
const from = options.from;
|
|
90
|
+
const to = options.to;
|
|
91
|
+
await runCommand(runtime, "database.usage", options, (context) => runDatabaseUsage(context, databaseRef, {
|
|
92
|
+
projectRef,
|
|
93
|
+
branchName,
|
|
94
|
+
from,
|
|
95
|
+
to
|
|
96
|
+
}), {
|
|
97
|
+
renderHuman: (context, descriptor, result) => renderDatabaseUsage(context, descriptor, result),
|
|
98
|
+
renderJson: (result) => serializeDatabaseUsage(result)
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
return command;
|
|
102
|
+
}
|
|
103
|
+
function createDatabaseRestoreCommand(runtime) {
|
|
104
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("restore"), runtime), "database.restore");
|
|
105
|
+
command.argument("<database>", "Target database id or name").addOption(new Option("--backup <backup-id>", "Backup to restore from")).addOption(new Option("--source-database <database>", "Database the backup belongs to (defaults to the target)")).addOption(new Option("--confirm <database-id>", "Exact target database id required to restore"));
|
|
106
|
+
addProjectAndBranchOptions(command);
|
|
107
|
+
addGlobalFlags(command);
|
|
108
|
+
command.action(async (databaseRef, options) => {
|
|
109
|
+
const projectRef = options.project;
|
|
110
|
+
const branchName = options.branch;
|
|
111
|
+
const backupId = options.backup;
|
|
112
|
+
const sourceDatabaseRef = options.sourceDatabase;
|
|
113
|
+
const confirm = options.confirm;
|
|
114
|
+
await runCommand(runtime, "database.restore", options, (context) => runDatabaseRestore(context, databaseRef, {
|
|
115
|
+
projectRef,
|
|
116
|
+
branchName,
|
|
117
|
+
backupId,
|
|
118
|
+
sourceDatabaseRef,
|
|
119
|
+
confirm
|
|
120
|
+
}), {
|
|
121
|
+
renderHuman: (context, descriptor, result) => renderDatabaseRestore(context, descriptor, result),
|
|
122
|
+
renderJson: (result) => serializeDatabaseRestore(result)
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
return command;
|
|
126
|
+
}
|
|
127
|
+
function createDatabaseBackupCommand(runtime) {
|
|
128
|
+
const backup = attachCommandDescriptor(configureRuntimeCommand(new Command("backup"), runtime), "database.backup");
|
|
129
|
+
addCompactGlobalFlags(backup);
|
|
130
|
+
backup.addCommand(createDatabaseBackupListCommand(runtime));
|
|
131
|
+
return backup;
|
|
132
|
+
}
|
|
133
|
+
function createDatabaseBackupListCommand(runtime) {
|
|
134
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "database.backup.list");
|
|
135
|
+
command.argument("<database>", "Database id or name").addOption(new Option("--limit <n>", "Maximum number of backups to return"));
|
|
136
|
+
addProjectAndBranchOptions(command);
|
|
137
|
+
addGlobalFlags(command);
|
|
138
|
+
command.action(async (databaseRef, options) => {
|
|
139
|
+
const projectRef = options.project;
|
|
140
|
+
const branchName = options.branch;
|
|
141
|
+
const limit = options.limit;
|
|
142
|
+
await runCommand(runtime, "database.backup.list", options, (context) => runDatabaseBackupList(context, databaseRef, {
|
|
143
|
+
projectRef,
|
|
144
|
+
branchName,
|
|
145
|
+
limit
|
|
146
|
+
}), {
|
|
147
|
+
renderHuman: (context, descriptor, result) => renderDatabaseBackupList(context, descriptor, result),
|
|
148
|
+
renderJson: (result) => serializeDatabaseBackupList(result)
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
return command;
|
|
152
|
+
}
|
|
78
153
|
function createDatabaseRemoveCommand(runtime) {
|
|
79
154
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("remove"), runtime), "database.remove");
|
|
80
155
|
command.argument("<database>", "Database id or name").addOption(new Option("--confirm <database-id>", "Exact database id required to remove"));
|
|
@@ -100,9 +175,24 @@ function createDatabaseConnectionCommand(runtime) {
|
|
|
100
175
|
addCompactGlobalFlags(connection);
|
|
101
176
|
connection.addCommand(createDatabaseConnectionListCommand(runtime));
|
|
102
177
|
connection.addCommand(createDatabaseConnectionCreateCommand(runtime));
|
|
178
|
+
connection.addCommand(createDatabaseConnectionRotateCommand(runtime));
|
|
103
179
|
connection.addCommand(createDatabaseConnectionRemoveCommand(runtime));
|
|
104
180
|
return connection;
|
|
105
181
|
}
|
|
182
|
+
function createDatabaseConnectionRotateCommand(runtime) {
|
|
183
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("rotate"), runtime), "database.connection.rotate");
|
|
184
|
+
command.argument("<connection>", "Connection id").addOption(new Option("--confirm <connection-id>", "Exact connection id required to rotate"));
|
|
185
|
+
addGlobalFlags(command);
|
|
186
|
+
command.action(async (connectionRef, options) => {
|
|
187
|
+
const confirm = options.confirm;
|
|
188
|
+
await runCommand(runtime, "database.connection.rotate", options, (context) => runDatabaseConnectionRotate(context, connectionRef, { confirm }), {
|
|
189
|
+
renderStdout: (context, descriptor, result) => renderDatabaseConnectionRotateStdout(context, descriptor, result),
|
|
190
|
+
renderHuman: (context, descriptor, result) => renderDatabaseConnectionRotate(context, descriptor, result),
|
|
191
|
+
renderJson: (result) => serializeDatabaseConnectionRotate(result)
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
return command;
|
|
195
|
+
}
|
|
106
196
|
function createDatabaseConnectionListCommand(runtime) {
|
|
107
197
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "database.connection.list");
|
|
108
198
|
command.argument("<database>", "Database id or name");
|
|
@@ -5,7 +5,8 @@ async function findVariableByNaturalKey(client, projectId, key, resolved, signal
|
|
|
5
5
|
params: { query: {
|
|
6
6
|
projectId,
|
|
7
7
|
class: resolved.apiTarget.class,
|
|
8
|
-
key
|
|
8
|
+
key,
|
|
9
|
+
...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {}
|
|
9
10
|
} },
|
|
10
11
|
signal
|
|
11
12
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvePrismaCliPackageCommandFormatterSync } from "../lib/agent/cli-command.js";
|
|
1
2
|
import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
|
|
2
3
|
import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
|
|
3
4
|
import { requireComputeAuth } from "../lib/auth/guard.js";
|
|
@@ -149,6 +150,197 @@ async function runDatabaseConnectionRemove(context, connectionRef, flags) {
|
|
|
149
150
|
nextSteps: []
|
|
150
151
|
};
|
|
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
|
+
}
|
|
152
344
|
async function requireDatabaseContext(context, flags, commandName) {
|
|
153
345
|
const workspace = (await requireAuthenticatedAuthState(context)).workspace;
|
|
154
346
|
if (!workspace) throw workspaceRequiredError();
|
|
@@ -164,7 +356,7 @@ async function requireDatabaseContext(context, flags, commandName) {
|
|
|
164
356
|
});
|
|
165
357
|
if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
|
|
166
358
|
return {
|
|
167
|
-
provider: createManagementDatabaseProvider(client),
|
|
359
|
+
provider: createManagementDatabaseProvider(client, { formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd) }),
|
|
168
360
|
target: targetResult.value
|
|
169
361
|
};
|
|
170
362
|
}
|
|
@@ -186,7 +378,7 @@ async function requireDatabaseProviderOnly(context) {
|
|
|
186
378
|
if (isRealMode(context)) {
|
|
187
379
|
const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
|
|
188
380
|
if (!client) throw authRequiredError();
|
|
189
|
-
return createManagementDatabaseProvider(client);
|
|
381
|
+
return createManagementDatabaseProvider(client, { formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd) });
|
|
190
382
|
}
|
|
191
383
|
return createFixtureDatabaseProvider(context);
|
|
192
384
|
}
|
|
@@ -224,6 +416,40 @@ function createFixtureDatabaseProvider(context) {
|
|
|
224
416
|
},
|
|
225
417
|
async removeConnection(connectionId) {
|
|
226
418
|
if (!context.api.removeDatabaseConnection(connectionId)) throw connectionNotFoundError(connectionId);
|
|
419
|
+
},
|
|
420
|
+
async getUsage(databaseId, options) {
|
|
421
|
+
if (!context.api.getDatabase(databaseId)) throw databaseNotFoundError(databaseId);
|
|
422
|
+
return context.api.getDatabaseUsage(databaseId, {
|
|
423
|
+
from: options?.from,
|
|
424
|
+
to: options?.to
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
async listBackups(databaseId, options) {
|
|
428
|
+
if (!context.api.getDatabase(databaseId)) throw databaseNotFoundError(databaseId);
|
|
429
|
+
return context.api.listDatabaseBackups(databaseId, options?.limit);
|
|
430
|
+
},
|
|
431
|
+
async restoreDatabase(options) {
|
|
432
|
+
const restored = context.api.restoreDatabase({
|
|
433
|
+
targetDatabaseId: options.targetDatabaseId,
|
|
434
|
+
sourceDatabaseId: options.sourceDatabaseId,
|
|
435
|
+
backupId: options.backupId
|
|
436
|
+
});
|
|
437
|
+
if (restored.outcome === "target-not-found") throw databaseNotFoundError(options.targetDatabaseId);
|
|
438
|
+
if (restored.outcome === "backup-not-found") throw backupNotFoundError(options.backupId, options.sourceDatabaseId, resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd));
|
|
439
|
+
return normalizeDatabase(restored.database, options.projectId);
|
|
440
|
+
},
|
|
441
|
+
async rotateConnection(connectionId) {
|
|
442
|
+
const rotated = context.api.rotateDatabaseConnection(connectionId);
|
|
443
|
+
if (!rotated) throw connectionNotFoundError(connectionId);
|
|
444
|
+
const database = context.api.getDatabase(rotated.connection.databaseId);
|
|
445
|
+
return {
|
|
446
|
+
connection: normalizeConnection(rotated.connection, rotated.connection.databaseId),
|
|
447
|
+
database: database ? {
|
|
448
|
+
id: database.id,
|
|
449
|
+
name: database.name
|
|
450
|
+
} : null,
|
|
451
|
+
connectionString: rotated.connectionString
|
|
452
|
+
};
|
|
227
453
|
}
|
|
228
454
|
};
|
|
229
455
|
}
|
|
@@ -262,11 +488,11 @@ function requireExactConfirmation(options) {
|
|
|
262
488
|
throw new CliError({
|
|
263
489
|
code: "CONFIRMATION_REQUIRED",
|
|
264
490
|
domain: "database",
|
|
265
|
-
summary: `Confirm ${options.resourceName} removal`,
|
|
266
|
-
why: `Removing this ${options.resourceName} is destructive and requires the exact id.`,
|
|
491
|
+
summary: options.summary ?? `Confirm ${options.resourceName} removal`,
|
|
492
|
+
why: options.why ?? `Removing this ${options.resourceName} is destructive and requires the exact id.`,
|
|
267
493
|
fix: `Rerun with --confirm ${options.id}.`,
|
|
268
494
|
exitCode: 2,
|
|
269
|
-
nextSteps: [`prisma-cli ${options.commandName} ${options.id} --confirm ${options.id}`],
|
|
495
|
+
nextSteps: [options.nextStep ?? `prisma-cli ${options.commandName} ${options.id} --confirm ${options.id}`],
|
|
270
496
|
meta: {
|
|
271
497
|
expectedConfirm: options.id,
|
|
272
498
|
receivedConfirm: options.confirm ?? null
|
|
@@ -303,6 +529,23 @@ function databaseAmbiguousError(databaseRef, matches, branchName) {
|
|
|
303
529
|
})) }
|
|
304
530
|
});
|
|
305
531
|
}
|
|
532
|
+
function backupNotFoundError(backupId, sourceDatabaseId, formatCommand) {
|
|
533
|
+
const listCommand = formatCommand([
|
|
534
|
+
"database",
|
|
535
|
+
"backup",
|
|
536
|
+
"list",
|
|
537
|
+
sourceDatabaseId
|
|
538
|
+
]);
|
|
539
|
+
return new CliError({
|
|
540
|
+
code: "DATABASE_BACKUP_NOT_FOUND",
|
|
541
|
+
domain: "database",
|
|
542
|
+
summary: "Database backup not found",
|
|
543
|
+
why: `No backup matched "${backupId}" for database "${sourceDatabaseId}".`,
|
|
544
|
+
fix: `Pass a backup id from ${listCommand}.`,
|
|
545
|
+
exitCode: 1,
|
|
546
|
+
nextSteps: [listCommand]
|
|
547
|
+
});
|
|
548
|
+
}
|
|
306
549
|
function connectionNotFoundError(connectionId) {
|
|
307
550
|
return new CliError({
|
|
308
551
|
code: "DATABASE_CONNECTION_NOT_FOUND",
|
|
@@ -315,4 +558,4 @@ function connectionNotFoundError(connectionId) {
|
|
|
315
558
|
});
|
|
316
559
|
}
|
|
317
560
|
//#endregion
|
|
318
|
-
export { runDatabaseConnectionCreate, runDatabaseConnectionList, runDatabaseConnectionRemove, runDatabaseCreate, runDatabaseList, runDatabaseRemove, runDatabaseShow };
|
|
561
|
+
export { runDatabaseBackupList, runDatabaseConnectionCreate, runDatabaseConnectionList, runDatabaseConnectionRemove, runDatabaseConnectionRotate, runDatabaseCreate, runDatabaseList, runDatabaseRemove, runDatabaseRestore, runDatabaseShow, runDatabaseUsage };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { formatPrismaCliCommand } from "../../shell/cli-command.js";
|
|
1
2
|
import { CliError } from "../../shell/errors.js";
|
|
2
3
|
//#region src/lib/database/provider.ts
|
|
3
|
-
function createManagementDatabaseProvider(client) {
|
|
4
|
+
function createManagementDatabaseProvider(client, options) {
|
|
5
|
+
const formatCommand = options?.formatCommand ?? ((args) => formatPrismaCliCommand(args));
|
|
4
6
|
return {
|
|
5
7
|
async listDatabases(options) {
|
|
6
8
|
const databases = [];
|
|
@@ -75,6 +77,55 @@ function createManagementDatabaseProvider(client) {
|
|
|
75
77
|
signal: options?.signal
|
|
76
78
|
});
|
|
77
79
|
if (result.error) throw databaseApiError("Failed to remove database connection", result.response, result.error);
|
|
80
|
+
},
|
|
81
|
+
async getUsage(databaseId, options) {
|
|
82
|
+
const result = await client.GET("/v1/databases/{databaseId}/usage", {
|
|
83
|
+
params: {
|
|
84
|
+
path: { databaseId },
|
|
85
|
+
query: {
|
|
86
|
+
...options?.from ? { startDate: options.from } : {},
|
|
87
|
+
...options?.to ? { endDate: options.to } : {}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
signal: options?.signal
|
|
91
|
+
});
|
|
92
|
+
if (result.error || !result.data) throw databaseApiError("Failed to fetch database usage", result.response, result.error);
|
|
93
|
+
return normalizeUsage(result.data);
|
|
94
|
+
},
|
|
95
|
+
async listBackups(databaseId, options) {
|
|
96
|
+
const result = await client.GET("/v1/databases/{databaseId}/backups", {
|
|
97
|
+
params: {
|
|
98
|
+
path: { databaseId },
|
|
99
|
+
query: { ...options?.limit !== void 0 ? { limit: options.limit } : {} }
|
|
100
|
+
},
|
|
101
|
+
signal: options?.signal
|
|
102
|
+
});
|
|
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);
|
|
105
|
+
return normalizeBackupList(result.data);
|
|
106
|
+
},
|
|
107
|
+
async restoreDatabase(options) {
|
|
108
|
+
const result = await client.POST("/v1/databases/{targetDatabaseId}/restore", {
|
|
109
|
+
params: { path: { targetDatabaseId: options.targetDatabaseId } },
|
|
110
|
+
body: { source: {
|
|
111
|
+
type: "backup",
|
|
112
|
+
databaseId: options.sourceDatabaseId,
|
|
113
|
+
backupId: options.backupId
|
|
114
|
+
} },
|
|
115
|
+
signal: options.signal
|
|
116
|
+
});
|
|
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);
|
|
120
|
+
return normalizeDatabase(result.data.data, options.projectId);
|
|
121
|
+
},
|
|
122
|
+
async rotateConnection(connectionId, options) {
|
|
123
|
+
const result = await client.POST("/v1/connections/{id}/rotate", {
|
|
124
|
+
params: { path: { id: connectionId } },
|
|
125
|
+
signal: options?.signal
|
|
126
|
+
});
|
|
127
|
+
if (result.error || !result.data) throw databaseApiError("Failed to rotate database connection", result.response, result.error);
|
|
128
|
+
return normalizeRotatedConnection(result.data.data);
|
|
78
129
|
}
|
|
79
130
|
};
|
|
80
131
|
}
|
|
@@ -151,6 +202,102 @@ function requireDatabaseProjectId(database, fallbackProjectId) {
|
|
|
151
202
|
function extractConnectionString(connection) {
|
|
152
203
|
return connection.endpoints?.pooled?.connectionString ?? connection.connectionString ?? connection.endpoints?.direct?.connectionString ?? connection.endpoints?.accelerate?.connectionString ?? null;
|
|
153
204
|
}
|
|
205
|
+
function normalizeUsage(usage) {
|
|
206
|
+
return {
|
|
207
|
+
period: {
|
|
208
|
+
start: usage.period?.start ?? "",
|
|
209
|
+
end: usage.period?.end ?? ""
|
|
210
|
+
},
|
|
211
|
+
metrics: {
|
|
212
|
+
operations: {
|
|
213
|
+
used: usage.metrics?.operations?.used ?? 0,
|
|
214
|
+
unit: usage.metrics?.operations?.unit ?? "ops"
|
|
215
|
+
},
|
|
216
|
+
storage: {
|
|
217
|
+
used: usage.metrics?.storage?.used ?? 0,
|
|
218
|
+
unit: usage.metrics?.storage?.unit ?? "GiB"
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
generatedAt: usage.generatedAt ?? ""
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function normalizeBackupList(body) {
|
|
225
|
+
return {
|
|
226
|
+
backups: (body.data ?? []).map((backup) => ({
|
|
227
|
+
id: backup.id,
|
|
228
|
+
backupType: backup.backupType ?? "unknown",
|
|
229
|
+
status: backup.status ?? "unknown",
|
|
230
|
+
size: backup.size ?? null,
|
|
231
|
+
createdAt: backup.createdAt ?? ""
|
|
232
|
+
})),
|
|
233
|
+
retentionDays: body.meta?.backupRetentionDays ?? null,
|
|
234
|
+
hasMore: body.pagination?.hasMore ?? false
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function normalizeRotatedConnection(connection) {
|
|
238
|
+
const connectionString = extractConnectionString(connection);
|
|
239
|
+
if (!connectionString) throw new CliError({
|
|
240
|
+
code: "DATABASE_CONNECTION_STRING_MISSING",
|
|
241
|
+
domain: "database",
|
|
242
|
+
summary: "Rotated connection did not return a connection string",
|
|
243
|
+
why: "Rotated connection strings are one-time-view secrets, but the Management API did not include one in this rotate response.",
|
|
244
|
+
fix: "Re-run the rotation, or create a replacement connection and store the returned URL immediately.",
|
|
245
|
+
exitCode: 1,
|
|
246
|
+
nextSteps: []
|
|
247
|
+
});
|
|
248
|
+
const database = connection.database?.id && connection.database?.name ? {
|
|
249
|
+
id: connection.database.id,
|
|
250
|
+
name: connection.database.name
|
|
251
|
+
} : null;
|
|
252
|
+
return {
|
|
253
|
+
connection: normalizeConnection(connection, connection.database?.id ?? connection.databaseId ?? ""),
|
|
254
|
+
database,
|
|
255
|
+
connectionString
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function backupsUnsupportedError(databaseId, error) {
|
|
259
|
+
return new CliError({
|
|
260
|
+
code: "DATABASE_BACKUPS_UNSUPPORTED",
|
|
261
|
+
domain: "database",
|
|
262
|
+
summary: "Backups are not available for this database",
|
|
263
|
+
why: error?.error?.message ?? `The platform does not manage backups for database "${databaseId}", for example because it is a remote/BYO database.`,
|
|
264
|
+
fix: "Use your own backup tooling for externally managed databases.",
|
|
265
|
+
exitCode: 1,
|
|
266
|
+
nextSteps: []
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function restoreBackupNotFoundError(options, error, formatCommand) {
|
|
270
|
+
const listCommand = formatCommand([
|
|
271
|
+
"database",
|
|
272
|
+
"backup",
|
|
273
|
+
"list",
|
|
274
|
+
options.sourceDatabaseId
|
|
275
|
+
]);
|
|
276
|
+
return new CliError({
|
|
277
|
+
code: "DATABASE_BACKUP_NOT_FOUND",
|
|
278
|
+
domain: "database",
|
|
279
|
+
summary: "Database backup not found",
|
|
280
|
+
why: error?.error?.message ?? `No backup matched "${options.backupId}" for database "${options.sourceDatabaseId}".`,
|
|
281
|
+
fix: `Pass a backup id from ${listCommand}.`,
|
|
282
|
+
exitCode: 1,
|
|
283
|
+
nextSteps: [listCommand]
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function restoreConflictError(targetDatabaseId, error, formatCommand) {
|
|
287
|
+
return new CliError({
|
|
288
|
+
code: "DATABASE_RESTORE_CONFLICT",
|
|
289
|
+
domain: "database",
|
|
290
|
+
summary: "Database cannot be restored right now",
|
|
291
|
+
why: error?.error?.message ?? `Database "${targetDatabaseId}" is provisioning or already recovering.`,
|
|
292
|
+
fix: "Wait for the database to become ready, then retry the restore.",
|
|
293
|
+
exitCode: 1,
|
|
294
|
+
nextSteps: [formatCommand([
|
|
295
|
+
"database",
|
|
296
|
+
"show",
|
|
297
|
+
targetDatabaseId
|
|
298
|
+
])]
|
|
299
|
+
});
|
|
300
|
+
}
|
|
154
301
|
function databaseApiError(summary, response, error) {
|
|
155
302
|
const status = response?.status ?? 0;
|
|
156
303
|
return new CliError({
|
|
@@ -236,6 +236,180 @@ function renderDatabaseConnectionRemove(context, descriptor, result) {
|
|
|
236
236
|
function serializeDatabaseConnectionRemove(result) {
|
|
237
237
|
return { connection: result.connection };
|
|
238
238
|
}
|
|
239
|
+
function renderDatabaseUsage(context, descriptor, result) {
|
|
240
|
+
const lines = renderShow({
|
|
241
|
+
title: "Showing database usage metrics.",
|
|
242
|
+
descriptor,
|
|
243
|
+
fields: [
|
|
244
|
+
{
|
|
245
|
+
key: "project",
|
|
246
|
+
value: result.projectName
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
key: "database",
|
|
250
|
+
value: result.database.name
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
key: "id",
|
|
254
|
+
value: result.database.id,
|
|
255
|
+
tone: "dim"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: "period",
|
|
259
|
+
value: `${formatUsageTimestamp(result.period.start)} to ${formatUsageTimestamp(result.period.end)}`
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
key: "operations",
|
|
263
|
+
value: `${result.metrics.operations.used} ${result.metrics.operations.unit}`
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
key: "storage",
|
|
267
|
+
value: `${result.metrics.storage.used} ${result.metrics.storage.unit}`
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
key: "generated",
|
|
271
|
+
value: formatUsageTimestamp(result.generatedAt),
|
|
272
|
+
tone: "dim"
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
}, context.ui);
|
|
276
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
277
|
+
return lines;
|
|
278
|
+
}
|
|
279
|
+
function serializeDatabaseUsage(result) {
|
|
280
|
+
return stripVerboseContext(result);
|
|
281
|
+
}
|
|
282
|
+
function renderDatabaseBackupList(context, descriptor, result) {
|
|
283
|
+
const ui = context.ui;
|
|
284
|
+
const lines = [`${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim("Listing platform-created database backups.")}`, ""];
|
|
285
|
+
const rail = ui.dim("│");
|
|
286
|
+
lines.push(`${rail} ${ui.accent("database:")} ${result.database.name}`);
|
|
287
|
+
if (result.retentionDays !== null) lines.push(`${rail} ${ui.accent("retention:")} ${result.retentionDays} days`);
|
|
288
|
+
lines.push(rail);
|
|
289
|
+
if (result.backups.length === 0) {
|
|
290
|
+
lines.push(`${rail} ${ui.dim("No backups found.")}`);
|
|
291
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
292
|
+
return lines;
|
|
293
|
+
}
|
|
294
|
+
const rows = result.backups.map((backup) => [
|
|
295
|
+
backup.id,
|
|
296
|
+
backup.backupType,
|
|
297
|
+
backup.status,
|
|
298
|
+
formatBackupSize(backup.size),
|
|
299
|
+
backup.createdAt
|
|
300
|
+
]);
|
|
301
|
+
const widths = [
|
|
302
|
+
Math.max(2, ...rows.map((row) => row[0].length)),
|
|
303
|
+
Math.max(4, ...rows.map((row) => row[1].length)),
|
|
304
|
+
Math.max(6, ...rows.map((row) => row[2].length)),
|
|
305
|
+
Math.max(4, ...rows.map((row) => row[3].length)),
|
|
306
|
+
Math.max(7, ...rows.map((row) => row[4].length))
|
|
307
|
+
];
|
|
308
|
+
lines.push(`${rail} ${ui.accent(formatColumns([
|
|
309
|
+
"Id",
|
|
310
|
+
"Type",
|
|
311
|
+
"Status",
|
|
312
|
+
"Size",
|
|
313
|
+
"Created"
|
|
314
|
+
], widths))}`);
|
|
315
|
+
for (const row of rows) lines.push(`${rail} ${formatColumns(row, widths)}`);
|
|
316
|
+
if (result.hasMore) {
|
|
317
|
+
lines.push(rail);
|
|
318
|
+
lines.push(`${rail} ${ui.dim("More backups exist; raise --limit to see them.")}`);
|
|
319
|
+
}
|
|
320
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
321
|
+
return lines;
|
|
322
|
+
}
|
|
323
|
+
function serializeDatabaseBackupList(result) {
|
|
324
|
+
return {
|
|
325
|
+
...serializeList({
|
|
326
|
+
context: {
|
|
327
|
+
project: result.projectName,
|
|
328
|
+
database: result.database.name
|
|
329
|
+
},
|
|
330
|
+
items: result.backups.map((backup) => ({
|
|
331
|
+
noun: "backup",
|
|
332
|
+
label: backup.id,
|
|
333
|
+
id: backup.id,
|
|
334
|
+
status: null
|
|
335
|
+
}))
|
|
336
|
+
}),
|
|
337
|
+
projectId: result.projectId,
|
|
338
|
+
database: result.database,
|
|
339
|
+
backups: result.backups,
|
|
340
|
+
retentionDays: result.retentionDays,
|
|
341
|
+
hasMore: result.hasMore
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function renderDatabaseRestore(context, descriptor, result) {
|
|
345
|
+
const lines = renderMutate({
|
|
346
|
+
title: "Restoring database from backup.",
|
|
347
|
+
descriptor,
|
|
348
|
+
context: [
|
|
349
|
+
{
|
|
350
|
+
key: "project",
|
|
351
|
+
value: result.projectName
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
key: "database",
|
|
355
|
+
value: result.database.name
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
key: "id",
|
|
359
|
+
value: result.database.id,
|
|
360
|
+
tone: "dim"
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
key: "backup",
|
|
364
|
+
value: result.source.backupId
|
|
365
|
+
},
|
|
366
|
+
...result.source.databaseId !== result.database.id ? [{
|
|
367
|
+
key: "source",
|
|
368
|
+
value: result.source.databaseId
|
|
369
|
+
}] : []
|
|
370
|
+
],
|
|
371
|
+
operationDescription: "Restoring database",
|
|
372
|
+
operationCount: 1,
|
|
373
|
+
details: [`The restore is running; the database status is "${result.database.status ?? "recovering"}" until it completes.`, "Connections and credentials are preserved."]
|
|
374
|
+
}, context.ui);
|
|
375
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
376
|
+
return lines;
|
|
377
|
+
}
|
|
378
|
+
function serializeDatabaseRestore(result) {
|
|
379
|
+
return stripVerboseContext(result);
|
|
380
|
+
}
|
|
381
|
+
function renderDatabaseConnectionRotateStdout(_context, _descriptor, result) {
|
|
382
|
+
return [result.connectionString];
|
|
383
|
+
}
|
|
384
|
+
function renderDatabaseConnectionRotate(context, _descriptor, result) {
|
|
385
|
+
const ui = context.ui;
|
|
386
|
+
const lines = [
|
|
387
|
+
"Rotating connection...",
|
|
388
|
+
renderSummaryLine(ui, "success", `Rotated credentials for ${result.database ? `"${result.database.name}"` : `connection ${result.connection.id}`}. The previous credentials no longer work.`),
|
|
389
|
+
" The connection URL below is shown once, so save it now."
|
|
390
|
+
];
|
|
391
|
+
if (ui.verbose) {
|
|
392
|
+
lines.push("");
|
|
393
|
+
lines.push(...renderDatabaseConnectionRotateVerboseRows(context, result));
|
|
394
|
+
}
|
|
395
|
+
return lines;
|
|
396
|
+
}
|
|
397
|
+
function serializeDatabaseConnectionRotate(result) {
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
function renderDatabaseConnectionRotateVerboseRows(context, result) {
|
|
401
|
+
return renderMetadataRows([...result.database ? [["database", formatResourceWithId(context, result.database.name, result.database.id)]] : [], ["connection", formatResourceWithId(context, result.connection.name, result.connection.id)]]);
|
|
402
|
+
}
|
|
403
|
+
function formatBackupSize(size) {
|
|
404
|
+
if (size === null) return "unknown";
|
|
405
|
+
if (size < 1024) return `${size} B`;
|
|
406
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KiB`;
|
|
407
|
+
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MiB`;
|
|
408
|
+
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
|
409
|
+
}
|
|
410
|
+
function formatUsageTimestamp(value) {
|
|
411
|
+
return value || "unknown";
|
|
412
|
+
}
|
|
239
413
|
function formatStatus(database) {
|
|
240
414
|
return database.status ?? (database.isDefault ? "default" : "unknown");
|
|
241
415
|
}
|
|
@@ -271,4 +445,4 @@ function renderMetadataRows(rows) {
|
|
|
271
445
|
return rows.map(([key, value]) => ` ${formatColumns([key, value], widths)}`);
|
|
272
446
|
}
|
|
273
447
|
//#endregion
|
|
274
|
-
export { renderDatabaseConnectionCreate, renderDatabaseConnectionCreateStdout, renderDatabaseConnectionList, renderDatabaseConnectionRemove, renderDatabaseCreate, renderDatabaseCreateStdout, renderDatabaseList, renderDatabaseRemove, renderDatabaseShow, serializeDatabaseConnectionCreate, serializeDatabaseConnectionList, serializeDatabaseConnectionRemove, serializeDatabaseCreate, serializeDatabaseList, serializeDatabaseRemove, serializeDatabaseShow };
|
|
448
|
+
export { renderDatabaseBackupList, renderDatabaseConnectionCreate, renderDatabaseConnectionCreateStdout, renderDatabaseConnectionList, renderDatabaseConnectionRemove, renderDatabaseConnectionRotate, renderDatabaseConnectionRotateStdout, renderDatabaseCreate, renderDatabaseCreateStdout, renderDatabaseList, renderDatabaseRemove, renderDatabaseRestore, renderDatabaseShow, renderDatabaseUsage, serializeDatabaseBackupList, serializeDatabaseConnectionCreate, serializeDatabaseConnectionList, serializeDatabaseConnectionRemove, serializeDatabaseConnectionRotate, serializeDatabaseCreate, serializeDatabaseList, serializeDatabaseRemove, serializeDatabaseRestore, serializeDatabaseShow, serializeDatabaseUsage };
|
|
@@ -339,6 +339,26 @@ const DESCRIPTORS = [
|
|
|
339
339
|
description: "Create a Prisma Postgres database and print its one-time connection URL",
|
|
340
340
|
examples: ["prisma-cli database create my-db", "prisma-cli database create my-db --branch feature/foo --region eu-central-1"]
|
|
341
341
|
},
|
|
342
|
+
{
|
|
343
|
+
id: "database.usage",
|
|
344
|
+
path: [
|
|
345
|
+
"prisma",
|
|
346
|
+
"database",
|
|
347
|
+
"usage"
|
|
348
|
+
],
|
|
349
|
+
description: "Show usage metrics for a database",
|
|
350
|
+
examples: ["prisma-cli database usage db_123", "prisma-cli database usage acme-production --from 2026-06-01 --to 2026-06-30"]
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: "database.restore",
|
|
354
|
+
path: [
|
|
355
|
+
"prisma",
|
|
356
|
+
"database",
|
|
357
|
+
"restore"
|
|
358
|
+
],
|
|
359
|
+
description: "Restore a database from a backup after exact id confirmation",
|
|
360
|
+
examples: ["prisma-cli database restore db_123 --backup bkp_456 --confirm db_123"]
|
|
361
|
+
},
|
|
342
362
|
{
|
|
343
363
|
id: "database.remove",
|
|
344
364
|
path: [
|
|
@@ -349,6 +369,27 @@ const DESCRIPTORS = [
|
|
|
349
369
|
description: "Remove a database after exact id confirmation",
|
|
350
370
|
examples: ["prisma-cli database remove db_123 --confirm db_123"]
|
|
351
371
|
},
|
|
372
|
+
{
|
|
373
|
+
id: "database.backup",
|
|
374
|
+
path: [
|
|
375
|
+
"prisma",
|
|
376
|
+
"database",
|
|
377
|
+
"backup"
|
|
378
|
+
],
|
|
379
|
+
description: "Inspect platform-created database backups",
|
|
380
|
+
examples: ["prisma-cli database backup list db_123"]
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: "database.backup.list",
|
|
384
|
+
path: [
|
|
385
|
+
"prisma",
|
|
386
|
+
"database",
|
|
387
|
+
"backup",
|
|
388
|
+
"list"
|
|
389
|
+
],
|
|
390
|
+
description: "List backups for a database",
|
|
391
|
+
examples: ["prisma-cli database backup list db_123", "prisma-cli database backup list acme-production --limit 50"]
|
|
392
|
+
},
|
|
352
393
|
{
|
|
353
394
|
id: "database.connection",
|
|
354
395
|
path: [
|
|
@@ -385,6 +426,17 @@ const DESCRIPTORS = [
|
|
|
385
426
|
description: "Create a database connection and print its one-time connection URL",
|
|
386
427
|
examples: ["prisma-cli database connection create db_123", "prisma-cli database connection create db_123 --name readonly"]
|
|
387
428
|
},
|
|
429
|
+
{
|
|
430
|
+
id: "database.connection.rotate",
|
|
431
|
+
path: [
|
|
432
|
+
"prisma",
|
|
433
|
+
"database",
|
|
434
|
+
"connection",
|
|
435
|
+
"rotate"
|
|
436
|
+
],
|
|
437
|
+
description: "Rotate connection credentials and print the new one-time connection URL",
|
|
438
|
+
examples: ["prisma-cli database connection rotate conn_123 --confirm conn_123"]
|
|
439
|
+
},
|
|
388
440
|
{
|
|
389
441
|
id: "database.connection.remove",
|
|
390
442
|
path: [
|