papir
Version:
Rest API Modelling library
493 lines (435 loc) • 14.6 kB
JavaScript
import util from './util'
import axios from 'axios'
import sign from './sign'
import { clone } from './util'
/**
* Requester
*/
export default class Requester {
constructor (customConf = {}) {
if (typeof customConf.key !== 'undefined') {
localStorage.setItem('papir.key', customConf.key)
}
if (typeof customConf.secret !== 'undefined') {
localStorage.setItem('papir.secret', customConf.secret)
}
if (typeof customConf.token !== 'undefined' && customConf.token.constructor === Object) {
localStorage.setItem('papir.token', JSON.stringify(customConf.token))
}
this.conf = {
responseType: 'json', // ['arraybuffer', 'blob', 'document', 'json', 'text', 'stream']
headers: {},
override: {
arg: '_method', // The query argument to be given actual method
method: null // The replacement method. (will be fired instead of actual method. Ex.: 'OPTIONS')
},
name: null, // Root key where data is attached in data
addDataToQuery: true,
keepEmpty: true,
keepEmptyInArrays: true,
keepEmptyArray: true,
keepNull: true,
keepNullInArrays: true,
protocol: 'rfc3986',
delimiter: '&',
splitter: '=',
divider: '?',
encodeNames: true,
encodeValues: true,
encodeNull: true,
excludes: [],
includes: [],
dotNotation: false,
authQuery: true,
authHeader: false,
dateFormat: '',
authentication: 'oauth',
version: '1.0a',
type: 'one_legged',
algorithm: 'HMAC-SHA1',
key: (localStorage.getItem('papir.key') !== null ? localStorage.getItem('papir.key') : ''),
secret: (localStorage.getItem('papir.secret') !== null ? localStorage.getItem('papir.secret') : ''),
token: (localStorage.getItem('papir.token') !== null ? JSON.parse(localStorage.getItem('papir.token')) : { key: '', secret: '' }),
nonce: '',
nonceLength: 6,
nonceTale: '',
timestampLength: 30,
indexArrays: true,
emptyArrayToZero: false,
keepArrayTags: true,
requester: null,
base64: true,
ampersand: true,
sort: true,
// Conf specific per method type. (Same Options as above)
get: {},
post: {
keepNull: false,
keepEmpty: false,
keepEmptyArray: false
},
put: {},
patch: {},
delete: {},
head: {},
trace: {},
connect: {},
options: {},
perform: true // If false, axios config will be returned instead
}
this.objMerge = (target, custom) => {
let cl = Object.assign({}, target) // Ensures target not to inherit params from custom
return Object.assign(cl, custom)
}
this.conf = this.objMerge(this.conf, customConf)
this.getConf = this.objMerge(this.conf, this.conf.get)
this.postConf = this.objMerge(this.conf, this.conf.post)
this.putConf = this.objMerge(this.conf, this.conf.put)
this.patchConf = this.objMerge(this.conf, this.conf.patch)
this.deleteConf = this.objMerge(this.conf, this.conf.delete)
this.headConf = this.objMerge(this.conf, this.conf.head)
this.traceConf = this.objMerge(this.conf, this.conf.trace)
this.connectConf = this.objMerge(this.conf, this.conf.connect)
this.optionsConf = this.objMerge(this.conf, this.conf.options)
// READ
this.get = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.getConf, conf)
return this.custom('GET', url, promise, data, upload, conf)
}
// CREATE
this.post = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.postConf, conf)
return this.custom('POST', url, promise, data, upload, conf)
}
// UPDATE / REPLACE
this.put = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.putConf, conf)
return this.custom('PUT', url, promise, data, upload, conf)
}
// UPDATE / MODIFY
this.patch = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.patchConf, conf)
return this.custom('PATCH', url, promise, data, upload, conf)
}
// DELETE
this.delete = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.deleteConf, conf)
return this.custom('DELETE', url, promise, data, upload, conf)
}
// GET HEADERS ONLY / NO CONTENT
this.head = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.headConf, conf)
return this.custom('HEAD', url, promise, data, upload, conf)
}
// GET ADDITIONS / CHANGES
this.trace = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.traceConf, conf)
return this.custom('TRACE', url, promise, data, upload, conf)
}
// CONVERT TO TCP / IP TUNNEL
this.connect = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.connectConf, conf)
return this.custom('CONNECT', url, promise, data, upload, conf)
}
// PERMISSION
this.options = (
url,
promise,
data = null,
upload = false,
conf = {}
) => {
conf = this.objMerge(this.optionsConf, conf)
return this.custom('OPTIONS', url, promise, data, upload, conf)
}
this.custom = (
method,
url,
promise,
data = null,
upload = false,
conf = this.conf
) => {
conf = this.objMerge(this.conf, conf)
return this.request(method, url, promise, data, upload, conf)
}
this.request = (
method,
url,
abortPromise,
data = null,
upload = false,
conf = this.conf
) => {
/**
* Correct order of creating a request:
*/
let request = {}
// 1. Append protocol (http / https)
// 2. Append base (://baseurl.com)
// 3. Append path (/api/v1/users/362)
// 4.1 Append arguments (?arg1=0&arg2=1)
request.url = url
// 4.2 Encode arguments after first divider until second divider.
// Ex.: ignored?encoded?ignored?ignored
request.url = util.querystring.encode(request.url, {
protocol: conf.protocol,
divider: conf.divider,
delimiter: conf.delimiter,
splitter: conf.splitter,
encodeNull: conf.encodeNull,
keepEmpty: conf.keepEmpty,
encodeNames: conf.encodeNames,
encodeValues: conf.encodeValues
})
// 5.1 Append data to querystring arguments if required
if (conf.addDataToQuery && !upload) {
request.url = this.makeDataQuery(request.url, data, {
name: conf.name, // Root key where data is attached
protocol: conf.protocol,
encodeNull: conf.encodeNull,
dateFormat: conf.dateFormat, // Default ISO 8601
keepEmpty: conf.keepEmpty,
keepNull: conf.keepNull,
keepNullInArrays: conf.keepNullInArrays,
keepEmptyArray: conf.keepEmptyArray,
keepEmptyInArrays: conf.keepEmptyInArrays,
delimiter: conf.delimiter,
splitter: conf.splitter,
dotNotation: conf.dotNotation,
encodeNames: conf.encodeNames,
encodeValues: conf.encodeValues,
indexArrays: conf.indexArrays,
excludes: conf.excludes, // At first level
includes: conf.includes, // At first level. includes overrides excludes
emptyArrayToZero: conf.emptyArrayToZero,
keepArrayTags: conf.keepArrayTags
}, conf)
data = null
}
// 5.2. Append index to arrays in querystring if required
if (conf.indexArrays) {
request.url = util.querystring.indexArrays(request.url)
}
// 6 Sort arguments if required
let querystring = ''
let sortable = []
util.getParams(request.url).forEach(param => {
sortable.push(param.key + conf.splitter + param.value + conf.delimiter)
})
sortable.sort()
sortable.forEach(param => {
querystring += param
})
if (querystring !== '') {
querystring = querystring.slice(0, -1)
request.url = util.stripUri(request.url) + conf.divider + querystring
} else {
request.url = util.stripUri(request.url)
}
// 7. Append data to request
if (data !== null) {
request.data = data
}
// 8. Append method
request.method = method
// 9. Append method override if required
if (conf.override.method !== null && method !== conf.override.method) {
request.method = conf.override.method
request.url += request.url.indexOf(conf.divider) === -1 ? conf.divider : conf.delimiter
request.url += conf.override.arg + conf.splitter + method
}
// 10. Append headers
/**
* @note - Simple headers which passes preflight:
* Accept (This should be declared in api config. [what response content does the client accept?]) 'application/json'
* Accept-Language
* Content-Language (This should be declared in api config. [what request content does the client send?]) 'application/json'
* Content-Type [application/x-www-form-urlencoded, multipart/form-data, text/plain] (Others creates preflight)
* DPR
* Downlink
* Save-Data
* Viewport-Width
* Width
* (Other headers creates preflight)
*/
// Get predefined headers
request.headers = conf.headers
// If no data provided, set content-type to text/plain so preflight is avoided
if (typeof request.data === 'undefined') {
request.headers['Content-Type'] = 'text/plain'
}
// If data provided and it is a upload request, tell the server
if (upload) {
// @todo - Could be changed to multipart/form-data for multiupload, multi content later?
request.headers['Content-Type'] = 'application/x-www-form-urlencoded'
}
// 11. Authorize
if (typeof conf.authentication === 'function') {
request = conf.authentication(request, conf)
} else if (conf.authentication === 'oauth') {
// OAUTH
// 1. Prepare configuration to be signed
conf.url = request.url
conf.method = request.method
// 2. If authentication should be applied to querystring
if (conf.authQuery) {
request.url = util.stripUri(request.url) + conf.divider + sign.gen(conf).string
}
// 3. If authentication should be applied to header
if (conf.authHeader) {
request.headers['Authorization'] = sign.gen(conf).header
}
} else if (conf.authentication === 'nonce') {
// ADD TOKEN / NONCE AT END OF QUERYSTRING
request = this.makeTale(request, conf)
}
// 12. Make request abortable
request = this.makeAbortable(request, abortPromise)
// 13. Transform response
// Set response type ['arraybuffer', 'blob', 'document', 'json', 'text', 'stream']
request.responseType = conf.responseType
// Transform Response to raw
request.transformResponse = (response) => {
return response
}
// 14. If request should be applied, perform and return
if (conf.perform) {
return axios.request(request)
}
// 15. If only the axios config object is needed, return resolved promise
return new Promise(resolve => {
resolve(request)
})
}
this.makeDataQuery = (url, data, options, conf = this.conf) => {
if (data !== null) {
let queryString = util.querystring.stringify(data, options)
if (url.indexOf(conf.divider) === -1 && queryString !== '') {
url += conf.divider + queryString
} else if (queryString !== '') {
url += conf.delimiter + queryString
}
}
return url
}
this.makeTale = (request, conf = this.conf) => {
// Set nonce based on localized object / var
if (conf.nonceTale !== '') {
let query = conf.nonceTale.split(conf.splitter)
if (query.length === 2) {
let param = query[0]
let hook = query[1]
if (param.length > 0) {
try {
// @warning - eval can be harmful if used server side
/* eslint-disable */
let nonce = String(eval(hook))
/* eslint-enable */
if (request.url.indexOf(conf.divider) === -1) {
request.url += conf.divider + param + conf.splitter + nonce
} else {
request.url += conf.delimiter + param + conf.splitter + nonce
}
} catch (e) {
console.error(e)
}
}
}
}
return request
}
this.makeAbortable = (request, promise) => {
let cancel
request.cancelToken = axios.CancelToken(function executor (c) {
cancel = c
})
promise.then(() => {
cancel()
})
return request
}
this.verifyToken = (url, token = '', conf = this.conf) => {
window.open(url + conf.divider + (token !== '' ? util.querystring.stringify({ oauth_token: token }) : ''),
'_blank'
)
}
this.getTokenRequest = (url) => {
let scope = this
let conf = clone({}, this.getConf)
conf.addDataToQuery = false
conf.authHeader = true
return new Promise((resolve, reject) => {
scope.get(url, null, false, false, conf).then(res => {
resolve(res)
}).catch(e => {
reject(e)
})
})
}
this.getTokenAccess = (url, requestToken, requestTokenSecret, verifierToken) => {
url = url + '?oauth_verifier=' + verifierToken
let scope = this
let conf = clone({}, this.getConf)
conf.addDataToQuery = false
conf.authHeader = true
conf.key = requestToken
conf.secret = requestTokenSecret
return new Promise((resolve, reject) => {
scope.get(url, null, false, false, conf).then(res => {
resolve(res)
}).catch(e => {
reject(e)
})
})
}
}
}