Skip to content

Commit

Permalink
Ensure no undefines in update requests.
Browse files Browse the repository at this point in the history
  • Loading branch information
wparad committed Sep 23, 2024
1 parent 602420e commit b0b22ca
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 7 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"cSpell.words": [
"atsquad",
"cimpress",
"clonedeep",
"Fqdn",
"microsoftonline",
"nosniff",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Change log

## 1.0 ##
* Coerce `undefined` expression attribute values to `null`, because dynamoDb can't handle that, and it strips them from the request
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,8 @@
"homepage": "https://rhosys.ch",
"engines": {
"node": ">=16"
},
"dependencies": {
"lodash.clonedeep": "^4.5.0"
}
}
25 changes: 19 additions & 6 deletions src/dynamoDbSafe.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const DynamoDbOriginal = require('aws-sdk/clients/dynamodb');
const cloneDeep = require('lodash.clonedeep');

process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1;

Expand All @@ -25,6 +26,18 @@ function parseExpression(logger, expression, isMultiExpression) {

return { keys: {}, values: {} };
}

function coerceExpressionAttributeNamesAndValues(parameters) {
const newParameters = cloneDeep(parameters);
Object.keys(newParameters.ExpressionAttributeValues || {}).map(key => {
if (newParameters.ExpressionAttributeValues[key] === undefined) {
newParameters.ExpressionAttributeValues[key] = null;
}
});

return newParameters;
}

class DynamoDB extends DynamoDbOriginal.DocumentClient {
constructor(args) {
super(args);
Expand All @@ -35,7 +48,7 @@ class DynamoDB extends DynamoDbOriginal.DocumentClient {
if (!originalParams || !originalParams.TableName) { throw new DynamoDbError({ error: 'TableName not specified', parameters: originalParams }, 'InvalidParameters'); }
if (!originalParams.Key) { throw new DynamoDbError({ error: 'Key not specified', parameters: originalParams }, 'InvalidParameters'); }

const params = originalParams;
const params = coerceExpressionAttributeNamesAndValues(originalParams);
const capturedStack = { name: 'DynamoDB.update() Error:' };
Error.captureStackTrace(capturedStack);
const resultAsync = super.get(params).promise().catch(error => {
Expand All @@ -53,7 +66,7 @@ class DynamoDB extends DynamoDbOriginal.DocumentClient {
if (!originalParams || !originalParams.TableName) { throw new DynamoDbError({ error: 'TableName not specified', parameters: originalParams }, 'InvalidParameters'); }
if (!originalParams.KeyConditionExpression) { throw new DynamoDbError({ error: 'KeyConditionExpression not specified', parameters: originalParams }, 'InvalidParameters'); }

const params = originalParams;
const params = coerceExpressionAttributeNamesAndValues(originalParams);
const capturedStack = { name: 'DynamoDB.update() Error:' };
Error.captureStackTrace(capturedStack);
const resultAsync = super.query(params).promise().catch(error => {
Expand All @@ -76,7 +89,7 @@ class DynamoDB extends DynamoDbOriginal.DocumentClient {
// Validate the tokens
}

const params = originalParams;
const params = coerceExpressionAttributeNamesAndValues(originalParams);
const capturedStack = { name: 'DynamoDB.update() Error:' };
Error.captureStackTrace(capturedStack);
const resultAsync = super.delete(params).promise().catch(error => {
Expand All @@ -99,7 +112,7 @@ class DynamoDB extends DynamoDbOriginal.DocumentClient {
// Validate the tokens
}

const params = originalParams;
const params = coerceExpressionAttributeNamesAndValues(originalParams);
const capturedStack = { name: 'DynamoDB.update() Error:' };
Error.captureStackTrace(capturedStack);
const resultAsync = super.put(params).promise().catch(error => {
Expand All @@ -116,7 +129,7 @@ class DynamoDB extends DynamoDbOriginal.DocumentClient {
scan(originalParams) {
if (!originalParams || !originalParams.TableName) { throw new DynamoDbError({ error: 'TableName not specified', parameters: originalParams }, 'InvalidParameters'); }

const params = originalParams;
const params = coerceExpressionAttributeNamesAndValues(originalParams);
const capturedStack = { name: 'DynamoDB.update() Error:' };
Error.captureStackTrace(capturedStack);
const resultAsync = super.scan(params).promise().catch(error => {
Expand All @@ -142,7 +155,7 @@ class DynamoDB extends DynamoDbOriginal.DocumentClient {
// Validate the tokens
}

const params = originalParams;
const params = coerceExpressionAttributeNamesAndValues(originalParams);
const capturedStack = { name: 'DynamoDB.update() Error:' };
Error.captureStackTrace(capturedStack);
const resultAsync = super.update(params).promise().catch(error => {
Expand Down
47 changes: 47 additions & 0 deletions tests/attributeNamesAndValues.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { expect } = require('chai');
const { describe, it, beforeEach, afterEach } = require('mocha');
const sinon = require('sinon');
const { DynamoDB: DynamoDbOriginal } = require('aws-sdk');

let sandbox;
beforeEach(() => { sandbox = sinon.createSandbox(); });
afterEach(() => sandbox.restore());

const { DynamoDB } = require('../src/dynamoDbSafe');
const cloneDeep = require('lodash.clonedeep');

describe('dynamoDbArmor.js', () => {
describe('namesAndValuesCoercion()', () => {
it('Fixes undefined expressionAttributeValue - Coerce `undefined` expression attribute values to `null`, because dynamoDb cant handle them, and it strips them from the request. nulls are not stripped', async () => {
const testTable = 'Test-TableId';
const testHash = 'testHash';
const testRange = 'testRange';
const params = {
TableName: testTable,
Key: {
hash: testHash,
rang: testRange
},
UpdateExpression: 'SET #key = :value',
ConditionExpression: 'attribute_exists(hash)',
ExpressionAttributeNames: {
'#key': 'key'
},
ExpressionAttributeValues: {
':value': undefined
}
};
try {
const dynamoDbOriginalMock = sandbox.mock(DynamoDbOriginal.DocumentClient.prototype);

const expectedParams = cloneDeep(params);
expectedParams.ExpressionAttributeValues[':value'] = null;
dynamoDbOriginalMock.expects('update').once().withArgs(expectedParams).returns({ promise() { return Promise.resolve(); } });
await new DynamoDB().update(params);
dynamoDbOriginalMock.verify();
} catch (error) {
expect(error.message).to.eql(null, JSON.stringify(error.message, null, 2));
}
});
});
});
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2619,7 +2619,7 @@ locate-path@^6.0.0:
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==

lodash.defaults@^4.2.0:
version "4.2.0"
Expand Down

0 comments on commit b0b22ca

Please sign in to comment.