Skip to content

Commit

Permalink
Add new DgsDataFetchingEnvironment.isArgumentSet (#1987)
Browse files Browse the repository at this point in the history
* Add new DgsDataFetchingEnvironment.isArgumentSet utility method to check if an argument is explicitly set.

* Support whitespace in String paths

* Separate method for "." and "->" so that there is no overhead when not used, and optimized the argument traversing.

* format
  • Loading branch information
paulbakker authored Aug 16, 2024
1 parent 3be2d9d commit 30bc5e3
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@

import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.DgsDataFetchingEnvironment;
import com.netflix.graphql.dgs.InputArgument;
import com.netflix.graphql.dgs.example.shared.types.Rating;

@DgsComponent
public class RatingMutation {
@DgsData(parentType = "Mutation", field = "addRating")
public Rating addRating(@InputArgument("input") RatingInput ratingInput) {
public Rating addRating(@InputArgument("input") RatingInput ratingInput, DgsDataFetchingEnvironment dfe) {
if(!dfe.isArgumentSet("input", "title")) {
throw new IllegalArgumentException("Title must be explicitly provided");
}

if(ratingInput.getStars() < 0) {
throw new IllegalArgumentException("Stars must be 1-5");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.netflix.graphql.dgs.exceptions.NoDataLoaderFoundException
import com.netflix.graphql.dgs.internal.utils.DataLoaderNameUtil
import graphql.schema.DataFetchingEnvironment
import org.dataloader.DataLoader
import java.util.*

class DgsDataFetchingEnvironment(
private val dfe: DataFetchingEnvironment,
Expand Down Expand Up @@ -51,4 +52,35 @@ class DgsDataFetchingEnvironment(
}
return getDataLoader(loaderName) ?: throw NoDataLoaderFoundException("DataLoader with name $loaderName not found")
}

/**
* Check if an argument is explicitly set using "argument.nested.property" or "argument->nested->property" syntax.
* Note that this requires String splitting which is expensive for hot code paths.
* Use the isArgumentSet(String...) as a faster alternative.
*/
fun isNestedArgumentSet(path: String): Boolean {
val pathParts = path.split(".", "->").map { s -> s.trim() }
return isArgumentSet(*pathParts.toTypedArray())
}

/**
* Check if an argument is explicitly set.
* For complex object arguments, use the isArgumentSet("root", "nested", "property") syntax.
*/
fun isArgumentSet(vararg path: String): Boolean = isArgumentSet(path.asSequence())

private fun isArgumentSet(keys: Sequence<String>): Boolean {
var args: Map<*, *> = dfe.executionStepInfo.arguments
var value: Any?
for (key in keys) {
// Explicitly check contains to support explicit null values
if (!args.contains(key)) return false
value = args[key]
if (value !is Map<*, *>) {
return true
}
args = value
}
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2024 Netflix, 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
*
* http://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.
*/

package com.netflix.graphql.dgs

import com.netflix.graphql.dgs.internal.DefaultInputObjectMapper
import com.netflix.graphql.dgs.internal.DgsSchemaProvider
import com.netflix.graphql.dgs.internal.method.DataFetchingEnvironmentArgumentResolver
import com.netflix.graphql.dgs.internal.method.InputArgumentResolver
import com.netflix.graphql.dgs.internal.method.MethodDataFetcherFactory
import graphql.ExecutionInput
import graphql.GraphQL
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.runner.ApplicationContextRunner
import java.util.*

class DgsDataFetchingEnvironmentIsArgumentSet {
private val contextRunner = ApplicationContextRunner()

@Test
fun `DgsDataFetcherEnvironment isArgumentSet should support top level arguments, complex arguments, and explicit nulls`() {
contextRunner.withBean(HelloFetcher::class.java).run { context ->
val schemaProvider =
DgsSchemaProvider(
applicationContext = context,
federationResolver = Optional.empty(),
existingTypeDefinitionRegistry = Optional.empty(),
methodDataFetcherFactory =
MethodDataFetcherFactory(
listOf(
DataFetchingEnvironmentArgumentResolver(),
InputArgumentResolver(
DefaultInputObjectMapper(),
),
),
),
)
val schema =
schemaProvider
.schema(
"""
type Mutation {
inputTester(topLevelArg: String): Boolean
complexInputTester(personInput: PersonInput): InputTestResult
complexInputTesterWithDelimiter(personInput: PersonInput): InputTestResult
}
type InputTestResult {
name: Boolean
city: Boolean
}
input PersonInput {
name: String
address: Address
}
input Address {
city: String
}
""".trimIndent(),
).graphQLSchema
val build = GraphQL.newGraphQL(schema).build()
val executionInput: ExecutionInput =
ExecutionInput
.newExecutionInput()
.query(
"""mutation {
| providedTopLevel: inputTester(topLevelArg: "test")
| notProvidedTopLevel: inputTester
| explicitNullTopLevel: inputTester(topLevelArg: null)
| noPersonProvided: complexInputTester { name, city }
| nameProvided: complexInputTester(personInput: {name: "DGS" }) { name, city }
| nameAndCityProvided: complexInputTester(personInput: {name: "DGS", address: {city: "San Jose"} }) { name, city }
| explicitNullCity: complexInputTester(personInput: {address: {city: null} }) { name, city }
| complexInputTesterWithDelimiter(personInput: {name: "DGS", address: {city: "San Jose"} }) { name, city }
|}
|
""".trimMargin(),
).build()
val executionResult = build.execute(executionInput)
assertTrue(executionResult.isDataPresent)
val result = executionResult.getData() as Map<String, String>
assertEquals(true, result["providedTopLevel"], "Explicitly provided top level argument")
assertEquals(false, result["notProvidedTopLevel"], "Not provided top level argument")
assertEquals(true, result["explicitNullTopLevel"], "Explicitly null value provided for top level argument")
assertEquals(
mapOf(Pair("name", false), Pair("city", false)),
result["noPersonProvided"] as Map<*, *>,
"Complex input type not provided at all",
)
assertEquals(
mapOf(Pair("name", true), Pair("city", false)),
result["nameProvided"] as Map<*, *>,
"Complex input type with first level property provided",
)
assertEquals(
mapOf(Pair("name", true), Pair("city", true)),
result["nameAndCityProvided"] as Map<*, *>,
"Complex input type with multiple levels of properties provided",
)
assertEquals(
mapOf(Pair("name", false), Pair("city", true)),
result["explicitNullCity"] as Map<*, *>,
"Explicit null value for a nested property",
)
assertEquals(
mapOf(Pair("name", true), Pair("city", true)),
result["complexInputTesterWithDelimiter"] as Map<*, *>,
"Paths can be expressed with . and ->",
)
}
}

@DgsComponent
class HelloFetcher {
/**
* Check if a top level argument was provided explicitly.
* An explicit null value should also return true.
*/
@DgsMutation
fun inputTester(
@InputArgument topLevelArg: String?,
dfe: DgsDataFetchingEnvironment,
): Boolean = dfe.isArgumentSet("topLevelArg")

/**
* Check properties of complex input type if they were provided explicitly.
* An explicit null value should also return true.
* This example uses var args for the property path (e.g. isArgumentSet("personInput", "address", "city"))
*/
@DgsMutation
fun complexInputTester(
@InputArgument personInput: PersonInput?,
dfe: DgsDataFetchingEnvironment,
): InputTestResult {
val nameIsSet = dfe.isArgumentSet("personInput", "name")
val cityIsSet = dfe.isArgumentSet("personInput", "address", "city")

return InputTestResult(nameIsSet, cityIsSet)
}

/**
* Check properties of complex input type if they were provided explicitly.
* An explicit null value should also return true.
* This example uses the . and -> delimiters for the property paths (e.g. isArgumentSet(personInput->address->city))
*/
@DgsMutation
fun complexInputTesterWithDelimiter(
@InputArgument personInput: PersonInput?,
dfe: DgsDataFetchingEnvironment,
): InputTestResult {
val nameIsSet = dfe.isNestedArgumentSet("personInput.name")
val cityIsSet = dfe.isNestedArgumentSet("personInput->address -> city")

return InputTestResult(nameIsSet, cityIsSet)
}
}

data class PersonInput(
val name: String?,
val address: Address?,
)

data class Address(
val city: String?,
)

data class InputTestResult(
val name: Boolean,
val city: Boolean,
)
}

0 comments on commit 30bc5e3

Please sign in to comment.