Pular para o conteúdo

Na prática

CRUD, banco de dados, DDD, testes, documentação, Docker, CI e como organizar um projeto Lavra.

A organização de um projeto#

text
minha-api/
    forge.toml
    src/
        main.df              sobe o servidor
        esquema.df           monta o esquema e devolve
        tipos/
            usuario.df       record + tipo + campos + resolvedores
            pedido.df
        dominio/
            preco.df         a REGRA, sem saber que existe Lavra
            estoque.df
        infra/
            banco.df         as consultas SQL
    consultas/
        painel.lavra         as consultas guardadas
    tests/
        esquema_test.df
        usuario_test.df

A regra que faz a diferença: `dominio/` não importa `Arcane.Lavra`. O cálculo de preço não muda porque a API mudou de forma, e um teste de domínio não precisa montar esquema nenhum.

dataforge
// dominio/preco.df — sem uma linha de Lavra
action preco_final(produto, cupom):
    base := produto.preco
    given cupom isnt void and cupom.valido:
        base := base * (1.0 - cupom.desconto)
    yield round(base, 2)

relay preco_final
dataforge
// tipos/produto.df — a casca fina que liga os dois
adopt ../dominio/preco as Preco

action preco_de(produto, args, ctx):
    yield Preco.preco_final(produto, buscar_cupom(args["cupom"]))

Lavra.campo(esq, "Produto", "preco_final", "Float!",
    args := {"cupom": "String"}, resolve := preco_de)

CRUD completo#

dataforge
// ── ler ──
Lavra.busca(esq, "produto", "Produto", args := {"id": "Integer!"},
    resolve := lambda r, a, ctx => Banco.achar(ctx["banco"], "produtos", a["id"]))

Lavra.busca(esq, "produtos", "PaginaProduto!",
    args := {"primeiros": {"tipo": "Integer", "padrao": 20},
             "depois": "String",
             "busca": "String"},
    resolve := listar_produtos)

// ── criar ──
Lavra.mudanca(esq, "criarProduto", "Produto!",
    args := {"dados": "NovoProduto!"}, resolve := criar_produto)

// ── alterar ──
Lavra.mudanca(esq, "alterarProduto", "Produto!",
    args := {"id": "Integer!", "dados": "AlteraProduto!"},
    resolve := alterar_produto)

// ── apagar ──
Lavra.mudanca(esq, "apagarProduto", "Boolean!",
    args := {"id": "Integer!"}, resolve := apagar_produto)

Com banco de dados#

dataforge
adopt Arcane.Database as Banco

action listar_produtos(raiz, args, ctx):
    consulta := Banco.select(ctx["banco"], "produtos")
    given args["busca"] isnt void:
        consulta := Banco.search(consulta, args["busca"])
    yield Lavra.pagina(Banco.todos(consulta),
                       primeiros := args["primeiros"],
                       depois := args["depois"])

action criar_produto(raiz, args, ctx):
    yield Banco.transacao(ctx["banco"], lambda => gravar(ctx, args["dados"]))

A transação é do Arcane.Database, e não do Lavra: a venda gravada com o estoque não baixado é o mesmo problema em qualquer camada, e ele já está resolvido embaixo.

Testes#

dataforge
adopt Arcane.Crucible as Crucible
adopt ../src/esquema as E

crucible "o esquema":
    trial "fecha":
        Lavra.conferir(E.montar())

    trial "toda busca tem resolvedor":
        esq := E.montar()
        descricao := Lavra.descrever(esq)
        cycle campo in descricao["busca"]["campos"]:
            assert campo["tipo"] isnt "", $"{campo['nome']} sem tipo"

crucible "usuario":
    trial "traz só o que foi pedido":
        c := Lavra.local(E.montar())
        dados := c.dados("busca:
    usuario(id: 1):
        nome")
        assert dados["usuario"] is {"nome": "Ana"}

    trial "o campo que não existe é recusado":
        problemas := Lavra.validar(E.montar(), "busca:
    usuario(id: 1):
        nomee")
        assert len(problemas) is 1
        assert "nome" in problemas[0]["dica"]

Três coisas valem um teste próprio, e são as que mais quebram:

  • O esquema fecha. Um conferir no teste é o que impede uma subida quebrada.
  • O N+1 não voltou. Conte as idas ao banco, e não o resultado: um lote que devolve o valor certo e consulta cinquenta vezes passa em qualquer teste que só olhe os dados.
  • A consulta guardada ainda vale. Valide cada arquivo de consultas/ contra o esquema atual; é assim que uma mudança que quebra o cliente aparece na CI.
dataforge
crucible "as consultas guardadas ainda valem":
    cycle arquivo in IO.list_dir("consultas"):
        trial arquivo:
            problemas := Lavra.validar(E.montar(), IO.read($"consultas/{arquivo}"))
            assert len(problemas) is 0, $"{arquivo}: {problemas}"

Documentação#

A do esquema sai do esquema — descricao em cada campo, e Lavra.descrever ou Lavra.texto_do_esquema para lê-la. Uma página escrita à mão ao lado envelheceria em silêncio, que é o defeito que este projeto persegue em todo lugar.

dataforge
Lavra.campo(esq, "Produto", "preco_final", "Float!",
    args := {"cupom": "String"},
    resolve := preco_de,
    descricao := "O preço com o cupom aplicado. Sem cupom, é o preço de tabela.")

Lavra.campo(esq, "Produto", "preco_antigo", "Float",
    obsoleto := "use 'preco_final'; este some na 2.0")

Um campo obsoleto continua funcionando e aparece na validação como aviso. É como se tira um campo sem quebrar quem ainda o usa: primeiro ele avisa, depois some.

Convivendo com REST#

Os dois no mesmo app, sem escolher:

dataforge
server api on 8080:
    route GET "/saude":
        respond json {"ok": yes}

    route POST "/webhooks/pagamento":
        respond 200 json processar(body)

Lavra.montar(api, esq, "/lavra")

Webhook, upload e download continuam REST — e devem. Um webhook é chamado por quem não conhece o seu esquema; um download é um arquivo, não um grafo.

Docker e CI#

bash
dataforge devops dockerfile
dataforge devops compose
dataforge devops ci

O que muda para uma API Lavra:

  • `--host=0.0.0.0` no ignite. O padrão é 127.0.0.1, que de dentro do container significa o próprio container — e o sintoma engana: o log diz "no ar" e o curl de fora não recebe nada.
  • O esquema em texto entra no repositório. Lavra.texto_do_esquema num arquivo versionado faz cada mudança aparecer no diff.
  • A CI valida as consultas guardadas contra o esquema do commit. É o que transforma "quebrou o app" em "a revisão não passou".
yaml
# .github/workflows/ci.yml
- name: o esquema nao mudou sem aviso
  run: |
    dataforge run scripts/exportar_esquema.df > /tmp/atual.lavra
    diff -u esquema.lavra /tmp/atual.lavra

Boas práticas#

FaçaPorque
Declare os três limitesa consulta funda é a forma mais barata de derrubar o servidor
Use ! onde a promessa é realum ! que às vezes é void é pior que não ter !
Autorize no campo, não na buscanum grafo, todo caminho é uma porta
Um lote por relacionamentoo N+1 não aparece em teste que só olha o resultado
Pagine por cursorpaginar por posição faz itens sumirem
Entrada separada da saídao mesmo tipo nos dois lados deixa de conferir os dois
Desligue a introspecção em produçãoo esquema é um mapa para quem for procurar
Versione o esquema em textoa quebra aparece na revisão, e não no app