Workfront/workfront-api

View on GitHub
src/Api.ts

Summary

Maintainability
F
3 days
Test Coverage
/**
 * Copyright 2015 Workfront
 *
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @author Hovhannes Babayan <bhovhannes at gmail dot com>
 * @author Sassoun Derderian <citizen.sas at gmail dot com>
 */

import {INTERNAL_PREFIX} from 'workfront-api-constants'

export interface IHttpOptions {
    path?: string
    method?: string
    url: string
    alwaysUseGet?: boolean
    headers: {
        sessionID?: string
        'X-XSRF-TOKEN'?: string
        apiKey?: string
    }
}
export interface IApiConfig {
    url: string
    version?: string
    alwaysUseGet?: boolean
    apiKey?: string
    headers?: {[key: string]: string}
}
export type TFields = string | string[]

/**
 * Configuration for the Api constructor
 * @typedef {Object} config
 * @property {String} url - Required. A url to Workfront server (for example: http://localhost:8080)
 * @property {String} [version=internal] - Which version of api to use. At the moment of writing can be 2.0, 3.0, ..., 8.0. Pass 'unsupported' to use Workfront latest API (maybe unstable)
 * @property {Boolean} [alwaysUseGet=false] - Will cause the api to make every request as a GET with params in the query string and add method=DESIRED_METHOD_TYPE in the query string. Some Workfront urls will have issues with PUT and DELETE calls if this value is false
 * @property {String} [apiKey] - It is used to pass apiKey with every api request headers
 * @property {Object} [headers] - An key-value object that sets custom headers (for example: {'user-agent': DESIRED_USER_AGENT_NAME})
 */

/**
 * Creates new Api instance.
 * @param {Object} config   An object with the following keys:<br/>
 * @constructor
 */
export class Api {
    static Methods = {
        GET: 'GET',
        PUT: 'PUT',
        DELETE: 'DELETE',
        POST: 'POST',
    }
    _httpOptions: IHttpOptions
    serverAcceptsJSON: boolean

    _uriGenerationMode: boolean

    constructor(config: IApiConfig) {
        this.serverAcceptsJSON = true
        this._uriGenerationMode = false
        this._httpOptions = {
            url: config.url,
            alwaysUseGet: config.alwaysUseGet,
            headers: config.headers || {},
        }
        if (config.apiKey) {
            this._httpOptions.headers.apiKey = config.apiKey
        }
        // Append version to path if provided
        let path
        const {
            version = 'internal',
        }: {
            version?: string
        } = config
        if (['internal', 'unsupported', 'asp'].indexOf(version) >= 0) {
            path = '/attask/api-' + version
        } else {
            path = '/attask/api/v' + version
            if (version === '2.0' || version === '3.0' || version === '4.0') {
                this.serverAcceptsJSON = false
            }
        }
        this._httpOptions.path = path
    }

    /**
     * Used to obtain an API key
     * @memberOf Api
     * @param {String} username    A username in Workfront
     * @param {String} password    Password to use
     * @param {String} subdomain    Sub-domain to use
     * @return {Promise}    A promise which will resolved with API key if everything went ok and rejected otherwise
     */
    getApiKey(username: string, password: string, subdomain?: string): Promise<string> {
        const loginParams = {
            username,
            password,
        }
        if (subdomain !== undefined) {
            loginParams['subdomain'] = subdomain
        }
        return new Promise<string>((resolve, reject) => {
            if (typeof this._httpOptions.headers.apiKey !== 'undefined') {
                resolve(this._httpOptions.headers.apiKey)
            } else {
                const req = this.execute('USER', null, 'getApiKey', loginParams)
                ;(req as Promise<any>).then((getApiKeyData) => {
                    if (getApiKeyData.result === '') {
                        const req2 = this.execute('USER', null, 'generateApiKey', loginParams)
                        ;(req2 as Promise<any>).then((generateApiKeyData) => {
                            this._httpOptions.headers.apiKey = generateApiKeyData.result
                            resolve(this._httpOptions.headers.apiKey)
                        }, reject)
                    } else {
                        this._httpOptions.headers.apiKey = getApiKeyData.result
                        resolve(this._httpOptions.headers.apiKey)
                    }
                }, reject)
            }
        })
    }

