From 53eb1e7954922fdc7cef4b00e6ca3b31a6521c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Thu, 20 Jun 2019 14:48:50 +0200 Subject: [PATCH 1/6] feat(aws-apigateway): expand RestApi support to models, parameters and validators Fixes #905: "apigateway: "methodResponses" is missing from MethodOptions" Fixes #1695: apigateway: missing support for models Fixes #727: API Gateway: improve API for request parameters and responses Fixes #723: API Gateway: missing features Fixes #2957: RestApi to use logical id as a name for APIs instrea of name of current construct --- packages/@aws-cdk/aws-apigateway/README.md | 3 - packages/@aws-cdk/aws-apigateway/lib/index.ts | 1 + .../@aws-cdk/aws-apigateway/lib/method.ts | 36 +++- .../aws-apigateway/lib/methodresponse.ts | 4 +- packages/@aws-cdk/aws-apigateway/lib/model.ts | 161 +++++++++++++----- .../aws-apigateway/lib/requestvalidator.ts | 74 ++++++++ .../@aws-cdk/aws-apigateway/lib/restapi.ts | 26 ++- .../test/integ.restapi.books.expected.json | 2 +- .../test/integ.restapi.defaults.expected.json | 2 +- .../test/integ.restapi.expected.json | 2 +- .../aws-apigateway/test/test.deployment.ts | 4 +- .../aws-apigateway/test/test.method.ts | 142 ++++++++++++++- .../aws-apigateway/test/test.model.ts | 78 +++++++++ .../test/test.requestvalidator.ts | 64 +++++++ .../aws-apigateway/test/test.resource.ts | 2 +- .../aws-apigateway/test/test.restapi.ts | 48 ++++-- .../aws-apigateway/test/test.stage.ts | 2 +- 17 files changed, 580 insertions(+), 71 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.model.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index bb0aee504cc19..45a6641991fd7 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -256,12 +256,9 @@ to allow users revert the stage to an old deployment manually. ### Missing Features -See [awslabs/aws-cdk#723](https://github.com/awslabs/aws-cdk/issues/723) for a -list of missing features. ### Roadmap -- [ ] Support defining REST API Models [#1695](https://github.com/awslabs/aws-cdk/issues/1695) ---- diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index e4d77bb5bf238..cb60d4c2d097f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -11,6 +11,7 @@ export * from './usage-plan'; export * from './vpc-link'; export * from './methodresponse'; export * from './model'; +export * from './requestvalidator'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index d357423f00ccc..aaf160e9d442e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -3,6 +3,8 @@ import { CfnMethod, CfnMethodProps } from './apigateway.generated'; import { ConnectionType, Integration } from './integration'; import { MockIntegration } from './integrations/mock'; import { MethodResponse } from './methodresponse'; +import { IModelRef } from './model'; +import { IRequestValidatorRef } from './requestvalidator'; import { IResource } from './resource'; import { RestApi } from './restapi'; import { validateHttpMethod } from './util'; @@ -56,9 +58,17 @@ export interface MethodOptions { */ readonly requestParameters?: { [param: string]: boolean }; - // TODO: - // - RequestValidatorId - // - RequestModels + /** + * The resources that are used for the response's content type. Specify request + * models as key-value pairs (string-to-string mapping), with a content type + * as the key and a Model resource name as the value + */ + readonly requestModels?: { [param: string]: IModelRef }; + + /** + * The ID of the associated request validator. + */ + readonly requestValidator?: IRequestValidatorRef; } export interface MethodProps { @@ -120,6 +130,8 @@ export class Method extends Resource { requestParameters: options.requestParameters, integration: this.renderIntegration(props.integration), methodResponses: this.renderMethodResponses(options.methodResponses), + requestModels: this.renderRequestModels(options.requestModels), + requestValidatorId: options.requestValidator ? options.requestValidator.requestValidatorId : undefined }; const resource = new CfnMethod(this, 'Resource', methodProps); @@ -230,7 +242,7 @@ export class Method extends Resource { responseModels = {}; for (const contentType in mr.responseModels) { if (mr.responseModels.hasOwnProperty(contentType)) { - responseModels[contentType] = mr.responseModels[contentType].modelId; + responseModels[contentType] = mr.responseModels[contentType].modelName; } } } @@ -244,6 +256,22 @@ export class Method extends Resource { return methodResponseProp; }); } + + private renderRequestModels(requestModels: { [param: string]: IModelRef } | undefined): { [param: string]: string } | undefined { + if (!requestModels) { + // Fall back to nothing + return undefined; + } + + const models: {[param: string]: string} = {}; + for (const contentType in requestModels) { + if (requestModels.hasOwnProperty(contentType)) { + models[contentType] = requestModels[contentType].modelName; + } + } + + return models; + } } export enum AuthorizationType { diff --git a/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts b/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts index a3b504ae4cbba..72cae41868d4e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts @@ -1,4 +1,4 @@ -import { IModel } from './model'; +import { IModelRef } from './model'; export interface MethodResponse { @@ -24,5 +24,5 @@ export interface MethodResponse { * resource name as the value. * @default None */ - readonly responseModels?: { [contentType: string]: IModel }; + readonly responseModels?: { [contentType: string]: IModelRef }; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/model.ts b/packages/@aws-cdk/aws-apigateway/lib/model.ts index f38de58375302..5e31b16e23848 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/model.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/model.ts @@ -1,46 +1,127 @@ -export interface IModel { - readonly modelId: string; +import { Construct, Resource } from '@aws-cdk/cdk'; +import { CfnModel, CfnModelProps } from './apigateway.generated'; +import { IRestApi, RestApi } from './restapi'; + +export interface IModelRef { + /** @attribute */ + readonly modelName: string; } -/** - * Represents a reference to a REST API's Empty model, which is available - * as part of the model collection by default. This can be used for mapping - * JSON responses from an integration to what is returned to a client, - * where strong typing is not required. In the absence of any defined - * model, the Empty model will be used to return the response payload - * unmapped. - * - * Definition - * { - * "$schema" : "http://json-schema.org/draft-04/schema#", - * "title" : "Empty Schema", - * "type" : "object" - * } - * - * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models - */ -export class EmptyModel implements IModel { - public readonly modelId = 'Empty'; +export interface ModelOptions { + /** + * The content type for the model. + * @default None + */ + readonly contentType?: string; + + /** + * A description that identifies this model.. + * @default None + */ + readonly description?: string; + + /** + * A name for the model. If you don't specify a name, + * AWS CloudFormation generates a unique physical ID and + * uses that ID for the model name. For more information, + * see Name Type. + * + * Important + * If you specify a name, you cannot perform updates that + * require replacement of this resource. You can perform + * updates that require no or some interruption. If you + * must replace the resource, specify a new name.. + */ + readonly name?: string; + + /** + * The schema to use to transform data to one or more output formats. + * Specify null ({}) if you don't want to specify a schema. + */ + readonly schema: any; } -/** - * Represents a reference to a REST API's Error model, which is available - * as part of the model collection by default. This can be used for mapping - * error JSON responses from an integration to a client, where a simple - * generic message field is sufficient to map and return an error payload. - * - * Definition - * { - * "$schema" : "http://json-schema.org/draft-04/schema#", - * "title" : "Error Schema", - * "type" : "object", - * "properties" : { - * "message" : { "type" : "string" } - * } - * } - */ -export class ErrorModel implements IModel { - public readonly modelId = 'Error'; +export interface ModelProps { + /** + * The rest API that this model is part of. + * + * The reason we need the RestApi object itself and not just the ID is because the model + * is being tracked by the top-level RestApi object for the purpose of calculating it's + * hash to determine the ID of the deployment. This allows us to automatically update + * the deployment when the model of the REST API changes. + */ + readonly restApi: IRestApi; + + /** + * Model options. + */ + readonly options: ModelOptions; } -// TODO: Implement Model, enabling management of custom models. \ No newline at end of file +export class Model extends Resource implements IModelRef { + /** + * Represents a reference to a REST API's Error model, which is available + * as part of the model collection by default. This can be used for mapping + * error JSON responses from an integration to a client, where a simple + * generic message field is sufficient to map and return an error payload. + * + * Definition + * { + * "$schema" : "http://json-schema.org/draft-04/schema#", + * "title" : "Error Schema", + * "type" : "object", + * "properties" : { + * "message" : { "type" : "string" } + * } + * } + */ + public static readonly ErrorModel: IModelRef = { modelName: "Error" }; + + /** + * Represents a reference to a REST API's Empty model, which is available + * as part of the model collection by default. This can be used for mapping + * JSON responses from an integration to what is returned to a client, + * where strong typing is not required. In the absence of any defined + * model, the Empty model will be used to return the response payload + * unmapped. + * + * Definition + * { + * "$schema" : "http://json-schema.org/draft-04/schema#", + * "title" : "Empty Schema", + * "type" : "object" + * } + * + * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models + */ + public static readonly EmptyModel: IModelRef = { modelName: "Empty" }; + + /** @attribute */ + public readonly modelName: string; + + /** @attribute */ + public readonly restApi: IRestApi; + + constructor(scope: Construct, id: string, props: ModelProps) { + super(scope, id); + + this.restApi = props.restApi; + + const options = props.options; + + const modelProps: CfnModelProps = { + ...options, + restApiId: this.restApi.restApiId + }; + + const resource = new CfnModel(this, 'Resource', modelProps); + + this.modelName = resource.modelName; + + const deployment = (this.restApi instanceof RestApi) ? this.restApi.latestDeployment : undefined; + if (deployment) { + deployment.node.addDependency(resource); + deployment.addToLogicalId({ model: modelProps }); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts new file mode 100644 index 0000000000000..45cb28789a98e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts @@ -0,0 +1,74 @@ +import { Construct, Resource } from '@aws-cdk/cdk'; +import { CfnRequestValidator, CfnRequestValidatorProps } from './apigateway.generated'; +import { IRestApi, RestApi } from './restapi'; + +export interface IRequestValidatorRef { + /** @attribute */ + readonly requestValidatorId: string; +} + +export interface RequestValidatorOptions { + /** + * The name of this request validator. + */ + readonly name?: string; + + /** + * Indicates whether to validate the request body according to + * the configured schema for the targeted API and method. + */ + readonly validateRequestBody?: boolean; + + /** + * Indicates whether to validate request parameters. + */ + readonly validateRequestParameters?: boolean; +} + +export interface RequestValidatorProps { + /** + * The rest API that this model is part of. + * + * The reason we need the RestApi object itself and not just the ID is because the model + * is being tracked by the top-level RestApi object for the purpose of calculating it's + * hash to determine the ID of the deployment. This allows us to automatically update + * the deployment when the model of the REST API changes. + */ + readonly restApi: IRestApi; + + /** + * Validator options. + */ + readonly options: RequestValidatorOptions; +} + +export class RequestValidator extends Resource implements IRequestValidatorRef { + /** @attribute */ + public readonly requestValidatorId: string; + + /** @attribute */ + public readonly restApi: IRestApi; + + constructor(scope: Construct, id: string, props: RequestValidatorProps) { + super(scope, id); + + this.restApi = props.restApi; + + const options = props.options; + + const validatorProps: CfnRequestValidatorProps = { + ...options, + restApiId: this.restApi.restApiId + }; + + const resource = new CfnRequestValidator(this, 'Resource', validatorProps); + + this.requestValidatorId = resource.requestValidatorId; + + const deployment = (this.restApi instanceof RestApi) ? this.restApi.latestDeployment : undefined; + if (deployment) { + deployment.node.addDependency(resource); + deployment.addToLogicalId({ validator: validatorProps }); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index baba37991b047..1f463cc72e760 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -5,6 +5,8 @@ import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { Deployment } from './deployment'; import { Integration } from './integration'; import { Method, MethodOptions } from './method'; +import { Model, ModelOptions } from './model'; +import { RequestValidator, RequestValidatorOptions } from './requestvalidator'; import { IResource, ResourceBase, ResourceOptions } from './resource'; import { Stage, StageOptions } from './stage'; import { UsagePlan, UsagePlanProps } from './usage-plan'; @@ -207,7 +209,6 @@ export class RestApi extends Resource implements IRestApi { super(scope, id); const resource = new CfnRestApi(this, 'Resource', { - name: props.restApiName || id, description: props.description, policy: props.policy, failOnWarnings: props.failOnWarnings, @@ -217,8 +218,9 @@ export class RestApi extends Resource implements IRestApi { apiKeySourceType: props.apiKeySourceType, cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, parameters: props.parameters, + name: '@@Error@@' }); - + resource.addPropertyOverride('Name', props.restApiName || resource.logicalId); this.restApiId = resource.refAsString; this.configureDeployment(props); @@ -267,6 +269,26 @@ export class RestApi extends Resource implements IRestApi { }); } + /** + * Adds a new model. + */ + public addModel(id: string, props: ModelOptions): Model { + return new Model(this, id, { + restApi: this, + options: props + }); + } + + /** + * Adds a new model. + */ + public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { + return new RequestValidator(this, id, { + restApi: this, + options: props + }); + } + /** * @returns The "execute-api" ARN. * @default "*" returns the execute API ARN for all methods/resources in diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 93f3524ea8a44..5086cc82c8d75 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -573,7 +573,7 @@ "booksapiE1885304": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "books-api" + "Name": "booksapiE1885304" } }, "booksapiDeployment308B08F1c828b08824c062376eba921738884f85": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index ba1a3b6829daf..4a0fe5131614e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -3,7 +3,7 @@ "myapi4C7BF186": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "my-api" + "Name": "myapi4C7BF186" } }, "myapiDeployment92F2CB49916eaecf87f818f1e175215b8d086029": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index e5298cbfc2bb2..52775b0b843fd 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -3,7 +3,7 @@ "myapi4C7BF186": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "my-api" + "Name": "myapi4C7BF186" } }, "myapiDeployment92F2CB4919460d935da8177bcfbc418506e514ff": { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index d3647822b2bf5..501609d196af8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -38,7 +38,7 @@ export = { apiC8550315: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "api" + Name: "apiC8550315" } }, deployment33381975: { @@ -89,7 +89,7 @@ export = { apiC8550315: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "api" + Name: "apiC8550315" } }, deployment33381975: { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index 8897601bcdbe0..99cd0361038aa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -5,7 +5,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import apigateway = require('../lib'); -import { ConnectionType, EmptyModel, ErrorModel } from '../lib'; +import { ConnectionType } from '../lib'; export = { 'default setup'(test: Test) { @@ -349,8 +349,8 @@ export = { 'method.response.header.errthing': true }, responseModels: { - 'application/json': new EmptyModel(), - 'text/plain': new ErrorModel() + 'application/json': apigateway.Model.EmptyModel, + 'text/plain': apigateway.Model.ErrorModel } } ] @@ -443,6 +443,142 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "POST" })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "GET" })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "PUT" })); + test.done(); + }, + + 'requestModel can be set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const model = new apigateway.CfnModel(stack, 'test-model', { + contentType: "application/json", + name: 'TestModel', + restApiId: api.restApiId, + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + }); + + // WHEN + new apigateway.Method(stack, 'method-man', { + httpMethod: 'GET', + resource: api.root, + options: { + requestModels: { + "application/json": { modelName: model.modelName } + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + RequestModels: { + "application/json": { Ref: stack.getLogicalId(model) } + } + })); + + test.done(); + }, + + 'methodResponse has a mix of response modes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const htmlModel = api.addModel('my-model', { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + }); + + // WHEN + new apigateway.Method(stack, 'method-man', { + httpMethod: 'GET', + resource: api.root, + options: { + methodResponses: [{ + statusCode: '200' + }, { + statusCode: "400", + responseParameters: { + 'method.response.header.killerbees': false + } + }, { + statusCode: "500", + responseParameters: { + 'method.response.header.errthing': true + }, + responseModels: { + 'application/json': apigateway.Model.EmptyModel, + 'text/plain': apigateway.Model.ErrorModel, + 'text/html': htmlModel + } + } + ] + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + MethodResponses: [{ + StatusCode: "200" + }, { + StatusCode: "400", + ResponseParameters: { + 'method.response.header.killerbees': false + } + }, { + StatusCode: "500", + ResponseParameters: { + 'method.response.header.errthing': true + }, + ResponseModels: { + 'application/json': 'Empty', + 'text/plain': 'Error', + 'text/html': { Ref: stack.getLogicalId(htmlModel.node.findChild('Resource') as cdk.CfnElement) } + } + } + ] + })); + + test.done(); + }, + + 'method has a request validator'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const validator = api.addRequestValidator('validator', { + validateRequestBody: true, + validateRequestParameters: false + }); + + // WHEN + new apigateway.Method(stack, 'method-man', { + httpMethod: 'GET', + resource: api.root, + options: { + requestValidator: validator + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + RequestValidatorId: { Ref: stack.getLogicalId(validator.node.findChild('Resource') as cdk.CfnElement) } + })); + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + ValidateRequestBody: true, + ValidateRequestParameters: false + })); + test.done(); } }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.model.ts b/packages/@aws-cdk/aws-apigateway/test/test.model.ts new file mode 100644 index 0000000000000..724d0e7561d38 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.model.ts @@ -0,0 +1,78 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.Model(stack, 'my-model', { + restApi: api, + options: { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Model', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + })); + + test.done(); + }, + + 'no deployment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.Model(stack, 'my-model', { + restApi: api, + options: { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Model', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts b/packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts new file mode 100644 index 0000000000000..1db23cb9035f9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts @@ -0,0 +1,64 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.RequestValidator(stack, 'my-model', { + restApi: api, + options: { + validateRequestBody: true, + validateRequestParameters: false + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + ValidateRequestBody: true, + ValidateRequestParameters: false + })); + + test.done(); + }, + + 'no deployment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.RequestValidator(stack, 'my-model', { + restApi: api, + options: { + name: "Test Validator", + validateRequestBody: false, + validateRequestParameters: true + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Name: "Test Validator", + ValidateRequestBody: false, + ValidateRequestParameters: true + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index 627998d524f3c..5b3d910a5d10a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -85,7 +85,7 @@ export = { "apiC8550315": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "api" + "Name": "apiC8550315" } }, "apiv25206B108": { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 7b3b7f462cacc..ef3ffe88dfa63 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -21,7 +21,7 @@ export = { myapi4C7BF186: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "my-api" + Name: "myapi4C7BF186" } }, myapiGETF990CE3C: { @@ -100,12 +100,12 @@ export = { test.done(); }, - '"name" is defaulted to construct id'(test: Test) { + '"name" is defaulted to resource unique id'(test: Test) { // GIVEN const stack = new cdk.Stack(); // WHEN - const api = new apigateway.RestApi(stack, 'my-first-api', { + const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, cloudWatchRole: false, }); @@ -114,7 +114,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { - Name: "my-first-api" + Name: "restapiC5611D27" })); test.done(); @@ -140,8 +140,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, - cloudWatchRole: false, - restApiName: 'my-rest-api' + cloudWatchRole: false }); api.root.addMethod('GET'); @@ -175,8 +174,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, - cloudWatchRole: false, - restApiName: 'my-rest-api' + cloudWatchRole: false }); // WHEN @@ -208,7 +206,7 @@ export = { restapiC5611D27: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "restapi" + Name: "restapiC5611D27" } }, restapir1CF2997EA: { @@ -491,7 +489,7 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { CloneFrom: "foobar", - Name: "api" + Name: "apiC8550315" })); test.done(); @@ -601,4 +599,34 @@ export = { test.done(); }, + + 'addModel is supported'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'myapi'); + api.root.addMethod('OPTIONS'); + + // WHEN + api.addModel('model', { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Model', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts index 6aef209a42fc3..1c0b1b81c23be 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts @@ -20,7 +20,7 @@ export = { testapiD6451F70: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "test-api" + Name: "testapiD6451F70" } }, testapiGETD8DE4ED1: { From 221fc0dada752f921176f8093239a3b14e8ca735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Thu, 20 Jun 2019 14:48:50 +0200 Subject: [PATCH 2/6] feat(aws-apigateway): expand RestApi support to models, parameters and validators Fixes #905: "apigateway: "methodResponses" is missing from MethodOptions" Fixes #1695: apigateway: missing support for models Fixes #727: API Gateway: improve API for request parameters and responses Fixes #723: API Gateway: missing features Fixes #2957: RestApi to use logical id as a name for APIs instrea of name of current construct --- packages/@aws-cdk/aws-apigateway/README.md | 144 +++++++++++++++- packages/@aws-cdk/aws-apigateway/lib/index.ts | 1 + .../@aws-cdk/aws-apigateway/lib/method.ts | 36 +++- .../aws-apigateway/lib/methodresponse.ts | 4 +- packages/@aws-cdk/aws-apigateway/lib/model.ts | 161 +++++++++++++----- .../aws-apigateway/lib/requestvalidator.ts | 74 ++++++++ .../@aws-cdk/aws-apigateway/lib/restapi.ts | 26 ++- .../test/integ.restapi.books.expected.json | 2 +- .../test/integ.restapi.defaults.expected.json | 2 +- .../test/integ.restapi.expected.json | 2 +- .../aws-apigateway/test/test.deployment.ts | 4 +- .../aws-apigateway/test/test.method.ts | 142 ++++++++++++++- .../aws-apigateway/test/test.model.ts | 78 +++++++++ .../test/test.requestvalidator.ts | 64 +++++++ .../aws-apigateway/test/test.resource.ts | 2 +- .../aws-apigateway/test/test.restapi.ts | 48 ++++-- .../aws-apigateway/test/test.stage.ts | 2 +- 17 files changed, 721 insertions(+), 71 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.model.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index bb0aee504cc19..7167d97bb4d6f 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -152,6 +152,147 @@ plan.addApiStage({ }); ``` +### Working with models + +When you work with Lambda integrations that are not Proxy integrations, you +have to define your models and mappings for the request, response, and integration. + +```ts +const hello = new lambda.Function(this, 'hello', { + runtime: lambda.Runtime.Nodejs10x, + handler: 'hello.handler', + code: lambda.Code.asset('lambda') +}); + +const api = new apigateway.RestApi(this, 'hello-api', { }); +const resource = api.root.addResource('v1'); +``` + +You can define more parameters on the integration to tune the behavior of API Gateway + +```ts +const integration = new LambdaIntegration(hello, { + proxy: false, + requestParameters: { + // You can define mapping parameters from your method to your integration + // - Destination parameters (the key) are the integration parameters (used in mappings) + // - Source parameters (the value) are the source request parameters or expressions + // @see: https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html + "integration.request.querystring.who": "method.request.querystring.who" + }, + allowTestInvoke: true, + requestTemplates: { + // You can define a mapping that will build a payload for your integration, based + // on the integration parameters that you have specified + // Check: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html + "application/json": '{ "action": "sayHello", "pollId": "$util.escapeJavaScript($input.params(\'who\'))" }' + }, + // This parameter defines the behavior of the engine is no suitable response template is found + passthroughBehavior: PassthroughBehavior.Never, + integrationResponses: [ + { + // Successful response from the Lambda function, no filter defined + // - the selectionPattern filter only tests the error message + // We will set the response status code to 200 + statusCode: "200", + responseTemplates: { + // This template takes the "message" result from the Lambda function, adn embeds it in a JSON response + // Check https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html + "application/json": '{ "state": "ok", "greeting": "$util.escapeJavaScript($input.body)" }' + }, + responseParameters: { + // We can map response parameters + // - Destination parameters (the key) are the response parameters (used in mappings) + // - Source parameters (the value) are the integration response parameters or expressions + 'method.response.header.Content-Type': "'application/json'", + 'method.response.header.Access-Control-Allow-Origin': "'*'", + 'method.response.header.Access-Control-Allow-Credentials': "'true'" + } + }, + { + // For errors, we check if the error message is not empty, get the error data + selectionPattern: '.+', + // We will set the response status code to 200 + statusCode: "400", + responseTemplates: { + "application/json": '{ "state": "error", "message": "$util.escapeJavaScript($input.path(\'$.errorMessage\'))" }' + }, + responseParameters: { + 'method.response.header.Content-Type': "'application/json'", + 'method.response.header.Access-Control-Allow-Origin': "'*'", + 'method.response.header.Access-Control-Allow-Credentials': "'true'" + } + } + ] +}); + +``` + +You can define validation models for your responses (and requests) + +```ts +// We define the JSON Schema for the transformed valid response +const responseModel = api.addModel('ResponseModel', { + contentType: "application/json", + name: 'ResponseModel', + schema: { "$schema": "http://json-schema.org/draft-04/schema#", "title": "pollResponse", "type": "object", "properties": { "state": { "type": "string" }, "greeting": { "type": "string" } } } +}); + +// We define the JSON Schema for the transformed error response +const errorResponseModel = api.addModel('ErrorResponseModel', { + contentType: "application/json", + name: 'ErrorResponseModel', + schema: { "$schema": "http://json-schema.org/draft-04/schema#", "title": "errorResponse", "type": "object", "properties": { "state": { "type": "string" }, "message": { "type": "string" } } } +}); + +``` + +And reference all on your method definition. + +```ts +// If you want to define parameter mappings for the request, you need a validator +const validator = api.addRequestValidator('DefaultValidator', { + validateRequestBody: false, + validateRequestParameters: true +}); +resource.addMethod('GET', integration, { + // We can mark the parameters as required + requestParameters: { + "method.request.querystring.who": true + }, + // We need to set the validator for ensuring they are passed + requestValidator: validator, + methodResponses: [ + { + // Successful response from the integration + statusCode: "200", + // Define what parameters are allowed or not + responseParameters: { + 'method.response.header.Content-Type': true, + 'method.response.header.Access-Control-Allow-Origin': true, + 'method.response.header.Access-Control-Allow-Credentials': true + }, + // Validate the schema on the response + responseModels: { + "application/json": responseModel + } + }, + { + // Same thing for the error responses + statusCode: "400", + responseParameters: { + 'method.response.header.Content-Type': true, + 'method.response.header.Access-Control-Allow-Origin': true, + 'method.response.header.Access-Control-Allow-Credentials': true + }, + responseModels: { + "application/json": errorResponseModel + } + } + ] +}); +``` + #### Default Integration and Method Options The `defaultIntegration` and `defaultMethodOptions` properties can be used to @@ -256,12 +397,9 @@ to allow users revert the stage to an old deployment manually. ### Missing Features -See [awslabs/aws-cdk#723](https://github.com/awslabs/aws-cdk/issues/723) for a -list of missing features. ### Roadmap -- [ ] Support defining REST API Models [#1695](https://github.com/awslabs/aws-cdk/issues/1695) ---- diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index e4d77bb5bf238..cb60d4c2d097f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -11,6 +11,7 @@ export * from './usage-plan'; export * from './vpc-link'; export * from './methodresponse'; export * from './model'; +export * from './requestvalidator'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index d357423f00ccc..aaf160e9d442e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -3,6 +3,8 @@ import { CfnMethod, CfnMethodProps } from './apigateway.generated'; import { ConnectionType, Integration } from './integration'; import { MockIntegration } from './integrations/mock'; import { MethodResponse } from './methodresponse'; +import { IModelRef } from './model'; +import { IRequestValidatorRef } from './requestvalidator'; import { IResource } from './resource'; import { RestApi } from './restapi'; import { validateHttpMethod } from './util'; @@ -56,9 +58,17 @@ export interface MethodOptions { */ readonly requestParameters?: { [param: string]: boolean }; - // TODO: - // - RequestValidatorId - // - RequestModels + /** + * The resources that are used for the response's content type. Specify request + * models as key-value pairs (string-to-string mapping), with a content type + * as the key and a Model resource name as the value + */ + readonly requestModels?: { [param: string]: IModelRef }; + + /** + * The ID of the associated request validator. + */ + readonly requestValidator?: IRequestValidatorRef; } export interface MethodProps { @@ -120,6 +130,8 @@ export class Method extends Resource { requestParameters: options.requestParameters, integration: this.renderIntegration(props.integration), methodResponses: this.renderMethodResponses(options.methodResponses), + requestModels: this.renderRequestModels(options.requestModels), + requestValidatorId: options.requestValidator ? options.requestValidator.requestValidatorId : undefined }; const resource = new CfnMethod(this, 'Resource', methodProps); @@ -230,7 +242,7 @@ export class Method extends Resource { responseModels = {}; for (const contentType in mr.responseModels) { if (mr.responseModels.hasOwnProperty(contentType)) { - responseModels[contentType] = mr.responseModels[contentType].modelId; + responseModels[contentType] = mr.responseModels[contentType].modelName; } } } @@ -244,6 +256,22 @@ export class Method extends Resource { return methodResponseProp; }); } + + private renderRequestModels(requestModels: { [param: string]: IModelRef } | undefined): { [param: string]: string } | undefined { + if (!requestModels) { + // Fall back to nothing + return undefined; + } + + const models: {[param: string]: string} = {}; + for (const contentType in requestModels) { + if (requestModels.hasOwnProperty(contentType)) { + models[contentType] = requestModels[contentType].modelName; + } + } + + return models; + } } export enum AuthorizationType { diff --git a/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts b/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts index a3b504ae4cbba..72cae41868d4e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts @@ -1,4 +1,4 @@ -import { IModel } from './model'; +import { IModelRef } from './model'; export interface MethodResponse { @@ -24,5 +24,5 @@ export interface MethodResponse { * resource name as the value. * @default None */ - readonly responseModels?: { [contentType: string]: IModel }; + readonly responseModels?: { [contentType: string]: IModelRef }; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/model.ts b/packages/@aws-cdk/aws-apigateway/lib/model.ts index f38de58375302..5e31b16e23848 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/model.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/model.ts @@ -1,46 +1,127 @@ -export interface IModel { - readonly modelId: string; +import { Construct, Resource } from '@aws-cdk/cdk'; +import { CfnModel, CfnModelProps } from './apigateway.generated'; +import { IRestApi, RestApi } from './restapi'; + +export interface IModelRef { + /** @attribute */ + readonly modelName: string; } -/** - * Represents a reference to a REST API's Empty model, which is available - * as part of the model collection by default. This can be used for mapping - * JSON responses from an integration to what is returned to a client, - * where strong typing is not required. In the absence of any defined - * model, the Empty model will be used to return the response payload - * unmapped. - * - * Definition - * { - * "$schema" : "http://json-schema.org/draft-04/schema#", - * "title" : "Empty Schema", - * "type" : "object" - * } - * - * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models - */ -export class EmptyModel implements IModel { - public readonly modelId = 'Empty'; +export interface ModelOptions { + /** + * The content type for the model. + * @default None + */ + readonly contentType?: string; + + /** + * A description that identifies this model.. + * @default None + */ + readonly description?: string; + + /** + * A name for the model. If you don't specify a name, + * AWS CloudFormation generates a unique physical ID and + * uses that ID for the model name. For more information, + * see Name Type. + * + * Important + * If you specify a name, you cannot perform updates that + * require replacement of this resource. You can perform + * updates that require no or some interruption. If you + * must replace the resource, specify a new name.. + */ + readonly name?: string; + + /** + * The schema to use to transform data to one or more output formats. + * Specify null ({}) if you don't want to specify a schema. + */ + readonly schema: any; } -/** - * Represents a reference to a REST API's Error model, which is available - * as part of the model collection by default. This can be used for mapping - * error JSON responses from an integration to a client, where a simple - * generic message field is sufficient to map and return an error payload. - * - * Definition - * { - * "$schema" : "http://json-schema.org/draft-04/schema#", - * "title" : "Error Schema", - * "type" : "object", - * "properties" : { - * "message" : { "type" : "string" } - * } - * } - */ -export class ErrorModel implements IModel { - public readonly modelId = 'Error'; +export interface ModelProps { + /** + * The rest API that this model is part of. + * + * The reason we need the RestApi object itself and not just the ID is because the model + * is being tracked by the top-level RestApi object for the purpose of calculating it's + * hash to determine the ID of the deployment. This allows us to automatically update + * the deployment when the model of the REST API changes. + */ + readonly restApi: IRestApi; + + /** + * Model options. + */ + readonly options: ModelOptions; } -// TODO: Implement Model, enabling management of custom models. \ No newline at end of file +export class Model extends Resource implements IModelRef { + /** + * Represents a reference to a REST API's Error model, which is available + * as part of the model collection by default. This can be used for mapping + * error JSON responses from an integration to a client, where a simple + * generic message field is sufficient to map and return an error payload. + * + * Definition + * { + * "$schema" : "http://json-schema.org/draft-04/schema#", + * "title" : "Error Schema", + * "type" : "object", + * "properties" : { + * "message" : { "type" : "string" } + * } + * } + */ + public static readonly ErrorModel: IModelRef = { modelName: "Error" }; + + /** + * Represents a reference to a REST API's Empty model, which is available + * as part of the model collection by default. This can be used for mapping + * JSON responses from an integration to what is returned to a client, + * where strong typing is not required. In the absence of any defined + * model, the Empty model will be used to return the response payload + * unmapped. + * + * Definition + * { + * "$schema" : "http://json-schema.org/draft-04/schema#", + * "title" : "Empty Schema", + * "type" : "object" + * } + * + * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models + */ + public static readonly EmptyModel: IModelRef = { modelName: "Empty" }; + + /** @attribute */ + public readonly modelName: string; + + /** @attribute */ + public readonly restApi: IRestApi; + + constructor(scope: Construct, id: string, props: ModelProps) { + super(scope, id); + + this.restApi = props.restApi; + + const options = props.options; + + const modelProps: CfnModelProps = { + ...options, + restApiId: this.restApi.restApiId + }; + + const resource = new CfnModel(this, 'Resource', modelProps); + + this.modelName = resource.modelName; + + const deployment = (this.restApi instanceof RestApi) ? this.restApi.latestDeployment : undefined; + if (deployment) { + deployment.node.addDependency(resource); + deployment.addToLogicalId({ model: modelProps }); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts new file mode 100644 index 0000000000000..45cb28789a98e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts @@ -0,0 +1,74 @@ +import { Construct, Resource } from '@aws-cdk/cdk'; +import { CfnRequestValidator, CfnRequestValidatorProps } from './apigateway.generated'; +import { IRestApi, RestApi } from './restapi'; + +export interface IRequestValidatorRef { + /** @attribute */ + readonly requestValidatorId: string; +} + +export interface RequestValidatorOptions { + /** + * The name of this request validator. + */ + readonly name?: string; + + /** + * Indicates whether to validate the request body according to + * the configured schema for the targeted API and method. + */ + readonly validateRequestBody?: boolean; + + /** + * Indicates whether to validate request parameters. + */ + readonly validateRequestParameters?: boolean; +} + +export interface RequestValidatorProps { + /** + * The rest API that this model is part of. + * + * The reason we need the RestApi object itself and not just the ID is because the model + * is being tracked by the top-level RestApi object for the purpose of calculating it's + * hash to determine the ID of the deployment. This allows us to automatically update + * the deployment when the model of the REST API changes. + */ + readonly restApi: IRestApi; + + /** + * Validator options. + */ + readonly options: RequestValidatorOptions; +} + +export class RequestValidator extends Resource implements IRequestValidatorRef { + /** @attribute */ + public readonly requestValidatorId: string; + + /** @attribute */ + public readonly restApi: IRestApi; + + constructor(scope: Construct, id: string, props: RequestValidatorProps) { + super(scope, id); + + this.restApi = props.restApi; + + const options = props.options; + + const validatorProps: CfnRequestValidatorProps = { + ...options, + restApiId: this.restApi.restApiId + }; + + const resource = new CfnRequestValidator(this, 'Resource', validatorProps); + + this.requestValidatorId = resource.requestValidatorId; + + const deployment = (this.restApi instanceof RestApi) ? this.restApi.latestDeployment : undefined; + if (deployment) { + deployment.node.addDependency(resource); + deployment.addToLogicalId({ validator: validatorProps }); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index baba37991b047..1f463cc72e760 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -5,6 +5,8 @@ import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { Deployment } from './deployment'; import { Integration } from './integration'; import { Method, MethodOptions } from './method'; +import { Model, ModelOptions } from './model'; +import { RequestValidator, RequestValidatorOptions } from './requestvalidator'; import { IResource, ResourceBase, ResourceOptions } from './resource'; import { Stage, StageOptions } from './stage'; import { UsagePlan, UsagePlanProps } from './usage-plan'; @@ -207,7 +209,6 @@ export class RestApi extends Resource implements IRestApi { super(scope, id); const resource = new CfnRestApi(this, 'Resource', { - name: props.restApiName || id, description: props.description, policy: props.policy, failOnWarnings: props.failOnWarnings, @@ -217,8 +218,9 @@ export class RestApi extends Resource implements IRestApi { apiKeySourceType: props.apiKeySourceType, cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, parameters: props.parameters, + name: '@@Error@@' }); - + resource.addPropertyOverride('Name', props.restApiName || resource.logicalId); this.restApiId = resource.refAsString; this.configureDeployment(props); @@ -267,6 +269,26 @@ export class RestApi extends Resource implements IRestApi { }); } + /** + * Adds a new model. + */ + public addModel(id: string, props: ModelOptions): Model { + return new Model(this, id, { + restApi: this, + options: props + }); + } + + /** + * Adds a new model. + */ + public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { + return new RequestValidator(this, id, { + restApi: this, + options: props + }); + } + /** * @returns The "execute-api" ARN. * @default "*" returns the execute API ARN for all methods/resources in diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 93f3524ea8a44..5086cc82c8d75 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -573,7 +573,7 @@ "booksapiE1885304": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "books-api" + "Name": "booksapiE1885304" } }, "booksapiDeployment308B08F1c828b08824c062376eba921738884f85": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index ba1a3b6829daf..4a0fe5131614e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -3,7 +3,7 @@ "myapi4C7BF186": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "my-api" + "Name": "myapi4C7BF186" } }, "myapiDeployment92F2CB49916eaecf87f818f1e175215b8d086029": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index e5298cbfc2bb2..52775b0b843fd 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -3,7 +3,7 @@ "myapi4C7BF186": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "my-api" + "Name": "myapi4C7BF186" } }, "myapiDeployment92F2CB4919460d935da8177bcfbc418506e514ff": { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index d3647822b2bf5..501609d196af8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -38,7 +38,7 @@ export = { apiC8550315: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "api" + Name: "apiC8550315" } }, deployment33381975: { @@ -89,7 +89,7 @@ export = { apiC8550315: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "api" + Name: "apiC8550315" } }, deployment33381975: { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index 8897601bcdbe0..99cd0361038aa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -5,7 +5,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import apigateway = require('../lib'); -import { ConnectionType, EmptyModel, ErrorModel } from '../lib'; +import { ConnectionType } from '../lib'; export = { 'default setup'(test: Test) { @@ -349,8 +349,8 @@ export = { 'method.response.header.errthing': true }, responseModels: { - 'application/json': new EmptyModel(), - 'text/plain': new ErrorModel() + 'application/json': apigateway.Model.EmptyModel, + 'text/plain': apigateway.Model.ErrorModel } } ] @@ -443,6 +443,142 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "POST" })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "GET" })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "PUT" })); + test.done(); + }, + + 'requestModel can be set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const model = new apigateway.CfnModel(stack, 'test-model', { + contentType: "application/json", + name: 'TestModel', + restApiId: api.restApiId, + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + }); + + // WHEN + new apigateway.Method(stack, 'method-man', { + httpMethod: 'GET', + resource: api.root, + options: { + requestModels: { + "application/json": { modelName: model.modelName } + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + RequestModels: { + "application/json": { Ref: stack.getLogicalId(model) } + } + })); + + test.done(); + }, + + 'methodResponse has a mix of response modes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const htmlModel = api.addModel('my-model', { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + }); + + // WHEN + new apigateway.Method(stack, 'method-man', { + httpMethod: 'GET', + resource: api.root, + options: { + methodResponses: [{ + statusCode: '200' + }, { + statusCode: "400", + responseParameters: { + 'method.response.header.killerbees': false + } + }, { + statusCode: "500", + responseParameters: { + 'method.response.header.errthing': true + }, + responseModels: { + 'application/json': apigateway.Model.EmptyModel, + 'text/plain': apigateway.Model.ErrorModel, + 'text/html': htmlModel + } + } + ] + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + MethodResponses: [{ + StatusCode: "200" + }, { + StatusCode: "400", + ResponseParameters: { + 'method.response.header.killerbees': false + } + }, { + StatusCode: "500", + ResponseParameters: { + 'method.response.header.errthing': true + }, + ResponseModels: { + 'application/json': 'Empty', + 'text/plain': 'Error', + 'text/html': { Ref: stack.getLogicalId(htmlModel.node.findChild('Resource') as cdk.CfnElement) } + } + } + ] + })); + + test.done(); + }, + + 'method has a request validator'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const validator = api.addRequestValidator('validator', { + validateRequestBody: true, + validateRequestParameters: false + }); + + // WHEN + new apigateway.Method(stack, 'method-man', { + httpMethod: 'GET', + resource: api.root, + options: { + requestValidator: validator + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + RequestValidatorId: { Ref: stack.getLogicalId(validator.node.findChild('Resource') as cdk.CfnElement) } + })); + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + ValidateRequestBody: true, + ValidateRequestParameters: false + })); + test.done(); } }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.model.ts b/packages/@aws-cdk/aws-apigateway/test/test.model.ts new file mode 100644 index 0000000000000..724d0e7561d38 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.model.ts @@ -0,0 +1,78 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.Model(stack, 'my-model', { + restApi: api, + options: { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Model', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + })); + + test.done(); + }, + + 'no deployment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.Model(stack, 'my-model', { + restApi: api, + options: { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Model', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts b/packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts new file mode 100644 index 0000000000000..1db23cb9035f9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.requestvalidator.ts @@ -0,0 +1,64 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.RequestValidator(stack, 'my-model', { + restApi: api, + options: { + validateRequestBody: true, + validateRequestParameters: false + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + ValidateRequestBody: true, + ValidateRequestParameters: false + })); + + test.done(); + }, + + 'no deployment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + }); + + // WHEN + new apigateway.RequestValidator(stack, 'my-model', { + restApi: api, + options: { + name: "Test Validator", + validateRequestBody: false, + validateRequestParameters: true + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Name: "Test Validator", + ValidateRequestBody: false, + ValidateRequestParameters: true + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index 627998d524f3c..5b3d910a5d10a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -85,7 +85,7 @@ export = { "apiC8550315": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "api" + "Name": "apiC8550315" } }, "apiv25206B108": { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 7b3b7f462cacc..ef3ffe88dfa63 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -21,7 +21,7 @@ export = { myapi4C7BF186: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "my-api" + Name: "myapi4C7BF186" } }, myapiGETF990CE3C: { @@ -100,12 +100,12 @@ export = { test.done(); }, - '"name" is defaulted to construct id'(test: Test) { + '"name" is defaulted to resource unique id'(test: Test) { // GIVEN const stack = new cdk.Stack(); // WHEN - const api = new apigateway.RestApi(stack, 'my-first-api', { + const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, cloudWatchRole: false, }); @@ -114,7 +114,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { - Name: "my-first-api" + Name: "restapiC5611D27" })); test.done(); @@ -140,8 +140,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, - cloudWatchRole: false, - restApiName: 'my-rest-api' + cloudWatchRole: false }); api.root.addMethod('GET'); @@ -175,8 +174,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, - cloudWatchRole: false, - restApiName: 'my-rest-api' + cloudWatchRole: false }); // WHEN @@ -208,7 +206,7 @@ export = { restapiC5611D27: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "restapi" + Name: "restapiC5611D27" } }, restapir1CF2997EA: { @@ -491,7 +489,7 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { CloneFrom: "foobar", - Name: "api" + Name: "apiC8550315" })); test.done(); @@ -601,4 +599,34 @@ export = { test.done(); }, + + 'addModel is supported'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'myapi'); + api.root.addMethod('OPTIONS'); + + // WHEN + api.addModel('model', { + schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Model', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, + Schema: { + $schema: "http://json-schema.org/draft-04/schema#", + title: "test", + type: "object", + properties: { message: { type: "string" } } + } + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts index 6aef209a42fc3..1c0b1b81c23be 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts @@ -20,7 +20,7 @@ export = { testapiD6451F70: { Type: "AWS::ApiGateway::RestApi", Properties: { - Name: "test-api" + Name: "testapiD6451F70" } }, testapiGETD8DE4ED1: { From c5603b89dde1c48bed8901d2ebe5aaeb0463ea72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Tue, 25 Jun 2019 18:34:19 +0200 Subject: [PATCH 3/6] Move back to modelId Use PhysicalName constructs --- packages/@aws-cdk/aws-apigateway/lib/method.ts | 4 ++-- packages/@aws-cdk/aws-apigateway/lib/model.ts | 12 +++++++----- .../@aws-cdk/aws-apigateway/lib/requestvalidator.ts | 4 +++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index c95759fb6a055..f4c1125772e0b 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -241,7 +241,7 @@ export class Method extends Resource { responseModels = {}; for (const contentType in mr.responseModels) { if (mr.responseModels.hasOwnProperty(contentType)) { - responseModels[contentType] = mr.responseModels[contentType].modelName; + responseModels[contentType] = mr.responseModels[contentType].modelId; } } } @@ -265,7 +265,7 @@ export class Method extends Resource { const models: {[param: string]: string} = {}; for (const contentType in requestModels) { if (requestModels.hasOwnProperty(contentType)) { - models[contentType] = requestModels[contentType].modelName; + models[contentType] = requestModels[contentType].modelId; } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/model.ts b/packages/@aws-cdk/aws-apigateway/lib/model.ts index 3b16cb986f937..a2c6cfb9a6c2e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/model.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/model.ts @@ -16,7 +16,7 @@ export interface IModel extends IResource { * * @attribute */ - readonly modelName: string; + readonly modelId: string; } export interface ModelOptions { @@ -107,7 +107,7 @@ export class Model extends Resource implements IModel { public static fromModelName(scope: Construct, id: string, modelName: string): IModel { class Import extends Resource implements IModel { - public readonly modelName = modelName; + public readonly modelId = modelName; } return new Import(scope, id); @@ -125,10 +125,12 @@ export class Model extends Resource implements IModel { * * @attribute */ - public readonly modelName: string; + public readonly modelId: string; constructor(scope: Construct, id: string, props: ModelProps) { - super(scope, id); + super(scope, id, { + physicalName: props.modelName, + }); const modelProps: CfnModelProps = { name: props.modelName, @@ -140,7 +142,7 @@ export class Model extends Resource implements IModel { const resource = new CfnModel(this, 'Resource', modelProps); - this.modelName = resource.ref; + this.modelId = resource.ref; const deployment = (props.restApi instanceof RestApi) ? props.restApi.latestDeployment : undefined; if (deployment) { diff --git a/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts index da8591af07c6b..5820d75d8d2fc 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts @@ -64,7 +64,9 @@ export class RequestValidator extends Resource implements IRequestValidator { public readonly requestValidatorId: string; constructor(scope: Construct, id: string, props: RequestValidatorProps) { - super(scope, id); + super(scope, id, { + physicalName: props.requestValidatorName, + }); const validatorProps: CfnRequestValidatorProps = { name: props.requestValidatorName, From da873668f05a99e22aee335e56c6133fa0bff3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Tue, 25 Jun 2019 20:56:02 +0200 Subject: [PATCH 4/6] Change modelName to modelId in ERROR_MODEL and EMPTY_MODEL --- packages/@aws-cdk/aws-apigateway/lib/model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/model.ts b/packages/@aws-cdk/aws-apigateway/lib/model.ts index a2c6cfb9a6c2e..34627855460a0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/model.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/model.ts @@ -84,7 +84,7 @@ export class Model extends Resource implements IModel { * } * } */ - public static readonly ERROR_MODEL: IModel = ({ modelName: "Error", stack: null, node: null } as unknown) as IModel; + public static readonly ERROR_MODEL: IModel = ({ modelId: "Error", stack: null, node: null } as unknown) as IModel; /** * Represents a reference to a REST API's Empty model, which is available @@ -103,7 +103,7 @@ export class Model extends Resource implements IModel { * * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models */ - public static readonly EMPTY_MODEL: IModel = ({ modelName: "Empty", stack: null, node: null } as unknown) as IModel; + public static readonly EMPTY_MODEL: IModel = ({ modelId: "Empty", stack: null, node: null } as unknown) as IModel; public static fromModelName(scope: Construct, id: string, modelName: string): IModel { class Import extends Resource implements IModel { From 31138b75b99c5fec7d726030ab99d022582149a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Wed, 26 Jun 2019 16:43:46 +0200 Subject: [PATCH 5/6] re-implement ErrorModel and EmptyModel classes. Completing migration to PhysicalName Moved JsonSchemaMapper to internal class in util.ts Removed dependency to IConstruct on IModel (and added linter exclusion in package.json) --- packages/@aws-cdk/aws-apigateway/README.md | 6 +- .../aws-apigateway/lib/json-schema.ts | 57 +------------ packages/@aws-cdk/aws-apigateway/lib/model.ts | 82 +++++++++++++------ .../aws-apigateway/lib/requestvalidator.ts | 2 +- packages/@aws-cdk/aws-apigateway/lib/util.ts | 55 +++++++++++++ packages/@aws-cdk/aws-apigateway/package.json | 4 +- .../aws-apigateway/test/test.restapi.ts | 36 ++++++++ 7 files changed, 158 insertions(+), 84 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index f910c5e7f6389..c949362c5039d 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -214,7 +214,7 @@ const integration = new LambdaIntegration(hello, { }, { // For errors, we check if the error message is not empty, get the error data - selectionPattern: '.+', + selectionPattern: '(\n|.)+', // We will set the response status code to 200 statusCode: "400", responseTemplates: { @@ -237,14 +237,14 @@ You can define validation models for your responses (and requests) // We define the JSON Schema for the transformed valid response const responseModel = api.addModel('ResponseModel', { contentType: "application/json", - name: 'ResponseModel', + modelName: 'ResponseModel', schema: { "$schema": "http://json-schema.org/draft-04/schema#", "title": "pollResponse", "type": "object", "properties": { "state": { "type": "string" }, "greeting": { "type": "string" } } } }); // We define the JSON Schema for the transformed error response const errorResponseModel = api.addModel('ErrorResponseModel', { contentType: "application/json", - name: 'ErrorResponseModel', + modelName: 'ErrorResponseModel', schema: { "$schema": "http://json-schema.org/draft-04/schema#", "title": "errorResponse", "type": "object", "properties": { "state": { "type": "string" }, "message": { "type": "string" } } } }); diff --git a/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts b/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts index 7c68717cd1d8c..9749269fb2302 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts @@ -1,4 +1,8 @@ export enum JsonSchemaVersion { + /** + * In API Gateway models are defined using the JSON schema draft 4. + * @see https://tools.ietf.org/html/draft-zyp-json-schema-04 + */ DRAFT4 = 'http://json-schema.org/draft-04/schema#', DRAFT7 = 'http://json-schema.org/draft-07/schema#' } @@ -70,56 +74,3 @@ export interface JsonSchema { readonly oneOf?: JsonSchema[]; readonly not?: JsonSchema; } - -export class JsonSchemaMapper { - /** - * Transforms naming of some properties to prefix with a $, where needed - * according to the JSON schema spec - * @param schema The JsonSchema object to transform for CloudFormation output - */ - public static toCfnJsonSchema(schema: JsonSchema): any { - const result = JsonSchemaMapper._toCfnJsonSchema(schema); - if (! ("$schema" in result)) { - result.$schema = JsonSchemaVersion.DRAFT7; - } - return result; - } - - private static readonly SchemaPropsWithPrefix: { [key: string]: string } = { - schema: '$schema', - ref: '$ref', - id: '$id' - }; - private static readonly SubSchemaProps: { [key: string]: boolean } = { - definitions: true, - items: true, - additionalItems: true, - contains: true, - properties: true, - additionalProperties: true, - patternProperties: true, - dependencies: true, - propertyNames: true - }; - - private static _toCfnJsonSchema(schema: any): any { - if (schema === null || schema === undefined) { - return schema; - } - if ((typeof(schema) === "string") || (typeof(schema) === "boolean") || (typeof(schema) === "number")) { - return schema; - } - if (Array.isArray(schema)) { - return schema.map((entry) => JsonSchemaMapper._toCfnJsonSchema(entry)); - } - if (typeof(schema) === "object") { - return Object.assign({}, ...Object.entries(schema).map((entry) => { - const key = entry[0]; - const newKey = (key in JsonSchemaMapper.SchemaPropsWithPrefix) ? JsonSchemaMapper.SchemaPropsWithPrefix[key] : key; - const value = (key in JsonSchemaMapper.SubSchemaProps) ? JsonSchemaMapper._toCfnJsonSchema(entry[1]) : entry[1]; - return { [newKey]: value }; - })); - } - return schema; - } -} diff --git a/packages/@aws-cdk/aws-apigateway/lib/model.ts b/packages/@aws-cdk/aws-apigateway/lib/model.ts index 34627855460a0..1d5f91e4d9ce8 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/model.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/model.ts @@ -1,33 +1,70 @@ -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, Resource } from '@aws-cdk/core'; import { CfnModel, CfnModelProps } from './apigateway.generated'; import jsonSchema = require('./json-schema'); import { IRestApi, RestApi } from './restapi'; +import util = require('./util'); -export interface IModel extends IResource { +export interface IModel { /** - * A name for the model. If you don't specify a name, AWS CloudFormation - * generates a unique physical ID and uses that ID for the model name. - * For more information, @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html. - * - * Important - * If you specify a name, you cannot perform updates that require replacement - * of this resource. You can perform updates that require no or some interruption. - * If you must replace the resource, specify a new name. + * Returns the model name, such as 'myModel' * * @attribute */ readonly modelId: string; } +/** + * Represents a reference to a REST API's Empty model, which is available + * as part of the model collection by default. This can be used for mapping + * JSON responses from an integration to what is returned to a client, + * where strong typing is not required. In the absence of any defined + * model, the Empty model will be used to return the response payload + * unmapped. + * + * Definition + * { + * "$schema" : "http://json-schema.org/draft-04/schema#", + * "title" : "Empty Schema", + * "type" : "object" + * } + * + * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models + */ +export class EmptyModel implements IModel { + public readonly modelId = 'Empty'; +} + +/** + * Represents a reference to a REST API's Error model, which is available + * as part of the model collection by default. This can be used for mapping + * error JSON responses from an integration to a client, where a simple + * generic message field is sufficient to map and return an error payload. + * + * Definition + * { + * "$schema" : "http://json-schema.org/draft-04/schema#", + * "title" : "Error Schema", + * "type" : "object", + * "properties" : { + * "message" : { "type" : "string" } + * } + * } + */ +export class ErrorModel implements IModel { + public readonly modelId = 'Error'; +} + export interface ModelOptions { /** - * The content type for the model. - * @default None + * The content type for the model. You can also force a + * content type in the request or response model mapping. + * + * @default - */ readonly contentType?: string; /** - * A description that identifies this model.. + * A description that identifies this model. * @default None */ readonly description?: string; @@ -84,7 +121,7 @@ export class Model extends Resource implements IModel { * } * } */ - public static readonly ERROR_MODEL: IModel = ({ modelId: "Error", stack: null, node: null } as unknown) as IModel; + public static readonly ERROR_MODEL: IModel = new ErrorModel(); /** * Represents a reference to a REST API's Empty model, which is available @@ -103,7 +140,7 @@ export class Model extends Resource implements IModel { * * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models */ - public static readonly EMPTY_MODEL: IModel = ({ modelId: "Empty", stack: null, node: null } as unknown) as IModel; + public static readonly EMPTY_MODEL: IModel = new EmptyModel(); public static fromModelName(scope: Construct, id: string, modelName: string): IModel { class Import extends Resource implements IModel { @@ -114,14 +151,7 @@ export class Model extends Resource implements IModel { } /** - * A name for the model. If you don't specify a name, AWS CloudFormation - * generates a unique physical ID and uses that ID for the model name. - * For more information, @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html. - * - * Important - * If you specify a name, you cannot perform updates that require replacement - * of this resource. You can perform updates that require no or some interruption. - * If you must replace the resource, specify a new name. + * Returns the model name, such as 'myModel' * * @attribute */ @@ -133,16 +163,16 @@ export class Model extends Resource implements IModel { }); const modelProps: CfnModelProps = { - name: props.modelName, + name: this.physicalName, restApiId: props.restApi.restApiId, contentType: props.contentType, description: props.description, - schema: jsonSchema.JsonSchemaMapper.toCfnJsonSchema(props.schema) + schema: util.JsonSchemaMapper.toCfnJsonSchema(props.schema) }; const resource = new CfnModel(this, 'Resource', modelProps); - this.modelId = resource.ref; + this.modelId = this.getResourceNameAttribute(resource.ref); const deployment = (props.restApi instanceof RestApi) ? props.restApi.latestDeployment : undefined; if (deployment) { diff --git a/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts index 5820d75d8d2fc..783ef74fbbe1c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/requestvalidator.ts @@ -69,7 +69,7 @@ export class RequestValidator extends Resource implements IRequestValidator { }); const validatorProps: CfnRequestValidatorProps = { - name: props.requestValidatorName, + name: this.physicalName, restApiId: props.restApi.restApiId, validateRequestBody: props.validateRequestBody, validateRequestParameters: props.validateRequestParameters diff --git a/packages/@aws-cdk/aws-apigateway/lib/util.ts b/packages/@aws-cdk/aws-apigateway/lib/util.ts index b42557a60a7c1..8a5e6a696eedf 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/util.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/util.ts @@ -1,4 +1,6 @@ import { format as formatUrl } from 'url'; +import jsonSchema = require('./json-schema'); + const ALLOWED_METHODS = [ 'ANY', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]; export function validateHttpMethod(method: string, messagePrefix: string = '') { @@ -73,3 +75,56 @@ export function validateInteger(property: number | undefined, messagePrefix: str throw new Error(`${messagePrefix} should be an integer`); } } + +export class JsonSchemaMapper { + /** + * Transforms naming of some properties to prefix with a $, where needed + * according to the JSON schema spec + * @param schema The JsonSchema object to transform for CloudFormation output + */ + public static toCfnJsonSchema(schema: jsonSchema.JsonSchema): any { + const result = JsonSchemaMapper._toCfnJsonSchema(schema); + if (! ("$schema" in result)) { + result.$schema = jsonSchema.JsonSchemaVersion.DRAFT4; + } + return result; + } + + private static readonly SchemaPropsWithPrefix: { [key: string]: string } = { + schema: '$schema', + ref: '$ref', + id: '$id' + }; + private static readonly SubSchemaProps: { [key: string]: boolean } = { + definitions: true, + items: true, + additionalItems: true, + contains: true, + properties: true, + additionalProperties: true, + patternProperties: true, + dependencies: true, + propertyNames: true + }; + + private static _toCfnJsonSchema(schema: any): any { + if (schema === null || schema === undefined) { + return schema; + } + if ((typeof(schema) === "string") || (typeof(schema) === "boolean") || (typeof(schema) === "number")) { + return schema; + } + if (Array.isArray(schema)) { + return schema.map((entry) => JsonSchemaMapper._toCfnJsonSchema(entry)); + } + if (typeof(schema) === "object") { + return Object.assign({}, ...Object.entries(schema).map((entry) => { + const key = entry[0]; + const newKey = (key in JsonSchemaMapper.SchemaPropsWithPrefix) ? JsonSchemaMapper.SchemaPropsWithPrefix[key] : key; + const value = (key in JsonSchemaMapper.SubSchemaProps) ? JsonSchemaMapper._toCfnJsonSchema(entry[1]) : entry[1]; + return { [newKey]: value }; + })); + } + return schema; + } +} diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index c3a0268096a4e..fb8d18790463a 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -101,7 +101,9 @@ "props-physical-name:@aws-cdk/aws-apigateway.ResourceProps", "props-physical-name:@aws-cdk/aws-apigateway.UsagePlanProps", "props-physical-name-type:@aws-cdk/aws-apigateway.StageProps.stageName", - "props-physical-name:@aws-cdk/aws-apigateway.LambdaRestApiProps" + "props-physical-name:@aws-cdk/aws-apigateway.LambdaRestApiProps", + "construct-interface-extends-iconstruct:@aws-cdk/aws-apigateway.IModel", + "resource-interface-extends-resource:@aws-cdk/aws-apigateway.IModel" ] }, "stability": "experimental" diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index a41e5478fbdb4..d501a9dc5e9d9 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -631,4 +631,40 @@ export = { test.done(); }, + + 'addRequestValidator is supported'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigateway.RestApi(stack, 'myapi'); + api.root.addMethod('OPTIONS'); + + // WHEN + api.addRequestValidator('params-validator', { + requestValidatorName: 'Parameters', + validateRequestBody: false, + validateRequestParameters: true + }); + api.addRequestValidator('body-validator', { + requestValidatorName: "Body", + validateRequestBody: true, + validateRequestParameters: false + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, + Name: "Parameters", + ValidateRequestBody: false, + ValidateRequestParameters: true + })); + + expect(stack).to(haveResource('AWS::ApiGateway::RequestValidator', { + RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, + Name: "Body", + ValidateRequestBody: true, + ValidateRequestParameters: false + })); + + test.done(); + } }; From a03608212964b15db6d1e2f1c9b56421ebd5df89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20L=C3=A9pine?= Date: Wed, 26 Jun 2019 16:54:00 +0200 Subject: [PATCH 6/6] @deprecated to old entries --- packages/@aws-cdk/aws-apigateway/lib/model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/aws-apigateway/lib/model.ts b/packages/@aws-cdk/aws-apigateway/lib/model.ts index 1d5f91e4d9ce8..ae5188c7e800c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/model.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/model.ts @@ -29,6 +29,7 @@ export interface IModel { * } * * @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models + * @deprecated You should use @see Model.EMPTY_MODEL */ export class EmptyModel implements IModel { public readonly modelId = 'Empty'; @@ -49,6 +50,7 @@ export class EmptyModel implements IModel { * "message" : { "type" : "string" } * } * } + * @deprecated You should use @see Model.ERROR_MODEL */ export class ErrorModel implements IModel { public readonly modelId = 'Error';