Before I get started the project can be found on github. However, this project is based on an amazing article that can be found on dev.
I have dockerized this project but is not required for functionality. Finally, access to an auth provider like google or auth0 is required. But, you are not limited to those as we will be using goth which supports quite a number of providers.
The following will be the structure of the app
.
├── build
│ └── gql-server
├── cmd
│ └── gql-server
│ └── main.go
├── docker
│ ├── app
│ │ └── dev.Dockerfile
│ └── mysql
│ └── my.cnf
├── docker-compose.yml
├── go.mod
├── go.sum
├── gqlgen.yml
├── internal
│ ├── gql
│ │ ├── generated.go
│ │ ├── models
│ │ │ └── generated.go
│ │ ├── resolvers
│ │ │ ├── generated
│ │ │ │ └── resolver.go
│ │ │ ├── main.go
│ │ │ ├── transformations
│ │ │ │ ├── users.go
│ │ │ │ └── users_test.go
│ │ │ └── users.go
│ │ └── schemas
│ │ └── schema.graphql
│ ├── handlers
│ │ ├── auth
│ │ │ ├── handlers.go
│ │ │ ├── main.go
│ │ │ └── middleware
│ │ │ ├── auth.go
│ │ │ └── main.go
│ │ ├── gql.go
│ │ └── ping.go
│ ├── logger
│ │ └── main.go
│ └── orm
│ ├── main.go
│ ├── migration
│ │ ├── jobs
│ │ │ ├── seed_rbac.go
│ │ │ └── seed_users.go
│ │ └── main.go
│ └── models
│ ├── base.go
│ ├── rbac.go
│ └── user.go
├── pkg
│ ├── server
│ │ ├── init.go
│ │ ├── main.go
│ │ ├── router.go
│ │ └── routes
│ │ ├── auth.go
│ │ ├── graphql.go
│ │ └── misc.go
│ └── utils
│ ├── consts
│ │ └── main.go
│ ├── context-keys.go
│ ├── env.go
│ └── types.go
└── scripts
├── build.sh
├── dev-run.sh
├── gqlgen.sh
└── run.sh
I believe it will be much easier to speak about each folder and/or subfolder individually to break down code and to better understand the logic.
As the heart of this server is to provide GraphQL I will first explain the folder internal\gql
. This folder is based on the package gqlgen and requires a gqlgen.yml
in the root directory.
The file contains the following
# go-gql-server gqlgen.yml file
# Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation.
# Pick up all the schema files you put in this directory
schema:
- 'internal/gql/schemas/**/*.graphql'
# Let gqlgen know where to put the generated server
exec:
filename: internal/gql/generated.go
package: gql
# Let gqlgen know where to put the generated models (if any)
model:
filename: internal/gql/models/generated.go
package: models
# Let gqlgen know where to put the generated resolvers
resolver:
filename: internal/gql/resolvers/generated/resolver.go
type: Resolver
package: resolvers
autobind: []
According to the documentation for gqlgen
the following command go run github.com/99designs/gqlgen init
would produce several important files
- gqlgen.yml — The gqlgen config file, knobs for controlling the generated code.
- generated.go — The GraphQL execution runtime, the bulk of the generated code.
- models_gen.go — Generated models required to build the graph. Often you will override these with your own models. Still very useful for input types.
- resolver.go — This is where your application code lives. generated.go will call into this to get the data the user has requested.
- server/server.go — This is a minimal entry point that sets up an http.Handler to the generated GraphQL server.
But we have configured a gqlgen.yml
to place these files in specific folders with specific package names. So in fact we really only need to run go run github.com/99designs/gqlgen
from the root directory. server.go
will not be produced as we do not need this. The entire point of this project is to create this server. For further information on gqlgen.yml
you can read the documentation.
We could also, for simplicity, incorporate the gqlgen
command into a bash script and place it in the scripts
folder as gqlgen.sh
#!/bin/bash
printf "\nRegenerating gqlgen files\n"
# Optional, delete the resolver to regenerate, only if there are new queries
# or mutations, if you are just chageing the input or type definition and
# doesn't impact the resolvers definitions, no need to do it
while [[ "$#" -gt 0 ]]; do case $1 in
-r|--resolvers)
rm -f internal/gql/resolvers/generated/resolver.go
;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
time go run -v github.com/99designs/gqlgen
printf "\nDone.\n\n"
Anyhow, let’s get back to the gql
folder. We will need to define the schema of our GraphQL
server by using a schema.graphql
file that describes our API using the GraphQL Schema Definition Language that is placed in the folder internal/gql/schemas
. For instance, we could use the following for users
important note
As defined by our gqlgen.yml
we do not need to use schema
as the filename and to be better organized, especially with a larger project, I believe we should name the schema more appropriately such as users_schema.graphql
.
scalar Time
# Types
type User {
id: ID!
email: String!
avatarURL: String
name: String
firstName: String
lastName: String
nickName: String
description: String
location: String
APIkey: String
profiles: [UserProfile]
createdAt: Time
updatedAt: Time
}
type UserProfile {
id: ID!
email: String!
avatarURL: String
name: String
firstName: String
lastName: String
nickName: String
description: String
location: String
APIkey: String
profiles: [UserProfile]
createdAt: Time!
updatedAt: Time
}
# Input Types
input UserInput {
email: String
password: String
avatarURL: String
displayName: String
name: String
firstName: String
lastName: String
nickName: String
description: String
location: String
}
# List Types
type Users {
count: Int
list: [User!]!
}
# Define mutations here
type Mutation {
createUser(input: UserInput!): User!
updateUser(id: ID!, input: UserInput!): User!
deleteUser(id: ID!): Boolean!
}
# Define queries here
type Query {
users(id: ID): Users!
}
I believe the schema
is self-explanatory, but you can always leave a comment, and I will respond. Just remember though, any modifications or additional schemas, you will need to rerun the gqlgen.sh
script.
Now let’s get into the application code found under internal/gql/resolvers
. Here we have two files namely main.go
and users.go
.
The main.go
contains the following which includes a Resolver struct
that passes a database connection and we also define two functions that expose both the mutation
and query
types.
package resolvers
import (
"github.com/jessequinn/go-gql-server/internal/gql"
"github.com/jessequinn/go-gql-server/internal/orm"
)
// Resolver is a modifiable struct that can be used to pass on properties used
// in the resolvers, such as DB access
type Resolver struct {
ORM *orm.ORM
}
// Mutation exposes mutation methods
func (r *Resolver) Mutation() gql.MutationResolver {
return &mutationResolver{r}
}
// Query exposes query methods
func (r *Resolver) Query() gql.QueryResolver {
return &queryResolver{r}
}
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
The more specific users.go
contains all fields expressed in the schema
such as CreateUser
, UpdateUser
, DeleteUser
, etc. You’ll also notice some key imports namely log
which in fact uses a wrapper for logrus
, which will be discussed soon and finally tf
, which is needed as we are using a db
so we need a way to transform
the data to gorm
and vice-versa. Yes, this project requires gorm.
package resolvers
import (
"context"
log "github.com/jessequinn/go-gql-server/internal/logger"
"github.com/jessequinn/go-gql-server/internal/gql/models"
tf "github.com/jessequinn/go-gql-server/internal/gql/resolvers/transformations"
dbm "github.com/jessequinn/go-gql-server/internal/orm/models"
)
// CreateUser creates a record
func (r *mutationResolver) CreateUser(ctx context.Context, input models.UserInput) (*models.User, error) {
return userCreateUpdate(r, input, false)
}
// UpdateUser updates a record
func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input models.UserInput) (*models.User, error) {
return userCreateUpdate(r, input, true, id)
}
// DeleteUser deletes a record
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
return userDelete(r, id)
}
// Users lists records
func (r *queryResolver) Users(ctx context.Context, id *string) (*models.Users, error) {
return userList(r, id)
}
// ## Helper functions
func userCreateUpdate(r *mutationResolver, input models.UserInput, update bool, ids ...string) (*models.User, error) {
dbo, err := tf.GQLInputUserToDBUser(&input, update, ids...)
if err != nil {
return nil, err
}
// Create scoped clean db interface
db := r.ORM.DB.New().Begin()
if !update {
db = db.Create(dbo).First(dbo) // Create the user
} else {
db = db.Model(&dbo).Update(dbo).First(dbo) // Or update it
}
gql, err := tf.DBUserToGQLUser(dbo)
if err != nil {
db.RollbackUnlessCommitted()
return nil, err
}
db = db.Commit()
return gql, db.Error
}
func userDelete(r *mutationResolver, id string) (bool, error) {
return false, nil
}
func userList(r *queryResolver, id *string) (*models.Users, error) {
entity := "users"
whereID := "id = ?"
record := &models.Users{}
dbRecords := []*dbm.User{}
db := r.ORM.DB.New()
if id != nil {
db = db.Where(whereID, *id)
}
db = db.Find(&dbRecords).Count(&record.Count)
for _, dbRec := range dbRecords {
if rec, err := tf.DBUserToGQLUser(dbRec); err != nil {
log.Errorfn(entity, err)
} else {
record.List = append(record.List, rec)
}
}
return record, db.Error
}
The transformations
that we import, as mentioned above, provides a way to transform
the input type
to gorm
and vice-versa. For instance, transformations/users.go
contains several functions to do this
package transformations
import (
"errors"
"github.com/markbates/goth"
"github.com/gofrs/uuid"
gql "github.com/jessequinn/go-gql-server/internal/gql/models"
dbm "github.com/jessequinn/go-gql-server/internal/orm/models"
)
// DBUserToGQLUser transforms [user] db input to gql type
func DBUserToGQLUser(i *dbm.User) (o *gql.User, err error) {
o = &gql.User{
AvatarURL: i.AvatarURL,
ID: i.ID.String(),
Email: i.Email,
Name: i.Name,
FirstName: i.FirstName,
LastName: i.LastName,
NickName: i.NickName,
Description: i.Description,
Location: i.Location,
CreatedAt: i.CreatedAt,
UpdatedAt: i.UpdatedAt,
}
return o, err
}
// GQLInputUserToDBUser transforms [user] gql input to db model
func GQLInputUserToDBUser(i *gql.UserInput, update bool, ids ...string) (o *dbm.User, err error) {
o = &dbm.User{
Name: i.Name,
FirstName: i.FirstName,
LastName: i.LastName,
NickName: i.NickName,
Description: i.Description,
Location: i.Location,
}
if i.Email == nil && !update {
return nil, errors.New("field [email] is required")
}
if i.Password == nil && !update {
return nil, errors.New("field [password] is required")
}
if i.Email != nil {
o.Email = *i.Email
}
if i.Password != nil {
o.Password = *i.Password
}
if len(ids) > 0 {
updID, err := uuid.FromString(ids[0])
if err != nil {
return nil, err
}
o.ID = updID
}
return o, err
}
// GothUserToDBUser transforms [user] goth to db model
func GothUserToDBUser(i *goth.User, update bool, ids ...string) (o *dbm.User, err error) {
if i.Email == "" && !update {
return nil, errors.New("field [Email] is required")
}
o = &dbm.User{
Email: i.Email,
Name: &i.Name,
FirstName: &i.FirstName,
LastName: &i.LastName,
NickName: &i.NickName,
Location: &i.Location,
AvatarURL: &i.AvatarURL,
Description: &i.Description,
}
if len(ids) > 0 {
updID, err := uuid.FromString(ids[0])
if err != nil {
return nil, err
}
o.ID = updID
}
return o, err
}
// GothUserToDBUserProfile transforms [user] goth to db model
func GothUserToDBUserProfile(i *goth.User, update bool, ids ...int) (o *dbm.UserProfile, err error) {
if i.UserID == "" && !update {
return nil, errors.New("field [UserID] is required")
}
if i.Email == "" && !update {
return nil, errors.New("field [Email] is required")
}
o = &dbm.UserProfile{
ExternalUserID: i.UserID,
Provider: i.Provider,
Email: i.Email,
Name: i.Name,
FirstName: i.FirstName,
LastName: i.LastName,
NickName: i.NickName,
Location: i.Location,
AvatarURL: i.AvatarURL,
Description: &i.Description,
}
if len(ids) > 0 {
updID := ids[0]
o.ID = updID
}
return o, err
}
to be continued …