Shall I create resolver for each query or for all of its fields in GraphQL? - go

I'm interested in using GraphQL and I've just started experimenting with it.
In a GraphQL tutorial, one can see the following quote:
Each field in a GraphQL schema is backed by a resolver.
But if you look at gqlgen (which is a golang library for building GraphQL servers) todo example that uses the following schema:
...
type MyQuery {
todo(id: ID!): Todo
lastTodo: Todo
todos: [Todo!]!
}
type MyMutation {
createTodo(todo: TodoInput!): Todo!
updateTodo(id: ID!, changes: Map!): Todo
}
type Todo {
id: ID!
text: String!
done: Boolean!
}
...
it actually uses 3 autogenerated resolvers (i.e., 1 for each query, not a field):
func (r *QueryResolver) Todo(ctx context.Context, id int) (*Todo, error) {
func (r *QueryResolver) LastTodo(ctx context.Context) (*Todo, error) {
func (r *QueryResolver) Todos(ctx context.Context) ([]*Todo, error) {
Is it the expected behavior not to autogenerate resolvers for each field (but for each query instead)?

When entire type is backed by a resolver then it's still true that each field is backed by resolver ;)
Comparing to SQL - usually you don't ask for each (single) field separately when you want entire record/row.
Field level resolvers are needed when type resolver don't return data for all required (by query) fields - f.e. for relations.

Related

Golang gqlgen, fetching separate fields in a single DB query via Dataloading

I am trying to see if its possible to take several fields that have their own resolver and somehow get them in a single DB query since they are fetched from the same place.
Eg. the schema
type ExampleResposne {
A: Int!
B: Int!
C: Int!
}
The gqlgen.yml
models:
ExampleResponse:
fields:
A:
resolver: true
B:
resolver: true
C:
resolver: true
And now somehow I'd like to fetch A and B from a same DB call if they are requested at the same time
I have an API that returns these based on the keys
exampleRes, err := resolver.service.GetExample(ctx, GetExampleRequest{
Keys: []ExampleKey{
AKey,
BKey,
},
})
Is something like this possible or should I rethink my approach?
The generated resolvers
func (r *exampleResponseResolver) A(ctx context.Context, obj *model.ExampleResponse) (int, error) { panic(fmt.Errorf("not implemented"))
}
func (r *exampleResponseResolver) B(ctx context.Context, obj *model.ExampleResponse) (int, error) { panic(fmt.Errorf("not implemented"))
}
func (r *exampleResponseResolver) C(ctx context.Context, obj *model.ExampleResponse) (int, error) { panic(fmt.Errorf("not implemented"))
}
It seems you may have some topics confused:
Dataloading
The concept of dataloading is to batch multiple requests for data under one resolver's function / method call. This should be considered at the level of the resolving type and not at the level of individual fields of that type.
An example of this (taken from https://gqlgen.com/reference/dataloaders/) would be to resolve for multiple Users under one database request.
Graphql Field Resolvers
With your gqlgen.yml configuration, you have specified individual resolvers for fields with:
models:
ExampleResponse:
fields:
A:
resolver: true
...
The concept of field resolvers aim to resolve individual fields of a type rather than to resolve the type as a whole, essentially having the opposite effect to what dataloading has. This is useful when we are trying to resolve argumented fields or code splitting resolver logic.
To note, field resolvers can often cause N+1 problems when not used effectively.
The Solution
You might not be seeing a resolver for your type ExampleResponse because you need to append your queries entry-point under the Query type.
schema.graphql:
type Query {
getExample: ExampleResposne
}
Then run: go run github.com/99designs/gqlgen generate
You should see:
func (r *queryResolver) GetExample(ctx context.Context) (*model.ExampleResponse, error) {
panic(fmt.Errorf("not implemented"))
}
See https://graphql.org/learn/schema/#the-query-and-mutation-types for details.

Pattern for writing graphql schema with gorm decorator?

I am writing graphql schema that need to work with gorm, my understanding is that graphql can't inherently use gorm decorator, so to make it work, I wrote a separate Output type:
Let's say I have this table ORM:
type Character struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"unique" json:"name"`
CliqueType string `json:"clique"`
IsHero bool `json:"hero"`
}
This is hardcoded and put into a model.go file. The gorm decorator works here as this is native go file.
In the schema.graphql I have this instead:
type CharacterOutput {
id: ID!
name: String!
isHero: Boolean!
cliqueType: CliqueType!
}
input CharacterInput {
name: String!
isHero: Boolean
cliqueType: CliqueType
}
As you can see, I have both a input and output graphql schema definition. This allow graphql to generate the required type without conflicting with gorm.
The downside of this is that I need to write a separate manual mapper between the 2 type, which kind of defeat the purpose of a ORM:
func mapChar2Output(character *model.Character) *model.CharacterOutput {
output := model.CharacterOutput{
ID: strconv.FormatUint(uint64(character.ID), 10),
Name: character.Name,
CliqueType: model.CliqueType(character.CliqueType),
IsHero: character.IsHero,
}
return &output
}
func mapInputToChar(CharacterInput *model.CharacterInput, Character *model.Character) {
Character.Name = CharacterInput.Name
Character.IsHero = *CharacterInput.IsHero
Character.CliqueType = CharacterInput.CliqueType.String()
}
My graphql resolver look like this, notice I call the mapper after getting the result from repo:
// CreateCharacter is the resolver for the createCharacter field.
func (r *mutationResolver) CreateCharacter(ctx context.Context, input model.CharacterInput) (*model.CharacterOutput, error) {
result, err := r.Resolver.CharacterRepository.CreateCharacter(&input)
output := mapChar2Output(result)
return output, err
}
My database CRUD function call the input mapper:
func (b *CharacterService) CreateCharacter(CharacterInput *model.CharacterInput) (*model.Character, error) {
character := &model.Character{}
mapInputToChar(CharacterInput, character)
err := b.Db.Create(&character).Error
return character, err
}
What is a better way of designing this schema so graphql can work with gorm without needing to write any extra mapper?

Overcome repetition in graphql

Suppose my graphql API allows me to manage ModelA, ModelB, ModelC.
Those models have very simple typeDefs:
type ModelA {
id: ID!
tags: [SomeComplexTagType!]
}
type ModelB {
id: ID!
tags: [SomeComplexTagType!]
}
type ModelC {
id: ID!
tags: [SomeComplexTagType!]
}
In order for users of the API to add tags to ModelA, ModelB, ModelC the following mutations are provided:
type Mutation {
addTagToModelA(id: ID!, tag: SomeComplexTagType!): Boolean
addTagToModelB(id: ID!, tag: SomeComplexTagType!): Boolean
addTagToModelC(id: ID!, tag: SomeComplexTagType!): Boolean
}
What is the graphql recommended way to get rid of the repetive nature in this API design?
As far as I understand mutations are always top level elements of the Mutation type in graphql. That means, my resolver function for addTagToModelX will never be passed an instance of ModelX as its parent, i.e. something like this will never work:
type Mutation {
ModelA(id: ID!) {
addTagToX(tag: SomeComplexTagType!): Boolean
}
ModelB(id: ID!) {
addTagToX(tag: SomeComplexTagType!): Boolean
}
ModelC(id: ID!) {
addTagToX(tag: SomeComplexTagType!): Boolean
}
}
I found a good solution while digging into the GitHub API.
Summary:
They use global node ids which allow them to conclude the object type from the ID, because the ID is base64 encoded and contains that information.
For my scenario this would mean:
2.1) Make ModelA, ModelB, ModelC implement Taggable
2.2) Introduce mutation addTagToTaggable accepting an ID from Taggable
2.3) In the resolver extract the object type from ID, query object from database, add Tag etc.
2.4) Profit

too many parameters returned by Resolver.ModuleName

I am working on breaking the monolithic architecture into Microservice architecture.
I did that but when I am building the code in my current repository I am getting this error.
We use graphql-gophers library
panic: too many parameters returned by (Resolver).Dummy
Has anyone ever seen this error in golang using graphql for querying?
Tried so many things but nothing has worked.
Any help would be appreciated
The error message comes from graph-gophers/graphql-go internal/exec/resolvable/resolvable.go#makeFieldExec
It is called when you parse a schema which does not match the field of an existing struct.
The one illustrated in example/customerrors/starwars.go does match every field and would not trigger the error message:
var Schema = `
schema {
query: Query
}
type Query {
droid(id: ID!): Droid!
}
# An autonomous mechanical character in the Star Wars universe
type Droid {
# The ID of the droid
id: ID!
# What others call this droid
name: String!
}
`
type droid struct {
ID graphql.ID
Name string
}
Its resolver does use the right parameters:
type Resolver struct{}
func (r *Resolver) Droid(args struct{ ID graphql.ID }) (*droidResolver, error) {
if d := droidData[args.ID]; d != nil {
return &droidResolver{d: d}, nil
}
return nil, &droidNotFoundError{Code: "NotFound", Message: "This is not the droid you are looking for"}
}
Try and use that example to check it does work, then modify it to transition to your own code.

GraphQL nested mutations

I cannot find some example of a schema where the mutations are nested in it's own resolver, to group some actions in a sub-mutations object.
Something like this:
schema {
query: Query
mutation: RootMutation
}
type RootMutation {
user: UserMutation
post: PostMutation
}
type UserMutation {
create(user: UserInput!): User
delete(id: ID!): Bool
}
type PostMutation {
create(user: PostInput!): Post
delete(id: ID!): Bool
}
And the query would be like this:
mutation CreateUser($u: UserInput!) {
user {
create($u) {
id
}
}
}
I think that, this approach is completely ok with the specification. So why does everybody design the mutations as a first class function? Like so:
type RootMutation {
createUser(user: UserInput!): User
deleteUser(id: ID!): Bool
createPost(user: UserInput!): User
deletePost(id: ID!): Bool
}
When you got a lot of actions to make on your huge system, then the RootMutation will have just an endless list of similar functions (like create, delete and so on).
And isn't this a thing in REST? Where every function is it's own endpoint? But there it's better because you have the HTTP methods. So you would have an /user endpoint with a POST and a DELETE method and so on.

Resources