Skip to content

Add Pointer support to GraphQL #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ var _ Named = (*Interface)(nil)
var _ Named = (*Union)(nil)
var _ Named = (*Enum)(nil)
var _ Named = (*InputObject)(nil)
var _ Named = (*Pointer)(nil)

func GetNamed(ttype Type) Named {
unmodifiedType := ttype
Expand Down Expand Up @@ -1304,6 +1305,58 @@ func (gl *NonNull) Error() error {
return gl.err
}

// Pointer represents just that, a pointer to a value of another type. Pointers
// are specific to langauges that support them, such as Go.
type Pointer struct {
OfType Type `json:"ofType"`

err error
}

// NewPointer takes the given type and constructs a GraphQL pointer for that
// type.
func NewPointer(t Type) *Pointer {
p := &Pointer{}

err := invariant(t != nil, fmt.Sprintf("Can only create Pointer of a Type but got: %v.", t))
if err != nil {
p.err = err

return p
}

p.OfType = t

return p
}

// Name returns a GraphQL 'type name' that should represent what piece of data
// is defined.
func (p *Pointer) Name() string {
return fmt.Sprintf("*%v", p.OfType)
}

// Description is empty at the moment.
func (p *Pointer) Description() string {
return ""
}

// String returns the Name of the pointer, also defined to fit *Pointer to
// the fmt.Stringer interface.
func (p *Pointer) String() string {
if p.OfType != nil {
return p.Name()
}

return ""
}

// Error returns an error value that may be attached to the Pointer value from
// previous encounters.
func (p *Pointer) Error() error {
return p.err
}

var NAME_REGEXP, _ = regexp.Compile("^[_a-zA-Z][_a-zA-Z0-9]*$")

func assertValidName(name string) error {
Expand Down
31 changes: 31 additions & 0 deletions definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ var blogArticle = graphql.NewObject(graphql.ObjectConfig{
"body": &graphql.Field{
Type: graphql.String,
},
"comments": &graphql.Field{
Type: graphql.NewPointer(graphql.NewList(blogComment)),
},
},
})

var blogComment = graphql.NewObject(graphql.ObjectConfig{
Name: "Comment",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewPointer(graphql.String),
},
"body": &graphql.Field{
Type: graphql.NewPointer(graphql.String),
},
},
})

