Skip to content

Commit a5e6e72

Browse files
authored
Merge pull request #432 from jgilbert01/feature-ddb-set-support
DDB set support in updateExpression
2 parents d0617b8 + 6c82cb6 commit a5e6e72

File tree

4 files changed

+142
-31
lines changed

4 files changed

+142
-31
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aws-lambda-stream",
3-
"version": "1.1.15",
3+
"version": "1.1.16",
44
"description": "Create stream processors with AWS Lambda functions.",
55
"keywords": [
66
"aws",

src/sinks/dynamodb.js

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import _ from 'highland';
2-
import merge from 'lodash/merge';
32

43
import Connector from '../connectors/dynamodb';
54

@@ -8,35 +7,58 @@ import { debug as d } from '../utils/print';
87
import { ratelimit } from '../utils/ratelimit';
98

109
export const updateExpression = (Item) => {
11-
const keys = Object.keys(Item);
12-
13-
const ExpressionAttributeNames = keys
14-
.filter((attrName) => Item[attrName] !== undefined)
15-
.map((attrName) => ({ [`#${attrName}`]: attrName }))
16-
.reduce(merge, {});
17-
18-
const ExpressionAttributeValues = keys
19-
.filter((attrName) => Item[attrName] !== undefined && Item[attrName] !== null)
20-
.map((attrName) => ({ [`:${attrName}`]: Item[attrName] }))
21-
.reduce(merge, {});
22-
23-
let UpdateExpression = `SET ${keys
24-
.filter((attrName) => Item[attrName] !== undefined && Item[attrName] !== null)
25-
.map((attrName) => `#${attrName} = :${attrName}`)
26-
.join(', ')}`;
27-
28-
const UpdateExpressionRemove = keys
29-
.filter((attrName) => Item[attrName] === null)
30-
.map((attrName) => `#${attrName}`)
31-
.join(', ');
32-
33-
if (UpdateExpressionRemove.length) {
34-
UpdateExpression = `${UpdateExpression} REMOVE ${UpdateExpressionRemove}`;
35-
}
10+
const exprAttributes = Object.entries(Item)
11+
.filter(([key, value]) => value !== undefined)
12+
.reduce((acc, [key, value]) => {
13+
// If this attribute ends with '_delete'...assume we're deleting values from a set.
14+
const isDeleteSet = key.endsWith('_delete');
15+
const baseKey = isDeleteSet ? key.replace(/_delete$/, '') : key;
16+
acc.ExpressionAttributeNames[`#${baseKey}`] = baseKey;
17+
18+
if (value === null) {
19+
acc.removeClauses.push(`#${baseKey}`);
20+
return acc;
21+
}
22+
23+
if (isDeleteSet) {
24+
let setValue = value;
25+
if (!(setValue instanceof Set)) {
26+
setValue = new Set([setValue]);
27+
}
28+
acc.ExpressionAttributeValues[`:${key}`] = setValue;
29+
acc.deleteClauses.push(`#${baseKey} :${key}`);
30+
return acc;
31+
}
32+
33+
if (value instanceof Set) {
34+
acc.ExpressionAttributeValues[`:${key}`] = value;
35+
acc.addClauses.push(`#${key} :${key}`);
36+
return acc;
37+
}
38+
39+
acc.ExpressionAttributeValues[`:${key}`] = value;
40+
acc.setClauses.push(`#${key} = :${key}`);
41+
return acc;
42+
}, {
43+
ExpressionAttributeNames: {},
44+
ExpressionAttributeValues: {},
45+
setClauses: [],
46+
addClauses: [],
47+
deleteClauses: [],
48+
removeClauses: [],
49+
});
50+
51+
// Construct UpdateExpression
52+
const updateExpressionParts = [];
53+
if (exprAttributes.setClauses.length) updateExpressionParts.push(`SET ${exprAttributes.setClauses.join(', ')}`);
54+
if (exprAttributes.removeClauses.length) updateExpressionParts.push(`REMOVE ${exprAttributes.removeClauses.join(', ')}`);
55+
if (exprAttributes.addClauses.length) updateExpressionParts.push(`ADD ${exprAttributes.addClauses.join(', ')}`);
56+
if (exprAttributes.deleteClauses.length) updateExpressionParts.push(`DELETE ${exprAttributes.deleteClauses.join(', ')}`);
57+
const UpdateExpression = updateExpressionParts.join(' ');
3658

3759
return {
38-
ExpressionAttributeNames,
39-
ExpressionAttributeValues,
60+
ExpressionAttributeNames: exprAttributes.ExpressionAttributeNames,
61+
ExpressionAttributeValues: exprAttributes.ExpressionAttributeValues,
4062
UpdateExpression,
4163
ReturnValues: 'ALL_NEW',
4264
};

test/unit/sinks/dynamodb.test.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,90 @@ describe('sinks/dynamodb.js', () => {
5858
});
5959
});
6060

