import fs from 'node:fs'; import path from 'node:path'; import { paths } from '../../env.mjs'; import { countLeads } from '../../treatment/queries.mjs'; const TIPOS = new Set(['excel', 'zpro-oportunidades', 'zpro-bulk']); function parseExportRow(row) { return { ...row, filtro: JSON.parse(row.filtro_json || '{}'), params: JSON.parse(row.params_json || '{}'), arquivos: JSON.parse(row.arquivos_json || '[]'), filtro_json: undefined, params_json: undefined, arquivos_json: undefined }; } export function registerExportRoutes(router, { db, jobs }) { router.add('POST', '/api/exports', async (req, res, { json, body }) => { const tipo = String(body?.tipo || ''); if (!TIPOS.has(tipo)) { json(400, { erro: `tipo invalido. Use: ${[...TIPOS].join(', ')}` }); return; } const filtro = body?.filtro && typeof body.filtro === 'object' ? body.filtro : {}; const params = body?.params && typeof body.params === 'object' ? body.params : {}; const total = countLeads(db, filtro); if (!total) { json(400, { erro: 'nenhum lead corresponde ao filtro selecionado' }); return; } const inserted = db.prepare( "INSERT INTO exports (tipo, filtro_json, params_json, status) VALUES (?, ?, ?, 'pending')" ).run(tipo, JSON.stringify(filtro), JSON.stringify(params)); const exportId = Number(inserted.lastInsertRowid); const jobId = jobs.startJob({ type: 'exportacao', script: 'src/export/export-job.mjs', args: [`--export-id=${exportId}`], meta: { exportId, tipo, total } }); db.prepare('UPDATE exports SET job_id = ? WHERE id = ?').run(jobId, exportId); json(202, { exportId, jobId, totalEstimado: total }); }); router.add('GET', '/api/exports', async (req, res, { json, query }) => { const limit = Math.min(100, Math.max(1, Number(query.get('limit')) || 20)); const rows = db.prepare('SELECT * FROM exports ORDER BY id DESC LIMIT ?').all(limit); json(200, { exports: rows.map(parseExportRow) }); }); router.add('GET', '/api/exports/:id', async (req, res, { json, params }) => { const row = db.prepare('SELECT * FROM exports WHERE id = ?').get(Number(params.id)); if (!row) { json(404, { erro: 'exportacao nao encontrada' }); return; } json(200, parseExportRow(row)); }); router.add('GET', '/api/exports/:id/download', async (req, res, { json, params, query }) => { const row = db.prepare('SELECT * FROM exports WHERE id = ?').get(Number(params.id)); if (!row) { json(404, { erro: 'exportacao nao encontrada' }); return; } const arquivos = JSON.parse(row.arquivos_json || '[]'); const nome = String(query.get('arquivo') || arquivos[0] || ''); if (!arquivos.includes(nome)) { json(400, { erro: 'arquivo nao pertence a esta exportacao' }); return; } const dir = path.resolve(paths.exports, `export-${row.id}`); const resolved = path.resolve(dir, nome); if (!resolved.startsWith(dir + path.sep) || !fs.existsSync(resolved)) { json(404, { erro: 'arquivo nao encontrado' }); return; } res.writeHead(200, { 'content-type': 'application/octet-stream', 'content-disposition': `attachment; filename="${encodeURIComponent(nome)}"`, 'content-length': fs.statSync(resolved).size }); fs.createReadStream(resolved).pipe(res); }); }