diff --git a/.vscode/launch.json b/.vscode/launch.json index 0d52caf47..61cf16458 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,6 +23,7 @@ "options": [ "apollo-cache", "apollo-federation", + "apollo-federation-2", "authorization", "automatic-validation", "custom-validation", diff --git a/docs/examples.md b/docs/examples.md index 4ea50125b..a501f1d02 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -41,7 +41,8 @@ Each subdirectory contains a `examples.graphql` file with predefined GraphQL que - [TypeORM (automatic, lazy relations) \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/typeorm-lazy-relations) - [MikroORM \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/mikro-orm) - [Typegoose \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/typegoose) -- [Apollo federation](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-federation) +- [Apollo Federation](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-federation) +- [Apollo Federation 2](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-federation-2) - [Apollo Cache Control](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-cache) - [GraphQL Scalars](https://github.com/MichalLytek/type-graphql/tree/master/examples/graphql-scalars) - [TSyringe](https://github.com/MichalLytek/type-graphql/tree/master/examples/tsyringe) diff --git a/examples/README.md b/examples/README.md index 7d5e6638f..935b1b01a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -41,7 +41,8 @@ Each subdirectory contains a `examples.graphql` file with predefined GraphQL que - [TypeORM (automatic, lazy relations) \*](./typeorm-lazy-relations) - [MikroORM \*](./mikro-orm) - [Typegoose \*](./typegoose) -- [Apollo federation](./apollo-federation) +- [Apollo Federation](./apollo-federation) +- [Apollo Federation 2](./apollo-federation-2) - [Apollo Cache Control](./apollo-cache) - [GraphQL Scalars](./graphql-scalars) - [TSyringe](./tsyringe) diff --git a/examples/apollo-federation-2/accounts/data.ts b/examples/apollo-federation-2/accounts/data.ts new file mode 100644 index 000000000..fde6e39b3 --- /dev/null +++ b/examples/apollo-federation-2/accounts/data.ts @@ -0,0 +1,20 @@ +import { User } from "./user"; + +function createUser(userData: Partial) { + return Object.assign(new User(), userData); +} + +export const users: User[] = [ + createUser({ + id: "1", + name: "Ada Lovelace", + birthDate: "1815-12-10", + username: "@ada", + }), + createUser({ + id: "2", + name: "Alan Turing", + birthDate: "1912-06-23", + username: "@complete", + }), +]; diff --git a/examples/apollo-federation-2/accounts/index.ts b/examples/apollo-federation-2/accounts/index.ts new file mode 100644 index 000000000..ca29e999a --- /dev/null +++ b/examples/apollo-federation-2/accounts/index.ts @@ -0,0 +1,25 @@ +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { AccountsResolver } from "./resolver"; +import { User } from "./user"; +import { resolveUserReference } from "./user.reference"; +import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; + +export async function listen(port: number): Promise { + const schema = await buildFederatedSchema( + { + resolvers: [AccountsResolver], + orphanedTypes: [User], + }, + { + User: { __resolveReference: resolveUserReference }, + }, + ); + + const server = new ApolloServer({ schema }); + + const { url } = await startStandaloneServer(server, { listen: { port } }); + console.log(`Accounts service ready at ${url}`); + + return url; +} diff --git a/examples/apollo-federation-2/accounts/resolver.ts b/examples/apollo-federation-2/accounts/resolver.ts new file mode 100644 index 000000000..1f92764f5 --- /dev/null +++ b/examples/apollo-federation-2/accounts/resolver.ts @@ -0,0 +1,11 @@ +import { Query, Resolver } from "type-graphql"; +import { users } from "./data"; +import { User } from "./user"; + +@Resolver(_of => User) +export class AccountsResolver { + @Query(_returns => User) + me(): User { + return users[0]; + } +} diff --git a/examples/apollo-federation-2/accounts/user.reference.ts b/examples/apollo-federation-2/accounts/user.reference.ts new file mode 100644 index 000000000..3d7112253 --- /dev/null +++ b/examples/apollo-federation-2/accounts/user.reference.ts @@ -0,0 +1,6 @@ +import { users } from "./data"; +import { type User } from "./user"; + +export async function resolveUserReference(reference: Pick): Promise { + return users.find(u => u.id === reference.id)!; +} diff --git a/examples/apollo-federation-2/accounts/user.ts b/examples/apollo-federation-2/accounts/user.ts new file mode 100644 index 000000000..ea9e6c423 --- /dev/null +++ b/examples/apollo-federation-2/accounts/user.ts @@ -0,0 +1,18 @@ +import { Directive, Field, ID, ObjectType } from "type-graphql"; + +@Directive(`@key(fields: "id")`) +@ObjectType() +export class User { + @Field(_type => ID) + id!: string; + + @Directive("@shareable") + @Field() + username!: string; + + @Field() + name!: string; + + @Field() + birthDate!: string; +} diff --git a/examples/apollo-federation-2/examples.graphql b/examples/apollo-federation-2/examples.graphql new file mode 100644 index 000000000..c1c39f816 --- /dev/null +++ b/examples/apollo-federation-2/examples.graphql @@ -0,0 +1,23 @@ +query { + topProducts { + __typename + name + price + shippingEstimate + inStock + reviews { + body + author { + name + birthDate + reviews { + product { + __typename + name + } + body + } + } + } + } +} diff --git a/examples/apollo-federation-2/helpers/buildFederatedSchema.ts b/examples/apollo-federation-2/helpers/buildFederatedSchema.ts new file mode 100644 index 000000000..a303dfb33 --- /dev/null +++ b/examples/apollo-federation-2/helpers/buildFederatedSchema.ts @@ -0,0 +1,40 @@ +import { buildSubgraphSchema } from "@apollo/subgraph"; +import { type IResolvers, printSchemaWithDirectives } from "@graphql-tools/utils"; +import gql from "graphql-tag"; +import deepMerge from "lodash.merge"; +import { type BuildSchemaOptions, buildSchema, createResolversMap } from "type-graphql"; + +export async function buildFederatedSchema( + options: Omit, + referenceResolvers?: IResolvers, +) { + // build TypeGraphQL schema + const schema = await buildSchema({ + ...options, + skipCheck: true, // disable check to allow schemas without query, etc. + }); + + // build Apollo Subgraph schema + const federatedSchema = buildSubgraphSchema({ + typeDefs: gql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: [ + "@key" + "@shareable" + "@provides" + "@extends" + "@requires" + "@external" + "@interfaceObject" + ] + ) + ${printSchemaWithDirectives(schema)} + `, + // merge schema's resolvers with reference resolvers + resolvers: deepMerge(createResolversMap(schema) as any, referenceResolvers), + }); + + return federatedSchema; +} diff --git a/examples/apollo-federation-2/index.ts b/examples/apollo-federation-2/index.ts new file mode 100644 index 000000000..e13b587c3 --- /dev/null +++ b/examples/apollo-federation-2/index.ts @@ -0,0 +1,46 @@ +import "reflect-metadata"; +import path from "path"; +import { ApolloGateway, IntrospectAndCompose } from "@apollo/gateway"; +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { emitSchemaDefinitionFile } from "type-graphql"; +import * as accounts from "./accounts"; +import * as inventory from "./inventory"; +import * as products from "./products"; +import * as reviews from "./reviews"; + +const startGraph = async (name: string, urlOrPromise: string | Promise) => { + const url = await urlOrPromise; + return { name, url }; +}; + +async function bootstrap() { + const subgraphs = await Promise.all([ + startGraph("accounts", accounts.listen(4001)), + startGraph("reviews", reviews.listen(4002)), + startGraph("products", products.listen(4003)), + startGraph("inventory", inventory.listen(4004)), + ]); + + const schemaGateway = new ApolloGateway({ + supergraphSdl: new IntrospectAndCompose({ + subgraphs, + }), + }); + const { schema } = await schemaGateway.load(); + await emitSchemaDefinitionFile(path.resolve(__dirname, "schema.graphql"), schema); + await schemaGateway.stop(); + + const gateway = new ApolloGateway({ + supergraphSdl: new IntrospectAndCompose({ + subgraphs, + }), + }); + const server = new ApolloServer({ gateway }); + + const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); + + console.log(`Apollo Gateway ready at ${url}`); +} + +bootstrap().catch(console.error); diff --git a/examples/apollo-federation-2/inventory/data.ts b/examples/apollo-federation-2/inventory/data.ts new file mode 100644 index 000000000..4796e02b2 --- /dev/null +++ b/examples/apollo-federation-2/inventory/data.ts @@ -0,0 +1,10 @@ +export interface Inventory { + upc: string; + inStock: boolean; +} + +export const inventory: Inventory[] = [ + { upc: "1", inStock: true }, + { upc: "2", inStock: false }, + { upc: "3", inStock: true }, +]; diff --git a/examples/apollo-federation-2/inventory/index.ts b/examples/apollo-federation-2/inventory/index.ts new file mode 100644 index 000000000..ad8cdfb1d --- /dev/null +++ b/examples/apollo-federation-2/inventory/index.ts @@ -0,0 +1,25 @@ +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { Product } from "./product"; +import { resolveProductReference } from "./product.reference"; +import { InventoryResolver } from "./resolver"; +import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; + +export async function listen(port: number): Promise { + const schema = await buildFederatedSchema( + { + resolvers: [InventoryResolver], + orphanedTypes: [Product], + }, + { + Product: { __resolveReference: resolveProductReference }, + }, + ); + + const server = new ApolloServer({ schema }); + + const { url } = await startStandaloneServer(server, { listen: { port } }); + console.log(`Inventory service ready at ${url}`); + + return url; +} diff --git a/examples/apollo-federation-2/inventory/product.reference.ts b/examples/apollo-federation-2/inventory/product.reference.ts new file mode 100644 index 000000000..31798d5f8 --- /dev/null +++ b/examples/apollo-federation-2/inventory/product.reference.ts @@ -0,0 +1,17 @@ +import { inventory } from "./data"; +import { Product } from "./product"; + +export async function resolveProductReference( + reference: Pick, +): Promise { + const found = inventory.find(i => i.upc === reference.upc); + + if (!found) { + return undefined; + } + + return Object.assign(new Product(), { + ...reference, + ...found, + }); +} diff --git a/examples/apollo-federation-2/inventory/product.ts b/examples/apollo-federation-2/inventory/product.ts new file mode 100644 index 000000000..c581bb2ae --- /dev/null +++ b/examples/apollo-federation-2/inventory/product.ts @@ -0,0 +1,22 @@ +import { Directive, Field, ObjectType } from "type-graphql"; + +@ObjectType() +@Directive("@extends") +@Directive("@interfaceObject") +@Directive(`@key(fields: "upc")`) +export class Product { + @Field() + @Directive("@external") + upc!: string; + + @Field() + @Directive("@external") + weight!: number; + + @Field() + @Directive("@external") + price!: number; + + @Field() + inStock!: boolean; +} diff --git a/examples/apollo-federation-2/inventory/resolver.ts b/examples/apollo-federation-2/inventory/resolver.ts new file mode 100644 index 000000000..cf593d874 --- /dev/null +++ b/examples/apollo-federation-2/inventory/resolver.ts @@ -0,0 +1,17 @@ +import { Directive, FieldResolver, Resolver, Root } from "type-graphql"; +import { Product } from "./product"; + +@Resolver(_of => Product) +export class InventoryResolver { + @Directive(`@requires(fields: "price weight")`) + @FieldResolver(_returns => Number) + async shippingEstimate(@Root() product: Product): Promise { + // free for expensive items + if (product.price > 1000) { + return 0; + } + + // estimate is based on weight + return product.weight * 0.5; + } +} diff --git a/examples/apollo-federation-2/products/data.ts b/examples/apollo-federation-2/products/data.ts new file mode 100644 index 000000000..93de15818 --- /dev/null +++ b/examples/apollo-federation-2/products/data.ts @@ -0,0 +1,27 @@ +import { Dining } from "./dining"; +import { type Product } from "./product"; +import { Seating } from "./seating"; + +export const products: Product[] = [ + Object.assign(new Dining(), { + upc: "1", + name: "Table", + price: 899, + weight: 100, + height: "3ft", + }), + Object.assign(new Seating(), { + upc: "2", + name: "Couch", + price: 1299, + weight: 1000, + seats: 2, + }), + Object.assign(new Seating(), { + upc: "3", + name: "Chair", + price: 54, + weight: 50, + seats: 1, + }), +]; diff --git a/examples/apollo-federation-2/products/dining.ts b/examples/apollo-federation-2/products/dining.ts new file mode 100644 index 000000000..251781c49 --- /dev/null +++ b/examples/apollo-federation-2/products/dining.ts @@ -0,0 +1,9 @@ +import { Directive, Field, ObjectType } from "type-graphql"; +import { Product } from "./product"; + +@Directive(`@key(fields: "upc")`) +@ObjectType({ implements: Product }) +export class Dining extends Product { + @Field() + height!: string; +} diff --git a/examples/apollo-federation-2/products/index.ts b/examples/apollo-federation-2/products/index.ts new file mode 100644 index 000000000..b5c31911f --- /dev/null +++ b/examples/apollo-federation-2/products/index.ts @@ -0,0 +1,25 @@ +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { Product } from "./product"; +import { resolveProductReference } from "./product.reference"; +import { ProductsResolver } from "./resolver"; +import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; + +export async function listen(port: number): Promise { + const schema = await buildFederatedSchema( + { + resolvers: [ProductsResolver], + orphanedTypes: [Product], + }, + { + Product: { __resolveReference: resolveProductReference }, + }, + ); + + const server = new ApolloServer({ schema }); + + const { url } = await startStandaloneServer(server, { listen: { port } }); + console.log(`Products service ready at ${url}`); + + return url; +} diff --git a/examples/apollo-federation-2/products/product.reference.ts b/examples/apollo-federation-2/products/product.reference.ts new file mode 100644 index 000000000..e6488b6c5 --- /dev/null +++ b/examples/apollo-federation-2/products/product.reference.ts @@ -0,0 +1,8 @@ +import { products } from "./data"; +import { type Product } from "./product"; + +export async function resolveProductReference( + reference: Pick, +): Promise { + return products.find(p => p.upc === reference.upc); +} diff --git a/examples/apollo-federation-2/products/product.ts b/examples/apollo-federation-2/products/product.ts new file mode 100644 index 000000000..bcdb56180 --- /dev/null +++ b/examples/apollo-federation-2/products/product.ts @@ -0,0 +1,17 @@ +import { Directive, Field, InterfaceType } from "type-graphql"; + +@Directive(`@key(fields: "upc")`) +@InterfaceType() +export abstract class Product { + @Field() + upc!: string; + + @Field() + name!: string; + + @Field() + price!: number; + + @Field() + weight!: number; +} diff --git a/examples/apollo-federation-2/products/resolver.ts b/examples/apollo-federation-2/products/resolver.ts new file mode 100644 index 000000000..047cf52cb --- /dev/null +++ b/examples/apollo-federation-2/products/resolver.ts @@ -0,0 +1,14 @@ +import { Arg, Query, Resolver } from "type-graphql"; +import { products } from "./data"; +import { Product } from "./product"; + +@Resolver(_of => Product) +export class ProductsResolver { + @Query(_returns => [Product]) + async topProducts( + @Arg("first", { defaultValue: 5 }) + first: number, + ): Promise { + return products.slice(0, first); + } +} diff --git a/examples/apollo-federation-2/products/seating.ts b/examples/apollo-federation-2/products/seating.ts new file mode 100644 index 000000000..8f76c4e58 --- /dev/null +++ b/examples/apollo-federation-2/products/seating.ts @@ -0,0 +1,9 @@ +import { Directive, Field, ObjectType } from "type-graphql"; +import { Product } from "./product"; + +@Directive(`@key(fields: "upc")`) +@ObjectType({ implements: Product }) +export class Seating extends Product { + @Field() + seats!: number; +} diff --git a/examples/apollo-federation-2/reviews/index.ts b/examples/apollo-federation-2/reviews/index.ts new file mode 100644 index 000000000..876531428 --- /dev/null +++ b/examples/apollo-federation-2/reviews/index.ts @@ -0,0 +1,23 @@ +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { Product } from "./product/product"; +import { ProductReviewsResolver } from "./product/resolver"; +import { ReviewsResolver } from "./review/resolver"; +import { Review } from "./review/review"; +import { UserReviewsResolver } from "./user/resolver"; +import { User } from "./user/user"; +import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; + +export async function listen(port: number): Promise { + const schema = await buildFederatedSchema({ + resolvers: [ReviewsResolver, ProductReviewsResolver, UserReviewsResolver], + orphanedTypes: [User, Review, Product], + }); + + const server = new ApolloServer({ schema }); + + const { url } = await startStandaloneServer(server, { listen: { port } }); + console.log(`Reviews service ready at ${url}`); + + return url; +} diff --git a/examples/apollo-federation-2/reviews/product/product.ts b/examples/apollo-federation-2/reviews/product/product.ts new file mode 100644 index 000000000..22f1588dc --- /dev/null +++ b/examples/apollo-federation-2/reviews/product/product.ts @@ -0,0 +1,11 @@ +import { Directive, Field, ObjectType } from "type-graphql"; + +@Directive("@extends") +@Directive(`@key(fields: "upc")`) +@Directive("@interfaceObject") +@ObjectType() +export class Product { + @Directive("@external") + @Field() + upc!: string; +} diff --git a/examples/apollo-federation-2/reviews/product/resolver.ts b/examples/apollo-federation-2/reviews/product/resolver.ts new file mode 100644 index 000000000..15f800ee1 --- /dev/null +++ b/examples/apollo-federation-2/reviews/product/resolver.ts @@ -0,0 +1,12 @@ +import { FieldResolver, Resolver, Root } from "type-graphql"; +import { Product } from "./product"; +import { reviews } from "../review/data"; +import { Review } from "../review/review"; + +@Resolver(_of => Product) +export class ProductReviewsResolver { + @FieldResolver(() => [Review]) + async reviews(@Root() product: Product): Promise { + return reviews.filter(review => review.product.upc === product.upc); + } +} diff --git a/examples/apollo-federation-2/reviews/review/data.ts b/examples/apollo-federation-2/reviews/review/data.ts new file mode 100644 index 000000000..c6e9ed0fc --- /dev/null +++ b/examples/apollo-federation-2/reviews/review/data.ts @@ -0,0 +1,62 @@ +import { Review } from "./review"; +import { Product } from "../product/product"; +import { User } from "../user/user"; + +function createReview(reviewData: Partial) { + return Object.assign(new Review(), reviewData); +} + +function createUser(userData: Partial) { + return Object.assign(new User(), userData); +} + +function createProduct(productData: Partial) { + return Object.assign(new Product(), productData); +} + +export const reviews: Review[] = [ + createReview({ + id: "1", + author: createUser({ + id: "1", + username: "@ada", + }), + product: createProduct({ + upc: "1", + }), + body: "Love it!", + }), + createReview({ + id: "2", + author: createUser({ + id: "1", + username: "@ada", + }), + product: createProduct({ + upc: "2", + }), + body: "Too expensive.", + }), + createReview({ + id: "3", + author: createUser({ + id: "2", + username: "@complete", + }), + product: createProduct({ + upc: "3", + }), + body: "Could be better.", + }), + createReview({ + id: "4", + author: createUser({ + id: "2", + username: "@complete", + }), + product: createProduct({ + upc: "1", + }), + body: "Prefer something else.", + }), +]; diff --git a/examples/apollo-federation-2/reviews/review/resolver.ts b/examples/apollo-federation-2/reviews/review/resolver.ts new file mode 100644 index 000000000..eb59bf84b --- /dev/null +++ b/examples/apollo-federation-2/reviews/review/resolver.ts @@ -0,0 +1,11 @@ +import { FieldResolver, Resolver } from "type-graphql"; +import { reviews } from "./data"; +import { Review } from "./review"; + +@Resolver(_of => Review) +export class ReviewsResolver { + @FieldResolver(_returns => [Review]) + async reviews(): Promise { + return reviews; + } +} diff --git a/examples/apollo-federation-2/reviews/review/review.ts b/examples/apollo-federation-2/reviews/review/review.ts new file mode 100644 index 000000000..24c74e7ab --- /dev/null +++ b/examples/apollo-federation-2/reviews/review/review.ts @@ -0,0 +1,20 @@ +import { Directive, Field, ID, ObjectType } from "type-graphql"; +import { Product } from "../product/product"; +import { User } from "../user/user"; + +@Directive(`@key(fields: "id")`) +@ObjectType() +export class Review { + @Field(_type => ID) + id!: string; + + @Field() + body!: string; + + @Directive(`@provides(fields: "username")`) + @Field() + author!: User; + + @Field() + product!: Product; +} diff --git a/examples/apollo-federation-2/reviews/user/resolver.ts b/examples/apollo-federation-2/reviews/user/resolver.ts new file mode 100644 index 000000000..ae1858d3d --- /dev/null +++ b/examples/apollo-federation-2/reviews/user/resolver.ts @@ -0,0 +1,12 @@ +import { FieldResolver, Resolver, Root } from "type-graphql"; +import { User } from "./user"; +import { reviews } from "../review/data"; +import { Review } from "../review/review"; + +@Resolver(_of => User) +export class UserReviewsResolver { + @FieldResolver(_returns => [Review]) + async reviews(@Root() user: User): Promise { + return reviews.filter(review => review.author.id === user.id); + } +} diff --git a/examples/apollo-federation-2/reviews/user/user.ts b/examples/apollo-federation-2/reviews/user/user.ts new file mode 100644 index 000000000..8a1eb5eef --- /dev/null +++ b/examples/apollo-federation-2/reviews/user/user.ts @@ -0,0 +1,12 @@ +import { Directive, Field, ID, ObjectType } from "type-graphql"; + +@Directive(`@key(fields: "id")`) +@ObjectType() +export class User { + @Field(_type => ID) + id!: string; + + @Directive("@external") + @Field() + username!: string; +} diff --git a/examples/apollo-federation-2/schema.graphql b/examples/apollo-federation-2/schema.graphql new file mode 100644 index 000000000..65ba384e2 --- /dev/null +++ b/examples/apollo-federation-2/schema.graphql @@ -0,0 +1,57 @@ +# ----------------------------------------------- +# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! +# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! +# ----------------------------------------------- + +type Dining implements Product { + height: String! + inStock: Boolean! + name: String! + price: Float! + reviews: [Review!]! + shippingEstimate: Float! + upc: String! + weight: Float! +} + +interface Product { + inStock: Boolean! + name: String! + price: Float! + reviews: [Review!]! + shippingEstimate: Float! + upc: String! + weight: Float! +} + +type Query { + me: User! + topProducts(first: Float! = 5): [Product!]! +} + +type Review { + author: User! + body: String! + id: ID! + product: Product! + reviews: [Review!]! +} + +type Seating implements Product { + inStock: Boolean! + name: String! + price: Float! + reviews: [Review!]! + seats: Float! + shippingEstimate: Float! + upc: String! + weight: Float! +} + +type User { + birthDate: String! + id: ID! + name: String! + reviews: [Review!]! + username: String! +} diff --git a/examples/apollo-federation/examples.graphql b/examples/apollo-federation/examples.graphql index 45dca094f..38474e680 100644 --- a/examples/apollo-federation/examples.graphql +++ b/examples/apollo-federation/examples.graphql @@ -13,6 +13,7 @@ query { product { name } + body } } }