Skip to content

Commit

Permalink
add subgraph compatibility check
Browse files Browse the repository at this point in the history
  • Loading branch information
paulpdaniels committed May 15, 2024
1 parent 2e927df commit 1345a3c
Show file tree
Hide file tree
Showing 32 changed files with 707 additions and 4 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/compatibility.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Federation Specification Compatibility Test

on:
pull_request:
branches:
- series/2.x

jobs:
compatibility:
runs-on: ubuntu-latest
steps:
- name: Checkout current branch (full)
uses: actions/checkout@v2
with:
fetch-depth: 0

- name: Setup Java (temurin@17)
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '17'

- name: Compatibility Test
uses: apollographql/federation-subgraph-compatibility@v2
with:
compose: 'apollo-compatibility/docker-compose.yaml'
schema: 'apollo-compatibility/schema.graphql'
path: 'graphql'
port: 4001
debug: true
token: ${{ secrets.GITHUB_TOKEN }}
# Boolean flag to indicate whether any failing test should fail the script
failOnWarning: false
# Boolean flag to indicate whether any required test should fail the script
failOnRequired: false
# Working directory to run the test from
workingDirectory: ''
12 changes: 12 additions & 0 deletions apollo-compatibility/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM hseeberger/scala-sbt:17.0.2_1.9.9_3.1.1 AS build

WORKDIR /build
COPY build.sbt .
COPY project ./project
COPY core ./core
COPY federation ./federation
COPY adapters/quick ./adapters/quick
COPY macros ./macros
COPY apollo-compatibility/src ./apollo-compatibility/src
EXPOSE 4001
CMD sbt apollo-compatibility/run
18 changes: 18 additions & 0 deletions apollo-compatibility/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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 compability against [Apollo Federation Subgraph Specification](https://www.apollographql.com/docs/federation/subgraph-spec).

### Run compatibility tests
Execute the following command from the root of the repo

```
npx @apollo/federation-subgraph-compatibility docker --compose apollo-compatibility/docker-compose.yml --schema apollo-compatibility/schema.graphql
```

### Printing the GraphQL Schema (SDL)

```
sbt "apollo-compability/run printSchema"
```
7 changes: 7 additions & 0 deletions apollo-compatibility/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
products:
build:
context: .
dockerfile: ./apollo-compatibility/Dockerfile
ports:
- 4001:4001
83 changes: 83 additions & 0 deletions apollo-compatibility/products.graphql
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!]!
}
35 changes: 35 additions & 0 deletions apollo-compatibility/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import caliban.CalibanError
import zio._
import caliban.quick._
import services.{ InventoryService, ProductService, UserService }
import zio.http.Server

object Main extends ZIOAppDefault {

def run = for {
args <- ZIOAppArgs.getArgs
_ <- (args match {
case Chunk("printSchema") => printSchema
case _ => runServer
})
} yield ()

val printSchema = Console.printLine(ProductSchema.print)

val runServer = {
val server: ZIO[
ProductService with UserService with InventoryService with Server,
CalibanError.ValidationError,
Int
] = (ProductSchema.api.toApp("/graphql") flatMap Server.install)

(server *> Console.printLine("Press any key to exit...") *> Console.readLine).orDie
.provide(
Server.defaultWithPort(4001),
ProductService.inMemory,
UserService.inMemory,
InventoryService.inMemory
)
}

}
103 changes: 103 additions & 0 deletions apollo-compatibility/src/main/scala/ProductSchema.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import caliban._
import caliban.federation.EntityResolver
import caliban.federation.tracing.ApolloFederatedTracing
import caliban.introspection.adt.{ __Directive, __DirectiveLocation }
import caliban.schema.Annotations.GQLDeprecated
import caliban.schema.{ GenericSchema, Schema }
import models._
import services.{ InventoryService, ProductService, UserService }
import zio.query.ZQuery
import zio.{ URIO, ZIO }

case class Query(
product: QueryProductArgs => URIO[ProductService, Option[models.Product]],
@GQLDeprecated("Use product query instead") deprecatedProduct: DeprecatedProductArgs => URIO[
ProductService,
Option[DeprecatedProduct]
]
)

object Query {
object apiSchema extends GenericSchema[ProductService with UserService]
implicit val schema: Schema[ProductService with UserService, Query] = apiSchema.gen
}

