@prisma/cli 3.0.0-dev.58.1 → 3.0.0-dev.61.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/commands/app/index.js +21 -11
- package/dist/controllers/app.js +25 -3
- package/dist/lib/app/branch-database-deploy.js +323 -0
- package/dist/lib/app/branch-database.js +316 -0
- package/dist/lib/app/preview-branch-database.js +102 -0
- package/dist/lib/app/preview-provider.js +19 -0
- package/dist/presenters/app.js +25 -0
- package/dist/shell/command-meta.js +2 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usageError } from "../../shell/errors.js";
|
|
1
2
|
import { attachCommandDescriptor } from "../../shell/command-meta.js";
|
|
2
3
|
import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
|
|
3
4
|
import { configureRuntimeCommand } from "../../shell/runtime.js";
|
|
@@ -64,7 +65,7 @@ function createDeployCommand(runtime) {
|
|
|
64
65
|
"hono",
|
|
65
66
|
"tanstack-start",
|
|
66
67
|
"bun"
|
|
67
|
-
])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues)).addOption(new Option("--prod", "Confirm intent to deploy to production"));
|
|
68
|
+
])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues)).addOption(new Option("--db", "Create and wire an isolated database for the preview Branch")).addOption(new Option("--no-db", "Skip branch database setup")).addOption(new Option("--prod", "Confirm intent to deploy to production"));
|
|
68
69
|
addGlobalFlags(command);
|
|
69
70
|
command.action(async (options) => {
|
|
70
71
|
const appName = options.app;
|
|
@@ -76,22 +77,31 @@ function createDeployCommand(runtime) {
|
|
|
76
77
|
const projectRef = options.project;
|
|
77
78
|
const createProjectName = options.createProject;
|
|
78
79
|
const prod = options.prod;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
const db = options.db;
|
|
81
|
+
const hasDbConflict = hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
|
|
82
|
+
await runCommand(runtime, "app.deploy", options, (context) => {
|
|
83
|
+
if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests branch database setup, while --no-db disables it.", "Pass exactly one database setup flag.", ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"], "app");
|
|
84
|
+
return runAppDeploy(context, appName, {
|
|
85
|
+
projectRef,
|
|
86
|
+
createProjectName,
|
|
87
|
+
branchName,
|
|
88
|
+
entrypoint: entry,
|
|
89
|
+
framework,
|
|
90
|
+
httpPort,
|
|
91
|
+
envAssignments,
|
|
92
|
+
prod: prod === true,
|
|
93
|
+
db
|
|
94
|
+
});
|
|
95
|
+
}, {
|
|
89
96
|
renderHuman: (context, descriptor, result) => renderAppDeploy(context, descriptor, result),
|
|
90
97
|
renderJson: (result) => serializeAppDeploy(result)
|
|
91
98
|
});
|
|
92
99
|
});
|
|
93
100
|
return command;
|
|
94
101
|
}
|
|
102
|
+
function hasFlag(argv, flag) {
|
|
103
|
+
return argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
104
|
+
}
|
|
95
105
|
function createShowCommand(runtime) {
|
|
96
106
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("show"), runtime), "app.show");
|
|
97
107
|
command.addOption(new Option("--app <name>", "App name")).addOption(new Option("--project <id-or-name>", "Project id or name"));
|
package/dist/controllers/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js
|
|
|
19
19
|
import { readLocalGitBranch } from "../lib/git/local-branch.js";
|
|
20
20
|
import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
|
|
21
21
|
import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
|
|
22
|
+
import { maybeSetupBranchDatabase } from "../lib/app/branch-database-deploy.js";
|
|
22
23
|
import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
|
|
23
24
|
import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
|
|
24
25
|
import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate.js";
|
|
@@ -170,6 +171,10 @@ async function runAppDeploy(context, appName, options) {
|
|
|
170
171
|
assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
|
|
171
172
|
const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
|
|
172
173
|
const portMapping = parseDeployPortMapping(String(runtime.port));
|
|
174
|
+
const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
|
|
175
|
+
db: options?.db,
|
|
176
|
+
inlineEnvVars: envVars
|
|
177
|
+
});
|
|
173
178
|
const progressState = createPreviewDeployProgressState();
|
|
174
179
|
const deployStartedAt = Date.now();
|
|
175
180
|
const deployResult = await provider.deployApp({
|
|
@@ -200,8 +205,9 @@ async function runAppDeploy(context, appName, options) {
|
|
|
200
205
|
result: {
|
|
201
206
|
workspace: target.workspace,
|
|
202
207
|
project: target.project,
|
|
203
|
-
branch: target.branch,
|
|
208
|
+
branch: toResultBranch(target.branch),
|
|
204
209
|
resolution: target.resolution,
|
|
210
|
+
branchDatabase: branchDatabaseSetup.result,
|
|
205
211
|
app: {
|
|
206
212
|
id: deployResult.app.id,
|
|
207
213
|
name: deployResult.app.name
|
|
@@ -210,7 +216,7 @@ async function runAppDeploy(context, appName, options) {
|
|
|
210
216
|
durationMs: deployDurationMs,
|
|
211
217
|
localPin: localPinResult
|
|
212
218
|
},
|
|
213
|
-
warnings:
|
|
219
|
+
warnings: branchDatabaseSetup.warnings,
|
|
214
220
|
nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${deployResult.deployment.id}`]
|
|
215
221
|
};
|
|
216
222
|
}
|
|
@@ -757,7 +763,7 @@ async function resolveAppDomainTarget(context, options, commandName = "app domai
|
|
|
757
763
|
resultTarget: {
|
|
758
764
|
workspace: target.workspace,
|
|
759
765
|
project: target.project,
|
|
760
|
-
branch: target.branch,
|
|
766
|
+
branch: toResultBranch(target.branch),
|
|
761
767
|
app: {
|
|
762
768
|
id: selectedApp.id,
|
|
763
769
|
name: selectedApp.name
|
|
@@ -1320,6 +1326,7 @@ async function resolveProjectContext(context, client, explicitProject, options)
|
|
|
1320
1326
|
return {
|
|
1321
1327
|
...resolved,
|
|
1322
1328
|
branch: {
|
|
1329
|
+
id: null,
|
|
1323
1330
|
name: branch.name,
|
|
1324
1331
|
kind: toBranchKind(branch.name)
|
|
1325
1332
|
}
|
|
@@ -1446,6 +1453,7 @@ async function withRemoteDeployBranch(provider, target, branch, signal) {
|
|
|
1446
1453
|
return {
|
|
1447
1454
|
...target,
|
|
1448
1455
|
branch: {
|
|
1456
|
+
id: remoteBranch.id,
|
|
1449
1457
|
name: remoteBranch.name,
|
|
1450
1458
|
kind: remoteBranch.role
|
|
1451
1459
|
}
|
|
@@ -1454,6 +1462,20 @@ async function withRemoteDeployBranch(provider, target, branch, signal) {
|
|
|
1454
1462
|
function toBranchKind(name) {
|
|
1455
1463
|
return name === "production" || name === "main" ? "production" : "preview";
|
|
1456
1464
|
}
|
|
1465
|
+
function toResultBranch(branch) {
|
|
1466
|
+
return {
|
|
1467
|
+
name: branch.name,
|
|
1468
|
+
kind: branch.kind
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function toBranchDatabaseDeployBranch(branch) {
|
|
1472
|
+
if (!branch.id) throw new Error(`Deploy branch "${branch.name}" was not resolved remotely.`);
|
|
1473
|
+
return {
|
|
1474
|
+
id: branch.id,
|
|
1475
|
+
name: branch.name,
|
|
1476
|
+
kind: branch.kind
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1457
1479
|
function assertExclusiveDeployProjectInputs(options) {
|
|
1458
1480
|
const provided = [
|
|
1459
1481
|
options.projectRef ? "--project" : null,
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { CliError, usageError } from "../../shell/errors.js";
|
|
2
|
+
import { renderSummaryLine } from "../../shell/ui.js";
|
|
3
|
+
import { canPrompt } from "../../shell/runtime.js";
|
|
4
|
+
import { confirmPrompt } from "../../shell/prompt.js";
|
|
5
|
+
import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
6
|
+
import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup } from "./branch-database.js";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
//#region src/lib/app/branch-database-deploy.ts
|
|
9
|
+
async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
|
|
10
|
+
if (options.db === false) return emptyBranchDatabaseSetupOutcome();
|
|
11
|
+
if (hasInlineDatabaseEnvVars(options.inlineEnvVars)) {
|
|
12
|
+
if (options.db === true) throw usageError("Branch database setup cannot be combined with inline database env vars", "The deploy command received --db and an inline DATABASE_URL or DIRECT_URL value.", "Remove the inline --env database value to let --db create a branch override, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
|
|
13
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
14
|
+
}
|
|
15
|
+
if (branch.kind === "production") {
|
|
16
|
+
if (options.db === true) throw usageError("Branch database setup is only available for preview branches", "Production database wiring is a durable environment decision and is not created implicitly by app deploy.", "Use project env commands to manage production DATABASE_URL, or deploy a preview branch with --db.", ["prisma-cli project env add DATABASE_URL=<value> --role production", "prisma-cli app deploy --branch feature/db --db"], "app");
|
|
17
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
18
|
+
}
|
|
19
|
+
const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
|
|
20
|
+
const envState = await inspectBranchDatabaseEnv(provider, projectId, branch.id, context.runtime.signal);
|
|
21
|
+
const branchEnvVars = [envState.branchDatabaseUrl, envState.branchDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
|
|
22
|
+
if (envState.branchDatabaseUrl) {
|
|
23
|
+
const warning = options.db === true ? `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.` : null;
|
|
24
|
+
if (warning) emitBranchDatabaseWarning(context, warning);
|
|
25
|
+
return {
|
|
26
|
+
result: options.db === true ? {
|
|
27
|
+
status: "skipped",
|
|
28
|
+
reason: "branch-env-exists",
|
|
29
|
+
envVars: branchEnvVars,
|
|
30
|
+
schema: null
|
|
31
|
+
} : void 0,
|
|
32
|
+
warnings: warning ? [warning] : []
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (localSignal.unsupportedSchema) {
|
|
36
|
+
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch.name, context);
|
|
37
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
38
|
+
}
|
|
39
|
+
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.previewDatabaseUrl);
|
|
40
|
+
if (options.db !== true) {
|
|
41
|
+
if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
|
|
42
|
+
if (!canPrompt(context) || context.flags.yes) {
|
|
43
|
+
const warning = "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
|
|
44
|
+
emitBranchDatabaseWarning(context, warning);
|
|
45
|
+
return {
|
|
46
|
+
result: void 0,
|
|
47
|
+
warnings: [warning]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
maybeRenderBranchDatabaseSignal(context, branch.name, localSignal, envState);
|
|
51
|
+
if (!await confirmPrompt({
|
|
52
|
+
input: context.runtime.stdin,
|
|
53
|
+
output: context.output.stderr,
|
|
54
|
+
message: `Create an isolated database for branch "${branch.name}"?`,
|
|
55
|
+
initialValue: false
|
|
56
|
+
})) return emptyBranchDatabaseSetupOutcome();
|
|
57
|
+
}
|
|
58
|
+
return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
|
|
59
|
+
}
|
|
60
|
+
async function setupBranchDatabase(context, provider, projectId, branch, signal, envState) {
|
|
61
|
+
emitBranchDatabaseProgress(context, "pending", "Creating branch database");
|
|
62
|
+
const database = await provider.createBranchDatabase({
|
|
63
|
+
projectId,
|
|
64
|
+
branchId: branch.id,
|
|
65
|
+
branchName: branch.name,
|
|
66
|
+
signal: context.runtime.signal
|
|
67
|
+
}).catch((error) => {
|
|
68
|
+
throw branchDatabaseSetupFailedError("Failed to create branch database", error, branch.name);
|
|
69
|
+
});
|
|
70
|
+
emitBranchDatabaseProgress(context, "success", "Created branch database");
|
|
71
|
+
try {
|
|
72
|
+
let schemaSetup = null;
|
|
73
|
+
const warnings = [];
|
|
74
|
+
let skippedSchemaWarning = null;
|
|
75
|
+
if (signal.schema) {
|
|
76
|
+
emitBranchDatabaseProgress(context, "pending", `Applying database schema with ${formatSchemaSetupCommand(signal.schema.command)}`);
|
|
77
|
+
schemaSetup = await runBranchDatabaseSchemaSetup({
|
|
78
|
+
context,
|
|
79
|
+
schema: signal.schema,
|
|
80
|
+
databaseUrl: database.databaseUrl,
|
|
81
|
+
directUrl: database.directUrl
|
|
82
|
+
}).catch((error) => {
|
|
83
|
+
throw schemaSetupFailedError(error, signal.schema, branch.name, context.runtime.cwd);
|
|
84
|
+
});
|
|
85
|
+
emitBranchDatabaseProgress(context, "success", "Applied database schema");
|
|
86
|
+
} else skippedSchemaWarning = "No supported Prisma schema source was found. Branch database env vars were created, but schema setup was skipped.";
|
|
87
|
+
const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
|
|
88
|
+
emitBranchDatabaseProgress(context, "success", `Added branch env override${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
|
|
89
|
+
if (skippedSchemaWarning) {
|
|
90
|
+
emitBranchDatabaseWarning(context, skippedSchemaWarning);
|
|
91
|
+
warnings.push(skippedSchemaWarning);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
result: {
|
|
95
|
+
status: "created",
|
|
96
|
+
database: {
|
|
97
|
+
id: database.id,
|
|
98
|
+
name: database.name
|
|
99
|
+
},
|
|
100
|
+
envVars,
|
|
101
|
+
schema: schemaSetup ? {
|
|
102
|
+
command: schemaSetup.command,
|
|
103
|
+
source: schemaSetup.source,
|
|
104
|
+
path: schemaSetup.schemaPath
|
|
105
|
+
} : null
|
|
106
|
+
},
|
|
107
|
+
warnings
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch.name, error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState) {
|
|
114
|
+
const written = [];
|
|
115
|
+
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
116
|
+
projectId,
|
|
117
|
+
branchId: branch.id,
|
|
118
|
+
className: "preview",
|
|
119
|
+
key: "DATABASE_URL",
|
|
120
|
+
value: database.databaseUrl,
|
|
121
|
+
existing: envState.branchDatabaseUrl,
|
|
122
|
+
branchName: branch.name
|
|
123
|
+
});
|
|
124
|
+
written.push("DATABASE_URL");
|
|
125
|
+
if (database.directUrl) {
|
|
126
|
+
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
127
|
+
projectId,
|
|
128
|
+
branchId: branch.id,
|
|
129
|
+
className: "preview",
|
|
130
|
+
key: "DIRECT_URL",
|
|
131
|
+
value: database.directUrl,
|
|
132
|
+
existing: envState.branchDirectUrl,
|
|
133
|
+
branchName: branch.name
|
|
134
|
+
});
|
|
135
|
+
written.push("DIRECT_URL");
|
|
136
|
+
} else if (envState.branchDirectUrl) await provider.deleteEnvironmentVariable({
|
|
137
|
+
envVarId: envState.branchDirectUrl.id,
|
|
138
|
+
signal: context.runtime.signal
|
|
139
|
+
}).catch((error) => {
|
|
140
|
+
throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch.name);
|
|
141
|
+
});
|
|
142
|
+
return written;
|
|
143
|
+
}
|
|
144
|
+
async function upsertBranchDatabaseEnvVar(context, provider, options) {
|
|
145
|
+
if (options.existing) {
|
|
146
|
+
await provider.updateEnvironmentVariable({
|
|
147
|
+
envVarId: options.existing.id,
|
|
148
|
+
value: options.value,
|
|
149
|
+
signal: context.runtime.signal
|
|
150
|
+
}).catch((error) => {
|
|
151
|
+
throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branchName);
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await provider.createEnvironmentVariable({
|
|
156
|
+
projectId: options.projectId,
|
|
157
|
+
branchId: options.branchId,
|
|
158
|
+
className: options.className,
|
|
159
|
+
key: options.key,
|
|
160
|
+
value: options.value,
|
|
161
|
+
signal: context.runtime.signal
|
|
162
|
+
}).catch((error) => {
|
|
163
|
+
throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branchName);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function inspectBranchDatabaseEnv(provider, projectId, branchId, signal) {
|
|
167
|
+
const [databaseUrlRows, directUrlRows] = await Promise.all([provider.listEnvironmentVariables({
|
|
168
|
+
projectId,
|
|
169
|
+
className: "preview",
|
|
170
|
+
key: "DATABASE_URL",
|
|
171
|
+
signal
|
|
172
|
+
}), provider.listEnvironmentVariables({
|
|
173
|
+
projectId,
|
|
174
|
+
className: "preview",
|
|
175
|
+
key: "DIRECT_URL",
|
|
176
|
+
signal
|
|
177
|
+
})]);
|
|
178
|
+
return {
|
|
179
|
+
branchDatabaseUrl: findEnvVar(databaseUrlRows, { branchId }),
|
|
180
|
+
branchDirectUrl: findEnvVar(directUrlRows, { branchId }),
|
|
181
|
+
previewDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: null })
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function findEnvVar(rows, options) {
|
|
185
|
+
return rows.find((row) => row.branchId === options.branchId) ?? null;
|
|
186
|
+
}
|
|
187
|
+
function hasInlineDatabaseEnvVars(envVars) {
|
|
188
|
+
return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
|
|
189
|
+
}
|
|
190
|
+
function maybeRenderBranchDatabaseSignal(context, branchName, signal, envState) {
|
|
191
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
192
|
+
const rows = [
|
|
193
|
+
signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
|
|
194
|
+
signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
|
|
195
|
+
envState.previewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
|
|
196
|
+
].filter((row) => Boolean(row));
|
|
197
|
+
context.output.stderr.write(`Database signal found for branch "${branchName}"\n${rows.join("\n")}\n\n`);
|
|
198
|
+
}
|
|
199
|
+
function emitBranchDatabaseProgress(context, status, message) {
|
|
200
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
201
|
+
const line = status === "pending" ? `${context.ui.warning("◇")} ${message}...` : renderSummaryLine(context.ui, "success", message);
|
|
202
|
+
context.output.stderr.write(`${line}\n`);
|
|
203
|
+
}
|
|
204
|
+
function emitBranchDatabaseWarning(context, warning) {
|
|
205
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
206
|
+
context.output.stderr.write(`${renderSummaryLine(context.ui, "warning", warning)}\n`);
|
|
207
|
+
}
|
|
208
|
+
function emptyBranchDatabaseSetupOutcome() {
|
|
209
|
+
return {
|
|
210
|
+
result: void 0,
|
|
211
|
+
warnings: []
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function formatSchemaSetupCommand(command) {
|
|
215
|
+
switch (command) {
|
|
216
|
+
case "migrate-deploy": return "prisma migrate deploy";
|
|
217
|
+
case "db-push": return "prisma db push";
|
|
218
|
+
case "prisma-next-db-init": return "prisma-next db init";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function branchDatabaseSetupFailedError(summary, error, branchName) {
|
|
222
|
+
return new CliError({
|
|
223
|
+
code: "BRANCH_DATABASE_SETUP_FAILED",
|
|
224
|
+
domain: "app",
|
|
225
|
+
summary,
|
|
226
|
+
why: error instanceof Error ? error.message : String(error),
|
|
227
|
+
fix: "Retry the command, or create the branch database and env vars manually with project env commands.",
|
|
228
|
+
debug: formatDebugDetails(error),
|
|
229
|
+
meta: { branch: branchName },
|
|
230
|
+
exitCode: 1,
|
|
231
|
+
nextSteps: [`prisma-cli app deploy --branch ${formatCommandArgument(branchName)} --db`, `prisma-cli project env list --branch ${formatCommandArgument(branchName)}`]
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branchName, error) {
|
|
235
|
+
const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Branch database setup failed", error, branchName);
|
|
236
|
+
emitBranchDatabaseProgress(context, "pending", "Removing branch database after setup failed");
|
|
237
|
+
try {
|
|
238
|
+
await provider.deleteBranchDatabase({
|
|
239
|
+
databaseId: database.id,
|
|
240
|
+
signal: context.runtime.signal
|
|
241
|
+
});
|
|
242
|
+
emitBranchDatabaseProgress(context, "success", "Removed branch database after setup failed");
|
|
243
|
+
} catch (cleanupError) {
|
|
244
|
+
return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branchName);
|
|
245
|
+
}
|
|
246
|
+
return setupError;
|
|
247
|
+
}
|
|
248
|
+
function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branchName) {
|
|
249
|
+
const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
250
|
+
const setupWhy = setupError.why ?? "Branch database setup failed.";
|
|
251
|
+
return new CliError({
|
|
252
|
+
code: setupError.code,
|
|
253
|
+
domain: setupError.domain,
|
|
254
|
+
summary: setupError.summary,
|
|
255
|
+
why: `${setupWhy} Prisma could not delete the created database "${database.name}" (${database.id}): ${cleanupWhy}`,
|
|
256
|
+
fix: "Delete the created branch database from Console or contact Prisma support, then rerun deploy with --db.",
|
|
257
|
+
debug: formatCombinedDebugDetails(setupError, cleanupError),
|
|
258
|
+
meta: {
|
|
259
|
+
...setupError.meta,
|
|
260
|
+
branch: branchName,
|
|
261
|
+
databaseId: database.id,
|
|
262
|
+
databaseName: database.name,
|
|
263
|
+
cleanupFailed: true
|
|
264
|
+
},
|
|
265
|
+
exitCode: setupError.exitCode,
|
|
266
|
+
nextSteps: []
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function schemaSetupFailedError(error, schema, branchName, cwd) {
|
|
270
|
+
return new CliError({
|
|
271
|
+
code: "SCHEMA_SETUP_FAILED",
|
|
272
|
+
domain: "app",
|
|
273
|
+
summary: "Database schema setup failed",
|
|
274
|
+
why: error instanceof Error ? error.message : String(error),
|
|
275
|
+
fix: "Fix the Prisma schema or migrations, then rerun deploy with --db.",
|
|
276
|
+
debug: formatDebugDetails(error),
|
|
277
|
+
meta: {
|
|
278
|
+
branch: branchName,
|
|
279
|
+
schemaPath: schema.path,
|
|
280
|
+
source: schema.kind,
|
|
281
|
+
command: schema.command
|
|
282
|
+
},
|
|
283
|
+
exitCode: 1,
|
|
284
|
+
nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), `prisma-cli app deploy --branch ${formatCommandArgument(branchName)} --db`]
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function unsupportedBranchDatabaseSchemaError(schema, branchName, context) {
|
|
288
|
+
return usageError("Branch database setup is not available for this Prisma schema", `${path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema)} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`, "Use project env commands to provide a database URL for this branch, or switch the Prisma schema source to PostgreSQL before using --db.", [`prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branchName)}`, `prisma-cli app deploy --branch ${formatCommandArgument(branchName)}`], "app");
|
|
289
|
+
}
|
|
290
|
+
function formatSchemaSetupNextSteps(schema, cwd) {
|
|
291
|
+
const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
|
|
292
|
+
switch (schema.command) {
|
|
293
|
+
case "migrate-deploy": return [`npx --no-install prisma migrate deploy --schema ${formatCommandArgument(sourcePath)}`];
|
|
294
|
+
case "db-push": return [`npx --no-install prisma db push --schema ${formatCommandArgument(sourcePath)}`];
|
|
295
|
+
case "prisma-next-db-init": return [`npx --no-install prisma-next contract emit --config ${formatCommandArgument(sourcePath)}`, `npx --no-install prisma-next db init --config ${formatCommandArgument(sourcePath)} --db <DATABASE_URL>`];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function defaultSchemaSourcePath(schema) {
|
|
299
|
+
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
300
|
+
}
|
|
301
|
+
function defaultUnsupportedSchemaSourcePath(schema) {
|
|
302
|
+
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
303
|
+
}
|
|
304
|
+
function formatUnsupportedSchemaTarget(target) {
|
|
305
|
+
switch (target) {
|
|
306
|
+
case "cockroachdb": return "CockroachDB";
|
|
307
|
+
case "mongodb": return "MongoDB";
|
|
308
|
+
case "mysql": return "MySQL";
|
|
309
|
+
case "sqlite": return "SQLite";
|
|
310
|
+
case "sqlserver": return "SQL Server";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function formatDebugDetails(error) {
|
|
314
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
315
|
+
return typeof error === "string" ? error : null;
|
|
316
|
+
}
|
|
317
|
+
function formatCombinedDebugDetails(setupError, cleanupError) {
|
|
318
|
+
const setupDebug = setupError.debug ?? setupError.stack ?? setupError.message;
|
|
319
|
+
const cleanupDebug = formatDebugDetails(cleanupError);
|
|
320
|
+
return [setupDebug ? `Setup error:\n${setupDebug}` : null, cleanupDebug ? `Cleanup error:\n${cleanupDebug}` : null].filter((line) => Boolean(line)).join("\n\n") || null;
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
export { maybeSetupBranchDatabase };
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
//#region src/lib/app/branch-database.ts
|
|
5
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
6
|
+
".git",
|
|
7
|
+
".next",
|
|
8
|
+
".nuxt",
|
|
9
|
+
".output",
|
|
10
|
+
".prisma",
|
|
11
|
+
".turbo",
|
|
12
|
+
".vercel",
|
|
13
|
+
".wrangler",
|
|
14
|
+
"build",
|
|
15
|
+
"coverage",
|
|
16
|
+
"dist",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"out"
|
|
19
|
+
]);
|
|
20
|
+
const DATABASE_URL_SCAN_EXTENSIONS = new Set([
|
|
21
|
+
".cjs",
|
|
22
|
+
".cts",
|
|
23
|
+
".env",
|
|
24
|
+
".js",
|
|
25
|
+
".json",
|
|
26
|
+
".jsx",
|
|
27
|
+
".mjs",
|
|
28
|
+
".mts",
|
|
29
|
+
".prisma",
|
|
30
|
+
".ts",
|
|
31
|
+
".tsx"
|
|
32
|
+
]);
|
|
33
|
+
const MAX_SCAN_DEPTH = 6;
|
|
34
|
+
const MAX_SCAN_FILES = 1e3;
|
|
35
|
+
const MAX_DATABASE_URL_REFERENCE_FILES = 10;
|
|
36
|
+
const MAX_TEXT_FILE_BYTES = 1024 * 1024;
|
|
37
|
+
async function inspectBranchDatabaseSignal(cwd, signal) {
|
|
38
|
+
const state = {
|
|
39
|
+
filesVisited: 0,
|
|
40
|
+
schemaCandidates: [],
|
|
41
|
+
prismaNextConfigCandidates: [],
|
|
42
|
+
databaseUrlReferences: []
|
|
43
|
+
};
|
|
44
|
+
await scanDirectory(cwd, cwd, 0, state, signal);
|
|
45
|
+
const prismaNextConfigs = await Promise.all(state.prismaNextConfigCandidates.map((configPath) => classifyPrismaNextConfig(configPath, signal)));
|
|
46
|
+
const supportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "supported");
|
|
47
|
+
const unsupportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "unsupported");
|
|
48
|
+
const selectedPrismaOrmSchema = await selectPrismaOrmSchema(cwd, state.schemaCandidates, signal);
|
|
49
|
+
const schema = supportedPrismaNextConfig ? {
|
|
50
|
+
kind: "prisma-next",
|
|
51
|
+
path: supportedPrismaNextConfig.path,
|
|
52
|
+
hasMigrations: false,
|
|
53
|
+
command: "prisma-next-db-init",
|
|
54
|
+
target: supportedPrismaNextConfig.target
|
|
55
|
+
} : selectedPrismaOrmSchema.schema;
|
|
56
|
+
return {
|
|
57
|
+
schema,
|
|
58
|
+
unsupportedSchema: schema ? null : unsupportedPrismaNextConfig ? {
|
|
59
|
+
kind: "prisma-next",
|
|
60
|
+
path: unsupportedPrismaNextConfig.path,
|
|
61
|
+
target: unsupportedPrismaNextConfig.target
|
|
62
|
+
} : selectedPrismaOrmSchema.unsupportedSchema,
|
|
63
|
+
databaseUrlReferences: state.databaseUrlReferences
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function hasBranchDatabaseSignal(signal) {
|
|
67
|
+
if (signal.unsupportedSchema) return false;
|
|
68
|
+
return Boolean(signal.schema || signal.databaseUrlReferences.length > 0);
|
|
69
|
+
}
|
|
70
|
+
async function runBranchDatabaseSchemaSetup(options) {
|
|
71
|
+
const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) || defaultSchemaSourcePath(options.schema);
|
|
72
|
+
const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl);
|
|
73
|
+
for (const command of commands) await runPrismaCommand({
|
|
74
|
+
context: options.context,
|
|
75
|
+
...command,
|
|
76
|
+
env: {
|
|
77
|
+
DATABASE_URL: options.databaseUrl,
|
|
78
|
+
...options.directUrl ? { DIRECT_URL: options.directUrl } : {}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
command: options.schema.command,
|
|
83
|
+
source: options.schema.kind,
|
|
84
|
+
schemaPath
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function scanDirectory(cwd, directory, depth, state, signal) {
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.code === "ENOENT") return;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
signal.throwIfAborted();
|
|
100
|
+
if (state.filesVisited >= MAX_SCAN_FILES) return;
|
|
101
|
+
const entryPath = path.join(directory, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!entry.isFile()) continue;
|
|
107
|
+
state.filesVisited += 1;
|
|
108
|
+
if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
|
|
109
|
+
if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
|
|
110
|
+
if (state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entry.name) && await fileContainsDatabaseUrl(entryPath, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function selectPrismaOrmSchema(cwd, candidates, signal) {
|
|
114
|
+
const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
|
|
115
|
+
for (const schemaPath of sorted) {
|
|
116
|
+
const target = await classifyPrismaOrmSchemaTarget(schemaPath, signal);
|
|
117
|
+
if (target === "postgresql" || target === "unknown") {
|
|
118
|
+
const hasMigrations = await hasMigrationsDirectory(path.dirname(schemaPath), signal);
|
|
119
|
+
return {
|
|
120
|
+
schema: {
|
|
121
|
+
kind: "prisma-orm",
|
|
122
|
+
path: schemaPath,
|
|
123
|
+
hasMigrations,
|
|
124
|
+
command: hasMigrations ? "migrate-deploy" : "db-push",
|
|
125
|
+
target
|
|
126
|
+
},
|
|
127
|
+
unsupportedSchema: null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
schema: null,
|
|
132
|
+
unsupportedSchema: {
|
|
133
|
+
kind: "prisma-orm",
|
|
134
|
+
path: schemaPath,
|
|
135
|
+
target
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
schema: null,
|
|
141
|
+
unsupportedSchema: null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function selectPrismaNextConfig(cwd, candidates, mode) {
|
|
145
|
+
const matches = candidates.filter((candidate) => {
|
|
146
|
+
const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
|
|
147
|
+
return mode === "supported" ? isSupported : !isSupported;
|
|
148
|
+
});
|
|
149
|
+
return sortByPreferredRelativePath(cwd, matches.map((candidate) => candidate.path), "prisma-next.config.ts").map((candidatePath) => matches.find((candidate) => candidate.path === candidatePath)).find((candidate) => Boolean(candidate)) ?? null;
|
|
150
|
+
}
|
|
151
|
+
function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
|
|
152
|
+
return candidates.map((candidate) => ({
|
|
153
|
+
absolute: candidate,
|
|
154
|
+
relative: path.relative(cwd, candidate) || preferredRootFile
|
|
155
|
+
})).sort((left, right) => {
|
|
156
|
+
if (left.relative === preferredRootFile) return -1;
|
|
157
|
+
if (right.relative === preferredRootFile) return 1;
|
|
158
|
+
return left.relative.length - right.relative.length || left.relative.localeCompare(right.relative);
|
|
159
|
+
}).map((candidate) => candidate.absolute);
|
|
160
|
+
}
|
|
161
|
+
async function hasMigrationsDirectory(schemaDirectory, signal) {
|
|
162
|
+
signal.throwIfAborted();
|
|
163
|
+
const migrationsPath = path.join(schemaDirectory, "migrations");
|
|
164
|
+
try {
|
|
165
|
+
await access(migrationsPath);
|
|
166
|
+
return (await readdir(migrationsPath)).length > 0;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code === "ENOENT") return false;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function classifyPrismaNextConfig(configPath, signal) {
|
|
173
|
+
const content = await readTextFileIfSmall(configPath, signal);
|
|
174
|
+
if (!content) return {
|
|
175
|
+
path: configPath,
|
|
176
|
+
target: "unknown"
|
|
177
|
+
};
|
|
178
|
+
if (content.includes("@prisma-next/postgres/config")) return {
|
|
179
|
+
path: configPath,
|
|
180
|
+
target: "postgresql"
|
|
181
|
+
};
|
|
182
|
+
if (content.includes("@prisma-next/mongo/config")) return {
|
|
183
|
+
path: configPath,
|
|
184
|
+
target: "mongodb"
|
|
185
|
+
};
|
|
186
|
+
if (content.includes("@prisma-next/sqlite/config")) return {
|
|
187
|
+
path: configPath,
|
|
188
|
+
target: "sqlite"
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
path: configPath,
|
|
192
|
+
target: "unknown"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function classifyPrismaOrmSchemaTarget(schemaPath, signal) {
|
|
196
|
+
switch ((await readTextFileIfSmall(schemaPath, signal))?.match(/\bprovider\s*=\s*"([^"]+)"/)?.[1] ?? null) {
|
|
197
|
+
case "postgresql": return "postgresql";
|
|
198
|
+
case "mongodb": return "mongodb";
|
|
199
|
+
case "mysql": return "mysql";
|
|
200
|
+
case "sqlite": return "sqlite";
|
|
201
|
+
case "sqlserver": return "sqlserver";
|
|
202
|
+
case "cockroachdb": return "cockroachdb";
|
|
203
|
+
default: return "unknown";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function shouldScanForDatabaseUrl(fileName) {
|
|
207
|
+
if (fileName === ".env" || fileName.startsWith(".env.")) return true;
|
|
208
|
+
return DATABASE_URL_SCAN_EXTENSIONS.has(path.extname(fileName));
|
|
209
|
+
}
|
|
210
|
+
function isPrismaNextConfigFile(fileName) {
|
|
211
|
+
if (!fileName.startsWith("prisma-next.config.")) return false;
|
|
212
|
+
return [
|
|
213
|
+
".cjs",
|
|
214
|
+
".cts",
|
|
215
|
+
".js",
|
|
216
|
+
".mjs",
|
|
217
|
+
".mts",
|
|
218
|
+
".ts"
|
|
219
|
+
].some((extension) => fileName.endsWith(extension));
|
|
220
|
+
}
|
|
221
|
+
async function fileContainsDatabaseUrl(filePath, signal) {
|
|
222
|
+
return (await readTextFileIfSmall(filePath, signal))?.includes("DATABASE_URL") ?? false;
|
|
223
|
+
}
|
|
224
|
+
async function readTextFileIfSmall(filePath, signal) {
|
|
225
|
+
signal.throwIfAborted();
|
|
226
|
+
if ((await stat(filePath)).size > MAX_TEXT_FILE_BYTES) return null;
|
|
227
|
+
return readFile(filePath, {
|
|
228
|
+
encoding: "utf8",
|
|
229
|
+
signal
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function buildSchemaSetupCommands(schema, schemaPath, databaseUrl) {
|
|
233
|
+
if (schema.command === "migrate-deploy") return [{
|
|
234
|
+
args: [
|
|
235
|
+
"--no-install",
|
|
236
|
+
"prisma",
|
|
237
|
+
"migrate",
|
|
238
|
+
"deploy",
|
|
239
|
+
"--schema",
|
|
240
|
+
schemaPath
|
|
241
|
+
],
|
|
242
|
+
displayCommand: "npx --no-install prisma migrate deploy"
|
|
243
|
+
}];
|
|
244
|
+
if (schema.command === "db-push") return [{
|
|
245
|
+
args: [
|
|
246
|
+
"--no-install",
|
|
247
|
+
"prisma",
|
|
248
|
+
"db",
|
|
249
|
+
"push",
|
|
250
|
+
"--schema",
|
|
251
|
+
schemaPath
|
|
252
|
+
],
|
|
253
|
+
displayCommand: "npx --no-install prisma db push"
|
|
254
|
+
}];
|
|
255
|
+
return [{
|
|
256
|
+
args: [
|
|
257
|
+
"--no-install",
|
|
258
|
+
"prisma-next",
|
|
259
|
+
"contract",
|
|
260
|
+
"emit",
|
|
261
|
+
"--config",
|
|
262
|
+
schemaPath
|
|
263
|
+
],
|
|
264
|
+
displayCommand: "npx --no-install prisma-next contract emit"
|
|
265
|
+
}, {
|
|
266
|
+
args: [
|
|
267
|
+
"--no-install",
|
|
268
|
+
"prisma-next",
|
|
269
|
+
"db",
|
|
270
|
+
"init",
|
|
271
|
+
"--config",
|
|
272
|
+
schemaPath,
|
|
273
|
+
"--db",
|
|
274
|
+
databaseUrl
|
|
275
|
+
],
|
|
276
|
+
displayCommand: "npx --no-install prisma-next db init"
|
|
277
|
+
}];
|
|
278
|
+
}
|
|
279
|
+
function defaultSchemaSourcePath(schema) {
|
|
280
|
+
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
281
|
+
}
|
|
282
|
+
async function runPrismaCommand(options) {
|
|
283
|
+
const shouldPipeOutput = !options.context.flags.json && !options.context.flags.quiet;
|
|
284
|
+
const child = spawn("npx", options.args, {
|
|
285
|
+
cwd: options.context.runtime.cwd,
|
|
286
|
+
env: {
|
|
287
|
+
...options.context.runtime.env,
|
|
288
|
+
...options.env
|
|
289
|
+
},
|
|
290
|
+
signal: options.context.runtime.signal,
|
|
291
|
+
stdio: shouldPipeOutput ? [
|
|
292
|
+
"ignore",
|
|
293
|
+
"pipe",
|
|
294
|
+
"pipe"
|
|
295
|
+
] : [
|
|
296
|
+
"ignore",
|
|
297
|
+
"ignore",
|
|
298
|
+
"ignore"
|
|
299
|
+
]
|
|
300
|
+
});
|
|
301
|
+
if (shouldPipeOutput) {
|
|
302
|
+
child.stdout?.pipe(options.context.output.stderr, { end: false });
|
|
303
|
+
child.stderr?.pipe(options.context.output.stderr, { end: false });
|
|
304
|
+
}
|
|
305
|
+
const exit = await new Promise((resolve, reject) => {
|
|
306
|
+
child.once("error", reject);
|
|
307
|
+
child.once("close", (code, signal) => resolve({
|
|
308
|
+
code,
|
|
309
|
+
signal
|
|
310
|
+
}));
|
|
311
|
+
});
|
|
312
|
+
if (exit.signal) throw new Error(`${options.displayCommand} was terminated by ${exit.signal}.`);
|
|
313
|
+
if (exit.code !== 0) throw new Error(`${options.displayCommand} exited with code ${exit.code ?? 1}.`);
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/lib/app/preview-branch-database.ts
|
|
2
|
+
async function createBranchDatabase(client, options) {
|
|
3
|
+
const result = await client.POST("/v1/databases", {
|
|
4
|
+
body: {
|
|
5
|
+
projectId: options.projectId,
|
|
6
|
+
branchId: options.branchId,
|
|
7
|
+
name: options.branchName,
|
|
8
|
+
source: { type: "empty" }
|
|
9
|
+
},
|
|
10
|
+
signal: options.signal
|
|
11
|
+
});
|
|
12
|
+
if (result.error || !result.data) throw apiCallError(`Failed to create database for branch "${options.branchName}"`, result.response, result.error);
|
|
13
|
+
return normalizeBranchDatabaseRecord(result.data.data);
|
|
14
|
+
}
|
|
15
|
+
async function listEnvironmentVariables(client, options) {
|
|
16
|
+
const variables = [];
|
|
17
|
+
let cursor;
|
|
18
|
+
while (true) {
|
|
19
|
+
const result = await client.GET("/v1/environment-variables", {
|
|
20
|
+
params: { query: {
|
|
21
|
+
projectId: options.projectId,
|
|
22
|
+
class: options.className,
|
|
23
|
+
key: options.key,
|
|
24
|
+
branchId: options.branchId,
|
|
25
|
+
cursor
|
|
26
|
+
} },
|
|
27
|
+
signal: options.signal
|
|
28
|
+
});
|
|
29
|
+
if (result.error || !result.data) throw apiCallError("Failed to list environment variables", result.response, result.error);
|
|
30
|
+
variables.push(...result.data.data);
|
|
31
|
+
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
32
|
+
cursor = result.data.pagination.nextCursor;
|
|
33
|
+
}
|
|
34
|
+
return variables.map((variable) => normalizeEnvironmentVariable(variable));
|
|
35
|
+
}
|
|
36
|
+
async function createEnvironmentVariable(client, options) {
|
|
37
|
+
const result = await client.POST("/v1/environment-variables", {
|
|
38
|
+
body: {
|
|
39
|
+
projectId: options.projectId,
|
|
40
|
+
class: options.className,
|
|
41
|
+
key: options.key,
|
|
42
|
+
value: options.value,
|
|
43
|
+
...options.branchId ? { branchId: options.branchId } : {}
|
|
44
|
+
},
|
|
45
|
+
signal: options.signal
|
|
46
|
+
});
|
|
47
|
+
if (result.error || !result.data) throw apiCallError(`Failed to add ${options.key}`, result.response, result.error);
|
|
48
|
+
return normalizeEnvironmentVariable(result.data.data);
|
|
49
|
+
}
|
|
50
|
+
async function deleteBranchDatabase(client, options) {
|
|
51
|
+
const result = await client.DELETE("/v1/databases/{databaseId}", {
|
|
52
|
+
params: { path: { databaseId: options.databaseId } },
|
|
53
|
+
signal: options.signal
|
|
54
|
+
});
|
|
55
|
+
if (result.error) throw apiCallError("Failed to delete branch database", result.response, result.error);
|
|
56
|
+
}
|
|
57
|
+
async function updateEnvironmentVariable(client, options) {
|
|
58
|
+
const result = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
59
|
+
params: { path: { envVarId: options.envVarId } },
|
|
60
|
+
body: { value: options.value },
|
|
61
|
+
signal: options.signal
|
|
62
|
+
});
|
|
63
|
+
if (result.error || !result.data) throw apiCallError("Failed to update environment variable", result.response, result.error);
|
|
64
|
+
return normalizeEnvironmentVariable(result.data.data);
|
|
65
|
+
}
|
|
66
|
+
async function deleteEnvironmentVariable(client, options) {
|
|
67
|
+
const result = await client.DELETE("/v1/environment-variables/{envVarId}", {
|
|
68
|
+
params: { path: { envVarId: options.envVarId } },
|
|
69
|
+
signal: options.signal
|
|
70
|
+
});
|
|
71
|
+
if (result.error) throw apiCallError("Failed to delete environment variable", result.response, result.error);
|
|
72
|
+
}
|
|
73
|
+
function normalizeEnvironmentVariable(variable) {
|
|
74
|
+
return {
|
|
75
|
+
id: variable.id,
|
|
76
|
+
key: variable.key,
|
|
77
|
+
branchId: variable.branchId,
|
|
78
|
+
className: variable.class,
|
|
79
|
+
isManagedBySystem: variable.isManagedBySystem
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function normalizeBranchDatabaseRecord(database) {
|
|
83
|
+
const connection = database.connections?.[0];
|
|
84
|
+
const databaseUrl = connection?.endpoints?.pooled?.connectionString;
|
|
85
|
+
const directUrl = connection?.endpoints?.direct?.connectionString ?? null;
|
|
86
|
+
if (!databaseUrl) throw new Error("Created database did not return a pooled connection string.");
|
|
87
|
+
return {
|
|
88
|
+
id: database.id,
|
|
89
|
+
name: database.name,
|
|
90
|
+
branchId: database.branchId,
|
|
91
|
+
databaseUrl,
|
|
92
|
+
directUrl
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function apiCallError(summary, response, error) {
|
|
96
|
+
if (response.status === 404) return /* @__PURE__ */ new Error("Resource Not Found");
|
|
97
|
+
const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`;
|
|
98
|
+
const hint = error.error?.hint ? ` ${error.error.hint}` : "";
|
|
99
|
+
return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { envVarNames } from "./env-vars.js";
|
|
2
2
|
import { PreviewBuildStrategy } from "./preview-build.js";
|
|
3
|
+
import { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable } from "./preview-branch-database.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
|
|
5
6
|
//#region src/lib/app/preview-provider.ts
|
|
@@ -48,6 +49,24 @@ function createPreviewAppProvider(client, options) {
|
|
|
48
49
|
role: branch.role
|
|
49
50
|
};
|
|
50
51
|
},
|
|
52
|
+
async createBranchDatabase(options) {
|
|
53
|
+
return createBranchDatabase(client, options);
|
|
54
|
+
},
|
|
55
|
+
async deleteBranchDatabase(options) {
|
|
56
|
+
return deleteBranchDatabase(client, options);
|
|
57
|
+
},
|
|
58
|
+
async listEnvironmentVariables(options) {
|
|
59
|
+
return listEnvironmentVariables(client, options);
|
|
60
|
+
},
|
|
61
|
+
async createEnvironmentVariable(options) {
|
|
62
|
+
return createEnvironmentVariable(client, options);
|
|
63
|
+
},
|
|
64
|
+
async updateEnvironmentVariable(options) {
|
|
65
|
+
return updateEnvironmentVariable(client, options);
|
|
66
|
+
},
|
|
67
|
+
async deleteEnvironmentVariable(options) {
|
|
68
|
+
return deleteEnvironmentVariable(client, options);
|
|
69
|
+
},
|
|
51
70
|
async removeApp(appId, options) {
|
|
52
71
|
const appResult = await sdk.showService({
|
|
53
72
|
serviceId: appId,
|
package/dist/presenters/app.js
CHANGED
|
@@ -30,6 +30,7 @@ function renderAppDeploy(context, descriptor, result) {
|
|
|
30
30
|
return [
|
|
31
31
|
`Live in ${formatDuration(result.durationMs)}`,
|
|
32
32
|
...result.deployment.url ? [context.ui.link(result.deployment.url)] : [],
|
|
33
|
+
...renderBranchDatabaseDeploySummary(context, result),
|
|
33
34
|
"",
|
|
34
35
|
...renderDeployOutputRows(context.ui, [{
|
|
35
36
|
label: "Logs",
|
|
@@ -41,6 +42,30 @@ function serializeAppDeploy(result) {
|
|
|
41
42
|
const { localPin: _localPin, ...serialized } = result;
|
|
42
43
|
return serialized;
|
|
43
44
|
}
|
|
45
|
+
function renderBranchDatabaseDeploySummary(context, result) {
|
|
46
|
+
if (!result.branchDatabase || result.branchDatabase.status !== "created") return [];
|
|
47
|
+
return ["", ...renderDeployOutputRows(context.ui, [
|
|
48
|
+
{
|
|
49
|
+
label: "Database",
|
|
50
|
+
value: result.branchDatabase.database?.name ?? "created"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Env",
|
|
54
|
+
value: result.branchDatabase.envVars.join(", ")
|
|
55
|
+
},
|
|
56
|
+
...result.branchDatabase.schema ? [{
|
|
57
|
+
label: "Schema",
|
|
58
|
+
value: formatBranchDatabaseSchemaCommand(result.branchDatabase.schema.command)
|
|
59
|
+
}] : []
|
|
60
|
+
])];
|
|
61
|
+
}
|
|
62
|
+
function formatBranchDatabaseSchemaCommand(command) {
|
|
63
|
+
switch (command) {
|
|
64
|
+
case "migrate-deploy": return "prisma migrate deploy";
|
|
65
|
+
case "db-push": return "prisma db push";
|
|
66
|
+
case "prisma-next-db-init": return "prisma-next db init";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
44
69
|
function formatDuration(durationMs) {
|
|
45
70
|
if (durationMs < 1e3) return `${durationMs}ms`;
|
|
46
71
|
return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
@@ -194,6 +194,8 @@ const DESCRIPTORS = [
|
|
|
194
194
|
"prisma-cli app deploy --project proj_123",
|
|
195
195
|
"prisma-cli app deploy --create-project my-app --yes",
|
|
196
196
|
"prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example",
|
|
197
|
+
"prisma-cli app deploy --db",
|
|
198
|
+
"prisma-cli app deploy --db --yes",
|
|
197
199
|
"prisma-cli app deploy --app my-app --framework nextjs --http-port 3000",
|
|
198
200
|
"prisma-cli app deploy --branch feat-login --framework hono",
|
|
199
201
|
"prisma-cli app deploy --prod --yes",
|