Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ sealed class NadelHydrationCondition {
}
}

data class BooleanResultEquals(
override val fieldPath: NadelQueryPath,
val value: Boolean,
) : NadelHydrationCondition() {
override fun evaluate(fieldValue: Any?): Boolean {
return fieldValue is Boolean && fieldValue == value
}
}

data class StringResultMatches(
override val fieldPath: NadelQueryPath,
val regex: Regex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,13 +553,14 @@ data class NadelHydrationResultConditionUnsupportedFieldTypeError(
val str = Scalars.GraphQLString.name
val int = Scalars.GraphQLInt.name
val id = Scalars.GraphQLID.name
val boolean = Scalars.GraphQLBoolean.name
val parentTypeName = parentType.overall.name

getHydrationErrorMessage(
parentType,
virtualField,
hydration,
reason = "condition field $parentTypeName.$conditionField must be of type $str, $int, $id or enum but is $conditionFieldType",
reason = "condition field $parentTypeName.$conditionField must be of type $str, $int, $id, $boolean or enum but is $conditionFieldType",
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package graphql.nadel.validation.hydration

import graphql.Scalars.GraphQLBoolean
import graphql.Scalars.GraphQLID
import graphql.Scalars.GraphQLInt
import graphql.Scalars.GraphQLString
Expand Down Expand Up @@ -57,6 +58,10 @@ private sealed class NadelConditionFieldType {
override val graphQLType = GraphQLID
}

object BooleanType : NadelConditionFieldType() {
override val graphQLType = GraphQLBoolean
}

data class EnumType(
val enumType: GraphQLEnumType,
) : NadelConditionFieldType() {
Expand Down Expand Up @@ -134,6 +139,7 @@ internal class NadelHydrationConditionValidation {
GraphQLString -> NadelConditionFieldType.StringType
GraphQLInt -> NadelConditionFieldType.IntType
GraphQLID -> NadelConditionFieldType.IdType
GraphQLBoolean -> NadelConditionFieldType.BooleanType
is GraphQLEnumType -> NadelConditionFieldType.EnumType(conditionFieldOutputType)
else -> null
} ?: return NadelHydrationResultConditionUnsupportedFieldTypeError(
Expand Down Expand Up @@ -304,6 +310,11 @@ internal class NadelHydrationConditionValidation {
fieldPath = NadelQueryPath(resultCondition.pathToSourceField),
value = expectedValue.toLong(),
).asInterimSuccess()
} else if (expectedValue is Boolean && conditionFieldType == NadelConditionFieldType.BooleanType) {
return NadelHydrationCondition.BooleanResultEquals(
fieldPath = NadelQueryPath(resultCondition.pathToSourceField),
value = expectedValue,
).asInterimSuccess()
} else {
return NadelHydrationConditionIncompatibleValueError(
parentType = parent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,71 @@ private const val source = "$" + "source"
private const val argument = "$" + "argument"

class NadelHydrationWhenConditionValidationTest {
private fun booleanConditionFixture(
conditionFieldType: String = "Boolean",
underlyingConditionFieldType: String = "Boolean!",
predicate: String = "equals: true",
Comment thread
gnawf marked this conversation as resolved.
Outdated
): NadelValidationTestFixture {
return NadelValidationTestFixture(
overallSchema = mapOf(
"issues" to """
type Query {
issue: JiraIssue
}
type JiraIssue @renamed(from: "Issue") {
id: ID!
}
""".trimIndent(),
"users" to """
type Query {
users(id: [ID!]!): [User]
}
type User {
id: ID!
name: String!
}
extend type JiraIssue {
shouldHydrate: $conditionFieldType
collaborators: [User] @hydrated(
service: "users"
field: "users"
arguments: [
{name: "id", value: "$source.collaboratorIds"}
]
when: {
result: {
sourceField: "shouldHydrate"
predicate: { $predicate }
}
}
)
}
""".trimIndent(),
),
underlyingSchema = mapOf(
"issues" to """
type Query {
issue: Issue
}
type Issue {
id: ID!
collaboratorIds: [ID!]
shouldHydrate: $underlyingConditionFieldType
}
""".trimIndent(),
"users" to """
type Query {
users(id: [ID!]!): [User]
}
type User {
id: ID!
name: String!
}
""".trimIndent(),
),
)
}

private fun enumConditionFixture(
conditionFieldType: String = "IssueType",
underlyingConditionFieldType: String = "IssueType!",
Expand Down Expand Up @@ -82,6 +147,86 @@ class NadelHydrationWhenConditionValidationTest {
)
}

@Test
fun `boolean condition field is acceptable for true equals predicate`() {
val fixture = booleanConditionFixture()

// When
val errors = validate(fixture)

// Then
assertTrue(errors.map { it.message }.isEmpty())
}

@Test
fun `boolean condition field is acceptable for false equals predicate`() {
val fixture = booleanConditionFixture(predicate = "equals: false")

// When
val errors = validate(fixture)

// Then
assertTrue(errors.map { it.message }.isEmpty())
}

@Test
fun `non null boolean condition field is acceptable for equals predicate`() {
val fixture = booleanConditionFixture(conditionFieldType = "Boolean!")

// When
val errors = validate(fixture)

// Then
assertTrue(errors.map { it.message }.isEmpty())
}

@Test
fun `list boolean condition field is not acceptable for equals predicate`() {
val fixture = booleanConditionFixture(
conditionFieldType = "[Boolean]",
underlyingConditionFieldType = "[Boolean]",
)

// When
val errors = validate(fixture)

// Then
errors.assertSingleOfType<NadelHydrationResultConditionUnsupportedFieldTypeError>()
}

@Test
fun `boolean condition field rejects string equals value`() {
val fixture = booleanConditionFixture(predicate = "equals: \"true\"")

// When
val errors = validate(fixture)

// Then
errors.assertSingleOfType<NadelHydrationConditionIncompatibleValueError>()
}

@Test
fun `boolean condition field rejects matches predicate`() {
val fixture = booleanConditionFixture(predicate = "matches: \"true\"")

// When
val errors = validate(fixture)

// Then
errors.assertSingleOfType<NadelHydrationConditionMatchesPredicateRequiresStringFieldError>()
}

@Test
fun `boolean condition field rejects startsWith predicate`() {
val fixture = booleanConditionFixture(predicate = "startsWith: \"tr\"")

// When
val errors = validate(fixture)

// Then
errors.assertSingleOfType<NadelHydrationConditionStartsWithPredicateRequiresStringFieldError>()
}

@Test
fun `list type field is acceptable if its the source field`() {
val fixture = NadelValidationTestFixture(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package graphql.nadel.tests.next.fixtures.hydration.conditional

import graphql.nadel.tests.next.NadelIntegrationTest

class HydrationConditionalBooleanConditionTest : NadelIntegrationTest(
query = """
query {
foo {
matchingBar {
name
}
nonMatchingBar {
name
}
}
}
""".trimIndent(),
variables = mapOf(),
services = listOf(
Service(
name = "service1",
overallSchema = """
type Query {
foo: Foo
}
type Foo {
id: ID
shouldHydrate: Boolean
matchingBarId: ID @hidden
nonMatchingBarId: ID @hidden
matchingBar: Bar
@hydrated(
service: "service2"
field: "barById"
arguments: [{name: "id", value: "$source.matchingBarId"}]
when: { result: { sourceField: "shouldHydrate", predicate: { equals: true } } }
)
nonMatchingBar: Bar
@hydrated(
service: "service2"
field: "barById"
arguments: [{name: "id", value: "$source.nonMatchingBarId"}]
when: { result: { sourceField: "shouldHydrate", predicate: { equals: false } } }
)
}
""".trimIndent(),
runtimeWiring = { wiring ->
data class Foo(
val id: String,
val shouldHydrate: Boolean,
val matchingBarId: String,
val nonMatchingBarId: String,
)

val foo = Foo(
id = "foo-id",
shouldHydrate = true,
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<String>("id")!!]
}
}
},
),
),
)
Loading
Loading