import { Command } from "commander"; import createDBSchema from "../lib/schema/create-db-schema"; import grabDirNames from "../data/grab-dir-names"; import path from "path"; import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef"; import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema"; import chalk from "chalk"; import { writeLiveSchema } from "../functions/live-schema"; import grabDBDir from "../utils/grab-db-dir"; import snapshotDb from "../utils/snapshot-db"; import replaceDbFile from "../utils/replace-db-file"; import { confirm } from "@inquirer/prompts"; export default function () { return new Command("schema") .description("Build DB From Schema") .option("-v, --vector", "Recreate Vector Tables. This will drop and rebuild all vector tables") .option("-t, --typedef", "Generate typescript type definitions") .option("--dry-run", "Print the schema changes that would be applied, without applying them") .option("-y, --yes", "Apply destructive changes (dropped tables / columns) without confirmation") .action(async (opts) => { console.log(`Starting process ...`); const config = global.CONFIG; const dbSchema = global.DB_SCHEMA; const DbClient = global.SQLITE_CLIENT; const { ROOT_DIR, BUN_SQLITE_TEMP_DB_FILE_PATH } = grabDirNames(); const { db_file_path } = grabDBDir({ config }); const isVector = Boolean(opts.vector || opts.v); const isTypeDef = Boolean(opts.typedef || opts.t); const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema, }); const params = { db: DbClient, db_schema: finaldbSchema, recreate_vector_table: isVector, }; /** * Plan: run the sync in a transaction that is rolled back */ const plan = []; try { createDBSchema({ ...params, dry_run: true, changes: plan }); } catch (error) { console.error(chalk.red(`Schema sync ERROR => ${error.message}`)); process.exit(1); } const destructive = plan.filter((change) => change.destructive); if (opts.dryRun) { if (!plan.length) { console.log(chalk.green(`Schema is up to date.`)); } else { console.log(chalk.bold(`Planned schema changes:\n`)); for (const change of plan) { console.log(change.destructive ? chalk.red(` ${change.description}`) : ` ${change.description}`); } if (destructive.length) { console.log(chalk.red(`\n${destructive.length} destructive change(s) (in red) would discard data.`)); } } process.exit(0); } if (destructive.length && !opts.yes) { console.log(chalk.yellow(`\nThese changes will permanently discard data:\n`)); for (const change of destructive) { console.log(chalk.red(` ${change.description}`)); } console.log(); if (!process.stdin.isTTY) { console.error(chalk.red(`Refusing to apply destructive changes non-interactively. Re-run with --yes to apply them.`)); process.exit(1); } const proceed = await confirm({ message: `Apply ${destructive.length} destructive change(s)?`, default: false, }); if (!proceed) { console.log(chalk.dim(`Schema sync cancelled.`)); process.exit(1); } } snapshotDb({ db: DbClient, dst: BUN_SQLITE_TEMP_DB_FILE_PATH }); try { createDBSchema({ ...params, allow_destructive: opts.yes ? true : new Set(destructive.map((c) => c.description)), }); DbClient.close(); if (isTypeDef && config.typedef_file_path) { const out_file = path.resolve(ROOT_DIR, config.typedef_file_path); dbSchemaToTypeDef({ dbSchema: finaldbSchema, dst_file: out_file, config, }); } writeLiveSchema({ schema: finaldbSchema }); console.log(`${chalk.bold(chalk.green(`DB Schema setup success!`))}`); process.exit(); } catch (error) { console.log(error); DbClient.close(); replaceDbFile({ src: BUN_SQLITE_TEMP_DB_FILE_PATH, db_file_path, }); process.exit(1); } }); }