Skip to content

Commit 543e8ac

Browse files
committed
don't remove fragments if they are added by other transforms
1 parent 2a4f8eb commit 543e8ac

3 files changed

Lines changed: 195 additions & 1 deletion

File tree

lib/src/main/java/graphql/nadel/engine/transform/NadelNoInterfaceToObjectFragmentExpansionTransform.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ class NadelNoInterfaceToObjectFragmentExpansionTransform : NadelTransform<State>
4444
val aliasHelper: NadelAliasHelper,
4545
val exposedOverallImplNames: Set<String>,
4646
val relaxedFieldResultKey: String,
47-
)
47+
) {
48+
// Applicability is planned before query transforms run. Record whether the final field was actually relaxed
49+
// so result filtering cannot act on stale planned state.
50+
var wasRelaxed = false
51+
}
4852

4953
override suspend fun isApplicable(
5054
executionContext: NadelExecutionContext,
@@ -96,6 +100,14 @@ class NadelNoInterfaceToObjectFragmentExpansionTransform : NadelTransform<State>
96100
state: State,
97101
transformServiceExecutionContext: NadelTransformServiceExecutionContext?,
98102
): NadelTransformFieldResult {
103+
// An earlier transform may have narrowed this field since isApplicable was evaluated.
104+
val currentExposedImplNames =
105+
computeExposedImplNamesIfRelaxable(executionBlueprint, service, field)
106+
if (currentExposedImplNames != state.exposedOverallImplNames) {
107+
return NadelTransformFieldResult.unmodified(field)
108+
}
109+
state.wasRelaxed = true
110+
99111
// Keep objectTypeNames honest (only the exposed members). Rather than widen them, flag the field and
100112
// the injected __typename as forcePrintBare so the forked compiler emits them without a `... on Type`
101113
// fragment. See [NadelTransformFieldResult.forcePrintBare].
@@ -124,6 +136,10 @@ class NadelNoInterfaceToObjectFragmentExpansionTransform : NadelTransform<State>
124136
nodes: JsonNodes,
125137
transformServiceExecutionContext: NadelTransformServiceExecutionContext?,
126138
): List<NadelResultInstruction> {
139+
if (!state.wasRelaxed) {
140+
return emptyList()
141+
}
142+
127143
val parentPath = underlyingParentField?.queryPath ?: overallField.parent?.queryPath ?: return emptyList()
128144
val parentNodes = nodes.getNodesAt(parentPath, flatten = true)
129145
val key = state.aliasHelper.typeNameResultKey
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package graphql.nadel.tests.next.fixtures.execution.interfaceexpansion.hiddenmembership
2+
3+
import graphql.nadel.Nadel
4+
import graphql.nadel.NadelExecutionHints
5+
import graphql.nadel.Service
6+
import graphql.nadel.ServiceExecutionHydrationDetails
7+
import graphql.nadel.engine.NadelExecutionContext
8+
import graphql.nadel.engine.NadelServiceExecutionContext
9+
import graphql.nadel.engine.blueprint.NadelOverallExecutionBlueprint
10+
import graphql.nadel.engine.transform.NadelTransformFieldResult
11+
import graphql.nadel.engine.transform.NadelTransformServiceExecutionContext
12+
import graphql.nadel.engine.transform.query.NadelQueryTransformer
13+
import graphql.nadel.engine.util.toBuilder
14+
import graphql.nadel.tests.util.NadelTransformAdapter
15+
import graphql.normalized.ExecutableNormalizedField
16+
17+
/**
18+
* Simulates an earlier transform narrowing an abstract selection to one implementation. The
19+
* [NadelNoInterfaceToObjectFragmentExpansionTransform] transform must re-evaluate the narrowed field instead of
20+
* applying its precomputed plan: should keep the fragments on interfaces if they were added by previous transforms.
21+
*/
22+
class HiddenImplRelaxedSelectionNarrowedByEarlierTransformTest : HiddenImplementationTestBase(
23+
query = """
24+
query {
25+
nodes {
26+
id
27+
}
28+
}
29+
""".trimIndent(),
30+
) {
31+
override fun makeExecutionHints(): NadelExecutionHints.Builder {
32+
return super.makeExecutionHints().noInterfaceToObjectFragmentExpansion { _ -> true }
33+
}
34+
35+
override fun makeNadel(): Nadel.Builder {
36+
return super.makeNadel()
37+
.transforms(listOf(NarrowIdSelectionToIssueTransform()))
38+
}
39+
}
40+
41+
private class NarrowIdSelectionToIssueTransform : NadelTransformAdapter {
42+
override suspend fun isApplicable(
43+
executionContext: NadelExecutionContext,
44+
serviceExecutionContext: NadelServiceExecutionContext,
45+
executionBlueprint: NadelOverallExecutionBlueprint,
46+
services: Map<String, Service>,
47+
service: Service,
48+
overallField: ExecutableNormalizedField,
49+
transformServiceExecutionContext: NadelTransformServiceExecutionContext?,
50+
hydrationDetails: ServiceExecutionHydrationDetails?,
51+
): Unit? {
52+
return Unit.takeIf {
53+
overallField.name == "id" &&
54+
overallField.objectTypeNames.toSet() == setOf("Issue", "Story")
55+
}
56+
}
57+
58+
override suspend fun transformField(
59+
executionContext: NadelExecutionContext,
60+
serviceExecutionContext: NadelServiceExecutionContext,
61+
transformer: NadelQueryTransformer,
62+
executionBlueprint: NadelOverallExecutionBlueprint,
63+
service: Service,
64+
field: ExecutableNormalizedField,
65+
state: Unit,
66+
transformServiceExecutionContext: NadelTransformServiceExecutionContext?,
67+
): NadelTransformFieldResult {
68+
return NadelTransformFieldResult(
69+
newField = field.toBuilder()
70+
.clearObjectTypesNames()
71+
.objectTypeNames(listOf("Issue"))
72+
.build(),
73+
)
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// @formatter:off
2+
package graphql.nadel.tests.next.fixtures.execution.interfaceexpansion.hiddenmembership
3+
4+
import graphql.nadel.tests.next.ExpectedNadelResult
5+
import graphql.nadel.tests.next.ExpectedServiceCall
6+
import graphql.nadel.tests.next.TestSnapshot
7+
import graphql.nadel.tests.next.listOfJsonStrings
8+
import kotlin.Suppress
9+
import kotlin.collections.List
10+
import kotlin.collections.listOf
11+
12+
private suspend fun main() {
13+
graphql.nadel.tests.next.update<HiddenImplRelaxedSelectionNarrowedByEarlierTransformTest>()
14+
}
15+
16+
/**
17+
* This class is generated. Do NOT modify.
18+
*
19+
* Refer to [graphql.nadel.tests.next.UpdateTestSnapshots]
20+
*/
21+
@Suppress("unused")
22+
public class HiddenImplRelaxedSelectionNarrowedByEarlierTransformTestSnapshot : TestSnapshot() {
23+
/**
24+
* Query
25+
*
26+
* ```graphql
27+
* query {
28+
* nodes {
29+
* id
30+
* }
31+
* }
32+
* ```
33+
*
34+
* Variables
35+
*
36+
* ```json
37+
* {}
38+
* ```
39+
*/
40+
override val calls: List<ExpectedServiceCall> = listOf(
41+
ExpectedServiceCall(
42+
service = "data",
43+
query = """
44+
| {
45+
| nodes {
46+
| ... on Issue {
47+
| id
48+
| }
49+
| }
50+
| }
51+
""".trimMargin(),
52+
variables = "{}",
53+
result = """
54+
| {
55+
| "data": {
56+
| "nodes": [
57+
| {
58+
| "id": "ISSUE-1"
59+
| },
60+
| {},
61+
| {}
62+
| ]
63+
| }
64+
| }
65+
""".trimMargin(),
66+
delayedResults = listOfJsonStrings(
67+
),
68+
),
69+
)
70+
71+
/**
72+
* ```json
73+
* {
74+
* "data": {
75+
* "nodes": [
76+
* {
77+
* "id": "ISSUE-1"
78+
* },
79+
* {},
80+
* {}
81+
* ]
82+
* }
83+
* }
84+
* ```
85+
*/
86+
override val result: ExpectedNadelResult = ExpectedNadelResult(
87+
result = """
88+
| {
89+
| "data": {
90+
| "nodes": [
91+
| {
92+
| "id": "ISSUE-1"
93+
| },
94+
| {},
95+
| {}
96+
| ]
97+
| }
98+
| }
99+
""".trimMargin(),
100+
delayedResults = listOfJsonStrings(
101+
),
102+
)
103+
}

0 commit comments

Comments
 (0)