Skip to content

Commit a389613

Browse files
committed
feat: add request validator schema for http events
1 parent 4cc436c commit a389613

File tree

4 files changed

+259
-1
lines changed

4 files changed

+259
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use strict';
2+
3+
const BbPromise = require('bluebird');
4+
const _ = require('lodash');
5+
6+
module.exports = {
7+
compileRequestValidators() {
8+
const apiGatewayConfig = this.serverless.service.provider.apiGateway || {};
9+
10+
this.pluginhttpValidated.events.forEach((event) => {
11+
const resourceName = this.getResourceName(event.http.path);
12+
const methodLogicalId = this.provider.naming
13+
.getMethodLogicalId(resourceName, event.http.method);
14+
const template = this.serverless.service.provider
15+
.compiledCloudFormationTemplate.Resources[methodLogicalId];
16+
let validatorLogicalId;
17+
18+
if (event.http.request && event.http.request.schemas) {
19+
for (const [contentType, schemaConfig] of Object.entries(event.http.request.schemas)) {
20+
let modelLogicalId;
21+
22+
const referencedDefinitionFromProvider = !_.isObject(schemaConfig) && _.get(apiGatewayConfig, `request.schemas.${schemaConfig}`);
23+
24+
if (referencedDefinitionFromProvider) {
25+
modelLogicalId = this.createProviderModel(
26+
schemaConfig,
27+
apiGatewayConfig.request.schemas[schemaConfig],
28+
);
29+
} else {
30+
// In this situation, we have two options - schema is defined as
31+
// string that does not reference model from provider or as an object
32+
let modelName;
33+
let description;
34+
let definition;
35+
36+
if (_.isObject(schemaConfig)) {
37+
if (schemaConfig.schema) {
38+
// In this case, schema is defined as an object with explicit properties
39+
modelName = schemaConfig.name;
40+
description = schemaConfig.description;
41+
definition = schemaConfig.schema;
42+
} else {
43+
// In this case, schema is defined as an implicit object that
44+
// stores whole schema definition
45+
definition = schemaConfig;
46+
}
47+
} else {
48+
// In this case, schema is defined as an implicit string
49+
definition = schemaConfig;
50+
}
51+
52+
modelLogicalId = this.provider.naming.getEndpointModelLogicalId(
53+
resourceName,
54+
event.http.method,
55+
contentType,
56+
);
57+
58+
Object.assign(
59+
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
60+
{
61+
[modelLogicalId]: {
62+
Type: 'AWS::ApiGateway::Model',
63+
Properties: {
64+
RestApiId: this.provider.getApiGatewayRestApiId(),
65+
ContentType: contentType,
66+
Schema: definition,
67+
Name: modelName,
68+
Description: description,
69+
},
70+
},
71+
},
72+
);
73+
}
74+
75+
if (!validatorLogicalId) {
76+
const requestValidator = this.createRequestValidator();
77+
validatorLogicalId = requestValidator.validatorLogicalId;
78+
}
79+
80+
template.Properties.RequestValidatorId = { Ref: validatorLogicalId };
81+
template.Properties.RequestModels = template.Properties.RequestModels || {};
82+
template.Properties.RequestModels[contentType] = { Ref: modelLogicalId };
83+
}
84+
}
85+
});
86+
87+
return BbPromise.resolve();
88+
},
89+
90+
createProviderModel(schemaId, schemaConfig) {
91+
let modelName;
92+
let description;
93+
let definition;
94+
95+
// If schema is not defined this will try to map resourceDefinition as the schema
96+
if (!schemaConfig.schema) {
97+
definition = schemaConfig;
98+
} else {
99+
definition = schemaConfig.schema;
100+
}
101+
102+
const modelLogicalId = this.provider.naming.getModelLogicalId(schemaId);
103+
104+
if (schemaConfig.name) {
105+
modelName = schemaConfig.name;
106+
}
107+
108+
if (schemaConfig.description) {
109+
description = schemaConfig.description;
110+
}
111+
112+
Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
113+
[modelLogicalId]: {
114+
Type: 'AWS::ApiGateway::Model',
115+
Properties: {
116+
RestApiId: this.provider.getApiGatewayRestApiId(),
117+
Schema: definition,
118+
ContentType: 'application/json',
119+
},
120+
},
121+
});
122+
123+
if (modelName) {
124+
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
125+
modelLogicalId
126+
].Properties.Name = modelName;
127+
}
128+
129+
if (description) {
130+
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
131+
modelLogicalId
132+
].Properties.Description = description;
133+
}
134+
135+
return modelLogicalId;
136+
},
137+
138+
createRequestValidator() {
139+
const validatorLogicalId = this.provider.naming.getValidatorLogicalId();
140+
const validatorName = `${
141+
this.serverless.service.service
142+
}-${this.provider.getStage()} | Validate request body and querystring parameters`;
143+
144+
this.serverless.service.provider.compiledCloudFormationTemplate
145+
.Resources[validatorLogicalId] = {
146+
Type: 'AWS::ApiGateway::RequestValidator',
147+
Properties: {
148+
RestApiId: this.provider.getApiGatewayRestApiId(),
149+
ValidateRequestBody: true,
150+
ValidateRequestParameters: true,
151+
Name: validatorName,
152+
},
153+
};
154+
155+
return {
156+
validatorLogicalId,
157+
validatorName,
158+
};
159+
},
160+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
const expect = require('chai').expect;
4+
const Serverless = require('serverless/lib/Serverless');
5+
const AwsProvider = require('serverless/lib/plugins/aws/provider');
6+
const ServerlessStepFunctions = require('../../../index');
7+
8+
describe('#requestValidator()', () => {
9+
let serverless;
10+
let serverlessStepFunctions;
11+
12+
beforeEach(() => {
13+
const options = {
14+
stage: 'dev',
15+
region: 'us-east-1',
16+
};
17+
18+
serverless = new Serverless();
19+
serverless.service.service = 'step-functions';
20+
serverless.setProvider('aws', new AwsProvider(serverless));
21+
serverless.service.provider.compiledCloudFormationTemplate = {
22+
Resources: {
23+
ApiGatewayMethodFirstPost: {
24+
Properties: {},
25+
},
26+
},
27+
};
28+
serverless.configSchemaHandler = {
29+
// eslint-disable-next-line no-unused-vars
30+
defineTopLevelProperty: (propertyName, propertySchema) => {},
31+
};
32+
33+
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
34+
serverlessStepFunctions.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
35+
serverlessStepFunctions.apiGatewayResourceNames = {
36+
'foo/bar1': 'apiGatewayResourceNamesFirst',
37+
'foo/bar2': 'apiGatewayResourceNamesSecond',
38+
};
39+
serverlessStepFunctions.pluginhttpValidated = {
40+
events: [
41+
{
42+
stateMachineName: 'first',
43+
http: {
44+
path: 'foo/bar1',
45+
method: 'post',
46+
request: {
47+
schemas: {
48+
'application/json': {
49+
name: 'StartExecutionSchema',
50+
schema: {},
51+
},
52+
},
53+
},
54+
},
55+
},
56+
{
57+
stateMachineName: 'second',
58+
http: {
59+
path: 'foo/bar2',
60+
method: 'post',
61+
private: true,
62+
},
63+
},
64+
],
65+
};
66+
serverlessStepFunctions.apiGatewayResources = {
67+
'foo/bar1': {
68+
name: 'First',
69+
resourceLogicalId: 'ApiGatewayResourceFirst',
70+
},
71+
72+
'foo/bar2': {
73+
name: 'Second',
74+
resourceLogicalId: 'ApiGatewayResourceSecond',
75+
},
76+
};
77+
});
78+
79+
describe('#compileRequestValidators()', () => {
80+
it('should process schema from http event request schemas', () => serverlessStepFunctions
81+
.compileRequestValidators().then(() => {
82+
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
83+
.Resources)
84+
.to.have.property('ApiGatewayMethodFirstPostApplicationJsonModel');
85+
86+
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
87+
.Resources)
88+
.to.have.property('ApiGatewayStepfunctionsRequestValidator');
89+
}));
90+
});
91+
});

