apply-ddl.mjs
4.2 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
125
126
127
128
129
130
131
132
133
/**
* 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)
}
}