Skip to content

Instantly share code, notes, and snippets.

@richtera
Created June 30, 2019 14:04
Show Gist options
  • Select an option

  • Save richtera/ec539df29d129136f38f4365ae8a6895 to your computer and use it in GitHub Desktop.

Select an option

Save richtera/ec539df29d129136f38f4365ae8a6895 to your computer and use it in GitHub Desktop.

Revisions

  1. richtera created this gist Jun 30, 2019.
    159 changes: 159 additions & 0 deletions fetch.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,159 @@
    /* eslint-disable @typescript-eslint/no-use-before-define */
    import { print } from 'graphql';
    import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka';
    import { CombinedError } from 'urql';
    import 'isomorphic-fetch';

    /** A default exchange for fetching GraphQL requests. */
    export const fetchExchange = ({ forward }) => {
    const isOperationFetchable = (operation) => {
    const { operationName } = operation;
    return operationName === 'query' || operationName === 'mutation';
    };

    return ops$ => {
    const sharedOps$ = share(ops$);
    const fetchResults$ = pipe(
    sharedOps$,
    filter(isOperationFetchable),
    mergeMap(operation => {
    const { key } = operation;
    const teardown$ = pipe(
    sharedOps$,
    filter(op => op.operationName === 'teardown' && op.key === key)
    );

    return pipe(
    createFetchSource(operation),
    takeUntil(teardown$)
    );
    })
    );

    const forward$ = pipe(
    sharedOps$,
    filter(op => !isOperationFetchable(op)),
    forward
    );

    return merge([fetchResults$, forward$]);
    };
    };

    const createFetchSource = (operation) => {
    if (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(([next, complete]) => {
    const abortController =
    typeof AbortController !== 'undefined'
    ? new AbortController()
    : undefined;

    const { context } = operation;

    const extraOptions =
    typeof context.fetchOptions === 'function'
    ? context.fetchOptions()
    : context.fetchOptions || {};

    const extraPromise = typeof extraOptions.then === 'function' ? extraOptions : null;

    const fetchOptions = {
    body: JSON.stringify({
    query: print(operation.query),
    variables: operation.variables,
    }),
    ...extraOptions,
    method: 'POST',
    headers: {
    'content-type': 'application/json',
    ...extraOptions.headers,
    },
    signal:
    abortController !== undefined ? abortController.signal : undefined,
    extraPromise,
    };

    executeFetch(operation, fetchOptions).then(result => {
    if (result !== undefined) {
    if (result !== undefined) {
    next(result);
    }
    }

    complete();
    });

    return () => {
    if (abortController !== undefined) {
    abortController.abort();
    }
    };
    });
    };

    const executeFetch = (operation, opts) => {
    let response;
    const { url } = operation.context;

    return (opts.extraPromise || Promise.resolve(null))
    .then((extra) => {
    let { extraPromise, ...myOpts } = opts;
    if (extra) {
    const { headers, ...rest } = extra;
    const { headers: optsHeaders, extraPromise, ...optsRest } = opts;
    myOpts = {
    ...optsRest,
    ...rest,
    headers: {
    ...optsHeaders,
    ...headers,
    },
    };
    }
    return fetch(url, myOpts);
    })
    .then(res => {
    response = res;
    checkStatus(opts.redirect, response);
    return response.json();
    })
    .then(result => ({
    operation,
    data: result.data,
    error: Array.isArray(result.errors)
    ? new CombinedError({
    graphQLErrors: result.errors,
    response,
    })
    : undefined,
    }))
    .catch(err => {
    if (err.name === 'AbortError') {
    return undefined;
    }

    return {
    operation,
    data: undefined,
    error: new CombinedError({
    networkError: err,
    response,
    }),
    };
    });
    };

    const checkStatus = (redirectMode = 'follow', response) => {
    const statusRangeEnd = redirectMode === 'manual' ? 400 : 300;

    if (response.status < 200 || response.status > statusRangeEnd) {
    throw new Error(response.statusText);
    }
    };

    export default fetchExchange;