playlyfe / go-graphql

A powerful GraphQL server implementation for Golang

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Resolver Api with dynamic argument parsing

fungl164 opened this issue · comments

Was wondering if there is any appetite for enhancing the resolver api by enabling dynamic argument parsing for faster/safer coding.

Below is a rough (but working) example of how it could work...aided by some minimal external packages I quickly pulled together...

Any comments? Takers?

package main

import (
	"errors"
	"net/http"

	handler "github.com/krypton97/HandleGraphQL"
	router "github.com/luisjakon/playlyfe-router"
	graphiql "github.com/luisjakon/graphiql"
	graphql "github.com/playlyfe/go-graphql"
)

var (
	Salutations = map[string]string{
		"ro": "Salut",
		"en": "Hello",
		"fr": "Oui",
		"it": "Ciao",
		"de": "Hallo",
	}

	Students = map[string]map[string]interface{}{
		"1": {"__typename": "Student", "name": "alex", "age": 18, "id": "1"},
		"2": {"__typename": "Student", "name": "bob", "age": 19, "id": "2"},
		"3": {"__typename": "Student", "name": "john", "age": 20, "id": "3"},
	}
)

func main() {

	schema := `
type Student {
	name: String
	age: Int
}

type Query {
	hello: String
	salutation(lang: String!): String
	student(id: String!): Student
}	
`
	router := router.NewRouter()

	router.Register("Query/hello", QueryResolver.Hello)
	router.Register("Query/salutation", QueryResolver.Salutation)
	router.Register("Query/student", QueryResolver.StudentById)
	router.Register("Student/age", StudentResolver.Age)

	executor, err := graphql.NewExecutor(schema, "Query", "", router)
	if err != nil {
		panic(err)
	}

	api := handler.New(&handler.Config{
		Executor: executor,
		Context:  "",
		Pretty:   true,
	})

	http.Handle("/graphql", graphiql.Handler(api))
	http.ListenAndServe("localhost:3000", nil)
}

resolvers.go

// Resolvers
var (
	QueryResolver   = queryRes{}
	StudentResolver = studentRes{}
)

// Query Resolver
type queryRes struct{}

func (r queryRes) Hello(params *graphql.ResolveParams) (interface{}, error) {
	return "world", nil
}

func (r queryRes) Salutation(params *graphql.ResolveParams, args struct { Lang string }) (interface{}, error) {
	s := Salutations[args.Lang]
	if s == "" {
		return nil, errors.New("Unknown Language: " + args.Lang)
	}
	return s, nil
}

func (r queryRes) StudentById(params *graphql.ResolveParams, args struct { Id string }) (interface{}, error) {
	return Students[args.Id], nil
}

// Student Resolver
type studentRes struct{}

func (r studentRes) Age(params *graphql.ResolveParams) (interface{}, error) {
	if v, ok := params.Source.(int); ok {
		return v, nil
	}
	source := params.Source.(map[string]interface{})
	id := source["id"].(string)
	return Students[id]["age"].(int), nil
}

This is a very interesting approach, but I'm not sure if it's the best way to do this and if we should include it directly within the go-graphql library.

I personally use a slightly different approach to handle this using a simple Map struct with functions for parsing each key into a specific type of value.

type Map map[string]interface{}

// String returns the value at 'key' as a string
func (m Map) String(key string, defaultValue ...string) string {
	if len(defaultValue) == 0 {
		return m[key].(string)
	}
	value, ok := m[key].(string)
	if !ok {
		value = defaultValue[0]
	}
	return value
}

// Uint32 returns the value at 'key' as a uint32
func (m Map) Uint32(key string, defaultValue ...uint32) uint32 {
	if len(defaultValue) == 0 {
		return m[key].(uint32)
	}
	value, ok := m[key].(uint32)
	if !ok {
		value = defaultValue[0]
	}
	return value
}

resolvers["Example/resolver"] = func(params *graphql.ResolveParams) (interface{}, error) {
		tx := helpers.LoadTransaction(params)
		args := model.Map(params.Args)
                println(args.String("key1")) // Fetch the value of argument 'key1' as a string, will crash on nil assertion if the argument is not passed
                println(args.Uint32("key2", uint32(42))) // Fetch value of argument 'key2' as an uint32. If it does not exist use the value 42
                return nil, nil
}

The biggest issue I've had with my approach is that it will crash while trying to access a nil value if you don't pass an input argument and haven't mentioned a default value.

The main reason I don't want to include it in the library is that it needs to include a way to parse all the basic types you could read out from the input.

One disadvantage with your approach would be that you can't specify default values easily without verbose if checks for each field.

@atrniv Thnxs for the response, perhaps for now it is better to leave as an external package.

Am sure the basic resolver argument-parsing mechanism I'm using in the github.com/luisjakon/playlyfe-router package can be improved and/or generalized a bit more to include hooks for additional validations using external packages and/or other user-defined functions as needed .

I'm happy with any thoughts and/or contributions anyone provides. : )