Skip to content

Commit

Permalink
feat: Add support for multiple route for same type of response
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasdao committed Dec 12, 2017
1 parent da4a632 commit 1395c88
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 34 deletions.
20 changes: 11 additions & 9 deletions src/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,30 @@
*/
const pathToRegexp = require('path-to-regexp')

/**
* Converts one or many route string templates to a route detail objects with specific info about that route.
* From 'users/{username}/account/{id}' to { "name": "/users/{username}/account/{id}/", "params": [ "username", "id"], "regex": {} }
*
* @param {String|[String]} route Uri paths
* @return {[Object]} e.g. [{ "name": "/users/{username}/account/{id}/", "params": [ "username", "id"], "regex": {} }]
*/
const getRouteDetails = (route='') => (typeof(route) == 'string' ? [route] : route).map(r => _getRouteDetails(r))

/**
* Converts a route string template to a route detail object with specific info about that route.
* From 'users/{username}/account/{id}' to { "name": "/users/{username}/account/{id}/", "params": [ "username", "id"], "regex": {} }
*
* @param {String} route Uri path
* @return {Object} e.g. { "name": "/users/{username}/account/{id}/", "params": [ "username", "id"], "regex": {} }
*/
const getRouteDetails = route => {
const _getRouteDetails = route => {
let wellFormattedRoute = (route.trim().match(/\/$/) ? route.trim() : route.trim() + '/')
wellFormattedRoute = wellFormattedRoute.match(/^\//) ? wellFormattedRoute : '/' + wellFormattedRoute

const keys = []
const rx = pathToRegexp(wellFormattedRoute, keys)
const variableNames = keys.map(x => x.name)

// const variables = wellFormattedRoute.match(/{(.*?)}/g) || []
// variables.push(...(wellFormattedRoute.match(/:(.*?)\//g) || [])) // add support for standard express routing convention
// const variableNames = variables.map(x => x.replace(/^{|:/, '').replace(/}|\/$/, ''))

// const routeRegex = variables.reduce((a, v) => a.replace(v, '(.*?)'), wellFormattedRoute).toLowerCase()
// const rx = new RegExp(routeRegex)


return {
name: wellFormattedRoute,
params: variableNames,
Expand Down
58 changes: 40 additions & 18 deletions src/webfunc.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,11 @@ const handleHttpRequest = (req, res, appconfig) => Promise.resolve(appconfig ||
* Returns a function (req, res) => ... that the Google Cloud Function expects.
*
* @param {String|Function|Array|Object} arg1 Here what it means based on its type:
* - String: Route path (e.g. '/users/{userId}/account')
* - String: Route path (e.g. '/users/:userId/account')
* - Function: Callback function (req, res) => ... This gets executed after all the headers checks.
* - Array: Array of endpoints (e.g. [app.get('/users', (req, res, params) => ...), app.post('/stories', (req, res, params) => ...)])
* - Array:
* - Array of endpoints (e.g. [app.get('/users', (req, res, params) => ...), app.post('/stories', (req, res, params) => ...)])
* - Array of strings of routes (e.g. ['/users/:userId/account', '/users/:userId/portfolio'])
* - Object: Endpoint (e.g. app.get('/users', (req, res, params) => ...))
* @param {Function|Object} arg2 Here what it means based on its type:
* - Function: Callback function (req, res) => ... This gets executed after all the headers checks.
Expand All @@ -140,40 +142,54 @@ const serveHttp = (arg1, arg2, arg3) => {
const appConfigFile = getAppConfig() || {}
const appConfigArg = arg3 || {}
let _appconfig = null
let route = null
let routes = null
let httpNextRequest = null
const typeOfArg1 = typeof(arg1 || undefined)
const typeOfArg2 = typeof(arg2 || undefined)

if (arg1) {
if (typeOfArg1 == 'string') {
route = getRouteDetails(arg1)
routes = getRouteDetails(arg1)
_appconfig = Object.assign(appConfigFile, appConfigArg)
if (typeOfArg2 == 'function')
httpNextRequest = arg2
else
throw new Error('Wrong argument exception. If the first argument of the \'serveHttp\' method is a route, then the second argument must be a function similar to (req, res, params) => ...')
throw new Error('Wrong argument exception. If the first argument of the \'serveHttp\' or \'serve\' method is a route, then the second argument must be a function similar to (req, res, params) => ...')
}
else {
_appconfig = Object.assign(appConfigFile, arg2 || {})
// 1. arg1 is a function (req, res) => ...
if (typeOfArg1 == 'function')
httpNextRequest = arg1
else if (arg1.length != undefined)
return serveHttpEndpoints(arg1, _appconfig)
// 2. arg1 is an Array
else if (arg1.length != undefined && arg1.length > 0) {
// 2.1. arg1 is an array of routes
if (typeof(arg1[0]) == 'string') {
routes = getRouteDetails(arg1)
if (typeOfArg2 == 'function')
httpNextRequest = arg2
else
throw new Error('Wrong argument exception. If the first argument of the \'serveHttp\' or \'serve\' method is a route, then the second argument must be a function similar to (req, res, params) => ...')
}
// 2.2. arg1 is an array of endpoints
else
return serveHttpEndpoints(arg1, _appconfig)
}
// 3. arg1 is an endpoint object
else if (typeOfArg1 == 'object')
return serveHttpEndpoints([arg1], _appconfig)
else
throw new Error('Wrong argument exception. If the first argument of the \'serveHttp\' method is not a route, then it must either be a function similar to (req, res, params) => ... or an array of endpoints.')
throw new Error('Wrong argument exception. If the first argument of the \'serveHttp\' or \'serve\' method is not a route, then it must either be a function similar to (req, res, params) => ... or an array of endpoints.')
}
}
else
throw new Error('Wrong argument exception. The first argument of the \'serveHttp\' method must either be a route, a function similar to (req, res, params) => ... or an array of endpoints.')
throw new Error('Wrong argument exception. The first argument of the \'serveHttp\' or \'serve\' method must either be a route, a function similar to (req, res, params) => ... or an array of endpoints.')

const cloudFunction = (req, res) => {
let parameters = {}
if (route) {
if (routes) {
const httpEndpoint = ((req._parsedUrl || {}).pathname || '/').toLowerCase()
const r = matchRoute(httpEndpoint, route)
const r = (routes.map(route => matchRoute(httpEndpoint, route)).filter(route => route) || [])[0]
if (!r) {
return setResponseHeaders(res, _appconfig).then(res => {
res.status(404).send(`Endpoint '${httpEndpoint}' not found.`)
Expand Down Expand Up @@ -250,6 +266,8 @@ const getRequestParameters = req => {
return parameters
}

const getLongestRoute = (routes=[]) => routes.sort((a,b) => b.match.length - a.match.length)[0]

/**
* Returns a function (req, res) => ... that the Google Cloud Function expects.
*
Expand All @@ -268,22 +286,26 @@ const serveHttpEndpoints = (endpoints, appconfig) => {
const httpEndpoint = ((req._parsedUrl || {}).pathname || '/').toLowerCase()
const httpMethod = (req.method || '').toUpperCase()
const endpoint = httpEndpoint == '/'
? endpoints.filter(e => e.route.name == '/' && (e.method == httpMethod || !e.method))[0]
: (endpoints.map(e => ({ endpoint: e, route: matchRoute(httpEndpoint, e.route) }))
.filter(e => e.route && (e.endpoint.method == httpMethod || !e.endpoint.method))
.sort((a, b) => b.route.match.length - a.route.match.length)[0] || {}).endpoint
? endpoints.filter(e => e.route.some(x => x.name == '/') && (e.method == httpMethod || !e.method))[0]
: (endpoints.map(e => ({ endpoint: e, route: e.route.map(r => matchRoute(httpEndpoint, r)).filter(r => r) }))
.filter(e => e.route.length > 0 && (e.endpoint.method == httpMethod || !e.endpoint.method))
.map(e => {
const winningRoute = getLongestRoute(e.route)
e.endpoint.winningRoute = winningRoute
return { endpoint: e.endpoint, winningRoute: winningRoute }
})
.sort((a, b) => b.winningRoute.match.length - a.winningRoute.match.length)[0] || {}).endpoint

if (!endpoint)
return res.status(404).send(`Endpoint '${httpEndpoint}' for method ${httpMethod} not found.`)

const next = endpoint.next || (() => Promise.resolve(null))
if (typeof(next) != 'function')
return res.status(500).send(`Wrong argument exception. Endpoint '${httpEndpoint}' for method ${httpMethod} defines a 'next' argument that is not a function similar to '(req, res, params) => ...'.`)

const parameters = getRequestParameters(req)
const requestParameters = matchRoute(httpEndpoint, endpoint.route).parameters

return next(req, res, Object.assign(parameters, requestParameters))
return next(req, res, Object.assign(parameters, endpoint.winningRoute.parameters))
})
: res)
.then(() => ({ req, res }))
Expand Down
10 changes: 6 additions & 4 deletions test/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ describe('routing', () =>
describe('#getRouteDetails', () =>
it(`Should support the express standard for routing variables, i.e. support for ':'.`, () => {
/*eslint-enable */
const route = getRouteDetails('users/:username/account/:id')
const routes = getRouteDetails('users/:username/account/:id')
assert.equal(routes.length, 1)
const route = routes[0]
assert.equal(route.name, '/users/:username/account/:id/')
assert.isOk(route.params)
assert.equal(route.params.length, 2)
Expand All @@ -26,12 +28,12 @@ describe('routing', () =>
describe('#matchRoute', () =>
it(`Should analyse a route and extract details from it.`, () => {
/*eslint-enable */
let routeDetails = getRouteDetails('users/:username/account/:id/(.*)')
let details = matchRoute('/users/nic/account/1/blabla', routeDetails)
const routes = getRouteDetails('users/:username/account/:id/(.*)')
let details = matchRoute('/users/nic/account/1/blabla', routes[0])
assert.isOk(details)
assert.isOk(details.parameters)
assert.equal(details.parameters.username, 'nic')
assert.equal(details.parameters.id, '1')
details = matchRoute('/user/nic/account/1/blabla', routeDetails)
details = matchRoute('/user/nic/account/1/blabla', routes[0])
assert.isOk(!details)
})))
71 changes: 68 additions & 3 deletions test/webfunc.js
Original file line number Diff line number Diff line change
Expand Up @@ -914,9 +914,9 @@ describe('webfunc', () =>
})
]

assert.throws(() => serveHttp('/users/:username', endpoints, appconfig), Error, 'Wrong argument exception. If the first argument of the \'serveHttp\' method is a route, then the second argument must be a function similar to (req, res, params) => ...')
assert.throws(() => serveHttp('/users/:username', appconfig), Error, 'Wrong argument exception. If the first argument of the \'serveHttp\' method is a route, then the second argument must be a function similar to (req, res, params) => ...')
assert.throws(() => serveHttp(), Error, 'Wrong argument exception. The first argument of the \'serveHttp\' method must either be a route, a function similar to (req, res, params) => ... or an array of endpoints.')
assert.throws(() => serveHttp('/users/:username', endpoints, appconfig), Error, 'Wrong argument exception. If the first argument of the \'serveHttp\' or \'serve\' method is a route, then the second argument must be a function similar to (req, res, params) => ...')
assert.throws(() => serveHttp('/users/:username', appconfig), Error, 'Wrong argument exception. If the first argument of the \'serveHttp\' or \'serve\' method is a route, then the second argument must be a function similar to (req, res, params) => ...')
assert.throws(() => serveHttp(), Error, 'Wrong argument exception. The first argument of the \'serveHttp\' or \'serve\' method must either be a route, a function similar to (req, res, params) => ... or an array of endpoints.')
})))

/*eslint-disable */
Expand Down Expand Up @@ -1347,6 +1347,71 @@ describe('webfunc', () =>
return Promise.all([result_01, result_02])
})))

/*eslint-disable */
describe('webfunc', () =>
describe('#serveHttp: 21', () =>
it(`Should support collection of routes for a single response type.`, () => {
/*eslint-enable */
const req_01 = httpMocks.createRequest({
method: 'GET',
headers: {
origin: 'http://localhost:8080',
referer: 'http://localhost:8080'
},
_parsedUrl: {
pathname: '/users/1'
}
})
const res_01 = httpMocks.createResponse()

const req_02 = httpMocks.createRequest({
method: 'GET',
headers: {
origin: 'http://localhost:8080',
referer: 'http://localhost:8080'
},
_parsedUrl: {
pathname: '/companies/2'
}
})
const res_02 = httpMocks.createResponse()

const appconfig = {
headers: {
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS, POST',
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Origin',
'Access-Control-Allow-Origin': 'http://boris.com, http://localhost:8080',
'Access-Control-Max-Age': '1296000'
}
}

const endpoints = [
app.get(['/users/:userId', '/companies/:companyId'], (req, res, params) => { res.status(200).send(`Hello No. ${params.userId || params.companyId}`); return res })
]

const fn = serveHttp(endpoints, appconfig)

const result_01 = fn(req_01, res_01).then(() => {
assert.equal(res_01._getData(),'Hello No. 1')
const headers = res_01._getHeaders()
assert.isOk(headers)
assert.equal(headers['Access-Control-Allow-Methods'], 'GET, HEAD, OPTIONS, POST')
assert.equal(headers['Access-Control-Allow-Headers'], 'Authorization, Content-Type, Origin')
assert.equal(headers['Access-Control-Allow-Origin'], 'http://boris.com, http://localhost:8080')
assert.equal(headers['Access-Control-Max-Age'], '1296000')
})
const result_02 = fn(req_02, res_02).then(() => {
assert.equal(res_02._getData(),'Hello No. 2')
const headers = res_02._getHeaders()
assert.isOk(headers)
assert.equal(headers['Access-Control-Allow-Methods'], 'GET, HEAD, OPTIONS, POST')
assert.equal(headers['Access-Control-Allow-Headers'], 'Authorization, Content-Type, Origin')
assert.equal(headers['Access-Control-Allow-Origin'], 'http://boris.com, http://localhost:8080')
assert.equal(headers['Access-Control-Max-Age'], '1296000')
})

return Promise.all([result_01, result_02])
})))



Expand Down

0 comments on commit 1395c88

Please sign in to comment.