Pattern for writing graphql schema with gorm decorator? - go

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?

Related

Gorm create and return value many2many

I want to create a data and then return the value, but the value is related to another table.
User response struct
type UserRespone struct {
ID int `json:"id,omitempty"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Address string `json:"address"`
Roles []roles.Roles `json:"roles" gorm:"many2many:users_roles;foreignKey:ID;joinForeignKey:UserID;references:ID;joinReferences:RolesID"`
}
Roles struct
type Roles struct {
ID int `json:"id" gorm:"<-:false;primaryKey"`
Name string
}
And this is a function to create a data, and I want to return the value [ ]roles.Roles
func (r *UserRepositoryImpl) AuthSignUp(ctx context.Context, req user.AuthSignUp) (user.UserRespone, error) {
createdUser := req.ToUser()
hashPassword, _ := bcrypt.GenerateFromPassword([]byte(createdUser.Password), bcrypt.DefaultCost)
createdUser.Password = string(hashPassword[:])
result := r.Db.
WithContext(ctx).
Create(&createdUser)
for _, rolesID := range req.RolesID {
userRole := new(user.UsersRoles)
userRole.UserID = createdUser.ID
userRole.RolesID = rolesID
result = r.Db.WithContext(ctx).Create(&userRole)
}
return createdUser.ToResponse(), result.Error
}
I want to return the value like this:
user.UserResponse{
ID: 4,
Email: "putri4#gmail.com",
FirstName: "putri",
LastName: "cantik",
Address: "bonang",
Roles: []roles.Roles{
{
ID: 1,
Name: "Admin",
},
{
ID: 2,
Name: "Consumer",
},
},
}
But when I create a data set, I only get values like :
user.UserRespones{
ID: 4,
Email: "putri4#gmail.com",
FirstName: "putri",
LastName: "cantik",
Address: "bonang",
Roles: []roles.Roles(nil),
}
As far as I can tell, you already have the user data and the role ids. I figure you just want to get the role names as well:
err := r.db.Find(&createdUser.Roles, req.RolesID)
// err handling
That said, your types and names are a bit unclear. UserRespone (note the typo) should probably named like DBUser - it isn't important whether it is used as response or something else, it represents the database entry for an user, right?
Additionally, I can only make assumptions about the type and fields of createdUser and UsersRoles, so I might have missed something.
I figure createdUser is of type UserRespone - but then I would expect that Roles is already complete (and you don't have to query anything at all). If not, then you should introduce a new type for that, e.g. RequestUser (as opposed to DBUser), which only contains Role ids but not Role names. Squeezing the data from the request, the db entry and the response data into the same type is confusing (and a tad too tightly coupled for my taste).

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.

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.

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.

Determine if POST data value matches struct field type

Using the gin framework I am trying to determine if POST'ed data does not match the struct field type and inform API user of their error.
type CreateApp struct {
LearnMoreImage string `db:"learn_more_image" json:"learn_more_image,omitempty" valid:"string,omitempty"`
ApiVersion int64 `db:"api_version" json:"api_version" valid:"int,omitempty"`
}
...
func CreateApps(c *gin.Context) {
var json models.CreateApp
c.Bind(&json)
So when I POST
curl -H "Content-Type: application/json" -d '{"learn_more_image":"someimage.jpg","api_version":"somestring"}' "http://127.0.0.1:8080/v1.0/apps"
I would like to determine whether the POST'ed data for field 'api_version' (passed as string) does not match the struct field it is being binded to (int). If the data doesnt match I'd like to send a message back to the user. Its for this reason I was hoping I could loop through the gin contexts data and check it.
The gin function 'c.Bind()' seems to omit invalid data and all subsequent data fields with it.
Gin has a built-in validation engine: https://github.com/bluesuncorp/validator/blob/v5/baked_in.go
but you can use your own or disable it completely.
The validator does not validate the wire data (json string), instead it validates the binded struct:
LearnMoreImage string `db:"learn_more_image" json:"learn_more_image,omitempty" binding:"required"`
ApiVersion int64 `db:"api_version" json:"api_version" binding:"required,min=1"`
Notice this: binding:"required,min=1"
Then:
err := c.Bind(&json)
or use a middleware and read c.Errors.
UPDATED:
Three workarounds:
Validate the json string your own (it can not be done with enconding/json)
Validate if integer is > 0 binding:"min=1"
Use a map[string]interface{} instead of a Struct, then validate the type.
func endpoint(c *gin.Context) {
var json map[string]interface{}
c.Bind(&json)
struct, ok := validateCreateApp(json)
if ok { /** DO SOMETHING */ }
}
func validateCreateApp(json map[string]interface{}) (CreateApp, bool) {
learn_more_image, ok := json["learn_more_image"].(string)
if !ok {
return CreateApp{}, false
}
api_version, ok = json["api_version"].(int)
if !ok {
return CreateApp{}, false
}
return CreateApp{
learn_more_image, api_version,
}
}

Resources