From c508f1ba6d476d720209c647ef9853de0391dd5f Mon Sep 17 00:00:00 2001 From: Michail Goncharov Date: Fri, 22 Feb 2019 11:05:33 +0300 Subject: [PATCH] support calcs fields and functions --- .../impl/CashGraphQLCalculatedFields.java | 66 ++++++++++++++++ .../schema/impl/GraphQLJpaSchemaBuilder.java | 71 ++++++++++++++++-- .../impl/QraphQLJpaBaseDataFetcher.java | 15 +--- .../jpa/query/schema/CalcEntityTests.java | 75 +++++++++++++++++++ .../query/schema/model/calc/CalcEntity.java | 46 ++++++++++++ .../src/test/resources/data.sql | 6 +- 6 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/CashGraphQLCalculatedFields.java create mode 100644 graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalcEntityTests.java create mode 100644 graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calc/CalcEntity.java diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/CashGraphQLCalculatedFields.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/CashGraphQLCalculatedFields.java new file mode 100644 index 000000000..262769fe1 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/CashGraphQLCalculatedFields.java @@ -0,0 +1,66 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + + +import javax.persistence.Transient; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class CashGraphQLCalculatedFields { + protected static Map>> cashCalcFields = new ConcurrentHashMap<>(); + + public static void clearCashCalcFields() { + cashCalcFields.values().stream().forEach(v -> v.clear()); + cashCalcFields.clear(); + } + + public static boolean isCalcField(Class cls, String field) { + if (cashCalcFields.containsKey(cls)) { + if (cashCalcFields.get(cls).containsKey(field)) { + return cashCalcFields.get(cls).get(field).isPresent(); + } + } + + Optional cf = getTransient(cls, field); + addCashCalcFields(cls, field, cf); + + return cf.isPresent(); + } + + public static void addCashCalcFields(Class cls, String field, Optional an) { + if (!cashCalcFields.containsKey(cls)) { + Map> tpMap = new ConcurrentHashMap<>(); + cashCalcFields.put(cls, tpMap); + } + + cashCalcFields.get(cls).put(field, an); + } + + public static Optional getTransient(Class cls, String field) { + Optional calcField = Arrays.stream(cls.getDeclaredFields()) + .filter(f -> f.getName().equals(field) && f.isAnnotationPresent(Transient.class)) + .map(f -> f.getAnnotation(Transient.class)) + .findFirst(); + + if (!calcField.isPresent()) { + calcField = getGraphQLCalcMethod(cls, field, "get"); + } + + if (!calcField.isPresent()) { + calcField = getGraphQLCalcMethod(cls, field, "is"); + } + + return calcField; + } + + public static Optional getGraphQLCalcMethod(Class cls, String field, String prefix) { + String methodName = prefix + field.substring(0,1).toUpperCase() + field.substring(1); + + return Arrays.stream(cls.getDeclaredMethods()) + .filter(m -> m.getName().equals(methodName) && m.isAnnotationPresent(Transient.class)) + .map(m -> m.getAnnotation(Transient.class)) + .findFirst() + ; + } +} 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 e08e3fbd6..4a35d16d5 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 @@ -24,16 +24,13 @@ import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.persistence.EntityManager; +import javax.persistence.Transient; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EmbeddableType; import javax.persistence.metamodel.EntityType; @@ -469,6 +466,8 @@ private GraphQLObjectType getObjectType(EntityType entityType) { .map(this::getObjectField) .collect(Collectors.toList()) ) + .fields(getObjectCalcFields(entityType.getJavaType())) + .fields(getObjectCalcMethods(entityType.getJavaType())) .build(); entityCache.putIfAbsent(entityType, objectType); @@ -476,6 +475,68 @@ private GraphQLObjectType getObjectType(EntityType entityType) { return objectType; } + private List getObjectCalcFields(Class cls) { + return + Arrays.stream(cls.getDeclaredFields()) + .filter( + f -> + f instanceof Member && + f.isAnnotationPresent(Transient.class) && + isNotIgnored((Member) f) && + isNotIgnored(f.getType()) + ) + .map(f -> getObjectCalcField(f)) + .collect(Collectors.toList()); + } + + private List getObjectCalcMethods(Class cls) { + return + Arrays.stream(cls.getDeclaredMethods()) + .filter( + m -> + m instanceof Member && + m.isAnnotationPresent(Transient.class) && + isNotIgnored((Member) m) && + isNotIgnored(m.getReturnType()) + ) + .map(m -> getObjectCalcMethtod(m)) + .collect(Collectors.toList()); + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + private GraphQLFieldDefinition getObjectCalcField(Field field) { + GraphQLType type = getGraphQLTypeFromJavaType(field.getType()); + DataFetcher dataFetcher = PropertyDataFetcher.fetching(field.getName()); + + return GraphQLFieldDefinition.newFieldDefinition() + .name(field.getName()) + .description(getSchemaDescription((AnnotatedElement) field)) + .type((GraphQLOutputType) type) + .dataFetcher(dataFetcher) + .build(); + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + private GraphQLFieldDefinition getObjectCalcMethtod(Method method) { + String nm = method.getName(); + if (nm.startsWith("is")) { + nm = Introspector.decapitalize(nm.substring(2)); + } + if (nm.startsWith("get")) { + nm = Introspector.decapitalize(nm.substring(3)); + } + + GraphQLType type = getGraphQLTypeFromJavaType(method.getReturnType()); + DataFetcher dataFetcher = PropertyDataFetcher.fetching(nm); + + return GraphQLFieldDefinition.newFieldDefinition() + .name(nm) + .description(getSchemaDescription((AnnotatedElement) method)) + .type((GraphQLOutputType) type) + .dataFetcher(dataFetcher) + .build(); + } + @SuppressWarnings( { "rawtypes", "unchecked" } ) private GraphQLFieldDefinition getObjectField(Attribute attribute) { GraphQLOutputType type = getAttributeOutputType(attribute); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java index c9627afe8..2fc8083e6 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java @@ -19,16 +19,7 @@ import static graphql.introspection.Introspection.TypeMetaFieldDef; import static graphql.introspection.Introspection.TypeNameMetaFieldDef; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -146,7 +137,7 @@ protected final List getFieldArguments(Field field, CriteriaQuery q Field selectedField = (Field) selection; // "__typename" is part of the graphql introspection spec and has to be ignored by jpa - if(!TYPENAME.equals(selectedField.getName())) { + if(!TYPENAME.equals(selectedField.getName()) && !CashGraphQLCalculatedFields.isCalcField(from.getJavaType(), selectedField.getName())) { Path fieldPath = from.get(selectedField.getName()); @@ -728,7 +719,7 @@ && isManagedType(entityType.getAttribute(it.getName())) Subgraph sg = entityGraph.addSubgraph(it.getName()); buildSubgraph(it, sg); } else { - if(!TYPENAME.equals(it.getName())) + if(!TYPENAME.equals(it.getName()) && !CashGraphQLCalculatedFields.isCalcField(entityType.getJavaType(), it.getName())) entityGraph.addAttributeNodes(it.getName()); } }); diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalcEntityTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalcEntityTests.java new file mode 100644 index 000000000..70641cb8d --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalcEntityTests.java @@ -0,0 +1,75 @@ +package com.introproventures.graphql.jpa.query.schema; + +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.Assert; + +import javax.persistence.EntityManager; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE) +@TestPropertySource({"classpath:hibernate.properties"}) +public class CalcEntityTests { + @SpringBootApplication + static class Application { + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + + return new GraphQLJpaSchemaBuilder(entityManager) + .name("GraphQLCalcFields") + .description("CalcFields JPA test schema"); + } + + } + + @Autowired + private GraphQLExecutor executor; + + @Test + public void contextLoads() { + Assert.isAssignable(GraphQLExecutor.class, executor.getClass()); + } + + @Test + public void getAllRecords() { + //given + String query = "query GraphQLCalcFields { CalcEntities { select {id title fieldMem fieldFun logic customLogic } } }"; + + String expected = "{CalcEntities={select=[{id=1, title=title 1, fieldMem=member, fieldFun=title 1 function, logic=true, customLogic=false}, {id=2, title=title 2, fieldMem=member, fieldFun=title 2 function, logic=true, customLogic=false}]}}"; + + //when + Object result = executor.execute(query).getData(); + + //then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void testIgnoreFields() { + String query = "query GraphQLCalcFields { CalcEntities { select {id title fieldMem fieldFun logic customLogic hideField hideFieldFunction } } }"; + + String expected = "[ValidationError{validationErrorType=FieldUndefined, queryPath=[CalcEntities, select, hideField], message=Validation error of type FieldUndefined: Field 'hideField' in type 'CalcEntity' is undefined @ 'CalcEntities/select/hideField', locations=[SourceLocation{line=1, column=95}], description='Field 'hideField' in type 'CalcEntity' is undefined'}, ValidationError{validationErrorType=FieldUndefined, queryPath=[CalcEntities, select, hideFieldFunction], message=Validation error of type FieldUndefined: Field 'hideFieldFunction' in type 'CalcEntity' is undefined @ 'CalcEntities/select/hideFieldFunction', locations=[SourceLocation{line=1, column=105}], description='Field 'hideFieldFunction' in type 'CalcEntity' is undefined'}]"; + + //when + Object result = executor.execute(query).getErrors(); + + //then + assertThat(result.toString()).isEqualTo(expected); + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calc/CalcEntity.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calc/CalcEntity.java new file mode 100644 index 000000000..7411a33a8 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calc/CalcEntity.java @@ -0,0 +1,46 @@ +package com.introproventures.graphql.jpa.query.schema.model.calc; + +import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; +import lombok.Data; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Transient; + +@Data +@Entity +public class CalcEntity { + @Id + Long id; + + String title; + + String info; + + @Transient + boolean logic = true; + + @Transient + @GraphQLDescription("i desc member") + String fieldMem = "member"; + + @Transient + @GraphQLIgnore + String hideField = "hideField"; + + @Transient + @GraphQLDescription("i desc function") + public String getFieldFun() { + return title + " function"; + } + + @Transient + public boolean isCustomLogic() { + return false; + } + + public String getHideFieldFunction() { + return "getHideFieldFunction"; + } +} diff --git a/graphql-jpa-query-schema/src/test/resources/data.sql b/graphql-jpa-query-schema/src/test/resources/data.sql index 30ddb158b..9c580088e 100644 --- a/graphql-jpa-query-schema/src/test/resources/data.sql +++ b/graphql-jpa-query-schema/src/test/resources/data.sql @@ -133,4 +133,8 @@ insert into Boat (id, country, identification) values (1, 'EN', '12345'), (2, 'EN', '23456'), (1, 'FR', '34567'); - \ No newline at end of file + +-- Calculate entity +insert into calc_entity (id, title, info) values + (1, 'title 1', 'inf 1'), + (2, 'title 2', 'inf 2');