diff --git a/.gitignore b/.gitignore
index 70358b9..037c95d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+.open-next/
# TypeScript
*.tsbuildinfo
diff --git a/claim-db-worker/next-env.d.ts b/claim-db-worker/next-env.d.ts
new file mode 100644
index 0000000..1b3be08
--- /dev/null
+++ b/claim-db-worker/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/create-db-worker/src/index.ts b/create-db-worker/src/index.ts
index 038ec70..60f14fb 100644
--- a/create-db-worker/src/index.ts
+++ b/create-db-worker/src/index.ts
@@ -70,30 +70,39 @@ export default {
return new Response('Missing region or name in request body', { status: 400 });
}
- const payload = JSON.stringify({ region, name });
const prismaResponse = await fetch('https://api.prisma.io/v1/projects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${env.INTEGRATION_TOKEN}`,
},
- body: payload,
+ body: JSON.stringify({
+ region,
+ name,
+ }),
});
const prismaText = await prismaResponse.text();
- // Trigger delete workflow for the new project
- try {
- const response = JSON.parse(prismaText);
- const projectID = response.data ? response.data.id : response.id;
- await env.DELETE_DB_WORKFLOW.create({ params: { projectID } });
- env.CREATE_DB_DATASET.writeDataPoint({
- blobs: ['database_created'],
- indexes: ['create_db'],
- });
- } catch (e) {
- console.error('Error parsing prismaText or triggering workflow:', e);
- }
+ const backgroundTasks = async () => {
+ try {
+ const response = JSON.parse(prismaText);
+ const projectID = response.data ? response.data.id : response.id;
+
+ const workflowPromise = env.DELETE_DB_WORKFLOW.create({ params: { projectID } });
+
+ const analyticsPromise = env.CREATE_DB_DATASET.writeDataPoint({
+ blobs: ['database_created'],
+ indexes: ['create_db'],
+ });
+
+ await Promise.all([workflowPromise, analyticsPromise]);
+ } catch (e) {
+ console.error('Error in background tasks:', e);
+ }
+ };
+
+ ctx.waitUntil(backgroundTasks());
return new Response(prismaText, {
status: prismaResponse.status,
diff --git a/create-db/index.js b/create-db/index.js
index b5fd1b4..798a35e 100755
--- a/create-db/index.js
+++ b/create-db/index.js
@@ -3,14 +3,7 @@
import dotenv from "dotenv";
dotenv.config();
-import {
- select,
- spinner,
- intro,
- outro,
- log,
- cancel,
-} from "@clack/prompts";
+import { select, spinner, intro, outro, log, cancel } from "@clack/prompts";
import chalk from "chalk";
import terminalLink from "terminal-link";
import { analytics } from "./analytics.js";
@@ -71,9 +64,7 @@ async function showHelp() {
if (regions && regions.length > 0) {
regionExamples = regions.map((r) => r.id).join(", ");
}
- } catch {
- // Fallback to default examples if fetching fails
- }
+ } catch {}
console.log(`
${chalk.cyan.bold("Prisma Postgres Create DB")}
@@ -84,6 +75,8 @@ Usage:
Options:
${chalk.yellow(`--region , -r `)} Specify the region (e.g., ${regionExamples})
${chalk.yellow("--interactive, -i")} Run in interactive mode to select a region and create the database
+ ${chalk.yellow("--json, -j")} Output machine-readable JSON and exit
+ ${chalk.yellow("--list-regions")} List available regions and exit
${chalk.yellow("--help, -h")} Show this help message
Examples:
@@ -91,20 +84,27 @@ Examples:
${chalk.gray(`npx ${CLI_NAME} -r us-east-1`)}
${chalk.gray(`npx ${CLI_NAME} --interactive`)}
${chalk.gray(`npx ${CLI_NAME} -i`)}
+ ${chalk.gray(`npx ${CLI_NAME} --json --region us-east-1`)}
`);
process.exit(0);
}
-// Parse command line arguments into flags and positional arguments
async function parseArgs() {
const args = process.argv.slice(2);
const flags = {};
- const allowedFlags = ["region", "help", "list-regions", "interactive"];
+ const allowedFlags = [
+ "region",
+ "help",
+ "list-regions",
+ "interactive",
+ "json",
+ ];
const shorthandMap = {
r: "region",
i: "interactive",
h: "help",
+ j: "json",
};
const exitWithError = (message) => {
@@ -116,7 +116,6 @@ async function parseArgs() {
for (let i = 0; i < args.length; i++) {
const arg = args[i];
- // Handle long flags (--region, --help, etc.)
if (arg.startsWith("--")) {
const flag = arg.slice(2);
if (flag === "help") await showHelp();
@@ -134,11 +133,9 @@ async function parseArgs() {
continue;
}
- // Handle short and multi-letter shorthand flags
if (arg.startsWith("-")) {
const short = arg.slice(1);
- // Check if it's a multi-letter shorthand like -cs or -lr
if (shorthandMap[short]) {
const mappedFlag = shorthandMap[short];
if (mappedFlag === "help") showHelp();
@@ -154,7 +151,6 @@ async function parseArgs() {
continue;
}
- // Fall back to single-letter flags like -r -l
for (const letter of short.split("")) {
const mappedFlag = shorthandMap[letter];
if (!mappedFlag) exitWithError(`Invalid flag: -${letter}`);
@@ -181,14 +177,16 @@ async function parseArgs() {
return { flags };
}
-/**
- * Fetch available regions from the API.
- */
-export async function getRegions() {
+export async function getRegions(returnJson = false) {
const url = `${CREATE_DB_WORKER_URL}/regions`;
const res = await fetch(url);
if (!res.ok) {
+ if (returnJson) {
+ throw new Error(
+ `Failed to fetch regions. Status: ${res.status} ${res.statusText}`
+ );
+ }
handleError(
`Failed to fetch regions. Status: ${res.status} ${res.statusText}`
);
@@ -199,18 +197,23 @@ export async function getRegions() {
const regions = Array.isArray(data) ? data : data.data;
return regions.filter((region) => region.status === "available");
} catch (e) {
+ if (returnJson) {
+ throw new Error("Failed to parse JSON from /regions endpoint.");
+ }
handleError("Failed to parse JSON from /regions endpoint.", e);
}
}
-/**
- * Validate the provided region against the available list.
- */
-export async function validateRegion(region) {
- const regions = await getRegions();
+export async function validateRegion(region, returnJson = false) {
+ const regions = await getRegions(returnJson);
const regionIds = regions.map((r) => r.id);
if (!regionIds.includes(region)) {
+ if (returnJson) {
+ throw new Error(
+ `Invalid region: ${region}. Available regions: ${regionIds.join(", ")}`
+ );
+ }
handleError(
`Invalid region: ${chalk.yellow(region)}.\nAvailable regions: ${chalk.green(
regionIds.join(", ")
@@ -221,9 +224,6 @@ export async function validateRegion(region) {
return region;
}
-/**
- * Prettified error handler
- */
function handleError(message, extra = "") {
console.error(
"\n" +
@@ -239,8 +239,6 @@ function handleError(message, extra = "") {
process.exit(1);
}
-// Get region from user input
-
async function promptForRegion(defaultRegion) {
let regions;
try {
@@ -265,24 +263,23 @@ async function promptForRegion(defaultRegion) {
process.exit(0);
}
- // Track region selection event
try {
await analytics.capture("create_db:region_selected", {
command: CLI_NAME,
region: region,
- "selection-method": "interactive"
+ "selection-method": "interactive",
});
- } catch (error) {
- // Silently fail analytics
- }
+ } catch (error) {}
return region;
}
-// Create a database
-async function createDatabase(name, region) {
- const s = spinner();
- s.start("Creating your database...");
+async function createDatabase(name, region, returnJson = false) {
+ let s;
+ if (!returnJson) {
+ s = spinner();
+ s.start("Creating your database...");
+ }
const resp = await fetch(`${CREATE_DB_WORKER_URL}/create`, {
method: "POST",
@@ -290,13 +287,22 @@ async function createDatabase(name, region) {
body: JSON.stringify({ region, name, utm_source: CLI_NAME }),
});
- // Rate limit exceeded
if (resp.status === 429) {
- s.stop(
- "We're experiencing a high volume of requests. Please try again later."
- );
-
- // Track database creation failure
+ if (returnJson) {
+ return {
+ error: "rate_limit_exceeded",
+ message:
+ "We're experiencing a high volume of requests. Please try again later.",
+ status: 429,
+ };
+ }
+
+ if (s) {
+ s.stop(
+ "We're experiencing a high volume of requests. Please try again later."
+ );
+ }
+
try {
await analytics.capture("create_db:database_creation_failed", {
command: CLI_NAME,
@@ -304,21 +310,92 @@ async function createDatabase(name, region) {
"error-type": "rate_limit",
"status-code": 429,
});
- } catch (error) {
- // Silently fail analytics
+ } catch (error) {}
+
+ process.exit(1);
+ }
+
+ let result;
+ let raw;
+ try {
+ raw = await resp.text();
+ result = JSON.parse(raw);
+ } catch (e) {
+ if (returnJson) {
+ return {
+ error: "invalid_json",
+ message: "Unexpected response from create service.",
+ raw,
+ status: resp.status,
+ };
}
-
+ if (s) {
+ s.stop("Unexpected response from create service.");
+ }
+ try {
+ await analytics.capture("create_db:database_creation_failed", {
+ command: CLI_NAME,
+ region,
+ "error-type": "invalid_json",
+ "status-code": resp.status,
+ });
+ } catch {}
process.exit(1);
}
- const result = await resp.json();
+ const database = result.data ? result.data.database : result.databases?.[0];
+ const projectId = result.data ? result.data.id : result.id;
+ const prismaConn = database?.connectionString;
+
+ const directConnDetails = result.data
+ ? database?.apiKeys?.[0]?.directConnection
+ : result.databases?.[0]?.apiKeys?.[0]?.ppgDirectConnection;
+ const directUser = directConnDetails?.user
+ ? encodeURIComponent(directConnDetails.user)
+ : "";
+ const directPass = directConnDetails?.pass
+ ? encodeURIComponent(directConnDetails.pass)
+ : "";
+ const directHost = directConnDetails?.host;
+ const directPort = directConnDetails?.port
+ ? `:${directConnDetails.port}`
+ : "";
+ const directDbName = directConnDetails?.database || "postgres";
+ const directConn =
+ directConnDetails && directHost
+ ? `postgresql://${directUser}:${directPass}@${directHost}${directPort}/${directDbName}`
+ : null;
+
+ const claimUrl = `${CLAIM_DB_WORKER_URL}?projectID=${projectId}&utm_source=${CLI_NAME}&utm_medium=cli`;
+ const expiryDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
+
+ if (returnJson && !result.error) {
+ return {
+ connectionString: prismaConn,
+ directConnectionString: directConn,
+ claimUrl: claimUrl,
+ deletionDate: expiryDate.toISOString(),
+ region: database?.region?.id || region,
+ name: database?.name,
+ projectId: projectId,
+ };
+ }
if (result.error) {
- s.stop(
- `Error creating database: ${result.error.message || "Unknown error"}`
- );
-
- // Track database creation failure
+ if (returnJson) {
+ return {
+ error: "api_error",
+ message: result.error.message || "Unknown error",
+ details: result.error,
+ };
+ }
+
+ if (s) {
+ s.stop(
+ `Error creating database: ${result.error.message || "Unknown error"}`
+ );
+ }
+
try {
await analytics.capture("create_db:database_creation_failed", {
command: CLI_NAME,
@@ -326,31 +403,20 @@ async function createDatabase(name, region) {
"error-type": "api_error",
"error-message": result.error.message,
});
- } catch (error) {
- // Silently fail analytics
- }
+ } catch (error) {}
process.exit(1);
}
- s.stop("Database created successfully!");
+ if (s) {
+ s.stop("Database created successfully!");
+ }
- const expiryDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
const expiryFormatted = expiryDate.toLocaleString();
log.message("");
- // Determine which connection string to display
- const database = result.data ? result.data.database : result.databases?.[0];
- const prismaConn = database?.connectionString;
- const directConnDetails = result.data
- ? database?.apiKeys?.[0]?.directConnection
- : result.databases?.[0]?.apiKeys?.[0]?.ppgDirectConnection;
- const directConn = directConnDetails
- ? `postgresql://${directConnDetails.user}:${directConnDetails.pass}@${directConnDetails.host}/postgres`
- : null;
log.info(chalk.bold("Connect to your database →"));
- // Show Prisma Postgres connection string
if (prismaConn) {
log.message(
chalk.magenta(" Use this connection string optimized for Prisma ORM:")
@@ -359,7 +425,6 @@ async function createDatabase(name, region) {
log.message("");
}
- // Show Direct connection string (if available)
if (directConn) {
log.message(
chalk.cyan(" Use this connection string for everything else:")
@@ -374,9 +439,6 @@ async function createDatabase(name, region) {
);
}
- // Claim Database
- const projectId = result.data ? result.data.id : result.id;
- const claimUrl = `${CLAIM_DB_WORKER_URL}?projectID=${projectId}&utm_source=${CLI_NAME}&utm_medium=cli`;
const clickableUrl = terminalLink(claimUrl, claimUrl, { fallback: false });
log.success(`${chalk.bold("Claim your database →")}`);
log.message(
@@ -394,35 +456,32 @@ async function createDatabase(name, region) {
);
}
-// Main function
-
async function main() {
try {
const rawArgs = process.argv.slice(2);
try {
await analytics.capture("create_db:cli_command_ran", {
command: CLI_NAME,
- "full-command": `${CLI_NAME} ${rawArgs.join(' ')}`.trim(),
- "has-region-flag": rawArgs.includes('--region') || rawArgs.includes('-r'),
- "has-interactive-flag": rawArgs.includes('--interactive') || rawArgs.includes('-i'),
- "has-help-flag": rawArgs.includes('--help') || rawArgs.includes('-h'),
- "has-list-regions-flag": rawArgs.includes('--list-regions'),
+ "full-command": `${CLI_NAME} ${rawArgs.join(" ")}`.trim(),
+ "has-region-flag":
+ rawArgs.includes("--region") || rawArgs.includes("-r"),
+ "has-interactive-flag":
+ rawArgs.includes("--interactive") || rawArgs.includes("-i"),
+ "has-help-flag": rawArgs.includes("--help") || rawArgs.includes("-h"),
+ "has-list-regions-flag": rawArgs.includes("--list-regions"),
+ "has-json-flag": rawArgs.includes("--json") || rawArgs.includes("-j"),
"node-version": process.version,
platform: process.platform,
- arch: process.arch
+ arch: process.arch,
});
- } catch (error) {
- // Silently fail analytics
- }
+ } catch (error) {}
- // Parse command line arguments
const { flags } = await parseArgs();
- if (!flags.help) {
+ if (!flags.help && !flags.json) {
await isOffline();
}
- // Set default values
let name = new Date().toISOString();
let region = "us-east-1";
let chooseRegionPrompt = false;
@@ -436,25 +495,44 @@ async function main() {
process.exit(0);
}
- // Apply command line flags
if (flags.region) {
region = flags.region;
-
- // Track region selection via flag
+
try {
await analytics.capture("create_db:region_selected", {
command: CLI_NAME,
region: region,
- "selection-method": "flag"
+ "selection-method": "flag",
});
- } catch (error) {
- // Silently fail analytics
- }
+ } catch (error) {}
}
+
if (flags.interactive) {
chooseRegionPrompt = true;
}
+ if (flags.json) {
+ try {
+ if (chooseRegionPrompt) {
+ region = await promptForRegion(region);
+ } else {
+ await validateRegion(region, true);
+ }
+ const result = await createDatabase(name, region, true);
+ console.log(JSON.stringify(result, null, 2));
+ process.exit(0);
+ } catch (e) {
+ console.log(
+ JSON.stringify(
+ { error: "cli_error", message: e?.message || String(e) },
+ null,
+ 2
+ )
+ );
+ process.exit(1);
+ }
+ }
+
intro(chalk.cyan.bold("🚀 Creating a Prisma Postgres database"));
log.message(
chalk.white(`Provisioning a temporary database in ${region}...`)
@@ -464,16 +542,12 @@ async function main() {
`It will be automatically deleted in 24 hours, but you can claim it.`
)
);
- // Interactive mode prompts
if (chooseRegionPrompt) {
- // Prompt for region
region = await promptForRegion(region);
}
- // Validate the region
region = await validateRegion(region);
- // Create the database
await createDatabase(name, region);
outro("");