Skip to content

Data Sources

Um data source declara os dados que o relatório espera receber. A definição fica no relatório; os dados reais vêm no payload.

Declaração

json
{
  "dataSources": [
    {
      "id": "items",
      "type": "list",
      "schema": {
        "product": "string",
        "quantity": "integer",
        "price": "number"
      }
    }
  ]
}

Propriedades

PropriedadeTipoObrigatórioDescrição
idstringSimIdentificador. Deve corresponder à chave no payload.
typestringSim"list" (array de objetos) ou "object" (objeto único)
schemaobjectSimMapa de campo → tipo para validação
sourcestringNão"inline" (padrão) ou "external"
sampleobjectNãoDados de exemplo para preview no Studio

Tipos de schema

TipoDescrição
"string"Texto
"integer"Número inteiro
"number"Número decimal
"boolean"Verdadeiro ou falso

Payload

O payload é um mapa JSON onde cada chave corresponde ao id de um data source:

json
{
  "items": [
    { "product": "Notebook", "quantity": 1, "price": 2500.00 },
    { "product": "Mouse", "quantity": 2, "price": 89.90 }
  ]
}

No código Dart:

dart
final report = await engine.parseReport(reportJson);
final context = await engine.processData(report, {
  'items': [
    {'product': 'Notebook', 'quantity': 1, 'price': 2500.00},
    {'product': 'Mouse', 'quantity': 2, 'price': 89.90},
  ],
});

Vinculação com bands

Uma DetailBand se vincula a um data source pelo campo dataSourceId:

json
{
  "type": "detail",
  "id": "det",
  "dataSourceId": "items",
  "height": 30,
  "elements": [
    { "type": "field", "id": "f1", "x": 0, "y": 5, "width": 200, "binding": "product" }
  ]
}

O engine gera uma instância da band para cada registro em items.

Múltiplos data sources

json
{
  "dataSources": [
    { "id": "header_info", "type": "object", "schema": { "company": "string" } },
    { "id": "items", "type": "list", "schema": { "name": "string", "price": "number" } }
  ]
}

Payload:

json
{
  "header_info": { "company": "ACME Corp" },
  "items": [
    { "name": "Item A", "price": 100.0 }
  ]
}

Modo strict vs resilient

  • strict (padrão): data source ausente no payload gera erro.
  • resilient: data source ausente resulta em lista vazia; erros ficam em ProcessedData.errors.
json
{
  "processingMode": "resilient"
}

Transformando dados via scripts

Filtros, ordenações, campos calculados e manipulações de payload são feitos via scripts com o hook afterQuery. O script recebe o ScriptContext com acesso total aos data sources carregados e pode modificá-los antes da renderização.

dart
engine.register('preparar_produtos', (ctx) {
  // filtra, ordena e adiciona campo calculado
  final rows = ctx.datasource('produtos')
    .where((r) => (r['estoque'] as int) > 0)
    .map((r) {
      final fat = (r['preco'] as num) * (r['vendidos'] as num);
      return {...r, 'faturamento': fat};
    })
    .toList()
    ..sort((a, b) => (b['faturamento'] as num).compareTo(a['faturamento'] as num));

  ctx.setDatasource('produtos', rows.take(10).toList());
});

Declare o script no JSON referenciando o mesmo id:

json
{
  "scripts": [
    { "id": "preparar_produtos", "hook": "afterQuery" }
  ]
}

Lógica simples sem estado

Para expressões puramente calculadas por row, use valueExpression no FieldElement (ex: "valueExpression": "preco * vendidos") ou printWhen em elementos e bands para visibilidade condicional. Reserve scripts para lógica que envolva filtragem, reordenação, joins entre data sources ou acesso a contexto externo.

Ver Definição do relatório → Scripts para a API completa do ScriptContext.

Resolvers externos (package sulfite_datasources)

Para data sources com source: "external", o engine precisa de um resolver que saiba buscar os dados. O pacote sulfite_datasources oferece implementações prontas para REST e PostgreSQL.

Instalação

yaml
dependencies:
  sulfite_datasources: ^<versão>

Interface DataSourceResolver

dart
abstract class DataSourceResolver {
  bool canResolve(DataSource ds);
  Future<List<Map<String, dynamic>>> resolve(DataSource ds, {ReportParams params});
  Future<DataSourceSchema> introspect(DataSource ds, {ReportParams params});
}

O engine chama canResolve() em cada resolver fornecido para escolher o adequado para cada data source. O método introspect() é usado pelo Studio para autocompletar bindings no DataSource Inspector.

RestDataSourceResolver

Resolve data sources com source: "external" e URL HTTP/HTTPS.

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

final resolver = RestDataSourceResolver(
  timeout: Duration(seconds: 30),
);

