├── .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 | [](https://badge.fury.io/js/reactive-graphql) [](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 [](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 |
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 |
--------------------------------------------------------------------------------