Skip to content

Commit

Permalink
add subgraph compatibility check (#2234)
Browse files Browse the repository at this point in the history
* add subgraph compatibility check

* fix schema resolution

* fix docker image

* fix graphql path

* fix project name

* try with graalvm

* prebuild repo for faster startup

* remove readline

* revert accidental changes, disable assembly outside of compat

* remove compat build from 2.12 and 3

* use RC7 methods

* try enabling scala3

* Update apollo-compatibility/README.md

Co-authored-by: Pierre Ricadat <[email protected]>

---------

Co-authored-by: Pierre Ricadat <[email protected]>
  • Loading branch information
paulpdaniels and ghostdogpr authored May 21, 2024
1 parent 38ff734 commit 378ef0d
Show file tree
Hide file tree
Showing 31 changed files with 739 additions and 3 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/compatibility.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Federation Specification Compatibility Test

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

jobs:
compatibility:
runs-on: ubuntu-latest
steps:
- name: Checkout current branch
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'adopt'
cache: 'sbt'
- name: Cache scala dependencies
uses: coursier/cache-action@v6
- name: Run assembly
run: sbt "apolloCompatibility/assembly"

- name: Compatibility Test
uses: apollographql/federation-subgraph-compatibility@v2
with:
compose: 'apollo-compatibility/docker-compose.yaml'
schema: 'apollo-compatibility/products.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: ''
7 changes: 7 additions & 0 deletions apollo-compatibility/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM sbtscala/scala-sbt:graalvm-ce-22.3.3-b1-java17_1.9.8_2.13.12 AS build

WORKDIR /app
COPY build.sbt .
COPY apollo-compatibility/target/apollo-subgraph-compatibility.jar /app/artifact.jar
EXPOSE 4001
CMD java $* -jar artifact.jar
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.{ Response, Routes, Server }

object Main extends ZIOAppDefault {

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

private val printSchema = Console.printLine(ProductSchema.print)

private val runServer = {
val routes: Task[Routes[ProductService with UserService with InventoryService, Response]] =
ProductSchema.api.routes("/graphql")

val server: ZIO[ProductService with UserService with InventoryService with Server, Throwable, Response] =
routes.flatMap(Server.serve(_))

server.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(_))
}
16 changes: 16 additions & 0 deletions apollo-compatibility/src/main/scala/models/Inventory.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package models

import caliban.schema.{ GenericSchema, Schema }
import services.{ InventoryService, UserService }

@GQLInterfaceObject
@GQLKey("email")
case class Inventory(
id: ID,
deprecatedProducts: List[DeprecatedProduct]
)

object Inventory {
object genSchema extends GenericSchema[InventoryService with UserService]
implicit val schema: Schema[InventoryService with UserService, Inventory] = genSchema.gen
}
Loading

0 comments on commit 378ef0d

Please sign in to comment.