packages/aws-cdk-lib/aws-appsync/lib/data-source.ts (240 lines of code) (raw):

import { Construct } from 'constructs'; import { BaseAppsyncFunctionProps, AppsyncFunction } from './appsync-function'; import { CfnDataSource } from './appsync.generated'; import { IGraphqlApi } from './graphqlapi-base'; import { BaseResolverProps, Resolver } from './resolver'; import { ITable } from '../../aws-dynamodb'; import { IDomain as IElasticsearchDomain } from '../../aws-elasticsearch'; import { IEventBus } from '../../aws-events'; import { Grant, IGrantable, IPrincipal, IRole, Role, ServicePrincipal } from '../../aws-iam'; import { IFunction } from '../../aws-lambda'; import { IDomain as IOpenSearchDomain } from '../../aws-opensearchservice'; import { IServerlessCluster, IDatabaseCluster } from '../../aws-rds'; import { ISecret } from '../../aws-secretsmanager'; import { IResolvable, Lazy, Stack, Token } from '../../core'; /** * Base properties for an AppSync datasource */ export interface BaseDataSourceProps { /** * The API to attach this data source to */ readonly api: IGraphqlApi; /** * The name of the data source * * @default - id of data source */ readonly name?: string; /** * the description of the data source * * @default - None */ readonly description?: string; } /** * properties for an AppSync datasource backed by a resource */ export interface BackedDataSourceProps extends BaseDataSourceProps { /** * The IAM service role to be assumed by AppSync to interact with the data source * * @default - Create a new role */ readonly serviceRole?: IRole; } /** * props used by implementations of BaseDataSource to provide configuration. Should not be used directly. */ export interface ExtendedDataSourceProps { /** * the type of the AppSync datasource */ readonly type: string; /** * configuration for DynamoDB Datasource * * @default - No config */ readonly dynamoDbConfig?: CfnDataSource.DynamoDBConfigProperty | IResolvable; /** * configuration for Elasticsearch data source * * @deprecated - use `openSearchConfig` * @default - No config */ readonly elasticsearchConfig?: CfnDataSource.ElasticsearchConfigProperty | IResolvable; /** * configuration for OpenSearch data source * * @default - No config */ readonly openSearchServiceConfig?: CfnDataSource.OpenSearchServiceConfigProperty | IResolvable; /** * configuration for HTTP Datasource * * @default - No config */ readonly httpConfig?: CfnDataSource.HttpConfigProperty | IResolvable; /** * configuration for EventBridge Datasource * * @default - No config */ readonly eventBridgeConfig?: CfnDataSource.EventBridgeConfigProperty | IResolvable; /** * configuration for Lambda Datasource * * @default - No config */ readonly lambdaConfig?: CfnDataSource.LambdaConfigProperty | IResolvable; /** * configuration for RDS Datasource * * @default - No config */ readonly relationalDatabaseConfig?: CfnDataSource.RelationalDatabaseConfigProperty | IResolvable; } /** * Abstract AppSync datasource implementation. Do not use directly but use subclasses for concrete datasources */ export abstract class BaseDataSource extends Construct { /** * the name of the data source */ public readonly name: string; /** * the underlying CFN data source resource */ public readonly ds: CfnDataSource; protected api: IGraphqlApi; protected serviceRole?: IRole; constructor(scope: Construct, id: string, props: BackedDataSourceProps, extended: ExtendedDataSourceProps) { super(scope, id); if (extended.type !== 'NONE') { this.serviceRole = props.serviceRole || new Role(this, 'ServiceRole', { assumedBy: new ServicePrincipal('appsync.amazonaws.com') }); } // Replace unsupported characters from DataSource name. The only allowed pattern is: {[_A-Za-z][_0-9A-Za-z]*} const name = (props.name ?? id); const supportedName = Token.isUnresolved(name) ? name : name.replace(/[\W]+/g, ''); this.ds = new CfnDataSource(this, 'Resource', { apiId: props.api.apiId, name: supportedName, description: props.description, serviceRoleArn: this.serviceRole?.roleArn, ...extended, }); this.name = supportedName; this.api = props.api; } /** * creates a new resolver for this datasource and API using the given properties */ public createResolver(id: string, props: BaseResolverProps): Resolver { return new Resolver(this.api, id, { api: this.api, dataSource: this, ...props, }); } /** * creates a new appsync function for this datasource and API using the given properties */ public createFunction(id: string, props: BaseAppsyncFunctionProps): AppsyncFunction { return new AppsyncFunction(this.api, id, { api: this.api, dataSource: this, ...props, }); } } /** * Abstract AppSync datasource implementation. Do not use directly but use subclasses for resource backed datasources */ export abstract class BackedDataSource extends BaseDataSource implements IGrantable { /** * the principal of the data source to be IGrantable */ public readonly grantPrincipal: IPrincipal; constructor(scope: Construct, id: string, props: BackedDataSourceProps, extended: ExtendedDataSourceProps) { super(scope, id, props, extended); this.grantPrincipal = this.serviceRole!; } } /** * Properties for an AppSync dummy datasource */ export interface NoneDataSourceProps extends BaseDataSourceProps { } /** * An AppSync dummy datasource */ export class NoneDataSource extends BaseDataSource { constructor(scope: Construct, id: string, props: NoneDataSourceProps) { super(scope, id, props, { type: 'NONE', }); } } /** * Properties for an AppSync DynamoDB datasource */ export interface DynamoDbDataSourceProps extends BackedDataSourceProps { /** * The DynamoDB table backing this data source */ readonly table: ITable; /** * Specify whether this DS is read only or has read and write permissions to the DynamoDB table * * @default false */ readonly readOnlyAccess?: boolean; /** * use credentials of caller to access DynamoDB * * @default false */ readonly useCallerCredentials?: boolean; } /** * An AppSync datasource backed by a DynamoDB table */ export class DynamoDbDataSource extends BackedDataSource { constructor(scope: Construct, id: string, props: DynamoDbDataSourceProps) { super(scope, id, props, { type: 'AMAZON_DYNAMODB', dynamoDbConfig: { tableName: props.table.tableName, awsRegion: props.table.env.region, useCallerCredentials: props.useCallerCredentials, }, }); if (props.readOnlyAccess) { props.table.grantReadData(this); } else { props.table.grantReadWriteData(this); } } } /** * The authorization config in case the HTTP endpoint requires authorization */ export interface AwsIamConfig { /** * The signing region for AWS IAM authorization */ readonly signingRegion: string; /** * The signing service name for AWS IAM authorization */ readonly signingServiceName: string; } /** * Properties for an AppSync http datasource */ export interface HttpDataSourceProps extends BackedDataSourceProps { /** * The http endpoint */ readonly endpoint: string; /** * The authorization config in case the HTTP endpoint requires authorization * * @default - none * */ readonly authorizationConfig?: AwsIamConfig; } /** * An AppSync datasource backed by a http endpoint */ export class HttpDataSource extends BackedDataSource { constructor(scope: Construct, id: string, props: HttpDataSourceProps) { const authorizationConfig = props.authorizationConfig ? { authorizationType: 'AWS_IAM', awsIamConfig: props.authorizationConfig, } : undefined; super(scope, id, props, { type: 'HTTP', httpConfig: { endpoint: props.endpoint, authorizationConfig, }, }); } } /** * Properties for an AppSync EventBridge datasource */ export interface EventBridgeDataSourceProps extends BackedDataSourceProps { /** * The EventBridge EventBus */ readonly eventBus: IEventBus; } /** * An AppSync datasource backed by EventBridge */ export class EventBridgeDataSource extends BackedDataSource { constructor(scope: Construct, id: string, props: EventBridgeDataSourceProps) { super(scope, id, props, { type: 'AMAZON_EVENTBRIDGE', eventBridgeConfig: { eventBusArn: props.eventBus.eventBusArn, }, }); props.eventBus.grantPutEventsTo(this); } } /** * Properties for an AppSync Lambda datasource */ export interface LambdaDataSourceProps extends BackedDataSourceProps { /** * The Lambda function to call to interact with this data source */ readonly lambdaFunction: IFunction; } /** * An AppSync datasource backed by a Lambda function */ export class LambdaDataSource extends BackedDataSource { constructor(scope: Construct, id: string, props: LambdaDataSourceProps) { super(scope, id, props, { type: 'AWS_LAMBDA', lambdaConfig: { lambdaFunctionArn: props.lambdaFunction.functionArn, }, }); props.lambdaFunction.grantInvoke(this); } } /** * Properties for an AppSync RDS datasource Aurora Serverless V1 */ export interface RdsDataSourceProps extends BackedDataSourceProps { /** * The serverless cluster to call to interact with this data source */ readonly serverlessCluster: IServerlessCluster; /** * The secret containing the credentials for the database */ readonly secretStore: ISecret; /** * The name of the database to use within the cluster * * @default - None */ readonly databaseName?: string; } /** * Properties for an AppSync RDS datasource Aurora Serverless V2 */ export interface RdsDataSourcePropsV2 extends BackedDataSourceProps { /** * The serverless cluster to call to interact with this data source */ readonly serverlessCluster: IDatabaseCluster; /** * The secret containing the credentials for the database */ readonly secretStore: ISecret; /** * The name of the database to use within the cluster * * @default - None */ readonly databaseName?: string; } /** * An AppSync datasource backed by RDS */ export class RdsDataSource extends BackedDataSource { constructor(scope: Construct, id: string, props: RdsDataSourceProps) constructor(scope: Construct, id: string, props: RdsDataSourcePropsV2) { super(scope, id, props, { type: 'RELATIONAL_DATABASE', relationalDatabaseConfig: { rdsHttpEndpointConfig: { awsRegion: props.serverlessCluster.env.region, dbClusterIdentifier: Lazy.string({ produce: () => { return Stack.of(this).formatArn({ service: 'rds', resource: `cluster:${props.serverlessCluster.clusterIdentifier}`, }); }, }), awsSecretStoreArn: props.secretStore.secretArn, databaseName: props.databaseName, }, relationalDatabaseSourceType: 'RDS_HTTP_ENDPOINT', }, }); const clusterArn = Stack.of(this).formatArn({ service: 'rds', resource: `cluster:${props.serverlessCluster.clusterIdentifier}`, }); props.secretStore.grantRead(this); // Change to grant with RDS grant becomes implemented props.serverlessCluster.grantDataApiAccess(this); Grant.addToPrincipal({ grantee: this, actions: [ 'rds-data:DeleteItems', 'rds-data:ExecuteSql', 'rds-data:GetItems', 'rds-data:InsertItems', 'rds-data:UpdateItems', ], resourceArns: [clusterArn, `${clusterArn}:*`], scope: this, }); } } /** * Properties for the Elasticsearch Data Source * * @deprecated - use `OpenSearchDataSourceProps` with `OpenSearchDataSource` */ export interface ElasticsearchDataSourceProps extends BackedDataSourceProps { /** * The elasticsearch domain containing the endpoint for the data source */ readonly domain: IElasticsearchDomain; } /** * An Appsync datasource backed by Elasticsearch * * @deprecated - use `OpenSearchDataSource` */ export class ElasticsearchDataSource extends BackedDataSource { constructor(scope: Construct, id: string, props: ElasticsearchDataSourceProps) { super(scope, id, props, { type: 'AMAZON_ELASTICSEARCH', elasticsearchConfig: { awsRegion: props.domain.env.region, endpoint: `https://${props.domain.domainEndpoint}`, }, }); props.domain.grantReadWrite(this); } } /** * Properties for the OpenSearch Data Source */ export interface OpenSearchDataSourceProps extends BackedDataSourceProps { /** * The OpenSearch domain containing the endpoint for the data source */ readonly domain: IOpenSearchDomain; } /** * An Appsync datasource backed by OpenSearch */ export class OpenSearchDataSource extends BackedDataSource { constructor(scope: Construct, id: string, props: OpenSearchDataSourceProps) { super(scope, id, props, { type: 'AMAZON_OPENSEARCH_SERVICE', openSearchServiceConfig: { awsRegion: props.domain.env.region, endpoint: `https://${props.domain.domainEndpoint}`, }, }); props.domain.grantReadWrite(this); } }