Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
@skava/graphql / src / apollo-fork / oneRequest.ts
Size: Mime:
/**
 * @file @todo
 * 1. fork axios
 * 2. make axios compat with fetch interface
 */
// import axios from 'axios'
import { isObj, isArray, EMPTY_OBJ } from 'exotic'
import { always, cloneJSON } from 'chain-able'
import {
  stringifyParamsRecursively,
  stringifyProperties,
} from '@skava/modules/___dist/oneRequest/deps/queryStringify'
import { config } from '@skava/modules/___dist/oneRequest/config'

const IS_BROWSER = typeof window === 'object'
// @todo @@perf freeze default headers
const defaultPostHeaders = {
  'Content-Type': 'application/x-www-form-urlencoded',
}

interface ApolloRequest {
  httpRequest: Request
}
type ApolloRequestOrRequest = ApolloRequest | Request


/**
 * @type {Map}
 */
class OneRequest {
  /**
   * @description map of middleware we can call on lifecycle hooks
   * @typedef {IntersectionObserverCallback}
   *
   * @note could also be .subscribe() to add, since it's quite observable-like
   *
   * ...for now though...
   *
   * @example
   * import Request from 'modules/oneRequest'
   * function middlewareForLocalStorage(request: Request, eventData: {}): void {}
   * Request.middleware.set('name', middlewareForLocalStorage)
   */
  static middleware = new Map();

  constructor() {
    // store is a standard name for every piece of data being set and get in a class
    this.store = {}
    // until we fix, set debug always true manually
    this.setDebug(true)
    // default to POST -- 50/1 POST TO GET
    // this.setMethod(POST)
    // could be done onCreate
    const POST = config.get('POST')
    this.setMethod(POST)
    this.emit('onCreate')
  }

  /**
   * @event lifecycle middleware called for this event
   * @param {String} eventName
   * @param {Object} [eventData=""]
   * @return {Request} @chainable
   */
  emit(eventName, eventData = EMPTY_OBJ) {
    // @todo @perf can use Object.seal
    const type = { type: eventName }
    const arg =
      eventData === EMPTY_OBJ
        ? type
        : {
          ...eventData,
          ...type,
        }

    const callMiddleware = (middleware, index) => {
      // if (isFunction(middleware.when)) / isSatisfied / @todo
      middleware(this, arg)
    }
    OneRequest.middleware.forEach(callMiddleware)

    if (isArray(this.middlewareList)) {
      this.middlewareList.forEach(callMiddleware)
    }

    // !!!!!!!!!
    // this.setParams(defaultParams)

    return this
  }
  use(middleware) {
    this.middlewareList.push(middleware)
    return this
  }
  /**
   * @private
   */
  _set(name, value) {
    this.store[name] = value
    return this
  }
  _get(name) {
    return this.store[name]
  }
  _has(name) {
    return !!this.store[name]
  }

  /**
   *
   */
  del() {
    //
  }
  /**
   *
   */
  get() {
    //
  }
  /**
   * patch, put
   */
  post() {
    //
  }
  /**
   *
   */
  then() {
    //
  }
  /**
   * ===
   */
  type(type) {
    this._set('type', type)
  }
  // cert() {}
  // key() {}
  // json() {}
  // xml() {}
  // responseType() {}
  // auth('tobi', 'learnboost')
  // .withCredentials()
  // .redirects(2)
  // .on('error', handle)

  // --- setters
  setDebug(shouldDebug = false) {
    return this._set('debug', shouldDebug)
  }
  url(url) {
    return this.setUrl(url)
  }
  setUrl(url) {
    return this._set('url', url)
  }
  /**
   * === sadly is ALSO for query ===
   * === matters for POST ===
   */
  setParams(params) {
    return this._set('params', params)
  }
  params(params) {
    return this.setParams(params)
    // return this._set('params', params)
  }