    /**
     * Copies an existing object with making changes on a copy.
     * Copying is supported only for some objects. The {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} page displays which objects support the Copy action.
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {String} objID    ID of object to copy
     * @param {Object} updates    Which fields to set on copied object. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @param {String|String[]} [fields]    Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @param {String[]} options    A list of options that are attached to the copy request (object specific)
     * @return {Promise}    A promise which will resolved with results if everything went ok and rejected otherwise
     */
    copy(objCode: string, objID: string, updates: object, fields?: TFields, options?: string[]) {
        const params: {
            copySourceID: string
            updates?: string
            options?: string
        } = {
            copySourceID: objID,
        }
        if (updates) {
            params.updates = JSON.stringify(updates)
        }
        if (options) {
            params.options = JSON.stringify(options)
        }
        return this.request(objCode, params, fields, Api.Methods.POST)
    }

    /**
     * Used to retrieve number of objects matching given search criteria
     * @memberOf Api
     * @param {String} objCode
     * @param {[Object]} query    An object with search criteria
     * @return {Promise}
     */
    count(objCode: string, query?: object): Promise<number> {
        const req = this.request(objCode + '/count', query, null, Api.Methods.GET)
        if (this._uriGenerationMode) {
            return req
        }
        return (req as Promise<any>).then(function (data) {
            return data.count
        })
    }

    /**
     * Invalidates the current API key.
     * Call this to be able to retrieve a new one using getApiKey().
     * @memberOf Api
     * @return {Promise}    A promise which will resolved if everything went ok and rejected otherwise
     */
    clearApiKey() {
        return new Promise<void>((resolve, reject) => {
            const req = this.execute('USER', null, 'clearApiKey')
            req.then((result) => {
                if (result) {
                    delete this._httpOptions.headers.apiKey
                    resolve()
                } else {
                    reject()
                }
            })
        })
    }

    /**
     * Creates a new object.
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {Object} params    Values of fields to be set for the new object. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @param {String|String[]} [fields]    Which fields of newly created object to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @returns {Promise}    A promise which will resolved with the ID and any other specified fields of newly created object
     */
    create(objCode: string, params: any, fields?: TFields) {
        if (params.hasOwnProperty('updates')) {
            return this.request(objCode, params, fields, Api.Methods.POST)
        }
        return this.request(objCode, {updates: params}, fields, Api.Methods.POST)
    }

    /**
     * Edits an existing object
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {String} objID    ID of object to modify
     * @param {Object} updates    Which fields to set. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @param {String|String[]} [fields]    Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @return {Promise}    A promise which will resolved with results if everything went ok and rejected otherwise
     */
    edit(objCode: string, objID: string, updates: any, fields?: TFields) {
        if (updates.hasOwnProperty('updates')) {
            return this.request(objCode + '/' + objID, updates, fields, Api.Methods.PUT)
        }
        return this.request(objCode + '/' + objID, {updates: updates}, fields, Api.Methods.PUT)
    }

    /**
     * Edit multiple existing objects
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {Array} updates    Array of fields for each object to be edited. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @param {String|String[]} [fields]    Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @return {Promise}    A promise which will resolved with results if everything went ok and rejected otherwise
     */
    editMultiple(objCode: string, updates: any[], fields?: TFields) {
        return this.request(objCode, {updates: updates}, fields, Api.Methods.PUT)
    }

    /**
     * Executes an action for the given object
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {String|null} objID    ID of object. Optional, pass null or undefined to omit
     * @param {String} action    An action to execute. A list of allowed actions are available within the {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} under "actions" for each object.
     * @param {Object} [actionArgs]    Optional. Arguments for the action. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of valid arguments
     * @returns {Promise}    A promise which will resolved if everything went ok and rejected otherwise
     */
    execute(objCode: string, objID: string | null, action: string, actionArgs?: object) {
        let endPoint = objCode
        let params: any = {method: Api.Methods.PUT}
        if (objID) {
            endPoint += '/' + objID + '/' + action
        } else {
            params.action = action
        }
        if (actionArgs) {
            params = {...params, ...actionArgs}
        }
        return this.request(endPoint, params, null, Api.Methods.POST)
    }

