Graphql API com clojure
Esse post é uma edição comemorativa da criação da comunidade graphql-brasil
, que já conta com
O fonte final está disponivel em github.com/souenzzo/clj-graphql-tutorial
Criando projeto
- Confira se há uma versão recente do clojure instalado na sua maquina
- Em um diretório vazio, crie um arquivo
deps.edn
Neste arquivo, configuramos das dependencias do projeto.
No path
colocamos src
, onde ficará o códio e resources
, onde ficarão nossos schemas
No deps
temos:
-
org.clojure/clojure
Clojure é apenas uma biblioteca. Podemos escolher sua versão aqui -
com.walmartlabs/lacinia
Implementação do graphql para clojure. Ela que nos da a função(execute schema "{hello}" vars ctx) ;;=> {:data {:hello "world"}}
-
com.walmartlabs/lacinia-pedestal
Ajuda a adaptar o lacinia a funcionar via HTTP, usando o servidor HTTPpedestal
. Também traz o graphiql -
io.pedestal/pedestal.service
Servidor HTTP pedestal. Usaremos algumas funcionalidades dele diretamente, principalmente para teste -
io.pedestal/pedestal.jetty
O pedestal permite vários backends. Usaremos o jetty, mas poderia ser immutant, tomcat ou mesmo aws lambda. -
org.clojure/java.jdbc
Adaptador para usar JDBC sem ter que instanciar 200 classes -
org.postgresql/postgresql
Backend usado no jdbc.
Crimos também em aliases
um alias chamado dev
, que adiciona a pasta de testes test
e a dependencia de testes.
Neste momento, já podemos abrir o REPL
para nunca mais fechar
Depois de baixar algumas dependencias, deve aparecer um REPL para vc.
Agora vamos ao SQL. Caso você não tenha um postgres rodando na sua porta 5432, execute:
Irá iniciar um postgres, que será destruido (incluindo dados) quando vc apetar ctrl-c
no terminal.
Interagindo com o SQL
Definiremos o schema do SQL oo arquivo resources/schema.sql
.
De volta no seu REPL (você deixou aberto, né?), usaremos ele para instalar esse schema
Para isso, vamos usar a jdbc/execute!, que recebe 3 argumetnos:
- um mapa com as configurações
- Uma string com os comandos SQL
- Um mapa de configurações extras.
Por padrão, o jdbc/execute! engloba seu comando numa transação, por isso passamos o terceiro parametro desabilitando essa funcionalidade
No mapa de configuração é necessário dizer o dbname
, que ainda não existe. Colocamos uma string vazia.
Depois de criar o database, aproveitamos para instalar o schema (dessa vez com o dbname
já setado)
Essa tarefa que fizemos no REPL é util e provavelmente vamos querer ela algumas vezes. Por isso vou salvar ela num arquivo
src/app/core.clj
Alguns detalhes:
-
Os arquivos clojure sempre devem declarar um namespace correspondente a sua posição
-
Não é uma boa pratica usar namespaces não qualificados
(ns app ...)
-
Usamos
ìo/resource
para evitar a leitura de arquivos que não sejam recursos e para funcionar corretamente caso a aplicação esteja rodando em um.jar
-
Isso está longe de ser boas praticas de SQL ;)
De volta ao REPL, podemos brincar um pouco com o jdbc:
Interagindo com o GraphQL
Agora vamos ao graphql. Será que é possivel fazer um { hello }
em uma linha de lacinia?
Vamos entender:
-
lacinia/execute
recebe 4 argumentos: o schema compilado, a string de query, um mapa com as variaveis, e um mapa de contexto -
schema/compile
gera um “schema compilado”, baseado numa descrição de EDN -
No retorno, era esperado
{:data {:hello "mundo!"}}
, porém, como na especificação do GraphQL é dito que a ordem dos pareskv
no mapa importam e devem ser respeitadas, e o mapa padrão do clojure é indiferente quanto ordem, o lacinia precisa usar esse tipo customizado, que se comporta como mapa 99% do tempo, porém na hora de serializar respeita a ordem.
Recomendo fortemente consultar os manuais e documentações do lacinia
O lacinia permite que vc escreva seu schema usando o GraphQL SDL, e faremos isso
Podemos observar que a parse-schema
retorna uma estrutura de dados quase igual aquela que passamos como argumento
para o schema/compile
, a menos do :look-at-me
, que eu deveria ter posto uma função no lugar da keyword.
Você pode tentar fazer o {hello}
novamente: (-> (parse-schema "..." {...}) (compile) (execute ".." {} {}))
Vamos jogar nosso schema em resources/schema.graphql
Vale dizer que QueryRoot
e MutationRoot
é o nome padrão que o lacinia procura para seus roots. Outras implemntações
podem usar outros nomes.
Já podemos fazer uma função que pega esse schema e retorna o schema compilado do lacinia.
Os resolvers do lacinia: resolve-todos
, resolve-eu
, me-cria
, novo-todo
e deleta-todo
recebem 3 argumentos:
-
O “contexto” da aplicação. Aquele que vem no ultimo argumento do
lacinia/execute
. Geralmente é um mapa, vc pode colocar qualquer coisa lá. -
Os argumentos da query/mutation. Se a query é
{ hello (id: 1) }
, no segundo argumento deve chegar{:id 1}
-
A entidade pai. Se temos a query
{ eu { id nome todos { id } } }
etodos
tem um resolver proprio, então ele recebe o resutado do resolveeu
no terceiro argumento.
Coloquei alguns prn
para tentar a ajudar a entender
Não fechou o REPL né? Ele deve ficar aberto para sempre!!!!!!
-
O prn da resolve-eu, mostrando que recebeu no primeiro parametro os argumentos passados em
eu
na query -
O prn da resolve-todos, mostrndo que não há argumentos, porém á um “parent”
API HTTP
Vamos aproveitar esse momento que temos uma query funcional e subir o servidor HTTP.
Faça o require de [io.pedestal.http :as http]
, [com.walmartlabs.lacinia.pedestal :as pedestal]
e adicione
no fim do src/app/core.clj
o código:
Agora vc pode chamar no REPL o (require app.core :reload)
e iniciar o servidor via (run-dev)
.
Em http://localhost:8888 deve ter um playground graphiql
onde vc pode tentar executar novamente nossa query { eu(nome: \"mesmo\") { id nome todos { id } } }
Implementando resolvers
Agora já pode tentar implementar essas funções.
Repare na variavel app-context
, que é passada como “Contexto da aplicação” no execute.
Ela coloca o db
em ::jdbc/db
. Para acessaar ela, precisamos pegar esse valor do contexto (primeiro argumento do resolver)
Exemplo de uma possivel implementação de “resolve-eu”
A implementação está no github!
Testes, testes, testes, testes
Na hora de implementar as funçoes, muitas vezes vc vai fazer reload, rodar novamente uma função e conferir o retorno. Essa tarefa chata e repetitiva pode ser vacilitada com TESTES.
No github tem testes. Após copiar o arquivo de teste (e algumas funções que ele requer da app.core), basta rodar no REPL