lambdasync
Version:
Easy sync between local code and AWS lambda functions
273 lines (247 loc) • 7.76 kB
JavaScript
const fs = require('fs');
const path = require('path');
const ncp = require('copy-paste');
const chainData = require('chain-promise-data');
const aws = require('../aws.js');
const {
LAMBDASYNC_SRC,
API_STAGE_NAME,
HTTP_ANY,
HTTP_OPTIONS
} = require('../constants.js');
const {updateSettings, getSettings} = require('../settings.js');
const {
awsPromise,
markdown,
startWith
} = require('../util.js');
let AWS;
let apigateway;
function defaultName() {
return 'LambdaSyncApi' + Math.floor(Math.random() * 100000000);
}
function getGateway(settings) {
if (apigateway) {
return apigateway;
}
AWS = AWS || aws(settings);
apigateway = new AWS.APIGateway();
return apigateway;
}
function createApi({name, description} = {name: defaultName(), description: 'lambdasync deployed api'}, settings) {
apigateway = getGateway(settings);
return awsPromise(apigateway, 'createRestApi', {name, description});
}
function addResource({parentId, restApiId} = {}, settings) {
apigateway = getGateway(settings);
return awsPromise(apigateway, 'createResource', {parentId, restApiId, pathPart: '{proxy+}'});
}
function getResources({restApiId} = {}, settings) {
apigateway = getGateway(settings);
return awsPromise(apigateway, 'getResources', {restApiId});
}
function getRootResource({id} = {}) {
return getResources({restApiId: id})
.then(({items}) => {
const rootResource = items && items
.filter(resource => resource.path === '/')
.reduce((prev, current) => current.id, '');
return rootResource;
});
}
function addResourceToApiGateway({apiGatewayId} = {}) {
const restApiId = apiGatewayId;
return getRootResource({id: apiGatewayId})
.then(id => startWith({
restApiId,
parentId: id
}))
.then(chainData(addResource));
}
function persistApiGateway({id, name} = {}) {
return updateSettings({apiGatewayId: id, apiGatewayName: name});
}
function addMethodAny({restApiId, resourceId}) {
return awsPromise(apigateway, 'putMethod', {
restApiId,
resourceId,
httpMethod: HTTP_ANY,
authorizationType: 'NONE',
apiKeyRequired: false,
requestParameters: {
'method.request.path.proxy': true
}
});
}
function addMethodAnyIntegration({restApiId, resourceId, region, lambdaArn}) {
const uri = `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations`;
return awsPromise(apigateway, 'putIntegration', {
restApiId,
resourceId,
uri,
type: 'AWS_PROXY',
httpMethod: HTTP_ANY,
integrationHttpMethod: 'POST',
passthroughBehavior: 'WHEN_NO_MATCH',
contentHandling: 'CONVERT_TO_TEXT',
cacheNamespace: resourceId,
cacheKeyParameters: ['method.request.path.proxy']
});
}
function addMethodOptions({restApiId, resourceId}) {
return awsPromise(apigateway, 'putMethod', {
restApiId,
resourceId,
httpMethod: HTTP_OPTIONS,
authorizationType: 'NONE',
apiKeyRequired: false,
requestParameters: {}
});
}
function addMethodOptionsResponse({restApiId, resourceId}) {
return awsPromise(apigateway, 'putMethodResponse', {
restApiId,
resourceId,
httpMethod: HTTP_OPTIONS,
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': false,
'method.response.header.Access-Control-Allow-Methods': false,
'method.response.header.Access-Control-Allow-Origin': false
},
responseModels: {
'application/json': 'Empty'
}
});
}
function addMethodOptionsIntegration({restApiId, resourceId}) {
return awsPromise(apigateway, 'putIntegration', {
restApiId,
resourceId,
httpMethod: HTTP_OPTIONS,
type: 'MOCK',
requestTemplates: {
'application/json': '{"statusCode": 200}'
},
passthroughBehavior: 'WHEN_NO_MATCH',
cacheNamespace: resourceId,
cacheKeyParameters: []
});
}
function addMethodOptionsIntegrationResponse({restApiId, resourceId}) {
return awsPromise(apigateway, 'putIntegrationResponse', {
restApiId,
resourceId,
httpMethod: HTTP_OPTIONS,
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': '\'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token\'',
'method.response.header.Access-Control-Allow-Methods': '\'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT\'',
'method.response.header.Access-Control-Allow-Origin': '\'*\''
},
responseTemplates: {
'application/json': null
}
});
}
function addCorsSupport({restApiId, resourceId}) {
return startWith({
restApiId,
resourceId
})
.then(chainData(addMethodOptions))
.then(chainData(addMethodOptionsResponse))
.then(chainData(addMethodOptionsIntegration))
.then(chainData(addMethodOptionsIntegrationResponse));
}
function addMappings({id, restApiId, region, lambdaArn, lambdaRole}) {
return startWith({
restApiId,
resourceId: id,
region,
lambdaArn,
lambdaRole
})
.then(chainData(addMethodAny))
.then(chainData(addMethodAnyIntegration))
.then(chainData(addCorsSupport));
}
function deployApi(settings) {
if (settings.apiGatewayUrl) {
return settings;
}
apigateway = getGateway(settings);
const {apiGatewayRestApiId, region} = settings;
const stageName = API_STAGE_NAME;
const apiGatewayUrl = `https://${apiGatewayRestApiId}.execute-api.${region}.amazonaws.com/${stageName}`;
return awsPromise(apigateway, 'createDeployment', {
restApiId: apiGatewayRestApiId,
stageName
})
.then(({id}) => updateSettings({
apiGatewayUrl,
apiGatewayDeploymentId: id
}))
.then(settings => {
console.log(markdown({
templatePath: 'markdown/deploy-success.md',
data: settings
}));
ncp.copy(settings.apiGatewayUrl);
return settings;
});
}
function addStageVariables(vars = {}) {
const op = (key, value) => ({
op: 'replace',
path: `/variables/${key}`,
value
});
return getSettings()
.then(settings => {
const apigateway = getGateway(settings);
const restApiId = settings.apiGatewayRestApiId;
const patchOperations = Object.keys(vars).map(key => op(key, vars[key]));
return awsPromise(apigateway, 'updateStage', {restApiId, stageName: API_STAGE_NAME, patchOperations})
.then(() => vars)
.then(handleStageVariablesSuccess);
});
}
function handleStageVariablesSuccess(vars) {
let templateString = fs.readFileSync(path.join(LAMBDASYNC_SRC, 'markdown', 'secret-success.md'), 'utf8');
templateString = Object.keys(vars).reduce((acc, key) =>
acc + '**Secret key:** `' + key + '`\n', templateString);
console.log(markdown({templateString}));
}
function setupApiGateway(settings) {
if (settings.apiGatewayId) {
return settings;
}
console.log('Deploying your API endpoint...');
const name = `api-${settings.lambdaName}`;
const description = `Lambdasync API for function ${settings.lambdaName}`;
return createApi({name, description}, settings)
.then(({id, name}) => persistApiGateway({id, name}))
.then(addResourceToApiGateway)
.then(res => {
const params = custom => Object.assign(res, settings, custom);
return addMappings(params({httpMethod: HTTP_ANY}))
.then(() => getRootResource({id: res.restApiId}))
.then(id => addMappings(params({httpMethod: HTTP_ANY, id})));
})
.then(result => {
return updateSettings({
apiGatewayRestApiId: result.restApiId,
apiGatewayResourceId: result.resourceId
});
})
.catch(err => console.log(err));
}
module.exports = {
createApi,
addResource,
getResources,
setupApiGateway,
deployApi,
addStageVariables
};