diff --git a/.changeset/tricky-mice-crash.md b/.changeset/tricky-mice-crash.md new file mode 100644 index 00000000000..8c20d2bf190 --- /dev/null +++ b/.changeset/tricky-mice-crash.md @@ -0,0 +1,5 @@ +--- +"@graphql-tools/batch-delegate": patch +--- + +Add parentType name to the batch delegation key diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index 607fcd123d7..39e86d628f6 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -87,6 +87,11 @@ export function getLoader( let cacheKey = fieldName; + if (info.returnType) { + const namedType = getNamedType(info.returnType); + cacheKey += '@' + namedType.name; + } + if (selectionSet != null) { cacheKey += memoizedPrint(selectionSet); } diff --git a/packages/federation/test/fixtures/gateway/discount/discount.graphql b/packages/federation/test/fixtures/gateway/discount/discount.graphql new file mode 100644 index 00000000000..9c305bf5ee7 --- /dev/null +++ b/packages/federation/test/fixtures/gateway/discount/discount.graphql @@ -0,0 +1,18 @@ +type Query { + discounts(first: Int = 5): [Discount] +} + +extend type Product @key(fields: "upc") { + upc: String! @external + discounts: [Discount!]! +} + +extend type Category @key(fields: "id") { + id: ID! @external + discounts: [Discount!]! +} + +type Discount @key(fields: "id") { + id: ID! + discount: Int! +} diff --git a/packages/federation/test/fixtures/gateway/discount/index.ts b/packages/federation/test/fixtures/gateway/discount/index.ts new file mode 100644 index 00000000000..a2dd58f5fe4 --- /dev/null +++ b/packages/federation/test/fixtures/gateway/discount/index.ts @@ -0,0 +1,42 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { IResolvers } from '@graphql-tools/utils'; + +export const typeDefs = readFileSync(join(__dirname, './discount.graphql'), 'utf8'); + +export const resolvers: IResolvers = { + Product: { + __resolveReference(object) { + return { + ...object, + discounts, + }; + }, + }, + Category: { + __resolveReference(object) { + return { + ...object, + discounts, + }; + }, + }, + Discount: { + __resolveReference(object) { + return { + ...object, + ...discounts.find(discount => discount.id === object.id), + }; + }, + }, + Query: { + discounts(_, args) { + return discounts.slice(0, args.first); + }, + }, +}; +const discounts = [ + { id: '1', discount: 10 }, + { id: '2', discount: 20 }, + { id: '3', discount: 30 }, +]; diff --git a/packages/federation/test/fixtures/gateway/products/index.ts b/packages/federation/test/fixtures/gateway/products/index.ts index 52ed459618d..47fc7c7cc02 100644 --- a/packages/federation/test/fixtures/gateway/products/index.ts +++ b/packages/federation/test/fixtures/gateway/products/index.ts @@ -6,24 +6,52 @@ export const typeDefs = readFileSync(join(__dirname, './products.graphql'), 'utf const listSize = parseInt(process.env['PRODUCTS_SIZE'] || '3'); +const categories = [ + { + id: 'c_1', + name: 'Furniture', + }, + { + id: 'c_2', + name: 'Kitchen', + }, +]; + const definedProducts = [ { upc: '1', name: 'Table', price: 899, weight: 100, + categories: [categories[0]], }, { upc: '2', name: 'Couch', price: 1299, weight: 1000, + categories: [categories[0]], }, { upc: '3', name: 'Chair', price: 54, weight: 50, + categories: [categories[0]], + }, + { + upc: '4', + name: 'Knife', + price: 54, + weight: 50, + categories: [categories[1]], + }, + { + id: 'p_5', + name: 'Spoon', + price: 54, + weight: 50, + categories: [categories[1]], }, ]; const products = [...Array(listSize)].map((_, index) => definedProducts[index % 3]); @@ -34,6 +62,14 @@ export const resolvers: IResolvers = { return products.find(product => product.upc === object.upc); }, }, + Category: { + __resolveReference(object) { + return { + ...object, + ...categories.find(category => category.id === object.id), + }; + }, + }, Query: { topProducts(_, args) { return args.first ? products.slice(0, args.first) : products; diff --git a/packages/federation/test/fixtures/gateway/products/products.graphql b/packages/federation/test/fixtures/gateway/products/products.graphql index 9cec2ff5dc3..9889614b4b3 100644 --- a/packages/federation/test/fixtures/gateway/products/products.graphql +++ b/packages/federation/test/fixtures/gateway/products/products.graphql @@ -7,4 +7,10 @@ type Product @key(fields: "upc") { name: String price: Int weight: Int + categories: [Category!]! +} + +type Category @key(fields: "id") { + id: ID! + name: String! } diff --git a/packages/federation/test/fixtures/gateway/supergraph.graphql b/packages/federation/test/fixtures/gateway/supergraph.graphql index 70344a27397..a726e81289c 100644 --- a/packages/federation/test/fixtures/gateway/supergraph.graphql +++ b/packages/federation/test/fixtures/gateway/supergraph.graphql @@ -1,103 +1,98 @@ schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + @core(feature: "https://specs.apollo.dev/core/v0.2") + @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) { query: Query } -directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE +directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA directive @join__field( graph: join__Graph - requires: join__FieldSet provides: join__FieldSet - type: String - external: Boolean - override: String - usedOverridden: Boolean -) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + requires: join__FieldSet +) on FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__implements( - graph: join__Graph! - interface: String! -) repeatable on OBJECT | INTERFACE - -directive @join__type( - graph: join__Graph! - key: join__FieldSet - extension: Boolean! = false - resolvable: Boolean! = true - isInterfaceObject: Boolean! = false -) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - -directive @link( - url: String - as: String - for: link__Purpose - import: [link__Import] -) repeatable on SCHEMA +directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT -scalar join__FieldSet +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT -enum join__Graph { - accounts @join__graph(name: "accounts", url: "http://accounts:4001/graphql") - inventory @join__graph(name: "inventory", url: "http://inventory:4002/graphql") - products @join__graph(name: "products", url: "http://products:4003/graphql") - reviews @join__graph(name: "reviews", url: "http://reviews:4004/graphql") +type Category + @join__owner(graph: products) + @join__type(graph: products, key: "id") + @join__type(graph: discount, key: "id") { + discounts: [Discount!]! @join__field(graph: discount) + id: ID! @join__field(graph: products) + name: String! @join__field(graph: products) } -scalar link__Import - -enum link__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION +type Discount @join__owner(graph: discount) @join__type(graph: discount, key: "id") { + discount: Int! @join__field(graph: discount) + id: ID! @join__field(graph: discount) } type Product - @join__type(graph: inventory, key: "upc") + @join__owner(graph: products) @join__type(graph: products, key: "upc") + @join__type(graph: discount, key: "upc") + @join__type(graph: inventory, key: "upc") @join__type(graph: reviews, key: "upc") { - upc: String! - weight: Int @join__field(graph: inventory, external: true) @join__field(graph: products) - price: Int @join__field(graph: inventory, external: true) @join__field(graph: products) + categories: [Category!]! @join__field(graph: products) + discounts: [Discount!]! @join__field(graph: discount) inStock: Boolean @join__field(graph: inventory) - shippingEstimate: Int @join__field(graph: inventory, requires: "price weight") name: String @join__field(graph: products) + price: Int @join__field(graph: products) reviews: [Review] @join__field(graph: reviews) + shippingEstimate: Int @join__field(graph: inventory, requires: "price weight") + upc: String! @join__field(graph: products) + weight: Int @join__field(graph: products) } -type Query - @join__type(graph: accounts) - @join__type(graph: inventory) - @join__type(graph: products) - @join__type(graph: reviews) { +type Query { + discounts(first: Int = 5): [Discount] @join__field(graph: discount) me: User @join__field(graph: accounts) - users: [User] @join__field(graph: accounts) topProducts(first: Int): [Product] @join__field(graph: products) + users: [User] @join__field(graph: accounts) } -type Review @join__type(graph: reviews, key: "id") { - id: ID! - body: String - product: Product +type Review @join__owner(graph: reviews) @join__type(graph: reviews, key: "id") { author: User @join__field(graph: reviews, provides: "username") + body: String @join__field(graph: reviews) + id: ID! @join__field(graph: reviews) + product: Product @join__field(graph: reviews) } -type User @join__type(graph: accounts, key: "id") @join__type(graph: reviews, key: "id") { - id: ID! - name: String @join__field(graph: accounts) - username: String @join__field(graph: accounts) @join__field(graph: reviews, external: true) +type User + @join__owner(graph: accounts) + @join__type(graph: accounts, key: "id") + @join__type(graph: reviews, key: "id") { birthDate: String @join__field(graph: accounts) + id: ID! @join__field(graph: accounts) + name: String @join__field(graph: accounts) numberOfReviews: Int @join__field(graph: reviews) reviews: [Review] @join__field(graph: reviews) + username: String @join__field(graph: accounts) +} + +enum core__Purpose { + """ + `EXECUTION` features provide metadata necessary to for operation execution. + """ + EXECUTION + + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY +} + +scalar join__FieldSet + +enum join__Graph { + accounts @join__graph(name: "accounts", url: "http://www.accounts.com") + discount @join__graph(name: "discount", url: "http://www.discount.com") + inventory @join__graph(name: "inventory", url: "http://www.inventory.com") + products @join__graph(name: "products", url: "http://www.products.com") + reviews @join__graph(name: "reviews", url: "http://www.reviews.com") } diff --git a/packages/federation/test/fixtures/gateway/supergraph.yaml b/packages/federation/test/fixtures/gateway/supergraph.yaml index 2eede9a4879..60c957835b3 100644 --- a/packages/federation/test/fixtures/gateway/supergraph.yaml +++ b/packages/federation/test/fixtures/gateway/supergraph.yaml @@ -15,3 +15,7 @@ subgraphs: routing_url: http://www.inventory.com schema: file: ./inventory/inventory.graphql + discount: + routing_url: http://www.discount.com + schema: + file: ./discount/discount.graphql diff --git a/packages/federation/test/gateway.test.ts b/packages/federation/test/gateway.test.ts index 424b331cf9a..97d63f358b8 100644 --- a/packages/federation/test/gateway.test.ts +++ b/packages/federation/test/gateway.test.ts @@ -24,6 +24,7 @@ import { stitchSchemas } from '@graphql-tools/stitch'; import { ExecutionResult, IResolvers } from '@graphql-tools/utils'; import { getStitchedSchemaFromSupergraphSdl } from '../src/supergraph'; import * as accounts from './fixtures/gateway/accounts'; +import * as discount from './fixtures/gateway/discount'; import * as inventory from './fixtures/gateway/inventory'; import * as products from './fixtures/gateway/products'; import * as reviews from './fixtures/gateway/reviews'; @@ -35,6 +36,7 @@ const services = { inventory, products, reviews, + discount, }; interface ServiceInput { @@ -112,6 +114,19 @@ const exampleQuery = parse(/* GraphQL */ ` } } } + productsWithDiscount: topProducts(first: 1) { + upc + name + discounts { + id + } + categories { + id + discounts { + id + } + } + } } `); @@ -390,7 +405,20 @@ describe('Federation', () => { const result = await builtGateway.executor(parse(getIntrospectionQuery())); const schema = buildClientSchema(result.data); expect(printSchema(lexicographicSortSchema(schema))).toBeSimilarGqlDoc(/* GraphQL */ ` + type Category { + discounts: [Discount!]! + id: ID! + name: String! + } + + type Discount { + discount: Int! + id: ID! + } + type Product { + categories: [Category!]! + discounts: [Discount!]! inStock: Boolean name: String price: Int @@ -401,6 +429,7 @@ describe('Federation', () => { } type Query { + discounts(first: Int = 5): [Discount] me: User topProducts(first: Int): [Product] users: [User] @@ -881,6 +910,39 @@ describe('Federation', () => { username: '@complete', }, ], + productsWithDiscount: [ + { + categories: [ + { + discounts: [ + { + id: '1', + }, + { + id: '2', + }, + { + id: '3', + }, + ], + id: 'c_1', + }, + ], + discounts: [ + { + id: '1', + }, + { + id: '2', + }, + { + id: '3', + }, + ], + name: 'Table', + upc: '1', + }, + ], }, }); /* diff --git a/packages/federation/test/optimizations.test.ts b/packages/federation/test/optimizations.test.ts index 9e10aec663d..9f8079232e5 100644 --- a/packages/federation/test/optimizations.test.ts +++ b/packages/federation/test/optimizations.test.ts @@ -6,6 +6,7 @@ import { normalizedExecutor } from '@graphql-tools/executor'; import { buildSubgraphSchema } from '../src/subgraph'; import { getStitchedSchemaFromSupergraphSdl } from '../src/supergraph'; import * as accounts from './fixtures/gateway/accounts'; +import * as discount from './fixtures/gateway/discount'; import * as inventory from './fixtures/gateway/inventory'; import * as products from './fixtures/gateway/products'; import * as reviews from './fixtures/gateway/reviews'; @@ -16,6 +17,7 @@ describe('Optimizations', () => { inventory, products, reviews, + discount, }; let serviceCallCnt: Record; let schema: GraphQLSchema;