Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add @urql/core/internal entrypoint for internal utilities (#722)

authored by

Phil Plückthun and committed by
GitHub
12b2e61c e6e2fa92

+1141 -480
+6
.changeset/wet-rabbits-tie.md
··· 1 + --- 2 + '@urql/core': minor 3 + '@urql/exchange-multipart-fetch': patch 4 + --- 5 + 6 + Add @urql/core/internal entrypoint for internally shared utilities and start sharing fetchExchange-related code.
+1
exchanges/graphcache/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1 -1
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
··· 409 409 } 410 410 `; 411 411 412 - exports[`on success returns response data 2`] = `"{\\"query\\":\\"query getUser($name: String) {\\\\n user(name: $name) {\\\\n id\\\\n firstName\\\\n lastName\\\\n }\\\\n}\\\\n\\",\\"variables\\":{\\"name\\":\\"Clara\\"},\\"operationName\\":\\"getUser\\"}"`; 412 + exports[`on success returns response data 2`] = `"{\\"query\\":\\"query getUser($name: String) {\\\\n user(name: $name) {\\\\n id\\\\n firstName\\\\n lastName\\\\n }\\\\n}\\\\n\\",\\"operationName\\":\\"getUser\\",\\"variables\\":{\\"name\\":\\"Clara\\"}}"`; 413 413 414 414 exports[`on success uses a file when given 1`] = ` 415 415 Object {
+2 -39
exchanges/multipart-fetch/src/multipartFetchExchange.test.ts
··· 1 1 import { Client, OperationResult, OperationType } from '@urql/core'; 2 2 import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka'; 3 - import gql from 'graphql-tag'; 4 - import { print } from 'graphql'; 3 + 4 + import { multipartFetchExchange } from './multipartFetchExchange'; 5 5 6 - import { multipartFetchExchange, convertToGet } from './multipartFetchExchange'; 7 6 import { 8 7 uploadOperation, 9 8 queryOperation, ··· 215 214 expect(abort).toHaveBeenCalledTimes(0); 216 215 }); 217 216 }); 218 - 219 - describe('convert for GET', () => { 220 - it('should do a basic conversion', () => { 221 - const query = `query ($id: ID!) { node(id: $id) { id } }`; 222 - const variables = { id: 2 }; 223 - expect(convertToGet('http://localhost:3000', { query, variables })).toBe( 224 - `http://localhost:3000?query=${encodeURIComponent( 225 - query 226 - )}&variables=${encodeURIComponent(JSON.stringify(variables))}` 227 - ); 228 - }); 229 - 230 - it('should do a basic conversion with fragments', () => { 231 - const nodeFragment = gql` 232 - fragment nodeFragment on Node { 233 - id 234 - } 235 - `; 236 - 237 - const variables = { id: 2 }; 238 - const query = print(gql` 239 - query($id: ID!) { 240 - node(id: $id) { 241 - ...nodeFragment 242 - } 243 - } 244 - ${nodeFragment} 245 - `); 246 - 247 - expect(convertToGet('http://localhost:3000', { query, variables })).toBe( 248 - `http://localhost:3000?query=${encodeURIComponent( 249 - query 250 - )}&variables=${encodeURIComponent(JSON.stringify(variables))}` 251 - ); 252 - }); 253 - });
+75 -216
exchanges/multipart-fetch/src/multipartFetchExchange.ts
··· 1 - import { Kind, DocumentNode, OperationDefinitionNode, print } from 'graphql'; 2 - import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka'; 1 + import { filter, merge, mergeMap, pipe, share, takeUntil, onPush } from 'wonka'; 3 2 import { extractFiles } from 'extract-files'; 3 + import { Exchange } from '@urql/core'; 4 4 5 5 import { 6 - ExchangeInput, 7 - Exchange, 8 - Operation, 9 - OperationResult, 10 - makeResult, 11 - makeErrorResult, 12 - } from '@urql/core'; 13 - 14 - interface Body { 15 - query: string; 16 - variables: void | object; 17 - operationName?: string; 18 - } 19 - 20 - const isOperationFetchable = (operation: Operation) => 21 - operation.operationName === 'query' || operation.operationName === 'mutation'; 6 + makeFetchBody, 7 + makeFetchURL, 8 + makeFetchOptions, 9 + makeFetchSource, 10 + } from '@urql/core/internal'; 22 11 23 12 export const multipartFetchExchange: Exchange = ({ 24 13 forward, 25 14 dispatchDebug, 26 15 }) => ops$ => { 27 16 const sharedOps$ = share(ops$); 28 - 29 17 const fetchResults$ = pipe( 30 18 sharedOps$, 31 - filter(isOperationFetchable), 19 + filter(operation => { 20 + return ( 21 + operation.operationName === 'query' || 22 + operation.operationName === 'mutation' 23 + ); 24 + }), 32 25 mergeMap(operation => { 33 26 const teardown$ = pipe( 34 27 sharedOps$, ··· 37 30 ) 38 31 ); 39 32 40 - return pipe( 41 - createFetchSource( 42 - operation, 43 - operation.operationName === 'query' && 44 - !!operation.context.preferGetMethod, 45 - dispatchDebug 46 - ), 47 - takeUntil(teardown$) 48 - ); 49 - }) 50 - ); 51 - 52 - const forward$ = pipe( 53 - sharedOps$, 54 - filter(op => !isOperationFetchable(op)), 55 - forward 56 - ); 57 - 58 - return merge([fetchResults$, forward$]); 59 - }; 60 - 61 - const getOperationName = (query: DocumentNode): string | null => { 62 - const node = query.definitions.find( 63 - (node: any): node is OperationDefinitionNode => { 64 - return node.kind === Kind.OPERATION_DEFINITION && node.name; 65 - } 66 - ); 67 - 68 - return node && node.name ? node.name.value : null; 69 - }; 70 - 71 - const createFetchSource = ( 72 - operation: Operation, 73 - shouldUseGet: boolean, 74 - dispatchDebug: ExchangeInput['dispatchDebug'] 75 - ) => { 76 - if ( 77 - process.env.NODE_ENV !== 'production' && 78 - operation.operationName === 'subscription' 79 - ) { 80 - throw new Error( 81 - 'Received a subscription operation in the httpExchange. You are probably trying to create a subscription. Have you added a subscriptionExchange?' 82 - ); 83 - } 84 - 85 - return make<OperationResult>(({ next, complete }) => { 86 - const abortController = 87 - typeof AbortController !== 'undefined' 88 - ? new AbortController() 89 - : undefined; 90 - 91 - const { context } = operation; 92 - // Spreading operation.variables here in case someone made a variables with Object.create(null). 93 - const { files, clone } = extractFiles({ ...operation.variables }); 94 - 95 - const extraOptions = 96 - typeof context.fetchOptions === 'function' 97 - ? context.fetchOptions() 98 - : context.fetchOptions || {}; 99 - 100 - const operationName = getOperationName(operation.query); 101 - 102 - const body: Body = { 103 - query: print(operation.query), 104 - variables: operation.variables, 105 - }; 106 - 107 - if (operationName !== null) { 108 - body.operationName = operationName; 109 - } 110 - 111 - const fetchOptions = { 112 - ...extraOptions, 113 - method: shouldUseGet ? 'GET' : 'POST', 114 - headers: { 115 - 'content-type': 'application/json', 116 - ...extraOptions.headers, 117 - }, 118 - signal: 119 - abortController !== undefined ? abortController.signal : undefined, 120 - }; 121 - 122 - if (!!files.size) { 123 - fetchOptions.body = new FormData(); 124 - fetchOptions.method = 'POST'; 125 - // Make fetch auto-append this for correctness 126 - delete fetchOptions.headers['content-type']; 127 - 128 - fetchOptions.body.append( 129 - 'operations', 130 - JSON.stringify({ 131 - ...body, 132 - variables: clone, 133 - }) 134 - ); 135 - 136 - const map = {}; 137 - let i = 0; 138 - files.forEach(paths => { 139 - map[++i] = paths.map(path => `variables.${path}`); 33 + // Spreading operation.variables here in case someone made a variables with Object.create(null). 34 + const { files, clone: variables } = extractFiles({ 35 + ...operation.variables, 140 36 }); 141 - fetchOptions.body.append('map', JSON.stringify(map)); 37 + const body = makeFetchBody({ query: operation.query, variables }); 142 38 143 - i = 0; 144 - files.forEach((_, file) => { 145 - (fetchOptions.body as FormData).append(`${++i}`, file, file.name); 146 - }); 147 - } else if (shouldUseGet) { 148 - operation.context.url = convertToGet(operation.context.url, body); 149 - } else { 150 - fetchOptions.body = JSON.stringify(body); 151 - } 152 - 153 - let ended = false; 154 - 155 - Promise.resolve() 156 - .then(() => 157 - ended ? undefined : executeFetch(operation, fetchOptions, dispatchDebug) 158 - ) 159 - .then((result: OperationResult | undefined) => { 160 - if (!ended) { 161 - ended = true; 162 - if (result) next(result); 163 - complete(); 39 + let url: string; 40 + let fetchOptions: RequestInit; 41 + if (files.size) { 42 + url = makeFetchURL(operation); 43 + fetchOptions = makeFetchOptions(operation); 44 + if (fetchOptions.headers!['content-type'] === 'application/json') { 45 + delete fetchOptions.headers!['content-type']; 164 46 } 165 - }); 166 47 167 - return () => { 168 - ended = true; 169 - if (abortController !== undefined) { 170 - abortController.abort(); 171 - } 172 - }; 173 - }); 174 - }; 48 + fetchOptions.method = 'POST'; 49 + fetchOptions.body = new FormData(); 50 + fetchOptions.body.append('operations', JSON.stringify(body)); 175 51 176 - const executeFetch = ( 177 - operation: Operation, 178 - opts: RequestInit, 179 - dispatchDebug: ExchangeInput['dispatchDebug'] 180 - ): Promise<OperationResult> => { 181 - const { url, fetch: fetcher } = operation.context; 182 - let statusNotOk = false; 183 - let response: Response; 52 + const map = {}; 53 + let i = 0; 54 + files.forEach(paths => { 55 + map[++i] = paths.map(path => `variables.${path}`); 56 + }); 184 57 185 - dispatchDebug({ 186 - type: 'fetchRequest', 187 - message: 'A fetch request is being executed.', 188 - operation, 189 - data: { 190 - url, 191 - fetchOptions: opts, 192 - }, 193 - }); 58 + fetchOptions.body.append('map', JSON.stringify(map)); 194 59 195 - return (fetcher || fetch)(url, opts) 196 - .then((res: Response) => { 197 - response = res; 198 - statusNotOk = 199 - res.status < 200 || 200 - res.status >= (opts.redirect === 'manual' ? 400 : 300); 201 - return res.json(); 202 - }) 203 - .then((result: any) => { 204 - if (!('data' in result) && !('errors' in result)) { 205 - throw new Error('No Content'); 60 + i = 0; 61 + files.forEach((_, file) => { 62 + (fetchOptions.body as FormData).append(`${++i}`, file, file.name); 63 + }); 64 + } else { 65 + fetchOptions = makeFetchOptions(operation, body); 66 + url = makeFetchURL(operation, body); 206 67 } 207 68 208 69 dispatchDebug({ 209 - type: result.errors && !result.data ? 'fetchError' : 'fetchSuccess', 210 - message: `A ${ 211 - result.errors ? 'failed' : 'successful' 212 - } fetch response has been returned.`, 70 + type: 'fetchRequest', 71 + message: 'A fetch request is being executed.', 213 72 operation, 214 73 data: { 215 74 url, 216 - fetchOptions: opts, 217 - value: result, 75 + fetchOptions, 218 76 }, 219 77 }); 220 78 221 - return makeResult(operation, result, response); 222 - }) 223 - .catch((error: Error) => { 224 - if (error.name !== 'AbortError') { 225 - dispatchDebug({ 226 - type: 'fetchError', 227 - message: error.name, 228 - operation, 229 - data: { 230 - url, 231 - fetchOptions: opts, 232 - value: error, 233 - }, 234 - }); 235 - 236 - return makeErrorResult( 237 - operation, 238 - statusNotOk ? new Error(response.statusText) : error, 239 - response 240 - ); 241 - } 242 - }); 243 - }; 79 + return pipe( 80 + makeFetchSource(operation, url, fetchOptions), 81 + takeUntil(teardown$), 82 + onPush(result => { 83 + const error = !result.data ? result.error : undefined; 244 84 245 - export const convertToGet = (uri: string, body: Body): string => { 246 - const queryParams: string[] = [`query=${encodeURIComponent(body.query)}`]; 85 + dispatchDebug({ 86 + type: error ? 'fetchError' : 'fetchSuccess', 87 + message: `A ${ 88 + error ? 'failed' : 'successful' 89 + } fetch response has been returned.`, 90 + operation, 91 + data: { 92 + url, 93 + fetchOptions, 94 + value: error || result, 95 + }, 96 + }); 97 + }) 98 + ); 99 + }) 100 + ); 247 101 248 - if (body.variables) { 249 - queryParams.push( 250 - `variables=${encodeURIComponent(JSON.stringify(body.variables))}` 251 - ); 252 - } 102 + const forward$ = pipe( 103 + sharedOps$, 104 + filter(operation => { 105 + return ( 106 + operation.operationName !== 'query' && 107 + operation.operationName !== 'mutation' 108 + ); 109 + }), 110 + forward 111 + ); 253 112 254 - return uri + '?' + queryParams.join('&'); 113 + return merge([fetchResults$, forward$]); 255 114 };
+1
exchanges/multipart-fetch/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1
exchanges/populate/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1
exchanges/retry/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1
exchanges/suspense/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+11
packages/core/internal/package.json
··· 1 + { 2 + "name": "urql-core-internal", 3 + "private": true, 4 + "main": "../dist/urql-core-internal.js", 5 + "module": "../dist/urql-core-internal.mjs", 6 + "types": "../dist/types/internal/index.d.ts", 7 + "source": "../src/internal/index.ts", 8 + "dependencies": { 9 + "wonka": "^4.0.9" 10 + } 11 + }
+6
packages/core/package.json
··· 28 28 "require": "./dist/urql-core.js", 29 29 "types": "./dist/types/index.d.ts", 30 30 "source": "./src/index.ts" 31 + }, 32 + "./internal": { 33 + "import": "./dist/urql-core-internal.mjs", 34 + "require": "./dist/urql-core-internal.js", 35 + "types": "./dist/types/internal/index.d.ts", 36 + "source": "./src/internal/index.ts" 31 37 } 32 38 }, 33 39 "files": [
+1 -1
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
··· 409 409 } 410 410 `; 411 411 412 - exports[`on success returns response data 2`] = `"{\\"query\\":\\"query getUser($name: String) {\\\\n user(name: $name) {\\\\n id\\\\n firstName\\\\n lastName\\\\n }\\\\n}\\\\n\\",\\"variables\\":{\\"name\\":\\"Clara\\"},\\"operationName\\":\\"getUser\\"}"`; 412 + exports[`on success returns response data 2`] = `"{\\"query\\":\\"query getUser($name: String) {\\\\n user(name: $name) {\\\\n id\\\\n firstName\\\\n lastName\\\\n }\\\\n}\\\\n\\",\\"operationName\\":\\"getUser\\",\\"variables\\":{\\"name\\":\\"Clara\\"}}"`;
+1 -39
packages/core/src/exchanges/fetch.test.ts
··· 1 1 import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka'; 2 - import gql from 'graphql-tag'; 3 - import { print } from 'graphql'; 4 2 5 3 import { Client } from '../client'; 6 4 import { queryOperation } from '../test-utils'; 7 5 import { OperationResult, OperationType } from '../types'; 8 - import { fetchExchange, convertToGet } from './fetch'; 6 + import { fetchExchange } from './fetch'; 9 7 10 8 const fetch = (global as any).fetch as jest.Mock; 11 9 const abort = jest.fn(); ··· 174 172 expect(abort).toHaveBeenCalledTimes(0); 175 173 }); 176 174 }); 177 - 178 - describe('convert for GET', () => { 179 - it('should do a basic conversion', () => { 180 - const query = `query ($id: ID!) { node(id: $id) { id } }`; 181 - const variables = { id: 2 }; 182 - expect(convertToGet('http://localhost:3000', { query, variables })).toBe( 183 - `http://localhost:3000?query=${encodeURIComponent( 184 - query 185 - )}&variables=${encodeURIComponent(JSON.stringify(variables))}` 186 - ); 187 - }); 188 - 189 - it('should do a basic conversion with fragments', () => { 190 - const nodeFragment = gql` 191 - fragment nodeFragment on Node { 192 - id 193 - } 194 - `; 195 - 196 - const variables = { id: 2 }; 197 - const query = print(gql` 198 - query($id: ID!) { 199 - node(id: $id) { 200 - ...nodeFragment 201 - } 202 - } 203 - ${nodeFragment} 204 - `); 205 - 206 - expect(convertToGet('http://localhost:3000', { query, variables })).toBe( 207 - `http://localhost:3000?query=${encodeURIComponent( 208 - query 209 - )}&variables=${encodeURIComponent(JSON.stringify(variables))}` 210 - ); 211 - }); 212 - });
+40 -184
packages/core/src/exchanges/fetch.ts
··· 1 1 /* eslint-disable @typescript-eslint/no-use-before-define */ 2 - import { Kind, DocumentNode, OperationDefinitionNode, print } from 'graphql'; 3 - import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka'; 4 - import { Exchange, Operation, OperationResult, ExchangeInput } from '../types'; 5 - import { makeResult, makeErrorResult } from '../utils'; 2 + import { filter, merge, mergeMap, pipe, share, takeUntil, onPush } from 'wonka'; 6 3 7 - interface Body { 8 - query: string; 9 - variables: void | object; 10 - operationName?: string; 11 - } 4 + import { Exchange } from '../types'; 5 + import { 6 + makeFetchBody, 7 + makeFetchURL, 8 + makeFetchOptions, 9 + makeFetchSource, 10 + } from '../internal'; 12 11 13 12 /** A default exchange for fetching GraphQL requests. */ 14 13 export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => { ··· 29 28 filter(op => op.operationName === 'teardown' && op.key === key) 30 29 ); 31 30 31 + const body = makeFetchBody(operation); 32 + const url = makeFetchURL(operation, body); 33 + const fetchOptions = makeFetchOptions(operation, body); 34 + 35 + dispatchDebug({ 36 + type: 'fetchRequest', 37 + message: 'A fetch request is being executed.', 38 + operation, 39 + data: { 40 + url, 41 + fetchOptions, 42 + }, 43 + }); 44 + 32 45 return pipe( 33 - createFetchSource( 34 - operation, 35 - operation.operationName === 'query' && 36 - !!operation.context.preferGetMethod, 37 - dispatchDebug 38 - ), 39 - takeUntil(teardown$) 46 + makeFetchSource(operation, url, fetchOptions), 47 + takeUntil(teardown$), 48 + onPush(result => { 49 + const error = !result.data ? result.error : undefined; 50 + 51 + dispatchDebug({ 52 + type: error ? 'fetchError' : 'fetchSuccess', 53 + message: `A ${ 54 + error ? 'failed' : 'successful' 55 + } fetch response has been returned.`, 56 + operation, 57 + data: { 58 + url, 59 + fetchOptions, 60 + value: error || result, 61 + }, 62 + }); 63 + }) 40 64 ); 41 65 }) 42 66 ); ··· 55 79 return merge([fetchResults$, forward$]); 56 80 }; 57 81 }; 58 - 59 - const getOperationName = (query: DocumentNode): string | null => { 60 - const node = query.definitions.find( 61 - (node: any): node is OperationDefinitionNode => { 62 - return node.kind === Kind.OPERATION_DEFINITION && node.name; 63 - } 64 - ); 65 - 66 - return node ? node.name!.value : null; 67 - }; 68 - 69 - const createFetchSource = ( 70 - operation: Operation, 71 - shouldUseGet: boolean, 72 - dispatchDebug: ExchangeInput['dispatchDebug'] 73 - ) => { 74 - if ( 75 - process.env.NODE_ENV !== 'production' && 76 - operation.operationName === 'subscription' 77 - ) { 78 - throw new Error( 79 - 'Received a subscription operation in the httpExchange. You are probably trying to create a subscription. Have you added a subscriptionExchange?' 80 - ); 81 - } 82 - 83 - return make<OperationResult>(({ next, complete }) => { 84 - const abortController = 85 - typeof AbortController !== 'undefined' 86 - ? new AbortController() 87 - : undefined; 88 - 89 - const { context } = operation; 90 - 91 - const extraOptions = 92 - typeof context.fetchOptions === 'function' 93 - ? context.fetchOptions() 94 - : context.fetchOptions || {}; 95 - 96 - const operationName = getOperationName(operation.query); 97 - 98 - const body: Body = { 99 - query: print(operation.query), 100 - variables: operation.variables, 101 - }; 102 - 103 - if (operationName !== null) { 104 - body.operationName = operationName; 105 - } 106 - 107 - const fetchOptions = { 108 - ...extraOptions, 109 - body: shouldUseGet ? undefined : JSON.stringify(body), 110 - method: shouldUseGet ? 'GET' : 'POST', 111 - headers: { 112 - 'content-type': 'application/json', 113 - ...extraOptions.headers, 114 - }, 115 - signal: 116 - abortController !== undefined ? abortController.signal : undefined, 117 - }; 118 - 119 - if (shouldUseGet) { 120 - operation.context.url = convertToGet(operation.context.url, body); 121 - } 122 - 123 - let ended = false; 124 - 125 - Promise.resolve() 126 - .then(() => 127 - ended ? undefined : executeFetch(operation, fetchOptions, dispatchDebug) 128 - ) 129 - .then((result: OperationResult | undefined) => { 130 - if (!ended) { 131 - ended = true; 132 - if (result) next(result); 133 - complete(); 134 - } 135 - }); 136 - 137 - return () => { 138 - ended = true; 139 - if (abortController !== undefined) { 140 - abortController.abort(); 141 - } 142 - }; 143 - }); 144 - }; 145 - 146 - const executeFetch = ( 147 - operation: Operation, 148 - opts: RequestInit, 149 - dispatchDebug: ExchangeInput['dispatchDebug'] 150 - ): Promise<OperationResult> => { 151 - const { url, fetch: fetcher } = operation.context; 152 - let statusNotOk = false; 153 - let response: Response; 154 - 155 - dispatchDebug({ 156 - type: 'fetchRequest', 157 - message: 'A fetch request is being executed.', 158 - operation, 159 - data: { 160 - url, 161 - fetchOptions: opts, 162 - }, 163 - }); 164 - 165 - return (fetcher || fetch)(url, opts) 166 - .then((res: Response) => { 167 - response = res; 168 - statusNotOk = 169 - res.status < 200 || 170 - res.status >= (opts.redirect === 'manual' ? 400 : 300); 171 - return res.json(); 172 - }) 173 - .then((result: any) => { 174 - if (!('data' in result) && !('errors' in result)) { 175 - throw new Error('No Content'); 176 - } 177 - 178 - dispatchDebug({ 179 - type: result.errors && !result.data ? 'fetchError' : 'fetchSuccess', 180 - message: `A ${ 181 - result.errors ? 'failed' : 'successful' 182 - } fetch response has been returned.`, 183 - operation, 184 - data: { 185 - url, 186 - fetchOptions: opts, 187 - value: result, 188 - }, 189 - }); 190 - 191 - return makeResult(operation, result, response); 192 - }) 193 - .catch((error: Error) => { 194 - if (error.name !== 'AbortError') { 195 - dispatchDebug({ 196 - type: 'fetchError', 197 - message: error.name, 198 - operation, 199 - data: { 200 - url, 201 - fetchOptions: opts, 202 - value: error, 203 - }, 204 - }); 205 - 206 - return makeErrorResult( 207 - operation, 208 - statusNotOk ? new Error(response.statusText) : error, 209 - response 210 - ); 211 - } 212 - }); 213 - }; 214 - 215 - export const convertToGet = (uri: string, body: Body): string => { 216 - const queryParams: string[] = [`query=${encodeURIComponent(body.query)}`]; 217 - 218 - if (body.variables) { 219 - queryParams.push( 220 - `variables=${encodeURIComponent(JSON.stringify(body.variables))}` 221 - ); 222 - } 223 - 224 - return uri + '?' + queryParams.join('&'); 225 - };
+670
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
··· 1 + // Jest Snapshot v1, https://goo.gl/fbAQLP 2 + 3 + exports[`on error ignores the error when a result is available 1`] = ` 4 + Object { 5 + "data": undefined, 6 + "error": [CombinedError: [Network] ], 7 + "extensions": undefined, 8 + "operation": Object { 9 + "context": Object { 10 + "fetchOptions": Object { 11 + "method": "POST", 12 + }, 13 + "requestPolicy": "cache-first", 14 + "url": "http://localhost:3000/graphql", 15 + }, 16 + "key": 2, 17 + "operationName": "query", 18 + "query": Object { 19 + "definitions": Array [ 20 + Object { 21 + "directives": Array [], 22 + "kind": "OperationDefinition", 23 + "name": Object { 24 + "kind": "Name", 25 + "value": "getUser", 26 + }, 27 + "operation": "query", 28 + "selectionSet": Object { 29 + "kind": "SelectionSet", 30 + "selections": Array [ 31 + Object { 32 + "alias": undefined, 33 + "arguments": Array [ 34 + Object { 35 + "kind": "Argument", 36 + "name": Object { 37 + "kind": "Name", 38 + "value": "name", 39 + }, 40 + "value": Object { 41 + "kind": "Variable", 42 + "name": Object { 43 + "kind": "Name", 44 + "value": "name", 45 + }, 46 + }, 47 + }, 48 + ], 49 + "directives": Array [], 50 + "kind": "Field", 51 + "name": Object { 52 + "kind": "Name", 53 + "value": "user", 54 + }, 55 + "selectionSet": Object { 56 + "kind": "SelectionSet", 57 + "selections": Array [ 58 + Object { 59 + "alias": undefined, 60 + "arguments": Array [], 61 + "directives": Array [], 62 + "kind": "Field", 63 + "name": Object { 64 + "kind": "Name", 65 + "value": "id", 66 + }, 67 + "selectionSet": undefined, 68 + }, 69 + Object { 70 + "alias": undefined, 71 + "arguments": Array [], 72 + "directives": Array [], 73 + "kind": "Field", 74 + "name": Object { 75 + "kind": "Name", 76 + "value": "firstName", 77 + }, 78 + "selectionSet": undefined, 79 + }, 80 + Object { 81 + "alias": undefined, 82 + "arguments": Array [], 83 + "directives": Array [], 84 + "kind": "Field", 85 + "name": Object { 86 + "kind": "Name", 87 + "value": "lastName", 88 + }, 89 + "selectionSet": undefined, 90 + }, 91 + ], 92 + }, 93 + }, 94 + ], 95 + }, 96 + "variableDefinitions": Array [ 97 + Object { 98 + "defaultValue": undefined, 99 + "directives": Array [], 100 + "kind": "VariableDefinition", 101 + "type": Object { 102 + "kind": "NamedType", 103 + "name": Object { 104 + "kind": "Name", 105 + "value": "String", 106 + }, 107 + }, 108 + "variable": Object { 109 + "kind": "Variable", 110 + "name": Object { 111 + "kind": "Name", 112 + "value": "name", 113 + }, 114 + }, 115 + }, 116 + ], 117 + }, 118 + ], 119 + "kind": "Document", 120 + "loc": Object { 121 + "end": 124, 122 + "start": 0, 123 + }, 124 + }, 125 + "variables": Object { 126 + "name": "Clara", 127 + }, 128 + }, 129 + } 130 + `; 131 + 132 + exports[`on error returns error data 1`] = ` 133 + Object { 134 + "data": undefined, 135 + "error": [CombinedError: [Network] ], 136 + "extensions": undefined, 137 + "operation": Object { 138 + "context": Object { 139 + "fetchOptions": Object { 140 + "method": "POST", 141 + }, 142 + "requestPolicy": "cache-first", 143 + "url": "http://localhost:3000/graphql", 144 + }, 145 + "key": 2, 146 + "operationName": "query", 147 + "query": Object { 148 + "definitions": Array [ 149 + Object { 150 + "directives": Array [], 151 + "kind": "OperationDefinition", 152 + "name": Object { 153 + "kind": "Name", 154 + "value": "getUser", 155 + }, 156 + "operation": "query", 157 + "selectionSet": Object { 158 + "kind": "SelectionSet", 159 + "selections": Array [ 160 + Object { 161 + "alias": undefined, 162 + "arguments": Array [ 163 + Object { 164 + "kind": "Argument", 165 + "name": Object { 166 + "kind": "Name", 167 + "value": "name", 168 + }, 169 + "value": Object { 170 + "kind": "Variable", 171 + "name": Object { 172 + "kind": "Name", 173 + "value": "name", 174 + }, 175 + }, 176 + }, 177 + ], 178 + "directives": Array [], 179 + "kind": "Field", 180 + "name": Object { 181 + "kind": "Name", 182 + "value": "user", 183 + }, 184 + "selectionSet": Object { 185 + "kind": "SelectionSet", 186 + "selections": Array [ 187 + Object { 188 + "alias": undefined, 189 + "arguments": Array [], 190 + "directives": Array [], 191 + "kind": "Field", 192 + "name": Object { 193 + "kind": "Name", 194 + "value": "id", 195 + }, 196 + "selectionSet": undefined, 197 + }, 198 + Object { 199 + "alias": undefined, 200 + "arguments": Array [], 201 + "directives": Array [], 202 + "kind": "Field", 203 + "name": Object { 204 + "kind": "Name", 205 + "value": "firstName", 206 + }, 207 + "selectionSet": undefined, 208 + }, 209 + Object { 210 + "alias": undefined, 211 + "arguments": Array [], 212 + "directives": Array [], 213 + "kind": "Field", 214 + "name": Object { 215 + "kind": "Name", 216 + "value": "lastName", 217 + }, 218 + "selectionSet": undefined, 219 + }, 220 + ], 221 + }, 222 + }, 223 + ], 224 + }, 225 + "variableDefinitions": Array [ 226 + Object { 227 + "defaultValue": undefined, 228 + "directives": Array [], 229 + "kind": "VariableDefinition", 230 + "type": Object { 231 + "kind": "NamedType", 232 + "name": Object { 233 + "kind": "Name", 234 + "value": "String", 235 + }, 236 + }, 237 + "variable": Object { 238 + "kind": "Variable", 239 + "name": Object { 240 + "kind": "Name", 241 + "value": "name", 242 + }, 243 + }, 244 + }, 245 + ], 246 + }, 247 + ], 248 + "kind": "Document", 249 + "loc": Object { 250 + "end": 124, 251 + "start": 0, 252 + }, 253 + }, 254 + "variables": Object { 255 + "name": "Clara", 256 + }, 257 + }, 258 + } 259 + `; 260 + 261 + exports[`on error returns error data with status 400 and manual redirect mode 1`] = ` 262 + Object { 263 + "data": undefined, 264 + "error": [CombinedError: [Network] ], 265 + "extensions": undefined, 266 + "operation": Object { 267 + "context": Object { 268 + "fetchOptions": Object { 269 + "method": "POST", 270 + }, 271 + "requestPolicy": "cache-first", 272 + "url": "http://localhost:3000/graphql", 273 + }, 274 + "key": 2, 275 + "operationName": "query", 276 + "query": Object { 277 + "definitions": Array [ 278 + Object { 279 + "directives": Array [], 280 + "kind": "OperationDefinition", 281 + "name": Object { 282 + "kind": "Name", 283 + "value": "getUser", 284 + }, 285 + "operation": "query", 286 + "selectionSet": Object { 287 + "kind": "SelectionSet", 288 + "selections": Array [ 289 + Object { 290 + "alias": undefined, 291 + "arguments": Array [ 292 + Object { 293 + "kind": "Argument", 294 + "name": Object { 295 + "kind": "Name", 296 + "value": "name", 297 + }, 298 + "value": Object { 299 + "kind": "Variable", 300 + "name": Object { 301 + "kind": "Name", 302 + "value": "name", 303 + }, 304 + }, 305 + }, 306 + ], 307 + "directives": Array [], 308 + "kind": "Field", 309 + "name": Object { 310 + "kind": "Name", 311 + "value": "user", 312 + }, 313 + "selectionSet": Object { 314 + "kind": "SelectionSet", 315 + "selections": Array [ 316 + Object { 317 + "alias": undefined, 318 + "arguments": Array [], 319 + "directives": Array [], 320 + "kind": "Field", 321 + "name": Object { 322 + "kind": "Name", 323 + "value": "id", 324 + }, 325 + "selectionSet": undefined, 326 + }, 327 + Object { 328 + "alias": undefined, 329 + "arguments": Array [], 330 + "directives": Array [], 331 + "kind": "Field", 332 + "name": Object { 333 + "kind": "Name", 334 + "value": "firstName", 335 + }, 336 + "selectionSet": undefined, 337 + }, 338 + Object { 339 + "alias": undefined, 340 + "arguments": Array [], 341 + "directives": Array [], 342 + "kind": "Field", 343 + "name": Object { 344 + "kind": "Name", 345 + "value": "lastName", 346 + }, 347 + "selectionSet": undefined, 348 + }, 349 + ], 350 + }, 351 + }, 352 + ], 353 + }, 354 + "variableDefinitions": Array [ 355 + Object { 356 + "defaultValue": undefined, 357 + "directives": Array [], 358 + "kind": "VariableDefinition", 359 + "type": Object { 360 + "kind": "NamedType", 361 + "name": Object { 362 + "kind": "Name", 363 + "value": "String", 364 + }, 365 + }, 366 + "variable": Object { 367 + "kind": "Variable", 368 + "name": Object { 369 + "kind": "Name", 370 + "value": "name", 371 + }, 372 + }, 373 + }, 374 + ], 375 + }, 376 + ], 377 + "kind": "Document", 378 + "loc": Object { 379 + "end": 124, 380 + "start": 0, 381 + }, 382 + }, 383 + "variables": Object { 384 + "name": "Clara", 385 + }, 386 + }, 387 + } 388 + `; 389 + 390 + exports[`on success returns response data 1`] = ` 391 + Object { 392 + "data": Object { 393 + "data": Object { 394 + "user": 1200, 395 + }, 396 + }, 397 + "error": undefined, 398 + "extensions": undefined, 399 + "operation": Object { 400 + "context": Object { 401 + "fetchOptions": Object { 402 + "method": "POST", 403 + }, 404 + "requestPolicy": "cache-first", 405 + "url": "http://localhost:3000/graphql", 406 + }, 407 + "key": 2, 408 + "operationName": "query", 409 + "query": Object { 410 + "definitions": Array [ 411 + Object { 412 + "directives": Array [], 413 + "kind": "OperationDefinition", 414 + "name": Object { 415 + "kind": "Name", 416 + "value": "getUser", 417 + }, 418 + "operation": "query", 419 + "selectionSet": Object { 420 + "kind": "SelectionSet", 421 + "selections": Array [ 422 + Object { 423 + "alias": undefined, 424 + "arguments": Array [ 425 + Object { 426 + "kind": "Argument", 427 + "name": Object { 428 + "kind": "Name", 429 + "value": "name", 430 + }, 431 + "value": Object { 432 + "kind": "Variable", 433 + "name": Object { 434 + "kind": "Name", 435 + "value": "name", 436 + }, 437 + }, 438 + }, 439 + ], 440 + "directives": Array [], 441 + "kind": "Field", 442 + "name": Object { 443 + "kind": "Name", 444 + "value": "user", 445 + }, 446 + "selectionSet": Object { 447 + "kind": "SelectionSet", 448 + "selections": Array [ 449 + Object { 450 + "alias": undefined, 451 + "arguments": Array [], 452 + "directives": Array [], 453 + "kind": "Field", 454 + "name": Object { 455 + "kind": "Name", 456 + "value": "id", 457 + }, 458 + "selectionSet": undefined, 459 + }, 460 + Object { 461 + "alias": undefined, 462 + "arguments": Array [], 463 + "directives": Array [], 464 + "kind": "Field", 465 + "name": Object { 466 + "kind": "Name", 467 + "value": "firstName", 468 + }, 469 + "selectionSet": undefined, 470 + }, 471 + Object { 472 + "alias": undefined, 473 + "arguments": Array [], 474 + "directives": Array [], 475 + "kind": "Field", 476 + "name": Object { 477 + "kind": "Name", 478 + "value": "lastName", 479 + }, 480 + "selectionSet": undefined, 481 + }, 482 + ], 483 + }, 484 + }, 485 + ], 486 + }, 487 + "variableDefinitions": Array [ 488 + Object { 489 + "defaultValue": undefined, 490 + "directives": Array [], 491 + "kind": "VariableDefinition", 492 + "type": Object { 493 + "kind": "NamedType", 494 + "name": Object { 495 + "kind": "Name", 496 + "value": "String", 497 + }, 498 + }, 499 + "variable": Object { 500 + "kind": "Variable", 501 + "name": Object { 502 + "kind": "Name", 503 + "value": "name", 504 + }, 505 + }, 506 + }, 507 + ], 508 + }, 509 + ], 510 + "kind": "Document", 511 + "loc": Object { 512 + "end": 124, 513 + "start": 0, 514 + }, 515 + }, 516 + "variables": Object { 517 + "name": "Clara", 518 + }, 519 + }, 520 + } 521 + `; 522 + 523 + exports[`on success uses the mock fetch if given 1`] = ` 524 + Object { 525 + "data": Object { 526 + "data": Object { 527 + "user": 1200, 528 + }, 529 + }, 530 + "error": undefined, 531 + "extensions": undefined, 532 + "operation": Object { 533 + "context": Object { 534 + "fetch": [MockFunction] { 535 + "calls": Array [ 536 + Array [ 537 + "https://test.com/graphql", 538 + Object { 539 + "signal": undefined, 540 + }, 541 + ], 542 + ], 543 + "results": Array [ 544 + Object { 545 + "type": "return", 546 + "value": Promise {}, 547 + }, 548 + ], 549 + }, 550 + "fetchOptions": Object { 551 + "method": "POST", 552 + }, 553 + "requestPolicy": "cache-first", 554 + "url": "http://localhost:3000/graphql", 555 + }, 556 + "key": 2, 557 + "operationName": "query", 558 + "query": Object { 559 + "definitions": Array [ 560 + Object { 561 + "directives": Array [], 562 + "kind": "OperationDefinition", 563 + "name": Object { 564 + "kind": "Name", 565 + "value": "getUser", 566 + }, 567 + "operation": "query", 568 + "selectionSet": Object { 569 + "kind": "SelectionSet", 570 + "selections": Array [ 571 + Object { 572 + "alias": undefined, 573 + "arguments": Array [ 574 + Object { 575 + "kind": "Argument", 576 + "name": Object { 577 + "kind": "Name", 578 + "value": "name", 579 + }, 580 + "value": Object { 581 + "kind": "Variable", 582 + "name": Object { 583 + "kind": "Name", 584 + "value": "name", 585 + }, 586 + }, 587 + }, 588 + ], 589 + "directives": Array [], 590 + "kind": "Field", 591 + "name": Object { 592 + "kind": "Name", 593 + "value": "user", 594 + }, 595 + "selectionSet": Object { 596 + "kind": "SelectionSet", 597 + "selections": Array [ 598 + Object { 599 + "alias": undefined, 600 + "arguments": Array [], 601 + "directives": Array [], 602 + "kind": "Field", 603 + "name": Object { 604 + "kind": "Name", 605 + "value": "id", 606 + }, 607 + "selectionSet": undefined, 608 + }, 609 + Object { 610 + "alias": undefined, 611 + "arguments": Array [], 612 + "directives": Array [], 613 + "kind": "Field", 614 + "name": Object { 615 + "kind": "Name", 616 + "value": "firstName", 617 + }, 618 + "selectionSet": undefined, 619 + }, 620 + Object { 621 + "alias": undefined, 622 + "arguments": Array [], 623 + "directives": Array [], 624 + "kind": "Field", 625 + "name": Object { 626 + "kind": "Name", 627 + "value": "lastName", 628 + }, 629 + "selectionSet": undefined, 630 + }, 631 + ], 632 + }, 633 + }, 634 + ], 635 + }, 636 + "variableDefinitions": Array [ 637 + Object { 638 + "defaultValue": undefined, 639 + "directives": Array [], 640 + "kind": "VariableDefinition", 641 + "type": Object { 642 + "kind": "NamedType", 643 + "name": Object { 644 + "kind": "Name", 645 + "value": "String", 646 + }, 647 + }, 648 + "variable": Object { 649 + "kind": "Variable", 650 + "name": Object { 651 + "kind": "Name", 652 + "value": "name", 653 + }, 654 + }, 655 + }, 656 + ], 657 + }, 658 + ], 659 + "kind": "Document", 660 + "loc": Object { 661 + "end": 124, 662 + "start": 0, 663 + }, 664 + }, 665 + "variables": Object { 666 + "name": "Clara", 667 + }, 668 + }, 669 + } 670 + `;
+83
packages/core/src/internal/fetchOptions.ts
··· 1 + import { Kind, print, DocumentNode } from 'graphql'; 2 + 3 + import { stringifyVariables } from '../utils'; 4 + import { Operation } from '../types'; 5 + 6 + export interface FetchBody { 7 + query: string; 8 + operationName: string | undefined; 9 + variables: undefined | Record<string, any>; 10 + extensions: undefined | Record<string, any>; 11 + } 12 + 13 + const getOperationName = (query: DocumentNode): string | undefined => { 14 + for (let i = 0, l = query.definitions.length; i < l; i++) { 15 + const node = query.definitions[i]; 16 + if (node.kind === Kind.OPERATION_DEFINITION && node.name) { 17 + return node.name.value; 18 + } 19 + } 20 + }; 21 + 22 + const shouldUseGet = (operation: Operation): boolean => { 23 + return ( 24 + operation.operationName === 'query' && !!operation.context.preferGetMethod 25 + ); 26 + }; 27 + 28 + export const makeFetchBody = (request: { 29 + query: DocumentNode; 30 + variables?: object; 31 + }): FetchBody => ({ 32 + query: print(request.query), 33 + operationName: getOperationName(request.query), 34 + variables: request.variables || undefined, 35 + extensions: undefined, 36 + }); 37 + 38 + export const makeFetchURL = ( 39 + operation: Operation, 40 + body?: FetchBody 41 + ): string => { 42 + const useGETMethod = shouldUseGet(operation); 43 + let url = operation.context.url; 44 + if (!useGETMethod || !body) return url; 45 + 46 + url += `?query=${encodeURIComponent(body.query)}`; 47 + 48 + if (body.variables) { 49 + url += `&variables=${encodeURIComponent( 50 + stringifyVariables(body.variables) 51 + )}`; 52 + } 53 + 54 + if (body.extensions) { 55 + url += `&extensions=${encodeURIComponent( 56 + stringifyVariables(body.extensions) 57 + )}`; 58 + } 59 + 60 + return url; 61 + }; 62 + 63 + export const makeFetchOptions = ( 64 + operation: Operation, 65 + body?: FetchBody 66 + ): RequestInit => { 67 + const useGETMethod = shouldUseGet(operation); 68 + 69 + const extraOptions = 70 + typeof operation.context.fetchOptions === 'function' 71 + ? operation.context.fetchOptions() 72 + : operation.context.fetchOptions || {}; 73 + 74 + return { 75 + ...extraOptions, 76 + body: !useGETMethod && body ? JSON.stringify(body) : undefined, 77 + method: useGETMethod ? 'GET' : 'POST', 78 + headers: { 79 + 'content-type': 'application/json', 80 + ...extraOptions.headers, 81 + }, 82 + }; 83 + };
+154
packages/core/src/internal/fetchSource.test.ts
··· 1 + import { pipe, subscribe, toPromise } from 'wonka'; 2 + 3 + import { queryOperation } from '../test-utils'; 4 + import { makeFetchSource } from './fetchSource'; 5 + 6 + const fetch = (global as any).fetch as jest.Mock; 7 + const abort = jest.fn(); 8 + 9 + const abortError = new Error(); 10 + abortError.name = 'AbortError'; 11 + 12 + beforeAll(() => { 13 + (global as any).AbortController = function AbortController() { 14 + this.signal = undefined; 15 + this.abort = abort; 16 + }; 17 + }); 18 + 19 + afterEach(() => { 20 + fetch.mockClear(); 21 + abort.mockClear(); 22 + }); 23 + 24 + afterAll(() => { 25 + (global as any).AbortController = undefined; 26 + }); 27 + 28 + const response = { 29 + status: 200, 30 + data: { 31 + data: { 32 + user: 1200, 33 + }, 34 + }, 35 + }; 36 + 37 + describe('on success', () => { 38 + beforeEach(() => { 39 + fetch.mockResolvedValue({ 40 + status: 200, 41 + json: jest.fn().mockResolvedValue(response), 42 + }); 43 + }); 44 + 45 + it('returns response data', async () => { 46 + const fetchOptions = {}; 47 + const data = await pipe( 48 + makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions), 49 + toPromise 50 + ); 51 + 52 + expect(data).toMatchSnapshot(); 53 + 54 + expect(fetch).toHaveBeenCalled(); 55 + expect(fetch.mock.calls[0][0]).toBe('https://test.com/graphql'); 56 + expect(fetch.mock.calls[0][1]).toBe(fetchOptions); 57 + }); 58 + 59 + it('uses the mock fetch if given', async () => { 60 + const fetchOptions = {}; 61 + const fetcher = jest.fn().mockResolvedValue({ 62 + status: 200, 63 + json: jest.fn().mockResolvedValue(response), 64 + }); 65 + 66 + const data = await pipe( 67 + makeFetchSource( 68 + { 69 + ...queryOperation, 70 + context: { 71 + ...queryOperation.context, 72 + fetch: fetcher, 73 + }, 74 + }, 75 + 'https://test.com/graphql', 76 + fetchOptions 77 + ), 78 + toPromise 79 + ); 80 + 81 + expect(data).toMatchSnapshot(); 82 + expect(fetch).not.toHaveBeenCalled(); 83 + expect(fetcher).toHaveBeenCalled(); 84 + }); 85 + }); 86 + 87 + describe('on error', () => { 88 + beforeEach(() => { 89 + fetch.mockResolvedValue({ 90 + status: 400, 91 + json: jest.fn().mockResolvedValue({}), 92 + }); 93 + }); 94 + 95 + it('returns error data', async () => { 96 + const fetchOptions = {}; 97 + const data = await pipe( 98 + makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions), 99 + toPromise 100 + ); 101 + 102 + expect(data).toMatchSnapshot(); 103 + }); 104 + 105 + it('returns error data with status 400 and manual redirect mode', async () => { 106 + const data = await pipe( 107 + makeFetchSource(queryOperation, 'https://test.com/graphql', { 108 + redirect: 'manual', 109 + }), 110 + toPromise 111 + ); 112 + 113 + expect(data).toMatchSnapshot(); 114 + }); 115 + 116 + it('ignores the error when a result is available', async () => { 117 + const data = await pipe( 118 + makeFetchSource(queryOperation, 'https://test.com/graphql', {}), 119 + toPromise 120 + ); 121 + 122 + expect(data).toMatchSnapshot(); 123 + }); 124 + }); 125 + 126 + describe('on teardown', () => { 127 + it('does not start the outgoing request on immediate teardowns', () => { 128 + fetch.mockRejectedValueOnce(abortError); 129 + 130 + const { unsubscribe } = pipe( 131 + makeFetchSource(queryOperation, 'https://test.com/graphql', {}), 132 + subscribe(fail) 133 + ); 134 + 135 + unsubscribe(); 136 + expect(fetch).toHaveBeenCalledTimes(0); 137 + expect(abort).toHaveBeenCalledTimes(1); 138 + }); 139 + 140 + it('aborts the outgoing request', async () => { 141 + fetch.mockRejectedValueOnce(abortError); 142 + 143 + const { unsubscribe } = pipe( 144 + makeFetchSource(queryOperation, 'https://test.com/graphql', {}), 145 + subscribe(fail) 146 + ); 147 + 148 + await Promise.resolve(); 149 + 150 + unsubscribe(); 151 + expect(fetch).toHaveBeenCalledTimes(1); 152 + expect(abort).toHaveBeenCalledTimes(1); 153 + }); 154 + });
+77
packages/core/src/internal/fetchSource.ts
··· 1 + import { Operation, OperationResult } from '../types'; 2 + import { makeResult, makeErrorResult } from '../utils'; 3 + import { make } from 'wonka'; 4 + 5 + const executeFetch = ( 6 + operation: Operation, 7 + url: string, 8 + fetchOptions: RequestInit 9 + ): Promise<OperationResult> => { 10 + const fetcher = operation.context.fetch; 11 + 12 + let statusNotOk = false; 13 + let response: Response; 14 + 15 + return (fetcher || fetch)(url, fetchOptions) 16 + .then((res: Response) => { 17 + response = res; 18 + statusNotOk = 19 + res.status < 200 || 20 + res.status >= (fetchOptions.redirect === 'manual' ? 400 : 300); 21 + return res.json(); 22 + }) 23 + .then((result: any) => { 24 + if (!('data' in result) && !('errors' in result)) { 25 + throw new Error('No Content'); 26 + } 27 + 28 + return makeResult(operation, result, response); 29 + }) 30 + .catch((error: Error) => { 31 + if (error.name !== 'AbortError') { 32 + return makeErrorResult( 33 + operation, 34 + statusNotOk ? new Error(response.statusText) : error, 35 + response 36 + ); 37 + } 38 + }) as Promise<OperationResult>; 39 + }; 40 + 41 + export const makeFetchSource = ( 42 + operation: Operation, 43 + url: string, 44 + fetchOptions: RequestInit 45 + ) => { 46 + return make<OperationResult>(({ next, complete }) => { 47 + const abortController = 48 + typeof AbortController !== 'undefined' ? new AbortController() : null; 49 + 50 + let ended = false; 51 + 52 + Promise.resolve() 53 + .then(() => { 54 + if (ended) { 55 + return; 56 + } else if (abortController) { 57 + fetchOptions.signal = abortController.signal; 58 + } 59 + 60 + return executeFetch(operation, url, fetchOptions); 61 + }) 62 + .then((result: OperationResult | undefined) => { 63 + if (!ended) { 64 + ended = true; 65 + if (result) next(result); 66 + complete(); 67 + } 68 + }); 69 + 70 + return () => { 71 + ended = true; 72 + if (abortController) { 73 + abortController.abort(); 74 + } 75 + }; 76 + }); 77 + };
+2
packages/core/src/internal/index.ts
··· 1 + export * from './fetchOptions'; 2 + export * from './fetchSource';
+1
packages/core/src/types.ts
··· 42 42 export interface OperationContext { 43 43 [key: string]: any; 44 44 additionalTypenames?: string[]; 45 + fetch?: typeof fetch; 45 46 fetchOptions?: RequestInit | (() => RequestInit); 46 47 requestPolicy: RequestPolicy; 47 48 url: string;
+1
packages/core/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1
packages/preact-urql/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1
packages/react-urql/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1
packages/svelte-urql/tsconfig.json
··· 6 6 "paths": { 7 7 "urql": ["../../node_modules/urql/src"], 8 8 "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 9 10 "@urql/*": ["../../node_modules/@urql/*/src"] 10 11 } 11 12 }
+1
scripts/jest/preset.js
··· 9 9 moduleNameMapper: { 10 10 "^urql$": "<rootDir>/../../node_modules/urql/src", 11 11 "^(.*-urql)$": "<rootDir>/../../node_modules/$1/src", 12 + "^@urql/(.*)/(.*)$": "<rootDir>/../../node_modules/@urql/$1/src/$2", 12 13 "^@urql/(.*)$": "<rootDir>/../../node_modules/@urql/$1/src", 13 14 }, 14 15 watchPlugins: ['jest-watch-yarn-workspaces'],
+1
tsconfig.json
··· 5 5 "urql": ["packages/react-urql/src"], 6 6 "*-urql": ["packages/*-urql/src"], 7 7 "@urql/exchange-*": ["exchanges/*/src"], 8 + "@urql/core/*": ["packages/core/src/*"], 8 9 "@urql/*": ["packages/*-urql/src", "packages/*/src"] 9 10 }, 10 11 "esModuleInterop": true,