Skip to content
Merged
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
59 changes: 52 additions & 7 deletions Writerside/topics/federation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ dependencies {
}
```

## Defining entity keys
The `apollo-execution-federation` artifact contains the `@GraphQLKey` annotation allowing you to define [entities](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/entities/intro).

You can define [entity](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/entities/intro) key using the `GraphQLKey` annotation:
## Defining entities

You can define an [entity](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/entities/intro) key using the `@GraphQLKey` annotation:

```kotlin
class Product(
Expand All @@ -23,7 +25,7 @@ class Product(
)
```

The `GraphQLKey` annotation is translated at build time into a matching federation `@key` directive:
The `@GraphQLKey` annotation is translated at build time into a matching federation `@key` directive:

```graphql
@key(fields: "id")
Expand All @@ -36,7 +38,7 @@ type Product {
> By adding the annotation on the field definition instead of the type definition, Apollo Kotlin Execution gives you more type safety.
{style="note"}

## Federation subgraph fields
## Auto-generated meta fields

Whenever a type containing a `@GraphQLKey` field is present, Apollo Kotlin Execution adds the [federation subgraph fields](https://www.apollographql.com/docs/graphos/reference/federation/subgraph-specific-fields), `_service` and `_entities`:

Expand All @@ -56,9 +58,9 @@ extend type Query {
}
```

## Defining federated resolvers
## Defining entity resolvers

In order to support the `_entities` field, federation requires a resolver that can resolve an entity from its key field.
In order to support the `_entities` field, federation requires a resolver that can resolve an entity from its key fields.

You can add one by defining a `resolve` function on the companion object:

Expand Down Expand Up @@ -95,4 +97,47 @@ class Product(
}
}
}
```
```

## Tracing (ftv1)

Apollo Kotlin Execution supports [federated tracing](https://www.apollographql.com/docs/federation/v1/metrics) (ftv1).

Ftv1 records timing information for each field and reports that information to the router through the `"ftv1"` extension.

This is done through the `Ftv1Instrumentation` and its matching `Ftv1Context`:

```kotlin
// Install the Ftv1Instrumentation in the executable schema
val schema = ServiceExecutableSchemaBuilder()
.addInstrumentation(Ftv1Instrumentation())
.build()

// Create a new Ftv1Context() for each operation and use it through execution
val ftv1Context = Ftv1Context()
val response = schema.execute(request, ftv1Context)

// The information is a Base64 encoded protobuf message used by the router
val ftv1 = response.extensions.get("ftv1")
```

Sending the `"ftv1"` extension has some overhead and in real life scenarios, the router uses sampling to save network bandwidth.

This is done using the `"apollo-federation-include-trace"` HTTP header:

```kotlin
val ftv1Context = if (httpHeaders.get("apollo-federation-include-trace") == "ftv1") {
// The router required tracing information for this request
Ftv1Context()
} else {
// No tracing information is required, skip processing
ExecutionContext.Empty
}
val response = schema.execute(request, ftv1Context)

```

```kotlin

```

17 changes: 17 additions & 0 deletions apollo-execution-federation/api/apollo-execution-federation.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
public final class com/apollographql/execution/federation/Ftv1Context : com/apollographql/apollo/api/ExecutionContext$Element {
public static final field Key Lcom/apollographql/execution/federation/Ftv1Context$Key;
public fun <init> ()V
public final fun getApolloOperationTracing ()Lcom/apollographql/execution/tracing/ApolloOperationTracing;
public synthetic fun getKey ()Lcom/apollographql/apollo/api/ExecutionContext$Key;
public fun getKey ()Lcom/apollographql/execution/federation/Ftv1Context$Key;
}

public final class com/apollographql/execution/federation/Ftv1Context$Key : com/apollographql/apollo/api/ExecutionContext$Key {
}

public final class com/apollographql/execution/federation/Ftv1Instrumentation : com/apollographql/execution/Instrumentation {
public fun <init> ()V
public fun beforeField (Lcom/apollographql/execution/ResolveInfo;)Lcom/apollographql/execution/InstrumentationCallback;
public fun onResponse (Lcom/apollographql/execution/GraphQLResponse;Lcom/apollographql/apollo/api/ExecutionContext;)Lcom/apollographql/execution/GraphQLResponse;
}

public abstract interface annotation class com/apollographql/execution/federation/GraphQLKey : java/lang/annotation/Annotation {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
// - Show declarations: true

// Library unique name: <com.apollographql.execution:apollo-execution-federation>
final class com.apollographql.execution.federation/Ftv1Context : com.apollographql.apollo.api/ExecutionContext.Element { // com.apollographql.execution.federation/Ftv1Context|null[0]
constructor <init>() // com.apollographql.execution.federation/Ftv1Context.<init>|<init>(){}[0]
final object Key : com.apollographql.apollo.api/ExecutionContext.Key<com.apollographql.execution.federation/Ftv1Context> // com.apollographql.execution.federation/Ftv1Context.Key|null[0]
final val apolloOperationTracing // com.apollographql.execution.federation/Ftv1Context.apolloOperationTracing|{}apolloOperationTracing[0]
final fun <get-apolloOperationTracing>(): com.apollographql.execution.tracing/ApolloOperationTracing // com.apollographql.execution.federation/Ftv1Context.apolloOperationTracing.<get-apolloOperationTracing>|<get-apolloOperationTracing>(){}[0]
final val key // com.apollographql.execution.federation/Ftv1Context.key|{}key[0]
final fun <get-key>(): com.apollographql.execution.federation/Ftv1Context.Key // com.apollographql.execution.federation/Ftv1Context.key.<get-key>|<get-key>(){}[0]
}
final class com.apollographql.execution.federation/Ftv1Instrumentation : com.apollographql.execution/Instrumentation { // com.apollographql.execution.federation/Ftv1Instrumentation|null[0]
constructor <init>() // com.apollographql.execution.federation/Ftv1Instrumentation.<init>|<init>(){}[0]
final fun beforeField(com.apollographql.execution/ResolveInfo): com.apollographql.execution/InstrumentationCallback? // com.apollographql.execution.federation/Ftv1Instrumentation.beforeField|beforeField(com.apollographql.execution.ResolveInfo){}[0]
final fun onResponse(com.apollographql.execution/GraphQLResponse, com.apollographql.apollo.api/ExecutionContext): com.apollographql.execution/GraphQLResponse // com.apollographql.execution.federation/Ftv1Instrumentation.onResponse|onResponse(com.apollographql.execution.GraphQLResponse;com.apollographql.apollo.api.ExecutionContext){}[0]
}
final object com.apollographql.execution.federation/_AnyCoercing : com.apollographql.execution/Coercing<kotlin/Any?> { // com.apollographql.execution.federation/_AnyCoercing|null[0]
final fun deserialize(kotlin/Any?): kotlin/Any? // com.apollographql.execution.federation/_AnyCoercing.deserialize|deserialize(kotlin.Any?){}[0]
final fun parseLiteral(com.apollographql.apollo.ast/GQLValue): kotlin/Any? // com.apollographql.execution.federation/_AnyCoercing.parseLiteral|parseLiteral(com.apollographql.apollo.ast.GQLValue){}[0]
Expand Down
1 change: 1 addition & 0 deletions apollo-execution-federation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ kotlin {
dependencies {
api(libs.apollo.ast)
api(libs.apollo.api)
implementation(project(":apollo-execution-tracing"))
api(project(":apollo-execution-runtime"))
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@file:OptIn(ExperimentalEncodingApi::class)

package com.apollographql.execution.federation

import com.apollographql.apollo.api.ExecutionContext
import com.apollographql.execution.GraphQLResponse
import com.apollographql.execution.Instrumentation
import com.apollographql.execution.InstrumentationCallback
import com.apollographql.execution.ResolveInfo
import com.apollographql.execution.tracing.ApolloOperationTracing
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

class Ftv1Instrumentation: Instrumentation() {
override fun beforeField(resolveInfo: ResolveInfo): InstrumentationCallback? {
val ftv1Context = resolveInfo.executionContext[Ftv1Context]
if (ftv1Context == null) {
return null
}
return ftv1Context.apolloOperationTracing.beforeField(resolveInfo)
}

override fun onResponse(response: GraphQLResponse, executionContext: ExecutionContext): GraphQLResponse {
val ftv1Context = executionContext[Ftv1Context]
if (ftv1Context == null) {
return response
}
return response.newBuilder()
.extensions(mapOf("ftv1" to Base64.encode(ftv1Context.apolloOperationTracing.toProtoTrace().encode())))
.build()
}
}

class Ftv1Context() : ExecutionContext.Element {
val apolloOperationTracing: ApolloOperationTracing = ApolloOperationTracing()

override val key = Key

companion object Key : ExecutionContext.Key<Ftv1Context>
}
4 changes: 2 additions & 2 deletions apollo-execution-ktor/api/apollo-execution-ktor.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
public final class com/apollographql/execution/ktor/MainKt {
public static final fun apolloModule (Lio/ktor/server/application/Application;Lcom/apollographql/execution/ExecutableSchema;Ljava/lang/String;Lcom/apollographql/apollo/api/ExecutionContext;)V
public static synthetic fun apolloModule$default (Lio/ktor/server/application/Application;Lcom/apollographql/execution/ExecutableSchema;Ljava/lang/String;Lcom/apollographql/apollo/api/ExecutionContext;ILjava/lang/Object;)V
public static final fun apolloModule (Lio/ktor/server/application/Application;Lcom/apollographql/execution/ExecutableSchema;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun apolloModule$default (Lio/ktor/server/application/Application;Lcom/apollographql/execution/ExecutableSchema;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun apolloSandboxModule (Lio/ktor/server/application/Application;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun apolloSandboxModule$default (Lio/ktor/server/application/Application;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public static final fun parseAsGraphQLRequest (Lio/ktor/server/request/ApplicationRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,27 @@ import io.ktor.server.util.*
import io.ktor.utils.io.*
import okio.Buffer

suspend fun ApplicationCall.respondGraphQL(executableSchema: ExecutableSchema, executionContext: ExecutionContext = ExecutionContext.Empty, configure: OutgoingContent.(GraphQLResponse?) -> Unit = {}) {
suspend fun ApplicationCall.respondGraphQL(
executableSchema: ExecutableSchema,
executionContext: ExecutionContext = ExecutionContext.Empty,
configure: OutgoingContent.(GraphQLResponse?) -> Unit = {}
) {
val request = request.parseAsGraphQLRequest()
val contentType = ContentType.parse("application/graphql-response+json")
if (request.isFailure) {
respondText(
contentType = contentType,
status = HttpStatusCode.BadRequest,
text = request.exceptionOrNull()?.message ?: "",
configure = { configure(null) }
contentType = contentType,
status = HttpStatusCode.BadRequest,
text = request.exceptionOrNull()?.message ?: "",
configure = { configure(null) }
)
} else {
val response = executableSchema.execute(request.getOrThrow(), executionContext)
respondBytes(
contentType = contentType,
status = HttpStatusCode.OK,
bytes = response.toByteArray(),
configure = { configure(response) }
contentType = contentType,
status = HttpStatusCode.OK,
bytes = response.toByteArray(),
configure = { configure(response) }
)
}
}
Expand Down Expand Up @@ -68,16 +72,16 @@ suspend fun ApplicationRequest.parseAsGraphQLRequest(): Result<GraphQLRequest> {
}

fun Application.apolloModule(
executableSchema: ExecutableSchema,
path: String = "/graphql",
executionContext: ExecutionContext = ExecutionContext.Empty
executableSchema: ExecutableSchema,
path: String = "/graphql",
executionContext: (RoutingRequest) -> ExecutionContext = { ExecutionContext.Empty }
) {
routing {
post(path) {
call.respondGraphQL(executableSchema, executionContext)
call.respondGraphQL(executableSchema, executionContext(call.request))
}
get(path) {
call.respondGraphQL(executableSchema, executionContext)
call.respondGraphQL(executableSchema, executionContext(call.request))
}
}
}
Expand All @@ -92,7 +96,17 @@ fun Application.apolloSandboxModule(
call.respondRedirect(call.url { path("/sandbox/index.html") }, permanent = true)
}
get("$sandboxPath/index.html") {
val initialEndpoint = call.url { path(graphqlPath) }
val initialEndpoint = call.url {
/**
* Trying to guess if the client connected through HTTPS
*/
val proto = call.request.header("x-forwarded-proto")
when (proto) {
"http" -> protocol = URLProtocol.HTTP
"https" -> protocol = URLProtocol.HTTPS
}
path(graphqlPath)
}
call.respondText(sandboxHtml(title, initialEndpoint), ContentType.parse("text/html"))
}
}
Expand Down
15 changes: 14 additions & 1 deletion apollo-execution-runtime/api/apollo-execution-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public final class com/apollographql/execution/ErrorPersistedDocument : com/apol
}

public final class com/apollographql/execution/ExecutableSchema {
public fun <init> (Lcom/apollographql/apollo/ast/Schema;Ljava/util/Map;Lcom/apollographql/execution/RootResolver;Lcom/apollographql/execution/RootResolver;Lcom/apollographql/execution/RootResolver;Lcom/apollographql/execution/Resolver;Lcom/apollographql/execution/TypeResolver;Lcom/apollographql/execution/PersistedDocumentCache;)V
public fun <init> (Lcom/apollographql/apollo/ast/Schema;Ljava/util/Map;Lcom/apollographql/execution/RootResolver;Lcom/apollographql/execution/RootResolver;Lcom/apollographql/execution/RootResolver;Lcom/apollographql/execution/Resolver;Lcom/apollographql/execution/TypeResolver;Ljava/util/List;Lcom/apollographql/execution/PersistedDocumentCache;)V
public final fun execute (Lcom/apollographql/execution/GraphQLRequest;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun execute$default (Lcom/apollographql/execution/ExecutableSchema;Lcom/apollographql/execution/GraphQLRequest;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun subscribe (Lcom/apollographql/execution/GraphQLRequest;Lcom/apollographql/apollo/api/ExecutionContext;)Lkotlinx/coroutines/flow/Flow;
Expand All @@ -49,6 +49,7 @@ public final class com/apollographql/execution/ExecutableSchema {
public final class com/apollographql/execution/ExecutableSchema$Builder {
public fun <init> ()V
public final fun addCoercing (Ljava/lang/String;Lcom/apollographql/execution/Coercing;)Lcom/apollographql/execution/ExecutableSchema$Builder;
public final fun addInstrumentation (Lcom/apollographql/execution/Instrumentation;)Lcom/apollographql/execution/ExecutableSchema$Builder;
public final fun build ()Lcom/apollographql/execution/ExecutableSchema;
public final fun mutationRoot (Lcom/apollographql/execution/RootResolver;)Lcom/apollographql/execution/ExecutableSchema$Builder;
public final fun persistedDocumentCache (Lcom/apollographql/execution/PersistedDocumentCache;)Lcom/apollographql/execution/ExecutableSchema$Builder;
Expand Down Expand Up @@ -106,6 +107,7 @@ public final class com/apollographql/execution/GraphQLResponse {
public final fun getData ()Ljava/lang/Object;
public final fun getErrors ()Ljava/util/List;
public final fun getExtensions ()Ljava/util/Map;
public final fun newBuilder ()Lcom/apollographql/execution/GraphQLResponse$Builder;
public final fun serialize (Lcom/apollographql/apollo/api/json/JsonWriter;)V
public final fun serialize (Lokio/Sink;)V
}
Expand All @@ -130,6 +132,16 @@ public final class com/apollographql/execution/InMemoryPersistedDocumentCache :
public fun put (Ljava/lang/String;Lcom/apollographql/execution/PersistedDocument;)V
}

public abstract class com/apollographql/execution/Instrumentation {
public fun <init> ()V
public fun beforeField (Lcom/apollographql/execution/ResolveInfo;)Lcom/apollographql/execution/InstrumentationCallback;
public fun onResponse (Lcom/apollographql/execution/GraphQLResponse;Lcom/apollographql/apollo/api/ExecutionContext;)Lcom/apollographql/execution/GraphQLResponse;
}

public abstract interface class com/apollographql/execution/InstrumentationCallback {
public abstract fun afterComplete (Ljava/lang/Object;)V
}

public final class com/apollographql/execution/IntCoercing : com/apollographql/execution/Coercing {
public static final field INSTANCE Lcom/apollographql/execution/IntCoercing;
public fun deserialize (Ljava/lang/Object;)Ljava/lang/Integer;
Expand Down Expand Up @@ -158,6 +170,7 @@ public final class com/apollographql/execution/ResolveInfo {
public final fun getFields ()Ljava/util/List;
public final fun getParentObject ()Ljava/lang/Object;
public final fun getParentType ()Ljava/lang/String;
public final fun getPath ()Ljava/util/List;
public final fun getRequiredArgument (Ljava/lang/String;)Ljava/lang/Object;
public final fun getSchema ()Lcom/apollographql/apollo/ast/Schema;
}
Expand Down
Loading