-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #287 from oskargotte/federation-compatibility
Add Apollo federation spec compliance tool in CI
- Loading branch information
Showing
25 changed files
with
897 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
name: Federation Specification Compatibility Test | ||
|
||
on: | ||
pull_request: | ||
branches: | ||
- main | ||
jobs: | ||
compatibility: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout current branch (full) | ||
uses: actions/checkout@v4 | ||
with: | ||
fetch-depth: 0 | ||
|
||
- name: Setup Java (temurin@8) | ||
uses: actions/setup-java@v3 | ||
with: | ||
distribution: temurin | ||
java-version: 8 | ||
cache: sbt | ||
|
||
- name: Compatibility Test | ||
uses: apollographql/federation-subgraph-compatibility@v2 | ||
with: | ||
# [Required] Docker Compose file to start up the subgraph | ||
compose: 'example/product/docker-compose.yaml' | ||
# [Required] Path to the GraphQL schema file | ||
schema: 'example/product/products.graphql' | ||
# GraphQL endpoint path, defaults to '' (empty) | ||
path: '' | ||
# GraphQL endpoint HTTP port, defaults to 4001 | ||
port: 4001 | ||
# Turn on debug mode with extra log info | ||
debug: false | ||
# Github Token / PAT for submitting PR comments | ||
token: ${{ secrets.GITHUB_TOKEN }} | ||
# Boolean flag to indicate whether any failing test should fail the script | ||
failOnWarning: false | ||
# Boolean flag to indicate whether any failing required functionality test should fail the script | ||
failOnRequired: false | ||
# Working directory to run the action from. Should be relative from the root of the project. | ||
workingDirectory: '' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
FROM hseeberger/scala-sbt:17.0.2_1.6.2_3.1.1 AS build | ||
|
||
WORKDIR /build | ||
COPY build.sbt . | ||
COPY project ./project/ | ||
COPY core ./core | ||
COPY example/product/src ./example/product/src | ||
EXPOSE 4001 | ||
CMD sbt example-product/run |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# federated subgraph to test apollo federation spec compatibility | ||
|
||
Implementation of a federated subgraph aligned to the requirements outlined in [apollo-federation-subgraph-compatibility](https://github.com/apollographql/apollo-federation-subgraph-compatibility). | ||
|
||
The subgraph can be used to verify compatibility against [Apollo Federation Subgraph Specification](https://www.apollographql.com/docs/federation/subgraph-spec/). | ||
|
||
### Run compatibility test | ||
Execute the following command from the repo root | ||
``` | ||
npx @apollo/federation-subgraph-compatibility docker --compose example/product/docker-compose.yaml --schema example/product/products.graphql | ||
``` | ||
|
||
### Printing the GraphQL schema (SQL) | ||
|
||
``` | ||
sbt "example-product/run printSchema" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
services: | ||
products: | ||
build: | ||
context: . | ||
dockerfile: ./example/product/Dockerfile | ||
ports: | ||
- 4001:4001 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
extend schema | ||
@link( | ||
url: "https://specs.apollo.dev/federation/v2.3" | ||
import: [ | ||
"@composeDirective" | ||
"@extends" | ||
"@external" | ||
"@key" | ||
"@inaccessible" | ||
"@interfaceObject" | ||
"@override" | ||
"@provides" | ||
"@requires" | ||
"@shareable" | ||
"@tag" | ||
] | ||
) | ||
@link(url: "https://myspecs.dev/myCustomDirective/v1.0", import: ["@custom"]) | ||
@composeDirective(name: "@custom") | ||
|
||
directive @custom on OBJECT | ||
|
||
type Product | ||
@custom | ||
@key(fields: "id") | ||
@key(fields: "sku package") | ||
@key(fields: "sku variation { id }") { | ||
id: ID! | ||
sku: String | ||
package: String | ||
variation: ProductVariation | ||
dimensions: ProductDimension | ||
createdBy: User @provides(fields: "totalProductsCreated") | ||
notes: String @tag(name: "internal") | ||
research: [ProductResearch!]! | ||
} | ||
|
||
type DeprecatedProduct @key(fields: "sku package") { | ||
sku: String! | ||
package: String! | ||
reason: String | ||
createdBy: User | ||
} | ||
|
||
type ProductVariation { | ||
id: ID! | ||
} | ||
|
||
type ProductResearch @key(fields: "study { caseNumber }") { | ||
study: CaseStudy! | ||
outcome: String | ||
} | ||
|
||
type CaseStudy { | ||
caseNumber: ID! | ||
description: String | ||
} | ||
|
||
type ProductDimension @shareable { | ||
size: String | ||
weight: Float | ||
unit: String @inaccessible | ||
} | ||
|
||
extend type Query { | ||
product(id: ID!): Product | ||
deprecatedProduct(sku: String!, package: String!): DeprecatedProduct | ||
@deprecated(reason: "Use product query instead") | ||
} | ||
|
||
extend type User @key(fields: "email") { | ||
averageProductsCreatedPerYear: Int | ||
@requires(fields: "totalProductsCreated yearsOfEmployment") | ||
email: ID! @external | ||
name: String @override(from: "users") | ||
totalProductsCreated: Int @external | ||
yearsOfEmployment: Int! @external | ||
} | ||
|
||
type Inventory @interfaceObject @key(fields: "id") { | ||
id: ID! | ||
deprecatedProducts: [DeprecatedProduct!]! | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import cats.effect.{ExitCode, IO, IOApp} | ||
import com.comcast.ip4s.IpLiteralSyntax | ||
import graphql.{ | ||
AppContext, | ||
CustomDirectiveSpec, | ||
GraphQLSchema, | ||
InventoryGraphQLSchema, | ||
ProductGraphQLSchema, | ||
UserGraphQLSchema | ||
} | ||
import http.{GraphQLExecutor, GraphQLServer} | ||
import io.circe.Json | ||
import sangria.federation.v2.{CustomDirectivesDefinition, Federation, Spec} | ||
import sangria.marshalling.InputUnmarshaller | ||
import sangria.renderer.QueryRenderer | ||
import sangria.schema.Schema | ||
import service.{ProductResearchService, ProductService, UserService} | ||
|
||
object Main extends IOApp { | ||
|
||
override def run(args: List[String]): IO[ExitCode] = (args match { | ||
case "printSchema" :: Nil => printSchema | ||
case _ => runGraphQLServer | ||
}).as(ExitCode.Success) | ||
|
||
private def printSchema: IO[Unit] = | ||
for { | ||
schema <- IO(schemaAndUm).map(_._1) | ||
_ <- IO.println(QueryRenderer.renderPretty(schema.toAst)) | ||
} yield () | ||
|
||
private def runGraphQLServer: IO[Unit] = | ||
for { | ||
ctx <- appContext | ||
executor <- graphQLExecutor(ctx) | ||
host = host"0.0.0.0" | ||
port = port"4001" | ||
_ <- IO.println(s"starting GraphQL HTTP server on http://$host:$port") | ||
_ <- GraphQLServer.bind(executor, host, port).use(_ => IO.never) | ||
} yield () | ||
|
||
private def appContext: IO[AppContext] = IO { | ||
new AppContext { | ||
override def productService: ProductService = ProductService.inMemory | ||
override def productResearchService: ProductResearchService = ProductResearchService.inMemory | ||
override def userService: UserService = UserService.inMemory | ||
} | ||
} | ||
|
||
private def schemaAndUm: (Schema[AppContext, Unit], InputUnmarshaller[Json]) = | ||
Federation.federate( | ||
GraphQLSchema.schema, | ||
CustomDirectivesDefinition( | ||
Spec("https://myspecs.dev/myCustomDirective/v1.0") -> List( | ||
CustomDirectiveSpec.CustomDirectiveDefinition) | ||
), | ||
sangria.marshalling.circe.CirceInputUnmarshaller, | ||
ProductGraphQLSchema.productResolver, | ||
ProductGraphQLSchema.deprecatedProductResolver, | ||
ProductGraphQLSchema.productResearchResolver, | ||
UserGraphQLSchema.userResolver, | ||
InventoryGraphQLSchema.inventoryResolver | ||
) | ||
|
||
private def graphQLExecutor(context: AppContext): IO[GraphQLExecutor[AppContext]] = IO { | ||
val (schema, um) = schemaAndUm | ||
GraphQLExecutor(schema, context)(um) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package graphql | ||
|
||
import service.{ProductResearchService, ProductService, UserService} | ||
|
||
trait AppContext { | ||
def productService: ProductService | ||
def productResearchService: ProductResearchService | ||
def userService: UserService | ||
} |
10 changes: 10 additions & 0 deletions
10
example/product/src/main/scala/graphql/CustomDirectiveSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package graphql | ||
|
||
import sangria.ast | ||
import sangria.schema | ||
|
||
object CustomDirectiveSpec { | ||
val CustomDirective: ast.Directive = ast.Directive("custom") | ||
val CustomDirectiveDefinition: schema.Directive = | ||
schema.Directive("custom", locations = Set(schema.DirectiveLocation.Object)) | ||
} |
24 changes: 24 additions & 0 deletions
24
example/product/src/main/scala/graphql/GraphQLSchema.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package graphql | ||
|
||
import graphql.InventoryGraphQLSchema.InventoryType | ||
import graphql.ProductGraphQLSchema.{deprecatedProductQueryField, productQueryField} | ||
import graphql.UserGraphQLSchema.UserType | ||
import sangria.schema._ | ||
|
||
object GraphQLSchema { | ||
private val QueryType: ObjectType[AppContext, Unit] = | ||
ObjectType( | ||
name = "Query", | ||
fieldsFn = () => | ||
fields[AppContext, Unit]( | ||
productQueryField, | ||
deprecatedProductQueryField | ||
) | ||
) | ||
|
||
val schema: Schema[AppContext, Unit] = Schema( | ||
query = QueryType, | ||
mutation = None, | ||
additionalTypes = List(UserType, InventoryType) | ||
) | ||
} |
34 changes: 34 additions & 0 deletions
34
example/product/src/main/scala/graphql/InventoryGraphQLSchema.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package graphql | ||
|
||
import io.circe.Json | ||
import model.{ID, Inventory} | ||
import graphql.ProductGraphQLSchema.DeprecatedProductType | ||
import io.circe.generic.semiauto.deriveDecoder | ||
import sangria.federation.v2.Directives.{InterfaceObject, Key} | ||
import sangria.federation.v2.{Decoder, EntityResolver} | ||
import sangria.schema._ | ||
|
||
object InventoryGraphQLSchema { | ||
val InventoryType: ObjectType[Unit, Inventory] = ObjectType( | ||
"Inventory", | ||
fields = fields[Unit, Inventory]( | ||
Field("id", IDType, resolve = _.value.id.value), | ||
Field( | ||
"deprecatedProducts", | ||
ListType(DeprecatedProductType), | ||
resolve = _.value.deprecatedProducts | ||
) | ||
) | ||
).withDirectives(Key("id"), InterfaceObject) | ||
|
||
case class InventoryArgs(id: ID) | ||
|
||
val jsonDecoder: io.circe.Decoder[InventoryArgs] = deriveDecoder[InventoryArgs] | ||
val decoder: Decoder[Json, InventoryArgs] = jsonDecoder.decodeJson | ||
|
||
def inventoryResolver: EntityResolver[AppContext, Json] { type Arg = InventoryArgs } = | ||
EntityResolver[AppContext, Json, Inventory, InventoryArgs]( | ||
__typeName = InventoryType.name, | ||
(arg, ctx) => ctx.ctx.productService.inventory(arg.id) | ||
)(decoder) | ||
} |
Oops, something went wrong.