9 min read

[Jornada do DevOps] #8 - API Client com GraphQL em Golang

[Jornada do DevOps] #8 - API Client com GraphQL em Golang
Photo by Douglas Lopes / Unsplash

No último artigo estuamos sobre Real Time Communication com Melody no Golang.

Hoje vamos estudar sobre API Client no Golang, especificamente, GraphQL. Borá?

Entrando nesse tema, precisamos mais a frente definir o banco de dados, geralmente utilizamos na Overall.Cloud o DynamoDB, MongoDB ou MySQL (RDS).

Vou seguir o tutorial que a comunidade fez do GraphQL(HowTo).

Sobre o GraphQL

GraphQL é um novo padrão de API que fornece uma alternativa mais eficiente, poderosa e flexível ao REST. Foi desenvolvido e de código aberto pelo Facebook e agora é mantido por uma grande comunidade de empresas e indivíduos de todo o mundo.

As APIs tornaram-se componentes onipresentes das infraestruturas de software. Resumindo, uma API define como um cliente pode carregar dados de um servidor .

Em sua essência, o GraphQL permite a busca declarativa de dados, onde um cliente pode especificar exatamente quais dados ele precisa de uma API. Em vez de vários endpoints que retornam estruturas de dados fixas, um servidor GraphQL expõe apenas um único endpoint e responde precisamente com os dados solicitados pelo cliente.

Configuração do projeto

Crie um diretório para o projeto e inicialize o arquivo de módulos go:

go mod init howto

depois disso, use o comando initpara configurar um projeto gqlgen.

go run github.com/99designs/gqlgen init

Aqui está uma descrição do gqlgen sobre os arquivos gerados:

  • gqlgen.yml— O arquivo de configuração gqlgen, botões para controlar o código gerado.
  • graph/generated/generated.go— O tempo de execução do GraphQL, a maior parte do código gerado.
  • graph/model/models_gen.go— Modelos gerados necessários para construir o gráfico. Frequentemente, você os substituirá por seus próprios modelos. Ainda muito útil para tipos de entrada.
  • graph/schema.graphqls— Este é o arquivo onde você adicionará esquemas GraphQL.
  • graph/schema.resolvers.go— É aqui que mora o código do seu aplicativo. O gerado.go chamará isso para obter os dados que o usuário solicitou.
  • server.go— Este é um ponto de entrada mínimo que configura um http.Handler para o servidor GraphQL gerado. inicie o servidor go run server.go e abra seu navegador e você deverá ver o playground graphql, então a configuração está correta!

Definindo nosso esquema

Agora vamos começar com a definição do esquema que precisamos para nossa API.

Arquivo: graph/schema.graphqls

type Link {
  id: ID!
  title: String!
  address: String!
  user: User!
}

type User {
  id: ID!
  name: String!
}

type Query {
  links: [Link!]!
}

input NewLink {
  title: String!
  address: String!
}

input RefreshTokenInput{
  token: String!
}

input NewUser {
  username: String!
  password: String!
}

input Login {
  username: String!
  password: String!
}

type Mutation {
  createLink(input: NewLink!): Link!
  createUser(input: NewUser!): String!
  login(input: Login!): String!
  # we'll talk about this in authentication section
  refreshToken(input: RefreshTokenInput!): String!
}


Agora execute o seguinte comando para regenerar os arquivos.

go get github.com/99designs/[email protected]
go get github.com/99designs/gqlgen/internal/[email protected]
go get github.com/99designs/gqlgen/codegen/[email protected]
go get github.com/99designs/gqlgen/internal/[email protected]
go run github.com/99designs/gqlgen generate
Nota : Se você está recebendo validation failed: packages.Loaderro. Isso pode ocorrer, pois gqlgenusa o projeto todo como modelo inicial. Para se livrar desse erro, edite graph/schema.resolvers.goo arquivo e exclua as funções CreateTodoe arquivos Todos. Agora execute o comando novamente.

Depois que o gqlgen gerou o código para nós, teremos que implementar nosso esquema, fazemos isso em schema.resolvers.go‍‍‍‍ , como você vê, existem funções para Consultas e Mutações que definimos em nosso esquema.

Consultas

O que é uma consulta

Uma consulta no graphql está solicitando dados, você usa uma consulta e especifica o que deseja e o graphql retornará para você.

Consulta Simples

Abra o arquivo schema.resolvers.goe dê uma olhada na função Links:

