Logosulfite.app
rafagazani/sulfite 999999

Data Sources

Como declarar fontes de dados e alimentar o relatório com payload JSON

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#

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

Propriedades#

PropriedadeTipoObrigatórioDescrição
id string Sim Identificador. Deve corresponder à chave no payload.
type string Sim "list" (array de objetos) ou "object" (objeto único)
schema object Não Mapa de campo → tipo para validação
source string Não "inline" (padrão) ou "external"
driver string Não Driver explícito: "rest" , "postgres" ou "postgrest" . Quando omitido, o engine detecta automaticamente pelo URL ou connectionRef .
url string Não URL para resolução externa (credenciais inline). Ex: "https://api.example.com/data" ou "postgres://user:pass@host:5432/db"
connectionRef string Não Nome lógico de uma ConnectionEntry no ConnectionRegistry . Desacopla credenciais do relatório.
query string Não Query SQL ou path HTTP relativo usado com connectionRef . Ex: "SELECT * FROM vendas" ou "vendas?select=*&order=data.desc"
method string Não Método HTTP para resolução externa. Padrão: "GET"
headers object Não Headers HTTP extras para resolução inline (ex: {"apikey": "xxx"})
filter string Não Expressão de filtro avaliada por row. Rows onde a expressão resulta em 0 / false / null são excluídas.
sample object Não Dados 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:

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

No código 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:

{
  "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#

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

Payload:

{
  "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.
{
  "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.

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:

{
  "scripts": [
    { "id": "preparar_produtos", "hook": "afterQuery" }
  ]
}
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, PostgreSQL e PostgREST/Supabase.

Instalação#

dependencies:
  sulfite_datasources: ^<versão>

Interface DataSourceResolver#

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. Quando o campo driver está presente (ex: "postgrest", "postgres", "rest"), o resolver correspondente responde imediatamente. Quando driver é omitido, o resolver tenta detectar automaticamente pelo URL ou connectionRef. O método introspect() é usado pelo Studio para autocompletar bindings no DataSource Inspector.

RestDataSourceResolver#

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

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:

{ "url": "https://api.example.com/data?period={period}&currency={currency}" }
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).

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.

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

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.

{
  "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:

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).

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

Placeholder {_schema}

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.

{
  "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 no DataSource)
"connectionRef": "nome_da_conexao"
"driver": "postgres" (opcional, detectado automaticamente)
:paramName nos parâmetros{paramName} (sintaxe unificada)

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

PostgRestDataSourceResolver#

Resolve data sources Supabase/PostgREST. Usa a biblioteca postgrest do Dart para montar queries com a sintaxe PostgREST (filtros, select, order).

import 'package:sulfite_datasources/sulfite_datasources.dart';

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

O resolver aceita data sources de três formas:

  1. driver: "postgrest" explícito — sempre resolve, independente da URL ou connectionRef.
  2. connectionRef apontando para um ConnectionEntry.http cujo baseUrl contém supabase.co ou /rest/v1.
  3. URL inline contendo supabase.co ou /rest/v1.

Modo connectionRef (recomendado)

{
  "id": "vendas",
  "type": "list",
  "source": "external",
  "driver": "postgrest",
  "connectionRef": "supabase_prod",
  "query": "vendas?select=id,total,data&status=eq.active&order=data.desc"
}

A conexão HTTP correspondente no registry:

final registry = MutableConnectionRegistry([
  ConnectionEntry.http(
    name: 'supabase_prod',
    baseUrl: 'https://xyz.supabase.co/rest/v1',
    defaultHeaders: {
      'apikey': 'your-anon-key',
      'Authorization': 'Bearer your-anon-key',
    },
  ),
]);

final resolver = PostgRestDataSourceResolver(registry: registry);

Os defaultHeaders configurados na ConnectionEntry.http são injetados automaticamente em todas as requisições — ideal para API keys e tokens de autenticação.

Modo URL inline

{
  "id": "vendas",
  "type": "list",
  "source": "external",
  "url": "https://xyz.supabase.co/rest/v1",
  "headers": {
    "apikey": "your-anon-key",
    "Authorization": "Bearer your-anon-key"
  },
  "query": "vendas?select=id,total,data&order=data.desc"
}

Sintaxe de query PostgREST

O campo query usa a sintaxe de query string do PostgREST:

OperadorExemploDescrição
select ?select=id,nome,total Seleciona colunas específicas
eq?status=eq.activeIgual a
gt, gte, lt, lte ?price=gt.100 Comparações
like, ilike ?name=ilike.*silva* Busca textual
order?order=data.descOrdenação
limit?limit=50Limitar resultados

Usando com proxy (Flutter Web)

Para usar Supabase em Flutter Web sem expor a anon key no client, configure um proxy no seu backend:

// No backend (ex: sulfite_api_example)
// O proxy recebe a request e injeta os headers reais do Supabase

// No report JSON — aponte para o proxy:
{
  "id": "vendas",
  "type": "list",
  "source": "external",
  "driver": "postgrest",
  "connectionRef": "supabase_proxy",
  "query": "vendas?select=*"
}
// No app Flutter — conexão aponta para o proxy:
ConnectionEntry.http(
  name: 'supabase_proxy',
  baseUrl: 'https://my-api.onrender.com/supabase',
  defaultHeaders: {
    'apikey': 'demo',
    'Authorization': 'Bearer demo',
  },
)

Usando com generate()#

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: [
    PostgRestDataSourceResolver(registry: registry), // mais específico primeiro
    RestDataSourceResolver(),
    PostgresDataSourceResolver(registry: registry),
  ],
  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. O PostgRestDataSourceResolver deve vir antes do RestDataSourceResolver pois é mais específico (ambos lidam com HTTP, mas o PostgREST monta queries otimizadas). Data sources inline continuam sendo resolvidos pelo payload passado em dataPayload.

Veja o tutorial [Relatório com API externa →](/tutorials/rest-datasource) para um exemplo passo a passo com o `RestDataSourceResolver`, ou [Supabase/PostgREST como fonte →](/tutorials/postgrest-datasource) para o `PostgRestDataSourceResolver`.

Próximo passo#

Processamento de dados →