Skip to content

Commit

Permalink
feat(appsync): add support for mapping DynamoDB queries
Browse files Browse the repository at this point in the history
Add support to the L2 AppSync constructs for mapping DynamoDB queries.

Fixes aws#5861

Signed-off-by: Duarte Nunes <[email protected]>
  • Loading branch information
duarten committed Jan 23, 2020
1 parent 667443c commit 7e11115
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 2 deletions.
202 changes: 202 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,199 @@ export class LambdaDataSource extends BaseDataSource {
}
}

function concatAndDedup<T>(left: T[], right: T[]): T[] {
return left.concat(right).filter((elem, index, self) => {
return index === self.indexOf(elem);
});
}

/**
* Utility class to represent DynamoDB key conditions.
*/
abstract class BaseKeyCondition {
public and(cond: BaseKeyCondition): BaseKeyCondition {
return new (class extends BaseKeyCondition {
constructor(private readonly left: BaseKeyCondition, private readonly right: BaseKeyCondition) {
super();
}

public renderCondition(): string {
return `${this.left.renderCondition()} AND ${this.right.renderCondition()}`;
}

public keyNames(): string[] {
return concatAndDedup(this.left.keyNames(), this.right.keyNames());
}

public args(): string[] {
return concatAndDedup(this.left.args(), this.right.args());
}
})(this, cond);
}

public renderExpressionNames(): string {
return this.keyNames()
.map((keyName: string) => {
return `"#${keyName}" : "${keyName}"`;
})
.join(", ");
}

public renderExpressionValues(): string {
return this.args()
.map((arg: string) => {
return `":${arg}" : $util.dynamodb.toDynamoDBJson($ctx.args.${arg})`;
})
.join(", ");
}

public abstract renderCondition(): string;
public abstract keyNames(): string[];
public abstract args(): string[];
}

/**
* Utility class to represent DynamoDB "begins_with" key conditions.
*/
class BeginsWith extends BaseKeyCondition {
constructor(private readonly keyName: string, private readonly arg: string) {
super();
}

public renderCondition(): string {
return `begins_with(#${this.keyName}, :${this.arg})`;
}

public keyNames(): string[] {
return [this.keyName];
}

public args(): string[] {
return [this.arg];
}
}

/**
* Utility class to represent DynamoDB binary key conditions.
*/
class BinaryCondition extends BaseKeyCondition {
constructor(private readonly keyName: string, private readonly op: string, private readonly arg: string) {
super();
}

public renderCondition(): string {
return `#${this.keyName} ${this.op} :${this.arg}`;
}

public keyNames(): string[] {
return [this.keyName];
}

public args(): string[] {
return [this.arg];
}
}

/**
* Utility class to represent DynamoDB "between" key conditions.
*/
class Between extends BaseKeyCondition {
constructor(private readonly keyName: string, private readonly arg1: string, private readonly arg2: string) {
super();
}

public renderCondition(): string {
return `#${this.keyName} BETWEEN :${this.arg1} AND :${this.arg2}`;
}

public keyNames(): string[] {
return [this.keyName];
}

public args(): string[] {
return [this.arg1, this.arg2];
}
}

/**
* Factory class for DynamoDB key conditions.
*/
export class KeyCondition {

/**
* Condition k = arg, true if the key attribute k is equal to the Query argument
*/
public static eq(keyName: string, arg: string): KeyCondition {
return new KeyCondition(new BinaryCondition(keyName, '=', arg));
}

/**
* Condition k < arg, true if the key attribute k is less than the Query argument
*/
public static lt(keyName: string, arg: string): KeyCondition {
return new KeyCondition(new BinaryCondition(keyName, '<', arg));
}

/**
* Condition k <= arg, true if the key attribute k is less than or equal to the Query argument
*/
public static le(keyName: string, arg: string): KeyCondition {
return new KeyCondition(new BinaryCondition(keyName, '<=', arg));
}

/**
* Condition k > arg, true if the key attribute k is greater than the the Query argument
*/
public static gt(keyName: string, arg: string): KeyCondition {
return new KeyCondition(new BinaryCondition(keyName, '>', arg));
}

/**
* Condition k >= arg, true if the key attribute k is greater or equal to the Query argument
*/
public static ge(keyName: string, arg: string): KeyCondition {
return new KeyCondition(new BinaryCondition(keyName, '>=', arg));
}

/**
* Condition (k, arg). True if the key attribute k begins with the Query argument.
*/
public static beginsWith(keyName: string, arg: string): KeyCondition {
return new KeyCondition(new BeginsWith(keyName, arg));
}

/**
* Condition k BETWEEN arg1 AND arg2, true if k >= arg1 and k <= arg2.
*/
public static between(keyName: string, arg1: string, arg2: string): KeyCondition {
return new KeyCondition(new Between(keyName, arg1, arg2));
}

private constructor(private readonly cond: BaseKeyCondition) { }

/**
* Conjunction between two conditions.
*/
public and(keyCond: KeyCondition): KeyCondition {
return new KeyCondition(this.cond.and(keyCond.cond));
}

/**
* Renders the key condition to a VTL string.
*/
public renderTemplate(): string {
return `"query" : {
"expression" : "${this.cond.renderCondition()}",
"expressionNames" : {
${this.cond.renderExpressionNames()}
},
"expressionValues" : {
${this.cond.renderExpressionValues()}
}
}`;
}
}

