Pular para o conteúdo principal

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​

ListaFormatoComo
Detalhada, sem teto (chassis, emplacamentos)CSVRota em stream (este padrão)
Pequena e agregada (grid do relatório, totais)xlsx/PDF/CSVNo 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çaArquivo
Resposta em stream (BOM + ;, backpressure, cancelamento, erro aborta)apps/web/lib/server/csv-stream.ts (createCsvStreamResponse)
Colunas, SELECT, lote do cursor, busca chassi/placareports/_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áginacomponents/reports/export-download.ts (downloadFromUrl)

Rotas hoje:

DrawerRotaRecorte
Por Território, cidade / área/api/emplacamento/por-territorio/{cidade,area}/exportpor-territorio/_lib/server/{cidade-veiculos,area-municipios}-query.ts
Por Marca, marca / modelo/api/emplacamento/por-marca/chassi/exportpor-marca/_lib/server/chassi-query.ts
Por Modelo, marca / modelo / versão/api/emplacamento/por-modelo/chassi/exportpor-modelo/_lib/server/chassi-query.ts
Por Invasão, município/api/emplacamento/por-invasao/chassi/exportpor-invasao/_lib/server/invasao-chassi-query.ts
Sua Concessionária, cidade / área/api/emplacamento/sua-concessionaria/veiculos/exportsua-concessionaria/_lib/server/cidade-veiculos-query.ts

Como plugar uma lista nova​

  1. Tire o recorte do arquivo 'use server'. Num arquivo *-query.ts (com import 'server-only'), deixe as funções que a tela e o export compartilham: leitura dos filtros da URL, escopo do chamador e o WHERE. 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.
  2. Escreva o open*Export. Ele resolve escopo e recorte na hora (lê cookies, só funciona dentro da request) e devolve os lotes com streamChassiExportRows(() => dw\SELECT ...`). Recorte vazio ou sem acesso devolve noBatches()`.
    • A query vai numa função (() => dw\...`): o fragmento do postgres.js é "thenable", e devolvido por função async` 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).
  3. Crie a rota em app/api/emplacamento/<relatorio>/.../export/route.ts: enhanceRouteHandler com auth: true, maxDuration = 300, zod na query string (400 sem abrir cursor) e createCsvStreamResponse com CHASSI_EXPORT_COLUMNS.
  4. No drawer, o botão chama downloadFromUrl(buildChassiExportUrl(rota, params, searchParamsDaPágina)) e mostra o toast DRAWER_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.
  5. Testes: o WHERE por 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.