@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.5
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 +4 -2
- package/dist/commands/app/index.js +22 -10
- package/dist/commands/branch/index.js +2 -27
- 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 +200 -86
- package/dist/controllers/app.js +51 -53
- package/dist/controllers/branch.js +73 -47
- package/dist/lib/app/branch-database-deploy.js +323 -0
- package/dist/lib/app/branch-database.js +316 -0
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/preview-branch-database.js +102 -0
- package/dist/lib/app/preview-provider.js +45 -6
- package/dist/lib/app/production-deploy-gate.js +160 -0
- package/dist/lib/git/local-branch.js +41 -0
- package/dist/lib/project/resolution.js +49 -4
- package/dist/presenters/app-env.js +44 -3
- package/dist/presenters/app.js +25 -0
- package/dist/presenters/branch.js +29 -101
- package/dist/presenters/project.js +15 -16
- package/dist/shell/command-meta.js +12 -24
- package/dist/shell/ui.js +4 -1
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/package.json +2 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
//#region src/lib/app/branch-database.ts
|
|
5
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
6
|
+
".git",
|
|
7
|
+
".next",
|
|
8
|
+
".nuxt",
|
|
9
|
+
".output",
|
|
10
|
+
".prisma",
|
|
11
|
+
".turbo",
|
|
12
|
+
".vercel",
|
|
13
|
+
".wrangler",
|
|
14
|
+
"build",
|
|
15
|
+
"coverage",
|
|
16
|
+
"dist",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"out"
|
|
19
|
+
]);
|
|
20
|
+
const DATABASE_URL_SCAN_EXTENSIONS = new Set([
|
|
21
|
+
".cjs",
|
|
22
|
+
".cts",
|
|
23
|
+
".env",
|
|
24
|
+
".js",
|
|
25
|
+
".json",
|
|
26
|
+
".jsx",
|
|
27
|
+
".mjs",
|
|
28
|
+
".mts",
|
|
29
|
+
".prisma",
|
|
30
|
+
".ts",
|
|
31
|
+
".tsx"
|
|
32
|
+
]);
|
|
33
|
+
const MAX_SCAN_DEPTH = 6;
|
|
34
|
+
const MAX_SCAN_FILES = 1e3;
|
|
35
|
+
const MAX_DATABASE_URL_REFERENCE_FILES = 10;
|
|
36
|
+
const MAX_TEXT_FILE_BYTES = 1024 * 1024;
|
|
37
|
+
async function inspectBranchDatabaseSignal(cwd, signal) {
|
|
38
|
+
const state = {
|
|
39
|
+
filesVisited: 0,
|
|
40
|
+
schemaCandidates: [],
|
|
41
|
+
prismaNextConfigCandidates: [],
|
|
42
|
+
databaseUrlReferences: []
|
|
43
|
+
};
|
|
44
|
+
await scanDirectory(cwd, cwd, 0, state, signal);
|
|
45
|
+
const prismaNextConfigs = await Promise.all(state.prismaNextConfigCandidates.map((configPath) => classifyPrismaNextConfig(configPath, signal)));
|
|
46
|
+
const supportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "supported");
|
|
47
|
+
const unsupportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "unsupported");
|
|
48
|
+
const selectedPrismaOrmSchema = await selectPrismaOrmSchema(cwd, state.schemaCandidates, signal);
|
|
49
|
+
const schema = supportedPrismaNextConfig ? {
|
|
50
|
+
kind: "prisma-next",
|
|
51
|
+
path: supportedPrismaNextConfig.path,
|
|
52
|
+
hasMigrations: false,
|
|
53
|
+
command: "prisma-next-db-init",
|
|
54
|
+
target: supportedPrismaNextConfig.target
|
|
55
|
+
} : selectedPrismaOrmSchema.schema;
|
|
56
|
+
return {
|
|
57
|
+
schema,
|
|
58
|
+
unsupportedSchema: schema ? null : unsupportedPrismaNextConfig ? {
|
|
59
|
+
kind: "prisma-next",
|
|
60
|
+
path: unsupportedPrismaNextConfig.path,
|
|
61
|
+
target: unsupportedPrismaNextConfig.target
|
|
62
|
+
} : selectedPrismaOrmSchema.unsupportedSchema,
|
|
63
|
+
databaseUrlReferences: state.databaseUrlReferences
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function hasBranchDatabaseSignal(signal) {
|
|
67
|
+
if (signal.unsupportedSchema) return false;
|
|
68
|
+
return Boolean(signal.schema || signal.databaseUrlReferences.length > 0);
|
|
69
|
+
}
|
|
70
|
+
async function runBranchDatabaseSchemaSetup(options) {
|
|
71
|
+
const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) || defaultSchemaSourcePath(options.schema);
|
|
72
|
+
const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl);
|
|
73
|
+
for (const command of commands) await runPrismaCommand({
|
|
74
|
+
context: options.context,
|
|
75
|
+
...command,
|
|
76
|
+
env: {
|
|
77
|
+
DATABASE_URL: options.databaseUrl,
|
|
78
|
+
...options.directUrl ? { DIRECT_URL: options.directUrl } : {}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
command: options.schema.command,
|
|
83
|
+
source: options.schema.kind,
|
|
84
|
+
schemaPath
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function scanDirectory(cwd, directory, depth, state, signal) {
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.code === "ENOENT") return;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
signal.throwIfAborted();
|
|
100
|
+
if (state.filesVisited >= MAX_SCAN_FILES) return;
|
|
101
|
+
const entryPath = path.join(directory, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!entry.isFile()) continue;
|
|
107
|
+
state.filesVisited += 1;
|
|
108
|
+
if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
|
|
109
|
+
if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
|
|
110
|
+
if (state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entry.name) && await fileContainsDatabaseUrl(entryPath, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function selectPrismaOrmSchema(cwd, candidates, signal) {
|
|
114
|
+
const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
|
|
115
|
+
for (const schemaPath of sorted) {
|
|
116
|
+
const target = await classifyPrismaOrmSchemaTarget(schemaPath, signal);
|
|
117
|
+
if (target === "postgresql" || target === "unknown") {
|
|
118
|
+
const hasMigrations = await hasMigrationsDirectory(path.dirname(schemaPath), signal);
|
|
119
|
+
return {
|
|
120
|
+
schema: {
|
|
121
|
+
kind: "prisma-orm",
|
|
122
|
+
path: schemaPath,
|
|
123
|
+
hasMigrations,
|
|
124
|
+
command: hasMigrations ? "migrate-deploy" : "db-push",
|
|
125
|
+
target
|
|
126
|
+
},
|
|
127
|
+
unsupportedSchema: null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
schema: null,
|
|
132
|
+
unsupportedSchema: {
|
|
133
|
+
kind: "prisma-orm",
|
|
134
|
+
path: schemaPath,
|
|
135
|
+
target
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
schema: null,
|
|
141
|
+
unsupportedSchema: null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function selectPrismaNextConfig(cwd, candidates, mode) {
|
|
145
|
+
const matches = candidates.filter((candidate) => {
|
|
146
|
+
const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
|
|
147
|
+
return mode === "supported" ? isSupported : !isSupported;
|
|
148
|
+
});
|
|
149
|
+
return sortByPreferredRelativePath(cwd, matches.map((candidate) => candidate.path), "prisma-next.config.ts").map((candidatePath) => matches.find((candidate) => candidate.path === candidatePath)).find((candidate) => Boolean(candidate)) ?? null;
|
|
150
|
+
}
|
|
151
|
+
function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
|
|
152
|
+
return candidates.map((candidate) => ({
|
|
153
|
+
absolute: candidate,
|
|
154
|
+
relative: path.relative(cwd, candidate) || preferredRootFile
|
|
155
|
+
})).sort((left, right) => {
|
|
156
|
+
if (left.relative === preferredRootFile) return -1;
|
|
157
|
+
if (right.relative === preferredRootFile) return 1;
|
|
158
|
+
return left.relative.length - right.relative.length || left.relative.localeCompare(right.relative);
|
|
159
|
+
}).map((candidate) => candidate.absolute);
|
|
160
|
+
}
|
|
161
|
+
async function hasMigrationsDirectory(schemaDirectory, signal) {
|
|
162
|
+
signal.throwIfAborted();
|
|
163
|
+
const migrationsPath = path.join(schemaDirectory, "migrations");
|
|
164
|
+
try {
|
|
165
|
+
await access(migrationsPath);
|
|
166
|
+
return (await readdir(migrationsPath)).length > 0;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code === "ENOENT") return false;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function classifyPrismaNextConfig(configPath, signal) {
|
|
173
|
+
const content = await readTextFileIfSmall(configPath, signal);
|
|
174
|
+
if (!content) return {
|
|
175
|
+
path: configPath,
|
|
176
|
+
target: "unknown"
|
|
177
|
+
};
|
|
178
|
+
if (content.includes("@prisma-next/postgres/config")) return {
|
|
179
|
+
path: configPath,
|
|
180
|
+
target: "postgresql"
|
|
181
|
+
};
|
|
182
|
+
if (content.includes("@prisma-next/mongo/config")) return {
|
|
183
|
+
path: configPath,
|
|
184
|
+
target: "mongodb"
|
|
185
|
+
};
|
|
186
|
+
if (content.includes("@prisma-next/sqlite/config")) return {
|
|
187
|
+
path: configPath,
|
|
188
|
+
target: "sqlite"
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
path: configPath,
|
|
192
|
+
target: "unknown"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function classifyPrismaOrmSchemaTarget(schemaPath, signal) {
|
|
196
|
+
switch ((await readTextFileIfSmall(schemaPath, signal))?.match(/\bprovider\s*=\s*"([^"]+)"/)?.[1] ?? null) {
|
|
197
|
+
case "postgresql": return "postgresql";
|
|
198
|
+
case "mongodb": return "mongodb";
|
|
199
|
+
case "mysql": return "mysql";
|
|
200
|
+
case "sqlite": return "sqlite";
|
|
201
|
+
case "sqlserver": return "sqlserver";
|
|
202
|
+
case "cockroachdb": return "cockroachdb";
|
|
203
|
+
default: return "unknown";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function shouldScanForDatabaseUrl(fileName) {
|
|
207
|
+
if (fileName === ".env" || fileName.startsWith(".env.")) return true;
|
|
208
|
+
return DATABASE_URL_SCAN_EXTENSIONS.has(path.extname(fileName));
|
|
209
|
+
}
|
|
210
|
+
function isPrismaNextConfigFile(fileName) {
|
|
211
|
+
if (!fileName.startsWith("prisma-next.config.")) return false;
|
|
212
|
+
return [
|
|
213
|
+
".cjs",
|
|
214
|
+
".cts",
|
|
215
|
+
".js",
|
|
216
|
+
".mjs",
|
|
217
|
+
".mts",
|
|
218
|
+
".ts"
|
|
219
|
+
].some((extension) => fileName.endsWith(extension));
|
|
220
|
+
}
|
|
221
|
+
async function fileContainsDatabaseUrl(filePath, signal) {
|
|
222
|
+
return (await readTextFileIfSmall(filePath, signal))?.includes("DATABASE_URL") ?? false;
|
|
223
|
+
}
|
|
224
|
+
async function readTextFileIfSmall(filePath, signal) {
|
|
225
|
+
signal.throwIfAborted();
|
|
226
|
+
if ((await stat(filePath)).size > MAX_TEXT_FILE_BYTES) return null;
|
|
227
|
+
return readFile(filePath, {
|
|
228
|
+
encoding: "utf8",
|
|
229
|
+
signal
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function buildSchemaSetupCommands(schema, schemaPath, databaseUrl) {
|
|
233
|
+
if (schema.command === "migrate-deploy") return [{
|
|
234
|
+
args: [
|
|
235
|
+
"--no-install",
|
|
236
|
+
"prisma",
|
|
237
|
+
"migrate",
|
|
238
|
+
"deploy",
|
|
239
|
+
"--schema",
|
|
240
|
+
schemaPath
|
|
241
|
+
],
|
|
242
|
+
displayCommand: "npx --no-install prisma migrate deploy"
|
|
243
|
+
}];
|
|
244
|
+
if (schema.command === "db-push") return [{
|
|
245
|
+
args: [
|
|
246
|
+
"--no-install",
|
|
247
|
+
"prisma",
|
|
248
|
+
"db",
|
|
249
|
+
"push",
|
|
250
|
+
"--schema",
|
|
251
|
+
schemaPath
|
|
252
|
+
],
|
|
253
|
+
displayCommand: "npx --no-install prisma db push"
|
|
254
|
+
}];
|
|
255
|
+
return [{
|
|
256
|
+
args: [
|
|
257
|
+
"--no-install",
|
|
258
|
+
"prisma-next",
|
|
259
|
+
"contract",
|
|
260
|
+
"emit",
|
|
261
|
+
"--config",
|
|
262
|
+
schemaPath
|
|
263
|
+
],
|
|
264
|
+
displayCommand: "npx --no-install prisma-next contract emit"
|
|
265
|
+
}, {
|
|
266
|
+
args: [
|
|
267
|
+
"--no-install",
|
|
268
|
+
"prisma-next",
|
|
269
|
+
"db",
|
|
270
|
+
"init",
|
|
271
|
+
"--config",
|
|
272
|
+
schemaPath,
|
|
273
|
+
"--db",
|
|
274
|
+
databaseUrl
|
|
275
|
+
],
|
|
276
|
+
displayCommand: "npx --no-install prisma-next db init"
|
|
277
|
+
}];
|
|
278
|
+
}
|
|
279
|
+
function defaultSchemaSourcePath(schema) {
|
|
280
|
+
return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
|
|
281
|
+
}
|
|
282
|
+
async function runPrismaCommand(options) {
|
|
283
|
+
const shouldPipeOutput = !options.context.flags.json && !options.context.flags.quiet;
|
|
284
|
+
const child = spawn("npx", options.args, {
|
|
285
|
+
cwd: options.context.runtime.cwd,
|
|
286
|
+
env: {
|
|
287
|
+
...options.context.runtime.env,
|
|
288
|
+
...options.env
|
|
289
|
+
},
|
|
290
|
+
signal: options.context.runtime.signal,
|
|
291
|
+
stdio: shouldPipeOutput ? [
|
|
292
|
+
"ignore",
|
|
293
|
+
"pipe",
|
|
294
|
+
"pipe"
|
|
295
|
+
] : [
|
|
296
|
+
"ignore",
|
|
297
|
+
"ignore",
|
|
298
|
+
"ignore"
|
|
299
|
+
]
|
|
300
|
+
});
|
|
301
|
+
if (shouldPipeOutput) {
|
|
302
|
+
child.stdout?.pipe(options.context.output.stderr, { end: false });
|
|
303
|
+
child.stderr?.pipe(options.context.output.stderr, { end: false });
|
|
304
|
+
}
|
|
305
|
+
const exit = await new Promise((resolve, reject) => {
|
|
306
|
+
child.once("error", reject);
|
|
307
|
+
child.once("close", (code, signal) => resolve({
|
|
308
|
+
code,
|
|
309
|
+
signal
|
|
310
|
+
}));
|
|
311
|
+
});
|
|
312
|
+
if (exit.signal) throw new Error(`${options.displayCommand} was terminated by ${exit.signal}.`);
|
|
313
|
+
if (exit.code !== 0) throw new Error(`${options.displayCommand} exited with code ${exit.code ?? 1}.`);
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup };
|
|
@@ -0,0 +1,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 };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/lib/app/preview-branch-database.ts
|
|
2
|
+
async function createBranchDatabase(client, options) {
|
|
3
|
+
const result = await client.POST("/v1/databases", {
|
|
4
|
+
body: {
|
|
5
|
+
projectId: options.projectId,
|
|
6
|
+
branchId: options.branchId,
|
|
7
|
+
name: options.branchName,
|
|
8
|
+
source: { type: "empty" }
|
|
9
|
+
},
|
|
10
|
+
signal: options.signal
|
|
11
|
+
});
|
|
12
|
+
if (result.error || !result.data) throw apiCallError(`Failed to create database for branch "${options.branchName}"`, result.response, result.error);
|
|
13
|
+
return normalizeBranchDatabaseRecord(result.data.data);
|
|
14
|
+
}
|
|
15
|
+
async function listEnvironmentVariables(client, options) {
|
|
16
|
+
const variables = [];
|
|
17
|
+
let cursor;
|
|
18
|
+
while (true) {
|
|
19
|
+
const result = await client.GET("/v1/environment-variables", {
|
|
20
|
+
params: { query: {
|
|
21
|
+
projectId: options.projectId,
|
|
22
|
+
class: options.className,
|
|
23
|
+
key: options.key,
|
|
24
|
+
branchId: options.branchId,
|
|
25
|
+
cursor
|
|
26
|
+
} },
|
|
27
|
+
signal: options.signal
|
|
28
|
+
});
|
|
29
|
+
if (result.error || !result.data) throw apiCallError("Failed to list environment variables", result.response, result.error);
|
|
30
|
+
variables.push(...result.data.data);
|
|
31
|
+
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
32
|
+
cursor = result.data.pagination.nextCursor;
|
|
33
|
+
}
|
|
34
|
+
return variables.map((variable) => normalizeEnvironmentVariable(variable));
|
|
35
|
+
}
|
|
36
|
+
async function createEnvironmentVariable(client, options) {
|
|
37
|
+
const result = await client.POST("/v1/environment-variables", {
|
|
38
|
+
body: {
|
|
39
|
+
projectId: options.projectId,
|
|
40
|
+
class: options.className,
|
|
41
|
+
key: options.key,
|
|
42
|
+
value: options.value,
|
|
43
|
+
...options.branchId ? { branchId: options.branchId } : {}
|
|
44
|
+
},
|
|
45
|
+
signal: options.signal
|
|
46
|
+
});
|
|
47
|
+
if (result.error || !result.data) throw apiCallError(`Failed to add ${options.key}`, result.response, result.error);
|
|
48
|
+
return normalizeEnvironmentVariable(result.data.data);
|
|
49
|
+
}
|
|
50
|
+
async function deleteBranchDatabase(client, options) {
|
|
51
|
+
const result = await client.DELETE("/v1/databases/{databaseId}", {
|
|
52
|
+
params: { path: { databaseId: options.databaseId } },
|
|
53
|
+
signal: options.signal
|
|
54
|
+
});
|
|
55
|
+
if (result.error) throw apiCallError("Failed to delete branch database", result.response, result.error);
|
|
56
|
+
}
|
|
57
|
+
async function updateEnvironmentVariable(client, options) {
|
|
58
|
+
const result = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
59
|
+
params: { path: { envVarId: options.envVarId } },
|
|
60
|
+
body: { value: options.value },
|
|
61
|
+
signal: options.signal
|
|
62
|
+
});
|
|
63
|
+
if (result.error || !result.data) throw apiCallError("Failed to update environment variable", result.response, result.error);
|
|
64
|
+
return normalizeEnvironmentVariable(result.data.data);
|
|
65
|
+
}
|
|
66
|
+
async function deleteEnvironmentVariable(client, options) {
|
|
67
|
+
const result = await client.DELETE("/v1/environment-variables/{envVarId}", {
|
|
68
|
+
params: { path: { envVarId: options.envVarId } },
|
|
69
|
+
signal: options.signal
|
|
70
|
+
});
|
|
71
|
+
if (result.error) throw apiCallError("Failed to delete environment variable", result.response, result.error);
|
|
72
|
+
}
|
|
73
|
+
function normalizeEnvironmentVariable(variable) {
|
|
74
|
+
return {
|
|
75
|
+
id: variable.id,
|
|
76
|
+
key: variable.key,
|
|
77
|
+
branchId: variable.branchId,
|
|
78
|
+
className: variable.class,
|
|
79
|
+
isManagedBySystem: variable.isManagedBySystem
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function normalizeBranchDatabaseRecord(database) {
|
|
83
|
+
const connection = database.connections?.[0];
|
|
84
|
+
const databaseUrl = connection?.endpoints?.pooled?.connectionString;
|
|
85
|
+
const directUrl = connection?.endpoints?.direct?.connectionString ?? null;
|
|
86
|
+
if (!databaseUrl) throw new Error("Created database did not return a pooled connection string.");
|
|
87
|
+
return {
|
|
88
|
+
id: database.id,
|
|
89
|
+
name: database.name,
|
|
90
|
+
branchId: database.branchId,
|
|
91
|
+
databaseUrl,
|
|
92
|
+
directUrl
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function apiCallError(summary, response, error) {
|
|
96
|
+
if (response.status === 404) return /* @__PURE__ */ new Error("Resource Not Found");
|
|
97
|
+
const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`;
|
|
98
|
+
const hint = error.error?.hint ? ` ${error.error.hint}` : "";
|
|
99
|
+
return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { envVarNames } from "./env-vars.js";
|
|
2
2
|
import { PreviewBuildStrategy } from "./preview-build.js";
|
|
3
|
+
import { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable } from "./preview-branch-database.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
|
|
5
6
|
//#region src/lib/app/preview-provider.ts
|
|
@@ -36,6 +37,36 @@ function createPreviewAppProvider(client, options) {
|
|
|
36
37
|
signal: options?.signal
|
|
37
38
|
});
|
|
38
39
|
},
|
|
40
|
+
async resolveBranch(projectId, options) {
|
|
41
|
+
const branch = await resolveOrCreateBranch(client, {
|
|
42
|
+
projectId,
|
|
43
|
+
gitName: options.branchName,
|
|
44
|
+
signal: options.signal
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
id: branch.id,
|
|
48
|
+
name: branch.gitName,
|
|
49
|
+
role: branch.role
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
async createBranchDatabase(options) {
|
|
53
|
+
return createBranchDatabase(client, options);
|
|
54
|
+
},
|
|
55
|
+
async deleteBranchDatabase(options) {
|
|
56
|
+
return deleteBranchDatabase(client, options);
|
|
57
|
+
},
|
|
58
|
+
async listEnvironmentVariables(options) {
|
|
59
|
+
return listEnvironmentVariables(client, options);
|
|
60
|
+
},
|
|
61
|
+
async createEnvironmentVariable(options) {
|
|
62
|
+
return createEnvironmentVariable(client, options);
|
|
63
|
+
},
|
|
64
|
+
async updateEnvironmentVariable(options) {
|
|
65
|
+
return updateEnvironmentVariable(client, options);
|
|
66
|
+
},
|
|
67
|
+
async deleteEnvironmentVariable(options) {
|
|
68
|
+
return deleteEnvironmentVariable(client, options);
|
|
69
|
+
},
|
|
39
70
|
async removeApp(appId, options) {
|
|
40
71
|
const appResult = await sdk.showService({
|
|
41
72
|
serviceId: appId,
|
|
@@ -316,17 +347,19 @@ async function listBranches(client, options) {
|
|
|
316
347
|
signal: options.signal
|
|
317
348
|
});
|
|
318
349
|
if (result.error || !result.data) throw apiCallError("Failed to list branches", result.response, result.error);
|
|
319
|
-
return result.data.data
|
|
350
|
+
return result.data.data.map((branch) => ({
|
|
351
|
+
id: branch.id,
|
|
352
|
+
gitName: branch.gitName,
|
|
353
|
+
isDefault: branch.isDefault,
|
|
354
|
+
role: branch.role
|
|
355
|
+
}));
|
|
320
356
|
}
|
|
321
357
|
async function resolveOrCreateBranch(client, options) {
|
|
322
358
|
const existing = (await listBranches(client, options))[0];
|
|
323
359
|
if (existing) return existing;
|
|
324
360
|
const result = await client.POST("/v1/projects/{projectId}/branches", {
|
|
325
361
|
params: { path: { projectId: options.projectId } },
|
|
326
|
-
body: {
|
|
327
|
-
gitName: options.gitName,
|
|
328
|
-
isDefault: options.gitName === "main"
|
|
329
|
-
},
|
|
362
|
+
body: { gitName: options.gitName },
|
|
330
363
|
signal: options.signal
|
|
331
364
|
});
|
|
332
365
|
if (result.error || !result.data) {
|
|
@@ -336,7 +369,13 @@ async function resolveOrCreateBranch(client, options) {
|
|
|
336
369
|
}
|
|
337
370
|
throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
|
|
338
371
|
}
|
|
339
|
-
|
|
372
|
+
const branch = result.data.data;
|
|
373
|
+
return {
|
|
374
|
+
id: branch.id,
|
|
375
|
+
gitName: branch.gitName,
|
|
376
|
+
isDefault: branch.isDefault,
|
|
377
|
+
role: branch.role
|
|
378
|
+
};
|
|
340
379
|
}
|
|
341
380
|
async function listComputeServices(client, options) {
|
|
342
381
|
const services = [];
|