Pular para o conteúdo

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#

dataforge
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
RotaO que faz
POST /lavraexecuta a consulta
GET /lavradevolve o esquema em texto
WS /lavra/assinaras 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#

json
{
  "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.

StatusQuando
200a consulta foi lida — com ou sem erro nos campos
400o corpo nem chegou a ser consulta (ilegível, sem consulta)
500quebrou fora da consulta

O contexto vem do pedido#

dataforge
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#

dataforge
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:

dataforge
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.

dataforge
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:

dataforge
action criar_pedido(raiz, args, ctx):
    novo := Banco.inserir(ctx["banco"], "pedidos", args["dados"])
    _ := novos.publicar(novo)
    yield novo
consulta lavra
assinatura:
    pedidoCriado:
        numero
        total
        cliente:
            nome

Repare 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#

dataforge
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#

dataforge
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.