object ProductSchema extends GenericSchema[ProductService with UserService] {
val productResolver: EntityResolver[ProductService with UserService] =
EntityResolver[ProductService with UserService, ProductArgs, models.Product] {
case ProductArgs.IdOnly(id) =>
ZQuery.serviceWithZIO[ProductService](_.getProductById(id.id))
case ProductArgs.SkuAndPackage(sku, p) =>
ZQuery.serviceWithZIO[ProductService](_.getProductBySkuAndPackage(sku, p))
case ProductArgs.SkuAndVariationId(sku, variation) =>
ZQuery.serviceWithZIO[ProductService](_.getProductBySkuAndVariationId(sku, variation.id.id))
}

val userResolver: EntityResolver[UserService with ProductService] =
EntityResolver[UserService with ProductService, UserArgs, User] { args =>
ZQuery.serviceWithZIO[UserService](_.getUser)
}

val productResearchResolver: EntityResolver[UserService with ProductService] =
EntityResolver.from[ProductResearchArgs] { args =>
ZQuery.some(
ProductResearch(
CaseStudy(caseNumber = args.study.caseNumber, Some("Federation Study")),
None
)
)
}

val deprecatedProductResolver: EntityResolver[ProductService with UserService] =
EntityResolver[ProductService with UserService, DeprecatedProductArgs, DeprecatedProduct] { args =>
ZQuery.some(
models.DeprecatedProduct(
sku = "apollo-federation-v1",
`package` = "@apollo/federation-v1",
reason = Some("Migrate to Federation V2"),
createdBy = ZIO.serviceWithZIO[UserService](_.getUser)
)
)
}

val inventoryResolver: EntityResolver[InventoryService with UserService] =
EntityResolver[InventoryService with UserService, InventoryArgs, Inventory] { args =>
ZQuery.serviceWith[InventoryService](_.getById(args.id.id))
}

val api: GraphQL[ProductService with UserService with InventoryService] =
graphQL(
RootResolver(
Query(
args => ZIO.serviceWithZIO[ProductService](_.getProductById(args.id.id)),
args =>
ZIO.some(
models.DeprecatedProduct(
sku = "apollo-federation-v1",
`package` = "@apollo/federation-v1",
reason = Some("Migrate to Federation V2"),
createdBy = ZIO.serviceWithZIO[UserService](_.getUser)
)
)
)
),
directives = List(
__Directive(
"custom",
None,
Set(__DirectiveLocation.OBJECT),
_ => Nil,
isRepeatable = false
)
)
) @@ federated(
productResolver,
userResolver,
productResearchResolver,
deprecatedProductResolver,
inventoryResolver
) @@ ApolloFederatedTracing.wrapper()

val print = api.render

}
12 changes: 12 additions & 0 deletions apollo-compatibility/src/main/scala/models/CaseStudy.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package models

import caliban.schema.Schema

case class CaseStudy(
caseNumber: ID,
description: Option[String]
)

object CaseStudy {
implicit val schema: Schema[Any, CaseStudy] = Schema.gen
}
10 changes: 10 additions & 0 deletions apollo-compatibility/src/main/scala/models/CaseStudyArgs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package models

import caliban.schema.{ ArgBuilder, Schema }

case class CaseStudyArgs(caseNumber: ID)

object CaseStudyArgs {
implicit val schema: Schema[Any, CaseStudyArgs] = Schema.gen
implicit val argBuilder: ArgBuilder[CaseStudyArgs] = ArgBuilder.gen
}
6 changes: 6 additions & 0 deletions apollo-compatibility/src/main/scala/models/Custom.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package models

import caliban.parsing.adt.Directive
import caliban.schema.Annotations.GQLDirective

case class Custom() extends GQLDirective(Directive("custom"))
19 changes: 19 additions & 0 deletions apollo-compatibility/src/main/scala/models/DeprecatedProduct.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package models

import caliban.schema.{ GenericSchema, Schema }
import services.UserService
import zio.URIO

@GQLKey("sku package")
case class DeprecatedProduct(
sku: String,
`package`: String,
reason: Option[String],
createdBy: URIO[UserService, Option[User]]
)

object DeprecatedProduct {
object apiSchema extends GenericSchema[UserService]

implicit val schema: Schema[UserService, DeprecatedProduct] = apiSchema.gen[UserService, DeprecatedProduct]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package models

import caliban.schema.{ ArgBuilder, Schema }

case class DeprecatedProductArgs(
sku: String,
`package`: String
)

object DeprecatedProductArgs {
implicit val schema: Schema[Any, DeprecatedProductArgs] = Schema.gen
implicit val argBuilder: ArgBuilder[DeprecatedProductArgs] = ArgBuilder.gen[DeprecatedProductArgs]
}
11 changes: 11 additions & 0 deletions apollo-compatibility/src/main/scala/models/ID.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package models

import caliban.schema.{ ArgBuilder, Schema }
import caliban.Value.StringValue

case class ID(id: String) extends AnyVal

object ID {
implicit val schema: Schema[Any, ID] = Schema.scalarSchema[ID]("ID", None, None, None, id => StringValue(id.id))
implicit val argBuilder: ArgBuilder[ID] = ArgBuilder.string.map(ID(_))
}
Loading

0 comments on commit 1345a3c

Please sign in to comment.