-
Notifications
You must be signed in to change notification settings - Fork 4k
/
api.ts
496 lines (431 loc) · 14 KB
/
api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch';
import { Duration } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnApi, CfnApiProps } from '../apigatewayv2.generated';
import { IApi } from '../common/api';
import { ApiBase } from '../common/base';
import { DomainMappingOptions, IStage } from '../common/stage';
import { IHttpRouteAuthorizer } from './authorizer';
import { IHttpRouteIntegration, HttpIntegration, HttpRouteIntegrationConfig } from './integration';
import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route';
import { HttpStage, HttpStageOptions } from './stage';
import { VpcLink, VpcLinkProps } from './vpc-link';
/**
* Represents an HTTP API
*/
export interface IHttpApi extends IApi {
/**
* The identifier of this API Gateway HTTP API.
* @attribute
* @deprecated - use apiId instead
*/
readonly httpApiId: string;
/**
* Metric for the number of client-side errors captured in a given period.
*
* @default - sum over 5 minutes
*/
metricClientError(props?: MetricOptions): Metric;
/**
* Metric for the number of server-side errors captured in a given period.
*
* @default - sum over 5 minutes
*/
metricServerError(props?: MetricOptions): Metric;
/**
* Metric for the amount of data processed in bytes.
*
* @default - sum over 5 minutes
*/
metricDataProcessed(props?: MetricOptions): Metric;
/**
* Metric for the total number API requests in a given period.
*
* @default - SampleCount over 5 minutes
*/
metricCount(props?: MetricOptions): Metric;
/**
* Metric for the time between when API Gateway relays a request to the backend
* and when it receives a response from the backend.
*
* @default - no statistic
*/
metricIntegrationLatency(props?: MetricOptions): Metric;
/**
* The time between when API Gateway receives a request from a client
* and when it returns a response to the client.
* The latency includes the integration latency and other API Gateway overhead.
*
* @default - no statistic
*/
metricLatency(props?: MetricOptions): Metric;
/**
* Add a new VpcLink
*/
addVpcLink(options: VpcLinkProps): VpcLink
/**
* Add a http integration
* @internal
*/
_addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration;
}
/**
* Properties to initialize an instance of `HttpApi`.
*/
export interface HttpApiProps {
/**
* Name for the HTTP API resoruce
* @default - id of the HttpApi construct.
*/
readonly apiName?: string;
/**
* The description of the API.
* @default - none
*/
readonly description?: string;
/**
* An integration that will be configured on the catch-all route ($default).
* @default - none
*/
readonly defaultIntegration?: IHttpRouteIntegration;
/**
* Whether a default stage and deployment should be automatically created.
* @default true
*/
readonly createDefaultStage?: boolean;
/**
* Specifies a CORS configuration for an API.
* @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html
* @default - CORS disabled.
*/
readonly corsPreflight?: CorsPreflightOptions;
/**
* Configure a custom domain with the API mapping resource to the HTTP API
*
* @default - no default domain mapping configured. meaningless if `createDefaultStage` is `false`.
*/
readonly defaultDomainMapping?: DomainMappingOptions;
/**
* Specifies whether clients can invoke your API using the default endpoint.
* By default, clients can invoke your API with the default
* `https://{api_id}.execute-api.{region}.amazonaws.com` endpoint. Enable
* this if you would like clients to use your custom domain name.
* @default false execute-api endpoint enabled.
*/
readonly disableExecuteApiEndpoint?: boolean;
/**
* Default Authorizer to applied to all routes in the gateway
*
* @default - No authorizer
*/
readonly defaultAuthorizer?: IHttpRouteAuthorizer;
/**
* Default OIDC scopes attached to all routes in the gateway, unless explicitly configured on the route.
*
* @default - no default authorization scopes
*/
readonly defaultAuthorizationScopes?: string[];
}
/**
* Supported CORS HTTP methods
*/
export enum CorsHttpMethod{
/** HTTP ANY */
ANY = '*',
/** HTTP DELETE */
DELETE = 'DELETE',
/** HTTP GET */
GET = 'GET',
/** HTTP HEAD */
HEAD = 'HEAD',
/** HTTP OPTIONS */
OPTIONS = 'OPTIONS',
/** HTTP PATCH */
PATCH = 'PATCH',
/** HTTP POST */
POST = 'POST',
/** HTTP PUT */
PUT = 'PUT',
}
/**
* Options for the CORS Configuration
*/
export interface CorsPreflightOptions {
/**
* Specifies whether credentials are included in the CORS request.
* @default false
*/
readonly allowCredentials?: boolean;
/**
* Represents a collection of allowed headers.
* @default - No Headers are allowed.
*/
readonly allowHeaders?: string[];
/**
* Represents a collection of allowed HTTP methods.
* @default - No Methods are allowed.
*/
readonly allowMethods?: CorsHttpMethod[];
/**
* Represents a collection of allowed origins.
* @default - No Origins are allowed.
*/
readonly allowOrigins?: string[];
/**
* Represents a collection of exposed headers.
* @default - No Expose Headers are allowed.
*/
readonly exposeHeaders?: string[];
/**
* The duration that the browser should cache preflight request results.
* @default Duration.seconds(0)
*/
readonly maxAge?: Duration;
}
/**
* Options for the Route with Integration resoruce
*/
export interface AddRoutesOptions extends BatchHttpRouteOptions {
/**
* The path at which all of these routes are configured.
*/
readonly path: string;
/**
* The HTTP methods to be configured
* @default HttpMethod.ANY
*/
readonly methods?: HttpMethod[];
/**
* Authorizer to be associated to these routes.
*
* Use NoneAuthorizer to remove the default authorizer for the api
*
* @default - uses the default authorizer if one is specified on the HttpApi
*/
readonly authorizer?: IHttpRouteAuthorizer;
/**
* The list of OIDC scopes to include in the authorization.
*
* These scopes will override the default authorization scopes on the gateway.
* Set to [] to remove default scopes
*
* @default - uses defaultAuthorizationScopes if configured on the API, otherwise none.
*/
readonly authorizationScopes?: string[];
}
abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that this is not exported
public abstract readonly apiId: string;
public abstract readonly httpApiId: string;
public abstract readonly apiEndpoint: string;
private vpcLinks: Record<string, VpcLink> = {};
public metricClientError(props?: MetricOptions): Metric {
return this.metric('4xx', { statistic: 'Sum', ...props });
}
public metricServerError(props?: MetricOptions): Metric {
return this.metric('5xx', { statistic: 'Sum', ...props });
}
public metricDataProcessed(props?: MetricOptions): Metric {
return this.metric('DataProcessed', { statistic: 'Sum', ...props });
}
public metricCount(props?: MetricOptions): Metric {
return this.metric('Count', { statistic: 'SampleCount', ...props });
}
public metricIntegrationLatency(props?: MetricOptions): Metric {
return this.metric('IntegrationLatency', props);
}
public metricLatency(props?: MetricOptions): Metric {
return this.metric('Latency', props);
}
public addVpcLink(options: VpcLinkProps): VpcLink {
const { vpcId } = options.vpc;
if (vpcId in this.vpcLinks) {
return this.vpcLinks[vpcId];
}
const count = Object.keys(this.vpcLinks).length + 1;
const vpcLink = new VpcLink(this, `VpcLink-${count}`, options);
this.vpcLinks[vpcId] = vpcLink;
return vpcLink;
}
/**
* @internal
*/
public _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration {
const { configHash, integration: existingIntegration } = this._integrationCache.getIntegration(scope, config);
if (existingIntegration) {
return existingIntegration as HttpIntegration;
}
const integration = new HttpIntegration(scope, `HttpIntegration-${configHash}`, {
httpApi: this,
integrationType: config.type,
integrationUri: config.uri,
method: config.method,
connectionId: config.connectionId,
connectionType: config.connectionType,
payloadFormatVersion: config.payloadFormatVersion,
});
this._integrationCache.saveIntegration(scope, config, integration);
return integration;
}
}
/**
* Attributes for importing an HttpApi into the CDK
*/
export interface HttpApiAttributes {
/**
* The identifier of the HttpApi
*/
readonly httpApiId: string;
/**
* The endpoint URL of the HttpApi
* @default - throws an error if apiEndpoint is accessed.
*/
readonly apiEndpoint?: string;
}
/**
* Create a new API Gateway HTTP API endpoint.
* @resource AWS::ApiGatewayV2::Api
*/
export class HttpApi extends HttpApiBase {
/**
* Import an existing HTTP API into this CDK app.
*/
public static fromHttpApiAttributes(scope: Construct, id: string, attrs: HttpApiAttributes): IHttpApi {
class Import extends HttpApiBase {
public readonly apiId = attrs.httpApiId;
public readonly httpApiId = attrs.httpApiId;
private readonly _apiEndpoint = attrs.apiEndpoint;
public get apiEndpoint(): string {
if (!this._apiEndpoint) {
throw new Error('apiEndpoint is not configured on the imported HttpApi.');
}
return this._apiEndpoint;
}
}
return new Import(scope, id);
}
/**
* A human friendly name for this HTTP API. Note that this is different from `httpApiId`.
*/
public readonly httpApiName?: string;
public readonly apiId: string;
public readonly httpApiId: string;
/**
* Specifies whether clients can invoke this HTTP API by using the default execute-api endpoint.
*/
public readonly disableExecuteApiEndpoint?: boolean;
/**
* The default stage of this API
*/
public readonly defaultStage: IStage | undefined;
private readonly _apiEndpoint: string;
private readonly defaultAuthorizer?: IHttpRouteAuthorizer;
private readonly defaultAuthorizationScopes?: string[];
constructor(scope: Construct, id: string, props?: HttpApiProps) {
super(scope, id);
this.httpApiName = props?.apiName ?? id;
this.disableExecuteApiEndpoint = props?.disableExecuteApiEndpoint;
let corsConfiguration: CfnApi.CorsProperty | undefined;
if (props?.corsPreflight) {
const cors = props.corsPreflight;
if (cors.allowOrigins && cors.allowOrigins.includes('*') && cors.allowCredentials) {
throw new Error("CORS preflight - allowCredentials is not supported when allowOrigin is '*'");
}
const {
allowCredentials,
allowHeaders,
allowMethods,
allowOrigins,
exposeHeaders,
maxAge,
} = props.corsPreflight;
corsConfiguration = {
allowCredentials,
allowHeaders,
allowMethods,
allowOrigins,
exposeHeaders,
maxAge: maxAge?.toSeconds(),
};
}
const apiProps: CfnApiProps = {
name: this.httpApiName,
protocolType: 'HTTP',
corsConfiguration,
description: props?.description,
disableExecuteApiEndpoint: this.disableExecuteApiEndpoint,
};
const resource = new CfnApi(this, 'Resource', apiProps);
this.apiId = resource.ref;
this.httpApiId = resource.ref;
this._apiEndpoint = resource.attrApiEndpoint;
this.defaultAuthorizer = props?.defaultAuthorizer;
this.defaultAuthorizationScopes = props?.defaultAuthorizationScopes;
if (props?.defaultIntegration) {
new HttpRoute(this, 'DefaultRoute', {
httpApi: this,
routeKey: HttpRouteKey.DEFAULT,
integration: props.defaultIntegration,
authorizer: props.defaultAuthorizer,
authorizationScopes: props.defaultAuthorizationScopes,
});
}
if (props?.createDefaultStage === undefined || props.createDefaultStage === true) {
this.defaultStage = new HttpStage(this, 'DefaultStage', {
httpApi: this,
autoDeploy: true,
domainMapping: props?.defaultDomainMapping,
});
// to ensure the domain is ready before creating the default stage
if (props?.defaultDomainMapping) {
this.defaultStage.node.addDependency(props.defaultDomainMapping.domainName);
}
}
if (props?.createDefaultStage === false && props.defaultDomainMapping) {
throw new Error('defaultDomainMapping not supported with createDefaultStage disabled',
);
}
}
/**
* Get the default endpoint for this API.
*/
public get apiEndpoint(): string {
if (this.disableExecuteApiEndpoint) {
throw new Error('apiEndpoint is not accessible when disableExecuteApiEndpoint is set to true.');
}
return this._apiEndpoint;
}
/**
* Get the URL to the default stage of this API.
* Returns `undefined` if `createDefaultStage` is unset.
*/
public get url(): string | undefined {
return this.defaultStage ? this.defaultStage.url : undefined;
}
/**
* Add a new stage.
*/
public addStage(id: string, options: HttpStageOptions): HttpStage {
const stage = new HttpStage(this, id, {
httpApi: this,
...options,
});
return stage;
}
/**
* Add multiple routes that uses the same configuration. The routes all go to the same path, but for different
* methods.
*/
public addRoutes(options: AddRoutesOptions): HttpRoute[] {
const methods = options.methods ?? [HttpMethod.ANY];
return methods.map((method) => {
const authorizationScopes = options.authorizationScopes ?? this.defaultAuthorizationScopes;
return new HttpRoute(this, `${method}${options.path}`, {
httpApi: this,
routeKey: HttpRouteKey.with(options.path, method),
integration: options.integration,
authorizer: options.authorizer ?? this.defaultAuthorizer,
authorizationScopes,
});
});
}
}