Skip to content

Commit

Permalink
Supported embedded entities in graphql (#1339)
Browse files Browse the repository at this point in the history
* Added test

* Added complex attribute support for GraphQL.

* Added complex attribute write tests

* Fixed tests

* Resolving CVE security issue with rest easy

* Fixing codacy warnings

* Added support for lists of complex attributes

* Added test cases for lists of complex attributes

* Minor cleanup

* Added map support

* Fixed heisenbug in ModelBuilder

* Added map support for updates

* Fixed checkstyle

Co-authored-by: Aaron Klish <klish@verizonmedia.com>
  • Loading branch information
aklish and Aaron Klish committed May 25, 2020
1 parent 6dbf98e commit 96d8f4c
Show file tree
Hide file tree
Showing 32 changed files with 691 additions and 57 deletions.
2 changes: 1 addition & 1 deletion elide-example/elide-blog-example-resteasy/pom.xml
Expand Up @@ -18,7 +18,7 @@

<properties>
<mysql.data.directory>${project.build.directory}/mysql-data</mysql.data.directory>
<resteasy.version>3.11.2.Final</resteasy.version>
<resteasy.version>3.12.0.Final</resteasy.version>
</properties>

<dependencies>
Expand Down
Expand Up @@ -51,7 +51,7 @@ public Entity(Optional<Entity> parentResource, Map<String, Object> data,
}

@AllArgsConstructor
class Attribute {
public class Attribute {
@Getter private String name;
@Getter private Object value;
}
Expand Down
Expand Up @@ -48,16 +48,17 @@ public class GraphQLConversionUtils {

private final Map<Class<?>, GraphQLScalarType> scalarMap = new HashMap<>();

protected NonEntityDictionary nonEntityDictionary = new NonEntityDictionary();
protected NonEntityDictionary nonEntityDictionary;
protected EntityDictionary entityDictionary;

private final Map<Class, GraphQLObjectType> outputConversions = new HashMap<>();
private final Map<Class, GraphQLInputObjectType> inputConversions = new HashMap<>();
private final Map<Class, GraphQLEnumType> enumConversions = new HashMap<>();
private final Map<String, GraphQLList> mapConversions = new HashMap<>();

public GraphQLConversionUtils(EntityDictionary dictionary) {
this.entityDictionary = dictionary;
public GraphQLConversionUtils(EntityDictionary entityDictionary, NonEntityDictionary nonEntityDictionary) {
this.entityDictionary = entityDictionary;
this.nonEntityDictionary = nonEntityDictionary;
registerCustomScalars();
}

Expand Down
Expand Up @@ -49,7 +49,7 @@ public class ModelBuilder {
public static final String ARGUMENT_AFTER = "after";
public static final String ARGUMENT_OPERATION = "op";

private EntityDictionary dictionary;
private EntityDictionary entityDictionary;
private DataFetcher dataFetcher;
private GraphQLArgument relationshipOpArg;
private GraphQLArgument idArgument;
Expand All @@ -69,12 +69,15 @@ public class ModelBuilder {

/**
* Class constructor, constructs the custom arguments to handle mutations
* @param dictionary elide entity dictionary
* @param entityDictionary elide entity dictionary
* @param nonEntityDictionary elide non-entity dictionary
* @param dataFetcher graphQL data fetcher
*/
public ModelBuilder(EntityDictionary dictionary, DataFetcher dataFetcher) {
this.generator = new GraphQLConversionUtils(dictionary);
this.dictionary = dictionary;
public ModelBuilder(EntityDictionary entityDictionary,
NonEntityDictionary nonEntityDictionary,
DataFetcher dataFetcher) {
this.generator = new GraphQLConversionUtils(entityDictionary, nonEntityDictionary);
this.entityDictionary = entityDictionary;
this.dataFetcher = dataFetcher;

relationshipOpArg = newArgument()
Expand Down Expand Up @@ -143,24 +146,24 @@ public void withExcludedEntities(Set<Class<?>> excludedEntities) {
* @return The built schema.
*/
public GraphQLSchema build() {
Set<Class<?>> allClasses = dictionary.getBindings();
Set<Class<?>> allClasses = entityDictionary.getBindings();

if (allClasses.isEmpty()) {
throw new IllegalArgumentException("None of the provided classes are exported by Elide");
}

Set<Class<?>> rootClasses = allClasses.stream().filter(dictionary::isRoot).collect(Collectors.toSet());
Set<Class<?>> rootClasses = allClasses.stream().filter(entityDictionary::isRoot).collect(Collectors.toSet());

/*
* Walk the object graph (avoiding cycles) and construct the GraphQL input object types.
*/
dictionary.walkEntityGraph(rootClasses, this::buildInputObjectStub);
entityDictionary.walkEntityGraph(rootClasses, this::buildInputObjectStub);
resolveInputObjectRelationships();

/* Construct root object */
GraphQLObjectType.Builder root = newObject().name("_root");
for (Class<?> clazz : rootClasses) {
String entityName = dictionary.getJsonAliasFor(clazz);
String entityName = entityDictionary.getJsonAliasFor(clazz);
root.field(newFieldDefinition()
.name(entityName)
.dataFetcher(dataFetcher)
Expand All @@ -180,7 +183,7 @@ public GraphQLSchema build() {
/*
* Walk the object graph (avoiding cycles) and construct the GraphQL output object types.
*/
dictionary.walkEntityGraph(rootClasses, this::buildConnectionObject);
entityDictionary.walkEntityGraph(rootClasses, this::buildConnectionObject);

/* Construct the schema */
GraphQLSchema schema = GraphQLSchema.newSchema()
Expand All @@ -205,7 +208,7 @@ private GraphQLObjectType buildConnectionObject(Class<?> entityClass) {
return connectionObjectRegistry.get(entityClass);
}

String entityName = dictionary.getJsonAliasFor(entityClass);
String entityName = entityDictionary.getJsonAliasFor(entityClass);

GraphQLObjectType connectionObject = newObject()
.name(entityName)
Expand Down Expand Up @@ -236,21 +239,21 @@ private GraphQLObjectType buildQueryObject(Class<?> entityClass) {

log.debug("Building query object for {}", entityClass.getName());

String entityName = dictionary.getJsonAliasFor(entityClass);
String entityName = entityDictionary.getJsonAliasFor(entityClass);

GraphQLObjectType.Builder builder = newObject()
.name("_node__" + entityName);

String id = dictionary.getIdFieldName(entityClass);
String id = entityDictionary.getIdFieldName(entityClass);

/* our id types are DeferredId objects (not Scalars.GraphQLID) */
builder.field(newFieldDefinition()
.name(id)
.dataFetcher(dataFetcher)
.type(GraphQLScalars.GRAPHQL_DEFERRED_ID));

for (String attribute : dictionary.getAttributes(entityClass)) {
Class<?> attributeClass = dictionary.getType(entityClass, attribute);
for (String attribute : entityDictionary.getAttributes(entityClass)) {
Class<?> attributeClass = entityDictionary.getType(entityClass, attribute);
if (excludedEntities.contains(attributeClass)) {
continue;
}
Expand All @@ -274,14 +277,14 @@ private GraphQLObjectType buildQueryObject(Class<?> entityClass) {
);
}

for (String relationship : dictionary.getElideBoundRelationships(entityClass)) {
Class<?> relationshipClass = dictionary.getParameterizedType(entityClass, relationship);
for (String relationship : entityDictionary.getElideBoundRelationships(entityClass)) {
Class<?> relationshipClass = entityDictionary.getParameterizedType(entityClass, relationship);
if (excludedEntities.contains(relationshipClass)) {
continue;
}

String relationshipEntityName = dictionary.getJsonAliasFor(relationshipClass);
RelationshipType type = dictionary.getRelationshipType(entityClass, relationship);
String relationshipEntityName = entityDictionary.getJsonAliasFor(relationshipClass);
RelationshipType type = entityDictionary.getRelationshipType(entityClass, relationship);

if (type.isToOne()) {
builder.field(newFieldDefinition()
Expand Down Expand Up @@ -351,18 +354,18 @@ private GraphQLArgument buildInputObjectArgument(Class<?> entityClass, boolean a
private GraphQLInputType buildInputObjectStub(Class<?> clazz) {
log.debug("Building input object for {}", clazz.getName());

String entityName = dictionary.getJsonAliasFor(clazz);
String entityName = entityDictionary.getJsonAliasFor(clazz);

MutableGraphQLInputObjectType.Builder builder = MutableGraphQLInputObjectType.newMutableInputObject();
builder.name(entityName + ARGUMENT_INPUT);

String id = dictionary.getIdFieldName(clazz);
String id = entityDictionary.getIdFieldName(clazz);
builder.field(newInputObjectField()
.name(id)
.type(Scalars.GraphQLID));

for (String attribute : dictionary.getAttributes(clazz)) {
Class<?> attributeClass = dictionary.getType(clazz, attribute);
for (String attribute : entityDictionary.getAttributes(clazz)) {
Class<?> attributeClass = entityDictionary.getType(clazz, attribute);

if (excludedEntities.contains(attributeClass)) {
continue;
Expand Down Expand Up @@ -390,10 +393,6 @@ private GraphQLInputType buildInputObjectStub(Class<?> clazz) {
} else {
attributeType = convertedInputs.get(objectName);
}
} else {
String attributeTypeName = attributeType.getName();
convertedInputs.putIfAbsent(attributeTypeName, attributeType);
attributeType = convertedInputs.get(attributeTypeName);
}

builder.field(newInputObjectField()
Expand All @@ -412,14 +411,14 @@ private GraphQLInputType buildInputObjectStub(Class<?> clazz) {
*/
private void resolveInputObjectRelationships() {
inputObjectRegistry.forEach((clazz, inputObj) -> {
for (String relationship : dictionary.getElideBoundRelationships(clazz)) {
for (String relationship : entityDictionary.getElideBoundRelationships(clazz)) {
log.debug("Resolving relationship {} for {}", relationship, clazz.getName());
Class<?> relationshipClass = dictionary.getParameterizedType(clazz, relationship);
Class<?> relationshipClass = entityDictionary.getParameterizedType(clazz, relationship);
if (excludedEntities.contains(relationshipClass)) {
continue;
}

RelationshipType type = dictionary.getRelationshipType(clazz, relationship);
RelationshipType type = entityDictionary.getRelationshipType(clazz, relationship);

if (type.isToOne()) {
inputObj.setField(relationship, newInputObjectField()
Expand Down
Expand Up @@ -16,6 +16,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* Basically the same class as GraphQLInputObjectType except fields can be added after the
Expand Down Expand Up @@ -46,7 +47,7 @@
public class MutableGraphQLInputObjectType extends GraphQLInputObjectType {

private final Map<String, GraphQLInputObjectField> fieldMap = new LinkedHashMap<String, GraphQLInputObjectField>();
private String name;
private final String name;

public MutableGraphQLInputObjectType(String name, String description, List<GraphQLInputObjectField> fields) {
super(name, description, fields);
Expand All @@ -59,10 +60,6 @@ public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

private void buildMap(List<GraphQLInputObjectField> fields) {
for (GraphQLInputObjectField field : fields) {
String name = field.getName();
Expand Down Expand Up @@ -137,4 +134,21 @@ public MutableGraphQLInputObjectType build() {
return new MutableGraphQLInputObjectType(name, description, fields);
}
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MutableGraphQLInputObjectType that = (MutableGraphQLInputObjectType) o;
return Objects.equals(name, that.name);
}

@Override
public int hashCode() {
return Objects.hash(name);
}
}
Expand Up @@ -20,6 +20,7 @@
import com.yahoo.elide.core.pagination.Pagination;
import com.yahoo.elide.core.sort.Sorting;
import com.yahoo.elide.graphql.containers.ConnectionContainer;
import com.yahoo.elide.graphql.containers.MapEntryContainer;

import com.google.common.collect.Sets;

Expand All @@ -29,6 +30,7 @@
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLType;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayDeque;
Expand All @@ -53,8 +55,12 @@
public class PersistentResourceFetcher implements DataFetcher<Object> {
private final ElideSettings settings;

public PersistentResourceFetcher(ElideSettings settings) {
@Getter
private final NonEntityDictionary nonEntityDictionary;

public PersistentResourceFetcher(ElideSettings settings, NonEntityDictionary nonEntityDictionary) {
this.settings = settings;
this.nonEntityDictionary = nonEntityDictionary;
}

/**
Expand Down Expand Up @@ -455,7 +461,14 @@ private PersistentResource<?> updateAttributes(PersistentResource<?> toUpdate,
/* iterate through each attribute provided */
for (Entity.Attribute attribute : attributes) {
if (dictionary.isAttribute(entityClass, attribute.getName())) {
toUpdate.updateAttribute(attribute.getName(), attribute.getValue());
Class<?> attributeType = dictionary.getType(entityClass, attribute.getName());
Object attributeValue;
if (Map.class.isAssignableFrom(attributeType)) {
attributeValue = MapEntryContainer.translateFromGraphQLMap(attribute);
} else {
attributeValue = attribute.getValue();
}
toUpdate.updateAttribute(attribute.getName(), attributeValue);
} else if (!Objects.equals(attribute.getName(), idFieldName)) {
throw new IllegalStateException("Unrecognized attribute passed to 'data': " + attribute.getName());
}
Expand Down
Expand Up @@ -64,8 +64,10 @@ public class QueryRunner {
public QueryRunner(Elide elide) {
this.elide = elide;

PersistentResourceFetcher fetcher = new PersistentResourceFetcher(elide.getElideSettings());
ModelBuilder builder = new ModelBuilder(elide.getElideSettings().getDictionary(), fetcher);
NonEntityDictionary nonEntityDictionary = new NonEntityDictionary();
PersistentResourceFetcher fetcher = new PersistentResourceFetcher(elide.getElideSettings(),
nonEntityDictionary);
ModelBuilder builder = new ModelBuilder(elide.getElideSettings().getDictionary(), nonEntityDictionary, fetcher);

this.api = new GraphQL(builder.build());

Expand Down
Expand Up @@ -6,9 +6,13 @@
package com.yahoo.elide.graphql.containers;

import com.yahoo.elide.core.exceptions.BadRequestException;
import com.yahoo.elide.graphql.Entity;
import com.yahoo.elide.graphql.Environment;
import com.yahoo.elide.graphql.NonEntityDictionary;
import com.yahoo.elide.graphql.PersistentResourceFetcher;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
Expand All @@ -31,14 +35,56 @@ public MapEntryContainer(Map.Entry entry) {

@Override
public Object processFetch(Environment context, PersistentResourceFetcher fetcher) {
NonEntityDictionary nonEntityDictionary = fetcher.getNonEntityDictionary();
String fieldName = context.field.getName();

Object returnObject;
if (KEY.equalsIgnoreCase(fieldName)) {
return entry.getKey();
returnObject = entry.getKey();

} else if (VALUE.equalsIgnoreCase(fieldName)) {
return entry.getValue();
returnObject = entry.getValue();
} else {
throw new BadRequestException("Invalid field: '" + fieldName
+ "'. Maps only contain fields 'key' and 'value'");
}

if (nonEntityDictionary.hasBinding(returnObject.getClass())) {
return new NonEntityContainer(returnObject);
}
return returnObject;
}

/**
* Converts an attribute which is a list of maps - each containing a KEY and a VALUE
* into a HashMap with the value of KEY as key and the value of VALUE as the value.
* @param attribute The attribute to convert.
* @return The converted map.
*/
public static Map translateFromGraphQLMap(Entity.Attribute attribute) {
Map returnMap = new HashMap();
Object collection = attribute.getValue();

if (collection == null) {
return null;
}

if (! (collection instanceof Collection)) {
throw new BadRequestException("Invalid map format for GraphQL request");
}

throw new BadRequestException("Invalid field: '" + fieldName + "'. Maps only contain fields 'key' and 'value'");
((Collection) collection).stream().forEach((entry -> {
if (! (entry instanceof Map)) {
throw new BadRequestException("Invalid map format for GraphQL request");
}

if (! ((Map) entry).containsKey(KEY) && ((Map) entry).containsKey(VALUE)) {
throw new BadRequestException("Invalid map format for GraphQL request");
}

returnMap.put(((Map) entry).get(KEY), ((Map) entry).get(VALUE));
}));

return returnMap;
}
}

0 comments on commit 96d8f4c

Please sign in to comment.