From 9cb230d25273233f0b1f6523be3e678a0ef3fcbd Mon Sep 17 00:00:00 2001 From: Jeff Willette Date: Sat, 8 Dec 2018 22:00:18 +0900 Subject: [PATCH] Updated to use better resolvers - the past resolvers used an all CAPS system to differentiate the resolvers from the other methods, this was strange and felt wrong. THey have been changed into their own structs which have the db added in through a dependency injection - added some helper functions for converting graphql and number types - added a logging middleware - changed some errant int32's to graphql.ID's - added .gqlint file - changed the schema descriptions to be quotes strings instead of comments --- .gqlint | 12 +++ README.md | 15 ++-- helpers.go | 32 ++++++++ pet_tags.go | 59 +++++++++----- pets.go | 89 ++++++++++++++------- resolvers.go | 103 ------------------------ root-resolver.go | 145 ++++++++++++++++++++++++++++++++++ schema.graphql | 81 ++++++++++--------- server.go | 34 ++++++-- user-pet-connection.go | 143 +++++++++++++++++++++++++++++++++ users.go | 174 +++++++---------------------------------- 11 files changed, 542 insertions(+), 345 deletions(-) create mode 100644 .gqlint create mode 100644 helpers.go delete mode 100644 resolvers.go create mode 100644 root-resolver.go create mode 100644 user-pet-connection.go diff --git a/.gqlint b/.gqlint new file mode 100644 index 0000000..60c8e0d --- /dev/null +++ b/.gqlint @@ -0,0 +1,12 @@ +{ + "rules": { + "camelcase": "error", + "fieldname.typename": "off", + "relay.connection": "error", + "relay.id": "error", + "singular.mutations": "error", + "enum.casing": "error", + "description.type": "warn", + "description.field": "off" + } +} diff --git a/README.md b/README.md index d4ba3ff..b75882c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ This example is meant to show a full implementation of the server using an SQL d ### Starting -start the server +start the server, if you don't have the sqlite library already installed from gorm, then it +may take a while to compile it ``` go run *.go @@ -16,11 +17,15 @@ and then visit the GraphiQL dev server at `localhost:8080` - int32 maps to the graphql type Int, so if a number is desired, int32 must be used -- the data structs are used for both database storage and graphql resolution. +- there are data structs for database models, and there is another struct that is usually + in the form of... -- resolver methods are all caps because the gorm ORM needs camel cased exported struct - fields in order to create database columns correctly. Another differentiation scheme can - be used if desired + ``` + type FooResolver struct { + db *DB + m Foo + } + ``` - authentication could go in middleware in the server part diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..88a2a99 --- /dev/null +++ b/helpers.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "strconv" + + graphql "github.com/graph-gophers/graphql-go" + "github.com/pkg/errors" +) + +func gqlIDToUint(i graphql.ID) (uint, error) { + r, err := strconv.ParseInt(string(i), 10, 32) + if err != nil { + return 0, errors.Wrap(err, "GqlIDToUint") + } + + return uint(r), nil +} + +func int32P(i uint) *int32 { + r := int32(i) + return &r +} + +func boolP(b bool) *bool { + return &b +} + +func gqlIDP(id uint) *graphql.ID { + r := graphql.ID(fmt.Sprint(id)) + return &r +} diff --git a/pet_tags.go b/pet_tags.go index 716ff67..0b9f8d8 100644 --- a/pet_tags.go +++ b/pet_tags.go @@ -5,6 +5,7 @@ import ( graphql "github.com/graph-gophers/graphql-go" "github.com/jinzhu/gorm" + "github.com/pkg/errors" ) // Tag is the base type for a pet tag to be used by the db and gql @@ -14,32 +15,14 @@ type Tag struct { Pets []Pet `gorm:"many2many:pet_tags"` } -// RESOLVERS =========================================================================== - -// ID resolves the ID for Tag -func (t *Tag) ID(ctx context.Context) *graphql.ID { - return gqlIDP(t.Model.ID) -} - -// TITLE resolves the title field -func (t *Tag) TITLE(ctx context.Context) *string { - return &t.Title -} - -// PETS resolves the pets field -func (t *Tag) PETS(ctx context.Context) (*[]*Pet, error) { - return db.getTagPets(ctx, t) -} - -// DB ================================================================================== -func (db *DB) getTagPets(ctx context.Context, t *Tag) (*[]*Pet, error) { - var p []*Pet +func (db *DB) getTagPets(ctx context.Context, t *Tag) ([]Pet, error) { + var p []Pet err := db.DB.Model(t).Related(&p, "Pets").Error if err != nil { return nil, err } - return &p, nil + return p, nil } func (db *DB) getTagBytTitle(ctx context.Context, title string) (*Tag, error) { @@ -51,3 +34,37 @@ func (db *DB) getTagBytTitle(ctx context.Context, title string) (*Tag, error) { return &t, nil } + +// TagResolver contains the db and the Tag model for resolving +type TagResolver struct { + db *DB + m Tag +} + +// ID resolves the ID for Tag +func (t *TagResolver) ID(ctx context.Context) *graphql.ID { + return gqlIDP(t.m.ID) +} + +// Title resolves the title field +func (t *TagResolver) Title(ctx context.Context) *string { + return &t.m.Title +} + +// Pets resolves the pets field +func (t *TagResolver) Pets(ctx context.Context) (*[]*PetResolver, error) { + pets, err := t.db.getTagPets(ctx, &t.m) + if err != nil { + return nil, errors.Wrap(err, "Pets") + } + + r := make([]*PetResolver, len(pets)) + for i := range pets { + r[i] = &PetResolver{ + db: t.db, + m: pets[i], + } + } + + return &r, nil +} diff --git a/pets.go b/pets.go index defb783..b76b752 100644 --- a/pets.go +++ b/pets.go @@ -5,6 +5,7 @@ import ( graphql "github.com/graph-gophers/graphql-go" "github.com/jinzhu/gorm" + "github.com/pkg/errors" ) // Pet is the base type for pets to be used by the db and gql @@ -15,30 +16,8 @@ type Pet struct { Tags []Tag `gorm:"many2many:pet_tags"` } -// RESOLVERS =========================================================================== -// ID resolves the ID field for Pet -func (p *Pet) ID(ctx context.Context) *graphql.ID { - return gqlIDP(p.Model.ID) -} - -// OWNER resolves the owner field for Pet -func (p *Pet) OWNER(ctx context.Context) (*User, error) { - return db.getPetOwner(ctx, int32(p.OwnerID)) -} - -// NAME resolves the name field for Pet -func (p *Pet) NAME(ctx context.Context) *string { - return &p.Name -} - -// TAGS resolves the pet tags -func (p *Pet) TAGS(ctx context.Context) (*[]*Tag, error) { - return db.getPetTags(ctx, p) -} - -// DB =================================================================================== // GetPet should authorize the user in ctx and return a pet or error -func (db *DB) getPet(ctx context.Context, id int32) (*Pet, error) { +func (db *DB) getPet(ctx context.Context, id uint) (*Pet, error) { var p Pet err := db.DB.First(&p, id).Error if err != nil { @@ -57,14 +36,14 @@ func (db *DB) getPetOwner(ctx context.Context, id int32) (*User, error) { return &u, nil } -func (db *DB) getPetTags(ctx context.Context, p *Pet) (*[]*Tag, error) { - var t []*Tag +func (db *DB) getPetTags(ctx context.Context, p *Pet) ([]Tag, error) { + var t []Tag err := db.DB.Model(p).Related(&t, "Tags").Error if err != nil { return nil, err } - return &t, nil + return t, nil } func (db *DB) getPetsByID(ctx context.Context, ids []int, from, to int) ([]Pet, error) { @@ -84,9 +63,14 @@ func (db *DB) updatePet(ctx context.Context, args *petInput) (*Pet, error) { return nil, err } + // so the pointer dereference is safe + if args.TagIDs == nil { + return nil, errors.Wrap(err, "UpdatePet") + } + // if there are tags to be updated, go through that process var newTags []Tag - if len(args.TagIDs) > 0 { + if len(*args.TagIDs) > 0 { err = db.DB.Where("id in (?)", args.TagIDs).Find(&newTags).Error if err != nil { return nil, err @@ -117,7 +101,7 @@ func (db *DB) updatePet(ctx context.Context, args *petInput) (*Pet, error) { return &p, nil } -func (db *DB) deletePet(ctx context.Context, userID, petID int32) (*bool, error) { +func (db *DB) deletePet(ctx context.Context, userID, petID uint) (*bool, error) { // make sure the record exist var p Pet err := db.DB.First(&p, petID).Error @@ -161,3 +145,52 @@ func (db *DB) addPet(ctx context.Context, input petInput) (*Pet, error) { return &pet, nil } + +// PetResolver contains the DB and the model for resolving +type PetResolver struct { + db *DB + m Pet +} + +// ID resolves the ID field for Pet +func (p *PetResolver) ID(ctx context.Context) *graphql.ID { + return gqlIDP(p.m.ID) +} + +// Owner resolves the owner field for Pet +func (p *PetResolver) Owner(ctx context.Context) (*UserResolver, error) { + user, err := p.db.getPetOwner(ctx, int32(p.m.OwnerID)) + if err != nil { + return nil, errors.Wrap(err, "Owner") + } + + r := UserResolver{ + db: p.db, + m: *user, + } + + return &r, nil +} + +// Name resolves the name field for Pet +func (p *PetResolver) Name(ctx context.Context) *string { + return &p.m.Name +} + +// Tags resolves the pet tags +func (p *PetResolver) Tags(ctx context.Context) (*[]*TagResolver, error) { + tags, err := p.db.getPetTags(ctx, &p.m) + if err != nil { + return nil, errors.Wrap(err, "Tags") + } + + r := make([]*TagResolver, len(tags)) + for i := range tags { + r[i] = &TagResolver{ + db: p.db, + m: tags[i], + } + } + + return &r, nil +} diff --git a/resolvers.go b/resolvers.go deleted file mode 100644 index ef395a6..0000000 --- a/resolvers.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "context" - "encoding/base64" - "fmt" - "strconv" - "strings" - - graphql "github.com/graph-gophers/graphql-go" -) - -var db DB - -func init() { - var err error - d, err := newDB("./db.sqlite") - if err != nil { - panic(err) - } - - db = *d -} - -// Resolver is the root resolver -type Resolver struct{} - -// GetUser resolves the getUser query -func (r *Resolver) GetUser(ctx context.Context, args struct{ ID int32 }) (*User, error) { - return db.getUser(ctx, int32(args.ID)) -} - -// GetPet resolves the getPet query -func (r *Resolver) GetPet(ctx context.Context, args struct{ ID int32 }) (*Pet, error) { - pet, err := db.getPet(ctx, args.ID) - if err != nil { - return nil, err - } - - return pet, nil -} - -// GetTag resolves the getTag query -func (r *Resolver) GetTag(ctx context.Context, args struct{ Title string }) (*Tag, error) { - return db.getTagBytTitle(ctx, args.Title) -} - -// petInput has everything needed to do adds and updates on a pet -type petInput struct { - ID int32 - OwnerID int32 - Name string - TagIDs []*int32 -} - -// AddPet Resolves the addPet mutation -func (r *Resolver) AddPet(ctx context.Context, args struct{ Pet petInput }) (*Pet, error) { - return db.addPet(ctx, args.Pet) -} - -// UpdatePet takes care of updating any field on the pet -func (r *Resolver) UpdatePet(ctx context.Context, args struct{ Pet petInput }) (*Pet, error) { - return db.updatePet(ctx, &args.Pet) -} - -// DeletePet takes care of deleting a pet record -func (r *Resolver) DeletePet(ctx context.Context, args struct{ UserID, PetID int32 }) (*bool, error) { - return db.deletePet(ctx, args.UserID, args.PetID) -} - -// encode cursor encodes the cursot position in base64 -func encodeCursor(i int) graphql.ID { - return graphql.ID(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("cursor%d", i)))) -} - -// decode cursor decodes the base 64 encoded cursor and resturns the integer -func decodeCursor(s string) (int, error) { - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return 0, err - } - - i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor")) - if err != nil { - return 0, err - } - - return i, nil -} - -func int32P(i uint) *int32 { - r := int32(i) - return &r -} - -func boolP(b bool) *bool { - return &b -} - -func gqlIDP(id uint) *graphql.ID { - r := graphql.ID(fmt.Sprint(id)) - return &r -} diff --git a/root-resolver.go b/root-resolver.go new file mode 100644 index 0000000..8c055e0 --- /dev/null +++ b/root-resolver.go @@ -0,0 +1,145 @@ +package main + +import ( + "context" + "encoding/base64" + "fmt" + "strconv" + "strings" + + graphql "github.com/graph-gophers/graphql-go" + "github.com/pkg/errors" +) + +// Resolver is the root resolver +type Resolver struct { + db *DB +} + +// GetUser resolves the getUser query +func (r *Resolver) GetUser(ctx context.Context, args struct{ ID graphql.ID }) (*UserResolver, error) { + id, err := gqlIDToUint(args.ID) + if err != nil { + return nil, errors.Wrap(err, "GetPet") + } + + user, err := r.db.getUser(ctx, id) + if err != nil { + return nil, errors.Wrap(err, "GetUser") + } + + s := UserResolver{ + db: r.db, + m: *user, + } + + return &s, nil +} + +// GetPet resolves the getPet query +func (r *Resolver) GetPet(ctx context.Context, args struct{ ID graphql.ID }) (*PetResolver, error) { + id, err := gqlIDToUint(args.ID) + if err != nil { + return nil, errors.Wrap(err, "GetPet") + } + + pet, err := r.db.getPet(ctx, id) + if err != nil { + return nil, err + } + + s := PetResolver{ + db: r.db, + m: *pet, + } + + return &s, nil +} + +// GetTag resolves the getTag query +func (r *Resolver) GetTag(ctx context.Context, args struct{ Title string }) (*TagResolver, error) { + tag, err := r.db.getTagBytTitle(ctx, args.Title) + if err != nil { + return nil, errors.Wrap(err, "GetTag") + } + + s := TagResolver{ + db: r.db, + m: *tag, + } + + return &s, nil +} + +// petInput has everything needed to do adds and updates on a pet +type petInput struct { + ID *graphql.ID + OwnerID int32 + Name string + TagIDs *[]*int32 +} + +// AddPet Resolves the addPet mutation +func (r *Resolver) AddPet(ctx context.Context, args struct{ Pet petInput }) (*PetResolver, error) { + pet, err := r.db.addPet(ctx, args.Pet) + if err != nil { + return nil, errors.Wrap(err, "AddPet") + } + + s := PetResolver{ + db: r.db, + m: *pet, + } + + return &s, nil +} + +// UpdatePet takes care of updating any field on the pet +func (r *Resolver) UpdatePet(ctx context.Context, args struct{ Pet petInput }) (*PetResolver, error) { + pet, err := r.db.updatePet(ctx, &args.Pet) + if err != nil { + return nil, errors.Wrap(err, "UpdatePet") + } + + s := PetResolver{ + db: r.db, + m: *pet, + } + + return &s, nil +} + +// DeletePet takes care of deleting a pet record +func (r *Resolver) DeletePet(ctx context.Context, args struct{ UserID, PetID graphql.ID }) (*bool, error) { + petID, err := gqlIDToUint(args.PetID) + if err != nil { + return nil, errors.Wrap(err, "DeletePet") + } + + userID, err := gqlIDToUint(args.UserID) + if err != nil { + return nil, errors.Wrap(err, "DeletePet") + } + + return r.db.deletePet(ctx, userID, petID) +} + +// encode cursor encodes the cursot position in base64 +func encodeCursor(i int) graphql.ID { + return graphql.ID(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("cursor%d", i)))) +} + +// decode cursor decodes the base 64 encoded cursor and resturns the integer +func decodeCursor(s string) (int, error) { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return 0, err + } + + i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor")) + if err != nil { + return 0, err + } + + return i, nil +} diff --git a/schema.graphql b/schema.graphql index dae3547..a49443e 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,68 +1,71 @@ schema { - query: Query - mutation: Mutation + query: Query + mutation: Mutation } -# The query type, represents all of the entry points into our object graph +"The query type, represents all of the entry points into our object graph" type Query { - getUser(id: ID!): User - getPet(id: ID!): Pet - getTag(title: String!): Tag + getUser(id: ID!): User + getPet(id: ID!): Pet + getTag(title: String!): Tag } -# The mutation type, represents all updates we can make to our data +"The mutation type, represents all updates we can make to our data" type Mutation { - addPet(pet: PetInput!): Pet - updatePet(pet: PetInput!): Pet - deletePet(userID: ID!, petID: ID!): Boolean + addPet(pet: PetInput!): Pet + updatePet(pet: PetInput!): Pet + deletePet(userID: ID!, petID: ID!): Boolean } -# what is needed for a pet +"what is needed for a pet" type Pet { - id: ID - owner: User - name: String - tags: [Tag] + id: ID + owner: User + name: String + tags: [Tag] } -# Tag has everything needed for a tag +"Tag has everything needed for a tag" type Tag { - id: ID - title: String - pets: [Pet] + id: ID + title: String + pets: [Pet] } -# what is needed for a user +"what is needed for a user" type User { - id: ID - name: String - # user pets exposed as a full list - pets: [Pet] - # user pets exposed as a connection with edges - petsConnection(first: Int, after: ID): UserPetConnection! + id: ID + name: String + # user pets exposed as a full list + pets: [Pet] + # user pets exposed as a connection with edges + petsConnection(first: Int, after: ID): UserPetConnection! } +"The connection between users and pets" type UserPetConnection { - totalCount: Int! - edges: [UserPetEdge] - pageInfo: PageInfo! + totalCount: Int! + edges: [UserPetEdge] + pageInfo: PageInfo! } +"The edge of the user pet connection" type UserPetEdge { - cursor: ID! - node: Pet + cursor: ID! + node: Pet } +"Page info for pagination" type PageInfo { - startCursor: ID - endCursor: ID - hasNextPage: Boolean! - hasPreviousPage: Boolean! + startCursor: ID + endCursor: ID + hasNextPage: Boolean! + hasPreviousPage: Boolean! } input PetInput { - id: ID! - ownerID: ID! - name: String! - tagIDs: [Int]! + id: ID + ownerID: ID! + name: String! + tagIDs: [Int] } diff --git a/server.go b/server.go index f19418b..6f28fe1 100644 --- a/server.go +++ b/server.go @@ -2,10 +2,11 @@ package main import ( "io/ioutil" - "log" "net/http" + "time" graphql "github.com/graph-gophers/graphql-go" + log "github.com/sirupsen/logrus" "github.com/graph-gophers/graphql-go/relay" ) @@ -17,15 +18,23 @@ func main() { panic(err) } - schema := graphql.MustParseSchema(s, &Resolver{}) + db, err := newDB("./db.sqlite") + if err != nil { + panic(err) + } + + schema := graphql.MustParseSchema(s, &Resolver{db: db}, graphql.UseStringDescriptions()) + + mux := http.NewServeMux() - http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(page) })) - http.Handle("/query", &relay.Handler{Schema: schema}) + mux.Handle("/query", &relay.Handler{Schema: schema}) - log.Fatal(http.ListenAndServe("localhost:8080", nil)) + log.WithFields(log.Fields{"time": time.Now()}).Info("starting server") + log.Fatal(http.ListenAndServe("localhost:8080", logged(mux))) } var page = []byte(` @@ -74,3 +83,18 @@ func getSchema(path string) (string, error) { return string(b), nil } + +// logging middleware +func logged(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now().UTC() + + next.ServeHTTP(w, r) + + log.WithFields(log.Fields{ + "path": r.RequestURI, + "IP": r.RemoteAddr, + "elapsed": time.Now().UTC().Sub(start), + }).Info() + }) +} diff --git a/user-pet-connection.go b/user-pet-connection.go new file mode 100644 index 0000000..903a4b9 --- /dev/null +++ b/user-pet-connection.go @@ -0,0 +1,143 @@ +package main + +import ( + "context" + "encoding/base64" + "strconv" + "strings" + + graphql "github.com/graph-gophers/graphql-go" +) + +type petsConnArgs struct { + First *int32 + After *graphql.ID +} + +// PetsConnection returns nodes (pets) connected by edges (relationships) +func (u *UserResolver) PetsConnection(ctx context.Context, args petsConnArgs) (*UserPetsConnectionResolver, error) { + // query only the ID fields from the pets otherwise it would be wasteful + ids, err := u.db.getUserPetIDs(ctx, u.m.ID) + if err != nil { + return nil, err + } + + from := 0 + if args.After != nil { + b, err := base64.StdEncoding.DecodeString(string(*args.After)) + if err != nil { + return nil, err + } + i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor")) + if err != nil { + return nil, err + } + from = i + 1 + } + + to := len(ids) + if args.First != nil { + to = from + int(*args.First) + if to > len(ids) { + to = len(ids) + } + } + + upc := UserPetsConnectionResolver{ + ids: ids, + from: from, + to: to, + } + return &upc, nil +} + +// UserPetEdge is an edge (related node) that is returned in pagination +type UserPetEdge struct { + cursor graphql.ID + node PetResolver +} + +// Cursor resolves the cursor for pagination +func (u *UserPetEdge) Cursor(ctx context.Context) graphql.ID { + return u.cursor +} + +// Node resolves the node for pagination +func (u *UserPetEdge) Node(ctx context.Context) *PetResolver { + return &u.node +} + +// PageInfo gives page info for pagination +type PageInfo struct { + startCursor graphql.ID + endCursor graphql.ID + hasNextPage bool + hasPreviousPage bool +} + +// StartCursor ... +func (u *PageInfo) StartCursor(ctx context.Context) *graphql.ID { + return &u.startCursor +} + +// EndCursor ... +func (u *PageInfo) EndCursor(ctx context.Context) *graphql.ID { + return &u.endCursor +} + +// HasNextPage returns true if there are more results to show +func (u *PageInfo) HasNextPage(ctx context.Context) bool { + return u.hasNextPage +} + +// HasPreviousPage returns true if there are results behind the current cursor position +func (u *PageInfo) HasPreviousPage(ctx context.Context) bool { + return u.hasPreviousPage +} + +// UserPetsConnectionResolver is all the pets that are connected to a certain user +type UserPetsConnectionResolver struct { + db *DB + ids []int + from int + to int +} + +// TotalCount gives the total amount of pets in UserPetsConnection +func (u UserPetsConnectionResolver) TotalCount(ctx context.Context) int32 { + return int32(len(u.ids)) +} + +// Edges gives a list of all the edges (related pets) that belong to a user +func (u *UserPetsConnectionResolver) Edges(ctx context.Context) (*[]*UserPetEdge, error) { + // query goes here because I know all of the ids that are needed. If I queried in the + // UserPetEdge resolver method, it would run multiple single queries + pets, err := u.db.getPetsByID(ctx, u.ids, u.from, u.to) + if err != nil { + return nil, err + } + + l := make([]*UserPetEdge, u.to-u.from) + for i := range l { + l[i] = &UserPetEdge{ + cursor: encodeCursor(u.from + i), + node: PetResolver{ + db: u.db, + m: pets[i], + }, + } + } + + return &l, nil +} + +// PageInfo resolves page info +func (u *UserPetsConnectionResolver) PageInfo(ctx context.Context) (*PageInfo, error) { + p := PageInfo{ + startCursor: encodeCursor(u.from), + endCursor: encodeCursor(u.to - 1), + hasNextPage: u.to < len(u.ids), + hasPreviousPage: u.from > 0, + } + return &p, nil +} diff --git a/users.go b/users.go index 482e7b9..7acb456 100644 --- a/users.go +++ b/users.go @@ -2,50 +2,19 @@ package main import ( "context" - "encoding/base64" - "strconv" - "strings" graphql "github.com/graph-gophers/graphql-go" "github.com/jinzhu/gorm" + "github.com/pkg/errors" ) // User is the base user model to be used throughout the app type User struct { gorm.Model Name string - pets []Pet + Pets []Pet `gorm:"foreignkey:OwnerID"` } -// RESOLVER METHODS ==================================================================== - -// ID resolves the user ID -func (u *User) ID(ctx context.Context) *graphql.ID { - return gqlIDP(u.Model.ID) -} - -// NAME resolves the Name field for User, it is all caps to avoid name clashes -func (u *User) NAME(ctx context.Context) *string { - return &u.Name -} - -// PETS resolves the Pets field for User -func (u *User) PETS(ctx context.Context) (*[]*Pet, error) { - return db.GetUserPets(ctx, int32(u.Model.ID)) -} - -// GetUserPets gets pets associated with the user -func (db *DB) GetUserPets(ctx context.Context, id int32) (*[]*Pet, error) { - var p []*Pet - err := db.DB.Model(&User{}).Association("pets").Find(&p).Error - if err != nil { - return nil, err - } - - return &p, nil -} - -// DB METHODS ========================================================================== func (db *DB) getUserPetIDs(ctx context.Context, userID uint) ([]int, error) { var ids []int err := db.DB.Where("owner_id = ?", userID).Find(&[]Pet{}).Pluck("id", &ids).Error @@ -55,7 +24,7 @@ func (db *DB) getUserPetIDs(ctx context.Context, userID uint) ([]int, error) { return ids, nil } -func (db *DB) getUser(ctx context.Context, id int32) (*User, error) { +func (db *DB) getUser(ctx context.Context, id uint) (*User, error) { var user User err := db.DB.First(&user, id).Error if err != nil { @@ -65,133 +34,50 @@ func (db *DB) getUser(ctx context.Context, id int32) (*User, error) { return &user, nil } -// PAGINATION ========================================================================== -type petsConnArgs struct { - First *int32 - After *graphql.ID -} - -// PETSCONNECTION returns nodes (pets) connected by edges (relationships) -func (u *User) PETSCONNECTION(ctx context.Context, args petsConnArgs) (*UserPetsConnection, error) { +// GetUserPets gets pets associated with the user +func (db *DB) GetUserPets(ctx context.Context, id uint) ([]Pet, error) { + var u User + u.ID = id - // query only the ID fields from the pets otherwise it would be wasteful - ids, err := db.getUserPetIDs(ctx, u.Model.ID) + var p []Pet + err := db.DB.Model(&u).Association("Pets").Find(&p).Error if err != nil { return nil, err } - from := 0 - if args.After != nil { - b, err := base64.StdEncoding.DecodeString(string(*args.After)) - if err != nil { - return nil, err - } - i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor")) - if err != nil { - return nil, err - } - from = i + 1 - } - - to := len(ids) - if args.First != nil { - to = from + int(*args.First) - if to > len(ids) { - to = len(ids) - } - } - - upc := UserPetsConnection{ - ids: ids, - from: from, - to: to, - } - return &upc, nil -} - -// UserPetEdge is an edge (related node) that is returned in pagination -type UserPetEdge struct { - cursor graphql.ID - node Pet -} - -// CURSOR resolves the cursor for pagination -func (u *UserPetEdge) CURSOR(ctx context.Context) graphql.ID { - return u.cursor -} - -// NODE resolves the node for pagination -func (u *UserPetEdge) NODE(ctx context.Context) *Pet { - return &u.node -} - -// PageInfo gives page info for pagination -type PageInfo struct { - StartCursor graphql.ID - EndCursor graphql.ID - HasNextPage bool - HasPreviousPage bool + return p, nil } -// STARTCURSOR ... -func (u *PageInfo) STARTCURSOR(ctx context.Context) *graphql.ID { - return &u.StartCursor +// UserResolver contains the database and the user model to resolve against +type UserResolver struct { + db *DB + m User } -// ENDCURSOR ... -func (u *PageInfo) ENDCURSOR(ctx context.Context) *graphql.ID { - return &u.EndCursor -} - -// HASNEXTPAGE returns true if there are more results to show -func (u *PageInfo) HASNEXTPAGE(ctx context.Context) bool { - return u.HasNextPage -} - -// HASPREVIOUSPAGE returns true if there are results behind the current cursor position -func (u *PageInfo) HASPREVIOUSPAGE(ctx context.Context) bool { - return u.HasPreviousPage -} - -// UserPetsConnection is all the pets that are connected to a certain user -type UserPetsConnection struct { - ids []int - from int - to int +// ID resolves the user ID +func (u *UserResolver) ID(ctx context.Context) *graphql.ID { + return gqlIDP(u.m.ID) } -// TOTALCOUNT gives the total amount of pets in UserPetsConnection -func (u UserPetsConnection) TOTALCOUNT(ctx context.Context) int32 { - return int32(len(u.ids)) +// Name resolves the Name field for User, it is all caps to avoid name clashes +func (u *UserResolver) Name(ctx context.Context) *string { + return &u.m.Name } -// EDGES gives a list of all the edges (related pets) that belong to a user -func (u *UserPetsConnection) EDGES(ctx context.Context) (*[]*UserPetEdge, error) { - // query goes here because I know all of the ids that are needed. If I queried in the - // UserPetEdge resolver method, it would run multiple single queries - pets, err := db.getPetsByID(ctx, u.ids, u.from, u.to) +// Pets resolves the Pets field for User +func (u *UserResolver) Pets(ctx context.Context) (*[]*PetResolver, error) { + pets, err := u.db.GetUserPets(ctx, u.m.ID) if err != nil { - return nil, err + return nil, errors.Wrap(err, "Pets") } - l := make([]*UserPetEdge, u.to-u.from) - for i := range l { - l[i] = &UserPetEdge{ - cursor: encodeCursor(u.from + i), - node: pets[i], + r := make([]*PetResolver, len(pets)) + for i := range pets { + r[i] = &PetResolver{ + db: u.db, + m: pets[i], } } - return &l, nil -} - -// PAGEINFO resolves page info -func (u *UserPetsConnection) PAGEINFO(ctx context.Context) (*PageInfo, error) { - p := PageInfo{ - StartCursor: encodeCursor(u.from), - EndCursor: encodeCursor(u.to - 1), - HasNextPage: u.to < len(u.ids), - HasPreviousPage: u.from > 0, - } - return &p, nil + return &r, nil }