diff --git a/packages/amplify-graphql-function-transformer/src/graphql-function-transformer.ts b/packages/amplify-graphql-function-transformer/src/graphql-function-transformer.ts index 78c1e87da65..c6a193ddd58 100644 --- a/packages/amplify-graphql-function-transformer/src/graphql-function-transformer.ts +++ b/packages/amplify-graphql-function-transformer/src/graphql-function-transformer.ts @@ -68,7 +68,7 @@ export class FunctionTransformer extends TransformerPluginBase { const dataSourceId = FunctionResourceIDs.FunctionDataSourceID(config.name, config.region); if (!createdResources.has(dataSourceId)) { - const dataSource = context.api.addLambdaDataSource( + const dataSource = context.api.host.addLambdaDataSource( dataSourceId, lambda.Function.fromFunctionAttributes(stack, `${dataSourceId}Function`, { functionArn: lambdaArnResource(env, config.name, config.region), @@ -84,7 +84,7 @@ export class FunctionTransformer extends TransformerPluginBase { let func = createdResources.get(functionId); if (func === undefined) { - func = context.api.addAppSyncFunction( + func = context.api.host.addAppSyncFunction( functionId, MappingTemplate.s3MappingTemplateFromString( printBlock(`Invoke AWS Lambda data source: ${dataSourceId}`)( @@ -125,7 +125,7 @@ export class FunctionTransformer extends TransformerPluginBase { let resolver = createdResources.get(resolverId); if (resolver === undefined) { - resolver = context.api.addResolver( + resolver = context.api.host.addResolver( config.resolverTypeName, config.resolverFieldName, MappingTemplate.s3MappingTemplateFromString( diff --git a/packages/amplify-graphql-http-transformer/src/graphql-http-transformer.ts b/packages/amplify-graphql-http-transformer/src/graphql-http-transformer.ts index 0c780621ce0..aa4862cc17e 100644 --- a/packages/amplify-graphql-http-transformer/src/graphql-http-transformer.ts +++ b/packages/amplify-graphql-http-transformer/src/graphql-http-transformer.ts @@ -200,8 +200,8 @@ export class HttpTransformer extends TransformerPluginBase { // Create a new data source if necessary. const dataSourceId = HttpResourceIDs.HttpDataSourceID(directive.origin); - if (context.api.getDataSource(dataSourceId) === undefined) { - context.api.addHttpDataSource(dataSourceId, replaceEnvAndRegion(env, region, directive.origin), {}, stack); + if (context.api.host.getDataSource(dataSourceId) === undefined) { + context.api.host.addHttpDataSource(dataSourceId, replaceEnvAndRegion(env, region, directive.origin), {}, stack); } // Create the GraphQL resolver. @@ -272,7 +272,7 @@ function createResolver(stack: cdk.Stack, dataSourceId: string, context: Transfo ? MappingTemplate.inlineTemplateFromString(requestTemplateString) : MappingTemplate.s3MappingTemplateFromString(requestTemplateString, `${config.resolverTypeName}.${config.resolverFieldName}.req.vtl`); - return context.api.addResolver( + return context.api.host.addResolver( config.resolverTypeName, config.resolverFieldName, requestMappingTemplate, diff --git a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts index 5d2e298f4f9..e73be42f1d1 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts @@ -262,7 +262,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme removalPolicy: RemovalPolicy.DESTROY, }); // Expose a better API to select what stack this belongs to - const dataSource = context.api.addDynamoDbDataSource( + const dataSource = context.api.host.addDynamoDbDataSource( `${def!.name.value}DS`, table, { diff --git a/packages/amplify-graphql-predictions-transformer/src/graphql-predictions-transformer.ts b/packages/amplify-graphql-predictions-transformer/src/graphql-predictions-transformer.ts index 0840b442dc6..bd3b5eb2885 100644 --- a/packages/amplify-graphql-predictions-transformer/src/graphql-predictions-transformer.ts +++ b/packages/amplify-graphql-predictions-transformer/src/graphql-predictions-transformer.ts @@ -178,7 +178,7 @@ export class PredictionsTransformer extends TransformerPluginBase { const datasourceName = actionToDataSourceMap.get(action) as string; const functionName = PredictionsResourceIDs.getPredictionFunctionName(action); const roleAction = actionToRoleAction.get(action); - let datasource = context.api.getDataSource(datasourceName); + let datasource = context.api.host.getDataSource(datasourceName); if (roleAction && !seenActions.has(action)) { role.attachInlinePolicy( @@ -263,7 +263,7 @@ function createPredictionsDataSource( case identifyEntities: case identifyText: case identifyLabels: - datasource = context.api.addHttpDataSource( + datasource = context.api.host.addHttpDataSource( 'RekognitionDataSource', cdk.Fn.sub('https://rekognition.${AWS::Region}.amazonaws.com'), { @@ -276,7 +276,7 @@ function createPredictionsDataSource( ); break; case translateText: - datasource = context.api.addHttpDataSource( + datasource = context.api.host.addHttpDataSource( 'TranslateDataSource', cdk.Fn.sub('https://translate.${AWS::Region}.amazonaws.com'), { @@ -290,7 +290,7 @@ function createPredictionsDataSource( break; case convertTextToSpeech: default: - datasource = context.api.addLambdaDataSource( + datasource = context.api.host.addLambdaDataSource( 'LambdaDataSource', lambda.Function.fromFunctionAttributes(stack, 'LambdaDataSourceFunction', { functionArn: (lambdaFn as lambda.IFunction)?.functionArn, @@ -321,7 +321,7 @@ function createResolver( substitutions.env = (env as unknown) as string; } - return context.api.addResolver( + return context.api.host.addResolver( config.resolverTypeName, config.resolverFieldName, MappingTemplate.inlineTemplateFromString( @@ -374,7 +374,7 @@ function createPredictionsLambda(context: TransformerContextProvider, stack: cdk // Update the runtime to Node 14 once the following issue is resolved: // https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/80#issuecomment-831796699 - return context.api.addLambdaFunction( + return context.api.host.addLambdaFunction( PredictionsResourceIDs.lambdaHandlerName, `functions/${functionId}.zip`, PredictionsResourceIDs.lambdaHandlerName, @@ -669,7 +669,7 @@ function createActionFunction(context: TransformerContextProvider, stack: cdk.St break; } - return context.api.addAppSyncFunction( + return context.api.host.addAppSyncFunction( `${action}Function`, MappingTemplate.inlineTemplateFromString(print(actionFunctionResolver.request)), MappingTemplate.inlineTemplateFromString(print(actionFunctionResolver.response)), diff --git a/packages/amplify-graphql-searchable-transformer/src/cdk/create-es-datasource.ts b/packages/amplify-graphql-searchable-transformer/src/cdk/create-es-datasource.ts index 058c8961cec..5097c06731a 100644 --- a/packages/amplify-graphql-searchable-transformer/src/cdk/create-es-datasource.ts +++ b/packages/amplify-graphql-searchable-transformer/src/cdk/create-es-datasource.ts @@ -15,7 +15,7 @@ export const createEsDataSource = ( const { ElasticsearchDataSourceLogicalID } = ResourceConstants.RESOURCES; assert(region); const dsEndpoint = 'https://' + domainEndpoint; - return graphqlApiProvider.addElasticSearchDataSource( + return graphqlApiProvider.host.addElasticSearchDataSource( ElasticsearchDataSourceLogicalID, region, dsEndpoint, diff --git a/packages/amplify-graphql-searchable-transformer/src/cdk/create-streaming-lambda.ts b/packages/amplify-graphql-searchable-transformer/src/cdk/create-streaming-lambda.ts index f19a5752a09..01952c30e75 100644 --- a/packages/amplify-graphql-searchable-transformer/src/cdk/create-streaming-lambda.ts +++ b/packages/amplify-graphql-searchable-transformer/src/cdk/create-streaming-lambda.ts @@ -25,7 +25,7 @@ export const createLambda = ( ES_USE_EXTERNAL_VERSIONING: isProjectUsingDataStore.toString(), }; - return apiGraphql.addLambdaFunction( + return apiGraphql.host.addLambdaFunction( ElasticsearchStreamingLambdaFunctionLogicalID, 'functions/' + ElasticsearchStreamingLambdaFunctionLogicalID + '.zip', parameterMap.get(ElasticsearchStreamingLambdaHandlerName)!.valueAsString, diff --git a/packages/amplify-graphql-transformer-core/src/graphql-api.ts b/packages/amplify-graphql-transformer-core/src/graphql-api.ts index 6b0b7e22ac9..71d9fa52dc8 100644 --- a/packages/amplify-graphql-transformer-core/src/graphql-api.ts +++ b/packages/amplify-graphql-transformer-core/src/graphql-api.ts @@ -1,36 +1,22 @@ -import { APIIAMResourceProvider, GraphQLAPIProvider, MappingTemplateProvider } from '@aws-amplify/graphql-transformer-interfaces'; -import { ElasticSearchDataSourceOptions } from '@aws-amplify/graphql-transformer-interfaces/src/graphql-api-provider'; +import { APIIAMResourceProvider, GraphQLAPIProvider, TransformHostProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { ApiKeyConfig, AuthorizationConfig, AuthorizationMode, AuthorizationType, - BaseDataSource, CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema, - CfnResolver, - DataSourceOptions, - DynamoDbDataSource, GraphqlApiBase, - HttpDataSource, - HttpDataSourceOptions, - LambdaDataSource, LogConfig, - NoneDataSource, OpenIdConnectConfig, UserPoolConfig, UserPoolDefaultAction, } from '@aws-cdk/aws-appsync'; -import { ITable } from '@aws-cdk/aws-dynamodb'; -import { Grant, IGrantable, IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; -import { CfnFunction, Code, Function, IFunction, ILayerVersion, Runtime } from '@aws-cdk/aws-lambda'; -import { CfnResource, Construct, Duration, Stack, Token } from '@aws-cdk/core'; -import { toCamelCase } from 'graphql-transformer-common'; -import { AppSyncFunctionConfiguration } from './appsync-function'; -import { ElasticsearchDataSource } from './cdk-compat/elasticsearch-datasource'; +import { Grant, IGrantable, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { CfnResource, Construct, Duration, Stack } from '@aws-cdk/core'; import { TransformerSchema } from './cdk-compat/schema-asset'; -import { InlineTemplate, S3MappingFunctionCode } from './cdk-compat/template-asset'; +import { DefaultTransformHost } from './transform-host'; export interface GraphqlApiProps { /** @@ -126,6 +112,7 @@ export class IamResource implements APIIAMResourceProvider { export type TransformerAPIProps = GraphqlApiProps & { readonly createApiKey?: boolean; + readonly host?: TransformHostProvider; }; export class GraphQLApi extends GraphqlApiBase implements GraphQLAPIProvider { /** @@ -134,6 +121,12 @@ export class GraphQLApi extends GraphqlApiBase implements GraphQLAPIProvider { */ public readonly apiId: string; + /** + * The TransformHost object provides resource creation utilities in AWS + * such as a LambdaDataSource or a DynamoDBDataSource + */ + public readonly host: TransformHostProvider + /** * the ARN of the API */ @@ -173,8 +166,6 @@ export class GraphQLApi extends GraphqlApiBase implements GraphQLAPIProvider { private apiKeyResource?: CfnApiKey; private authorizationConfig?: Required; - private dataSources: Map = new Map(); - constructor(scope: Construct, id: string, props: TransformerAPIProps) { super(scope, id); this.authorizationConfig = { @@ -215,108 +206,16 @@ export class GraphQLApi extends GraphqlApiBase implements GraphQLAPIProvider { this.apiKeyResource.addDependsOn(this.schemaResource); this.apiKey = this.apiKeyResource.attrApiKey; } - } - - addElasticSearchDataSource( - name: string, - awsRegion: string, - endpoint: string, - options?: ElasticSearchDataSourceOptions, - stack?: Stack, - ): ElasticsearchDataSource { - if (this.dataSources.has(name)) { - throw new Error(`DataSource ${name} already exists in the API`); - } - const data = this.doAddElasticSearchDataSource(name, endpoint, awsRegion, options, stack); - this.dataSources.set(options?.name || name, data); - return data; - } - - public addHttpDataSource(name: string, endpoint: string, options?: DataSourceOptions, stack?: Stack): HttpDataSource { - if (this.dataSources.has(name)) { - throw new Error(`DataSource ${name} already exists in the API`); - } - const dataSource = this.doAddHttpDataSource(name, endpoint, options, stack); - this.dataSources.set(name, dataSource); - return dataSource; - } - - public addDynamoDbDataSource(name: string, table: ITable, options?: DataSourceOptions, stack?: Stack): DynamoDbDataSource { - if (this.dataSources.has(name)) { - throw new Error(`DataSource ${name} already exists in the API`); - } - const dataSource = this.doAddDynamoDbDataSource(name, table, options, stack); - this.dataSources.set(options?.name || name, dataSource); - return dataSource; - } - - public addNoneDataSource(name: string, options?: DataSourceOptions, stack?: Stack): NoneDataSource { - if (this.dataSources.has(name)) { - throw new Error(`DataSource ${name} already exists in the API`); - } - const dataSource = this.doAddNoneDataSource(name, options, stack); - this.dataSources.set(name, dataSource); - return dataSource; - } - public addLambdaDataSource(name: string, lambdaFunction: IFunction, options?: DataSourceOptions, stack?: Stack): LambdaDataSource { - if (!Token.isUnresolved(name) && this.dataSources.has(name)) { - throw new Error(`DataSource ${name} already exists in the API`); + if (props.host) { + this.host = props.host; + this.host.setAPI(this); } - const dataSource = this.doAddLambdaDataSource(name, lambdaFunction, options, stack); - this.dataSources.set(name, dataSource); - return dataSource; - } - - public addAppSyncFunction( - name: string, - requestMappingTemplate: MappingTemplateProvider, - responseMappingTemplate: MappingTemplateProvider, - dataSourceName: string, - stack?: Stack, - ): AppSyncFunctionConfiguration { - if (dataSourceName && !Token.isUnresolved(dataSourceName) && !this.dataSources.has(dataSourceName)) { - throw new Error(`DataSource ${dataSourceName} is missing in the API`); + else { + this.host = new DefaultTransformHost({ + api: this + }); } - const dataSource = this.dataSources.get(dataSourceName); - const fn = new AppSyncFunctionConfiguration(stack || this, name, { - api: this, - dataSource: dataSource || dataSourceName, - requestMappingTemplate, - responseMappingTemplate, - }); - return fn; - } - - addLambdaFunction( - functionName: string, - functionKey: string, - handlerName: string, - filePath: string, - runtime: Runtime, - layers?: ILayerVersion[], - role?: IRole, - environment?: { [key: string]: string }, - timeout?: Duration, - stack?: Stack, - ): IFunction { - const dummycode = `if __name__ == "__main__":`; // assing dummy code so as to be overriden later - const fn = new Function(stack || this, functionName, { - code: Code.fromInline(dummycode), - handler: handlerName, - runtime, - role, - layers, - environment, - timeout, - }); - fn.addLayers(); - const functionCode = new S3MappingFunctionCode(functionKey, filePath).bind(fn); - (fn.node.defaultChild as CfnFunction).code = { - s3Key: functionCode.s3ObjectKey, - s3Bucket: functionCode.s3BucketName, - }; - return fn; } /** @@ -375,161 +274,6 @@ export class GraphQLApi extends GraphqlApiBase implements GraphQLAPIProvider { return this.authorizationConfig?.defaultAuthorization; } - public addResolver( - typeName: string, - fieldName: string, - requestMappingTemplate: MappingTemplateProvider, - responseMappingTemplate: MappingTemplateProvider, - dataSourceName?: string, - pipelineConfig?: string[], - stack?: Stack, - ) { - if (dataSourceName && !Token.isUnresolved(dataSourceName) && !this.dataSources.has(dataSourceName)) { - throw new Error(`DataSource ${dataSourceName} is missing in the API`); - } - - const requestTemplateLocation = requestMappingTemplate.bind(this); - const responseTemplateLocation = responseMappingTemplate.bind(this); - const resolverName = toCamelCase([typeName, fieldName, 'Resolver']); - if (dataSourceName) { - const dataSource = this.dataSources.get(dataSourceName); - - const resolver = new CfnResolver(stack || this, resolverName, { - apiId: this.apiId, - fieldName: fieldName, - typeName: typeName, - kind: 'UNIT', - dataSourceName: dataSource?.ds.attrName || dataSourceName, - ...(requestMappingTemplate instanceof InlineTemplate - ? { requestMappingTemplate: requestTemplateLocation } - : { requestMappingTemplateS3Location: requestTemplateLocation }), - ...(responseMappingTemplate instanceof InlineTemplate - ? { responseMappingTemplate: responseTemplateLocation } - : { responseMappingTemplateS3Location: responseTemplateLocation }), - }); - this.addSchemaDependency(resolver); - return resolver; - } else if (pipelineConfig) { - const resolver = new CfnResolver(stack || this, resolverName, { - apiId: this.apiId, - fieldName: fieldName, - typeName: typeName, - kind: 'PIPELINE', - ...(requestMappingTemplate instanceof InlineTemplate - ? { requestMappingTemplate: requestTemplateLocation } - : { requestMappingTemplateS3Location: requestTemplateLocation }), - ...(responseMappingTemplate instanceof InlineTemplate - ? { responseMappingTemplate: responseTemplateLocation } - : { responseMappingTemplateS3Location: responseTemplateLocation }), - pipelineConfig: { - functions: pipelineConfig, - }, - }); - this.addSchemaDependency(resolver); - return resolver; - } else { - throw new Error('Resolver needs either dataSourceName or pipelineConfig to be passed'); - } - } - - public hasDataSource(name: string): boolean { - return this.dataSources.has(name); - } - public getDataSource = (name: string): BaseDataSource | void => { - if (this.hasDataSource(name)) { - return this.dataSources.get(name); - } - }; - - /** - * - * @param id The data source's id - * @param options optional configuration for data source - * @param stack Stack to which this datasource needs to mapped to - */ - protected doAddNoneDataSource(id: string, options?: DataSourceOptions, stack?: Stack): NoneDataSource { - return new NoneDataSource(stack ?? 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 - * @param stack Stack to which this datasource needs to mapped to - */ - protected doAddDynamoDbDataSource(id: string, table: ITable, options?: DataSourceOptions, stack?: Stack): DynamoDbDataSource { - return new DynamoDbDataSource(stack ?? 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 - * @param stack Stack to which the http datasource needs to be created in - */ - protected doAddHttpDataSource(id: string, endpoint: string, options?: HttpDataSourceOptions, stack?: Stack): HttpDataSource { - return new HttpDataSource(stack ?? this, id, { - api: this, - endpoint, - name: options?.name, - description: options?.description, - authorizationConfig: options?.authorizationConfig, - }); - } - - /** - * add a new elasticsearch data source to this API - * - * @param id The data source's id - * @param endpoint The elasticsearch endpoint - * @param region The elasticsearch datasource region - * @param options The optional configuration for this data source - * @param stack Stack to which the elasticsearch datasource needs to be created in - */ - protected doAddElasticSearchDataSource( - id: string, - endpoint: string, - region: string, - options?: ElasticSearchDataSourceOptions, - stack?: Stack, - ): ElasticsearchDataSource { - return new ElasticsearchDataSource(stack ?? this, id, { - api: this, - name: options?.name, - endpoint, - region, - serviceRole: options?.serviceRole, - }); - } - - /** - * 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 - */ - protected doAddLambdaDataSource(id: string, lambdaFunction: IFunction, options?: DataSourceOptions, stack?: Stack): LambdaDataSource { - return new LambdaDataSource(stack || this, id, { - api: this, - lambdaFunction, - name: options?.name, - description: options?.description, - }); - } private validateAuthorizationProps(modes: AuthorizationMode[]) { modes.forEach(mode => { if (mode.authorizationType === AuthorizationType.OIDC && !mode.openIdConnectConfig) { diff --git a/packages/amplify-graphql-transformer-core/src/transform-host.ts b/packages/amplify-graphql-transformer-core/src/transform-host.ts new file mode 100644 index 00000000000..f9347bc701c --- /dev/null +++ b/packages/amplify-graphql-transformer-core/src/transform-host.ts @@ -0,0 +1,301 @@ +import { TransformHostProvider } from '@aws-amplify/graphql-transformer-interfaces'; +import { + ElasticSearchDataSourceOptions, + MappingTemplateProvider +} from '@aws-amplify/graphql-transformer-interfaces/lib/graphql-api-provider'; +import { Duration, Stack, Token } from '@aws-cdk/core'; +import { ElasticsearchDataSource } from './cdk-compat/elasticsearch-datasource'; +import { + BaseDataSource, CfnResolver, + DataSourceOptions, + DynamoDbDataSource, + HttpDataSource, + HttpDataSourceOptions, + LambdaDataSource, + NoneDataSource +} from '@aws-cdk/aws-appsync'; +import { ITable } from '@aws-cdk/aws-dynamodb'; +import { CfnFunction, Code, Function, IFunction, ILayerVersion, Runtime } from '@aws-cdk/aws-lambda'; +import { AppSyncFunctionConfiguration } from './appsync-function'; +import { IRole } from '@aws-cdk/aws-iam'; +import { InlineTemplate, S3MappingFunctionCode } from './cdk-compat/template-asset'; +import { toCamelCase } from 'graphql-transformer-common'; +import { GraphQLApi } from './graphql-api'; + + +export interface DefaultTransformHostOptions { + readonly api: GraphQLApi; +} + + +export class DefaultTransformHost implements TransformHostProvider { + private dataSources: Map = new Map() + private api: GraphQLApi; + + public constructor(options: DefaultTransformHostOptions) { + this.api = options.api; + } + + public setAPI(api: GraphQLApi): void { + this.api = api; + } + + public hasDataSource(name: string): boolean { + return this.dataSources.has(name); + } + public getDataSource = (name: string): BaseDataSource | void => { + if (this.hasDataSource(name)) { + return this.dataSources.get(name); + } + }; + + addElasticSearchDataSource( + name: string, + awsRegion: string, + endpoint: string, + options?: ElasticSearchDataSourceOptions, + stack?: Stack, + ): ElasticsearchDataSource { + if (this.dataSources.has(name)) { + throw new Error(`DataSource ${name} already exists in the API`); + } + const data = this.doAddElasticSearchDataSource(name, endpoint, awsRegion, options, stack); + this.dataSources.set(options?.name || name, data); + return data; + } + + public addHttpDataSource(name: string, endpoint: string, options?: DataSourceOptions, stack?: Stack): HttpDataSource { + if (this.dataSources.has(name)) { + throw new Error(`DataSource ${name} already exists in the API`); + } + const dataSource = this.doAddHttpDataSource(name, endpoint, options, stack); + this.dataSources.set(name, dataSource); + return dataSource; + } + + public addDynamoDbDataSource(name: string, table: ITable, options?: DataSourceOptions, stack?: Stack): DynamoDbDataSource { + if (this.dataSources.has(name)) { + throw new Error(`DataSource ${name} already exists in the API`); + } + const dataSource = this.doAddDynamoDbDataSource(name, table, options, stack); + this.dataSources.set(options?.name || name, dataSource); + return dataSource; + } + + public addNoneDataSource(name: string, options?: DataSourceOptions, stack?: Stack): NoneDataSource { + if (this.dataSources.has(name)) { + throw new Error(`DataSource ${name} already exists in the API`); + } + const dataSource = this.doAddNoneDataSource(name, options, stack); + this.dataSources.set(name, dataSource); + return dataSource; + } + + public addLambdaDataSource(name: string, lambdaFunction: IFunction, options?: DataSourceOptions, stack?: Stack): LambdaDataSource { + if (!Token.isUnresolved(name) && this.dataSources.has(name)) { + throw new Error(`DataSource ${name} already exists in the API`); + } + const dataSource = this.doAddLambdaDataSource(name, lambdaFunction, options, stack); + this.dataSources.set(name, dataSource); + return dataSource; + } + + public addAppSyncFunction( + name: string, + requestMappingTemplate: MappingTemplateProvider, + responseMappingTemplate: MappingTemplateProvider, + dataSourceName: string, + stack?: Stack, + ): AppSyncFunctionConfiguration { + if (dataSourceName && !Token.isUnresolved(dataSourceName) && !this.dataSources.has(dataSourceName)) { + throw new Error(`DataSource ${dataSourceName} is missing in the API`); + } + + const dataSource = this.dataSources.get(dataSourceName); + const fn = new AppSyncFunctionConfiguration(stack || this.api, name, { + api: this.api, + dataSource: dataSource || dataSourceName, + requestMappingTemplate, + responseMappingTemplate, + }); + return fn; + } + + public addResolver( + typeName: string, + fieldName: string, + requestMappingTemplate: MappingTemplateProvider, + responseMappingTemplate: MappingTemplateProvider, + dataSourceName?: string, + pipelineConfig?: string[], + stack?: Stack, + ) { + if (dataSourceName && !Token.isUnresolved(dataSourceName) && !this.dataSources.has(dataSourceName)) { + throw new Error(`DataSource ${dataSourceName} is missing in the API`); + } + + const requestTemplateLocation = requestMappingTemplate.bind(this.api); + const responseTemplateLocation = responseMappingTemplate.bind(this.api); + const resolverName = toCamelCase([typeName, fieldName, 'Resolver']); + if (dataSourceName) { + const dataSource = this.dataSources.get(dataSourceName); + + const resolver = new CfnResolver(stack || this.api, resolverName, { + apiId: this.api.apiId, + fieldName: fieldName, + typeName: typeName, + kind: 'UNIT', + dataSourceName: dataSource?.ds.attrName || dataSourceName, + ...(requestMappingTemplate instanceof InlineTemplate + ? { requestMappingTemplate: requestTemplateLocation } + : { requestMappingTemplateS3Location: requestTemplateLocation }), + ...(responseMappingTemplate instanceof InlineTemplate + ? { responseMappingTemplate: responseTemplateLocation } + : { responseMappingTemplateS3Location: responseTemplateLocation }), + }); + this.api.addSchemaDependency(resolver); + return resolver; + } else if (pipelineConfig) { + const resolver = new CfnResolver(stack || this.api, resolverName, { + apiId: this.api.apiId, + fieldName: fieldName, + typeName: typeName, + kind: 'PIPELINE', + ...(requestMappingTemplate instanceof InlineTemplate + ? { requestMappingTemplate: requestTemplateLocation } + : { requestMappingTemplateS3Location: requestTemplateLocation }), + ...(responseMappingTemplate instanceof InlineTemplate + ? { responseMappingTemplate: responseTemplateLocation } + : { responseMappingTemplateS3Location: responseTemplateLocation }), + pipelineConfig: { + functions: pipelineConfig, + }, + }); + this.api.addSchemaDependency(resolver); + return resolver; + } else { + throw new Error('Resolver needs either dataSourceName or pipelineConfig to be passed'); + } + } + + addLambdaFunction( + functionName: string, + functionKey: string, + handlerName: string, + filePath: string, + runtime: Runtime, + layers?: ILayerVersion[], + role?: IRole, + environment?: { [key: string]: string }, + timeout?: Duration, + stack?: Stack, + ): IFunction { + const dummycode = `if __name__ == "__main__":`; // assing dummy code so as to be overriden later + const fn = new Function(stack || this.api, functionName, { + code: Code.fromInline(dummycode), + handler: handlerName, + runtime, + role, + layers, + environment, + timeout, + }); + fn.addLayers(); + const functionCode = new S3MappingFunctionCode(functionKey, filePath).bind(fn); + (fn.node.defaultChild as CfnFunction).code = { + s3Key: functionCode.s3ObjectKey, + s3Bucket: functionCode.s3BucketName, + }; + return fn; + } + + /** + * + * @param id The data source's id + * @param options optional configuration for data source + * @param stack Stack to which this datasource needs to mapped to + */ + protected doAddNoneDataSource(id: string, options?: DataSourceOptions, stack?: Stack): NoneDataSource { + return new NoneDataSource(stack ?? this.api, id, { + api: this.api, + 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 + * @param stack Stack to which this datasource needs to mapped to + */ + protected doAddDynamoDbDataSource(id: string, table: ITable, options?: DataSourceOptions, stack?: Stack): DynamoDbDataSource { + return new DynamoDbDataSource(stack ?? this.api, id, { + api: this.api, + 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 + * @param stack Stack to which the http datasource needs to be created in + */ + protected doAddHttpDataSource(id: string, endpoint: string, options?: HttpDataSourceOptions, stack?: Stack): HttpDataSource { + return new HttpDataSource(stack ?? this.api, id, { + api: this.api, + endpoint, + name: options?.name, + description: options?.description, + authorizationConfig: options?.authorizationConfig, + }); + } + + /** + * add a new elasticsearch data source to this API + * + * @param id The data source's id + * @param endpoint The elasticsearch endpoint + * @param region The elasticsearch datasource region + * @param options The optional configuration for this data source + * @param stack Stack to which the elasticsearch datasource needs to be created in + */ + protected doAddElasticSearchDataSource( + id: string, + endpoint: string, + region: string, + options?: ElasticSearchDataSourceOptions, + stack?: Stack, + ): ElasticsearchDataSource { + return new ElasticsearchDataSource(stack ?? this.api, id, { + api: this.api, + name: options?.name, + endpoint, + region, + serviceRole: options?.serviceRole, + }); + } + + /** + * 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 + */ + protected doAddLambdaDataSource(id: string, lambdaFunction: IFunction, options?: DataSourceOptions, stack?: Stack): LambdaDataSource { + return new LambdaDataSource(stack || this.api, id, { + api: this.api, + lambdaFunction, + name: options?.name, + description: options?.description, + }); + } +} diff --git a/packages/amplify-graphql-transformer-core/src/transformation/transform.ts b/packages/amplify-graphql-transformer-core/src/transformation/transform.ts index 3442ce2d12c..151d942f21d 100644 --- a/packages/amplify-graphql-transformer-core/src/transformation/transform.ts +++ b/packages/amplify-graphql-transformer-core/src/transformation/transform.ts @@ -1,5 +1,5 @@ /* eslint-disable no-new */ -import { FeatureFlagProvider, GraphQLAPIProvider, TransformerPluginProvider } from '@aws-amplify/graphql-transformer-interfaces'; +import { FeatureFlagProvider, GraphQLAPIProvider, TransformerPluginProvider, TransformHostProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { AuthorizationMode, AuthorizationType } from '@aws-cdk/aws-appsync'; import { App, Aws, CfnOutput, Fn } from '@aws-cdk/core'; import assert from 'assert'; @@ -62,6 +62,7 @@ export interface GraphQLTransformOptions { readonly buildParameters?: Record; readonly stacks?: Record; readonly featureFlags?: FeatureFlagProvider; + readonly host?: TransformHostProvider; } export type StackMapping = { [resourceId: string]: string }; export class GraphQLTransform { @@ -246,6 +247,7 @@ export class GraphQLTransform { const api = new GraphQLApi(rootStack, 'GraphQLAPI', { name: `${apiName}-${envName.valueAsString}`, authorizationConfig, + host: this.options.host }); const authModes = [authorizationConfig.defaultAuthorization, ...(authorizationConfig.additionalAuthorizationModes || [])].map( mode => mode?.authorizationType, diff --git a/packages/amplify-graphql-transformer-core/src/transformer-context/resolver.ts b/packages/amplify-graphql-transformer-core/src/transformer-context/resolver.ts index c2fbb69b152..7f24e383b86 100644 --- a/packages/amplify-graphql-transformer-core/src/transformer-context/resolver.ts +++ b/packages/amplify-graphql-transformer-core/src/transformer-context/resolver.ts @@ -162,7 +162,7 @@ export class TransformerResolver implements TransformerResolverProvider { // substitue template name values [this.requestMappingTemplate, this.requestMappingTemplate].map(template => this.substitueSlotInfo(template, 'main', 0)); - const dataSourceProviderFn = api.addAppSyncFunction( + const dataSourceProviderFn = api.host.addAppSyncFunction( toPascalCase([this.typeName, this.fieldName, 'DataResolverFn']), this.requestMappingTemplate, this.responseMappingTemplate, @@ -213,7 +213,7 @@ export class TransformerResolver implements TransformerResolverProvider { throw new Error('Unknow DataSource type'); } } - api.addResolver( + api.host.addResolver( this.typeName, this.fieldName, MappingTemplate.inlineTemplateFromString( @@ -249,7 +249,7 @@ export class TransformerResolver implements TransformerResolverProvider { this.substitueSlotInfo(requestMappingTemplate, slotName, index); // eslint-disable-next-line no-unused-expressions responseMappingTemplate && this.substitueSlotInfo(responseMappingTemplate, slotName, index); - const fn = api.addAppSyncFunction( + const fn = api.host.addAppSyncFunction( name, requestMappingTemplate, responseMappingTemplate || MappingTemplate.inlineTemplateFromString('$util.toJson({})'), @@ -270,8 +270,8 @@ export class TransformerResolver implements TransformerResolverProvider { } private ensureNoneDataSource(api: GraphQLAPIProvider) { - if (!api.hasDataSource(NONE_DATA_SOURCE_NAME)) { - api.addNoneDataSource(NONE_DATA_SOURCE_NAME, { + if (!api.host.hasDataSource(NONE_DATA_SOURCE_NAME)) { + api.host.addNoneDataSource(NONE_DATA_SOURCE_NAME, { name: NONE_DATA_SOURCE_NAME, description: 'None Data Source for Pipeline functions', }); diff --git a/packages/amplify-graphql-transformer-interfaces/src/graphql-api-provider.ts b/packages/amplify-graphql-transformer-interfaces/src/graphql-api-provider.ts index 0fb39bbf337..168d818afb4 100644 --- a/packages/amplify-graphql-transformer-interfaces/src/graphql-api-provider.ts +++ b/packages/amplify-graphql-transformer-interfaces/src/graphql-api-provider.ts @@ -1,8 +1,6 @@ -import { NoneDataSource, HttpDataSource, DynamoDbDataSource, LambdaDataSource, BaseDataSource, CfnResolver } from '@aws-cdk/aws-appsync'; -import { IFunction, ILayerVersion, Runtime } from '@aws-cdk/aws-lambda'; -import { ITable } from '@aws-cdk/aws-dynamodb'; -import { CfnResource, Construct, Duration, IAsset, IConstruct, Stack } from '@aws-cdk/core'; +import { CfnResource, Construct, IAsset, IConstruct } from '@aws-cdk/core'; import { Grant, IGrantable, IRole } from '@aws-cdk/aws-iam'; +import {TransformHostProvider} from './transform-host-provider'; export interface AppSyncFunctionConfigurationProvider extends IConstruct { readonly arn: string; @@ -52,50 +50,8 @@ export type MappingTemplateProvider = InlineMappingTemplateProvider | S3MappingT export interface GraphQLAPIProvider { readonly apiId: string; - addHttpDataSource(name: string, endpoint: string, options?: DataSourceOptions, stack?: Stack): HttpDataSource; - addDynamoDbDataSource(name: string, table: ITable, options?: DataSourceOptions, stack?: Stack): DynamoDbDataSource; - addNoneDataSource(name: string, options?: DataSourceOptions, stack?: Stack): NoneDataSource; - addLambdaDataSource(name: string, lambdaFunction: IFunction, options?: DataSourceOptions, stack?: Stack): LambdaDataSource; - addElasticSearchDataSource( - name: string, - endpoint: string, - region: string, - options?: ElasticSearchDataSourceOptions, - stack?: Stack, - ): BaseDataSource; - addAppSyncFunction: ( - name: string, - requestMappingTemplate: MappingTemplateProvider, - responseMappingTemplate: MappingTemplateProvider, - dataSourceName: string, - stack?: Stack, - ) => AppSyncFunctionConfigurationProvider; + readonly host: TransformHostProvider; - addResolver: ( - typeName: string, - fieldName: string, - requestMappingTemplate: MappingTemplateProvider, - responseMappingTemplate: MappingTemplateProvider, - dataSourceName?: string, - pipelineConfig?: string[], - stack?: Stack, - ) => CfnResolver; - - addLambdaFunction: ( - functionName: string, - functionKey: string, - handlerName: string, - filePath: string, - runtime: Runtime, - layers?: ILayerVersion[], - role?: IRole, - environment?: { [key: string]: string }, - timeout?: Duration, - stack?: Stack, - ) => IFunction; - - getDataSource: (name: string) => BaseDataSource | void; - hasDataSource: (name: string) => boolean; // getDefaultAuthorization(): Readonly; // getAdditionalAuthorizationModes(): Readonly; addToSchema(addition: string): void; diff --git a/packages/amplify-graphql-transformer-interfaces/src/index.ts b/packages/amplify-graphql-transformer-interfaces/src/index.ts index e9ca2949819..191d18f00f2 100644 --- a/packages/amplify-graphql-transformer-interfaces/src/index.ts +++ b/packages/amplify-graphql-transformer-interfaces/src/index.ts @@ -37,3 +37,7 @@ export { APIIAMResourceProvider, TemplateType as MappingTemplateType, } from './graphql-api-provider'; + +export { + TransformHostProvider +} from './transform-host-provider'; diff --git a/packages/amplify-graphql-transformer-interfaces/src/transform-host-provider.ts b/packages/amplify-graphql-transformer-interfaces/src/transform-host-provider.ts new file mode 100644 index 00000000000..1bf91663f1d --- /dev/null +++ b/packages/amplify-graphql-transformer-interfaces/src/transform-host-provider.ts @@ -0,0 +1,70 @@ +import { Duration, Stack } from '@aws-cdk/core'; +import { + BaseDataSource, + CfnResolver, + DynamoDbDataSource, + GraphqlApiBase, + HttpDataSource, + LambdaDataSource, + NoneDataSource +} from '@aws-cdk/aws-appsync'; +import { ITable } from '@aws-cdk/aws-dynamodb'; +import { IFunction, ILayerVersion, Runtime } from '@aws-cdk/aws-lambda'; +import { + AppSyncFunctionConfigurationProvider, + DataSourceOptions, + ElasticSearchDataSourceOptions, + MappingTemplateProvider +} from './graphql-api-provider'; +import { IRole } from '@aws-cdk/aws-iam'; + + +export interface TransformHostProvider { + setAPI(api: GraphqlApiBase): void; + + addHttpDataSource(name: string, endpoint: string, options?: DataSourceOptions, stack?: Stack): HttpDataSource; + addDynamoDbDataSource(name: string, table: ITable, options?: DataSourceOptions, stack?: Stack): DynamoDbDataSource; + addNoneDataSource(name: string, options?: DataSourceOptions, stack?: Stack): NoneDataSource; + addLambdaDataSource(name: string, lambdaFunction: IFunction, options?: DataSourceOptions, stack?: Stack): LambdaDataSource; + addElasticSearchDataSource( + name: string, + endpoint: string, + region: string, + options?: ElasticSearchDataSourceOptions, + stack?: Stack, + ): BaseDataSource; + + addAppSyncFunction: ( + name: string, + requestMappingTemplate: MappingTemplateProvider, + responseMappingTemplate: MappingTemplateProvider, + dataSourceName: string, + stack?: Stack, + ) => AppSyncFunctionConfigurationProvider; + + addResolver: ( + typeName: string, + fieldName: string, + requestMappingTemplate: MappingTemplateProvider, + responseMappingTemplate: MappingTemplateProvider, + dataSourceName?: string, + pipelineConfig?: string[], + stack?: Stack, + ) => CfnResolver; + + addLambdaFunction: ( + functionName: string, + functionKey: string, + handlerName: string, + filePath: string, + runtime: Runtime, + layers?: ILayerVersion[], + role?: IRole, + environment?: { [key: string]: string }, + timeout?: Duration, + stack?: Stack, + ) => IFunction; + + getDataSource: (name: string) => BaseDataSource | void; + hasDataSource: (name: string) => boolean; +}