Untitled

mail@pastecode.io avatar
unknown
javascript
3 years ago
15 kB
4
Indexable
Never
import ReactGA from 'react-ga'
import { C, actions, selectors } from 'state'
import store from 'store'
import * as Sentry from '@sentry/browser'

import defaultClient from 'client'

const message = 'If the issue persists please let us know.'

const queryErrors = {
  startLiveShare: { title: 'starting Live Share', message },
  addProject: { title: 'adding a project', message },
  continueProject: { title: 'staring a project', message },
  acceptInvite: { title: 'accepting invite', message },
  addMember: { title: 'adding member', message },
  removeMember: { title: 'removing member', message },
  githubAuth: { title: 'login', message },
  gitlabAuth: { title: 'login', message },
  bitbucketAuth: { title: 'login', message },
  createTeam: { title: 'adding your team', message },
  addTeamLeader: { title: 'adding a team leader', message },
  renameTeam: { title: 'renaming a team', message },
  deleteProject: { title: 'deleting a project', message },
  user: {
    title: 'fetching your data',
    message:
      'Refresh the browser. If this happens again, use logout button or let us know.',
  },
}

const queriesThatShouldNotifyOnError = Object.keys(queryErrors)

const getLoading = selectors.api.getLoading
const getToken = selectors.getToken

/**
 * Gracefully handle mutations. Handles all the async aspects of querying, automatically dispatches redux
 * actions for loading, error and success. Adds ability to handle success and error outside of redux,
 * for example to set localStorage.
 * @param {Object} objectParam mutation takes one param will all the details, like variables living inside
 * @param {string} objectParam.name - Name of the mutation, for example githubAuth
 * @param {string} objectParam.storeKey - Key where mutation result will be stored unless a custom onSuccessDispatch or onErrorDispatch are provided
 * @param {string} objectParam.variables - Mutation variables
 * @param {string} objectParam.context - Mutation context
 * @param {string} objectParam.errorPolicy - Mutation errorPolicy, defaults to all
 * @param {string} objectParam.mutation - Actual mutation
 * @param {function} objectParam.onSuccess - Function called with the result after mutation succeeds. Example: user => localStorage.setItem('token', user.token)
 * @param {function} objectParam.onError - Function called with the error after mutation fails. Example: () => localStorage.removeItem('token')
 * @param {function} objectParam.onSuccessDispatch - Function called with the result - returns action dispatch. Example: user => ({ type: "USER_LOGIN_SUCCESS", payload: user })
 * @param {function} objectParam.onSuccessError - Function called with the error - returns action dispatch. Example: error => ({ type: "USER_LOGIN_ERROR", payload: error })
 * @param {function} objectParam.dataSelector - Function used to get result data, useful for example for paginated results like data.myProjects.edges
 * @param {string} objectParam.client - GraphQL client, for example Github
 */
