UNPKG

iagate-querykit

Version:

QueryKit: lightweight TypeScript query toolkit with models, views, triggers, events, scheduler and adapters (better-sqlite3).

269 lines (268 loc) 8.74 kB
import { QueryKitConfig } from './config'; import { QueryBuilder } from './query-builder'; /** * Obtém executor do banco de dados, priorizando o explícito. * * @param explicit - Executor explícito opcional * @returns Executor configurado * @throws Error se nenhum executor estiver disponível */ function getExec(explicit) { const exec = explicit || QueryKitConfig.defaultExecutor; if (!exec) throw new Error('No executor configured for QueryKit'); return exec; } /** * Retorna SQL para criar tabela de migrações baseado no dialeto. * Suporta múltiplos bancos de dados com sintaxes específicas. * * @param dialect - Dialeto SQL do banco * @returns SQL para criar tabela de migrações * * @example * ```typescript * // Dados iniciais * const dialect = 'postgres'; * * // Como usar * const sql = migrationsTableSql(dialect); * * // Output: "CREATE TABLE IF NOT EXISTS querykit_migrations..." * ``` */ function migrationsTableSql(dialect) { switch (dialect) { case 'postgres': return `CREATE TABLE IF NOT EXISTS querykit_migrations (id VARCHAR(255) PRIMARY KEY, applied_at TIMESTAMP DEFAULT NOW())`; case 'mysql': return `CREATE TABLE IF NOT EXISTS querykit_migrations (id VARCHAR(255) PRIMARY KEY, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`; case 'mssql': return `IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='querykit_migrations' and xtype='U') CREATE TABLE querykit_migrations (id NVARCHAR(255) PRIMARY KEY, applied_at DATETIME DEFAULT GETDATE())`; case 'oracle': return `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE querykit_migrations (id VARCHAR2(255) PRIMARY KEY, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`; default: return `CREATE TABLE IF NOT EXISTS querykit_migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP)`; } } /** * Garante que a tabela de migrações existe no banco. * Cria a tabela se não existir. * * @param exec - Executor do banco de dados * @returns Promise que resolve quando a tabela for criada */ async function ensureTable(exec) { const sql = migrationsTableSql(exec.dialect); if (exec.runSync) { exec.runSync(sql, []); } else { await exec.executeQuery(sql, []); } } /** * Lista todas as migrações já aplicadas no banco. * * @param executor - Executor opcional (usa padrão se não fornecido) * @returns Promise que resolve com array de IDs de migrações aplicadas * * @example * ```typescript * // Dados iniciais * const executor = databaseExecutor; * * // Como usar * const appliedMigrations = await listAppliedMigrations(executor); * * // Output: ['001_create_users', '002_add_email_column'] * ``` */ export async function listAppliedMigrations(executor) { const exec = getExec(executor); await ensureTable(exec); const sql = `SELECT id FROM querykit_migrations ORDER BY applied_at ASC`; if (exec.executeQuerySync) { const res = exec.executeQuerySync(sql, []); return (res?.data || []).map(r => r.id); } const res = await exec.executeQuery(sql, []); return (res?.data || []).map(r => r.id); } /** * Executa um passo de migração individual. * Suporta strings SQL, arrays e funções. * * @param step - Passo da migração para executar * @param ctx - Contexto da migração * @returns Promise que resolve quando o passo for executado */ async function execStep(step, ctx) { if (typeof step === 'string') { await ctx.query(step); return; } if (Array.isArray(step)) { for (const s of step) await execStep(s, ctx); return; } await Promise.resolve(step(ctx)); } /** * Aplica migrações para cima (up) até um alvo específico. * Executa migrações não aplicadas em ordem sequencial. * * @param migrations - Array de especificações de migração * @param opts - Opções incluindo alvo e executor * @returns Promise que resolve com lista de migrações aplicadas * * @example * ```typescript * // Dados iniciais * const migrations = [ * { id: '001_create_users', up: 'CREATE TABLE users (id INTEGER PRIMARY KEY)' }, * { id: '002_add_email', up: 'ALTER TABLE users ADD COLUMN email TEXT' } * ]; * * // Como usar * const result = await migrateUp(migrations, { to: '002_add_email' }); * * // Output: { applied: ['001_create_users', '002_add_email'] } * ``` */ export async function migrateUp(migrations, opts = {}) { const exec = getExec(opts.executor); await ensureTable(exec); const applied = new Set(await listAppliedMigrations(exec)); const target = opts.to; const ctx = { exec, dialect: exec.dialect, query: async (sql, bindings = []) => { await exec.executeQuery(sql, bindings); }, runSync: (sql, bindings = []) => { if (exec.runSync) { exec.runSync(sql, bindings); } else { throw new Error('runSync not supported by executor'); } }, qb: (name) => new QueryBuilder(name) }; const newlyApplied = []; for (const mig of migrations) { if (applied.has(mig.id)) { if (target && mig.id === target) break; continue; } await execStep(mig.up, ctx); const ins = `INSERT INTO querykit_migrations (id) VALUES (?)`; if (exec.runSync) { exec.runSync(ins, [mig.id]); } else { await exec.executeQuery(ins, [mig.id]); } newlyApplied.push(mig.id); if (target && mig.id === target) break; } return { applied: newlyApplied }; } /** * Reverte migrações para baixo (down) até um alvo específico. * Executa passos de reversão em ordem reversa. * * @param migrations - Array de especificações de migração * @param opts - Opções incluindo alvo, número de passos e executor * @returns Promise que resolve com lista de migrações revertidas * * @example * ```typescript * // Dados iniciais * const migrations = [ * { id: '001_create_users', up: 'CREATE TABLE users', down: 'DROP TABLE users' }, * { id: '002_add_email', up: 'ALTER TABLE users ADD email', down: 'ALTER TABLE users DROP email' } * ]; * * // Como usar * const result = await migrateDown(migrations, { steps: 1 }); * * // Output: { reverted: ['002_add_email'] } * ``` */ export async function migrateDown(migrations, opts = {}) { const exec = getExec(opts.executor); await ensureTable(exec); const applied = await listAppliedMigrations(exec); const byId = {}; migrations.forEach(m => { byId[m.id] = m; }); const ctx = { exec, dialect: exec.dialect, query: async (sql, bindings = []) => { await exec.executeQuery(sql, bindings); }, runSync: (sql, bindings = []) => { if (exec.runSync) { exec.runSync(sql, bindings); } else { throw new Error('runSync not supported by executor'); } }, qb: (name) => new QueryBuilder(name) }; const target = opts.to; let remaining = typeof opts.steps === 'number' ? Math.max(0, opts.steps) : Infinity; const reverted = []; for (let i = applied.length - 1; i >= 0 && remaining > 0; i--) { const id = applied[i]; const mig = byId[id]; if (!mig) continue; if (mig.down) { await execStep(mig.down, ctx); } const del = `DELETE FROM querykit_migrations WHERE id = ?`; if (exec.runSync) { exec.runSync(del, [id]); } else { await exec.executeQuery(del, [id]); } reverted.push(id); remaining--; if (target && id === target) break; } return { reverted }; } /** * Remove completamente a tabela de migrações. * Útil para resetar o estado de migrações. * * @param opts - Opções incluindo executor * @returns Promise que resolve quando a tabela for removida * * @example * ```typescript * // Dados iniciais * const executor = databaseExecutor; * * // Como usar * await resetMigrations({ executor }); * * // Output: Tabela de migrações removida, estado resetado * ``` */ export async function resetMigrations(opts = {}) { const exec = getExec(opts.executor); const dropSql = `DROP TABLE IF EXISTS querykit_migrations`; if (exec.runSync) { exec.runSync(dropSql, []); } else { await exec.executeQuery(dropSql, []); } }