Skip to content

Commit 1cbf29c

Browse files
miklemvigdianov
authored andcommitted
support calcs fields and functions (#85)
1 parent 0def68d commit 1cbf29c

File tree

6 files changed

+261
-18
lines changed

6 files changed

+261
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.introproventures.graphql.jpa.query.schema.impl;
2+
3+
4+
import javax.persistence.Transient;
5+
import java.util.Arrays;
6+
import java.util.Map;
7+
import java.util.Optional;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
10+
public class CashGraphQLCalculatedFields {
11+
protected static Map<Class, Map<String, Optional<Transient>>> cashCalcFields = new ConcurrentHashMap<>();
12+
13+
public static void clearCashCalcFields() {
14+
cashCalcFields.values().stream().forEach(v -> v.clear());
15+
cashCalcFields.clear();
16+
}
17+
18+
public static boolean isCalcField(Class cls, String field) {
19+
if (cashCalcFields.containsKey(cls)) {
20+
if (cashCalcFields.get(cls).containsKey(field)) {
21+
return cashCalcFields.get(cls).get(field).isPresent();
22+
}
23+
}
24+
25+
Optional<Transient> cf = getTransient(cls, field);
26+
addCashCalcFields(cls, field, cf);
27+
28+
return cf.isPresent();
29+
}
30+
31+
public static void addCashCalcFields(Class cls, String field, Optional<Transient> an) {
32+
if (!cashCalcFields.containsKey(cls)) {
33+
Map<String, Optional<Transient>> tpMap = new ConcurrentHashMap<>();
34+
cashCalcFields.put(cls, tpMap);
35+
}
36+
37+
cashCalcFields.get(cls).put(field, an);
38+
}
39+
40+
public static Optional<Transient> getTransient(Class cls, String field) {
41+
Optional<Transient> calcField = Arrays.stream(cls.getDeclaredFields())
42+
.filter(f -> f.getName().equals(field) && f.isAnnotationPresent(Transient.class))
43+
.map(f -> f.getAnnotation(Transient.class))
44+
.findFirst();
45+
46+
if (!calcField.isPresent()) {
47+
calcField = getGraphQLCalcMethod(cls, field, "get");
48+
}
49+
50+
if (!calcField.isPresent()) {
51+
calcField = getGraphQLCalcMethod(cls, field, "is");
52+
}
53+
54+
return calcField;
55+
}
56+
57+
public static Optional<Transient> getGraphQLCalcMethod(Class cls, String field, String prefix) {
58+
String methodName = prefix + field.substring(0,1).toUpperCase() + field.substring(1);
59+
60+
return Arrays.stream(cls.getDeclaredMethods())
61+
.filter(m -> m.getName().equals(methodName) && m.isAnnotationPresent(Transient.class))
62+
.map(m -> m.getAnnotation(Transient.class))
63+
.findFirst()
64+
;
65+
}
66+
}

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

+66-5
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,13 @@
2424
import java.lang.reflect.Field;
2525
import java.lang.reflect.Member;
2626
import java.lang.reflect.Method;
27-
import java.util.ArrayList;
28-
import java.util.Collection;
29-
import java.util.HashMap;
30-
import java.util.List;
31-
import java.util.Map;
27+
import java.util.*;
3228
import java.util.concurrent.atomic.AtomicInteger;
3329
import java.util.stream.Collectors;
3430
import java.util.stream.Stream;
3531

3632
import javax.persistence.EntityManager;
33+
import javax.persistence.Transient;
3734
import javax.persistence.metamodel.Attribute;
3835
import javax.persistence.metamodel.EmbeddableType;
3936
import javax.persistence.metamodel.EntityType;
@@ -469,13 +466,77 @@ private GraphQLObjectType getObjectType(EntityType<?> entityType) {
469466
.map(this::getObjectField)
470467
.collect(Collectors.toList())
471468
)
469+
.fields(getObjectCalcFields(entityType.getJavaType()))
470+
.fields(getObjectCalcMethods(entityType.getJavaType()))
472471
.build();
473472

474473
entityCache.putIfAbsent(entityType, objectType);
475474

476475
return objectType;
477476
}
478477

