Skip to content

Commit

Permalink
Add code to ensure there are not multiple list source input fields
Browse files Browse the repository at this point in the history
  • Loading branch information
gnawf committed Dec 10, 2023
1 parent 93fe0d9 commit 3629453
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ internal class NadelHydrationValidation(
}

val indexHydrationErrors = limitUseOfIndexHydration(parent, overallField, hydrations)
val sourceFieldErrors = limitSourceField(parent, overallField, hydrations)

if (hasMoreThanOneHydration) {
val (batched, notBatched) = hydrations.partition(::isBatched)
Expand All @@ -88,14 +89,55 @@ internal class NadelHydrationValidation(
}
}

return indexHydrationErrors + errors
return indexHydrationErrors + sourceFieldErrors + errors
}

private fun limitSourceField(
parent: NadelServiceSchemaElement,
overallField: GraphQLFieldDefinition,
hydrations: List<UnderlyingServiceHydration>,
): List<NadelSchemaValidationError> {
if (hydrations.size > 1) {
val hasListSourceInputField = hydrations
.any { hydration ->
val parentType = parent.underlying as GraphQLFieldsContainer
hydration
.arguments
.asSequence()
.mapNotNull { argument ->
argument.remoteArgumentSource.pathToField
}
.any { path ->
parentType.getFieldAt(path)?.type?.unwrapNonNull()?.isList == true
}
}

if (hasListSourceInputField) {
val sourceFields = hydrations
.flatMapTo(LinkedHashSet()) { hydration ->
hydration.arguments
.mapNotNull { argument ->
argument.remoteArgumentSource.pathToField
}
}

if (sourceFields.size > 1) {
return listOf(
NadelSchemaValidationError.MultipleHydrationSourceInputFields(parent, overallField),
)
}
}
}

return emptyList()
}

