Skip to content
Merged
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
7 changes: 7 additions & 0 deletions src/main/java/graphql/normalized/ENFMerger.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graphql.normalized;

import graphql.Internal;
import graphql.introspection.Introspection;
import graphql.language.Argument;
import graphql.language.AstComparator;
import graphql.schema.GraphQLInterfaceType;
Expand All @@ -27,6 +28,11 @@ public static void merge(ExecutableNormalizedField parent, List<ExecutableNormal
overPossibleGroups:
for (Set<ExecutableNormalizedField> group : possibleGroupsToMerge) {
for (ExecutableNormalizedField fieldInGroup : group) {
if(field.getFieldName().equals(Introspection.TypeNameMetaFieldDef.getName())) {
addToGroup = true;
group.add(field);
continue overPossibleGroups;
}
if (field.getFieldName().equals(fieldInGroup.getFieldName()) &&
sameArguments(field.getAstArguments(), fieldInGroup.getAstArguments())
&& isFieldInSharedInterface(field, fieldInGroup, schema)
Expand Down Expand Up @@ -66,6 +72,7 @@ && isFieldInSharedInterface(field, fieldInGroup, schema)
}

private static boolean isFieldInSharedInterface(ExecutableNormalizedField fieldOne, ExecutableNormalizedField fieldTwo, GraphQLSchema schema) {

/*
* we can get away with only checking one of the object names, because all object names in one ENF are guaranteed to be the same field.
* This comes from how the ENFs are created in the factory before.
Expand Down
108 changes: 62 additions & 46 deletions src/main/java/graphql/normalized/ExecutableNormalizedField.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
import graphql.collect.ImmutableKit;
import graphql.introspection.Introspection;
import graphql.language.Argument;
import graphql.schema.GraphQLCompositeType;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLNamedOutputType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLUnionType;
import graphql.schema.GraphQLUnmodifiedType;
import graphql.util.FpKit;
import org.dataloader.impl.Assertions;
import org.jetbrains.annotations.NotNull;
Expand All @@ -33,6 +36,7 @@

import static graphql.Assert.assertNotNull;
import static graphql.Assert.assertTrue;
import static graphql.schema.GraphQLTypeUtil.simplePrint;
import static graphql.schema.GraphQLTypeUtil.unwrapAll;

/**
Expand Down Expand Up @@ -67,14 +71,6 @@ private ExecutableNormalizedField(Builder builder) {
this.parent = builder.parent;
}

@Deprecated // doesn't really work
public boolean isConditional(GraphQLSchema schema) {
if (parent == null) {
return false;
}
return objectTypeNames.size() > 1 || unwrapAll(parent.getType(schema)) != getOneObjectType(schema);
}

/**
* Determines whether this NF needs a fragment to select the field. However, it considers the parent
* output type when determining whether it needs a fragment.
Expand Down Expand Up @@ -127,63 +123,87 @@ public boolean isConditional(GraphQLSchema schema) {
* We MUST consider that the output type of the {@code parent} field is {@code Animal} and
* NOT {@code Cat} or {@code Dog} as their respective impls would say.
*/
public boolean isConditional(GraphQLSchema schema, @Nullable String parentOutputTypeName) {
if (parent == null || parentOutputTypeName == null) {
public boolean isConditional(@NotNull GraphQLSchema schema) {
if (parent == null) {
return false;
}

GraphQLType parentOutputType = schema.getType(parentOutputTypeName);

if (parentOutputType instanceof GraphQLInterfaceType) {
GraphQLInterfaceType parentOutputTypeAsInterface = (GraphQLInterfaceType) parentOutputType;
List<GraphQLObjectType> interfaceImpls = schema.getImplementations(parentOutputTypeAsInterface);

/**
* checking if we have an interface which can be used as an unconditional parent type
*/
ImmutableList<GraphQLType> parentTypes = ImmutableKit.map(parent.getFieldDefinitions(schema), fd -> unwrapAll(fd.getType()));

Set<GraphQLInterfaceType> interfacesImplementedByAllParents = null;
for (GraphQLType parentType : parentTypes) {
List<GraphQLInterfaceType> toAdd = new ArrayList<>();
if (parentType instanceof GraphQLObjectType) {
toAdd.addAll((List) ((GraphQLObjectType) parentType).getInterfaces());
} else if (parentType instanceof GraphQLInterfaceType) {
toAdd.add((GraphQLInterfaceType) parentType);
toAdd.addAll((List) ((GraphQLInterfaceType) parentType).getInterfaces());
}
if (interfacesImplementedByAllParents == null) {
interfacesImplementedByAllParents = new LinkedHashSet<>(toAdd);
} else {
interfacesImplementedByAllParents.retainAll(toAdd);
}
}
for (GraphQLInterfaceType parentInterfaceType : interfacesImplementedByAllParents) {
List<GraphQLObjectType> implementations = schema.getImplementations(parentInterfaceType);
// __typename
if (this.fieldName.equals(Introspection.TypeNameMetaFieldDef.getName()) && interfaceImpls.size() == objectTypeNames.size()) {
return false; // Not conditional
if (this.fieldName.equals(Introspection.TypeNameMetaFieldDef.getName()) && implementations.size() == objectTypeNames.size()) {
return false;
}

// If field does not exist on the output type, it is conditional
if (parentOutputTypeAsInterface.getField(fieldName) == null) {
return true; // Conditional
if (parentInterfaceType.getField(fieldName) == null) {
continue;
}
if (implementations.size() == objectTypeNames.size()) {
return false;
}
}

return interfaceImpls.size() != objectTypeNames.size();
} else if (parentOutputType instanceof GraphQLUnionType) {
GraphQLUnionType parentOutputTypeAsUnion = (GraphQLUnionType) parentOutputType;

// __typename is the only field in a union type that CAN be NOT conditional
/**
*__typename is the only field in a union type that CAN be NOT conditional
*/
List<GraphQLFieldDefinition> fieldDefinitions = parent.getFieldDefinitions(schema);
if (unwrapAll(fieldDefinitions.get(0).getType()) instanceof GraphQLUnionType) {
GraphQLUnionType parentOutputTypeAsUnion = (GraphQLUnionType) unwrapAll(fieldDefinitions.get(0).getType());
if (this.fieldName.equals(Introspection.TypeNameMetaFieldDef.getName()) && objectTypeNames.size() == parentOutputTypeAsUnion.getTypes().size()) {
return false; // Not conditional
} else {
return true; // Conditional
}
}

/**
* This means there is no Union or Interface which could serve as unconditional parent
*/
if (objectTypeNames.size() > 1) {
return true; // Conditional
}
if (parent.objectTypeNames.size() > 1) {
return true;
}

GraphQLObjectType oneObjectType = (GraphQLObjectType) schema.getType(objectTypeNames.iterator().next());
return unwrapAll(parent.getFieldDefinitions(schema).get(0).getType()) != oneObjectType;
}

return parentOutputType != getOneObjectType(schema);
public boolean hasChildren() {
return children.size() > 0;
}

@Deprecated // using this is ALMOST always wrong
public GraphQLOutputType getType(GraphQLSchema schema) {
return getOneFieldDefinition(schema).getType();
List<GraphQLFieldDefinition> fieldDefinitions = getFieldDefinitions(schema);
Set<String> fieldTypes = fieldDefinitions.stream().map(fd -> simplePrint(fd.getType())).collect(Collectors.toSet());
Assert.assertTrue(fieldTypes.size() == 1, () -> "More than one type ... use getTypes");
return fieldDefinitions.get(0).getType();
}

@Deprecated // using this is ALMOST always wrong
public GraphQLFieldDefinition getOneFieldDefinition(GraphQLSchema schema) {
GraphQLFieldDefinition fieldDefinition;
GraphQLFieldDefinition introspectionField = resolveIntrospectionField(schema, objectTypeNames, fieldName);
if (introspectionField != null) {
return introspectionField;
}
GraphQLObjectType type = (GraphQLObjectType) assertNotNull(schema.getType(objectTypeNames.iterator().next()));
fieldDefinition = assertNotNull(type.getField(fieldName), () -> String.format("no field %s found for type %s", fieldName, objectTypeNames.iterator().next()));
return fieldDefinition;
public List<GraphQLOutputType> getTypes(GraphQLSchema schema) {
List<GraphQLOutputType> fieldTypes = ImmutableKit.map(getFieldDefinitions(schema), fd -> fd.getType());
return fieldTypes;
}


public List<GraphQLFieldDefinition> getFieldDefinitions(GraphQLSchema schema) {
GraphQLFieldDefinition fieldDefinition = resolveIntrospectionField(schema, objectTypeNames, fieldName);
if (fieldDefinition != null) {
Expand Down Expand Up @@ -300,10 +320,6 @@ public String getSingleObjectTypeName() {
return objectTypeNames.iterator().next();
}

@Deprecated // using this is ALMOST always wrong
public GraphQLObjectType getOneObjectType(GraphQLSchema schema) {
return (GraphQLObjectType) schema.getType(objectTypeNames.iterator().next());
}

public String printDetails() {
StringBuilder result = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private static List<Selection<?>> subselectionsForNormalizedField(GraphQLSchema
Map<String, List<Field>> fieldsByTypeCondition = new LinkedHashMap<>();

for (ExecutableNormalizedField nf : executableNormalizedFields) {
if (nf.isConditional(schema, parentOutputType)) {
if (nf.isConditional(schema)) {
selectionForNormalizedField(schema, nf, variableAccumulator)
.forEach((objectTypeName, field) ->
fieldsByTypeCondition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,7 @@ private void traverseSubSelectedFields(ExecutableNormalizedField currentNormalis
normalisedSelectionSetFields.computeIfAbsent(globQualifiedName, newList()).add(selectedField);
normalisedSelectionSetFields.computeIfAbsent(globSimpleName, newList()).add(selectedField);

GraphQLType unwrappedType = GraphQLTypeUtil.unwrapAll(normalizedSubSelectedField.getType(schema));
if (!GraphQLTypeUtil.isLeaf(unwrappedType)) {
if (normalizedSubSelectedField.hasChildren()) {
traverseSubSelectedFields(normalizedSubSelectedField, immediateFieldsBuilder, globQualifiedName, globSimpleName, false);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ type Dog implements Animal{
'Human.name']
}

def "test interface fields with different output types on the implementations"() {
def "test interface fields with different output types (covariance) on the implementations"() {
def graphQLSchema = schema("""
interface Animal {
parent: Animal
Expand Down Expand Up @@ -790,6 +790,95 @@ type Dog implements Animal{
]
}

def "__typename in unions get merged"() {
def graphQLSchema = schema("""

type Cat {
name: String
}
type Dog {
name: String
}
union CatOrDog = Cat | Dog
type Query {
animal: CatOrDog
}
""")

def query = """
{
animal {
... on Cat {__typename}
... on Dog {__typename}
}
}
"""

assertValidQuery(graphQLSchema, query)

Document document = TestUtil.parseQuery(query)

def dependencyGraph = new ExecutableNormalizedOperationFactory()
def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, [:])
def printedTree = printTreeWithLevelInfo(tree, graphQLSchema)

expect:
printedTree == [
"-Query.animal: CatOrDog",
"--[Cat, Dog].__typename: String!",
]
}

def "test union fields with different output types (covariance) on the implementations"() {
def graphQLSchema = schema("""

interface Animal {
parent: CatOrDog
name: String
}
type Cat implements Animal{
name: String
parent: Cat
}
type Dog implements Animal {
name: String
parent: Dog
isGoodBoy: Boolean
}
union CatOrDog = Cat | Dog
type Query {
animal: Animal
}
""")

def query = """
{
animal {
parent {
... on Cat {name __typename }
... on Dog {name __typename }
}
}
}
"""

assertValidQuery(graphQLSchema, query)

Document document = TestUtil.parseQuery(query)

def dependencyGraph = new ExecutableNormalizedOperationFactory()
def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, [:])
def printedTree = printTreeWithLevelInfo(tree, graphQLSchema)

expect:
printedTree == [
"-Query.animal: Animal",
"--[Cat, Dog].parent: Cat, Dog",
"---[Cat, Dog].name: String",
"---[Cat, Dog].__typename: String!"
]
}

List<String> printTree(ExecutableNormalizedOperation queryExecutionTree) {
def result = []
Traverser<ExecutableNormalizedField> traverser = Traverser.depthFirst({ it.getChildren() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,12 +706,11 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification {
// --Dog.isGoodBoy: Boolean
printed == """query {
animal {
__typename
... on Cat {
__typename
name
}
... on Dog {
__typename
isGoodBoy
name
}
Expand Down
Loading