diff --git a/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLReadEntityForRole.java b/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLReadEntityForRole.java new file mode 100644 index 000000000..f3e577502 --- /dev/null +++ b/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLReadEntityForRole.java @@ -0,0 +1,13 @@ +package com.introproventures.graphql.jpa.query.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GraphQLReadEntityForRole { + String[] value(); +} diff --git a/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Author.java b/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Author.java index 406c81b6e..e4b91886c 100644 --- a/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Author.java +++ b/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Author.java @@ -31,6 +31,7 @@ import javax.persistence.OneToMany; import javax.persistence.OrderBy; +import com.introproventures.graphql.jpa.query.annotation.GraphQLReadEntityForRole; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -41,6 +42,7 @@ @Setter @ToString @EqualsAndHashCode(exclude={"books","phoneNumbers"}) // Fixes NPE in Hibernate when initializing loaded collections #1 +@GraphQLReadEntityForRole({"admin"}) public class Author { @Id Long id; diff --git a/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java b/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java index ce8045270..2fe40307d 100644 --- a/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java +++ b/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java @@ -29,10 +29,7 @@ import javax.persistence.ManyToOne; import javax.persistence.Transient; -import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; -import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; -import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreFilter; -import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreOrder; +import com.introproventures.graphql.jpa.query.annotation.*; import lombok.Data; import lombok.EqualsAndHashCode; @@ -40,6 +37,7 @@ @Data @Entity @EqualsAndHashCode(exclude= {"author", "tags"}) +@GraphQLReadEntityForRole({"user", "admin"}) public class Book { @Id Long id; diff --git a/graphql-jpa-query-schema-mutations/pom.xml b/graphql-jpa-query-schema-mutations/pom.xml new file mode 100644 index 000000000..2cfdb74f0 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/pom.xml @@ -0,0 +1,76 @@ + + + + com.introproventures + graphql-jpa-query-build + 0.3.36-SNAPSHOT + ../graphql-jpa-query-build + + 4.0.0 + + graphql-jpa-query-schema-mutations + graphql-jpa-query-schema-mutations + + + + com.introproventures + graphql-jpa-query-annotations + + + + com.introproventures + graphql-jpa-query-schema + + + + javax.persistence + javax.persistence-api + + + + javax.transaction + javax.transaction-api + + + + javax.interceptor + javax.interceptor-api + + + + org.slf4j + slf4j-api + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.9.8 + + + + + org.json + json + 20180813 + + + + + org.apache.commons + commons-lang3 + 3.8.1 + + + + org.springframework.boot + spring-boot-starter-data-jpa + test + + + + + \ No newline at end of file diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/GraphQLJpaSchemaBuilderWithMutation.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/GraphQLJpaSchemaBuilderWithMutation.java new file mode 100644 index 000000000..058e45d98 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/GraphQLJpaSchemaBuilderWithMutation.java @@ -0,0 +1,206 @@ +package com.introproventures.graphql.jpa.query.mutations; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import com.introproventures.graphql.jpa.query.mutations.fetcher.impl.*; + +import javax.persistence.EntityManager; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EmbeddableType; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.Type; + +import com.introproventures.graphql.jpa.query.schema.impl.FetcherParams; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + +import graphql.schema.*; + + +public class GraphQLJpaSchemaBuilderWithMutation extends GraphQLJpaSchemaBuilder { + public class GqlMutationOper { + private String operation; + private String description; + private Class fetcher; + + public GqlMutationOper(String oper, String description, Class fetcher) { + this.operation = oper; + this.description = description; + this.fetcher = fetcher; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Class getFetcher() { + return fetcher; + } + + public void setFetcher(Class fetcher) { + this.fetcher = fetcher; + } + } + + protected String suffixInputObjectType = "Input"; + private List operations = new ArrayList<>(); + private Map, GraphQLInputObjectType> entityInputCache = new HashMap<>(); + + public GraphQLJpaSchemaBuilderWithMutation(EntityManager em) { + super(em); + + operations.add(new GqlMutationOper("insert", "This insert entity", InsertFecher.class)); + operations.add(new GqlMutationOper("update", "This update entity", UpdateFetcher.class)); + operations.add(new GqlMutationOper("merge", "This update entity", MergeFetcher.class)); + operations.add(new GqlMutationOper("delete", "This delete entity", DeleteFetcher.class)); + } + + public GraphQLJpaSchemaBuilderWithMutation suffixInputObjectType(String suffixInputObjectType) { + this.suffixInputObjectType = suffixInputObjectType; + return this; + } + + @Override + public GraphQLSchema build() { + createFetcherParams(); + return GraphQLSchema.newSchema() + .query(getQueryType()) + .mutation(getMutation()) + .build(); + } + + protected GraphQLObjectType getMutation() { + GraphQLObjectType.Builder queryType = + GraphQLObjectType.newObject() + .name("editEntities") + .description(this.getDescription()); + + List fields = new ArrayList<>(); + + for (EntityType entityType : entityManager.getMetamodel().getEntities()) { + if (!isNotIgnored(entityType)) continue; + + for (GqlMutationOper oper : operations) { + fields.add(getMutationDefinition(entityType, oper)); + } + } + + queryType.fields(fields); + return queryType.build(); + } + + private GraphQLFieldDefinition getMutationDefinition(EntityType entityType, GqlMutationOper oper) { + return GraphQLFieldDefinition.newFieldDefinition() + .name(oper.getOperation()+entityType.getName()) + .description(oper.getDescription()) + .type(getObjectType(entityType)) + .dataFetcher(getInputFetcher(entityType, oper)) + .argument(getArgumentInputEntity(entityType)) + .build(); + } + + private GraphQLJpaEntityInputFetcher getInputFetcher(EntityType entityType, GqlMutationOper oper) { + Class classFetch = oper.getFetcher(); + try { + return classFetch.getDeclaredConstructor(EntityManager.class, FetcherParams.class, EntityType.class) + .newInstance(entityManager, fetcherParams, entityType); + } catch(Exception e) { + throw new RuntimeException(e); + } + } + + + private GraphQLArgument getArgumentInputEntity(EntityType entityType) { + return GraphQLArgument.newArgument() + .name("entity") + .type(getObjectInputType(entityType)) + .description("this description entity " + entityType.getName()) + .build(); + } + + private GraphQLInputObjectType getObjectInputType(EntityType entityType) { + if (entityInputCache.containsKey(entityType)) + return entityInputCache.get(entityType); + + GraphQLInputObjectType objectInputType = GraphQLInputObjectType.newInputObject() + .name(entityType.getName()+suffixInputObjectType) + .description("Input type GraphQL for entity "+entityType.getName()) + + .fields(entityType.getAttributes().stream() + .filter(this::isNotIgnored) + .map(this::getObjectInputAttribute) + .collect(Collectors.toList()) + ) + .build(); + + entityInputCache.putIfAbsent(entityType, objectInputType); + + return objectInputType; + } + + public GraphQLInputObjectField getObjectInputAttribute(Attribute attribute) { + GraphQLType type = getAttributeInputType(attribute); + + if (!(type instanceof GraphQLInputType)) { + throw new IllegalArgumentException("Attribute " + attribute + " cannot be instanceof GraphQLInputType"); + } + + GraphQLInputObjectField field = GraphQLInputObjectField.newInputObjectField() + .name(attribute.getName()) + .type((GraphQLInputType)type) + .build(); + + return field; + } + + @SuppressWarnings( "rawtypes" ) + protected GraphQLType getAttributeInputType(Attribute attribute) { + + if (isBasic(attribute)) { + return getGraphQLTypeFromJavaType(attribute.getJavaType()); + } + else if (isEmbeddable(attribute)) { + EmbeddableType embeddableType = (EmbeddableType) ((SingularAttribute) attribute).getType(); + return getEmbeddableType(embeddableType, true ); + } + else if (isToMany(attribute)) { + EntityType foreignType = (EntityType) ((PluralAttribute) attribute).getElementType(); + return new GraphQLList(new GraphQLTypeReference(foreignType.getName()+suffixInputObjectType)); + } + else if (isToOne(attribute)) { + EntityType foreignType = (EntityType) ((SingularAttribute) attribute).getType(); + return new GraphQLTypeReference(foreignType.getName()+suffixInputObjectType); + } + else if (isElementCollection(attribute)) { + Type foreignType = ((PluralAttribute) attribute).getElementType(); + + if(foreignType.getPersistenceType() == Type.PersistenceType.BASIC) { + return new GraphQLList(getGraphQLTypeFromJavaType(foreignType.getJavaType())); + } + } + + final String declaringType = attribute.getDeclaringType().getJavaType().getName(); // fully qualified name of the entity class + final String declaringMember = attribute.getJavaMember().getName(); // field name in the entity class + + throw new UnsupportedOperationException( + "Attribute could not be mapped to GraphQL: field '" + declaringMember + "' of entity class '"+ declaringType +"'"); + } +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteEntityForRole.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteEntityForRole.java new file mode 100644 index 000000000..d6ec0318d --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteEntityForRole.java @@ -0,0 +1,17 @@ +package com.introproventures.graphql.jpa.query.mutations.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GraphQLWriteEntityForRole { + String[] value(); + + GraphQLWriteType[] operations() default {GraphQLWriteType.ALL}; +} + + diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteEntityList.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteEntityList.java new file mode 100644 index 000000000..6c4eeae44 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteEntityList.java @@ -0,0 +1,17 @@ +package com.introproventures.graphql.jpa.query.mutations.annotation; + +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityForRole; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GraphQLWriteEntityList { + GraphQLWriteEntityForRole[] value(); +} + + + diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteType.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteType.java new file mode 100644 index 000000000..374acd797 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/annotation/GraphQLWriteType.java @@ -0,0 +1,8 @@ +package com.introproventures.graphql.jpa.query.mutations.annotation; + +public enum GraphQLWriteType { + ALL, + INSERT, + UPDATE, + DELETE +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/IGraphQLJpaEntityInputFetcher.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/IGraphQLJpaEntityInputFetcher.java new file mode 100644 index 000000000..9f2b9c66c --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/IGraphQLJpaEntityInputFetcher.java @@ -0,0 +1,7 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher; + +import graphql.schema.DataFetcher; + +public interface IGraphQLJpaEntityInputFetcher extends DataFetcher { + Object executeMutation(Object entity, MutationContext mutationContext); +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/MutationContext.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/MutationContext.java new file mode 100644 index 000000000..819b5194b --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/MutationContext.java @@ -0,0 +1,26 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MutationContext { + private Map> objFields = new HashMap<>(); + private String operationName; + + public String getOperationName() { + return operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public void addContextFields(Object obj, List fields) { + objFields.put(obj, fields); + } + + public List getObjectFields(Object obj) { + return objFields.get(obj); + } +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/DeleteFetcher.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/DeleteFetcher.java new file mode 100644 index 000000000..9b397ff36 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/DeleteFetcher.java @@ -0,0 +1,33 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher.impl; + +import com.introproventures.graphql.jpa.query.schema.ExceptionGraphQLRuntime; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import com.introproventures.graphql.jpa.query.mutations.fetcher.MutationContext; +import com.introproventures.graphql.jpa.query.schema.impl.FetcherParams; + +import javax.persistence.EntityManager; +import javax.persistence.metamodel.EntityType; + +public class DeleteFetcher extends GraphQLJpaEntityInputFetcher { + public DeleteFetcher(EntityManager entityManager, FetcherParams fetcherParams, EntityType entityType) { + super(entityManager, fetcherParams, entityType); + } + + @Override + public Object executeMutation(Object entity, MutationContext mutationContext) { + + try { + Object newEntity = reloadEntityNotNull(entity); + + checkAccessWriteOperation(newEntity.getClass(), GraphQLWriteType.DELETE); + entityManager.remove(newEntity); + entityManager.flush(); + + return reloadEntity(newEntity); + } catch (ExceptionGraphQLRuntime e) { + throw e; + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + } +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/GraphQLJpaEntityInputFetcher.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/GraphQLJpaEntityInputFetcher.java new file mode 100644 index 000000000..a0fa56265 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/GraphQLJpaEntityInputFetcher.java @@ -0,0 +1,472 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher.impl; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.persistence.*; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.PluralAttribute; + +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityForRole; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityList; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import com.introproventures.graphql.jpa.query.mutations.fetcher.IGraphQLJpaEntityInputFetcher; +import com.introproventures.graphql.jpa.query.mutations.fetcher.MutationContext; +import com.introproventures.graphql.jpa.query.schema.ExceptionGraphQLRuntime; +import com.introproventures.graphql.jpa.query.schema.impl.FetcherParams; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaBaseFetcher; +import graphql.language.EnumValue; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.BeanDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import graphql.schema.DataFetchingEnvironment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class GraphQLJpaEntityInputFetcher extends GraphQLJpaBaseFetcher implements IGraphQLJpaEntityInputFetcher { + + protected EntityType entityType; + + Logger logger = LoggerFactory.getLogger(GraphQLJpaEntityInputFetcher.class); + + public static class DeserializeResult { + private Object entity; + private MutationContext mutationContext = new MutationContext(); + + public Object getEntity() { + return entity; + } + + public void setEntity(Object entity) { + this.entity = entity; + } + + public MutationContext getMutationContext() { + return mutationContext; + } + } + + public GraphQLJpaEntityInputFetcher(EntityManager entityManager, FetcherParams fetcherParams, EntityType entityType) { + super(entityManager, fetcherParams); + + this.entityType = entityType; + } + + protected Logger getLogger() { + return logger; + } + + protected void convertEnumValue(List lst) { + for (int i = 0; i < lst.size(); i++) { + Object ob = lst.get(i); + if (ob instanceof EnumValue) { + lst.add(i, ((EnumValue) ob).getName()); + } + if (ob instanceof Map) { + convertEnumValue((Map)ob); + } + if (ob instanceof List) { + convertEnumValue((List)ob); + } + } + } + + protected void convertEnumValue(Map mp) { + for (String key : mp.keySet()) { + Object ob = mp.get(key); + if (ob instanceof EnumValue) { + mp.put(key, ((EnumValue) ob).getName()); + } + if (ob instanceof Map) { + convertEnumValue((Map)ob); + } + if (ob instanceof List) { + convertEnumValue((List)ob); + } + } + } + + + public DeserializeResult deserialize(DataFetchingEnvironment environment, String argument) { + try { + DeserializeResult res = new DeserializeResult(); + + res.getMutationContext().setOperationName(environment.getFieldDefinition().getName()); + Map mp = environment.getArgument(argument); + convertEnumValue(mp); + + JSONObject json = new JSONObject(mp); + + ObjectMapper oMapper = new ObjectMapper(); + oMapper.registerModule(new JavaTimeModule()); + SimpleModule module = new SimpleModule(); + + module.setDeserializerModifier(new BeanDeserializerModifier() + { + @Override public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer deserializer) + { + //custom deserialize + if (deserializer instanceof BeanDeserializer) + return new GraphQLStdDeserializer((BeanDeserializer)deserializer, res.getMutationContext()); + + return deserializer; + } + }); + + oMapper.registerModule(module); + + Object entity = oMapper.readValue(json.toString(), entityType.getJavaType()); + res.setEntity(entity); + return res; + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + } + + + @Override + public Object get(DataFetchingEnvironment environment) { + DeserializeResult res = deserialize(environment, "entity"); + return executeMutation(res.getEntity(), res.getMutationContext()); + } + + public Object reloadEntity(Object entity) { + if (entity == null) { + return null; + } + Object id = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity); + if (id == null) { + return null; + } + return entityManager.find(entity.getClass(), id); + } + + public Object reloadEntityNotNull(Object entity) { + if (entity == null) { + return null; + } + + Object id = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity); + if (id == null) { + throw new ExceptionGraphQLRuntime("Cann`t reload object "+entity.getClass().getSimpleName()+" - id not set"); + } + + Object reloadEntity = entityManager.find(entity.getClass(), id); + + if (reloadEntity == null) { + throw new ExceptionGraphQLRuntime("Not found object "+entity.getClass().getSimpleName() + " by id = "+id.toString()); + } + + return reloadEntity; + } + + public EntityType getEntityType() { + return entityType; + } + + public EntityManager getEntityManager() { + return entityManager; + } + + public void updateValueAttribute(Attribute attr, Object entity, Object newVal) { + updateValueAttribute(attr.getName(), entity, newVal, attr.getJavaType(), true); + } + + public void updateValueAttribute(String attr, Object entity, Object newVal, Class typeValue) { + updateValueAttribute(attr, entity, newVal, typeValue, true); + } + + public void updateValueAttribute(String attr, Object entity, Object newVal, Class typeValue, boolean useSetter) { + try { + String setter = "set"+StringUtils.capitalize(attr); + Method method = null; + + try { + if (!useSetter) { + throw new NoSuchMethodException(); + } + method = entity.getClass().getMethod(setter, typeValue); + method.invoke(entity, newVal); + } catch (NoSuchMethodException e) { + getLogger().warn("Access to field "+attr+" in class "+entity.getClass().getName()+". check getter"); + Field field = entity.getClass().getDeclaredField(attr); + field.setAccessible(true); + + field.set(entity, newVal); + } + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + } + + public Object getValueAttribute(Attribute attr, Object entity) { + return getValueAttribute(attr.getName(), entity); + } + + public Object getValueAttribute(String attr, Object entity) { + try { + String getter = "get"+StringUtils.capitalize(attr); + try { + Method method = entity.getClass().getMethod(getter); + return method.invoke(entity); + } catch (NoSuchMethodException e) { + getLogger().warn("Access to field "+attr+" in class "+entity.getClass().getName()+" check getter"); + + Field field = entity.getClass().getDeclaredField(attr); + field.setAccessible(true); + return field.get(entity); + } + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + } + + public void copyEntityFields(Object source, Object dist, List fields) { + try { + for (String fieldName : fields) { + Field field = source.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + Object distField = field.get(dist); + + if (distField instanceof Map) { + + ((Map) distField).clear(); + ((Map) distField).putAll((Map)field.get(source)); + continue; + } + + if (distField instanceof Collection) { + ((Collection) distField).clear(); + ((Collection) distField).addAll((Collection)field.get(source)); + continue ; + } + + field.set(dist, field.get(source)); + } + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + } + + public void copyEntityFields(Object source, Object dist, String field) { + copyEntityFields(source, dist, Arrays.asList(field)); + } + + + + public void reloadChildEntities(EntityType et, Object entity, MutationContext mutationContext) throws Exception { + if (entity == null) return ; + + for (Attribute attr : et.getAttributes()) { + //getLogger().info("Attr = "+attr.getName()); + Attribute.PersistentAttributeType persistType = attr.getPersistentAttributeType(); + //OneToMany an = attr.getJavaType().getAnnotation(OneToMany.class); + + + if (persistType == Attribute.PersistentAttributeType.ONE_TO_ONE || + persistType == Attribute.PersistentAttributeType.MANY_TO_ONE ) { + Object attrVal = getValueAttribute(attr, entity); + updateValueAttribute(attr, entity, reloadEntityNotNull(attrVal)); + continue; + } + + if (persistType == Attribute.PersistentAttributeType.ONE_TO_MANY ) { + Object attrVal = getValueAttribute(attr, entity); + if (attrVal == null) continue; + + int size = 0; + + if (attrVal instanceof Map) { + size = ((Map) attrVal).size(); + } + + if (attrVal instanceof Collection) { + size = ((Collection) attrVal).size(); + } + + if (size > 0) { + throw new ExceptionGraphQLRuntime("For field " + attr.getName() + " cannot set objects for insert. (ONE_TO_MANY). Use operation merge or update child entity"); + } + + continue; + } + + if (persistType == Attribute.PersistentAttributeType.MANY_TO_MANY) { + EntityType attrType = (EntityType) ((PluralAttribute) attr).getElementType(); + + String mappedBychild = null; + ManyToMany mtm = ((AnnotatedElement) attr.getJavaMember()).getAnnotation(ManyToMany.class); + if (mtm != null) { + mappedBychild = mtm.mappedBy(); + } + + Object attrVal = getValueAttribute(attr, entity); + + Function fun = new Function() { + private EntityType attrType; + private Object parent; + private String mappedByChildFinal; + + private Function init(EntityType attrType, Object parent, String mappedByChildFinal) { + this.attrType = attrType; + this.parent = parent; + this.mappedByChildFinal = mappedByChildFinal; + return this; + } + + @Override + public Object apply(Object o) { + return reloadEntityMTMAndSetMappedParent(attrType, o, entity, mappedByChildFinal); + } + }.init(attrType, entity, mappedBychild); + + reloadCollection(attrVal, fun); + + //???? + } + } + + } + + public Object reloadEntityMTMAndSetMappedParent(EntityType et, Object entity, Object parent, String mappedBy) { + Object reloadEntity = reloadEntityNotNull(entity); + + if (mappedBy != null && mappedBy.length() > 0) { + Object col = getValueAttribute(mappedBy, reloadEntity); + Attribute attr = et.getAttribute(mappedBy); + MapKey mk = ((AnnotatedElement) attr.getJavaMember()).getAnnotation(MapKey.class); + Object key = null; + if (mk != null) { + key = getValueAttribute(mk.name(), parent); + } + + addManyToManyParent(col, parent, key); + } + + return reloadEntity; + } + + public void addManyToManyParent(Object col, Object parent, Object key) { + if (col instanceof Map) { + ((Map) col).put(key, parent); + return; + } + + if (col instanceof Set) { + ((Set) col).add(parent); + return ; + } + + if (col instanceof List) { + ((List) col).add(parent); + return; + } + + if (col instanceof Collection) { + ((Collection) col).add(parent); + return; + } + } + + public void reloadCollection(Object collection, Function function) { + if (collection instanceof Map) { + ((Map) collection).replaceAll((k, v) -> function.apply(v)); + return ; + } + + if (collection instanceof List) { + ((List) collection).replaceAll(v -> function.apply(v)); + return; + } + + if (collection instanceof Collection) { + ArrayList array = ((Collection) collection).stream() + .map(v -> function.apply(v)) + .collect(Collectors.toCollection(ArrayList::new)); + + ((Collection) collection).clear(); + ((Collection) collection).addAll(array); + } + } + + public static String getAttrMappedBy(Attribute attr) { + String mappedBy = null; + OneToMany otm = ((AnnotatedElement) attr.getJavaMember()).getAnnotation(OneToMany.class); + if (otm != null) { + mappedBy = otm.mappedBy(); + } + + ManyToMany mtm = ((AnnotatedElement) attr.getJavaMember()).getAnnotation(ManyToMany.class); + if (mtm != null) { + mappedBy = mtm.mappedBy(); + } + + OneToOne oto = ((AnnotatedElement) attr.getJavaMember()).getAnnotation(OneToOne.class); + if (oto != null) { + mappedBy = oto.mappedBy(); + } + + return mappedBy; + } + + public static int compare(Attribute a1, Attribute a2) { + String m1 = getAttrMappedBy(a1); + String m2 = getAttrMappedBy(a2); + + if (m1 == null && m2 != null) { + return -1; + } + + if (m1 != null && m2 == null) { + return 1; + } + + return 0; + } + + + protected boolean checkOperation(GraphQLWriteType operation, GraphQLWriteType[] operList) { + return Arrays.stream(operList).anyMatch(v -> v.equals(operation) || v.equals(GraphQLWriteType.ALL)); + } + + public void checkAccessWriteOperation(Class cls, GraphQLWriteType operation) { + if (fetcherParams.getPredicateRole() == null) { + return ; + } + + AnnotatedElement annotatedElement = (AnnotatedElement)cls; + GraphQLWriteEntityForRole writeRoles = annotatedElement.getAnnotation(GraphQLWriteEntityForRole.class); + if (writeRoles != null) { + if (checkOperation(operation, writeRoles.operations())) { + if (fetcherParams.getPredicateRole().test(writeRoles.value())) + return; + } + } + + GraphQLWriteEntityList writeList = annotatedElement.getAnnotation(GraphQLWriteEntityList.class); + if (writeList != null) { + for (GraphQLWriteEntityForRole wr : writeList.value()) { + if (checkOperation(operation, wr.operations())) { + if (fetcherParams.getPredicateRole().test(wr.value())) + return; + } + } + } + + throw new ExceptionGraphQLRuntime("For entity "+cls.getSimpleName() + " cannot "+operation.name()); + } +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/GraphQLStdDeserializer.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/GraphQLStdDeserializer.java new file mode 100644 index 000000000..c904059a4 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/GraphQLStdDeserializer.java @@ -0,0 +1,50 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher.impl; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; + + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.BeanDeserializer; +import com.introproventures.graphql.jpa.query.mutations.fetcher.MutationContext; + + +public class GraphQLStdDeserializer extends BeanDeserializer implements java.io.Serializable { + private final BeanDeserializer defaultDeserializer; + private MutationContext mutationContext; + + public GraphQLStdDeserializer(BeanDeserializer defaultDeserializer, MutationContext mutationContext) + { + super(defaultDeserializer); + this.defaultDeserializer = defaultDeserializer; + this.mutationContext = mutationContext; + } + + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + JsonLocation startLocation = p.getCurrentLocation(); + long charOffsetStart = startLocation.getCharOffset(); + + Object obj = defaultDeserializer.deserialize(p, ctxt); + + JsonLocation endLocation = p.getCurrentLocation(); + long charOffsetEnd = endLocation.getCharOffset(); + String jsonSubString = endLocation.getSourceRef().toString().substring((int)charOffsetStart - 1, (int)charOffsetEnd); + + try { + Map jsonObj = + new ObjectMapper().readValue(jsonSubString, HashMap.class); + + mutationContext.addContextFields(obj, jsonObj.keySet().stream().collect(Collectors.toList())); + + return obj; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/InsertFecher.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/InsertFecher.java new file mode 100644 index 000000000..963176666 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/InsertFecher.java @@ -0,0 +1,38 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher.impl; + +import com.introproventures.graphql.jpa.query.schema.ExceptionGraphQLRuntime; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import com.introproventures.graphql.jpa.query.mutations.fetcher.MutationContext; +import com.introproventures.graphql.jpa.query.schema.impl.FetcherParams; + +import javax.persistence.EntityManager; +import javax.persistence.metamodel.EntityType; + +public class InsertFecher extends GraphQLJpaEntityInputFetcher { + public InsertFecher(EntityManager entityManager, FetcherParams fetcherParams, EntityType entityType) { + super(entityManager, fetcherParams, entityType); + } + + @Override + public Object executeMutation(Object entity, MutationContext mutationContext) { + try { + reloadChildEntities(entityType, entity, mutationContext); + + checkAccessWriteOperation(entity.getClass(), GraphQLWriteType.INSERT); + entityManager.persist(entity); + + entityManager.flush(); + + entityManager.refresh(entity); + + return entity; + } catch (ExceptionGraphQLRuntime e) { + throw e; + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + + } + + +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/MergeFetcher.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/MergeFetcher.java new file mode 100644 index 000000000..de7d307ef --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/MergeFetcher.java @@ -0,0 +1,194 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher.impl; + +import com.introproventures.graphql.jpa.query.schema.ExceptionGraphQLRuntime; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import com.introproventures.graphql.jpa.query.mutations.fetcher.MutationContext; +import com.introproventures.graphql.jpa.query.schema.impl.FetcherParams; + +import javax.persistence.EntityManager; +import javax.persistence.MapKey; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; +import java.lang.reflect.AnnotatedElement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class MergeFetcher extends GraphQLJpaEntityInputFetcher { + public MergeFetcher(EntityManager entityManager, FetcherParams fetcherParams, EntityType entityType) { + super(entityManager, fetcherParams, entityType); + } + + @Override + public Object executeMutation(Object entity, MutationContext mutationContext) { + + try { + Object resEntity = mergeChildEntities(entityType, entity, null, mutationContext); + + //Object resEntity = entityManager.merge(entity); + + + entityManager.flush(); + + entityManager.refresh(resEntity); + + return entity; + } catch (ExceptionGraphQLRuntime e) { + throw e; + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + } + + public Object mergeChildEntities(EntityType et, Object entity, Object parent, MutationContext mutationContext) throws Exception { + return mergeChildEntities(et, entity, parent, mutationContext,null); + } + + + public Object mergeChildEntities(EntityType et, Object entity, Object parent, MutationContext mutationContext, String mappedBy) throws Exception { + if (entity == null) return null; + + List editFields = mutationContext.getObjectFields(entity); + if (editFields == null) return entity; + + for (Attribute attr : et.getAttributes()) { + if (attr.getName().equals(mappedBy)) { + Attribute.PersistentAttributeType persistType = attr.getPersistentAttributeType(); + + if (persistType == Attribute.PersistentAttributeType.ONE_TO_ONE || + persistType == Attribute.PersistentAttributeType.MANY_TO_ONE) { + updateValueAttribute(attr, entity, parent); + continue; + } + } + } + + Object currentEntity = reloadEntity(entity); + + Boolean isPersist = false; + if (currentEntity != null) { + copyEntityFields(entity, currentEntity, editFields); + entity = currentEntity; + checkAccessWriteOperation(entity.getClass(), GraphQLWriteType.UPDATE); + } else { + isPersist = true; + checkAccessWriteOperation(entity.getClass(), GraphQLWriteType.INSERT); + } + + + Set sortAttr = et.getAttributes().stream() + .sorted((a1, a2) -> GraphQLJpaEntityInputFetcher.compare(a1, a2) ) + .collect(Collectors.toSet()); + + Map mappedObjects = new HashMap<>(); + + for (Attribute attr : sortAttr) { + if (editFields.indexOf(attr.getName()) >= 0 ) { + String mappedBychild = getAttrMappedBy(attr); + if (mappedBychild != null) { + mappedObjects.put(attr.getName(), getValueAttribute(attr.getName(), entity)); + updateValueAttribute(attr.getName(), entity, null, attr.getJavaType(), false); + } + } + } + + for (Attribute attr : et.getAttributes()) { + + //getLogger().info("Attr= " + attr.getName()); + if (editFields.indexOf(attr.getName()) >= 0 || attr.getName().equals(mappedBy)) { + + String mappedBychild = getAttrMappedBy(attr); + + if (isPersist && mappedBychild!=null) { + entityManager.persist(entity); + isPersist = false; + } + + Attribute.PersistentAttributeType persistType = attr.getPersistentAttributeType(); + + //OneToMany an = attr.getJavaType().getAnnotation(OneToMany.class); + + if (persistType == Attribute.PersistentAttributeType.ONE_TO_ONE || + persistType == Attribute.PersistentAttributeType.MANY_TO_ONE) { + EntityType attrType = (EntityType) ((SingularAttribute) attr).getType(); + + + if (mappedBy != null && attr.getName().equals(mappedBy)) { + updateValueAttribute(attr, entity, parent); + continue; + } + + Object childEntity = mergeChildEntities(attrType, getValueAttribute(attr, entity), entity, mutationContext, null); + updateValueAttribute(attr, entity, childEntity); + + continue; + } + + if (persistType == Attribute.PersistentAttributeType.ONE_TO_MANY || + persistType == Attribute.PersistentAttributeType.MANY_TO_MANY) { + + EntityType attrType = (EntityType) ((PluralAttribute) attr).getElementType(); + + Object attrVal = null; + if (mappedObjects.containsKey(attr.getName())) { + attrVal = mappedObjects.get(attr.getName()); + } else { + attrVal = getValueAttribute(attr, entity); + } + + if (mappedBy != null && attr.getName().equals(mappedBy)) { + Object key = null; + MapKey mk = ((AnnotatedElement) attr.getJavaMember()).getAnnotation(MapKey.class); + if (mk != null) { + key = getValueAttribute(mk.name(), parent); + } + addManyToManyParent(attrVal, parent, key); + continue; + } + Function fun = new Function() { + private EntityType attrType; + private Object parent; + private String mappedByChildFinal; + + private Function init(EntityType attrType, Object parent, String mappedByChildFinal) { + this.attrType = attrType; + this.parent = parent; + this.mappedByChildFinal = mappedByChildFinal; + return this; + } + + @Override + public Object apply(Object obj) { + try { + return mergeChildEntities(attrType, obj, parent, mutationContext, mappedByChildFinal); + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + } + }.init(attrType, entity, mappedBychild); + + reloadCollection(attrVal, fun); + } + } + } + + if (isPersist) { + entityManager.persist(entity); + } + + for (Attribute attr : et.getAttributes()) { + if (mappedObjects.containsKey(attr.getName())) { + updateValueAttribute(attr.getName(), entity, mappedObjects.get(attr.getName()), attr.getJavaType(), false); + } + } + + //Object res = entityManager.merge(entity); + //entityManager.flush(); + return entity; + } +} diff --git a/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/UpdateFetcher.java b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/UpdateFetcher.java new file mode 100644 index 000000000..157b66645 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/main/java/com/introproventures/graphql/jpa/query/mutations/fetcher/impl/UpdateFetcher.java @@ -0,0 +1,43 @@ +package com.introproventures.graphql.jpa.query.mutations.fetcher.impl; + +import com.introproventures.graphql.jpa.query.schema.ExceptionGraphQLRuntime; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import com.introproventures.graphql.jpa.query.mutations.fetcher.MutationContext; +import com.introproventures.graphql.jpa.query.schema.impl.FetcherParams; + +import javax.persistence.EntityManager; +import javax.persistence.metamodel.EntityType; +import java.util.List; + +public class UpdateFetcher extends GraphQLJpaEntityInputFetcher { + public UpdateFetcher(EntityManager entityManager, FetcherParams fetcherParams, EntityType entityType) { + super(entityManager, fetcherParams, entityType); + } + + @Override + public Object executeMutation(Object entity, MutationContext mutationContext) { + List updateFields = mutationContext.getObjectFields(entity); + + try { + reloadChildEntities(entityType, entity, mutationContext); + + Object currentEntity = reloadEntity(entity); + + checkAccessWriteOperation(currentEntity.getClass(), GraphQLWriteType.INSERT); + copyEntityFields(entity, currentEntity, updateFields); + + entityManager.flush(); + + entityManager.refresh(currentEntity); + + return currentEntity; + } catch (ExceptionGraphQLRuntime e) { + throw e; + } catch (Exception e) { + throw new ExceptionGraphQLRuntime(e); + } + + } + + +} diff --git a/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/MutationTests.java b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/MutationTests.java new file mode 100644 index 000000000..96a5b17f9 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/MutationTests.java @@ -0,0 +1,310 @@ +package com.introproventures.graphql.jpa.query.mutations; + +import com.introproventures.graphql.jpa.query.mutations.model.book.*; +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.schema.GraphQLSchemaBuilder; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import graphql.ExecutionResult; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.sql.DataSource; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE) +@TestPropertySource({"classpath:hibernate.properties"}) +public class MutationTests { + + static class TransactionQLExecutor { + @Autowired + private GraphQLExecutor executor; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public ExecutionResult execute(String query) { + ExecutionResult res = executor.execute(query); + return res; + } + + public GraphQLExecutor getExecutor() { + return executor; + } + } + + @SpringBootApplication + static class Application { + private static String currentRole = "user"; + + public static String getCurrentRole() { + return currentRole; + } + + public static void setCurrentRole(String currentRole) { + Application.currentRole = currentRole; + } + + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + Predicate predicateAccess = roles -> { + for(int i = 0; i < roles.length; i++) { + if (roles[i].equals(this.currentRole)) { + return true; + } + } + return false; + }; + + return new GraphQLJpaSchemaBuilderWithMutation(entityManager) + .predicateRole(predicateAccess) + .name("GraphQLBooks") + .description("Books JPA test schema"); + } + + @Bean + public TransactionQLExecutor transactionQLExecutor() { + return new TransactionQLExecutor(); + } + + } + + @Autowired + TransactionQLExecutor transactionQLExecutor; + + @PersistenceContext + EntityManager entityManager; + + @Autowired + DataSource dataSource; + + Logger log = LoggerFactory.getLogger(MutationTests.class); + + public void checkCorrectExecutionResult(ExecutionResult result) { + if (result.getErrors().size() > 0) { + String errorMsg = result.getErrors().stream().map(Object::toString) + .collect(Collectors.joining(", ")); + throw new RuntimeException("Error graphQL result: "+errorMsg); + } + } + + @Test + public void contextLoads() { + Application.setCurrentRole("user"); + + org.springframework.util.Assert.isAssignable(GraphQLExecutor.class, transactionQLExecutor.getExecutor().getClass()); + } + + @Test + public void queryInsertStudentOkWriteAccess() { + Application.setCurrentRole("user"); + + String query = "mutation { insertStudent(entity: {id: 46, name: \"nm\"}) {id, name} }"; + + String expected = "{insertStudent={id=46, name=nm}}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("InsertStudent Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + } + + @Test + public void queryInsertStudentErrorWriteAccess() { + Application.setCurrentRole("test"); + + String query = "mutation { insertStudent(entity: {id: 46, name: \"nm\"}) {id, name} }"; + + ExecutionResult result = transactionQLExecutor.execute(query); + + assertThat(result.getErrors()) + .isNotEmpty() + .extracting("message") + .containsOnly( + "Exception while fetching data (/insertStudent) : For entity Student cannot INSERT" + ); + } + + @Test + @Transactional + public void queryInsertBook() { + Application.setCurrentRole("user"); + + Author author = entityManager.find(Author.class, 1L); + + String query = "mutation { insertBook(" + + "entity: {id: 5, title: \"Shot\", author: {id: 1}, houses: [{id: 1}, {id: 2}] }) " + + "{id, title, genre, author{id, name, genre}, houses {id, name}} }"; + + String expected = "{insertBook={id=5, title=Shot, genre=null, author={id=1, name=Pushkin, genre=NOVEL}, houses=[{id=2, name=house 2}, {id=1, name=house 1}]}}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("InsertBook Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + + Book book = entityManager.find(Book.class, 5L); + assertThat(book.getTitle()).isEqualTo("Shot"); + assertThat(book.getHouses().size()).isEqualTo(2); + } + + @Test + public void queryUpdateBook() { + Application.setCurrentRole("user"); + + String query = "mutation { updateBook(entity: {id: 2, title: \"Shot 2\", author: {id: 2}}) {id, title, genre, author{id, name, genre}} }"; + + String expected = "{updateBook={id=2, title=Shot 2, genre=PLAY, author={id=2, name=Lermontov, genre=PLAY}}}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("UpdateBook Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + } + + @Test + public void queryDeleteBook() { + Application.setCurrentRole("user"); + + String query = "mutation { deleteBook(entity: {id: 3}) {id, title} }"; + + String expected = "{deleteBook=null}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("DeleteBook Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + } + + @Test + @Transactional + public void queryMergeBook() { + Application.setCurrentRole("user"); + + String query = "mutation { mergeBook(entity: {id: 40, title: \"new book\", houses: [{id: 100, name:\"new house\"}, {id: 2}]}) " + + "{id, title, genre, author{id, name, genre}, houses {id, name}} }"; + + String expected = "{mergeBook={id=40, title=new book, genre=null, author=null, houses=[{id=2, name=house 2}, {id=100, name=new house}]}}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("MergeBook Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + + + Book book = entityManager.find(Book.class, 40L); + assertThat(book.getHouses().size()).isEqualTo(2); + Assert.assertTrue(book.getHouses().stream().anyMatch(e -> e.getId().equals(100L))); + Assert.assertTrue(book.getHouses().stream().anyMatch(e -> e.getId().equals(2L))); + + //delete array element + query = "mutation { mergeBook(entity: {id: 40, title: \"new book\", houses: [{id: 100, name: \"new house\"}]}) " + + "{id, title, genre, author{id, name, genre}, houses {id, name}} }"; + + expected = "{mergeBook={id=40, title=new book, genre=null, author=null, houses=[{id=100, name=new house}]}}"; + + result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + strRes = result.getData().toString(); + log.info("MergeBook Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + + book = entityManager.find(Book.class, 40L); + assertThat(book.getHouses().size()).isEqualTo(2); + Assert.assertTrue(book.getHouses().stream().anyMatch(e -> e.getId().equals(100L))); + } + + @Test + @Transactional + public void queryMergeHouse() { + Application.setCurrentRole("user"); + + String query = "mutation { mergePublishingHouse(entity: {id: 400, name: \"new house\", books: [{id: 201, title:\"new book 201\"}, {id: 2}]}) " + + "{id, name, books{id, title, genre}} }"; + + String expected = "{mergePublishingHouse={id=400, name=new house, books=[{id=201, title=new book 201, genre=null}, {id=2, title=book2, genre=PLAY}]}}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("MergeHouse Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + + PublishingHouse ph = entityManager.find(PublishingHouse.class, 400L); + assertThat(ph.getBooks().size()).isEqualTo(2); + ph.getBooks().stream().forEach(element -> System.out.println(element.getId())); + Assert.assertTrue(ph.getBooks().stream().anyMatch(e -> e.getId().equals(201L))); + Assert.assertTrue(ph.getBooks().stream().anyMatch(e -> e.getId().equals(2L))); + } + + @Test + @Transactional + public void queryInsertAuthor() { + Application.setCurrentRole("user"); + + String query = "mutation { insertAuthor(entity: {id: 40, name: \"qwe\", phoneNumbers: [\"123456\", \"987654\"]}) " + + "{id, name, genre, phoneNumbers} }"; + + String expected = "{insertAuthor={id=40, name=qwe, genre=null, phoneNumbers=[123456, 987654]}}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("InsertAuthor Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + + Author author = entityManager.find(Author.class, 40L); + assertThat(author.getPhoneNumbers().size()).isEqualTo(2); + } + + @Test + public void queryMergeAuthor() { + Application.setCurrentRole("user"); + + String query = "mutation { mergeAuthor(entity: {id: 40, name: \"qwe\", books: [{id: 100, title:\"new book\"}, {id: 2}]}) " + + "{id, name, genre, books {id, title}} }"; + + String expected = "{mergeAuthor={id=40, name=qwe, genre=null, books=[{id=100, title=new book}, {id=2, title=null}]}}"; + + ExecutionResult result = transactionQLExecutor.execute(query); + checkCorrectExecutionResult(result); + + String strRes = result.getData().toString(); + log.info("MergeAuthor Result: "+strRes); + assertThat(strRes).isEqualTo(expected); + } +} diff --git a/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/Student.java b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/Student.java new file mode 100644 index 000000000..a9f21b738 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/Student.java @@ -0,0 +1,38 @@ +package com.introproventures.graphql.jpa.query.mutations.model; + +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityForRole; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityList; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +@GraphQLWriteEntityList( + @GraphQLWriteEntityForRole(value = {"user"}, operations = {GraphQLWriteType.ALL}) +) +public class Student { + + @Id + private long id; + + @Column(name = "name", nullable = true, length = 2000) + private String name; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Alias.java b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Alias.java new file mode 100644 index 000000000..b23c7ba7c --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Alias.java @@ -0,0 +1,25 @@ +package com.introproventures.graphql.jpa.query.mutations.model.book; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@ToString +public class Alias { + @Id + Long id; + + @Column(name = "name", nullable = true, length = 256) + String name; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "a_id") + Author author; + + +} diff --git a/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Author.java b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Author.java new file mode 100644 index 000000000..c9356828d --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Author.java @@ -0,0 +1,85 @@ +package com.introproventures.graphql.jpa.query.mutations.model.book; + +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityForRole; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.*; +import java.util.*; + +@Entity +@ToString +@EqualsAndHashCode(exclude={"books","phoneNumbers"}) // Fixes NPE in Hibernate when initializing loaded collections #1 +@GraphQLWriteEntityForRole(value = {"user"}, operations = {GraphQLWriteType.ALL}) +public class Author { + @Id + Long id; + + String name; + + @OneToMany(mappedBy="author", fetch=FetchType.LAZY, /*cascade = CascadeType.ALL,*/ orphanRemoval=true) + List books = new ArrayList<>(); + + @ElementCollection(fetch=FetchType.LAZY) + @CollectionTable(name = "author_phone_numbers", joinColumns = @JoinColumn(name = "author_id")) + @Column(name = "phone_number") + private Set phoneNumbers = new HashSet<>(); + + @Enumerated(EnumType.STRING) + Genre genre; + + @OneToOne(mappedBy = "author" /*, cascade = CascadeType.ALL*/, fetch = FetchType.LAZY, optional = true) + private Alias alias; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getBooks() { + return books; + } + + public void setBooks(List books) { + this.books.clear(); + this.books.addAll(books); + } + + public Set getPhoneNumbers() { + return phoneNumbers; + } + + public void setPhoneNumbers(Set phoneNumbers) { + this.phoneNumbers = phoneNumbers; + } + + public Genre getGenre() { + return genre; + } + + public void setGenre(Genre genre) { + this.genre = genre; + } + + public Alias getAlias() { + return alias; + } + + public void setAlias(Alias alias) { + this.alias = alias; + } +} diff --git a/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Book.java b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Book.java new file mode 100644 index 000000000..1add039bc --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Book.java @@ -0,0 +1,70 @@ +package com.introproventures.graphql.jpa.query.mutations.model.book; + +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityForRole; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import lombok.Data; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Entity +@GraphQLWriteEntityForRole(value = {"user"}, operations = {GraphQLWriteType.ALL}) +public class Book { + @Id + Long id; + + String title; + + @ManyToOne(fetch=FetchType.LAZY) + Author author; + + @Enumerated(EnumType.STRING) + Genre genre; + + @ManyToMany(mappedBy = "books", fetch = FetchType.EAGER/*, cascade = CascadeType.ALL*/) + private Set houses = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Author getAuthor() { + return author; + } + + public void setAuthor(Author author) { + this.author = author; + } + + public Genre getGenre() { + return genre; + } + + public void setGenre(Genre genre) { + this.genre = genre; + } + + public Set getHouses() { + return houses; + } + + public void setHouses(Set houses) { + this.houses.clear(); + this.houses.addAll(houses); + } +} diff --git a/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Genre.java b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Genre.java new file mode 100644 index 000000000..c64695982 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/Genre.java @@ -0,0 +1,5 @@ +package com.introproventures.graphql.jpa.query.mutations.model.book; + +public enum Genre { + NOVEL, PLAY +} diff --git a/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/PublishingHouse.java b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/PublishingHouse.java new file mode 100644 index 000000000..d5c42bb93 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/java/com/introproventures/graphql/jpa/query/mutations/model/book/PublishingHouse.java @@ -0,0 +1,60 @@ +package com.introproventures.graphql.jpa.query.mutations.model.book; + +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteEntityForRole; +import com.introproventures.graphql.jpa.query.mutations.annotation.GraphQLWriteType; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +/* +@Getter +@Setter +*/ +@ToString +@EqualsAndHashCode(exclude={"books"}) // Fixes NPE in Hibernate when initializing loaded collections #1 +@GraphQLWriteEntityForRole(value = {"user"}, operations = {GraphQLWriteType.ALL}) +public class PublishingHouse { + @Id + Long id; + + @Column(name = "name", nullable = true, length = 256) + String name; + + @ManyToMany(fetch = FetchType.LAZY /*, cascade = CascadeType.ALL*/) + @JoinTable(name = "publish_book", + joinColumns = @JoinColumn(name = "ph_id"), + inverseJoinColumns = @JoinColumn(name = "b_id") + ) + private List books = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getBooks() { + return books; + } + + public void setBooks(List books) { + this.books.clear(); + this.books.addAll(books); + } +} diff --git a/graphql-jpa-query-schema-mutations/src/test/resources/data.sql b/graphql-jpa-query-schema-mutations/src/test/resources/data.sql new file mode 100644 index 000000000..d03d7a979 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/resources/data.sql @@ -0,0 +1,24 @@ +-- Insert Code Lists +insert into student (id, name) values + (1, 'org.crygier.graphql.model.starwars.Gender'), + (2, 'org.crygier.graphql.model.starwars.Gender'); + + + +insert into author (id, name, genre) values + (1, 'Pushkin', 'NOVEL'), + (2, 'Lermontov', 'PLAY'), + (3, 'Tolstoy', 'PLAY'); + + +insert into book (id, title, author_id, genre) values + (1, 'book1', 1, 'NOVEL'), + (2, 'book2', 1, 'PLAY'), + (3, 'book3', 1, 'PLAY'), + (4, 'book4', 3, 'PLAY'); + + +insert into publishing_house (id, name) values + (1, 'house 1'), + (2, 'house 2'); + diff --git a/graphql-jpa-query-schema-mutations/src/test/resources/hibernate.properties b/graphql-jpa-query-schema-mutations/src/test/resources/hibernate.properties new file mode 100644 index 000000000..5fb4fad96 --- /dev/null +++ b/graphql-jpa-query-schema-mutations/src/test/resources/hibernate.properties @@ -0,0 +1,12 @@ +#spring.datasource.url = jdbc:h2:file:./testdb +hibernate.generate_statistics=true +org.hibernate.stat=DEBUG +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.properties.hibernate.format_sql=true + +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE + +#logging.level.org.hibernate=debug +#logging.level.org.hibernate.type.descriptor.sql=trace +#logging.level.org.hibernate.SQL=debug diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/ExceptionGraphQLRuntime.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/ExceptionGraphQLRuntime.java new file mode 100644 index 000000000..393a64d2b --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/ExceptionGraphQLRuntime.java @@ -0,0 +1,17 @@ +package com.introproventures.graphql.jpa.query.schema; + +public class ExceptionGraphQLRuntime extends RuntimeException { + + public ExceptionGraphQLRuntime(String msg) { + super(msg); + } + + public ExceptionGraphQLRuntime(String message, Throwable cause) { + super(message, cause); + } + + public ExceptionGraphQLRuntime(Throwable cause) { + super(cause); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/FetcherParams.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/FetcherParams.java new file mode 100644 index 000000000..a1c98e899 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/FetcherParams.java @@ -0,0 +1,21 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import java.util.function.Predicate; + +public class FetcherParams { + private MapEntityType mapEntityType; + private Predicate predicateRole; + + public FetcherParams(MapEntityType mapEntityType, Predicate predicateRole) { + this.mapEntityType = mapEntityType; + this.predicateRole = predicateRole; + } + + public MapEntityType getMapEntityType() { + return mapEntityType; + } + + public Predicate getPredicateRole() { + return predicateRole; + } +} 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/GraphQLJpaBaseDataFetcher.java similarity index 98% rename from graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java rename to graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaBaseDataFetcher.java index dd42d963c..319eba9c1 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/GraphQLJpaBaseDataFetcher.java @@ -44,17 +44,7 @@ import javax.persistence.EntityManager; import javax.persistence.Subgraph; import javax.persistence.TypedQuery; -import javax.persistence.criteria.AbstractQuery; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Fetch; -import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; -import javax.persistence.criteria.JoinType; -import javax.persistence.criteria.Path; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import javax.persistence.criteria.Subquery; +import javax.persistence.criteria.*; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.Attribute.PersistentAttributeType; import javax.persistence.metamodel.EntityType; @@ -84,7 +74,6 @@ import graphql.language.StringValue; import graphql.language.Value; import graphql.language.VariableReference; -import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentBuilder; import graphql.schema.GraphQLArgument; @@ -96,13 +85,14 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; + /** * Provides base implemetation for GraphQL JPA Query Data Fetchers * * @author Igor Dianov * */ -class QraphQLJpaBaseDataFetcher implements DataFetcher { +class GraphQLJpaBaseDataFetcher extends GraphQLJpaBaseFetcher { private static final String WHERE = "where"; @@ -111,7 +101,6 @@ class QraphQLJpaBaseDataFetcher implements DataFetcher { // "__typename" is part of the graphql introspection spec and has to be ignored private static final String TYPENAME = "__typename"; - protected final EntityManager entityManager; protected final EntityType entityType; private boolean toManyDefaultOptional = true; @@ -120,18 +109,21 @@ class QraphQLJpaBaseDataFetcher implements DataFetcher { * Creates JPA entity DataFetcher instance * * @param entityManager + * @param predicateRole * @param entityType */ - public QraphQLJpaBaseDataFetcher(EntityManager entityManager, - EntityType entityType, + public GraphQLJpaBaseDataFetcher(EntityManager entityManager, + FetcherParams fetcherParams, + EntityType entityType, boolean toManyDefaultOptional) { - this.entityManager = entityManager; + super(entityManager, fetcherParams); this.entityType = entityType; this.toManyDefaultOptional = toManyDefaultOptional; } @Override public Object get(DataFetchingEnvironment environment) { + checkAccessDataFetching(environment); return getQuery(environment, environment.getFields().iterator().next(), true).getResultList(); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaBaseFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaBaseFetcher.java new file mode 100644 index 000000000..a79ab083f --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaBaseFetcher.java @@ -0,0 +1,93 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import com.introproventures.graphql.jpa.query.annotation.GraphQLReadEntityForRole; +import graphql.execution.AbortExecutionException; +import graphql.language.Field; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import graphql.schema.*; + +import javax.persistence.EntityManager; +import java.lang.reflect.AnnotatedElement; + +public abstract class GraphQLJpaBaseFetcher implements DataFetcher { + protected final EntityManager entityManager; + protected final FetcherParams fetcherParams; + + public GraphQLJpaBaseFetcher(EntityManager entityManager, FetcherParams fetcherParams) { + this.entityManager = entityManager; + this.fetcherParams = fetcherParams; + } + + public EntityManager getEntityManager() { + return entityManager; + } + + public FetcherParams getFetcherParams() { + return fetcherParams; + } + + public void checkAccessDataFetching(DataFetchingEnvironment environment) { + if (fetcherParams.getPredicateRole() == null) { + return ; + } + if (environment.getFieldType() instanceof GraphQLObjectType) { + System.out.println(environment.getFieldType()); + for (Field field : environment.getFields()) { + checkAccessSelectionFields(field.getSelectionSet(), (GraphQLObjectType) environment.getFieldType()); + } + } + } + + public boolean isReadEntity(AnnotatedElement annotatedElement) { + if (annotatedElement != null) { + GraphQLReadEntityForRole readRoles = annotatedElement.getAnnotation(GraphQLReadEntityForRole.class); + if (readRoles != null) { + return fetcherParams.getPredicateRole().test(readRoles.value()); + } + } + + return false; + } + + public void checkAccessGraphQLObjectType(GraphQLType graphQLType) { + if (fetcherParams.getMapEntityType().existEntityType(graphQLType.getName())) { + Class cls = fetcherParams.getMapEntityType().getEntityType(graphQLType.getName()).getJavaType(); + + if (!isReadEntity(cls)) { + throw new RuntimeException("Read access error for entity "+graphQLType.getName()); + } + } + } + + public void checkAccessSelectionFields(SelectionSet selectionSet, GraphQLObjectType parentType) { + checkAccessGraphQLObjectType(parentType); + + if (selectionSet == null) + return; + + for (Selection sel :selectionSet.getSelections()) { + if (sel instanceof Field) { + Field field = (Field) sel; + + GraphQLType graphQLType; + + + GraphQLFieldDefinition fieldDef = parentType.getFieldDefinition(field.getName()); + if (fieldDef == null) { + throw new AbortExecutionException("Field "+field.getName()+" not found"); + } + graphQLType = fieldDef.getType(); + + + if (graphQLType instanceof GraphQLList) { + graphQLType = ((GraphQLList) graphQLType).getWrappedType(); + } + + if (graphQLType instanceof GraphQLObjectType) { + checkAccessSelectionFields(field.getSelectionSet(), (GraphQLObjectType)graphQLType); + } + } + } + } +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java index 4b9b2e006..bb99c59fe 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java @@ -48,18 +48,21 @@ class GraphQLJpaOneToManyDataFetcher extends GraphQLJpaQueryDataFetcher { private final PluralAttribute attribute; - public GraphQLJpaOneToManyDataFetcher(EntityManager entityManager, + public GraphQLJpaOneToManyDataFetcher(EntityManager entityManager, + FetcherParams fetcherParams, EntityType entityType, boolean toManyDefaultOptional, boolean defaultDistinct, PluralAttribute attribute) { - super(entityManager, entityType, defaultDistinct, toManyDefaultOptional); + super(entityManager, fetcherParams, entityType, defaultDistinct, toManyDefaultOptional); this.attribute = attribute; } @Override public Object get(DataFetchingEnvironment environment) { + checkAccessDataFetching(environment); + Field field = environment.getFields().iterator().next(); Object source = environment.getSource(); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index 8b0bbc460..876988640 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -44,7 +44,7 @@ * @author Igor Dianov * */ -class GraphQLJpaQueryDataFetcher extends QraphQLJpaBaseDataFetcher { +class GraphQLJpaQueryDataFetcher extends GraphQLJpaBaseDataFetcher { private boolean defaultDistinct = true; @@ -54,15 +54,21 @@ class GraphQLJpaQueryDataFetcher extends QraphQLJpaBaseDataFetcher { protected static final String ORG_HIBERNATE_READ_ONLY = "org.hibernate.readOnly"; protected static final String JAVAX_PERSISTENCE_FETCHGRAPH = "javax.persistence.fetchgraph"; - private GraphQLJpaQueryDataFetcher(EntityManager entityManager, EntityType entityType, boolean toManyDefaultOptional) { - super(entityManager, entityType, toManyDefaultOptional); + private GraphQLJpaQueryDataFetcher( + EntityManager entityManager, + FetcherParams fetcherParams, + EntityType entityType, + boolean toManyDefaultOptional + ) { + super(entityManager, fetcherParams, entityType, toManyDefaultOptional); } - public GraphQLJpaQueryDataFetcher(EntityManager entityManager, + public GraphQLJpaQueryDataFetcher(EntityManager entityManager, + FetcherParams fetcherParams, EntityType entityType, boolean defaultDistinct, boolean toManyDefaultOptional) { - super(entityManager, entityType, toManyDefaultOptional); + super(entityManager, fetcherParams, entityType, toManyDefaultOptional); this.defaultDistinct = defaultDistinct; } @@ -76,6 +82,8 @@ public void setDefaultDistinct(boolean defaultDistinct) { @Override public Object get(DataFetchingEnvironment environment) { + checkAccessDataFetching(environment); + Field field = environment.getFields().iterator().next(); Map result = new LinkedHashMap<>(); 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 eff118eec..a09db8681 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 @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -105,7 +106,9 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { private static final Logger log = LoggerFactory.getLogger(GraphQLJpaSchemaBuilder.class); - private EntityManager entityManager; + protected EntityManager entityManager; + protected Predicate predicateRole; + protected FetcherParams fetcherParams; private String name = "GraphQLJPAQuery"; @@ -120,17 +123,36 @@ public GraphQLJpaSchemaBuilder(EntityManager entityManager) { this.entityManager = entityManager; } + public GraphQLJpaSchemaBuilder predicateRole(Predicate predicateRole) { + + fetcherParams = new FetcherParams( + new MapEntityType(entityManager), + predicateRole + ); + + this.predicateRole = predicateRole; + return this; + } + /* (non-Javadoc) * @see org.activiti.services.query.qraphql.jpa.GraphQLSchemaBuilder#getGraphQLSchema() */ @Override public GraphQLSchema build() { + createFetcherParams(); return GraphQLSchema.newSchema() .query(getQueryType()) .build(); } - private GraphQLObjectType getQueryType() { + protected void createFetcherParams() { + fetcherParams = new FetcherParams( + new MapEntityType(entityManager), + predicateRole + ); + } + + protected GraphQLObjectType getQueryType() { GraphQLObjectType.Builder queryType = GraphQLObjectType.newObject() .name(this.name) @@ -160,7 +182,7 @@ private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType entityT .name(entityType.getName()) .description(getSchemaDescription(entityType)) .type(getObjectType(entityType)) - .dataFetcher(new GraphQLJpaSimpleDataFetcher(entityManager, entityType, toManyDefaultOptional)) + .dataFetcher(new GraphQLJpaSimpleDataFetcher(entityManager, fetcherParams, entityType, toManyDefaultOptional)) .argument(entityType.getAttributes().stream() .filter(this::isValidInput) .filter(this::isNotIgnored) @@ -203,7 +225,8 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType entit + "Use the '"+QUERY_SELECT_PARAM_NAME+"' field to request actual fields. " + "Use the '"+ORDER_BY_PARAM_NAME+"' on a field to specify sort order for each field. ") .type(pageType) - .dataFetcher(new GraphQLJpaQueryDataFetcher(entityManager, + .dataFetcher(new GraphQLJpaQueryDataFetcher(entityManager, + fetcherParams, entityType, isDefaultDistinct, toManyDefaultOptional)) @@ -638,7 +661,7 @@ private GraphQLArgument getArgument(Attribute attribute) { .build(); } - private GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean input) { + protected GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean input) { if (input && embeddableInputCache.containsKey(embeddableType.getJavaType())) return embeddableInputCache.get(embeddableType.getJavaType()); @@ -677,7 +700,7 @@ private GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean } - private GraphQLObjectType getObjectType(EntityType entityType) { + protected GraphQLObjectType getObjectType(EntityType entityType) { return entityCache.computeIfAbsent(entityType, this::computeObjectType); } @@ -773,6 +796,7 @@ else if (attribute instanceof PluralAttribute if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY) { dataFetcher = new GraphQLJpaOneToManyDataFetcher(entityManager, + fetcherParams, baseEntity, toManyDefaultOptional, isDefaultDistinct, @@ -927,28 +951,28 @@ private String getSchemaDescription(EmbeddableType embeddableType) { .getSchemaDescription() .orElse(null); } - - private boolean isNotIgnored(EmbeddableType attribute) { + + protected boolean isNotIgnored(EmbeddableType attribute) { return isNotIgnored(attribute.getJavaType()); } - - private boolean isNotIgnored(Attribute attribute) { + + protected boolean isNotIgnored(Attribute attribute) { return isNotIgnored(attribute.getJavaMember()) && isNotIgnored(attribute.getJavaType()); } - private boolean isIdentity(Attribute attribute) { + protected boolean isIdentity(Attribute attribute) { return attribute instanceof SingularAttribute && ((SingularAttribute)attribute).isId(); } - private boolean isNotIgnored(EntityType entityType) { + protected boolean isNotIgnored(EntityType entityType) { return isNotIgnored(entityType.getJavaType()); } - private boolean isNotIgnored(Member member) { + protected boolean isNotIgnored(Member member) { return member instanceof AnnotatedElement && isNotIgnored((AnnotatedElement) member); } - private boolean isNotIgnored(AnnotatedElement annotatedElement) { + protected boolean isNotIgnored(AnnotatedElement annotatedElement) { return annotatedElement != null && annotatedElement.getAnnotation(GraphQLIgnore.class) == null; } @@ -984,7 +1008,7 @@ protected boolean isNotIgnoredOrder(Attribute attribute) { @SuppressWarnings( "unchecked" ) - private GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { + protected GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { if (clazz.isEnum()) { if (classCache.containsKey(clazz)) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSimpleDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSimpleDataFetcher.java index d690850ea..6b4265c96 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSimpleDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSimpleDataFetcher.java @@ -30,16 +30,18 @@ import graphql.language.ObjectValue; import graphql.schema.DataFetchingEnvironment; -class GraphQLJpaSimpleDataFetcher extends QraphQLJpaBaseDataFetcher { +class GraphQLJpaSimpleDataFetcher extends GraphQLJpaBaseDataFetcher { - public GraphQLJpaSimpleDataFetcher(EntityManager entityManager, + public GraphQLJpaSimpleDataFetcher(EntityManager entityManager, + FetcherParams fetcherParams, EntityType entityType, boolean toManyDefaultOptional) { - super(entityManager, entityType, toManyDefaultOptional); + super(entityManager, fetcherParams, entityType, toManyDefaultOptional); } @Override public Object get(DataFetchingEnvironment environment) { + checkAccessDataFetching(environment); Field field = environment.getFields().iterator().next(); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/MapEntityType.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/MapEntityType.java new file mode 100644 index 000000000..7eb1d615d --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/MapEntityType.java @@ -0,0 +1,28 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import javax.persistence.EntityManager; +import javax.persistence.metamodel.EntityType; +import java.util.HashMap; +import java.util.Map; + +public class MapEntityType { + private EntityManager entityManager; + private Map entityTypeMap = new HashMap<>(); + + public MapEntityType(EntityManager entityManager) { + this.entityManager = entityManager; + + for (EntityType entityType : entityManager.getMetamodel().getEntities()) { + entityTypeMap.put(entityType.getName(), entityType); + } + } + + public boolean existEntityType(String name) { + return entityTypeMap.containsKey(name); + } + + public EntityType getEntityType(String name) { + return entityTypeMap.get(name); + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/AccessTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/AccessTests.java new file mode 100644 index 000000000..31d676f8c --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/AccessTests.java @@ -0,0 +1,122 @@ +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 graphql.ExecutionResult; +import graphql.validation.ValidationErrorType; +import org.junit.Before; +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.junit4.SpringRunner; + +import javax.persistence.EntityManager; + +import java.util.function.Predicate; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment= SpringBootTest.WebEnvironment.NONE +) +public class AccessTests { + + @SpringBootApplication + static class Application { + private static String currentRole = "user"; + + public static String getCurrentRole() { + return currentRole; + } + + public static void setCurrentRole(String currentRole) { + Application.currentRole = currentRole; + } + + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + + Predicate predicateAccess = roles -> { + for(int i = 0; i < roles.length; i++) { + if (roles[i].equals(this.currentRole)) { + return true; + } + } + return false; + }; + + return new GraphQLJpaSchemaBuilder(entityManager) + .predicateRole(predicateAccess) + .name("BooksExampleSchema") + .description("Books Example Schema"); + } + + } + + + @Autowired + private GraphQLJpaSchemaBuilder builder; + + @Autowired + private GraphQLExecutor executor; + + @Before + public void setup() { + } + + @Test + public void readErrorOperation() { + Application.setCurrentRole("user"); + + String query = "" + + "query { " + + " Books { " + + " select {" + + " id title author {id}" + + " } " + + " } " + + "}"; + + //when + ExecutionResult result = executor.execute(query); + + //then + assertThat(result.getErrors()) + .isNotEmpty() + .extracting("message") + .containsOnly( + "Exception while fetching data (/Books) : Read access error for entity Author" + ); + } + + @Test + public void readOkOperation() { + Application.setCurrentRole("admin"); + + String query = "" + + "query { " + + " Books { " + + " select {" + + " id title author {id}" + + " } " + + " } " + + "}"; + + //when + ExecutionResult result = executor.execute(query); + + //then + assertThat(result.getErrors()).isEmpty(); + + + } +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java index 35f553c06..e748c0b27 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java @@ -19,8 +19,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import java.sql.Date; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.time.*; import java.util.Map; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import com.introproventures.graphql.jpa.query.converter.model.VariableValue; @@ -276,4 +281,43 @@ public void string2Instant() { assert resultLDT.getMinute() == 15; assert resultLDT.getSecond() == 07; } + + @Test + public void Long2SqlDate() { + //given + Coercing coercing = JavaScalars.of(Date.class).getCoercing(); + final Long input = 1573294499000L; + + //when + Object result = coercing.serialize(input); + + //then + assertThat(result).isInstanceOf(Date.class); + + Date resultLDT = (Date) result; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + + assert sdf.format(resultLDT).equals("2019-11-09") == true; + } + + @Test + public void Long2SqlTimestamp() { + //given + Coercing coercing = JavaScalars.of(Timestamp.class).getCoercing(); + final Long input = 1573294499000L; + + //when + Object result = coercing.serialize(input); + + //then + assertThat(result).isInstanceOf(Timestamp.class); + + Timestamp resultLDT = (Timestamp) result; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + System.out.println(sdf.format(resultLDT)); + + assert sdf.format(resultLDT).equals("2019-11-09 10:14:59") == true; + } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java index b08e3dd89..b6c0e531f 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java @@ -7,6 +7,7 @@ import javax.persistence.Id; import javax.persistence.Transient; +import com.introproventures.graphql.jpa.query.annotation.GraphQLDefaultOrderBy; import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; @@ -51,6 +52,7 @@ public class CalculatedEntity extends ParentCalculatedEntity { Long id; @GraphQLDescription("title") + @GraphQLDefaultOrderBy String title; String info; diff --git a/pom.xml b/pom.xml index ad05856c6..c1538d1c8 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ graphql-jpa-query-example-model-starwars graphql-jpa-query-example-model-books graphql-jpa-query-graphiql + graphql-jpa-query-schema-mutations