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

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.

Related

How do I solve SQL Scan error on column in Go?

Technologies used
Go
Gorm
PostgreSQL 14.5 (In Docker container)
OpenAPI
oapi-codegen package v1.11.0
I am building an API for CRUD operations on Personas from the Shin Megami Tensei Persona spin-off series of games. I have an issue when trying to fetch data from my database using the technologies above.
Error message
sql: Scan error on column index 0, name "arcana_id": unsupported Scan, storing driver.Value type string into type *api.ArcanaID
I think the issue is that when retrieving the data it is trying to store a string inside of a *api.ArcanaID.
How can I adjust my data model so that I can pull a UUID from my DB?
I have looked at this question and it did not solve my issue because it is dealing with nil values.
I have tried changing the type of the ArcanaID from string to uuid.UUID with no success. Same error message.
Data Model - openapi.yaml
components:
schemas:
P5Arcana:
type: object
required:
- ArcanaID
properties:
ArcanaID:
$ref: "#/components/schemas/ArcanaID"
ArcanaID:
description: A universally unique identifier for identifying one of the 22 Major Arcana.
type: string
x-go-type: uuid.UUID
x-go-type-import:
path: github.com/google/uuid
x-oapi-codegen-extra-tags:
gorm: "primaryKey;unique;type:uuid;default:uuid_generate_v4()"
interface interface.go
packages databases
import (
"context"
"github.com/bradleyGamiMarques/PersonaGrimoire/api
)
type PersonaGrimoire interface {
GetPersona5ArcanaByUUID(ctx context.Context, arcanaUUID api.ArcanaID) (arcana api.P5Arcana, err error)
}
interfaceimpl interfaceimpl.go
packages databases
import (
"context"
"errors"
"fmt"
"github.com/bradleyGamiMarques/PersonaGrimoire/api"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type PersonaGrimoireImpl struct {
Gorm *gorm.DB
Logger *logrus.Logger
}
func (p *PersonaGrimoireImpl) GetPersona5ArcanaByUUID(ctx context.Context, arcanaUUID api.ArcanaID) (arcana api.P5Arcana, err error) {
err = p.Gorm.WithContext(ctx).Model(&api.P5Arcana{ArcanaID: arcana.ArcanaID}).Where(&api.P5Arcana{ArcanaID: arcanaUUID}).First(&arcana).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
p.Logger.Warnf("Attempted to get Persona 5 Arcana by ID that does not exist. Error: %s", err.Error())
return api.P5Arcana{}, fmt.Errorf("attempted to get Persona 5 Arcana by ID that does not exist Error: %w", err)
}
}
return arcana, nil
}
Implementation code
// Check if ID exists
// Calls GetPersona5ArcanaByUUID()
// Return result
Thank you to Jamie Tanna at https://www.jvt.me/posts/2022/07/12/go-openapi-server/.
Their solution involved not using the github.com/google/uuid package and instead used the openapi_types.UUID type.
This was done by defining the schema as such.
ArcanaID:
description: A universally unique identifier for identifying one of the 22 Major Arcana.
type: string
format: uuid
pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}"
x-oapi-codegen-extra-tags:
gorm: "type:uuid;primaryKey"
This results in generated code that looks like
// ArcanaID A universally unique identifier for identifying one of the 22 Major Arcana.
type ArcanaID = openapi_types.UUID

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?

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.

Passing nested JSON as variable in Machinebox GraphQL mutation using golang

Hi there Golang experts,
I am using the Machinebox "github.com/machinebox/graphql" library in golang as client for my GraphQL server.
Mutations with single layer JSON variables work just fine
I am, however, at a loss as to how to pass a nested JSON as a variable
With a single layer JSON I simply create a map[string]string type and pass into the Var method. This in turn populates my graphql $data variable
The machinebox (graphql.Request).Var method takes an empty interface{} as value so the map[string]string works fine. But embedded json simply throws an error.
code:
func Mutate(data map[string]string, mutation string) interface{} {
client := GQLClient()
graphqlRequest := graphql.NewRequest(mutation)
graphqlRequest.Var("data", data)
var graphqlResponse interface{}
if err := client.Run(context.Background(), graphqlRequest, &graphqlResponse); err != nil {
panic(err)
}
return graphqlResponse
}
Mutation:
mutation createWfLog($data: WfLogCreateInput)
{
createWfLog (data: $data){
taskGUID {
id
status
notes
}
event
log
createdBy
}
}
data variable shape:
{
"data": {
"event": "Task Create",
"taskGUID": {
"connect": {"id": "606f46cdbbe767001a3b4707"}
},
"log": "my log and information",
"createdBy": "calvin cheng"
}
}
As mentioned, the embedded json (value of taskGUID) presents the problem. If value was simple string type, it's not an issue.
Have tried using a struct to define every nesting, passed in struct.
Have tried unmarshaling a struct to json. Same error.
Any help appreciated
Calvin
I have figured it out... and it is a case of my noobness with Golang.
I didn't need to do all this conversion of data or any such crazy type conversions. For some reason I got in my head everything HAD to be a map for the machinebox Var(key, value) to work
thanks to xarantolus's referenced site I was able to construct a proper strut. I populated the strut with my variable data (which was a nested json) and the mutation ran perfectly!
thanks!

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

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.

Resources