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 }