@aws-amplify/graphql-api-construct
Version:
AppSync GraphQL Api Construct using Amplify GraphQL Transformer.
260 lines (238 loc) • 9.64 kB
text/typescript
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;
});