From 6e44e9efd89438809be54601c7f2665113029a90 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Sun, 28 Apr 2024 15:30:23 -0500 Subject: [PATCH 01/18] unit test --- .../expected.kt | 68 ++++++++----------- .../schema.graphql | 19 ++++-- 2 files changed, 40 insertions(+), 47 deletions(-) diff --git a/test/unit/should_generate_field_resolver_interfaces/expected.kt b/test/unit/should_generate_field_resolver_interfaces/expected.kt index f0a6055..d702388 100644 --- a/test/unit/should_generate_field_resolver_interfaces/expected.kt +++ b/test/unit/should_generate_field_resolver_interfaces/expected.kt @@ -2,54 +2,42 @@ package com.kotlin.generated import com.expediagroup.graphql.generator.annotations.* -@GraphQLIgnore -interface Query { - suspend fun nullableField(): FieldType? = null - suspend fun nonNullableField(): FieldType - suspend fun nullableResolver(arg: String): String? = null - suspend fun nonNullableResolver(arg: InputTypeGenerateFieldResolverInterfaces): String +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +open class TypeWithOnlyFieldArgs { + open fun nullableResolver(arg: String): String? = throw NotImplementedError("Query.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("Query.nonNullableResolver must be implemented.") } -@GraphQLIgnore -interface QueryCompletableFuture { - fun nullableField(): java.util.concurrent.CompletableFuture - fun nonNullableField(): java.util.concurrent.CompletableFuture - fun nullableResolver(arg: String): java.util.concurrent.CompletableFuture - fun nonNullableResolver(arg: InputTypeGenerateFieldResolverInterfaces): java.util.concurrent.CompletableFuture +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +open class HybridType( + val nullableField: String? = null, + val nonNullableField: String +) { + open fun nullableResolver(arg: String): String? = throw NotImplementedError("Query.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("Query.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT]) -data class InputTypeGenerateFieldResolverInterfaces( +data class InputTypeForResolver( val field: String? = null ) -interface MyFieldInterface { - suspend fun field1(): String? - suspend fun field2(): String - suspend fun nullableListResolver(arg1: Int?, arg2: Int): List? - suspend fun nonNullableListResolver(arg1: Int, arg2: Int?): List +interface HybridInterface { + val field1: String? + val field2: String + fun nullableListResolver(arg1: Int?, arg2: Int): List? + fun nonNullableListResolver(arg1: Int, arg2: Int?): List } -@GraphQLIgnore -interface FieldType : MyFieldInterface { - override suspend fun field1(): String? = null - override suspend fun field2(): String - suspend fun booleanField1(): Boolean? = null - suspend fun booleanField2(): Boolean = false - suspend fun integerField1(): Int? = null - suspend fun integerField2(): Int - override suspend fun nullableListResolver(arg1: Int?, arg2: Int): List? = null - override suspend fun nonNullableListResolver(arg1: Int, arg2: Int?): List = emptyList() -} - -@GraphQLIgnore -interface FieldTypeCompletableFuture { - fun field1(): java.util.concurrent.CompletableFuture - fun field2(): java.util.concurrent.CompletableFuture - fun booleanField1(): java.util.concurrent.CompletableFuture - fun booleanField2(): java.util.concurrent.CompletableFuture - fun integerField1(): java.util.concurrent.CompletableFuture - fun integerField2(): java.util.concurrent.CompletableFuture - fun nullableListResolver(arg1: Int?, arg2: Int): java.util.concurrent.CompletableFuture?> - fun nonNullableListResolver(arg1: Int, arg2: Int?): java.util.concurrent.CompletableFuture> +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +open class TypeImplementingInterface( + override val field1: String? = null, + override val field2: String, + val booleanField1: Boolean? = null, + val booleanField2: Boolean, + val integerField1: Int? = null, + val integerField2: Int +) : HybridInterface { + override fun nullableListResolver(arg1: Int?, arg2: Int): List? = throw NotImplementedError("FieldType.nullableListResolver must be implemented.") + override fun nonNullableListResolver(arg1: Int, arg2: Int?): List = throw NotImplementedError("FieldType.nonNullableListResolver must be implemented.") } diff --git a/test/unit/should_generate_field_resolver_interfaces/schema.graphql b/test/unit/should_generate_field_resolver_interfaces/schema.graphql index ff57062..117cff1 100644 --- a/test/unit/should_generate_field_resolver_interfaces/schema.graphql +++ b/test/unit/should_generate_field_resolver_interfaces/schema.graphql @@ -1,22 +1,27 @@ -type Query { - nullableField: FieldType - nonNullableField: FieldType! +type TypeWithOnlyFieldArgs { nullableResolver(arg: String!): String - nonNullableResolver(arg: InputTypeGenerateFieldResolverInterfaces!): String! + nonNullableResolver(arg: InputTypeForResolver!): String! } -input InputTypeGenerateFieldResolverInterfaces { +type HybridType { + nullableField: String + nonNullableField: String! + nullableResolver(arg: String!): String + nonNullableResolver(arg: InputTypeForResolver!): String! +} + +input InputTypeForResolver { field: String } -interface MyFieldInterface { +interface HybridInterface { field1: String field2: String! nullableListResolver(arg1: Int, arg2: Int!): [String] nonNullableListResolver(arg1: Int!, arg2: Int): [String!]! } -type FieldType implements MyFieldInterface { +type TypeImplementingInterface implements HybridInterface { field1: String field2: String! booleanField1: Boolean From 9fdef060afbc5fb482bb8f411132b1b51510cff8 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Mon, 29 Apr 2024 20:01:58 -0500 Subject: [PATCH 02/18] unit test progress --- .../expected.kt | 12 +++--- .../codegen.config.ts | 15 ++++++- .../expected.kt | 40 +++++++++---------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/test/unit/should_generate_field_resolver_interfaces/expected.kt b/test/unit/should_generate_field_resolver_interfaces/expected.kt index d702388..ae04d37 100644 --- a/test/unit/should_generate_field_resolver_interfaces/expected.kt +++ b/test/unit/should_generate_field_resolver_interfaces/expected.kt @@ -4,8 +4,8 @@ import com.expediagroup.graphql.generator.annotations.* @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) open class TypeWithOnlyFieldArgs { - open fun nullableResolver(arg: String): String? = throw NotImplementedError("Query.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("Query.nonNullableResolver must be implemented.") + open fun nullableResolver(arg: String): String? = throw NotImplementedError("TypeWithOnlyFieldArgs.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("TypeWithOnlyFieldArgs.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) @@ -13,8 +13,8 @@ open class HybridType( val nullableField: String? = null, val nonNullableField: String ) { - open fun nullableResolver(arg: String): String? = throw NotImplementedError("Query.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("Query.nonNullableResolver must be implemented.") + open fun nullableResolver(arg: String): String? = throw NotImplementedError("HybridType.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("HybridType.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT]) @@ -38,6 +38,6 @@ open class TypeImplementingInterface( val integerField1: Int? = null, val integerField2: Int ) : HybridInterface { - override fun nullableListResolver(arg1: Int?, arg2: Int): List? = throw NotImplementedError("FieldType.nullableListResolver must be implemented.") - override fun nonNullableListResolver(arg1: Int, arg2: Int?): List = throw NotImplementedError("FieldType.nonNullableListResolver must be implemented.") + override fun nullableListResolver(arg1: Int?, arg2: Int): List? = throw NotImplementedError("TypeImplementingInterface.nullableListResolver must be implemented.") + override fun nonNullableListResolver(arg1: Int, arg2: Int?): List = throw NotImplementedError("TypeImplementingInterface.nonNullableListResolver must be implemented.") } diff --git a/test/unit/should_honor_resolverTypes_config/codegen.config.ts b/test/unit/should_honor_resolverTypes_config/codegen.config.ts index f6bf307..4c2b7d6 100644 --- a/test/unit/should_honor_resolverTypes_config/codegen.config.ts +++ b/test/unit/should_honor_resolverTypes_config/codegen.config.ts @@ -1,5 +1,18 @@ import { GraphQLKotlinCodegenConfig } from "../../../src/plugin"; export default { - resolverTypes: ["MyIncludedResolverType", "MyIncludedInterface"], + resolverTypes: [ + { + typeName: "MyResolverType", + methodType: "DEFAULT", + }, + { + typeName: "MyResolverType", + methodType: "SUSPEND", + }, + { + typeName: "MyResolverType", + methodType: "COMPLETABLE_FUTURE", + }, + ], } satisfies GraphQLKotlinCodegenConfig; diff --git a/test/unit/should_honor_resolverTypes_config/expected.kt b/test/unit/should_honor_resolverTypes_config/expected.kt index efa09f7..50fbcc1 100644 --- a/test/unit/should_honor_resolverTypes_config/expected.kt +++ b/test/unit/should_honor_resolverTypes_config/expected.kt @@ -2,32 +2,28 @@ package com.kotlin.generated import com.expediagroup.graphql.generator.annotations.* -@GraphQLIgnore -interface MyResolverType { - suspend fun nullableField(): String? = null - suspend fun nonNullableField(): String - suspend fun nullableResolver(arg: String): String? = null - suspend fun nonNullableResolver(arg: String): String -} - -@GraphQLIgnore -interface MyResolverTypeCompletableFuture { - fun nullableField(): java.util.concurrent.CompletableFuture - fun nonNullableField(): java.util.concurrent.CompletableFuture - fun nullableResolver(arg: String): java.util.concurrent.CompletableFuture - fun nonNullableResolver(arg: String): java.util.concurrent.CompletableFuture +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +open class MyResolverType { + open fun nullableField(): String? = throw NotImplementedError("MyResolverType.nullableField must be implemented.") + open fun nonNullableField(): String = throw NotImplementedError("MyResolverType.nonNullableField must be implemented.") + open fun nullableResolver(arg: String): String? = throw NotImplementedError("MyResolverType.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("MyResolverType.nonNullableResolver must be implemented.") } -@GraphQLIgnore -interface MyIncludedResolverType { - suspend fun nullableField(): String? = null - suspend fun nonNullableField(): String +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +open class MySuspendResolverType { + open suspend fun nullableField(): String? = throw NotImplementedError("MySuspendResolverType.nullableField must be implemented.") + open suspend fun nonNullableField(): String = throw NotImplementedError("MySuspendResolverType.nonNullableField must be implemented.") + open suspend fun nullableResolver(arg: String): String? = throw NotImplementedError("MySuspendResolverType.nullableResolver must be implemented.") + open suspend fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("MySuspendResolverType.nonNullableResolver must be implemented.") } -@GraphQLIgnore -interface MyIncludedResolverTypeCompletableFuture { - fun nullableField(): java.util.concurrent.CompletableFuture - fun nonNullableField(): java.util.concurrent.CompletableFuture +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +open class MyCompletableFutureResolverType { + open fun nullableField(): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nullableField must be implemented.") + open fun nonNullableField(): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableField must be implemented.") + open fun nullableResolver(arg: String): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: String): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) From ed44662814ed30cd26d4544e934c9d8d3f99d7f6 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Mon, 29 Apr 2024 20:17:42 -0500 Subject: [PATCH 03/18] remove config and finish unit tests --- src/config.ts | 27 +------------ src/definitions/object.ts | 6 +-- src/helpers/build-config-with-defaults.ts | 7 ---- src/helpers/build-field-definition.ts | 28 +++++-------- ...e.ts => should-generate-resolver-class.ts} | 4 +- .../expected.kt | 11 ++---- .../expected.kt | 16 ++++---- .../codegen.config.ts | 12 ------ .../expected.kt | 39 ------------------- .../schema.graphql | 14 ------- .../codegen.config.ts | 2 +- .../expected.kt | 26 ++++++------- .../expected.kt | 18 +++------ 13 files changed, 46 insertions(+), 164 deletions(-) rename src/helpers/{is-resolver-type.ts => should-generate-resolver-class.ts} (90%) delete mode 100644 test/unit/should_honor_extraResolverArguments_config/codegen.config.ts delete mode 100644 test/unit/should_honor_extraResolverArguments_config/expected.kt delete mode 100644 test/unit/should_honor_extraResolverArguments_config/schema.graphql diff --git a/src/config.ts b/src/config.ts index 2ee2e23..62e4769 100644 --- a/src/config.ts +++ b/src/config.ts @@ -103,34 +103,11 @@ export const configSchema = object({ ), ), /** - * Denotes types that should be generated as interfaces with suspense functions. Resolver classes can inherit from these to enforce a type contract. + * Denotes types that should be generated as classes. Resolver classes can inherit from these to enforce a type contract. * @description Two interfaces will be generated: one with suspend functions, and one with `java.util.concurrent.CompletableFuture` functions. * @example ["MyResolverType1", "MyResolverType2"] */ - resolverTypes: optional(array(string())), - /** - * Denotes extra arguments that should be added to functions on resolver classes. - * @example [{ typeNames: ["MyType", "MyType2"], argumentName: "myArgument", argumentValue: "myValue" }] - * @deprecated This will be removed in a future release now that DataFetchingEnvironment is added to functions by default. - */ - extraResolverArguments: optional( - array( - object({ - /** - * The types whose fields to add the argument to. The argument will be added to all fields on each type. If omitted, the argument will be added to all fields on all types. - */ - typeNames: optional(array(string())), - /** - * The name of the argument to add. - */ - argumentName: string(), - /** - * The type of the argument to add. - */ - argumentType: string(), - }), - ), - ), + resolverClasses: optional(array(string())), /** * Denotes the generation strategy for union types. Defaults to `MARKER_INTERFACE`. * @description The `MARKER_INTERFACE` option is highly recommended, since it is more type-safe than using annotation classes. diff --git a/src/definitions/object.ts b/src/definitions/object.ts index 4f8893e..8d6bc6a 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -25,7 +25,7 @@ import { getDependentInterfaceNames, getDependentUnionsForType, } from "../helpers/dependent-type-utils"; -import { isResolverType } from "../helpers/is-resolver-type"; +import { shouldGenerateResolverClass } from "../helpers/should-generate-resolver-class"; import { buildFieldDefinition } from "../helpers/build-field-definition"; import { isExternalField } from "../helpers/is-external-field"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; @@ -53,7 +53,7 @@ export function buildObjectTypeDefinition( : dependentInterfaces; const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`; - if (isResolverType(node, config)) { + if (shouldGenerateResolverClass(node, config)) { return `${annotations}@GraphQLIgnore\ninterface ${name}${interfaceInheritance} { ${getDataClassMembers({ node, schema, config })} } @@ -87,7 +87,7 @@ function getDataClassMembers({ config: CodegenConfigWithDefaults; completableFuture?: boolean; }) { - const resolverType = isResolverType(node, config); + const resolverType = shouldGenerateResolverClass(node, config); return node.fields ?.map((fieldNode) => { diff --git a/src/helpers/build-config-with-defaults.ts b/src/helpers/build-config-with-defaults.ts index 644ec93..7f08012 100644 --- a/src/helpers/build-config-with-defaults.ts +++ b/src/helpers/build-config-with-defaults.ts @@ -15,13 +15,6 @@ export function buildConfigWithDefaults( "com.expediagroup.graphql.generator.annotations.*", ...(config.extraImports ?? []), ], - extraResolverArguments: [ - { - argumentName: "dataFetchingEnvironment", - argumentType: "graphql.schema.DataFetchingEnvironment", - }, - ...(config.extraResolverArguments ?? []), - ], } as const satisfies GraphQLKotlinCodegenConfig; } diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index e368172..78a31cd 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -19,7 +19,7 @@ import { Kind, ObjectTypeDefinitionNode, } from "graphql"; -import { isResolverType } from "./is-resolver-type"; +import { shouldGenerateResolverClass } from "./should-generate-resolver-class"; import { isExternalField } from "./is-external-field"; import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; @@ -31,7 +31,8 @@ export function buildFieldDefinition( completableFuture?: boolean, ) { const shouldUseFunction = - isResolverType(definitionNode, config) && !isExternalField(fieldNode); + shouldGenerateResolverClass(definitionNode, config) && + !isExternalField(fieldNode); const modifier = shouldUseFunction ? completableFuture ? "fun" @@ -41,23 +42,12 @@ export function buildFieldDefinition( const typeMetadata = buildTypeMetadata(arg.type, schema, config); return `${arg.name.value}: ${typeMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; }); - const additionalFieldArguments = config.extraResolverArguments - ?.map((resolverArgument) => { - const { argumentName, argumentType } = resolverArgument; - const shouldIncludeArg = - !("typeNames" in resolverArgument) || - !resolverArgument.typeNames || - resolverArgument.typeNames.some( - (typeName) => typeName === definitionNode.name.value, - ); - return shouldUseFunction && shouldIncludeArg - ? `${argumentName}: ${argumentType}` - : undefined; - }) - .filter(Boolean); - const allFieldArguments = existingFieldArguments?.concat( - additionalFieldArguments ?? [], - ); + const dataFetchingEnvironmentArgument = + "dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment"; + const extraFieldArguments = shouldUseFunction + ? [dataFetchingEnvironmentArgument] + : []; + const allFieldArguments = existingFieldArguments?.concat(extraFieldArguments); const fieldArguments = allFieldArguments?.length ? `(${allFieldArguments?.join(", ")})` : shouldUseFunction diff --git a/src/helpers/is-resolver-type.ts b/src/helpers/should-generate-resolver-class.ts similarity index 90% rename from src/helpers/is-resolver-type.ts rename to src/helpers/should-generate-resolver-class.ts index 559ed46..8fbc891 100644 --- a/src/helpers/is-resolver-type.ts +++ b/src/helpers/should-generate-resolver-class.ts @@ -14,12 +14,12 @@ limitations under the License. import { InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode } from "graphql"; import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; -export function isResolverType( +export function shouldGenerateResolverClass( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, config: CodegenConfigWithDefaults, ) { return ( node.fields?.some((fieldNode) => fieldNode.arguments?.length) || - config.resolverTypes?.includes(node.name.value) + config.resolverClasses?.includes(node.name.value) ); } 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 366755a..441f9a6 100644 --- a/test/unit/should_consolidate_input_and_output_types/expected.kt +++ b/test/unit/should_consolidate_input_and_output_types/expected.kt @@ -68,14 +68,9 @@ data class MyTypeToConsolidateInputParent( val field: MyTypeToConsolidate? = null ) -@GraphQLIgnore -interface MyTypeToConsolidateParent2 { - suspend fun field(input: MyTypeToConsolidate, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = null -} - -@GraphQLIgnore -interface MyTypeToConsolidateParent2CompletableFuture { - fun field(input: MyTypeToConsolidate, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +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_field_resolver_interfaces/expected.kt b/test/unit/should_generate_field_resolver_interfaces/expected.kt index ae04d37..acecbe2 100644 --- a/test/unit/should_generate_field_resolver_interfaces/expected.kt +++ b/test/unit/should_generate_field_resolver_interfaces/expected.kt @@ -4,8 +4,8 @@ import com.expediagroup.graphql.generator.annotations.* @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) open class TypeWithOnlyFieldArgs { - open fun nullableResolver(arg: String): String? = throw NotImplementedError("TypeWithOnlyFieldArgs.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("TypeWithOnlyFieldArgs.nonNullableResolver must be implemented.") + 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]) @@ -13,8 +13,8 @@ open class HybridType( val nullableField: String? = null, val nonNullableField: String ) { - open fun nullableResolver(arg: String): String? = throw NotImplementedError("HybridType.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("HybridType.nonNullableResolver must be implemented.") + 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]) @@ -25,8 +25,8 @@ data class InputTypeForResolver( interface HybridInterface { val field1: String? val field2: String - fun nullableListResolver(arg1: Int?, arg2: Int): List? - fun nonNullableListResolver(arg1: Int, arg2: Int?): List + fun nullableListResolver(arg1: Int?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? + fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) @@ -38,6 +38,6 @@ open class TypeImplementingInterface( val integerField1: Int? = null, val integerField2: Int ) : HybridInterface { - override fun nullableListResolver(arg1: Int?, arg2: Int): List? = throw NotImplementedError("TypeImplementingInterface.nullableListResolver must be implemented.") - override fun nonNullableListResolver(arg1: Int, arg2: Int?): List = throw NotImplementedError("TypeImplementingInterface.nonNullableListResolver must be implemented.") + 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_extraResolverArguments_config/codegen.config.ts b/test/unit/should_honor_extraResolverArguments_config/codegen.config.ts deleted file mode 100644 index 6906094..0000000 --- a/test/unit/should_honor_extraResolverArguments_config/codegen.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { GraphQLKotlinCodegenConfig } from "../../../src/plugin"; - -export default { - resolverTypes: ["MyIncludedExtraFieldArgsType"], - extraResolverArguments: [ - { - argumentName: "myExtraFieldArg", - argumentType: "String", - typeNames: ["MyExtraFieldArgsType", "MyIncludedExtraFieldArgsType"], - }, - ], -} satisfies GraphQLKotlinCodegenConfig; diff --git a/test/unit/should_honor_extraResolverArguments_config/expected.kt b/test/unit/should_honor_extraResolverArguments_config/expected.kt deleted file mode 100644 index 64a985e..0000000 --- a/test/unit/should_honor_extraResolverArguments_config/expected.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.kotlin.generated - -import com.expediagroup.graphql.generator.annotations.* - -@GraphQLIgnore -interface MyExtraFieldArgsType { - suspend fun myField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): String? = null - suspend fun fieldWithArgs(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): String -} - -@GraphQLIgnore -interface MyExtraFieldArgsTypeCompletableFuture { - fun myField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): java.util.concurrent.CompletableFuture - fun fieldWithArgs(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): java.util.concurrent.CompletableFuture -} - -@GraphQLIgnore -interface MyIncludedExtraFieldArgsType { - suspend fun myField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): String? = null - suspend fun myOtherField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): String? = null -} - -@GraphQLIgnore -interface MyIncludedExtraFieldArgsTypeCompletableFuture { - fun myField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): java.util.concurrent.CompletableFuture - fun myOtherField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment, myExtraFieldArg: String): java.util.concurrent.CompletableFuture -} - -@GraphQLIgnore -interface MyOtherType { - suspend fun myField(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String - suspend fun myOtherField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = null -} - -@GraphQLIgnore -interface MyOtherTypeCompletableFuture { - fun myField(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture - fun myOtherField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture -} diff --git a/test/unit/should_honor_extraResolverArguments_config/schema.graphql b/test/unit/should_honor_extraResolverArguments_config/schema.graphql deleted file mode 100644 index 543b7c6..0000000 --- a/test/unit/should_honor_extraResolverArguments_config/schema.graphql +++ /dev/null @@ -1,14 +0,0 @@ -type MyExtraFieldArgsType { - myField: String - fieldWithArgs(arg: String!): String! -} - -type MyIncludedExtraFieldArgsType { - myField: String - myOtherField: String -} - -type MyOtherType { - myField(arg: String!): String! - myOtherField: String -} diff --git a/test/unit/should_honor_resolverTypes_config/codegen.config.ts b/test/unit/should_honor_resolverTypes_config/codegen.config.ts index 4c2b7d6..50a442d 100644 --- a/test/unit/should_honor_resolverTypes_config/codegen.config.ts +++ b/test/unit/should_honor_resolverTypes_config/codegen.config.ts @@ -1,7 +1,7 @@ import { GraphQLKotlinCodegenConfig } from "../../../src/plugin"; export default { - resolverTypes: [ + resolverClasses: [ { typeName: "MyResolverType", methodType: "DEFAULT", diff --git a/test/unit/should_honor_resolverTypes_config/expected.kt b/test/unit/should_honor_resolverTypes_config/expected.kt index 50fbcc1..b859f94 100644 --- a/test/unit/should_honor_resolverTypes_config/expected.kt +++ b/test/unit/should_honor_resolverTypes_config/expected.kt @@ -4,26 +4,26 @@ import com.expediagroup.graphql.generator.annotations.* @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) open class MyResolverType { - open fun nullableField(): String? = throw NotImplementedError("MyResolverType.nullableField must be implemented.") - open fun nonNullableField(): String = throw NotImplementedError("MyResolverType.nonNullableField must be implemented.") - open fun nullableResolver(arg: String): String? = throw NotImplementedError("MyResolverType.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("MyResolverType.nonNullableResolver must be implemented.") + open fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyResolverType.nullableField must be implemented.") + open fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyResolverType.nonNullableField must be implemented.") + open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyResolverType.nullableResolver must be implemented.") + open fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyResolverType.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) open class MySuspendResolverType { - open suspend fun nullableField(): String? = throw NotImplementedError("MySuspendResolverType.nullableField must be implemented.") - open suspend fun nonNullableField(): String = throw NotImplementedError("MySuspendResolverType.nonNullableField must be implemented.") - open suspend fun nullableResolver(arg: String): String? = throw NotImplementedError("MySuspendResolverType.nullableResolver must be implemented.") - open suspend fun nonNullableResolver(arg: InputTypeForResolver): String = throw NotImplementedError("MySuspendResolverType.nonNullableResolver must be implemented.") + 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: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MySuspendResolverType.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) open class MyCompletableFutureResolverType { - open fun nullableField(): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nullableField must be implemented.") - open fun nonNullableField(): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableField must be implemented.") - open fun nullableResolver(arg: String): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: String): java.util.concurrent.CompletableFuture = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableResolver must be implemented.") + 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.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) @@ -33,7 +33,7 @@ data class MyExcludedResolverType( ) interface MyIncludedInterface { - suspend fun field(): String? + suspend fun field(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? } interface MyExcludedInterface { diff --git a/test/unit/should_replace_federation_directives/expected.kt b/test/unit/should_replace_federation_directives/expected.kt index 23d18c0..0a6a4e1 100644 --- a/test/unit/should_replace_federation_directives/expected.kt +++ b/test/unit/should_replace_federation_directives/expected.kt @@ -12,18 +12,10 @@ 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")) -@GraphQLIgnore -interface FederatedTypeResolver { - suspend fun field(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String - @com.expediagroup.graphql.generator.federation.directives.ExternalDirective - val field2: String? -} - -@com.expediagroup.graphql.generator.federation.directives.ExtendsDirective -@com.expediagroup.graphql.generator.federation.directives.KeyDirective(com.expediagroup.graphql.generator.federation.directives.FieldSet("some other field")) -@GraphQLIgnore -interface FederatedTypeResolverCompletableFuture { - fun field(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +open class FederatedTypeResolver( @com.expediagroup.graphql.generator.federation.directives.ExternalDirective - val field2: java.util.concurrent.CompletableFuture + val field2: String? = null +) { + open fun field(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("FederatedTypeResolver.field must be implemented.") } From 4704604daeefab468c07a396a5245b385a17d818 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Mon, 29 Apr 2024 20:26:29 -0500 Subject: [PATCH 04/18] finish config change --- src/config.ts | 30 +++++++++++++++++-- .../codegen.config.ts | 13 ++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/config.ts b/src/config.ts index 62e4769..d7d52f9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -104,10 +104,34 @@ export const configSchema = object({ ), /** * Denotes types that should be generated as classes. Resolver classes can inherit from these to enforce a type contract. - * @description Two interfaces will be generated: one with suspend functions, and one with `java.util.concurrent.CompletableFuture` functions. - * @example ["MyResolverType1", "MyResolverType2"] + * @description Type names can be passed as strings to generate default functions. + * Also, suspend functions or `java.util.concurrent.CompletableFuture` functions can be generated per type. + * @example + * [ + * "MyResolverType", + * { + * typeName: "MySuspendResolverType", + * classMethods: "SUSPEND", + * }, + * { + * typeName: "MyCompletableFutureResolverType", + * classMethods: "COMPLETABLE_FUTURE", + * } + * ] */ - resolverClasses: optional(array(string())), + resolverClasses: optional( + array( + union([ + string(), + object({ + typeName: string(), + classMethods: optional( + union([literal("SUSPEND"), literal("COMPLETABLE_FUTURE")]), + ), + }), + ]), + ), + ), /** * Denotes the generation strategy for union types. Defaults to `MARKER_INTERFACE`. * @description The `MARKER_INTERFACE` option is highly recommended, since it is more type-safe than using annotation classes. diff --git a/test/unit/should_honor_resolverTypes_config/codegen.config.ts b/test/unit/should_honor_resolverTypes_config/codegen.config.ts index 50a442d..ea7d780 100644 --- a/test/unit/should_honor_resolverTypes_config/codegen.config.ts +++ b/test/unit/should_honor_resolverTypes_config/codegen.config.ts @@ -2,17 +2,14 @@ import { GraphQLKotlinCodegenConfig } from "../../../src/plugin"; export default { resolverClasses: [ + "MyResolverType", { - typeName: "MyResolverType", - methodType: "DEFAULT", + typeName: "MySuspendResolverType", + classMethods: "SUSPEND", }, { - typeName: "MyResolverType", - methodType: "SUSPEND", - }, - { - typeName: "MyResolverType", - methodType: "COMPLETABLE_FUTURE", + typeName: "MyCompletableFutureResolverType", + classMethods: "COMPLETABLE_FUTURE", }, ], } satisfies GraphQLKotlinCodegenConfig; From 4339380f67cbbeb881913fb89b4727f1adfde752 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Mon, 29 Apr 2024 20:48:52 -0500 Subject: [PATCH 05/18] first test case --- src/definitions/interface.ts | 7 +---- src/definitions/object.ts | 31 ++++++++++--------- src/helpers/build-field-definition.ts | 16 +++------- .../expected.kt | 0 .../schema.graphql | 0 .../codegen.config.ts | 0 .../expected.kt | 0 .../schema.graphql | 0 8 files changed, 21 insertions(+), 33 deletions(-) rename test/unit/{should_generate_field_resolver_interfaces => should_generate_classes_for_types_with_field_args}/expected.kt (100%) rename test/unit/{should_generate_field_resolver_interfaces => should_generate_classes_for_types_with_field_args}/schema.graphql (100%) rename test/unit/{should_honor_resolverTypes_config => should_honor_resolverClasses_config}/codegen.config.ts (100%) rename test/unit/{should_honor_resolverTypes_config => should_honor_resolverClasses_config}/expected.kt (100%) rename test/unit/{should_honor_resolverTypes_config => should_honor_resolverClasses_config}/schema.graphql (100%) diff --git a/src/definitions/interface.ts b/src/definitions/interface.ts index 3f54f8e..06f3f52 100644 --- a/src/definitions/interface.ts +++ b/src/definitions/interface.ts @@ -36,12 +36,7 @@ export function buildInterfaceDefinition( config, definitionNode: fieldNode, }); - const fieldDefinition = buildFieldDefinition( - fieldNode, - node, - schema, - config, - ); + const fieldDefinition = buildFieldDefinition(fieldNode, schema, config); const fieldText = indent( `${fieldDefinition}: ${typeToUse.typeName}${ typeToUse.isNullable ? "?" : "" diff --git a/src/definitions/object.ts b/src/definitions/object.ts index 8d6bc6a..dca9dcb 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -53,16 +53,6 @@ export function buildObjectTypeDefinition( : dependentInterfaces; const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`; - if (shouldGenerateResolverClass(node, config)) { - return `${annotations}@GraphQLIgnore\ninterface ${name}${interfaceInheritance} { -${getDataClassMembers({ node, schema, config })} -} - -${annotations}@GraphQLIgnore\ninterface ${name}CompletableFuture { -${getDataClassMembers({ node, schema, config, completableFuture: true })} -}`; - } - const potentialMatchingInputType = schema.getType(`${name}Input`); const typeWillBeConsolidated = isInputObjectType(potentialMatchingInputType) && @@ -71,6 +61,14 @@ ${getDataClassMembers({ node, schema, config, completableFuture: true })} const outputRestrictionAnnotation = typeWillBeConsolidated ? "" : "@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])\n"; + + const shouldGenerateFunctions = shouldGenerateResolverClass(node, config); + if (shouldGenerateFunctions) { + return `${annotations}${outputRestrictionAnnotation}open class ${name}${interfaceInheritance} { +${getDataClassMembers({ node, schema, config, shouldGenerateFunctions })} +}`; + } + return `${annotations}${outputRestrictionAnnotation}data class ${name}( ${getDataClassMembers({ node, schema, config })} )${interfaceInheritance}`; @@ -80,15 +78,15 @@ function getDataClassMembers({ node, schema, config, + shouldGenerateFunctions, completableFuture, }: { node: ObjectTypeDefinitionNode; schema: GraphQLSchema; config: CodegenConfigWithDefaults; + shouldGenerateFunctions?: boolean; completableFuture?: boolean; }) { - const resolverType = shouldGenerateResolverClass(node, config); - return node.fields ?.map((fieldNode) => { const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config); @@ -105,13 +103,16 @@ function getDataClassMembers({ }); const fieldDefinition = buildFieldDefinition( fieldNode, - node, schema, config, + shouldGenerateFunctions, completableFuture, ); const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; - const defaultDefinition = `${typeMetadata.typeName}${isExternalField(fieldNode) ? (typeMetadata.isNullable ? "?" : "") : typeMetadata.defaultValue}`; + const defaultValue = shouldGenerateFunctions + ? `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")` + : typeMetadata.defaultValue; + const defaultDefinition = `${typeMetadata.typeName}${isExternalField(fieldNode) ? (typeMetadata.isNullable ? "?" : "") : defaultValue}`; const field = indent( `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${completableFuture ? completableFutureDefinition : defaultDefinition}`, 2, @@ -123,5 +124,5 @@ function getDataClassMembers({ }); return `${annotations}${field}`; }) - .join(`${resolverType ? "" : ","}\n`); + .join(`${shouldGenerateFunctions ? "" : ","}\n`); } diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index 78a31cd..c6a3529 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -12,31 +12,23 @@ limitations under the License. */ import { buildTypeMetadata } from "./build-type-metadata"; -import { - FieldDefinitionNode, - GraphQLSchema, - InterfaceTypeDefinitionNode, - Kind, - ObjectTypeDefinitionNode, -} from "graphql"; -import { shouldGenerateResolverClass } from "./should-generate-resolver-class"; +import { FieldDefinitionNode, GraphQLSchema, Kind } from "graphql"; import { isExternalField } from "./is-external-field"; import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; export function buildFieldDefinition( fieldNode: FieldDefinitionNode, - definitionNode: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, schema: GraphQLSchema, config: CodegenConfigWithDefaults, + shouldGenerateFunctions?: boolean, completableFuture?: boolean, ) { const shouldUseFunction = - shouldGenerateResolverClass(definitionNode, config) && - !isExternalField(fieldNode); + shouldGenerateFunctions && !isExternalField(fieldNode); const modifier = shouldUseFunction ? completableFuture ? "fun" - : "suspend fun" + : "open fun" : "val"; const existingFieldArguments = fieldNode.arguments?.map((arg) => { const typeMetadata = buildTypeMetadata(arg.type, schema, config); diff --git a/test/unit/should_generate_field_resolver_interfaces/expected.kt b/test/unit/should_generate_classes_for_types_with_field_args/expected.kt similarity index 100% rename from test/unit/should_generate_field_resolver_interfaces/expected.kt rename to test/unit/should_generate_classes_for_types_with_field_args/expected.kt diff --git a/test/unit/should_generate_field_resolver_interfaces/schema.graphql b/test/unit/should_generate_classes_for_types_with_field_args/schema.graphql similarity index 100% rename from test/unit/should_generate_field_resolver_interfaces/schema.graphql rename to test/unit/should_generate_classes_for_types_with_field_args/schema.graphql diff --git a/test/unit/should_honor_resolverTypes_config/codegen.config.ts b/test/unit/should_honor_resolverClasses_config/codegen.config.ts similarity index 100% rename from test/unit/should_honor_resolverTypes_config/codegen.config.ts rename to test/unit/should_honor_resolverClasses_config/codegen.config.ts diff --git a/test/unit/should_honor_resolverTypes_config/expected.kt b/test/unit/should_honor_resolverClasses_config/expected.kt similarity index 100% rename from test/unit/should_honor_resolverTypes_config/expected.kt rename to test/unit/should_honor_resolverClasses_config/expected.kt diff --git a/test/unit/should_honor_resolverTypes_config/schema.graphql b/test/unit/should_honor_resolverClasses_config/schema.graphql similarity index 100% rename from test/unit/should_honor_resolverTypes_config/schema.graphql rename to test/unit/should_honor_resolverClasses_config/schema.graphql From 263be447aa525dad7abc9806ca13aad4875bb28a Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Mon, 29 Apr 2024 21:08:00 -0500 Subject: [PATCH 06/18] another test case --- src/definitions/object.ts | 30 ++++++++++++++++++- .../expected.kt | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/definitions/object.ts b/src/definitions/object.ts index dca9dcb..0a4e124 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -64,7 +64,32 @@ export function buildObjectTypeDefinition( const shouldGenerateFunctions = shouldGenerateResolverClass(node, config); if (shouldGenerateFunctions) { - return `${annotations}${outputRestrictionAnnotation}open class ${name}${interfaceInheritance} { + const fieldsWithNoArguments = node.fields?.filter( + (fieldNode) => !fieldNode.arguments?.length, + ); + const constructor = fieldsWithNoArguments?.length + ? `(\n${fieldsWithNoArguments + .map((fieldNode) => { + const fieldDefinition = buildFieldDefinition( + fieldNode, + schema, + config, + ); + const typeMetadata = buildTypeMetadata( + fieldNode.type, + schema, + config, + ); + + return indent( + `${fieldDefinition}: ${typeMetadata.typeName}${typeMetadata.defaultValue}`, + 2, + ); + }) + .join(",\n")}\n)` + : ""; + + return `${annotations}${outputRestrictionAnnotation}open class ${name}${constructor}${interfaceInheritance} { ${getDataClassMembers({ node, schema, config, shouldGenerateFunctions })} }`; } @@ -88,6 +113,9 @@ function getDataClassMembers({ completableFuture?: boolean; }) { return node.fields + ?.filter( + (fieldNode) => !shouldGenerateFunctions || fieldNode.arguments?.length, + ) ?.map((fieldNode) => { const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config); const shouldOverrideField = 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 acecbe2..4d0101f 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 @@ -34,7 +34,7 @@ open class TypeImplementingInterface( override val field1: String? = null, override val field2: String, val booleanField1: Boolean? = null, - val booleanField2: Boolean, + val booleanField2: Boolean = false, val integerField1: Int? = null, val integerField2: Int ) : HybridInterface { From 3f638b0e8c43a8796d0d2e439abe843e0ad105b2 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 08:23:19 -0500 Subject: [PATCH 07/18] pass test in an ugly way --- src/definitions/interface.ts | 12 +++++-- src/definitions/object.ts | 35 ++++++++++++++----- src/helpers/build-field-definition.ts | 49 +++++++++++++++++++-------- 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/src/definitions/interface.ts b/src/definitions/interface.ts index 06f3f52..2aaad51 100644 --- a/src/definitions/interface.ts +++ b/src/definitions/interface.ts @@ -16,8 +16,12 @@ import { buildAnnotations } from "../helpers/build-annotations"; import { indent } from "@graphql-codegen/visitor-plugin-common"; import { buildTypeMetadata } from "../helpers/build-type-metadata"; import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition"; -import { buildFieldDefinition } from "../helpers/build-field-definition"; +import { + buildFieldDefinition, + buildFunctionFieldDefinition, +} from "../helpers/build-field-definition"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; +import { shouldGenerateResolverClass } from "../helpers/should-generate-resolver-class"; export function buildInterfaceDefinition( node: InterfaceTypeDefinitionNode, @@ -27,6 +31,7 @@ export function buildInterfaceDefinition( if (!shouldIncludeTypeDefinition(node, config)) { return ""; } + const shouldGenerateFunctions = shouldGenerateResolverClass(node, config); const classMembers = node.fields ?.map((fieldNode) => { @@ -36,7 +41,10 @@ export function buildInterfaceDefinition( config, definitionNode: fieldNode, }); - const fieldDefinition = buildFieldDefinition(fieldNode, schema, config); + const fieldDefinition = + shouldGenerateFunctions && fieldNode.arguments?.length + ? buildFunctionFieldDefinition(node, fieldNode, schema, config) + : buildFieldDefinition(fieldNode, schema, config); const fieldText = indent( `${fieldDefinition}: ${typeToUse.typeName}${ typeToUse.isNullable ? "?" : "" diff --git a/src/definitions/object.ts b/src/definitions/object.ts index 0a4e124..3ba8ef8 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -26,7 +26,10 @@ import { getDependentUnionsForType, } from "../helpers/dependent-type-utils"; import { shouldGenerateResolverClass } from "../helpers/should-generate-resolver-class"; -import { buildFieldDefinition } from "../helpers/build-field-definition"; +import { + buildFieldDefinition, + buildFunctionFieldDefinition, +} from "../helpers/build-field-definition"; import { isExternalField } from "../helpers/is-external-field"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type"; @@ -80,9 +83,20 @@ export function buildObjectTypeDefinition( schema, config, ); + const shouldOverrideField = node.interfaces?.some( + (interfaceNode) => { + const typeNode = schema.getType(interfaceNode.name.value); + return ( + isInterfaceType(typeNode) && + typeNode.astNode?.fields?.some( + (field) => field.name.value === fieldNode.name.value, + ) + ); + }, + ); return indent( - `${fieldDefinition}: ${typeMetadata.typeName}${typeMetadata.defaultValue}`, + `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${typeMetadata.typeName}${typeMetadata.defaultValue}`, 2, ); }) @@ -129,13 +143,16 @@ function getDataClassMembers({ ) ); }); - const fieldDefinition = buildFieldDefinition( - fieldNode, - schema, - config, - shouldGenerateFunctions, - completableFuture, - ); + const fieldDefinition = shouldGenerateFunctions + ? buildFunctionFieldDefinition( + node, + fieldNode, + schema, + config, + completableFuture, + shouldOverrideField, + ) + : buildFieldDefinition(fieldNode, schema, config); const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; const defaultValue = shouldGenerateFunctions ? `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")` diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index c6a3529..f567a60 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -12,38 +12,57 @@ limitations under the License. */ import { buildTypeMetadata } from "./build-type-metadata"; -import { FieldDefinitionNode, GraphQLSchema, Kind } from "graphql"; -import { isExternalField } from "./is-external-field"; +import { + FieldDefinitionNode, + GraphQLSchema, + InterfaceTypeDefinitionNode, + Kind, + ObjectTypeDefinitionNode, +} from "graphql"; import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; export function buildFieldDefinition( fieldNode: FieldDefinitionNode, schema: GraphQLSchema, config: CodegenConfigWithDefaults, - shouldGenerateFunctions?: boolean, +) { + const modifier = "val"; + const existingFieldArguments = fieldNode.arguments?.map((arg) => { + const typeMetadata = buildTypeMetadata(arg.type, schema, config); + return `${arg.name.value}: ${typeMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; + }); + const extraFieldArguments = [] satisfies string[]; + const allFieldArguments = existingFieldArguments?.concat(extraFieldArguments); + const fieldArguments = allFieldArguments?.length + ? `(${allFieldArguments?.join(", ")})` + : ""; + return `${modifier} ${fieldNode.name.value}${fieldArguments}`; +} + +export function buildFunctionFieldDefinition( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + fieldNode: FieldDefinitionNode, + schema: GraphQLSchema, + config: CodegenConfigWithDefaults, completableFuture?: boolean, + shouldOverrideField?: boolean, ) { - const shouldUseFunction = - shouldGenerateFunctions && !isExternalField(fieldNode); - const modifier = shouldUseFunction - ? completableFuture + const modifier = + completableFuture || + node.kind === Kind.INTERFACE_TYPE_DEFINITION || + shouldOverrideField ? "fun" - : "open fun" - : "val"; + : "open fun"; const existingFieldArguments = fieldNode.arguments?.map((arg) => { const typeMetadata = buildTypeMetadata(arg.type, schema, config); return `${arg.name.value}: ${typeMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; }); const dataFetchingEnvironmentArgument = "dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment"; - const extraFieldArguments = shouldUseFunction - ? [dataFetchingEnvironmentArgument] - : []; + const extraFieldArguments = [dataFetchingEnvironmentArgument]; const allFieldArguments = existingFieldArguments?.concat(extraFieldArguments); const fieldArguments = allFieldArguments?.length ? `(${allFieldArguments?.join(", ")})` - : shouldUseFunction - ? "()" - : ""; + : "()"; return `${modifier} ${fieldNode.name.value}${fieldArguments}`; } From fa1a8a7caf9c4b428413a346214cc3698b594e68 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 08:38:28 -0500 Subject: [PATCH 08/18] pass another test --- src/definitions/interface.ts | 2 +- src/definitions/object.ts | 12 +++++++++--- src/helpers/build-field-definition.ts | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/definitions/interface.ts b/src/definitions/interface.ts index 2aaad51..fa87765 100644 --- a/src/definitions/interface.ts +++ b/src/definitions/interface.ts @@ -44,7 +44,7 @@ export function buildInterfaceDefinition( const fieldDefinition = shouldGenerateFunctions && fieldNode.arguments?.length ? buildFunctionFieldDefinition(node, fieldNode, schema, config) - : buildFieldDefinition(fieldNode, schema, config); + : buildFieldDefinition(node, fieldNode, schema, config); const fieldText = indent( `${fieldDefinition}: ${typeToUse.typeName}${ typeToUse.isNullable ? "?" : "" diff --git a/src/definitions/object.ts b/src/definitions/object.ts index 3ba8ef8..a36a156 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -74,6 +74,7 @@ export function buildObjectTypeDefinition( ? `(\n${fieldsWithNoArguments .map((fieldNode) => { const fieldDefinition = buildFieldDefinition( + node, fieldNode, schema, config, @@ -94,11 +95,16 @@ export function buildObjectTypeDefinition( ); }, ); - - return indent( + const annotations = buildAnnotations({ + config, + definitionNode: fieldNode, + typeMetadata, + }); + const field = indent( `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${typeMetadata.typeName}${typeMetadata.defaultValue}`, 2, ); + return `${annotations}${field}`; }) .join(",\n")}\n)` : ""; @@ -152,7 +158,7 @@ function getDataClassMembers({ completableFuture, shouldOverrideField, ) - : buildFieldDefinition(fieldNode, schema, config); + : buildFieldDefinition(node, fieldNode, schema, config); const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; const defaultValue = shouldGenerateFunctions ? `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")` diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index f567a60..1fe564f 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -22,6 +22,7 @@ import { import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; export function buildFieldDefinition( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, fieldNode: FieldDefinitionNode, schema: GraphQLSchema, config: CodegenConfigWithDefaults, From 7b1a3e58afc9e34639d336e32fa8e1ae1be32d06 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 08:50:47 -0500 Subject: [PATCH 09/18] refactor a little --- src/definitions/interface.ts | 25 ++++----- src/definitions/object.ts | 63 +++------------------- src/helpers/build-field-definition.ts | 78 +++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 74 deletions(-) diff --git a/src/definitions/interface.ts b/src/definitions/interface.ts index fa87765..86a5499 100644 --- a/src/definitions/interface.ts +++ b/src/definitions/interface.ts @@ -13,7 +13,6 @@ limitations under the License. import { GraphQLSchema, InterfaceTypeDefinitionNode } from "graphql"; import { buildAnnotations } from "../helpers/build-annotations"; -import { indent } from "@graphql-codegen/visitor-plugin-common"; import { buildTypeMetadata } from "../helpers/build-type-metadata"; import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition"; import { @@ -35,23 +34,19 @@ export function buildInterfaceDefinition( const classMembers = node.fields ?.map((fieldNode) => { - const typeToUse = buildTypeMetadata(fieldNode.type, schema, config); + const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config); - const annotations = buildAnnotations({ - config, - definitionNode: fieldNode, - }); const fieldDefinition = shouldGenerateFunctions && fieldNode.arguments?.length - ? buildFunctionFieldDefinition(node, fieldNode, schema, config) - : buildFieldDefinition(node, fieldNode, schema, config); - const fieldText = indent( - `${fieldDefinition}: ${typeToUse.typeName}${ - typeToUse.isNullable ? "?" : "" - }`, - 2, - ); - return `${annotations}${fieldText}`; + ? buildFunctionFieldDefinition( + node, + fieldNode, + schema, + config, + typeMetadata, + ) + : buildFieldDefinition(node, fieldNode, schema, config, typeMetadata); + return fieldDefinition; }) .join("\n"); diff --git a/src/definitions/object.ts b/src/definitions/object.ts index a36a156..d514f80 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -14,11 +14,9 @@ limitations under the License. import { GraphQLSchema, isInputObjectType, - isInterfaceType, ObjectTypeDefinitionNode, } from "graphql"; import { buildAnnotations } from "../helpers/build-annotations"; -import { indent } from "@graphql-codegen/visitor-plugin-common"; import { buildTypeMetadata } from "../helpers/build-type-metadata"; import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition"; import { @@ -30,7 +28,6 @@ import { buildFieldDefinition, buildFunctionFieldDefinition, } from "../helpers/build-field-definition"; -import { isExternalField } from "../helpers/is-external-field"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type"; @@ -73,38 +70,18 @@ export function buildObjectTypeDefinition( const constructor = fieldsWithNoArguments?.length ? `(\n${fieldsWithNoArguments .map((fieldNode) => { - const fieldDefinition = buildFieldDefinition( - node, - fieldNode, - schema, - config, - ); const typeMetadata = buildTypeMetadata( fieldNode.type, schema, config, ); - const shouldOverrideField = node.interfaces?.some( - (interfaceNode) => { - const typeNode = schema.getType(interfaceNode.name.value); - return ( - isInterfaceType(typeNode) && - typeNode.astNode?.fields?.some( - (field) => field.name.value === fieldNode.name.value, - ) - ); - }, - ); - const annotations = buildAnnotations({ + return buildFieldDefinition( + node, + fieldNode, + schema, config, - definitionNode: fieldNode, typeMetadata, - }); - const field = indent( - `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${typeMetadata.typeName}${typeMetadata.defaultValue}`, - 2, ); - return `${annotations}${field}`; }) .join(",\n")}\n)` : ""; @@ -138,42 +115,18 @@ function getDataClassMembers({ ) ?.map((fieldNode) => { const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config); - const shouldOverrideField = - !completableFuture && - node.interfaces?.some((interfaceNode) => { - const typeNode = schema.getType(interfaceNode.name.value); - return ( - isInterfaceType(typeNode) && - typeNode.astNode?.fields?.some( - (field) => field.name.value === fieldNode.name.value, - ) - ); - }); const fieldDefinition = shouldGenerateFunctions ? buildFunctionFieldDefinition( node, fieldNode, schema, config, + typeMetadata, completableFuture, - shouldOverrideField, ) - : buildFieldDefinition(node, fieldNode, schema, config); - const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; - const defaultValue = shouldGenerateFunctions - ? `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")` - : typeMetadata.defaultValue; - const defaultDefinition = `${typeMetadata.typeName}${isExternalField(fieldNode) ? (typeMetadata.isNullable ? "?" : "") : defaultValue}`; - const field = indent( - `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${completableFuture ? completableFutureDefinition : defaultDefinition}`, - 2, - ); - const annotations = buildAnnotations({ - config, - definitionNode: fieldNode, - typeMetadata, - }); - return `${annotations}${field}`; + : buildFieldDefinition(node, fieldNode, schema, config, typeMetadata); + + return fieldDefinition; }) .join(`${shouldGenerateFunctions ? "" : ","}\n`); } diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index 1fe564f..7f3cac3 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -11,21 +11,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { buildTypeMetadata } from "./build-type-metadata"; +import { buildTypeMetadata, TypeMetadata } from "./build-type-metadata"; import { FieldDefinitionNode, GraphQLSchema, InterfaceTypeDefinitionNode, Kind, ObjectTypeDefinitionNode, + isInterfaceType, } from "graphql"; import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; +import { isExternalField } from "./is-external-field"; +import { indent } from "@graphql-codegen/visitor-plugin-common"; +import { buildAnnotations } from "./build-annotations"; export function buildFieldDefinition( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, fieldNode: FieldDefinitionNode, schema: GraphQLSchema, config: CodegenConfigWithDefaults, + typeMetadata: TypeMetadata, ) { const modifier = "val"; const existingFieldArguments = fieldNode.arguments?.map((arg) => { @@ -37,7 +42,38 @@ export function buildFieldDefinition( const fieldArguments = allFieldArguments?.length ? `(${allFieldArguments?.join(", ")})` : ""; - return `${modifier} ${fieldNode.name.value}${fieldArguments}`; + const fieldDefinition = `${modifier} ${fieldNode.name.value}${fieldArguments}`; + const annotations = buildAnnotations({ + config, + definitionNode: fieldNode, + typeMetadata, + }); + if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) { + const fieldText = indent( + `${fieldDefinition}: ${typeMetadata.typeName}${ + typeMetadata.isNullable ? "?" : "" + }`, + 2, + ); + return `${annotations}${fieldText}`; + } + // const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; + const defaultValue = typeMetadata.defaultValue; + const defaultDefinition = `${typeMetadata.typeName}${isExternalField(fieldNode) ? (typeMetadata.isNullable ? "?" : "") : defaultValue}`; + const shouldOverrideField = node.interfaces?.some((interfaceNode) => { + const typeNode = schema.getType(interfaceNode.name.value); + return ( + isInterfaceType(typeNode) && + typeNode.astNode?.fields?.some( + (field) => field.name.value === fieldNode.name.value, + ) + ); + }); + const field = indent( + `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${defaultDefinition}`, + 2, + ); + return `${annotations}${field}`; } export function buildFunctionFieldDefinition( @@ -45,9 +81,20 @@ export function buildFunctionFieldDefinition( fieldNode: FieldDefinitionNode, schema: GraphQLSchema, config: CodegenConfigWithDefaults, + typeMetadata: TypeMetadata, completableFuture?: boolean, - shouldOverrideField?: boolean, ) { + const shouldOverrideField = + !completableFuture && + node.interfaces?.some((interfaceNode) => { + const typeNode = schema.getType(interfaceNode.name.value); + return ( + isInterfaceType(typeNode) && + typeNode.astNode?.fields?.some( + (field) => field.name.value === fieldNode.name.value, + ) + ); + }); const modifier = completableFuture || node.kind === Kind.INTERFACE_TYPE_DEFINITION || @@ -65,5 +112,28 @@ export function buildFunctionFieldDefinition( const fieldArguments = allFieldArguments?.length ? `(${allFieldArguments?.join(", ")})` : "()"; - return `${modifier} ${fieldNode.name.value}${fieldArguments}`; + const fieldDefinition = `${modifier} ${fieldNode.name.value}${fieldArguments}`; + const annotations = buildAnnotations({ + config, + definitionNode: fieldNode, + typeMetadata, + }); + if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) { + const fieldText = indent( + `${fieldDefinition}: ${typeMetadata.typeName}${ + typeMetadata.isNullable ? "?" : "" + }`, + 2, + ); + return `${annotations}${fieldText}`; + } + + const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; + const defaultValue = `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`; + const defaultDefinition = `${typeMetadata.typeName}${isExternalField(fieldNode) ? (typeMetadata.isNullable ? "?" : "") : defaultValue}`; + const field = indent( + `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${completableFuture ? completableFutureDefinition : defaultDefinition}`, + 2, + ); + return `${annotations}${field}`; } From 3f0459d230038473fbc45caa4bce36ee65ad572e Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 09:02:06 -0500 Subject: [PATCH 10/18] refactor a little --- src/definitions/interface.ts | 27 ++--- src/definitions/object.ts | 27 ++--- src/helpers/build-field-definition.ts | 142 +++++++++++++------------- src/helpers/is-external-field.ts | 20 ---- 4 files changed, 87 insertions(+), 129 deletions(-) delete mode 100644 src/helpers/is-external-field.ts diff --git a/src/definitions/interface.ts b/src/definitions/interface.ts index 86a5499..419634c 100644 --- a/src/definitions/interface.ts +++ b/src/definitions/interface.ts @@ -15,12 +15,8 @@ import { GraphQLSchema, InterfaceTypeDefinitionNode } from "graphql"; import { buildAnnotations } from "../helpers/build-annotations"; import { buildTypeMetadata } from "../helpers/build-type-metadata"; import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition"; -import { - buildFieldDefinition, - buildFunctionFieldDefinition, -} from "../helpers/build-field-definition"; +import { buildFieldDefinition } from "../helpers/build-field-definition"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; -import { shouldGenerateResolverClass } from "../helpers/should-generate-resolver-class"; export function buildInterfaceDefinition( node: InterfaceTypeDefinitionNode, @@ -30,23 +26,18 @@ export function buildInterfaceDefinition( if (!shouldIncludeTypeDefinition(node, config)) { return ""; } - const shouldGenerateFunctions = shouldGenerateResolverClass(node, config); const classMembers = node.fields ?.map((fieldNode) => { const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config); - - const fieldDefinition = - shouldGenerateFunctions && fieldNode.arguments?.length - ? buildFunctionFieldDefinition( - node, - fieldNode, - schema, - config, - typeMetadata, - ) - : buildFieldDefinition(node, fieldNode, schema, config, typeMetadata); - return fieldDefinition; + return buildFieldDefinition( + node, + fieldNode, + schema, + config, + typeMetadata, + Boolean(fieldNode.arguments?.length), + ); }) .join("\n"); diff --git a/src/definitions/object.ts b/src/definitions/object.ts index d514f80..ddafd66 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -24,10 +24,7 @@ import { getDependentUnionsForType, } from "../helpers/dependent-type-utils"; import { shouldGenerateResolverClass } from "../helpers/should-generate-resolver-class"; -import { - buildFieldDefinition, - buildFunctionFieldDefinition, -} from "../helpers/build-field-definition"; +import { buildFieldDefinition } from "../helpers/build-field-definition"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type"; @@ -101,13 +98,11 @@ function getDataClassMembers({ schema, config, shouldGenerateFunctions, - completableFuture, }: { node: ObjectTypeDefinitionNode; schema: GraphQLSchema; config: CodegenConfigWithDefaults; shouldGenerateFunctions?: boolean; - completableFuture?: boolean; }) { return node.fields ?.filter( @@ -115,18 +110,14 @@ function getDataClassMembers({ ) ?.map((fieldNode) => { const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config); - const fieldDefinition = shouldGenerateFunctions - ? buildFunctionFieldDefinition( - node, - fieldNode, - schema, - config, - typeMetadata, - completableFuture, - ) - : buildFieldDefinition(node, fieldNode, schema, config, typeMetadata); - - return fieldDefinition; + return buildFieldDefinition( + node, + fieldNode, + schema, + config, + typeMetadata, + shouldGenerateFunctions, + ); }) .join(`${shouldGenerateFunctions ? "" : ","}\n`); } diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index 7f3cac3..7cfe59b 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -21,7 +21,6 @@ import { isInterfaceType, } from "graphql"; import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; -import { isExternalField } from "./is-external-field"; import { indent } from "@graphql-codegen/visitor-plugin-common"; import { buildAnnotations } from "./build-annotations"; @@ -31,17 +30,10 @@ export function buildFieldDefinition( schema: GraphQLSchema, config: CodegenConfigWithDefaults, typeMetadata: TypeMetadata, + shouldUseFunction?: boolean, ) { - const modifier = "val"; - const existingFieldArguments = fieldNode.arguments?.map((arg) => { - const typeMetadata = buildTypeMetadata(arg.type, schema, config); - return `${arg.name.value}: ${typeMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; - }); - const extraFieldArguments = [] satisfies string[]; - const allFieldArguments = existingFieldArguments?.concat(extraFieldArguments); - const fieldArguments = allFieldArguments?.length - ? `(${allFieldArguments?.join(", ")})` - : ""; + const modifier = buildFieldModifier(node, fieldNode, schema); + const fieldArguments = buildFieldArguments(fieldNode, schema, config); const fieldDefinition = `${modifier} ${fieldNode.name.value}${fieldArguments}`; const annotations = buildAnnotations({ config, @@ -49,18 +41,54 @@ export function buildFieldDefinition( typeMetadata, }); if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) { - const fieldText = indent( - `${fieldDefinition}: ${typeMetadata.typeName}${ - typeMetadata.isNullable ? "?" : "" - }`, - 2, + return buildInterfaceFieldDefinition( + fieldDefinition, + typeMetadata, + annotations, ); - return `${annotations}${fieldText}`; } - // const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; - const defaultValue = typeMetadata.defaultValue; - const defaultDefinition = `${typeMetadata.typeName}${isExternalField(fieldNode) ? (typeMetadata.isNullable ? "?" : "") : defaultValue}`; - const shouldOverrideField = node.interfaces?.some((interfaceNode) => { + + const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`; + const defaultValue = shouldUseFunction + ? defaultFunctionValue + : typeMetadata.defaultValue; + const defaultDefinition = `${typeMetadata.typeName}${defaultValue}`; + const completableFuture = false; + const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; + const field = indent( + `${fieldDefinition}: ${completableFuture ? completableFutureDefinition : defaultDefinition}`, + 2, + ); + return `${annotations}${field}`; +} + +function buildFieldModifier( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + fieldNode: FieldDefinitionNode, + schema: GraphQLSchema, +) { + const completableFuture = false; + const shouldOverrideField = + !completableFuture && + shouldModifyFieldWithOverride(node, fieldNode, schema); + if (!fieldNode.arguments?.length) { + return shouldOverrideField ? "override val" : "val"; + } + if (completableFuture || node.kind === Kind.INTERFACE_TYPE_DEFINITION) { + return "fun"; + } + if (shouldOverrideField) { + return "override fun"; + } + return "open fun"; +} + +function shouldModifyFieldWithOverride( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + fieldNode: FieldDefinitionNode, + schema: GraphQLSchema, +) { + return node.interfaces?.some((interfaceNode) => { const typeNode = schema.getType(interfaceNode.name.value); return ( isInterfaceType(typeNode) && @@ -69,71 +97,39 @@ export function buildFieldDefinition( ) ); }); - const field = indent( - `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${defaultDefinition}`, +} + +function buildInterfaceFieldDefinition( + fieldDefinition: string, + typeMetadata: TypeMetadata, + annotations: string, +) { + const fieldText = indent( + `${fieldDefinition}: ${typeMetadata.typeName}${ + typeMetadata.isNullable ? "?" : "" + }`, 2, ); - return `${annotations}${field}`; + return `${annotations}${fieldText}`; } -export function buildFunctionFieldDefinition( - node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, +function buildFieldArguments( fieldNode: FieldDefinitionNode, schema: GraphQLSchema, config: CodegenConfigWithDefaults, - typeMetadata: TypeMetadata, - completableFuture?: boolean, ) { - const shouldOverrideField = - !completableFuture && - node.interfaces?.some((interfaceNode) => { - const typeNode = schema.getType(interfaceNode.name.value); - return ( - isInterfaceType(typeNode) && - typeNode.astNode?.fields?.some( - (field) => field.name.value === fieldNode.name.value, - ) - ); - }); - const modifier = - completableFuture || - node.kind === Kind.INTERFACE_TYPE_DEFINITION || - shouldOverrideField - ? "fun" - : "open fun"; - const existingFieldArguments = fieldNode.arguments?.map((arg) => { - const typeMetadata = buildTypeMetadata(arg.type, schema, config); - return `${arg.name.value}: ${typeMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; + if (!fieldNode.arguments?.length) { + return ""; + } + const existingFieldArguments = fieldNode.arguments.map((arg) => { + const argMetadata = buildTypeMetadata(arg.type, schema, config); + return `${arg.name.value}: ${argMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; }); const dataFetchingEnvironmentArgument = "dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment"; const extraFieldArguments = [dataFetchingEnvironmentArgument]; const allFieldArguments = existingFieldArguments?.concat(extraFieldArguments); - const fieldArguments = allFieldArguments?.length + return allFieldArguments?.length ? `(${allFieldArguments?.join(", ")})` : "()"; - const fieldDefinition = `${modifier} ${fieldNode.name.value}${fieldArguments}`; - const annotations = buildAnnotations({ - config, - definitionNode: fieldNode, - typeMetadata, - }); - if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) { - const fieldText = indent( - `${fieldDefinition}: ${typeMetadata.typeName}${ - typeMetadata.isNullable ? "?" : "" - }`, - 2, - ); - return `${annotations}${fieldText}`; - } - - const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; - const defaultValue = `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`; - const defaultDefinition = `${typeMetadata.typeName}${isExternalField(fieldNode) ? (typeMetadata.isNullable ? "?" : "") : defaultValue}`; - const field = indent( - `${shouldOverrideField ? "override " : ""}${fieldDefinition}: ${completableFuture ? completableFutureDefinition : defaultDefinition}`, - 2, - ); - return `${annotations}${field}`; } diff --git a/src/helpers/is-external-field.ts b/src/helpers/is-external-field.ts deleted file mode 100644 index 3f75171..0000000 --- a/src/helpers/is-external-field.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2024 Expedia, Inc. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { FieldDefinitionNode } from "graphql"; - -export function isExternalField(fieldNode: FieldDefinitionNode) { - return fieldNode.directives?.some( - (directive) => directive.name.value === "external", - ); -} From 564f22a2c5cc62db5a27d9520ad9e025653b92e9 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 10:14:04 -0500 Subject: [PATCH 11/18] first case --- src/definitions/object.ts | 61 +++++++++++-------- src/helpers/build-field-definition.ts | 56 +++++++++-------- src/helpers/should-generate-resolver-class.ts | 25 -------- .../expected.kt | 4 +- 4 files changed, 70 insertions(+), 76 deletions(-) delete mode 100644 src/helpers/should-generate-resolver-class.ts diff --git a/src/definitions/object.ts b/src/definitions/object.ts index ddafd66..a0a0744 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -12,6 +12,7 @@ limitations under the License. */ import { + FieldDefinitionNode, GraphQLSchema, isInputObjectType, ObjectTypeDefinitionNode, @@ -23,7 +24,6 @@ import { getDependentInterfaceNames, getDependentUnionsForType, } from "../helpers/dependent-type-utils"; -import { shouldGenerateResolverClass } from "../helpers/should-generate-resolver-class"; import { buildFieldDefinition } from "../helpers/build-field-definition"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type"; @@ -59,32 +59,44 @@ export function buildObjectTypeDefinition( ? "" : "@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])\n"; - const shouldGenerateFunctions = shouldGenerateResolverClass(node, config); + const shouldGenerateFunctions = node.fields?.some( + (fieldNode) => fieldNode.arguments?.length, + ); if (shouldGenerateFunctions) { const fieldsWithNoArguments = node.fields?.filter( (fieldNode) => !fieldNode.arguments?.length, ); - const constructor = fieldsWithNoArguments?.length - ? `(\n${fieldsWithNoArguments - .map((fieldNode) => { - const typeMetadata = buildTypeMetadata( - fieldNode.type, - schema, - config, - ); - return buildFieldDefinition( - node, - fieldNode, - schema, - config, - typeMetadata, - ); - }) - .join(",\n")}\n)` - : ""; + const resolverClassesContainsType = config.resolverClasses?.includes( + node.name.value, + ); + const constructor = + !resolverClassesContainsType && fieldsWithNoArguments?.length + ? `(\n${fieldsWithNoArguments + .map((fieldNode) => { + const typeMetadata = buildTypeMetadata( + fieldNode.type, + schema, + config, + ); + return buildFieldDefinition( + node, + fieldNode, + schema, + config, + typeMetadata, + ); + }) + .join(",\n")}\n)` + : ""; + const fieldsWithArguments = node.fields?.filter( + (fieldNode) => fieldNode.arguments?.length, + ); + const fieldNodes = resolverClassesContainsType + ? node.fields + : fieldsWithArguments; return `${annotations}${outputRestrictionAnnotation}open class ${name}${constructor}${interfaceInheritance} { -${getDataClassMembers({ node, schema, config, shouldGenerateFunctions })} +${getDataClassMembers({ node, fieldNodes, schema, config, shouldGenerateFunctions })} }`; } @@ -95,19 +107,18 @@ ${getDataClassMembers({ node, schema, config })} function getDataClassMembers({ node, + fieldNodes, schema, config, shouldGenerateFunctions, }: { node: ObjectTypeDefinitionNode; + fieldNodes?: readonly FieldDefinitionNode[]; schema: GraphQLSchema; config: CodegenConfigWithDefaults; shouldGenerateFunctions?: boolean; }) { - return node.fields - ?.filter( - (fieldNode) => !shouldGenerateFunctions || fieldNode.arguments?.length, - ) + return (fieldNodes ?? node.fields) ?.map((fieldNode) => { const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config); return buildFieldDefinition( diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index 7cfe59b..935f2a8 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -32,8 +32,8 @@ export function buildFieldDefinition( typeMetadata: TypeMetadata, shouldUseFunction?: boolean, ) { - const modifier = buildFieldModifier(node, fieldNode, schema); - const fieldArguments = buildFieldArguments(fieldNode, schema, config); + const modifier = buildFieldModifier(node, fieldNode, schema, config); + const fieldArguments = buildFieldArguments(node, fieldNode, schema, config); const fieldDefinition = `${modifier} ${fieldNode.name.value}${fieldArguments}`; const annotations = buildAnnotations({ config, @@ -66,12 +66,16 @@ function buildFieldModifier( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, fieldNode: FieldDefinitionNode, schema: GraphQLSchema, + config: CodegenConfigWithDefaults, ) { + const resolverClassesContainsType = config.resolverClasses?.includes( + node.name.value, + ); const completableFuture = false; const shouldOverrideField = !completableFuture && shouldModifyFieldWithOverride(node, fieldNode, schema); - if (!fieldNode.arguments?.length) { + if (!resolverClassesContainsType && !fieldNode.arguments?.length) { return shouldOverrideField ? "override val" : "val"; } if (completableFuture || node.kind === Kind.INTERFACE_TYPE_DEFINITION) { @@ -83,6 +87,31 @@ function buildFieldModifier( return "open fun"; } +function buildFieldArguments( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + fieldNode: FieldDefinitionNode, + schema: GraphQLSchema, + config: CodegenConfigWithDefaults, +) { + const resolverClassesContainsType = config.resolverClasses?.includes( + node.name.value, + ); + if (!resolverClassesContainsType && !fieldNode.arguments?.length) { + return ""; + } + const existingFieldArguments = fieldNode.arguments?.map((arg) => { + const argMetadata = buildTypeMetadata(arg.type, schema, config); + return `${arg.name.value}: ${argMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; + }); + const dataFetchingEnvironmentArgument = + "dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment"; + const extraFieldArguments = [dataFetchingEnvironmentArgument]; + const allFieldArguments = existingFieldArguments?.concat(extraFieldArguments); + return allFieldArguments?.length + ? `(${allFieldArguments?.join(", ")})` + : "()"; +} + function shouldModifyFieldWithOverride( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, fieldNode: FieldDefinitionNode, @@ -112,24 +141,3 @@ function buildInterfaceFieldDefinition( ); return `${annotations}${fieldText}`; } - -function buildFieldArguments( - fieldNode: FieldDefinitionNode, - schema: GraphQLSchema, - config: CodegenConfigWithDefaults, -) { - if (!fieldNode.arguments?.length) { - return ""; - } - const existingFieldArguments = fieldNode.arguments.map((arg) => { - const argMetadata = buildTypeMetadata(arg.type, schema, config); - return `${arg.name.value}: ${argMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : "?"}`; - }); - const dataFetchingEnvironmentArgument = - "dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment"; - const extraFieldArguments = [dataFetchingEnvironmentArgument]; - const allFieldArguments = existingFieldArguments?.concat(extraFieldArguments); - return allFieldArguments?.length - ? `(${allFieldArguments?.join(", ")})` - : "()"; -} diff --git a/src/helpers/should-generate-resolver-class.ts b/src/helpers/should-generate-resolver-class.ts deleted file mode 100644 index 8fbc891..0000000 --- a/src/helpers/should-generate-resolver-class.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 Expedia, Inc. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode } from "graphql"; -import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; - -export function shouldGenerateResolverClass( - node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - config: CodegenConfigWithDefaults, -) { - return ( - node.fields?.some((fieldNode) => fieldNode.arguments?.length) || - config.resolverClasses?.includes(node.name.value) - ); -} diff --git a/test/unit/should_honor_resolverClasses_config/expected.kt b/test/unit/should_honor_resolverClasses_config/expected.kt index b859f94..6469e24 100644 --- a/test/unit/should_honor_resolverClasses_config/expected.kt +++ b/test/unit/should_honor_resolverClasses_config/expected.kt @@ -7,7 +7,7 @@ open class MyResolverType { open fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyResolverType.nullableField must be implemented.") open fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyResolverType.nonNullableField must be implemented.") open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyResolverType.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyResolverType.nonNullableResolver must be implemented.") + open fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyResolverType.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) @@ -15,7 +15,7 @@ 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: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MySuspendResolverType.nonNullableResolver must be implemented.") + open suspend fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MySuspendResolverType.nonNullableResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) From 265ffcee20c156d5a02fe27020a3030e53092243 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 10:39:00 -0500 Subject: [PATCH 12/18] pass all tests --- src/config.ts | 23 +++++---- src/definitions/object.ts | 12 +++-- src/helpers/build-field-definition.ts | 49 ++++++++++++------- .../findTypeInResolverClassesConfig.ts | 11 +++++ .../codegen.config.ts | 11 ++++- .../expected.kt | 20 ++++++-- .../schema.graphql | 23 ++++++++- 7 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 src/helpers/findTypeInResolverClassesConfig.ts diff --git a/src/config.ts b/src/config.ts index d7d52f9..7bfa079 100644 --- a/src/config.ts +++ b/src/config.ts @@ -104,11 +104,13 @@ export const configSchema = object({ ), /** * Denotes types that should be generated as classes. Resolver classes can inherit from these to enforce a type contract. - * @description Type names can be passed as strings to generate default functions. - * Also, suspend functions or `java.util.concurrent.CompletableFuture` functions can be generated per type. + * @description Type names can be optionally passed with the classMethods config to generate suspend functions or + * `java.util.concurrent.CompletableFuture` functions. * @example * [ - * "MyResolverType", + * { + * typeName: "MyResolverType", + * }, * { * typeName: "MySuspendResolverType", * classMethods: "SUSPEND", @@ -121,15 +123,12 @@ export const configSchema = object({ */ resolverClasses: optional( array( - union([ - string(), - object({ - typeName: string(), - classMethods: optional( - union([literal("SUSPEND"), literal("COMPLETABLE_FUTURE")]), - ), - }), - ]), + object({ + typeName: string(), + classMethods: optional( + union([literal("SUSPEND"), literal("COMPLETABLE_FUTURE")]), + ), + }), ), ), /** diff --git a/src/definitions/object.ts b/src/definitions/object.ts index a0a0744..20361ae 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -27,6 +27,7 @@ import { import { buildFieldDefinition } from "../helpers/build-field-definition"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type"; +import { findTypeInResolverClassesConfig } from "../helpers/findTypeInResolverClassesConfig"; export function buildObjectTypeDefinition( node: ObjectTypeDefinitionNode, @@ -62,15 +63,16 @@ export function buildObjectTypeDefinition( const shouldGenerateFunctions = node.fields?.some( (fieldNode) => fieldNode.arguments?.length, ); + const typeInResolverClassesConfig = findTypeInResolverClassesConfig( + node, + config, + ); if (shouldGenerateFunctions) { const fieldsWithNoArguments = node.fields?.filter( (fieldNode) => !fieldNode.arguments?.length, ); - const resolverClassesContainsType = config.resolverClasses?.includes( - node.name.value, - ); const constructor = - !resolverClassesContainsType && fieldsWithNoArguments?.length + !typeInResolverClassesConfig && fieldsWithNoArguments?.length ? `(\n${fieldsWithNoArguments .map((fieldNode) => { const typeMetadata = buildTypeMetadata( @@ -92,7 +94,7 @@ export function buildObjectTypeDefinition( const fieldsWithArguments = node.fields?.filter( (fieldNode) => fieldNode.arguments?.length, ); - const fieldNodes = resolverClassesContainsType + const fieldNodes = typeInResolverClassesConfig ? node.fields : fieldsWithArguments; return `${annotations}${outputRestrictionAnnotation}open class ${name}${constructor}${interfaceInheritance} { diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index 935f2a8..2f15074 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -23,6 +23,7 @@ import { import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; import { indent } from "@graphql-codegen/visitor-plugin-common"; import { buildAnnotations } from "./build-annotations"; +import { findTypeInResolverClassesConfig } from "./findTypeInResolverClassesConfig"; export function buildFieldDefinition( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, @@ -48,15 +49,21 @@ export function buildFieldDefinition( ); } - const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""} = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`; + const notImplementedError = ` = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`; + const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""}${notImplementedError}`; const defaultValue = shouldUseFunction ? defaultFunctionValue : typeMetadata.defaultValue; const defaultDefinition = `${typeMetadata.typeName}${defaultValue}`; - const completableFuture = false; - const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`; + const typeInResolverClassesConfig = findTypeInResolverClassesConfig( + node, + config, + ); + const isCompletableFuture = + typeInResolverClassesConfig?.classMethods === "COMPLETABLE_FUTURE"; + const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>${notImplementedError}`; const field = indent( - `${fieldDefinition}: ${completableFuture ? completableFutureDefinition : defaultDefinition}`, + `${fieldDefinition}: ${isCompletableFuture ? completableFutureDefinition : defaultDefinition}`, 2, ); return `${annotations}${field}`; @@ -68,23 +75,29 @@ function buildFieldModifier( schema: GraphQLSchema, config: CodegenConfigWithDefaults, ) { - const resolverClassesContainsType = config.resolverClasses?.includes( - node.name.value, + const typeInResolverClassesConfig = findTypeInResolverClassesConfig( + node, + config, ); - const completableFuture = false; - const shouldOverrideField = - !completableFuture && - shouldModifyFieldWithOverride(node, fieldNode, schema); - if (!resolverClassesContainsType && !fieldNode.arguments?.length) { + const shouldOverrideField = shouldModifyFieldWithOverride( + node, + fieldNode, + schema, + ); + if (!typeInResolverClassesConfig && !fieldNode.arguments?.length) { return shouldOverrideField ? "override val" : "val"; } - if (completableFuture || node.kind === Kind.INTERFACE_TYPE_DEFINITION) { - return "fun"; + const functionModifier = + typeInResolverClassesConfig?.classMethods === "SUSPEND" ? "suspend " : ""; + if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) { + return `${functionModifier}fun`; } - if (shouldOverrideField) { + const isCompletableFuture = + typeInResolverClassesConfig?.classMethods === "COMPLETABLE_FUTURE"; + if (shouldOverrideField && !isCompletableFuture) { return "override fun"; } - return "open fun"; + return `open ${functionModifier}fun`; } function buildFieldArguments( @@ -93,10 +106,8 @@ function buildFieldArguments( schema: GraphQLSchema, config: CodegenConfigWithDefaults, ) { - const resolverClassesContainsType = config.resolverClasses?.includes( - node.name.value, - ); - if (!resolverClassesContainsType && !fieldNode.arguments?.length) { + const typeIsInResolverClasses = findTypeInResolverClassesConfig(node, config); + if (!typeIsInResolverClasses && !fieldNode.arguments?.length) { return ""; } const existingFieldArguments = fieldNode.arguments?.map((arg) => { diff --git a/src/helpers/findTypeInResolverClassesConfig.ts b/src/helpers/findTypeInResolverClassesConfig.ts new file mode 100644 index 0000000..2f42d15 --- /dev/null +++ b/src/helpers/findTypeInResolverClassesConfig.ts @@ -0,0 +1,11 @@ +import { InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode } from "graphql"; +import { CodegenConfigWithDefaults } from "./build-config-with-defaults"; + +export function findTypeInResolverClassesConfig( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + config: CodegenConfigWithDefaults, +) { + return config.resolverClasses?.find( + (resolverClass) => resolverClass.typeName === node.name.value, + ); +} diff --git a/test/unit/should_honor_resolverClasses_config/codegen.config.ts b/test/unit/should_honor_resolverClasses_config/codegen.config.ts index ea7d780..fca563d 100644 --- a/test/unit/should_honor_resolverClasses_config/codegen.config.ts +++ b/test/unit/should_honor_resolverClasses_config/codegen.config.ts @@ -2,7 +2,9 @@ import { GraphQLKotlinCodegenConfig } from "../../../src/plugin"; export default { resolverClasses: [ - "MyResolverType", + { + typeName: "MyIncludedResolverType", + }, { typeName: "MySuspendResolverType", classMethods: "SUSPEND", @@ -11,5 +13,12 @@ export default { typeName: "MyCompletableFutureResolverType", classMethods: "COMPLETABLE_FUTURE", }, + { + typeName: "MyIncludedInterface", + }, + { + typeName: "MyIncludedInterfaceSuspend", + classMethods: "SUSPEND", + }, ], } satisfies GraphQLKotlinCodegenConfig; diff --git a/test/unit/should_honor_resolverClasses_config/expected.kt b/test/unit/should_honor_resolverClasses_config/expected.kt index 6469e24..a1e1e4f 100644 --- a/test/unit/should_honor_resolverClasses_config/expected.kt +++ b/test/unit/should_honor_resolverClasses_config/expected.kt @@ -3,11 +3,13 @@ package com.kotlin.generated import com.expediagroup.graphql.generator.annotations.* @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -open class MyResolverType { - open fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyResolverType.nullableField must be implemented.") - open fun nonNullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyResolverType.nonNullableField must be implemented.") - open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyResolverType.nullableResolver must be implemented.") - open fun nonNullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("MyResolverType.nonNullableResolver must be implemented.") +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?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? = throw NotImplementedError("MyIncludedResolverType.nullableListResolver must be implemented.") + open fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List = throw NotImplementedError("MyIncludedResolverType.nonNullableListResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) @@ -16,6 +18,8 @@ open class MySuspendResolverType { 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?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List? = throw NotImplementedError("MySuspendResolverType.nullableListResolver must be implemented.") + open suspend fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List = throw NotImplementedError("MySuspendResolverType.nonNullableListResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) @@ -24,6 +28,8 @@ open class MyCompletableFutureResolverType { 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?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture?> = throw NotImplementedError("MyCompletableFutureResolverType.nullableListResolver must be implemented.") + open fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): java.util.concurrent.CompletableFuture> = throw NotImplementedError("MyCompletableFutureResolverType.nonNullableListResolver must be implemented.") } @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) @@ -33,6 +39,10 @@ data class MyExcludedResolverType( ) interface MyIncludedInterface { + fun field(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? +} + +interface MyIncludedInterfaceSuspend { suspend fun field(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? } diff --git a/test/unit/should_honor_resolverClasses_config/schema.graphql b/test/unit/should_honor_resolverClasses_config/schema.graphql index d7f5372..e24653b 100644 --- a/test/unit/should_honor_resolverClasses_config/schema.graphql +++ b/test/unit/should_honor_resolverClasses_config/schema.graphql @@ -1,13 +1,28 @@ -type MyResolverType { +type MyIncludedResolverType { nullableField: String nonNullableField: String! nullableResolver(arg: String!): String nonNullableResolver(arg: String!): String! + nullableListResolver(arg1: Int, arg2: Int!): [String] + nonNullableListResolver(arg1: Int!, arg2: Int): [String!]! } -type MyIncludedResolverType { +type MySuspendResolverType { nullableField: String nonNullableField: String! + nullableResolver(arg: String!): String + nonNullableResolver(arg: String!): String! + nullableListResolver(arg1: Int, arg2: Int!): [String] + nonNullableListResolver(arg1: Int!, arg2: Int): [String!]! +} + +type MyCompletableFutureResolverType { + nullableField: String + nonNullableField: String! + nullableResolver(arg: String!): String + nonNullableResolver(arg: String!): String! + nullableListResolver(arg1: Int, arg2: Int!): [String] + nonNullableListResolver(arg1: Int!, arg2: Int): [String!]! } type MyExcludedResolverType { @@ -19,6 +34,10 @@ interface MyIncludedInterface { field: String } +interface MyIncludedInterfaceSuspend { + field: String +} + interface MyExcludedInterface { field: String } From 2e4b3608a9d80809ddd73710a1363903fde5e8fd Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 10:48:06 -0500 Subject: [PATCH 13/18] feat: dummy From ecf0edd1acee03b06b20d4578739a1c99b14e9f4 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 12:16:12 -0500 Subject: [PATCH 14/18] fix: bug --- src/definitions/object.ts | 7 ++++--- .../should_honor_resolverClasses_config/codegen.config.ts | 3 +++ test/unit/should_honor_resolverClasses_config/expected.kt | 6 ++++++ .../should_honor_resolverClasses_config/schema.graphql | 5 +++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/definitions/object.ts b/src/definitions/object.ts index 20361ae..b3ddf3b 100644 --- a/src/definitions/object.ts +++ b/src/definitions/object.ts @@ -60,13 +60,14 @@ export function buildObjectTypeDefinition( ? "" : "@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])\n"; - const shouldGenerateFunctions = node.fields?.some( - (fieldNode) => fieldNode.arguments?.length, - ); const typeInResolverClassesConfig = findTypeInResolverClassesConfig( node, config, ); + const shouldGenerateFunctions = Boolean( + typeInResolverClassesConfig || + node.fields?.some((fieldNode) => fieldNode.arguments?.length), + ); if (shouldGenerateFunctions) { const fieldsWithNoArguments = node.fields?.filter( (fieldNode) => !fieldNode.arguments?.length, diff --git a/test/unit/should_honor_resolverClasses_config/codegen.config.ts b/test/unit/should_honor_resolverClasses_config/codegen.config.ts index fca563d..1df958f 100644 --- a/test/unit/should_honor_resolverClasses_config/codegen.config.ts +++ b/test/unit/should_honor_resolverClasses_config/codegen.config.ts @@ -5,6 +5,9 @@ export default { { typeName: "MyIncludedResolverType", }, + { + typeName: "MyIncludedResolverTypeWithNoFieldArgs", + }, { typeName: "MySuspendResolverType", classMethods: "SUSPEND", diff --git a/test/unit/should_honor_resolverClasses_config/expected.kt b/test/unit/should_honor_resolverClasses_config/expected.kt index a1e1e4f..a4944d1 100644 --- a/test/unit/should_honor_resolverClasses_config/expected.kt +++ b/test/unit/should_honor_resolverClasses_config/expected.kt @@ -12,6 +12,12 @@ open class MyIncludedResolverType { open fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List = throw NotImplementedError("MyIncludedResolverType.nonNullableListResolver must be implemented.") } +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +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]) open class MySuspendResolverType { open suspend fun nullableField(dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MySuspendResolverType.nullableField must be implemented.") diff --git a/test/unit/should_honor_resolverClasses_config/schema.graphql b/test/unit/should_honor_resolverClasses_config/schema.graphql index e24653b..03c3279 100644 --- a/test/unit/should_honor_resolverClasses_config/schema.graphql +++ b/test/unit/should_honor_resolverClasses_config/schema.graphql @@ -7,6 +7,11 @@ type MyIncludedResolverType { nonNullableListResolver(arg1: Int!, arg2: Int): [String!]! } +type MyIncludedResolverTypeWithNoFieldArgs { + nullableField: String + nonNullableField: String! +} + type MySuspendResolverType { nullableField: String nonNullableField: String! From 960e917c4ed75b4d8348903d4dc98658989c0c04 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 13:57:08 -0500 Subject: [PATCH 15/18] refactor --- src/helpers/build-field-definition.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/build-field-definition.ts b/src/helpers/build-field-definition.ts index 2f15074..e06c2ce 100644 --- a/src/helpers/build-field-definition.ts +++ b/src/helpers/build-field-definition.ts @@ -31,7 +31,7 @@ export function buildFieldDefinition( schema: GraphQLSchema, config: CodegenConfigWithDefaults, typeMetadata: TypeMetadata, - shouldUseFunction?: boolean, + shouldGenerateFunctions?: boolean, ) { const modifier = buildFieldModifier(node, fieldNode, schema, config); const fieldArguments = buildFieldArguments(node, fieldNode, schema, config); @@ -51,7 +51,7 @@ export function buildFieldDefinition( const notImplementedError = ` = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`; const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""}${notImplementedError}`; - const defaultValue = shouldUseFunction + const defaultValue = shouldGenerateFunctions ? defaultFunctionValue : typeMetadata.defaultValue; const defaultDefinition = `${typeMetadata.typeName}${defaultValue}`; From dbef0fb14de8be7e35dbd86f8fc037560f65f586 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 15:39:19 -0500 Subject: [PATCH 16/18] include recommended usage docs --- docs/docs/recommended-usage.md | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/docs/recommended-usage.md diff --git a/docs/docs/recommended-usage.md b/docs/docs/recommended-usage.md new file mode 100644 index 0000000..663f700 --- /dev/null +++ b/docs/docs/recommended-usage.md @@ -0,0 +1,118 @@ +--- +sidebar_position: 4 +--- + +# Recommended Usage + +In general, the `resolverClasses` 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 extension class to be inherited in source code. However, when fields have no +arguments, we generate data classes by default. + +## Example + +The following demonstrates the problem with using generated data classes to implement your resolvers with GraphQL Kotlin. + +Say you want to implement the schema below: + +```graphql +type Query { + resolveMyType(input: String!): MyType +} + +type MyType { + field1: String! + field2: String +} +``` + +### Here is the default behavior. + +Generated Kotlin: + +```kotlin +package com.types.generated + +open class Query { + open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.") +} + +data class MyType( + val field1: String, + val field2: String? = null +) +``` + +Source code: + +```kotlin +import com.expediagroup.graphql.server.operations.Query +import com.expediagroup.sharedGraphql.generated.Query as QueryInterface +import com.types.generated.MyType + +class MyQuery : Query, QueryInterface() { + override suspend fun resolveMyType(input: String): MyType = + MyType( + field1 = myExpensiveCall1(), + field2 = myExpensiveCall2() + ) +} + +``` + +The resulting source code is at risk of being extremely unperformant. The `MyType` class is a data class, which means +that the `field1` and `field2` properties are both initialized when the `MyType` object is created, and +`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! + +Codegen config: + +```ts +import { GraphQLKotlinCodegenConfig } from "@expediagroup/graphql-kotlin-codegen"; + +export default { + resolverClasses: [ + { + typeName: "MyType", + }, + ], +} satisfies GraphQLKotlinCodegenConfig; +``` + +Generated Kotlin: + +```kotlin +package com.types.generated + +open class Query { + open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.") +} + +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.") +} +``` + +Source code: + +```kotlin +import com.types.generated.MyType as MyTypeInterface +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore + +class MyQuery : Query, QueryInterface() { + override suspend fun resolveMyType(input: String): MyType = MyType() +} + +@GraphQLIgnore +class MyType : MyTypeInterface() { + override fun field1(): String = myExpensiveCall1() + override fun field2(): String? = myExpensiveCall2() +} +``` + +This code is much more performant. The `MyType` class is no longer a data class, so the `field1` and `field2` properties +can now be resolved independently of each other. If I query for only `field1`, only `myExpensiveCall1()` will be called, and +if I query for only `field2`, only `myExpensiveCall2()` will be called. From 2f683a2ffc6a5bc1560e5dfe064a4f700be99509 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 15:55:54 -0500 Subject: [PATCH 17/18] update usage --- docs/docs/recommended-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/recommended-usage.md b/docs/docs/recommended-usage.md index 663f700..0c4ee79 100644 --- a/docs/docs/recommended-usage.md +++ b/docs/docs/recommended-usage.md @@ -6,8 +6,8 @@ sidebar_position: 4 In general, the `resolverClasses` 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 extension class to be inherited in source code. However, when fields have no -arguments, we generate data classes by default. +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 From 8ec79ab91d4a746223bc3dc334d481e06eae3775 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Tue, 30 Apr 2024 16:31:57 -0500 Subject: [PATCH 18/18] fix interface inheritance --- src/definitions/interface.ts | 7 ++++++- src/helpers/dependent-type-utils.ts | 2 +- .../expected.kt | 11 +++++++++-- .../schema.graphql | 9 ++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/definitions/interface.ts b/src/definitions/interface.ts index 419634c..00525ac 100644 --- a/src/definitions/interface.ts +++ b/src/definitions/interface.ts @@ -17,6 +17,7 @@ import { buildTypeMetadata } from "../helpers/build-type-metadata"; import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition"; import { buildFieldDefinition } from "../helpers/build-field-definition"; import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults"; +import { getDependentInterfaceNames } from "../helpers/dependent-type-utils"; export function buildInterfaceDefinition( node: InterfaceTypeDefinitionNode, @@ -45,7 +46,11 @@ export function buildInterfaceDefinition( config, definitionNode: node, }); - return `${annotations}interface ${node.name.value} { + + const interfacesToInherit = getDependentInterfaceNames(node); + const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`; + + return `${annotations}interface ${node.name.value}${interfaceInheritance} { ${classMembers} }`; } diff --git a/src/helpers/dependent-type-utils.ts b/src/helpers/dependent-type-utils.ts index 57724c9..17e7e18 100644 --- a/src/helpers/dependent-type-utils.ts +++ b/src/helpers/dependent-type-utils.ts @@ -42,7 +42,7 @@ function getFieldTypeName(fieldType: TypeNode) { } export function getDependentInterfaceNames(node: TypeDefinitionNode) { - return node.kind === Kind.OBJECT_TYPE_DEFINITION + return "interfaces" in node ? node.interfaces?.map((interfaceNode) => interfaceNode.name.value) ?? [] : []; } diff --git a/test/unit/should_generate_interfaces_with_inheritance/expected.kt b/test/unit/should_generate_interfaces_with_inheritance/expected.kt index 9868688..b9af0ab 100644 --- a/test/unit/should_generate_interfaces_with_inheritance/expected.kt +++ b/test/unit/should_generate_interfaces_with_inheritance/expected.kt @@ -8,13 +8,20 @@ interface InterfaceWithInheritance { val field2: String } -@GraphQLDescription("A description for MyInterfaceImplementation") +@GraphQLDescription("A description for MyImplementation") @GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) -data class MyInterfaceImplementation( +data class MyImplementation( override val field: String? = null, override val field2: String ) : InterfaceWithInheritance +@GraphQLDescription("A description for MyInterfaceImplementation") +interface MyInterfaceImplementation : InterfaceWithInheritance { + override val field: String? + override val field2: String + val field3: Int? +} + interface InheritedInterface1 { val field: String? } diff --git a/test/unit/should_generate_interfaces_with_inheritance/schema.graphql b/test/unit/should_generate_interfaces_with_inheritance/schema.graphql index 90826cb..283d771 100644 --- a/test/unit/should_generate_interfaces_with_inheritance/schema.graphql +++ b/test/unit/should_generate_interfaces_with_inheritance/schema.graphql @@ -4,10 +4,17 @@ interface InterfaceWithInheritance { field2: String! } +"A description for MyImplementation" +type MyImplementation implements InterfaceWithInheritance { + field: String + field2: String! +} + "A description for MyInterfaceImplementation" -type MyInterfaceImplementation implements InterfaceWithInheritance { +interface MyInterfaceImplementation implements InterfaceWithInheritance { field: String field2: String! + field3: Int } interface InheritedInterface1 {