Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ This monorepo contains tools and services that enable developers to quickly prov
| `--env`, `-e` | Write `DATABASE_URL` and `CLAIM_URL` to the specified file | `--env .env` |
| `--ttl`, `-t` | Set auto-delete TTL (`30m`, `1h` ... `24h`) | `--ttl 5h` |
| `--copy`, `-c` | Copy connection string to clipboard | `--copy` |
| `--quiet`, `-q` | Output only the connection string | `--quiet` |
| `--quiet`, `-q` | Output only the connection string (claim URL on stderr) | `--quiet` |
| `--open`, `-o` | Open claim URL in browser | `--open` |
| `--help`, `-h` | Show help information | `--help` |

Expand Down
11 changes: 9 additions & 2 deletions packages/create-db/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ npx create-db regions # List available regions
| `--env <path>` | `-e` | Write DATABASE_URL and CLAIM_URL to specified .env file |
| `--ttl <duration>` | `-t` | Custom database TTL (`30m`, `1h` ... `24h`) |
| `--copy` | `-c` | Copy connection string to clipboard |
| `--quiet` | `-q` | Output only the connection string |
| `--quiet` | `-q` | Output only the connection string (the claim URL goes to stderr) |
| `--open` | `-o` | Open claim URL in browser |
| `--help` | `-h` | Show help message |
| `--version` | | Show version |
Expand Down Expand Up @@ -116,10 +116,15 @@ When using `--json` with `create`, the output includes:
"deletionDate": "2025-12-13T12:00:00.000Z",
"region": "us-east-1",
"name": "2025-12-12T12:00:00.000Z",
"projectId": "proj_..."
"projectId": "proj_...",
"nextStep": "The database is deleted at 2025-12-13T12:00:00.000Z unless the user claims it, which keeps it for free. Give the user this claim URL: https://create-db.prisma.io/claim?projectID=..."
}
```

`nextStep` is for agents and scripts that run `create-db` for someone else: the database is deleted at `deletionDate` unless that person opens `claimUrl` and claims it.

Add `--env <path>` to also write `DATABASE_URL` and `CLAIM_URL` to that file. If the file can't be written, the JSON is still printed and the command exits with code 1.

When using `--json` with `regions`, the output is an array of region objects:

```json
Expand All @@ -141,6 +146,8 @@ DATABASE_URL="postgresql://user:pass@host:5432/postgres?sslmode=require"
CLAIM_URL="https://create-db.prisma.io/claim?projectID=..."
```

The CLI also prints the claim URL and deletion time, so the person running it can claim the database before it is deleted.

## Programmatic API

You can also use `create-db` as a library in your Node.js applications:
Expand Down
8 changes: 6 additions & 2 deletions packages/create-db/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { describe, it, expect } from "vitest";
import { execa } from "execa";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CLI_PATH = path.resolve(__dirname, "../dist/cli.mjs");
const { version: PACKAGE_VERSION } = JSON.parse(
fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8")
);

const runCli = async (
args: string[] = [],
Expand Down Expand Up @@ -43,13 +47,13 @@ describe("CLI help and version", () => {
it("displays version with --version flag", async () => {
const result = await runCli(["--version"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
expect(result.stdout.trim()).toBe(PACKAGE_VERSION);
});

it("displays version with -V flag", async () => {
const result = await runCli(["-V"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
expect(result.stdout.trim()).toBe(PACKAGE_VERSION);
});

it("displays create command help", async () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/create-db/__tests__/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, it, expect } from "vitest";
import { execa } from "execa";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
import os from "os";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CLI_PATH = path.resolve(__dirname, "../dist/cli.mjs");
Expand Down Expand Up @@ -62,6 +64,24 @@ describe("CLI database creation", () => {
expect(parsed.success).toBe(true);
}, 30000);

it("writes the env file and prints JSON with --json --env", async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "create-db-json-env-"));
const envPath = path.join(dir, ".env");
try {
const result = await runCli(["--json", "--env", envPath]);
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout.trim());
expect(parsed.success).toBe(true);
expect(parsed.nextStep).toContain(parsed.claimUrl);

const content = fs.readFileSync(envPath, "utf8");
expect(content).toContain(`CLAIM_URL="${parsed.claimUrl}"`);
expect(content).toContain("DATABASE_URL=");
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}, 30000);

it("lists regions with regions command", async () => {
const result = await runCli(["regions"]);
if (result.exitCode !== 0) {
Expand Down
93 changes: 92 additions & 1 deletion packages/create-db/__tests__/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
import { writeEnvFile } from "../src/cli/output.js";
import {
writeEnvFile,
withOutputMode,
formatDeletionDate,
formatJsonResult,
printClaimReminder,
} from "../src/cli/output.js";
import type { DatabaseResult } from "../src/types.js";

describe("writeEnvFile", () => {
let tempDir: string;
Expand Down Expand Up @@ -142,3 +149,87 @@ describe("writeEnvFile", () => {
expect(content).toContain(`DATABASE_URL="${connectionString}"`);
});
});

const sampleResult: DatabaseResult = {
success: true,
connectionString: "postgresql://user:pass@host:5432/postgres",
claimUrl:
"https://create-db.prisma.io/claim?projectID=proj_123&utm_source=create-db&utm_medium=cli",
deletionDate: "2026-10-29T12:00:00.000Z",
region: "us-east-1",
name: "2026-10-28T12:00:00.000Z",
projectId: "proj_123",
};

describe("withOutputMode", () => {
it("appends the output mode to the claim URL as utm_content", () => {
const tagged = withOutputMode(sampleResult, "json");
expect(tagged.claimUrl).toBe(`${sampleResult.claimUrl}&utm_content=json`);
expect(new URL(tagged.claimUrl).searchParams.get("projectID")).toBe("proj_123");
});

it("starts a query string when the claim URL has none", () => {
const tagged = withOutputMode(
{ ...sampleResult, claimUrl: "https://create-db.prisma.io/claim/proj_123" },
"env"
);
expect(tagged.claimUrl).toBe("https://create-db.prisma.io/claim/proj_123?utm_content=env");
});

it("keeps the encoding of existing params", () => {
const claimUrl =
"https://create-db.prisma.io/claim?projectID=proj_123&utm_source=claude-code/my-app&utm_medium=cli";
const tagged = withOutputMode({ ...sampleResult, claimUrl }, "default");
expect(tagged.claimUrl).toBe(`${claimUrl}&utm_content=default`);
});

it("leaves the input result unchanged", () => {
withOutputMode(sampleResult, "quiet");
expect(sampleResult.claimUrl).not.toContain("utm_content");
});
});

describe("formatDeletionDate", () => {
it("includes the time zone", () => {
const date = new Date(sampleResult.deletionDate);
const zone = new Intl.DateTimeFormat(undefined, { timeZoneName: "short" })
.formatToParts(date)
.find((part) => part.type === "timeZoneName")?.value;
expect(zone).toBeTruthy();
expect(formatDeletionDate(sampleResult.deletionDate)).toContain(zone);
});
});

describe("formatJsonResult", () => {
it("keeps every result field and adds a nextStep with the claim URL and deletion time", () => {
const json = formatJsonResult(sampleResult);
expect(json).toMatchObject(sampleResult);
expect(json.nextStep).toContain(sampleResult.claimUrl);
expect(json.nextStep).toContain(sampleResult.deletionDate);
expect(json.nextStep.endsWith(sampleResult.claimUrl)).toBe(true);
});
});

describe("printClaimReminder", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("prints the claim URL to stdout by default", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const error = vi.spyOn(console, "error").mockImplementation(() => {});
printClaimReminder(sampleResult);
expect(log).toHaveBeenCalledTimes(1);
expect(log.mock.calls[0][0]).toContain(sampleResult.claimUrl);
expect(error).not.toHaveBeenCalled();
});

it("prints to stderr when asked, keeping stdout clean", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const error = vi.spyOn(console, "error").mockImplementation(() => {});
printClaimReminder(sampleResult, "stderr");
expect(error).toHaveBeenCalledTimes(1);
expect(error.mock.calls[0][0]).toContain(sampleResult.claimUrl);
expect(log).not.toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion packages/create-db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-db",
"version": "1.2.0",
"version": "1.3.0",
"description": "Instantly create a temporary Prisma Postgres database with one command, then claim and persist it in your Prisma Data Platform project when ready.",
"type": "module",
"exports": {
Expand Down
55 changes: 46 additions & 9 deletions packages/create-db/src/cli/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import {
printJson,
printError,
printSuccess,
printClaimReminder,
formatJsonResult,
withOutputMode,
writeEnvFile,
copyToClipboard,
openUrlInBrowser,
Expand Down Expand Up @@ -57,6 +60,20 @@ function applyOpenFlag(result: DatabaseResult, quiet: boolean) {
}
}

/**
* Exit once stdout and stderr have flushed, so JSON written to a pipe isn't
* cut off. trpc-cli exits with 0 after the handler returns, so setting
* `process.exitCode` and returning would lose the failure code.
*/
async function exitWhenFlushed(code: number): Promise<never> {
await Promise.all(
[process.stdout, process.stderr].map(
(stream) => new Promise<void>((resolve) => stream.write("", () => resolve()))
)
);
process.exit(code);
}

export async function handleCreate(input: CreateFlagsInput): Promise<void> {
const cliRunId = randomUUID();
const CLI_NAME = getCommandName();
Expand Down Expand Up @@ -141,7 +158,7 @@ export async function handleCreate(input: CreateFlagsInput): Promise<void> {
}

await ensureOnline();
const result = await createDatabase(
const created = await createDatabase(
region,
userAgent,
cliRunId,
Expand All @@ -151,18 +168,34 @@ export async function handleCreate(input: CreateFlagsInput): Promise<void> {
await flushAnalytics();

if (input.json) {
printJson(result);
if (!result.success) {
process.exit(1);
if (!created.success) {
printJson(created);
return exitWhenFlushed(1);
}

const result = withOutputMode(created, "json");
const writeResult = envEnabled
? writeEnvFile(envPath!, result.connectionString, result.claimUrl)
: null;

// Print the JSON even if the env write failed, so the connection
// string and claim URL aren't lost.
printJson(formatJsonResult(result));

if (writeResult && !writeResult.success) {
printError(`Failed to write environment variables to ${envPath}: ${writeResult.error}`);
return exitWhenFlushed(1);
}
return;
}

if (!result.success) {
printError(result.message);
if (!created.success) {
printError(created.message);
process.exit(1);
}

const result = withOutputMode(created, input.quiet ? "quiet" : "env");

if (envEnabled) {
const writeResult = writeEnvFile(envPath!, result.connectionString, result.claimUrl);
if (!writeResult.success) {
Expand All @@ -172,11 +205,13 @@ export async function handleCreate(input: CreateFlagsInput): Promise<void> {

if (!input.quiet) {
printSuccess(`Wrote DATABASE_URL and CLAIM_URL to ${envPath}`);
printClaimReminder(result);
}
}

if (input.quiet) {
console.log(result.connectionString ?? "");
printClaimReminder(result, "stderr");
}

if (input.copy) {
Expand Down Expand Up @@ -228,20 +263,22 @@ export async function handleCreate(input: CreateFlagsInput): Promise<void> {
const spinner = createSpinner();
spinner.start(region);

const result = await createDatabase(
const created = await createDatabase(
region,
userAgent,
cliRunId,
"cli",
ttlMs
);

if (!result.success) {
spinner.error(result.message);
if (!created.success) {
spinner.error(created.message);
await flushAnalytics();
process.exit(1);
}

const result = withOutputMode(created, "default");

spinner.success();
printDatabaseResult(result);

Expand Down
Loading
Loading