4 min read

[Jornada do DevOps] #4 - GraphQL com Golang

[Jornada do DevOps] #4 - GraphQL com Golang

No último post da [Jornada do DevOps] #3 - Aprofundando os estudos no Go ficou pendente nosso estudo sobre GraphQL.

Hoje, precisamos aprender como usar a biblioteca gqlgen para criar uma API GraphQL simples com Golang e MySQL/MongoDB.

O que é

  • Request (Graphql doc) -> Response (JSON)
  • Language Specification
  • Schema (domain-specific)
  • Server Implementation

O que não é

  • A Graph database query language
  • A client-side state management solution
  • A solution for binary streams
  • Facebook Graph API
  • Limited to specific databases
  • Limited to Javascript/Node on the backend
  • Limited to use with React/Relay/Web

Operadores

Query, Read, Mutation, Write, Subscription e Observe Event.

Problemas para resolver

Os principais problemas que o GraphQL resolve são: Overfetching e Underfetching.

E as principais dificuldades são trabalhar com Cache e Erros.

Servidor em Graphql e Golang

Desenvolvi o servidor seguindo o tutorial abaixo:

No exemplo, utilizamos a biblioteca 99designs/gqlgen.

Exemplo para iniciar o servidor:

mkdir example
cd example
go mod init example

Add github.com/99designs/gqlgen no repositório:

printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go
go mod tidy

Inicia as configs do gqlgen config e gera os models

go run github.com/99designs/gqlgen init

Inicia o servidor GraphQL

go run server.go

Veja aqui o passo a passo para criar um servidor GraphQL em Golang.

O Graphql facilita e muito!

As consultas são feitas com a query, que facilitam bastante.

Query para registrar
Consultando os registros
Consultando os registros com IDs

O repositório ficou aqui.

Aqui separei uma comparação dos recursos de outras implementações do Go GraphQL:

Autenticação

Temos um aplicativo em que os usuários são autenticados usando um cookie na solicitação HTTP e queremos verificar esse status de autenticação em algum lugar do nosso gráfico. Como o GraphQL é agnóstico de transporte, não podemos assumir que haverá uma solicitação HTTP, portanto, precisamos expor esses detalhes de autenticação ao nosso gráfico usando um middleware.

package auth

import (
	"database/sql"
	"net/http"
	"context"
)

// A private key for context that only this package can access. This is important
// to prevent collisions between different context uses
var userCtxKey = &contextKey{"user"}
type contextKey struct {
	name string
}

// A stand-in for our database backed user object
type User struct {
	Name string
	IsAdmin bool
}

// Middleware decodes the share session cookie and packs the session into context
func Middleware(db *sql.DB) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			c, err := r.Cookie("auth-cookie")

			// Allow unauthenticated users in
			if err != nil || c == nil {
				next.ServeHTTP(w, r)
				return
			}

			userId, err := validateAndGetUserID(c)
			if err != nil {
				http.Error(w, "Invalid cookie", http.StatusForbidden)
				return
			}

			// get the user from the database
			user := getUserByID(db, userId)

			// put it in context
			ctx := context.WithValue(r.Context(), userCtxKey, user)

			// and call the next with our new context
			r = r.WithContext(ctx)
			next.ServeHTTP(w, r)
		})
	}
}

// ForContext finds the user from the context. REQUIRES Middleware to have run.
func ForContext(ctx context.Context) *User {
	raw, _ := ctx.Value(userCtxKey).(*User)
	return raw
}

Nota: getUserByID e validateAndGetUserIDforam deixados para o usuário implementar.

Agora, quando criamos o servidor, devemos envolvê-lo em nosso middleware de autenticação:

package main

import (
	"net/http"

	"github.com/99designs/gqlgen/example/starwars"
	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/go-chi/chi"
)

func main() {
	router := chi.NewRouter()

	router.Use(auth.Middleware(db))

	srv := handler.NewDefaultServer(starwars.NewExecutableSchema(starwars.NewResolver()))
	router.Handle("/", playground.Handler("Starwars", "/query"))
	router.Handle("/query", srv)

	err := http.ListenAndServe(":8080", router)
	if err != nil {
		panic(err)
	}
}

E em nossos resolvedores (ou diretivas) podemos chamar ForContextpara recuperar os dados:

func (r *queryResolver) Hero(ctx context.Context, episode Episode) (Character, error) {
	if user := auth.ForContext(ctx) ; user == nil || !user.IsAdmin {
		return Character{}, fmt.Errorf("Access denied")
	}

	if episode == EpisodeEmpire {
		return r.humans["1000"], nil
	}
	return r.droid["2001"], nil
}

Websockets

Se você precisar acessar a carga útil do websocket init, podemos fazer a mesma coisa com o WebsocketInitFunc:

func main() {
	router := chi.NewRouter()

	router.Use(auth.Middleware(db))

	router.Handle("/", handler.Playground("Starwars", "/query"))
	router.Handle("/query",
		handler.GraphQL(starwars.NewExecutableSchema(starwars.NewResolver())),
		WebsocketInitFunc(func(ctx context.Context, initPayload InitPayload) (context.Context, error) {
			userId, err := validateAndGetUserID(payload["token"])
			if err != nil {
				return nil, err
			}

			// get the user from the database
			user := getUserByID(db, userId)

			// put it in context
			userCtx := context.WithValue(r.Context(), userCtxKey, user)

			// and return it so the resolvers can see it
			return userCtx, nil
		}))
	)

	err := http.ListenAndServe(":8080", router)
	if err != nil {
		panic(err)
	}
}

Observação: As assinaturas são de longa duração, se seus tokens atingirem o tempo limite ou precisarem ser atualizados, você também deverá manter o token no contexto e verificar se ainda é válido em auth.ForContext.

Ainda no tema do Golang, senti dificuldades em relação aos ORMs e aos Loggins. Por isso, no próximo estudo vou fazer:

  • [Jornada do DevOps] #5 - ORMs em Golang
  • [Jornada do DevOps] #6 - Loggings em Golang.

E também acho bem importante a gente entender alguns conceitos do Golang na AWS, vamos estudar no [Jornada do DevOps] #7 - AWS e Golang.

Fontes: https://gqlgen.com/ https://www.howtographql.com/