diff --git a/docs/docs/recommended-usage.md b/docs/docs/recommended-usage.md index a1b09cd..3459037 100644 --- a/docs/docs/recommended-usage.md +++ b/docs/docs/recommended-usage.md @@ -4,9 +4,9 @@ sidebar_position: 4 # Recommended Usage -In general, the `resolverClasses` config should be used to generate more performant code. This is especially important +In general, the `resolverInterfaces` config should be used to generate more performant code. This is especially important when dealing with expensive operations, such as database queries or network requests. When at least one field has -arguments in a type, we generate an interface with function signatures to be inherited in source code. +arguments in a type, we generate an open class with function signatures to be inherited in source code. However, when fields have no arguments, we generate data classes by default. ## Example @@ -33,8 +33,8 @@ Generated Kotlin: ```kotlin package com.types.generated -interface Query { - fun resolveMyType(input: String): MyType +open class Query { + open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.") } data class MyType( @@ -50,7 +50,7 @@ import com.expediagroup.graphql.server.operations.Query import com.types.generated.MyType import com.types.generated.Query as QueryInterface -class MyQuery : Query, QueryInterface { +class MyQuery : Query, QueryInterface() { override fun resolveMyType(input: String): MyType = MyType( field1 = myExpensiveCall1(), @@ -65,7 +65,7 @@ that the `field1` and `field2` properties are both initialized when the `MyType` `myExpensiveCall1()` and `myExpensiveCall2()` will both be called in sequence! Even if I only query for `field1`, not only will `myExpensiveCall2()` still run, but it will also wait until `myExpensiveCall1()` is totally finished. -### Instead, use the `resolverClasses` config! +### Instead, use the `resolverInterfaces` config! Codegen config: @@ -73,7 +73,7 @@ Codegen config: import { GraphQLKotlinCodegenConfig } from "@expediagroup/graphql-kotlin-codegen"; export default { - resolverClasses: [ + resolverInterfaces: [ { typeName: "MyType", }, @@ -86,13 +86,13 @@ Generated Kotlin: ```kotlin package com.types.generated -interface Query { - fun resolveMyType(input: String): MyType +open class Query { + open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.") } -interface MyType { - fun field1(): String - fun field2(): String? +open class MyType { + open fun field1(): String = throw NotImplementedError("MyType.field1 must be implemented.") + open fun field2(): String? = throw NotImplementedError("MyType.field2 must be implemented.") } ``` @@ -102,12 +102,12 @@ Source code: import com.types.generated.MyType as MyTypeInterface import com.expediagroup.graphql.generator.annotations.GraphQLIgnore -class MyQuery : Query, QueryInterface { +class MyQuery : Query, QueryInterface() { override fun resolveMyType(input: String): MyType = MyType() } @GraphQLIgnore -class MyType : MyTypeInterface { +class MyType : MyTypeInterface() { override fun field1(): String = myExpensiveCall1() override fun field2(): String? = myExpensiveCall2() } diff --git a/src/definitions/object.ts b/src/definitions/object.ts index ecb5820..138b8ed 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -98,10 +98,8 @@ export function buildObjectTypeDefinition( const fieldNodes = typeInResolverInterfacesConfig ? node.fields : fieldsWithArguments; - const abstractModifier = constructor ? "abstract " : ""; - const keyWord = constructor ? "abstract class" : "interface"; - return `${annotations}${outputRestrictionAnnotation}${keyWord} ${name}${constructor}${interfaceInheritance} { -${getDataClassMembers({ node, fieldNodes, schema, config, shouldGenerateFunctions, abstractModifier })} + return `${annotations}${outputRestrictionAnnotation}open class ${name}${constructor}${interfaceInheritance} { +${getDataClassMembers({ node, fieldNodes, schema, config, shouldGenerateFunctions })} }`; } @@ -115,14 +113,12 @@ function getDataClassMembers({ fieldNodes, schema, config, - abstractModifier, shouldGenerateFunctions, }: { node: ObjectTypeDefinitionNode; fieldNodes?: readonly FieldDefinitionNode[]; schema: GraphQLSchema; config: CodegenConfigWithDefaults; - abstractModifier?: string; shouldGenerateFunctions?: boolean; }) { return (fieldNodes ?? node.fields) @@ -135,7 +131,6 @@ function getDataClassMembers({ config, typeMetadata, shouldGenerateFunctions, - abstractModifier, ); }) .join(`${shouldGenerateFunctions ? "" : ","}\n`); diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index 93613df..e7bb9b3 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -32,15 +32,8 @@ export function buildFieldDefinition( config: CodegenConfigWithDefaults, typeMetadata: TypeMetadata, shouldGenerateFunctions?: boolean, - abstractModifier: string = "", ) { - const modifier = buildFieldModifier( - node, - fieldNode, - schema, - config, - abstractModifier, - ); + const modifier = buildFieldModifier(node, fieldNode, schema, config); const fieldArguments = buildFieldArguments(node, fieldNode, schema, config); const fieldDefinition = `${modifier} ${fieldNode.name.value}${fieldArguments}`; const annotations = buildAnnotations({ @@ -56,7 +49,8 @@ export function buildFieldDefinition( ); } - const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""}`; + const notImplementedError = ` = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`; + const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""}${notImplementedError}`; const defaultValue = shouldGenerateFunctions ? defaultFunctionValue : typeMetadata.defaultValue; @@ -67,7 +61,7 @@ export function buildFieldDefinition( ); const isCompletableFuture = typeInResolverInterfacesConfig?.classMethods === "COMPLETABLE_FUTURE"; - const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; + const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>${notImplementedError}`; const field = indent( `${fieldDefinition}: ${isCompletableFuture ? completableFutureDefinition : defaultDefinition}`, 2, @@ -80,7 +74,6 @@ function buildFieldModifier( fieldNode: FieldDefinitionNode, schema: GraphQLSchema, config: CodegenConfigWithDefaults, - abstractModifier: string, ) { const typeInResolverInterfacesConfig = findTypeInResolverInterfacesConfig( node, @@ -91,9 +84,8 @@ function buildFieldModifier( fieldNode, schema, ); - const overrideModifier = shouldOverrideField ? "override " : ""; if (!typeInResolverInterfacesConfig && !fieldNode.arguments?.length) { - return `${overrideModifier}val`; + return shouldOverrideField ? "override val" : "val"; } const functionModifier = typeInResolverInterfacesConfig?.classMethods === "SUSPEND" @@ -102,8 +94,12 @@ function buildFieldModifier( if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) { return `${functionModifier}fun`; } - - return `${abstractModifier}${overrideModifier}${functionModifier}fun`; + const isCompletableFuture = + typeInResolverInterfacesConfig?.classMethods === "COMPLETABLE_FUTURE"; + if (shouldOverrideField && !isCompletableFuture) { + return "override fun"; + } + return `open ${functionModifier}fun`; } function buildFieldArguments( diff --git a/test/integration/Query.kt b/test/integration/Query.kt index 171cd1a..9786c01 100644 --- a/test/integration/Query.kt +++ b/test/integration/Query.kt @@ -4,6 +4,6 @@ import com.expediagroup.graphql.server.operations.Query import graphql.schema.DataFetchingEnvironment import test.integration.Query as QueryInterface -class IntegrationTestQuery : Query, QueryInterface { +class IntegrationTestQuery() : Query, QueryInterface() { override fun testQuery(dataFetchingEnvironment: DataFetchingEnvironment): SomeType = SomeType() } diff --git a/test/unit/should_consolidate_input_and_output_types/expected.kt b/test/unit/should_consolidate_input_and_output_types/expected.kt index 86f8d4a..8a34614 100644 --- a/test/unit/should_consolidate_input_and_output_types/expected.kt +++ b/test/unit/should_consolidate_input_and_output_types/expected.kt @@ -69,8 +69,8 @@ data class MyTypeToConsolidateInputParent( ) @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -interface MyTypeToConsolidateParent2 { - fun field(input: MyTypeToConsolidate, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? +open class MyTypeToConsolidateParent2 { + open fun field(input: MyTypeToConsolidate, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyTypeToConsolidateParent2.field must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) diff --git a/test/unit/should_generate_classes_for_types_with_field_args/expected.kt b/test/unit/should_generate_classes_for_types_with_field_args/expected.kt index fd0040e..745447d 100644 --- a/test/unit/should_generate_classes_for_types_with_field_args/expected.kt +++ b/test/unit/should_generate_classes_for_types_with_field_args/expected.kt @@ -3,18 +3,18 @@ package com.kotlin.generated import com.expediagroup.graphql.generator.annotations.* @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -interface TypeWithOnlyFieldArgs { - fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? - fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String +open class TypeWithOnlyFieldArgs { + open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("TypeWithOnlyFieldArgs.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("TypeWithOnlyFieldArgs.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -abstract class HybridType( +open class HybridType( val nullableField: String? = null, val nonNullableField: String ) { - abstract fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? - abstract fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String + open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("HybridType.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("HybridType.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT]) @@ -30,7 +30,7 @@ interface HybridInterface { } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -abstract class TypeImplementingInterface( +open class TypeImplementingInterface( override val field1: String? = null, override val field2: String, val booleanField1: Boolean? = null, @@ -38,6 +38,6 @@ abstract class TypeImplementingInterface( val integerField1: Int? = null, val integerField2: Int ) : HybridInterface { - abstract override fun nullableListResolver(arg1: Int?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? - abstract override fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List + override fun nullableListResolver(arg1: Int?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? = throw NotImplementedError("TypeImplementingInterface.nullableListResolver must be implemented.") + override fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List = throw NotImplementedError("TypeImplementingInterface.nonNullableListResolver must be implemented.") } diff --git a/test/unit/should_honor_resolverInterfaces_config/expected.kt b/test/unit/should_honor_resolverInterfaces_config/expected.kt index f3a1554..4f0e457 100644 --- a/test/unit/should_honor_resolverInterfaces_config/expected.kt +++ b/test/unit/should_honor_resolverInterfaces_config/expected.kt @@ -3,39 +3,39 @@ package com.kotlin.generated import com.expediagroup.graphql.generator.annotations.* @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -interface MyIncludedResolverType { - fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? - fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String - fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? - fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String - fun nullableListResolver(arg1: Int? = null, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? - fun nonNullableListResolver(arg1: Int, arg2: Int? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List +open class MyIncludedResolverType { + open fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyIncludedResolverType.nullableField must be implemented.") + open fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyIncludedResolverType.nonNullableField must be implemented.") + open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyIncludedResolverType.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyIncludedResolverType.nonNullableResolver must be implemented.") + open fun nullableListResolver(arg1: Int? = null, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? = throw NotImplementedError("MyIncludedResolverType.nullableListResolver must be implemented.") + open fun nonNullableListResolver(arg1: Int, arg2: Int? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List = throw NotImplementedError("MyIncludedResolverType.nonNullableListResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -interface MyIncludedResolverTypeWithNoFieldArgs { - fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? - fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String +open class MyIncludedResolverTypeWithNoFieldArgs { + open fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyIncludedResolverTypeWithNoFieldArgs.nullableField must be implemented.") + open fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyIncludedResolverTypeWithNoFieldArgs.nonNullableField must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -interface MySuspendResolverType { - suspend fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? - suspend fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String - suspend fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? - suspend fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String - suspend fun nullableListResolver(arg1: Int? = null, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? - suspend fun nonNullableListResolver(arg1: Int, arg2: Int? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List +open class MySuspendResolverType { + open suspend fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MySuspendResolverType.nullableField must be implemented.") + open suspend fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MySuspendResolverType.nonNullableField must be implemented.") + open suspend fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MySuspendResolverType.nullableResolver must be implemented.") + open suspend fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MySuspendResolverType.nonNullableResolver must be implemented.") + open suspend fun nullableListResolver(arg1: Int? = null, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? = throw NotImplementedError("MySuspendResolverType.nullableListResolver must be implemented.") + open suspend fun nonNullableListResolver(arg1: Int, arg2: Int? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List = throw NotImplementedError("MySuspendResolverType.nonNullableListResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -interface MyCompletableFutureResolverType { - fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture - fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture - fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture - fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture - fun nullableListResolver(arg1: Int? = null, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture?> - fun nonNullableListResolver(arg1: Int, arg2: Int? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture> +open class MyCompletableFutureResolverType { + open fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nullableField must be implemented.") + open fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableField must be implemented.") + open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableResolver must be implemented.") + open fun nullableListResolver(arg1: Int? = null, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture?> = throw NotImplementedError("MyCompletableFutureResolverType.nullableListResolver must be implemented.") + open fun nonNullableListResolver(arg1: Int, arg2: Int? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture> = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableListResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) diff --git a/test/unit/should_replace_federation_directives/expected.kt b/test/unit/should_replace_federation_directives/expected.kt index 797fa33..0a6a4e1 100644 --- a/test/unit/should_replace_federation_directives/expected.kt +++ b/test/unit/should_replace_federation_directives/expected.kt @@ -13,9 +13,9 @@ data class FederatedType( @com.expediagroup.graphql.generator.federation.directives.ExtendsDirective @com.expediagroup.graphql.generator.federation.directives.KeyDirective(com.expediagroup.graphql.generator.federation.directives.FieldSet("some other field")) @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -abstract class FederatedTypeResolver( +open class FederatedTypeResolver( @com.expediagroup.graphql.generator.federation.directives.ExternalDirective val field2: String? = null ) { - abstract fun field(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String + open fun field(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("FederatedTypeResolver.field must be implemented.") }