Repository URL to install this package:
|
Version:
1.2.16 ▾
|
/* eslint-disable max-lines */
/* eslint-disable max-statements */
/* eslint-disable max-statements-per-line */
/* eslint-disable brace-style */
/* eslint-disable curly */
/* eslint-disable strict */
'use strict'; // const {XMLHttpRequest} = require('xmlhttprequest')
// global.fetch = require('node-fetch')
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
const _require = require('buffer'),
Buffer = _require.Buffer;
const qs = require('qs');
const _require2 = require('exotic'),
isFunction = _require2.isFunction,
isString = _require2.isString,
isObjPure = _require2.isObjPure,
isReal = _require2.isReal,
isUndefined = _require2.isUndefined,
isObj = _require2.isObj,
isArray = _require2.isArray,
isNull = _require2.isNull,
isNill = _require2.isNill,
isJSON = _require2.isJSON,
isJSONSafe = _require2.isJSONSafe;
const _require3 = require('chain'),
Chain = _require3.Chain,
merge = _require3.merge,
encase = _require3.encase;
/**
* @todo IS_PROD IS_BROWSER
*/
const fetch = typeof window === 'object' ? window.fetch : global.fetch;
const mergeOpts = {
clone: true // base URI for everything
};
global._options = {
baseURI: 'http://localhost:8080',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
} // scope the old function to the new one like .has .get since they are common
// have a requireable fn
//
// @IMPORTANT TO UPGRADE THE EXPORTING SO EVERYTHING IS FLAT
// NEEDS SOLID CHAIN-ABLE-FS
};
function renameMethod(originalMethodName, newMethodName) {
this[newMethodName] = this[originalMethodName];
}
const toDefaultedCred = cred => isReal(cred) ? cred : '';
const validations = [// 0 fatal.fetch
'A global `fetch` method is required as either `window.fetch` ' + 'for browsers or `global.fetch` for node runtime environments. ' + 'Please add `require(\'isomorphic-fetch\')` before importing `frisbee`. ' + 'You may optionally `require(\'es6-promise\').polyfill()` before you ' + 'require `isomorphic-fetch` if you want to support older browsers.' + '\n\nFor more info: https://github.com/niftylettuce/frisbee#usage', // 1 validate.fatal.baseuri*
'baseURI option is required', // 2 validate.path.string
'`path` must be a string', // 3 validate.opts.obj
'`options` must be an object', // 4 validate.auth.keys
'auth option can only have two keys `[user, pass]`', // 5 validate.auth.user.string
'auth option `user` must be a string', // 6 validate.auth.pass.string
'auth option `pass` must be a string', // 7 validate.auth.jwt.string
'jwt token must be a string', `Invalid JSON received from`]; // @TODO could also just put strings here but then they are allocated each time
// using `_` instead of dot for easier selection
// could also assign to variables but array isn't so bad for order and the eyes and ease
// smaller this way, but changes stack...
const errMsg = msg => {
if (msg === 'req_fetch') return validations[0];else if (msg === 'req_base') return validations[1];else if (msg === 'str_path') return validations[2];else if (msg === 'obj_opts') return validations[3];else if (msg === 'auth_keys') return validations[4];else if (msg === 'str_user') return validations[5];else if (msg === 'str_pass') return validations[6];else if (msg === 'str_jwt') return validations[7];else if (msg === 'json') return validations[8];
};
const throwWithMsg = msg => {
throw new Error(errMsg(msg));
};
if (!fetch) throwWithMsg('req_fetch');
const methods = ['get', 'head', 'post', 'put', 'del', 'options', 'patch'];
const respProperties = {
readOnly: ['headers', 'ok', 'redirected', 'status', 'statusText', 'type', 'url', 'bodyUsed'],
writable: ['useFinalURL'],
callable: ['clone', 'error', 'redirect', 'arrayBuffer', 'blob', 'formData', 'json', 'text']
};
const getContentType = headers => {
if (isNill(headers)) {
return null;
} else if (isFunction(headers.get)) {
return headers.get('Content-Type') || headers.get('content-type');
} else if (isObj(headers)) {
return headers['Content-Type'] || headers['content-type'];
} else {
return false;
}
};
/**
* @NOTE using caseless means checking
* permutations of casings
* encouraging bad practice
* and doing a massive amount of loops
* when it just simply isn't in the headers
*/
const isHeadersJSON = requestOptions => {
const contentType = requestOptions.headers ? getContentType(requestOptions.headers) : false;
return contentType !== false && contentType.split(';')[0] === 'application/json';
}; // @TODO could copy in the typed version to generate comments to generate docs too
// and allows backwards parsing thinking for creating typedefs
//
// clone() - Creates a clone of a Response object.
// error() - Returns a new Response object associated with a network error.
// redirect() - Creates a new response with a different URL.
// arrayBuffer() - Returns a promise that resolves with an ArrayBuffer.
// blob() - Returns a promise that resolves with a Blob.
// formData() - Returns a promise that resolves with a FormData object.
// json() - Returns a promise that resolves with a JSON object.
// text()
// determine whether we're returning text or json for body
// or attempt to parse json body to use as error message
async function parseFrisbeeResponseBody(res, contentTypeJSON) {
// for (var originalProp in res.originalResponse) {
// // if (!isUndefined(res[originalProp])) {
// const has = originalProp in res
// if (has) continue
// Object.defineProperty(res, originalProp, Object.getOwnPropertyDescriptor(res.originalResponse, originalProp) || {})
// // }
// }
try {
if (contentTypeJSON) {
// console.log('is contentTypeJSON')
if (isFunction(res.json)) {
// console.log('is json on response')
try {
// @TODO encase()
res.body = await res.json();
} catch (errorParsingBody) {
// console.log('errored parsing json on response', e)
// return e
res.prettyError = errorParsingBody;
res.originalResponse.statusText = isObj(errorParsingBody) ? errorParsingBody.message : errorParsingBody; // res.originalResponse.err = e
// res.statusText = e.message
// console.log({res})
} // console.log('parsed json on response, done')
} else {
// console.log('is isFoshoJSON - calling text')
res.body = await res.text(); // console.log('is isFoshoJSON -pre')
const isFoshoJSON = isJSONSafe(res.body); // console.log('is isFoshoJSON - pre parse')
// @TODO another fn here could do
// @NOTE good thing to test solidly
if (isJSON(res.body)) {
const onInvalid = error => res.err = error;
res.body = encase(JSON.parse(res.body)).onInvalid(onInvalid); // console.log('error?')
} else {
// console.log('handling it, on own')
res.prettyError = this.handleError('json');
}
}
return res;
} else {
// console.log('LAST ELSE')
res.body = await res.text();
}
} catch (fetchError) {
res.prettyError = fetchError; // console.log('ERROR PARSING', e)
} // console.log('parsed response')
return res;
}
/**
* @TODO needs more features that make axios viable
* easy middleware for local storage & jwt retry que
*/
/* prettier-ignore */
function formatFrisbeeResponseError(res, contentTypeJSON, baseURI) {
// res.err = new Error(res.statusText)
const FrisbeeResponse = res; // new Response(res)
// type - basic, cors
// url
// useFinalURL - Boolean for if url is the final URL
// status - status code (ex: 200, 404, etc.)
// ok - Boolean for successful response (status in the range 200-299)
// statusText - status code (ex: OK)
// headers
// check if the response was JSON, and if so, better the error
if (contentTypeJSON) {
// @TODO Glazed?
// attempt to use Glazed error messages
if (isObj(FrisbeeResponse.body) && isString(FrisbeeResponse.body.message)) {
// @TODO these are the same...?
// FrisbeeResponse.err = new Error(FrisbeeResponse.body.message)
FrisbeeResponse.prettyError = FrisbeeResponse.body;
} // attempt to utilize Stripe-inspired error messages
if (!(isArray(FrisbeeResponse.body) && isObj(FrisbeeResponse.body.error))) {
// was here
res.prettyError = FrisbeeResponse.body.error;
} // if (isObj(res.err)) {
// if (res.err.message) res.err = new Error(res.err.message)
// if (res.err.stack) res.err.stack = (res.err.stack)
// if (res.err.code) res.err.code = (res.err.code)
// if (res.err.param) res.err.param = (res.err.param)
// }
}
return FrisbeeResponse;
} // enhanceError
function createFrisbeeResponse(origResp) {
const resp = {
originalResponse: origResp // curry
// const define = (prop, value) => Object.defineProperty(resp, prop, value)
// console.log('creating frisbee response', {resp})
};
respProperties.readOnly.forEach(prop => Object.defineProperty(resp, prop, {
value: origResp[prop]
}) // && console.log({ prop })
);
respProperties.writable.forEach(prop => Object.defineProperty(resp, prop, {
get() {
return origResp[prop];
},
set(value) {
origResp[prop] = value;
}
}) // && console.log({ prop })
);
let callable;
respProperties.callable.forEach(prop => {
Object.defineProperty(resp, prop, {
// enumerable: true,
value: (callable = origResp[prop], isFunction(callable) && callable.bind(origResp))
}); // && console.log({ prop })
}); // easy vanilla access headers
const headersObj = {};
origResp.headers.forEach(pair => {
headersObj[pair[0]] = pair[1];
});
Object.defineProperty(resp, 'headersObj', {
value: headersObj
}); // const descriptors = obj => {
// const descs = []
// for (let prop in obj) {
// descs.push({ [prop]: Object.getOwnPropertyDescriptor(obj, prop) })
// }
// return descs
// }
// .body
// descriptors(resp.originalResponse).forEach(desc => {
// const prop = Object.keys(desc)[0]
// if (resp[prop]) return
// Object.defineProperty(resp, prop, desc[prop])
// console.log({ prop })
// })
// console.log('created frisbee response')
// console.log(descriptors(resp.originalResponse))
return resp;
}
function copySetToMethodPlugin(name, parent) {
const copySetOntoMethod = arg => {
if (isUndefined(arg)) {
parent.get(name);
} else {
parent.set(name, arg);
Object.assign(parent[name], arg);
}
return parent;
}; // so we know if we defaulted them
copySetOntoMethod.copySetOntoMethod = true;
return this.onSet(copySetOntoMethod).onGet(copySetOntoMethod).onCall(copySetOntoMethod);
} // const makeBody = () => {}
const makeRequest = (url, opts) => {
const requestConfig = {
method: 'POST',
mode: 'cors',
redirect: 'follow',
headers: new Headers({
'Content-Type': 'text/plain'
})
};
Object.assign(requestConfig, opts);
return new Request(url, requestConfig);
};
/**
* @TODO formData (use util)
*/
// const blob = () => {
// fetch('https://davidwalsh.name/submit', {
// method: 'post',
// body: new FormData(document.getElementById('comment-form'))
// });
//
// .then(function(response) {
// return response.blob();
// })
// .then(function(imageBlob) {
// document.querySelector('img').src = URL.createObjectURL(imageBlob);
// });
// }
// easy destructure err
const fetchIt = async (url, opts) => {
let error = null;
try {
const request = makeRequest(url, opts); // console.log({request})
// url, opts
const result = await fetch(request); // console.log(result)
return [error, result];
} catch (fetchException) {
// should be undefined, or empty symbol, or empty string...
return [fetchException, null];
}
};
/* prettier-ignore */
class Frisbee extends Chain {
constructor(opts = {}) {
// ('frisbee'
super(); // because conflicting names
this._get = this.get.bind(this);
this.defaultErrorHandler();
this.method('headers').plugin(copySetToMethodPlugin).build().extend(['arrayFormat']) // .autoGetSet()
// .getSet()
// .build()
.auth(opts.auth).opts(opts).headers(opts.headers).arrayFormat(opts.arrayFormat || 'indices'); // .when(opts.auth, () => this.auth(opts.auth))
methods.forEach(method => {
this[method] = this._setup(method);
}); // already bound
// this.httpGet = this._get
// this._get = (arg1, arg2) => {
// if (isUndefined(arg2))
// }
}
opts(opts) {
// validate
if (!opts.baseURI) this.handleError('req_base');
this.set('opts', opts);
return this;
}
/**
* @access @private
*/
defaultErrorHandler() {
// @default
// wish you could make better stack traces once thrown? extend error??
this.onError(function defaultErrorThrower(error) {
console.log('throwing...', {
error
});
throw error;
}); // try {
// this.method('_setup').encase().onInvalid((error) => {
// require('fliplog').quick(error)
// }).build()
// }
// catch (e) {
// console.log('ugh', e)
// }
} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types
// can have arrays of handlers, middleware, this is baby steps
onError(handler) {
// console.log('onerror')
return this.set('onError', handler);
}
handleError(msg, data) {
const messageForIndex = isString(msg) ? errMsg(msg) : msg; // console.log('handleerror')
const errorPlus = new Error(messageForIndex);
errorPlus.data = data; // isError [object Object] FetchError {
// name: 'FetchError',
// message: 'invalid json response body at http://localhost:8080/404-with-invalid-json reason: Unexpected token o in JSON at position 1',
// type: 'invalid-json'
// console.log(error.message, error.stack)
// throw error
// try {
// const errorPlus = new Error(messageForIndex)
// newest at top, remove this line
// errorPlus.stack = errorPlus.stack.split('\n')
// errorPlus.stack.shift()
// errorPlus.stack = errorPlus.stack.join('\n')
// throw errorPlus
const onerr = this._get('onError'); // console.log({onerr})
// console.log(this.store)
onerr(errorPlus); // }
// // when onerr throws an error
// catch (errorError) {
// console.log(errorError.message)
// console.log(errorError.stack)
// throw errorError
// }
// this._get('onError').call(this, error, this)
return errorPlus;
}
/**
* @methodFactory
* @method
* @memberOf frisbee
* @protected
* @param {string} method enum
* @return {Frisbee} @chainable
*/
_setup(methodKeyword) {
// eslint-disable-next-line
const internalCallableFrisbee = (path = '/', options = {}) => {
// validate ---
// path must be string
if (!isString(path)) return this.handleError('str_path'); // otherwise check if its an object
if (!isObjPure(options)) return this.handleError('obj_opts', options); // console.log('about to get', this)
// require('fliplog').quick(this)
// setup data ---
const _get = this._get('opts'),
baseURI = _get.baseURI; // swappable/placeholder var to use existing them update with merged
let headers = this._get('headers'); // console.log({baseURI, headers}, this)
// --- here down is not tied to any instance ---
// don't want to override param,
// and it's easier to read as a descriptive variable
// @NOTE would be a good transform
const method = methodKeyword === 'del' ? 'DELETE' : methodKeyword.toUpperCase(); // merge headers when we have them
if (isObj(options) && options.headers) {
headers = merge(headers, options.headers, mergeOpts);
}
const opts = merge(options, {
headers,
method
}, mergeOpts); // remove any nil or blank headers
// (e.g. to automatically set Content-Type with `FormData` boundary)
Object.keys(opts.headers).forEach(key => {
if (!isReal(opts.headers[key])) delete opts.headers[key];
});
/**
* in order to support Android POST requests
* we must allow an empty body to be sent
* @see https://github.com/facebook/react-native/issues/4890
*/
if (isUndefined(opts.body)) {
if (opts.method === 'POST') {
opts.body = '';
}
} else if (isObj(opts.body)) {
if (opts.method === 'GET' || opts.method === 'DELETE') {
let qsOpts = null;
if (this.has('arrayFormat')) {
qsOpts = {
arrayFormat: this._get('arrayFormat')
};
}
console.log('QS', qs.stringify(opts.body));
path += `?${qs.stringify(opts.body, qsOpts)}`;
delete opts.body;
} // @TODO: better stringify here
else if (isHeadersJSON(opts)) {
try {
opts.body = JSON.stringify(opts.body);
} catch (parseException) {
this.handleError(parseException);
}
}
} // @TODO does this part here ever throw to wrap try catch?
const dofetch = async () => {
// console.log('do fetch', {path, opts})
const _ref = await fetchIt(baseURI + path, opts),
_ref2 = _slicedToArray(_ref, 2),
error = _ref2[0],
ogRes = _ref2[1]; // console.log('do fetch - PASS')
// simple error
if (!isNill(error)) {
// @TODO @DEV
// console.log('has error', {error})
return this.handleError(error); // return Promise.reject(error)
}
let res = createFrisbeeResponse(ogRes);
const contentType = res.headers.get('Content-Type');
const contentTypeJSON = isString(contentType) && contentType.includes('application/json'); // console.log('enhanced contentType')
const encasedParse = encase(parseFrisbeeResponseBody);
res = await encasedParse(res, contentTypeJSON); // console.log('parsed response body')
if (!res.ok) res = formatFrisbeeResponseError(res, contentTypeJSON, baseURI); // console.log('formatted')
return Promise.resolve(res);
};
return dofetch();
};
return internalCallableFrisbee;
}
/**
* @TODO have option to allow .setEh .getEh & access as normal properties so never `eh()`
*/
delAuth() {
// @TODO this is kind of weird
delete this.headers.Authorization;
return this.delete('headers.Authorization');
}
setAuth(Authorization) {
this.headers.Authorization = Authorization;
return this.set('headers.Authorization', Authorization);
}
auth(creditStringOrArray) {
let creds = creditStringOrArray;
console.log({
creditStringOrArray
}); // if it has :, split into array
if (isString(creds)) {
const index = creds.indexOf(':');
if (index !== -1) {
// aka creds.split(':')
creds = [creds.substr(0, index), creds.substr(index + 1)];
}
} // @TODO argumentor undefined, else array
// @TODO this is no good...
if (!isArray(creds)) creds = [].slice.call(arguments); // essentially padd out our credentials with empty
if (creds.length === 0) creds = ['', ''];else if (creds.length === 1) creds.push('');else if (creds.length !== 2) this.handleError('auth_keys');
creds = creds.map(toDefaultedCred); // console.log({creds})
// @TODO can do 1 step further with validation as in split plugin
if (!isString(creds[0])) this.handleError('str_user');
if (!isString(creds[1])) this.handleError('str_pass');
if (!creds[0] && !creds[1]) this.delAuth();else this.setAuth(`Basic ${Buffer.from(creds.join(':')).toString('base64')}`);
return this;
}
jwt(token) {
if (isNull(token)) return this.delAuth();else if (isString(token)) return this.setAuth(`Bearer ${token}`);else return this.handleError('str_jwt');
}
}
module.exports = function Frisbees(opts) {
return new Frisbee(opts);
};