first commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { readdir, stat } from 'node:fs/promises'
|
||||
|
||||
const watchDir = process.env.MIGRATIONS_DIR || '/workspace/server/prisma/migrations'
|
||||
const intervalMs = Number(process.env.MIGRATION_WATCH_INTERVAL_MS || 3000)
|
||||
|
||||
let initialized = false
|
||||
let lastFingerprint = ''
|
||||
let isRunning = false
|
||||
let rerunRequested = false
|
||||
|
||||
async function collectFiles(dir, files = []) {
|
||||
const entries = await readdir(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await collectFiles(fullPath, files)
|
||||
continue
|
||||
}
|
||||
if (!entry.isFile()) continue
|
||||
|
||||
const info = await stat(fullPath)
|
||||
files.push({
|
||||
path: fullPath,
|
||||
size: info.size,
|
||||
mtimeMs: Math.trunc(info.mtimeMs)
|
||||
})
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
async function buildFingerprint() {
|
||||
try {
|
||||
const files = await collectFiles(watchDir)
|
||||
files.sort((a, b) => a.path.localeCompare(b.path))
|
||||
|
||||
const hash = createHash('sha1')
|
||||
for (const file of files) {
|
||||
hash.update(file.path)
|
||||
hash.update(':')
|
||||
hash.update(String(file.size))
|
||||
hash.update(':')
|
||||
hash.update(String(file.mtimeMs))
|
||||
hash.update('\n')
|
||||
}
|
||||
|
||||
return hash.digest('hex')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return `missing:${message}`
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(command, args) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(command, args, { stdio: 'inherit' })
|
||||
child.on('error', () => resolve(1))
|
||||
child.on('close', (code) => resolve(typeof code === 'number' ? code : 1))
|
||||
})
|
||||
}
|
||||
|
||||
async function applyMigrations(reason) {
|
||||
if (isRunning) {
|
||||
rerunRequested = true
|
||||
return
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
console.log(`[MigrationWatcher] Change detected (${reason}). Running migrate deploy...`)
|
||||
|
||||
const deployCode = await runCommand('pnpm', ['--filter', 'server', 'exec', 'prisma', 'migrate', 'deploy'])
|
||||
let generateCode = 0
|
||||
if (deployCode === 0) {
|
||||
generateCode = await runCommand('pnpm', ['--filter', 'server', 'exec', 'prisma', 'generate'])
|
||||
}
|
||||
|
||||
if (deployCode === 0 && generateCode === 0) {
|
||||
console.log('[MigrationWatcher] Migration sync finished.')
|
||||
} else {
|
||||
console.error(
|
||||
`[MigrationWatcher] Migration sync failed (deploy=${deployCode}, generate=${generateCode}).`
|
||||
)
|
||||
}
|
||||
|
||||
isRunning = false
|
||||
if (rerunRequested) {
|
||||
rerunRequested = false
|
||||
await applyMigrations('queued rerun')
|
||||
}
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
const current = await buildFingerprint()
|
||||
if (!initialized) {
|
||||
initialized = true
|
||||
lastFingerprint = current
|
||||
console.log(`[MigrationWatcher] Watching ${watchDir} (interval: ${intervalMs}ms).`)
|
||||
return
|
||||
}
|
||||
|
||||
if (current === lastFingerprint) return
|
||||
|
||||
const previous = lastFingerprint
|
||||
lastFingerprint = current
|
||||
await applyMigrations(`${previous} -> ${current}`)
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
tick().catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[MigrationWatcher] Tick failed: ${message}`)
|
||||
})
|
||||
}, intervalMs)
|
||||
|
||||
tick().catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[MigrationWatcher] Initial tick failed: ${message}`)
|
||||
})
|
||||
Reference in New Issue
Block a user