文档对齐 React 技术栈,并支持渐进式数据库迁移
将 README 与部署文档从仅 init 改为 001 基线 + 增量迁移约定,并补充 create/status 脚手架与序号校验。
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 创建下一份渐进式数据库迁移文件。
|
||||
*
|
||||
* 用法:
|
||||
* npm run db:migrate:create -- add_order_note
|
||||
* npm run db:migrate:create -- "add order note"
|
||||
*
|
||||
* 规则:
|
||||
* - 001_init.sql 是基线,只用于空库首次建表
|
||||
* - 后续结构变更一律新增 002_xxx.sql / 003_xxx.sql ...
|
||||
* - 已应用到任何环境的迁移文件禁止修改内容
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const MIGRATIONS_DIR = path.join(CURRENT_DIR, '../src/db/migrations')
|
||||
const MIGRATION_FILE_PATTERN = /^(\d{3})_(.+)\.sql$/i
|
||||
|
||||
function slugify(input: string): string {
|
||||
return input
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.replace(/_+/g, '_')
|
||||
}
|
||||
|
||||
function listMigrationFiles(): string[] {
|
||||
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||
throw new Error(`迁移目录不存在: ${MIGRATIONS_DIR}`)
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(MIGRATIONS_DIR)
|
||||
.filter((name) => MIGRATION_FILE_PATTERN.test(name))
|
||||
.sort((a, b) => a.localeCompare(b, 'en'))
|
||||
}
|
||||
|
||||
function resolveNextVersion(files: string[]): number {
|
||||
let maxVersion = 0
|
||||
for (const file of files) {
|
||||
const match = file.match(MIGRATION_FILE_PATTERN)
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
maxVersion = Math.max(maxVersion, Number(match[1]))
|
||||
}
|
||||
return maxVersion + 1
|
||||
}
|
||||
|
||||
function buildTemplate(version: string, name: string): string {
|
||||
const stamp = new Date().toISOString()
|
||||
return `-- ${version}_${name}.sql
|
||||
-- 创建时间: ${stamp}
|
||||
-- 说明: TODO 填写本次结构变更目的
|
||||
--
|
||||
-- 渐进迁移约定:
|
||||
-- 1. 只写增量 SQL(ALTER / CREATE INDEX / 数据回填等)
|
||||
-- 2. 禁止修改已经提交并应用过的历史迁移文件
|
||||
-- 3. 空库会按文件名顺序执行 001_init.sql → 后续增量迁移
|
||||
-- 4. 线上/开发库启动时自动应用尚未记录在 schema_migrations 中的文件
|
||||
|
||||
-- 在此编写增量 SQL
|
||||
`
|
||||
}
|
||||
|
||||
function main() {
|
||||
const rawName = process.argv.slice(2).join(' ').trim()
|
||||
if (!rawName || rawName.startsWith('-')) {
|
||||
console.error('用法: npm run db:migrate:create -- <migration_name>')
|
||||
console.error('示例: npm run db:migrate:create -- add_task_priority')
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const name = slugify(rawName)
|
||||
if (!name) {
|
||||
console.error('迁移名称无效,请使用英文/数字/下划线描述,例如 add_task_priority')
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const existing = listMigrationFiles()
|
||||
const nextVersion = resolveNextVersion(existing)
|
||||
if (nextVersion > 999) {
|
||||
console.error('迁移序号已超过 999,请调整命名策略')
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const version = String(nextVersion).padStart(3, '0')
|
||||
const fileName = `${version}_${name}.sql`
|
||||
const filePath = path.join(MIGRATIONS_DIR, fileName)
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.error(`迁移文件已存在: ${fileName}`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const conflict = existing.find((item) => item.startsWith(`${version}_`))
|
||||
if (conflict) {
|
||||
console.error(`迁移序号冲突: ${version} 已被 ${conflict} 占用`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, buildTemplate(version, name), 'utf8')
|
||||
console.info(`[db:migrate:create] 已创建 ${path.relative(process.cwd(), filePath)}`)
|
||||
console.info('[db:migrate:create] 编辑 SQL 后提交;后端启动或 npm run db:migrate 会自动应用')
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user