UNPKG

iagate-querykit

Version:

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

369 lines (368 loc) 12.7 kB
import { QueryKitConfig } from './config'; import { scheduler } from './scheduler'; import { table } from './table'; /** * Retorna queries específicas para listar views baseado no dialeto SQL. * Cada dialeto tem sua própria sintaxe para consultar views do sistema. * * @param dialect - Dialeto SQL ('sqlite', 'mysql', 'postgres', 'mssql', 'oracle') * @returns Array de objetos com SQL e função de mapeamento para cada dialeto * * @example * ```typescript * // Dados iniciais * const dialect = 'postgres'; * * // Como usar * const queries = namesByDialect(dialect); * * // Output: [{ sql: "SELECT table_name FROM information_schema.views...", map: (r) => r.table_name }] * ``` */ function namesByDialect(dialect) { switch (dialect) { case 'sqlite': return [{ sql: "SELECT name FROM sqlite_master WHERE type='view'", map: (r) => r.name }]; case 'mysql': return [{ sql: "SHOW FULL TABLES WHERE TABLE_TYPE = 'VIEW'", map: (r) => Object.values(r)[0] }]; case 'postgres': return [{ sql: "SELECT table_name FROM information_schema.views WHERE table_schema = current_schema()", map: (r) => r.table_name }]; case 'mssql': return [{ sql: "SELECT name FROM sys.views", map: (r) => r.name }]; case 'oracle': return [{ sql: "SELECT VIEW_NAME AS name FROM USER_VIEWS", map: (r) => r.name }]; default: return [ { sql: "SELECT name FROM sqlite_master WHERE type='view'", map: (r) => r.name }, { sql: "SHOW FULL TABLES WHERE TABLE_TYPE = 'VIEW'", map: (r) => Object.values(r)[0] }, { sql: "SELECT table_name FROM information_schema.views WHERE table_schema = current_schema()", map: (r) => r.table_name }, { sql: "SELECT name FROM sys.views", map: (r) => r.name }, { sql: "SELECT VIEW_NAME AS name FROM USER_VIEWS", map: (r) => r.name }, ]; } } /** * Escapa valores para uso seguro em SQL literals. * Converte diferentes tipos de dados para strings SQL válidas. * * @param value - Valor a ser escapado * @returns String SQL escapada e segura * * @example * ```typescript * // Dados iniciais * const values = [null, 42, 'John\'s data', true, new Date()]; * * // Como usar * const escaped = values.map(escapeSqlLiteral); * * // Output: ['NULL', '42', "'John''s data'", '1', "'2024-01-01T00:00:00.000Z'"] * ``` */ function escapeSqlLiteral(value) { if (value === null || value === undefined) return 'NULL'; if (typeof value === 'number' && isFinite(value)) return String(value); if (typeof value === 'bigint') return String(value); if (typeof value === 'boolean') return value ? '1' : '0'; if (value instanceof Date) return `'${value.toISOString().replace(/'/g, "''")}'`; if (Buffer && Buffer.isBuffer && Buffer.isBuffer(value)) return `X'${value.toString('hex')}'`; if (typeof value === 'object') return `'${JSON.stringify(value).replace(/'/g, "''")}'`; return `'${String(value).replace(/'/g, "''")}'`; } /** * Substitui placeholders (?) em SQL por valores literais escapados. * Útil para debug e logging de queries com bindings. * * @param sql - Query SQL com placeholders * @param bindings - Array de valores para substituir os placeholders * @returns SQL com valores inline substituindo os placeholders * * @example * ```typescript * // Dados iniciais * const sql = 'SELECT * FROM users WHERE age > ? AND name LIKE ?'; * const bindings = [18, '%John%']; * * // Como usar * const inlined = inlineBindings(sql, bindings); * * // Output: "SELECT * FROM users WHERE age > 18 AND name LIKE '%John%'" * ``` */ function inlineBindings(sql, bindings) { if (!bindings || bindings.length === 0) return sql; let i = 0; return sql.replace(/\?/g, () => { if (i >= bindings.length) return '?'; const lit = escapeSqlLiteral(bindings[i++]); return lit; }); } /** * Gerenciador de views para o QueryKit. * Permite criar, substituir, remover e listar views de banco de dados. * Suporta múltiplos dialetos SQL e agendamento de refresh automático. * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * const userQuery = table('users').select('id', 'name', 'email').where('active', true); * * // Como usar * await viewManager.createOrReplaceView('active_users', userQuery); * * // Output: View 'active_users' criada com sucesso * ``` */ export class ViewManager { /** * Cria ou substitui uma view baseada em um QueryBuilder. * Remove a view existente se houver e cria uma nova. * * @param viewName - Nome da view a ser criada * @param query - QueryBuilder que define o conteúdo da view * @returns Promise que resolve quando a view for criada * @throws Error se não houver executor configurado * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * const productQuery = table('products') * .select('id', 'name', 'price') * .where('category', 'electronics'); * * // Como usar * await viewManager.createOrReplaceView('electronics_products', productQuery); * * // Output: View 'electronics_products' criada com produtos da categoria electronics * ``` */ async createOrReplaceView(viewName, query) { const { sql, bindings } = query.toSql(); await this.dropView(viewName); const inlined = inlineBindings(sql, bindings); const createViewSql = `CREATE VIEW ${viewName} AS ${inlined}`; const exec = QueryKitConfig.defaultExecutor; if (!exec) throw new Error('No executor configured for QueryKit'); if (exec.runSync) exec.runSync(createViewSql, []); else await exec.executeQuery(createViewSql, []); } /** * Agenda refresh automático de uma view em intervalos regulares. * A view será recriada automaticamente usando a query fornecida. * * @param viewName - Nome da view para agendar refresh * @param query - QueryBuilder para recriar a view * @param intervalMs - Intervalo em milissegundos entre refreshes * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * const statsQuery = table('logs').select('date', 'count(*) as total').groupBy('date'); * * // Como usar * viewManager.scheduleViewRefresh('daily_stats', statsQuery, 3600000); // A cada hora * * // Output: Refresh da view 'daily_stats' agendado para cada hora * ``` */ scheduleViewRefresh(viewName, query, intervalMs) { const task = () => this.createOrReplaceView(viewName, query); scheduler.schedule(`refresh-view-${viewName}`, task, intervalMs); } /** * Cancela o refresh automático de uma view. * * @param viewName - Nome da view para cancelar refresh * * @example * ```typescript * // Dados iniciais * viewManager.scheduleViewRefresh('temp_view', query, 5000); * * // Como usar * viewManager.unscheduleViewRefresh('temp_view'); * * // Output: Refresh automático da view 'temp_view' cancelado * ``` */ unscheduleViewRefresh(viewName) { scheduler.unschedule(`refresh-view-${viewName}`); } /** * Remove uma view do banco de dados. * Usa DROP VIEW IF EXISTS para evitar erros se a view não existir. * * @param viewName - Nome da view a ser removida * @returns Promise que resolve quando a view for removida * @throws Error se não houver executor configurado * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * * // Como usar * await viewManager.dropView('old_view'); * * // Output: View 'old_view' removida com sucesso * ``` */ async dropView(viewName) { const dropViewSql = `DROP VIEW IF EXISTS ${viewName}`; const exec = QueryKitConfig.defaultExecutor; if (!exec) throw new Error('No executor configured for QueryKit'); if (exec.runSync) exec.runSync(dropViewSql, []); else await exec.executeQuery(dropViewSql, []); } /** * Lista todas as views existentes no banco de dados (síncrono). * Tenta diferentes queries baseado no dialeto do executor. * * @returns Array com nomes das views existentes * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * * // Como usar * const views = viewManager.listViews(); * * // Output: ['active_users', 'daily_stats', 'product_summary'] * ``` */ listViews() { const exec = QueryKitConfig.defaultExecutor; if (!exec) throw new Error('No executor configured for QueryKit'); if (!exec.executeQuerySync) return []; const candidates = namesByDialect(exec.dialect || QueryKitConfig.defaultDialect); for (const c of candidates) { try { const res = exec.executeQuerySync(c.sql, []); const rows = res?.data || []; const names = rows.map(c.map).filter(Boolean); if (names.length || rows.length >= 0) return names; } catch { } } return []; } /** * Verifica se uma view específica existe (síncrono). * * @param viewName - Nome da view para verificar * @returns true se a view existir, false caso contrário * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * * // Como usar * const exists = viewManager.viewExists('active_users'); * * // Output: true se a view 'active_users' existir * ``` */ viewExists(viewName) { const names = this.listViews(); return names.includes(viewName); } /** * Lista todas as views existentes no banco de dados (assíncrono). * Versão assíncrona do método listViews(). * * @returns Promise que resolve com array de nomes das views * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * * // Como usar * const views = await viewManager.listViewsAsync(); * * // Output: Promise resolve com ['active_users', 'daily_stats'] * ``` */ async listViewsAsync() { const exec = QueryKitConfig.defaultExecutor; if (!exec) throw new Error('No executor configured for QueryKit'); if (exec.executeQuerySync) return this.listViews(); const candidates = namesByDialect(exec.dialect || QueryKitConfig.defaultDialect); for (const c of candidates) { try { const res = await exec.executeQuery(c.sql, []); const rows = res?.data || []; const names = rows.map(c.map).filter(Boolean); if (names.length || rows.length >= 0) return names; } catch { } } return []; } /** * Verifica se uma view específica existe (assíncrono). * Versão assíncrona do método viewExists(). * * @param viewName - Nome da view para verificar * @returns Promise que resolve com true se a view existir * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * * // Como usar * const exists = await viewManager.viewExistsAsync('active_users'); * * // Output: Promise resolve com true se a view existir * ``` */ async viewExistsAsync(viewName) { const names = await this.listViewsAsync(); return names.includes(viewName); } /** * Cria um QueryBuilder para uma view existente. * Permite fazer queries em views como se fossem tabelas normais. * * @param viewName - Nome da view * @returns QueryBuilder configurado para a view * * @example * ```typescript * // Dados iniciais * const viewManager = new ViewManager(); * * // Como usar * const viewQuery = viewManager.view('active_users').select('*').limit(10); * const results = await viewQuery.execute(); * * // Output: QueryBuilder configurado para a view 'active_users' * ``` */ view(viewName) { return table(viewName); } }