From 7cdc69e0454ab99ea5c3b8072ac28da8f81a8796 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 31 Jul 2023 21:58:48 +0300 Subject: [PATCH] Supergraph Handler (#5744) * Go * chore(dependencies): updated changesets for modified dependencies * Cleanup * Go * Do not do batching for a single subschema * Go * Trigger * Fix dependenceis * chore(dependencies): updated changesets for modified dependencies --------- Co-authored-by: github-actions[bot] --- ...@graphql-mesh_graphql-5744-dependencies.md | 6 + ...mesh_merger-stitching-5744-dependencies.md | 5 + ...@graphql-mesh_runtime-5744-dependencies.md | 5 + .changeset/ninety-llamas-live.md | 6 + .changeset/three-toes-shave.md | 6 + .../gateway-supergraph/.meshrc.yml | 11 + .../gateway-supergraph/example-query.graphql | 58 ++ .../gateway-supergraph/package.json | 11 + .../gateway-supergraph/supergraph.graphql | 78 +++ .../gateway-supergraph/supergraph.yaml | 17 + examples/federation-example/package.json | 1 + .../services/accounts-subgraph/index.ts | 6 + .../services/accounts-subgraph/package.json | 16 + .../services/accounts-subgraph/server.ts | 71 +++ .../accounts-subgraph/typeDefs.graphql | 11 + .../services/inventory/server.ts | 4 + .../services/inventory/typeDefs.graphql | 7 + .../services/products/typeDefs.graphql | 10 + .../services/reviews/typeDefs.graphql | 17 + .../federation-example.test.ts.snap | 460 ---------------- .../tests/federation-example.test.ts | 505 +++++++++++++++++- packages/handlers/graphql/package.json | 2 +- packages/handlers/graphql/src/index.ts | 59 +- packages/handlers/supergraph/package.json | 56 ++ packages/handlers/supergraph/src/index.ts | 100 ++++ .../handlers/supergraph/yaml-config.graphql | 14 + packages/mergers/stitching/package.json | 3 +- packages/mergers/stitching/src/index.ts | 108 +--- packages/runtime/package.json | 1 - packages/runtime/src/useSubschema.ts | 3 +- packages/types/src/config-schema.json | 30 ++ packages/types/src/config.ts | 10 + packages/types/src/index.ts | 2 +- .../SupergraphHandler.generated.md | 5 + yarn.lock | 39 +- 35 files changed, 1150 insertions(+), 593 deletions(-) create mode 100644 .changeset/@graphql-mesh_graphql-5744-dependencies.md create mode 100644 .changeset/@graphql-mesh_merger-stitching-5744-dependencies.md create mode 100644 .changeset/@graphql-mesh_runtime-5744-dependencies.md create mode 100644 .changeset/ninety-llamas-live.md create mode 100644 .changeset/three-toes-shave.md create mode 100644 examples/federation-example/gateway-supergraph/.meshrc.yml create mode 100644 examples/federation-example/gateway-supergraph/example-query.graphql create mode 100644 examples/federation-example/gateway-supergraph/package.json create mode 100644 examples/federation-example/gateway-supergraph/supergraph.graphql create mode 100644 examples/federation-example/gateway-supergraph/supergraph.yaml create mode 100644 examples/federation-example/services/accounts-subgraph/index.ts create mode 100644 examples/federation-example/services/accounts-subgraph/package.json create mode 100755 examples/federation-example/services/accounts-subgraph/server.ts create mode 100644 examples/federation-example/services/accounts-subgraph/typeDefs.graphql create mode 100644 examples/federation-example/services/inventory/typeDefs.graphql create mode 100644 examples/federation-example/services/products/typeDefs.graphql create mode 100644 examples/federation-example/services/reviews/typeDefs.graphql delete mode 100644 examples/federation-example/tests/__snapshots__/federation-example.test.ts.snap create mode 100644 packages/handlers/supergraph/package.json create mode 100644 packages/handlers/supergraph/src/index.ts create mode 100644 packages/handlers/supergraph/yaml-config.graphql create mode 100644 website/src/generated-markdown/SupergraphHandler.generated.md diff --git a/.changeset/@graphql-mesh_graphql-5744-dependencies.md b/.changeset/@graphql-mesh_graphql-5744-dependencies.md new file mode 100644 index 0000000000000..90a8bce5d2884 --- /dev/null +++ b/.changeset/@graphql-mesh_graphql-5744-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-mesh/graphql": patch +--- +dependencies updates: + - Added dependency [`@graphql-tools/federation@^1.1.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/federation/v/1.1.0) (to `dependencies`) + - Removed dependency [`@graphql-tools/wrap@^10.0.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/wrap/v/10.0.0) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_merger-stitching-5744-dependencies.md b/.changeset/@graphql-mesh_merger-stitching-5744-dependencies.md new file mode 100644 index 0000000000000..aca6da9ee281a --- /dev/null +++ b/.changeset/@graphql-mesh_merger-stitching-5744-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/merger-stitching": patch +--- +dependencies updates: + - Removed dependency [`@graphql-tools/stitching-directives@^3.0.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/stitching-directives/v/3.0.0) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_runtime-5744-dependencies.md b/.changeset/@graphql-mesh_runtime-5744-dependencies.md new file mode 100644 index 0000000000000..d3823cee4d5f3 --- /dev/null +++ b/.changeset/@graphql-mesh_runtime-5744-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/runtime": patch +--- +dependencies updates: + - Removed dependency [`@graphql-tools/batch-execute@^9.0.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/batch-execute/v/9.0.0) (from `dependencies`) diff --git a/.changeset/ninety-llamas-live.md b/.changeset/ninety-llamas-live.md new file mode 100644 index 0000000000000..5b6c3727b0f8f --- /dev/null +++ b/.changeset/ninety-llamas-live.md @@ -0,0 +1,6 @@ +--- +'@graphql-mesh/merger-stitching': patch +'@graphql-mesh/graphql': patch +--- + +New strategy to handle Federation diff --git a/.changeset/three-toes-shave.md b/.changeset/three-toes-shave.md new file mode 100644 index 0000000000000..3a1a9fc40010a --- /dev/null +++ b/.changeset/three-toes-shave.md @@ -0,0 +1,6 @@ +--- +'@graphql-mesh/supergraph': patch +'@graphql-mesh/types': patch +--- + +New Supergraph handler diff --git a/examples/federation-example/gateway-supergraph/.meshrc.yml b/examples/federation-example/gateway-supergraph/.meshrc.yml new file mode 100644 index 0000000000000..292c87da03394 --- /dev/null +++ b/examples/federation-example/gateway-supergraph/.meshrc.yml @@ -0,0 +1,11 @@ +sources: + - name: Supergraph + handler: + supergraph: + source: ./supergraph.graphql + +documents: + - example-query.graphql + +serve: + playground: true diff --git a/examples/federation-example/gateway-supergraph/example-query.graphql b/examples/federation-example/gateway-supergraph/example-query.graphql new file mode 100644 index 0000000000000..bf6762cc001ad --- /dev/null +++ b/examples/federation-example/gateway-supergraph/example-query.graphql @@ -0,0 +1,58 @@ +fragment User on User { + id + username + name +} + +fragment Review on Review { + id + body +} + +fragment Product on Product { + inStock + name + price + shippingEstimate + upc + weight +} + +query TestQuery { + users { + ...User + reviews { + ...Review + product { + ...Product + reviews { + ...Review + author { + ...User + reviews { + ...Review + product { + ...Product + } + } + } + } + } + } + } + topProducts { + ...Product + reviews { + ...Review + author { + ...User + reviews { + ...Review + product { + ...Product + } + } + } + } + } +} diff --git a/examples/federation-example/gateway-supergraph/package.json b/examples/federation-example/gateway-supergraph/package.json new file mode 100644 index 0000000000000..b3b35a3522870 --- /dev/null +++ b/examples/federation-example/gateway-supergraph/package.json @@ -0,0 +1,11 @@ +{ + "name": "federation-supergraph-gateway", + "version": "0.0.0", + "license": "MIT", + "private": true, + "dependencies": { + "@graphql-mesh/cli": "0.86.0", + "@graphql-mesh/supergraph": "0.0.0", + "graphql": "16.7.1" + } +} diff --git a/examples/federation-example/gateway-supergraph/supergraph.graphql b/examples/federation-example/gateway-supergraph/supergraph.graphql new file mode 100644 index 0000000000000..2ab164965b15b --- /dev/null +++ b/examples/federation-example/gateway-supergraph/supergraph.graphql @@ -0,0 +1,78 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.2") + @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) { + query: Query +} + +directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph + provides: join__FieldSet + requires: join__FieldSet +) on FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT + +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT + +type Product + @join__owner(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean @join__field(graph: INVENTORY) + 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 { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) + user(id: ID!): User @join__field(graph: ACCOUNTS) + users: [User] @join__field(graph: ACCOUNTS) +} + +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__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + 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://localhost:9880/graphql") + INVENTORY @join__graph(name: "inventory", url: "http://localhost:9872/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:9873/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:9874/graphql") +} diff --git a/examples/federation-example/gateway-supergraph/supergraph.yaml b/examples/federation-example/gateway-supergraph/supergraph.yaml new file mode 100644 index 0000000000000..a5283f5197b7e --- /dev/null +++ b/examples/federation-example/gateway-supergraph/supergraph.yaml @@ -0,0 +1,17 @@ +subgraphs: + accounts: + routing_url: http://localhost:9880/graphql + schema: + file: ../services/accounts-subgraph/typeDefs.graphql + reviews: + routing_url: http://localhost:9874/graphql + schema: + file: ../services/reviews/typeDefs.graphql + products: + routing_url: http://localhost:9873/graphql + schema: + file: ../services/products/typeDefs.graphql + inventory: + routing_url: http://localhost:9872/graphql + schema: + file: ../services/inventory/typeDefs.graphql diff --git a/examples/federation-example/package.json b/examples/federation-example/package.json index a8cc845533653..23d6aebb4f04a 100644 --- a/examples/federation-example/package.json +++ b/examples/federation-example/package.json @@ -12,6 +12,7 @@ "start-gateway": "mesh dev --dir gateway", "start-gateway-delayed": "delay 1 && npm run start-gateway", "start-service-accounts": "ts-node services/accounts/index.ts", + "start-service-accounts-subgraph": "ts-node services/accounts-subgraph/index.ts", "start-service-inventory": "ts-node services/inventory/index.ts", "start-service-products": "ts-node services/products/index.ts", "start-service-reviews": "ts-node services/reviews/index.ts", diff --git a/examples/federation-example/services/accounts-subgraph/index.ts b/examples/federation-example/services/accounts-subgraph/index.ts new file mode 100644 index 0000000000000..b4dc653ac2849 --- /dev/null +++ b/examples/federation-example/services/accounts-subgraph/index.ts @@ -0,0 +1,6 @@ +import { accountsSubgraphServer } from './server'; + +accountsSubgraphServer().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/examples/federation-example/services/accounts-subgraph/package.json b/examples/federation-example/services/accounts-subgraph/package.json new file mode 100644 index 0000000000000..4cdc55015774e --- /dev/null +++ b/examples/federation-example/services/accounts-subgraph/package.json @@ -0,0 +1,16 @@ +{ + "name": "accounts-subgraph", + "version": "0.0.0", + "license": "MIT", + "private": true, + "scripts": { + "start": "ts-node index.ts" + }, + "dependencies": { + "@apollo/subgraph": "2.5.1", + "apollo-server": "3.12.0", + "graphql": "16.7.1", + "ts-node": "10.9.1", + "typescript": "5.1.6" + } +} diff --git a/examples/federation-example/services/accounts-subgraph/server.ts b/examples/federation-example/services/accounts-subgraph/server.ts new file mode 100755 index 0000000000000..68560b43c0e4e --- /dev/null +++ b/examples/federation-example/services/accounts-subgraph/server.ts @@ -0,0 +1,71 @@ +import { ApolloServer, gql } from 'apollo-server'; +import { buildSubgraphSchema } from '@apollo/subgraph'; + +const typeDefs = gql` + extend type Query { + me: User + user(id: ID!): User + users: [User] + } + + type User @key(fields: "id") { + id: ID! + name: String + username: String + } +`; + +const resolvers = { + User: { + __resolveReference(object, context) { + return { + ...object, + ...context.users.find(user => user.id === object.id), + }; + }, + }, + Query: { + me(_root, _args, context) { + return context.users[0]; + }, + users(_root, _args, context) { + return context.users; + }, + user(_root, args, context) { + return context.users.find(user => user.id === args.id); + }, + }, +}; + +const server = new ApolloServer({ + schema: buildSubgraphSchema([ + { + typeDefs, + resolvers, + }, + ]), + context: { + users: [ + { + id: '1', + name: 'Ada Lovelace', + birthDate: '1815-12-10', + username: '@ada', + }, + { + id: '2', + name: 'Alan Turing', + birthDate: '1912-06-23', + username: '@complete', + }, + ], + }, +}); + +export const accountsSubgraphServer = () => + server.listen({ port: 9880 }).then(({ url }) => { + if (!process.env.CI) { + console.log(`🚀 Server ready at ${url}`); + } + return server; + }); diff --git a/examples/federation-example/services/accounts-subgraph/typeDefs.graphql b/examples/federation-example/services/accounts-subgraph/typeDefs.graphql new file mode 100644 index 0000000000000..8e2ef4578e309 --- /dev/null +++ b/examples/federation-example/services/accounts-subgraph/typeDefs.graphql @@ -0,0 +1,11 @@ +extend type Query { + me: User + user(id: ID!): User + users: [User] +} + +type User @key(fields: "id") { + id: ID! + name: String + username: String +} diff --git a/examples/federation-example/services/inventory/server.ts b/examples/federation-example/services/inventory/server.ts index 34e2e5cfbb7b3..a941cb88cabf8 100755 --- a/examples/federation-example/services/inventory/server.ts +++ b/examples/federation-example/services/inventory/server.ts @@ -1,3 +1,4 @@ +import { inspect } from 'node:util'; import { ApolloServer, gql } from 'apollo-server'; import { buildSubgraphSchema } from '@apollo/subgraph'; @@ -20,6 +21,9 @@ const resolvers = { }; }, shippingEstimate(object) { + if (object.price == null || object.weight == null) { + throw new Error(`${inspect(object)} doesn't have required fields; "price" and "weight".`); + } // free for expensive items if (object.price > 1000) return 0; // estimate is based on weight diff --git a/examples/federation-example/services/inventory/typeDefs.graphql b/examples/federation-example/services/inventory/typeDefs.graphql new file mode 100644 index 0000000000000..09e98750a989d --- /dev/null +++ b/examples/federation-example/services/inventory/typeDefs.graphql @@ -0,0 +1,7 @@ +extend type Product @key(fields: "upc") { + upc: String! @external + weight: Int @external + price: Int @external + inStock: Boolean + shippingEstimate: Int @requires(fields: "price weight") +} diff --git a/examples/federation-example/services/products/typeDefs.graphql b/examples/federation-example/services/products/typeDefs.graphql new file mode 100644 index 0000000000000..539fa6c70e225 --- /dev/null +++ b/examples/federation-example/services/products/typeDefs.graphql @@ -0,0 +1,10 @@ +extend type Query { + topProducts(first: Int = 5): [Product] +} + +type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + weight: Int +} diff --git a/examples/federation-example/services/reviews/typeDefs.graphql b/examples/federation-example/services/reviews/typeDefs.graphql new file mode 100644 index 0000000000000..bee04bb5ae2bf --- /dev/null +++ b/examples/federation-example/services/reviews/typeDefs.graphql @@ -0,0 +1,17 @@ +type Review @key(fields: "id") { + id: ID! + body: String + author: User @provides(fields: "username") + product: Product +} + +extend type User @key(fields: "id") { + id: ID! @external + username: String @external + reviews: [Review] +} + +extend type Product @key(fields: "upc") { + upc: String! @external + reviews: [Review] +} diff --git a/examples/federation-example/tests/__snapshots__/federation-example.test.ts.snap b/examples/federation-example/tests/__snapshots__/federation-example.test.ts.snap deleted file mode 100644 index ee9c873a09d71..0000000000000 --- a/examples/federation-example/tests/__snapshots__/federation-example.test.ts.snap +++ /dev/null @@ -1,460 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Federation Example should give correct response for example queries 1`] = ` -{ - "topProducts": [ - { - "inStock": true, - "name": "Table", - "price": 899, - "reviews": [ - { - "author": { - "id": "1", - "name": "Ada Lovelace", - "reviews": [ - { - "body": "Love it!", - "id": "1", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - { - "body": "Too expensive.", - "id": "2", - "product": { - "inStock": false, - "name": "Couch", - "price": 1299, - "shippingEstimate": 0, - "upc": "2", - "weight": 1000, - }, - }, - ], - "username": "@ada", - }, - "body": "Love it!", - "id": "1", - }, - { - "author": { - "id": "2", - "name": "Alan Turing", - "reviews": [ - { - "body": "Could be better.", - "id": "3", - "product": { - "inStock": true, - "name": "Chair", - "price": 54, - "shippingEstimate": 25, - "upc": "3", - "weight": 50, - }, - }, - { - "body": "Prefer something else.", - "id": "4", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - ], - "username": "@complete", - }, - "body": "Prefer something else.", - "id": "4", - }, - ], - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - { - "inStock": false, - "name": "Couch", - "price": 1299, - "reviews": [ - { - "author": { - "id": "1", - "name": "Ada Lovelace", - "reviews": [ - { - "body": "Love it!", - "id": "1", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - { - "body": "Too expensive.", - "id": "2", - "product": { - "inStock": false, - "name": "Couch", - "price": 1299, - "shippingEstimate": 0, - "upc": "2", - "weight": 1000, - }, - }, - ], - "username": "@ada", - }, - "body": "Too expensive.", - "id": "2", - }, - ], - "shippingEstimate": 0, - "upc": "2", - "weight": 1000, - }, - { - "inStock": true, - "name": "Chair", - "price": 54, - "reviews": [ - { - "author": { - "id": "2", - "name": "Alan Turing", - "reviews": [ - { - "body": "Could be better.", - "id": "3", - "product": { - "inStock": true, - "name": "Chair", - "price": 54, - "shippingEstimate": 25, - "upc": "3", - "weight": 50, - }, - }, - { - "body": "Prefer something else.", - "id": "4", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - ], - "username": "@complete", - }, - "body": "Could be better.", - "id": "3", - }, - ], - "shippingEstimate": 25, - "upc": "3", - "weight": 50, - }, - ], - "users": [ - { - "id": "1", - "name": "Ada Lovelace", - "reviews": [ - { - "body": "Love it!", - "id": "1", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "reviews": [ - { - "author": { - "id": "1", - "name": "Ada Lovelace", - "reviews": [ - { - "body": "Love it!", - "id": "1", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - { - "body": "Too expensive.", - "id": "2", - "product": { - "inStock": false, - "name": "Couch", - "price": 1299, - "shippingEstimate": 0, - "upc": "2", - "weight": 1000, - }, - }, - ], - "username": "@ada", - }, - "body": "Love it!", - "id": "1", - }, - { - "author": { - "id": "2", - "name": "Alan Turing", - "reviews": [ - { - "body": "Could be better.", - "id": "3", - "product": { - "inStock": true, - "name": "Chair", - "price": 54, - "shippingEstimate": 25, - "upc": "3", - "weight": 50, - }, - }, - { - "body": "Prefer something else.", - "id": "4", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - ], - "username": "@complete", - }, - "body": "Prefer something else.", - "id": "4", - }, - ], - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - { - "body": "Too expensive.", - "id": "2", - "product": { - "inStock": false, - "name": "Couch", - "price": 1299, - "reviews": [ - { - "author": { - "id": "1", - "name": "Ada Lovelace", - "reviews": [ - { - "body": "Love it!", - "id": "1", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - { - "body": "Too expensive.", - "id": "2", - "product": { - "inStock": false, - "name": "Couch", - "price": 1299, - "shippingEstimate": 0, - "upc": "2", - "weight": 1000, - }, - }, - ], - "username": "@ada", - }, - "body": "Too expensive.", - "id": "2", - }, - ], - "shippingEstimate": 0, - "upc": "2", - "weight": 1000, - }, - }, - ], - "username": "@ada", - }, - { - "id": "2", - "name": "Alan Turing", - "reviews": [ - { - "body": "Could be better.", - "id": "3", - "product": { - "inStock": true, - "name": "Chair", - "price": 54, - "reviews": [ - { - "author": { - "id": "2", - "name": "Alan Turing", - "reviews": [ - { - "body": "Could be better.", - "id": "3", - "product": { - "inStock": true, - "name": "Chair", - "price": 54, - "shippingEstimate": 25, - "upc": "3", - "weight": 50, - }, - }, - { - "body": "Prefer something else.", - "id": "4", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - ], - "username": "@complete", - }, - "body": "Could be better.", - "id": "3", - }, - ], - "shippingEstimate": 25, - "upc": "3", - "weight": 50, - }, - }, - { - "body": "Prefer something else.", - "id": "4", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "reviews": [ - { - "author": { - "id": "1", - "name": "Ada Lovelace", - "reviews": [ - { - "body": "Love it!", - "id": "1", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - { - "body": "Too expensive.", - "id": "2", - "product": { - "inStock": false, - "name": "Couch", - "price": 1299, - "shippingEstimate": 0, - "upc": "2", - "weight": 1000, - }, - }, - ], - "username": "@ada", - }, - "body": "Love it!", - "id": "1", - }, - { - "author": { - "id": "2", - "name": "Alan Turing", - "reviews": [ - { - "body": "Could be better.", - "id": "3", - "product": { - "inStock": true, - "name": "Chair", - "price": 54, - "shippingEstimate": 25, - "upc": "3", - "weight": 50, - }, - }, - { - "body": "Prefer something else.", - "id": "4", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - ], - "username": "@complete", - }, - "body": "Prefer something else.", - "id": "4", - }, - ], - "shippingEstimate": 50, - "upc": "1", - "weight": 100, - }, - }, - ], - "username": "@complete", - }, - ], -} -`; diff --git a/examples/federation-example/tests/federation-example.test.ts b/examples/federation-example/tests/federation-example.test.ts index fb9982597f63a..45bf3fe3cef3e 100644 --- a/examples/federation-example/tests/federation-example.test.ts +++ b/examples/federation-example/tests/federation-example.test.ts @@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { findAndParseConfig } from '@graphql-mesh/cli'; import { getMesh, MeshInstance } from '@graphql-mesh/runtime'; +import { accountsSubgraphServer } from '../services/accounts-subgraph/server'; import { accountsServer } from '../services/accounts/server'; import { inventoryServer } from '../services/inventory/server'; import { productsServer } from '../services/products/server'; @@ -11,37 +12,515 @@ const problematicModulePath = join(__dirname, '../../../node_modules/core-js/fea const emptyModuleContent = 'module.exports = {};'; const exampleQuery = readFileSync(join(__dirname, '../gateway/example-query.graphql'), 'utf8'); - +const expectedResult = { + topProducts: [ + { + inStock: true, + name: 'Table', + price: 899, + reviews: [ + { + author: { + id: '1', + name: 'Ada Lovelace', + reviews: [ + { + body: 'Love it!', + id: '1', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + { + body: 'Too expensive.', + id: '2', + product: { + inStock: false, + name: 'Couch', + price: 1299, + shippingEstimate: 0, + upc: '2', + weight: 1000, + }, + }, + ], + username: '@ada', + }, + body: 'Love it!', + id: '1', + }, + { + author: { + id: '2', + name: 'Alan Turing', + reviews: [ + { + body: 'Could be better.', + id: '3', + product: { + inStock: true, + name: 'Chair', + price: 54, + shippingEstimate: 25, + upc: '3', + weight: 50, + }, + }, + { + body: 'Prefer something else.', + id: '4', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + ], + username: '@complete', + }, + body: 'Prefer something else.', + id: '4', + }, + ], + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + { + inStock: false, + name: 'Couch', + price: 1299, + reviews: [ + { + author: { + id: '1', + name: 'Ada Lovelace', + reviews: [ + { + body: 'Love it!', + id: '1', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + { + body: 'Too expensive.', + id: '2', + product: { + inStock: false, + name: 'Couch', + price: 1299, + shippingEstimate: 0, + upc: '2', + weight: 1000, + }, + }, + ], + username: '@ada', + }, + body: 'Too expensive.', + id: '2', + }, + ], + shippingEstimate: 0, + upc: '2', + weight: 1000, + }, + { + inStock: true, + name: 'Chair', + price: 54, + reviews: [ + { + author: { + id: '2', + name: 'Alan Turing', + reviews: [ + { + body: 'Could be better.', + id: '3', + product: { + inStock: true, + name: 'Chair', + price: 54, + shippingEstimate: 25, + upc: '3', + weight: 50, + }, + }, + { + body: 'Prefer something else.', + id: '4', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + ], + username: '@complete', + }, + body: 'Could be better.', + id: '3', + }, + ], + shippingEstimate: 25, + upc: '3', + weight: 50, + }, + ], + users: [ + { + id: '1', + name: 'Ada Lovelace', + reviews: [ + { + body: 'Love it!', + id: '1', + product: { + inStock: true, + name: 'Table', + price: 899, + reviews: [ + { + author: { + id: '1', + name: 'Ada Lovelace', + reviews: [ + { + body: 'Love it!', + id: '1', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + { + body: 'Too expensive.', + id: '2', + product: { + inStock: false, + name: 'Couch', + price: 1299, + shippingEstimate: 0, + upc: '2', + weight: 1000, + }, + }, + ], + username: '@ada', + }, + body: 'Love it!', + id: '1', + }, + { + author: { + id: '2', + name: 'Alan Turing', + reviews: [ + { + body: 'Could be better.', + id: '3', + product: { + inStock: true, + name: 'Chair', + price: 54, + shippingEstimate: 25, + upc: '3', + weight: 50, + }, + }, + { + body: 'Prefer something else.', + id: '4', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + ], + username: '@complete', + }, + body: 'Prefer something else.', + id: '4', + }, + ], + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + { + body: 'Too expensive.', + id: '2', + product: { + inStock: false, + name: 'Couch', + price: 1299, + reviews: [ + { + author: { + id: '1', + name: 'Ada Lovelace', + reviews: [ + { + body: 'Love it!', + id: '1', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + { + body: 'Too expensive.', + id: '2', + product: { + inStock: false, + name: 'Couch', + price: 1299, + shippingEstimate: 0, + upc: '2', + weight: 1000, + }, + }, + ], + username: '@ada', + }, + body: 'Too expensive.', + id: '2', + }, + ], + shippingEstimate: 0, + upc: '2', + weight: 1000, + }, + }, + ], + username: '@ada', + }, + { + id: '2', + name: 'Alan Turing', + reviews: [ + { + body: 'Could be better.', + id: '3', + product: { + inStock: true, + name: 'Chair', + price: 54, + reviews: [ + { + author: { + id: '2', + name: 'Alan Turing', + reviews: [ + { + body: 'Could be better.', + id: '3', + product: { + inStock: true, + name: 'Chair', + price: 54, + shippingEstimate: 25, + upc: '3', + weight: 50, + }, + }, + { + body: 'Prefer something else.', + id: '4', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + ], + username: '@complete', + }, + body: 'Could be better.', + id: '3', + }, + ], + shippingEstimate: 25, + upc: '3', + weight: 50, + }, + }, + { + body: 'Prefer something else.', + id: '4', + product: { + inStock: true, + name: 'Table', + price: 899, + reviews: [ + { + author: { + id: '1', + name: 'Ada Lovelace', + reviews: [ + { + body: 'Love it!', + id: '1', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + { + body: 'Too expensive.', + id: '2', + product: { + inStock: false, + name: 'Couch', + price: 1299, + shippingEstimate: 0, + upc: '2', + weight: 1000, + }, + }, + ], + username: '@ada', + }, + body: 'Love it!', + id: '1', + }, + { + author: { + id: '2', + name: 'Alan Turing', + reviews: [ + { + body: 'Could be better.', + id: '3', + product: { + inStock: true, + name: 'Chair', + price: 54, + shippingEstimate: 25, + upc: '3', + weight: 50, + }, + }, + { + body: 'Prefer something else.', + id: '4', + product: { + inStock: true, + name: 'Table', + price: 899, + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + ], + username: '@complete', + }, + body: 'Prefer something else.', + id: '4', + }, + ], + shippingEstimate: 50, + upc: '1', + weight: 100, + }, + }, + ], + username: '@complete', + }, + ], +}; // Fix core-js issue mkdirSync(problematicModulePath, { recursive: true }); writeFileSync(join(problematicModulePath, './flat.js'), emptyModuleContent); writeFileSync(join(problematicModulePath, './flat-map.js'), emptyModuleContent); describe('Federation Example', () => { - let mesh: MeshInstance; let servicesToStop: Array<{ stop: () => Promise; }> = []; beforeAll(async () => { - const [config, ...services] = await Promise.all([ - findAndParseConfig({ - dir: join(__dirname, '../gateway'), - }), + servicesToStop = await Promise.all([ accountsServer(), + accountsSubgraphServer(), inventoryServer(), productsServer(), reviewsServer(), ]); - servicesToStop = [...services]; - mesh = await getMesh(config); }); afterAll(async () => { await Promise.all(servicesToStop.map(service => service.stop())); - mesh.destroy(); }); - it('should give correct response for example queries', async () => { - const result = await mesh.execute(exampleQuery, undefined); - expect(result?.errors).toBeFalsy(); - expect(result?.data).toMatchSnapshot(); + describe('supergraph', () => { + let mesh: MeshInstance; + beforeAll(async () => { + const config = await findAndParseConfig({ + dir: join(__dirname, '../gateway-supergraph'), + }); + mesh = await getMesh(config); + }); + it('should give correct response for example queries', async () => { + const result = await mesh.execute(exampleQuery, undefined); + expect(result?.errors).toBeFalsy(); + expect(result?.data).toMatchObject(expectedResult); + }); + afterAll(() => { + mesh.destroy(); + }); + }); + describe('regular', () => { + let mesh: MeshInstance; + beforeAll(async () => { + const config = await findAndParseConfig({ + dir: join(__dirname, '../gateway'), + }); + mesh = await getMesh(config); + }); + it('should give correct response for example queries', async () => { + const result = await mesh.execute(exampleQuery, undefined); + expect(result?.errors).toBeFalsy(); + expect(result?.data).toMatchObject(expectedResult); + }); + afterAll(() => { + mesh.destroy(); + }); }); }); diff --git a/packages/handlers/graphql/package.json b/packages/handlers/graphql/package.json index 55e89ade0f38b..165e4f962f469 100644 --- a/packages/handlers/graphql/package.json +++ b/packages/handlers/graphql/package.json @@ -43,8 +43,8 @@ "dependencies": { "@graphql-mesh/string-interpolation": "^0.5.1", "@graphql-tools/delegate": "^10.0.0", + "@graphql-tools/federation": "^1.1.0", "@graphql-tools/url-loader": "^8.0.0", - "@graphql-tools/wrap": "^10.0.0", "lodash.get": "^4.4.2" }, "publishConfig": { diff --git a/packages/handlers/graphql/src/index.ts b/packages/handlers/graphql/src/index.ts index 5543d32c70410..54f39f2469c63 100644 --- a/packages/handlers/graphql/src/index.ts +++ b/packages/handlers/graphql/src/index.ts @@ -4,12 +4,15 @@ import { buildSchema, DocumentNode, ExecutionResult, + getIntrospectionQuery, GraphQLResolveInfo, GraphQLSchema, IntrospectionQuery, Kind, + parse, print, SelectionNode, + visit, } from 'graphql'; import lodashGet from 'lodash.get'; import { process, util } from '@graphql-mesh/cross-helpers'; @@ -37,17 +40,18 @@ import { readFileOrUrl, readUrl, } from '@graphql-mesh/utils'; +import { getSubschemaForFederationWithTypeDefs, SubgraphSDLQuery } from '@graphql-tools/federation'; import { SubscriptionProtocol, UrlLoader } from '@graphql-tools/url-loader'; import { ExecutionRequest, Executor, + getDocumentNodeFromSchema, getOperationASTFromRequest, isAsyncIterable, isDocumentNode, memoize1, parseSelectionSet, } from '@graphql-tools/utils'; -import { schemaFromExecutor } from '@graphql-tools/wrap'; const getResolverData = memoize1(function getResolverData(params: ExecutionRequest) { return { @@ -144,6 +148,9 @@ export default class GraphQLHandler implements MeshHandler { private getSchemaFromContent(sdlOrIntrospection: string | IntrospectionQuery | DocumentNode) { if (typeof sdlOrIntrospection === 'string') { + if (sdlOrIntrospection.includes('@key')) { + sdlOrIntrospection = sdlOrIntrospection.replace(/extend type (\w+)/g, 'type $1 @extends'); + } return buildSchema(sdlOrIntrospection, { assumeValid: true, assumeValidSDL: true, @@ -201,14 +208,14 @@ export default class GraphQLHandler implements MeshHandler { ); return this.getSchemaFromContent(sdlOrIntrospection); } - return this.nonExecutableSchema.getWithSet(() => { + return this.nonExecutableSchema.getWithSet(async () => { const endpointFactory = getInterpolatedStringFactory(httpSourceConfig.endpoint); const executor = this.urlLoader.getExecutorAsync(httpSourceConfig.endpoint, { ...httpSourceConfig, customFetch: this.fetchFn, subscriptionsProtocol: httpSourceConfig.subscriptionsProtocol as SubscriptionProtocol, }); - return schemaFromExecutor(function meshIntrospectionExecutor(params: ExecutionRequest) { + function meshIntrospectionExecutor(params: ExecutionRequest) { const resolverData = getResolverData(params); return executor({ ...params, @@ -218,6 +225,25 @@ export default class GraphQLHandler implements MeshHandler { endpoint: endpointFactory(resolverData), }, }); + } + const introspection = (await meshIntrospectionExecutor({ + document: parse(getIntrospectionQuery()), + })) as ExecutionResult; + if (introspection.data.__schema.types.find(t => t.name === '_Service')) { + const sdl = (await meshIntrospectionExecutor({ + document: parse(SubgraphSDLQuery), + })) as ExecutionResult<{ _service: { sdl: string } }>; + const schema = buildSchema( + sdl.data._service.sdl.replace(/extend type (\w+)/g, 'type $1 @extends'), + { + assumeValid: true, + assumeValidSDL: true, + }, + ); + return schema; + } + return buildClientSchema(introspection.data, { + assumeValid: true, }); }); } @@ -308,7 +334,32 @@ export default class GraphQLHandler implements MeshHandler { }; } - async getMeshSource({ fetchFn }: GetMeshSourcePayload): Promise { + async getMeshSource(payload: GetMeshSourcePayload): Promise { + const meshSource = await this.getMeshSourceWithoutFederation(payload); + if (meshSource.schema.getDirective('key') != null) { + const typeDefs = visit(getDocumentNodeFromSchema(meshSource.schema), { + ObjectTypeDefinition(node) { + if (node.directives?.find(d => d.name.value === 'extends')) { + return { + ...node, + directives: node.directives.filter(d => d.name.value !== 'extends'), + kind: Kind.OBJECT_TYPE_EXTENSION, + }; + } + return node; + }, + }); + const extraConfig = getSubschemaForFederationWithTypeDefs(typeDefs); + return { + ...meshSource, + ...extraConfig, + batch: true, + }; + } + return meshSource; + } + + async getMeshSourceWithoutFederation({ fetchFn }: GetMeshSourcePayload): Promise { this.fetchFn = fetchFn; if ('sources' in this.config) { if (this.config.strategy === 'race') { diff --git a/packages/handlers/supergraph/package.json b/packages/handlers/supergraph/package.json new file mode 100644 index 0000000000000..9f00d154dab38 --- /dev/null +++ b/packages/handlers/supergraph/package.json @@ -0,0 +1,56 @@ +{ + "name": "@graphql-mesh/supergraph", + "version": "0.0.0", + "type": "module", + "repository": { + "type": "git", + "url": "Urigo/graphql-mesh", + "directory": "packages/handlers/supergraph" + }, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "peerDependencies": { + "@graphql-mesh/cross-helpers": "^0.4.0", + "@graphql-mesh/store": "^0.95.0", + "@graphql-mesh/types": "^0.95.0", + "@graphql-mesh/utils": "^0.95.0", + "graphql": "*", + "tslib": "^2.4.0" + }, + "dependencies": { + "@graphql-mesh/string-interpolation": "^0.5.1", + "@graphql-tools/executor-http": "^1.0.2", + "@graphql-tools/federation": "^1.1.1", + "lodash.get": "^4.4.2" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "sideEffects": false, + "typescript": { + "definition": "dist/typings/index.d.ts" + } +} diff --git a/packages/handlers/supergraph/src/index.ts b/packages/handlers/supergraph/src/index.ts new file mode 100644 index 0000000000000..0cf1fac372208 --- /dev/null +++ b/packages/handlers/supergraph/src/index.ts @@ -0,0 +1,100 @@ +import { DocumentNode, parse } from 'graphql'; +import { process } from '@graphql-mesh/cross-helpers'; +import { PredefinedProxyOptions, StoreProxy } from '@graphql-mesh/store'; +import { + getInterpolatedHeadersFactory, + stringInterpolator, +} from '@graphql-mesh/string-interpolation'; +import { + GetMeshSourcePayload, + ImportFn, + Logger, + MeshFetch, + MeshHandler, + MeshHandlerOptions, + MeshSource, + YamlConfig, +} from '@graphql-mesh/types'; +import { isUrl, readFile, readUrl } from '@graphql-mesh/utils'; +import { buildHTTPExecutor } from '@graphql-tools/executor-http'; +import { getStitchedSchemaFromSupergraphSdl } from '@graphql-tools/federation'; + +export default class SupergraphHandler implements MeshHandler { + private config: YamlConfig.Handler['supergraph']; + private baseDir: string; + private supergraphSdl: StoreProxy; + private importFn: ImportFn; + private fetchFn: MeshFetch; + private logger: Logger; + + constructor({ + config, + baseDir, + store, + importFn, + logger, + }: MeshHandlerOptions) { + this.config = config; + this.baseDir = baseDir; + this.supergraphSdl = store.proxy( + 'nonExecutableSchema', + PredefinedProxyOptions.JsonWithoutValidation, + ); + this.importFn = importFn; + this.logger = logger; + } + + private async getSupergraphSdl(): Promise { + const schemaHeadersFactory = getInterpolatedHeadersFactory(this.config.schemaHeaders); + if (isUrl(this.config.source)) { + const interpolatedSource = stringInterpolator.parse(this.config.source, { + env: process.env, + }); + const res = await readUrl(interpolatedSource, { + headers: schemaHeadersFactory({ + env: process.env, + }), + cwd: this.baseDir, + allowUnknownExtensions: true, + importFn: this.importFn, + fetch: this.fetchFn, + logger: this.logger, + }); + if (typeof res === 'string') { + return parse(res, { noLocation: true }); + } + return res; + } + return this.supergraphSdl.getWithSet(async () => { + const interpolatedSource = stringInterpolator.parse(this.config.source, { + env: process.env, + }); + const sdlOrIntrospection = await readFile(interpolatedSource, { + cwd: this.baseDir, + allowUnknownExtensions: true, + importFn: this.importFn, + fetch: this.fetchFn, + logger: this.logger, + }); + if (typeof sdlOrIntrospection === 'string') { + return parse(sdlOrIntrospection, { noLocation: true }); + } + return sdlOrIntrospection; + }); + } + + async getMeshSource({ fetchFn }: GetMeshSourcePayload): Promise { + this.fetchFn = fetchFn; + const supergraphSdl = await this.getSupergraphSdl(); + const schema = getStitchedSchemaFromSupergraphSdl({ + supergraphSdl, + onExecutor({ endpoint }) { + return buildHTTPExecutor({ endpoint, fetch: fetchFn }); + }, + batch: true, + }); + return { + schema, + }; + } +} diff --git a/packages/handlers/supergraph/yaml-config.graphql b/packages/handlers/supergraph/yaml-config.graphql new file mode 100644 index 0000000000000..972ab72e6c720 --- /dev/null +++ b/packages/handlers/supergraph/yaml-config.graphql @@ -0,0 +1,14 @@ +extend type Handler { + supergraph: SupergraphHandler +} + +type SupergraphHandler @md { + """ + A file path to your Supergraph Schema + If you provide a path to a code file(js or ts), + other options will be ignored and the schema exported from the file will be used directly. + """ + source: String! + + schemaHeaders: Any +} diff --git a/packages/mergers/stitching/package.json b/packages/mergers/stitching/package.json index ecc7c42816017..37e156811d5db 100644 --- a/packages/mergers/stitching/package.json +++ b/packages/mergers/stitching/package.json @@ -42,8 +42,7 @@ "dependencies": { "@graphql-tools/delegate": "^10.0.0", "@graphql-tools/schema": "^10.0.0", - "@graphql-tools/stitch": "^9.0.0", - "@graphql-tools/stitching-directives": "^3.0.0" + "@graphql-tools/stitch": "^9.0.0" }, "publishConfig": { "access": "public", diff --git a/packages/mergers/stitching/src/index.ts b/packages/mergers/stitching/src/index.ts index 2714910932079..23d8c7653810c 100644 --- a/packages/mergers/stitching/src/index.ts +++ b/packages/mergers/stitching/src/index.ts @@ -1,5 +1,3 @@ -import { buildSchema, ExecutionResult, GraphQLSchema, parse } from 'graphql'; -import { MeshStore, PredefinedProxyOptions } from '@graphql-mesh/store'; import { Logger, MeshMerger, @@ -7,125 +5,21 @@ import { MeshMergerOptions, RawSourceOutput, } from '@graphql-mesh/types'; -import { extractResolvers } from '@graphql-mesh/utils'; import { StitchingInfo } from '@graphql-tools/delegate'; -import { addResolversToSchema } from '@graphql-tools/schema'; import { stitchSchemas, ValidationLevel } from '@graphql-tools/stitch'; -import { - federationToStitchingSDL, - stitchingDirectives, - StitchingDirectivesResult, -} from '@graphql-tools/stitching-directives'; -import { Executor, printSchemaWithDirectives } from '@graphql-tools/utils'; - -const APOLLO_GET_SERVICE_DEFINITION_QUERY = /* GraphQL */ ` - query __ApolloGetServiceDefinition__ { - _service { - sdl - } - } -`; export default class StitchingMerger implements MeshMerger { name = 'stitching'; private logger: Logger; - private store: MeshStore; constructor(options: MeshMergerOptions) { this.logger = options.logger; - this.store = options.store; - } - - private isFederatedSchema(schema: GraphQLSchema) { - const queryType = schema.getQueryType(); - if (queryType) { - const queryFields = queryType.getFields(); - return '_service' in queryFields; - } - return false; - } - - private async replaceFederationSDLWithStitchingSDL( - name: string, - oldSchema: GraphQLSchema, - executor: Executor, - stitchingDirectives: StitchingDirectivesResult, - ) { - const rawSourceLogger = this.logger.child(name); - - rawSourceLogger.debug(`Extracting existing resolvers if available`); - const resolvers = extractResolvers(oldSchema); - - let newSchema = await this.store - .proxy(`${name}_stitching`, PredefinedProxyOptions.GraphQLSchemaWithDiffing) - .getWithSet(async () => { - this.logger.debug(`Fetching Apollo Federated Service SDL for ${name}`); - let federationSdl: string; - if ((oldSchema.extensions?.directives as any)?.link) { - federationSdl = printSchemaWithDirectives(oldSchema); - } else { - const sdlQueryResult: any = (await executor({ - document: parse(APOLLO_GET_SERVICE_DEFINITION_QUERY), - })) as ExecutionResult; - if (sdlQueryResult.errors?.length) { - throw new AggregateError( - sdlQueryResult.errors, - `Failed on fetching Federated SDL for ${name}`, - ); - } - federationSdl = sdlQueryResult.data._service.sdl; - } - this.logger.debug(`Generating Stitching SDL for ${name}`); - const stitchingSdl = federationToStitchingSDL(federationSdl, stitchingDirectives); - return buildSchema(stitchingSdl, { - assumeValid: true, - assumeValidSDL: true, - }); - }); - - rawSourceLogger.debug(`Adding existing resolvers back to the schema`); - newSchema = addResolversToSchema({ - schema: newSchema, - resolvers, - updateResolversInPlace: true, - resolverValidationOptions: { - requireResolversToMatchSchema: 'ignore', - }, - }); - - newSchema.extensions = oldSchema.extensions; - - return newSchema; } async getUnifiedSchema(context: MeshMergerContext) { const { rawSources, typeDefs, resolvers } = context; - this.logger.debug(`Stitching directives are being generated`); - const defaultStitchingDirectives = stitchingDirectives({ - pathToDirectivesInExtensions: ['directives'], - }); - this.logger.debug(`Checking if any of sources has federation metadata`); - const subschemas = await Promise.all( - rawSources.map(async rawSource => { - if (rawSource.batch == null) { - rawSource.batch = true; - } - if (this.isFederatedSchema(rawSource.schema)) { - this.logger.debug(`${rawSource.name} has federated schema.`); - rawSource.schema = await this.replaceFederationSDLWithStitchingSDL( - rawSource.name, - rawSource.schema, - rawSource.executor, - defaultStitchingDirectives, - ); - } - rawSource.merge = - defaultStitchingDirectives.stitchingDirectivesTransformer(rawSource).merge; - return rawSource; - }), - ); this.logger.debug(`Stitching the source schemas`); const unifiedSchema = stitchSchemas({ - subschemas, + subschemas: rawSources, typeDefs, resolvers, typeMergingOptions: { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 4bc67a7fde179..1b111fd2f9938 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -44,7 +44,6 @@ "@envelop/extended-validation": "^3.0.0", "@graphql-mesh/string-interpolation": "^0.5.1", "@graphql-tools/batch-delegate": "^9.0.0", - "@graphql-tools/batch-execute": "^9.0.0", "@graphql-tools/delegate": "^10.0.0", "@graphql-tools/wrap": "^10.0.0", "@whatwg-node/fetch": "^0.9.0" diff --git a/packages/runtime/src/useSubschema.ts b/packages/runtime/src/useSubschema.ts index 2490e00a516f7..9031c470853fe 100644 --- a/packages/runtime/src/useSubschema.ts +++ b/packages/runtime/src/useSubschema.ts @@ -1,7 +1,6 @@ import { BREAK, execute, FieldNode, OperationDefinitionNode, visit } from 'graphql'; import { mapAsyncIterator, Plugin, TypedExecutionArgs } from '@envelop/core'; import { applyRequestTransforms, applyResultTransforms } from '@graphql-mesh/utils'; -import { createBatchingExecutor } from '@graphql-tools/batch-execute'; import { applySchemaTransforms, createDefaultExecutor, @@ -82,9 +81,11 @@ function getExecuteFn(subschema: Subschema) { if (executor == null) { executor = createDefaultExecutor(subschema.schema); } + /* if (subschema.batch) { executor = createBatchingExecutor(executor); } + */ const transformationContext: Record = {}; const transformedRequest = applyRequestTransforms( originalRequest, diff --git a/packages/types/src/config-schema.json b/packages/types/src/config-schema.json index e5f173fb784d3..1a64552f39347 100644 --- a/packages/types/src/config-schema.json +++ b/packages/types/src/config-schema.json @@ -465,6 +465,9 @@ "$ref": "#/definitions/SoapHandler", "description": "Handler for SOAP" }, + "supergraph": { + "$ref": "#/definitions/SupergraphHandler" + }, "thrift": { "$ref": "#/definitions/ThriftHandler", "description": "Handler for OData" @@ -3080,6 +3083,33 @@ }, "required": ["source"] }, + "SupergraphHandler": { + "additionalProperties": false, + "type": "object", + "title": "SupergraphHandler", + "properties": { + "source": { + "type": "string", + "description": "A file path to your Supergraph Schema\nIf you provide a path to a code file(js or ts),\nother options will be ignored and the schema exported from the file will be used directly." + }, + "schemaHeaders": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "string" + }, + { + "type": "array", + "additionalItems": true + } + ] + } + }, + "required": ["source"] + }, "ThriftHandler": { "additionalProperties": false, "type": "object", diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 9a41fb6a89aec..3e5782e1ac4ca 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -182,6 +182,7 @@ export interface Handler { postgraphile?: PostGraphileHandler; raml?: RAMLHandler; soap?: SoapHandler; + supergraph?: SupergraphHandler; thrift?: ThriftHandler; tuql?: TuqlHandler; [k: string]: any; @@ -1003,6 +1004,15 @@ export interface SoapHandler { [k: string]: any; }; } +export interface SupergraphHandler { + /** + * A file path to your Supergraph Schema + * If you provide a path to a code file(js or ts), + * other options will be ignored and the schema exported from the file will be used directly. + */ + source: string; + schemaHeaders?: any; +} /** * Handler for OData */ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 460d38685f323..ce498bd813425 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,7 +3,7 @@ import { DocumentNode, GraphQLResolveInfo, GraphQLSchema, SelectionSetNode } fro import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue'; import type { Plugin } from '@envelop/core'; import { MeshStore } from '@graphql-mesh/store'; -import { BatchDelegateOptions } from '@graphql-tools/batch-delegate'; +import type { BatchDelegateOptions } from '@graphql-tools/batch-delegate'; import { CreateProxyingResolverFn, IDelegateToSchemaOptions, diff --git a/website/src/generated-markdown/SupergraphHandler.generated.md b/website/src/generated-markdown/SupergraphHandler.generated.md new file mode 100644 index 0000000000000..02cb454b9d579 --- /dev/null +++ b/website/src/generated-markdown/SupergraphHandler.generated.md @@ -0,0 +1,5 @@ + +* `source` (type: `String`, required) - A file path to your Supergraph Schema +If you provide a path to a code file(js or ts), +other options will be ignored and the schema exported from the file will be used directly. +* `schemaHeaders` (type: `Any`) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index c87dc75409ea7..cac21e91266c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -206,7 +206,7 @@ resolved "https://registry.yarnpkg.com/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz#5da62cf64c3b4419dabfef4536b57a40c8ff0b47" integrity sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g== -"@apollo/client@3.7.17", "@apollo/client@^3.7.0": +"@apollo/client@3.7.17", "@apollo/client@^3.7.0", "@apollo/client@~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0": version "3.7.17" resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.7.17.tgz#1d2538729fd8ef138aa301a7cf62704474e57b72" integrity sha512-0EErSHEtKPNl5wgWikHJbKFAzJ/k11O0WO2QyqZSHpdxdAnw7UWHY4YiLbHCFG7lhrD+NTQ3Z/H9Jn4rcikoJA== @@ -3250,7 +3250,7 @@ tslib "^2.4.0" ws "^8.13.0" -"@graphql-tools/executor-http@^1.0.0": +"@graphql-tools/executor-http@^1.0.0", "@graphql-tools/executor-http@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@graphql-tools/executor-http/-/executor-http-1.0.2.tgz#d7964a6e5ec883842f9a8e3f104f93c9b8f472be" integrity sha512-JKTB4E3kdQM2/1NEcyrVPyQ8057ZVthCV5dFJiKktqY9IdmF00M8gupFcW3jlbM/Udn78ickeUBsUzA3EouqpA== @@ -3285,6 +3285,40 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/federation@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/federation/-/federation-1.1.0.tgz#047547fcd7d28a5afdd802c47b5fd63828fc6719" + integrity sha512-+22aWiPqAZfdssJXcDe1RaUtPYtILU8+8wNOrsSQXXp/o0dZyAfeqXLs8CP2pyg5zaXkX25x2cnciqqR0QOi2w== + dependencies: + "@graphql-tools/delegate" "^10.0.0" + "@graphql-tools/executor-http" "^1.0.0" + "@graphql-tools/merge" "^9.0.0" + "@graphql-tools/schema" "^10.0.0" + "@graphql-tools/stitch" "^9.0.0" + "@graphql-tools/utils" "^10.0.0" + "@graphql-tools/wrap" "^10.0.0" + tslib "^2.4.0" + value-or-promise "^1.0.12" + optionalDependencies: + "@apollo/client" "~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0" + +"@graphql-tools/federation@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@graphql-tools/federation/-/federation-1.1.1.tgz#a8bc35ed341543b5dd48220602138cf238ded0d3" + integrity sha512-Yr0f8kfMoy4vZYRTB7aAFcUx/irEGxLac+YYWY8hejwExfUpjllFYr81Jt2mmJSNZ6nJtJW9fzLmxGOApBWDGA== + dependencies: + "@graphql-tools/delegate" "^10.0.0" + "@graphql-tools/executor-http" "^1.0.0" + "@graphql-tools/merge" "^9.0.0" + "@graphql-tools/schema" "^10.0.0" + "@graphql-tools/stitch" "^9.0.0" + "@graphql-tools/utils" "^10.0.0" + "@graphql-tools/wrap" "^10.0.0" + tslib "^2.4.0" + value-or-promise "^1.0.12" + optionalDependencies: + "@apollo/client" "~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0" + "@graphql-tools/graphql-file-loader@6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-6.1.0.tgz#8b0d8dc7ffd0417c68ad97a916a0ea1841d55d36" @@ -21800,7 +21834,6 @@ typical@^5.2.0: uWebSockets.js@uNetworking/uWebSockets.js#v20.30.0: version "20.30.0" - uid d39d4181daf5b670d44cbc1b18f8c28c85fd4142 resolved "https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/d39d4181daf5b670d44cbc1b18f8c28c85fd4142" ua-parser-js@^1.0.35: