apply-ddl.mjs
4.28 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
// lib/apply-ddl.mjs
//
// Replaces the inline `set -a; . .env.local; mysql < V1.sql` bash from db-init.
//
// parseEnv(): dotenv-style line parser. Pure parsing, NO variable expansion and
// NO shell execution — `$VAR`, backticks, `$(...)` and other shell constructs are
// kept verbatim as literal characters, which eliminates the shell-injection vector
// of `source`-ing an untrusted .env file.
//
// applyDDL(): connects with mysql2/promise (multipleStatements) to run a DDL file.
/**
* 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.
*
* Reads connection settings from the parsed env file. Recognised keys (with
* common aliases) — DB_HOST/MYSQL_HOST, DB_PORT/MYSQL_PORT, DB_USER/MYSQL_USER,
* DB_PASS/DB_PASSWORD/MYSQL_PASSWORD, DB_NAME/MYSQL_DATABASE.
*
* @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 = 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_NAME || env.MYSQL_DATABASE || undefined
const conn = await mysql.createConnection({
host,
port,
user,
password,
database,
multipleStatements: true,
})
try {
await conn.query(ddl)
} finally {
await conn.end()
}
}
/** 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: node lib/apply-ddl.mjs <envPath> <ddlPath>
// Use pathToFileURL so the guard matches even when the path contains spaces or
// other characters that get percent-encoded in import.meta.url.
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)
}
}