    /**
     * Used for retrieve an object or multiple objects.
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {String|Array} objIDs    Either one or multiple object ids
     * @param {String|String[]} fields    Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @return {Promise}    A promise which will resolved with results if everything went ok and rejected otherwise
     */
    get(objCode: string, objIDs: string | string[], fields?: TFields) {
        if (typeof objIDs === 'string') {
            objIDs = [objIDs]
        }
        let endPoint = objCode,
            params = null
        if (objIDs.length === 1) {
            if (objIDs[0].indexOf(INTERNAL_PREFIX) === 0) {
                params = {id: objIDs[0]}
            } else {
                endPoint += '/' + objIDs[0]
            }
        } else {
            params = {id: objIDs}
        }
        return this.request(endPoint, params, fields, Api.Methods.GET)
    }

    /**
     * Logs in into Workfront. Should be a first call to Workfront API.
     * Other calls should be made after this one will be completed.
     * @memberOf Api
     * @param {String} username    A username in Workfront
     * @param {String} password    Password to use
     * @param {String} subdomain    Sub-domain to use
     * @return {Promise}    A promise which will resolved with logged in user data if everything went ok and rejected otherwise
     */
    login(username: string, password: string, subdomain?: string) {
        const params = {username, password}
        if (subdomain !== undefined) {
            params['subdomain'] = subdomain
        }
        const req = this.request('login', params, null, Api.Methods.POST)
        return (req as Promise<any>).then((data) => {
            this.setSessionID(data.sessionID)
            return data
        })
    }

