UNPKG

graphql-typed-apollo-client

Version:

A tool that generates a strongly typed client library for any GraphQL endpoint. The client allows writing GraphQL queries as plain JS objects (with type safety, awesome code completion experience, custom scalar type mapping, type guards and more)

198 lines (163 loc) 7.09 kB
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<GraphQLError>) { 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<ExecutionResult<any>> } export interface Client<QR, QC, Q, MR, MC, M, SR, SC, S> { apolloQuery(apolloContext: any, request: QR, options?: QueryHookOptions<any, Record<string, any>> | undefined): any useQuery(request: QR, options?: QueryHookOptions<any, Record<string, any>> | undefined): any useMutation(request: Function, options?: MutationHookOptions<any, Record<string, any>> | undefined): any query(request: QR): Promise<ExecutionResult<Q>> mutation(request: MR): Promise<ExecutionResult<M>> subscription(request: SR): Observable<ExecutionResult<S>> 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 = <QR extends Fields, QC, Q, MR extends Fields, MC, M, SR extends Fields, SC, S>({ fetcher, subscriptionCreatorOptions, queryRoot, mutationRoot, subscriptionRoot, typeMapper, }: ClientOptions & ClientEmbeddedOptions): Client<QR, QC, Q, MR, MC, M, SR, SC, S> => { 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<any, Record<string, any>> | 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<TData = any, TVariables = OperationVariables>( request: Function, options?: MutationHookOptions<any, Record<string, any>> | undefined, ): MutationTuple<TData, TVariables> { 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<TData, TVariables> = {} as MutationFunctionOptions<TData, TVariables>, ) => { 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<TData, TVariables> } const query = (request: QR): Promise<ExecutionResult<Q>> => { 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<ExecutionResult<M>> => { 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<ExecutionResult<S>> => { 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: <any>chain((path, request, defaultValue) => query(request).then(mapResponse(path, defaultValue))), mutation: <any>chain((path, request, defaultValue) => mutation(request).then(mapResponse(path, defaultValue))), subscription: <any>( chain((path, request, defaultValue) => subscription(request).pipe(map(mapResponse(path, defaultValue)))) ), }, } }