From 48f97b0e7c533a7d847327e539b53d644328d461 Mon Sep 17 00:00:00 2001 From: Aaron Klish Date: Mon, 2 Aug 2021 09:32:08 -0500 Subject: [PATCH 1/3] Upgrade graphql-java from 6 to 16.2 --- .../yahoo/elide/core/EntityDictionary.java | 2 + .../AggregationDataStoreIntegrationTest.java | 1815 +++++++++++++++++ elide-graphql/pom.xml | 2 +- .../com/yahoo/elide/graphql/Environment.java | 2 +- .../elide/graphql/GraphQLConversionUtils.java | 53 +- .../elide/graphql/GraphQLErrorSerializer.java | 1 + .../yahoo/elide/graphql/GraphQLNameUtils.java | 38 + .../yahoo/elide/graphql/GraphQLScalars.java | 24 +- .../java/com/yahoo/elide/graphql/KeyWord.java | 49 + .../com/yahoo/elide/graphql/ModelBuilder.java | 145 +- .../MutableGraphQLInputObjectType.java | 154 -- .../graphql/PersistentResourceFetcher.java | 8 +- .../com/yahoo/elide/graphql/QueryRunner.java | 5 +- .../yahoo/elide/graphql/FetcherFetchTest.java | 1 + .../graphql/GraphQLConversionUtilsTest.java | 9 +- .../elide/graphql/GraphQLEndpointTest.java | 2 +- .../yahoo/elide/graphql/ModelBuilderTest.java | 33 +- .../PersistentResourceFetcherTest.java | 31 +- .../src/test/java/example/Author.java | 8 + .../graphql/requests/fetch/rootSingle.graphql | 1 + .../requests/upsert/rootSingleWithId.graphql | 3 +- .../graphql/responses/fetch/rootSingle.json | 3 +- .../responses/upsert/rootSingleWithId.json | 3 +- .../java/com/yahoo/elide/tests/GraphQLIT.java | 3 +- .../graphQLMutationError.json | 3 +- 25 files changed, 2095 insertions(+), 303 deletions(-) create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLNameUtils.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/KeyWord.java delete mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/MutableGraphQLInputObjectType.java diff --git a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java index ad7c0bbe7b..6b5ba1c75c 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java @@ -77,6 +77,8 @@ @SuppressWarnings("static-method") public class EntityDictionary { + public static final String NO_VERSION = ""; + protected final ConcurrentHashMap> bindJsonApiToEntity = new ConcurrentHashMap<>(); protected final ConcurrentHashMap, EntityBinding> entityBindings = new ConcurrentHashMap<>(); protected final CopyOnWriteArrayList> bindEntityRoots = new CopyOnWriteArrayList<>(); diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java new file mode 100644 index 0000000000..946c508642 --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java @@ -0,0 +1,1815 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.datastores.aggregation.integration; + +import static com.yahoo.elide.test.graphql.GraphQLDSL.argument; +import static com.yahoo.elide.test.graphql.GraphQLDSL.arguments; +import static com.yahoo.elide.test.graphql.GraphQLDSL.document; +import static com.yahoo.elide.test.graphql.GraphQLDSL.field; +import static com.yahoo.elide.test.graphql.GraphQLDSL.mutation; +import static com.yahoo.elide.test.graphql.GraphQLDSL.selection; +import static com.yahoo.elide.test.graphql.GraphQLDSL.selections; +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import com.yahoo.elide.Elide; +import com.yahoo.elide.ElideSettingsBuilder; +import com.yahoo.elide.core.audit.TestAuditLogger; +import com.yahoo.elide.core.datastore.test.DataStoreTestHarness; +import com.yahoo.elide.core.dictionary.EntityDictionary; +import com.yahoo.elide.core.exceptions.HttpStatus; +import com.yahoo.elide.core.security.checks.Check; +import com.yahoo.elide.core.security.checks.prefab.Role; +import com.yahoo.elide.datastores.aggregation.AggregationDataStore; +import com.yahoo.elide.datastores.aggregation.checks.OperatorCheck; +import com.yahoo.elide.datastores.aggregation.checks.VideoGameFilterCheck; +import com.yahoo.elide.datastores.aggregation.example.PlayerStats; +import com.yahoo.elide.datastores.aggregation.framework.AggregationDataStoreTestHarness; +import com.yahoo.elide.datastores.aggregation.framework.SQLUnitTest; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; +import com.yahoo.elide.datastores.aggregation.queryengines.sql.ConnectionDetails; +import com.yahoo.elide.datastores.aggregation.queryengines.sql.DataSourceConfiguration; +import com.yahoo.elide.datastores.aggregation.queryengines.sql.dialects.SQLDialect; +import com.yahoo.elide.datastores.aggregation.queryengines.sql.dialects.SQLDialectFactory; +import com.yahoo.elide.initialization.GraphQLIntegrationTest; +import com.yahoo.elide.jsonapi.resources.JsonApiEndpoint; +import com.yahoo.elide.modelconfig.DBPasswordExtractor; +import com.yahoo.elide.modelconfig.model.DBConfig; +import com.yahoo.elide.modelconfig.validator.DynamicConfigValidator; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import example.TestCheckMappings; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import javax.inject.Inject; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.SecurityContext; + +/** + * Integration tests for {@link AggregationDataStore}. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AggregationDataStoreIntegrationTest extends GraphQLIntegrationTest { + + @Mock private static SecurityContext securityContextMock; + + public static DynamicConfigValidator VALIDATOR; + + static { + VALIDATOR = new DynamicConfigValidator("src/test/resources/configs"); + + try { + VALIDATOR.readAndValidateConfigs(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static final class SecurityHjsonIntegrationTestResourceConfig extends ResourceConfig { + + @Inject + public SecurityHjsonIntegrationTestResourceConfig() { + register(new AbstractBinder() { + @Override + protected void configure() { + Map> map = new HashMap<>(TestCheckMappings.MAPPINGS); + map.put(OperatorCheck.OPERTOR_CHECK, OperatorCheck.class); + map.put(VideoGameFilterCheck.NAME_FILTER, VideoGameFilterCheck.class); + EntityDictionary dictionary = new EntityDictionary(map); + + VALIDATOR.getElideSecurityConfig().getRoles().forEach(role -> + dictionary.addRoleCheck(role, new Role.RoleMemberCheck(role)) + ); + + Elide elide = new Elide(new ElideSettingsBuilder(getDataStore()) + .withEntityDictionary(dictionary) + .withAuditLogger(new TestAuditLogger()) + .withISO8601Dates("yyyy-MM-dd'T'HH:mm'Z'", Calendar.getInstance().getTimeZone()) + .build()); + bind(elide).to(Elide.class).named("elide"); + } + }); + register((ContainerRequestFilter) requestContext -> requestContext.setSecurityContext(securityContextMock)); + } + } + + public AggregationDataStoreIntegrationTest() { + super(SecurityHjsonIntegrationTestResourceConfig.class, JsonApiEndpoint.class.getPackage().getName()); + } + + @BeforeAll + public void beforeAll() { + SQLUnitTest.init(); + } + + @BeforeEach + public void setUp() { + reset(securityContextMock); + when(securityContextMock.isUserInRole("admin.user")).thenReturn(true); + when(securityContextMock.isUserInRole("operator")).thenReturn(true); + when(securityContextMock.isUserInRole("guest user")).thenReturn(true); + } + + @Override + protected DataStoreTestHarness createHarness() { + + HikariConfig config = new HikariConfig(File.separator + "jpah2db.properties"); + DataSource defaultDataSource = new HikariDataSource(config); + SQLDialect defaultDialect = SQLDialectFactory.getDefaultDialect(); + ConnectionDetails defaultConnectionDetails = new ConnectionDetails(defaultDataSource, defaultDialect); + + Properties prop = new Properties(); + prop.put("javax.persistence.jdbc.driver", config.getDriverClassName()); + prop.put("javax.persistence.jdbc.url", config.getJdbcUrl()); + EntityManagerFactory emf = Persistence.createEntityManagerFactory("aggregationStore", prop); + + Map connectionDetailsMap = new HashMap<>(); + + // Add an entry for "mycon" connection which is not from hjson + connectionDetailsMap.put("mycon", defaultConnectionDetails); + // Add connection details fetched from hjson + VALIDATOR.getElideSQLDBConfig().getDbconfigs().forEach(dbConfig -> + connectionDetailsMap.put(dbConfig.getName(), + new ConnectionDetails(getDataSource(dbConfig, getDBPasswordExtractor()), + SQLDialectFactory.getDialect(dbConfig.getDialect()))) + ); + + return new AggregationDataStoreTestHarness(emf, defaultConnectionDetails, connectionDetailsMap, VALIDATOR); + } + + static DataSource getDataSource(DBConfig dbConfig, DBPasswordExtractor dbPasswordExtractor) { + return new DataSourceConfiguration() { + }.getDataSource(dbConfig, dbPasswordExtractor); + } + + static DBPasswordExtractor getDBPasswordExtractor() { + return new DBPasswordExtractor() { + @Override + public String getDBPassword(DBConfig config) { + String encrypted = (String) config.getPropertyMap().get("encrypted.password"); + byte[] decrypted = Base64.getDecoder().decode(encrypted.getBytes()); + try { + return new String(decrypted, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + }; + } + + @Test + public void testGraphQLSchema() throws IOException { + String graphQLRequest = "{" + + "__type(name: \"PlayerStatsWithViewEdge\") {" + + " name " + + " fields {" + + " name " + + " type {" + + " name" + + " fields {" + + " name " + + " type {" + + " name " + + " fields {" + + " name" + + " }" + + " }" + + " }" + + " }" + + " }" + + "}" + + "}"; + + String expected = loadGraphQLResponse("testGraphQLSchema.json"); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testGraphQLMetdata() throws Exception { + String graphQLRequest = document( + selection( + field( + "table", + arguments( + argument("ids", Arrays.asList("playerStatsView")) + ), + selections( + field("name"), + field("arguments", + selections( + field("name"), + field("type"), + field("defaultValue") + ) + + ) + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "table", + selections( + field("name", "playerStatsView"), + field("arguments", + selections( + field("name", "rating"), + field("type", "TEXT"), + field("defaultValue", "") + ), + selections( + field("name", "minScore"), + field("type", "INTEGER"), + field("defaultValue", "0") + ) + ) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void basicAggregationTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"highScore\"") + ), + selections( + field("highScore"), + field("overallRating"), + field("countryIsoCode"), + field("playerRank") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("highScore", 1000), + field("overallRating", "Good"), + field("countryIsoCode", "HKG"), + field("playerRank", 3) + ), + selections( + field("highScore", 1234), + field("overallRating", "Good"), + field("countryIsoCode", "USA"), + field("playerRank", 1) + ), + selections( + field("highScore", 2412), + field("overallRating", "Great"), + field("countryIsoCode", "USA"), + field("playerRank", 2) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void metricFormulaTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "videoGame", + arguments( + argument("sort", "\"timeSpentPerSession\"") + ), + selections( + field("timeSpent"), + field("sessions"), + field("timeSpentPerSession"), + field("playerName") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "videoGame", + selections( + field("timeSpent", 720), + field("sessions", 60), + field("timeSpentPerSession", 12.0), + field("playerName", "Jon Doe") + ), + selections( + field("timeSpent", 350), + field("sessions", 25), + field("timeSpentPerSession", 14.0), + field("playerName", "Jane Doe") + ), + selections( + field("timeSpent", 300), + field("sessions", 10), + field("timeSpentPerSession", 30.0), + field("playerName", "Han") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + + //When admin = false + + when(securityContextMock.isUserInRole("admin.user")).thenReturn(false); + + expected = document( + selections( + field( + "videoGame", + selections( + field("timeSpent", 720), + field("sessions", 60), + field("timeSpentPerSession", 12.0), + field("playerName", "Jon Doe") + ), + selections( + field("timeSpent", 350), + field("sessions", 25), + field("timeSpentPerSession", 14.0), + field("playerName", "Jane Doe") + ) + ) + ) + ).toResponse(); + runQueryWithExpectedResult(graphQLRequest, expected); + } + + /** + * Test sql expression in where, sorting, group by and projection. + * @throws Exception exception + */ + @Test + public void dimensionFormulaTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"playerLevel\""), + argument("filter", "\"playerLevel>\\\"0\\\"\"") + ), + selections( + field("highScore"), + field("playerLevel") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("highScore", 1234), + field("playerLevel", 1) + ), + selections( + field("highScore", 2412), + field("playerLevel", 2) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void noMetricQueryTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStatsWithView", + arguments( + argument("sort", "\"countryViewViewIsoCode\"") + ), + selections( + field("countryViewViewIsoCode") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStatsWithView", + selections( + field("countryViewViewIsoCode", "HKG") + ), + selections( + field("countryViewViewIsoCode", "USA") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void whereFilterTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"overallRating==\\\"Good\\\"\"") + ), + selections( + field("highScore"), + field("overallRating") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("highScore", 1234), + field("overallRating", "Good") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void havingFilterTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"lowScore<\\\"45\\\"\"") + ), + selections( + field("lowScore"), + field("overallRating"), + field("playerName") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("lowScore", 35), + field("overallRating", "Good"), + field("playerName", "Jon Doe") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + /** + * Test the case that a where clause is promoted into having clause. + * @throws Exception exception + */ + @Test + public void wherePromotionTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"overallRating==\\\"Good\\\",lowScore<\\\"45\\\"\""), + argument("sort", "\"lowScore\"") + ), + selections( + field("lowScore"), + field("overallRating"), + field("playerName") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("lowScore", 35), + field("overallRating", "Good"), + field("playerName", "Jon Doe") + ), + selections( + field("lowScore", 72), + field("overallRating", "Good"), + field("playerName", "Han") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + /** + * Test the case that a where clause, which requires dimension join, is promoted into having clause. + * @throws Exception exception + */ + @Test + public void havingClauseJoinTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"countryIsoCode==\\\"USA\\\",lowScore<\\\"45\\\"\""), + argument("sort", "\"lowScore\"") + ), + selections( + field("lowScore"), + field("countryIsoCode"), + field("playerName") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("lowScore", 35), + field("countryIsoCode", "USA"), + field("playerName", "Jon Doe") + ), + selections( + field("lowScore", 241), + field("countryIsoCode", "USA"), + field("playerName", "Jane Doe") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + /** + * Test invalid where promotion on a dimension field that is not grouped. + * @throws Exception exception + */ + @Test + public void ungroupedHavingDimensionTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"countryIsoCode==\\\"USA\\\",lowScore<\\\"45\\\"\"") + ), + selections( + field("lowScore") + ) + ) + ) + ).toQuery(); + + String errorMessage = "Exception while fetching data (/playerStats) : Invalid operation: " + + "Post aggregation filtering on 'countryIsoCode' requires the field to be projected in the response"; + + runQueryWithExpectedError(graphQLRequest, errorMessage); + } + + /** + * Test invalid having clause on a metric field that is not aggregated. + * @throws Exception exception + */ + @Test + public void nonAggregatedHavingMetricTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"highScore>\\\"0\\\"\"") + ), + selections( + field("lowScore") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("lowScore", 35) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + /** + * Test invalid where promotion on a different class than the queried class. + * @throws Exception exception + */ + @Test + public void invalidHavingClauseClassTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"country.isoCode==\\\"USA\\\",lowScore<\\\"45\\\"\"") + ), + selections( + field("lowScore") + ) + ) + ) + ).toQuery(); + + String errorMessage = "Exception while fetching data (/playerStats) : Invalid operation: " + + "Relationship traversal not supported for analytic queries."; + + runQueryWithExpectedError(graphQLRequest, errorMessage); + } + + @Test + public void dimensionSortingTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"overallRating\"") + ), + selections( + field("lowScore"), + field("overallRating") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("lowScore", 35), + field("overallRating", "Good") + ), + selections( + field("lowScore", 241), + field("overallRating", "Great") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void metricSortingTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"-highScore\"") + ), + selections( + field("highScore"), + field("countryIsoCode") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("highScore", 2412), + field("countryIsoCode", "USA") + ), + selections( + field("highScore", 1000), + field("countryIsoCode", "HKG") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void multipleColumnsSortingTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"overallRating,playerName\"") + ), + selections( + field("lowScore"), + field("overallRating"), + field("playerName") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("lowScore", 72), + field("overallRating", "Good"), + field("playerName", "Han") + ), + selections( + field("lowScore", 35), + field("overallRating", "Good"), + field("playerName", "Jon Doe") + ), + selections( + field("lowScore", 241), + field("overallRating", "Great"), + field("playerName", "Jane Doe") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void idSortingTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"id\"") + ), + selections( + field("lowScore"), + field("id") + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/playerStats) : Invalid operation: Sorting on id field is not permitted"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void nestedDimensionNotInQuerySortingTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"-countryIsoCode,lowScore\"") + ), + selections( + field("lowScore") + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/playerStats) : Invalid operation: Can not sort on countryIsoCode as it is not present in query"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void sortingOnMetricNotInQueryTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("sort", "\"highScore\"") + ), + selections( + field("lowScore"), + field("countryIsoCode") + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/playerStats) : Invalid operation: Can not sort on highScore as it is not present in query"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void basicViewAggregationTest() throws Exception { + String graphQLRequest = document( + selection( + field( + "playerStatsWithView", + arguments( + argument("sort", "\"highScore\"") + ), + selections( + field("highScore"), + field("countryViewIsoCode") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStatsWithView", + selections( + field("highScore", 1000), + field("countryViewIsoCode", "HKG") + ), + selections( + field("highScore", 2412), + field("countryViewIsoCode", "USA") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void multiTimeDimensionTest() throws IOException { + String graphQLRequest = document( + selection( + field( + "playerStats", + selections( + field("recordedDate"), + field("updatedDate") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("recordedDate", "2019-07-13"), + field("updatedDate", "2020-01-12") + ), + selections( + field("recordedDate", "2019-07-12"), + field("updatedDate", "2019-10-12") + ), + selections( + field("recordedDate", "2019-07-11"), + field("updatedDate", "2020-07-12") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testGraphqlQueryDynamicModelById() throws IOException { + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + selections( + field("id"), + field("orderTotal") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "SalesNamespace_orderDetails", + selections( + field("id", "0"), + field("orderTotal", 434.84) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void jsonApiAggregationTest() { + given() + .accept("application/vnd.api+json") + .get("/playerStats") + .then() + .statusCode(HttpStatus.SC_OK) + .body("data.id", hasItems("0", "1", "2")) + .body("data.attributes.highScore", hasItems(1000, 1234, 2412)) + .body("data.attributes.countryIsoCode", hasItems("USA", "HKG")); + } + + /** + * Below tests demonstrate using the aggregation store from dynamic configuration. + */ + @Test + public void testDynamicAggregationModel() { + String getPath = "/SalesNamespace_orderDetails?sort=customerRegion,orderTime&page[totals]&" + + "fields[SalesNamespace_orderDetails]=orderTotal,customerRegion,orderTime&filter=orderTime>=2020-08"; + given() + .when() + .get(getPath) + .then() + .statusCode(HttpStatus.SC_OK) + .body("data", hasSize(3)) + .body("data.id", hasItems("0", "1", "2")) + .body("data.attributes", hasItems( + allOf(hasEntry("customerRegion", "NewYork"), hasEntry("orderTime", "2020-08")), + allOf(hasEntry("customerRegion", "Virginia"), hasEntry("orderTime", "2020-08")), + allOf(hasEntry("customerRegion", "Virginia"), hasEntry("orderTime", "2020-09")))) + .body("data.attributes.orderTotal", hasItems(61.43F, 113.07F, 260.34F)) + .body("meta.page.number", equalTo(1)) + .body("meta.page.totalRecords", equalTo(3)) + .body("meta.page.totalPages", equalTo(1)) + .body("meta.page.limit", equalTo(500)); + } + + @Test + public void testInvalidSparseFields() { + String expectedError = "Invalid value: SalesNamespace_orderDetails does not contain the fields: [orderValue, customerState]"; + String getPath = "/SalesNamespace_orderDetails?fields[SalesNamespace_orderDetails]=orderValue,customerState,orderTime"; + given() + .when() + .get(getPath) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST) + .body("errors.detail", hasItems(expectedError)); + } + + @Test + public void missingClientFilterTest() { + String expectedError = "Querying SalesNamespace_deliveryDetails requires a mandatory filter:" + + " month>={{start}};month<{{end}}"; + when() + .get("/SalesNamespace_deliveryDetails/") + .then() + .body("errors.detail", hasItems(expectedError)) + .statusCode(HttpStatus.SC_BAD_REQUEST); + } + + @Test + public void incompleteClientFilterTest() { + String expectedError = "Querying SalesNamespace_deliveryDetails requires a mandatory filter:" + + " month>={{start}};month<{{end}}"; + when() + .get("/SalesNamespace_deliveryDetails?filter=month>=2020-08") + .then() + .body("errors.detail", hasItems(expectedError)) + .statusCode(HttpStatus.SC_BAD_REQUEST); + } + + @Test + public void completeClientFilterTest() { + when() + .get("/SalesNamespace_deliveryDetails?filter=month>=2020-08;month<2020-09") + .then() + .statusCode(HttpStatus.SC_OK); + } + + @Test + public void testGraphQLDynamicAggregationModel() throws Exception { + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"orderTime=='2020-08'\"") + ), + selections( + field("orderTotal"), + field("customerRegion"), + field("customerRegionRegion"), + field("orderTime", arguments( + argument("grain", TimeGrain.MONTH) + )) + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "SalesNamespace_orderDetails", + selections( + field("orderTotal", 61.43F), + field("customerRegion", "NewYork"), + field("customerRegionRegion", "NewYork"), + field("orderTime", "2020-08") + ), + selections( + field("orderTotal", 113.07F), + field("customerRegion", "Virginia"), + field("customerRegionRegion", "Virginia"), + field("orderTime", "2020-08") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + /** + * Tests for below type of column references. + * + * a) Physical Column Reference in same table. + * b) Logical Column Reference in same table, which references Physical column in same table. + * c) Logical Column Reference in same table, which references another Logical column in same table, which + * references Physical column in same table. + * d) Physical Column Reference in referred table. + * e) Logical Column Reference in referred table, which references Physical column in referred table. + * f) Logical Column Reference in referred table, which references another Logical column in referred table, which + * references another Logical column in referred table, which references Physical column in referred table. + * g) Logical Column Reference in same table, which references Physical column in referred table. + * h) Logical Column Reference in same table, which references another Logical Column in referred table, which + * references another Logical column in referred table, which references another Logical column in referred table, + * which references Physical column in referred table + * + * @throws Exception + */ + @Test + public void testGraphQLDynamicAggregationModelAllFields() throws Exception { + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"courierName,deliveryDate,orderTotal\""), + argument("filter", "\"deliveryDate>='2020-09-01',orderTotal>50\"") + ), + selections( + field("courierName"), + field("deliveryTime"), + field("deliveryHour"), + field("deliveryDate"), + field("deliveryMonth"), + field("deliveryYear"), + field("deliveryDefault"), + field("orderTime", "bySecond", arguments( + argument("grain", TimeGrain.SECOND) + )), + field("orderTime", "byDay", arguments( + argument("grain", TimeGrain.DAY) + )), + field("orderTime", "byMonth", arguments( + argument("grain", TimeGrain.MONTH) + )), + field("customerRegion"), + field("customerRegionRegion"), + field("orderTotal"), + field("zipCode"), + field("orderId") + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "SalesNamespace_orderDetails", + selections( + field("courierName", "FEDEX"), + field("deliveryTime", "2020-09-11T16:30:11"), + field("deliveryHour", "2020-09-11T16"), + field("deliveryDate", "2020-09-11"), + field("deliveryMonth", "2020-09"), + field("deliveryYear", "2020"), + field("bySecond", "2020-09-08T16:30:11"), + field("deliveryDefault", "2020-09-11"), + field("byDay", "2020-09-08"), + field("byMonth", "2020-09"), + field("customerRegion", "Virginia"), + field("customerRegionRegion", "Virginia"), + field("orderTotal", 84.11F), + field("zipCode", 20166), + field("orderId", "order-1b") + ), + selections( + field("courierName", "FEDEX"), + field("deliveryTime", "2020-09-11T16:30:11"), + field("deliveryHour", "2020-09-11T16"), + field("deliveryDate", "2020-09-11"), + field("deliveryMonth", "2020-09"), + field("deliveryYear", "2020"), + field("bySecond", "2020-09-08T16:30:11"), + field("deliveryDefault", "2020-09-11"), + field("byDay", "2020-09-08"), + field("byMonth", "2020-09"), + field("customerRegion", "Virginia"), + field("customerRegionRegion", "Virginia"), + field("orderTotal", 97.36F), + field("zipCode", 20166), + field("orderId", "order-1c") + ), + selections( + field("courierName", "UPS"), + field("deliveryTime", "2020-09-05T16:30:11"), + field("deliveryHour", "2020-09-05T16"), + field("deliveryDate", "2020-09-05"), + field("deliveryMonth", "2020-09"), + field("deliveryYear", "2020"), + field("bySecond", "2020-08-30T16:30:11"), + field("deliveryDefault", "2020-09-05"), + field("byDay", "2020-08-30"), + field("byMonth", "2020-08"), + field("customerRegion", "Virginia"), + field("customerRegionRegion", "Virginia"), + field("orderTotal", 103.72F), + field("zipCode", 20166), + field("orderId", "order-1a") + ), + selections( + field("courierName", "UPS"), + field("deliveryTime", "2020-09-13T16:30:11"), + field("deliveryHour", "2020-09-13T16"), + field("deliveryDate", "2020-09-13"), + field("deliveryMonth", "2020-09"), + field("deliveryYear", "2020"), + field("bySecond", "2020-09-09T16:30:11"), + field("deliveryDefault", "2020-09-13"), + field("byDay", "2020-09-09"), + field("byMonth", "2020-09"), + field("customerRegion", "Virginia"), + field("customerRegionRegion", "Virginia"), + field("orderTotal", 78.87F), + field("zipCode", 20170), + field("orderId", "order-3b") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testGraphQLDynamicAggregationModelDateTime() throws Exception { + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"bySecond=='2020-09-08T16:30:11'\"") + ), + selections( + field("orderTotal"), + field("customerRegion"), + field("orderTime", "byMonth", arguments( + argument("grain", TimeGrain.MONTH) + )), + field("orderTime", "bySecond", arguments( + argument("grain", TimeGrain.SECOND) + )) + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "SalesNamespace_orderDetails", + selections( + field("orderTotal", 181.47F), + field("customerRegion", "Virginia"), + field("byMonth", "2020-09"), + field("bySecond", "2020-09-08T16:30:11") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testTimeDimMismatchArgs() throws Exception { + + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"orderTime[grain:DAY]=='2020-08',orderTotal>50\"") + ), + selections( + field("orderTotal"), + field("customerRegion"), + field("orderTime", arguments( + argument("grain", TimeGrain.MONTH) // Does not match grain argument in filter + )) + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/SalesNamespace_orderDetails) : Invalid operation: Post aggregation filtering on 'orderTime' requires the field to be projected in the response with matching arguments"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void testTimeDimMismatchArgsWithDefaultSelect() throws Exception { + + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"orderTime[grain:DAY]=='2020-08',orderTotal>50\"") + ), + selections( + field("orderTotal"), + field("customerRegion"), + field("orderTime") //Default Grain for OrderTime is Month. + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/SalesNamespace_orderDetails) : Invalid operation: Post aggregation filtering on 'orderTime' requires the field to be projected in the response with matching arguments"; + + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void testTimeDimMismatchArgsWithDefaultFilter() throws Exception { + + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"orderTime\""), + argument("filter", "\"orderTime=='2020-08-01',orderTotal>50\"") //No Grain Arg passed, so works based on Alias's argument in Selection. + ), + selections( + field("orderTotal"), + field("customerRegion"), + field("orderTime", arguments( + argument("grain", TimeGrain.DAY) + )) + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "SalesNamespace_orderDetails", + selections( + field("orderTotal", 103.72F), + field("customerRegion", "Virginia"), + field("orderTime", "2020-08-30") + ), + selections( + field("orderTotal", 181.47F), + field("customerRegion", "Virginia"), + field("orderTime", "2020-09-08") + ), + selections( + field("orderTotal", 78.87F), + field("customerRegion", "Virginia"), + field("orderTime", "2020-09-09") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testAdminRole() throws Exception { + + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"orderTime=='2020-08'\"") + ), + selections( + field("orderTotal"), + field("customerRegion"), + field("orderTime", arguments( + argument("grain", TimeGrain.MONTH) + )) + ) + ) + ) + ).toQuery(); + + String expected = document( + selection( + field( + "SalesNamespace_orderDetails", + selections( + field("orderTotal", 61.43F), + field("customerRegion", "NewYork"), + field("orderTime", "2020-08") + ), + selections( + field("orderTotal", 113.07F), + field("customerRegion", "Virginia"), + field("orderTime", "2020-08") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testOperatorRole() throws Exception { + + when(securityContextMock.isUserInRole("admin")).thenReturn(false); + + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"orderTime=='2020-08'\"") + ), + selections( + field("customerRegion"), + field("orderTime", arguments( + argument("grain", TimeGrain.MONTH) + )) + ) + ) + ) + ).toQuery(); + + String expected = document( + selection( + field( + "SalesNamespace_orderDetails", + selections( + field("customerRegion", "NewYork"), + field("orderTime", "2020-08") + ), + selections( + field("customerRegion", "Virginia"), + field("orderTime", "2020-08") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testGuestUserRole() throws Exception { + + when(securityContextMock.isUserInRole("admin")).thenReturn(false); + when(securityContextMock.isUserInRole("operator")).thenReturn(false); + + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"orderTime=='2020-08'\"") + ), + selections( + field("customerRegion"), + field("orderTime", arguments( + argument("grain", TimeGrain.MONTH) + )) + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/SalesNamespace_orderDetails/edges[0]/node/customerRegion) : ReadPermission Denied"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void testTimeDimensionAliases() throws Exception { + + String graphQLRequest = document( + selection( + field( + "playerStats", + arguments( + argument("filter", "\"byDay>='2019-07-12'\""), + argument("sort", "\"byDay\"") + ), + selections( + field("highScore"), + field("recordedDate", "byDay", arguments( + argument("grain", TimeGrain.DAY) + )), + field("recordedDate", "byMonth", arguments( + argument("grain", TimeGrain.MONTH) + )), + field("recordedDate", "byQuarter", arguments( + argument("grain", TimeGrain.QUARTER) + )) + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "playerStats", + selections( + field("highScore", 1234), + field("byDay", "2019-07-12"), + field("byMonth", "2019-07"), + field("byQuarter", "2019-07") + ), + selections( + field("highScore", 1000), + field("byDay", "2019-07-13"), + field("byMonth", "2019-07"), + field("byQuarter", "2019-07") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testTimeDimensionArgumentsInFilter() throws Exception { + + String graphQLRequest = document( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("sort", "\"customerRegion\""), + argument("filter", "\"orderTime[grain:day]=='2020-09-08'\"") + ), + selections( + field("customerRegion"), + field("orderTotal"), + field("orderTime", arguments( + argument("grain", TimeGrain.MONTH) + )) + ) + ) + ) + ).toQuery(); + + String expected = document( + selection( + field( + "SalesNamespace_orderDetails", + selections( + field("customerRegion", "Virginia"), + field("orderTotal", 181.47F), + field("orderTime", "2020-09") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void testSchemaIntrospection() throws Exception { + String graphQLRequest = "{" + + "__schema {" + + " mutationType {" + + " name " + + " fields {" + + " name " + + " args {" + + " name" + + " defaultValue" + + " }" + + " }" + + " }" + + "}" + + "}"; + + String query = toJsonQuery(graphQLRequest, new HashMap<>()); + + given() + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(query) + .post("/graphQL") + .then() + .statusCode(HttpStatus.SC_OK) + // Verify that the SalesNamespace_orderDetails Model has an argument "denominator". + .body("data.__schema.mutationType.fields.find { it.name == 'SalesNamespace_orderDetails' }.args.name[7] ", equalTo("denominator")); + + graphQLRequest = "{" + + "__type(name: \"SalesNamespace_orderDetails\") {" + + " name" + + " fields {" + + " name " + + " args {" + + " name" + + " defaultValue" + + " }" + + " }" + + "}" + + "}"; + + query = toJsonQuery(graphQLRequest, new HashMap<>()); + + given() + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(query) + .post("/graphQL") + .then() + .statusCode(HttpStatus.SC_OK) + // Verify that the orderTotal attribute has an argument "precision". + .body("data.__type.fields.find { it.name == 'orderTotal' }.args.name[0]", equalTo("precision")); + } + + @Test + public void testDelete() throws IOException { + String graphQLRequest = mutation( + selection( + field( + "playerStats", + arguments( + argument("op", "DELETE"), + argument("ids", Arrays.asList("0")) + ), + selections( + field("id"), + field("overallRating") + ) + ) + ) + ).toGraphQLSpec(); + + String expected = "Exception while fetching data (/playerStats) : Invalid operation: Filtering by ID is not supported on playerStats"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void testUpdate() throws IOException { + + PlayerStats playerStats = new PlayerStats(); + playerStats.setId("1"); + playerStats.setHighScore(100); + + String graphQLRequest = mutation( + selection( + field( + "playerStats", + arguments( + argument("op", "UPDATE"), + argument("data", playerStats) + ), + selections( + field("id"), + field("overallRating") + ) + ) + ) + ).toGraphQLSpec(); + + String expected = "Exception while fetching data (/playerStats) : Invalid operation: Filtering by ID is not supported on playerStats"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void testUpsertWithStaticModel() throws IOException { + + PlayerStats playerStats = new PlayerStats(); + playerStats.setId("1"); + playerStats.setHighScore(100); + + String graphQLRequest = mutation( + selection( + field( + "playerStats", + arguments( + argument("op", "UPSERT"), + argument("data", playerStats) + ), + selections( + field("id"), + field("overallRating") + ) + ) + ) + ).toGraphQLSpec(); + + String expected = "Exception while fetching data (/playerStats) : Invalid operation: Filtering by ID is not supported on playerStats"; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + @Test + public void testUpsertWithDynamicModel() throws IOException { + + Map order = new HashMap<>(); + order.put("orderId", "1"); + order.put("courierName", "foo"); + + String graphQLRequest = mutation( + selection( + field( + "SalesNamespace_orderDetails", + arguments( + argument("op", "UPSERT"), + argument("data", order) + ), + selections( + field("orderId") + ) + ) + ) + ).toGraphQLSpec(); + + String expected = "Invalid operation: SalesNamespace_orderDetails is read only."; + + runQueryWithExpectedError(graphQLRequest, expected); + } + + + //Security + @Test + public void testPermissionFilters() throws IOException { + when(securityContextMock.isUserInRole("admin.user")).thenReturn(false); + + String graphQLRequest = document( + selection( + field( + "videoGame", + arguments( + argument("sort", "\"timeSpentPerSession\"") + ), + selections( + field("timeSpent"), + field("sessions"), + field("timeSpentPerSession") + ) + ) + ) + ).toQuery(); + + //Records for Jon Doe and Jane Doe will only be aggregated. + String expected = document( + selections( + field( + "videoGame", + selections( + field("timeSpent", 1070), + field("sessions", 85), + field("timeSpentPerSession", 12.588235) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + + } + + @Test + public void testFieldPermissions() throws IOException { + when(securityContextMock.isUserInRole("operator")).thenReturn(false); + String graphQLRequest = document( + selection( + field( + "videoGame", + selections( + field("timeSpent"), + field("sessions"), + field("timeSpentPerSession"), + field("timeSpentPerGame") + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/videoGame/edges[0]/node/timeSpentPerGame) : ReadPermission Denied"; + + runQueryWithExpectedError(graphQLRequest, expected); + + } +} diff --git a/elide-graphql/pom.xml b/elide-graphql/pom.xml index 6311ae3b50..7fa6c0060b 100644 --- a/elide-graphql/pom.xml +++ b/elide-graphql/pom.xml @@ -59,7 +59,7 @@ com.graphql-java graphql-java - 6.0 + 16.2 diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/Environment.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/Environment.java index cacb21a007..1dfd868ea7 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/Environment.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/Environment.java @@ -68,7 +68,7 @@ public Environment(DataFetchingEnvironment environment) { parentResource = null; } - field = environment.getFields().get(0); + field = environment.getMergedField().getFields().get(0); this.ids = Optional.ofNullable((List) args.get(ModelBuilder.ARGUMENT_IDS)); diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLConversionUtils.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLConversionUtils.java index 5e340f9a1f..406b10d2a7 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLConversionUtils.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLConversionUtils.java @@ -15,7 +15,6 @@ import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.utils.coerce.CoerceUtil; import com.yahoo.elide.utils.coerce.converters.ElideTypeConverter; -import com.yahoo.elide.utils.coerce.converters.Serde; import graphql.Scalars; import graphql.schema.DataFetcher; @@ -35,6 +34,7 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * Contains methods that convert from a class to a GraphQL input or query type. @@ -47,6 +47,7 @@ public class GraphQLConversionUtils { protected static final String ERROR_MESSAGE = "Value should either be integer, String or float"; private final Map, GraphQLScalarType> scalarMap = new HashMap<>(); + private Set objectTypes; protected NonEntityDictionary nonEntityDictionary; protected EntityDictionary entityDictionary; @@ -55,24 +56,33 @@ public class GraphQLConversionUtils { private final Map inputConversions = new HashMap<>(); private final Map enumConversions = new HashMap<>(); private final Map mapConversions = new HashMap<>(); + private final GraphQLNameUtils nameUtils; - public GraphQLConversionUtils(EntityDictionary entityDictionary, NonEntityDictionary nonEntityDictionary) { + public GraphQLConversionUtils( + EntityDictionary entityDictionary, + NonEntityDictionary nonEntityDictionary, + Set objectTypes + ) { this.entityDictionary = entityDictionary; this.nonEntityDictionary = nonEntityDictionary; + this.nameUtils = new GraphQLNameUtils(entityDictionary); + this.objectTypes = objectTypes; registerCustomScalars(); } private void registerCustomScalars() { - for (Class serdeType : CoerceUtil.getSerdes().keySet()) { - Serde serde = CoerceUtil.lookup(serdeType); - ElideTypeConverter elideTypeConverter = serde.getClass() - .getAnnotation(ElideTypeConverter.class); - if (elideTypeConverter != null) { - SerdeCoercing serdeCoercing = new SerdeCoercing(ERROR_MESSAGE, serde); - scalarMap.put(elideTypeConverter.type(), new GraphQLScalarType(elideTypeConverter.name(), - elideTypeConverter.description(), serdeCoercing)); - } - } + CoerceUtil.getSerdes().forEach((type, serde) -> { + SerdeCoercing serdeCoercing = new SerdeCoercing<>(ERROR_MESSAGE, serde); + ElideTypeConverter elideTypeConverter = serde.getClass().getAnnotation(ElideTypeConverter.class); + String name = elideTypeConverter != null ? elideTypeConverter.name() : type.getSimpleName(); + String description = elideTypeConverter != null ? elideTypeConverter.description() : type.getSimpleName(); + scalarMap.put(type, GraphQLScalarType.newScalar() + .name(name) + .description(description) + .coercing(serdeCoercing) + .build()); + + }); } /** @@ -93,7 +103,7 @@ public GraphQLScalarType classToScalarType(Class clazz) { return Scalars.GraphQLFloat; } else if (clazz.equals(short.class) || clazz.equals(Short.class)) { return Scalars.GraphQLShort; - } else if (clazz.equals(String.class)) { + } else if (clazz.equals(String.class) || clazz.equals(Object.class)) { return Scalars.GraphQLString; } else if (clazz.equals(BigDecimal.class)) { return Scalars.GraphQLBigDecimal; @@ -148,20 +158,19 @@ public GraphQLList classToQueryMap(Class keyClazz, Class valueClazz, DataF GraphQLOutputType keyType = fetchScalarOrObjectOutput(keyClazz, fetcher); GraphQLOutputType valueType = fetchScalarOrObjectOutput(valueClazz, fetcher); - GraphQLList outputMap = new GraphQLList( - newObject() + GraphQLObjectType mapType = newObject() .name(mapName) .field(newFieldDefinition() .name(KEY) - .dataFetcher(fetcher) .type(keyType)) .field(newFieldDefinition() .name(VALUE) - .dataFetcher(fetcher) .type(valueType)) - .build() - ); + .build(); + GraphQLList outputMap = new GraphQLList(mapType); + + objectTypes.add(mapType); mapConversions.put(mapName, outputMap); return mapConversions.get(mapName); @@ -268,7 +277,6 @@ protected GraphQLOutputType attributeToQueryObject(Class parentClass, // If this is a collection of a boxed type scalar, we want to unwrap it properly return new GraphQLList(fetchScalarOrObjectOutput(listType, fetcher)); - } return fetchScalarOrObjectOutput(attributeClass, fetcher); } @@ -378,8 +386,7 @@ public GraphQLObjectType classToQueryObject( Class attributeClass = nonEntityDictionary.getType(clazz, attribute); GraphQLFieldDefinition.Builder fieldBuilder = newFieldDefinition() - .name(attribute) - .dataFetcher(fetcher); + .name(attribute); GraphQLOutputType attributeType = attributeToQueryObject(clazz, @@ -394,7 +401,7 @@ public GraphQLObjectType classToQueryObject( } GraphQLObjectType object = objectBuilder.build(); - + objectTypes.add(object); outputConversions.put(clazz, object); return object; diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLErrorSerializer.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLErrorSerializer.java index cfdc4f40e9..a14131d893 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLErrorSerializer.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLErrorSerializer.java @@ -56,6 +56,7 @@ public void serialize(GraphQLError value, JsonGenerator gen, SerializerProvider } if (errorSpec.containsKey("extensions")) { + gen.writeFieldName("extensions"); gen.writeObject(errorSpec.get("extensions")); } diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLNameUtils.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLNameUtils.java new file mode 100644 index 0000000000..5445931098 --- /dev/null +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLNameUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.graphql; + + +import com.yahoo.elide.core.EntityDictionary; + +import org.apache.commons.lang3.StringUtils; + +public class GraphQLNameUtils { + private static final String INPUT_SUFFIX = "Input"; + private static final String EDGE_SUFFIX = "Edge"; + + private final EntityDictionary dictionary; + + public GraphQLNameUtils(EntityDictionary dictionary) { + this.dictionary = dictionary; + } + + public String toOutputTypeName(Class clazz) { + if (dictionary.hasBinding(clazz)) { + return StringUtils.capitalize(dictionary.getJsonAliasFor(clazz)); + } + return clazz.getSimpleName(); + } + + public String toInputTypeName(Class clazz) { + return toOutputTypeName(clazz) + INPUT_SUFFIX; + } + + public String toEdgesName(Class clazz) { + return toOutputTypeName(clazz) + EDGE_SUFFIX; + } +} diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLScalars.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLScalars.java index 5310f211d3..c772b330df 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLScalars.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLScalars.java @@ -27,10 +27,10 @@ public class GraphQLScalars { // TODO: Should we make this a class that can be configured? Should determine if there are other customizeable // TODO: scalar types. // NOTE: Non-final so it's overrideable if someone wants _different_ date representations. - public static GraphQLScalarType GRAPHQL_DATE_TYPE = new GraphQLScalarType( - "Date", - "Built-in date", - new Coercing() { + public static GraphQLScalarType GRAPHQL_DATE_TYPE = GraphQLScalarType.newScalar() + .name("Date") + .description("Built-in date") + .coercing(new Coercing() { @Override public Object serialize(Object o) { Serde dateSerde = CoerceUtil.lookup(Date.class); @@ -57,13 +57,13 @@ public Date parseLiteral(Object o) { } return parseValue(input); } - } - ); + }) + .build(); - public static GraphQLScalarType GRAPHQL_DEFERRED_ID = new GraphQLScalarType( - "DeferredID", - "custom id type", - new Coercing() { + public static GraphQLScalarType GRAPHQL_DEFERRED_ID = GraphQLScalarType.newScalar() + .name("DeferredID") + .description("custom id type") + .coercing(new Coercing() { @Override public Object serialize(Object o) { return o; @@ -85,6 +85,6 @@ public String parseLiteral(Object o) { log.debug("Found unexpected object type: {}", o.getClass()); return o.toString(); } - } - ); + }) + .build(); } diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/KeyWord.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/KeyWord.java new file mode 100644 index 0000000000..890580f576 --- /dev/null +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/KeyWord.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.graphql; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.function.Function; + +/** + * Key words used in graphql parsing. + */ +public enum KeyWord { + NODE("node"), + EDGES("edges"), + PAGE_INFO("pageInfo"), + PAGE_INFO_HAS_NEXT_PAGE("hasNextPage"), + PAGE_INFO_START_CURSOR("startCursor"), + PAGE_INFO_END_CURSOR("endCursor"), + PAGE_INFO_TOTAL_RECORDS("totalRecords"), + TYPENAME("__typename"), + SCHEMA("__schema"), + TYPE("__type"), + UNKNOWN("unknown"); + + private static final ImmutableMap NAME_MAP = + Arrays.stream(values()).collect(ImmutableMap.toImmutableMap(KeyWord::getName, Function.identity())); + + @Getter + private final String name; + + KeyWord(String name) { + this.name = name; + } + + public boolean hasName(String name) { + return this.name.equals(name); + } + + public static KeyWord byName(String value) { + return NAME_MAP.getOrDefault(value, UNKNOWN); + } +} diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/ModelBuilder.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/ModelBuilder.java index c44915b1a2..9e5a79a415 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/ModelBuilder.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/ModelBuilder.java @@ -18,7 +18,10 @@ import graphql.Scalars; import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; import graphql.schema.GraphQLList; @@ -58,14 +61,14 @@ public class ModelBuilder { private GraphQLArgument pageFirstArgument; private GraphQLArgument sortArgument; private GraphQLConversionUtils generator; + private GraphQLNameUtils nameUtils; private GraphQLObjectType pageInfoObject; - private Map, MutableGraphQLInputObjectType> inputObjectRegistry; + private Map, GraphQLInputObjectType> inputObjectRegistry; private Map, GraphQLObjectType> queryObjectRegistry; private Map, GraphQLObjectType> connectionObjectRegistry; private Set> excludedEntities; - - private HashMap convertedInputs = new HashMap<>(); + private Set objectTypes; /** * Class constructor, constructs the custom arguments to handle mutations @@ -76,8 +79,10 @@ public class ModelBuilder { public ModelBuilder(EntityDictionary entityDictionary, NonEntityDictionary nonEntityDictionary, DataFetcher dataFetcher) { - this.generator = new GraphQLConversionUtils(entityDictionary, nonEntityDictionary); + objectTypes = new HashSet<>(); + this.generator = new GraphQLConversionUtils(entityDictionary, nonEntityDictionary, objectTypes); this.entityDictionary = entityDictionary; + this.nameUtils = new GraphQLNameUtils(entityDictionary); this.dataFetcher = dataFetcher; relationshipOpArg = newArgument() @@ -115,22 +120,20 @@ public ModelBuilder(EntityDictionary entityDictionary, .name("_pageInfoObject") .field(newFieldDefinition() .name("hasNextPage") - .dataFetcher(dataFetcher) .type(Scalars.GraphQLBoolean)) .field(newFieldDefinition() .name("startCursor") - .dataFetcher(dataFetcher) .type(Scalars.GraphQLString)) .field(newFieldDefinition() .name("endCursor") - .dataFetcher(dataFetcher) .type(Scalars.GraphQLString)) .field(newFieldDefinition() .name("totalRecords") - .dataFetcher(dataFetcher) .type(Scalars.GraphQLLong)) .build(); + objectTypes.add(pageInfoObject); + inputObjectRegistry = new HashMap<>(); queryObjectRegistry = new HashMap<>(); connectionObjectRegistry = new HashMap<>(); @@ -158,7 +161,6 @@ public GraphQLSchema build() { * Walk the object graph (avoiding cycles) and construct the GraphQL input object types. */ entityDictionary.walkEntityGraph(rootClasses, this::buildInputObjectStub); - resolveInputObjectRelationships(); /* Construct root object */ GraphQLObjectType.Builder root = newObject().name("_root"); @@ -166,7 +168,6 @@ public GraphQLSchema build() { String entityName = entityDictionary.getJsonAliasFor(clazz); root.field(newFieldDefinition() .name(entityName) - .dataFetcher(dataFetcher) .argument(relationshipOpArg) .argument(idArgument) .argument(filterArgument) @@ -177,22 +178,38 @@ public GraphQLSchema build() { .type(buildConnectionObject(clazz))); } + GraphQLObjectType queryRoot = root.build(); GraphQLObjectType mutationRoot = root.name("_mutation_root").build(); + objectTypes.add(queryRoot); + objectTypes.add(mutationRoot); + /* * Walk the object graph (avoiding cycles) and construct the GraphQL output object types. */ entityDictionary.walkEntityGraph(rootClasses, this::buildConnectionObject); + GraphQLCodeRegistry.Builder codeRegistry = GraphQLCodeRegistry.newCodeRegistry(); + + for (GraphQLObjectType objectType : objectTypes) { + String objectName = objectType.getName(); + for (GraphQLFieldDefinition fieldDefinition : objectType.getFieldDefinitions()) { + codeRegistry.dataFetcher( + FieldCoordinates.coordinates(objectName, fieldDefinition.getName()), + dataFetcher); + } + } + /* Construct the schema */ GraphQLSchema schema = GraphQLSchema.newSchema() .query(queryRoot) .mutation(mutationRoot) - .build(new HashSet<>(CollectionUtils.union( - connectionObjectRegistry.values(), - inputObjectRegistry.values() - ))); + .codeRegistry(codeRegistry.build()) + .additionalTypes(new HashSet<>(CollectionUtils.union( + connectionObjectRegistry.values(), + inputObjectRegistry.values()))) + .build(); return schema; } @@ -214,14 +231,14 @@ private GraphQLObjectType buildConnectionObject(Class entityClass) { .name(entityName) .field(newFieldDefinition() .name("edges") - .dataFetcher(dataFetcher) - .type(buildEdgesObject(entityName, buildQueryObject(entityClass)))) + .type(buildEdgesObject(entityClass, buildQueryObject(entityClass)))) .field(newFieldDefinition() .name("pageInfo") - .dataFetcher(dataFetcher) .type(pageInfoObject)) .build(); + objectTypes.add(connectionObject); + connectionObjectRegistry.put(entityClass, connectionObject); return connectionObject; @@ -249,7 +266,6 @@ private GraphQLObjectType buildQueryObject(Class 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 : entityDictionary.getAttributes(entityClass)) { @@ -272,7 +288,6 @@ private GraphQLObjectType buildQueryObject(Class entityClass) { builder.field(newFieldDefinition() .name(attribute) - .dataFetcher(dataFetcher) .type((GraphQLOutputType) attributeType) ); } @@ -289,7 +304,6 @@ private GraphQLObjectType buildQueryObject(Class entityClass) { if (type.isToOne()) { builder.field(newFieldDefinition() .name(relationship) - .dataFetcher(dataFetcher) .argument(relationshipOpArg) .argument(buildInputObjectArgument(relationshipClass, false)) .type(new GraphQLTypeReference(relationshipEntityName)) @@ -297,7 +311,6 @@ private GraphQLObjectType buildQueryObject(Class entityClass) { } else { builder.field(newFieldDefinition() .name(relationship) - .dataFetcher(dataFetcher) .argument(relationshipOpArg) .argument(filterArgument) .argument(sortArgument) @@ -311,18 +324,24 @@ private GraphQLObjectType buildQueryObject(Class entityClass) { } GraphQLObjectType queryObject = builder.build(); + + objectTypes.add(queryObject); + queryObjectRegistry.put(entityClass, queryObject); return queryObject; } - private GraphQLList buildEdgesObject(String relationName, GraphQLOutputType entityType) { - return new GraphQLList(newObject() - .name("_edges__" + relationName) + private GraphQLList buildEdgesObject(Class relationClass, GraphQLOutputType entityType) { + GraphQLObjectType edgesObject = newObject() + .name(nameUtils.toEdgesName(relationClass)) .field(newFieldDefinition() .name("node") - .dataFetcher(dataFetcher) .type(entityType)) - .build()); + .build(); + + objectTypes.add(edgesObject); + + return new GraphQLList(edgesObject); } /** @@ -354,10 +373,8 @@ private GraphQLArgument buildInputObjectArgument(Class entityClass, boolean a private GraphQLInputType buildInputObjectStub(Class clazz) { log.debug("Building input object for {}", clazz.getName()); - String entityName = entityDictionary.getJsonAliasFor(clazz); - - MutableGraphQLInputObjectType.Builder builder = MutableGraphQLInputObjectType.newMutableInputObject(); - builder.name(entityName + ARGUMENT_INPUT); + GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject(); + builder.name(nameUtils.toInputTypeName(clazz)); String id = entityDictionary.getIdFieldName(clazz); builder.field(newInputObjectField() @@ -378,62 +395,36 @@ private GraphQLInputType buildInputObjectStub(Class clazz) { GraphQLInputType attributeType = generator.attributeToInputObject(clazz, attributeClass, attribute); - /* If the attribute is an object, we need to change its name so it doesn't conflict with query objects */ - if (attributeType instanceof GraphQLInputObjectType) { - String objectName = attributeType.getName() + ARGUMENT_INPUT; - if (!convertedInputs.containsKey(objectName)) { - MutableGraphQLInputObjectType wrappedType = - new MutableGraphQLInputObjectType( - objectName, - ((GraphQLInputObjectType) attributeType).getDescription(), - ((GraphQLInputObjectType) attributeType).getFields() - ); - convertedInputs.put(objectName, wrappedType); - attributeType = wrappedType; - } else { - attributeType = convertedInputs.get(objectName); - } - } - builder.field(newInputObjectField() .name(attribute) .type(attributeType) ); } + for (String relationship : entityDictionary.getElideBoundRelationships(clazz)) { + log.debug("Resolving relationship {} for {}", relationship, clazz.getName()); + Class relationshipClass = entityDictionary.getParameterizedType(clazz, relationship); + if (excludedEntities.contains(relationshipClass)) { + continue; + } - MutableGraphQLInputObjectType constructed = builder.build(); - inputObjectRegistry.put(clazz, constructed); - return constructed; - } + RelationshipType type = entityDictionary.getRelationshipType(clazz, relationship); + String relationshipEntityName = nameUtils.toInputTypeName(relationshipClass); - /** - * Constructs relationship links for input objects. - */ - private void resolveInputObjectRelationships() { - inputObjectRegistry.forEach((clazz, inputObj) -> { - for (String relationship : entityDictionary.getElideBoundRelationships(clazz)) { - log.debug("Resolving relationship {} for {}", relationship, clazz.getName()); - Class relationshipClass = entityDictionary.getParameterizedType(clazz, relationship); - if (excludedEntities.contains(relationshipClass)) { - continue; - } - - RelationshipType type = entityDictionary.getRelationshipType(clazz, relationship); - - if (type.isToOne()) { - inputObj.setField(relationship, newInputObjectField() + if (type.isToOne()) { + builder.field(newInputObjectField() .name(relationship) - .type(inputObjectRegistry.get(relationshipClass)) - .build() - ); - } else { - inputObj.setField(relationship, newInputObjectField() + .type(new GraphQLTypeReference(relationshipEntityName)) + .build()); + } else { + builder.field(newInputObjectField() .name(relationship) - .type(new GraphQLList(inputObjectRegistry.get(relationshipClass))) - .build() - ); - } + .type(new GraphQLList(new GraphQLTypeReference(relationshipEntityName))) + .build()); } - }); + } + + GraphQLInputObjectType constructed = builder.build(); + inputObjectRegistry.put(clazz, constructed); + return constructed; } } diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/MutableGraphQLInputObjectType.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/MutableGraphQLInputObjectType.java deleted file mode 100644 index 7fab7740d1..0000000000 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/MutableGraphQLInputObjectType.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2017, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ - -package com.yahoo.elide.graphql; - -import static graphql.Assert.assertNotNull; - -import graphql.AssertException; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; - -import java.util.ArrayList; -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 - * object is constructed. This mutable behavior is useful for constructing input objects from - * object graphs with cycles. - * - * Portions of this file are taken from GraphQL-Java which has the following license: - * - * The MIT License (MIT) - * - * Copyright (c) 2015 Andreas Marek and Contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - * associated documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - * of the Software, and to permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT - * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -public class MutableGraphQLInputObjectType extends GraphQLInputObjectType { - - private final Map fieldMap = new LinkedHashMap(); - private final String name; - - public MutableGraphQLInputObjectType(String name, String description, List fields) { - super(name, description, fields); - this.name = name; - buildMap(fields); - } - - @Override - public String getName() { - return name; - } - - private void buildMap(List fields) { - for (GraphQLInputObjectField field : fields) { - String name = field.getName(); - if (fieldMap.containsKey(name)) { - throw new AssertException("field " + name + " redefined"); - } - fieldMap.put(name, field); - } - } - - public void setField(String name, GraphQLInputObjectField field) { - fieldMap.put(name, field); - } - - @Override - public List getFields() { - return new ArrayList(fieldMap.values()); - } - - public GraphQLInputObjectField getField(String name) { - return fieldMap.get(name); - } - - public static Builder newMutableInputObject() { - return new Builder(); - } - - /** - * Builder for constructing MutableGraphQLInputObjectType - */ - public static class Builder { - private String name; - private String description; - private List fields = new ArrayList(); - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder description(String description) { - this.description = description; - return this; - } - - public Builder field(GraphQLInputObjectField field) { - assertNotNull(field, "field can't be null"); - fields.add(field); - return this; - } - - /** - * Same effect as the field(GraphQLFieldDefinition). Builder.build() is called - * from within - * - * @param builder an un-built/incomplete GraphQLFieldDefinition - * @return this - */ - public Builder field(GraphQLInputObjectField.Builder builder) { - this.fields.add(builder.build()); - return this; - } - - public Builder fields(List fields) { - for (GraphQLInputObjectField field : fields) { - field(field); - } - return this; - } - - 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); - } -} diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/PersistentResourceFetcher.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/PersistentResourceFetcher.java index f82a6fec83..aca4992ee4 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/PersistentResourceFetcher.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/PersistentResourceFetcher.java @@ -30,6 +30,7 @@ import graphql.language.FragmentSpread; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLNamedType; import graphql.schema.GraphQLType; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -154,8 +155,11 @@ private void logContext(RelationshipOp operation, Environment environment) { GraphQLType parent = environment.parentType; if (log.isDebugEnabled()) { - log.debug("{} {} fields with parent {}<{}>", - operation, requestedFields, EntityDictionary.getSimpleName(parent.getClass()), parent.getName()); + String typeName = (parent instanceof GraphQLNamedType) + ? ((GraphQLNamedType) parent).getName() + : parent.toString(); + log.debug("{} {} fields with parent {}<{}>", operation, requestedFields, + EntityDictionary.getSimpleName(parent.getClass()), typeName); } } diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/QueryRunner.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/QueryRunner.java index 6b9a0a28dd..7dfec4dfd2 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/QueryRunner.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/QueryRunner.java @@ -32,6 +32,7 @@ import graphql.ExecutionResult; import graphql.GraphQL; import graphql.GraphQLError; +import graphql.execution.AsyncSerialExecutionStrategy; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @@ -69,7 +70,9 @@ public QueryRunner(Elide elide) { nonEntityDictionary); ModelBuilder builder = new ModelBuilder(elide.getElideSettings().getDictionary(), nonEntityDictionary, fetcher); - this.api = new GraphQL(builder.build()); + api = GraphQL.newGraphQL(builder.build()) + .queryExecutionStrategy(new AsyncSerialExecutionStrategy()) + .build(); // TODO - add serializers to allow for custom handling of ExecutionResult and GraphQLError objects GraphQLErrorSerializer errorSerializer = diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/FetcherFetchTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/FetcherFetchTest.java index 009b565c72..85acc64999 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/FetcherFetchTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/FetcherFetchTest.java @@ -193,6 +193,7 @@ public void testTypeIntrospection() throws Exception { + " name" + " fields {" + " name" + + " type { name }" + " }" + "}" + "}"; diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLConversionUtilsTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLConversionUtilsTest.java index 1201d4dbf4..cd8d1e242f 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLConversionUtilsTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLConversionUtilsTest.java @@ -18,6 +18,7 @@ import java.time.OffsetDateTime; import java.util.HashMap; +import java.util.HashSet; public class GraphQLConversionUtilsTest { @@ -25,10 +26,10 @@ public class GraphQLConversionUtilsTest { public void testGraphQLConversionUtilsClassToScalarType() { CoerceUtil.register(OffsetDateTime.class, new OffsetDateTimeSerde()); GraphQLConversionUtils graphQLConversionUtils = - new GraphQLConversionUtils(new EntityDictionary(new HashMap<>()), new NonEntityDictionary()); - GraphQLScalarType type = graphQLConversionUtils.classToScalarType(OffsetDateTime.class); - assertNotNull(type); + new GraphQLConversionUtils(new EntityDictionary(new HashMap<>()), new NonEntityDictionary(), new HashSet<>()); + GraphQLScalarType offsetDateTimeType = graphQLConversionUtils.classToScalarType(OffsetDateTime.class); + assertNotNull(offsetDateTimeType); String expected = "OffsetDateTime"; - assertEquals(expected, type.getName()); + assertEquals(expected, offsetDateTimeType.getName()); } } diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLEndpointTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLEndpointTest.java index 5484de0dec..9f54bc7f47 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLEndpointTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/GraphQLEndpointTest.java @@ -840,7 +840,7 @@ private static JsonNode extract200Response(Response response) throws IOException } private static String extract200ResponseString(Response response) { - assertEquals(response.getStatus(), 200); + assertEquals(200, response.getStatus()); return (String) response.getEntity(); } diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java index 7d847235ef..bbeea07ff4 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java @@ -7,6 +7,7 @@ package com.yahoo.elide.graphql; import static graphql.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -147,23 +148,23 @@ public void testBuild() { GraphQLObjectType bookType = getConnectedType((GraphQLObjectType) schema.getType(BOOK), null); GraphQLObjectType authorType = getConnectedType((GraphQLObjectType) schema.getType(AUTHOR), null); - assertTrue(bookType.getFieldDefinition(TITLE).getType().equals(Scalars.GraphQLString)); - assertTrue(bookType.getFieldDefinition(GENRE).getType().equals(Scalars.GraphQLString)); - assertTrue(bookType.getFieldDefinition(LANGUAGE).getType().equals(Scalars.GraphQLString)); - assertTrue(bookType.getFieldDefinition(PUBLISH_DATE).getType().equals(Scalars.GraphQLLong)); - assertTrue(bookType.getFieldDefinition(WEIGHT_LBS).getType().equals(Scalars.GraphQLBigDecimal)); + assertEquals(Scalars.GraphQLString, bookType.getFieldDefinition(TITLE).getType()); + assertEquals(Scalars.GraphQLString, bookType.getFieldDefinition(GENRE).getType()); + assertEquals(Scalars.GraphQLString, bookType.getFieldDefinition(LANGUAGE).getType()); + assertEquals(Scalars.GraphQLLong, bookType.getFieldDefinition(PUBLISH_DATE).getType()); + assertEquals(Scalars.GraphQLBigDecimal, bookType.getFieldDefinition(WEIGHT_LBS).getType()); GraphQLObjectType addressType = (GraphQLObjectType) authorType.getFieldDefinition("homeAddress").getType(); - assertTrue(addressType.getFieldDefinition("street1").getType().equals(Scalars.GraphQLString)); - assertTrue(addressType.getFieldDefinition("street2").getType().equals(Scalars.GraphQLString)); + assertEquals(Scalars.GraphQLString, addressType.getFieldDefinition("street1").getType()); + assertEquals(Scalars.GraphQLString, addressType.getFieldDefinition("street2").getType()); GraphQLObjectType authorsType = (GraphQLObjectType) bookType.getFieldDefinition(AUTHORS).getType(); GraphQLObjectType authorsNodeType = getConnectedType(authorsType, null); - assertTrue(authorsNodeType.equals(authorType)); + assertEquals(authorType, authorsNodeType); - assertTrue(authorType.getFieldDefinition(NAME).getType().equals(Scalars.GraphQLString)); + assertEquals(Scalars.GraphQLString, authorType.getFieldDefinition(NAME).getType()); assertTrue(validateEnum(Author.AuthorType.class, (GraphQLEnumType) authorType.getFieldDefinition(TYPE).getType())); @@ -175,18 +176,18 @@ public void testBuild() { GraphQLInputObjectType bookInputType = (GraphQLInputObjectType) schema.getType(BOOK_INPUT); GraphQLInputObjectType authorInputType = (GraphQLInputObjectType) schema.getType(AUTHOR_INPUT); - assertTrue(bookInputType.getField(TITLE).getType().equals(Scalars.GraphQLString)); - assertTrue(bookInputType.getField(GENRE).getType().equals(Scalars.GraphQLString)); - assertTrue(bookInputType.getField(LANGUAGE).getType().equals(Scalars.GraphQLString)); - assertTrue(bookInputType.getField(PUBLISH_DATE).getType().equals(Scalars.GraphQLLong)); + assertEquals(Scalars.GraphQLString, bookInputType.getField(TITLE).getType()); + assertEquals(Scalars.GraphQLString, bookInputType.getField(GENRE).getType()); + assertEquals(Scalars.GraphQLString, bookInputType.getField(LANGUAGE).getType()); + assertEquals(Scalars.GraphQLLong, bookInputType.getField(PUBLISH_DATE).getType()); GraphQLList authorsInputType = (GraphQLList) bookInputType.getField(AUTHORS).getType(); - assertTrue(authorsInputType.getWrappedType().equals(authorInputType)); + assertEquals(authorInputType, authorsInputType.getWrappedType()); - assertTrue(authorInputType.getField(NAME).getType().equals(Scalars.GraphQLString)); + assertEquals(Scalars.GraphQLString, authorInputType.getField(NAME).getType()); GraphQLList booksInputType = (GraphQLList) authorInputType.getField(BOOKS).getType(); - assertTrue(booksInputType.getWrappedType().equals(bookInputType)); + assertEquals(bookInputType, booksInputType.getWrappedType()); } private GraphQLObjectType getConnectedType(GraphQLObjectType root, String connectionName) { diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java index fc745bd925..8d3379788e 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java @@ -31,12 +31,16 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.GraphQLError; +import graphql.execution.AsyncSerialExecutionStrategy; import java.io.IOException; import java.io.InputStream; @@ -93,7 +97,9 @@ public PersistentResourceFetcherTest() { ModelBuilder builder = new ModelBuilder(dictionary, nonEntityDictionary, new PersistentResourceFetcher(settings, nonEntityDictionary)); - api = new GraphQL(builder.build()); + api = GraphQL.newGraphQL(builder.build()) + .queryExecutionStrategy(new AsyncSerialExecutionStrategy()) + .build(); initTestData(); } @@ -192,7 +198,13 @@ protected void assertQueryEquals(String graphQLRequest, String expectedResponse, DataStoreTransaction tx = inMemoryDataStore.beginTransaction(); RequestScope requestScope = new GraphQLRequestScope(baseUrl, tx, null, settings); - ExecutionResult result = api.execute(graphQLRequest, requestScope, variables); + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(graphQLRequest) + .context(requestScope) + .variables(variables) + .build(); + + ExecutionResult result = api.execute(executionInput); // NOTE: We're forcing commit even in case of failures. GraphQLEndpoint tests should ensure we do not commit on // failure. if (isMutation) { @@ -201,10 +213,12 @@ protected void assertQueryEquals(String graphQLRequest, String expectedResponse, requestScope.getTransaction().commit(requestScope); assertEquals(0, result.getErrors().size(), "Errors [" + errorsToString(result.getErrors()) + "]:"); try { + LOG.info(expectedResponse); LOG.info(mapper.writeValueAsString(result.getData())); - assertEquals( - mapper.readTree(expectedResponse), - mapper.readTree(mapper.writeValueAsString(result.getData())) + JSONAssert.assertEquals( + expectedResponse, + mapper.writeValueAsString(result.getData()), + JSONCompareMode.STRICT ); } catch (JsonProcessingException e) { fail("JSON parsing exception", e); @@ -217,7 +231,12 @@ protected void assertQueryFailsWith(String graphQLRequest, String expectedMessag DataStoreTransaction tx = inMemoryDataStore.beginTransaction(); RequestScope requestScope = new GraphQLRequestScope(baseUrl, tx, null, settings); - ExecutionResult result = api.execute(graphQLRequest, requestScope); + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(graphQLRequest) + .context(requestScope) + .build(); + + ExecutionResult result = api.execute(executionInput); if (isMutation) { requestScope.saveOrCreateObjects(); } diff --git a/elide-graphql/src/test/java/example/Author.java b/elide-graphql/src/test/java/example/Author.java index 36f694b2c5..498207f6fc 100644 --- a/elide-graphql/src/test/java/example/Author.java +++ b/elide-graphql/src/test/java/example/Author.java @@ -65,6 +65,7 @@ public enum AuthorType { private Set publicationFormats = new HashSet<>(); private Map publishedBookFormats = new HashMap<>(); private Map favoriteBookFormats = new HashMap<>(); + private Object obj = "foo"; private Map booksPublishedByFormat = new HashMap<>(); public String getName() { @@ -75,6 +76,13 @@ public void setName(String name) { this.name = name; } + public void setObj(Object obj) { + this.obj = obj; + } + + public Object getObj() { + return obj; + } public AuthorType getType() { return type; } diff --git a/elide-graphql/src/test/resources/graphql/requests/fetch/rootSingle.graphql b/elide-graphql/src/test/resources/graphql/requests/fetch/rootSingle.graphql index a83465d76e..4618eaf60e 100644 --- a/elide-graphql/src/test/resources/graphql/requests/fetch/rootSingle.graphql +++ b/elide-graphql/src/test/resources/graphql/requests/fetch/rootSingle.graphql @@ -9,6 +9,7 @@ node { id name + obj } } } diff --git a/elide-graphql/src/test/resources/graphql/requests/upsert/rootSingleWithId.graphql b/elide-graphql/src/test/resources/graphql/requests/upsert/rootSingleWithId.graphql index 72ccf62032..6f4ad5e235 100644 --- a/elide-graphql/src/test/resources/graphql/requests/upsert/rootSingleWithId.graphql +++ b/elide-graphql/src/test/resources/graphql/requests/upsert/rootSingleWithId.graphql @@ -1,9 +1,10 @@ mutation { - author(op:UPSERT, data: {id: "1", name: "abc" }) { + author(op:UPSERT, data: {id: "1", name: "abc", obj: "bar" }) { edges { node { id name + obj } } } diff --git a/elide-graphql/src/test/resources/graphql/responses/fetch/rootSingle.json b/elide-graphql/src/test/resources/graphql/responses/fetch/rootSingle.json index 7138cfd022..7f191dd0bd 100644 --- a/elide-graphql/src/test/resources/graphql/responses/fetch/rootSingle.json +++ b/elide-graphql/src/test/resources/graphql/responses/fetch/rootSingle.json @@ -10,7 +10,8 @@ { "node": { "id": "1", - "name": "Mark Twain" + "name": "Mark Twain", + "obj": "foo" } } ] diff --git a/elide-graphql/src/test/resources/graphql/responses/upsert/rootSingleWithId.json b/elide-graphql/src/test/resources/graphql/responses/upsert/rootSingleWithId.json index 280d4d913b..21c6bde68f 100644 --- a/elide-graphql/src/test/resources/graphql/responses/upsert/rootSingleWithId.json +++ b/elide-graphql/src/test/resources/graphql/responses/upsert/rootSingleWithId.json @@ -4,7 +4,8 @@ { "node": { "id": "1", - "name": "abc" + "name": "abc", + "obj" : "bar" } } ] diff --git a/elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java b/elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java index 99020849c1..5b9cdb1bd3 100644 --- a/elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java +++ b/elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java @@ -281,7 +281,8 @@ public void testInvalidFetch() throws IOException { String expected = "{\"data\":{\"book\":null},\"errors\":[{\"message\":\"Exception while fetching data " + "(/book) : FETCH must not include data\"," - + "\"locations\":[{\"line\":1,\"column\":2}],\"path\":[\"book\"]}]}"; + + "\"locations\":[{\"line\":1,\"column\":2}],\"path\":[\"book\"]," + + "\"extensions\":{\"classification\":\"DataFetchingException\"}}]}"; runQueryWithExpectedResult(graphQLRequest, expected); } diff --git a/elide-integration-tests/src/test/resources/EncodedErrorResponsesIT/graphQLMutationError.json b/elide-integration-tests/src/test/resources/EncodedErrorResponsesIT/graphQLMutationError.json index afb6d47f76..66fc135b75 100644 --- a/elide-integration-tests/src/test/resources/EncodedErrorResponsesIT/graphQLMutationError.json +++ b/elide-integration-tests/src/test/resources/EncodedErrorResponsesIT/graphQLMutationError.json @@ -6,7 +6,8 @@ "locations":[ {"line":1,"column":11} ], - "path":["invoice"] + "path":["invoice"], + "extensions":{"classification":"DataFetchingException"} } ] } From 85c9ce8a27a0e0882c5dacf248855fbea7ac0d48 Mon Sep 17 00:00:00 2001 From: wcekan Date: Sat, 12 Aug 2023 15:26:31 -0400 Subject: [PATCH 2/3] assert not null --- .../com/yahoo/elide/graphql/ModelBuilderTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java index bbeea07ff4..d6c63164f7 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java @@ -6,10 +6,9 @@ package com.yahoo.elide.graphql; -import static graphql.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -139,11 +138,11 @@ public void testBuild() { GraphQLSchema schema = builder.build(); - assertNotEquals(schema.getType(AUTHOR), null); - assertNotEquals(schema.getType(BOOK), null); - assertNotEquals(schema.getType(AUTHOR_INPUT), null); - assertNotEquals(schema.getType(BOOK_INPUT), null); - assertNotEquals(schema.getType("_root"), null); + assertNotNull(schema.getType(AUTHOR)); + assertNotNull(schema.getType(BOOK)); + assertNotNull(schema.getType(AUTHOR_INPUT)); + assertNotNull(schema.getType(BOOK_INPUT)); + assertNotNull(schema.getType("_root")); GraphQLObjectType bookType = getConnectedType((GraphQLObjectType) schema.getType(BOOK), null); GraphQLObjectType authorType = getConnectedType((GraphQLObjectType) schema.getType(AUTHOR), null); From 6092d725762dded08f83f51ced68ec6bee7e9921 Mon Sep 17 00:00:00 2001 From: wcekan Date: Sat, 12 Aug 2023 16:05:30 -0400 Subject: [PATCH 3/3] additional test info --- .../java/com/yahoo/elide/graphql/ModelBuilderTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java index d6c63164f7..adffed9112 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/ModelBuilderTest.java @@ -138,11 +138,11 @@ public void testBuild() { GraphQLSchema schema = builder.build(); - assertNotNull(schema.getType(AUTHOR)); - assertNotNull(schema.getType(BOOK)); - assertNotNull(schema.getType(AUTHOR_INPUT)); - assertNotNull(schema.getType(BOOK_INPUT)); - assertNotNull(schema.getType("_root")); + assertNotNull(schema.getType(AUTHOR), AUTHOR); + assertNotNull(schema.getType(BOOK), BOOK); + assertNotNull(schema.getType(AUTHOR_INPUT), AUTHOR_INPUT); + assertNotNull(schema.getType(BOOK_INPUT), BOOK_INPUT); + assertNotNull(schema.getType("_root"), "_root"); GraphQLObjectType bookType = getConnectedType((GraphQLObjectType) schema.getType(BOOK), null); GraphQLObjectType authorType = getConnectedType((GraphQLObjectType) schema.getType(AUTHOR), null);