  /**
   * @tutorial https://github.com/node-nock/nock#specifying-request-query-string
   * @param {Object} params
   * @return {Request} @chainable
   */
  setDefaultParams(params) {
    // for now just sets params since we object assign
    // return this._set('params', params)
    return this.setParams(params)
    // return this._set('_defaultParams', params)
    // return this._set('params', params)
  }
  /**
   * @deprecated - is handled in setData for now
   * @TODO
   * @description for POST calls
   * @param {*} body
   */
  setBody(body) {
    return this._set('body', body)
  }
  setData(fixtureData) {
    return this._set('data', fixtureData)
  }
  setFormData(formData) {
    return this._set('formData', formData)
  }
  transformData(fn, data) {
    return this._set('data', fn(data))
  }
  setMethod(method) {
    return this._set('method', method)
  }

  // --- helpers
  log() {
    if (this.store.debug === true) {
      console.log.apply(console, arguments)
    }
    return this
  }
  validateRequestStore() {
    if (!this._has('url')) {
      throw new Error('missing url, use request.setUrl(urlForThisEndpoint)')
    }
    if (!this._has('data')) {
      throw new Error('missing fixture data, use request.setData(fixtureData)')
    }
    // if (!this._has('params')) {
    //   console.log(
    //     'did not pass in params, might want to disable this log because not every request needs params?'
    //   )
    // }
  }
  // PATCH, PUT, REQUEST, AXIOSER
  get isPost(): boolean {
    const methodName = this.store.method.methodName || this.store.methodName
    return (
      methodName === 'POST' ||
      methodName === 'REQUEST' ||
      methodName === 'AXIOSER' ||
      methodName === 'post'
    )
  }
  get isGet(): boolean {
    const methodName = this.store.method.methodName || this.store.methodName
    return methodName === 'GET' || methodName === 'get'
    // return this.store.method.methodType === 'GET'
  }

  setMethodType(type) {
    console.warn('@deprecated .setMethodType')
    return this
  }
  /**
   * @deprecated
   */
  setMethodName(methodName) {
    console.warn('@deprecated .setMethodName')
    this.store.methodName = name
    // The actual string representation of 'post'|'get'
    return this._set('methodName', methodName)
  }

  stringifyParams(paramArg = undefined): string {
    let params = paramArg || this._get('params')

    /**
     * HORRIBLE_BAD_SHOULD_BE_FIXED_BY_BACKEND_SHAME
     */
    // params.storeId = params.storeid

    // @todo @perf
    params = cloneJSON(params)
    params = stringifyParamsRecursively(params)

    return params
  }

