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

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

+6
.changeset/wet-rabbits-tie.md
···
+
---
+
'@urql/core': minor
+
'@urql/exchange-multipart-fetch': patch
+
---
+
+
Add @urql/core/internal entrypoint for internally shared utilities and start sharing fetchExchange-related code.
+1
exchanges/graphcache/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1 -1
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
···
}
`;
-
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\\"}"`;
+
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\\"}}"`;
exports[`on success uses a file when given 1`] = `
Object {
+2 -39
exchanges/multipart-fetch/src/multipartFetchExchange.test.ts
···
import { Client, OperationResult, OperationType } from '@urql/core';
import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka';
-
import gql from 'graphql-tag';
-
import { print } from 'graphql';
+
+
import { multipartFetchExchange } from './multipartFetchExchange';
-
import { multipartFetchExchange, convertToGet } from './multipartFetchExchange';
import {
uploadOperation,
queryOperation,
···
expect(abort).toHaveBeenCalledTimes(0);
});
});
-
-
describe('convert for GET', () => {
-
it('should do a basic conversion', () => {
-
const query = `query ($id: ID!) { node(id: $id) { id } }`;
-
const variables = { id: 2 };
-
expect(convertToGet('http://localhost:3000', { query, variables })).toBe(
-
`http://localhost:3000?query=${encodeURIComponent(
-
query
-
)}&variables=${encodeURIComponent(JSON.stringify(variables))}`
-
);
-
});
-
-
it('should do a basic conversion with fragments', () => {
-
const nodeFragment = gql`
-
fragment nodeFragment on Node {
-
id
-
}
-
`;
-
-
const variables = { id: 2 };
-
const query = print(gql`
-
query($id: ID!) {
-
node(id: $id) {
-
...nodeFragment
-
}
-
}
-
${nodeFragment}
-
`);
-
-
expect(convertToGet('http://localhost:3000', { query, variables })).toBe(
-
`http://localhost:3000?query=${encodeURIComponent(
-
query
-
)}&variables=${encodeURIComponent(JSON.stringify(variables))}`
-
);
-
});
-
});
+75 -216
exchanges/multipart-fetch/src/multipartFetchExchange.ts
···
-
import { Kind, DocumentNode, OperationDefinitionNode, print } from 'graphql';
-
import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka';
+
import { filter, merge, mergeMap, pipe, share, takeUntil, onPush } from 'wonka';
import { extractFiles } from 'extract-files';
+
import { Exchange } from '@urql/core';
import {
-
ExchangeInput,
-
Exchange,
-
Operation,
-
OperationResult,
-
makeResult,
-
makeErrorResult,
-
} from '@urql/core';
-
-
interface Body {
-
query: string;
-
variables: void | object;
-
operationName?: string;
-
}
-
-
const isOperationFetchable = (operation: Operation) =>
-
operation.operationName === 'query' || operation.operationName === 'mutation';
+
makeFetchBody,
+
makeFetchURL,
+
makeFetchOptions,
+
makeFetchSource,
+
} from '@urql/core/internal';
export const multipartFetchExchange: Exchange = ({
forward,
dispatchDebug,
}) => ops$ => {
const sharedOps$ = share(ops$);
-
const fetchResults$ = pipe(
sharedOps$,
-
filter(isOperationFetchable),
+
filter(operation => {
+
return (
+
operation.operationName === 'query' ||
+
operation.operationName === 'mutation'
+
);
+
}),
mergeMap(operation => {
const teardown$ = pipe(
sharedOps$,
···
)
);
-
return pipe(
-
createFetchSource(
-
operation,
-
operation.operationName === 'query' &&
-
!!operation.context.preferGetMethod,
-
dispatchDebug
-
),
-
takeUntil(teardown$)
-
);
-
})
-
);
-
-
const forward$ = pipe(
-
sharedOps$,
-
filter(op => !isOperationFetchable(op)),
-
forward
-
);
-
-
return merge([fetchResults$, forward$]);
-
};
-
-
const getOperationName = (query: DocumentNode): string | null => {
-
const node = query.definitions.find(
-
(node: any): node is OperationDefinitionNode => {
-
return node.kind === Kind.OPERATION_DEFINITION && node.name;
-
}
-
);
-
-
return node && node.name ? node.name.value : null;
-
};
-
-
const createFetchSource = (
-
operation: Operation,
-
shouldUseGet: boolean,
-
dispatchDebug: ExchangeInput['dispatchDebug']
-
) => {
-
if (
-
process.env.NODE_ENV !== 'production' &&
-
operation.operationName === 'subscription'
-
) {
-
throw new Error(
-
'Received a subscription operation in the httpExchange. You are probably trying to create a subscription. Have you added a subscriptionExchange?'
-
);
-
}
-
-
return make<OperationResult>(({ next, complete }) => {
-
const abortController =
-
typeof AbortController !== 'undefined'
-
? new AbortController()
-
: undefined;
-
-
const { context } = operation;
-
// Spreading operation.variables here in case someone made a variables with Object.create(null).
-
const { files, clone } = extractFiles({ ...operation.variables });
-
-
const extraOptions =
-
typeof context.fetchOptions === 'function'
-
? context.fetchOptions()
-
: context.fetchOptions || {};
-
-
const operationName = getOperationName(operation.query);
-
-
const body: Body = {
-
query: print(operation.query),
-
variables: operation.variables,
-
};
-
-
if (operationName !== null) {
-
body.operationName = operationName;
-
}
-
-
const fetchOptions = {
-
...extraOptions,
-
method: shouldUseGet ? 'GET' : 'POST',
-
headers: {
-
'content-type': 'application/json',
-
...extraOptions.headers,
-
},
-
signal:
-
abortController !== undefined ? abortController.signal : undefined,
-
};
-
-
if (!!files.size) {
-
fetchOptions.body = new FormData();
-
fetchOptions.method = 'POST';
-
// Make fetch auto-append this for correctness
-
delete fetchOptions.headers['content-type'];
-
-
fetchOptions.body.append(
-
'operations',
-
JSON.stringify({
-
...body,
-
variables: clone,
-
})
-
);
-
-
const map = {};
-
let i = 0;
-
files.forEach(paths => {
-
map[++i] = paths.map(path => `variables.${path}`);
+
// Spreading operation.variables here in case someone made a variables with Object.create(null).
+
const { files, clone: variables } = extractFiles({
+
...operation.variables,
});
-
fetchOptions.body.append('map', JSON.stringify(map));
+
const body = makeFetchBody({ query: operation.query, variables });
-
i = 0;
-
files.forEach((_, file) => {
-
(fetchOptions.body as FormData).append(`${++i}`, file, file.name);
-
});
-
} else if (shouldUseGet) {
-
operation.context.url = convertToGet(operation.context.url, body);
-
} else {
-
fetchOptions.body = JSON.stringify(body);
-
}
-
-
let ended = false;
-
-
Promise.resolve()
-
.then(() =>
-
ended ? undefined : executeFetch(operation, fetchOptions, dispatchDebug)
-
)
-
.then((result: OperationResult | undefined) => {
-
if (!ended) {
-
ended = true;
-
if (result) next(result);
-
complete();
+
let url: string;
+
let fetchOptions: RequestInit;
+
if (files.size) {
+
url = makeFetchURL(operation);
+
fetchOptions = makeFetchOptions(operation);
+
if (fetchOptions.headers!['content-type'] === 'application/json') {
+
delete fetchOptions.headers!['content-type'];
}
-
});
-
return () => {
-
ended = true;
-
if (abortController !== undefined) {
-
abortController.abort();
-
}
-
};
-
});
-
};
+
fetchOptions.method = 'POST';
+
fetchOptions.body = new FormData();
+
fetchOptions.body.append('operations', JSON.stringify(body));
-
const executeFetch = (
-
operation: Operation,
-
opts: RequestInit,
-
dispatchDebug: ExchangeInput['dispatchDebug']
-
): Promise<OperationResult> => {
-
const { url, fetch: fetcher } = operation.context;
-
let statusNotOk = false;
-
let response: Response;
+
const map = {};
+
let i = 0;
+
files.forEach(paths => {
+
map[++i] = paths.map(path => `variables.${path}`);
+
});
-
dispatchDebug({
-
type: 'fetchRequest',
-
message: 'A fetch request is being executed.',
-
operation,
-
data: {
-
url,
-
fetchOptions: opts,
-
},
-
});
+
fetchOptions.body.append('map', JSON.stringify(map));
-
return (fetcher || fetch)(url, opts)
-
.then((res: Response) => {
-
response = res;
-
statusNotOk =
-
res.status < 200 ||
-
res.status >= (opts.redirect === 'manual' ? 400 : 300);
-
return res.json();
-
})
-
.then((result: any) => {
-
if (!('data' in result) && !('errors' in result)) {
-
throw new Error('No Content');
+
i = 0;
+
files.forEach((_, file) => {
+
(fetchOptions.body as FormData).append(`${++i}`, file, file.name);
+
});
+
} else {
+
fetchOptions = makeFetchOptions(operation, body);
+
url = makeFetchURL(operation, body);
}
dispatchDebug({
-
type: result.errors && !result.data ? 'fetchError' : 'fetchSuccess',
-
message: `A ${
-
result.errors ? 'failed' : 'successful'
-
} fetch response has been returned.`,
+
type: 'fetchRequest',
+
message: 'A fetch request is being executed.',
operation,
data: {
url,
-
fetchOptions: opts,
-
value: result,
+
fetchOptions,
},
});
-
return makeResult(operation, result, response);
-
})
-
.catch((error: Error) => {
-
if (error.name !== 'AbortError') {
-
dispatchDebug({
-
type: 'fetchError',
-
message: error.name,
-
operation,
-
data: {
-
url,
-
fetchOptions: opts,
-
value: error,
-
},
-
});
-
-
return makeErrorResult(
-
operation,
-
statusNotOk ? new Error(response.statusText) : error,
-
response
-
);
-
}
-
});
-
};
+
return pipe(
+
makeFetchSource(operation, url, fetchOptions),
+
takeUntil(teardown$),
+
onPush(result => {
+
const error = !result.data ? result.error : undefined;
-
export const convertToGet = (uri: string, body: Body): string => {
-
const queryParams: string[] = [`query=${encodeURIComponent(body.query)}`];
+
dispatchDebug({
+
type: error ? 'fetchError' : 'fetchSuccess',
+
message: `A ${
+
error ? 'failed' : 'successful'
+
} fetch response has been returned.`,
+
operation,
+
data: {
+
url,
+
fetchOptions,
+
value: error || result,
+
},
+
});
+
})
+
);
+
})
+
);
-
if (body.variables) {
-
queryParams.push(
-
`variables=${encodeURIComponent(JSON.stringify(body.variables))}`
-
);
-
}
+
const forward$ = pipe(
+
sharedOps$,
+
filter(operation => {
+
return (
+
operation.operationName !== 'query' &&
+
operation.operationName !== 'mutation'
+
);
+
}),
+
forward
+
);
-
return uri + '?' + queryParams.join('&');
+
return merge([fetchResults$, forward$]);
};
+1
exchanges/multipart-fetch/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1
exchanges/populate/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1
exchanges/retry/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1
exchanges/suspense/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+11
packages/core/internal/package.json
···
+
{
+
"name": "urql-core-internal",
+
"private": true,
+
"main": "../dist/urql-core-internal.js",
+
"module": "../dist/urql-core-internal.mjs",
+
"types": "../dist/types/internal/index.d.ts",
+
"source": "../src/internal/index.ts",
+
"dependencies": {
+
"wonka": "^4.0.9"
+
}
+
}
+6
packages/core/package.json
···
"require": "./dist/urql-core.js",
"types": "./dist/types/index.d.ts",
"source": "./src/index.ts"
+
},
+
"./internal": {
+
"import": "./dist/urql-core-internal.mjs",
+
"require": "./dist/urql-core-internal.js",
+
"types": "./dist/types/internal/index.d.ts",
+
"source": "./src/internal/index.ts"
}
},
"files": [
+1 -1
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
···
}
`;
-
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\\"}"`;
+
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
···
import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka';
-
import gql from 'graphql-tag';
-
import { print } from 'graphql';
import { Client } from '../client';
import { queryOperation } from '../test-utils';
import { OperationResult, OperationType } from '../types';
-
import { fetchExchange, convertToGet } from './fetch';
+
import { fetchExchange } from './fetch';
const fetch = (global as any).fetch as jest.Mock;
const abort = jest.fn();
···
expect(abort).toHaveBeenCalledTimes(0);
});
});
-
-
describe('convert for GET', () => {
-
it('should do a basic conversion', () => {
-
const query = `query ($id: ID!) { node(id: $id) { id } }`;
-
const variables = { id: 2 };
-
expect(convertToGet('http://localhost:3000', { query, variables })).toBe(
-
`http://localhost:3000?query=${encodeURIComponent(
-
query
-
)}&variables=${encodeURIComponent(JSON.stringify(variables))}`
-
);
-
});
-
-
it('should do a basic conversion with fragments', () => {
-
const nodeFragment = gql`
-
fragment nodeFragment on Node {
-
id
-
}
-
`;
-
-
const variables = { id: 2 };
-
const query = print(gql`
-
query($id: ID!) {
-
node(id: $id) {
-
...nodeFragment
-
}
-
}
-
${nodeFragment}
-
`);
-
-
expect(convertToGet('http://localhost:3000', { query, variables })).toBe(
-
`http://localhost:3000?query=${encodeURIComponent(
-
query
-
)}&variables=${encodeURIComponent(JSON.stringify(variables))}`
-
);
-
});
-
});
+40 -184
packages/core/src/exchanges/fetch.ts
···
/* eslint-disable @typescript-eslint/no-use-before-define */
-
import { Kind, DocumentNode, OperationDefinitionNode, print } from 'graphql';
-
import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka';
-
import { Exchange, Operation, OperationResult, ExchangeInput } from '../types';
-
import { makeResult, makeErrorResult } from '../utils';
+
import { filter, merge, mergeMap, pipe, share, takeUntil, onPush } from 'wonka';
-
interface Body {
-
query: string;
-
variables: void | object;
-
operationName?: string;
-
}
+
import { Exchange } from '../types';
+
import {
+
makeFetchBody,
+
makeFetchURL,
+
makeFetchOptions,
+
makeFetchSource,
+
} from '../internal';
/** A default exchange for fetching GraphQL requests. */
export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => {
···
filter(op => op.operationName === 'teardown' && op.key === key)
);
+
const body = makeFetchBody(operation);
+
const url = makeFetchURL(operation, body);
+
const fetchOptions = makeFetchOptions(operation, body);
+
+
dispatchDebug({
+
type: 'fetchRequest',
+
message: 'A fetch request is being executed.',
+
operation,
+
data: {
+
url,
+
fetchOptions,
+
},
+
});
+
return pipe(
-
createFetchSource(
-
operation,
-
operation.operationName === 'query' &&
-
!!operation.context.preferGetMethod,
-
dispatchDebug
-
),
-
takeUntil(teardown$)
+
makeFetchSource(operation, url, fetchOptions),
+
takeUntil(teardown$),
+
onPush(result => {
+
const error = !result.data ? result.error : undefined;
+
+
dispatchDebug({
+
type: error ? 'fetchError' : 'fetchSuccess',
+
message: `A ${
+
error ? 'failed' : 'successful'
+
} fetch response has been returned.`,
+
operation,
+
data: {
+
url,
+
fetchOptions,
+
value: error || result,
+
},
+
});
+
})
);
})
);
···
return merge([fetchResults$, forward$]);
};
};
-
-
const getOperationName = (query: DocumentNode): string | null => {
-
const node = query.definitions.find(
-
(node: any): node is OperationDefinitionNode => {
-
return node.kind === Kind.OPERATION_DEFINITION && node.name;
-
}
-
);
-
-
return node ? node.name!.value : null;
-
};
-
-
const createFetchSource = (
-
operation: Operation,
-
shouldUseGet: boolean,
-
dispatchDebug: ExchangeInput['dispatchDebug']
-
) => {
-
if (
-
process.env.NODE_ENV !== 'production' &&
-
operation.operationName === 'subscription'
-
) {
-
throw new Error(
-
'Received a subscription operation in the httpExchange. You are probably trying to create a subscription. Have you added a subscriptionExchange?'
-
);
-
}
-
-
return make<OperationResult>(({ next, complete }) => {
-
const abortController =
-
typeof AbortController !== 'undefined'
-
? new AbortController()
-
: undefined;
-
-
const { context } = operation;
-
-
const extraOptions =
-
typeof context.fetchOptions === 'function'
-
? context.fetchOptions()
-
: context.fetchOptions || {};
-
-
const operationName = getOperationName(operation.query);
-
-
const body: Body = {
-
query: print(operation.query),
-
variables: operation.variables,
-
};
-
-
if (operationName !== null) {
-
body.operationName = operationName;
-
}
-
-
const fetchOptions = {
-
...extraOptions,
-
body: shouldUseGet ? undefined : JSON.stringify(body),
-
method: shouldUseGet ? 'GET' : 'POST',
-
headers: {
-
'content-type': 'application/json',
-
...extraOptions.headers,
-
},
-
signal:
-
abortController !== undefined ? abortController.signal : undefined,
-
};
-
-
if (shouldUseGet) {
-
operation.context.url = convertToGet(operation.context.url, body);
-
}
-
-
let ended = false;
-
-
Promise.resolve()
-
.then(() =>
-
ended ? undefined : executeFetch(operation, fetchOptions, dispatchDebug)
-
)
-
.then((result: OperationResult | undefined) => {
-
if (!ended) {
-
ended = true;
-
if (result) next(result);
-
complete();
-
}
-
});
-
-
return () => {
-
ended = true;
-
if (abortController !== undefined) {
-
abortController.abort();
-
}
-
};
-
});
-
};
-
-
const executeFetch = (
-
operation: Operation,
-
opts: RequestInit,
-
dispatchDebug: ExchangeInput['dispatchDebug']
-
): Promise<OperationResult> => {
-
const { url, fetch: fetcher } = operation.context;
-
let statusNotOk = false;
-
let response: Response;
-
-
dispatchDebug({
-
type: 'fetchRequest',
-
message: 'A fetch request is being executed.',
-
operation,
-
data: {
-
url,
-
fetchOptions: opts,
-
},
-
});
-
-
return (fetcher || fetch)(url, opts)
-
.then((res: Response) => {
-
response = res;
-
statusNotOk =
-
res.status < 200 ||
-
res.status >= (opts.redirect === 'manual' ? 400 : 300);
-
return res.json();
-
})
-
.then((result: any) => {
-
if (!('data' in result) && !('errors' in result)) {
-
throw new Error('No Content');
-
}
-
-
dispatchDebug({
-
type: result.errors && !result.data ? 'fetchError' : 'fetchSuccess',
-
message: `A ${
-
result.errors ? 'failed' : 'successful'
-
} fetch response has been returned.`,
-
operation,
-
data: {
-
url,
-
fetchOptions: opts,
-
value: result,
-
},
-
});
-
-
return makeResult(operation, result, response);
-
})
-
.catch((error: Error) => {
-
if (error.name !== 'AbortError') {
-
dispatchDebug({
-
type: 'fetchError',
-
message: error.name,
-
operation,
-
data: {
-
url,
-
fetchOptions: opts,
-
value: error,
-
},
-
});
-
-
return makeErrorResult(
-
operation,
-
statusNotOk ? new Error(response.statusText) : error,
-
response
-
);
-
}
-
});
-
};
-
-
export const convertToGet = (uri: string, body: Body): string => {
-
const queryParams: string[] = [`query=${encodeURIComponent(body.query)}`];
-
-
if (body.variables) {
-
queryParams.push(
-
`variables=${encodeURIComponent(JSON.stringify(body.variables))}`
-
);
-
}
-
-
return uri + '?' + queryParams.join('&');
-
};
+670
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
···
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+
exports[`on error ignores the error when a result is available 1`] = `
+
Object {
+
"data": undefined,
+
"error": [CombinedError: [Network] ],
+
"extensions": undefined,
+
"operation": Object {
+
"context": Object {
+
"fetchOptions": Object {
+
"method": "POST",
+
},
+
"requestPolicy": "cache-first",
+
"url": "http://localhost:3000/graphql",
+
},
+
"key": 2,
+
"operationName": "query",
+
"query": Object {
+
"definitions": Array [
+
Object {
+
"directives": Array [],
+
"kind": "OperationDefinition",
+
"name": Object {
+
"kind": "Name",
+
"value": "getUser",
+
},
+
"operation": "query",
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [
+
Object {
+
"kind": "Argument",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
"value": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "user",
+
},
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "id",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "firstName",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "lastName",
+
},
+
"selectionSet": undefined,
+
},
+
],
+
},
+
},
+
],
+
},
+
"variableDefinitions": Array [
+
Object {
+
"defaultValue": undefined,
+
"directives": Array [],
+
"kind": "VariableDefinition",
+
"type": Object {
+
"kind": "NamedType",
+
"name": Object {
+
"kind": "Name",
+
"value": "String",
+
},
+
},
+
"variable": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
},
+
],
+
"kind": "Document",
+
"loc": Object {
+
"end": 124,
+
"start": 0,
+
},
+
},
+
"variables": Object {
+
"name": "Clara",
+
},
+
},
+
}
+
`;
+
+
exports[`on error returns error data 1`] = `
+
Object {
+
"data": undefined,
+
"error": [CombinedError: [Network] ],
+
"extensions": undefined,
+
"operation": Object {
+
"context": Object {
+
"fetchOptions": Object {
+
"method": "POST",
+
},
+
"requestPolicy": "cache-first",
+
"url": "http://localhost:3000/graphql",
+
},
+
"key": 2,
+
"operationName": "query",
+
"query": Object {
+
"definitions": Array [
+
Object {
+
"directives": Array [],
+
"kind": "OperationDefinition",
+
"name": Object {
+
"kind": "Name",
+
"value": "getUser",
+
},
+
"operation": "query",
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [
+
Object {
+
"kind": "Argument",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
"value": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "user",
+
},
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "id",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "firstName",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "lastName",
+
},
+
"selectionSet": undefined,
+
},
+
],
+
},
+
},
+
],
+
},
+
"variableDefinitions": Array [
+
Object {
+
"defaultValue": undefined,
+
"directives": Array [],
+
"kind": "VariableDefinition",
+
"type": Object {
+
"kind": "NamedType",
+
"name": Object {
+
"kind": "Name",
+
"value": "String",
+
},
+
},
+
"variable": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
},
+
],
+
"kind": "Document",
+
"loc": Object {
+
"end": 124,
+
"start": 0,
+
},
+
},
+
"variables": Object {
+
"name": "Clara",
+
},
+
},
+
}
+
`;
+
+
exports[`on error returns error data with status 400 and manual redirect mode 1`] = `
+
Object {
+
"data": undefined,
+
"error": [CombinedError: [Network] ],
+
"extensions": undefined,
+
"operation": Object {
+
"context": Object {
+
"fetchOptions": Object {
+
"method": "POST",
+
},
+
"requestPolicy": "cache-first",
+
"url": "http://localhost:3000/graphql",
+
},
+
"key": 2,
+
"operationName": "query",
+
"query": Object {
+
"definitions": Array [
+
Object {
+
"directives": Array [],
+
"kind": "OperationDefinition",
+
"name": Object {
+
"kind": "Name",
+
"value": "getUser",
+
},
+
"operation": "query",
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [
+
Object {
+
"kind": "Argument",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
"value": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "user",
+
},
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "id",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "firstName",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "lastName",
+
},
+
"selectionSet": undefined,
+
},
+
],
+
},
+
},
+
],
+
},
+
"variableDefinitions": Array [
+
Object {
+
"defaultValue": undefined,
+
"directives": Array [],
+
"kind": "VariableDefinition",
+
"type": Object {
+
"kind": "NamedType",
+
"name": Object {
+
"kind": "Name",
+
"value": "String",
+
},
+
},
+
"variable": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
},
+
],
+
"kind": "Document",
+
"loc": Object {
+
"end": 124,
+
"start": 0,
+
},
+
},
+
"variables": Object {
+
"name": "Clara",
+
},
+
},
+
}
+
`;
+
+
exports[`on success returns response data 1`] = `
+
Object {
+
"data": Object {
+
"data": Object {
+
"user": 1200,
+
},
+
},
+
"error": undefined,
+
"extensions": undefined,
+
"operation": Object {
+
"context": Object {
+
"fetchOptions": Object {
+
"method": "POST",
+
},
+
"requestPolicy": "cache-first",
+
"url": "http://localhost:3000/graphql",
+
},
+
"key": 2,
+
"operationName": "query",
+
"query": Object {
+
"definitions": Array [
+
Object {
+
"directives": Array [],
+
"kind": "OperationDefinition",
+
"name": Object {
+
"kind": "Name",
+
"value": "getUser",
+
},
+
"operation": "query",
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [
+
Object {
+
"kind": "Argument",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
"value": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "user",
+
},
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "id",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "firstName",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "lastName",
+
},
+
"selectionSet": undefined,
+
},
+
],
+
},
+
},
+
],
+
},
+
"variableDefinitions": Array [
+
Object {
+
"defaultValue": undefined,
+
"directives": Array [],
+
"kind": "VariableDefinition",
+
"type": Object {
+
"kind": "NamedType",
+
"name": Object {
+
"kind": "Name",
+
"value": "String",
+
},
+
},
+
"variable": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
},
+
],
+
"kind": "Document",
+
"loc": Object {
+
"end": 124,
+
"start": 0,
+
},
+
},
+
"variables": Object {
+
"name": "Clara",
+
},
+
},
+
}
+
`;
+
+
exports[`on success uses the mock fetch if given 1`] = `
+
Object {
+
"data": Object {
+
"data": Object {
+
"user": 1200,
+
},
+
},
+
"error": undefined,
+
"extensions": undefined,
+
"operation": Object {
+
"context": Object {
+
"fetch": [MockFunction] {
+
"calls": Array [
+
Array [
+
"https://test.com/graphql",
+
Object {
+
"signal": undefined,
+
},
+
],
+
],
+
"results": Array [
+
Object {
+
"type": "return",
+
"value": Promise {},
+
},
+
],
+
},
+
"fetchOptions": Object {
+
"method": "POST",
+
},
+
"requestPolicy": "cache-first",
+
"url": "http://localhost:3000/graphql",
+
},
+
"key": 2,
+
"operationName": "query",
+
"query": Object {
+
"definitions": Array [
+
Object {
+
"directives": Array [],
+
"kind": "OperationDefinition",
+
"name": Object {
+
"kind": "Name",
+
"value": "getUser",
+
},
+
"operation": "query",
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [
+
Object {
+
"kind": "Argument",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
"value": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "user",
+
},
+
"selectionSet": Object {
+
"kind": "SelectionSet",
+
"selections": Array [
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "id",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "firstName",
+
},
+
"selectionSet": undefined,
+
},
+
Object {
+
"alias": undefined,
+
"arguments": Array [],
+
"directives": Array [],
+
"kind": "Field",
+
"name": Object {
+
"kind": "Name",
+
"value": "lastName",
+
},
+
"selectionSet": undefined,
+
},
+
],
+
},
+
},
+
],
+
},
+
"variableDefinitions": Array [
+
Object {
+
"defaultValue": undefined,
+
"directives": Array [],
+
"kind": "VariableDefinition",
+
"type": Object {
+
"kind": "NamedType",
+
"name": Object {
+
"kind": "Name",
+
"value": "String",
+
},
+
},
+
"variable": Object {
+
"kind": "Variable",
+
"name": Object {
+
"kind": "Name",
+
"value": "name",
+
},
+
},
+
},
+
],
+
},
+
],
+
"kind": "Document",
+
"loc": Object {
+
"end": 124,
+
"start": 0,
+
},
+
},
+
"variables": Object {
+
"name": "Clara",
+
},
+
},
+
}
+
`;
+83
packages/core/src/internal/fetchOptions.ts
···
+
import { Kind, print, DocumentNode } from 'graphql';
+
+
import { stringifyVariables } from '../utils';
+
import { Operation } from '../types';
+
+
export interface FetchBody {
+
query: string;
+
operationName: string | undefined;
+
variables: undefined | Record<string, any>;
+
extensions: undefined | Record<string, any>;
+
}
+
+
const getOperationName = (query: DocumentNode): string | undefined => {
+
for (let i = 0, l = query.definitions.length; i < l; i++) {
+
const node = query.definitions[i];
+
if (node.kind === Kind.OPERATION_DEFINITION && node.name) {
+
return node.name.value;
+
}
+
}
+
};
+
+
const shouldUseGet = (operation: Operation): boolean => {
+
return (
+
operation.operationName === 'query' && !!operation.context.preferGetMethod
+
);
+
};
+
+
export const makeFetchBody = (request: {
+
query: DocumentNode;
+
variables?: object;
+
}): FetchBody => ({
+
query: print(request.query),
+
operationName: getOperationName(request.query),
+
variables: request.variables || undefined,
+
extensions: undefined,
+
});
+
+
export const makeFetchURL = (
+
operation: Operation,
+
body?: FetchBody
+
): string => {
+
const useGETMethod = shouldUseGet(operation);
+
let url = operation.context.url;
+
if (!useGETMethod || !body) return url;
+
+
url += `?query=${encodeURIComponent(body.query)}`;
+
+
if (body.variables) {
+
url += `&variables=${encodeURIComponent(
+
stringifyVariables(body.variables)
+
)}`;
+
}
+
+
if (body.extensions) {
+
url += `&extensions=${encodeURIComponent(
+
stringifyVariables(body.extensions)
+
)}`;
+
}
+
+
return url;
+
};
+
+
export const makeFetchOptions = (
+
operation: Operation,
+
body?: FetchBody
+
): RequestInit => {
+
const useGETMethod = shouldUseGet(operation);
+
+
const extraOptions =
+
typeof operation.context.fetchOptions === 'function'
+
? operation.context.fetchOptions()
+
: operation.context.fetchOptions || {};
+
+
return {
+
...extraOptions,
+
body: !useGETMethod && body ? JSON.stringify(body) : undefined,
+
method: useGETMethod ? 'GET' : 'POST',
+
headers: {
+
'content-type': 'application/json',
+
...extraOptions.headers,
+
},
+
};
+
};
+154
packages/core/src/internal/fetchSource.test.ts
···
+
import { pipe, subscribe, toPromise } from 'wonka';
+
+
import { queryOperation } from '../test-utils';
+
import { makeFetchSource } from './fetchSource';
+
+
const fetch = (global as any).fetch as jest.Mock;
+
const abort = jest.fn();
+
+
const abortError = new Error();
+
abortError.name = 'AbortError';
+
+
beforeAll(() => {
+
(global as any).AbortController = function AbortController() {
+
this.signal = undefined;
+
this.abort = abort;
+
};
+
});
+
+
afterEach(() => {
+
fetch.mockClear();
+
abort.mockClear();
+
});
+
+
afterAll(() => {
+
(global as any).AbortController = undefined;
+
});
+
+
const response = {
+
status: 200,
+
data: {
+
data: {
+
user: 1200,
+
},
+
},
+
};
+
+
describe('on success', () => {
+
beforeEach(() => {
+
fetch.mockResolvedValue({
+
status: 200,
+
json: jest.fn().mockResolvedValue(response),
+
});
+
});
+
+
it('returns response data', async () => {
+
const fetchOptions = {};
+
const data = await pipe(
+
makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions),
+
toPromise
+
);
+
+
expect(data).toMatchSnapshot();
+
+
expect(fetch).toHaveBeenCalled();
+
expect(fetch.mock.calls[0][0]).toBe('https://test.com/graphql');
+
expect(fetch.mock.calls[0][1]).toBe(fetchOptions);
+
});
+
+
it('uses the mock fetch if given', async () => {
+
const fetchOptions = {};
+
const fetcher = jest.fn().mockResolvedValue({
+
status: 200,
+
json: jest.fn().mockResolvedValue(response),
+
});
+
+
const data = await pipe(
+
makeFetchSource(
+
{
+
...queryOperation,
+
context: {
+
...queryOperation.context,
+
fetch: fetcher,
+
},
+
},
+
'https://test.com/graphql',
+
fetchOptions
+
),
+
toPromise
+
);
+
+
expect(data).toMatchSnapshot();
+
expect(fetch).not.toHaveBeenCalled();
+
expect(fetcher).toHaveBeenCalled();
+
});
+
});
+
+
describe('on error', () => {
+
beforeEach(() => {
+
fetch.mockResolvedValue({
+
status: 400,
+
json: jest.fn().mockResolvedValue({}),
+
});
+
});
+
+
it('returns error data', async () => {
+
const fetchOptions = {};
+
const data = await pipe(
+
makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions),
+
toPromise
+
);
+
+
expect(data).toMatchSnapshot();
+
});
+
+
it('returns error data with status 400 and manual redirect mode', async () => {
+
const data = await pipe(
+
makeFetchSource(queryOperation, 'https://test.com/graphql', {
+
redirect: 'manual',
+
}),
+
toPromise
+
);
+
+
expect(data).toMatchSnapshot();
+
});
+
+
it('ignores the error when a result is available', async () => {
+
const data = await pipe(
+
makeFetchSource(queryOperation, 'https://test.com/graphql', {}),
+
toPromise
+
);
+
+
expect(data).toMatchSnapshot();
+
});
+
});
+
+
describe('on teardown', () => {
+
it('does not start the outgoing request on immediate teardowns', () => {
+
fetch.mockRejectedValueOnce(abortError);
+
+
const { unsubscribe } = pipe(
+
makeFetchSource(queryOperation, 'https://test.com/graphql', {}),
+
subscribe(fail)
+
);
+
+
unsubscribe();
+
expect(fetch).toHaveBeenCalledTimes(0);
+
expect(abort).toHaveBeenCalledTimes(1);
+
});
+
+
it('aborts the outgoing request', async () => {
+
fetch.mockRejectedValueOnce(abortError);
+
+
const { unsubscribe } = pipe(
+
makeFetchSource(queryOperation, 'https://test.com/graphql', {}),
+
subscribe(fail)
+
);
+
+
await Promise.resolve();
+
+
unsubscribe();
+
expect(fetch).toHaveBeenCalledTimes(1);
+
expect(abort).toHaveBeenCalledTimes(1);
+
});
+
});
+77
packages/core/src/internal/fetchSource.ts
···
+
import { Operation, OperationResult } from '../types';
+
import { makeResult, makeErrorResult } from '../utils';
+
import { make } from 'wonka';
+
+
const executeFetch = (
+
operation: Operation,
+
url: string,
+
fetchOptions: RequestInit
+
): Promise<OperationResult> => {
+
const fetcher = operation.context.fetch;
+
+
let statusNotOk = false;
+
let response: Response;
+
+
return (fetcher || fetch)(url, fetchOptions)
+
.then((res: Response) => {
+
response = res;
+
statusNotOk =
+
res.status < 200 ||
+
res.status >= (fetchOptions.redirect === 'manual' ? 400 : 300);
+
return res.json();
+
})
+
.then((result: any) => {
+
if (!('data' in result) && !('errors' in result)) {
+
throw new Error('No Content');
+
}
+
+
return makeResult(operation, result, response);
+
})
+
.catch((error: Error) => {
+
if (error.name !== 'AbortError') {
+
return makeErrorResult(
+
operation,
+
statusNotOk ? new Error(response.statusText) : error,
+
response
+
);
+
}
+
}) as Promise<OperationResult>;
+
};
+
+
export const makeFetchSource = (
+
operation: Operation,
+
url: string,
+
fetchOptions: RequestInit
+
) => {
+
return make<OperationResult>(({ next, complete }) => {
+
const abortController =
+
typeof AbortController !== 'undefined' ? new AbortController() : null;
+
+
let ended = false;
+
+
Promise.resolve()
+
.then(() => {
+
if (ended) {
+
return;
+
} else if (abortController) {
+
fetchOptions.signal = abortController.signal;
+
}
+
+
return executeFetch(operation, url, fetchOptions);
+
})
+
.then((result: OperationResult | undefined) => {
+
if (!ended) {
+
ended = true;
+
if (result) next(result);
+
complete();
+
}
+
});
+
+
return () => {
+
ended = true;
+
if (abortController) {
+
abortController.abort();
+
}
+
};
+
});
+
};
+2
packages/core/src/internal/index.ts
···
+
export * from './fetchOptions';
+
export * from './fetchSource';
+1
packages/core/src/types.ts
···
export interface OperationContext {
[key: string]: any;
additionalTypenames?: string[];
+
fetch?: typeof fetch;
fetchOptions?: RequestInit | (() => RequestInit);
requestPolicy: RequestPolicy;
url: string;
+1
packages/core/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1
packages/preact-urql/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1
packages/react-urql/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1
packages/svelte-urql/tsconfig.json
···
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
+
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
+1
scripts/jest/preset.js
···
moduleNameMapper: {
"^urql$": "<rootDir>/../../node_modules/urql/src",
"^(.*-urql)$": "<rootDir>/../../node_modules/$1/src",
+
"^@urql/(.*)/(.*)$": "<rootDir>/../../node_modules/@urql/$1/src/$2",
"^@urql/(.*)$": "<rootDir>/../../node_modules/@urql/$1/src",
},
watchPlugins: ['jest-watch-yarn-workspaces'],
+1
tsconfig.json
···
"urql": ["packages/react-urql/src"],
"*-urql": ["packages/*-urql/src"],
"@urql/exchange-*": ["exchanges/*/src"],
+
"@urql/core/*": ["packages/core/src/*"],
"@urql/*": ["packages/*-urql/src", "packages/*/src"]
},
"esModuleInterop": true,