Substituição de parâmetros na URL: Placeholders {paramName} são substituídos pelos valores do mapa params com URI encoding automático:

json
{ "url": "https://api.example.com/data?period={period}&currency={currency}" }
dart
await engine.generate(report,
  resolvers: [resolver],
  params: {'period': '30', 'currency': 'USD'},
  format: 'pdf',
);

Suporta GET (padrão) e POST. A resposta pode ser List ou Map na raiz do JSON. Em caso de falha HTTP, lança DataSourceHttpException (carrega a Uri da requisição).

PostgresDataSourceResolver

Resolve data sources PostgreSQL. Suporta dois modos: URL inline (credenciais embutidas no JSON) e conexão nomeada via connectionRef (RFC-019).

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

final resolver = PostgresDataSourceResolver(
  defaultConnectTimeout: Duration(seconds: 10),
  defaultQueryTimeout: Duration(seconds: 30),
);

Modo URL (legado)

Credenciais embutidas diretamente no datasource. Útil para ambientes de desenvolvimento ou quando a definição do relatório não é compartilhada.

json
{
  "id": "vendas",
  "type": "list",
  "source": "external",
  "url": "postgres://user:pass@host:5432/db",
  "schema": {
    "product": "string",
    "total": "number",
    "query": "SELECT product, SUM(total) AS total FROM orders WHERE date >= {startDate} AND date <= {endDate} GROUP BY product"
  }
}

Para usar um schema específico via URL, adicione ?schema=nome à URL:

postgres://user:pass@host:5432/db?schema=vendas

Modo connectionRef (RFC-019) — recomendado

Desacopla as credenciais do relatório. O datasource referencia uma conexão pelo nome (connectionRef); as credenciais ficam no ConnectionRegistry gerenciado pela aplicação.

json
{
  "id": "vendas",
  "type": "list",
  "source": "external",
  "connectionRef": "producao",
  "query": "SELECT product, SUM(total) AS total FROM orders WHERE date >= {startDate} GROUP BY product",
  "schema": {
    "product": "string",
    "total": "number"
  }
}

Passe o registry ao criar o resolver:

dart
import 'package:sulfite_report_manager/sulfite_report_manager.dart';
import 'package:sulfite_datasources/sulfite_datasources.dart';

final registry = MutableConnectionRegistry([
  ConnectionEntry.database(
    name: 'producao',
    host: 'db.example.com',
    port: 5432,
    database: 'app_db',
    username: 'app_user',
    password: 'secret',
    ssl: true,
    extra: {'schema': 'public'},
  ),
]);

final resolver = PostgresDataSourceResolver(registry: registry);

Parâmetros SQL

Placeholders usam a sintaxe {paramName} — a mesma dos datasources REST e dos elementos de texto. O resolver compila para named parameters do driver PostgreSQL (sem interpolação direta de string).

json
{
  "query": "SELECT * FROM {_schema}.pedidos WHERE cliente = {clienteId} AND data >= {inicio}"
}

Placeholder

Quando configurado, o resolver substitui {_schema} pelo schema definido na conexão antes de parametrizar a query. Isso é útil para um mesmo relatório rodar em diferentes schemas de um mesmo banco sem editar o JSON.

json
{
  "query": "SELECT * FROM {_schema}.pedidos WHERE status = {status}"
}
  • No modo connectionRef: o schema vem de ConnectionEntry.extra['schema']
  • No modo URL: o schema vem do query param ?schema= da URL

{_schema} é substituído como identificador SQL — nunca como parâmetro — pois nomes de schema não podem ser bound em prepared statements.

Migração: URL → connectionRef

Para migrar um relatório existente do modo URL para o modo connectionRef:

Antes (URL)Depois (connectionRef)
"url": "postgres://user:pass@host:5432/db"Remover o campo url
"schema": { "query": "SELECT ..." }"query": "SELECT ..." (campo de primeiro nível)
"connectionRef": "nome_da_conexao"
:paramName nos parâmetros{paramName} (sintaxe unificada)

Em caso de falha de conexão ou query, lança DataSourcePostgresException.

Usando com generate()

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

final engine = SulfiteEngineImpl();
final report = await engine.parseReport(reportJson);

final pdfBytes = await engine.generate(
  report,
  resolvers: [
    RestDataSourceResolver(),
    PostgresDataSourceResolver(),
  ],
  params: {'startDate': '2025-01-01', 'endDate': '2025-03-31'},
  format: 'pdf',
);

O engine itera os data sources com source: "external" e delega ao primeiro resolver cujo canResolve() retornar true. Data sources inline continuam sendo resolvidos pelo payload passado em dataPayload.

Tutorial completo

Veja o tutorial Relatório com API externa → para um exemplo passo a passo com o RestDataSourceResolver.

Próximo passo

Processamento de dados →

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