UNPKG

@aws-amplify/graphql-api-construct

Version:

AppSync GraphQL Api Construct using Amplify GraphQL Transformer.

440 lines (393 loc) 18.2 kB
import * as path from 'path'; import { Construct } from 'constructs'; import { ExecuteTransformConfig, executeTransform } from '@aws-amplify/graphql-transformer'; import { NestedStack, Stack, FeatureFlags } from 'aws-cdk-lib'; import { AttributionMetadataStorage, StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; import { graphqlOutputKey } from '@aws-amplify/backend-output-schemas'; import type { GraphqlOutput, AwsAppsyncAuthenticationType } from '@aws-amplify/backend-output-schemas'; import { AppsyncFunction, DataSourceOptions, DynamoDbDataSource, ElasticsearchDataSource, EventBridgeDataSource, ExtendedResolverProps, HttpDataSource, HttpDataSourceOptions, LambdaDataSource, NoneDataSource, OpenSearchDataSource, RdsDataSource, Resolver, } from 'aws-cdk-lib/aws-appsync'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { IDomain } from 'aws-cdk-lib/aws-elasticsearch'; import { IDomain as IOpenSearchDomain } from 'aws-cdk-lib/aws-opensearchservice'; import { IEventBus } from 'aws-cdk-lib/aws-events'; import { IFunction } from 'aws-cdk-lib/aws-lambda'; import { IServerlessCluster } from 'aws-cdk-lib/aws-rds'; import { ISecret } from 'aws-cdk-lib/aws-secretsmanager'; import { parseUserDefinedSlots, validateFunctionSlots, separateSlots } from './internal/user-defined-slots'; import type { AmplifyGraphqlApiResources, AmplifyGraphqlApiProps, FunctionSlot, IBackendOutputStorageStrategy, AddFunctionProps, DataStoreConfiguration, } from './types'; import { convertAuthorizationModesToTransformerAuthConfig, convertToResolverConfig, defaultTranslationBehavior, AssetProvider, getGeneratedResources, getGeneratedFunctionSlots, CodegenAssets, getAdditionalAuthenticationTypes, validateAuthorizationModes, } from './internal'; import { getStackForScope, walkAndProcessNodes } from './internal/construct-tree'; import { getDataSourceStrategiesProvider } from './internal/data-source-config'; import { getMetadataDataSources, getMetadataAuthorizationModes, getMetadataCustomOperations } from './internal/metadata'; import { BackendOutputStorageStrategy, BackendOutputEntry } from '@aws-amplify/plugin-types'; /** * L3 Construct which invokes the Amplify Transformer Pattern over an input Graphql Schema. * * This can be used to quickly define appsync apis which support full CRUD+List and Subscriptions, relationships, * auth, search over data, the ability to inject custom business logic and query/mutation operations, and connect to ML services. * * For more information, refer to the docs links below: * Data Modeling - https://docs.amplify.aws/cli/graphql/data-modeling/ * Authorization - https://docs.amplify.aws/cli/graphql/authorization-rules/ * Custom Business Logic - https://docs.amplify.aws/cli/graphql/custom-business-logic/ * Search - https://docs.amplify.aws/cli/graphql/search-and-result-aggregations/ * ML Services - https://docs.amplify.aws/cli/graphql/connect-to-machine-learning-services/ * * For a full reference of the supported custom graphql directives - https://docs.amplify.aws/cli/graphql/directives-reference/ * * The output of this construct is a mapping of L2 or L1 resources generated by the transformer, which generally follow the access pattern * * ```typescript * const api = new AmplifyGraphQlApi(this, 'api', { <params> }); * // Access L2 resources under `.resources` * api.resources.tables["Todo"].tableArn; * * // Access L1 resources under `.resources.cfnResources` * api.resources.cfnResources.cfnGraphqlApi.xrayEnabled = true; * Object.values(api.resources.cfnResources.cfnTables).forEach(table => { * table.pointInTimeRecoverySpecification = { pointInTimeRecoveryEnabled: false }; * }); * ``` * `resources.<ResourceType>.<ResourceName>` - you can then perform any CDK action on these resulting resoureces. */ export class AmplifyGraphqlApi extends Construct { /** * Generated L1 and L2 CDK resources. */ public readonly resources: AmplifyGraphqlApiResources; /** * Reference to parent stack of data construct */ public readonly stack: Stack; /** * Generated assets required for codegen steps. Persisted in order to render as part of the output strategy. */ private readonly codegenAssets: CodegenAssets; /** * Resolvers generated by the transform process, persisted on the side in order to facilitate pulling a manifest * for the purposes of inspecting and producing overrides. */ public readonly generatedFunctionSlots: FunctionSlot[]; /** * Graphql URL For the generated API. May be a CDK Token. */ public readonly graphqlUrl: string; /** * Realtime URL For the generated API. May be a CDK Token. */ public readonly realtimeUrl: string; /** * Generated Api Key if generated. May be a CDK Token. */ public readonly apiKey: string | undefined; /** * Generated Api Id. May be a CDK Token. */ public readonly apiId: string; /** * DataStore conflict resolution setting */ private readonly dataStoreConfiguration: DataStoreConfiguration | undefined; /** * Be very careful editing this value. This is the string that is used to identify graphql stacks in BI metrics */ private readonly stackType = 'api-AppSync'; /** * New AmplifyGraphqlApi construct, this will create an appsync api with authorization, a schema, and all necessary resolvers, functions, * and datasources. * @param scope the scope to create this construct within. * @param id the id to use for this api. * @param props the properties used to configure the generated api. */ constructor(scope: Construct, id: string, props: AmplifyGraphqlApiProps) { super(scope, id); this.stack = Stack.of(scope); // Fall back to default true if no feature flag is provided, otherwise honor the feature flag value provided this.node.setContext( '@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections', FeatureFlags.of(this).isEnabled('@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections') ?? true, ); validateNoOtherAmplifyGraphqlApiInStack(this); const { definition, authorizationModes, conflictResolution, functionSlots, transformerPlugins, predictionsBucket, stackMappings, translationBehavior, functionNameMap, outputStorageStrategy, dataStoreConfiguration, logging, } = props; if (conflictResolution && dataStoreConfiguration) { throw new Error( 'conflictResolution is deprecated. conflictResolution and dataStoreConfiguration cannot be used together. Please use dataStoreConfiguration.', ); } this.dataStoreConfiguration = dataStoreConfiguration || conflictResolution; const attributionMetadata = { dataSources: getMetadataDataSources(definition), authorizationModes: getMetadataAuthorizationModes(authorizationModes), customOperations: getMetadataCustomOperations(definition), }; new AttributionMetadataStorage().storeAttributionMetadata( Stack.of(scope), this.stackType, path.join(__dirname, '..', 'package.json'), attributionMetadata, ); validateAuthorizationModes(authorizationModes); const { authConfig, authSynthParameters } = convertAuthorizationModesToTransformerAuthConfig(authorizationModes); validateFunctionSlots(functionSlots ?? []); const separatedFunctionSlots = separateSlots([...(functionSlots ?? []), ...definition.functionSlots]); // Allow amplifyEnvironmentName to be retrieve from context, and use value 'NONE' if no value can be found. // amplifyEnvironmentName is required for logical id suffixing, as well as Exports from the nested stacks. // Allow export so customers can reuse the env in their own references downstream. const amplifyEnvironmentName = this.node.tryGetContext('amplifyEnvironmentName') ?? 'NONE'; if (amplifyEnvironmentName.length > 8) { throw new Error(`or cdk --context env must have a length <= 8, found ${amplifyEnvironmentName}`); } const assetProvider = new AssetProvider(this); const transformParameters = { ...defaultTranslationBehavior, ...(translationBehavior ?? {}), allowGen1Patterns: false, }; const executeTransformConfig: ExecuteTransformConfig = { scope: this, nestedStackProvider: { provide: (nestedStackScope: Construct, name: string) => new NestedStack(nestedStackScope, name), }, assetProvider, synthParameters: { amplifyEnvironmentName: amplifyEnvironmentName, apiName: props.apiName ?? id, ...authSynthParameters, provisionHotswapFriendlyResources: translationBehavior?._provisionHotswapFriendlyResources, }, schema: definition.schema, userDefinedSlots: parseUserDefinedSlots(separatedFunctionSlots), transformersFactoryArgs: { customTransformers: transformerPlugins ?? [], ...(predictionsBucket ? { storageConfig: { bucketName: predictionsBucket.bucketName } } : {}), functionNameMap: { ...definition.referencedLambdaFunctions, ...functionNameMap, }, outputStorageStrategy: outputStorageStrategy as BackendOutputStorageStrategy<BackendOutputEntry>, }, authConfig, stackMapping: stackMappings ?? {}, resolverConfig: this.dataStoreConfiguration ? convertToResolverConfig(this.dataStoreConfiguration) : undefined, transformParameters, // CDK construct uses a custom resource. We'll define this explicitly here to remind ourselves that this value is unused in the CDK // construct flow rdsLayerMapping: undefined, rdsSnsTopicMapping: undefined, ...getDataSourceStrategiesProvider(definition), logging, }; executeTransform(executeTransformConfig); this.codegenAssets = new CodegenAssets(this, 'AmplifyCodegenAssets', { modelSchema: definition.schema }); this.resources = getGeneratedResources(this); this.generatedFunctionSlots = getGeneratedFunctionSlots(assetProvider.resolverAssets); this.storeOutput(outputStorageStrategy); this.apiId = this.resources.cfnResources.cfnGraphqlApi.attrApiId; this.graphqlUrl = this.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl; this.realtimeUrl = this.resources.cfnResources.cfnGraphqlApi.attrRealtimeUrl; this.apiKey = this.resources.cfnResources.cfnApiKey?.attrApiKey; } /** * Stores graphql api output to be used for client config generation * @param outputStorageStrategy Strategy to store construct outputs. If no strategy is provided a default strategy will be used. */ private storeOutput( outputStorageStrategy: IBackendOutputStorageStrategy = new StackMetadataBackendOutputStorageStrategy(Stack.of(this)), ): void { const stack = Stack.of(this); const output: GraphqlOutput = { version: '1', payload: { awsAppsyncApiId: this.resources.cfnResources.cfnGraphqlApi.attrApiId, awsAppsyncApiEndpoint: this.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl, awsAppsyncAuthenticationType: this.resources.cfnResources.cfnGraphqlApi.authenticationType as AwsAppsyncAuthenticationType, awsAppsyncRegion: stack.region, amplifyApiModelSchemaS3Uri: this.codegenAssets.modelSchemaS3Uri, }, }; if (this.resources.cfnResources.cfnApiKey) { output.payload.awsAppsyncApiKey = this.resources.cfnResources.cfnApiKey.attrApiKey; } const additionalAuthTypes = getAdditionalAuthenticationTypes(this.resources.cfnResources.cfnGraphqlApi); if (additionalAuthTypes) { output.payload.awsAppsyncAdditionalAuthenticationTypes = additionalAuthTypes; } if (this.dataStoreConfiguration?.project?.handlerType) { output.payload.awsAppsyncConflictResolutionMode = this.dataStoreConfiguration?.project?.handlerType; } outputStorageStrategy.addBackendOutputEntry(graphqlOutputKey, output); } /** * The following are proxy methods to the L2 IGraphqlApi interface, to facilitate easier use of the L3 without needing * to access the underlying resources. */ /** * Add a new DynamoDB data source to this API. This is a proxy method to the L2 GraphqlApi Construct. * @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. * @returns the generated data source. */ public addDynamoDbDataSource(id: string, table: ITable, options?: DataSourceOptions): DynamoDbDataSource { return this.resources.graphqlApi.addDynamoDbDataSource(id, table, options); } /** * Add a new elasticsearch data source to this API. This is a proxy method to the L2 GraphqlApi Construct. * @deprecated use `addOpenSearchDataSource` * @param id The data source's id. * @param domain The elasticsearch domain for this data source. * @param options The optional configuration for this data source. * @returns the generated data source. */ public addElasticsearchDataSource(id: string, domain: IDomain, options?: DataSourceOptions): ElasticsearchDataSource { return this.resources.graphqlApi.addElasticsearchDataSource(id, domain, options); } /** * Add an EventBridge data source to this api. This is a proxy method to the L2 GraphqlApi Construct. * @param id The data source's id. * @param eventBus The EventBridge EventBus on which to put events. * @param options The optional configuration for this data source. */ public addEventBridgeDataSource(id: string, eventBus: IEventBus, options?: DataSourceOptions): EventBridgeDataSource { return this.resources.graphqlApi.addEventBridgeDataSource(id, eventBus, options); } /** * Add a new http data source to this API. This is a proxy method to the L2 GraphqlApi Construct. * @param id The data source's id. * @param endpoint The http endpoint. * @param options The optional configuration for this data source. * @returns the generated data source. */ public addHttpDataSource(id: string, endpoint: string, options?: HttpDataSourceOptions): HttpDataSource { return this.resources.graphqlApi.addHttpDataSource(id, endpoint, options); } /** * Add a new Lambda data source to this API. This is a proxy method to the L2 GraphqlApi Construct. * @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. * @returns the generated data source. */ public addLambdaDataSource(id: string, lambdaFunction: IFunction, options?: DataSourceOptions): LambdaDataSource { return this.resources.graphqlApi.addLambdaDataSource(id, lambdaFunction, options); } /** * Add a new dummy data source to this API. This is a proxy method to the L2 GraphqlApi Construct. * 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. * @returns the generated data source. */ public addNoneDataSource(id: string, options?: DataSourceOptions): NoneDataSource { return this.resources.graphqlApi.addNoneDataSource(id, options); } /** * dd a new OpenSearch data source to this API. This is a proxy method to the L2 GraphqlApi Construct. * @param id The data source's id. * @param domain The OpenSearch domain for this data source. * @param options The optional configuration for this data source. * @returns the generated data source. */ public addOpenSearchDataSource(id: string, domain: IOpenSearchDomain, options?: DataSourceOptions): OpenSearchDataSource { return this.resources.graphqlApi.addOpenSearchDataSource(id, domain, options); } /** * Add a new Rds data source to this API. This is a proxy method to the L2 GraphqlApi Construct. * @param id The data source's id. * @param serverlessCluster The serverless cluster to interact with this data source. * @param secretStore The secret store that contains the username and password for the serverless cluster. * @param databaseName The optional name of the database to use within the cluster. * @param options The optional configuration for this data source. * @returns the generated data source. */ public addRdsDataSource( id: string, serverlessCluster: IServerlessCluster, secretStore: ISecret, databaseName?: string, options?: DataSourceOptions, ): RdsDataSource { return this.resources.graphqlApi.addRdsDataSource(id, serverlessCluster, secretStore, databaseName, options); } /** * Add a resolver to the api. This is a proxy method to the L2 GraphqlApi Construct. * @param id The resolver's id. * @param props the resolver properties. * @returns the generated resolver. */ public addResolver(id: string, props: ExtendedResolverProps): Resolver { return this.resources.graphqlApi.createResolver(id, props); } /** * Add an appsync function to the api. * @param id the function's id. * @returns the generated appsync function. */ public addFunction(id: string, props: AddFunctionProps): AppsyncFunction { return new AppsyncFunction(this, id, { api: this.resources.graphqlApi, ...props, }); } } /** * Given the provided scope, walk the node tree, and throw an exception if any other AmplifyGraphqlApi constructs * are found in the stack. * @param scope the scope this construct is created in. */ const validateNoOtherAmplifyGraphqlApiInStack = (scope: Construct): void => { const rootStack = getStackForScope(scope, false); let wasOtherAmplifyGraphlApiFound = false; walkAndProcessNodes(rootStack, (node: Construct) => { if (node instanceof AmplifyGraphqlApi && scope !== node) { wasOtherAmplifyGraphlApiFound = true; } }); if (wasOtherAmplifyGraphlApiFound) { throw new Error('Only one AmplifyGraphqlApi is expected in a stack. Place the AmplifyGraphqlApis in separate nested stacks.'); } };