|
| 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. |
0 commit comments