Export de listas grandes — CSV em stream
Os drawers de chassi do Emplacamento exportam a lista detalhada (um veículo
por linha) como CSV em stream: uma rota GET lê o DW por cursor e escreve
o arquivo em lotes direto no download do navegador. Nada é montado em memória,
nem no servidor nem na página, e não existe teto de linhas.
Decidido na TEC-1661 (alternativa A, com Hugo e Melo) e aplicado a todos os drawers de chassi na TEC-1674/TEC-1707.
Quando usar
| Lista | Formato | Como |
|---|---|---|
| Detalhada, sem teto (chassis, emplacamentos) | CSV | Rota em stream (este padrão) |
| Pequena e agregada (grid do relatório, totais) | xlsx/PDF/CSV | No navegador, a partir do que a tela já tem |
Regra: export de lista grande é rota em stream. Nunca uma server action devolvendo a lista inteira pro navegador montar o arquivo: com volume grande a aba trava e a resposta passa do limite da Vercel.
Onde está
| Peça | Arquivo |
|---|---|
Resposta em stream (BOM + ;, backpressure, cancelamento, erro aborta) | apps/web/lib/server/csv-stream.ts (createCsvStreamResponse) |
| Colunas, SELECT, lote do cursor, busca chassi/placa | reports/_lib/server/chassi-export.ts |
Query string (filters = searchParams da página em JSON) | reports/_lib/chassi-export-params.ts (buildChassiExportUrl) |
| Download sem passar pela memória da página | components/reports/export-download.ts (downloadFromUrl) |
Rotas hoje:
| Drawer | Rota | Recorte |
|---|---|---|
| Por Território, cidade / área | /api/emplacamento/por-territorio/{cidade,area}/export | por-territorio/_lib/server/{cidade-veiculos,area-municipios}-query.ts |
| Por Marca, marca / modelo | /api/emplacamento/por-marca/chassi/export | por-marca/_lib/server/chassi-query.ts |
| Por Modelo, marca / modelo / versão | /api/emplacamento/por-modelo/chassi/export | por-modelo/_lib/server/chassi-query.ts |
| Por Invasão, município | /api/emplacamento/por-invasao/chassi/export | por-invasao/_lib/server/invasao-chassi-query.ts |
| Sua Concessionária, cidade / área | /api/emplacamento/sua-concessionaria/veiculos/export | sua-concessionaria/_lib/server/cidade-veiculos-query.ts |
Como plugar uma lista nova
- Tire o recorte do arquivo
'use server'. Num arquivo*-query.ts(comimport 'server-only'), deixe as funções que a tela e o export compartilham: leitura dos filtros da URL, escopo do chamador e oWHERE. A server action da tela passa a importar de lá. Arquivo'use server'só aceita server actions, e o export precisa do mesmo recorte da tela. - Escreva o
open*Export. Ele resolve escopo e recorte na hora (lê cookies, só funciona dentro da request) e devolve os lotes comstreamChassiExportRows(() => dw\SELECT ...`). Recorte vazio ou sem acesso devolvenoBatches()`.- A query vai numa função (
() => dw\...`): o fragmento do postgres.js é "thenable", e devolvido por funçãoasync` ele executa inteiro. - Escopo sempre resolvido no servidor a partir do slug da conta (TEC-558). Perfil restrito vê só os próprios CNPJs (TEC-1554).
- A query vai numa função (
- Crie a rota em
app/api/emplacamento/<relatorio>/.../export/route.ts:enhanceRouteHandlercomauth: true,maxDuration = 300, zod na query string (400 sem abrir cursor) ecreateCsvStreamResponsecomCHASSI_EXPORT_COLUMNS. - No drawer, o botão chama
downloadFromUrl(buildChassiExportUrl(rota, params, searchParamsDaPágina))e mostra o toastDRAWER_EXPORT_STARTED_MESSAGE. Filtros que só existem no drawer (busca, modelo, mês do seletor) vão como parâmetros próprios, pro arquivo sair igual à lista na tela. - Testes: o
WHEREpor perfil (conc_loja, conc_grupo, montadora sem acesso) e a rota (repasse dos parâmetros, 400 com parâmetro inválido).
Limites
- Vercel: a função tem até 300 s (
maxDuration), contando o download do cliente. Resposta que não é stream tem teto de 4,5 MB; stream não tem. - Lote de 10 mil linhas (
DRAWER_EXPORT_BATCH_SIZE): o DW fica em sa-east-1 e a função em iad1 (~120 ms por ida), então lote maior soma menos latência. 10 mil linhas ficam em poucos MB por lote. - URL: os filtros vão na query string do
GET, e a Vercel recusa URL acima de 14 KB (URL_TOO_LONG). Seleção que não cabe na URL vai pela referência (?areaRef=do Por Invasão, TEC-821; nome da área no Sua Concessionária, municípios resolvidos no servidor), nunca a lista expandida: a área R4 da Abrare tem 966 municípios (~20 KB). - CNPJ sai pontuado (
formatCnpj): 14 dígitos crus o Excel abre como número e perde o zero à esquerda. - Acima de ~240 s (caso real ainda não visto): o próximo passo seria job assíncrono + Storage, fora deste padrão.
- Erro no meio do cursor aborta o download: o navegador marca como falho, sem
arquivo truncado passando por completo. Log em
Falha no export CSV.