  /**
   * @event toRequest
   * @event ->onRequest
   *
   * @see https://github.com/mzabriskie/axios
   * @return {AsyncFunction} call to get data
   */
  toRequest() {
    // @todo - add cloning through chaining or scoped props
    // @todo add dynamic url
    // add a property to not create fn each time
    if (this.dynamicRequest) {
      return this.dynamicRequest
    }

    this.emit('toRequest')

    /**
     * @note could accept string param as first arg
     * @todo could chain set the params or url
     */
    // @lint @todo @fixme @split
    // eslint-disable-next-line
    const dynamicAxiosRequest = async(
      dynamicParams = undefined,
      dynamicUrl = undefined
    ) => {
      // @NOTE this is how to return mocks
      // return this.store.data
      let { url, debug, method, onError, onSuccess } = this.store

      console.log('[1request]: ' + url)

      const FETCHREQUEST = config.get('FETCHREQUEST')

      // dynamicParams => formBody
      // defaultParams/queryParams => string for the end of the url
      let queryParams = this.store.params
      let params = queryParams

      // merge in dynamic params.. but not everything has params
      if (dynamicParams !== undefined) {
        // if (params !== undefined) {
        //   params = Object.assign(params, dynamicParams)
        // } else {
        //   params = Object.assign({}, dynamicParams)
        // }
        if (params !== undefined) {
          params = { ...queryParams, ...dynamicParams }
        } else {
          params = { ...dynamicParams }
        }
      }

      const constantParams = config.get('constantParams')
      Object.assign(params, constantParams)
      Object.assign(queryParams, constantParams)

      if (process.env.LOG_REQUEST_PARAMS) {
        console.log('_______constantParams', constantParams)
        console.log('___MERGED_PARAMS', queryParams)
      }

      if (dynamicUrl !== undefined) {
        url = url + '/' + dynamicUrl
      }
      // handle the transform
      if (isObj(params)) {
        params = stringifyProperties(params)
      }

      // NOW CHANGED BACK
      // if (clientDomain === 'reactdemo.skavaone.com') {
      //   queryParams.storeId = storeId
      // } else {
      //   queryParams.storeid = storeId
      // }
      // queryParams.storeId = queryParams.storeid

      queryParams = this.stringifyParams(queryParams || EMPTY_OBJ)
      // does it just remove at top level...?
      queryParams = decodeURIComponent(queryParams)

      /**
       * @type {AxiosData}
       */
      const axiosData = {
        params,
      }

      // @@perf use .has & cleanup @james @@fork
      if (this._get('headers')) {
        // console.log('[1request] had headers')
        axiosData.headers = this._get('headers')
      }


      /**
       * @todo split here ===
       *
       * @type {Array<Data>}
       */
      const args = [url, axiosData]
      // const ENABLE_FOR_POST = true
      if (this.isPost) {
        // @todo - should work for every request get and post
        url = `${url}?${queryParams}`

        const formData = this.stringifyParams(dynamicParams)
        const axiosPostData = {
          data: formData,
        }

        const axiosHeaders = this._get('headers')
        ? {
            ...defaultPostHeaders,
            ...this._get('headers'),
          }
        : {
          ...defaultPostHeaders,
        }

        // console.log('[1request] axiosHeaders: ', axiosHeaders)

        /**
         * @type {AxiosRequestConfig}
         */
        const axiosRequestConfig = {
          withCredentials: true,

          // @@perf use .has & cleanup @james @@fork
          headers: axiosHeaders,
        }

        const axioserRequestConfig = {
          url,
          method: 'post',

          ...axiosPostData,
          ...axiosRequestConfig,
        }

        if (process.env.LOG_REQUEST_PARAMS) {
          console.log('[oneRequest] params: ', axioserRequestConfig)
        }

        const [error, response] = await FETCHREQUEST.call(
          this,
          axioserRequestConfig
        )

        // !!!!! @@fork @@todo @james THIS IS TO CLEAR IT FOR EACH REQUEST
        this._set('headers', undefined)


        // console.dev({ error, response })
        if (process.env.LOG_REQUEST_RESPONSE) {
          console.log('[oneRequest] response: ', response)
        }

        if (error && onError) {
          if (process.env.LOG_REQUEST_ERROR) {
            console.log('[oneRequest] error: ', error)
          }

          // , response, this
          return onError(error) || response.data
        } else if (!error && onSuccess) {
          // or .data...
          onSuccess(response.data, response)
        }

        return response.data
      }
      // =========

      if (process.env.LOG_REQUEST_PARAMS) {
        console.log('[oneRequest] params: ', args)
      }
      /**
       * @description do our post call
       * @see chain/lego/oneRequest
       */
      const [error, response] = await method.apply(this, args)

      if (process.env.LOG_REQUEST_RESPONSE) {
        console.log('[oneRequest] response: ', response)
      }
      if (process.env.LOG_REQUEST_ERROR) {
        console.log('[oneRequest] error: ', error)
      }

      // if we have an error, for now, log it
      if (error) {
        // this.log({ error, response, ...this.store })

        if (error && onError) {
          // , response, this
          return onError(error) || response.data
        } else if (!error && onSuccess) {
          // or .data...
          onSuccess(response.data, response)
          return response.data
        }
        // return onCall(response.data, response) || response.data
        return response.data
      }

      // if (debug) {
      //   this.log({ error, response, params, ...this.store })
      // }
      // autofix(response.data)

      return response.data
    }

    this.dynamicAxiosRequest = dynamicAxiosRequest

    dynamicAxiosRequest.setHeaders = (headers = defaultPostHeaders) => {
      this._set('headers', headers)
      return dynamicAxiosRequest
    }
    dynamicAxiosRequest.setCookie = (cookieToSet: string) => {
      this._set('headers', {
        'Cookie': Array.isArray(cookieToSet) ? cookieToSet.join('') : cookieToSet,
        'cookie': Array.isArray(cookieToSet) ? cookieToSet.join('') : cookieToSet,
        '_cookie': Array.isArray(cookieToSet) ? cookieToSet.join('') : cookieToSet,
      })
      return dynamicAxiosRequest
    }

    dynamicAxiosRequest.forwardRequest = (request: ApolloRequestOrRequest) => {
      const finalRequest: Request = request.httpRequest ? request.httpRequest : request
      // console.log('[1request] forwarding')
      const cookieHeader = finalRequest.headers.get('Cookie')
      console.log('[1request] trying cookie:', cookieHeader)
      if (cookieHeader) {
        // console.log('[1request] has cookie')
        dynamicAxiosRequest.setCookie(cookieHeader)
        const headers = this._get('headers')
        headers['user-agent'] = finalRequest.headers.get('user-agent')
        // we don't want this
        // finalRequest.headers.forEach((value, key) => {
        //   console.log('setting header: ', {[key]: value})
        //   headers[key] = value
        // })
      }

      console.log('[1request] headers:', this._get('headers'))
      console.log('[1request] set headers, back to base')
      return dynamicAxiosRequest
    }

    dynamicAxiosRequest.toChain = always(this)
    dynamicAxiosRequest.onError = this.onError
    dynamicAxiosRequest.onSuccess = this.onSuccess

    // just for a better name than .forwardRequest(context)()
    dynamicAxiosRequest.doRequest = (dynamicParams, dynamicUrl) => dynamicAxiosRequest(dynamicParams, dynamicUrl)

    return dynamicAxiosRequest
  }

