Skip to content

Commit 5838974

Browse files
authored
Initial federation support (#36)
* Initial federation support * add apiDump * fix introspection * fix test in CI * use kotlinpoet functions * remove unneeded tests * Revert "fix test in CI" This reverts commit 382630e. * update workflow file * fix enums with KSP2
1 parent 75eb863 commit 5838974

File tree

41 files changed

+1191
-311
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1191
-311
lines changed

.github/workflows/build-pull-request.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ jobs:
1111
- uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2
1212
- run: |
1313
./gradlew build
14-
./gradlew -p execution-tests build
14+
./gradlew -p tests build
1515
./gradlew -p sample-ktor build
1616
./gradlew -p sample-http4k build

Writerside/doc.tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@
2121
<toc-element topic="http4k.md"/>
2222
<toc-element topic="spring.md"/>
2323
</toc-element>
24+
<toc-element toc-title="Apollo Federation">
25+
<toc-element topic="federation.md"/>
26+
</toc-element>
2427
</instance-profile>

Writerside/topics/federation.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Apollo Federation
2+
3+
Apollo Kotlin Execution supports [Apollo Federation](https://www.apollographql.com/federation).
4+
5+
To use federation, add the `apollo-execution-federation` artifact to your project:
6+
7+
```kotlin
8+
dependencies {
9+
// Add the federation dependency
10+
implementation("com.apollographql.execution:apollo-execution-federation:%latest_version%")
11+
}
12+
```
13+
14+
## Defining entity keys
15+
16+
You can define [entity](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/entities/intro) key using the `GraphQLKey` annotation:
17+
18+
```kotlin
19+
class Product(
20+
@GraphQLKey
21+
val id: String,
22+
val name: String
23+
)
24+
```
25+
26+
The `GraphQLKey` annotation is translated at build time into a matching federation `@key` directive:
27+
28+
```graphql
29+
@key(fields: "id")
30+
type Product {
31+
id: String!,
32+
name: String!
33+
}
34+
```
35+
36+
> By adding the annotation on the field definition instead of the type definition, Apollo Kotlin Execution gives you more type safety.
37+
{style="note"}
38+
39+
## Federation subgraph fields
40+
41+
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`:
42+
43+
```graphql
44+
# an union containing all the federated types in the schema, constructed at build time
45+
union _Entity = Product | ...
46+
# coerced as a JSON object containing '__typename' and all the key fields.
47+
scalar _Any
48+
49+
type _Service {
50+
sdl: String!
51+
}
52+
53+
extend type Query {
54+
_entities(representations: [_Any!]!): [_Entity]!
55+
_service: _Service!
56+
}
57+
```
58+
59+
## Defining federated resolvers
60+
61+
In order to support the `_entities` field, federation requires a resolver that can resolve an entity from its key field.
62+
63+
You can add one by defining a `resolve` function on the companion object:
64+
65+
```kotlin
66+
class Product(
67+
@GraphQLKey
68+
val id: String,
69+
val name: String
70+
) {
71+
companion object {
72+
fun resolve(id: String): Product {
73+
return products.first { it.id == id }
74+
}
75+
}
76+
}
77+
78+
val products = listOf(
79+
Product("1", "foo"),
80+
Product("2", "bar")
81+
)
82+
```
83+
84+
Just like regular resolvers, the entity resolvers can be suspend and/or have an `ExecutionContext` parameter:
85+
86+
```kotlin
87+
class Product(
88+
@GraphQLKey
89+
val id: String,
90+
val name: String
91+
) {
92+
companion object {
93+
suspend fun resolve(executionContext: ExecutionContext, id: String): Product {
94+
return executionContext.loader.getProdut(id)
95+
}
96+
}
97+
}
98+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Module apollo-execution-federation
2+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
public abstract interface annotation class com/apollographql/execution/federation/GraphQLKey : java/lang/annotation/Annotation {
2+
}
3+
4+
public final class com/apollographql/execution/federation/_AnyCoercing : com/apollographql/execution/Coercing {
5+
public static final field INSTANCE Lcom/apollographql/execution/federation/_AnyCoercing;
6+
public fun deserialize (Ljava/lang/Object;)Ljava/lang/Object;
7+
public fun parseLiteral (Lcom/apollographql/apollo/ast/GQLValue;)Ljava/lang/Object;
8+
public fun serialize (Ljava/lang/Object;)Ljava/lang/Object;
9+
}
10+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Klib ABI Dump
2+
// Targets: [macosArm64]
3+
// Rendering settings:
4+
// - Signature version: 2
5+
// - Show manifest properties: true
6+
// - Show declarations: true
7+
8+
// Library unique name: <com.apollographql.execution:apollo-execution-federation>
9+
final object com.apollographql.execution.federation/_AnyCoercing : com.apollographql.execution/Coercing<kotlin/Any?> { // com.apollographql.execution.federation/_AnyCoercing|null[0]
10+
final fun deserialize(kotlin/Any?): kotlin/Any? // com.apollographql.execution.federation/_AnyCoercing.deserialize|deserialize(kotlin.Any?){}[0]
11+
final fun parseLiteral(com.apollographql.apollo.ast/GQLValue): kotlin/Any? // com.apollographql.execution.federation/_AnyCoercing.parseLiteral|parseLiteral(com.apollographql.apollo.ast.GQLValue){}[0]
12+
final fun serialize(kotlin/Any?): kotlin/Any? // com.apollographql.execution.federation/_AnyCoercing.serialize|serialize(kotlin.Any?){}[0]
13+
}
14+
open annotation class com.apollographql.execution.federation/GraphQLKey : kotlin/Annotation { // com.apollographql.execution.federation/GraphQLKey|null[0]
15+
constructor <init>() // com.apollographql.execution.federation/GraphQLKey.<init>|<init>(){}[0]
16+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import com.gradleup.librarian.gradle.librarianModule
2+
3+
plugins {
4+
id("org.jetbrains.kotlin.multiplatform")
5+
}
6+
7+
librarianModule(true)
8+
9+
kotlin {
10+
jvm()
11+
macosArm64()
12+
13+
sourceSets {
14+
getByName("commonMain") {
15+
dependencies {
16+
api(libs.apollo.ast)
17+
api(libs.apollo.api)
18+
api(project(":apollo-execution-runtime"))
19+
}
20+
}
21+
22+
getByName("commonTest") {
23+
dependencies {
24+
implementation(libs.kotlin.test)
25+
}
26+
}
27+
}
28+
}
29+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.apollographql.execution.federation
2+
3+
4+
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
5+
annotation class GraphQLKey
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.apollographql.execution.federation
2+
3+
import com.apollographql.apollo.ast.GQLBooleanValue
4+
import com.apollographql.apollo.ast.GQLEnumValue
5+
import com.apollographql.apollo.ast.GQLFloatValue
6+
import com.apollographql.apollo.ast.GQLIntValue
7+
import com.apollographql.apollo.ast.GQLListValue
8+
import com.apollographql.apollo.ast.GQLNullValue
9+
import com.apollographql.apollo.ast.GQLObjectValue
10+
import com.apollographql.apollo.ast.GQLStringValue
11+
import com.apollographql.apollo.ast.GQLValue
12+
import com.apollographql.apollo.ast.GQLVariableValue
13+
import com.apollographql.execution.Coercing
14+
import com.apollographql.execution.JsonValue
15+
16+
object _AnyCoercing: Coercing<Any?> {
17+
override fun serialize(internalValue: Any?): JsonValue {
18+
return internalValue
19+
}
20+
21+
override fun deserialize(value: JsonValue): Any? {
22+
return value
23+
}
24+
25+
override fun parseLiteral(value: GQLValue): Any? {
26+
return when (value) {
27+
is GQLBooleanValue -> value.value
28+
is GQLEnumValue -> value.value
29+
is GQLFloatValue -> value.value
30+
is GQLIntValue -> value.value
31+
is GQLListValue -> value.values.map { parseLiteral(it) }
32+
is GQLNullValue -> null
33+
is GQLObjectValue -> value.fields.map { it.name to parseLiteral(it.value) }.toMap()
34+
is GQLStringValue -> value.value
35+
is GQLVariableValue -> error("Cannot coerce variable")
36+
}
37+
}
38+
}

apollo-execution-processor/src/main/kotlin/com/apollographql/execution/processor/ApolloProcessor.kt

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.apollographql.execution.processor
22

3+
import com.apollographql.apollo.ast.GQLObjectTypeDefinition
34
import com.apollographql.execution.processor.codegen.*
45
import com.apollographql.execution.processor.sir.SirClassName
6+
import com.apollographql.execution.processor.sir.SirObjectDefinition
7+
import com.apollographql.execution.processor.sir.maybeFederate
58
import com.google.devtools.ksp.containingFile
69
import com.google.devtools.ksp.getConstructors
710
import com.google.devtools.ksp.isAbstract
@@ -63,10 +66,11 @@ class ApolloProcessor(
6366
val definitions = result.definitions
6467

6568
val context = KotlinExecutableSchemaContext(packageName)
69+
val maybeFederatedDefinitions = definitions.maybeFederate()
6670
val schemaDocumentBuilder = SchemaDocumentBuilder(
6771
context = context,
6872
serviceName = serviceName,
69-
sirDefinitions = definitions
73+
sirDefinitions = maybeFederatedDefinitions
7074
)
7175

7276
val builders = mutableListOf<CgFileBuilder>()
@@ -81,12 +85,27 @@ class ApolloProcessor(
8185
logger = logger
8286
)
8387
)
88+
89+
val entities = definitions.filterIsInstance<SirObjectDefinition>().filter { it.isEntity }
90+
val entityResolverBuilder = if (entities.isNotEmpty()) {
91+
EntityResolverBuilder(
92+
context = context,
93+
serviceName = serviceName,
94+
entities = entities,
95+
).also {
96+
builders.add(it)
97+
}
98+
} else {
99+
null
100+
}
101+
84102
builders.add(
85103
ExecutableSchemaBuilderBuilder(
86104
context = context,
87105
serviceName = serviceName,
88106
schemaDocument = schemaDocumentBuilder.schemaDocument,
89-
sirDefinitions = definitions
107+
sirDefinitions = definitions,
108+
entityResolver = entityResolverBuilder?.entityResolver
90109
)
91110
)
92111

@@ -105,14 +124,14 @@ class ApolloProcessor(
105124
106125
This class was automatically generated by Apollo GraphQL version '$VERSION'.
107126
108-
""".trimIndent()
127+
""".trimIndent()
109128
).build()
110129
}
111130
.forEach { sourceFile ->
112131
codeGenerator.createNewFile(
113132
dependencies,
114133
packageName = sourceFile.packageName,
115-
// SourceFile contains .kt
134+
// SourceFile contains.kt
116135
fileName = sourceFile.name.substringBeforeLast('.'),
117136
).bufferedWriter().use {
118137
sourceFile.writeTo(it)
@@ -124,7 +143,7 @@ class ApolloProcessor(
124143
"${serviceName}Schema.graphqls",
125144
"",
126145
).bufferedWriter().use {
127-
it.write(schemaString(definitions))
146+
it.write(schemaString(maybeFederatedDefinitions))
128147
}
129148
return emptyList()
130149
}

0 commit comments

Comments
 (0)