All Downloads are FREE. Search and download functionalities are using the official Maven repository.

graphql.normalized.ExecutableNormalizedField Maven / Gradle / Ivy

There is a newer version: 230521-nf-execution
Show newest version
package graphql.normalized;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import graphql.ExperimentalApi;
import graphql.Internal;
import graphql.Mutable;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import graphql.introspection.Introspection;
import graphql.language.Argument;
import graphql.normalized.incremental.NormalizedDeferredExecution;
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.GraphQLUnionType;
import graphql.util.FpKit;
import graphql.util.MutableRef;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import static graphql.Assert.assertNotNull;
import static graphql.Assert.assertTrue;
import static graphql.schema.GraphQLTypeUtil.simplePrint;
import static graphql.schema.GraphQLTypeUtil.unwrapAll;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

/**
 * An {@link ExecutableNormalizedField} represents a field in an executable graphql operation.  Its models what
 * could be executed during a given operation.
 * 

* This class is intentionally mutable for performance reasons since building immutable parent child * objects is too expensive. */ @PublicApi @Mutable public class ExecutableNormalizedField { private final String alias; private final ImmutableMap normalizedArguments; private final LinkedHashMap resolvedArguments; private final ImmutableList astArguments; // Mutable List on purpose: it is modified after creation private final LinkedHashSet objectTypeNames; private final ArrayList children; private ExecutableNormalizedField parent; private final String fieldName; private final int level; // Mutable List on purpose: it is modified after creation private final LinkedHashSet deferredExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; this.resolvedArguments = builder.resolvedArguments; this.normalizedArguments = builder.normalizedArguments; this.astArguments = builder.astArguments; this.objectTypeNames = builder.objectTypeNames; this.fieldName = assertNotNull(builder.fieldName); this.children = builder.children; this.level = builder.level; this.parent = builder.parent; this.deferredExecutions = builder.deferredExecutions; } /** * Determines whether this {@link ExecutableNormalizedField} needs a fragment to select the field. However, it considers the parent * output type when determining whether it needs a fragment. *

* Consider the following schema * *

     * interface Animal {
     *     name: String
     *     parent: Animal
     * }
     * type Cat implements Animal {
     *     name: String
     *     parent: Cat
     * }
     * type Dog implements Animal {
     *     name: String
     *     parent: Dog
     *     isGoodBoy: Boolean
     * }
     * type Query {
     *     animal: Animal
     * }
     * 
*

* and the following query * *

     * {
     *     animal {
     *         parent {
     *             name
     *         }
     *     }
     * }
     * 
*

* Then we would get the following {@link ExecutableNormalizedOperation} * *

     * -Query.animal: Animal
     * --[Cat, Dog].parent: Cat, Dog
     * ---[Cat, Dog].name: String
     * 
*

* If we simply checked the {@link #parent}'s {@link #getFieldDefinitions(GraphQLSchema)} that would * point us to {@code Cat.parent} and {@code Dog.parent} whose output types would incorrectly answer * our question whether this is conditional? *

* 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 implementations would say. * * @param schema - the graphql schema in play * * @return true if the field is conditional */ public boolean isConditional(@NotNull GraphQLSchema schema) { if (parent == null) { return false; } for (GraphQLInterfaceType commonParentOutputInterface : parent.getInterfacesCommonToAllOutputTypes(schema)) { List implementations = schema.getImplementations(commonParentOutputInterface); // __typename if (fieldName.equals(Introspection.TypeNameMetaFieldDef.getName()) && implementations.size() == objectTypeNames.size()) { return false; } if (commonParentOutputInterface.getField(fieldName) == null) { continue; } if (implementations.size() == objectTypeNames.size()) { return false; } } // __typename is the only field in a union type that CAN be NOT conditional GraphQLFieldDefinition parentFieldDef = parent.getOneFieldDefinition(schema); if (unwrapAll(parentFieldDef.getType()) instanceof GraphQLUnionType) { GraphQLUnionType parentOutputTypeAsUnion = (GraphQLUnionType) unwrapAll(parentFieldDef.getType()); if (fieldName.equals(Introspection.TypeNameMetaFieldDef.getName()) && objectTypeNames.size() == parentOutputTypeAsUnion.getTypes().size()) { return false; // Not 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(parentFieldDef.getType()) != oneObjectType; } public boolean hasChildren() { return children.size() > 0; } public GraphQLOutputType getType(GraphQLSchema schema) { List fieldDefinitions = getFieldDefinitions(schema); Set fieldTypes = fieldDefinitions.stream().map(fd -> simplePrint(fd.getType())).collect(toSet()); assertTrue(fieldTypes.size() == 1, () -> "More than one type ... use getTypes"); return fieldDefinitions.get(0).getType(); } public List getTypes(GraphQLSchema schema) { return ImmutableKit.map(getFieldDefinitions(schema), fd -> fd.getType()); } public void forEachFieldDefinition(GraphQLSchema schema, Consumer consumer) { var fieldDefinition = resolveIntrospectionField(schema, objectTypeNames, fieldName); if (fieldDefinition != null) { consumer.accept(fieldDefinition); return; } for (String objectTypeName : objectTypeNames) { GraphQLObjectType type = (GraphQLObjectType) assertNotNull(schema.getType(objectTypeName)); consumer.accept(assertNotNull(type.getField(fieldName), "No field %s found for type %s", fieldName, objectTypeName)); } } public List getFieldDefinitions(GraphQLSchema schema) { ImmutableList.Builder builder = ImmutableList.builder(); forEachFieldDefinition(schema, builder::add); return builder.build(); } /** * This is NOT public as it is not recommended usage. *

* Internally there are cases where we know it is safe to use this, so this exists. */ private GraphQLFieldDefinition getOneFieldDefinition(GraphQLSchema schema) { var fieldDefinition = resolveIntrospectionField(schema, objectTypeNames, fieldName); if (fieldDefinition != null) { return fieldDefinition; } String objectTypeName = objectTypeNames.iterator().next(); GraphQLObjectType type = (GraphQLObjectType) assertNotNull(schema.getType(objectTypeName)); return assertNotNull(type.getField(fieldName), "No field %s found for type %s", fieldName, objectTypeName); } private static GraphQLFieldDefinition resolveIntrospectionField(GraphQLSchema schema, Set objectTypeNames, String fieldName) { if (fieldName.equals(schema.getIntrospectionTypenameFieldDefinition().getName())) { return schema.getIntrospectionTypenameFieldDefinition(); } else if (objectTypeNames.size() == 1 && objectTypeNames.iterator().next().equals(schema.getQueryType().getName())) { if (fieldName.equals(schema.getIntrospectionSchemaFieldDefinition().getName())) { return schema.getIntrospectionSchemaFieldDefinition(); } else if (fieldName.equals(schema.getIntrospectionTypeFieldDefinition().getName())) { return schema.getIntrospectionTypeFieldDefinition(); } } return null; } @Internal public void addObjectTypeNames(Collection objectTypeNames) { this.objectTypeNames.addAll(objectTypeNames); } @Internal public void setObjectTypeNames(Collection objectTypeNames) { this.objectTypeNames.clear(); this.objectTypeNames.addAll(objectTypeNames); } @Internal public void addChild(ExecutableNormalizedField executableNormalizedField) { this.children.add(executableNormalizedField); } @Internal public void clearChildren() { this.children.clear(); } @Internal public void setDeferredExecutions(Collection deferredExecutions) { this.deferredExecutions.clear(); this.deferredExecutions.addAll(deferredExecutions); } public void addDeferredExecutions(Collection deferredExecutions) { this.deferredExecutions.addAll(deferredExecutions); } /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

* WARNING: This is not always the key in the execution result, because of possible field aliases. * * @return the name of this {@link ExecutableNormalizedField} * * @see #getResultKey() * @see #getAlias() */ public String getName() { return getFieldName(); } /** * @return the same value as {@link #getName()} * * @see #getResultKey() * @see #getAlias() */ public String getFieldName() { return fieldName; } /** * Returns the result key of this {@link ExecutableNormalizedField} within the overall result. * This is either a field alias or the value of {@link #getName()} * * @return the result key for this {@link ExecutableNormalizedField}. * * @see #getName() */ public String getResultKey() { if (alias != null) { return alias; } return getName(); } /** * @return the field alias used or null if there is none * * @see #getResultKey() * @see #getName() */ public String getAlias() { return alias; } /** * @return a list of the {@link Argument}s on the field */ public ImmutableList getAstArguments() { return astArguments; } /** * Returns an argument value as a {@link NormalizedInputValue} which contains its type name and its current value * * @param name the name of the argument * * @return an argument value */ public NormalizedInputValue getNormalizedArgument(String name) { return normalizedArguments.get(name); } /** * @return a map of all the arguments in {@link NormalizedInputValue} form */ public ImmutableMap getNormalizedArguments() { return normalizedArguments; } /** * @return a map of the resolved argument values */ public LinkedHashMap getResolvedArguments() { return resolvedArguments; } /** * A {@link ExecutableNormalizedField} can sometimes (for non-concrete types like interfaces and unions) * have more than one object type it could be when executed. There is no way to know what it will be until * the field is executed over data and the type is resolved via a {@link graphql.schema.TypeResolver}. *

* This method returns all the possible types a field can be which is one or more {@link GraphQLObjectType} * names. *

* Warning: This returns a Mutable Set. No defensive copy is made for performance reasons. * * @return a set of the possible type names this field could be. */ public Set getObjectTypeNames() { return objectTypeNames; } /** * This returns the first entry in {@link #getObjectTypeNames()}. Sometimes you know a field cant be more than one * type and this method is a shortcut one to help you. * * @return the first entry from */ public String getSingleObjectTypeName() { return objectTypeNames.iterator().next(); } /** * @return a helper method show field details */ public String printDetails() { StringBuilder result = new StringBuilder(); if (getAlias() != null) { result.append(getAlias()).append(": "); } return result + objectTypeNamesToString() + "." + fieldName; } /** * @return a helper method to show the object types names as a string */ public String objectTypeNamesToString() { if (objectTypeNames.size() == 1) { return objectTypeNames.iterator().next(); } else { return objectTypeNames.toString(); } } /** * This returns the list of the result keys (see {@link #getResultKey()} that lead from this field upwards to * its parent field * * @return a list of the result keys from this {@link ExecutableNormalizedField} to the top of the operation via parent fields */ public List getListOfResultKeys() { LinkedList list = new LinkedList<>(); ExecutableNormalizedField current = this; while (current != null) { list.addFirst(current.getResultKey()); current = current.parent; } return list; } /** * @return the children of the {@link ExecutableNormalizedField} */ public List getChildren() { return children; } /** * Returns the list of child fields that would have the same result key * * @param resultKey the result key to check * * @return a list of all direct {@link ExecutableNormalizedField} children with the specified result key */ public List getChildrenWithSameResultKey(String resultKey) { return FpKit.filterList(children, child -> child.getResultKey().equals(resultKey)); } public List getChildren(int includingRelativeLevel) { List result = new ArrayList<>(); assertTrue(includingRelativeLevel >= 1, () -> "relative level must be >= 1"); this.getChildren().forEach(child -> { traverseImpl(child, result::add, 1, includingRelativeLevel); }); return result; } /** * This returns the child fields that can be used if the object is of the specified object type * * @param objectTypeName the object type * * @return a list of child fields that would apply to that object type */ public List getChildren(String objectTypeName) { return children.stream() .filter(cld -> cld.objectTypeNames.contains(objectTypeName)) .collect(toList()); } /** * the level of the {@link ExecutableNormalizedField} in the operation hierarchy with top level fields * starting at 1 * * @return the level of the {@link ExecutableNormalizedField} in the operation hierarchy */ public int getLevel() { return level; } /** * @return the parent of this {@link ExecutableNormalizedField} or null if it's a top level field */ public ExecutableNormalizedField getParent() { return parent; } /** * @return the {@link NormalizedDeferredExecution}s associated with this {@link ExecutableNormalizedField}. * * @see NormalizedDeferredExecution */ @ExperimentalApi public LinkedHashSet getDeferredExecutions() { return deferredExecutions; } @Internal public void replaceParent(ExecutableNormalizedField newParent) { this.parent = newParent; } @Override public String toString() { return "NormalizedField{" + objectTypeNamesToString() + "." + fieldName + ", alias=" + alias + ", level=" + level + ", children=" + children.stream().map(ExecutableNormalizedField::toString).collect(joining("\n")) + '}'; } /** * Traverse from this {@link ExecutableNormalizedField} down into itself and all of its children * * @param consumer the callback for each {@link ExecutableNormalizedField} in the hierarchy. */ public void traverseSubTree(Consumer consumer) { this.getChildren().forEach(child -> { traverseImpl(child, consumer, 1, Integer.MAX_VALUE); }); } private void traverseImpl(ExecutableNormalizedField root, Consumer consumer, int curRelativeLevel, int abortAfter) { if (curRelativeLevel > abortAfter) { return; } consumer.accept(root); root.getChildren().forEach(child -> { traverseImpl(child, consumer, curRelativeLevel + 1, abortAfter); }); } /** * This tries to find interfaces common to all the field output types. *

* i.e. goes through {@link #getFieldDefinitions(GraphQLSchema)} and finds interfaces that * all the field's unwrapped output types are assignable to. */ @SuppressWarnings({"unchecked", "rawtypes"}) private Set getInterfacesCommonToAllOutputTypes(GraphQLSchema schema) { // Shortcut for performance if (objectTypeNames.size() == 1) { var fieldDef = getOneFieldDefinition(schema); var outputType = unwrapAll(fieldDef.getType()); if (outputType instanceof GraphQLObjectType) { return new LinkedHashSet<>((List) ((GraphQLObjectType) outputType).getInterfaces()); } else if (outputType instanceof GraphQLInterfaceType) { var result = new LinkedHashSet<>((List) ((GraphQLInterfaceType) outputType).getInterfaces()); result.add(outputType); return result; } else { return Collections.emptySet(); } } MutableRef> commonInterfaces = new MutableRef<>(); forEachFieldDefinition(schema, (fieldDef) -> { var outputType = unwrapAll(fieldDef.getType()); List outputTypeInterfaces; if (outputType instanceof GraphQLObjectType) { outputTypeInterfaces = (List) ((GraphQLObjectType) outputType).getInterfaces(); } else if (outputType instanceof GraphQLInterfaceType) { // This interface and superinterfaces List superInterfaces = ((GraphQLInterfaceType) outputType).getInterfaces(); outputTypeInterfaces = new ArrayList<>(superInterfaces.size() + 1); outputTypeInterfaces.add((GraphQLInterfaceType) outputType); if (!superInterfaces.isEmpty()) { outputTypeInterfaces.addAll((List) superInterfaces); } } else { outputTypeInterfaces = Collections.emptyList(); } if (commonInterfaces.value == null) { commonInterfaces.value = new LinkedHashSet<>(outputTypeInterfaces); } else { commonInterfaces.value.retainAll(outputTypeInterfaces); } }); return commonInterfaces.value; } /** * @return a {@link Builder} of {@link ExecutableNormalizedField}s */ public static Builder newNormalizedField() { return new Builder(); } /** * Allows this {@link ExecutableNormalizedField} to be transformed via a {@link Builder} consumer callback * * @param builderConsumer the consumer given a builder * * @return a new transformed {@link ExecutableNormalizedField} */ public ExecutableNormalizedField transform(Consumer builderConsumer) { Builder builder = new Builder(this); builderConsumer.accept(builder); return builder.build(); } public static class Builder { private LinkedHashSet objectTypeNames = new LinkedHashSet<>(); private String fieldName; private ArrayList children = new ArrayList<>(); private int level; private ExecutableNormalizedField parent; private String alias; private ImmutableMap normalizedArguments = ImmutableKit.emptyMap(); private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); private LinkedHashSet deferredExecutions = new LinkedHashSet<>(); private Builder() { } private Builder(ExecutableNormalizedField existing) { this.alias = existing.alias; this.normalizedArguments = existing.normalizedArguments; this.astArguments = existing.astArguments; this.resolvedArguments = existing.resolvedArguments; this.objectTypeNames = new LinkedHashSet<>(existing.getObjectTypeNames()); this.fieldName = existing.getFieldName(); this.children = new ArrayList<>(existing.children); this.level = existing.getLevel(); this.parent = existing.getParent(); this.deferredExecutions = existing.getDeferredExecutions(); } public Builder clearObjectTypesNames() { this.objectTypeNames.clear(); return this; } public Builder objectTypeNames(List objectTypeNames) { this.objectTypeNames.addAll(objectTypeNames); return this; } public Builder alias(String alias) { this.alias = alias; return this; } public Builder normalizedArguments(@Nullable Map arguments) { this.normalizedArguments = arguments == null ? ImmutableKit.emptyMap() : ImmutableMap.copyOf(arguments); return this; } public Builder resolvedArguments(@Nullable Map arguments) { this.resolvedArguments = arguments == null ? new LinkedHashMap<>() : new LinkedHashMap<>(arguments); return this; } public Builder astArguments(@NotNull List astArguments) { this.astArguments = ImmutableList.copyOf(astArguments); return this; } public Builder fieldName(String fieldName) { this.fieldName = fieldName; return this; } public Builder children(List children) { this.children.clear(); this.children.addAll(children); return this; } public Builder level(int level) { this.level = level; return this; } public Builder parent(ExecutableNormalizedField parent) { this.parent = parent; return this; } public Builder deferredExecutions(LinkedHashSet deferredExecutions) { this.deferredExecutions = deferredExecutions; return this; } public ExecutableNormalizedField build() { return new ExecutableNormalizedField(this); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy