未验证 提交 fda4235d 编写于 作者: B Bogdan Kobylynskyi 提交者: GitHub

Relay support #295 (#304)

上级 da059611
......@@ -38,6 +38,7 @@
| `apiReturnType` | String | Empty | Return type for api methods (query/mutation). For example: `reactor.core.publisher.Mono`, etc. |
| `apiReturnListType` | String | Empty | Return type for api methods (query/mutation) having list type. For example: `reactor.core.publisher.Flux`, etc. By default is empty, so `apiReturnType` will be used. |
| `subscriptionReturnType` | String | Empty | Return type for subscription methods. For example: `org.reactivestreams.Publisher`, `io.reactivex.Observable`, etc. |
| `relayConfig` | *See [RelayConfig](#option-relayconfig)* | `@connection(for: ...)` | *See [RelayConfig](#option-relayconfig)* |
| `generateClient` | Boolean | False | Specifies whether client-side classes should be generated for each query, mutation and subscription. This includes: `Request` classes (contain input data), `ResponseProjection` classes for each type (contain response fields) and `Response` classes (contain response data). |
| `requestSuffix` | String | Request | Sets the suffix for `Request` classes. |
| `responseSuffix` | String | Response | Sets the suffix for `Response` classes. |
......@@ -137,6 +138,27 @@ You can also use one of the formatters for directive argument value: `{{val?toSt
### Option `relayConfig`
Can be used to supply a custom configuration for Relay support.
For reference see: https://www.graphql-java-kickstart.com/tools/relay/
| Key inside `relayConfig` | Data Type | Default value | Description |
| ------------------------ | --------- | -------------------------- | ----------- |
| `directiveName` | String | `connection` | Directive name used for marking a field. |
| `directiveArgumentName` | String | `for` | Directive argument name that contains a GraphQL type name. |
| `connectionType` | String | `graphql.relay.Connection` | Generic Connection type. |
For example, the following schema:
```
type Query { users(first: Int, after: String): UserConnection @connection(for: "User") }
```
will result in generating the interface with the following method:
```
graphql.relay.Connection<User> users(Integer first, String after) throws Exception;
```
### External mapping configuration
......
......@@ -76,6 +76,7 @@ public class GraphQLCodegenGradleTask extends DefaultTask implements GraphQLCode
private Boolean useOptionalForNullableReturnTypes = MappingConfigConstants.DEFAULT_USE_OPTIONAL_FOR_NULLABLE_RETURN_TYPES;
private Set<String> fieldsWithResolvers = new HashSet<>();
private Set<String> fieldsWithoutResolvers = new HashSet<>();
private RelayConfig relayConfig = new RelayConfig();
private Boolean generateClient;
......@@ -134,6 +135,7 @@ public class GraphQLCodegenGradleTask extends DefaultTask implements GraphQLCode
mappingConfig.setQueryResolverParentInterface(getQueryResolverParentInterface());
mappingConfig.setMutationResolverParentInterface(getMutationResolverParentInterface());
mappingConfig.setSubscriptionResolverParentInterface(getSubscriptionResolverParentInterface());
mappingConfig.setRelayConfig(relayConfig);
new GraphQLCodegen(getActualSchemaPaths(), graphqlQueryIntrospectionResultPath, outputDir, mappingConfig, buildJsonSupplier()).generate();
}
......@@ -618,6 +620,17 @@ public class GraphQLCodegenGradleTask extends DefaultTask implements GraphQLCode
this.parametrizedInputSuffix = parametrizedInputSuffix;
}
@Nested
@Optional
@Override
public RelayConfig getRelayConfig() {
return relayConfig;
}
public void relayConfig(Action<? super RelayConfig> action) {
action.execute(relayConfig);
}
@Nested
@Optional
public ParentInterfacesConfig getParentInterfaces() {
......
package io.github.kobylynskyi.graphql.codegen.gradle;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
public class RelayConfig extends com.kobylynskyi.graphql.codegen.model.RelayConfig {
@Input
@Optional
@Override
public String getDirectiveName() {
return super.getDirectiveName();
}
@Input
@Optional
@Override
public String getDirectiveArgumentName() {
return super.getDirectiveArgumentName();
}
@Input
@Optional
@Override
public String getConnectionType() {
return super.getConnectionType();
}
}
......@@ -7,6 +7,7 @@ import com.kobylynskyi.graphql.codegen.model.ApiRootInterfaceStrategy;
import com.kobylynskyi.graphql.codegen.model.GraphQLCodegenConfiguration;
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
import com.kobylynskyi.graphql.codegen.model.MappingConfigConstants;
import com.kobylynskyi.graphql.codegen.model.RelayConfig;
import com.kobylynskyi.graphql.codegen.supplier.JsonMappingConfigSupplier;
import com.kobylynskyi.graphql.codegen.supplier.MappingConfigSupplier;
import com.kobylynskyi.graphql.codegen.supplier.SchemaFinder;
......@@ -154,6 +155,9 @@ public class GraphQLCodegenMojo extends AbstractMojo implements GraphQLCodegenCo
@Parameter(defaultValue = MappingConfigConstants.DEFAULT_PARAMETRIZED_INPUT_SUFFIX)
private String parametrizedInputSuffix;
@Parameter
private RelayConfig relayConfig = new RelayConfig();
@Parameter
private String jsonConfigurationFile;
......@@ -211,6 +215,7 @@ public class GraphQLCodegenMojo extends AbstractMojo implements GraphQLCodegenCo
mappingConfig.setQueryResolverParentInterface(getQueryResolverParentInterface());
mappingConfig.setMutationResolverParentInterface(getMutationResolverParentInterface());
mappingConfig.setSubscriptionResolverParentInterface(getSubscriptionResolverParentInterface());
mappingConfig.setRelayConfig(relayConfig);
MappingConfigSupplier mappingConfigSupplier = buildJsonSupplier(jsonConfigurationFile);
......@@ -513,6 +518,15 @@ public class GraphQLCodegenMojo extends AbstractMojo implements GraphQLCodegenCo
return generateDataFetchingEnvironmentArgumentInApis;
}
@Override
public RelayConfig getRelayConfig() {
return relayConfig;
}
public void setRelayConfig(RelayConfig relayConfig) {
this.relayConfig = relayConfig;
}
public void setGenerateDataFetchingEnvironmentArgumentInApis(boolean generateDataFetchingEnvironmentArgumentInApis) {
this.generateDataFetchingEnvironmentArgumentInApis = generateDataFetchingEnvironmentArgumentInApis;
}
......
......@@ -4,10 +4,14 @@ import com.kobylynskyi.graphql.codegen.model.MappingContext;
import com.kobylynskyi.graphql.codegen.model.NamedDefinition;
import com.kobylynskyi.graphql.codegen.model.OperationDefinition;
import com.kobylynskyi.graphql.codegen.model.ParameterDefinition;
import com.kobylynskyi.graphql.codegen.model.RelayConfig;
import com.kobylynskyi.graphql.codegen.model.definitions.ExtendedFieldDefinition;
import com.kobylynskyi.graphql.codegen.model.definitions.ExtendedObjectTypeDefinition;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
import com.kobylynskyi.graphql.codegen.utils.Utils;
import graphql.language.Argument;
import graphql.language.Directive;
import graphql.language.StringValue;
import graphql.language.TypeName;
import java.util.ArrayList;
......@@ -131,23 +135,26 @@ public class FieldDefinitionsToResolverDataModelMapper {
* Builds a Freemarker-understandable structure representing an operation to resolve a field for a given parent type.
*
* @param mappingContext Global mapping context
* @param resolvedField The GraphQL definition of the field that the method should resolve
* @param fieldDef The GraphQL definition of the field that the method should resolve
* @param parentTypeName Name of the parent type which the field belongs to
* @return Freemarker-understandable format of operation
*/
private static OperationDefinition map(MappingContext mappingContext, ExtendedFieldDefinition resolvedField,
private static OperationDefinition map(MappingContext mappingContext, ExtendedFieldDefinition fieldDef,
String parentTypeName) {
NamedDefinition javaType = GraphqlTypeToJavaTypeMapper.getJavaType(
mappingContext, resolvedField.getType(), resolvedField.getName(), parentTypeName);
String name = MapperUtils.capitalizeIfRestricted(fieldDef.getName());
NamedDefinition javaType = GraphqlTypeToJavaTypeMapper.getJavaType(mappingContext, fieldDef.getType(), fieldDef.getName(), parentTypeName);
String returnType = getReturnType(mappingContext, fieldDef, javaType, parentTypeName);
List<String> annotations = GraphqlTypeToJavaTypeMapper.getAnnotations(mappingContext, fieldDef.getType(), fieldDef, parentTypeName, false);
List<ParameterDefinition> parameters = getOperationParameters(mappingContext, fieldDef, parentTypeName);
OperationDefinition operation = new OperationDefinition();
operation.setName(MapperUtils.capitalizeIfRestricted(resolvedField.getName()));
operation.setOriginalName(resolvedField.getName());
operation.setType(GraphqlTypeToJavaTypeMapper.wrapIntoReturnTypeIfRequired(mappingContext, javaType, parentTypeName));
operation.setAnnotations(GraphqlTypeToJavaTypeMapper.getAnnotations(mappingContext,
resolvedField.getType(), resolvedField, parentTypeName, false));
operation.setParameters(getOperationParameters(mappingContext, resolvedField, parentTypeName));
operation.setJavaDoc(resolvedField.getJavaDoc());
operation.setDeprecated(resolvedField.isDeprecated());
operation.setName(name);
operation.setOriginalName(fieldDef.getName());
operation.setType(returnType);
operation.setAnnotations(annotations);
operation.setParameters(parameters);
operation.setJavaDoc(fieldDef.getJavaDoc());
operation.setDeprecated(fieldDef.isDeprecated());
return operation;
}
......@@ -196,4 +203,24 @@ public class FieldDefinitionsToResolverDataModelMapper {
MapperUtils.getModelClassNameWithPrefixAndSuffix(mappingContext, typeName));
}
private static String getReturnType(MappingContext mappingContext, ExtendedFieldDefinition fieldDef,
NamedDefinition namedDefinition, String parentTypeName) {
RelayConfig relayConfig = mappingContext.getRelayConfig();
if (relayConfig != null && relayConfig.getDirectiveName() != null) {
Directive connectionDirective = fieldDef.getDirective(relayConfig.getDirectiveName());
if (connectionDirective != null) {
Argument argument = connectionDirective.getArgument(relayConfig.getDirectiveArgumentName());
// as of now supporting only string value of directive argument
if (argument != null && argument.getValue() instanceof StringValue) {
String graphqlTypeName = ((StringValue) argument.getValue()).getValue();
String javaTypeName = GraphqlTypeToJavaTypeMapper.getJavaType(mappingContext,
new TypeName(graphqlTypeName), graphqlTypeName, parentTypeName, false).getName();
return GraphqlTypeToJavaTypeMapper.getGenericsString(relayConfig.getConnectionType(), javaTypeName);
}
}
}
return GraphqlTypeToJavaTypeMapper.wrapApiReturnTypeIfRequired(mappingContext, namedDefinition, parentTypeName);
}
}
......@@ -255,7 +255,7 @@ class GraphqlTypeToJavaTypeMapper {
* @param parentTypeName Name of the parent type
* @return Java type wrapped into the subscriptionReturnType
*/
static String wrapIntoReturnTypeIfRequired(MappingContext mappingContext, NamedDefinition namedDefinition, String parentTypeName) {
static String wrapApiReturnTypeIfRequired(MappingContext mappingContext, NamedDefinition namedDefinition, String parentTypeName) {
String javaTypeName = namedDefinition.getName();
if (parentTypeName.equalsIgnoreCase(GraphQLOperation.SUBSCRIPTION.name())) {
if (Utils.isNotBlank(mappingContext.getSubscriptionReturnType())) {
......@@ -281,7 +281,7 @@ class GraphqlTypeToJavaTypeMapper {
return javaTypeName;
}
private static String getGenericsString(String genericType, String typeParameter) {
static String getGenericsString(String genericType, String typeParameter) {
return String.format("%s<%s>", genericType, typeParameter);
}
......
......@@ -235,6 +235,13 @@ public interface GraphQLCodegenConfiguration {
*/
Boolean getGenerateDataFetchingEnvironmentArgumentInApis();
/**
* Relay-related configurations.
*
* @return Relay-related configurations.
*/
RelayConfig getRelayConfig();
/**
* Fields that require Resolvers should be defined here in format: TypeName, TypeName.fieldName, @directive
* If just type is specified, then all fields of this type will have resolvers
......
......@@ -38,6 +38,7 @@ public class MappingConfig implements GraphQLCodegenConfiguration, Combinable<Ma
private String apiReturnType;
private String apiReturnListType;
private String subscriptionReturnType;
private RelayConfig relayConfig = new RelayConfig();
// various toggles
private Boolean generateApis;
......@@ -99,6 +100,7 @@ public class MappingConfig implements GraphQLCodegenConfiguration, Combinable<Ma
generateDataFetchingEnvironmentArgumentInApis = getValueOrDefaultToThis(source, GraphQLCodegenConfiguration::getGenerateDataFetchingEnvironmentArgumentInApis);
generateModelsForRootTypes = getValueOrDefaultToThis(source, GraphQLCodegenConfiguration::getGenerateModelsForRootTypes);
useOptionalForNullableReturnTypes = getValueOrDefaultToThis(source, GraphQLCodegenConfiguration::getUseOptionalForNullableReturnTypes);
relayConfig = getValueOrDefaultToThis(source, GraphQLCodegenConfiguration::getRelayConfig);
queryResolverParentInterface = getValueOrDefaultToThis(source, GraphQLCodegenConfiguration::getQueryResolverParentInterface);
mutationResolverParentInterface = getValueOrDefaultToThis(source, GraphQLCodegenConfiguration::getMutationResolverParentInterface);
subscriptionResolverParentInterface = getValueOrDefaultToThis(source, GraphQLCodegenConfiguration::getSubscriptionResolverParentInterface);
......@@ -404,6 +406,15 @@ public class MappingConfig implements GraphQLCodegenConfiguration, Combinable<Ma
this.generateDataFetchingEnvironmentArgumentInApis = generateDataFetchingEnvironmentArgumentInApis;
}
@Override
public RelayConfig getRelayConfig() {
return relayConfig;
}
public void setRelayConfig(RelayConfig relayConfig) {
this.relayConfig = relayConfig;
}
@Override
public Boolean getGenerateModelsForRootTypes() {
return generateModelsForRootTypes;
......
......@@ -166,6 +166,11 @@ public class MappingContext implements GraphQLCodegenConfiguration {
return config.getGenerateDataFetchingEnvironmentArgumentInApis();
}
@Override
public RelayConfig getRelayConfig() {
return config.getRelayConfig();
}
@Override
public Boolean getUseOptionalForNullableReturnTypes() {
return config.getUseOptionalForNullableReturnTypes();
......
package com.kobylynskyi.graphql.codegen.model;
import java.io.Serializable;
public class RelayConfig implements Serializable {
// Increment this when the serialization output changes
private static final long serialVersionUID = 937465092341L;
private String directiveName = "connection";
private String directiveArgumentName = "for";
private String connectionType = "graphql.relay.Connection";
public String getDirectiveName() {
return directiveName;
}
public void setDirectiveName(String directiveName) {
this.directiveName = directiveName;
}
public String getDirectiveArgumentName() {
return directiveArgumentName;
}
public void setDirectiveArgumentName(String directiveArgumentName) {
this.directiveArgumentName = directiveArgumentName;
}
public String getConnectionType() {
return connectionType;
}
public void setConnectionType(String connectionType) {
this.connectionType = connectionType;
}
}
package com.kobylynskyi.graphql.codegen;
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
import com.kobylynskyi.graphql.codegen.supplier.SchemaFinder;
import com.kobylynskyi.graphql.codegen.utils.Utils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import static com.kobylynskyi.graphql.codegen.TestUtils.assertSameTrimmedContent;
import static com.kobylynskyi.graphql.codegen.TestUtils.getFileByName;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
class GraphQLCodegenRelayTest {
private final File outputBuildDir = new File("build/generated");
private final File outputJavaClassesDir = new File("build/generated");
private final MappingConfig mappingConfig = new MappingConfig();
private final SchemaFinder schemaFinder = new SchemaFinder(Paths.get("src/test/resources"));
@BeforeEach
void init() {
mappingConfig.setGenerateDataFetchingEnvironmentArgumentInApis(true);
schemaFinder.setIncludePattern("relay.graphqls");
}
@AfterEach
void cleanup() {
Utils.deleteDir(outputBuildDir);
}
@Test
void generateServerSideRelayClasses() throws Exception {
new GraphQLCodegen(schemaFinder.findSchemas(), outputBuildDir, mappingConfig, TestUtils.getStaticGeneratedInfo()).generate();
File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles());
Set<String> generatedFileNames = Arrays.stream(files).map(File::getName).collect(toSet());
assertEquals(new HashSet<>(asList("Organization.java", "User.java", "OrganizationsQueryResolver.java",
"QueryResolver.java", "UsersQueryResolver.java")), generatedFileNames);
for (File file : files) {
assertSameTrimmedContent(
new File(String.format("src/test/resources/expected-classes/relay/%s.txt", file.getName())),
file);
}
}
}
\ No newline at end of file
......@@ -72,6 +72,7 @@ class MappingConfigTest {
assertFalse(mappingConfig.getGenerateModelsForRootTypes());
assertEquals("11", mappingConfig.getTypeResolverPrefix());
assertEquals("12", mappingConfig.getTypeResolverSuffix());
assertEquals("key", mappingConfig.getRelayConfig().getDirectiveArgumentName());
}
@Test
......@@ -112,6 +113,7 @@ class MappingConfigTest {
assertEquals("9", mappingConfig.getParametrizedInputSuffix());
assertEquals("11", mappingConfig.getTypeResolverPrefix());
assertEquals("12", mappingConfig.getTypeResolverSuffix());
assertEquals("key", mappingConfig.getRelayConfig().getDirectiveArgumentName());
}
@Test
......@@ -155,6 +157,7 @@ class MappingConfigTest {
assertEquals("99", mappingConfig.getParametrizedInputSuffix());
assertEquals("1111", mappingConfig.getTypeResolverPrefix());
assertEquals("1212", mappingConfig.getTypeResolverSuffix());
assertEquals("for", mappingConfig.getRelayConfig().getDirectiveArgumentName());
}
private static <T> Map<String, T> hashMap(AbstractMap.SimpleEntry<String, T>... entries) {
......@@ -197,6 +200,9 @@ class MappingConfigTest {
config.setParametrizedInputSuffix("9");
config.setTypeResolverPrefix("11");
config.setTypeResolverSuffix("12");
RelayConfig relayConfig = new RelayConfig();
relayConfig.setDirectiveArgumentName("key");
config.setRelayConfig(relayConfig);
return config;
}
......@@ -235,6 +241,9 @@ class MappingConfigTest {
config.setParametrizedInputSuffix("99");
config.setTypeResolverPrefix("1111");
config.setTypeResolverSuffix("1212");
RelayConfig relayConfig = new RelayConfig();
relayConfig.setDirectiveArgumentName("for");
config.setRelayConfig(relayConfig);
return config;
}
......@@ -245,6 +254,7 @@ class MappingConfigTest {
mappingConfig.setDirectiveAnnotationsMapping(null);
mappingConfig.setFieldsWithResolvers(null);
mappingConfig.setFieldsWithoutResolvers(null);
mappingConfig.setRelayConfig(null);
return mappingConfig;
}
......@@ -278,6 +288,7 @@ class MappingConfigTest {
assertEquals(expectedMappingConfig.getTypeResolverPrefix(), mappingConfig.getTypeResolverPrefix());
assertEquals(expectedMappingConfig.getTypeResolverSuffix(), mappingConfig.getTypeResolverSuffix());
assertEquals(expectedMappingConfig.getUseOptionalForNullableReturnTypes(), mappingConfig.getUseOptionalForNullableReturnTypes());
assertEquals(expectedMappingConfig.getRelayConfig(), mappingConfig.getRelayConfig());
}
}
\ No newline at end of file
@javax.annotation.Generated(
value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",
date = "2020-12-31T23:59:59-0500"
)
public class Organization implements java.io.Serializable {
@javax.validation.constraints.NotNull
private String id;
public Organization() {
}
public Organization(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public static Organization.Builder builder() {
return new Organization.Builder();
}
public static class Builder {
private String id;
public Builder() {
}
public Builder setId(String id) {
this.id = id;
return this;
}
public Organization build() {
return new Organization(id);
}
}
}
@javax.annotation.Generated(
value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",
date = "2020-12-31T23:59:59-0500"
)
public interface OrganizationsQueryResolver {
graphql.relay.Connection<Organization> organizations(Integer first, String after, graphql.schema.DataFetchingEnvironment env) throws Exception;
}
\ No newline at end of file
@javax.annotation.Generated(
value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",
date = "2020-12-31T23:59:59-0500"
)
public interface QueryResolver {
graphql.relay.Connection<User> users(Integer first, String after, graphql.schema.DataFetchingEnvironment env) throws Exception;
graphql.relay.Connection<Organization> organizations(Integer first, String after, graphql.schema.DataFetchingEnvironment env) throws Exception;
}
\ No newline at end of file
@javax.annotation.Generated(
value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",
date = "2020-12-31T23:59:59-0500"
)
public class User implements java.io.Serializable {
@javax.validation.constraints.NotNull
private String id;
private String name;
public User() {
}
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static User.Builder builder() {
return new User.Builder();
}
public static class Builder {
private String id;
private String name;
public Builder() {
}
public Builder setId(String id) {
this.id = id;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public User build() {
return new User(id, name);
}
}
}
@javax.annotation.Generated(
value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",
date = "2020-12-31T23:59:59-0500"
)
public interface UsersQueryResolver {
graphql.relay.Connection<User> users(Integer first, String after, graphql.schema.DataFetchingEnvironment env) throws Exception;
}
\ No newline at end of file
type Query {
users(first: Int, after: String): UserConnection @connection(for: "User")
organizations(first: Int, after: String): OrganizationConnection @connection(for: "Organization")
}
type User {
id: ID!
name: String
}
type Organization {
id: ID!
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册