    /**
     * Logs out from Workfront
     * @memberOf Api
     * @return {Promise}    A promise which will resolved if everything went ok and rejected otherwise
     */
    logout(): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            const req = this.request('logout', null, null, Api.Methods.GET)
            req.then((result) => {
                if (result && result.success) {
                    delete this._httpOptions.headers['X-XSRF-TOKEN']
                    delete this._httpOptions.headers.sessionID
                    resolve()
                } else {
                    reject()
                }
            })
        })
    }

    /**
     * Retrieves API metadata for an object.
     * @memberOf Api
     * @param {String} [objCode]    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}. If omitted will return list of objects available in API.
     * @param {String|String[]} fields    Which fields to return.
     * @return {Promise}    A promise which will resolved with object metadata if everything went ok and rejected otherwise
     */
    metadata(objCode?: string, fields?: TFields) {
        let path = '/metadata'
        if (objCode) {
            path = objCode + path
        }
        return this.request(path, null, fields, Api.Methods.GET)
    }

    /**
     * Executes a named query for the given obj code
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {String} query    A query to execute. A list of allowed named queries are available within the {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} under "actions" for each object.
     * @param {Object} [queryArgs]    Optional. Arguments for the action. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of valid arguments
     * @param {String|String[]} fields    Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @returns {Promise}    A promise which will resolved with received data if everything went ok and rejected with error info otherwise
     */
    namedQuery(objCode: string, query: string, queryArgs?: object, fields?: TFields) {
        return this.request(objCode + '/' + query, queryArgs, fields, Api.Methods.GET)
    }

    /**
     * Deletes an object
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {String} objID    ID of object
     * @param {Boolean} [bForce]    Pass true to cause the server to remove the specified data and its dependants
     * @returns {Promise}    A promise which will resolved if everything went ok and rejected otherwise
     */
    remove(objCode: string, objID: string, bForce?: boolean): Promise<void> {
        const params = bForce ? {force: true} : null
        const req = this.request(objCode + '/' + objID, params, null, Api.Methods.DELETE)

        if (this._uriGenerationMode) {
            return req
        } else {
            return new Promise<void>((resolve, reject) => {
                ;(req as Promise<any>).then((result) => {
                    if (result && result.success) {
                        resolve()
                    } else {
                        reject()
                    }
                }, reject)
            })
        }
    }

    /**
     * Performs report request, where only the aggregate of some field is desired, with one or more groupings.
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {Object} query    An object with search criteria and aggregate functions
     * @param {Boolean} [useHttpPost=false] Whenever to use POST to send query params
     * @return {Promise}    A promise which will resolved with results if everything went ok and rejected otherwise
     */
    report(objCode: string, query: object, useHttpPost = false) {
        let reportQuery, method
        if (useHttpPost) {
            reportQuery = {...query, method: Api.Methods.GET}
            method = Api.Methods.POST
        } else {
            reportQuery = query
            method = Api.Methods.GET
        }
        return this.request(objCode + '/report', reportQuery, null, method)
    }

    /**
     * Do the request using Fetch API.
     * @memberOf Api
     * @param {String} path     URI path where the request calls
     * @param {Object} params   An object with params
     * @param {Object} [fields] Fields to query for the request
     * @param {String} [method=GET] The method which the request will do (GET|POST|PUT|DELETE)
     * @return {Promise}    A promise which will resolved with results if everything went ok and rejected otherwise
     */
    request(
        path: string,
        params,
        fields: TFields = [],
        method: string = Api.Methods.GET
    ): Promise<any> {
        const clonedParams = {...params}

        const options = this.getOptions(
            path,
            clonedParams,
            this._uriGenerationMode ? Api.Methods.GET : method
        )

        const stringifiedFields = this.getFields(fields)
        if (stringifiedFields) {
            clonedParams.fields = stringifiedFields
        }

        const headers = this.getHeaders()

        const {bodyParams, queryString, contentType} = this.populateQueryStringAndBodyParams(
            clonedParams,
            options
        )
        if (contentType) {
            headers.append('Content-Type', contentType)
        }

        if (this._uriGenerationMode) {
            let appendGetMethod = ''
            if (queryString.indexOf('method=') === -1) {
                appendGetMethod = (queryString === '' ? '?' : '&') + 'method=' + Api.Methods.GET
            }
            // @ts-ignore-line
            return path + queryString + appendGetMethod
        }
        return makeFetchCall(options.url + options.path + queryString, {
            headers,
            body: bodyParams,
            method: options.method,
        })
    }

    /**
     * Used for object retrieval by multiple search criteria.
     * @memberOf Api
     * @param {String} objCode    One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
     * @param {Object} [query]    An object with search criteria
     * @param {String|String[]} [fields]    Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
     * @param {Boolean} [useHttpPost=false] Whenever to use POST to send query params
     * @return {Promise}    A promise which will resolved with search results if everything went ok and rejected otherwise
     */
    search(objCode: string, query?: object, fields?: TFields, useHttpPost = false) {
        let searchQuery, method
        if (useHttpPost) {
            searchQuery = {...query, method: Api.Methods.GET}
            method = Api.Methods.POST
        } else {
            searchQuery = query
            method = Api.Methods.GET
        }
        return this.request(objCode + '/search', searchQuery, fields, method)
    }

    /**
     * Performs batch call to the API.
     * @memberOf Api
     *
     * @param {(batchApi: IBatchApi) => string[]} uriCollector   A function which will be invoked with api instance.
     *     This instance is special, as all methods there return a url string instead of making a backend call.
     *     `uriCollector` should return an array of uris to be executed in batch.
     *     So, for example, one may return `[batchApi.metadata(), batchApi.count(...)]` from `uriCollector`.
     *     That will mean `call metadata() method` and then `call count() method`.
     *
     * @param {boolean} isAtomic    Pass true if you want all operations to happen in the same transaction.
     *     There is a limitation, however. Atomic batch operations can only return success or error.
     *
     * @param {boolean} isConcurrent  Requests to the DB are made asynchronously.
     *
     * @returns {Promise<any[] | void>}
     */
    batch(
        uriCollector: (batchApi: IBatchApi) => string[],
        isAtomic?: false,
        isConcurrent?: boolean
    ): Promise<any[]>
    batch(
        uriCollector: (batchApi: IBatchApi) => string[],
        isAtomic?: true,
        isConcurrent?: boolean
    ): Promise<void>
    batch(
        uriCollector: (batchApi: IBatchApi) => string[],
        isAtomic?: boolean,
        isConcurrent?: boolean
    ): Promise<any[] | void> {
        const batchApi = batchApiFactory(this)
        const uris = uriCollector(batchApi)
        if (uris.length === 0) {
            return Promise.resolve(isAtomic ? undefined : [])
        }
        const req = this.request(
            '/batch',
            {
                atomic: !!isAtomic,
                uri: uris,
                concurrent: !!isConcurrent,
            },
            undefined,
            Api.Methods.POST
        )
        if (isAtomic) {
            return req.then((result) => {
                if (result && result.success) {
                    return undefined
                }
                throw new Error()
            })
        }
        return req.then((results) => {
            return results.map((resultItem) => resultItem.data)
        })
    }

    /**
     * Sets a current API key for future requests
     * @memberOf Api
     * @return {string} returns the given api key value
     */
    setApiKey(apiKey) {
        return (this._httpOptions.headers.apiKey = apiKey)
    }

    /**
     * Sets a sessionID in the headers or removes sessionID if passed argument is undefined
     * @memberOf Api
     * @param {String|undefined} sessionID   sessionID to set
     */
    setSessionID(sessionID) {
        if (sessionID) {
            this._httpOptions.headers.sessionID = sessionID
        } else {
            delete this._httpOptions.headers.sessionID
        }
    }

    /**
     * Sets a 'X-XSRF-TOKEN' in the headers or removes 'X-XSRF-TOKEN' if passed argument is undefined
     * @memberOf Api
     * @param {String|undefined} xsrfToken   X-XSRF-TOKEN to set
     */
    setXSRFToken(xsrfToken?: string) {
        if (xsrfToken) {
            this._httpOptions.headers['X-XSRF-TOKEN'] = xsrfToken
        } else {
            delete this._httpOptions.headers['X-XSRF-TOKEN']
        }
    }

    uploadFileContent(fileContent, filename: string) {
        const data = new FormData()
        data.append('uploadedFile', fileContent, filename)
        return this.request('upload', data, null, Api.Methods.POST)
    }

    protected getHeaders() {
        const headers = new Headers()
        headers.append('X-Requested-With', 'XMLHttpRequest')
        if (this._httpOptions.headers.sessionID) {
            headers.append('sessionID', this._httpOptions.headers.sessionID)
        } else if (this._httpOptions.headers['X-XSRF-TOKEN']) {
            headers.append('X-XSRF-TOKEN', this._httpOptions.headers['X-XSRF-TOKEN'])
        } else if (this._httpOptions.headers.apiKey) {
            headers.append('apiKey', this._httpOptions.headers.apiKey)
        }
        return headers
    }

    private getFields(fields: TFields): string | undefined {
        if (typeof fields === 'string') {
            return fields
        }
        if (Array.isArray(fields)) {
            return fields.join(',')
        }
    }

    private getOptions(path, clonedParams, method) {
        const options = {...this._httpOptions}

        if (options.alwaysUseGet && path !== 'login') {
            clonedParams.method = method
            options.method = Api.Methods.GET
        } else {
            options.method = method
        }

        if (path.indexOf('/') !== 0) {
            path = '/' + path
        }
        options.path = this._httpOptions.path + path
        return options
    }

    private populateQueryStringAndBodyParams(clonedParams, options) {
        let bodyParams = null,
            queryString = '',
            contentType = null
        if (typeof FormData !== 'undefined' && clonedParams instanceof FormData) {
            bodyParams = clonedParams
        } else if (
            this.serverAcceptsJSON &&
            typeof clonedParams.updates === 'object' &&
            (options.method === Api.Methods.POST || options.method === Api.Methods.PUT)
        ) {
            contentType = 'application/json'
            bodyParams = JSON.stringify(clonedParams.updates)

            delete clonedParams.updates
            const qs = queryStringify(clonedParams)
            if (qs) {
                queryString = '?' + qs
            }
        } else {
            contentType = 'application/x-www-form-urlencoded'
            if (
                clonedParams.hasOwnProperty('updates') &&
                typeof clonedParams.updates !== 'string'
            ) {
                clonedParams.updates = JSON.stringify(clonedParams.updates)
            }
            bodyParams = queryStringify(clonedParams)
            if (options.method === Api.Methods.GET || options.method === Api.Methods.DELETE) {
                if (bodyParams) {
                    queryString = '?' + bodyParams
                }
                bodyParams = null
            }
        }
        return {
            bodyParams,
            queryString,
            contentType,
        }
    }
}