478+
private List<GraphQLFieldDefinition> getObjectCalcFields(Class cls) {
479+
return
480+
Arrays.stream(cls.getDeclaredFields())
481+
.filter(
482+
f ->
483+
f instanceof Member &&
484+
f.isAnnotationPresent(Transient.class) &&
485+
isNotIgnored((Member) f) &&
486+
isNotIgnored(f.getType())
487+
)
488+
.map(f -> getObjectCalcField(f))
489+
.collect(Collectors.toList());
490+
}
491+
492+
private List<GraphQLFieldDefinition> getObjectCalcMethods(Class cls) {
493+
return
494+
Arrays.stream(cls.getDeclaredMethods())
495+
.filter(
496+
m ->
497+
m instanceof Member &&
498+
m.isAnnotationPresent(Transient.class) &&
499+
isNotIgnored((Member) m) &&
500+
isNotIgnored(m.getReturnType())
501+
)
502+
.map(m -> getObjectCalcMethtod(m))
503+
.collect(Collectors.toList());
504+
}
505+
506+
@SuppressWarnings( { "rawtypes", "unchecked" } )
507+
private GraphQLFieldDefinition getObjectCalcField(Field field) {
508+
GraphQLType type = getGraphQLTypeFromJavaType(field.getType());
509+
DataFetcher dataFetcher = PropertyDataFetcher.fetching(field.getName());
510+
511+
return GraphQLFieldDefinition.newFieldDefinition()
512+
.name(field.getName())
513+
.description(getSchemaDescription((AnnotatedElement) field))
514+
.type((GraphQLOutputType) type)
515+
.dataFetcher(dataFetcher)
516+
.build();
517+
}
518+
519+
@SuppressWarnings( { "rawtypes", "unchecked" } )
520+
private GraphQLFieldDefinition getObjectCalcMethtod(Method method) {
521+
String nm = method.getName();
522+
if (nm.startsWith("is")) {
523+
nm = Introspector.decapitalize(nm.substring(2));
524+
}
525+
if (nm.startsWith("get")) {
526+
nm = Introspector.decapitalize(nm.substring(3));
527+
}
528+
529+
GraphQLType type = getGraphQLTypeFromJavaType(method.getReturnType());
530+
DataFetcher dataFetcher = PropertyDataFetcher.fetching(nm);
531+
532+
return GraphQLFieldDefinition.newFieldDefinition()
533+
.name(nm)
534+
.description(getSchemaDescription((AnnotatedElement) method))
535+
.type((GraphQLOutputType) type)
536+
.dataFetcher(dataFetcher)
537+
.build();
538+
}
539+
479540
@SuppressWarnings( { "rawtypes", "unchecked" } )
480541
private GraphQLFieldDefinition getObjectField(Attribute attribute) {
481542
GraphQLOutputType type = getAttributeOutputType(attribute);

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

+3-12
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,7 @@
1919
import static graphql.introspection.Introspection.TypeMetaFieldDef;
2020
import static graphql.introspection.Introspection.TypeNameMetaFieldDef;
2121

22-
import java.util.ArrayList;
23-
import java.util.Arrays;
24-
import java.util.Collection;
25-
import java.util.Collections;
26-
import java.util.EnumSet;
27-
import java.util.LinkedHashMap;
28-
import java.util.List;
29-
import java.util.Map;
30-
import java.util.NoSuchElementException;
31-
import java.util.Optional;
22+
import java.util.*;
3223
import java.util.stream.Collectors;
3324
import java.util.stream.Stream;
3425

@@ -146,7 +137,7 @@ protected final List<Argument> getFieldArguments(Field field, CriteriaQuery<?> q
146137
Field selectedField = (Field) selection;
147138

148139
// "__typename" is part of the graphql introspection spec and has to be ignored by jpa
149-
if(!TYPENAME.equals(selectedField.getName())) {
140+
if(!TYPENAME.equals(selectedField.getName()) && !CashGraphQLCalculatedFields.isCalcField(from.getJavaType(), selectedField.getName())) {
150141

151142
Path<?> fieldPath = from.get(selectedField.getName());
152143

@@ -728,7 +719,7 @@ && isManagedType(entityType.getAttribute(it.getName()))
728719
Subgraph<?> sg = entityGraph.addSubgraph(it.getName());
729720
buildSubgraph(it, sg);
730721
} else {
731-
if(!TYPENAME.equals(it.getName()))
722+
if(!TYPENAME.equals(it.getName()) && !CashGraphQLCalculatedFields.isCalcField(entityType.getJavaType(), it.getName()))
732723
entityGraph.addAttributeNodes(it.getName());
733724
}
734725
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.introproventures.graphql.jpa.query.schema;
2+
3+
import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor;
4+
import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.autoconfigure.SpringBootApplication;
9+
import org.springframework.boot.test.context.SpringBootTest;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.test.context.TestPropertySource;
12+
import org.springframework.test.context.junit4.SpringRunner;
13+
import org.springframework.util.Assert;
14+
15+
import javax.persistence.EntityManager;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
@RunWith(SpringRunner.class)
20+
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE)
21+
@TestPropertySource({"classpath:hibernate.properties"})
22+
public class CalcEntityTests {
23+
@SpringBootApplication
24+
static class Application {
25+
@Bean
26+
public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) {
27+
return new GraphQLJpaExecutor(graphQLSchemaBuilder.build());
28+
}
29+
30+
@Bean
31+
public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) {
32+
33+
return new GraphQLJpaSchemaBuilder(entityManager)
34+
.name("GraphQLCalcFields")
35+
.description("CalcFields JPA test schema");
36+
}
37+
38+
}
39+
40+
@Autowired
41+
private GraphQLExecutor executor;
42+
43+
@Test
44+
public void contextLoads() {
45+
Assert.isAssignable(GraphQLExecutor.class, executor.getClass());
46+
}
47+
48+
@Test
49+
public void getAllRecords() {
50+
//given
51+
String query = "query GraphQLCalcFields { CalcEntities { select {id title fieldMem fieldFun logic customLogic } } }";
52+
53+
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}]}}";
54+
55+
//when
56+
Object result = executor.execute(query).getData();
57+
58+
//then
59+
assertThat(result.toString()).isEqualTo(expected);
60+
}
61+
62+
@Test
63+
public void testIgnoreFields() {
64+
String query = "query GraphQLCalcFields { CalcEntities { select {id title fieldMem fieldFun logic customLogic hideField hideFieldFunction } } }";
65+
66+
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'}]";
67+
68+
//when
69+
Object result = executor.execute(query).getErrors();
70+
71+
//then
72+
assertThat(result.toString()).isEqualTo(expected);
73+
}
74+
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.introproventures.graphql.jpa.query.schema.model.calc;
2+
3+
import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription;
4+
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore;
5+
import lombok.Data;
6+
7+
import javax.persistence.Entity;
8+
import javax.persistence.Id;
9+
import javax.persistence.Transient;
10+
11+
@Data
12+
@Entity
13+
public class CalcEntity {
14+
@Id
15+
Long id;
16+
17+
String title;
18+
19+
String info;
20+
21+
@Transient
22+
boolean logic = true;
23+
24+
@Transient
25+
@GraphQLDescription("i desc member")
26+
String fieldMem = "member";
27+
28+
@Transient
29+
@GraphQLIgnore
30+
String hideField = "hideField";
31+
32+
@Transient
33+
@GraphQLDescription("i desc function")
34+
public String getFieldFun() {
35+
return title + " function";
36+
}
37+
38+
@Transient
39+
public boolean isCustomLogic() {
40+
return false;
41+
}
42+
43+
public String getHideFieldFunction() {
44+
return "getHideFieldFunction";
45+
}
46+
}

graphql-jpa-query-schema/src/test/resources/data.sql

+5-1
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,8 @@ insert into Boat (id, country, identification) values
133133
(1, 'EN', '12345'),
134134
(2, 'EN', '23456'),
135135
(1, 'FR', '34567');
136-
136+
137+
-- Calculate entity
138+
insert into calc_entity (id, title, info) values
139+
(1, 'title 1', 'inf 1'),
140+
(2, 'title 2', 'inf 2');

0 commit comments

Comments
 (0)