import { parseYamlConfig } from './yaml-config.mjs' import { createRequire } from 'node:module' import { pathToFileURL } from 'node:url' import { dirname, resolve as resolvePath, join } from 'node:path' /** * Resolve the `mysql2/promise` driver from the TARGET PROJECT directory. * * ESM resolves a bare specifier relative to the importing module — and this helper * lives inside the PLUGIN, not the project being scaffolded. A plain * `import('mysql2/promise')` here would therefore look in the plugin's own * node_modules and never see a target-project `npm i mysql2`. We instead build a * `require` rooted at the target project so the documented "install mysql2 in your * project" actually takes effect. Throws (MODULE_NOT_FOUND) when it is absent. * * @param {string} [baseDir] target project root (defaults to cwd) * @returns {string} absolute path to the resolved mysql2/promise entry */ export function resolveMysql2Path(baseDir = process.cwd()) { const require = createRequire(pathToFileURL(join(baseDir, 'package.json')).href) return require.resolve('mysql2/promise') } /** * 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} */ 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 { // 从目标项目(config-vars.yaml 所在目录)解析 mysql2,而非插件自身目录(见 resolveMysql2Path)。 const resolved = resolveMysql2Path(dirname(resolvePath(configPath))) ;({ default: mysql } = await import(pathToFileURL(resolved).href)) } 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} 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 的整数`) } // DDL-7:lib 自保护——拒绝把未填的「【人工填写】」凭据占位直连 MySQL。 // db-init 步骤 B 已用 LLM 文本检查把关,这里在真正建连的那一层再加一道防御(占位文本绝不应连库)。 for (const [k, v] of [['host', host], ['user', user], ['password', password], ['schema', database]]) { if (typeof v === 'string' && v.includes('【人工填写')) { throw new Error(`apply-ddl: ${cfgPath} 的 database.${k} 仍是「【人工填写】」占位 — 请先填真实凭据`) } } 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 反斜杠时字面比较会失配) 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 ') process.exit(2) } // DDL-6:缺文件属用法/路径错 → 退出码 2(与 db-init C.2 文档「2 = 用法错(路径找不到)」及 validate-ddl 对齐)。 const { existsSync } = await import('node:fs') if (!existsSync(configPath)) { console.error(`apply-ddl: 配置文件不存在: ${configPath}`); process.exit(2) } if (!existsSync(ddlPath)) { console.error(`apply-ddl: DDL 文件不存在: ${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) } }