const queryStringify = function (params) {
    return Object.keys(params)
        .reduce(function (a, k) {
            if (Array.isArray(params[k])) {
                params[k].forEach(function (param) {
                    a.push(k + '=' + encodeURIComponent(param))
                })
            } else {
                a.push(k + '=' + encodeURIComponent(params[k]))
            }
            return a
        }, [])
        .join('&')
}

export interface IBatchApi {
    copy: (
        objCode: string,
        objID: string,
        updates: object,
        fields?: TFields,
        options?: string[]
    ) => string
    count: (objCode: string, query?: object) => string
    create: (objCode: string, params: any, fields?: TFields) => string
    edit: (objCode: string, objID: string, updates: any, fields?: TFields) => string
    editMultiple: (objCode: string, updates: any[], fields?: TFields) => string
    execute: (objCode: string, objID: string | null, action: string, actionArgs?: object) => string
    get: (objCode: string, objIDs: string | string[], fields?: TFields) => string
    metadata: (objCode?: string, fields?: TFields) => string
    namedQuery: (objCode: string, query: string, queryArgs?: object, fields?: TFields) => string
    remove: (objCode: string, objID: string, bForce?: boolean) => string
    report: (objCode: string, query: object) => string
    request: (path: string, params, fields?: TFields, method?: string) => string
    search: (objCode: string, query?: object, fields?: TFields) => string
}

