diff --git a/.changeset/many-stingrays-attend.md b/.changeset/many-stingrays-attend.md new file mode 100644 index 00000000000..ca343338abe --- /dev/null +++ b/.changeset/many-stingrays-attend.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/stitching-directives-http': patch +--- + +Helper package to create a basic Stitching gateway based on directives via HTTP diff --git a/packages/stitching-directives-http/package.json b/packages/stitching-directives-http/package.json new file mode 100644 index 00000000000..3a29314fe66 --- /dev/null +++ b/packages/stitching-directives-http/package.json @@ -0,0 +1,72 @@ +{ + "name": "@graphql-tools/stitching-directives-http", + "version": "0.0.0", + "description": "A set of utils for faster development of GraphQL tools", + "repository": { + "type": "git", + "url": "ardatan/graphql-tools", + "directory": "packages/stitching-directives-http" + }, + "license": "MIT", + "sideEffects": false, + "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" + } + }, + "./*": { + "require": { + "types": "./dist/typings/*.d.cts", + "default": "./dist/cjs/*.js" + }, + "import": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + }, + "default": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "dependencies": { + "@graphql-tools/stitching-directives": "^2.3.31", + "@graphql-tools/executor-http": "^0.1.9", + "@graphql-tools/delegate": "^9.0.28", + "@graphql-tools/stitch": "^8.7.43", + "@graphql-tools/wrap": "^9.3.8", + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0" + }, + "devDependencies": { + "@graphql-tools/schema": "9.0.16" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "type": "module" +} diff --git a/packages/stitching-directives-http/src/index.ts b/packages/stitching-directives-http/src/index.ts new file mode 100644 index 00000000000..a04d900e79a --- /dev/null +++ b/packages/stitching-directives-http/src/index.ts @@ -0,0 +1,86 @@ +import { SubschemaConfig } from '@graphql-tools/delegate'; +import { buildHTTPExecutor, HTTPExecutorOptions } from '@graphql-tools/executor-http'; +import { stitchSchemas } from '@graphql-tools/stitch'; +import { stitchingDirectives, StitchingDirectivesOptions } from '@graphql-tools/stitching-directives'; +import { ExecutionResult, isAsyncIterable } from '@graphql-tools/utils'; +import { buildASTSchema, buildSchema, DocumentNode, GraphQLSchema, parse } from 'graphql'; + +export type StitchingDirectivesHTTPService = ( + | { + sdl: string; + url: string; + } + | { + schema: GraphQLSchema; + url: string; + } + | { + ast: DocumentNode; + url: string; + } + | { + sdlQuery: string | DocumentNode; + getSdlFromResult: (result: ExecutionResult) => string; + url: string; + } +) & + Omit & + Omit; + +async function getSubschemaConfig(service: StitchingDirectivesHTTPService): Promise { + const executor = buildHTTPExecutor({ + endpoint: service.url, + ...service, + }); + let schema: GraphQLSchema; + if ('sdl' in service) { + schema = buildSchema(service.sdl, { + assumeValidSDL: true, + assumeValid: true, + }); + } else if ('schema' in service) { + schema = service.schema; + } else if ('ast' in service) { + schema = buildASTSchema(service.ast, { + assumeValidSDL: true, + assumeValid: true, + }); + } else { + const sdlQueryResult = await executor({ + document: typeof service.sdlQuery === 'string' ? parse(service.sdlQuery) : service.sdlQuery, + }); + if (isAsyncIterable(sdlQueryResult)) { + throw new Error('sdlQuery must return a single result'); + } + const sdl = service.getSdlFromResult(sdlQueryResult); + schema = buildSchema(sdl, { + assumeValidSDL: true, + assumeValid: true, + }); + } + return { + schema, + executor, + }; +} + +export async function createStitchingDirectivesHTTPGateway( + services: StitchingDirectivesHTTPService[], + opts?: StitchingDirectivesOptions & { + sdlQuery?: string | DocumentNode; + } +): Promise { + const { stitchingDirectivesTransformer } = stitchingDirectives(opts); + const subschemas = await Promise.all( + services.map(service => + getSubschemaConfig({ + sdlQuery: opts?.sdlQuery, + ...service, + }) + ) + ); + return stitchSchemas({ + subschemas, + subschemaConfigTransforms: [stitchingDirectivesTransformer], + }); +} diff --git a/packages/stitching-directives-http/tests/stitching-directives-http.spec.ts b/packages/stitching-directives-http/tests/stitching-directives-http.spec.ts new file mode 100644 index 00000000000..28d23a269ba --- /dev/null +++ b/packages/stitching-directives-http/tests/stitching-directives-http.spec.ts @@ -0,0 +1,182 @@ +import { createYoga, createSchema } from 'graphql-yoga'; +import { stitchingDirectives } from '@graphql-tools/stitching-directives'; +import { GraphQLSchema, parse } from 'graphql'; +import { createStitchingDirectivesHTTPGateway } from '@graphql-tools/stitching-directives-http'; +import { normalizedExecutor } from '@graphql-tools/executor'; +import { printSchemaWithDirectives } from '@graphql-tools/utils'; + +describe('stitching-directives-http', () => { + const { stitchingDirectivesTypeDefs } = stitchingDirectives(); + + const books = [ + { + id: '1', + title: 'Harry Potter and the Chamber of Secrets', + }, + { + id: '2', + title: 'Jurassic Park', + }, + { + id: '3', + title: 'The Hobbit', + }, + ]; + + const authors = [ + { + id: '1', + name: 'J.K. Rowling', + }, + { + id: '2', + name: 'Michael Crichton', + }, + { + id: '3', + name: 'J.R.R. Tolkien', + }, + ]; + + const booksWithAuthors = [ + { + id: '1', + author: { + id: '1', + }, + }, + { + id: '2', + author: { + id: '2', + }, + }, + { + id: '3', + author: { + id: '3', + }, + }, + ]; + + const bookSchema = createSchema({ + typeDefs: /* GraphQL */ ` + ${stitchingDirectivesTypeDefs} + type Query { + book(id: ID!): Book! @merge(keyField: "id") @canonical + } + type Book { + id: ID! + title: String! + } + `, + resolvers: { + Query: { + book: (parent, args) => books.find(book => book.id === args.id), + }, + }, + }); + + const authorSchema = createSchema({ + typeDefs: /* GraphQL */ ` + ${stitchingDirectivesTypeDefs} + type Query { + author(id: ID!): Author! @merge(keyField: "id") @canonical + } + type Author { + id: ID! + name: String! + } + `, + resolvers: { + Query: { + author: (parent, args) => authors.find(author => author.id === args.id), + }, + }, + }); + + const bookWithAuthor = createSchema({ + typeDefs: /* GraphQL */ ` + ${stitchingDirectivesTypeDefs} + type Query { + book(id: ID!): Book! @merge(keyField: "id") + } + type Book { + id: ID! + author: Author! + } + type Author { + id: ID! + } + `, + resolvers: { + Query: { + book: (parent, args) => booksWithAuthors.find(bookWithAuthor => bookWithAuthor.id === args.id), + }, + }, + }); + + const bookServer = createYoga({ + schema: bookSchema, + }); + + const authorServer = createYoga({ + schema: authorSchema, + }); + + const bookWithAuthorServer = createYoga({ + schema: bookWithAuthor, + }); + + let gateway: GraphQLSchema; + beforeAll(async () => { + gateway = await createStitchingDirectivesHTTPGateway([ + { + url: 'http://localhost:4001/graphql', + sdl: printSchemaWithDirectives(bookSchema), + fetch: bookServer.fetch, + }, + { + url: 'http://localhost:4002/graphql', + sdl: printSchemaWithDirectives(authorSchema), + fetch: authorServer.fetch, + }, + { + url: 'http://localhost:4003/graphql', + sdl: printSchemaWithDirectives(bookWithAuthor), + fetch: bookWithAuthorServer.fetch, + }, + ]); + }); + it('should work', async () => { + const result = await normalizedExecutor({ + schema: gateway, + document: parse(/* GraphQL */ ` + query { + book(id: "1") { + id + title + author { + id + name + } + } + } + `), + }); + expect(result).toMatchInlineSnapshot(` + { + "data": { + "book": { + "author": { + "id": "1", + "name": "J.K. Rowling", + }, + "id": "1", + "title": "Harry Potter and the Chamber of Secrets", + }, + }, + } + `); + }); +}); diff --git a/yarn.lock b/yarn.lock index 57eb4266b56..6280d2362d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1788,6 +1788,81 @@ dependencies: giscus "^1.2.6" +"@graphql-tools/batch-delegate@8.4.21": + version "8.4.21" + resolved "https://registry.yarnpkg.com/@graphql-tools/batch-delegate/-/batch-delegate-8.4.21.tgz#765476c7b09ef4699beb07af228f2eeb1a78204b" + integrity sha512-NrnMGF6SHv7b0OWSyPUURZDoPGKEFTmTyYwVQ+iM950ZPBx3gOUPODZaXWpFVlFK2UGVNk6atvbigPDHnwSZnw== + dependencies: + "@graphql-tools/delegate" "9.0.28" + "@graphql-tools/utils" "9.2.1" + dataloader "2.2.2" + tslib "^2.4.0" + +"@graphql-tools/delegate@9.0.28", "@graphql-tools/delegate@^9.0.28": + version "9.0.28" + resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-9.0.28.tgz#026275094b2ff3f4cbbe99caff2d48775aeb67d6" + integrity sha512-8j23JCs2mgXqnp+5K0v4J3QBQU/5sXd9miaLvMfRf/6963DznOXTECyS9Gcvj1VEeR5CXIw6+aX/BvRDKDdN1g== + dependencies: + "@graphql-tools/batch-execute" "^8.5.18" + "@graphql-tools/executor" "^0.0.15" + "@graphql-tools/schema" "^9.0.16" + "@graphql-tools/utils" "^9.2.1" + dataloader "^2.2.2" + tslib "^2.5.0" + value-or-promise "^1.0.12" + +"@graphql-tools/executor@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-0.0.15.tgz#cbd29af2ec54213a52f6c516a7792b3e626a4c49" + integrity sha512-6U7QLZT8cEUxAMXDP4xXVplLi6RBwx7ih7TevlBto66A/qFp3PDb6o/VFo07yBKozr8PGMZ4jMfEWBGxmbGdxA== + dependencies: + "@graphql-tools/utils" "9.2.1" + "@graphql-typed-document-node/core" "3.1.2" + "@repeaterjs/repeater" "3.0.4" + tslib "^2.4.0" + value-or-promise "1.0.12" + +"@graphql-tools/merge@8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.4.0.tgz#47fbe5c4b6764276dc35bd19c4e7d3c46d3dc0fc" + integrity sha512-3XYCWe0d3I4F1azNj1CdShlbHfTIfiDgj00R9uvFH8tHKh7i1IWN3F7QQYovcHKhayaR6zPok3YYMESYQcBoaA== + dependencies: + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" + +"@graphql-tools/schema@9.0.17": + version "9.0.17" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-9.0.17.tgz#d731e9899465f88d5b9bf69e607ec465bb88b062" + integrity sha512-HVLq0ecbkuXhJlpZ50IHP5nlISqH2GbNgjBJhhRzHeXhfwlUOT4ISXGquWTmuq61K0xSaO0aCjMpxe4QYbKTng== + dependencies: + "@graphql-tools/merge" "8.4.0" + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" + value-or-promise "1.0.12" + +"@graphql-tools/stitch@^8.7.43": + version "8.7.43" + resolved "https://registry.yarnpkg.com/@graphql-tools/stitch/-/stitch-8.7.43.tgz#f779c0fc3b9c6883bb6337be85090cff972ce253" + integrity sha512-XfZEtGU/zKUuAvVdRl+ZyZuZra67wwg0BqDvYJJ4gzhm9X2JcV8jALEqrTMVuFn4xK5oxcTe1uLlaLmhxQXHWQ== + dependencies: + "@graphql-tools/batch-delegate" "8.4.21" + "@graphql-tools/delegate" "9.0.28" + "@graphql-tools/merge" "8.4.0" + "@graphql-tools/schema" "9.0.17" + "@graphql-tools/utils" "9.2.1" + "@graphql-tools/wrap" "9.3.8" + tslib "^2.4.0" + value-or-promise "^1.0.11" + +"@graphql-tools/stitching-directives@^2.3.31": + version "2.3.31" + resolved "https://registry.yarnpkg.com/@graphql-tools/stitching-directives/-/stitching-directives-2.3.31.tgz#299bcdfdbd5f1a808ded0d7dd4e9f9a217448b01" + integrity sha512-XX81hqZy4IHB2OwrG1escEA5yT5ZBxUYoHaxohAa8gaYUij5Xc6l4qzZ8HT05T1x1mNhFFNILEgQaJA1EDGv/A== + dependencies: + "@graphql-tools/delegate" "9.0.28" + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" + "@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.8.0": version "8.13.1" resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.13.1.tgz#b247607e400365c2cd87ff54654d4ad25a7ac491" @@ -1795,6 +1870,17 @@ dependencies: tslib "^2.4.0" +"@graphql-tools/wrap@9.3.8", "@graphql-tools/wrap@^9.3.8": + version "9.3.8" + resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-9.3.8.tgz#c6f53b7bc98cf3fa3d91e41be3b99254ae99b409" + integrity sha512-MGsExYPiILMw4Qff7HcvE9MMSYdjb/tr5IQYJbxJIU4/TrBHox1/smne8HG+Bd7kmDlTTj7nU/Z8sxmoRd0hOQ== + dependencies: + "@graphql-tools/delegate" "9.0.28" + "@graphql-tools/schema" "9.0.17" + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" + value-or-promise "1.0.12" + "@graphql-typed-document-node/core@3.1.2", "@graphql-typed-document-node/core@^3.1.1": version "3.1.2" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04"