func (r *queryResolver) Links(ctx context.Context) ([]*model.Link, error) {

Observe que esta função recebe um Context e retorna uma fatia de Links e um erro (se houver). ctx contém os dados da pessoa que envia a solicitação, como qual usuário está trabalhando com o aplicativo (veremos como mais tarde), etc.

schema.resolvers.go:

func (r *queryResolver) Links(ctx context.Context) ([]*model.Link, error) {
  var links []*model.Link
  dummyLink := model.Link{
    Title: "our dummy link",
    Address: "https://address.org",
    User: &model.User{Name: "admin"},
  }
	links = append(links, &dummyLink)
	return links, nil
}

Agora execute o servidor com go run server.goe envie esta consulta no Graphiql:

query {
	links{
    title
    address,
    user{
      name
    }
  }
}

A saída deve ser:

{
  "data": {
    "links": [
      {
        "title": "our dummy link",
        "address": "https://address.org",
        "user": {
          "name": "admin"
        }
      }
    ]
  }
}

Agora sabemos como geramos resposta para nosso servidor graphql. Mas esta resposta é apenas uma resposta fictícia, queremos poder consultar todos os links de outros usuários.

Agora vamos configurar o banco de dados para que nosso aplicativo possa salvar links e recuperá-los do banco de dados.

Mutações

O que é uma mutação

Simplesmente mutações são como consultas, mas podem ser uma gravação de dados. Tecnicamente, as consultas também podem ser usadas para gravar dados, mas não é sugerido usá-las. Portanto, as mutações são como consultas, elas têm nomes, parâmetros e podem retornar dados.

Uma simples mutação

Vamos tentar implementar a mutação createLink, já que ainda não temos um banco de dados configurado, apenas recebemos os dados do link e construímos um objeto link e o enviamos de volta para resposta!

Abra schema.resolvers.goe veja a função CreateLink:

func (r *mutationResolver) CreateLink(ctx context.Context, input model.NewLink) (*model.Link, error) {

Esta função recebe um NewLinktipo de inpute a estrutura NewLink que definimos em nosso arquivo schema.graphqls.

Vamos criar um objeto Linkque definimos em nosso schema.graphqls:

func (r *mutationResolver) CreateLink(ctx context.Context, input model.NewLink) (*model.Link, error) {
	var link model.Link
	var user model.User
	link.Address = input.Address
	link.Title = input.Title
	user.Name = "test"
	link.User = &user
	return &link, nil
}

Agora vamos executar o servidor e use a mutação para criar um novo link:

mutation {
  createLink(input: {title: "new link", address:"http://address.org"}){
    title,
    user{
      name
    }
    address
  }
}

O retorno deve ser:

{
  "data": {
    "createLink": {
      "title": "new link",
      "user": {
        "name": "test"
      },
      "address": "http://address.org"
    }
  }
}

Legal agora que sabemos o que são mutações e consultas podemos configurar nosso banco de dados e tornar essas implementações mais práticas.

Banco de Dados

Antes de começarmos a implementar o esquema GraphQL, precisamos configurar o banco de dados para salvar usuários e links.

  • Configurar MySQL
  • Criar banco de dados MySQL
  • Definir nossos modelos e criar migrações

Configurar MySQL

Se você tiver o docker, vamos executar a imagem do Mysql do docker.

docker run -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=dbpass -e MYSQL_DATABASE=aprendendo-go -d mysql:latest

Agora se a gente executar o docker psvamos ver a imagem do mysql executando:

CONTAINER ID        IMAGE                                                               COMMAND                  CREATED             STATUS              PORTS                  NAMES
8fea71529bb2        mysql:latest                                                        "docker-entrypoint.s…"   2 hours ago         Up 2 hours          3306/tcp, 33060/tcp    mysql

Criar banco de dados MySQL

Agora precisaremos criar nosso banco de dados nessa instância. Para criar o banco de dados:

docker exec -it mysql bash
mysql -u root -p

Ele pedirá a senha do root, édbpass.

CREATE DATABASE aprendendo-go;

Modelos e migrações

Precisamos criar migrações para que toda vez que nosso aplicativo for executado, ele crie tabelas para funcionar corretamente, usaremos o pacote golang-migrate.

Vamos criar uma estrutura de pastas para nossos arquivos de banco de dados no diretório raiz do projeto:

graphql-go-howto
--internal
----pkg
------db
--------migrations
----------mysql

Instale o driver go mysql e os pacotes golang-migrate e crie as migrações:

go get -u github.com/go-sql-driver/mysql
go get github.com/golang-migrate/migrate/v4/cmd/migrate
go build -tags 'mysql' -ldflags="-X main.Version=1.0.0" -o $GOPATH/bin/migrate github.com/golang-migrate/migrate/v4/cmd/migrate/
cd internal/pkg/db/migrations/
migrate create -ext sql -dir mysql -seq create_users_table
migrate create -ext sql -dir mysql -seq create_links_table

O comando migrate criará dois arquivos para cada migração que termina com .up e .down; up é responsável por aplicar a migração e down é responsável por revertê-la. abra 000001_create_users_table.up.sqle adicione a tabela para nossos usuários:

CREATE TABLE IF NOT EXISTS Users(
    ID INT NOT NULL UNIQUE AUTO_INCREMENT,
    Username VARCHAR (127) NOT NULL UNIQUE,
    Password VARCHAR (127) NOT NULL,
    PRIMARY KEY (ID)
)

em 000002_create_links_table.up.sql:

CREATE TABLE IF NOT EXISTS Links(
    ID INT NOT NULL UNIQUE AUTO_INCREMENT,
    Title VARCHAR (255) ,
    Address VARCHAR (255) ,
    UserID INT ,
    FOREIGN KEY (UserID) REFERENCES Users(ID) ,
    PRIMARY KEY (ID)
)

Precisamos de uma tabela para salvar links e uma tabela para salvar usuários, então aplicamos isso ao nosso banco de dados usando o comando migrate. Vamos executar isso na pasta do projeto:

migrate -database mysql://root:[email protected]/aprendendo-go -path internal/pkg/db/migrations/mysql up

A última coisa é que precisamos de uma conexão com nosso banco de dados, para isso criamos um mysql.go na pasta mysql(Nomeamos este arquivo após mysql já que agora estamos usando mysql e se quisermos ter vários bancos de dados podemos adicionar outras pastas) com uma função para inicializar a conexão com o banco de dados para uso posterior.

internal/pkg/db/mysql/mysql.go:

package database

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
	"github.com/golang-migrate/migrate"
	"github.com/golang-migrate/migrate/database/mysql"
	_ "github.com/golang-migrate/migrate/source/file"
	"log"
)

var Db *sql.DB

func InitDB() {
	// Use root:[email protected](172.17.0.2)/aprendendo-go, if you're using Windows.
	db, err := sql.Open("mysql", "root:[email protected](localhost)/aprendendo-go")
	if err != nil {
		log.Panic(err)
	}

	if err = db.Ping(); err != nil {
 		log.Panic(err)
	}
	Db = db
}

func Migrate() {
	if err := Db.Ping(); err != nil {
		log.Fatal(err)
	}
	driver, _ := mysql.WithInstance(Db, &mysql.Config{})
	m, _ := migrate.NewWithDatabaseInstance(
		"file://internal/pkg/db/migrations/mysql",
		"mysql",
		driver,
	)
	if err := m.Up(); err != nil && err != migrate.ErrNoChange {
		log.Fatal(err)
	}

}

A função InitDB cria uma conexão com nosso banco de dados e a função Migrateexecuta o arquivo de migração para nós.

Na função `Migrate, aplicamos as migrações como fizemos com a linha de comando, mas com esta função seu aplicativo sempre aplicará as migrações mais recentes antes de iniciar.

Em seguida, vamos chamar oInitDBe Migratena função principal para criar uma conexão com o banco de dados no início do aplicativo:

server.go:

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}

	router := chi.NewRouter()

	database.InitDB()
	database.Migrate()
	server := handler.NewDefaultServer(aprendendo-go.NewExecutableSchema(aprendendo-go.Config{Resolvers: &aprendendo-go.Resolver{}}))
	router.Handle("/", playground.Handler("GraphQL playground", "/query"))
	router.Handle("/query", server)

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, router))
}

Criar e recuperar links

Vamos implementar a mutação CreateLink; primeiro precisamos de uma função que nos permita escrever um link para o banco de dados. Crie pastas linkse usersdentro internaldo diretório, esses pacotes são camadas entre banco de dados e nosso aplicativo.

internal/users/users.go:

package users

type User struct {
	ID       string `json:"id"`
	Username string `json:"name"`
	Password string `json:"password"`
}

internal/links/links.go:

package links

import (
	database "github.com/glyphack/go-graphql-hackernews/internal/pkg/db/mysql"
	"github.com/glyphack/go-graphql-hackernews/internal/users"
	"log"
)

// #1
type Link struct {
	ID      string
	Title   string
	Address string
	User    *users.User
}

//#2
func (link Link) Save() int64 {
	//#3
	stmt, err := database.Db.Prepare("INSERT INTO Links(Title,Address) VALUES(?,?)")
	if err != nil {
		log.Fatal(err)
	}
	//#4
	res, err := stmt.Exec(link.Title, link.Address)
	if err != nil {
		log.Fatal(err)
	}
	//#5
	id, err := res.LastInsertId()
	if err != nil {
		log.Fatal("Error:", err.Error())
	}
	log.Print("Row inserted!")
	return id
}

Em users.go, acabamos de definir um structque representa os usuários que obtemos do banco de dados, mas deixe-me explicar links.go parte por parte:

  • 1: definição de struct que representa um link.
  • 2: função que insere um objeto Link no banco de dados e retorna seu ID.
  • 3: nossa consulta sql para inserir link na tabela Links. você vê que usamos prepare aqui antes do db.Exec, as instruções preparadas ajudam na segurança e também na melhoria de desempenho em alguns casos. você pode ler mais sobre isso aqui .
  • 4: execução da nossa instrução sql.
  • 5: recuperando o Id do Link inserido.

Agora usamos esta função em nosso resolvedor CreateLink:

schema.resolvers.go:

func (r *mutationResolver) CreateLink(ctx context.Context, input model.NewLink) (*model.Link, error) {
	var link links.Link
	link.Title = input.Title
	link.Address = input.Address
	linkID := link.Save()
	return &model.Link{ID: strconv.FormatInt(linkID, 10), Title:link.Title, Address:link.Address}, nil
}

Criamos um objeto de link da entrada e o salvamos no banco de dados e, em seguida, retornamos o link recém-criado (observe que convertemos o ID em string com strconv.FormatInt).

Note que aqui temos 2 structs para Link em nosso projeto, uma é usada para nosso servidor graphql e outra é para nosso banco de dados. execute o servidor e abra a página graphiql para testar o que acabamos de escrever:

mutation create{
  createLink(input: {title: "something", address: "somewhere"}){
    title,
    address,
    id,
  }
}
{
  "data": {
    "createLink": {
      "title": "something",
      "address": "somewhere",
      "id": "1"
    }
  }
}

Assim como implementamos a mutação CreateLink, implementamos a consulta de links, precisamos de uma função para recuperar links do banco de dados e passá-los para o servidor graphql em nosso resolvedor.

Crie uma função chamada GetAll:

internal/links/links.go:

func GetAll() []Link {
	stmt, err := database.Db.Prepare("select id, title, address from Links")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()
	rows, err := stmt.Query()
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	var links []Link
	for rows.Next() {
		var link Link
		err := rows.Scan(&link.ID, &link.Title, &link.Address)
		if err != nil{
			log.Fatal(err)
		}
		links = append(links, link)
	}
	if err = rows.Err(); err != nil {
		log.Fatal(err)
	}
	return links
}

Retornar links de GetAll na consulta Links.

schema.resolvers.go:

func (r *queryResolver) Links(ctx context.Context) ([]*model.Link, error) {
	var resultLinks []*model.Link
	var dbLinks []links.Link
	dbLinks = links.GetAll()
	for _, link := range dbLinks{
		resultLinks = append(resultLinks, &model.Link{ID:link.ID, Title:link.Title, Address:link.Address})
	}
	return resultLinks, nil
}

Agora consulte Links em graphiql:

query {
  links {
    title
    address
    id
  }
}

Resultado:

{
  "data": {
    "links": [
      {
        "title": "something",
        "address": "somewhere",
        "id": "1"
      }
    ]
  }
}

Neste estudo vimos sobre API Client no Golang, utilizando GraphQL.

Sobre o Golang, não vou entrar a fundo sobre as ferramentas para microserviços. Por aqui encerramos os estudos da linguagem para DevOps.

Agora vamos entrar na seção dos Conceitos de S.O.

No roadmap do DevOps estão os principais conceitos:

  • Process Management
  • Threads and Concurrency
  • Sockets
  • POSIX Basics
  • Networking Concepts
  • Service Management - systemd
  • Startup Management - intid
  • I/O Management
  • Virtualization
  • Memory/Storage
  • File Systems

No próximo estudo vamos ver [Jornada do DevOps] #9 - Conceitos de um Sistema Operacional.