Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-apigateway): expand RestApi support to models, parameters and validators #2960

Merged
merged 17 commits into from
Jun 27, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: '.+',
// 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
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
125 changes: 125 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,125 @@
export enum JsonSchemaVersion {
eladb marked this conversation as resolved.
Show resolved Hide resolved
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 | string;
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;
}

export class JsonSchemaMapper {
julienlepine marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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;
}
}
36 changes: 32 additions & 4 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 { IModelRef } from './model';
import { IRequestValidatorRef } 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]: IModelRef };

/**
* The ID of the associated request validator.
*/
readonly requestValidator?: IRequestValidatorRef;
julienlepine marked this conversation as resolved.
Show resolved Hide resolved
}

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 @@ -229,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].modelId;
responseModels[contentType] = mr.responseModels[contentType].modelName;
}
}
}
Expand All @@ -243,6 +255,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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IModel } from './model';
import { IModelRef } from './model';

export interface MethodResponse {

Expand All @@ -24,5 +24,5 @@ export interface MethodResponse {
* resource name as the value.
* @default None
*/
readonly responseModels?: { [contentType: string]: IModel };
readonly responseModels?: { [contentType: string]: IModelRef };
}
Loading