Skip to content

Commit

Permalink
Merge pull request #31 from skibz/async-req-transform
Browse files Browse the repository at this point in the history
Makes async request transforms a thing.  Thanks @skibz
  • Loading branch information
skellock authored Feb 6, 2017
2 parents 62510db + 35950d2 commit e668def
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 18 deletions.
65 changes: 47 additions & 18 deletions lib/apisauce.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from 'axios'
import R from 'ramda'
import RS from 'ramdasauce'
import pWaterfall from 'p-waterfall'

// check for an invalid config
const isInvalidConfig = R.anyPass([
Expand Down Expand Up @@ -57,9 +58,11 @@ export const create = (config) => {
}

const requestTransforms = []
const asyncRequestTransforms = []
const responseTransforms = []

const addRequestTransform = transform => requestTransforms.push(transform)
const addAsyncRequestTransform = transform => asyncRequestTransforms.push(transform)
const addResponseTransform = transform => responseTransforms.push(transform)

// convenience for setting new request headers
Expand Down Expand Up @@ -89,46 +92,70 @@ export const create = (config) => {
return doRequest(R.merge({ url, method, data }, axiosConfig))
}

const doRequestTransforms = (transforms, requestConfig) => {
// create an object to feed through the request transforms
const request = R.pick(['url', 'method', 'data', 'headers', 'params'], requestConfig)

// go go go!
R.forEach(transform => transform(request), transforms)

// overwrite our axios request with whatever our object looks like now
return R.merge(requestConfig, request)
}

const doAsyncRequestTransforms = async (transforms, requestConfig) => {
// create an object to feed through the request transforms
const request = R.pick(['url', 'method', 'data', 'headers', 'params'], requestConfig)

// await the promise pipeline and return the merged request config
return R.mergeAll([
requestConfig,
request,
...(await pWaterfall(transforms, request))
])
}

/**
Make the request with this config!
*/
const doRequest = (axiosRequestConfig) => {
const startedAt = RS.toNumber(new Date())
const doRequest = async (axiosRequestConfig) => {

axiosRequestConfig.headers = { ...headers, ...axiosRequestConfig.headers }
// add the request transforms
if (requestTransforms.length > 0) {
// create an object to feed through the request transforms
const request = R.pick(['url', 'method', 'data', 'headers', 'params'], axiosRequestConfig)
axiosRequestConfig.headers = {
...headers,
...axiosRequestConfig.headers
}

// go go go!
R.forEach(transform => transform(request), requestTransforms)
// add the async request transforms
if (asyncRequestTransforms.length > 0) {
// overwrite our axios request with whatever our object looks like now
axiosRequestConfig = await doAsyncRequestTransforms(asyncRequestTransforms, axiosRequestConfig)
}

// add the request transforms
if (requestTransforms.length > 0) {
// overwrite our axios request with whatever our object looks like now
axiosRequestConfig = R.merge(axiosRequestConfig, request)
axiosRequestConfig = doRequestTransforms(requestTransforms, axiosRequestConfig)
}

// first convert the axios response, then execute our callback
const chain = R.pipe(
R.partial(convertResponse, [startedAt]),
R.partial(convertResponse, [RS.toNumber(new Date())]),
runMonitors
)

// Make the request and execute the identical pipeline for both promise paths.
return instance
.request(axiosRequestConfig)
.then(chain)
.catch(chain)
return instance.request(
axiosRequestConfig
).then(chain).catch(chain)
}

/**
Fires after we convert from axios' response into our response. Exceptions
raised for each monitor will be ignored.
*/
const runMonitors = (ourResponse) => {
monitors.forEach((fn) => {
monitors.forEach((monitor) => {
try {
fn(ourResponse)
monitor(ourResponse)
} catch (error) {
// all monitor complaints will be ignored
}
Expand Down Expand Up @@ -202,8 +229,10 @@ export const create = (config) => {
monitors,
addMonitor,
requestTransforms,
asyncRequestTransforms,
responseTransforms,
addRequestTransform,
addAsyncRequestTransform,
addResponseTransform,
setHeader,
setHeaders,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"dependencies": {
"axios": "^0.15.3",
"ramda": "^0.23.0",
"p-waterfall": "^1.0.0",
"ramdasauce": "^1.1.1"
},
"devDependencies": {
Expand Down
203 changes: 203 additions & 0 deletions test/asyncRequestTransformTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@

import test from 'ava'
import {create} from '../lib/apisauce'
import createServer from '../support/server'
import R from 'ramda'
import getFreePort from '../support/getFreePort'

const MOCK = {b: 1}
let port
let server = null

test.before(async t => {
port = await getFreePort()
server = createServer(port, MOCK)
})

test.after('cleanup', (t) => {
server.close()
})

test('attaches an async request transform', (t) => {
const api = create({ baseURL: `http://localhost:${port}` })
t.truthy(api.addAsyncRequestTransform)
t.truthy(api.asyncRequestTransforms)
t.is(api.asyncRequestTransforms.length, 0)
api.addAsyncRequestTransform(R.identity)
t.is(api.asyncRequestTransforms.length, 1)
})

test('alters the request data', (t) => {
const x = create({ baseURL: `http://localhost:${port}` })
let count = 0
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
t.is(count, 0)
count = 1
t.is(req.data.b, 1)
req.data.b = 2
resolve(req)
})
})
})
return x.post('/post', MOCK).then(response => {
t.is(response.status, 200)
t.is(count, 1)
t.is(response.data.got.b, 2)
})
})

test('transformers should run serially', (t) => {
const x = create({ baseURL: `http://localhost:${port}` })
let first = false
let second = false
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
t.is(second, false)
t.is(first, false)
first = true
resolve(req)
})
})
})
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
t.is(first, true)
t.is(second, false)
second = true
resolve(req)
})
})
})
return x.post('/post', MOCK).then(response => {
t.is(response.status, 200)
t.is(first, true)
t.is(second, true)
})
})

test('survives empty PUTs', (t) => {
const x = create({ baseURL: `http://localhost:${port}` })
let count = 0
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
count++
resolve(req)
})
})
})
t.is(count, 0)
return x.put('/post', {}).then(response => {
t.is(response.status, 200)
t.is(count, 1)
})
})

test('fires for gets', (t) => {
const x = create({ baseURL: `http://localhost:${port}` })
let count = 0
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
count++
resolve(req)
})
})
})
t.is(count, 0)
return x.get('/number/201').then(response => {
t.is(response.status, 201)
t.is(count, 1)
t.deepEqual(response.data, MOCK)
})
})

test('url can be changed', t => {
const x = create({ baseURL: `http://localhost:${port}` })
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
req.url = R.replace('/201', '/200', req.url)
resolve(req)
})
})
})
return x.get('/number/201', {x: 1}).then(response => {
t.is(response.status, 200)
})
})

test('params can be added, edited, and deleted', t => {
const x = create({ baseURL: `http://localhost:${port}` })
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
req.params.x = 2
req.params.y = 1
delete req.params.z
resolve(req)
})
})
})
return x.get('/number/200', {x: 1, z: 4}).then(response => {
t.is(response.status, 200)
t.is(response.config.params.x, 2)
t.is(response.config.params.y, 1)
t.falsy(response.config.params.z)
})
})

test('headers can be created', t => {
const x = create({ baseURL: `http://localhost:${port}` })
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
t.falsy(req.headers['X-APISAUCE'])
req.headers['X-APISAUCE'] = 'new'
resolve(req)
})
})
})
return x.get('/number/201', {x: 1}).then(response => {
t.is(response.status, 201)
t.is(response.config.headers['X-APISAUCE'], 'new')
})
})

test('headers from creation time can be changed', t => {
const x = create({ baseURL: `http://localhost:${port}`, headers: { 'X-APISAUCE': 'hello' } })
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
t.is(req.headers['X-APISAUCE'], 'hello')
req.headers['X-APISAUCE'] = 'change'
resolve(req)
})
})
})
return x.get('/number/201', {x: 1}).then(response => {
t.is(response.status, 201)
t.is(response.config.headers['X-APISAUCE'], 'change')
})
})

test('headers can be deleted', t => {
const x = create({ baseURL: `http://localhost:${port}`, headers: { 'X-APISAUCE': 'omg' } })
x.addAsyncRequestTransform(req => {
return new Promise((resolve, reject) => {
setImmediate(_ => {
t.is(req.headers['X-APISAUCE'], 'omg')
delete req.headers['X-APISAUCE']
resolve(req)
})
})
})
return x.get('/number/201', {x: 1}).then(response => {
t.is(response.status, 201)
t.falsy(response.config.headers['X-APISAUCE'])
})
})

0 comments on commit e668def

Please sign in to comment.