diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e7b7c0b0f5..2a9362e735a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,9 @@
- Eliminate "generated" cache IDs to avoid normalizing objects with no meaningful ID, significantly reducing cache memory usage.
[@benjamn](https://github.com/benjamn) in [#5146](https://github.com/apollographql/apollo-client/pull/5146)
+- Apollo Link core and HTTP related functionality has been merged into `@apollo/client`. Functionality that was previously available through the `apollo-link`, `apollo-link-http-common` and `apollo-link-http` packages is now directly available from `@apollo/client` (e.g. `import { HttpLink } from '@apollo/client'`). The `ApolloClient` constructor has also been updated to accept new `uri`, `headers` and `credentials` options. If `uri` is specified, Apollo Client will take care of creating the necessary `HttpLink` behind the scenes.
+ [@hwillson](https://github.com/hwillson) in [#5412](https://github.com/apollographql/apollo-client/pull/5412)
+
### Breaking Changes
- Removed `graphql-anywhere` since it's no longer used by Apollo Client.
diff --git a/config/rollup.config.js b/config/rollup.config.js
index c654c0a51b7..68b3ad8d549 100644
--- a/config/rollup.config.js
+++ b/config/rollup.config.js
@@ -8,17 +8,19 @@ import packageJson from '../package.json';
const distDir = './dist';
const globals = {
- 'apollo-link': 'apolloLink.core',
'tslib': 'tslib',
'ts-invariant': 'invariant',
'symbol-observable': '$$observable',
'graphql/language/printer': 'print',
optimism: 'optimism',
'graphql/language/visitor': 'visitor',
+ 'graphql/language/printer': 'printer',
+ 'graphql/execution/execute': 'execute',
'fast-json-stable-stringify': 'stringify',
'@wry/equality': 'wryEquality',
graphql: 'graphql',
- react: 'React'
+ react: 'React',
+ 'zen-observable': 'Observable'
};
const hasOwn = Object.prototype.hasOwnProperty;
diff --git a/package-lock.json b/package-lock.json
index 181fff3adb5..86b72616159 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1156,12 +1156,6 @@
"integrity": "sha512-mky/O83TXmGY39P1H9YbUpjV6l6voRYlufqfFCvel8l1phuy8HRjdWc1rrPuN53ITBJlbyMSV6z3niOySO5pgQ==",
"dev": true
},
- "@types/isomorphic-fetch": {
- "version": "0.0.35",
- "resolved": "https://registry.npmjs.org/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.35.tgz",
- "integrity": "sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw==",
- "dev": true
- },
"@types/istanbul-lib-coverage": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
@@ -1688,30 +1682,6 @@
}
}
},
- "apollo-link": {
- "version": "1.2.13",
- "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.13.tgz",
- "integrity": "sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw==",
- "requires": {
- "apollo-utilities": "^1.3.0",
- "ts-invariant": "^0.4.0",
- "tslib": "^1.9.3",
- "zen-observable-ts": "^0.8.20"
- },
- "dependencies": {
- "apollo-utilities": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.2.tgz",
- "integrity": "sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==",
- "requires": {
- "@wry/equality": "^0.1.2",
- "fast-json-stable-stringify": "^2.0.0",
- "ts-invariant": "^0.4.0",
- "tslib": "^1.9.3"
- }
- }
- }
- },
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -2669,15 +2639,6 @@
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
- "encoding": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
- "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
- "dev": true,
- "requires": {
- "iconv-lite": "~0.4.13"
- }
- },
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -3047,28 +3008,28 @@
"dependencies": {
"abbrev": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "resolved": false,
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true,
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "resolved": false,
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.5",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+ "resolved": false,
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"dev": true,
"optional": true,
@@ -3079,14 +3040,14 @@
},
"balanced-match": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"optional": true,
@@ -3097,42 +3058,42 @@
},
"chownr": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
+ "resolved": false,
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
"dev": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true,
"optional": true
},
"debug": {
"version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "resolved": false,
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"optional": true,
@@ -3142,28 +3103,28 @@
},
"deep-extend": {
"version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "resolved": false,
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "resolved": false,
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"dev": true,
"optional": true
},
"detect-libc": {
"version": "1.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "resolved": false,
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"dev": true,
"optional": true
},
"fs-minipass": {
"version": "1.2.5",
- "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
+ "resolved": false,
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"dev": true,
"optional": true,
@@ -3173,14 +3134,14 @@
},
"fs.realpath": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "resolved": false,
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true,
"optional": true
},
"gauge": {
"version": "2.7.4",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "resolved": false,
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dev": true,
"optional": true,
@@ -3197,7 +3158,7 @@
},
"glob": {
"version": "7.1.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "resolved": false,
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"optional": true,
@@ -3212,14 +3173,14 @@
},
"has-unicode": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "resolved": false,
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "resolved": false,
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"optional": true,
@@ -3229,7 +3190,7 @@
},
"ignore-walk": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
+ "resolved": false,
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"dev": true,
"optional": true,
@@ -3239,7 +3200,7 @@
},
"inflight": {
"version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"optional": true,
@@ -3250,21 +3211,21 @@
},
"inherits": {
"version": "2.0.3",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "resolved": false,
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"optional": true,
@@ -3274,14 +3235,14 @@
},
"isarray": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true,
"optional": true
},
"minimatch": {
"version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"optional": true,
@@ -3291,14 +3252,14 @@
},
"minimist": {
"version": "0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
+ "resolved": false,
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true,
"optional": true,
@@ -3309,7 +3270,7 @@
},
"minizlib": {
"version": "1.2.1",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
+ "resolved": false,
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
"dev": true,
"optional": true,
@@ -3319,7 +3280,7 @@
},
"mkdirp": {
"version": "0.5.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true,
@@ -3329,14 +3290,14 @@
},
"ms": {
"version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "resolved": false,
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true,
"optional": true
},
"needle": {
"version": "2.3.0",
- "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz",
+ "resolved": false,
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
"dev": true,
"optional": true,
@@ -3348,7 +3309,7 @@
},
"node-pre-gyp": {
"version": "0.12.0",
- "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz",
+ "resolved": false,
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
"dev": true,
"optional": true,
@@ -3367,7 +3328,7 @@
},
"nopt": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+ "resolved": false,
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"dev": true,
"optional": true,
@@ -3378,14 +3339,14 @@
},
"npm-bundled": {
"version": "1.0.6",
- "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
+ "resolved": false,
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.4.1",
- "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz",
+ "resolved": false,
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"dev": true,
"optional": true,
@@ -3396,7 +3357,7 @@
},
"npmlog": {
"version": "4.1.2",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "resolved": false,
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"optional": true,
@@ -3409,21 +3370,21 @@
},
"number-is-nan": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "resolved": false,
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"optional": true,
@@ -3433,21 +3394,21 @@
},
"os-homedir": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "resolved": false,
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "resolved": false,
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.5",
- "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "resolved": false,
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true,
"optional": true,
@@ -3458,21 +3419,21 @@
},
"path-is-absolute": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"optional": true
},
"process-nextick-args": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "resolved": false,
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true,
"optional": true
},
"rc": {
"version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "resolved": false,
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"optional": true,
@@ -3485,7 +3446,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": false,
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true,
"optional": true
@@ -3494,7 +3455,7 @@
},
"readable-stream": {
"version": "2.3.6",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "resolved": false,
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"optional": true,
@@ -3510,7 +3471,7 @@
},
"rimraf": {
"version": "2.6.3",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "resolved": false,
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"dev": true,
"optional": true,
@@ -3520,49 +3481,49 @@
},
"safe-buffer": {
"version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "resolved": false,
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"optional": true
},
"sax": {
"version": "1.2.4",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "resolved": false,
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true,
"optional": true
},
"semver": {
"version": "5.7.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+ "resolved": false,
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true,
"optional": true
},
"string-width": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"optional": true,
@@ -3574,7 +3535,7 @@
},
"string_decoder": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "resolved": false,
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"optional": true,
@@ -3584,7 +3545,7 @@
},
"strip-ansi": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"optional": true,
@@ -3594,14 +3555,14 @@
},
"strip-json-comments": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "resolved": false,
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true,
"optional": true
},
"tar": {
"version": "4.4.8",
- "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
+ "resolved": false,
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
"dev": true,
"optional": true,
@@ -3617,14 +3578,14 @@
},
"util-deprecate": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true,
"optional": true
},
"wide-align": {
"version": "1.1.3",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "resolved": false,
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"dev": true,
"optional": true,
@@ -3634,14 +3595,14 @@
},
"wrappy": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
+ "resolved": false,
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"dev": true,
"optional": true
@@ -3742,9 +3703,9 @@
"dev": true
},
"graphql": {
- "version": "14.5.4",
- "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.5.4.tgz",
- "integrity": "sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==",
+ "version": "14.5.8",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.5.8.tgz",
+ "integrity": "sha512-MMwmi0zlVLQKLdGiMfWkgQD7dY/TUKt4L+zgJ/aR0Howebod3aNgP5JkgvAULiR2HPVZaP2VEElqtdidHweLkg==",
"dev": true,
"requires": {
"iterall": "^1.2.2"
@@ -4187,28 +4148,6 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
- "isomorphic-fetch": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
- "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
- "dev": true,
- "requires": {
- "node-fetch": "^1.0.1",
- "whatwg-fetch": ">=0.10.0"
- },
- "dependencies": {
- "node-fetch": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
- "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
- "dev": true,
- "requires": {
- "encoding": "^0.1.11",
- "is-stream": "^1.0.1"
- }
- }
- }
- },
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -8666,12 +8605,6 @@
"iconv-lite": "0.4.24"
}
},
- "whatwg-fetch": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
- "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==",
- "dev": true
- },
"whatwg-mimetype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
@@ -8773,15 +8706,6 @@
"version": "0.8.14",
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.14.tgz",
"integrity": "sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g=="
- },
- "zen-observable-ts": {
- "version": "0.8.20",
- "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz",
- "integrity": "sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA==",
- "requires": {
- "tslib": "^1.9.3",
- "zen-observable": "^0.8.0"
- }
}
}
}
diff --git a/package.json b/package.json
index bb360b46db6..9b66550f5d6 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.cjs.min.js",
- "maxSize": "21 kB"
+ "maxSize": "24 kB"
}
],
"peerDependencies": {
@@ -59,7 +59,6 @@
"dependencies": {
"@types/zen-observable": "^0.8.0",
"@wry/equality": "^0.1.9",
- "apollo-link": "^1.2.13",
"fast-json-stable-stringify": "^2.0.0",
"optimism": "^0.11.3",
"symbol-observable": "^1.2.0",
@@ -70,7 +69,6 @@
"devDependencies": {
"@testing-library/react": "^9.1.4",
"@types/fast-json-stable-stringify": "^2.0.0",
- "@types/isomorphic-fetch": "0.0.35",
"@types/jest": "24.0.18",
"@types/lodash": "4.14.139",
"@types/node": "12.7.5",
@@ -78,10 +76,9 @@
"@types/react-dom": "16.9.0",
"bundlesize": "0.18.0",
"codecov": "3.5.0",
- "fetch-mock": "7.3.9",
- "graphql": "14.5.4",
+ "fetch-mock": "^7.3.9",
+ "graphql": "^14.5.8",
"graphql-tag": "2.10.1",
- "isomorphic-fetch": "2.2.1",
"jest": "24.9.0",
"jest-junit": "8.0.0",
"lodash": "4.17.15",
diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts
index 1b1b1f51b23..ee6dc79658a 100644
--- a/src/ApolloClient.ts
+++ b/src/ApolloClient.ts
@@ -1,12 +1,12 @@
+import { ExecutionResult, DocumentNode } from 'graphql';
+import { invariant, InvariantError } from 'ts-invariant';
+
import {
ApolloLink,
FetchResult,
GraphQLRequest,
execute,
-} from 'apollo-link';
-import { ExecutionResult, DocumentNode } from 'graphql';
-import { invariant, InvariantError } from 'ts-invariant';
-
+} from './link/core';
import { ApolloCache, DataProxy } from './cache/core';
import { QueryManager } from './core/QueryManager';
import {
@@ -26,6 +26,7 @@ import {
} from './core/watchQueryOptions';
import { DataStore } from './data/store';
import { version } from './version';
+import { UriFunction, HttpLink } from './link/http';
export interface DefaultOptions {
watchQuery?: Partial;
@@ -36,6 +37,9 @@ export interface DefaultOptions {
let hasSuggestedDevtools = false;
export type ApolloClientOptions = {
+ uri?: string | UriFunction;
+ credentials?: string;
+ headers?: Record;
link?: ApolloLink;
cache: ApolloCache;
ssrForceFetchDelay?: number;
@@ -76,6 +80,8 @@ export default class ApolloClient implements DataProxy {
/**
* Constructs an instance of {@link ApolloClient}.
*
+ * @param uri The GraphQL endpoint that Apollo Client will connect to. If
+ * `link` is configured, this option is ignored.
* @param link The {@link ApolloLink} over which GraphQL documents will be resolved into a response.
*
* @param cache The initial cache to use in the data store.
@@ -109,6 +115,9 @@ export default class ApolloClient implements DataProxy {
*/
constructor(options: ApolloClientOptions) {
const {
+ uri,
+ credentials,
+ headers,
cache,
ssrMode = false,
ssrForceFetchDelay = 0,
@@ -125,21 +134,23 @@ export default class ApolloClient implements DataProxy {
let { link } = options;
- // If a link hasn't been defined, but local state resolvers have been set,
- // setup a default empty link.
- if (!link && resolvers) {
- link = ApolloLink.empty();
+ if (!link) {
+ if (uri) {
+ link = new HttpLink({ uri, credentials, headers });
+ } else if (resolvers) {
+ link = ApolloLink.empty();
+ }
}
if (!link || !cache) {
throw new InvariantError(
- "In order to initialize Apollo Client, you must specify 'link' and 'cache' properties in the options object.\n" +
- "These options are part of the upgrade requirements when migrating from Apollo Client 1.x to Apollo Client 2.x.\n" +
- "For more information, please visit: https://www.apollographql.com/docs/tutorial/client.html#apollo-client-setup"
+ "To initialize Apollo Client, you must specify 'uri' or 'link' and " +
+ "'cache' properties in the options object. \n" +
+ "For more information, please visit: " +
+ "https://www.apollographql.com/docs/react/"
);
}
- // remove apollo-client supported directives
this.link = link;
this.cache = cache;
this.store = new DataStore(cache);
diff --git a/src/__mocks__/mockLinks.ts b/src/__mocks__/mockLinks.ts
index 3002350a9d2..46a05a82537 100644
--- a/src/__mocks__/mockLinks.ts
+++ b/src/__mocks__/mockLinks.ts
@@ -1,12 +1,12 @@
+import { print } from 'graphql/language/printer';
+
+import { Observable } from '../util/Observable';
import {
Operation,
ApolloLink,
FetchResult,
- Observable,
GraphQLRequest,
-} from 'apollo-link';
-
-import { print } from 'graphql/language/printer';
+} from '../link/core';
interface MockApolloLink extends ApolloLink {
operation?: Operation;
diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts
index 44600c164b9..34db3f60b5d 100644
--- a/src/__tests__/ApolloClient.ts
+++ b/src/__tests__/ApolloClient.ts
@@ -1,6 +1,8 @@
import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
+import { Observable } from '../util/Observable';
+import { ApolloLink } from '../link/core';
+import { HttpLink } from '../link/http';
import { InMemoryCache, makeReference } from '../cache/inmemory';
import { stripSymbols } from '../utilities';
import { withWarning } from '../util/wrap';
@@ -10,7 +12,18 @@ import { FetchPolicy, QueryOptions } from '../core/watchQueryOptions';
describe('ApolloClient', () => {
describe('constructor', () => {
- it('will throw an error if link is not passed in', () => {
+ let oldFetch: any;
+
+ beforeEach(() => {
+ oldFetch = window.fetch;
+ window.fetch = () => null;
+ })
+
+ afterEach(() => {
+ window.fetch = oldFetch;
+ });
+
+ it('will throw an error if `uri` or `link` is not passed in', () => {
expect(() => {
new ApolloClient({ cache: new InMemoryCache() } as any);
}).toThrowErrorMatchingSnapshot();
@@ -21,6 +34,28 @@ describe('ApolloClient', () => {
new ApolloClient({ link: ApolloLink.empty() } as any);
}).toThrowErrorMatchingSnapshot();
});
+
+ it('should create an `HttpLink` instance if `uri` is provided', () => {
+ const uri = 'http://localhost:4000';
+ const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ uri,
+ });
+
+ expect(client.link).toBeDefined();
+ expect((client.link as HttpLink).options.uri).toEqual(uri);
+ });
+
+ it('should accept `link` over `uri` if both are provided', () => {
+ const uri1 = 'http://localhost:3000';
+ const uri2 = 'http://localhost:4000';
+ const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ uri: uri1,
+ link: new HttpLink({ uri: uri2 })
+ });
+ expect((client.link as HttpLink).options.uri).toEqual(uri2);
+ });
});
describe('readQuery', () => {
diff --git a/src/__tests__/__snapshots__/ApolloClient.ts.snap b/src/__tests__/__snapshots__/ApolloClient.ts.snap
index 08e5848b250..ab1a665922f 100644
--- a/src/__tests__/__snapshots__/ApolloClient.ts.snap
+++ b/src/__tests__/__snapshots__/ApolloClient.ts.snap
@@ -1,15 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ApolloClient constructor will throw an error if cache is not passed in 1`] = `
-"In order to initialize Apollo Client, you must specify 'link' and 'cache' properties in the options object.
-These options are part of the upgrade requirements when migrating from Apollo Client 1.x to Apollo Client 2.x.
-For more information, please visit: https://www.apollographql.com/docs/tutorial/client.html#apollo-client-setup"
+exports[`ApolloClient constructor will throw an error if \`uri\` or \`link\` is not passed in 1`] = `
+"To initialize Apollo Client, you must specify 'uri' or 'link' and 'cache' properties in the options object.
+For more information, please visit: https://www.apollographql.com/docs/react/"
`;
-exports[`ApolloClient constructor will throw an error if link is not passed in 1`] = `
-"In order to initialize Apollo Client, you must specify 'link' and 'cache' properties in the options object.
-These options are part of the upgrade requirements when migrating from Apollo Client 1.x to Apollo Client 2.x.
-For more information, please visit: https://www.apollographql.com/docs/tutorial/client.html#apollo-client-setup"
+exports[`ApolloClient constructor will throw an error if cache is not passed in 1`] = `
+"To initialize Apollo Client, you must specify 'uri' or 'link' and 'cache' properties in the options object.
+For more information, please visit: https://www.apollographql.com/docs/react/"
`;
exports[`ApolloClient write then read will not use a default id getter if either _id or id is present when __typename is not also present 1`] = `
diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts
index 46088d75a25..7f3f2067bec 100644
--- a/src/__tests__/client.ts
+++ b/src/__tests__/client.ts
@@ -1,8 +1,9 @@
import { cloneDeep, assign } from 'lodash';
import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql';
import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
+import { Observable } from '../util/Observable';
+import { ApolloLink } from '../link/core';
import { InMemoryCache, PossibleTypesMap } from '../cache/inmemory';
import { stripSymbols } from '../utilities';
import { WatchQueryOptions, FetchPolicy } from '../core/watchQueryOptions';
diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts
index 58550b0cfff..3ac26031a3a 100644
--- a/src/__tests__/local-state/export.ts
+++ b/src/__tests__/local-state/export.ts
@@ -1,7 +1,8 @@
import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
import { print } from 'graphql/language/printer';
+import { Observable } from '../../util/Observable';
+import { ApolloLink } from '../../link/core';
import ApolloClient from '../..';
import { InMemoryCache } from '../../cache/inmemory';
diff --git a/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts
index 98471f8c21f..0479dc46f78 100644
--- a/src/__tests__/local-state/general.ts
+++ b/src/__tests__/local-state/general.ts
@@ -1,8 +1,9 @@
import gql from 'graphql-tag';
import { DocumentNode, GraphQLError } from 'graphql';
import { introspectionQuery } from 'graphql/utilities';
-import { ApolloLink, Observable, Operation } from 'apollo-link';
+import { Observable } from '../../util/Observable';
+import { ApolloLink, Operation } from '../../link/core';
import ApolloClient from '../..';
import { ApolloCache } from '../../cache/core';
import { InMemoryCache } from '../../cache/inmemory';
diff --git a/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts
index 4c339cac722..85d0a84ade3 100644
--- a/src/__tests__/local-state/resolvers.ts
+++ b/src/__tests__/local-state/resolvers.ts
@@ -1,8 +1,9 @@
import gql from 'graphql-tag';
import { DocumentNode, ExecutionResult } from 'graphql';
import { assign } from 'lodash';
-import { ApolloLink, Observable } from 'apollo-link';
+import { Observable } from '../../util/Observable';
+import { ApolloLink } from '../../link/core';
import ApolloClient from '../..';
import mockQueryManager from '../../__mocks__/mockQueryManager';
import { Observer } from '../../util/Observable';
diff --git a/src/__tests__/local-state/subscriptions.ts b/src/__tests__/local-state/subscriptions.ts
index 72339a38d4f..b13c9982928 100644
--- a/src/__tests__/local-state/subscriptions.ts
+++ b/src/__tests__/local-state/subscriptions.ts
@@ -1,6 +1,7 @@
import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
+import { Observable } from '../../util/Observable';
+import { ApolloLink } from '../../link/core';
import ApolloClient from '../..';
import { InMemoryCache } from '../../cache/inmemory';
diff --git a/src/__tests__/mutationResults.ts b/src/__tests__/mutationResults.ts
index 89f6e28c819..f41ec01f68a 100644
--- a/src/__tests__/mutationResults.ts
+++ b/src/__tests__/mutationResults.ts
@@ -1,7 +1,8 @@
-import { ApolloLink, Observable } from 'apollo-link';
import { cloneDeep } from 'lodash';
import gql from 'graphql-tag';
+import { Observable } from '../util/Observable';
+import { ApolloLink } from '../link/core';
import { mockSingleLink } from '../__mocks__/mockLinks';
import ApolloClient from '..';
import { InMemoryCache } from '../cache/inmemory';
diff --git a/src/__tests__/subscribeToMore.ts b/src/__tests__/subscribeToMore.ts
index 0ddaa19433f..74a61fd5f1e 100644
--- a/src/__tests__/subscribeToMore.ts
+++ b/src/__tests__/subscribeToMore.ts
@@ -1,7 +1,7 @@
import gql from 'graphql-tag';
-import { ApolloLink, Operation } from 'apollo-link';
-
import { DocumentNode, OperationDefinitionNode } from 'graphql';
+
+import { ApolloLink, Operation } from '../link/core';
import { mockSingleLink, mockObservableLink } from '../__mocks__/mockLinks';
import ApolloClient from '../';
import { InMemoryCache } from '../cache/inmemory';
diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts
index 2ea65037193..ecd1c0437e7 100644
--- a/src/core/QueryManager.ts
+++ b/src/core/QueryManager.ts
@@ -1,7 +1,7 @@
-import { execute, ApolloLink, FetchResult } from 'apollo-link';
import { ExecutionResult, DocumentNode } from 'graphql';
import { invariant, InvariantError } from 'ts-invariant';
+import { execute, ApolloLink, FetchResult } from '../link/core';
import { Cache } from '../cache/core';
import {
getDefaultValues,
diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts
index 80ca834091e..f466b8ac6c2 100644
--- a/src/core/__tests__/ObservableQuery.ts
+++ b/src/core/__tests__/ObservableQuery.ts
@@ -1,8 +1,9 @@
import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
-import { InMemoryCache } from '../../cache/inmemory';
import { GraphQLError } from 'graphql';
+import { Observable } from '../../util/Observable';
+import { ApolloLink } from '../../link/core';
+import { InMemoryCache } from '../../cache/inmemory';
import mockQueryManager from '../../__mocks__/mockQueryManager';
import mockWatchQuery from '../../__mocks__/mockWatchQuery';
import { mockSingleLink } from '../../__mocks__/mockLinks';
diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts
index 4211e20fc76..44f09941289 100644
--- a/src/core/__tests__/QueryManager/index.ts
+++ b/src/core/__tests__/QueryManager/index.ts
@@ -4,8 +4,9 @@ import { map } from 'rxjs/operators';
import { assign } from 'lodash';
import gql from 'graphql-tag';
import { DocumentNode, ExecutionResult, GraphQLError } from 'graphql';
-import { ApolloLink, Operation, Observable } from 'apollo-link';
+import { Observable } from '../../../util/Observable';
+import { ApolloLink, Operation } from '../../../link/core';
import {
InMemoryCache,
ApolloReducerConfig,
diff --git a/src/core/__tests__/QueryManager/links.ts b/src/core/__tests__/QueryManager/links.ts
index 2b283f787ea..465598641ab 100644
--- a/src/core/__tests__/QueryManager/links.ts
+++ b/src/core/__tests__/QueryManager/links.ts
@@ -1,6 +1,8 @@
// externals
import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
+
+import { Observable } from '../../../util/Observable';
+import { ApolloLink } from '../../../link/core';
import { InMemoryCache } from '../../../cache/inmemory';
import { stripSymbols } from '../../../utilities';
diff --git a/src/core/__tests__/fetchPolicies.ts b/src/core/__tests__/fetchPolicies.ts
index 2373c578fab..dcd77a2ac51 100644
--- a/src/core/__tests__/fetchPolicies.ts
+++ b/src/core/__tests__/fetchPolicies.ts
@@ -1,6 +1,6 @@
import gql from 'graphql-tag';
-import { ApolloLink } from 'apollo-link';
+import { ApolloLink } from '../../link/core';
import { InMemoryCache } from '../../cache/inmemory';
import { stripSymbols } from '../../utilities';
import ApolloClient from '../..';
diff --git a/src/core/types.ts b/src/core/types.ts
index caafd37cf85..6c18fe9f51a 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,6 +1,6 @@
-import { FetchResult } from 'apollo-link';
import { DocumentNode, GraphQLError } from 'graphql';
+import { FetchResult } from '../link/core';
import { QueryStoreValue } from '../data/queries';
import { NetworkStatus } from './networkStatus';
import { Resolver } from './LocalState';
diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts
index 07cdf185156..5cc5762297a 100644
--- a/src/core/watchQueryOptions.ts
+++ b/src/core/watchQueryOptions.ts
@@ -1,6 +1,6 @@
import { DocumentNode, ExecutionResult } from 'graphql';
-import { FetchResult } from 'apollo-link';
+import { FetchResult } from '../link/core';
import { DataProxy } from '../cache/core';
import { MutationQueryReducersMap } from './types';
import { PureQueryOptions, OperationVariables } from './types';
diff --git a/src/index.ts b/src/index.ts
index 44ae88c0c40..6934d778aa8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,7 @@ export {
UpdateQueryOptions,
ApolloCurrentQueryResult,
} from './core/ObservableQuery';
+
export {
QueryBaseOptions,
QueryOptions,
@@ -17,8 +18,11 @@ export {
SubscribeToMoreOptions,
MutationUpdaterFn,
} from './core/watchQueryOptions';
+
export { NetworkStatus } from './core/networkStatus';
+
export * from './core/types';
+
export {
Resolver,
FragmentMatcher as LocalStateFragmentMatcher,
@@ -40,3 +44,11 @@ export * from './cache/core';
export * from './cache/inmemory';
export * from './react';
+
+export * from './link/core';
+export * from './link/http';
+export * from './link/utils';
+
+export { Observable } from './util/Observable';
+
+export * from './utilities';
diff --git a/src/link/core/ApolloLink.ts b/src/link/core/ApolloLink.ts
new file mode 100644
index 00000000000..dfa8debc758
--- /dev/null
+++ b/src/link/core/ApolloLink.ts
@@ -0,0 +1,141 @@
+import { InvariantError, invariant } from 'ts-invariant';
+
+import { Observable } from '../../util/Observable';
+import {
+ NextLink,
+ Operation,
+ RequestHandler,
+ FetchResult,
+ GraphQLRequest
+} from './types';
+import { validateOperation } from '../utils/validateOperation';
+import { createOperation } from '../utils/createOperation';
+import { transformOperation } from '../utils/transformOperation';
+
+function passthrough(op: Operation, forward: NextLink) {
+ return forward ? forward(op) : Observable.of();
+}
+
+function toLink(handler: RequestHandler | ApolloLink) {
+ return typeof handler === 'function' ? new ApolloLink(handler) : handler;
+}
+
+function isTerminating(link: ApolloLink): boolean {
+ return link.request.length <= 1;
+}
+
+class LinkError extends Error {
+ public link: ApolloLink;
+ constructor(message?: string, link?: ApolloLink) {
+ super(message);
+ this.link = link;
+ }
+}
+
+export class ApolloLink {
+ public static empty(): ApolloLink {
+ return new ApolloLink(() => Observable.of());
+ }
+
+ public static from(links: ApolloLink[]): ApolloLink {
+ if (links.length === 0) return ApolloLink.empty();
+ return links.map(toLink).reduce((x, y) => x.concat(y)) as ApolloLink;
+ }
+
+ public static split(
+ test: (op: Operation) => boolean,
+ left: ApolloLink | RequestHandler,
+ right?: ApolloLink | RequestHandler,
+ ): ApolloLink {
+ const leftLink = toLink(left);
+ const rightLink = toLink(right || new ApolloLink(passthrough));
+
+ if (isTerminating(leftLink) && isTerminating(rightLink)) {
+ return new ApolloLink(operation => {
+ return test(operation)
+ ? leftLink.request(operation) || Observable.of()
+ : rightLink.request(operation) || Observable.of();
+ });
+ } else {
+ return new ApolloLink((operation, forward) => {
+ return test(operation)
+ ? leftLink.request(operation, forward) || Observable.of()
+ : rightLink.request(operation, forward) || Observable.of();
+ });
+ }
+ }
+
+ public static execute(
+ link: ApolloLink,
+ operation: GraphQLRequest,
+ ): Observable {
+ return (
+ link.request(
+ createOperation(
+ operation.context,
+ transformOperation(validateOperation(operation)),
+ ),
+ ) || Observable.of()
+ );
+ }
+
+ public static concat(
+ first: ApolloLink | RequestHandler,
+ second: ApolloLink | RequestHandler,
+ ) {
+ const firstLink = toLink(first);
+ if (isTerminating(firstLink)) {
+ invariant.warn(
+ new LinkError(
+ `You are calling concat on a terminating link, which will have no effect`,
+ firstLink,
+ ),
+ );
+ return firstLink;
+ }
+ const nextLink = toLink(second);
+
+ if (isTerminating(nextLink)) {
+ return new ApolloLink(
+ operation =>
+ firstLink.request(
+ operation,
+ op => nextLink.request(op) || Observable.of(),
+ ) || Observable.of(),
+ );
+ } else {
+ return new ApolloLink((operation, forward) => {
+ return (
+ firstLink.request(operation, op => {
+ return nextLink.request(op, forward) || Observable.of();
+ }) || Observable.of()
+ );
+ });
+ }
+ }
+
+ constructor(request?: RequestHandler) {
+ if (request) this.request = request;
+ }
+
+ public split(
+ test: (op: Operation) => boolean,
+ left: ApolloLink | RequestHandler,
+ right?: ApolloLink | RequestHandler,
+ ): ApolloLink {
+ return this.concat(
+ ApolloLink.split(test, left, right || new ApolloLink(passthrough))
+ );
+ }
+
+ public concat(next: ApolloLink | RequestHandler): ApolloLink {
+ return ApolloLink.concat(this, next);
+ }
+
+ public request(
+ operation: Operation,
+ forward?: NextLink,
+ ): Observable | null {
+ throw new InvariantError('request is not implemented');
+ }
+}
diff --git a/src/link/core/__tests__/ApolloLink.ts b/src/link/core/__tests__/ApolloLink.ts
new file mode 100644
index 00000000000..eed1a9bb04c
--- /dev/null
+++ b/src/link/core/__tests__/ApolloLink.ts
@@ -0,0 +1,1052 @@
+import gql from 'graphql-tag';
+import { print } from 'graphql/language/printer';
+
+import { Observable } from '../../../util/Observable';
+import { FetchResult, Operation, NextLink, GraphQLRequest } from '../types';
+import { ApolloLink } from '../ApolloLink';
+
+export class SetContextLink extends ApolloLink {
+ constructor(
+ private setContext: (
+ context: Record,
+ ) => Record = c => c,
+ ) {
+ super();
+ }
+
+ public request(
+ operation: Operation,
+ forward: NextLink,
+ ): Observable {
+ operation.setContext(this.setContext(operation.getContext()));
+ return forward(operation);
+ }
+}
+
+export const sampleQuery = gql`
+ query SampleQuery {
+ stub {
+ id
+ }
+ }
+`;
+
+function checkCalls(calls: any[] = [], results: Array) {
+ expect(calls.length).toBe(results.length);
+ calls.map((call, i) => expect(call.data).toEqual(results[i]));
+}
+
+interface TestResultType {
+ link: ApolloLink;
+ results?: any[];
+ query?: string;
+ done?: () => void;
+ context?: any;
+ variables?: any;
+}
+
+export function testLinkResults(params: TestResultType) {
+ const { link, context, variables } = params;
+ const results = params.results || [];
+ const query = params.query || sampleQuery;
+ const done = params.done || (() => void 0);
+
+ const spy = jest.fn();
+ ApolloLink.execute(link, { query, context, variables }).subscribe({
+ next: spy,
+ error: error => {
+ expect(error).toEqual(results.pop());
+ checkCalls(spy.mock.calls[0], results);
+ if (done) {
+ done();
+ }
+ },
+ complete: () => {
+ checkCalls(spy.mock.calls[0], results);
+ if (done) {
+ done();
+ }
+ },
+ });
+}
+
+export const setContext = () => ({ add: 1 });
+
+describe('ApolloClient', () => {
+ describe('context', () => {
+ it('should merge context when using a function', done => {
+ const returnOne = new SetContextLink(setContext);
+ const mock = new ApolloLink((op, forward) => {
+ op.setContext(({ add }) => ({ add: add + 2 }));
+ op.setContext(() => ({ substract: 1 }));
+
+ return forward(op);
+ });
+ const link = returnOne.concat(mock).concat(op => {
+ expect(op.getContext()).toEqual({
+ add: 3,
+ substract: 1,
+ });
+ return Observable.of({ data: op.getContext().add });
+ });
+
+ testLinkResults({
+ link,
+ results: [3],
+ done,
+ });
+ });
+
+ it('should merge context when not using a function', done => {
+ const returnOne = new SetContextLink(setContext);
+ const mock = new ApolloLink((op, forward) => {
+ op.setContext({ add: 3 });
+ op.setContext({ substract: 1 });
+
+ return forward(op);
+ });
+ const link = returnOne.concat(mock).concat(op => {
+ expect(op.getContext()).toEqual({
+ add: 3,
+ substract: 1,
+ });
+ return Observable.of({ data: op.getContext().add });
+ });
+
+ testLinkResults({
+ link,
+ results: [3],
+ done,
+ });
+ });
+ });
+
+ describe('concat', () => {
+ it('should concat a function', done => {
+ const returnOne = new SetContextLink(setContext);
+ const link = returnOne.concat((operation, forward) => {
+ return Observable.of({ data: { count: operation.getContext().add } });
+ });
+
+ testLinkResults({
+ link,
+ results: [{ count: 1 }],
+ done,
+ });
+ });
+
+ it('should concat a Link', done => {
+ const returnOne = new SetContextLink(setContext);
+ const mock = new ApolloLink(op =>
+ Observable.of({ data: op.getContext().add }),
+ );
+ const link = returnOne.concat(mock);
+
+ testLinkResults({
+ link,
+ results: [1],
+ done,
+ });
+ });
+
+ it("should pass error to observable's error", done => {
+ const error = new Error('thrown');
+ const returnOne = new SetContextLink(setContext);
+ const mock = new ApolloLink(
+ op =>
+ new Observable(observer => {
+ observer.next({ data: op.getContext().add });
+ observer.error(error);
+ }),
+ );
+ const link = returnOne.concat(mock);
+
+ testLinkResults({
+ link,
+ results: [1, error],
+ done,
+ });
+ });
+
+ it('should concat a Link and function', done => {
+ const returnOne = new SetContextLink(setContext);
+ const mock = new ApolloLink((op, forward) => {
+ op.setContext(({ add }) => ({ add: add + 2 }));
+ return forward(op);
+ });
+ const link = returnOne.concat(mock).concat(op => {
+ return Observable.of({ data: op.getContext().add });
+ });
+
+ testLinkResults({
+ link,
+ results: [3],
+ done,
+ });
+ });
+
+ it('should concat a function and Link', done => {
+ const returnOne = new SetContextLink(setContext);
+ const mock = new ApolloLink((op, forward) =>
+ Observable.of({ data: op.getContext().add }),
+ );
+
+ const link = returnOne
+ .concat((operation, forward) => {
+ operation.setContext({
+ add: operation.getContext().add + 2,
+ });
+ return forward(operation);
+ })
+ .concat(mock);
+ testLinkResults({
+ link,
+ results: [3],
+ done,
+ });
+ });
+
+ it('should concat two functions', done => {
+ const returnOne = new SetContextLink(setContext);
+ const link = returnOne
+ .concat((operation, forward) => {
+ operation.setContext({
+ add: operation.getContext().add + 2,
+ });
+ return forward(operation);
+ })
+ .concat((op, forward) => Observable.of({ data: op.getContext().add }));
+ testLinkResults({
+ link,
+ results: [3],
+ done,
+ });
+ });
+
+ it('should concat two Links', done => {
+ const returnOne = new SetContextLink(setContext);
+ const mock1 = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ add: operation.getContext().add + 2,
+ });
+ return forward(operation);
+ });
+ const mock2 = new ApolloLink((op, forward) =>
+ Observable.of({ data: op.getContext().add }),
+ );
+
+ const link = returnOne.concat(mock1).concat(mock2);
+ testLinkResults({
+ link,
+ results: [3],
+ done,
+ });
+ });
+
+ it("should return an link that can be concat'd multiple times", done => {
+ const returnOne = new SetContextLink(setContext);
+ const mock1 = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ add: operation.getContext().add + 2,
+ });
+ return forward(operation);
+ });
+ const mock2 = new ApolloLink((op, forward) =>
+ Observable.of({ data: op.getContext().add + 2 }),
+ );
+ const mock3 = new ApolloLink((op, forward) =>
+ Observable.of({ data: op.getContext().add + 3 }),
+ );
+ const link = returnOne.concat(mock1);
+
+ testLinkResults({
+ link: link.concat(mock2),
+ results: [5],
+ });
+ testLinkResults({
+ link: link.concat(mock3),
+ results: [6],
+ done,
+ });
+ });
+ });
+
+ describe('empty', () => {
+ it('should returns an immediately completed Observable', done => {
+ testLinkResults({
+ link: ApolloLink.empty(),
+ done,
+ });
+ });
+ });
+
+ describe('execute', () => {
+ it('transforms an opearation with context into something serlizable', done => {
+ const query = gql`
+ {
+ id
+ }
+ `;
+ const link = new ApolloLink(operation => {
+ const str = JSON.stringify({
+ ...operation,
+ query: print(operation.query),
+ });
+
+ expect(str).toBe(
+ JSON.stringify({
+ variables: { id: 1 },
+ extensions: { cache: true },
+ operationName: null,
+ query: print(operation.query),
+ }),
+ );
+ return Observable.of();
+ });
+ const noop = () => {};
+ ApolloLink.execute(link, {
+ query,
+ variables: { id: 1 },
+ extensions: { cache: true },
+ }).subscribe(noop, noop, done);
+ });
+
+ describe('execute', () => {
+ let _warn: (message?: any, ...originalParams: any[]) => void;
+
+ beforeEach(() => {
+ _warn = console.warn;
+ console.warn = jest.fn(warning => {
+ expect(warning).toBe(`query should either be a string or GraphQL AST`);
+ });
+ });
+
+ afterEach(() => {
+ console.warn = _warn;
+ });
+
+ it('should return an empty observable when a link returns null', done => {
+ const link = new ApolloLink();
+ link.request = () => null;
+ testLinkResults({
+ link,
+ results: [],
+ done,
+ });
+ });
+
+ it('should return an empty observable when a link is empty', done => {
+ testLinkResults({
+ link: ApolloLink.empty(),
+ results: [],
+ done,
+ });
+ });
+
+ it("should return an empty observable when a concat'd link returns null", done => {
+ const link = new ApolloLink((operation, forward) => {
+ return forward(operation);
+ }).concat(() => null);
+ testLinkResults({
+ link,
+ results: [],
+ done,
+ });
+ });
+
+ it('should return an empty observable when a split link returns null', done => {
+ let context = { test: true };
+ const link = new SetContextLink(() => context).split(
+ op => op.getContext().test,
+ () => Observable.of(),
+ () => null,
+ );
+ testLinkResults({
+ link,
+ results: [],
+ });
+ context.test = false;
+ testLinkResults({
+ link,
+ results: [],
+ done,
+ });
+ });
+
+ it('should set a default context, variable, query and operationName on a copy of operation', done => {
+ const operation = {
+ query: gql`
+ {
+ id
+ }
+ `,
+ };
+ const link = new ApolloLink(op => {
+ expect(operation['operationName']).toBeUndefined();
+ expect(operation['variables']).toBeUndefined();
+ expect(operation['context']).toBeUndefined();
+ expect(operation['extensions']).toBeUndefined();
+ expect(op['operationName']).toBeDefined();
+ expect(op['variables']).toBeDefined();
+ expect(op['context']).toBeUndefined();
+ expect(op['extensions']).toBeDefined();
+ return Observable.of();
+ });
+
+ ApolloLink.execute(link, operation).subscribe({
+ complete: done,
+ });
+ });
+ })
+ });
+
+ describe('from', () => {
+ const uniqueOperation: GraphQLRequest = {
+ query: sampleQuery,
+ context: { name: 'uniqueName' },
+ operationName: 'SampleQuery',
+ extensions: {},
+ };
+
+ it('should create an observable that completes when passed an empty array', done => {
+ const observable = ApolloLink.execute(ApolloLink.from([]), {
+ query: sampleQuery,
+ });
+ observable.subscribe(() => expect(false), () => expect(false), done);
+ });
+
+ it('can create chain of one', () => {
+ expect(() => ApolloLink.from([new ApolloLink()])).not.toThrow();
+ });
+
+ it('can create chain of two', () => {
+ expect(() =>
+ ApolloLink.from([
+ new ApolloLink((operation, forward) => forward(operation)),
+ new ApolloLink(),
+ ]),
+ ).not.toThrow();
+ });
+
+ it('should receive result of one link', done => {
+ const data: FetchResult = {
+ data: {
+ hello: 'world',
+ },
+ };
+ const chain = ApolloLink.from([new ApolloLink(() => Observable.of(data))]);
+ // Smoke tests execute as a static method
+ const observable = ApolloLink.execute(chain, uniqueOperation);
+ observable.subscribe({
+ next: actualData => {
+ expect(data).toEqual(actualData);
+ },
+ error: () => {
+ throw new Error();
+ },
+ complete: () => done(),
+ });
+ });
+
+ it('should accept AST query and pass AST to link', () => {
+ const astOperation = {
+ ...uniqueOperation,
+ query: sampleQuery,
+ };
+
+ const stub = jest.fn();
+
+ const chain = ApolloLink.from([new ApolloLink(stub)]);
+ ApolloLink.execute(chain, astOperation);
+
+ expect(stub).toBeCalledWith({
+ query: sampleQuery,
+ operationName: 'SampleQuery',
+ variables: {},
+ extensions: {},
+ });
+ });
+
+ it('should pass operation from one link to next with modifications', done => {
+ const chain = ApolloLink.from([
+ new ApolloLink((op, forward) =>
+ forward({
+ ...op,
+ query: sampleQuery,
+ }),
+ ),
+ new ApolloLink(op => {
+ expect({
+ extensions: {},
+ operationName: 'SampleQuery',
+ query: sampleQuery,
+ variables: {},
+ }).toEqual(op);
+ return done();
+ }),
+ ]);
+ ApolloLink.execute(chain, uniqueOperation);
+ });
+
+ it('should pass result of one link to another with forward', done => {
+ const data: FetchResult = {
+ data: {
+ hello: 'world',
+ },
+ };
+
+ const chain = ApolloLink.from([
+ new ApolloLink((op, forward) => {
+ const observable = forward(op);
+
+ observable.subscribe({
+ next: actualData => {
+ expect(data).toEqual(actualData);
+ },
+ error: () => {
+ throw new Error();
+ },
+ complete: done,
+ });
+
+ return observable;
+ }),
+ new ApolloLink(() => Observable.of(data)),
+ ]);
+ ApolloLink.execute(chain, uniqueOperation);
+ });
+
+ it('should receive final result of two link chain', done => {
+ const data: FetchResult = {
+ data: {
+ hello: 'world',
+ },
+ };
+
+ const chain = ApolloLink.from([
+ new ApolloLink((op, forward) => {
+ const observable = forward(op);
+
+ return new Observable(observer => {
+ observable.subscribe({
+ next: actualData => {
+ expect(data).toEqual(actualData);
+ observer.next({
+ data: {
+ ...actualData.data,
+ modification: 'unique',
+ },
+ });
+ },
+ error: error => observer.error(error),
+ complete: () => observer.complete(),
+ });
+ });
+ }),
+ new ApolloLink(() => Observable.of(data)),
+ ]);
+
+ const result = ApolloLink.execute(chain, uniqueOperation);
+
+ result.subscribe({
+ next: modifiedData => {
+ expect({
+ data: {
+ ...data.data,
+ modification: 'unique',
+ },
+ }).toEqual(modifiedData);
+ },
+ error: () => {
+ throw new Error();
+ },
+ complete: done,
+ });
+ });
+
+ it('should chain together a function with links', done => {
+ const add1 = new ApolloLink((operation: Operation, forward: NextLink) => {
+ operation.setContext(({ num }) => ({ num: num + 1 }));
+ return forward(operation);
+ });
+ const add1Link = new ApolloLink((operation, forward) => {
+ operation.setContext(({ num }) => ({ num: num + 1 }));
+ return forward(operation);
+ });
+
+ const link = ApolloLink.from([
+ add1,
+ add1,
+ add1Link,
+ add1,
+ add1Link,
+ new ApolloLink(operation =>
+ Observable.of({ data: operation.getContext() }),
+ ),
+ ]);
+ testLinkResults({
+ link,
+ results: [{ num: 5 }],
+ context: { num: 0 },
+ done,
+ });
+ });
+ });
+
+ describe('split', () => {
+ it('should split two functions', done => {
+ const context = { add: 1 };
+ const returnOne = new SetContextLink(() => context);
+ const link1 = returnOne.concat((operation, forward) =>
+ Observable.of({ data: operation.getContext().add + 1 }),
+ );
+ const link2 = returnOne.concat((operation, forward) =>
+ Observable.of({ data: operation.getContext().add + 2 }),
+ );
+ const link = returnOne.split(
+ operation => operation.getContext().add === 1,
+ link1,
+ link2,
+ );
+
+ testLinkResults({
+ link,
+ results: [2],
+ });
+
+ context.add = 2;
+
+ testLinkResults({
+ link,
+ results: [4],
+ done,
+ });
+ });
+
+ it('should split two Links', done => {
+ const context = { add: 1 };
+ const returnOne = new SetContextLink(() => context);
+ const link1 = returnOne.concat(
+ new ApolloLink((operation, forward) =>
+ Observable.of({ data: operation.getContext().add + 1 }),
+ ),
+ );
+ const link2 = returnOne.concat(
+ new ApolloLink((operation, forward) =>
+ Observable.of({ data: operation.getContext().add + 2 }),
+ ),
+ );
+ const link = returnOne.split(
+ operation => operation.getContext().add === 1,
+ link1,
+ link2,
+ );
+
+ testLinkResults({
+ link,
+ results: [2],
+ });
+
+ context.add = 2;
+
+ testLinkResults({
+ link,
+ results: [4],
+ done,
+ });
+ });
+
+ it('should split a link and a function', done => {
+ const context = { add: 1 };
+ const returnOne = new SetContextLink(() => context);
+ const link1 = returnOne.concat((operation, forward) =>
+ Observable.of({ data: operation.getContext().add + 1 }),
+ );
+ const link2 = returnOne.concat(
+ new ApolloLink((operation, forward) =>
+ Observable.of({ data: operation.getContext().add + 2 }),
+ ),
+ );
+ const link = returnOne.split(
+ operation => operation.getContext().add === 1,
+ link1,
+ link2,
+ );
+
+ testLinkResults({
+ link,
+ results: [2],
+ });
+
+ context.add = 2;
+
+ testLinkResults({
+ link,
+ results: [4],
+ done,
+ });
+ });
+
+ it('should allow concat after split to be join', done => {
+ const context = { test: true, add: 1 };
+ const start = new SetContextLink(() => ({ ...context }));
+ const link = start
+ .split(
+ operation => operation.getContext().test,
+ (operation, forward) => {
+ operation.setContext(({ add }) => ({ add: add + 1 }));
+ return forward(operation);
+ },
+ (operation, forward) => {
+ operation.setContext(({ add }) => ({ add: add + 2 }));
+ return forward(operation);
+ },
+ )
+ .concat(operation =>
+ Observable.of({ data: operation.getContext().add }),
+ );
+
+ testLinkResults({
+ link,
+ context,
+ results: [2],
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ context,
+ results: [3],
+ done,
+ });
+ });
+
+ it('should allow default right to be empty or passthrough when forward available', done => {
+ let context = { test: true };
+ const start = new SetContextLink(() => context);
+ const link = start.split(
+ operation => operation.getContext().test,
+ operation =>
+ Observable.of({
+ data: {
+ count: 1,
+ },
+ }),
+ );
+ const concat = link.concat(operation =>
+ Observable.of({
+ data: {
+ count: 2,
+ },
+ }),
+ );
+
+ testLinkResults({
+ link,
+ results: [{ count: 1 }],
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ results: [],
+ });
+
+ testLinkResults({
+ link: concat,
+ results: [{ count: 2 }],
+ done,
+ });
+ });
+
+ it('should create filter when single link passed in', done => {
+ const link = ApolloLink.split(
+ operation => operation.getContext().test,
+ (operation, forward) => Observable.of({ data: { count: 1 } }),
+ );
+
+ let context = { test: true };
+
+ testLinkResults({
+ link,
+ results: [{ count: 1 }],
+ context,
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ results: [],
+ context,
+ done,
+ });
+ });
+
+ it('should split two functions', done => {
+ const link = ApolloLink.split(
+ operation => operation.getContext().test,
+ (operation, forward) => Observable.of({ data: { count: 1 } }),
+ (operation, forward) => Observable.of({ data: { count: 2 } }),
+ );
+
+ let context = { test: true };
+
+ testLinkResults({
+ link,
+ results: [{ count: 1 }],
+ context,
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ results: [{ count: 2 }],
+ context,
+ done,
+ });
+ });
+
+ it('should split two Links', done => {
+ const link = ApolloLink.split(
+ operation => operation.getContext().test,
+ (operation, forward) => Observable.of({ data: { count: 1 } }),
+ new ApolloLink((operation, forward) =>
+ Observable.of({ data: { count: 2 } }),
+ ),
+ );
+
+ let context = { test: true };
+
+ testLinkResults({
+ link,
+ results: [{ count: 1 }],
+ context,
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ results: [{ count: 2 }],
+ context,
+ done,
+ });
+ });
+
+ it('should split a link and a function', done => {
+ const link = ApolloLink.split(
+ operation => operation.getContext().test,
+ (operation, forward) => Observable.of({ data: { count: 1 } }),
+ new ApolloLink((operation, forward) =>
+ Observable.of({ data: { count: 2 } }),
+ ),
+ );
+
+ let context = { test: true };
+
+ testLinkResults({
+ link,
+ results: [{ count: 1 }],
+ context,
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ results: [{ count: 2 }],
+ context,
+ done,
+ });
+ });
+
+ it('should allow concat after split to be join', done => {
+ const context = { test: true };
+ const link = ApolloLink.split(
+ operation => operation.getContext().test,
+ (operation, forward) =>
+ forward(operation).map(data => ({
+ data: { count: data.data.count + 1 },
+ })),
+ ).concat(() => Observable.of({ data: { count: 1 } }));
+
+ testLinkResults({
+ link,
+ context,
+ results: [{ count: 2 }],
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ context,
+ results: [{ count: 1 }],
+ done,
+ });
+ });
+
+ it('should allow default right to be passthrough', done => {
+ const context = { test: true };
+ const link = ApolloLink.split(
+ operation => operation.getContext().test,
+ operation => Observable.of({ data: { count: 2 } }),
+ ).concat(operation => Observable.of({ data: { count: 1 } }));
+
+ testLinkResults({
+ link,
+ context,
+ results: [{ count: 2 }],
+ });
+
+ context.test = false;
+
+ testLinkResults({
+ link,
+ context,
+ results: [{ count: 1 }],
+ done,
+ });
+ });
+ });
+
+ describe('Terminating links', () => {
+ const _warn = console.warn;
+ const warningStub = jest.fn(warning => {
+ expect(warning.message).toBe(
+ `You are calling concat on a terminating link, which will have no effect`,
+ );
+ });
+ const data = {
+ stub: 'data',
+ };
+
+ beforeAll(() => {
+ console.warn = warningStub;
+ });
+
+ beforeEach(() => {
+ warningStub.mockClear();
+ });
+
+ afterAll(() => {
+ console.warn = _warn;
+ });
+
+ describe('split', () => {
+ it('should not warn if attempting to split a terminating and non-terminating Link', () => {
+ const split = ApolloLink.split(
+ () => true,
+ operation => Observable.of({ data }),
+ (operation, forward) => forward(operation),
+ );
+ split.concat((operation, forward) => forward(operation));
+ expect(warningStub).not.toBeCalled();
+ });
+
+ it('should warn if attempting to concat to split two terminating links', () => {
+ const split = ApolloLink.split(
+ () => true,
+ operation => Observable.of({ data }),
+ operation => Observable.of({ data }),
+ );
+ expect(split.concat((operation, forward) => forward(operation))).toEqual(
+ split,
+ );
+ expect(warningStub).toHaveBeenCalledTimes(1);
+ });
+
+ it('should warn if attempting to split to split two terminating links', () => {
+ const split = ApolloLink.split(
+ () => true,
+ operation => Observable.of({ data }),
+ operation => Observable.of({ data }),
+ );
+ expect(
+ split.split(
+ () => true,
+ (operation, forward) => forward(operation),
+ (operation, forward) => forward(operation),
+ ),
+ ).toEqual(split);
+ expect(warningStub).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('from', () => {
+ it('should not warn if attempting to form a terminating then non-terminating Link', () => {
+ ApolloLink.from([
+ (operation, forward) => forward(operation),
+ operation => Observable.of({ data }),
+ ]);
+ expect(warningStub).not.toBeCalled();
+ });
+
+ it('should warn if attempting to add link after termination', () => {
+ ApolloLink.from([
+ (operation, forward) => forward(operation),
+ operation => Observable.of({ data }),
+ (operation, forward) => forward(operation),
+ ]);
+ expect(warningStub).toHaveBeenCalledTimes(1);
+ });
+
+ it('should warn if attempting to add link after termination', () => {
+ ApolloLink.from([
+ new ApolloLink((operation, forward) => forward(operation)),
+ new ApolloLink(operation => Observable.of({ data })),
+ new ApolloLink((operation, forward) => forward(operation)),
+ ]);
+ expect(warningStub).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('concat', () => {
+ it('should warn if attempting to concat to a terminating Link from function', () => {
+ const link = new ApolloLink(operation => Observable.of({ data }));
+ expect(ApolloLink.concat(link, (operation, forward) => forward(operation))).toEqual(
+ link,
+ );
+ expect(warningStub).toHaveBeenCalledTimes(1);
+ expect(warningStub.mock.calls[0][0].link).toEqual(link);
+ });
+
+ it('should warn if attempting to concat to a terminating Link', () => {
+ const link = new ApolloLink(operation => Observable.of());
+ expect(link.concat((operation, forward) => forward(operation))).toEqual(
+ link,
+ );
+ expect(warningStub).toHaveBeenCalledTimes(1);
+ expect(warningStub.mock.calls[0][0].link).toEqual(link);
+ });
+
+ it('should not warn if attempting concat a terminating Link at end', () => {
+ const link = new ApolloLink((operation, forward) => forward(operation));
+ link.concat(operation => Observable.of());
+ expect(warningStub).not.toBeCalled();
+ });
+ });
+
+ describe('warning', () => {
+ it('should include link that terminates', () => {
+ const terminatingLink = new ApolloLink(operation =>
+ Observable.of({ data }),
+ );
+ ApolloLink.from([
+ new ApolloLink((operation, forward) => forward(operation)),
+ new ApolloLink((operation, forward) => forward(operation)),
+ terminatingLink,
+ new ApolloLink((operation, forward) => forward(operation)),
+ new ApolloLink((operation, forward) => forward(operation)),
+ new ApolloLink(operation => Observable.of({ data })),
+ new ApolloLink((operation, forward) => forward(operation)),
+ ]);
+ expect(warningStub).toHaveBeenCalledTimes(4);
+ });
+ });
+ });
+});
diff --git a/src/link/core/concat.ts b/src/link/core/concat.ts
new file mode 100644
index 00000000000..a6cbcee58e2
--- /dev/null
+++ b/src/link/core/concat.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const concat = ApolloLink.concat;
diff --git a/src/link/core/empty.ts b/src/link/core/empty.ts
new file mode 100644
index 00000000000..8d93c8ce90d
--- /dev/null
+++ b/src/link/core/empty.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const empty = ApolloLink.empty;
diff --git a/src/link/core/execute.ts b/src/link/core/execute.ts
new file mode 100644
index 00000000000..49217fa8ad3
--- /dev/null
+++ b/src/link/core/execute.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const execute = ApolloLink.execute;
diff --git a/src/link/core/from.ts b/src/link/core/from.ts
new file mode 100644
index 00000000000..c2bed562ea6
--- /dev/null
+++ b/src/link/core/from.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const from = ApolloLink.from;
diff --git a/src/link/core/index.ts b/src/link/core/index.ts
new file mode 100644
index 00000000000..8093e7c6cd2
--- /dev/null
+++ b/src/link/core/index.ts
@@ -0,0 +1,8 @@
+export { empty } from './empty';
+export { from } from './from';
+export { split } from './split';
+export { concat } from './concat';
+export { execute } from './execute';
+export { ApolloLink } from './ApolloLink';
+
+export * from './types';
diff --git a/src/link/core/split.ts b/src/link/core/split.ts
new file mode 100644
index 00000000000..c109ebd29f4
--- /dev/null
+++ b/src/link/core/split.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const split = ApolloLink.split;
diff --git a/src/link/core/types.ts b/src/link/core/types.ts
new file mode 100644
index 00000000000..4a15f5035c2
--- /dev/null
+++ b/src/link/core/types.ts
@@ -0,0 +1,38 @@
+import { DocumentNode } from 'graphql/language/ast';
+import { ExecutionResult } from 'graphql/execution/execute';
+export { ExecutionResult, DocumentNode };
+
+import { Observable } from '../../util/Observable';
+
+export interface GraphQLRequest {
+ query: DocumentNode;
+ variables?: Record;
+ operationName?: string;
+ context?: Record;
+ extensions?: Record;
+}
+
+export interface Operation {
+ query: DocumentNode;
+ variables: Record;
+ operationName: string;
+ extensions: Record;
+ setContext: (context: Record) => Record;
+ getContext: () => Record;
+}
+
+export type FetchResult<
+ TData = { [key: string]: any },
+ C = Record,
+ E = Record
+> = ExecutionResult & {
+ extensions?: E;
+ context?: C;
+};
+
+export type NextLink = (operation: Operation) => Observable;
+
+export type RequestHandler = (
+ operation: Operation,
+ forward: NextLink,
+) => Observable | null;
diff --git a/src/link/http/HttpLink.ts b/src/link/http/HttpLink.ts
new file mode 100644
index 00000000000..cc2a8d5fa14
--- /dev/null
+++ b/src/link/http/HttpLink.ts
@@ -0,0 +1,10 @@
+import { ApolloLink, RequestHandler } from '../core';
+import { HttpOptions } from './selectHttpOptionsAndBody';
+import { createHttpLink } from './createHttpLink';
+
+export class HttpLink extends ApolloLink {
+ public requester: RequestHandler;
+ constructor(public options: HttpOptions = {}) {
+ super(createHttpLink(options).request);
+ }
+}
diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts
new file mode 100644
index 00000000000..981ebb23925
--- /dev/null
+++ b/src/link/http/__tests__/HttpLink.ts
@@ -0,0 +1,1111 @@
+import gql from 'graphql-tag';
+import fetchMock from 'fetch-mock';
+import { print } from 'graphql';
+
+import { Observable } from '../../../util/Observable';
+import { ApolloLink, execute } from '../../core';
+import { HttpLink } from '../HttpLink';
+import { createHttpLink } from '../createHttpLink';
+
+const sampleQuery = gql`
+ query SampleQuery {
+ stub {
+ id
+ }
+ }
+`;
+
+const sampleMutation = gql`
+ mutation SampleMutation {
+ stub {
+ id
+ }
+ }
+`;
+
+const makeCallback = (done, body) => {
+ return (...args) => {
+ try {
+ body(...args);
+ done();
+ } catch (error) {
+ done.fail(error);
+ }
+ };
+};
+
+const convertBatchedBody = body => {
+ const parsed = JSON.parse(body);
+ return parsed;
+};
+
+const makePromise =
+ res => new Promise((resolve) => setTimeout(() => resolve(res)));
+
+describe('HttpLink', () => {
+ describe('General', () => {
+ const data = { data: { hello: 'world' } };
+ const data2 = { data: { hello: 'everyone' } };
+ const mockError = { throws: new TypeError('mock me') };
+ let subscriber;
+
+ beforeEach(() => {
+ fetchMock.restore();
+ fetchMock.post('begin:/data2', makePromise(data2));
+ fetchMock.post('begin:/data', makePromise(data));
+ fetchMock.post('begin:/error', mockError);
+ fetchMock.post('begin:/apollo', makePromise(data));
+
+ fetchMock.get('begin:/data', makePromise(data));
+ fetchMock.get('begin:/data2', makePromise(data2));
+
+ const next = jest.fn();
+ const error = jest.fn();
+ const complete = jest.fn();
+
+ subscriber = {
+ next,
+ error,
+ complete,
+ };
+ });
+
+ afterEach(() => {
+ fetchMock.restore();
+ });
+
+ it('does not need any constructor arguments', () => {
+ expect(() => new HttpLink()).not.toThrow();
+ });
+
+ it('constructor creates link that can call next and then complete', done => {
+ const next = jest.fn();
+ const link = new HttpLink({ uri: '/data' });
+ const observable = execute(link, {
+ query: sampleQuery,
+ });
+ observable.subscribe({
+ next,
+ error: error => expect(false),
+ complete: () => {
+ expect(next).toHaveBeenCalledTimes(1);
+ done();
+ },
+ });
+ });
+
+ it('supports using a GET request', done => {
+ const variables = { params: 'stub' };
+ const extensions = { myExtension: 'foo' };
+
+ const link = createHttpLink({
+ uri: '/data',
+ fetchOptions: { method: 'GET' },
+ includeExtensions: true,
+ });
+
+ execute(link, { query: sampleQuery, variables, extensions }).subscribe({
+ next: makeCallback(done, result => {
+ const [uri, options] = fetchMock.lastCall();
+ const { method, body } = options;
+ expect(body).toBeUndefined();
+ expect(method).toBe('GET');
+ expect(uri).toBe(
+ '/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D',
+ );
+ }),
+ error: error => done.fail(error),
+ });
+ });
+
+ it('supports using a GET request with search', done => {
+ const variables = { params: 'stub' };
+
+ const link = createHttpLink({
+ uri: '/data?foo=bar',
+ fetchOptions: { method: 'GET' },
+ });
+
+ execute(link, { query: sampleQuery, variables }).subscribe({
+ next: makeCallback(done, result => {
+ const [uri, options] = fetchMock.lastCall();
+ const { method, body } = options;
+ expect(body).toBeUndefined();
+ expect(method).toBe('GET');
+ expect(uri).toBe(
+ '/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D',
+ );
+ }),
+ error: error => done.fail(error),
+ });
+ });
+
+ it('supports using a GET request on the context', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({
+ uri: '/data',
+ });
+
+ execute(link, {
+ query: sampleQuery,
+ variables,
+ context: {
+ fetchOptions: { method: 'GET' },
+ },
+ }).subscribe(
+ makeCallback(done, result => {
+ const [uri, options] = fetchMock.lastCall();
+ const { method, body } = options;
+ expect(body).toBeUndefined();
+ expect(method).toBe('GET');
+ expect(uri).toBe(
+ '/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D',
+ );
+ }),
+ );
+ });
+
+ it('uses GET with useGETForQueries', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({
+ uri: '/data',
+ useGETForQueries: true,
+ });
+
+ execute(link, {
+ query: sampleQuery,
+ variables,
+ }).subscribe(
+ makeCallback(done, result => {
+ const [uri, options] = fetchMock.lastCall();
+ const { method, body } = options;
+ expect(body).toBeUndefined();
+ expect(method).toBe('GET');
+ expect(uri).toBe(
+ '/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D',
+ );
+ }),
+ );
+ });
+
+ it('uses POST for mutations with useGETForQueries', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({
+ uri: '/data',
+ useGETForQueries: true,
+ });
+
+ execute(link, {
+ query: sampleMutation,
+ variables,
+ }).subscribe(
+ makeCallback(done, result => {
+ const [uri, options] = fetchMock.lastCall();
+ const { method, body } = options;
+ expect(body).toBeDefined();
+ expect(method).toBe('POST');
+ expect(uri).toBe('/data');
+ }),
+ );
+ });
+
+ it('should add client awareness settings to request headers', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({
+ uri: '/data',
+ });
+
+ const clientAwareness = {
+ name: 'Some Client Name',
+ version: '1.0.1',
+ };
+
+ execute(link, {
+ query: sampleQuery,
+ variables,
+ context: {
+ clientAwareness,
+ },
+ }).subscribe(
+ makeCallback(done, result => {
+ const [uri, options] = fetchMock.lastCall();
+ const { headers } = options;
+ expect(headers['apollographql-client-name']).toBeDefined();
+ expect(headers['apollographql-client-name']).toEqual(
+ clientAwareness.name,
+ );
+ expect(headers['apollographql-client-version']).toBeDefined();
+ expect(headers['apollographql-client-version']).toEqual(
+ clientAwareness.version,
+ );
+ }),
+ );
+ });
+
+ it('should not add empty client awareness settings to request headers', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({
+ uri: '/data',
+ });
+
+ const hasOwn = Object.prototype.hasOwnProperty;
+ const clientAwareness = {};
+ execute(link, {
+ query: sampleQuery,
+ variables,
+ context: {
+ clientAwareness,
+ },
+ }).subscribe(
+ makeCallback(done, result => {
+ const [uri, options] = fetchMock.lastCall();
+ const { headers } = options;
+ expect(hasOwn.call(headers, 'apollographql-client-name')).toBe(false);
+ expect(hasOwn.call(headers, 'apollographql-client-version')).toBe(
+ false,
+ );
+ }),
+ );
+ });
+
+ it("throws for GET if the variables can't be stringified", done => {
+ const link = createHttpLink({
+ uri: '/data',
+ useGETForQueries: true,
+ });
+
+ let b;
+ const a = { b };
+ b = { a };
+ a.b = b;
+ const variables = {
+ a,
+ b,
+ };
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ result => {
+ done.fail('next should have been thrown from the link');
+ },
+ makeCallback(done, e => {
+ expect(e.message).toMatch(/Variables map is not serializable/);
+ expect(e.parseError.message).toMatch(
+ /Converting circular structure to JSON/,
+ );
+ }),
+ );
+ });
+
+ it("throws for GET if the extensions can't be stringified", done => {
+ const link = createHttpLink({
+ uri: '/data',
+ useGETForQueries: true,
+ includeExtensions: true,
+ });
+
+ let b;
+ const a = { b };
+ b = { a };
+ a.b = b;
+ const extensions = {
+ a,
+ b,
+ };
+ execute(link, { query: sampleQuery, extensions }).subscribe(
+ result => {
+ done.fail('next should have been thrown from the link');
+ },
+ makeCallback(done, e => {
+ expect(e.message).toMatch(/Extensions map is not serializable/);
+ expect(e.parseError.message).toMatch(
+ /Converting circular structure to JSON/,
+ );
+ }),
+ );
+ });
+
+ it('raises warning if called with concat', () => {
+ const link = createHttpLink();
+ const _warn = console.warn;
+ console.warn = warning => expect(warning['message']).toBeDefined();
+ expect(link.concat((operation, forward) => forward(operation))).toEqual(
+ link,
+ );
+ console.warn = _warn;
+ });
+
+ it('does not need any constructor arguments', () => {
+ expect(() => createHttpLink()).not.toThrow();
+ });
+
+ it('calls next and then complete', done => {
+ const next = jest.fn();
+ const link = createHttpLink({ uri: 'data' });
+ const observable = execute(link, {
+ query: sampleQuery,
+ });
+ observable.subscribe({
+ next,
+ error: error => done.fail(error),
+ complete: makeCallback(done, () => {
+ expect(next).toHaveBeenCalledTimes(1);
+ }),
+ });
+ });
+
+ it('calls error when fetch fails', done => {
+ const link = createHttpLink({ uri: 'error' });
+ const observable = execute(link, {
+ query: sampleQuery,
+ });
+ observable.subscribe(
+ result => done.fail('next should not have been called'),
+ makeCallback(done, error => {
+ expect(error).toEqual(mockError.throws);
+ }),
+ () => done.fail('complete should not have been called'),
+ );
+ });
+
+ it('calls error when fetch fails', done => {
+ const link = createHttpLink({ uri: 'error' });
+ const observable = execute(link, {
+ query: sampleMutation,
+ });
+ observable.subscribe(
+ result => done.fail('next should not have been called'),
+ makeCallback(done, error => {
+ expect(error).toEqual(mockError.throws);
+ }),
+ () => done.fail('complete should not have been called'),
+ );
+ });
+
+ it('unsubscribes without calling subscriber', done => {
+ const link = createHttpLink({ uri: 'data' });
+ const observable = execute(link, {
+ query: sampleQuery,
+ });
+ const subscription = observable.subscribe(
+ result => done.fail('next should not have been called'),
+ error => done.fail(error),
+ () => done.fail('complete should not have been called'),
+ );
+ subscription.unsubscribe();
+ expect(subscription.closed).toBe(true);
+ setTimeout(done, 50);
+ });
+
+ const verifyRequest = (
+ link: ApolloLink,
+ after: () => void,
+ includeExtensions: boolean,
+ done: any,
+ ) => {
+ const next = jest.fn();
+ const context = { info: 'stub' };
+ const variables = { params: 'stub' };
+
+ const observable = execute(link, {
+ query: sampleMutation,
+ context,
+ variables,
+ });
+ observable.subscribe({
+ next,
+ error: error => done.fail(error),
+ complete: () => {
+ try {
+ let body = convertBatchedBody(fetchMock.lastCall()[1].body);
+ expect(body.query).toBe(print(sampleMutation));
+ expect(body.variables).toEqual(variables);
+ expect(body.context).not.toBeDefined();
+ if (includeExtensions) {
+ expect(body.extensions).toBeDefined();
+ } else {
+ expect(body.extensions).not.toBeDefined();
+ }
+ expect(next).toHaveBeenCalledTimes(1);
+
+ after();
+ } catch (e) {
+ done.fail(e);
+ }
+ },
+ });
+ };
+
+ it('passes all arguments to multiple fetch body including extensions', done => {
+ debugger;
+ const link = createHttpLink({ uri: 'data', includeExtensions: true });
+ verifyRequest(
+ link,
+ () => verifyRequest(link, done, true, done),
+ true,
+ done,
+ );
+ });
+
+ it('passes all arguments to multiple fetch body excluding extensions', done => {
+ const link = createHttpLink({ uri: 'data' });
+ verifyRequest(
+ link,
+ () => verifyRequest(link, done, false, done),
+ false,
+ done,
+ );
+ });
+
+ it('calls multiple subscribers', done => {
+ const link = createHttpLink({ uri: 'data' });
+ const context = { info: 'stub' };
+ const variables = { params: 'stub' };
+
+ const observable = execute(link, {
+ query: sampleMutation,
+ context,
+ variables,
+ });
+ observable.subscribe(subscriber);
+ observable.subscribe(subscriber);
+
+ setTimeout(() => {
+ expect(subscriber.next).toHaveBeenCalledTimes(2);
+ expect(subscriber.complete).toHaveBeenCalledTimes(2);
+ expect(subscriber.error).not.toHaveBeenCalled();
+ done();
+ }, 50);
+ });
+
+ it('calls remaining subscribers after unsubscribe', done => {
+ const link = createHttpLink({ uri: 'data' });
+ const context = { info: 'stub' };
+ const variables = { params: 'stub' };
+
+ const observable = execute(link, {
+ query: sampleMutation,
+ context,
+ variables,
+ });
+
+ observable.subscribe(subscriber);
+
+ setTimeout(() => {
+ const subscription = observable.subscribe(subscriber);
+ subscription.unsubscribe();
+ }, 10);
+
+ setTimeout(
+ makeCallback(done, () => {
+ expect(subscriber.next).toHaveBeenCalledTimes(1);
+ expect(subscriber.complete).toHaveBeenCalledTimes(1);
+ expect(subscriber.error).not.toHaveBeenCalled();
+ done();
+ }),
+ 50,
+ );
+ });
+
+ it('allows for dynamic endpoint setting', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({ uri: 'data' });
+
+ execute(link, {
+ query: sampleQuery,
+ variables,
+ context: { uri: 'data2' },
+ }).subscribe(result => {
+ expect(result).toEqual(data2);
+ done();
+ });
+ });
+
+ it('adds headers to the request from the context', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ headers: { authorization: '1234' },
+ });
+ return forward(operation).map(result => {
+ const { headers } = operation.getContext();
+ try {
+ expect(headers).toBeDefined();
+ } catch (e) {
+ done.fail(e);
+ }
+ return result;
+ });
+ });
+ const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const headers = fetchMock.lastCall()[1].headers;
+ expect(headers.authorization).toBe('1234');
+ expect(headers['content-type']).toBe('application/json');
+ expect(headers.accept).toBe('*/*');
+ }),
+ );
+ });
+
+ it('adds headers to the request from the setup', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({
+ uri: 'data',
+ headers: { authorization: '1234' },
+ });
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const headers = fetchMock.lastCall()[1].headers;
+ expect(headers.authorization).toBe('1234');
+ expect(headers['content-type']).toBe('application/json');
+ expect(headers.accept).toBe('*/*');
+ }),
+ );
+ });
+
+ it('prioritizes context headers over setup headers', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ headers: { authorization: '1234' },
+ });
+ return forward(operation);
+ });
+ const link = middleware.concat(
+ createHttpLink({ uri: 'data', headers: { authorization: 'no user' } }),
+ );
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const headers = fetchMock.lastCall()[1].headers;
+ expect(headers.authorization).toBe('1234');
+ expect(headers['content-type']).toBe('application/json');
+ expect(headers.accept).toBe('*/*');
+ }),
+ );
+ });
+
+ it('adds headers to the request from the context on an operation', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({ uri: 'data' });
+
+ const context = {
+ headers: { authorization: '1234' },
+ };
+ execute(link, {
+ query: sampleQuery,
+ variables,
+ context,
+ }).subscribe(
+ makeCallback(done, result => {
+ const headers = fetchMock.lastCall()[1].headers;
+ expect(headers.authorization).toBe('1234');
+ expect(headers['content-type']).toBe('application/json');
+ expect(headers.accept).toBe('*/*');
+ }),
+ );
+ });
+
+ it('adds creds to the request from the context', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ credentials: 'same-team-yo',
+ });
+ return forward(operation);
+ });
+ const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const creds = fetchMock.lastCall()[1].credentials;
+ expect(creds).toBe('same-team-yo');
+ }),
+ );
+ });
+
+ it('adds creds to the request from the setup', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({ uri: 'data', credentials: 'same-team-yo' });
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const creds = fetchMock.lastCall()[1].credentials;
+ expect(creds).toBe('same-team-yo');
+ }),
+ );
+ });
+
+ it('prioritizes creds from the context over the setup', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ credentials: 'same-team-yo',
+ });
+ return forward(operation);
+ });
+ const link = middleware.concat(
+ createHttpLink({ uri: 'data', credentials: 'error' }),
+ );
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const creds = fetchMock.lastCall()[1].credentials;
+ expect(creds).toBe('same-team-yo');
+ }),
+ );
+ });
+
+ it('adds uri to the request from the context', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ uri: 'data',
+ });
+ return forward(operation);
+ });
+ const link = middleware.concat(createHttpLink());
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const uri = fetchMock.lastUrl();
+ expect(uri).toBe('/data');
+ }),
+ );
+ });
+
+ it('adds uri to the request from the setup', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({ uri: 'data' });
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const uri = fetchMock.lastUrl();
+ expect(uri).toBe('/data');
+ }),
+ );
+ });
+
+ it('prioritizes context uri over setup uri', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ uri: 'apollo',
+ });
+ return forward(operation);
+ });
+ const link = middleware.concat(
+ createHttpLink({ uri: 'data', credentials: 'error' }),
+ );
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const uri = fetchMock.lastUrl();
+
+ expect(uri).toBe('/apollo');
+ }),
+ );
+ });
+
+ it('allows uri to be a function', done => {
+ const variables = { params: 'stub' };
+ const customFetch = (uri, options) => {
+ const { operationName } = convertBatchedBody(options.body);
+ try {
+ expect(operationName).toBe('SampleQuery');
+ } catch (e) {
+ done.fail(e);
+ }
+ return fetch('dataFunc', options);
+ };
+
+ const link = createHttpLink({ fetch: customFetch });
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const uri = fetchMock.lastUrl();
+ expect(fetchMock.lastUrl()).toBe('/dataFunc');
+ }),
+ );
+ });
+
+ it('adds fetchOptions to the request from the setup', done => {
+ const variables = { params: 'stub' };
+ const link = createHttpLink({
+ uri: 'data',
+ fetchOptions: { someOption: 'foo', mode: 'no-cors' },
+ });
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const { someOption, mode, headers } = fetchMock.lastCall()[1];
+ expect(someOption).toBe('foo');
+ expect(mode).toBe('no-cors');
+ expect(headers['content-type']).toBe('application/json');
+ }),
+ );
+ });
+
+ it('adds fetchOptions to the request from the context', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ fetchOptions: {
+ someOption: 'foo',
+ },
+ });
+ return forward(operation);
+ });
+ const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const { someOption } = fetchMock.lastCall()[1];
+ expect(someOption).toBe('foo');
+ done();
+ }),
+ );
+ });
+
+ it('prioritizes context over setup', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ fetchOptions: {
+ someOption: 'foo',
+ },
+ });
+ return forward(operation);
+ });
+ const link = middleware.concat(
+ createHttpLink({ uri: 'data', fetchOptions: { someOption: 'bar' } }),
+ );
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ const { someOption } = fetchMock.lastCall()[1];
+ expect(someOption).toBe('foo');
+ }),
+ );
+ });
+
+ it('allows for not sending the query with the request', done => {
+ const variables = { params: 'stub' };
+ const middleware = new ApolloLink((operation, forward) => {
+ operation.setContext({
+ http: {
+ includeQuery: false,
+ includeExtensions: true,
+ },
+ });
+ operation.extensions.persistedQuery = { hash: '1234' };
+ return forward(operation);
+ });
+ const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ makeCallback(done, result => {
+ let body = convertBatchedBody(fetchMock.lastCall()[1].body);
+
+ expect(body.query).not.toBeDefined();
+ expect(body.extensions).toEqual({ persistedQuery: { hash: '1234' } });
+ done();
+ }),
+ );
+ });
+
+ it('sets the raw response on context', done => {
+ const middleware = new ApolloLink((operation, forward) => {
+ return new Observable(ob => {
+ const op = forward(operation);
+ const sub = op.subscribe({
+ next: ob.next.bind(ob),
+ error: ob.error.bind(ob),
+ complete: makeCallback(done, e => {
+ expect(operation.getContext().response.headers.toBeDefined);
+ ob.complete();
+ }),
+ });
+
+ return () => {
+ sub.unsubscribe();
+ };
+ });
+ });
+
+ const link = middleware.concat(createHttpLink({ uri: 'data', fetch }));
+
+ execute(link, { query: sampleQuery }).subscribe(
+ result => {
+ done();
+ },
+ () => {},
+ );
+ });
+ });
+
+ describe('Dev warnings', () => {
+ let oldFetch;
+ beforeEach(() => {
+ oldFetch = window.fetch;
+ delete window.fetch;
+ });
+
+ afterEach(() => {
+ window.fetch = oldFetch;
+ });
+
+ it('warns if fetch is undeclared', done => {
+ try {
+ const link = createHttpLink({ uri: 'data' });
+ done.fail("warning wasn't called");
+ } catch (e) {
+ makeCallback(done, () =>
+ expect(e.message).toMatch(/has not been found globally/),
+ )();
+ }
+ });
+
+ it('warns if fetch is undefined', done => {
+ window.fetch = undefined;
+ try {
+ const link = createHttpLink({ uri: 'data' });
+ done.fail("warning wasn't called");
+ } catch (e) {
+ makeCallback(done, () =>
+ expect(e.message).toMatch(/has not been found globally/),
+ )();
+ }
+ });
+
+ it('does not warn if fetch is undeclared but a fetch is passed', () => {
+ expect(() => {
+ const link = createHttpLink({ uri: 'data', fetch: () => {} });
+ }).not.toThrow();
+ });
+ });
+
+ describe('Error handling', () => {
+ let responseBody;
+ const text = jest.fn(() => {
+ const responseBodyText = '{}';
+ responseBody = JSON.parse(responseBodyText);
+ return Promise.resolve(responseBodyText);
+ });
+ const textWithData = jest.fn(() => {
+ responseBody = {
+ data: { stub: { id: 1 } },
+ errors: [{ message: 'dangit' }],
+ };
+
+ return Promise.resolve(JSON.stringify(responseBody));
+ });
+
+ const textWithErrors = jest.fn(() => {
+ responseBody = {
+ errors: [{ message: 'dangit' }],
+ };
+
+ return Promise.resolve(JSON.stringify(responseBody));
+ });
+ const fetch = jest.fn((uri, options) => {
+ return Promise.resolve({ text });
+ });
+ beforeEach(() => {
+ fetch.mockReset();
+ });
+ it('makes it easy to do stuff on a 401', done => {
+ const middleware = new ApolloLink((operation, forward) => {
+ return new Observable(ob => {
+ fetch.mockReturnValueOnce(Promise.resolve({ status: 401, text }));
+ const op = forward(operation);
+ const sub = op.subscribe({
+ next: ob.next.bind(ob),
+ error: makeCallback(done, e => {
+ expect(e.message).toMatch(/Received status code 401/);
+ expect(e.statusCode).toEqual(401);
+ ob.error(e);
+ }),
+ complete: ob.complete.bind(ob),
+ });
+
+ return () => {
+ sub.unsubscribe();
+ };
+ });
+ });
+
+ const link = middleware.concat(createHttpLink({ uri: 'data', fetch }));
+
+ execute(link, { query: sampleQuery }).subscribe(
+ result => {
+ done.fail('next should have been thrown from the network');
+ },
+ () => {},
+ );
+ });
+
+ it('throws an error if response code is > 300', done => {
+ fetch.mockReturnValueOnce(Promise.resolve({ status: 400, text }));
+ const link = createHttpLink({ uri: 'data', fetch });
+
+ execute(link, { query: sampleQuery }).subscribe(
+ result => {
+ done.fail('next should have been thrown from the network');
+ },
+ makeCallback(done, e => {
+ expect(e.message).toMatch(/Received status code 400/);
+ expect(e.statusCode).toBe(400);
+ expect(e.result).toEqual(responseBody);
+ }),
+ );
+ });
+ it('throws an error if response code is > 300 and returns data', done => {
+ fetch.mockReturnValueOnce(
+ Promise.resolve({ status: 400, text: textWithData }),
+ );
+
+ const link = createHttpLink({ uri: 'data', fetch });
+
+ let called = false;
+
+ execute(link, { query: sampleQuery }).subscribe(
+ result => {
+ called = true;
+ expect(result).toEqual(responseBody);
+ },
+ e => {
+ expect(called).toBe(true);
+ expect(e.message).toMatch(/Received status code 400/);
+ expect(e.statusCode).toBe(400);
+ expect(e.result).toEqual(responseBody);
+ done();
+ },
+ );
+ });
+ it('throws an error if only errors are returned', done => {
+ fetch.mockReturnValueOnce(
+ Promise.resolve({ status: 400, text: textWithErrors }),
+ );
+
+ const link = createHttpLink({ uri: 'data', fetch });
+
+ let called = false;
+
+ execute(link, { query: sampleQuery }).subscribe(
+ result => {
+ done.fail('should not have called result because we have no data');
+ },
+ e => {
+ expect(e.message).toMatch(/Received status code 400/);
+ expect(e.statusCode).toBe(400);
+ expect(e.result).toEqual(responseBody);
+ done();
+ },
+ );
+ });
+ it('throws an error if empty response from the server ', done => {
+ fetch.mockReturnValueOnce(Promise.resolve({ text }));
+ text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }'));
+ const link = createHttpLink({ uri: 'data', fetch });
+
+ execute(link, { query: sampleQuery }).subscribe(
+ result => {
+ done.fail('next should have been thrown from the network');
+ },
+ makeCallback(done, e => {
+ expect(e.message).toMatch(
+ /Server response was missing for query 'SampleQuery'/,
+ );
+ }),
+ );
+ });
+ it("throws if the body can't be stringified", done => {
+ fetch.mockReturnValueOnce(Promise.resolve({ data: {}, text }));
+ const link = createHttpLink({ uri: 'data', fetch });
+
+ let b;
+ const a = { b };
+ b = { a };
+ a.b = b;
+ const variables = {
+ a,
+ b,
+ };
+ execute(link, { query: sampleQuery, variables }).subscribe(
+ result => {
+ done.fail('next should have been thrown from the link');
+ },
+ makeCallback(done, e => {
+ expect(e.message).toMatch(/Payload is not serializable/);
+ expect(e.parseError.message).toMatch(
+ /Converting circular structure to JSON/,
+ );
+ }),
+ );
+ });
+ it('supports being cancelled and does not throw', done => {
+ let called;
+ class AbortController {
+ signal: {};
+ abort = () => {
+ called = true;
+ };
+ }
+
+ global.AbortController = AbortController;
+
+ fetch.mockReturnValueOnce(Promise.resolve({ text }));
+ text.mockReturnValueOnce(
+ Promise.resolve('{ "data": { "hello": "world" } }'),
+ );
+
+ const link = createHttpLink({ uri: 'data', fetch });
+
+ const sub = execute(link, { query: sampleQuery }).subscribe({
+ next: result => {
+ done.fail('result should not have been called');
+ },
+ error: e => {
+ done.fail(e);
+ },
+ complete: () => {
+ done.fail('complete should not have been called');
+ },
+ });
+ sub.unsubscribe();
+
+ setTimeout(
+ makeCallback(done, () => {
+ delete global.AbortController;
+ expect(called).toBe(true);
+ fetch.mockReset();
+ text.mockReset();
+ }),
+ 150,
+ );
+ });
+
+ const body = '{';
+ const unparsableJson = jest.fn(() => Promise.resolve(body));
+ it('throws an error if response is unparsable', done => {
+ fetch.mockReturnValueOnce(
+ Promise.resolve({ status: 400, text: unparsableJson }),
+ );
+ const link = createHttpLink({ uri: 'data', fetch });
+
+ execute(link, { query: sampleQuery }).subscribe(
+ result => {
+ done.fail('next should have been thrown from the network');
+ },
+ makeCallback(done, e => {
+ expect(e.message).toMatch(/JSON/);
+ expect(e.statusCode).toBe(400);
+ expect(e.response).toBeDefined();
+ expect(e.bodyText).toBe(body);
+ }),
+ );
+ });
+ });
+});
diff --git a/src/link/http/__tests__/checkFetcher.ts b/src/link/http/__tests__/checkFetcher.ts
new file mode 100644
index 00000000000..431c3b4905c
--- /dev/null
+++ b/src/link/http/__tests__/checkFetcher.ts
@@ -0,0 +1,23 @@
+import { checkFetcher } from '../checkFetcher';
+
+describe('checkFetcher', () => {
+ let oldFetch;
+ beforeEach(() => {
+ oldFetch = window.fetch;
+ delete window.fetch;
+ });
+
+ afterEach(() => {
+ window.fetch = oldFetch;
+ });
+
+ it('throws if no fetch is present', () => {
+ expect(() => checkFetcher(undefined)).toThrow(
+ /has not been found globally/,
+ );
+ });
+
+ it('does not throws if no fetch is present but a fetch is passed', () => {
+ expect(() => checkFetcher(() => {})).not.toThrow();
+ });
+});
diff --git a/src/link/http/__tests__/parseAndCheckHttpResponse.ts b/src/link/http/__tests__/parseAndCheckHttpResponse.ts
new file mode 100644
index 00000000000..7670675483f
--- /dev/null
+++ b/src/link/http/__tests__/parseAndCheckHttpResponse.ts
@@ -0,0 +1,91 @@
+import gql from 'graphql-tag';
+import fetchMock from 'fetch-mock';
+
+import { createOperation } from '../../utils/createOperation';
+import { parseAndCheckHttpResponse } from '../parseAndCheckHttpResponse';
+
+const query = gql`
+ query SampleQuery {
+ stub {
+ id
+ }
+ }
+`;
+
+describe('parseAndCheckResponse', () => {
+ beforeEach(() => {
+ fetchMock.restore();
+ });
+
+ const operations = [createOperation({}, { query })];
+
+ it('throws a parse error with a status code on unparsable response', done => {
+ const status = 400;
+ fetchMock.mock('begin:/error', status);
+ fetch('error')
+ .then(parseAndCheckHttpResponse(operations))
+ .then(done.fail)
+ .catch(e => {
+ expect(e.statusCode).toBe(status);
+ expect(e.name).toBe('ServerParseError');
+ expect(e).toHaveProperty('response');
+ expect(e).toHaveProperty('bodyText');
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('throws a network error with a status code and result', done => {
+ const status = 403;
+ const body = { data: 'fail' }; //does not contain data or errors
+ fetchMock.mock('begin:/error', {
+ body,
+ status,
+ });
+ fetch('error')
+ .then(parseAndCheckHttpResponse(operations))
+ .then(done.fail)
+ .catch(e => {
+ expect(e.statusCode).toBe(status);
+ expect(e.name).toBe('ServerError');
+ expect(e).toHaveProperty('response');
+ expect(e).toHaveProperty('result');
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('throws a server error on incorrect data', done => {
+ const data = { hello: 'world' }; //does not contain data or erros
+ fetchMock.mock('begin:/incorrect', data);
+ fetch('incorrect')
+ .then(parseAndCheckHttpResponse(operations))
+ .then(done.fail)
+ .catch(e => {
+ expect(e.statusCode).toBe(200);
+ expect(e.name).toBe('ServerError');
+ expect(e).toHaveProperty('response');
+ expect(e.result).toEqual(data);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('is able to return a correct GraphQL result', done => {
+ const errors = ['', '' + new Error('hi')];
+ const data = { data: { hello: 'world' }, errors };
+
+ fetchMock.mock('begin:/data', {
+ body: data,
+ });
+ fetch('data')
+ .then(parseAndCheckHttpResponse(operations))
+ .then(({ data, errors: e }) => {
+ expect(data).toEqual({ hello: 'world' });
+ expect(e.length).toEqual(errors.length);
+ expect(e).toEqual(errors);
+ done();
+ })
+ .catch(done.fail);
+ });
+});
diff --git a/src/link/http/__tests__/selectHttpOptionsAndBody.ts b/src/link/http/__tests__/selectHttpOptionsAndBody.ts
new file mode 100644
index 00000000000..e9e7a538722
--- /dev/null
+++ b/src/link/http/__tests__/selectHttpOptionsAndBody.ts
@@ -0,0 +1,91 @@
+import gql from 'graphql-tag';
+
+import { createOperation } from '../../utils/createOperation';
+import {
+ selectHttpOptionsAndBody,
+ fallbackHttpConfig,
+} from '../selectHttpOptionsAndBody';
+
+const query = gql`
+ query SampleQuery {
+ stub {
+ id
+ }
+ }
+`;
+
+describe('selectHttpOptionsAndBody', () => {
+ it('includeQuery allows the query to be ignored', () => {
+ const { options, body } = selectHttpOptionsAndBody(
+ createOperation({}, { query }),
+ { http: { includeQuery: false } },
+ );
+ expect(body).not.toHaveProperty('query');
+ });
+
+ it('includeExtensions allows the extensions to be added', () => {
+ const extensions = { yo: 'what up' };
+ const { options, body } = selectHttpOptionsAndBody(
+ createOperation({}, { query, extensions }),
+ { http: { includeExtensions: true } },
+ );
+ expect(body).toHaveProperty('extensions');
+ expect((body as any).extensions).toEqual(extensions);
+ });
+
+ it('the fallbackConfig is used if no other configs are specified', () => {
+ const defaultHeaders = {
+ accept: '*/*',
+ 'content-type': 'application/json',
+ };
+
+ const defaultOptions = {
+ method: 'POST',
+ };
+
+ const extensions = { yo: 'what up' };
+ const { options, body } = selectHttpOptionsAndBody(
+ createOperation({}, { query, extensions }),
+ fallbackHttpConfig,
+ );
+
+ expect(body).toHaveProperty('query');
+ expect(body).not.toHaveProperty('extensions');
+
+ expect(options.headers).toEqual(defaultHeaders);
+ expect(options.method).toEqual(defaultOptions.method);
+ });
+
+ it('allows headers, credentials, and setting of method to function correctly', () => {
+ const headers = {
+ accept: 'application/json',
+ 'content-type': 'application/graphql',
+ };
+
+ const credentials = {
+ 'X-Secret': 'djmashko',
+ };
+
+ const opts = {
+ opt: 'hi',
+ };
+
+ const config = { headers, credentials, options: opts };
+
+ const extensions = { yo: 'what up' };
+
+ const { options, body } = selectHttpOptionsAndBody(
+ createOperation({}, { query, extensions }),
+ fallbackHttpConfig,
+ config,
+ );
+
+ expect(body).toHaveProperty('query');
+ expect(body).not.toHaveProperty('extensions');
+
+ expect(options.headers).toEqual(headers);
+ expect(options.credentials).toEqual(credentials);
+ expect(options.opt).toEqual('hi');
+ expect(options.method).toEqual('POST'); //from default
+ });
+});
diff --git a/src/link/http/__tests__/selectURI.ts b/src/link/http/__tests__/selectURI.ts
new file mode 100644
index 00000000000..5e5271c27d5
--- /dev/null
+++ b/src/link/http/__tests__/selectURI.ts
@@ -0,0 +1,32 @@
+import gql from 'graphql-tag';
+
+import { createOperation } from '../../utils/createOperation';
+import { selectURI } from '../selectURI';
+
+const query = gql`
+ query SampleQuery {
+ stub {
+ id
+ }
+ }
+`;
+
+describe('selectURI', () => {
+ it('returns a passed in string', () => {
+ const uri = '/somewhere';
+ const operation = createOperation({ uri }, { query });
+ expect(selectURI(operation)).toEqual(uri);
+ });
+
+ it('returns a fallback of /graphql', () => {
+ const uri = '/graphql';
+ const operation = createOperation({}, { query });
+ expect(selectURI(operation)).toEqual(uri);
+ });
+
+ it('returns the result of a UriFunction', () => {
+ const uri = '/somewhere';
+ const operation = createOperation({}, { query });
+ expect(selectURI(operation, () => uri)).toEqual(uri);
+ });
+});
diff --git a/src/link/http/__tests__/serializeFetchParameter.ts b/src/link/http/__tests__/serializeFetchParameter.ts
new file mode 100644
index 00000000000..b22dbf25156
--- /dev/null
+++ b/src/link/http/__tests__/serializeFetchParameter.ts
@@ -0,0 +1,17 @@
+import { serializeFetchParameter } from '../serializeFetchParameter';
+
+describe('serializeFetchParameter', () => {
+ it('throws a parse error on an unparsable body', () => {
+ const b = {};
+ const a = { b };
+ (b as any).a = a;
+
+ expect(() => serializeFetchParameter(b, 'Label')).toThrow(/Label/);
+ });
+
+ it('returns a correctly parsed body', () => {
+ const body = { no: 'thing' };
+
+ expect(serializeFetchParameter(body, 'Label')).toEqual('{"no":"thing"}');
+ });
+});
diff --git a/src/link/http/checkFetcher.ts b/src/link/http/checkFetcher.ts
new file mode 100644
index 00000000000..6dc594b80cd
--- /dev/null
+++ b/src/link/http/checkFetcher.ts
@@ -0,0 +1,20 @@
+import { InvariantError } from 'ts-invariant';
+
+export const checkFetcher = (fetcher: WindowOrWorkerGlobalScope['fetch']) => {
+ if (!fetcher && typeof fetch === 'undefined') {
+ let library: string = 'unfetch';
+ if (typeof window === 'undefined') library = 'node-fetch';
+ throw new InvariantError(
+ '"fetch" has not been found globally and no fetcher has been ' +
+ 'configured. To fix this, install a fetch package ' +
+ `(like https://www.npmjs.com/package/${library}), instantiate the ` +
+ 'fetcher, and pass it into your `HttpLink` constructor. For example:' +
+ '\n\n' +
+ `import fetch from '${library}';\n` +
+ "import { ApolloClient, HttpLink } from '@apollo/client';\n" +
+ 'const client = new ApolloClient({\n' +
+ " link: new HttpLink({ uri: '/graphq', fetch })\n" +
+ '});'
+ );
+ }
+};
diff --git a/src/link/http/createHttpLink.ts b/src/link/http/createHttpLink.ts
new file mode 100644
index 00000000000..42281dc69af
--- /dev/null
+++ b/src/link/http/createHttpLink.ts
@@ -0,0 +1,181 @@
+import { DefinitionNode } from 'graphql';
+
+import { Observable } from '../../util/Observable';
+import { serializeFetchParameter } from './serializeFetchParameter';
+import { selectURI } from './selectURI';
+import { parseAndCheckHttpResponse } from './parseAndCheckHttpResponse';
+import { checkFetcher } from './checkFetcher';
+import {
+ selectHttpOptionsAndBody,
+ fallbackHttpConfig,
+ HttpOptions
+} from './selectHttpOptionsAndBody';
+import { createSignalIfSupported } from './createSignalIfSupported';
+import { rewriteURIForGET } from './rewriteURIForGET';
+import { ApolloLink } from '../core';
+import { fromError } from '../utils/fromError';
+
+export const createHttpLink = (linkOptions: HttpOptions = {}) => {
+ let {
+ uri = '/graphql',
+ // use default global fetch if nothing passed in
+ fetch: fetcher,
+ includeExtensions,
+ useGETForQueries,
+ ...requestOptions
+ } = linkOptions;
+
+ // dev warnings to ensure fetch is present
+ checkFetcher(fetcher);
+
+ //fetcher is set here rather than the destructuring to ensure fetch is
+ //declared before referencing it. Reference in the destructuring would cause
+ //a ReferenceError
+ if (!fetcher) {
+ fetcher = fetch;
+ }
+
+ const linkConfig = {
+ http: { includeExtensions },
+ options: requestOptions.fetchOptions,
+ credentials: requestOptions.credentials,
+ headers: requestOptions.headers,
+ };
+
+ return new ApolloLink(operation => {
+ let chosenURI = selectURI(operation, uri);
+
+ const context = operation.getContext();
+
+ // `apollographql-client-*` headers are automatically set if a
+ // `clientAwareness` object is found in the context. These headers are
+ // set first, followed by the rest of the headers pulled from
+ // `context.headers`. If desired, `apollographql-client-*` headers set by
+ // the `clientAwareness` object can be overridden by
+ // `apollographql-client-*` headers set in `context.headers`.
+ const clientAwarenessHeaders: {
+ 'apollographql-client-name'?: string;
+ 'apollographql-client-version'?: string;
+ } = {};
+
+ if (context.clientAwareness) {
+ const { name, version } = context.clientAwareness;
+ if (name) {
+ clientAwarenessHeaders['apollographql-client-name'] = name;
+ }
+ if (version) {
+ clientAwarenessHeaders['apollographql-client-version'] = version;
+ }
+ }
+
+ const contextHeaders = { ...clientAwarenessHeaders, ...context.headers };
+
+ const contextConfig = {
+ http: context.http,
+ options: context.fetchOptions,
+ credentials: context.credentials,
+ headers: contextHeaders,
+ };
+
+ //uses fallback, link, and then context to build options
+ const { options, body } = selectHttpOptionsAndBody(
+ operation,
+ fallbackHttpConfig,
+ linkConfig,
+ contextConfig,
+ );
+
+ let controller: any;
+ if (!(options as any).signal) {
+ const { controller: _controller, signal } = createSignalIfSupported();
+ controller = _controller;
+ if (controller) (options as any).signal = signal;
+ }
+
+ // If requested, set method to GET if there are no mutations.
+ const definitionIsMutation = (d: DefinitionNode) => {
+ return d.kind === 'OperationDefinition' && d.operation === 'mutation';
+ };
+ if (
+ useGETForQueries &&
+ !operation.query.definitions.some(definitionIsMutation)
+ ) {
+ options.method = 'GET';
+ }
+
+ if (options.method === 'GET') {
+ const { newURI, parseError } = rewriteURIForGET(chosenURI, body);
+ if (parseError) {
+ return fromError(parseError);
+ }
+ chosenURI = newURI;
+ } else {
+ try {
+ (options as any).body = serializeFetchParameter(body, 'Payload');
+ } catch (parseError) {
+ return fromError(parseError);
+ }
+ }
+
+ return new Observable(observer => {
+ fetcher(chosenURI, options)
+ .then(response => {
+ operation.setContext({ response });
+ return response;
+ })
+ .then(parseAndCheckHttpResponse(operation))
+ .then(result => {
+ // we have data and can send it to back up the link chain
+ observer.next(result);
+ observer.complete();
+ return result;
+ })
+ .catch(err => {
+ // fetch was cancelled so it's already been cleaned up in the unsubscribe
+ if (err.name === 'AbortError') return;
+ // if it is a network error, BUT there is graphql result info
+ // fire the next observer before calling error
+ // this gives apollo-client (and react-apollo) the `graphqlErrors` and `networErrors`
+ // to pass to UI
+ // this should only happen if we *also* have data as part of the response key per
+ // the spec
+ if (err.result && err.result.errors && err.result.data) {
+ // if we don't call next, the UI can only show networkError because AC didn't
+ // get any graphqlErrors
+ // this is graphql execution result info (i.e errors and possibly data)
+ // this is because there is no formal spec how errors should translate to
+ // http status codes. So an auth error (401) could have both data
+ // from a public field, errors from a private field, and a status of 401
+ // {
+ // user { // this will have errors
+ // firstName
+ // }
+ // products { // this is public so will have data
+ // cost
+ // }
+ // }
+ //
+ // the result of above *could* look like this:
+ // {
+ // data: { products: [{ cost: "$10" }] },
+ // errors: [{
+ // message: 'your session has timed out',
+ // path: []
+ // }]
+ // }
+ // status code of above would be a 401
+ // in the UI you want to show data where you can, errors as data where you can
+ // and use correct http status codes
+ observer.next(err.result);
+ }
+ observer.error(err);
+ });
+
+ return () => {
+ // XXX support canceling this request
+ // https://developers.google.com/web/updates/2017/09/abortable-fetch
+ if (controller) controller.abort();
+ };
+ });
+ });
+};
diff --git a/src/link/http/createSignalIfSupported.ts b/src/link/http/createSignalIfSupported.ts
new file mode 100644
index 00000000000..560e8a92645
--- /dev/null
+++ b/src/link/http/createSignalIfSupported.ts
@@ -0,0 +1,8 @@
+export const createSignalIfSupported = () => {
+ if (typeof AbortController === 'undefined')
+ return { controller: false, signal: false };
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ return { controller, signal };
+};
diff --git a/src/link/http/index.ts b/src/link/http/index.ts
new file mode 100644
index 00000000000..2dde558a321
--- /dev/null
+++ b/src/link/http/index.ts
@@ -0,0 +1,26 @@
+export {
+ parseAndCheckHttpResponse,
+ ServerParseError
+} from './parseAndCheckHttpResponse';
+
+export {
+ serializeFetchParameter,
+ ClientParseError
+} from './serializeFetchParameter';
+
+export {
+ HttpOptions,
+ fallbackHttpConfig,
+ selectHttpOptionsAndBody,
+ UriFunction
+} from './selectHttpOptionsAndBody';
+
+export { checkFetcher } from './checkFetcher';
+
+export { createSignalIfSupported } from './createSignalIfSupported';
+
+export { selectURI } from './selectURI';
+
+export { createHttpLink } from './createHttpLink';
+
+export { HttpLink } from './HttpLink';
diff --git a/src/link/http/parseAndCheckHttpResponse.ts b/src/link/http/parseAndCheckHttpResponse.ts
new file mode 100644
index 00000000000..96910eecd74
--- /dev/null
+++ b/src/link/http/parseAndCheckHttpResponse.ts
@@ -0,0 +1,57 @@
+import { Operation } from '../core';
+import { throwServerError } from '../utils/throwServerError';
+
+const { hasOwnProperty } = Object.prototype;
+
+export type ServerParseError = Error & {
+ response: Response;
+ statusCode: number;
+ bodyText: string;
+};
+
+export function parseAndCheckHttpResponse(
+ operations: Operation | Operation[],
+) {
+ return (response: Response) => response
+ .text()
+ .then(bodyText => {
+ try {
+ return JSON.parse(bodyText);
+ } catch (err) {
+ const parseError = err as ServerParseError;
+ parseError.name = 'ServerParseError';
+ parseError.response = response;
+ parseError.statusCode = response.status;
+ parseError.bodyText = bodyText;
+ throw parseError;
+ }
+ })
+ .then((result: any) => {
+ if (response.status >= 300) {
+ // Network error
+ throwServerError(
+ response,
+ result,
+ `Response not successful: Received status code ${response.status}`,
+ );
+ }
+
+ if (
+ !Array.isArray(result) &&
+ !hasOwnProperty.call(result, 'data') &&
+ !hasOwnProperty.call(result, 'errors')
+ ) {
+ // Data error
+ throwServerError(
+ response,
+ result,
+ `Server response was missing for query '${
+ Array.isArray(operations)
+ ? operations.map(op => op.operationName)
+ : operations.operationName
+ }'.`,
+ );
+ }
+ return result;
+ });
+}
diff --git a/src/link/http/rewriteURIForGET.ts b/src/link/http/rewriteURIForGET.ts
new file mode 100644
index 00000000000..c652183e67b
--- /dev/null
+++ b/src/link/http/rewriteURIForGET.ts
@@ -0,0 +1,62 @@
+import { serializeFetchParameter } from './serializeFetchParameter';
+import { Body } from './selectHttpOptionsAndBody';
+
+// For GET operations, returns the given URI rewritten with parameters, or a
+// parse error.
+export function rewriteURIForGET(chosenURI: string, body: Body) {
+ // Implement the standard HTTP GET serialization, plus 'extensions'. Note
+ // the extra level of JSON serialization!
+ const queryParams: string[] = [];
+ const addQueryParam = (key: string, value: string) => {
+ queryParams.push(`${key}=${encodeURIComponent(value)}`);
+ };
+
+ if ('query' in body) {
+ addQueryParam('query', body.query);
+ }
+ if (body.operationName) {
+ addQueryParam('operationName', body.operationName);
+ }
+ if (body.variables) {
+ let serializedVariables;
+ try {
+ serializedVariables = serializeFetchParameter(
+ body.variables,
+ 'Variables map',
+ );
+ } catch (parseError) {
+ return { parseError };
+ }
+ addQueryParam('variables', serializedVariables);
+ }
+ if (body.extensions) {
+ let serializedExtensions;
+ try {
+ serializedExtensions = serializeFetchParameter(
+ body.extensions,
+ 'Extensions map',
+ );
+ } catch (parseError) {
+ return { parseError };
+ }
+ addQueryParam('extensions', serializedExtensions);
+ }
+
+ // Reconstruct the URI with added query params.
+ // XXX This assumes that the URI is well-formed and that it doesn't
+ // already contain any of these query params. We could instead use the
+ // URL API and take a polyfill (whatwg-url@6) for older browsers that
+ // don't support URLSearchParams. Note that some browsers (and
+ // versions of whatwg-url) support URL but not URLSearchParams!
+ let fragment = '',
+ preFragment = chosenURI;
+ const fragmentStart = chosenURI.indexOf('#');
+ if (fragmentStart !== -1) {
+ fragment = chosenURI.substr(fragmentStart);
+ preFragment = chosenURI.substr(0, fragmentStart);
+ }
+ const queryParamsPrefix = preFragment.indexOf('?') === -1 ? '?' : '&';
+ const newURI =
+ preFragment + queryParamsPrefix + queryParams.join('&') + fragment;
+ return { newURI };
+}
diff --git a/src/link/http/selectHttpOptionsAndBody.ts b/src/link/http/selectHttpOptionsAndBody.ts
new file mode 100644
index 00000000000..77b3e1641c3
--- /dev/null
+++ b/src/link/http/selectHttpOptionsAndBody.ts
@@ -0,0 +1,138 @@
+import { print } from 'graphql/language/printer';
+
+import { Operation } from '../core';
+
+export interface UriFunction {
+ (operation: Operation): string;
+}
+
+export interface Body {
+ query?: string;
+ operationName?: string;
+ variables?: Record;
+ extensions?: Record;
+}
+
+export interface HttpOptions {
+ /**
+ * The URI to use when fetching operations.
+ *
+ * Defaults to '/graphql'.
+ */
+ uri?: string | UriFunction;
+
+ /**
+ * Passes the extensions field to your graphql server.
+ *
+ * Defaults to false.
+ */
+ includeExtensions?: boolean;
+
+ /**
+ * A `fetch`-compatible API to use when making requests.
+ */
+ fetch?: WindowOrWorkerGlobalScope['fetch'];
+
+ /**
+ * An object representing values to be sent as headers on the request.
+ */
+ headers?: any;
+
+ /**
+ * The credentials policy you want to use for the fetch call.
+ */
+ credentials?: string;
+
+ /**
+ * Any overrides of the fetch options argument to pass to the fetch call.
+ */
+ fetchOptions?: any;
+
+ /**
+ * If set to true, use the HTTP GET method for query operations. Mutations
+ * will still use the method specified in fetchOptions.method (which defaults
+ * to POST).
+ */
+ useGETForQueries?: boolean;
+}
+
+export interface HttpQueryOptions {
+ includeQuery?: boolean;
+ includeExtensions?: boolean;
+}
+
+export interface HttpConfig {
+ http?: HttpQueryOptions;
+ options?: any;
+ headers?: any;
+ credentials?: any;
+}
+
+const defaultHttpOptions: HttpQueryOptions = {
+ includeQuery: true,
+ includeExtensions: false,
+};
+
+const defaultHeaders = {
+ // headers are case insensitive (https://stackoverflow.com/a/5259004)
+ accept: '*/*',
+ 'content-type': 'application/json',
+};
+
+const defaultOptions = {
+ method: 'POST',
+};
+
+export const fallbackHttpConfig = {
+ http: defaultHttpOptions,
+ headers: defaultHeaders,
+ options: defaultOptions,
+};
+
+export const selectHttpOptionsAndBody = (
+ operation: Operation,
+ fallbackConfig: HttpConfig,
+ ...configs: Array
+) => {
+ let options: HttpConfig & Record = {
+ ...fallbackConfig.options,
+ headers: fallbackConfig.headers,
+ credentials: fallbackConfig.credentials,
+ };
+ let http: HttpQueryOptions = fallbackConfig.http;
+
+ /*
+ * use the rest of the configs to populate the options
+ * configs later in the list will overwrite earlier fields
+ */
+ configs.forEach(config => {
+ options = {
+ ...options,
+ ...config.options,
+ headers: {
+ ...options.headers,
+ ...config.headers,
+ },
+ };
+ if (config.credentials) options.credentials = config.credentials;
+
+ http = {
+ ...http,
+ ...config.http,
+ };
+ });
+
+ //The body depends on the http options
+ const { operationName, extensions, variables, query } = operation;
+ const body: Body = { operationName, variables };
+
+ if (http.includeExtensions) (body as any).extensions = extensions;
+
+ // not sending the query (i.e persisted queries)
+ if (http.includeQuery) (body as any).query = print(query);
+
+ return {
+ options,
+ body,
+ };
+};
diff --git a/src/link/http/selectURI.ts b/src/link/http/selectURI.ts
new file mode 100644
index 00000000000..461098f2e2e
--- /dev/null
+++ b/src/link/http/selectURI.ts
@@ -0,0 +1,17 @@
+import { Operation } from '../core';
+
+export const selectURI = (
+ operation: Operation,
+ fallbackURI?: string | ((operation: Operation) => string),
+) => {
+ const context = operation.getContext();
+ const contextURI = context.uri;
+
+ if (contextURI) {
+ return contextURI;
+ } else if (typeof fallbackURI === 'function') {
+ return fallbackURI(operation);
+ } else {
+ return (fallbackURI as string) || '/graphql';
+ }
+};
diff --git a/src/link/http/serializeFetchParameter.ts b/src/link/http/serializeFetchParameter.ts
new file mode 100644
index 00000000000..f127e36b5b3
--- /dev/null
+++ b/src/link/http/serializeFetchParameter.ts
@@ -0,0 +1,19 @@
+import { InvariantError } from 'ts-invariant';
+
+export type ClientParseError = InvariantError & {
+ parseError: Error;
+};
+
+export const serializeFetchParameter = (p: any, label: string) => {
+ let serialized;
+ try {
+ serialized = JSON.stringify(p);
+ } catch (e) {
+ const parseError = new InvariantError(
+ `Network request failed. ${label} is not serializable: ${e.message}`,
+ ) as ClientParseError;
+ parseError.parseError = e;
+ throw parseError;
+ }
+ return serialized;
+};
diff --git a/src/link/utils/__tests__/fromError.ts b/src/link/utils/__tests__/fromError.ts
new file mode 100644
index 00000000000..4bae95ca74f
--- /dev/null
+++ b/src/link/utils/__tests__/fromError.ts
@@ -0,0 +1,12 @@
+import { toPromise } from '../toPromise';
+import { fromError, } from '../fromError';
+
+describe('fromError', () => {
+ it('acts as error call', () => {
+ const error = new Error('I always error');
+ const observable = fromError(error);
+ return toPromise(observable)
+ .then(expect.fail)
+ .catch(actualError => expect(error).toEqual(actualError));
+ });
+});
diff --git a/src/link/utils/__tests__/fromPromise.ts b/src/link/utils/__tests__/fromPromise.ts
new file mode 100644
index 00000000000..4849f040828
--- /dev/null
+++ b/src/link/utils/__tests__/fromPromise.ts
@@ -0,0 +1,25 @@
+import { fromPromise } from '../fromPromise';
+import { toPromise, } from '../toPromise';
+
+describe('fromPromise', () => {
+ const data = {
+ data: {
+ hello: 'world',
+ },
+ };
+ const error = new Error('I always error');
+
+ it('return next call as Promise resolution', () => {
+ const observable = fromPromise(Promise.resolve(data));
+ return toPromise(observable).then(result =>
+ expect(data).toEqual(result),
+ );
+ });
+
+ it('return Promise rejection as error call', () => {
+ const observable = fromPromise(Promise.reject(error));
+ return toPromise(observable)
+ .then(expect.fail)
+ .catch(actualError => expect(error).toEqual(actualError));
+ });
+});
diff --git a/src/link/utils/__tests__/toPromise.ts b/src/link/utils/__tests__/toPromise.ts
new file mode 100644
index 00000000000..9fc6a5a842d
--- /dev/null
+++ b/src/link/utils/__tests__/toPromise.ts
@@ -0,0 +1,46 @@
+import { Observable } from '../../../util/Observable';
+import { toPromise } from '../toPromise';
+import { fromError } from '../fromError';
+
+describe('toPromise', () => {
+ const data = {
+ data: {
+ hello: 'world',
+ },
+ };
+ const error = new Error('I always error');
+
+ it('return next call as Promise resolution', () => {
+ return toPromise(Observable.of(data)).then(result =>
+ expect(data).toEqual(result),
+ );
+ });
+
+ it('return error call as Promise rejection', () => {
+ return toPromise(fromError(error))
+ .then(expect.fail)
+ .catch(actualError => expect(error).toEqual(actualError));
+ });
+
+ describe('warnings', () => {
+ const spy = jest.fn();
+ let _warn: (message?: any, ...originalParams: any[]) => void;
+
+ beforeEach(() => {
+ _warn = console.warn;
+ console.warn = spy;
+ });
+
+ afterEach(() => {
+ console.warn = _warn;
+ });
+
+ it('return error call as Promise rejection', done => {
+ toPromise(Observable.of(data, data)).then(result => {
+ expect(data).toEqual(result);
+ expect(spy).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+});
diff --git a/src/link/utils/__tests__/validateOperation.ts b/src/link/utils/__tests__/validateOperation.ts
new file mode 100644
index 00000000000..941892fd4e4
--- /dev/null
+++ b/src/link/utils/__tests__/validateOperation.ts
@@ -0,0 +1,17 @@
+import { validateOperation, } from '../validateOperation';
+
+describe('validateOperation', () => {
+ it('should throw when invalid field in operation', () => {
+ expect(() => validateOperation({ qwerty: '' })).toThrow();
+ });
+
+ it('should not throw when valid fields in operation', () => {
+ expect(() =>
+ validateOperation({
+ query: '1234',
+ context: {},
+ variables: {},
+ }),
+ ).not.toThrow();
+ });
+});
diff --git a/src/link/utils/createOperation.ts b/src/link/utils/createOperation.ts
new file mode 100644
index 00000000000..dba0c410b09
--- /dev/null
+++ b/src/link/utils/createOperation.ts
@@ -0,0 +1,28 @@
+import { GraphQLRequest, Operation } from '../core/types';
+
+export function createOperation(
+ starting: any,
+ operation: GraphQLRequest,
+): Operation {
+ let context = { ...starting };
+ const setContext = (next: any) => {
+ if (typeof next === 'function') {
+ context = { ...context, ...next(context) };
+ } else {
+ context = { ...context, ...next };
+ }
+ };
+ const getContext = () => ({ ...context });
+
+ Object.defineProperty(operation, 'setContext', {
+ enumerable: false,
+ value: setContext,
+ });
+
+ Object.defineProperty(operation, 'getContext', {
+ enumerable: false,
+ value: getContext,
+ });
+
+ return operation as Operation;
+}
diff --git a/src/link/utils/fromError.ts b/src/link/utils/fromError.ts
new file mode 100644
index 00000000000..e6d2c52ece3
--- /dev/null
+++ b/src/link/utils/fromError.ts
@@ -0,0 +1,7 @@
+import { Observable } from '../../util/Observable';
+
+export function fromError(errorValue: any): Observable {
+ return new Observable(observer => {
+ observer.error(errorValue);
+ });
+}
diff --git a/src/link/utils/fromPromise.ts b/src/link/utils/fromPromise.ts
new file mode 100644
index 00000000000..9ec31fd675f
--- /dev/null
+++ b/src/link/utils/fromPromise.ts
@@ -0,0 +1,12 @@
+import { Observable } from '../../util/Observable';
+
+export function fromPromise(promise: Promise): Observable {
+ return new Observable(observer => {
+ promise
+ .then((value: T) => {
+ observer.next(value);
+ observer.complete();
+ })
+ .catch(observer.error.bind(observer));
+ });
+}
diff --git a/src/link/utils/index.ts b/src/link/utils/index.ts
new file mode 100644
index 00000000000..d7998382661
--- /dev/null
+++ b/src/link/utils/index.ts
@@ -0,0 +1,2 @@
+export { fromError } from './fromError';
+export { ServerError, throwServerError } from './throwServerError';
diff --git a/src/link/utils/throwServerError.ts b/src/link/utils/throwServerError.ts
new file mode 100644
index 00000000000..b9283d16cd3
--- /dev/null
+++ b/src/link/utils/throwServerError.ts
@@ -0,0 +1,18 @@
+export type ServerError = Error & {
+ response: Response;
+ result: Record;
+ statusCode: number;
+};
+
+export const throwServerError = (
+ response: Response,
+ result: any,
+ message: string
+) => {
+ const error = new Error(message) as ServerError;
+ error.name = 'ServerError';
+ error.response = response;
+ error.statusCode = response.status;
+ error.result = result;
+ throw error;
+};
diff --git a/src/link/utils/toPromise.ts b/src/link/utils/toPromise.ts
new file mode 100644
index 00000000000..13d9fcc5353
--- /dev/null
+++ b/src/link/utils/toPromise.ts
@@ -0,0 +1,22 @@
+import { invariant } from 'ts-invariant';
+
+import { Observable } from '../../util/Observable';
+
+export function toPromise(observable: Observable): Promise {
+ let completed = false;
+ return new Promise((resolve, reject) => {
+ observable.subscribe({
+ next: data => {
+ if (completed) {
+ invariant.warn(
+ `Promise Wrapper does not support multiple results from Observable`,
+ );
+ } else {
+ completed = true;
+ resolve(data);
+ }
+ },
+ error: reject,
+ });
+ });
+}
diff --git a/src/link/utils/transformOperation.ts b/src/link/utils/transformOperation.ts
new file mode 100644
index 00000000000..7fde936c120
--- /dev/null
+++ b/src/link/utils/transformOperation.ts
@@ -0,0 +1,21 @@
+import { GraphQLRequest, Operation } from '../core/types';
+import { getOperationName } from '../../utilities/getFromAST';
+
+export function transformOperation(operation: GraphQLRequest): GraphQLRequest {
+ const transformedOperation: GraphQLRequest = {
+ variables: operation.variables || {},
+ extensions: operation.extensions || {},
+ operationName: operation.operationName,
+ query: operation.query,
+ };
+
+ // Best guess at an operation name
+ if (!transformedOperation.operationName) {
+ transformedOperation.operationName =
+ typeof transformedOperation.query !== 'string'
+ ? getOperationName(transformedOperation.query)
+ : '';
+ }
+
+ return transformedOperation as Operation;
+}
diff --git a/src/link/utils/validateOperation.ts b/src/link/utils/validateOperation.ts
new file mode 100644
index 00000000000..e04fb46ddb1
--- /dev/null
+++ b/src/link/utils/validateOperation.ts
@@ -0,0 +1,20 @@
+import { InvariantError } from 'ts-invariant';
+
+import { GraphQLRequest } from '../core/types';
+
+export function validateOperation(operation: GraphQLRequest): GraphQLRequest {
+ const OPERATION_FIELDS = [
+ 'query',
+ 'operationName',
+ 'variables',
+ 'extensions',
+ 'context',
+ ];
+ for (let key of Object.keys(operation)) {
+ if (OPERATION_FIELDS.indexOf(key) < 0) {
+ throw new InvariantError(`illegal argument: ${key}`);
+ }
+ }
+
+ return operation;
+}
diff --git a/src/react/context/__tests__/ApolloConsumer.test.tsx b/src/react/context/__tests__/ApolloConsumer.test.tsx
index f3f8ed35474..dc0a6875ae9 100644
--- a/src/react/context/__tests__/ApolloConsumer.test.tsx
+++ b/src/react/context/__tests__/ApolloConsumer.test.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-import { ApolloLink } from 'apollo-link';
import { render, cleanup } from '@testing-library/react';
+import { ApolloLink } from '../../../link/core';
import ApolloClient from '../../../ApolloClient';
import { InMemoryCache as Cache } from '../../../cache/inmemory/inMemoryCache';
import { ApolloProvider } from '../ApolloProvider';
diff --git a/src/react/context/__tests__/ApolloProvider.test.tsx b/src/react/context/__tests__/ApolloProvider.test.tsx
index 2a10fa6bb1f..c16ad4780fa 100644
--- a/src/react/context/__tests__/ApolloProvider.test.tsx
+++ b/src/react/context/__tests__/ApolloProvider.test.tsx
@@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import { render, cleanup } from '@testing-library/react';
-import { ApolloLink } from 'apollo-link';
+import { ApolloLink } from '../../../link/core';
import ApolloClient from '../../../ApolloClient';
import { InMemoryCache as Cache } from '../../../cache/inmemory/inMemoryCache';
import { ApolloProvider } from '../ApolloProvider';
diff --git a/src/react/data/MutationData.ts b/src/react/data/MutationData.ts
index 226dfa809db..ca8d512b000 100644
--- a/src/react/data/MutationData.ts
+++ b/src/react/data/MutationData.ts
@@ -6,12 +6,12 @@ import { ApolloError } from '../../errors/ApolloError';
import {
MutationOptions,
MutationTuple,
- ExecutionResult,
MutationFunctionOptions,
MutationResult
} from '../types/types';
import { OperationData } from './OperationData';
import { OperationVariables } from '../../core/types';
+import { FetchResult } from '../../link/core';
export class MutationData<
TData = any,
@@ -66,7 +66,7 @@ export class MutationData<
const mutationId = this.generateNewMutationId();
return this.mutate(mutationFunctionOptions)
- .then((response: ExecutionResult) => {
+ .then((response: FetchResult) => {
this.onMutationCompleted(response, mutationId);
return response;
})
@@ -123,7 +123,7 @@ export class MutationData<
}
private onMutationCompleted(
- response: ExecutionResult,
+ response: FetchResult,
mutationId: number
) {
const { onCompleted, ignoreResults } = this.getOptions();
diff --git a/src/react/hooks/__tests__/useApolloClient.test.tsx b/src/react/hooks/__tests__/useApolloClient.test.tsx
index 63b818225ab..65a5e13921a 100644
--- a/src/react/hooks/__tests__/useApolloClient.test.tsx
+++ b/src/react/hooks/__tests__/useApolloClient.test.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
-import { ApolloLink } from 'apollo-link';
import { InvariantError } from 'ts-invariant';
+import { ApolloLink } from '../../../link/core';
import { ApolloProvider } from '../../context/ApolloProvider';
import { resetApolloContext } from '../../context/ApolloContext';
import ApolloClient from '../../../ApolloClient';
diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx
index 4cc3b094b95..e7906957761 100644
--- a/src/react/hooks/__tests__/useQuery.test.tsx
+++ b/src/react/hooks/__tests__/useQuery.test.tsx
@@ -2,8 +2,9 @@ import React, { useState, useReducer } from 'react';
import { DocumentNode, GraphQLError } from 'graphql';
import gql from 'graphql-tag';
import { render, cleanup, wait } from '@testing-library/react';
-import { ApolloLink, Observable } from 'apollo-link';
+import { Observable } from '../../../util/Observable';
+import { ApolloLink } from '../../../link/core';
import { MockedProvider, MockLink } from '../../testing';
import ApolloClient from '../../../ApolloClient';
import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
diff --git a/src/react/testing/mocks/mockLink.ts b/src/react/testing/mocks/mockLink.ts
index f1e75f3f405..82390cdabd9 100644
--- a/src/react/testing/mocks/mockLink.ts
+++ b/src/react/testing/mocks/mockLink.ts
@@ -1,13 +1,13 @@
+import { print } from 'graphql/language/printer';
+import stringify from 'fast-json-stable-stringify';
+
+import { Observable } from '../../../util/Observable';
import {
Operation,
GraphQLRequest,
ApolloLink,
FetchResult,
- Observable
-} from 'apollo-link';
-import { print } from 'graphql/language/printer';
-import stringify from 'fast-json-stable-stringify';
-
+} from '../../../link/core';
import {
addTypenameToDocument,
removeClientSetsFromDocument,
diff --git a/src/react/testing/mocks/mockSubscriptionLink.ts b/src/react/testing/mocks/mockSubscriptionLink.ts
index 0fc62afdbc4..77901105c66 100644
--- a/src/react/testing/mocks/mockSubscriptionLink.ts
+++ b/src/react/testing/mocks/mockSubscriptionLink.ts
@@ -1,5 +1,5 @@
-import { ApolloLink, FetchResult, Observable } from 'apollo-link';
-
+import { Observable } from '../../../util/Observable';
+import { ApolloLink, FetchResult } from '../../../link/core';
import { MockedSubscriptionResult } from './types';
export class MockSubscriptionLink extends ApolloLink {
diff --git a/src/react/testing/mocks/types.ts b/src/react/testing/mocks/types.ts
index 4b95df0acb5..f32e312ea22 100644
--- a/src/react/testing/mocks/types.ts
+++ b/src/react/testing/mocks/types.ts
@@ -1,5 +1,4 @@
-import { ApolloLink, GraphQLRequest, FetchResult } from 'apollo-link';
-
+import { ApolloLink, GraphQLRequest, FetchResult } from '../../../link/core';
import ApolloClient, { DefaultOptions } from '../../../ApolloClient';
import { Resolvers } from '../../../core/types';
import { ApolloCache } from '../../../cache/core/cache';
diff --git a/src/react/types/types.ts b/src/react/types/types.ts
index bb1a8242ebd..056fd3c7a6e 100644
--- a/src/react/types/types.ts
+++ b/src/react/types/types.ts
@@ -1,7 +1,8 @@
import { ReactNode } from 'react';
-import { DocumentNode, GraphQLError } from 'graphql';
-import { Observable, FetchResult } from 'apollo-link';
+import { DocumentNode } from 'graphql';
+import { Observable } from '../../util/Observable';
+import { FetchResult } from '../../link/core';
import ApolloClient from '../../ApolloClient';
import {
ApolloQueryResult,
@@ -23,12 +24,6 @@ import { NetworkStatus } from '../../core/networkStatus';
export type Context = Record;
-export interface ExecutionResult> {
- data?: T;
- extensions?: Record;
- errors?: GraphQLError[];
-}
-
export type CommonOptions = TOptions & {
client?: ApolloClient