diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index fa1b312cd..8d29725c9 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -24,6 +24,7 @@ layout: Doc - [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api) - [Configuring endpoint types](#configuring-endpoint-types) - [Request Parameters](#request-parameters) + - [Request Schema Validation](#request-schema-validation) - [Setting source of API key for metering requests](#setting-source-of-api-key-for-metering-requests) - [Lambda Integration](#lambda-integration) - [Example "LAMBDA" event (before customization)](#example-lambda-event-before-customization) @@ -649,6 +650,50 @@ functions: id: true ``` +### Request Schema Validators + +To use request schema validation 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. + +```yml +functions: + create: + handler: posts.create + events: + - http: + path: posts/create + method: post + request: + schema: + application/json: ${file(create_request.json)} +``` + +A sample schema contained in `create_request.json` might look something like this: + +```json +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/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. + ### Setting source of API key for metering requests API Gateway provide a feature for metering your API's requests and you can choice [the source of key](https://docs.aws.amazon.com/apigateway/api-reference/resource/rest-api/#apiKeySource) which is used for metering. If you want to acquire that key from the request's X-API-Key header, set option like this: diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 7f78238c3..69d3f344d 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -242,6 +242,13 @@ module.exports = { getMethodLogicalId(resourceId, methodName) { return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`; }, + getValidatorLogicalId(resourceId, methodName) { + return `${this.getMethodLogicalId(resourceId, methodName)}Validator`; + }, + getModelLogicalId(resourceId, methodName, contentType) { + return `${this.getMethodLogicalId(resourceId, methodName)}${_.startCase( + contentType).replace(' ', '')}Model`; + }, getApiKeyLogicalId(apiKeyNumber, apiKeyName) { if (apiKeyName) { return `ApiGatewayApiKey${this.normalizeName(apiKeyName)}${apiKeyNumber}`; diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js index da0557213..3eee2e787 100644 --- a/lib/plugins/aws/lib/naming.test.js +++ b/lib/plugins/aws/lib/naming.test.js @@ -390,6 +390,22 @@ describe('#naming()', () => { }); }); + describe('#getValidatorLogicalId()', () => { + it('', () => { + expect(sdk.naming.getValidatorLogicalId( + 'ResourceId', 'get' + )).to.equal('ApiGatewayMethodResourceIdGetValidator'); + }); + }); + + describe('#getModelLogicalId()', () => { + it('', () => { + expect(sdk.naming.getModelLogicalId( + 'ResourceId', 'get', 'application/json' + )).to.equal('ApiGatewayMethodResourceIdGetApplicationJsonModel'); + }); + }); + describe('#getApiKeyLogicalId(keyIndex)', () => { it('should produce the given index with ApiGatewayApiKey as a prefix', () => { expect(sdk.naming.getApiKeyLogicalId(1)).to.equal('ApiGatewayApiKey1'); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js index 77fd557c1..0d12adb4d 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js @@ -30,6 +30,8 @@ module.exports = { const methodLogicalId = this.provider.naming .getMethodLogicalId(resourceName, event.http.method); + const validatorLogicalId = this.provider.naming + .getValidatorLogicalId(resourceName, event.http.method); const lambdaLogicalId = this.provider.naming .getLambdaLogicalId(event.functionName); @@ -67,6 +69,42 @@ module.exports = { this.apiGatewayMethodLogicalIds.push(methodLogicalId); + if (event.http.request && event.http.request.schema) { + for (const requestSchema of _.entries(event.http.request.schema)) { + const contentType = requestSchema[0]; + const schema = requestSchema[1]; + + const modelLogicalId = this.provider.naming + .getModelLogicalId(resourceName, event.http.method, contentType); + + template.Properties.RequestValidatorId = { Ref: validatorLogicalId }; + template.Properties.RequestModels = { [contentType]: { Ref: modelLogicalId } }; + + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [modelLogicalId]: { + Type: 'AWS::ApiGateway::Model', + Properties: { + RestApiId: { + Ref: this.provider.naming.getRestApiLogicalId(), + }, + ContentType: contentType, + Schema: schema, + }, + }, + [validatorLogicalId]: { + Type: 'AWS::ApiGateway::RequestValidator', + Properties: { + RestApiId: { + Ref: this.provider.naming.getRestApiLogicalId(), + }, + ValidateRequestBody: true, + ValidateRequestParameters: true, + }, + }, + }); + } + } + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [methodLogicalId]: template, }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js index 705e477ce..78ef18d37 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js @@ -57,6 +57,54 @@ describe('#compileMethods()', () => { }; }); + it('should have request validators/models defined when they are set', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + integration: 'AWS', + request: { schema: { 'application/json': { foo: 'bar' } } }, + }, + }, + ]; + return awsCompileApigEvents.compileMethods().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePostValidator + ).to.deep.equal({ + Type: 'AWS::ApiGateway::RequestValidator', + Properties: { + RestApiId: { Ref: 'ApiGatewayRestApi' }, + ValidateRequestBody: true, + ValidateRequestParameters: true, + }, + }); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePostApplicationJsonModel + ).to.deep.equal({ + Type: 'AWS::ApiGateway::Model', + Properties: { + RestApiId: { Ref: 'ApiGatewayRestApi' }, + ContentType: 'application/json', + Schema: { foo: 'bar' }, + }, + }); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePost.Properties.RequestModels + ).to.deep.equal({ + 'application/json': { Ref: 'ApiGatewayMethodUsersCreatePostApplicationJsonModel' }, + }); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePost.Properties.RequestValidatorId + ).to.deep.equal({ Ref: 'ApiGatewayMethodUsersCreatePostValidator' }); + }); + }); + it('should have request parameters defined when they are set', () => { awsCompileApigEvents.validated.events = [ { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js index f0b724b68..b435c8fc3 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -98,7 +98,9 @@ module.exports = { if (http.request) { const keys = Object.keys(http.request); const allowedKeys = - http.integration === 'AWS_PROXY' ? ['parameters'] : ['parameters', 'uri']; + http.integration === 'AWS_PROXY' + ? ['parameters', 'schema'] + : ['parameters', 'uri', 'schema']; if (!_.isEmpty(_.difference(keys, allowedKeys))) { const requestWarningMessage = [