Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(appsync): add caching config to AppSync resolvers #17815

22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/caching-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Duration } from '@aws-cdk/core';

/**
* CachingConfig for AppSync resolvers
*/
export interface CachingConfig {
/**
* The caching keys for a resolver that has caching enabled.
* Valid values are entries from the $context.arguments, $context.source, and $context.identity maps.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please also add validation for this constraint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

Copy link
Contributor Author

@kylevillegas93 kylevillegas93 Dec 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright - added validation for caching keys + unit test - added some constants for $context.source, $context.arguments and $context.identity to help validate that caching keys are prefixed by these keys. Technically, there's some VTL syntax that should be adhered to but I think that may be a bit much for CDK to take on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! One last thing: Before validating you should check whether the value is encoded as a token, using the Token.isUnresolved() static method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah cool - I'll make this change!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright sweet - done! Checked to see if the caching key is resolved first before validating.

*
* @default - No caching keys
*/
readonly cachingKeys?: string[];

/**
* The TTL in seconds for a resolver that has caching enabled.
* Valid values are between 1 and 3600 seconds.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation + unit tests for ttl!

*
* @default - No TTL
*/
readonly ttl?: Duration;
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/caching-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const CONTEXT_ARGUMENTS_CACHING_KEY = '$context.arguments';
export const CONTEXT_SOURCE_CACHING_KEY = '$context.source';
export const CONTEXT_IDENTITY_CACHING_KEY = '$context.identity';
export const BASE_CACHING_KEYS = [CONTEXT_ARGUMENTS_CACHING_KEY, CONTEXT_SOURCE_CACHING_KEY, CONTEXT_IDENTITY_CACHING_KEY];
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// AWS::AppSync CloudFormation Resources:
export * from './appsync-function';
export * from './appsync.generated';
export * from './caching-config';
export * from './caching-key';
export * from './key';
export * from './data-source';
export * from './mapping-template';
Expand Down
25 changes: 24 additions & 1 deletion packages/@aws-cdk/aws-appsync/lib/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Construct } from 'constructs';
import { IAppsyncFunction } from './appsync-function';
import { CfnResolver } from './appsync.generated';
import { CachingConfig } from './caching-config';
import { BASE_CACHING_KEYS } from './caching-key';
import { BaseDataSource } from './data-source';
import { IGraphqlApi } from './graphqlapi-base';
import { MappingTemplate } from './mapping-template';

// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch.
// eslint-disable-next-line
import { Construct as CoreConstruct } from '@aws-cdk/core';
import { Construct as CoreConstruct, Token } from '@aws-cdk/core';

/**
* Basic properties for an AppSync resolver
Expand Down Expand Up @@ -40,6 +42,12 @@ export interface BaseResolverProps {
* @default - No mapping template
*/
readonly responseMappingTemplate?: MappingTemplate;
/**
* The caching configuration for this resolver
*
* @default - No caching configuration
*/
readonly cachingConfig?: CachingConfig;
}

