├── .dockerignore ├── .gitignore ├── .releaserc ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── graphqlObservable-test.ts │ └── reference │ │ ├── starWarsData.ts │ │ ├── starWarsQuery-test.ts │ │ └── starWarsSchema.ts ├── execution │ ├── __tests__ │ │ └── rx-subscriptions-test.ts │ ├── execute.ts │ └── index.ts ├── graphql.ts ├── index.ts ├── jstutils │ ├── inspect.ts │ ├── invariant.ts │ ├── isInvalid.ts │ ├── isNullish.ts │ └── nodejsCustomInspectSymbol.ts └── rxutils │ ├── combinePropsLatest.ts │ ├── mapPromiseToObservale.ts │ └── mapToFirstValue.ts ├── tsconfig.dist.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | 4 | .idea/ -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "master" 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 10 5 | - 8 6 | 7 | before_script: npm run build 8 | 9 | jobs: 10 | include: 11 | # Define the release stage that runs semantic-release 12 | - stage: release 13 | node_js: lts/* 14 | # Advanced: optionally overwrite your default `script` step to skip the tests 15 | # script: skip 16 | deploy: 17 | provider: script 18 | skip_cleanup: true 19 | script: 20 | - npx semantic-release -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.12.0-alpine@sha256:d75742c5fd41261113ed4706f961a21238db84648c825a5126ada373c361f46e 2 | 3 | # For deploying the gh-pages 4 | RUN apk update && apk upgrade && \ 5 | apk add --no-cache bash git openssh 6 | 7 | ENV CI true 8 | COPY package*.json ./ 9 | RUN npm install --ignore-scripts 10 | 11 | COPY . . 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive GraphQL 2 | 3 | > GraphQL reactive executor 4 | 5 | [![npm version](https://badge.fury.io/js/reactive-graphql.svg)](https://badge.fury.io/js/reactive-graphql) [![Build Status](https://travis-ci.org/mesosphere/reactive-graphql.svg?branch=master)](https://travis-ci.org/mesosphere/reactive-graphql) 6 | 7 | Execute GraphQL queries against reactive resolvers (resolvers that return Observable) to get a reactive results. 8 | 9 | ## Install 10 | 11 | ``` 12 | $ npm i reactive-graphql --save 13 | ``` 14 | 15 | ## Usage 16 | 17 | The usage is very similar to `graphql-js`'s [`graphql`](https://graphql.org/graphql-js/graphql/#graphql) function, except that: 18 | 19 | - resolvers can return an Observable 20 | - the result of a query is an Observable 21 | 22 | ```js 23 | import { graphql } from "reactive-graphql"; 24 | import { makeExecutableSchema } from "graphql-tools"; 25 | import { timer } from "rxjs"; 26 | 27 | const typeDefs = ` 28 | type Query { 29 | time: Int! 30 | } 31 | `; 32 | 33 | const resolvers = { 34 | Query: { 35 | // resolvers can return an Observable 36 | time: () => { 37 | // Observable that emits increasing numbers every 1 second 38 | return timer(1000, 1000); 39 | } 40 | } 41 | }; 42 | 43 | const schema = makeExecutableSchema({ 44 | typeDefs, 45 | resolvers 46 | }); 47 | 48 | const query = ` 49 | query { 50 | time 51 | } 52 | `; 53 | 54 | const stream = graphql(schema, query); 55 | // stream is an Observable 56 | stream.subscribe(res => console.log(res)); 57 | ``` 58 | 59 | outputs 60 | 61 | ``` 62 | { data: { time: 0 } } 63 | { data: { time: 1 } } 64 | { data: { time: 2 } } 65 | { data: { time: 3 } } 66 | { data: { time: 4 } } 67 | ... 68 | ``` 69 | 70 | ## Caveats 71 | GraphQL Subscriptions are not supported (see [issue #27](https://github.com/mesosphere/reactive-graphql/issues/27)) as everything is treated as subscriptions. 72 | 73 | ## See Also 74 | 75 | - [reactive-graphql-react](https://github.com/DanielMSchmidt/reactive-graphql-react) 76 | - [apollo-link-reactive-schema](https://github.com/getstation/apollo-link-reactive-schema) 77 | - [@dcos/data-service](https://github.com/dcos-labs/data-service) 78 | - [graphql-rxjs](https://github.com/DxCx/graphql-rxjs/) 79 | 80 | ## Advanced usage [![Edit](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/DanielMSchmidt/reactive-graphql-demo/tree/master/?hidenavigation=1) 81 | 82 | ```js 83 | import React from "react"; 84 | import ReactDOM from "react-dom"; 85 | import "./styles.css"; 86 | 87 | import { graphql } from "reactive-graphql"; 88 | 89 | import { makeExecutableSchema } from "graphql-tools"; 90 | import { from, interval, of } from "rxjs"; 91 | import { map, merge, scan, combineLatest } from "rxjs/operators"; 92 | import { componentFromStream } from "@dcos/data-service"; 93 | 94 | // mocked API clients that return Observables 95 | const oldPosts = from(["my first post", "a second post"]); 96 | const newPosts = interval(3000).pipe(map(v => `Blog Post #${v + 1}`)); 97 | const fetchPosts = () => 98 | oldPosts.pipe( 99 | merge(newPosts), 100 | scan((acc, item) => [...acc, item], []) 101 | ); 102 | const votesStore = {}; 103 | const fetchVotesForPost = name => of(votesStore[name] || 0); 104 | 105 | const schema = makeExecutableSchema({ 106 | typeDefs: ` 107 | type Post { 108 | id: Int! 109 | title: String! 110 | votes: Int! 111 | } 112 | 113 | # the schema allows the following query: 114 | type Query { 115 | posts: [Post] 116 | } 117 | 118 | # this schema allows the following mutation: 119 | type Mutation { 120 | upvotePost ( 121 | postId: Int! 122 | ): Post 123 | } 124 | `, 125 | resolvers: { 126 | Query: { 127 | posts(parent, args, context) { 128 | return fetchPosts().pipe( 129 | map(emittedValue => 130 | emittedValue.map((value, index) => ({ id: index, title: value })) 131 | ) 132 | ); 133 | } 134 | }, 135 | Post: { 136 | votes(parent, args, context) { 137 | return fetchVotesForPost(parent.title); 138 | } 139 | } 140 | } 141 | }); 142 | 143 | const query = ` 144 | query { 145 | posts { 146 | title 147 | votes 148 | } 149 | } 150 | `; 151 | 152 | const postStream = graphql(schema, query); 153 | const PostsList = componentFromStream(propsStream => 154 | propsStream.pipe( 155 | combineLatest(postStream, (props, result) => { 156 | const { 157 | data: { posts } 158 | } = result; 159 | 160 | return posts.map(post => ( 161 |
162 |

{post.title}

