diff --git a/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java b/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java index 368aae7857..40f27817c0 100644 --- a/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java +++ b/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java @@ -47,6 +47,7 @@ public class ElideSettings { @Getter private final Map serdes; @Getter private final boolean enableJsonLinks; @Getter private final boolean strictQueryParams; + @Getter private final boolean enableGraphQLFederation; @Getter private final String baseUrl; @Getter private final String jsonApiPath; @Getter private final String graphQLApiPath; diff --git a/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java b/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java index 805e4c21c9..c659704c49 100644 --- a/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java +++ b/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java @@ -63,6 +63,8 @@ public class ElideSettingsBuilder { private int defaultPageSize = PaginationImpl.DEFAULT_PAGE_LIMIT; private int updateStatusCode; private boolean enableJsonLinks; + + private boolean enableGraphQLFederation; private boolean strictQueryParams = true; private String baseUrl = ""; private String jsonApiPath; @@ -85,6 +87,7 @@ public ElideSettingsBuilder(DataStore dataStore) { updateStatusCode = HttpStatus.SC_NO_CONTENT; this.serdes = new LinkedHashMap<>(); this.enableJsonLinks = false; + this.enableGraphQLFederation = false; //By default, Elide supports epoch based dates. this.withEpochDates(); @@ -128,6 +131,7 @@ public ElideSettings build() { serdes, enableJsonLinks, strictQueryParams, + enableGraphQLFederation, baseUrl, jsonApiPath, graphQLApiPath, @@ -259,4 +263,9 @@ public ElideSettingsBuilder withStrictQueryParams(boolean enabled) { this.strictQueryParams = enabled; return this; } + + public ElideSettingsBuilder withGraphQLFederation(boolean enabled) { + this.enableGraphQLFederation = enabled; + return this; + } } diff --git a/elide-graphql/pom.xml b/elide-graphql/pom.xml index 72b59034e9..09110d430e 100644 --- a/elide-graphql/pom.xml +++ b/elide-graphql/pom.xml @@ -131,7 +131,11 @@ javax.persistence-api test - + + com.apollographql.federation + federation-graphql-java-support + 2.0.0-alpha.5 + org.glassfish.jersey.containers jersey-container-servlet 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 298de721a8..8bb008db15 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 @@ -10,10 +10,13 @@ import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; import static graphql.schema.GraphQLInputObjectField.newInputObjectField; import static graphql.schema.GraphQLObjectType.newObject; + +import com.yahoo.elide.ElideSettings; import com.yahoo.elide.core.dictionary.EntityDictionary; import com.yahoo.elide.core.dictionary.RelationshipType; import com.yahoo.elide.core.type.ClassType; import com.yahoo.elide.core.type.Type; +import com.apollographql.federation.graphqljava.Federation; import org.apache.commons.collections4.CollectionUtils; import graphql.Scalars; import graphql.schema.DataFetcher; @@ -72,6 +75,8 @@ public class ModelBuilder { private Set> excludedEntities; private Set objectTypes; + private boolean enableFederation; + /** * Class constructor, constructs the custom arguments to handle mutations. * @param entityDictionary elide entity dictionary @@ -80,6 +85,7 @@ public class ModelBuilder { */ public ModelBuilder(EntityDictionary entityDictionary, NonEntityDictionary nonEntityDictionary, + ElideSettings settings, DataFetcher dataFetcher, String apiVersion) { objectTypes = new HashSet<>(); this.generator = new GraphQLConversionUtils(entityDictionary, nonEntityDictionary); @@ -88,6 +94,7 @@ public ModelBuilder(EntityDictionary entityDictionary, this.nameUtils = new GraphQLNameUtils(entityDictionary); this.dataFetcher = dataFetcher; this.apiVersion = apiVersion; + this.enableFederation = settings.isEnableGraphQLFederation(); relationshipOpArg = newArgument() .name(ARGUMENT_OPERATION) @@ -218,6 +225,8 @@ public GraphQLSchema build() { inputObjectRegistry.values()))) .build(); + //Enable Apollo Federation + schema = (enableFederation) ? Federation.transform(schema).build() : schema; return schema; } 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 3523714dca..37da10d026 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 @@ -85,7 +85,7 @@ public QueryRunner(Elide elide, String apiVersion) { PersistentResourceFetcher fetcher = new PersistentResourceFetcher(nonEntityDictionary); ModelBuilder builder = new ModelBuilder(elide.getElideSettings().getDictionary(), - nonEntityDictionary, fetcher, apiVersion); + nonEntityDictionary, elide.getElideSettings(), fetcher, apiVersion); api = GraphQL.newGraphQL(builder.build()) .queryExecutionStrategy(new AsyncSerialExecutionStrategy()) diff --git a/elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/GraphQLEntityProjectionMaker.java b/elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/GraphQLEntityProjectionMaker.java index 1a64dcfd81..a3ba7c0178 100644 --- a/elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/GraphQLEntityProjectionMaker.java +++ b/elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/GraphQLEntityProjectionMaker.java @@ -162,10 +162,13 @@ private void addRootProjection(SelectionSet selectionSet) { Field rootSelectionField = (Field) rootSelection; String entityName = rootSelectionField.getName(); String aliasName = rootSelectionField.getAlias(); - if (SCHEMA.hasName(entityName) || TYPE.hasName(entityName)) { - // '__schema' and '__type' would not be handled by entity projection + + //_service comes from Apollo federation spec + if ("_service".equals(entityName) || SCHEMA.hasName(entityName) || TYPE.hasName(entityName)) { + // '_service' and '__schema' and '__type' would not be handled by entity projection return; } + Type entityType = getRootEntity(rootSelectionField.getName(), apiVersion); if (entityType == null) { throw new InvalidEntityBodyException(String.format("Unknown entity {%s}.", 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 6850f070f3..2471869fcf 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 @@ -277,6 +277,15 @@ public void testAliasPartialQueryAmbiguous() throws Exception { assertParsingFails(loadGraphQLRequest("fetch/aliasPartialQueryAmbiguous.graphql")); } + @Test + public void testFederationServiceIntrospection() throws Exception { + String graphQLRequest = "{ _service { sdl }}"; + + ElideResponse response = runGraphQLRequest(graphQLRequest, new HashMap<>()); + + assertTrue(! response.getBody().contains("errors")); + } + @Test public void testSchemaIntrospection() throws Exception { String graphQLRequest = "{" 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 39fd30c14b..4f43a10b3d 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 @@ -14,6 +14,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; + +import com.yahoo.elide.ElideSettings; import com.yahoo.elide.core.dictionary.ArgumentType; import com.yahoo.elide.core.dictionary.EntityDictionary; import com.yahoo.elide.core.request.Sorting; @@ -102,8 +104,10 @@ public ModelBuilderTest() { @Test public void testInternalModelConflict() { DataFetcher fetcher = mock(DataFetcher.class); + ElideSettings settings = mock(ElideSettings.class); ModelBuilder builder = new ModelBuilder(dictionary, - new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), fetcher, NO_VERSION); + new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), + settings, fetcher, NO_VERSION); GraphQLSchema schema = builder.build(); @@ -124,8 +128,10 @@ public void testInternalModelConflict() { @Test public void testPageInfoObject() { DataFetcher fetcher = mock(DataFetcher.class); + ElideSettings settings = mock(ElideSettings.class); ModelBuilder builder = new ModelBuilder(dictionary, - new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), fetcher, NO_VERSION); + new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), + settings, fetcher, NO_VERSION); GraphQLSchema schema = builder.build(); @@ -136,8 +142,10 @@ public void testPageInfoObject() { @Test public void testRelationshipParameters() { DataFetcher fetcher = mock(DataFetcher.class); + ElideSettings settings = mock(ElideSettings.class); ModelBuilder builder = new ModelBuilder(dictionary, - new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), fetcher, NO_VERSION); + new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), + settings, fetcher, NO_VERSION); GraphQLSchema schema = builder.build(); GraphQLObjectType root = schema.getQueryType(); @@ -173,8 +181,10 @@ public void testRelationshipParameters() { @Test public void testBuild() { DataFetcher fetcher = mock(DataFetcher.class); + ElideSettings settings = mock(ElideSettings.class); ModelBuilder builder = new ModelBuilder(dictionary, - new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), fetcher, NO_VERSION); + new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), + settings, fetcher, NO_VERSION); GraphQLSchema schema = builder.build(); @@ -251,8 +261,10 @@ public void checkAttributeArguments() { dictionary.addArgumentsToAttribute(ClassType.of(Book.class), FIELD_PUBLISH_DATE, arguments); DataFetcher fetcher = mock(DataFetcher.class); + ElideSettings settings = mock(ElideSettings.class); ModelBuilder builder = new ModelBuilder(dictionary, - new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), fetcher, NO_VERSION); + new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), + settings, fetcher, NO_VERSION); GraphQLSchema schema = builder.build(); @@ -270,8 +282,10 @@ public void checkModelArguments() { dictionary.addArgumentToEntity(ClassType.of(Author.class), new ArgumentType("filterAuthor", ClassType.STRING_TYPE)); DataFetcher fetcher = mock(DataFetcher.class); + ElideSettings settings = mock(ElideSettings.class); ModelBuilder builder = new ModelBuilder(dictionary, - new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), fetcher, NO_VERSION); + new NonEntityDictionary(DefaultClassScanner.getInstance(), CoerceUtil::lookup), + settings, fetcher, NO_VERSION); GraphQLSchema schema = builder.build(); 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 5c7640323c..ff5974ce68 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 @@ -81,6 +81,7 @@ public void initializeQueryRunner() { .withEntityDictionary(dictionary) .withJoinFilterDialect(filterDialect) .withSubqueryFilterDialect(filterDialect) + .withGraphQLFederation(true) .withISO8601Dates("yyyy-MM-dd'T'HH:mm'Z'", TimeZone.getTimeZone("UTC")) .build(); diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java index d32384a4b5..dc534b6491 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java @@ -195,6 +195,10 @@ public RefreshableElide getRefreshableElide(EntityDictionary dictionary, builder.withExportApiPath(settings.getAsync().getExport().getPath()); } + if (settings.getGraphql() != null && settings.getGraphql().enableFederation) { + builder.withGraphQLFederation(true); + } + if (settings.getJsonApi() != null && settings.getJsonApi().isEnabled() && settings.getJsonApi().isEnableLinks()) { diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideConfigProperties.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideConfigProperties.java index 2c02a45899..ed4a1a9085 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideConfigProperties.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideConfigProperties.java @@ -23,7 +23,7 @@ public class ElideConfigProperties { /** * Settings for the GraphQL controller. */ - private ControllerProperties graphql; + private GraphQLControllerProperties graphql; /** * Settings for the Swagger document controller. diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/GraphQLControllerProperties.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/GraphQLControllerProperties.java new file mode 100644 index 0000000000..2126777c4c --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/GraphQLControllerProperties.java @@ -0,0 +1,22 @@ +/* + * Copyright 2022, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Extra controller properties for the GraphQL endpoint. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GraphQLControllerProperties extends ControllerProperties { + + /** + * Turns on/off Apollo federation schema. + */ + boolean enableFederation = false; +}