serverless-step-functions
Advanced tools
Comparing version 1.5.0 to 1.6.0
# 0.2.0(08.01.2017) | ||
## Breaking Changes | ||
The service name is added to the statemachin prefix. | ||
The service name is added to the statemachine prefix. | ||
This is because there is a possibility of conflict with the state machine deployed from another service without the service name. | ||
@@ -21,2 +21,2 @@ | ||
# 0.1.1(29.12.2016) | ||
First Release | ||
First Release |
@@ -15,2 +15,3 @@ 'use strict'; | ||
// TODO remove once "origins" config is deprecated | ||
let origin = config.origin; | ||
@@ -28,2 +29,12 @@ if (config.origins && config.origins.length) { | ||
// Enable CORS Max Age usage if set | ||
if (_.has(config, 'maxAge')) { | ||
if (_.isInteger(config.maxAge) && config.maxAge > 0) { | ||
preflightHeaders['Access-Control-Max-Age'] = `'${config.maxAge}'`; | ||
} else { | ||
const errorMessage = 'maxAge should be an integer over 0'; | ||
throw new this.serverless.classes.Error(errorMessage); | ||
} | ||
} | ||
if (_.includes(config.methods, 'ANY')) { | ||
@@ -48,6 +59,7 @@ preflightHeaders['Access-Control-Allow-Methods'] = | ||
}, | ||
ContentHandling: 'CONVERT_TO_TEXT', | ||
IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders), | ||
}, | ||
ResourceId: resourceRef, | ||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, | ||
RestApiId: this.provider.getApiGatewayRestApiId(), | ||
}, | ||
@@ -54,0 +66,0 @@ }, |
@@ -8,13 +8,7 @@ 'use strict'; | ||
describe('#methods()', () => { | ||
describe('#compileCors()', () => { | ||
let serverless; | ||
let serverlessStepFunctions; | ||
let awsCompileApigEvents; | ||
beforeEach(() => { | ||
serverless = new Serverless(); | ||
serverless.setProvider('aws', new AwsProvider(serverless)); | ||
serverless.service.provider.compiledCloudFormationTemplate = { | ||
Resources: {}, | ||
}; | ||
const options = { | ||
@@ -24,27 +18,50 @@ stage: 'dev', | ||
}; | ||
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: {}, | ||
serverless = new Serverless(); | ||
serverless.setProvider('aws', new AwsProvider(serverless, options)); | ||
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', | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
serverlessStepFunctions.apiGatewayResourceLogicalIds = { | ||
'users/create': 'ApiGatewayResourceUsersCreate', | ||
'users/list': 'ApiGatewayResourceUsersList', | ||
'users/update': 'ApiGatewayResourceUsersUpdate', | ||
'users/delete': 'ApiGatewayResourceUsersDelete', | ||
'users/any': 'ApiGatewayResourceUsersAny', | ||
awsCompileApigEvents = new ServerlessStepFunctions(serverless, options); | ||
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; | ||
awsCompileApigEvents.apiGatewayResources = { | ||
'users/create': { | ||
name: 'UsersCreate', | ||
resourceLogicalId: 'ApiGatewayResourceUsersCreate', | ||
}, | ||
'users/list': { | ||
name: 'UsersList', | ||
resourceLogicalId: 'ApiGatewayResourceUsersList', | ||
}, | ||
'users/update': { | ||
name: 'UsersUpdate', | ||
resourceLogicalId: 'ApiGatewayResourceUsersUpdate', | ||
}, | ||
'users/delete': { | ||
name: 'UsersDelete', | ||
resourceLogicalId: 'ApiGatewayResourceUsersDelete', | ||
}, | ||
'users/any': { | ||
name: 'UsersAny', | ||
resourceLogicalId: 'ApiGatewayResourceUsersAny', | ||
}, | ||
}; | ||
serverlessStepFunctions.apiGatewayResourceNames = { | ||
'users/create': 'UsersCreate', | ||
'users/list': 'UsersList', | ||
'users/update': 'UsersUpdate', | ||
'users/delete': 'UsersDelete', | ||
'users/any': 'UsersAny', | ||
}; | ||
serverlessStepFunctions.pluginhttpValidated = {}; | ||
awsCompileApigEvents.pluginhttpValidated = {}; | ||
}); | ||
it('should create preflight method for CORS enabled resource', () => { | ||
serverlessStepFunctions.pluginhttpValidated.corsPreflight = { | ||
awsCompileApigEvents.pluginhttpValidated.corsPreflight = { | ||
'users/update': { | ||
@@ -55,2 +72,3 @@ origin: 'http://example.com', | ||
allowCredentials: false, | ||
maxAge: 86400, | ||
}, | ||
@@ -62,2 +80,3 @@ 'users/create': { | ||
allowCredentials: true, | ||
maxAge: 86400, | ||
}, | ||
@@ -69,2 +88,3 @@ 'users/delete': { | ||
allowCredentials: false, | ||
maxAge: 86400, | ||
}, | ||
@@ -78,6 +98,6 @@ 'users/any': { | ||
}; | ||
return serverlessStepFunctions.compileCors().then(() => { | ||
return awsCompileApigEvents.compileCors().then(() => { | ||
// users/create | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersCreateOptions | ||
@@ -89,3 +109,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersCreateOptions | ||
@@ -97,3 +117,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersCreateOptions | ||
@@ -105,3 +125,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersCreateOptions | ||
@@ -112,5 +132,12 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersCreateOptions | ||
.Properties.Integration.IntegrationResponses[0] | ||
.ResponseParameters['method.response.header.Access-Control-Max-Age'] | ||
).to.equal('\'86400\''); | ||
// users/update | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersUpdateOptions | ||
@@ -122,3 +149,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersUpdateOptions | ||
@@ -130,3 +157,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersUpdateOptions | ||
@@ -137,5 +164,12 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersUpdateOptions | ||
.Properties.Integration.IntegrationResponses[0] | ||
.ResponseParameters['method.response.header.Access-Control-Max-Age'] | ||
).to.equal('\'86400\''); | ||
// users/delete | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersDeleteOptions | ||
@@ -147,3 +181,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersDeleteOptions | ||
@@ -155,3 +189,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersDeleteOptions | ||
@@ -163,3 +197,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersDeleteOptions | ||
@@ -170,5 +204,12 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersDeleteOptions | ||
.Properties.Integration.IntegrationResponses[0] | ||
.ResponseParameters['method.response.header.Access-Control-Max-Age'] | ||
).to.equal('\'86400\''); | ||
// users/any | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersAnyOptions | ||
@@ -180,3 +221,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersAnyOptions | ||
@@ -188,3 +229,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersAnyOptions | ||
@@ -196,3 +237,3 @@ .Properties.Integration.IntegrationResponses[0] | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayMethodUsersAnyOptions | ||
@@ -204,2 +245,32 @@ .Properties.Integration.IntegrationResponses[0] | ||
}); | ||
it('should throw error if maxAge is not an integer greater than 0', () => { | ||
awsCompileApigEvents.pluginhttpValidated.corsPreflight = { | ||
'users/update': { | ||
origin: 'http://example.com', | ||
headers: ['*'], | ||
methods: ['OPTIONS', 'PUT'], | ||
allowCredentials: false, | ||
maxAge: -1, | ||
}, | ||
}; | ||
expect(() => awsCompileApigEvents.compileCors()) | ||
.to.throw(Error, 'maxAge should be an integer over 0'); | ||
}); | ||
it('should throw error if maxAge is not an integer', () => { | ||
awsCompileApigEvents.pluginhttpValidated.corsPreflight = { | ||
'users/update': { | ||
origin: 'http://example.com', | ||
headers: ['*'], | ||
methods: ['OPTIONS', 'PUT'], | ||
allowCredentials: false, | ||
maxAge: 'five', | ||
}, | ||
}; | ||
expect(() => awsCompileApigEvents.compileCors()) | ||
.to.throw(Error, 'maxAge should be an integer over 0'); | ||
}); | ||
}); |
@@ -15,3 +15,3 @@ 'use strict'; | ||
Properties: { | ||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, | ||
RestApiId: this.provider.getApiGatewayRestApiId(), | ||
StageName: this.options.stage, | ||
@@ -30,3 +30,3 @@ }, | ||
'https://', | ||
{ Ref: this.apiGatewayRestApiLogicalId }, | ||
this.provider.getApiGatewayRestApiId(), | ||
`.execute-api.${this.options.region}.amazonaws.com/${this.options.stage}`, | ||
@@ -33,0 +33,0 @@ ], |
@@ -5,2 +5,3 @@ 'use strict'; | ||
const _ = require('lodash'); | ||
const awsArnRegExs = require('../../../utils/arnRegularExpressions'); | ||
@@ -24,3 +25,3 @@ module.exports = { | ||
ResourceId: resourceId, | ||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, | ||
RestApiId: this.provider.getApiGatewayRestApiId(), | ||
}, | ||
@@ -31,3 +32,4 @@ }; | ||
this.getMethodIntegration(event.stateMachineName, stateMachineObj, event.http), | ||
this.getMethodResponses(event.http) | ||
this.getMethodResponses(event.http), | ||
this.getMethodAuthorization(event.http) | ||
); | ||
@@ -185,2 +187,49 @@ | ||
}, | ||
getMethodAuthorization(http) { | ||
if (_.get(http, 'authorizer.type') === 'AWS_IAM') { | ||
return { | ||
Properties: { | ||
AuthorizationType: 'AWS_IAM', | ||
}, | ||
}; | ||
} | ||
if (http.authorizer) { | ||
if (http.authorizer.type && http.authorizer.authorizerId) { | ||
return { | ||
Properties: { | ||
AuthorizationType: http.authorizer.type, | ||
AuthorizerId: http.authorizer.authorizerId, | ||
}, | ||
}; | ||
} | ||
const authorizerLogicalId = this.provider.naming | ||
.getAuthorizerLogicalId(http.authorizer.name || http.authorizer); | ||
let authorizationType; | ||
const authorizerArn = http.authorizer.arn; | ||
if (typeof authorizerArn === 'string' | ||
&& awsArnRegExs.cognitoIdpArnExpr.test(authorizerArn)) { | ||
authorizationType = 'COGNITO_USER_POOLS'; | ||
} else { | ||
authorizationType = 'CUSTOM'; | ||
} | ||
return { | ||
Properties: { | ||
AuthorizationType: authorizationType, | ||
AuthorizerId: { Ref: authorizerLogicalId }, | ||
}, | ||
DependsOn: authorizerLogicalId, | ||
}; | ||
} | ||
return { | ||
Properties: { | ||
AuthorizationType: 'NONE', | ||
}, | ||
}; | ||
}, | ||
}; |
@@ -13,2 +13,7 @@ 'use strict'; | ||
beforeEach(() => { | ||
const options = { | ||
stage: 'dev', | ||
region: 'us-east-1', | ||
}; | ||
serverless = new Serverless(); | ||
@@ -20,7 +25,4 @@ serverless.setProvider('aws', new AwsProvider(serverless)); | ||
const options = { | ||
stage: 'dev', | ||
region: 'us-east-1', | ||
}; | ||
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); | ||
serverlessStepFunctions.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
@@ -56,2 +58,15 @@ stateMachines: { | ||
}; | ||
serverlessStepFunctions.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; | ||
serverlessStepFunctions.apiGatewayResources = { | ||
'foo/bar1': { | ||
name: 'First', | ||
resourceLogicalId: 'ApiGatewayResourceFirst', | ||
}, | ||
'foo/bar2': { | ||
name: 'Second', | ||
resourceLogicalId: 'ApiGatewayResourceSecond', | ||
}, | ||
}; | ||
}); | ||
@@ -64,3 +79,3 @@ | ||
.Resources) | ||
.to.have.property('ApiGatewayMethodapiGatewayResourceNamesFirstPost'); | ||
.to.have.property('ApiGatewayMethodFirstPost'); | ||
}) | ||
@@ -75,5 +90,5 @@ ); | ||
expect(resources.ApiGatewayMethodapiGatewayResourceNamesFirstPost | ||
expect(resources.ApiGatewayMethodFirstPost | ||
.Properties.ApiKeyRequired).to.eql(false); | ||
expect(resources.ApiGatewayMethodapiGatewayResourceNamesSecondPost | ||
expect(resources.ApiGatewayMethodSecondPost | ||
.Properties.ApiKeyRequired).to.eql(true); | ||
@@ -235,2 +250,73 @@ }) | ||
}); | ||
describe('#getMethodAuthorization()', () => { | ||
it('should return properties with AuthorizationType: NONE if no authorizer provided', () => { | ||
const event = { | ||
path: 'foo/bar1', | ||
method: 'post', | ||
}; | ||
expect(serverlessStepFunctions.getMethodAuthorization(event) | ||
.Properties.AuthorizationType).to.equal('NONE'); | ||
}); | ||
it('should return resource properties with AuthorizationType: AWS_IAM', () => { | ||
const event = { | ||
authorizer: { | ||
type: 'AWS_IAM', | ||
authorizerId: 'foo12345', | ||
}, | ||
}; | ||
expect(serverlessStepFunctions.getMethodAuthorization(event) | ||
.Properties.AuthorizationType).to.equal('AWS_IAM'); | ||
}); | ||
it('should return properties with AuthorizationType: CUSTOM and authotizerId', () => { | ||
const event = { | ||
authorizer: { | ||
type: 'CUSTOM', | ||
authorizerId: 'foo12345', | ||
}, | ||
}; | ||
expect(serverlessStepFunctions.getMethodAuthorization(event) | ||
.Properties.AuthorizationType).to.equal('CUSTOM'); | ||
expect(serverlessStepFunctions.getMethodAuthorization(event) | ||
.Properties.AuthorizerId).to.equal('foo12345'); | ||
}); | ||
it('should return properties with AuthorizationType: CUSTOM and resource reference', () => { | ||
const event = { | ||
authorizer: { | ||
name: 'authorizer', | ||
arn: { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] }, | ||
resultTtlInSeconds: 300, | ||
identitySource: 'method.request.header.Authorization', | ||
}, | ||
}; | ||
const autorization = serverlessStepFunctions.getMethodAuthorization(event); | ||
expect(autorization.Properties.AuthorizationType) | ||
.to.equal('CUSTOM'); | ||
expect(autorization.Properties.AuthorizerId) | ||
.to.deep.equal({ Ref: 'AuthorizerApiGatewayAuthorizer' }); | ||
}); | ||
it('should return properties with AuthorizationType: COGNITO_USER_POOLS', () => { | ||
const event = { | ||
authorizer: { | ||
name: 'authorizer', | ||
arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', | ||
}, | ||
}; | ||
const autorization = serverlessStepFunctions.getMethodAuthorization(event); | ||
expect(autorization.Properties.AuthorizationType) | ||
.to.equal('COGNITO_USER_POOLS'); | ||
expect(autorization.Properties.AuthorizerId) | ||
.to.deep.equal({ Ref: 'AuthorizerApiGatewayAuthorizer' }); | ||
}); | ||
}); | ||
}); |
@@ -9,25 +9,23 @@ 'use strict'; | ||
compileResources() { | ||
const resourcePaths = this.getResourcePaths(); | ||
this.apiGatewayResources = this.getResourcePaths(); | ||
// ['users', 'users/create', 'users/create/something'] | ||
_.keys(this.apiGatewayResources).forEach((path) => { | ||
const resource = this.apiGatewayResources[path]; | ||
if (resource.resourceId) { | ||
return; | ||
} | ||
this.apiGatewayResourceNames = {}; | ||
this.apiGatewayResourceLogicalIds = {}; | ||
resource.resourceLogicalId = this.provider.naming.getResourceLogicalId(path); | ||
resource.resourceId = { Ref: resource.resourceLogicalId }; | ||
resourcePaths.forEach(path => { | ||
const pathArray = path.split('/'); | ||
const resourceName = this.provider.naming.normalizePath(path); | ||
const resourceLogicalId = this.provider.naming.getResourceLogicalId(path); | ||
const pathPart = pathArray.pop(); | ||
const parentPath = pathArray.join('/'); | ||
const parentRef = this.getResourceId(parentPath); | ||
const parentRef = resource.parent | ||
? resource.parent.resourceId : this.getResourceId(); | ||
this.apiGatewayResourceNames[path] = resourceName; | ||
this.apiGatewayResourceLogicalIds[path] = resourceLogicalId; | ||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { | ||
[resourceLogicalId]: { | ||
[resource.resourceLogicalId]: { | ||
Type: 'AWS::ApiGateway::Resource', | ||
Properties: { | ||
ParentId: parentRef, | ||
PathPart: pathPart, | ||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, | ||
PathPart: resource.pathPart, | ||
RestApiId: this.provider.getApiGatewayRestApiId(), | ||
}, | ||
@@ -37,36 +35,220 @@ }, | ||
}); | ||
return BbPromise.resolve(); | ||
}, | ||
combineResourceTrees(trees) { | ||
const self = this; | ||
function getNodePaths(result, node) { | ||
const r = result; | ||
r[node.path] = node; | ||
if (!node.name) { | ||
r[node.path].name = self.provider.naming.normalizePath(node.path); | ||
} | ||
node.children.forEach((child) => getNodePaths(result, child)); | ||
} | ||
return _.reduce(trees, (result, tree) => { | ||
getNodePaths(result, tree); | ||
return result; | ||
}, {}); | ||
}, | ||
getResourcePaths() { | ||
const paths = _.reduce(this.pluginhttpValidated.events, (resourcePaths, event) => { | ||
let path = event.http.path; | ||
const trees = []; | ||
const predefinedResourceNodes = []; | ||
const methodNodes = []; | ||
const predefinedResources = this.provider.getApiGatewayPredefinedResources(); | ||
while (path !== '') { | ||
if (resourcePaths.indexOf(path) === -1) { | ||
resourcePaths.push(path); | ||
function cutBranch(node) { | ||
if (!node.parent) { | ||
return; | ||
} | ||
const n = node; | ||
if (node.parent.children.length <= 1) { | ||
n.parent.children = []; | ||
} else { | ||
n.parent.children = node.parent.children.filter((c) => c.path !== n.path); | ||
n.parent.isCut = true; | ||
} | ||
n.parent = null; | ||
} | ||
// organize all resource paths into N-ary tree | ||
function applyResource(resource, isMethod) { | ||
let root; | ||
let parent; | ||
let currentPath; | ||
const path = resource.path.replace(/^\//, '').replace(/\/$/, ''); | ||
const pathParts = path.split('/'); | ||
function applyNodeResource(node, parts, index) { | ||
const n = node; | ||
if (index === parts.length - 1) { | ||
n.name = resource.name; | ||
if (resource.resourceId) { | ||
n.resourceId = resource.resourceId; | ||
if (_.every(predefinedResourceNodes, (iter) => iter.path !== n.path)) { | ||
predefinedResourceNodes.push(node); | ||
} | ||
} | ||
if (isMethod && !node.hasMethod) { | ||
n.hasMethod = true; | ||
if (_.every(methodNodes, (iter) => iter.path !== n.path)) { | ||
methodNodes.push(node); | ||
} | ||
} | ||
} | ||
const splittedPath = path.split('/'); | ||
splittedPath.pop(); | ||
path = splittedPath.join('/'); | ||
parent = node; | ||
} | ||
return resourcePaths; | ||
}, []); | ||
return _.sortBy(paths, path => path.split('/').length); | ||
pathParts.forEach((pathPart, index) => { | ||
currentPath = currentPath ? `${currentPath}/${pathPart}` : pathPart; | ||
root = root || _.find(trees, (node) => node.path === currentPath); | ||
parent = parent || root; | ||
let node; | ||
if (parent) { | ||
if (parent.path === currentPath) { | ||
applyNodeResource(parent, pathParts, index); | ||
return; | ||
} else if (parent.children.length > 0) { | ||
node = _.find(parent.children, (n) => n.path === currentPath); | ||
if (node) { | ||
applyNodeResource(node, pathParts, index); | ||
return; | ||
} | ||
} | ||
} | ||
node = { | ||
path: currentPath, | ||
pathPart, | ||
parent, | ||
level: index, | ||
children: [], | ||
}; | ||
if (parent) { | ||
parent.children.push(node); | ||
} | ||
if (!root) { | ||
root = node; | ||
trees.push(root); | ||
} | ||
applyNodeResource(node, pathParts, index); | ||
}); | ||
} | ||
predefinedResources.forEach(applyResource); | ||
this.pluginhttpValidated.events.forEach((event) => { | ||
if (event.http.path) { | ||
applyResource(event.http, true); | ||
} | ||
}); | ||
// if predefinedResources array is empty, return all paths | ||
if (predefinedResourceNodes.length === 0) { | ||
return this.combineResourceTrees(trees); | ||
} | ||
// if all methods have resource ID already, no need to validate resource trees | ||
if (_.every(this.pluginhttpValidated.events, (event) => | ||
_.some(predefinedResourceNodes, (node) => | ||
node.path === event.http.path))) { | ||
return _.reduce(predefinedResources, (resourceMap, resource) => { | ||
const r = resourceMap; | ||
r[resource.path] = resource; | ||
if (!resource.name) { | ||
r[resource.path].name = this.provider.naming.normalizePath(resource.path); | ||
} | ||
return r; | ||
}, {}); | ||
} | ||
// cut resource branches from trees | ||
const sortedResourceNodes = _.sortBy(predefinedResourceNodes, | ||
node => node.level); | ||
const validatedTrees = []; | ||
for (let i = sortedResourceNodes.length - 1; i >= 0; i--) { | ||
const node = sortedResourceNodes[i]; | ||
let parent = node; | ||
while (parent && parent.parent) { | ||
if (parent.parent.hasMethod && !parent.parent.resourceId) { | ||
throw new Error(`Resource ID for path ${parent.parent.path} is required`); | ||
} | ||
if (parent.parent.resourceId || parent.parent.children.length > 1) { | ||
cutBranch(parent); | ||
break; | ||
} | ||
parent = parent.parent; | ||
} | ||
} | ||
// get branches that begin from root resource | ||
methodNodes.forEach((node) => { | ||
let iter = node; | ||
while (iter) { | ||
if (iter.resourceId) { | ||
cutBranch(iter); | ||
if (_.every(validatedTrees, (t) => t.path !== node.path)) { | ||
validatedTrees.push(iter); | ||
} | ||
break; | ||
} | ||
if (iter.isCut || (!iter.parent && iter.level > 0)) { | ||
throw new Error(`Resource ID for path ${iter.path} is required`); | ||
} | ||
if (!iter.parent) { | ||
validatedTrees.push(iter); | ||
break; | ||
} | ||
iter = iter.parent; | ||
} | ||
}); | ||
return this.combineResourceTrees(validatedTrees); | ||
}, | ||
getResourceId(path) { | ||
if (path === '') { | ||
return { 'Fn::GetAtt': [this.apiGatewayRestApiLogicalId, 'RootResourceId'] }; | ||
if (!path) { | ||
return this.provider.getApiGatewayRestApiRootResourceId(); | ||
} | ||
return { Ref: this.apiGatewayResourceLogicalIds[path] }; | ||
if (!this.apiGatewayResources || !this.apiGatewayResources[path]) { | ||
throw new Error(`Can not find API Gateway resource from path ${path}`); | ||
} | ||
if (!this.apiGatewayResources[path].resourceId | ||
&& this.apiGatewayResources[path].resourceLogicalId) { | ||
this.apiGatewayResources[path].resourceId = | ||
{ Ref: this.apiGatewayResources[path].resourceLogicalId }; | ||
} | ||
return this.apiGatewayResources[path].resourceId; | ||
}, | ||
getResourceName(path) { | ||
if (path === '') { | ||
if (path === '' || !this.apiGatewayResources) { | ||
return ''; | ||
} | ||
return this.apiGatewayResourceNames[path]; | ||
return this.apiGatewayResources[path].name; | ||
}, | ||
}; |
@@ -73,11 +73,11 @@ 'use strict'; | ||
]; | ||
expect(serverlessStepFunctions.getResourcePaths()).to.deep.equal([ | ||
expect(Object.keys(serverlessStepFunctions.getResourcePaths())).to.deep.equal([ | ||
'foo', | ||
'foo/bar', | ||
'bar', | ||
'foo/bar', | ||
'bar/-', | ||
'bar/foo', | ||
'bar/{id}', | ||
'bar/{id}/foobar', | ||
'bar/{foo_id}', | ||
'bar/{id}/foobar', | ||
'bar/{foo_id}/foobar', | ||
@@ -164,3 +164,3 @@ ]); | ||
return serverlessStepFunctions.compileResources().then(() => { | ||
expect(serverlessStepFunctions.apiGatewayResourceLogicalIds).to.deep.equal({ | ||
const expectedResourceLogicalIds = { | ||
baz: 'ApiGatewayResourceBaz', | ||
@@ -171,2 +171,6 @@ 'baz/foo': 'ApiGatewayResourceBazFoo', | ||
'foo/{foo_id}/bar': 'ApiGatewayResourceFooFooidVarBar', | ||
}; | ||
Object.keys(expectedResourceLogicalIds).forEach((path) => { | ||
expect(serverlessStepFunctions.apiGatewayResources[path].resourceLogicalId) | ||
.equal(expectedResourceLogicalIds[path]); | ||
}); | ||
@@ -192,6 +196,10 @@ }); | ||
return serverlessStepFunctions.compileResources().then(() => { | ||
expect(serverlessStepFunctions.apiGatewayResourceLogicalIds).to.deep.equal({ | ||
const expectedResourceLogicalIds = { | ||
foo: 'ApiGatewayResourceFoo', | ||
'foo/bar': 'ApiGatewayResourceFooBar', | ||
'foo/{bar}': 'ApiGatewayResourceFooBarVar', | ||
}; | ||
Object.keys(expectedResourceLogicalIds).forEach((path) => { | ||
expect(serverlessStepFunctions.apiGatewayResources[path].resourceLogicalId) | ||
.equal(expectedResourceLogicalIds[path]); | ||
}); | ||
@@ -250,8 +258,284 @@ }); | ||
describe('#getResourceName()', () => { | ||
it('should return empty if empty string gives to argument', () => { | ||
expect(serverlessStepFunctions.getResourceName('')) | ||
.to.equal(''); | ||
it('should create child resources only if there are predefined parent resources', () => { | ||
serverlessStepFunctions.serverless.service.provider.apiGateway = { | ||
restApiId: '6fyzt1pfpk', | ||
restApiRootResourceId: 'z5d4qh4oqi', | ||
restApiResources: { | ||
'/foo': 'axcybf2i39', | ||
'/users': 'zxcvbnmasd', | ||
'/users/friends': 'fcasdoojp1', | ||
'/groups': 'iuoyiusduo', | ||
}, | ||
}; | ||
serverlessStepFunctions.pluginhttpValidated.events = [ | ||
{ | ||
http: { | ||
path: 'foo/bar', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'foo/bar', | ||
method: 'POST', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'foo/bar', | ||
method: 'DELETE', | ||
}, | ||
}, | ||
{ | ||
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: 'users/friends/comments', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/me/posts', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'groups/categories', | ||
method: 'GET', | ||
}, | ||
}, | ||
]; | ||
return serverlessStepFunctions.compileResources().then(() => { | ||
try { | ||
serverlessStepFunctions.getResourceId('users/{userId}'); | ||
throw new Error('Expected API Gateway resource not found error, got success'); | ||
} catch (e) { | ||
expect(e.message).to.equal('Can not find API Gateway resource from path users/{userId}'); | ||
} | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceFoo).to.equal(undefined); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceBar.Properties.RestApiId) | ||
.to.equal('6fyzt1pfpk'); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceBar.Properties.ParentId) | ||
.to.equal('z5d4qh4oqi'); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceFooBar.Properties.ParentId) | ||
.to.equal('axcybf2i39'); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref) | ||
.to.equal('ApiGatewayResourceBar'); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceUsersMePosts).not.equal(undefined); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceUsersFriendsComments.Properties.ParentId) | ||
.to.equal('fcasdoojp1'); | ||
}); | ||
}); | ||
it('should not create any child resources if all resources exists', () => { | ||
serverlessStepFunctions.serverless.service.provider.apiGateway = { | ||
restApiId: '6fyzt1pfpk', | ||
restApiRootResourceId: 'z5d4qh4oqi', | ||
restApiResources: { | ||
foo: 'axcybf2i39', | ||
users: 'zxcvbnmasd', | ||
'users/friends': 'fcasdoojp1', | ||
'users/is/this/a/long/path': 'sadvgpoujk', | ||
}, | ||
}; | ||
serverlessStepFunctions.pluginhttpValidated.events = [ | ||
{ | ||
http: { | ||
path: 'foo', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/friends', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/is/this/a/long/path', | ||
method: 'GET', | ||
}, | ||
}, | ||
]; | ||
return serverlessStepFunctions.compileResources().then(() => { | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceFoo).to.equal(undefined); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceUsers).to.equal(undefined); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceUsersFriends).to.equal(undefined); | ||
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources.ApiGatewayResourceUsersIsThis).to.equal(undefined); | ||
}); | ||
}); | ||
it('should throw error if parent of existing resources is required', () => { | ||
serverlessStepFunctions.serverless.service.provider.apiGateway = { | ||
restApiId: '6fyzt1pfpk', | ||
restApiRootResourceId: 'z5d4qh4oqi', | ||
restApiResources: { | ||
'users/friends': 'fcasdoojp1', | ||
}, | ||
}; | ||
serverlessStepFunctions.pluginhttpValidated.events = [ | ||
{ | ||
http: { | ||
path: 'users', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/friends/{friendId}', | ||
method: 'GET', | ||
}, | ||
}, | ||
]; | ||
expect(() => serverlessStepFunctions.compileResources()) | ||
.to.throw(Error, 'Resource ID for path users is required'); | ||
}); | ||
it('should throw error if API root resourceId of existing resource is required', () => { | ||
serverlessStepFunctions.serverless.service.provider.apiGateway = { | ||
restApiId: '6fyzt1pfpk', | ||
restApiRootResourceId: '', | ||
restApiResources: { | ||
'users/friends': 'fcasdoojp1', | ||
}, | ||
}; | ||
serverlessStepFunctions.pluginhttpValidated.events = [ | ||
{ | ||
http: { | ||
path: 'users/test/{id}', | ||
method: 'GET', | ||
}, | ||
}, | ||
]; | ||
expect(() => serverlessStepFunctions.compileResources()) | ||
.to.throw(Error, 'Resource ID for path users is required'); | ||
}); | ||
it('should named all method paths if all resources are predefined', () => { | ||
serverlessStepFunctions.serverless.service.provider.apiGateway = { | ||
restApiId: '6fyzt1pfpk', | ||
restApiRootResourceId: 'z5d4qh4oqi', | ||
restApiResources: { | ||
'users/{id}': 'fcasdoojp1', | ||
'users/friends': 'fcasdoojp1', | ||
'users/friends/{id}': 'fcasdoojp1', | ||
}, | ||
}; | ||
serverlessStepFunctions.pluginhttpValidated.events = [ | ||
{ | ||
http: { | ||
path: '/users/{id}', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/friends', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/friends', | ||
method: 'POST', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/friends', | ||
method: 'DELETE', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/friends/{id}', | ||
method: 'GET', | ||
}, | ||
}, | ||
{ | ||
http: { | ||
path: 'users/friends/{id}', | ||
method: 'POST', | ||
}, | ||
}, | ||
]; | ||
return serverlessStepFunctions.compileResources().then(() => { | ||
expect(Object.keys(serverlessStepFunctions.serverless | ||
.service.provider.compiledCloudFormationTemplate | ||
.Resources).every((k) => ['ApiGatewayMethodundefinedGet', | ||
'ApiGatewayMethodundefinedPost'].indexOf(k) === -1)) | ||
.to.equal(true); | ||
}); | ||
}); | ||
it('should return empty string if path is empty', () => { | ||
expect(serverlessStepFunctions.getResourceName('')).to.equal(''); | ||
}); | ||
it('should return empty string if no resources', () => { | ||
expect(serverlessStepFunctions.getResourceName('users/friends')).to.equal(''); | ||
}); | ||
it('should return resource name for given path', () => { | ||
serverlessStepFunctions.apiGatewayResources = { | ||
'users/create': { | ||
name: 'UsersCreate', | ||
resourceLogicalId: 'ApiGatewayResourceUsersCreate', | ||
}, | ||
}; | ||
expect(serverlessStepFunctions.getResourceName('users/create')).to.equal('UsersCreate'); | ||
}); | ||
}); |
@@ -8,4 +8,28 @@ 'use strict'; | ||
compileRestApi() { | ||
if (this.serverless.service.provider.apiGateway && | ||
this.serverless.service.provider.apiGateway.restApiId) { | ||
return BbPromise.resolve(); | ||
} | ||
this.apiGatewayRestApiLogicalId = this.provider.naming.getRestApiLogicalId(); | ||
let endpointType = 'EDGE'; | ||
if (this.serverless.service.provider.endpointType) { | ||
const validEndpointTypes = ['REGIONAL', 'EDGE', 'PRIVATE']; | ||
endpointType = this.serverless.service.provider.endpointType; | ||
if (typeof endpointType !== 'string') { | ||
throw new this.serverless.classes.Error('endpointType must be a string'); | ||
} | ||
if (!_.includes(validEndpointTypes, endpointType.toUpperCase())) { | ||
const message = 'endpointType must be one of "REGIONAL" or "EDGE" or "PRIVATE". ' + | ||
`You provided ${endpointType}.`; | ||
throw new this.serverless.classes.Error(message); | ||
} | ||
endpointType = endpointType.toUpperCase(); | ||
} | ||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { | ||
@@ -16,2 +40,5 @@ [this.apiGatewayRestApiLogicalId]: { | ||
Name: this.provider.naming.getApiGatewayName(), | ||
EndpointConfiguration: { | ||
Types: [endpointType], | ||
}, | ||
}, | ||
@@ -21,4 +48,15 @@ }, | ||
if (!_.isEmpty(this.serverless.service.provider.resourcePolicy)) { | ||
const policy = { | ||
Version: '2012-10-17', | ||
Statement: this.serverless.service.provider.resourcePolicy, | ||
}; | ||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources[this.apiGatewayRestApiLogicalId].Properties, { | ||
Policy: policy, | ||
}); | ||
} | ||
return BbPromise.resolve(); | ||
}, | ||
}; |
@@ -18,2 +18,5 @@ 'use strict'; | ||
Name: 'dev-new-service', | ||
EndpointConfiguration: { | ||
Types: ['EDGE'], | ||
}, | ||
}, | ||
@@ -24,20 +27,118 @@ }, | ||
const serviceResourcesAwsResourcesObjectWithResourcePolicyMock = { | ||
Resources: { | ||
ApiGatewayRestApi: { | ||
Type: 'AWS::ApiGateway::RestApi', | ||
Properties: { | ||
Name: 'dev-new-service', | ||
EndpointConfiguration: { | ||
Types: ['EDGE'], | ||
}, | ||
Policy: { | ||
Version: '2012-10-17', | ||
Statement: [ | ||
{ | ||
Effect: 'Allow', | ||
Principal: '*', | ||
Action: 'execute-api:Invoke', | ||
Resource: ['execute-api:/*/*/*'], | ||
Condition: { | ||
IpAddress: { | ||
'aws:SourceIp': ['123.123.123.123'], | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
beforeEach(() => { | ||
const options = { | ||
stage: 'dev', | ||
region: 'us-east-1', | ||
}; | ||
serverless = new Serverless(); | ||
serverless.setProvider('aws', new AwsProvider(serverless)); | ||
serverless.setProvider('aws', new AwsProvider(serverless, options)); | ||
serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; | ||
serverless.service.service = 'new-service'; | ||
serverlessStepFunctions = new ServerlessStepFunctions(serverless); | ||
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); | ||
serverlessStepFunctions.serverless.service.service = 'new-service'; | ||
serverlessStepFunctions.serverless.service.functions = { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
path: 'foo/bar', | ||
method: 'POST', | ||
}, | ||
}, | ||
], | ||
}, | ||
}; | ||
}); | ||
it('should create a REST API resource', () => serverlessStepFunctions | ||
.compileRestApi().then(() => { | ||
expect( | ||
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate | ||
.Resources | ||
).to.deep.equal( | ||
serviceResourcesAwsResourcesObjectMock.Resources | ||
); | ||
}) | ||
); | ||
it('should create a REST API resource', () => | ||
serverlessStepFunctions.compileRestApi().then(() => { | ||
expect(serverlessStepFunctions.serverless.service | ||
.provider.compiledCloudFormationTemplate.Resources).to.deep.equal( | ||
serviceResourcesAwsResourcesObjectMock.Resources | ||
); | ||
})); | ||
it('should create a REST API resource with resource policy', () => { | ||
serverlessStepFunctions.serverless.service.provider.resourcePolicy = [ | ||
{ | ||
Effect: 'Allow', | ||
Principal: '*', | ||
Action: 'execute-api:Invoke', | ||
Resource: ['execute-api:/*/*/*'], | ||
Condition: { | ||
IpAddress: { | ||
'aws:SourceIp': ['123.123.123.123'], | ||
}, | ||
}, | ||
}, | ||
]; | ||
return serverlessStepFunctions.compileRestApi().then(() => { | ||
expect(serverlessStepFunctions.serverless.service.provider | ||
.compiledCloudFormationTemplate.Resources).to.deep.equal( | ||
serviceResourcesAwsResourcesObjectWithResourcePolicyMock.Resources | ||
); | ||
}); | ||
}); | ||
it('should ignore REST API resource creation if there is predefined restApi config', () => { | ||
serverlessStepFunctions.serverless.service.provider.apiGateway = { | ||
restApiId: '6fyzt1pfpk', | ||
restApiRootResourceId: 'z5d4qh4oqi', | ||
}; | ||
return serverlessStepFunctions.compileRestApi().then(() => { | ||
expect(serverlessStepFunctions.serverless.service.provider | ||
.compiledCloudFormationTemplate.Resources).to.deep.equal( | ||
{} | ||
); | ||
}); | ||
}); | ||
it('throw error if endpointType property is not a string', () => { | ||
serverlessStepFunctions.serverless.service.provider.endpointType = ['EDGE']; | ||
expect(() => serverlessStepFunctions.compileRestApi()).to.throw(Error); | ||
}); | ||
it('should compile if endpointType property is REGIONAL', () => { | ||
serverlessStepFunctions.serverless.service.provider.endpointType = 'REGIONAL'; | ||
expect(() => serverlessStepFunctions.compileRestApi()).to.not.throw(Error); | ||
}); | ||
it('should compile if endpointType property is PRIVATE', () => { | ||
serverlessStepFunctions.serverless.service.provider.endpointType = 'PRIVATE'; | ||
expect(() => serverlessStepFunctions.compileRestApi()).to.not.throw(Error); | ||
}); | ||
it('throw error if endpointType property is not EDGE or REGIONAL', () => { | ||
serverlessStepFunctions.serverless.service.provider.endpointType = 'Testing'; | ||
expect(() => serverlessStepFunctions.compileRestApi()).to.throw('endpointType must be one of'); | ||
}); | ||
}); |
'use strict'; | ||
const NOT_FOUND = -1; | ||
const _ = require('lodash'); | ||
const awsArnRegExs = require('../../../utils/arnRegularExpressions'); | ||
@@ -18,2 +19,6 @@ module.exports = { | ||
if (http.authorizer) { | ||
http.authorizer = this.getAuthorizer(http, stateMachineName); | ||
} | ||
if (http.cors) { | ||
@@ -30,2 +35,7 @@ http.cors = this.getCors(http); | ||
// when merging, last one defined wins | ||
if (_.has(http.cors, 'maxAge')) { | ||
cors.maxAge = http.cors.maxAge; | ||
} | ||
corsPreflight[http.path] = cors; | ||
@@ -107,2 +117,119 @@ } | ||
getIntegration(http, stateMachineName) { | ||
if (http.integration) { | ||
// normalize the integration for further processing | ||
const normalizedIntegration = http.integration.toUpperCase().replace('-', '_'); | ||
const allowedIntegrations = [ | ||
'LAMBDA_PROXY', 'LAMBDA', 'AWS', 'AWS_PROXY', 'HTTP', 'HTTP_PROXY', 'MOCK', | ||
]; | ||
// 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 "${stateMachineName}".`, | ||
' Supported integrations are:', | ||
' lambda, lambda-proxy, aws, aws-proxy, http, http-proxy, mock.', | ||
].join(''); | ||
throw new this.serverless.classes.Error(errorMessage); | ||
} | ||
if (normalizedIntegration === 'LAMBDA') { | ||
return 'AWS'; | ||
} else if (normalizedIntegration === 'LAMBDA_PROXY') { | ||
return 'AWS_PROXY'; | ||
} | ||
return normalizedIntegration; | ||
} | ||
return 'AWS_PROXY'; | ||
}, | ||
getAuthorizer(http, functionName) { | ||
const authorizer = http.authorizer; | ||
let type; | ||
let name; | ||
let arn; | ||
let identitySource; | ||
let resultTtlInSeconds; | ||
let identityValidationExpression; | ||
let claims; | ||
let authorizerId; | ||
if (typeof authorizer === 'string') { | ||
if (authorizer.toUpperCase() === 'AWS_IAM') { | ||
type = 'AWS_IAM'; | ||
} else if (authorizer.indexOf(':') === -1) { | ||
name = authorizer; | ||
arn = this.getLambdaArn(authorizer); | ||
} else { | ||
arn = authorizer; | ||
name = this.provider.naming.extractAuthorizerNameFromArn(arn); | ||
} | ||
} else if (typeof authorizer === 'object') { | ||
if (authorizer.type && authorizer.authorizerId) { | ||
type = authorizer.type; | ||
authorizerId = authorizer.authorizerId; | ||
} else if (authorizer.type && authorizer.type.toUpperCase() === 'AWS_IAM') { | ||
type = 'AWS_IAM'; | ||
} else if (authorizer.arn) { | ||
arn = authorizer.arn; | ||
if (_.isString(authorizer.name)) { | ||
name = authorizer.name; | ||
} else { | ||
name = this.provider.naming.extractAuthorizerNameFromArn(arn); | ||
} | ||
} else if (authorizer.name) { | ||
name = authorizer.name; | ||
arn = this.getLambdaArn(name); | ||
} else { | ||
throw new this.serverless.classes.Error('Please provide either an authorizer name or ARN'); | ||
} | ||
if (!type) { | ||
type = authorizer.type; | ||
} | ||
resultTtlInSeconds = Number.parseInt(authorizer.resultTtlInSeconds, 10); | ||
resultTtlInSeconds = Number.isNaN(resultTtlInSeconds) ? 300 : resultTtlInSeconds; | ||
claims = authorizer.claims || []; | ||
identitySource = authorizer.identitySource; | ||
identityValidationExpression = authorizer.identityValidationExpression; | ||
} else { | ||
const errorMessage = [ | ||
`authorizer property in function ${functionName} is not an object nor a string.`, | ||
' The correct format is: authorizer: functionName', | ||
' OR an object containing a name property.', | ||
' Please check the docs for more info.', | ||
].join(''); | ||
throw new this.serverless.classes.Error(errorMessage); | ||
} | ||
if (typeof identitySource === 'undefined') { | ||
identitySource = 'method.request.header.Authorization'; | ||
} | ||
const integration = this.getIntegration(http); | ||
if (integration === 'AWS_PROXY' | ||
&& typeof arn === 'string' | ||
&& awsArnRegExs.cognitoIdpArnExpr.test(arn) | ||
&& authorizer.claims) { | ||
const errorMessage = [ | ||
'Cognito claims can only be filtered when using the lambda integration type', | ||
]; | ||
throw new this.serverless.classes.Error(errorMessage); | ||
} | ||
return { | ||
type, | ||
name, | ||
arn, | ||
authorizerId, | ||
resultTtlInSeconds, | ||
identitySource, | ||
identityValidationExpression, | ||
claims, | ||
}; | ||
}, | ||
getCors(http) { | ||
@@ -165,2 +292,8 @@ const headers = [ | ||
}, | ||
getLambdaArn(name) { | ||
this.serverless.service.getFunction(name); | ||
const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(name); | ||
return { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] }; | ||
}, | ||
}; |
@@ -222,2 +222,351 @@ 'use strict'; | ||
it('should throw if an authorizer is an invalid value', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: true, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
expect(() => serverlessStepFunctions.httpValidate()).to.throw(Error); | ||
}); | ||
it('should throw if an authorizer is an empty object', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: {}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
expect(() => serverlessStepFunctions.httpValidate()).to.throw(Error); | ||
}); | ||
it('should throw if an cognito claims are being with a lambda proxy', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
integration: 'lambda-proxy', | ||
authorizer: { | ||
arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', | ||
claims: [ | ||
'email', | ||
'nickname', | ||
], | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
expect(() => serverlessStepFunctions.httpValidate()).to.throw(Error); | ||
}); | ||
it('should throw if an integration is not supported', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
integration: 'fake', | ||
authorizer: { | ||
arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
expect(() => serverlessStepFunctions.httpValidate()).to.throw(Error); | ||
}); | ||
it('should accept AWS_IAM as authorizer', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
foo: {}, | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: 'aws_iam', | ||
}, | ||
}, | ||
], | ||
}, | ||
second: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: { | ||
type: 'aws_iam', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
expect(validated.events).to.be.an('Array').with.length(2); | ||
expect(validated.events[0].http.authorizer.type).to.equal('AWS_IAM'); | ||
expect(validated.events[1].http.authorizer.type).to.equal('AWS_IAM'); | ||
}); | ||
it('should accept an authorizer as a string', () => { | ||
serverlessStepFunctions.serverless.service.functions = { | ||
foo: {}, | ||
}; | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: 'foo', | ||
}, | ||
}, | ||
], | ||
}, | ||
second: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: 'sss:dev-authorizer', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
expect(validated.events).to.be.an('Array').with.length(2); | ||
expect(validated.events[0].http.authorizer.name).to.equal('foo'); | ||
expect(validated.events[0].http.authorizer.arn).to.deep.equal({ | ||
'Fn::GetAtt': [ | ||
'FooLambdaFunction', | ||
'Arn', | ||
], | ||
}); | ||
expect(validated.events[1].http.authorizer.name).to.equal('authorizer'); | ||
expect(validated.events[1].http.authorizer.arn).to.equal('sss:dev-authorizer'); | ||
}); | ||
it('should set authorizer defaults', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: { | ||
arn: 'sss:dev-authorizer', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
const authorizer = validated.events[0].http.authorizer; | ||
expect(authorizer.resultTtlInSeconds).to.equal(300); | ||
expect(authorizer.identitySource).to.equal('method.request.header.Authorization'); | ||
}); | ||
it('should accept authorizer config', () => { | ||
serverlessStepFunctions.serverless.service.functions = { | ||
foo: {}, | ||
}; | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
integration: 'LAMBDA', | ||
authorizer: { | ||
name: 'foo', | ||
resultTtlInSeconds: 500, | ||
identitySource: 'method.request.header.Custom', | ||
identityValidationExpression: 'foo', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
const authorizer = validated.events[0].http.authorizer; | ||
expect(authorizer.resultTtlInSeconds).to.equal(500); | ||
expect(authorizer.identitySource).to.equal('method.request.header.Custom'); | ||
expect(authorizer.identityValidationExpression).to.equal('foo'); | ||
}); | ||
it('should accept authorizer config with a type', () => { | ||
serverlessStepFunctions.serverless.service.functions = { | ||
foo: {}, | ||
}; | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
integration: 'MOCK', | ||
authorizer: { | ||
name: 'foo', | ||
type: 'request', | ||
resultTtlInSeconds: 500, | ||
identitySource: 'method.request.header.Custom', | ||
identityValidationExpression: 'foo', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
const authorizer = validated.events[0].http.authorizer; | ||
expect(authorizer.type).to.equal('request'); | ||
}); | ||
it('should accept authorizer config with a type and authorizerId', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: { | ||
name: 'foo', | ||
type: 'CUSTOM', | ||
authorizerId: '12345', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
expect(validated.events[0].http.authorizer.type).to.equal('CUSTOM'); | ||
expect(validated.events[0].http.authorizer.authorizerId).to.equal('12345'); | ||
}); | ||
it('should accept authorizer config with a lambda arn', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: { | ||
name: 'foo', | ||
arn: 'xxx:xxx:Lambda-Name', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
expect(validated.events[0].http.authorizer.arn).to.equal('xxx:xxx:Lambda-Name'); | ||
}); | ||
it('should accept authorizer config when resultTtlInSeconds is 0', () => { | ||
serverlessStepFunctions.serverless.service.functions = { | ||
foo: {}, | ||
}; | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
foo: {}, | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'GET', | ||
path: 'foo/bar', | ||
authorizer: { | ||
name: 'foo', | ||
resultTtlInSeconds: 0, | ||
identitySource: 'method.request.header.Custom', | ||
identityValidationExpression: 'foo', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
const authorizer = validated.events[0].http.authorizer; | ||
expect(authorizer.resultTtlInSeconds).to.equal(0); | ||
expect(authorizer.identitySource).to.equal('method.request.header.Custom'); | ||
expect(authorizer.identityValidationExpression).to.equal('foo'); | ||
}); | ||
it('should throw an error if "origin" and "origins" CORS config is used', () => { | ||
@@ -417,2 +766,27 @@ serverlessStepFunctions.serverless.service.stepFunctions = { | ||
}); | ||
it('should set cors Access-Control-Max-Age headers', () => { | ||
serverlessStepFunctions.serverless.service.stepFunctions = { | ||
stateMachines: { | ||
first: { | ||
events: [ | ||
{ | ||
http: { | ||
method: 'POST', | ||
path: '/foo/bar', | ||
cors: { | ||
origin: '*', | ||
maxAge: 86400, | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
const validated = serverlessStepFunctions.httpValidate(); | ||
expect(validated.events[0].http.cors.origin).to.equal('*'); | ||
expect(validated.events[0].http.cors.maxAge).to.equal(86400); | ||
}); | ||
}); |
@@ -10,2 +10,5 @@ 'use strict'; | ||
const httpMethods = require('./deploy/events/apiGateway/methods'); | ||
// eslint-disable-next-line max-len | ||
const httpAuthorizers = require('serverless/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers'); | ||
const httpCors = require('./deploy/events/apiGateway/cors'); | ||
@@ -44,2 +47,3 @@ const httpApiKeys = require('./deploy/events/apiGateway/apiKeys'); | ||
httpMethods, | ||
httpAuthorizers, | ||
httpCors, | ||
@@ -106,3 +110,5 @@ httpApiKeys, | ||
this.compileScheduledEvents().then(() => { | ||
this.pluginhttpValidated = this.httpValidate(); | ||
// FIXME: Rename pluginhttpValidated to validated | ||
// so that we can use internal serverless libraries | ||
this.validated = this.pluginhttpValidated = this.httpValidate(); | ||
@@ -117,2 +123,3 @@ if (this.pluginhttpValidated.events.length === 0) { | ||
.then(this.compileMethods) | ||
.then(this.compileAuthorizers) | ||
.then(this.compileCors) | ||
@@ -147,2 +154,3 @@ .then(this.compileHttpIamRole) | ||
} | ||
this.serverless.cli.consoleLog(result); | ||
@@ -149,0 +157,0 @@ return BbPromise.resolve(); |
@@ -104,2 +104,4 @@ 'use strict'; | ||
.stub(serverlessStepFunctions, 'compileMethods').returns(BbPromise.resolve()); | ||
const compileAuthorizersStub = sinon | ||
.stub(serverlessStepFunctions, 'compileAuthorizers').returns(BbPromise.resolve()); | ||
const compileCorsStub = sinon | ||
@@ -124,2 +126,3 @@ .stub(serverlessStepFunctions, 'compileCors').returns(BbPromise.resolve()); | ||
expect(compileMethodsStub.notCalled).to.be.equal(true); | ||
expect(compileAuthorizersStub.notCalled).to.be.equal(true); | ||
expect(compileCorsStub.notCalled).to.be.equal(true); | ||
@@ -136,2 +139,3 @@ expect(compileHttpIamRoleStub.notCalled).to.be.equal(true); | ||
serverlessStepFunctions.compileMethods.restore(); | ||
serverlessStepFunctions.compileAuthorizers.restore(); | ||
serverlessStepFunctions.compileCors.restore(); | ||
@@ -158,2 +162,4 @@ serverlessStepFunctions.compileHttpIamRole.restore(); | ||
.stub(serverlessStepFunctions, 'compileMethods').returns(BbPromise.resolve()); | ||
const compileAuthorizersStub = sinon | ||
.stub(serverlessStepFunctions, 'compileAuthorizers').returns(BbPromise.resolve()); | ||
const compileCorsStub = sinon | ||
@@ -178,2 +184,3 @@ .stub(serverlessStepFunctions, 'compileCors').returns(BbPromise.resolve()); | ||
expect(compileMethodsStub.calledAfter(compileResourcesStub)).to.be.equal(true); | ||
expect(compileAuthorizersStub.calledAfter(compileResourcesStub)).to.be.equal(true); | ||
expect(compileCorsStub.calledAfter(compileMethodsStub)).to.be.equal(true); | ||
@@ -191,2 +198,3 @@ expect(compileHttpIamRoleStub.calledAfter(compileCorsStub)).to.be.equal(true); | ||
serverlessStepFunctions.compileMethods.restore(); | ||
serverlessStepFunctions.compileAuthorizers.restore(); | ||
serverlessStepFunctions.compileHttpIamRole.restore(); | ||
@@ -193,0 +201,0 @@ serverlessStepFunctions.compileDeployment.restore(); |
{ | ||
"name": "serverless-step-functions", | ||
"version": "1.5.0", | ||
"version": "1.6.0", | ||
"description": "The module is AWS Step Functions plugin for Serverless Framework", | ||
@@ -35,11 +35,11 @@ "main": "lib/index.js", | ||
"mocha-lcov-reporter": "^1.2.0", | ||
"serverless": "^1.26.1", | ||
"serverless": "^1.29.0", | ||
"sinon": "^1.17.5" | ||
}, | ||
"dependencies": { | ||
"lodash": "^4.13.1", | ||
"aws-sdk": "^2.7.19", | ||
"aws-sdk": "^2.282.1", | ||
"bluebird": "^3.4.0", | ||
"chalk": "^1.1.1" | ||
"chalk": "^1.1.1", | ||
"lodash": "^4.17.10" | ||
} | ||
} |
128
README.md
@@ -172,2 +172,49 @@ [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) [![Build Status](https://travis-ci.org/horike37/serverless-step-functions.svg?branch=master)](https://travis-ci.org/horike37/serverless-step-functions) [![npm version](https://badge.fury.io/js/serverless-step-functions.svg)](https://badge.fury.io/js/serverless-step-functions) [![Coverage Status](https://coveralls.io/repos/github/horike37/serverless-step-functions/badge.svg?branch=master)](https://coveralls.io/github/horike37/serverless-step-functions?branch=master) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) | ||
#### Share API Gateway and API Resources | ||
You can [share the same API Gateway](https://serverless.com/framework/docs/providers/aws/events/apigateway/#share-api-gateway-and-api-resources) between multiple projects by referencing its REST API ID and Root Resource ID in serverless.yml as follows: | ||
```yml | ||
service: service-name | ||
provider: | ||
name: aws | ||
apiGateway: | ||
# REST API resource ID. Default is generated by the framework | ||
restApiId: xxxxxxxxxx | ||
# Root resource, represent as / path | ||
restApiRootResourceId: xxxxxxxxxx | ||
functions: | ||
... | ||
``` | ||
If your application has many nested paths, you might also want to break them out into smaller services. | ||
However, Cloudformation will throw an error if we try to generate an existing path resource. To avoid that, we reference the resource ID: | ||
```yml | ||
service: service-a | ||
provider: | ||
apiGateway: | ||
restApiId: xxxxxxxxxx | ||
restApiRootResourceId: xxxxxxxxxx | ||
# List of existing resources that were created in the REST API. This is required or the stack will be conflicted | ||
restApiResources: | ||
/users: xxxxxxxxxx | ||
functions: | ||
... | ||
``` | ||
Now we can define endpoints using existing API Gateway ressources | ||
```yml | ||
stepFunctions: | ||
stateMachines: | ||
hello: | ||
events: | ||
- http: | ||
path: users/create | ||
method: POST | ||
``` | ||
#### Enabling CORS | ||
@@ -213,3 +260,84 @@ | ||
Configuring the cors property sets Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods,Access-Control-Allow-Credentials headers in the CORS preflight response. | ||
To enable the Access-Control-Max-Age preflight response header, set the maxAge property in the cors object: | ||
```yml | ||
stepFunctions: | ||
stateMachines: | ||
SfnApiGateway: | ||
events: | ||
- http: | ||
path: /playground/start | ||
method: post | ||
cors: | ||
origin: '*' | ||
maxAge: 86400 | ||
``` | ||
#### HTTP Endpoints with AWS_IAM Authorizers | ||
If you want to require that the caller submit the IAM user's access keys in order to be authenticated to invoke your Lambda Function, set the authorizer to AWS_IAM as shown in the following example: | ||
```yml | ||
stepFunctions: | ||
stateMachines: | ||
hello: | ||
events: | ||
- http: | ||
path: posts/create | ||
method: POST | ||
authorizer: aws_iam | ||
definition: | ||
``` | ||
#### HTTP Endpoints with Custom Authorizers | ||
[Custom Authorizers](https://serverless.com/framework/docs/providers/aws/events/apigateway/#http-endpoints-with-custom-authorizers) allow you to run an AWS Lambda Function before your targeted AWS Lambda Function. This is useful for Microservice Architectures or when you simply want to do some Authorization before running your business logic. | ||
You can enable Custom Authorizers for your HTTP endpoint by setting the Authorizer in your http event to another function in the same service, as shown in the following example: | ||
```yml | ||
stepFunctions: | ||
stateMachines: | ||
hello: | ||
- http: | ||
path: posts/create | ||
method: post | ||
authorizer: authorizerFunc | ||
definition: | ||
``` | ||
If the Authorizer function does not exist in your service but exists in AWS, you can provide the ARN of the Lambda function instead of the function name, as shown in the following example: | ||
```yml | ||
stepFunctions: | ||
stateMachines: | ||
hello: | ||
- http: | ||
path: posts/create | ||
method: post | ||
authorizer: xxx:xxx:Lambda-Name | ||
definition: | ||
``` | ||
### Share Authorizer | ||
Auto-created Authorizer is convenient for conventional setup. However, when you need to define your custom Authorizer, or use COGNITO_USER_POOLS authorizer with shared API Gateway, it is painful because of AWS limitation. Sharing Authorizer is a better way to do. | ||
```yml | ||
stepFunctions: | ||
stateMachines: | ||
createUser: | ||
... | ||
events: | ||
- http: | ||
path: /users | ||
... | ||
authorizer: | ||
# Provide both type and authorizerId | ||
type: COGNITO_USER_POOLS # TOKEN, CUSTOM or COGNITO_USER_POOLS, same as AWS Cloudformation documentation | ||
authorizerId: | ||
Ref: ApiGatewayAuthorizer # or hard-code Authorizer ID | ||
``` | ||
#### Customizing request body mapping templates | ||
@@ -216,0 +344,0 @@ |
250730
6042
756
Updatedaws-sdk@^2.282.1
Updatedlodash@^4.17.10