Skip to content

Commit 045b2e5

Browse files
authored
Merge pull request #573 from paolorossig/master
feat: add request validator schema for http events
2 parents 28fa07b + 79fe069 commit 045b2e5

File tree

5 files changed

+342
-1
lines changed

5 files changed

+342
-1
lines changed

README.md

+83
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Serverless Framework v2.32.0 or later is required.
3838
- [Customizing response headers and templates](#customizing-response-headers-and-templates)
3939
- [Send request to an API](#send-request-to-an-api)
4040
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
41+
- [Request Schema Validators](#request-schema-validators)
4142
- [Schedule](#schedule)
4243
- [Enabling / Disabling](#enabling--disabling)
4344
- [Specify Name and Description](#specify-name-and-description)
@@ -933,6 +934,88 @@ Please note that those are the API keys names, not the actual values. Once you d
933934

934935
Clients connecting to this Rest API will then need to set any of these API keys values in the x-api-key header of their request. This is only necessary for functions where the private property is set to true.
935936

937+
#### Request Schema Validators
938+
939+
To use [request schema validation](https://serverless.com/framework/docs/providers/aws/events/apigateway/#request-schema-validators) with API gateway, add the [JSON Schema](https://json-schema.org/) for your content type. Since JSON Schema is represented in JSON, it's easier to include it from a file.
940+
941+
```yaml
942+
stepFunctions:
943+
stateMachines:
944+
create:
945+
events:
946+
- http:
947+
path: posts/create
948+
method: post
949+
request:
950+
schemas:
951+
application/json: ${file(create_request.json)}
952+
```
953+
954+
In addition, you can also customize created model with name and description properties.
955+
956+
```yaml
957+
stepFunctions:
958+
stateMachines:
959+
create:
960+
events:
961+
- http:
962+
path: posts/create
963+
method: post
964+
request:
965+
schemas:
966+
application/json:
967+
schema: ${file(create_request.json)}
968+
name: PostCreateModel
969+
description: 'Validation model for Creating Posts'
970+
```
971+
972+
To reuse the same model across different events, you can define global models on provider level. In order to define global model you need to add its configuration to `provider.apiGateway.request.schemas`. After defining a global model, you can use it in the event by referencing it by the key. Provider models are created for application/json content type.
973+
974+
```yaml
975+
provider:
976+
...
977+
apiGateway:
978+
request:
979+
schemas:
980+
post-create-model:
981+
name: PostCreateModel
982+
schema: ${file(api_schema/post_add_schema.json)}
983+
description: "A Model validation for adding posts"
984+
985+
stepFunctions:
986+
stateMachines:
987+
create:
988+
events:
989+
- http:
990+
path: posts/create
991+
method: post
992+
request:
993+
schemas:
994+
application/json: post-create-model
995+
```
996+
997+
A sample schema contained in `create_request.json` might look something like this:
998+
999+
```json
1000+
{
1001+
"definitions": {},
1002+
"$schema": "http://json-schema.org/draft-04/schema#",
1003+
"type": "object",
1004+
"title": "The Root Schema",
1005+
"required": ["username"],
1006+
"properties": {
1007+
"username": {
1008+
"type": "string",
1009+
"title": "The Foo Schema",
1010+
"default": "",
1011+
"pattern": "^[a-zA-Z0-9]+$"
1012+
}
1013+
}
1014+
}
1015+
```
1016+
1017+
**NOTE:** schema validators are only applied to content types you specify. Other content types are not blocked. Currently, API Gateway [supports](https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html) JSON Schema draft-04.
1018+
9361019
### Schedule
9371020

9381021
The following config will attach a schedule event and causes the stateMachine `crawl` to be called every 2 hours. The configuration allows you to attach multiple schedules to the same stateMachine. You can either use the `rate` or `cron` syntax. Take a look at the [AWS schedule syntax documentation](http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html) for more details.
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)

0 commit comments

Comments
 (0)