@prisma/cli 3.0.0-dev.60.1 → 3.0.0-dev.62.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/README.md +1 -0
- package/dist/commands/env.js +8 -4
- package/dist/controllers/app-env-api.js +54 -0
- package/dist/controllers/app-env-file.js +179 -0
- package/dist/controllers/app-env.js +50 -67
- package/dist/lib/app/branch-database-deploy.js +42 -6
- package/dist/lib/app/branch-database.js +172 -40
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/presenters/app-env.js +30 -0
- package/dist/presenters/app.js +8 -1
- package/dist/shell/command-meta.js +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ Useful next commands:
|
|
|
57
57
|
npx prisma-cli app logs
|
|
58
58
|
npx prisma-cli app open
|
|
59
59
|
npx prisma-cli project env add DATABASE_URL=postgresql://example --role preview
|
|
60
|
+
npx prisma-cli project env add --file .env --role preview
|
|
60
61
|
npx prisma-cli project env list
|
|
61
62
|
npx prisma-cli project env list --role preview
|
|
62
63
|
```
|
package/dist/commands/env.js
CHANGED
|
@@ -17,16 +17,18 @@ function createEnvCommand(runtime) {
|
|
|
17
17
|
}
|
|
18
18
|
function createEnvAddCommand(runtime) {
|
|
19
19
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("add"), runtime), "project.env.add");
|
|
20
|
-
command.argument("
|
|
20
|
+
command.argument("[assignment]", "Variable assignment as KEY=VALUE or KEY from the current environment").addOption(new Option("--file <path>", "Read KEY=VALUE assignments from a dotenv file")).addOption(new Option("--role <role>", "Project template scope (production or preview)").choices(["production", "preview"])).addOption(new Option("--branch <git-name>", "Preview branch override scope")).addOption(new Option("--project <id-or-name>", "Project id or name"));
|
|
21
21
|
addGlobalFlags(command);
|
|
22
22
|
command.action(async (assignment, options) => {
|
|
23
23
|
const roleName = options.role;
|
|
24
24
|
const branchName = options.branch;
|
|
25
25
|
const projectRef = options.project;
|
|
26
|
+
const filePath = options.file;
|
|
26
27
|
await runCommand(runtime, "project.env.add", options, (context) => runEnvAdd(context, assignment, {
|
|
27
28
|
roleName,
|
|
28
29
|
branchName,
|
|
29
|
-
projectRef
|
|
30
|
+
projectRef,
|
|
31
|
+
filePath
|
|
30
32
|
}), {
|
|
31
33
|
renderHuman: (context, descriptor, result) => renderEnvAdd(context, descriptor, result),
|
|
32
34
|
renderJson: (result) => serializeEnvAdd(result)
|
|
@@ -36,16 +38,18 @@ function createEnvAddCommand(runtime) {
|
|
|
36
38
|
}
|
|
37
39
|
function createEnvUpdateCommand(runtime) {
|
|
38
40
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("update"), runtime), "project.env.update");
|
|
39
|
-
command.argument("
|
|
41
|
+
command.argument("[assignment]", "Variable assignment as KEY=VALUE or KEY from the current environment").addOption(new Option("--file <path>", "Read KEY=VALUE assignments from a dotenv file")).addOption(new Option("--role <role>", "Project template scope (production or preview)").choices(["production", "preview"])).addOption(new Option("--branch <git-name>", "Preview branch override scope")).addOption(new Option("--project <id-or-name>", "Project id or name"));
|
|
40
42
|
addGlobalFlags(command);
|
|
41
43
|
command.action(async (assignment, options) => {
|
|
42
44
|
const roleName = options.role;
|
|
43
45
|
const branchName = options.branch;
|
|
44
46
|
const projectRef = options.project;
|
|
47
|
+
const filePath = options.file;
|
|
45
48
|
await runCommand(runtime, "project.env.update", options, (context) => runEnvUpdate(context, assignment, {
|
|
46
49
|
roleName,
|
|
47
50
|
branchName,
|
|
48
|
-
projectRef
|
|
51
|
+
projectRef,
|
|
52
|
+
filePath
|
|
49
53
|
}), {
|
|
50
54
|
renderHuman: (context, descriptor, result) => renderEnvUpdate(context, descriptor, result),
|
|
51
55
|
renderJson: (result) => serializeEnvUpdate(result)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CliError, authRequiredError } from "../shell/errors.js";
|
|
2
|
+
//#region src/controllers/app-env-api.ts
|
|
3
|
+
async function findVariableByNaturalKey(client, projectId, key, resolved, signal) {
|
|
4
|
+
const { data, error, response } = await client.GET("/v1/environment-variables", {
|
|
5
|
+
params: { query: {
|
|
6
|
+
projectId,
|
|
7
|
+
class: resolved.apiTarget.class,
|
|
8
|
+
key
|
|
9
|
+
} },
|
|
10
|
+
signal
|
|
11
|
+
});
|
|
12
|
+
if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
|
|
13
|
+
return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
|
|
14
|
+
}
|
|
15
|
+
function toMetadata(row, requestedScope) {
|
|
16
|
+
const rowScope = row.branchId === null ? {
|
|
17
|
+
kind: "role",
|
|
18
|
+
role: row.class
|
|
19
|
+
} : requestedScope;
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
key: row.key,
|
|
23
|
+
scope: rowScope,
|
|
24
|
+
source: formatDescriptorLabel(rowScope),
|
|
25
|
+
isManagedBySystem: row.isManagedBySystem,
|
|
26
|
+
updatedAt: row.updatedAt
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function rowMatchesExactScope(row, resolved) {
|
|
30
|
+
return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
|
|
31
|
+
}
|
|
32
|
+
function apiCallError(summary, response, error) {
|
|
33
|
+
const status = response?.status ?? 0;
|
|
34
|
+
const apiCode = error?.error?.code;
|
|
35
|
+
const apiMessage = error?.error?.message;
|
|
36
|
+
const apiHint = error?.error?.hint;
|
|
37
|
+
if (status === 401 || status === 403) return authRequiredError(["prisma auth login"]);
|
|
38
|
+
return new CliError({
|
|
39
|
+
code: apiCode ?? "ENV_API_ERROR",
|
|
40
|
+
domain: "app",
|
|
41
|
+
summary,
|
|
42
|
+
why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
|
|
43
|
+
fix: apiHint ?? "Re-run with --trace for the underlying API response details.",
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
nextSteps: []
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function formatDescriptorLabel(scope) {
|
|
49
|
+
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
50
|
+
if (scope.kind === "overview") return "overview";
|
|
51
|
+
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { apiCallError, findVariableByNaturalKey, toMetadata };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { CliError } from "../shell/errors.js";
|
|
2
|
+
import { formatScopeLabel } from "../lib/app/env-config.js";
|
|
3
|
+
import { apiCallError, findVariableByNaturalKey, toMetadata } from "./app-env-api.js";
|
|
4
|
+
//#region src/controllers/app-env-file.ts
|
|
5
|
+
async function runEnvAddFile(context, client, projectId, resolved, filePath, assignments) {
|
|
6
|
+
const existing = await findVariablesByNaturalKey(client, projectId, assignments.map((assignment) => assignment.key), resolved, context.runtime.signal);
|
|
7
|
+
const existingKeys = assignments.map((assignment) => assignment.key).filter((key) => existing.has(key));
|
|
8
|
+
if (existingKeys.length > 0) throw new CliError({
|
|
9
|
+
code: "ENV_VARIABLE_ALREADY_EXISTS",
|
|
10
|
+
domain: "app",
|
|
11
|
+
summary: `${existingKeys.length} environment variable(s) already exist in ${formatScopeLabel(resolved.scope)}`,
|
|
12
|
+
why: `Existing keys: ${formatKeyList(existingKeys)}.`,
|
|
13
|
+
fix: "Split the input file by key state: update existing keys and add new keys separately.",
|
|
14
|
+
exitCode: 1,
|
|
15
|
+
nextSteps: splitFileNextSteps(filePath, resolved.scope, {
|
|
16
|
+
existingKeys,
|
|
17
|
+
first: "update-existing"
|
|
18
|
+
}),
|
|
19
|
+
meta: { keys: existingKeys }
|
|
20
|
+
});
|
|
21
|
+
const warnings = await missingPreviewDefaultWarnings(client, projectId, resolved.scope, assignments.map((assignment) => assignment.key), context.runtime.signal);
|
|
22
|
+
const variables = [];
|
|
23
|
+
for (const assignment of assignments) try {
|
|
24
|
+
const { data, error, response } = await client.POST("/v1/environment-variables", {
|
|
25
|
+
body: {
|
|
26
|
+
projectId,
|
|
27
|
+
class: resolved.apiTarget.class,
|
|
28
|
+
...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
|
|
29
|
+
key: assignment.key,
|
|
30
|
+
value: assignment.value
|
|
31
|
+
},
|
|
32
|
+
signal: context.runtime.signal
|
|
33
|
+
});
|
|
34
|
+
if (error || !data) throw apiCallError(`Failed to add ${assignment.key}`, response, error);
|
|
35
|
+
variables.push(toMetadata(data.data, resolved.descriptor));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw envFileApplyFailedError("add", filePath, resolved.scope, assignment.key, variables, error);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command: "project.env.add",
|
|
41
|
+
result: {
|
|
42
|
+
projectId,
|
|
43
|
+
scope: resolved.descriptor,
|
|
44
|
+
variables,
|
|
45
|
+
file: {
|
|
46
|
+
path: filePath,
|
|
47
|
+
count: variables.length
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
warnings,
|
|
51
|
+
nextSteps: []
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function runEnvUpdateFile(context, client, projectId, resolved, filePath, assignments) {
|
|
55
|
+
const existing = await findVariablesByNaturalKey(client, projectId, assignments.map((assignment) => assignment.key), resolved, context.runtime.signal);
|
|
56
|
+
const missingKeys = assignments.map((assignment) => assignment.key).filter((key) => !existing.has(key));
|
|
57
|
+
if (missingKeys.length > 0) throw new CliError({
|
|
58
|
+
code: "ENV_VARIABLE_NOT_FOUND",
|
|
59
|
+
domain: "app",
|
|
60
|
+
summary: `${missingKeys.length} environment variable(s) not found in ${formatScopeLabel(resolved.scope)}`,
|
|
61
|
+
why: `Missing keys: ${formatKeyList(missingKeys)}.`,
|
|
62
|
+
fix: "Split the input file by key state: add missing keys and update existing keys separately.",
|
|
63
|
+
exitCode: 1,
|
|
64
|
+
nextSteps: splitFileNextSteps(filePath, resolved.scope, {
|
|
65
|
+
missingKeys,
|
|
66
|
+
first: "add-missing"
|
|
67
|
+
}),
|
|
68
|
+
meta: { keys: missingKeys }
|
|
69
|
+
});
|
|
70
|
+
const variables = [];
|
|
71
|
+
for (const assignment of assignments) {
|
|
72
|
+
const existingVariable = existing.get(assignment.key);
|
|
73
|
+
if (!existingVariable) continue;
|
|
74
|
+
try {
|
|
75
|
+
const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
76
|
+
params: { path: { envVarId: existingVariable.id } },
|
|
77
|
+
body: { value: assignment.value },
|
|
78
|
+
signal: context.runtime.signal
|
|
79
|
+
});
|
|
80
|
+
if (error || !data) throw apiCallError(`Failed to update value for ${assignment.key}`, response, error);
|
|
81
|
+
variables.push(toMetadata(data.data, resolved.descriptor));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw envFileApplyFailedError("update", filePath, resolved.scope, assignment.key, variables, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
command: "project.env.update",
|
|
88
|
+
result: {
|
|
89
|
+
projectId,
|
|
90
|
+
scope: resolved.descriptor,
|
|
91
|
+
variables,
|
|
92
|
+
file: {
|
|
93
|
+
path: filePath,
|
|
94
|
+
count: variables.length
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
warnings: [],
|
|
98
|
+
nextSteps: []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function findVariablesByNaturalKey(client, projectId, keys, resolved, signal) {
|
|
102
|
+
const found = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
const row = await findVariableByNaturalKey(client, projectId, key, resolved, signal);
|
|
105
|
+
if (row) found.set(key, row);
|
|
106
|
+
}
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
109
|
+
async function missingPreviewDefaultWarnings(client, projectId, scope, keys, signal) {
|
|
110
|
+
if (scope.kind !== "branch") return [];
|
|
111
|
+
const previewScope = {
|
|
112
|
+
scope: {
|
|
113
|
+
kind: "role",
|
|
114
|
+
role: "preview"
|
|
115
|
+
},
|
|
116
|
+
descriptor: {
|
|
117
|
+
kind: "role",
|
|
118
|
+
role: "preview"
|
|
119
|
+
},
|
|
120
|
+
apiTarget: {
|
|
121
|
+
class: "preview",
|
|
122
|
+
branchId: null
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const missing = [];
|
|
126
|
+
for (const key of keys) if (!await findVariableByNaturalKey(client, projectId, key, previewScope, signal)) missing.push(key);
|
|
127
|
+
if (missing.length === 0) return [];
|
|
128
|
+
if (missing.length === 1) return [`Variable "${missing[0]}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`];
|
|
129
|
+
return [`Variables ${formatKeyList(missing)} do not exist in preview. They will only exist on ${formatScopeLabel(scope)}.`];
|
|
130
|
+
}
|
|
131
|
+
function envFileApplyFailedError(command, filePath, scope, failedKey, writtenVariables, error) {
|
|
132
|
+
const writtenKeys = writtenVariables.map((variable) => variable.key);
|
|
133
|
+
const cause = error instanceof CliError ? error.summary : error instanceof Error ? error.message : "Unknown error.";
|
|
134
|
+
return new CliError({
|
|
135
|
+
code: "ENV_FILE_APPLY_FAILED",
|
|
136
|
+
domain: "app",
|
|
137
|
+
summary: `Failed to ${command} "${failedKey}" from "${filePath}"`,
|
|
138
|
+
why: writtenKeys.length === 0 ? `No variables were written before ${failedKey} failed. Cause: ${cause}` : `Written keys before failure: ${formatKeyList(writtenKeys)}. Cause: ${cause}`,
|
|
139
|
+
fix: "Inspect the target scope, then retry the remaining keys once the API issue is resolved.",
|
|
140
|
+
exitCode: 1,
|
|
141
|
+
nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`, retryStepForApplyFailure(command, filePath, scope, writtenKeys)],
|
|
142
|
+
meta: {
|
|
143
|
+
file: filePath,
|
|
144
|
+
failedKey,
|
|
145
|
+
writtenKeys
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function retryStepForApplyFailure(command, filePath, scope, writtenKeys) {
|
|
150
|
+
if (command === "update") return `prisma-cli project env update --file ${filePath} ${formatScopeFlag(scope)}`;
|
|
151
|
+
if (writtenKeys.length === 0) return `prisma-cli project env add --file ${filePath} ${formatScopeFlag(scope)}`;
|
|
152
|
+
return `prisma-cli project env add --file <remaining.env> ${formatScopeFlag(scope)}`;
|
|
153
|
+
}
|
|
154
|
+
function splitFileNextSteps(filePath, scope, options) {
|
|
155
|
+
const scopeFlag = formatScopeFlag(scope);
|
|
156
|
+
const existingFile = `${filePath}.existing`;
|
|
157
|
+
const newFile = `${filePath}.new`;
|
|
158
|
+
if (options.first === "update-existing") return [
|
|
159
|
+
`# existing keys: ${formatKeyList(options.existingKeys)}`,
|
|
160
|
+
`prisma-cli project env update --file ${existingFile} ${scopeFlag}`,
|
|
161
|
+
"# new keys only",
|
|
162
|
+
`prisma-cli project env add --file ${newFile} ${scopeFlag}`
|
|
163
|
+
];
|
|
164
|
+
return [
|
|
165
|
+
`# missing keys: ${formatKeyList(options.missingKeys)}`,
|
|
166
|
+
`prisma-cli project env add --file ${newFile} ${scopeFlag}`,
|
|
167
|
+
"# existing keys only",
|
|
168
|
+
`prisma-cli project env update --file ${existingFile} ${scopeFlag}`
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
function formatKeyList(keys) {
|
|
172
|
+
return keys.map((key) => `"${key}"`).join(", ");
|
|
173
|
+
}
|
|
174
|
+
function formatScopeFlag(scope) {
|
|
175
|
+
if (scope.kind === "role") return `--role ${scope.role}`;
|
|
176
|
+
return `--branch ${scope.branchName}`;
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
export { runEnvAddFile, runEnvUpdateFile };
|
|
@@ -5,29 +5,34 @@ import { readLocalGitBranch } from "../lib/git/local-branch.js";
|
|
|
5
5
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
6
6
|
import { listRealWorkspaceProjects } from "./project.js";
|
|
7
7
|
import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
|
|
8
|
+
import { readEnvFileAssignments } from "../lib/app/env-file.js";
|
|
9
|
+
import { apiCallError, findVariableByNaturalKey, toMetadata } from "./app-env-api.js";
|
|
10
|
+
import { runEnvAddFile, runEnvUpdateFile } from "./app-env-file.js";
|
|
8
11
|
//#region src/controllers/app-env.ts
|
|
9
12
|
async function runEnvAdd(context, rawAssignment, flags) {
|
|
10
|
-
const
|
|
13
|
+
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "add");
|
|
11
14
|
const scope = resolveEnvScope(flags, {
|
|
12
15
|
requireExplicit: true,
|
|
13
16
|
command: "add"
|
|
14
17
|
});
|
|
15
|
-
if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [
|
|
18
|
+
if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", ["prisma-cli project env add KEY=value --role production"], "app");
|
|
19
|
+
const input = await resolveEnvWriteInput(context, source, "add");
|
|
16
20
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env add");
|
|
17
21
|
const resolved = await resolveScopeToApi(client, projectId, scope, {
|
|
18
22
|
createBranchIfMissing: true,
|
|
19
23
|
signal: context.runtime.signal
|
|
20
24
|
});
|
|
21
|
-
if (
|
|
25
|
+
if (input.kind === "file") return runEnvAddFile(context, client, projectId, resolved, input.filePath, input.assignments);
|
|
26
|
+
if (await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal)) throw new CliError({
|
|
22
27
|
code: "ENV_VARIABLE_ALREADY_EXISTS",
|
|
23
28
|
domain: "app",
|
|
24
|
-
summary: `Variable "${key}" already exists in ${formatScopeLabel(scope)}`,
|
|
29
|
+
summary: `Variable "${input.key}" already exists in ${formatScopeLabel(scope)}`,
|
|
25
30
|
why: "A variable with this key already exists in the targeted scope.",
|
|
26
31
|
fix: "Use `prisma-cli project env update` to change an existing variable's value.",
|
|
27
32
|
exitCode: 1,
|
|
28
|
-
nextSteps: [`prisma-cli project env update ${key}=<new-value> ${formatScopeFlag(scope)}`]
|
|
33
|
+
nextSteps: [`prisma-cli project env update ${input.key}=<new-value> ${formatScopeFlag(scope)}`]
|
|
29
34
|
});
|
|
30
|
-
const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, key, {
|
|
35
|
+
const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, input.key, {
|
|
31
36
|
scope: {
|
|
32
37
|
kind: "role",
|
|
33
38
|
role: "preview"
|
|
@@ -40,18 +45,18 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
40
45
|
class: "preview",
|
|
41
46
|
branchId: null
|
|
42
47
|
}
|
|
43
|
-
}, context.runtime.signal) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
|
|
48
|
+
}, context.runtime.signal) ? [`Variable "${input.key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
|
|
44
49
|
const { data, error, response } = await client.POST("/v1/environment-variables", {
|
|
45
50
|
body: {
|
|
46
51
|
projectId,
|
|
47
52
|
class: resolved.apiTarget.class,
|
|
48
53
|
...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
|
|
49
|
-
key,
|
|
50
|
-
value
|
|
54
|
+
key: input.key,
|
|
55
|
+
value: input.value
|
|
51
56
|
},
|
|
52
57
|
signal: context.runtime.signal
|
|
53
58
|
});
|
|
54
|
-
if (error || !data) throw apiCallError(`Failed to add ${key}`, response, error);
|
|
59
|
+
if (error || !data) throw apiCallError(`Failed to add ${input.key}`, response, error);
|
|
55
60
|
return {
|
|
56
61
|
command: "project.env.add",
|
|
57
62
|
result: {
|
|
@@ -64,33 +69,35 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
64
69
|
};
|
|
65
70
|
}
|
|
66
71
|
async function runEnvUpdate(context, rawAssignment, flags) {
|
|
67
|
-
const
|
|
72
|
+
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "update");
|
|
68
73
|
const scope = resolveEnvScope(flags, {
|
|
69
74
|
requireExplicit: true,
|
|
70
75
|
command: "update"
|
|
71
76
|
});
|
|
72
|
-
if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [
|
|
77
|
+
if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", ["prisma-cli project env update KEY=value --role production"], "app");
|
|
78
|
+
const input = await resolveEnvWriteInput(context, source, "update");
|
|
73
79
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env update");
|
|
74
80
|
const resolved = await resolveScopeToApi(client, projectId, scope, {
|
|
75
81
|
createBranchIfMissing: false,
|
|
76
82
|
signal: context.runtime.signal
|
|
77
83
|
});
|
|
78
|
-
|
|
84
|
+
if (input.kind === "file") return runEnvUpdateFile(context, client, projectId, resolved, input.filePath, input.assignments);
|
|
85
|
+
const existing = await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal);
|
|
79
86
|
if (!existing) throw new CliError({
|
|
80
87
|
code: "ENV_VARIABLE_NOT_FOUND",
|
|
81
88
|
domain: "app",
|
|
82
|
-
summary: `Variable "${key}" not found in ${formatScopeLabel(scope)}`,
|
|
89
|
+
summary: `Variable "${input.key}" not found in ${formatScopeLabel(scope)}`,
|
|
83
90
|
why: "No variable with this key exists in the targeted scope.",
|
|
84
91
|
fix: "Use `prisma-cli project env add` to create a new variable.",
|
|
85
92
|
exitCode: 1,
|
|
86
|
-
nextSteps: [`prisma-cli project env add ${key}=<value> ${formatScopeFlag(scope)}`]
|
|
93
|
+
nextSteps: [`prisma-cli project env add ${input.key}=<value> ${formatScopeFlag(scope)}`]
|
|
87
94
|
});
|
|
88
95
|
const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
89
96
|
params: { path: { envVarId: existing.id } },
|
|
90
|
-
body: { value },
|
|
97
|
+
body: { value: input.value },
|
|
91
98
|
signal: context.runtime.signal
|
|
92
99
|
});
|
|
93
|
-
if (error || !data) throw apiCallError(`Failed to update value for ${key}`, response, error);
|
|
100
|
+
if (error || !data) throw apiCallError(`Failed to update value for ${input.key}`, response, error);
|
|
94
101
|
return {
|
|
95
102
|
command: "project.env.update",
|
|
96
103
|
result: {
|
|
@@ -102,6 +109,32 @@ async function runEnvUpdate(context, rawAssignment, flags) {
|
|
|
102
109
|
nextSteps: []
|
|
103
110
|
};
|
|
104
111
|
}
|
|
112
|
+
function resolveEnvWriteSource(rawAssignment, filePath, command) {
|
|
113
|
+
if (filePath !== void 0 && rawAssignment !== void 0) throw usageError(`prisma-cli project env ${command} accepts either KEY=VALUE or --file`, "The command received both a positional assignment and a dotenv file path.", "Pass one input source.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
114
|
+
if (filePath !== void 0) {
|
|
115
|
+
if (filePath.length === 0) throw usageError(`prisma-cli project env ${command} --file requires a path`, "The --file flag was passed without a file path.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
116
|
+
return {
|
|
117
|
+
kind: "file",
|
|
118
|
+
filePath
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (rawAssignment === void 0) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE or --file`, "No environment variable input was supplied.", "Pass a single KEY=VALUE assignment or a dotenv file path.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
122
|
+
return {
|
|
123
|
+
kind: "single",
|
|
124
|
+
rawAssignment
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function resolveEnvWriteInput(context, source, command) {
|
|
128
|
+
if (source.kind === "file") return {
|
|
129
|
+
kind: "file",
|
|
130
|
+
filePath: source.filePath,
|
|
131
|
+
assignments: await readEnvFileAssignments(context.runtime.cwd, source.filePath, command)
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
kind: "single",
|
|
135
|
+
...parseKeyValuePositional(source.rawAssignment, command, context.runtime.env)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
105
138
|
async function runEnvList(context, flags) {
|
|
106
139
|
const explicit = resolveEnvScope(flags, {
|
|
107
140
|
requireExplicit: false,
|
|
@@ -410,18 +443,6 @@ async function projectHasDefaultBranch(client, projectId, signal) {
|
|
|
410
443
|
cursor = result.data.pagination.nextCursor;
|
|
411
444
|
}
|
|
412
445
|
}
|
|
413
|
-
async function findVariableByNaturalKey(client, projectId, key, resolved, signal) {
|
|
414
|
-
const { data, error, response } = await client.GET("/v1/environment-variables", {
|
|
415
|
-
params: { query: {
|
|
416
|
-
projectId,
|
|
417
|
-
class: resolved.apiTarget.class,
|
|
418
|
-
key
|
|
419
|
-
} },
|
|
420
|
-
signal
|
|
421
|
-
});
|
|
422
|
-
if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
|
|
423
|
-
return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
|
|
424
|
-
}
|
|
425
446
|
async function listVariables(client, projectId, resolved, signal) {
|
|
426
447
|
return materializeEffectiveRows(await collectEnvironmentVariables(client, projectId, signal, {
|
|
427
448
|
className: resolved.apiTarget.class,
|
|
@@ -461,9 +482,6 @@ function rowMatchesScope(row, resolved) {
|
|
|
461
482
|
if (resolved.apiTarget.branchId === null) return row.branchId === null;
|
|
462
483
|
return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
|
|
463
484
|
}
|
|
464
|
-
function rowMatchesExactScope(row, resolved) {
|
|
465
|
-
return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
|
|
466
|
-
}
|
|
467
485
|
function materializeEffectiveRows(rows, resolved) {
|
|
468
486
|
if (resolved.apiTarget.branchId === null) return rows;
|
|
469
487
|
const byKey = /* @__PURE__ */ new Map();
|
|
@@ -471,40 +489,5 @@ function materializeEffectiveRows(rows, resolved) {
|
|
|
471
489
|
for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
|
|
472
490
|
return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
473
491
|
}
|
|
474
|
-
function toMetadata(row, requestedScope) {
|
|
475
|
-
const rowScope = row.branchId === null ? {
|
|
476
|
-
kind: "role",
|
|
477
|
-
role: row.class
|
|
478
|
-
} : requestedScope;
|
|
479
|
-
return {
|
|
480
|
-
id: row.id,
|
|
481
|
-
key: row.key,
|
|
482
|
-
scope: rowScope,
|
|
483
|
-
source: formatDescriptorLabel(rowScope),
|
|
484
|
-
isManagedBySystem: row.isManagedBySystem,
|
|
485
|
-
updatedAt: row.updatedAt
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
function formatDescriptorLabel(scope) {
|
|
489
|
-
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
490
|
-
if (scope.kind === "overview") return "overview";
|
|
491
|
-
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
492
|
-
}
|
|
493
|
-
function apiCallError(summary, response, error) {
|
|
494
|
-
const status = response?.status ?? 0;
|
|
495
|
-
const apiCode = error?.error?.code;
|
|
496
|
-
const apiMessage = error?.error?.message;
|
|
497
|
-
const apiHint = error?.error?.hint;
|
|
498
|
-
if (status === 401 || status === 403) return authRequiredError(["prisma auth login"]);
|
|
499
|
-
return new CliError({
|
|
500
|
-
code: apiCode ?? "ENV_API_ERROR",
|
|
501
|
-
domain: "app",
|
|
502
|
-
summary,
|
|
503
|
-
why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
|
|
504
|
-
fix: apiHint ?? "Re-run with --trace for the underlying API response details.",
|
|
505
|
-
exitCode: 1,
|
|
506
|
-
nextSteps: []
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
492
|
//#endregion
|
|
510
493
|
export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };
|
|
@@ -32,6 +32,10 @@ async function maybeSetupBranchDatabase(context, provider, projectId, branch, op
|
|
|
32
32
|
warnings: warning ? [warning] : []
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
if (localSignal.unsupportedSchema) {
|
|
36
|
+
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch.name, context);
|
|
37
|
+
return emptyBranchDatabaseSetupOutcome();
|
|
38
|
+
}
|
|
35
39
|
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.previewDatabaseUrl);
|
|
36
40
|
if (options.db !== true) {
|
|
37
41
|
if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
|
|
@@ -76,10 +80,10 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
76
80
|
databaseUrl: database.databaseUrl,
|
|
77
81
|
directUrl: database.directUrl
|
|
78
82
|
}).catch((error) => {
|
|
79
|
-
throw schemaSetupFailedError(error, signal.schema, branch.name);
|
|
83
|
+
throw schemaSetupFailedError(error, signal.schema, branch.name, context.runtime.cwd);
|
|
80
84
|
});
|
|
81
85
|
emitBranchDatabaseProgress(context, "success", "Applied database schema");
|
|
82
|
-
} else skippedSchemaWarning = "No schema
|
|
86
|
+
} else skippedSchemaWarning = "No supported Prisma schema source was found. Branch database env vars were created, but schema setup was skipped.";
|
|
83
87
|
const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
|
|
84
88
|
emitBranchDatabaseProgress(context, "success", `Added branch env override${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
|
|
85
89
|
if (skippedSchemaWarning) {
|
|
@@ -96,6 +100,7 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
96
100
|
envVars,
|
|
97
101
|
schema: schemaSetup ? {
|
|
98
102
|
command: schemaSetup.command,
|
|
103
|
+
source: schemaSetup.source,
|
|
99
104
|
path: schemaSetup.schemaPath
|
|
100
105
|
} : null
|
|
101
106
|
},
|
|
@@ -185,7 +190,7 @@ function hasInlineDatabaseEnvVars(envVars) {
|
|
|
185
190
|
function maybeRenderBranchDatabaseSignal(context, branchName, signal, envState) {
|
|
186
191
|
if (context.flags.json || context.flags.quiet) return;
|
|
187
192
|
const rows = [
|
|
188
|
-
signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) ||
|
|
193
|
+
signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
|
|
189
194
|
signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
|
|
190
195
|
envState.previewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
|
|
191
196
|
].filter((row) => Boolean(row));
|
|
@@ -207,7 +212,11 @@ function emptyBranchDatabaseSetupOutcome() {
|
|
|
207
212
|
};
|
|
208
213
|
}
|
|
209
214
|
function formatSchemaSetupCommand(command) {
|
|
210
|
-
|
|
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
|
+
}
|
|
211
220
|
}
|
|
212
221
|
function branchDatabaseSetupFailedError(summary, error, branchName) {
|
|
213
222
|
return new CliError({
|
|
@@ -257,7 +266,7 @@ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, br
|
|
|
257
266
|
nextSteps: []
|
|
258
267
|
});
|
|
259
268
|
}
|
|
260
|
-
function schemaSetupFailedError(error, schema, branchName) {
|
|
269
|
+
function schemaSetupFailedError(error, schema, branchName, cwd) {
|
|
261
270
|
return new CliError({
|
|
262
271
|
code: "SCHEMA_SETUP_FAILED",
|
|
263
272
|
domain: "app",
|
|
@@ -268,12 +277,39 @@ function schemaSetupFailedError(error, schema, branchName) {
|
|
|
268
277
|
meta: {
|
|
269
278
|
branch: branchName,
|
|
270
279
|
schemaPath: schema.path,
|
|
280
|
+
source: schema.kind,
|
|
271
281
|
command: schema.command
|
|
272
282
|
},
|
|
273
283
|
exitCode: 1,
|
|
274
|
-
nextSteps: [schema
|
|
284
|
+
nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), `prisma-cli app deploy --branch ${formatCommandArgument(branchName)} --db`]
|
|
275
285
|
});
|
|
276
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
|
+
}
|
|
277
313
|
function formatDebugDetails(error) {
|
|
278
314
|
if (error instanceof Error) return error.stack ?? error.message;
|
|
279
315
|
return typeof error === "string" ? error : null;
|
|
@@ -10,6 +10,7 @@ const SKIPPED_DIRECTORIES = new Set([
|
|
|
10
10
|
".prisma",
|
|
11
11
|
".turbo",
|
|
12
12
|
".vercel",
|
|
13
|
+
".wrangler",
|
|
13
14
|
"build",
|
|
14
15
|
"coverage",
|
|
15
16
|
"dist",
|
|
@@ -37,29 +38,41 @@ async function inspectBranchDatabaseSignal(cwd, signal) {
|
|
|
37
38
|
const state = {
|
|
38
39
|
filesVisited: 0,
|
|
39
40
|
schemaCandidates: [],
|
|
41
|
+
prismaNextConfigCandidates: [],
|
|
40
42
|
databaseUrlReferences: []
|
|
41
43
|
};
|
|
42
44
|
await scanDirectory(cwd, cwd, 0, state, signal);
|
|
43
|
-
const
|
|
44
|
-
const
|
|
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;
|
|
45
56
|
return {
|
|
46
|
-
schema
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
schema,
|
|
58
|
+
unsupportedSchema: schema ? null : unsupportedPrismaNextConfig ? {
|
|
59
|
+
kind: "prisma-next",
|
|
60
|
+
path: unsupportedPrismaNextConfig.path,
|
|
61
|
+
target: unsupportedPrismaNextConfig.target
|
|
62
|
+
} : selectedPrismaOrmSchema.unsupportedSchema,
|
|
51
63
|
databaseUrlReferences: state.databaseUrlReferences
|
|
52
64
|
};
|
|
53
65
|
}
|
|
54
66
|
function hasBranchDatabaseSignal(signal) {
|
|
67
|
+
if (signal.unsupportedSchema) return false;
|
|
55
68
|
return Boolean(signal.schema || signal.databaseUrlReferences.length > 0);
|
|
56
69
|
}
|
|
57
70
|
async function runBranchDatabaseSchemaSetup(options) {
|
|
58
|
-
const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) ||
|
|
59
|
-
const
|
|
60
|
-
await runPrismaCommand({
|
|
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({
|
|
61
74
|
context: options.context,
|
|
62
|
-
|
|
75
|
+
...command,
|
|
63
76
|
env: {
|
|
64
77
|
DATABASE_URL: options.databaseUrl,
|
|
65
78
|
...options.directUrl ? { DIRECT_URL: options.directUrl } : {}
|
|
@@ -67,6 +80,7 @@ async function runBranchDatabaseSchemaSetup(options) {
|
|
|
67
80
|
});
|
|
68
81
|
return {
|
|
69
82
|
command: options.schema.command,
|
|
83
|
+
source: options.schema.kind,
|
|
70
84
|
schemaPath
|
|
71
85
|
};
|
|
72
86
|
}
|
|
@@ -92,18 +106,57 @@ async function scanDirectory(cwd, directory, depth, state, signal) {
|
|
|
92
106
|
if (!entry.isFile()) continue;
|
|
93
107
|
state.filesVisited += 1;
|
|
94
108
|
if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
|
|
109
|
+
if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
|
|
95
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);
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
|
-
function
|
|
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) {
|
|
99
152
|
return candidates.map((candidate) => ({
|
|
100
153
|
absolute: candidate,
|
|
101
|
-
relative: path.relative(cwd, candidate) ||
|
|
154
|
+
relative: path.relative(cwd, candidate) || preferredRootFile
|
|
102
155
|
})).sort((left, right) => {
|
|
103
|
-
if (left.relative ===
|
|
104
|
-
if (right.relative ===
|
|
156
|
+
if (left.relative === preferredRootFile) return -1;
|
|
157
|
+
if (right.relative === preferredRootFile) return 1;
|
|
105
158
|
return left.relative.length - right.relative.length || left.relative.localeCompare(right.relative);
|
|
106
|
-
})
|
|
159
|
+
}).map((candidate) => candidate.absolute);
|
|
107
160
|
}
|
|
108
161
|
async function hasMigrationsDirectory(schemaDirectory, signal) {
|
|
109
162
|
signal.throwIfAborted();
|
|
@@ -116,36 +169,115 @@ async function hasMigrationsDirectory(schemaDirectory, signal) {
|
|
|
116
169
|
throw error;
|
|
117
170
|
}
|
|
118
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
|
+
}
|
|
119
206
|
function shouldScanForDatabaseUrl(fileName) {
|
|
120
207
|
if (fileName === ".env" || fileName.startsWith(".env.")) return true;
|
|
121
208
|
return DATABASE_URL_SCAN_EXTENSIONS.has(path.extname(fileName));
|
|
122
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
|
+
}
|
|
123
221
|
async function fileContainsDatabaseUrl(filePath, signal) {
|
|
222
|
+
return (await readTextFileIfSmall(filePath, signal))?.includes("DATABASE_URL") ?? false;
|
|
223
|
+
}
|
|
224
|
+
async function readTextFileIfSmall(filePath, signal) {
|
|
124
225
|
signal.throwIfAborted();
|
|
125
|
-
if ((await stat(filePath)).size > MAX_TEXT_FILE_BYTES) return
|
|
126
|
-
return
|
|
226
|
+
if ((await stat(filePath)).size > MAX_TEXT_FILE_BYTES) return null;
|
|
227
|
+
return readFile(filePath, {
|
|
127
228
|
encoding: "utf8",
|
|
128
229
|
signal
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
function
|
|
132
|
-
if (command === "migrate-deploy") return [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"--no-install"
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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";
|
|
149
281
|
}
|
|
150
282
|
async function runPrismaCommand(options) {
|
|
151
283
|
const shouldPipeOutput = !options.context.flags.json && !options.context.flags.quiet;
|
|
@@ -177,8 +309,8 @@ async function runPrismaCommand(options) {
|
|
|
177
309
|
signal
|
|
178
310
|
}));
|
|
179
311
|
});
|
|
180
|
-
if (exit.signal) throw new Error(
|
|
181
|
-
if (exit.code !== 0) throw new Error(
|
|
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}.`);
|
|
182
314
|
}
|
|
183
315
|
//#endregion
|
|
184
316
|
export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { usageError } from "../../shell/errors.js";
|
|
2
|
+
import { validateKey } from "./env-config.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parse } from "dotenv";
|
|
6
|
+
//#region src/lib/app/env-file.ts
|
|
7
|
+
const ASSIGNMENT_KEY_PATTERN = /^\s*(?:export\s+)?([^#=\s]+)\s*=/;
|
|
8
|
+
async function readEnvFileAssignments(cwd, filePath, command) {
|
|
9
|
+
const resolvedPath = path.resolve(cwd, filePath);
|
|
10
|
+
let contents;
|
|
11
|
+
try {
|
|
12
|
+
contents = await readFile(resolvedPath, "utf8");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw usageError(`Failed to read env file "${filePath}"`, error instanceof Error ? error.message : "The file could not be read.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
15
|
+
}
|
|
16
|
+
return parseEnvFileContents(contents, filePath, command);
|
|
17
|
+
}
|
|
18
|
+
function parseEnvFileContents(contents, filePath, command) {
|
|
19
|
+
const parsedKeys = extractParsedKeys(contents);
|
|
20
|
+
if (parsedKeys.length === 0) throw usageError(`No environment variables found in "${filePath}"`, "The file does not contain any KEY=VALUE assignments.", "Pass a dotenv file with at least one non-empty variable.", [], "app");
|
|
21
|
+
const seen = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const entry of parsedKeys) {
|
|
23
|
+
validateEnvFileKey(entry.key, entry.line, filePath, command);
|
|
24
|
+
const firstLine = seen.get(entry.key);
|
|
25
|
+
if (firstLine !== void 0) throw usageError(`Duplicate environment variable "${entry.key}" in "${filePath}"`, `Lines ${firstLine} and ${entry.line} both define ${entry.key}.`, "Keep one assignment for each key before importing the file.", [], "app");
|
|
26
|
+
seen.set(entry.key, entry.line);
|
|
27
|
+
}
|
|
28
|
+
const parsedValues = parse(contents);
|
|
29
|
+
return parsedKeys.map(({ key }) => {
|
|
30
|
+
const value = parsedValues[key];
|
|
31
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
32
|
+
const line = seen.get(key);
|
|
33
|
+
throw usageError(`Environment variable "${key}" in "${filePath}" has an empty value`, line === void 0 ? `${key} has an empty value.` : `Line ${line} defines ${key} with an empty value.`, "Pass a non-empty value, or omit the key from the file.", [], "app");
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
key,
|
|
37
|
+
value
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function extractParsedKeys(contents) {
|
|
42
|
+
const keys = [];
|
|
43
|
+
let multilineQuote = null;
|
|
44
|
+
const lines = contents.split(/\n/);
|
|
45
|
+
for (const [index, line] of lines.entries()) {
|
|
46
|
+
const lineNumber = index + 1;
|
|
47
|
+
if (multilineQuote !== null) {
|
|
48
|
+
if (hasClosingQuote(line, multilineQuote, 0)) multilineQuote = null;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const match = ASSIGNMENT_KEY_PATTERN.exec(line);
|
|
52
|
+
if (!match) continue;
|
|
53
|
+
const key = match[1];
|
|
54
|
+
keys.push({
|
|
55
|
+
key,
|
|
56
|
+
line: lineNumber
|
|
57
|
+
});
|
|
58
|
+
const valueStart = line.slice(match[0].length).trimStart();
|
|
59
|
+
const openingQuote = valueStart[0];
|
|
60
|
+
if ((openingQuote === "\"" || openingQuote === "'" || openingQuote === "`") && !hasClosingQuote(valueStart, openingQuote, 1)) multilineQuote = openingQuote;
|
|
61
|
+
}
|
|
62
|
+
return keys;
|
|
63
|
+
}
|
|
64
|
+
function validateEnvFileKey(key, line, filePath, command) {
|
|
65
|
+
try {
|
|
66
|
+
validateKey(key, command);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const reason = error instanceof Error && error.message.length > 0 ? error.message : "Invalid environment variable key.";
|
|
69
|
+
throw usageError(`Invalid environment variable "${key}" in "${filePath}"`, `Line ${line}: ${reason}`, "Use a valid env-var key and retry the import.", [], "app");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function hasClosingQuote(value, quote, startIndex) {
|
|
73
|
+
for (let index = startIndex; index < value.length; index += 1) if (value[index] === quote && !isEscaped(value, index)) return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function isEscaped(value, index) {
|
|
77
|
+
let backslashes = 0;
|
|
78
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) backslashes += 1;
|
|
79
|
+
return backslashes % 2 === 1;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { readEnvFileAssignments };
|
|
@@ -15,6 +15,21 @@ function listTargetLabel(result) {
|
|
|
15
15
|
return scopeLabel(result.scope);
|
|
16
16
|
}
|
|
17
17
|
function renderEnvAdd(context, descriptor, result) {
|
|
18
|
+
if (result.variables !== void 0) return renderList({
|
|
19
|
+
title: "Setting new environment variables from file.",
|
|
20
|
+
descriptor,
|
|
21
|
+
parentContext: {
|
|
22
|
+
key: "target",
|
|
23
|
+
value: `${scopeLabel(result.scope)} from ${result.file.path}`
|
|
24
|
+
},
|
|
25
|
+
items: result.variables.map((variable) => ({
|
|
26
|
+
noun: "variable",
|
|
27
|
+
label: `${variable.key} (${variable.source})`,
|
|
28
|
+
id: variable.id,
|
|
29
|
+
status: variable.isManagedBySystem ? "default" : null
|
|
30
|
+
})),
|
|
31
|
+
emptyMessage: "No environment variables imported."
|
|
32
|
+
}, context.ui);
|
|
18
33
|
return renderShow({
|
|
19
34
|
title: "Setting a new environment variable.",
|
|
20
35
|
descriptor,
|
|
@@ -48,6 +63,21 @@ function serializeEnvAdd(result) {
|
|
|
48
63
|
return result;
|
|
49
64
|
}
|
|
50
65
|
function renderEnvUpdate(context, descriptor, result) {
|
|
66
|
+
if (result.variables !== void 0) return renderList({
|
|
67
|
+
title: "Replacing environment variable values from file.",
|
|
68
|
+
descriptor,
|
|
69
|
+
parentContext: {
|
|
70
|
+
key: "target",
|
|
71
|
+
value: `${scopeLabel(result.scope)} from ${result.file.path}`
|
|
72
|
+
},
|
|
73
|
+
items: result.variables.map((variable) => ({
|
|
74
|
+
noun: "variable",
|
|
75
|
+
label: `${variable.key} (${variable.source})`,
|
|
76
|
+
id: variable.id,
|
|
77
|
+
status: variable.isManagedBySystem ? "default" : null
|
|
78
|
+
})),
|
|
79
|
+
emptyMessage: "No environment variables updated."
|
|
80
|
+
}, context.ui);
|
|
51
81
|
return renderShow({
|
|
52
82
|
title: "Replacing the environment variable's value.",
|
|
53
83
|
descriptor,
|
package/dist/presenters/app.js
CHANGED
|
@@ -55,10 +55,17 @@ function renderBranchDatabaseDeploySummary(context, result) {
|
|
|
55
55
|
},
|
|
56
56
|
...result.branchDatabase.schema ? [{
|
|
57
57
|
label: "Schema",
|
|
58
|
-
value: result.branchDatabase.schema.command
|
|
58
|
+
value: formatBranchDatabaseSchemaCommand(result.branchDatabase.schema.command)
|
|
59
59
|
}] : []
|
|
60
60
|
])];
|
|
61
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
|
+
}
|
|
62
69
|
function formatDuration(durationMs) {
|
|
63
70
|
if (durationMs < 1e3) return `${durationMs}ms`;
|
|
64
71
|
return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
@@ -363,6 +363,7 @@ const DESCRIPTORS = [
|
|
|
363
363
|
examples: [
|
|
364
364
|
"prisma-cli project env list",
|
|
365
365
|
"prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production",
|
|
366
|
+
"prisma-cli project env add --file .env --role preview",
|
|
366
367
|
"prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo",
|
|
367
368
|
"prisma-cli project env remove STRIPE_KEY --role preview"
|
|
368
369
|
]
|
|
@@ -379,7 +380,9 @@ const DESCRIPTORS = [
|
|
|
379
380
|
examples: [
|
|
380
381
|
"prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production",
|
|
381
382
|
"prisma-cli project env add STRIPE_KEY=sk_test_xxx --role preview",
|
|
383
|
+
"prisma-cli project env add --file .env --role preview",
|
|
382
384
|
"prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo",
|
|
385
|
+
"prisma-cli project env add --file .env.local --branch feature/foo",
|
|
383
386
|
"API_URL=https://api.example prisma-cli project env add API_URL --project proj_123 --role preview"
|
|
384
387
|
]
|
|
385
388
|
},
|
|
@@ -395,6 +398,7 @@ const DESCRIPTORS = [
|
|
|
395
398
|
examples: [
|
|
396
399
|
"prisma-cli project env update STRIPE_KEY=sk_new_xxx --role production",
|
|
397
400
|
"prisma-cli project env update STRIPE_KEY=sk_new_xxx --role preview",
|
|
401
|
+
"prisma-cli project env update --file .env --role production",
|
|
398
402
|
"prisma-cli project env update DATABASE_URL=postgresql://branch --branch feature/foo"
|
|
399
403
|
]
|
|
400
404
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma/cli",
|
|
3
|
-
"version": "3.0.0-dev.
|
|
3
|
+
"version": "3.0.0-dev.62.1",
|
|
4
4
|
"description": "Command-line interface for the Prisma Developer Platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"c12": "4.0.0-beta.5",
|
|
43
43
|
"colorette": "^2.0.20",
|
|
44
44
|
"commander": "^14.0.3",
|
|
45
|
+
"dotenv": "^17.4.2",
|
|
45
46
|
"magicast": "^0.5.3",
|
|
46
47
|
"open": "^11.0.0",
|
|
47
48
|
"string-width": "^8.2.1",
|