Skip to content

Commit

Permalink
feat(lambda-event-sources): "api" event source (#1742)
Browse files Browse the repository at this point in the history
Introduce an "api" lambda event source which will implicit create an
API Gateway API and connect the resource in the specified path + method
to the Lambda target.

Added `resourceForPath` which creates a new resource (and all intermediate 
resources) mounted at a specified path in the API model.

Added `getResource` can be used to find a resource by path part.

Introduce an abstract `ResourceBase` which is used to share the implementation
between the root resource and all other resources.

Convert HTTP methods to uppercase.
  • Loading branch information
Elad Ben-Israel committed Feb 12, 2019
1 parent 642c8a6 commit 5c11680
Show file tree
Hide file tree
Showing 11 changed files with 424 additions and 43 deletions.
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-apigateway/lib/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class Method extends cdk.Construct {

this.resource = props.resource;
this.restApi = props.resource.resourceApi;
this.httpMethod = props.httpMethod;
this.httpMethod = props.httpMethod.toUpperCase();

validateHttpMethod(this.httpMethod);

Expand All @@ -87,7 +87,7 @@ export class Method extends cdk.Construct {
const methodProps: CfnMethodProps = {
resourceId: props.resource.resourceId,
restApiId: this.restApi.restApiId,
httpMethod: props.httpMethod,
httpMethod: this.httpMethod,
operationName: options.operationName || defaultMethodOptions.operationName,
apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired,
authorizationType: options.authorizationType || defaultMethodOptions.authorizationType || AuthorizationType.None,
Expand Down
114 changes: 96 additions & 18 deletions packages/@aws-cdk/aws-apigateway/lib/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { Method, MethodOptions } from './method';
import { RestApi } from './restapi';

export interface IRestApiResource extends cdk.IConstruct {
/**
* The parent of this resource or undefined for the root resource.
*/
readonly parentResource?: IRestApiResource;

/**
* The rest API that this resource is part of.
*
Expand Down Expand Up @@ -37,6 +42,17 @@ export interface IRestApiResource extends cdk.IConstruct {
*/
readonly defaultMethodOptions?: MethodOptions;

/**
* Gets or create all resources leading up to the specified path.
*
* - Path may only start with "/" if this method is called on the root resource.
* - All resources are created using default options.
*
* @param path The relative path
* @returns a new or existing resource.
*/
resourceForPath(path: string): Resource;

/**
* Defines a new child resource where this resource is the parent.
* @param pathPart The path part for the child resource
Expand All @@ -45,6 +61,14 @@ export interface IRestApiResource extends cdk.IConstruct {
*/
addResource(pathPart: string, options?: ResourceOptions): Resource;

/**
* Retrieves a child resource by path part.
*
* @param pathPart The path part of the child resource
* @returns the child resource or undefined if not found
*/
getResource(pathPart: string): IRestApiResource | undefined;

/**
* Adds a greedy proxy resource ("{proxy+}") and an ANY method to this route.
* @param options Default integration and method options.
Expand Down Expand Up @@ -89,7 +113,71 @@ export interface ResourceProps extends ResourceOptions {
pathPart: string;
}

export class Resource extends cdk.Construct implements IRestApiResource {
export abstract class ResourceBase extends cdk.Construct implements IRestApiResource {
public abstract readonly parentResource?: IRestApiResource;
public abstract readonly resourceApi: RestApi;
public abstract readonly resourceId: string;
public abstract readonly resourcePath: string;
public abstract readonly defaultIntegration?: Integration;
public abstract readonly defaultMethodOptions?: MethodOptions;

private readonly children: { [pathPart: string]: Resource } = { };

constructor(scope: cdk.Construct, id: string) {
super(scope, id);
}

public addResource(pathPart: string, options?: ResourceOptions): Resource {
return new Resource(this, pathPart, { parent: this, pathPart, ...options });
}

public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method {
return new Method(this, httpMethod, { resource: this, httpMethod, integration, options });
}

public addProxy(options?: ResourceOptions): ProxyResource {
return new ProxyResource(this, '{proxy+}', { parent: this, ...options });
}

public getResource(pathPart: string): IRestApiResource | undefined {
return this.children[pathPart];
}

public trackChild(pathPart: string, resource: Resource) {
this.children[pathPart] = resource;
}

public resourceForPath(path: string): Resource {
if (!path) {
return this;
}

if (path.startsWith('/')) {
if (this.resourcePath !== '/') {
throw new Error(`Path may start with "/" only for the resource, but we are at: ${this.resourcePath}`);
}

// trim trailing "/"
return this.resourceForPath(path.substr(1));
}

const parts = path.split('/');
const next = parts.shift();
if (!next || next === '') {
throw new Error(`resourceForPath cannot be called with an empty path`);
}

let resource = this.getResource(next);
if (!resource) {
resource = this.addResource(next);
}

return resource.resourceForPath(parts.join('/'));
}
}

export class Resource extends ResourceBase {
public readonly parentResource?: IRestApiResource;
public readonly resourceApi: RestApi;
public readonly resourceId: string;
public readonly resourcePath: string;
Expand All @@ -101,6 +189,12 @@ export class Resource extends cdk.Construct implements IRestApiResource {

validateResourcePathPart(props.pathPart);

this.parentResource = props.parent;

if (props.parent instanceof ResourceBase) {
props.parent.trackChild(props.pathPart, this);
}

const resourceProps: CfnResourceProps = {
restApiId: props.parent.resourceApi.restApiId,
parentId: props.parent.resourceId,
Expand Down Expand Up @@ -130,18 +224,6 @@ export class Resource extends cdk.Construct implements IRestApiResource {
...props.defaultMethodOptions
};
}

public addResource(pathPart: string, options?: ResourceOptions): Resource {
return new Resource(this, pathPart, { parent: this, pathPart, ...options });
}

public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method {
return new Method(this, httpMethod, { resource: this, httpMethod, integration, options });
}

public addProxy(options?: ResourceOptions): ProxyResource {
return new ProxyResource(this, '{proxy+}', { parent: this, ...options });
}
}

export interface ProxyResourceProps extends ResourceOptions {
Expand Down Expand Up @@ -171,8 +253,6 @@ export class ProxyResource extends Resource {
*/
public readonly anyMethod?: Method;

private readonly parentResource: IRestApiResource;

constructor(scope: cdk.Construct, id: string, props: ProxyResourceProps) {
super(scope, id, {
parent: props.parent,
Expand All @@ -181,8 +261,6 @@ export class ProxyResource extends Resource {
defaultMethodOptions: props.defaultMethodOptions,
});

this.parentResource = props.parent;

const anyMethod = props.anyMethod !== undefined ? props.anyMethod : true;
if (anyMethod) {
this.anyMethod = this.addMethod('ANY');
Expand All @@ -192,7 +270,7 @@ export class ProxyResource extends Resource {
public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method {
// In case this proxy is mounted under the root, also add this method to
// the root so that empty paths are proxied as well.
if (this.parentResource.resourcePath === '/') {
if (this.parentResource && this.parentResource.resourcePath === '/') {
this.parentResource.addMethod(httpMethod);
}
return super.addMethod(httpMethod, integration, options);
Expand Down
42 changes: 22 additions & 20 deletions packages/@aws-cdk/aws-apigateway/lib/restapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CfnAccount, CfnRestApi } from './apigateway.generated';
import { Deployment } from './deployment';
import { Integration } from './integration';
import { Method, MethodOptions } from './method';
import { IRestApiResource, ProxyResource, Resource, ResourceOptions } from './resource';
import { IRestApiResource, ResourceBase, ResourceOptions } from './resource';
import { Stage, StageOptions } from './stage';

export interface RestApiImportProps {
Expand Down Expand Up @@ -221,25 +221,7 @@ export class RestApi extends cdk.Construct implements IRestApi {
this.configureCloudWatchRole(resource);
}

// configure the "root" resource
this.root = {
get dependencyRoots() { return [this]; },
node: this.node,
addResource: (pathPart: string, options?: ResourceOptions) => {
return new Resource(this, pathPart, { parent: this.root, pathPart, ...options });
},
addMethod: (httpMethod: string, integration?: Integration, options?: MethodOptions) => {
return new Method(this, httpMethod, { resource: this.root, httpMethod, integration, options });
},
addProxy: (options?: ResourceOptions) => {
return new ProxyResource(this, '{proxy+}', { parent: this.root, ...options });
},
defaultIntegration: props.defaultIntegration,
defaultMethodOptions: props.defaultMethodOptions,
resourceApi: this,
resourceId: resource.restApiRootResourceId,
resourcePath: '/'
};
this.root = new RootResource(this, props, resource.restApiRootResourceId);
}

/**
Expand Down Expand Up @@ -406,3 +388,23 @@ class ImportedRestApi extends cdk.Construct implements IRestApi {
return this.props;
}
}

class RootResource extends ResourceBase {
public readonly parentResource?: IRestApiResource;
public readonly resourceApi: RestApi;
public readonly resourceId: string;
public readonly resourcePath: string;
public readonly defaultIntegration?: Integration | undefined;
public readonly defaultMethodOptions?: MethodOptions | undefined;

constructor(api: RestApi, props: RestApiProps, resourceId: string) {
super(api, 'Default');

this.parentResource = undefined;
this.defaultIntegration = props.defaultIntegration;
this.defaultMethodOptions = props.defaultMethodOptions;
this.resourceApi = api;
this.resourceId = resourceId;
this.resourcePath = '/';
}
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-apigateway/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { format as formatUrl } from 'url';
const ALLOWED_METHODS = [ 'ANY', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ];

export function validateHttpMethod(method: string, messagePrefix: string = '') {
if (!ALLOWED_METHODS.includes(method.toUpperCase())) {
if (!ALLOWED_METHODS.includes(method)) {
throw new Error(`${messagePrefix}Invalid HTTP method "${method}". Allowed methods: ${ALLOWED_METHODS.join(',')}`);
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-apigateway/test/test.method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,5 +348,22 @@ export = {
}
}));
test.done();
},

'method is always set as uppercase'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'api');

// WHEN
api.root.addMethod('get');
api.root.addMethod('PoSt');
api.root.addMethod('PUT');

// THEN
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();
}
};
Loading

0 comments on commit 5c11680

Please sign in to comment.