Package: cdk-appsync-transformer
AppSync Transformer Construct for AWS CDKNoticeFor CDK versions < 1.64.0 please useaws-cdk-appsync-transformer.Why This PackageIn April 2020 I wrote ablog poston using the AWS Cloud Development Kit with AppSync. I wrote my own transformer in order to emulate AWS Amplify's method of using GraphQL directives in order to template a lot of the Schema Definition Language.This package is my attempt to convert all of that effort into a separate construct in order to clean up the process.How Do I Use ItExample UsageAPI With Default Valuesimport{AppSyncTransformer}from'cdk-appsync-transformer';...newAppSyncTransformer(this,"my-cool-api",{schemaPath:'schema.graphql'});schema.graphqltypeCustomer@model@auth(rules:[{allow:groups,groups:["Admins"]}{allow:private,provider:iam,operations:[read,update]}]){id:ID!firstName:String!lastName:String!active:Boolean!address:String!}typeProduct@model@auth(rules:[{allow:groups,groups:["Admins"]}{allow:public,provider:iam,operations:[read]}]){id:ID!name:String!description:String!price:String!active:Boolean!added:AWSDateTime!orders:[Order]@connection}typeOrder@model@key(fields:["id","productID"]){id:ID!productID:ID!total:String!ordered:AWSDateTime!}Supported Amplify DirectivesTested:@model@auth@connection@key@functionThese work differently here than they do in Amplify - seeFunctionsbelowExperimental:@versioned@http@ttlCommunity directive transformerNot Yet Supported:@searchable@predictionsCustom Transformers & DirectivesThis is an advanced featureIt is possible to add pre/post custom transformers that extend the Amplify ITransformer. To see a simple example please look atmapped-transformer.tsin the tests section.This allows you to modify the data either before or after thecdk-transformeris run.Limitation:Due to some limitations withjsiiwe are unable to export the ITransformer interface fromgraphql-transformer-coreto ensure complete type safety. Instead, there is a validation method that will check forname,directiveandtypeDefinitionsmembers in the transformers that are passed in.import{PreTransformer,PostTransformer}from"./customTransformers";newAppSyncTransformer(this,"my-cool-api",{schemaPath:"schema.graphql",preCdkTransformers:[newPreTransformer()],postCdkTransformers:[newPostTransformer()],});Custom VTL TransformerCan be used to create customNONEdatasource resolvers.This allows for custom or special logic to be used and added via a transformer.Example:typeThing{fooBar:String}typeQuery{listThingCustom:Thing@custom(request:"test/custom-resolvers/Test/request.vtl",response:"test/custom-resolvers/Test/response.vtl")}The above will generate aQuery.listThingCustomrequest and response resolver.
You can customize the location of custom resolvers using thecustomVtlTransformerRootDirectoryproperty.Overriding generated vtlThis is an advanced featureYou can override generated request and response mapping templates using theoverrideResolverconvenience method.constappsyncTransformer=newAppSyncTransformer(this,"my-cool-api",{schemaPath:"schema.graphql",});//Youcanoverridethejusttherequest,justtheresponse,orBOTHappsyncTransformer.overrideResolver({typeName:'Query',fieldName:'listThings',requestMappingTemplateFile:path.join(process.cwd(),'custom-resolvers','Things','request.vtl'),responseMappingTemplateFile:path.join(process.cwd(),'custom-resolvers','Things','response.vtl'),});AuthenticationUser Pool AuthenticationconstuserPool=newUserPool(this,'my-cool-user-pool',{...})...constuserPoolClient=newUserPoolClient(this,`${id}-client`,{userPool:this.userPool,...})...newAppSyncTransformer(this,"my-cool-api",{schemaPath:'schema.graphql',authorizationConfig:{defaultAuthorization:{authorizationType:AuthorizationType.USER_POOL,userPoolConfig:{userPool:userPool,appIdClientRegex:userPoolClient.userPoolClientId,defaultAction:UserPoolDefaultAction.ALLOW}}}});IAMUnauth RoleYou can grant access to thepublicpolicies generated from the@authtransformer by usingappsyncTransformer.grantPublic(...). In the example below you can see we give public iam read access for the Product type. This will generate permissions forlistProducts,getProductandProduct(to get all the fields). We then attach it to ourpublicRoleusing the grantPublic method.Example:typeProduct@model@auth(rules:[{allow:groups,groups:["Admins"]},{allow:public,provider:iam,operations:[read]}])@key(name:"productsByName",fields:["name","added"],queryField:"productsByName"){id:ID!name:String!description:String!price:String!active:Boolean!added:AWSDateTime!orders:[Order]@connection}constidentityPool=newCfnIdentityPool(stack,'test-identity-pool',{identityPoolName:'test-identity-pool',cognitoIdentityProviders:[{clientId:userPoolClient.userPoolClientId,providerName:`cognito-idp.${stack.region}.amazonaws.com/${userPool.userPoolId}`,},],allowUnauthenticatedIdentities:true,});constpublicRole=newRole(stack,'public-role',{assumedBy:newWebIdentityPrincipal('cognito-identity.amazonaws.com').withConditions({'StringEquals':{'cognito-identity.amazonaws.com:aud':`${identityPool.ref}`},'ForAnyValue:StringLike':{'cognito-identity.amazonaws.com:amr':'unauthenticated'},}),});appSyncTransformer.grantPublic(publicRole);Auth RoleYou can grant access to theprivatepolicies generated from the@authtransformer by usingappsyncTransformer.grantPrivate(...). In the example below you can see we give private iam read and update access for the Customer type. This will generate permissions forlistCustomers,getCustomer,updateCustomerandCustomer(to get all the fields). We then attach it to ourprivateFunctionusing the grantPrivate method.You could also use an identity pool as in the unauth example above, I just wanted to show a varied range of usetypeCustomer@model@auth(rules:[{allow:groups,groups:["Admins"]},{allow:private,provider:iam,operations:[read,update]}]){id:ID!firstName:String!lastName:String!active:Boolean!address:String!}constprivateFunction=newFunction(stack,'test-function',{runtime:Runtime.NODEJS_12_X,code:Code.fromInline('export function handler() { }'),handler:'handler',});appSyncTransformer.grantPrivate(privateFunction);FunctionsDirective ExampletypeQuery{listUsers:UserConnection@function(name:"myFunction")getUser(id:ID!):User@function(name:"myFunction")}There are two ways to add functions as data sources (and their resolvers)Construct Convenience MethodconstmyFunction=newFunction(...);//firstargumentisthenameinthe@functiondirectiveappsyncTransformer.addLambdaDataSourceAndResolvers('myFunction','unique-id',myFunction,{name:'lambdaDatasourceName'})addLambdaDataSourceAndResolversdoes the same thing as the manual version below. However, if you want to customize mapping templates you will have to bypass this and set up the data source and resolvers yourselfManuallyFields with the@functiondirective will be accessible viaappsyncTransformer.functionResolvers. It will return a map like so:{'user-function':[{typeName:'Query',fieldName:'listUsers'},{typeName:'Query',fieldName:'getUser'},{typeName:'Mutation',fieldName:'createUser'},{typeName:'Mutation',fieldName:'updateUser'}]}You can grab your function resolvers via the map and assign them your own function(s). Example might be something like:constuserFunction=newFunction(...);constuserFunctionDataSource=appsyncTransformer.appsyncAPI.addLambdaDataSource('some-id',userFunction);constdataSourceMap={'user-function':userFunctionDataSource};for(const[functionName,resolver]ofObject.entries(appsyncTransformer.functionResolvers)){constdataSource=dataSourceMap[functionName];newResolver(this.nestedAppsyncStack,`${resolver.typeName}-${resolver.fieldName}-resolver`,{api:appsyncTransformer.appsyncAPI,typeName:resolver.typeName,fieldName:resolver.fieldName,dataSource:dataSource,requestMappingTemplate:resolver.defaultRequestMappingTemplate,responseMappingTemplate:resolver.defaultResponseMappingTemplate//Thisdefaultstoallowerrorstoreturntotheclientinsteadofthrowing});}Table Name MapOften you will need to access your table names in a lambda function or elsewhere. The cdk-appsync-transformer will return these values as a map of table names to cdk tokens. These tokens will be resolved at deploy time. They can be accessed viaappSyncTransformer.tableNameMap.{CustomerTable:'${Token[TOKEN.1300]}',ProductTable:'${Token[TOKEN.1346]}',OrderTable:'${Token[TOKEN.1392]}',BlogTable:'${Token[TOKEN.1442]}',PostTable:'${Token[TOKEN.1492]}',CommentTable:'${Token[TOKEN.1546]}',UserTable:'${Token[TOKEN.1596]}'}Table MapYou may need to access your dynamo table L2 constructs. These can be accessed viaappSyncTransformer.tableMap.Custom Table NamesIf you do not like autogenerated names for your Dynamo tables you can pass in props to specify them. Use thetableKeyvalue derived from the@modeldirective. Example, if you havetype Foo @modelyou would useFooTableas the key value.constappSyncTransformer=newAppSyncTransformer(stack,'test-transformer',{schemaPath:testSchemaPath,tableNames:{CustomerTable:customerTableName,OrderTable:orderTableName},});DynamoDB StreamsThere are two ways to enable DynamoDB streams for a table. The first version is probably most preferred. You pass in the@modeltype name and the StreamViewType as properties when creating the AppSyncTransformer. This will also allow you to access thetableStreamArnproperty of the L2 table construct from thetableMap.constappSyncTransformer=newAppSyncTransformer(stack,'test-transformer',{schemaPath:testSchemaPath,dynamoDbStreamConfig:{Order:StreamViewType.NEW_IMAGE,Blog:StreamViewType.NEW_AND_OLD_IMAGES}});constorderTable=appSyncTransformer.tableMap.OrderTable;//Dosomethingwiththetablestreamarn-orderTable.tableStreamArnA convenience method is also available. It returns the stream arn because the L2 Table construct does not seem to update with the value since we are updating the underlying CfnTable. Normally a Table construct must pass in the stream specification as a propconststreamArn=appSyncTransformer.addDynamoDBStream({modelTypeName:'Order',streamViewType:StreamViewType.NEW_IMAGE,});//DosomethingwiththestreamArnDataStore SupportPasssyncEnabled: trueto theAppSyncTransformerPropsGenerate necessary exports (seeCode Generationbelow)Cfn OutputsappsyncGraphQLEndpointOutput- the appsync graphql endpointCode GenerationI've written some helpers to generate code similarly to how AWS Amplify generates statements and types. You can find the codehere.VersioningI willattemptto align the major and minor version of this package with [AWS CDK], but always check the release descriptions for compatibility.I currently supportContributingSeeCONTRIBUTINGfor detailsLicenseDistributed underApache License, Version 2.0Referencesaws cdkamplify-cliAmplify DirectivesSponsorsStedi