Skip to content

Commit 5436fbd

Browse files
authored
feat: First support for schema stitching (#195)
This adds first support for schema stitching without aiming for full feature compatibility. And while I have a pretty extensive internal use case, schema stitching is still to be considered experimental and subject to change as needed. Schema stitching is a way to combine multiple GraphQL schemas under a single endpoint, so that clients can request data from different APIs in a single query. It also allows to add links between these schemas to e.g., automatically resolve references to actual types. Main goals/limitations for the first implementation: - Avoid interfering with existing code and to stick to existing architecture where possible - Avoid introducing new dependencies for existing users of the library; in particular, kgraphql core should not get any ktor specific dependencies - Have the stitching API as lean and close to the existing API as possible - Focus on the non-experimental `ParallelRequestExecutor` first Over the course of implementing schema stitching, several bugs were resolved on the way but schema stitching is currently still impacted by some major issues like incomplete error handling (#114) that need to be addressed separately. Resolves #9
2 parents ab27f68 + efdffdd commit 5436fbd

File tree

57 files changed

+6573
-130
lines changed

Some content is hidden

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

57 files changed

+6573
-130
lines changed

buildSrc/src/main/kotlin/library-conventions.gradle.kts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import com.vanniktech.maven.publish.JavadocJar
2-
import com.vanniktech.maven.publish.KotlinJvm
31
import com.vanniktech.maven.publish.SonatypeHost
42
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
53

docs/content/Reference/Type System/objects-and-interfaces.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ Returns:
252252
This feature can be used in production but does currently have some issues:
253253

254254
1. The `useDefaultPrettyPrint` doesn't work
255-
1. Order of fields are not guaranteed, to match the order that was requested
255+
1. Order of fields are not guaranteed to match the order that was requested
256256
1. Custom generic type resolvers are not supported
257257
1. Other than that it should work as expected
258+
1. Schema stitching is not supported

docs/content/Reference/stitching.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Schema Stitching
2+
3+
*This feature is still in experimental state.*
4+
5+
## Overview
6+
7+
Schema stitching is a method to take multiple GraphQL schemas and combine them into a single, unified schema. This can
8+
be useful when implementing an integration layer for a frontend that orchestrates multiple backend APIs to provide your
9+
UI with all the required data in a single request, without having to think about CORS.
10+
11+
By linking properties to remote queries, one can also enhance individual schemas by e.g. automatically resolving
12+
identifiers.
13+
14+
In KGraphQL, schema stitching is configured via the `stitchedSchema` DSL. Each stitched schema has 1-n *remote* schemas,
15+
and up to one *local* schema.
16+
17+
*Example*:
18+
19+
```kotlin
20+
stitchedSchema {
21+
configure {
22+
remoteExecutor = TestRemoteRequestExecutor(client, objectMapper)
23+
}
24+
localSchema {
25+
query("local") {
26+
resolver { -> "local" }
27+
}
28+
}
29+
remoteSchema("remote1") {
30+
...
31+
}
32+
remoteSchema("remote2") {
33+
...
34+
}
35+
}
36+
```
37+
38+
### Remote Schema Fetching
39+
40+
Remote schemas are usually fetched via introspection query.
41+
42+
*Example*:
43+
44+
```kotlin
45+
remoteSchema(url) {
46+
runBlocking {
47+
val responseText = httpClient.post(url) {
48+
setBody(
49+
TextContent(
50+
text = graphQLJson.toJson(mapOf("query" to Introspection.query())),
51+
contentType = ContentType.Application.Json
52+
)
53+
)
54+
}.bodyAsText()
55+
IntrospectedSchema.fromIntrospectionResponse(responseText)
56+
}
57+
}
58+
```
59+
60+
### Duplicate Types
61+
62+
Currently, if multiple schemas define types with the same name, the local type wins. For identical types from remote
63+
schemas there is no guaranteed order of precedence. Future versions may provide better tools to deal with such
64+
situations.
65+
66+
### Remote Execution
67+
68+
To execute remote queries, consumers need to provide a `RemoteRequestExecutor` that receives an `Execution.Remote` node
69+
and the current `Context`, and has to return the result as `JsonNode?`:
70+
71+
```kotlin
72+
interface RemoteRequestExecutor {
73+
// ParallelRequestExecutor expects a JsonNode as result of any execution
74+
suspend fun execute(node: Execution.Remote, ctx: Context): JsonNode?
75+
}
76+
```
77+
78+
To simplify implementation, consumers can extend the `AbstractRemoteRequestExecutor` and only provide the implementation
79+
for actually executing the HTTP request itself.
80+
81+
### Fragments
82+
83+
Fragments based on remote types work but cannot use Kotlin's type system to determine the correct condition type.
84+
Therefore, queries including fragments must also request the `__typename`. Future implementation might automatically
85+
include this.
86+
87+
*Example*:
88+
89+
```graphql
90+
query {
91+
getRemote {
92+
__typename
93+
foo
94+
child {
95+
__typename
96+
...on RemoteA { specialA }
97+
...on RemoteB { specialB }
98+
}
99+
}
100+
}
101+
```
102+
103+
### Local "Remote" Execution
104+
105+
Due to current implementation details, properties stitched to a *local* query will also be handled by the
106+
`RemoteRequestExecutor`, and therefore the schema has to provide a `localUrl`. Future implementation will likely support
107+
actual local execution.
108+
109+
*Example:*
110+
111+
```kotlin
112+
stitchedSchema {
113+
configure {
114+
localUrl = "/graphql"
115+
}
116+
}
117+
```
118+
119+
### Linking Properties
120+
121+
All (local and remote) types of a schema can be extended via stitched properties that are translated into remote query
122+
calls during execution. The following example adds two fields to the `Type1` type:
123+
124+
- `stitched1`, which executes the remote query `getStitched1` and is marked as required
125+
- `stitched2`, which executes the remote query `getStitched2` and provides the value of the property `foo` from the
126+
parent type as argument named `fooValue`
127+
128+
*Example:*
129+
130+
```kotlin
131+
type("Type1") {
132+
stitchedProperty("stitched1") {
133+
nullable = false
134+
remoteQuery("getStitched1")
135+
}
136+
stitchedProperty("stitched2") {
137+
remoteQuery("getStitched2").withArgs {
138+
arg { name = "fooValue"; parentFieldName = "foo" }
139+
}
140+
}
141+
}
142+
```
143+
144+
Stitched properties are nullable by default, and if a parent property is `null`, the remote execution is skipped and
145+
results in a value of `null` for the stitched property itself.
146+
147+
See `StitchedSchemaExecutionTest.kt` for an extensive list of different examples.

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ jackson-core-databind = { module = "com.fasterxml.jackson.core:jackson-databind"
2424
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
2525
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "3.2.0" }
2626
deferredJsonBuilder = { module = "com.apurebase:DeferredJsonBuilder", version = "1.0.0" }
27+
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
28+
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
2729
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
2830
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
2931
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }

0 commit comments

Comments
 (0)