@prisma/cli 3.0.0-beta.7 → 3.0.0-beta.9
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 +75 -0
- package/dist/cli2.js +2 -0
- package/dist/commands/app/index.js +2 -2
- package/dist/commands/database/index.js +159 -0
- package/dist/controllers/app-env.js +2 -2
- package/dist/controllers/app.js +55 -8
- package/dist/controllers/database.js +316 -0
- package/dist/controllers/project.js +17 -3
- package/dist/lib/app/branch-database-deploy.js +116 -66
- package/dist/lib/app/env-file.js +2 -2
- package/dist/lib/app/env-vars.js +28 -2
- package/dist/lib/app/preview-build-settings.js +385 -0
- package/dist/lib/app/preview-build.js +196 -33
- package/dist/lib/app/preview-provider.js +2 -1
- package/dist/lib/app/production-deploy-gate.js +5 -4
- package/dist/lib/database/provider.js +167 -0
- package/dist/lib/project/local-pin.js +64 -14
- package/dist/lib/project/resolution.js +16 -2
- package/dist/presenters/app.js +7 -2
- package/dist/presenters/database.js +274 -0
- package/dist/shell/command-meta.js +102 -0
- package/dist/shell/command-runner.js +9 -2
- package/package.json +2 -1
|
@@ -165,7 +165,8 @@ function createPreviewAppProvider(client, options) {
|
|
|
165
165
|
appPath: path.resolve(options.cwd),
|
|
166
166
|
entrypoint: options.entrypoint,
|
|
167
167
|
buildType: options.buildType,
|
|
168
|
-
signal: options.signal
|
|
168
|
+
signal: options.signal,
|
|
169
|
+
buildSettings: options.buildSettings
|
|
169
170
|
}),
|
|
170
171
|
projectId: options.projectId,
|
|
171
172
|
serviceId: resolvedApp.appId,
|
|
@@ -3,22 +3,22 @@ import { canPrompt } from "../../shell/runtime.js";
|
|
|
3
3
|
import { confirmPrompt } from "../../shell/prompt.js";
|
|
4
4
|
//#region src/lib/app/production-deploy-gate.ts
|
|
5
5
|
async function enforceProductionDeployGate(context, provider, options) {
|
|
6
|
-
if (options.branchKind !== "production") return;
|
|
6
|
+
if (options.branchKind !== "production") return { firstProductionDeploy: false };
|
|
7
7
|
if (!options.appId) {
|
|
8
8
|
renderFirstProductionDeployLine(context, options.appName);
|
|
9
|
-
return;
|
|
9
|
+
return { firstProductionDeploy: true };
|
|
10
10
|
}
|
|
11
11
|
const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
|
|
12
12
|
throw productionDeployInspectionFailedError(error);
|
|
13
13
|
}));
|
|
14
14
|
if (!currentLiveDeployment) {
|
|
15
15
|
renderFirstProductionDeployLine(context, options.appName);
|
|
16
|
-
return;
|
|
16
|
+
return { firstProductionDeploy: true };
|
|
17
17
|
}
|
|
18
18
|
if (!options.prod) throw productionDeployRequiresFlagError();
|
|
19
19
|
if (context.flags.yes) {
|
|
20
20
|
renderProductionDeployYesLine(context);
|
|
21
|
-
return;
|
|
21
|
+
return { firstProductionDeploy: false };
|
|
22
22
|
}
|
|
23
23
|
if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
|
|
24
24
|
renderProductionDeployConfirmation(context, currentLiveDeployment);
|
|
@@ -28,6 +28,7 @@ async function enforceProductionDeployGate(context, provider, options) {
|
|
|
28
28
|
message: "Deploy to production?",
|
|
29
29
|
initialValue: false
|
|
30
30
|
})) throw productionDeployCancelledError();
|
|
31
|
+
return { firstProductionDeploy: false };
|
|
31
32
|
}
|
|
32
33
|
function resolveCurrentProductionDeployment(result) {
|
|
33
34
|
if (result.deployments.length === 0) return null;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { CliError } from "../../shell/errors.js";
|
|
2
|
+
//#region src/lib/database/provider.ts
|
|
3
|
+
function createManagementDatabaseProvider(client) {
|
|
4
|
+
return {
|
|
5
|
+
async listDatabases(options) {
|
|
6
|
+
const databases = [];
|
|
7
|
+
let cursor;
|
|
8
|
+
while (true) {
|
|
9
|
+
const result = await client.GET("/v1/databases", {
|
|
10
|
+
params: { query: {
|
|
11
|
+
projectId: options.projectId,
|
|
12
|
+
branchGitName: options.branchName,
|
|
13
|
+
cursor
|
|
14
|
+
} },
|
|
15
|
+
signal: options.signal
|
|
16
|
+
});
|
|
17
|
+
if (result.error || !result.data) throw databaseApiError("Failed to list databases", result.response, result.error);
|
|
18
|
+
databases.push(...result.data.data);
|
|
19
|
+
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
20
|
+
cursor = result.data.pagination.nextCursor;
|
|
21
|
+
}
|
|
22
|
+
return databases.map((database) => normalizeDatabase(database, options.projectId));
|
|
23
|
+
},
|
|
24
|
+
async showDatabase(databaseId, options) {
|
|
25
|
+
const result = await client.GET("/v1/databases/{databaseId}", {
|
|
26
|
+
params: { path: { databaseId } },
|
|
27
|
+
signal: options?.signal
|
|
28
|
+
});
|
|
29
|
+
if (result.response?.status === 404) return null;
|
|
30
|
+
if (result.error || !result.data) throw databaseApiError("Failed to show database", result.response, result.error);
|
|
31
|
+
const database = result.data.data;
|
|
32
|
+
return normalizeDatabase(database, requireDatabaseProjectId(database, options?.projectId));
|
|
33
|
+
},
|
|
34
|
+
async createDatabase(options) {
|
|
35
|
+
const result = await client.POST("/v1/databases", {
|
|
36
|
+
body: {
|
|
37
|
+
projectId: options.projectId,
|
|
38
|
+
name: options.name,
|
|
39
|
+
source: { type: "empty" },
|
|
40
|
+
...options.branchName ? { branchGitName: options.branchName } : {},
|
|
41
|
+
...options.region ? { region: options.region } : {}
|
|
42
|
+
},
|
|
43
|
+
signal: options.signal
|
|
44
|
+
});
|
|
45
|
+
if (result.error || !result.data) throw databaseApiError("Failed to create database", result.response, result.error);
|
|
46
|
+
return normalizeCreatedDatabase(result.data.data, options.projectId);
|
|
47
|
+
},
|
|
48
|
+
async removeDatabase(databaseId, options) {
|
|
49
|
+
const result = await client.DELETE("/v1/databases/{databaseId}", {
|
|
50
|
+
params: { path: { databaseId } },
|
|
51
|
+
signal: options?.signal
|
|
52
|
+
});
|
|
53
|
+
if (result.error) throw databaseApiError("Failed to remove database", result.response, result.error);
|
|
54
|
+
},
|
|
55
|
+
async listConnections(databaseId, options) {
|
|
56
|
+
const result = await client.GET("/v1/databases/{databaseId}/connections", {
|
|
57
|
+
params: { path: { databaseId } },
|
|
58
|
+
signal: options?.signal
|
|
59
|
+
});
|
|
60
|
+
if (result.error || !result.data) throw databaseApiError("Failed to list database connections", result.response, result.error);
|
|
61
|
+
return result.data.data.map((connection) => normalizeConnection(connection, databaseId));
|
|
62
|
+
},
|
|
63
|
+
async createConnection(options) {
|
|
64
|
+
const result = await client.POST("/v1/databases/{databaseId}/connections", {
|
|
65
|
+
params: { path: { databaseId: options.databaseId } },
|
|
66
|
+
body: { name: options.name },
|
|
67
|
+
signal: options.signal
|
|
68
|
+
});
|
|
69
|
+
if (result.error || !result.data) throw databaseApiError("Failed to create database connection", result.response, result.error);
|
|
70
|
+
return normalizeCreatedConnection(result.data.data, options.databaseId);
|
|
71
|
+
},
|
|
72
|
+
async removeConnection(connectionId, options) {
|
|
73
|
+
const result = await client.DELETE("/v1/connections/{id}", {
|
|
74
|
+
params: { path: { id: connectionId } },
|
|
75
|
+
signal: options?.signal
|
|
76
|
+
});
|
|
77
|
+
if (result.error) throw databaseApiError("Failed to remove database connection", result.response, result.error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function normalizeDatabase(database, fallbackProjectId) {
|
|
82
|
+
return {
|
|
83
|
+
id: database.id,
|
|
84
|
+
name: database.name,
|
|
85
|
+
projectId: database.projectId ?? fallbackProjectId,
|
|
86
|
+
branchId: database.branchId ?? database.branch?.id ?? null,
|
|
87
|
+
branchName: database.branchGitName ?? database.branchName ?? database.branch?.gitName ?? database.branch?.name ?? null,
|
|
88
|
+
region: normalizeRegion(database),
|
|
89
|
+
status: database.status ?? null,
|
|
90
|
+
isDefault: database.isDefault ?? null,
|
|
91
|
+
createdAt: database.createdAt ?? null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function normalizeConnection(connection, fallbackDatabaseId) {
|
|
95
|
+
return {
|
|
96
|
+
id: connection.id,
|
|
97
|
+
name: connection.name ?? connection.id,
|
|
98
|
+
databaseId: connection.databaseId ?? fallbackDatabaseId,
|
|
99
|
+
createdAt: connection.createdAt ?? null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeCreatedDatabase(database, fallbackProjectId) {
|
|
103
|
+
const rawConnection = database.connections?.[0];
|
|
104
|
+
if (!rawConnection) throw new CliError({
|
|
105
|
+
code: "DATABASE_CONNECTION_MISSING",
|
|
106
|
+
domain: "database",
|
|
107
|
+
summary: "Created database did not return a connection string",
|
|
108
|
+
why: "The Management API created the database but did not include the one-time connection payload.",
|
|
109
|
+
fix: "Create a connection explicitly with prisma-cli database connection create <database>.",
|
|
110
|
+
exitCode: 1,
|
|
111
|
+
nextSteps: [`prisma-cli database connection create ${database.id}`]
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
database: normalizeDatabase(database, fallbackProjectId),
|
|
115
|
+
...normalizeCreatedConnection(rawConnection, database.id)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function normalizeCreatedConnection(connection, fallbackDatabaseId) {
|
|
119
|
+
const connectionString = extractConnectionString(connection);
|
|
120
|
+
if (!connectionString) throw new CliError({
|
|
121
|
+
code: "DATABASE_CONNECTION_STRING_MISSING",
|
|
122
|
+
domain: "database",
|
|
123
|
+
summary: "Created connection did not return a connection string",
|
|
124
|
+
why: "Database connection strings are one-time-view secrets, but the Management API did not include one in this create response.",
|
|
125
|
+
fix: "Create another database connection and store the returned URL immediately.",
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
nextSteps: [`prisma-cli database connection create ${fallbackDatabaseId}`]
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
connection: normalizeConnection(connection, fallbackDatabaseId),
|
|
131
|
+
connectionString
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function normalizeRegion(database) {
|
|
135
|
+
if (typeof database.region === "string") return database.region;
|
|
136
|
+
return database.region?.id ?? database.regionId ?? null;
|
|
137
|
+
}
|
|
138
|
+
function requireDatabaseProjectId(database, fallbackProjectId) {
|
|
139
|
+
const projectId = database.projectId ?? fallbackProjectId;
|
|
140
|
+
if (projectId) return projectId;
|
|
141
|
+
throw new CliError({
|
|
142
|
+
code: "DATABASE_API_ERROR",
|
|
143
|
+
domain: "database",
|
|
144
|
+
summary: "Database response did not include a project id",
|
|
145
|
+
why: "The Management API returned database metadata without project context.",
|
|
146
|
+
fix: "Re-run with --trace for the underlying API response details.",
|
|
147
|
+
exitCode: 1,
|
|
148
|
+
nextSteps: []
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function extractConnectionString(connection) {
|
|
152
|
+
return connection.endpoints?.pooled?.connectionString ?? connection.connectionString ?? connection.endpoints?.direct?.connectionString ?? connection.endpoints?.accelerate?.connectionString ?? null;
|
|
153
|
+
}
|
|
154
|
+
function databaseApiError(summary, response, error) {
|
|
155
|
+
const status = response?.status ?? 0;
|
|
156
|
+
return new CliError({
|
|
157
|
+
code: error?.error?.code ?? "DATABASE_API_ERROR",
|
|
158
|
+
domain: "database",
|
|
159
|
+
summary,
|
|
160
|
+
why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
|
|
161
|
+
fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
|
|
162
|
+
exitCode: 1,
|
|
163
|
+
nextSteps: []
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { createManagementDatabaseProvider, normalizeConnection, normalizeDatabase };
|
|
@@ -1,25 +1,75 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { Result, TaggedError, UnhandledException } from "better-result";
|
|
3
4
|
//#region src/lib/project/local-pin.ts
|
|
4
5
|
const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
var LocalResolutionPinInvalidJsonError = class extends TaggedError("LocalResolutionPinInvalidJsonError")() {
|
|
7
|
+
constructor(cause) {
|
|
8
|
+
super({
|
|
9
|
+
message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} contains invalid JSON.`,
|
|
10
|
+
cause,
|
|
11
|
+
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var LocalResolutionPinInvalidShapeError = class extends TaggedError("LocalResolutionPinInvalidShapeError")() {
|
|
16
|
+
constructor() {
|
|
17
|
+
super({
|
|
18
|
+
message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} must contain workspaceId and projectId string fields only.`,
|
|
19
|
+
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var LocalResolutionPinReadAbortedError = class extends TaggedError("LocalResolutionPinReadAbortedError")() {
|
|
24
|
+
constructor(cause) {
|
|
25
|
+
super({
|
|
26
|
+
message: `Reading ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} was aborted.`,
|
|
27
|
+
cause,
|
|
28
|
+
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
|
|
11
29
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
async function readLocalResolutionPin(cwd, signal) {
|
|
33
|
+
return Result.gen(async function* () {
|
|
34
|
+
yield* ensureLocalResolutionPinReadNotAborted(signal);
|
|
35
|
+
const file = yield* Result.await(readLocalResolutionPinFile(cwd, signal));
|
|
36
|
+
if (file.kind === "missing") return Result.ok({ kind: "missing" });
|
|
37
|
+
const parsed = yield* parseLocalResolutionPin(file.raw);
|
|
38
|
+
if (!isLocalResolutionPin(parsed)) return Result.err(new LocalResolutionPinInvalidShapeError());
|
|
39
|
+
return Result.ok({
|
|
15
40
|
kind: "present",
|
|
16
41
|
pin: parsed
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function ensureLocalResolutionPinReadNotAborted(signal) {
|
|
46
|
+
return Result.try({
|
|
47
|
+
try: () => signal?.throwIfAborted(),
|
|
48
|
+
catch: (cause) => new LocalResolutionPinReadAbortedError(cause)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function readLocalResolutionPinFile(cwd, signal) {
|
|
52
|
+
const readResult = await Result.tryPromise({
|
|
53
|
+
try: () => readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
signal
|
|
56
|
+
}),
|
|
57
|
+
catch: (cause) => signal?.aborted ? new LocalResolutionPinReadAbortedError(cause) : new UnhandledException({ cause })
|
|
58
|
+
});
|
|
59
|
+
if (readResult.isErr()) {
|
|
60
|
+
if (readResult.error instanceof UnhandledException && readResult.error.cause.code === "ENOENT") return Result.ok({ kind: "missing" });
|
|
61
|
+
return Result.err(readResult.error);
|
|
22
62
|
}
|
|
63
|
+
return Result.ok({
|
|
64
|
+
kind: "present",
|
|
65
|
+
raw: readResult.value
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function parseLocalResolutionPin(raw) {
|
|
69
|
+
return Result.try({
|
|
70
|
+
try: () => JSON.parse(raw),
|
|
71
|
+
catch: (cause) => cause instanceof SyntaxError ? new LocalResolutionPinInvalidJsonError(cause) : new UnhandledException({ cause })
|
|
72
|
+
});
|
|
23
73
|
}
|
|
24
74
|
async function writeLocalResolutionPin(cwd, pin, signal) {
|
|
25
75
|
const prismaDir = path.join(cwd, ".prisma");
|
|
@@ -3,6 +3,7 @@ import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
|
3
3
|
import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { matchError } from "better-result";
|
|
6
7
|
//#region src/lib/project/resolution.ts
|
|
7
8
|
async function resolveProjectTarget(options) {
|
|
8
9
|
const localPin = await readImplicitLocalPin(options, { allowEnvProjectId: true });
|
|
@@ -224,7 +225,6 @@ async function resolveBoundProjectTarget(options, projects, settings) {
|
|
|
224
225
|
}
|
|
225
226
|
const localPin = settings.localPin;
|
|
226
227
|
if (!localPin) return null;
|
|
227
|
-
if (localPin.kind === "invalid") throw localStateStaleError();
|
|
228
228
|
if (localPin.kind === "present") {
|
|
229
229
|
if (localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
|
|
230
230
|
pinnedWorkspaceId: localPin.pin.workspaceId,
|
|
@@ -247,7 +247,9 @@ async function resolveBoundProjectTarget(options, projects, settings) {
|
|
|
247
247
|
}
|
|
248
248
|
async function readImplicitLocalPin(options, settings) {
|
|
249
249
|
if (options.explicitProject || settings.allowEnvProjectId && options.envProjectId) return null;
|
|
250
|
-
const
|
|
250
|
+
const localPinResult = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
|
|
251
|
+
if (localPinResult.isErr()) throw localPinReadErrorToProjectError(localPinResult.error);
|
|
252
|
+
const localPin = localPinResult.value;
|
|
251
253
|
if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
|
|
252
254
|
pinnedWorkspaceId: localPin.pin.workspaceId,
|
|
253
255
|
pinnedProjectId: localPin.pin.projectId,
|
|
@@ -255,6 +257,18 @@ async function readImplicitLocalPin(options, settings) {
|
|
|
255
257
|
});
|
|
256
258
|
return localPin;
|
|
257
259
|
}
|
|
260
|
+
function localPinReadErrorToProjectError(error) {
|
|
261
|
+
return matchError(error, {
|
|
262
|
+
LocalResolutionPinInvalidJsonError: () => localStateStaleError(),
|
|
263
|
+
LocalResolutionPinInvalidShapeError: () => localStateStaleError(),
|
|
264
|
+
LocalResolutionPinReadAbortedError: (error) => {
|
|
265
|
+
throw error;
|
|
266
|
+
},
|
|
267
|
+
UnhandledException: (error) => {
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
258
272
|
function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
|
|
259
273
|
return {
|
|
260
274
|
workspace,
|
package/dist/presenters/app.js
CHANGED
|
@@ -43,11 +43,16 @@ function renderAppDeploy(context, descriptor, result) {
|
|
|
43
43
|
];
|
|
44
44
|
}
|
|
45
45
|
function serializeAppDeploy(result) {
|
|
46
|
-
const { deploySettings
|
|
46
|
+
const { deploySettings, localPin: _localPin, ...serialized } = result;
|
|
47
47
|
const { id: _branchId, ...branch } = serialized.branch;
|
|
48
48
|
return {
|
|
49
49
|
...serialized,
|
|
50
|
-
branch
|
|
50
|
+
branch,
|
|
51
|
+
deploySettings: {
|
|
52
|
+
config: deploySettings.config,
|
|
53
|
+
buildCommand: deploySettings.buildCommand,
|
|
54
|
+
outputDirectory: deploySettings.outputDirectory
|
|
55
|
+
}
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
58
|
function renderBranchDatabaseDeploySummary(context, result) {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { formatColumns, renderSummaryLine } from "../shell/ui.js";
|
|
2
|
+
import { formatDescriptorLabel } from "../shell/command-meta.js";
|
|
3
|
+
import { renderMutate, renderShow, serializeList } from "../output/patterns.js";
|
|
4
|
+
import { renderResolvedProjectContextBlock, stripVerboseContext } from "./verbose-context.js";
|
|
5
|
+
//#region src/presenters/database.ts
|
|
6
|
+
function renderDatabaseList(context, descriptor, result) {
|
|
7
|
+
const ui = context.ui;
|
|
8
|
+
const lines = [`${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim("Listing databases for the resolved project.")}`, ""];
|
|
9
|
+
const rail = ui.dim("│");
|
|
10
|
+
lines.push(`${rail} ${ui.accent("project:")} ${result.projectName}`);
|
|
11
|
+
if (result.branchName) lines.push(`${rail} ${ui.accent("branch:")} ${result.branchName}`);
|
|
12
|
+
lines.push(rail);
|
|
13
|
+
if (result.databases.length === 0) {
|
|
14
|
+
lines.push(`${rail} ${ui.dim("No databases found.")}`);
|
|
15
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
16
|
+
return lines;
|
|
17
|
+
}
|
|
18
|
+
const rows = result.databases.map((database) => [
|
|
19
|
+
database.name,
|
|
20
|
+
database.branchName ?? "unscoped",
|
|
21
|
+
database.region ?? "unknown",
|
|
22
|
+
formatStatus(database),
|
|
23
|
+
database.id
|
|
24
|
+
]);
|
|
25
|
+
const widths = [
|
|
26
|
+
Math.max(4, ...rows.map((row) => row[0].length)),
|
|
27
|
+
Math.max(6, ...rows.map((row) => row[1].length)),
|
|
28
|
+
Math.max(6, ...rows.map((row) => row[2].length)),
|
|
29
|
+
Math.max(6, ...rows.map((row) => row[3].length)),
|
|
30
|
+
Math.max(2, ...rows.map((row) => row[4].length))
|
|
31
|
+
];
|
|
32
|
+
lines.push(`${rail} ${ui.accent(formatColumns([
|
|
33
|
+
"Name",
|
|
34
|
+
"Branch",
|
|
35
|
+
"Region",
|
|
36
|
+
"Status",
|
|
37
|
+
"Id"
|
|
38
|
+
], widths))}`);
|
|
39
|
+
for (const row of rows) lines.push(`${rail} ${formatColumns(row, widths)}`);
|
|
40
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
function serializeDatabaseList(result) {
|
|
44
|
+
return {
|
|
45
|
+
...serializeList({
|
|
46
|
+
context: {
|
|
47
|
+
project: result.projectName,
|
|
48
|
+
...result.branchName ? { branch: result.branchName } : {}
|
|
49
|
+
},
|
|
50
|
+
items: result.databases.map((database) => ({
|
|
51
|
+
noun: "database",
|
|
52
|
+
label: database.name,
|
|
53
|
+
id: database.id,
|
|
54
|
+
status: database.isDefault ? "default" : null
|
|
55
|
+
}))
|
|
56
|
+
}),
|
|
57
|
+
projectId: result.projectId,
|
|
58
|
+
branchName: result.branchName,
|
|
59
|
+
databases: result.databases
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function renderDatabaseShow(context, descriptor, result) {
|
|
63
|
+
const lines = renderShow({
|
|
64
|
+
title: "Showing database metadata.",
|
|
65
|
+
descriptor,
|
|
66
|
+
fields: [
|
|
67
|
+
{
|
|
68
|
+
key: "project",
|
|
69
|
+
value: result.projectName
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "database",
|
|
73
|
+
value: result.database.name
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "id",
|
|
77
|
+
value: result.database.id,
|
|
78
|
+
tone: "dim"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "branch",
|
|
82
|
+
value: result.database.branchName ?? "unscoped",
|
|
83
|
+
tone: result.database.branchName ? "default" : "dim"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "region",
|
|
87
|
+
value: result.database.region ?? "unknown",
|
|
88
|
+
tone: result.database.region ? "default" : "dim"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "status",
|
|
92
|
+
value: formatStatus(result.database)
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "connections",
|
|
96
|
+
value: String(result.connections.length)
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}, context.ui);
|
|
100
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
function serializeDatabaseShow(result) {
|
|
104
|
+
return stripVerboseContext(result);
|
|
105
|
+
}
|
|
106
|
+
function renderDatabaseCreateStdout(_context, _descriptor, result) {
|
|
107
|
+
return [result.connectionString];
|
|
108
|
+
}
|
|
109
|
+
function renderDatabaseCreate(context, _descriptor, result) {
|
|
110
|
+
const ui = context.ui;
|
|
111
|
+
const lines = [
|
|
112
|
+
"Creating database...",
|
|
113
|
+
renderSummaryLine(ui, "success", `Created database "${result.database.name}" in ${formatDatabaseTarget(result.projectName, result.database.branchName)}.`),
|
|
114
|
+
" The connection URL below is shown once, so save it now."
|
|
115
|
+
];
|
|
116
|
+
if (ui.verbose) {
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push(...renderDatabaseCreateVerboseRows(context, result));
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
function serializeDatabaseCreate(result) {
|
|
123
|
+
return stripVerboseContext(result);
|
|
124
|
+
}
|
|
125
|
+
function renderDatabaseRemove(context, descriptor, result) {
|
|
126
|
+
const lines = renderMutate({
|
|
127
|
+
title: "Removing database.",
|
|
128
|
+
descriptor,
|
|
129
|
+
context: [
|
|
130
|
+
{
|
|
131
|
+
key: "project",
|
|
132
|
+
value: result.projectName
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: "database",
|
|
136
|
+
value: result.database.name
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: "id",
|
|
140
|
+
value: result.database.id,
|
|
141
|
+
tone: "dim"
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
operationDescription: "Removing database",
|
|
145
|
+
operationCount: 1,
|
|
146
|
+
details: ["Database and its connection metadata were removed."]
|
|
147
|
+
}, context.ui);
|
|
148
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
function serializeDatabaseRemove(result) {
|
|
152
|
+
return stripVerboseContext(result);
|
|
153
|
+
}
|
|
154
|
+
function renderDatabaseConnectionList(context, descriptor, result) {
|
|
155
|
+
const ui = context.ui;
|
|
156
|
+
const lines = [`${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim("Listing database connection metadata.")}`, ""];
|
|
157
|
+
const rail = ui.dim("│");
|
|
158
|
+
lines.push(`${rail} ${ui.accent("database:")} ${result.database.name}`);
|
|
159
|
+
lines.push(rail);
|
|
160
|
+
if (result.connections.length === 0) {
|
|
161
|
+
lines.push(`${rail} ${ui.dim("No database connections found.")}`);
|
|
162
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
const rows = result.connections.map((connection) => [
|
|
166
|
+
connection.name,
|
|
167
|
+
connection.id,
|
|
168
|
+
connection.createdAt ?? "unknown"
|
|
169
|
+
]);
|
|
170
|
+
const widths = [
|
|
171
|
+
Math.max(4, ...rows.map((row) => row[0].length)),
|
|
172
|
+
Math.max(2, ...rows.map((row) => row[1].length)),
|
|
173
|
+
Math.max(7, ...rows.map((row) => row[2].length))
|
|
174
|
+
];
|
|
175
|
+
lines.push(`${rail} ${ui.accent(formatColumns([
|
|
176
|
+
"Name",
|
|
177
|
+
"Id",
|
|
178
|
+
"Created"
|
|
179
|
+
], widths))}`);
|
|
180
|
+
for (const row of rows) lines.push(`${rail} ${formatColumns(row, widths)}`);
|
|
181
|
+
lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext));
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
function serializeDatabaseConnectionList(result) {
|
|
185
|
+
return {
|
|
186
|
+
...serializeList({
|
|
187
|
+
context: {
|
|
188
|
+
project: result.projectName,
|
|
189
|
+
database: result.database.name
|
|
190
|
+
},
|
|
191
|
+
items: result.connections.map((connection) => ({
|
|
192
|
+
noun: "connection",
|
|
193
|
+
label: connection.name,
|
|
194
|
+
id: connection.id,
|
|
195
|
+
status: null
|
|
196
|
+
}))
|
|
197
|
+
}),
|
|
198
|
+
projectId: result.projectId,
|
|
199
|
+
database: result.database,
|
|
200
|
+
connections: result.connections
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function renderDatabaseConnectionCreateStdout(_context, _descriptor, result) {
|
|
204
|
+
return [result.connectionString];
|
|
205
|
+
}
|
|
206
|
+
function renderDatabaseConnectionCreate(context, _descriptor, result) {
|
|
207
|
+
const ui = context.ui;
|
|
208
|
+
const lines = [
|
|
209
|
+
"Creating connection...",
|
|
210
|
+
renderSummaryLine(ui, "success", `Added a connection to "${result.database.name}" in ${formatDatabaseTarget(result.projectName, result.database.branchName)}.`),
|
|
211
|
+
" The connection URL below is shown once, so save it now."
|
|
212
|
+
];
|
|
213
|
+
if (ui.verbose) {
|
|
214
|
+
lines.push("");
|
|
215
|
+
lines.push(...renderDatabaseConnectionCreateVerboseRows(context, result));
|
|
216
|
+
}
|
|
217
|
+
return lines;
|
|
218
|
+
}
|
|
219
|
+
function serializeDatabaseConnectionCreate(result) {
|
|
220
|
+
return stripVerboseContext(result);
|
|
221
|
+
}
|
|
222
|
+
function renderDatabaseConnectionRemove(context, descriptor, result) {
|
|
223
|
+
return renderMutate({
|
|
224
|
+
title: "Removing database connection.",
|
|
225
|
+
descriptor,
|
|
226
|
+
context: [{
|
|
227
|
+
key: "connection",
|
|
228
|
+
value: result.connection.id,
|
|
229
|
+
tone: "dim"
|
|
230
|
+
}],
|
|
231
|
+
operationDescription: "Removing database connection",
|
|
232
|
+
operationCount: 1,
|
|
233
|
+
details: ["The connection metadata was removed. Existing one-time secrets were not shown."]
|
|
234
|
+
}, context.ui);
|
|
235
|
+
}
|
|
236
|
+
function serializeDatabaseConnectionRemove(result) {
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
function formatStatus(database) {
|
|
240
|
+
return database.status ?? (database.isDefault ? "default" : "unknown");
|
|
241
|
+
}
|
|
242
|
+
function formatDatabaseTarget(projectName, branchName) {
|
|
243
|
+
return branchName ? `${projectName} / ${branchName}` : projectName;
|
|
244
|
+
}
|
|
245
|
+
function renderDatabaseCreateVerboseRows(context, result) {
|
|
246
|
+
return renderMetadataRows([
|
|
247
|
+
...renderWorkspaceProjectRows(result),
|
|
248
|
+
["branch", result.database.branchName ?? "unscoped"],
|
|
249
|
+
["database", formatResourceWithId(context, result.database.name, result.database.id)],
|
|
250
|
+
["region", result.database.region ?? "unknown"],
|
|
251
|
+
["status", formatStatus(result.database)],
|
|
252
|
+
["connection", formatResourceWithId(context, result.connection.name, result.connection.id)]
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
255
|
+
function renderDatabaseConnectionCreateVerboseRows(context, result) {
|
|
256
|
+
return renderMetadataRows([
|
|
257
|
+
...renderWorkspaceProjectRows(result),
|
|
258
|
+
["branch", result.database.branchName ?? "unscoped"],
|
|
259
|
+
["database", formatResourceWithId(context, result.database.name, result.database.id)],
|
|
260
|
+
["connection", formatResourceWithId(context, result.connection.name, result.connection.id)]
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
function renderWorkspaceProjectRows(result) {
|
|
264
|
+
return [...result.verboseContext ? [["workspace", result.verboseContext.workspace.name]] : [], ["project", result.projectName]];
|
|
265
|
+
}
|
|
266
|
+
function formatResourceWithId(context, name, id) {
|
|
267
|
+
return `${name} ${context.ui.dim(`(${id})`)}`;
|
|
268
|
+
}
|
|
269
|
+
function renderMetadataRows(rows) {
|
|
270
|
+
const widths = [Math.max(...rows.map((row) => row[0].length)), 0];
|
|
271
|
+
return rows.map(([key, value]) => ` ${formatColumns([key, value], widths)}`);
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
export { renderDatabaseConnectionCreate, renderDatabaseConnectionCreateStdout, renderDatabaseConnectionList, renderDatabaseConnectionRemove, renderDatabaseCreate, renderDatabaseCreateStdout, renderDatabaseList, renderDatabaseRemove, renderDatabaseShow, serializeDatabaseConnectionCreate, serializeDatabaseConnectionList, serializeDatabaseConnectionRemove, serializeDatabaseCreate, serializeDatabaseList, serializeDatabaseRemove, serializeDatabaseShow };
|