Skip to content

Commit 5c11680

Browse files
author
Elad Ben-Israel
authored
feat(lambda-event-sources): "api" event source (#1742)
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.
1 parent 642c8a6 commit 5c11680

File tree

11 files changed

+424
-43
lines changed

11 files changed

+424
-43
lines changed

packages/@aws-cdk/aws-apigateway/lib/method.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class Method extends cdk.Construct {
7676

7777
this.resource = props.resource;
7878
this.restApi = props.resource.resourceApi;
79-
this.httpMethod = props.httpMethod;
79+
this.httpMethod = props.httpMethod.toUpperCase();
8080

8181
validateHttpMethod(this.httpMethod);
8282

@@ -87,7 +87,7 @@ export class Method extends cdk.Construct {
8787
const methodProps: CfnMethodProps = {
8888
resourceId: props.resource.resourceId,
8989
restApiId: this.restApi.restApiId,
90-
httpMethod: props.httpMethod,
90+
httpMethod: this.httpMethod,
9191
operationName: options.operationName || defaultMethodOptions.operationName,
9292
apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired,
9393
authorizationType: options.authorizationType || defaultMethodOptions.authorizationType || AuthorizationType.None,

packages/@aws-cdk/aws-apigateway/lib/resource.ts

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import { Method, MethodOptions } from './method';
55
import { RestApi } from './restapi';
66

77
export interface IRestApiResource extends cdk.IConstruct {
8+
/**
9+
* The parent of this resource or undefined for the root resource.
10+
*/
11+
readonly parentResource?: IRestApiResource;
12+
813
/**
914
* The rest API that this resource is part of.
1015
*
@@ -37,6 +42,17 @@ export interface IRestApiResource extends cdk.IConstruct {
3742
*/
3843
readonly defaultMethodOptions?: MethodOptions;
3944

45+
/**
46+
* Gets or create all resources leading up to the specified path.
47+
*
48+
* - Path may only start with "/" if this method is called on the root resource.
49+
* - All resources are created using default options.
50+
*
51+
* @param path The relative path
52+
* @returns a new or existing resource.
53+
*/
54+
resourceForPath(path: string): Resource;
55+
4056
/**
4157
* Defines a new child resource where this resource is the parent.
4258
* @param pathPart The path part for the child resource
@@ -45,6 +61,14 @@ export interface IRestApiResource extends cdk.IConstruct {
4561
*/
4662
addResource(pathPart: string, options?: ResourceOptions): Resource;
4763

64+
/**
65+
* Retrieves a child resource by path part.
66+
*
67+
* @param pathPart The path part of the child resource
68+
* @returns the child resource or undefined if not found
69+
*/
70+
getResource(pathPart: string): IRestApiResource | undefined;
71+
4872
/**
4973
* Adds a greedy proxy resource ("{proxy+}") and an ANY method to this route.
5074
* @param options Default integration and method options.
@@ -89,7 +113,71 @@ export interface ResourceProps extends ResourceOptions {
89113
pathPart: string;
90114
}
91115

92-
export class Resource extends cdk.Construct implements IRestApiResource {
116+
export abstract class ResourceBase extends cdk.Construct implements IRestApiResource {
117+
public abstract readonly parentResource?: IRestApiResource;
118+
public abstract readonly resourceApi: RestApi;
119+
public abstract readonly resourceId: string;
120+
public abstract readonly resourcePath: string;
121+
public abstract readonly defaultIntegration?: Integration;
122+
public abstract readonly defaultMethodOptions?: MethodOptions;
123+
124+
private readonly children: { [pathPart: string]: Resource } = { };
125+
126+
constructor(scope: cdk.Construct, id: string) {
127+
super(scope, id);
128+
}
129+
130+
public addResource(pathPart: string, options?: ResourceOptions): Resource {
131+
return new Resource(this, pathPart, { parent: this, pathPart, ...options });
132+
}
133+
134+
public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method {
135+
return new Method(this, httpMethod, { resource: this, httpMethod, integration, options });
136+
}
137+
138+
public addProxy(options?: ResourceOptions): ProxyResource {
139+
return new ProxyResource(this, '{proxy+}', { parent: this, ...options });
140+
}
141+
142+
public getResource(pathPart: string): IRestApiResource | undefined {
143+
return this.children[pathPart];
144+
}
145+
146+
public trackChild(pathPart: string, resource: Resource) {
147+
this.children[pathPart] = resource;
148+
}
149+
150+
public resourceForPath(path: string): Resource {
151+
if (!path) {
152+
return this;
153+
}
154+
155+
if (path.startsWith('/')) {
156+
if (this.resourcePath !== '/') {
157+
throw new Error(`Path may start with "/" only for the resource, but we are at: ${this.resourcePath}`);
158+
}
159+
160+
// trim trailing "/"
161+
return this.resourceForPath(path.substr(1));
162+
}
163+
164+
const parts = path.split('/');
165+
const next = parts.shift();
166+
if (!next || next === '') {
167+
throw new Error(`resourceForPath cannot be called with an empty path`);
168+
}
169+
170+
let resource = this.getResource(next);
171+
if (!resource) {
172+
resource = this.addResource(next);
173+
}
174+
175+
return resource.resourceForPath(parts.join('/'));
176+
}
177+
}
178+
179+
export class Resource extends ResourceBase {
180+
public readonly parentResource?: IRestApiResource;
93181
public readonly resourceApi: RestApi;
94182
public readonly resourceId: string;
95183
public readonly resourcePath: string;
@@ -101,6 +189,12 @@ export class Resource extends cdk.Construct implements IRestApiResource {
101189

102190
validateResourcePathPart(props.pathPart);
103191

192+
this.parentResource = props.parent;
193+
194+
if (props.parent instanceof ResourceBase) {
195+
props.parent.trackChild(props.pathPart, this);
196+
}
197+
104198
const resourceProps: CfnResourceProps = {
105199
restApiId: props.parent.resourceApi.restApiId,
106200
parentId: props.parent.resourceId,
@@ -130,18 +224,6 @@ export class Resource extends cdk.Construct implements IRestApiResource {
130224
...props.defaultMethodOptions
131225
};
132226
}
133-
134-
public addResource(pathPart: string, options?: ResourceOptions): Resource {
135-
return new Resource(this, pathPart, { parent: this, pathPart, ...options });
136-
}
137-
138-
public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method {
139-
return new Method(this, httpMethod, { resource: this, httpMethod, integration, options });
140-
}
141-
142-
public addProxy(options?: ResourceOptions): ProxyResource {
143-
return new ProxyResource(this, '{proxy+}', { parent: this, ...options });
144-
}
145227
}
146228

147229
export interface ProxyResourceProps extends ResourceOptions {
@@ -171,8 +253,6 @@ export class ProxyResource extends Resource {
171253
*/
172254
public readonly anyMethod?: Method;
173255

174-
private readonly parentResource: IRestApiResource;
175-
176256
constructor(scope: cdk.Construct, id: string, props: ProxyResourceProps) {
177257
super(scope, id, {
178258
parent: props.parent,
@@ -181,8 +261,6 @@ export class ProxyResource extends Resource {
181261
defaultMethodOptions: props.defaultMethodOptions,
182262
});
183263

184-
this.parentResource = props.parent;
185-
186264
const anyMethod = props.anyMethod !== undefined ? props.anyMethod : true;
187265
if (anyMethod) {
188266
this.anyMethod = this.addMethod('ANY');
@@ -192,7 +270,7 @@ export class ProxyResource extends Resource {
192270
public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method {
193271
// In case this proxy is mounted under the root, also add this method to
194272
// the root so that empty paths are proxied as well.
195-
if (this.parentResource.resourcePath === '/') {
273+
if (this.parentResource && this.parentResource.resourcePath === '/') {
196274
this.parentResource.addMethod(httpMethod);
197275
}
198276
return super.addMethod(httpMethod, integration, options);

packages/@aws-cdk/aws-apigateway/lib/restapi.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CfnAccount, CfnRestApi } from './apigateway.generated';
44
import { Deployment } from './deployment';
55
import { Integration } from './integration';
66
import { Method, MethodOptions } from './method';
7-
import { IRestApiResource, ProxyResource, Resource, ResourceOptions } from './resource';
7+
import { IRestApiResource, ResourceBase, ResourceOptions } from './resource';
88
import { Stage, StageOptions } from './stage';
99

1010
export interface RestApiImportProps {
@@ -221,25 +221,7 @@ export class RestApi extends cdk.Construct implements IRestApi {
221221
this.configureCloudWatchRole(resource);
222222
}
223223

224-
// configure the "root" resource
225-
this.root = {
226-
get dependencyRoots() { return [this]; },
227-
node: this.node,
228-
addResource: (pathPart: string, options?: ResourceOptions) => {
229-
return new Resource(this, pathPart, { parent: this.root, pathPart, ...options });
230-
},
231-
addMethod: (httpMethod: string, integration?: Integration, options?: MethodOptions) => {
232-
return new Method(this, httpMethod, { resource: this.root, httpMethod, integration, options });
233-
},
234-
addProxy: (options?: ResourceOptions) => {
235-
return new ProxyResource(this, '{proxy+}', { parent: this.root, ...options });
236-
},
237-
defaultIntegration: props.defaultIntegration,
238-
defaultMethodOptions: props.defaultMethodOptions,
239-
resourceApi: this,
240-
resourceId: resource.restApiRootResourceId,
241-
resourcePath: '/'
242-
};
224+
this.root = new RootResource(this, props, resource.restApiRootResourceId);
243225
}
244226

245227
/**
@@ -406,3 +388,23 @@ class ImportedRestApi extends cdk.Construct implements IRestApi {
406388
return this.props;
407389
}
408390
}
391+
392+
class RootResource extends ResourceBase {
393+
public readonly parentResource?: IRestApiResource;
394+
public readonly resourceApi: RestApi;
395+
public readonly resourceId: string;
396+
public readonly resourcePath: string;
397+
public readonly defaultIntegration?: Integration | undefined;
398+
public readonly defaultMethodOptions?: MethodOptions | undefined;
399+
400+
constructor(api: RestApi, props: RestApiProps, resourceId: string) {
401+
super(api, 'Default');
402+
403+
this.parentResource = undefined;
404+
this.defaultIntegration = props.defaultIntegration;
405+
this.defaultMethodOptions = props.defaultMethodOptions;
406+
this.resourceApi = api;
407+
this.resourceId = resourceId;
408+
this.resourcePath = '/';
409+
}
410+
}

packages/@aws-cdk/aws-apigateway/lib/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { format as formatUrl } from 'url';
22
const ALLOWED_METHODS = [ 'ANY', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ];
33

44
export function validateHttpMethod(method: string, messagePrefix: string = '') {
5-
if (!ALLOWED_METHODS.includes(method.toUpperCase())) {
5+
if (!ALLOWED_METHODS.includes(method)) {
66
throw new Error(`${messagePrefix}Invalid HTTP method "${method}". Allowed methods: ${ALLOWED_METHODS.join(',')}`);
77
}
88
}

packages/@aws-cdk/aws-apigateway/test/test.method.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,5 +348,22 @@ export = {
348348
}
349349
}));
350350
test.done();
351+
},
352+
353+
'method is always set as uppercase'(test: Test) {
354+
// GIVEN
355+
const stack = new cdk.Stack();
356+
const api = new apigateway.RestApi(stack, 'api');
357+
358+
// WHEN
359+
api.root.addMethod('get');
360+
api.root.addMethod('PoSt');
361+
api.root.addMethod('PUT');
362+
363+
// THEN
364+
expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "POST" }));
365+
expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "GET" }));
366+
expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: "PUT" }));
367+
test.done();
351368
}
352369
};

0 commit comments

Comments
 (0)