From 08e205813fd8480554381a793ae6e947cf654cb8 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 15 Feb 2018 13:52:03 +0200 Subject: [PATCH] Next api Squashed commit of the following: commit a108877910e0556759f92e9acab4643c63be13ff Author: Mikhail Novikov Date: Wed Jan 31 16:05:00 2018 +0200 First transforms commit 334802c1554cee7e6065fdd5108d911379b140ac Merge: 99884b6 e9c0eb5 Author: Mikhail Novikov Date: Fri Jan 26 12:27:57 2018 +0200 Merge remote-tracking branch 'origin/master' into next-api commit 99884b6bad653af6aef49992a4bf73caf9cb9c24 Author: Mikhail Novikov Date: Fri Jan 26 11:57:15 2018 +0200 Restored valid fragments commit f1b24329bb86588f1a39fb4668adc6724f01ceaa Author: Mikhail Novikov Date: Thu Jan 25 14:55:57 2018 +0200 Progress :P commit db2806aae595b06562c876c66aeca0acac1cfeb0 Author: Mikhail Novikov Date: Thu Jan 25 13:35:35 2018 +0200 Fragment replacement commit e9c0eb56c41dffc5e04644c2650cd0080ed77aa4 Author: Mikhail Novikov Date: Wed Jan 24 18:28:12 2018 +0200 v2.19.0 (#594) commit e33c2152992b1c22a3f0b09895e4118aace1c83e Author: Mikhail Novikov Date: Wed Jan 24 16:31:37 2018 +0200 Progress commit 464670f9ee011d543c845b721dbd702ace4e9005 Author: Mikhail Novikov Date: Tue Jan 23 15:03:40 2018 +0200 More progress commit 22d2295d037e32b81bdc2937ab40f97539adca74 Author: Sebastian Richter Date: Tue Jan 23 12:45:49 2018 +0100 Also recreate astNode for fields (#580) * Also recreate astNode for fields In a [previous commit](https://github.com/apollographql/graphql-tools/commit/fd9f6260faa779b2bfc12f1f707cdf2b778d306b) we added the `astNode` property in the `reacreateCompositeType` function. That resulted in cache control working with schema stitching but only for GraphQL Types. By recreating the `astNode` prop also in `fieldToFieldConfig` cache control also works for fields. This is required for caching fields and hence queries. * Add ast to input field node too commit 03ad432387774232d794729f64db27010701337d Author: Renato Benkendorf Date: Tue Jan 23 08:35:00 2018 -0200 Fix resolvers to accept and move forward args with zero or false values (#586) Fixing the args with zero value or false commit 72ac16ffd7b75d1f654f29bf89263093760a7b19 Author: Mikhail Novikov Date: Tue Jan 23 11:37:02 2018 +0200 Upgrade remap-istanbul (#590) commit 54205575701ff2cbba38fa9dfb4f01e68d18e4fb Merge: 24a0f3f 3ef557a Author: Sashko Stubailo Date: Mon Jan 22 11:55:07 2018 -0800 Merge pull request #584 from shackpank/double_deps Remove graphql-subscriptions from devDependencies commit 3ef557acbbe7cdde8afda54261d9439b22b51397 Author: Ollie Buck Date: Fri Jan 19 14:40:03 2018 +0000 Remove graphql-subscriptions from devDependencies commit 7e4efe99d51b79094ed5606faf28a3b69196a27a Author: Mikhail Novikov Date: Thu Jan 18 16:38:13 2018 +0200 Progress commit 24a0f3f4a611666cbfecdbb015ffbe35b5145826 Author: Mikhail Novikov Date: Wed Jan 10 12:36:43 2018 +0200 v2.18.0 (#574) commit 966e102c35d728b1b9782237f4abb03e4fd69ec7 Author: Amit-A Date: Wed Jan 10 02:53:56 2018 -0500 Fixing broken links (#573) commit 94b8f68e419acf00119d115d947d270a8e3eb4f6 Author: Pascal Kaufmann Date: Tue Jan 9 12:51:53 2018 +0100 Fix fragment filtering edge case when a type implements multiple interfaces (#546) * add failing test for multi interface issue * interface / interface always implements an abstract type * changelog * the linter always should be happy * Update CHANGELOG.md commit 608414b0dfd2ff7ba8a27389e0bb8c400fb631c6 Author: Tim Griesser Date: Tue Jan 9 06:15:12 2018 -0500 Allow IEnumResolver values to be number type (#568) * Allow IEnumResolver values to be number type commit 6177034624e21f4d240def6fbb1e984f7ba04814 Author: Mikhail Novikov Date: Tue Jan 9 11:26:04 2018 +0200 v2.17.0 (#571) commit fd9f6260faa779b2bfc12f1f707cdf2b778d306b Author: Sebastian Richter Date: Tue Jan 9 09:25:16 2018 +0100 Include astNode in schema recreation (#569) * Update schemaRecreation.ts `mergeSchemas` does not set `astNode` properties, when recreating types. But `CacheControlExtension.willResolveField` uses the `astNode` property in order to get the `cacheControl` directives. commit 4f489d3e447a89445a2e6ffcd7fbcd599826f1b5 Author: Mikhail Novikov Date: Wed Jan 3 15:40:21 2018 +0200 v2.16.0 (#564) commit c4c5d91655587b13dd3579c1446e42b5e72132a7 Author: VimalRaj Selvam Date: Wed Jan 3 19:00:16 2018 +0530 #544 - Update docs and tests to reflect new strings-as-descriptions pattern in new GraphQL SDL (#559) * #64 - Update docs and tests to reflect new strings-as-descriptions pattern in new GraphQL SDL * Make tests compatible with graphql v0.11 * Fix test errors * Remove extra space * Fix review comments commit 66d58bb5e48a9b7b75a08e94594224494eea7f7b Author: Alvis Tang Date: Wed Jan 3 13:25:59 2018 +0000 fix: correct the dependency issue in typescript caused by #445 (#561) fix #472 commit 342beceff6fc51e5d01d08a1cdfbc634d207e675 Author: Johannes Schickling Date: Wed Jan 3 14:21:35 2018 +0100 Add subscriptions support to `makeRemoteExectuableSchema` (#563) Added subscriptions support to makeRemoteExectuableSchema commit 8e8e34831e7e1bb0835f82d1da7300522f6e65e4 Author: Mikhail Novikov Date: Tue Jan 2 14:50:06 2018 +0200 v2.15.0 (#562) commit f2905ac407a4125aafcb988012d5ba7160bb6b61 Author: Johannes Schickling Date: Tue Jan 2 13:40:56 2018 +0100 Add document validation to `delegateToSchema` (#551) * Add document validation to `delegateToSchema` commit 59592e14b9b2883c50c6adc6b6c8d1c3030bda42 Merge: 3e24c69 3612216 Author: Sashko Stubailo Date: Thu Dec 28 10:02:28 2017 -0800 Merge pull request #556 from enaqx/patch-4 doc: fix Generating schema URL on Mocking page commit 36122163a2d368b0bcadb8b3690bd40841916d06 Author: Sashko Stubailo Date: Thu Dec 28 10:00:16 2017 -0800 Update mocking.md commit 3e24c69d26fd21d5f8919947adf5b5c51bfbf928 Merge: 7089bbf 42d180d Author: Sashko Stubailo Date: Thu Dec 28 09:58:31 2017 -0800 Merge pull request #557 from tbroadley/fix-typos docs: fix typos commit 42d180d38f03ecf6ee28c436aee6b0cbaefb46ca Merge: 5130cf6 7089bbf Author: Sashko Stubailo Date: Thu Dec 28 09:57:11 2017 -0800 Merge branch 'master' into fix-typos commit 7089bbf0f45ce45a4eefc967f39ccaec787552c4 Merge: 3a58286 5048434 Author: Sashko Stubailo Date: Thu Dec 28 09:52:44 2017 -0800 Merge pull request #552 from mklopets/patch-2 Fix link to Apollo Link docs commit 5048434c9ba4a85a2802867359e0a713baca08b3 Author: Marko Klopets Date: Thu Dec 28 14:23:07 2017 +0200 Re-fix link commit 5130cf6ef0eca1f62687ee10a363b0c1252c9469 Author: Thomas Broadley Date: Mon Dec 25 17:45:44 2017 -0500 docs: fix typos commit 80f2f4cd2c98e2c7da585e7ac874be1b72e62a88 Author: Nick Raienko Date: Mon Dec 25 09:19:03 2017 +0200 doc: fix Generating schema URL on Mocking page commit e295019558e9558e3d2284d699ee5ee803b92ea5 Author: Marko Klopets Date: Thu Dec 21 13:18:59 2017 +0200 Fix link to Apollo Link docs commit 3a582868d02fa38b7dde01b84d99677ca84a57da Merge: 259f22e 971e96e Author: Sashko Stubailo Date: Wed Dec 20 11:26:55 2017 -0800 Merge pull request #548 from apollographql/create-2.14.1 v2.14.1 commit 971e96e0aa088bcdbfba930b73230fe7a87f1b77 Author: Mikhail Novikov Date: Wed Dec 20 12:44:14 2017 +0200 v2.14.1 commit 259f22edce2b5c519a7ae4f07ca82645fcba0ccc Author: Mikhail Novikov Date: Wed Dec 20 12:42:16 2017 +0200 Guard against schemas without QueryType (#547) commit 1e3bb14360e037ff05b8767565ce1c4f8bb99628 Author: Mikhail Novikov Date: Tue Dec 19 14:06:13 2017 +0200 Check null resolvers commit 2e2c9b1fc06abc4da3f02c42b8012c6af0be94ac Author: Mikhail Novikov Date: Mon Dec 11 16:09:06 2017 +0200 Simplify API --- .npmignore | 1 + .vscode/settings.json | 1 + CHANGELOG.md | 30 +- README.md | 5 +- designs/connectors.md | 4 +- docs/source/generate-schema.md | 23 +- docs/source/mocking.md | 2 +- docs/source/remote-schemas.md | 2 +- docs/source/resolvers.md | 6 +- docs/source/scalars.md | 14 +- docs/source/schema-stitching.md | 4 +- package.json | 12 +- src/Interfaces.ts | 37 +- src/index.ts | 1 + src/stitching/TypeRegistry.ts | 133 ---- src/stitching/delegateToSchema.ts | 626 +++---------------- src/stitching/linkToFetcher.ts | 2 +- src/stitching/makeRemoteExecutableSchema.ts | 83 ++- src/stitching/mergeSchemas.ts | 504 ++++++++------- src/stitching/resolvers.ts | 103 +++ src/stitching/schemaRecreation.ts | 173 ++++- src/stitching/typeFromAST.ts | 76 ++- src/test/testMakeRemoteExecutableSchema.ts | 48 ++ src/test/testMergeSchemas.ts | 457 +++++++++++--- src/test/testSchemaGenerator.ts | 45 +- src/test/testTransforms.ts | 129 ++++ src/test/testingSchemas.ts | 151 ++++- src/test/tests.ts | 2 + src/transforms/AddArgumentsAsVariables.ts | 196 ++++++ src/transforms/AddTypenameToAbstract.ts | 75 +++ src/transforms/CheckResultAndHandleErrors.ts | 14 + src/transforms/FilterToSchema.ts | 304 +++++++++ src/transforms/ReplaceFieldWithFragment.ts | 78 +++ src/transforms/index.ts | 16 + src/transforms/makeSimpleTransformSchema.ts | 23 + src/transforms/transforms.ts | 44 ++ src/transforms/visitSchema.ts | 142 +++++ 37 files changed, 2476 insertions(+), 1090 deletions(-) delete mode 100644 src/stitching/TypeRegistry.ts create mode 100644 src/stitching/resolvers.ts create mode 100644 src/test/testMakeRemoteExecutableSchema.ts create mode 100644 src/test/testTransforms.ts create mode 100644 src/transforms/AddArgumentsAsVariables.ts create mode 100644 src/transforms/AddTypenameToAbstract.ts create mode 100644 src/transforms/CheckResultAndHandleErrors.ts create mode 100644 src/transforms/FilterToSchema.ts create mode 100644 src/transforms/ReplaceFieldWithFragment.ts create mode 100644 src/transforms/index.ts create mode 100644 src/transforms/makeSimpleTransformSchema.ts create mode 100644 src/transforms/transforms.ts create mode 100644 src/transforms/visitSchema.ts diff --git a/.npmignore b/.npmignore index f27a6939860..927190ca980 100644 --- a/.npmignore +++ b/.npmignore @@ -2,6 +2,7 @@ !dist/ !dist/* !dist/stitching/* +!dist/transforms/* !package.json !*.md !*.png diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d988b1ebc0..e93660f7dbd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "editor.insertSpaces": true, "editor.rulers": [110], "editor.wordWrapColumn": 110, + "prettier.semi": true, "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "prettier.singleQuote": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 9259e0bf95e..fda35626b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,33 @@ ### vNEXT -* ... +### v2.19.0 + +* Also recreate `astNode` property for fields, not only types, when recreating schemas. [PR #580](https://github.com/apollographql/graphql-tools/pull/580) +* Fix `delegateToSchema.js` to accept and move forward args with zero or false values [PR #586](https://github.com/apollographql/graphql-tools/pull/586) + +### v2.18.0 + +* Fix a bug where inline fragments got filtered in merged schemas when a type implemented multiple interfaces [PR #546](https://github.com/apollographql/graphql-tools/pull/546) +* IEnumResolver value can be a `number` type [PR #568](https://github.com/apollographql/graphql-tools/pull/568) + +### v2.17.0 + +* Include `astNode` property in schema recreation [PR #569](https://github.com/apollographql/graphql-tools/pull/569) + +### v2.16.0 + +* Added GraphQL Subscriptions support for schema stitching and `makeRemoteExecutableSchema` [PR #563](https://github.com/apollographql/graphql-tools/pull/563) +* Make `apollo-link` a direct dependency [PR #561](https://github.com/apollographql/graphql-tools/pull/561) +* Update tests to use `graphql-js@0.12` docstring format [PR #559](https://github.com/apollographql/graphql-tools/pull/559) + +### v2.15.0 + +* Validate query before delegation [PR #551](https://github.com/apollographql/graphql-tools/pull/551) + +### v2.14.1 + +* Add guard against invalid schemas being constructed from AST [PR #547](https://github.com/apollographql/graphql-tools/pull/547) ### v2.14.0 @@ -177,7 +203,7 @@ Update to add support for `graphql@0.12`, and drop versions before `0.11` from t * Removed testing on Node 5 ([@DxCx](https://github.com/DxCx) in [#129](https://github.com/apollostack/graphql-tools/pull/129)) -* Changed GraphQL typings requirment from peer to standard ([@DxCx](https://github.com/DxCx) in [#129](https://github.com/apollostack/graphql-tools/pull/129)) +* Changed GraphQL typings requirement from peer to standard ([@DxCx](https://github.com/DxCx) in [#129](https://github.com/apollostack/graphql-tools/pull/129)) * Change the missing resolve function validator to show a warning instead of an error ([@nicolaslopezj](https://github.com/nicolaslopezj) in [#134](https://github.com/apollostack/graphql-tools/pull/134)) diff --git a/README.md b/README.md index bffa4142608..e58ee4ae577 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ type Author { id: ID! # the ! means that every author object _must_ have an id firstName: String lastName: String - posts: [Post] # the list of Posts by this author + """ + the list of Posts by this author + """ + posts: [Post] } type Post { diff --git a/designs/connectors.md b/designs/connectors.md index 7a075675452..c1077b16873 100644 --- a/designs/connectors.md +++ b/designs/connectors.md @@ -37,7 +37,7 @@ In this example, an author has multiple posts, and each post has one author. Here's an illustration for how connectors and models would look like for this example if Authors and Posts were stored in MySQL, but view counts in MongoDB: -![Connectors are database-specfic, models are application-specific](connector-model-diagram.png) +![Connectors are database-specific, models are application-specific](connector-model-diagram.png) The Posts model connects to both SQL and MongoDB. Title, text and authorId come from SQL, the view count comes from MongoDB. @@ -60,7 +60,7 @@ Both batching and caching are more important in GraphQL than in traditional endp Models are the glue between connectors - which are backend-specific - and GraphQL types - which are app-specific. They are very similar to models in ORMs, such as Rails' Active Record. -Let's say for example that you have two types, Author and Post, which are both stored in MySQL. Rather than calling the MySQL connector directly from your resolve funcitons, you should create models for Author and Post, which use the MongoDB connector. This additional level of abstraction helps separate the data fetching logic from the GraphQL schema, which makes reusing and refactoring it easier. +Let's say for example that you have two types, Author and Post, which are both stored in MySQL. Rather than calling the MySQL connector directly from your resolve functions, you should create models for Author and Post, which use the MongoDB connector. This additional level of abstraction helps separate the data fetching logic from the GraphQL schema, which makes reusing and refactoring it easier. In the example schema above, the Authors model would have the following methods: ``` diff --git a/docs/source/generate-schema.md b/docs/source/generate-schema.md index 930ee522d70..a39f65b5fca 100644 --- a/docs/source/generate-schema.md +++ b/docs/source/generate-schema.md @@ -17,7 +17,10 @@ const typeDefs = ` id: Int! firstName: String lastName: String - posts: [Post] # the list of Posts by this author + """ + the list of Posts by this author + """ + posts: [Post] } type Post { @@ -279,19 +282,29 @@ const typeDefs = [`

Descriptions & Deprecations

GraphiQL has built-in support for displaying docstrings with markdown syntax. You can easily add docstrings to types, fields and arguments like below: + ``` -# Description for the type +""" +Description for the type +""" type MyObjectType { - # Description for field + """ + Description for field + Supports multi-line description + """ myField: String! otherField( - # Description for argument + """ + Description for argument + """ arg: Int ) oldField( - # Description for argument + """ + Description for argument + """ arg: Int ) @deprecated(reason: "Use otherField instead.") } diff --git a/docs/source/mocking.md b/docs/source/mocking.md index 1fdb0fadc8d..4d77317a731 100644 --- a/docs/source/mocking.md +++ b/docs/source/mocking.md @@ -13,7 +13,7 @@ Let's take a look at how we can mock a GraphQL schema with just one line of code [See a complete runnable example on Launchpad.](https://launchpad.graphql.com/98lq7vz8r) -To start, let's grab the schema definition string from the `makeExecutableSchema` example [in the "Generating a schema" article](/tools/graphql-tools/generate-schema.html#example). +To start, let's grab the schema definition string from the `makeExecutableSchema` example [in the "Generating a schema" article](./generate-schema.html#example). ```js import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; diff --git a/docs/source/remote-schemas.md b/docs/source/remote-schemas.md index 220d60b57f4..dfede5be250 100644 --- a/docs/source/remote-schemas.md +++ b/docs/source/remote-schemas.md @@ -39,7 +39,7 @@ A link is a function capable of retrieving GraphQL results. It is the same way t Link API -Since graphql-tools supports using a link for the network layer, the API is the same as you would write on the client. To learn more about how Apollo Link works, check out the [docs](https://apollo-links-docs.netlify.com/links); Both GraphQL and Apollo Links have slightly varying concepts of what `context` is used for. To make it easy to use your GraphQL context to create your Apollo Link context, `makeRemoteExecutableSchema` attaches the context from the graphql resolver onto the link context under `graphqlContext`. +Since graphql-tools supports using a link for the network layer, the API is the same as you would write on the client. To learn more about how Apollo Link works, check out the [docs](https://www.apollographql.com/docs/link/); Both GraphQL and Apollo Links have slightly varying concepts of what `context` is used for. To make it easy to use your GraphQL context to create your Apollo Link context, `makeRemoteExecutableSchema` attaches the context from the graphql resolver onto the link context under `graphqlContext`. Basic usage diff --git a/docs/source/resolvers.md b/docs/source/resolvers.md index 89b6ab7aceb..206ef6eecf2 100644 --- a/docs/source/resolvers.md +++ b/docs/source/resolvers.md @@ -42,9 +42,9 @@ fieldName(obj, args, context, info) { result } These arguments have the following meanings and conventional names: -1. `obj`: The object that contains the result returned from the resolver on the parent field, or, in the case of a top-level `Query` field, the `rootValue` passed from the [server configuration](/tools/apollo-server/setup.html). This argument enables the nested nature of GraphQL queries. +1. `obj`: The object that contains the result returned from the resolver on the parent field, or, in the case of a top-level `Query` field, the `rootValue` passed from the [server configuration](/docs/apollo-server/setup.html). This argument enables the nested nature of GraphQL queries. 2. `args`: An object with the arguments passed into the field in the query. For example, if the field was called with `author(name: "Ada")`, the `args` object would be: `{ "name": "Ada" }`. -3. `context`: This is an object shared by all resolvers in a particular query, and is used to contain per-request state, including authentication information, dataloader instances, and anything else that should be taken into account when resolving the query. If you're using Apollo Server, [read about how to set the context in the setup documentation](/tools/apollo-server/setup.html). +3. `context`: This is an object shared by all resolvers in a particular query, and is used to contain per-request state, including authentication information, dataloader instances, and anything else that should be taken into account when resolving the query. If you're using Apollo Server, [read about how to set the context in the setup documentation](/docs/apollo-server/setup.html). 4. `info`: This argument should only be used in advanced cases, but it contains information about the execution state of the query, including the field name, path to the field from the root, and more. It's only documented in the [GraphQL.js source code](https://github.com/graphql/graphql-js/blob/c82ff68f52722c20f10da69c9e50a030a1f218ae/src/type/definition.js#L489-L500). ### Resolver result format @@ -178,7 +178,7 @@ Modules and extensions built by the community. Composition library for GraphQL, with helpers to combine multiple resolvers into one, specify dependencies between fields, and more. -When developing a GraphQL server, it is common to perform some authorization logic on your resolvers, usually based on the context of a request. With `graphql-resolvers` you can easily accomplish that and still make the code decoupled - thus testable - by combining multiple sigle-logic resolvers into one. +When developing a GraphQL server, it is common to perform some authorization logic on your resolvers, usually based on the context of a request. With `graphql-resolvers` you can easily accomplish that and still make the code decoupled - thus testable - by combining multiple single-logic resolvers into one. The following is an example of a simple logged-in authorization logic: diff --git a/docs/source/scalars.md b/docs/source/scalars.md index 72dfdcda0bd..11478c82e99 100644 --- a/docs/source/scalars.md +++ b/docs/source/scalars.md @@ -208,11 +208,8 @@ You can use it in your schema anywhere you could use a scalar: ```graphql type Query { - # As a return value - favoriteColor: AllowedColor - - # As an argument - avatar(borderColor: AllowedColor): String + favoriteColor: AllowedColor # As a return value + avatar(borderColor: AllowedColor): String # As an argument } ``` @@ -249,11 +246,8 @@ const typeDefs = ` } type Query { - # As a return value - favoriteColor: AllowedColor - - # As an argument - avatar(borderColor: AllowedColor): String + favoriteColor: AllowedColor # As a return value + avatar(borderColor: AllowedColor): String # As an argument } `; diff --git a/docs/source/schema-stitching.md b/docs/source/schema-stitching.md index 4e40416069a..281faec8e84 100644 --- a/docs/source/schema-stitching.md +++ b/docs/source/schema-stitching.md @@ -112,7 +112,7 @@ You won't be able to query `User.chirps` or `Chirp.author` yet however, because So what should these resolvers look like? -When we resolve `User.chirps` or `Chirp.author`, we want to delegate to the revelant root fields. To get from a user to its chirps for example, we'll want to use the `id` of the user to call `chirpsByAuthorId`. And to get from a chirp to its author, we can use the chirp's `authorId` field to call into `userById`. +When we resolve `User.chirps` or `Chirp.author`, we want to delegate to the relevant root fields. To get from a user to its chirps for example, we'll want to use the `id` of the user to call `chirpsByAuthorId`. And to get from a chirp to its author, we can use the chirp's `authorId` field to call into `userById`. Resolvers specified as part of `mergeSchema` have access to a `delegate` function that allows you to delegate to root fields. @@ -220,7 +220,7 @@ resolvers: mergeInfo => ({ #### mergeInfo and delegate -`mergeInfo` currenty is an object with one property - `delegate`. It looks like this: +`mergeInfo` currently is an object with one property - `delegate`. It looks like this: ```js type MergeInfo = { diff --git a/package.json b/package.json index f84beafd098..8711f22b669 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-tools", - "version": "2.14.0", + "version": "3.0.0-alpha.5", "description": "Useful tools to create and manipulate GraphQL schemas.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -49,34 +49,34 @@ "homepage": "https://github.com/apollostack/graphql-tools#readme", "dependencies": { "apollo-utilities": "^1.0.1", + "apollo-link": "^1.0.0", "deprecated-decorator": "^0.1.6", + "graphql-subscriptions": "^0.5.6", "uuid": "^3.1.0" }, "peerDependencies": { "graphql": "^0.11.0 || ^0.12.0" }, "devDependencies": { - "@types/chai": "4.0.4", + "@types/chai": "4.0.10", "@types/graphql": "0.11.7", "@types/mocha": "^2.2.44", "@types/node": "^8.0.47", "@types/uuid": "^3.4.3", "@types/zen-observable": "^0.5.3", - "apollo-link": "^1.0.0", "body-parser": "^1.18.2", "chai": "^4.1.2", "express": "^4.16.2", "graphql": "^0.12.3", - "graphql-subscriptions": "^0.5.4", "graphql-type-json": "^0.1.4", "istanbul": "^0.4.5", "iterall": "^1.1.3", "mocha": "^4.0.1", "prettier": "^1.7.4", - "remap-istanbul": "0.9.5", + "remap-istanbul": "0.9.6", "rimraf": "^2.6.2", "source-map-support": "^0.5.0", "tslint": "^5.8.0", - "typescript": "2.6.1" + "typescript": "2.6.2" } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index e22490c7c1f..a0ae63531c9 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -8,6 +8,7 @@ import { GraphQLIsTypeOfFn, GraphQLTypeResolver, GraphQLScalarType, + GraphQLNamedType, DocumentNode, } from 'graphql'; @@ -29,12 +30,14 @@ export interface IResolverOptions { export type MergeInfo = { delegate: ( + schemaName: string, type: 'query' | 'mutation' | 'subscription', fieldName: string, args: { [key: string]: any }, context: { [key: string]: any }, info: GraphQLResolveInfo, ) => any; + getSubSchema: (schemaName: string) => GraphQLSchema; }; export type IFieldResolver = ( @@ -49,7 +52,7 @@ export type ITypeDefinitions = ITypedef | ITypedef[]; export type IResolverObject = { [key: string]: IFieldResolver | IResolverOptions; }; -export type IEnumResolver = { [key: string]: string }; +export type IEnumResolver = { [key: string]: string | number }; export interface IResolvers { [key: string]: | (() => any) @@ -119,3 +122,35 @@ export interface IMockServer { vars?: { [key: string]: any }, ) => Promise; } + +export type MergeTypeCandidate = { + schemaName: string; + schema?: GraphQLSchema; + type: GraphQLNamedType; +}; + +export type TypeWithResolvers = { + type: GraphQLNamedType; + resolvers?: IResolvers; +}; + +export type VisitTypeResult = GraphQLNamedType | TypeWithResolvers | null; + +export type VisitType = ( + name: string, + candidates: Array, +) => VisitTypeResult; + +export type ResolveType = (type: T) => T; + +export type Operation = 'query' | 'mutation' | 'subscription'; + +export type Request = { + document: DocumentNode; + variables: Record; + extensions?: Record; +}; + +export type Result = ExecutionResult & { + extensions?: Record; +}; diff --git a/src/index.ts b/src/index.ts index a758157fff0..826d2d2c9dd 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './schemaGenerator'; export * from './mock'; export * from './stitching'; +export * from './transforms'; diff --git a/src/stitching/TypeRegistry.ts b/src/stitching/TypeRegistry.ts deleted file mode 100644 index d0bc9e592af..00000000000 --- a/src/stitching/TypeRegistry.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - GraphQLSchema, - GraphQLNonNull, - GraphQLList, - GraphQLNamedType, - GraphQLType, - isNamedType, - getNamedType, - InlineFragmentNode, - Kind, - parse, -} from 'graphql'; - -export default class TypeRegistry { - public fragmentReplacements: { - [typeName: string]: { [fieldName: string]: InlineFragmentNode }; - }; - private types: { [key: string]: GraphQLNamedType }; - private schemaByField: { - query: { [key: string]: GraphQLSchema }; - mutation: { [key: string]: GraphQLSchema }; - subscription: { [key: string]: GraphQLSchema }; - }; - - constructor() { - this.types = {}; - this.schemaByField = { - query: {}, - mutation: {}, - subscription: {}, - }; - this.fragmentReplacements = {}; - } - - public getSchemaByField( - operation: 'query' | 'mutation' | 'subscription', - fieldName: string, - ): GraphQLSchema { - return this.schemaByField[operation][fieldName]; - } - - public getAllTypes(): Array { - return Object.keys(this.types).map(name => this.types[name]); - } - - public getType(name: string): GraphQLNamedType { - if (!this.types[name]) { - throw new Error(`No such type: ${name}`); - } - return this.types[name]; - } - - public resolveType(type: T): T { - if (type instanceof GraphQLList) { - return new GraphQLList(this.resolveType(type.ofType)) as T; - } else if (type instanceof GraphQLNonNull) { - return new GraphQLNonNull(this.resolveType(type.ofType)) as T; - } else if (isNamedType(type)) { - return this.getType(getNamedType(type).name) as T; - } else { - return type; - } - } - - public addSchema(schema: GraphQLSchema) { - const query = schema.getQueryType(); - if (query) { - const fieldNames = Object.keys(query.getFields()); - fieldNames.forEach(field => { - this.schemaByField.query[field] = schema; - }); - } - - const mutation = schema.getMutationType(); - if (mutation) { - const fieldNames = Object.keys(mutation.getFields()); - fieldNames.forEach(field => { - this.schemaByField.mutation[field] = schema; - }); - } - - const subscription = schema.getSubscriptionType(); - if (subscription) { - const fieldNames = Object.keys(subscription.getFields()); - fieldNames.forEach(field => { - this.schemaByField.subscription[field] = schema; - }); - } - } - - public addType( - name: string, - type: GraphQLNamedType, - onTypeConflict?: ( - leftType: GraphQLNamedType, - rightType: GraphQLNamedType, - ) => GraphQLNamedType, - ): void { - if (this.types[name]) { - if (onTypeConflict) { - type = onTypeConflict(this.types[name], type); - } else { - throw new Error(`Type name conflict: ${name}`); - } - } - this.types[name] = type; - } - - public addFragment(typeName: string, fieldName: string, fragment: string) { - if (!this.fragmentReplacements[typeName]) { - this.fragmentReplacements[typeName] = {}; - } - this.fragmentReplacements[typeName][ - fieldName - ] = parseFragmentToInlineFragment(fragment); - } -} - -function parseFragmentToInlineFragment( - definitions: string, -): InlineFragmentNode { - const document = parse(definitions); - for (const definition of document.definitions) { - if (definition.kind === Kind.FRAGMENT_DEFINITION) { - return { - kind: Kind.INLINE_FRAGMENT, - typeCondition: definition.typeCondition, - selectionSet: definition.selectionSet, - }; - } - } - throw new Error('Could not parse fragment'); -} diff --git a/src/stitching/delegateToSchema.ts b/src/stitching/delegateToSchema.ts index b60b0aa2594..90f7d2cafde 100644 --- a/src/stitching/delegateToSchema.ts +++ b/src/stitching/delegateToSchema.ts @@ -2,573 +2,125 @@ import { DocumentNode, FieldNode, FragmentDefinitionNode, - FragmentSpreadNode, - GraphQLField, - GraphQLInputType, - GraphQLInterfaceType, - GraphQLList, - GraphQLNamedType, - GraphQLNonNull, - GraphQLObjectType, GraphQLResolveInfo, GraphQLSchema, - GraphQLType, - GraphQLUnionType, - InlineFragmentNode, Kind, OperationDefinitionNode, SelectionSetNode, - TypeNameMetaFieldDef, - TypeNode, - VariableDefinitionNode, - VariableNode, - execute, - visit, + SelectionNode, subscribe, + graphql, + print, + validate, + VariableDefinitionNode, } from 'graphql'; -import { checkResultAndHandleErrors } from './errors'; +import { Operation, Request } from '../Interfaces'; +import { + Transform, + applyRequestTransforms, + applyResultTransforms, +} from '../transforms/transforms'; +import AddArgumentsAsVariables from '../transforms/AddArgumentsAsVariables'; +import FilterToSchema from '../transforms/FilterToSchema'; +import AddTypenameToAbstract from '../transforms/AddTypenameToAbstract'; +import CheckResultAndHandleErrors from '../transforms/CheckResultAndHandleErrors'; export default async function delegateToSchema( - schema: GraphQLSchema, - fragmentReplacements: { - [typeName: string]: { [fieldName: string]: InlineFragmentNode }; - }, - operation: 'query' | 'mutation' | 'subscription', - fieldName: string, + targetSchema: GraphQLSchema, + targetOperation: Operation, + targetField: string, args: { [key: string]: any }, context: { [key: string]: any }, info: GraphQLResolveInfo, + transforms: Array, ): Promise { - let type; - if (operation === 'mutation') { - type = schema.getMutationType(); - } else if (operation === 'subscription') { - type = schema.getSubscriptionType(); - } else { - type = schema.getQueryType(); + const rawDocument: DocumentNode = createDocument( + targetField, + targetOperation, + info.fieldNodes, + Object.keys(info.fragments).map( + fragmentName => info.fragments[fragmentName], + ), + info.operation.variableDefinitions, + ); + + const rawRequest: Request = { + document: rawDocument, + variables: info.variableValues as Record, + }; + + transforms = [ + ...transforms, + AddArgumentsAsVariables(targetSchema, args), + FilterToSchema(targetSchema), + AddTypenameToAbstract(targetSchema), + CheckResultAndHandleErrors(info, targetField), + ]; + + const processedRequest = applyRequestTransforms(rawRequest, transforms); + + const errors = validate(targetSchema, processedRequest.document); + if (errors.length > 0) { + throw errors; } - if (type) { - const graphqlDoc: DocumentNode = createDocument( - schema, - fragmentReplacements, - type, - fieldName, - operation, - info.fieldNodes, - info.fragments, - info.operation.variableDefinitions, - ); - const operationDefinition = graphqlDoc.definitions.find( - ({ kind }) => kind === Kind.OPERATION_DEFINITION, + if (targetOperation === 'query' || targetOperation === 'mutation') { + const rawResult = await graphql( + targetSchema, + print(processedRequest.document), + info.rootValue, + context, + processedRequest.variables, ); - let variableValues = {}; - if ( - operationDefinition && - operationDefinition.kind === Kind.OPERATION_DEFINITION && - operationDefinition.variableDefinitions - ) { - operationDefinition.variableDefinitions.forEach(definition => { - const key = definition.variable.name.value; - // (XXX) This is kinda hacky - let actualKey = key; - if (actualKey.startsWith('_')) { - actualKey = actualKey.slice(1); - } - const value = args[actualKey] || args[key] || info.variableValues[key]; - variableValues[key] = value; - }); - } - if (operation === 'query' || operation === 'mutation') { - const result = await execute( - schema, - graphqlDoc, - info.rootValue, - context, - variableValues, - ); - return checkResultAndHandleErrors(result, info, fieldName); - } - - if (operation === 'subscription') { - return subscribe( - schema, - graphqlDoc, - info.rootValue, - context, - variableValues, - ); - } + const result = applyResultTransforms(rawResult, transforms); + return result; } - throw new Error('Could not forward to merged schema'); + if (targetOperation === 'subscription') { + // apply result processing ??? + return subscribe( + targetSchema, + processedRequest.document, + info.rootValue, + context, + processedRequest.variables, + ); + } } export function createDocument( - schema: GraphQLSchema, - fragmentReplacements: { - [typeName: string]: { [fieldName: string]: InlineFragmentNode }; - }, - type: GraphQLObjectType, - rootFieldName: string, - operation: 'query' | 'mutation' | 'subscription', - selections: Array, - fragments: { [fragmentName: string]: FragmentDefinitionNode }, - variableDefinitions?: Array, + targetField: string, + targetOperation: Operation, + selections: Array, + fragments: Array, + variables: Array, ): DocumentNode { - const rootField = type.getFields()[rootFieldName]; - const newVariables: Array<{ arg: string; variable: string }> = []; - const rootSelectionSet = { + const originalSelection = selections[0] as FieldNode; + const rootField: FieldNode = { + kind: Kind.FIELD, + alias: null, + arguments: originalSelection.arguments, + selectionSet: originalSelection.selectionSet, + name: { + kind: Kind.NAME, + value: targetField, + }, + }; + const rootSelectionSet: SelectionSetNode = { kind: Kind.SELECTION_SET, - // (XXX) This (wrongly) assumes only having one fieldNode - selections: selections.map(selection => { - if (selection.kind === Kind.FIELD) { - const { selection: newSelection, variables } = processRootField( - selection, - rootFieldName, - rootField, - ); - newVariables.push(...variables); - return newSelection; - } else { - return selection; - } - }), + selections: [rootField], }; - const newVariableDefinitions = newVariables.map(({ arg, variable }) => { - const argDef = rootField.args.find(rootArg => rootArg.name === arg); - if (!argDef) { - throw new Error('Unexpected missing arg'); - } - const typeName = typeToAst(argDef.type); - return { - kind: Kind.VARIABLE_DEFINITION, - variable: { - kind: Kind.VARIABLE, - name: { - kind: Kind.NAME, - value: variable, - }, - }, - type: typeName, - }; - }); - - const { - selectionSet, - fragments: processedFragments, - usedVariables, - } = filterSelectionSetDeep( - schema, - fragmentReplacements, - type, - rootSelectionSet, - fragments, - ); - const operationDefinition: OperationDefinitionNode = { kind: Kind.OPERATION_DEFINITION, - operation, - variableDefinitions: [ - ...(variableDefinitions || []).filter( - variableDefinition => - usedVariables.indexOf(variableDefinition.variable.name.value) !== -1, - ), - ...newVariableDefinitions, - ], - selectionSet, - }; - - const newDoc: DocumentNode = { - kind: Kind.DOCUMENT, - definitions: [operationDefinition, ...processedFragments], + operation: targetOperation, + variableDefinitions: variables, + selectionSet: rootSelectionSet, }; - return newDoc; -} - -function processRootField( - selection: FieldNode, - rootFieldName: string, - rootField: GraphQLField, -): { - selection: FieldNode; - variables: Array<{ arg: string; variable: string }>; -} { - const existingArguments = selection.arguments || []; - const existingArgumentNames = existingArguments.map(arg => arg.name.value); - const allowedArguments = rootField.args.map(arg => arg.name); - const missingArgumentNames = difference( - allowedArguments, - existingArgumentNames, - ); - const extraArguments = difference(existingArgumentNames, allowedArguments); - const filteredExistingArguments = existingArguments.filter( - arg => extraArguments.indexOf(arg.name.value) === -1, - ); - const variables: Array<{ arg: string; variable: string }> = []; - const missingArguments = missingArgumentNames.map(name => { - // (XXX): really needs better var generation - const variableName = `_${name}`; - variables.push({ - arg: name, - variable: variableName, - }); - return { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: name, - }, - value: { - kind: Kind.VARIABLE, - name: { - kind: Kind.NAME, - value: variableName, - }, - }, - }; - }); - return { - selection: { - kind: Kind.FIELD, - alias: null, - arguments: [...filteredExistingArguments, ...missingArguments], - selectionSet: selection.selectionSet, - name: { - kind: Kind.NAME, - value: rootFieldName, - }, - }, - variables, - }; -} - -function filterSelectionSetDeep( - schema: GraphQLSchema, - fragmentReplacements: { - [typeName: string]: { [fieldName: string]: InlineFragmentNode }; - }, - type: GraphQLType, - selectionSet: SelectionSetNode, - fragments: { [fragmentName: string]: FragmentDefinitionNode }, -): { - selectionSet: SelectionSetNode; - fragments: Array; - usedVariables: Array; -} { - const validFragments: Array = []; - Object.keys(fragments).forEach(fragmentName => { - const fragment = fragments[fragmentName]; - const typeName = fragment.typeCondition.name.value; - const innerType = schema.getType(typeName); - if (innerType) { - validFragments.push(fragment); - } - }); - let { - selectionSet: newSelectionSet, - usedFragments: remainingFragments, - usedVariables, - } = filterSelectionSet( - schema, - fragmentReplacements, - type, - selectionSet, - validFragments, - ); - - const newFragments = {}; - // (XXX): So this will break if we have a fragment that only has link fields - while (remainingFragments.length > 0) { - const name = remainingFragments.pop(); - if (newFragments[name]) { - continue; - } else { - const nextFragment = fragments[name]; - if (!name) { - throw new Error(`Could not find fragment ${name}`); - } - const typeName = nextFragment.typeCondition.name.value; - const innerType = schema.getType(typeName); - if (!innerType) { - continue; - } - const { - selectionSet: fragmentSelectionSet, - usedFragments: fragmentUsedFragments, - usedVariables: fragmentUsedVariables, - } = filterSelectionSet( - schema, - fragmentReplacements, - innerType, - nextFragment.selectionSet, - validFragments, - ); - remainingFragments = union(remainingFragments, fragmentUsedFragments); - usedVariables = union(usedVariables, fragmentUsedVariables); - newFragments[name] = { - kind: Kind.FRAGMENT_DEFINITION, - name: { - kind: Kind.NAME, - value: name, - }, - typeCondition: nextFragment.typeCondition, - selectionSet: fragmentSelectionSet, - }; - } - } - const newFragmentValues: Array = Object.keys( - newFragments, - ).map(name => newFragments[name]); - return { - selectionSet: newSelectionSet, - fragments: newFragmentValues, - usedVariables, - }; -} - -function filterSelectionSet( - schema: GraphQLSchema, - fragmentReplacements: { - [typeName: string]: { [fieldName: string]: InlineFragmentNode }; - }, - type: GraphQLType, - selectionSet: SelectionSetNode, - validFragments: Array, -): { - selectionSet: SelectionSetNode; - usedFragments: Array; - usedVariables: Array; -} { - const usedFragments: Array = []; - const usedVariables: Array = []; - const typeStack: Array = [type]; - const filteredSelectionSet = visit(selectionSet, { - [Kind.FIELD]: { - enter(node: FieldNode): null | undefined | FieldNode { - let parentType: GraphQLNamedType = resolveType( - typeStack[typeStack.length - 1], - ); - if ( - parentType instanceof GraphQLObjectType || - parentType instanceof GraphQLInterfaceType - ) { - const fields = parentType.getFields(); - const field = - node.name.value === '__typename' - ? TypeNameMetaFieldDef - : fields[node.name.value]; - if (!field) { - return null; - } else { - typeStack.push(field.type); - } - } else if ( - parentType instanceof GraphQLUnionType && - node.name.value === '__typename' - ) { - typeStack.push(TypeNameMetaFieldDef.type); - } - }, - leave() { - typeStack.pop(); - }, - }, - [Kind.SELECTION_SET]( - node: SelectionSetNode, - ): SelectionSetNode | null | undefined { - const parentType: GraphQLType = resolveType( - typeStack[typeStack.length - 1], - ); - const parentTypeName = parentType.name; - let selections = node.selections; - if ( - (parentType instanceof GraphQLInterfaceType || - parentType instanceof GraphQLUnionType) && - !selections.find( - _ => - (_ as FieldNode).kind === Kind.FIELD && - (_ as FieldNode).name.value === '__typename', - ) - ) { - selections = selections.concat({ - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: '__typename', - }, - }); - } - - if (fragmentReplacements[parentTypeName]) { - selections.forEach(selection => { - if (selection.kind === Kind.FIELD) { - const name = selection.name.value; - const fragment = fragmentReplacements[parentTypeName][name]; - if (fragment) { - selections = selections.concat(fragment); - } - } - }); - } - - if (selections !== node.selections) { - return { - ...node, - selections, - }; - } - }, - [Kind.FRAGMENT_SPREAD](node: FragmentSpreadNode): null | undefined { - const fragmentFiltered = validFragments.filter( - frg => frg.name.value === node.name.value, - ); - const fragment = fragmentFiltered[0]; - if (fragment) { - if (fragment.typeCondition) { - const innerType = schema.getType(fragment.typeCondition.name.value); - const parentType: GraphQLNamedType = resolveType( - typeStack[typeStack.length - 1], - ); - if (!implementsAbstractType(parentType, innerType)) { - return null; - } - } - usedFragments.push(node.name.value); - return; - } else { - return null; - } - }, - [Kind.INLINE_FRAGMENT]: { - enter(node: InlineFragmentNode): null | undefined { - if (node.typeCondition) { - const innerType = schema.getType(node.typeCondition.name.value); - const parentType: GraphQLNamedType = resolveType( - typeStack[typeStack.length - 1], - ); - if (implementsAbstractType(parentType, innerType)) { - typeStack.push(innerType); - } else { - return null; - } - } - }, - leave(node: InlineFragmentNode): null | undefined { - if (node.typeCondition) { - const innerType = schema.getType(node.typeCondition.name.value); - if (innerType) { - typeStack.pop(); - } else { - return null; - } - } - }, - }, - [Kind.VARIABLE](node: VariableNode) { - usedVariables.push(node.name.value); - }, - }); - - return { - selectionSet: filteredSelectionSet, - usedFragments, - usedVariables, + kind: Kind.DOCUMENT, + definitions: [operationDefinition, ...fragments], }; } - -function resolveType(type: GraphQLType): GraphQLNamedType { - let lastType = type; - while ( - lastType instanceof GraphQLNonNull || - lastType instanceof GraphQLList - ) { - lastType = lastType.ofType; - } - return lastType; -} - -function implementsAbstractType( - parent: GraphQLType, - child: GraphQLType, - bail: boolean = false, -): boolean { - if (parent === child) { - return true; - } else if ( - parent instanceof GraphQLInterfaceType && - child instanceof GraphQLObjectType - ) { - return child.getInterfaces().indexOf(parent) !== -1; - } else if ( - parent instanceof GraphQLUnionType && - child instanceof GraphQLObjectType - ) { - return parent.getTypes().indexOf(child) !== -1; - } else if (parent instanceof GraphQLObjectType && !bail) { - return implementsAbstractType(child, parent, true); - } - - return false; -} - -function typeToAst(type: GraphQLInputType): TypeNode { - if (type instanceof GraphQLNonNull) { - const innerType = typeToAst(type.ofType); - if ( - innerType.kind === Kind.LIST_TYPE || - innerType.kind === Kind.NAMED_TYPE - ) { - return { - kind: Kind.NON_NULL_TYPE, - type: innerType, - }; - } else { - throw new Error('Incorrent inner non-null type'); - } - } else if (type instanceof GraphQLList) { - return { - kind: Kind.LIST_TYPE, - type: typeToAst(type.ofType), - }; - } else { - return { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: type.toString(), - }, - }; - } -} - -function union(...arrays: Array>): Array { - const cache: { [key: string]: Boolean } = {}; - const result: Array = []; - arrays.forEach(array => { - array.forEach(item => { - if (!cache[item]) { - cache[item] = true; - result.push(item); - } - }); - }); - return result; -} - -function difference( - from: Array, - ...arrays: Array> -): Array { - const cache: { [key: string]: Boolean } = {}; - arrays.forEach(array => { - array.forEach(item => { - cache[item] = true; - }); - }); - return from.filter(item => !cache[item]); -} diff --git a/src/stitching/linkToFetcher.ts b/src/stitching/linkToFetcher.ts index 18a4b1bc7d7..cd835c30f6b 100644 --- a/src/stitching/linkToFetcher.ts +++ b/src/stitching/linkToFetcher.ts @@ -45,7 +45,7 @@ function makePromise(observable: Observable): Promise { }); } -function execute( +export function execute( link: ApolloLink, operation: GraphQLRequest, ): Observable { diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index 28ee534dbb4..ea7038b8506 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -1,6 +1,3 @@ -import { printSchema, Kind, ValueNode } from 'graphql'; -import linkToFetcher from './linkToFetcher'; - // This import doesn't actually import code - only the types. // Don't use ApolloLink to actually construct a link here. import { ApolloLink } from 'apollo-link'; @@ -20,13 +17,26 @@ import { ExecutionResult, print, buildSchema, + printSchema, + Kind, + ValueNode, + GraphQLResolveInfo, } from 'graphql'; +import linkToFetcher, { execute } from './linkToFetcher'; import isEmptyObject from '../isEmptyObject'; import { IResolvers, IResolverObject } from '../Interfaces'; import { makeExecutableSchema } from '../schemaGenerator'; import resolveParentFromTypename from './resolveFromParentTypename'; import defaultMergedResolver from './defaultMergedResolver'; import { checkResultAndHandleErrors } from './errors'; +import { PubSub, PubSubEngine } from 'graphql-subscriptions'; + +export type ResolverFn = ( + rootValue?: any, + args?: any, + context?: any, + info?: GraphQLResolveInfo, +) => AsyncIterator; export type Fetcher = (operation: FetcherOperation) => Promise; @@ -41,10 +51,12 @@ export default function makeRemoteExecutableSchema({ schema, link, fetcher, + createPubSub, }: { schema: GraphQLSchema | string; link?: ApolloLink; fetcher?: Fetcher; + createPubSub?: () => PubSubEngine; }): GraphQLSchema { if (!fetcher && link) { fetcher = linkToFetcher(link); @@ -59,13 +71,16 @@ export default function makeRemoteExecutableSchema({ typeDefs = printSchema(schema); } + // prepare query resolvers + const queryResolvers: IResolverObject = {}; const queryType = schema.getQueryType(); const queries = queryType.getFields(); - const queryResolvers: IResolverObject = {}; Object.keys(queries).forEach(key => { queryResolvers[key] = createResolver(fetcher); }); - let mutationResolvers: IResolverObject = {}; + + // prepare mutation resolvers + const mutationResolvers: IResolverObject = {}; const mutationType = schema.getMutationType(); if (mutationType) { const mutations = mutationType.getFields(); @@ -74,12 +89,31 @@ export default function makeRemoteExecutableSchema({ }); } + // prepare subscription resolvers + const subscriptionResolvers: IResolverObject = {}; + const subscriptionType = schema.getSubscriptionType(); + if (subscriptionType) { + const pubSub = createPubSub ? createPubSub() : new PubSub(); + const subscriptions = subscriptionType.getFields(); + Object.keys(subscriptions).forEach(key => { + subscriptionResolvers[key] = { + subscribe: createSubscriptionResolver(key, link, pubSub), + }; + }); + } + + // merge resolvers into resolver map const resolvers: IResolvers = { [queryType.name]: queryResolvers }; if (!isEmptyObject(mutationResolvers)) { resolvers[mutationType.name] = mutationResolvers; } + if (!isEmptyObject(subscriptionResolvers)) { + resolvers[subscriptionType.name] = subscriptionResolvers; + } + + // add missing abstract resolvers (scalar, unions, interfaces) const typeMap = schema.getTypeMap(); const types = Object.keys(typeMap).map(name => typeMap[name]); for (const type of types) { @@ -108,7 +142,8 @@ export default function makeRemoteExecutableSchema({ type instanceof GraphQLObjectType && type.name.slice(0, 2) !== '__' && type !== queryType && - type !== mutationType + type !== mutationType && + type !== subscriptionType ) { const resolver = {}; Object.keys(type.getFields()).forEach(field => { @@ -142,6 +177,42 @@ function createResolver(fetcher: Fetcher): GraphQLFieldResolver { }; } +function createSubscriptionResolver( + name: string, + link: ApolloLink, + pubSub: PubSubEngine, +): ResolverFn { + return (root, args, context, info) => { + const fragments = Object.keys(info.fragments).map( + fragment => info.fragments[fragment], + ); + const document = { + kind: Kind.DOCUMENT, + definitions: [info.operation, ...fragments], + }; + + const operation = { + query: document, + variables: info.variableValues, + context: { graphqlContext: context }, + }; + const observable = execute(link, operation); + + const observer = { + next(value: any) { + pubSub.publish(`remote-schema-${name}`, value.data); + }, + error(err: Error) { + pubSub.publish(`remote-schema-${name}`, { errors: [err] }); + }, + }; + + observable.subscribe(observer); + + return pubSub.asyncIterator(`remote-schema-${name}`); + }; +} + function createPassThroughScalar({ name, description, diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 8cbc632001f..c39d772925f 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -1,243 +1,221 @@ import { DocumentNode, GraphQLField, - GraphQLFieldMap, GraphQLInputObjectType, + GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, GraphQLResolveInfo, GraphQLScalarType, GraphQLSchema, - GraphQLType, - buildASTSchema, + GraphQLString, + InlineFragmentNode, + Kind, extendSchema, getNamedType, - isCompositeType, isNamedType, parse, } from 'graphql'; -import TypeRegistry from './TypeRegistry'; -import { IResolvers, MergeInfo, IFieldResolver } from '../Interfaces'; -import isEmptyObject from '../isEmptyObject'; +import { + IResolvers, + MergeInfo, + IFieldResolver, + VisitType, + MergeTypeCandidate, + TypeWithResolvers, + VisitTypeResult, +} from '../Interfaces'; import { extractExtensionDefinitions, addResolveFunctionsToSchema, } from '../schemaGenerator'; import { - recreateCompositeType, + recreateType, fieldMapToFieldConfigMap, + createResolveType, } from './schemaRecreation'; import delegateToSchema from './delegateToSchema'; -import typeFromAST from './typeFromAST'; - -const backcompatOptions = { commentDescriptions: true }; +import typeFromAST, { GetType } from './typeFromAST'; +import ReplaceFieldWithFragment from '../transforms/ReplaceFieldWithFragment'; export default function mergeSchemas({ schemas, - onTypeConflict, + visitType, resolvers, }: { - schemas: Array; - onTypeConflict?: ( - left: GraphQLNamedType, - right: GraphQLNamedType, - ) => GraphQLNamedType; + schemas: Array<{ name: string; schema: string | GraphQLSchema }>; + visitType?: VisitType; resolvers?: IResolvers | ((mergeInfo: MergeInfo) => IResolvers); }): GraphQLSchema { - if (!onTypeConflict) { - onTypeConflict = defaultOnTypeConflict; + const allSchemas: { [name: string]: GraphQLSchema } = {}; + const typeCandidates: { [name: string]: Array } = {}; + const types: { [name: string]: GraphQLNamedType } = {}; + const extensions: Array = []; + const fragments = {}; + + if (!resolvers) { + resolvers = {}; } - let queryFields: GraphQLFieldMap = {}; - let mutationFields: GraphQLFieldMap = {}; - let subscriptionFields: GraphQLFieldMap = {}; - const typeRegistry = new TypeRegistry(); + if (!visitType) { + visitType = defaultVisitType; + } - const mergeInfo: MergeInfo = createMergeInfo(typeRegistry); + const resolveType = createResolveType(name => { + if (types[name] === undefined) { + throw new Error(`Can't find type ${name}.`); + } + return types[name]; + }); - const actualSchemas: Array = []; - const typeFragments: Array = []; - const extensions: Array = []; - let fullResolvers: IResolvers = {}; + const createNamedStub: GetType = (name, type) => { + let constructor: any; + if (type === 'object') { + constructor = GraphQLObjectType; + } else if (type === 'interface') { + constructor = GraphQLInterfaceType; + } else { + constructor = GraphQLInputObjectType; + } + return new constructor({ + name, + fields: { + __fake: { + type: GraphQLString, + }, + }, + }); + }; - schemas.forEach(schema => { - if (schema instanceof GraphQLSchema) { - actualSchemas.push(schema); - } else if (typeof schema === 'string') { - let parsedSchemaDocument = parse(schema); - try { - // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 - const actualSchema = (buildASTSchema as any)( - parsedSchemaDocument, - backcompatOptions, - ); - actualSchemas.push(actualSchema); - } catch (e) { - typeFragments.push(parsedSchemaDocument); + schemas.forEach(subSchema => { + if (subSchema.schema instanceof GraphQLSchema) { + const schema = subSchema.schema; + allSchemas[subSchema.name] = schema; + const queryType = schema.getQueryType(); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); + addTypeCandidate(typeCandidates, 'Query', { + schemaName: subSchema.name, + schema, + type: queryType, + }); + if (mutationType) { + addTypeCandidate(typeCandidates, 'Mutation', { + schemaName: subSchema.name, + schema, + type: mutationType, + }); } - parsedSchemaDocument = extractExtensionDefinitions(parsedSchemaDocument); - if (parsedSchemaDocument.definitions.length > 0) { - extensions.push(parsedSchemaDocument); + if (subscriptionType) { + addTypeCandidate(typeCandidates, 'Subscription', { + schemaName: subSchema.name, + schema, + type: subscriptionType, + }); } - } - }); - - actualSchemas.forEach(schema => { - typeRegistry.addSchema(schema); - const queryType = schema.getQueryType(); - const mutationType = schema.getMutationType(); - const subscriptionType = schema.getSubscriptionType(); - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach(typeName => { - const type: GraphQLType = typeMap[typeName]; - if ( - isNamedType(type) && - getNamedType(type).name.slice(0, 2) !== '__' && - type !== queryType && - type !== mutationType && - type !== subscriptionType - ) { - let newType; - if (isCompositeType(type) || type instanceof GraphQLInputObjectType) { - newType = recreateCompositeType(schema, type, typeRegistry); - } else { - newType = getNamedType(type); + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach(typeName => { + const type: GraphQLNamedType = typeMap[typeName]; + if ( + isNamedType(type) && + getNamedType(type).name.slice(0, 2) !== '__' && + type !== queryType && + type !== mutationType && + type !== subscriptionType + ) { + addTypeCandidate(typeCandidates, type.name, { + schemaName: subSchema.name, + schema, + type: type, + }); } - if (newType instanceof GraphQLObjectType) { - delete newType.isTypeOf; + }); + } else if (typeof subSchema.schema === 'string') { + let parsedSchemaDocument = parse(subSchema.schema); + parsedSchemaDocument.definitions.forEach(def => { + const type = typeFromAST(def, createNamedStub); + if (type) { + addTypeCandidate(typeCandidates, type.name, { + schemaName: subSchema.name, + type: type, + }); } - typeRegistry.addType(newType.name, newType, onTypeConflict); - } - }); + }); - Object.keys(queryType.getFields()).forEach(name => { - if (!fullResolvers.Query) { - fullResolvers.Query = {}; - } - fullResolvers.Query[name] = createDelegatingResolver( - mergeInfo, - 'query', - name, + const extensionsDocument = extractExtensionDefinitions( + parsedSchemaDocument, ); - }); - - queryFields = { - ...queryFields, - ...queryType.getFields(), - }; - - if (mutationType) { - if (!fullResolvers.Mutation) { - fullResolvers.Mutation = {}; + if (extensionsDocument.definitions.length > 0) { + extensions.push(extensionsDocument); } - Object.keys(mutationType.getFields()).forEach(name => { - fullResolvers.Mutation[name] = createDelegatingResolver( - mergeInfo, - 'mutation', - name, - ); - }); - - mutationFields = { - ...mutationFields, - ...mutationType.getFields(), - }; + } else { + throw new Error(`Invalid schema ${subSchema.name}`); } + }); - if (subscriptionType) { - if (!fullResolvers.Subscription) { - fullResolvers.Subscription = {}; - } - Object.keys(subscriptionType.getFields()).forEach(name => { - fullResolvers.Subscription[name] = { - subscribe: createDelegatingResolver(mergeInfo, 'subscription', name), - }; - }); + let generatedResolvers = {}; - subscriptionFields = { - ...subscriptionFields, - ...subscriptionType.getFields(), - }; + Object.keys(typeCandidates).forEach(typeName => { + const resultType: VisitTypeResult = visitType( + typeName, + typeCandidates[typeName], + ); + if (resultType === null) { + types[typeName] = null; + } else { + let type: GraphQLNamedType; + let typeResolvers: IResolvers; + if (isNamedType(resultType)) { + type = resultType; + } else if ((resultType).type) { + type = (resultType).type; + typeResolvers = (resultType).resolvers; + } else { + throw new Error('Invalid `visitType` result for type "${typeName}"'); + } + types[typeName] = recreateType(type, resolveType); + if (typeResolvers) { + generatedResolvers[typeName] = typeResolvers; + } } }); - typeFragments.forEach(document => { - document.definitions.forEach(def => { - const type = typeFromAST(typeRegistry, def); - if (type) { - typeRegistry.addType(type.name, type, onTypeConflict); - } - }); + let mergedSchema = new GraphQLSchema({ + query: types.Query as GraphQLObjectType, + mutation: types.Mutation as GraphQLObjectType, + subscription: types.Subscription as GraphQLObjectType, + types: Object.keys(types).map(key => types[key]), }); - let passedResolvers = {}; - if (resolvers) { - if (typeof resolvers === 'function') { - passedResolvers = resolvers(mergeInfo); - } else { - passedResolvers = { ...resolvers }; - } - } + extensions.forEach(extension => { + mergedSchema = (extendSchema as any)(mergedSchema, extension, { + commentDescriptions: true, + }); + }); - Object.keys(passedResolvers).forEach(typeName => { - const type = passedResolvers[typeName]; + Object.keys(resolvers).forEach(typeName => { + const type = resolvers[typeName]; if (type instanceof GraphQLScalarType) { return; } Object.keys(type).forEach(fieldName => { const field = type[fieldName]; if (field.fragment) { - typeRegistry.addFragment(typeName, fieldName, field.fragment); + fragments[typeName] = fragments[typeName] || {}; + fragments[typeName][fieldName] = parseFragmentToInlineFragment( + field.fragment, + ); } }); }); - fullResolvers = mergeDeep(fullResolvers, passedResolvers); - - const query = new GraphQLObjectType({ - name: 'Query', - fields: () => fieldMapToFieldConfigMap(queryFields, typeRegistry), - }); - - let mutation; - if (!isEmptyObject(mutationFields)) { - mutation = new GraphQLObjectType({ - name: 'Mutation', - fields: () => fieldMapToFieldConfigMap(mutationFields, typeRegistry), - }); - } - - let subscription; - if (!isEmptyObject(subscriptionFields)) { - subscription = new GraphQLObjectType({ - name: 'Subscription', - fields: () => fieldMapToFieldConfigMap(subscriptionFields, typeRegistry), - }); - } - - typeRegistry.addType('Query', query); - typeRegistry.addType('Mutation', mutation); - typeRegistry.addType('Subscription', subscription); - - let mergedSchema = new GraphQLSchema({ - query, - mutation, - subscription, - types: typeRegistry.getAllTypes(), - }); - - extensions.forEach(extension => { - // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 - mergedSchema = (extendSchema as any)( - mergedSchema, - extension, - backcompatOptions, - ); - }); - - addResolveFunctionsToSchema(mergedSchema, fullResolvers); + addResolveFunctionsToSchema( + mergedSchema, + mergeDeep(generatedResolvers, resolvers), + ); + const mergeInfo = createMergeInfo(allSchemas, fragments); forEachField(mergedSchema, field => { if (field.resolve) { const fieldResolver = field.resolve; @@ -246,57 +224,102 @@ export default function mergeSchemas({ return fieldResolver(parent, args, context, newInfo); }; } + if (field.subscribe) { + const fieldResolver = field.subscribe; + field.subscribe = (parent, args, context, info) => { + const newInfo = { ...info, mergeInfo }; + return fieldResolver(parent, args, context, newInfo); + }; + } }); return mergedSchema; } -function defaultOnTypeConflict( - left: GraphQLNamedType, - right: GraphQLNamedType, -): GraphQLNamedType { - return left; -} - -function createMergeInfo(typeRegistry: TypeRegistry): MergeInfo { +function createMergeInfo( + schemas: { [name: string]: GraphQLSchema }, + fragmentReplacements: { + [name: string]: { [fieldName: string]: InlineFragmentNode }; + }, +): MergeInfo { return { + getSubSchema(schemaName: string): GraphQLSchema { + const schema = schemas[schemaName]; + if (!schema) { + throw new Error(`No subschema named ${schemaName}.`); + } + return schema; + }, delegate( + schemaName: string, operation: 'query' | 'mutation' | 'subscription', fieldName: string, args: { [key: string]: any }, context: { [key: string]: any }, info: GraphQLResolveInfo, ): any { - const schema = typeRegistry.getSchemaByField(operation, fieldName); + const schema = schemas[schemaName]; + const fragmentTransform = ReplaceFieldWithFragment( + schema, + fragmentReplacements, + ); if (!schema) { - throw new Error( - `Cannot find subschema for root field ${operation}.${fieldName}`, - ); + throw new Error(`No subschema named ${schemaName}.`); } - const fragmentReplacements = typeRegistry.fragmentReplacements; return delegateToSchema( schema, - fragmentReplacements, operation, fieldName, args, context, info, + [fragmentTransform], ); }, }; } function createDelegatingResolver( - mergeInfo: MergeInfo, + schemaName: string, operation: 'query' | 'mutation' | 'subscription', fieldName: string, ): IFieldResolver { return (root, args, context, info) => { - return mergeInfo.delegate(operation, fieldName, args, context, info); + return info.mergeInfo.delegate( + schemaName, + operation, + fieldName, + args, + context, + info, + ); }; } +type FieldIteratorFn = ( + fieldDef: GraphQLField, + typeName: string, + fieldName: string, +) => void; + +function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach(typeName => { + const type = typeMap[typeName]; + + if ( + !getNamedType(type).name.startsWith('__') && + type instanceof GraphQLObjectType + ) { + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + const field = fields[fieldName]; + fn(field, typeName, fieldName); + }); + } + }); +} + function isObject(item: any): Boolean { return item && typeof item === 'object' && !Array.isArray(item); } @@ -319,26 +342,79 @@ function mergeDeep(target: any, source: any): any { return output; } -type FieldIteratorFn = ( - fieldDef: GraphQLField, - typeName: string, - fieldName: string, -) => void; +function parseFragmentToInlineFragment( + definitions: string, +): InlineFragmentNode { + const document = parse(definitions); + for (const definition of document.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + return { + kind: Kind.INLINE_FRAGMENT, + typeCondition: definition.typeCondition, + selectionSet: definition.selectionSet, + }; + } + } + throw new Error('Could not parse fragment'); +} -function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach(typeName => { - const type = typeMap[typeName]; +function addTypeCandidate( + typeCandidates: { [name: string]: Array }, + name: string, + typeCandidate: MergeTypeCandidate, +) { + if (!typeCandidates[name]) { + typeCandidates[name] = []; + } + typeCandidates[name].push(typeCandidate); +} - if ( - !getNamedType(type).name.startsWith('__') && - type instanceof GraphQLObjectType - ) { - const fields = type.getFields(); - Object.keys(fields).forEach(fieldName => { - const field = fields[fieldName]; - fn(field, typeName, fieldName); - }); +const defaultVisitType: VisitType = ( + name: string, + candidates: Array, +) => { + const resolveType = createResolveType((_, type) => type); + if (name === 'Query' || name === 'Mutation' || name === 'Subscription') { + let fields = {}; + let operationName: 'query' | 'mutation' | 'subscription'; + switch (name) { + case 'Query': + operationName = 'query'; + break; + case 'Mutation': + operationName = 'mutation'; + break; + case 'Subscription': + operationName = 'subscription'; + break; + default: + break; } - }); -} + const resolvers = {}; + const resolverKey = + operationName === 'subscription' ? 'subscribe' : 'resolve'; + candidates.forEach(({ type: candidateType, schemaName }) => { + const candidateFields = (candidateType as GraphQLObjectType).getFields(); + fields = { ...fields, ...candidateFields }; + Object.keys(candidateFields).forEach(fieldName => { + resolvers[fieldName] = { + [resolverKey]: createDelegatingResolver( + schemaName, + operationName, + fieldName, + ), + }; + }); + }); + const type = new GraphQLObjectType({ + name, + fields: fieldMapToFieldConfigMap(fields, resolveType), + }); + return { + type, + resolvers, + }; + } else { + return candidates[candidates.length - 1].type; + } +}; diff --git a/src/stitching/resolvers.ts b/src/stitching/resolvers.ts new file mode 100644 index 00000000000..fc89a9e0955 --- /dev/null +++ b/src/stitching/resolvers.ts @@ -0,0 +1,103 @@ +import { + GraphQLSchema, + GraphQLFieldResolver, + GraphQLObjectType, +} from 'graphql'; +import { IResolvers, Operation } from '../Interfaces'; +import delegateToSchema from './delegateToSchema'; +import { Transform } from '../transforms/index'; + +export type Mapping = { + [typeName: string]: { + [fieldName: string]: { + name: string; + operation: Operation; + }; + }; +}; + +export function generateProxyingResolvers( + targetSchema: GraphQLSchema, + transforms: Array, + mapping: Mapping, +): IResolvers { + const result = {}; + Object.keys(mapping).forEach(name => { + result[name] = {}; + const innerMapping = mapping[name]; + Object.keys(innerMapping).forEach(from => { + const to = innerMapping[from]; + const resolverType = + to.operation === 'subscription' ? 'subscribe' : 'resolve'; + result[name][from] = { + [resolverType]: createProxyingResolver( + targetSchema, + to.operation, + to.name, + transforms, + ), + }; + }); + }); + return result; +} + +export function generateSimpleMapping(targetSchema: GraphQLSchema): Mapping { + const query = targetSchema.getQueryType(); + const mutation = targetSchema.getMutationType(); + const subscription = targetSchema.getSubscriptionType(); + + const result: Mapping = {}; + if (query) { + result[query.name] = generateMappingFromObjectType(query, 'query'); + } + if (mutation) { + result[mutation.name] = generateMappingFromObjectType(mutation, 'mutation'); + } + if (subscription) { + result[subscription.name] = generateMappingFromObjectType( + subscription, + 'subscription', + ); + } + + return result; +} + +function generateMappingFromObjectType( + type: GraphQLObjectType, + operation: Operation, +): { + [fieldName: string]: { + name: string; + operation: Operation; + }; +} { + const result = {}; + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + result[fieldName] = { + name: fieldName, + operation, + }; + }); + return result; +} + +function createProxyingResolver( + targetSchema: GraphQLSchema, + targetOperation: Operation, + targetField: string, + transforms: Array, +): GraphQLFieldResolver { + return (parent, args, context, info) => + delegateToSchema( + targetSchema, + targetOperation, + targetField, + {}, + context, + info, + transforms, + ); +} diff --git a/src/stitching/schemaRecreation.ts b/src/stitching/schemaRecreation.ts index 0e4e3983850..f08f48a41c0 100644 --- a/src/stitching/schemaRecreation.ts +++ b/src/stitching/schemaRecreation.ts @@ -1,31 +1,43 @@ import { GraphQLArgument, GraphQLArgumentConfig, - GraphQLCompositeType, + GraphQLBoolean, + GraphQLEnumType, GraphQLField, GraphQLFieldConfig, GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLFieldMap, + GraphQLFloat, + GraphQLID, GraphQLInputField, GraphQLInputFieldConfig, GraphQLInputFieldConfigMap, GraphQLInputFieldMap, GraphQLInputObjectType, + GraphQLInt, GraphQLInterfaceType, + GraphQLList, + GraphQLNamedType, + GraphQLNonNull, GraphQLObjectType, - GraphQLSchema, + GraphQLScalarType, + GraphQLString, + GraphQLType, GraphQLUnionType, + Kind, + ValueNode, + getNamedType, + isNamedType, } from 'graphql'; -import TypeRegistry from './TypeRegistry'; +import { ResolveType } from '../Interfaces'; import resolveFromParentTypename from './resolveFromParentTypename'; import defaultMergedResolver from './defaultMergedResolver'; -export function recreateCompositeType( - schema: GraphQLSchema, - type: GraphQLCompositeType | GraphQLInputObjectType, - registry: TypeRegistry, -): GraphQLCompositeType | GraphQLInputObjectType { +export function recreateType( + type: GraphQLNamedType, + resolveType: ResolveType, +): GraphQLNamedType { if (type instanceof GraphQLObjectType) { const fields = type.getFields(); const interfaces = type.getInterfaces(); @@ -33,9 +45,9 @@ export function recreateCompositeType( return new GraphQLObjectType({ name: type.name, description: type.description, - isTypeOf: type.isTypeOf, - fields: () => fieldMapToFieldConfigMap(fields, registry), - interfaces: () => interfaces.map(iface => registry.resolveType(iface)), + astNode: type.astNode, + fields: () => fieldMapToFieldConfigMap(fields, resolveType), + interfaces: () => interfaces.map(iface => resolveType(iface)), }); } else if (type instanceof GraphQLInterfaceType) { const fields = type.getFields(); @@ -43,7 +55,8 @@ export function recreateCompositeType( return new GraphQLInterfaceType({ name: type.name, description: type.description, - fields: () => fieldMapToFieldConfigMap(fields, registry), + astNode: type.astNode, + fields: () => fieldMapToFieldConfigMap(fields, resolveType), resolveType: (parent, context, info) => resolveFromParentTypename(parent, info.schema), }); @@ -51,8 +64,9 @@ export function recreateCompositeType( return new GraphQLUnionType({ name: type.name, description: type.description, - types: () => - type.getTypes().map(unionMember => registry.resolveType(unionMember)), + astNode: type.astNode, + + types: () => type.getTypes().map(unionMember => resolveType(unionMember)), resolveType: (parent, context, info) => resolveFromParentTypename(parent, info.schema), }); @@ -60,44 +74,142 @@ export function recreateCompositeType( return new GraphQLInputObjectType({ name: type.name, description: type.description, - fields: () => inputFieldMapToFieldConfigMap(type.getFields(), registry), + astNode: type.astNode, + + fields: () => + inputFieldMapToFieldConfigMap(type.getFields(), resolveType), }); + } else if (type instanceof GraphQLEnumType) { + const values = type.getValues(); + const newValues = {}; + values.forEach(value => { + newValues[value.name] = { value: value.name }; + }); + return new GraphQLEnumType({ + name: type.name, + description: type.description, + astNode: type.astNode, + values: newValues, + }); + } else if (type instanceof GraphQLScalarType) { + if ( + type === GraphQLID || + type === GraphQLString || + type === GraphQLFloat || + type === GraphQLBoolean || + type === GraphQLInt + ) { + return type; + } else { + return new GraphQLScalarType({ + name: type.name, + description: type.description, + astNode: type.astNode, + serialize(value: any) { + return value; + }, + parseValue(value: any) { + return value; + }, + parseLiteral(ast: ValueNode) { + return parseLiteral(ast); + }, + }); + } } else { throw new Error(`Invalid type ${type}`); } } +function parseLiteral(ast: ValueNode): any { + switch (ast.kind) { + case Kind.STRING: + case Kind.BOOLEAN: { + return ast.value; + } + case Kind.INT: + case Kind.FLOAT: { + return parseFloat(ast.value); + } + case Kind.OBJECT: { + const value = Object.create(null); + ast.fields.forEach(field => { + value[field.name.value] = parseLiteral(field.value); + }); + + return value; + } + case Kind.LIST: { + return ast.values.map(parseLiteral); + } + default: + return null; + } +} + export function fieldMapToFieldConfigMap( fields: GraphQLFieldMap, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLFieldConfigMap { const result: GraphQLFieldConfigMap = {}; Object.keys(fields).forEach(name => { - result[name] = fieldToFieldConfig(fields[name], registry); + const field = fields[name]; + const type = resolveType(field.type); + if (type !== null) { + result[name] = fieldToFieldConfig(fields[name], resolveType); + } }); return result; } +export function createResolveType( + getType: (name: string, type: GraphQLType) => GraphQLType | null, +): ResolveType { + const resolveType = (type: T): T => { + if (type instanceof GraphQLList) { + const innerType = resolveType(type.ofType); + if (innerType === null) { + return null; + } else { + return new GraphQLList(innerType) as T; + } + } else if (type instanceof GraphQLNonNull) { + const innerType = resolveType(type.ofType); + if (innerType === null) { + return null; + } else { + return new GraphQLNonNull(innerType) as T; + } + } else if (isNamedType(type)) { + return getType(getNamedType(type).name, type) as T; + } else { + return type; + } + }; + return resolveType; +} + function fieldToFieldConfig( field: GraphQLField, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLFieldConfig { return { - type: registry.resolveType(field.type), - args: argsToFieldConfigArgumentMap(field.args, registry), + type: resolveType(field.type), + args: argsToFieldConfigArgumentMap(field.args, resolveType), resolve: defaultMergedResolver, description: field.description, deprecationReason: field.deprecationReason, + astNode: field.astNode, }; } function argsToFieldConfigArgumentMap( args: Array, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLFieldConfigArgumentMap { const result: GraphQLFieldConfigArgumentMap = {}; args.forEach(arg => { - const [name, def] = argumentToArgumentConfig(arg, registry); + const [name, def] = argumentToArgumentConfig(arg, resolveType); result[name] = def; }); return result; @@ -105,12 +217,12 @@ function argsToFieldConfigArgumentMap( function argumentToArgumentConfig( argument: GraphQLArgument, - registry: TypeRegistry, + resolveType: ResolveType, ): [string, GraphQLArgumentConfig] { return [ argument.name, { - type: registry.resolveType(argument.type), + type: resolveType(argument.type), defaultValue: argument.defaultValue, description: argument.description, }, @@ -119,22 +231,27 @@ function argumentToArgumentConfig( function inputFieldMapToFieldConfigMap( fields: GraphQLInputFieldMap, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLInputFieldConfigMap { const result: GraphQLInputFieldConfigMap = {}; Object.keys(fields).forEach(name => { - result[name] = inputFieldToFieldConfig(fields[name], registry); + const field = fields[name]; + const type = resolveType(field.type); + if (type !== null) { + result[name] = inputFieldToFieldConfig(fields[name], resolveType); + } }); return result; } function inputFieldToFieldConfig( field: GraphQLInputField, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLInputFieldConfig { return { - type: registry.resolveType(field.type), + type: resolveType(field.type), defaultValue: field.defaultValue, description: field.description, + astNode: field.astNode, }; } diff --git a/src/stitching/typeFromAST.ts b/src/stitching/typeFromAST.ts index 63458ab7a2d..ebeb30bea9d 100644 --- a/src/stitching/typeFromAST.ts +++ b/src/stitching/typeFromAST.ts @@ -23,60 +23,60 @@ import { UnionTypeDefinitionNode, valueFromAST, } from 'graphql'; -// -// TODO put back import once PR is merged -// https://github.com/graphql/graphql-js/pull/1165 -// import { getDescription } from 'graphql/utilities/buildASTSchema'; +import resolveFromParentType from './resolveFromParentTypename'; const backcompatOptions = { commentDescriptions: true }; -import resolveFromParentType from './resolveFromParentTypename'; -import TypeRegistry from './TypeRegistry'; +export type GetType = ( + name: string, + // this is a hack + type: 'object' | 'interface' | 'input', +) => GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType; export default function typeFromAST( - typeRegistry: TypeRegistry, node: DefinitionNode, + getType: GetType, ): GraphQLNamedType | null { switch (node.kind) { case Kind.OBJECT_TYPE_DEFINITION: - return makeObjectType(typeRegistry, node); + return makeObjectType(node, getType); case Kind.INTERFACE_TYPE_DEFINITION: - return makeInterfaceType(typeRegistry, node); + return makeInterfaceType(node, getType); case Kind.ENUM_TYPE_DEFINITION: - return makeEnumType(typeRegistry, node); + return makeEnumType(node, getType); case Kind.UNION_TYPE_DEFINITION: - return makeUnionType(typeRegistry, node); + return makeUnionType(node, getType); case Kind.SCALAR_TYPE_DEFINITION: - return makeScalarType(typeRegistry, node); + return makeScalarType(node, getType); case Kind.INPUT_OBJECT_TYPE_DEFINITION: - return makeInputObjectType(typeRegistry, node); + return makeInputObjectType(node, getType); default: return null; } } function makeObjectType( - typeRegistry: TypeRegistry, node: ObjectTypeDefinitionNode, + getType: GetType, ): GraphQLObjectType { return new GraphQLObjectType({ name: node.name.value, - fields: () => makeFields(typeRegistry, node.fields), + fields: () => makeFields(node.fields, getType), interfaces: () => node.interfaces.map( - iface => typeRegistry.getType(iface.name.value) as GraphQLInterfaceType, + iface => getType(iface.name.value, 'interface') as GraphQLInterfaceType, ), description: getDescription(node, backcompatOptions), }); } function makeInterfaceType( - typeRegistry: TypeRegistry, node: InterfaceTypeDefinitionNode, + getType: GetType, ): GraphQLInterfaceType { return new GraphQLInterfaceType({ name: node.name.value, - fields: () => makeFields(typeRegistry, node.fields), + fields: () => makeFields(node.fields, getType), description: getDescription(node, backcompatOptions), resolveType: (parent, context, info) => resolveFromParentType(parent, info.schema), @@ -84,8 +84,8 @@ function makeInterfaceType( } function makeEnumType( - typeRegistry: TypeRegistry, node: EnumTypeDefinitionNode, + getType: GetType, ): GraphQLEnumType { const values = {}; node.values.forEach(value => { @@ -101,14 +101,14 @@ function makeEnumType( } function makeUnionType( - typeRegistry: TypeRegistry, node: UnionTypeDefinitionNode, + getType: GetType, ): GraphQLUnionType { return new GraphQLUnionType({ name: node.name.value, types: () => node.types.map( - type => resolveType(typeRegistry, type) as GraphQLObjectType, + type => resolveType(type, getType, 'object') as GraphQLObjectType, ), description: getDescription(node, backcompatOptions), resolveType: (parent, context, info) => @@ -117,8 +117,8 @@ function makeUnionType( } function makeScalarType( - typeRegistry: TypeRegistry, node: ScalarTypeDefinitionNode, + getType: GetType, ): GraphQLScalarType { return new GraphQLScalarType({ name: node.name.value, @@ -134,38 +134,32 @@ function makeScalarType( } function makeInputObjectType( - typeRegistry: TypeRegistry, node: InputObjectTypeDefinitionNode, + getType: GetType, ): GraphQLInputObjectType { return new GraphQLInputObjectType({ name: node.name.value, - fields: () => makeValues(typeRegistry, node.fields), + fields: () => makeValues(node.fields, getType), description: getDescription(node, backcompatOptions), }); } -function makeFields( - typeRegistry: TypeRegistry, - nodes: Array, -) { +function makeFields(nodes: Array, getType: GetType) { const result = {}; nodes.forEach(node => { result[node.name.value] = { - type: resolveType(typeRegistry, node.type), - args: makeValues(typeRegistry, node.arguments), + type: resolveType(node.type, getType, 'object'), + args: makeValues(node.arguments, getType), description: getDescription(node, backcompatOptions), }; }); return result; } -function makeValues( - typeRegistry: TypeRegistry, - nodes: Array, -) { +function makeValues(nodes: Array, getType: GetType) { const result = {}; nodes.forEach(node => { - const type = resolveType(typeRegistry, node.type) as GraphQLInputType; + const type = resolveType(node.type, getType, 'input') as GraphQLInputType; result[node.name.value] = { type, defaultValue: valueFromAST(node.defaultValue, type), @@ -175,14 +169,18 @@ function makeValues( return result; } -function resolveType(typeRegistry: TypeRegistry, node: TypeNode): GraphQLType { +function resolveType( + node: TypeNode, + getType: GetType, + type: 'object' | 'interface' | 'input', +): GraphQLType { switch (node.kind) { case Kind.LIST_TYPE: - return new GraphQLList(resolveType(typeRegistry, node.type)); + return new GraphQLList(resolveType(node.type, getType, type)); case Kind.NON_NULL_TYPE: - return new GraphQLNonNull(resolveType(typeRegistry, node.type)); + return new GraphQLNonNull(resolveType(node.type, getType, type)); default: - return typeRegistry.getType(node.name.value); + return getType(node.name.value, type); } } diff --git a/src/test/testMakeRemoteExecutableSchema.ts b/src/test/testMakeRemoteExecutableSchema.ts new file mode 100644 index 00000000000..7eb79dc6d05 --- /dev/null +++ b/src/test/testMakeRemoteExecutableSchema.ts @@ -0,0 +1,48 @@ +/* tslint:disable:no-unused-expression */ + +import { expect } from 'chai'; +import { forAwaitEach } from 'iterall'; +import { GraphQLSchema, ExecutionResult, subscribe, parse } from 'graphql'; +import { + subscriptionSchema, + subscriptionPubSubTrigger, + subscriptionPubSub, + makeSchemaRemoteFromLink, +} from '../test/testingSchemas'; + +describe('remote subscriptions', () => { + let schema: GraphQLSchema; + before(async () => { + schema = await makeSchemaRemoteFromLink(subscriptionSchema); + }); + + it('should work', done => { + const mockNotification = { + notifications: { + text: 'Hello world', + }, + }; + + const subscription = parse(` + subscription Subscription { + notifications { + text + } + } + `); + + let notificationCnt = 0; + subscribe(schema, subscription).then(results => + forAwaitEach( + results as AsyncIterable, + (result: ExecutionResult) => { + expect(result).to.have.property('data'); + expect(result.data).to.deep.equal(mockNotification); + !notificationCnt++ ? done() : null; + }, + ), + ); + + subscriptionPubSub.publish(subscriptionPubSubTrigger, mockNotification); + }); +}); diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index 01df3ef40a7..a055dafa458 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -4,19 +4,21 @@ import { expect } from 'chai'; import { graphql, GraphQLSchema, - GraphQLScalarType, GraphQLObjectType, subscribe, parse, ExecutionResult, } from 'graphql'; +import { VisitType } from '../Interfaces'; import mergeSchemas from '../stitching/mergeSchemas'; import { propertySchema as localPropertySchema, + productSchema as localProductSchema, bookingSchema as localBookingSchema, subscriptionSchema as localSubscriptionSchema, remoteBookingSchema, remotePropertySchema, + remoteProductSchema, subscriptionPubSub, subscriptionPubSubTrigger, } from './testingSchemas'; @@ -24,27 +26,40 @@ import { forAwaitEach } from 'iterall'; import { makeExecutableSchema } from '../schemaGenerator'; const testCombinations = [ - { name: 'local', booking: localBookingSchema, property: localPropertySchema }, + { + name: 'local', + booking: localBookingSchema, + property: localPropertySchema, + product: localProductSchema, + }, { name: 'remote', booking: remoteBookingSchema, property: remotePropertySchema, + product: remoteProductSchema, }, { name: 'hybrid', booking: localBookingSchema, property: remotePropertySchema, + product: localProductSchema, }, ]; -const scalarTest = ` - # Description of TestScalar. +let scalarTest = ` + """ + Description of TestScalar. + """ scalar TestScalar - # Description of AnotherNewScalar. + """ + Description of AnotherNewScalar. + """ scalar AnotherNewScalar - # A type that uses TestScalar. + """ + A type that uses TestScalar. + """ type TestingScalar { value: TestScalar } @@ -54,31 +69,60 @@ const scalarTest = ` } `; -const enumTest = ` -# A type that uses an Enum. -enum Color { - RED -} +let enumTest = ` + """ + A type that uses an Enum. + """ + enum Color { + RED + } -schema { - query: Query -} + """ + A type that uses an Enum with a numeric constant. + """ + enum NumericEnum { + TEST + } -type Query { - color: Color -} + schema { + query: Query + } + + type Query { + color: Color + numericEnum: NumericEnum + } `; -let graphql11compat = ''; -if (process.env.GRAPHQL_VERSION === '^0.11') { - graphql11compat = '{}'; -} +const enumSchema = makeExecutableSchema({ + typeDefs: enumTest, + resolvers: { + Color: { + RED: '#EA3232', + }, + NumericEnum: { + TEST: 1, + }, + Query: { + color() { + return '#EA3232'; + }, + numericEnum() { + return 1; + }, + }, + }, +}); -const linkSchema = ` - # A new type linking the Property type. +let linkSchema = ` + """ + A new type linking the Property type. + """ type LinkType { test: String - # The property. + """ + The property. + """ property: Property } @@ -86,16 +130,21 @@ const linkSchema = ` id: ID! } - extend type Booking implements Node { - # The property of the booking. + """ + The property of the booking. + """ property: Property } extend type Property implements Node { - # A list of bookings. + """ + A list of bookings. + """ bookings( - # The maximum number of bookings to retrieve. + """ + The maximum number of bookings to retrieve. + """ limit: Int ): [Booking] } @@ -103,50 +152,154 @@ const linkSchema = ` extend type Query { delegateInterfaceTest: TestInterface delegateArgumentTest(arbitraryArg: Int): Property - # A new field on the root query. + """ + A new field on the root query. + """ linkTest: LinkType node(id: ID!): Node nodes: [Node] } - extend type Customer implements Node ${graphql11compat} + extend type Customer implements Node `; +const loneExtend = ` + extend type Booking { + foo: String! + } +`; + +if (process.env.GRAPHQL_VERSION === '^0.11') { + scalarTest = ` + # Description of TestScalar. + scalar TestScalar + + # Description of AnotherNewScalar. + scalar AnotherNewScalar + + # A type that uses TestScalar. + type TestingScalar { + value: TestScalar + } + + type Query { + testingScalar: TestingScalar + } + `; + + enumTest = ` + # A type that uses an Enum. + enum Color { + RED + } + + # A type that uses an Enum with a numeric constant. + enum NumericEnum { + TEST + } + + schema { + query: Query + } + + type Query { + color: Color + numericEnum: NumericEnum + } + `; + + linkSchema = ` + # A new type linking the Property type. + type LinkType { + test: String + # The property. + property: Property + } + + interface Node { + id: ID! + } + + extend type Booking implements Node { + # The property of the booking. + property: Property + } + + extend type Property implements Node { + # A list of bookings. + bookings( + # The maximum number of bookings to retrieve. + limit: Int + ): [Booking] + } + + extend type Query { + delegateInterfaceTest: TestInterface + delegateArgumentTest(arbitraryArg: Int): Property + # A new field on the root query. + linkTest: LinkType + node(id: ID!): Node + nodes: [Node] + } + + extend type Customer implements Node {} + `; +} + testCombinations.forEach(async combination => { describe('merging ' + combination.name, () => { let mergedSchema: GraphQLSchema, propertySchema: GraphQLSchema, + productSchema: GraphQLSchema, bookingSchema: GraphQLSchema; before(async () => { propertySchema = await combination.property; bookingSchema = await combination.booking; + productSchema = await combination.product; mergedSchema = mergeSchemas({ schemas: [ - propertySchema, - bookingSchema, - scalarTest, - enumTest, - linkSchema, - localSubscriptionSchema, + { + name: 'Property', + schema: propertySchema, + }, + { + name: 'Booking', + schema: bookingSchema, + }, + { + name: 'Product', + schema: productSchema, + }, + { + name: 'ScalarTest', + schema: scalarTest, + }, + { + name: 'EnumTest', + schema: enumSchema, + }, + { + name: 'LinkSchema', + schema: linkSchema, + }, + { + name: 'LoneExtend', + schema: loneExtend, + }, + { + name: 'LocalSubscription', + schema: localSubscriptionSchema, + }, ], resolvers: { - TestScalar: new GraphQLScalarType({ - name: 'TestScalar', - description: undefined, - serialize: value => value, - parseValue: value => value, - parseLiteral: () => null, - }), - Color: { - RED: '#EA3232', - }, Property: { bookings: { fragment: 'fragment PropertyFragment on Property { id }', resolve(parent, args, context, info) { return info.mergeInfo.delegate( + 'Booking', 'query', 'bookingsByPropertyId', { @@ -164,6 +317,7 @@ testCombinations.forEach(async combination => { fragment: 'fragment BookingFragment on Booking { propertyId }', resolve(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', { @@ -179,6 +333,7 @@ testCombinations.forEach(async combination => { property: { resolve(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', { @@ -191,11 +346,9 @@ testCombinations.forEach(async combination => { }, }, Query: { - color() { - return '#EA3232'; - }, delegateInterfaceTest(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'interfaceTest', { @@ -207,6 +360,7 @@ testCombinations.forEach(async combination => { }, delegateArgumentTest(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', { @@ -227,6 +381,7 @@ testCombinations.forEach(async combination => { resolve(parent, args, context, info) { if (args.id.startsWith('p')) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', args, @@ -235,6 +390,7 @@ testCombinations.forEach(async combination => { ); } else if (args.id.startsWith('b')) { return info.mergeInfo.delegate( + 'Booking', 'query', 'bookingById', args, @@ -243,6 +399,7 @@ testCombinations.forEach(async combination => { ); } else if (args.id.startsWith('c')) { return info.mergeInfo.delegate( + 'Booking', 'query', 'customerById', args, @@ -256,6 +413,7 @@ testCombinations.forEach(async combination => { }, async nodes(parent, args, context, info) { const bookings = await info.mergeInfo.delegate( + 'Booking', 'query', 'bookings', {}, @@ -263,6 +421,7 @@ testCombinations.forEach(async combination => { info, ); const properties = await info.mergeInfo.delegate( + 'Property', 'query', 'properties', {}, @@ -350,24 +509,12 @@ testCombinations.forEach(async combination => { }); it('works with custom enums', async () => { - const enumSchema = makeExecutableSchema({ - typeDefs: enumTest, - resolvers: { - Color: { - RED: '#EA3232', - }, - Query: { - color() { - return '#EA3232'; - }, - }, - }, - }); const enumResult = await graphql( enumSchema, ` query { color + numericEnum } `, ); @@ -377,6 +524,7 @@ testCombinations.forEach(async combination => { ` query { color + numericEnum } `, ); @@ -384,6 +532,7 @@ testCombinations.forEach(async combination => { expect(enumResult).to.deep.equal({ data: { color: 'RED', + numericEnum: 'TEST', }, }); expect(mergedResult).to.deep.equal(enumResult); @@ -1010,21 +1159,21 @@ bookingById(id: "b1") { describe('variables', () => { it('basic', async () => { const propertyFragment = ` -propertyById(id: $p1) { - id - name -} - `; + propertyById(id: $p1) { + id + name + } + `; const bookingFragment = ` -bookingById(id: $b1) { - id - customer { - name - } - startTime - endTime -} - `; + bookingById(id: $b1) { + id + customer { + name + } + startTime + endTime + } + `; const propertyResult = await graphql( propertySchema, @@ -1447,10 +1596,6 @@ bookingById(id: $b1) { describe('types in schema extensions', () => { it('should parse descriptions on new types', () => { - // Because we redefine it via `GraphQLScalarType` above, it will get - // its description from there. - expect(mergedSchema.getType('TestScalar').description).to.be.undefined; - expect(mergedSchema.getType('AnotherNewScalar').description).to.equal( 'Description of AnotherNewScalar.', ); @@ -1463,6 +1608,10 @@ bookingById(id: $b1) { 'A type that uses an Enum.', ); + expect(mergedSchema.getType('NumericEnum').description).to.equal( + 'A type that uses an Enum with a numeric constant.', + ); + expect(mergedSchema.getType('LinkType').description).to.equal( 'A new type linking the Property type.', ); @@ -1694,6 +1843,39 @@ bookingById(id: $b1) { // }); // }); + it('multi-interface filter', async () => { + const result = await graphql( + mergedSchema, + ` + query { + products { + id + __typename + ... on Sellable { + price + } + } + } + `, + ); + + expect(result).to.deep.equal({ + data: { + products: [ + { + id: 'pd1', + __typename: 'SimpleProduct', + price: 100, + }, + { + id: 'pd2', + __typename: 'DownloadableProduct', + }, + ], + }, + }); + }); + it('arbitrary transforms that return interfaces', async () => { const result = await graphql( mergedSchema, @@ -1786,3 +1968,118 @@ bookingById(id: $b1) { }); }); }); + +describe('mergeSchema options', () => { + describe('should filter types', () => { + let schema: GraphQLSchema; + + before(async () => { + const bookingSchema = await remoteBookingSchema; + const createTypeFilteringVisitTypes = ( + typeNames: Array, + ): VisitType => { + return (name, candidates) => { + if ( + ['ID', 'String', 'DateTime'].includes(name) || + typeNames.includes(name) + ) { + return candidates[candidates.length - 1].type; + } else { + return null; + } + }; + }; + schema = mergeSchemas({ + schemas: [ + { + name: 'Booking', + schema: bookingSchema, + }, + { + name: 'Selector', + schema: ` + type Query { + bookingById(id: ID!): Booking + }, + `, + }, + ], + visitType: createTypeFilteringVisitTypes(['Query', 'Booking']), + resolvers: { + Query: { + bookingById(parent, args, context, info) { + return info.mergeInfo.delegate( + 'Booking', + 'query', + 'bookingById', + args, + context, + info, + ); + }, + }, + }, + }); + }); + + it('should work normally', async () => { + const result = await graphql( + schema, + ` + query { + bookingById(id: "b1") { + id + propertyId + startTime + endTime + } + } + `, + ); + + expect(result).to.deep.equal({ + data: { + bookingById: { + endTime: '2016-06-03', + id: 'b1', + propertyId: 'p1', + startTime: '2016-05-04', + }, + }, + }); + }); + + it('should error on removed types', async () => { + const result = await graphql( + schema, + ` + query { + bookingById(id: "b1") { + id + propertyId + startTime + endTime + customer { + id + } + } + } + `, + ); + expect(result).to.deep.equal({ + errors: [ + { + locations: [ + { + column: 15, + line: 8, + }, + ], + message: 'Cannot query field "customer" on type "Booking".', + path: undefined, + }, + ], + }); + }); + }); +}); diff --git a/src/test/testSchemaGenerator.ts b/src/test/testSchemaGenerator.ts index f0e1502f6a6..738582dd059 100644 --- a/src/test/testSchemaGenerator.ts +++ b/src/test/testSchemaGenerator.ts @@ -148,8 +148,10 @@ describe('generating schema from shorthand', () => { }); it('can generate a schema', () => { - const shorthand = ` - # A bird species + let shorthand = ` + """ + A bird species + """ type BirdSpecies { name: String!, wingspan: Int @@ -163,6 +165,23 @@ describe('generating schema from shorthand', () => { } `; + if (process.env.GRAPHQL_VERSION === '^0.11') { + shorthand = ` + # A bird species + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + species(name: String!): [BirdSpecies] + } + + schema { + query: RootQuery + } + `; + } + const resolve = { RootQuery: { species() { @@ -871,12 +890,17 @@ describe('generating schema from shorthand', () => { RED } + enum NumericEnum { + TEST + } + schema { query: Query } type Query { color: Color + numericEnum: NumericEnum } `; @@ -884,6 +908,9 @@ describe('generating schema from shorthand', () => { Color: { RED: '#EA3232', }, + NumericEnum: { + TEST: 1 + } }; const jsSchema = makeExecutableSchema({ @@ -893,6 +920,7 @@ describe('generating schema from shorthand', () => { expect(jsSchema.getQueryType().name).to.equal('Query'); expect(jsSchema.getType('Color')).to.be.an.instanceof(GraphQLEnumType); + expect(jsSchema.getType('NumericEnum')).to.be.an.instanceof(GraphQLEnumType); }); it('supports passing the value for a GraphQLEnumType in resolveFunctions', () => { @@ -901,27 +929,39 @@ describe('generating schema from shorthand', () => { RED } + enum NumericEnum { + TEST + } + schema { query: Query } type Query { color: Color + numericEnum: NumericEnum } `; const testQuery = `{ color + numericEnum }`; const resolveFunctions = { Color: { RED: '#EA3232', }, + NumericEnum: { + TEST: 1, + }, Query: { color() { return '#EA3232'; }, + numericEnum() { + return 1; + } }, }; @@ -933,6 +973,7 @@ describe('generating schema from shorthand', () => { const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then(result => { assert.equal(result.data['color'], 'RED'); + assert.equal(result.data['numericEnum'], 'TEST'); assert.equal(result.errors, undefined); }); }); diff --git a/src/test/testTransforms.ts b/src/test/testTransforms.ts new file mode 100644 index 00000000000..3505a8072a7 --- /dev/null +++ b/src/test/testTransforms.ts @@ -0,0 +1,129 @@ +/* tslint:disable:no-unused-expression */ + +import { expect } from 'chai'; +import { + visit, + GraphQLSchema, + NamedTypeNode, + Kind, + GraphQLNamedType, + graphql, +} from 'graphql'; +import { Request } from '../Interfaces'; +import { Transform } from '../transforms/transforms'; +import { visitSchema, VisitSchemaKind } from '../transforms/visitSchema'; +import { propertySchema } from './testingSchemas'; +import makeSimpleTransformSchema from '../transforms/makeSimpleTransformSchema'; + +function RenameTypes(renameMap: { [originalName: string]: string }): Transform { + const reverseMap = {}; + Object.keys(renameMap).map(from => { + reverseMap[renameMap[from]] = from; + }); + return { + transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return visitSchema(originalSchema, { + [VisitSchemaKind.TYPE]( + type: GraphQLNamedType, + ): GraphQLNamedType | undefined { + if (type.name in renameMap) { + const newType = Object.assign(Object.create(type), type); + newType.name = renameMap[type.name]; + return newType; + } + }, + }); + }, + + transformRequest(originalRequest: Request): Request { + const newDocument = visit(originalRequest.document, { + [Kind.NAMED_TYPE](node: NamedTypeNode): NamedTypeNode | undefined { + const name = node.name.value; + if (name in reverseMap) { + return { + ...node, + name: { + kind: Kind.NAME, + value: reverseMap[name], + }, + }; + } + }, + }); + return { + document: newDocument, + variables: originalRequest.variables, + }; + }, + }; +} + +// function NamespaceSchema(namespace: string): Transform { +// return { +// transformSchema();, +// }; +// } + +// function importFromSchema(importString: string) {} +// +// + +describe('transforms', () => { + describe('rename type', () => { + let schema: GraphQLSchema; + before(() => { + const transforms = [ + RenameTypes({ + Property: 'House', + Location: 'Spots', + TestInterface: 'TestingInterface', + DateTime: 'Datum', + InputWithDefault: 'DefaultingInput', + TestInterfaceKind: 'TestingInterfaceKinds', + }), + ]; + schema = makeSimpleTransformSchema(propertySchema, transforms); + }); + it('should work', async () => { + const result = await graphql( + schema, + ` + query($input: DefaultingInput!) { + interfaceTest(kind: ONE) { + ... on TestingInterface { + testString + } + } + propertyById(id: "p1") { + ... on House { + id + } + } + dateTimeTest + defaultInputTest(input: $input) + } + `, + {}, + {}, + { + input: { + test: 'bar', + }, + }, + ); + + expect(result).to.deep.equal({ + data: { + dateTimeTest: '1987-09-25T12:00:00', + defaultInputTest: 'bar', + interfaceTest: { + testString: 'test', + }, + propertyById: { + id: 'p1', + }, + }, + }); + }); + }); +}); diff --git a/src/test/testingSchemas.ts b/src/test/testingSchemas.ts index 54d91663b77..70cf93c1692 100644 --- a/src/test/testingSchemas.ts +++ b/src/test/testingSchemas.ts @@ -2,9 +2,11 @@ import { GraphQLSchema, graphql, print, + subscribe, Kind, GraphQLScalarType, ValueNode, + ExecutionResult, } from 'graphql'; import { ApolloLink, Observable } from 'apollo-link'; import { makeExecutableSchema } from '../schemaGenerator'; @@ -23,6 +25,13 @@ export type Property = { }; }; +export type Product = { + id: string; + price?: number; + url?: string, + type: string, +}; + export type Booking = { id: string; propertyId: string; @@ -47,10 +56,23 @@ export type Vehicle = { export const sampleData: { Property: { [key: string]: Property }; + Product: { [key: string]: Product }; Booking: { [key: string]: Booking }; Customer: { [key: string]: Customer }; Vehicle: { [key: string]: Vehicle }; } = { + Product: { + pd1: { + id: 'pd1', + type: 'simple', + price: 100, + }, + pd2: { + id: 'pd2', + type: 'download', + url: 'https://graphql.org', + }, + }, Property: { p1: { id: 'p1', @@ -347,6 +369,53 @@ const propertyResolvers: IResolvers = { }, }; +const productTypeDefs = ` + interface Product { + id: ID! + } + + interface Sellable { + price: Int! + } + + interface Downloadable { + url: String! + } + + type SimpleProduct implements Product, Sellable { + id: ID! + price: Int! + } + + type DownloadableProduct implements Product, Downloadable { + id: ID! + url: String! + } + + type Query { + products: [Product] + } +`; + +const productResolvers: IResolvers = { + Query: { + products(root) { + const list = values(sampleData.Product); + return list; + }, + }, + + Product: { + __resolveType(obj) { + if (obj.type === 'simple') { + return 'SimpleProduct'; + } else { + return 'DownloadableProduct'; + } + }, + }, +}; + const customerAddressTypeDef = ` type Customer implements Person { id: ID! @@ -549,6 +618,11 @@ export const propertySchema: GraphQLSchema = makeExecutableSchema({ resolvers: propertyResolvers, }); +export const productSchema: GraphQLSchema = makeExecutableSchema({ + typeDefs: productTypeDefs, + resolvers: productResolvers, +}); + export const bookingSchema: GraphQLSchema = makeExecutableSchema({ typeDefs: bookingAddressTypeDefs, resolvers: bookingResolvers, @@ -559,25 +633,69 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ resolvers: subscriptionResolvers, }); +const hasSubscriptionOperation = ({ query }: { query: any }): boolean => { + for (let definition of query.definitions) { + if (definition.kind === 'OperationDefinition') { + const operation = definition.operation; + if (operation === 'subscription') { + return true; + } + } + } + return false; +}; + // Pretend this schema is remote -async function makeSchemaRemoteFromLink(schema: GraphQLSchema) { +export async function makeSchemaRemoteFromLink(schema: GraphQLSchema) { const link = new ApolloLink(operation => { return new Observable(observer => { - const { query, operationName, variables } = operation; - const { graphqlContext } = operation.getContext(); - graphql( - schema, - print(query), - null, - graphqlContext, - variables, - operationName, - ) - .then(result => { - observer.next(result); - observer.complete(); - }) - .catch(observer.error.bind(observer)); + (async () => { + const { query, operationName, variables } = operation; + const { graphqlContext } = operation.getContext(); + try { + if (!hasSubscriptionOperation(operation)) { + const result = await graphql( + schema, + print(query), + null, + graphqlContext, + variables, + operationName, + ); + observer.next(result); + observer.complete(); + } else { + const result = await subscribe( + schema, + query, + null, + graphqlContext, + variables, + operationName, + ); + if ( + typeof (>result).next === + 'function' + ) { + while (true) { + const next = await (>result).next(); + observer.next(next.value); + if (next.done) { + observer.complete(); + break; + } + } + } else { + observer.next(result as ExecutionResult); + observer.complete(); + } + } + } catch (error) { + observer.error.bind(observer); + } + })(); }); }); @@ -602,6 +720,7 @@ async function makeExecutableSchemaFromFetcher(schema: GraphQLSchema) { } export const remotePropertySchema = makeSchemaRemoteFromLink(propertySchema); +export const remoteProductSchema = makeSchemaRemoteFromLink(productSchema); export const remoteBookingSchema = makeExecutableSchemaFromFetcher( bookingSchema, ); diff --git a/src/test/tests.ts b/src/test/tests.ts index f12e88f0964..b767315c5db 100755 --- a/src/test/tests.ts +++ b/src/test/tests.ts @@ -4,4 +4,6 @@ import './testSchemaGenerator'; import './testLogger'; import './testMocking'; import './testResolution'; +import './testMakeRemoteExecutableSchema'; import './testMergeSchemas'; +import './testTransforms'; diff --git a/src/transforms/AddArgumentsAsVariables.ts b/src/transforms/AddArgumentsAsVariables.ts new file mode 100644 index 00000000000..232b416d3c3 --- /dev/null +++ b/src/transforms/AddArgumentsAsVariables.ts @@ -0,0 +1,196 @@ +import { + ArgumentNode, + DocumentNode, + FragmentDefinitionNode, + GraphQLArgument, + GraphQLInputType, + GraphQLList, + GraphQLField, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + Kind, + OperationDefinitionNode, + SelectionNode, + TypeNode, + VariableDefinitionNode, +} from 'graphql'; +import { Request } from '../Interfaces'; +import { Transform } from './transforms'; + +export default function AddArgumentsAsVariablesTransform( + schema: GraphQLSchema, + args: { [key: string]: any }, +): Transform { + return { + transformRequest(originalRequest: Request): Request { + const { document, newVariables } = addVariablesToRootField( + schema, + originalRequest.document, + args, + ); + const variables = { + ...originalRequest.variables, + ...newVariables, + }; + return { + document, + variables, + }; + }, + }; +} + +function addVariablesToRootField( + targetSchema: GraphQLSchema, + document: DocumentNode, + args: { [key: string]: any }, +): { + document: DocumentNode; + newVariables: { [key: string]: any }; +} { + const operations: Array< + OperationDefinitionNode + > = document.definitions.filter( + def => def.kind === Kind.OPERATION_DEFINITION, + ) as Array; + const fragments: Array = document.definitions.filter( + def => def.kind === Kind.FRAGMENT_DEFINITION, + ) as Array; + + const variableNames = {}; + + const newOperations = operations.map((operation: OperationDefinitionNode) => { + let existingVariables = operation.variableDefinitions.map( + (variableDefinition: VariableDefinitionNode) => + variableDefinition.variable.name.value, + ); + + let variableCounter = 0; + const variables = {}; + + const generateVariableName = (argName: string) => { + let varName; + do { + varName = `_v${variableCounter}_${argName}`; + variableCounter++; + } while (existingVariables.indexOf(varName) !== -1); + return varName; + }; + + let type: GraphQLObjectType; + if (operation.operation === 'subscription') { + type = targetSchema.getSubscriptionType(); + } else if (operation.operation === 'mutation') { + type = targetSchema.getMutationType(); + } else { + type = targetSchema.getQueryType(); + } + + const newSelectionSet: Array = []; + + operation.selectionSet.selections.forEach((selection: SelectionNode) => { + if (selection.kind === Kind.FIELD) { + let newArgs: { [name: string]: ArgumentNode } = {}; + selection.arguments.forEach((argument: ArgumentNode) => { + newArgs[argument.name.value] = argument; + }); + const name: string = selection.name.value; + const field: GraphQLField = type.getFields()[name]; + field.args.forEach((argument: GraphQLArgument) => { + if (argument.name in args) { + const variableName = generateVariableName(argument.name); + variableNames[argument.name] = variableName; + newArgs[argument.name] = { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: argument.name, + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: variableName, + }, + }, + }; + existingVariables.push(variableName); + variables[variableName] = { + kind: Kind.VARIABLE_DEFINITION, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: variableName, + }, + }, + type: typeToAst(argument.type), + }; + } + }); + + newSelectionSet.push({ + ...selection, + arguments: Object.keys(newArgs).map(argName => newArgs[argName]), + }); + } else { + newSelectionSet.push(selection); + } + }); + + return { + ...operation, + variableDefinitions: operation.variableDefinitions.concat( + Object.keys(variables).map(varName => variables[varName]), + ), + selectionSet: { + kind: Kind.SELECTION_SET, + selections: newSelectionSet, + }, + }; + }); + + const newVariables = {}; + Object.keys(variableNames).forEach(name => { + newVariables[variableNames[name]] = args[name]; + }); + + return { + document: { + ...document, + definitions: [...newOperations, ...fragments], + }, + newVariables, + }; +} + +function typeToAst(type: GraphQLInputType): TypeNode { + if (type instanceof GraphQLNonNull) { + const innerType = typeToAst(type.ofType); + if ( + innerType.kind === Kind.LIST_TYPE || + innerType.kind === Kind.NAMED_TYPE + ) { + return { + kind: Kind.NON_NULL_TYPE, + type: innerType, + }; + } else { + throw new Error('Incorrent inner non-null type'); + } + } else if (type instanceof GraphQLList) { + return { + kind: Kind.LIST_TYPE, + type: typeToAst(type.ofType), + }; + } else { + return { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: type.toString(), + }, + }; + } +} diff --git a/src/transforms/AddTypenameToAbstract.ts b/src/transforms/AddTypenameToAbstract.ts new file mode 100644 index 00000000000..5cf46c36e74 --- /dev/null +++ b/src/transforms/AddTypenameToAbstract.ts @@ -0,0 +1,75 @@ +import { + DocumentNode, + FieldNode, + GraphQLInterfaceType, + GraphQLSchema, + GraphQLType, + GraphQLUnionType, + Kind, + SelectionSetNode, + TypeInfo, + visit, + visitWithTypeInfo, +} from 'graphql'; +import { Request } from '../Interfaces'; +import { Transform } from './transforms'; + +export default function AddTypenameToAbstract( + targetSchema: GraphQLSchema, +): Transform { + return { + transformRequest(originalRequest: Request): Request { + const document = addTypenameToAbstract( + targetSchema, + originalRequest.document, + ); + return { + ...originalRequest, + document, + }; + }, + }; +} + +function addTypenameToAbstract( + targetSchema: GraphQLSchema, + document: DocumentNode, +): DocumentNode { + const typeInfo = new TypeInfo(targetSchema); + return visit( + document, + visitWithTypeInfo(typeInfo, { + [Kind.SELECTION_SET]( + node: SelectionSetNode, + ): SelectionSetNode | null | undefined { + const parentType: GraphQLType = typeInfo.getParentType(); + let selections = node.selections; + if ( + parentType && + (parentType instanceof GraphQLInterfaceType || + parentType instanceof GraphQLUnionType) && + !selections.find( + _ => + (_ as FieldNode).kind === Kind.FIELD && + (_ as FieldNode).name.value === '__typename', + ) + ) { + selections = selections.concat({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }); + } + + if (selections !== node.selections) { + return { + ...node, + selections, + }; + } + }, + }), + ); +} diff --git a/src/transforms/CheckResultAndHandleErrors.ts b/src/transforms/CheckResultAndHandleErrors.ts new file mode 100644 index 00000000000..c27367e817d --- /dev/null +++ b/src/transforms/CheckResultAndHandleErrors.ts @@ -0,0 +1,14 @@ +import { GraphQLResolveInfo } from 'graphql'; +import { checkResultAndHandleErrors } from '../stitching/errors'; +import { Transform } from './transforms'; + +export default function CheckResultAndHandleErrors( + info: GraphQLResolveInfo, + fieldName?: string, +): Transform { + return { + transformResult(result: any): any { + return checkResultAndHandleErrors(result, info, fieldName); + }, + }; +} diff --git a/src/transforms/FilterToSchema.ts b/src/transforms/FilterToSchema.ts new file mode 100644 index 00000000000..2eb48f5ed23 --- /dev/null +++ b/src/transforms/FilterToSchema.ts @@ -0,0 +1,304 @@ +import { + ArgumentNode, + DocumentNode, + FieldNode, + FragmentDefinitionNode, + FragmentSpreadNode, + GraphQLInterfaceType, + GraphQLList, + GraphQLNamedType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLType, + GraphQLUnionType, + InlineFragmentNode, + Kind, + OperationDefinitionNode, + SelectionSetNode, + TypeNameMetaFieldDef, + VariableDefinitionNode, + VariableNode, + visit, +} from 'graphql'; +import { Request } from '../Interfaces'; +import { Transform } from './transforms'; + +export default function FilterToSchema(targetSchema: GraphQLSchema): Transform { + return { + transformRequest(originalRequest: Request): Request { + const document = filterDocumentToSchema( + targetSchema, + originalRequest.document, + ); + return { + ...originalRequest, + document, + }; + }, + }; +} + +function filterDocumentToSchema( + targetSchema: GraphQLSchema, + document: DocumentNode, +): DocumentNode { + const operations: Array< + OperationDefinitionNode + > = document.definitions.filter( + def => def.kind === Kind.OPERATION_DEFINITION, + ) as Array; + const fragments: Array = document.definitions.filter( + def => def.kind === Kind.FRAGMENT_DEFINITION, + ) as Array; + + let usedVariables: Array = []; + let usedFragments: Array = []; + const newOperations: Array = []; + let newFragments: Array = []; + + const validFragments: Array< + FragmentDefinitionNode + > = fragments.filter((fragment: FragmentDefinitionNode) => { + const typeName = fragment.typeCondition.name.value; + const type = targetSchema.getType(typeName); + return Boolean(type); + }); + + const validFragmentsWithType: { [name: string]: GraphQLType } = {}; + validFragments.forEach((fragment: FragmentDefinitionNode) => { + const typeName = fragment.typeCondition.name.value; + const type = targetSchema.getType(typeName); + validFragmentsWithType[fragment.name.value] = type; + }); + + validFragments.forEach((fragment: FragmentDefinitionNode) => { + const name = fragment.name.value; + const typeName = fragment.typeCondition.name.value; + const type = targetSchema.getType(typeName); + const { + selectionSet, + usedFragments: fragmentUsedFragments, + usedVariables: fragmentUsedVariables, + } = filterSelectionSet( + targetSchema, + type, + validFragmentsWithType, + fragment.selectionSet, + ); + usedFragments = union(usedFragments, fragmentUsedFragments); + usedVariables = union(usedVariables, fragmentUsedVariables); + + newFragments.push({ + kind: Kind.FRAGMENT_DEFINITION, + name: { + kind: Kind.NAME, + value: name, + }, + typeCondition: fragment.typeCondition, + selectionSet, + }); + }); + + operations.forEach((operation: OperationDefinitionNode) => { + let type; + if (operation.operation === 'subscription') { + type = targetSchema.getSubscriptionType(); + } else if (operation.operation === 'mutation') { + type = targetSchema.getMutationType(); + } else { + type = targetSchema.getQueryType(); + } + const { + selectionSet, + usedFragments: operationUsedFragments, + usedVariables: operationUsedVariables, + } = filterSelectionSet( + targetSchema, + type, + validFragmentsWithType, + operation.selectionSet, + ); + + usedFragments = union(usedFragments, operationUsedFragments); + const fullUsedVariables = union(usedVariables, operationUsedVariables); + + const variableDefinitions = operation.variableDefinitions.filter( + (variable: VariableDefinitionNode) => + fullUsedVariables.indexOf(variable.variable.name.value) !== -1, + ); + + newOperations.push({ + kind: Kind.OPERATION_DEFINITION, + operation: operation.operation, + name: operation.name, + directives: operation.directives, + variableDefinitions, + selectionSet, + }); + }); + + newFragments = newFragments.filter( + (fragment: FragmentDefinitionNode) => + usedFragments.indexOf(fragment.name.value) !== -1, + ); + + return { + kind: Kind.DOCUMENT, + definitions: [...newOperations, ...newFragments], + }; +} + +function filterSelectionSet( + schema: GraphQLSchema, + type: GraphQLType, + validFragments: { [name: string]: GraphQLType }, + selectionSet: SelectionSetNode, +) { + const usedFragments: Array = []; + const usedVariables: Array = []; + const typeStack: Array = [type]; + + const filteredSelectionSet = visit(selectionSet, { + [Kind.FIELD]: { + enter(node: FieldNode): null | undefined | FieldNode { + let parentType: GraphQLNamedType = resolveType( + typeStack[typeStack.length - 1], + ); + if ( + parentType instanceof GraphQLObjectType || + parentType instanceof GraphQLInterfaceType + ) { + const fields = parentType.getFields(); + const field = + node.name.value === '__typename' + ? TypeNameMetaFieldDef + : fields[node.name.value]; + if (!field) { + return null; + } else { + typeStack.push(field.type); + } + + const argNames = (field.args || []).map(arg => arg.name); + if (node.arguments) { + let args = node.arguments.filter((arg: ArgumentNode) => { + return argNames.indexOf(arg.name.value) !== -1; + }); + if (args.length !== node.arguments.length) { + return { + ...node, + arguments: args, + }; + } + } + } else if ( + parentType instanceof GraphQLUnionType && + node.name.value === '__typename' + ) { + typeStack.push(TypeNameMetaFieldDef.type); + } + }, + leave() { + typeStack.pop(); + }, + }, + [Kind.FRAGMENT_SPREAD](node: FragmentSpreadNode): null | undefined { + if (node.name.value in validFragments) { + const parentType: GraphQLNamedType = resolveType( + typeStack[typeStack.length - 1], + ); + const innerType = validFragments[node.name.value]; + if (!implementsAbstractType(parentType, innerType)) { + return null; + } else { + usedFragments.push(node.name.value); + return; + } + } else { + return null; + } + }, + [Kind.INLINE_FRAGMENT]: { + enter(node: InlineFragmentNode): null | undefined { + if (node.typeCondition) { + const innerType = schema.getType(node.typeCondition.name.value); + const parentType: GraphQLNamedType = resolveType( + typeStack[typeStack.length - 1], + ); + if (implementsAbstractType(parentType, innerType)) { + typeStack.push(innerType); + } else { + return null; + } + } + }, + leave(node: InlineFragmentNode) { + typeStack.pop(); + }, + }, + [Kind.VARIABLE](node: VariableNode) { + usedVariables.push(node.name.value); + }, + }); + + return { + selectionSet: filteredSelectionSet, + usedFragments, + usedVariables, + }; +} + +function resolveType(type: GraphQLType): GraphQLNamedType { + let lastType = type; + while ( + lastType instanceof GraphQLNonNull || + lastType instanceof GraphQLList + ) { + lastType = lastType.ofType; + } + return lastType; +} + +function implementsAbstractType( + parent: GraphQLType, + child: GraphQLType, + bail: boolean = false, +): boolean { + if (parent === child) { + return true; + } else if ( + parent instanceof GraphQLInterfaceType && + child instanceof GraphQLObjectType + ) { + return child.getInterfaces().indexOf(parent) !== -1; + } else if ( + parent instanceof GraphQLInterfaceType && + child instanceof GraphQLInterfaceType + ) { + return true; + } else if ( + parent instanceof GraphQLUnionType && + child instanceof GraphQLObjectType + ) { + return parent.getTypes().indexOf(child) !== -1; + } else if (parent instanceof GraphQLObjectType && !bail) { + return implementsAbstractType(child, parent, true); + } + + return false; +} + +function union(...arrays: Array>): Array { + const cache: { [key: string]: Boolean } = {}; + const result: Array = []; + arrays.forEach(array => { + array.forEach(item => { + if (!cache[item]) { + cache[item] = true; + result.push(item); + } + }); + }); + return result; +} diff --git a/src/transforms/ReplaceFieldWithFragment.ts b/src/transforms/ReplaceFieldWithFragment.ts new file mode 100644 index 00000000000..d6229a20a05 --- /dev/null +++ b/src/transforms/ReplaceFieldWithFragment.ts @@ -0,0 +1,78 @@ +import { + DocumentNode, + GraphQLSchema, + GraphQLType, + InlineFragmentNode, + Kind, + SelectionSetNode, + TypeInfo, + visit, + visitWithTypeInfo, +} from 'graphql'; +import { Request } from '../Interfaces'; +import { Transform } from './transforms'; + +export type FieldToFragmentMapping = { + [typeName: string]: { [fieldName: string]: InlineFragmentNode }; +}; + +export default function ReplaceFieldWithFragment( + targetSchema: GraphQLSchema, + mapping: FieldToFragmentMapping, +): Transform { + return { + transformRequest(originalRequest: Request): Request { + const document = replaceFieldsWithFragments( + targetSchema, + originalRequest.document, + mapping, + ); + return { + ...originalRequest, + document, + }; + }, + }; +} + +function replaceFieldsWithFragments( + targetSchema: GraphQLSchema, + document: DocumentNode, + mapping: FieldToFragmentMapping, +): DocumentNode { + const typeInfo = new TypeInfo(targetSchema); + return visit( + document, + visitWithTypeInfo(typeInfo, { + [Kind.SELECTION_SET]( + node: SelectionSetNode, + ): SelectionSetNode | null | undefined { + const parentType: GraphQLType = typeInfo.getParentType(); + if (parentType) { + const parentTypeName = parentType.name; + + let selections = node.selections; + + if (mapping[parentTypeName]) { + node.selections.forEach(selection => { + if (selection.kind === Kind.FIELD) { + const name = selection.name.value; + const fragment = mapping[parentTypeName][name]; + if (fragment) { + selections = selections.concat(fragment); + } + } + }); + } + + if (selections !== node.selections) { + return { + ...node, + selections, + }; + } + } + }, + }), + ); +} diff --git a/src/transforms/index.ts b/src/transforms/index.ts new file mode 100644 index 00000000000..ac8aeb69089 --- /dev/null +++ b/src/transforms/index.ts @@ -0,0 +1,16 @@ +import AddArgumentsAsVariables from './AddArgumentsAsVariables'; +import CheckResultAndHandleErrors from './CheckResultAndHandleErrors'; +import ReplaceFieldWithFragment from './ReplaceFieldWithFragment'; +import AddTypenameToAbstract from './AddTypenameToAbstract'; +import FilterToSchema from './FilterToSchema'; +import makeSimpleTransformSchema from './makeSimpleTransformSchema'; +export * from './transforms'; +export * from './visitSchema'; +export { makeSimpleTransformSchema }; +export const Transforms: { [name: string]: any } = { + AddArgumentsAsVariables, + CheckResultAndHandleErrors, + ReplaceFieldWithFragment, + AddTypenameToAbstract, + FilterToSchema, +}; diff --git a/src/transforms/makeSimpleTransformSchema.ts b/src/transforms/makeSimpleTransformSchema.ts new file mode 100644 index 00000000000..bc564e778ba --- /dev/null +++ b/src/transforms/makeSimpleTransformSchema.ts @@ -0,0 +1,23 @@ +import { GraphQLSchema } from 'graphql'; +import { addResolveFunctionsToSchema } from '../schemaGenerator'; + +import { Transform, applySchemaTransforms } from '../transforms/transforms'; +import { + generateProxyingResolvers, + generateSimpleMapping, +} from '../stitching/resolvers'; + +export default function makeSimpleTransformSchema( + targetSchema: GraphQLSchema, + transforms: Array, +) { + const schema = applySchemaTransforms(targetSchema, transforms); + const mapping = generateSimpleMapping(targetSchema); + const resolvers = generateProxyingResolvers( + targetSchema, + transforms, + mapping, + ); + addResolveFunctionsToSchema(schema, resolvers); + return schema; +} diff --git a/src/transforms/transforms.ts b/src/transforms/transforms.ts new file mode 100644 index 00000000000..459f40071a4 --- /dev/null +++ b/src/transforms/transforms.ts @@ -0,0 +1,44 @@ +import { GraphQLSchema } from 'graphql'; +import { Request, Result } from '../Interfaces'; + +export type Transform = { + transformSchema?: (schema: GraphQLSchema) => GraphQLSchema; + transformRequest?: (originalRequest: Request) => Request; + transformResult?: (result: Result) => Result; +}; + +export function applySchemaTransforms( + originalSchema: GraphQLSchema, + transforms: Array, +): GraphQLSchema { + return transforms.reduce( + (schema: GraphQLSchema, transform: Transform) => + transform.transformSchema ? transform.transformSchema(schema) : schema, + originalSchema, + ); +} + +export function applyRequestTransforms( + originalRequest: Request, + transforms: Array, +): Request { + return transforms.reduce( + (request: Request, transform: Transform) => + transform.transformRequest + ? transform.transformRequest(request) + : request, + + originalRequest, + ); +} + +export function applyResultTransforms( + originalResult: any, + transforms: Array, +): any { + return transforms.reduce( + (result: any, transform: Transform) => + transform.transformResult ? transform.transformResult(result) : result, + originalResult, + ); +} diff --git a/src/transforms/visitSchema.ts b/src/transforms/visitSchema.ts new file mode 100644 index 00000000000..1c6e7fec1b8 --- /dev/null +++ b/src/transforms/visitSchema.ts @@ -0,0 +1,142 @@ +import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLScalarType, + GraphQLSchema, + GraphQLType, + GraphQLUnionType, + GraphQLNamedType, + isNamedType, + getNamedType, +} from 'graphql'; +import { recreateType, createResolveType } from '../stitching/schemaRecreation'; + +export enum VisitSchemaKind { + TYPE = 'VisitSchemaKind.TYPE', + SCALAR_TYPE = 'VisitSchemaKind.SCALAR_TYPE', + ENUM_TYPE = 'VisitSchemaKind.ENUM_TYPE', + COMPOSITE_TYPE = 'VisitSchemaKind.COMPOSITE_TYPE', + OBJECT_TYPE = 'VisitSchemaKind.OBJECT_TYPE', + INPUT_OBJECT_TYPE = 'VisitSchemaKind.INPUT_OBJECT_TYPE', + ABSTRACT_TYPE = 'VisitSchemaKind.ABSTRACT_TYPE', + UNION_TYPE = 'VisitSchemaKind.UNION_TYPE', + INTERFACE_TYPE = 'VisitSchemaKind.INTERFACE_TYPE', + ROOT_OBJECT = 'VisitSchemaKind.ROOT_OBJECT', + QUERY = 'VisitSchemaKind.QUERY', + MUTATION = 'VisitSchemaKind.MUTATION', + SUBSCRIPTION = 'VisitSchemaKind.SUBSCRIPTION', +} +// I couldn't make keys to be forced to be enum values +export type SchemaVisitor = { [key: string]: TypeVisitor }; +export type TypeVisitor = ( + type: GraphQLType, + schema: GraphQLSchema, +) => GraphQLNamedType; + +export function visitSchema(schema: GraphQLSchema, visitor: SchemaVisitor) { + const types = {}; + const resolveType = createResolveType(name => { + if (typeof types[name] === 'undefined') { + throw new Error(`Can't find type ${name}.`); + } + return types[name]; + }); + const queryType = schema.getQueryType(); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).map((typeName: string) => { + const type = typeMap[typeName]; + if (isNamedType(type) && getNamedType(type).name.slice(0, 2) !== '__') { + const specifiers = getTypeSpecifiers(type, schema); + const typeVisitor = getVisitor(visitor, specifiers); + if (typeVisitor) { + const result: GraphQLNamedType | null | undefined = typeVisitor( + type, + schema, + ); + if (typeof result === 'undefined') { + types[typeName] = recreateType(type, resolveType); + } else if (result === null) { + types[typeName] = null; + } else { + types[typeName] = recreateType(result, resolveType); + } + } else { + types[typeName] = recreateType(type, resolveType); + } + } + }); + return new GraphQLSchema({ + query: queryType ? types[queryType.name] as GraphQLObjectType : null, + mutation: mutationType + ? types[mutationType.name] as GraphQLObjectType + : null, + subscription: subscriptionType + ? types[subscriptionType.name] as GraphQLObjectType + : null, + types: Object.keys(types).map(name => types[name]), + }); +} + +function getTypeSpecifiers( + type: GraphQLType, + schema: GraphQLSchema, +): Array { + const specifiers = [VisitSchemaKind.TYPE]; + if (type instanceof GraphQLObjectType) { + specifiers.unshift( + VisitSchemaKind.COMPOSITE_TYPE, + VisitSchemaKind.OBJECT_TYPE, + ); + const query = schema.getQueryType(); + const mutation = schema.getMutationType(); + const subscription = schema.getSubscriptionType(); + if (type === query) { + specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.QUERY); + } else if (type === mutation) { + specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.MUTATION); + } else if (type === subscription) { + specifiers.push( + VisitSchemaKind.ROOT_OBJECT, + VisitSchemaKind.SUBSCRIPTION, + ); + } + } else if (type instanceof GraphQLInputObjectType) { + specifiers.push(VisitSchemaKind.INPUT_OBJECT_TYPE); + } else if (type instanceof GraphQLInterfaceType) { + specifiers.push( + VisitSchemaKind.COMPOSITE_TYPE, + VisitSchemaKind.ABSTRACT_TYPE, + VisitSchemaKind.INTERFACE_TYPE, + ); + } else if (type instanceof GraphQLUnionType) { + specifiers.push( + VisitSchemaKind.COMPOSITE_TYPE, + VisitSchemaKind.ABSTRACT_TYPE, + VisitSchemaKind.UNION_TYPE, + ); + } else if (type instanceof GraphQLEnumType) { + specifiers.push(VisitSchemaKind.ENUM_TYPE); + } else if (type instanceof GraphQLScalarType) { + specifiers.push(VisitSchemaKind.SCALAR_TYPE); + } + + return specifiers; +} + +function getVisitor( + visitor: SchemaVisitor, + specifiers: Array, +): TypeVisitor | null { + let typeVisitor = null; + const stack = [...specifiers]; + while (!typeVisitor && stack.length > 0) { + const next = stack.pop(); + typeVisitor = visitor[next]; + } + + return typeVisitor; +}