apply-ddl.mjs 3.12 KB
import { parseYamlConfig } from './yaml-config.mjs'

/**
 * Apply a DDL file to a MySQL database using mysql2/promise.
 * DB credentials are read from config-vars.yaml's `database:` section.
 *
 * @param {{configPath: string, ddlPath: string}} opts
 * @returns {Promise<void>}
 */
export async function applyDDL({ configPath, ddlPath }) {
  const { readFileSync } = await import('node:fs')

  const config = parseYamlConfig(readFileSync(configPath, 'utf8'))
  const ddl = readFileSync(ddlPath, 'utf8')
  const { host, port, user, password, database } = resolveDbConfig(config, configPath)

  let mysql
  try {
    ;({ default: mysql } = await import('mysql2/promise'))
  } catch {
    throw new MysqlUnavailableError()
  }

  const conn = await mysql.createConnection({
    host,
    port,
    user,
    password,
    database,
    multipleStatements: true,
  })
  try {
    await conn.query(ddl)
  } finally {
    await conn.end()
  }
}

/**
 * Resolve mysql2 connection settings from a parsed config-vars.yaml object,
 * reading its `database:` section directly. Pure (no I/O), so it is
 * unit-testable without mysql2 installed.
 *
 * Throws if no schema resolves — V1 has no USE/CREATE DATABASE.
 *
 * @param {Record<string, any>} config  parsed config-vars.yaml
 * @param {string} [cfgPath] only used to make the error message actionable
 * @returns {{host:string, port:number, user:string, password:string, database:string}}
 */
export function resolveDbConfig(config, cfgPath = 'config-vars.yaml') {
  const db = (config && config.database) || {}
  const host = db.host || '127.0.0.1'
  const port = Number(db.port || 3306)
  const user = db.user || 'root'
  const password = db.password || ''
  const database = db.schema || undefined
  if (!database) {
    throw new Error(`apply-ddl: 缺数据库名 — 请在 ${cfgPath} 的 database.schema 填写`)
  }
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
    throw new Error(`apply-ddl: 端口非法 — ${cfgPath} 的 database.port 必须是 1..65535 的整数`)
  }
  return { host, port, user, password, database }
}

/** Distinct error type so the CLI can emit a friendly install hint. */
export class MysqlUnavailableError extends Error {
  constructor() {
    super('mysql2 is not installed')
    this.name = 'MysqlUnavailableError'
  }
}

// CLI entry guard:pathToFileURL 规范化 argv[1] 以匹配 import.meta.url(路径含空格 / 非 ASCII / Windows 反斜杠时字面比较会失配)
const { pathToFileURL } = await import('node:url')
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const [configPath, ddlPath] = process.argv.slice(2)
  if (!configPath || !ddlPath) {
    console.error('usage: node lib/apply-ddl.mjs <configPath> <ddlPath>')
    process.exit(2)
  }
  try {
    await applyDDL({ configPath, ddlPath })
    console.log(`apply-ddl: applied ${ddlPath} using ${configPath}`)
  } catch (e) {
    if (e instanceof MysqlUnavailableError) {
      console.error('apply-ddl: mysql2 not found. Please run `npm i mysql2` in the target project.')
      process.exit(1)
    }
    console.error(`apply-ddl: failed — ${e?.message || e}`)
    process.exit(1)
  }
}