Skip to content

Commit

Permalink
Fix introspect's AST to allow queries of object fragments on interfaces
Browse files Browse the repository at this point in the history
Fixes #37
  • Loading branch information
JohnStarich committed Nov 18, 2023
1 parent b7f97e0 commit 14a9583
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 2 deletions.
16 changes: 14 additions & 2 deletions introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ func IntrospectAPI(queryer Queryer, opts ...*IntrospectOptions) (*ast.Schema, er

// make sure we record that a type implements itself
schema.AddImplements(remoteType.Name, storedType)
if storedType.Kind == ast.Object {
addPossibleTypeOnce(schema, remoteType.Name, storedType) // When evaluating matching fragments, Objects count as a possible type for themselves.
}

// if we are looking at an enum
if len(remoteType.PossibleTypes) > 0 {
Expand Down Expand Up @@ -235,7 +238,7 @@ func IntrospectAPI(queryer Queryer, opts ...*IntrospectOptions) (*ast.Schema, er
}

// add the possible type to the schema
schema.AddPossibleType(remoteType.Name, possibleTypeDef)
addPossibleTypeOnce(schema, remoteType.Name, possibleTypeDef)
schema.AddImplements(possibleType.Name, storedType)
}
}
Expand All @@ -258,7 +261,7 @@ func IntrospectAPI(queryer Queryer, opts ...*IntrospectOptions) (*ast.Schema, er
}

// add the possible type to the schema
schema.AddPossibleType(iFaceDef.Name, storedType)
addPossibleTypeOnce(schema, iFaceDef.Name, storedType)
schema.AddImplements(storedType.Name, iFaceDef)
}
}
Expand Down Expand Up @@ -320,6 +323,15 @@ func IntrospectAPI(queryer Queryer, opts ...*IntrospectOptions) (*ast.Schema, er
return schema, nil
}

func addPossibleTypeOnce(schema *ast.Schema, name string, definition *ast.Definition) {
for _, typ := range schema.PossibleTypes[name] {
if typ.Name == definition.Name {
return
}
}
schema.AddPossibleType(name, definition)
}

func introspectionConvertArgList(args []IntrospectionInputValue) ast.ArgumentDefinitionList {
result := ast.ArgumentDefinitionList{}

Expand Down
153 changes: 153 additions & 0 deletions introspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ func TestIntrospectAPI_union(t *testing.T) {
expectSubtype1,
expectSubtype2,
},
"Subtype1": {
expectSubtype1,
},
"Subtype2": {
expectSubtype2,
},
},
Implements: map[string][]*ast.Definition{
"Subtype1": {expectSubtype1, expectTypeA},
Expand Down Expand Up @@ -1365,3 +1371,150 @@ func TestIntrospectAPI_valid_interface_implementation(t *testing.T) {
_, err = gqlparser.LoadSchema(&ast.Source{Name: "input.graphql", Input: schemaBuffer.String()})
assert.Nil(t, err, "Type should implement an interface without formatting/re-parsing failures.")
}

// Tests fix for https://github.com/nautilus/graphql/issues/37
func TestIntrospectAPI_spread_fragment_on_interface(t *testing.T) {
t.Parallel()
schema, err := IntrospectAPI(&mockJSONQueryer{
JSONResult: `{
"__schema": {
"queryType": {
"name": "Query"
},
"types": [
{
"description": null,
"enumValues": [],
"fields": [
{
"args": [
{
"defaultValue": null,
"description": null,
"name": "id",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
}
],
"deprecationReason": null,
"description": "Find a Node for the given ID. Use fragments to select additional fields.",
"isDeprecated": false,
"name": "node",
"type": {
"kind": "INTERFACE",
"name": "Node",
"ofType": null
}
}
],
"inputFields": [],
"interfaces": [],
"kind": "OBJECT",
"name": "Query",
"possibleTypes": []
},
{
"description": "A resource.",
"enumValues": [],
"fields": [
{
"args": [],
"deprecationReason": null,
"description": "The ID of this resource.",
"isDeprecated": false,
"name": "id",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
}
],
"inputFields": [],
"interfaces": [
{
"kind": "INTERFACE",
"name": "Node",
"ofType": null
}
],
"kind": "OBJECT",
"name": "Resource",
"possibleTypes": []
},
{
"description": "Fetches an object given its ID.",
"enumValues": [],
"fields": [
{
"args": [],
"deprecationReason": null,
"description": "The globally unique object ID.",
"isDeprecated": false,
"name": "id",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
}
],
"inputFields": [],
"interfaces": [],
"kind": "INTERFACE",
"name": "Node",
"possibleTypes": [
{
"kind": "INTERFACE",
"name": "Node",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Resource",
"ofType": null
}
]
}
]
}
}`,
})
assert.NoError(t, err)

typeNames := func(defs []*ast.Definition) []string {
var names []string
for _, def := range defs {
names = append(names, def.Name)
}
return names
}
assert.Equal(t, []string{"Resource"}, typeNames(schema.GetPossibleTypes(schema.Types["Node"])))
assert.Equal(t, []string{"Resource"}, typeNames(schema.GetPossibleTypes(schema.Types["Resource"])))

_, err = gqlparser.LoadQuery(schema, `
query {
node(id: "resource") {
... on Resource {
id
}
}
}
`)
assert.Nil(t, err, "Spreading object fragment on matching interface should be allowed")
}

0 comments on commit 14a9583

Please sign in to comment.