mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
Merge pull request #2666 from serverless/refactor-api-gateway-resources-and-methods
Refactor api gateway resources and methods
This commit is contained in:
commit
2854b9b91e
@ -6,10 +6,14 @@ const validate = require('./lib/validate');
|
||||
const compileRestApi = require('./lib/restApi');
|
||||
const compileApiKeys = require('./lib/apiKeys');
|
||||
const compileResources = require('./lib/resources');
|
||||
const compileMethods = require('./lib/methods');
|
||||
const compileCors = require('./lib/cors');
|
||||
const compileMethods = require('./lib/method/index');
|
||||
const compileAuthorizers = require('./lib/authorizers');
|
||||
const compileDeployment = require('./lib/deployment');
|
||||
const compilePermissions = require('./lib/permissions');
|
||||
const getMethodAuthorization = require('./lib/method/authorization');
|
||||
const getMethodIntegration = require('./lib/method/integration');
|
||||
const getMethodResponses = require('./lib/method/responses');
|
||||
|
||||
class AwsCompileApigEvents {
|
||||
constructor(serverless, options) {
|
||||
@ -23,10 +27,14 @@ class AwsCompileApigEvents {
|
||||
compileRestApi,
|
||||
compileApiKeys,
|
||||
compileResources,
|
||||
compileCors,
|
||||
compileMethods,
|
||||
compileAuthorizers,
|
||||
compileDeployment,
|
||||
compilePermissions
|
||||
compilePermissions,
|
||||
getMethodAuthorization,
|
||||
getMethodIntegration,
|
||||
getMethodResponses
|
||||
);
|
||||
|
||||
this.hooks = {
|
||||
@ -40,6 +48,7 @@ class AwsCompileApigEvents {
|
||||
return BbPromise.bind(this)
|
||||
.then(this.compileRestApi)
|
||||
.then(this.compileResources)
|
||||
.then(this.compileCors)
|
||||
.then(this.compileMethods)
|
||||
.then(this.compileAuthorizers)
|
||||
.then(this.compileDeployment)
|
||||
|
||||
@ -31,8 +31,7 @@ module.exports = {
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedAuthorizerName = authorizer.name[0].toUpperCase()
|
||||
+ authorizer.name.substr(1);
|
||||
const normalizedAuthorizerName = _.capitalize(authorizer.name);
|
||||
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
|
||||
[`${normalizedAuthorizerName}ApiGatewayAuthorizer`]: {
|
||||
|
||||
75
lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js
Normal file
75
lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js
Normal file
@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const BbPromise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
|
||||
compileCors() {
|
||||
_.forEach(this.validated.corsPreflight, (config, path) => {
|
||||
const resourceName = this.getResourceName(path);
|
||||
const resourceRef = this.getResourceId(path);
|
||||
|
||||
const preflightHeaders = {
|
||||
'Access-Control-Allow-Origin': `'${config.origins.join(',')}'`,
|
||||
'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`,
|
||||
'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`,
|
||||
};
|
||||
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
|
||||
[`ApiGatewayMethod${resourceName}Options`]: {
|
||||
Type: 'AWS::ApiGateway::Method',
|
||||
Properties: {
|
||||
AuthorizationType: 'NONE',
|
||||
HttpMethod: 'OPTIONS',
|
||||
MethodResponses: this.generateCorsMethodResponses(preflightHeaders),
|
||||
RequestParameters: {},
|
||||
Integration: {
|
||||
Type: 'MOCK',
|
||||
RequestTemplates: {
|
||||
'application/json': '{statusCode:200}',
|
||||
},
|
||||
IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders),
|
||||
},
|
||||
ResourceId: resourceRef,
|
||||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return BbPromise.resolve();
|
||||
},
|
||||
|
||||
generateCorsMethodResponses(preflightHeaders) {
|
||||
const methodResponseHeaders = {};
|
||||
|
||||
_.forEach(preflightHeaders, (value, header) => {
|
||||
methodResponseHeaders[`method.response.header.${header}`] = true;
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
StatusCode: '200',
|
||||
ResponseParameters: methodResponseHeaders,
|
||||
ResponseModels: {},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
generateCorsIntegrationResponses(preflightHeaders) {
|
||||
const responseParameters = _.mapKeys(preflightHeaders,
|
||||
(value, header) => `method.response.header.${header}`);
|
||||
|
||||
return [
|
||||
{
|
||||
StatusCode: '200',
|
||||
ResponseParameters: responseParameters,
|
||||
ResponseTemplates: {
|
||||
'application/json': '',
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
};
|
||||
@ -14,7 +14,7 @@ module.exports = {
|
||||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
|
||||
StageName: this.options.stage,
|
||||
},
|
||||
DependsOn: this.methodDependencies,
|
||||
DependsOn: this.apiGatewayMethodLogicalIds,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
getMethodAuthorization(http) {
|
||||
if (http.authorizer) {
|
||||
const normalizedAuthorizerName = _.capitalize(http.authorizer.name);
|
||||
const authorizerLogicalId = `${normalizedAuthorizerName}ApiGatewayAuthorizer`;
|
||||
|
||||
return {
|
||||
Properties: {
|
||||
AuthorizationType: 'CUSTOM',
|
||||
AuthorizerId: { Ref: authorizerLogicalId },
|
||||
},
|
||||
DependsOn: authorizerLogicalId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
Properties: {
|
||||
AuthorizationType: 'NONE',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const BbPromise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
|
||||
compileMethods() {
|
||||
this.apiGatewayMethodLogicalIds = [];
|
||||
|
||||
this.validated.events.forEach((event) => {
|
||||
const resourceId = this.getResourceId(event.http.path);
|
||||
const resourceName = this.getResourceName(event.http.path);
|
||||
const requestParameters = (event.http.request && event.http.request.parameters) || {};
|
||||
|
||||
const template = {
|
||||
Type: 'AWS::ApiGateway::Method',
|
||||
Properties: {
|
||||
HttpMethod: event.http.method.toUpperCase(),
|
||||
RequestParameters: requestParameters,
|
||||
ResourceId: resourceId,
|
||||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
|
||||
},
|
||||
};
|
||||
|
||||
if (event.http.private) {
|
||||
template.Properties.ApiKeyRequired = true;
|
||||
}
|
||||
|
||||
_.merge(template,
|
||||
this.getMethodAuthorization(event.http),
|
||||
this.getMethodIntegration(event.http, event.functionName),
|
||||
this.getMethodResponses(event.http)
|
||||
);
|
||||
|
||||
const methodName = _.capitalize(event.http.method);
|
||||
const methodLogicalId = `ApiGatewayMethod${resourceName}${methodName}`;
|
||||
|
||||
this.apiGatewayMethodLogicalIds.push(methodLogicalId);
|
||||
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
|
||||
[methodLogicalId]: template,
|
||||
});
|
||||
});
|
||||
|
||||
return BbPromise.resolve();
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,193 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
getMethodIntegration(http, functionName) {
|
||||
const normalizedFunctionName = _.capitalize(functionName);
|
||||
const integration = {
|
||||
IntegrationHttpMethod: 'POST',
|
||||
Type: http.integration,
|
||||
Uri: {
|
||||
'Fn::Join': ['',
|
||||
[
|
||||
'arn:aws:apigateway:',
|
||||
{ Ref: 'AWS::Region' },
|
||||
':lambda:path/2015-03-31/functions/',
|
||||
{ 'Fn::GetAtt': [`${normalizedFunctionName}LambdaFunction`, 'Arn'] },
|
||||
'/invocations',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (http.integration === 'AWS') {
|
||||
_.assign(integration, {
|
||||
PassthroughBehavior: http.request && http.request.passThrough,
|
||||
RequestTemplates: this.getIntegrationRequestTemplates(http),
|
||||
IntegrationResponses: this.getIntegrationResponses(http),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
Properties: {
|
||||
Integration: integration,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
getIntegrationResponses(http) {
|
||||
const integrationResponses = [];
|
||||
|
||||
if (http.response) {
|
||||
const integrationResponseHeaders = [];
|
||||
|
||||
if (http.cors) {
|
||||
_.merge(integrationResponseHeaders, {
|
||||
'Access-Control-Allow-Origin': `'${http.cors.origins.join(',')}'`,
|
||||
});
|
||||
}
|
||||
|
||||
if (http.response.headers) {
|
||||
_.merge(integrationResponseHeaders, http.response.headers);
|
||||
}
|
||||
|
||||
_.each(http.response.statusCodes, (config, statusCode) => {
|
||||
const responseParameters = _.mapKeys(integrationResponseHeaders,
|
||||
(value, header) => `method.response.header.${header}`);
|
||||
|
||||
const integrationResponse = {
|
||||
StatusCode: parseInt(statusCode, 10),
|
||||
SelectionPattern: config.pattern || '',
|
||||
ResponseParameters: responseParameters,
|
||||
ResponseTemplates: {},
|
||||
};
|
||||
|
||||
if (config.headers) {
|
||||
_.merge(integrationResponse.ResponseParameters, _.mapKeys(config.headers,
|
||||
(value, header) => `method.response.header.${header}`));
|
||||
}
|
||||
|
||||
if (http.response.template) {
|
||||
_.merge(integrationResponse.ResponseTemplates, {
|
||||
'application/json': http.response.template,
|
||||
});
|
||||
}
|
||||
|
||||
if (config.template) {
|
||||
const template = typeof config.template === 'string' ?
|
||||
{ 'application/json': config.template }
|
||||
: config.template;
|
||||
|
||||
_.merge(integrationResponse.ResponseTemplates, template);
|
||||
}
|
||||
|
||||
integrationResponses.push(integrationResponse);
|
||||
});
|
||||
}
|
||||
|
||||
return integrationResponses;
|
||||
},
|
||||
|
||||
getIntegrationRequestTemplates(http) {
|
||||
// default request templates
|
||||
const integrationRequestTemplates = {
|
||||
'application/json': this.DEFAULT_JSON_REQUEST_TEMPLATE,
|
||||
'application/x-www-form-urlencoded': this.DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE,
|
||||
};
|
||||
|
||||
// set custom request templates if provided
|
||||
if (http.request && typeof http.request.template === 'object') {
|
||||
_.assign(integrationRequestTemplates, http.request.template);
|
||||
}
|
||||
|
||||
return integrationRequestTemplates;
|
||||
},
|
||||
|
||||
DEFAULT_JSON_REQUEST_TEMPLATE: `
|
||||
#define( $loop )
|
||||
{
|
||||
#foreach($key in $map.keySet())
|
||||
"$util.escapeJavaScript($key)":
|
||||
"$util.escapeJavaScript($map.get($key))"
|
||||
#if( $foreach.hasNext ) , #end
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
{
|
||||
"body": $input.json("$"),
|
||||
"method": "$context.httpMethod",
|
||||
"principalId": "$context.authorizer.principalId",
|
||||
"stage": "$context.stage",
|
||||
|
||||
#set( $map = $input.params().header )
|
||||
"headers": $loop,
|
||||
|
||||
#set( $map = $input.params().querystring )
|
||||
"query": $loop,
|
||||
|
||||
#set( $map = $input.params().path )
|
||||
"path": $loop,
|
||||
|
||||
#set( $map = $context.identity )
|
||||
"identity": $loop,
|
||||
|
||||
#set( $map = $stageVariables )
|
||||
"stageVariables": $loop
|
||||
}
|
||||
`,
|
||||
|
||||
DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE: `
|
||||
#define( $body )
|
||||
{
|
||||
#foreach( $token in $input.path('$').split('&') )
|
||||
#set( $keyVal = $token.split('=') )
|
||||
#set( $keyValSize = $keyVal.size() )
|
||||
#if( $keyValSize >= 1 )
|
||||
#set( $key = $util.escapeJavaScript($util.urlDecode($keyVal[0])) )
|
||||
#if( $keyValSize >= 2 )
|
||||
#set( $val = $util.escapeJavaScript($util.urlDecode($keyVal[1])) )
|
||||
#else
|
||||
#set( $val = '' )
|
||||
#end
|
||||
"$key": "$val"#if($foreach.hasNext),#end
|
||||
#end
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
#define( $loop )
|
||||
{
|
||||
#foreach($key in $map.keySet())
|
||||
"$util.escapeJavaScript($key)":
|
||||
"$util.escapeJavaScript($map.get($key))"
|
||||
#if( $foreach.hasNext ) , #end
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
{
|
||||
"body": $body,
|
||||
"method": "$context.httpMethod",
|
||||
"principalId": "$context.authorizer.principalId",
|
||||
"stage": "$context.stage",
|
||||
|
||||
#set( $map = $input.params().header )
|
||||
"headers": $loop,
|
||||
|
||||
#set( $map = $input.params().querystring )
|
||||
"query": $loop,
|
||||
|
||||
#set( $map = $input.params().path )
|
||||
"path": $loop,
|
||||
|
||||
#set( $map = $context.identity )
|
||||
"identity": $loop,
|
||||
|
||||
#set( $map = $stageVariables )
|
||||
"stageVariables": $loop
|
||||
}
|
||||
`,
|
||||
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
|
||||
getMethodResponses(http) {
|
||||
const methodResponses = [];
|
||||
|
||||
if (http.integration === 'AWS') {
|
||||
if (http.response) {
|
||||
const methodResponseHeaders = [];
|
||||
|
||||
if (http.cors) {
|
||||
_.merge(methodResponseHeaders, {
|
||||
'Access-Control-Allow-Origin': `'${http.cors.origins.join(',')}'`,
|
||||
});
|
||||
}
|
||||
|
||||
if (http.response.headers) {
|
||||
_.merge(methodResponseHeaders, http.response.headers);
|
||||
}
|
||||
|
||||
_.each(http.response.statusCodes, (config, statusCode) => {
|
||||
const methodResponse = {
|
||||
ResponseParameters: {},
|
||||
ResponseModels: {},
|
||||
StatusCode: parseInt(statusCode, 10),
|
||||
};
|
||||
|
||||
_.merge(methodResponse.ResponseParameters,
|
||||
this.getMethodResponseHeaders(methodResponseHeaders));
|
||||
|
||||
if (config.headers) {
|
||||
_.merge(methodResponse.ResponseParameters,
|
||||
this.getMethodResponseHeaders(config.headers));
|
||||
}
|
||||
|
||||
methodResponses.push(methodResponse);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Properties: {
|
||||
MethodResponses: methodResponses,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
getMethodResponseHeaders(headers) {
|
||||
const methodResponseHeaders = {};
|
||||
|
||||
Object.keys(headers).forEach(header => {
|
||||
methodResponseHeaders[`method.response.header.${header}`] = true;
|
||||
});
|
||||
|
||||
return methodResponseHeaders;
|
||||
},
|
||||
|
||||
};
|
||||
@ -1,660 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const BbPromise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
|
||||
const NOT_FOUND = -1;
|
||||
|
||||
module.exports = {
|
||||
compileMethods() {
|
||||
const corsPreflight = {};
|
||||
|
||||
const defaultStatusCodes = {
|
||||
200: {
|
||||
pattern: '',
|
||||
},
|
||||
400: {
|
||||
pattern: '.*\\[400\\].*',
|
||||
},
|
||||
401: {
|
||||
pattern: '.*\\[401\\].*',
|
||||
},
|
||||
403: {
|
||||
pattern: '.*\\[403\\].*',
|
||||
},
|
||||
404: {
|
||||
pattern: '.*\\[404\\].*',
|
||||
},
|
||||
422: {
|
||||
pattern: '.*\\[422\\].*',
|
||||
},
|
||||
500: {
|
||||
pattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*',
|
||||
},
|
||||
502: {
|
||||
pattern: '.*\\[502\\].*',
|
||||
},
|
||||
504: {
|
||||
pattern: '.*\\[504\\].*',
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Private helper functions
|
||||
*/
|
||||
|
||||
const generateMethodResponseHeaders = (headers) => {
|
||||
const methodResponseHeaders = {};
|
||||
|
||||
Object.keys(headers).forEach(header => {
|
||||
methodResponseHeaders[`method.response.header.${header}`] = true;
|
||||
});
|
||||
|
||||
return methodResponseHeaders;
|
||||
};
|
||||
|
||||
const generateIntegrationResponseHeaders = (headers) => {
|
||||
const integrationResponseHeaders = {};
|
||||
|
||||
Object.keys(headers).forEach(header => {
|
||||
integrationResponseHeaders[`method.response.header.${header}`] = headers[header];
|
||||
});
|
||||
|
||||
return integrationResponseHeaders;
|
||||
};
|
||||
|
||||
const generateCorsPreflightConfig = (corsConfig, corsPreflightConfig, method) => {
|
||||
const headers = [
|
||||
'Content-Type',
|
||||
'X-Amz-Date',
|
||||
'Authorization',
|
||||
'X-Api-Key',
|
||||
'X-Amz-Security-Token',
|
||||
];
|
||||
|
||||
let newCorsPreflightConfig;
|
||||
|
||||
const cors = {
|
||||
origins: ['*'],
|
||||
methods: ['OPTIONS'],
|
||||
headers,
|
||||
};
|
||||
|
||||
if (typeof corsConfig === 'object') {
|
||||
Object.assign(cors, corsConfig);
|
||||
|
||||
cors.methods = [];
|
||||
if (cors.headers) {
|
||||
if (!Array.isArray(cors.headers)) {
|
||||
const errorMessage = [
|
||||
'CORS header values must be provided as an array.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes
|
||||
.Error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
cors.headers = headers;
|
||||
}
|
||||
|
||||
if (cors.methods.indexOf('OPTIONS') === NOT_FOUND) {
|
||||
cors.methods.push('OPTIONS');
|
||||
}
|
||||
|
||||
if (cors.methods.indexOf(method.toUpperCase()) === NOT_FOUND) {
|
||||
cors.methods.push(method.toUpperCase());
|
||||
}
|
||||
} else {
|
||||
cors.methods.push(method.toUpperCase());
|
||||
}
|
||||
|
||||
if (corsPreflightConfig) {
|
||||
cors.methods = _.union(cors.methods, corsPreflightConfig.methods);
|
||||
cors.headers = _.union(cors.headers, corsPreflightConfig.headers);
|
||||
cors.origins = _.union(cors.origins, corsPreflightConfig.origins);
|
||||
newCorsPreflightConfig = _.merge(corsPreflightConfig, cors);
|
||||
} else {
|
||||
newCorsPreflightConfig = cors;
|
||||
}
|
||||
|
||||
return newCorsPreflightConfig;
|
||||
};
|
||||
|
||||
const hasDefaultStatusCode = (statusCodes) =>
|
||||
Object.keys(statusCodes).some((statusCode) => (statusCodes[statusCode].pattern === ''));
|
||||
|
||||
const generateResponse = (responseConfig) => {
|
||||
const response = {
|
||||
methodResponses: [],
|
||||
integrationResponses: [],
|
||||
};
|
||||
|
||||
const statusCodes = {};
|
||||
Object.assign(statusCodes, responseConfig.statusCodes);
|
||||
|
||||
if (!hasDefaultStatusCode(statusCodes)) {
|
||||
_.merge(statusCodes, { 200: defaultStatusCodes['200'] });
|
||||
}
|
||||
|
||||
Object.keys(statusCodes).forEach((statusCode) => {
|
||||
const methodResponse = {
|
||||
ResponseParameters: {},
|
||||
ResponseModels: {},
|
||||
StatusCode: parseInt(statusCode, 10),
|
||||
};
|
||||
|
||||
const integrationResponse = {
|
||||
StatusCode: parseInt(statusCode, 10),
|
||||
SelectionPattern: statusCodes[statusCode].pattern || '',
|
||||
ResponseParameters: {},
|
||||
ResponseTemplates: {},
|
||||
};
|
||||
|
||||
_.merge(methodResponse.ResponseParameters,
|
||||
generateMethodResponseHeaders(responseConfig.methodResponseHeaders));
|
||||
if (statusCodes[statusCode].headers) {
|
||||
_.merge(methodResponse.ResponseParameters,
|
||||
generateMethodResponseHeaders(statusCodes[statusCode].headers));
|
||||
}
|
||||
|
||||
_.merge(integrationResponse.ResponseParameters,
|
||||
generateIntegrationResponseHeaders(responseConfig.integrationResponseHeaders));
|
||||
if (statusCodes[statusCode].headers) {
|
||||
_.merge(integrationResponse.ResponseParameters,
|
||||
generateIntegrationResponseHeaders(statusCodes[statusCode].headers));
|
||||
}
|
||||
|
||||
if (responseConfig.integrationResponseTemplate) {
|
||||
_.merge(integrationResponse.ResponseTemplates, {
|
||||
'application/json': responseConfig.integrationResponseTemplate,
|
||||
});
|
||||
}
|
||||
|
||||
if (statusCodes[statusCode].template) {
|
||||
if (typeof statusCodes[statusCode].template === 'string') {
|
||||
_.merge(integrationResponse.ResponseTemplates, {
|
||||
'application/json': statusCodes[statusCode].template,
|
||||
});
|
||||
} else {
|
||||
_.merge(integrationResponse.ResponseTemplates, statusCodes[statusCode].template);
|
||||
}
|
||||
}
|
||||
|
||||
response.methodResponses.push(methodResponse);
|
||||
response.integrationResponses.push(integrationResponse);
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const hasRequestTemplate = (event) => {
|
||||
// check if custom request configuration should be used
|
||||
if (Boolean(event.http.request) === true) {
|
||||
if (typeof event.http.request === 'object') {
|
||||
// merge custom request templates if provided
|
||||
if (Boolean(event.http.request.template) === true) {
|
||||
if (typeof event.http.request.template === 'object') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const errorMessage = [
|
||||
'Template config must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = [
|
||||
'Request config must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasRequestParameters = (event) => (event.http.request && event.http.request.parameters);
|
||||
|
||||
const hasPassThroughRequest = (event) => {
|
||||
const requestPassThroughBehaviors = [
|
||||
'NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES',
|
||||
];
|
||||
|
||||
if (event.http.request && Boolean(event.http.request.passThrough) === true) {
|
||||
if (requestPassThroughBehaviors.indexOf(event.http.request.passThrough) === -1) {
|
||||
const errorMessage = [
|
||||
'Request passThrough "',
|
||||
event.http.request.passThrough,
|
||||
'" is not one of ',
|
||||
requestPassThroughBehaviors.join(', '),
|
||||
].join('');
|
||||
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasCors = (event) => (Boolean(event.http.cors) === true);
|
||||
|
||||
const hasResponseTemplate = (event) => (event.http.response && event.http.response.template);
|
||||
|
||||
const hasResponseHeaders = (event) => {
|
||||
// check if custom response configuration should be used
|
||||
if (Boolean(event.http.response) === true) {
|
||||
if (typeof event.http.response === 'object') {
|
||||
// prepare the headers if set
|
||||
if (Boolean(event.http.response.headers) === true) {
|
||||
if (typeof event.http.response.headers === 'object') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const errorMessage = [
|
||||
'Response headers must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = [
|
||||
'Response config must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const configurePreflightMethods = (corsConfig, logicalIds) => {
|
||||
const preflightMethods = {};
|
||||
|
||||
_.forOwn(corsConfig, (config, path) => {
|
||||
const resourceLogicalId = logicalIds[path];
|
||||
|
||||
const preflightHeaders = {
|
||||
'Access-Control-Allow-Origin': `'${config.origins.join(',')}'`,
|
||||
'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`,
|
||||
'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`,
|
||||
};
|
||||
|
||||
const preflightMethodResponse = generateMethodResponseHeaders(preflightHeaders);
|
||||
const preflightIntegrationResponse = generateIntegrationResponseHeaders(preflightHeaders);
|
||||
|
||||
const preflightTemplate = `
|
||||
{
|
||||
"Type" : "AWS::ApiGateway::Method",
|
||||
"Properties" : {
|
||||
"AuthorizationType" : "NONE",
|
||||
"HttpMethod" : "OPTIONS",
|
||||
"MethodResponses" : [
|
||||
{
|
||||
"ResponseModels" : {},
|
||||
"ResponseParameters" : ${JSON.stringify(preflightMethodResponse)},
|
||||
"StatusCode" : "200"
|
||||
}
|
||||
],
|
||||
"RequestParameters" : {},
|
||||
"Integration" : {
|
||||
"Type" : "MOCK",
|
||||
"RequestTemplates" : {
|
||||
"application/json": "{statusCode:200}"
|
||||
},
|
||||
"IntegrationResponses" : [
|
||||
{
|
||||
"StatusCode" : "200",
|
||||
"ResponseParameters" : ${JSON.stringify(preflightIntegrationResponse)},
|
||||
"ResponseTemplates" : {
|
||||
"application/json": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ResourceId" : { "Ref": "${resourceLogicalId}" },
|
||||
"RestApiId" : { "Ref": "ApiGatewayRestApi" }
|
||||
}
|
||||
}
|
||||
`;
|
||||
const extractedResourceId = resourceLogicalId.match(/ApiGatewayResource(.*)/)[1];
|
||||
|
||||
_.merge(preflightMethods, {
|
||||
[`ApiGatewayMethod${extractedResourceId}Options`]:
|
||||
JSON.parse(preflightTemplate),
|
||||
});
|
||||
});
|
||||
|
||||
return preflightMethods;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets start the real work now!
|
||||
*/
|
||||
_.forEach(this.serverless.service.functions, (functionObject, functionName) => {
|
||||
functionObject.events.forEach(event => {
|
||||
if (event.http) {
|
||||
let method;
|
||||
let path;
|
||||
let requestPassThroughBehavior = 'NEVER';
|
||||
let integrationType = 'AWS_PROXY';
|
||||
let integrationResponseTemplate = null;
|
||||
|
||||
// Validate HTTP event object
|
||||
if (typeof event.http === 'object') {
|
||||
method = event.http.method;
|
||||
path = event.http.path;
|
||||
} else if (typeof event.http === 'string') {
|
||||
method = event.http.split(' ')[0];
|
||||
path = event.http.split(' ')[1];
|
||||
} else {
|
||||
const errorMessage = [
|
||||
`HTTP event of function ${functionName} is not an object nor a string.`,
|
||||
' The correct syntax is: http: get users/list',
|
||||
' OR an object with "path" and "method" properties.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes
|
||||
.Error(errorMessage);
|
||||
}
|
||||
|
||||
// Templates required to generate the cloudformation config
|
||||
|
||||
const DEFAULT_JSON_REQUEST_TEMPLATE = `
|
||||
#define( $loop )
|
||||
{
|
||||
#foreach($key in $map.keySet())
|
||||
"$util.escapeJavaScript($key)":
|
||||
"$util.escapeJavaScript($map.get($key))"
|
||||
#if( $foreach.hasNext ) , #end
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
{
|
||||
"body": $input.json("$"),
|
||||
"method": "$context.httpMethod",
|
||||
"principalId": "$context.authorizer.principalId",
|
||||
"stage": "$context.stage",
|
||||
|
||||
#set( $map = $input.params().header )
|
||||
"headers": $loop,
|
||||
|
||||
#set( $map = $input.params().querystring )
|
||||
"query": $loop,
|
||||
|
||||
#set( $map = $input.params().path )
|
||||
"path": $loop,
|
||||
|
||||
#set( $map = $context.identity )
|
||||
"identity": $loop,
|
||||
|
||||
#set( $map = $stageVariables )
|
||||
"stageVariables": $loop
|
||||
}
|
||||
`;
|
||||
|
||||
const DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE = `
|
||||
#define( $body )
|
||||
{
|
||||
#foreach( $token in $input.path('$').split('&') )
|
||||
#set( $keyVal = $token.split('=') )
|
||||
#set( $keyValSize = $keyVal.size() )
|
||||
#if( $keyValSize >= 1 )
|
||||
#set( $key = $util.escapeJavaScript($util.urlDecode($keyVal[0])) )
|
||||
#if( $keyValSize >= 2 )
|
||||
#set( $val = $util.escapeJavaScript($util.urlDecode($keyVal[1])) )
|
||||
#else
|
||||
#set( $val = '' )
|
||||
#end
|
||||
"$key": "$val"#if($foreach.hasNext),#end
|
||||
#end
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
#define( $loop )
|
||||
{
|
||||
#foreach($key in $map.keySet())
|
||||
"$util.escapeJavaScript($key)":
|
||||
"$util.escapeJavaScript($map.get($key))"
|
||||
#if( $foreach.hasNext ) , #end
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
{
|
||||
"body": $body,
|
||||
"method": "$context.httpMethod",
|
||||
"principalId": "$context.authorizer.principalId",
|
||||
"stage": "$context.stage",
|
||||
|
||||
#set( $map = $input.params().header )
|
||||
"headers": $loop,
|
||||
|
||||
#set( $map = $input.params().querystring )
|
||||
"query": $loop,
|
||||
|
||||
#set( $map = $input.params().path )
|
||||
"path": $loop,
|
||||
|
||||
#set( $map = $context.identity )
|
||||
"identity": $loop,
|
||||
|
||||
#set( $map = $stageVariables )
|
||||
"stageVariables": $loop
|
||||
}
|
||||
`;
|
||||
|
||||
// default integration request templates
|
||||
const integrationRequestTemplates = {
|
||||
'application/json': DEFAULT_JSON_REQUEST_TEMPLATE,
|
||||
'application/x-www-form-urlencoded': DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE,
|
||||
};
|
||||
|
||||
// configuring logical names for resources
|
||||
const resourceLogicalId = this.resourceLogicalIds[path];
|
||||
const normalizedMethod = method[0].toUpperCase() +
|
||||
method.substr(1).toLowerCase();
|
||||
const extractedResourceId = resourceLogicalId.match(/ApiGatewayResource(.*)/)[1];
|
||||
const normalizedFunctionName = functionName[0].toUpperCase()
|
||||
+ functionName.substr(1);
|
||||
|
||||
// scaffolds for method responses headers
|
||||
const methodResponseHeaders = [];
|
||||
const integrationResponseHeaders = [];
|
||||
const requestParameters = {};
|
||||
|
||||
// 1. Has request template
|
||||
if (hasRequestTemplate(event)) {
|
||||
_.forEach(event.http.request.template, (value, key) => {
|
||||
const requestTemplate = {};
|
||||
requestTemplate[key] = value;
|
||||
_.merge(integrationRequestTemplates, requestTemplate);
|
||||
});
|
||||
}
|
||||
|
||||
if (hasRequestParameters(event)) {
|
||||
// only these locations are currently supported
|
||||
const locations = ['querystrings', 'paths', 'headers'];
|
||||
_.each(locations, (location) => {
|
||||
// strip the plural s
|
||||
const singular = location.substring(0, location.length - 1);
|
||||
_.each(event.http.request.parameters[location], (value, key) => {
|
||||
requestParameters[`method.request.${singular}.${key}`] = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Has pass-through options
|
||||
if (hasPassThroughRequest(event)) {
|
||||
requestPassThroughBehavior = event.http.request.passThrough;
|
||||
}
|
||||
|
||||
// 3. Has response template
|
||||
if (hasResponseTemplate(event)) {
|
||||
integrationResponseTemplate = event.http.response.template;
|
||||
}
|
||||
|
||||
// 4. Has CORS enabled?
|
||||
if (hasCors(event)) {
|
||||
corsPreflight[path] = generateCorsPreflightConfig(event.http.cors,
|
||||
corsPreflight[path], method);
|
||||
|
||||
const corsHeader = {
|
||||
'Access-Control-Allow-Origin':
|
||||
`'${corsPreflight[path].origins.join('\',\'')}'`,
|
||||
};
|
||||
|
||||
_.merge(methodResponseHeaders, corsHeader);
|
||||
_.merge(integrationResponseHeaders, corsHeader);
|
||||
}
|
||||
|
||||
// Sort out response headers
|
||||
if (hasResponseHeaders(event)) {
|
||||
_.merge(methodResponseHeaders, event.http.response.headers);
|
||||
_.merge(integrationResponseHeaders, event.http.response.headers);
|
||||
}
|
||||
|
||||
// Sort out response config
|
||||
const responseConfig = {
|
||||
methodResponseHeaders,
|
||||
integrationResponseHeaders,
|
||||
integrationResponseTemplate,
|
||||
};
|
||||
|
||||
// Merge in any custom response config
|
||||
if (event.http.response && event.http.response.statusCodes) {
|
||||
responseConfig.statusCodes = event.http.response.statusCodes;
|
||||
} else {
|
||||
responseConfig.statusCodes = defaultStatusCodes;
|
||||
}
|
||||
|
||||
const response = generateResponse(responseConfig);
|
||||
|
||||
// check if LAMBDA or LAMBDA-PROXY was used for the integration type
|
||||
if (typeof event.http === 'object') {
|
||||
if (Boolean(event.http.integration) === true) {
|
||||
// normalize the integration for further processing
|
||||
const normalizedIntegration = event.http.integration.toUpperCase();
|
||||
// check if the user has entered a non-valid integration
|
||||
const allowedIntegrations = [
|
||||
'LAMBDA', 'LAMBDA-PROXY',
|
||||
];
|
||||
if (allowedIntegrations.indexOf(normalizedIntegration) === -1) {
|
||||
const errorMessage = [
|
||||
`Invalid APIG integration "${event.http.integration}"`,
|
||||
` in function "${functionName}".`,
|
||||
' Supported integrations are: lambda, lambda-proxy.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
// map the Serverless integration to the corresponding CloudFormation types
|
||||
// LAMBDA --> AWS
|
||||
// LAMBDA-PROXY --> AWS_PROXY
|
||||
if (normalizedIntegration === 'LAMBDA') {
|
||||
integrationType = 'AWS';
|
||||
} else if (normalizedIntegration === 'LAMBDA-PROXY') {
|
||||
integrationType = 'AWS_PROXY';
|
||||
} else {
|
||||
// default to AWS_PROXY (just in case…)
|
||||
integrationType = 'AWS_PROXY';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// show a warning when request / response config is used with AWS_PROXY (LAMBDA-PROXY)
|
||||
if (integrationType === 'AWS_PROXY' && (
|
||||
(!!event.http.request) || (!!event.http.response)
|
||||
)) {
|
||||
const warningMessage = [
|
||||
'Warning! You\'re using the LAMBDA-PROXY in combination with request / response',
|
||||
` configuration in your function "${functionName}".`,
|
||||
' This configuration will be ignored during deployment.',
|
||||
].join('');
|
||||
this.serverless.cli.log(warningMessage);
|
||||
}
|
||||
|
||||
const methodTemplate = `
|
||||
{
|
||||
"Type" : "AWS::ApiGateway::Method",
|
||||
"Properties" : {
|
||||
"AuthorizationType" : "NONE",
|
||||
"HttpMethod" : "${method.toUpperCase()}",
|
||||
"MethodResponses" : ${JSON.stringify(response.methodResponses)},
|
||||
"RequestParameters" : ${JSON.stringify(requestParameters)},
|
||||
"Integration" : {
|
||||
"IntegrationHttpMethod" : "POST",
|
||||
"Type" : "${integrationType}",
|
||||
"Uri" : {
|
||||
"Fn::Join": [ "",
|
||||
[
|
||||
"arn:aws:apigateway:",
|
||||
{"Ref" : "AWS::Region"},
|
||||
":lambda:path/2015-03-31/functions/",
|
||||
{"Fn::GetAtt" : ["${normalizedFunctionName}LambdaFunction", "Arn"]},
|
||||
"/invocations"
|
||||
]
|
||||
]
|
||||
},
|
||||
"RequestTemplates" : ${JSON.stringify(integrationRequestTemplates)},
|
||||
"PassthroughBehavior": "${requestPassThroughBehavior}",
|
||||
"IntegrationResponses" : ${JSON.stringify(response.integrationResponses)}
|
||||
},
|
||||
"ResourceId" : { "Ref": "${resourceLogicalId}" },
|
||||
"RestApiId" : { "Ref": "ApiGatewayRestApi" }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const methodTemplateJson = JSON.parse(methodTemplate);
|
||||
|
||||
// set authorizer config if available
|
||||
if (event.http.authorizer) {
|
||||
const authorizerName = event.http.authorizer.name;
|
||||
const normalizedAuthorizerName = authorizerName[0].toUpperCase()
|
||||
+ authorizerName.substr(1);
|
||||
const AuthorizerLogicalId = `${normalizedAuthorizerName}ApiGatewayAuthorizer`;
|
||||
|
||||
methodTemplateJson.Properties.AuthorizationType = 'CUSTOM';
|
||||
methodTemplateJson.Properties.AuthorizerId = {
|
||||
Ref: AuthorizerLogicalId,
|
||||
};
|
||||
methodTemplateJson.DependsOn = AuthorizerLogicalId;
|
||||
}
|
||||
|
||||
if (event.http.private) methodTemplateJson.Properties.ApiKeyRequired = true;
|
||||
|
||||
const methodObject = {
|
||||
[`ApiGatewayMethod${extractedResourceId}${normalizedMethod}`]:
|
||||
methodTemplateJson,
|
||||
};
|
||||
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
|
||||
methodObject);
|
||||
|
||||
// store a method logical id in memory to be used
|
||||
// by Deployment resources "DependsOn" property
|
||||
if (this.methodDependencies) {
|
||||
this.methodDependencies
|
||||
.push(`ApiGatewayMethod${extractedResourceId}${normalizedMethod}`);
|
||||
} else {
|
||||
this.methodDependencies =
|
||||
[`ApiGatewayMethod${extractedResourceId}${normalizedMethod}`];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!_.isEmpty(corsPreflight)) {
|
||||
// If we have some CORS config. configure the preflight method and merge
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
|
||||
configurePreflightMethods(corsPreflight, this.resourceLogicalIds));
|
||||
}
|
||||
|
||||
return BbPromise.resolve();
|
||||
},
|
||||
};
|
||||
@ -7,8 +7,7 @@ module.exports = {
|
||||
|
||||
compilePermissions() {
|
||||
this.validated.events.forEach((event) => {
|
||||
const normalizedFunctionName = event.functionName[0].toUpperCase()
|
||||
+ event.functionName.substr(1);
|
||||
const normalizedFunctionName = _.capitalize(event.functionName);
|
||||
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
|
||||
[`${normalizedFunctionName}LambdaPermissionApiGateway`]: {
|
||||
@ -25,8 +24,7 @@ module.exports = {
|
||||
|
||||
if (event.http.authorizer) {
|
||||
const authorizer = event.http.authorizer;
|
||||
const normalizedAuthorizerName = authorizer.name[0].toUpperCase()
|
||||
+ authorizer.name.substr(1);
|
||||
const normalizedAuthorizerName = _.capitalize(authorizer.name);
|
||||
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
|
||||
[`${normalizedAuthorizerName}LambdaPermissionApiGateway`]: {
|
||||
|
||||
@ -4,91 +4,78 @@ const BbPromise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
|
||||
compileResources() {
|
||||
this.resourceFunctions = [];
|
||||
this.resourcePaths = [];
|
||||
this.resourceLogicalIds = {};
|
||||
const resourcePaths = this.getResourcePaths();
|
||||
|
||||
_.forEach(this.serverless.service.functions, (functionObject, functionName) => {
|
||||
functionObject.events.forEach(event => {
|
||||
if (event.http) {
|
||||
let path;
|
||||
this.apiGatewayResourceNames = {};
|
||||
this.apiGatewayResourceLogicalIds = {};
|
||||
|
||||
if (typeof event.http === 'object') {
|
||||
path = event.http.path;
|
||||
} else if (typeof event.http === 'string') {
|
||||
path = event.http.split(' ')[1];
|
||||
} else {
|
||||
const errorMessage = [
|
||||
`HTTP event of function ${functionName} is not an object nor a string.`,
|
||||
' The correct syntax is: http: get users/list',
|
||||
' OR an object with "path" and "method" proeprties.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes
|
||||
.Error(errorMessage);
|
||||
}
|
||||
// ['users', 'users/create', 'users/create/something']
|
||||
resourcePaths.forEach(path => {
|
||||
const pathArray = path.split('/');
|
||||
const resourceName = pathArray.map(this.capitalizeAlphaNumericPath).join('');
|
||||
const resourceLogicalId = `ApiGatewayResource${resourceName}`;
|
||||
const pathPart = pathArray.pop();
|
||||
const parentPath = pathArray.join('/');
|
||||
const parentRef = this.getResourceId(parentPath);
|
||||
|
||||
while (path !== '') {
|
||||
if (this.resourcePaths.indexOf(path) === -1) {
|
||||
this.resourcePaths.push(path);
|
||||
this.resourceFunctions.push(functionName);
|
||||
}
|
||||
this.apiGatewayResourceNames[path] = resourceName;
|
||||
this.apiGatewayResourceLogicalIds[path] = resourceLogicalId;
|
||||
|
||||
const splittedPath = path.split('/');
|
||||
splittedPath.pop();
|
||||
path = splittedPath.join('/');
|
||||
}
|
||||
}
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
|
||||
[resourceLogicalId]: {
|
||||
Type: 'AWS::ApiGateway::Resource',
|
||||
Properties: {
|
||||
ParentId: parentRef,
|
||||
PathPart: pathPart,
|
||||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return BbPromise.resolve();
|
||||
},
|
||||
|
||||
const capitalizeAlphaNumericPath = (path) => _.upperFirst(
|
||||
getResourcePaths() {
|
||||
const paths = _.reduce(this.validated.events, (resourcePaths, event) => {
|
||||
let path = event.http.path;
|
||||
|
||||
while (path !== '') {
|
||||
if (resourcePaths.indexOf(path) === -1) {
|
||||
resourcePaths.push(path);
|
||||
}
|
||||
|
||||
const splittedPath = path.split('/');
|
||||
splittedPath.pop();
|
||||
path = splittedPath.join('/');
|
||||
}
|
||||
return resourcePaths;
|
||||
}, []);
|
||||
// (stable) sort so that parents get processed before children
|
||||
return _.sortBy(paths, path => path.split('/').length);
|
||||
},
|
||||
|
||||
capitalizeAlphaNumericPath(path) {
|
||||
return _.upperFirst(
|
||||
_.capitalize(path)
|
||||
.replace(/-/g, 'Dash')
|
||||
.replace(/\{(.*)\}/g, '$1Var')
|
||||
.replace(/[^0-9A-Za-z]/g, '')
|
||||
);
|
||||
},
|
||||
|
||||
// ['users', 'users/create', 'users/create/something']
|
||||
this.resourcePaths.forEach(path => {
|
||||
const resourcesArray = path.split('/');
|
||||
// resource name is the last element in the endpoint. It's not unique.
|
||||
const resourceName = path.split('/')[path.split('/').length - 1];
|
||||
const resourcePath = path;
|
||||
const normalizedResourceName = resourcesArray.map(capitalizeAlphaNumericPath).join('');
|
||||
const resourceLogicalId = `ApiGatewayResource${normalizedResourceName}`;
|
||||
this.resourceLogicalIds[resourcePath] = resourceLogicalId;
|
||||
resourcesArray.pop();
|
||||
getResourceId(path) {
|
||||
if (path === '') {
|
||||
return { 'Fn::GetAtt': [this.apiGatewayRestApiLogicalId, 'RootResourceId'] };
|
||||
}
|
||||
return { Ref: this.apiGatewayResourceLogicalIds[path] };
|
||||
},
|
||||
|
||||
let resourceParentId;
|
||||
if (resourcesArray.length === 0) {
|
||||
resourceParentId = '{ "Fn::GetAtt": ["ApiGatewayRestApi", "RootResourceId"] }';
|
||||
} else {
|
||||
const normalizedResourceParentName = resourcesArray
|
||||
.map(capitalizeAlphaNumericPath).join('');
|
||||
resourceParentId = `{ "Ref" : "ApiGatewayResource${normalizedResourceParentName}" }`;
|
||||
}
|
||||
|
||||
const resourceTemplate = `
|
||||
{
|
||||
"Type" : "AWS::ApiGateway::Resource",
|
||||
"Properties" : {
|
||||
"ParentId" : ${resourceParentId},
|
||||
"PathPart" : "${resourceName}",
|
||||
"RestApiId" : { "Ref" : "ApiGatewayRestApi" }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const resourceObject = {
|
||||
[resourceLogicalId]: JSON.parse(resourceTemplate),
|
||||
};
|
||||
|
||||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
|
||||
resourceObject);
|
||||
});
|
||||
|
||||
return BbPromise.resolve();
|
||||
getResourceName(path) {
|
||||
if (path === '') {
|
||||
return '';
|
||||
}
|
||||
return this.apiGatewayResourceNames[path];
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,9 +2,41 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const NOT_FOUND = -1;
|
||||
const DEFAULT_STATUS_CODES = {
|
||||
200: {
|
||||
pattern: '',
|
||||
},
|
||||
400: {
|
||||
pattern: '.*\\[400\\].*',
|
||||
},
|
||||
401: {
|
||||
pattern: '.*\\[401\\].*',
|
||||
},
|
||||
403: {
|
||||
pattern: '.*\\[403\\].*',
|
||||
},
|
||||
404: {
|
||||
pattern: '.*\\[404\\].*',
|
||||
},
|
||||
422: {
|
||||
pattern: '.*\\[422\\].*',
|
||||
},
|
||||
500: {
|
||||
pattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*',
|
||||
},
|
||||
502: {
|
||||
pattern: '.*\\[502\\].*',
|
||||
},
|
||||
504: {
|
||||
pattern: '.*\\[504\\].*',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validate() {
|
||||
const events = [];
|
||||
const corsPreflight = {};
|
||||
|
||||
_.forEach(this.serverless.service.functions, (functionObject, functionName) => {
|
||||
_.forEach(functionObject.events, (event) => {
|
||||
@ -18,6 +50,60 @@ module.exports = {
|
||||
http.authorizer = this.getAuthorizer(http, functionName);
|
||||
}
|
||||
|
||||
if (http.cors) {
|
||||
http.cors = this.getCors(http);
|
||||
|
||||
const cors = corsPreflight[http.path] || {};
|
||||
|
||||
cors.headers = _.union(http.cors.headers, cors.headers);
|
||||
cors.methods = _.union(http.cors.methods, cors.methods);
|
||||
cors.origins = _.union(http.cors.origins, cors.origins);
|
||||
|
||||
corsPreflight[http.path] = cors;
|
||||
}
|
||||
|
||||
http.integration = this.getIntegration(http);
|
||||
|
||||
if (http.integration === 'AWS') {
|
||||
if (http.request) {
|
||||
http.request = this.getRequest(http);
|
||||
|
||||
if (http.request.parameters) {
|
||||
http.request.parameters = this.getRequestParameters(http.request);
|
||||
}
|
||||
} else {
|
||||
http.request = {};
|
||||
}
|
||||
|
||||
http.request.passThrough = this.getRequestPassThrough(http);
|
||||
|
||||
if (http.response) {
|
||||
http.response = this.getResponse(http);
|
||||
} else {
|
||||
http.response = {};
|
||||
}
|
||||
|
||||
if (http.response.statusCodes) {
|
||||
http.response.statusCodes = _.assign({}, http.response.statusCodes);
|
||||
|
||||
if (!_.some(http.response.statusCodes, code => code.pattern === '')) {
|
||||
http.response.statusCodes['200'] = DEFAULT_STATUS_CODES['200'];
|
||||
}
|
||||
} else {
|
||||
http.response.statusCodes = DEFAULT_STATUS_CODES;
|
||||
}
|
||||
} else if (http.integration === 'AWS_PROXY') {
|
||||
// show a warning when request / response config is used with AWS_PROXY (LAMBDA-PROXY)
|
||||
if (http.request || http.response) {
|
||||
const warningMessage = [
|
||||
'Warning! You\'re using the LAMBDA-PROXY in combination with request / response',
|
||||
` configuration in your function "${functionName}".`,
|
||||
' This configuration will be ignored during deployment.',
|
||||
].join('');
|
||||
this.serverless.cli.log(warningMessage);
|
||||
}
|
||||
}
|
||||
|
||||
events.push({
|
||||
functionName,
|
||||
http,
|
||||
@ -28,6 +114,7 @@ module.exports = {
|
||||
|
||||
return {
|
||||
events,
|
||||
corsPreflight,
|
||||
};
|
||||
},
|
||||
|
||||
@ -146,9 +233,149 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
|
||||
getCors(http) {
|
||||
const headers = [
|
||||
'Content-Type',
|
||||
'X-Amz-Date',
|
||||
'Authorization',
|
||||
'X-Api-Key',
|
||||
'X-Amz-Security-Token',
|
||||
];
|
||||
|
||||
let cors = {
|
||||
origins: ['*'],
|
||||
methods: ['OPTIONS'],
|
||||
headers,
|
||||
};
|
||||
|
||||
if (typeof http.cors === 'object') {
|
||||
cors = http.cors;
|
||||
cors.methods = cors.methods || [];
|
||||
if (cors.headers) {
|
||||
if (!Array.isArray(cors.headers)) {
|
||||
const errorMessage = [
|
||||
'CORS header values must be provided as an array.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
cors.headers = headers;
|
||||
}
|
||||
|
||||
if (cors.methods.indexOf('OPTIONS') === NOT_FOUND) {
|
||||
cors.methods.push('OPTIONS');
|
||||
}
|
||||
|
||||
if (cors.methods.indexOf(http.method.toUpperCase()) === NOT_FOUND) {
|
||||
cors.methods.push(http.method.toUpperCase());
|
||||
}
|
||||
} else {
|
||||
cors.methods.push(http.method.toUpperCase());
|
||||
}
|
||||
|
||||
return cors;
|
||||
},
|
||||
|
||||
getIntegration(http) {
|
||||
if (http.integration) {
|
||||
const allowedIntegrations = [
|
||||
'LAMBDA-PROXY', 'LAMBDA',
|
||||
];
|
||||
// normalize the integration for further processing
|
||||
const normalizedIntegration = http.integration.toUpperCase();
|
||||
// check if the user has entered a non-valid integration
|
||||
if (allowedIntegrations.indexOf(normalizedIntegration) === NOT_FOUND) {
|
||||
const errorMessage = [
|
||||
`Invalid APIG integration "${http.integration}"`,
|
||||
` in function "${http.functionName}".`,
|
||||
' Supported integrations are: lambda, lambda-proxy.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
if (normalizedIntegration === 'LAMBDA') {
|
||||
return 'AWS';
|
||||
}
|
||||
}
|
||||
return 'AWS_PROXY';
|
||||
},
|
||||
|
||||
getRequest(http) {
|
||||
if (typeof http.request !== 'object') {
|
||||
const errorMessage = [
|
||||
'Request config must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
if (http.request.template && typeof http.request.template !== 'object') {
|
||||
const errorMessage = [
|
||||
'Template config must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
|
||||
return http.request;
|
||||
},
|
||||
|
||||
getRequestParameters(httpRequest) {
|
||||
const parameters = {};
|
||||
// only these locations are currently supported
|
||||
const locations = ['querystrings', 'paths', 'headers'];
|
||||
_.each(locations, (location) => {
|
||||
// strip the plural s
|
||||
const singular = location.substring(0, location.length - 1);
|
||||
_.each(httpRequest.parameters[location], (value, key) => {
|
||||
parameters[`method.request.${singular}.${key}`] = value;
|
||||
});
|
||||
});
|
||||
return parameters;
|
||||
},
|
||||
|
||||
getRequestPassThrough(http) {
|
||||
const requestPassThroughBehaviors = [
|
||||
'NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES',
|
||||
];
|
||||
|
||||
if (http.request.passThrough) {
|
||||
if (requestPassThroughBehaviors.indexOf(http.request.passThrough) === -1) {
|
||||
const errorMessage = [
|
||||
'Request passThrough "',
|
||||
http.request.passThrough,
|
||||
'" is not one of ',
|
||||
requestPassThroughBehaviors.join(', '),
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
|
||||
return http.request.passThrough;
|
||||
}
|
||||
|
||||
return requestPassThroughBehaviors[0];
|
||||
},
|
||||
|
||||
getResponse(http) {
|
||||
if (typeof http.response !== 'object') {
|
||||
const errorMessage = [
|
||||
'Response config must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
if (http.response.headers && typeof http.response.headers !== 'object') {
|
||||
const errorMessage = [
|
||||
'Response headers must be provided as an object.',
|
||||
' Please check the docs for more info.',
|
||||
].join('');
|
||||
throw new this.serverless.classes.Error(errorMessage);
|
||||
}
|
||||
return http.response;
|
||||
},
|
||||
|
||||
getLambdaArn(name) {
|
||||
this.serverless.service.getFunction(name);
|
||||
const normalizedName = name[0].toUpperCase() + name.substr(1);
|
||||
const normalizedName = _.capitalize(name);
|
||||
return { 'Fn::GetAtt': [`${normalizedName}LambdaFunction`, 'Arn'] };
|
||||
},
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ require('./validate');
|
||||
require('./restApi');
|
||||
require('./apiKeys');
|
||||
require('./resources');
|
||||
require('./cors');
|
||||
require('./methods');
|
||||
require('./authorizers');
|
||||
require('./deployment');
|
||||
|
||||
126
lib/plugins/aws/deploy/compile/events/apiGateway/tests/cors.js
Normal file
126
lib/plugins/aws/deploy/compile/events/apiGateway/tests/cors.js
Normal file
@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const AwsCompileApigEvents = require('../index');
|
||||
const Serverless = require('../../../../../../../Serverless');
|
||||
|
||||
describe('#compileCors()', () => {
|
||||
let serverless;
|
||||
let awsCompileApigEvents;
|
||||
|
||||
beforeEach(() => {
|
||||
serverless = new Serverless();
|
||||
serverless.service.service = 'first-service';
|
||||
serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} };
|
||||
serverless.service.environment = {
|
||||
stages: {
|
||||
dev: {
|
||||
regions: {
|
||||
'us-east-1': {
|
||||
vars: {
|
||||
IamRoleLambdaExecution:
|
||||
'arn:aws:iam::12345678:role/service-dev-IamRoleLambdaExecution-FOO12345678',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const options = {
|
||||
stage: 'dev',
|
||||
region: 'us-east-1',
|
||||
};
|
||||
awsCompileApigEvents = new AwsCompileApigEvents(serverless, options);
|
||||
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
|
||||
awsCompileApigEvents.apiGatewayResourceLogicalIds = {
|
||||
'users/create': 'ApiGatewayResourceUsersCreate',
|
||||
'users/list': 'ApiGatewayResourceUsersList',
|
||||
'users/update': 'ApiGatewayResourceUsersUpdate',
|
||||
'users/delete': 'ApiGatewayResourceUsersDelete',
|
||||
};
|
||||
awsCompileApigEvents.apiGatewayResourceNames = {
|
||||
'users/create': 'UsersCreate',
|
||||
'users/list': 'UsersList',
|
||||
'users/update': 'UsersUpdate',
|
||||
'users/delete': 'UsersDelete',
|
||||
};
|
||||
awsCompileApigEvents.validated = {};
|
||||
});
|
||||
|
||||
it('should create preflight method for CORS enabled resource', () => {
|
||||
awsCompileApigEvents.validated.corsPreflight = {
|
||||
'users/update': {
|
||||
origins: ['*'],
|
||||
headers: ['*'],
|
||||
methods: ['OPTIONS', 'PUT'],
|
||||
},
|
||||
'users/create': {
|
||||
origins: ['*'],
|
||||
headers: ['*'],
|
||||
methods: ['OPTIONS', 'POST'],
|
||||
},
|
||||
'users/delete': {
|
||||
origins: ['*'],
|
||||
headers: ['CustomHeaderA', 'CustomHeaderB'],
|
||||
methods: ['OPTIONS', 'DELETE'],
|
||||
},
|
||||
};
|
||||
return awsCompileApigEvents.compileCors().then(() => {
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersCreateOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
|
||||
).to.equal('\'*\'');
|
||||
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersCreateOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
|
||||
).to.equal('\'*\'');
|
||||
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersCreateOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
|
||||
).to.equal('\'OPTIONS,POST\'');
|
||||
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersUpdateOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
|
||||
).to.equal('\'*\'');
|
||||
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersUpdateOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
|
||||
).to.equal('\'OPTIONS,PUT\'');
|
||||
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersDeleteOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
|
||||
).to.equal('\'*\'');
|
||||
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersDeleteOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
|
||||
).to.equal('\'CustomHeaderA,CustomHeaderB\'');
|
||||
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayMethodUsersDeleteOptions
|
||||
.Properties.Integration.IntegrationResponses[0]
|
||||
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
|
||||
).to.equal('\'OPTIONS,DELETE\'');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -20,7 +20,7 @@ describe('#compileDeployment()', () => {
|
||||
};
|
||||
awsCompileApigEvents = new AwsCompileApigEvents(serverless, options);
|
||||
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
|
||||
awsCompileApigEvents.methodDependencies = ['method-dependency1', 'method-dependency2'];
|
||||
awsCompileApigEvents.apiGatewayMethodLogicalIds = ['method-dependency1', 'method-dependency2'];
|
||||
});
|
||||
|
||||
it('should create a deployment resource', () => awsCompileApigEvents
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -12,124 +12,232 @@ describe('#compileResources()', () => {
|
||||
serverless = new Serverless();
|
||||
serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} };
|
||||
awsCompileApigEvents = new AwsCompileApigEvents(serverless);
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'POST',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: 'GET bar/-',
|
||||
},
|
||||
{
|
||||
http: 'GET bar/foo',
|
||||
},
|
||||
{
|
||||
http: 'GET bar/{id}',
|
||||
},
|
||||
{
|
||||
http: 'GET bar/{id}/foobar',
|
||||
},
|
||||
{
|
||||
http: 'GET bar/{foo_id}',
|
||||
},
|
||||
{
|
||||
http: 'GET bar/{foo_id}/foobar',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
|
||||
awsCompileApigEvents.validated = {};
|
||||
});
|
||||
|
||||
it('should throw an error if http event type is not a string or an object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: 42,
|
||||
},
|
||||
],
|
||||
// sorted makes parent refs easier
|
||||
it('should construct the correct (sorted) resourcePaths array', () => {
|
||||
awsCompileApigEvents.validated.events = [
|
||||
{
|
||||
http: {
|
||||
path: '',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.compileResources()).to.throw(Error);
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'POST',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/-',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/foo',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/{id}/foobar',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/{id}',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/{foo_id}',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/{foo_id}/foobar',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(awsCompileApigEvents.getResourcePaths()).to.deep.equal([
|
||||
'foo',
|
||||
'bar',
|
||||
'foo/bar',
|
||||
'bar/-',
|
||||
'bar/foo',
|
||||
'bar/{id}',
|
||||
'bar/{foo_id}',
|
||||
'bar/{id}/foobar',
|
||||
'bar/{foo_id}/foobar',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should construct the correct resourcePaths array', () => awsCompileApigEvents
|
||||
.compileResources().then(() => {
|
||||
const expectedResourcePaths = [
|
||||
'foo/bar',
|
||||
'foo',
|
||||
'bar/-',
|
||||
'bar',
|
||||
'bar/foo',
|
||||
'bar/{id}',
|
||||
'bar/{id}/foobar',
|
||||
'bar/{foo_id}',
|
||||
'bar/{foo_id}/foobar',
|
||||
];
|
||||
expect(awsCompileApigEvents.resourcePaths).to.deep.equal(expectedResourcePaths);
|
||||
})
|
||||
);
|
||||
|
||||
it('should construct the correct resourceLogicalIds object', () => awsCompileApigEvents
|
||||
.compileResources().then(() => {
|
||||
const expectedResourceLogicalIds = {
|
||||
'bar/-': 'ApiGatewayResourceBarDash',
|
||||
'foo/bar': 'ApiGatewayResourceFooBar',
|
||||
foo: 'ApiGatewayResourceFoo',
|
||||
'bar/{id}/foobar': 'ApiGatewayResourceBarIdVarFoobar',
|
||||
'bar/{id}': 'ApiGatewayResourceBarIdVar',
|
||||
'bar/{foo_id}/foobar': 'ApiGatewayResourceBarFooidVarFoobar',
|
||||
'bar/{foo_id}': 'ApiGatewayResourceBarFooidVar',
|
||||
'bar/foo': 'ApiGatewayResourceBarFoo',
|
||||
bar: 'ApiGatewayResourceBar',
|
||||
};
|
||||
expect(awsCompileApigEvents.resourceLogicalIds).to.deep.equal(expectedResourceLogicalIds);
|
||||
})
|
||||
);
|
||||
|
||||
it('should create resource resources when http events are given', () => awsCompileApigEvents
|
||||
.compileResources().then(() => {
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceFooBar.Properties.PathPart)
|
||||
.to.equal('bar');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceFooBar.Properties.ParentId.Ref)
|
||||
.to.equal('ApiGatewayResourceFoo');
|
||||
it('should reference the appropriate ParentId', () => {
|
||||
awsCompileApigEvents.validated.events = [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'POST',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/-',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/foo',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/{id}/foobar',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'bar/{id}',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
];
|
||||
return awsCompileApigEvents.compileResources().then(() => {
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][0])
|
||||
.to.equal('ApiGatewayRestApi');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceBar.Properties.ParentId['Fn::GetAtt'][1])
|
||||
.Resources.ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][1])
|
||||
.to.equal('RootResourceId');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceFooBar.Properties.ParentId.Ref)
|
||||
.to.equal('ApiGatewayResourceFoo');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref)
|
||||
.to.equal('ApiGatewayResourceBar');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceBarFooidVar.Properties.ParentId.Ref)
|
||||
.to.equal('ApiGatewayResourceBar');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceBarFooidVarFoobar.Properties.ParentId.Ref)
|
||||
.to.equal('ApiGatewayResourceBarFooidVar');
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create resource resources when http events are not given', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [],
|
||||
it('should construct the correct resourceLogicalIds object', () => {
|
||||
awsCompileApigEvents.validated.events = [
|
||||
{
|
||||
http: {
|
||||
path: '',
|
||||
method: 'POST',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
http: {
|
||||
path: 'foo',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'foo/{foo_id}/bar',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'baz/foo',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
];
|
||||
return awsCompileApigEvents.compileResources().then(() => {
|
||||
expect(
|
||||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources
|
||||
).to.deep.equal({});
|
||||
expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({
|
||||
baz: 'ApiGatewayResourceBaz',
|
||||
'baz/foo': 'ApiGatewayResourceBazFoo',
|
||||
foo: 'ApiGatewayResourceFoo',
|
||||
'foo/{foo_id}': 'ApiGatewayResourceFooFooidVar',
|
||||
'foo/{foo_id}/bar': 'ApiGatewayResourceFooFooidVarBar',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should construct resourceLogicalIds that do not collide', () => {
|
||||
awsCompileApigEvents.validated.events = [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'POST',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'foo/{bar}',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
];
|
||||
return awsCompileApigEvents.compileResources().then(() => {
|
||||
expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({
|
||||
foo: 'ApiGatewayResourceFoo',
|
||||
'foo/bar': 'ApiGatewayResourceFooBar',
|
||||
'foo/{bar}': 'ApiGatewayResourceFooBarVar',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the appropriate Pathpart', () => {
|
||||
awsCompileApigEvents.validated.events = [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/{bar}',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
path: 'foo/{bar}/baz',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
];
|
||||
return awsCompileApigEvents.compileResources().then(() => {
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceFooBar.Properties.PathPart)
|
||||
.to.equal('bar');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceFooBarVar.Properties.PathPart)
|
||||
.to.equal('{bar}');
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources.ApiGatewayResourceFooBarVarBaz.Properties.PathPart)
|
||||
.to.equal('baz');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle root resource references', () => {
|
||||
awsCompileApigEvents.validated.events = [
|
||||
{
|
||||
http: {
|
||||
path: '',
|
||||
method: 'GET',
|
||||
},
|
||||
},
|
||||
];
|
||||
return awsCompileApigEvents.compileResources().then(() => {
|
||||
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
|
||||
.Resources).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,26 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const sinon = require('sinon');
|
||||
const AwsCompileApigEvents = require('../index');
|
||||
const Serverless = require('../../../../../../../Serverless');
|
||||
|
||||
describe('#validate()', () => {
|
||||
let serverless;
|
||||
let awsCompileApigEvents;
|
||||
|
||||
beforeEach(() => {
|
||||
const serverless = new Serverless();
|
||||
serverless.service.environment = {
|
||||
vars: {},
|
||||
stages: {
|
||||
dev: {
|
||||
vars: {},
|
||||
regions: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
serverless.service.environment.stages.dev.regions['us-east-1'] = {
|
||||
vars: {},
|
||||
};
|
||||
serverless = new Serverless();
|
||||
const options = {
|
||||
stage: 'dev',
|
||||
region: 'us-east-1',
|
||||
@ -28,6 +18,20 @@ describe('#validate()', () => {
|
||||
awsCompileApigEvents = new AwsCompileApigEvents(serverless, options);
|
||||
});
|
||||
|
||||
it('should ignore non-http events', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
ignored: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(0);
|
||||
});
|
||||
|
||||
it('should reject an invalid http event', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
@ -41,6 +45,20 @@ describe('#validate()', () => {
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw an error if http event type is not a string or an object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: 42,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should validate the http events "path" property', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
@ -158,6 +176,28 @@ describe('#validate()', () => {
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
});
|
||||
|
||||
it('should discard a starting slash from paths', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: 'GET /foo/bar',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(2);
|
||||
expect(validated.events[0].http).to.have.property('path', 'foo/bar');
|
||||
expect(validated.events[1].http).to.have.property('path', 'foo/bar');
|
||||
});
|
||||
|
||||
it('should throw if an authorizer is an invalid value', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
@ -312,4 +352,748 @@ describe('#validate()', () => {
|
||||
expect(authorizer.identitySource).to.equal('method.request.header.Custom');
|
||||
expect(authorizer.identityValidationExpression).to.equal('foo');
|
||||
});
|
||||
|
||||
it('should process cors defaults', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
cors: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.cors).to.deep.equal({
|
||||
headers: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token'],
|
||||
methods: ['OPTIONS', 'POST'],
|
||||
origins: ['*'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if request is malformed', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
integration: 'lambda',
|
||||
request: 'invalid',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw if request.passThrough is invalid', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
integration: 'lambda',
|
||||
request: {
|
||||
passThrough: 'INVALID',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw if request.template is malformed', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
integration: 'lambda',
|
||||
request: {
|
||||
template: 'invalid',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw if response is malformed', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
integration: 'lambda',
|
||||
response: 'invalid',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw if response.headers are malformed', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
integration: 'lambda',
|
||||
response: {
|
||||
headers: 'invalid',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw if cors headers are not an array', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
cors: {
|
||||
headers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should process cors options', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
cors: {
|
||||
headers: ['X-Foo-Bar'],
|
||||
origins: ['acme.com'],
|
||||
methods: ['POST', 'OPTIONS'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.cors).to.deep.equal({
|
||||
headers: ['X-Foo-Bar'],
|
||||
methods: ['POST', 'OPTIONS'],
|
||||
origins: ['acme.com'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge all preflight origins, method, and headers for a path', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users',
|
||||
cors: {
|
||||
origins: [
|
||||
'http://example.com',
|
||||
],
|
||||
},
|
||||
},
|
||||
}, {
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: 'users',
|
||||
cors: {
|
||||
origins: [
|
||||
'http://example2.com',
|
||||
],
|
||||
},
|
||||
},
|
||||
}, {
|
||||
http: {
|
||||
method: 'PUT',
|
||||
path: 'users/{id}',
|
||||
cors: {
|
||||
headers: [
|
||||
'TestHeader',
|
||||
],
|
||||
},
|
||||
},
|
||||
}, {
|
||||
http: {
|
||||
method: 'DELETE',
|
||||
path: 'users/{id}',
|
||||
cors: {
|
||||
headers: [
|
||||
'TestHeader2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.corsPreflight['users/{id}'].methods)
|
||||
.to.deep.equal(['OPTIONS', 'DELETE', 'PUT']);
|
||||
expect(validated.corsPreflight.users.origins)
|
||||
.to.deep.equal(['http://example2.com', 'http://example.com']);
|
||||
expect(validated.corsPreflight['users/{id}'].headers)
|
||||
.to.deep.equal(['TestHeader2', 'TestHeader']);
|
||||
});
|
||||
|
||||
it('should add default statusCode to custom statusCodes', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
integration: 'lambda',
|
||||
response: {
|
||||
statusCodes: {
|
||||
404: {
|
||||
pattern: '.*"statusCode":404,.*',
|
||||
template: '$input.path(\'$.errorMessage\')',
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.response.statusCodes).to.deep.equal({
|
||||
200: {
|
||||
pattern: '',
|
||||
},
|
||||
404: {
|
||||
pattern: '.*"statusCode":404,.*',
|
||||
template: '$input.path(\'$.errorMessage\')',
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow custom statusCode with default pattern', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
integration: 'lambda',
|
||||
response: {
|
||||
statusCodes: {
|
||||
418: {
|
||||
pattern: '',
|
||||
template: '$input.path(\'$.foo\')',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.response.statusCodes).to.deep.equal({
|
||||
418: {
|
||||
pattern: '',
|
||||
template: '$input.path(\'$.foo\')',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle expicit methods', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: '/foo/bar',
|
||||
cors: {
|
||||
methods: ['POST'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.cors.methods).to.deep.equal(['POST', 'OPTIONS']);
|
||||
});
|
||||
|
||||
it('should throw an error if the method is invalid', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'INVALID',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should set authorizer.arn when provided a name string', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
authorizer: {},
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
authorizer: 'authorizer',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.authorizer.name).to.equal('authorizer');
|
||||
expect(validated.events[0].http.authorizer.arn).to.deep.equal({
|
||||
'Fn::GetAtt': ['AuthorizerLambdaFunction', 'Arn'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should set authorizer.arn when provided an ARN string', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
authorizer: 'xxx:dev-authorizer',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.authorizer.name).to.equal('authorizer');
|
||||
expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer');
|
||||
});
|
||||
|
||||
it('should handle authorizer.name object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
authorizer: {},
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
authorizer: {
|
||||
name: 'authorizer',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.authorizer.name).to.equal('authorizer');
|
||||
expect(validated.events[0].http.authorizer.arn).to.deep.equal({
|
||||
'Fn::GetAtt': [
|
||||
'AuthorizerLambdaFunction',
|
||||
'Arn',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an authorizer.arn object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
authorizer: {
|
||||
arn: 'xxx:dev-authorizer',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.authorizer.name).to.equal('authorizer');
|
||||
expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer');
|
||||
});
|
||||
|
||||
it('should throw an error if the provided config is not an object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
request: 'some string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw an error if the template config is not an object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
request: {
|
||||
template: 'some string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should process request parameters', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
integration: 'lambda',
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
request: {
|
||||
parameters: {
|
||||
querystrings: {
|
||||
foo: true,
|
||||
bar: false,
|
||||
},
|
||||
paths: {
|
||||
foo: true,
|
||||
bar: false,
|
||||
},
|
||||
headers: {
|
||||
foo: true,
|
||||
bar: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.request.parameters).to.deep.equal({
|
||||
'method.request.querystring.foo': true,
|
||||
'method.request.querystring.bar': false,
|
||||
'method.request.path.foo': true,
|
||||
'method.request.path.bar': false,
|
||||
'method.request.header.foo': true,
|
||||
'method.request.header.bar': false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the provided response config is not an object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
response: 'some string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should throw an error if the response headers are not objects', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
response: {
|
||||
headers: 'some string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('throw error if authorizer property is not a string or object', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
authorizer: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('throw error if authorizer property is an object but no name or arn provided', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
path: 'foo/bar',
|
||||
method: 'GET',
|
||||
authorizer: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should set "AWS_PROXY" as the default integration type', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.integration).to.equal('AWS_PROXY');
|
||||
});
|
||||
|
||||
it('should support LAMBDA integration', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
integration: 'LAMBDA',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
method: 'PUT',
|
||||
path: 'users/list',
|
||||
integration: 'lambda',
|
||||
},
|
||||
},
|
||||
{
|
||||
http: {
|
||||
method: 'POST',
|
||||
path: 'users/list',
|
||||
integration: 'lambda-proxy',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(3);
|
||||
expect(validated.events[0].http.integration).to.equal('AWS');
|
||||
expect(validated.events[1].http.integration).to.equal('AWS');
|
||||
expect(validated.events[2].http.integration).to.equal('AWS_PROXY');
|
||||
});
|
||||
|
||||
it('should show a warning message when using request / response config with LAMBDA-PROXY', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
integration: 'lambda-proxy',
|
||||
request: {
|
||||
template: {
|
||||
'template/1': '{ "stage" : "$context.stage" }',
|
||||
'template/2': '{ "httpMethod" : "$context.httpMethod" }',
|
||||
},
|
||||
},
|
||||
response: {
|
||||
template: "$input.path('$.foo')",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
// initialize so we get the log method from the CLI in place
|
||||
serverless.init();
|
||||
|
||||
const logStub = sinon.stub(serverless.cli, 'log');
|
||||
|
||||
awsCompileApigEvents.validate();
|
||||
|
||||
expect(logStub.calledOnce).to.be.equal(true);
|
||||
expect(logStub.args[0][0].length).to.be.at.least(1);
|
||||
});
|
||||
|
||||
it('should throw an error when an invalid integration type was provided', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
integration: 'INVALID',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should accept a valid passThrough', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
integration: 'lambda',
|
||||
request: {
|
||||
passThrough: 'WHEN_NO_MATCH',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.request.passThrough).to.equal('WHEN_NO_MATCH');
|
||||
});
|
||||
|
||||
it('should default pass through to NEVER', () => {
|
||||
awsCompileApigEvents.serverless.service.functions = {
|
||||
first: {
|
||||
events: [
|
||||
{
|
||||
http: {
|
||||
method: 'GET',
|
||||
path: 'users/list',
|
||||
integration: 'lambda',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validated = awsCompileApigEvents.validate();
|
||||
expect(validated.events).to.be.an('Array').with.length(1);
|
||||
expect(validated.events[0].http.request.passThrough).to.equal('NEVER');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user