| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
| 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, | ||
| @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; | ||
| } | ||
|
|
||
| } | ||
| } |
| Back | FazBrowse Home | New Git URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Qualityjavadoc
I am unsure what arguments and typeClassifier mean
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.