From 26bd1d39b4bce049bcffd9bb917345b49a4d188c Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Mon, 25 Mar 2019 17:35:12 -0400 Subject: [PATCH 1/9] Initial implemenation of request body schemas --- .../events/apiGateway/lib/method/index.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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..3e2b7b1ec 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 @@ -67,6 +67,36 @@ module.exports = { this.apiGatewayMethodLogicalIds.push(methodLogicalId); + if (event.http.schema) { + template.Properties.RequestValidatorId = { Ref: `${methodLogicalId}Validator` }; + template.Properties.RequestModels = { + 'application/json': { Ref: `${methodLogicalId}Model` }, + }; + + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [`${methodLogicalId}Model`]: { + Type: 'AWS::ApiGateway::Model', + Properties: { + RestApiId: { + Ref: 'ApiGatewayRestApi', + }, + ContentType: 'application/json', + Schema: event.http.schema, + }, + }, + [`${methodLogicalId}Validator`]: { + Type: 'AWS::ApiGateway::RequestValidator', + Properties: { + RestApiId: { + Ref: 'ApiGatewayRestApi', + }, + ValidateRequestBody: true, + ValidateRequestParameters: false, + }, + }, + }); + } + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [methodLogicalId]: template, }); From 84c5124b0be1bc0ed541c4fdc5d36cf11e6908fd Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Fri, 29 Mar 2019 09:48:52 -0400 Subject: [PATCH 2/9] put schema under request key and include content type in yaml --- .../events/apiGateway/lib/method/index.js | 50 ++++++++++--------- .../compile/events/apiGateway/lib/validate.js | 4 +- 2 files changed, 30 insertions(+), 24 deletions(-) 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 3e2b7b1ec..080231c02 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 @@ -67,34 +67,38 @@ module.exports = { this.apiGatewayMethodLogicalIds.push(methodLogicalId); - if (event.http.schema) { - template.Properties.RequestValidatorId = { Ref: `${methodLogicalId}Validator` }; - template.Properties.RequestModels = { - 'application/json': { Ref: `${methodLogicalId}Model` }, - }; + if (event.http.request && event.http.request.schema) { + for (const [contentType, schema] of _.entries(event.http.request.schema)) { + template.Properties.RequestValidatorId = { Ref: `${methodLogicalId}Validator` }; + template.Properties.RequestModels = { + [contentType]: { + Ref: `${methodLogicalId}${_.startCase(contentType).replace(' ', '')}Model`, + }, + }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { - [`${methodLogicalId}Model`]: { - Type: 'AWS::ApiGateway::Model', - Properties: { - RestApiId: { - Ref: 'ApiGatewayRestApi', + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [`${methodLogicalId}${_.startCase(contentType).replace(' ', '')}Model`]: { + Type: 'AWS::ApiGateway::Model', + Properties: { + RestApiId: { + Ref: 'ApiGatewayRestApi', + }, + ContentType: contentType, + Schema: schema, }, - ContentType: 'application/json', - Schema: event.http.schema, }, - }, - [`${methodLogicalId}Validator`]: { - Type: 'AWS::ApiGateway::RequestValidator', - Properties: { - RestApiId: { - Ref: 'ApiGatewayRestApi', + [`${methodLogicalId}Validator`]: { + Type: 'AWS::ApiGateway::RequestValidator', + Properties: { + RestApiId: { + Ref: 'ApiGatewayRestApi', + }, + ValidateRequestBody: true, + ValidateRequestParameters: true, }, - ValidateRequestBody: true, - ValidateRequestParameters: false, }, - }, - }); + }); + } } _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { 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 = [ From 0d98a986fdf6b3a4a5fdef87439bb52043bf1beb Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Fri, 29 Mar 2019 10:37:02 -0400 Subject: [PATCH 3/9] old versions of node can't destructure --- .../aws/package/compile/events/apiGateway/lib/method/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 080231c02..dd507f4a1 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 @@ -68,7 +68,9 @@ module.exports = { this.apiGatewayMethodLogicalIds.push(methodLogicalId); if (event.http.request && event.http.request.schema) { - for (const [contentType, schema] of _.entries(event.http.request.schema)) { + for (const requestSchema of _.entries(event.http.request.schema)) { + const contentType = requestSchema[0]; + const schema = requestSchema[1]; template.Properties.RequestValidatorId = { Ref: `${methodLogicalId}Validator` }; template.Properties.RequestModels = { [contentType]: { From 618094d5bb1f117374ec2a1866543f2804cfab5c Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Wed, 17 Apr 2019 10:24:05 -0400 Subject: [PATCH 4/9] request validator test covg --- .../apiGateway/lib/method/index.test.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) 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..0f16b78d0 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,65 @@ 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'} + }, + }, + response: { + statusCodes: { + 200: { + pattern: '', + }, + }, + }, + }, + }, + ]; + 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 = [ { From 0c214bc8da77986672b0f41c70f84d6e23e51d50 Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Wed, 17 Apr 2019 10:33:23 -0400 Subject: [PATCH 5/9] add to & use naming methods --- lib/plugins/aws/lib/naming.js | 7 ++++++ .../events/apiGateway/lib/method/index.js | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 7f78238c3..8b15ef03f 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 `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}Validator`; + }, + getModelLogicalId(resourceId, methodName, contentType) { + return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}${_.startCase( + contentType).replace(' ', '')}Model`; + }, getApiKeyLogicalId(apiKeyNumber, apiKeyName) { if (apiKeyName) { return `ApiGatewayApiKey${this.normalizeName(apiKeyName)}${apiKeyNumber}`; 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 dd507f4a1..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); @@ -71,29 +73,29 @@ module.exports = { for (const requestSchema of _.entries(event.http.request.schema)) { const contentType = requestSchema[0]; const schema = requestSchema[1]; - template.Properties.RequestValidatorId = { Ref: `${methodLogicalId}Validator` }; - template.Properties.RequestModels = { - [contentType]: { - Ref: `${methodLogicalId}${_.startCase(contentType).replace(' ', '')}Model`, - }, - }; + + 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, { - [`${methodLogicalId}${_.startCase(contentType).replace(' ', '')}Model`]: { + [modelLogicalId]: { Type: 'AWS::ApiGateway::Model', Properties: { RestApiId: { - Ref: 'ApiGatewayRestApi', + Ref: this.provider.naming.getRestApiLogicalId(), }, ContentType: contentType, Schema: schema, }, }, - [`${methodLogicalId}Validator`]: { + [validatorLogicalId]: { Type: 'AWS::ApiGateway::RequestValidator', Properties: { RestApiId: { - Ref: 'ApiGatewayRestApi', + Ref: this.provider.naming.getRestApiLogicalId(), }, ValidateRequestBody: true, ValidateRequestParameters: true, From 1efa53e78ec7937f240dc7caaa5151a908a44f5c Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Wed, 17 Apr 2019 10:45:25 -0400 Subject: [PATCH 6/9] lint --- .../apiGateway/lib/method/index.test.js | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) 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 0f16b78d0..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 @@ -65,18 +65,7 @@ describe('#compileMethods()', () => { path: 'users/create', method: 'post', integration: 'AWS', - request: { - schema: { - 'application/json': {foo: 'bar'} - }, - }, - response: { - statusCodes: { - 200: { - pattern: '', - }, - }, - }, + request: { schema: { 'application/json': { foo: 'bar' } } }, }, }, ]; @@ -87,7 +76,7 @@ describe('#compileMethods()', () => { ).to.deep.equal({ Type: 'AWS::ApiGateway::RequestValidator', Properties: { - RestApiId: { Ref: "ApiGatewayRestApi" }, + RestApiId: { Ref: 'ApiGatewayRestApi' }, ValidateRequestBody: true, ValidateRequestParameters: true, }, @@ -98,9 +87,9 @@ describe('#compileMethods()', () => { ).to.deep.equal({ Type: 'AWS::ApiGateway::Model', Properties: { - RestApiId: { Ref: "ApiGatewayRestApi" }, + RestApiId: { Ref: 'ApiGatewayRestApi' }, ContentType: 'application/json', - Schema: {foo: 'bar'}, + Schema: { foo: 'bar' }, }, }); expect( @@ -112,7 +101,7 @@ describe('#compileMethods()', () => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Properties.RequestValidatorId - ).to.deep.equal({Ref: 'ApiGatewayMethodUsersCreatePostValidator'}); + ).to.deep.equal({ Ref: 'ApiGatewayMethodUsersCreatePostValidator' }); }); }); From 2816c388ff3f19deda5dd26d54536da0a6235c22 Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Wed, 17 Apr 2019 10:51:23 -0400 Subject: [PATCH 7/9] docs --- docs/providers/aws/events/apigateway.md | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index 0a50158a4..a934df339 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) @@ -641,6 +642,47 @@ 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]+$" + } + } +} +``` + ### 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: From 88189d9451d6de41082f332179757bd4793d6fbd Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Thu, 18 Apr 2019 11:53:27 -0400 Subject: [PATCH 8/9] DRY up naming functions and add unit tests --- lib/plugins/aws/lib/naming.js | 4 ++-- lib/plugins/aws/lib/naming.test.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 8b15ef03f..69d3f344d 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -243,10 +243,10 @@ module.exports = { return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`; }, getValidatorLogicalId(resourceId, methodName) { - return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}Validator`; + return `${this.getMethodLogicalId(resourceId, methodName)}Validator`; }, getModelLogicalId(resourceId, methodName, contentType) { - return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}${_.startCase( + return `${this.getMethodLogicalId(resourceId, methodName)}${_.startCase( contentType).replace(' ', '')}Model`; }, getApiKeyLogicalId(apiKeyNumber, apiKeyName) { 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'); From a4e0a26d536c3941a7abfdcbcfe3271df54bf82d Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Fri, 19 Apr 2019 10:54:06 -0400 Subject: [PATCH 9/9] add note about other content types going right through --- docs/providers/aws/events/apigateway.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index a934df339..b319692e4 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -683,6 +683,9 @@ A sample schema contained in `create_request.json` might look something like thi } ``` +**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: