Skip to content

Instantly share code, notes, and snippets.

@alexanderson1993
Last active November 4, 2024 13:06
Show Gist options
  • Save alexanderson1993/0852a8162ebac591b62a79883a81e1a8 to your computer and use it in GitHub Desktop.
Save alexanderson1993/0852a8162ebac591b62a79883a81e1a8 to your computer and use it in GitHub Desktop.
Prisma D1 Migration CLI
migrate.mov

A handy CLI for working with the new Cloudflare D1/Prisma integration. You can read about that here: https://blog.cloudflare.com/prisma-orm-and-d1

Getting Started

  • Install wrangler, Prisma, and the other dependencies
npm install prisma@latest @prisma/client@latest @prisma/adapter-d1
npm install --save-dev wrangler toml tiny-parse-argv @clack/prompts
  • Create your D1 Database

npx wrangler d1 create prod-prisma-d1-app

  • Create a wrangler.toml file
// wrangler.toml
name="my-d1-prisma-app"
main = "src/index.ts"
compatibility_date = "2024-03-20"
compatibility_flags = ["nodejs_compat"]

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "prod-prisma-d1-app"
database_id = "<unique-ID-for-your-database>"
  • Initialize Prisma
npx prisma init --datasource-provider sqlite
  • Turn on the adapters feature of Prisma
// schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
+ previewFeatures = ["driverAdapters"]
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

Usage

npx tsx prisma/migrate.ts

┌  D1 Prisma Migrate CLI
│
│  migrate <command>
│    Commands:
│      create - Create a new migration
│      apply - Apply pending migrations
│    Options:
│      -h, --help - Show this help message

Each command includes help with the --help option.

import fs from "node:fs/promises";
import path from "node:path";
import { exec } from "node:child_process";
import {
intro,
outro,
log,
select,
text,
spinner,
isCancel,
confirm,
} from "@clack/prompts";
import toml from "toml";
import parseArgv from "tiny-parse-argv";
const args = parseArgv(process.argv.slice(2));
const command = args._[0];
const projectRoot = path.resolve();
const asyncExec = (command: string) =>
new Promise<string>((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(stderr);
} else {
resolve(stdout);
}
});
});
intro("D1 Prisma Migrate CLI");
if (args.help || !command) {
switch (command) {
case "create":
log.message(`migrate create
Create a new migration
Options:
-n, --name - The name of the migration
-d, --database - The name of the D1 database
--create-only - Only create the migration file, do not apply it
--schema - Custom path to the Prisma schema
-h, --help - Show this help message`);
break;
case "apply":
log.message(`migrate apply
Apply pending migrations
Options:
-d, --database - The name of the D1 database
--remote - Apply migrations to your remote database
--schema - Custom path to the Prisma schema
-h, --help - Show this help message`);
break;
default:
log.message(`migrate <command>
Commands:
create - Create a new migration
apply - Apply pending migrations
Options:
-h, --help - Show this help message`);
break;
}
process.exit(0);
}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
let wranglerConfig: any;
// Check wrangler.toml to see what D1 namespaces are used
try {
const wranglerToml = await fs.readFile("wrangler.toml", "utf-8");
wranglerConfig = toml.parse(wranglerToml);
} catch {
log.error("Could not read wrangler.toml");
process.exit(1);
}
const databases: { value: string; label: string }[] =
wranglerConfig.d1_databases?.map((db: { database_name: string }) => ({
value: db.database_name,
label: db.database_name,
})) || [];
let database = args.d || args.database || databases[0]?.value;
if (command === "create") {
const database = await getDatabase();
const migrationName =
args.name ||
args.n ||
(await text({
message: "What is the name of the migration?",
validate: (input) => {
if (input.length === 0) {
return "Migration name cannot be empty";
}
},
}));
if (isCancel(migrationName)) {
process.exit(1);
}
const s = spinner();
s.start("Creating migration");
const result = await asyncExec(
`npx wrangler d1 migrations create ${database} ${migrationName}`
);
s.stop("Creating migration");
const migrationPath = result
.trim()
.split("\n")
.find((line) => line.endsWith(".sql"));
if (!migrationPath) {
log.error("Could not find migration path");
process.exit(1);
}
s.start("Generating migration diff from Prisma schema");
await asyncExec(
`npx prisma migrate diff --script --from-local-d1 --to-schema-datamodel ${
args.schema || "./prisma/schema.prisma"
} >> ${migrationPath}`
);
s.stop("Generating migration diff from Prisma schema");
if (args["create-only"]) {
outro(`Migration created at ${migrationPath.replace(projectRoot, ".")}`);
process.exit();
}
}
if (command === "apply" || command === "create") {
const database = await getDatabase();
const s = spinner();
s.start("Applying migrations");
await asyncExec(
`npx wrangler d1 migrations apply ${database} ${
args.remote ? "--remote" : "--local"
}`
);
s.stop("Applying migrations");
s.start("Generating Prisma client");
await asyncExec(
`npx prisma generate ${args.schema ? `--schema ${args.schema}` : ""}`
);
s.stop("Generating Prisma client");
outro("Migrations applied");
}
async function getDatabase() {
if (databases.length === 0) {
log.error("No D1 databases found in wrangler.toml");
process.exit(1);
}
database =
database ||
(await select({
message: "Select a database",
options: databases,
initialValue: databases[0].value,
}));
if (isCancel(database)) {
process.exit(1);
}
return database;
}
@dpuscher
Copy link

Thank you for your script, really helps with creating migrations easily.

I stumbled upon a bug with longer paths, where the output of the wrangler d1 migrations create command splits into multiple lines. I was able to fix this by using this implementation, but the check is not very solid by just checking for the beginning of the absolute path. If you have another solution, that would be great.

My implementation for reference:

  s.start("Creating migration");
  const result = await asyncExec(
    `npx wrangler d1 migrations create ${database} "${migrationName}"`,
  );

  s.stop("Creating migration");

  const resultLines = result.trim().split("\n");
  let pathIndex = resultLines.findIndex(line => line.endsWith(".sql"));
  if (pathIndex === -1) {
    log.error("Could not find migration path");
    process.exit(1);
  }

  let migrationPath = "";

  while (pathIndex >= 0 && !migrationPath.startsWith("/")) {
    migrationPath = resultLines[pathIndex--] + migrationPath;
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment