Repository navigation
Expand file tree
/
Copy pathQueryGenerator.java
More file actions
147 lines (131 loc) · 7.23 KB
/
Copy pathQueryGenerator.java
File metadata and controls
147 lines (131 loc) · 7.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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);
}
}