diff --git a/lib/deploy/events/apiGateway/compilationPipeline.js b/lib/deploy/events/apiGateway/compilationPipeline.js new file mode 100644 index 00000000..7e70dc9f --- /dev/null +++ b/lib/deploy/events/apiGateway/compilationPipeline.js @@ -0,0 +1,21 @@ +'use strict'; + +/** + * Runs an ordered list of compiler steps sequentially on a given context. + * Each step is a method name that exists on the context object. + * Steps are reorderable and conditionally includable without modifying callers. + */ +class CompilationPipeline { + constructor(steps) { + this.steps = steps; + } + + run(context) { + return this.steps.reduce( + (promise, step) => promise.then(() => context[step]()), + Promise.resolve(), + ); + } +} + +module.exports = CompilationPipeline; diff --git a/lib/deploy/events/apiGateway/compilationPipeline.test.js b/lib/deploy/events/apiGateway/compilationPipeline.test.js new file mode 100644 index 00000000..64ae67e6 --- /dev/null +++ b/lib/deploy/events/apiGateway/compilationPipeline.test.js @@ -0,0 +1,69 @@ +'use strict'; + +const expect = require('chai').expect; +const sinon = require('sinon'); +const CompilationPipeline = require('./compilationPipeline'); + +describe('CompilationPipeline', () => { + it('should call all steps in order', async () => { + const callOrder = []; + const context = { + stepA: sinon.stub().callsFake(() => { + callOrder.push('A'); + return Promise.resolve(); + }), + stepB: sinon.stub().callsFake(() => { + callOrder.push('B'); + return Promise.resolve(); + }), + stepC: sinon.stub().callsFake(() => { + callOrder.push('C'); + return Promise.resolve(); + }), + }; + + const pipeline = new CompilationPipeline(['stepA', 'stepB', 'stepC']); + await pipeline.run(context); + + expect(callOrder).to.deep.equal(['A', 'B', 'C']); + }); + + it('should call each step on the given context', async () => { + const context = { + step: sinon.stub().returns(Promise.resolve()), + }; + + const pipeline = new CompilationPipeline(['step']); + await pipeline.run(context); + + expect(context.step.callCount).to.equal(1); + }); + + it('should wait for an async step to resolve before calling the next', async () => { + let firstResolved = false; + const context = { + stepA: sinon.stub().returns( + new Promise((resolve) => { + setTimeout(() => { + firstResolved = true; + resolve(); + }, 10); + }), + ), + stepB: sinon.stub().callsFake(() => { + expect(firstResolved).to.equal(true); + return Promise.resolve(); + }), + }; + + const pipeline = new CompilationPipeline(['stepA', 'stepB']); + await pipeline.run(context); + + expect(context.stepB.callCount).to.equal(1); + }); + + it('should resolve immediately with an empty steps array', async () => { + const pipeline = new CompilationPipeline([]); + await pipeline.run({}); + }); +}); diff --git a/lib/index.js b/lib/index.js index 201f750e..800b1d52 100644 --- a/lib/index.js +++ b/lib/index.js @@ -23,6 +23,7 @@ const httpLambdaPermissions = require('./deploy/events/apiGateway/lambdaPermissi const httpDeployment = require('./deploy/events/apiGateway/deployment'); const httpRestApi = require('./deploy/events/apiGateway/restApi'); const httpInfo = require('./deploy/events/apiGateway/endpointInfo'); +const CompilationPipeline = require('./deploy/events/apiGateway/compilationPipeline'); const compileScheduledEvents = require('./deploy/events/schedule/compileScheduledEvents'); const compileCloudWatchEventEvents = require('./deploy/events/cloudWatchEvent/compileCloudWatchEventEvents'); const invoke = require('./invoke/invoke'); @@ -31,6 +32,21 @@ const naming = require('./naming'); const logger = require('./utils/logger'); +const apiGatewayPipeline = new CompilationPipeline([ + 'compileRestApi', + 'compileResources', + 'compileMethods', + 'compileRequestValidators', + 'compileAuthorizers', + 'compileHttpLambdaPermissions', + 'compileCors', + 'compileHttpIamRole', + 'compileDeployment', + 'compileApiKeys', + 'compileUsagePlan', + 'compileUsagePlanKeys', +]); + class ServerlessStepFunctions { constructor(serverless, options, v3Api) { this.serverless = serverless; @@ -135,19 +151,7 @@ class ServerlessStepFunctions { return BbPromise.resolve(); } - return BbPromise.bind(this) - .then(this.compileRestApi) - .then(this.compileResources) - .then(this.compileMethods) - .then(this.compileRequestValidators) - .then(this.compileAuthorizers) - .then(this.compileHttpLambdaPermissions) - .then(this.compileCors) - .then(this.compileHttpIamRole) - .then(this.compileDeployment) - .then(this.compileApiKeys) - .then(this.compileUsagePlan) - .then(this.compileUsagePlanKeys); + return apiGatewayPipeline.run(this); }).then(() => this.compileCloudWatchEventEvents()), 'after:deploy:deploy': () => BbPromise.bind(this) .then(this.getEndpointInfo)