61+
it('should calculate updateExpression adding values to a set', () => {
62+
const result = updateExpression({
63+
tags: new Set(['a', 'b']),
64+
});
65+
66+
expect(normalizeObj(result)).to.deep.equal({
67+
ExpressionAttributeNames: {
68+
'#tags': 'tags',
69+
},
70+
ExpressionAttributeValues: {
71+
':tags': ['a', 'b'],
72+
},
73+
UpdateExpression: 'ADD #tags :tags',
74+
ReturnValues: 'ALL_NEW',
75+
});
76+
});
77+
78+
it('should calculate updateExpression removing values from a set', () => {
79+
const result = updateExpression({
80+
tags_delete: new Set(['x', 'y']),
81+
});
82+
83+
expect(normalizeObj(result)).to.deep.equal({
84+
ExpressionAttributeNames: {
85+
'#tags': 'tags',
86+
},
87+
ExpressionAttributeValues: {
88+
':tags_delete': ['x', 'y'],
89+
},
90+
UpdateExpression: 'DELETE #tags :tags_delete',
91+
ReturnValues: 'ALL_NEW',
92+
});
93+
});
94+
95+
it('should wrap calculate updateExpression wrapping a delete set value in a set', () => {
96+
const result = updateExpression({
97+
tags_delete: 'x',
98+
});
99+
100+
expect(normalizeObj(result)).to.deep.equal({
101+
ExpressionAttributeNames: {
102+
'#tags': 'tags',
103+
},
104+
ExpressionAttributeValues: {
105+
':tags_delete': ['x'],
106+
},
107+
UpdateExpression: 'DELETE #tags :tags_delete',
108+
ReturnValues: 'ALL_NEW',
109+
});
110+
});
111+
112+
it('should calculate complex updateExpression using SET, REMOVE, ADD, and DELETE', () => {
113+
const result = updateExpression({
114+
id: '123',
115+
name: 'Complex Thing',
116+
description: null,
117+
tags: new Set(['blue', 'green']),
118+
categories: new Set(['a', 'b']),
119+
tags_delete: 'red',
120+
categories_delete: new Set(['x', 'y']),
121+
ignoredField: undefined,
122+
});
123+
124+
expect(normalizeObj(result)).to.deep.equal({
125+
ExpressionAttributeNames: {
126+
'#id': 'id',
127+
'#name': 'name',
128+
'#description': 'description',
129+
'#tags': 'tags',
130+
'#categories': 'categories',
131+
},
132+
ExpressionAttributeValues: {
133+
':id': '123',
134+
':name': 'Complex Thing',
135+
':tags': ['blue', 'green'],
136+
':categories': ['a', 'b'],
137+
':tags_delete': ['red'],
138+
':categories_delete': ['x', 'y'],
139+
},
140+
UpdateExpression: 'SET #id = :id, #name = :name REMOVE #description ADD #tags :tags, #categories :categories DELETE #tags :tags_delete, #categories :categories_delete',
141+
ReturnValues: 'ALL_NEW',
142+
});
143+
});
144+
61145
it('should calculate timestampCondition', () => {
62146
expect(timestampCondition()).to.deep.equal({
63147
ConditionExpression: 'attribute_not_exists(#timestamp) OR #timestamp < :timestamp',
@@ -134,3 +218,8 @@ describe('sinks/dynamodb.js', () => {
134218
.done(done);
135219
});
136220
});
221+
222+
// Chai doesn't like sets...we can convert them to arrays to help it out.
223+
const normalizeObj = (obj) =>
224+
JSON.parse(JSON.stringify(obj, (thisArg, value) =>
225+
(value instanceof Set ? [...value] : value)));

0 commit comments

Comments
 (0)