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
59 changes: 41 additions & 18 deletions src/main/java/graphql/util/querygenerator/QueryGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLTypeUtil;
import graphql.schema.GraphQLUnionType;
import graphql.util.querygenerator.QueryGeneratorFieldSelection.FieldSelection;
import graphql.util.querygenerator.QueryGeneratorFieldSelection.FieldSelectionResult;
import org.jspecify.annotations.Nullable;

import java.util.stream.Stream;
Expand All @@ -30,6 +33,18 @@ public class QueryGenerator {
private final QueryGeneratorFieldSelection fieldSelectionGenerator;
private final QueryGeneratorPrinter printer;


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

/**
* Constructor for QueryGenerator.
*
Expand All @@ -54,22 +69,22 @@ public QueryGenerator(GraphQLSchema schema, QueryGeneratorOptions options) {
* <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
* typeName 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")
* @param typeName optional: the type name for when operationFieldPath points to a field of union or interface types (e.g., "FirstPartyUser")
*
* @return the generated GraphQL query string
* @return a QueryGeneratorResult containing the generated query string and additional information
*/
public String generateQuery(
public QueryGeneratorResult generateQuery(
String operationFieldPath,
@Nullable String operationName,
@Nullable String arguments,
@Nullable String typeClassifier
@Nullable String typeName
) {
String[] fieldParts = operationFieldPath.split("\\.");
String operation = fieldParts[0];
Expand Down Expand Up @@ -104,44 +119,52 @@ public String generateQuery(
}

// last field may be an object, interface or union type
GraphQLOutputType lastType = lastFieldDefinition.getType();
GraphQLOutputType lastType = GraphQLTypeUtil.unwrapAllAs(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");
if (typeName != null) {
throw new IllegalArgumentException("typeName 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");
if (typeName == null) {
throw new IllegalArgumentException("typeName is required for union types");
}
lastFieldContainer = ((GraphQLUnionType) lastType).getTypes().stream()
.filter(GraphQLFieldsContainer.class::isInstance)
.map(GraphQLFieldsContainer.class::cast)
.filter(type -> type.getName().equals(typeClassifier))
.filter(type -> type.getName().equals(typeName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Type " + typeClassifier + " not found in union " + ((GraphQLUnionType) lastType).getName()));
.orElseThrow(() -> new IllegalArgumentException("Type " + typeName + " not found in union " + ((GraphQLUnionType) lastType).getName()));
} else if (lastType instanceof GraphQLInterfaceType) {
if (typeClassifier == null) {
throw new IllegalArgumentException("typeClassifier is required for interface types");
if (typeName == null) {
throw new IllegalArgumentException("typeName 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))
.filter(type -> type.getName().equals(typeName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Type " + typeClassifier + " not found in interface " + ((GraphQLInterfaceType) lastType).getName()));
.orElseThrow(() -> new IllegalArgumentException("Type " + typeName + " not found in interface " + ((GraphQLInterfaceType) lastType).getName()));
} else {
throw new IllegalArgumentException("Type " + lastType + " is not a field container");
}

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

String query = printer.print(operationFieldPath, operationName, arguments, rootFieldSelection);

return printer.print(operationFieldPath, operationName, arguments, rootFieldSelection);
return new QueryGeneratorResult(
query,
lastFieldContainer.getName(),
fieldSelectionResult.totalFieldCount,
fieldSelectionResult.reachedMaxFieldCount
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class QueryGeneratorFieldSelection {
this.schema = schema;
}

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

Expand All @@ -46,16 +46,21 @@ FieldSelection buildFields(GraphQLFieldsContainer fieldsContainer) {

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

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

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

return root;
return new FieldSelectionResult(
root,
totalFieldCount.get(),
reachedMaxFieldCount
);
}

private void processContainers(Queue<List<GraphQLFieldsContainer>> containersQueue,
Expand Down Expand Up @@ -166,16 +171,27 @@ private boolean hasRequiredArgs(GraphQLFieldDefinition fieldDefinition) {
.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;
static class FieldSelection {
final String name;
final boolean needsTypeClassifier;
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;
}
}

static class FieldSelectionResult {
final FieldSelection rootFieldSelection;
final Integer totalFieldCount;
final Boolean reachedMaxFieldCount;

FieldSelectionResult(FieldSelection rootFieldSelection, Integer totalFieldCount, Boolean reachedMaxFieldCount) {
this.rootFieldSelection = rootFieldSelection;
this.totalFieldCount = totalFieldCount;
this.reachedMaxFieldCount = reachedMaxFieldCount;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package graphql.util.querygenerator;

import graphql.ExperimentalApi;

/**
* Represents the result of a query generation process.
*/
@ExperimentalApi
public class QueryGeneratorResult {
private final String query;
private final String usedType;
private final int totalFieldCount;
private final boolean reachedMaxFieldCount;

public QueryGeneratorResult(
String query,
String usedType,
int totalFieldCount,
boolean reachedMaxFieldCount
) {
this.query = query;
this.usedType = usedType;
this.totalFieldCount = totalFieldCount;
this.reachedMaxFieldCount = reachedMaxFieldCount;
}

/**
* Returns the generated query string.
*
* @return the query string
*/
public String getQuery() {
return query;
}

/**
* Returns the type that ultimately was used to generate the query.
*
* @return the used type
*/
public String getUsedType() {
return usedType;
}

/**
* Returns the total number of fields that were considered during query generation.
*
* @return the total field count
*/
public int getTotalFieldCount() {
return totalFieldCount;
}

/**
* Indicates whether the maximum field count was reached during query generation.
*
* @return true if the maximum field count was reached, false otherwise
*/
public boolean isReachedMaxFieldCount() {
return reachedMaxFieldCount;
}
}
Loading