163 |
164 | )); 165 | }) 166 | ) 167 | ); 168 | 169 | function App() { 170 | return ( 171 |
172 | 173 |
174 | ); 175 | } 176 | 177 | const rootElement = document.getElementById("root"); 178 | ReactDOM.render(, rootElement); 179 | ``` 180 | 181 | ## License 182 | 183 | Apache 2.0 184 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testMatch: ["**/__tests__/**/*-test.js?(x)", "**/__tests__/**/*-test.ts?(x)"], 5 | testPathIgnorePatterns: ["/node_modules/", "/dist/"] 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-graphql", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/mesosphere/reactive-graphql" 6 | }, 7 | "main": "./dist/index.js", 8 | "typings": "dist/index", 9 | "files": [ 10 | "dist" 11 | ], 12 | "version": "0.0.0-dev+semantic-release", 13 | "license": "Apache-2.0", 14 | "scripts": { 15 | "test": "jest", 16 | "clean": "rimraf dist", 17 | "prepare": "npm run build", 18 | "prebuild": "npm run clean", 19 | "build": "tsc -p tsconfig.dist.json" 20 | }, 21 | "dependencies": { 22 | "graphql": "^14.5.4", 23 | "iterall": "^1.2.2", 24 | "memoizee": "^0.4.14", 25 | "util": "^0.12.1" 26 | }, 27 | "peerDependencies": { 28 | "rxjs": "^6.0.0" 29 | }, 30 | "devDependencies": { 31 | "@types/graphql": "14.0.2", 32 | "@types/jest": "23.3.10", 33 | "@types/memoizee": "^0.4.2", 34 | "delay": "^4.3.0", 35 | "graphql-tools": "4.0.3", 36 | "jest": "23.6.0", 37 | "rimraf": "2.6.2", 38 | "rxjs": "6.3.3", 39 | "rxjs-marbles": "5.0.0", 40 | "semantic-release": "^15.12.4", 41 | "ts-jest": "23.10.5", 42 | "typescript": "3.2.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/__tests__/graphqlObservable-test.ts: -------------------------------------------------------------------------------- 1 | import delay from 'delay'; 2 | import { of } from "rxjs"; 3 | import { take, map, combineLatest } from "rxjs/operators"; 4 | 5 | import { marbles } from "rxjs-marbles/jest"; 6 | 7 | import { makeExecutableSchema } from "graphql-tools"; 8 | 9 | import { graphql } from "../"; 10 | 11 | const typeDefs = ` 12 | type Shuttle { 13 | name: String! 14 | firstFlight: Int 15 | } 16 | 17 | type Query { 18 | launched(name: String): [Shuttle!]! 19 | } 20 | 21 | type Mutation { 22 | createShuttle(name: String): Shuttle! 23 | createShuttleList(name: String): [Shuttle!]! 24 | } 25 | `; 26 | 27 | const mockResolvers = { 28 | Query: { 29 | launched: (parent, args, ctx) => { 30 | const { name } = args; 31 | 32 | // act according with the type of filter 33 | if (name === undefined) { 34 | // When no filter is passed 35 | if (!parent) { 36 | return ctx.query; 37 | } 38 | 39 | return ctx.query.pipe( 40 | map((shuttles: any[]) => 41 | shuttles.map(shuttle => ({ 42 | ...shuttle, 43 | name: shuttle.name + parent.shuttleSuffix 44 | })) 45 | ) 46 | ); 47 | } else if (typeof name === "string") { 48 | // When the filter is a value 49 | return ctx.query.pipe( 50 | map(els => (els as any[]).filter(el => el.name === name)) 51 | ); 52 | } else { 53 | // when the filter is an observable 54 | return ctx.query.pipe( 55 | combineLatest(name, (res, name) => [res, name]), 56 | map(els => els[0].filter(el => el.name === els[1])) 57 | ); 58 | } 59 | } 60 | }, 61 | Mutation: { 62 | createShuttle: (_, args, ctx) => { 63 | return ctx.mutation.pipe( 64 | map(() => ({ 65 | name: args.name 66 | })) 67 | ); 68 | }, 69 | createShuttleList: (_, args, ctx) => { 70 | return ctx.mutation.pipe( 71 | map(() => [ 72 | { name: "discovery" }, 73 | { name: "challenger" }, 74 | { name: args.name } 75 | ]) 76 | ); 77 | } 78 | } 79 | }; 80 | 81 | const schema = makeExecutableSchema({ 82 | typeDefs, 83 | resolvers: mockResolvers 84 | }); 85 | 86 | const fieldResolverSchema = makeExecutableSchema({ 87 | typeDefs: ` 88 | type Plain { 89 | noFieldResolver: String! 90 | fieldResolver: String! 91 | fieldResolvesUndefined: String! 92 | giveMeTheParentFieldResolver: String! 93 | giveMeTheArgsFieldResolver(arg: String!): String! 94 | giveMeTheContextFieldResolver: String! 95 | } 96 | 97 | type ObjectValue { 98 | value: String! 99 | } 100 | 101 | type Item { 102 | nodeFieldResolver: ObjectValue! 103 | nullableNodeFieldResolver: ObjectValue 104 | giveMeTheParentFieldResolver: ObjectValue! 105 | giveMeTheArgsFieldResolver(arg: String!): ObjectValue! 106 | giveMeTheContextFieldResolver: ObjectValue! 107 | } 108 | 109 | type Nested { 110 | firstFieldResolver: Nesting! 111 | } 112 | 113 | type Nesting { 114 | noFieldResolverValue: String! 115 | secondFieldResolver: String! 116 | } 117 | 118 | type Query { 119 | plain: Plain! 120 | item: Item! 121 | nested: Nested! 122 | throwingResolver: String 123 | } 124 | `, 125 | resolvers: { 126 | Plain: { 127 | fieldResolver() { 128 | return of("I am a field resolver"); 129 | }, 130 | fieldResolvesUndefined() { 131 | return of(undefined); 132 | }, 133 | giveMeTheParentFieldResolver(parent) { 134 | return of(JSON.stringify(parent)); 135 | }, 136 | giveMeTheArgsFieldResolver(_parent, args) { 137 | return of(JSON.stringify(args)); 138 | }, 139 | giveMeTheContextFieldResolver(_parent, _args, context) { 140 | return of(context.newValue); 141 | } 142 | }, 143 | Item: { 144 | nodeFieldResolver() { 145 | return of({ value: "I am a node field resolver" }); 146 | }, 147 | nullableNodeFieldResolver() { 148 | return null; 149 | }, 150 | giveMeTheParentFieldResolver(parent) { 151 | return of({ value: JSON.stringify(parent) }); 152 | }, 153 | giveMeTheArgsFieldResolver(_parent, args) { 154 | return of({ value: JSON.stringify(args) }); 155 | }, 156 | giveMeTheContextFieldResolver(_parent, _args, context) { 157 | return of({ value: context.newValue }); 158 | } 159 | }, 160 | Nested: { 161 | firstFieldResolver(_parent, _args, ctx) { 162 | ctx.contextValue = " resolvers are great"; 163 | 164 | return of({ noFieldResolverValue: "nested" }); 165 | } 166 | }, 167 | Nesting: { 168 | secondFieldResolver({ noFieldResolverValue }, _, { contextValue }) { 169 | return of(noFieldResolverValue.toLocaleUpperCase() + contextValue); 170 | } 171 | }, 172 | Query: { 173 | plain(_parent, _args, ctx) { 174 | ctx.newValue = "ContextValue"; 175 | 176 | return of({ 177 | noFieldResolver: "Yes" 178 | }); 179 | }, 180 | item(_parent, _args, ctx) { 181 | ctx.newValue = "NodeContextValue"; 182 | 183 | return of({ thisIsANodeFieldResolver: "Yes" }); 184 | }, 185 | 186 | nested() { 187 | return of({}); 188 | }, 189 | throwingResolver() { 190 | throw new Error("my personal error"); 191 | } 192 | } 193 | } 194 | }); 195 | 196 | // jest helper who binds the marbles for you 197 | const itMarbles = (title, test) => { 198 | return it(title, marbles(test)); 199 | }; 200 | 201 | itMarbles.only = (title, test) => { 202 | return it.only(title, marbles(test)); 203 | }; 204 | 205 | describe("graphqlObservable", function() { 206 | describe("Query", function() { 207 | itMarbles("solves listing all fields", function(m) { 208 | const query = ` 209 | query { 210 | launched { 211 | name 212 | } 213 | } 214 | `; 215 | 216 | const expectedData = [{ name: "discovery" }]; 217 | const dataSource = of(expectedData); 218 | const expected = m.cold("(a|)", { 219 | a: { data: { launched: expectedData } } 220 | }); 221 | 222 | const result = graphql(schema, query, null, { query: dataSource }); 223 | 224 | m.expect(result.pipe(take(1))).toBeObservable(expected); 225 | }); 226 | 227 | itMarbles("solves listing all fields with string query", function(m) { 228 | const query = ` 229 | query { 230 | launched { 231 | name 232 | } 233 | } 234 | `; 235 | 236 | const expectedData = [{ name: "discovery" }]; 237 | const dataSource = of(expectedData); 238 | const expected = m.cold("(a|)", { 239 | a: { data: { launched: expectedData } } 240 | }); 241 | 242 | const result = graphql(schema, query, null, { query: dataSource }); 243 | 244 | m.expect(result.pipe(take(1))).toBeObservable(expected); 245 | }); 246 | 247 | itMarbles("filters by variable argument", function(m) { 248 | const query = ` 249 | query($nameFilter: String) { 250 | launched(name: $nameFilter) { 251 | name 252 | firstFlight 253 | } 254 | } 255 | `; 256 | 257 | const expectedData = [{ name: "apollo11", firstFlight: null }, { name: "challenger", firstFlight: null }]; 258 | const dataSource = of(expectedData); 259 | const expected = m.cold("(a|)", { 260 | a: { data: { launched: [expectedData[0]] } } 261 | }); 262 | 263 | const nameFilter = "apollo11"; 264 | const result = graphql( 265 | schema, 266 | query, 267 | null, 268 | { 269 | query: dataSource 270 | }, 271 | { 272 | nameFilter 273 | } 274 | ); 275 | 276 | m.expect(result.pipe(take(1))).toBeObservable(expected); 277 | }); 278 | 279 | itMarbles("filters by static argument", function(m) { 280 | const query = ` 281 | query { 282 | launched(name: "apollo13") { 283 | name 284 | firstFlight 285 | } 286 | } 287 | `; 288 | 289 | const expectedData = [{ name: "apollo13", firstFlight: null }, { name: "challenger", firstFlight: null }]; 290 | const dataSource = of(expectedData); 291 | const expected = m.cold("(a|)", { 292 | a: { data: { launched: [expectedData[0]] } } 293 | }); 294 | 295 | const result = graphql(schema, query, null, { 296 | query: dataSource 297 | }); 298 | 299 | m.expect(result.pipe(take(1))).toBeObservable(expected); 300 | }); 301 | 302 | itMarbles("filters out fields", function(m) { 303 | const query = ` 304 | query { 305 | launched { 306 | name 307 | } 308 | } 309 | `; 310 | 311 | const expectedData = [{ name: "discovery", firstFlight: 1984 }]; 312 | const dataSource = of(expectedData); 313 | const expected = m.cold("(a|)", { 314 | a: { data: { launched: [{ name: "discovery" }] } } 315 | }); 316 | 317 | const result = graphql(schema, query, null, { 318 | query: dataSource 319 | }); 320 | 321 | m.expect(result.pipe(take(1))).toBeObservable(expected); 322 | }); 323 | 324 | itMarbles("resolve with name alias", function(m) { 325 | const query = ` 326 | query { 327 | launched { 328 | title: name 329 | } 330 | } 331 | `; 332 | 333 | const expectedData = [{ name: "challenger", firstFlight: 1984 }]; 334 | const dataSource = of(expectedData); 335 | const expected = m.cold("(a|)", { 336 | a: { data: { launched: [{ title: "challenger" }] } } 337 | }); 338 | 339 | const result = graphql(schema, query, null, { 340 | query: dataSource 341 | }); 342 | 343 | m.expect(result.pipe(take(1))).toBeObservable(expected); 344 | }); 345 | 346 | itMarbles("resolves using root value", function(m) { 347 | const query = ` 348 | query { 349 | launched { 350 | name 351 | } 352 | } 353 | `; 354 | 355 | const expectedData = [{ name: "challenger", firstFlight: 1984 }]; 356 | const dataSource = of(expectedData); 357 | const expected = m.cold("(a|)", { 358 | a: { data: { launched: [{ name: "challenger-nasa" }] } } 359 | }); 360 | 361 | const result = graphql( 362 | schema, 363 | query, 364 | { 365 | shuttleSuffix: "-nasa" 366 | }, 367 | { 368 | query: dataSource 369 | } 370 | ); 371 | 372 | m.expect(result.pipe(take(1))).toBeObservable(expected); 373 | }); 374 | 375 | describe("Field Resolvers", function() { 376 | describe("Leafs", function() { 377 | itMarbles("defaults to return the property on the object", function(m) { 378 | const query = ` 379 | query { 380 | plain { 381 | noFieldResolver 382 | } 383 | } 384 | `; 385 | const expected = m.cold("(a|)", { 386 | a: { data: { plain: { noFieldResolver: "Yes" } } } 387 | }); 388 | const result = graphql(fieldResolverSchema, query, null, {}); 389 | m.expect(result.pipe(take(1))).toBeObservable(expected); 390 | }); 391 | 392 | itMarbles("if defined it executes the field resolver", function(m) { 393 | const query = ` 394 | query { 395 | plain { 396 | fieldResolver 397 | } 398 | } 399 | `; 400 | const expected = m.cold("(a|)", { 401 | a: { data: { plain: { fieldResolver: "I am a field resolver" } } } 402 | }); 403 | const result = graphql(fieldResolverSchema, query, null, {}); 404 | m.expect(result.pipe(take(1))).toBeObservable(expected); 405 | }); 406 | 407 | itMarbles("if defined but returns undefined, field is null", function (m) { 408 | const query = ` 409 | query { 410 | plain { 411 | fieldResolvesUndefined 412 | } 413 | } 414 | `; 415 | const expected = m.cold("(a|)", { 416 | a: { data: { plain: { fieldResolvesUndefined: null } } } 417 | }); 418 | const result = graphql(fieldResolverSchema, query, null, {}); 419 | m.expect(result.pipe(take(1))).toBeObservable(expected); 420 | }); 421 | 422 | itMarbles("the field resolvers 1st argument is parent", function(m) { 423 | const query = ` 424 | query { 425 | plain { 426 | giveMeTheParentFieldResolver 427 | } 428 | } 429 | `; 430 | const expected = m.cold("(a|)", { 431 | a: { 432 | data: { 433 | plain: { 434 | giveMeTheParentFieldResolver: JSON.stringify({ 435 | noFieldResolver: "Yes" 436 | }) 437 | } 438 | } 439 | } 440 | }); 441 | const result = graphql(fieldResolverSchema, query, null, {}); 442 | m.expect(result.pipe(take(1))).toBeObservable(expected); 443 | }); 444 | 445 | itMarbles("the field resolvers 2nd argument is arguments", function(m) { 446 | const query = ` 447 | query { 448 | plain { 449 | giveMeTheArgsFieldResolver(arg: "My passed arg") 450 | } 451 | } 452 | `; 453 | const expected = m.cold("(a|)", { 454 | a: { 455 | data: { 456 | plain: { 457 | giveMeTheArgsFieldResolver: JSON.stringify({ 458 | arg: "My passed arg" 459 | }) 460 | } 461 | } 462 | } 463 | }); 464 | const result = graphql(fieldResolverSchema, query, null, {}); 465 | m.expect(result.pipe(take(1))).toBeObservable(expected); 466 | }); 467 | 468 | itMarbles("the field resolvers 3rd argument is context", function(m) { 469 | const query = ` 470 | query { 471 | plain { 472 | giveMeTheContextFieldResolver 473 | } 474 | } 475 | `; 476 | const expected = m.cold("(a|)", { 477 | a: { 478 | data: { 479 | plain: { 480 | giveMeTheContextFieldResolver: "ContextValue" 481 | } 482 | } 483 | } 484 | }); 485 | const result = graphql(fieldResolverSchema, query, null, {}); 486 | m.expect(result.pipe(take(1))).toBeObservable(expected); 487 | }); 488 | }); 489 | 490 | describe("Nodes", function() { 491 | itMarbles("if defined it executes the field resolver", function(m) { 492 | const query = ` 493 | query { 494 | item { 495 | nodeFieldResolver { 496 | value 497 | } 498 | } 499 | } 500 | `; 501 | const expected = m.cold("(a|)", { 502 | a: { 503 | data: { 504 | item: { 505 | nodeFieldResolver: { value: "I am a node field resolver" } 506 | } 507 | } 508 | } 509 | }); 510 | const result = graphql(fieldResolverSchema, query, null, {}); 511 | m.expect(result.pipe(take(1))).toBeObservable(expected); 512 | }); 513 | 514 | itMarbles("if nullable field resolver returns null, it resolves null", function(m) { 515 | const query = ` 516 | query { 517 | item { 518 | nullableNodeFieldResolver { 519 | value 520 | } 521 | } 522 | } 523 | `; 524 | const expected = m.cold("(a|)", { 525 | a: { 526 | data: { 527 | item: { 528 | nullableNodeFieldResolver: null 529 | } 530 | } 531 | } 532 | }); 533 | const result = graphql(fieldResolverSchema, query, null, {}); 534 | m.expect(result.pipe(take(1))).toBeObservable(expected); 535 | }); 536 | 537 | itMarbles("the field resolvers 1st argument is parent", function(m) { 538 | const query = ` 539 | query { 540 | item { 541 | giveMeTheParentFieldResolver { 542 | value 543 | } 544 | } 545 | } 546 | `; 547 | const expected = m.cold("(a|)", { 548 | a: { 549 | data: { 550 | item: { 551 | giveMeTheParentFieldResolver: { 552 | value: JSON.stringify({ 553 | thisIsANodeFieldResolver: "Yes" 554 | }) 555 | } 556 | } 557 | } 558 | } 559 | }); 560 | const result = graphql(fieldResolverSchema, query, null, {}); 561 | m.expect(result.pipe(take(1))).toBeObservable(expected); 562 | }); 563 | 564 | itMarbles("the field resolvers 2nd argument is arguments", function(m) { 565 | const query = ` 566 | query { 567 | item { 568 | giveMeTheArgsFieldResolver(arg: "My passed arg") { 569 | value 570 | } 571 | } 572 | } 573 | `; 574 | const expected = m.cold("(a|)", { 575 | a: { 576 | data: { 577 | item: { 578 | giveMeTheArgsFieldResolver: { 579 | value: JSON.stringify({ 580 | arg: "My passed arg" 581 | }) 582 | } 583 | } 584 | } 585 | } 586 | }); 587 | const result = graphql(fieldResolverSchema, query, null, {}); 588 | m.expect(result.pipe(take(1))).toBeObservable(expected); 589 | }); 590 | 591 | itMarbles("the field resolvers 3rd argument is context", function(m) { 592 | const query = ` 593 | query { 594 | item { 595 | giveMeTheContextFieldResolver { 596 | value 597 | } 598 | } 599 | } 600 | `; 601 | const expected = m.cold("(a|)", { 602 | a: { 603 | data: { 604 | item: { 605 | giveMeTheContextFieldResolver: { value: "NodeContextValue" } 606 | } 607 | } 608 | } 609 | }); 610 | const result = graphql(fieldResolverSchema, query, null, {}); 611 | m.expect(result.pipe(take(1))).toBeObservable(expected); 612 | }); 613 | }); 614 | 615 | itMarbles("nested resolvers pass down the context and parent", function( 616 | m 617 | ) { 618 | const query = ` 619 | query { 620 | nested { 621 | firstFieldResolver { 622 | noFieldResolverValue 623 | secondFieldResolver 624 | } 625 | } 626 | } 627 | `; 628 | const expected = m.cold("(a|)", { 629 | a: { 630 | data: { 631 | nested: { 632 | firstFieldResolver: { 633 | noFieldResolverValue: "nested", 634 | secondFieldResolver: "NESTED resolvers are great" 635 | } 636 | } 637 | } 638 | } 639 | }); 640 | const result = graphql(fieldResolverSchema, query, null, {}); 641 | m.expect(result.pipe(take(1))).toBeObservable(expected); 642 | }); 643 | }); 644 | 645 | it("throwing an error results in an error in execution result", async function() { 646 | const query = ` 647 | query { 648 | throwingResolver 649 | } 650 | `; 651 | 652 | const result = await graphql(fieldResolverSchema, query, null, {}).pipe(take(1)).toPromise(); 653 | expect(result).toHaveProperty('errors'); 654 | expect(result.errors![0].message).toBe('my personal error'); 655 | expect(result.errors![0].path).toEqual(['throwingResolver']); 656 | 657 | }); 658 | 659 | it( 660 | "accessing an unknown query field results in an error in execution result", 661 | async function() { 662 | const query = ` 663 | query { 664 | youDontKnowMe 665 | } 666 | `; 667 | const result = await graphql(fieldResolverSchema, query, null, {}).pipe(take(1)).toPromise(); 668 | expect(result).toHaveProperty('errors'); 669 | expect(result.errors![0].message).toBe('Cannot query field \"youDontKnowMe\" on type \"Query\".') 670 | } 671 | ); 672 | }); 673 | 674 | describe("Mutation", function() { 675 | itMarbles("createShuttle adds a shuttle and return its name", function(m) { 676 | const mutation = ` 677 | mutation { 678 | createShuttle(name: "RocketShip") { 679 | name 680 | } 681 | } 682 | `; 683 | 684 | const fakeRequest = { name: "RocketShip" }; 685 | const commandContext = of(fakeRequest); 686 | 687 | const result = graphql(schema, mutation, null, { 688 | mutation: commandContext 689 | }); 690 | 691 | const expected = m.cold("(a|)", { 692 | a: { data: { createShuttle: { name: "RocketShip" } } } 693 | }); 694 | 695 | m.expect(result).toBeObservable(expected); 696 | }); 697 | 698 | itMarbles( 699 | "createShuttleList adds a shuttle and return all shuttles", 700 | function(m) { 701 | const mutation = ` 702 | mutation { 703 | createShuttleList(name: "RocketShip") { 704 | name 705 | } 706 | } 707 | `; 708 | 709 | const commandContext = of("a request"); 710 | 711 | const result = graphql(schema, mutation, null, { 712 | mutation: commandContext 713 | }); 714 | 715 | const expected = m.cold("(a|)", { 716 | a: { 717 | data: { 718 | createShuttleList: [ 719 | { name: "discovery" }, 720 | { name: "challenger" }, 721 | { name: "RocketShip" } 722 | ] 723 | } 724 | } 725 | }); 726 | 727 | m.expect(result).toBeObservable(expected); 728 | } 729 | ); 730 | 731 | itMarbles("accept alias name", function(m) { 732 | const mutation = ` 733 | mutation ($name: String){ 734 | shut: createShuttle(name: $name) { 735 | name 736 | } 737 | } 738 | `; 739 | 740 | const commandContext = of("a resquest"); 741 | const variables = { 742 | name: "RocketShip" 743 | }; 744 | const result = graphql( 745 | schema, 746 | mutation, 747 | null, 748 | { 749 | mutation: commandContext 750 | }, 751 | variables 752 | ); 753 | 754 | const expected = m.cold("(a|)", { 755 | a: { data: { shut: { name: "RocketShip" } } } 756 | }); 757 | 758 | m.expect(result).toBeObservable(expected); 759 | }); 760 | 761 | it('respects serial execution of resolvers', async () => { 762 | let theNumber = 0; 763 | const schema = makeExecutableSchema({ 764 | typeDefs: ` 765 | type Mutation { 766 | increment: Int! 767 | } 768 | type Query { 769 | theNumber: Int! 770 | } 771 | `, 772 | resolvers: { 773 | Mutation: { 774 | // atomic resolver 775 | increment: async () => { 776 | const _theNumber = theNumber; 777 | await delay(100); 778 | theNumber = _theNumber + 1; 779 | return theNumber; 780 | } 781 | }, 782 | } 783 | }); 784 | 785 | const result$ = graphql({ 786 | schema, 787 | source: ` 788 | mutation { 789 | first: increment, 790 | second: increment, 791 | third: increment, 792 | } 793 | `, 794 | }) 795 | const result = await result$.pipe(take(1)).toPromise(); 796 | expect(result).toEqual({ 797 | data: { 798 | first: 1, 799 | second: 2, // 1 if not serial 800 | third: 3, // 1 if not serial 801 | } 802 | }) 803 | }) 804 | }); 805 | }); 806 | -------------------------------------------------------------------------------- /src/__tests__/reference/starWarsData.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsData.js 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | /** 12 | * This defines a basic set of data for our Star Wars Schema. 13 | * 14 | * This data is hard coded for the sake of the demo, but you could imagine 15 | * fetching this data from a backend service rather than from hardcoded 16 | * JSON objects in a more complex demo. 17 | */ 18 | 19 | const luke = { 20 | type: "Human", 21 | id: "1000", 22 | name: "Luke Skywalker", 23 | friends: ["1002", "1003", "2000", "2001"], 24 | appearsIn: [4, 5, 6], 25 | homePlanet: "Tatooine" 26 | }; 27 | 28 | const vader = { 29 | type: "Human", 30 | id: "1001", 31 | name: "Darth Vader", 32 | friends: ["1004"], 33 | appearsIn: [4, 5, 6], 34 | homePlanet: "Tatooine" 35 | }; 36 | 37 | const han = { 38 | type: "Human", 39 | id: "1002", 40 | name: "Han Solo", 41 | friends: ["1000", "1003", "2001"], 42 | appearsIn: [4, 5, 6] 43 | }; 44 | 45 | const leia = { 46 | type: "Human", 47 | id: "1003", 48 | name: "Leia Organa", 49 | friends: ["1000", "1002", "2000", "2001"], 50 | appearsIn: [4, 5, 6], 51 | homePlanet: "Alderaan" 52 | }; 53 | 54 | const tarkin = { 55 | type: "Human", 56 | id: "1004", 57 | name: "Wilhuff Tarkin", 58 | friends: ["1001"], 59 | appearsIn: [4] 60 | }; 61 | 62 | const humanData = { 63 | "1000": luke, 64 | "1001": vader, 65 | "1002": han, 66 | "1003": leia, 67 | "1004": tarkin 68 | }; 69 | 70 | const threepio = { 71 | type: "Droid", 72 | id: "2000", 73 | name: "C-3PO", 74 | friends: ["1000", "1002", "1003", "2001"], 75 | appearsIn: [4, 5, 6], 76 | primaryFunction: "Protocol" 77 | }; 78 | 79 | const artoo = { 80 | type: "Droid", 81 | id: "2001", 82 | name: "R2-D2", 83 | friends: ["1000", "1002", "1003"], 84 | appearsIn: [4, 5, 6], 85 | primaryFunction: "Astromech" 86 | }; 87 | 88 | const droidData = { 89 | "2000": threepio, 90 | "2001": artoo 91 | }; 92 | 93 | /* 94 | * Helper function to get a character by ID. 95 | */ 96 | function getCharacter(id) { 97 | // Returning a promise just to illustrate GraphQL.js's support. 98 | return Promise.resolve(humanData[id] || droidData[id]); 99 | } 100 | 101 | /* 102 | * Allows us to query for a character's friends. 103 | */ 104 | export function getFriends(character) { 105 | // Notice that GraphQL accepts Arrays of Promises. 106 | return character.friends.map(id => getCharacter(id)); 107 | } 108 | 109 | /* 110 | * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2. 111 | */ 112 | export function getHero(episode) { 113 | if (episode === 5) { 114 | // Luke is the hero of Episode V. 115 | return luke; 116 | } 117 | 118 | // Artoo is the hero otherwise. 119 | return artoo; 120 | } 121 | 122 | /* 123 | * Allows us to query for the human with the given id. 124 | */ 125 | export function getHuman(id) { 126 | return humanData[id]; 127 | } 128 | 129 | /* 130 | * Allows us to query for the droid with the given id. 131 | */ 132 | export function getDroid(id) { 133 | return droidData[id]; 134 | } 135 | -------------------------------------------------------------------------------- /src/__tests__/reference/starWarsQuery-test.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionResult, SourceLocation } from "graphql"; 2 | import { take } from "rxjs/operators"; 3 | 4 | import StarWarsSchema from "./starWarsSchema"; 5 | import { graphql as graphqlObservable } from "../../"; 6 | 7 | const graphql = (schema, query, rootValue?, contextValue?, variableValues?) => { 8 | return new Promise(resolve => { 9 | graphqlObservable( 10 | schema, 11 | query, 12 | rootValue, 13 | contextValue, 14 | variableValues 15 | ) 16 | .pipe(take(1)) 17 | .subscribe(resolve); 18 | }); 19 | }; 20 | 21 | type SerializedExecutionResult = { 22 | data?: TData | null, 23 | errors?: ReadonlyArray<{ 24 | message: string, 25 | locations?: ReadonlyArray, 26 | path?: ReadonlyArray 27 | }> 28 | } 29 | declare global { 30 | namespace jest { 31 | interface Matchers { 32 | /** 33 | * Will test the equality of GraphQL's `ExecutionResult`. 34 | * 35 | * In opposite to the simple `toEqual` it will test the `errors` field 36 | * with `GraphqQLErrors`. Specifically it will test the equlity of the 37 | * properties `message`, `locations` and `path`. 38 | * @param expected 39 | */ 40 | toEqualExecutionResult(expected: SerializedExecutionResult): R; 41 | } 42 | } 43 | } 44 | 45 | expect.extend({ 46 | toEqualExecutionResult(actual: ExecutionResult, expected: SerializedExecutionResult) { 47 | let actualSerialized: SerializedExecutionResult = { 48 | data: actual.data, 49 | }; 50 | if(actual.errors) { 51 | actualSerialized.errors = actual.errors.map(e => ({ 52 | message: e.message, 53 | locations: e.locations, 54 | path: e.path, 55 | })) 56 | } 57 | expect(actualSerialized).toEqual(expected); 58 | return { 59 | message: 'ok', 60 | pass: true, 61 | } 62 | } 63 | }) 64 | 65 | describe("Star Wars Query Tests", () => { 66 | describe("Basic Queries", () => { 67 | it("Correctly identifies R2-D2 as the hero of the Star Wars Saga", async () => { 68 | const query = ` 69 | query { 70 | hero { 71 | name 72 | } 73 | } 74 | `; 75 | const result = await graphql(StarWarsSchema, query); 76 | expect(result).toEqualExecutionResult({ 77 | data: { 78 | hero: { 79 | name: "R2-D2" 80 | } 81 | } 82 | }); 83 | }); 84 | 85 | it("Correctly identifies R2-D2 with alias", async () => { 86 | const query = ` 87 | query { 88 | myrobot: hero { 89 | name 90 | } 91 | } 92 | `; 93 | const result = await graphql(StarWarsSchema, query); 94 | expect(result).toEqualExecutionResult({ 95 | data: { 96 | myrobot: { 97 | name: "R2-D2" 98 | } 99 | } 100 | }); 101 | }); 102 | 103 | it("Allows us to query for the ID and friends of R2-D2", async () => { 104 | const query = ` 105 | query HerNameAndFriendsQuery { 106 | hero { 107 | id 108 | name 109 | friends { 110 | name 111 | } 112 | } 113 | } 114 | `; 115 | const result = await graphql(StarWarsSchema, query); 116 | expect(result).toEqualExecutionResult({ 117 | data: { 118 | hero: { 119 | id: "2001", 120 | name: "R2-D2", 121 | friends: [ 122 | { 123 | name: "Luke Skywalker" 124 | }, 125 | { 126 | name: "Han Solo" 127 | }, 128 | { 129 | name: "Leia Organa" 130 | } 131 | ] 132 | } 133 | } 134 | }); 135 | }); 136 | }); 137 | 138 | // Requires support to nested queries https://jira.mesosphere.com/browse/DCOS-22358 139 | describe("Nested Queries", () => { 140 | it("Allows us to query for the friends of friends of R2-D2", async () => { 141 | const query = ` 142 | query NestedQuery { 143 | hero { 144 | name 145 | friends { 146 | name 147 | appearsIn 148 | friends { 149 | name 150 | } 151 | } 152 | } 153 | } 154 | `; 155 | const result = await graphql(StarWarsSchema, query); 156 | expect(result).toEqualExecutionResult({ 157 | data: { 158 | hero: { 159 | name: "R2-D2", 160 | friends: [ 161 | { 162 | name: "Luke Skywalker", 163 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 164 | friends: [ 165 | { 166 | name: "Han Solo" 167 | }, 168 | { 169 | name: "Leia Organa" 170 | }, 171 | { 172 | name: "C-3PO" 173 | }, 174 | { 175 | name: "R2-D2" 176 | } 177 | ] 178 | }, 179 | { 180 | name: "Han Solo", 181 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 182 | friends: [ 183 | { 184 | name: "Luke Skywalker" 185 | }, 186 | { 187 | name: "Leia Organa" 188 | }, 189 | { 190 | name: "R2-D2" 191 | } 192 | ] 193 | }, 194 | { 195 | name: "Leia Organa", 196 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 197 | friends: [ 198 | { 199 | name: "Luke Skywalker" 200 | }, 201 | { 202 | name: "Han Solo" 203 | }, 204 | { 205 | name: "C-3PO" 206 | }, 207 | { 208 | name: "R2-D2" 209 | } 210 | ] 211 | } 212 | ] 213 | } 214 | } 215 | }); 216 | }); 217 | }); 218 | 219 | describe("Using IDs and query parameters to refetch objects", () => { 220 | it("Allows us to query for Luke Skywalker directly, using his ID", async () => { 221 | const query = ` 222 | query FetchLukeQuery { 223 | human(id: "1000") { 224 | name 225 | } 226 | } 227 | `; 228 | const result = await graphql(StarWarsSchema, query); 229 | expect(result).toEqualExecutionResult({ 230 | data: { 231 | human: { 232 | name: "Luke Skywalker" 233 | } 234 | } 235 | }); 236 | }); 237 | 238 | it("Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID", async () => { 239 | const query = ` 240 | query FetchSomeIDQuery($someId: String!) { 241 | human(id: $someId) { 242 | name 243 | } 244 | } 245 | `; 246 | const params = { someId: "1000" }; 247 | const result = await graphql(StarWarsSchema, query, null, null, params); 248 | expect(result).toEqualExecutionResult({ 249 | data: { 250 | human: { 251 | name: "Luke Skywalker" 252 | } 253 | } 254 | }); 255 | }); 256 | 257 | it("Allows us to create a generic query, then use it to fetch Han Solo using his ID", async () => { 258 | const query = ` 259 | query FetchSomeIDQuery($someId: String!) { 260 | human(id: $someId) { 261 | name 262 | } 263 | } 264 | `; 265 | const params = { someId: "1002" }; 266 | const result = await graphql(StarWarsSchema, query, null, null, params); 267 | expect(result).toEqualExecutionResult({ 268 | data: { 269 | human: { 270 | name: "Han Solo" 271 | } 272 | } 273 | }); 274 | }); 275 | 276 | // Requires support to errors https://jira.mesosphere.com/browse/DCOS-22062 277 | it("Allows us to create a generic query, then pass an invalid ID to get null back", async () => { 278 | const query = ` 279 | query humanQuery($id: String!) { 280 | human(id: $id) { 281 | name 282 | } 283 | } 284 | `; 285 | const params = { id: "not a valid id" }; 286 | const result = await graphql(StarWarsSchema, query, null, null, params); 287 | expect(result).toEqualExecutionResult({ 288 | data: { 289 | human: null 290 | } 291 | }); 292 | }); 293 | }); 294 | 295 | describe("Using aliases to change the key in the response", () => { 296 | it("Allows us to query for Luke, changing his key with an alias", async () => { 297 | const query = ` 298 | query FetchLukeAliased { 299 | luke: human(id: "1000") { 300 | name 301 | } 302 | } 303 | `; 304 | const result = await graphql(StarWarsSchema, query); 305 | expect(result).toEqualExecutionResult({ 306 | data: { 307 | luke: { 308 | name: "Luke Skywalker" 309 | } 310 | } 311 | }); 312 | }); 313 | 314 | it("Allows us to query for both Luke and Leia, using two root fields and an alias", async () => { 315 | const query = ` 316 | query FetchLukeAndLeiaAliased { 317 | luke: human(id: "1000") { 318 | name 319 | } 320 | leia: human(id: "1003") { 321 | name 322 | } 323 | } 324 | `; 325 | const result = await graphql(StarWarsSchema, query); 326 | expect(result).toEqualExecutionResult({ 327 | data: { 328 | luke: { 329 | name: "Luke Skywalker" 330 | }, 331 | leia: { 332 | name: "Leia Organa" 333 | } 334 | } 335 | }); 336 | }); 337 | }); 338 | 339 | describe("Uses fragments to express more complex queries", () => { 340 | it("Allows us to query using duplicated content", async () => { 341 | const query = ` 342 | query DuplicateFields { 343 | luke: human(id: "1000") { 344 | name 345 | homePlanet 346 | } 347 | leia: human(id: "1003") { 348 | name 349 | homePlanet 350 | } 351 | } 352 | `; 353 | const result = await graphql(StarWarsSchema, query); 354 | expect(result).toEqualExecutionResult({ 355 | data: { 356 | luke: { 357 | name: "Luke Skywalker", 358 | homePlanet: "Tatooine" 359 | }, 360 | leia: { 361 | name: "Leia Organa", 362 | homePlanet: "Alderaan" 363 | } 364 | } 365 | }); 366 | }); 367 | 368 | // Require support to fragments https://jira.mesosphere.com/browse/DCOS-22356 369 | it("Allows us to use a fragment to avoid duplicating content", async () => { 370 | const query = ` 371 | query UseFragment { 372 | luke: human(id: "1000") { 373 | ...HumanFragment 374 | } 375 | leia: human(id: "1003") { 376 | ...HumanFragment 377 | } 378 | } 379 | 380 | fragment HumanFragment on Human { 381 | name 382 | homePlanet 383 | } 384 | `; 385 | const result = await graphql(StarWarsSchema, query); 386 | expect(result).toEqualExecutionResult({ 387 | data: { 388 | luke: { 389 | name: "Luke Skywalker", 390 | homePlanet: "Tatooine" 391 | }, 392 | leia: { 393 | name: "Leia Organa", 394 | homePlanet: "Alderaan" 395 | } 396 | } 397 | }); 398 | }); 399 | }); 400 | 401 | // Not supporting introspection 402 | describe("Using __typename to find the type of an object", () => { 403 | it("Allows us to verify that R2-D2 is a droid", async () => { 404 | const query = ` 405 | query CheckTypeOfR2 { 406 | hero { 407 | __typename 408 | name 409 | } 410 | } 411 | `; 412 | const result = await graphql(StarWarsSchema, query); 413 | expect(result).toEqualExecutionResult({ 414 | data: { 415 | hero: { 416 | __typename: "Droid", 417 | name: "R2-D2" 418 | } 419 | } 420 | }); 421 | }); 422 | 423 | // Requires support to introspection https://jira.mesosphere.com/browse/DCOS-22357 424 | it("Allows us to verify that Luke is a human", async () => { 425 | const query = ` 426 | query CheckTypeOfLuke { 427 | hero(episode: EMPIRE) { 428 | __typename 429 | name 430 | } 431 | } 432 | `; 433 | const result = await graphql(StarWarsSchema, query); 434 | expect(result).toEqualExecutionResult({ 435 | data: { 436 | hero: { 437 | __typename: "Human", 438 | name: "Luke Skywalker" 439 | } 440 | } 441 | }); 442 | }); 443 | }); 444 | 445 | // Requires support to errors https://jira.mesosphere.com/browse/DCOS-22062 446 | describe("Reporting errors raised in resolvers", () => { 447 | it("Correctly reports error on accessing secretBackstory", async () => { 448 | const query = ` 449 | query HeroNameQuery { 450 | hero { 451 | name 452 | secretBackstory 453 | } 454 | } 455 | `; 456 | const result = await graphql(StarWarsSchema, query); 457 | expect(result).toEqualExecutionResult({ 458 | data: { 459 | hero: { 460 | name: "R2-D2", 461 | secretBackstory: null 462 | } 463 | }, 464 | errors: [ 465 | { 466 | message: "secretBackstory is secret.", 467 | locations: [{ line: 5, column: 13 }], 468 | path: ["hero", "secretBackstory"] 469 | } 470 | ] 471 | }); 472 | }); 473 | 474 | it("Correctly reports error on accessing secretBackstory in a list", async () => { 475 | const query = ` 476 | query HeroNameQuery { 477 | hero { 478 | name 479 | friends { 480 | name 481 | secretBackstory 482 | } 483 | } 484 | } 485 | `; 486 | const result = await graphql(StarWarsSchema, query); 487 | expect(result).toEqualExecutionResult({ 488 | data: { 489 | hero: { 490 | name: "R2-D2", 491 | friends: [ 492 | { 493 | name: "Luke Skywalker", 494 | secretBackstory: null 495 | }, 496 | { 497 | name: "Han Solo", 498 | secretBackstory: null 499 | }, 500 | { 501 | name: "Leia Organa", 502 | secretBackstory: null 503 | } 504 | ] 505 | } 506 | }, 507 | errors: [ 508 | { 509 | message: "secretBackstory is secret.", 510 | locations: [{ line: 7, column: 15 }], 511 | path: ["hero", "friends", 0, "secretBackstory"] 512 | }, 513 | { 514 | message: "secretBackstory is secret.", 515 | locations: [{ line: 7, column: 15 }], 516 | path: ["hero", "friends", 1, "secretBackstory"] 517 | }, 518 | { 519 | message: "secretBackstory is secret.", 520 | locations: [{ line: 7, column: 15 }], 521 | path: ["hero", "friends", 2, "secretBackstory"] 522 | } 523 | ] 524 | }); 525 | }); 526 | 527 | it("Correctly reports error on accessing through an alias", async () => { 528 | const query = ` 529 | query HeroNameQuery { 530 | mainHero: hero { 531 | name 532 | story: secretBackstory 533 | } 534 | } 535 | `; 536 | const result = await graphql(StarWarsSchema, query); 537 | expect(result).toEqualExecutionResult({ 538 | data: { 539 | mainHero: { 540 | name: "R2-D2", 541 | story: null 542 | } 543 | }, 544 | errors: [ 545 | { 546 | message: "secretBackstory is secret.", 547 | locations: [{ line: 5, column: 13 }], 548 | path: ["mainHero", "story"] 549 | } 550 | ] 551 | }); 552 | }); 553 | }); 554 | }); 555 | -------------------------------------------------------------------------------- /src/__tests__/reference/starWarsSchema.ts: -------------------------------------------------------------------------------- 1 | // source https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsSchema.js 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLEnumType, 13 | GraphQLInterfaceType, 14 | GraphQLObjectType, 15 | GraphQLList, 16 | GraphQLNonNull, 17 | GraphQLSchema, 18 | GraphQLString 19 | } from "graphql"; 20 | 21 | import { of, from, throwError } from "rxjs"; 22 | 23 | import { getFriends, getHero, getHuman, getDroid } from "./starWarsData"; 24 | 25 | /** 26 | * This is designed to be an end-to-end test, demonstrating 27 | * the full GraphQL stack. 28 | * 29 | * We will create a GraphQL schema that describes the major 30 | * characters in the original Star Wars trilogy. 31 | * 32 | * NOTE: This may contain spoilers for the original Star 33 | * Wars trilogy. 34 | */ 35 | 36 | /** 37 | * Using our shorthand to describe type systems, the type system for our 38 | * Star Wars example is: 39 | * 40 | * enum Episode { NEWHOPE, EMPIRE, JEDI } 41 | * 42 | * interface Character { 43 | * id: String! 44 | * name: String 45 | * friends: [Character] 46 | * appearsIn: [Episode] 47 | * } 48 | * 49 | * type Human implements Character { 50 | * id: String! 51 | * name: String 52 | * friends: [Character] 53 | * appearsIn: [Episode] 54 | * homePlanet: String 55 | * } 56 | * 57 | * type Droid implements Character { 58 | * id: String! 59 | * name: String 60 | * friends: [Character] 61 | * appearsIn: [Episode] 62 | * primaryFunction: String 63 | * } 64 | * 65 | * type Query { 66 | * hero(episode: Episode): Character 67 | * human(id: String!): Human 68 | * droid(id: String!): Droid 69 | * } 70 | * 71 | * We begin by setting up our schema. 72 | */ 73 | 74 | /** 75 | * The original trilogy consists of three movies. 76 | * 77 | * This implements the following type system shorthand: 78 | * enum Episode { NEWHOPE, EMPIRE, JEDI } 79 | */ 80 | const episodeEnum = new GraphQLEnumType({ 81 | name: "Episode", 82 | description: "One of the films in the Star Wars Trilogy", 83 | values: { 84 | NEWHOPE: { 85 | value: 4, 86 | description: "Released in 1977." 87 | }, 88 | EMPIRE: { 89 | value: 5, 90 | description: "Released in 1980." 91 | }, 92 | JEDI: { 93 | value: 6, 94 | description: "Released in 1983." 95 | } 96 | } 97 | }); 98 | 99 | /** 100 | * Characters in the Star Wars trilogy are either humans or droids. 101 | * 102 | * This implements the following type system shorthand: 103 | * interface Character { 104 | * id: String! 105 | * name: String 106 | * friends: [Character] 107 | * appearsIn: [Episode] 108 | * secretBackstory: String 109 | * } 110 | */ 111 | const characterInterface = new GraphQLInterfaceType({ 112 | name: "Character", 113 | description: "A character in the Star Wars Trilogy", 114 | fields: () => ({ 115 | id: { 116 | type: GraphQLNonNull(GraphQLString), 117 | description: "The id of the character." 118 | }, 119 | name: { 120 | type: GraphQLString, 121 | description: "The name of the character." 122 | }, 123 | friends: { 124 | type: GraphQLList(characterInterface), 125 | description: "The friends of the character, or an empty list." 126 | }, 127 | appearsIn: { 128 | type: GraphQLList(episodeEnum), 129 | description: "Which movies they appear in." 130 | }, 131 | secretBackstory: { 132 | type: GraphQLString, 133 | description: "All secrets about their past." 134 | } 135 | }), 136 | resolveType(character) { 137 | if (character.type === "Human") { 138 | return humanType; 139 | } 140 | if (character.type === "Droid") { 141 | return droidType; 142 | } 143 | } 144 | }); 145 | 146 | /** 147 | * We define our human type, which implements the character interface. 148 | * 149 | * This implements the following type system shorthand: 150 | * type Human : Character { 151 | * id: String! 152 | * name: String 153 | * friends: [Character] 154 | * appearsIn: [Episode] 155 | * secretBackstory: String 156 | * } 157 | */ 158 | const humanType = new GraphQLObjectType({ 159 | name: "Human", 160 | description: "A humanoid creature in the Star Wars universe.", 161 | fields: () => ({ 162 | id: { 163 | type: GraphQLNonNull(GraphQLString), 164 | description: "The id of the human." 165 | }, 166 | name: { 167 | type: GraphQLString, 168 | description: "The name of the human." 169 | }, 170 | friends: { 171 | type: GraphQLList(characterInterface), 172 | description: 173 | "The friends of the human, or an empty list if they have none.", 174 | resolve: human => from(Promise.all(getFriends(human))) 175 | }, 176 | appearsIn: { 177 | type: GraphQLList(episodeEnum), 178 | description: "Which movies they appear in." 179 | }, 180 | homePlanet: { 181 | type: GraphQLString, 182 | description: "The home planet of the human, or null if unknown." 183 | }, 184 | secretBackstory: { 185 | type: GraphQLString, 186 | description: "Where are they from and how they came to be who they are.", 187 | resolve() { 188 | return throwError(new Error("secretBackstory is secret.")); 189 | } 190 | } 191 | }), 192 | interfaces: [characterInterface] 193 | }); 194 | 195 | /** 196 | * The other type of character in Star Wars is a droid. 197 | * 198 | * This implements the following type system shorthand: 199 | * type Droid : Character { 200 | * id: String! 201 | * name: String 202 | * friends: [Character] 203 | * appearsIn: [Episode] 204 | * secretBackstory: String 205 | * primaryFunction: String 206 | * } 207 | */ 208 | const droidType = new GraphQLObjectType({ 209 | name: "Droid", 210 | description: "A mechanical creature in the Star Wars universe.", 211 | fields: () => ({ 212 | id: { 213 | type: GraphQLNonNull(GraphQLString), 214 | description: "The id of the droid." 215 | }, 216 | name: { 217 | type: GraphQLString, 218 | description: "The name of the droid." 219 | }, 220 | friends: { 221 | type: GraphQLList(characterInterface), 222 | description: 223 | "The friends of the droid, or an empty list if they have none.", 224 | resolve: droid => from(Promise.all(getFriends(droid))) 225 | }, 226 | appearsIn: { 227 | type: GraphQLList(episodeEnum), 228 | description: "Which movies they appear in." 229 | }, 230 | secretBackstory: { 231 | type: GraphQLString, 232 | description: "Construction date and the name of the designer.", 233 | resolve() { 234 | throw new Error("secretBackstory is secret."); 235 | } 236 | }, 237 | primaryFunction: { 238 | type: GraphQLString, 239 | description: "The primary function of the droid." 240 | } 241 | }), 242 | interfaces: [characterInterface] 243 | }); 244 | 245 | /** 246 | * This is the type that will be the root of our query, and the 247 | * entry point into our schema. It gives us the ability to fetch 248 | * objects by their IDs, as well as to fetch the undisputed hero 249 | * of the Star Wars trilogy, R2-D2, directly. 250 | * 251 | * This implements the following type system shorthand: 252 | * type Query { 253 | * hero(episode: Episode): Character 254 | * human(id: String!): Human 255 | * droid(id: String!): Droid 256 | * } 257 | * 258 | */ 259 | const queryType = new GraphQLObjectType({ 260 | name: "Query", 261 | fields: () => ({ 262 | hero: { 263 | type: characterInterface, 264 | args: { 265 | episode: { 266 | description: 267 | "If omitted, returns the hero of the whole saga. If " + 268 | "provided, returns the hero of that particular episode.", 269 | type: episodeEnum 270 | } 271 | }, 272 | resolve: (_, { episode }) => of(getHero(episode)) 273 | }, 274 | human: { 275 | type: humanType, 276 | args: { 277 | id: { 278 | description: "id of the human", 279 | type: GraphQLNonNull(GraphQLString) 280 | } 281 | }, 282 | resolve: (_, { id }) => of(getHuman(id)) 283 | }, 284 | droid: { 285 | type: droidType, 286 | args: { 287 | id: { 288 | description: "id of the droid", 289 | type: GraphQLNonNull(GraphQLString) 290 | } 291 | }, 292 | resolve: (_, { id }) => of(getDroid(id)) 293 | } 294 | }) 295 | }); 296 | 297 | /** 298 | * Finally, we construct our schema (whose starting query type is the query 299 | * type we defined above) and export it. 300 | */ 301 | export default new GraphQLSchema({ 302 | query: queryType, 303 | types: [humanType, droidType] 304 | }); 305 | -------------------------------------------------------------------------------- /src/execution/__tests__/rx-subscriptions-test.ts: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from "graphql-tools"; 2 | import { marbles } from "rxjs-marbles/jest"; 3 | import { TestObservableLike } from "rxjs-marbles/types"; 4 | import { parse } from "graphql"; 5 | import { execute } from "../.."; 6 | 7 | describe('Execution: Rx subscriptions management', () => { 8 | describe('subscription/unsubscription sycnhronization of resolved observable with result of query', () => { 9 | const executeScenario = ( 10 | revolvedValue$: TestObservableLike, 11 | ) => { 12 | const schema = makeExecutableSchema({ 13 | typeDefs: ` 14 | type Query { 15 | value: String! 16 | }`, 17 | resolvers: { 18 | Query: { 19 | value: () => revolvedValue$, 20 | } 21 | } 22 | }); 23 | 24 | return execute({ 25 | schema, 26 | document: parse(` 27 | query { 28 | value 29 | } 30 | `) 31 | }) 32 | } 33 | 34 | it('should wait for result subscription to subscribe to Observable returned by resolver', marbles(m => { 35 | const value$ = m.hot( 36 | '-a--b--c---' 37 | ) 38 | m.expect( 39 | executeScenario(value$), 40 | '--^--------' 41 | ).toBeObservable( 42 | '----B--C---', { 43 | B: { data: { value: 'b' } }, 44 | C: { data: { value: 'c' } }, 45 | } 46 | ) 47 | m.expect(value$).toHaveSubscriptions( 48 | '--^--------' 49 | ) 50 | })); 51 | 52 | it('should unsubsribe from Observable returned by resolver when unsubscribe from result', marbles(m => { 53 | const value$ = m.hot( 54 | '-a--b--c---' 55 | ) 56 | m.expect( 57 | executeScenario(value$), 58 | '^----!----' 59 | ).toBeObservable( 60 | '-A--B------', { 61 | A: { data: { value: 'a' } }, 62 | B: { data: { value: 'b' } }, 63 | } 64 | ) 65 | m.expect(value$).toHaveSubscriptions( 66 | '^----!----' 67 | ) 68 | })); 69 | }); 70 | 71 | describe('giving up a resolved Observable', () => { 72 | const executeScenario = ( 73 | currentEmitter$: TestObservableLike, 74 | emitter$s: { 75 | [key: string]: TestObservableLike, 76 | }, 77 | ) => { 78 | const schema = makeExecutableSchema({ 79 | typeDefs: ` 80 | type Emitter { 81 | value: String! 82 | } 83 | type Query { 84 | currentEmitter: Emitter! 85 | }`, 86 | resolvers: { 87 | Query: { 88 | currentEmitter: () => currentEmitter$, 89 | }, 90 | Emitter: { 91 | value: (p: string) => emitter$s[p] 92 | } 93 | } 94 | }); 95 | 96 | return execute({ 97 | schema, 98 | document: parse(` 99 | query { 100 | currentEmitter { 101 | value 102 | } 103 | } 104 | `) 105 | }) 106 | } 107 | 108 | it('should unsubscribe from it (switchMap)', marbles(m => { 109 | const currentEmitter$ = m.hot( 110 | '-A-----B---' 111 | ); 112 | const emitter$s = { 113 | A: m.hot('aaaaaaaaaaa'), 114 | B: m.hot('bbbbbbbbbbb'), 115 | } 116 | m.expect( 117 | executeScenario(currentEmitter$, emitter$s), 118 | '^---------!' 119 | ).toBeObservable( 120 | // -A-----B--- 121 | '-aaaaaabbb-', { 122 | a: { data: { currentEmitter: { value: 'a' }}}, 123 | b: { data: { currentEmitter: { value: 'b' }}}, 124 | } 125 | ) 126 | m.expect(emitter$s.A).toHaveSubscriptions( 127 | // -A-----B--- 128 | '-^-----!--' 129 | ) 130 | m.expect(emitter$s.B).toHaveSubscriptions( 131 | // -A-----B--- 132 | '-------^--!' 133 | ) 134 | })); 135 | }) 136 | }); 137 | -------------------------------------------------------------------------------- /src/execution/execute.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `execute.ts` is the equivalent of `graphql-js`'s `src/execution/execute.js`: 3 | * it follows the same structure and same function names. 4 | * 5 | * The implementation of each function is very close to its sibling in `graphql-js` 6 | * and, for the most part, is just adapted for reactive execution (dealing with `Observable`). 7 | * Some functions are just copy-pasted because they did not require any change 8 | * but could not be imported from `graphql-js`. 9 | * Some functions are not present because they could be imported from `graphql-js`. 10 | */ 11 | import { Observable, of, from, isObservable, combineLatest, throwError } from "rxjs"; 12 | import { map, catchError, switchMap } from "rxjs/operators"; 13 | import { forEach, isIterable } from "iterall"; 14 | import memoize from "memoizee"; 15 | import { 16 | ExecutionResult, 17 | DocumentNode, 18 | GraphQLObjectType, 19 | GraphQLSchema, 20 | FieldNode, 21 | GraphQLField, 22 | GraphQLOutputType, 23 | GraphQLFieldResolver, 24 | isObjectType, 25 | isNonNullType, 26 | responsePathAsArray, 27 | ExecutionArgs, 28 | OperationDefinitionNode, 29 | ResponsePath, 30 | GraphQLResolveInfo, 31 | GraphQLError, 32 | getOperationRootType, 33 | GraphQLList, 34 | GraphQLLeafType, 35 | isListType, 36 | isLeafType, 37 | isAbstractType, 38 | GraphQLAbstractType, 39 | } from "graphql"; 40 | 41 | import { 42 | ExecutionResultDataDefault, 43 | assertValidExecutionArguments, 44 | buildExecutionContext, 45 | ExecutionContext, 46 | collectFields, 47 | buildResolveInfo, 48 | getFieldDef, 49 | } from 'graphql/execution/execute'; 50 | import Maybe from 'graphql/tsutils/Maybe'; 51 | import { addPath } from "graphql/jsutils/Path"; 52 | import combinePropsLatest from "../rxutils/combinePropsLatest"; 53 | import { getArgumentValues } from "graphql/execution/values"; 54 | import { locatedError } from "graphql/error"; 55 | import invariant from "../jstutils/invariant"; 56 | import isInvalid from "../jstutils/isInvalid"; 57 | import inspect from "../jstutils/inspect"; 58 | import isNullish from "../jstutils/isNullish"; 59 | import mapPromiseToObservale from "../rxutils/mapPromiseToObservale"; 60 | import mapToFirstValue from "../rxutils/mapToFirstValue"; 61 | 62 | /** 63 | * Implements the "Evaluating requests" section of the GraphQL specification. 64 | * 65 | * Returns a RxJS's `Observable` of `ExecutionResult`. 66 | * 67 | * Note: reactive equivalent of `graphql-js`'s `execute`. 68 | */ 69 | export function execute(args: ExecutionArgs) 70 | : Observable>; 71 | export function execute( 72 | schema: GraphQLSchema, 73 | document: DocumentNode, 74 | rootValue?: unknown, 75 | contextValue?: unknown, 76 | variableValues?: Maybe<{ [key: string]: unknown }>, 77 | operationName?: Maybe, 78 | fieldResolver?: Maybe> 79 | ): Observable>; 80 | export function execute( 81 | argsOrSchema, 82 | document?, 83 | rootValue?, 84 | contextValue?, 85 | variableValues?, 86 | operationName?, 87 | fieldResolver?, 88 | ) { 89 | return isExecutionArgs(argsOrSchema, arguments) 90 | ? executeImpl( 91 | argsOrSchema.schema, 92 | argsOrSchema.document, 93 | argsOrSchema.rootValue, 94 | argsOrSchema.contextValue, 95 | argsOrSchema.variableValues, 96 | argsOrSchema.operationName, 97 | argsOrSchema.fieldResolver, 98 | ) 99 | : executeImpl( 100 | argsOrSchema, 101 | document, 102 | rootValue, 103 | contextValue, 104 | variableValues, 105 | operationName, 106 | fieldResolver, 107 | ); 108 | } 109 | 110 | function isExecutionArgs( 111 | _argsOrSchema: GraphQLSchema | ExecutionArgs, 112 | args: IArguments 113 | ): _argsOrSchema is ExecutionArgs { 114 | return args.length === 1; 115 | } 116 | 117 | function executeImpl( 118 | schema: GraphQLSchema, 119 | document: DocumentNode, 120 | rootValue?: unknown, 121 | contextValue?: unknown, 122 | variableValues?: Maybe<{ [key: string]: unknown }>, 123 | operationName?: Maybe, 124 | fieldResolver?: Maybe> 125 | ): Observable> { 126 | // If arguments are missing or incorrect, throw an error. 127 | assertValidExecutionArguments(schema, document, variableValues); 128 | 129 | // If a valid execution context cannot be created due to incorrect arguments, 130 | // a "Response" with only errors is returned. 131 | const exeContext = buildExecutionContext( 132 | schema, 133 | document, 134 | rootValue, 135 | contextValue, 136 | variableValues, 137 | operationName, 138 | fieldResolver, 139 | ); 140 | 141 | // Return early errors if execution context failed. 142 | if (!isValidExecutionContext(exeContext)) { 143 | return of({ errors: exeContext }); 144 | } 145 | 146 | const data = executeOperation(exeContext, exeContext.operation, rootValue); 147 | return buildResponse(exeContext, data); 148 | } 149 | 150 | /** 151 | * Returns true if subject is a valid `ExecutionContext` and not array of `GraphQLError`. 152 | * 153 | * Note: reference implementation does a `Array.isArray` in `executeImpl` function body. In comparison, 154 | * `isValidExecutionContext` ensures typing correctness with type assertion. 155 | * @param subject value to be tested 156 | */ 157 | function isValidExecutionContext(subject: ReadonlyArray | ExecutionContext): subject is ExecutionContext { 158 | return !Array.isArray(subject); 159 | } 160 | 161 | /** 162 | * Given a completed execution context and data as Observable, build the `{ errors, data }` 163 | * response defined by the "Response" section of the GraphQL specification. 164 | * 165 | * Note: reactive equivalent of `graphql-js`'s `buildResponse`. 166 | */ 167 | function buildResponse( 168 | exeContext: ExecutionContext, 169 | data: Observable<{ [key: string]: unknown} | null> 170 | ): Observable> { 171 | // @ts-ignore `'{ [key: string]: unknown; }' is assignable to the constraint of type 'TData', but 'TData' could be instantiated with a different subtype of constraint '{}'` 172 | return data.pipe(map(d => { 173 | if (exeContext.errors.length === 0 && d !== null) { 174 | return { 175 | data: d, 176 | } 177 | } else { 178 | return { 179 | errors: exeContext.errors, 180 | data: d, 181 | } 182 | } 183 | })) 184 | } 185 | 186 | /** 187 | * Implements the "Evaluating operations" section of the spec. 188 | * 189 | * Note: reactive equivalent of `graphql-js`'s `executeOperation`. The difference lies 190 | * in the fact that, here, a RxJS's `Observable` is returned instead of a `Promise` (or plain value). 191 | */ 192 | function executeOperation( 193 | exeContext: ExecutionContext, 194 | operation: OperationDefinitionNode, 195 | rootValue: unknown 196 | ): Observable<({ [key: string]: unknown }) | null> { 197 | const type = getOperationRootType(exeContext.schema, operation); 198 | const fields = collectFields( 199 | exeContext, 200 | type, 201 | operation.selectionSet, 202 | Object.create(null), 203 | Object.create(null), 204 | ); 205 | 206 | const path = undefined; 207 | 208 | // Errors from sub-fields of a NonNull type may propagate to the top level, 209 | // at which point we still log the error and null the parent field, which 210 | // in this case is the entire response. 211 | // 212 | // Similar to completeValueCatchingError. 213 | try { 214 | const result = 215 | operation.operation === 'mutation' 216 | ? executeFieldsSerially(exeContext, type, rootValue, path, fields) 217 | : executeFields(exeContext, type, rootValue, path, fields); 218 | return result; 219 | } catch (error) { 220 | exeContext.errors.push(error); 221 | return of(null); 222 | } 223 | } 224 | 225 | /** 226 | * Implements the "Evaluating selection sets" section of the spec for "write" mode, 227 | * ie with serial execution. 228 | * 229 | * Note: reactive equivalent of `graphql-js`'s `executeFieldsSerially`. The difference 230 | * lies in the fact that: 231 | * - here, a RxJS's `Observable` is returned instead of a `Promise` (or plain value) 232 | * - in `graphql-js`, serial execution is implemented by waiting, one by one, for the 233 | * resolution of the `Promise` returned by the resolution of each `field`. Here we wait 234 | * for the first value of the resolved `Observable` to be emited before passing to 235 | * the next field resolution. 236 | * 237 | * Thus, in case of resolvers resolving `Promises`, we match 238 | * reference implementation's behavior. 239 | */ 240 | function executeFieldsSerially( 241 | exeContext: ExecutionContext, 242 | parentType: GraphQLObjectType, 243 | sourceValue: unknown, 244 | path: ResponsePath | undefined, 245 | fields: { [key: string]: FieldNode[]} 246 | ): Observable<{ [key: string]: unknown }> { 247 | const results: { [key: string]: Observable } = {}; 248 | 249 | // during iteration, we keep track of the result of the previously 250 | // resolved field so that we can queue the resolution of the next field 251 | // after the emition of the first value of the previous result. 252 | let previousResolvedResult: (Observable | undefined); 253 | 254 | for (let i = 0, keys = Object.keys(fields); i < keys.length; ++i) { 255 | const responseName = keys[i]; 256 | const fieldNodes = fields[responseName]; 257 | const fieldPath = addPath(path, responseName); 258 | 259 | const resolve = () => resolveField( 260 | exeContext, 261 | parentType, 262 | sourceValue, 263 | fieldNodes, 264 | fieldPath, 265 | ); 266 | 267 | const result = previousResolvedResult ? 268 | // queuing `resolve` after first emition of `previousResolvedResult` 269 | // using `mapToFirstValue` to get an Observable that represents this process 270 | mapToFirstValue(previousResolvedResult, resolve) 271 | : 272 | // first iteration: no previous result need to queue after 273 | resolve(); 274 | 275 | previousResolvedResult = result; 276 | 277 | if (result !== undefined) { 278 | results[responseName] = result; 279 | } 280 | } 281 | 282 | return combinePropsLatest(results); 283 | } 284 | 285 | /** 286 | * Implements the "Evaluating selection sets" section of the spec 287 | * for "read" mode. 288 | * 289 | * Note: reactive equivalent of `graphql-js`'s `executeFields`. The difference lies 290 | * in the fact that, here, a RxJS's `Observable` is returned instead of a `Promise` (or plain value). 291 | */ 292 | function executeFields( 293 | exeContext: ExecutionContext, 294 | parentType: GraphQLObjectType, 295 | sourceValue: unknown, 296 | path: ResponsePath | undefined, 297 | fields: { [key: string]: FieldNode[] } 298 | ): Observable<{ [key: string]: unknown }> { 299 | const results: { [key: string]: Observable } = {}; 300 | 301 | for (let i = 0, keys = Object.keys(fields); i < keys.length; ++i) { 302 | const responseName = keys[i]; 303 | const fieldNodes = fields[responseName]; 304 | const fieldPath = addPath(path, responseName); 305 | const result = resolveField( 306 | exeContext, 307 | parentType, 308 | sourceValue, 309 | fieldNodes, 310 | fieldPath, 311 | ); 312 | 313 | if (result !== undefined) { 314 | results[responseName] = result; 315 | } 316 | } 317 | 318 | return combinePropsLatest(results); 319 | } 320 | 321 | /** 322 | * Resolves the field on the given source object. 323 | * 324 | * Note: reactive equivalent of `graphql-js`'s `resolveField`. The difference lies 325 | * in the fact that, here, a RxJS's `Observable` is returned instead of a `Promise` (or plain value). 326 | */ 327 | function resolveField( 328 | exeContext: ExecutionContext, 329 | parentType: GraphQLObjectType, 330 | source: unknown, 331 | fieldNodes: FieldNode[], 332 | path: ResponsePath, 333 | ): Observable { 334 | const fieldNode = fieldNodes[0]; 335 | const fieldName = fieldNode.name.value; 336 | 337 | const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName); 338 | if (!fieldDef) { 339 | return of(undefined); 340 | } 341 | 342 | const resolveFn = fieldDef.resolve || exeContext.fieldResolver; 343 | 344 | const info = buildResolveInfo( 345 | exeContext, 346 | fieldDef, 347 | fieldNodes, 348 | parentType, 349 | path, 350 | ); 351 | 352 | const result = resolveFieldValueOrError( 353 | exeContext, 354 | fieldDef, 355 | fieldNodes, 356 | resolveFn, 357 | source, 358 | info, 359 | ); 360 | 361 | return completeValueCatchingError( 362 | exeContext, 363 | fieldDef.type, 364 | fieldNodes, 365 | info, 366 | path, 367 | result, 368 | ); 369 | } 370 | 371 | /** 372 | * Note: reactive equivalent of `graphql-js`'s `resolveFieldValueOrError`. The difference lies 373 | * in the fact that, here, a RxJS's `Observable` is returned. 374 | */ 375 | function resolveFieldValueOrError( 376 | exeContext: ExecutionContext, 377 | fieldDef: GraphQLField, 378 | fieldNodes: ReadonlyArray, 379 | resolveFn: GraphQLFieldResolver, 380 | source: TSource, 381 | info: GraphQLResolveInfo 382 | ): (Error | Observable) { 383 | try { 384 | const args = getArgumentValues( 385 | fieldDef, 386 | fieldNodes[0], 387 | exeContext.variableValues, 388 | ); 389 | 390 | const contextValue = exeContext.contextValue; 391 | 392 | const result = resolveFn(source, args, contextValue, info); 393 | 394 | if (isObservable(result)) { 395 | return result 396 | .pipe(catchError(err => throwError(asErrorInstance(err)))); 397 | } 398 | 399 | if (result instanceof Promise) { 400 | return from(result) 401 | .pipe(catchError(err => throwError(asErrorInstance(err)))); 402 | } 403 | 404 | // it lloks like plain value 405 | return of(result) 406 | } catch (err) { 407 | return asErrorInstance(err); 408 | } 409 | } 410 | 411 | /** 412 | * Sometimes a non-error is thrown, wrap it as an Error instance to ensure a 413 | * consistent Error interface. 414 | * 415 | * Note: copy-paste of `graphql-js`'s `asErrorInstance` in `execute.js`. 416 | */ 417 | function asErrorInstance(error: unknown): Error { 418 | if (error instanceof Error) { 419 | return error; 420 | } 421 | return new Error('Unexpected error value: ' + inspect(error)); 422 | } 423 | 424 | /** 425 | * This is a small wrapper around completeValue which detects and logs errors 426 | * in the execution context. 427 | * 428 | * Note: reactive equivalent of `graphql-js`'s `completeValueCatchingError`. The difference lies 429 | * in the fact that, here, a RxJS's `Observable` is returned. 430 | */ 431 | function completeValueCatchingError( 432 | exeContext: ExecutionContext, 433 | returnType: GraphQLOutputType, 434 | fieldNodes: ReadonlyArray, 435 | info: GraphQLResolveInfo, 436 | path: ResponsePath, 437 | result: Error | Observable, 438 | ): Observable { 439 | if (result instanceof Error) { 440 | return of(handleFieldError( 441 | result, 442 | fieldNodes, 443 | path, 444 | returnType, 445 | exeContext, 446 | )); 447 | } 448 | try { 449 | return result 450 | .pipe( 451 | switchMap(res => completeValue( 452 | exeContext, 453 | returnType, 454 | fieldNodes, 455 | info, 456 | path, 457 | res, 458 | ) 459 | ) 460 | ) 461 | .pipe(catchError(err => of(handleFieldError( 462 | asErrorInstance(err), 463 | fieldNodes, 464 | path, 465 | returnType, 466 | exeContext, 467 | )))) 468 | } catch (error) { 469 | return of(handleFieldError( 470 | asErrorInstance(error), 471 | fieldNodes, 472 | path, 473 | returnType, 474 | exeContext, 475 | )) 476 | } 477 | } 478 | 479 | /** 480 | * Note: copy-paste of `graphql-js`'s `handleFieldError`. 481 | */ 482 | function handleFieldError( 483 | rawError: Error, 484 | fieldNodes: ReadonlyArray, 485 | path: ResponsePath, 486 | returnType: GraphQLOutputType, 487 | exeContext: ExecutionContext, 488 | ): null { 489 | const error = locatedError( 490 | asErrorInstance(rawError), 491 | fieldNodes, 492 | responsePathAsArray(path), 493 | ); 494 | 495 | // If the field type is non-nullable, then it is resolved without any 496 | // protection from errors, however it still properly locates the error. 497 | if (isNonNullType(returnType)) { 498 | throw error; 499 | } 500 | 501 | // Otherwise, error protection is applied, logging the error and resolving 502 | // a null value for this field if one is encountered. 503 | exeContext.errors.push(error); 504 | return null; 505 | } 506 | 507 | /** 508 | * Implements the instructions for completeValue as defined in the 509 | * "Field entries" section of the spec. 510 | * 511 | * Note: reactive equivalent of `graphql-js`'s `completeValue`. The difference lies 512 | * in the fact that, here, we deal with RxJS's `Observable` and an `Observable` is returned. 513 | */ 514 | function completeValue( 515 | exeContext: ExecutionContext, 516 | returnType: GraphQLOutputType, 517 | fieldNodes: ReadonlyArray, 518 | info: GraphQLResolveInfo, 519 | path: ResponsePath, 520 | result: unknown, 521 | ): Observable { 522 | // If result is an Error, throw a located error. 523 | if (result instanceof Error) { 524 | throw result; 525 | } 526 | 527 | // If field type is NonNull, complete for inner type, and throw field error 528 | // if result is null. 529 | if (isNonNullType(returnType)) { 530 | const completed = completeValue( 531 | exeContext, 532 | returnType.ofType, 533 | fieldNodes, 534 | info, 535 | path, 536 | result, 537 | ); 538 | if (completed === null) { 539 | throw new Error( 540 | `Cannot return null for non-nullable field ${info.parentType.name}.${ 541 | info.fieldName 542 | }.`, 543 | ); 544 | } 545 | return completed; 546 | } 547 | 548 | // If result value is null-ish (null, undefined, or NaN) then return null. 549 | if (isNullish(result)) { 550 | return of(null); 551 | } 552 | 553 | // If field type is List, complete each item in the list with the inner type 554 | if (isListType(returnType)) { 555 | return completeListValue( 556 | exeContext, 557 | returnType, 558 | fieldNodes, 559 | info, 560 | path, 561 | result, 562 | ); 563 | } 564 | 565 | // If field type is a leaf type, Scalar or Enum, serialize to a valid value, 566 | // returning null if serialization is not possible. 567 | if (isLeafType(returnType)) { 568 | return completeLeafValue(returnType, result); 569 | } 570 | 571 | // If field type is an abstract type, Interface or Union, determine the 572 | // runtime Object type and complete for that type. 573 | if (isAbstractType(returnType)) { 574 | return completeAbstractValue( 575 | exeContext, 576 | returnType, 577 | fieldNodes, 578 | info, 579 | path, 580 | result, 581 | ); 582 | } 583 | 584 | // If field type is Object, execute and complete all sub-selections. 585 | if (isObjectType(returnType)) { 586 | return completeObjectValue( 587 | exeContext, 588 | returnType, 589 | fieldNodes, 590 | info, 591 | path, 592 | result, 593 | ); 594 | } 595 | 596 | // Not reachable. All possible output types have been considered. 597 | /* istanbul ignore next */ 598 | throw new Error( 599 | `Cannot complete value of unexpected type "${inspect( 600 | (returnType), 601 | )}".`, 602 | ); 603 | }; 604 | 605 | /** 606 | * Complete a list value by completing each item in the list with the 607 | * inner type 608 | * 609 | * Note: reactive equivalent of `graphql-js`'s `completeListValue`. The difference lies 610 | * in the fact that, here, we deal with RxJS's `Observable` and an `Observable` is returned. 611 | */ 612 | function completeListValue( 613 | exeContext: ExecutionContext, 614 | returnType: GraphQLList, 615 | fieldNodes: ReadonlyArray, 616 | info: GraphQLResolveInfo, 617 | path: ResponsePath, 618 | result: unknown, 619 | ): Observable> { 620 | invariant( 621 | isIterable(result), 622 | `Expected Iterable, but did not find one for field ${ 623 | info.parentType.name 624 | }.${info.fieldName}.`, 625 | ); 626 | 627 | // for typescript only: asserts `result` type 628 | if (!isIterable(result)) throw new Error('Expected Iterable'); 629 | 630 | const itemType = returnType.ofType; 631 | const completedResults: Observable[] = []; 632 | 633 | forEach(result, (item, index) => { 634 | const fieldPath = addPath(path, index); 635 | const completedItem = completeValueCatchingError( 636 | exeContext, 637 | itemType, 638 | fieldNodes, 639 | info, 640 | fieldPath, 641 | of(item), 642 | ); 643 | completedResults.push(completedItem); 644 | }); 645 | 646 | // avoid blocking in switchMap with empty arrays 647 | if (completedResults.length === 0) return of([]); 648 | return combineLatest(completedResults); 649 | } 650 | /** 651 | * Complete a Scalar or Enum by serializing to a valid value, returning 652 | * null if serialization is not possible. 653 | * 654 | * Note: reactive equivalent of `graphql-js`'s `completeLeafValue`. The difference lies 655 | * in the fact that, here, an `Observable` is returned. 656 | */ 657 | function completeLeafValue(returnType: GraphQLLeafType, result: unknown): Observable { 658 | invariant(returnType.serialize, 'Missing serialize method on type'); 659 | const serializedResult = returnType.serialize(result); 660 | if (isInvalid(serializedResult)) { 661 | throw new Error( 662 | `Expected a value of type "${inspect(returnType)}" but ` + 663 | `received: ${inspect(result)}`, 664 | ); 665 | } 666 | return of(serializedResult); 667 | } 668 | /** 669 | * Complete a value of an abstract type by determining the runtime object type 670 | * of that value, then complete the value for that type. 671 | * 672 | * Note: reactive equivalent of `graphql-js`'s `completeAbstractValue`. The difference lies 673 | * in the fact that, here, we deal with asychronisity in a Observable fashion and that 674 | * an `Observable` is returned. 675 | */ 676 | function completeAbstractValue( 677 | exeContext: ExecutionContext, 678 | returnType: GraphQLAbstractType, 679 | fieldNodes: ReadonlyArray, 680 | info: GraphQLResolveInfo, 681 | path: ResponsePath, 682 | result: unknown, 683 | ): Observable<{ [key: string]: unknown }> { 684 | const runtimeType = returnType.resolveType 685 | ? returnType.resolveType(result, exeContext.contextValue, info) 686 | : defaultResolveTypeFn(result, exeContext.contextValue, info, returnType); 687 | 688 | return mapPromiseToObservale( 689 | Promise.resolve(runtimeType), 690 | resolvedRuntimeType => completeObjectValue( 691 | exeContext, 692 | ensureValidRuntimeType( 693 | resolvedRuntimeType, 694 | exeContext, 695 | returnType, 696 | fieldNodes, 697 | info, 698 | result, 699 | ), 700 | fieldNodes, 701 | info, 702 | path, 703 | result, 704 | ) 705 | ) 706 | } 707 | 708 | /** 709 | * Note: copy-pasted from `graphql-js`'s `ensureValidRuntimeType`. 710 | */ 711 | function ensureValidRuntimeType( 712 | runtimeTypeOrName: Maybe | string, 713 | exeContext: ExecutionContext, 714 | returnType: GraphQLAbstractType, 715 | fieldNodes: ReadonlyArray, 716 | info: GraphQLResolveInfo, 717 | result: unknown, 718 | ): GraphQLObjectType { 719 | const runtimeType = 720 | typeof runtimeTypeOrName === 'string' 721 | ? exeContext.schema.getType(runtimeTypeOrName) 722 | : runtimeTypeOrName; 723 | 724 | if (!isObjectType(runtimeType)) { 725 | throw new GraphQLError( 726 | `Abstract type ${returnType.name} must resolve to an Object type at ` + 727 | `runtime for field ${info.parentType.name}.${info.fieldName} with ` + 728 | `value ${inspect(result)}, received "${inspect(runtimeType)}". ` + 729 | `Either the ${returnType.name} type should provide a "resolveType" ` + 730 | 'function or each possible type should provide an "isTypeOf" function.', 731 | fieldNodes, 732 | ); 733 | } 734 | 735 | if (!exeContext.schema.isPossibleType(returnType, runtimeType)) { 736 | throw new GraphQLError( 737 | `Runtime Object type "${runtimeType.name}" is not a possible type ` + 738 | `for "${returnType.name}".`, 739 | fieldNodes, 740 | ); 741 | } 742 | 743 | return runtimeType; 744 | } 745 | 746 | /** 747 | * Complete an Object value by executing all sub-selections. 748 | * 749 | * Note: reactive equivalent of `graphql-js`'s `completeObjectValue`. The difference lies 750 | * in the fact that, here, we deal with asychronisity in a Observable fashion and that 751 | * an `Observable` is returned. 752 | */ 753 | function completeObjectValue( 754 | exeContext: ExecutionContext, 755 | returnType: GraphQLObjectType, 756 | fieldNodes: ReadonlyArray, 757 | info: GraphQLResolveInfo, 758 | path: ResponsePath, 759 | result: unknown, 760 | ): Observable<{ [key: string]: unknown }> { 761 | // If there is an isTypeOf predicate function, call it with the 762 | // current result. If isTypeOf returns false, then raise an error rather 763 | // than continuing execution. 764 | if (returnType.isTypeOf) { 765 | const isTypeOf = returnType.isTypeOf(result, exeContext.contextValue, info); 766 | 767 | if (isTypeOf instanceof Promise) { 768 | return mapPromiseToObservale( 769 | isTypeOf, 770 | resolvedIsTypeOf => { 771 | if (!resolvedIsTypeOf) { 772 | throw invalidReturnTypeError(returnType, result, fieldNodes); 773 | } 774 | 775 | return collectAndExecuteSubfields( 776 | exeContext, 777 | returnType, 778 | fieldNodes, 779 | path, 780 | result, 781 | ); 782 | }) 783 | } 784 | 785 | if (!isTypeOf) { 786 | throw invalidReturnTypeError(returnType, result, fieldNodes); 787 | } 788 | } 789 | 790 | return collectAndExecuteSubfields( 791 | exeContext, 792 | returnType, 793 | fieldNodes, 794 | path, 795 | result, 796 | ); 797 | } 798 | 799 | /** 800 | * 801 | * Note: copy-pasted from `graphql-js`. 802 | */ 803 | function invalidReturnTypeError( 804 | returnType: GraphQLObjectType, 805 | result: unknown, 806 | fieldNodes: ReadonlyArray, 807 | ): GraphQLError { 808 | return new GraphQLError( 809 | `Expected value of type "${returnType.name}" but got: ${inspect(result)}.`, 810 | fieldNodes, 811 | ); 812 | } 813 | 814 | /** 815 | * 816 | * Note: reactive equivalent of `graphql-js`'s `collectAndExecuteSubfields`. The difference lies 817 | * in the fact that, here, an `Observable` is returned. 818 | */ 819 | function collectAndExecuteSubfields( 820 | exeContext: ExecutionContext, 821 | returnType: GraphQLObjectType, 822 | fieldNodes: ReadonlyArray, 823 | path: ResponsePath, 824 | result: unknown, 825 | ): Observable<{ [key: string]: unknown }> { 826 | // Collect sub-fields to execute to complete this value. 827 | const subFieldNodes = collectSubfields(exeContext, returnType, fieldNodes); 828 | return executeFields(exeContext, returnType, result, path, subFieldNodes); 829 | } 830 | 831 | /** 832 | * A memoized collection of relevant subfields with regard to the return 833 | * type. Memoizing ensures the subfields are not repeatedly calculated, which 834 | * saves overhead when resolving lists of values. 835 | * 836 | * Note: copy-pasted from `graphql-js`. The difference lies in the fact that 837 | * `graphql-js` implements its own memoization, while we use `memoizee` package. 838 | */ 839 | const collectSubfields = memoize(_collectSubfields); 840 | function _collectSubfields( 841 | exeContext: ExecutionContext, 842 | returnType: GraphQLObjectType, 843 | fieldNodes: ReadonlyArray, 844 | ): { [key: string]: FieldNode[] } { 845 | let subFieldNodes = Object.create(null); 846 | const visitedFragmentNames = Object.create(null); 847 | for (let i = 0; i < fieldNodes.length; i++) { 848 | const selectionSet = fieldNodes[i].selectionSet; 849 | if (selectionSet) { 850 | subFieldNodes = collectFields( 851 | exeContext, 852 | returnType, 853 | selectionSet, 854 | subFieldNodes, 855 | visitedFragmentNames, 856 | ); 857 | } 858 | } 859 | return subFieldNodes; 860 | } 861 | 862 | type MaybePromise = T | Promise; 863 | 864 | /** 865 | * If a resolveType function is not given, then a default resolve behavior is 866 | * used which attempts two strategies: 867 | * 868 | * First, See if the provided value has a `__typename` field defined, if so, use 869 | * that value as name of the resolved type. 870 | * 871 | * Otherwise, test each possible type for the abstract type by calling 872 | * isTypeOf for the object being coerced, returning the first type that matches. 873 | * Note: copy-pasted from `graphql-js` 874 | */ 875 | function defaultResolveTypeFn( 876 | value: unknown, 877 | contextValue: unknown, 878 | info: GraphQLResolveInfo, 879 | abstractType: GraphQLAbstractType, 880 | ): MaybePromise | string> { 881 | // First, look for `__typename`. 882 | if ( 883 | typeof value === 'object' && 884 | value !== null && 885 | typeof value['__typename'] === 'string' 886 | ) { 887 | return value['__typename']; 888 | } 889 | 890 | // Otherwise, test each possible type. 891 | const possibleTypes = info.schema.getPossibleTypes(abstractType); 892 | const promisedIsTypeOfResults: Promise[] = []; 893 | 894 | for (let i = 0; i < possibleTypes.length; i++) { 895 | const type = possibleTypes[i]; 896 | 897 | if (type.isTypeOf) { 898 | const isTypeOfResult = type.isTypeOf(value, contextValue, info); 899 | 900 | if (isTypeOfResult instanceof Promise) { 901 | promisedIsTypeOfResults[i] = isTypeOfResult; 902 | } else if (isTypeOfResult) { 903 | return type; 904 | } 905 | } 906 | } 907 | 908 | if (promisedIsTypeOfResults.length) { 909 | return Promise.all(promisedIsTypeOfResults).then(isTypeOfResults => { 910 | for (let i = 0; i < isTypeOfResults.length; i++) { 911 | if (isTypeOfResults[i]) { 912 | return possibleTypes[i]; 913 | } 914 | } 915 | }); 916 | } 917 | } 918 | -------------------------------------------------------------------------------- /src/execution/index.ts: -------------------------------------------------------------------------------- 1 | export { execute } from './execute'; -------------------------------------------------------------------------------- /src/graphql.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | import { execute } from "./execution/execute"; 3 | import { ExecutionResult, validateSchema, parse, validate, GraphQLSchema, Source, GraphQLFieldResolver, DocumentNode } from "graphql"; 4 | import Maybe from "graphql/tsutils/Maybe"; 5 | 6 | export type GraphQLArgs = { 7 | schema: GraphQLSchema; 8 | source: string | Source; 9 | rootValue?: unknown; 10 | contextValue?: unknown; 11 | variableValues?: Maybe<{ [key: string]: unknown }>, 12 | operationName?: string | null; 13 | fieldResolver?: GraphQLFieldResolver | null; 14 | }; 15 | 16 | function isGraphQLArgs( 17 | _argsOrSchema: GraphQLSchema | GraphQLArgs, 18 | args: IArguments 19 | ): _argsOrSchema is GraphQLArgs { 20 | return args.length === 1; 21 | } 22 | 23 | export function graphql(arg0: GraphQLArgs): Observable>; 24 | 25 | export function graphql( 26 | schema: GraphQLSchema, 27 | source: Source | string, 28 | rootValue?: unknown, 29 | contextValue?: unknown, 30 | variableValues?: Maybe<{ [key: string]: unknown }>, 31 | operationName?: string | null, 32 | fieldResolver?: GraphQLFieldResolver | null, 33 | ): Observable>; 34 | 35 | export function graphql( 36 | argsOrSchema, 37 | source?, 38 | rootValue?, 39 | contextValue?, 40 | variableValues?, 41 | operationName?, 42 | fieldResolver?, 43 | 44 | ) { 45 | return isGraphQLArgs(argsOrSchema, arguments) 46 | ? graphqlImpl( 47 | argsOrSchema.schema, 48 | argsOrSchema.source, 49 | argsOrSchema.rootValue, 50 | argsOrSchema.contextValue, 51 | argsOrSchema.variableValues, 52 | argsOrSchema.operationName, 53 | argsOrSchema.fieldResolver, 54 | ) 55 | : graphqlImpl( 56 | argsOrSchema, 57 | source, 58 | rootValue, 59 | contextValue, 60 | variableValues, 61 | operationName, 62 | fieldResolver, 63 | ); 64 | } 65 | 66 | function graphqlImpl( 67 | schema: GraphQLSchema, 68 | source: string | Source, 69 | rootValue?: any, 70 | contextValue?: any, 71 | variableValues?: Maybe<{ [key: string]: unknown }>, 72 | operationName?: string | null, 73 | fieldResolver?: GraphQLFieldResolver | null, 74 | ): Observable> { 75 | // Validate Schema 76 | const schemaValidationErrors = validateSchema(schema); 77 | if (schemaValidationErrors.length > 0) { 78 | return of({ errors: schemaValidationErrors }); 79 | } 80 | 81 | // Parse 82 | let document: DocumentNode; 83 | try { 84 | document = parse(source); 85 | } catch (syntaxError) { 86 | return of({ errors: [syntaxError] }); 87 | } 88 | 89 | // Validate 90 | const validationErrors = validate(schema, document); 91 | if (validationErrors.length > 0) { 92 | return of({ errors: validationErrors }); 93 | } 94 | 95 | // Execute 96 | return execute( 97 | schema, 98 | document, 99 | rootValue, 100 | contextValue, 101 | variableValues, 102 | operationName, 103 | fieldResolver, 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | graphql 3 | } from './graphql'; 4 | 5 | export { 6 | execute, 7 | } from './execution'; 8 | -------------------------------------------------------------------------------- /src/jstutils/inspect.ts: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | 3 | import nodejsCustomInspectSymbol from './nodejsCustomInspectSymbol'; 4 | 5 | const MAX_ARRAY_LENGTH = 10; 6 | const MAX_RECURSIVE_DEPTH = 2; 7 | 8 | /** 9 | * Used to print values in error messages. 10 | */ 11 | export default function inspect(value: any): string { 12 | return formatValue(value, []); 13 | } 14 | 15 | function formatValue(value, seenValues) { 16 | switch (typeof value) { 17 | case 'string': 18 | return JSON.stringify(value); 19 | case 'function': 20 | return value.name ? `[function ${value.name}]` : '[function]'; 21 | case 'object': 22 | if (value === null) { 23 | return 'null'; 24 | } 25 | return formatObjectValue(value, seenValues); 26 | default: 27 | return String(value); 28 | } 29 | } 30 | 31 | function formatObjectValue(value, previouslySeenValues) { 32 | if (previouslySeenValues.indexOf(value) !== -1) { 33 | return '[Circular]'; 34 | } 35 | 36 | const seenValues = [...previouslySeenValues, value]; 37 | const customInspectFn = getCustomFn(value); 38 | 39 | if (customInspectFn !== undefined) { 40 | // $FlowFixMe(>=0.90.0) 41 | const customValue = customInspectFn.call(value); 42 | 43 | // check for infinite recursion 44 | if (customValue !== value) { 45 | return typeof customValue === 'string' 46 | ? customValue 47 | : formatValue(customValue, seenValues); 48 | } 49 | } else if (Array.isArray(value)) { 50 | return formatArray(value, seenValues); 51 | } 52 | 53 | return formatObject(value, seenValues); 54 | } 55 | 56 | function formatObject(object, seenValues) { 57 | const keys = Object.keys(object); 58 | if (keys.length === 0) { 59 | return '{}'; 60 | } 61 | 62 | if (seenValues.length > MAX_RECURSIVE_DEPTH) { 63 | return '[' + getObjectTag(object) + ']'; 64 | } 65 | 66 | const properties = keys.map((key) => { 67 | const value = formatValue(object[key], seenValues); 68 | return key + ': ' + value; 69 | }); 70 | 71 | return '{ ' + properties.join(', ') + ' }'; 72 | } 73 | 74 | function formatArray(array, seenValues) { 75 | if (array.length === 0) { 76 | return '[]'; 77 | } 78 | 79 | if (seenValues.length > MAX_RECURSIVE_DEPTH) { 80 | return '[Array]'; 81 | } 82 | 83 | const len = Math.min(MAX_ARRAY_LENGTH, array.length); 84 | const remaining = array.length - len; 85 | const items: any[] = []; 86 | 87 | for (let i = 0; i < len; ++i) { 88 | items.push(formatValue(array[i], seenValues)); 89 | } 90 | 91 | if (remaining === 1) { 92 | items.push('... 1 more item'); 93 | } else if (remaining > 1) { 94 | items.push(`... ${remaining} more items`); 95 | } 96 | 97 | return '[' + items.join(', ') + ']'; 98 | } 99 | 100 | function getCustomFn(object) { 101 | const customInspectFn = object[String(nodejsCustomInspectSymbol)]; 102 | 103 | if (typeof customInspectFn === 'function') { 104 | return customInspectFn; 105 | } 106 | 107 | if (typeof object.inspect === 'function') { 108 | return object.inspect; 109 | } 110 | } 111 | 112 | function getObjectTag(object) { 113 | const tag = Object.prototype.toString 114 | .call(object) 115 | .replace(/^\[object /, '') 116 | .replace(/]$/, ''); 117 | 118 | if (tag === 'Object' && typeof object.constructor === 'function') { 119 | const name = object.constructor.name; 120 | if (typeof name === 'string' && name !== '') { 121 | return name; 122 | } 123 | } 124 | 125 | return tag; 126 | } -------------------------------------------------------------------------------- /src/jstutils/invariant.ts: -------------------------------------------------------------------------------- 1 | export default function invariant(condition: any, message: string) { 2 | if (!condition) { 3 | throw new Error(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/jstutils/isInvalid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if a value is undefined, or NaN. 3 | */ 4 | export default function isInvalid(value: any): boolean { 5 | return value === undefined || value !== value; 6 | } 7 | -------------------------------------------------------------------------------- /src/jstutils/isNullish.ts: -------------------------------------------------------------------------------- 1 | // inspired from https://github.com/graphql/graphql-js/blob/926e4d80c558b107c49e9403e943086fa9b043a8/src/jsutils/isNullish.js 2 | 3 | /** 4 | * Returns true if a value is null, undefined, or NaN. 5 | */ 6 | export default function isNullish(value: any) { 7 | return value === null || value === undefined || value !== value; 8 | }; 9 | -------------------------------------------------------------------------------- /src/jstutils/nodejsCustomInspectSymbol.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2317') 2 | const nodejsCustomInspectSymbol = 3 | typeof Symbol === 'function' && typeof Symbol.for === 'function' 4 | ? Symbol.for('nodejs.util.inspect.custom') 5 | : undefined; 6 | 7 | export default nodejsCustomInspectSymbol; -------------------------------------------------------------------------------- /src/rxutils/combinePropsLatest.ts: -------------------------------------------------------------------------------- 1 | import { Observable, combineLatest } from 'rxjs'; 2 | import { map } from 'rxjs/operators'; 3 | 4 | /** 5 | * Combine multiple Observables passed as an object to create 6 | * an Observable that emits an object with the same keys and, 7 | * as values, the latest values of each of the corresponding 8 | * input Observables. 9 | * 10 | * Like `combineLatest` but for object properties instead of 11 | * iterable of Observables. 12 | * 13 | * @param input 14 | */ 15 | function combinePropsLatest( 16 | input: { [key: string]: Observable } 17 | ): Observable<{[key: string]: any}> { 18 | const keys = Object.keys(input); 19 | return combineLatest( 20 | keys.map(key => input[key]) 21 | ).pipe(map(resultArray => { 22 | const results: { [key: string]: any } = {}; 23 | resultArray.forEach((result, i) => { 24 | results[keys[i]] = result; 25 | }) 26 | return results; 27 | })) 28 | } 29 | 30 | export default combinePropsLatest; 31 | -------------------------------------------------------------------------------- /src/rxutils/mapPromiseToObservale.ts: -------------------------------------------------------------------------------- 1 | import { Observable, from } from "rxjs"; 2 | import { switchMap } from "rxjs/operators"; 3 | 4 | /** 5 | * Build an Observable given a Promise's resolved value. 6 | * 7 | * @param promise 8 | * @param observableFn 9 | */ 10 | export default function mapPromiseToObservale( 11 | promise: Promise, 12 | observableFn: (resolved: TPromiseValue) => Observable 13 | ): Observable { 14 | return from(promise).pipe(switchMap(observableFn)); 15 | }; 16 | -------------------------------------------------------------------------------- /src/rxutils/mapToFirstValue.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs"; 2 | import { switchMap, take } from "rxjs/operators"; 3 | 4 | /** 5 | * Will wait for the first value of the given Observable 6 | * for mapping and emits the value of the Observable 7 | * returned by `mappingFn`. 8 | * 9 | * @param observable 10 | * @param mappingFn 11 | */ 12 | export default function mapToFirstValue( 13 | observable: Observable, 14 | mappingFn: (firstValue: TObservableValue) => Observable 15 | ): Observable { 16 | return observable 17 | .pipe(take(1)) 18 | .pipe(switchMap(mappingFn)) 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "allowJs": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "allowJs": true, 5 | "rootDir": "./src", 6 | "sourceMap": true, 7 | "noImplicitAny": false, 8 | "target": "es5", 9 | "module": "commonjs", 10 | "jsx": "react", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "esModuleInterop": true, 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "moduleResolution": "node", 17 | "lib": ["es6", "es2017", "esnext.asynciterable"], 18 | "typeRoots": ["node_modules/@types"] 19 | }, 20 | "include": ["./**/*.ts"], 21 | "exclude": ["**/*.test.*"] 22 | } 23 | --------------------------------------------------------------------------------