Pular para o conteúdo

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#

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

dataforge
Lavra.tipo(esq, {"id": "Integer", "nome": "String"}, nome := "Cliente")
Lavra.tipo(esq, Usuario, esconder := ["senha_hash"])

A notação de tipo#

EscritaSignifica
Stringpode 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#

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

dataforge
args := {
    "id": "Integer!",                              // obrigatório
    "limite": {"tipo": "Integer", "padrao": 10},   // com padrão
    "ordem": {"tipo": "Ordem", "padrao": "Recente"},
}

Enums#

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

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

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

dataforge
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)
consulta lavra
busca:
    acervo:
        titulo
        ... em Video:
            minutos

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

dataforge
Lavra.uniao(esq, "Achado", ["Usuario", "Produto", "Artigo"],
    resolve_tipo := classificar)
consulta lavra
busca:
    buscar(termo: "forja"):
        ... em Usuario:
            nome
        ... em Produto:
            titulo
            preco

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

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

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