Skip to content

Commit

Permalink
Merge pull request #573 from paolorossig/master
Browse files Browse the repository at this point in the history
feat: add request validator schema for http events
  • Loading branch information
horike37 authored Sep 13, 2023
2 parents 28fa07b + 79fe069 commit 045b2e5
Show file tree
Hide file tree
Showing 5 changed files with 342 additions and 1 deletion.
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Serverless Framework v2.32.0 or later is required.
- [Customizing response headers and templates](#customizing-response-headers-and-templates)
- [Send request to an API](#send-request-to-an-api)
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
- [Request Schema Validators](#request-schema-validators)
- [Schedule](#schedule)
- [Enabling / Disabling](#enabling--disabling)
- [Specify Name and Description](#specify-name-and-description)
Expand Down Expand Up @@ -933,6 +934,88 @@ Please note that those are the API keys names, not the actual values. Once you d

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.

#### Request Schema Validators

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.

```yaml
stepFunctions:
stateMachines:
create:
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json: ${file(create_request.json)}
```

In addition, you can also customize created model with name and description properties.

```yaml
stepFunctions:
stateMachines:
create:
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json:
schema: ${file(create_request.json)}
name: PostCreateModel
description: 'Validation model for Creating Posts'
```

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.

```yaml
provider:
...
apiGateway:
request:
schemas:
post-create-model:
name: PostCreateModel
schema: ${file(api_schema/post_add_schema.json)}
description: "A Model validation for adding posts"
stepFunctions:
stateMachines:
create:
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json: post-create-model
```

A sample schema contained in `create_request.json` might look something like this:

```json
{
"definitions": {},
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "The Root Schema",
"required": ["username"],
"properties": {
"username": {
"type": "string",
"title": "The Foo Schema",
"default": "",
"pattern": "^[a-zA-Z0-9]+$"
}
}
}
```

**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.

### Schedule

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.
Expand Down
160 changes: 160 additions & 0 deletions lib/deploy/events/apiGateway/requestValidators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
'use strict';

const BbPromise = require('bluebird');
const _ = require('lodash');

module.exports = {
compileRequestValidators() {
const apiGatewayConfig = this.serverless.service.provider.apiGateway || {};

this.pluginhttpValidated.events.forEach((event) => {
const resourceName = this.getResourceName(event.http.path);
const methodLogicalId = this.provider.naming
.getMethodLogicalId(resourceName, event.http.method);
const template = this.serverless.service.provider
.compiledCloudFormationTemplate.Resources[methodLogicalId];
let validatorLogicalId;

if (event.http.request && event.http.request.schemas) {
for (const [contentType, schemaConfig] of Object.entries(event.http.request.schemas)) {
let modelLogicalId;

const referencedDefinitionFromProvider = !_.isObject(schemaConfig) && _.get(apiGatewayConfig, `request.schemas.${schemaConfig}`);

if (referencedDefinitionFromProvider) {
modelLogicalId = this.createProviderModel(
schemaConfig,
apiGatewayConfig.request.schemas[schemaConfig],
);
} else {
// In this situation, we have two options - schema is defined as
// string that does not reference model from provider or as an object
let modelName;
let description;
let definition;

if (_.isObject(schemaConfig)) {
if (schemaConfig.schema) {
// In this case, schema is defined as an object with explicit properties
modelName = schemaConfig.name;
description = schemaConfig.description;
definition = schemaConfig.schema;
} else {
// In this case, schema is defined as an implicit object that
// stores whole schema definition
definition = schemaConfig;
}
} else {
// In this case, schema is defined as an implicit string
definition = schemaConfig;
}

modelLogicalId = this.provider.naming.getEndpointModelLogicalId(
resourceName,
event.http.method,
contentType,
);

Object.assign(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
{
[modelLogicalId]: {
Type: 'AWS::ApiGateway::Model',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
ContentType: contentType,
Schema: definition,
Name: modelName,
Description: description,
},
},
},
);
}

if (!validatorLogicalId) {
const requestValidator = this.createRequestValidator();
validatorLogicalId = requestValidator.validatorLogicalId;
}

template.Properties.RequestValidatorId = { Ref: validatorLogicalId };
template.Properties.RequestModels = template.Properties.RequestModels || {};
template.Properties.RequestModels[contentType] = { Ref: modelLogicalId };
}
}
});

return BbPromise.resolve();
},

createProviderModel(schemaId, schemaConfig) {
let modelName;
let description;
let definition;

// If schema is not defined this will try to map resourceDefinition as the schema
if (!schemaConfig.schema) {
definition = schemaConfig;
} else {
definition = schemaConfig.schema;
}

const modelLogicalId = this.provider.naming.getModelLogicalId(schemaId);

if (schemaConfig.name) {
modelName = schemaConfig.name;
}

if (schemaConfig.description) {
description = schemaConfig.description;
}

Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[modelLogicalId]: {
Type: 'AWS::ApiGateway::Model',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
Schema: definition,
ContentType: 'application/json',
},
},
});

if (modelName) {
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
modelLogicalId
].Properties.Name = modelName;
}

if (description) {
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
modelLogicalId
].Properties.Description = description;
}

return modelLogicalId;
},

createRequestValidator() {
const validatorLogicalId = this.provider.naming.getValidatorLogicalId();
const validatorName = `${
this.serverless.service.service
}-${this.provider.getStage()} | Validate request body and querystring parameters`;

this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[validatorLogicalId] = {
Type: 'AWS::ApiGateway::RequestValidator',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
ValidateRequestBody: true,
ValidateRequestParameters: true,
Name: validatorName,
},
};

return {
validatorLogicalId,
validatorName,
};
},
};
91 changes: 91 additions & 0 deletions lib/deploy/events/apiGateway/requestValidators.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';

