import 'isomorphic-fetch' import qs from 'qs' import { ExecutionResult, GraphQLError } from 'graphql' import { NEVER, Observable } from 'rxjs' import { get } from 'lodash' import { map } from 'rxjs/operators' import { useQuery as _useQuery, useMutation as _useMutation, QueryHookOptions, MutationHookOptions, MutationTuple, OperationVariables, } from '@apollo/client' import { MutationFunctionOptions } from '@apollo/client/react/types/types' import gql from 'graphql-tag' import { applyTypeMapperToResponse, TypeMapper } from './applyTypeMapperToResponse' import { chain } from './chain' import { LinkedType } from './linkTypeMap' import { Fields, Gql, requestToGql } from './requestToGql' import { getSubscriptionCreator, SubscriptionCreatorOptions } from './getSubscriptionCreator' export class ClientError extends Error { constructor(message?: string, public errors?: ReadonlyArray) { super(errors ? `${message}\n${errors.map(error => JSON.stringify(error, null, 2)).join('\n')}` : message) new.target.prototype.name = new.target.name Object.setPrototypeOf(this, new.target.prototype) if (Error.captureStackTrace) Error.captureStackTrace(this, ClientError) } } export interface Fetcher { (gql: Gql, fetchImpl: typeof fetch, qsImpl: typeof qs): Promise> } export interface Client { apolloQuery(apolloContext: any, request: QR, options?: QueryHookOptions> | undefined): any useQuery(request: QR, options?: QueryHookOptions> | undefined): any useMutation(request: Function, options?: MutationHookOptions> | undefined): any query(request: QR): Promise> mutation(request: MR): Promise> subscription(request: SR): Observable> chain: { query: QC mutation: MC subscription: SC } } export interface ClientOptions { fetcher?: Fetcher subscriptionCreatorOptions?: SubscriptionCreatorOptions } export interface ClientEmbeddedOptions { queryRoot?: LinkedType mutationRoot?: LinkedType subscriptionRoot?: LinkedType typeMapper?: TypeMapper } const dummyMutation = gql` mutation dummy { dummy { id } } ` export const createClient = ({ fetcher, subscriptionCreatorOptions, queryRoot, mutationRoot, subscriptionRoot, typeMapper, }: ClientOptions & ClientEmbeddedOptions): Client => { const createSubscription = subscriptionCreatorOptions ? getSubscriptionCreator(subscriptionCreatorOptions) : () => NEVER function apolloQuery(apolloContext: any, request: QR, options: any) { if (!queryRoot) throw new Error('queryRoot argument is missing') const { query, variables } = requestToGql('query', queryRoot, request, typeMapper) return apolloContext.client?.query({ query: gql(query), variables, ...options, }) } function useQuery(request: QR, options?: QueryHookOptions> | undefined) { if (!queryRoot) throw new Error('queryRoot argument is missing') let gqlQuery: any, gqlVars: any if (request) { const { query, variables } = requestToGql('query', queryRoot, request, typeMapper) gqlQuery = gql(query) gqlVars = variables } else { gqlQuery = gql`query {}` gqlVars = {} } const result = _useQuery(gqlQuery, { variables: gqlVars, ...options }) return result } function useMutation( request: Function, options?: MutationHookOptions> | undefined, ): MutationTuple { if (!mutationRoot) throw new Error('mutationRoot argument is missing') // use a trick here to delay the evaluation of actual mutation query function const [_executeMutation, mutateState] = _useMutation(dummyMutation, options) const executeMutation = ( executeOptions: MutationFunctionOptions = {} as MutationFunctionOptions, ) => { const { variables: executeVariables, ...otherOptions } = executeOptions const { query, variables } = requestToGql('mutation', mutationRoot, request(executeVariables), typeMapper) const gqlQuery = gql(query) const overrideExecuteOptions = { ...otherOptions, mutation: gqlQuery, variables, } return _executeMutation(overrideExecuteOptions) } return [executeMutation, mutateState] as MutationTuple } const query = (request: QR): Promise> => { if (!fetcher) throw new Error('fetcher argument is missing') if (!queryRoot) throw new Error('queryRoot argument is missing') const resultPromise = fetcher(requestToGql('query', queryRoot, request, typeMapper), fetch, qs) return typeMapper ? resultPromise.then(result => applyTypeMapperToResponse(queryRoot, result, typeMapper)) : resultPromise } const mutation = (request: MR): Promise> => { if (!fetcher) throw new Error('fetcher argument is missing') if (!mutationRoot) throw new Error('mutationRoot argument is missing') const resultPromise = fetcher(requestToGql('mutation', mutationRoot, request, typeMapper), fetch, qs) return typeMapper ? resultPromise.then(result => applyTypeMapperToResponse(mutationRoot, result, typeMapper)) : resultPromise } const subscription = (request: SR): Observable> => { if (!subscriptionCreatorOptions) throw new Error('subscriptionClientOptions argument is missing') if (!subscriptionRoot) throw new Error('subscriptionRoot argument is missing') const resultObservable = createSubscription(requestToGql('subscription', subscriptionRoot, request, typeMapper)) return typeMapper ? resultObservable.pipe(map(result => applyTypeMapperToResponse(subscriptionRoot, result, typeMapper))) : resultObservable } const mapResponse = (path: string[], defaultValue: any) => (response: ExecutionResult) => { if (response.errors) throw new ClientError(`Response contains errors`, response.errors) if (!response.data) throw new ClientError('Response data is empty') const result = get(response, ['data', ...path], defaultValue) if (result === undefined) throw new ClientError(`Response path \`${path.join('.')}\` is empty`) return result } return { apolloQuery, useQuery, useMutation, query, mutation, subscription, chain: { query: chain((path, request, defaultValue) => query(request).then(mapResponse(path, defaultValue))), mutation: chain((path, request, defaultValue) => mutation(request).then(mapResponse(path, defaultValue))), subscription: ( chain((path, request, defaultValue) => subscription(request).pipe(map(mapResponse(path, defaultValue)))) ), }, } }