UNPKG

@stackend/api

Version:

JS bindings to api.stackend.com

305 lines (275 loc) 8.07 kB
//import log4js from 'log4js'; import { logger, Parameters, XcapJsonResult } from './index'; require('es6-promise').polyfill(); require('isomorphic-fetch'); /** * Content type for json data * @type {string} */ export const CONTENT_TYPE_JSON = 'application/json'; /** * Content type for form data * @type {string} */ export const CONTENT_TYPE_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded'; /** * Url encode a parameter object. * @param parameters * @returns {String} */ export function urlEncodeParameters(parameters: Parameters | null | undefined): string { if (typeof parameters === 'string') { return parameters; } if (typeof parameters === 'undefined' || parameters === null) { return ''; } let f = true; let s = ''; for (const k of Object.keys(parameters)) { const v = parameters[k]; if (v !== null) { if (Array.isArray(v)) { for (const p of v) { if (f) { f = false; } else { s += '&'; } s += encodeURIComponent(k); if (typeof p !== 'undefined' && p != null) { s += '=' + encodeURIComponent(p); } } } else { if (f) { f = false; } else { s += '&'; } s += encodeURIComponent(k); if (typeof v !== 'undefined') { s += '=' + encodeURIComponent(v); } } } } return s; } /** * Append to the query string * @param url * @param queryString * @return {String} */ export function appendQueryString(url: string, queryString: string): string { let u = url; if (queryString && queryString !== '') { if (u.indexOf('?') !== -1) { u = u + '&' + queryString; } else { u = u + '?' + queryString; } } return u; } export interface LoadJsonResult { status: number; error?: string; response: Response | null; json?: XcapJsonResult; } export const DEFAULT_OPTIONS: RequestInit = { mode: 'cors', cache: 'default' //credentials: 'include', }; /** * Load an url as json. Supports GET and POST (using application/x-www-form-urlencoded or a json body) * * @param url {String} * @param method {String} Optional method string. * @param parameters {Object} Parameters * @param body Optional body content. May be an object or string. * @param bodyContentType Mime type of the body content (optional) * @param xpressToken * @param cookie Optional cookie string * @constructor */ export async function LoadJson({ url, method = 'GET', parameters = null, body = null, bodyContentType = CONTENT_TYPE_X_WWW_FORM_URLENCODED, xpressToken, cookie, options }: { url: string; method?: string; parameters?: any; body?: any; bodyContentType?: string; xpressToken?: string; cookie?: string | null; options?: RequestInit; }): Promise<LoadJsonResult> { //console.log("Fetching json: ",url); //console.trace("Fetching json: ",url); let headers; if (typeof Headers === 'function') { headers = new Headers(); headers.set('x-custom-headless', 'true'); headers.set('Accept', CONTENT_TYPE_JSON); // In most cases handled by the browser but supplied in SSR initial data fetch if (typeof cookie === 'string') { headers.set('cookie', cookie); } } const opts: RequestInit = { ...DEFAULT_OPTIONS, method, headers, body: undefined }; if (options) { Object.assign(opts, options); } // Encode json if needed if (body && bodyContentType === CONTENT_TYPE_JSON && typeof body === 'object') { body = JSON.stringify(body); } let queryString = ''; if (method === 'POST') { if (bodyContentType !== null) { if (body && typeof body.toString === 'function' && body.toString().indexOf('FormData') !== -1) { // This is a file upload. Don't set the content type, // since that would remove the boundary parameter for the binary data. } else if (headers) { headers.set('Content-Type', bodyContentType); } } // FIXME: Should use xpress ajax token that includes checksumming of parameters if (xpressToken && !!headers) { headers.append('x-custom-xpress-token', xpressToken); } // When body is already set, add the parameters to the url instead if (body) { queryString = urlEncodeParameters(parameters); } else if (bodyContentType === CONTENT_TYPE_JSON) { body = JSON.stringify(parameters); } else { //body = createFormData(parameters); body = urlEncodeParameters(parameters); } opts.body = body; } else { queryString = urlEncodeParameters(parameters); } url = appendQueryString(url, queryString); let response = null; let request; try { if (typeof Headers === 'function' && typeof Request === 'function') { request = new Request(url, opts); response = await fetch(request); } else { response = await fetch(url, opts); } // FIXME: Should throw to encourage proper error handling and not cause any weird errors in the data layer. if (!response.ok) { if (response.status === 401 || response.status === 404 || response.status === 500) { logger.error('The Fetch request of ' + url + ' failed: ' + response.status + response.statusText); return { error: response.status + ': ' + response.statusText, status: response.status, response }; } else { const status = response.status ? 'Error Status:' + response.status : ''; logger.error( status + ' The Fetch request of ' + url + ' failed: ' + JSON.stringify(typeof request !== 'undefined' ? request : null) + JSON.stringify(response) ); return { error: response.status + ': ' + response.statusText, status: response.status, response }; } } const contentType = response.headers.get('content-type'); if (contentType && contentType.indexOf(CONTENT_TYPE_JSON) > -1) { const json = (await response.json()) as XcapJsonResult; const r: LoadJsonResult = { status: 200, json: json, response }; // The check for __resultCode exist because of media upload that does not produce the same json if ( json.__resultCode && (json.__resultCode === 'error' || json.__resultCode === 'accessDenied' || json.__resultCode === 'notfound') ) { r.error = json.__resultCode; // Add error messages, if not done by server if (!json.error) { json.error = { actionErrors: ['error'], fieldErrors: {} }; } } return r; } else { logger.error( 'Error Status:500 The Fetch request of ' + url + ' failed, Wrong content-type.' + JSON.stringify(typeof request !== 'undefined' ? request : null) + JSON.stringify(response) ); return { error: 'Response is not a json object', status: 200, response }; } } catch (e: any) { logger.error(e, request ? JSON.stringify(request) : ''); if (e?.message === 'Failed to fetch') { return { error: "Can't access stackend api. Please make sure you've allowed this domain in your stack settings.", status: 500, response }; } return { error: e, status: 500, response }; } } export default LoadJson; /** * Given a set of parameters, construct a FormData object that can be used as the body of a request * @param parameters * @return {FormData} */ export function createFormData(parameters: { [key: string]: any }): FormData { const formData = new FormData(); if (typeof parameters !== 'undefined') { for (const i in parameters) { if (Object.prototype.hasOwnProperty.call(parameters, i)) { formData.append(i, String(parameters[i])); } } } return formData; }