diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java new file mode 100644 index 000000000..31cc31de2 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java @@ -0,0 +1,50 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.dataloader.MappedBatchLoaderWithContext; + +public class BatchLoaderRegistry { + private final static Map<String, MappedBatchLoaderWithContext<Object, List<Object>>> mappedToManyBatchLoaders = new LinkedHashMap<>(); + private final static Map<String, MappedBatchLoaderWithContext<Object, Object>> mappedToOneBatchLoaders = new LinkedHashMap<>(); + private static BatchLoaderRegistry instance = new BatchLoaderRegistry(); + + public static BatchLoaderRegistry getInstance() { + return instance; + } + + public static void registerToMany(String batchLoaderKey, MappedBatchLoaderWithContext<Object, List<Object>> mappedBatchLoader) { + mappedToManyBatchLoaders.putIfAbsent(batchLoaderKey, mappedBatchLoader); + } + + public static void registerToOne(String batchLoaderKey, MappedBatchLoaderWithContext<Object, Object> mappedBatchLoader) { + mappedToOneBatchLoaders.putIfAbsent(batchLoaderKey, mappedBatchLoader); + } + + public static DataLoaderRegistry newDataLoaderRegistry(DataLoaderOptions dataLoaderOptions) { + DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + + mappedToManyBatchLoaders.entrySet() + .forEach(entry -> { + DataLoader<Object, List<Object>> dataLoader = DataLoader.newMappedDataLoader(entry.getValue(), + dataLoaderOptions); + dataLoaderRegistry.register(entry.getKey(), dataLoader); + }); + + mappedToOneBatchLoaders.entrySet() + .forEach(entry -> { + DataLoader<Object, Object> dataLoader = DataLoader.newMappedDataLoader(entry.getValue(), + dataLoaderOptions); + dataLoaderRegistry.register(entry.getKey(), dataLoader); + }); + + return dataLoaderRegistry; + + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java index d1232f789..dae052159 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java @@ -16,26 +16,38 @@ package com.introproventures.graphql.jpa.query.schema.impl; +import java.util.Arrays; +import java.util.List; import java.util.function.Supplier; +import org.dataloader.DataLoaderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.introproventures.graphql.jpa.query.schema.GraphQLExecutionInputFactory; import com.introproventures.graphql.jpa.query.schema.GraphQLExecutorContext; - import graphql.ExecutionInput; import graphql.GraphQL; import graphql.GraphQLContext; +import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLSchema; import graphql.schema.visibility.GraphqlFieldVisibility; public class GraphQLJpaExecutorContext implements GraphQLExecutorContext { - + + private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaExecutorContext.class); + private final GraphQLSchema graphQLSchema; private final GraphQLExecutionInputFactory executionInputFactory; private final Supplier<GraphqlFieldVisibility> graphqlFieldVisibility; private final Supplier<Instrumentation> instrumentation; private final Supplier<GraphQLContext> graphqlContext; + private final Supplier<DataLoaderDispatcherInstrumentationOptions> dataLoaderDispatcherInstrumentationOptions; + private final Supplier<DataLoaderRegistry> dataLoaderRegistry; private GraphQLJpaExecutorContext(Builder builder) { this.graphQLSchema = builder.graphQLSchema; @@ -43,28 +55,59 @@ private GraphQLJpaExecutorContext(Builder builder) { this.graphqlFieldVisibility = builder.graphqlFieldVisibility; this.instrumentation = builder.instrumentation; this.graphqlContext = builder.graphqlContext; + this.dataLoaderDispatcherInstrumentationOptions = builder.dataLoaderDispatcherInstrumentationOptions; + this.dataLoaderRegistry = builder.dataLoaderRegistry; } - + @Override public ExecutionInput.Builder newExecutionInput() { + DataLoaderRegistry dataLoaderRegistry = newDataLoaderRegistry(); + + GraphQLContext context = graphqlContext.get(); + + context.put("dataLoaderRegistry", dataLoaderRegistry); + return executionInputFactory.create() - .context(graphqlContext.get()); + .dataLoaderRegistry(dataLoaderRegistry) + .context(context); } @Override public GraphQL.Builder newGraphQL() { + Instrumentation instrumentation = newIstrumentation(); + return GraphQL.newGraphQL(getGraphQLSchema()) - .instrumentation(instrumentation.get()); + .instrumentation(instrumentation); + } + + public DataLoaderRegistry newDataLoaderRegistry() { + return dataLoaderRegistry.get(); + } + + public Instrumentation newIstrumentation() { + DataLoaderDispatcherInstrumentationOptions options = dataLoaderDispatcherInstrumentationOptions.get(); + + if (logger.isDebugEnabled()) { + options.includeStatistics(true); + } + + DataLoaderDispatcherInstrumentation dispatcherInstrumentation = new DataLoaderDispatcherInstrumentation(options); + + List<Instrumentation> list = Arrays.asList(dispatcherInstrumentation, + instrumentation.get()); + + return new ChainedInstrumentation(list); + } - + @Override public GraphQLSchema getGraphQLSchema() { GraphQLCodeRegistry codeRegistry = graphQLSchema.getCodeRegistry() .transform(builder -> builder.fieldVisibility(graphqlFieldVisibility.get())); - + return graphQLSchema.transform(builder -> builder.codeRegistry(codeRegistry)); } - + /** * Creates builder to build {@link GraphQLJpaExecutorContext}. * @return created builder @@ -85,9 +128,13 @@ public interface IBuildStage { public IBuildStage graphqlFieldVisibility(Supplier<GraphqlFieldVisibility> graphqlFieldVisibility); public IBuildStage instrumentation(Supplier<Instrumentation> instrumentation); - + public IBuildStage graphqlContext(Supplier<GraphQLContext> graphqlContext); + public IBuildStage dataLoaderDispatcherInstrumentationOptions(Supplier<DataLoaderDispatcherInstrumentationOptions> dataLoaderDispatcherInstrumentationOptions); + + public IBuildStage dataLoaderRegistry(Supplier<DataLoaderRegistry> dataLoaderRegistry); + public GraphQLJpaExecutorContext build(); } @@ -101,6 +148,8 @@ public static final class Builder implements IGraphQLSchemaStage, IBuildStage { private Supplier<GraphqlFieldVisibility> graphqlFieldVisibility; private Supplier<Instrumentation> instrumentation; private Supplier<GraphQLContext> graphqlContext; + private Supplier<DataLoaderDispatcherInstrumentationOptions> dataLoaderDispatcherInstrumentationOptions; + private Supplier<DataLoaderRegistry> dataLoaderRegistry; private Builder() { } @@ -135,10 +184,23 @@ public IBuildStage graphqlContext(Supplier<GraphQLContext> graphqlContext) { return this; } + @Override + public IBuildStage dataLoaderDispatcherInstrumentationOptions(Supplier<DataLoaderDispatcherInstrumentationOptions> dataLoaderDispatcherInstrumentationOptions) { + this.dataLoaderDispatcherInstrumentationOptions = dataLoaderDispatcherInstrumentationOptions; + + return this; + } + + @Override + public IBuildStage dataLoaderRegistry(Supplier<DataLoaderRegistry> dataLoaderRegistry) { + this.dataLoaderRegistry = dataLoaderRegistry; + + return this; + } + @Override public GraphQLJpaExecutorContext build() { return new GraphQLJpaExecutorContext(this); } - } } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java index fa6aead17..dfc609f85 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java @@ -18,27 +18,46 @@ import java.util.function.Supplier; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.introproventures.graphql.jpa.query.schema.GraphQLExecutionInputFactory; import com.introproventures.graphql.jpa.query.schema.GraphQLExecutorContext; import com.introproventures.graphql.jpa.query.schema.GraphQLExecutorContextFactory; - import graphql.GraphQLContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; import graphql.schema.GraphQLSchema; import graphql.schema.visibility.DefaultGraphqlFieldVisibility; import graphql.schema.visibility.GraphqlFieldVisibility; public class GraphQLJpaExecutorContextFactory implements GraphQLExecutorContextFactory { - + private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaExecutorContext.class); + private GraphQLExecutionInputFactory executionInputFactory = new GraphQLExecutionInputFactory() {}; private Supplier<GraphqlFieldVisibility> graphqlFieldVisibility = () -> DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; private Supplier<Instrumentation> instrumentation = () -> new SimpleInstrumentation(); private Supplier<GraphQLContext> graphqlContext = () -> GraphQLContext.newContext().build(); - + private Supplier<DataLoaderDispatcherInstrumentationOptions> dataLoaderDispatcherInstrumentationOptions = () -> { + return DataLoaderDispatcherInstrumentationOptions.newOptions(); + }; + + private Supplier<DataLoaderOptions> dataLoaderOptions = () -> DataLoaderOptions.newOptions(); + + private Supplier<DataLoaderRegistry> dataLoaderRegistry = () -> { + DataLoaderOptions options = dataLoaderOptions.get() + .setCachingEnabled(false); + + return BatchLoaderRegistry.newDataLoaderRegistry(options); + }; + + public GraphQLJpaExecutorContextFactory() { } - + @Override public GraphQLExecutorContext newExecutorContext(GraphQLSchema graphQLSchema) { return GraphQLJpaExecutorContext.builder() @@ -47,6 +66,8 @@ public GraphQLExecutorContext newExecutorContext(GraphQLSchema graphQLSchema) { .graphqlFieldVisibility(graphqlFieldVisibility) .instrumentation(instrumentation) .graphqlContext(graphqlContext) + .dataLoaderDispatcherInstrumentationOptions(dataLoaderDispatcherInstrumentationOptions) + .dataLoaderRegistry(dataLoaderRegistry) .build(); } @@ -55,13 +76,13 @@ public GraphQLJpaExecutorContextFactory withGraphqlFieldVisibility(Supplier<Grap return this; } - + public GraphQLJpaExecutorContextFactory withInstrumentation(Supplier<Instrumentation> instrumentation) { this.instrumentation = instrumentation; return this; } - + public GraphQLJpaExecutorContextFactory withExecutionInputFactory(GraphQLExecutionInputFactory executionInputFactory) { this.executionInputFactory = executionInputFactory; return this; @@ -71,11 +92,16 @@ public GraphQLJpaExecutorContextFactory withGraphqlContext(Supplier<GraphQLConte this.graphqlContext = graphqlContext; return this; }; - + + public GraphQLJpaExecutorContextFactory withDataLoaderDispatcherInstrumentationOptions(Supplier<DataLoaderDispatcherInstrumentationOptions> dataLoaderDispatcherInstrumentationOptions) { + this.dataLoaderDispatcherInstrumentationOptions = dataLoaderDispatcherInstrumentationOptions; + return this; + } + public GraphQLExecutionInputFactory getExecutionInputFactory() { return executionInputFactory; } - + public Supplier<GraphqlFieldVisibility> getGraphqlFieldVisibility() { return graphqlFieldVisibility; } @@ -84,9 +110,13 @@ public Supplier<Instrumentation> getInstrumentation() { return instrumentation; } - + public Supplier<GraphQLContext> getGraphqlContext() { return graphqlContext; } + public Supplier<DataLoaderDispatcherInstrumentationOptions> getDataLoaderDispatcherInstrumentationOptions() { + return dataLoaderDispatcherInstrumentationOptions; + } + } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index bc70cea21..118b9c4b4 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -26,6 +26,7 @@ import static graphql.introspection.Introspection.SchemaMetaFieldDef; import static graphql.introspection.Introspection.TypeMetaFieldDef; import static graphql.introspection.Introspection.TypeNameMetaFieldDef; +import static java.util.stream.Collectors.groupingBy; import java.beans.BeanInfo; import java.beans.Introspector; @@ -45,7 +46,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; -import java.util.function.BiFunction; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -80,7 +81,6 @@ import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult.AttributePropertyDescriptor; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; import com.introproventures.graphql.jpa.query.support.GraphQLSupport; - import graphql.GraphQLException; import graphql.execution.MergedField; import graphql.execution.ValuesResolver; @@ -115,11 +115,13 @@ */ public final class GraphQLJpaQueryFactory { + private static final String DESC = "DESC"; + private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaQueryFactory.class); - + protected static final String WHERE = "where"; protected static final String OPTIONAL = "optional"; - + protected static final String HIBERNATE_QUERY_PASS_DISTINCT_THROUGH = "hibernate.query.passDistinctThrough"; protected static final String ORG_HIBERNATE_CACHEABLE = "org.hibernate.cacheable"; protected static final String ORG_HIBERNATE_FETCH_SIZE = "org.hibernate.fetchSize"; @@ -146,33 +148,33 @@ private GraphQLJpaQueryFactory(Builder builder) { public DataFetchingEnvironment getQueryEnvironment(DataFetchingEnvironment environment, MergedField queryField) { - - // Override query environment with associated entity object type and select field + + // Override query environment with associated entity object type and select field return DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .fieldType(getEntityObjectType()) .mergedField(queryField) .build(); } - - public List<Object> queryKeys(DataFetchingEnvironment environment, - int firstResult, - int maxResults) { + + public List<Object> queryKeys(DataFetchingEnvironment environment, + int firstResult, + int maxResults) { MergedField queryField = resolveQueryField(environment.getField()); - // Override query environment with associated entity object type and + // Override query environment with associated entity object type and final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); - - TypedQuery<Object> keysQuery = getKeysQuery(queryEnvironment, + + TypedQuery<Object> keysQuery = getKeysQuery(queryEnvironment, queryEnvironment.getField()); - + keysQuery.setFirstResult(firstResult) .setMaxResults(maxResults); if (logger.isDebugEnabled()) { logger.info("\nGraphQL JPQL Keys Query String:\n {}", getJPQLQueryString(keysQuery)); } - + return keysQuery.getResultList(); } @@ -186,27 +188,34 @@ public List<Object> queryResultList(DataFetchingEnvironment environment, // Let's wrap stream into lazy list to pass it downstream return ResultStreamWrapper.wrap(resultStream, maxResults); - } - + protected Stream<Object> queryResultStream(DataFetchingEnvironment environment, int maxResults, List<Object> keys) { MergedField queryField = resolveQueryField(environment.getField()); - - // Override query environment with associated entity object type and + + // Override query environment with associated entity object type and final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); final int fetchSize = Integer.min(maxResults, defaultFetchSize); final boolean isDistinct = resolveDistinctArgument(queryEnvironment.getField()); - + final TypedQuery<Object> query = getQuery(queryEnvironment, queryEnvironment.getField(), isDistinct, keys.toArray()); - + + // Let's execute query and get wrap result into stream + return getResultStream(query, fetchSize, isDistinct); + } + + protected <T> Stream<T> getResultStream(TypedQuery<T> query, + int fetchSize, + boolean isDistinct) { + // Let' try reduce overhead and disable all caching query.setHint(ORG_HIBERNATE_READ_ONLY, true); - query.setHint(ORG_HIBERNATE_FETCH_SIZE, fetchSize); + query.setHint(ORG_HIBERNATE_FETCH_SIZE, fetchSize); query.setHint(ORG_HIBERNATE_CACHEABLE, false); - + // Let's not pass distinct if enabled to have better performance if(isDistinct) { query.setHint(HIBERNATE_QUERY_PASS_DISTINCT_THROUGH, false); @@ -218,49 +227,49 @@ protected Stream<Object> queryResultStream(DataFetchingEnvironment environment, // Let's execute query and get wrap result into stream return query.getResultStream() - .peek(entityManager::detach); + .peek(entityManager::detach); } - + protected Object querySingleResult(final DataFetchingEnvironment environment) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); - + final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); - + TypedQuery<Object> query = getQuery(queryEnvironment, queryEnvironment.getField(), true); - + if (logger.isDebugEnabled()) { logger.info("\nGraphQL JPQL Single Result Query String:\n {}", getJPQLQueryString(query)); } - + return query.getSingleResult(); - } - + } + public Long queryTotalCount(DataFetchingEnvironment environment) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); - + TypedQuery<Long> countQuery = getCountQuery(queryEnvironment, queryEnvironment.getField()); - + if (logger.isDebugEnabled()) { logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(countQuery)); } - + return countQuery.getSingleResult(); - } + } protected <T> TypedQuery<T> getQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Object... keys) { DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .localContext(Boolean.TRUE) // Fetch mode .build(); - + CriteriaQuery<T> criteriaQuery = getCriteriaQuery(queryEnvironment, field, isDistinct, keys); return entityManager.createQuery(criteriaQuery); } - + protected TypedQuery<Long> getCountQuery(DataFetchingEnvironment environment, Field field) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Long> query = cb.createQuery(Long.class); @@ -271,24 +280,24 @@ protected TypedQuery<Long> getCountQuery(DataFetchingEnvironment environment, Fi .localContext(Boolean.FALSE) // Join mode .build(); root.alias("root"); - + query.select(cb.count(root)); - + List<Predicate> predicates = field.getArguments().stream() - .map(it -> getPredicate(cb, root, null, queryEnvironment, it)) + .map(it -> getPredicate(field, cb, root, null, queryEnvironment, it)) .filter(it -> it != null) .collect(Collectors.toList()); - + query.where(predicates.toArray(new Predicate[0])); return entityManager.createQuery(query); } - + protected TypedQuery<Object> getKeysQuery(DataFetchingEnvironment environment, Field field) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Object> query = cb.createQuery(Object.class); Root<?> from = query.from(entityType); - + from.alias("root"); DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) @@ -303,12 +312,12 @@ protected TypedQuery<Object> getKeysQuery(DataFetchingEnvironment environment, F .collect(Collectors.toList()); query.multiselect(selection); } - + List<Predicate> predicates = field.getArguments().stream() - .map(it -> getPredicate(cb, from, null, queryEnvironment, it)) + .map(it -> getPredicate(field, cb, from, null, queryEnvironment, it)) .filter(it -> it != null) .collect(Collectors.toList()); - + query.where(predicates.toArray(new Predicate[0])); GraphQLSupport.fields(field.getSelectionSet()) @@ -319,46 +328,144 @@ protected TypedQuery<Object> getKeysQuery(DataFetchingEnvironment environment, F // Process the orderBy clause mayBeAddOrderBy(selection, query, cb, selectionPath, queryEnvironment); }); - + mayBeAddDefaultOrderBy(query, from, cb); - + return entityManager.createQuery(query); } - + + protected Map<Object, List<Object>> loadOneToMany(DataFetchingEnvironment environment, + Set<Object> keys) { + Field field = environment.getField(); + + TypedQuery<Object[]> query = getBatchQuery(environment, field, isDefaultDistinct(), keys); + + if (logger.isDebugEnabled()) { + logger.info("\nGraphQL JPQL Batch Query String:\n {}", getJPQLQueryString(query)); + } + + List<Object[]> resultList = query.getResultList(); + + Map<Object, List<Object>> batch = resultList.stream() + .collect(groupingBy(t -> t[0], + Collectors.mapping(t -> t[1], + GraphQLSupport.toResultList()))); + Map<Object, List<Object>> resultMap = new LinkedHashMap<>(); + + keys.forEach(it -> { + List<Object> list = batch.getOrDefault(it, Collections.emptyList()); + + if (!list.isEmpty()) { + list = list.stream() + .filter(GraphQLSupport.distinctByKey(GraphQLSupport::identityToString)) + .collect(Collectors.toList()); + } + + resultMap.put(it, list); + }); + + return resultMap; + } + + protected Map<Object, Object> loadManyToOne(DataFetchingEnvironment environment, + Set<Object> keys) { + Field field = environment.getField(); + + TypedQuery<Object[]> query = getBatchQuery(environment, field, isDefaultDistinct(), keys); + + if (logger.isDebugEnabled()) { + logger.info("\nGraphQL JPQL Batch Query String:\n {}", getJPQLQueryString(query)); + } + + List<Object[]> resultList = query.getResultList(); + + Map<Object, Object> batch = new LinkedHashMap<>(); + + resultList.forEach(item -> batch.put(item[0], item[1])); + + Map<Object, Object> resultMap = new LinkedHashMap<>(); + + keys.forEach(it -> { + Object list = batch.getOrDefault(it, null); + + resultMap.put(it, list); + }); + + return resultMap; + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + protected TypedQuery<Object[]> getBatchQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Set<Object> keys) { + + SingularAttribute parentIdAttribute = entityType.getId(Object.class); + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + //CriteriaQuery<Object> query = cb.createQuery((Class<Object>) entityType.getJavaType()); + CriteriaQuery<Object[]> query = cb.createQuery(Object[].class); + Root<?> from = query.from(entityType); + + DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .root(query) + .localContext(Boolean.TRUE) + .build(); + + from.alias("owner"); + + // Must use inner join in parent context + Join join = from.join(field.getName()) + .on(from.get(parentIdAttribute.getName()).in(keys)); + + query.multiselect(from.get(parentIdAttribute.getName()), + join.alias(field.getName())); + + List<Predicate> predicates = getFieldPredicates(field, query, cb, from, join, queryEnvironment); + + query.where( + predicates.toArray(new Predicate[0]) + ); + + // optionally add default ordering + mayBeAddDefaultOrderBy(query, join, cb); + + return entityManager.createQuery(query.distinct(isDistinct)); + } + @SuppressWarnings( { "rawtypes", "unchecked" } ) - protected TypedQuery<Object> getCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct) { - - Object source = environment.getSource(); - + protected TypedQuery<Object> getBatchCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Set<Object> keys) { + SingularAttribute parentIdAttribute = entityType.getId(Object.class); - - Object parentIdValue = getAttributeValue(source, parentIdAttribute); - + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery<Object> query = cb.createQuery((Class<Object>) entityType.getJavaType()); + //CriteriaQuery<Object> query = cb.createQuery((Class<Object>) entityType.getJavaType()); + CriteriaQuery<Object> query = cb.createQuery(); Root<?> from = query.from(entityType); - + + DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .root(query) + .localContext(Boolean.TRUE) + .build(); + from.alias("owner"); - + // Must use inner join in parent context Join join = from.join(field.getName()) - .on(cb.in(from.get(parentIdAttribute.getName())) - .value(parentIdValue)); - + .on(from.get(parentIdAttribute.getName()).in(keys)); + query.select(join.alias(field.getName())); - - List<Predicate> predicates = getFieldPredicates(field, query, cb, from, join, environment); + + List<Predicate> predicates = getFieldPredicates(field, query, cb, from, join, queryEnvironment); query.where( predicates.toArray(new Predicate[0]) ); - - // optionally add default ordering + + // optionally add default ordering mayBeAddDefaultOrderBy(query, join, cb); - + return entityManager.createQuery(query.distinct(isDistinct)); } - + + @SuppressWarnings("unchecked") protected <T> CriteriaQuery<T> getCriteriaQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Object... keys) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); @@ -376,7 +483,7 @@ protected <T> CriteriaQuery<T> getCriteriaQuery(DataFetchingEnvironment environm if (keys.length > 0) { if(hasIdAttribute()) { predicates.add(from.get(idAttributeName()).in(keys)); - } // array of idClass attributes + } // array of idClass attributes else if (hasIdClassAttribue()) { String[] names = idClassAttributeNames(); Map<String, List<Object>> idKeys = new HashMap<>(); @@ -388,20 +495,20 @@ else if (hasIdClassAttribue()) { .forEach(i -> { idKeys.computeIfAbsent(names[i], key -> new ArrayList<>()) .add(values[i]); - }); + }); }); - + List<Predicate> idPredicates = Stream.of(names) .map(name -> { return from.get(name) .in(idKeys.get(name).toArray(new Object[0])); }) .collect(Collectors.toList()); - - predicates.add(cb.and(idPredicates.toArray(new Predicate[0]))); + + predicates.add(cb.and(idPredicates.toArray(new Predicate[0]))); } } - + // Use AND clause to filter results if(!predicates.isEmpty()) query.where(predicates.toArray(new Predicate[0])); @@ -411,14 +518,7 @@ else if (hasIdClassAttribue()) { return query.distinct(isDistinct); } - - <A,B,C> Stream<C> zipped(List<A> lista, List<B> listb, BiFunction<A,B,C> zipper){ - int shortestLength = Math.min(lista.size(),listb.size()); - return IntStream.range(0,shortestLength).mapToObj( i -> { - return zipper.apply(lista.get(i), listb.get(i)); - }); - } - + protected void mayBeAddOrderBy(Field selectedField, CriteriaQuery<?> query, CriteriaBuilder cb, Path<?> fieldPath, DataFetchingEnvironment environment) { // Singular attributes only if (fieldPath.getModel() instanceof SingularAttribute) { @@ -429,18 +529,18 @@ protected void mayBeAddOrderBy(Field selectedField, CriteriaQuery<?> query, Crit .map(argument -> getOrderByValue(argument, environment)) .ifPresent(orderBy -> { List<Order> orders = new ArrayList<>(query.getOrderList()); - - if ("DESC".equals(orderBy.getName())) { + + if (DESC.equals(orderBy.getName())) { orders.add(cb.desc(fieldPath)); } else { orders.add(cb.asc(fieldPath)); } - + query.orderBy(orders); }); } } - + protected final List<Predicate> getFieldPredicates(Field field, CriteriaQuery<?> query, CriteriaBuilder cb, Root<?> root, From<?,?> from, DataFetchingEnvironment environment) { List<Argument> arguments = new ArrayList<>(); @@ -450,12 +550,10 @@ protected final List<Predicate> getFieldPredicates(Field field, CriteriaQuery<?> GraphQLSupport.fields(field.getSelectionSet()) .filter(selection -> isPersistent(environment, selection.getName())) .forEach(selection -> { - + Path<?> fieldPath = from.get(selection.getName()); From<?,?> fetch = null; - Optional<Argument> optionalArgument = getArgument(selection, OPTIONAL); Optional<Argument> whereArgument = getArgument(selection, WHERE); - Boolean isOptional = null; // Build predicate arguments for singular attributes only if(fieldPath.getModel() instanceof SingularAttribute) { @@ -467,12 +565,15 @@ protected final List<Predicate> getFieldPredicates(Field field, CriteriaQuery<?> if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE ) { - // Let's do fugly conversion - isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) - .orElse(attribute.isOptional()); + // Let's do fugly conversion + Boolean isOptional = getOptionalArgumentValue(environment, + selection, + attribute); // Let's apply left outer join to retrieve optional associations - fetch = reuseFetch(from, selection.getName(), isOptional); + if(!isOptional || !whereArgument.isPresent()) { + fetch = reuseFetch(from, selection.getName(), isOptional); + } } else if(attribute.getPersistentAttributeType() == PersistentAttributeType.EMBEDDED) { // Process where arguments clauses. arguments.addAll(selection.getArguments() @@ -481,63 +582,77 @@ protected final List<Predicate> getFieldPredicates(Field field, CriteriaQuery<?> .map(it -> new Argument(selection.getName() + "." + it.getName(), it.getValue())) .collect(Collectors.toList())); - + } } else { - // We must add plural attributes with explicit join fetch - // Let's do fugly conversion - // the many end is a collection, and it is always optional by default (empty collection) - isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) - .orElse(toManyDefaultOptional); - GraphQLObjectType objectType = getObjectType(environment); EntityType<?> entityType = getEntityType(objectType); PluralAttribute<?, ?, ?> attribute = (PluralAttribute<?, ?, ?>) entityType.getAttribute(selection.getName()); - + + // We must add plural attributes with explicit join fetch + // the many end is a collection, and it is always optional by default (empty collection) + Boolean isOptional = getOptionalArgumentValue(environment, + selection, + attribute); + // Let's join fetch element collections to avoid filtering their values used where search criteria if(PersistentAttributeType.ELEMENT_COLLECTION == attribute.getPersistentAttributeType()) { from.fetch(selection.getName(), JoinType.LEFT); - } else { + } else if(!whereArgument.isPresent()) { // Let's apply fetch join to retrieve associated plural attributes - fetch = reuseFetch(from, selection.getName(), isOptional); + if (!hasAnySelectionOrderBy(selection)) { + fetch = reuseFetch(from, selection.getName(), isOptional); + } } } - // Let's build join fetch graph to avoid Hibernate error: + // Let's build join fetch graph to avoid Hibernate error: // "query specified join fetching, but the owner of the fetched association was not present in the select list" if(selection.getSelectionSet() != null && fetch != null) { Map<String, Object> variables = environment.getVariables(); - + GraphQLFieldDefinition fieldDefinition = getFieldDefinition(environment.getGraphQLSchema(), this.getObjectType(environment), - selection); - + selection); + List<Argument> values = whereArgument.map(Collections::singletonList) .orElse(Collections.emptyList()); - + Map<String, Object> fieldArguments = new ValuesResolver().getArgumentValues(fieldDefinition.getArguments(), values, variables); - - DataFetchingEnvironment fieldEnvironment = wherePredicateEnvironment(environment, - fieldDefinition, + + DataFetchingEnvironment fieldEnvironment = wherePredicateEnvironment(environment, + fieldDefinition, fieldArguments); - - predicates.addAll(getFieldPredicates(selection, query, cb, root, fetch, fieldEnvironment)); + + predicates.addAll(getFieldPredicates(selection, + query, + cb, + root, + fetch, + fieldEnvironment)); } }); - + arguments.addAll(field.getArguments()); arguments.stream() .filter(this::isPredicateArgument) - .map(it -> getPredicate(cb, root, from, environment, it)) + .map(it -> getPredicate(field, cb, root, from, environment, it)) .filter(it -> it != null) .forEach(predicates::add); return predicates; } + protected Boolean getOptionalArgumentValue(DataFetchingEnvironment environment, + Field selection, + Attribute<?, ?> attribute) { + return getArgument(selection, OPTIONAL).map(it -> getArgumentValue(environment, it, Boolean.class)) + .orElseGet(() -> isOptionalAttribute(attribute)); + } + /** * if query orders are empty, then apply default ascending ordering * by root id attribute to prevent paging inconsistencies @@ -564,7 +679,7 @@ protected void mayBeAddDefaultOrderBy(CriteriaQuery<?> query, From<?,?> from, Cr } } else { AttributePropertyDescriptor attribute = attributePropertyDescriptor.get(); - + GraphQLDefaultOrderBy order = attribute.getDefaultOrderBy().get(); if (order.asc()) { query.orderBy(cb.asc(from.get(attribute.getName()))); @@ -578,7 +693,7 @@ protected void mayBeAddDefaultOrderBy(CriteriaQuery<?> query, From<?,?> from, Cr protected boolean isPredicateArgument(Argument argument) { return !isOrderByArgument(argument) && !isOptionalArgument(argument); } - + protected boolean isOrderByArgument(Argument argument) { return GraphQLJpaSchemaBuilder.ORDER_BY_PARAM_NAME.equals(argument.getName()); } @@ -586,37 +701,37 @@ protected boolean isOrderByArgument(Argument argument) { protected boolean isOptionalArgument(Argument argument) { return OPTIONAL.equals(argument.getName()); } - + protected Optional<Argument> getArgument(Field selectedField, String argumentName) { return selectedField.getArguments() .stream() .filter(it -> it.getName() .equals(argumentName)) - .findFirst(); + .findFirst(); } - + protected <R extends Attribute<?,?>> R getAttribute(String attributeName) { return (R) entityType.getAttribute(attributeName); } - + @SuppressWarnings( "unchecked" ) - protected Predicate getPredicate(CriteriaBuilder cb, Root<?> from, From<?,?> path, DataFetchingEnvironment environment, Argument argument) { - if(isLogicalArgument(argument) || - isDistinctArgument(argument) || isPageArgument(argument) || + protected Predicate getPredicate(Field field, CriteriaBuilder cb, Root<?> from, From<?,?> path, DataFetchingEnvironment environment, Argument argument) { + if(isLogicalArgument(argument) || + isDistinctArgument(argument) || isPageArgument(argument) || isAfterArgument(argument) || isFirstArgument(argument) ) { return null; - } - else if(isWhereArgument(argument)) { + } + else if(isWhereArgument(argument)) { return getWherePredicate(cb, from, path, argumentEnvironment(environment, argument), argument); - } + } else if(!argument.getName().contains(".")) { - Attribute<?,?> argumentEntityAttribute = getAttribute(environment, argument); + Attribute<?,?> argumentEntityAttribute = getAttribute(environment, argument.getName()); // If the argument is a list, let's assume we need to join and do an 'in' clause if (argumentEntityAttribute instanceof PluralAttribute) { // Apply left outer join to retrieve optional associations Boolean isFetch = environment.getLocalContext(); - + return (isFetch ? reuseFetch(from, argument.getName(), false) : reuseJoin(from, argument.getName(), false)) .in(convertValue(environment, argument, argument.getValue())); } @@ -624,9 +739,9 @@ else if(!argument.getName().contains(".")) { return cb.equal(path.get(argument.getName()), convertValue(environment, argument, argument.getValue())); } else { if(!argument.getName().endsWith(".where")) { - Path<?> field = getCompoundJoinedPath(path, argument.getName(), false); + Path<?> argumentPath = getCompoundJoinedPath(path, argument.getName(), false); - return cb.equal(field, convertValue(environment, argument, argument.getValue())); + return cb.equal(argumentPath, convertValue(environment, argument, argument.getValue())); } else { String fieldName = argument.getName().split("\\.")[0]; @@ -648,23 +763,23 @@ else if(!argument.getName().contains(".")) { } } } - - + + @SuppressWarnings( "unchecked" ) private <R extends Value<?>> R getValue(Argument argument, DataFetchingEnvironment environment) { Value<?> value = argument.getValue(); - + if(VariableReference.class.isInstance(value)) { Object variableValue = getVariableReferenceValue((VariableReference) value, environment); - + GraphQLArgument graphQLArgument = environment.getExecutionStepInfo() .getFieldDefinition() .getArgument(argument.getName()); - + return (R) AstValueHelper.astFromValue(variableValue, graphQLArgument.getType()); } - + return (R) value; } @@ -688,12 +803,12 @@ protected Predicate getWherePredicate(CriteriaBuilder cb, Root<?> root, From<?, if(whereValue.getChildren().isEmpty()) return cb.conjunction(); - + Logical logical = extractLogical(argument); - + Map<String, Object> predicateArguments = new LinkedHashMap<>(); predicateArguments.put(logical.name(), environment.getArguments()); - + DataFetchingEnvironment predicateDataFetchingEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .arguments(predicateArguments) .build(); @@ -708,23 +823,23 @@ protected Predicate getArgumentPredicate(CriteriaBuilder cb, From<?,?> from, ObjectValue whereValue = getValue(argument, environment); if (whereValue.getChildren().isEmpty()) - return cb.disjunction(); - + return cb.disjunction(); + Logical logical = extractLogical(argument); List<Predicate> predicates = new ArrayList<>(); whereValue.getObjectFields().stream() .filter(it -> Logical.names().contains(it.getName())) - .map(it -> { + .map(it -> { Map<String, Object> arguments = getFieldArguments(environment, it, argument); - + if(it.getValue() instanceof ArrayValue) { return getArgumentsPredicate(cb, from, argumentEnvironment(environment, arguments), new Argument(it.getName(), it.getValue())); } - + return getArgumentPredicate(cb, from, argumentEnvironment(environment, arguments), new Argument(it.getName(), it.getValue())); @@ -745,57 +860,57 @@ protected Predicate getArgumentPredicate(CriteriaBuilder cb, From<?,?> from, return getCompoundPredicate(cb, predicates, logical); } - - - protected Predicate getObjectFieldPredicate(DataFetchingEnvironment environment, - CriteriaBuilder cb, - From<?,?> from, + + + protected Predicate getObjectFieldPredicate(DataFetchingEnvironment environment, + CriteriaBuilder cb, + From<?,?> from, Logical logical, - ObjectField objectField, - Argument argument, + ObjectField objectField, + Argument argument, Map<String, Object> arguments ) { if(isEntityType(environment)) { - Attribute<?,?> attribute = getAttribute(environment, argument); - + Attribute<?,?> attribute = getAttribute(environment, argument.getName()); + if(attribute.isAssociation()) { GraphQLFieldDefinition fieldDefinition = getFieldDefinition(environment.getGraphQLSchema(), this.getObjectType(environment), new Field(objectField.getName())); - + if(Arrays.asList(Logical.EXISTS, Logical.NOT_EXISTS).contains(logical) ) { AbstractQuery<?> query = environment.getRoot(); Subquery<?> subquery = query.subquery(attribute.getJavaType()); From<?,?> correlation = Root.class.isInstance(from) ? subquery.correlate((Root<?>) from) : subquery.correlate((Join<?,?>) from); - + Join<?,?> correlationJoin = correlation.join(objectField.getName()); - + DataFetchingEnvironment existsEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .root(subquery) - .build(); - + .build(); + Predicate restriction = getArgumentPredicate(cb, correlationJoin, wherePredicateEnvironment(existsEnvironment, fieldDefinition, arguments), argument); - - + + Predicate exists = cb.exists(subquery.select((Join) correlationJoin) .where(restriction)); - + return logical == Logical.EXISTS ? exists : cb.not(exists); } - + AbstractQuery<?> query = environment.getRoot(); Boolean isFetch = environment.getLocalContext(); Boolean isOptional = isOptionalAttribute(attribute); - + From<?,?> context = (isSubquery(query) || isCountQuery(query) || !isFetch) ? reuseJoin(from, objectField.getName(), isOptional) : reuseFetch(from, objectField.getName(), isOptional); - - return getArgumentPredicate(cb, + + return getArgumentPredicate(cb, context, wherePredicateEnvironment(environment, fieldDefinition, arguments), argument); @@ -813,7 +928,7 @@ protected Predicate getObjectFieldPredicate(DataFetchingEnvironment environment, protected boolean isSubquery(AbstractQuery<?> query) { return Subquery.class.isInstance(query); } - + protected boolean isCountQuery(AbstractQuery<?> query) { return Optional.ofNullable(query.getSelection()) .map(Selection::getJavaType) @@ -833,13 +948,13 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, Logical logical = extractLogical(argument); List<Predicate> predicates = new ArrayList<>(); - + List<Map<String,Object>> arguments = environment.getArgument(logical.name()); List<ObjectValue> values = whereValue.getValues() .stream() .map(ObjectValue.class::cast).collect(Collectors.toList()); - - List<SimpleEntry<ObjectValue, Map<String, Object>>> tuples = + + List<SimpleEntry<ObjectValue, Map<String, Object>>> tuples = IntStream.range(0, values.size()) .mapToObj(i -> new SimpleEntry<ObjectValue, Map<String, Object>>(values.get(i), arguments.get(i))) @@ -853,19 +968,19 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, .map(it -> { Map<String, Object> args = e.getValue(); Argument arg = new Argument(it.getName(), it.getValue()); - + if(ArrayValue.class.isInstance(it.getValue())) { return getArgumentsPredicate(cb, from, argumentEnvironment(environment, args), arg); } - + return getArgumentPredicate(cb, from, argumentEnvironment(environment, args), arg); - + })) .forEach(predicates::add); @@ -877,18 +992,18 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, .map(it -> { Map<String, Object> args = e.getValue(); Argument arg = new Argument(it.getName(), it.getValue()); - + return getObjectFieldPredicate(environment, cb, from, logical, it, arg, args); })) .filter(predicate -> predicate != null) .forEach(predicates::add); - + return getCompoundPredicate(cb, predicates, logical); } - + private Map<String, Object> getFieldArguments(DataFetchingEnvironment environment, ObjectField field, Argument argument) { Map<String, Object> arguments; - + if (environment.getArgument(argument.getName()) instanceof Collection) { Collection<Map<String,Object>> list = environment.getArgument(argument.getName()); @@ -899,10 +1014,10 @@ private Map<String, Object> getFieldArguments(DataFetchingEnvironment environmen } else { arguments = environment.getArgument(argument.getName()); } - + return arguments; } - + private Logical extractLogical(Argument argument) { return Optional.of(argument.getName()) .filter(it -> Logical.names().contains(it)) @@ -929,7 +1044,7 @@ private Predicate getLogicalPredicates(String fieldName, .map(it -> { Map<String, Object> args = getFieldArguments(environment, it, argument); Argument arg = new Argument(it.getName(), it.getValue()); - + return getLogicalPredicate(it.getName(), cb, path, @@ -940,14 +1055,14 @@ private Predicate getLogicalPredicates(String fieldName, .forEach(predicates::add); return getCompoundPredicate(cb, predicates, logical); - } - + } + private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From<?,?> path, ObjectField objectField, DataFetchingEnvironment environment, Argument argument) { ObjectValue expressionValue; - + if(objectField.getValue() instanceof ObjectValue) expressionValue = (ObjectValue) objectField.getValue(); - else + else expressionValue = new ObjectValue(Arrays.asList(objectField)); if(expressionValue.getChildren().isEmpty()) @@ -960,22 +1075,22 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From // Let's parse logical expressions, i.e. AND, OR expressionValue.getObjectFields().stream() .filter(it -> Logical.names().contains(it.getName())) - .map(it -> { + .map(it -> { Map<String, Object> args = getFieldArguments(environment, it, argument); Argument arg = new Argument(it.getName(), it.getValue()); - + if(it.getValue() instanceof ArrayValue) { return getLogicalPredicates(fieldName, cb, path, it, argumentEnvironment(environment, args), arg); } - + return getLogicalPredicate(fieldName, cb, path, it, argumentEnvironment(environment, args), arg); }) .forEach(predicates::add); - + // Let's parse relation criteria expressions if present, i.e. books, author, etc. if(expressionValue.getObjectFields() .stream() @@ -987,21 +1102,21 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From Map<String, Object> args = new LinkedHashMap<>(); Argument arg = new Argument(logical.name(), expressionValue); boolean isOptional = false; - + if(Logical.names().contains(argument.getName())) { args.put(logical.name(), environment.getArgument(argument.getName())); } else { args.put(logical.name(), environment.getArgument(fieldName)); - isOptional = isOptionalAttribute(getAttribute(environment, argument)); + isOptional = isOptionalAttribute(getAttribute(environment, argument.getName())); } - - return getArgumentPredicate(cb, reuseJoin(path, fieldName, isOptional), + + return getArgumentPredicate(cb, reuseJoin(path, fieldName, isOptional), wherePredicateEnvironment(environment, fieldDefinition, args), arg); } - - // Let's parse simple Criteria expressions, i.e. EQ, LIKE, etc. + + // Let's parse simple Criteria expressions, i.e. EQ, LIKE, etc. JpaPredicateBuilder pb = new JpaPredicateBuilder(cb); expressionValue.getObjectFields() @@ -1018,15 +1133,15 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From return getCompoundPredicate(cb, predicates, logical); } - + private Predicate getCompoundPredicate(CriteriaBuilder cb, List<Predicate> predicates, Logical logical) { if(predicates.isEmpty()) return cb.disjunction(); - + if(predicates.size() == 1) { return predicates.get(0); } - + return (logical == Logical.OR) ? cb.or(predicates.toArray(new Predicate[0])) : cb.and(predicates.toArray(new Predicate[0])); @@ -1035,16 +1150,16 @@ private Predicate getCompoundPredicate(CriteriaBuilder cb, List<Predicate> predi private PredicateFilter getPredicateFilter(ObjectField objectField, DataFetchingEnvironment environment, Argument argument) { EnumSet<PredicateFilter.Criteria> options = EnumSet.of(PredicateFilter.Criteria.valueOf(argument.getName())); - + Map<String, Object> valueArguments = new LinkedHashMap<String,Object>(); valueArguments.put(objectField.getName(), environment.getArgument(argument.getName())); - + DataFetchingEnvironment dataFetchingEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .arguments(valueArguments) .build(); - + Argument dataFetchingArgument = new Argument(objectField.getName(), argument.getValue()); - + Object filterValue = convertValue( dataFetchingEnvironment, dataFetchingArgument, argument.getValue() ); return new PredicateFilter(objectField.getName(), filterValue, options ); @@ -1055,10 +1170,10 @@ protected final DataFetchingEnvironment argumentEnvironment(DataFetchingEnvironm .arguments(arguments) .build(); } - + protected final DataFetchingEnvironment argumentEnvironment(DataFetchingEnvironment environment, Argument argument) { Map<String, Object> arguments = environment.getArgument(argument.getName()); - + return DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .arguments(arguments) .build(); @@ -1071,7 +1186,7 @@ protected final DataFetchingEnvironment wherePredicateEnvironment(DataFetchingEn .fieldType(fieldDefinition.getType()) .build(); } - + /** * @param fieldName * @return Path of compound field to the primitive type @@ -1134,7 +1249,7 @@ private From<?,?> reuseJoin(From<?, ?> from, String fieldName, boolean outer) { } return outer ? from.join(fieldName, JoinType.LEFT) : from.join(fieldName); } - + // trying to find already existing fetch joins to reuse private From<?,?> reuseFetch(From<?, ?> from, String fieldName, boolean outer) { @@ -1144,8 +1259,8 @@ private From<?,?> reuseFetch(From<?, ?> from, String fieldName, boolean outer) { } } return outer ? (From<?,?>) from.fetch(fieldName, JoinType.LEFT) : (From<?,?>) from.fetch(fieldName); - } - + } + @SuppressWarnings( { "unchecked", "rawtypes" } ) protected Object convertValue(DataFetchingEnvironment environment, Argument argument, Value value) { if (value instanceof NullValue) { @@ -1167,15 +1282,15 @@ else if (value instanceof VariableReference) { if(javaType.isEnum()) { if(argumentValue instanceof Collection) { List<Enum> values = new ArrayList<>(); - + Collection.class.cast(argumentValue) - .forEach(it -> values.add(Enum.valueOf(javaType, it.toString()))); + .forEach(it -> values.add(Enum.valueOf(javaType, it.toString()))); return values; } else { return Enum.valueOf(javaType, argumentValue.toString()); } - } - else { + } + else { // Get resolved variable in environment arguments return argumentValue; } @@ -1189,28 +1304,28 @@ else if (value instanceof VariableReference) { arrayValue = Collection.class.cast(arrayValue.iterator() .next()); } - + // Let's convert enum types, i.e. array of strings or EnumValue into Java type if(getJavaType(environment, argument).isEnum()) { Function<Object, Value> objectValue = (obj) -> Value.class.isInstance(obj) ? Value.class.cast(obj) : new EnumValue(obj.toString()); - // Return real typed resolved array values converted into Java enums + // Return real typed resolved array values converted into Java enums return arrayValue.stream() - .map((it) -> convertValue(environment, - argument, + .map((it) -> convertValue(environment, + argument, objectValue.apply(it))) .collect(Collectors.toList()); - } - // Let's try handle Ast Value types + } + // Let's try handle Ast Value types else if(arrayValue.stream() .anyMatch(it->it instanceof Value)) { return arrayValue.stream() - .map(it -> convertValue(environment, - argument, + .map(it -> convertValue(environment, + argument, Value.class.cast(it))) .collect(Collectors.toList()); - } + } // Return real typed resolved array value, i.e. Date, UUID, Long else { return arrayValue; @@ -1235,7 +1350,7 @@ else if (value instanceof EnumValue) { } else if (value instanceof ObjectValue) { Class javaType = getJavaType(environment, argument); Map<String, Object> values = environment.getArgument(argument.getName()); - + try { return getJavaBeanValue(javaType, values); } catch (Exception cause) { @@ -1245,13 +1360,13 @@ else if (value instanceof EnumValue) { return value; } - + private Object getJavaBeanValue(Class<?> javaType, Map<String, Object> values) throws Exception { Constructor<?> constructor = javaType.getConstructor(); constructor.setAccessible(true); Object javaBean = constructor.newInstance(); - + values.entrySet() .stream() .forEach(entry -> { @@ -1259,11 +1374,11 @@ private Object getJavaBeanValue(Class<?> javaType, Map<String, Object> values) t entry.getKey(), entry.getValue()); }); - + return javaBean; } - - private void setPropertyValue(Object javaBean, String propertyName, Object propertyValue) { + + private void setPropertyValue(Object javaBean, String propertyName, Object propertyValue) { try { BeanInfo bi = Introspector.getBeanInfo(javaBean.getClass()); PropertyDescriptor pds[] = bi.getPropertyDescriptors(); @@ -1271,7 +1386,7 @@ private void setPropertyValue(Object javaBean, String propertyName, Object prope if (pd.getName().equals(propertyName)) { Method setter = pd.getWriteMethod(); setter.setAccessible(true); - + if (setter != null) { setter.invoke(javaBean, new Object[] {propertyValue} ); } @@ -1290,7 +1405,7 @@ private void setPropertyValue(Object javaBean, String propertyName, Object prope * @return Java class type */ protected Class<?> getJavaType(DataFetchingEnvironment environment, Argument argument) { - Attribute<?,?> argumentEntityAttribute = getAttribute(environment, argument); + Attribute<?,?> argumentEntityAttribute = getAttribute(environment, argument.getName()); if (argumentEntityAttribute instanceof PluralAttribute) return ((PluralAttribute<?,?,?>) argumentEntityAttribute).getElementType().getJavaType(); @@ -1305,13 +1420,15 @@ protected Class<?> getJavaType(DataFetchingEnvironment environment, Argument arg * @param argument * @return JPA model attribute */ - private Attribute<?,?> getAttribute(DataFetchingEnvironment environment, Argument argument) { + private Attribute<?,?> getAttribute(DataFetchingEnvironment environment, String argument) { GraphQLObjectType objectType = getObjectType(environment); EntityType<?> entityType = getEntityType(objectType); - return entityType.getAttribute(argument.getName()); + return entityType.getAttribute(argument); } + + private boolean isOptionalAttribute(Attribute<?,?> attribute) { if(SingularAttribute.class.isInstance(attribute)) { return SingularAttribute.class.cast(attribute).isOptional(); @@ -1319,10 +1436,10 @@ private boolean isOptionalAttribute(Attribute<?,?> attribute) { else if(PluralAttribute.class.isInstance(attribute)) { return true; } - + return false; } - + /** * Resolve JPA model entity type from GraphQL objectType * @@ -1338,13 +1455,13 @@ private EntityType<?> getEntityType(GraphQLObjectType objectType) { .findFirst() .get(); } - + private boolean isEntityType(DataFetchingEnvironment environment) { GraphQLObjectType objectType = getObjectType(environment); return entityManager.getMetamodel() .getEntities().stream() .anyMatch(it -> it.getName().equals(objectType.getName())); - } + } /** * Resolve GraphQL object type from Argument output type. @@ -1390,16 +1507,16 @@ protected GraphQLFieldDefinition getFieldDefinition(GraphQLSchema schema, GraphQ } GraphQLFieldDefinition fieldDefinition = parentType.getFieldDefinition(field.getName()); - + if (fieldDefinition != null) { return fieldDefinition; } - + throw new GraphQLException("unknown field " + field.getName()); } - + protected final boolean isManagedType(Attribute<?,?> attribute) { - return attribute.getPersistentAttributeType() != PersistentAttributeType.EMBEDDED + return attribute.getPersistentAttributeType() != PersistentAttributeType.EMBEDDED && attribute.getPersistentAttributeType() != PersistentAttributeType.BASIC && attribute.getPersistentAttributeType() != PersistentAttributeType.ELEMENT_COLLECTION; } @@ -1449,7 +1566,7 @@ protected final <R extends Value<?>> R getObjectFieldValue(ObjectValue objectVal @SuppressWarnings( "unchecked" ) protected final <T> T getArgumentValue(DataFetchingEnvironment environment, Argument argument, Class<T> type) { Value<?> value = argument.getValue(); - + if(VariableReference.class.isInstance(value)) { return (T) environment.getVariables() @@ -1470,7 +1587,7 @@ else if (FloatValue.class.isInstance(value)) { else if (NullValue.class.isInstance(value)) { return (T) null; } - + throw new IllegalArgumentException("Not supported"); } @@ -1478,7 +1595,7 @@ protected boolean isPersistent(DataFetchingEnvironment environment, String attributeName) { GraphQLObjectType objectType = getObjectType(environment); EntityType<?> entityType = getEntityType(objectType); - + return isPersistent(entityType, attributeName); } @@ -1486,11 +1603,11 @@ protected boolean isPersistent(EntityType<?> entityType, String attributeName) { try { return entityType.getAttribute(attributeName) != null; - } catch (Exception ignored) { } - + } catch (Exception ignored) { } + return false; } - + protected boolean isTransient(DataFetchingEnvironment environment, String attributeName) { return !isPersistent(environment, attributeName); @@ -1500,31 +1617,33 @@ protected boolean isTransient(EntityType<?> entityType, String attributeName) { return !isPersistent(entityType, attributeName); } - - + + protected String getJPQLQueryString(TypedQuery<?> query) { try { Object queryImpl = query.unwrap(TypedQuery.class); - + java.lang.reflect.Field queryStringField = ReflectionUtil.getField(queryImpl.getClass(), "queryString"); - + ReflectionUtil.forceAccess(queryStringField); - - return queryStringField.get(queryImpl) - .toString(); - + + if(queryStringField != null) { + return queryStringField.get(queryImpl) + .toString(); + } + } catch (Exception ignored) { logger.error("Error getting JPQL string", ignored); } - + return null; } protected boolean hasIdAttribute() { return entityType.getIdType() != null; } - + protected String idAttributeName() { return entityType.getId(entityType.getIdType() .getJavaType()).getName(); @@ -1533,7 +1652,7 @@ protected String idAttributeName() { protected boolean hasIdClassAttribue() { return entityType.getIdClassAttributes() != null; } - + protected String[] idClassAttributeNames() { return entityType.getIdClassAttributes() .stream() @@ -1542,7 +1661,14 @@ protected String[] idClassAttributeNames() { .collect(Collectors.toList()) .toArray(new String[0]); } - + + + protected <T> T getParentIdAttributeValue(T entity) { + SingularAttribute<?, Object> parentIdAttribute = entityType.getId(Object.class); + + return (T) getAttributeValue(entity, parentIdAttribute); + } + /** * Fetches the value of the given SingularAttribute on the given * entity. @@ -1550,21 +1676,21 @@ protected String[] idClassAttributeNames() { * http://stackoverflow.com/questions/7077464/how-to-get-singularattribute-mapped-value-of-a-persistent-object */ @SuppressWarnings("unchecked") - protected <EntityType, FieldType> FieldType getAttributeValue(EntityType entity, SingularAttribute<EntityType, FieldType> field) { + protected <E, T> T getAttributeValue(T entity, SingularAttribute<E, T> field) { try { Member member = field.getJavaMember(); if (member instanceof Method) { // this should be a getter method: - return (FieldType) ((Method)member).invoke(entity); + return (T) ((Method)member).invoke(entity); } else if (member instanceof java.lang.reflect.Field) { - return (FieldType) ((java.lang.reflect.Field)member).get(entity); + return (T) ((java.lang.reflect.Field)member).get(entity); } else { throw new IllegalArgumentException("Unexpected java member type. Expecting method or field, found: " + member); } } catch (Exception e) { throw new RuntimeException(e); } - } + } /** * Fetches the value of the given SingularAttribute on the given @@ -1588,48 +1714,48 @@ protected <EntityType, FieldType> FieldType getAttributeValue(EntityType entity, throw new RuntimeException(e); } } - + protected boolean resolveDistinctArgument(Field field) { - Argument distinctArg = extractArgument(field, - SELECT_DISTINCT_PARAM_NAME, + Argument distinctArg = extractArgument(field, + SELECT_DISTINCT_PARAM_NAME, new BooleanValue(defaultDistinct)); - + return BooleanValue.class.cast(distinctArg.getValue()) .isValue(); } - + public boolean isDefaultDistinct() { return defaultDistinct; } - + public String getSelectNodeName() { return selectNodeName; } public MergedField resolveQueryField(Field rootNode) { Optional<Field> recordsSelection = GraphQLSupport.searchByFieldName(rootNode, getSelectNodeName()); - + Field queryField = recordsSelection.map(selectNode -> Field.newField(selectNode.getName()) .selectionSet(selectNode.getSelectionSet()) .arguments(rootNode.getArguments()) .directives(selectNode.getDirectives()) .build()) - .orElse(rootNode); + .orElse(rootNode); return MergedField.newMergedField(queryField) .build(); } - + public GraphQLObjectType getEntityObjectType() { return entityObjectType; } - + public int getDefaultFetchSize() { return defaultFetchSize; } - + private MergedField flattenEmbeddedIdArguments(Field field) { // manage object arguments (EmbeddedId) final List<Argument> argumentsWhereObjectsAreFlattened = field.getArguments() @@ -1645,11 +1771,38 @@ private MergedField flattenEmbeddedIdArguments(Field field) { } }) .collect(Collectors.toList()); - + return MergedField.newMergedField(field.transform(builder -> builder.arguments(argumentsWhereObjectsAreFlattened))) .build(); } - + + protected boolean hasAnySelectionOrderBy(Field field) { + + if (!hasSelectionSet(field)) + return false; + + // Loop through all of the fields being requested + return field.getSelectionSet() + .getSelections() + .stream() + .filter(Field.class::isInstance) + .map(Field.class::cast) + .anyMatch(selectedField -> { + + // Optional orderBy argument + Optional<Argument> orderBy = selectedField.getArguments() + .stream() + .filter(this::isOrderByArgument) + .findFirst(); + + if (orderBy.isPresent()) { + return true; + } + + return false; + }); + + } /** * Creates builder to build {@link GraphQLJpaQueryFactory}. @@ -1812,5 +1965,5 @@ public IBuildStage withDefaultFetchSize(int defaultFetchSize) { public GraphQLJpaQueryFactory build() { return new GraphQLJpaQueryFactory(this); } - } + } } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 1154f1213..a1a1a102c 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,6 +45,7 @@ import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; +import org.dataloader.MappedBatchLoaderWithContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +58,6 @@ import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult.AttributePropertyDescriptor; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; import com.introproventures.graphql.jpa.query.schema.relay.GraphQLJpaRelayDataFetcher; - import graphql.Assert; import graphql.Directives; import graphql.Scalars; @@ -80,7 +81,7 @@ /** * JPA specific schema builder implementation of {code #GraphQLSchemaBuilder} interface - * + * * @author Igor Dianov * */ @@ -93,30 +94,30 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { public static final String PAGE_START_PARAM_NAME = "start"; public static final String PAGE_LIMIT_PARAM_NAME = "limit"; - + public static final String QUERY_SELECT_PARAM_NAME = "select"; public static final String QUERY_WHERE_PARAM_NAME = "where"; public static final String QUERY_LOGICAL_PARAM_NAME = "logical"; public static final String SELECT_DISTINCT_PARAM_NAME = "distinct"; - + protected NamingStrategy namingStrategy = new NamingStrategy() {}; - + public static final String ORDER_BY_PARAM_NAME = "orderBy"; - + private Map<Class<?>, GraphQLOutputType> classCache = new HashMap<>(); private Map<EntityType<?>, GraphQLObjectType> entityCache = new HashMap<>(); private Map<ManagedType<?>, GraphQLInputObjectType> inputObjectCache = new HashMap<>(); private Map<ManagedType<?>, GraphQLInputObjectType> subqueryInputObjectCache = new HashMap<>(); private Map<Class<?>, GraphQLObjectType> embeddableOutputCache = new HashMap<>(); private Map<Class<?>, GraphQLInputObjectType> embeddableInputCache = new HashMap<>(); - + private static final Logger log = LoggerFactory.getLogger(GraphQLJpaSchemaBuilder.class); private EntityManager entityManager; - + private String name = "GraphQLJPA"; - + private String description = "GraphQL Schema for all entities in this JPA application"; private boolean isUseDistinctParameter = false; @@ -128,11 +129,15 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { private int defaultMaxResults = 100; private int defaultFetchSize = 100; private int defaultPageLimitSize = 100; - + private final Relay relay = new Relay(); - + private final List<String> entityPaths = new ArrayList<>(); - + + private Supplier<BatchLoaderRegistry> batchLoadersRegistry = () -> { + return BatchLoaderRegistry.getInstance(); + }; + public GraphQLJpaSchemaBuilder(EntityManager entityManager) { this.entityManager = entityManager; } @@ -144,7 +149,7 @@ public GraphQLJpaSchemaBuilder(EntityManager entityManager) { public GraphQLSchema build() { GraphQLSchema.Builder schema = GraphQLSchema.newSchema() .query(getQueryType()); - + if(enableSubscription) { schema.subscription(getSubscriptionType()); } @@ -152,16 +157,16 @@ public GraphQLSchema build() { if(enableDeferDirective) { schema.additionalDirective(Directives.DeferDirective); } - + if(enableRelay) { schema.additionalType(Relay.pageInfoType); } - + return schema.build(); } private GraphQLObjectType getQueryType() { - GraphQLObjectType.Builder queryType = + GraphQLObjectType.Builder queryType = GraphQLObjectType.newObject() .name(this.name + "Query") .description(this.description); @@ -173,7 +178,7 @@ private GraphQLObjectType getQueryType() { .map(this::getQueryFieldByIdDefinition) .collect(Collectors.toList()) ); - + queryType.fields( entityManager.getMetamodel() .getEntities().stream() @@ -186,7 +191,7 @@ private GraphQLObjectType getQueryType() { } private GraphQLObjectType getSubscriptionType() { - GraphQLObjectType.Builder queryType = + GraphQLObjectType.Builder queryType = GraphQLObjectType.newObject() .name(this.name + "Subscription") .description(this.description); @@ -201,10 +206,10 @@ private GraphQLObjectType getSubscriptionType() { return queryType.build(); } - + private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType<?> entityType) { GraphQLObjectType entityObjectType = getObjectType(entityType); - + GraphQLJpaQueryFactory queryFactory = GraphQLJpaQueryFactory.builder() .withEntityManager(entityManager) .withEntityType(entityType) @@ -212,12 +217,12 @@ private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType<?> entityT .withSelectNodeName(entityObjectType.getName()) .withToManyDefaultOptional(toManyDefaultOptional) .build(); - + DataFetcher<Object> dataFetcher = GraphQLJpaSimpleDataFetcher.builder() .withQueryFactory(queryFactory) .build(); String fieldName = entityType.getName(); - + return GraphQLFieldDefinition.newFieldDefinition() .name(enableRelay ? Introspector.decapitalize(fieldName) : fieldName) .description(getSchemaDescription(entityType)) @@ -232,10 +237,10 @@ private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType<?> entityT ) .build(); } - + private GraphQLObjectType getConnectionType(GraphQLObjectType nodeType) { GraphQLObjectType edgeType = relay.edgeType(nodeType.getName(), nodeType, null, Collections.emptyList()); - + return relay.connectionType(nodeType.getName(), edgeType, Collections.emptyList()); } @@ -244,7 +249,7 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType<?> entit final GraphQLObjectType outputType = enableRelay ? getConnectionType(entityObjectType) : getSelectType(entityType); final DataFetcher<? extends Object> dataFetcher; - + GraphQLJpaQueryFactory queryFactory = GraphQLJpaQueryFactory.builder() .withEntityManager(entityManager) .withEntityType(entityType) @@ -254,7 +259,7 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType<?> entit .withDefaultDistinct(isDefaultDistinct) .withDefaultFetchSize(defaultFetchSize) .build(); - + if(enableRelay) { dataFetcher = GraphQLJpaRelayDataFetcher.builder() .withQueryFactory(queryFactory) @@ -270,7 +275,7 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType<?> entit } String fieldName = namingStrategy.pluralize(entityType.getName()); - + GraphQLFieldDefinition.Builder fieldDefinition = GraphQLFieldDefinition.newFieldDefinition() .name(enableRelay ? Introspector.decapitalize(fieldName) : fieldName) .description("Query request wrapper for " + entityType.getName() + " to request paginated data. " @@ -281,17 +286,17 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType<?> entit .dataFetcher(dataFetcher) .argument(getWhereArgument(entityType)) .arguments(enableRelay ? relay.getForwardPaginationConnectionFieldArguments() : Collections.singletonList(paginationArgument)); - + if (isUseDistinctParameter) { fieldDefinition.argument(distinctArgument(entityType)); } return fieldDefinition.build(); } - + private GraphQLObjectType getSelectType(EntityType<?> entityType) { GraphQLObjectType selectObjectType = getObjectType(entityType); - + GraphQLObjectType selectPagedResultType = GraphQLObjectType.newObject() .name(namingStrategy.pluralize(entityType.getName())) .description("Query response wrapper object for " + entityType.getName() + ". When page is requested, this object will be returned with query metadata.") @@ -313,14 +318,14 @@ private GraphQLObjectType getSelectType(EntityType<?> entityType) { .type(new GraphQLList(selectObjectType)) .build() ) - .build(); - + .build(); + return selectPagedResultType; } - + private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType<?> entityType) { GraphQLObjectType entityObjectType = getObjectType(entityType); - + GraphQLJpaQueryFactory queryFactory = GraphQLJpaQueryFactory.builder() .withEntityManager(entityManager) .withEntityType(entityType) @@ -328,8 +333,8 @@ private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType<?> entit .withSelectNodeName(SELECT_DISTINCT_PARAM_NAME) .withToManyDefaultOptional(toManyDefaultOptional) .withDefaultDistinct(isDefaultDistinct) - .build(); - + .build(); + DataFetcher<Object> dataFetcher = GraphQLJpaStreamDataFetcher.builder() .withQueryFactory(queryFactory) .build(); @@ -343,13 +348,13 @@ private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType<?> entit .dataFetcher(dataFetcher) .argument(paginationArgument) .argument(getWhereArgument(entityType)); - + if (isUseDistinctParameter) { fieldDefinition.argument(distinctArgument(entityType)); } return fieldDefinition.build(); - } + } private Map<Class<?>, GraphQLArgument> whereArgumentsMap = new HashMap<>(); @@ -365,7 +370,7 @@ private GraphQLArgument distinctArgument(EntityType<?> entityType) { private GraphQLArgument getWhereArgument(ManagedType<?> managedType) { return whereArgumentsMap.computeIfAbsent(managedType.getJavaType(), (javaType) -> computeWhereArgument(managedType)); } - + private GraphQLArgument computeWhereArgument(ManagedType<?> managedType) { String type=resolveWhereArgumentTypeName(managedType); @@ -388,18 +393,18 @@ private GraphQLArgument computeWhereArgument(ManagedType<?> managedType) { .name(Logical.EXISTS.name()) .description("Logical EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .field(GraphQLInputObjectField.newInputObjectField() .name(Logical.NOT_EXISTS.name()) .description("Logical NOT EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .fields(managedType.getAttributes().stream() .filter(this::isValidInput) .filter(this::isNotIgnored) - .filter(this::isNotIgnoredFilter) + .filter(this::isNotIgnoredFilter) .map(this::getWhereInputField) .collect(Collectors.toList()) ) @@ -411,34 +416,34 @@ private GraphQLArgument computeWhereArgument(ManagedType<?> managedType) { .collect(Collectors.toList()) ) .build(); - + return GraphQLArgument.newArgument() .name(QUERY_WHERE_PARAM_NAME) .description("Where logical specification") .type(whereInputObject) .build(); - + } private String resolveWhereArgumentTypeName(ManagedType<?> managedType) { String typeName=resolveTypeName(managedType); - + return namingStrategy.pluralize(typeName)+"CriteriaExpression"; } - + private String resolveSubqueryArgumentTypeName(ManagedType<?> managedType) { String typeName=resolveTypeName(managedType); - + return namingStrategy.pluralize(typeName)+"SubqueryCriteriaExpression"; } private GraphQLInputObjectType getSubqueryInputType(ManagedType<?> managedType) { return subqueryInputObjectCache.computeIfAbsent(managedType, this::computeSubqueryInputType); } - + private GraphQLInputObjectType computeSubqueryInputType(ManagedType<?> managedType) { String type=resolveSubqueryArgumentTypeName(managedType); - + Builder whereInputObject = GraphQLInputObjectType.newInputObject() .name(type) .description("Where logical AND specification of the provided list of criteria expressions") @@ -458,13 +463,13 @@ private GraphQLInputObjectType computeSubqueryInputType(ManagedType<?> managedTy .name(Logical.EXISTS.name()) .description("Logical EXISTS subquery expression") .type(new GraphQLList(new GraphQLTypeReference(type))) - .build() + .build() ) .field(GraphQLInputObjectField.newInputObjectField() .name(Logical.NOT_EXISTS.name()) .description("Logical NOT EXISTS subquery expression") .type(new GraphQLList(new GraphQLTypeReference(type))) - .build() + .build() ) .fields(managedType.getAttributes().stream() .filter(Attribute::isAssociation) @@ -473,37 +478,37 @@ private GraphQLInputObjectType computeSubqueryInputType(ManagedType<?> managedTy .map(this::getWhereInputRelationField) .collect(Collectors.toList()) ); - + return whereInputObject.build(); - - } - + + } + private String resolveTypeName(ManagedType<?> managedType) { String typeName=""; - + if (managedType instanceof EmbeddableType){ typeName = managedType.getJavaType().getSimpleName()+"EmbeddableType"; } else if (managedType instanceof EntityType) { typeName = ((EntityType<?>)managedType).getName(); } - + return typeName; } private GraphQLInputObjectType getWhereInputType(ManagedType<?> managedType) { return inputObjectCache.computeIfAbsent(managedType, this::computeWhereInputType); } - + private String resolveWhereInputTypeName(ManagedType<?> managedType) { String typeName=resolveTypeName(managedType); return namingStrategy.pluralize(typeName)+"RelationCriteriaExpression"; - + } - + private GraphQLInputObjectType computeWhereInputType(ManagedType<?> managedType) { String type=resolveWhereInputTypeName(managedType); - + Builder whereInputObject = GraphQLInputObjectType.newInputObject() .name(type) .description("Where logical AND specification of the provided list of criteria expressions") @@ -523,13 +528,13 @@ private GraphQLInputObjectType computeWhereInputType(ManagedType<?> managedType) .name(Logical.EXISTS.name()) .description("Logical EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .field(GraphQLInputObjectField.newInputObjectField() .name(Logical.NOT_EXISTS.name()) .description("Logical NOT EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .fields(managedType.getAttributes().stream() .filter(this::isValidInput) @@ -545,15 +550,15 @@ private GraphQLInputObjectType computeWhereInputType(ManagedType<?> managedType) .map(this::getWhereInputRelationField) .collect(Collectors.toList()) ); - - + + return whereInputObject.build(); - - } - + + } + private GraphQLInputObjectField getWhereInputRelationField(Attribute<?,?> attribute) { ManagedType<?> foreignType = getForeignType(attribute); - + String type = resolveWhereInputTypeName(foreignType); String description = getSchemaDescription(attribute); @@ -561,9 +566,9 @@ private GraphQLInputObjectField getWhereInputRelationField(Attribute<?,?> attrib .name(attribute.getName()) .description(description) .type(new GraphQLTypeReference(type)) - .build(); + .build(); } - + private GraphQLInputObjectField getWhereInputField(Attribute<?,?> attribute) { GraphQLInputType type = getWhereAttributeType(attribute); String description = getSchemaDescription(attribute); @@ -573,20 +578,20 @@ private GraphQLInputObjectField getWhereInputField(Attribute<?,?> attribute) { .name(attribute.getName()) .description(description) .type(type) - .build(); + .build(); } throw new IllegalArgumentException("Attribute " + attribute.getName() + " cannot be mapped as an Input Argument"); } private Map<String, GraphQLInputType> whereAttributesMap = new HashMap<>(); - + private GraphQLInputType getWhereAttributeType(Attribute<?,?> attribute) { String type = namingStrategy.singularize(attribute.getName())+attribute.getDeclaringType().getJavaType().getSimpleName()+"Criteria"; if(whereAttributesMap.containsKey(type)) return whereAttributesMap.get(type); - + GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject() .name(type) .description("Criteria expression specification of "+namingStrategy.singularize(attribute.getName())+" attribute in entity " + attribute.getDeclaringType().getJavaType()) @@ -614,7 +619,7 @@ private GraphQLInputType getWhereAttributeType(Attribute<?,?> attribute) { .type(getAttributeInputType(attribute)) .build() ); - + if(!attribute.getJavaType().isEnum()) { if(!attribute.getJavaType().equals(String.class)) { builder.field(GraphQLInputObjectField.newInputObjectField() @@ -642,7 +647,7 @@ private GraphQLInputType getWhereAttributeType(Attribute<?,?> attribute) { .build() ); } - + if(attribute.getJavaType().equals(String.class)) { builder.field(GraphQLInputObjectField.newInputObjectField() .name(Criteria.LIKE.name()) @@ -704,10 +709,10 @@ private GraphQLInputType getWhereAttributeType(Attribute<?,?> attribute) { .type(getAttributeInputType(attribute)) .build() ); - } - else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) + } + else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) && Field.class.cast(attribute.getJavaMember()) - .isAnnotationPresent(Convert.class)) + .isAnnotationPresent(Convert.class)) { builder.field(GraphQLInputObjectField.newInputObjectField() .name(Criteria.LOCATE.name()) @@ -716,7 +721,7 @@ else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) .build()); } } - + builder.field(GraphQLInputObjectField.newInputObjectField() .name(Criteria.IS_NULL.name()) .description("Is Null criteria") @@ -755,13 +760,13 @@ else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) ); GraphQLInputType answer = builder.build(); - + whereAttributesMap.putIfAbsent(type, answer); - + return answer; - + } - + private GraphQLArgument getArgument(Attribute<?,?> attribute) { GraphQLInputType type = getAttributeInputType(attribute); String description = getSchemaDescription(attribute); @@ -772,7 +777,7 @@ private GraphQLArgument getArgument(Attribute<?,?> attribute) { .description(description) .build(); } - + private GraphQLType getEmbeddableType(EmbeddableType<?> embeddableType, boolean input) { if (input && embeddableInputCache.containsKey(embeddableType.getJavaType())) return embeddableInputCache.get(embeddableType.getJavaType()); @@ -807,16 +812,16 @@ private GraphQLType getEmbeddableType(EmbeddableType<?> embeddableType, boolean } else{ embeddableOutputCache.putIfAbsent(embeddableType.getJavaType(), (GraphQLObjectType) graphQLType); } - + return graphQLType; } - + private GraphQLObjectType getObjectType(EntityType<?> entityType) { return entityCache.computeIfAbsent(entityType, this::computeObjectType); } - - + + private GraphQLObjectType computeObjectType(EntityType<?> entityType) { return GraphQLObjectType.newObject() .name(entityType.getName()) @@ -839,16 +844,16 @@ private List<GraphQLFieldDefinition> getTransientFields(ManagedType<?> managedTy return EntityIntrospector.introspect(managedType) .getTransientPropertyDescriptors() .stream() - .filter(AttributePropertyDescriptor::isNotIgnored) - .map(this::getJavaFieldDefinition) + .filter(AttributePropertyDescriptor::isNotIgnored) + .map(this::getJavaFieldDefinition) .collect(Collectors.toList()); } - + @SuppressWarnings( { "rawtypes" } ) private GraphQLFieldDefinition getJavaFieldDefinition(AttributePropertyDescriptor propertyDescriptor) { GraphQLOutputType type = getGraphQLTypeFromJavaType(propertyDescriptor.getPropertyType()); DataFetcher dataFetcher = PropertyDataFetcher.fetching(propertyDescriptor.getName()); - + String description = propertyDescriptor.getSchemaDescription().orElse(null); return GraphQLFieldDefinition.newFieldDefinition() @@ -883,31 +888,77 @@ && isNotIgnoredOrder(attribute) ) { ); } + // Get the fields that can be queried on (i.e. Simple Types, no Sub-Objects) - if (attribute instanceof SingularAttribute + if (attribute instanceof SingularAttribute && attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC) { ManagedType foreignType = getForeignType(attribute); SingularAttribute<?,?> singularAttribute = SingularAttribute.class.cast(attribute); // TODO fix page count query arguments.add(getWhereArgument(foreignType)); - - // to-one end could be optional + + // to-one end could be optional arguments.add(optionalArgument(singularAttribute.isOptional())); + GraphQLObjectType entityObjectType = GraphQLObjectType.newObject() + .name(baseEntity.getName()) + .build(); + + GraphQLJpaQueryFactory graphQLJpaQueryFactory = GraphQLJpaQueryFactory.builder() + .withEntityManager(entityManager) + .withEntityType(baseEntity) + .withEntityObjectType(entityObjectType) + .withSelectNodeName(entityObjectType.getName()) + .withDefaultDistinct(isDefaultDistinct) + .build(); + + String dataLoaderKey = baseEntity.getName() + "." + attribute.getName(); + + MappedBatchLoaderWithContext<Object, Object> mappedBatchLoader = new GraphQLJpaToOneMappedBatchLoader(graphQLJpaQueryFactory); + + batchLoadersRegistry.get() + .registerToOne(dataLoaderKey, mappedBatchLoader); + + 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.ONE_TO_MANY || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY)) { Assert.assertNotNull(baseEntity, "For attribute "+attribute.getName() + " cannot find declaring type!"); EntityType elementType = (EntityType) ((PluralAttribute) attribute).getElementType(); arguments.add(getWhereArgument(elementType)); - + // make it configurable via builder api arguments.add(optionalArgument(toManyDefaultOptional)); + + GraphQLObjectType entityObjectType = GraphQLObjectType.newObject() + .name(baseEntity.getName()) + .build(); + + GraphQLJpaQueryFactory graphQLJpaQueryFactory = GraphQLJpaQueryFactory.builder() + .withEntityManager(entityManager) + .withEntityType(baseEntity) + .withEntityObjectType(entityObjectType) + .withSelectNodeName(entityObjectType.getName()) + .withDefaultDistinct(isDefaultDistinct) + .build(); + + String dataLoaderKey = baseEntity.getName() + "." + attribute.getName(); + + MappedBatchLoaderWithContext<Object, List<Object>> mappedBatchLoader = new GraphQLJpaToManyMappedBatchLoader(graphQLJpaQueryFactory); + + batchLoadersRegistry.get() + .registerToMany(dataLoaderKey, mappedBatchLoader); + + dataFetcher = new GraphQLJpaToManyDataFetcher(graphQLJpaQueryFactory, + (PluralAttribute) attribute); } - + return GraphQLFieldDefinition.newFieldDefinition() .name(attribute.getName()) .description(getSchemaDescription(attribute)) @@ -916,7 +967,7 @@ else if (attribute instanceof PluralAttribute .arguments(arguments) .build(); } - + private GraphQLArgument optionalArgument(Boolean defaultValue) { return GraphQLArgument.newArgument() .name("optional") @@ -925,14 +976,14 @@ private GraphQLArgument optionalArgument(Boolean defaultValue) { .defaultValue(defaultValue) .build(); } - + protected ManagedType<?> getForeignType(Attribute<?,?> attribute) { if(SingularAttribute.class.isInstance(attribute)) return (ManagedType<?>) ((SingularAttribute<?,?>) attribute).getType(); else return (EntityType<?>) ((PluralAttribute<?, ?, ?>) attribute).getElementType(); } - + @SuppressWarnings( { "rawtypes" } ) private GraphQLInputObjectField getInputObjectField(Attribute attribute) { GraphQLInputType type = getAttributeInputType(attribute); @@ -949,7 +1000,7 @@ private Stream<Attribute<?,?>> findBasicAttributes(Collection<Attribute<?,?>> at } private GraphQLInputType getAttributeInputType(Attribute<?,?> attribute) { - + try { return (GraphQLInputType) getAttributeType(attribute, true); } catch (ClassCastException e){ @@ -970,27 +1021,27 @@ private GraphQLType getAttributeType(Attribute<?,?> attribute, boolean input) { if (isBasic(attribute)) { return getGraphQLTypeFromJavaType(attribute.getJavaType()); - } + } else if (isEmbeddable(attribute)) { EmbeddableType embeddableType = (EmbeddableType) ((SingularAttribute) attribute).getType(); return getEmbeddableType(embeddableType, input); - } + } else if (isToMany(attribute)) { EntityType foreignType = (EntityType) ((PluralAttribute) attribute).getElementType(); - + return input ? getWhereInputType(foreignType) : new GraphQLList(new GraphQLTypeReference(foreignType.getName())); - } + } else if (isToOne(attribute)) { EntityType foreignType = (EntityType) ((SingularAttribute) attribute).getType(); - + return input ? getWhereInputType(foreignType) : new GraphQLTypeReference(foreignType.getName()); - } + } else if (isElementCollection(attribute)) { Type foreignType = ((PluralAttribute) attribute).getElementType(); - + if(foreignType.getPersistenceType() == Type.PersistenceType.BASIC) { GraphQLType graphQLType = getGraphQLTypeFromJavaType(foreignType.getJavaType()); - + return input ? graphQLType : new GraphQLList(graphQLType); } } @@ -1005,15 +1056,15 @@ else if (isElementCollection(attribute)) { protected final boolean isEmbeddable(Attribute<?,?> attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED; } - + protected final boolean isBasic(Attribute<?,?> attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC; } - + protected final boolean isElementCollection(Attribute<?,?> attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ELEMENT_COLLECTION; } - + protected final boolean isToMany(Attribute<?,?> attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY; @@ -1022,12 +1073,12 @@ protected final boolean isToMany(Attribute<?,?> attribute) { protected final boolean isOneToMany(Attribute<?,?> attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY; } - + protected final boolean isToOne(Attribute<?,?> attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE; } - + protected final boolean isValidInput(Attribute<?,?> attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC || @@ -1040,7 +1091,7 @@ private String getSchemaDescription(Attribute<?,?> attribute) { .getSchemaDescription(attribute.getName()) .orElse(null); } - + private String getSchemaDescription(EntityType<?> entityType) { return EntityIntrospector.introspect(entityType) .getSchemaDescription() @@ -1052,11 +1103,11 @@ private String getSchemaDescription(EmbeddableType<?> embeddableType) { .getSchemaDescription() .orElse(null); } - + private boolean isNotIgnored(EmbeddableType<?> attribute) { return isNotIgnored(attribute.getJavaType()); } - + private boolean isNotIgnored(Attribute<?,?> attribute) { return isNotIgnored(attribute.getJavaMember()) && isNotIgnored(attribute.getJavaType()); } @@ -1064,11 +1115,11 @@ private boolean isNotIgnored(Attribute<?,?> attribute) { private boolean isIdentity(Attribute<?,?> attribute) { return attribute instanceof SingularAttribute && ((SingularAttribute<?,?>)attribute).isId(); } - + private boolean isNotIgnored(EntityType<?> entityType) { return isNotIgnored(entityType.getJavaType()) && isNotIgnored(entityType.getJavaType().getName()); } - + private boolean isNotIgnored(String name) { return entityPaths.isEmpty() || entityPaths.stream() .anyMatch(prefix -> name.startsWith(prefix)); @@ -1112,14 +1163,14 @@ protected boolean isNotIgnoredOrder(Attribute<?,?> attribute) { return false; } - + @SuppressWarnings( "unchecked" ) private GraphQLOutputType getGraphQLTypeFromJavaType(Class<?> clazz) { if (clazz.isEnum()) { - + if (classCache.containsKey(clazz)) return classCache.get(clazz); - + GraphQLEnumType.Builder enumBuilder = GraphQLEnumType.newEnum().name(clazz.getSimpleName()); int ordinal = 0; for (Enum<?> enumValue : ((Class<Enum<?>>)clazz).getEnumConstants()) @@ -1129,7 +1180,7 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class<?> clazz) { setNoOpCoercing(enumType); classCache.putIfAbsent(clazz, enumType); - + return enumType; } else if (clazz.isArray()) { return GraphQLList.list(JavaScalars.of(clazz.getComponentType())); @@ -1139,21 +1190,21 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class<?> clazz) { } protected GraphQLInputType getFieldsEnumType(EntityType<?> entityType) { - + GraphQLEnumType.Builder enumBuilder = GraphQLEnumType.newEnum().name(entityType.getName()+"FieldsEnum"); final AtomicInteger ordinal = new AtomicInteger(); - + entityType.getAttributes().stream() .filter(this::isValidInput) .filter(this::isNotIgnored) .forEach(it -> enumBuilder.value(it.getName(), ordinal.incrementAndGet())); - + GraphQLInputType answer = enumBuilder.build(); setNoOpCoercing(answer); return answer; } - + /** * JPA will deserialize Enum's for us...we don't want GraphQL doing it. * @@ -1168,8 +1219,8 @@ private void setNoOpCoercing(GraphQLType type) { log.error("Unable to set coercing for " + type, e); } } - - private static final GraphQLArgument paginationArgument = + + private static final GraphQLArgument paginationArgument = newArgument().name(PAGE_PARAM_NAME) .description("Page object for pageble requests, specifying the requested start page and limit size.") .type(newInputObject().name("Page") @@ -1194,7 +1245,7 @@ private void setNoOpCoercing(GraphQLType type) { .value("DESC", "DESC", "Descending") .build(); - + /** * @return the name */ @@ -1208,7 +1259,7 @@ public String getName() { @Override public GraphQLJpaSchemaBuilder name(String name) { this.name = name; - + return this; } @@ -1255,7 +1306,7 @@ public GraphQLJpaSchemaBuilder defaultDistinct(boolean isDefaultDistinct) { return this; } - + /** * @param namingStrategy the namingStrategy to set */ @@ -1263,7 +1314,7 @@ public void setNamingStrategy(NamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; } - + static class NoOpCoercing implements Coercing<Object, Object> { @Override @@ -1285,21 +1336,21 @@ public Object parseLiteral(Object input) { @Override public GraphQLJpaSchemaBuilder entityPath(String path) { Assert.assertNotNull(path, "path is null"); - + entityPaths.add(path); - + return this; } @Override public GraphQLJpaSchemaBuilder namingStrategy(NamingStrategy instance) { Assert.assertNotNull(instance, "instance is null"); - + this.namingStrategy = instance; - + return this; } - + public boolean isToManyDefaultOptional() { return toManyDefaultOptional; } @@ -1311,10 +1362,10 @@ public void setToManyDefaultOptional(boolean toManyDefaultOptional) { public GraphQLJpaSchemaBuilder toManyDefaultOptional(boolean toManyDefaultOptional) { this.toManyDefaultOptional = toManyDefaultOptional; - + return this; } - + public boolean isEnableSubscription() { return enableSubscription; } @@ -1328,23 +1379,23 @@ public GraphQLJpaSchemaBuilder enableSubscription(boolean enableSubscription) { public boolean isEnableDeferDirective() { return enableDeferDirective; } - + public GraphQLJpaSchemaBuilder enableDeferDirective(boolean enableDeferDirective) { this.enableDeferDirective = enableDeferDirective; return this; } - + public boolean isEnableRelay() { return enableRelay; } - + public GraphQLJpaSchemaBuilder enableRelay(boolean enableRelay) { this.enableRelay = enableRelay; return this; } - + public int getDefaultMaxResults() { return defaultMaxResults; } @@ -1358,7 +1409,7 @@ public GraphQLJpaSchemaBuilder defaultMaxResults(int defaultMaxResults) { public int getDefaultPageLimitSize() { return defaultPageLimitSize; } - + public GraphQLJpaSchemaBuilder defaultPageLimitSize(int defaultPageLimitSize) { this.defaultPageLimitSize = defaultPageLimitSize; @@ -1368,11 +1419,11 @@ public GraphQLJpaSchemaBuilder defaultPageLimitSize(int defaultPageLimitSize) { public int getDefaultFetchSize() { return defaultFetchSize; } - + public GraphQLJpaSchemaBuilder defaultFetchSize(int defaultFetchSize) { this.defaultFetchSize = defaultFetchSize; return this; } - + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyDataFetcher.java new file mode 100644 index 000000000..1ce48fa45 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyDataFetcher.java @@ -0,0 +1,101 @@ +/* + * 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.schema.impl; + +import java.util.List; +import java.util.Optional; + +import javax.persistence.metamodel.PluralAttribute; + +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.dataloader.MappedBatchLoaderWithContext; + +import com.introproventures.graphql.jpa.query.support.GraphQLSupport; +import graphql.GraphQLContext; +import graphql.language.Argument; +import graphql.language.Field; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLType; + +/** + * One-To-Many DataFetcher that uses where argument to filter collection attributes + * + * @author Igor Dianov + * + */ +class GraphQLJpaToManyDataFetcher implements DataFetcher<Object> { + + private final PluralAttribute<Object,Object,Object> attribute; + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaToManyDataFetcher(GraphQLJpaQueryFactory queryFactory, + PluralAttribute<Object,Object,Object> attribute) { + this.queryFactory = queryFactory; + this.attribute = attribute; + } + + @Override + public Object get(DataFetchingEnvironment environment) { + Field field = environment.getField(); + GraphQLType parentType = environment.getParentType(); + + Object source = environment.getSource(); + Optional<Argument> whereArg = GraphQLSupport.getWhereArgument(field); + + // Resolve collection query if where argument is present or any field in selection has orderBy argument + if (whereArg.isPresent() || queryFactory.hasAnySelectionOrderBy(field)) { + Object parentIdValue = queryFactory.getParentIdAttributeValue(source); + String dataLoaderKey = parentType.getName() + "." + Optional.ofNullable(field.getAlias()) + .orElseGet(attribute::getName); + + DataLoader<Object, List<Object>> dataLoader = getDataLoader(environment, + dataLoaderKey); + + return dataLoader.load(parentIdValue, environment); + } + + // Let hibernate resolve collection query + return queryFactory.getAttributeValue(source, + attribute); + } + + protected DataLoader<Object, List<Object>> getDataLoader(DataFetchingEnvironment environment, + String dataLoaderKey) { + GraphQLContext context = environment.getContext(); + DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); + + if (!dataLoaderRegistry.getKeys() + .contains(dataLoaderKey)) { + synchronized (dataLoaderRegistry) { + MappedBatchLoaderWithContext<Object, List<Object>> mappedBatchLoader = new GraphQLJpaToManyMappedBatchLoader(queryFactory); + + DataLoaderOptions options = DataLoaderOptions.newOptions() + .setCachingEnabled(false); + + DataLoader<Object, List<Object>> dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, + options); + dataLoaderRegistry.register(dataLoaderKey, dataLoader); + } + } + + return dataLoaderRegistry.getDataLoader(dataLoaderKey); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyMappedBatchLoader.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyMappedBatchLoader.java new file mode 100644 index 000000000..279fdde66 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyMappedBatchLoader.java @@ -0,0 +1,33 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.MappedBatchLoaderWithContext; + +import graphql.schema.DataFetchingEnvironment; + +// a batch loader function that will be called with N or more keys for batch loading +class GraphQLJpaToManyMappedBatchLoader implements MappedBatchLoaderWithContext<Object, List<Object>> { + + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaToManyMappedBatchLoader(GraphQLJpaQueryFactory queryFactory) { + this.queryFactory = queryFactory; + } + + @Override + public CompletionStage<Map<Object, List<Object>>> load(Set<Object> keys, BatchLoaderEnvironment environment) { + Object key = keys.iterator().next(); + DataFetchingEnvironment context = (DataFetchingEnvironment) environment.getKeyContexts() + .get(key); + + return CompletableFuture.supplyAsync(() -> queryFactory.loadOneToMany(context, keys)); + } + + +}; diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java new file mode 100644 index 000000000..ded77cbc6 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java @@ -0,0 +1,100 @@ +/* + * 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.schema.impl; + +import java.util.Optional; + +import javax.persistence.metamodel.Attribute.PersistentAttributeType; +import javax.persistence.metamodel.SingularAttribute; + +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.dataloader.MappedBatchLoaderWithContext; + +import graphql.GraphQLContext; +import graphql.language.Field; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLType; + +/** + * One-To-Many DataFetcher that uses where argument to filter collection attributes + * + * @author Igor Dianov + * + */ +class GraphQLJpaToOneDataFetcher implements DataFetcher<Object> { + + private final SingularAttribute<Object,Object> attribute; + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaToOneDataFetcher(GraphQLJpaQueryFactory queryFactory, + SingularAttribute<Object,Object> attribute) { + this.queryFactory = queryFactory; + this.attribute = attribute; + } + + @Override + public Object get(DataFetchingEnvironment environment) { + Field field = environment.getField(); + GraphQLType parentType = environment.getParentType(); + + Object source = environment.getSource(); + Boolean isOptional = queryFactory.getOptionalArgumentValue(environment, + field, + attribute); + // Resolve collection query if where argument is present + if (isOptional && !PersistentAttributeType.EMBEDDED.equals(attribute.getPersistentAttributeType())) { + Object parentIdValue = queryFactory.getParentIdAttributeValue(source); + String dataLoaderKey = parentType.getName() + "." + Optional.ofNullable(field.getAlias()) + .orElseGet(attribute::getName); + + DataLoader<Object, Object> dataLoader = getDataLoader(environment, + dataLoaderKey); + + return dataLoader.load(parentIdValue, environment); + } + + // Let hibernate resolve collection query + return queryFactory.getAttributeValue(source, + attribute); + } + + protected DataLoader<Object, Object> getDataLoader(DataFetchingEnvironment environment, + String dataLoaderKey) { + GraphQLContext context = environment.getContext(); + DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); + + if (!dataLoaderRegistry.getKeys() + .contains(dataLoaderKey)) { + synchronized (dataLoaderRegistry) { + MappedBatchLoaderWithContext<Object, Object> mappedBatchLoader = new GraphQLJpaToOneMappedBatchLoader(queryFactory); + + DataLoaderOptions options = DataLoaderOptions.newOptions() + .setCachingEnabled(false); + + DataLoader<Object, Object> dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, + options); + dataLoaderRegistry.register(dataLoaderKey, dataLoader); + } + } + + return environment.getDataLoader(dataLoaderKey); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneMappedBatchLoader.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneMappedBatchLoader.java new file mode 100644 index 000000000..93fb4668b --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneMappedBatchLoader.java @@ -0,0 +1,30 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.MappedBatchLoaderWithContext; + +import graphql.schema.DataFetchingEnvironment; + +// a batch loader function that will be called with N or more keys for batch loading +class GraphQLJpaToOneMappedBatchLoader implements MappedBatchLoaderWithContext<Object, Object> { + + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaToOneMappedBatchLoader(GraphQLJpaQueryFactory queryFactory) { + this.queryFactory = queryFactory; + } + + @Override + public CompletionStage<Map<Object, Object>> load(Set<Object> keys, BatchLoaderEnvironment environment) { + Object key = keys.iterator().next(); + DataFetchingEnvironment context = (DataFetchingEnvironment) environment.getKeyContexts() + .get(key); + + return CompletableFuture.supplyAsync(() -> queryFactory.loadManyToOne(context, keys)); + } +}; diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java index 7d5256a98..8779f1d2d 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java @@ -4,20 +4,22 @@ import static com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder.QUERY_WHERE_PARAM_NAME; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; import com.introproventures.graphql.jpa.query.schema.impl.PageArgument; - import graphql.language.Argument; import graphql.language.Field; import graphql.language.ObjectField; @@ -32,16 +34,16 @@ public static Stream<Field> fields(SelectionSet selectionSet) { .stream() .filter(Field.class::isInstance) .map(Field.class::cast); - + } public static Optional<Field> searchByFieldName(Field root, String fieldName) { Predicate<Field> matcher = (field) -> fieldName.equals(field.getName()); return search(root, matcher); - } + } + - public static Optional<Field> search(Field root, Predicate<Field> predicate) { Queue<Field> queue = new ArrayDeque<>(); queue.add(root); @@ -58,7 +60,7 @@ public static Optional<Field> search(Field root, Predicate<Field> predicate) { } return Optional.empty(); - } + } public static final Collection<Field> selections(Field field) { SelectionSet selectionSet = Optional.ofNullable(field.getSelectionSet()) @@ -67,7 +69,7 @@ public static final Collection<Field> selections(Field field) { return fields(selectionSet).collect(Collectors.toList()); } - + public static Optional<Argument> getPageArgument(Field field) { return field.getArguments() .stream() @@ -81,13 +83,13 @@ public static Optional<Argument> getWhereArgument(Field field) { .filter(it -> QUERY_WHERE_PARAM_NAME.equals(it.getName())) .findFirst(); } - + public static PageArgument extractPageArgument(DataFetchingEnvironment environment, Optional<Argument> paginationRequest, int defaultPageLimitSize) { if (paginationRequest.isPresent()) { Map<String, Integer> pagex = environment.getArgument(GraphQLJpaSchemaBuilder.PAGE_PARAM_NAME); - + Integer start = pagex.getOrDefault(GraphQLJpaSchemaBuilder.PAGE_START_PARAM_NAME, 1); Integer limit = pagex.getOrDefault(GraphQLJpaSchemaBuilder.PAGE_LIMIT_PARAM_NAME, defaultPageLimitSize); @@ -96,7 +98,7 @@ public static PageArgument extractPageArgument(DataFetchingEnvironment environm return new PageArgument(1, defaultPageLimitSize); } - + public static Field removeArgument(Field field, Optional<Argument> argument) { if (!argument.isPresent()) { @@ -109,7 +111,7 @@ public static Field removeArgument(Field field, Optional<Argument> argument) { return field.transform(builder -> builder.arguments(newArguments)); } - + public static Boolean isWhereArgument(Argument argument) { return GraphQLJpaSchemaBuilder.QUERY_WHERE_PARAM_NAME.equals(argument.getName()); } @@ -125,7 +127,7 @@ public static Boolean isFirstArgument(Argument argument) { public static Boolean isAfterArgument(Argument argument) { return "after".equals(argument.getName()); } - + public static Boolean isLogicalArgument(Argument argument) { return GraphQLJpaSchemaBuilder.QUERY_LOGICAL_PARAM_NAME.equals(argument.getName()); } @@ -133,7 +135,7 @@ public static Boolean isLogicalArgument(Argument argument) { public static Boolean isDistinctArgument(Argument argument) { return GraphQLJpaSchemaBuilder.SELECT_DISTINCT_PARAM_NAME.equals(argument.getName()); } - + public static final Optional<ObjectField> getObjectField(ObjectValue objectValue, String fieldName) { return objectValue.getObjectFields().stream() .filter(it -> fieldName.equals(it.getName())) @@ -144,6 +146,40 @@ public static final Optional<Field> getSelectionField(Field field, String fieldN return GraphQLSupport.fields(field.getSelectionSet()) .filter(it -> fieldName.equals(it.getName())) .findFirst(); - } - + } + + public static Collector<Object, List<Object>, List<Object>> toResultList() { + return Collector.of(ArrayList::new, + (list, item) -> { + if (item != null) { + list.add(item); + } + }, + (left, right) -> { + left.addAll(right); + return left; + }, + Collector.Characteristics.CONCURRENT); + } + + public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) { + Map<Object, Boolean> seen = new ConcurrentHashMap<>(); + + return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } + + public static String identityToString(final Object object) { + if (object == null) { + return null; + } + final String name = object.getClass().getName(); + final String hexString = Integer.toHexString(System.identityHashCode(object)); + final StringBuilder builder = new StringBuilder(name.length() + 1 + hexString.length()); + // @formatter:off + builder.append(name) + .append("@") + .append(hexString); + // @formatter:off + return builder.toString(); + } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java index aa202b42f..0e2245283 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java @@ -191,9 +191,11 @@ public void queryEnumArrayVariableBindingInEmbeddedRelation() { + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" - + "]}" + + "]}, " + + "{id=4, name=Anton Chekhov, books=[]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; - + //when Object result = executor.execute(query, variables).getData(); diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java index c774276a7..b2f621ec7 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java @@ -42,7 +42,6 @@ import com.introproventures.graphql.jpa.query.AbstractSpringBootTestSupport; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; - import graphql.ErrorType; import graphql.ExecutionResult; import graphql.GraphQLError; @@ -52,7 +51,7 @@ @SpringBootTest public class GraphQLExecutorTests extends AbstractSpringBootTestSupport { - + @SpringBootApplication static class Application { @Bean @@ -62,14 +61,14 @@ public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaB @Bean public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { - + return new GraphQLJpaSchemaBuilder(entityManager) .name("GraphQLBooks") .description("Books JPA test schema"); } - + } - + @Autowired private GraphQLExecutor executor; @@ -77,12 +76,12 @@ public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManag public static void init() { TimeZone.setDefault(TimeZone.getTimeZone("UTC")); } - + @Test public void contextLoads() { Assert.isAssignable(GraphQLExecutor.class, executor.getClass()); } - + @Test public void GetsAllThings() { //given @@ -94,14 +93,14 @@ public void GetsAllThings() { //then assertThat(result.toString()).isEqualTo(expected); - + } @Test public void queryForThingById() { //given String query = "query ThingByIdQuery { Thing(id: \"2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1\") { id type } }"; - + String expected = "{Thing={id=2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1, type=Thing1}}"; //when @@ -139,7 +138,7 @@ public void queryEmptyParameter() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @SuppressWarnings( { "rawtypes", "unchecked", "serial" } ) @Test public void queryWithParameterNoResult() { @@ -221,14 +220,14 @@ public void queryWithAlias() { //then: assertThat(result.toString()).isEqualTo(expected); } - - + + // https://github.com/introproventures/graphql-jpa-query/issues/33 @Test public void queryForElementCollection() { //given String query = "{ Author(id: 1) { id name, phoneNumbers } }"; - + String expected = "{Author={id=1, name=Leo Tolstoy, phoneNumbers=[1-123-1234, 1-123-5678]}}"; //when @@ -242,7 +241,7 @@ public void queryForElementCollection() { public void queryForEnumIn() { //given String query = "{ Books(where: {genre: {IN: PLAY}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + "{id=6, title=The Seagull, genre=PLAY}, " @@ -255,12 +254,12 @@ public void queryForEnumIn() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForEnumInArray() { //given String query = "{ Books(where: {genre: {IN: [NOVEL, PLAY]}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}, " @@ -280,7 +279,7 @@ public void queryForEnumInArray() { public void queryForEnumNinArray() { //given String query = "{ Books(where: {genre: {NIN: [NOVEL]}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + "{id=6, title=The Seagull, genre=PLAY}, " @@ -293,12 +292,12 @@ public void queryForEnumNinArray() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForEnumEq() { //given String query = "{ Books(where: {genre: {EQ: NOVEL}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" @@ -315,7 +314,7 @@ public void queryForEnumEq() { public void queryForEnumNe() { //given String query = "{ Books(where: {genre: {NE: PLAY}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" @@ -327,12 +326,12 @@ public void queryForEnumNe() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForEnumNin() { //given String query = "{ Books(where: {genre: {NIN: PLAY}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" @@ -344,12 +343,12 @@ public void queryForEnumNin() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForParentWithEnum() { //given String query = "{ Books { select { id title, author( where: { genre: { EQ: NOVEL } }) { name } } } }"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, author={name=Leo Tolstoy}}, " + "{id=3, title=Anna Karenina, author={name=Leo Tolstoy}}" @@ -361,32 +360,33 @@ public void queryForParentWithEnum() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryAuthorBooksWithExplictOptional() { //given String query = "query { " - + "Authors(" + - " where: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " ) {" + - " select {" + - " id" + - " name" + - " books(optional: true) {" + - " id" + - " title(orderBy: ASC)" + - " genre" + - " }" + - " }" + + + "Authors(" + + " where: {" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + " }" + "}"; - + String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + + "{id=3, title=Anna Karenina, genre=NOVEL}, " + "{id=2, title=War and Peace, genre=NOVEL}]}" + "]}}"; @@ -396,32 +396,32 @@ public void queryAuthorBooksWithExplictOptional() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryAuthorBooksWithExplictOptionalEXISTS() { //given String query = "query { " - + "Authors(" + + + "Authors(" + " where: {" + " EXISTS: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }" + - " ) {" + - " select {" + - " id" + - " name" + - " books(optional: true) {" + - " id" + - " title(orderBy: ASC)" + - " genre" + - " }" + - " }" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + " }" + "}"; - + String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[{id=3, title=Anna Karenina, genre=NOVEL}, " + "{id=2, title=War and Peace, genre=NOVEL}]}" @@ -432,30 +432,154 @@ public void queryAuthorBooksWithExplictOptionalEXISTS() { // then assertThat(result.toString()).isEqualTo(expected); - } + } + + @Test + public void queryAuthorBooksWithCollectionOrderBy() { + //given + String query = "query { " + + "Authors {" + + " select {" + + " id" + + " name(orderBy: ASC)" + + " books {" + + " id" + + " title(orderBy: DESC)" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Authors={select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=7, title=Three Sisters}, " + + "{id=6, title=The Seagull}, " + + "{id=5, title=The Cherry Orchard}" + + "]}, " + + "{id=8, name=Igor Dianov, books=[]}, " + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=2, title=War and Peace}, " + + "{id=3, title=Anna Karenina}" + + "]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryBooksAuthorWithImplicitOptionalFalse() { + //given + String query = "query { " + + "Books {" + + " select {" + + " id" + + " title" + + " author(where: {name: {LIKE: \"Leo\"}}) {" + + " name" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, author={name=Leo Tolstoy}}, " + + "{id=3, title=Anna Karenina, author={name=Leo Tolstoy}}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryAuthorBooksByAlliasesWithInlineCollections() { + //given + String query = "query { " + + " Authors {" + + " select {" + + " id" + + " name" + + " War: books(where: {title: {LIKE: \"War\"}}) {" + + " title" + + " }" + + " Anna: books(where: {title: {LIKE: \"Anna\"}}) {" + + " title" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, War=[{title=War and Peace}], Anna=[{title=Anna Karenina}]}, " + + "{id=4, name=Anton Chekhov, War=[], Anna=[]}, " + + "{id=8, name=Igor Dianov, War=[], Anna=[]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryAuthorBooksByAlliasesWithInlineWhereSearch() { + //given + String query = "query { " + + " Authors(where: {name: {LIKE: \"Leo\"}}) {" + + " select {" + + " id" + + " name" + + " War: books(where: {title: {LIKE: \"War\"}}) {" + + " title" + + " }" + + " Anna: books(where: {title: {LIKE: \"Anna\"}}) {" + + " title" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, War=[{title=War and Peace}], Anna=[{title=Anna Karenina}]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryAuthorBooksWithIsNullId() { //given String query = "query { " - + "Authors(" + - " where: {" + - " books: {" + - " id: {IS_NULL: true}" + - " }" + - " }" + - " ) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors(" + + " where: {" + + " books: {" + + " id: {IS_NULL: true}" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; - + String expected = "{Authors={select=[{id=8, name=Igor Dianov, books=[]}]}}"; //when @@ -464,14 +588,43 @@ public void queryAuthorBooksWithIsNullId() { // then assertThat(result.toString()).isEqualTo(expected); } - - + + @Test + public void queryBooksAuthorWithExplictOptionalTrue() { + //given + String query = "query { " + + "Books {" + + " select {" + + " id" + + " title" + + " author(optional: true, where: {name: {LIKE: \"Leo\"}}) {" + + " name" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, author={name=Leo Tolstoy}}, " + + "{id=3, title=Anna Karenina, author={name=Leo Tolstoy}}, " + + "{id=5, title=The Cherry Orchard, author=null}, " + + "{id=6, title=The Seagull, author=null}, " + + "{id=7, title=Three Sisters, author=null}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + // https://github.com/introproventures/graphql-jpa-query/issues/30 @Test public void queryForEntityWithMappedSuperclass() { //given String query = "{ Car(id: \"1\") { id brand } }"; - + String expected = "{Car={id=1, brand=Ford}}"; //when @@ -486,7 +639,7 @@ public void queryForEntityWithMappedSuperclass() { public void queryForEntityWithEmbeddedIdAndEmbeddedField() { //given String query = "{ Boat(boatId: {id: \"1\" country: \"EN\"}) { boatId {id country} engine { identification } } }"; - + String expected = "{Boat={boatId={id=1, country=EN}, engine={identification=12345}}}"; //when @@ -509,7 +662,7 @@ public void queryForEntityWithEmbeddedFieldWithWhere() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithNumericBetweenPredicate() { //given: @@ -578,8 +731,8 @@ public void queryWithDateNotBetweenPredicate() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForEntitiesWithWithEmbeddedIdWithWhere() { @@ -594,21 +747,21 @@ public void queryForEntitiesWithWithEmbeddedIdWithWhere() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForBooksWithWhereAuthorById() { //given String query = "query { " - + "Books(where: {author: {id: {EQ: 1}}}) {" + - " select {" + - " id" + - " title" + - " genre" + - " author {" + - " id" + - " name" + - " }" + - " }" + + + "Books(where: {author: {id: {EQ: 1}}}) {" + + " select {" + + " id" + + " title" + + " genre" + + " author {" + + " id" + + " name" + + " }" + + " }" + " }"+ "}"; @@ -622,20 +775,20 @@ public void queryForBooksWithWhereAuthorById() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForBooksWithWhereAuthorEqIdWithVariables() { //given String query = "query($authorId: Long ) { " - + " Books(where: {" + - " author: {id: {EQ: $authorId}}" + - " }) {" + - " select {" + - " id" + - " title" + - " genre" + - " }" + + + " Books(where: {" + + " author: {id: {EQ: $authorId}}" + + " }) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + " }"+ "}"; Map<String, Object> variables = new HashMap<String, Object>() {{ @@ -653,27 +806,27 @@ public void queryForBooksWithWhereAuthorEqIdWithVariables() { // then assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitle() { //given String query = "query { " - + "Authors(where: {" + - " EXISTS: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -689,28 +842,28 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitle() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDAuthorLIKEName() { //given String query = "query { " - + "Authors(where: {" + - " EXISTS: {" + - " books: {" + - " author: {name: {LIKE: \"Leo\"}}" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " author: {name: {LIKE: \"Leo\"}}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -726,31 +879,31 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDAuthorLIKEName() { // then assertThat(result.toString()).isEqualTo(expected); - } + } + - @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDEXISTSAuthorLIKEName() { //given String query = "query { " - + " Authors(where: {" + - " EXISTS: {" + - " books: {" + - " EXISTS: {" + - " author: {name: {LIKE: \"Leo\"}} " + - " }" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + " Authors(where: {" + + " EXISTS: {" + + " books: {" + + " EXISTS: {" + + " author: {name: {LIKE: \"Leo\"}} " + + " }" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -766,28 +919,28 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDEXISTSAuthorLIKEName( // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitleEmpty() { //given String query = "query { " - + "Authors(where: {" + - " EXISTS: {" + - " books: {" + - " author: {name: {LIKE: \"Anton\"}}" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " author: {name: {LIKE: \"Anton\"}}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -799,25 +952,25 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitleEmpty() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithWhereNOTEXISTSBooksLIKETitleWar() { //given String query = "query { " - + "Authors(where: {" + - " NOT_EXISTS: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + + + "Authors(where: {" + + " NOT_EXISTS: {" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + " }"+ " }"+ "}"; @@ -835,29 +988,29 @@ public void queryForAuthorsWithWhereNOTEXISTSBooksLIKETitleWar() { // then assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { //given String query = "query { " - + " Authors(where: {" + - " books: {" + - " NOT_EXISTS: {" + - " author: {" + - " name: {LIKE: \"Leo\"}" + - " }" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + " Authors(where: {" + + " books: {" + + " NOT_EXISTS: {" + + " author: {" + + " name: {LIKE: \"Leo\"}" + + " }" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -955,16 +1108,16 @@ public void queryTotalForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { public void queryForAuthorssWithWhereBooksGenreEquals() { //given String query = "query { " - + "Authors(where: {books: {genre: {EQ: NOVEL}}}) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors(where: {books: {genre: {EQ: NOVEL}}}) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -980,31 +1133,31 @@ public void queryForAuthorssWithWhereBooksGenreEquals() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForAuthorssWithWhereBooksManyToOneRelationCriteria() { //given String query = "query { " + - " Authors(where: {" + - " books: {" + - " author: {" + - " name: {LIKE: \"Leo\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author {" + - " name" + - " }" + - " }" + - " }" + + " Authors(where: {" + + " books: {" + + " author: {" + + " name: {LIKE: \"Leo\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author {" + + " name" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1020,28 +1173,28 @@ public void queryForAuthorssWithWhereBooksManyToOneRelationCriteria() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsImplicitAND() { //given: String query = "query { " - + "Authors(where: {" + - " books: {" + - " genre: {IN: NOVEL}" + - " title: {LIKE: \"War\"}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors(where: {" + + " books: {" + + " genre: {IN: NOVEL}" + + " title: {LIKE: \"War\"}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1058,29 +1211,29 @@ public void queryWithWhereInsideOneToManyRelationsImplicitAND() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideOneToManyRelationsImplicitANDWithEXISTS() { //given: String query = "query { " - + "Authors(where: {" + + + "Authors(where: {" + " EXISTS: {" + " books: {" + - " genre: {IN: NOVEL}" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " genre: {IN: NOVEL}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1098,29 +1251,29 @@ public void queryWithWhereInsideOneToManyRelationsImplicitANDWithEXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideOneToManyRelationsWithExplictAND() { //given: String query = "query { " - + "Authors(where: {" + - " books: {" + + + "Authors(where: {" + + " books: {" + " AND: { "+ - " genre: {IN: NOVEL}" + + " genre: {IN: NOVEL}" + " title: {LIKE: \"War\"}" + " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1135,31 +1288,31 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictAND() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsWithExplictANDEXISTS() { //given: String query = "query { " + "Authors(where: {" + " EXISTS: {" + - " books: {" + + " books: {" + " AND: { "+ - " genre: {IN: NOVEL}" + + " genre: {IN: NOVEL}" + " title: {LIKE: \"War\"}" + " }" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1174,29 +1327,29 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictANDEXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsWithExplictOR() { //given: String query = "query { " - + "Authors(where: {" + - " books: {" + + + "Authors(where: {" + + " books: {" + " OR: { "+ - " genre: {IN: NOVEL}" + + " genre: {IN: NOVEL}" + " title: {LIKE: \"War\"}" + " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1210,32 +1363,32 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictOR() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyNestedRelationsWithManyToOneAndOR() { //given: String query = "query { " + - " Authors(where: {" + - " books: {" + + " Authors(where: {" + + " books: {" + " author: {name: {LIKE:\"Leo\"}}" + " AND: {" + - " OR: {" + - " id: {EQ: 2}" + - " title: {LIKE: \"Anna\"}" + - " }" + + " OR: {" + + " id: {EQ: 2}" + + " title: {LIKE: \"Anna\"}" + + " }" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + " }" + "}"; @@ -1251,29 +1404,29 @@ public void queryWithWhereInsideOneToManyNestedRelationsWithManyToOneAndOR() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyNestedRelationsWithOneToManyDeepSelect() { //given: String query = "query { " + - " Authors(where: {" + - " books: {" + - " author: {name: {LIKE:\"Leo\"}}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author {" + - " name" + - " }" + - " }" + - " }" + + " Authors(where: {" + + " books: {" + + " author: {name: {LIKE:\"Leo\"}}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author {" + + " name" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1289,34 +1442,34 @@ public void queryWithWhereInsideOneToManyNestedRelationsWithOneToManyDeepSelect( //then: assertThat(result.toString()).isEqualTo(expected); - } - - + } + + @Test public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFilter() { //given: String query = "query { " + - " Books(where: {" + - " title:{LIKE: \"War\"}" + - " author: {" + - " name:{LIKE: \"Leo\"}" + - " books: {title: {LIKE: \"Anna\"}}" + - " }" + - " }) {" + - " select {" + - " id" + - " title" + - " genre" + - " author {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + " Books(where: {" + + " title:{LIKE: \"War\"}" + + " author: {" + + " name:{LIKE: \"Leo\"}" + + " books: {title: {LIKE: \"Anna\"}}" + + " }" + + " }) {" + + " select {" + + " id" + + " title" + + " genre" + + " author {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1336,35 +1489,35 @@ public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFi //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFilterEXISTS() { //given: String query = "query { " + - " Books(where: {" + - " title:{LIKE: \"War\"}" + + " Books(where: {" + + " title:{LIKE: \"War\"}" + " EXISTS: {" + - " author: {" + - " name:{LIKE: \"Leo\"}" + - " books: {title: {LIKE: \"Anna\"}}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " title" + - " genre" + - " author {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + " author: {" + + " name:{LIKE: \"Leo\"}" + + " books: {title: {LIKE: \"Anna\"}}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " title" + + " genre" + + " author {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1385,25 +1538,25 @@ public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFi //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithOneToManyNestedRelationsWithManyToOneOptionalTrue() { //given: String query = "query { " + - " Authors {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author(optional: true) {" + - " id" + - " }" + - " }" + - " }" + + " Authors {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author(optional: true) {" + + " id" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1425,25 +1578,25 @@ public void queryWithOneToManyNestedRelationsWithManyToOneOptionalTrue() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithOneToManyNestedRelationsWithManyToOneOptionalFalse() { //given: String query = "query { " + - " Authors {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author(optional: false) {" + - " id" + - " }" + - " }" + - " }" + + " Authors {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author(optional: false) {" + + " id" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1464,8 +1617,8 @@ public void queryWithOneToManyNestedRelationsWithManyToOneOptionalFalse() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void ignoreFilter() { //given @@ -1553,21 +1706,21 @@ public void titleOrder() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithDefaultOptionalBooks() { //given String query = "query { " - + "Authors {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -1590,21 +1743,21 @@ public void queryForAuthorsWithDefaultOptionalBooks() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithExlicitOptionalBooksFalse() { //given String query = "query { " - + "Authors {" + - " select {" + - " id" + - " name" + - " books(optional: false) {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors {" + + " select {" + + " id" + + " name" + + " books(optional: false) {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -1626,21 +1779,21 @@ public void queryForAuthorsWithExlicitOptionalBooksFalse() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithExlicitOptionalBooksTrue() { //given String query = "query { " - + "Authors {" + - " select {" + - " id" + - " name" + - " books(optional: true) {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -1685,7 +1838,7 @@ public void queryForTransientMethodAnnotatedWithGraphQLIgnoreShouldFail() { .extracting("validationErrorType", "queryPath") .containsOnly(tuple(ValidationErrorType.FieldUndefined, list("Books", "select", "authorName"))); } - + @Test public void queryWithEQNotMatchingCase() { //given: @@ -1699,7 +1852,7 @@ public void queryWithEQNotMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithEQMatchingCase() { //given: @@ -1715,7 +1868,7 @@ public void queryWithEQMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithLOWERNotMatchingCase() { //given: @@ -1731,7 +1884,7 @@ public void queryWithLOWERNotMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithLOWERMatchingCase() { //given: @@ -1747,7 +1900,7 @@ public void queryWithLOWERMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithEQCaseInsensitive() { //given: @@ -1793,7 +1946,7 @@ public void queryWithEQCaseSensitiveNotMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithNECaseInsensitive() { //given: @@ -1842,7 +1995,7 @@ public void queryWithNECaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithLIKECaseInsensitive() { //given: @@ -1888,7 +2041,7 @@ public void queryWithLIKECaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithSTARTSCaseInsensitive() { //given: @@ -1934,7 +2087,7 @@ public void queryWithSTARTSCaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithENDSCaseInsensitive() { //given: @@ -1965,7 +2118,7 @@ public void queryWithENDSCaseSensitive() { //then: assertThat(result.toString()).isEqualTo(expected); } - + public void queryWithENDSCaseSensitiveNonMatching() { //given: String query = "query { Books ( where: { title: {ENDS : \"peace\"}}) { select { id title} } }"; @@ -1977,32 +2130,32 @@ public void queryWithENDSCaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void shouldNotReturnStaleCacheResultsFromPreviousQueryForCollectionCriteriaExpression() { //given: - String query = "query ($genre: Genre) {" + - " Authors(where: { " + - " books: {" + - " genre: {EQ: $genre}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + String query = "query ($genre: Genre) {" + + " Authors(where: { " + + " books: {" + + " genre: {EQ: $genre}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + "}"; //when: 1st query Object result1 = executor.execute(query, Collections.singletonMap("genre", "PLAY")).getData(); - + String expected1 = "{Authors={select=[" + "{id=4, name=Anton Chekhov, books=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " @@ -2013,10 +2166,10 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForCollectionCriter //then: assertThat(result1.toString()).isEqualTo(expected1); - + //when: 2nd query Object result2 = executor.execute(query, Collections.singletonMap("genre", "NOVEL")).getData(); - + String expected2 = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " @@ -2027,51 +2180,55 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForCollectionCriter //then: assertThat(result2.toString()).isEqualTo(expected2); } - + @Test public void shouldNotReturnStaleCacheResultsFromPreviousQueryForEmbeddedCriteriaExpression() { //given: - String query = "query ($genre: Genre) {" + - " Authors {" + - " select {" + - " id" + - " name" + - " books(where:{ genre: {EQ: $genre} }) {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + String query = "query ($genre: Genre) {" + + " Authors {" + + " select {" + + " id" + + " name" + + " books(where:{ genre: {EQ: $genre} }) {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + "}"; //when: 1st query Object result1 = executor.execute(query, Collections.singletonMap("genre", "PLAY")).getData(); - + String expected1 = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[]}, " + "{id=4, name=Anton Chekhov, books=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + "{id=6, title=The Seagull, genre=PLAY}, " + "{id=7, title=Three Sisters, genre=PLAY}" - + "]}" + + "]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; //then: assertThat(result1.toString()).isEqualTo(expected1); - + //when: 2nd query Object result2 = executor.execute(query, Collections.singletonMap("genre", "NOVEL")).getData(); - + String expected2 = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" - + "]}" + + "]}, " + + "{id=4, name=Anton Chekhov, books=[]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; //then: assertThat(result2.toString()).isEqualTo(expected2); - } + } @Test public void queryWithEnumParameterShouldExecuteWithNoError() { @@ -2114,13 +2271,13 @@ public void queryWithEnumParameterShouldExecuteWithNoError() { "War and Peace") ); } - + // https://github.com/introproventures/graphql-jpa-query/issues/198 @Test public void queryOptionalElementCollections() { //given String query = "{ Author(id: 8) { id name phoneNumbers books { id title tags } } }"; - + String expected = "{Author={id=8, name=Igor Dianov, phoneNumbers=[], books=[]}}"; //when @@ -2128,19 +2285,19 @@ public void queryOptionalElementCollections() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryElementCollectionsWithWhereCriteriaExpression() { //given: - String query = "query {" + - " Books(where: {tags: {EQ: \"war\"}}) {" + - " select {" + - " id" + - " title" + - " tags" + - " }" + - " }" + + String query = "query {" + + " Books(where: {tags: {EQ: \"war\"}}) {" + + " select {" + + " id" + + " title" + + " tags" + + " }" + + " }" + "}"; String expected = "{Books={select=[{id=2, title=War and Peace, tags=[piece, war]}]}}"; @@ -2151,12 +2308,12 @@ public void queryElementCollectionsWithWhereCriteriaExpression() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithNullVarables() { //given String query = "{ Author(id: 1) { id name } }"; - + String expected = "{Author={id=1, name=Leo Tolstoy}}"; //when @@ -2165,5 +2322,5 @@ public void queryWithNullVarables() { // then assertThat(result.toString()).isEqualTo(expected); } - + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java index b9dcd9eb3..e9062d7ba 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java @@ -233,17 +233,14 @@ public void queryWithPropertyWhereVariableBinding() throws IOException { // then then(executionResult.getErrors()).isEmpty(); Map<String, Object> result = executionResult.getData(); - then(result) - .extracting("Authors") - .flatExtracting("select") - .extracting("name") - .containsOnly("Leo Tolstoy"); - then(result) - .extracting("Authors") - .flatExtracting("select") - .flatExtracting("books") - .extracting("genre") - .containsOnly(NOVEL); + + String expected = "{Authors={select=[" + + "{name=Leo Tolstoy, books=[{genre=NOVEL}, {genre=NOVEL}]}, " + + "{name=Anton Chekhov, books=[]}, " + + "{name=Igor Dianov, books=[]}" + + "]}}"; + + then(result.toString()).isEqualTo(expected); } @Test diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java index c59eccde8..f67d6eeb0 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java @@ -40,6 +40,7 @@ import com.introproventures.graphql.jpa.query.schema.model.starwars.Droid; @SpringBootTest +@Transactional public class StarwarsQueryExecutorTests extends AbstractSpringBootTestSupport { @SpringBootApplication @@ -83,7 +84,7 @@ public void JPASampleTester() { assertThat(result).isNotEmpty(); assertThat(result).hasSize(13); } - + @Test @Transactional public void queryManyToManyTester() { @@ -105,11 +106,11 @@ public void queryManyToManyTester() { assertThat(result.get(0).getName()).isEqualTo("C-3PO"); assertThat(result.get(0).getFriends()).hasSize(3); assertThat(result.get(0).getFriends()).extracting(Character::getName) - .containsOnly("Han Solo", "Leia Organa", "R2-D2"); + .containsOnly("Han Solo", "Leia Organa", "R2-D2"); assertThat(result.get(0).getFriends()).flatExtracting(Character::getFriendsOf) .extracting(Character::getName) .containsOnly("Luke Skywalker"); - + assertThat(result.get(1).getName()).isEqualTo("R2-D2"); assertThat(result.get(1).getFriends()).hasSize(2); assertThat(result.get(1).getFriends()).extracting(Character::getName) @@ -171,7 +172,44 @@ public void queryManyToOneJoinByIdWithVariables() { put("id", "2001"); }}; - String expected = "{Humans={select=[{name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2}}]}}"; + String expected = "{Humans={select=[" + + "{name=Luke Skywalker, homePlanet=Tatooine, favoriteDroid=null}, " + + "{name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2}}, " + + "{name=Han Solo, homePlanet=null, favoriteDroid=null}, " + + "{name=Leia Organa, homePlanet=Alderaan, favoriteDroid=null}, " + + "{name=Wilhuff Tarkin, homePlanet=null, favoriteDroid=null}" + + "]}}"; + + //when: + Object result = executor.execute(query,variables).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @SuppressWarnings("serial") + @Test + public void queryManyToOneJoinByIdWithVariablesOptionalFalse() { + //given: + String query = "query ($id: String!) {" + + " Humans {" + + " select {" + + " name" + + " homePlanet" + + " favoriteDroid(where: {id: {EQ: $id}}, optional: false) {" + + " name" + + " }" + + " }" + + " }" + + "}" + + ""; + Map<String, Object> variables = new HashMap<String, Object>() {{ + put("id", "2001"); + }}; + + String expected = "{Humans={select=[" + + "{name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2}}" + + "]}}"; //when: Object result = executor.execute(query,variables).getData(); @@ -269,7 +307,7 @@ public void queryDeepNesting() { + "{name=Leia Organa, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + "{name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}" + "]}}"; - + //when: Object result = executor.execute(query).getData(); @@ -289,7 +327,7 @@ public void queryDeepNestingPlural() { + "{name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}" + "]" + "}]}}"; - + //when: Object result = executor.execute(query).getData(); @@ -340,7 +378,7 @@ public void queryWhereRootPagedWithVariables() { put("limit", 2); }}; - + String expected = "{Humans={pages=3, total=5, select=[{name=Luke Skywalker}, {name=Darth Vader}]}}"; //when: @@ -349,7 +387,7 @@ public void queryWhereRootPagedWithVariables() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryPaginationWithoutRecords() { //given: @@ -450,14 +488,52 @@ public void queryByCollectionOfEnumsAtRootLevel() { @Test public void queryByRestrictingSubObject() { //given: - String query = "query { Humans { select { name gender(where:{ code: {EQ: \"Male\"}}) { description } } } }"; + String query = "query {" + + " Humans {" + + " select {" + + " name" + + " gender(where: {code: {EQ: \"Male\"}}) {" + + " description" + + " }" + + " }" + + " }" + + "}"; String expected = "{Humans={select=[" - + "{name=Luke Skywalker, gender={description=Male}}, " - + "{name=Darth Vader, gender={description=Male}}, " - + "{name=Han Solo, gender={description=Male}}, " - + "{name=Wilhuff Tarkin, gender={description=Male}}" - + "]}}"; + + "{name=Luke Skywalker, gender={description=Male}}, " + + "{name=Darth Vader, gender={description=Male}}, " + + "{name=Han Solo, gender={description=Male}}, " + + "{name=Leia Organa, gender=null}, " + + "{name=Wilhuff Tarkin, gender={description=Male}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryByRestrictingSubObjectOptionalFalse() { + //given: + String query = "query {" + + " Humans {" + + " select {" + + " name" + + " gender(where: {code: {EQ: \"Male\"}}, optional: false) {" + + " description" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Humans={select=[" + + "{name=Luke Skywalker, gender={description=Male}}, " + + "{name=Darth Vader, gender={description=Male}}, " + + "{name=Han Solo, gender={description=Male}}, " + + "{name=Wilhuff Tarkin, gender={description=Male}}" + + "]}}"; //when: Object result = executor.execute(query).getData(); @@ -567,14 +643,14 @@ public void queryWithTypenameDeepNesting() { + "{name=Han Solo, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=Leia Organa, __typename=Character}, {name=Luke Skywalker, __typename=Character}, {name=R2-D2, __typename=Character}], __typename=Character}, " + "{name=Leia Organa, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO, __typename=Character}, {name=Han Solo, __typename=Character}, {name=Luke Skywalker, __typename=Character}, {name=R2-D2, __typename=Character}], __typename=Character}, " + "{name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO, __typename=Character}, {name=Han Solo, __typename=Character}, {name=Leia Organa, __typename=Character}, {name=R2-D2, __typename=Character}], __typename=Character}], __typename=Droid}}"; - + //when: Object result = executor.execute(query).getData(); //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithTypenameSimple() { //given: @@ -590,7 +666,7 @@ public void queryWithTypenameSimple() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithStringBetweenPredicate() { //given: @@ -624,24 +700,24 @@ public void queryWithStringNotBetweenPredicate() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideManyToOneRelations() { //given: - String query = "query {" + - " Humans(where: {" + - " favoriteDroid: {appearsIn: {IN: [A_NEW_HOPE]}}" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " appearsIn" + - " }" + - " }" + - " }" + + String query = "query {" + + " Humans(where: {" + + " favoriteDroid: {appearsIn: {IN: [A_NEW_HOPE]}}" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " appearsIn" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -654,24 +730,24 @@ public void queryWithWhereInsideManyToOneRelations() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideManyToOneRelationsNotExisting() { //given: - String query = "query {" + - " Humans(where: {" + - " favoriteDroid: {appearsIn: {IN: [PHANTOM_MENACE]}}" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " appearsIn" + - " }" + - " }" + - " }" + + String query = "query {" + + " Humans(where: {" + + " favoriteDroid: {appearsIn: {IN: [PHANTOM_MENACE]}}" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " appearsIn" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[]}}"; @@ -681,30 +757,30 @@ public void queryWithWhereInsideManyToOneRelationsNotExisting() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsNotExisting() { //given: - String query = "query {" + - " Humans(where: {" + - " friends: {appearsIn: {EQ: PHANTOM_MENACE}}" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " primaryFunction { function }" + - " appearsIn" + - " }" + - " friends {" + - " id" + - " name" + - " appearsIn" + - " }" + - " }" + - " }" + + String query = "query {" + + " Humans(where: {" + + " friends: {appearsIn: {EQ: PHANTOM_MENACE}}" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " primaryFunction { function }" + + " appearsIn" + + " }" + + " friends {" + + " id" + + " name" + + " appearsIn" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[]}}"; @@ -714,40 +790,35 @@ public void queryWithWhereInsideOneToManyRelationsNotExisting() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering() { //given: - String query = "query {" + - " Characters(where: {" + - " friends: {appearsIn: {IN: A_NEW_HOPE}}" + - " }) {" + - " select {" + - " id" + - " name" + - " appearsIn" + - " friends(where: {name: {LIKE: \"Leia\"}}) {" + - " id" + - " name" + - " }" + - " }" + - " }" + + String query = "query {" + + " Characters(where: {" + + " friends: {appearsIn: {IN: A_NEW_HOPE}}" + + " }) {" + + " select {" + + " id" + + " name" + + " appearsIn" + + " friends(where: {name: {LIKE: \"Leia\"}}) {" + + " id" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Characters={select=[" - + "{id=1000, name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}, " - + "{id=1002, name=Han Solo, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}, " - + "{id=2000, name=C-3PO, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}, " - + "{id=2001, name=R2-D2, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}" + + "{id=1000, name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}, " + + "{id=1001, name=Darth Vader, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI], friends=[]}, " + + "{id=1002, name=Han Solo, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}, " + + "{id=1003, name=Leia Organa, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[]}, " + + "{id=1004, name=Wilhuff Tarkin, appearsIn=[A_NEW_HOPE], friends=[]}, " + + "{id=2000, name=C-3PO, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}, " + + "{id=2001, name=R2-D2, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}" + "]}}"; //when: @@ -755,32 +826,32 @@ public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering2() { //given: - String query = "query {" + - " Humans(where: {" + - " favoriteDroid: { id: {EQ: \"2000\"}}" + - " friends: {" + - " appearsIn: {IN: A_NEW_HOPE}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " id" + - " name" + - " primaryFunction { function }" + - " }" + - " friends(where: {name: {LIKE: \"Leia\"}}) {" + - " id" + - " name" + - " }" + - " }" + - " } " + + String query = "query {" + + " Humans(where: {" + + " favoriteDroid: { id: {EQ: \"2000\"}}" + + " friends: {" + + " appearsIn: {IN: A_NEW_HOPE}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " id" + + " name" + + " primaryFunction { function }" + + " }" + + " friends(where: {name: {LIKE: \"Leia\"}}) {" + + " id" + + " name" + + " }" + + " }" + + " } " + "}"; String expected = "{Humans={select=[" @@ -792,25 +863,25 @@ public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering2() { //then: assertThat(result.toString()).isEqualTo(expected); - } - - + } + + @Test public void queryWithWhereInsideOneToManyRelations() { //given: String query = "query { " - + " Humans(where: {friends: {appearsIn: {IN: A_NEW_HOPE}} }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " }" + - " friends {" + - " name" + - " appearsIn" + - " }" + - " }" + + + " Humans(where: {friends: {appearsIn: {IN: A_NEW_HOPE}} }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " }" + + " friends {" + + " name" + + " appearsIn" + + " }" + + " }" + " }" + "}"; @@ -855,21 +926,21 @@ public void queryWithWhereInsideOneToManyRelations() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryHumansWithFavoriteDroidDefaultOptionalTrue() { //given: String query = "query { " - + "Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " }" + - " }" + + + "Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -886,21 +957,21 @@ public void queryHumansWithFavoriteDroidDefaultOptionalTrue() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryHumansWittFavorideDroidExplicitOptionalFalse() { //given: String query = "query { " - + "Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid(optional: false) {" + - " name" + - " }" + - " }" + - " }" + + + "Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid(optional: false) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -914,23 +985,23 @@ public void queryHumansWittFavorideDroidExplicitOptionalFalse() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryHumansWittFavorideDroidExplicitOptionalFalseParameterBinding() { //given: String query = "query($optional: Boolean) { " - + "Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid(optional: $optional) {" + - " name" + - " }" + - " }" + - " }" + + + "Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid(optional: $optional) {" + + " name" + + " }" + + " }" + + " }" + "}"; - + Map<String, Object> variables = Collections.singletonMap("optional", false); String expected = "{Humans={select=[" @@ -943,19 +1014,53 @@ public void queryHumansWittFavorideDroidExplicitOptionalFalseParameterBinding() //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryFilterManyToOneEmbdeddedCriteria() { //given: - String query = "query { Droids { select { name primaryFunction(where: {function: {EQ:\"Astromech\"}}) { function }}}}"; + String query = "query {" + + " Droids {" + + " select {" + + " name" + + " primaryFunction(where: {function: {EQ: \"Astromech\"}}) {" + + " function" + + " }" + + " }" + + " }" + + "}" + + ""; - String expected = "{Droids={" + - "select=[{" + - "name=R2-D2, " + - "primaryFunction={function=Astromech}" + - "}]" + - "}}"; + String expected = "{Droids={select=[" + + "{name=C-3PO, primaryFunction=null}, " + + "{name=R2-D2, primaryFunction={function=Astromech}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryFilterManyToOneEmbdeddedCriteriaOptionalFalse() { + //given: + String query = "query {" + + " Droids {" + + " select {" + + " name" + + " primaryFunction(where: {function: {EQ: \"Astromech\"}} optional: false) {" + + " function" + + " }" + + " }" + + " }" + + "}" + + ""; + + String expected = "{Droids={select=[" + + "{name=R2-D2, primaryFunction={function=Astromech}}" + + "]}}"; //when: Object result = executor.execute(query).getData(); @@ -968,13 +1073,13 @@ public void queryFilterManyToOneEmbdeddedCriteria() { public void queryFilterManyToOnRelationCriteria() { //given: String query = "query { " + - " Droids(where: {primaryFunction: { function: {EQ:\"Astromech\"}}}) { " + - " select {" + - " name " + - " primaryFunction {" + - " function" + - " }" + - " }" + + " Droids(where: {primaryFunction: { function: {EQ:\"Astromech\"}}}) { " + + " select {" + + " name " + + " primaryFunction {" + + " function" + + " }" + + " }" + " }" + "}"; @@ -982,7 +1087,7 @@ public void queryFilterManyToOnRelationCriteria() { "select=[{" + "name=R2-D2, " + "primaryFunction={function=Astromech}" + - "}]" + + "}]" + "}}"; //when: @@ -991,7 +1096,7 @@ public void queryFilterManyToOnRelationCriteria() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryFilterNestedManyToOneToDo() { //given: @@ -1011,21 +1116,13 @@ public void queryFilterNestedManyToOneToDo() { " }" + "}"; - String expected = "{Humans={" + - "select=[" + - "{" + - "id=1001, " + - "name=Darth Vader, " + - "homePlanet=Tatooine, " + - "favoriteDroid={" + - "name=R2-D2, " + - "primaryFunction={" + - "function=Astromech" + - "}" + - "}" + - "}" + - "]" + - "}}"; + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, homePlanet=Tatooine, favoriteDroid={name=C-3PO, primaryFunction=null}}, " + + "{id=1001, name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2, primaryFunction={function=Astromech}}}, " + + "{id=1002, name=Han Solo, homePlanet=null, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, homePlanet=Alderaan, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, homePlanet=null, favoriteDroid=null}" + + "]}}"; //when: Object result = executor.execute(query).getData(); @@ -1033,7 +1130,38 @@ public void queryFilterNestedManyToOneToDo() { //then: assertThat(result.toString()).isEqualTo(expected); } - + + @Test + public void queryFilterNestedManyToOneToDoOptionalFalse() { + //given: + String query = "query {" + + " Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " primaryFunction(where:{function:{EQ:\"Astromech\"}}, optional: false) {" + + " function" + + " }" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Humans={select=[" + + "{id=1001, name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2, primaryFunction={function=Astromech}}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test public void queryFilterNestedManyToOneRelationCriteria() { //given: @@ -1075,29 +1203,29 @@ public void queryFilterNestedManyToOneRelationCriteria() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryFilterNestedManyToManyRelationCriteria() { //given: String query = "query {" + - " Humans(where: {" + - " friends: { name: { LIKE: \"Leia\" } } " + - " favoriteDroid: { primaryFunction: { function: { EQ: \"Protocol\" } } }" + - " }) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " primaryFunction {" + - " function" + - " }" + - " }" + - " friends {" + - " name" + - " }" + - " }" + + " Humans(where: {" + + " friends: { name: { LIKE: \"Leia\" } } " + + " favoriteDroid: { primaryFunction: { function: { EQ: \"Protocol\" } } }" + + " }) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " friends {" + + " name" + + " }" + + " }" + " } " + "}"; @@ -1129,24 +1257,24 @@ public void queryFilterNestedManyToManyRelationCriteriaWithEXISTS() { String query = "query {" + " Humans(where: {" + " EXISTS: {" + - " friends: { name: { LIKE: \"Leia\" } } " + + " friends: { name: { LIKE: \"Leia\" } } " + " favoriteDroid: { primaryFunction: { function: { EQ: \"Protocol\" } } }" + " }"+ - " }) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " primaryFunction {" + - " function" + - " }" + - " }" + - " friends {" + - " name" + - " }" + - " }" + + " }) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " friends {" + + " name" + + " }" + + " }" + " } " + "}"; @@ -1175,26 +1303,26 @@ public void queryFilterNestedManyToManyRelationCriteriaWithEXISTS() { assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithWhereInsideOneToManyRelationsShouldApplyFilterCriterias() { //given: String query = "query { " + " Humans(where: {" + "friends: {appearsIn: {IN: A_NEW_HOPE}} " - + "favoriteDroid: {name: {EQ: \"C-3PO\"}} " - + "}) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " }" + - " friends {" + - " name" + - " appearsIn" + - " }" + - " }" + + + "favoriteDroid: {name: {EQ: \"C-3PO\"}} " + + "}) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " }" + + " friends {" + + " name" + + " appearsIn" + + " }" + + " }" + " }" + "}"; @@ -1215,20 +1343,20 @@ public void queryWithWhereInsideOneToManyRelationsShouldApplyFilterCriterias() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithOneToManyRelationsShouldUseLeftOuterJoin() { //given: String query = "query { " + - " Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " }" + + " Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " }" + " }" + "}"; @@ -1246,21 +1374,21 @@ public void queryWithOneToManyRelationsShouldUseLeftOuterJoin() { //then: assertThat(result.toString()).isEqualTo(expected); } - - + + @Test public void queryWithWhereOneToManyRelationsShouldUseLeftOuterJoinAndApplyCriteria() { //given: String query = "query { " + - " Humans(where: {favoriteDroid: {name: {EQ: \"C-3PO\"}}}) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " }" + + " Humans(where: {favoriteDroid: {name: {EQ: \"C-3PO\"}}}) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " }" + " }" + "}"; @@ -1279,14 +1407,14 @@ public void queryWithWhereOneToManyRelationsShouldUseLeftOuterJoinAndApplyCriter public void queryWithNestedWhereSearchCriteriaShouldFetchElementCollectionsAttributes() { //given: String query = "query { " + - " Characters (where: {" + - " appearsIn :{IN: THE_FORCE_AWAKENS}" + - " }) {" + - " select {" + - " id, " + - " name," + - " appearsIn" + - " }" + + " Characters (where: {" + + " appearsIn :{IN: THE_FORCE_AWAKENS}" + + " }) {" + + " select {" + + " id, " + + " name," + + " appearsIn" + + " }" + " }" + "}"; @@ -1304,25 +1432,25 @@ public void queryWithNestedWhereSearchCriteriaShouldFetchElementCollectionsAttri //then: assertThat(result.toString()).isEqualTo(expected); } - - + + @Test public void queryWithNestedWhereCompoundSearchCriteriaShouldFetchElementCollectionsAttributes() { //given: String query = "query { " + - " Humans(where:{" + - " favoriteDroid: {name: {EQ: \"C-3PO\"}} " + - " appearsIn: {IN: [THE_FORCE_AWAKENS]}" + - " }) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " appearsIn" + - " }" + + " Humans(where:{" + + " favoriteDroid: {name: {EQ: \"C-3PO\"}} " + + " appearsIn: {IN: [THE_FORCE_AWAKENS]}" + + " }) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " appearsIn" + + " }" + " }" + "}"; @@ -1340,19 +1468,19 @@ public void queryWithNestedWhereCompoundSearchCriteriaShouldFetchElementCollecti //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryShouldReturnDistictResultsByDefault() { //given: String query = "query { " + - " Humans (where: { " + - " appearsIn: {IN: [A_NEW_HOPE, EMPIRE_STRIKES_BACK]}" + - " }) {" + - " select {" + - " id" + - " name" + - " }" + + " Humans (where: { " + + " appearsIn: {IN: [A_NEW_HOPE, EMPIRE_STRIKES_BACK]}" + + " }) {" + + " select {" + + " id" + + " name" + + " }" + " }" + "}"; @@ -1369,25 +1497,27 @@ public void queryShouldReturnDistictResultsByDefault() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseEQ() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1395,26 +1525,28 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseEQ() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void querySetOfEnumsWithinEmbeddedSelectClauseEQArray() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1422,26 +1554,28 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseEQArray() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseIN() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {IN: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {IN: THE_FORCE_AWAKENS}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1450,25 +1584,27 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseIN() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseINArray() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {IN: [THE_FORCE_AWAKENS]}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {IN: [THE_FORCE_AWAKENS]}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1476,24 +1612,27 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseINArray() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNE() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1502,24 +1641,27 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNE() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNEArray() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1529,24 +1671,27 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNEArray() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNIN() { - - //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NIN: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + + //given: + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NIN: THE_FORCE_AWAKENS}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1555,25 +1700,28 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNIN() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNINArray() { - - //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + - " name" + - " }" + - " }" + - " }" + + + //given: + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1582,61 +1730,67 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNINArray() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryEmbeddedWhereWithPluralAssociations() { - + //given: String query = "{ " - + "Droids {" + - " select {" + - " name" + - " friends(where:{" + - " NOT_EXISTS:{ friends:{name:{EQ:\"R2-D2\"}}}" + - " }) {" + - " name" + - " friends {" + - " name" + - " }" + - " }" + - " } " + - " } " + + + "Droids {" + + " select {" + + " name" + + " friends(where:{" + + " NOT_EXISTS:{ friendsOf: {name:{EQ:\"R2-D2\"}}}" + + " }) {" + + " name" + + " friendsOf {" + + " name" + + " }" + + " }" + + " } " + + " } " + "}"; String expected = "{Droids={select=[" + "{name=C-3PO, friends=[" - + "{name=R2-D2, friends=[" + + "{name=R2-D2, friendsOf=[" + + "{name=C-3PO}, " + "{name=Han Solo}, " + "{name=Leia Organa}, " + "{name=Luke Skywalker}" + "]}" - + "]}]}}"; + + "]}, " + + "{name=R2-D2, friends=[]}" + + "]}}"; //when: Object result = executor.execute(query).getData(); //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithPluralAssociationsNOT_EXISTS() { - - //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + - " name" + - " }" + - " }" + - " }" + + + //given: + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1645,26 +1799,26 @@ public void queryEmbeddedWhereWithPluralAssociationsNOT_EXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithNestedPluralAssociationsNOT_EXISTS() { - - //given: - String query = "{" + - " Droids {" + - " select {" + - " name" + - " friends(where:{" + - " NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}" + - " }) {" + - " name" + - " friends {" + - " name" + - " }" + - " }" + - " } " + - " }" + + + //given: + String query = "{" + + " Droids {" + + " select {" + + " name" + + " friends(where:{" + + " NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}" + + " }) {" + + " name" + + " friends {" + + " name" + + " }" + + " }" + + " } " + + " }" + "}"; String expected = "{Droids={select=[" @@ -1689,26 +1843,26 @@ public void queryEmbeddedWhereWithNestedPluralAssociationsNOT_EXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithRecursivePluralAssociationsNOT_EXISTS() { - - //given: - String query = "{" + - " Droids(where: {" + - " friends: {NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}}" + - " }) {" + - " select {" + - " name" + - " friends {" + - " name" + - " friends {" + - " name" + - " }" + - " }" + - " } " + - " }" + + + //given: + String query = "{" + + " Droids(where: {" + + " friends: {NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}}" + + " }) {" + + " select {" + + " name" + + " friends {" + + " name" + + " friends {" + + " name" + + " }" + + " }" + + " } " + + " }" + "}"; String expected = "{Droids={select=[" @@ -1733,26 +1887,26 @@ public void queryEmbeddedWhereWithRecursivePluralAssociationsNOT_EXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithManyToManyAssociations() { - - //given: - String query = "{" + - " Droids(where: {" + - " friends: {friendsOf:{name:{EQ:\"Leia Organa\"}}}" + - " }) {" + - " select {" + - " name" + - " friends {" + - " name" + - " friendsOf {" + - " name" + - " }" + - " }" + - " } " + - " } " + + + //given: + String query = "{" + + " Droids(where: {" + + " friends: {friendsOf:{name:{EQ:\"Leia Organa\"}}}" + + " }) {" + + " select {" + + " name" + + " friends {" + + " name" + + " friendsOf {" + + " name" + + " }" + + " }" + + " } " + + " } " + "}"; String expected = "{Droids={select=[" @@ -1772,34 +1926,35 @@ public void queryEmbeddedWhereWithManyToManyAssociations() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithManyToManyAssociationsUsingEXISTS() { - - //given: - String query = "{" + - " Droids {" + - " select {" + - " name" + - " friends(where: {EXISTS: {friendsOf:{name:{EQ:\"Leia Organa\"}}}}) {" + - " name" + - " friendsOf {" + - " name" + - " }" + - " }" + - " } " + - " } " + + + //given: + String query = "{" + + " Droids {" + + " select {" + + " name" + + " friends(where: {EXISTS: {friendsOf:{name:{EQ:\"Leia Organa\"}}}}) {" + + " name" + + " friendsOf {" + + " name" + + " }" + + " }" + + " } " + + " } " + "}"; String expected = "{Droids={select=[" + "{name=C-3PO, friends=[" - + "{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " + "{name=Luke Skywalker, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " + "{name=R2-D2, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=Luke Skywalker}]}" + "]}, " - + "{name=R2-D2, friends=[{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Luke Skywalker, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}" + + "{name=R2-D2, friends=[" + + "{name=Luke Skywalker, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}" + "]}" + "]}}"; @@ -1808,5 +1963,159 @@ public void queryEmbeddedWhereWithManyToManyAssociationsUsingEXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } + + @Test + public void queryWithInLineExplicitOptionalFalseForSingularAttribute() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(optional: false, where:{name:{LIKE:\"R%\"}}) {" + + " name" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2}}]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineExplicitOptionalTrueForSingularAttribute() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(optional:true, where:{name:{LIKE:\"R%\"}}) {" + + " name" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid=null}, " + + "{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2}}, " + + "{id=1002, name=Han Solo, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, favoriteDroid=null}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineImplictOptionalTrueForSingularAttribute() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO, primaryFunction={function=Protocol}}}, " + + "{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2, primaryFunction={function=Astromech}}}, " + + "{id=1002, name=Han Solo, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, favoriteDroid=null}]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineImplictOptionalTrueForSingularAttributeAndWhereSearchCriteria() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(where: {primaryFunction: {function: {EQ: \"Protocol\"}}}) {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO, primaryFunction={function=Protocol}}}, " + + "{id=1001, name=Darth Vader, favoriteDroid=null}, " + + "{id=1002, name=Han Solo, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, favoriteDroid=null}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineExplicitOptionalFalseForSingularAttributeAndWhereSearchCriteria() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(optional: false, where: {primaryFunction: {function: {EQ: \"Protocol\"}}}) {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO, primaryFunction={function=Protocol}}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + }