- Muda provider Prisma de sqlite para postgresql - Remove dependencias SQLite (better-sqlite3, adapter) - Atualiza Better Auth para provider postgresql - Simplifica prisma.ts removendo adapter SQLite - Atualiza stack.yml para usar PostgreSQL existente com 2 replicas - Remove logica de rebuild better-sqlite3 do start-web.sh - Adiciona script de migracao de dados SQLite -> PostgreSQL - Atualiza healthcheck para testar PostgreSQL via Prisma - Habilita start-first deploy para zero-downtime Melhoria: permite multiplas replicas e deploys sem downtime. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
264 lines
7 KiB
JavaScript
264 lines
7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Script de migracao SQLite -> PostgreSQL
|
|
* Executa com acesso ao SQLite e PostgreSQL para migrar todos os dados
|
|
*
|
|
* Uso:
|
|
* SQLITE_PATH=/path/to/db.sqlite POSTGRES_URL=postgresql://... node scripts/migrate-sqlite-to-postgres.mjs
|
|
*/
|
|
|
|
import Database from "better-sqlite3"
|
|
import pg from "pg"
|
|
|
|
const SQLITE_PATH = process.env.SQLITE_PATH || "/app/data/db.sqlite"
|
|
const POSTGRES_URL = process.env.POSTGRES_URL || process.env.DATABASE_URL
|
|
|
|
if (!POSTGRES_URL) {
|
|
console.error("ERRO: POSTGRES_URL ou DATABASE_URL e obrigatorio")
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log("=".repeat(60))
|
|
console.log("MIGRACAO SQLite -> PostgreSQL")
|
|
console.log("=".repeat(60))
|
|
console.log(`SQLite: ${SQLITE_PATH}`)
|
|
console.log(`PostgreSQL: ${POSTGRES_URL.replace(/:[^:@]+@/, ":***@")}`)
|
|
console.log("")
|
|
|
|
// Ordem das tabelas respeitando foreign keys
|
|
const TABLES_ORDER = [
|
|
// Tabelas sem dependencias
|
|
"Team",
|
|
"SlaPolicy",
|
|
"Company",
|
|
// Tabelas com dependencias simples
|
|
"User",
|
|
"Queue",
|
|
"AuthUser",
|
|
// Tabelas com chave composta
|
|
"TeamMember",
|
|
// Tabelas principais
|
|
"Ticket",
|
|
// Tabelas dependentes de Ticket
|
|
"TicketEvent",
|
|
"TicketComment",
|
|
"TicketRating",
|
|
"TicketAccessToken",
|
|
// Tabelas de usuario
|
|
"NotificationPreferences",
|
|
// Tabelas de relatorios
|
|
"ReportExportSchedule",
|
|
"ReportExportRun",
|
|
// Tabelas de autenticacao
|
|
"AuthSession",
|
|
"AuthAccount",
|
|
"AuthInvite",
|
|
"AuthInviteEvent",
|
|
"AuthVerification",
|
|
]
|
|
|
|
let sqlite
|
|
let pgClient
|
|
|
|
async function connect() {
|
|
console.log("[1/4] Conectando aos bancos de dados...")
|
|
|
|
try {
|
|
sqlite = new Database(SQLITE_PATH, { readonly: true })
|
|
console.log(" SQLite: conectado")
|
|
} catch (error) {
|
|
console.error(" SQLite: ERRO -", error.message)
|
|
process.exit(1)
|
|
}
|
|
|
|
try {
|
|
pgClient = new pg.Client({ connectionString: POSTGRES_URL })
|
|
await pgClient.connect()
|
|
console.log(" PostgreSQL: conectado")
|
|
} catch (error) {
|
|
console.error(" PostgreSQL: ERRO -", error.message)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
async function migrateTable(tableName) {
|
|
let rows
|
|
try {
|
|
rows = sqlite.prepare(`SELECT * FROM "${tableName}"`).all()
|
|
} catch (error) {
|
|
console.log(` ${tableName}: tabela nao existe no SQLite, pulando`)
|
|
return { table: tableName, migrated: 0, skipped: true }
|
|
}
|
|
|
|
if (rows.length === 0) {
|
|
console.log(` ${tableName}: 0 registros`)
|
|
return { table: tableName, migrated: 0 }
|
|
}
|
|
|
|
const columns = Object.keys(rows[0])
|
|
const quotedColumns = columns.map(c => `"${c}"`).join(", ")
|
|
const placeholders = columns.map((_, i) => `$${i + 1}`).join(", ")
|
|
const insertSql = `INSERT INTO "${tableName}" (${quotedColumns}) VALUES (${placeholders}) ON CONFLICT DO NOTHING`
|
|
|
|
let migrated = 0
|
|
let errors = 0
|
|
|
|
for (const row of rows) {
|
|
const values = columns.map(col => {
|
|
let val = row[col]
|
|
|
|
// Converter JSON string para objeto se necessario (PostgreSQL usa JSONB)
|
|
if (typeof val === "string") {
|
|
// Detectar campos JSON pelo conteudo
|
|
if ((val.startsWith("{") && val.endsWith("}")) || (val.startsWith("[") && val.endsWith("]"))) {
|
|
try {
|
|
val = JSON.parse(val)
|
|
} catch {
|
|
// Manter como string se nao for JSON valido
|
|
}
|
|
}
|
|
}
|
|
|
|
// SQLite usa 0/1 para boolean, PostgreSQL usa true/false
|
|
// Prisma ja trata isso, mas vamos garantir
|
|
if (val === 0 || val === 1) {
|
|
// Detectar campos boolean pelo nome
|
|
const booleanFields = ["emailVerified", "isLead", "isAvulso", "emailEnabled", "hasBranches", "privacyPolicyAccepted"]
|
|
if (booleanFields.includes(col)) {
|
|
val = val === 1
|
|
}
|
|
}
|
|
|
|
return val
|
|
})
|
|
|
|
try {
|
|
await pgClient.query(insertSql, values)
|
|
migrated++
|
|
} catch (error) {
|
|
errors++
|
|
if (errors <= 3) {
|
|
console.error(` Erro ao inserir em ${tableName}:`, error.message)
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(` ${tableName}: ${migrated}/${rows.length} registros migrados${errors > 0 ? ` (${errors} erros)` : ""}`)
|
|
return { table: tableName, migrated, total: rows.length, errors }
|
|
}
|
|
|
|
async function updateSequences() {
|
|
console.log("\n[3/4] Atualizando sequences do PostgreSQL...")
|
|
|
|
// PostgreSQL precisa atualizar sequences para auto-increment funcionar corretamente
|
|
// Mas como usamos CUIDs, isso geralmente nao e necessario
|
|
// Vamos apenas verificar se ha sequences e atualiza-las se existirem
|
|
|
|
const sequenceQuery = `
|
|
SELECT
|
|
t.relname as table_name,
|
|
a.attname as column_name,
|
|
pg_get_serial_sequence(t.relname::text, a.attname::text) as sequence_name
|
|
FROM pg_class t
|
|
JOIN pg_attribute a ON a.attrelid = t.oid
|
|
WHERE t.relkind = 'r'
|
|
AND pg_get_serial_sequence(t.relname::text, a.attname::text) IS NOT NULL
|
|
`
|
|
|
|
try {
|
|
const result = await pgClient.query(sequenceQuery)
|
|
for (const row of result.rows) {
|
|
const updateSeq = `
|
|
SELECT setval('${row.sequence_name}',
|
|
COALESCE((SELECT MAX("${row.column_name}") FROM "${row.table_name}"), 0) + 1, false)
|
|
`
|
|
try {
|
|
await pgClient.query(updateSeq)
|
|
console.log(` Sequence ${row.sequence_name} atualizada`)
|
|
} catch {
|
|
// Ignorar erros de sequence
|
|
}
|
|
}
|
|
} catch {
|
|
console.log(" Nenhuma sequence encontrada (usando CUIDs)")
|
|
}
|
|
}
|
|
|
|
async function validateMigration() {
|
|
console.log("\n[4/4] Validando migracao...")
|
|
|
|
const validation = []
|
|
|
|
for (const tableName of TABLES_ORDER) {
|
|
let sqliteCount = 0
|
|
let pgCount = 0
|
|
|
|
try {
|
|
const sqliteResult = sqlite.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get()
|
|
sqliteCount = sqliteResult?.count || 0
|
|
} catch {
|
|
// Tabela nao existe no SQLite
|
|
continue
|
|
}
|
|
|
|
try {
|
|
const pgResult = await pgClient.query(`SELECT COUNT(*) as count FROM "${tableName}"`)
|
|
pgCount = parseInt(pgResult.rows[0]?.count || 0)
|
|
} catch {
|
|
pgCount = 0
|
|
}
|
|
|
|
const match = sqliteCount === pgCount
|
|
validation.push({ table: tableName, sqlite: sqliteCount, postgres: pgCount, match })
|
|
|
|
const status = match ? "OK" : "DIFF"
|
|
console.log(` ${tableName}: SQLite=${sqliteCount}, PostgreSQL=${pgCount} [${status}]`)
|
|
}
|
|
|
|
const allMatch = validation.every(v => v.match)
|
|
return allMatch
|
|
}
|
|
|
|
async function main() {
|
|
await connect()
|
|
|
|
console.log("\n[2/4] Migrando tabelas...")
|
|
|
|
try {
|
|
await pgClient.query("BEGIN")
|
|
|
|
for (const tableName of TABLES_ORDER) {
|
|
await migrateTable(tableName)
|
|
}
|
|
|
|
await pgClient.query("COMMIT")
|
|
console.log("\n Transacao commitada com sucesso!")
|
|
|
|
} catch (error) {
|
|
await pgClient.query("ROLLBACK")
|
|
console.error("\nERRO: Rollback executado -", error.message)
|
|
process.exit(1)
|
|
}
|
|
|
|
await updateSequences()
|
|
|
|
const valid = await validateMigration()
|
|
|
|
console.log("\n" + "=".repeat(60))
|
|
if (valid) {
|
|
console.log("MIGRACAO CONCLUIDA COM SUCESSO!")
|
|
} else {
|
|
console.log("MIGRACAO CONCLUIDA COM DIFERENCAS (verifique os logs)")
|
|
}
|
|
console.log("=".repeat(60))
|
|
|
|
sqlite.close()
|
|
await pgClient.end()
|
|
|
|
process.exit(valid ? 0 : 1)
|
|
}
|
|
|
|
main().catch(error => {
|
|
console.error("Erro fatal:", error)
|
|
process.exit(1)
|
|
})
|