O esquema
Tipos, campos, argumentos, nulidade, listas, enums, entradas, escalares próprios, contratos e uniões.
O esquema é tudo o que a consulta pode pedir, e quem responde por cada parte. Ele é montado uma vez, na subida, e conferido inteiro antes de a primeira consulta chegar.
Tipos, a partir de records#
record Usuario:
id: Integer
nome: String
email: String
esq := Lavra.esquema("loja")
Lavra.tipo(esq, Usuario)Lavra.tipo lê os campos declarados. Também aceita um blueprint e um vault de tipos, para quando o dado não vem de um record:
Lavra.tipo(esq, {"id": "Integer", "nome": "String"}, nome := "Cliente")
Lavra.tipo(esq, Usuario, esconder := ["senha_hash"])A notação de tipo#
| Escrita | Significa |
|---|---|
String | pode ser void |
String! | nunca é void |
[String] | lista que pode ser void, de itens que podem ser void |
[String!]! | lista que nunca é void, de itens que nunca são void |
O ! não é decoração: ele muda o que acontece quando o resolvedor falha. Um campo que admite void vira void e o resto da resposta segue; um ! sobe o erro para o pai, e daí para cima, até achar alguém que admita void.
É a única forma de a promessa do ! valer alguma coisa. Se um String! pudesse voltar vazio, o cliente teria de conferir cada campo mesmo assim — e aí o ! não diria nada.
Campos calculados#
action pedidos_de(usuario, args, ctx):
yield Banco.pedidos_do_usuario(usuario.id)
Lavra.campo(esq, "Usuario", "pedidos", "[Pedido!]!",
args := {"limite": {"tipo": "Integer", "padrao": 10}},
resolve := pedidos_de,
descricao := "Os pedidos deste usuário, do mais recente ao mais antigo.")Um campo sem resolve lê o valor do próprio objeto — do record, do vault, da instância. É o caso da maioria, e por isso é o padrão.
Argumentos#
args := {
"id": "Integer!", // obrigatório
"limite": {"tipo": "Integer", "padrao": 10}, // com padrão
"ordem": {"tipo": "Ordem", "padrao": "Recente"},
}Enums#
enum Estado:
Rascunho
Publicado := "pub"
Lavra.enum(esq, "Estado", Estado)O enum da linguagem entra direto. A consulta escreve o nome (Publicado), e o resolvedor recebe o valor ("pub"). A tradução é do esquema, e é o que permite trocar o valor guardado no banco sem quebrar quem consulta.
Entradas: o que uma mudança recebe#
record NovoUsuario:
nome: String
email: String
Lavra.entrada(esq, NovoUsuario)
Lavra.campo(esq, "NovoUsuario", "nome", "String!")
Lavra.campo(esq, "NovoUsuario", "email", "String!")
Lavra.mudanca(esq, "criarUsuario", "Usuario!",
args := {"dados": "NovoUsuario!"}, resolve := criar)Entrada e saída são tipos diferentes, de propósito. O Usuario que sai tem id e criado_em; o que entra não tem nem um nem outro. Usar o mesmo tipo nos dois lados obrigaria a marcar metade dos campos como opcionais — e aí nenhum deles seria conferido.
Escalares próprios#
adopt Arcane.Time as Time
Lavra.escalar(esq, "Data",
serializa := lambda d => Time.format(d, "%Y-%m-%d"),
desserializa := lambda t => Time.parse(t, "%Y-%m-%d"),
descricao := "Uma data, sem hora, em ISO 8601.")Um escalar próprio é onde entra o que a linguagem não tem como primitivo: Data, Dinheiro, Email, CPF. A validação vive no desserializa e vale para todo campo daquele tipo, em vez de espalhada por cada resolvedor.
Contratos#
Um contrato são campos que vários tipos prometem ter. Quem consulta pede os campos do contrato e recebe de qualquer um dos que o cumprem.
Lavra.tipo(esq, Artigo, cumpre := ["Conteudo"])
Lavra.tipo(esq, Video, cumpre := ["Conteudo"])
action que_tipo(valor, ctx):
yield "Video" given "minutos" in valor.fields otherwise "Artigo"
Lavra.contrato(esq, "Conteudo", {"id": "Integer", "titulo": "String"},
resolve_tipo := que_tipo)busca:
acervo:
titulo
... em Video:
minutosO resolve_tipo responde qual tipo concreto é aquele valor. Sem ele, o Lavra tenta descobrir pelo nome do record — e quando não consegue, diz isso com a lista dos candidatos, em vez de devolver um objeto pela metade.
Uniões#
Uma união é quando o campo devolve um de vários tipos que não têm nada em comum — o resultado de uma busca, por exemplo.
Lavra.uniao(esq, "Achado", ["Usuario", "Produto", "Artigo"],
resolve_tipo := classificar)busca:
buscar(termo: "forja"):
... em Usuario:
nome
... em Produto:
titulo
precoA diferença para o contrato: uma união não tem campos próprios. Não dá para pedir id direto dela, porque não há promessa de que todos os membros tenham id.
Fechar o esquema#
Lavra.conferir(esq)Confere que todo tipo citado existe, que todo contrato é cumprido, que toda união aponta para tipos reais, e que há ao menos uma busca. Roda na montagem: um esquema que não fecha derruba a subida, e não a consulta de alguém.
O esquema em texto#
out Lavra.texto_do_esquema(esq)Serve para versionar. Um esquema em arquivo entra no diff, e uma mudança que quebra o cliente aparece na revisão em vez de na produção. É o mesmo texto que GET /lavra devolve.