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 d736cfa
Show file tree
Hide file tree
Showing 31 changed files with 624 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:
product:
build:
context: .
dockerfile: ./apollo-compatibility/Dockerfile
ports:
- 4001:4001
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(_))
}
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
}
10 changes: 10 additions & 0 deletions apollo-compatibility/src/main/scala/models/InventoryArgs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package models

import caliban.schema.{ ArgBuilder, Schema }

case class InventoryArgs(id: ID)

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

import caliban.federation.v2x._

abstract class MyFederation
extends caliban.federation.v2x.FederationV2(
Versions.v2_3 :: Link(
"https://myspecs.dev/myCustomDirective/v1.0",
List(
Import("@custom")
)
) :: ComposeDirective("@custom") :: Nil
)
with FederationDirectivesV2_3
25 changes: 25 additions & 0 deletions apollo-compatibility/src/main/scala/models/Product.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package models

import caliban.schema.Schema
import zio.UIO

@GQLKey("id")
@GQLKey("sku package")
@GQLKey("sku variation { id }")
@Custom
case class Product(
id: ID,
sku: Option[String],
`package`: Option[String],
variation: Option[ProductVariation],
dimensions: Option[ProductDimension],
@GQLProvides("totalProductsCreated") createdBy: UIO[Option[User]],
@GQLTag("internal") notes: Option[String],
research: List[ProductResearch]
)

object Product {

implicit val schema: Schema[Any, Product] = Schema.gen

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

import caliban.InputValue
import caliban.schema.{ ArgBuilder, Schema }

sealed trait ProductArgs

object ProductArgs {
case class IdOnly(id: ID) extends ProductArgs
case class SkuAndPackage(sku: String, `package`: String) extends ProductArgs
case class SkuAndVariationId(sku: String, variation: ProductVariation) extends ProductArgs

private implicit val variationArgs: ArgBuilder[ProductVariation] = ArgBuilder.gen[ProductVariation]
val idOnlyArgBuilder: ArgBuilder[IdOnly] = ArgBuilder.gen[IdOnly]
val skuAndPackageArgBuilder: ArgBuilder[SkuAndPackage] = ArgBuilder.gen[SkuAndPackage]
val skuAndVariationIdArgBuilder: ArgBuilder[SkuAndVariationId] = ArgBuilder.gen[SkuAndVariationId]

implicit val argBuilder: ArgBuilder[ProductArgs] = (input: InputValue) =>
(for {
error <- skuAndVariationIdArgBuilder.build(input).swap
_ <- skuAndPackageArgBuilder.build(input).swap
_ <- idOnlyArgBuilder.build(input).swap
} yield error).swap

implicit val idOnlySchema: Schema[Any, ProductArgs.IdOnly] = Schema.gen
implicit val skuAndPackageSchema: Schema[Any, ProductArgs.SkuAndPackage] = Schema.gen
implicit val skuAndVariationIdSchema: Schema[Any, ProductArgs.SkuAndVariationId] = Schema.gen

}
Loading

0 comments on commit d736cfa

Please sign in to comment.