/**
* MappingTemplates for AppSync resolvers
*/
Expand Down Expand Up @@ -458,6 +651,15 @@ export abstract class MappingTemplate {
return this.fromString('{"version" : "2017-02-28", "operation" : "Scan"}');
}

/**
* Mapping template to query a set of items from a DynamoDB table
*
* @param cond the key condition for the query
*/
public static dynamoDbQuery(cond: KeyCondition): MappingTemplate {
return this.fromString(`{"version" : "2017-02-28", "operation" : "Query", ${cond.renderTemplate()}}`);
}

/**
* Mapping template to get a single item from a DynamoDB table
*
Expand Down
23 changes: 22 additions & 1 deletion packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"ApiId"
]
},
"Definition": "type Customer {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Query {\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n}\n\ntype Mutation {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n}"
"Definition": "type Customer {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Order {\n customer: String!\n id: String!\n}\n\ntype Query {\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n getCustomerOrders(customer: String): Order\n}\n\ntype Mutation {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n}"
}
},
"ApiCustomerDSServiceRoleA929BCF7": {
Expand Down Expand Up @@ -211,6 +211,27 @@
"ApiSchema510EECD7"
]
},
"ApiCustomerDSQuerygetCustomerOrdersResolverBD1450FB": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"ApiF70053CD",
"ApiId"
]
},
"FieldName": "getCustomerOrders",
"TypeName": "Query",
"DataSourceName": "Customer",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Query\", \"query\" : {\n \"expression\" : \"#id = :id\",\n \"expressionNames\" : {\n \"#id\" : \"id\"\n },\n \"expressionValues\" : {\n \":id\" : $util.dynamodb.toDynamoDBJson($ctx.args.id)\n }\n }}",
"ResponseMappingTemplate": "$util.toJson($ctx.result.items)"
},
"DependsOn": [
"ApiCustomerDS8C23CB2D",
"ApiSchema510EECD7"
]
},
"CustomerTable260DCC08": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
Expand Down
8 changes: 7 additions & 1 deletion packages/@aws-cdk/aws-appsync/test/integ.graphql.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb';
import { App, Stack } from '@aws-cdk/core';
import { join } from 'path';
import { GraphQLApi, MappingTemplate } from '../lib';
import { GraphQLApi, KeyCondition, MappingTemplate } from '../lib';

const app = new App();
const stack = new Stack(app, 'aws-appsync-integ');
Expand Down Expand Up @@ -49,5 +49,11 @@ customerDS.createResolver({
requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem('id', 'id'),
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
});
customerDS.createResolver({
typeName: 'Query',
fieldName: 'getCustomerOrders',
requestMappingTemplate: MappingTemplate.dynamoDbQuery(KeyCondition.eq('id', 'id')),
responseMappingTemplate: MappingTemplate.dynamoDbResultList(),
});

app.synth();
6 changes: 6 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ input SaveCustomerInput {
name: String!
}

type Order {
customer: String!
id: String!
}

type Query {
getCustomers: [Customer]
getCustomer(id: String): Customer
getCustomerOrders(customer: String): Order
}

type Mutation {
Expand Down

0 comments on commit 7e11115

Please sign in to comment.