@prisma/cli 3.0.0-beta.2 → 3.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -16
- package/dist/adapters/git.js +8 -3
- package/dist/adapters/local-state.js +26 -7
- package/dist/adapters/mock-api.js +81 -2
- package/dist/adapters/token-storage.js +344 -25
- package/dist/cli.js +18 -3
- package/dist/cli2.js +12 -4
- package/dist/commands/agent/index.js +60 -0
- package/dist/commands/app/index.js +90 -61
- package/dist/commands/auth/index.js +55 -2
- package/dist/commands/branch/index.js +2 -27
- package/dist/commands/build/index.js +29 -0
- package/dist/commands/database/index.js +159 -0
- package/dist/commands/env.js +8 -4
- package/dist/commands/git/index.js +1 -1
- package/dist/commands/project/index.js +1 -1
- package/dist/controllers/agent.js +228 -0
- package/dist/controllers/app-env-api.js +54 -0
- package/dist/controllers/app-env-file.js +181 -0
- package/dist/controllers/app-env.js +286 -131
- package/dist/controllers/app.js +849 -309
- package/dist/controllers/auth.js +255 -11
- package/dist/controllers/branch.js +78 -48
- package/dist/controllers/build.js +88 -0
- package/dist/controllers/database.js +318 -0
- package/dist/controllers/project.js +156 -87
- package/dist/controllers/select-prompt-port.js +1 -0
- package/dist/lib/agent/cli-command.js +17 -0
- package/dist/lib/agent/constants.js +12 -0
- package/dist/lib/agent/package-manager.js +99 -0
- package/dist/lib/agent/setup-status.js +83 -0
- package/dist/lib/app/app-interaction.js +5 -0
- package/dist/lib/app/{preview-provider.js → app-provider.js} +220 -104
- package/dist/lib/app/branch-database-api.js +102 -0
- package/dist/lib/app/branch-database-deploy.js +326 -0
- package/dist/lib/app/branch-database.js +216 -0
- package/dist/lib/app/build-settings.js +93 -0
- package/dist/lib/app/build.js +82 -0
- package/dist/lib/app/bun-project.js +15 -9
- package/dist/lib/app/compute-config.js +145 -0
- package/dist/lib/app/deploy-plan.js +59 -0
- package/dist/lib/app/{preview-progress.js → deploy-progress.js} +12 -12
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/env-vars.js +28 -2
- package/dist/lib/app/local-dev.js +8 -49
- package/dist/lib/app/production-deploy-gate.js +162 -0
- package/dist/lib/app/read-branch.js +30 -0
- package/dist/lib/auth/auth-ops.js +38 -23
- package/dist/lib/auth/guard.js +6 -2
- package/dist/lib/auth/login.js +117 -15
- package/dist/lib/database/provider.js +167 -0
- package/dist/lib/diagnostics.js +15 -0
- package/dist/lib/fs/home-path.js +24 -0
- package/dist/lib/git/local-branch.js +53 -0
- package/dist/lib/git/local-status.js +57 -0
- package/dist/lib/project/interactive-setup.js +2 -1
- package/dist/lib/project/local-pin.js +183 -34
- package/dist/lib/project/resolution.js +206 -50
- package/dist/lib/project/setup.js +64 -18
- package/dist/output/patterns.js +1 -1
- package/dist/presenters/agent.js +74 -0
- package/dist/presenters/app-env.js +149 -14
- package/dist/presenters/app.js +187 -26
- package/dist/presenters/auth.js +99 -2
- package/dist/presenters/branch.js +37 -102
- package/dist/presenters/database.js +274 -0
- package/dist/presenters/project.js +44 -26
- package/dist/presenters/verbose-context.js +64 -0
- package/dist/shell/cli-command.js +12 -0
- package/dist/shell/command-arguments.js +7 -1
- package/dist/shell/command-meta.js +243 -15
- package/dist/shell/command-runner.js +60 -21
- package/dist/shell/diagnostics-output.js +57 -0
- package/dist/shell/errors.js +61 -1
- package/dist/shell/help.js +31 -20
- package/dist/shell/output.js +10 -1
- package/dist/shell/prompt.js +7 -3
- package/dist/shell/runtime.js +10 -6
- package/dist/shell/ui.js +42 -3
- package/dist/shell/update-check.js +247 -0
- package/dist/use-cases/auth.js +68 -1
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/package.json +27 -10
- package/dist/lib/app/preview-build.js +0 -284
- package/dist/lib/app/preview-interaction.js +0 -5
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/lib/app/branch-database-api.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 };
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
2
|
+
import { CliError, usageError } from "../../shell/errors.js";
|
|
3
|
+
import { renderSummaryLine } from "../../shell/ui.js";
|
|
4
|
+
import { canPrompt } from "../../shell/runtime.js";
|
|
5
|
+
import { confirmPrompt } from "../../shell/prompt.js";
|
|
6
|
+
import "../project/setup.js";
|
|
7
|
+
import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal } from "./branch-database.js";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
//#region src/lib/app/branch-database-deploy.ts
|
|
10
|
+
async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
|
|
11
|
+
if (options.db === false) return emptyBranchDatabaseSetupOutcome();
|
|
12
|
+
if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
|
|
13
|
+
if (options.db === true) throw usageError("Database setup cannot be combined with provided database env vars", "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", "Remove the --env database value to let --db create and wire a database, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
|
|
14
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
15
|
+
}
|
|
16
|
+
if (branch.kind === "production" && !options.firstProductionDeploy) {
|
|
17
|
+
if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
|
|
18
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
19
|
+
}
|
|
20
|
+
const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
|
|
21
|
+
const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
|
|
22
|
+
if (hasExistingDatabaseEnvForTarget(branch, envState)) {
|
|
23
|
+
const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
|
|
24
|
+
if (warning) emitBranchDatabaseWarning(context, warning);
|
|
25
|
+
return {
|
|
26
|
+
result: options.db === true ? {
|
|
27
|
+
status: "skipped",
|
|
28
|
+
reason: existingDatabaseEnvReason(branch),
|
|
29
|
+
envVars: targetEnvVars
|
|
30
|
+
} : void 0,
|
|
31
|
+
warnings: warning ? [warning] : []
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const localSignal = await inspectBranchDatabaseSignal(options.projectDir, context.runtime.signal);
|
|
35
|
+
if (localSignal.unsupportedSchema) {
|
|
36
|
+
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
|
|
37
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
38
|
+
}
|
|
39
|
+
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
|
|
40
|
+
if (options.db !== true) {
|
|
41
|
+
if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
|
|
42
|
+
if (!canPrompt(context) || context.flags.yes) {
|
|
43
|
+
const warning = databasePromptSuppressedWarning(branch);
|
|
44
|
+
emitBranchDatabaseWarning(context, warning);
|
|
45
|
+
return {
|
|
46
|
+
result: void 0,
|
|
47
|
+
warnings: [warning]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
|
|
51
|
+
if (!await confirmPrompt({
|
|
52
|
+
input: context.runtime.stdin,
|
|
53
|
+
output: context.output.stderr,
|
|
54
|
+
signal: context.runtime.signal,
|
|
55
|
+
message: databasePromptMessage(branch),
|
|
56
|
+
initialValue: false
|
|
57
|
+
})) return emptyBranchDatabaseSetupOutcome();
|
|
58
|
+
} else if (!canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
|
|
59
|
+
return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState, options.projectDir);
|
|
60
|
+
}
|
|
61
|
+
async function setupBranchDatabase(context, provider, projectId, branch, signal, envState, projectDir) {
|
|
62
|
+
emitBranchDatabaseProgress(context, "pending", "Creating database");
|
|
63
|
+
const database = await provider.createBranchDatabase({
|
|
64
|
+
projectId,
|
|
65
|
+
branchId: branch.id,
|
|
66
|
+
branchName: branch.name,
|
|
67
|
+
signal: context.runtime.signal
|
|
68
|
+
}).catch((error) => {
|
|
69
|
+
throw branchDatabaseSetupFailedError("Failed to create database", error, branch);
|
|
70
|
+
});
|
|
71
|
+
emitBranchDatabaseProgress(context, "success", "Created database");
|
|
72
|
+
try {
|
|
73
|
+
const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
|
|
74
|
+
emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")} — shared by every app on ${envScopeLabel(branch) === "production" ? "production" : `branch "${branch.name}"`}`);
|
|
75
|
+
const schemaCommand = signal.schema ? `DATABASE_URL=<url> npx ${formatSchemaSetupCommand(signal.schema.command)} (detected ${path.relative(projectDir, signal.schema.path) || signal.schema.path})` : "your own migration tooling";
|
|
76
|
+
const schemaSuggestion = `The new database is empty. Get a connection URL with \`prisma-cli database connection create ${database.id}\`, then apply your schema with ${schemaCommand}.`;
|
|
77
|
+
emitBranchDatabaseWarning(context, schemaSuggestion);
|
|
78
|
+
return {
|
|
79
|
+
result: {
|
|
80
|
+
status: "created",
|
|
81
|
+
database: {
|
|
82
|
+
id: database.id,
|
|
83
|
+
name: database.name
|
|
84
|
+
},
|
|
85
|
+
envVars
|
|
86
|
+
},
|
|
87
|
+
warnings: [schemaSuggestion]
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState) {
|
|
94
|
+
const scope = envScopeForBranch(branch);
|
|
95
|
+
const written = [];
|
|
96
|
+
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
97
|
+
projectId,
|
|
98
|
+
...scope,
|
|
99
|
+
key: "DATABASE_URL",
|
|
100
|
+
value: database.databaseUrl,
|
|
101
|
+
existing: envState.targetDatabaseUrl,
|
|
102
|
+
branch
|
|
103
|
+
});
|
|
104
|
+
written.push("DATABASE_URL");
|
|
105
|
+
if (database.directUrl) {
|
|
106
|
+
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
107
|
+
projectId,
|
|
108
|
+
...scope,
|
|
109
|
+
key: "DIRECT_URL",
|
|
110
|
+
value: database.directUrl,
|
|
111
|
+
existing: envState.targetDirectUrl,
|
|
112
|
+
branch
|
|
113
|
+
});
|
|
114
|
+
written.push("DIRECT_URL");
|
|
115
|
+
} else if (branch.kind === "preview" && envState.targetDirectUrl) await provider.deleteEnvironmentVariable({
|
|
116
|
+
envVarId: envState.targetDirectUrl.id,
|
|
117
|
+
signal: context.runtime.signal
|
|
118
|
+
}).catch((error) => {
|
|
119
|
+
throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch);
|
|
120
|
+
});
|
|
121
|
+
return written;
|
|
122
|
+
}
|
|
123
|
+
async function upsertBranchDatabaseEnvVar(context, provider, options) {
|
|
124
|
+
if (options.existing) {
|
|
125
|
+
await provider.updateEnvironmentVariable({
|
|
126
|
+
envVarId: options.existing.id,
|
|
127
|
+
value: options.value,
|
|
128
|
+
signal: context.runtime.signal
|
|
129
|
+
}).catch((error) => {
|
|
130
|
+
throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branch);
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
await provider.createEnvironmentVariable({
|
|
135
|
+
projectId: options.projectId,
|
|
136
|
+
className: options.className,
|
|
137
|
+
key: options.key,
|
|
138
|
+
value: options.value,
|
|
139
|
+
...options.branchId ? { branchId: options.branchId } : {},
|
|
140
|
+
signal: context.runtime.signal
|
|
141
|
+
}).catch((error) => {
|
|
142
|
+
throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branch);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async function inspectBranchDatabaseEnv(provider, projectId, branch, signal) {
|
|
146
|
+
const scope = envScopeForBranch(branch);
|
|
147
|
+
const [databaseUrlRows, directUrlRows] = await Promise.all([provider.listEnvironmentVariables({
|
|
148
|
+
projectId,
|
|
149
|
+
className: scope.className,
|
|
150
|
+
key: "DATABASE_URL",
|
|
151
|
+
signal
|
|
152
|
+
}), provider.listEnvironmentVariables({
|
|
153
|
+
projectId,
|
|
154
|
+
className: scope.className,
|
|
155
|
+
key: "DIRECT_URL",
|
|
156
|
+
signal
|
|
157
|
+
})]);
|
|
158
|
+
const targetBranchId = branch.kind === "preview" ? branch.id : null;
|
|
159
|
+
return {
|
|
160
|
+
targetDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: targetBranchId }),
|
|
161
|
+
targetDirectUrl: findEnvVar(directUrlRows, { branchId: targetBranchId }),
|
|
162
|
+
inheritedPreviewDatabaseUrl: branch.kind === "preview" ? findEnvVar(databaseUrlRows, { branchId: null }) : null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function findEnvVar(rows, options) {
|
|
166
|
+
return rows.find((row) => row.branchId === options.branchId) ?? null;
|
|
167
|
+
}
|
|
168
|
+
function hasProvidedDatabaseEnvVars(envVars) {
|
|
169
|
+
return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
|
|
170
|
+
}
|
|
171
|
+
function envScopeForBranch(branch) {
|
|
172
|
+
return branch.kind === "production" ? { className: "production" } : {
|
|
173
|
+
className: "preview",
|
|
174
|
+
branchId: branch.id
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function envScopeLabel(branch) {
|
|
178
|
+
return branch.kind === "production" ? "production" : "branch";
|
|
179
|
+
}
|
|
180
|
+
function getTargetDatabaseEnvVarKeys(envState) {
|
|
181
|
+
return [envState.targetDatabaseUrl, envState.targetDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
|
|
182
|
+
}
|
|
183
|
+
function hasExistingDatabaseEnvForTarget(branch, envState) {
|
|
184
|
+
if (branch.kind === "production") return Boolean(envState.targetDatabaseUrl || envState.targetDirectUrl);
|
|
185
|
+
return Boolean(envState.targetDatabaseUrl);
|
|
186
|
+
}
|
|
187
|
+
function existingDatabaseEnvReason(branch) {
|
|
188
|
+
return branch.kind === "production" ? "production-env-exists" : "branch-env-exists";
|
|
189
|
+
}
|
|
190
|
+
function existingDatabaseEnvWarning(branch, envVars) {
|
|
191
|
+
if (branch.kind === "production") return `Production already has ${envVars.join(" and ")}. Treating it as BYO database configuration and leaving env vars unchanged.`;
|
|
192
|
+
return `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.`;
|
|
193
|
+
}
|
|
194
|
+
function databasePromptSuppressedWarning(branch) {
|
|
195
|
+
if (branch.kind === "production") return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db --yes to create and wire a Prisma Postgres database for this first production deploy.";
|
|
196
|
+
return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
|
|
197
|
+
}
|
|
198
|
+
function databasePromptMessage(branch) {
|
|
199
|
+
return branch.kind === "production" ? "Create a Prisma Postgres database for production?" : `Create an isolated database for branch "${branch.name}"?`;
|
|
200
|
+
}
|
|
201
|
+
function maybeRenderBranchDatabaseSignal(context, branch, signal, envState) {
|
|
202
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
203
|
+
const rows = [
|
|
204
|
+
signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
|
|
205
|
+
signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
|
|
206
|
+
envState.inheritedPreviewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
|
|
207
|
+
].filter((row) => Boolean(row));
|
|
208
|
+
context.output.stderr.write(`Database signal found for ${databaseTargetLabel(branch)}\n${rows.join("\n")}\n\n`);
|
|
209
|
+
}
|
|
210
|
+
function databaseTargetLabel(branch) {
|
|
211
|
+
return branch.kind === "production" ? `production branch "${branch.name}"` : `branch "${branch.name}"`;
|
|
212
|
+
}
|
|
213
|
+
function emitBranchDatabaseProgress(context, status, message) {
|
|
214
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
215
|
+
const line = status === "pending" ? `${context.ui.warning("◇")} ${message}...` : renderSummaryLine(context.ui, "success", message);
|
|
216
|
+
context.output.stderr.write(`${line}\n`);
|
|
217
|
+
}
|
|
218
|
+
function emitBranchDatabaseWarning(context, warning) {
|
|
219
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
220
|
+
context.output.stderr.write(`${renderSummaryLine(context.ui, "warning", warning)}\n`);
|
|
221
|
+
}
|
|
222
|
+
function emptyBranchDatabaseSetupOutcome() {
|
|
223
|
+
return {
|
|
224
|
+
result: void 0,
|
|
225
|
+
warnings: []
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function productionDatabaseSetupAfterFirstDeployError() {
|
|
229
|
+
return usageError("Database setup is only available during the first production deploy", "The selected production app already has a live deployment.", "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");
|
|
230
|
+
}
|
|
231
|
+
function nonInteractiveDatabaseSetupRequiresYesError(branch) {
|
|
232
|
+
return usageError("Database setup requires --yes in non-interactive mode", "The deploy command received --db, but prompts are not available and --yes was not passed.", "Pass --yes together with --db to confirm non-interactive database creation.", [branch.kind === "production" ? "prisma-cli app deploy --prod --db --yes" : `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db --yes`], "app");
|
|
233
|
+
}
|
|
234
|
+
function formatSchemaSetupCommand(command) {
|
|
235
|
+
switch (command) {
|
|
236
|
+
case "migrate-deploy": return "prisma migrate deploy";
|
|
237
|
+
case "db-push": return "prisma db push";
|
|
238
|
+
case "prisma-next-db-init": return "prisma-next db init";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function branchDatabaseSetupFailedError(summary, error, branch) {
|
|
242
|
+
return new CliError({
|
|
243
|
+
code: "BRANCH_DATABASE_SETUP_FAILED",
|
|
244
|
+
domain: "app",
|
|
245
|
+
summary,
|
|
246
|
+
why: error instanceof Error ? error.message : String(error),
|
|
247
|
+
fix: "Retry the command, or create the database and env vars manually with project env commands.",
|
|
248
|
+
debug: formatDebugDetails(error),
|
|
249
|
+
meta: { branch: branch.name },
|
|
250
|
+
exitCode: 1,
|
|
251
|
+
nextSteps: [formatAppDeployWithDbNextStep(branch), formatProjectEnvListNextStep(branch)]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error) {
|
|
255
|
+
const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Database setup failed", error, branch);
|
|
256
|
+
emitBranchDatabaseProgress(context, "pending", "Removing database after setup failed");
|
|
257
|
+
try {
|
|
258
|
+
await provider.deleteBranchDatabase({
|
|
259
|
+
databaseId: database.id,
|
|
260
|
+
signal: context.runtime.signal
|
|
261
|
+
});
|
|
262
|
+
emitBranchDatabaseProgress(context, "success", "Removed database after setup failed");
|
|
263
|
+
} catch (cleanupError) {
|
|
264
|
+
return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch);
|
|
265
|
+
}
|
|
266
|
+
return setupError;
|
|
267
|
+
}
|
|
268
|
+
function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch) {
|
|
269
|
+
const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
270
|
+
const setupWhy = setupError.why ?? "Database setup failed.";
|
|
271
|
+
return new CliError({
|
|
272
|
+
code: setupError.code,
|
|
273
|
+
domain: setupError.domain,
|
|
274
|
+
summary: setupError.summary,
|
|
275
|
+
why: `${setupWhy} Prisma could not delete the created database "${database.name}" (${database.id}): ${cleanupWhy}`,
|
|
276
|
+
fix: "Delete the created database from Console or contact Prisma support, then rerun deploy with --db.",
|
|
277
|
+
debug: formatCombinedDebugDetails(setupError, cleanupError),
|
|
278
|
+
meta: {
|
|
279
|
+
...setupError.meta,
|
|
280
|
+
branch: branch.name,
|
|
281
|
+
databaseId: database.id,
|
|
282
|
+
databaseName: database.name,
|
|
283
|
+
cleanupFailed: true
|
|
284
|
+
},
|
|
285
|
+
exitCode: setupError.exitCode,
|
|
286
|
+
nextSteps: []
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function unsupportedBranchDatabaseSchemaError(schema, branch, context) {
|
|
290
|
+
return usageError("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, or switch the Prisma schema source to PostgreSQL before using --db.", [formatProjectEnvAddNextStep(branch), `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)}`], "app");
|
|
291
|
+
}
|
|
292
|
+
function formatAppDeployWithDbNextStep(branch) {
|
|
293
|
+
return `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db`;
|
|
294
|
+
}
|
|
295
|
+
function formatProjectEnvListNextStep(branch) {
|
|
296
|
+
return branch.kind === "production" ? "prisma-cli project env list --role production" : `prisma-cli project env list --branch ${formatCommandArgument(branch.name)}`;
|
|
297
|
+
}
|
|
298
|
+
function formatProjectEnvAddNextStep(branch) {
|
|
299
|
+
return branch.kind === "production" ? "prisma-cli project env add DATABASE_URL=<value> --role production" : `prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branch.name)}`;
|
|
300
|
+
}
|
|
301
|
+
function defaultSchemaSourcePath(schema) {
|
|
302
|
+
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
303
|
+
}
|
|
304
|
+
function defaultUnsupportedSchemaSourcePath(schema) {
|
|
305
|
+
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
306
|
+
}
|
|
307
|
+
function formatUnsupportedSchemaTarget(target) {
|
|
308
|
+
switch (target) {
|
|
309
|
+
case "cockroachdb": return "CockroachDB";
|
|
310
|
+
case "mongodb": return "MongoDB";
|
|
311
|
+
case "mysql": return "MySQL";
|
|
312
|
+
case "sqlite": return "SQLite";
|
|
313
|
+
case "sqlserver": return "SQL Server";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function formatDebugDetails(error) {
|
|
317
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
318
|
+
return typeof error === "string" ? error : null;
|
|
319
|
+
}
|
|
320
|
+
function formatCombinedDebugDetails(setupError, cleanupError) {
|
|
321
|
+
const setupDebug = setupError.debug ?? setupError.stack ?? setupError.message;
|
|
322
|
+
const cleanupDebug = formatDebugDetails(cleanupError);
|
|
323
|
+
return [setupDebug ? `Setup error:\n${setupDebug}` : null, cleanupDebug ? `Cleanup error:\n${cleanupDebug}` : null].filter((line) => Boolean(line)).join("\n\n") || null;
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
export { maybeSetupBranchDatabase };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
+
//#region src/lib/app/branch-database.ts
|
|
4
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
".agents",
|
|
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 scanDirectory(cwd, directory, depth, state, signal) {
|
|
71
|
+
signal.throwIfAborted();
|
|
72
|
+
if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code === "ENOENT") return;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
signal.throwIfAborted();
|
|
83
|
+
if (state.filesVisited >= MAX_SCAN_FILES) return;
|
|
84
|
+
const entryPath = path.join(directory, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!entry.isFile()) continue;
|
|
90
|
+
state.filesVisited += 1;
|
|
91
|
+
if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
|
|
92
|
+
if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
|
|
93
|
+
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);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function selectPrismaOrmSchema(cwd, candidates, signal) {
|
|
97
|
+
const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
|
|
98
|
+
for (const schemaPath of sorted) {
|
|
99
|
+
const target = await classifyPrismaOrmSchemaTarget(schemaPath, signal);
|
|
100
|
+
if (target === "postgresql" || target === "unknown") {
|
|
101
|
+
const hasMigrations = await hasMigrationsDirectory(path.dirname(schemaPath), signal);
|
|
102
|
+
return {
|
|
103
|
+
schema: {
|
|
104
|
+
kind: "prisma-orm",
|
|
105
|
+
path: schemaPath,
|
|
106
|
+
hasMigrations,
|
|
107
|
+
command: hasMigrations ? "migrate-deploy" : "db-push",
|
|
108
|
+
target
|
|
109
|
+
},
|
|
110
|
+
unsupportedSchema: null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
schema: null,
|
|
115
|
+
unsupportedSchema: {
|
|
116
|
+
kind: "prisma-orm",
|
|
117
|
+
path: schemaPath,
|
|
118
|
+
target
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
schema: null,
|
|
124
|
+
unsupportedSchema: null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function selectPrismaNextConfig(cwd, candidates, mode) {
|
|
128
|
+
const matches = candidates.filter((candidate) => {
|
|
129
|
+
const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
|
|
130
|
+
return mode === "supported" ? isSupported : !isSupported;
|
|
131
|
+
});
|
|
132
|
+
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;
|
|
133
|
+
}
|
|
134
|
+
function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
|
|
135
|
+
return candidates.map((candidate) => ({
|
|
136
|
+
absolute: candidate,
|
|
137
|
+
relative: path.relative(cwd, candidate) || preferredRootFile
|
|
138
|
+
})).sort((left, right) => {
|
|
139
|
+
if (left.relative === preferredRootFile) return -1;
|
|
140
|
+
if (right.relative === preferredRootFile) return 1;
|
|
141
|
+
return left.relative.length - right.relative.length || left.relative.localeCompare(right.relative);
|
|
142
|
+
}).map((candidate) => candidate.absolute);
|
|
143
|
+
}
|
|
144
|
+
async function hasMigrationsDirectory(schemaDirectory, signal) {
|
|
145
|
+
signal.throwIfAborted();
|
|
146
|
+
const migrationsPath = path.join(schemaDirectory, "migrations");
|
|
147
|
+
try {
|
|
148
|
+
await access(migrationsPath);
|
|
149
|
+
return (await readdir(migrationsPath)).length > 0;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error.code === "ENOENT") return false;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function classifyPrismaNextConfig(configPath, signal) {
|
|
156
|
+
const content = await readTextFileIfSmall(configPath, signal);
|
|
157
|
+
if (!content) return {
|
|
158
|
+
path: configPath,
|
|
159
|
+
target: "unknown"
|
|
160
|
+
};
|
|
161
|
+
if (content.includes("@prisma-next/postgres/config")) return {
|
|
162
|
+
path: configPath,
|
|
163
|
+
target: "postgresql"
|
|
164
|
+
};
|
|
165
|
+
if (content.includes("@prisma-next/mongo/config")) return {
|
|
166
|
+
path: configPath,
|
|
167
|
+
target: "mongodb"
|
|
168
|
+
};
|
|
169
|
+
if (content.includes("@prisma-next/sqlite/config")) return {
|
|
170
|
+
path: configPath,
|
|
171
|
+
target: "sqlite"
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
path: configPath,
|
|
175
|
+
target: "unknown"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function classifyPrismaOrmSchemaTarget(schemaPath, signal) {
|
|
179
|
+
switch ((await readTextFileIfSmall(schemaPath, signal))?.match(/\bprovider\s*=\s*"([^"]+)"/)?.[1] ?? null) {
|
|
180
|
+
case "postgresql": return "postgresql";
|
|
181
|
+
case "mongodb": return "mongodb";
|
|
182
|
+
case "mysql": return "mysql";
|
|
183
|
+
case "sqlite": return "sqlite";
|
|
184
|
+
case "sqlserver": return "sqlserver";
|
|
185
|
+
case "cockroachdb": return "cockroachdb";
|
|
186
|
+
default: return "unknown";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function shouldScanForDatabaseUrl(fileName) {
|
|
190
|
+
if (fileName === ".env" || fileName.startsWith(".env.")) return true;
|
|
191
|
+
return DATABASE_URL_SCAN_EXTENSIONS.has(path.extname(fileName));
|
|
192
|
+
}
|
|
193
|
+
function isPrismaNextConfigFile(fileName) {
|
|
194
|
+
if (!fileName.startsWith("prisma-next.config.")) return false;
|
|
195
|
+
return [
|
|
196
|
+
".cjs",
|
|
197
|
+
".cts",
|
|
198
|
+
".js",
|
|
199
|
+
".mjs",
|
|
200
|
+
".mts",
|
|
201
|
+
".ts"
|
|
202
|
+
].some((extension) => fileName.endsWith(extension));
|
|
203
|
+
}
|
|
204
|
+
async function fileContainsDatabaseUrl(filePath, signal) {
|
|
205
|
+
return (await readTextFileIfSmall(filePath, signal))?.includes("DATABASE_URL") ?? false;
|
|
206
|
+
}
|
|
207
|
+
async function readTextFileIfSmall(filePath, signal) {
|
|
208
|
+
signal.throwIfAborted();
|
|
209
|
+
if ((await stat(filePath)).size > MAX_TEXT_FILE_BYTES) return null;
|
|
210
|
+
return readFile(filePath, {
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
signal
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal };
|