Skip to content

Implement @defer "June 2023" response format #6331

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/defer-with-router-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,31 @@ jobs:
DEFER_WITH_ROUTER_TESTS: true
run: |
./gradlew --no-daemon --console plain -p tests :defer:allTests
defer-with-apollo-server-tests:
runs-on: ubuntu-latest
if: github.repository == 'apollographql/apollo-kotlin'
steps:
- name: Checkout project
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7

- name: Install and run graph
working-directory: tests/defer/apollo-server/
run: |
npm install --legacy-peer-deps
npx patch-package
APOLLO_PORT=4000 npm start &

- name: Setup Java
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 #v4.2.1
with:
distribution: 'temurin'
java-version: 17

- name: Setup Gradle
uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2

- name: Run Apollo Kotlin @defer tests
env:
DEFER_WITH_APOLLO_SERVER_TESTS: true
run: |
./gradlew --no-daemon --console plain -p tests :defer:allTests
1 change: 1 addition & 0 deletions libraries/apollo-api/api/apollo-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ public final class com/apollographql/apollo/api/CustomScalarAdapters$Builder {
public final fun deferredFragmentIdentifiers (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder;
public final fun errors (Ljava/util/List;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder;
public final fun falseVariables (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder;
public final fun pendingResultIds (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder;
}

public final class com/apollographql/apollo/api/CustomScalarAdapters$Key : com/apollographql/apollo/api/ExecutionContext$Key {
Expand Down
1 change: 1 addition & 0 deletions libraries/apollo-api/api/apollo-api.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ final class com.apollographql.apollo.api/CustomScalarAdapters : com.apollographq
final fun deferredFragmentIdentifiers(kotlin.collections/Set<com.apollographql.apollo.api/DeferredFragmentIdentifier>?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.deferredFragmentIdentifiers|deferredFragmentIdentifiers(kotlin.collections.Set<com.apollographql.apollo.api.DeferredFragmentIdentifier>?){}[0]
final fun errors(kotlin.collections/List<com.apollographql.apollo.api/Error>?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.errors|errors(kotlin.collections.List<com.apollographql.apollo.api.Error>?){}[0]
final fun falseVariables(kotlin.collections/Set<kotlin/String>?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.falseVariables|falseVariables(kotlin.collections.Set<kotlin.String>?){}[0]
final fun pendingResultIds(kotlin.collections/Set<com.apollographql.apollo.api/DeferredFragmentIdentifier>?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.pendingResultIds|pendingResultIds(kotlin.collections.Set<com.apollographql.apollo.api.DeferredFragmentIdentifier>?){}[0]
}

final object Key : com.apollographql.apollo.api/ExecutionContext.Key<com.apollographql.apollo.api/CustomScalarAdapters> { // com.apollographql.apollo.api/CustomScalarAdapters.Key|null[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ fun <T : Any> and(vararg other: BooleanExpression<T>): BooleanExpression<T> = Bo
fun <T : Any> not(other: BooleanExpression<T>): BooleanExpression<T> = BooleanExpression.Not(other)
fun variable(name: String): BooleanExpression<BVariable> = BooleanExpression.Element(BVariable(name))
fun label(label: String? = null): BooleanExpression<BLabel> = BooleanExpression.Element(BLabel(label))
fun possibleTypes(vararg typenames: String): BooleanExpression<BPossibleTypes> = BooleanExpression.Element(BPossibleTypes(typenames.toSet()))
fun possibleTypes(vararg typenames: String): BooleanExpression<BPossibleTypes> =
BooleanExpression.Element(BPossibleTypes(typenames.toSet()))

internal fun <T : Any> BooleanExpression<T>.evaluate(block: (T) -> Boolean): Boolean {
return when (this) {
Expand All @@ -66,26 +67,30 @@ internal fun <T : Any> BooleanExpression<T>.evaluate(block: (T) -> Boolean): Boo
fun BooleanExpression<BTerm>.evaluate(
variables: Set<String>?,
typename: String?,
deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>?,
pendingResultIds: Set<IncrementalResultIdentifier>?,
path: List<Any>?,
): Boolean {
// Remove "data" from the path
val croppedPath = path?.drop(1)
return evaluate {
when (it) {
is BVariable -> !(variables?.contains(it.name) ?: false)
is BLabel -> hasDeferredFragment(deferredFragmentIdentifiers, croppedPath!!, it.label)
is BLabel -> !isDeferredFragmentPending(pendingResultIds, croppedPath!!, it.label)
is BPossibleTypes -> it.possibleTypes.contains(typename)
}
}
}

private fun hasDeferredFragment(deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>?, path: List<Any>, label: String?): Boolean {
if (deferredFragmentIdentifiers == null) {
private fun isDeferredFragmentPending(
pendingResultIds: Set<IncrementalResultIdentifier>?,
path: List<Any>,
label: String?,
): Boolean {
if (pendingResultIds == null) {
// By default, parse all deferred fragments - this is the case when parsing from the normalized cache.
return true
return false
}
return deferredFragmentIdentifiers.contains(DeferredFragmentIdentifier(path, label))
return pendingResultIds.contains(IncrementalResultIdentifier(path, label))
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ class CustomScalarAdapters private constructor(
@JvmField
val falseVariables: Set<String>?,
/**
* Defer identifiers used to determine whether the parser must parse @defer fragments
* Pending incremental result identifiers used to determine whether the parser must parse deferred fragments
*/
@JvmField
val deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>?,
val deferredFragmentIdentifiers: Set<IncrementalResultIdentifier>?,
/**
* Errors to use with @catch
*/
Expand Down Expand Up @@ -125,21 +125,26 @@ class CustomScalarAdapters private constructor(
fun newBuilder(): Builder {
return Builder().addAll(this)
.falseVariables(falseVariables)
.deferredFragmentIdentifiers(deferredFragmentIdentifiers)
.pendingResultIds(deferredFragmentIdentifiers)
}

class Builder {
private val adaptersMap: MutableMap<String, Adapter<*>> = mutableMapOf()
private var falseVariables: Set<String>? = null
private var deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>? = null
private var pendingResultIds: Set<IncrementalResultIdentifier>? = null
private var errors: List<Error>? = null

fun falseVariables(falseVariables: Set<String>?) = apply {
this.falseVariables = falseVariables
}

fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>?) = apply {
this.deferredFragmentIdentifiers = deferredFragmentIdentifiers
@Deprecated("Use pendingResultIds instead", ReplaceWith("pendingResultIds(pendingResultIds = deferredFragmentIdentifiers)"))
fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set<IncrementalResultIdentifier>?) = apply {
this.pendingResultIds = deferredFragmentIdentifiers
}

fun pendingResultIds(pendingResultIds: Set<IncrementalResultIdentifier>?) = apply {
this.pendingResultIds = pendingResultIds
}

fun errors(errors: List<Error>?) = apply {
Expand Down Expand Up @@ -173,7 +178,7 @@ class CustomScalarAdapters private constructor(
return CustomScalarAdapters(
adaptersMap,
falseVariables,
deferredFragmentIdentifiers,
pendingResultIds,
errors,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ fun <D : Executable.Data> Executable<D>.parseData(
jsonReader: JsonReader,
customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty,
falseVariables: Set<String>? = null,
deferredFragmentIds: Set<DeferredFragmentIdentifier>? = null,
errors: List<Error>? = null
deferredFragmentIds: Set<IncrementalResultIdentifier>? = null,
errors: List<Error>? = null,
): D? {
val customScalarAdapters1 = customScalarAdapters.newBuilder()
.falseVariables(falseVariables)
.deferredFragmentIdentifiers(deferredFragmentIds)
.pendingResultIds(pendingResultIds = deferredFragmentIds)
.errors(errors)
.build()
return adapter().nullable().fromJson(jsonReader, customScalarAdapters1)
Expand All @@ -89,4 +89,4 @@ fun <D : Executable.Data> Executable<D>.composeData(
value: D
) {
adapter().toJson(jsonWriter, customScalarAdapters, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ data class DeferredFragmentIdentifier(
val path: List<Any>,
val label: String?,
)

/**
* Identifies an incremental result.
* [DeferredFragmentIdentifier] is kept to not break the API/ABI, but this alias is more descriptive of its purpose.
*/
typealias IncrementalResultIdentifier = DeferredFragmentIdentifier
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ fun <D : Operation.Data> Operation<D>.composeJsonRequest(
fun <D : Operation.Data> Operation<D>.parseJsonResponse(
jsonReader: JsonReader,
customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty,
deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>? = null,
deferredFragmentIdentifiers: Set<IncrementalResultIdentifier>? = null,
): ApolloResponse<D> {
return jsonReader.use {
ResponseParser.parse(
Expand Down Expand Up @@ -103,7 +103,7 @@ fun <D : Operation.Data> Operation<D>.parseResponse(
jsonReader: JsonReader,
requestUuid: Uuid? = null,
customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty,
deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>? = null,
deferredFragmentIdentifiers: Set<IncrementalResultIdentifier>? = null,
): ApolloResponse<D> {
return try {
ResponseParser.parse(
Expand Down Expand Up @@ -177,7 +177,7 @@ fun <D : Operation.Data> JsonReader.toApolloResponse(
operation: Operation<D>,
requestUuid: Uuid? = null,
customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty,
deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>? = null,
deferredFragmentIdentifiers: Set<IncrementalResultIdentifier>? = null,
): ApolloResponse<D> {
return use {
try {
Expand Down Expand Up @@ -213,7 +213,7 @@ fun <D : Operation.Data> JsonReader.parseResponse(
operation: Operation<D>,
requestUuid: Uuid? = null,
customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty,
deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>? = null,
deferredFragmentIdentifiers: Set<IncrementalResultIdentifier>? = null,
): ApolloResponse<D> {
return try {
ResponseParser.parse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.apollographql.apollo.api.internal
import com.apollographql.apollo.annotations.ApolloInternal
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.CustomScalarAdapters
import com.apollographql.apollo.api.DeferredFragmentIdentifier
import com.apollographql.apollo.api.Error
import com.apollographql.apollo.api.IncrementalResultIdentifier
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.falseVariables
import com.apollographql.apollo.api.json.JsonReader
Expand All @@ -24,7 +24,7 @@ internal object ResponseParser {
operation: Operation<D>,
requestUuid: Uuid?,
customScalarAdapters: CustomScalarAdapters,
deferredFragmentIds: Set<DeferredFragmentIdentifier>?,
pendingResultIds: Set<IncrementalResultIdentifier>?,
): ApolloResponse<D> {
jsonReader.beginObject()

Expand All @@ -36,8 +36,9 @@ internal object ResponseParser {
when (val name = jsonReader.nextName()) {
"data" -> {
val falseVariables = operation.falseVariables(customScalarAdapters)
data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, deferredFragmentIds, errors)
data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, pendingResultIds, errors)
}

"errors" -> errors = jsonReader.readErrors()
"extensions" -> extensions = jsonReader.readAny() as? Map<String, Any?>
else -> {
Expand Down Expand Up @@ -100,7 +101,8 @@ private fun JsonReader.readError(): Error {


@Suppress("DEPRECATION")
return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields).build()
return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields)
.build()
}

private fun JsonReader.readPath(): List<Any>? {
Expand Down Expand Up @@ -164,4 +166,4 @@ fun JsonReader.readErrors(): List<Error> {
}
endArray()
return list
}
}
6 changes: 3 additions & 3 deletions libraries/apollo-runtime/api/android/apollo-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK
public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor;
}

public final class com/apollographql/apollo/internal/DeferredJsonMerger {
public final class com/apollographql/apollo/internal/IncrementalResultsMerger {
public fun <init> ()V
public final fun getHasNext ()Z
public final fun getMerged ()Ljava/util/Map;
public final fun getMergedFragmentIds ()Ljava/util/Set;
public final fun isEmptyPayload ()Z
public final fun getPendingResultIds ()Ljava/util/Set;
public final fun isEmptyResponse ()Z
public final fun merge (Ljava/util/Map;)Ljava/util/Map;
public final fun merge (Lokio/BufferedSource;)Ljava/util/Map;
public final fun reset ()V
Expand Down
32 changes: 16 additions & 16 deletions libraries/apollo-runtime/api/apollo-runtime.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -200,22 +200,22 @@ final class com.apollographql.apollo.interceptor/AutoPersistedQueryInterceptor :
}
}

final class com.apollographql.apollo.internal/DeferredJsonMerger { // com.apollographql.apollo.internal/DeferredJsonMerger|null[0]
constructor <init>() // com.apollographql.apollo.internal/DeferredJsonMerger.<init>|<init>(){}[0]

final val merged // com.apollographql.apollo.internal/DeferredJsonMerger.merged|{}merged[0]
final fun <get-merged>(): kotlin.collections/Map<kotlin/String, kotlin/Any?> // com.apollographql.apollo.internal/DeferredJsonMerger.merged.<get-merged>|<get-merged>(){}[0]
final val mergedFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds|{}mergedFragmentIds[0]
final fun <get-mergedFragmentIds>(): kotlin.collections/Set<com.apollographql.apollo.api/DeferredFragmentIdentifier> // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds.<get-mergedFragmentIds>|<get-mergedFragmentIds>(){}[0]

final var hasNext // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext|{}hasNext[0]
final fun <get-hasNext>(): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext.<get-hasNext>|<get-hasNext>(){}[0]
final var isEmptyPayload // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload|{}isEmptyPayload[0]
final fun <get-isEmptyPayload>(): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload.<get-isEmptyPayload>|<get-isEmptyPayload>(){}[0]

final fun merge(kotlin.collections/Map<kotlin/String, kotlin/Any?>): kotlin.collections/Map<kotlin/String, kotlin/Any?> // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(kotlin.collections.Map<kotlin.String,kotlin.Any?>){}[0]
final fun merge(okio/BufferedSource): kotlin.collections/Map<kotlin/String, kotlin/Any?> // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(okio.BufferedSource){}[0]
final fun reset() // com.apollographql.apollo.internal/DeferredJsonMerger.reset|reset(){}[0]
final class com.apollographql.apollo.internal/IncrementalResultsMerger { // com.apollographql.apollo.internal/IncrementalResultsMerger|null[0]
constructor <init>() // com.apollographql.apollo.internal/IncrementalResultsMerger.<init>|<init>(){}[0]

final val merged // com.apollographql.apollo.internal/IncrementalResultsMerger.merged|{}merged[0]
final fun <get-merged>(): kotlin.collections/Map<kotlin/String, kotlin/Any?> // com.apollographql.apollo.internal/IncrementalResultsMerger.merged.<get-merged>|<get-merged>(){}[0]
final val pendingResultIds // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds|{}pendingResultIds[0]
final fun <get-pendingResultIds>(): kotlin.collections/Set<com.apollographql.apollo.api/DeferredFragmentIdentifier> // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds.<get-pendingResultIds>|<get-pendingResultIds>(){}[0]

final var hasNext // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext|{}hasNext[0]
final fun <get-hasNext>(): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext.<get-hasNext>|<get-hasNext>(){}[0]
final var isEmptyResponse // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0]
final fun <get-isEmptyResponse>(): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse.<get-isEmptyResponse>|<get-isEmptyResponse>(){}[0]

final fun merge(kotlin.collections/Map<kotlin/String, kotlin/Any?>): kotlin.collections/Map<kotlin/String, kotlin/Any?> // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(kotlin.collections.Map<kotlin.String,kotlin.Any?>){}[0]
final fun merge(okio/BufferedSource): kotlin.collections/Map<kotlin/String, kotlin/Any?> // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0]
final fun reset() // com.apollographql.apollo.internal/IncrementalResultsMerger.reset|reset(){}[0]
}

final class com.apollographql.apollo.internal/MultipartReader : okio/Closeable { // com.apollographql.apollo.internal/MultipartReader|null[0]
Expand Down
6 changes: 3 additions & 3 deletions libraries/apollo-runtime/api/jvm/apollo-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK
public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor;
}

public final class com/apollographql/apollo/internal/DeferredJsonMerger {
public final class com/apollographql/apollo/internal/IncrementalResultsMerger {
public fun <init> ()V
public final fun getHasNext ()Z
public final fun getMerged ()Ljava/util/Map;
public final fun getMergedFragmentIds ()Ljava/util/Set;
public final fun isEmptyPayload ()Z
public final fun getPendingResultIds ()Ljava/util/Set;
public final fun isEmptyResponse ()Z
public final fun merge (Ljava/util/Map;)Ljava/util/Map;
public final fun merge (Lokio/BufferedSource;)Ljava/util/Map;
public final fun reset ()V
Expand Down
Loading
Loading