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
18 changes: 13 additions & 5 deletions packages/wb/src/commands/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,22 @@ const generateCommand: CommandModule<unknown, InferredOptionTypes<typeof builder
},
};

const listBackupsCommand: CommandModule<unknown, InferredOptionTypes<typeof builder>> = {
const litestreamConfigBuilder = {
...builder,
config: {
description: 'Path of the Litestream configuration file.',
type: 'string',
},
} as const;

const listBackupsCommand: CommandModule<unknown, InferredOptionTypes<typeof litestreamConfigBuilder>> = {
command: 'list-backups',
describe: 'List Litestream backups',
builder,
builder: litestreamConfigBuilder,
async handler(argv) {
const allProjects = await findDatabaseOrmProjects(argv);
for (const { orm, project } of prepareForRunningDatabaseOrmCommand('db list-backups', allProjects)) {
await runWithSpawn(getDatabaseOrmScripts(orm).listBackups(project), project, argv);
await runWithSpawn(getDatabaseOrmScripts(orm).listBackups(project, argv.config), project, argv);
}
},
};
Expand Down Expand Up @@ -214,7 +222,7 @@ const resetCommand: CommandModule<unknown, InferredOptionTypes<typeof builder>>
};

const restoreBuilder = {
...builder,
...litestreamConfigBuilder,
output: {
description: 'Output path of the restored database. Defaults to "<db|drizzle|prisma>/restored.sqlite3".',
type: 'string',
Expand All @@ -229,7 +237,7 @@ const restoreCommand: CommandModule<unknown, InferredOptionTypes<typeof restoreB
const allProjects = await findDatabaseOrmProjects(argv);
for (const { orm, project } of prepareForRunningDatabaseOrmCommand('db restore', allProjects)) {
const output = argv.output ?? getDefaultRestoreOutput(project, orm);
await runWithSpawn(getDatabaseOrmScripts(orm).restore(project, output), project, argv);
await runWithSpawn(getDatabaseOrmScripts(orm).restore(project, output, argv.config), project, argv);
}
},
};
Expand Down
12 changes: 7 additions & 5 deletions packages/wb/src/scripts/drizzleScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ class DrizzleScripts {
&& litestream restore ${litestreamConfigOption} -o "${dbPath}" "${dbPath}" && ls -ahl "${dbPath}" && ALLOW_TO_SKIP_SEED=0 ${this.deploy(project)}`;
}

listBackups(project: Project): string {
listBackups(project: Project, configPath?: string): string {
const dbPath = getAbsoluteSqliteDbPath(project, 'list-backups');
return `litestream ltx ${getLitestreamConfigOption(project)} "${dbPath}"`;
return `litestream ltx ${getLitestreamConfigOption(project, configPath)} "${dbPath}"`;
}

restore(project: Project, outputPath: string): string {
restore(project: Project, outputPath: string, configPath?: string): string {
const dbPath = getAbsoluteSqliteDbPath(project, 'restore');
return `${buildRemoveSqliteDbCommandForPath(outputPath)}; litestream restore ${getLitestreamConfigOption(project)} -o "${outputPath}" "${dbPath}"`;
return `${buildRemoveSqliteDbCommandForPath(outputPath)}; litestream restore ${getLitestreamConfigOption(project, configPath)} -o "${outputPath}" "${dbPath}"`;
}

generate(_project: Project, additionalOptions = ''): string {
Expand Down Expand Up @@ -113,7 +113,9 @@ function getSqliteDbPath(project: Project): string | undefined {
return getAbsoluteFileDatabaseUrlPath(project);
}

function getLitestreamConfigOption(project: Project): string {
function getLitestreamConfigOption(project: Project, configPath?: string): string {
if (configPath) return `-config "${configPath}"`;

const localConfigPath = path.join(project.dirPath, LITESTREAM_CONFIG_FILE_NAME);
Comment on lines +116 to 119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In a monorepo/workspace setup, the command is executed from the package directory (project.dirPath), but the user might specify a relative --config path from the workspace root (where they ran the command). Resolving configPath to an absolute path using path.resolve() ensures it is correctly located regardless of the execution directory.

Suggested change
function getLitestreamConfigOption(project: Project, configPath?: string): string {
if (configPath) return `-config "${configPath}"`;
const localConfigPath = path.join(project.dirPath, LITESTREAM_CONFIG_FILE_NAME);
function getLitestreamConfigOption(project: Project, configPath?: string): string {
if (configPath) return `-config "${path.resolve(configPath)}"`;
const localConfigPath = path.join(project.dirPath, LITESTREAM_CONFIG_FILE_NAME);

if (fs.existsSync(localConfigPath)) return `-config ./${LITESTREAM_CONFIG_FILE_NAME}`;
if (fs.existsSync(DEFAULT_LITESTREAM_CONFIG_PATH)) return `-config ${DEFAULT_LITESTREAM_CONFIG_PATH}`;
Expand Down
12 changes: 7 additions & 5 deletions packages/wb/src/scripts/prismaScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ class PrismaScripts {
&& litestream restore ${litestreamConfigOption} -o ${dirPath}/prod.sqlite3 ${dirPath}/prod.sqlite3 && ls -ahl ${dirPath}/prod.sqlite3 && ALLOW_TO_SKIP_SEED=0 PRISMA migrate deploy`;
}

