From 1939ed0c74a697a6268cf62978248cb573689dd0 Mon Sep 17 00:00:00 2001 From: John Starich Date: Fri, 17 Nov 2023 18:40:37 -0600 Subject: [PATCH] Fix introspect's AST to allow queries of object fragments on interfaces (#38) Fixes https://github.com/nautilus/graphql/issues/37 --- introspection.go | 16 ++++- introspection_test.go | 153 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 2 deletions(-) diff --git a/introspection.go b/introspection.go index 582a4d7..7ca2bb3 100644 --- a/introspection.go +++ b/introspection.go @@ -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 { @@ -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) } } @@ -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) } } @@ -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{} diff --git a/introspection_test.go b/introspection_test.go index 870ec83..8d2e11e 100644 --- a/introspection_test.go +++ b/introspection_test.go @@ -118,6 +118,12 @@ func TestIntrospectAPI_union(t *testing.T) { expectSubtype1, expectSubtype2, }, + "Subtype1": { + expectSubtype1, + }, + "Subtype2": { + expectSubtype2, + }, }, Implements: map[string][]*ast.Definition{ "Subtype1": {expectSubtype1, expectTypeA}, @@ -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") +}