const expect = require('chai').expect;
const Serverless = require('serverless/lib/Serverless');
const AwsProvider = require('serverless/lib/plugins/aws/provider');
const ServerlessStepFunctions = require('../../../index');

describe('#requestValidator()', () => {
let serverless;
let serverlessStepFunctions;

beforeEach(() => {
const options = {
stage: 'dev',
region: 'us-east-1',
};

serverless = new Serverless();
serverless.service.service = 'step-functions';
serverless.setProvider('aws', new AwsProvider(serverless));
serverless.service.provider.compiledCloudFormationTemplate = {
Resources: {
ApiGatewayMethodFirstPost: {
Properties: {},
},
},
};
serverless.configSchemaHandler = {
// eslint-disable-next-line no-unused-vars
defineTopLevelProperty: (propertyName, propertySchema) => {},
};

serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
serverlessStepFunctions.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
serverlessStepFunctions.apiGatewayResourceNames = {
'foo/bar1': 'apiGatewayResourceNamesFirst',
'foo/bar2': 'apiGatewayResourceNamesSecond',
};
serverlessStepFunctions.pluginhttpValidated = {
events: [
{
stateMachineName: 'first',
http: {
path: 'foo/bar1',
method: 'post',
request: {
schemas: {
'application/json': {
name: 'StartExecutionSchema',
schema: {},
},
},
},
},
},
{
stateMachineName: 'second',
http: {
path: 'foo/bar2',
method: 'post',
private: true,
},
},
],
};
serverlessStepFunctions.apiGatewayResources = {
'foo/bar1': {
name: 'First',
resourceLogicalId: 'ApiGatewayResourceFirst',
},

'foo/bar2': {
name: 'Second',
resourceLogicalId: 'ApiGatewayResourceSecond',
},
};
});

describe('#compileRequestValidators()', () => {
it('should process schema from http event request schemas', () => serverlessStepFunctions
.compileRequestValidators().then(() => {
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources)
.to.have.property('ApiGatewayMethodFirstPostApplicationJsonModel');

expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources)
.to.have.property('ApiGatewayStepfunctionsRequestValidator');
}));
});
});
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const compileNotifications = require('./deploy/stepFunctions/compileNotification
const httpValidate = require('./deploy/events/apiGateway/validate');
const httpResources = require('./deploy/events/apiGateway/resources');
const httpMethods = require('./deploy/events/apiGateway/methods');
const httpRequestValidators = require('./deploy/events/apiGateway/requestValidators');

// eslint-disable-next-line max-len
const httpCors = require('./deploy/events/apiGateway/cors');
Expand Down Expand Up @@ -55,6 +56,7 @@ class ServerlessStepFunctions {
httpValidate,
httpResources,
httpMethods,
httpRequestValidators,
httpAuthorizers,
httpLambdaPermissions,
httpCors,
Expand Down Expand Up @@ -138,6 +140,7 @@ class ServerlessStepFunctions {
.then(this.compileRestApi)
.then(this.compileResources)
.then(this.compileMethods)
.then(this.compileRequestValidators)
.then(this.compileAuthorizers)
.then(this.compileHttpLambdaPermissions)
.then(this.compileCors)
Expand Down
Loading

0 comments on commit 045b2e5

Please sign in to comment.