Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[READY] Initial defer implementation #553

Merged
merged 11 commits into from
Jun 29, 2024
7 changes: 7 additions & 0 deletions lib/src/main/java/graphql/nadel/NextgenEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.asDeferred
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.reactive.asPublisher
import java.util.concurrent.CompletableFuture
import graphql.normalized.ExecutableNormalizedOperationFactory.Options.defaultOptions as executableNormalizedOperationFactoryOptions
Expand Down Expand Up @@ -358,6 +359,12 @@ internal class NextgenEngine(
)
}

if (serviceExecResult is NadelIncrementalServiceExecutionResult) {
executionContext.incrementalResultSupport.defer(
serviceExecResult.incrementalItemPublisher.asFlow()
)
}

return serviceExecResult.copy(
data = serviceExecResult.data.let { data ->
data.takeIf { transformedQuery.resultKey in data }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,15 @@ abstract class NadelIntegrationTest(
// Maybe this won't hold out longer term, but e.g. it's ok for the deferred errors to add a path
assertJsonEquals(
Copy link
Collaborator Author

@sbarker2 sbarker2 Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is because we want to ignore the locations field in errors when comparing defer vs non-defer queries because the line/column of the error would be different as the two queries look slightly different (one has @defer directive stripped)

expected = mapOf(
"errors" to noDeferResultMap["errors"],
"errors" to (noDeferResultMap["errors"] as? List<Map<String, Any>>)?.map { errorMap ->
errorMap.filterKeys { it != "locations" }
},
"extensions" to noDeferResultMap["extensions"],
),
actual = mapOf(
"errors" to combinedDeferResultMap["errors"],
"errors" to (combinedDeferResultMap["errors"] as? List<Map<String, Any>>)?.map { errorMap ->
errorMap.filterKeys { it != "locations" }
},
"extensions" to combinedDeferResultMap["extensions"],
),
mode = JSONCompareMode.LENIENT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package graphql.nadel.tests.next.fixtures.defer

import graphql.nadel.NadelExecutionHints
import graphql.nadel.tests.next.NadelIntegrationTest
import kotlin.test.Ignore

open class ComprehensiveDeferQueryWithDifferentServiceCalls : NadelIntegrationTest(
query = """
query {
user {
name
... @defer {
profilePicture
}
... @defer(label: "team-details") {
teamName
teamMembers
}
}
product {
productName
... @defer {
productImage
}
... @defer(if: false) {
productDescription
}
}
}
""".trimIndent(),
services = listOf(
Service(
name = "shared",
overallSchema = """
directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT
""".trimIndent(),
runtimeWiring = { wiring ->
},
),
Service(
name = "users",
overallSchema = """
type Query {
user: UserApi
}
type UserApi {
name: String
profilePicture: String
teamName: String
teamMembers: [String]
}
""".trimIndent(),
runtimeWiring = { wiring ->
wiring
.type("Query") { type ->
type
.dataFetcher("user") { env ->
Any()
}
}
.type("UserApi") { type ->
type
.dataFetcher("name") { env ->
"Steven"
}
.dataFetcher("profilePicture") { env ->
"https://examplesite.com/user/profile_picture.jpg"
}
.dataFetcher("teamName") { env ->
"The Unicorns"
}
.dataFetcher("teamMembers") { env ->
listOf("Felipe", "Franklin", "Juliano")
}
}
},
),
Service(
name = "product",
overallSchema = """
type Query {
product: ProductApi
}
type ProductApi {
productName: String
productImage: String
productDescription: String
}
""".trimIndent(),
runtimeWiring = { wiring ->
wiring
.type("Query") { type ->
type
.dataFetcher("product") { env ->
Any()
}
}
.type("ProductApi") { type ->
type
.dataFetcher("productName") { env ->
"Awesome Product"
}
.dataFetcher("profilePicture") { env ->
"https://examplesite.com/product/product_image.jpg"
}
.dataFetcher("profilePicture") { env ->
"This is a really awesome product with really awesome features."
}
}
},
),
),
) {
override fun makeExecutionHints(): NadelExecutionHints.Builder {
return super.makeExecutionHints()
.deferSupport { true }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// @formatter:off
package graphql.nadel.tests.next.fixtures.defer

import graphql.nadel.tests.next.ExpectedNadelResult
import graphql.nadel.tests.next.ExpectedServiceCall
import graphql.nadel.tests.next.TestSnapshot
import graphql.nadel.tests.next.listOfJsonStrings
import kotlin.Suppress
import kotlin.collections.List
import kotlin.collections.listOf

/**
* This class is generated. Do NOT modify.
*
* Refer to [graphql.nadel.tests.next.UpdateTestSnapshots
*/
@Suppress("unused")
public class ComprehensiveDeferQueryWithDifferentServiceCallsSnapshot : TestSnapshot() {
override val calls: List<ExpectedServiceCall> = listOf(
ExpectedServiceCall(
service = "product",
query = """
| {
| product {
| productName
| productDescription
| ... @defer {
| productImage
| }
| }
| }
""".trimMargin(),
variables = "{}",
result = """
| {
| "data": {
| "product": {
| "productName": "Awesome Product",
| "productDescription": null
| }
| },
| "hasNext": true
| }
""".trimMargin(),
delayedResults = listOfJsonStrings(
"""
| {
| "hasNext": false,
| "incremental": [
| {
| "path": [
| "product"
| ],
| "data": {
| "productImage": null
| }
| }
| ]
| }
""".trimMargin(),
),
),
ExpectedServiceCall(
service = "users",
query = """
| {
| user {
| name
| ... @defer {
| profilePicture
| }
| ... @defer(label: "team-details") {
| teamName
| teamMembers
| }
| }
| }
""".trimMargin(),
variables = "{}",
result = """
| {
| "data": {
| "user": {
| "name": "Steven"
| }
| },
| "hasNext": true
| }
""".trimMargin(),
delayedResults = listOfJsonStrings(
"""
| {
| "hasNext": false,
| "incremental": [
| {
| "path": [
| "user"
| ],
| "label": "team-details",
| "data": {
| "teamName": "The Unicorns",
| "teamMembers": [
| "Felipe",
| "Franklin",
| "Juliano"
| ]
| }
| }
| ]
| }
""".trimMargin(),
"""
| {
| "hasNext": true,
| "incremental": [
| {
| "path": [
| "user"
| ],
| "data": {
| "profilePicture": "https://examplesite.com/user/profile_picture.jpg"
| }
| }
| ]
| }
""".trimMargin(),
),
),
)

/**
* ```json
* {
* "data": {
* "user": {
* "name": "Steven",
* "profilePicture": "https://examplesite.com/user/profile_picture.jpg",
* "teamName": "The Unicorns",
* "teamMembers": [
* "Felipe",
* "Franklin",
* "Juliano"
* ]
* },
* "product": {
* "productName": "Awesome Product",
* "productDescription": null,
* "productImage": null
* }
* }
* }
* ```
*/
override val result: ExpectedNadelResult = ExpectedNadelResult(
result = """
| {
| "data": {
| "user": {
| "name": "Steven"
| },
| "product": {
| "productName": "Awesome Product",
| "productDescription": null
| }
| },
| "hasNext": true
| }
""".trimMargin(),
delayedResults = listOfJsonStrings(
"""
| {
| "hasNext": false,
| "incremental": [
| {
| "path": [
| "user"
| ],
| "label": "team-details",
| "data": {
| "teamName": "The Unicorns",
| "teamMembers": [
| "Felipe",
| "Franklin",
| "Juliano"
| ]
| }
| }
| ]
| }
""".trimMargin(),
"""
| {
| "hasNext": true,
| "incremental": [
| {
| "path": [
| "product"
| ],
| "data": {
| "productImage": null
| }
| }
| ]
| }
""".trimMargin(),
"""
| {
| "hasNext": true,
| "incremental": [
| {
| "path": [
| "user"
| ],
| "data": {
| "profilePicture": "https://examplesite.com/user/profile_picture.jpg"
| }
| }
| ]
| }
""".trimMargin(),
),
)
}
Loading
Loading