/**
Expand Down Expand Up @@ -86,6 +94,17 @@ export class Resolver extends CoreConstruct {
throw new Error(`Pipeline Resolver cannot have data source. Received: ${props.dataSource.name}`);
}

if (props.cachingConfig?.ttl && (props.cachingConfig.ttl.toSeconds() < 1 || props.cachingConfig.ttl.toSeconds() > 3600)) {
throw new Error(`Caching config TTL must be between 1 and 3600 seconds. Received: ${props.cachingConfig.ttl.toSeconds()}`);
}

if (props.cachingConfig?.cachingKeys) {
if (props.cachingConfig.cachingKeys.find(cachingKey =>
!Token.isUnresolved(cachingKey) && !BASE_CACHING_KEYS.find(baseCachingKey => cachingKey.startsWith(baseCachingKey)))) {
throw new Error(`Caching config keys must begin with $context.arguments, $context.source or $context.identity. Received: ${props.cachingConfig.cachingKeys}`);
}
}

this.resolver = new CfnResolver(this, 'Resource', {
apiId: props.api.apiId,
typeName: props.typeName,
Expand All @@ -95,6 +114,10 @@ export class Resolver extends CoreConstruct {
pipelineConfig: pipelineConfig,
requestMappingTemplate: props.requestMappingTemplate ? props.requestMappingTemplate.renderTemplate() : undefined,
responseMappingTemplate: props.responseMappingTemplate ? props.responseMappingTemplate.renderTemplate() : undefined,
cachingConfig: {
cachingKeys: props.cachingConfig?.cachingKeys,
ttl: props.cachingConfig?.ttl?.toSeconds(),
},
});
props.api.addSchemaDependency(this.resolver);
if (props.dataSource) {
Expand Down
109 changes: 109 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/appsync-caching-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as path from 'path';
import { Template } from '@aws-cdk/assertions';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import { Duration } from '@aws-cdk/core';
import * as appsync from '../lib';

let stack: cdk.Stack;
let api: appsync.GraphqlApi;

beforeEach(() => {
// GIVEN
stack = new cdk.Stack();
api = new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.lambda.graphql')),
});
});

describe('Lambda caching config', () => {
// GIVEN
let func: lambda.Function;

beforeEach(() => {
func = new lambda.Function(stack, 'func', {
code: lambda.Code.fromAsset(path.join(__dirname, 'verify/lambda-tutorial')),
handler: 'lambda-tutorial.handler',
runtime: lambda.Runtime.NODEJS_12_X,
});
});

test('Lambda resolver contains caching config with caching key and TTL', () => {
// WHEN
const lambdaDS = api.addLambdaDataSource('LambdaDS', func);

lambdaDS.createResolver({
typeName: 'Query',
fieldName: 'allPosts',
cachingConfig: {
cachingKeys: ['$context.arguments', '$context.source', '$context.identity'],
ttl: Duration.seconds(300),
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppSync::Resolver', {
TypeName: 'Query',
FieldName: 'allPosts',
CachingConfig: {
CachingKeys: ['$context.arguments', '$context.source', '$context.identity'],
Ttl: 300,
},
});
});

test('Lambda resolver throws error when caching config with TTL is less than 1 second', () => {
// WHEN
const ttlInSconds = 0;
const lambdaDS = api.addLambdaDataSource('LambdaDS', func);

// THEN
expect(() => {
lambdaDS.createResolver({
typeName: 'Query',
fieldName: 'allPosts',
cachingConfig: {
cachingKeys: ['$context.identity'],
ttl: Duration.seconds(0),
},
});
}).toThrowError(`Caching config TTL must be between 1 and 3600 seconds. Received: ${ttlInSconds}`);
});

test('Lambda resolver throws error when caching config with TTL is greater than 3600 seconds', () => {
// WHEN
const ttlInSconds = 4200;
const lambdaDS = api.addLambdaDataSource('LambdaDS', func);

// THEN
expect(() => {
lambdaDS.createResolver({
typeName: 'Query',
fieldName: 'allPosts',
cachingConfig: {
cachingKeys: ['$context.identity'],
ttl: Duration.seconds(ttlInSconds),
},
});
}).toThrowError(`Caching config TTL must be between 1 and 3600 seconds. Received: ${ttlInSconds}`);
});

test('Lambda resolver throws error when caching config has invalid caching keys', () => {
// WHEN
const invalidCachingKeys = ['$context.metadata'];
const lambdaDS = api.addLambdaDataSource('LambdaDS', func);

// THEN
expect(() => {
lambdaDS.createResolver({
typeName: 'Query',
fieldName: 'allPosts',
cachingConfig: {
cachingKeys: invalidCachingKeys,
ttl: Duration.seconds(300),
},
});
}).toThrowError(`Caching config keys must begin with $context.arguments, $context.source or $context.identity. Received: ${invalidCachingKeys}`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
},
"FieldName": "getTests",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "ds",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}",
Expand All @@ -160,6 +161,7 @@
},
"FieldName": "addTest",
"TypeName": "Mutation",
"CachingConfig": {},
"DataSourceName": "ds",
"Kind": "UNIT",
"RequestMappingTemplate": "\n #set($input = $ctx.args.test)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }",
Expand Down Expand Up @@ -223,6 +225,7 @@
},
"FieldName": "version",
"TypeName": "test",
"CachingConfig": {},
"Kind": "PIPELINE",
"PipelineConfig": {
"Functions": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
},
"FieldName": "getPost",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "LambdaDS",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"Invoke\", \"payload\": { \"field\": \"getPost\", \"arguments\": $utils.toJson($context.arguments)}}",
Expand All @@ -135,6 +136,7 @@
},
"FieldName": "allPosts",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "LambdaDS",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"Invoke\", \"payload\": { \"field\": \"allPosts\"}}",
Expand All @@ -156,6 +158,7 @@
},
"FieldName": "addPost",
"TypeName": "Mutation",
"CachingConfig": {},
"DataSourceName": "LambdaDS",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"Invoke\", \"payload\": { \"field\": \"addPost\", \"arguments\": $utils.toJson($context.arguments)}}",
Expand All @@ -177,6 +180,7 @@
},
"FieldName": "relatedPosts",
"TypeName": "Post",
"CachingConfig": {},
"DataSourceName": "LambdaDS",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"BatchInvoke\", \"payload\": { \"field\": \"relatedPosts\", \"source\": $utils.toJson($context.source)}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
},
"FieldName": "getTests",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "testDataSource",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}",
Expand All @@ -153,6 +154,7 @@
},
"FieldName": "addTest",
"TypeName": "Mutation",
"CachingConfig": {},
"DataSourceName": "testDataSource",
"Kind": "UNIT",
"RequestMappingTemplate": "\n #set($input = $ctx.args.test)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
},
"FieldName": "getTests",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "ds",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\":\"2017-02-28\",\"operation\":\"GET\",\"path\":\"/id/post/_search\",\"params\":{\"headers\":{},\"queryString\":{},\"body\":{\"from\":0,\"size\":50}}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
},
"FieldName": "getTest",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "testDataSource",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"GetItem\", \"key\": {\"id\": $util.dynamodb.toDynamoDBJson($ctx.args.id)}}",
Expand All @@ -184,6 +185,7 @@
},
"FieldName": "getTests",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "testDataSource",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}",
Expand All @@ -205,6 +207,7 @@
},
"FieldName": "addTest",
"TypeName": "Mutation",
"CachingConfig": {},
"DataSourceName": "testDataSource",
"Kind": "UNIT",
"RequestMappingTemplate": "\n #set($input = $ctx.args.test)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
},
"FieldName": "getPlanets",
"TypeName": "Query",
"CachingConfig": {},
"DataSourceName": "planets",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}",
Expand All @@ -152,6 +153,7 @@
},
"FieldName": "addPlanet",
"TypeName": "Mutation",
"CachingConfig": {},
"DataSourceName": "planets",
"Kind": "UNIT",
"RequestMappingTemplate": "\n #set($input = $context.arguments)\n $util.qr($input.put(\"name\", $context.arguments.name))\n$util.qr($input.put(\"diameter\", $context.arguments.diameter))\n$util.qr($input.put(\"rotationPeriod\", $context.arguments.rotationPeriod))\n$util.qr($input.put(\"orbitalPeriod\", $context.arguments.orbitalPeriod))\n$util.qr($input.put(\"gravityPeriod\", $context.arguments.gravity))\n$util.qr($input.put(\"population\", $context.arguments.population))\n$util.qr($input.put(\"climates\", $context.arguments.climates))\n$util.qr($input.put(\"terrains\", $context.arguments.terrains))\n$util.qr($input.put(\"surfaceWater\", $context.arguments.surfaceWater))\n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }",
Expand Down
Loading