export const mutation = ({
  name,
  storeKey = name,
  variables,
  context,
  errorPolicy = 'all',
  fetchPolicy = 'no-cache',
  mutation,
  onLoading,
  onSuccess,
  onError,
  onLoadingDispatch,
  onSuccessDispatch,
  onErrorDispatch,
  dataSelector = data => data[name],
  client = defaultClient,
  allowRepeated = true,
  storeInState = true,
}) => {
  return async dispatch => {
    const state = store.getState()
    const token = getToken(state)
    if (getLoading(name)(state) && !allowRepeated) {
      dispatch({
        type: C.api.SKIPPING_REPEATED_QUERY,
        payload: { name, storeKey },
      })
      return null
    }

    onLoading && onLoading(storeKey)
    if (onLoadingDispatch) {
      if (Array.isArray(onLoadingDispatch)) {
        onLoadingDispatch.forEach(action => dispatch(action(storeKey)))
      } else {
        dispatch(onLoadingDispatch(storeKey))
      }
    } else {
      dispatch({
        type: C.api.FETCH_START,
        payload: { storeKey },
      })
    }

    const requestStartTime = performance.now()

    try {
      const { data, errors } = await client.mutate({
        mutation,
        context:
          context || context === null
            ? context
            : {
                headers: {
                  Authorization: `Bearer ${token}`,
                  'User-Agent': 'node',
                },
              },
        variables,
        fetchPolicy,
        errorPolicy,
      })

      if (errors) {
        throw errors
      }

      const result = dataSelector(data)

      if (result) {
        const requestEndTime = performance.now()
        const requestTime = requestEndTime - requestStartTime
        if (name === 'resetCron') {
          dispatch(actions.latency.fullLatencyMeasurement(requestTime))
        }
        ReactGA.timing({
          category: 'Request Performance',
          variable: name,
          value: requestTime,
        })

        console.log('Request Performance', requestTime)
      }

      if (onSuccess) {
        if (Array.isArray(onSuccess)) {
          onSuccess.forEach(func => func(result))
        } else {
          onSuccess(result)
        }
      }

      if (storeInState) {
        dispatch({
          type: C.api.FETCH_SUCCESS,
          payload: { storeKey, data: result },
        })
      } else {
        dispatch({
          type: C.api.FETCH_SUCCESS,
          payload: { storeKey, data: true },
        })
      }

      if (onSuccessDispatch) {
        if (Array.isArray(onSuccessDispatch)) {
          onSuccessDispatch.forEach(action => dispatch(action(result)))
        } else {
          dispatch(onSuccessDispatch(result))
        }
      }

      const requestHandlingEndTime = performance.now()
      ReactGA.timing({
        category: 'Request Handling Performance',
        variable: name,
        value: requestHandlingEndTime - requestStartTime,
      })

      console.log(
        'Request Handling Performance',
        requestHandlingEndTime - requestStartTime,
        name
      )

      return result
    } catch (errorObj) {
      if (Array.isArray(errorObj)) {
        const error = errorObj[0].extensions.code
        const errorMessage = errorObj[0].message

        onError && onError(error)

        if (onErrorDispatch) {
          if (Array.isArray(onErrorDispatch)) {
            onErrorDispatch.forEach(action => dispatch(action(error)))
          } else {
            dispatch(onErrorDispatch(error))
          }
        } else {
          dispatch({
            type: C.api.FETCH_ERROR,
            storeKey,
            payload: { error, errorMessage, storeKey },
          })
        }

        const requestEndTime = performance.now()
        ReactGA.timing({
          category: 'Request Error Performance',
          variable: name,
          value: requestEndTime - requestStartTime,
        })

        if (queriesThatShouldNotifyOnError.includes(name.toLowerCase())) {
          dispatch(
            actions.messages.setMessage({
              key: name,
              title: `Error happened during ${queryErrors[name].title}`,
              message: `${queryErrors[name].message}`,
            })
          )

          Sentry.withScope(scope => {
            scope.setExtras({ errorObj, state })
          })
        }

        console.log(
          'Request Error Performance',
          requestEndTime - requestStartTime,
          name
        )

        return null
      } else {
        const error = errorObj.toString()
        console.log('Error', error)
        onError && onError(error)

        if (onErrorDispatch) {
          if (Array.isArray(onErrorDispatch)) {
            onErrorDispatch.forEach(action => dispatch(action(error)))
          } else {
            dispatch(onErrorDispatch(error))
          }
        } else {
          dispatch({
            type: C.api.FETCH_ERROR,
            storeKey,
            payload: { error, message: error, storeKey },
          })
        }

        const requestEndTime = performance.now()
        ReactGA.timing({
          category: 'Request Error Performance',
          variable: name,
          value: requestEndTime - requestStartTime,
        })

        if (queriesThatShouldNotifyOnError.includes(name.toLowerCase())) {
          dispatch(
            actions.messages.setMessage({
              key: name,
              title: `Error happened during ${queryErrors[name].title}`,
              message: `${queryErrors[name].message}`,
            })
          )

          Sentry.withScope(scope => {
            scope.setExtras({ errorObj, state })
          })
        }

        console.log(
          'Request Error Performance',
          requestEndTime - requestStartTime,
          name
        )

        return null
      }
    }
  }
}

/**
 * Gracefully handle queries. Handles all the async aspects of querying, automatically dispatches redux
 * actions for loading, error and success. Adds ability to handle success and error outside of redux,
 * for example to set localStorage.
 * @param {Object} objectParam mutation takes one param will all the details, like variables living inside
 * @param {string} objectParam.name - Name of the mutation, for example githubAuth
 * @param {string} objectParam.storeKey - Key where mutation result will be stored unless a custom onSuccessDispatch or onErrorDispatch are provided
 * @param {string} objectParam.variables - Mutation variables
 * @param {string} objectParam.context - Mutation context
 * @param {string} objectParam.errorPolicy - Mutation fetchPolicy, defaults to cache-first
 * @param {string} objectParam.errorPolicy - Mutation errorPolicy, defaults to all
 * @param {string} objectParam.query - Actual query
 * @param {function} objectParam.onSuccess - Function called with the result after query succeeds. Example: user => localStorage.setItem('token', user.token)
 * @param {function} objectParam.onError - Function called with the error after query fails. Example: () => localStorage.removeItem('token')
 * @param {function} objectParam.onSuccessDispatch - Function called with the result - returns action dispatch. Example: user => ({ type: "USER_LOGIN_SUCCESS", payload: user })
 * @param {function} objectParam.onErrorDispatch - Function called with the error - returns action dispatch. Example: error => ({ type: "USER_LOGIN_ERROR", payload: error })
 * @param {function} objectParam.dataSelector - Function used to get result data, useful for example for paginated results like data.myProjects.edges
 * @param {string} objectParam.client - GraphQL client, for example Github
 */

