import { Command } from "commander"; import grabDirNames from "../data/grab-dir-names"; import path from "path"; import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef"; import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema"; import chalk from "chalk"; import { writeLiveSchema } from "../functions/live-schema"; import createDBSchema from "../lib/schema/create-db-schema"; import { isDestructiveSchemaStatement, planSchemaQueries, runSchemaQueriesGuarded, } from "../lib/schema/run-schema-query"; import { confirm } from "@inquirer/prompts"; import { AppData } from "../data/app-data"; /** Silence progress logs while computing a plan */ async function quietly(fn) { const { log, warn } = console; console.log = () => { }; console.warn = () => { }; try { return await fn(); } finally { console.log = log; console.warn = warn; } } export default function () { return new Command("schema") .description("Build DB From Schema") .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 { ROOT_DIR } = grabDirNames(); try { const isTypeDef = Boolean(opts.typedef || opts.t); const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema, }); const params = { db_schema: finaldbSchema, config }; // Hide internal bookkeeping (schema manager rows, FK check toggles) const plan = (await quietly(() => planSchemaQueries(() => createDBSchema(params)))).filter((statement) => !statement.includes(AppData.DbSchemaManagerTableName) && !/^SET FOREIGN_KEY_CHECKS/i.test(statement)); const destructive = plan.filter(isDestructiveSchemaStatement); 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 statement of plan) { console.log(isDestructiveSchemaStatement(statement) ? chalk.red(` ${statement}`) : ` ${statement}`); } 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 statement of destructive) { console.log(chalk.red(` ${statement}`)); } 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); } } await runSchemaQueriesGuarded(() => createDBSchema(params), opts.yes ? true : new Set(destructive)); 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(0); } catch (error) { console.error(error); process.exit(1); } }); }