UNPKG

@aws-amplify/graphql-api-construct

Version:

AppSync GraphQL Api Construct using Amplify GraphQL Transformer.

260 lines (238 loc) 9.64 kB
import { Construct } from 'constructs'; import { AuthorizationType, CfnGraphQLApi, CfnGraphQLSchema, CfnApiKey, CfnResolver, CfnFunctionConfiguration, CfnDataSource, GraphqlApi, GraphqlApiAttributes, Visibility, } from 'aws-cdk-lib/aws-appsync'; import { CfnTable, Table, ITable } from 'aws-cdk-lib/aws-dynamodb'; import { CfnRole, Role } from 'aws-cdk-lib/aws-iam'; import { CfnResource, isResolvableObject, NestedStack } from 'aws-cdk-lib'; import { getResourceName } from '@aws-amplify/graphql-transformer-core'; import { CfnFunction, Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda'; import { AmplifyGraphqlApiResources, FunctionSlot } from '../types'; import { AmplifyDynamoDbTableWrapper } from '../amplify-dynamodb-table-wrapper'; import { walkAndProcessNodes } from './construct-tree'; /** * Check if a resource is implementing table interface * The required properties need to be present in the input * https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.ITable.html#properties * @param table table resource * @returns whether the resource is a ITable or not */ const isITable = (table: any): table is ITable => { return 'env' in table && 'node' in table && 'stack' in table && 'tableArn' in table && 'tableName' in table; }; /** * Everything below here is intended to help us gather the * output values and render out the L1 resources for access. * * This is done by recursing along the construct tree, and classifying the generated resources. * * @param scope root to search for generated resource against * @returns a mapping of L1 and L2 constructs generated by the Graphql Transformer. */ export const getGeneratedResources = (scope: Construct): AmplifyGraphqlApiResources => { let cfnGraphqlApi: CfnGraphQLApi | undefined; let cfnGraphqlSchema: CfnGraphQLSchema | undefined; let cfnApiKey: CfnApiKey | undefined; const cfnResolvers: Record<string, CfnResolver> = {}; const cfnFunctionConfigurations: Record<string, CfnFunctionConfiguration> = {}; const cfnDataSources: Record<string, CfnDataSource> = {}; const tables: Record<string, ITable> = {}; const cfnTables: Record<string, CfnTable> = {}; const amplifyDynamoDbTables: Record<string, AmplifyDynamoDbTableWrapper> = {}; const roles: Record<string, Role> = {}; const cfnRoles: Record<string, CfnRole> = {}; const functions: Record<string, LambdaFunction> = {}; const cfnFunctions: Record<string, CfnFunction> = {}; const additionalCfnResources: Record<string, CfnResource> = {}; const classifyConstruct = (currentScope: Construct): void => { if (currentScope instanceof CfnGraphQLApi) { cfnGraphqlApi = currentScope; return; } if (currentScope instanceof CfnGraphQLSchema) { cfnGraphqlSchema = currentScope; return; } if (currentScope instanceof CfnApiKey) { cfnApiKey = currentScope; return; } // Retrieve reference name for indexed resources, and bail if none is found. const resourceName = getResourceName(currentScope); if (!resourceName) return; if (currentScope instanceof CfnDataSource) { cfnDataSources[resourceName] = currentScope; return; } if (currentScope instanceof CfnResolver) { cfnResolvers[resourceName] = currentScope; return; } if (currentScope instanceof CfnFunctionConfiguration) { cfnFunctionConfigurations[resourceName] = currentScope; return; } if (currentScope instanceof Table || isITable(currentScope)) { tables[resourceName] = currentScope; return; } if (currentScope instanceof CfnTable) { cfnTables[resourceName] = currentScope; return; } if (AmplifyDynamoDbTableWrapper.isAmplifyDynamoDbTableResource(currentScope)) { amplifyDynamoDbTables[resourceName] = new AmplifyDynamoDbTableWrapper(currentScope); return; } if (currentScope instanceof Role) { roles[resourceName] = currentScope; return; } if (currentScope instanceof CfnRole) { cfnRoles[resourceName] = currentScope; return; } if (currentScope instanceof LambdaFunction) { functions[resourceName] = currentScope; return; } if (currentScope instanceof CfnFunction) { cfnFunctions[resourceName] = currentScope; return; } if (currentScope instanceof CfnResource) { additionalCfnResources[resourceName] = currentScope; return; } }; scope.node.children.forEach((child) => walkAndProcessNodes(child, classifyConstruct)); if (!cfnGraphqlApi) { throw new Error('Expected to find AWS::AppSync::GraphQLApi in the generated resource scope.'); } if (!cfnGraphqlSchema) { throw new Error('Expected to find AWS::AppSync::GraphQLSchema in the generated resource scope.'); } const nestedStacks: Record<string, NestedStack> = Object.fromEntries( scope.node.children.filter(NestedStack.isNestedStack).map((nestedStack: NestedStack) => [nestedStack.node.id, nestedStack]), ); const proxiedApiAttributes = graphqlApiAttributesFromCfnGraphQLApi(cfnGraphqlApi); return { graphqlApi: GraphqlApi.fromGraphqlApiAttributes(scope, 'L2GraphqlApi', proxiedApiAttributes), tables, roles, functions, nestedStacks, cfnResources: { cfnGraphqlApi, cfnGraphqlSchema, cfnApiKey, cfnResolvers, cfnFunctionConfigurations, cfnDataSources, cfnTables, amplifyDynamoDbTables, cfnRoles, cfnFunctions, additionalCfnResources, }, }; }; /** * Creates a set of L2 {@link GraphqlApiAttributes} from a CfnGraphqlApi L1 construct. Allows for getGeneratedResources to easily pass * attributes of the CfnGraphqlApi to the `resources` member. Without this the `resources.graphqlApi` member has no properties except for * the API ID. */ const graphqlApiAttributesFromCfnGraphQLApi = (cfnGraphqlApi: CfnGraphQLApi): GraphqlApiAttributes => { const visiblityStruct: { visibility?: Visibility } = {}; if (typeof cfnGraphqlApi.visibility === 'string') { switch (cfnGraphqlApi.visibility) { case 'GLOBAL': visiblityStruct.visibility = Visibility.GLOBAL; break; case 'PRIVATE': visiblityStruct.visibility = Visibility.PRIVATE; break; default: console.warn(`Unsupported AppSync API Visibility setting: ${cfnGraphqlApi.visibility}`); } } const proxiedApiAttributes: GraphqlApiAttributes = { graphqlApiId: cfnGraphqlApi.attrApiId, graphqlApiArn: cfnGraphqlApi.attrArn, graphQLEndpointArn: cfnGraphqlApi.attrGraphQlEndpointArn, ...visiblityStruct, modes: authenticationTypesFromCfnGraphQLApi(cfnGraphqlApi), }; return proxiedApiAttributes; }; const authenticationTypesFromCfnGraphQLApi = (cfnGraphqlApi: CfnGraphQLApi): AuthorizationType[] => { const additionalAuthenticationProviders = cfnGraphqlApi.additionalAuthenticationProviders; if (!additionalAuthenticationProviders) { return []; } // If this is a deploy-time value rather than an array, we can't convert accurately. if (isResolvableObject(additionalAuthenticationProviders)) { return []; } const unfilteredAuthorizationTypes: (AuthorizationType | undefined)[] = additionalAuthenticationProviders .filter( (additionalAuthProvider): additionalAuthProvider is CfnGraphQLApi.AdditionalAuthenticationProviderProperty => !isResolvableObject(additionalAuthProvider), ) .map((provider) => provider.authenticationType) .map(l2AuthorizationTypeFromL1AuthenticationProvider); const authorizationTypes = unfilteredAuthorizationTypes.filter((type): type is AuthorizationType => typeof type !== 'undefined'); const defaultAuthorizationType = l2AuthorizationTypeFromL1AuthenticationProvider(cfnGraphqlApi.authenticationType); if (defaultAuthorizationType) { authorizationTypes.push(defaultAuthorizationType); } return authorizationTypes; }; const l2AuthorizationTypeFromL1AuthenticationProvider = (authenticationType: string): AuthorizationType | undefined => { switch (authenticationType) { case 'API_KEY': return AuthorizationType.API_KEY; case 'AWS_IAM': return AuthorizationType.IAM; case 'OPENID_CONNECT': return AuthorizationType.OIDC; case 'AMAZON_COGNITO_USER_POOLS': return AuthorizationType.USER_POOL; case 'AWS_LAMBDA': return AuthorizationType.LAMBDA; default: console.warn(`Unrecognized Authentication type ${authenticationType}`); return undefined; } }; /** * Get the function slots generated by the Graphql transform operation, adhering to the FunctionSlot interface. * @param generatedResolvers the resolvers generated by the transformer to spit back out. * @returns the list of generated function slots in the transformer, in order to facilitate overrides. */ export const getGeneratedFunctionSlots = (generatedResolvers: Record<string, string>): FunctionSlot[] => Object.entries(generatedResolvers) .filter(([name]) => name.split('.').length === 6) .map(([name, resolverCode]) => { const [typeName, fieldName, slotName, slotIndex, templateType] = name.split('.'); return { typeName, fieldName, slotName, slotIndex: Number.parseInt(slotIndex, 10), function: { // TODO: this should consolidate req/req values back together ...(templateType === 'req' ? { requestMappingTemplate: resolverCode } : {}), ...(templateType === 'res' ? { responseMappingTemplate: resolverCode } : {}), }, } as FunctionSlot; });