function batchApiFactory(api: Api): IBatchApi {
    const apiClone = Object.create(api) as Api
    apiClone._uriGenerationMode = true
    return {
        copy: (
            objCode: string,
            objID: string,
            updates: object,
            fields?: TFields,
            options?: string[]
        ) => {
            return apiClone.copy(objCode, objID, updates, fields, options) as any as string
        },
        count: (objCode: string, query?: object) => {
            return apiClone.count(objCode, query) as any as string
        },
        create: (objCode: string, params: any, fields?: TFields) => {
            return apiClone.create(objCode, params, fields) as any as string
        },
        edit: (objCode: string, objID: string, updates: any, fields?: TFields) => {
            return apiClone.edit(objCode, objID, updates, fields) as any as string
        },
        editMultiple: (objCode: string, updates: any[], fields?: TFields) => {
            return apiClone.editMultiple(objCode, updates, fields) as any as string
        },
        execute: (objCode: string, objID: string | null, action: string, actionArgs?: object) => {
            return apiClone.execute(objCode, objID, action, actionArgs) as any as string
        },
        get: (objCode: string, objIDs: string | string[], fields?: TFields) => {
            return apiClone.get(objCode, objIDs, fields) as any as string
        },
        metadata: (objCode?: string, fields?: TFields) => {
            return apiClone.metadata(objCode, fields) as any as string
        },
        namedQuery: (objCode: string, query: string, queryArgs?: object, fields?: TFields) => {
            return apiClone.namedQuery(objCode, query, queryArgs, fields) as any as string
        },
        remove: (objCode: string, objID: string, bForce?: boolean) => {
            return apiClone.remove(objCode, objID, bForce) as any as string
        },
        report: (objCode: string, query: object) => {
            return apiClone.report(objCode, query) as any as string
        },
        request: (path: string, params, fields?: TFields, method: string = Api.Methods.GET) => {
            return apiClone.request(path, params, fields, method) as any as string
        },
        search: (objCode: string, query?: object, fields?: TFields) => {
            return apiClone.search(objCode, query, fields, false) as any as string
        },
    }
}

export type TSuccessHandler<T = any> = (response: any) => Promise<T>
export type TFailureHandler = (err: any) => never

export function makeFetchCall(url: string, fetchOptions: RequestInit) {
    return fetch(url, {...fetchOptions, credentials: 'same-origin'}).then(
        ResponseHandler.success,
        ResponseHandler.failure
    )
}

export const ResponseHandler: {
    success: TSuccessHandler<any>
    failure: TFailureHandler
} = {
    success: (response) => {
        if (response.ok) {
            return response.json().then((data) => {
                if (data.error) {
                    throw {
                        status: response.status,
                        message: data.error.message,
                    }
                }
                return data.data
            })
        } else {
            return response.json().then(
                (data) => {
                    throw {
                        status: response.status,
                        message: data.error.message,
                    }
                },
                () => {
                    throw {
                        status: response.status,
                        message: response.statusText,
                    }
                }
            )
        }
    },
    failure: (err) => {
        throw {
            message: err.message || err.statusText,
        }
    },
}