Expand Down Expand Up @@ -203,6 +218,19 @@ func TestTypeSystem_DefinitionExample_DefinesAQueryOnlySchema(t *testing.T) {
if feedField.Name != "feed" {
t.Fatalf("feedField.Name expected to equal `feed`, got: %v", feedField.Name)
}

commentField := articleFieldTypeObject.Fields()["comments"]
commentFieldPtr, ok := commentField.Type.(*graphql.Pointer)
if !ok {
t.Fatalf("expected commentFieldPtr to be a Pointer, got: %v", commentField)
}
commentFieldPtrList, ok := commentFieldPtr.OfType.(*graphql.List)
if !ok {
t.Fatalf("expected commentFieldPtrList to be a List, got: %v", commentFieldPtrList)
}
if commentFieldPtrList.OfType != blogComment {
t.Fatalf("commentFieldPtrList.OfType expected to equal blogComment, got: %v", commentFieldPtrList.OfType)
}
}
func TestTypeSystem_DefinitionExample_DefinesAMutationScheme(t *testing.T) {
blogSchema, err := graphql.NewSchema(graphql.SchemaConfig{
Expand Down Expand Up @@ -377,6 +405,9 @@ func TestTypeSystem_DefinitionExample_StringifiesSimpleTypes(t *testing.T) {
Test{graphql.NewNonNull(graphql.NewList(graphql.Int)), "[Int]!"},
Test{graphql.NewList(graphql.NewNonNull(graphql.Int)), "[Int!]"},
Test{graphql.NewList(graphql.NewList(graphql.Int)), "[[Int]]"},
Test{graphql.NewPointer(graphql.Int), "*Int"},
Test{graphql.NewPointer(graphql.NewList(graphql.Int)), "*[Int]"},
Test{graphql.NewNonNull(graphql.NewPointer(graphql.Int)), "*Int!"},
}
for _, test := range tests {
ttypeStr := fmt.Sprintf("%v", test.ttype)
Expand Down
18 changes: 16 additions & 2 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,10 @@ func doesFragmentConditionMatch(eCtx *ExecutionContext, fragment ast.Node, ttype
if conditionalType == ttype {
return true
}
if conditionalType.Name() == ttype.Name() {
if conditionalType.Name() == ttype.Name() {
return true
}

if conditionalType, ok := conditionalType.(Abstract); ok {
return conditionalType.IsPossibleType(ttype)
}
Expand Down Expand Up @@ -588,6 +588,20 @@ func completeValue(eCtx *ExecutionContext, returnType Type, fieldASTs []*ast.Fie
return nil
}

// If field type is Pointer, pull value from pointer and pass it along
if returnType, ok := returnType.(*Pointer); ok {
resultVal := reflect.ValueOf(result)
err := invariant(
resultVal.IsValid() && resultVal.Type().Kind() == reflect.Ptr,
"User Error: expected pointer, but did not find one.",
)
if err != nil {
panic(gqlerrors.FormatError(err))
}

return completeValue(eCtx, returnType.OfType, fieldASTs, info, resultVal.Elem().Interface())
}

// If field type is List, complete each item in the list with the inner type
if returnType, ok := returnType.(*List); ok {

Expand Down
55 changes: 55 additions & 0 deletions executor_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ type testAuthor struct {
Pic testPicFn `json:"pic"`
RecentArticle *testArticle `json:"recentArticle"`
}
type testComment struct {
Id *int `json:"id"`
Body *string `json:"body"`
}
type testArticle struct {
Id string `json:"id"`
IsPublished string `json:"isPublished"`
Expand All @@ -36,6 +40,7 @@ type testArticle struct {
Body string `json:"body"`
Hidden string `json:"hidden"`
Keywords []interface{} `json:"keywords"`
Comments []testComment `json:"comments"`
}

func getPic(id int, width, height string) *testPic {
Expand All @@ -48,6 +53,14 @@ func getPic(id int, width, height string) *testPic {

var johnSmith *testAuthor

func intToPtr(i int) *int {
return &i
}

func stringToPtr(s string) *string {
return &s
}

func article(id interface{}) *testArticle {
return &testArticle{
Id: fmt.Sprintf("%v", id),
Expand All @@ -59,6 +72,10 @@ func article(id interface{}) *testArticle {
Keywords: []interface{}{
"foo", "bar", 1, true, nil,
},
Comments: []testComment{
{intToPtr(10), stringToPtr("This is a comment body")},
{intToPtr(20), stringToPtr("This is another comment body")},
},
}
}

Expand Down Expand Up @@ -118,6 +135,17 @@ func TestExecutesUsingAComplexSchema(t *testing.T) {
"recentArticle": &graphql.Field{},
},
})
blogComment := graphql.NewObject(graphql.ObjectConfig{
Name: "Comment",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewPointer(graphql.Int),
},
"body": &graphql.Field{
Type: graphql.NewPointer(graphql.String),
},
},
})
blogArticle := graphql.NewObject(graphql.ObjectConfig{
Name: "Article",
Fields: graphql.Fields{
Expand All @@ -139,6 +167,9 @@ func TestExecutesUsingAComplexSchema(t *testing.T) {
"keywords": &graphql.Field{
Type: graphql.NewList(graphql.String),
},
"comments": &graphql.Field{
Type: graphql.NewList(blogComment),
},
},
})

Expand Down Expand Up @@ -219,6 +250,10 @@ func TestExecutesUsingAComplexSchema(t *testing.T) {
body,
hidden,
notdefined
comments {
id,
body
}
}
`

Expand Down Expand Up @@ -247,6 +282,26 @@ func TestExecutesUsingAComplexSchema(t *testing.T) {
"true",
nil,
},
"comments": []interface{}{
map[string]interface{}{
"id": 10,
"body": "This is a comment body",
},
map[string]interface{}{
"id": 20,
"body": "This is another comment body",
},
},
},
},
"comments": []interface{}{
map[string]interface{}{
"id": 10,
"body": "This is a comment body",
},
map[string]interface{}{
"id": 20,
"body": "This is another comment body",
},
},
"id": "1",
Expand Down