listBackups(project: Project): string {
listBackups(project: Project, configPath?: string): string {
const dirPath = getDatabaseDirPath(project);
return `litestream ltx ${getLitestreamConfigOption(project)} ${dirPath}/prod.sqlite3`;
return `litestream ltx ${getLitestreamConfigOption(project, configPath)} ${dirPath}/prod.sqlite3`;
}
Comment on lines +48 to 51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent issues with spaces or special characters in the database directory path, wrap the SQLite database path in double quotes, similar to how it is done in Drizzle scripts.

Suggested change
listBackups(project: Project, configPath?: string): string {
const dirPath = getDatabaseDirPath(project);
return `litestream ltx ${getLitestreamConfigOption(project)} ${dirPath}/prod.sqlite3`;
return `litestream ltx ${getLitestreamConfigOption(project, configPath)} ${dirPath}/prod.sqlite3`;
}
listBackups(project: Project, configPath?: string): string {
const dirPath = getDatabaseDirPath(project);
return `litestream ltx ${getLitestreamConfigOption(project, configPath)} "${dirPath}/prod.sqlite3"`;
}


migrate(project: Project, additionalOptions = ''): string {
Expand All @@ -72,9 +72,9 @@ class PrismaScripts {
return steps.filter(Boolean).join(' && ');
}

restore(project: Project, outputPath: string): string {
restore(project: Project, outputPath: string, configPath?: string): string {
const dirPath = getDatabaseDirPath(project);
return `rm -Rf ${outputPath}*; litestream restore ${getLitestreamConfigOption(project)} -o ${outputPath} ${dirPath}/prod.sqlite3`;
return `rm -Rf ${outputPath}*; litestream restore ${getLitestreamConfigOption(project, configPath)} -o ${outputPath} ${dirPath}/prod.sqlite3`;
}
Comment on lines +75 to 78

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Wrap outputPath and the SQLite database path in double quotes to ensure the command is robust against spaces or special characters in paths.

Suggested change
restore(project: Project, outputPath: string, configPath?: string): string {
const dirPath = getDatabaseDirPath(project);
return `rm -Rf ${outputPath}*; litestream restore ${getLitestreamConfigOption(project)} -o ${outputPath} ${dirPath}/prod.sqlite3`;
return `rm -Rf ${outputPath}*; litestream restore ${getLitestreamConfigOption(project, configPath)} -o ${outputPath} ${dirPath}/prod.sqlite3`;
}
restore(project: Project, outputPath: string, configPath?: string): string {
const dirPath = getDatabaseDirPath(project);
return `rm -Rf "${outputPath}"*; litestream restore ${getLitestreamConfigOption(project, configPath)} -o "${outputPath}" "${dirPath}/prod.sqlite3"`;
}


seed(project: Project, scriptPath?: string): string {
Expand Down Expand Up @@ -115,7 +115,9 @@ function getPrismaBaseDir(project: Project): string | undefined {
?.dbPath;
}

function getLitestreamConfigOption(project: Project): string {
function getLitestreamConfigOption(project: Project, configPath?: string): string {
if (configPath) return `-config "${configPath}"`;

const localConfigPath = path.join(project.dirPath, LITESTREAM_CONFIG_FILE_NAME);
Comment on lines +118 to 121

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In a monorepo/workspace setup, the command is executed from the package directory (project.dirPath), but the user might specify a relative --config path from the workspace root (where they ran the command). Resolving configPath to an absolute path using path.resolve() ensures it is correctly located regardless of the execution directory.

Suggested change
function getLitestreamConfigOption(project: Project, configPath?: string): string {
if (configPath) return `-config "${configPath}"`;
const localConfigPath = path.join(project.dirPath, LITESTREAM_CONFIG_FILE_NAME);
function getLitestreamConfigOption(project: Project, configPath?: string): string {
if (configPath) return `-config "${path.resolve(configPath)}"`;
const localConfigPath = path.join(project.dirPath, LITESTREAM_CONFIG_FILE_NAME);

if (fs.existsSync(localConfigPath)) return `-config ./${LITESTREAM_CONFIG_FILE_NAME}`;
if (fs.existsSync(DEFAULT_LITESTREAM_CONFIG_PATH)) return `-config ${DEFAULT_LITESTREAM_CONFIG_PATH}`;
Expand Down
20 changes: 20 additions & 0 deletions packages/wb/test/scripts/drizzleScripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ describe('drizzleScripts Litestream commands', () => {
);
});

it('lists backups with an explicit Litestream config path', () => {
const project = createDrizzleProject();

const command = drizzleScripts.listBackups(project, '/tmp/litestream.yml');

expect(command).toBe(
`litestream ltx -config "/tmp/litestream.yml" "${path.join(project.rootDirPath, 'drizzle/mount/prod.sqlite3')}"`
);
});

it('restores backups to the requested output path', () => {
const project = createDrizzleProject();

Expand All @@ -26,6 +36,16 @@ describe('drizzleScripts Litestream commands', () => {
);
});

it('restores backups with an explicit Litestream config path', () => {
const project = createDrizzleProject();

const command = drizzleScripts.restore(project, '/tmp/restored.sqlite3', '/tmp/litestream.yml');

expect(command).toBe(
`rm -f "/tmp/restored.sqlite3" "/tmp/restored.sqlite3-wal" "/tmp/restored.sqlite3-shm"; litestream restore -config "/tmp/litestream.yml" -o "/tmp/restored.sqlite3" "${path.join(project.rootDirPath, 'drizzle/mount/prod.sqlite3')}"`
);
});

it('requires a file DATABASE_URL for backup operations', () => {
const project = createDrizzleProject({ DATABASE_URL: 'postgresql://localhost:5432/db' });

Expand Down
Loading