From e8c02036474f2ad0f3bef8b5ec05b91ab6f67a41 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sat, 25 May 2024 18:47:53 -0700 Subject: [PATCH 01/11] Add aggregate count query support --- .../query/schema/impl/EntityIntrospector.java | 2 +- .../impl/GraphQLJpaQueryDataFetcher.java | 97 ++++ .../schema/impl/GraphQLJpaQueryFactory.java | 128 ++++- .../schema/impl/GraphQLJpaSchemaBuilder.java | 185 ++++++-- .../jpa/query/schema/impl/PagedResult.java | 19 + .../jpa/query/support/GraphQLSupport.java | 22 + .../GraphQLJpaQueryAggregateTests.java | 439 ++++++++++++++++++ .../resources/GraphQLJpaAggregateTests.sql | 25 + .../jpa/query/example/Application.java | 12 +- 9 files changed, 876 insertions(+), 53 deletions(-) create mode 100644 schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java create mode 100644 schema/src/test/resources/GraphQLJpaAggregateTests.sql diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java index 64f382bb2..16778e80a 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java @@ -441,7 +441,7 @@ private static NoSuchElementException noSuchElementException(Class containerC /** * Returns a String which capitalizes the first letter of the string. */ - private static String capitalize(String name) { + public static String capitalize(String name) { if (name == null || name.length() == 0) { return name; } diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index 1c265f3f0..69af13dd1 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -19,17 +19,27 @@ import static com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder.PAGE_TOTAL_PARAM_NAME; import static com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder.QUERY_SELECT_PARAM_NAME; import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.extractPageArgument; +import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.findArgument; +import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getAliasOrName; +import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getFields; import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getPageArgument; import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getSelectionField; import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.searchByFieldName; +import com.introproventures.graphql.jpa.query.schema.JavaScalars; +import graphql.GraphQLException; import graphql.language.Argument; +import graphql.language.EnumValue; import graphql.language.Field; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLScalarType; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +75,7 @@ public PagedResult get(DataFetchingEnvironment environment) { Optional pagesSelection = getSelectionField(rootNode, PAGE_PAGES_PARAM_NAME); Optional totalSelection = getSelectionField(rootNode, PAGE_TOTAL_PARAM_NAME); Optional recordsSelection = searchByFieldName(rootNode, QUERY_SELECT_PARAM_NAME); + Optional aggregateSelection = getSelectionField(rootNode, "aggregate"); final int firstResult = page.getOffset(); final int maxResults = Integer.min(page.getLimit(), defaultMaxResults); // Limit max results to avoid OoM @@ -98,9 +109,95 @@ public PagedResult get(DataFetchingEnvironment environment) { pagedResult.withTotal(total); } + aggregateSelection.ifPresent(aggregateField -> { + Map aggregate = new LinkedHashMap<>(); + + getFields(aggregateField.getSelectionSet(), "count") + .forEach(countField -> { + getCountOfArgument(countField) + .ifPresentOrElse(argument -> + aggregate.put(getAliasOrName(countField), queryFactory.queryAggregateCount(argument, environment, restrictedKeys)) + , + () -> + aggregate.put(getAliasOrName(countField), queryFactory.queryTotalCount(environment, restrictedKeys)) + ); + }); + + getFields(aggregateField.getSelectionSet(), "group") + .forEach(groupField -> { + var countField = getFields(groupField.getSelectionSet(), "count") + .stream() + .findFirst() + .orElseThrow(() -> new GraphQLException("Missing aggregate count for group: " + groupField)); + + var countOfArgumentValue = getCountOfArgument(groupField); + + Map.Entry[] groupings = + getFields(groupField.getSelectionSet(), "by") + .stream() + .map(GraphQLJpaQueryDataFetcher::groupByFieldEntry) + .toArray(Map.Entry[]::new); + + if (groupings.length == 0) { + throw new GraphQLException("At least one field is required for aggregate group: " + groupField); + } + + var resultList = queryFactory.queryAggregateGroupByCount(getAliasOrName(countField), countOfArgumentValue, environment, restrictedKeys, groupings) + .stream() + .peek(map -> + Stream + .of(groupings) + .forEach(group -> { + var value = map.get(group.getKey()); + + Optional + .ofNullable(value) + .map(Object::getClass) + .map(JavaScalars::of) + .map(GraphQLScalarType::getCoercing) + .ifPresent(coercing -> map.put(group.getKey(), coercing.serialize(value))); + }) + ) + .toList(); + + aggregate.put(getAliasOrName(groupField), resultList); + }); + + pagedResult.withAggregate(aggregate); + }); + return pagedResult.build(); } + static Map.Entry groupByFieldEntry(Field selectedField) { + String key = Optional.ofNullable(selectedField.getAlias()).orElse(selectedField.getName()); + + String value = findArgument(selectedField, "field") + .map(Argument::getValue) + .map(EnumValue.class::cast) + .map(EnumValue::getName) + .orElseThrow(() -> new GraphQLException("group by argument is required.")); + + return Map.entry(key, value); + } + + static Map.Entry countFieldEntry(Field selectedField) { + String key = Optional.ofNullable(selectedField.getAlias()).orElse(selectedField.getName()); + + String value = getCountOfArgument(selectedField) + .orElse(selectedField.getName()); + + return Map.entry(key, value); + } + + static Optional getCountOfArgument(Field selectedField) { + return findArgument(selectedField, "of") + .map(Argument::getValue) + .map(EnumValue.class::cast) + .map(EnumValue::getName); + } + + public int getDefaultMaxResults() { return defaultMaxResults; } diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 68628eeeb..11eb75b9d 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -70,6 +70,7 @@ import jakarta.persistence.criteria.AbstractQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Fetch; import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Join; @@ -355,6 +356,54 @@ public Long queryTotalCount(DataFetchingEnvironment environment, Optional> restrictedKeys) { + final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); + + final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); + + if (restrictedKeys.isPresent()) { + TypedQuery countQuery = getAggregateCountQuery( + queryEnvironment, + queryEnvironment.getField(), + aggregate, + restrictedKeys.get() + ); + + if (logger.isDebugEnabled()) { + logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(countQuery)); + } + + return countQuery.getSingleResult(); + } + + return 0L; + } + + public List queryAggregateGroupByCount(String alias, Optional countOf, DataFetchingEnvironment environment, Optional> restrictedKeys, Map.Entry... groupings) { + final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); + + final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); + + if (restrictedKeys.isPresent()) { + TypedQuery countQuery = getAggregateGroupByCountQuery( + queryEnvironment, + queryEnvironment.getField(), + alias, + countOf, + restrictedKeys.get(), + groupings + ); + + if (logger.isDebugEnabled()) { + logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(countQuery)); + } + + return countQuery.getResultList(); + } + + return Collections.emptyList(); + } + protected TypedQuery getQuery( DataFetchingEnvironment environment, Field field, @@ -402,6 +451,83 @@ protected TypedQuery getCountQuery(DataFetchingEnvironment environment, Fi return entityManager.createQuery(query); } + protected TypedQuery getAggregateCountQuery(DataFetchingEnvironment environment, Field field, String aggregate, List keys, String... groupings) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Long.class); + Root root = query.from(entityType); + Join join = root.join(aggregate); + + DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder + .newDataFetchingEnvironment(environment) + .root(query) + .localContext(Boolean.FALSE) // Join mode + .build(); + + query.select(cb.count(join)); + + List predicates = field + .getArguments() + .stream() + .map(it -> getPredicate(field, cb, root, null, queryEnvironment, it)) + .filter(it -> it != null) + .collect(Collectors.toList()); + + if (!keys.isEmpty() && hasIdAttribute()) { + Predicate restrictions = root.get(idAttributeName()).in(keys); + predicates.add(restrictions); + } + + query.where(predicates.toArray(new Predicate[0])); + + return entityManager.createQuery(query); + } + + protected TypedQuery getAggregateGroupByCountQuery(DataFetchingEnvironment environment, Field field, String alias, Optional countOfJoin, List keys, Map.Entry... groupBy) { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = cb.createQuery(Map.class); + final Root root = query.from(entityType); + final DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder + .newDataFetchingEnvironment(environment) + .root(query) + .localContext(Boolean.FALSE) // Join mode + .build(); + + final List> selections = new ArrayList<>(); + + Stream.of(groupBy) + .map(group -> root.get(group.getValue()).alias(group.getKey())) + .forEach(selections::add); + + final Expression[] groupings = Stream + .of(groupBy) + .map(group -> root.get(group.getValue())) + .toArray(Expression[]::new); + + countOfJoin + .ifPresentOrElse( + it ->selections.add(cb.count(root.join(it)).alias(alias)), + () -> selections.add(cb.count(root).alias(alias)) + ); + + query.multiselect(selections).groupBy(groupings); + + List predicates = field + .getArguments() + .stream() + .map(it -> getPredicate(field, cb, root, null, queryEnvironment, it)) + .filter(it -> it != null) + .collect(Collectors.toList()); + + if (!keys.isEmpty() && hasIdAttribute()) { + Predicate restrictions = root.get(idAttributeName()).in(keys); + predicates.add(restrictions); + } + + query.where(predicates.toArray(new Predicate[0])); + + return entityManager.createQuery(query); + } + protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, Field field, List keys) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Object.class); @@ -1116,7 +1242,7 @@ protected Predicate getObjectFieldPredicate( .orElseGet(List::of); From context; - if (logicalArguments.stream().filter(it -> it.containsKey(objectField.getName())).count() > 1) { + if (logicalArguments.stream().filter(it -> it.containsKey(objectField.getName())).count() >= 1) { context = isOptional ? from.join(objectField.getName(), JoinType.LEFT) : from.join(objectField.getName()); } else { diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 53bda1acc..1105bf233 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -16,10 +16,17 @@ package com.introproventures.graphql.jpa.query.schema.impl; +import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getAliasOrName; import static graphql.Scalars.GraphQLBoolean; +import static graphql.Scalars.GraphQLInt; +import static graphql.Scalars.GraphQLString; import static graphql.schema.GraphQLArgument.newArgument; +import static graphql.schema.GraphQLEnumType.newEnum; +import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition; +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; import static graphql.schema.GraphQLInputObjectField.newInputObjectField; import static graphql.schema.GraphQLInputObjectType.newInputObject; +import static graphql.schema.GraphQLObjectType.newObject; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreFilter; @@ -203,8 +210,7 @@ public GraphQLJpaSchemaBuilder scalar(Class javaType, GraphQLScalarType scala } private GraphQLObjectType getQueryType() { - GraphQLObjectType.Builder queryType = GraphQLObjectType - .newObject() + GraphQLObjectType.Builder queryType = newObject() .name(queryTypeNameCustomizer.apply(this.name)) .description(this.description); @@ -232,8 +238,7 @@ private GraphQLObjectType getQueryType() { } private GraphQLObjectType getSubscriptionType() { - GraphQLObjectType.Builder queryType = GraphQLObjectType - .newObject() + GraphQLObjectType.Builder queryType = newObject() .name(this.name + "Subscription") .description(this.description); @@ -269,8 +274,7 @@ private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType entityT String fieldName = singularize.andThen(queryByIdFieldNameCustomizer).apply(entityType.getName()); - return GraphQLFieldDefinition - .newFieldDefinition() + return newFieldDefinition() .name(enableRelay ? Introspector.decapitalize(fieldName) : fieldName) .description(getSchemaDescription(entityType)) .type(entityObjectType) @@ -338,8 +342,7 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType entit String fieldName = pluralize.andThen(queryAllFieldNameCustomizer).apply(entityType.getName()); - GraphQLFieldDefinition.Builder fieldDefinition = GraphQLFieldDefinition - .newFieldDefinition() + GraphQLFieldDefinition.Builder fieldDefinition = newFieldDefinition() .name(enableRelay ? Introspector.decapitalize(fieldName) : fieldName) .description( "Query request wrapper for " + @@ -377,8 +380,7 @@ private GraphQLObjectType getSelectType(EntityType entityType) { final GraphQLObjectType selectObjectType = getEntityObjectType(entityType); final var selectTypeName = resolveSelectTypeName(entityType); - GraphQLObjectType selectPagedResultType = GraphQLObjectType - .newObject() + GraphQLObjectType selectPagedResultType = newObject() .name(selectTypeName) .description( "Query response wrapper object for " + @@ -386,34 +388,123 @@ private GraphQLObjectType getSelectType(EntityType entityType) { ". When page is requested, this object will be returned with query metadata." ) .field( - GraphQLFieldDefinition - .newFieldDefinition() + newFieldDefinition() .name(GraphQLJpaSchemaBuilder.PAGE_PAGES_PARAM_NAME) .description("Total number of pages calculated on the database for this page size.") .type(JavaScalars.of(Long.class)) .build() ) .field( - GraphQLFieldDefinition - .newFieldDefinition() + newFieldDefinition() .name(GraphQLJpaSchemaBuilder.PAGE_TOTAL_PARAM_NAME) .description("Total number of records in the database for this query.") .type(JavaScalars.of(Long.class)) .build() ) .field( - GraphQLFieldDefinition - .newFieldDefinition() + newFieldDefinition() .name(GraphQLJpaSchemaBuilder.QUERY_SELECT_PARAM_NAME) .description("The queried records container") .type(new GraphQLList(selectObjectType)) .build() ) + .field(getAggregateFieldDefinition(entityType)) .build(); return selectPagedResultType; } + private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityType) { + final var selectTypeName = resolveSelectTypeName(entityType); + final var aggregateObjectTypeName = selectTypeName.concat("Aggregate"); + + var aggregateObjectType = newObject().name(aggregateObjectTypeName); + + DataFetcher aggregateDataFetcher = environment -> { + Map source = environment.getSource(); + + return source.get(getAliasOrName(environment.getField())); + }; + + var countFieldDefinition = newFieldDefinition() + .name("count") + .dataFetcher(aggregateDataFetcher) + .type(GraphQLInt); + + var associationEnumValueDefinitions = entityType + .getAttributes() + .stream() + .filter(it -> EntityIntrospector.introspect(entityType).isNotIgnored(it.getName())) + .filter(Attribute::isAssociation) + .map(Attribute::getName) + .map(name -> newEnumValueDefinition().name(name).build()) + .toList(); + + var fieldsEnumValueDefinitions = entityType + .getAttributes() + .stream() + .filter(it -> EntityIntrospector.introspect(entityType).isNotIgnored(it.getName())) + .filter(this::isBasic) + .map(Attribute::getName) + .map(name -> newEnumValueDefinition().name(name).build()) + .toList(); + + if (entityType.getAttributes() + .stream() + .anyMatch(Attribute::isAssociation)) { + countFieldDefinition + .argument(newArgument() + .name("of") + .type(newEnum() + .name(aggregateObjectTypeName.concat("CountOfAssociationsEnum")) + .values(associationEnumValueDefinitions) + .build())); + } + + + var groupFieldDefinition = newFieldDefinition() + .name("group") + .dataFetcher(aggregateDataFetcher) + .type(new GraphQLList(newObject() + .name(aggregateObjectTypeName.concat("GroupBy")) + .field(newFieldDefinition() + .name("by") + .dataFetcher(aggregateDataFetcher) + .argument(newArgument() + .name("field") + .type(newEnum() + .name(aggregateObjectTypeName.concat("GroupByFieldsEnum")) + .values(fieldsEnumValueDefinitions) + .build())) + .type(GraphQLString)) + .field(newFieldDefinition() + .name("count") + .type(GraphQLInt)) + .build())); + + if (entityType.getAttributes() + .stream() + .anyMatch(Attribute::isAssociation)) { + groupFieldDefinition + .argument(newArgument() + .name("of") + .type(newEnum() + .name(aggregateObjectTypeName.concat("GroupOfAssociationsEnum")) + .values(associationEnumValueDefinitions) + .build())); + } + + aggregateObjectType + .field(countFieldDefinition) + .field(groupFieldDefinition); + + var aggregateFieldDefinition = newFieldDefinition() + .name("aggregate") + .type(aggregateObjectType); + + return aggregateFieldDefinition.build(); + } + private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType entityType) { GraphQLObjectType entityObjectType = getEntityObjectType(entityType); @@ -433,8 +524,7 @@ private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType entit DataFetcher dataFetcher = GraphQLJpaStreamDataFetcher.builder().withQueryFactory(queryFactory).build(); var fieldName = pluralize.andThen(queryResultTypeNameCustomizer).apply(entityType.getName()); - GraphQLFieldDefinition.Builder fieldDefinition = GraphQLFieldDefinition - .newFieldDefinition() + GraphQLFieldDefinition.Builder fieldDefinition = newFieldDefinition() .name(fieldName) .description( "Query request wrapper for " + @@ -1038,8 +1128,7 @@ private GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean .apply(embeddableType.getJavaType().getSimpleName()); graphQLType = - GraphQLObjectType - .newObject() + newObject() .name(embeddableTypeName) .description(getSchemaDescription(embeddableType)) .fields( @@ -1068,8 +1157,7 @@ private String resolveEntityObjectTypeName(EntityType entityType) { private GraphQLObjectType computeEntityObjectType(EntityType entityType) { var typeName = resolveEntityObjectTypeName(entityType); - return GraphQLObjectType - .newObject() + return newObject() .name(typeName) .description(getSchemaDescription(entityType)) .fields(getEntityAttributesFields(entityType)) @@ -1103,8 +1191,7 @@ private GraphQLFieldDefinition getJavaFieldDefinition(AttributePropertyDescripto String description = propertyDescriptor.getSchemaDescription().orElse(null); - return GraphQLFieldDefinition - .newFieldDefinition() + return newFieldDefinition() .name(propertyDescriptor.getName()) .description(description) .type(type) @@ -1124,10 +1211,7 @@ private GraphQLFieldDefinition getObjectField(Attribute attribute, EntityType ba DataFetcher dataFetcher = PropertyDataFetcher.fetching(attribute.getName()); // Only add the orderBy argument for basic attribute types - if ( - attribute instanceof SingularAttribute && - attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC && - isNotIgnoredOrder(attribute) + if (isBasic(attribute) && isNotIgnoredOrder(attribute) ) { arguments.add( GraphQLArgument @@ -1141,10 +1225,7 @@ private GraphQLFieldDefinition getObjectField(Attribute attribute, EntityType ba } // Get the fields that can be queried on (i.e. Simple Types, no Sub-Objects) - if ( - attribute instanceof SingularAttribute && - attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC - ) { + if (isSingular(attribute)) { ManagedType foreignType = getForeignType(attribute); SingularAttribute singularAttribute = SingularAttribute.class.cast(attribute); @@ -1154,8 +1235,7 @@ private GraphQLFieldDefinition getObjectField(Attribute attribute, EntityType ba // to-one end could be optional arguments.add(optionalArgument(singularAttribute.isOptional())); - GraphQLObjectType entityObjectType = GraphQLObjectType - .newObject() + GraphQLObjectType entityObjectType = newObject() .name(resolveEntityObjectTypeName(baseEntity)) .build(); @@ -1181,13 +1261,7 @@ private GraphQLFieldDefinition getObjectField(Attribute attribute, EntityType ba dataFetcher = new GraphQLJpaToOneDataFetcher(graphQLJpaQueryFactory, (SingularAttribute) attribute); } // Get Sub-Objects fields queries via DataFetcher - else if ( - attribute instanceof PluralAttribute && - ( - attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY || - attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY - ) - ) { + else if (isPlural(attribute)) { Assert.assertNotNull( baseEntity, () -> "For attribute " + attribute.getName() + " cannot find declaring type!" @@ -1199,8 +1273,7 @@ else if ( // make it configurable via builder api arguments.add(optionalArgument(toManyDefaultOptional)); - GraphQLObjectType entityObjectType = GraphQLObjectType - .newObject() + GraphQLObjectType entityObjectType = newObject() .name(resolveEntityObjectTypeName(baseEntity)) .build(); @@ -1227,8 +1300,7 @@ else if ( dataFetcher = new GraphQLJpaToManyDataFetcher(graphQLJpaQueryFactory, (PluralAttribute) attribute); } - return GraphQLFieldDefinition - .newFieldDefinition() + return newFieldDefinition() .name(attribute.getName()) .description(getSchemaDescription(attribute)) .type(type) @@ -1348,7 +1420,8 @@ protected final boolean isEmbeddable(Attribute attribute) { } protected final boolean isBasic(Attribute attribute) { - return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC; + return attribute instanceof SingularAttribute && + attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC; } protected final boolean isElementCollection(Attribute attribute) { @@ -1373,6 +1446,19 @@ protected final boolean isToOne(Attribute attribute) { ); } + private boolean isPlural(Attribute attribute) { + return attribute instanceof PluralAttribute && + ( + attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY || + attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY + ); + } + + private boolean isSingular(Attribute attribute) { + return attribute instanceof SingularAttribute && + attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC; + } + protected final boolean isValidInput(Attribute attribute) { return ( attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC || @@ -1459,7 +1545,7 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { if (clazz.isEnum()) { if (classCache.containsKey(clazz)) return classCache.get(clazz); - GraphQLEnumType.Builder enumBuilder = GraphQLEnumType.newEnum().name(clazz.getSimpleName()); + GraphQLEnumType.Builder enumBuilder = newEnum().name(clazz.getSimpleName()); int ordinal = 0; for (Enum enumValue : ((Class>) clazz).getEnumConstants()) enumBuilder.value(enumValue.name()); @@ -1476,7 +1562,7 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { } protected GraphQLInputType getFieldsEnumType(EntityType entityType) { - GraphQLEnumType.Builder enumBuilder = GraphQLEnumType.newEnum().name(entityType.getName() + "FieldsEnum"); + GraphQLEnumType.Builder enumBuilder = newEnum().name(entityType.getName() + "FieldsEnum"); final AtomicInteger ordinal = new AtomicInteger(); entityType @@ -1517,8 +1603,7 @@ protected GraphQLInputType getFieldsEnumType(EntityType entityType) { ) .build(); - private static final GraphQLEnumType orderByDirectionEnum = GraphQLEnumType - .newEnum() + private static final GraphQLEnumType orderByDirectionEnum = newEnum() .name("OrderBy") .description("Specifies the direction (Ascending / Descending) to sort a field.") .value("ASC", "ASC", "Ascending") diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PagedResult.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PagedResult.java index 5b29cf24e..e41c9202d 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PagedResult.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PagedResult.java @@ -17,7 +17,9 @@ package com.introproventures.graphql.jpa.query.schema.impl; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class PagedResult { @@ -26,6 +28,7 @@ public class PagedResult { private final long pages; private final int offset; private final List select; + private final Map aggregate; private PagedResult(Builder builder) { this.limit = builder.limit; @@ -33,6 +36,7 @@ private PagedResult(Builder builder) { this.offset = builder.offset; this.select = builder.select; this.pages = ((Double) Math.ceil(total / (double) limit)).longValue(); + this.aggregate = builder.aggregate; } public Long getTotal() { @@ -55,6 +59,10 @@ public long getLimit() { return limit; } + public Map getAggregate() { + return aggregate; + } + /** * Creates builder to build {@link PagedResult}. * @return created builder @@ -73,6 +81,7 @@ public static final class Builder { private long pages; private int offset; private List select = Collections.emptyList(); + private Map aggregate = new LinkedHashMap<>(); private Builder() {} @@ -126,6 +135,16 @@ public Builder withSelect(List select) { return this; } + /** + * Builder method for select parameter. + * @param select field to set + * @return builder + */ + public Builder withAggregate(Map aggregate) { + this.aggregate.putAll(aggregate); + return this; + } + /** * Builder method of the builder. * @return built class diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java index fcd091149..9fd0e8df8 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java @@ -136,6 +136,28 @@ public static final Optional getSelectionField(Field field, String fieldN return GraphQLSupport.fields(field.getSelectionSet()).filter(it -> fieldName.equals(it.getName())).findFirst(); } + public static Optional findArgument(Field selectedField, String name) { + return Optional + .ofNullable(selectedField.getArguments()) + .flatMap(arguments -> arguments + .stream() + .filter(argument -> name.equals(argument.getName())) + .findFirst()); + } + + public static List getFields(SelectionSet selections, String fieldName) { + return selections + .getSelections() + .stream() + .map(Field.class::cast) + .filter(it -> fieldName.equals(it.getName())) + .collect(Collectors.toList()); + } + + public static String getAliasOrName(Field field) { + return Optional.ofNullable(field.getAlias()).orElse(field.getName()); + } + public static Collector, List> toResultList() { return Collector.of( ArrayList::new, diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java new file mode 100644 index 000000000..2ac847bd2 --- /dev/null +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java @@ -0,0 +1,439 @@ +/* + * Copyright 2017 IntroPro Ventures Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.introproventures.graphql.jpa.query.converter; + +import static graphql.schema.GraphQLScalarType.newScalar; +import static org.assertj.core.api.Assertions.assertThat; + +import com.introproventures.graphql.jpa.query.AbstractSpringBootTestSupport; +import com.introproventures.graphql.jpa.query.converter.model.TaskEntity; +import com.introproventures.graphql.jpa.query.converter.model.TaskVariableEntity; +import com.introproventures.graphql.jpa.query.converter.model.VariableValue; +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.schema.GraphQLSchemaBuilder; +import com.introproventures.graphql.jpa.query.schema.JavaScalars; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; +import graphql.ExecutionResult; +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Selection; +import jakarta.transaction.Transactional; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; + +@SpringBootTest( + properties = { + "spring.sql.init.data-locations=GraphQLJpaAggregateTests.sql", + "spring.datasource.url=jdbc:h2:mem:db;NON_KEYWORDS=VALUE", + } +) +public class GraphQLJpaQueryAggregateTests extends AbstractSpringBootTestSupport { + + @SpringBootApplication + static class Application { + + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + return new GraphQLJpaSchemaBuilder(entityManager) + .name("CustomAttributeConverterSchema") + .description("Custom Attribute Converter Schema") + .scalar(VariableValue.class, + newScalar() + .name("VariableValue") + .coercing(new JavaScalars.GraphQLObjectCoercing() { + public Object serialize(final Object input) { + return Optional + .ofNullable(input) + .filter(VariableValue.class::isInstance) + .map(VariableValue.class::cast) + .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) + .orElse(input); + } + }) + .build()); + + } + } + + @Autowired + private GraphQLExecutor executor; + + @Autowired + private EntityManager entityManager; + + @Test + public void contextLoads() {} + + @Test + @Transactional + public void criteriaTesterAggregateCountByName() { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Object.class); + Root taskVariable = query.from(TaskVariableEntity.class); + var taskJoin = taskVariable.join("task"); + + Selection[] selections = List.of(taskVariable.get("name"),cb.count(taskJoin)).toArray(Selection[]::new); + Expression[] groupings = List.of(taskVariable.get("name")).toArray(Expression[]::new); + + query.multiselect(selections).groupBy(groupings); + + // when: + List result = entityManager.createQuery(query).getResultList(); + + // then: + assertThat(result) + .isNotEmpty() + .hasSize(7) + .contains(new Object[]{"variable1", 1L}, + new Object[]{"variable2", 1L}, + new Object[]{"variable3", 1L}, + new Object[]{"variable4", 1L}, + new Object[]{"variable5", 2L}, + new Object[]{"variable6", 1L}, + new Object[]{"variable7", 1L}); + } + + @Test + @Transactional + public void criteriaTesterAggregateCountByNameMapProjection() { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Map.class); + Root taskVariable = query.from(TaskVariableEntity.class); + var taskJoin = taskVariable.join("task"); + + Selection[] selections = List.of(taskVariable.get("name").alias("by"),cb.count(taskJoin).alias("count")).toArray(Selection[]::new); + Expression[] groupings = List.of(taskVariable.get("name")).toArray(Expression[]::new); + + query.multiselect(selections).groupBy(groupings); + + // when: + List result = entityManager.createQuery(query).getResultList(); + + // then: + assertThat(result) + .isNotEmpty() + .hasSize(7) + .contains(Map.of("by", "variable1", "count", 1L), + Map.of("by", "variable2", "count", 1L), + Map.of("by", "variable3", "count", 1L), + Map.of("by", "variable4", "count", 1L), + Map.of("by", "variable5", "count", 2L), + Map.of("by", "variable6", "count", 1L), + Map.of("by", "variable7", "count", 1L)); + } + + @Test + @Transactional + public void criteriaTesterTaskAggregateCount() { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Object.class); + Root tasks = query.from(TaskEntity.class); + //var variablesJoin = tasks.join("variables"); + + Selection[] selections = List.of(cb.count(tasks)).toArray(Selection[]::new); + Expression[] groupings = List.of().toArray(Expression[]::new); + + query.multiselect(selections).groupBy(groupings); + + // when: + List result = entityManager.createQuery(query).getResultList(); + + // then: + assertThat(result) + .isNotEmpty() + .hasSize(1) + .contains(6L); + } + + @Test + @Transactional + public void criteriaTesterTaskVariablesAggregateCount() { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Object.class); + Root tasks = query.from(TaskEntity.class); + var variablesJoin = tasks.join("variables"); + + Selection[] selections = List.of(cb.count(variablesJoin)).toArray(Selection[]::new); + Expression[] groupings = List.of().toArray(Expression[]::new); + + query.distinct(true).multiselect(selections).groupBy(groupings); + + // when: + List result = entityManager.createQuery(query).getResultList(); + + // then: + assertThat(result) + .isNotEmpty() + .hasSize(1) + .contains(8L); + } + + @Test + @Transactional + public void criteriaTesterTaskVariablesAggregateCountByName() { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Object.class); + Root tasks = query.from(TaskEntity.class); + var variablesJoin = tasks.join("variables"); + + Selection[] selections = List.of(variablesJoin.get("name"), cb.count(variablesJoin)).toArray(Selection[]::new); + Expression[] groupings = List.of(variablesJoin.get("name")).toArray(Expression[]::new); + + query.distinct(true).multiselect(selections).groupBy(groupings); + + // when: + List result = entityManager.createQuery(query).getResultList(); + + // then: + assertThat(result) + .isNotEmpty() + .hasSize(7) + .contains(new Object[]{"variable1", 1L}, + new Object[]{"variable2", 1L}, + new Object[]{"variable3", 1L}, + new Object[]{"variable4", 1L}, + new Object[]{"variable5", 2L}, + new Object[]{"variable6", 1L}, + new Object[]{"variable7", 1L}); + } + + @Test + public void queryTasksVariablesWhereWithExplicitANDByMultipleNameAndValueCriteria2() { + //given + String query = + "query {" + + " Tasks(where: {" + + " status: {EQ: COMPLETED}" + + " AND: [" + + " { " + + " variables: {" + + " name: {EQ: \"variable1\"}" + + " value: {EQ: \"data\"}" + + " }" + + " }" + + " ]" + + " }) {" + + " select {" + + " id" + + " status" + + " variables(where: {name: {IN: [\"variable2\",\"variable1\"]}} ) {" + + " name" + + " value" + + " }" + + " }" + + " }" + + "}"; + + String expected = + "{Tasks={select=[" + + "{id=1, status=COMPLETED, variables=[" + + "{name=variable1, value=data}, " + + "{name=variable2, value=true}]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryTasksVariablesAggregateCount() { + //given + String query = """ + query { + Tasks( + where: { + status: { EQ: COMPLETED } + } + ) { + select { + id + status + } + aggregate { + count + } + } + } + """; + + String expected = + "{Tasks={select=[{id=1, status=COMPLETED}, {id=5, status=COMPLETED}], aggregate={count=2}}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryTasksVariablesNestedAggregateCount() { + //given + String query = """ + query { + Tasks( + where: { + status: { EQ: COMPLETED } + } + ) { + select { + id + status + } + aggregate { + count + variables: count(of: variables) + } + } + } + """; + + String expected = + "{Tasks={select=[{id=1, status=COMPLETED}, {id=5, status=COMPLETED}], aggregate={count=2, variables=2}}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryVariablesTaskNestedAggregateCount() { + //given + String query = """ + query { + TaskVariables { + aggregate { + count + tasks: count(of: task) + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={count=8, tasks=8}}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryVariablesTaskNestedAggregateCountWhere() { + //given + String query = """ + query { + TaskVariables(where:{task: {status: {EQ: COMPLETED}}}) { + aggregate { + count + tasks: count(of: task) + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={count=2, tasks=2}}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryVariablesTaskNestedAggregateCountGroupBy() { + //given + String query = """ + query { + TaskVariables { + aggregate { + # Aggregate by group of fields + group { + by(field: name) + count + } + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={group=[{by=variable1, count=1}, {by=variable2, count=1}, {by=variable3, count=1}, {by=variable4, count=1}, {by=variable5, count=2}, {by=variable6, count=1}, {by=variable7, count=1}]}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getData().toString()).isEqualTo(expected); + } + + @Test + public void queryVariablesTaskNestedAggregateCountGroupByMultipleFields() { + //given + String query = """ + query { + TaskVariables { + aggregate { + # Aggregate by group of fields + group { + name: by(field: name) + value: by(field: value) + count + } + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={group=[{name=variable1, value=data, count=1}, {name=variable2, value=true, count=1}, {name=variable3, value=null, count=1}, {name=variable4, value={key=data}, count=1}, {name=variable5, value=1.2345, count=2}, {name=variable6, value=12345, count=1}, {name=variable7, value=[1, 2, 3, 4, 5], count=1}]}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getData().toString()).isEqualTo(expected); + } + + +} diff --git a/schema/src/test/resources/GraphQLJpaAggregateTests.sql b/schema/src/test/resources/GraphQLJpaAggregateTests.sql new file mode 100644 index 000000000..4d3b554e3 --- /dev/null +++ b/schema/src/test/resources/GraphQLJpaAggregateTests.sql @@ -0,0 +1,25 @@ +-- Json entity +insert into json_entity (id, first_name, last_name, attributes) values + (1, 'john', 'doe', '{"attr":{"key":["1","2","3","4","5"]}}'), + (2, 'joe', 'smith', '{"attr":["1","2","3","4","5"]}'); + +insert into task (id, assignee, business_key, created_date, description, due_date, last_modified, last_modified_from, last_modified_to, name, priority, process_definition_id, process_instance_id, status, owner, claimed_date) values +('1', 'assignee', 'bk1', CURRENT_TIMESTAMP, 'description', null, null, null, null, 'task1', 5, 'process_definition_id', 0, 'COMPLETED' , 'owner', null), +('2', 'assignee', null, CURRENT_TIMESTAMP, 'description', null, null, null, null, 'task2', 10, 'process_definition_id', 0, 'CREATED' , 'owner', null), +('3', 'assignee', null, CURRENT_TIMESTAMP, 'description', null, null, null, null, 'task3', 5, 'process_definition_id', 0, 'CREATED' , 'owner', null), +('4', 'assignee', null, CURRENT_TIMESTAMP, 'description', null, null, null, null, 'task4', 10, 'process_definition_id', 1, 'CREATED' , 'owner', null), +('5', 'assignee', null, CURRENT_TIMESTAMP, 'description', null, null, null, null, 'task5', 10, 'process_definition_id', 1, 'COMPLETED' , 'owner', null), +('6', 'assignee', 'bk6', CURRENT_TIMESTAMP, 'description', null, null, null, null, 'task6', 10, 'process_definition_id', 0, 'ASSIGNED' , 'owner', null); + +insert into PROCESS_VARIABLE (create_time, execution_id, last_updated_time, name, process_instance_id, type, value) values + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'document', 1, 'json', '{"value":{"key":["1","2","3","4","5"]}}'); + +insert into TASK_VARIABLE (create_time, execution_id, last_updated_time, name, process_instance_id, task_id, type, value) values + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable1', 0, '1', 'string', '{"value":"data"}'), + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable2', 0, '1', 'boolean', '{"value":true}'), + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable3', 0, '2', 'null', '{"value":null}'), + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable4', 0, '2', 'json', '{"value":{"key":"data"}}'), + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable5', 1, '3', 'double', '{"value":1.2345}'), + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable5', 1, '4', 'double', '{"value":1.2345}'), + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable6', 1, '4', 'int', '{"value":12345}'), + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable7', 1, '4', 'json', '{"value":[1,2,3,4,5]}'); diff --git a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java index 28396f7a2..6a9dcbb26 100644 --- a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java +++ b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java @@ -21,6 +21,7 @@ import com.introproventures.graphql.jpa.query.autoconfigure.GraphQLJPASchemaBuilderCustomizer; import com.introproventures.graphql.jpa.query.schema.JavaScalars; import java.util.Date; +import java.util.Optional; import org.activiti.cloud.services.query.model.ProcessInstanceEntity; import org.activiti.cloud.services.query.model.VariableValue; import org.springframework.beans.factory.annotation.Value; @@ -54,7 +55,16 @@ GraphQLJPASchemaBuilderCustomizer graphQLJPASchemaBuilderCustomizer( newScalar() .name("VariableValue") .description("VariableValue type") - .coercing(new JavaScalars.GraphQLObjectCoercing()) + .coercing(new JavaScalars.GraphQLObjectCoercing() { + public Object serialize(final Object input) { + return Optional + .ofNullable(input) + .filter(VariableValue.class::isInstance) + .map(VariableValue.class::cast) + .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) + .orElse(input); + } + }) .build() ) .scalar( From 363c1413426402acf5258ff07bdf4892adec5002 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sat, 25 May 2024 19:03:30 -0700 Subject: [PATCH 02/11] Apply prettier formatting --- .../impl/GraphQLJpaQueryDataFetcher.java | 36 ++-- .../schema/impl/GraphQLJpaQueryFactory.java | 48 +++-- .../schema/impl/GraphQLJpaSchemaBuilder.java | 124 ++++++------- .../jpa/query/support/GraphQLSupport.java | 5 +- .../GraphQLJpaQueryAggregateTests.java | 164 +++++++++--------- .../jpa/query/example/Application.java | 20 ++- 6 files changed, 220 insertions(+), 177 deletions(-) diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index 69af13dd1..b3cc0cbf4 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -115,11 +115,17 @@ public PagedResult get(DataFetchingEnvironment environment) { getFields(aggregateField.getSelectionSet(), "count") .forEach(countField -> { getCountOfArgument(countField) - .ifPresentOrElse(argument -> - aggregate.put(getAliasOrName(countField), queryFactory.queryAggregateCount(argument, environment, restrictedKeys)) - , + .ifPresentOrElse( + argument -> + aggregate.put( + getAliasOrName(countField), + queryFactory.queryAggregateCount(argument, environment, restrictedKeys) + ), () -> - aggregate.put(getAliasOrName(countField), queryFactory.queryTotalCount(environment, restrictedKeys)) + aggregate.put( + getAliasOrName(countField), + queryFactory.queryTotalCount(environment, restrictedKeys) + ) ); }); @@ -132,17 +138,23 @@ public PagedResult get(DataFetchingEnvironment environment) { var countOfArgumentValue = getCountOfArgument(groupField); - Map.Entry[] groupings = - getFields(groupField.getSelectionSet(), "by") - .stream() - .map(GraphQLJpaQueryDataFetcher::groupByFieldEntry) - .toArray(Map.Entry[]::new); + Map.Entry[] groupings = getFields(groupField.getSelectionSet(), "by") + .stream() + .map(GraphQLJpaQueryDataFetcher::groupByFieldEntry) + .toArray(Map.Entry[]::new); if (groupings.length == 0) { throw new GraphQLException("At least one field is required for aggregate group: " + groupField); } - var resultList = queryFactory.queryAggregateGroupByCount(getAliasOrName(countField), countOfArgumentValue, environment, restrictedKeys, groupings) + var resultList = queryFactory + .queryAggregateGroupByCount( + getAliasOrName(countField), + countOfArgumentValue, + environment, + restrictedKeys, + groupings + ) .stream() .peek(map -> Stream @@ -184,8 +196,7 @@ static Map.Entry groupByFieldEntry(Field selectedField) { static Map.Entry countFieldEntry(Field selectedField) { String key = Optional.ofNullable(selectedField.getAlias()).orElse(selectedField.getName()); - String value = getCountOfArgument(selectedField) - .orElse(selectedField.getName()); + String value = getCountOfArgument(selectedField).orElse(selectedField.getName()); return Map.entry(key, value); } @@ -197,7 +208,6 @@ static Optional getCountOfArgument(Field selectedField) { .map(EnumValue::getName); } - public int getDefaultMaxResults() { return defaultMaxResults; } diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 11eb75b9d..536d281b9 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -356,7 +356,11 @@ public Long queryTotalCount(DataFetchingEnvironment environment, Optional> restrictedKeys) { + public Long queryAggregateCount( + String aggregate, + DataFetchingEnvironment environment, + Optional> restrictedKeys + ) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); @@ -379,7 +383,13 @@ public Long queryAggregateCount(String aggregate, DataFetchingEnvironment enviro return 0L; } - public List queryAggregateGroupByCount(String alias, Optional countOf, DataFetchingEnvironment environment, Optional> restrictedKeys, Map.Entry... groupings) { + public List queryAggregateGroupByCount( + String alias, + Optional countOf, + DataFetchingEnvironment environment, + Optional> restrictedKeys, + Map.Entry... groupings + ) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); @@ -451,11 +461,17 @@ protected TypedQuery getCountQuery(DataFetchingEnvironment environment, Fi return entityManager.createQuery(query); } - protected TypedQuery getAggregateCountQuery(DataFetchingEnvironment environment, Field field, String aggregate, List keys, String... groupings) { + protected TypedQuery getAggregateCountQuery( + DataFetchingEnvironment environment, + Field field, + String aggregate, + List keys, + String... groupings + ) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Long.class); Root root = query.from(entityType); - Join join = root.join(aggregate); + Join join = root.join(aggregate); DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder .newDataFetchingEnvironment(environment) @@ -482,7 +498,14 @@ protected TypedQuery getAggregateCountQuery(DataFetchingEnvironment enviro return entityManager.createQuery(query); } - protected TypedQuery getAggregateGroupByCountQuery(DataFetchingEnvironment environment, Field field, String alias, Optional countOfJoin, List keys, Map.Entry... groupBy) { + protected TypedQuery getAggregateGroupByCountQuery( + DataFetchingEnvironment environment, + Field field, + String alias, + Optional countOfJoin, + List keys, + Map.Entry... groupBy + ) { final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery query = cb.createQuery(Map.class); final Root root = query.from(entityType); @@ -494,20 +517,17 @@ protected TypedQuery getAggregateGroupByCountQuery(DataFetchingEnvironment final List> selections = new ArrayList<>(); - Stream.of(groupBy) - .map(group -> root.get(group.getValue()).alias(group.getKey())) - .forEach(selections::add); + Stream.of(groupBy).map(group -> root.get(group.getValue()).alias(group.getKey())).forEach(selections::add); final Expression[] groupings = Stream .of(groupBy) - .map(group -> root.get(group.getValue())) + .map(group -> root.get(group.getValue())) .toArray(Expression[]::new); - countOfJoin - .ifPresentOrElse( - it ->selections.add(cb.count(root.join(it)).alias(alias)), - () -> selections.add(cb.count(root).alias(alias)) - ); + countOfJoin.ifPresentOrElse( + it -> selections.add(cb.count(root.join(it)).alias(alias)), + () -> selections.add(cb.count(root).alias(alias)) + ); query.multiselect(selections).groupBy(groupings); diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 1105bf233..2f8139654 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -449,58 +449,63 @@ private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityT .map(name -> newEnumValueDefinition().name(name).build()) .toList(); - if (entityType.getAttributes() - .stream() - .anyMatch(Attribute::isAssociation)) { - countFieldDefinition - .argument(newArgument() + if (entityType.getAttributes().stream().anyMatch(Attribute::isAssociation)) { + countFieldDefinition.argument( + newArgument() .name("of") - .type(newEnum() - .name(aggregateObjectTypeName.concat("CountOfAssociationsEnum")) - .values(associationEnumValueDefinitions) - .build())); + .type( + newEnum() + .name(aggregateObjectTypeName.concat("CountOfAssociationsEnum")) + .values(associationEnumValueDefinitions) + .build() + ) + ); } - var groupFieldDefinition = newFieldDefinition() .name("group") .dataFetcher(aggregateDataFetcher) - .type(new GraphQLList(newObject() - .name(aggregateObjectTypeName.concat("GroupBy")) - .field(newFieldDefinition() - .name("by") - .dataFetcher(aggregateDataFetcher) - .argument(newArgument() - .name("field") - .type(newEnum() - .name(aggregateObjectTypeName.concat("GroupByFieldsEnum")) - .values(fieldsEnumValueDefinitions) - .build())) - .type(GraphQLString)) - .field(newFieldDefinition() - .name("count") - .type(GraphQLInt)) - .build())); - - if (entityType.getAttributes() - .stream() - .anyMatch(Attribute::isAssociation)) { - groupFieldDefinition - .argument(newArgument() + .type( + new GraphQLList( + newObject() + .name(aggregateObjectTypeName.concat("GroupBy")) + .field( + newFieldDefinition() + .name("by") + .dataFetcher(aggregateDataFetcher) + .argument( + newArgument() + .name("field") + .type( + newEnum() + .name(aggregateObjectTypeName.concat("GroupByFieldsEnum")) + .values(fieldsEnumValueDefinitions) + .build() + ) + ) + .type(GraphQLString) + ) + .field(newFieldDefinition().name("count").type(GraphQLInt)) + .build() + ) + ); + + if (entityType.getAttributes().stream().anyMatch(Attribute::isAssociation)) { + groupFieldDefinition.argument( + newArgument() .name("of") - .type(newEnum() - .name(aggregateObjectTypeName.concat("GroupOfAssociationsEnum")) - .values(associationEnumValueDefinitions) - .build())); + .type( + newEnum() + .name(aggregateObjectTypeName.concat("GroupOfAssociationsEnum")) + .values(associationEnumValueDefinitions) + .build() + ) + ); } - aggregateObjectType - .field(countFieldDefinition) - .field(groupFieldDefinition); + aggregateObjectType.field(countFieldDefinition).field(groupFieldDefinition); - var aggregateFieldDefinition = newFieldDefinition() - .name("aggregate") - .type(aggregateObjectType); + var aggregateFieldDefinition = newFieldDefinition().name("aggregate").type(aggregateObjectType); return aggregateFieldDefinition.build(); } @@ -1211,8 +1216,7 @@ private GraphQLFieldDefinition getObjectField(Attribute attribute, EntityType ba DataFetcher dataFetcher = PropertyDataFetcher.fetching(attribute.getName()); // Only add the orderBy argument for basic attribute types - if (isBasic(attribute) && isNotIgnoredOrder(attribute) - ) { + if (isBasic(attribute) && isNotIgnoredOrder(attribute)) { arguments.add( GraphQLArgument .newArgument() @@ -1235,9 +1239,7 @@ private GraphQLFieldDefinition getObjectField(Attribute attribute, EntityType ba // to-one end could be optional arguments.add(optionalArgument(singularAttribute.isOptional())); - GraphQLObjectType entityObjectType = newObject() - .name(resolveEntityObjectTypeName(baseEntity)) - .build(); + GraphQLObjectType entityObjectType = newObject().name(resolveEntityObjectTypeName(baseEntity)).build(); GraphQLJpaQueryFactory graphQLJpaQueryFactory = GraphQLJpaQueryFactory .builder() @@ -1273,9 +1275,7 @@ else if (isPlural(attribute)) { // make it configurable via builder api arguments.add(optionalArgument(toManyDefaultOptional)); - GraphQLObjectType entityObjectType = newObject() - .name(resolveEntityObjectTypeName(baseEntity)) - .build(); + GraphQLObjectType entityObjectType = newObject().name(resolveEntityObjectTypeName(baseEntity)).build(); GraphQLJpaQueryFactory graphQLJpaQueryFactory = GraphQLJpaQueryFactory .builder() @@ -1420,8 +1420,10 @@ protected final boolean isEmbeddable(Attribute attribute) { } protected final boolean isBasic(Attribute attribute) { - return attribute instanceof SingularAttribute && - attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC; + return ( + attribute instanceof SingularAttribute && + attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC + ); } protected final boolean isElementCollection(Attribute attribute) { @@ -1446,17 +1448,21 @@ protected final boolean isToOne(Attribute attribute) { ); } - private boolean isPlural(Attribute attribute) { - return attribute instanceof PluralAttribute && + private boolean isPlural(Attribute attribute) { + return ( + attribute instanceof PluralAttribute && ( attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY || - attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY - ); + attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY + ) + ); } - private boolean isSingular(Attribute attribute) { - return attribute instanceof SingularAttribute && - attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC; + private boolean isSingular(Attribute attribute) { + return ( + attribute instanceof SingularAttribute && + attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC + ); } protected final boolean isValidInput(Attribute attribute) { diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java index 9fd0e8df8..eb42ed5f6 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java @@ -139,10 +139,7 @@ public static final Optional getSelectionField(Field field, String fieldN public static Optional findArgument(Field selectedField, String name) { return Optional .ofNullable(selectedField.getArguments()) - .flatMap(arguments -> arguments - .stream() - .filter(argument -> name.equals(argument.getName())) - .findFirst()); + .flatMap(arguments -> arguments.stream().filter(argument -> name.equals(argument.getName())).findFirst()); } public static List getFields(SelectionSet selections, String fieldName) { diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java index 2ac847bd2..7da12529b 100644 --- a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java @@ -66,21 +66,24 @@ public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManag return new GraphQLJpaSchemaBuilder(entityManager) .name("CustomAttributeConverterSchema") .description("Custom Attribute Converter Schema") - .scalar(VariableValue.class, + .scalar( + VariableValue.class, newScalar() .name("VariableValue") - .coercing(new JavaScalars.GraphQLObjectCoercing() { - public Object serialize(final Object input) { - return Optional - .ofNullable(input) - .filter(VariableValue.class::isInstance) - .map(VariableValue.class::cast) - .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) - .orElse(input); + .coercing( + new JavaScalars.GraphQLObjectCoercing() { + public Object serialize(final Object input) { + return Optional + .ofNullable(input) + .filter(VariableValue.class::isInstance) + .map(VariableValue.class::cast) + .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) + .orElse(input); + } } - }) - .build()); - + ) + .build() + ); } } @@ -101,7 +104,7 @@ public void criteriaTesterAggregateCountByName() { Root taskVariable = query.from(TaskVariableEntity.class); var taskJoin = taskVariable.join("task"); - Selection[] selections = List.of(taskVariable.get("name"),cb.count(taskJoin)).toArray(Selection[]::new); + Selection[] selections = List.of(taskVariable.get("name"), cb.count(taskJoin)).toArray(Selection[]::new); Expression[] groupings = List.of(taskVariable.get("name")).toArray(Expression[]::new); query.multiselect(selections).groupBy(groupings); @@ -113,13 +116,15 @@ public void criteriaTesterAggregateCountByName() { assertThat(result) .isNotEmpty() .hasSize(7) - .contains(new Object[]{"variable1", 1L}, - new Object[]{"variable2", 1L}, - new Object[]{"variable3", 1L}, - new Object[]{"variable4", 1L}, - new Object[]{"variable5", 2L}, - new Object[]{"variable6", 1L}, - new Object[]{"variable7", 1L}); + .contains( + new Object[] { "variable1", 1L }, + new Object[] { "variable2", 1L }, + new Object[] { "variable3", 1L }, + new Object[] { "variable4", 1L }, + new Object[] { "variable5", 2L }, + new Object[] { "variable6", 1L }, + new Object[] { "variable7", 1L } + ); } @Test @@ -130,7 +135,9 @@ public void criteriaTesterAggregateCountByNameMapProjection() { Root taskVariable = query.from(TaskVariableEntity.class); var taskJoin = taskVariable.join("task"); - Selection[] selections = List.of(taskVariable.get("name").alias("by"),cb.count(taskJoin).alias("count")).toArray(Selection[]::new); + Selection[] selections = List + .of(taskVariable.get("name").alias("by"), cb.count(taskJoin).alias("count")) + .toArray(Selection[]::new); Expression[] groupings = List.of(taskVariable.get("name")).toArray(Expression[]::new); query.multiselect(selections).groupBy(groupings); @@ -142,13 +149,15 @@ public void criteriaTesterAggregateCountByNameMapProjection() { assertThat(result) .isNotEmpty() .hasSize(7) - .contains(Map.of("by", "variable1", "count", 1L), + .contains( + Map.of("by", "variable1", "count", 1L), Map.of("by", "variable2", "count", 1L), Map.of("by", "variable3", "count", 1L), Map.of("by", "variable4", "count", 1L), Map.of("by", "variable5", "count", 2L), Map.of("by", "variable6", "count", 1L), - Map.of("by", "variable7", "count", 1L)); + Map.of("by", "variable7", "count", 1L) + ); } @Test @@ -168,10 +177,7 @@ public void criteriaTesterTaskAggregateCount() { List result = entityManager.createQuery(query).getResultList(); // then: - assertThat(result) - .isNotEmpty() - .hasSize(1) - .contains(6L); + assertThat(result).isNotEmpty().hasSize(1).contains(6L); } @Test @@ -191,10 +197,7 @@ public void criteriaTesterTaskVariablesAggregateCount() { List result = entityManager.createQuery(query).getResultList(); // then: - assertThat(result) - .isNotEmpty() - .hasSize(1) - .contains(8L); + assertThat(result).isNotEmpty().hasSize(1).contains(8L); } @Test @@ -205,7 +208,9 @@ public void criteriaTesterTaskVariablesAggregateCountByName() { Root tasks = query.from(TaskEntity.class); var variablesJoin = tasks.join("variables"); - Selection[] selections = List.of(variablesJoin.get("name"), cb.count(variablesJoin)).toArray(Selection[]::new); + Selection[] selections = List + .of(variablesJoin.get("name"), cb.count(variablesJoin)) + .toArray(Selection[]::new); Expression[] groupings = List.of(variablesJoin.get("name")).toArray(Expression[]::new); query.distinct(true).multiselect(selections).groupBy(groupings); @@ -217,13 +222,15 @@ public void criteriaTesterTaskVariablesAggregateCountByName() { assertThat(result) .isNotEmpty() .hasSize(7) - .contains(new Object[]{"variable1", 1L}, - new Object[]{"variable2", 1L}, - new Object[]{"variable3", 1L}, - new Object[]{"variable4", 1L}, - new Object[]{"variable5", 2L}, - new Object[]{"variable6", 1L}, - new Object[]{"variable7", 1L}); + .contains( + new Object[] { "variable1", 1L }, + new Object[] { "variable2", 1L }, + new Object[] { "variable3", 1L }, + new Object[] { "variable4", 1L }, + new Object[] { "variable5", 2L }, + new Object[] { "variable6", 1L }, + new Object[] { "variable7", 1L } + ); } @Test @@ -231,34 +238,34 @@ public void queryTasksVariablesWhereWithExplicitANDByMultipleNameAndValueCriteri //given String query = "query {" + - " Tasks(where: {" + - " status: {EQ: COMPLETED}" + - " AND: [" + - " { " + - " variables: {" + - " name: {EQ: \"variable1\"}" + - " value: {EQ: \"data\"}" + - " }" + - " }" + - " ]" + - " }) {" + - " select {" + - " id" + - " status" + - " variables(where: {name: {IN: [\"variable2\",\"variable1\"]}} ) {" + - " name" + - " value" + - " }" + - " }" + - " }" + - "}"; + " Tasks(where: {" + + " status: {EQ: COMPLETED}" + + " AND: [" + + " { " + + " variables: {" + + " name: {EQ: \"variable1\"}" + + " value: {EQ: \"data\"}" + + " }" + + " }" + + " ]" + + " }) {" + + " select {" + + " id" + + " status" + + " variables(where: {name: {IN: [\"variable2\",\"variable1\"]}} ) {" + + " name" + + " value" + + " }" + + " }" + + " }" + + "}"; String expected = "{Tasks={select=[" + - "{id=1, status=COMPLETED, variables=[" + - "{name=variable1, value=data}, " + - "{name=variable2, value=true}]}" + - "]}}"; + "{id=1, status=COMPLETED, variables=[" + + "{name=variable1, value=data}, " + + "{name=variable2, value=true}]}" + + "]}}"; //when Object result = executor.execute(query).getData(); @@ -270,7 +277,8 @@ public void queryTasksVariablesWhereWithExplicitANDByMultipleNameAndValueCriteri @Test public void queryTasksVariablesAggregateCount() { //given - String query = """ + String query = + """ query { Tasks( where: { @@ -288,8 +296,7 @@ public void queryTasksVariablesAggregateCount() { } """; - String expected = - "{Tasks={select=[{id=1, status=COMPLETED}, {id=5, status=COMPLETED}], aggregate={count=2}}}"; + String expected = "{Tasks={select=[{id=1, status=COMPLETED}, {id=5, status=COMPLETED}], aggregate={count=2}}}"; //when Object result = executor.execute(query).getData(); @@ -301,7 +308,8 @@ public void queryTasksVariablesAggregateCount() { @Test public void queryTasksVariablesNestedAggregateCount() { //given - String query = """ + String query = + """ query { Tasks( where: { @@ -333,7 +341,8 @@ public void queryTasksVariablesNestedAggregateCount() { @Test public void queryVariablesTaskNestedAggregateCount() { //given - String query = """ + String query = + """ query { TaskVariables { aggregate { @@ -344,8 +353,7 @@ public void queryVariablesTaskNestedAggregateCount() { } """; - String expected = - "{TaskVariables={aggregate={count=8, tasks=8}}}"; + String expected = "{TaskVariables={aggregate={count=8, tasks=8}}}"; //when Object result = executor.execute(query).getData(); @@ -357,7 +365,8 @@ public void queryVariablesTaskNestedAggregateCount() { @Test public void queryVariablesTaskNestedAggregateCountWhere() { //given - String query = """ + String query = + """ query { TaskVariables(where:{task: {status: {EQ: COMPLETED}}}) { aggregate { @@ -368,8 +377,7 @@ public void queryVariablesTaskNestedAggregateCountWhere() { } """; - String expected = - "{TaskVariables={aggregate={count=2, tasks=2}}}"; + String expected = "{TaskVariables={aggregate={count=2, tasks=2}}}"; //when Object result = executor.execute(query).getData(); @@ -381,7 +389,8 @@ public void queryVariablesTaskNestedAggregateCountWhere() { @Test public void queryVariablesTaskNestedAggregateCountGroupBy() { //given - String query = """ + String query = + """ query { TaskVariables { aggregate { @@ -409,7 +418,8 @@ public void queryVariablesTaskNestedAggregateCountGroupBy() { @Test public void queryVariablesTaskNestedAggregateCountGroupByMultipleFields() { //given - String query = """ + String query = + """ query { TaskVariables { aggregate { @@ -434,6 +444,4 @@ public void queryVariablesTaskNestedAggregateCountGroupByMultipleFields() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getData().toString()).isEqualTo(expected); } - - } diff --git a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java index 6a9dcbb26..c82cd4f63 100644 --- a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java +++ b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java @@ -55,16 +55,18 @@ GraphQLJPASchemaBuilderCustomizer graphQLJPASchemaBuilderCustomizer( newScalar() .name("VariableValue") .description("VariableValue type") - .coercing(new JavaScalars.GraphQLObjectCoercing() { - public Object serialize(final Object input) { - return Optional - .ofNullable(input) - .filter(VariableValue.class::isInstance) - .map(VariableValue.class::cast) - .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) - .orElse(input); + .coercing( + new JavaScalars.GraphQLObjectCoercing() { + public Object serialize(final Object input) { + return Optional + .ofNullable(input) + .filter(VariableValue.class::isInstance) + .map(VariableValue.class::cast) + .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) + .orElse(input); + } } - }) + ) .build() ) .scalar( From e321de5baa6e108599c2109d10053d64252a2551 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sat, 25 May 2024 19:37:14 -0700 Subject: [PATCH 03/11] Fix regression caused by filtered embeddable attributes --- .../graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 2f8139654..881f24e2f 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -444,7 +444,7 @@ private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityT .getAttributes() .stream() .filter(it -> EntityIntrospector.introspect(entityType).isNotIgnored(it.getName())) - .filter(this::isBasic) + .filter(it -> isBasic(it) || isEmbeddable(it)) .map(Attribute::getName) .map(name -> newEnumValueDefinition().name(name).build()) .toList(); From 59791931c0dd9d037ec5f4ae9d16e104101c09e9 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sat, 25 May 2024 19:51:52 -0700 Subject: [PATCH 04/11] update javadoc lint configuration to none --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 7a52c09a9..4b10070b7 100644 --- a/pom.xml +++ b/pom.xml @@ -220,6 +220,7 @@ 3.6.3 ${java.version} + none From 11cf949f5c44dde46a3702168479ec950a66b01f Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 26 May 2024 07:52:40 -0700 Subject: [PATCH 05/11] Add enable aggregate feature flag to builder class --- .../schema/impl/GraphQLJpaSchemaBuilder.java | 19 ++++++++++++++----- .../GraphQLJpaQueryAggregateTests.java | 1 + .../jpa/query/example/Application.java | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 881f24e2f..e83bb34c2 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -147,6 +147,7 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { private boolean toManyDefaultOptional = true; // the many end is a collection, and it is always optional by default (empty collection) private boolean enableSubscription = false; // experimental private boolean enableRelay = false; // experimental + private boolean enableAggregate = false; // experimental private int defaultMaxResults = 100; private int defaultFetchSize = 100; private int defaultPageLimitSize = 100; @@ -380,7 +381,7 @@ private GraphQLObjectType getSelectType(EntityType entityType) { final GraphQLObjectType selectObjectType = getEntityObjectType(entityType); final var selectTypeName = resolveSelectTypeName(entityType); - GraphQLObjectType selectPagedResultType = newObject() + var selectPagedResultType = newObject() .name(selectTypeName) .description( "Query response wrapper object for " + @@ -407,11 +408,13 @@ private GraphQLObjectType getSelectType(EntityType entityType) { .description("The queried records container") .type(new GraphQLList(selectObjectType)) .build() - ) - .field(getAggregateFieldDefinition(entityType)) - .build(); + ); + + if (enableAggregate) { + selectPagedResultType.field(getAggregateFieldDefinition(entityType)); + } - return selectPagedResultType; + return selectPagedResultType.build(); } private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityType) { @@ -1685,6 +1688,12 @@ public void setNamingStrategy(NamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; } + public GraphQLSchemaBuilder enableAggregate(boolean enableAggregate) { + this.enableAggregate = enableAggregate; + + return this; + } + static class NoOpCoercing implements Coercing { @Override diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java index 7da12529b..25965bbd2 100644 --- a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java @@ -66,6 +66,7 @@ public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManag return new GraphQLJpaSchemaBuilder(entityManager) .name("CustomAttributeConverterSchema") .description("Custom Attribute Converter Schema") + .enableAggregate(true) .scalar( VariableValue.class, newScalar() diff --git a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java index c82cd4f63..451e3fdf8 100644 --- a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java +++ b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java @@ -50,6 +50,7 @@ GraphQLJPASchemaBuilderCustomizer graphQLJPASchemaBuilderCustomizer( builder .name("Query") .description("Activiti Cloud Query Schema") + .enableAggregate(true) .scalar( VariableValue.class, newScalar() From 0a161ff25ffd557b1f41c3032e717e3f9cd90b9e Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 26 May 2024 15:34:24 -0700 Subject: [PATCH 06/11] Add more aggregate test coverage --- .../GraphQLJpaQueryAggregateTests.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java index 25965bbd2..3d2ae176c 100644 --- a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java @@ -29,6 +29,7 @@ import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; import graphql.ExecutionResult; +import graphql.GraphQLError; import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; @@ -445,4 +446,145 @@ public void queryVariablesTaskNestedAggregateCountGroupByMultipleFields() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getData().toString()).isEqualTo(expected); } + + @Test + public void queryVariablesTaskNestedAggregateCountGroupByEmptyFields() { + //given + String query = + """ + query { + TaskVariables { + aggregate { + # Aggregate by group of fields + group { + count + } + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={group=[{name=variable1, value=data, count=1}, {name=variable2, value=true, count=1}, {name=variable3, value=null, count=1}, {name=variable4, value={key=data}, count=1}, {name=variable5, value=1.2345, count=2}, {name=variable6, value=12345, count=1}, {name=variable7, value=[1, 2, 3, 4, 5], count=1}]}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()) + .isNotEmpty() + .extracting(GraphQLError::getMessage) + .anyMatch(message -> message.contains("At least one field is required for aggregate group")); + } + + @Test + public void queryVariablesTaskNestedAggregateCountGroupByMissingCount() { + //given + String query = + """ + query { + TaskVariables { + aggregate { + # Aggregate by group of fields + group { + by(field: name) + } + } + } + } + """; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()) + .isNotEmpty() + .extracting(GraphQLError::getMessage) + .anyMatch(message -> message.contains("Missing aggregate count for group")); + } + + @Test + public void queryVariablesTaskNestedAggregateCountByMultipleFields() { + //given + String query = + """ + query { + TaskVariables( + # Apply filter criteria + where: {name: {IN: ["variable1", "variable5"]}} + ) { + aggregate { + # count by variables + variables: count + # Count by associated tasks + tasks: count(of: task) + } + } + } + """; + + String expected = "{TaskVariables={aggregate={variables=3, tasks=3}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getData().toString()).isEqualTo(expected); + } + + @Test + public void queryVariablesTaskNestedAggregateCountByMultipleEntities() { + //given + String query = + """ + query { + TaskVariables + # Apply filter criteria + (where: {name: {IN: ["variable1", "variable5"]}}) + { + aggregate { + # count by variables + totalVariables: count + # Count by associated tasks + totalTasks: count(of: task) + # Group by task variable entity fields + groupByNameValue: group { + # Use aliases to group by multiple fields + name: by(field: name) + value: by(field: value) + # Count aggregate + count + } + # Group by associated tasks + groupTasksByVariableName: group(of: task) { + variable: by(field: name) + count + } + } + } + Tasks { + aggregate { + totalTasks: count + totalVariables: count(of: variables) + groupByStatus: group { + status: by(field: status) + count + } + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={totalVariables=3, totalTasks=3, groupByNameValue=[{name=variable1, value=data, count=1}, {name=variable5, value=1.2345, count=2}], groupTasksByVariableName=[{variable=variable1, count=1}, {variable=variable5, count=2}]}}, Tasks={aggregate={totalTasks=6, totalVariables=8, groupByStatus=[{status=ASSIGNED, count=1}, {status=COMPLETED, count=2}, {status=CREATED, count=3}]}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getData().toString()).isEqualTo(expected); + } } From c3269e89cb035074fda30059723845cac0105a25 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 26 May 2024 16:59:48 -0700 Subject: [PATCH 07/11] Update group by field type to GraphQLObject scalar --- .../graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index e83bb34c2..7fc9b973a 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -19,7 +19,6 @@ import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getAliasOrName; import static graphql.Scalars.GraphQLBoolean; import static graphql.Scalars.GraphQLInt; -import static graphql.Scalars.GraphQLString; import static graphql.schema.GraphQLArgument.newArgument; import static graphql.schema.GraphQLEnumType.newEnum; import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition; @@ -486,7 +485,7 @@ private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityT .build() ) ) - .type(GraphQLString) + .type(JavaScalars.GraphQLObjectScalar) ) .field(newFieldDefinition().name("count").type(GraphQLInt)) .build() From 8f7f759dddc4b17b102cb65ebca4ade92eaaa21d Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 26 May 2024 17:00:37 -0700 Subject: [PATCH 08/11] Update VariableValue scalar to wrap null values with empty Optional --- .../jpa/query/converter/GraphQLJpaQueryAggregateTests.java | 2 +- .../introproventures/graphql/jpa/query/example/Application.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java index 3d2ae176c..2cc71d9b8 100644 --- a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java @@ -79,7 +79,7 @@ public Object serialize(final Object input) { .ofNullable(input) .filter(VariableValue.class::isInstance) .map(VariableValue.class::cast) - .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) + .map(it -> Optional.ofNullable(it.getValue()).orElse(Optional.empty())) .orElse(input); } } diff --git a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java index 451e3fdf8..ba69d37d5 100644 --- a/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java +++ b/tests/gatling/src/main/java/com/introproventures/graphql/jpa/query/example/Application.java @@ -63,7 +63,7 @@ public Object serialize(final Object input) { .ofNullable(input) .filter(VariableValue.class::isInstance) .map(VariableValue.class::cast) - .map(it -> Optional.ofNullable(it.getValue()).orElse("null")) + .map(it -> Optional.ofNullable(it.getValue()).orElse(Optional.empty())) .orElse(input); } } From b5f62d2e4dbf0fa749bff1e76d59b1fecd688d9b Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 26 May 2024 19:58:52 -0700 Subject: [PATCH 09/11] Implemented aggregate count for nested entity associations. --- .../impl/GraphQLJpaQueryDataFetcher.java | 52 ++++++++ .../schema/impl/GraphQLJpaQueryFactory.java | 80 +++++++++++++ .../schema/impl/GraphQLJpaSchemaBuilder.java | 49 ++++++++ .../GraphQLJpaQueryAggregateTests.java | 111 ++++++++++++++++++ 4 files changed, 292 insertions(+) diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index b3cc0cbf4..5f9aa3440 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -35,6 +35,7 @@ import graphql.schema.DataFetchingEnvironment; import graphql.schema.GraphQLScalarType; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -175,6 +176,57 @@ public PagedResult get(DataFetchingEnvironment environment) { aggregate.put(getAliasOrName(groupField), resultList); }); + aggregateField + .getSelectionSet() + .getSelections() + .stream() + .filter(Field.class::isInstance) + .map(Field.class::cast ) + .filter(it -> !Arrays.asList("count","group").contains(it.getName())) + .forEach(groupField -> { + var countField = getFields(groupField.getSelectionSet(), "count") + .stream() + .findFirst() + .orElseThrow(() -> new GraphQLException("Missing aggregate count for group: " + groupField)); + + Map.Entry[] groupings = getFields(groupField.getSelectionSet(), "by") + .stream() + .map(GraphQLJpaQueryDataFetcher::groupByFieldEntry) + .toArray(Map.Entry[]::new); + + if (groupings.length == 0) { + throw new GraphQLException("At least one field is required for aggregate group: " + groupField); + } + + var resultList = queryFactory + .queryAggregateGroupByAssociationCount( + getAliasOrName(countField), + groupField.getName(), + environment, + restrictedKeys, + groupings + ) + .stream() + .peek(map -> + Stream + .of(groupings) + .forEach(group -> { + var value = map.get(group.getKey()); + + Optional + .ofNullable(value) + .map(Object::getClass) + .map(JavaScalars::of) + .map(GraphQLScalarType::getCoercing) + .ifPresent(coercing -> map.put(group.getKey(), coercing.serialize(value))); + }) + ) + .toList(); + + aggregate.put(getAliasOrName(groupField), resultList); + + }); + pagedResult.withAggregate(aggregate); }); diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 536d281b9..83f8322df 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -414,6 +414,37 @@ public List queryAggregateGroupByCount( return Collections.emptyList(); } + public List queryAggregateGroupByAssociationCount( + String countAlias, + String association, + DataFetchingEnvironment environment, + Optional> restrictedKeys, + Map.Entry... groupings + ) { + final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); + + final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); + + if (restrictedKeys.isPresent()) { + TypedQuery countQuery = getAggregateGroupByAssociationCountQuery( + queryEnvironment, + queryEnvironment.getField(), + countAlias, + association, + restrictedKeys.get(), + groupings + ); + + if (logger.isDebugEnabled()) { + logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(countQuery)); + } + + return countQuery.getResultList(); + } + + return Collections.emptyList(); + } + protected TypedQuery getQuery( DataFetchingEnvironment environment, Field field, @@ -548,6 +579,55 @@ protected TypedQuery getAggregateGroupByCountQuery( return entityManager.createQuery(query); } + protected TypedQuery getAggregateGroupByAssociationCountQuery( + DataFetchingEnvironment environment, + Field field, + String countAlias, + String association, + List keys, + Map.Entry... groupBy + ) { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = cb.createQuery(Map.class); + final Root root = query.from(entityType); + final Join join = root.join(association); + + final DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder + .newDataFetchingEnvironment(environment) + .root(query) + .localContext(Boolean.FALSE) // Join mode + .build(); + + final List> selections = new ArrayList<>(); + + Stream.of(groupBy).map(group -> join.get(group.getValue()).alias(group.getKey())).forEach(selections::add); + + selections.add(cb.count(join).alias(countAlias)); + + final Expression[] groupings = Stream + .of(groupBy) + .map(group -> join.get(group.getValue())) + .toArray(Expression[]::new); + + query.multiselect(selections).groupBy(groupings); + + List predicates = field + .getArguments() + .stream() + .map(it -> getPredicate(field, cb, root, null, queryEnvironment, it)) + .filter(it -> it != null) + .collect(Collectors.toList()); + + if (!keys.isEmpty() && hasIdAttribute()) { + Predicate restrictions = root.get(idAttributeName()).in(keys); + predicates.add(restrictions); + } + + query.where(predicates.toArray(new Predicate[0])); + + return entityManager.createQuery(query); + } + protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, Field field, List keys) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Object.class); diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 7fc9b973a..ce0198fcb 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -16,6 +16,7 @@ package com.introproventures.graphql.jpa.query.schema.impl; +import static com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.capitalize; import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.getAliasOrName; import static graphql.Scalars.GraphQLBoolean; import static graphql.Scalars.GraphQLInt; @@ -505,6 +506,54 @@ private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityT ); } + entityType + .getAttributes() + .stream() + .filter(it -> EntityIntrospector.introspect(entityType).isNotIgnored(it.getName())) + .filter(Attribute::isAssociation) + .forEach(association -> { + var javaType = isPlural(association) ? PluralAttribute.class.cast(association).getBindableJavaType() : association.getJavaType(); + var attributes = EntityIntrospector.resultOf(javaType).getAttributes(); + var fields = attributes + .values() + .stream() + .filter(it -> isBasic(it) || isEmbeddable(it)) + .map(Attribute::getName) + .map(name -> newEnumValueDefinition().name(name).build()) + .toList(); + + if (!fields.isEmpty()) { + aggregateObjectType.field( + newFieldDefinition() + .name(association.getName()) + .dataFetcher(aggregateDataFetcher) + .type(new GraphQLList( + newObject() + .name(aggregateObjectTypeName.concat(capitalize(association.getName())).concat("GroupByNestedAssociation")) + .field( newFieldDefinition() + .name("by") + .dataFetcher(aggregateDataFetcher) + .argument( + newArgument() + .name("field") + .type( + newEnum() + .name(aggregateObjectTypeName.concat(capitalize(association.getName())).concat("GroupByNestedAssociationEnum")) + .values(fields) + .build() + ) + ) + .type(JavaScalars.GraphQLObjectScalar) + ) + .field(newFieldDefinition().name("count").type(GraphQLInt)) + .build() + ) + ) + ); + } + }); + + aggregateObjectType.field(countFieldDefinition).field(groupFieldDefinition); var aggregateFieldDefinition = newFieldDefinition().name("aggregate").type(aggregateObjectType); diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java index 2cc71d9b8..5be4692de 100644 --- a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java @@ -587,4 +587,115 @@ public void queryVariablesTaskNestedAggregateCountByMultipleEntities() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getData().toString()).isEqualTo(expected); } + + @Test + public void queryVariablesTaskNestedAggregateCountByNestedAssociation() { + //given + String query = + """ + query { + TaskVariables( + # Apply filter criteria + where: {name: {IN: ["variable1", "variable5"]}} + ) { + aggregate { + # count by variables + variables: count + # Count by associated tasks + tasks: task { + by(field: status) + count + } + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={variables=3, tasks=[{by=COMPLETED, count=1}, {by=CREATED, count=2}]}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getData().toString()).isEqualTo(expected); + } + + @Test + public void queryVariablesTaskNestedAggregateCountByNestedAssociationAlias() { + //given + String query = + """ + query { + TaskVariables( + # Apply filter criteria + where: {name: {IN: ["variable1", "variable5"]}} + ) { + aggregate { + # count by variables + variables: count + # Count by associated tasks + tasks: task { + status: by(field: status) + count + } + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={variables=3, tasks=[{status=COMPLETED, count=1}, {status=CREATED, count=2}]}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getData().toString()).isEqualTo(expected); + } + + @Test + public void queryVariablesTaskNestedAggregateCountByNestedAssociationMultipleAliases() { + //given + String query = + """ + query { + TaskVariables( + # Apply filter criteria + where: {name: {IN: ["variable1", "variable5"]}} + ) { + aggregate { + # count by variables + variables: count + # Count by associated tasks + groupByVariableName: group { + name: by(field: name) + count + } + groupByTaskStatus: task { + status: by(field: status) + count + } + # Count by associated tasks + groupByTaskAssignee: task { + assignee: by(field: assignee) + count + } + } + } + } + """; + + String expected = + "{TaskVariables={aggregate={variables=3, groupByVariableName=[{name=variable1, count=1}, {name=variable5, count=2}], groupByTaskStatus=[{status=COMPLETED, count=1}, {status=CREATED, count=2}], groupByTaskAssignee=[{assignee=assignee, count=3}]}}}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getData().toString()).isEqualTo(expected); + } } From 3bec49e5f2b0913a6be91e8db2c748756eff97a6 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 26 May 2024 19:59:35 -0700 Subject: [PATCH 10/11] Apply prettier formatting --- .../impl/GraphQLJpaQueryDataFetcher.java | 5 +- .../schema/impl/GraphQLJpaSchemaBuilder.java | 57 +++++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index 5f9aa3440..2ae8459cd 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -181,8 +181,8 @@ public PagedResult get(DataFetchingEnvironment environment) { .getSelections() .stream() .filter(Field.class::isInstance) - .map(Field.class::cast ) - .filter(it -> !Arrays.asList("count","group").contains(it.getName())) + .map(Field.class::cast) + .filter(it -> !Arrays.asList("count", "group").contains(it.getName())) .forEach(groupField -> { var countField = getFields(groupField.getSelectionSet(), "count") .stream() @@ -224,7 +224,6 @@ public PagedResult get(DataFetchingEnvironment environment) { .toList(); aggregate.put(getAliasOrName(groupField), resultList); - }); pagedResult.withAggregate(aggregate); diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index ce0198fcb..2bac87c43 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -512,9 +512,11 @@ private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityT .filter(it -> EntityIntrospector.introspect(entityType).isNotIgnored(it.getName())) .filter(Attribute::isAssociation) .forEach(association -> { - var javaType = isPlural(association) ? PluralAttribute.class.cast(association).getBindableJavaType() : association.getJavaType(); + var javaType = isPlural(association) + ? PluralAttribute.class.cast(association).getBindableJavaType() + : association.getJavaType(); var attributes = EntityIntrospector.resultOf(javaType).getAttributes(); - var fields = attributes + var fields = attributes .values() .stream() .filter(it -> isBasic(it) || isEmbeddable(it)) @@ -527,33 +529,42 @@ private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityT newFieldDefinition() .name(association.getName()) .dataFetcher(aggregateDataFetcher) - .type(new GraphQLList( - newObject() - .name(aggregateObjectTypeName.concat(capitalize(association.getName())).concat("GroupByNestedAssociation")) - .field( newFieldDefinition() - .name("by") - .dataFetcher(aggregateDataFetcher) - .argument( - newArgument() - .name("field") - .type( - newEnum() - .name(aggregateObjectTypeName.concat(capitalize(association.getName())).concat("GroupByNestedAssociationEnum")) - .values(fields) - .build() - ) - ) - .type(JavaScalars.GraphQLObjectScalar) + .type( + new GraphQLList( + newObject() + .name( + aggregateObjectTypeName + .concat(capitalize(association.getName())) + .concat("GroupByNestedAssociation") + ) + .field( + newFieldDefinition() + .name("by") + .dataFetcher(aggregateDataFetcher) + .argument( + newArgument() + .name("field") + .type( + newEnum() + .name( + aggregateObjectTypeName + .concat(capitalize(association.getName())) + .concat("GroupByNestedAssociationEnum") + ) + .values(fields) + .build() + ) + ) + .type(JavaScalars.GraphQLObjectScalar) + ) + .field(newFieldDefinition().name("count").type(GraphQLInt)) + .build() ) - .field(newFieldDefinition().name("count").type(GraphQLInt)) - .build() ) - ) ); } }); - aggregateObjectType.field(countFieldDefinition).field(groupFieldDefinition); var aggregateFieldDefinition = newFieldDefinition().name("aggregate").type(aggregateObjectType); From ce2845c84fbec02e10bee1299fc26ac358a98e65 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 26 May 2024 20:31:56 -0700 Subject: [PATCH 11/11] Refactor group aggregate count arguments --- .../schema/impl/GraphQLJpaQueryDataFetcher.java | 2 +- .../schema/impl/GraphQLJpaSchemaBuilder.java | 15 +-------------- .../converter/GraphQLJpaQueryAggregateTests.java | 4 ++-- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index 2ae8459cd..696b7eb63 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -137,7 +137,7 @@ public PagedResult get(DataFetchingEnvironment environment) { .findFirst() .orElseThrow(() -> new GraphQLException("Missing aggregate count for group: " + groupField)); - var countOfArgumentValue = getCountOfArgument(groupField); + var countOfArgumentValue = getCountOfArgument(countField); Map.Entry[] groupings = getFields(groupField.getSelectionSet(), "by") .stream() diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 2bac87c43..b058dcee4 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -488,24 +488,11 @@ private GraphQLFieldDefinition getAggregateFieldDefinition(EntityType entityT ) .type(JavaScalars.GraphQLObjectScalar) ) - .field(newFieldDefinition().name("count").type(GraphQLInt)) + .field(countFieldDefinition) .build() ) ); - if (entityType.getAttributes().stream().anyMatch(Attribute::isAssociation)) { - groupFieldDefinition.argument( - newArgument() - .name("of") - .type( - newEnum() - .name(aggregateObjectTypeName.concat("GroupOfAssociationsEnum")) - .values(associationEnumValueDefinitions) - .build() - ) - ); - } - entityType .getAttributes() .stream() diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java index 5be4692de..8684f534a 100644 --- a/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaQueryAggregateTests.java @@ -558,9 +558,9 @@ public void queryVariablesTaskNestedAggregateCountByMultipleEntities() { count } # Group by associated tasks - groupTasksByVariableName: group(of: task) { + groupTasksByVariableName: group { variable: by(field: name) - count + count(of: task) } } }