Port bun-mariadb conventions: init globals, db handler, split schema sync
- init() loads config, schema and the SQLite client once into globals - all CRUD queries go through lib/db-handler.ts (optional per-call config) - split db-schema-manager.ts into lib/schema/* with FK-aware table ordering - add bun:test tests for schema ordering, FK constraints and schema sync - config/schema files can be .ts/.js/.json/.yaml/.yml; db_schema_file_name is now optional (defaults to schema.* in db_dir) - add export/import commands (database snapshot + schema archives) - package exports map, type re-exports, hardened publish.sh - validate table name in delete Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { Command } from "commander";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import chalk from "chalk";
|
||||
import grabDBDir from "../utils/grab-db-dir";
|
||||
import grabDirNames from "../data/grab-dir-names";
|
||||
import snapshotDb from "../utils/snapshot-db";
|
||||
import { writeExportArchive } from "../utils/export-archive";
|
||||
import trimExports from "../utils/trim-exports";
|
||||
import { AppData } from "../data/app-data";
|
||||
|
||||
function defaultExportFileName(
|
||||
dbName: string,
|
||||
format: "tar.gz" | "zip",
|
||||
): string {
|
||||
return `${dbName}-${Date.now()}.${format}`;
|
||||
}
|
||||
|
||||
export default function () {
|
||||
return new Command("export")
|
||||
.description("Export database file + schema into a portable archive")
|
||||
.option(
|
||||
"-o, --output <path>",
|
||||
"Output archive path (.tar.gz or .zip). Defaults to db exports dir",
|
||||
)
|
||||
.option(
|
||||
"-f, --format <format>",
|
||||
"Archive format when --output is omitted: tar.gz | zip",
|
||||
"tar.gz",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
console.log(`Exporting database ...`);
|
||||
|
||||
const config = global.CONFIG;
|
||||
const { export_dir } = grabDBDir({ config });
|
||||
const { BUN_SQLITE_TEMP_DIR } = grabDirNames();
|
||||
|
||||
if (!fs.existsSync(export_dir)) {
|
||||
fs.mkdirSync(export_dir, { recursive: true });
|
||||
}
|
||||
|
||||
const formatRaw = String(opts.format || "tar.gz").toLowerCase();
|
||||
const format: "tar.gz" | "zip" =
|
||||
formatRaw === "zip" ? "zip" : "tar.gz";
|
||||
|
||||
let outPath: string;
|
||||
if (opts.output) {
|
||||
outPath = path.resolve(opts.output);
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
} else {
|
||||
outPath = path.join(
|
||||
export_dir,
|
||||
defaultExportFileName(config.db_name, format),
|
||||
);
|
||||
}
|
||||
|
||||
const snapshot_path = path.join(
|
||||
BUN_SQLITE_TEMP_DIR,
|
||||
`export-${Date.now()}.db`,
|
||||
);
|
||||
|
||||
/** Remove the temp database file before exiting */
|
||||
const exit = (code: number): never => {
|
||||
fs.rmSync(snapshot_path, { force: true });
|
||||
process.exit(code);
|
||||
};
|
||||
|
||||
try {
|
||||
snapshotDb({ db: global.SQLITE_CLIENT, dst: snapshot_path });
|
||||
|
||||
/**
|
||||
* The schema is always stored as `schema.<ext>` so `import`
|
||||
* can find it, whatever `db_schema_file_name` is.
|
||||
*/
|
||||
const schemaFileName = `${AppData.DbSchemaFileName}${path.extname(global.SCHEMA_FILE_PATH)}`;
|
||||
|
||||
await writeExportArchive({
|
||||
contents: {
|
||||
db: new Uint8Array(fs.readFileSync(snapshot_path)),
|
||||
schema: fs.readFileSync(
|
||||
global.SCHEMA_FILE_PATH,
|
||||
"utf-8",
|
||||
),
|
||||
schemaFileName,
|
||||
},
|
||||
outPath,
|
||||
});
|
||||
|
||||
if (path.dirname(outPath) === export_dir) {
|
||||
trimExports({ config });
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${chalk.bold(chalk.green(`DB Export Success!`))} → ${outPath}`,
|
||||
);
|
||||
console.log(
|
||||
chalk.dim(`Contains: database.sqlite + ${schemaFileName}`),
|
||||
);
|
||||
exit(0);
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Export ERROR => ${error.message}`));
|
||||
exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user