apply-ddl.mjs
5.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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<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 {
// 从目标项目(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<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 的整数`)
}
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} 仍是占位,请先填真实值(database.password 可填 '' 表示空密码)`)
}
}
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 <configPath> <ddlPath>')
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)
}
}