From 5d0c70195e781242bec75488e73ba72a4b13f5c4 Mon Sep 17 00:00:00 2001 From: Jerry Kindall <52084730+Jerry-AWS@users.noreply.github.com> Date: Fri, 14 Aug 2020 15:53:11 -0700 Subject: [PATCH 01/63] Merge pull request #1 from aws/master (#9729) From 1893a5eb45e8190f4d1ebbb07b49b6b3492084cc Mon Sep 17 00:00:00 2001 From: Jerry Kindall <52084730+Jerry-AWS@users.noreply.github.com> Date: Fri, 14 Aug 2020 16:35:12 -0700 Subject: [PATCH 02/63] docs: Add info about stability to repo README (#9691) Add information about AWS Construct Library module stability to repo README for highest possible visibility --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a406ed66cfe48..c413d0df84cb2 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ how to use AWS. The AWS Construct Library aims to reduce the complexity and glue-logic required when integrating various AWS services to achieve your goals on AWS. +Modules in the AWS Construct Library are designated Experimental while we build +them; experimental modules may have breaking API changes in any release. After +a module is designated Stable, it adheres to [semantic versioning](https://semver.org/), +and only major releases can have breaking changes. Each module's stability designation +is available on its Overview page in the [AWS CDK API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html). +For more information, see [Versioning](https://docs.aws.amazon.com/cdk/latest/guide/reference.html#versioning) +in the CDK Developer Guide. + [CDK framework]: https://docs.aws.amazon.com/cdk/latest/guide/home.html [constructs]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html [stacks]: https://docs.aws.amazon.com/cdk/latest/guide/stacks.html From 5732b8ee727d3c1a59e6dce100ec4179640970cb Mon Sep 17 00:00:00 2001 From: Bryan Pan Date: Fri, 14 Aug 2020 17:11:46 -0700 Subject: [PATCH 03/63] feat(appsync): import existing graphql api (#9254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **[ISSUE]** Appsync missing `fromXxx` functionality, making multiple stack appsync interaction impossible. **[Approach]** Created a base class for  `GraphQLApi` and a `IGraphQLApi` interface for imports. Added `fromXxxAttributes` functions to code base that can add dataSources. **[Notes]** Only accessible props from `IGraphQLApi` are `apiId` and `arn`. Only accessible functions from `IGraphQLApi` are the `addXxxDataSource` functions. Added `props-physical-name:@aws-cdk/aws-appsync.GraphQLApiProps` as linter exception because the breaking change isn't worth the return of making the physical name (name exists already). Added `from-method:@aws-cdk/aws-appsync.GraphQLApi` as linter exception because a `fromGraphQLApiAttributes` function will turn into `from_graph_q_l_api_attributes` in python. Fixes #6959 BREAKING CHANGE: **appsync.addXxxDataSource** `name` and `description` props are now optional and in an `DataSourceOptions` interface. - **appsync**: the props `name` and `description` in `addXxxDataSource` have been moved into new props `options` of type `DataSourceOptions` - **appsync**: `DataSourceOptions.name` defaults to id - **appsync**: `DataSourceOptions.description` defaults to undefined ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appsync/README.md | 20 +- .../@aws-cdk/aws-appsync/lib/data-source.ts | 16 +- .../aws-appsync/lib/graphqlapi-base.ts | 177 +++++++++++++++ .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 139 ++++++------ packages/@aws-cdk/aws-appsync/lib/index.ts | 1 + packages/@aws-cdk/aws-appsync/lib/resolver.ts | 6 +- packages/@aws-cdk/aws-appsync/package.json | 4 +- .../aws-appsync/test/appsync-apikey.test.ts | 2 +- .../aws-appsync/test/appsync-dynamodb.test.ts | 143 ++++++++++++- .../aws-appsync/test/appsync-http.test.ts | 106 +++++++++ .../aws-appsync/test/appsync-lambda.test.ts | 121 +++++++++++ .../aws-appsync/test/appsync-none.test.ts | 115 ++++++++++ .../test/integ.api-import.expected.json | 201 ++++++++++++++++++ .../aws-appsync/test/integ.api-import.ts | 68 ++++++ .../test/integ.graphql-iam.expected.json | 31 ++- .../aws-appsync/test/integ.graphql-iam.ts | 2 +- .../test/integ.graphql.expected.json | 119 +++++------ .../aws-appsync/test/integ.graphql.ts | 10 +- 18 files changed, 1099 insertions(+), 182 deletions(-) create mode 100644 packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.api-import.ts diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index f086184760132..cb3b4d3708c5f 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -64,7 +64,7 @@ const demoTable = new db.Table(stack, 'DemoTable', { }, }); -const demoDS = api.addDynamoDbDataSource('demoDataSource', 'Table for Demos"', demoTable); +const demoDS = api.addDynamoDbDataSource('demoDataSource', demoTable); // Resolver for the Query "getDemos" that scans the DyanmoDb table and returns the entire list. demoDS.createResolver({ @@ -83,6 +83,24 @@ demoDS.createResolver({ }); ``` +## Imports + +Any GraphQL Api that has been created outside the stack can be imported from +another stack into your CDK app. Utilizing the `fromXxx` function, you have +the ability to add data sources and resolvers through a `IGraphQLApi` interface. + +```ts +const importedApi = appsync.GraphQLApi.fromGraphQLApiAttributes(stack, 'IApi', { + graphqlApiId: api.apiId, + graphqlArn: api.arn, +}); +importedApi.addDynamoDbDataSource('TableDataSource', table); +``` + +If you don't specify `graphqlArn` in `fromXxxAttributes`, CDK will autogenerate +the expected `arn` for the imported api, given the `apiId`. For creating data +sources and resolvers, an `apiId` is sufficient. + ## Permissions When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role diff --git a/packages/@aws-cdk/aws-appsync/lib/data-source.ts b/packages/@aws-cdk/aws-appsync/lib/data-source.ts index 0daf1f996a452..04c617b12e5d0 100644 --- a/packages/@aws-cdk/aws-appsync/lib/data-source.ts +++ b/packages/@aws-cdk/aws-appsync/lib/data-source.ts @@ -3,7 +3,7 @@ import { IGrantable, IPrincipal, IRole, Role, ServicePrincipal } from '@aws-cdk/ import { IFunction } from '@aws-cdk/aws-lambda'; import { Construct, IResolvable } from '@aws-cdk/core'; import { CfnDataSource } from './appsync.generated'; -import { GraphQLApi } from './graphqlapi'; +import { IGraphqlApi } from './graphqlapi-base'; import { BaseResolverProps, Resolver } from './resolver'; /** @@ -13,11 +13,13 @@ export interface BaseDataSourceProps { /** * The API to attach this data source to */ - readonly api: GraphQLApi; + readonly api: IGraphqlApi; /** * The name of the data source + * + * @default - id of data source */ - readonly name: string; + readonly name?: string; /** * the description of the data source * @@ -91,7 +93,7 @@ export abstract class BaseDataSource extends Construct { */ public readonly ds: CfnDataSource; - protected api: GraphQLApi; + protected api: IGraphqlApi; protected serviceRole?: IRole; constructor(scope: Construct, id: string, props: BackedDataSourceProps, extended: ExtendedDataSourceProps) { @@ -100,15 +102,15 @@ export abstract class BaseDataSource extends Construct { if (extended.type !== 'NONE') { this.serviceRole = props.serviceRole || new Role(this, 'ServiceRole', { assumedBy: new ServicePrincipal('appsync') }); } - + const name = props.name ?? id; this.ds = new CfnDataSource(this, 'Resource', { apiId: props.api.apiId, - name: props.name, + name: name, description: props.description, serviceRoleArn: this.serviceRole?.roleArn, ...extended, }); - this.name = props.name; + this.name = name; this.api = props.api; } diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts new file mode 100644 index 0000000000000..2f357c142db91 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts @@ -0,0 +1,177 @@ +import { ITable } from '@aws-cdk/aws-dynamodb'; +import { IFunction } from '@aws-cdk/aws-lambda'; +import { CfnResource, IResource, Resource } from '@aws-cdk/core'; +import { DynamoDbDataSource, HttpDataSource, LambdaDataSource, NoneDataSource } from './data-source'; + +/** + * Optional configuration for data sources + */ +export interface DataSourceOptions { + /** + * The name of the data source, overrides the id given by cdk + * + * @default - generated by cdk given the id + */ + readonly name?: string; + + /** + * The description of the data source + * + * @default - No description + */ + readonly description?: string; +} + +/** + * Interface for GraphQL + */ +export interface IGraphqlApi extends IResource { + + /** + * an unique AWS AppSync GraphQL API identifier + * i.e. 'lxz775lwdrgcndgz3nurvac7oa' + * + * @attribute + */ + readonly apiId: string; + + /** + * the ARN of the API + * + * @attribute + */ + readonly arn: string; + + /** + * add a new dummy data source to this API. Useful for pipeline resolvers + * and for backend changes that don't require a data source. + * + * @param id The data source's id + * @param options The optional configuration for this data source + */ + addNoneDataSource(id: string, options?: DataSourceOptions): NoneDataSource; + + /** + * add a new DynamoDB data source to this API + * + * @param id The data source's id + * @param table The DynamoDB table backing this data source + * @param options The optional configuration for this data source + */ + addDynamoDbDataSource(id: string, table: ITable, options?: DataSourceOptions): DynamoDbDataSource; + + /** + * add a new http data source to this API + * + * @param id The data source's id + * @param endpoint The http endpoint + * @param options The optional configuration for this data source + */ + addHttpDataSource(id: string, endpoint: string, options?: DataSourceOptions): HttpDataSource; + + /** + * add a new Lambda data source to this API + * + * @param id The data source's id + * @param lambdaFunction The Lambda function to call to interact with this data source + * @param options The optional configuration for this data source + */ + addLambdaDataSource(id: string, lambdaFunction: IFunction, options?: DataSourceOptions): LambdaDataSource; + + /** + * Add schema dependency if not imported + * + * @param construct the dependee + */ + addSchemaDependency(construct: CfnResource): boolean; +} + +/** + * Base Class for GraphQL API + */ +export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { + + /** + * an unique AWS AppSync GraphQL API identifier + * i.e. 'lxz775lwdrgcndgz3nurvac7oa' + */ + public abstract readonly apiId: string; + + /** + * the ARN of the API + */ + public abstract readonly arn: string; + + /** + * add a new dummy data source to this API. Useful for pipeline resolvers + * and for backend changes that don't require a data source. + * + * @param id The data source's id + * @param options The optional configuration for this data source + */ + public addNoneDataSource(id: string, options?: DataSourceOptions): NoneDataSource { + return new NoneDataSource(this, id, { + api: this, + name: options?.name, + description: options?.description, + }); + } + + /** + * add a new DynamoDB data source to this API + * + * @param id The data source's id + * @param table The DynamoDB table backing this data source + * @param options The optional configuration for this data source + */ + public addDynamoDbDataSource(id: string, table: ITable, options?: DataSourceOptions): DynamoDbDataSource { + return new DynamoDbDataSource(this, id, { + api: this, + table, + name: options?.name, + description: options?.description, + }); + } + + /** + * add a new http data source to this API + * + * @param id The data source's id + * @param endpoint The http endpoint + * @param options The optional configuration for this data source + */ + public addHttpDataSource(id: string, endpoint: string, options?: DataSourceOptions): HttpDataSource { + return new HttpDataSource(this, id, { + api: this, + endpoint, + name: options?.name, + description: options?.description, + }); + } + + /** + * add a new Lambda data source to this API + * + * @param id The data source's id + * @param lambdaFunction The Lambda function to call to interact with this data source + * @param options The optional configuration for this data source + */ + public addLambdaDataSource(id: string, lambdaFunction: IFunction, options?: DataSourceOptions): LambdaDataSource { + return new LambdaDataSource(this, id, { + api: this, + lambdaFunction, + name: options?.name, + description: options?.description, + }); + } + + /** + * Add schema dependency if not imported + * + * @param construct the dependee + */ + public addSchemaDependency(construct: CfnResource): boolean { + construct; + return false; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index c2c1e768e2d9b..af51819d16f42 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -1,11 +1,9 @@ import { readFileSync } from 'fs'; import { IUserPool } from '@aws-cdk/aws-cognito'; -import { ITable } from '@aws-cdk/aws-dynamodb'; -import { Grant, IGrantable, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; -import { IFunction } from '@aws-cdk/aws-lambda'; -import { Construct, Duration, IResolvable, Stack } from '@aws-cdk/core'; -import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'; -import { DynamoDbDataSource, HttpDataSource, LambdaDataSource, NoneDataSource } from './data-source'; +import { ManagedPolicy, Role, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; +import { CfnResource, Construct, Duration, IResolvable, Stack } from '@aws-cdk/core'; +import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'; +import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base'; /** * enum with all possible values for AppSync authorization type @@ -221,7 +219,6 @@ export enum SchemaDefinition { * Properties for an AppSync GraphQL API */ export interface GraphQLApiProps { - /** * the name of the GraphQL API */ @@ -324,31 +321,79 @@ export class IamResource { } } +/** + * Attributes for GraphQL imports + */ +export interface GraphqlApiAttributes { + /** + * an unique AWS AppSync GraphQL API identifier + * i.e. 'lxz775lwdrgcndgz3nurvac7oa' + */ + readonly graphqlApiId: string, + + /** + * the arn for the GraphQL Api + * @default - autogenerated arn + */ + readonly graphqlApiArn?: string, +} + /** * An AppSync GraphQL API + * + * @resource AWS::AppSync::GraphQLApi */ -export class GraphQLApi extends Construct { +export class GraphQLApi extends GraphqlApiBase { + /** + * Import a GraphQL API through this function + * + * @param scope scope + * @param id id + * @param attrs GraphQL API Attributes of an API + */ + public static fromGraphqlApiAttributes(scope: Construct, id: string, attrs: GraphqlApiAttributes): IGraphqlApi { + const arn = attrs.graphqlApiArn ?? Stack.of(scope).formatArn({ + service: 'appsync', + resource: `apis/${attrs.graphqlApiId}`, + }); + class Import extends GraphqlApiBase { + public readonly apiId = attrs.graphqlApiId; + public readonly arn = arn; + constructor (s: Construct, i: string){ + super(s, i); + } + } + return new Import(scope, id); + } /** - * the id of the GraphQL API + * an unique AWS AppSync GraphQL API identifier + * i.e. 'lxz775lwdrgcndgz3nurvac7oa' */ public readonly apiId: string; + /** * the ARN of the API */ public readonly arn: string; + /** * the URL of the endpoint created by AppSync + * + * @attribute */ public readonly graphQlUrl: string; + /** * the name of the API */ public readonly name: string; + /** * underlying CFN schema resource */ public readonly schema: CfnGraphQLSchema; + /** * the configured API key, if present */ @@ -434,72 +479,6 @@ export class GraphQLApi extends Construct { this.schema = this.defineSchema(props.schemaDefinitionFile); } - /** - * add a new dummy data source to this API - * @param name The name of the data source - * @param description The description of the data source - */ - public addNoneDataSource(name: string, description: string): NoneDataSource { - return new NoneDataSource(this, `${name}DS`, { - api: this, - description, - name, - }); - } - - /** - * add a new DynamoDB data source to this API - * @param name The name of the data source - * @param description The description of the data source - * @param table The DynamoDB table backing this data source [disable-awslint:ref-via-interface] - */ - public addDynamoDbDataSource( - name: string, - description: string, - table: ITable, - ): DynamoDbDataSource { - return new DynamoDbDataSource(this, `${name}DS`, { - api: this, - description, - name, - table, - }); - } - - /** - * add a new http data source to this API - * @param name The name of the data source - * @param description The description of the data source - * @param endpoint The http endpoint - */ - public addHttpDataSource(name: string, description: string, endpoint: string): HttpDataSource { - return new HttpDataSource(this, `${name}DS`, { - api: this, - description, - endpoint, - name, - }); - } - - /** - * add a new Lambda data source to this API - * @param name The name of the data source - * @param description The description of the data source - * @param lambdaFunction The Lambda function to call to interact with this data source - */ - public addLambdaDataSource( - name: string, - description: string, - lambdaFunction: IFunction, - ): LambdaDataSource { - return new LambdaDataSource(this, `${name}DS`, { - api: this, - description, - name, - lambdaFunction, - }); - } - /** * Adds an IAM policy statement associated with this GraphQLApi to an IAM * principal's policy. @@ -613,6 +592,16 @@ export class GraphQLApi extends Construct { } } + /** + * Add schema dependency to a given construct + * + * @param construct the dependee + */ + public addSchemaDependency(construct: CfnResource): boolean { + construct.addDependsOn(this.schema); + return true; + } + private formatOpenIdConnectConfig( config: OpenIdConnectConfig, ): CfnGraphQLApi.OpenIDConnectConfigProperty { diff --git a/packages/@aws-cdk/aws-appsync/lib/index.ts b/packages/@aws-cdk/aws-appsync/lib/index.ts index 852cbed1c54f6..e0736c1ecc397 100644 --- a/packages/@aws-cdk/aws-appsync/lib/index.ts +++ b/packages/@aws-cdk/aws-appsync/lib/index.ts @@ -5,3 +5,4 @@ export * from './data-source'; export * from './mapping-template'; export * from './resolver'; export * from './graphqlapi'; +export * from './graphqlapi-base'; diff --git a/packages/@aws-cdk/aws-appsync/lib/resolver.ts b/packages/@aws-cdk/aws-appsync/lib/resolver.ts index 5a226c448069a..205155cc791c8 100644 --- a/packages/@aws-cdk/aws-appsync/lib/resolver.ts +++ b/packages/@aws-cdk/aws-appsync/lib/resolver.ts @@ -1,7 +1,7 @@ import { Construct } from '@aws-cdk/core'; import { CfnResolver } from './appsync.generated'; import { BaseDataSource } from './data-source'; -import { GraphQLApi } from './graphqlapi'; +import { IGraphqlApi } from './graphqlapi-base'; import { MappingTemplate } from './mapping-template'; /** @@ -44,7 +44,7 @@ export interface ResolverProps extends BaseResolverProps { /** * The API this resolver is attached to */ - readonly api: GraphQLApi; + readonly api: IGraphqlApi; /** * The data source this resolver is using * @@ -79,7 +79,7 @@ export class Resolver extends Construct { requestMappingTemplate: props.requestMappingTemplate ? props.requestMappingTemplate.renderTemplate() : undefined, responseMappingTemplate: props.responseMappingTemplate ? props.responseMappingTemplate.renderTemplate() : undefined, }); - this.resolver.addDependsOn(props.api.schema); + props.api.addSchemaDependency(this.resolver); if (props.dataSource) { this.resolver.addDependsOn(props.dataSource.ds); } diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index 07fa3726c0f9b..84bc2d3af6f7f 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -96,7 +96,9 @@ "exclude": [ "no-unused-type:@aws-cdk/aws-appsync.ApiKeyConfig", "no-unused-type:@aws-cdk/aws-appsync.UserPoolConfig", - "no-unused-type:@aws-cdk/aws-appsync.UserPoolDefaultAction" + "no-unused-type:@aws-cdk/aws-appsync.UserPoolDefaultAction", + "props-physical-name:@aws-cdk/aws-appsync.GraphQLApiProps", + "from-method:@aws-cdk/aws-appsync.GraphQLApi" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts index 39a09c1f1c955..9f47904770fcb 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; -import * as cdk from '@aws-cdk/core'; import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; import * as appsync from '../lib'; describe('AppSync Authorization Config', () => { diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts index 37e1ecc0ea57b..a43e13afa9528 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts @@ -1,15 +1,93 @@ import '@aws-cdk/assert/jest'; -import { MappingTemplate, PrimaryKey, Values } from '../lib'; +import * as path from 'path'; +import * as db from '@aws-cdk/aws-dynamodb'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; function joined(str: string): string { return str.replace(/\s+/g, ''); } +// GLOBAL GIVEN +let stack: cdk.Stack; +let api: appsync.GraphQLApi; +beforeEach(() => { + stack = new cdk.Stack(); + api = new appsync.GraphQLApi(stack, 'baseApi', { + name: 'api', + schemaDefinition: appsync.SchemaDefinition.FILE, + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + }); +}); + +describe('DynamoDb Data Source configuration', () => { + // GIVEN + let table: db.Table; + beforeEach(() => { + table = new db.Table(stack, 'table', { + partitionKey: { + name: 'id', + type: db.AttributeType.STRING, + }, + }); + }); + + test('default configuration produces name `DynamoDbCDKDataSource`', () => { + // WHEN + api.addDynamoDbDataSource('ds', table); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AMAZON_DYNAMODB', + Name: 'ds', + }); + }); + + test('appsync configures name correctly', () => { + // WHEN + api.addDynamoDbDataSource('ds', table, { + name: 'custom', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AMAZON_DYNAMODB', + Name: 'custom', + }); + }); + + test('appsync configures name and description correctly', () => { + // WHEN + api.addDynamoDbDataSource('ds', table, { + name: 'custom', + description: 'custom description', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AMAZON_DYNAMODB', + Name: 'custom', + Description: 'custom description', + }); + }); + + test('appsync errors when creating multiple dynamo db data sources with no configuration', () => { + // WHEN + const when = () => { + api.addDynamoDbDataSource('ds', table); + api.addDynamoDbDataSource('ds', table); + }; + + // THEN + expect(when).toThrow("There is already a Construct with name 'ds' in GraphQLApi [baseApi]"); + }); +}); + describe('DynamoDB Mapping Templates', () => { test('PutItem projecting all', () => { - const template = MappingTemplate.dynamoDbPutItem( - PrimaryKey.partition('id').is('id'), - Values.projecting(), + const template = appsync.MappingTemplate.dynamoDbPutItem( + appsync.PrimaryKey.partition('id').is('id'), + appsync.Values.projecting(), ); const rendered = joined(template.renderTemplate()); @@ -28,9 +106,9 @@ describe('DynamoDB Mapping Templates', () => { }); test('PutItem with invididual attributes', () => { - const template = MappingTemplate.dynamoDbPutItem( - PrimaryKey.partition('id').is('id'), - Values.attribute('val').is('ctx.args.val'), + const template = appsync.MappingTemplate.dynamoDbPutItem( + appsync.PrimaryKey.partition('id').is('id'), + appsync.Values.attribute('val').is('ctx.args.val'), ); const rendered = joined(template.renderTemplate()); @@ -50,9 +128,9 @@ describe('DynamoDB Mapping Templates', () => { }); test('PutItem with additional attributes', () => { - const template = MappingTemplate.dynamoDbPutItem( - PrimaryKey.partition('id').is('id'), - Values.projecting().attribute('val').is('ctx.args.val'), + const template = appsync.MappingTemplate.dynamoDbPutItem( + appsync.PrimaryKey.partition('id').is('id'), + appsync.Values.projecting().attribute('val').is('ctx.args.val'), ); const rendered = joined(template.renderTemplate()); @@ -70,4 +148,49 @@ describe('DynamoDB Mapping Templates', () => { }`), ); }); +}); + +describe('adding DynamoDb data source from imported api', () => { + // GIVEN + let table: db.Table; + beforeEach(() => { + table = new db.Table(stack, 'table', { + partitionKey: { + name: 'id', + type: db.AttributeType.STRING, + }, + }); + }); + + test('imported api can add DynamoDbDataSource from id', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + }); + importedApi.addDynamoDbDataSource('ds', table); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AMAZON_DYNAMODB', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); + + test('imported api can add DynamoDbDataSource from attributes', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + graphqlApiArn: api.arn, + }); + importedApi.addDynamoDbDataSource('ds', table); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AMAZON_DYNAMODB', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); + }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts new file mode 100644 index 0000000000000..25961b87cb8d2 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts @@ -0,0 +1,106 @@ +import '@aws-cdk/assert/jest'; +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +// GLOBAL GIVEN +let stack: cdk.Stack; +let api: appsync.GraphQLApi; +let endpoint: string; +beforeEach(() => { + stack = new cdk.Stack(); + api = new appsync.GraphQLApi(stack, 'baseApi', { + name: 'api', + schemaDefinition: appsync.SchemaDefinition.FILE, + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + }); + endpoint = 'aws.amazon.com'; +}); + +describe('Http Data Source configuration', () => { + + test('default configuration produces name `HttpCDKDataSource`', () => { + // WHEN + api.addHttpDataSource('ds', endpoint); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'HTTP', + Name: 'ds', + }); + }); + + test('appsync configures name correctly', () => { + // WHEN + api.addHttpDataSource('ds', endpoint, { + name: 'custom', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'HTTP', + Name: 'custom', + }); + }); + + test('appsync configures name and description correctly', () => { + // WHEN + api.addHttpDataSource('ds', endpoint, { + name: 'custom', + description: 'custom description', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'HTTP', + Name: 'custom', + Description: 'custom description', + }); + }); + + test('appsync errors when creating multiple http data sources with no configuration', () => { + // WHEN + const when = () => { + api.addHttpDataSource('ds', endpoint); + api.addHttpDataSource('ds', endpoint); + }; + + // THEN + expect(when).toThrow("There is already a Construct with name 'ds' in GraphQLApi [baseApi]"); + }); +}); + +describe('adding http data source from imported api', () => { + test('imported api can add HttpDataSource from id', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + }); + importedApi.addHttpDataSource('ds', endpoint); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'HTTP', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); + + test('imported api can add HttpDataSource from attributes', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + graphqlApiArn: api.arn, + }); + importedApi.addHttpDataSource('ds', endpoint); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'HTTP', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); +}); + + diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts new file mode 100644 index 0000000000000..b178fee2282cf --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts @@ -0,0 +1,121 @@ +import '@aws-cdk/assert/jest'; +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +// GLOBAL GIVEN +let stack: cdk.Stack; +let api: appsync.GraphQLApi; +beforeEach(() => { + stack = new cdk.Stack(); + api = new appsync.GraphQLApi(stack, 'baseApi', { + name: 'api', + schemaDefinition: appsync.SchemaDefinition.FILE, + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + }); +}); + +describe('Lambda Data Source configuration', () => { + // GIVEN + let func: lambda.Function; + beforeEach(() => { + func = new lambda.Function(stack, 'func', { + code: lambda.Code.fromAsset('test/verify'), + handler: 'iam-query.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + }); + + test('default configuration produces name `TableCDKDataSource`', () => { + // WHEN + api.addLambdaDataSource('ds', func); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + Name: 'ds', + }); + }); + + test('appsync configures name correctly', () => { + // WHEN + api.addLambdaDataSource('ds', func, { + name: 'custom', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + Name: 'custom', + }); + }); + + test('appsync configures name and description correctly', () => { + // WHEN + api.addLambdaDataSource('ds', func, { + name: 'custom', + description: 'custom description', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + Name: 'custom', + Description: 'custom description', + }); + }); + + test('appsync errors when creating multiple lambda data sources with no configuration', () => { + // WHEN + const when = () => { + api.addLambdaDataSource('ds', func); + api.addLambdaDataSource('ds', func); + }; + + // THEN + expect(when).toThrow("There is already a Construct with name 'ds' in GraphQLApi [baseApi]"); + }); +}); + +describe('adding lambda data source from imported api',() => { + let func: lambda.Function; + beforeEach(() => { + func = new lambda.Function(stack, 'func', { + code: lambda.Code.fromAsset('test/verify'), + handler: 'iam-query.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + }); + + test('imported api can add LambdaDbDataSource from id', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + }); + importedApi.addLambdaDataSource('ds', func); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); + + test('imported api can add LambdaDataSource from attributes', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + graphqlApiArn: api.arn, + }); + importedApi.addLambdaDataSource('ds', func); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts new file mode 100644 index 0000000000000..13ab095ed0fee --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts @@ -0,0 +1,115 @@ +import '@aws-cdk/assert/jest'; +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +// GLOBAL GIVEN +let stack: cdk.Stack; +let api: appsync.GraphQLApi; +beforeEach(() => { + stack = new cdk.Stack(); + api = new appsync.GraphQLApi(stack, 'baseApi', { + name: 'api', + schemaDefinition: appsync.SchemaDefinition.FILE, + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + }); +}); + +describe('None Data Source configuration', () => { + + test('default configuration produces name `NoneCDKDataSource`', () => { + // WHEN + api.addNoneDataSource('ds'); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'NONE', + Name: 'ds', + }); + }); + + test('appsync configures name correctly', () => { + // WHEN + api.addNoneDataSource('ds', { + name: 'custom', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'NONE', + Name: 'custom', + }); + }); + + test('appsync configures name and description correctly', () => { + // WHEN + api.addNoneDataSource('ds', { + name: 'custom', + description: 'custom description', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'NONE', + Name: 'custom', + Description: 'custom description', + }); + }); + + test('appsync errors when creating multiple none data sources with no configuration', () => { + // WHEN + const when = () => { + api.addNoneDataSource('ds'); + api.addNoneDataSource('ds'); + }; + + // THEN + expect(when).toThrow("There is already a Construct with name 'ds' in GraphQLApi [baseApi]"); + }); + + test('appsync errors when creating multiple none data sources with same name configuration', () => { + // WHEN + const when = () => { + api.addNoneDataSource('ds1', { name: 'custom' }); + api.addNoneDataSource('ds2', { name: 'custom' }); + }; + + // THEN + expect(when).not.toThrowError(); + }); +}); + +describe('adding none data source from imported api', () => { + test('imported api can add NoneDataSource from id', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + }); + importedApi.addNoneDataSource('none'); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'NONE', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); + + test('imported api can add NoneDataSource from attributes', () => { + // WHEN + const importedApi = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + graphqlApiArn: api.arn, + }); + importedApi.addNoneDataSource('none'); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'NONE', + ApiId: { 'Fn::GetAtt': [ 'baseApiCDA4D43A', 'ApiId' ], + }, + }); + }); +}); + + diff --git a/packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json new file mode 100644 index 0000000000000..3a0b9a76b760c --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json @@ -0,0 +1,201 @@ +[ + { + "Resources": { + "baseApiCDA4D43A": { + "Type": "AWS::AppSync::GraphQLApi", + "Properties": { + "AuthenticationType": "API_KEY", + "Name": "baseApi" + } + }, + "baseApiDefaultAPIKeyApiKey4804ACE5": { + "Type": "AWS::AppSync::ApiKey", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "Description": "Default API Key created by CDK" + } + }, + "baseApiSchemaB12C7BB0": { + "Type": "AWS::AppSync::GraphQLSchema", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "Definition": "type test {\n version: String!\n}\n\ntype Query {\n getTests: [ test! ]!\n}\n\ntype Mutation {\n addTest(version: String!): test\n}\n" + } + } + }, + "Outputs": { + "ExportsOutputFnGetAttbaseApiCDA4D43AApiId50287E68": { + "Value": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "Export": { + "Name": "baseStack:ExportsOutputFnGetAttbaseApiCDA4D43AApiId50287E68" + } + } + } + }, + { + "Resources": { + "ApidsServiceRoleADC7D124": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ApidsServiceRoleDefaultPolicyE5E18D6D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestTable5769773A", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ApidsServiceRoleDefaultPolicyE5E18D6D", + "Roles": [ + { + "Ref": "ApidsServiceRoleADC7D124" + } + ] + } + }, + "Apids0DB53FEA": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::ImportValue": "baseStack:ExportsOutputFnGetAttbaseApiCDA4D43AApiId50287E68" + }, + "Name": "ds", + "Type": "AMAZON_DYNAMODB", + "DynamoDBConfig": { + "AwsRegion": { + "Ref": "AWS::Region" + }, + "TableName": { + "Ref": "TestTable5769773A" + } + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "ApidsServiceRoleADC7D124", + "Arn" + ] + } + } + }, + "ApidsQuerygetTestsResolver952F49EE": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::ImportValue": "baseStack:ExportsOutputFnGetAttbaseApiCDA4D43AApiId50287E68" + }, + "FieldName": "getTests", + "TypeName": "Query", + "DataSourceName": "ds", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}", + "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" + }, + "DependsOn": [ + "Apids0DB53FEA" + ] + }, + "ApidsMutationaddTestResolverBCF0400B": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::ImportValue": "baseStack:ExportsOutputFnGetAttbaseApiCDA4D43AApiId50287E68" + }, + "FieldName": "addTest", + "TypeName": "Mutation", + "DataSourceName": "ds", + "Kind": "UNIT", + "RequestMappingTemplate": "\n #set($input = $ctx.args.test)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "ResponseMappingTemplate": "$util.toJson($ctx.result)" + }, + "DependsOn": [ + "Apids0DB53FEA" + ] + }, + "TestTable5769773A": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "api2noneC88DB89F": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::ImportValue": "baseStack:ExportsOutputFnGetAttbaseApiCDA4D43AApiId50287E68" + }, + "Name": "none", + "Type": "NONE" + } + } + } + } +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.api-import.ts b/packages/@aws-cdk/aws-appsync/test/integ.api-import.ts new file mode 100644 index 0000000000000..74a8942246e51 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.api-import.ts @@ -0,0 +1,68 @@ +/// !cdk-integ * + +import * as path from 'path'; +import * as db from '@aws-cdk/aws-dynamodb'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +/* + * Creates an Appsync GraphQL API in a separate stack. + * Add dependencies to imported api. + * + * Stack verification steps: + * Install dependencies and deploy integration test. Check if data sources are + * connected to the graphQL Api + * + * -- cdk deploy --app 'node integ.api-import.js' stack -- start -- + * -- aws appsync list-graphql-apis -- obtain api id -- + * -- aws appsync list-data-sources --api-id [api_id] -- testDS/None -- + * -- cdk destroy --app 'node integ.api-import.js' stack baseStack -- clean -- + */ + +const app = new cdk.App(); +const baseStack = new cdk.Stack(app, 'baseStack'); + +const baseApi = new appsync.GraphQLApi(baseStack, 'baseApi', { + name: 'baseApi', + schemaDefinition: appsync.SchemaDefinition.FILE, + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), +}); + +const stack = new cdk.Stack(app, 'stack'); +const api = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'Api', { + graphqlApiId: `${baseApi.apiId}`, +}); + +const testTable = new db.Table(stack, 'TestTable', { + billingMode: db.BillingMode.PAY_PER_REQUEST, + partitionKey: { + name: 'id', + type: db.AttributeType.STRING, + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +const testDS = api.addDynamoDbDataSource('ds', testTable); + +testDS.createResolver({ + typeName: 'Query', + fieldName: 'getTests', + requestMappingTemplate: appsync.MappingTemplate.dynamoDbScanTable(), + responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(), +}); + +testDS.createResolver({ + typeName: 'Mutation', + fieldName: 'addTest', + requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem(appsync.PrimaryKey.partition('id').auto(), appsync.Values.projecting('test')), + responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(), +}); + +const api2 = appsync.GraphQLApi.fromGraphqlApiAttributes(stack, 'api2', { + graphqlApiId: baseApi.apiId, + graphqlApiArn: baseApi.arn, +}); + +api2.addNoneDataSource('none'); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json index 1d90ae0c15d4b..eda4a662ffc58 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json @@ -63,7 +63,7 @@ "Definition": "type test @aws_iam {\n id: String!\n version: String!\n}\n\ntype Query {\n getTest(id: String!): test\n getTests: [ test! ]\n @aws_iam \n}\n\ninput TestInput {\n version: String!\n}\n\ntype Mutation {\n addTest(input: TestInput!): test\n @aws_iam\n}\n" } }, - "ApitestDataSourceDSServiceRoleE543E310": { + "ApidsServiceRoleADC7D124": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -80,7 +80,7 @@ } } }, - "ApitestDataSourceDSServiceRoleDefaultPolicy53D2252C": { + "ApidsServiceRoleDefaultPolicyE5E18D6D": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -114,15 +114,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ApitestDataSourceDSServiceRoleDefaultPolicy53D2252C", + "PolicyName": "ApidsServiceRoleDefaultPolicyE5E18D6D", "Roles": [ { - "Ref": "ApitestDataSourceDSServiceRoleE543E310" + "Ref": "ApidsServiceRoleADC7D124" } ] } }, - "ApitestDataSourceDS776EA507": { + "Apids0DB53FEA": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { @@ -133,7 +133,6 @@ }, "Name": "testDataSource", "Type": "AMAZON_DYNAMODB", - "Description": "Table for Tests\"", "DynamoDBConfig": { "AwsRegion": { "Ref": "AWS::Region" @@ -144,13 +143,13 @@ }, "ServiceRoleArn": { "Fn::GetAtt": [ - "ApitestDataSourceDSServiceRoleE543E310", + "ApidsServiceRoleADC7D124", "Arn" ] } } }, - "ApitestDataSourceDSQuerygetTestResolver1A3C9201": { + "ApidsQuerygetTestResolverCCED7EC2": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -167,11 +166,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiSchema510EECD7", - "ApitestDataSourceDS776EA507" + "Apids0DB53FEA", + "ApiSchema510EECD7" ] }, - "ApitestDataSourceDSQuerygetTestsResolver61ED88B6": { + "ApidsQuerygetTestsResolver952F49EE": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -188,11 +187,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiSchema510EECD7", - "ApitestDataSourceDS776EA507" + "Apids0DB53FEA", + "ApiSchema510EECD7" ] }, - "ApitestDataSourceDSMutationaddTestResolver0D3A4591": { + "ApidsMutationaddTestResolverBCF0400B": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -209,8 +208,8 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiSchema510EECD7", - "ApitestDataSourceDS776EA507" + "Apids0DB53FEA", + "ApiSchema510EECD7" ] }, "TestTable5769773A": { diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts index d55a63adcfb62..9613070f49dbc 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts @@ -65,7 +65,7 @@ const testTable = new Table(stack, 'TestTable', { removalPolicy: RemovalPolicy.DESTROY, }); -const testDS = api.addDynamoDbDataSource('testDataSource', 'Table for Tests"', testTable); +const testDS = api.addDynamoDbDataSource('ds', testTable, {name: 'testDataSource'}); testDS.createResolver({ typeName: 'Query', diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index 80d03e19f7767..0406904978e66 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -75,7 +75,7 @@ "Definition": "type ServiceVersion @aws_api_key {\n version: String!\n}\n\ntype Customer @aws_api_key {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Order @aws_api_key {\n customer: String!\n order: String!\n}\n\ntype Payment @aws_api_key {\n id: String!\n amount: String!\n}\n\ninput PaymentInput {\n amount: String!\n}\n\ntype Query @aws_api_key {\n getServiceVersion: ServiceVersion\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n getCustomerOrdersEq(customer: String): Order\n getCustomerOrdersLt(customer: String): Order\n getCustomerOrdersLe(customer: String): Order\n getCustomerOrdersGt(customer: String): Order\n getCustomerOrdersGe(customer: String): Order\n getCustomerOrdersFilter(customer: String, order: String): Order\n getCustomerOrdersBetween(customer: String, order1: String, order2: String): Order\n getPayment(id: String): Payment\n}\n\ninput FirstOrderInput {\n product: String!\n quantity: Int!\n}\n\ntype Mutation @aws_api_key {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order\n savePayment(payment: PaymentInput!): Payment\n doPostOnAws: String!\n}\n" } }, - "ApiNoneDSB4E6495F": { + "Apinone1F55F3F3": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { @@ -85,11 +85,10 @@ ] }, "Name": "None", - "Type": "NONE", - "Description": "Dummy data source" + "Type": "NONE" } }, - "ApiNoneDSQuerygetServiceVersionResolver4AF8FD42": { + "ApinoneQuerygetServiceVersionResolver336A3C2C": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -106,11 +105,11 @@ "ResponseMappingTemplate": "{\"version\":\"v1\"}" }, "DependsOn": [ - "ApiNoneDSB4E6495F", + "Apinone1F55F3F3", "ApiSchema510EECD7" ] }, - "ApiCustomerDSServiceRoleA929BCF7": { + "ApicustomerDsServiceRole76CAD539": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -127,7 +126,7 @@ } } }, - "ApiCustomerDSServiceRoleDefaultPolicy8C927D33": { + "ApicustomerDsServiceRoleDefaultPolicyF8E72AE7": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -161,15 +160,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ApiCustomerDSServiceRoleDefaultPolicy8C927D33", + "PolicyName": "ApicustomerDsServiceRoleDefaultPolicyF8E72AE7", "Roles": [ { - "Ref": "ApiCustomerDSServiceRoleA929BCF7" + "Ref": "ApicustomerDsServiceRole76CAD539" } ] } }, - "ApiCustomerDS8C23CB2D": { + "ApicustomerDsFE73DAC5": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { @@ -180,7 +179,6 @@ }, "Name": "Customer", "Type": "AMAZON_DYNAMODB", - "Description": "The customer data source", "DynamoDBConfig": { "AwsRegion": { "Ref": "AWS::Region" @@ -191,13 +189,13 @@ }, "ServiceRoleArn": { "Fn::GetAtt": [ - "ApiCustomerDSServiceRoleA929BCF7", + "ApicustomerDsServiceRole76CAD539", "Arn" ] } } }, - "ApiCustomerDSQuerygetCustomersResolver0F8B3416": { + "ApicustomerDsQuerygetCustomersResolverA74C8A2E": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -214,11 +212,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiCustomerDS8C23CB2D", + "ApicustomerDsFE73DAC5", "ApiSchema510EECD7" ] }, - "ApiCustomerDSQuerygetCustomerResolver0DC795BE": { + "ApicustomerDsQuerygetCustomerResolver3649A130": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -235,11 +233,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiCustomerDS8C23CB2D", + "ApicustomerDsFE73DAC5", "ApiSchema510EECD7" ] }, - "ApiCustomerDSMutationaddCustomerResolverC9041B1C": { + "ApicustomerDsMutationaddCustomerResolver4DE5B517": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -256,11 +254,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiCustomerDS8C23CB2D", + "ApicustomerDsFE73DAC5", "ApiSchema510EECD7" ] }, - "ApiCustomerDSMutationsaveCustomerResolverB707057E": { + "ApicustomerDsMutationsaveCustomerResolver241DD231": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -277,11 +275,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiCustomerDS8C23CB2D", + "ApicustomerDsFE73DAC5", "ApiSchema510EECD7" ] }, - "ApiCustomerDSMutationsaveCustomerWithFirstOrderResolver8B1277A8": { + "ApicustomerDsMutationsaveCustomerWithFirstOrderResolver7DE2CBC8": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -298,11 +296,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiCustomerDS8C23CB2D", + "ApicustomerDsFE73DAC5", "ApiSchema510EECD7" ] }, - "ApiCustomerDSMutationremoveCustomerResolverA0046C60": { + "ApicustomerDsMutationremoveCustomerResolverAD3AE7F5": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -319,11 +317,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiCustomerDS8C23CB2D", + "ApicustomerDsFE73DAC5", "ApiSchema510EECD7" ] }, - "ApiOrderDSServiceRole81A3E9E7": { + "ApiorderDsServiceRoleCC2040C0": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -340,7 +338,7 @@ } } }, - "ApiOrderDSServiceRoleDefaultPolicyDAB14B69": { + "ApiorderDsServiceRoleDefaultPolicy3315FCF4": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -374,15 +372,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ApiOrderDSServiceRoleDefaultPolicyDAB14B69", + "PolicyName": "ApiorderDsServiceRoleDefaultPolicy3315FCF4", "Roles": [ { - "Ref": "ApiOrderDSServiceRole81A3E9E7" + "Ref": "ApiorderDsServiceRoleCC2040C0" } ] } }, - "ApiOrderDS4B3EEEBA": { + "ApiorderDsB50C8AAD": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { @@ -393,7 +391,6 @@ }, "Name": "Order", "Type": "AMAZON_DYNAMODB", - "Description": "The order data source", "DynamoDBConfig": { "AwsRegion": { "Ref": "AWS::Region" @@ -404,13 +401,13 @@ }, "ServiceRoleArn": { "Fn::GetAtt": [ - "ApiOrderDSServiceRole81A3E9E7", + "ApiorderDsServiceRoleCC2040C0", "Arn" ] } } }, - "ApiOrderDSQuerygetCustomerOrdersEqResolverFCC2003B": { + "ApiorderDsQuerygetCustomerOrdersEqResolverEF9D5350": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -427,11 +424,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiOrderDS4B3EEEBA", + "ApiorderDsB50C8AAD", "ApiSchema510EECD7" ] }, - "ApiOrderDSQuerygetCustomerOrdersLtResolverE2C5E19E": { + "ApiorderDsQuerygetCustomerOrdersLtResolver909F3D8F": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -448,11 +445,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiOrderDS4B3EEEBA", + "ApiorderDsB50C8AAD", "ApiSchema510EECD7" ] }, - "ApiOrderDSQuerygetCustomerOrdersLeResolver95C3D740": { + "ApiorderDsQuerygetCustomerOrdersLeResolverF230A8BE": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -469,11 +466,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiOrderDS4B3EEEBA", + "ApiorderDsB50C8AAD", "ApiSchema510EECD7" ] }, - "ApiOrderDSQuerygetCustomerOrdersGtResolver1B99CF3D": { + "ApiorderDsQuerygetCustomerOrdersGtResolverF01F806B": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -490,11 +487,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiOrderDS4B3EEEBA", + "ApiorderDsB50C8AAD", "ApiSchema510EECD7" ] }, - "ApiOrderDSQuerygetCustomerOrdersGeResolver5138C680": { + "ApiorderDsQuerygetCustomerOrdersGeResolver63CAD303": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -511,11 +508,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiOrderDS4B3EEEBA", + "ApiorderDsB50C8AAD", "ApiSchema510EECD7" ] }, - "ApiOrderDSQuerygetCustomerOrdersFilterResolver4433E2B4": { + "ApiorderDsQuerygetCustomerOrdersFilterResolverCD2B8747": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -532,11 +529,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiOrderDS4B3EEEBA", + "ApiorderDsB50C8AAD", "ApiSchema510EECD7" ] }, - "ApiOrderDSQuerygetCustomerOrdersBetweenResolver14F90CFD": { + "ApiorderDsQuerygetCustomerOrdersBetweenResolver7DEE368E": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -553,11 +550,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" }, "DependsOn": [ - "ApiOrderDS4B3EEEBA", + "ApiorderDsB50C8AAD", "ApiSchema510EECD7" ] }, - "ApiPaymentDSServiceRole7A857DD9": { + "ApipaymentDsServiceRole0DAC58D6": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -574,7 +571,7 @@ } } }, - "ApiPaymentDSServiceRoleDefaultPolicy1BE875C5": { + "ApipaymentDsServiceRoleDefaultPolicy528E42B0": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -622,15 +619,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ApiPaymentDSServiceRoleDefaultPolicy1BE875C5", + "PolicyName": "ApipaymentDsServiceRoleDefaultPolicy528E42B0", "Roles": [ { - "Ref": "ApiPaymentDSServiceRole7A857DD9" + "Ref": "ApipaymentDsServiceRole0DAC58D6" } ] } }, - "ApiPaymentDS69022256": { + "ApipaymentDs95C7AC36": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { @@ -641,7 +638,6 @@ }, "Name": "Payment", "Type": "AMAZON_DYNAMODB", - "Description": "The payment data source", "DynamoDBConfig": { "AwsRegion": { "Ref": "AWS::Region" @@ -650,13 +646,13 @@ }, "ServiceRoleArn": { "Fn::GetAtt": [ - "ApiPaymentDSServiceRole7A857DD9", + "ApipaymentDsServiceRole0DAC58D6", "Arn" ] } } }, - "ApiPaymentDSQuerygetPaymentResolver25686F48": { + "ApipaymentDsQuerygetPaymentResolverD172BFC9": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -673,11 +669,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiPaymentDS69022256", + "ApipaymentDs95C7AC36", "ApiSchema510EECD7" ] }, - "ApiPaymentDSMutationsavePaymentResolver08FBC62D": { + "ApipaymentDsMutationsavePaymentResolverE09FE5BB": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -694,11 +690,11 @@ "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ - "ApiPaymentDS69022256", + "ApipaymentDs95C7AC36", "ApiSchema510EECD7" ] }, - "ApihttpDSServiceRole8B5C9457": { + "ApidsServiceRoleADC7D124": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -715,7 +711,7 @@ } } }, - "ApihttpDS91F12990": { + "Apids0DB53FEA": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { @@ -726,19 +722,18 @@ }, "Name": "http", "Type": "HTTP", - "Description": "The http data source", "HttpConfig": { "Endpoint": "https://aws.amazon.com/" }, "ServiceRoleArn": { "Fn::GetAtt": [ - "ApihttpDSServiceRole8B5C9457", + "ApidsServiceRoleADC7D124", "Arn" ] } } }, - "ApihttpDSMutationdoPostOnAwsResolverA9027953": { + "ApidsMutationdoPostOnAwsResolver9583D8A3": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { @@ -755,7 +750,7 @@ "ResponseMappingTemplate": "\n ## Raise a GraphQL field error in case of a datasource invocation error\n #if($ctx.error)\n $util.error($ctx.error.message, $ctx.error.type)\n #end\n ## if the response status code is not 200, then return an error. Else return the body **\n #if($ctx.result.statusCode == 200)\n ## If response is 200, return the body.\n $ctx.result.body\n #else\n ## If response is not 200, append the response to error block.\n $utils.appendError($ctx.result.body, \"$ctx.result.statusCode\")\n #end\n " }, "DependsOn": [ - "ApihttpDS91F12990", + "Apids0DB53FEA", "ApiSchema510EECD7" ] }, diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index 3b8564dba6865..add2e19cb8b68 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -54,7 +54,7 @@ const api = new GraphQLApi(stack, 'Api', { }, }); -const noneDS = api.addNoneDataSource('None', 'Dummy data source'); +const noneDS = api.addNoneDataSource('none', {name: 'None'}); noneDS.createResolver({ typeName: 'Query', @@ -99,9 +99,9 @@ new Table(stack, 'PaymentTable', { const paymentTable = Table.fromTableName(stack, 'ImportedPaymentTable', 'PaymentTable'); -const customerDS = api.addDynamoDbDataSource('Customer', 'The customer data source', customerTable); -const orderDS = api.addDynamoDbDataSource('Order', 'The order data source', orderTable); -const paymentDS = api.addDynamoDbDataSource('Payment', 'The payment data source', paymentTable); +const customerDS = api.addDynamoDbDataSource('customerDs', customerTable, {name: 'Customer'}); +const orderDS = api.addDynamoDbDataSource('orderDs', orderTable, {name: 'Order'}); +const paymentDS = api.addDynamoDbDataSource('paymentDs', paymentTable, {name: 'Payment'}); customerDS.createResolver({ typeName: 'Query', @@ -189,7 +189,7 @@ paymentDS.createResolver({ responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), }); -const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/'); +const httpDS = api.addHttpDataSource('ds', 'https://aws.amazon.com/', {name: 'http'}); httpDS.createResolver({ typeName: 'Mutation', From 6f1782fa679b5bc1a3bb3ec9e4afb71f8c64a951 Mon Sep 17 00:00:00 2001 From: Bryan Pan Date: Fri, 14 Aug 2020 20:46:02 -0700 Subject: [PATCH 04/63] feat(appsync): code-first schema allows for object type definition (#9417) AppSync now able to generate a code-first approach to generating object and interface types through code. You can also append to schema through the `GraphQLApi.appendToSchema` function. Feature List: - GraphqlTypes - InterfaceTypes - ObjectTypes - Directives All details can be found in the README.md Fixes #9307 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appsync/README.md | 171 ++++- .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 60 +- packages/@aws-cdk/aws-appsync/lib/index.ts | 1 + .../@aws-cdk/aws-appsync/lib/schema-types.ts | 604 ++++++++++++++++++ .../test/appsync-code-first.test.ts | 400 ++++++++++++ .../aws-appsync/test/appsync-schema.test.ts | 60 +- .../test/integ.graphql-schema.expected.json | 165 +++++ .../aws-appsync/test/integ.graphql-schema.ts | 66 ++ .../test/object-type-definitions.ts | 19 + .../test/scalar-type-defintions.ts | 64 ++ .../test/verify.integ.graphql-schema.sh | 32 + 11 files changed, 1606 insertions(+), 36 deletions(-) create mode 100644 packages/@aws-cdk/aws-appsync/lib/schema-types.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/object-type-definitions.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/scalar-type-defintions.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-schema.sh diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index cb3b4d3708c5f..96d0ce93d1879 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -60,7 +60,7 @@ const api = new appsync.GraphQLApi(stack, 'Api', { const demoTable = new db.Table(stack, 'DemoTable', { partitionKey: { name: 'id', - type: AttributeType.STRING, + type: db.AttributeType.STRING, }, }); @@ -179,3 +179,172 @@ api.grantMutation(role, 'updateExample'); // For custom types and granular design api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL'); ``` + +### Code-First Schema + +CDK offers the ability to generate your schema in a code-first approach. +A code-first approach offers a developer workflow with: +- **modularity**: organizing schema type definitions into different files +- **reusability**: simplifying down boilerplate/repetitive code +- **consistency**: resolvers and schema definition will always be synced + +The code-first approach allows for dynamic schema generation. You can generate your schema based on variables and templates to reduce code duplication. + +#### Code-First Example + +We are going to reference the [example](#Example) through a code-first approach. + +```ts +import * as appsync from '@aws-cdk/aws-appsync'; +import * as db from '@aws-cdk/aws-dynamodb'; + +const api = new appsync.GraphQLApi(stack, 'Api', { + name: 'demo', + schemaDefinition: appsync.SchemaDefinition.CODE, + authorizationConfig: { + defaultAuthorization: { + authorizationType: appsync.AuthorizationType.IAM + }, + }, +}); + +const demoTable = new db.Table(stack, 'DemoTable', { + partitionKey: { + name: 'id', + type: db.AttributeType.STRING, + }, +}); + +const demoDS = api.addDynamoDbDataSource('demoDataSource', 'Table for Demos', demoTable); + +// Schema Definition starts here + +const demo = api.addType('demo', { + definition: { + id: appsync.GraphqlType.string({ isRequired: true }), + version: appsync.GraphqlType.string({ isRequired: true }), + }, +}); + +``` + +#### GraphQL Types + +One of the benefits of GraphQL is its strongly typed nature. We define the +types within an object, query, mutation, interface, etc. as **GraphQL Types**. + +GraphQL Types are the building blocks of types, whether they are scalar, objects, +interfaces, etc. GraphQL Types can be: +- [**Scalar Types**](https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html): Id, Int, String, AWSDate, etc. +- **Object Types**: types that you generate (i.e. `demo` from the example above) +- **Interface Types**: abstract types that define the base implementation of other +Intermediate Types + +More concretely, GraphQL Types are simply the types appended to variables. +Referencing the object type `Demo` in the previous example, the GraphQL Types +is `String!` and is applied to both the names `id` and `version`. + +#### Intermediate Types + +Intermediate Types are abstractions above Scalar Types. They have a set of defined +fields, where each field corresponds to another type in the system. Intermediate +Types will be the meat of your GraphQL Schema as they are the types defined by you. + +Intermediate Types include: +- [**Interface Types**](#Interface-Types) +- [**Object Types**](#Object-Types) + +#### Interface Types + +**Interface Types** are abstract types that define the implementation of other +intermediate types. They are useful for eliminating duplication and can be used +to generate Object Types with less work. + +You can create Interface Types ***externally***. +```ts +const node = new appsync.InterfaceType('Node', { + definition: { + id: appsync.GraphqlType.string({ isRequired: true }), + }, +}); +``` + +#### Object Types + +**Object Types** are types that you declare. For example, in the [code-first example](#code-first-example) +the `demo` variable is an **Object Type**. **Object Types** are defined by +GraphQL Types and are only usable when linked to a GraphQL Api. + +You can create Object Types in three ways: + +1. Object Types can be created ***externally***. + ```ts + const api = new appsync.GraphQLApi(stack, 'Api', { + name: 'demo', + schemaDefinition: appsync.SchemaDefinition.CODE, + }); + const demo = new appsync.ObjectType('Demo', { + defintion: { + id: appsync.GraphqlType.string({ isRequired: true }), + version: appsync.GraphqlType.string({ isRequired: true }), + }, + }); + + api.appendToSchema(object.toString()); + ``` + > This method allows for reusability and modularity, ideal for larger projects. + For example, imagine moving all Object Type definition outside the stack. + + `scalar-types.ts` - a file for scalar type definitions + ```ts + export const required_string = new appsync.GraphqlType.string({ isRequired: true }); + ``` + + `object-types.ts` - a file for object type definitions + ```ts + import { required_string } from './scalar-types'; + export const demo = new appsync.ObjectType('Demo', { + defintion: { + id: required_string, + version: required_string, + }, + }); + ``` + + `cdk-stack.ts` - a file containing our cdk stack + ```ts + import { demo } from './object-types'; + api.appendToSchema(demo.toString()); + ``` + +2. Object Types can be created ***externally*** from an Interface Type. + ```ts + const node = new appsync.InterfaceType('Node', { + definition: { + id: appsync.GraphqlType.string({ isRequired: true }), + }, + }); + const demo = new appsync.ObjectType.implementInterface('Demo', { + interfaceTypes: [ node ], + defintion: { + version: appsync.GraphqlType.string({ isRequired: true }), + }, + }); + ``` + > This method allows for reusability and modularity, ideal for reducing code duplication. + +3. Object Types can be created ***internally*** within the GraphQL API. + ```ts + const api = new appsync.GraphQLApi(stack, 'Api', { + name: 'demo', + schemaDefinition: appsync.SchemaDefinition.CODE, + }); + api.addType('Demo', { + defintion: { + id: appsync.GraphqlType.string({ isRequired: true }), + version: appsync.GraphqlType.string({ isRequired: true }), + }, + }); + ``` + > This method provides easy use and is ideal for smaller projects. + \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index af51819d16f42..ac11c4a98decf 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -4,6 +4,7 @@ import { ManagedPolicy, Role, ServicePrincipal, Grant, IGrantable } from '@aws-c import { CfnResource, Construct, Duration, IResolvable, Stack } from '@aws-cdk/core'; import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'; import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base'; +import { ObjectType, ObjectTypeProps } from './schema-types'; /** * enum with all possible values for AppSync authorization type @@ -672,19 +673,6 @@ export class GraphQLApi extends GraphqlApiBase { return authModes ? this.formatAdditionalAuthorizationModes(authModes) : undefined; } - /** - * Sets schema defintiion to input if schema mode is configured with SchemaDefinition.CODE - * - * @param definition string that is the graphql representation of schema - * @experimental temporary - */ - public updateDefinition (definition: string): void{ - if ( this.schemaMode != SchemaDefinition.CODE ) { - throw new Error('API cannot add type because schema definition mode is not configured as CODE.'); - } - this.schema.definition = definition; - } - /** * Define schema based on props configuration * @param file the file name/s3 location of Schema @@ -692,13 +680,13 @@ export class GraphQLApi extends GraphqlApiBase { private defineSchema(file?: string): CfnGraphQLSchema { let definition; - if ( this.schemaMode == SchemaDefinition.FILE && !file) { + if ( this.schemaMode === SchemaDefinition.FILE && !file) { throw new Error('schemaDefinitionFile must be configured if using FILE definition mode.'); - } else if ( this.schemaMode == SchemaDefinition.FILE && file ) { + } else if ( this.schemaMode === SchemaDefinition.FILE && file ) { definition = readFileSync(file).toString('UTF-8'); - } else if ( this.schemaMode == SchemaDefinition.CODE && !file ) { + } else if ( this.schemaMode === SchemaDefinition.CODE && !file ) { definition = ''; - } else if ( this.schemaMode == SchemaDefinition.CODE && file) { + } else if ( this.schemaMode === SchemaDefinition.CODE && file) { throw new Error('definition mode CODE is incompatible with file definition. Change mode to FILE/S3 or unconfigure schemaDefinitionFile'); } @@ -707,4 +695,42 @@ export class GraphQLApi extends GraphqlApiBase { definition, }); } + + /** + * Escape hatch to append to Schema as desired. Will always result + * in a newline. + * + * @param addition the addition to add to schema + * @param delimiter the delimiter between schema and addition + * @default - '' + * + * @experimental + */ + public appendToSchema(addition: string, delimiter?: string): void { + if ( this.schemaMode != SchemaDefinition.CODE ) { + throw new Error('API cannot append to schema because schema definition mode is not configured as CODE.'); + } + const sep = delimiter ?? ''; + this.schema.definition = `${this.schema.definition}${sep}${addition}\n`; + } + + /** + * Add an object type to the schema + * + * @param name the name of the object type + * @param props the definition + * + * @experimental + */ + public addType(name: string, props: ObjectTypeProps): ObjectType { + if ( this.schemaMode != SchemaDefinition.CODE ) { + throw new Error('API cannot add type because schema definition mode is not configured as CODE.'); + }; + const type = new ObjectType(name, { + definition: props.definition, + directives: props.directives, + }); + this.appendToSchema(type.toString()); + return type; + } } diff --git a/packages/@aws-cdk/aws-appsync/lib/index.ts b/packages/@aws-cdk/aws-appsync/lib/index.ts index e0736c1ecc397..d418c63f0232c 100644 --- a/packages/@aws-cdk/aws-appsync/lib/index.ts +++ b/packages/@aws-cdk/aws-appsync/lib/index.ts @@ -4,5 +4,6 @@ export * from './key'; export * from './data-source'; export * from './mapping-template'; export * from './resolver'; +export * from './schema-types'; export * from './graphqlapi'; export * from './graphqlapi-base'; diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-types.ts b/packages/@aws-cdk/aws-appsync/lib/schema-types.ts new file mode 100644 index 0000000000000..2649fb41bc64b --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/lib/schema-types.ts @@ -0,0 +1,604 @@ +/** + * Directives for types + * + * i.e. @aws_iam or @aws_subscribe + * + * @experimental + */ +export class Directive { + /** + * Add the @aws_iam directive + */ + public static iam(): Directive{ + return new Directive('@aws_iam'); + } + + /** + * Add a custom directive + * + * @param statement - the directive statement to append + * Note: doesn't guarantee functionality + */ + public static custom(statement: string): Directive { + return new Directive(statement); + } + + /** + * the directive statement + */ + public readonly statement: string; + + private constructor(statement: string) { this.statement = statement; } +} + +/** + * Properties for configuring an Intermediate Type + * + * @param definition - the variables and types that define this type + * i.e. { string: GraphqlType, string: GraphqlType } + * + * @experimental + */ +export interface IntermediateTypeProps { + /** + * the attributes of this type + */ + readonly definition: { [key: string]: GraphqlType }; +} + +/** + * Interface Types are abstract types that includes a certain set of fields + * that other types must include if they implement the interface. + * + * @experimental + */ +export class InterfaceType { + /** + * the name of this type + */ + public readonly name: string; + /** + * the attributes of this type + */ + public readonly definition: { [key: string]: GraphqlType }; + + public constructor(name: string, props: IntermediateTypeProps) { + this.name = name; + this.definition = props.definition; + } + + /** + * Create an GraphQL Type representing this Intermediate Type + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public attribute(options?: BaseGraphqlTypeOptions): GraphqlType{ + return GraphqlType.intermediate({ + isList: options?.isList, + isRequired: options?.isRequired, + isRequiredList: options?.isRequiredList, + intermediateType: this, + }); + } + + /** + * Generate the string of this object type + */ + public toString(): string { + let schemaAddition = `interface ${this.name} {\n`; + Object.keys(this.definition).forEach( (key) => { + const attribute = this.definition[key]; + schemaAddition = `${schemaAddition} ${key}: ${attribute.toString()}\n`; + }); + return `${schemaAddition}}`; + } +} + +/** + * Properties for configuring an Object Type + * + * @param definition - the variables and types that define this type + * i.e. { string: GraphqlType, string: GraphqlType } + * @param interfaceTypes - the interfaces that this object type implements + * @param directives - the directives for this object type + * + * @experimental + */ +export interface ObjectTypeProps extends IntermediateTypeProps { + /** + * The Interface Types this Object Type implements + * + * @default - no interface types + */ + readonly interfaceTypes?: InterfaceType[]; + /** + * the directives for this object type + * + * @default - no directives + */ + readonly directives?: Directive[]; +} + +/** + * Object Types are types declared by you. + * + * @experimental + */ +export class ObjectType extends InterfaceType { + /** + * A method to define Object Types from an interface + */ + public static implementInterface(name: string, props: ObjectTypeProps): ObjectType { + if (!props.interfaceTypes || !props.interfaceTypes.length) { + throw new Error('Static function `implementInterface` requires an interfaceType to implement'); + } + return new ObjectType(name, { + interfaceTypes: props.interfaceTypes, + definition: props.interfaceTypes.reduce((def, interfaceType) => { + return Object.assign({}, def, interfaceType.definition); + }, props.definition), + directives: props.directives, + }); + } + /** + * The Interface Types this Object Type implements + * + * @default - no interface types + */ + public readonly interfaceTypes?: InterfaceType[]; + /** + * the directives for this object type + * + * @default - no directives + */ + public readonly directives?: Directive[]; + + public constructor(name: string, props: ObjectTypeProps) { + super(name, props); + this.interfaceTypes = props.interfaceTypes; + this.directives = props.directives; + } + + /** + * Generate the string of this object type + */ + public toString(): string { + let title = this.name; + if(this.interfaceTypes && this.interfaceTypes.length){ + title = `${title} implements`; + this.interfaceTypes.map((interfaceType) => { + title = `${title} ${interfaceType.name},`; + }); + title = title.slice(0, -1); + } + const directives = this.generateDirectives(this.directives); + let schemaAddition = `type ${title} ${directives}{\n`; + Object.keys(this.definition).forEach( (key) => { + const attribute = this.definition[key]; + schemaAddition = `${schemaAddition} ${key}: ${attribute.toString()}\n`; + }); + return `${schemaAddition}}`; + } + + /** + * Utility function to generate directives + * + * @param directives the directives of a given type + * @param delimiter the separator betweeen directives + * @default - ' ' + */ + private generateDirectives(directives?: Directive[], delimiter?: string): string{ + let schemaAddition = ''; + if (!directives){ return schemaAddition; } + directives.map((directive) => { + schemaAddition = `${schemaAddition}${directive.statement}${delimiter ?? ' '}`; + }); + return schemaAddition; + } +} + +/** + * Base options for GraphQL Types + * + * @option isList - is this attribute a list + * @option isRequired - is this attribute non-nullable + * @option isRequiredList - is this attribute a non-nullable list + * + * @experimental + */ +export interface BaseGraphqlTypeOptions { + /** + * property determining if this attribute is a list + * i.e. if true, attribute would be [Type] + * + * @default - false + */ + readonly isList?: boolean; + + /** + * property determining if this attribute is non-nullable + * i.e. if true, attribute would be Type! + * + * @default - false + */ + readonly isRequired?: boolean; + + /** + * property determining if this attribute is a non-nullable list + * i.e. if true, attribute would be [ Type ]! + * or if isRequired true, attribe would be [ Type! ]! + * + * @default - false + */ + readonly isRequiredList?: boolean; +} + +/** + * Options for GraphQL Types + * + * @option isList - is this attribute a list + * @option isRequired - is this attribute non-nullable + * @option isRequiredList - is this attribute a non-nullable list + * @option objectType - the object type linked to this attribute + * + * @experimental + */ +export interface GraphqlTypeOptions extends BaseGraphqlTypeOptions { + /** + * the intermediate type linked to this attribute + * @default - no intermediate type + */ + readonly intermediateType?: InterfaceType; +} + +/** + * The GraphQL Types in AppSync's GraphQL. GraphQL Types are the + * building blocks for object types, queries, mutations, etc. They are + * types like String, Int, Id or even Object Types you create. + * + * i.e. `String`, `String!`, `[String]`, `[String!]`, `[String]!` + * + * GraphQL Types are used to define the entirety of schema. + */ +export class GraphqlType { + /** + * `ID` scalar type is a unique identifier. `ID` type is serialized similar to `String`. + * + * Often used as a key for a cache and not intended to be human-readable. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static id(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.ID, options); + } + /** + * `String` scalar type is a free-form human-readable text. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static string(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.STRING, options); + } + /** + * `Int` scalar type is a signed non-fractional numerical value. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static int(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.INT, options); + } + /** + * `Float` scalar type is a signed double-precision fractional value. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static float(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.FLOAT, options); + } + /** + * `Boolean` scalar type is a boolean value: true or false. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static boolean(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.BOOLEAN, options); + } + + /** + * `AWSDate` scalar type represents a valid extended `ISO 8601 Date` string. + * + * In other words, accepts date strings in the form of `YYYY-MM-DD`. It accepts time zone offsets. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsDate(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_DATE, options); + } + /** + * `AWSTime` scalar type represents a valid extended `ISO 8601 Time` string. + * + * In other words, accepts date strings in the form of `hh:mm:ss.sss`. It accepts time zone offsets. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsTime(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_TIME, options); + } + /** + * `AWSDateTime` scalar type represents a valid extended `ISO 8601 DateTime` string. + * + * In other words, accepts date strings in the form of `YYYY-MM-DDThh:mm:ss.sssZ`. It accepts time zone offsets. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsDateTime(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_DATE_TIME, options); + } + /** + * `AWSTimestamp` scalar type represents the number of seconds since `1970-01-01T00:00Z`. + * + * Timestamps are serialized and deserialized as numbers. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsTimestamp(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_TIMESTAMP, options); + } + /** + * `AWSEmail` scalar type represents an email address string (i.e.`username@example.com`) + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsEmail(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_EMAIL, options); + } + /** + * `AWSJson` scalar type represents a JSON string. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsJson(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_JSON, options); + } + /** + * `AWSURL` scalar type represetns a valid URL string. + * + * URLs wihtout schemes or contain double slashes are considered invalid. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsUrl(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_URL, options); + } + /** + * `AWSPhone` scalar type represents a valid phone number. Phone numbers maybe be whitespace delimited or hyphenated. + * + * The number can specify a country code at the beginning, but is not required for US phone numbers. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsPhone(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_PHONE, options); + } + /** + * `AWSIPAddress` scalar type respresents a valid `IPv4` of `IPv6` address string. + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + public static awsIpAddress(options?: BaseGraphqlTypeOptions): GraphqlType { + return new GraphqlType(Type.AWS_IP_ADDRESS, options); + } + + /** + * an intermediate type to be added as an attribute + * (i.e. an interface or an object type) + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + * - intermediateType + */ + public static intermediate(options?: GraphqlTypeOptions): GraphqlType { + if (!options?.intermediateType) { + throw new Error('GraphQL Type of interface must be configured with corresponding Intermediate Type'); + } + return new GraphqlType(Type.INTERMEDIATE, options); + } + + /** + * the type of attribute + */ + public readonly type: Type; + + /** + * property determining if this attribute is a list + * i.e. if true, attribute would be `[Type]` + * + * @default - false + */ + public readonly isList: boolean; + + /** + * property determining if this attribute is non-nullable + * i.e. if true, attribute would be `Type!` and this attribute + * must always have a value + * + * @default - false + */ + public readonly isRequired: boolean; + + /** + * property determining if this attribute is a non-nullable list + * i.e. if true, attribute would be `[ Type ]!` and this attribute's + * list must always have a value + * + * @default - false + */ + public readonly isRequiredList: boolean; + + /** + * the intermediate type linked to this attribute + * (i.e. an interface or an object) + * + * @default - no intermediate type + */ + public readonly intermediateType?: InterfaceType; + + private constructor(type: Type, options?: GraphqlTypeOptions) { + this.type = type; + this.isList = options?.isList ?? false; + this.isRequired = options?.isRequired ?? false; + this.isRequiredList = options?.isRequiredList ?? false; + this.intermediateType = options?.intermediateType; + } + + /** + * Generate the string for this attribute + */ + public toString(): string{ + // If an Object Type, we use the name of the Object Type + let type = this.intermediateType ? this.intermediateType?.name : this.type; + // If configured as required, the GraphQL Type becomes required + type = this.isRequired ? `${type}!` : type; + // If configured with isXxxList, the GraphQL Type becomes a list + type = this.isList || this.isRequiredList ? `[${type}]` : type; + // If configured with isRequiredList, the list becomes required + type = this.isRequiredList ? `${type}!` : type; + return type; + } +} + +/** + * Enum containing the Types that can be used to define ObjectTypes + */ +export enum Type { + /** + * `ID` scalar type is a unique identifier. `ID` type is serialized similar to `String`. + * + * Often used as a key for a cache and not intended to be human-readable. + */ + ID = 'ID', + /** + * `String` scalar type is a free-form human-readable text. + */ + STRING = 'String', + /** + * `Int` scalar type is a signed non-fractional numerical value. + */ + INT = 'Int', + /** + * `Float` scalar type is a signed double-precision fractional value. + */ + FLOAT = 'Float', + /** + * `Boolean` scalar type is a boolean value: true or false. + */ + BOOLEAN = 'Boolean', + + /** + * `AWSDate` scalar type represents a valid extended `ISO 8601 Date` string. + * + * In other words, accepts date strings in the form of `YYYY-MM-DD`. It accepts time zone offsets. + * + * @see https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates + */ + AWS_DATE = 'AWSDate', + /** + * `AWSTime` scalar type represents a valid extended `ISO 8601 Time` string. + * + * In other words, accepts date strings in the form of `hh:mm:ss.sss`. It accepts time zone offsets. + * + * @see https://en.wikipedia.org/wiki/ISO_8601#Times + */ + AWS_TIME = 'AWSTime', + /** + * `AWSDateTime` scalar type represents a valid extended `ISO 8601 DateTime` string. + * + * In other words, accepts date strings in the form of `YYYY-MM-DDThh:mm:ss.sssZ`. It accepts time zone offsets. + * + * @see https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations + */ + AWS_DATE_TIME = 'AWSDateTime', + /** + * `AWSTimestamp` scalar type represents the number of seconds since `1970-01-01T00:00Z`. + * + * Timestamps are serialized and deserialized as numbers. + */ + AWS_TIMESTAMP = 'AWSTimestamp', + /** + * `AWSEmail` scalar type represents an email address string (i.e.`username@example.com`) + */ + AWS_EMAIL = 'AWSEmail', + /** + * `AWSJson` scalar type represents a JSON string. + */ + AWS_JSON = 'AWSJSON', + /** + * `AWSURL` scalar type represetns a valid URL string. + * + * URLs wihtout schemes or contain double slashes are considered invalid. + */ + AWS_URL = 'AWSURL', + /** + * `AWSPhone` scalar type represents a valid phone number. Phone numbers maybe be whitespace delimited or hyphenated. + * + * The number can specify a country code at the beginning, but is not required for US phone numbers. + */ + AWS_PHONE = 'AWSPhone', + /** + * `AWSIPAddress` scalar type respresents a valid `IPv4` of `IPv6` address string. + */ + AWS_IP_ADDRESS = 'AWSIPAddress', + + /** + * Type used for Intermediate Types + * (i.e. an interface or an object type) + */ + INTERMEDIATE = 'INTERMEDIATE', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts new file mode 100644 index 0000000000000..34e0eeaade11c --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts @@ -0,0 +1,400 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; +import * as t from './scalar-type-defintions'; + +let stack: cdk.Stack; +let api: appsync.GraphQLApi; +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + api = new appsync.GraphQLApi(stack, 'api', { + name: 'api', + schemaDefinition: appsync.SchemaDefinition.CODE, + }); +}); + +describe('testing addType for schema definition mode `code`', () => { + test('check scalar type id with all options', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + dupid: t.dup_id, + }, + }); + const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + +}); + +describe('testing all GraphQL Types', () => { + test('scalar type id', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.id, + }, + }); + const out = 'type Test {\n id: ID\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type string', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.string, + }, + }); + const out = 'type Test {\n id: String\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type int', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.int, + }, + }); + const out = 'type Test {\n id: Int\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type float', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.float, + }, + }); + const out = 'type Test {\n id: Float\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type boolean', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.boolean, + }, + }); + const out = 'type Test {\n id: Boolean\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSDate', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsDate, + }, + }); + const out = 'type Test {\n id: AWSDate\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSTime', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsTime, + }, + }); + const out = 'type Test {\n id: AWSTime\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSDateTime', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsDateTime, + }, + }); + const out = 'type Test {\n id: AWSDateTime\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSTimestamp', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsTimestamp, + }, + }); + const out = 'type Test {\n id: AWSTimestamp\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSEmail', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsEmail, + }, + }); + const out = 'type Test {\n id: AWSEmail\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSJSON', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsJson, + }, + }); + const out = 'type Test {\n id: AWSJSON\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + + test('scalar type AWSUrl', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsUrl, + }, + }); + const out = 'type Test {\n id: AWSURL\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSPhone', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsPhone, + }, + }); + const out = 'type Test {\n id: AWSPhone\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('scalar type AWSIPAddress', () => { + // WHEN + api.addType('Test', { + definition: { + id: t.awsIpAddress, + }, + }); + const out = 'type Test {\n id: AWSIPAddress\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); +}); + +describe('testing InterfaceType properties', () => { + let baseTest: appsync.InterfaceType; + beforeEach(()=>{ + baseTest = new appsync.InterfaceType('baseTest', { + definition: { + id: t.id, + }, + }); + }); + test('basic InterfaceType produces correct schema', () => { + // WHEN + api.appendToSchema(baseTest.toString()); + const out = 'interface baseTest {\n id: ID\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('Interface Type can be a Graphql Type', () => { + // WHEN + const graphqlType = baseTest.attribute(); + + const test = new appsync.ObjectType('Test', { + definition: { + test: graphqlType, + }, + }); + api.appendToSchema(test.toString()); + const out = 'type Test {\n test: baseTest\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); +}); + +describe('testing Object Type properties', () => { + + test('errors when no InterfaceTypes are specified', () => { + // WHEN + const when = () => { + appsync.ObjectType.implementInterface('objectTest', { + definition: { + id2: t.id, + }, + }); + }; + + // THEN + expect(when).toThrowError('Static function `implementInterface` requires an interfaceType to implement'); + }); + + test('errors when implementing empty InterfaceTypes properties', () => { + // WHEN + const when = () => { + appsync.ObjectType.implementInterface('objectTest', { + interfaceTypes: [], + definition: { + id2: t.id, + }, + }); + }; + + // THEN + expect(when).toThrowError('Static function `implementInterface` requires an interfaceType to implement'); + }); + + test('ObjectType can implement from interface types', () => { + // WHEN + const baseTest = new appsync.InterfaceType('baseTest', { + definition: { + id: t.id, + }, + }); + const objectTest = appsync.ObjectType.implementInterface('objectTest', { + interfaceTypes: [baseTest], + definition: { + id2: t.id, + }, + directives: [appsync.Directive.custom('@test')], + }); + + api.appendToSchema(baseTest.toString()); + api.appendToSchema(objectTest.toString()); + const gql_interface = 'interface baseTest {\n id: ID\n}\n'; + const gql_object = 'type objectTest implements baseTest @test {\n id2: ID\n id: ID\n}\n'; + const out = `${gql_interface}${gql_object}`; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('ObjectType can implement from multiple interface types', () => { + // WHEN + const baseTest = new appsync.InterfaceType('baseTest', { + definition: { id: t.id }, + }); + const anotherTest = new appsync.InterfaceType('anotherTest', { + definition: { id2: t.id }, + }); + const objectTest = appsync.ObjectType.implementInterface('objectTest', { + interfaceTypes: [anotherTest, baseTest], + definition: { + id3: t.id, + }, + }); + + api.appendToSchema(baseTest.toString()); + api.appendToSchema(anotherTest.toString()); + api.appendToSchema(objectTest.toString()); + + const gql_interface = 'interface baseTest {\n id: ID\n}\ninterface anotherTest {\n id2: ID\n}\n'; + const gql_object = 'type objectTest implements anotherTest, baseTest {\n id3: ID\n id2: ID\n id: ID\n}\n'; + const out = `${gql_interface}${gql_object}`; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('Object Type can be a Graphql Type', () => { + // WHEN + const baseTest = new appsync.ObjectType('baseTest', { + definition: { + id: t.id, + }, + }); + const graphqlType = baseTest.attribute(); + const test = new appsync.ObjectType('Test', { + definition: { + test: graphqlType, + }, + }); + api.appendToSchema(test.toString()); + const out = 'type Test {\n test: baseTest\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts index d031dbf77d542..25542722f92e9 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts @@ -2,6 +2,7 @@ import { join } from 'path'; import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; import * as appsync from '../lib'; +import * as t from './scalar-type-defintions'; // Schema Definitions const type = 'type test {\n version: String!\n}\n\n'; @@ -14,7 +15,7 @@ beforeEach(() => { stack = new cdk.Stack(); }); -describe('testing schema definition mode `code`', () => { +describe('basic testing schema definition mode `code`', () => { test('definition mode `code` produces empty schema definition', () => { // WHEN @@ -23,23 +24,25 @@ describe('testing schema definition mode `code`', () => { schemaDefinition: appsync.SchemaDefinition.CODE, }); - //THEN + // THEN expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { Definition: '', }); }); - test('definition mode `code` generates correct schema with updateDefinition', () => { + test('definition mode `code` generates correct schema with appendToSchema', () => { // WHEN const api = new appsync.GraphQLApi(stack, 'API', { name: 'demo', schemaDefinition: appsync.SchemaDefinition.CODE, }); - api.updateDefinition(`${type}${query}${mutation}`); + api.appendToSchema(type); + api.appendToSchema(query); + api.appendToSchema(mutation); - //THEN + // THEN expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { - Definition: `${type}${query}${mutation}`, + Definition: `${type}\n${query}\n${mutation}\n`, }); }); @@ -53,7 +56,7 @@ describe('testing schema definition mode `code`', () => { }); }; - //THEN + // THEN expect(when).toThrowError('definition mode CODE is incompatible with file definition. Change mode to FILE/S3 or unconfigure schemaDefinitionFile'); }); @@ -69,36 +72,57 @@ describe('testing schema definition mode `file`', () => { schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), }); - //THEN + // THEN expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { Definition: `${type}${query}${mutation}`, }); }); - test('definition mode `file` errors when calling updateDefiniton function', () => { + test('definition mode `file` errors when schemaDefinitionFile is not configured', () => { + // WHEN + const when = () => { + new appsync.GraphQLApi(stack, 'API', { + name: 'demo', + schemaDefinition: appsync.SchemaDefinition.FILE, + }); + }; + + // THEN + expect(when).toThrowError('schemaDefinitionFile must be configured if using FILE definition mode.'); + }); + + test('definition mode `file` errors when addType is called', () => { // WHEN const api = new appsync.GraphQLApi(stack, 'API', { name: 'demo', schemaDefinition: appsync.SchemaDefinition.FILE, schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), }); - const when = () => { api.updateDefinition('error'); }; - //THEN + const when = () => { + api.addType('blah', { + definition: { fail: t.id }, + }); + }; + + // THEN expect(when).toThrowError('API cannot add type because schema definition mode is not configured as CODE.'); }); - test('definition mode `file` errors when schemaDefinitionFile is not configured', () => { + test('definition mode `file` errors when appendToSchema is called', () => { // WHEN + const api = new appsync.GraphQLApi(stack, 'API', { + name: 'demo', + schemaDefinition: appsync.SchemaDefinition.FILE, + schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), + }); + const when = () => { - new appsync.GraphQLApi(stack, 'API', { - name: 'demo', - schemaDefinition: appsync.SchemaDefinition.FILE, - }); + api.appendToSchema('blah'); }; - //THEN - expect(when).toThrowError('schemaDefinitionFile must be configured if using FILE definition mode.'); + // THEN + expect(when).toThrowError('API cannot append to schema because schema definition mode is not configured as CODE.'); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json new file mode 100644 index 0000000000000..911fb65da3e0d --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json @@ -0,0 +1,165 @@ +{ + "Resources": { + "codefirstapi1A3CC7D2": { + "Type": "AWS::AppSync::GraphQLApi", + "Properties": { + "AuthenticationType": "API_KEY", + "Name": "api" + } + }, + "codefirstapiDefaultAPIKeyApiKeyB73DF94B": { + "Type": "AWS::AppSync::ApiKey", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "codefirstapi1A3CC7D2", + "ApiId" + ] + }, + "Description": "Default API Key created by CDK" + } + }, + "codefirstapiSchema148B6CDE": { + "Type": "AWS::AppSync::GraphQLSchema", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "codefirstapi1A3CC7D2", + "ApiId" + ] + }, + "Definition": "type Planet {\n name: String\n diameter: Int\n rotationPeriod: Int\n orbitalPeriod: Int\n gravity: String\n population: [String]\n climates: [String]\n terrains: [String]\n surfaceWater: Float\n created: String\n edited: String\n id: ID!\n}\ntype Species {\n name: String\n classification: String\n designation: String\n averageHeight: Float\n averageLifespan: Int\n eyeColors: [String]\n hairColors: [String]\n skinColors: [String]\n language: String\n homeworld: Planet\n created: String\n edited: String\n id: ID!\n}\n\ntype Query {\n getPlanets: [Planet]\n}\n" + } + }, + "codefirstapiplanetsServiceRole2F4AA8E7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "codefirstapiplanetsServiceRoleDefaultPolicy76FE4F2C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "table8235A42E", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "codefirstapiplanetsServiceRoleDefaultPolicy76FE4F2C", + "Roles": [ + { + "Ref": "codefirstapiplanetsServiceRole2F4AA8E7" + } + ] + } + }, + "codefirstapiplanets0F6BB479": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "codefirstapi1A3CC7D2", + "ApiId" + ] + }, + "Name": "planets", + "Type": "AMAZON_DYNAMODB", + "DynamoDBConfig": { + "AwsRegion": { + "Ref": "AWS::Region" + }, + "TableName": { + "Ref": "table8235A42E" + } + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "codefirstapiplanetsServiceRole2F4AA8E7", + "Arn" + ] + } + } + }, + "codefirstapiplanetsQuerygetPlanetsResolver633EA597": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "codefirstapi1A3CC7D2", + "ApiId" + ] + }, + "FieldName": "getPlanets", + "TypeName": "Query", + "DataSourceName": "planets", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}", + "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" + }, + "DependsOn": [ + "codefirstapiplanets0F6BB479", + "codefirstapiSchema148B6CDE" + ] + }, + "table8235A42E": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts new file mode 100644 index 0000000000000..4dc10dbf5479f --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts @@ -0,0 +1,66 @@ +import * as cdk from '@aws-cdk/core'; +import * as db from '@aws-cdk/aws-dynamodb'; +import * as appsync from '../lib'; +import * as ObjectType from './object-type-definitions'; +import * as ScalarType from './scalar-type-defintions'; + +/* + * Creates an Appsync GraphQL API and schema in a code-first approach. + * + * Stack verification steps: + * Deploy stack, get api key and endpoinScalarType. Check if schema connects to data source. + * + * -- bash verify.integ.graphql-schema.sh --start -- start -- + * -- aws appsync list-graphql-apis -- obtain apiId & endpoint -- + * -- aws appsync list-api-keys --api-id [apiId] -- obtain api key -- + * -- bash verify.integ.graphql-schema.sh --check [apiKey] [url] -- check if success -- + * -- bash verify.integ.graphql-schema.sh --clean -- clean -- + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'code-first-schema'); + +const api = new appsync.GraphQLApi(stack, 'code-first-api', { + name: 'api', + schemaDefinition: appsync.SchemaDefinition.CODE, +}); + +const planet = ObjectType.planet; +api.appendToSchema(planet.toString()); + +api.addType('Species', { + definition: { + name: ScalarType.string, + classification: ScalarType.string, + designation: ScalarType.string, + averageHeight: ScalarType.float, + averageLifespan: ScalarType.int, + eyeColors: ScalarType.list_string, + hairColors: ScalarType.list_string, + skinColors: ScalarType.list_string, + language: ScalarType.string, + homeworld: planet.attribute(), + created: ScalarType.string, + edited: ScalarType.string, + id: ScalarType.required_id, + }, +}); + +api.appendToSchema('type Query {\n getPlanets: [Planet]\n}', '\n'); + +const table = new db.Table(stack, 'table', { + partitionKey: { + name: 'id', + type: db.AttributeType.STRING, + }, +}); + +const tableDS = api.addDynamoDbDataSource('planets', table); + +tableDS.createResolver({ + typeName: 'Query', + fieldName: 'getPlanets', + requestMappingTemplate: appsync.MappingTemplate.dynamoDbScanTable(), + responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(), +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/object-type-definitions.ts b/packages/@aws-cdk/aws-appsync/test/object-type-definitions.ts new file mode 100644 index 0000000000000..138f2d7e1faa1 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/object-type-definitions.ts @@ -0,0 +1,19 @@ +import { ObjectType } from '../lib'; +import * as ScalarType from './scalar-type-defintions'; + +export const planet = new ObjectType('Planet', { + definition: { + name: ScalarType.string, + diameter: ScalarType.int, + rotationPeriod: ScalarType.int, + orbitalPeriod: ScalarType.int, + gravity: ScalarType.string, + population: ScalarType.list_string, + climates: ScalarType.list_string, + terrains: ScalarType.list_string, + surfaceWater: ScalarType.float, + created: ScalarType.string, + edited: ScalarType.string, + id: ScalarType.required_id, + }, +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/scalar-type-defintions.ts b/packages/@aws-cdk/aws-appsync/test/scalar-type-defintions.ts new file mode 100644 index 0000000000000..e13df50ca0fc4 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/scalar-type-defintions.ts @@ -0,0 +1,64 @@ +import { GraphqlType } from '../lib'; + +// ID +export const id = GraphqlType.id(); +export const list_id = GraphqlType.id({ + isList: true, +}); +export const required_id = GraphqlType.id({ + isRequired: true, +}); +export const required_list_id = GraphqlType.id({ + isRequiredList: true, +}); +export const required_list_required_id = GraphqlType.id({ + isRequired: true, + isRequiredList: true, +}); +export const dup_id = GraphqlType.id({ + isList: true, + isRequired: true, + isRequiredList: true, +}); + +// STRING +export const string = GraphqlType.string(); +export const list_string = GraphqlType.string({ + isList: true, +}); + +// INT +export const int = GraphqlType.int(); + +// FLOAT +export const float = GraphqlType.float(); + +// BOOLEAN +export const boolean = GraphqlType.boolean(); + +// AWSDate +export const awsDate = GraphqlType.awsDate(); + +// AWSTime +export const awsTime = GraphqlType.awsTime(); + +// AWSDateTime +export const awsDateTime = GraphqlType.awsDateTime(); + +// AWSTimestamp +export const awsTimestamp = GraphqlType.awsTimestamp(); + +// AWSEmail +export const awsEmail = GraphqlType.awsEmail(); + +// AWSJSON +export const awsJson = GraphqlType.awsJson(); + +// AWSUrl +export const awsUrl = GraphqlType.awsUrl(); + +// AWSPhone +export const awsPhone = GraphqlType.awsPhone(); + +// AWSIPAddress +export const awsIpAddress = GraphqlType.awsIpAddress(); diff --git a/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-schema.sh b/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-schema.sh new file mode 100644 index 0000000000000..95662531fece5 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-schema.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +function error { + printf "\e[91;5;81m$@\e[0m\n" +} + +function usage { + echo "###############################################################################" + echo "# run 'verify.integ.graphql-schema.sh --start' to deploy #" + echo "# run 'verify.integ.graphql-schema.sh --check [APIKEY] [ENDPOINT]' to run check #" + echo "# run 'verify.integ.graphql-schema.sh --clean' to clean up stack #" + echo "###############################################################################" +} + +if [[ "$1" == "--start" ]]; then + cdk deploy --app "node integ.graphql-schema.js" +elif [[ "$1" == "--check" ]]; then + if [[ -z $2 || -z $3 ]]; then + error "Error: --check flag requires [APIKEY] [ENDPOINT]" + usage + exit 1 + fi + echo THIS TEST SHOULD SUCCEED + curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:$2" -d '{ "query": "query { getPlanets { id name } }" }" }' $3 + echo "" +elif [[ "$1" == "--clean" ]];then + cdk destroy --app "node integ.graphql-schema.js" +else + error "Error: use flags --start, --check, --clean" + usage + exit 1 +fi \ No newline at end of file From 9ca1d0228824014f2397a4392bc264ab69f7ad1b Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 16 Aug 2020 08:39:49 -0700 Subject: [PATCH 05/63] fix(build): Prereq check - support paths with spaces. Prereq check before `yarn build` fails when there are spaces in the path. This is common for WSL2 on windows. For example, mvn is in the path at `/mnt/c/Program Files/`. Testing - Ran check on linux paths and windows paths with and without spaces. Tested with correct version, wrong version, and missing dep. fixes #9749 --- scripts/check-prerequisites.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-prerequisites.sh b/scripts/check-prerequisites.sh index fb2011360eb0e..12e85ae3789f1 100755 --- a/scripts/check-prerequisites.sh +++ b/scripts/check-prerequisites.sh @@ -22,7 +22,7 @@ check_which() { w=$(which ${app}) || w="" - if [ -z $w ] || [ $w == "$app not found" ] + if [ -z "$w" ] || [ "$w" == "$app not found" ] then die "Missing dependency: $app. Install $app >= $min" else From 4ee37a4b92218803c5d0123161f5502cc9e9a62c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 17 Aug 2020 05:19:54 +0200 Subject: [PATCH 06/63] feat(amplify): automatic branch deletion (#9663) Add the `autoBranchDeletion` prop to control automatic branch deletion. Closes #9650 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-amplify/README.md | 9 ++++++--- packages/@aws-cdk/aws-amplify/lib/app.ts | 11 ++++++++++- packages/@aws-cdk/aws-amplify/test/app.test.ts | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-amplify/README.md b/packages/@aws-cdk/aws-amplify/README.md index 9a6ec9b6e48f0..49f8f5028c5ab 100644 --- a/packages/@aws-cdk/aws-amplify/README.md +++ b/packages/@aws-cdk/aws-amplify/README.md @@ -140,14 +140,17 @@ app.addBranch('feature/next', { }); ``` -### Automatically creating branches -Use the `autoBranchCreation` prop to automatically create new branches: +### Automatically creating and deleting branches +Use the `autoBranchCreation` and `autoBranchDeletion` props to control creation/deletion +of branches: + ```ts const amplifyApp = new amplify.App(this, 'MyApp', { repository: 'https://github.com//', oauthToken: cdk.SecretValue.secretsManager('my-github-token'), - autoBranchCreation: { + autoBranchCreation: { // Automatically connect branches that match a pattern set patterns: ['feature/*', 'test/*'] } + autoBranchDeletion: true, // Automatically disconnect a branch when you delete a branch from your repository }); ``` diff --git a/packages/@aws-cdk/aws-amplify/lib/app.ts b/packages/@aws-cdk/aws-amplify/lib/app.ts index 1d41c0e07ede3..034edafaaf3c9 100644 --- a/packages/@aws-cdk/aws-amplify/lib/app.ts +++ b/packages/@aws-cdk/aws-amplify/lib/app.ts @@ -91,6 +91,14 @@ export interface AppProps { */ readonly autoBranchCreation?: AutoBranchCreation; + /** + * Automatically disconnect a branch in the Amplify Console when you delete a + * branch from your Git repository. + * + * @default false + */ + readonly autoBranchDeletion?: boolean; + /** * The Basic Auth configuration. Use this to set password protection at an * app level to all your branches. @@ -210,11 +218,12 @@ export class App extends Resource implements IApp, iam.IGrantable { buildSpec: props.autoBranchCreation.buildSpec && props.autoBranchCreation.buildSpec.toBuildSpec(), enableAutoBranchCreation: true, enableAutoBuild: props.autoBranchCreation.autoBuild === undefined ? true : props.autoBranchCreation.autoBuild, - environmentVariables: Lazy.anyValue({ produce: () => renderEnvironmentVariables(this.autoBranchEnvironmentVariables )}, { omitEmptyArray: true }), // eslint-disable-line max-len + environmentVariables: Lazy.anyValue({ produce: () => renderEnvironmentVariables(this.autoBranchEnvironmentVariables ) }, { omitEmptyArray: true }), // eslint-disable-line max-len enablePullRequestPreview: props.autoBranchCreation.pullRequestPreview === undefined ? true : props.autoBranchCreation.pullRequestPreview, pullRequestEnvironmentName: props.autoBranchCreation.pullRequestEnvironmentName, stage: props.autoBranchCreation.stage, }, + enableBranchAutoDeletion: props.autoBranchDeletion, basicAuthConfig: props.basicAuth && props.basicAuth.bind(this, 'AppBasicAuth'), buildSpec: props.buildSpec && props.buildSpec.toBuildSpec(), customRules: Lazy.anyValue({ produce: () => this.customRules }, { omitEmptyArray: true }), diff --git a/packages/@aws-cdk/aws-amplify/test/app.test.ts b/packages/@aws-cdk/aws-amplify/test/app.test.ts index 5af765cae6d75..8cff8e3d5b66d 100644 --- a/packages/@aws-cdk/aws-amplify/test/app.test.ts +++ b/packages/@aws-cdk/aws-amplify/test/app.test.ts @@ -371,3 +371,20 @@ test('with auto branch creation', () => { }, }); }); + +test('with auto branch deletion', () => { + // WHEN + new amplify.App(stack, 'App', { + sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ + owner: 'aws', + repository: 'aws-cdk', + oauthToken: SecretValue.plainText('secret'), + }), + autoBranchDeletion: true, + }); + + // THEN + expect(stack).toHaveResource('AWS::Amplify::App', { + EnableBranchAutoDeletion: true, + }); +}); From 44f7753cb33801f2c4c19ad5b72896331fbc1e71 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Mon, 17 Aug 2020 10:10:38 +0300 Subject: [PATCH 07/63] fix(eks): AMI changes in managed SSM store param causes rolling update of ASG (#9746) This might be the PR with the highest explanation/code ratio i've ever made :) When a value changes for an AMI in a managed SSM store parameter, it should not cause a replacement of the ASG nodes. The reasoning is that managed params can change over time with no control on the user's part. Because of this, the change will not be reflected in `cdk diff` and creates a situation where every deployment can potentially cause node replacement without notice. There are two scenarios in which the cluster interacts with an `AutoScalingGroup` ### `addCapacity` When one uses `cluster.addCapacity`, we implicitly create an `AutoScalingGroup` that uses either the `BottleRocketImage` or the `EksOptimizedImage` as the machine image, with no option to customize it. Both these images fetch their AMI's from a managed SSM parameter (`/aws/service/eks/optimized-ami` or `/aws/service/bottlerocket`). This means that we create the situation described above by **default**. https://github.com/aws/aws-cdk/blob/5af718bab8522f1a4e7f70e7221f4878a15aa4a4/packages/%40aws-cdk/aws-eks/lib/cluster.ts#L779-L785 Seems like a more reasonable default in this case would be to use `UpdateType.NONE` instead of `UpdateType.RollingUpdate`. Note that in such a case, even if the user explicitly changes the machine image configuration (by specifying a different `machineImageType`), node replacement will not occur, even though `cdk diff` will clearly show a configuration change. In any case, the `updateType` can always be explicitly passed to mitigate any issue caused by the default behavior. ### `addAutoScalingGroup` When one uses `cluster.addAutoScalingGroup`, the `AutoScalingGroup` is created by the user. The default value for `updateType` in the `AutoScalingGroup` construct is `UpdateType.NONE`, so unless the user explicitly configured `UpdateType.RollingUpdate` - node replacement should not occur. Having said that, when a user specifies `UpdateType.RollingUpdate`, its not super intuitive that this update might happen without any explicit configuration change, and in fact this is actually documented in the images that use SSM to fetch the API: https://github.com/aws/aws-cdk/blob/5af718bab8522f1a4e7f70e7221f4878a15aa4a4/packages/%40aws-cdk/aws-ec2/lib/machine-image.ts#L216-L226 ------------------------------------------- There is no way for us to selectively apply the update policy, we either dont use it at all, meaning intentional user changes won't replace nodes as well, or we use it for all, meaning implicit changes will cause it. Ideally, we should consider moving away from using these managed SSM params in launch configurations, but that requires some additional investigation. The PR simply suggests to remove the `UpdateType.RollingUpdate` default from the `addCapacity` method, as a form of balance between all the considerations mentioned above. Fixes https://github.com/aws/aws-cdk/issues/7273 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-eks/lib/cluster.ts | 9 ++-- .../@aws-cdk/aws-eks/lib/legacy-cluster.ts | 4 -- .../test/integ.eks-cluster.expected.json | 44 ------------------- .../@aws-cdk/aws-eks/test/test.cluster.ts | 19 ++++++++ 4 files changed, 25 insertions(+), 51 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 9bbc71ca73f5a..c7cd8d9719c67 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -763,9 +763,12 @@ export class Cluster extends Resource implements ICluster { * The nodes will automatically be configured with the right VPC and AMI * for the instance type and Kubernetes version. * + * Note that if you specify `updateType: RollingUpdate` or `updateType: ReplacingUpdate`, your nodes might be replaced at deploy + * time without notice in case the recommended AMI for your machine image type has been updated by AWS. + * The default behavior for `updateType` is `None`, which means only new instances will be launched using the new AMI. + * * Spot instances will be labeled `lifecycle=Ec2Spot` and tainted with `PreferNoSchedule`. - * If kubectl is enabled, the - * [spot interrupt handler](https://github.com/awslabs/ec2-spot-labs/tree/master/ec2-spot-eks-solution/spot-termination-handler) + * In addition, the [spot interrupt handler](https://github.com/awslabs/ec2-spot-labs/tree/master/ec2-spot-eks-solution/spot-termination-handler) * daemon will be installed on all spot instances to handle * [EC2 Spot Instance Termination Notices](https://aws.amazon.com/blogs/aws/new-ec2-spot-instance-termination-notices/). */ @@ -782,7 +785,7 @@ export class Cluster extends Resource implements ICluster { nodeType: nodeTypeForInstanceType(options.instanceType), kubernetesVersion: this.version.version, }), - updateType: options.updateType || autoscaling.UpdateType.ROLLING_UPDATE, + updateType: options.updateType, instanceType: options.instanceType, }); diff --git a/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts b/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts index 3e2ba0feb9abd..f8766e8ce3929 100644 --- a/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts @@ -230,10 +230,6 @@ export class LegacyCluster extends Resource implements ICluster { * for the instance type and Kubernetes version. * * Spot instances will be labeled `lifecycle=Ec2Spot` and tainted with `PreferNoSchedule`. - * If kubectl is enabled, the - * [spot interrupt handler](https://github.com/awslabs/ec2-spot-labs/tree/master/ec2-spot-eks-solution/spot-termination-handler) - * daemon will be installed on all spot instances to handle - * [EC2 Spot Instance Termination Notices](https://aws.amazon.com/blogs/aws/new-ec2-spot-instance-termination-notices/). */ public addCapacity(id: string, options: CapacityOptions): autoscaling.AutoScalingGroup { if (options.machineImageType === MachineImageType.BOTTLEROCKET && options.bootstrapOptions !== undefined ) { diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 471201d249202..62e1e78b850c1 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -1551,17 +1551,6 @@ ] }, "UpdatePolicy": { - "AutoScalingRollingUpdate": { - "WaitOnResourceSignals": false, - "PauseTime": "PT0S", - "SuspendProcesses": [ - "HealthCheck", - "ReplaceUnhealthy", - "AZRebalance", - "AlarmNotification", - "ScheduledActions" - ] - }, "AutoScalingScheduledAction": { "IgnoreUnmodifiedGroupSizeProperties": true } @@ -1853,17 +1842,6 @@ ] }, "UpdatePolicy": { - "AutoScalingRollingUpdate": { - "WaitOnResourceSignals": false, - "PauseTime": "PT0S", - "SuspendProcesses": [ - "HealthCheck", - "ReplaceUnhealthy", - "AZRebalance", - "AlarmNotification", - "ScheduledActions" - ] - }, "AutoScalingScheduledAction": { "IgnoreUnmodifiedGroupSizeProperties": true } @@ -2142,17 +2120,6 @@ ] }, "UpdatePolicy": { - "AutoScalingRollingUpdate": { - "WaitOnResourceSignals": false, - "PauseTime": "PT0S", - "SuspendProcesses": [ - "HealthCheck", - "ReplaceUnhealthy", - "AZRebalance", - "AlarmNotification", - "ScheduledActions" - ] - }, "AutoScalingScheduledAction": { "IgnoreUnmodifiedGroupSizeProperties": true } @@ -2462,17 +2429,6 @@ ] }, "UpdatePolicy": { - "AutoScalingRollingUpdate": { - "WaitOnResourceSignals": false, - "PauseTime": "PT0S", - "SuspendProcesses": [ - "HealthCheck", - "ReplaceUnhealthy", - "AZRebalance", - "AlarmNotification", - "ScheduledActions" - ] - }, "AutoScalingScheduledAction": { "IgnoreUnmodifiedGroupSizeProperties": true } diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 6c1d01a1f02ba..0ae01003d7e14 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -15,6 +15,7 @@ import { testFixture, testFixtureNoVpc } from './util'; const CLUSTER_VERSION = eks.KubernetesVersion.V1_16; export = { + 'a default cluster spans all subnets'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); @@ -201,6 +202,24 @@ export = { test.done(); }, + 'adding capacity creates an ASG without a rolling update policy'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + // WHEN + cluster.addCapacity('Default', { + instanceType: new ec2.InstanceType('t2.medium'), + }); + + test.deepEqual(expect(stack).value.Resources.ClusterASG0E4BA723.UpdatePolicy, { AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true } }); + test.done(); + }, + 'adding capacity creates an ASG with tags'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); From 90de605a0f9c5487c302e34db5f89dcd58e3b445 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 17 Aug 2020 09:35:09 +0200 Subject: [PATCH 08/63] chore: more style rules for eslint (#9517) Configure `comma-spacing`, `no-multi-spaces`, `array-bracket-spacing`, `array-bracket-newline`, `object-curly-spacing`, `object-curly-newline` and `object-property-newline` to uniformize arrays and objects. * Valid arrays (no bracket spacing, no space before comma, a single space after comma): ```ts [1, 2, 3] [ 1, 2, 3, ] [ 1, 2, 3, 4, ] ``` * Valid objects (curly spacing): ```ts { key: 'value' } { key1: 'value1', key2: 'value2 } { key1: 'value1', key2: 'value2', } ``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/test.pipeline-deploy-stack-action.ts | 2 +- .../@aws-cdk/assert/test/assertions.test.ts | 6 +- .../@aws-cdk/assert/test/have-output.test.ts | 2 +- packages/@aws-cdk/assets/test/test.staging.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/access-log.ts | 6 +- .../aws-apigateway/lib/apigatewayv2.ts | 44 +-- .../aws-apigateway/lib/authorizers/lambda.ts | 4 +- packages/@aws-cdk/aws-apigateway/lib/cors.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 6 +- .../aws-apigateway/lib/integrations/aws.ts | 22 +- .../@aws-cdk/aws-apigateway/lib/resource.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 2 +- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 5 +- packages/@aws-cdk/aws-apigateway/lib/util.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/vpc-link.ts | 2 +- .../authorizers/integ.request-authorizer.ts | 6 +- .../integ.token-authorizer-iam-role.ts | 2 +- .../authorizers/integ.token-authorizer.ts | 2 +- .../test/authorizers/test.lambda.ts | 2 +- .../test/integ.api-definition.asset.ts | 4 +- .../aws-apigateway/test/integ.cors.ts | 8 +- .../test/integ.restapi-import.lit.ts | 6 +- .../test/integ.restapi.vpc-endpoint.ts | 4 +- .../aws-apigateway/test/test.access-log.ts | 2 +- .../test/test.api-definition.ts | 2 +- .../@aws-cdk/aws-apigateway/test/test.cors.ts | 10 +- .../aws-apigateway/test/test.deployment.ts | 2 +- .../aws-apigateway/test/test.domains.ts | 22 +- .../aws-apigateway/test/test.lambda.ts | 2 +- .../aws-apigateway/test/test.method.ts | 20 +- .../aws-apigateway/test/test.restapi.ts | 90 +++--- .../aws-apigateway/test/test.stage.ts | 8 +- .../@aws-cdk/aws-apigateway/test/test.util.ts | 10 +- .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 2 +- .../aws-apigatewayv2/test/http/api.test.ts | 2 +- .../test/http/domain-name.test.ts | 2 +- .../aws-apigatewayv2/test/http/route.test.ts | 8 +- .../lib/scalable-target.ts | 2 +- .../test/test.step-scaling-policy.ts | 2 +- .../aws-appmesh/lib/virtual-router.ts | 2 +- .../aws-appmesh/lib/virtual-service.ts | 2 +- .../aws-appmesh/test/test.health-check.ts | 12 +- .../@aws-cdk/aws-appmesh/test/test.mesh.ts | 2 +- .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 4 +- packages/@aws-cdk/aws-appsync/lib/key.ts | 2 +- .../aws-appsync/test/appsync-dynamodb.test.ts | 6 +- .../aws-appsync/test/appsync-grant.test.ts | 117 ++++---- .../aws-appsync/test/appsync-http.test.ts | 6 +- .../aws-appsync/test/appsync-lambda.test.ts | 8 +- .../aws-appsync/test/appsync-none.test.ts | 6 +- .../@aws-cdk/aws-appsync/test/appsync.test.ts | 2 +- .../aws-appsync/test/integ.graphql-iam.ts | 8 +- .../aws-appsync/test/integ.graphql-schema.ts | 2 +- .../aws-appsync/test/integ.graphql.ts | 24 +- .../@aws-cdk/aws-athena/test/athena.test.ts | 3 +- .../aws-athena/test/integ.workgroup.ts | 3 +- .../lib/interval-utils.ts | 2 +- .../aws-autoscaling-common/lib/test-utils.ts | 2 +- .../test/test.intervals.ts | 2 +- .../test/hooks.test.ts | 5 +- .../aws-autoscaling/lib/auto-scaling-group.ts | 6 +- .../test/auto-scaling-group.test.ts | 22 +- .../aws-autoscaling/test/scaling.test.ts | 27 +- .../test/scheduled-action.test.ts | 2 +- packages/@aws-cdk/aws-backup/lib/selection.ts | 2 +- .../aws-batch/lib/compute-environment.ts | 4 +- .../@aws-cdk/aws-batch/lib/job-definition.ts | 6 +- .../test/compute-environment.test.ts | 8 +- .../aws-batch/test/job-definition.test.ts | 4 +- .../lib/dns-validated-certificate.ts | 2 +- .../aws-certificatemanager/test/util.test.ts | 6 +- .../test/integ.nested-stack.ts | 2 +- .../test/integ.nested-stacks-assets.ts | 2 +- .../test/integ.trivial-lambda-resource.ts | 2 +- .../aws-cloudformation/test/test.deps.ts | 36 +-- .../test/test.nested-stack.ts | 2 +- .../aws-cloudformation/test/test.resource.ts | 9 +- .../test/s3-origin.test.ts | 2 +- .../aws-cloudfront/lib/distribution.ts | 38 +-- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 22 +- .../lib/origin_access_identity.ts | 2 +- .../aws-cloudfront/lib/web_distribution.ts | 15 +- .../aws-cloudfront/test/distribution.test.ts | 8 +- .../test/integ.cloudfront-custom-s3.ts | 2 +- .../test/integ.cloudfront-geo-restrictions.ts | 2 +- .../test/integ.cloudfront-ipv6-disabled.ts | 2 +- .../integ.cloudfront-lambda-association.ts | 11 +- .../aws-cloudfront/test/integ.cloudfront.ts | 2 +- .../test/web_distribution.test.ts | 28 +- .../@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts | 4 +- .../aws-cloudtrail/test/cloudtrail.test.ts | 12 +- .../integ.cloudtrail-supplied-bucket.lit.ts | 8 +- .../test/integ.cloudtrail.lit.ts | 2 +- .../@aws-cdk/aws-cloudwatch/lib/dashboard.ts | 24 +- .../@aws-cdk/aws-cloudwatch/lib/layout.ts | 2 +- .../aws-cloudwatch/lib/private/rendering.ts | 2 +- .../test/test.cross-environment.ts | 27 +- .../aws-cloudwatch/test/test.dashboard.ts | 24 +- .../aws-cloudwatch/test/test.graphs.ts | 37 +-- .../aws-cloudwatch/test/test.layout.ts | 2 +- .../aws-cloudwatch/test/test.metric-math.ts | 97 ++++--- .../@aws-cdk/aws-codebuild/lib/build-spec.ts | 5 +- .../test/integ.docker-asset.lit.ts | 4 +- .../test/integ.docker-registry.lit.ts | 2 +- .../aws-codebuild/test/integ.ecr.lit.ts | 2 +- .../aws-codebuild/test/test.codebuild.ts | 10 +- .../@aws-cdk/aws-codecommit/lib/repository.ts | 22 +- .../lib/lambda/deployment-config.ts | 16 +- .../lib/server/deployment-config.ts | 4 +- .../test/lambda/integ.deployment-group.ts | 2 +- .../aws-codepipeline-actions/lib/action.ts | 30 +- .../lib/s3/deploy-action.ts | 2 +- .../lib/s3/source-action.ts | 2 +- .../test.cloudformation-pipeline-actions.ts | 60 ++-- .../cloudformation/test.pipeline-actions.ts | 19 +- .../test/integ.lambda-pipeline.ts | 4 +- .../test/integ.pipeline-events.ts | 2 +- .../test/integ.pipeline-stepfunctions.ts | 2 +- .../test/s3/test.s3-deploy-action.ts | 2 +- .../test.stepfunctions-invoke-actions.ts | 4 +- .../test/test.manual-approval.ts | 2 +- .../test/test.pipeline.ts | 2 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 8 +- .../@aws-cdk/aws-codepipeline/lib/stage.ts | 12 +- .../aws-codepipeline/test/test.pipeline.ts | 4 +- .../aws-cognito/lib/user-pool-client.ts | 8 +- .../aws-cognito/lib/user-pool-domain.ts | 2 +- .../aws-cognito/lib/user-pool-idps/amazon.ts | 2 +- .../lib/user-pool-idps/facebook.ts | 2 +- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 6 +- .../integ.user-pool-client-explicit-props.ts | 2 +- .../test/integ.user-pool-domain-signinurl.ts | 2 +- .../aws-cognito/test/user-pool-client.test.ts | 52 ++-- .../aws-cognito/test/user-pool-domain.test.ts | 4 +- .../test/user-pool-idps/amazon.test.ts | 2 +- .../test/user-pool-idps/facebook.test.ts | 2 +- .../aws-cognito/test/user-pool.test.ts | 34 +-- packages/@aws-cdk/aws-config/lib/rule.ts | 4 +- .../@aws-cdk/aws-docdb/test/cluster.test.ts | 16 +- .../@aws-cdk/aws-docdb/test/instance.test.ts | 4 +- .../lib/global-table-coordinator.ts | 2 +- .../test/test.dynamodb.global.ts | 6 +- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 17 +- .../aws-dynamodb/test/dynamodb.test.ts | 3 +- packages/@aws-cdk/aws-ec2/lib/cfn-init.ts | 2 +- packages/@aws-cdk/aws-ec2/lib/instance.ts | 20 +- .../@aws-cdk/aws-ec2/lib/machine-image.ts | 6 +- packages/@aws-cdk/aws-ec2/lib/nat.ts | 6 +- packages/@aws-cdk/aws-ec2/lib/network-acl.ts | 4 +- packages/@aws-cdk/aws-ec2/lib/network-util.ts | 2 +- .../@aws-cdk/aws-ec2/lib/security-group.ts | 6 +- packages/@aws-cdk/aws-ec2/lib/user-data.ts | 5 +- packages/@aws-cdk/aws-ec2/lib/util.ts | 2 +- packages/@aws-cdk/aws-ec2/lib/volume.ts | 18 +- packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts | 2 +- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 28 +- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 2 +- .../aws-ec2/test/bastion-host.test.ts | 4 +- .../@aws-cdk/aws-ec2/test/connections.test.ts | 40 +-- .../@aws-cdk/aws-ec2/test/instance.test.ts | 10 +- .../aws-ec2/test/integ.instance-init.ts | 2 +- .../aws-ec2/test/machine-image.test.ts | 8 +- .../aws-ec2/test/security-group.test.ts | 4 +- .../aws-ec2/test/vpc-endpoint.test.ts | 14 +- .../aws-ec2/test/vpc.from-lookup.test.ts | 2 +- packages/@aws-cdk/aws-ec2/test/vpc.test.ts | 42 +-- packages/@aws-cdk/aws-ec2/test/vpn.test.ts | 2 +- .../aws-ecr-assets/lib/image-asset.ts | 12 +- .../test/integ.assets-docker.ts | 2 +- .../test/integ.nested-stacks-docker.ts | 2 +- .../aws-ecr-assets/test/test.image-asset.ts | 4 +- packages/@aws-cdk/aws-ecr/lib/repository.ts | 4 +- .../@aws-cdk/aws-ecr/test/test.repository.ts | 7 +- .../application-load-balanced-service-base.ts | 6 +- ...ion-multiple-target-groups-service-base.ts | 6 +- ...ork-multiple-target-groups-service-base.ts | 6 +- ...tion-multiple-target-groups-ecs-service.ts | 6 +- ...work-multiple-target-groups-ecs-service.ts | 6 +- ...-multiple-target-groups-fargate-service.ts | 6 +- ...-multiple-target-groups-fargate-service.ts | 6 +- .../aws-ecs-patterns/test/ec2/test.l3s.ts | 26 +- .../test/fargate/integ.asset-image.ts | 2 +- .../integ.scheduled-fargate-task.lit.ts | 2 +- .../test.load-balanced-fargate-service.ts | 6 +- .../test.queue-processing-fargate-service.ts | 4 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 6 +- .../aws-ecs/lib/base/task-definition.ts | 5 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 6 +- .../lib/drain-hook/instance-drain-hook.ts | 6 +- .../@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 2 +- .../aws-ecs/lib/fargate/fargate-service.ts | 2 +- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 4 +- .../lib/log-drivers/firelens-log-driver.ts | 2 +- .../test/ec2/integ.firelens-s3-config.ts | 2 +- .../aws-ecs/test/ec2/test.ec2-service.ts | 4 +- .../test/ec2/test.ec2-task-definition.ts | 2 +- .../test/fargate/integ.nlb-awsvpc-nw.ts | 2 +- .../test/fargate/test.fargate-service.ts | 6 +- .../fargate/test.fargate-task-definition.ts | 4 +- .../aws-ecs/test/test.container-definition.ts | 4 +- .../aws-ecs/test/test.firelens-log-driver.ts | 4 +- .../aws-efs/test/efs-file-system.test.ts | 8 +- packages/@aws-cdk/aws-efs/test/integ.efs.ts | 2 +- .../@aws-cdk/aws-eks-legacy/lib/aws-auth.ts | 2 +- .../aws-eks-legacy/lib/cluster-resource.ts | 12 +- .../@aws-cdk/aws-eks-legacy/lib/cluster.ts | 6 +- .../@aws-cdk/aws-eks-legacy/lib/helm-chart.ts | 4 +- .../aws-eks-legacy/lib/kubectl-layer.ts | 4 +- .../integ.eks-cluster.kubectl-disabled.ts | 2 +- .../test/integ.eks-cluster.lit.ts | 2 +- .../test/integ.eks-kubectl.lit.ts | 4 +- .../aws-eks-legacy/test/test.cluster.ts | 22 +- .../aws-eks-legacy/test/test.user-data.ts | 2 +- packages/@aws-cdk/aws-eks-legacy/test/util.ts | 2 +- packages/@aws-cdk/aws-eks/lib/aws-auth.ts | 2 +- .../lib/cluster-resource-handler/cluster.ts | 12 +- .../lib/cluster-resource-handler/fargate.ts | 6 +- .../aws-eks/lib/cluster-resource-provider.ts | 2 +- .../@aws-cdk/aws-eks/lib/cluster-resource.ts | 26 +- packages/@aws-cdk/aws-eks/lib/cluster.ts | 18 +- .../@aws-cdk/aws-eks/lib/fargate-cluster.ts | 4 +- .../@aws-cdk/aws-eks/lib/fargate-profile.ts | 2 +- .../@aws-cdk/aws-eks/lib/kubectl-layer.ts | 4 +- .../@aws-cdk/aws-eks/lib/kubectl-provider.ts | 6 +- .../@aws-cdk/aws-eks/lib/legacy-cluster.ts | 4 +- .../@aws-cdk/aws-eks/lib/service-account.ts | 4 +- .../test/cluster-resource-handler-mocks.ts | 4 +- packages/@aws-cdk/aws-eks/test/hello-k8s.ts | 4 +- .../integ.eks-cluster.kubectl-disabled.ts | 2 +- .../test/test.cluster-resource-provider.ts | 12 +- .../@aws-cdk/aws-eks/test/test.cluster.ts | 45 +-- .../@aws-cdk/aws-eks/test/test.fargate.ts | 68 ++--- .../@aws-cdk/aws-eks/test/test.helm-chart.ts | 2 +- .../aws-eks/test/test.k8s-object-value.ts | 12 +- .../@aws-cdk/aws-eks/test/test.k8s-patch.ts | 10 +- .../@aws-cdk/aws-eks/test/test.nodegroup.ts | 2 +- .../@aws-cdk/aws-eks/test/test.user-data.ts | 2 +- packages/@aws-cdk/aws-eks/test/util.ts | 2 +- .../lib/load-balancer.ts | 8 +- .../test/test.loadbalancer.ts | 2 +- .../test/cognito.test.ts | 2 +- .../test/instance-target.test.ts | 2 +- .../test/lambda-target.test.ts | 2 +- .../lib/alb/application-listener-rule.ts | 2 +- .../lib/alb/application-target-group.ts | 2 +- .../lib/nlb/network-target-group.ts | 10 +- .../lib/shared/base-load-balancer.ts | 4 +- .../lib/shared/base-target-group.ts | 18 +- .../test/alb/test.listener.ts | 21 +- .../test/alb/test.load-balancer.ts | 4 +- .../test/alb/test.security-groups.ts | 6 +- .../test/alb/test.target-group.ts | 2 +- .../test/integ.alb.dualstack.ts | 2 +- .../test/integ.alb2.ts | 2 +- .../test/nlb/test.listener.ts | 4 +- .../test/nlb/test.load-balancer.ts | 25 +- .../aws-events-targets/lib/aws-api.ts | 2 +- .../test/aws-api/aws-api-handler.test.ts | 4 +- .../integ.pipeline-event-target.ts | 8 +- .../test/codepipeline/pipeline.test.ts | 16 +- .../test/ecs/event-rule-target.test.ts | 6 +- .../test/ecs/integ.event-ec2-task.lit.ts | 2 +- .../test/ecs/integ.event-fargate-task.ts | 2 +- .../test/kinesis/kinesis-stream.test.ts | 8 +- .../aws-events-targets/test/sns/sns.test.ts | 2 +- packages/@aws-cdk/aws-events/lib/input.ts | 6 +- .../@aws-cdk/aws-events/test/test.rule.ts | 60 ++-- .../@aws-cdk/aws-events/test/test.util.ts | 30 +- packages/@aws-cdk/aws-fsx/lib/file-system.ts | 6 +- .../aws-fsx/lib/lustre-file-system.ts | 12 +- .../aws-fsx/test/integ.lustre-file-system.ts | 6 +- .../aws-fsx/test/lustre-file-system.test.ts | 18 +- .../aws-fsx/test/maintenance-time.test.ts | 10 +- .../lib/accelerator-security-group.ts | 2 +- .../lib/endpoint-group.ts | 2 +- .../test/integ.globalaccelerator.ts | 4 +- packages/@aws-cdk/aws-glue/lib/table.ts | 2 +- .../@aws-cdk/aws-glue/test/database.test.ts | 6 +- .../@aws-cdk/aws-iam/lib/managed-policy.ts | 2 +- .../aws-iam/lib/oidc-provider/index.ts | 2 +- .../@aws-cdk/aws-iam/lib/policy-statement.ts | 10 +- .../aws-iam/lib/private/immutable-role.ts | 2 +- packages/@aws-cdk/aws-iam/lib/util.ts | 12 +- .../test/auto-cross-stack-refs.test.ts | 2 +- .../@aws-cdk/aws-iam/test/example.role.lit.ts | 3 +- packages/@aws-cdk/aws-iam/test/group.test.ts | 22 +- .../aws-iam/test/integ.oidc-provider.ts | 2 +- .../aws-iam/test/managed-policy.test.ts | 62 +++-- .../aws-iam/test/oidc-provider.test.ts | 64 ++--- .../aws-iam/test/policy-document.test.ts | 62 +++-- .../aws-iam/test/policy-statement.test.ts | 4 +- packages/@aws-cdk/aws-iam/test/policy.test.ts | 256 ++++++++++++------ .../@aws-cdk/aws-iam/test/principals.test.ts | 2 +- .../aws-iam/test/role.from-role-arn.test.ts | 9 +- packages/@aws-cdk/aws-iam/test/role.test.ts | 120 +++++--- packages/@aws-cdk/aws-kinesis/lib/stream.ts | 4 +- packages/@aws-cdk/aws-kms/lib/alias.ts | 2 +- packages/@aws-cdk/aws-kms/test/test.alias.ts | 6 +- .../aws-lambda-event-sources/lib/dynamodb.ts | 4 +- .../aws-lambda-event-sources/lib/kinesis.ts | 4 +- .../aws-lambda-event-sources/test/integ.s3.ts | 4 +- .../aws-lambda-event-sources/test/test.api.ts | 8 +- .../test/test.dynamo.ts | 2 +- .../aws-lambda-event-sources/test/test.s3.ts | 2 +- .../aws-lambda-nodejs/test/docker.test.ts | 6 +- .../test/integ.dependencies.ts | 2 +- .../aws-lambda-nodejs/test/integ.function.ts | 2 +- packages/@aws-cdk/aws-lambda/lib/alias.ts | 2 +- .../aws-lambda/lib/event-source-mapping.ts | 2 +- .../@aws-cdk/aws-lambda/lib/filesystem.ts | 10 +- packages/@aws-cdk/aws-lambda/lib/function.ts | 12 +- .../@aws-cdk/aws-lambda/lib/lambda-version.ts | 2 +- packages/@aws-cdk/aws-lambda/lib/runtime.ts | 34 +-- .../aws-lambda/test/integ.autoscaling.lit.ts | 4 +- .../@aws-cdk/aws-lambda/test/test.alias.ts | 26 +- .../@aws-cdk/aws-lambda/test/test.code.ts | 3 +- .../@aws-cdk/aws-lambda/test/test.function.ts | 5 +- .../@aws-cdk/aws-lambda/test/test.layers.ts | 2 +- .../@aws-cdk/aws-lambda/test/test.runtime.ts | 20 +- .../aws-lambda/test/test.singleton-lambda.ts | 12 +- .../aws-lambda/test/test.vpc-lambda.ts | 44 +-- .../test/kinesis.test.ts | 26 +- .../aws-logs-destinations/test/lambda.test.ts | 12 +- .../aws-logs/lib/cross-account-destination.ts | 5 +- packages/@aws-cdk/aws-logs/lib/log-group.ts | 2 +- .../aws-logs/test/test.destination.ts | 2 +- .../@aws-cdk/aws-logs/test/test.loggroup.ts | 6 +- .../aws-logs/test/test.metricfilter.ts | 2 +- packages/@aws-cdk/aws-rds/lib/proxy.ts | 6 +- .../@aws-cdk/aws-rds/test/test.cluster.ts | 8 +- .../@aws-cdk/aws-rds/test/test.instance.ts | 8 +- .../aws-redshift/lib/parameter-group.ts | 2 +- .../lib/bucket-website-target.ts | 10 +- .../test/bucket-website-target.test.ts | 6 +- .../test/classic-load-balancer-target.test.ts | 4 +- .../test/cloudfront-target.test.ts | 2 +- .../test/integ.cloudfront-alias-target.ts | 2 +- .../test/load-balancer-target.test.ts | 4 +- packages/@aws-cdk/aws-route53/lib/util.ts | 2 +- .../@aws-cdk/aws-route53/test/test.route53.ts | 4 +- packages/@aws-cdk/aws-s3-assets/lib/asset.ts | 2 +- .../@aws-cdk/aws-s3-assets/test/asset.test.ts | 8 +- .../lib/bucket-deployment.ts | 2 +- .../integ.bucket-deployment-cloudfront.ts | 2 +- .../aws-s3-notifications/lib/lambda.ts | 2 +- .../@aws-cdk/aws-s3-notifications/lib/sns.ts | 2 +- .../@aws-cdk/aws-s3-notifications/lib/sqs.ts | 2 +- .../test/lambda/lambda.test.ts | 26 +- .../test/notifications.test.ts | 13 +- .../aws-s3-notifications/test/queue.test.ts | 8 +- .../aws-s3-notifications/test/sns.test.ts | 2 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 8 +- .../notifications-resource.ts | 2 +- packages/@aws-cdk/aws-s3/test/test.bucket.ts | 19 +- packages/@aws-cdk/aws-s3/test/test.metrics.ts | 4 +- .../@aws-cdk/aws-s3/test/test.notification.ts | 9 +- .../aws-secretsmanager/lib/secret-rotation.ts | 4 +- .../aws-servicediscovery/lib/service.ts | 2 +- packages/@aws-cdk/aws-ses-actions/lib/s3.ts | 2 +- .../aws-sns-subscriptions/test/subs.test.ts | 12 +- .../aws-sns/lib/subscription-filter.ts | 4 +- packages/@aws-cdk/aws-sns/lib/topic-base.ts | 2 +- packages/@aws-cdk/aws-sns/test/test.sns.ts | 4 +- .../aws-sns/test/test.subscription.ts | 6 +- packages/@aws-cdk/aws-sqs/lib/queue-base.ts | 2 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 16 +- .../aws-ssm/test/integ.parameter-arns.ts | 6 +- .../@aws-cdk/aws-ssm/test/test.parameter.ts | 46 ++-- .../lib/ecs/run-ecs-ec2-task.ts | 2 +- .../lib/ecs/run-task.ts | 2 +- .../lib/emr/emr-add-step.ts | 2 +- .../lib/emr/emr-terminate-cluster.ts | 2 +- .../lib/evaluate-expression.ts | 2 +- .../test/batch/integ.run-batch-job.ts | 2 +- .../test/batch/integ.submit-job.ts | 2 +- .../test/batch/run-batch-job.test.ts | 2 +- .../test/batch/submit-job.test.ts | 2 +- .../test/ecs/ecs-tasks.test.ts | 56 ++-- .../test/ecs/integ.ec2-run-task.ts | 2 +- .../test/ecs/integ.ec2-task.ts | 2 +- .../test/ecs/integ.fargate-run-task.ts | 2 +- .../test/ecs/integ.fargate-task.ts | 2 +- .../test/emr/emr-create-cluster.test.ts | 7 +- .../test/glue/integ.glue-task.ts | 2 +- .../test/glue/integ.start-job-run.ts | 2 +- .../test/lambda/integ.invoke-function.ts | 2 +- .../test/private/task-utils.test.ts | 6 +- .../sagemaker/create-training-job.test.ts | 20 +- .../sagemaker/create-transform-job.test.ts | 4 +- .../sagemaker/integ.create-training-job.ts | 11 +- .../test/sns/integ.publish.ts | 2 +- .../test/sns/publish-to-topic.test.ts | 50 ++-- .../test/sns/publish.test.ts | 2 +- .../test/sqs/send-to-queue.test.ts | 44 +-- .../test/stepfunctions/invoke-activity.ts | 2 +- .../aws-stepfunctions/lib/states/pass.ts | 6 +- .../aws-stepfunctions/lib/states/task-base.ts | 2 +- .../aws-stepfunctions/test/activity.test.ts | 2 +- .../aws-stepfunctions/test/parallel.test.ts | 2 +- .../aws-stepfunctions/test/pass.test.ts | 2 +- .../test/state-machine-resources.test.ts | 38 ++- .../test/states-language.test.ts | 8 +- .../@aws-cdk/aws-synthetics/lib/canary.ts | 6 +- packages/@aws-cdk/aws-synthetics/lib/code.ts | 2 +- .../aws-synthetics/test/canary.test.ts | 10 +- .../aws-synthetics/test/metric.test.ts | 2 +- .../lib/cloud-assembly/artifact-schema.ts | 2 +- .../scripts/update-schema.ts | 6 +- .../cloudformation-diff/lib/diff/index.ts | 2 +- .../cloudformation-diff/lib/diff/types.ts | 3 +- .../cloudformation-diff/lib/format-table.ts | 2 +- .../cloudformation-diff/lib/format.ts | 8 +- .../test/diff-template.test.ts | 8 +- .../test/iam/detect-changes.test.ts | 76 +++--- .../test/iam/statement.test.ts | 8 +- .../cloudformation-include/lib/cfn-include.ts | 2 +- .../test/nested-stacks.test.ts | 108 +++++--- .../test/valid-templates.test.ts | 7 +- packages/@aws-cdk/core/lib/app.ts | 4 +- packages/@aws-cdk/core/lib/arn.ts | 4 +- packages/@aws-cdk/core/lib/aspect.ts | 2 +- packages/@aws-cdk/core/lib/asset-staging.ts | 2 +- packages/@aws-cdk/core/lib/cfn-fn.ts | 28 +- .../custom-resource-provider.ts | 2 +- .../core/lib/private/asset-parameters.ts | 2 +- .../core/lib/private/cfn-reference.ts | 2 +- .../core/lib/private/cloudformation-lang.ts | 17 +- .../@aws-cdk/core/lib/private/encoding.ts | 2 +- .../@aws-cdk/core/lib/private/node-version.ts | 2 +- packages/@aws-cdk/core/lib/private/refs.ts | 2 +- packages/@aws-cdk/core/lib/private/resolve.ts | 2 +- .../@aws-cdk/core/lib/private/token-map.ts | 6 +- packages/@aws-cdk/core/lib/resolvable.ts | 2 +- packages/@aws-cdk/core/lib/secret-value.ts | 6 +- packages/@aws-cdk/core/lib/stack-trace.ts | 2 +- packages/@aws-cdk/core/lib/stack.ts | 13 +- packages/@aws-cdk/core/lib/stage.ts | 2 +- .../@aws-cdk/core/lib/string-fragments.ts | 2 +- packages/@aws-cdk/core/lib/token.ts | 10 +- packages/@aws-cdk/core/lib/util.ts | 4 +- .../core/test/fs/test.fs-fingerprint.ts | 6 +- .../test.new-style-synthesis.ts | 2 +- packages/@aws-cdk/core/test/test.app.ts | 20 +- packages/@aws-cdk/core/test/test.arn.ts | 26 +- packages/@aws-cdk/core/test/test.cfn-json.ts | 6 +- .../core/test/test.cloudformation-json.ts | 24 +- packages/@aws-cdk/core/test/test.condition.ts | 23 +- packages/@aws-cdk/core/test/test.construct.ts | 18 +- packages/@aws-cdk/core/test/test.context.ts | 6 +- .../@aws-cdk/core/test/test.environment.ts | 12 +- packages/@aws-cdk/core/test/test.fn.ts | 29 +- packages/@aws-cdk/core/test/test.include.ts | 14 +- .../@aws-cdk/core/test/test.logical-id.ts | 8 +- packages/@aws-cdk/core/test/test.mappings.ts | 62 +++-- packages/@aws-cdk/core/test/test.output.ts | 14 +- packages/@aws-cdk/core/test/test.parameter.ts | 9 +- packages/@aws-cdk/core/test/test.resource.ts | 202 +++++++++----- packages/@aws-cdk/core/test/test.rule.ts | 6 +- .../@aws-cdk/core/test/test.runtime-info.ts | 4 +- packages/@aws-cdk/core/test/test.size.ts | 6 +- packages/@aws-cdk/core/test/test.stack.ts | 103 ++++--- packages/@aws-cdk/core/test/test.stage.ts | 4 +- packages/@aws-cdk/core/test/test.staging.ts | 14 +- packages/@aws-cdk/core/test/test.synthesis.ts | 6 +- .../@aws-cdk/core/test/test.tag-aspect.ts | 60 ++-- .../@aws-cdk/core/test/test.tag-manager.ts | 30 +- packages/@aws-cdk/core/test/test.tokens.ts | 32 +-- packages/@aws-cdk/core/test/test.util.ts | 26 +- .../provider-framework/runtime/framework.ts | 4 +- .../waiter-state-machine.ts | 16 +- .../aws-custom-resource.test.ts | 34 +-- .../integ.aws-custom-resource.ts | 6 +- .../integration-test-fixtures/s3-assert.ts | 2 +- .../integration-test-fixtures/s3-file.ts | 5 +- .../test/provider-framework/provider.test.ts | 6 +- .../waiter-state-machine.test.ts | 2 +- .../@aws-cdk/cx-api/lib/cloud-artifact.ts | 2 +- .../@aws-cdk/cx-api/lib/cloud-assembly.ts | 2 +- packages/@aws-cdk/cx-api/lib/environment.ts | 2 +- packages/@aws-cdk/cx-api/lib/placeholders.ts | 2 +- .../test/cloud-assembly-builder.test.ts | 4 +- .../cx-api/test/cloud-assembly.test.ts | 6 +- .../pipelines/lib/private/asset-manifest.ts | 2 +- .../@aws-cdk/pipelines/test/builds.test.ts | 2 +- .../test/cross-environment-infra.test.ts | 4 +- .../pipelines/test/pipeline-assets.test.ts | 28 +- .../@aws-cdk/pipelines/test/pipeline.test.ts | 6 +- .../pipelines/test/validation.test.ts | 6 +- packages/aws-cdk/bin/cdk.ts | 10 +- .../aws-cdk/lib/api/aws-auth/sdk-provider.ts | 2 +- .../lib/api/bootstrap/legacy-template.ts | 6 +- packages/aws-cdk/lib/api/cxapp/exec.ts | 2 +- packages/aws-cdk/lib/api/deploy-stack.ts | 4 +- packages/aws-cdk/lib/api/toolkit-info.ts | 7 +- .../cloudformation/stack-activity-monitor.ts | 2 +- packages/aws-cdk/lib/cdk-toolkit.ts | 4 +- packages/aws-cdk/lib/commands/docs.ts | 2 +- packages/aws-cdk/lib/commands/doctor.ts | 2 +- .../endpoint-service-availability-zones.ts | 2 +- .../aws-cdk/lib/context-providers/index.ts | 2 +- .../app/csharp/add-project.hook.ts | 4 +- .../app/fsharp/add-project.hook.ts | 4 +- .../sample-app/csharp/add-project.hook.ts | 4 +- .../sample-app/fsharp/add-project.hook.ts | 4 +- packages/aws-cdk/lib/init.ts | 2 +- packages/aws-cdk/lib/os.ts | 2 +- packages/aws-cdk/lib/settings.ts | 2 +- packages/aws-cdk/test/api/bootstrap.test.ts | 132 +++++---- .../aws-cdk/test/api/deploy-stack.test.ts | 36 +-- packages/aws-cdk/test/api/exec.test.ts | 2 +- .../aws-cdk/test/api/sdk-provider.test.ts | 11 +- packages/aws-cdk/test/cdk-toolkit.test.ts | 2 +- .../context-providers/asymmetric-vpcs.test.ts | 46 +++- .../test/context-providers/vpcs.test.ts | 8 +- packages/aws-cdk/test/context.test.ts | 2 +- packages/aws-cdk/test/diff.test.ts | 4 +- .../test/integ/cli/bootstrapping.integtest.ts | 13 +- .../aws-cdk/test/integ/cli/cdk-helpers.ts | 4 +- .../aws-cdk/test/integ/cli/cli.integtest.ts | 22 +- .../aws-cdk/test/util/cloudformation.test.ts | 11 +- packages/aws-cdk/test/util/mock-sdk.ts | 2 +- packages/awslint/bin/awslint.ts | 10 +- packages/awslint/lib/linter.ts | 2 +- packages/awslint/lib/rules/module.ts | 8 +- packages/awslint/lib/rules/resource.ts | 6 +- packages/cdk-assets/bin/cdk-assets.ts | 2 +- packages/cdk-assets/bin/publish.ts | 8 +- packages/cdk-assets/lib/asset-manifest.ts | 8 +- packages/cdk-assets/lib/private/docker.ts | 2 +- packages/cdk-assets/lib/private/shell.ts | 2 +- .../cdk-assets/test/docker-images.test.ts | 2 +- packages/cdk-assets/test/files.test.ts | 2 +- packages/cdk-assets/test/mock-aws.ts | 12 +- packages/cdk-assets/test/placeholders.test.ts | 2 +- tools/cdk-build-tools/bin/cdk-package.ts | 6 +- tools/cdk-build-tools/config/eslintrc.js | 67 +++-- tools/cdk-build-tools/lib/lint.ts | 2 +- tools/cdk-build-tools/lib/os.ts | 4 +- tools/cdk-integ-tools/bin/cdk-integ.ts | 4 +- tools/cdk-integ-tools/lib/integ-helpers.ts | 8 +- tools/cfn2ts/lib/codegen.ts | 5 +- tools/cfn2ts/lib/genspec.ts | 2 +- tools/cfn2ts/lib/spec-utils.ts | 2 +- tools/pkglint/bin/pkglint.ts | 2 +- tools/pkglint/lib/rules.ts | 2 +- 545 files changed, 3202 insertions(+), 2627 deletions(-) diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index 918279b480b30..7cb661c8601f7 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -504,7 +504,7 @@ function createSelfUpdatingStack(pipelineStack: cdk.Stack): SelfUpdatingPipeline stageName: 'build', actions: [buildAction], }); - return {synthesizedApp: buildOutput, pipeline}; + return { synthesizedApp: buildOutput, pipeline }; } function hasPipelineAction(expectedAction: any): (props: any) => boolean { diff --git a/packages/@aws-cdk/assert/test/assertions.test.ts b/packages/@aws-cdk/assert/test/assertions.test.ts index adf61cb0dec9a..a73b64aaf0329 100644 --- a/packages/@aws-cdk/assert/test/assertions.test.ts +++ b/packages/@aws-cdk/assert/test/assertions.test.ts @@ -92,8 +92,8 @@ passingExample('expect to be a superset of