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
| Propriedade | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | Identificador. Deve corresponder à chave no payload. |
type | string | Sim | "list" (array de objetos) ou "object" (objeto único) |
schema | object | Sim | Mapa de campo → tipo para validação |
source | string | Não | "inline" (padrão) ou "external" |
sample | object | Não | Dados de exemplo para preview no Studio |
Tipos de schema
| Tipo | Descriçã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" }
]
}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
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. 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}¤cy={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",
"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=vendasModo 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
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 deConnectionEntry.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()
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.