UNPKG

serverless-swagger-api

Version:

[![Build Status](https://travis-ci.org/drg-adaptive/serverless-swagger-api.svg)](https://travis-ci.org/drg-adaptive/serverless-swagger-api) [![Maintainability](https://api.codeclimate.com/v1/badges/006339522a8624e9bacb/maintainability)](https://codeclimat

311 lines 13.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); const AWS = require("aws-sdk"); const hash = require("string-hash"); const MAX_NAME_LENGTH = 64; function clipString(value, postfix) { const maxLength = MAX_NAME_LENGTH - 1 - postfix.length; if (value.length > maxLength) { value = [ value.substr(0, maxLength - 9), hash(value) .toString(16) .toUpperCase() ].join("-"); } return [value, postfix].join("-"); } class SwaggerApiPlugin { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.name = "serverless-swagger-api"; this.hooks = { "before:package:finalize": this.updateApiDefinitions(), "after:deploy": () => this.updateApiDeployments(), "updateDeployments:update": () => this.updateApiDeployments() }; this.commands = { updateDeployments: { usage: `Update API Gateway deployments defined with ${this.name}`, lifecycleEvents: ["update"], options: { message: { usage: "Specify the message attached to this deployment", required: false } } } }; } get stackName() { return [ this.serverless.service.getServiceName(), this.serverless.getProvider("aws").getStage() ].join("-"); } get pluginOptions() { return this.serverless.service.custom.swaggerApi || {}; } get stackApis() { return this.pluginOptions.apis || {}; } async updateApiDeployments() { const aws = this.serverless.getProvider("aws"); const region = aws.getRegion(); const options = this.pluginOptions; if (options.updateDeployments === false) { return; } const cloudFormation = new AWS.CloudFormation({ region, apiVersion: "2010-05-15" }); const apigateway = new AWS.APIGateway({ region, apiVersion: "2015-07-09" }); const stack = await cloudFormation .describeStackResources({ StackName: this.stackName }) .promise(); const apis = this.stackApis; for (const key of Object.keys(apis)) { try { const restApi = apis[key]; if (options.usePackageVersion) { const packageLocation = path.resolve("./package.json"); this.serverless.cli.log(`Loading version from ${packageLocation}`); const { version } = require(packageLocation); restApi.Body.info.version = version; } const stageName = restApi.Stage || this.serverless.service.provider.stage; const apiResource = stack.StackResources.find(x => x.LogicalResourceId === key); const restApiId = apiResource.PhysicalResourceId; this.serverless.cli.log(`Creating new deployment for ${restApiId} api stage ${stageName}...`); await apigateway .createDeployment({ restApiId, stageName, description: this.options.message || `${this.name} auto-deployment` }) .promise(); } catch (ex) { this.serverless.cli.log(`Could not update API: ${key}`); this.serverless.cli.log(ex.message); } } } updateApiDefinitions() { return (() => { const apis = this.stackApis; for (const key of Object.keys(apis)) { this.serverless.cli.log(`Creating ${key} api`); const api = apis[key]; this.createRestApi(key, api); } }).bind(this); } filterMethods(methods) { const acceptableMethods = [ "get", "post", "put", "patch", "delete", "head", "options" ]; return Object.keys(methods) .filter(method => acceptableMethods.includes(method)) .reduce((acc, p) => { return Object.assign(Object.assign({}, acc), { [p]: methods[p] }); }, {}); } createRestApi(key, restApi) { var _a, _b, _c; const resources = this.serverless.service.provider .compiledCloudFormationTemplate.Resources; const stage = restApi.Stage || this.serverless.service.provider.stage; const service = this.serverless.service.getServiceName(); const functionNames = []; const lambdaPermissions = {}; for (const path in restApi.Body.paths) { this.serverless.cli.log(`Connecting lambda for ${path} on ${key}`); const pathData = restApi.Body.paths[path]; const methods = this.filterMethods(restApi.Body.paths[path]); let headers = []; for (const method in methods) { const methodProps = methods[method]; (_c = (_b = (_a = methodProps.parameters) === null || _a === void 0 ? void 0 : _a.filter(x => x.in.toLowerCase() === "header")) === null || _b === void 0 ? void 0 : _b.map(x => x.name)) === null || _c === void 0 ? void 0 : _c.forEach(header => headers.push(header)); if (!methodProps["x-lambda-name"] && !methodProps["x-amazon-apigateway-integration"] && restApi.Lambda) { methodProps["x-lambda-name"] = restApi.Lambda; } const functionName = methodProps["x-lambda-name"]; if (!functionName) { this.serverless.cli.log(`No functionName defined for ${method} ${path}`); continue; } if (!functionNames.some(name => name === functionName)) { functionNames.push(functionName); } methodProps["x-amazon-apigateway-integration"] = { uri: { "Fn::Sub": `arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${${functionName}.Arn}/invocations` }, passthroughBehavior: "when_no_match", httpMethod: "POST", type: "aws_proxy", responses: {} }; if (!lambdaPermissions[functionName]) { lambdaPermissions[functionName] = []; } lambdaPermissions[functionName].push(`${method.toUpperCase()}${path}`); } headers = headers.reduce((current, next) => { if (!current.find(x => x.toLowerCase() === next.toLowerCase())) { current.push(next); } return current; }, []); const pathCors = pathData["cors"] || pathData["x-cors"]; if (pathCors && !pathData["options"]) { const origin = `'${pathCors.origin || "*"}'`; const allowedMethods = `'${(pathCors.methods || Object.keys(methods)) .join(",") .toUpperCase()}'`; const allowedHeaders = `'${(pathCors.headers || headers).join(",")}'`; pathData.options = { responses: { 200: { description: "Default response", content: {}, headers: { "Access-Control-Allow-Origin": { schema: { type: "string" } }, "Access-Control-Allow-Methods": { schema: { type: "string" } }, "Access-Control-Allow-Headers": { schema: { type: "string" } } } } }, "x-amazon-apigateway-integration": { responses: { default: { statusCode: 200, responseParameters: { "method.response.header.Access-Control-Allow-Origin": origin, "method.response.header.Access-Control-Allow-Methods": allowedMethods, "method.response.header.Access-Control-Allow-Headers": allowedHeaders } } }, passthroughBehavior: "when_no_match", requestTemplates: { "application/json": JSON.stringify({ statusCode: 200 }) }, type: "mock" } }; delete pathData.cors; delete pathData["x-cors"]; } } functionNames.forEach(functionName => { const paths = lambdaPermissions[functionName]; paths.forEach(path => { resources[`${key}${functionName}${path.replace(/[^A-Za-z0-9]/g, "")}Permission`] = this.createLambdaInvokePermission(functionName, key, path); }); }); // Create api this.createApiResources(resources, key, restApi, stage, service, functionNames); } createLambdaInvokePermission(functionName, key, path) { const fullPath = `\${${key}}/*/${path}`; this.serverless.cli.log(`Creating Lambda Invoke Permission for ${functionName} at ${fullPath}`); return { Type: "AWS::Lambda::Permission", Properties: { FunctionName: { "Fn::Sub": `\${${functionName}.Arn}` }, Action: "lambda:InvokeFunction", Principal: { "Fn::Sub": "apigateway.${AWS::URLSuffix}" }, SourceArn: { "Fn::Sub": `arn:aws:execute-api:\${AWS::Region}:\${AWS::AccountId}:${fullPath}` } } }; } createApiDeploymentName(key) { return `${key}Deployment`; } createApiResources(resources, key, restApi, stage, service, functionNames) { this.serverless.cli.log(`Creating API Resource ${key}`); resources[key] = this.createApi(restApi); resources[this.createApiDeploymentName(key)] = this.createDeployment(key, stage); resources[`${key}ServiceRole`] = this.createServiceRole(this.stackName, service, key, functionNames); } createServiceRole(stackName, service, key, functionNames) { this.serverless.cli.log(`Creating service role for ${key}`); return { Type: "AWS::IAM::Role", Properties: { RoleName: clipString(`${stackName}${service}${key}`, "APIRole"), AssumeRolePolicyDocument: { Version: "2012-10-17", Statement: [ { Effect: "Allow", Principal: { Service: "apigateway.amazonaws.com" }, Action: "sts:AssumeRole" } ] }, Policies: [ this.createLambdaExecutionPolicy(stackName, service, key, functionNames) ] } }; } createLambdaExecutionPolicy(stackName, service, key, functionNames) { this.serverless.cli.log(`Creating lambda execution policy for ${key}`); return { PolicyName: clipString(`${stackName}-${service}-${key}`, `APIPolicy`), PolicyDocument: { Version: "2012-10-17", Statement: functionNames.map(functionName => ({ Action: "lambda:InvokeFunction", Resource: { "Fn::Sub": `\${${functionName}.Arn}` }, Effect: "Allow" })) } }; } createDeployment(key, stage) { this.serverless.cli.log(`Creating API Deployment for ${key}`); return { Type: "AWS::ApiGateway::Deployment", DependsOn: [key], Properties: { RestApiId: { Ref: key }, StageName: stage } }; } createApi(restApi) { this.serverless.cli.log(`Creating RestApi ${restApi.Name}`); return { Type: "AWS::ApiGateway::RestApi", Properties: { Name: restApi.Name, Body: restApi.Body } }; } } exports.default = SwaggerApiPlugin; module.exports = SwaggerApiPlugin; //# sourceMappingURL=index.js.map