From a84c366daeaf1c28a07d8e48fdfe0d81d9eafbed Mon Sep 17 00:00:00 2001 From: Benjamin Giles Date: Mon, 13 Apr 2020 10:04:29 -0700 Subject: [PATCH 01/11] Implement API Gateway definition L2 construct --- .../aws-apigateway/lib/apidefinition.ts | 288 ++++++++++++++++++ packages/@aws-cdk/aws-apigateway/lib/index.ts | 1 + .../@aws-cdk/aws-apigateway/lib/restapi.ts | 24 ++ packages/@aws-cdk/aws-apigateway/package.json | 8 + .../test/integ.restapi.fromdefinition.ts | 13 + .../test/sample-definition.yaml | 111 +++++++ .../aws-apigateway/test/test.apidefinition.ts | 194 ++++++++++++ yarn.lock | 155 +++++++++- 8 files changed, 780 insertions(+), 14 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts b/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts new file mode 100644 index 0000000000000..3d06aa713bec8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts @@ -0,0 +1,288 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; +import { CfnRestApi } from './apigateway.generated'; + +export abstract class APIDefinition { + /** + * @returns `APIDefinitionS3` associated with the specified S3 object. + * @param bucket The S3 bucket + * @param key The object key + * @param objectVersion Optional S3 object version + */ + public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3APIDefinition { + return new S3APIDefinition(bucket, key, objectVersion); + } + + /** + * @returns `InlineAPIDefinition` with inline specification. + * @param code The actual API specification (limited to 4KiB) + */ + public static fromInline(code: string): InlineAPIDefinition { + return new InlineAPIDefinition(code); + } + + /** + * Loads the API specification from a local disk asset. + * @param file The path to the JSON or YAML specification file + */ + public static fromAsset(file: string, options?: s3_assets.AssetOptions): AssetAPIDefinition { + return new AssetAPIDefinition(file, options); + } + + /** + * Creates an OpenAPI/Swagger specification source using CloudFormation parameters. + * + * @returns a new instance of `CfnParametersAPIDefinition` + * @param props optional construction properties of {@link CfnParametersAPIDefinition} + */ + public static fromCfnParameters(props?: CfnParametersAPIDefinitionProps): CfnParametersAPIDefinition { + return new CfnParametersAPIDefinition(props); + } + + /** + * Determines whether this API definition is inline or not. + */ + public abstract readonly isInline: boolean; + + /** + * Called when the specification is initialized to allow this object to bind + * to the stack, add resources and have fun. + * + * @param scope The binding scope. Don't be smart about trying to down-cast or + * assume it's initialized. You may just use it as a construct scope. + */ + public abstract bind(scope: cdk.Construct): APIDefinitionConfig; + + /** + * Called after the CFN function resource has been created to allow the API definition + * class to bind to it. Specifically it's required to allow assets to add + * metadata for tooling like SAM CLI to be able to find their origins. + */ + public bindToResource(_resource: cdk.CfnResource, _options?: ResourceBindOptions) { + return; + } +} + +export interface APIDefinitionConfig { + /** + * The location of the specification in S3 (mutually exclusive with `inlineDefinition`). + * + * @default a new parameter will be created + */ + readonly s3Location?: CfnRestApi.S3LocationProperty; + + /** + * Inline specification (mutually exclusive with `s3Location`). + * + * @default a new parameter will be created + */ + readonly inlineDefinition?: string; +} + +/** + * Swagger/OpenAPI specification from an S3 archive + */ +export class S3APIDefinition extends APIDefinition { + public readonly isInline = false; + private bucketName: string; + + constructor(bucket: s3.IBucket, private key: string, private objectVersion?: string) { + super(); + + if (!bucket.bucketName) { + throw new Error('bucketName is undefined for the provided bucket'); + } + + this.bucketName = bucket.bucketName; + } + + public bind(_scope: cdk.Construct): APIDefinitionConfig { + return { + s3Location: { + bucket: this.bucketName, + key: this.key, + version: this.objectVersion + } + }; + } +} + +/** + * Swagger/OpenAPI specification from an inline string (limited to 4KiB) + */ +export class InlineAPIDefinition extends APIDefinition { + public readonly isInline = true; + + constructor(private definition: string) { + super(); + + if (definition.length === 0) { + throw new Error('Inline API definition cannot be empty'); + } + + if (definition.length > 4906) { + throw new Error('API definition is too large, must be <= 4096 but is ' + definition.length); + } + } + + public bind(_scope: cdk.Construct): APIDefinitionConfig { + return { + inlineDefinition: this.definition + }; + } +} + +/** + * Swagger/OpenAPI specification from a local file. + */ +export class AssetAPIDefinition extends APIDefinition { + public readonly isInline = false; + private asset?: s3_assets.Asset; + + /** + * @param path The path to the asset file + */ + constructor(public readonly path: string, private readonly options: s3_assets.AssetOptions = { }) { + super(); + } + + public bind(scope: cdk.Construct): APIDefinitionConfig { + // If the same AssetAPIDefinition is used multiple times, retain only the first instantiation. + if (this.asset === undefined) { + this.asset = new s3_assets.Asset(scope, 'APIDefinition', { + path: this.path, + ...this.options + }); + } + + if (this.asset.isZipArchive) { + throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`); + } + + return { + s3Location: { + bucket: this.asset.s3BucketName, + key: this.asset.s3ObjectKey + } + }; + } + + public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) { + if (!this.asset) { + throw new Error('bindToResource() must be called after bind()'); + } + + const resourceProperty = options.resourceProperty || 'APIDefinition'; + + // https://github.com/aws/aws-cdk/issues/1432 + this.asset.addResourceMetadata(resource, resourceProperty); + } +} + +export interface ResourceBindOptions { + /** + * The name of the CloudFormation property to annotate with asset metadata. + * @see https://github.com/aws/aws-cdk/issues/1432 + * @default Code + */ + readonly resourceProperty?: string; +} + +/** + * Construction properties for {@link CfnParametersAPIDefinition}. + */ +export interface CfnParametersAPIDefinitionProps { + /** + * The CloudFormation parameter that represents the name of the S3 Bucket + * where the API specification file is located. + * Must be of type 'String'. + * + * @default a new parameter will be created + */ + readonly bucketNameParam?: cdk.CfnParameter; + + /** + * The CloudFormation parameter that represents the path inside the S3 Bucket + * where the API specification file is located. + * Must be of type 'String'. + * + * @default a new parameter will be created + */ + readonly objectKeyParam?: cdk.CfnParameter; +} + +/** + * Swagger/OpenAPI specification using 2 CloudFormation parameters. + * Useful when you don't have access to the specification from your CDK code, so you can't use Assets, + * and you want to deploy a REST API with a pre-built spec in a CodePipeline, using CloudFormation Actions - + * you can fill the parameters using the {@link #assign} method. + */ +export class CfnParametersAPIDefinition extends APIDefinition { + public readonly isInline = false; + private _bucketNameParam?: cdk.CfnParameter; + private _objectKeyParam?: cdk.CfnParameter; + + constructor(props: CfnParametersAPIDefinitionProps = {}) { + super(); + + this._bucketNameParam = props.bucketNameParam; + this._objectKeyParam = props.objectKeyParam; + } + + public bind(scope: cdk.Construct): APIDefinitionConfig { + if (!this._bucketNameParam) { + this._bucketNameParam = new cdk.CfnParameter(scope, 'APIDefinitionBucketNameParameter', { + type: 'String', + }); + } + + if (!this._objectKeyParam) { + this._objectKeyParam = new cdk.CfnParameter(scope, 'APIDefinitionObjectKeyParameter', { + type: 'String', + }); + } + + return { + s3Location: { + bucket: this._bucketNameParam.valueAsString, + key: this._objectKeyParam.valueAsString, + } + }; + } + + /** + * Create a parameters map from this instance's CloudFormation parameters. + * + * It returns a map with 2 keys that correspond to the names of the parameters defined in this API definition, + * and as values it contains the appropriate expressions pointing at the provided S3 location + * (most likely, obtained from a CodePipeline Artifact by calling the `artifact.s3Location` method). + * The result should be provided to the CloudFormation Action + * that is deploying the Stack that the REST API with this definition is part of, + * in the `parameterOverrides` property. + * + * @param location the location of the object in S3 that represents the API definition + */ + public assign(location: s3.Location): { [name: string]: any } { + const ret: { [name: string]: any } = {}; + ret[this.bucketNameParam] = location.bucketName; + ret[this.objectKeyParam] = location.objectKey; + return ret; + } + + public get bucketNameParam(): string { + if (this._bucketNameParam) { + return this._bucketNameParam.logicalId; + } else { + throw new Error('Pass CfnParametersAPIDefinition to a REST API before accessing the bucketNameParam property'); + } + } + + public get objectKeyParam(): string { + if (this._objectKeyParam) { + return this._objectKeyParam.logicalId; + } else { + throw new Error('Pass CfnParametersAPIDefinition to a REST API before accessing the objectKeyParam property'); + } + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index fe71c1492ab21..da5638b8f4c11 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -20,6 +20,7 @@ export * from './base-path-mapping'; export * from './cors'; export * from './authorizers'; export * from './access-log'; +export * from './apidefinition'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 6b86481b6e46f..4d930e5cd9837 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -2,6 +2,7 @@ import { IVpcEndpoint } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { CfnOutput, Construct, IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; import { ApiKey, IApiKey } from './api-key'; +import { APIDefinition, APIDefinitionConfig } from './apidefinition'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { CorsOptions } from './cors'; import { Deployment } from './deployment'; @@ -71,6 +72,14 @@ export interface RestApiProps extends ResourceOptions { */ readonly restApiName?: string; + /** + * A swagger or OpenAPI specification that defines a set of RESTful APIs in + * JSON or YAML format. + * + * @default - No API specification. + */ + readonly apiDefinition?: APIDefinition; + /** * Custom header parameters for the request. * @see https://docs.aws.amazon.com/cli/latest/reference/apigateway/import-rest-api.html @@ -228,6 +237,7 @@ export class RestApi extends Resource implements IRestApi { public readonly methods = new Array(); private _domainName?: DomainName; + private _apiDefinition?: APIDefinitionConfig; private _latestDeployment: Deployment | undefined; constructor(scope: Construct, id: string, props: RestApiProps = { }) { @@ -235,10 +245,17 @@ export class RestApi extends Resource implements IRestApi { physicalName: props.restApiName || id, }); + if (props.apiDefinition !== undefined) { + this._apiDefinition = props.apiDefinition.bind(this); + verifyAPIDefinitionConfig(this._apiDefinition); + } + const resource = new CfnRestApi(this, 'Resource', { name: this.physicalName, description: props.description, policy: props.policy, + body: props.apiDefinition?.isInline ? this._apiDefinition?.inlineDefinition : undefined, + bodyS3Location: props.apiDefinition?.isInline ? undefined : this._apiDefinition?.s3Location, failOnWarnings: props.failOnWarnings, minimumCompressionSize: props.minimumCompressionSize, binaryMediaTypes: props.binaryMediaTypes, @@ -533,3 +550,10 @@ class RootResource extends ResourceBase { } } } + +export function verifyAPIDefinitionConfig(definition: APIDefinitionConfig) { + // mutually exclusive + if ((!definition.inlineDefinition && !definition.s3Location) || (definition.inlineDefinition && definition.s3Location)) { + throw new Error('APIDefinition must specify one of "inlineDefinition" or "s3Location" but not both'); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 00a94d1449ee4..840cdc799fe3c 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -71,24 +71,32 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^2.0.0" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^2.0.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts new file mode 100644 index 0000000000000..658b5fe1fb1ce --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts @@ -0,0 +1,13 @@ +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as apigateway from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'test-apigateway-restapi-fromdefinition'); + +new apigateway.RestApi(stack, 'my-api', { + apiDefinition: apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')) +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml new file mode 100644 index 0000000000000..b935ceff25540 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml @@ -0,0 +1,111 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts b/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts new file mode 100644 index 0000000000000..f84be324a2bbf --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts @@ -0,0 +1,194 @@ +import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { Test } from 'nodeunit'; +import * as path from 'path'; +import * as apigateway from '../lib'; + +// tslint:disable:no-string-literal + +export = { + 'apigateway.APIDefinition.fromInline': { + 'fails if larger than 4096 bytes or is empty'(test: Test) { + test.throws( + () => defineRestApi(apigateway.APIDefinition.fromInline(generateRandomString(4097))), + /too large, must be <= 4096 but is 4097/); + test.throws( + () => defineRestApi(apigateway.APIDefinition.fromInline('')), + /cannot be empty/ + ); + test.done(); + }, + }, + 'apigateway.APIDefinition.fromAsset': { + 'fails if a zip asset is used'(test: Test) { + // GIVEN + const fileAsset = apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + + // THEN + test.throws(() => defineRestApi(fileAsset), /Asset cannot be a \.zip file or a directory/); + test.done(); + }, + + 'only one Asset object gets created even if multiple functions use the same AssetAPIDefinition'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'MyStack'); + const directoryAsset = apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + + // WHEN + new apigateway.RestApi(stack, 'API1', { + apiDefinition: directoryAsset + }); + + new apigateway.RestApi(stack, 'API2', { + apiDefinition: directoryAsset + }); + + // THEN + const assembly = app.synth(); + const synthesized = assembly.stacks[0]; + + // API1 has an asset, API2 does not + test.deepEqual(synthesized.assets.length, 1); + test.done(); + }, + + 'adds definition asset metadata'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + const definition = apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + + // WHEN + new apigateway.RestApi(stack, 'API1', { + apiDefinition: definition + }); + + // THEN + expect(stack).to(haveResource('AWS::APIGateway::RestApi', { + Metadata: { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.9678c34eca93259d11f2d714177347afd66c50116e1e08996eff893d3ca81232', + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'APIDefinition' + } + }, ResourcePart.CompleteDefinition)); + test.done(); + } + }, + + 'apigateway.APIDefinition.fromCfnParameters': { + "automatically creates the Bucket and Key parameters when it's used in a Rest API"(test: Test) { + const stack = new cdk.Stack(); + const definition = new apigateway.CfnParametersAPIDefinition(); + new apigateway.RestApi(stack, 'API', { + apiDefinition: definition, + }); + + expect(stack).to(haveResourceLike('AWS::APIGateway::RestApi', { + BodyS3Location: { + Bucket: { + Ref: 'FunctionLambdaSourceBucketNameParameter9E9E108F', + }, + Key: { + Ref: 'FunctionLambdaSourceObjectKeyParameter1C7AED11', + }, + }, + })); + + test.equal(stack.resolve(definition.bucketNameParam), 'FunctionLambdaSourceBucketNameParameter9E9E108F'); + test.equal(stack.resolve(definition.objectKeyParam), 'FunctionLambdaSourceObjectKeyParameter1C7AED11'); + + test.done(); + }, + + 'does not allow accessing the Parameter properties before being used in a Rest API'(test: Test) { + const definition = new apigateway.CfnParametersAPIDefinition(); + + test.throws(() => { + test.notEqual(definition.bucketNameParam, undefined); + }, /bucketNameParam/); + + test.throws(() => { + test.notEqual(definition.objectKeyParam, undefined); + }, /objectKeyParam/); + + test.done(); + }, + + 'allows passing custom Parameters when creating it'(test: Test) { + const stack = new cdk.Stack(); + const bucketNameParam = new cdk.CfnParameter(stack, 'BucketNameParam', { + type: 'String', + }); + const bucketKeyParam = new cdk.CfnParameter(stack, 'ObjectKeyParam', { + type: 'String', + }); + + const definition = apigateway.APIDefinition.fromCfnParameters({ + bucketNameParam, + objectKeyParam: bucketKeyParam, + }); + + test.equal(stack.resolve(definition.bucketNameParam), 'BucketNameParam'); + test.equal(stack.resolve(definition.objectKeyParam), 'ObjectKeyParam'); + + new apigateway.RestApi(stack, 'API', { + apiDefinition: definition + }); + + expect(stack).to(haveResourceLike('AWS::APIGateway::RestApi', { + S3BodyLocation: { + Bucket: { + Ref: 'BucketNameParam', + }, + Key: { + Ref: 'ObjectKeyParam', + }, + }, + })); + + test.done(); + }, + + 'can assign parameters'(test: Test) { + // given + const stack = new cdk.Stack(); + const code = new apigateway.CfnParametersAPIDefinition({ + bucketNameParam: new cdk.CfnParameter(stack, 'BucketNameParam', { + type: 'String', + }), + objectKeyParam: new cdk.CfnParameter(stack, 'ObjectKeyParam', { + type: 'String', + }), + }); + + // when + const overrides = stack.resolve(code.assign({ + bucketName: 'SomeBucketName', + objectKey: 'SomeObjectKey', + })); + + // then + test.equal(overrides['BucketNameParam'], 'SomeBucketName'); + test.equal(overrides['ObjectKeyParam'], 'SomeObjectKey'); + + test.done(); + }, + }, +}; + +function defineRestApi(definition: apigateway.APIDefinition) { + const stack = new cdk.Stack(); + return new apigateway.RestApi(stack, 'API', { + apiDefinition: definition + }); +} + +function generateRandomString(bytes: number) { + let s = ''; + for (let i = 0; i < bytes; ++i) { + s += String.fromCharCode(Math.round(Math.random() * 256)); + } + return s; +} diff --git a/yarn.lock b/yarn.lock index 55456291e634a..6aefe2af57609 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2566,6 +2566,11 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +app-root-path@^2.2.1: + version "2.2.1" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" + integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== + append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -2799,7 +2804,7 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -aws-sdk-mock@^5.1.0: +aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-mock/-/aws-sdk-mock-5.1.0.tgz#6f2c0bd670d7f378c906a8dd806f812124db71aa" integrity sha512-Wa5eCSo8HX0Snqb7FdBylaXMmfrAWoWZ+d7MFhiYsgHPvNvMEGjV945FF2qqE1U0Tolr1ALzik1fcwgaOhqUWQ== @@ -2808,6 +2813,21 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" +aws-sdk@^2.596.0: + version "2.655.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/aws-sdk/-/aws-sdk-2.655.0.tgz#e95da28e66f02a4bfc0eab46731e2140364b3ea2" + integrity sha512-ywXbaPSwQ+YGo7ZGx7KnmoMO0O7fiEL+rttZIsx6AymLZUohfZ7GlRjG8z93jHa+22qWPMEJ+5UC05/PXWbf7Q== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sdk@^2.637.0, aws-sdk@^2.656.0: version "2.656.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.656.0.tgz#0d74664ddbf30701073be9f9913ee7266afef3b4" @@ -4758,11 +4778,21 @@ dotenv-expand@^5.1.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== +dotenv-json@^1.0.0: + version "1.0.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c" + integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ== + dotenv@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow== +dotenv@^8.0.0: + version "8.2.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + dotgitignore@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" @@ -4997,6 +5027,11 @@ escodegen@~1.9.0: optionalDependencies: source-map "~0.6.1" +eslint-config-standard@^14.1.0: + version "14.1.1" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" + integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== + eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" @@ -5024,7 +5059,15 @@ eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-import@^2.20.2: +eslint-plugin-es@^2.0.0: + version "2.0.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" + integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== + dependencies: + eslint-utils "^1.4.2" + regexpp "^3.0.0" + +eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: version "2.20.2" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== @@ -5042,6 +5085,28 @@ eslint-plugin-import@^2.20.2: read-pkg-up "^2.0.0" resolve "^1.12.0" +eslint-plugin-node@^10.0.0: + version "10.0.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" + integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== + dependencies: + eslint-plugin-es "^2.0.0" + eslint-utils "^1.4.2" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-promise@^4.2.1: + version "4.2.1" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" + integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== + +eslint-plugin-standard@^4.0.1: + version "4.0.1" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" + integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== + eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -5050,7 +5115,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.3: +eslint-utils@^1.4.2, eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== @@ -6248,6 +6313,11 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.1: + version "5.1.4" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" + integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -7974,6 +8044,24 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +lambda-leak@^2.0.0: + version "2.0.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e" + integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4= + +lambda-tester@^3.6.0: + version "3.6.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/lambda-tester/-/lambda-tester-3.6.0.tgz#ceb7d4f4f0da768487a05cff37dcd088508b5247" + integrity sha512-F2ZTGWCLyIR95o/jWK46V/WnOCFAEUG/m/V7/CLhPJ7PCM+pror1rZ6ujP3TkItSGxUfpJi0kqwidw+M/nEqWw== + dependencies: + app-root-path "^2.2.1" + dotenv "^8.0.0" + dotenv-json "^1.0.0" + lambda-leak "^2.0.0" + semver "^6.1.1" + uuid "^3.3.2" + vandium-utils "^1.1.1" + lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -8562,7 +8650,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -8623,6 +8711,13 @@ mkdirp@*, mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== +mkdirp@0.x: + version "0.5.5" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -8764,6 +8859,17 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" +nock@^11.7.0: + version "11.9.1" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/nock/-/nock-11.9.1.tgz#2b026c5beb6d0dbcb41e7e4cefa671bc36db9c61" + integrity sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash "^4.17.13" + mkdirp "^0.5.0" + propagate "^2.0.0" + nock@^12.0.3: version "12.0.3" resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" @@ -10821,7 +10927,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.x, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.4.0: +resolve@1.x, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.4.0: version "1.15.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== @@ -10985,12 +11091,12 @@ semver-intersect@^1.4.0: dependencies: semver "^5.0.0" -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.3.0, semver@6.x, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@6.3.0, semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -12143,6 +12249,22 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= +ts-jest@^24.2.0: + version "24.3.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" + integrity sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "0.x" + resolve "1.x" + semver "^5.5" + yargs-parser "10.x" + ts-jest@^25.3.1: version "25.3.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.3.1.tgz#58e2ed3506e4e4487c0b9b532846a5cade9656ba" @@ -12567,6 +12689,11 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +vandium-utils@^1.1.1: + version "1.2.0" + resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59" + integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k= + vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" @@ -12927,6 +13054,13 @@ yapool@^1.0.0: resolved "https://registry.yarnpkg.com/yapool/-/yapool-1.0.0.tgz#f693f29a315b50d9a9da2646a7a6645c96985b6a" integrity sha1-9pPymjFbUNmp2iZGp6ZkXJaYW2o= +yargs-parser@10.x, yargs-parser@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + yargs-parser@18.x, yargs-parser@^18.1.1: version "18.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1" @@ -12935,13 +13069,6 @@ yargs-parser@18.x, yargs-parser@^18.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@^13.0.0, yargs-parser@^13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" From 0dcd6c56e4392128a6d64fb78440178476dd1b93 Mon Sep 17 00:00:00 2001 From: Benjamin Giles Date: Wed, 15 Apr 2020 00:49:29 -0700 Subject: [PATCH 02/11] Add unit and integration tests, refine documentation --- .../aws-apigateway/lib/apidefinition.ts | 21 ++- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 4 +- ...integ.restapi.fromdefinition.expected.json | 156 ++++++++++++++++++ .../test/sample-definition.yaml | 119 +++---------- .../aws-apigateway/test/test.apidefinition.ts | 55 +++--- .../aws-apigateway/test/test.restapi.ts | 4 +- 6 files changed, 223 insertions(+), 136 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.expected.json diff --git a/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts b/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts index 3d06aa713bec8..95e1330258a09 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts @@ -3,8 +3,12 @@ import * as s3_assets from '@aws-cdk/aws-s3-assets'; import * as cdk from '@aws-cdk/core'; import { CfnRestApi } from './apigateway.generated'; +/** + * Represents a API Gateway Swagger or OpenAPI specification. + */ export abstract class APIDefinition { /** + * Creates an API definition from a specification file in an S3 bucket * @returns `APIDefinitionS3` associated with the specified S3 object. * @param bucket The S3 bucket * @param key The object key @@ -15,6 +19,7 @@ export abstract class APIDefinition { } /** + * Creates an API definition from a string * @returns `InlineAPIDefinition` with inline specification. * @param code The actual API specification (limited to 4KiB) */ @@ -64,6 +69,9 @@ export abstract class APIDefinition { } } +/** + * Post-Binding Configuration for a CDK construct + */ export interface APIDefinitionConfig { /** * The location of the specification in S3 (mutually exclusive with `inlineDefinition`). @@ -121,7 +129,7 @@ export class InlineAPIDefinition extends APIDefinition { throw new Error('Inline API definition cannot be empty'); } - if (definition.length > 4906) { + if (definition.length > 4096) { throw new Error('API definition is too large, must be <= 4096 but is ' + definition.length); } } @@ -156,20 +164,20 @@ export class AssetAPIDefinition extends APIDefinition { }); } - if (this.asset.isZipArchive) { + if (this.asset?.isZipArchive) { throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`); } return { s3Location: { - bucket: this.asset.s3BucketName, - key: this.asset.s3ObjectKey + bucket: this.asset?.s3BucketName, + key: this.asset?.s3ObjectKey } }; } public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) { - if (!this.asset) { + if (this.asset === undefined) { throw new Error('bindToResource() must be called after bind()'); } @@ -180,6 +188,9 @@ export class AssetAPIDefinition extends APIDefinition { } } +/** + * Post-Synthesis options + */ export interface ResourceBindOptions { /** * The name of the CloudFormation property to annotate with asset metadata. diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 4d930e5cd9837..2c64357f1fd4e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -265,7 +265,7 @@ export class RestApi extends Resource implements IRestApi { parameters: props.parameters }); this.node.defaultChild = resource; - + props.apiDefinition?.bindToResource(resource); this.restApiId = resource.ref; this.configureDeployment(props); @@ -411,7 +411,7 @@ export class RestApi extends Resource implements IRestApi { * Performs validation of the REST API. */ protected validate() { - if (this.methods.length === 0) { + if (this.methods.length === 0 && this._apiDefinition === undefined) { return [ 'The REST API doesn\'t contain any methods' ]; } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.expected.json new file mode 100644 index 0000000000000..a03d0395259c2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.expected.json @@ -0,0 +1,156 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BodyS3Location": { + "Bucket": { + "Ref": "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3BucketC3882222" + }, + "Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3VersionKey1FFB68C8" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3VersionKey1FFB68C8" + } + ] + } + ] + } + ] + ] + } + }, + "Name": "my-api" + } + }, + "myapiDeployment92F2CB49": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + } + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB49" + }, + "StageName": "prod" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3BucketC3882222": { + "Type": "String", + "Description": "S3 bucket for asset \"b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58\"" + }, + "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3VersionKey1FFB68C8": { + "Type": "String", + "Description": "S3 key for asset version \"b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58\"" + }, + "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58ArtifactHash1A35D17D": { + "Type": "String", + "Description": "Artifact hash for asset \"b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml index b935ceff25540..b40573a0c08b8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml +++ b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml @@ -1,111 +1,30 @@ -openapi: "3.0.0" +openapi: "3.0.2" info: version: 1.0.0 - title: Swagger Petstore - license: - name: MIT -servers: - - url: http://petstore.swagger.io/v1 + title: Test API for CDK paths: - /pets: + /testing: get: - summary: List all pets - operationId: listPets - tags: - - pets - parameters: - - name: limit - in: query - description: How many items to return at one time (max 100) - required: false - schema: - type: integer - format: int32 + summary: Test Method + operationId: testMethod responses: - '200': + "200": description: A paged array of pets - headers: - x-next: - description: A link to the next page of responses - schema: - type: string - content: - application/json: - schema: - $ref: "#/components/schemas/Pets" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - post: - summary: Create a pet - operationId: createPets - tags: - - pets - responses: - '201': - description: Null response - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /pets/{petId}: - get: - summary: Info for a specific pet - operationId: showPetById - tags: - - pets - parameters: - - name: petId - in: path - required: true - description: The id of the pet to retrieve - schema: - type: string - responses: - '200': - description: Expected response to a valid request - content: - application/json: - schema: - $ref: "#/components/schemas/Pet" - default: - description: unexpected error content: application/json: schema: - $ref: "#/components/schemas/Error" + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-integration: + responses: + default: + statusCode: "200" + requestTemplates: + application/json: "{\"statusCode\": 200}" + passthroughBehavior: when_no_match + type: mock + components: schemas: - Pet: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string - tag: - type: string - Pets: - type: array - items: - $ref: "#/components/schemas/Pet" - Error: - type: object - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string \ No newline at end of file + Empty: + title: Empty Schema + type: object \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts b/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts index f84be324a2bbf..11aa8192092ac 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts @@ -3,7 +3,7 @@ import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as path from 'path'; -import * as apigateway from '../lib'; +import * as apigw from '../lib'; // tslint:disable:no-string-literal @@ -11,19 +11,19 @@ export = { 'apigateway.APIDefinition.fromInline': { 'fails if larger than 4096 bytes or is empty'(test: Test) { test.throws( - () => defineRestApi(apigateway.APIDefinition.fromInline(generateRandomString(4097))), + () => defineRestApi(apigw.APIDefinition.fromInline(generateRandomString(4097))), /too large, must be <= 4096 but is 4097/); test.throws( - () => defineRestApi(apigateway.APIDefinition.fromInline('')), + () => defineRestApi(apigw.APIDefinition.fromInline('')), /cannot be empty/ ); test.done(); }, }, 'apigateway.APIDefinition.fromAsset': { - 'fails if a zip asset is used'(test: Test) { + 'fails if a directory is given for an asset'(test: Test) { // GIVEN - const fileAsset = apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + const fileAsset = apigw.APIDefinition.fromAsset(path.join(__dirname, 'authorizers')); // THEN test.throws(() => defineRestApi(fileAsset), /Asset cannot be a \.zip file or a directory/); @@ -34,14 +34,14 @@ export = { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyStack'); - const directoryAsset = apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + const directoryAsset = apigw.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); // WHEN - new apigateway.RestApi(stack, 'API1', { + new apigw.RestApi(stack, 'API1', { apiDefinition: directoryAsset }); - new apigateway.RestApi(stack, 'API2', { + new apigw.RestApi(stack, 'API2', { apiDefinition: directoryAsset }); @@ -59,17 +59,17 @@ export = { const stack = new cdk.Stack(); stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); - const definition = apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + const definition = apigw.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); // WHEN - new apigateway.RestApi(stack, 'API1', { + new apigw.RestApi(stack, 'API1', { apiDefinition: definition }); // THEN - expect(stack).to(haveResource('AWS::APIGateway::RestApi', { + expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { Metadata: { - [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.9678c34eca93259d11f2d714177347afd66c50116e1e08996eff893d3ca81232', + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58.yaml', [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'APIDefinition' } }, ResourcePart.CompleteDefinition)); @@ -80,30 +80,31 @@ export = { 'apigateway.APIDefinition.fromCfnParameters': { "automatically creates the Bucket and Key parameters when it's used in a Rest API"(test: Test) { const stack = new cdk.Stack(); - const definition = new apigateway.CfnParametersAPIDefinition(); - new apigateway.RestApi(stack, 'API', { + const definition = new apigw.CfnParametersAPIDefinition(); + new apigw.RestApi(stack, 'API', { apiDefinition: definition, }); - expect(stack).to(haveResourceLike('AWS::APIGateway::RestApi', { + expect(stack).to(haveResourceLike('AWS::ApiGateway::RestApi', { + Name: 'API', BodyS3Location: { Bucket: { - Ref: 'FunctionLambdaSourceBucketNameParameter9E9E108F', + Ref: 'APIAPIDefinitionBucketNameParameter95D18AC4', }, Key: { - Ref: 'FunctionLambdaSourceObjectKeyParameter1C7AED11', + Ref: 'APIAPIDefinitionObjectKeyParameterDB007608', }, }, })); - test.equal(stack.resolve(definition.bucketNameParam), 'FunctionLambdaSourceBucketNameParameter9E9E108F'); - test.equal(stack.resolve(definition.objectKeyParam), 'FunctionLambdaSourceObjectKeyParameter1C7AED11'); + test.equal(stack.resolve(definition.bucketNameParam), 'APIAPIDefinitionBucketNameParameter95D18AC4'); + test.equal(stack.resolve(definition.objectKeyParam), 'APIAPIDefinitionObjectKeyParameterDB007608'); test.done(); }, 'does not allow accessing the Parameter properties before being used in a Rest API'(test: Test) { - const definition = new apigateway.CfnParametersAPIDefinition(); + const definition = new apigw.CfnParametersAPIDefinition(); test.throws(() => { test.notEqual(definition.bucketNameParam, undefined); @@ -125,7 +126,7 @@ export = { type: 'String', }); - const definition = apigateway.APIDefinition.fromCfnParameters({ + const definition = apigw.APIDefinition.fromCfnParameters({ bucketNameParam, objectKeyParam: bucketKeyParam, }); @@ -133,12 +134,12 @@ export = { test.equal(stack.resolve(definition.bucketNameParam), 'BucketNameParam'); test.equal(stack.resolve(definition.objectKeyParam), 'ObjectKeyParam'); - new apigateway.RestApi(stack, 'API', { + new apigw.RestApi(stack, 'API', { apiDefinition: definition }); - expect(stack).to(haveResourceLike('AWS::APIGateway::RestApi', { - S3BodyLocation: { + expect(stack).to(haveResourceLike('AWS::ApiGateway::RestApi', { + BodyS3Location: { Bucket: { Ref: 'BucketNameParam', }, @@ -154,7 +155,7 @@ export = { 'can assign parameters'(test: Test) { // given const stack = new cdk.Stack(); - const code = new apigateway.CfnParametersAPIDefinition({ + const code = new apigw.CfnParametersAPIDefinition({ bucketNameParam: new cdk.CfnParameter(stack, 'BucketNameParam', { type: 'String', }), @@ -178,9 +179,9 @@ export = { }, }; -function defineRestApi(definition: apigateway.APIDefinition) { +function defineRestApi(definition: apigw.APIDefinition) { const stack = new cdk.Stack(); - return new apigateway.RestApi(stack, 'API', { + return new apigw.RestApi(stack, 'API', { apiDefinition: definition }); } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 48002fbdbe36c..ce23554684774 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -13,7 +13,7 @@ export = { // WHEN const api = new apigw.RestApi(stack, 'my-api'); - api.root.addMethod('GET'); // must have at least one method + api.root.addMethod('GET'); // must have at least one method or an API definition // THEN expect(stack).toMatch({ @@ -127,7 +127,7 @@ export = { test.done(); }, - 'fails in synthesis if there are no methods'(test: Test) { + 'fails in synthesis if there are no methods or definition'(test: Test) { // GIVEN const app = new App(); const stack = new Stack(app, 'my-stack'); From 343abb845a096ef3b867bb355d492125ac8b7b43 Mon Sep 17 00:00:00 2001 From: Benjamin Giles Date: Wed, 15 Apr 2020 13:27:08 -0700 Subject: [PATCH 03/11] Revert lockfile to previous master, update README with feature --- packages/@aws-cdk/aws-apigateway/README.md | 8 ++ yarn.lock | 155 ++------------------- 2 files changed, 22 insertions(+), 141 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 41f1a2e8aaef3..14dc7094e624b 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -38,6 +38,14 @@ book.addMethod('GET'); book.addMethod('DELETE'); ``` +Optionally, you can import a Swagger/OpenAPI definition, and API Gateway will create resources +and methods from your specification: +``` +const api = new apigateway.RestApi(this, 'books-api', { + apiDefinition: apigateway.APIDefinition.fromAsset('path-to-your-swagger-file.json') +}); +``` + ### AWS Lambda-backed APIs A very common practice is to use Amazon API Gateway with AWS Lambda as the diff --git a/yarn.lock b/yarn.lock index 6aefe2af57609..55456291e634a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2566,11 +2566,6 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" -app-root-path@^2.2.1: - version "2.2.1" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" - integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== - append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -2804,7 +2799,7 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: +aws-sdk-mock@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-mock/-/aws-sdk-mock-5.1.0.tgz#6f2c0bd670d7f378c906a8dd806f812124db71aa" integrity sha512-Wa5eCSo8HX0Snqb7FdBylaXMmfrAWoWZ+d7MFhiYsgHPvNvMEGjV945FF2qqE1U0Tolr1ALzik1fcwgaOhqUWQ== @@ -2813,21 +2808,6 @@ aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.596.0: - version "2.655.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/aws-sdk/-/aws-sdk-2.655.0.tgz#e95da28e66f02a4bfc0eab46731e2140364b3ea2" - integrity sha512-ywXbaPSwQ+YGo7ZGx7KnmoMO0O7fiEL+rttZIsx6AymLZUohfZ7GlRjG8z93jHa+22qWPMEJ+5UC05/PXWbf7Q== - dependencies: - buffer "4.9.1" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - aws-sdk@^2.637.0, aws-sdk@^2.656.0: version "2.656.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.656.0.tgz#0d74664ddbf30701073be9f9913ee7266afef3b4" @@ -4778,21 +4758,11 @@ dotenv-expand@^5.1.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== -dotenv-json@^1.0.0: - version "1.0.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c" - integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ== - dotenv@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow== -dotenv@^8.0.0: - version "8.2.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - dotgitignore@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" @@ -5027,11 +4997,6 @@ escodegen@~1.9.0: optionalDependencies: source-map "~0.6.1" -eslint-config-standard@^14.1.0: - version "14.1.1" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" - integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== - eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" @@ -5059,15 +5024,7 @@ eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-es@^2.0.0: - version "2.0.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" - integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== - dependencies: - eslint-utils "^1.4.2" - regexpp "^3.0.0" - -eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: +eslint-plugin-import@^2.20.2: version "2.20.2" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== @@ -5085,28 +5042,6 @@ eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: read-pkg-up "^2.0.0" resolve "^1.12.0" -eslint-plugin-node@^10.0.0: - version "10.0.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" - integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== - dependencies: - eslint-plugin-es "^2.0.0" - eslint-utils "^1.4.2" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" - -eslint-plugin-promise@^4.2.1: - version "4.2.1" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" - integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== - -eslint-plugin-standard@^4.0.1: - version "4.0.1" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" - integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== - eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -5115,7 +5050,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.2, eslint-utils@^1.4.3: +eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== @@ -6313,11 +6248,6 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1: - version "5.1.4" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" - integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== - immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -8044,24 +7974,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lambda-leak@^2.0.0: - version "2.0.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e" - integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4= - -lambda-tester@^3.6.0: - version "3.6.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/lambda-tester/-/lambda-tester-3.6.0.tgz#ceb7d4f4f0da768487a05cff37dcd088508b5247" - integrity sha512-F2ZTGWCLyIR95o/jWK46V/WnOCFAEUG/m/V7/CLhPJ7PCM+pror1rZ6ujP3TkItSGxUfpJi0kqwidw+M/nEqWw== - dependencies: - app-root-path "^2.2.1" - dotenv "^8.0.0" - dotenv-json "^1.0.0" - lambda-leak "^2.0.0" - semver "^6.1.1" - uuid "^3.3.2" - vandium-utils "^1.1.1" - lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -8650,7 +8562,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -8711,13 +8623,6 @@ mkdirp@*, mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== -mkdirp@0.x: - version "0.5.5" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -8859,17 +8764,6 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^11.7.0: - version "11.9.1" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/nock/-/nock-11.9.1.tgz#2b026c5beb6d0dbcb41e7e4cefa671bc36db9c61" - integrity sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - lodash "^4.17.13" - mkdirp "^0.5.0" - propagate "^2.0.0" - nock@^12.0.3: version "12.0.3" resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" @@ -10927,7 +10821,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.x, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.4.0: +resolve@1.x, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.4.0: version "1.15.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== @@ -11091,12 +10985,12 @@ semver-intersect@^1.4.0: dependencies: semver "^5.0.0" -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.3.0, semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@6.3.0, semver@6.x, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -12249,22 +12143,6 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^24.2.0: - version "24.3.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" - integrity sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ== - dependencies: - bs-logger "0.x" - buffer-from "1.x" - fast-json-stable-stringify "2.x" - json5 "2.x" - lodash.memoize "4.x" - make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "10.x" - ts-jest@^25.3.1: version "25.3.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.3.1.tgz#58e2ed3506e4e4487c0b9b532846a5cade9656ba" @@ -12689,11 +12567,6 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" -vandium-utils@^1.1.1: - version "1.2.0" - resolved "https://amazon.goshawk.aws.a2z.com:443/npm/shared/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59" - integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k= - vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" @@ -13054,13 +12927,6 @@ yapool@^1.0.0: resolved "https://registry.yarnpkg.com/yapool/-/yapool-1.0.0.tgz#f693f29a315b50d9a9da2646a7a6645c96985b6a" integrity sha1-9pPymjFbUNmp2iZGp6ZkXJaYW2o= -yargs-parser@10.x, yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@18.x, yargs-parser@^18.1.1: version "18.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1" @@ -13069,6 +12935,13 @@ yargs-parser@18.x, yargs-parser@^18.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + yargs-parser@^13.0.0, yargs-parser@^13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" From 63c5550bc9112ff285547b864bf2497dfaa4ac5f Mon Sep 17 00:00:00 2001 From: Benjamin Giles Date: Tue, 21 Apr 2020 18:13:59 -0700 Subject: [PATCH 04/11] Pass all tests and linting --- packages/@aws-cdk/aws-apigateway/README.md | 12 +- .../aws-apigateway/lib/apidefinition.ts | 14 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 445 ++++++++++-------- .../test/integ.restapi.fromdefinition.ts | 4 +- .../aws-apigateway/test/test.apidefinition.ts | 31 +- 5 files changed, 291 insertions(+), 215 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index c53aee9d19e55..3db3660d6c945 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -63,10 +63,16 @@ book.addMethod('DELETE'); Optionally, you can import a Swagger/OpenAPI definition, and API Gateway will create resources and methods from your specification: ``` -const api = new apigateway.RestApi(this, 'books-api', { - apiDefinition: apigateway.APIDefinition.fromAsset('path-to-your-swagger-file.json') -}); +const api = apigateway.RestApi.fromAPIDefinition( + this, + 'books-api', + apigateway.APIDefinition.fromAsset('path-to-your-swagger-file.json') +); ``` +[API Gateway Swagger/OpenAPI import limitations](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html) apply when creating a Rest API in this way. + +You can either create a Rest API from an imported Swagger/OpenAPI definition or define your +API hierarchy with resources and methods, but _not both_. ## AWS Lambda-backed APIs diff --git a/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts b/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts index 95e1330258a09..7d89fb4e782f5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts @@ -110,8 +110,8 @@ export class S3APIDefinition extends APIDefinition { s3Location: { bucket: this.bucketName, key: this.key, - version: this.objectVersion - } + version: this.objectVersion, + }, }; } } @@ -136,7 +136,7 @@ export class InlineAPIDefinition extends APIDefinition { public bind(_scope: cdk.Construct): APIDefinitionConfig { return { - inlineDefinition: this.definition + inlineDefinition: this.definition, }; } } @@ -160,7 +160,7 @@ export class AssetAPIDefinition extends APIDefinition { if (this.asset === undefined) { this.asset = new s3_assets.Asset(scope, 'APIDefinition', { path: this.path, - ...this.options + ...this.options, }); } @@ -171,8 +171,8 @@ export class AssetAPIDefinition extends APIDefinition { return { s3Location: { bucket: this.asset?.s3BucketName, - key: this.asset?.s3ObjectKey - } + key: this.asset?.s3ObjectKey, + }, }; } @@ -258,7 +258,7 @@ export class CfnParametersAPIDefinition extends APIDefinition { s3Location: { bucket: this._bucketNameParam.valueAsString, key: this._objectKeyParam.valueAsString, - } + }, }; } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 849a9ba254ede..29fea77c0868b 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -23,7 +23,10 @@ export interface IRestApi extends IResourceBase { readonly restApiId: string; } -export interface RestApiProps extends ResourceOptions { +/** + * Represents the props that all Rest APIs share + */ +export interface BaseRestApiProps extends ResourceOptions { /** * Indicates if a Deployment should be automatically created for this API, * and recreated when the API model (resources, methods) changes. @@ -72,14 +75,6 @@ export interface RestApiProps extends ResourceOptions { */ readonly restApiName?: string; - /** - * A swagger or OpenAPI specification that defines a set of RESTful APIs in - * JSON or YAML format. - * - * @default - No API specification. - */ - readonly apiDefinition?: APIDefinition; - /** * Custom header parameters for the request. * @see https://docs.aws.amazon.com/cli/latest/reference/apigateway/import-rest-api.html @@ -96,52 +91,54 @@ export interface RestApiProps extends ResourceOptions { readonly policy?: iam.PolicyDocument; /** - * A description of the purpose of this API Gateway RestApi resource. + * Indicates whether to roll back the resource if a warning occurs while API + * Gateway is creating the RestApi resource. * - * @default - No description. + * @default false */ - readonly description?: string; + readonly failOnWarnings?: boolean; /** - * The EndpointConfiguration property type specifies the endpoint types of a REST API - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html + * Configure a custom domain name and map it to this API. * - * @default - No endpoint configuration + * @default - no domain name is defined, use `addDomainName` or directly define a `DomainName`. */ - readonly endpointConfiguration?: EndpointConfiguration; + readonly domainName?: DomainNameOptions; /** - * A list of the endpoint types of the API. Use this property when creating - * an API. + * Automatically configure an AWS CloudWatch role for API Gateway. * - * @default - No endpoint types. - * @deprecated this property is deprecated, use endpointConfiguration instead + * @default true */ - readonly endpointTypes?: EndpointType[]; + readonly cloudWatchRole?: boolean; /** - * The source of the API key for metering requests according to a usage - * plan. + * Export name for the CfnOutput containing the API endpoint * - * @default - Metering is disabled. + * @default - when no export name is given, output will be created without export */ - readonly apiKeySourceType?: ApiKeySourceType; + readonly endpointExportName?: string; +} +/** + * Represents props unique to creating a new Rest API (without a Swagger/OpenAPI + * specification) + */ +export interface RestApiProps extends BaseRestApiProps { /** - * The list of binary media mime-types that are supported by the RestApi - * resource, such as "image/png" or "application/octet-stream" + * A description of the purpose of this API Gateway RestApi resource. * - * @default - RestApi supports only UTF-8-encoded text payloads. + * @default - No description. */ - readonly binaryMediaTypes?: string[]; + readonly description?: string; /** - * Indicates whether to roll back the resource if a warning occurs while API - * Gateway is creating the RestApi resource. + * The list of binary media mime-types that are supported by the RestApi + * resource, such as "image/png" or "application/octet-stream" * - * @default false + * @default - RestApi supports only UTF-8-encoded text payloads. */ - readonly failOnWarnings?: boolean; + readonly binaryMediaTypes?: string[]; /** * A nullable integer that is used to enable compression (with non-negative @@ -163,65 +160,53 @@ export interface RestApiProps extends ResourceOptions { readonly cloneFrom?: IRestApi; /** - * Automatically configure an AWS CloudWatch role for API Gateway. + * The source of the API key for metering requests according to a usage + * plan. * - * @default true + * @default - Metering is disabled. */ - readonly cloudWatchRole?: boolean; + readonly apiKeySourceType?: ApiKeySourceType; /** - * Configure a custom domain name and map it to this API. + * The EndpointConfiguration property type specifies the endpoint types of a REST API + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html * - * @default - no domain name is defined, use `addDomainName` or directly define a `DomainName`. + * @default - No endpoint configuration */ - readonly domainName?: DomainNameOptions; + readonly endpointConfiguration?: EndpointConfiguration; /** - * Export name for the CfnOutput containing the API endpoint + * A list of the endpoint types of the API. Use this property when creating + * an API. * - * @default - when no export name is given, output will be created without export + * @default - No endpoint types. + * @deprecated this property is deprecated, use endpointConfiguration instead */ - readonly endpointExportName?: string; + readonly endpointTypes?: EndpointType[]; } /** - * Represents a REST API in Amazon API Gateway. - * - * Use `addResource` and `addMethod` to configure the API model. - * - * By default, the API will automatically be deployed and accessible from a - * public endpoint. + * Represents props unique to creating a Rest API with a Swagger/OpenAPI specification */ -export class RestApi extends Resource implements IRestApi { - - public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { - class Import extends Resource implements IRestApi { - public readonly restApiId = restApiId; - } - - return new Import(scope, id); - } +export interface APIDefinitionRestApiProps extends BaseRestApiProps { + /** + * A Swagger/OpenAPI definition compatible with API Gateway. + */ + readonly apiDefinition: APIDefinition; +} +abstract class BaseRestApi extends Resource implements IRestApi { /** * The ID of this API Gateway RestApi. */ - public readonly restApiId: string; + public abstract readonly restApiId: string; /** * The resource ID of the root resource. * * @attribute */ - public readonly restApiRootResourceId: string; - - /** - * Represents the root resource ("/") of this API. Use it to define the API model: - * - * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" - * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" - * - */ - public readonly root: IResource; + public abstract readonly restApiRootResourceId: string; /** * API Gateway stage that points to the latest deployment (if defined). @@ -231,80 +216,13 @@ export class RestApi extends Resource implements IRestApi { */ public deploymentStage!: Stage; - /** - * The list of methods bound to this RestApi - */ - public readonly methods = new Array(); - + private _latestDeployment?: Deployment; private _domainName?: DomainName; - private _apiDefinition?: APIDefinitionConfig; - private _latestDeployment: Deployment | undefined; - constructor(scope: Construct, id: string, props: RestApiProps = { }) { + constructor(scope: Construct, id: string, props: BaseRestApiProps = { }) { super(scope, id, { physicalName: props.restApiName || id, }); - - if (props.apiDefinition !== undefined) { - this._apiDefinition = props.apiDefinition.bind(this); - verifyAPIDefinitionConfig(this._apiDefinition); - } - - const resource = new CfnRestApi(this, 'Resource', { - name: this.physicalName, - description: props.description, - policy: props.policy, - body: props.apiDefinition?.isInline ? this._apiDefinition?.inlineDefinition : undefined, - bodyS3Location: props.apiDefinition?.isInline ? undefined : this._apiDefinition?.s3Location, - failOnWarnings: props.failOnWarnings, - minimumCompressionSize: props.minimumCompressionSize, - binaryMediaTypes: props.binaryMediaTypes, - endpointConfiguration: this.configureEndpoints(props), - apiKeySourceType: props.apiKeySourceType, - cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, - parameters: props.parameters, - }); - this.node.defaultChild = resource; - props.apiDefinition?.bindToResource(resource); - this.restApiId = resource.ref; - - this.configureDeployment(props); - - const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; - if (cloudWatchRole) { - this.configureCloudWatchRole(resource); - } - - this.root = new RootResource(this, props, resource.attrRootResourceId); - this.restApiRootResourceId = resource.attrRootResourceId; - - if (props.domainName) { - this.addDomainName('CustomDomain', props.domainName); - } - } - - /** - * The first domain name mapped to this API, if defined through the `domainName` - * configuration prop, or added via `addDomainName` - */ - public get domainName() { - return this._domainName; - } - - /** - * API Gateway deployment that represents the latest changes of the API. - * This resource will be automatically updated every time the REST API model changes. - * This will be undefined if `deploy` is false. - */ - public get latestDeployment() { - return this._latestDeployment; - } - - /** - * The deployed root URL of this REST API. - */ - public get url() { - return this.urlForPath(); } /** @@ -320,6 +238,15 @@ export class RestApi extends Resource implements IRestApi { return this.deploymentStage.urlForPath(path); } + /** + * API Gateway deployment that represents the latest changes of the API. + * This resource will be automatically updated every time the REST API model changes. + * This will be undefined if `deploy` is false. + */ + public get latestDeployment() { + return this._latestDeployment; + } + /** * Defines an API Gateway domain name and maps it to this API. * @param id The construct id @@ -344,35 +271,15 @@ export class RestApi extends Resource implements IRestApi { } /** - * Add an ApiKey - */ - public addApiKey(id: string): IApiKey { - return new ApiKey(this, id, { - resources: [this], - }); - } - - /** - * Adds a new model. - */ - public addModel(id: string, props: ModelOptions): Model { - return new Model(this, id, { - ...props, - restApi: this, - }); - } - - /** - * Adds a new request validator. + * The first domain name mapped to this API, if defined through the `domainName` + * configuration prop, or added via `addDomainName` */ - public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { - return new RequestValidator(this, id, { - ...props, - restApi: this, - }); + public get domainName() { + return this._domainName; } /** + * Gets the "execute-api" ARN * @returns The "execute-api" ARN. * @default "*" returns the execute API ARN for all methods/resources in * this API. @@ -397,28 +304,20 @@ export class RestApi extends Resource implements IRestApi { }); } - /** - * Internal API used by `Method` to keep an inventory of methods at the API - * level for validation purposes. - * - * @internal - */ - public _attachMethod(method: Method) { - this.methods.push(method); - } + protected configureCloudWatchRole(apiResource: CfnRestApi) { + const role = new iam.Role(this, 'CloudWatchRole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs')], + }); - /** - * Performs validation of the REST API. - */ - protected validate() { - if (this.methods.length === 0 && this._apiDefinition === undefined) { - return [ 'The REST API doesn\'t contain any methods' ]; - } + const resource = new CfnAccount(this, 'Account', { + cloudWatchRoleArn: role.roleArn, + }); - return []; + resource.node.addDependency(apiResource); } - private configureDeployment(props: RestApiProps) { + protected configureDeployment(props: RestApiProps) { const deploy = props.deploy === undefined ? true : props.deploy; if (deploy) { @@ -444,18 +343,190 @@ export class RestApi extends Resource implements IRestApi { } } } +} - private configureCloudWatchRole(apiResource: CfnRestApi) { - const role = new iam.Role(this, 'CloudWatchRole', { - assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), - managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs')], +/** + * Represents a REST API in Amazon API Gateway, created via a Swagger/OpenAPI specification. + * + * Some properties normally accessible on @see {@link RestApi} - such as the description - + * must be declared in the specification. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + * + * @resource AWS::ApiGateway::RestApi + */ +export class APIDefinitionRestApi extends BaseRestApi { + /** + * The ID of this API Gateway RestApi. + */ + public readonly restApiId: string; + + /** + * The resource ID of the root resource. + * + * @attribute + */ + public readonly restApiRootResourceId: string; + + constructor(scope: Construct, id: string, props: APIDefinitionRestApiProps) { + super(scope, id, props); + const apiDefConfig = props.apiDefinition.bind(this); + const resource = new CfnRestApi(this, 'Resource', { + name: this.physicalName, + policy: props.policy, + failOnWarnings: props.failOnWarnings, + body: props.apiDefinition.isInline ? apiDefConfig.inlineDefinition : undefined, + bodyS3Location: props.apiDefinition.isInline ? undefined : apiDefConfig.s3Location, + parameters: props.parameters, }); + props.apiDefinition.bindToResource(resource); + this.node.defaultChild = resource; + this.restApiId = resource.ref; + this.restApiRootResourceId = resource.attrRootResourceId; - const resource = new CfnAccount(this, 'Account', { - cloudWatchRoleArn: role.roleArn, + this.configureDeployment(props); + if (props.domainName) { + this.addDomainName('CustomDomain', props.domainName); + } + + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + if (cloudWatchRole) { + this.configureCloudWatchRole(resource); + } + } +} + +/** + * Represents a REST API in Amazon API Gateway. + * + * Use `addResource` and `addMethod` to configure the API model. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + */ +export class RestApi extends BaseRestApi implements IRestApi { + public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { + class Import extends Resource implements IRestApi { + public readonly restApiId = restApiId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway RestApi. + */ + public readonly restApiId: string; + + /** + * Represents the root resource ("/") of this API. Use it to define the API model: + * + * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" + * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" + * + */ + public readonly root: IResource; + + /** + * The resource ID of the root resource. + * + * @attribute + */ + public readonly restApiRootResourceId: string; + + /** + * The list of methods bound to this RestApi + */ + public readonly methods = new Array(); + + constructor(scope: Construct, id: string, props: RestApiProps = { }) { + super(scope, id, props); + + const resource = new CfnRestApi(this, 'Resource', { + name: this.physicalName, + description: props.description, + policy: props.policy, + failOnWarnings: props.failOnWarnings, + minimumCompressionSize: props.minimumCompressionSize, + binaryMediaTypes: props.binaryMediaTypes, + endpointConfiguration: this.configureEndpoints(props), + apiKeySourceType: props.apiKeySourceType, + cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, + parameters: props.parameters, }); + this.node.defaultChild = resource; + this.restApiId = resource.ref; - resource.node.addDependency(apiResource); + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + if (cloudWatchRole) { + this.configureCloudWatchRole(resource); + } + + this.configureDeployment(props); + if (props.domainName) { + this.addDomainName('CustomDomain', props.domainName); + } + + this.root = new RootResource(this, props, resource.attrRootResourceId); + this.restApiRootResourceId = resource.attrRootResourceId; + } + + /** + * The deployed root URL of this REST API. + */ + public get url() { + return this.urlForPath(); + } + + /** + * Add an ApiKey + */ + public addApiKey(id: string): IApiKey { + return new ApiKey(this, id, { + resources: [this], + }); + } + + /** + * Adds a new model. + */ + public addModel(id: string, props: ModelOptions): Model { + return new Model(this, id, { + ...props, + restApi: this, + }); + } + + /** + * Adds a new request validator. + */ + public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { + return new RequestValidator(this, id, { + ...props, + restApi: this, + }); + } + + /** + * Internal API used by `Method` to keep an inventory of methods at the API + * level for validation purposes. + * + * @internal + */ + public _attachMethod(method: Method) { + this.methods.push(method); + } + + /** + * Performs validation of the REST API. + */ + protected validate() { + if (this.methods.length === 0) { + return [ 'The REST API doesn\'t contain any methods' ]; + } + + return []; } private configureEndpoints(props: RestApiProps): CfnRestApi.EndpointConfigurationProperty | undefined { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts index 658b5fe1fb1ce..41dfeda5929ac 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts @@ -6,8 +6,8 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'test-apigateway-restapi-fromdefinition'); -new apigateway.RestApi(stack, 'my-api', { - apiDefinition: apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')) +new apigateway.APIDefinitionRestApi(stack, 'my-api', { + apiDefinition: apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), }); app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts b/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts index 11aa8192092ac..38da5f730e589 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts @@ -15,8 +15,7 @@ export = { /too large, must be <= 4096 but is 4097/); test.throws( () => defineRestApi(apigw.APIDefinition.fromInline('')), - /cannot be empty/ - ); + /cannot be empty/); test.done(); }, }, @@ -37,12 +36,12 @@ export = { const directoryAsset = apigw.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); // WHEN - new apigw.RestApi(stack, 'API1', { - apiDefinition: directoryAsset + new apigw.APIDefinitionRestApi(stack, 'API1', { + apiDefinition: directoryAsset, }); - new apigw.RestApi(stack, 'API2', { - apiDefinition: directoryAsset + new apigw.APIDefinitionRestApi(stack, 'API2', { + apiDefinition: directoryAsset, }); // THEN @@ -62,26 +61,26 @@ export = { const definition = apigw.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); // WHEN - new apigw.RestApi(stack, 'API1', { - apiDefinition: definition + new apigw.APIDefinitionRestApi(stack, 'API1', { + apiDefinition: definition, }); // THEN expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { Metadata: { [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58.yaml', - [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'APIDefinition' - } + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'APIDefinition', + }, }, ResourcePart.CompleteDefinition)); test.done(); - } + }, }, 'apigateway.APIDefinition.fromCfnParameters': { "automatically creates the Bucket and Key parameters when it's used in a Rest API"(test: Test) { const stack = new cdk.Stack(); const definition = new apigw.CfnParametersAPIDefinition(); - new apigw.RestApi(stack, 'API', { + new apigw.APIDefinitionRestApi(stack, 'API', { apiDefinition: definition, }); @@ -134,8 +133,8 @@ export = { test.equal(stack.resolve(definition.bucketNameParam), 'BucketNameParam'); test.equal(stack.resolve(definition.objectKeyParam), 'ObjectKeyParam'); - new apigw.RestApi(stack, 'API', { - apiDefinition: definition + new apigw.APIDefinitionRestApi(stack, 'API', { + apiDefinition: definition, }); expect(stack).to(haveResourceLike('AWS::ApiGateway::RestApi', { @@ -181,8 +180,8 @@ export = { function defineRestApi(definition: apigw.APIDefinition) { const stack = new cdk.Stack(); - return new apigw.RestApi(stack, 'API', { - apiDefinition: definition + return new apigw.APIDefinitionRestApi(stack, 'API', { + apiDefinition: definition, }); } From 3b36e02b14faab56091c7bfdfc380847c2113119 Mon Sep 17 00:00:00 2001 From: Benjamin Giles Date: Tue, 21 Apr 2020 19:01:03 -0700 Subject: [PATCH 05/11] Minor changes to README, ensure docs are up to date --- packages/@aws-cdk/aws-apigateway/README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 3db3660d6c945..08d1629318045 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -62,12 +62,10 @@ book.addMethod('DELETE'); Optionally, you can import a Swagger/OpenAPI definition, and API Gateway will create resources and methods from your specification: -``` -const api = apigateway.RestApi.fromAPIDefinition( - this, - 'books-api', - apigateway.APIDefinition.fromAsset('path-to-your-swagger-file.json') -); +```ts +const api = new apigateway.APIDefinitionRestApi(this, 'books-api', { + apiDefinition: apigateway.APIDefinition.fromAsset('path-to-your-swagger-file.json') +}); ``` [API Gateway Swagger/OpenAPI import limitations](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html) apply when creating a Rest API in this way. From 68f693deef58d4c3aee5cf1c3e351d9f45157b94 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 6 May 2020 17:58:32 +0100 Subject: [PATCH 06/11] addressed some of the PR feedback --- packages/@aws-cdk/aws-apigateway/README.md | 70 ++-- .../aws-apigateway/lib/api-definition.ts | 136 ++++++++ .../aws-apigateway/lib/apidefinition.ts | 299 ------------------ packages/@aws-cdk/aws-apigateway/lib/index.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 21 +- .../test/integ.restapi.fromdefinition.ts | 4 +- .../test/test.api-definition.ts | 57 ++++ .../aws-apigateway/test/test.apidefinition.ts | 194 ------------ 8 files changed, 248 insertions(+), 535 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/api-definition.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 08d1629318045..1c6e8ff29c4ca 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -1,4 +1,5 @@ ## Amazon API Gateway Construct Library + --- @@ -17,8 +18,6 @@ running on AWS Lambda, or any web application. ## Table of Contents -- [Amazon API Gateway Construct Library](#amazon-api-gateway-construct-library) -- [Table of Contents](#table-of-contents) - [Defining APIs](#defining-apis) - [AWS Lambda-backed APIs](#aws-lambda-backed-apis) - [Integration Targets](#integration-targets) @@ -35,6 +34,8 @@ running on AWS Lambda, or any web application. - [Access Logging](#access-logging) - [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Endpoint Configuration](#endpoint-configuration) +- [Using OpenAPI Definitions](#using-openapi-definitions) +- [OpenAPI Definition](#openapi-definition) - [APIGateway v2](#apigateway-v2) ## Defining APIs @@ -60,18 +61,6 @@ book.addMethod('GET'); book.addMethod('DELETE'); ``` -Optionally, you can import a Swagger/OpenAPI definition, and API Gateway will create resources -and methods from your specification: -```ts -const api = new apigateway.APIDefinitionRestApi(this, 'books-api', { - apiDefinition: apigateway.APIDefinition.fromAsset('path-to-your-swagger-file.json') -}); -``` -[API Gateway Swagger/OpenAPI import limitations](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html) apply when creating a Rest API in this way. - -You can either create a Rest API from an imported Swagger/OpenAPI definition or define your -API hierarchy with resources and methods, but _not both_. - ## AWS Lambda-backed APIs A very common practice is to use Amazon API Gateway with AWS Lambda as the @@ -114,11 +103,11 @@ item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); Methods are associated with backend integrations, which are invoked when this method is called. API Gateway supports the following integrations: - * `MockIntegration` - can be used to test APIs. This is the default +* `MockIntegration` - can be used to test APIs. This is the default integration if one is not specified. - * `LambdaIntegration` - can be used to invoke an AWS Lambda function. - * `AwsIntegration` - can be used to invoke arbitrary AWS service APIs. - * `HttpIntegration` - can be used to invoke HTTP endpoints. +* `LambdaIntegration` - can be used to invoke an AWS Lambda function. +* `AwsIntegration` - can be used to invoke arbitrary AWS service APIs. +* `HttpIntegration` - can be used to invoke HTTP endpoints. The following example shows how to integrate the `GET /book/{book_id}` method to an AWS Lambda function: @@ -192,6 +181,7 @@ This construct lets you specify rate limiting properties which should be applied The API key created has the specified rate limits, such as quota and throttles, applied. The following example shows how to use a rate limited api key : + ```ts const hello = new lambda.Function(this, 'hello', { runtime: lambda.Runtime.NODEJS_10_X, @@ -463,9 +453,9 @@ iamUser.attachInlinePolicy(new iam.Policy(this, 'AllowBooks', { API Gateway also allows [lambda functions to be used as authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html). This module provides support for token-based Lambda authorizers. When a client makes a request to an API's methods configured with such -an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output. +an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output. A token-based Lambda authorizer (also called a token authorizer) receives the caller's identity in a bearer token, such as -a JSON Web Token (JWT) or an OAuth token. +a JSON Web Token (JWT) or an OAuth token. API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format. The event object that the handler is called with contains the `authorizationToken` and the `methodArn` from the request to the @@ -504,7 +494,7 @@ depending on where the defaults were specified. This module provides support for request-based Lambda authorizers. When a client makes a request to an API's methods configured with such an authorizer, API Gateway calls the Lambda authorizer, which takes specified parts of the request, known as identity sources, -as input and returns an IAM policy as output. A request-based Lambda authorizer (also called a request authorizer) receives +as input and returns an IAM policy as output. A request-based Lambda authorizer (also called a request authorizer) receives the identity sources in a series of values pulled from the request, from the headers, stage variables, query strings, and the context. API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format. @@ -647,8 +637,8 @@ new apigw.DomainName(this, 'custom-domain', { ``` Once you have a domain, you can map base paths of the domain to APIs. -The following example will map the URL https://example.com/go-to-api1 -to the `api1` API and https://example.com/boom to the `api2` API. +The following example will map the URL +to the `api1` API and to the `api2` API. ```ts domain.addBasePathMapping(api1, { basePath: 'go-to-api1' }); @@ -656,7 +646,7 @@ domain.addBasePathMapping(api2, { basePath: 'boom' }); ``` You can specify the API `Stage` to which this base path URL will map to. By default, this will be the -`deploymentStage` of the `RestApi`. +`deploymentStage` of the `RestApi`. ```ts const betaDeploy = new Deployment(this, 'beta-deployment', { @@ -800,7 +790,7 @@ running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own. -You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS +You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS HTTP method to any API resource via the `defaultCorsPreflightOptions` option or by calling the `addCorsPreflight` on a specific resource. The following example will enable CORS for all methods and all origins on all resources of the API: @@ -815,7 +805,7 @@ new apigateway.RestApi(this, 'api', { ``` The following example will add an OPTIONS method to the `myResource` API resource, which -only allows GET and PUT HTTP requests from the origin https://amazon.com. +only allows GET and PUT HTTP requests from the origin ```ts myResource.addCorsPreflight({ @@ -846,8 +836,8 @@ features which are not yet supported. ## Endpoint Configuration -API gateway allows you to specify an -[API Endpoint Type](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html). +API gateway allows you to specify an +[API Endpoint Type](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html). To define an endpoint type for the API gateway, use `endpointConfiguration` property: ```ts @@ -881,6 +871,30 @@ By performing this association, we can invoke the API gateway using the followin https://{rest-api-id}-{vpce-id}.execute-api.{region}.amazonaws.com/{stage} ``` +## OpenAPI Definition + +CDK supports creating a REST API by importing an OpenAPI definition file. It currently supports OpenAPI v2.0 and OpenAPI +v3.0 definition files. Read more about [Configuring a REST API using +OpenAPI](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-import-api.html). + +The following code creates a REST API using an external OpenAPI definition JSON file - + +```ts +const api = new apigateway.SpecRestApi(this, 'books-api', { + apiDefinition: apigateway.APIDefinition.fromAsset('path-to-file.json') +}); +``` + +There are a number of limitations in using OpenAPI definitions in API Gateway. Read the [Amazon API Gateway important +notes for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-apis) +for more details. + +**Note:** When starting off with an OpenAPI definition, it is possible to add Resources and Methods via the CDK APIs, in +addition to what has already been defined in the OpenAPI. +However, it is important to note that it is not possible to override or modify Resources and Methods that are already +defined in the OpenAPI definition. While these will be present in the final CloudFormation template, they will be +ignored by API Gateway. The definition in the OpenAPI definition file always takes precedent. + ## APIGateway v2 APIGateway v2 APIs are now moved to its own package named `aws-apigatewayv2`. For backwards compatibility, existing diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts new file mode 100644 index 0000000000000..e4ffbe0266668 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -0,0 +1,136 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; +import { CfnRestApi } from './apigateway.generated'; + +/** + * Represents an OpenAPI definition asset. + */ +export abstract class ApiDefinition { + /** + * Creates an API definition from a specification file in an S3 bucket + */ + public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3ApiDefinition { + return new S3ApiDefinition(bucket, key, objectVersion); + } + + /** + * Creates an API definition from a string + */ + public static fromInline(definition: string): InlineApiDefinition { + return new InlineApiDefinition(definition); + } + + /** + * Loads the API specification from a local disk asset. + */ + public static fromAsset(file: string, options?: s3_assets.AssetOptions): AssetApiDefinition { + return new AssetApiDefinition(file, options); + } + + /** + * Called when the specification is initialized to allow this object to bind + * to the stack, add resources and have fun. + * + * @param scope The binding scope. Don't be smart about trying to down-cast or + * assume it's initialized. You may just use it as a construct scope. + */ + public abstract bind(scope: cdk.Construct): ApiDefinitionConfig; +} + +/** + * Post-Binding Configuration for a CDK construct + */ +export interface ApiDefinitionConfig { + /** + * The location of the specification in S3 (mutually exclusive with `inlineDefinition`). + * + * @default a new parameter will be created + */ + readonly s3Location?: CfnRestApi.S3LocationProperty; + + /** + * Inline specification (mutually exclusive with `s3Location`). + * + * @default a new parameter will be created + */ + readonly inlineDefinition?: string; +} + +/** + * Swagger/OpenAPI specification from an S3 archive + */ +export class S3ApiDefinition extends ApiDefinition { + private bucketName: string; + + constructor(bucket: s3.IBucket, private key: string, private objectVersion?: string) { + super(); + + if (!bucket.bucketName) { + throw new Error('bucketName is undefined for the provided bucket'); + } + + this.bucketName = bucket.bucketName; + } + + public bind(_scope: cdk.Construct): ApiDefinitionConfig { + return { + s3Location: { + bucket: this.bucketName, + key: this.key, + version: this.objectVersion, + }, + }; + } +} + +/** + * OpenAPI specification from an inline string (limited to 4KiB) + */ +export class InlineApiDefinition extends ApiDefinition { + constructor(private definition: string) { + super(); + + if (definition.length === 0) { + throw new Error('Inline API definition cannot be empty'); + } + } + + public bind(_scope: cdk.Construct): ApiDefinitionConfig { + return { + inlineDefinition: this.definition, + }; + } +} + +/** + * OpenAPI specification from a local file. + */ +export class AssetApiDefinition extends ApiDefinition { + private asset?: s3_assets.Asset; + + constructor(private readonly path: string, private readonly options: s3_assets.AssetOptions = { }) { + super(); + } + + public bind(scope: cdk.Construct): ApiDefinitionConfig { + // If the same AssetAPIDefinition is used multiple times, retain only the first instantiation. + if (this.asset === undefined) { + this.asset = new s3_assets.Asset(scope, 'APIDefinition', { + path: this.path, + ...this.options, + }); + } + + if (this.asset?.isZipArchive) { + throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`); + } + + return { + s3Location: { + bucket: this.asset?.s3BucketName, + key: this.asset?.s3ObjectKey, + }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts b/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts deleted file mode 100644 index 7d89fb4e782f5..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/lib/apidefinition.ts +++ /dev/null @@ -1,299 +0,0 @@ -import * as s3 from '@aws-cdk/aws-s3'; -import * as s3_assets from '@aws-cdk/aws-s3-assets'; -import * as cdk from '@aws-cdk/core'; -import { CfnRestApi } from './apigateway.generated'; - -/** - * Represents a API Gateway Swagger or OpenAPI specification. - */ -export abstract class APIDefinition { - /** - * Creates an API definition from a specification file in an S3 bucket - * @returns `APIDefinitionS3` associated with the specified S3 object. - * @param bucket The S3 bucket - * @param key The object key - * @param objectVersion Optional S3 object version - */ - public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3APIDefinition { - return new S3APIDefinition(bucket, key, objectVersion); - } - - /** - * Creates an API definition from a string - * @returns `InlineAPIDefinition` with inline specification. - * @param code The actual API specification (limited to 4KiB) - */ - public static fromInline(code: string): InlineAPIDefinition { - return new InlineAPIDefinition(code); - } - - /** - * Loads the API specification from a local disk asset. - * @param file The path to the JSON or YAML specification file - */ - public static fromAsset(file: string, options?: s3_assets.AssetOptions): AssetAPIDefinition { - return new AssetAPIDefinition(file, options); - } - - /** - * Creates an OpenAPI/Swagger specification source using CloudFormation parameters. - * - * @returns a new instance of `CfnParametersAPIDefinition` - * @param props optional construction properties of {@link CfnParametersAPIDefinition} - */ - public static fromCfnParameters(props?: CfnParametersAPIDefinitionProps): CfnParametersAPIDefinition { - return new CfnParametersAPIDefinition(props); - } - - /** - * Determines whether this API definition is inline or not. - */ - public abstract readonly isInline: boolean; - - /** - * Called when the specification is initialized to allow this object to bind - * to the stack, add resources and have fun. - * - * @param scope The binding scope. Don't be smart about trying to down-cast or - * assume it's initialized. You may just use it as a construct scope. - */ - public abstract bind(scope: cdk.Construct): APIDefinitionConfig; - - /** - * Called after the CFN function resource has been created to allow the API definition - * class to bind to it. Specifically it's required to allow assets to add - * metadata for tooling like SAM CLI to be able to find their origins. - */ - public bindToResource(_resource: cdk.CfnResource, _options?: ResourceBindOptions) { - return; - } -} - -/** - * Post-Binding Configuration for a CDK construct - */ -export interface APIDefinitionConfig { - /** - * The location of the specification in S3 (mutually exclusive with `inlineDefinition`). - * - * @default a new parameter will be created - */ - readonly s3Location?: CfnRestApi.S3LocationProperty; - - /** - * Inline specification (mutually exclusive with `s3Location`). - * - * @default a new parameter will be created - */ - readonly inlineDefinition?: string; -} - -/** - * Swagger/OpenAPI specification from an S3 archive - */ -export class S3APIDefinition extends APIDefinition { - public readonly isInline = false; - private bucketName: string; - - constructor(bucket: s3.IBucket, private key: string, private objectVersion?: string) { - super(); - - if (!bucket.bucketName) { - throw new Error('bucketName is undefined for the provided bucket'); - } - - this.bucketName = bucket.bucketName; - } - - public bind(_scope: cdk.Construct): APIDefinitionConfig { - return { - s3Location: { - bucket: this.bucketName, - key: this.key, - version: this.objectVersion, - }, - }; - } -} - -/** - * Swagger/OpenAPI specification from an inline string (limited to 4KiB) - */ -export class InlineAPIDefinition extends APIDefinition { - public readonly isInline = true; - - constructor(private definition: string) { - super(); - - if (definition.length === 0) { - throw new Error('Inline API definition cannot be empty'); - } - - if (definition.length > 4096) { - throw new Error('API definition is too large, must be <= 4096 but is ' + definition.length); - } - } - - public bind(_scope: cdk.Construct): APIDefinitionConfig { - return { - inlineDefinition: this.definition, - }; - } -} - -/** - * Swagger/OpenAPI specification from a local file. - */ -export class AssetAPIDefinition extends APIDefinition { - public readonly isInline = false; - private asset?: s3_assets.Asset; - - /** - * @param path The path to the asset file - */ - constructor(public readonly path: string, private readonly options: s3_assets.AssetOptions = { }) { - super(); - } - - public bind(scope: cdk.Construct): APIDefinitionConfig { - // If the same AssetAPIDefinition is used multiple times, retain only the first instantiation. - if (this.asset === undefined) { - this.asset = new s3_assets.Asset(scope, 'APIDefinition', { - path: this.path, - ...this.options, - }); - } - - if (this.asset?.isZipArchive) { - throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`); - } - - return { - s3Location: { - bucket: this.asset?.s3BucketName, - key: this.asset?.s3ObjectKey, - }, - }; - } - - public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) { - if (this.asset === undefined) { - throw new Error('bindToResource() must be called after bind()'); - } - - const resourceProperty = options.resourceProperty || 'APIDefinition'; - - // https://github.com/aws/aws-cdk/issues/1432 - this.asset.addResourceMetadata(resource, resourceProperty); - } -} - -/** - * Post-Synthesis options - */ -export interface ResourceBindOptions { - /** - * The name of the CloudFormation property to annotate with asset metadata. - * @see https://github.com/aws/aws-cdk/issues/1432 - * @default Code - */ - readonly resourceProperty?: string; -} - -/** - * Construction properties for {@link CfnParametersAPIDefinition}. - */ -export interface CfnParametersAPIDefinitionProps { - /** - * The CloudFormation parameter that represents the name of the S3 Bucket - * where the API specification file is located. - * Must be of type 'String'. - * - * @default a new parameter will be created - */ - readonly bucketNameParam?: cdk.CfnParameter; - - /** - * The CloudFormation parameter that represents the path inside the S3 Bucket - * where the API specification file is located. - * Must be of type 'String'. - * - * @default a new parameter will be created - */ - readonly objectKeyParam?: cdk.CfnParameter; -} - -/** - * Swagger/OpenAPI specification using 2 CloudFormation parameters. - * Useful when you don't have access to the specification from your CDK code, so you can't use Assets, - * and you want to deploy a REST API with a pre-built spec in a CodePipeline, using CloudFormation Actions - - * you can fill the parameters using the {@link #assign} method. - */ -export class CfnParametersAPIDefinition extends APIDefinition { - public readonly isInline = false; - private _bucketNameParam?: cdk.CfnParameter; - private _objectKeyParam?: cdk.CfnParameter; - - constructor(props: CfnParametersAPIDefinitionProps = {}) { - super(); - - this._bucketNameParam = props.bucketNameParam; - this._objectKeyParam = props.objectKeyParam; - } - - public bind(scope: cdk.Construct): APIDefinitionConfig { - if (!this._bucketNameParam) { - this._bucketNameParam = new cdk.CfnParameter(scope, 'APIDefinitionBucketNameParameter', { - type: 'String', - }); - } - - if (!this._objectKeyParam) { - this._objectKeyParam = new cdk.CfnParameter(scope, 'APIDefinitionObjectKeyParameter', { - type: 'String', - }); - } - - return { - s3Location: { - bucket: this._bucketNameParam.valueAsString, - key: this._objectKeyParam.valueAsString, - }, - }; - } - - /** - * Create a parameters map from this instance's CloudFormation parameters. - * - * It returns a map with 2 keys that correspond to the names of the parameters defined in this API definition, - * and as values it contains the appropriate expressions pointing at the provided S3 location - * (most likely, obtained from a CodePipeline Artifact by calling the `artifact.s3Location` method). - * The result should be provided to the CloudFormation Action - * that is deploying the Stack that the REST API with this definition is part of, - * in the `parameterOverrides` property. - * - * @param location the location of the object in S3 that represents the API definition - */ - public assign(location: s3.Location): { [name: string]: any } { - const ret: { [name: string]: any } = {}; - ret[this.bucketNameParam] = location.bucketName; - ret[this.objectKeyParam] = location.objectKey; - return ret; - } - - public get bucketNameParam(): string { - if (this._bucketNameParam) { - return this._bucketNameParam.logicalId; - } else { - throw new Error('Pass CfnParametersAPIDefinition to a REST API before accessing the bucketNameParam property'); - } - } - - public get objectKeyParam(): string { - if (this._objectKeyParam) { - return this._objectKeyParam.logicalId; - } else { - throw new Error('Pass CfnParametersAPIDefinition to a REST API before accessing the objectKeyParam property'); - } - } -} diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index da5638b8f4c11..3db6ca161897e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -20,7 +20,7 @@ export * from './base-path-mapping'; export * from './cors'; export * from './authorizers'; export * from './access-log'; -export * from './apidefinition'; +export * from './api-definition'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 29fea77c0868b..f8339313c44d0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,8 +1,8 @@ import { IVpcEndpoint } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { CfnOutput, Construct, IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; +import { ApiDefinition, ApiDefinitionConfig } from './api-definition'; import { ApiKey, IApiKey } from './api-key'; -import { APIDefinition, APIDefinitionConfig } from './apidefinition'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { CorsOptions } from './cors'; import { Deployment } from './deployment'; @@ -186,13 +186,13 @@ export interface RestApiProps extends BaseRestApiProps { } /** - * Represents props unique to creating a Rest API with a Swagger/OpenAPI specification + * Props to instantiate a new SpecRestApi */ -export interface APIDefinitionRestApiProps extends BaseRestApiProps { +export interface SpecRestApiProps extends BaseRestApiProps { /** * A Swagger/OpenAPI definition compatible with API Gateway. */ - readonly apiDefinition: APIDefinition; + readonly apiDefinition: ApiDefinition; } abstract class BaseRestApi extends Resource implements IRestApi { @@ -346,7 +346,7 @@ abstract class BaseRestApi extends Resource implements IRestApi { } /** - * Represents a REST API in Amazon API Gateway, created via a Swagger/OpenAPI specification. + * Represents a REST API in Amazon API Gateway, created with an OpenAPI specification. * * Some properties normally accessible on @see {@link RestApi} - such as the description - * must be declared in the specification. @@ -356,7 +356,7 @@ abstract class BaseRestApi extends Resource implements IRestApi { * * @resource AWS::ApiGateway::RestApi */ -export class APIDefinitionRestApi extends BaseRestApi { +export class SpecRestApi extends BaseRestApi { /** * The ID of this API Gateway RestApi. */ @@ -369,18 +369,17 @@ export class APIDefinitionRestApi extends BaseRestApi { */ public readonly restApiRootResourceId: string; - constructor(scope: Construct, id: string, props: APIDefinitionRestApiProps) { + constructor(scope: Construct, id: string, props: SpecRestApiProps) { super(scope, id, props); const apiDefConfig = props.apiDefinition.bind(this); const resource = new CfnRestApi(this, 'Resource', { name: this.physicalName, policy: props.policy, failOnWarnings: props.failOnWarnings, - body: props.apiDefinition.isInline ? apiDefConfig.inlineDefinition : undefined, - bodyS3Location: props.apiDefinition.isInline ? undefined : apiDefConfig.s3Location, + body: apiDefConfig.inlineDefinition ? apiDefConfig.inlineDefinition : undefined, + bodyS3Location: apiDefConfig.inlineDefinition ? undefined : apiDefConfig.s3Location, parameters: props.parameters, }); - props.apiDefinition.bindToResource(resource); this.node.defaultChild = resource; this.restApiId = resource.ref; this.restApiRootResourceId = resource.attrRootResourceId; @@ -622,7 +621,7 @@ class RootResource extends ResourceBase { } } -export function verifyAPIDefinitionConfig(definition: APIDefinitionConfig) { +export function verifyAPIDefinitionConfig(definition: ApiDefinitionConfig) { // mutually exclusive if ((!definition.inlineDefinition && !definition.s3Location) || (definition.inlineDefinition && definition.s3Location)) { throw new Error('APIDefinition must specify one of "inlineDefinition" or "s3Location" but not both'); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts index 41dfeda5929ac..3abbdc09b14d4 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts @@ -6,8 +6,8 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'test-apigateway-restapi-fromdefinition'); -new apigateway.APIDefinitionRestApi(stack, 'my-api', { - apiDefinition: apigateway.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), +new apigateway.SpecRestApi(stack, 'my-api', { + apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), }); app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts new file mode 100644 index 0000000000000..9c7a01643cdef --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts @@ -0,0 +1,57 @@ +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as path from 'path'; +import * as apigw from '../lib'; + +export = { + 'apigateway.ApiDefinition.fromInline': { + 'fails if inline definition is empty'(test: Test) { + test.throws( + () => defineRestApi(apigw.ApiDefinition.fromInline('')), + /cannot be empty/); + test.done(); + }, + }, + + 'apigateway.ApiDefinition.fromAsset': { + 'fails if a directory is given for an asset'(test: Test) { + // GIVEN + const fileAsset = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'authorizers')); + + // THEN + test.throws(() => defineRestApi(fileAsset), /Asset cannot be a \.zip file or a directory/); + test.done(); + }, + + 'only one Asset object gets created even if multiple functions use the same AssetApiDefinition'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'MyStack'); + const directoryAsset = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + + // WHEN + new apigw.SpecRestApi(stack, 'API1', { + apiDefinition: directoryAsset, + }); + + new apigw.SpecRestApi(stack, 'API2', { + apiDefinition: directoryAsset, + }); + + // THEN + const assembly = app.synth(); + const synthesized = assembly.stacks[0]; + + // API1 has an asset, API2 does not + test.deepEqual(synthesized.assets.length, 1); + test.done(); + }, + }, +}; + +function defineRestApi(definition: apigw.ApiDefinition) { + const stack = new cdk.Stack(); + return new apigw.SpecRestApi(stack, 'API', { + apiDefinition: definition, + }); +} diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts b/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts deleted file mode 100644 index 38da5f730e589..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/test.apidefinition.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; -import * as cdk from '@aws-cdk/core'; -import * as cxapi from '@aws-cdk/cx-api'; -import { Test } from 'nodeunit'; -import * as path from 'path'; -import * as apigw from '../lib'; - -// tslint:disable:no-string-literal - -export = { - 'apigateway.APIDefinition.fromInline': { - 'fails if larger than 4096 bytes or is empty'(test: Test) { - test.throws( - () => defineRestApi(apigw.APIDefinition.fromInline(generateRandomString(4097))), - /too large, must be <= 4096 but is 4097/); - test.throws( - () => defineRestApi(apigw.APIDefinition.fromInline('')), - /cannot be empty/); - test.done(); - }, - }, - 'apigateway.APIDefinition.fromAsset': { - 'fails if a directory is given for an asset'(test: Test) { - // GIVEN - const fileAsset = apigw.APIDefinition.fromAsset(path.join(__dirname, 'authorizers')); - - // THEN - test.throws(() => defineRestApi(fileAsset), /Asset cannot be a \.zip file or a directory/); - test.done(); - }, - - 'only one Asset object gets created even if multiple functions use the same AssetAPIDefinition'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'MyStack'); - const directoryAsset = apigw.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); - - // WHEN - new apigw.APIDefinitionRestApi(stack, 'API1', { - apiDefinition: directoryAsset, - }); - - new apigw.APIDefinitionRestApi(stack, 'API2', { - apiDefinition: directoryAsset, - }); - - // THEN - const assembly = app.synth(); - const synthesized = assembly.stacks[0]; - - // API1 has an asset, API2 does not - test.deepEqual(synthesized.assets.length, 1); - test.done(); - }, - - 'adds definition asset metadata'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); - - const definition = apigw.APIDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); - - // WHEN - new apigw.APIDefinitionRestApi(stack, 'API1', { - apiDefinition: definition, - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { - Metadata: { - [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58.yaml', - [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'APIDefinition', - }, - }, ResourcePart.CompleteDefinition)); - test.done(); - }, - }, - - 'apigateway.APIDefinition.fromCfnParameters': { - "automatically creates the Bucket and Key parameters when it's used in a Rest API"(test: Test) { - const stack = new cdk.Stack(); - const definition = new apigw.CfnParametersAPIDefinition(); - new apigw.APIDefinitionRestApi(stack, 'API', { - apiDefinition: definition, - }); - - expect(stack).to(haveResourceLike('AWS::ApiGateway::RestApi', { - Name: 'API', - BodyS3Location: { - Bucket: { - Ref: 'APIAPIDefinitionBucketNameParameter95D18AC4', - }, - Key: { - Ref: 'APIAPIDefinitionObjectKeyParameterDB007608', - }, - }, - })); - - test.equal(stack.resolve(definition.bucketNameParam), 'APIAPIDefinitionBucketNameParameter95D18AC4'); - test.equal(stack.resolve(definition.objectKeyParam), 'APIAPIDefinitionObjectKeyParameterDB007608'); - - test.done(); - }, - - 'does not allow accessing the Parameter properties before being used in a Rest API'(test: Test) { - const definition = new apigw.CfnParametersAPIDefinition(); - - test.throws(() => { - test.notEqual(definition.bucketNameParam, undefined); - }, /bucketNameParam/); - - test.throws(() => { - test.notEqual(definition.objectKeyParam, undefined); - }, /objectKeyParam/); - - test.done(); - }, - - 'allows passing custom Parameters when creating it'(test: Test) { - const stack = new cdk.Stack(); - const bucketNameParam = new cdk.CfnParameter(stack, 'BucketNameParam', { - type: 'String', - }); - const bucketKeyParam = new cdk.CfnParameter(stack, 'ObjectKeyParam', { - type: 'String', - }); - - const definition = apigw.APIDefinition.fromCfnParameters({ - bucketNameParam, - objectKeyParam: bucketKeyParam, - }); - - test.equal(stack.resolve(definition.bucketNameParam), 'BucketNameParam'); - test.equal(stack.resolve(definition.objectKeyParam), 'ObjectKeyParam'); - - new apigw.APIDefinitionRestApi(stack, 'API', { - apiDefinition: definition, - }); - - expect(stack).to(haveResourceLike('AWS::ApiGateway::RestApi', { - BodyS3Location: { - Bucket: { - Ref: 'BucketNameParam', - }, - Key: { - Ref: 'ObjectKeyParam', - }, - }, - })); - - test.done(); - }, - - 'can assign parameters'(test: Test) { - // given - const stack = new cdk.Stack(); - const code = new apigw.CfnParametersAPIDefinition({ - bucketNameParam: new cdk.CfnParameter(stack, 'BucketNameParam', { - type: 'String', - }), - objectKeyParam: new cdk.CfnParameter(stack, 'ObjectKeyParam', { - type: 'String', - }), - }); - - // when - const overrides = stack.resolve(code.assign({ - bucketName: 'SomeBucketName', - objectKey: 'SomeObjectKey', - })); - - // then - test.equal(overrides['BucketNameParam'], 'SomeBucketName'); - test.equal(overrides['ObjectKeyParam'], 'SomeObjectKey'); - - test.done(); - }, - }, -}; - -function defineRestApi(definition: apigw.APIDefinition) { - const stack = new cdk.Stack(); - return new apigw.APIDefinitionRestApi(stack, 'API', { - apiDefinition: definition, - }); -} - -function generateRandomString(bytes: number) { - let s = ''; - for (let i = 0; i < bytes; ++i) { - s += String.fromCharCode(Math.round(Math.random() * 256)); - } - return s; -} From 926f070d99c72846d6b436326f2a9d38c1aebbe5 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 7 May 2020 07:57:06 +0100 Subject: [PATCH 07/11] switch out from using the CfnRestApi.S3LocationProperty --- .../aws-apigateway/lib/api-definition.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts index e4ffbe0266668..dca45c7fb3792 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -1,7 +1,6 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import * as cdk from '@aws-cdk/core'; -import { CfnRestApi } from './apigateway.generated'; /** * Represents an OpenAPI definition asset. @@ -38,6 +37,21 @@ export abstract class ApiDefinition { public abstract bind(scope: cdk.Construct): ApiDefinitionConfig; } +/** + * S3 location of the API definition file + */ +export interface ApiDefinitionS3Location { + /** The S3 bucket */ + readonly bucket: string; + /** The S3 key */ + readonly key: string; + /** + * An optional version + * @default - latest version + */ + readonly version?: string; +} + /** * Post-Binding Configuration for a CDK construct */ @@ -45,20 +59,20 @@ export interface ApiDefinitionConfig { /** * The location of the specification in S3 (mutually exclusive with `inlineDefinition`). * - * @default a new parameter will be created + * @default - API definition is not an S3 location */ - readonly s3Location?: CfnRestApi.S3LocationProperty; + readonly s3Location?: ApiDefinitionS3Location; /** * Inline specification (mutually exclusive with `s3Location`). * - * @default a new parameter will be created + * @default - API definition is not defined inline */ readonly inlineDefinition?: string; } /** - * Swagger/OpenAPI specification from an S3 archive + * OpenAPI specification from an S3 archive. */ export class S3ApiDefinition extends ApiDefinition { private bucketName: string; @@ -85,7 +99,7 @@ export class S3ApiDefinition extends ApiDefinition { } /** - * OpenAPI specification from an inline string (limited to 4KiB) + * OpenAPI specification from an inline string. */ export class InlineApiDefinition extends ApiDefinition { constructor(private definition: string) { @@ -122,14 +136,14 @@ export class AssetApiDefinition extends ApiDefinition { }); } - if (this.asset?.isZipArchive) { + if (this.asset.isZipArchive) { throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`); } return { s3Location: { - bucket: this.asset?.s3BucketName, - key: this.asset?.s3ObjectKey, + bucket: this.asset.s3BucketName, + key: this.asset.s3ObjectKey, }, }; } From e936e990886d6bff4a42695dff60ee70f536ffd1 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 7 May 2020 12:19:16 +0100 Subject: [PATCH 08/11] switch from inline string to inline object. upgraded testing. --- .../aws-apigateway/lib/api-definition.ts | 61 +++++- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 9 +- ... integ.api-definition.asset.expected.json} | 44 ++++- .../test/integ.api-definition.asset.ts | 21 +++ .../integ.api-definition.inline.expected.json | 177 ++++++++++++++++++ .../test/integ.api-definition.inline.ts | 66 +++++++ .../test/integ.restapi.fromdefinition.ts | 13 -- .../test/sample-definition.yaml | 2 +- .../test/test.api-definition.ts | 51 ++++- 9 files changed, 403 insertions(+), 41 deletions(-) rename packages/@aws-cdk/aws-apigateway/test/{integ.restapi.fromdefinition.expected.json => integ.api-definition.asset.expected.json} (67%) create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts index dca45c7fb3792..64eac660619ff 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -14,9 +14,52 @@ export abstract class ApiDefinition { } /** - * Creates an API definition from a string + * Create an API definition from an inline object. The inline object must follow the + * schema of OpenAPI 2.0 or OpenAPI 3.0 + * + * @example + * ApiDefinition.fromInline({ + * openapi: '3.0.2', + * paths: { + * '/pets': { + * get: { + * 'responses': { + * 200: { + * content: { + * 'application/json': { + * schema: { + * $ref: '#/components/schemas/Empty', + * }, + * }, + * }, + * }, + * }, + * 'x-amazon-apigateway-integration': { + * responses: { + * default: { + * statusCode: '200', + * }, + * }, + * requestTemplates: { + * 'application/json': '{"statusCode": 200}', + * }, + * passthroughBehavior: 'when_no_match', + * type: 'mock', + * }, + * }, + * }, + * }, + * components: { + * schemas: { + * Empty: { + * title: 'Empty Schema', + * type: 'object', + * }, + * }, + * }, + * }); */ - public static fromInline(definition: string): InlineApiDefinition { + public static fromInline(definition: any): InlineApiDefinition { return new InlineApiDefinition(definition); } @@ -68,7 +111,7 @@ export interface ApiDefinitionConfig { * * @default - API definition is not defined inline */ - readonly inlineDefinition?: string; + readonly inlineDefinition?: any; } /** @@ -99,14 +142,18 @@ export class S3ApiDefinition extends ApiDefinition { } /** - * OpenAPI specification from an inline string. + * OpenAPI specification from an inline JSON object. */ export class InlineApiDefinition extends ApiDefinition { - constructor(private definition: string) { + constructor(private definition: any) { super(); - if (definition.length === 0) { - throw new Error('Inline API definition cannot be empty'); + if (typeof(definition) !== 'object') { + throw new Error('definition should be of type object'); + } + + if (Object.keys(definition).length === 0) { + throw new Error('JSON definition cannot be empty'); } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index f8339313c44d0..290935693337f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,7 +1,7 @@ import { IVpcEndpoint } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { CfnOutput, Construct, IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; -import { ApiDefinition, ApiDefinitionConfig } from './api-definition'; +import { ApiDefinition } from './api-definition'; import { ApiKey, IApiKey } from './api-key'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { CorsOptions } from './cors'; @@ -620,10 +620,3 @@ class RootResource extends ResourceBase { } } } - -export function verifyAPIDefinitionConfig(definition: ApiDefinitionConfig) { - // mutually exclusive - if ((!definition.inlineDefinition && !definition.s3Location) || (definition.inlineDefinition && definition.s3Location)) { - throw new Error('APIDefinition must specify one of "inlineDefinition" or "s3Location" but not both'); - } -} diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json similarity index 67% rename from packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.expected.json rename to packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json index a03d0395259c2..71dd02f17ab9a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json @@ -5,7 +5,7 @@ "Properties": { "BodyS3Location": { "Bucket": { - "Ref": "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3BucketC3882222" + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29" }, "Key": { "Fn::Join": [ @@ -18,7 +18,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3VersionKey1FFB68C8" + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F" } ] } @@ -31,7 +31,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3VersionKey1FFB68C8" + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F" } ] } @@ -137,20 +137,46 @@ ] ] } + }, + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/pets" + ] + ] + } } }, "Parameters": { - "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3BucketC3882222": { + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29": { "Type": "String", - "Description": "S3 bucket for asset \"b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58\"" + "Description": "S3 bucket for asset \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" }, - "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58S3VersionKey1FFB68C8": { + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F": { "Type": "String", - "Description": "S3 key for asset version \"b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58\"" + "Description": "S3 key for asset version \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" }, - "AssetParametersb4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58ArtifactHash1A35D17D": { + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbArtifactHashA9C91B6D": { "Type": "String", - "Description": "Artifact hash for asset \"b4e546901387aeeb588eabec043d35a7fdbe4d304185ae7ab763bb3ae4e61d58\"" + "Description": "Artifact hash for asset \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts new file mode 100644 index 0000000000000..1b8531ccad8d5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts @@ -0,0 +1,21 @@ +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as apigateway from '../lib'; + +/* + * Stack verification steps: + * * `curl -i ` should return HTTP code 200 + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integtest-restapi-fromdefinition-asset'); + +const api = new apigateway.SpecRestApi(stack, 'my-api', { + apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), +}); + +new cdk.CfnOutput(stack, 'PetsURL', { + value: api.urlForPath('/pets'), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json new file mode 100644 index 0000000000000..3eaae1ff8fd58 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json @@ -0,0 +1,177 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "openapi": "3.0.2", + "info": { + "version": "1.0.0", + "title": "Test API for CDK" + }, + "paths": { + "/pets": { + "get": { + "summary": "Test Method", + "operationId": "testMethod", + "responses": { + "200": { + "description": "A paged array of pets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Empty" + } + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200" + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "passthroughBehavior": "when_no_match", + "type": "mock" + } + } + } + }, + "components": { + "schemas": { + "Empty": { + "title": "Empty Schema", + "type": "object" + } + } + } + }, + "Name": "my-api" + } + }, + "myapiDeployment92F2CB49": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + } + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB49" + }, + "StageName": "prod" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + } + }, + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/pets" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts new file mode 100644 index 0000000000000..4d9f3bcf76364 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts @@ -0,0 +1,66 @@ +import * as cdk from '@aws-cdk/core'; +import * as apigateway from '../lib'; + +/* + * Stack verification steps: + * * `curl -i ` should return HTTP code 200 + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integtest-restapi-fromdefinition-inline'); + +const api = new apigateway.SpecRestApi(stack, 'my-api', { + apiDefinition: apigateway.ApiDefinition.fromInline({ + openapi: '3.0.2', + info: { + version: '1.0.0', + title: 'Test API for CDK', + }, + paths: { + '/pets': { + get: { + 'summary': 'Test Method', + 'operationId': 'testMethod', + 'responses': { + 200: { + description: 'A paged array of pets', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Empty', + }, + }, + }, + }, + }, + 'x-amazon-apigateway-integration': { + responses: { + default: { + statusCode: '200', + }, + }, + requestTemplates: { + 'application/json': '{"statusCode": 200}', + }, + passthroughBehavior: 'when_no_match', + type: 'mock', + }, + }, + }, + }, + components: { + schemas: { + Empty: { + title: 'Empty Schema', + type: 'object', + }, + }, + }, + }), +}); + +new cdk.CfnOutput(stack, 'PetsURL', { + value: api.urlForPath('/pets'), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts deleted file mode 100644 index 3abbdc09b14d4..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.fromdefinition.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import * as path from 'path'; -import * as apigateway from '../lib'; - -const app = new cdk.App(); - -const stack = new cdk.Stack(app, 'test-apigateway-restapi-fromdefinition'); - -new apigateway.SpecRestApi(stack, 'my-api', { - apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), -}); - -app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml index b40573a0c08b8..a0dd197f67c37 100644 --- a/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml +++ b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml @@ -3,7 +3,7 @@ info: version: 1.0.0 title: Test API for CDK paths: - /testing: + /pets: get: summary: Test Method operationId: testMethod diff --git a/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts index 9c7a01643cdef..7753c83fe47a8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts @@ -1,19 +1,49 @@ +import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as path from 'path'; import * as apigw from '../lib'; export = { - 'apigateway.ApiDefinition.fromInline': { - 'fails if inline definition is empty'(test: Test) { + 'apigateway.ApiDefinition.fromJson': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const definition = { + key1: 'val1', + }; + const config = apigw.ApiDefinition.fromInline(definition).bind(stack); + test.deepEqual(config.inlineDefinition, definition); + test.ok(config.s3Location === undefined); + test.done(); + }, + + 'fails if Json definition is empty'(test: Test) { test.throws( - () => defineRestApi(apigw.ApiDefinition.fromInline('')), + () => defineRestApi(apigw.ApiDefinition.fromInline({})), /cannot be empty/); test.done(); }, + + 'fails if definition is not an object'(test: Test) { + test.throws( + () => defineRestApi(apigw.ApiDefinition.fromInline('not-json')), + /should be of type object/); + test.done(); + }, }, 'apigateway.ApiDefinition.fromAsset': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const config = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')).bind(stack); + test.ok(config.inlineDefinition === undefined); + test.ok(config.s3Location !== undefined); + test.deepEqual(stack.resolve(config.s3Location!.bucket), { + Ref: 'AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29', + }); + test.done(); + }, + 'fails if a directory is given for an asset'(test: Test) { // GIVEN const fileAsset = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'authorizers')); @@ -47,6 +77,21 @@ export = { test.done(); }, }, + + 'apigateway.ApiDefinition.fromBucket': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'my-bucket'); + const config = apigw.ApiDefinition.fromBucket(bucket, 'my-key', 'my-version').bind(stack); + test.ok(config.inlineDefinition === undefined); + test.ok(config.s3Location !== undefined); + test.deepEqual(stack.resolve(config.s3Location!.bucket), { + Ref: 'mybucket15D133BF', + }); + test.equals(config.s3Location!.key, 'my-key'); + test.done(); + }, + }, }; function defineRestApi(definition: apigw.ApiDefinition) { From 7f06cccaf9a179b30e69799aeed1e0bb90054826 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 7 May 2020 15:06:21 +0100 Subject: [PATCH 09/11] renaming & adjustments --- packages/@aws-cdk/aws-apigateway/README.md | 10 ++--- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 40 +++++++------------ 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 32164132f6086..43917439f8eaf 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -915,11 +915,11 @@ There are a number of limitations in using OpenAPI definitions in API Gateway. R notes for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-apis) for more details. -**Note:** When starting off with an OpenAPI definition, it is possible to add Resources and Methods via the CDK APIs, in -addition to what has already been defined in the OpenAPI. -However, it is important to note that it is not possible to override or modify Resources and Methods that are already -defined in the OpenAPI definition. While these will be present in the final CloudFormation template, they will be -ignored by API Gateway. The definition in the OpenAPI definition file always takes precedent. +**Note:** When starting off with an OpenAPI definition using `SpecRestApi`, it is not possible to configure some +properties that can be configured directly in the OpenAPI specification file. This is to prevent people duplication +of these properties and potential confusion. +Further, it is currently also not possible to configure Methods and Resources in addition to the ones in the +specification file. ## APIGateway v2 diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index a86730d8568a2..a4f29e888a99a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -27,7 +27,7 @@ export interface IRestApi extends IResourceBase { /** * Represents the props that all Rest APIs share */ -export interface BaseRestApiProps extends ResourceOptions { +export interface RestApiOptions extends ResourceOptions { /** * Indicates if a Deployment should be automatically created for this API, * and recreated when the API model (resources, methods) changes. @@ -122,10 +122,9 @@ export interface BaseRestApiProps extends ResourceOptions { } /** - * Represents props unique to creating a new Rest API (without a Swagger/OpenAPI - * specification) + * Props to create a new instance of RestApi */ -export interface RestApiProps extends BaseRestApiProps { +export interface RestApiProps extends RestApiOptions { /** * A description of the purpose of this API Gateway RestApi resource. * @@ -189,14 +188,15 @@ export interface RestApiProps extends BaseRestApiProps { /** * Props to instantiate a new SpecRestApi */ -export interface SpecRestApiProps extends BaseRestApiProps { +export interface SpecRestApiProps extends RestApiOptions { /** - * A Swagger/OpenAPI definition compatible with API Gateway. + * An OpenAPI definition compatible with API Gateway. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-import-api.html */ readonly apiDefinition: ApiDefinition; } -abstract class BaseRestApi extends Resource implements IRestApi { +abstract class RestApiBase extends Resource implements IRestApi { /** * The ID of this API Gateway RestApi. */ @@ -220,7 +220,7 @@ abstract class BaseRestApi extends Resource implements IRestApi { private _latestDeployment?: Deployment; private _domainName?: DomainName; - constructor(scope: Construct, id: string, props: BaseRestApiProps = { }) { + constructor(scope: Construct, id: string, props: RestApiOptions = { }) { super(scope, id, { physicalName: props.restApiName || id, }); @@ -315,17 +315,6 @@ abstract class BaseRestApi extends Resource implements IRestApi { }); } - /** - * Performs validation of the REST API. - */ - protected validate() { - if (this.methods.length === 0) { - return [ 'The REST API doesn\'t contain any methods' ]; - } - - return []; - } - protected configureCloudWatchRole(apiResource: CfnRestApi) { const role = new iam.Role(this, 'CloudWatchRole', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), @@ -335,11 +324,11 @@ abstract class BaseRestApi extends Resource implements IRestApi { const resource = new CfnAccount(this, 'Account', { cloudWatchRoleArn: role.roleArn, }); - + resource.node.addDependency(apiResource); } - protected configureDeployment(props: RestApiProps) { + protected configureDeployment(props: RestApiOptions) { const deploy = props.deploy === undefined ? true : props.deploy; if (deploy) { @@ -371,14 +360,15 @@ abstract class BaseRestApi extends Resource implements IRestApi { * Represents a REST API in Amazon API Gateway, created with an OpenAPI specification. * * Some properties normally accessible on @see {@link RestApi} - such as the description - - * must be declared in the specification. + * must be declared in the specification. All Resources and Methods need to be defined as + * part of the OpenAPI specification file, and cannot be added via the CDK. * * By default, the API will automatically be deployed and accessible from a * public endpoint. * * @resource AWS::ApiGateway::RestApi */ -export class SpecRestApi extends BaseRestApi { +export class SpecRestApi extends RestApiBase { /** * The ID of this API Gateway RestApi. */ @@ -426,7 +416,7 @@ export class SpecRestApi extends BaseRestApi { * By default, the API will automatically be deployed and accessible from a * public endpoint. */ -export class RestApi extends BaseRestApi implements IRestApi { +export class RestApi extends RestApiBase implements IRestApi { public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { class Import extends Resource implements IRestApi { public readonly restApiId = restApiId; @@ -544,7 +534,7 @@ export class RestApi extends BaseRestApi implements IRestApi { */ protected validate() { if (this.methods.length === 0) { - return [ 'The REST API doesn\'t contain any methods' ]; + return [ "The REST API doesn't contain any methods" ]; } return []; From 85630604b1ad1baa995845cb8ad74b88a4ce88a0 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 11 May 2020 10:34:36 +0100 Subject: [PATCH 10/11] Update packages/@aws-cdk/aws-apigateway/README.md --- packages/@aws-cdk/aws-apigateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 43917439f8eaf..6ad94b6a3e22f 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -907,7 +907,7 @@ The following code creates a REST API using an external OpenAPI definition JSON ```ts const api = new apigateway.SpecRestApi(this, 'books-api', { - apiDefinition: apigateway.APIDefinition.fromAsset('path-to-file.json') + apiDefinition: apigateway.ApiDefinition.fromAsset('path-to-file.json') }); ``` From f85618c82a0b62c4597ce1cb1711bdd8e84db92f Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 11 May 2020 11:55:44 +0100 Subject: [PATCH 11/11] add experimental tags --- packages/@aws-cdk/aws-apigateway/lib/api-definition.ts | 8 ++++++++ packages/@aws-cdk/aws-apigateway/lib/restapi.ts | 3 +++ 2 files changed, 11 insertions(+) diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts index 64eac660619ff..652c531de9c38 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -4,10 +4,12 @@ import * as cdk from '@aws-cdk/core'; /** * Represents an OpenAPI definition asset. + * @experimental */ export abstract class ApiDefinition { /** * Creates an API definition from a specification file in an S3 bucket + * @experimental */ public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3ApiDefinition { return new S3ApiDefinition(bucket, key, objectVersion); @@ -65,6 +67,7 @@ export abstract class ApiDefinition { /** * Loads the API specification from a local disk asset. + * @experimental */ public static fromAsset(file: string, options?: s3_assets.AssetOptions): AssetApiDefinition { return new AssetApiDefinition(file, options); @@ -82,6 +85,7 @@ export abstract class ApiDefinition { /** * S3 location of the API definition file + * @experimental */ export interface ApiDefinitionS3Location { /** The S3 bucket */ @@ -97,6 +101,7 @@ export interface ApiDefinitionS3Location { /** * Post-Binding Configuration for a CDK construct + * @experimental */ export interface ApiDefinitionConfig { /** @@ -116,6 +121,7 @@ export interface ApiDefinitionConfig { /** * OpenAPI specification from an S3 archive. + * @experimental */ export class S3ApiDefinition extends ApiDefinition { private bucketName: string; @@ -143,6 +149,7 @@ export class S3ApiDefinition extends ApiDefinition { /** * OpenAPI specification from an inline JSON object. + * @experimental */ export class InlineApiDefinition extends ApiDefinition { constructor(private definition: any) { @@ -166,6 +173,7 @@ export class InlineApiDefinition extends ApiDefinition { /** * OpenAPI specification from a local file. + * @experimental */ export class AssetApiDefinition extends ApiDefinition { private asset?: s3_assets.Asset; diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index a4f29e888a99a..4e372f91c996c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -187,6 +187,7 @@ export interface RestApiProps extends RestApiOptions { /** * Props to instantiate a new SpecRestApi + * @experimental */ export interface SpecRestApiProps extends RestApiOptions { /** @@ -366,6 +367,8 @@ abstract class RestApiBase extends Resource implements IRestApi { * By default, the API will automatically be deployed and accessible from a * public endpoint. * + * @experimental + * * @resource AWS::ApiGateway::RestApi */ export class SpecRestApi extends RestApiBase {