diff --git a/lib/src/main/java/graphql/nadel/validation/NadelSchemaHydrationValidationError.kt b/lib/src/main/java/graphql/nadel/validation/NadelSchemaHydrationValidationError.kt index f830138a3..3e8d82933 100644 --- a/lib/src/main/java/graphql/nadel/validation/NadelSchemaHydrationValidationError.kt +++ b/lib/src/main/java/graphql/nadel/validation/NadelSchemaHydrationValidationError.kt @@ -10,6 +10,7 @@ import graphql.nadel.definition.hydration.NadelIdHydrationDefinition import graphql.nadel.engine.util.makeFieldCoordinates import graphql.nadel.engine.util.unwrapAll import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLFieldsContainer import graphql.schema.GraphQLInputFieldsContainer @@ -304,6 +305,29 @@ data class NadelHydrationConditionIncompatibleValueError( override val subject = virtualField } +data class NadelHydrationConditionInvalidEnumValueError( + val parentType: NadelServiceSchemaElement, + val virtualField: GraphQLFieldDefinition, + val hydration: NadelHydrationDefinition, + val pathToConditionField: List, + val enumType: GraphQLEnumType, + val suppliedValue: String, +) : NadelSchemaValidationError { + override val message = run { + val parentTypeName = parentType.overall.name + val conditionField = pathToConditionField.joinToString(separator = ".") + val enumValues = enumType.values.joinToString(separator = ", ") { it.name } + getHydrationErrorMessage( + parentType, + virtualField, + hydration, + reason = "condition field $parentTypeName.$conditionField must equal one of enum values [$enumValues] but was \"$suppliedValue\"", + ) + } + + override val subject = virtualField +} + data class NadelHydrationConditionInvalidRegexError( val parentType: NadelServiceSchemaElement, val virtualField: GraphQLFieldDefinition, @@ -535,7 +559,7 @@ data class NadelHydrationResultConditionUnsupportedFieldTypeError( parentType, virtualField, hydration, - reason = "condition field $parentTypeName.$conditionField must to be of type $str, $int or $id but is $conditionFieldType", + reason = "condition field $parentTypeName.$conditionField must be of type $str, $int, $id or enum but is $conditionFieldType", ) } diff --git a/lib/src/main/java/graphql/nadel/validation/hydration/NadelHydrationConditionValidation.kt b/lib/src/main/java/graphql/nadel/validation/hydration/NadelHydrationConditionValidation.kt index 2bfd3b539..602c4bee3 100644 --- a/lib/src/main/java/graphql/nadel/validation/hydration/NadelHydrationConditionValidation.kt +++ b/lib/src/main/java/graphql/nadel/validation/hydration/NadelHydrationConditionValidation.kt @@ -15,6 +15,7 @@ import graphql.nadel.engine.util.isList import graphql.nadel.engine.util.unwrapAll import graphql.nadel.engine.util.unwrapNonNull import graphql.nadel.validation.NadelHydrationConditionIncompatibleValueError +import graphql.nadel.validation.NadelHydrationConditionInvalidEnumValueError import graphql.nadel.validation.NadelHydrationConditionInvalidRegexError import graphql.nadel.validation.NadelHydrationConditionMatchesPredicateRequiresStringFieldError import graphql.nadel.validation.NadelHydrationConditionStartsWithPredicateRequiresStringFieldError @@ -29,8 +30,9 @@ import graphql.nadel.validation.NadelValidationInterimResult.Error.Companion.asI import graphql.nadel.validation.NadelValidationInterimResult.Success.Companion.asInterimSuccess import graphql.nadel.validation.ok import graphql.nadel.validation.onErrorCast +import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLFieldDefinition -import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLType import java.math.BigInteger private data class NadelHydrationConditionValidationContext( @@ -40,6 +42,28 @@ private data class NadelHydrationConditionValidationContext( val condition: NadelHydrationConditionDefinition, ) +private sealed class NadelConditionFieldType { + abstract val graphQLType: GraphQLType + + object StringType : NadelConditionFieldType() { + override val graphQLType = GraphQLString + } + + object IntType : NadelConditionFieldType() { + override val graphQLType = GraphQLInt + } + + object IdType : NadelConditionFieldType() { + override val graphQLType = GraphQLID + } + + data class EnumType( + val enumType: GraphQLEnumType, + ) : NadelConditionFieldType() { + override val graphQLType = enumType + } +} + internal class NadelHydrationConditionValidation { context(NadelValidationContext) fun validateHydrations( @@ -93,40 +117,34 @@ internal class NadelHydrationConditionValidation { } /** - * The result field used in a condition must be a scalar. + * The result field used in a condition must have a type supported by [NadelHydrationCondition]. */ context(NadelValidationContext, NadelHydrationConditionValidationContext) private fun getResultFieldType( pathToConditionField: List, conditionField: GraphQLFieldDefinition, - ): NadelValidationInterimResult { + ): NadelValidationInterimResult { val conditionFieldOutputType = if (isConditionFieldSameAsBatchId(pathToConditionField)) { conditionField.type.unwrapAll() // Accept list if it's the batch ID, so that each batch ID can have its own instruction } else { conditionField.type.unwrapNonNull() // We do not accept list, hence not unwrapAll } - val scalarType = conditionFieldOutputType as? GraphQLScalarType - ?: return NadelHydrationResultConditionUnsupportedFieldTypeError( - parentType = parent, - virtualField = virtualField, - hydration = hydration, - pathToConditionField = pathToConditionField, - conditionField = conditionField, - ).asInterimError() - - // Limit sourceField to simple values like String, Boolean, Int etc. - if (!(scalarType == GraphQLString || scalarType == GraphQLInt || scalarType == GraphQLID)) { - return NadelHydrationResultConditionUnsupportedFieldTypeError( - parentType = parent, - virtualField = virtualField, - hydration = hydration, - pathToConditionField = pathToConditionField, - conditionField = conditionField, - ).asInterimError() - } + val conditionFieldType = when (conditionFieldOutputType) { + GraphQLString -> NadelConditionFieldType.StringType + GraphQLInt -> NadelConditionFieldType.IntType + GraphQLID -> NadelConditionFieldType.IdType + is GraphQLEnumType -> NadelConditionFieldType.EnumType(conditionFieldOutputType) + else -> null + } ?: return NadelHydrationResultConditionUnsupportedFieldTypeError( + parentType = parent, + virtualField = virtualField, + hydration = hydration, + pathToConditionField = pathToConditionField, + conditionField = conditionField, + ).asInterimError() - return scalarType.asInterimSuccess() + return conditionFieldType.asInterimSuccess() } /** @@ -171,7 +189,7 @@ internal class NadelHydrationConditionValidation { context(NadelValidationContext, NadelHydrationConditionValidationContext) private fun validateResultCondition( resultCondition: NadelHydrationResultConditionDefinition, - conditionFieldType: GraphQLScalarType, + conditionFieldType: NadelConditionFieldType, predicateDefinition: NadelHydrationResultFieldPredicateDefinition, ): NadelValidationInterimResult { if (predicateDefinition.equals != null) { @@ -203,11 +221,11 @@ internal class NadelHydrationConditionValidation { context(NadelValidationContext, NadelHydrationConditionValidationContext) private fun validateStartsWithCondition( - conditionFieldType: GraphQLScalarType, + conditionFieldType: NadelConditionFieldType, resultCondition: NadelHydrationResultConditionDefinition, startsWith: String, ): NadelValidationInterimResult { - return if (conditionFieldType == GraphQLString || conditionFieldType == GraphQLID) { + return if (conditionFieldType == NadelConditionFieldType.StringType || conditionFieldType == NadelConditionFieldType.IdType) { NadelHydrationCondition.StringResultStartsWith( fieldPath = NadelQueryPath(resultCondition.pathToSourceField), prefix = startsWith, @@ -224,11 +242,11 @@ internal class NadelHydrationConditionValidation { context(NadelValidationContext, NadelHydrationConditionValidationContext) private fun validateMatchesCondition( - conditionFieldType: GraphQLScalarType, + conditionFieldType: NadelConditionFieldType, resultCondition: NadelHydrationResultConditionDefinition, matches: String, ): NadelValidationInterimResult { - return if (conditionFieldType == GraphQLString || conditionFieldType == GraphQLID) { + return if (conditionFieldType == NadelConditionFieldType.StringType || conditionFieldType == NadelConditionFieldType.IdType) { val regex = try { matches.toRegex() } catch (e: Exception) { @@ -256,16 +274,32 @@ internal class NadelHydrationConditionValidation { context(NadelValidationContext, NadelHydrationConditionValidationContext) private fun validateEqualsCondition( - conditionFieldType: GraphQLScalarType, + conditionFieldType: NadelConditionFieldType, resultCondition: NadelHydrationResultConditionDefinition, expectedValue: Any, ): NadelValidationInterimResult { - if (expectedValue is String && (conditionFieldType == GraphQLString || conditionFieldType == GraphQLID)) { + if (expectedValue is String && (conditionFieldType == NadelConditionFieldType.StringType || conditionFieldType == NadelConditionFieldType.IdType)) { + return NadelHydrationCondition.StringResultEquals( + fieldPath = NadelQueryPath(resultCondition.pathToSourceField), + value = expectedValue, + ).asInterimSuccess() + } else if (expectedValue is String && conditionFieldType is NadelConditionFieldType.EnumType) { + if (conditionFieldType.enumType.getValue(expectedValue) == null) { + return NadelHydrationConditionInvalidEnumValueError( + parentType = parent, + virtualField = virtualField, + hydration = hydration, + pathToConditionField = resultCondition.pathToSourceField, + enumType = conditionFieldType.enumType, + suppliedValue = expectedValue, + ).asInterimError() + } + return NadelHydrationCondition.StringResultEquals( fieldPath = NadelQueryPath(resultCondition.pathToSourceField), value = expectedValue, ).asInterimSuccess() - } else if (expectedValue is BigInteger && (conditionFieldType == GraphQLInt || conditionFieldType == GraphQLID)) { + } else if (expectedValue is BigInteger && (conditionFieldType == NadelConditionFieldType.IntType || conditionFieldType == NadelConditionFieldType.IdType)) { return NadelHydrationCondition.LongResultEquals( fieldPath = NadelQueryPath(resultCondition.pathToSourceField), value = expectedValue.toLong(), @@ -276,7 +310,7 @@ internal class NadelHydrationConditionValidation { virtualField = virtualField, hydration = hydration, pathToConditionField = resultCondition.pathToSourceField, - requiredType = conditionFieldType, + requiredType = conditionFieldType.graphQLType, suppliedValue = expectedValue, ).asInterimError() } diff --git a/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationWhenConditionValidationTest.kt b/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationWhenConditionValidationTest.kt index 246d88f29..fafb74150 100644 --- a/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationWhenConditionValidationTest.kt +++ b/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationWhenConditionValidationTest.kt @@ -2,12 +2,86 @@ package graphql.nadel.validation import graphql.nadel.validation.util.assertSingleOfType import org.junit.jupiter.api.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue private const val source = "$" + "source" private const val argument = "$" + "argument" class NadelHydrationWhenConditionValidationTest { + private fun enumConditionFixture( + conditionFieldType: String = "IssueType", + underlyingConditionFieldType: String = "IssueType!", + predicate: String = "equals: \"BUG\"", + ): NadelValidationTestFixture { + return NadelValidationTestFixture( + overallSchema = mapOf( + "issues" to """ + type Query { + issue: JiraIssue + } + type JiraIssue @renamed(from: "Issue") { + id: ID! + } + enum IssueType { + BUG + STORY + } + """.trimIndent(), + "users" to """ + type Query { + users(id: [ID!]!): [User] + } + type User { + id: ID! + name: String! + } + extend type JiraIssue { + type: $conditionFieldType + collaborators: [User] @hydrated( + service: "users" + field: "users" + arguments: [ + {name: "id", value: "$source.collaboratorIds"} + ] + when: { + result: { + sourceField: "type" + predicate: { $predicate } + } + } + ) + } + """.trimIndent(), + ), + underlyingSchema = mapOf( + "issues" to """ + type Query { + issue: Issue + } + enum IssueType { + BUG + STORY + } + type Issue { + id: ID! + collaboratorIds: [ID!] + type: $underlyingConditionFieldType + } + """.trimIndent(), + "users" to """ + type Query { + users(id: [ID!]!): [User] + } + type User { + id: ID! + name: String! + } + """.trimIndent(), + ), + ) + } + @Test fun `list type field is acceptable if its the source field`() { val fixture = NadelValidationTestFixture( @@ -76,6 +150,259 @@ class NadelHydrationWhenConditionValidationTest { assertTrue(errors.map { it.message }.isEmpty()) } + @Test + fun `enum condition field is acceptable for equals predicate`() { + val fixture = enumConditionFixture() + + // When + val errors = validate(fixture) + + // Then + assertTrue(errors.map { it.message }.isEmpty()) + } + + @Test + fun `non null enum condition field is acceptable for equals predicate`() { + val fixture = enumConditionFixture(conditionFieldType = "IssueType!") + + // When + val errors = validate(fixture) + + // Then + assertTrue(errors.map { it.message }.isEmpty()) + } + + @Test + fun `list enum condition field is not acceptable for equals predicate`() { + val fixture = enumConditionFixture( + conditionFieldType = "[IssueType]", + underlyingConditionFieldType = "[IssueType]", + ) + + // When + val errors = validate(fixture) + + // Then + errors.assertSingleOfType() + } + + @Test + fun `enum condition field rejects invalid equals value`() { + val fixture = enumConditionFixture(predicate = "equals: \"TASK\"") + + // When + val errors = validate(fixture) + + // Then + val error = errors.assertSingleOfType() + assertEquals("TASK", error.suppliedValue) + } + + @Test + fun `enum condition field equals comparison is case sensitive`() { + val fixture = enumConditionFixture(predicate = "equals: \"bug\"") + + // When + val errors = validate(fixture) + + // Then + val error = errors.assertSingleOfType() + assertEquals("bug", error.suppliedValue) + } + + @Test + fun `enum condition field rejects empty string equals value`() { + val fixture = enumConditionFixture(predicate = "equals: \"\"") + + // When + val errors = validate(fixture) + + // Then + val error = errors.assertSingleOfType() + assertEquals("", error.suppliedValue) + } + + @Test + fun `enum condition field rejects matches predicate`() { + val fixture = enumConditionFixture(predicate = "matches: \"BUG\"") + + // When + val errors = validate(fixture) + + // Then + errors.assertSingleOfType() + } + + @Test + fun `enum condition field rejects startsWith predicate`() { + val fixture = enumConditionFixture(predicate = "startsWith: \"BU\"") + + // When + val errors = validate(fixture) + + // Then + errors.assertSingleOfType() + } + + @Test + fun `list enum condition field is acceptable if its the batch source field`() { + val fixture = NadelValidationTestFixture( + overallSchema = mapOf( + "issues" to """ + type Query { + issue: JiraIssue + } + enum IssueType { + BUG + STORY + } + type JiraIssue @renamed(from: "Issue") { + id: ID! + types: [IssueType] + } + """.trimIndent(), + "users" to """ + type Query { + users(id: [UserIssueType]): [User] + } + enum UserIssueType { + BUG + STORY + } + type User { + id: ID! + name: String! + } + extend type JiraIssue { + collaborators: [User] @hydrated( + service: "users" + field: "users" + arguments: [ + {name: "id", value: "$source.types"} + ] + when: { + result: { + sourceField: "types" + predicate: { equals: "BUG" } + } + } + ) + } + """.trimIndent(), + ), + underlyingSchema = mapOf( + "issues" to """ + type Query { + issue: Issue + } + enum IssueType { + BUG + STORY + } + type Issue { + id: ID! + types: [IssueType] + } + """.trimIndent(), + "users" to """ + type Query { + users(id: [UserIssueType]): [User] + } + enum UserIssueType { + BUG + STORY + } + type User { + id: ID! + name: String! + } + """.trimIndent(), + ), + ) + + // When + val errors = validate(fixture) + + // Then + assertTrue(errors.map { it.message }.isEmpty()) + } + + @Test + fun `renamed enum condition field is acceptable for equals predicate`() { + val fixture = NadelValidationTestFixture( + overallSchema = mapOf( + "issues" to """ + type Query { + issue: JiraIssue + } + enum IssueType @renamed(from: "UnderlyingIssueType") { + BUG + STORY + } + type JiraIssue @renamed(from: "Issue") { + id: ID! + type: IssueType + } + """.trimIndent(), + "users" to """ + type Query { + users(id: [ID!]!): [User] + } + type User { + id: ID! + name: String! + } + extend type JiraIssue { + collaborators: [User] @hydrated( + service: "users" + field: "users" + arguments: [ + {name: "id", value: "$source.collaboratorIds"} + ] + when: { + result: { + sourceField: "type" + predicate: { equals: "BUG" } + } + } + ) + } + """.trimIndent(), + ), + underlyingSchema = mapOf( + "issues" to """ + type Query { + issue: Issue + } + enum UnderlyingIssueType { + BUG + STORY + } + type Issue { + id: ID! + collaboratorIds: [ID!] + type: UnderlyingIssueType + } + """.trimIndent(), + "users" to """ + type Query { + users(id: [ID!]!): [User] + } + type User { + id: ID! + name: String! + } + """.trimIndent(), + ), + ) + + // When + val errors = validate(fixture) + + // Then + assertTrue(errors.map { it.message }.isEmpty()) + } + @Test fun `matches predicate fails validation if it is invalid regex`() { val fixture = NadelValidationTestFixture( diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/BatchHydrationConditionalEnumConditionTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/BatchHydrationConditionalEnumConditionTest.kt new file mode 100644 index 000000000..a968f4e49 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/BatchHydrationConditionalEnumConditionTest.kt @@ -0,0 +1,114 @@ +package graphql.nadel.tests.next.fixtures.hydration.conditional + +import graphql.nadel.engine.util.strictAssociateBy +import graphql.nadel.tests.next.NadelIntegrationTest +import graphql.nadel.tests.next.SimpleClassNameTypeResolver + +class BatchHydrationConditionalEnumConditionTest : NadelIntegrationTest( + query = """ + query { + foo { + bars { + ... on Bar { + name + } + } + } + } + """.trimIndent(), + variables = mapOf(), + services = listOf( + Service( + name = "service1", + overallSchema = """ + type Query { + foo: Foo + } + enum FooType { + BUG + STORY + } + type Foo { + id: ID + type: FooType + barIds: [ID] @hidden + bars: [Bars] + @hydrated( + service: "service2" + field: "bugBarsById" + arguments: [{name: "ids", value: "$source.barIds"}] + when: { result: { sourceField: "type", predicate: { equals: "BUG" } } } + ) + @hydrated( + service: "service2" + field: "storyBarsById" + arguments: [{name: "ids", value: "$source.barIds"}] + when: { result: { sourceField: "type", predicate: { equals: "STORY" } } } + ) + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class Foo( + val id: String, + val type: String, + val barIds: List, + ) + + val foo = Foo( + id = "foo-id", + type = "STORY", + barIds = listOf("bar-id-1", "bar-id-2"), + ) + + wiring + .type("Query") { type -> + type.dataFetcher("foo") { foo } + } + }, + ), + Service( + name = "service2", + overallSchema = """ + type Query { + bugBarsById(ids: [ID]): [Bar] + storyBarsById(ids: [ID]): [Bar] + } + type Bar { + id: ID + name: String + } + union Bars = Bar + """.trimIndent(), + runtimeWiring = { wiring -> + data class Bar( + val id: String, + val name: String, + ) + + val bugBarsById = listOf( + Bar(id = "bar-id-1", name = "Bug Bar 1"), + Bar(id = "bar-id-2", name = "Bug Bar 2"), + ).strictAssociateBy { it.id } + + val storyBarsById = listOf( + Bar(id = "bar-id-1", name = "Story Bar 1"), + Bar(id = "bar-id-2", name = "Story Bar 2"), + ).strictAssociateBy { it.id } + + wiring + .type("Query") { type -> + type + .dataFetcher("bugBarsById") { fetchEnv -> + fetchEnv.getArgument>("ids")!!.map(bugBarsById::get) + } + .dataFetcher("storyBarsById") { fetchEnv -> + fetchEnv.getArgument>("ids")!!.map(storyBarsById::get) + } + } + .type("Bars") { type -> + type.typeResolver(SimpleClassNameTypeResolver) + } + }, + ), + ), +) diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/BatchHydrationConditionalEnumConditionTestSnapshot.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/BatchHydrationConditionalEnumConditionTestSnapshot.kt new file mode 100644 index 000000000..1f681944e --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/BatchHydrationConditionalEnumConditionTestSnapshot.kt @@ -0,0 +1,107 @@ +// @formatter:off +package graphql.nadel.tests.next.fixtures.hydration.conditional + +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 BatchHydrationConditionalEnumConditionTestSnapshot : TestSnapshot() { + override val calls: List = listOf( + ExpectedServiceCall( + service = "service2", + query = """ + | { + | storyBarsById(ids: ["bar-id-1", "bar-id-2"]) { + | batch_hydration__bars__id: id + | name + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "storyBarsById": [ + | { + | "batch_hydration__bars__id": "bar-id-1", + | "name": "Story Bar 1" + | }, + | { + | "batch_hydration__bars__id": "bar-id-2", + | "name": "Story Bar 2" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "service1", + query = """ + | { + | foo { + | __typename__batch_hydration__bars: __typename + | batch_hydration__bars__barIds: barIds + | batch_hydration__bars__barIds: barIds + | batch_hydration__bars__type: type + | batch_hydration__bars__type: type + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "foo": { + | "__typename__batch_hydration__bars": "Foo", + | "batch_hydration__bars__barIds": [ + | "bar-id-1", + | "bar-id-2" + | ], + | "batch_hydration__bars__type": "STORY" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ) + + override val result: ExpectedNadelResult = ExpectedNadelResult( + result = """ + | { + | "data": { + | "foo": { + | "bars": [ + | { + | "name": "Story Bar 1" + | }, + | { + | "name": "Story Bar 2" + | } + | ] + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ) +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalEnumConditionTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalEnumConditionTest.kt new file mode 100644 index 000000000..c5f46b989 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalEnumConditionTest.kt @@ -0,0 +1,103 @@ +package graphql.nadel.tests.next.fixtures.hydration.conditional + +import graphql.nadel.tests.next.NadelIntegrationTest + +class HydrationConditionalEnumConditionTest : NadelIntegrationTest( + query = """ + query { + foo { + matchingBar { + name + } + nonMatchingBar { + name + } + } + } + """.trimIndent(), + variables = mapOf(), + services = listOf( + Service( + name = "service1", + overallSchema = """ + type Query { + foo: Foo + } + enum FooType { + BUG + STORY + } + type Foo { + id: ID + type: FooType + matchingBarId: ID @hidden + nonMatchingBarId: ID @hidden + matchingBar: Bar + @hydrated( + service: "service2" + field: "barById" + arguments: [{name: "id", value: "$source.matchingBarId"}] + when: { result: { sourceField: "type", predicate: { equals: "BUG" } } } + ) + nonMatchingBar: Bar + @hydrated( + service: "service2" + field: "barById" + arguments: [{name: "id", value: "$source.nonMatchingBarId"}] + when: { result: { sourceField: "type", predicate: { equals: "STORY" } } } + ) + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class Foo( + val id: String, + val type: String, + val matchingBarId: String, + val nonMatchingBarId: String, + ) + + val foo = Foo( + id = "foo-id", + type = "BUG", + matchingBarId = "matching-bar-id", + nonMatchingBarId = "non-matching-bar-id", + ) + + wiring + .type("Query") { type -> + type.dataFetcher("foo") { foo } + } + }, + ), + Service( + name = "service2", + overallSchema = """ + type Query { + barById(id: ID): Bar + } + type Bar { + id: ID + name: String + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class Bar( + val id: String, + val name: String, + ) + + val barsById = listOf( + Bar(id = "matching-bar-id", name = "Matching Bar"), + Bar(id = "non-matching-bar-id", name = "Non Matching Bar"), + ).associateBy { it.id } + + wiring + .type("Query") { type -> + type.dataFetcher("barById") { + barsById[it.getArgument("id")!!] + } + } + }, + ), + ), +) diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalEnumConditionTestSnapshot.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalEnumConditionTestSnapshot.kt new file mode 100644 index 000000000..288756503 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalEnumConditionTestSnapshot.kt @@ -0,0 +1,96 @@ +// @formatter:off +package graphql.nadel.tests.next.fixtures.hydration.conditional + +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 HydrationConditionalEnumConditionTestSnapshot : TestSnapshot() { + override val calls: List = listOf( + ExpectedServiceCall( + service = "service2", + query = """ + | { + | barById(id: "matching-bar-id") { + | name + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "barById": { + | "name": "Matching Bar" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "service1", + query = """ + | { + | foo { + | __typename__hydration__matchingBar: __typename + | hydration__matchingBar__matchingBarId: matchingBarId + | hydration__matchingBar__type: type + | __typename__hydration__nonMatchingBar: __typename + | hydration__nonMatchingBar__nonMatchingBarId: nonMatchingBarId + | hydration__nonMatchingBar__type: type + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "foo": { + | "__typename__hydration__matchingBar": "Foo", + | "hydration__matchingBar__matchingBarId": "matching-bar-id", + | "hydration__matchingBar__type": "BUG", + | "__typename__hydration__nonMatchingBar": "Foo", + | "hydration__nonMatchingBar__nonMatchingBarId": "non-matching-bar-id", + | "hydration__nonMatchingBar__type": "BUG" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ) + + override val result: ExpectedNadelResult = ExpectedNadelResult( + result = """ + | { + | "data": { + | "foo": { + | "matchingBar": { + | "name": "Matching Bar" + | }, + | "nonMatchingBar": null + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ) +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalMultipleEnumConditionsTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalMultipleEnumConditionsTest.kt new file mode 100644 index 000000000..7c893cb01 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalMultipleEnumConditionsTest.kt @@ -0,0 +1,110 @@ +package graphql.nadel.tests.next.fixtures.hydration.conditional + +import graphql.nadel.tests.next.NadelIntegrationTest +import graphql.nadel.tests.next.SimpleClassNameTypeResolver + +class HydrationConditionalMultipleEnumConditionsTest : NadelIntegrationTest( + query = """ + query { + foo { + bar { + ... on Bar { + name + } + } + } + } + """.trimIndent(), + variables = mapOf(), + services = listOf( + Service( + name = "service1", + overallSchema = """ + type Query { + foo: Foo + } + enum FooType { + BUG + STORY + } + type Foo { + id: ID + type: FooType + barId: ID @hidden + bar: Bars + @hydrated( + service: "service2" + field: "bugBarById" + arguments: [{name: "id", value: "$source.barId"}] + when: { result: { sourceField: "type", predicate: { equals: "BUG" } } } + ) + @hydrated( + service: "service2" + field: "storyBarById" + arguments: [{name: "id", value: "$source.barId"}] + when: { result: { sourceField: "type", predicate: { equals: "STORY" } } } + ) + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class Foo( + val id: String, + val type: String, + val barId: String, + ) + + val foo = Foo( + id = "foo-id", + type = "STORY", + barId = "bar-id", + ) + + wiring + .type("Query") { type -> + type.dataFetcher("foo") { foo } + } + }, + ), + Service( + name = "service2", + overallSchema = """ + type Query { + bugBarById(id: ID): Bar + storyBarById(id: ID): Bar + } + type Bar { + id: ID + name: String + } + union Bars = Bar + """.trimIndent(), + runtimeWiring = { wiring -> + data class Bar( + val id: String, + val name: String, + ) + + val bugBarsById = listOf( + Bar(id = "bar-id", name = "Bug Bar"), + ).associateBy { it.id } + val storyBarsById = listOf( + Bar(id = "bar-id", name = "Story Bar"), + ).associateBy { it.id } + + wiring + .type("Query") { type -> + type + .dataFetcher("bugBarById") { + bugBarsById[it.getArgument("id")!!] + } + .dataFetcher("storyBarById") { + storyBarsById[it.getArgument("id")!!] + } + } + .type("Bars") { type -> + type.typeResolver(SimpleClassNameTypeResolver) + } + }, + ), + ), +) diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalMultipleEnumConditionsTestSnapshot.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalMultipleEnumConditionsTestSnapshot.kt new file mode 100644 index 000000000..b5fd80627 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/conditional/HydrationConditionalMultipleEnumConditionsTestSnapshot.kt @@ -0,0 +1,91 @@ +// @formatter:off +package graphql.nadel.tests.next.fixtures.hydration.conditional + +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 HydrationConditionalMultipleEnumConditionsTestSnapshot : TestSnapshot() { + override val calls: List = listOf( + ExpectedServiceCall( + service = "service2", + query = """ + | { + | storyBarById(id: "bar-id") { + | name + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "storyBarById": { + | "name": "Story Bar" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "service1", + query = """ + | { + | foo { + | __typename__hydration__bar: __typename + | hydration__bar__barId: barId + | hydration__bar__barId: barId + | hydration__bar__type: type + | hydration__bar__type: type + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "foo": { + | "__typename__hydration__bar": "Foo", + | "hydration__bar__barId": "bar-id", + | "hydration__bar__type": "STORY" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ) + + override val result: ExpectedNadelResult = ExpectedNadelResult( + result = """ + | { + | "data": { + | "foo": { + | "bar": { + | "name": "Story Bar" + | } + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ) +}