lambda-api
Advanced tools
Comparing version 0.10.7 to 0.11.0
import { | ||
APIGatewayEvent, | ||
APIGatewayEventRequestContext, | ||
Context | ||
Context, | ||
} from 'aws-lambda'; | ||
@@ -45,6 +45,19 @@ | ||
export declare type Middleware = (req: Request, res: Response, next: () => void) => void; | ||
export declare type ErrorHandlingMiddleware = (error: Error, req: Request, res: Response, next: () => void) => void; | ||
export declare type Middleware = ( | ||
req: Request, | ||
res: Response, | ||
next: () => void | ||
) => void; | ||
export declare type ErrorHandlingMiddleware = ( | ||
error: Error, | ||
req: Request, | ||
res: Response, | ||
next: () => void | ||
) => void; | ||
export declare type ErrorCallback = (error?: Error) => void; | ||
export declare type HandlerFunction = (req: Request, res: Response, next?: NextFunction) => void | any | Promise<any>; | ||
export declare type HandlerFunction = ( | ||
req: Request, | ||
res: Response, | ||
next?: NextFunction | ||
) => void | any | Promise<any>; | ||
export declare type LoggerFunction = (message: string) => void; | ||
@@ -55,10 +68,11 @@ export declare type NextFunction = () => void; | ||
export declare type FinallyFunction = (req: Request, res: Response) => void; | ||
export declare type METHODS = 'GET' | ||
| 'POST' | ||
| 'PUT' | ||
| 'PATCH' | ||
| 'DELETE' | ||
| 'OPTIONS' | ||
| 'HEAD' | ||
| 'ANY'; | ||
export declare type METHODS = | ||
| 'GET' | ||
| 'POST' | ||
| 'PUT' | ||
| 'PATCH' | ||
| 'DELETE' | ||
| 'OPTIONS' | ||
| 'HEAD' | ||
| 'ANY'; | ||
@@ -68,3 +82,3 @@ export declare interface SamplingOptions { | ||
target?: number; | ||
rate?: number | ||
rate?: number; | ||
period?: number; | ||
@@ -109,2 +123,3 @@ method?: string | string[]; | ||
isBase64?: boolean; | ||
compression?: boolean; | ||
headers?: object; | ||
@@ -126,3 +141,3 @@ } | ||
multiValueQuery: { | ||
[key: string]: string[] | undefined | ||
[key: string]: string[] | undefined; | ||
}; | ||
@@ -178,3 +193,7 @@ headers: { | ||
removeHeader(key: string): this; | ||
getLink(s3Path: string, expires?: number, callback?: ErrorCallback): Promise<string>; | ||
getLink( | ||
s3Path: string, | ||
expires?: number, | ||
callback?: ErrorCallback | ||
): Promise<string>; | ||
send(body: any): void; | ||
@@ -197,4 +216,13 @@ json(body: any): void; | ||
attachment(fileName?: string): this; | ||
download(file: string | Buffer, fileName?: string, options?: FileOptions, callback?: ErrorCallback): void; | ||
sendFile(file: string | Buffer, options?: FileOptions, callback?: ErrorCallback): Promise<void>; | ||
download( | ||
file: string | Buffer, | ||
fileName?: string, | ||
options?: FileOptions, | ||
callback?: ErrorCallback | ||
): void; | ||
sendFile( | ||
file: string | Buffer, | ||
options?: FileOptions, | ||
callback?: ErrorCallback | ||
): Promise<void>; | ||
} | ||
@@ -224,3 +252,6 @@ | ||
METHOD(method: METHODS, ...handler: HandlerFunction[]): void; | ||
register(routes: (api: API, options?: RegisterOptions) => void, options?: RegisterOptions): void; | ||
register( | ||
routes: (api: API, options?: RegisterOptions) => void, | ||
options?: RegisterOptions | ||
): void; | ||
routes(format: true): void; | ||
@@ -230,4 +261,2 @@ routes(format: false): string[][]; | ||
use(path: string, ...middleware: Middleware[]): void; | ||
@@ -240,3 +269,7 @@ use(paths: string[], ...middleware: Middleware[]): void; | ||
run(event: APIGatewayEvent, context: Context, cb: (err: Error, result: any) => void): void; | ||
run( | ||
event: APIGatewayEvent, | ||
context: Context, | ||
cb: (err: Error, result: any) => void | ||
): void; | ||
run(event: APIGatewayEvent, context: Context): Promise<any>; | ||
@@ -243,0 +276,0 @@ } |
604
index.js
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -6,37 +6,56 @@ /** | ||
* @author Jeremy Daly <jeremy@jeremydaly.com> | ||
* @version 0.10.7 | ||
* @version 0.11.0 | ||
* @license MIT | ||
*/ | ||
const REQUEST = require('./lib/request') // Resquest object | ||
const RESPONSE = require('./lib/response') // Response object | ||
const UTILS = require('./lib/utils') // Require utils library | ||
const LOGGER = require('./lib/logger') // Require logger library | ||
const prettyPrint = require('./lib/prettyPrint') // Pretty print for debugging | ||
const { ConfigurationError } = require('./lib/errors') // Require custom errors | ||
const REQUEST = require('./lib/request'); // Resquest object | ||
const RESPONSE = require('./lib/response'); // Response object | ||
const UTILS = require('./lib/utils'); // Require utils library | ||
const LOGGER = require('./lib/logger'); // Require logger library | ||
const prettyPrint = require('./lib/prettyPrint'); // Pretty print for debugging | ||
const { ConfigurationError } = require('./lib/errors'); // Require custom errors | ||
// Create the API class | ||
class API { | ||
// Create the constructor function. | ||
constructor(props) { | ||
// Set the version and base paths | ||
this._version = props && props.version ? props.version : 'v1' | ||
this._base = props && props.base && typeof props.base === 'string' ? props.base.trim() : '' | ||
this._callbackName = props && props.callback ? props.callback.trim() : 'callback' | ||
this._mimeTypes = props && props.mimeTypes && typeof props.mimeTypes === 'object' ? props.mimeTypes : {} | ||
this._serializer = props && props.serializer && typeof props.serializer === 'function' ? props.serializer : JSON.stringify | ||
this._errorHeaderWhitelist = props && Array.isArray(props.errorHeaderWhitelist) ? props.errorHeaderWhitelist.map(header => header.toLowerCase()) : [] | ||
this._isBase64 = props && typeof props.isBase64 === 'boolean' ? props.isBase64 : false | ||
this._headers = props && props.headers && typeof props.headers === 'object' ? props.headers : {} | ||
this._version = props && props.version ? props.version : 'v1'; | ||
this._base = | ||
props && props.base && typeof props.base === 'string' | ||
? props.base.trim() | ||
: ''; | ||
this._callbackName = | ||
props && props.callback ? props.callback.trim() : 'callback'; | ||
this._mimeTypes = | ||
props && props.mimeTypes && typeof props.mimeTypes === 'object' | ||
? props.mimeTypes | ||
: {}; | ||
this._serializer = | ||
props && props.serializer && typeof props.serializer === 'function' | ||
? props.serializer | ||
: JSON.stringify; | ||
this._errorHeaderWhitelist = | ||
props && Array.isArray(props.errorHeaderWhitelist) | ||
? props.errorHeaderWhitelist.map((header) => header.toLowerCase()) | ||
: []; | ||
this._isBase64 = | ||
props && typeof props.isBase64 === 'boolean' ? props.isBase64 : false; | ||
this._headers = | ||
props && props.headers && typeof props.headers === 'object' | ||
? props.headers | ||
: {}; | ||
this._compression = | ||
props && typeof props.compression === 'boolean' | ||
? props.compression | ||
: false; | ||
// Set sampling info | ||
this._sampleCounts = {} | ||
this._sampleCounts = {}; | ||
// Init request counter | ||
this._requestCount = 0 | ||
this._requestCount = 0; | ||
// Track init date/time | ||
this._initTime = Date.now() | ||
this._initTime = Date.now(); | ||
@@ -50,176 +69,238 @@ // Logging levels | ||
error: 50, | ||
fatal: 60 | ||
} | ||
fatal: 60, | ||
}; | ||
// Configure logger | ||
this._logger = LOGGER.config(props && props.logger,this._logLevels) | ||
this._logger = LOGGER.config(props && props.logger, this._logLevels); | ||
// Prefix stack w/ base | ||
this._prefix = this.parseRoute(this._base) | ||
this._prefix = this.parseRoute(this._base); | ||
// Stores route mappings | ||
this._routes = {} | ||
this._routes = {}; | ||
// Init callback | ||
this._cb | ||
this._cb; | ||
// Error middleware stack | ||
this._errors = [] | ||
this._errors = []; | ||
// Store app packages and namespaces | ||
this._app = {} | ||
this._app = {}; | ||
// Executed after the callback | ||
this._finally = () => {} | ||
this._finally = () => {}; | ||
// Global error status (used for response parsing errors) | ||
this._errorStatus = 500 | ||
this._errorStatus = 500; | ||
// Methods | ||
this._methods = ['get','post','put','patch','delete','options','head','any'] | ||
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) | ||
}) | ||
this._methods.forEach((m) => { | ||
this[m] = (...a) => this.METHOD(m.toUpperCase(), ...a); | ||
}); | ||
} // end constructor | ||
// METHOD: Adds method, middleware, and handlers to routes | ||
METHOD(method,...args) { | ||
METHOD(method, ...args) { | ||
// Extract path if provided, otherwise default to global wildcard | ||
let path = typeof args[0] === 'string' ? args.shift() : '/*' | ||
let path = typeof args[0] === 'string' ? args.shift() : '/*'; | ||
// Extract the execution stack | ||
let stack = args.map((fn,i) => { | ||
if (typeof fn === 'function' && (fn.length === 3 || (i === args.length-1))) | ||
return fn | ||
throw new ConfigurationError('Route-based middleware must have 3 parameters') | ||
}) | ||
let stack = args.map((fn, i) => { | ||
if ( | ||
typeof fn === 'function' && | ||
(fn.length === 3 || i === args.length - 1) | ||
) | ||
return fn; | ||
throw new ConfigurationError( | ||
'Route-based middleware must have 3 parameters' | ||
); | ||
}); | ||
if (stack.length === 0) | ||
throw new ConfigurationError(`No handler or middleware specified for ${method} method on ${path} route.`) | ||
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(',') | ||
// Ensure methods is an array and upper case | ||
let methods = (Array.isArray(method) ? method : method.split(',')).map( | ||
(x) => (typeof x === 'string' ? x.trim().toUpperCase() : null) | ||
); | ||
// Parse the path | ||
let parsedPath = this.parseRoute(path) | ||
let parsedPath = this.parseRoute(path); | ||
// Split the route and clean it up | ||
let route = this._prefix.concat(parsedPath) | ||
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 = {} | ||
let pathVars = {}; | ||
// Make a local copy of routes | ||
let routes = this._routes | ||
let routes = this._routes; | ||
// Create a local stack for inheritance | ||
let _stack = {} | ||
let _stack = { '*': [], m: [] }; | ||
// Loop through the paths | ||
for (let i=0; i<route.length; i++) { | ||
// Loop through the path levels | ||
for (let i = 0; i < route.length; i++) { | ||
// Flag as end of the path | ||
let end = i === route.length - 1; | ||
let end = i === route.length-1 | ||
// If this is a variable | ||
// If this is a parameter 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__' | ||
route[i] = '__VAR__'; | ||
} // end if variable | ||
// Add methods to routess | ||
methods.forEach(_method => { | ||
// Create routes and add path if they don't exist | ||
if (!routes['ROUTES']) { | ||
routes['ROUTES'] = {}; | ||
} | ||
if (!routes['ROUTES'][route[i]]) { | ||
routes['ROUTES'][route[i]] = {}; | ||
} | ||
// Loop through methods for the route | ||
methods.forEach((_method) => { | ||
// Method must be a string | ||
if (typeof _method === 'string') { | ||
// Check for wild card at this level | ||
if (routes['ROUTES']['*']) { | ||
if ( | ||
routes['ROUTES']['*']['MIDDLEWARE'] && | ||
(route[i] !== '*' || _method !== '__MW__') | ||
) { | ||
_stack['*'][method] = routes['ROUTES']['*']['MIDDLEWARE'].stack; | ||
} | ||
if ( | ||
routes['ROUTES']['*']['METHODS'] && | ||
routes['ROUTES']['*']['METHODS'][method] | ||
) { | ||
_stack['m'][method] = | ||
routes['ROUTES']['*']['METHODS'][method].stack; | ||
} | ||
} // end if wild card | ||
if (routes['ROUTES']) { | ||
// If this is the end of the path | ||
if (end) { | ||
// Check for matching middleware | ||
if ( | ||
route[i] !== '*' && | ||
routes['ROUTES'][route[i]] && | ||
routes['ROUTES'][route[i]]['MIDDLEWARE'] | ||
) { | ||
_stack['m'][method] = | ||
routes['ROUTES'][route[i]]['MIDDLEWARE'].stack; | ||
} // end if | ||
// Wildcard routes | ||
if (routes['ROUTES']['*']) { | ||
// Generate the route/method meta data | ||
let meta = { | ||
vars: pathVars, | ||
stack: _stack['m'][method] | ||
? _stack['m'][method].concat(stack) | ||
: _stack['*'][method] | ||
? _stack['*'][method].concat(stack) | ||
: stack, | ||
// inherited: _stack[method] ? _stack[method] : [], | ||
route: '/' + parsedPath.join('/'), | ||
path: '/' + this._prefix.concat(parsedPath).join('/'), | ||
}; | ||
// 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 | ||
// If mounting middleware | ||
if (method === '__MW__') { | ||
// Merge stacks if middleware exists | ||
if (routes['ROUTES'][route[i]]['MIDDLEWARE']) { | ||
meta.stack = | ||
routes['ROUTES'][route[i]]['MIDDLEWARE'].stack.concat(stack); | ||
meta.vars = UTILS.mergeObjects( | ||
routes['ROUTES'][route[i]]['MIDDLEWARE'].vars, | ||
pathVars | ||
); | ||
} | ||
// Add/update middleware | ||
routes['ROUTES'][route[i]]['MIDDLEWARE'] = meta; | ||
// 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 | ||
} | ||
} | ||
// Apply middleware to all child middlware routes | ||
// if (route[i] === "*") { | ||
// // console.log("APPLY NESTED MIDDLEWARE"); | ||
// // console.log(JSON.stringify(routes["ROUTES"], null, 2)); | ||
// Object.keys(routes["ROUTES"]).forEach((nestedRoute) => { | ||
// if (nestedRoute != "*") { | ||
// console.log(nestedRoute); | ||
// } | ||
// }); | ||
// } | ||
} else { | ||
// Create the methods section if it doesn't exist | ||
if (!routes['ROUTES'][route[i]]['METHODS']) | ||
routes['ROUTES'][route[i]]['METHODS'] = {}; | ||
// 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 | ||
// Merge stacks if method already exists for this route | ||
if (routes['ROUTES'][route[i]]['METHODS'][_method]) { | ||
meta.stack = | ||
routes['ROUTES'][route[i]]['METHODS'][_method].stack.concat( | ||
stack | ||
); | ||
meta.vars = UTILS.mergeObjects( | ||
routes['ROUTES'][route[i]]['METHODS'][_method].vars, | ||
pathVars | ||
); | ||
} | ||
// 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 method and meta data | ||
routes['ROUTES'][route[i]]['METHODS'][_method] = meta; | ||
} // end else | ||
// Add the route to the global _routes | ||
this.setRoute( | ||
this._routes, | ||
_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) | ||
) | ||
// console.log('STACK:',meta); | ||
} | ||
}) // end methods loop | ||
// If there's a wild card that's not at the end | ||
} else if (route[i] === '*') { | ||
throw new ConfigurationError( | ||
'Wildcards can only be at the end of a route definition' | ||
); | ||
} // end if end of path | ||
} // end if method is string | ||
}); // end methods loop | ||
routes = routes['ROUTES'][route[i]] | ||
// Update the current routes pointer | ||
routes = routes['ROUTES'][route[i]]; | ||
} // end path traversal loop | ||
} // end for loop | ||
// console.log(JSON.stringify(this._routes,null,2)); | ||
} // end main METHOD function | ||
// RUN: This runs the routes | ||
async run(event,context,cb) { | ||
async run(event, context, cb) { | ||
// Set the event, context and callback | ||
this._event = event || {} | ||
this._context = this.context = typeof context === 'object' ? context : {} | ||
this._cb = cb ? cb : undefined | ||
this._event = event || {}; | ||
this._context = this.context = typeof context === 'object' ? context : {}; | ||
this._cb = cb ? cb : undefined; | ||
// Initalize request and response objects | ||
let request = new REQUEST(this) | ||
let response = new RESPONSE(this,request) | ||
let request = new REQUEST(this); | ||
let response = new RESPONSE(this, request); | ||
try { | ||
// Parse the request | ||
await request.parseRequest() | ||
await request.parseRequest(); | ||
@@ -229,50 +310,50 @@ // Loop through the execution stack | ||
// Only run if in processing state | ||
if (response._state !== 'processing') break | ||
if (response._state !== 'processing') break; | ||
await new Promise(async r => { | ||
// eslint-disable-next-line | ||
await new Promise(async (r) => { | ||
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 | ||
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 | ||
} catch(e) { | ||
await this.catchErrors(e,response) | ||
} catch (e) { | ||
// console.log(e); | ||
await this.catchErrors(e, response); | ||
} | ||
// Return the final response | ||
return response._response | ||
return response._response; | ||
} // end run function | ||
// Catch all async/sync errors | ||
async catchErrors(e,response,code,detail) { | ||
async catchErrors(e, response, code, detail) { | ||
// Error messages should respect the app's base64 configuration | ||
response._isBase64 = this._isBase64 | ||
response._isBase64 = this._isBase64; | ||
// Strip the headers, keep whitelist | ||
const strippedHeaders = Object.entries(response._headers).reduce((acc, [headerName, value]) => { | ||
if (!this._errorHeaderWhitelist.includes(headerName.toLowerCase())) { return acc } | ||
const strippedHeaders = Object.entries(response._headers).reduce( | ||
(acc, [headerName, value]) => { | ||
if (!this._errorHeaderWhitelist.includes(headerName.toLowerCase())) { | ||
return acc; | ||
} | ||
return Object.assign( | ||
acc, | ||
{ [headerName]: value } | ||
) | ||
}, {}) | ||
return Object.assign(acc, { [headerName]: value }); | ||
}, | ||
{} | ||
); | ||
response._headers = Object.assign(strippedHeaders, this._headers) | ||
response._headers = Object.assign(strippedHeaders, this._headers); | ||
let message | ||
let message; | ||
// Set the status code | ||
response.status(code ? code : this._errorStatus) | ||
response.status(code ? code : this._errorStatus); | ||
@@ -283,14 +364,14 @@ let info = { | ||
coldStart: response._request.coldStart, | ||
stack: this._logger.stack && e.stack || undefined | ||
} | ||
stack: (this._logger.stack && e.stack) || undefined, | ||
}; | ||
if (e instanceof Error) { | ||
message = e.message | ||
message = e.message; | ||
if (this._logger.errorLogging) { | ||
this.log.fatal(message, info) | ||
this.log.fatal(message, info); | ||
} | ||
} else { | ||
message = e | ||
message = e; | ||
if (this._logger.errorLogging) { | ||
this.log.error(message, info) | ||
this.log.error(message, info); | ||
} | ||
@@ -301,14 +382,15 @@ } | ||
if (response._state === 'processing') { | ||
// Flag error state (this will avoid infinite error loops) | ||
response._state = 'error' | ||
response._state = 'error'; | ||
// Execute error middleware | ||
for (const err of this._errors) { | ||
if (response._state === 'done') break | ||
if (response._state === 'done') break; | ||
// Promisify error middleware | ||
await new Promise(r => { | ||
let rtn = err(e,response._request,response,() => { r() }) | ||
if (rtn) response.send(rtn) | ||
}) | ||
await new Promise((r) => { | ||
let rtn = err(e, response._request, response, () => { | ||
r(); | ||
}); | ||
if (rtn) response.send(rtn); | ||
}); | ||
} // end for | ||
@@ -318,50 +400,66 @@ } | ||
// Throw standard error unless callback has already been executed | ||
if (response._state !== 'done') response.json({'error':message}) | ||
if (response._state !== 'done') response.json({ error: message }); | ||
} // end catch | ||
// Custom callback | ||
async _callback(err,res,response) { | ||
async _callback(err, res, response) { | ||
// Set done status | ||
response._state = 'done' | ||
response._state = 'done'; | ||
// Execute finally | ||
await this._finally(response._request,response) | ||
await this._finally(response._request, response); | ||
// Output logs | ||
response._request._logs.forEach(log => { | ||
this._logger.logger(JSON.stringify(this._logger.detail ? | ||
this._logger.format(log,response._request,response) : log)) | ||
}) | ||
response._request._logs.forEach((log) => { | ||
this._logger.logger( | ||
JSON.stringify( | ||
this._logger.detail | ||
? this._logger.format(log, response._request, response) | ||
: log | ||
) | ||
); | ||
}); | ||
// Generate access log | ||
if ((this._logger.access || response._request._logs.length > 0) && this._logger.access !== 'never') { | ||
if ( | ||
(this._logger.access || response._request._logs.length > 0) && | ||
this._logger.access !== 'never' | ||
) { | ||
let access = Object.assign( | ||
this._logger.log('access',undefined,response._request,response._request.context), | ||
{ statusCode: res.statusCode, coldStart: response._request.coldStart, count: response._request.requestCount } | ||
) | ||
this._logger.logger(JSON.stringify(this._logger.format(access,response._request,response))) | ||
this._logger.log( | ||
'access', | ||
undefined, | ||
response._request, | ||
response._request.context | ||
), | ||
{ | ||
statusCode: res.statusCode, | ||
coldStart: response._request.coldStart, | ||
count: response._request.requestCount, | ||
} | ||
); | ||
this._logger.logger( | ||
JSON.stringify(this._logger.format(access, response._request, response)) | ||
); | ||
} | ||
// Reset global error code | ||
this._errorStatus = 500 | ||
this._errorStatus = 500; | ||
// Execute the primary callback | ||
typeof this._cb === 'function' && this._cb(err,res) | ||
typeof this._cb === 'function' && this._cb(err, res); | ||
} // end _callback | ||
// Middleware handler | ||
use(...args) { | ||
// Extract routes | ||
let routes = typeof args[0] === 'string' ? Array.of(args.shift()) : (Array.isArray(args[0]) ? args.shift() : ['/*']) | ||
let routes = | ||
typeof args[0] === 'string' | ||
? Array.of(args.shift()) | ||
: Array.isArray(args[0]) | ||
? args.shift() | ||
: ['/*']; | ||
// Init middleware stack | ||
let middleware = [] | ||
let middleware = []; | ||
@@ -372,7 +470,9 @@ // Add func args as middleware | ||
if (args[arg].length === 3) { | ||
middleware.push(args[arg]) | ||
middleware.push(args[arg]); | ||
} else if (args[arg].length === 4) { | ||
this._errors.push(args[arg]) | ||
this._errors.push(args[arg]); | ||
} else { | ||
throw new ConfigurationError('Middleware must have 3 or 4 parameters') | ||
throw new ConfigurationError( | ||
'Middleware must have 3 or 4 parameters' | ||
); | ||
} | ||
@@ -382,19 +482,15 @@ } | ||
// Add middleware to path | ||
// Add middleware for all methods | ||
if (middleware.length > 0) { | ||
routes.forEach(route => { | ||
this.METHOD('__MW__',route,...middleware) | ||
}) | ||
routes.forEach((route) => { | ||
this.METHOD('__MW__', route, ...middleware); | ||
}); | ||
} | ||
} // end use | ||
// Finally handler | ||
finally(fn) { | ||
this._finally = fn | ||
this._finally = fn; | ||
} | ||
//-------------------------------------------------------------------------// | ||
@@ -405,56 +501,11 @@ // UTILITY FUNCTIONS | ||
parseRoute(path) { | ||
return path.trim().replace(/^\/(.*?)(\/)*$/,'$1').split('/').filter(x => x.trim() !== '') | ||
return path | ||
.trim() | ||
.replace(/^\/(.*?)(\/)*$/, '$1') | ||
.split('/') | ||
.filter((x) => x.trim() !== ''); | ||
} | ||
// Recursive function to create/merge routes object | ||
setRoute(obj, method, value, path) { | ||
if (path.length > 1) { | ||
let p = path.shift() | ||
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 { | ||
// 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) { | ||
// 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) { | ||
// Check for supplied packages | ||
@@ -465,56 +516,53 @@ if (typeof packages === 'object') { | ||
try { | ||
this._app[namespace] = packages[namespace] | ||
} catch(e) { | ||
console.error(e.message) // eslint-disable-line no-console | ||
this._app[namespace] = packages[namespace]; | ||
} catch (e) { | ||
console.error(e.message); // eslint-disable-line no-console | ||
} | ||
} | ||
} else if (arguments.length === 2 && typeof packages === 'string') { | ||
this._app[packages] = arguments[1] | ||
}// end if | ||
this._app[packages] = arguments[1]; | ||
} // end if | ||
// Return a reference | ||
return this._app | ||
return this._app; | ||
} | ||
// Register routes with options | ||
register(fn,opts) { | ||
register(fn, opts) { | ||
let options = typeof opts === 'object' ? opts : {}; | ||
let options = typeof opts === 'object' ? opts : {} | ||
// Extract Prefix | ||
let prefix = options.prefix && options.prefix.toString().trim() !== '' ? | ||
this.parseRoute(options.prefix) : [] | ||
let prefix = | ||
options.prefix && options.prefix.toString().trim() !== '' | ||
? this.parseRoute(options.prefix) | ||
: []; | ||
// Concat to existing prefix | ||
this._prefix = this._prefix.concat(prefix) | ||
this._prefix = this._prefix.concat(prefix); | ||
// Execute the routing function | ||
fn(this,options) | ||
fn(this, options); | ||
// Remove the last prefix (if a prefix exists) | ||
if (prefix.length > 0) { | ||
this._prefix = this._prefix.slice(0,-(prefix.length)) | ||
this._prefix = this._prefix.slice(0, -prefix.length); | ||
} | ||
} // end register | ||
// prettyPrint debugger | ||
routes(format) { | ||
// Parse the routes | ||
let routes = UTILS.extractRoutes(this._routes) | ||
let routes = UTILS.extractRoutes(this._routes); | ||
if (format) { | ||
console.log(prettyPrint(routes)) // eslint-disable-line no-console | ||
console.log(prettyPrint(routes)); // eslint-disable-line no-console | ||
} else { | ||
return routes | ||
return routes; | ||
} | ||
} | ||
} // end API class | ||
// Export the API class as a new instance | ||
module.exports = opts => new API(opts) | ||
module.exports = (opts) => new API(opts); | ||
// Add createAPI as default export (to match index.d.ts) | ||
module.exports.default = module.exports | ||
module.exports.default = module.exports; |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -12,6 +12,6 @@ /** | ||
class RouteError extends Error { | ||
constructor(message,path) { | ||
super(message) | ||
this.name = this.constructor.name | ||
this.path = path | ||
constructor(message, path) { | ||
super(message); | ||
this.name = this.constructor.name; | ||
this.path = path; | ||
} | ||
@@ -21,7 +21,7 @@ } | ||
class MethodError extends Error { | ||
constructor(message,method,path) { | ||
super(message) | ||
this.name = this.constructor.name | ||
this.method = method | ||
this.path = path | ||
constructor(message, method, path) { | ||
super(message); | ||
this.name = this.constructor.name; | ||
this.method = method; | ||
this.path = path; | ||
} | ||
@@ -32,4 +32,4 @@ } | ||
constructor(message) { | ||
super(message) | ||
this.name = this.constructor.name | ||
super(message); | ||
this.name = this.constructor.name; | ||
} | ||
@@ -39,6 +39,6 @@ } | ||
class ResponseError extends Error { | ||
constructor(message,code) { | ||
super(message) | ||
this.name = this.constructor.name | ||
this.code = code | ||
constructor(message, code) { | ||
super(message); | ||
this.name = this.constructor.name; | ||
this.code = code; | ||
} | ||
@@ -48,6 +48,6 @@ } | ||
class FileError extends Error { | ||
constructor(message,err) { | ||
super(message) | ||
this.name = this.constructor.name | ||
for (let e in err) this[e] = err[e] | ||
constructor(message, err) { | ||
super(message); | ||
this.name = this.constructor.name; | ||
for (let e in err) this[e] = err[e]; | ||
} | ||
@@ -62,3 +62,3 @@ } | ||
ResponseError, | ||
FileError | ||
} | ||
FileError, | ||
}; |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -7,3 +7,3 @@ /** | ||
* @license MIT | ||
*/ | ||
*/ | ||
@@ -14,10 +14,9 @@ // IDEA: add unique function identifier | ||
const UTILS = require('./utils') // Require utils library | ||
const { ConfigurationError } = require('./errors') // Require custom errors | ||
const UTILS = require('./utils'); // Require utils library | ||
const { ConfigurationError } = require('./errors'); // Require custom errors | ||
// Config logger | ||
exports.config = (config,levels) => { | ||
exports.config = (config, levels) => { | ||
let cfg = config ? config : {}; | ||
let cfg = config ? config : {} | ||
// Add custom logging levels | ||
@@ -27,37 +26,55 @@ if (cfg.levels && typeof cfg.levels === 'object') { | ||
if (!/^[A-Za-z_]\w*$/.test(lvl) || isNaN(cfg.levels[lvl])) { | ||
throw new ConfigurationError('Invalid level configuration') | ||
throw new ConfigurationError('Invalid level configuration'); | ||
} | ||
} | ||
levels = Object.assign(levels,cfg.levels) | ||
levels = Object.assign(levels, cfg.levels); | ||
} | ||
// Configure sampling rules | ||
let sampling = cfg.sampling ? parseSamplerConfig(cfg.sampling,levels) : false | ||
let sampling = cfg.sampling | ||
? parseSamplerConfig(cfg.sampling, levels) | ||
: false; | ||
// Parse/default the logging level | ||
let level = cfg === true ? 'info' : | ||
cfg.level && levels[cfg.level.toLowerCase()] ? | ||
cfg.level.toLowerCase() : cfg.level === 'none' ? | ||
'none' : Object.keys(cfg).length > 0 ? 'info' : 'none' | ||
let level = | ||
cfg === true | ||
? 'info' | ||
: cfg.level && levels[cfg.level.toLowerCase()] | ||
? cfg.level.toLowerCase() | ||
: cfg.level === 'none' | ||
? 'none' | ||
: Object.keys(cfg).length > 0 | ||
? 'info' | ||
: 'none'; | ||
let messageKey = cfg.messageKey && typeof cfg.messageKey === 'string' ? | ||
cfg.messageKey.trim() : 'msg' | ||
let messageKey = | ||
cfg.messageKey && typeof cfg.messageKey === 'string' | ||
? cfg.messageKey.trim() | ||
: 'msg'; | ||
let customKey = cfg.customKey && typeof cfg.customKey === 'string' ? | ||
cfg.customKey.trim() : 'custom' | ||
let customKey = | ||
cfg.customKey && typeof cfg.customKey === 'string' | ||
? cfg.customKey.trim() | ||
: 'custom'; | ||
let timestamp = cfg.timestamp === false ? () => undefined : | ||
typeof cfg.timestamp === 'function' ? cfg.timestamp : () => Date.now() | ||
let timestamp = | ||
cfg.timestamp === false | ||
? () => undefined | ||
: typeof cfg.timestamp === 'function' | ||
? cfg.timestamp | ||
: () => Date.now(); | ||
let timer = cfg.timer === false ? () => undefined : (start) => (Date.now()-start) | ||
let timer = | ||
cfg.timer === false ? () => undefined : (start) => Date.now() - start; | ||
let nested = cfg.nested === true ? true : false // nest serializers | ||
let stack = cfg.stack === true ? true : false // show stack traces in errors | ||
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 nested = cfg.nested === true ? true : false; // nest serializers | ||
let stack = cfg.stack === true ? true : false; // show stack traces in errors | ||
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 multiValue = cfg.multiValue === true ? true : false; // return qs as multiValue | ||
let defaults = { | ||
req: req => { | ||
req: (req) => { | ||
return { | ||
@@ -70,42 +87,72 @@ path: req.path, | ||
version: req.version, | ||
qs: multiValue ? (Object.keys(req.multiValueQuery).length > 0 ? req.multiValueQuery : undefined) | ||
: (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: () => { | ||
return { } | ||
return {}; | ||
}, | ||
context: context => { | ||
context: (context) => { | ||
return { | ||
remaining: context.getRemainingTimeInMillis && context.getRemainingTimeInMillis(), | ||
remaining: | ||
context.getRemainingTimeInMillis && | ||
context.getRemainingTimeInMillis(), | ||
function: context.functionName && context.functionName, | ||
memory: context.memoryLimitInMB && context.memoryLimitInMB | ||
} | ||
memory: context.memoryLimitInMB && context.memoryLimitInMB, | ||
}; | ||
}, | ||
custom: custom => typeof custom === 'object' && !Array.isArray(custom) | ||
|| nested ? custom : { [customKey]: custom } | ||
} | ||
custom: (custom) => | ||
(typeof custom === 'object' && !Array.isArray(custom)) || nested | ||
? custom | ||
: { [customKey]: custom }, | ||
}; | ||
let serializers = { | ||
main: cfg.serializers && typeof cfg.serializers.main === 'function' ? cfg.serializers.main : () => {}, | ||
req: cfg.serializers && typeof cfg.serializers.req === 'function' ? cfg.serializers.req : () => {}, | ||
res: cfg.serializers && typeof cfg.serializers.res === 'function' ? cfg.serializers.res : () => {}, | ||
context: cfg.serializers && typeof cfg.serializers.context === 'function' ? cfg.serializers.context : () => {}, | ||
custom: cfg.serializers && typeof cfg.serializers.custom === 'function' ? cfg.serializers.custom : () => {} | ||
} | ||
main: | ||
cfg.serializers && typeof cfg.serializers.main === 'function' | ||
? cfg.serializers.main | ||
: () => {}, | ||
req: | ||
cfg.serializers && typeof cfg.serializers.req === 'function' | ||
? cfg.serializers.req | ||
: () => {}, | ||
res: | ||
cfg.serializers && typeof cfg.serializers.res === 'function' | ||
? cfg.serializers.res | ||
: () => {}, | ||
context: | ||
cfg.serializers && typeof cfg.serializers.context === 'function' | ||
? cfg.serializers.context | ||
: () => {}, | ||
custom: | ||
cfg.serializers && typeof cfg.serializers.custom === 'function' | ||
? cfg.serializers.custom | ||
: () => {}, | ||
}; | ||
// Overridable logging function | ||
let logger = cfg.log && typeof cfg.log === 'function' ? | ||
cfg.log : | ||
(...a) => console.log(...a) // eslint-disable-line no-console | ||
let logger = | ||
cfg.log && typeof cfg.log === 'function' | ||
? cfg.log | ||
: (...a) => console.log(...a); // eslint-disable-line no-console | ||
// Main logging function | ||
let log = (level,msg,req,context,custom) => { | ||
let log = (level, msg, req, context, custom) => { | ||
let _context = Object.assign( | ||
{}, | ||
defaults.context(context), | ||
serializers.context(context) | ||
); | ||
let _custom = | ||
typeof custom === 'object' && !Array.isArray(custom) | ||
? Object.assign({}, defaults.custom(custom), serializers.custom(custom)) | ||
: defaults.custom(custom); | ||
let _context = Object.assign({},defaults.context(context),serializers.context(context)) | ||
let _custom = typeof custom === 'object' && !Array.isArray(custom) ? | ||
Object.assign({},defaults.custom(custom),serializers.custom(custom)) : | ||
defaults.custom(custom) | ||
return Object.assign({}, | ||
return Object.assign( | ||
{}, | ||
{ | ||
@@ -120,3 +167,3 @@ level, | ||
int: req.interface, | ||
sample: req._sample ? true : undefined | ||
sample: req._sample ? true : undefined, | ||
}, | ||
@@ -126,18 +173,17 @@ serializers.main(req), | ||
nested ? { context: _context } : _context | ||
) | ||
); | ||
}; // end log | ||
} // end log | ||
// Formatting function for additional log data enrichment | ||
let format = function(info,req,res) { | ||
let format = function (info, req, res) { | ||
let _req = Object.assign({}, defaults.req(req), serializers.req(req)); | ||
let _res = Object.assign({}, defaults.res(res), serializers.res(res)); | ||
let _req = Object.assign({},defaults.req(req),serializers.req(req)) | ||
let _res = Object.assign({},defaults.res(res),serializers.res(res)) | ||
return Object.assign({}, | ||
return Object.assign( | ||
{}, | ||
info, | ||
nested ? { req: _req } : _req, | ||
nested ? { res: _res } : _res | ||
) | ||
} // end format | ||
); | ||
}; // end format | ||
@@ -154,74 +200,99 @@ // Return logger object | ||
sampling, | ||
errorLogging: cfg.errorLogging !== false | ||
} | ||
} | ||
errorLogging: cfg.errorLogging !== false, | ||
}; | ||
}; | ||
// Determine if we should sample this request | ||
exports.sampler = (app,req) => { | ||
exports.sampler = (app, req) => { | ||
if (app._logger.sampling) { | ||
// Default level to false | ||
let level = false | ||
let level = false; | ||
// Create local reference to the rulesMap | ||
let map = app._logger.sampling.rulesMap | ||
let map = app._logger.sampling.rulesMap; | ||
// Parse the current route | ||
let route = UTILS.parsePath(req.route) | ||
let route = UTILS.parsePath(req.route); | ||
// Default wildcard mapping | ||
let wildcard = {} | ||
let wildcard = {}; | ||
// Loop the map and see if this route matches | ||
route.forEach(part => { | ||
route.forEach((part) => { | ||
// Capture wildcard mappings | ||
if (map['*']) wildcard = map['*'] | ||
if (map['*']) wildcard = map['*']; | ||
// Traverse map | ||
map = map[part] ? map[part] : {} | ||
}) // end for loop | ||
map = map[part] ? map[part] : {}; | ||
}); // end for loop | ||
// Set rule reference based on route | ||
let ref = map['__'+req.method] ? map['__'+req.method] : | ||
map['__ANY'] ? map['__ANY'] : wildcard['__'+req.method] ? | ||
wildcard['__'+req.method] : wildcard['__ANY'] ? | ||
wildcard['__ANY'] : -1 | ||
let ref = map['__' + req.method] | ||
? map['__' + req.method] | ||
: map['__ANY'] | ||
? map['__ANY'] | ||
: wildcard['__' + req.method] | ||
? wildcard['__' + req.method] | ||
: wildcard['__ANY'] | ||
? wildcard['__ANY'] | ||
: -1; | ||
let rule = ref >= 0 ? app._logger.sampling.rules[ref] : app._logger.sampling.defaults | ||
let rule = | ||
ref >= 0 | ||
? app._logger.sampling.rules[ref] | ||
: app._logger.sampling.defaults; | ||
// Assign rule reference to the REQUEST | ||
req._sampleRule = rule | ||
req._sampleRule = rule; | ||
// Get last sample time (default start, last, fixed count, period count and total count) | ||
let counts = app._sampleCounts[rule.default ? 'default' : req.route] | ||
|| Object.assign(app._sampleCounts, { | ||
[rule.default ? 'default' : req.route]: { start: 0, fCount: 0, pCount: 0, tCount: 0 } | ||
})[rule.default ? 'default' : req.route] | ||
let counts = | ||
app._sampleCounts[rule.default ? 'default' : req.route] || | ||
Object.assign(app._sampleCounts, { | ||
[rule.default ? 'default' : req.route]: { | ||
start: 0, | ||
fCount: 0, | ||
pCount: 0, | ||
tCount: 0, | ||
}, | ||
})[rule.default ? 'default' : req.route]; | ||
let now = Date.now() | ||
let now = Date.now(); | ||
// Calculate the current velocity | ||
let velocity = rule.rate > 0 ? rule.period*1000/(counts.tCount/(now-app._initTime)*rule.period*1000*rule.rate) : 0 | ||
let velocity = | ||
rule.rate > 0 | ||
? (rule.period * 1000) / | ||
((counts.tCount / (now - app._initTime)) * | ||
rule.period * | ||
1000 * | ||
rule.rate) | ||
: 0; | ||
// If this is a new period, reset values | ||
if ((now-counts.start) > rule.period*1000) { | ||
counts.start = now | ||
counts.pCount = 0 | ||
if (now - counts.start > rule.period * 1000) { | ||
counts.start = now; | ||
counts.pCount = 0; | ||
// If a rule target is set, sample the start | ||
if (rule.target > 0) { | ||
counts.fCount = 1 | ||
level = rule.level // set the sample level | ||
counts.fCount = 1; | ||
level = rule.level; // set the sample level | ||
// console.log('\n*********** NEW PERIOD ***********'); | ||
} | ||
// Enable sampling if last sample is passed target split | ||
} else if (rule.target > 0 && | ||
counts.start+Math.floor(rule.period*1000/rule.target*counts.fCount) < now) { | ||
level = rule.level | ||
counts.fCount++ | ||
// Enable sampling if last sample is passed target split | ||
} else if ( | ||
rule.target > 0 && | ||
counts.start + | ||
Math.floor(((rule.period * 1000) / rule.target) * counts.fCount) < | ||
now | ||
) { | ||
level = rule.level; | ||
counts.fCount++; | ||
// console.log('\n*********** FIXED ***********'); | ||
} else if (rule.rate > 0 && | ||
counts.start+Math.floor(velocity*counts.pCount+velocity/2) < now) { | ||
level = rule.level | ||
counts.pCount++ | ||
} else if ( | ||
rule.rate > 0 && | ||
counts.start + Math.floor(velocity * counts.pCount + velocity / 2) < now | ||
) { | ||
level = rule.level; | ||
counts.pCount++; | ||
// console.log('\n*********** RATE ***********'); | ||
@@ -231,68 +302,78 @@ } | ||
// Increment total count | ||
counts.tCount++ | ||
counts.tCount++; | ||
return level | ||
return level; | ||
} // end if sampling | ||
return false | ||
} | ||
return false; | ||
}; | ||
// Parse sampler configuration | ||
const parseSamplerConfig = (config,levels) => { | ||
const parseSamplerConfig = (config, levels) => { | ||
// Default config | ||
let cfg = typeof config === 'object' ? config : config === true ? {} : false | ||
let cfg = typeof config === 'object' ? config : config === true ? {} : false; | ||
// Error on invalid config | ||
if (cfg === false) throw new ConfigurationError('Invalid sampler configuration') | ||
if (cfg === false) | ||
throw new ConfigurationError('Invalid sampler configuration'); | ||
// Create rule default | ||
let defaults = (inputs) => { | ||
return { // target, rate, period, method, level | ||
return { | ||
// target, rate, period, method, level | ||
target: Number.isInteger(inputs.target) ? inputs.target : 1, | ||
rate: !isNaN(inputs.rate) && inputs.rate <= 1 ? inputs.rate : 0.1, | ||
period: Number.isInteger(inputs.period) ? inputs.period : 60, // in seconds | ||
level: Object.keys(levels).includes(inputs.level) ? inputs.level : 'trace' | ||
} | ||
} | ||
level: Object.keys(levels).includes(inputs.level) | ||
? inputs.level | ||
: 'trace', | ||
}; | ||
}; | ||
// Init ruleMap | ||
let rulesMap = {} | ||
let rulesMap = {}; | ||
// Parse and default rules | ||
let rules = Array.isArray(cfg.rules) ? cfg.rules.map((rule,i) => { | ||
// Error if missing route or not a string | ||
if (!rule.route || typeof rule.route !== 'string') | ||
throw new ConfigurationError('Invalid route specified in rule') | ||
let rules = Array.isArray(cfg.rules) | ||
? cfg.rules.map((rule, i) => { | ||
// Error if missing route or not a string | ||
if (!rule.route || typeof rule.route !== 'string') | ||
throw new ConfigurationError('Invalid route specified in rule'); | ||
// Parse methods into array (if not already) | ||
let methods = (Array.isArray(rule.method) ? rule.method : | ||
typeof rule.method === 'string' ? | ||
rule.method.split(',') : ['ANY']).map(x => x.toString().trim().toUpperCase()) | ||
// Parse methods into array (if not already) | ||
let methods = ( | ||
Array.isArray(rule.method) | ||
? rule.method | ||
: typeof rule.method === 'string' | ||
? rule.method.split(',') | ||
: ['ANY'] | ||
).map((x) => x.toString().trim().toUpperCase()); | ||
let map = {} | ||
let recursive = map // create recursive reference | ||
let map = {}; | ||
let recursive = map; // create recursive reference | ||
UTILS.parsePath(rule.route).forEach(part => { | ||
Object.assign(recursive,{ [part === '' ? '/' : part]: {} }) | ||
recursive = recursive[part === '' ? '/' : part] | ||
}) | ||
UTILS.parsePath(rule.route).forEach((part) => { | ||
Object.assign(recursive, { [part === '' ? '/' : part]: {} }); | ||
recursive = recursive[part === '' ? '/' : part]; | ||
}); | ||
Object.assign(recursive, methods.reduce((acc,method) => { | ||
return Object.assign(acc, { ['__'+method]: i }) | ||
},{})) | ||
Object.assign( | ||
recursive, | ||
methods.reduce((acc, method) => { | ||
return Object.assign(acc, { ['__' + method]: i }); | ||
}, {}) | ||
); | ||
// Deep merge the maps | ||
UTILS.deepMerge(rulesMap,map) | ||
// Deep merge the maps | ||
UTILS.deepMerge(rulesMap, map); | ||
return defaults(rule) | ||
},{}) : {} | ||
return defaults(rule); | ||
}, {}) | ||
: {}; | ||
return { | ||
defaults: Object.assign(defaults(cfg),{ default:true }), | ||
defaults: Object.assign(defaults(cfg), { default: true }), | ||
rules, | ||
rulesMap | ||
} | ||
} // end parseSamplerConfig | ||
rulesMap, | ||
}; | ||
}; // end parseSamplerConfig |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -71,4 +71,3 @@ /** | ||
ppsm: 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', | ||
mdb: 'application/vnd.ms-access' | ||
} | ||
mdb: 'application/vnd.ms-access', | ||
}; |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -9,26 +9,77 @@ /** | ||
module.exports = routes => { | ||
module.exports = (routes) => { | ||
let out = ''; | ||
let out = '' | ||
// Calculate column widths | ||
let widths = routes.reduce((acc,row) => { | ||
return [ | ||
Math.max(acc[0],Math.max(6,row[0].length)), | ||
Math.max(acc[1],Math.max(5,row[1].length)) | ||
] | ||
},[0,0]) | ||
let widths = routes.reduce( | ||
(acc, row) => { | ||
return [ | ||
Math.max(acc[0], Math.max(6, row[0].length)), | ||
Math.max(acc[1], Math.max(5, row[1].length)), | ||
Math.max(acc[2], Math.max(6, row[2].join(', ').length)), | ||
]; | ||
}, | ||
[0, 0, 0] | ||
); | ||
out += '╔══' + ''.padEnd(widths[0],'═') + '══╤══' + ''.padEnd(widths[1],'═') + '══╗\n' | ||
out += '║ ' + '\u001b[1m' + 'METHOD'.padEnd(widths[0]) + '\u001b[0m' + ' │ ' + '\u001b[1m' + 'ROUTE'.padEnd(widths[1]) + '\u001b[0m' + ' ║\n' | ||
out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n' | ||
routes.forEach((route,i) => { | ||
out += '║ ' + route[0].padEnd(widths[0]) + ' │ ' + route[1].padEnd(widths[1]) + ' ║\n' | ||
if (i < routes.length-1) { | ||
out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n' | ||
out += | ||
'╔══' + | ||
''.padEnd(widths[0], '═') + | ||
'══╤══' + | ||
''.padEnd(widths[1], '═') + | ||
'══╤══' + | ||
''.padEnd(widths[2], '═') + | ||
'══╗\n'; | ||
out += | ||
'║ ' + | ||
'\u001b[1m' + | ||
'METHOD'.padEnd(widths[0]) + | ||
'\u001b[0m' + | ||
' │ ' + | ||
'\u001b[1m' + | ||
'ROUTE'.padEnd(widths[1]) + | ||
'\u001b[0m' + | ||
' │ ' + | ||
'\u001b[1m' + | ||
'STACK'.padEnd(widths[2]) + | ||
'\u001b[0m' + | ||
' ║\n'; | ||
out += | ||
'╟──' + | ||
''.padEnd(widths[0], '─') + | ||
'──┼──' + | ||
''.padEnd(widths[1], '─') + | ||
'──┼──' + | ||
''.padEnd(widths[2], '─') + | ||
'──╢\n'; | ||
routes.forEach((route, i) => { | ||
out += | ||
'║ ' + | ||
route[0].padEnd(widths[0]) + | ||
' │ ' + | ||
route[1].padEnd(widths[1]) + | ||
' │ ' + | ||
route[2].join(', ').padEnd(widths[2]) + | ||
' ║\n'; | ||
if (i < routes.length - 1) { | ||
out += | ||
'╟──' + | ||
''.padEnd(widths[0], '─') + | ||
'──┼──' + | ||
''.padEnd(widths[1], '─') + | ||
'──┼──' + | ||
''.padEnd(widths[2], '─') + | ||
'──╢\n'; | ||
} // end if | ||
}) | ||
out += '╚══' + ''.padEnd(widths[0],'═') + '══╧══' + ''.padEnd(widths[1],'═') + '══╝' | ||
}); | ||
out += | ||
'╚══' + | ||
''.padEnd(widths[0], '═') + | ||
'══╧══' + | ||
''.padEnd(widths[1], '═') + | ||
'══╧══' + | ||
''.padEnd(widths[2], '═') + | ||
'══╝'; | ||
return out | ||
} | ||
return out; | ||
}; |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -9,52 +9,54 @@ /** | ||
const QS = require('querystring') // Require the querystring library | ||
const UTILS = require('./utils') // Require utils library | ||
const LOGGER = require('./logger') // Require logger library | ||
const { RouteError, MethodError } = require('./errors') // Require custom errors | ||
const QS = require('querystring'); // Require the querystring library | ||
const UTILS = require('./utils'); // Require utils library | ||
const LOGGER = require('./logger'); // Require logger library | ||
const { RouteError, MethodError } = require('./errors'); // Require custom errors | ||
class REQUEST { | ||
// Create the constructor function. | ||
constructor(app) { | ||
// Record start time | ||
this._start = Date.now() | ||
this._start = Date.now(); | ||
// Create a reference to the app | ||
this.app = app | ||
this.app = app; | ||
// Flag cold starts | ||
this.coldStart = app._requestCount === 0 ? true : false | ||
this.coldStart = app._requestCount === 0 ? true : false; | ||
// Increment the requests counter | ||
this.requestCount = ++app._requestCount | ||
this.requestCount = ++app._requestCount; | ||
// Init the handler | ||
this._handler | ||
this._handler; | ||
// Init the execution stack | ||
this._stack | ||
this._stack; | ||
// Expose Namespaces | ||
this.namespace = this.ns = app._app | ||
this.namespace = this.ns = app._app; | ||
// Set the version | ||
this.version = app._version | ||
this.version = app._version; | ||
// Init the params | ||
this.params = {} | ||
this.params = {}; | ||
// Init headers | ||
this.headers = {} | ||
this.headers = {}; | ||
// Init multi-value support flag | ||
this._multiValueSupport = null | ||
this._multiValueSupport = null; | ||
// Init log helpers (message,custom) and create app reference | ||
app.log = this.log = Object.keys(app._logLevels).reduce((acc,lvl) => | ||
Object.assign(acc,{ [lvl]: (m,c) => this.logger(lvl, m, this, this.context, c) }),{}) | ||
app.log = this.log = Object.keys(app._logLevels).reduce( | ||
(acc, lvl) => | ||
Object.assign(acc, { | ||
[lvl]: (m, c) => this.logger(lvl, m, this, this.context, c), | ||
}), | ||
{} | ||
); | ||
// Init _logs array for storage | ||
this._logs = [] | ||
this._logs = []; | ||
} // end constructor | ||
@@ -64,195 +66,273 @@ | ||
async parseRequest() { | ||
// Set the payload version | ||
this.payloadVersion = this.app._event.version | ||
? this.app._event.version | ||
: null; | ||
// Detect multi-value support | ||
this._multiValueSupport = 'multiValueHeaders' in this.app._event | ||
this._multiValueSupport = 'multiValueHeaders' in this.app._event; | ||
// Set the method | ||
this.method = this.app._event.httpMethod ? this.app._event.httpMethod.toUpperCase() : 'GET' | ||
this.method = this.app._event.httpMethod | ||
? this.app._event.httpMethod.toUpperCase() | ||
: this.app._event.requestContext && this.app._event.requestContext.http | ||
? this.app._event.requestContext.http.method.toUpperCase() | ||
: 'GET'; | ||
// Set the path | ||
this.path = this.app._event.path | ||
this.path = | ||
this.payloadVersion === '2.0' | ||
? this.app._event.rawPath | ||
: this.app._event.path; | ||
// Set the query parameters (backfill for ALB) | ||
this.query = Object.assign({}, this.app._event.queryStringParameters, | ||
'queryStringParameters' in this.app._event ? {} // do nothing | ||
: Object.keys(Object.assign({},this.app._event.multiValueQueryStringParameters)) | ||
.reduce((qs,key) => Object.assign(qs, // get the last value of the array | ||
{ [key]: decodeURIComponent(this.app._event.multiValueQueryStringParameters[key].slice(-1)[0]) } | ||
), {}) | ||
) | ||
this.query = Object.assign( | ||
{}, | ||
this.app._event.queryStringParameters, | ||
'queryStringParameters' in this.app._event | ||
? {} // do nothing | ||
: Object.keys( | ||
Object.assign({}, this.app._event.multiValueQueryStringParameters) | ||
).reduce( | ||
(qs, key) => | ||
Object.assign( | ||
qs, // get the last value of the array | ||
{ | ||
[key]: decodeURIComponent( | ||
this.app._event.multiValueQueryStringParameters[key].slice( | ||
-1 | ||
)[0] | ||
), | ||
} | ||
), | ||
{} | ||
) | ||
); | ||
// Set the multi-value query parameters (simulate if no multi-value support) | ||
this.multiValueQuery = Object.assign({}, | ||
this._multiValueSupport ? {} : Object.keys(this.query) | ||
.reduce((qs,key) => Object.assign(qs, { [key]: [this.query[key]] }), {}), | ||
this.app._event.multiValueQueryStringParameters) | ||
this.multiValueQuery = Object.assign( | ||
{}, | ||
this._multiValueSupport | ||
? {} | ||
: Object.keys(this.query).reduce( | ||
(qs, key) => | ||
Object.assign(qs, { [key]: this.query[key].split(',') }), | ||
{} | ||
), | ||
this.app._event.multiValueQueryStringParameters | ||
); | ||
// Set the raw headers (normalize multi-values) | ||
// per https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 | ||
this.rawHeaders = this._multiValueSupport && this.app._event.multiValueHeaders !== null ? | ||
Object.keys(this.app._event.multiValueHeaders).reduce((headers,key) => | ||
Object.assign(headers,{ [key]: UTILS.fromArray(this.app._event.multiValueHeaders[key]) }),{}) | ||
: this.app._event.headers || {} | ||
this.rawHeaders = | ||
this._multiValueSupport && this.app._event.multiValueHeaders !== null | ||
? Object.keys(this.app._event.multiValueHeaders).reduce( | ||
(headers, key) => | ||
Object.assign(headers, { | ||
[key]: UTILS.fromArray(this.app._event.multiValueHeaders[key]), | ||
}), | ||
{} | ||
) | ||
: this.app._event.headers || {}; | ||
// Set the headers to lowercase | ||
this.headers = Object.keys(this.rawHeaders).reduce((acc,header) => | ||
Object.assign(acc,{[header.toLowerCase()]:this.rawHeaders[header]}), {}) | ||
this.headers = Object.keys(this.rawHeaders).reduce( | ||
(acc, header) => | ||
Object.assign(acc, { [header.toLowerCase()]: this.rawHeaders[header] }), | ||
{} | ||
); | ||
this.multiValueHeaders = this._multiValueSupport ? this.app._event.multiValueHeaders | ||
: Object.keys(this.headers).reduce((headers,key) => | ||
Object.assign(headers,{ [key.toLowerCase()]: [this.headers[key]] }),{}) | ||
this.multiValueHeaders = this._multiValueSupport | ||
? this.app._event.multiValueHeaders | ||
: Object.keys(this.headers).reduce( | ||
(headers, key) => | ||
Object.assign(headers, { | ||
[key.toLowerCase()]: this.headers[key].split(','), | ||
}), | ||
{} | ||
); | ||
// Extract user agent | ||
this.userAgent = this.headers['user-agent'] | ||
this.userAgent = this.headers['user-agent']; | ||
// Get cookies from event | ||
let cookies = this.app._event.cookies | ||
? this.app._event.cookies | ||
: this.headers.cookie | ||
? this.headers.cookie.split(';') | ||
: []; | ||
// Set and parse cookies | ||
this.cookies = this.headers.cookie ? | ||
this.headers.cookie.split(';') | ||
.reduce( | ||
(acc,cookie) => { | ||
cookie = cookie.trim().split('=') | ||
return Object.assign(acc,{ [cookie[0]] : UTILS.parseBody(decodeURIComponent(cookie[1])) }) | ||
}, | ||
{} | ||
) : {} | ||
this.cookies = cookies.reduce((acc, cookie) => { | ||
cookie = cookie.trim().split('='); | ||
return Object.assign(acc, { | ||
[cookie[0]]: UTILS.parseBody(decodeURIComponent(cookie[1])), | ||
}); | ||
}, {}); | ||
// Attempt to parse the auth | ||
this.auth = UTILS.parseAuth(this.headers.authorization) | ||
this.auth = UTILS.parseAuth(this.headers.authorization); | ||
// Set the requestContext | ||
this.requestContext = this.app._event.requestContext || {} | ||
this.requestContext = this.app._event.requestContext || {}; | ||
// Extract IP (w/ sourceIp fallback) | ||
this.ip = (this.headers['x-forwarded-for'] && this.headers['x-forwarded-for'].split(',')[0].trim()) | ||
|| (this.requestContext['identity'] && this.requestContext['identity']['sourceIp'] | ||
&& this.requestContext['identity']['sourceIp'].split(',')[0].trim()) | ||
this.ip = | ||
(this.headers['x-forwarded-for'] && | ||
this.headers['x-forwarded-for'].split(',')[0].trim()) || | ||
(this.requestContext['identity'] && | ||
this.requestContext['identity']['sourceIp'] && | ||
this.requestContext['identity']['sourceIp'].split(',')[0].trim()); | ||
// Assign the requesting interface | ||
this.interface = this.requestContext.elb ? 'alb' : 'apigateway' | ||
this.interface = this.requestContext.elb ? 'alb' : 'apigateway'; | ||
// Set the pathParameters | ||
this.pathParameters = this.app._event.pathParameters || {} | ||
this.pathParameters = this.app._event.pathParameters || {}; | ||
// Set the stageVariables | ||
this.stageVariables = this.app._event.stageVariables || {} | ||
this.stageVariables = this.app._event.stageVariables || {}; | ||
// Set the isBase64Encoded | ||
this.isBase64Encoded = this.app._event.isBase64Encoded || false | ||
this.isBase64Encoded = this.app._event.isBase64Encoded || false; | ||
// Add context | ||
this.context = this.app.context && typeof this.app.context === 'object' ? this.app.context : {} | ||
this.context = | ||
this.app.context && typeof this.app.context === 'object' | ||
? this.app.context | ||
: {}; | ||
// Parse id from context | ||
this.id = this.context.awsRequestId ? this.context.awsRequestId : null | ||
this.id = this.context.awsRequestId ? this.context.awsRequestId : null; | ||
// Determine client type | ||
this.clientType = | ||
this.headers['cloudfront-is-desktop-viewer'] === 'true' ? 'desktop' : | ||
this.headers['cloudfront-is-mobile-viewer'] === 'true' ? 'mobile' : | ||
this.headers['cloudfront-is-smarttv-viewer'] === 'true' ? 'tv' : | ||
this.headers['cloudfront-is-tablet-viewer'] === 'true' ? 'tablet' : | ||
'unknown' | ||
this.headers['cloudfront-is-desktop-viewer'] === 'true' | ||
? 'desktop' | ||
: this.headers['cloudfront-is-mobile-viewer'] === 'true' | ||
? 'mobile' | ||
: this.headers['cloudfront-is-smarttv-viewer'] === 'true' | ||
? 'tv' | ||
: this.headers['cloudfront-is-tablet-viewer'] === 'true' | ||
? 'tablet' | ||
: 'unknown'; | ||
// Parse country | ||
this.clientCountry = this.headers['cloudfront-viewer-country'] ? | ||
this.headers['cloudfront-viewer-country'].toUpperCase() : 'unknown' | ||
this.clientCountry = this.headers['cloudfront-viewer-country'] | ||
? this.headers['cloudfront-viewer-country'].toUpperCase() | ||
: 'unknown'; | ||
// Capture the raw body | ||
this.rawBody = this.app._event.body | ||
this.rawBody = this.app._event.body; | ||
// Set the body (decode it if base64 encoded) | ||
this.body = this.app._event.isBase64Encoded ? Buffer.from(this.app._event.body || '', 'base64').toString() : this.app._event.body | ||
this.body = this.app._event.isBase64Encoded | ||
? Buffer.from(this.app._event.body || '', 'base64').toString() | ||
: this.app._event.body; | ||
// Set the body | ||
if (this.headers['content-type'] && this.headers['content-type'].includes('application/x-www-form-urlencoded')) { | ||
this.body = QS.parse(this.body) | ||
if ( | ||
this.headers['content-type'] && | ||
this.headers['content-type'].includes('application/x-www-form-urlencoded') | ||
) { | ||
this.body = QS.parse(this.body); | ||
} else if (typeof this.body === 'object') { | ||
this.body = this.body | ||
// Do nothing | ||
} else { | ||
this.body = UTILS.parseBody(this.body) | ||
this.body = UTILS.parseBody(this.body); | ||
} | ||
// Init the stack reporter | ||
this.stack = null | ||
this.stack = null; | ||
// Extract path from event (strip querystring just in case) | ||
let path = UTILS.parsePath(this.path) | ||
let path = UTILS.parsePath(this.path); | ||
// Init the route | ||
this.route = null | ||
this.route = null; | ||
// Create a local routes reference | ||
let routes = this.app._routes | ||
let routes = this.app._routes; | ||
// Init wildcard | ||
let wc = [] | ||
let wc = []; | ||
// Loop the routes and see if this matches | ||
for (let i=0; i<path.length; i++) { | ||
for (let i = 0; i < path.length; i++) { | ||
// Capture wildcard routes | ||
if (routes['ROUTES'] && routes['ROUTES']['*']) { wc.push(routes['ROUTES']['*']) } | ||
if (routes['ROUTES'] && routes['ROUTES']['*']) { | ||
wc.push(routes['ROUTES']['*']); | ||
} | ||
// Traverse routes | ||
if (routes['ROUTES'] && routes['ROUTES'][path[i]]) { | ||
routes = routes['ROUTES'][path[i]] | ||
routes = routes['ROUTES'][path[i]]; | ||
} else if (routes['ROUTES'] && routes['ROUTES']['__VAR__']) { | ||
routes = routes['ROUTES']['__VAR__'] | ||
routes = routes['ROUTES']['__VAR__']; | ||
} else if ( | ||
wc[wc.length-1] | ||
&& wc[wc.length-1]['METHODS'] | ||
wc[wc.length - 1] && | ||
wc[wc.length - 1]['METHODS'] && | ||
// && (wc[wc.length-1]['METHODS'][this.method] || wc[wc.length-1]['METHODS']['ANY']) | ||
&& ( | ||
(this.method !== 'OPTIONS' | ||
&& Object.keys(wc[wc.length-1]['METHODS']).toString() !== 'OPTIONS') | ||
|| this.validWildcard(wc,this.method) | ||
) | ||
((this.method !== 'OPTIONS' && | ||
Object.keys(wc[wc.length - 1]['METHODS']).toString() !== 'OPTIONS') || | ||
this.validWildcard(wc, this.method)) | ||
) { | ||
routes = wc[wc.length-1] | ||
routes = wc[wc.length - 1]; | ||
} else { | ||
this.app._errorStatus = 404 | ||
throw new RouteError('Route not found','/'+path.join('/')) | ||
this.app._errorStatus = 404; | ||
throw new RouteError('Route not found', '/' + path.join('/')); | ||
} | ||
} // end for loop | ||
// Grab the deepest wildcard path | ||
let wildcard = wc.pop() | ||
let wildcard = wc.pop(); | ||
// Select ROUTE if exist for method, default ANY, apply wildcards, alias HEAD requests | ||
let route = routes['METHODS'] && routes['METHODS'][this.method] ? routes['METHODS'][this.method] : | ||
(routes['METHODS'] && routes['METHODS']['ANY'] ? routes['METHODS']['ANY'] : | ||
(wildcard && wildcard['METHODS'] && wildcard['METHODS'][this.method] ? wildcard['METHODS'][this.method] : | ||
(wildcard && wildcard['METHODS'] && wildcard['METHODS']['ANY'] ? wildcard['METHODS']['ANY'] : | ||
(this.method === 'HEAD' && routes['METHODS'] && routes['METHODS']['GET'] ? routes['METHODS']['GET'] : | ||
undefined)))) | ||
let route = | ||
routes['METHODS'] && routes['METHODS'][this.method] | ||
? routes['METHODS'][this.method] | ||
: routes['METHODS'] && routes['METHODS']['ANY'] | ||
? routes['METHODS']['ANY'] | ||
: wildcard && wildcard['METHODS'] && wildcard['METHODS'][this.method] | ||
? wildcard['METHODS'][this.method] | ||
: wildcard && wildcard['METHODS'] && wildcard['METHODS']['ANY'] | ||
? wildcard['METHODS']['ANY'] | ||
: this.method === 'HEAD' && | ||
routes['METHODS'] && | ||
routes['METHODS']['GET'] | ||
? routes['METHODS']['GET'] | ||
: undefined; | ||
// Check for the requested method | ||
if (route) { | ||
// Assign path parameters | ||
for (let x in route.vars) { | ||
route.vars[x].map(y => this.params[y] = path[x]) | ||
route.vars[x].map((y) => (this.params[y] = path[x])); | ||
} // end for | ||
// Set the route used | ||
this.route = route.route | ||
this.route = route.route; | ||
// Set the execution stack | ||
this._stack = route.inherited.concat(route.stack) | ||
// this._stack = route.inherited.concat(route.stack); | ||
this._stack = route.stack; | ||
// Set the stack reporter | ||
this.stack = this._stack.map(x => x.name.trim() !== '' ? x.name : 'unnamed') | ||
this.stack = this._stack.map((x) => | ||
x.name.trim() !== '' ? x.name : 'unnamed' | ||
); | ||
} else { | ||
this.app._errorStatus = 405 | ||
throw new MethodError('Method not allowed',this.method,'/'+path.join('/')) | ||
this.app._errorStatus = 405; | ||
throw new MethodError( | ||
'Method not allowed', | ||
this.method, | ||
'/' + path.join('/') | ||
); | ||
} | ||
// Reference to sample rule | ||
this._sampleRule = {} | ||
this._sampleRule = {}; | ||
// Enable sampling | ||
this._sample = LOGGER.sampler(this.app,this) | ||
this._sample = LOGGER.sampler(this.app, this); | ||
} // end parseRequest | ||
@@ -264,4 +344,6 @@ | ||
this.app._logLevels[args[0]] >= | ||
this.app._logLevels[this._sample ? this._sample : this.app._logger.level] && | ||
this._logs.push(this.app._logger.log(...args)) | ||
this.app._logLevels[ | ||
this._sample ? this._sample : this.app._logger.level | ||
] && | ||
this._logs.push(this.app._logger.log(...args)); | ||
} | ||
@@ -271,9 +353,10 @@ | ||
validWildcard(wc) { | ||
return Object.keys(wc[wc.length-1]['METHODS']).length > 1 | ||
|| (wc.length > 1 && this.validWildcard(wc.slice(0,-1))) | ||
return ( | ||
Object.keys(wc[wc.length - 1]['METHODS']).length > 1 || | ||
(wc.length > 1 && this.validWildcard(wc.slice(0, -1))) | ||
); | ||
} | ||
} // end REQUEST class | ||
// Export the response object | ||
module.exports = REQUEST | ||
module.exports = REQUEST; |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -9,51 +9,56 @@ /** | ||
const UTILS = require('./utils.js') | ||
const UTILS = require('./utils.js'); | ||
const fs = require('fs') // Require Node.js file system | ||
const path = require('path') // Require Node.js path | ||
const { ResponseError, FileError } = require('./errors') // Require custom errors | ||
const fs = require('fs'); // Require Node.js file system | ||
const path = require('path'); // Require Node.js path | ||
const compression = require('./compression'); // Require compression lib | ||
const { ResponseError, FileError } = require('./errors'); // Require custom errors | ||
// Require AWS S3 service | ||
const S3 = require('./s3-service') | ||
const S3 = require('./s3-service'); | ||
class RESPONSE { | ||
// Create the constructor function. | ||
constructor(app,request) { | ||
constructor(app, request) { | ||
// Add a reference to the main app | ||
app._response = this | ||
app._response = this; | ||
// Create a reference to the app | ||
this.app = app | ||
this.app = app; | ||
// Create a reference to the request | ||
this._request = request | ||
this._request = request; | ||
// Create a reference to the JSON serializer | ||
this._serializer = app._serializer | ||
this._serializer = app._serializer; | ||
// Set the default state to processing | ||
this._state = 'processing' | ||
this._state = 'processing'; | ||
// Default statusCode to 200 | ||
this._statusCode = 200 | ||
this._statusCode = 200; | ||
// Default the header | ||
this._headers = Object.assign({ | ||
// Set the Content-Type by default | ||
'content-type': ['application/json'], //charset=UTF-8 | ||
}, app._headers) | ||
this._headers = Object.assign( | ||
{ | ||
// Set the Content-Type by default | ||
'content-type': ['application/json'], //charset=UTF-8 | ||
}, | ||
app._headers | ||
); | ||
// base64 encoding flag | ||
this._isBase64 = app._isBase64 | ||
this._isBase64 = app._isBase64; | ||
// compression flag | ||
this._compression = app._compression; | ||
// Default callback function | ||
this._callback = 'callback' | ||
this._callback = 'callback'; | ||
// Default Etag support | ||
this._etag = false | ||
this._etag = false; | ||
// Default response object | ||
this._response = {} | ||
this._response = {}; | ||
} | ||
@@ -63,29 +68,37 @@ | ||
status(code) { | ||
this._statusCode = code | ||
return this | ||
this._statusCode = code; | ||
return this; | ||
} | ||
// Adds a header field | ||
header(key,value,append) { | ||
let _key = key.toLowerCase() // store as lowercase | ||
let _values = value ? (Array.isArray(value) ? value : [value]) : [''] | ||
this._headers[_key] = append ? | ||
this.hasHeader(_key) ? this._headers[_key].concat(_values) : _values | ||
: _values | ||
return this | ||
header(key, value, append) { | ||
let _key = key.toLowerCase(); // store as lowercase | ||
let _values = value ? (Array.isArray(value) ? value : [value]) : ['']; | ||
this._headers[_key] = append | ||
? this.hasHeader(_key) | ||
? this._headers[_key].concat(_values) | ||
: _values | ||
: _values; | ||
return this; | ||
} | ||
// Gets a header field | ||
getHeader(key,asArr) { | ||
if (!key) return asArr ? this._headers : | ||
Object.keys(this._headers).reduce((headers,key) => | ||
Object.assign(headers, { [key]: this._headers[key].toString() }) | ||
,{}) // return all headers | ||
return asArr ? this._headers[key.toLowerCase()] | ||
: this._headers[key.toLowerCase()] ? | ||
this._headers[key.toLowerCase()].toString() : undefined | ||
getHeader(key, asArr) { | ||
if (!key) | ||
return asArr | ||
? this._headers | ||
: Object.keys(this._headers).reduce( | ||
(headers, key) => | ||
Object.assign(headers, { [key]: this._headers[key].toString() }), | ||
{} | ||
); // return all headers | ||
return asArr | ||
? this._headers[key.toLowerCase()] | ||
: this._headers[key.toLowerCase()] | ||
? this._headers[key.toLowerCase()].toString() | ||
: undefined; | ||
} | ||
getHeaders() { | ||
return this._headers | ||
return this._headers; | ||
} | ||
@@ -95,4 +108,4 @@ | ||
removeHeader(key) { | ||
delete this._headers[key.toLowerCase()] | ||
return this | ||
delete this._headers[key.toLowerCase()]; | ||
return this; | ||
} | ||
@@ -102,3 +115,3 @@ | ||
hasHeader(key) { | ||
return this.getHeader(key ? key : '') !== undefined | ||
return this.getHeader(key ? key : '') !== undefined; | ||
} | ||
@@ -108,3 +121,5 @@ | ||
json(body) { | ||
this.header('Content-Type','application/json').send(this._serializer(body)) | ||
this.header('Content-Type', 'application/json').send( | ||
this._serializer(body) | ||
); | ||
} | ||
@@ -115,7 +130,11 @@ | ||
// Check the querystring for callback or cb | ||
let query = this.app._event.queryStringParameters || {} | ||
let cb = query[this.app._callbackName] | ||
let query = this.app._event.queryStringParameters || {}; | ||
let cb = query[this.app._callbackName]; | ||
this.header('Content-Type','application/json') | ||
.send((cb ? cb.replace(' ','_') : 'callback') + '(' + this._serializer(body) + ')') | ||
this.header('Content-Type', 'application/json').send( | ||
(cb ? cb.replace(' ', '_') : 'callback') + | ||
'(' + | ||
this._serializer(body) + | ||
')' | ||
); | ||
} | ||
@@ -125,3 +144,3 @@ | ||
html(body) { | ||
this.header('Content-Type','text/html').send(body) | ||
this.header('Content-Type', 'text/html').send(body); | ||
} | ||
@@ -131,4 +150,4 @@ | ||
location(path) { | ||
this.header('Location',UTILS.encodeUrl(path)) | ||
return this | ||
this.header('Location', UTILS.encodeUrl(path)); | ||
return this; | ||
} | ||
@@ -138,3 +157,3 @@ | ||
async redirect(path) { | ||
let statusCode = 302 // default | ||
let statusCode = 302; // default | ||
@@ -144,7 +163,10 @@ try { | ||
if (arguments.length === 2) { | ||
if ([300,301,302,303,307,308].includes(arguments[0])) { | ||
statusCode = arguments[0] | ||
path = arguments[1] | ||
if ([300, 301, 302, 303, 307, 308].includes(arguments[0])) { | ||
statusCode = arguments[0]; | ||
path = arguments[1]; | ||
} else { | ||
throw new ResponseError(arguments[0] + ' is an invalid redirect status code',arguments[0]) | ||
throw new ResponseError( | ||
arguments[0] + ' is an invalid redirect status code', | ||
arguments[0] | ||
); | ||
} | ||
@@ -154,12 +176,13 @@ } | ||
// Auto convert S3 paths to signed URLs | ||
if (UTILS.isS3(path)) path = await this.getLink(path) | ||
if (UTILS.isS3(path)) path = await this.getLink(path); | ||
let url = UTILS.escapeHtml(path) | ||
let url = UTILS.escapeHtml(path); | ||
this.location(path) | ||
.status(statusCode) | ||
.html(`<p>${statusCode} Redirecting to <a href="${url}">${url}</a></p>`) | ||
} catch(e) { | ||
this.error(e) | ||
.html( | ||
`<p>${statusCode} Redirecting to <a href="${url}">${url}</a></p>` | ||
); | ||
} catch (e) { | ||
this.error(e); | ||
} | ||
@@ -169,21 +192,29 @@ } // end redirect | ||
// Convenience method for retrieving a signed link to an S3 bucket object | ||
async getLink(path,expires,callback) { | ||
let params = UTILS.parseS3(path) | ||
async getLink(path, expires, callback) { | ||
let params = UTILS.parseS3(path); | ||
// Default Expires | ||
params.Expires = !isNaN(expires) ? parseInt(expires) : 900 | ||
params.Expires = !isNaN(expires) ? parseInt(expires) : 900; | ||
// Default callback | ||
let fn = typeof expires === 'function' ? expires : | ||
typeof callback === 'function' ? callback : e => { if (e) this.error(e) } | ||
let fn = | ||
typeof expires === 'function' | ||
? expires | ||
: typeof callback === 'function' | ||
? callback | ||
: (e) => { | ||
if (e) this.error(e); | ||
}; | ||
// getSignedUrl doesn't support .promise() | ||
return await new Promise(r => S3.getSignedUrl('getObject',params, async (e,url) => { | ||
if (e) { | ||
// Execute callback with caught error | ||
await fn(e) | ||
this.error(e) // Throw error if not done in callback | ||
} | ||
r(url) // return the url | ||
})) | ||
return await new Promise((r) => | ||
S3.getSignedUrl('getObject', params, async (e, url) => { | ||
if (e) { | ||
// Execute callback with caught error | ||
await fn(e); | ||
this.error(e); // Throw error if not done in callback | ||
} | ||
r(url); // return the url | ||
}) | ||
); | ||
} // end getLink | ||
@@ -193,73 +224,90 @@ | ||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie | ||
cookie(name,value,opts={}) { | ||
cookie(name, value, opts = {}) { | ||
// Set the name and value of the cookie | ||
let cookieString = (typeof name !== 'string' ? name.toString() : name) | ||
+ '=' + encodeURIComponent(UTILS.encodeBody(value)) | ||
let cookieString = | ||
(typeof name !== 'string' ? name.toString() : name) + | ||
'=' + | ||
encodeURIComponent(UTILS.encodeBody(value)); | ||
// domain (String): Domain name for the cookie | ||
cookieString += opts.domain ? '; Domain=' + opts.domain : '' | ||
cookieString += opts.domain ? '; Domain=' + opts.domain : ''; | ||
// expires (Date): Expiry date of the cookie, convert to GMT | ||
cookieString += opts.expires && typeof opts.expires.toUTCString === 'function' ? | ||
'; Expires=' + opts.expires.toUTCString() : '' | ||
cookieString += | ||
opts.expires && typeof opts.expires.toUTCString === 'function' | ||
? '; Expires=' + opts.expires.toUTCString() | ||
: ''; | ||
// httpOnly (Boolean): Flags the cookie to be accessible only by the web server | ||
cookieString += opts.httpOnly && opts.httpOnly === true ? '; HttpOnly' : '' | ||
cookieString += opts.httpOnly && opts.httpOnly === true ? '; HttpOnly' : ''; | ||
// maxAge (Number) Set expiry time relative to the current time in milliseconds | ||
cookieString += opts.maxAge && !isNaN(opts.maxAge) ? | ||
'; MaxAge=' + (opts.maxAge/1000|0) | ||
+ (!opts.expires ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() : '') | ||
: '' | ||
cookieString += | ||
opts.maxAge && !isNaN(opts.maxAge) | ||
? '; MaxAge=' + | ||
((opts.maxAge / 1000) | 0) + | ||
(!opts.expires | ||
? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() | ||
: '') | ||
: ''; | ||
// path (String): Path for the cookie | ||
cookieString += opts.path ? '; Path=' + opts.path : '; Path=/' | ||
cookieString += opts.path ? '; Path=' + opts.path : '; Path=/'; | ||
// secure (Boolean): Marks the cookie to be used with HTTPS only | ||
cookieString += opts.secure && opts.secure === true ? '; Secure' : '' | ||
cookieString += opts.secure && opts.secure === true ? '; Secure' : ''; | ||
// sameSite (Boolean or String) Value of the “SameSite” Set-Cookie attribute | ||
// see https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1. | ||
cookieString += opts.sameSite !== undefined ? '; SameSite=' | ||
+ (opts.sameSite === true ? 'Strict' : | ||
(opts.sameSite === false ? 'Lax' : opts.sameSite )) | ||
: '' | ||
cookieString += | ||
opts.sameSite !== undefined | ||
? '; SameSite=' + | ||
(opts.sameSite === true | ||
? 'Strict' | ||
: opts.sameSite === false | ||
? 'Lax' | ||
: opts.sameSite) | ||
: ''; | ||
this.header('Set-Cookie',cookieString,true) | ||
return this | ||
this.header('Set-Cookie', cookieString, true); | ||
return this; | ||
} | ||
// Convenience method for clearing cookies | ||
clearCookie(name,opts={}) { | ||
let options = Object.assign(opts, { expires: new Date(1), maxAge: -1000 }) | ||
return this.cookie(name,'',options) | ||
clearCookie(name, opts = {}) { | ||
let options = Object.assign(opts, { expires: new Date(1), maxAge: -1000 }); | ||
return this.cookie(name, '', options); | ||
} | ||
// Set content-disposition header and content type | ||
attachment(filename) { | ||
// Check for supplied filename/path | ||
let name = typeof filename === 'string' && filename.trim().length > 0 ? path.parse(filename) : undefined | ||
this.header('Content-Disposition','attachment' + (name ? '; filename="' + name.base + '"' : '')) | ||
let name = | ||
typeof filename === 'string' && filename.trim().length > 0 | ||
? path.parse(filename) | ||
: undefined; | ||
this.header( | ||
'Content-Disposition', | ||
'attachment' + (name ? '; filename="' + name.base + '"' : '') | ||
); | ||
// If name exits, attempt to set the type | ||
if (name) { this.type(name.ext) } | ||
return this | ||
if (name) { | ||
this.type(name.ext); | ||
} | ||
return this; | ||
} | ||
// Convenience method combining attachment() and sendFile() | ||
download(file, filename, options, callback) { | ||
let name = filename; | ||
let opts = typeof options === 'object' ? options : {}; | ||
let fn = typeof callback === 'function' ? callback : undefined; | ||
let name = filename | ||
let opts = typeof options === 'object' ? options : {} | ||
let fn = typeof callback === 'function' ? callback : undefined | ||
// Add optional parameter support for callback | ||
if (typeof filename === 'function') { | ||
name = undefined | ||
fn = filename | ||
name = undefined; | ||
fn = filename; | ||
} else if (typeof options === 'function') { | ||
fn = options | ||
fn = options; | ||
} | ||
@@ -269,26 +317,25 @@ | ||
if (typeof filename === 'object') { | ||
name = undefined | ||
opts = filename | ||
name = undefined; | ||
opts = filename; | ||
} | ||
// Add the Content-Disposition header | ||
this.attachment(name ? name : (typeof file === 'string' ? path.basename(file) : null) ) | ||
this.attachment( | ||
name ? name : typeof file === 'string' ? path.basename(file) : null | ||
); | ||
// Send the file | ||
this.sendFile(file, opts, fn) | ||
this.sendFile(file, opts, fn); | ||
} | ||
// Convenience method for returning static files | ||
async sendFile(file, options, callback) { | ||
let buffer, modified; | ||
let buffer, modified | ||
let opts = typeof options === 'object' ? options : {}; | ||
let fn = typeof callback === 'function' ? callback : () => {}; | ||
let opts = typeof options === 'object' ? options : {} | ||
let fn = typeof callback === 'function' ? callback : () => {} | ||
// Add optional parameter support | ||
if (typeof options === 'function') { | ||
fn = options | ||
fn = options; | ||
} | ||
@@ -298,33 +345,33 @@ | ||
try { | ||
// Create buffer based on input | ||
if (typeof file === 'string') { | ||
let filepath = file.trim(); | ||
let filepath = file.trim() | ||
// If an S3 file identifier | ||
if (/^s3:\/\//i.test(filepath)) { | ||
let params = UTILS.parseS3(filepath); | ||
let params = UTILS.parseS3(filepath) | ||
// Attempt to get the object from S3 | ||
let data = await S3.getObject(params).promise() | ||
let data = await S3.getObject(params).promise(); | ||
// Set results, type and header | ||
buffer = data.Body | ||
modified = data.LastModified | ||
this.type(data.ContentType) | ||
this.header('ETag',data.ETag) | ||
buffer = data.Body; | ||
modified = data.LastModified; | ||
this.type(data.ContentType); | ||
this.header('ETag', data.ETag); | ||
// else try and load the file locally | ||
// else try and load the file locally | ||
} else { | ||
buffer = fs.readFileSync((opts.root ? opts.root : '') + filepath) | ||
modified = opts.lastModified !== false ? fs.statSync((opts.root ? opts.root : '') + filepath).mtime : undefined | ||
this.type(path.extname(filepath)) | ||
buffer = fs.readFileSync((opts.root ? opts.root : '') + filepath); | ||
modified = | ||
opts.lastModified !== false | ||
? fs.statSync((opts.root ? opts.root : '') + filepath).mtime | ||
: undefined; | ||
this.type(path.extname(filepath)); | ||
} | ||
// If the input is a buffer, pass through | ||
// If the input is a buffer, pass through | ||
} else if (Buffer.isBuffer(file)) { | ||
buffer = file | ||
buffer = file; | ||
} else { | ||
throw new FileError('Invalid file',{path:file}) | ||
throw new FileError('Invalid file', { path: file }); | ||
} | ||
@@ -334,5 +381,5 @@ | ||
if (typeof opts.headers === 'object') { | ||
Object.keys(opts.headers).map(header => { | ||
this.header(header,opts.headers[header]) | ||
}) | ||
Object.keys(opts.headers).map((header) => { | ||
this.header(header, opts.headers[header]); | ||
}); | ||
} | ||
@@ -343,8 +390,5 @@ | ||
if (opts.cacheControl !== true && opts.cacheControl !== undefined) { | ||
this.cache(opts.cacheControl) | ||
this.cache(opts.cacheControl); | ||
} else { | ||
this.cache( | ||
!isNaN(opts.maxAge) ? opts.maxAge : 0, | ||
opts.private | ||
) | ||
this.cache(!isNaN(opts.maxAge) ? opts.maxAge : 0, opts.private); | ||
} | ||
@@ -355,87 +399,110 @@ } | ||
if (opts.lastModified !== false) { | ||
this.modified(opts.lastModified ? opts.lastModified : modified) | ||
this.modified(opts.lastModified ? opts.lastModified : modified); | ||
} | ||
// Execute callback | ||
await fn() | ||
await fn(); | ||
// Set base64 encoding flag | ||
this._isBase64 = true | ||
this._isBase64 = true; | ||
// Convert buffer to base64 string | ||
this.send(buffer.toString('base64')) | ||
} catch(e) { // TODO: Add second catch? | ||
this.send(buffer.toString('base64')); | ||
} catch (e) { | ||
// TODO: Add second catch? | ||
// Execute callback with caught error | ||
await fn(e) | ||
await fn(e); | ||
// If missing file | ||
if (e.code === 'ENOENT') { | ||
this.error(new FileError('No such file',e)) | ||
this.error(new FileError('No such file', e)); | ||
} else { | ||
this.error(e) // Throw error if not done in callback | ||
this.error(e); // Throw error if not done in callback | ||
} | ||
} | ||
} // end sendFile | ||
// Convenience method for setting type | ||
type(type) { | ||
let mimeType = UTILS.mimeLookup(type,this.app._mimeTypes) | ||
let mimeType = UTILS.mimeLookup(type, this.app._mimeTypes); | ||
if (mimeType) { | ||
this.header('Content-Type',mimeType) | ||
this.header('Content-Type', mimeType); | ||
} | ||
return this | ||
return this; | ||
} | ||
// Convenience method for sending status codes | ||
sendStatus(status) { | ||
this.status(status).send(UTILS.statusLookup(status)) | ||
this.status(status).send(UTILS.statusLookup(status)); | ||
} | ||
// Convenience method for setting CORS headers | ||
cors(options) { | ||
const opts = typeof options === 'object' ? options : {} | ||
const opts = typeof options === 'object' ? options : {}; | ||
// Check for existing headers | ||
let acao = this.getHeader('Access-Control-Allow-Origin') | ||
let acam = this.getHeader('Access-Control-Allow-Methods') | ||
let acah = this.getHeader('Access-Control-Allow-Headers') | ||
let acao = this.getHeader('Access-Control-Allow-Origin'); | ||
let acam = this.getHeader('Access-Control-Allow-Methods'); | ||
let acah = this.getHeader('Access-Control-Allow-Headers'); | ||
// Default CORS headers | ||
this.header('Access-Control-Allow-Origin',opts.origin ? opts.origin : (acao ? acao : '*')) | ||
this.header('Access-Control-Allow-Methods',opts.methods ? opts.methods : (acam ? acam : 'GET, PUT, POST, DELETE, OPTIONS')) | ||
this.header('Access-Control-Allow-Headers',opts.headers ? opts.headers : (acah ? acah : 'Content-Type, Authorization, Content-Length, X-Requested-With')) | ||
this.header( | ||
'Access-Control-Allow-Origin', | ||
opts.origin ? opts.origin : acao ? acao : '*' | ||
); | ||
this.header( | ||
'Access-Control-Allow-Methods', | ||
opts.methods | ||
? opts.methods | ||
: acam | ||
? acam | ||
: 'GET, PUT, POST, DELETE, OPTIONS' | ||
); | ||
this.header( | ||
'Access-Control-Allow-Headers', | ||
opts.headers | ||
? opts.headers | ||
: acah | ||
? acah | ||
: 'Content-Type, Authorization, Content-Length, X-Requested-With' | ||
); | ||
// Optional CORS headers | ||
if(opts.maxAge && !isNaN(opts.maxAge)) this.header('Access-Control-Max-Age',(opts.maxAge/1000|0).toString()) | ||
if(opts.credentials) this.header('Access-Control-Allow-Credentials',opts.credentials.toString()) | ||
if(opts.exposeHeaders) this.header('Access-Control-Expose-Headers',opts.exposeHeaders) | ||
if (opts.maxAge && !isNaN(opts.maxAge)) | ||
this.header( | ||
'Access-Control-Max-Age', | ||
((opts.maxAge / 1000) | 0).toString() | ||
); | ||
if (opts.credentials) | ||
this.header( | ||
'Access-Control-Allow-Credentials', | ||
opts.credentials.toString() | ||
); | ||
if (opts.exposeHeaders) | ||
this.header('Access-Control-Expose-Headers', opts.exposeHeaders); | ||
return this | ||
return this; | ||
} | ||
// Enable/Disable Etag | ||
etag(enable) { | ||
this._etag = enable === true ? true : false | ||
return this | ||
this._etag = enable === true ? true : false; | ||
return this; | ||
} | ||
// Add cache-control headers | ||
cache(maxAge,isPrivate=false) { | ||
cache(maxAge, isPrivate = false) { | ||
// if custom string value | ||
if (maxAge !== true && maxAge !== undefined && typeof maxAge === 'string') { | ||
this.header('Cache-Control', maxAge) | ||
this.header('Cache-Control', maxAge); | ||
} else if (maxAge === false) { | ||
this.header('Cache-Control', 'no-cache, no-store, must-revalidate') | ||
this.header('Cache-Control', 'no-cache, no-store, must-revalidate'); | ||
} else { | ||
maxAge = maxAge && !isNaN(maxAge) ? (maxAge/1000|0) : 0 | ||
this.header('Cache-Control', (isPrivate === true ? 'private, ' : '') + 'max-age=' + maxAge) | ||
this.header('Expires',new Date(Date.now() + maxAge).toUTCString()) | ||
maxAge = maxAge && !isNaN(maxAge) ? (maxAge / 1000) | 0 : 0; | ||
this.header( | ||
'Cache-Control', | ||
(isPrivate === true ? 'private, ' : '') + 'max-age=' + maxAge | ||
); | ||
this.header('Expires', new Date(Date.now() + maxAge).toUTCString()); | ||
} | ||
return this | ||
return this; | ||
} | ||
@@ -446,7 +513,11 @@ | ||
if (date !== false) { | ||
let lastModified = date && typeof date.toUTCString === 'function' ? date : | ||
date && Date.parse(date) ? new Date(date) : new Date() | ||
this.header('Last-Modified', lastModified.toUTCString()) | ||
let lastModified = | ||
date && typeof date.toUTCString === 'function' | ||
? date | ||
: date && Date.parse(date) | ||
? new Date(date) | ||
: new Date(); | ||
this.header('Last-Modified', lastModified.toUTCString()); | ||
} | ||
return this | ||
return this; | ||
} | ||
@@ -456,10 +527,10 @@ | ||
send(body) { | ||
// Generate Etag | ||
if ( this._etag // if etag support enabled | ||
&& ['GET','HEAD'].includes(this._request.method) | ||
&& !this.hasHeader('etag') | ||
&& this._statusCode === 200 | ||
if ( | ||
this._etag && // if etag support enabled | ||
['GET', 'HEAD'].includes(this._request.method) && | ||
!this.hasHeader('etag') && | ||
this._statusCode === 200 | ||
) { | ||
this.header('etag','"'+UTILS.generateEtag(body)+'"') | ||
this.header('etag', '"' + UTILS.generateEtag(body) + '"'); | ||
} | ||
@@ -469,39 +540,82 @@ | ||
if ( | ||
this._request.headers['if-none-match'] | ||
&& this._request.headers['if-none-match'] === this.getHeader('etag') | ||
this._request.headers['if-none-match'] && | ||
this._request.headers['if-none-match'] === this.getHeader('etag') | ||
) { | ||
this.status(304) | ||
body = '' | ||
this.status(304); | ||
body = ''; | ||
} | ||
let headers = {}; | ||
let cookies = {}; | ||
if (this._request.payloadVersion === '2.0') { | ||
if (this._headers['set-cookie']) { | ||
cookies = { cookies: this._headers['set-cookie'] }; | ||
delete this._headers['set-cookie']; | ||
} | ||
} | ||
if (this._request._multiValueSupport) { | ||
headers = { multiValueHeaders: this._headers }; | ||
} else { | ||
headers = { headers: UTILS.stringifyHeaders(this._headers) }; | ||
} | ||
// Create the response | ||
this._response = Object.assign({}, | ||
this._request._multiValueSupport ? { multiValueHeaders: this._headers } | ||
: { headers: UTILS.stringifyHeaders(this._headers) }, | ||
this._response = Object.assign( | ||
{}, | ||
headers, | ||
cookies, | ||
{ | ||
statusCode: this._statusCode, | ||
body: this._request.method === 'HEAD' ? '' : UTILS.encodeBody(body,this._serializer), | ||
isBase64Encoded: this._isBase64 | ||
body: | ||
this._request.method === 'HEAD' | ||
? '' | ||
: UTILS.encodeBody(body, this._serializer), | ||
isBase64Encoded: this._isBase64, | ||
}, | ||
this._request.interface === 'alb' ? { statusDescription: `${this._statusCode} ${UTILS.statusLookup(this._statusCode)}` } : {} | ||
) | ||
this._request.interface === 'alb' | ||
? { | ||
statusDescription: `${this._statusCode} ${UTILS.statusLookup( | ||
this._statusCode | ||
)}`, | ||
} | ||
: {} | ||
); | ||
// Compress the body | ||
if (this._compression && this._response.body) { | ||
const { data, contentEncoding } = compression.compress( | ||
this._response.body, | ||
this._request.headers | ||
); | ||
if (contentEncoding) { | ||
Object.assign(this._response, { | ||
body: data.toString('base64'), | ||
isBase64Encoded: true, | ||
}); | ||
if (this._response.multiValueHeaders) { | ||
this._response.multiValueHeaders['content-encoding'] = [ | ||
contentEncoding, | ||
]; | ||
} else { | ||
this._response.headers['content-encoding'] = contentEncoding; | ||
} | ||
} | ||
} | ||
// Trigger the callback function | ||
this.app._callback(null, this._response, this) | ||
this.app._callback(null, this._response, this); | ||
} // end send | ||
// Trigger API error | ||
error(code,e,detail) { | ||
detail = typeof code !== 'number' && e !== undefined ? e : detail | ||
e = typeof code !== 'number' ? code : e | ||
code = typeof code === 'number' ? code : undefined | ||
this.app.catchErrors(e,this,code,detail) | ||
error(code, e, detail) { | ||
detail = typeof code !== 'number' && e !== undefined ? e : detail; | ||
e = typeof code !== 'number' ? code : e; | ||
code = typeof code === 'number' ? code : undefined; | ||
this.app.catchErrors(e, this, code, detail); | ||
} // end error | ||
} // end Response class | ||
// Export the response object | ||
module.exports = RESPONSE | ||
module.exports = RESPONSE; |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -10,5 +10,5 @@ /** | ||
// Require AWS SDK | ||
const AWS = require('aws-sdk') // AWS SDK | ||
const AWS = require('aws-sdk'); // AWS SDK | ||
// Export | ||
module.exports = new AWS.S3() | ||
module.exports = new AWS.S3(); |
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -81,3 +81,3 @@ /** | ||
510: 'Not Extended', | ||
511: 'Network Authentication Required' | ||
} | ||
511: 'Network Authentication Required', | ||
}; |
232
lib/utils.js
@@ -1,2 +0,2 @@ | ||
'use strict' | ||
'use strict'; | ||
@@ -9,5 +9,5 @@ /** | ||
const QS = require('querystring') // Require the querystring library | ||
const crypto = require('crypto') // Require Node.js crypto library | ||
const { FileError } = require('./errors') // Require custom errors | ||
const QS = require('querystring'); // Require the querystring library | ||
const crypto = require('crypto'); // Require Node.js crypto library | ||
const { FileError } = require('./errors'); // Require custom errors | ||
@@ -19,91 +19,105 @@ const entityMap = { | ||
'"': '"', | ||
'\'': ''' | ||
} | ||
"'": ''', | ||
}; | ||
exports.escapeHtml = html => html.replace(/[&<>"']/g, s => entityMap[s]) | ||
exports.escapeHtml = (html) => html.replace(/[&<>"']/g, (s) => entityMap[s]); | ||
// From encodeurl by Douglas Christopher Wilson | ||
let ENCODE_CHARS_REGEXP = /(?:[^\x21\x25\x26-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7A\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g | ||
let UNMATCHED_SURROGATE_PAIR_REGEXP = /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g | ||
let UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2' | ||
let ENCODE_CHARS_REGEXP = | ||
/(?:[^\x21\x25\x26-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7A\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g; | ||
let UNMATCHED_SURROGATE_PAIR_REGEXP = | ||
/(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g; | ||
let UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2'; | ||
exports.encodeUrl = url => String(url) | ||
.replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE) | ||
.replace(ENCODE_CHARS_REGEXP, encodeURI) | ||
exports.encodeUrl = (url) => | ||
String(url) | ||
.replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE) | ||
.replace(ENCODE_CHARS_REGEXP, encodeURI); | ||
const encodeBody = (body, serializer) => { | ||
const encode = typeof serializer === 'function' ? serializer : JSON.stringify; | ||
return typeof body === 'object' | ||
? encode(body) | ||
: body && typeof body !== 'string' | ||
? body.toString() | ||
: body | ||
? body | ||
: ''; | ||
}; | ||
exports.encodeBody = encodeBody; | ||
const encodeBody = (body,serializer) => { | ||
const encode = typeof serializer === 'function' ? serializer : JSON.stringify | ||
return typeof body === 'object' ? encode(body) : (body && typeof body !== 'string' ? body.toString() : (body ? body : '')) | ||
} | ||
exports.parsePath = (path) => { | ||
return path | ||
? path | ||
.trim() | ||
.split('?')[0] | ||
.replace(/^\/(.*?)(\/)*$/, '$1') | ||
.split('/') | ||
: []; | ||
}; | ||
exports.encodeBody = encodeBody | ||
exports.parsePath = path => { | ||
return path ? path.trim().split('?')[0].replace(/^\/(.*?)(\/)*$/,'$1').split('/') : [] | ||
} | ||
exports.parseBody = body => { | ||
exports.parseBody = (body) => { | ||
try { | ||
return JSON.parse(body) | ||
} catch(e) { | ||
return body | ||
return JSON.parse(body); | ||
} catch (e) { | ||
return body; | ||
} | ||
} | ||
}; | ||
// Parses auth values into known formats | ||
const parseAuthValue = (type,value) => { | ||
const parseAuthValue = (type, value) => { | ||
switch (type) { | ||
case 'Basic': { | ||
let creds = Buffer.from(value, 'base64').toString().split(':') | ||
return { type, value, username: creds[0], password: creds[1] ? creds[1] : null } | ||
let creds = Buffer.from(value, 'base64').toString().split(':'); | ||
return { | ||
type, | ||
value, | ||
username: creds[0], | ||
password: creds[1] ? creds[1] : null, | ||
}; | ||
} | ||
case 'OAuth': { | ||
let params = QS.parse(value.replace(/",\s*/g,'&').replace(/"/g,'').trim()) | ||
return Object.assign({ type, value }, params) | ||
let params = QS.parse( | ||
value.replace(/",\s*/g, '&').replace(/"/g, '').trim() | ||
); | ||
return Object.assign({ type, value }, params); | ||
} | ||
default: { | ||
return { type, value } | ||
return { type, value }; | ||
} | ||
} | ||
} | ||
}; | ||
exports.parseAuth = authStr => { | ||
let auth = authStr && typeof authStr === 'string' ? authStr.split(' ') : [] | ||
return auth.length > 1 && ['Bearer','Basic','Digest','OAuth'].includes(auth[0]) ? | ||
parseAuthValue(auth[0], auth.slice(1).join(' ').trim()) : | ||
{ type: 'none', value: null } | ||
} | ||
exports.parseAuth = (authStr) => { | ||
let auth = authStr && typeof authStr === 'string' ? authStr.split(' ') : []; | ||
return auth.length > 1 && | ||
['Bearer', 'Basic', 'Digest', 'OAuth'].includes(auth[0]) | ||
? parseAuthValue(auth[0], auth.slice(1).join(' ').trim()) | ||
: { type: 'none', value: null }; | ||
}; | ||
const mimeMap = require('./mimemap.js'); // MIME Map | ||
exports.mimeLookup = (input, custom = {}) => { | ||
let type = input.trim().replace(/^\./, ''); | ||
const mimeMap = require('./mimemap.js') // MIME Map | ||
exports.mimeLookup = (input,custom={}) => { | ||
let type = input.trim().replace(/^\./,'') | ||
// If it contains a slash, return unmodified | ||
if (/.*\/.*/.test(type)) { | ||
return input.trim() | ||
return input.trim(); | ||
} else { | ||
// Lookup mime type | ||
let mime = Object.assign(mimeMap,custom)[type] | ||
return mime ? mime : false | ||
let mime = Object.assign(mimeMap, custom)[type]; | ||
return mime ? mime : false; | ||
} | ||
} | ||
}; | ||
const statusCodes = require('./statusCodes.js') // MIME Map | ||
const statusCodes = require('./statusCodes.js'); // MIME Map | ||
exports.statusLookup = status => { | ||
return status in statusCodes ? statusCodes[status] : 'Unknown' | ||
} | ||
exports.statusLookup = (status) => { | ||
return status in statusCodes ? statusCodes[status] : 'Unknown'; | ||
}; | ||
// Parses routes into readable array | ||
const extractRoutes = (routes,table=[]) => { | ||
const extractRoutes = (routes, table = []) => { | ||
// Loop through all routes | ||
@@ -113,59 +127,79 @@ for (let route in routes['ROUTES']) { | ||
for (let method in routes['ROUTES'][route]['METHODS']) { | ||
table.push([method,routes['ROUTES'][route]['METHODS'][method].path]) | ||
table.push([ | ||
method, | ||
routes['ROUTES'][route]['METHODS'][method].path, | ||
routes['ROUTES'][route]['METHODS'][method].stack.map((x) => | ||
x.name.trim() !== '' ? x.name : 'unnamed' | ||
), | ||
]); | ||
} | ||
extractRoutes(routes['ROUTES'][route],table) | ||
extractRoutes(routes['ROUTES'][route], table); | ||
} | ||
return table | ||
} | ||
return table; | ||
}; | ||
exports.extractRoutes = extractRoutes | ||
exports.extractRoutes = extractRoutes; | ||
// Generate an Etag for the supplied value | ||
exports.generateEtag = data => | ||
crypto.createHash('sha256').update(encodeBody(data)).digest('hex').substr(0,32) | ||
exports.generateEtag = (data) => | ||
crypto | ||
.createHash('sha256') | ||
.update(encodeBody(data)) | ||
.digest('hex') | ||
.substr(0, 32); | ||
// Check if valid S3 path | ||
exports.isS3 = path => /^s3:\/\/.+\/.+/i.test(path) | ||
exports.isS3 = (path) => /^s3:\/\/.+\/.+/i.test(path); | ||
// Parse S3 path | ||
exports.parseS3 = path => { | ||
if (!this.isS3(path)) throw new FileError('Invalid S3 path',{path}) | ||
let s3object = path.replace(/^s3:\/\//i,'').split('/') | ||
return { Bucket: s3object.shift(), Key: s3object.join('/') } | ||
} | ||
exports.parseS3 = (path) => { | ||
if (!this.isS3(path)) throw new FileError('Invalid S3 path', { path }); | ||
let s3object = path.replace(/^s3:\/\//i, '').split('/'); | ||
return { Bucket: s3object.shift(), Key: s3object.join('/') }; | ||
}; | ||
// Deep Merge | ||
exports.deepMerge = (a,b) => { | ||
Object.keys(b).forEach(key => (key in a) ? | ||
this.deepMerge(a[key],b[key]) : Object.assign(a,b) ) | ||
return a | ||
} | ||
exports.deepMerge = (a, b) => { | ||
Object.keys(b).forEach((key) => | ||
key in a ? this.deepMerge(a[key], b[key]) : Object.assign(a, b) | ||
); | ||
return a; | ||
}; | ||
exports.mergeObjects = (obj1,obj2) => | ||
Object.keys(Object.assign({},obj1,obj2)).reduce((acc,key) => { | ||
if (obj1[key] && obj2[key] && obj1[key].every(e => obj2[key].includes(e))) { | ||
return Object.assign(acc,{ [key]: obj1[key] }) | ||
// Concatenate arrays when merging two objects | ||
exports.mergeObjects = (obj1, obj2) => | ||
Object.keys(Object.assign({}, obj1, obj2)).reduce((acc, key) => { | ||
if ( | ||
obj1[key] && | ||
obj2[key] && | ||
obj1[key].every((e) => obj2[key].includes(e)) | ||
) { | ||
return Object.assign(acc, { [key]: obj1[key] }); | ||
} else { | ||
return Object.assign(acc,{ | ||
[key]: obj1[key] ? obj2[key] ? obj1[key].concat(obj2[key]) : obj1[key] : obj2[key] | ||
}) | ||
return Object.assign(acc, { | ||
[key]: obj1[key] | ||
? obj2[key] | ||
? obj1[key].concat(obj2[key]) | ||
: obj1[key] | ||
: obj2[key], | ||
}); | ||
} | ||
},{}) | ||
}, {}); | ||
// Concats values from an array to ',' separated string | ||
exports.fromArray = val => | ||
val && val instanceof Array ? val.toString() : undefined | ||
exports.fromArray = (val) => | ||
val && val instanceof Array ? val.toString() : undefined; | ||
// Stringify multi-value headers | ||
exports.stringifyHeaders = headers => | ||
Object.keys(headers) | ||
.reduce((acc,key) => | ||
Object.assign(acc,{ | ||
exports.stringifyHeaders = (headers) => | ||
Object.keys(headers).reduce( | ||
(acc, key) => | ||
Object.assign(acc, { | ||
// set-cookie cannot be concatenated with a comma | ||
[key]: key === 'set-cookie' ? headers[key].slice(-1)[0] : headers[key].toString() | ||
}) | ||
,{}) | ||
[key]: | ||
key === 'set-cookie' | ||
? headers[key].slice(-1)[0] | ||
: headers[key].toString(), | ||
}), | ||
{} | ||
); |
{ | ||
"name": "lambda-api", | ||
"version": "0.10.7", | ||
"version": "0.11.0", | ||
"description": "Lightweight web framework for your serverless applications", | ||
@@ -8,6 +8,9 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "mocha --check-leaks --recursive", | ||
"test-cov": "nyc --reporter=html mocha --check-leaks --recursive", | ||
"test-ci": "eslint . && nyc npm test && nyc report --reporter=text-lcov | ./node_modules/coveralls/bin/coveralls.js", | ||
"lint": "eslint ." | ||
"test": "jest unit", | ||
"prettier": "prettier --check .", | ||
"test-cov": "jest unit --coverage", | ||
"test-ci": "eslint . && prettier --check . && jest unit --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", | ||
"lint": "eslint .", | ||
"prepublishOnly": "npm test && npm run lint", | ||
"changelog": "git log $(git describe --tags --abbrev=0)..HEAD --oneline" | ||
}, | ||
@@ -40,23 +43,14 @@ "repository": { | ||
"bluebird": "^3.7.2", | ||
"chai": "^4.2.0", | ||
"coveralls": "^3.1.0", | ||
"eslint": "^4.19.1", | ||
"eslint-config-airbnb-base": "^12.1.0", | ||
"eslint-plugin-import": "^2.20.2", | ||
"istanbul": "^0.4.5", | ||
"mocha": "^4.1.0", | ||
"mocha-lcov-reporter": "^1.3.0", | ||
"nyc": "^14.1.1", | ||
"eslint": "^7.22.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"jest": "^26.6.3", | ||
"prettier": "^2.3.2", | ||
"sinon": "^4.5.0" | ||
}, | ||
"files": [ | ||
"LICENSE", | ||
"README.md", | ||
"index.js", | ||
"index.d.ts", | ||
"lib/" | ||
], | ||
"engines": { | ||
"node": ">= 8.10.0" | ||
} | ||
] | ||
} |
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
160784
10
15
2374
1501
1