private fun limitUseOfIndexHydration(
parent: NadelServiceSchemaElement,
overallField: GraphQLFieldDefinition,
hydrations: List<UnderlyingServiceHydration>,
): List<NadelSchemaValidationError> {
// todo: or maybe just don't allow polymorphic index hydration
val (indexCount, nonIndexCount) = hydrations.partitionCount { it.isObjectMatchByIndex }
if (indexCount > 0 && nonIndexCount > 0) {
return listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,20 @@ sealed interface NadelSchemaValidationError {
override val subject = overallField
}

data class MultipleHydrationSourceInputFields(
val parentType: NadelServiceSchemaElement,
val overallField: GraphQLFieldDefinition,
) : NadelSchemaValidationError {
val service: Service get() = parentType.service

override val message = run {
val coords = makeFieldCoordinates(parentType.overall.name, overallField.name)
"Field $coords uses multiple \$source fields"
}

override val subject = overallField
}

data class StaticArgIsNotAssignable(
val parentType: NadelServiceSchemaElement,
val overallField: GraphQLFieldDefinition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,264 @@ class NadelHydrationValidationTest2 {
assertTrue(errors.single() is NadelSchemaValidationError.MixedIndexHydration)
assertTrue(errors.single().subject.name == "creator")
}

@Test
fun `prohibit multiple source input fields if they are list types`() {
val fixture = NadelValidationTestFixture(
overallSchema = mapOf(
"activity" to /* language=GraphQL*/ """
type Query {
myActivity: [Activity]
}
union ActivityContent = User | Issue
type Activity {
id: ID!
data: [ActivityContent]
@hydrated(
service: "users"
field: "usersByIds"
arguments: [
{name: "ids", value: "$source.userIds"}
]
)
@hydrated(
service: "issues"
field: "issuesByIds"
arguments: [
{name: "ids", value: "$source.issueIds"}
]
)
}
""".trimIndent(),
"users" to /* language=GraphQL*/ """
type Query {
usersByIds(ids: [ID]!): [User]
}
type User {
id: ID!
name: String!
}
""".trimIndent(),
"issues" to /* language=GraphQL*/ """
type Query {
issuesByIds(ids: [ID]!): [Issue]
}
type Issue {
id: ID!
key: String
}
""".trimIndent(),
),
underlyingSchema = mapOf(
"activity" to /* language=GraphQL*/ """
type Query {
myActivity: [Activity]
}
type Activity {
id: ID!
userIds: [ID]
issueIds: [ID]
}
""".trimIndent(),
"users" to /* language=GraphQL*/ """
type Query {
usersByIds(ids: [ID]!): [User]
}
type User {
id: ID!
name: String!
}
type Account {
id: ID!
}
""".trimIndent(),
"issues" to /* language=GraphQL*/ """
type Query {
issuesByIds(ids: [ID]!): [Issue]
}
type Issue {
id: ID!
key: String
}
""".trimIndent(),
),
)

val errors = validate(fixture)
assertTrue(errors.map { it.message }.isNotEmpty())
assertTrue(errors.single() is NadelSchemaValidationError.MultipleHydrationSourceInputFields)
assertTrue(errors.single().subject.name == "data")
}

@Test
fun `permit multiple source fields if source input field is not list type`() {
val fixture = NadelValidationTestFixture(
overallSchema = mapOf(
"activity" to /* language=GraphQL*/ """
type Query {
myActivity: [Activity]
}
union ActivityContent = User | Issue
type Activity {
id: ID!
data: ActivityContent
@hydrated(
service: "users"
field: "usersByIds"
arguments: [
{name: "ids", value: "$source.userId"}
]
)
@hydrated(
service: "issues"
field: "issuesByIds"
arguments: [
{name: "ids", value: "$source.issueId"}
]
)
}
""".trimIndent(),
"users" to /* language=GraphQL*/ """
type Query {
usersByIds(ids: [ID]!): [User]
}
type User {
id: ID!
name: String!
}
""".trimIndent(),
"issues" to /* language=GraphQL*/ """
type Query {
issuesByIds(ids: [ID]!): [Issue]
}
type Issue {
id: ID!
key: String
}
""".trimIndent(),
),
underlyingSchema = mapOf(
"activity" to /* language=GraphQL*/ """
type Query {
myActivity: [Activity]
}
type Activity {
id: ID!
userId: ID
issueId: ID
}
""".trimIndent(),
"users" to /* language=GraphQL*/ """
type Query {
usersByIds(ids: [ID]!): [User]
}
type User {
id: ID!
name: String!
}
type Account {
id: ID!
}
""".trimIndent(),
"issues" to /* language=GraphQL*/ """
type Query {
issuesByIds(ids: [ID]!): [Issue]
}
type Issue {
id: ID!
key: String
}
""".trimIndent(),
),
)

val errors = validate(fixture)
assertTrue(errors.map { it.message }.isEmpty())
}

@Test
fun `permit multiple source fields non batched hydration`() {
val fixture = NadelValidationTestFixture(
overallSchema = mapOf(
"activity" to /* language=GraphQL*/ """
type Query {
myActivity: [Activity]
}
union ActivityContent = User | Issue
type Activity {
id: ID!
data: ActivityContent
@hydrated(
service: "users"
field: "userById"
arguments: [
{name: "id", value: "$source.userId"}
]
)
@hydrated(
service: "issues"
field: "issueById"
arguments: [
{name: "id", value: "$source.issueId"}
]
)
}
""".trimIndent(),
"users" to /* language=GraphQL*/ """
type Query {
userById(id: ID): User
}
type User {
id: ID!
name: String!
}
""".trimIndent(),
"issues" to /* language=GraphQL*/ """
type Query {
issueById(id: ID): Issue
}
type Issue {
id: ID!
key: String
}
""".trimIndent(),
),
underlyingSchema = mapOf(
"activity" to /* language=GraphQL*/ """
type Query {
myActivity: [Activity]
}
type Activity {
id: ID!
userId: ID
issueId: ID
}
""".trimIndent(),
"users" to /* language=GraphQL*/ """
type Query {
userById(id: ID): User
}
type User {
id: ID!
name: String!
}
type Account {
id: ID!
}
""".trimIndent(),
"issues" to /* language=GraphQL*/ """
type Query {
issueById(id: ID): Issue
}
type Issue {
id: ID!
key: String
}
""".trimIndent(),
),
)

val errors = validate(fixture)
assertTrue(errors.map { it.message }.isEmpty())
}
}

0 comments on commit 3629453

Please sign in to comment.