#!/usr/bin/env node // scripts/drop-test-db.mjs —— 删除一次性测试副本库(跑完收尾;源库绝不被删)。 // // 与 scripts/setup-test-db.mjs 同一命名约定: // SOURCE = config-vars.yaml database.schema(真实持久库,本脚本硬拒删除)。 // COPY = ERP_TEST_DB_SCHEMA(env) > .tmp/erp-lane-db(marker) > `__test`(串行缺省)。 // 由 test.mjs / seed / 行为门 runner 的 finally 调用(做完测试直接删副本)。幂等:副本不存在也返回 0。 // 纯 mysql CLI(spawnSync),零 npm 依赖。 import { spawnSync } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const CONFIG_FILE = join(SCRIPT_DIR, '..', 'config-vars.yaml') // 极简 YAML 读取(2 层 map + 标量;与 scripts/setup-test-db.mjs 同规则,内联以免运行时依赖)。 function parseScalar(raw) { let s = String(raw).trim() if (s === '' || s[0] === '#') return '' const q = s[0] if (q === '"' || q === "'") { const end = s.indexOf(q, 1) if (end !== -1) return s.slice(1, end) } const hash = s.indexOf(' #') if (hash !== -1) s = s.slice(0, hash).trim() return s } function parseYamlConfig(text) { const root = {} let section = null for (const rawLine of text.split('\n')) { const line = rawLine.replace(/\r$/, '') const trimmed = line.trim() if (trimmed === '' || trimmed[0] === '#') continue const colon = line.indexOf(':') if (colon === -1) continue const key = line.slice(0, colon).trim() if (key === '') continue const indent = line.length - line.replace(/^\s+/, '').length const value = parseScalar(line.slice(colon + 1)) if (indent === 0) { if (value === '') { section = {} root[key] = section } else { root[key] = value section = null } } else if (section) { section[key] = value } else { root[key] = value } } return root } if (!existsSync(CONFIG_FILE)) { console.error(`[drop-test-db] config-vars.yaml 不存在(${CONFIG_FILE})`) process.exit(1) } const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} function readLaneDbMarker() { const markerFile = join(SCRIPT_DIR, '..', '.tmp', 'erp-lane-db') if (!existsSync(markerFile)) return '' return readFileSync(markerFile, 'utf8').split('\n')[0].trim() } const DB_HOST = db.host ?? '' const DB_PORT = db.port ?? '3306' const DB_USER = db.user ?? '' const DB_PASSWORD = db.password ?? '' const SOURCE_SCHEMA = db.schema ?? '' function rejectPlaceholder(key, value) { if (typeof value === 'string' && value.includes('【人工填写')) { console.error(`[drop-test-db] database.${key} 仍是占位,请先在 config-vars.yaml 填真实值`) process.exit(1) } } for (const [key, value] of [['host', DB_HOST], ['port', DB_PORT], ['user', DB_USER], ['password', DB_PASSWORD], ['schema', SOURCE_SCHEMA]]) { rejectPlaceholder(key, value) } if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) { console.error(`[drop-test-db] database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`) process.exit(1) } if (String(SOURCE_SCHEMA).trim() === '') { console.error('[drop-test-db] database.schema(源库名)未填') process.exit(1) } const LANE_MARKER_SCHEMA = readLaneDbMarker() const COPY_SCHEMA = process.env.ERP_TEST_DB_SCHEMA || LANE_MARKER_SCHEMA || `${SOURCE_SCHEMA}__test` // **安全红线**:绝不删源库。副本名与源库同名 → 拒绝(说明副本名注入出错)。 if (COPY_SCHEMA === SOURCE_SCHEMA) { console.error(`[drop-test-db] 拒绝:待删副本名与源库名相同(${SOURCE_SCHEMA})——绝不删源库。`) process.exit(1) } function quoteMySqlIdent(value) { return '`' + String(value).replaceAll('`', '``') + '`' } console.log(`[drop-test-db] 删除一次性副本 ${COPY_SCHEMA} on ${DB_HOST}:${DB_PORT}(源库 ${SOURCE_SCHEMA} 不动)`) const res = spawnSync( 'mysql', [`--host=${DB_HOST}`, `--port=${DB_PORT}`, `--user=${DB_USER}`, `--password=${DB_PASSWORD}`, '-e', `DROP DATABASE IF EXISTS ${quoteMySqlIdent(COPY_SCHEMA)};`], { stdio: 'inherit' }, ) if (res.error) { console.error(`[drop-test-db] FATAL: 无法执行 mysql(请确认其在 PATH 中): ${res.error.message}`) process.exit(1) } if (res.status !== 0) { console.error(`[drop-test-db] FAIL: mysql exit=${res.status}`) process.exit(res.status === null ? 1 : res.status) } console.log('[drop-test-db] done')