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-orders no repositório do Pipevine.