lib/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const compileNotifications = require('./deploy/stepFunctions/compileNotification
1212
const httpValidate = require('./deploy/events/apiGateway/validate');
1313
const httpResources = require('./deploy/events/apiGateway/resources');
1414
const httpMethods = require('./deploy/events/apiGateway/methods');
15+
const httpRequestValidators = require('./deploy/events/apiGateway/requestValidators');
1516

1617
// eslint-disable-next-line max-len
1718
const httpCors = require('./deploy/events/apiGateway/cors');
@@ -55,6 +56,7 @@ class ServerlessStepFunctions {
5556
httpValidate,
5657
httpResources,
5758
httpMethods,
59+
httpRequestValidators,
5860
httpAuthorizers,
5961
httpLambdaPermissions,
6062
httpCors,
@@ -138,6 +140,7 @@ class ServerlessStepFunctions {
138140
.then(this.compileRestApi)
139141
.then(this.compileResources)
140142
.then(this.compileMethods)
143+
.then(this.compileRequestValidators)
141144
.then(this.compileAuthorizers)
142145
.then(this.compileHttpLambdaPermissions)
143146
.then(this.compileCors)

lib/index.test.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ describe('#index', () => {
176176
.stub(serverlessStepFunctions, 'compileResources').returns(BbPromise.resolve());
177177
const compileMethodsStub = sinon
178178
.stub(serverlessStepFunctions, 'compileMethods').returns(BbPromise.resolve());
179+
const compileRequestValidatorsStub = sinon
180+
.stub(serverlessStepFunctions, 'compileRequestValidators').returns(BbPromise.resolve());
179181
const compileAuthorizersStub = sinon
180182
.stub(serverlessStepFunctions, 'compileAuthorizers').returns(BbPromise.resolve());
181183
const compileCorsStub = sinon
@@ -199,7 +201,8 @@ describe('#index', () => {
199201
expect(compileResourcesStub.calledAfter(compileRestApiStub)).to.be.equal(true);
200202
expect(compileMethodsStub.calledAfter(compileResourcesStub)).to.be.equal(true);
201203
expect(compileAuthorizersStub.calledAfter(compileResourcesStub)).to.be.equal(true);
202-
expect(compileCorsStub.calledAfter(compileMethodsStub)).to.be.equal(true);
204+
expect(compileRequestValidatorsStub.calledAfter(compileMethodsStub)).to.be.equal(true);
205+
expect(compileCorsStub.calledAfter(compileRequestValidatorsStub)).to.be.equal(true);
203206
expect(compileHttpIamRoleStub.calledAfter(compileCorsStub)).to.be.equal(true);
204207
expect(compileDeploymentStub.calledAfter(compileHttpIamRoleStub)).to.be.equal(true);
205208
expect(compileApiKeysStub.calledAfter(compileDeploymentStub)).to.be.equal(true);
@@ -212,6 +215,7 @@ describe('#index', () => {
212215
serverlessStepFunctions.compileRestApi.restore();
213216
serverlessStepFunctions.compileResources.restore();
214217
serverlessStepFunctions.compileMethods.restore();
218+
serverlessStepFunctions.compileRequestValidators.restore();
215219
serverlessStepFunctions.compileAuthorizers.restore();
216220
serverlessStepFunctions.compileHttpIamRole.restore();
217221
serverlessStepFunctions.compileDeployment.restore();

0 commit comments

Comments
 (0)