apply-ddl.mjs 4.2 KB
/**
 * Parse dotenv-style text into a plain object.
 *
 * Rules:
 *  - one `KEY=VALUE` per line
 *  - blank lines and full-line comments (first non-space char is `#`) are skipped
 *  - an optional leading `export ` is stripped
 *  - key and value are trimmed
 *  - a single layer of matching surrounding quotes ('...' or "...") is removed
 *  - NO variable expansion: `$FOO`, `${FOO}`, `$(...)`, backticks stay literal
 *
 * @param {string} text
 * @returns {Record<string, string>}
 */
export function parseEnv(text) {
  const env = {}
  if (typeof text !== 'string') return env
  for (const rawLine of text.split('\n')) {
    let line = rawLine.replace(/\r$/, '') // tolerate CRLF
    const trimmed = line.trim()
    if (trimmed === '' || trimmed.startsWith('#')) continue

    // strip an optional `export ` prefix (off the trimmed-left view)
    let body = line.replace(/^\s*export\s+/, '')

    const eq = body.indexOf('=')
    if (eq === -1) continue // not a KEY=VALUE line; ignore

    const key = body.slice(0, eq).trim()
    if (key === '') continue

    let value = body.slice(eq + 1).trim()

    // remove one layer of matching surrounding quotes, if present.
    if (
      value.length >= 2 &&
      ((value[0] === '"' && value[value.length - 1] === '"') ||
        (value[0] === "'" && value[value.length - 1] === "'"))
    ) {
      value = value.slice(1, -1)
    }

    // NOTE: no variable expansion is performed — value is inserted literally.
    env[key] = value
  }
  return env
}

/**
 * Apply a DDL file to a MySQL database using mysql2/promise.
 *
 * @param {{envPath: string, ddlPath: string}} opts
 * @returns {Promise<void>}
 */
export async function applyDDL({ envPath, ddlPath }) {
  const { readFileSync } = await import('node:fs')

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

  const env = parseEnv(readFileSync(envPath, 'utf8'))
  const ddl = readFileSync(ddlPath, 'utf8')
  const { host, port, user, password, database } = resolveDbConfig(env, envPath)

  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 env object. 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,string>} env
 * @param {string} [envPath] only used to make the error message actionable
 * @returns {{host:string, port:number, user:string, password:string, database:string}}
 */
export function resolveDbConfig(env, envPath = '.env.local') {
  const host = env.DB_HOST || env.MYSQL_HOST || '127.0.0.1'
  const port = Number(env.DB_PORT || env.MYSQL_PORT || 3306)
  const user = env.DB_USER || env.MYSQL_USER || 'root'
  const password = env.DB_PASS || env.DB_PASSWORD || env.MYSQL_PASSWORD || ''
  const database = env.DB_SCHEMA || env.DB_NAME || env.MYSQL_DATABASE || undefined
  if (!database) {
    throw new Error(`apply-ddl: 缺数据库名 — 请在 ${envPath} 设置 DB_SCHEMA(或 DB_NAME / MYSQL_DATABASE)`)
  }
  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 (see render.mjs for pathToFileURL rationale)
const { pathToFileURL } = await import('node:url')
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const [envPath, ddlPath] = process.argv.slice(2)
  if (!envPath || !ddlPath) {
    console.error('usage: node lib/apply-ddl.mjs <envPath> <ddlPath>')
    process.exit(2)
  }
  try {
    await applyDDL({ envPath, ddlPath })
    console.log(`apply-ddl: applied ${ddlPath} using ${envPath}`)
  } 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)
  }
}