yuku-t/hyperapp-apollo

View on GitHub
src/Mutation.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { Component, ActionsType, VNode } from "hyperapp"
import { DocumentNode } from "graphql"
import { FetchResult } from "apollo-link"
import ApolloClient, { ApolloError, PureQueryOptions, MutationUpdaterFn } from "apollo-client"

import { OperationVariables, RefetchQueriesProviderFn } from "./types"
import * as apollo from "./apollo"
import addLifeCycleHandlers from "./util/addLifeCycleHandlers"
import omit from "./util/omit"
import resolveNode from "./util/resolveNode"
import setData from "./util/setData"

export interface MutationResult<Data = Record<string, any>> {
  called: boolean
  data?: Data
  error?: ApolloError
  loading: boolean
}

export interface MutationOptions<Data = any, Variables = OperationVariables> {
  optimisticResponse?: object
  refetchQueries?: string[] | PureQueryOptions[] | RefetchQueriesProviderFn
  update?: MutationUpdaterFn<Data>
  variables?: Variables
}

export declare type MutationFn<Data = any, Variables = OperationVariables> = (
  options?: MutationOptions<Data, Variables>
) => Promise<void | FetchResult<Data>>

export interface MutationProps<Data = any, Variables = OperationVariables> {
  ignoreResults?: boolean
  key: string // Globally unique key
  mutation: DocumentNode
  onCompleted?: (data: Data) => void
  onError?: (error: ApolloError) => void
  optimisticResponse?: object
  refetchQueries?: string[] | PureQueryOptions[] | RefetchQueriesProviderFn
  render: Component<RenderProps<Data, Variables>, any, any>
  update?: MutationUpdaterFn<Data>
  variables?: Variables
}

export declare type RenderProps<Data, Variables> = MutationResult<Data> & {
  runMutation: (options?: MutationOptions<Data, Variables>) => void
}

export interface MutationState<Data = any> {
  called: boolean
  data?: Data
  error?: ApolloError
  loading: boolean
}

export interface MutationLocals<Data, Variables> {
  hasMounted: boolean
  mostRecentMutationId: number
  props: MutationProps<Data, Variables>
}

const locals: {
  [key: string]: MutationLocals<any, any>
} = {}

// globalState.apollo.mutation
export interface State {
  modules: {
    [id: string]: MutationState<any>
  }
  client?: ApolloClient<any>
}

// wiredActions.apollo.mutation
export interface Actions {
  initialize: (params: { props: MutationProps<any, any>; client: ApolloClient<any> }) => void
  mutate: (params: { props: MutationProps<any, any>; options: MutationOptions<any> }) => Promise<FetchResult>
  onStartMutation: (params: MutationProps<any, any>) => void
  onCompletedMutation: (params: { props: MutationProps<any, any>; response: FetchResult; mutationId: number }) => void
  onMutationError: (params: { props: MutationProps<any, any>; error: ApolloError; mutationId: number }) => void
  destroy: (key: string) => void
  modules: {
    setData: (data: { key: string; data: Partial<MutationState<any>> }) => void
  }
}

export const state: State = {
  modules: {}
}

export const actions: ActionsType<State, Actions> = {
  initialize: (params: { props: MutationProps<any, any>; client: ApolloClient<any> }) => (_, actions) => {
    locals[params.props.key] = {
      hasMounted: false,
      mostRecentMutationId: 0,
      props: params.props
    }
    actions.modules.setData({
      key: params.props.key,
      data: {
        loading: false,
        called: false,
        error: undefined,
        data: undefined
      }
    })
    return {
      client: params.client
    }
  },
  mutate: (params: { props: MutationProps<any, any>; options: MutationOptions<any> }) => state => {
    const { mutation, variables, optimisticResponse, update } = params.props
    const refetchQueries = params.options.refetchQueries || params.props.refetchQueries
    return state.client!.mutate({
      mutation,
      variables,
      optimisticResponse,
      refetchQueries,
      update,
      ...params.options
    })
  },
  onStartMutation: (params: MutationProps) => state => {
    const mutationState = state.modules[params.key]
    if (!mutationState.loading && !params.ignoreResults) {
      return {
        loading: true,
        error: undefined,
        data: undefined,
        called: true
      }
    }
    return
  },
  onCompletedMutation: (params: { props: MutationProps<any, any>; response: FetchResult; mutationId: number }) => {
    const { onCompleted, ignoreResults } = params.props
    const data = params.response.data
    const callOnCompleted = () => (onCompleted ? onCompleted(data) : null)
    if (isMostRecentMutation(params.props.key, params.mutationId) && !ignoreResults) {
      setTimeout(callOnCompleted, 10) // huristic
      return {
        loading: false,
        data
      }
    }
    callOnCompleted()
    return
  },
  onMutationError: (params: { props: MutationProps<any, any>; error: ApolloError; mutationId: number }) => {
    const onError = params.props.onError
    const callOnError = () => (onError ? onError(params.error) : null)
    if (isMostRecentMutation(params.props.key, params.mutationId)) {
      setTimeout(callOnError, 10) // huristic
      return {
        loading: false,
        error: params.error
      }
    }
    callOnError()
    return
  },
  destroy: (key: string) => state => ({ modules: omit(state.modules, key) }),
  modules: {
    setData
  }
}

function generateNewMutationId(key: string) {
  locals[key].mostRecentMutationId = locals[key].mostRecentMutationId + 1
  return locals[key].mostRecentMutationId
}

function isMostRecentMutation(key: string, mutationId: number) {
  return locals[key].mostRecentMutationId === mutationId
}

function getRenderProps<Data, Variables>(
  props: MutationProps<Data, Variables>,
  state: MutationState<Data>,
  actions: Actions
): RenderProps<Data, Variables> {
  return {
    called: state.called,
    loading: state.loading,
    data: state.data,
    error: state.error,
    runMutation: (options?: MutationOptions<Data, Variables>) => {
      actions.onStartMutation(props)
      const mutationId = generateNewMutationId(props.key)
      return actions
        .mutate({ props, options: options || {} })
        .then(response => {
          actions.onCompletedMutation({
            props,
            response,
            mutationId
          })
          return response
        })
        .catch(error => {
          actions.onMutationError({ props, error, mutationId })
          if (!props.onError) {
            throw error
          }
        })
    }
  }
}

export function Mutation<Data = any, Variables = OperationVariables>(
  props: MutationProps<Data, Variables>,
  children: any
) {
  return (state: { apollo: apollo.State }, actions: { apollo: apollo.Actions }) => {
    const mutationState: MutationState<Data> | void = state.apollo.mutation.modules[props.key]
    if (!mutationState) {
      actions.apollo.initMutation(props)
      return
    }
    const vnode: VNode<any> = resolveNode(
      props.render(getRenderProps(props, mutationState, actions.apollo.mutation), children),
      state,
      actions
    )
    vnode.attributes = addLifeCycleHandlers(
      {
        ...vnode.attributes,
        key: props.key
      },
      {
        oncreate() {
          locals[props.key].hasMounted = true
        },
        ondestroy() {
          actions.apollo.mutation.destroy(props.key)
        }
      }
    )
    locals[props.key].props = props
    return vnode
  }
}