Skip to content

Commit 5ef0f9f

Browse files
authored
Add optional feature for aggregate count query support on plural entity object types #488
* Add aggregate count query support * Apply prettier formatting * Fix regression caused by filtered embeddable attributes * update javadoc lint configuration to none * Add enable aggregate feature flag to builder class * Add more aggregate test coverage * Update group by field type to GraphQLObject scalar * Update VariableValue scalar to wrap null values with empty Optional * Implemented aggregate count for nested entity associations. * Apply prettier formatting * Refactor group aggregate count arguments
1 parent 4e0a854 commit 5ef0f9f

File tree

10 files changed

+1369
-61
lines changed

10 files changed

+1369
-61
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
<version>3.6.3</version>
221221
<configuration>
222222
<source>${java.version}</source>
223+
<doclint>none</doclint>
223224
</configuration>
224225
<executions>
225226
<execution>

schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ private static NoSuchElementException noSuchElementException(Class<?> containerC
441441
/**
442442
* Returns a String which capitalizes the first letter of the string.
443443
*/
444-
private static String capitalize(String name) {
444+
public static String capitalize(String name) {
445445
if (name == null || name.length() == 0) {
446446
return name;
447447
}

schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,28 @@
1919
import static com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder.PAGE_TOTAL_PARAM_NAME;
2020
import static com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder.QUERY_SELECT_PARAM_NAME;
2121
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.extractPageArgument;
22+
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.findArgument;
23+
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getAliasOrName;
24+
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getFields;
2225
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getPageArgument;
2326
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getSelectionField;
2427
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.searchByFieldName;
2528

29+
import com.introproventures.graphql.jpa.query.schema.JavaScalars;
30+
import graphql.GraphQLException;
2631
import graphql.language.Argument;
32+
import graphql.language.EnumValue;
2733
import graphql.language.Field;
2834
import graphql.schema.DataFetcher;
2935
import graphql.schema.DataFetchingEnvironment;
36+
import graphql.schema.GraphQLScalarType;
3037
import java.util.ArrayList;
38+
import java.util.Arrays;
39+
import java.util.LinkedHashMap;
3140
import java.util.List;
41+
import java.util.Map;
3242
import java.util.Optional;
43+
import java.util.stream.Stream;
3344
import org.slf4j.Logger;
3445
import org.slf4j.LoggerFactory;
3546

@@ -65,6 +76,7 @@ public PagedResult<Object> get(DataFetchingEnvironment environment) {
6576
Optional<Field> pagesSelection = getSelectionField(rootNode, PAGE_PAGES_PARAM_NAME);
6677
Optional<Field> totalSelection = getSelectionField(rootNode, PAGE_TOTAL_PARAM_NAME);
6778
Optional<Field> recordsSelection = searchByFieldName(rootNode, QUERY_SELECT_PARAM_NAME);
79+
Optional<Field> aggregateSelection = getSelectionField(rootNode, "aggregate");
6880

6981
final int firstResult = page.getOffset();
7082
final int maxResults = Integer.min(page.getLimit(), defaultMaxResults); // Limit max results to avoid OoM
@@ -98,9 +110,155 @@ public PagedResult<Object> get(DataFetchingEnvironment environment) {
98110
pagedResult.withTotal(total);
99111
}
100112

113+
aggregateSelection.ifPresent(aggregateField -> {
114+
Map<String, Object> aggregate = new LinkedHashMap<>();
115+
116+
getFields(aggregateField.getSelectionSet(), "count")
117+
.forEach(countField -> {
118+
getCountOfArgument(countField)
119+
.ifPresentOrElse(
120+
argument ->
121+
aggregate.put(
122+
getAliasOrName(countField),
123+
queryFactory.queryAggregateCount(argument, environment, restrictedKeys)
124+
),
125+
() ->
126+
aggregate.put(
127+
getAliasOrName(countField),
128+
queryFactory.queryTotalCount(environment, restrictedKeys)
129+
)
130+
);
131+
});
132+
133+
getFields(aggregateField.getSelectionSet(), "group")
134+
.forEach(groupField -> {
135+
var countField = getFields(groupField.getSelectionSet(), "count")
136+
.stream()
137+
.findFirst()
138+
.orElseThrow(() -> new GraphQLException("Missing aggregate count for group: " + groupField));
139+
140+
var countOfArgumentValue = getCountOfArgument(countField);
141+
142+
Map.Entry<String, String>[] groupings = getFields(groupField.getSelectionSet(), "by")
143+
.stream()
144+
.map(GraphQLJpaQueryDataFetcher::groupByFieldEntry)
145+
.toArray(Map.Entry[]::new);
146+
147+
if (groupings.length == 0) {
148+
throw new GraphQLException("At least one field is required for aggregate group: " + groupField);
149+
}
150+
151+
var resultList = queryFactory
152+
.queryAggregateGroupByCount(
153+
getAliasOrName(countField),
154+
countOfArgumentValue,
155+
environment,
156+
restrictedKeys,
157+
groupings
158+
)
159+
.stream()
160+
.peek(map ->
161+
Stream
162+
.of(groupings)
163+
.forEach(group -> {
164+
var value = map.get(group.getKey());
165+
166+
Optional
167+
.ofNullable(value)
168+
.map(Object::getClass)
169+
.map(JavaScalars::of)
170+
.map(GraphQLScalarType::getCoercing)
171+
.ifPresent(coercing -> map.put(group.getKey(), coercing.serialize(value)));
172+
})
173+
)
174+
.toList();
175+
176+
aggregate.put(getAliasOrName(groupField), resultList);
177+
});
178+
179+
aggregateField
180+
.getSelectionSet()
181+
.getSelections()
182+
.stream()
183+
.filter(Field.class::isInstance)
184+
.map(Field.class::cast)
185+
.filter(it -> !Arrays.asList("count", "group").contains(it.getName()))
186+
.forEach(groupField -> {
187+
var countField = getFields(groupField.getSelectionSet(), "count")
188+
.stream()
189+
.findFirst()
190+
.orElseThrow(() -> new GraphQLException("Missing aggregate count for group: " + groupField));
191+
192+
Map.Entry<String, String>[] groupings = getFields(groupField.getSelectionSet(), "by")
193+
.stream()
194+
.map(GraphQLJpaQueryDataFetcher::groupByFieldEntry)
195+
.toArray(Map.Entry[]::new);
196+
197+
if (groupings.length == 0) {
198+
throw new GraphQLException("At least one field is required for aggregate group: " + groupField);
199+
}
200+
201+
var resultList = queryFactory
202+
.queryAggregateGroupByAssociationCount(
203+
getAliasOrName(countField),
204+
groupField.getName(),
205+
environment,
206+
restrictedKeys,
207+
groupings
208+
)
209+
.stream()
210+
.peek(map ->
211+
Stream
212+
.of(groupings)
213+
.forEach(group -> {
214+
var value = map.get(group.getKey());
215+
216+
Optional
217+
.ofNullable(value)
218+
.map(Object::getClass)
219+
.map(JavaScalars::of)
220+
.map(GraphQLScalarType::getCoercing)
221+
.ifPresent(coercing -> map.put(group.getKey(), coercing.serialize(value)));
222+
})
223+
)
224+
.toList();
225+
226+
aggregate.put(getAliasOrName(groupField), resultList);
227+
});
228+
229+
pagedResult.withAggregate(aggregate);
230+
});
231+
101232
return pagedResult.build();
102233
}
103234

235+
static Map.Entry<String, String> groupByFieldEntry(Field selectedField) {
236+
String key = Optional.ofNullable(selectedField.getAlias()).orElse(selectedField.getName());
237+
238+
String value = findArgument(selectedField, "field")
239+
.map(Argument::getValue)
240+
.map(EnumValue.class::cast)
241+
.map(EnumValue::getName)
242+
.orElseThrow(() -> new GraphQLException("group by argument is required."));
243+
244+
return Map.entry(key, value);
245+
}
246+
247+
static Map.Entry<String, String> countFieldEntry(Field selectedField) {
248+
String key = Optional.ofNullable(selectedField.getAlias()).orElse(selectedField.getName());
249+
250+
String value = getCountOfArgument(selectedField).orElse(selectedField.getName());
251+
252+
return Map.entry(key, value);
253+
}
254+
255+
static Optional<String> getCountOfArgument(Field selectedField) {
256+
return findArgument(selectedField, "of")
257+
.map(Argument::getValue)
258+
.map(EnumValue.class::cast)
259+
.map(EnumValue::getName);
260+
}
261+
104262
public int getDefaultMaxResults() {
105263
return defaultMaxResults;
106264
}

0 commit comments

Comments
 (0)