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 |
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#
| 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" }
]
}
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}¤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",
"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 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 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:
-
driver: "postgrest"explícito — sempre resolve, independente da URL ou connectionRef. -
connectionRefapontando para umConnectionEntry.httpcujobaseUrlcontémsupabase.coou/rest/v1. - URL inline contendo
supabase.coou/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:
| Operador | Exemplo | Descrição |
|---|---|---|
select |
?select=id,nome,total |
Seleciona colunas específicas |
eq | ?status=eq.active | Igual a |
gt, gte, lt, lte |
?price=gt.100 |
Comparações |
like, ilike |
?name=ilike.*silva* |
Busca textual |
order | ?order=data.desc | Ordenação |
limit | ?limit=50 | Limitar 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.