Persistência
Declarando onde e o quê
Declare dataSources e entities uma vez em spec; use os steps db.* no pipeline para ler e
escrever.
spec:
dataSources:
- { id: main, provider: postgres, connectionString: "${env:PIPEVINE_PG_CONNECTION}" }
entities:
- name: widget
table: widgets
dataSource: main
key: id
properties:
- { name: id, type: uuid }
- { name: name, type: string }
provider: postgres é a implementação de referência para dados relacionais. Outro provider —
relacional ou não — pode ser adicionado por um pacote de terceiro sem tocar no core; veja
IDataProvider.
Tipos de coluna aceitos hoje: uuid, string, int/integer, long, decimal (com
precision/scale), bool/boolean, datetime, timestamptz, jsonb/json.
Bootstrap de tabela é uma conveniência do CLI (pipevine db script minha-api.pipevine.yaml | psql ...), não uma ferramenta de migração — não é seguro rodar duas vezes contra um schema que já
mudou por fora.
Os steps db.*
| Step | Função |
|---|---|
db.persist |
Insert ou upsert (onConflict.columns opcional) |
db.update |
Update parcial — faz merge do patch com o registro existente, por chave |
db.delete |
Remoção, por chave |
db.sql |
Escape hatch para SQL arbitrário — parâmetros por expressão, nunca por interpolação de string |
db.find |
Uma entidade (where) ou null. Publica em steps.<id>.result |
db.exists |
Booleano (where). Publica em steps.<id>.result |
db.query |
Lista (where, sort, skip/take). Publica em steps.<id>.result |
where é sempre igualdade por campo. db.find, db.exists e db.query exigem id: — é assim
que publicam o resultado em steps.<id>.result, para o resto do pipeline ler.
- type: db.persist
entity: widget
data: vars.widget
- { type: db.find, id: found, entity: widget, where: { id: "input.route.id" } }
- { type: fail, when: "steps.found.result = null", status: 404, code: WIDGET_NOT_FOUND }
db.sql é o escape hatch para o que os steps tipados acima não cobrem. Os parâmetros são sempre
parâmetros ADO.NET reais, resolvidos por expressão e vinculados pelo provider — nunca interpolados
no texto do SQL. É isso que torna um valor malicioso em um campo de entrada apenas dado comum, em
vez de uma injeção: o texto do SQL em si é fixo desde o build e nunca toca os valores avaliados.
Transações
Para uma rota que faz mais de uma escrita e precisa que tudo aconteça atomicamente, declare
transaction: required na rota:
- id: createOrder
transaction: required
pipeline:
- { type: db.persist, entity: Order, data: vars.order }
- { type: db.update, entity: Inventory, where: { sku: "vars.order.sku" }, patch: "{ 'reserved': true }" }
Uma sessão transacional é uma única conexão — não é seguro fazer db.* concorrente dentro dela.
Isso importa especificamente quando você combina transação com os steps de controle de fluxo
paralelo (parallel, foreach com parallelism > 1); veja
Controle de fluxo — a seção "Um detalhe que morde: transação e
concorrência" — para o detalhe completo. É um erro fácil de cometer e vale a leitura antes de
combinar as duas coisas.
Para onde ir daqui
- Controle de fluxo — decisões condicionais e iteração sobre os dados que você acabou de ler ou gravar.
- Cache — evitar ir ao banco quando o dado já foi lido recentemente.
- Um exemplo real com transação e múltiplas escritas:
samples/03-inventory-ordersno repositório do Pipevine.