increments/graphql-client-js

View on GitHub
src/sendRequest.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { GraphQLError } from "graphql"

import { uniqId } from "./uniqId"

export type Handler = (query: string, variables: Variables, resolve: ResolveCallback, reject: RejectCallback) => void

export type ResolveCallback = (resp: { data?: any; errors?: GraphQLError[] }) => void

export type RejectCallback = (message: string) => void

export type OperationName = "query" | "mutation"

export interface VariableDecls {
  [name: string]: {
    type: string
    value: any
  }
}

export interface Variables {
  [name: string]: any
}

export interface RequestParams {
  [alias: string]: {
    query: string
    decls: VariableDecls
    resolve: (value?: any | PromiseLike<any> | undefined) => void
    reject: (reason?: any) => void
  }
}

const SELECTION_DELIMITER = /\s|\(|{/

const buildQueryAndVariables = (
  name: OperationName,
  params: RequestParams
): { query: string; variables: Variables } => {
  const queries: string[] = []
  const variableDefinitions: string[] = []
  const variables: Variables = {}

  Object.keys(params).forEach(aliasName => {
    const param = params[aliasName]
    let query = param.query
    Object.keys(param.decls)
      .sort((a, b) => b.length - a.length) // Sort by length in descending order
      .forEach(varName => {
        const uniqName = uniqId(varName)
        query = query.replace(`$${varName}`, `$${uniqName}`)
        variableDefinitions.push(`$${uniqName}: ${param.decls[varName].type}`)
        variables[uniqName] = param.decls[varName].value
      })
    queries.push(`${aliasName}:${query}`)
  })

  const defs = variableDefinitions.length ? `(${variableDefinitions.join(",")})` : ""
  const query = `${name}${defs}{\n${queries.join("\n")}\n}`
  return { query, variables }
}

const createResolveCallback = (params: RequestParams): ResolveCallback => ({ data, errors }) => {
  const payloads: {
    [name: string]: { data?: any; errors?: GraphQLError[] }
  } = {}
  const nameMap: { [aliasName: string]: string } = {}

  if (data) {
    Object.keys(data).forEach(aliasName => {
      const param = params[aliasName]
      const originalName = param.query.split(SELECTION_DELIMITER, 1)[0]
      payloads[aliasName] = {
        data: {
          [originalName]: data[aliasName]
        }
      }
      nameMap[aliasName] = originalName
    })
  }
  if (errors) {
    errors.forEach(error => {
      if (error.path && error.path.length) {
        const field = error.path[0]
        if (!nameMap[field]) {
          return // Unknown field
        }
        if (!payloads[field]) {
          payloads[field] = {
            errors: []
          }
        }
        if (!payloads[field].errors) {
          payloads[field].errors = []
        }
        payloads[field].errors!.push({
          ...error,
          path: [nameMap[field]].concat(error.path.slice(1) as any)
        })
      }
    })
  }
  Object.keys(payloads).forEach(name => {
    if (params[name]) {
      params[name].resolve(payloads[name])
    }
  })
  // Resolve all unresolved promises
  Object.keys(params).forEach(name => {
    params[name].resolve({})
  })
}

const createRejectCallback = (params: RequestParams): RejectCallback => (...args: any[]) => {
  Object.keys(params).forEach(aliasName => {
    params[aliasName].reject(...args)
  })
}

export const sendRequest = (name: OperationName, params: RequestParams, handler: Handler): void => {
  const { query, variables } = buildQueryAndVariables(name, params)
  handler(query, variables, createResolveCallback(params), createRejectCallback(params))
}