Skip to content

Commit

Permalink
feat(aws-apigateway): expand RestApi support to models, parameters an…
Browse files Browse the repository at this point in the history
…d validators (#2960)

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 instead of name of current construct
Adds support for JsonSchema in Model
Aligns Model to the PhysicalName convention.
No breaking change, documentation updated
  • Loading branch information
julienlepine authored and Elad Ben-Israel committed Jun 27, 2019
1 parent b84caab commit 12e6380
Show file tree
Hide file tree
Showing 13 changed files with 926 additions and 41 deletions.
144 changes: 141 additions & 3 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,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: '(\n|.)+',
// 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",
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",
modelName: '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
Expand Down Expand Up @@ -259,12 +400,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)

----

Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export * from './usage-plan';
export * from './vpc-link';
export * from './methodresponse';
export * from './model';
export * from './requestvalidator';
export * from './authorizer';
export * from './json-schema';

// AWS::ApiGateway CloudFormation Resources:
export * from './apigateway.generated';
Expand Down
76 changes: 76 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/json-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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#'
}

export enum JsonSchemaType {
NULL = "null",
BOOLEAN = "boolean",
OBJECT = "object",
ARRAY = "array",
NUMBER = "number",
INTEGER = "integer",
STRING = "string"
}

/**
* Represents a JSON schema definition of the structure of a
* REST API model. Copied from npm module jsonschema.
*
* @see http://json-schema.org/
* @see https://github.com/tdegrunt/jsonschema
*/
export interface JsonSchema {
// Special keywords
readonly schema?: JsonSchemaVersion;
readonly id?: string;
readonly ref?: string;

// Common properties
readonly type?: JsonSchemaType | JsonSchemaType[];
readonly title?: string;
readonly description?: string;
readonly 'enum'?: any[];
readonly format?: string;
readonly definitions?: { [name: string]: JsonSchema };

// Number or Integer
readonly multipleOf?: number;
readonly maximum?: number;
readonly exclusiveMaximum?: boolean;
readonly minimum?: number;
readonly exclusiveMinimum?: boolean;

// String
readonly maxLength?: number;
readonly minLength?: number;
readonly pattern?: string;

// Array
readonly items?: JsonSchema | JsonSchema[];
readonly additionalItems?: JsonSchema[];
readonly maxItems?: number;
readonly minItems?: number;
readonly uniqueItems?: boolean;
readonly contains?: JsonSchema | JsonSchema[];

// Object
readonly maxProperties?: number;
readonly minProperties?: number;
readonly required?: string[];
readonly properties?: { [name: string]: JsonSchema };
readonly additionalProperties?: JsonSchema;
readonly patternProperties?: { [name: string]: JsonSchema };
readonly dependencies?: { [name: string]: JsonSchema | string[] };
readonly propertyNames?: JsonSchema;

// Conditional
readonly allOf?: JsonSchema[];
readonly anyOf?: JsonSchema[];
readonly oneOf?: JsonSchema[];
readonly not?: JsonSchema;
}
34 changes: 31 additions & 3 deletions packages/@aws-cdk/aws-apigateway/lib/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { IAuthorizer } from './authorizer';
import { ConnectionType, Integration } from './integration';
import { MockIntegration } from './integrations/mock';
import { MethodResponse } from './methodresponse';
import { IModel } from './model';
import { IRequestValidator } from './requestvalidator';
import { IResource } from './resource';
import { RestApi } from './restapi';
import { validateHttpMethod } from './util';
Expand Down Expand Up @@ -54,9 +56,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]: IModel };

/**
* The ID of the associated request validator.
*/
readonly requestValidator?: IRequestValidator;
}

export interface MethodProps {
Expand Down Expand Up @@ -119,6 +129,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);
Expand Down Expand Up @@ -243,6 +255,22 @@ export class Method extends Resource {
return methodResponseProp;
});
}

private renderRequestModels(requestModels: { [param: string]: IModel } | 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].modelId;
}
}

return models;
}
}

export enum AuthorizationType {
Expand Down
Loading

0 comments on commit 12e6380

Please sign in to comment.