diff --git a/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt b/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt index c85f11f22..fc8ed4d58 100644 --- a/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt +++ b/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt @@ -10,14 +10,14 @@ import graphql.language.FieldDefinition import graphql.language.ImplementingTypeDefinition import graphql.nadel.Service import graphql.nadel.dsl.FieldMappingDefinition +import graphql.nadel.dsl.NadelHydrationDefinition import graphql.nadel.dsl.RemoteArgumentSource import graphql.nadel.dsl.TypeMappingDefinition -import graphql.nadel.dsl.NadelHydrationDefinition import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef.ValueSource.FieldResultValue -import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy import graphql.nadel.engine.blueprint.hydration.NadelHydrationCondition +import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy import graphql.nadel.engine.transform.query.NadelQueryPath import graphql.nadel.engine.util.AnyImplementingTypeDefinition import graphql.nadel.engine.util.AnyNamedNode @@ -771,44 +771,68 @@ private class SharedTypesAnalysis( overallParentType: AnyImplementingTypeDefinition, underlyingParentType: GraphQLFieldsContainer, ): List { - val overallOutputTypeName = overallField.type.unwrapAll().name - val underlyingField = getUnderlyingField(overallField, overallParentType, underlyingParentType) ?: return emptyList() - val renameInstruction = if (overallOutputTypeName !in serviceDefinedTypes) { + val overallOutputTypeName = overallField.type.unwrapAll().name + val underlyingOutputTypeName = underlyingField.type.unwrapAll().name + val outputTypeRenameInstruction = getTypeRenameInstructionOrNull( + overallTypeName = overallOutputTypeName, + underlyingTypeName = underlyingOutputTypeName, + serviceDefinedTypes = serviceDefinedTypes, + service = service, + ) + + val argumentTypeRenameInstructions = overallField.inputValueDefinitions + .mapNotNull { overallArgument -> + val underlyingArgument = underlyingField.getArgument(overallArgument.name) + getTypeRenameInstructionOrNull( + overallTypeName = overallArgument.type.unwrapAll().name, + underlyingTypeName = underlyingArgument.type.unwrapAll().name, + serviceDefinedTypes = serviceDefinedTypes, + service = service, + ) + } + + val overallOutputTypeDefinition = (engineSchema.getType(overallOutputTypeName) as? GraphQLFieldsContainer?) + ?.definition as AnyImplementingTypeDefinition? + + return listOfNotNull(outputTypeRenameInstruction) + argumentTypeRenameInstructions + (overallOutputTypeDefinition + ?.let { + investigateTypeRenames( + visitedTypes, + service, + serviceDefinedTypes, + overallType = overallOutputTypeDefinition, + underlyingType = underlyingField.type.unwrapAll() as GraphQLFieldsContainer, + ) + } ?: emptyList()) + } + + private fun getTypeRenameInstructionOrNull( + overallTypeName: String, + underlyingTypeName: String, + serviceDefinedTypes: Set, + service: Service, + ): NadelTypeRenameInstruction? { + return if (overallTypeName !in serviceDefinedTypes) { // Service does not own type, it is shared // If the name is different than the overall type, then we mark the rename - when (val underlyingOutputTypeName = underlyingField.type.unwrapAll().name) { - overallOutputTypeName -> null + when (underlyingTypeName) { + overallTypeName -> null in scalarTypeNames -> null - else -> when (typeRenameInstructions[overallOutputTypeName]) { + else -> when (typeRenameInstructions[overallTypeName]) { null -> error("Nadel does not allow implicit renames") else -> NadelTypeRenameInstruction( service, - overallName = overallOutputTypeName, - underlyingName = underlyingOutputTypeName, + overallName = overallTypeName, + underlyingName = underlyingTypeName, ) } } } else { null } - - val overallOutputType = engineSchema.getType(overallOutputTypeName) - // Ensure type exists, schema transformation can delete types, so let's just ignore it - .let { it ?: return emptyList() } - // Return if not field container - .let { it as? GraphQLFieldsContainer ?: return emptyList() } - .let { it.definition as AnyImplementingTypeDefinition } - - return listOfNotNull(renameInstruction) + investigateTypeRenames( - visitedTypes, - service, - serviceDefinedTypes, - overallType = overallOutputType, - underlyingType = underlyingField.type.unwrapAll() as GraphQLFieldsContainer, - ) } private fun getUnderlyingField( diff --git a/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt b/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt index 771f371dd..666a9344d 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt @@ -21,11 +21,10 @@ interface JsonNodes { fun getNodesAt(queryPath: NadelQueryPath, flatten: Boolean = false): List companion object { - internal var nodesFactory: (JsonMap, NadelQueryPath?) -> JsonNodes = { data, pathPrefix -> NadelCachingJsonNodes(data, pathPrefix) } - + /** * @param data The JSON map data. * @param pathPrefix For incremental (defer) payloads, this is the prefix that needs to be removed from the path. diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/UnderlyingSchemaGenerator.kt b/test/src/test/kotlin/graphql/nadel/tests/next/UnderlyingSchemaGenerator.kt index 7580e6904..f0d622eaf 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/next/UnderlyingSchemaGenerator.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/next/UnderlyingSchemaGenerator.kt @@ -333,11 +333,11 @@ private fun transformFields(fieldDefinitions: List): List field.transform { fieldBuilder -> - fieldBuilder .name(field.getUnderlyingName()) .directives(field.directives.filterNotNadelDirectives()) .type(field.type.getUnderlyingType()) + .inputValueDefinitions(transformInputValueDefinitions(field.inputValueDefinitions)) } } .toList() diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/UpdateTestSnapshots.kt b/test/src/test/kotlin/graphql/nadel/tests/next/UpdateTestSnapshots.kt index c836e34d1..264e73bb6 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/next/UpdateTestSnapshots.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/next/UpdateTestSnapshots.kt @@ -223,7 +223,7 @@ private fun makeConstructorInvocationToExpectedServiceCall(call: TestExecutionCa ExpectedServiceCall::query.name add("%L = %S,\n", ExpectedServiceCall::service.name, call.service) add("%L = %S,\n", ExpectedServiceCall::query.name, call.query.replaceIndent(" ")) - add("%L = %S,\n", ExpectedServiceCall::variables.name, call.variables) + add("%L = %S,\n", ExpectedServiceCall::variables.name, writeResultJson(call.variables)) add("%L = %S,\n", ExpectedServiceCall::result.name, writeResultJson(call.result)) add("delayedResults = %M", listOfJsonStringsMember) diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedInputTypeTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedInputTypeTest.kt new file mode 100644 index 000000000..23fcc9a9b --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedInputTypeTest.kt @@ -0,0 +1,83 @@ +package graphql.nadel.tests.next.fixtures.rename + +import graphql.nadel.NadelExecutionHints +import graphql.nadel.tests.next.NadelIntegrationTest + +/** + * The `ConfluenceLegacyPathType` input type was renamed. + * + * In the test snapshot we ensure the variable is defined as `PathType`. + */ +class RenamedInputTypeTest : NadelIntegrationTest( + query = """ + query { + me { + profilePicture { + path(type: ABSOLUTE) + } + } + } + """.trimIndent(), + services = listOf( + Service( + name = "confluence_legacy", + overallSchema = """ + type Query { + me: ConfluenceLegacyUser + } + type ConfluenceLegacyUser @renamed(from: "User") { + profilePicture: ConfluenceLegacyProfilePicture + } + type ConfluenceLegacyProfilePicture @renamed(from: "ProfilePicture") { + path(type: ConfluenceLegacyPathType!): String + } + enum ConfluenceLegacyPathType @renamed(from: "PathType") { + ABSOLUTE + RELATIVE + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class ProfilePicture( + val absolutePath: String, + val relativePath: String, + ) + + data class User( + val profilePicture: ProfilePicture, + ) + + wiring + .type("Query") { type -> + type + .dataFetcher("me") { env -> + User( + profilePicture = ProfilePicture( + relativePath = "/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70", + absolutePath = "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70", + ), + ) + } + } + .type("ProfilePicture") { type -> + type + .dataFetcher("path") { env -> + val pfp = env.getSource()!! + when (val urlType = env.getArgument("type")) { + "ABSOLUTE" -> pfp.absolutePath + "RELATIVE" -> pfp.relativePath + else -> throw IllegalArgumentException(urlType) + } + } + } + }, + ), + ), +) { + override fun makeExecutionHints(): NadelExecutionHints.Builder { + return super.makeExecutionHints() + // todo: this should be on by default + .allDocumentVariablesHint { + true + } + } +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedInputTypeTestSnapshot.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedInputTypeTestSnapshot.kt new file mode 100644 index 000000000..6810dc60e --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedInputTypeTestSnapshot.kt @@ -0,0 +1,84 @@ +// @formatter:off +package graphql.nadel.tests.next.fixtures.rename + +import graphql.nadel.tests.next.ExpectedNadelResult +import graphql.nadel.tests.next.ExpectedServiceCall +import graphql.nadel.tests.next.TestSnapshot +import graphql.nadel.tests.next.listOfJsonStrings +import kotlin.Suppress +import kotlin.collections.List +import kotlin.collections.listOf + +private suspend fun main() { + graphql.nadel.tests.next.update() +} + +/** + * This class is generated. Do NOT modify. + * + * Refer to [graphql.nadel.tests.next.UpdateTestSnapshots + */ +@Suppress("unused") +public class RenamedInputTypeTestSnapshot : TestSnapshot() { + override val calls: List = listOf( + ExpectedServiceCall( + service = "confluence_legacy", + query = """ + | query (${'$'}v0: PathType!) { + | me { + | profilePicture { + | path(type: ${'$'}v0) + | } + | } + | } + """.trimMargin(), + variables = """ + | { + | "v0": "ABSOLUTE" + | } + """.trimMargin(), + result = """ + | { + | "data": { + | "me": { + | "profilePicture": { + | "path": "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70" + | } + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ) + + /** + * ```json + * { + * "data": { + * "me": { + * "profilePicture": { + * "path": "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70" + * } + * } + * } + * } + * ``` + */ + override val result: ExpectedNadelResult = ExpectedNadelResult( + result = """ + | { + | "data": { + | "me": { + | "profilePicture": { + | "path": "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70" + | } + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ) +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedSharedInputTypeTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedSharedInputTypeTest.kt new file mode 100644 index 000000000..b1db15f1b --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedSharedInputTypeTest.kt @@ -0,0 +1,133 @@ +package graphql.nadel.tests.next.fixtures.rename + +import graphql.nadel.NadelExecutionHints +import graphql.nadel.tests.next.NadelIntegrationTest + +/** + * The ConfluenceLegacyPathType enum type is renamed, and we share it between the two defined services in this test. + * The enum is also exclusively used as an input type, never as an output type. + * + * We need to ensure that it's renamed when sent to the underlying service. + */ +class RenamedSharedInputTypeTest : NadelIntegrationTest( + query = """ + query { + something { + users { + profilePicture { + path(type: ABSOLUTE) + } + } + } + } + """.trimIndent(), + services = listOf( + Service( + name = "confluence_legacy", + overallSchema = """ + type Query { + me: ConfluenceLegacyUser + } + type ConfluenceLegacyUser @renamed(from: "User") { + profilePicture: ConfluenceLegacyProfilePicture + } + type ConfluenceLegacyProfilePicture @renamed(from: "ProfilePicture") { + path(type: ConfluenceLegacyPathType!): String + } + enum ConfluenceLegacyPathType @renamed(from: "PathType") { + ABSOLUTE + RELATIVE + } + """.trimIndent(), + runtimeWiring = { wiring -> + wiring + .type("Query") { type -> + type + .dataFetcher("me") { env -> + throw UnsupportedOperationException("Not implemented") + } + } + }, + ), + Service( + name = "confluence_something", + overallSchema = """ + type Query { + something: ConfluenceLegacySomething + } + type ConfluenceLegacySomething @renamed(from: "Something") { + users: [ConfluenceLegacyUser] + } + """.trimIndent(), + // Need to explicitly declare underlying schema for shared type + underlyingSchema = """ + type Query { + something: Something + } + type Something { + users: [User] + } + type User { + profilePicture: ProfilePicture + } + type ProfilePicture { + path(type: PathType!): String + } + enum PathType { + ABSOLUTE + RELATIVE + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class ProfilePicture( + val absolutePath: String, + val relativePath: String, + ) + + data class User( + val profilePicture: ProfilePicture, + ) + + data class Something( + val users: List, + ) + + wiring + .type("Query") { type -> + type + .dataFetcher("something") { env -> + Something( + users = listOf( + User( + profilePicture = ProfilePicture( + relativePath = "/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70", + absolutePath = "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70", + ), + ), + ), + ) + } + } + .type("ProfilePicture") { type -> + type + .dataFetcher("path") { env -> + val pfp = env.getSource()!! + when (val urlType = env.getArgument("type")) { + "ABSOLUTE" -> pfp.absolutePath + "RELATIVE" -> pfp.relativePath + else -> throw IllegalArgumentException(urlType) + } + } + } + }, + ), + ), +) { + override fun makeExecutionHints(): NadelExecutionHints.Builder { + return super.makeExecutionHints() + // todo: this should be on by default + .allDocumentVariablesHint { + true + } + } +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedSharedInputTypeTestSnapshot.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedSharedInputTypeTestSnapshot.kt new file mode 100644 index 000000000..38e2e20fa --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/rename/RenamedSharedInputTypeTestSnapshot.kt @@ -0,0 +1,98 @@ +// @formatter:off +package graphql.nadel.tests.next.fixtures.rename + +import graphql.nadel.tests.next.ExpectedNadelResult +import graphql.nadel.tests.next.ExpectedServiceCall +import graphql.nadel.tests.next.TestSnapshot +import graphql.nadel.tests.next.listOfJsonStrings +import kotlin.Suppress +import kotlin.collections.List +import kotlin.collections.listOf + +private suspend fun main() { + graphql.nadel.tests.next.update() +} + +/** + * This class is generated. Do NOT modify. + * + * Refer to [graphql.nadel.tests.next.UpdateTestSnapshots + */ +@Suppress("unused") +public class RenamedSharedInputTypeTestSnapshot : TestSnapshot() { + override val calls: List = listOf( + ExpectedServiceCall( + service = "confluence_something", + query = """ + | query (${'$'}v0: PathType!) { + | something { + | users { + | profilePicture { + | path(type: ${'$'}v0) + | } + | } + | } + | } + """.trimMargin(), + variables = """ + | { + | "v0": "ABSOLUTE" + | } + """.trimMargin(), + result = """ + | { + | "data": { + | "something": { + | "users": [ + | { + | "profilePicture": { + | "path": "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70" + | } + | } + | ] + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ) + + /** + * ```json + * { + * "data": { + * "something": { + * "users": [ + * { + * "profilePicture": { + * "path": "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70" + * } + * } + * ] + * } + * } + * } + * ``` + */ + override val result: ExpectedNadelResult = ExpectedNadelResult( + result = """ + | { + | "data": { + | "something": { + | "users": [ + | { + | "profilePicture": { + | "path": "https://atlassian.net/wiki/aa-avatar/5ee0a4ef55749e0ab6e0fb70" + | } + | } + | ] + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ) +}