diff --git a/elide-async/src/main/java/com/yahoo/elide/async/export/validator/NoRelationshipsProjectionValidator.java b/elide-async/src/main/java/com/yahoo/elide/async/export/validator/NoRelationshipsProjectionValidator.java new file mode 100644 index 0000000000..215b54a726 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/export/validator/NoRelationshipsProjectionValidator.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.export.validator; + +import com.yahoo.elide.core.exceptions.BadRequestException; +import com.yahoo.elide.core.request.EntityProjection; + +import java.util.Collection; + +/** + * Validates none of the projections have relationships. + */ +public class NoRelationshipsProjectionValidator implements Validator { + + @Override + public void validateProjection(Collection projections) { + for (EntityProjection projection : projections) { + if (!projection.getRelationships().isEmpty()) { + throw new BadRequestException( + "Export is not supported for Query that requires traversing Relationships."); + } + } + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/export/validator/SingleRootProjectionValidator.java b/elide-async/src/main/java/com/yahoo/elide/async/export/validator/SingleRootProjectionValidator.java new file mode 100644 index 0000000000..3704284a3f --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/export/validator/SingleRootProjectionValidator.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.export.validator; + +import com.yahoo.elide.core.exceptions.BadRequestException; +import com.yahoo.elide.core.request.EntityProjection; + +import java.util.Collection; + +/** + * Validates each projection in collection have one projection only. + */ +public class SingleRootProjectionValidator implements Validator { + + @Override + public void validateProjection(Collection projections) { + if (projections.size() != 1) { + throw new BadRequestException("Export is only supported for single Query with one root projection."); + } + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/export/validator/Validator.java b/elide-async/src/main/java/com/yahoo/elide/async/export/validator/Validator.java index 29a53eb33d..638ebba215 100644 --- a/elide-async/src/main/java/com/yahoo/elide/async/export/validator/Validator.java +++ b/elide-async/src/main/java/com/yahoo/elide/async/export/validator/Validator.java @@ -5,7 +5,9 @@ */ package com.yahoo.elide.async.export.validator; -import graphql.language.Field; +import com.yahoo.elide.core.request.EntityProjection; + +import java.util.Collection; /** * Utility interface used to validate Entity Projections. @@ -14,8 +16,7 @@ public interface Validator { /** * Validates the EntityProjection. - * @param entityType Class of the Entity to be validated. - * @param field GraphQL Field. + * @param projections Collection of EntityProjections to validate. */ - public void validateProjection(Class entityType, Field field); + public void validateProjection(Collection projections); } diff --git a/elide-async/src/main/java/com/yahoo/elide/async/operation/GraphQLTableExportOperation.java b/elide-async/src/main/java/com/yahoo/elide/async/operation/GraphQLTableExportOperation.java index 8837dbcadf..1e84609ab9 100644 --- a/elide-async/src/main/java/com/yahoo/elide/async/operation/GraphQLTableExportOperation.java +++ b/elide-async/src/main/java/com/yahoo/elide/async/operation/GraphQLTableExportOperation.java @@ -7,13 +7,13 @@ import com.yahoo.elide.Elide; import com.yahoo.elide.async.export.formatter.TableExportFormatter; +import com.yahoo.elide.async.export.validator.NoRelationshipsProjectionValidator; import com.yahoo.elide.async.models.AsyncAPI; import com.yahoo.elide.async.models.TableExport; import com.yahoo.elide.async.service.AsyncExecutorService; import com.yahoo.elide.async.service.storageengine.ResultStorageEngine; import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.datastore.DataStoreTransaction; -import com.yahoo.elide.core.exceptions.BadRequestException; import com.yahoo.elide.core.request.EntityProjection; import com.yahoo.elide.core.security.User; import com.yahoo.elide.graphql.GraphQLRequestScope; @@ -27,10 +27,10 @@ import lombok.extern.slf4j.Slf4j; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; import java.util.UUID; /** @@ -41,7 +41,8 @@ public class GraphQLTableExportOperation extends TableExportOperation { public GraphQLTableExportOperation(TableExportFormatter formatter, AsyncExecutorService service, AsyncAPI export, RequestScope scope, ResultStorageEngine engine) { - super(formatter, service, export, scope, engine); + super(formatter, service, export, scope, engine, + Arrays.asList(new NoRelationshipsProjectionValidator())); } @Override @@ -53,9 +54,8 @@ public RequestScope getRequestScope(TableExport export, User user, String apiVer } @Override - public EntityProjection getProjection(TableExport export, RequestScope scope) - throws BadRequestException { - EntityProjection projection; + public Collection getProjections(TableExport export, RequestScope scope) { + GraphQLProjectionInfo projectionInfo; try { String graphQLDocument = export.getQuery(); Elide elide = getService().getElide(); @@ -65,20 +65,13 @@ public EntityProjection getProjection(TableExport export, RequestScope scope) Map variables = QueryRunner.extractVariables(mapper, node); String queryString = QueryRunner.extractQuery(node); - GraphQLProjectionInfo projectionInfo = - new GraphQLEntityProjectionMaker(elide.getElideSettings(), variables, scope.getApiVersion()) - .make(queryString); - - //TODO Call Validators. - Optional> optionalEntry = - projectionInfo.getProjections().entrySet().stream().findFirst(); - - projection = optionalEntry.isPresent() ? optionalEntry.get().getValue() : null; + projectionInfo = new GraphQLEntityProjectionMaker(elide.getElideSettings(), variables, + scope.getApiVersion()).make(queryString); } catch (IOException e) { throw new IllegalStateException(e); } - return projection; + return projectionInfo.getProjections().values(); } } diff --git a/elide-async/src/main/java/com/yahoo/elide/async/operation/JSONAPITableExportOperation.java b/elide-async/src/main/java/com/yahoo/elide/async/operation/JSONAPITableExportOperation.java index 0c95c84e9d..c444664c8e 100644 --- a/elide-async/src/main/java/com/yahoo/elide/async/operation/JSONAPITableExportOperation.java +++ b/elide-async/src/main/java/com/yahoo/elide/async/operation/JSONAPITableExportOperation.java @@ -7,6 +7,7 @@ import com.yahoo.elide.Elide; import com.yahoo.elide.async.export.formatter.TableExportFormatter; +import com.yahoo.elide.async.export.validator.NoRelationshipsProjectionValidator; import com.yahoo.elide.async.models.AsyncAPI; import com.yahoo.elide.async.models.TableExport; import com.yahoo.elide.async.service.AsyncExecutorService; @@ -22,6 +23,8 @@ import lombok.extern.slf4j.Slf4j; import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.UUID; @@ -35,7 +38,8 @@ public class JSONAPITableExportOperation extends TableExportOperation { public JSONAPITableExportOperation(TableExportFormatter formatter, AsyncExecutorService service, AsyncAPI export, RequestScope scope, ResultStorageEngine engine) { - super(formatter, service, export, scope, engine); + super(formatter, service, export, scope, engine, + Arrays.asList(new NoRelationshipsProjectionValidator())); } @Override @@ -53,7 +57,7 @@ public RequestScope getRequestScope(TableExport export, User user, String apiVer } @Override - public EntityProjection getProjection(TableExport export, RequestScope scope) throws BadRequestException { + public Collection getProjections(TableExport export, RequestScope scope) { EntityProjection projection = null; try { URIBuilder uri = new URIBuilder(export.getQuery()); @@ -64,6 +68,6 @@ public EntityProjection getProjection(TableExport export, RequestScope scope) th } catch (URISyntaxException e) { throw new BadRequestException(e.getMessage()); } - return projection; + return Collections.singletonList(projection); } } diff --git a/elide-async/src/main/java/com/yahoo/elide/async/operation/TableExportOperation.java b/elide-async/src/main/java/com/yahoo/elide/async/operation/TableExportOperation.java index 6bb3beaaa5..669ef26b4d 100644 --- a/elide-async/src/main/java/com/yahoo/elide/async/operation/TableExportOperation.java +++ b/elide-async/src/main/java/com/yahoo/elide/async/operation/TableExportOperation.java @@ -7,6 +7,8 @@ import com.yahoo.elide.Elide; import com.yahoo.elide.async.export.formatter.TableExportFormatter; +import com.yahoo.elide.async.export.validator.SingleRootProjectionValidator; +import com.yahoo.elide.async.export.validator.Validator; import com.yahoo.elide.async.models.AsyncAPI; import com.yahoo.elide.async.models.AsyncAPIResult; import com.yahoo.elide.async.models.TableExport; @@ -28,8 +30,12 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.UUID; import java.util.concurrent.Callable; @@ -45,14 +51,16 @@ public abstract class TableExportOperation implements Callable { private TableExport exportObj; private RequestScope scope; private ResultStorageEngine engine; + private List validators = new ArrayList<>(Arrays.asList(new SingleRootProjectionValidator())); public TableExportOperation(TableExportFormatter formatter, AsyncExecutorService service, - AsyncAPI exportObj, RequestScope scope, ResultStorageEngine engine) { + AsyncAPI exportObj, RequestScope scope, ResultStorageEngine engine, List validators) { this.formatter = formatter; this.service = service; this.exportObj = (TableExport) exportObj; this.scope = scope; this.engine = engine; + this.validators.addAll(validators); } @Override @@ -64,7 +72,9 @@ public AsyncAPIResult call() { try (DataStoreTransaction tx = elide.getDataStore().beginTransaction()) { RequestScope requestScope = getRequestScope(exportObj, scope.getUser(), apiVersion, tx); - EntityProjection projection = getProjection(exportObj, requestScope); + Collection projections = getProjections(exportObj, requestScope); + validateProjections(projections); + EntityProjection projection = projections.iterator().next(); Observable observableResults = export(exportObj, requestScope, projection); @@ -85,7 +95,7 @@ public AsyncAPIResult call() { exportResult.setUrl(new URL(generateDownloadURL(exportObj, (RequestScope) scope))); exportResult.setRecordCount(recordNumber); } catch (BadRequestException e) { - exportResult.setMessage("Bad Request body"); + exportResult.setMessage(e.getMessage()); } catch (MalformedURLException e) { exportResult.setMessage("Download url generation failure."); } catch (Exception e) { @@ -194,14 +204,16 @@ protected TableExport storeResults(TableExport exportObj, ResultStorageEngine re return resultStorageEngine.storeResults(exportObj, result); } + private void validateProjections(Collection projections) { + validators.forEach(validator -> validator.validateProjection(projections)); + } + /** * Generate Entity Projection from the query. * @param exportObj TableExport type object. * @param requestScope requestScope object. - * @return EntityProjection object. - * @throws BadRequestException BadRequestException. + * @return Collection of EntityProjection object. */ - public abstract EntityProjection getProjection(TableExport exportObj, RequestScope requestScope) - throws BadRequestException; + public abstract Collection getProjections(TableExport exportObj, RequestScope requestScope); } diff --git a/elide-async/src/test/java/com/yahoo/elide/async/models/ArtifactGroup.java b/elide-async/src/test/java/com/yahoo/elide/async/models/ArtifactGroup.java new file mode 100644 index 0000000000..7eebdf9b8e --- /dev/null +++ b/elide-async/src/test/java/com/yahoo/elide/async/models/ArtifactGroup.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021, Verizon Media. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +import com.yahoo.elide.annotation.Include; + +import java.util.ArrayList; +import java.util.List; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; + +@Include(rootLevel = true, type = "group") +@Entity +public class ArtifactGroup { + @Id + private String name = ""; + + @OneToMany(mappedBy = "group") + private List products = new ArrayList<>(); +} diff --git a/elide-async/src/test/java/com/yahoo/elide/async/models/ArtifactProduct.java b/elide-async/src/test/java/com/yahoo/elide/async/models/ArtifactProduct.java new file mode 100644 index 0000000000..ea7898063f --- /dev/null +++ b/elide-async/src/test/java/com/yahoo/elide/async/models/ArtifactProduct.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021, Verizon Media. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +import com.yahoo.elide.annotation.Include; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; + +@Include(type = "product") +@Entity +public class ArtifactProduct { + @Id + private String name = ""; + + @ManyToOne + private ArtifactGroup group = null; +} diff --git a/elide-async/src/test/java/com/yahoo/elide/async/operation/GraphQLTableExportOperationTest.java b/elide-async/src/test/java/com/yahoo/elide/async/operation/GraphQLTableExportOperationTest.java index 49b538cf30..c769a8c9f3 100644 --- a/elide-async/src/test/java/com/yahoo/elide/async/operation/GraphQLTableExportOperationTest.java +++ b/elide-async/src/test/java/com/yahoo/elide/async/operation/GraphQLTableExportOperationTest.java @@ -10,9 +10,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; + import com.yahoo.elide.Elide; import com.yahoo.elide.ElideSettingsBuilder; import com.yahoo.elide.async.export.formatter.JSONExportFormatter; +import com.yahoo.elide.async.models.ArtifactGroup; import com.yahoo.elide.async.models.QueryType; import com.yahoo.elide.async.models.ResultType; import com.yahoo.elide.async.models.TableExport; @@ -37,8 +39,10 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.UUID; @@ -53,7 +57,8 @@ public class GraphQLTableExportOperationTest { @BeforeEach public void setupMocks(@TempDir Path tempDir) { - dataStore = new HashMapDataStore(TableExport.class.getPackage()); + dataStore = new HashMapDataStore( + new HashSet<>(Arrays.asList(TableExport.class.getPackage(), ArtifactGroup.class.getPackage()))); Map> map = new HashMap<>(); map.put(AsyncAPIInlineChecks.AsyncAPIOwner.PRINCIPAL_IS_OWNER, AsyncAPIInlineChecks.AsyncAPIOwner.class); @@ -138,6 +143,69 @@ public void testProcessBadQuery() throws URISyntaxException, IOException { assertEquals("Bad Request Body'Can't parse query: { tableExport { edges { node { id principalName} } }'", queryResultObj.getMessage()); } + @Test + public void testProcessQueryWithRelationship() { + TableExport queryObj = new TableExport(); + String query = "{\"query\":\"{ group { edges { node { name products {edges { node { name } } } } } } }\", \"variables\":null}"; + String id = "edc4a871-dff2-4194-804e-d80075cf827d"; + queryObj.setId(id); + queryObj.setQuery(query); + queryObj.setQueryType(QueryType.GRAPHQL_V1_0); + queryObj.setResultType(ResultType.CSV); + + GraphQLTableExportOperation graphQLOperation = new GraphQLTableExportOperation(new JSONExportFormatter(elide), + asyncExecutorService, queryObj, requestScope, engine); + TableExportResult queryResultObj = (TableExportResult) graphQLOperation.call(); + + assertEquals(200, queryResultObj.getHttpStatus()); + assertEquals("Export is not supported for Query that requires traversing Relationships.", + queryResultObj.getMessage()); + assertEquals(null, queryResultObj.getRecordCount()); + assertEquals(null, queryResultObj.getUrl()); + } + + @Test + public void testProcessQueryWithMultipleProjection() { + TableExport queryObj = new TableExport(); + String query = "{\"query\":\"{ tableExport { edges { node { principalName } } } asyncQuery { edges { node { principalName } } } }\",\"variables\":null}"; + String id = "edc4a871-dff2-4094-804e-d80075cf827d"; + queryObj.setId(id); + queryObj.setQuery(query); + queryObj.setQueryType(QueryType.GRAPHQL_V1_0); + queryObj.setResultType(ResultType.CSV); + + GraphQLTableExportOperation graphQLOperation = new GraphQLTableExportOperation(new JSONExportFormatter(elide), + asyncExecutorService, queryObj, requestScope, engine); + TableExportResult queryResultObj = (TableExportResult) graphQLOperation.call(); + + assertEquals(200, queryResultObj.getHttpStatus()); + assertEquals("Export is only supported for single Query with one root projection.", + queryResultObj.getMessage()); + assertEquals(null, queryResultObj.getRecordCount()); + assertEquals(null, queryResultObj.getUrl()); + } + + @Test + public void testProcessMultipleQuery() { + TableExport queryObj = new TableExport(); + String query = "{\"query\":\"{ tableExport { edges { node { principalName } } } } { asyncQuery { edges { node { principalName } } } }\",\"variables\":null}"; + String id = "edc4a871-dff2-4094-804e-d80075cf827d"; + queryObj.setId(id); + queryObj.setQuery(query); + queryObj.setQueryType(QueryType.GRAPHQL_V1_0); + queryObj.setResultType(ResultType.CSV); + + GraphQLTableExportOperation graphQLOperation = new GraphQLTableExportOperation(new JSONExportFormatter(elide), + asyncExecutorService, queryObj, requestScope, engine); + TableExportResult queryResultObj = (TableExportResult) graphQLOperation.call(); + + assertEquals(200, queryResultObj.getHttpStatus()); + assertEquals("Export is only supported for single Query with one root projection.", + queryResultObj.getMessage()); + assertEquals(null, queryResultObj.getRecordCount()); + assertEquals(null, queryResultObj.getUrl()); + } + /** * Prepping and Storing an TableExport entry to be queried later on. * @throws IOException IOException diff --git a/elide-async/src/test/java/com/yahoo/elide/async/operation/JsonAPITableExportOperationTest.java b/elide-async/src/test/java/com/yahoo/elide/async/operation/JsonAPITableExportOperationTest.java index 66c3db372d..da87c102b9 100644 --- a/elide-async/src/test/java/com/yahoo/elide/async/operation/JsonAPITableExportOperationTest.java +++ b/elide-async/src/test/java/com/yahoo/elide/async/operation/JsonAPITableExportOperationTest.java @@ -11,9 +11,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; + import com.yahoo.elide.Elide; import com.yahoo.elide.ElideSettingsBuilder; import com.yahoo.elide.async.export.formatter.JSONExportFormatter; +import com.yahoo.elide.async.models.ArtifactGroup; import com.yahoo.elide.async.models.QueryType; import com.yahoo.elide.async.models.ResultType; import com.yahoo.elide.async.models.TableExport; @@ -38,8 +40,10 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.UUID; @@ -54,7 +58,8 @@ public class JsonAPITableExportOperationTest { @BeforeEach public void setupMocks(@TempDir Path tempDir) { - dataStore = new HashMapDataStore(TableExport.class.getPackage()); + dataStore = new HashMapDataStore( + new HashSet<>(Arrays.asList(TableExport.class.getPackage(), ArtifactGroup.class.getPackage()))); Map> map = new HashMap<>(); map.put(AsyncAPIInlineChecks.AsyncAPIOwner.PRINCIPAL_IS_OWNER, AsyncAPIInlineChecks.AsyncAPIOwner.class); @@ -138,7 +143,29 @@ public void testProcessBadQuery() throws URISyntaxException, IOException { TableExportResult queryResultObj = (TableExportResult) jsonAPIOperation.call(); assertEquals(200, queryResultObj.getHttpStatus()); - assertEquals("Bad Request body", queryResultObj.getMessage()); + assertEquals("Illegal character in path at index 12: tableExport/^IllegalCharacter^", + queryResultObj.getMessage()); + } + + @Test + public void testProcessQueryWithRelationship() { + TableExport queryObj = new TableExport(); + String query = "/group?fields[group]=products"; + String id = "edc4a871-dff2-4194-804e-d80075cf827d"; + queryObj.setId(id); + queryObj.setQuery(query); + queryObj.setQueryType(QueryType.JSONAPI_V1_0); + queryObj.setResultType(ResultType.CSV); + + JSONAPITableExportOperation jsonAPIOperation = new JSONAPITableExportOperation(new JSONExportFormatter(elide), + asyncExecutorService, queryObj, requestScope, engine); + TableExportResult queryResultObj = (TableExportResult) jsonAPIOperation.call(); + + assertEquals(200, queryResultObj.getHttpStatus()); + assertEquals("Export is not supported for Query that requires traversing Relationships.", + queryResultObj.getMessage()); + assertEquals(null, queryResultObj.getRecordCount()); + assertEquals(null, queryResultObj.getUrl()); } /**