Skip to content

Commit aa9eb6c

Browse files
authored
Merge pull request #2147 from Netflix/feature/source-argument
Add @source annotation to get easy access to the Source object in a @DgsData fetcher.
2 parents 0dc9187 + a63ec7d commit aa9eb6c

File tree

5 files changed

+246
-0
lines changed

5 files changed

+246
-0
lines changed

graphql-dgs-spring-graphql/src/main/kotlin/com/netflix/graphql/dgs/autoconfig/DgsInputArgumentConfiguration.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.netflix.graphql.dgs.internal.method.ContinuationArgumentResolver
2323
import com.netflix.graphql.dgs.internal.method.DataFetchingEnvironmentArgumentResolver
2424
import com.netflix.graphql.dgs.internal.method.FallbackEnvironmentArgumentResolver
2525
import com.netflix.graphql.dgs.internal.method.InputArgumentResolver
26+
import com.netflix.graphql.dgs.internal.method.SourceArgumentResolver
2627
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
2728
import org.springframework.context.ApplicationContext
2829
import org.springframework.context.annotation.Bean
@@ -50,4 +51,7 @@ open class DgsInputArgumentConfiguration {
5051
@Bean
5152
@ConditionalOnMissingBean
5253
open fun defaultInputObjectMapper(): InputObjectMapper = DefaultInputObjectMapper()
54+
55+
@Bean
56+
open fun sourceArgumentResolver(): ArgumentResolver = SourceArgumentResolver()
5357
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.graphql.dgs;
18+
19+
import java.lang.annotation.*;
20+
21+
@Target(ElementType.PARAMETER)
22+
@Retention(RetentionPolicy.RUNTIME)
23+
@Inherited
24+
public @interface Source {
25+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.graphql.dgs.internal.method
18+
19+
import com.netflix.graphql.dgs.Source
20+
import graphql.schema.DataFetchingEnvironment
21+
import org.springframework.core.MethodParameter
22+
23+
class SourceArgumentResolver : ArgumentResolver {
24+
override fun supportsParameter(parameter: MethodParameter): Boolean = parameter.hasParameterAnnotation(Source::class.java)
25+
26+
override fun resolveArgument(
27+
parameter: MethodParameter,
28+
dfe: DataFetchingEnvironment,
29+
): Any {
30+
val source = dfe.getSource<Any>()
31+
if (source == null) {
32+
throw IllegalArgumentException("Source is null. Are you trying to use @Source on a root field (e.g. @DgsQuery)?")
33+
}
34+
35+
if (parameter.parameterType == source.javaClass) {
36+
return source
37+
} else {
38+
throw IllegalArgumentException(
39+
"Invalid source type '${source?.javaClass?.name}'. Expected type '${parameter.parameterType.name}'",
40+
)
41+
}
42+
}
43+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.graphql.dgs.internal
18+
19+
import com.netflix.graphql.dgs.DgsComponent
20+
import com.netflix.graphql.dgs.DgsData
21+
import com.netflix.graphql.dgs.DgsQuery
22+
import com.netflix.graphql.dgs.Source
23+
import com.netflix.graphql.dgs.internal.method.DataFetchingEnvironmentArgumentResolver
24+
import com.netflix.graphql.dgs.internal.method.FallbackEnvironmentArgumentResolver
25+
import com.netflix.graphql.dgs.internal.method.InputArgumentResolver
26+
import com.netflix.graphql.dgs.internal.method.MethodDataFetcherFactory
27+
import com.netflix.graphql.dgs.internal.method.SourceArgumentResolver
28+
import graphql.GraphQL
29+
import org.assertj.core.api.Assertions.assertThat
30+
import org.junit.jupiter.api.Test
31+
import org.springframework.boot.test.context.runner.ApplicationContextRunner
32+
import org.springframework.context.ApplicationContext
33+
import java.util.*
34+
35+
internal class SourceArgumentTest {
36+
private val contextRunner = ApplicationContextRunner()
37+
38+
private fun schemaProvider(applicationContext: ApplicationContext) =
39+
DgsSchemaProvider(
40+
applicationContext = applicationContext,
41+
federationResolver = Optional.empty(),
42+
existingTypeDefinitionRegistry = Optional.empty(),
43+
schemaLocations = listOf("source-argument-test/schema.graphqls"),
44+
methodDataFetcherFactory =
45+
MethodDataFetcherFactory(
46+
listOf(
47+
InputArgumentResolver(DefaultInputObjectMapper()),
48+
DataFetchingEnvironmentArgumentResolver(applicationContext),
49+
SourceArgumentResolver(),
50+
FallbackEnvironmentArgumentResolver(DefaultInputObjectMapper()),
51+
),
52+
),
53+
)
54+
55+
data class Show(
56+
val title: String,
57+
)
58+
59+
@Test
60+
fun `@Source argument`() {
61+
@DgsComponent
62+
class Fetcher {
63+
@DgsQuery
64+
fun shows(): List<Show> = listOf(Show("Stranger Things"))
65+
66+
@DgsData(parentType = "Show")
67+
fun description(
68+
@Source show: Show,
69+
): String = "Description of ${show.title}"
70+
}
71+
72+
contextRunner.withBean(Fetcher::class.java).run { context ->
73+
val provider = schemaProvider(context)
74+
val schema = provider.schema().graphQLSchema
75+
76+
val build = GraphQL.newGraphQL(schema).build()
77+
val executionResult =
78+
build.execute(
79+
"""{
80+
| shows {
81+
| title
82+
| description
83+
| }
84+
|}
85+
""".trimMargin(),
86+
)
87+
88+
assertThat(executionResult.errors).isEmpty()
89+
assertThat(executionResult.isDataPresent).isTrue
90+
val data = executionResult.getData<Map<String, *>>()
91+
92+
@Suppress("UNCHECKED_CAST")
93+
val showData = (data["shows"] as List<Map<*, *>>)[0]
94+
assertThat(showData["title"]).isEqualTo("Stranger Things")
95+
assertThat(showData["description"]).isEqualTo("Description of Stranger Things")
96+
}
97+
}
98+
99+
@Test
100+
fun `Incorrect @Source argument type`() {
101+
@DgsComponent
102+
class Fetcher {
103+
@DgsQuery
104+
fun shows(): List<Show> = listOf(Show("Stranger Things"))
105+
106+
@DgsData(parentType = "Show")
107+
fun description(
108+
@Source show: String,
109+
): String = "Should not be called"
110+
}
111+
112+
contextRunner.withBean(Fetcher::class.java).run { context ->
113+
val provider = schemaProvider(context)
114+
val schema = provider.schema().graphQLSchema
115+
116+
val build = GraphQL.newGraphQL(schema).build()
117+
val executionResult =
118+
build.execute(
119+
"""{
120+
| shows {
121+
| title
122+
| description
123+
| }
124+
|}
125+
""".trimMargin(),
126+
)
127+
128+
assertThat(executionResult.errors).isNotEmpty()
129+
assertThat(
130+
executionResult.errors[0].message,
131+
).contains("Invalid source type 'com.netflix.graphql.dgs.internal.SourceArgumentTest\$Show'. Expected type 'java.lang.String'")
132+
}
133+
}
134+
135+
@Test
136+
fun `Using @Source on a root datafetcher should fail`() {
137+
@DgsComponent
138+
class Fetcher {
139+
@DgsQuery
140+
fun shows(
141+
@Source something: String,
142+
): List<Show> = listOf(Show("Stranger Things"))
143+
}
144+
145+
contextRunner.withBean(Fetcher::class.java).run { context ->
146+
val provider = schemaProvider(context)
147+
val schema = provider.schema().graphQLSchema
148+
149+
val build = GraphQL.newGraphQL(schema).build()
150+
val executionResult =
151+
build.execute(
152+
"""{
153+
| shows {
154+
| title
155+
| }
156+
|}
157+
""".trimMargin(),
158+
)
159+
160+
assertThat(executionResult.errors).isNotEmpty()
161+
assertThat(
162+
executionResult.errors[0].message,
163+
).contains("Source is null. Are you trying to use @Source on a root field (e.g. @DgsQuery)?")
164+
}
165+
}
166+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type Query {
2+
shows: [Show]
3+
}
4+
5+
type Show {
6+
title: String
7+
description: String
8+
}

0 commit comments

Comments
 (0)