X Tutup
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
147 changes: 147 additions & 0 deletions src/main/java/graphql/util/querygenerator/QueryGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package graphql.util.querygenerator;

import graphql.ExperimentalApi;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLFieldsContainer;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLUnionType;
import org.jspecify.annotations.Nullable;

import java.util.stream.Stream;

/**
* Generates a GraphQL query string based on the provided operation field path, operation name, arguments, and type classifier.
* <p>
* While this class is useful for testing purposes, such as ensuring that all fields from a certain type are being
* fetched correctly, it is important to note that generating GraphQL queries with all possible fields defeats one of
* the main purposes of a GraphQL API: allowing clients to be selective about the fields they want to fetch.
* <p>
* Callers can pass options to customize the query generation process, such as filtering fields or
* limiting the maximum number of fields.
* <p>
*
*/
@ExperimentalApi
public class QueryGenerator {
private final GraphQLSchema schema;
private final QueryGeneratorFieldSelection fieldSelectionGenerator;
private final QueryGeneratorPrinter printer;

/**
* Constructor for QueryGenerator.
*
* @param schema the GraphQL schema
* @param options the options for query generation
*/
public QueryGenerator(GraphQLSchema schema, QueryGeneratorOptions options) {
this.schema = schema;
this.fieldSelectionGenerator = new QueryGeneratorFieldSelection(schema, options);
this.printer = new QueryGeneratorPrinter();
}

/**
* Generates a GraphQL query string based on the provided operation field path, operation name, arguments,
* and type classifier.
*
* <p>
* operationFieldPath is a string that represents the path to the field in the GraphQL schema. This method
* will generate a query that includes all fields from the specified type, including nested fields.
* <p>
* operationName is optional. When passed, the generated query will contain that value in the operation name.
* <p>
* arguments are optional. When passed, the generated query will contain that value in the arguments.
* <p>
* typeClassifier is optional. It should not be passed in when the field in the path is an object type, and it
* **should** be passed when the field in the path is an interface or union type. In the latter case, its value
* should be an object type that is part of the union or implements the interface.
*
* @param operationFieldPath the operation field path (e.g., "Query.user", "Mutation.createUser", "Subscription.userCreated")
* @param operationName optional: the operation name (e.g., "getUser")
* @param arguments optional: the arguments for the operation in a plain text form (e.g., "(id: 1)")
* @param typeClassifier optional: the type classifier for union or interface types (e.g., "FirstPartyUser")
*
* @return the generated GraphQL query string
*/
public String generateQuery(
String operationFieldPath,
@Nullable String operationName,
@Nullable String arguments,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

javadoc

I am unsure what arguments and typeClassifier mean

@Nullable String typeClassifier
) {
String[] fieldParts = operationFieldPath.split("\\.");
String operation = fieldParts[0];

if (fieldParts.length < 2) {
throw new IllegalArgumentException("Field path must contain at least an operation and a field");
}

if (!operation.equals("Query") && !operation.equals("Mutation") && !operation.equals("Subscription")) {
throw new IllegalArgumentException("Operation must be 'Query', 'Mutation' or 'Subscription'");
}

GraphQLFieldsContainer fieldContainer = schema.getObjectType(operation);

for (int i = 1; i < fieldParts.length - 1; i++) {
String fieldName = fieldParts[i];
GraphQLFieldDefinition fieldDefinition = fieldContainer.getFieldDefinition(fieldName);
if (fieldDefinition == null) {
throw new IllegalArgumentException("Field " + fieldName + " not found in type " + fieldContainer.getName());
}
// intermediate fields in the path need to be a field container
if (!(fieldDefinition.getType() instanceof GraphQLFieldsContainer)) {
throw new IllegalArgumentException("Type " + fieldDefinition.getType() + " is not a field container");
}
fieldContainer = (GraphQLFieldsContainer) fieldDefinition.getType();
}

String lastFieldName = fieldParts[fieldParts.length - 1];
GraphQLFieldDefinition lastFieldDefinition = fieldContainer.getFieldDefinition(lastFieldName);
if (lastFieldDefinition == null) {
throw new IllegalArgumentException("Field " + lastFieldName + " not found in type " + fieldContainer.getName());
}

// last field may be an object, interface or union type
GraphQLOutputType lastType = lastFieldDefinition.getType();

final GraphQLFieldsContainer lastFieldContainer;

if (lastType instanceof GraphQLObjectType) {
if (typeClassifier != null) {
throw new IllegalArgumentException("typeClassifier should be used only with interface or union types");
}
lastFieldContainer = (GraphQLObjectType) lastType;
} else if (lastType instanceof GraphQLUnionType) {
if (typeClassifier == null) {
throw new IllegalArgumentException("typeClassifier is required for union types");
}
lastFieldContainer = ((GraphQLUnionType) lastType).getTypes().stream()
.filter(GraphQLFieldsContainer.class::isInstance)
.map(GraphQLFieldsContainer.class::cast)
.filter(type -> type.getName().equals(typeClassifier))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Type " + typeClassifier + " not found in union " + ((GraphQLUnionType) lastType).getName()));
} else if (lastType instanceof GraphQLInterfaceType) {
if (typeClassifier == null) {
throw new IllegalArgumentException("typeClassifier is required for interface types");
}
Stream<GraphQLFieldsContainer> fieldsContainerStream = Stream.concat(
Stream.of((GraphQLInterfaceType) lastType),
schema.getImplementations((GraphQLInterfaceType) lastType).stream()
);

lastFieldContainer = fieldsContainerStream
.filter(type -> type.getName().equals(typeClassifier))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Type " + typeClassifier + " not found in interface " + ((GraphQLInterfaceType) lastType).getName()));
} else {
throw new IllegalArgumentException("Type " + lastType + " is not a field container");
}

QueryGeneratorFieldSelection.FieldSelection rootFieldSelection = fieldSelectionGenerator.buildFields(lastFieldContainer);

return printer.print(operationFieldPath, operationName, arguments, rootFieldSelection);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package graphql.util.querygenerator;

import graphql.schema.FieldCoordinates;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLFieldsContainer;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import graphql.schema.GraphQLUnionType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

class QueryGeneratorFieldSelection {
private final QueryGeneratorOptions options;
private final GraphQLSchema schema;

private static final GraphQLObjectType EMPTY_OBJECT_TYPE = GraphQLObjectType.newObject()
.name("Empty")
.build();

QueryGeneratorFieldSelection(GraphQLSchema schema, QueryGeneratorOptions options) {
this.options = options;
this.schema = schema;
}

FieldSelection buildFields(GraphQLFieldsContainer fieldsContainer) {
Queue<List<GraphQLFieldsContainer>> containersQueue = new LinkedList<>();
containersQueue.add(Collections.singletonList(fieldsContainer));

Queue<FieldSelection> fieldSelectionQueue = new LinkedList<>();
FieldSelection root = new FieldSelection(fieldsContainer.getName(), new HashMap<>(), false);
fieldSelectionQueue.add(root);

Set<FieldCoordinates> visited = new HashSet<>();
AtomicInteger totalFieldCount = new AtomicInteger(0);

while (!containersQueue.isEmpty()) {
processContainers(containersQueue, fieldSelectionQueue, visited, totalFieldCount);

if (totalFieldCount.get() >= options.getMaxFieldCount()) {
break;
}
}

return root;
}

private void processContainers(Queue<List<GraphQLFieldsContainer>> containersQueue,
Queue<FieldSelection> fieldSelectionQueue,
Set<FieldCoordinates> visited,
AtomicInteger totalFieldCount) {
List<GraphQLFieldsContainer> containers = containersQueue.poll();
FieldSelection fieldSelection = fieldSelectionQueue.poll();

for (GraphQLFieldsContainer container : Objects.requireNonNull(containers)) {
if (!options.getFilterFieldContainerPredicate().test(container)) {
continue;
}

for (GraphQLFieldDefinition fieldDef : container.getFieldDefinitions()) {
if (!options.getFilterFieldDefinitionPredicate().test(fieldDef)) {
continue;
}

if (totalFieldCount.get() >= options.getMaxFieldCount()) {
break;
}

if (hasRequiredArgs(fieldDef)) {
continue;
}

FieldCoordinates fieldCoordinates = FieldCoordinates.coordinates(container, fieldDef.getName());

if (visited.contains(fieldCoordinates)) {
continue;
}

processField(
container,
fieldDef,
Objects.requireNonNull(fieldSelection),
containersQueue,
fieldSelectionQueue,
fieldCoordinates,
visited,
totalFieldCount
);
}
}
}

private void processField(GraphQLFieldsContainer container,
GraphQLFieldDefinition fieldDef,
FieldSelection fieldSelection,
Queue<List<GraphQLFieldsContainer>> containersQueue,
Queue<FieldSelection> fieldSelectionQueue,
FieldCoordinates fieldCoordinates,
Set<FieldCoordinates> visited,
AtomicInteger totalFieldCount) {

GraphQLType unwrappedType = GraphQLTypeUtil.unwrapAll(fieldDef.getType());
FieldSelection newFieldSelection = getFieldSelection(fieldDef, unwrappedType);

fieldSelection.fieldsByContainer.computeIfAbsent(container.getName(), key -> new ArrayList<>()).add(newFieldSelection);

fieldSelectionQueue.add(newFieldSelection);

if (unwrappedType instanceof GraphQLInterfaceType) {
visited.add(fieldCoordinates);
GraphQLInterfaceType interfaceType = (GraphQLInterfaceType) unwrappedType;
List<GraphQLFieldsContainer> possibleTypes = new ArrayList<>(schema.getImplementations(interfaceType));

containersQueue.add(possibleTypes);
} else if (unwrappedType instanceof GraphQLUnionType) {
visited.add(fieldCoordinates);
GraphQLUnionType unionType = (GraphQLUnionType) unwrappedType;
List<GraphQLFieldsContainer> possibleTypes = unionType.getTypes().stream()
.filter(possibleType -> possibleType instanceof GraphQLFieldsContainer)
.map(possibleType -> (GraphQLFieldsContainer) possibleType)
.collect(Collectors.toList());

containersQueue.add(possibleTypes);
} else if (unwrappedType instanceof GraphQLFieldsContainer) {
visited.add(fieldCoordinates);
containersQueue.add(Collections.singletonList((GraphQLFieldsContainer) unwrappedType));
} else {
containersQueue.add(Collections.singletonList(EMPTY_OBJECT_TYPE));
}

totalFieldCount.incrementAndGet();
}

private static FieldSelection getFieldSelection(GraphQLFieldDefinition fieldDef, GraphQLType unwrappedType) {
boolean typeNeedsClassifier = unwrappedType instanceof GraphQLUnionType || unwrappedType instanceof GraphQLInterfaceType;

// TODO: This statement is kinda awful
final FieldSelection newFieldSelection;

if (typeNeedsClassifier) {
newFieldSelection = new FieldSelection(fieldDef.getName(), new HashMap<>(), true);
} else if (unwrappedType instanceof GraphQLFieldsContainer) {
newFieldSelection = new FieldSelection(fieldDef.getName(), new HashMap<>(), false);
} else {
newFieldSelection = new FieldSelection(fieldDef.getName(), null, false);
}
return newFieldSelection;
}

private boolean hasRequiredArgs(GraphQLFieldDefinition fieldDefinition) {
// TODO: Maybe provide a hook to allow callers to resolve required arguments
return fieldDefinition.getArguments().stream()
.anyMatch(arg -> GraphQLTypeUtil.isNonNull(arg.getType()) && !arg.hasSetDefaultValue());
}

static class FieldSelection {
public final String name;
public final boolean needsTypeClassifier;
public final Map<String, List<FieldSelection>> fieldsByContainer;

public FieldSelection(String name, Map<String, List<FieldSelection>> fieldsByContainer, boolean needsTypeClassifier) {
this.name = name;
this.needsTypeClassifier = needsTypeClassifier;
this.fieldsByContainer = fieldsByContainer;
}

}
}
Loading
X Tutup