O servidor
Servir o esquema por HTTP, assinaturas por WebSocket, e o cliente que consulta outro serviço.
O servidor do Lavra é o Kiln. HTTP, rotas, CORS, sessão, cabeçalhos de segurança e WebSocket já existem lá, testados — reimplementá-los aqui criaria duas implementações do mesmo protocolo para divergirem.
Montar num app que já existe#
adopt Kiln
adopt Arcane.Lavra as Lavra
server api on 8080:
route GET "/saude":
respond json {"ok": yes}
Lavra.montar(api, esq, "/lavra")
ignite api| Rota | O que faz |
|---|---|
POST /lavra | executa a consulta |
GET /lavra | devolve o esquema em texto |
WS /lavra/assinar | as assinaturas |
Uma rota, um método. Não há uma rota por busca: a consulta já diz o que quer, e uma rota por campo desfaria a razão de o Lavra existir.
O corpo do pedido#
{
"consulta": "busca:
usuario(id: $id):
nome",
"variaveis": {"id": 7},
"operacao": "Painel"
}operacao só é preciso quando o documento tem mais de uma. Com uma só, ela é a escolhida.
Erro de consulta responde 200#
Parece errado e não é: o HTTP falou, e a resposta tem dados e erros. Um 400 obrigaria o cliente a ter dois caminhos de leitura para o mesmo corpo, e esconderia o caso normal — dados parciais com um erro num campo.
| Status | Quando |
|---|---|
200 | a consulta foi lida — com ou sem erro nos campos |
400 | o corpo nem chegou a ser consulta (ilegível, sem consulta) |
500 | quebrou fora da consulta |
O contexto vem do pedido#
action contexto_de(req):
yield {
"usuario": autenticar(req["headers"]["authorization"] ?? ""),
"banco": conexao,
"ip": req["ip"],
}
Lavra.montar(api, esq, "/lavra", contexto_de := contexto_de)Um servidor só para o esquema#
Lavra.servir(esq, porta := 8080, contexto_de := contexto_de)Sobe e bloqueia. Para teste, Lavra.em_segundo_plano devolve (app, porta) e não bloqueia:
par := Lavra.em_segundo_plano(esq)
app := par[0]
porta := par[1]
defer:
Lavra.parar(app)Assinaturas#
Uma assinatura é o servidor empurrando cada novo valor. O transporte é WebSocket, e o formato de cada mensagem é o mesmo de uma resposta comum.
novos := Lavra.fonte("pedidos")
action pedido_criado(raiz, args, ctx):
yield novos
Lavra.assinatura(esq, "pedidoCriado", "Pedido!", resolve := pedido_criado)
Lavra.montar_assinaturas(api, esq, "/lavra/assinar")Quando um pedido nasce, quem publica é o código que o criou:
action criar_pedido(raiz, args, ctx):
novo := Banco.inserir(ctx["banco"], "pedidos", args["dados"])
_ := novos.publicar(novo)
yield novoassinatura:
pedidoCriado:
numero
total
cliente:
nomeRepare que a assinatura também é uma consulta: cada evento passa pelo mesmo esquema, com os mesmos resolvedores e o mesmo lote. Quem acompanha escolhe os campos, como em qualquer outra operação.
Por que uma Fonte, e não um generator#
Um generator serve um consumidor, e uma assinatura tem muitos. Publicar num generator obrigaria a manter um por conexão, o que multiplica o trabalho pelo número de pessoas com a aba aberta. A Fonte é uma fila com assinantes: publica-se uma vez, e ela entrega a todos.
Consultar outro serviço#
c := Lavra.cliente("http://contas.interno/lavra",
cabecalhos := {"Authorization": $"Bearer {token}"})
r := c.consultar("""
busca Um($id: Integer!):
usuario(id: $id):
nome
""", {"id": 7})
out r["dados"]["usuario"]["nome"]O cliente é fino de propósito: monta o corpo, chama a Malha e lê a resposta. Retentativa com recuo, disjuntor, propagação de rastro e idempotência já estão resolvidos lá.
Testar sem socket#
c := Lavra.local(esq)
dados := c.dados("""
busca:
usuario(id: 1):
nome
""")
assert dados["usuario"]["nome"] is "Ana"Lavra.local tem o mesmo contrato do cliente remoto, sem rede. É o que torna barato o teste de quem consome — e o que permite trocar um pelo outro sem mudar o código que usa.