claudia-api-builder
Advanced tools
Comparing version 1.6.0 to 2.0.0
{ | ||
"name": "claudia-api-builder", | ||
"version": "1.6.0", | ||
"version": "2.0.0", | ||
"description": "Simplify AWS ApiGateway handling", | ||
@@ -32,3 +32,4 @@ "license": "MIT", | ||
"pretest": "jshint src spec && jscs src spec", | ||
"test": "node spec/support/jasmine-runner.js" | ||
"test": "node spec/support/jasmine-runner.js", | ||
"debug": "node debug spec/support/jasmine-runner.js" | ||
}, | ||
@@ -38,4 +39,4 @@ "author": "Gojko Adzic <gojko@gojko.com> http://gojko.net", | ||
"bluebird": "^3.3.0", | ||
"jasmine": "^2.4.1", | ||
"jasmine-spec-reporter": "^2.4.0", | ||
"jasmine": "^2.5.2", | ||
"jasmine-spec-reporter": "^2.7.0", | ||
"jscs": "^2.9.0", | ||
@@ -42,0 +43,0 @@ "jshint": "^2.9.1" |
#Claudia API Builder | ||
[![npm](https://img.shields.io/npm/v/claudia-api-builder.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/claudia-api-builder) | ||
[![npm](https://img.shields.io/npm/dt/claudia-api-builder.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/claudia-api-builder) | ||
[![npm](https://img.shields.io/npm/l/claudia-api-builder.svg?maxAge=2592000?style=plastic)](https://github.com/claudiajs/claudia-api-builder/blob/master/LICENSE) | ||
[![Build Status](https://travis-ci.org/claudiajs/claudia-api-builder.svg?branch=master)](https://travis-ci.org/claudiajs/claudia-api-builder) | ||
[![Join the chat at https://gitter.im/claudiajs/claudia](https://badges.gitter.im/claudiajs/claudia.svg)](https://gitter.im/claudiajs/claudia?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
Claudia API Builder makes it possible to use AWS API Gateway as if it were a lightweight JavaScript web server, so it helps developers get started easily and reduces the learning curve required to launch web APIs in AWS. [Check out this video to see how to create and deploy an API in under 5 minutes](https://vimeo.com/156232471). | ||
@@ -4,0 +10,0 @@ |
# Release history | ||
## 2.0.0, 27. September 2016 | ||
- support for API Gateway [Lambda Proxy Integrations](docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html) | ||
- support for routing via .ANY method | ||
- support for selecting request type (either CLAUDIA_API_BUILDER or AWS_PROXY) | ||
- support for dynamic response codes | ||
- completed CORS support (all HTTP method request handlers can now also limit CORS allowed origins, instead of just the OPTIONS one) | ||
- support for asynchronous CORS origin filters | ||
- stopping support for Node 0.10 | ||
- (will only work with claudia 2.x) | ||
## 1.6.0, 26 August 2016 | ||
@@ -4,0 +15,0 @@ |
@@ -1,5 +0,21 @@ | ||
/*global module, require */ | ||
module.exports = function ApiBuilder(components) { | ||
/*global module, require, Promise, console */ | ||
var convertApiGWProxyRequest = require('./convert-api-gw-proxy-request'), | ||
lowercaseKeys = require('./lowercase-keys'); | ||
module.exports = function ApiBuilder(options) { | ||
'use strict'; | ||
var self = this, | ||
getRequestFormat = function (newFormat) { | ||
var supportedFormats = ['AWS_PROXY', 'CLAUDIA_API_BUILDER']; | ||
if (!newFormat) { | ||
return 'CLAUDIA_API_BUILDER'; | ||
} else { | ||
if (supportedFormats.indexOf(newFormat) >= 0) { | ||
return newFormat; | ||
} else { | ||
throw 'Unsupported request format ' + newFormat; | ||
} | ||
} | ||
}, | ||
requestFormat = getRequestFormat(options && options.requestFormat), | ||
logger = (options && options.logger) || console.log, | ||
methodConfigurations = {}, | ||
@@ -12,88 +28,218 @@ routes = {}, | ||
authorizers, | ||
v2DeprecationWarning = function (what) { | ||
logger(what + ' are deprecated, and be removed in claudia api builder v3. Check https://claudiajs.com/tutorials/migrating_to_2.html'); | ||
}, | ||
supportedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'], | ||
interceptCallback, | ||
prompter = (components && components.prompter) || require('./ask'), | ||
prompter = (options && options.prompter) || require('./ask'), | ||
isApiResponse = function (obj) { | ||
return obj && (typeof obj === 'object') && (Object.getPrototypeOf(obj) === self.ApiResponse.prototype); | ||
}, | ||
packResult = function (handlerResult, route, method) { | ||
var path = route.replace(/^\//, ''), | ||
customHeaders = methodConfigurations[path] && methodConfigurations[path][method] && methodConfigurations[path][method].success && methodConfigurations[path][method].success.headers; | ||
mergeObjects = function (from, to) { | ||
Object.keys(from).forEach(function (key) { | ||
to[key] = from[key]; | ||
}); | ||
return to; | ||
}, | ||
isRedirect = function (code) { | ||
return /3[0-9][0-9]/.test(code); | ||
}, | ||
getContentType = function (configuration, result) { | ||
var staticHeader = (configuration && configuration.headers && lowercaseKeys(configuration.headers)['content-type']), | ||
dynamicHeader = (result && isApiResponse(result) && result.headers && lowercaseKeys(result.headers)['content-type']), | ||
staticConfig = configuration && configuration.contentType; | ||
if (isApiResponse(handlerResult)) { | ||
if (!customHeaders) { | ||
throw 'cannot use ApiResponse without enumerating headers in ' + method + ' ' + route; | ||
return dynamicHeader || staticHeader || staticConfig || 'application/json'; | ||
}, | ||
getStatusCode = function (configuration, result, resultType) { | ||
var defaultCode = { | ||
'success': 200, | ||
'error': 500 | ||
}, | ||
staticCode = (configuration && configuration.code) || (typeof configuration === 'number' && configuration), | ||
dynamicCode = (result && isApiResponse(result) && result.code); | ||
return dynamicCode || staticCode || defaultCode[resultType]; | ||
}, | ||
getRedirectLocation = function (configuration, result) { | ||
var dynamicHeader = result && isApiResponse(result) && result.headers && lowercaseKeys(result.headers).location, | ||
dynamicBody = isApiResponse(result) ? result.response : result, | ||
staticHeader = configuration && configuration.headers && lowercaseKeys(configuration.headers).location; | ||
return dynamicHeader || dynamicBody || staticHeader; | ||
}, | ||
getCanonicalContentType = function (contentType) { | ||
return (contentType && contentType.split(';')[0]) || 'application/json'; | ||
}, | ||
getSuccessBody = function (contentType, handlerResult) { | ||
var contents = isApiResponse(handlerResult) ? handlerResult.response : handlerResult; | ||
if (getCanonicalContentType(contentType) === 'application/json') { | ||
if (contents === '' || contents === undefined) { | ||
return '{}'; | ||
} else { | ||
return JSON.stringify(contents); | ||
} | ||
if (!Array.isArray(customHeaders)) { | ||
throw 'cannot use ApiResponse with default header values in ' + method + ' ' + route; | ||
} else { | ||
if (!contents) { | ||
return ''; | ||
} else if (typeof contents === 'object') { | ||
return JSON.stringify(contents); | ||
} else { | ||
return String(contents); | ||
} | ||
Object.keys(handlerResult.headers).forEach(function (header) { | ||
if (customHeaders.indexOf(header) < 0) { | ||
throw 'unexpected header ' + header + ' in ' + method + ' ' + route; | ||
} | ||
}); | ||
return { response: handlerResult.response, headers: handlerResult.headers }; | ||
} | ||
if (customHeaders && Array.isArray(customHeaders)) { | ||
return { response: handlerResult, headers: {} }; | ||
}, | ||
isError = function (object) { | ||
return object && (object.message !== undefined) && object.stack; | ||
}, | ||
logError = function (err) { | ||
var logInfo = err; | ||
if (isApiResponse(err)) { | ||
logInfo = JSON.stringify(err); | ||
} else if (isError(err)) { | ||
logInfo = err.stack; | ||
} | ||
return handlerResult; | ||
logger(logInfo); | ||
}, | ||
isThenable = function (param) { | ||
return param && param.then && (typeof param.then === 'function'); | ||
getErrorBody = function (contentType, handlerResult) { | ||
var contents = isApiResponse(handlerResult) ? handlerResult.response : handlerResult; | ||
if (isError(contents)) { | ||
contents = contents.message; | ||
} | ||
if (getCanonicalContentType(contentType) === 'application/json') { | ||
return JSON.stringify({errorMessage: contents || '' }); | ||
} else { | ||
return contents || ''; | ||
} | ||
}, | ||
routeEvent = function (event, context /*, callback*/) { | ||
var handler, result, path; | ||
context.callbackWaitsForEmptyEventLoop = false; | ||
if (event && event.context && event.context.path && event.context.method) { | ||
path = event.context.path; | ||
if (event.context.method === 'OPTIONS' && customCorsHandler) { | ||
return context.done(null, customCorsHandler(event)); | ||
getBody = function (contentType, handlerResult, resultType) { | ||
return resultType === 'success' ? getSuccessBody(contentType, handlerResult) : getErrorBody(contentType, handlerResult); | ||
}, | ||
packResult = function (handlerResult, routingInfo, corsHeaders, resultType) { | ||
var path = routingInfo.path.replace(/^\//, ''), | ||
method = routingInfo.method, | ||
configuration = methodConfigurations[path] && methodConfigurations[path][method] && methodConfigurations[path][method][resultType], | ||
customHeaders = configuration && configuration.headers, | ||
contentType = getContentType(configuration, handlerResult), | ||
statusCode = getStatusCode(configuration, handlerResult, resultType), | ||
result = { | ||
statusCode: statusCode, | ||
headers: { 'Content-Type': contentType }, | ||
body: getBody(contentType, handlerResult, resultType) | ||
}; | ||
mergeObjects(corsHeaders, result.headers); | ||
if (customHeaders) { | ||
if (Array.isArray(customHeaders)) { | ||
v2DeprecationWarning('enumerated headers'); | ||
} else { | ||
mergeObjects(customHeaders, result.headers); | ||
} | ||
handler = routes[path] && routes[path][event.context.method]; | ||
if (handler) { | ||
try { | ||
event.lambdaContext = context; | ||
result = handler(event); | ||
if (isThenable(result)) { | ||
return result.then(function (promiseResult) { | ||
context.done(null, packResult(promiseResult, path, event.context.method)); | ||
}, function (promiseError) { | ||
context.done(promiseError); | ||
}); | ||
} else { | ||
context.done(null, packResult(result, path, event.context.method)); | ||
} | ||
} catch (e) { | ||
context.done(e); | ||
} | ||
} | ||
if (isApiResponse(handlerResult)) { | ||
mergeObjects(handlerResult.headers, result.headers); | ||
} | ||
if (isRedirect(statusCode)) { | ||
result.headers.Location = getRedirectLocation(configuration, handlerResult); | ||
} | ||
return result; | ||
}, | ||
getCorsHeaders = function (request, methods) { | ||
if (methods.indexOf('ANY') >= 0) { | ||
methods = supportedMethods; | ||
} | ||
return Promise.resolve().then(function () { | ||
if (customCorsHandler === false) { | ||
return ''; | ||
} else if (customCorsHandler) { | ||
return customCorsHandler(request); | ||
} else { | ||
context.done('no handler for ' + event.context.method + ' ' + event.context.path); | ||
return '*'; | ||
} | ||
} else { | ||
if (unsupportedEventCallback) { | ||
unsupportedEventCallback.apply(this, arguments); | ||
}).then(function (corsOrigin) { | ||
return { | ||
'Access-Control-Allow-Origin': corsOrigin, | ||
'Access-Control-Allow-Headers': corsOrigin && (customCorsHeaders || 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'), | ||
'Access-Control-Allow-Methods': corsOrigin && methods.sort().join(',') + ',OPTIONS' | ||
}; | ||
}); | ||
}, | ||
routeEvent = function (routingInfo, event, context) { | ||
var handler; | ||
if (!routingInfo) { | ||
throw 'routingInfo not set'; | ||
} | ||
handler = routes[routingInfo.path] && ( | ||
routes[routingInfo.path][routingInfo.method] || | ||
routes[routingInfo.path].ANY | ||
); | ||
return getCorsHeaders(event, Object.keys(routes[routingInfo.path] || {})).then(function (corsHeaders) { | ||
if (routingInfo.method === 'OPTIONS') { | ||
return { | ||
statusCode: 200, | ||
body: '', | ||
headers: corsHeaders | ||
}; | ||
} else if (handler) { | ||
return Promise.resolve().then(function () { | ||
return handler(event, context); | ||
}).then(function (result) { | ||
return packResult(result, routingInfo, corsHeaders, 'success'); | ||
}).catch(function (error) { | ||
logError(error); | ||
return packResult(error, routingInfo, corsHeaders, 'error'); | ||
}); | ||
} else { | ||
context.done('event must contain context.path and context.method'); | ||
return Promise.reject('no handler for ' + routingInfo.method + ' ' + routingInfo.path); | ||
} | ||
}); | ||
}, | ||
getRequestRoutingInfo = function (request) { | ||
if (requestFormat === 'AWS_PROXY') { | ||
if (!request.requestContext) { | ||
return {}; | ||
} | ||
return { | ||
path: request.requestContext.resourcePath, | ||
method: request.requestContext.httpMethod | ||
}; | ||
} else { | ||
return request.context || {}; | ||
} | ||
}; | ||
['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'].forEach(function (method) { | ||
self[method.toLowerCase()] = function (route, handler, options) { | ||
var pathPart = route.replace(/^\//, ''), | ||
canonicalRoute = route; | ||
if (!/^\//.test(canonicalRoute)) { | ||
canonicalRoute = '/' + route; | ||
}, | ||
getRequest = function (event, context) { | ||
if (requestFormat === 'AWS_PROXY' || requestFormat === 'DEPRECATED') { | ||
return event; | ||
} else { | ||
return convertApiGWProxyRequest(event, context); | ||
} | ||
if (!methodConfigurations[pathPart]) { | ||
methodConfigurations[pathPart] = {} ; | ||
}, | ||
executeInterceptor = function (request, context) { | ||
if (!interceptCallback) { | ||
return Promise.resolve(request); | ||
} else { | ||
return Promise.resolve().then(function () { | ||
return interceptCallback(request, context); | ||
}); | ||
} | ||
methodConfigurations[pathPart][method] = (options || {}); | ||
if (!routes[canonicalRoute]) { | ||
routes[canonicalRoute] = {}; | ||
} | ||
routes[canonicalRoute][method] = handler; | ||
}, | ||
setUpHandler = function (method) { | ||
self[method.toLowerCase()] = function (route, handler, options) { | ||
var pathPart = route.replace(/^\//, ''), | ||
canonicalRoute = route; | ||
if (!/^\//.test(canonicalRoute)) { | ||
canonicalRoute = '/' + route; | ||
} | ||
if (!methodConfigurations[pathPart]) { | ||
methodConfigurations[pathPart] = {} ; | ||
} | ||
methodConfigurations[pathPart][method] = (options || {}); | ||
if (!routes[canonicalRoute]) { | ||
routes[canonicalRoute] = {}; | ||
} | ||
routes[canonicalRoute][method] = handler; | ||
}; | ||
}; | ||
}); | ||
['ANY'].concat(supportedMethods).forEach(setUpHandler); | ||
self.apiConfig = function () { | ||
var result = {version: 2, routes: methodConfigurations}; | ||
var result = {version: 3, routes: methodConfigurations}; | ||
if (customCorsHandler !== undefined) { | ||
@@ -130,7 +276,9 @@ result.corsHandlers = !!customCorsHandler; | ||
}; | ||
self.ApiResponse = function (responseBody, responseHeaders) { | ||
self.ApiResponse = function (responseBody, responseHeaders, code) { | ||
this.response = responseBody; | ||
this.headers = responseHeaders; | ||
this.code = code; | ||
}; | ||
self.unsupportedEvent = function (callback) { | ||
v2DeprecationWarning('.unsupportedEvent handlers'); | ||
unsupportedEventCallback = callback; | ||
@@ -141,28 +289,35 @@ }; | ||
}; | ||
self.router = function (event, context, callback) { | ||
var result, | ||
handleResult = function (r) { | ||
if (!r) { | ||
return context.done(null, null); | ||
} | ||
return routeEvent(r, context, callback); | ||
}, | ||
self.proxyRouter = function (event, context, callback) { | ||
var request = getRequest(event, context), | ||
routingInfo, | ||
handleError = function (e) { | ||
context.done(e); | ||
}; | ||
if (!interceptCallback) { | ||
return routeEvent(event, context, callback); | ||
} | ||
try { | ||
result = interceptCallback(event); | ||
if (isThenable(result)) { | ||
return result.then(handleResult, handleError); | ||
context.callbackWaitsForEmptyEventLoop = false; | ||
return executeInterceptor(request, context).then(function (modifiedRequest) { | ||
if (!modifiedRequest) { | ||
return context.done(null, null); | ||
} else { | ||
handleResult(result); | ||
routingInfo = getRequestRoutingInfo(modifiedRequest); | ||
if (routingInfo && routingInfo.path && routingInfo.method) { | ||
return routeEvent(routingInfo, modifiedRequest, context, callback).then(function (result) { | ||
context.done(null, result); | ||
}); | ||
} else { | ||
if (unsupportedEventCallback) { | ||
unsupportedEventCallback(event, context, callback); | ||
} else { | ||
return Promise.reject('event does not contain routing information'); | ||
} | ||
} | ||
} | ||
} catch (e) { | ||
handleError(e); | ||
} | ||
}).catch(handleError); | ||
}; | ||
self.router = function (event, context, callback) { | ||
requestFormat = 'DEPRECATED'; | ||
event.lambdaContext = context; | ||
v2DeprecationWarning('.router methods'); | ||
return self.proxyRouter(event, context, callback); | ||
}; | ||
self.addPostDeployStep = function (name, stepFunction) { | ||
@@ -169,0 +324,0 @@ if (typeof name !== 'string') { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
22520
10
498
51
1