Skip to content
This repository has been archived by the owner on Sep 30, 2019. It is now read-only.

Commit

Permalink
[134] AppSync Emulator - Implement conditionalCheckFailedHandler (#136
Browse files Browse the repository at this point in the history
)

* Initial attempt at conditionalCheckFailedHandler.strategy == "Reject" for PutItem, Strategy "Custom" needs more work
  • Loading branch information
asodeur authored and lightsofapollo committed May 31, 2019
1 parent c0753d1 commit 3e89cbc
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 17 deletions.
106 changes: 105 additions & 1 deletion packages/appsync-emulator-serverless/__test__/dynamodbSource.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe('dynamodbSource', () => {
expect(result).toEqual(expected);
});

it('no conditions', async () => {
it('failing conditions and unexpected outcome', async () => {
// create the initial object
await runOp({
version: '2017-02-28',
Expand Down Expand Up @@ -177,6 +177,110 @@ describe('dynamodbSource', () => {
}
throw new Error('expected exception');
});

it('failing conditions and outcome as expected', async () => {
// create the initial object
await runOp({
version: '2017-02-28',
operation: 'PutItem',
key: {
id: {
S: 'foo',
},
},
attributeValues: {
bar: {
S: 'bar',
},
},
});

// use a condition which will fail but since no values
// change this is ok using the default strategy 'Reject'
const result = await runOp({
version: '2017-02-28',
operation: 'PutItem',
key: {
id: {
S: 'foo',
},
},
attributeValues: {
bar: {
S: 'bar',
},
},
condition: {
expression: 'attribute_not_exists(bar)',
},
});

const { Item: output } = await docClient
.get({
TableName: tableName,
Key: { id: 'foo' },
})
.promise();

const expected = {
id: 'foo',
bar: 'bar',
};
expect(output).toEqual(expected);
expect(result).toEqual(expected);
});

it('failing conditions and differences ignored', async () => {
// create the initial object
await runOp({
version: '2017-02-28',
operation: 'PutItem',
key: {
id: {
S: 'foo',
},
},
attributeValues: {
bar: {
S: 'bar',
},
},
});

// use a condition which will fail, ok because we are ignoring 'bar'
const result = await runOp({
version: '2017-02-28',
operation: 'PutItem',
key: {
id: {
S: 'foo',
},
},
attributeValues: {
bar: {
S: 'soupbar',
},
},
condition: {
expression: 'attribute_not_exists(bar)',
equalsIgnore: ['bar'],
},
});

const { Item: output } = await docClient
.get({
TableName: tableName,
Key: { id: 'foo' },
})
.promise();

const expected = {
id: 'foo',
bar: 'bar',
};
expect(output).toEqual(expected);
expect(result).toEqual(expected);
});
});

describe('UpdateItem', () => {
Expand Down
71 changes: 55 additions & 16 deletions packages/appsync-emulator-serverless/dynamodbSource.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const {
DynamoDB: { Converter },
} = require('aws-sdk');
const isEqual = require('lodash.isequal'); // TODO: hoist

const nullIfEmpty = obj => (Object.keys(obj).length === 0 ? null : obj);

Expand Down Expand Up @@ -58,25 +59,63 @@ const putItem = async (
expression,
expressionNames,
expressionValues,
conditionalCheckFailedHandler,
equalsIgnore,
} = {},
},
) => {
await db
.putItem({
TableName: table,
Item: {
...attributeValues,
...key,
},
ConditionExpression: expression,
ExpressionAttributeNames: expressionNames,
ExpressionAttributeValues: expressionValues,
})
.promise();

// put does not return us anything useful so we need to fetch the object.

return getItem(db, table, { key, consistentRead: true });
try {
await db
.putItem({
TableName: table,
Item: {
...attributeValues,
...key,
},
ConditionExpression: expression,
ExpressionAttributeNames: expressionNames,
ExpressionAttributeValues: expressionValues,
})
.promise();

return await getItem(db, table, { key, consistentRead: true });
} catch (err) {
if (err.code === 'ConditionalCheckFailedException') {
// not an error if PutItem would not have had any effect
let oldValues = {};
try {
oldValues = await getItem(db, table, { key, consistentRead: true });
} catch (ignore) {
/* ignore */
}

const shouldBeValues = unmarshall({ ...attributeValues, ...key });
const ignoreKeys = equalsIgnore || [];

if (
isEqual(
Object.entries(oldValues).reduce((newObject, [k, v]) => {
if (ignoreKeys.indexOf(k) === -1) return { ...newObject, [k]: v };
return newObject;
}, {}),
Object.entries(shouldBeValues).reduce((newObject, [k, v]) => {
if (ignoreKeys.indexOf(k) === -1) return { ...newObject, [k]: v };
return newObject;
}, {}),
)
)
return Promise.resolve(oldValues);

if (
conditionalCheckFailedHandler &&
conditionalCheckFailedHandler.strategy !== 'Reject'
) {
// if there is a custom check-failed handler pass oldValues up to give the caller a chance to call conditionalCheckFailedHandler
err.oldValues = oldValues;
}
}
throw err;
}
};

const updateItem = async (
Expand Down
1 change: 1 addition & 0 deletions packages/appsync-emulator-serverless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"jwt-decode": "^2.2.0",
"libphonenumber-js": "^1.7.16",
"logdown": "^3.2.3",
"lodash.isequal": "^4.5.0",
"node-fetch": "^2.2.0",
"paho-mqtt": "^1.0.4",
"pkg-up": "^2.0.0",
Expand Down

0 comments on commit 3e89cbc

Please sign in to comment.