copybase
Version:
Copy or backup databases quickly
238 lines (209 loc) • 5.54 kB
text/typescript
import os from "os";
import path from "path";
import fs from "fs";
import { cosmiconfigSync } from "cosmiconfig";
import dayjs from "dayjs";
import providers from "./providers";
import { parseDatabaseUri } from "./util";
export interface CopybaseConfig {
currentDir: string;
backup: { folder: string; filenamePattern: string };
databases: Record<
string,
{
database: string;
protocol: keyof typeof providers;
hostname: string;
port: number;
user: string;
password: string;
uri: string;
mongodump: any;
psql: any;
psql_dump: any;
mysql: any;
mysqldump: any;
}
>;
}
export interface CliOptions {
verbose: boolean;
}
/**
* check databaseName exists in the provided copybase configuration
*
* @param databaseName
* @param config
* @returns
*/
function checkDatabaseConfig(databaseName: string, config: CopybaseConfig) {
const dbConfig = config.databases[databaseName];
const databaseNames = Object.keys(config.databases);
const supportedDbTypes = Object.keys(providers);
if (!dbConfig) {
console.error(
`Invalid database source: "${databaseName}". Must be ${databaseNames.join(
", "
)}`
);
return false;
}
if (!providers[dbConfig.protocol]) {
console.error(
`Invalid database protocol: ${
dbConfig.protocol
}. Must be [${supportedDbTypes.join(", ")}]`
);
return false;
}
return true;
}
/**
* Load copybase configuration
*
* @param moduleName
* @returns
*/
export function loadConfig(moduleName: string) {
const explorerSync = cosmiconfigSync(moduleName);
const config = explorerSync.search();
if (!config) {
return null;
}
const databases = config.config.databases;
Object.entries(config.config.databases).map(
([key, dbConfig]: [string, any]) => {
config.config.databases[key] = {
...(dbConfig.uri ? parseDatabaseUri(dbConfig.uri) : {}),
...dbConfig,
};
}
);
return {
currentDir: process.cwd(),
databases,
backup: {
filenamePattern: "YYYYMMDDTHHmmss",
...config?.config.backup,
},
} as any as CopybaseConfig;
}
/**
* backup a database into a given folder
* @param config
* @param param1
* @param options
* @returns
*/
export async function backup(
config: CopybaseConfig,
{ database }: { database: string },
options?: CliOptions
) {
const { verbose } = options || {};
if (!checkDatabaseConfig(database, config)) {
return;
}
if (!config?.backup?.folder) {
console.info("Please provide a backupFolder value in config");
return;
}
console.info(`Backup ${database} into ${config.backup.folder}`);
const dbConfig = config.databases[database];
const provider = new providers[dbConfig.protocol]({ ...dbConfig, verbose });
const outputFile = path.join(
config.backup.folder,
database,
`${dayjs().format(config.backup.filenamePattern)}`
);
await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
await provider.dump(outputFile);
}
/**
* Restore a database from a given folder
* @param config
* @param param1
* @param options
* @returns
*/
export async function restore(
config: CopybaseConfig,
{ from, toDatabase }: { from: string; toDatabase: string },
options?: CliOptions
) {
const { verbose } = options || {};
if (!checkDatabaseConfig(toDatabase, config)) {
return;
}
const dbConfig = config.databases[toDatabase];
const provider = new providers[dbConfig.protocol]({ ...dbConfig, verbose });
const filePath = [
from,
path.join(config.backup.folder, from),
path.join(process.cwd(), from),
].find((filePath) => fs.existsSync(filePath));
if (!filePath) {
console.error(`Folder "${from}" not found`);
return;
}
await provider.restore(from);
}
/**
* Copy a database to another database
*
* @param config
* @param param1
* @param options
* @returns
*/
export async function copy(
config: CopybaseConfig,
{ fromDatabase, toDatabase }: { fromDatabase: string; toDatabase: string },
options?: CliOptions
) {
const { verbose } = options || {};
if (!checkDatabaseConfig(fromDatabase, config)) {
return;
}
if (!checkDatabaseConfig(toDatabase, config)) {
return;
}
const fromConfig = config.databases[fromDatabase];
const toConfig = config.databases[toDatabase];
if (fromConfig.protocol !== fromConfig.protocol) {
console.error(
`Incompatible database protocol: ${fromConfig.protocol} !== ${toConfig.protocol}`
);
return;
}
const from = new providers[fromConfig.protocol]({ ...fromConfig, verbose });
const to = new providers[toConfig.protocol]({ ...toConfig, verbose });
console.info(`Copy ${fromDatabase} to ${toDatabase}`);
const outputFile = path.join(os.tmpdir(), fromDatabase);
const exitCode = await from.dump(outputFile);
if (exitCode !== 0) {
return;
}
await to.restore(outputFile);
}
/**
* List all tables from a given database
* @param config
* @param param1
* @param options
* @returns
*/
export async function listTables(
config: CopybaseConfig,
{ database }: { database: string },
options?: CliOptions
) {
const { verbose } = options || {};
if (!checkDatabaseConfig(database, config)) {
return;
}
console.info(`List tables on ${database}`);
const dbConfig = config.databases[database];
const provider = new providers[dbConfig.protocol]({ ...dbConfig, verbose });
await provider.listTables();
}