diff --git a/packages/wb/src/commands/prisma.ts b/packages/wb/src/commands/prisma.ts index 4ef392e1..90547621 100644 --- a/packages/wb/src/commands/prisma.ts +++ b/packages/wb/src/commands/prisma.ts @@ -21,8 +21,9 @@ export const prismaCommand: CommandModule = { return yargs .command(deployCommand) .command(deployForceCommand) - .command(litestreamCommand) .command(createLitestreamConfigCommand) + .command(listBackupsCommand) + .command(litestreamCommand) .command(migrateCommand) .command(migrateDevCommand) .command(resetCommand) @@ -63,26 +64,38 @@ const deployForceCommand: CommandModule> = { - command: 'litestream', - describe: 'Setup DB for Litestream', +const createLitestreamConfigCommand: CommandModule> = { + command: 'create-litestream-config', + describe: 'Create Litestream configuration file', builder, async handler(argv) { const allProjects = await findPrismaProjects(argv); - for (const project of prepareForRunningCommand('prisma litestream', allProjects)) { - await runWithSpawn(prismaScripts.litestream(project), project, argv); + for (const project of prepareForRunningCommand('prisma create-litestream-config', allProjects)) { + createLitestreamConfig(project); } }, }; -const createLitestreamConfigCommand: CommandModule> = { - command: 'create-litestream-config', - describe: 'Create Litestream configuration file', +const listBackupsCommand: CommandModule> = { + command: 'list-backups', + describe: 'List Litestream backups', builder, async handler(argv) { const allProjects = await findPrismaProjects(argv); - for (const project of prepareForRunningCommand('prisma create-litestream-config', allProjects)) { - createLitestreamConfig(project); + for (const project of prepareForRunningCommand('prisma list-backups', allProjects)) { + await runWithSpawn(prismaScripts.listBackups(project), project, argv); + } + }, +}; + +const litestreamCommand: CommandModule> = { + command: 'litestream', + describe: 'Setup DB for Litestream', + builder, + async handler(argv) { + const allProjects = await findPrismaProjects(argv); + for (const project of prepareForRunningCommand('prisma litestream', allProjects)) { + await runWithSpawn(prismaScripts.litestream(project), project, argv); } }, }; @@ -245,7 +258,8 @@ function createLitestreamConfig(project: Project): void { bucket: ${requiredEnvVars.CLOUDFLARE_R2_LITESTREAM_BUCKET_NAME} access-key-id: ${requiredEnvVars.CLOUDFLARE_R2_LITESTREAM_ACCESS_KEY_ID} secret-access-key: ${requiredEnvVars.CLOUDFLARE_R2_LITESTREAM_SECRET_ACCESS_KEY} - retention: 8h + snapshot-interval: 24h # Create a backup per day + retention: 72h # Keep backups for 3 days retention-check-interval: ${retentionCheckInterval} sync-interval: 60s `; diff --git a/packages/wb/src/scripts/prismaScripts.ts b/packages/wb/src/scripts/prismaScripts.ts index 3b69dc34..144ac475 100644 --- a/packages/wb/src/scripts/prismaScripts.ts +++ b/packages/wb/src/scripts/prismaScripts.ts @@ -17,15 +17,38 @@ class PrismaScripts { deployForce(project: Project): string { const dirName = project.packageJson.dependencies?.blitz ? 'db' : 'prisma'; // Don't skip "migrate deploy" because restored database may be older than the current schema. - return `rm -Rf ${dirName}/mount/prod.sqlite3*; PRISMA migrate reset --force && rm -Rf ${dirName}/mount/prod.sqlite3* + return `PRISMA migrate reset --force --skip-seed && rm -Rf ${dirName}/mount/prod.sqlite3* && litestream restore -config litestream.yml -o ${dirName}/mount/prod.sqlite3 ${dirName}/mount/prod.sqlite3 && ls -ahl ${dirName}/mount/prod.sqlite3 && ALLOW_TO_SKIP_SEED=0 PRISMA migrate deploy`; } + listBackups(project: Project): string { + const dirName = project.packageJson.dependencies?.blitz ? 'db' : 'prisma'; + return `litestream ltx -config litestream.yml ${dirName}/mount/prod.sqlite3`; + } + litestream(_: Project): string { + // cf. https://litestream.io/tips/ return `${runtimeWithArgs} -e ' const { PrismaClient } = require("@prisma/client"); -new PrismaClient().$queryRaw\`PRAGMA journal_mode = WAL;\` - .catch((error) => { console.log("Failed due to:", error); process.exit(1); }); +const prisma = new PrismaClient(); +const pragmas = [ + "PRAGMA busy_timeout = 5000;", + "PRAGMA journal_mode = WAL;", + "PRAGMA synchronous = NORMAL;", + "PRAGMA wal_autocheckpoint = 0;", +]; +(async () => { + try { + for (const pragma of pragmas) { + await prisma.$executeRawUnsafe(pragma); + } + } catch (error) { + console.error("Failed due to:", error); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +})(); '`; } @@ -50,7 +73,7 @@ new PrismaClient().$queryRaw\`PRAGMA journal_mode = WAL;\` restore(project: Project, outputPath: string): string { const dirName = project.packageJson.dependencies?.blitz ? 'db' : 'prisma'; - return `rm -Rf ${outputPath}; litestream restore -config litestream.yml -o ${outputPath} ${dirName}/mount/prod.sqlite3`; + return `${this.removeSqliteArtifacts(outputPath)}; litestream restore -config litestream.yml -o ${outputPath} ${dirName}/mount/prod.sqlite3`; } seed(project: Project, scriptPath?: string): string { @@ -92,6 +115,11 @@ new PrismaClient().$queryRaw\`PRAGMA journal_mode = WAL;\` } return `${prefix}PRISMA studio ${additionalOptions}`; } + + private removeSqliteArtifacts(sqlitePath: string): string { + // Litestream requires removing WAL/SHM and Litestream sidecar files when recreating databases. + return `rm -Rf ${sqlitePath} ${sqlitePath}-shm ${sqlitePath}-wal ${sqlitePath}-litestream`; + } } export const prismaScripts = new PrismaScripts();