export const query = ({
  name,
  storeKey = name,
  variables,
  context,
  fetchPolicy = 'network-only',
  errorPolicy = 'all',
  query,
  onLoading,
  onSuccess,
  onError,
  onLoadingDispatch,
  onSuccessDispatch,
  onErrorDispatch,
  dataSelector = data => data[name],
  client = defaultClient,
  allowRepeated = true,
  isPagination = false,
  id,
}) => {
  return async dispatch => {
    const state = store.getState()
    const token = getToken(state)
    if (getLoading(name)(state) && !allowRepeated) {
      dispatch({
        type: C.api.SKIPPING_REPEATED_QUERY,
        payload: { name, storeKey },
      })
      return null
    }

    onLoading && onLoading(storeKey)
    if (onLoadingDispatch) {
      if (Array.isArray(onLoadingDispatch)) {
        onLoadingDispatch.forEach(action => dispatch(action(storeKey)))
      } else {
        dispatch(onLoadingDispatch(storeKey))
      }
    } else {
      dispatch({
        type: C.api.FETCH_START,
        payload: { storeKey },
      })
    }

    const requestStartTime = performance.now()

    try {
      const { data } = await client.query({
        query,
        context:
          context || context === null
            ? context
            : {
                headers: {
                  Authorization: `Bearer ${token}`,
                  'User-Agent': 'node',
                },
              },
        variables,
        fetchPolicy,
        errorPolicy,
      })

      const result = dataSelector(data)

      if (result) {
        const requestEndTime = performance.now()
        const requestTime = requestEndTime - requestStartTime
        if (name === 'resetCron') {
          dispatch(actions.latency.fullLatencyMeasurement(requestTime))
        }
        ReactGA.timing({
          category: 'Request Performance',
          variable: name,
          value: requestTime,
        })

        console.log('Request Performance', requestTime)
      }

      if (onSuccess) {
        if (Array.isArray(onSuccess)) {
          onSuccess.forEach(func => func(result))
        } else {
          onSuccess(result)
        }
      }

      if (isPagination) {
        dispatch({
          type: C.api.APPEND_PAGINATION_DATA,
          payload: { storeKey, data: result, id },
        })
        if (storeKey === 'getInvites') {
          const flatResult = result?.edges?.flatMap(invite =>
            invite?.steps?.flatMap(step => step?.task)
          )

          dispatch({
            type: C.api.APPEND_PAGINATION_DATA,
            payload: {
              storeKey: 'getProjects',
              data: { edges: flatResult },
              id: 'candidatesTasks',
            },
          })
        }
      } else {
        dispatch({
          type: C.api.FETCH_SUCCESS,
          payload: { storeKey, data: result },
        })
      }

      if (onSuccessDispatch) {
        if (Array.isArray(onSuccessDispatch)) {
          onSuccessDispatch.forEach(action => dispatch(action(result)))
        } else {
          dispatch(onSuccessDispatch(result))
        }
      }

      const requestHandlingEndTime = performance.now()
      ReactGA.timing({
        category: 'Request Handling Performance',
        variable: name,
        value: requestHandlingEndTime - requestStartTime,
      })
      console.log(
        'Request Handling Performance',
        requestHandlingEndTime - requestStartTime,
        name
      )

      return result
    } catch (errorObj) {
      const error = errorObj.toString()
      console.log('fetch error: ', error, 'errorObj', errorObj)

      if (queriesThatShouldNotifyOnError.includes(name.toLowerCase())) {
        dispatch(
          actions.messages.setMessage({
            key: name,
            title: `Error happened during ${queryErrors[name].title}`,
            message: `${queryErrors[name].message}`,
          })
        )

        Sentry.withScope(scope => {
          scope.setExtras({
            errorObj,
            state,
          })
        })
      }

      onError && onError(error)
      if (onErrorDispatch) {
        if (Array.isArray(onErrorDispatch)) {
          onErrorDispatch.forEach(action => dispatch(action(error)))
        } else {
          dispatch(onErrorDispatch(error))
        }
      } else {
        dispatch({
          type: C.api.FETCH_ERROR,
          storeKey,
          payload: { error, errorMessage: error, storeKey },
        })
      }

      const requestEndTime = performance.now()
      ReactGA.timing({
        category: 'Request Error Performance',
        variable: name,
        value: requestEndTime - requestStartTime,
      })

      console.log(
        'Request Error Performance',
        requestEndTime - requestStartTime,
        name
      )

      return null
    }
  }
}