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
38 changes: 26 additions & 12 deletions packages/wb/src/commands/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -63,26 +64,38 @@ const deployForceCommand: CommandModule<unknown, InferredOptionTypes<typeof buil
},
};

const litestreamCommand: CommandModule<unknown, InferredOptionTypes<typeof builder>> = {
command: 'litestream',
describe: 'Setup DB for Litestream',
const createLitestreamConfigCommand: CommandModule<unknown, InferredOptionTypes<typeof builder>> = {
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<unknown, InferredOptionTypes<typeof builder>> = {
command: 'create-litestream-config',
describe: 'Create Litestream configuration file',
const listBackupsCommand: CommandModule<unknown, InferredOptionTypes<typeof builder>> = {
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<unknown, InferredOptionTypes<typeof builder>> = {
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);
}
},
};
Expand Down Expand Up @@ -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
`;
Expand Down
36 changes: 32 additions & 4 deletions packages/wb/src/scripts/prismaScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
})();
'`;
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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();