Skip to content

Commit 14efcc8

Browse files
authored
Handle hydration redefinition in virtual types (#671)
* Handle hydration redefinition in virtual types * Add doco * Kick build
1 parent da8ac0d commit 14efcc8

4 files changed

Lines changed: 588 additions & 14 deletions

File tree

lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationTransform.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,10 @@ internal class NadelHydrationTransform(
7777
hydrationDetails: ServiceExecutionHydrationDetails?,
7878
): State? {
7979
val instructionsByObjectTypeName = executionBlueprint
80-
.getTypeNameToInstructionsMap<NadelHydrationFieldInstruction>(overallField)
80+
.getInstructionInsideVirtualType<NadelHydrationFieldInstruction>(hydrationDetails, overallField)
8181
.ifEmpty {
82-
if (executionContext.hints.virtualTypeSupport(service)) {
83-
executionBlueprint
84-
.getInstructionInsideVirtualType(hydrationDetails, overallField)
85-
} else {
86-
emptyMap()
87-
}
82+
executionBlueprint
83+
.getTypeNameToInstructionsMap<NadelHydrationFieldInstruction>(overallField)
8884
}
8985

9086
return if (instructionsByObjectTypeName.isEmpty()) {

lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationTransform.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,10 @@ internal class NadelBatchHydrationTransform(
4747
hydrationDetails: ServiceExecutionHydrationDetails?,
4848
): State? {
4949
val instructionsByObjectTypeName = executionBlueprint
50-
.getTypeNameToInstructionsMap<NadelBatchHydrationFieldInstruction>(overallField)
50+
.getInstructionInsideVirtualType<NadelBatchHydrationFieldInstruction>(hydrationDetails, overallField)
5151
.ifEmpty {
52-
if (executionContext.hints.virtualTypeSupport(service)) {
53-
executionBlueprint
54-
.getInstructionInsideVirtualType(hydrationDetails, overallField)
55-
} else {
56-
emptyMap()
57-
}
52+
executionBlueprint
53+
.getTypeNameToInstructionsMap<NadelBatchHydrationFieldInstruction>(overallField)
5854
}
5955

6056
return if (instructionsByObjectTypeName.isNotEmpty()) {
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package graphql.nadel.tests.next.fixtures.hydration.statics
2+
3+
import graphql.nadel.Nadel
4+
import graphql.nadel.NadelExecutionHints
5+
import graphql.nadel.engine.blueprint.NadelGenericHydrationInstruction
6+
import graphql.nadel.engine.transform.artificial.NadelAliasHelper
7+
import graphql.nadel.engine.transform.result.json.JsonNode
8+
import graphql.nadel.engine.util.JsonMap
9+
import graphql.nadel.engine.util.strictAssociateBy
10+
import graphql.nadel.hooks.NadelExecutionHooks
11+
import graphql.nadel.tests.next.NadelIntegrationTest
12+
import graphql.normalized.ExecutableNormalizedField
13+
14+
/**
15+
* Tests when the backing type itself declares a hydration.
16+
* Then the virtual type redefines that hydration.
17+
*
18+
* i.e. `GraphStoreQueryEdge.node` defines a hydration and `WorkEdge.node` defines
19+
* a hydration over it.
20+
*/
21+
class StaticHydrationOverlappingHydrationTest : NadelIntegrationTest(
22+
query = """
23+
query {
24+
graphStore_query(query: "Hello World") {
25+
edges {
26+
node {
27+
__typename
28+
}
29+
}
30+
}
31+
businessReport_findRecentWorkByTeam(teamId: "hello") {
32+
__typename
33+
edges {
34+
__typename
35+
node {
36+
__typename
37+
... on JiraIssue {
38+
key
39+
}
40+
}
41+
cursor
42+
}
43+
pageInfo {
44+
__typename
45+
hasNextPage
46+
}
47+
}
48+
}
49+
""".trimIndent(),
50+
services = listOf(
51+
// Backing service
52+
Service(
53+
name = "graph_store",
54+
overallSchema = """
55+
type Query {
56+
graphStore_query(
57+
query: String!
58+
first: Int
59+
after: String
60+
): GraphStoreQueryConnection
61+
}
62+
type GraphStoreQueryConnection {
63+
edges: [GraphStoreQueryEdge]
64+
pageInfo: PageInfo
65+
}
66+
type GraphStoreQueryEdge {
67+
nodeId: ID
68+
node: GraphStoreQueryNode @idHydrated(idField: "nodeId")
69+
cursor: String
70+
}
71+
union GraphStoreQueryNode = JiraComment
72+
type PageInfo {
73+
hasNextPage: Boolean!
74+
hasPreviousPage: Boolean!
75+
startCursor: String
76+
endCursor: String
77+
}
78+
""".trimIndent(),
79+
underlyingSchema = """
80+
type Query {
81+
graphStore_query(
82+
query: String!
83+
first: Int
84+
after: String
85+
): GraphStoreQueryConnection
86+
}
87+
type GraphStoreQueryConnection {
88+
edges: [GraphStoreQueryEdge]
89+
pageInfo: PageInfo
90+
}
91+
type GraphStoreQueryEdge {
92+
nodeId: ID
93+
cursor: String
94+
}
95+
type PageInfo {
96+
hasNextPage: Boolean!
97+
hasPreviousPage: Boolean!
98+
startCursor: String
99+
endCursor: String
100+
}
101+
""".trimIndent(),
102+
runtimeWiring = { wiring ->
103+
data class GraphStoreQueryEdge(
104+
val nodeId: String,
105+
val cursor: String?,
106+
)
107+
108+
data class PageInfo(
109+
val hasNextPage: Boolean,
110+
val hasPreviousPage: Boolean,
111+
val startCursor: String?,
112+
val endCursor: String?,
113+
)
114+
115+
data class GraphStoreQueryConnection(
116+
val edges: List<GraphStoreQueryEdge>,
117+
val pageInfo: PageInfo,
118+
)
119+
120+
wiring
121+
.type("Query") { type ->
122+
type
123+
.dataFetcher("graphStore_query") { env ->
124+
GraphStoreQueryConnection(
125+
edges = listOf(
126+
GraphStoreQueryEdge(
127+
nodeId = "ari:cloud:jira::issue/1",
128+
cursor = "1",
129+
),
130+
GraphStoreQueryEdge(
131+
nodeId = "ari:cloud:jira::comment/2",
132+
cursor = "1",
133+
),
134+
),
135+
pageInfo = PageInfo(
136+
hasNextPage = true,
137+
hasPreviousPage = false,
138+
startCursor = null,
139+
endCursor = "1",
140+
),
141+
)
142+
}
143+
}
144+
},
145+
),
146+
// Service for hydration
147+
Service(
148+
name = "jira",
149+
overallSchema = """
150+
type Query {
151+
issues(ids: [ID!]!): [JiraIssue]
152+
comments(ids: [ID!]!): [JiraComment]
153+
}
154+
type JiraIssue @defaultHydration(field: "issues", idArgument: "ids") {
155+
id: ID!
156+
key: String!
157+
title: String!
158+
}
159+
type JiraComment @defaultHydration(field: "comments", idArgument: "ids") {
160+
id: ID!
161+
text: String!
162+
}
163+
""".trimIndent(),
164+
runtimeWiring = { runtime ->
165+
data class JiraIssue(
166+
val id: String,
167+
val key: String,
168+
val title: String,
169+
)
170+
171+
data class JiraComment(
172+
val id: String,
173+
val text: String,
174+
)
175+
176+
val issuesById = listOf(
177+
JiraIssue(
178+
id = "ari:cloud:jira::issue/1",
179+
key = "GQLGW-1",
180+
title = "Fix the speed of light",
181+
),
182+
JiraIssue(
183+
id = "ari:cloud:jira::issue/2",
184+
key = "GQLGW-2",
185+
title = "Refactor Nadel",
186+
),
187+
).strictAssociateBy { it.id }
188+
189+
val commentsById = listOf(
190+
JiraComment(
191+
id = "ari:cloud:jira::comment/2",
192+
text = "Hello world",
193+
),
194+
).strictAssociateBy { it.id }
195+
196+
runtime
197+
.type("Query") { type ->
198+
type
199+
.dataFetcher("issues") { env ->
200+
env.getArgument<List<String>>("ids")!!.map(issuesById::get)
201+
}
202+
.dataFetcher("comments") { env ->
203+
env.getArgument<List<String>>("ids")!!.map(commentsById::get)
204+
}
205+
}
206+
},
207+
),
208+
// Service that introduces virtual type
209+
Service(
210+
name = "work",
211+
overallSchema = """
212+
type Query {
213+
businessReport_findRecentWorkByTeam(
214+
teamId: ID!
215+
first: Int
216+
after: String
217+
): WorkConnection
218+
@hydrated(
219+
service: "graph_store",
220+
field: "graphStore_query"
221+
arguments: [
222+
{
223+
name: "query"
224+
value: "SELECT * FROM Work WHERE teamId = ?"
225+
}
226+
{
227+
name: "first"
228+
value: "$argument.first"
229+
}
230+
{
231+
name: "after"
232+
value: "$argument.after"
233+
}
234+
]
235+
)
236+
}
237+
type WorkConnection @virtualType {
238+
edges: [WorkEdge]
239+
pageInfo: PageInfo
240+
}
241+
type WorkEdge @virtualType {
242+
nodeId: ID @hidden
243+
node: WorkNode @idHydrated(idField: "nodeId")
244+
cursor: String
245+
}
246+
union WorkNode = JiraIssue | JiraComment
247+
""".trimIndent(),
248+
underlyingSchema = """
249+
type Query {
250+
business_stub: String
251+
}
252+
""".trimIndent(),
253+
runtimeWiring = { wiring ->
254+
},
255+
),
256+
),
257+
) {
258+
override fun makeExecutionHints(): NadelExecutionHints.Builder {
259+
return super.makeExecutionHints()
260+
.virtualTypeSupport { true }
261+
.shortCircuitEmptyQuery { true }
262+
}
263+
264+
override fun makeNadel(): Nadel.Builder {
265+
return super.makeNadel()
266+
.executionHooks(
267+
object : NadelExecutionHooks {
268+
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
269+
virtualField: ExecutableNormalizedField,
270+
instructions: List<T>,
271+
sourceInput: JsonNode,
272+
userContext: Any?,
273+
): T? {
274+
throw UnsupportedOperationException()
275+
}
276+
277+
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
278+
virtualField: ExecutableNormalizedField,
279+
instructions: List<T>,
280+
parentNode: JsonNode,
281+
aliasHelper: NadelAliasHelper,
282+
userContext: Any?,
283+
): T {
284+
return if (instructions.size > 1) {
285+
@Suppress("UNCHECKED_CAST")
286+
val nodeId = (parentNode.value as JsonMap)[aliasHelper.getResultKey("nodeId")] as String
287+
val ati = nodeId.substringBefore("/")
288+
when (ati) {
289+
"ari:cloud:jira::issue" -> instructions.single { it.backingFieldDef.name == "issues" }
290+
"ari:cloud:jira::comment" -> instructions.single { it.backingFieldDef.name == "comments" }
291+
else -> throw UnsupportedOperationException()
292+
}
293+
} else {
294+
instructions.single()
295+
}
296+
}
297+
}
298+
)
299+
}
300+
}

0 commit comments

Comments
 (0)