Skip to content

Tutorial: Relatório com API externa

Neste tutorial você criará um relatório de câmbio USD/BRL que busca dados em tempo real de uma API pública, enriquece e ordena os registros via script e exibe um gráfico de linha com tabela de cotações. Usa o exemplo 10-forex do repositório.

O que você vai aprender:

  • Declarar um data source externo com source: "external" e url
  • Usar RestDataSourceResolver do pacote sulfite_datasources
  • Chamar SulfiteEngine.generate() com resolvers injetados
  • Enriquecer e filtrar dados com um script afterQuery
  • Exibir gráfico de linha + tabela em uma SummaryBand

Pré-requisitos

  1. Sulfite instalado (Instalação →)
  2. Instale o pacote de resolvers externos:
yaml
dependencies:
  sulfite_core: ^<versão>
  sulfite_datasources: ^<versão>

Passo 1 — Data source externo

Declare o data source com source: "external" e forneça a URL da API. O schema pode ser vazio {} quando os campos são inferidos em runtime:

json
{
  "dataSources": [
    {
      "id": "cotacoes",
      "type": "list",
      "source": "external",
      "url": "https://economia.awesomeapi.com.br/json/daily/USD-BRL/15",
      "schema": {}
    }
  ]
}

A URL pode conter placeholders {paramName} que serão substituídos por parâmetros em runtime. Consulte Filtros de impressão para detalhes.

Passo 2 — Script afterQuery para enriquecer os dados

Declare o script no JSON e registre o handler em Dart para enriquecer, ordenar e limitar os registros:

json
{
  "scripts": [
    { "id": "preparar_cotacoes", "hook": "afterQuery" }
  ]
}
dart
engine.register('preparar_cotacoes', (ctx) {
  final rows = ctx.datasource('cotacoes')
    .map((r) => {
      ...r,
      // date_label usa create_date quando disponível, fallback para timestamp
      'date_label': r['create_date'] ?? r['timestamp'],
    })
    .toList()
    ..sort((a, b) {
      final ta = int.tryParse(a['timestamp'].toString()) ?? 0;
      final tb = int.tryParse(b['timestamp'].toString()) ?? 0;
      return tb.compareTo(ta); // decrescente
    });

  ctx.setDatasource('cotacoes', rows.take(10).toList());
});
  • Cria date_label com fallback entre create_date e timestamp.
  • Ordena por timestamp decrescente.
  • Restringe para os 10 registros mais recentes.

O resultado é um array ordenado e enriquecido, pronto para ser vinculado ao gráfico e à tabela.

Passo 3 — Header com variável de sistema

Use {SYSDATE} em elementos text para exibir a data de geração:

json
{
  "type": "text",
  "id": "header_date",
  "x": 612, "y": 40, "width": 190, "height": 18,
  "content": "Gerado em: {SYSDATE}",
  "fontSize": 8,
  "color": "bbdefb",
  "align": "right"
}

Passo 4 — Detail band vazia

O data source cotacoes é do tipo externo, mas ainda precisa de uma DetailBand vinculada para que o engine reconheça qual data source alimenta a SummaryBand. A detail band pode ter height 0 e sem elementos:

json
{
  "type": "detail",
  "id": "detail_cotacoes",
  "dataSourceId": "cotacoes",
  "height": 0,
  "elements": []
}

Passo 5 — Gráfico de linha na SummaryBand

json
{
  "type": "chart",
  "id": "line_cotacao",
  "x": 0, "y": 8, "width": 802, "height": 216,
  "chartType": "line",
  "dataSourceId": "cotacoes",
  "categoryField": "date_label",
  "series": [
    { "name": "Compra (bid)",  "valueField": "bid",  "color": "1565c0" },
    { "name": "Máxima (high)", "valueField": "high", "color": "b71c1c" },
    { "name": "Mínima (low)",  "valueField": "low",  "color": "2e7d32" }
  ],
  "options": {
    "showLegend": true,
    "legendPosition": "top",
    "showGridLines": true,
    "labelFontSize": 8
  }
}

Passo 6 — Tabela com arredondamento

Abaixo do gráfico, uma tabela exibe os valores numéricos com arredondamento bancário (half_even, precisão 3):

json
{
  "type": "table",
  "id": "tbl_cotacoes",
  "x": 0, "y": 234, "width": 802,
  "dataSourceId": "cotacoes",
  "showHeader": true,
  "alternateRowColors": true,
  "headerFill": "1565c0",
  "columns": [
    { "id": "col_date", "header": "Data",       "binding": "date_label", "width": 148, "format": "date:dd/MM/yyyy" },
    { "id": "col_bid",  "header": "Compra",     "binding": "bid",        "width": 110, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
    { "id": "col_ask",  "header": "Venda",      "binding": "ask",        "width": 110, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
    { "id": "col_high", "header": "Máxima",     "binding": "high",       "width": 100, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
    { "id": "col_low",  "header": "Mínima",     "binding": "low",        "width": 100, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
    { "id": "col_pct",  "header": "Variação %", "binding": "pctChange",  "width": 110, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } }
  ]
}
json
{
  "type": "footer",
  "id": "footer_main",
  "height": 25,
  "elements": [
    {
      "type": "text",
      "id": "footer_page",
      "x": 680, "y": 8, "width": 120,
      "content": "Página {PAGE} de {PAGES}",
      "fontSize": 7,
      "align": "right"
    }
  ]
}

Passo 8 — Dart: injetando o RestDataSourceResolver

dart
import 'dart:io';
import 'package:sulfite_core/sulfite_core.dart';
import 'package:sulfite_datasources/sulfite_datasources.dart';

void main() async {
  final engine = SulfiteEngineImpl();
  final report = await engine.parseReport(reportJson);

  final pdfBytes = await engine.generate(
    report,
    resolvers: [RestDataSourceResolver()],
    format: 'pdf',
  );

  await File('forex_report.pdf').writeAsBytes(pdfBytes);
}

O engine.generate() itera os data sources com source: "external", chama canResolve() em cada resolver da lista e delega ao primeiro que aceitar. O RestDataSourceResolver faz o GET para a URL e decodifica o JSON automaticamente.

Outros formatos

dart
// HTML
final htmlBytes = await engine.generate(report,
  resolvers: [RestDataSourceResolver()],
  format: 'html',
);

// Excel
final xlsxBytes = await engine.generate(report,
  resolvers: [RestDataSourceResolver()],
  format: 'excel',
);

Parâmetros em runtime

Se a URL contiver placeholders {paramName}, passe os valores via params:

dart
final pdfBytes = await engine.generate(
  report,
  resolvers: [RestDataSourceResolver()],
  params: {'period': '30', 'currency': 'EUR'},
  format: 'pdf',
);

Uma URL como https://api.example.com/rates/{currency}/last/{period} será resolvida para https://api.example.com/rates/EUR/last/30.

Fluxo completo

report.json (source: external, url: ...)


engine.generate(resolvers: [RestDataSourceResolver()])

        ├─ RestDataSourceResolver.resolve(ds) → GET url → List<Map>

        ├─ DataProcessor.process() + script afterQuery → enriquece e ordena registros

        └─ PdfRenderer.render() → PDF bytes

Próximos passos

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro