Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.10.0 #91

Merged
merged 37 commits into from
Jan 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
126bfbf
[issues-72] add support for multi-value query parameters
Dec 5, 2018
20d2eed
version bump and package updates
jeremydaly Dec 5, 2018
0bce299
delete package-lock.json
jeremydaly Dec 5, 2018
c91d94d
Merge branch 'master' of https://github.com/GuidoNebiolo/lambda-api i…
jeremydaly Dec 5, 2018
f130242
Merge branch 'GuidoNebiolo-master' into v0.10.0
jeremydaly Dec 5, 2018
43b0f13
[issue-72] test case for multiValueQueryStringParameters support
Dec 5, 2018
c4d7d2b
fix #77 with updated declaration
jeremydaly Dec 21, 2018
9b29529
version bump
jeremydaly Dec 21, 2018
e130b99
close #78 by adding wildcard route support
jeremydaly Dec 22, 2018
8367713
update wildcard route documentation
jeremydaly Dec 22, 2018
872b277
close #85 by adding sourceIp fallback
jeremydaly Dec 22, 2018
176597b
additional test cases for #72
jeremydaly Dec 22, 2018
882ab0d
Merge pull request #83 from GuidoNebiolo/feature/issue-72
jeremydaly Dec 22, 2018
1545475
reimplement multi-value query params
jeremydaly Dec 22, 2018
a878dd2
add documentation for multiValueQuery
jeremydaly Dec 22, 2018
cdea6c9
wip support for albs #80
jeremydaly Dec 22, 2018
60ca34a
add interface detection to REQUEST parsing
jeremydaly Dec 22, 2018
454005d
add documentation for REQUEST interface detection
jeremydaly Dec 22, 2018
09f6596
add multi-value support detection for #72 and #73, simulate multiValu…
jeremydaly Dec 23, 2018
e572f6c
add query fallback support for alb multi-value
jeremydaly Dec 23, 2018
3fce7ef
fix query fallback and tests
jeremydaly Dec 23, 2018
c2e0f2f
wip for #73 - request header parsing support
jeremydaly Dec 23, 2018
446044b
fix linting error
jeremydaly Dec 24, 2018
c2cfe6b
close #73 with multi-value header support and test updates
jeremydaly Dec 24, 2018
96687cb
close #89 by adding multi-value querystring support to logging
jeremydaly Dec 26, 2018
7d6c169
close #88 by adding interface to logs
jeremydaly Dec 26, 2018
776dde2
close #80 with initial ALB support
jeremydaly Dec 26, 2018
20303b2
add tests for statusDescriptions
jeremydaly Dec 26, 2018
2990ada
add sendStatus support
jeremydaly Dec 26, 2018
01a59e9
documentation for sendStatus
jeremydaly Dec 26, 2018
87a718f
documentation fix for sendStatus
jeremydaly Dec 26, 2018
ebbb62e
close #87 by adding new execution stack processing
jeremydaly Jan 3, 2019
149ff6c
added test coverage and misc fixes
jeremydaly Jan 4, 2019
34bb29b
fix 'route not found' error with wildcard paths
jeremydaly Jan 6, 2019
125993b
add 'stack' property to REQUEST
jeremydaly Jan 7, 2019
4642275
update documentation
jeremydaly Jan 7, 2019
11ed69b
documentation updates
jeremydaly Jan 7, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 136 additions & 86 deletions README.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ export declare class API {

finally(callback: FinallyFunction): void;

run(event: APIGatewayEvent, context: Context): {};
run(event: APIGatewayEvent, context: Context, cb: (err: Error, result: any) => void): void;
run(event: APIGatewayEvent, context: Context): Promise<any>;
}

export declare class RouteError extends Error {
Expand Down
248 changes: 161 additions & 87 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* Lightweight web framework for your serverless applications
* @author Jeremy Daly <jeremy@jeremydaly.com>
* @version 0.9.0
* @version 0.10.0
* @license MIT
*/

Expand Down Expand Up @@ -58,9 +58,6 @@ class API {
// Init callback
this._cb

// Middleware stack
this._middleware = []

// Error middleware stack
this._errors = []

Expand All @@ -73,27 +70,31 @@ class API {
// Global error status (used for response parsing errors)
this._errorStatus = 500

} // end constructor
// Methods
this._methods = ['get','post','put','patch','delete','options','head','any']

// Convenience methods for METHOD
this._methods.forEach(m => {
this[m] = (...a) => this.METHOD(m.toUpperCase(),...a)
})

} // end constructor

// Convenience methods (path, handler)
get(p,h) { this.METHOD('GET',p,h) }
post(p,h) { this.METHOD('POST',p,h) }
put(p,h) { this.METHOD('PUT',p,h) }
patch(p,h) { this.METHOD('PATCH',p,h) }
delete(p,h) { this.METHOD('DELETE',p,h) }
options(p,h) { this.METHOD('OPTIONS',p,h) }
head(p,h) { this.METHOD('HEAD',p,h) }
any(p,h) { this.METHOD('ANY',p,h) }
// METHOD: Adds method, middleware, and handlers to routes
METHOD(method,...args) {

// Extract path if provided, otherwise default to global wildcard
let path = typeof args[0] === 'string' ? args.shift() : '/*'

// METHOD: Adds method and handler to routes
METHOD(method, path, handler) {
// Extract the execution stack
let stack = args.map((fn,i) => {
if (typeof fn === 'function' && (fn.length === 3 || (fn.length === 2 && i === args.length-1)))
return fn
throw new ConfigurationError('Route-based middleware must have 3 parameters')
})

if (typeof handler !== 'function') {
throw new ConfigurationError(`No route handler specified for ${method} method on ${path} route.`)
}
if (stack.length === 0)
throw new ConfigurationError(`No handler or middleware specified for ${method} method on ${path} route.`)

// Ensure method is an array
let methods = Array.isArray(method) ? method : method.split(',')
Expand All @@ -105,46 +106,101 @@ class API {
let route = this._prefix.concat(parsedPath)

// For root path support
if (route.length === 0) { route.push('')}
if (route.length === 0) { route.push('') }

// Keep track of path variables
let pathVars = {}

// Make a local copy of routes
let routes = this._routes

// Create a local stack for inheritance
let _stack = {}

// Loop through the paths
for (let i=0; i<route.length; i++) {

let end = i === route.length-1

// If this is a variable
if (/^:(.*)$/.test(route[i])) {
// Assign it to the pathVars (trim off the : at the beginning)
pathVars[i] = route[i].substr(1)
pathVars[i] = [route[i].substr(1)]
// Set the route to __VAR__
route[i] = '__VAR__'
} // end if variable

// Add methods to routess
methods.forEach(_method => {
if (typeof _method === 'string') {

if (routes['ROUTES']) {

// Wildcard routes
if (routes['ROUTES']['*']) {

// Inherit middleware
if (routes['ROUTES']['*']['MIDDLEWARE']) {
_stack[method] = routes['ROUTES']['*']['MIDDLEWARE'].stack
//_stack[method] ?
// _stack[method].concat(routes['ROUTES']['*']['MIDDLEWARE'].stack)
// : routes['ROUTES']['*']['MIDDLEWARE'].stack
}

// Inherit methods and ANY
if (routes['ROUTES']['*']['METHODS'] && routes['ROUTES']['*']['METHODS']) {
['ANY',method].forEach(m => {
if (routes['ROUTES']['*']['METHODS'][m]) {
_stack[method] = _stack[method] ?
_stack[method].concat(routes['ROUTES']['*']['METHODS'][m].stack)
: routes['ROUTES']['*']['METHODS'][m].stack
}
}) // end for
}
}

// Matching routes
if (routes['ROUTES'][route[i]]) {

// Inherit middleware
if (end && routes['ROUTES'][route[i]]['MIDDLEWARE']) {
_stack[method] = _stack[method] ?
_stack[method].concat(routes['ROUTES'][route[i]]['MIDDLEWARE'].stack)
: routes['ROUTES'][route[i]]['MIDDLEWARE'].stack
}

// Inherit ANY methods (DISABLED)
// if (end && routes['ROUTES'][route[i]]['METHODS'] && routes['ROUTES'][route[i]]['METHODS']['ANY']) {
// _stack[method] = _stack[method] ?
// _stack[method].concat(routes['ROUTES'][route[i]]['METHODS']['ANY'].stack)
// : routes['ROUTES'][route[i]]['METHODS']['ANY'].stack
// }
}
}

// Add the route to the global _routes
this.setRoute(
this._routes,
(i === route.length-1 ? {
['__'+_method.trim().toUpperCase()]: {
vars: pathVars,
handler: handler,
route: '/'+parsedPath.join('/'),
path: '/'+this._prefix.concat(parsedPath).join('/') }
} : {}),
_method.trim().toUpperCase(),
(end ? {
vars: pathVars,
stack,
inherited: _stack[method] ? _stack[method] : [],
route: '/'+parsedPath.join('/'),
path: '/'+this._prefix.concat(parsedPath).join('/')
} : null),
route.slice(0,i+1)
)

}
}) // end methods loop

routes = routes['ROUTES'][route[i]]

} // end for loop

} // end main METHOD function



// RUN: This runs the routes
async run(event,context,cb) {

Expand All @@ -162,44 +218,24 @@ class API {
// Parse the request
await request.parseRequest()

// Loop through the middleware and await response
for (const mw of this._middleware) {
// Only run middleware if in processing state
// Loop through the execution stack
for (const fn of request._stack) {
// Only run if in processing state
if (response._state !== 'processing') break

// Init for matching routes
let matched = false

// Test paths if they are supplied
for (const path of mw[0]) {
if (
path === request.path || // If exact path match
path === request.route || // If exact route match
// If a wildcard match
(path.substr(-1) === '*' && new RegExp('^' + path.slice(0, -1) + '.*$').test(request.route))
) {
matched = true
break
}
}

if (mw[0].length > 0 && !matched) continue

// Promisify middleware
await new Promise(async r => {
let rtn = await mw[1](request,response,() => { r() })
if (rtn) response.send(rtn)
if (response._state === 'done') r() // if state is done, resolve promise
try {
let rtn = await fn(request,response,() => { r() })
if (rtn) response.send(rtn)
if (response._state === 'done') r() // if state is done, resolve promise
} catch(e) {
await this.catchErrors(e,response)
r() // resolve the promise
}
})

} // end for

// Execute the primary handler if in processing state
if (response._state === 'processing') {
let rtn = await request._handler(request,response)
if (rtn) response.send(rtn)
}

} catch(e) {
await this.catchErrors(e,response)
}
Expand All @@ -214,8 +250,6 @@ class API {
// Catch all async/sync errors
async catchErrors(e,response,code,detail) {

// console.log('\n\n------------------------\n',e,'\n------------------------\n\n');

// Error messages should never be base64 encoded
response._isBase64 = false

Expand Down Expand Up @@ -297,24 +331,34 @@ class API {


// Middleware handler
use(path) {
use(...args) {

// Extract routes
let routes = typeof path === 'string' ? Array.of(path) : (Array.isArray(path) ? path : [])
let routes = typeof args[0] === 'string' ? Array.of(args.shift()) : (Array.isArray(args[0]) ? args.shift() : ['/*'])

// Init middleware stack
let middleware = []

// Add func args as middleware
for (let arg in arguments) {
if (typeof arguments[arg] === 'function') {
if (arguments[arg].length === 3) {
this._middleware.push([routes,arguments[arg]])
} else if (arguments[arg].length === 4) {
this._errors.push(arguments[arg])
for (let arg in args) {
if (typeof args[arg] === 'function') {
if (args[arg].length === 3) {
middleware.push(args[arg])
} else if (args[arg].length === 4) {
this._errors.push(args[arg])
} else {
throw new ConfigurationError('Middleware must have 3 or 4 parameters')
}
}
}

// Add middleware to path
if (middleware.length > 0) {
routes.forEach(route => {
this.METHOD('__MW__',route,...middleware)
})
}

} // end use


Expand All @@ -333,28 +377,58 @@ class API {
return path.trim().replace(/^\/(.*?)(\/)*$/,'$1').split('/').filter(x => x.trim() !== '')
}

// Recursive function to create routes object
setRoute(obj, value, path) {
if (typeof path === 'string') {
path = path.split('.')
}

if (path.length > 1){
// Recursive function to create/merge routes object
setRoute(obj, method, value, path) {
if (path.length > 1) {
let p = path.shift()
if (obj[p] === null) {
obj[p] = {}
}
this.setRoute(obj[p], value, path)
if (p === '*') { throw new ConfigurationError('Wildcards can only be at the end of a route definition') }
this.setRoute(obj['ROUTES'][p], method, value, path)
} else {
if (obj[path[0]] === null) {
obj[path[0]] = value
} else {
obj[path[0]] = Object.assign(value,obj[path[0]])
// Create routes and add path if they don't exist
if (!obj['ROUTES']) obj['ROUTES'] = {}
if (!obj['ROUTES'][path[0]]) obj['ROUTES'][path[0]] = {}

// If a value exists in this iteration
if (value !== null) {

// TEMP: debug
// value._STACK = value.stack.map(x => x.name)
// value._STACK2 = value.inherited.map(x => x.name)

// If mounting middleware
if (method === '__MW__') {
// Merge stacks if middleware exists
if (obj['ROUTES'][path[0]]['MIDDLEWARE']) {
value.stack = obj['ROUTES'][path[0]]['MIDDLEWARE'].stack.concat(value.stack)
value.vars = UTILS.mergeObjects(obj['ROUTES'][path[0]]['MIDDLEWARE'].vars,value.vars)
}

// Add/Update the middleware
obj['ROUTES'][path[0]]['MIDDLEWARE'] = value

// Else if mounting a regular route
} else {

// Create the methods section if it doesn't exist
if (!obj['ROUTES'][path[0]]['METHODS']) obj['ROUTES'][path[0]]['METHODS'] = {}

// Merge stacks if method exists
if (obj['ROUTES'][path[0]]['METHODS'][method]) {
value.stack = obj['ROUTES'][path[0]]['METHODS'][method].stack.concat(value.stack)
value.vars = UTILS.mergeObjects(obj['ROUTES'][path[0]]['METHODS'][method].vars,value.vars)
}

// Add/Update the method
obj['ROUTES'][path[0]]['METHODS'] = Object.assign(
{},obj['ROUTES'][path[0]]['METHODS'],{ [method]: value }
)

}
}

}
} // end setRoute


// Load app packages
app(packages) {

Expand Down
6 changes: 5 additions & 1 deletion lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ exports.config = (config,levels) => {
let access = cfg.access === true ? true : cfg.access === 'never' ? 'never' : false // create access logs
let detail = cfg.detail === true ? true : false // add req/res detail to all logs

let multiValue = cfg.multiValue === true ? true : false // return qs as multiValue

let defaults = {
req: req => {
return {
Expand All @@ -62,7 +64,8 @@ exports.config = (config,levels) => {
device: req.clientType,
country: req.clientCountry,
version: req.version,
qs: Object.keys(req.query).length > 0 ? req.query : undefined
qs: multiValue ? (Object.keys(req.multiValueQuery).length > 0 ? req.multiValueQuery : undefined)
: (Object.keys(req.query).length > 0 ? req.query : undefined)
}
},
res: () => {
Expand Down Expand Up @@ -104,6 +107,7 @@ exports.config = (config,levels) => {
method: req.method,
[messageKey]: msg,
timer: timer(req._start),
int: req.interface,
sample: req._sample ? true : undefined
},
serializers.main(req),
Expand Down
Loading