  // query<T>(options: WatchQueryOptions): Promise<ApolloQueryResult<T>>
  // mutate<T>(options: MutationOptions<T>): Promise<FetchResult<T>>
  // subscribe(options: SubscriptionOptions): Observable<any>
  // readQuery<T>(options: DataProxy.Query): T | null
  // readFragment<T>(options: DataProxy.Fragment): T | null
  // writeQuery(options: DataProxy.WriteQueryOptions): void
  // writeFragment(options: DataProxy.WriteFragmentOptions): void
  // gql = {
  //   // CRUD
  //   query: 'QueryHere',
  //   // dynamic
  //   // variables: {},
  // }

  /* @TODO split this out once it works as it broke all the tests :( */
  toMock() {
    let { url, params, data, method } = this.store
    // eslint-disable-next-line
    const nock = require("nock");
    const mockUrl = process.env.BASE_URL || 'http://localhost:4000'
    // console.log('mock data', this.store)
    // @NOTE naming it apiUrl was better, oops
    // show helpful messages to make sure people use it
    this.validateRequestStore()
    // for now, autofix it to include the baseUrl
    if (!url.includes('http')) {
      url = mockUrl + url
    }
    const handleGet = urlRequested => {
      // `api/${url}`
      const doesRequestIncludeUrl =
        urlRequested.includes(url) || url.includes(urlRequested)
      // this.log('getCall', { url, urlRequested, doesRequestIncludeUrl })
      return doesRequestIncludeUrl
    }
    if (this.isPost) {
      // console.dev(`${this.store.method} MOCKED`)
      return nock(mockUrl)
        .post(url)
        .query(params)
        .reply(201, data)
    } else {
      // console.dev(`${url} GET Crocked`)
      return nock(mockUrl)
        .get(handleGet)
        .query(params)
        .reply(200, data)
    }
  }
  setVariables(variables) {
    return this._set('variables', variables)
  }
  setQuery(Query) {
    return this._set('query', Query)
  }
  gql() {
    // all the things
  }
  /**
   * (subscriber)
   * same as () / .call() / .end() / .fetch()
   */
  send() {
    // @todo
    // const response: ApolloQueryResult<Query> = await client.query(args)
    // subscriber

    return this.toRequest().apply(this, arguments)
  }
  onBrowser = handler => {
    // this.store.onBrowser = handler
    const onBrowser = (oneRequest, arg) => {
      if (IS_BROWSER === false) {
        handler(oneRequest, arg)
      }
    }
    this.use(onBrowser)
    return this
  }
  onServer = handler => {
    // this.store.onServer = handler
    const onServer = (oneRequest, arg) => {
      if (IS_BROWSER === false) {
        handler(oneRequest, arg)
      }
    }
    this.use(onServer)
    return this
  }
  onSuccess = handler => {
    return this._set('onSuccess', handler)
  }
  onError = handler => {
    return this._set('onError', handler)
  }
}

export { OneRequest }
export { OneRequest as Request }
// export default Request