Skip to content

Commit

Permalink
Disallow change to createdAt in partial update
Browse files Browse the repository at this point in the history
  • Loading branch information
SethO committed Oct 28, 2023
1 parent 9f568e3 commit be1d242
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 25 deletions.
65 changes: 41 additions & 24 deletions lib/keyValueRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ScanCommandOutput,
UpdateCommand,
UpdateCommandInput,
UpdateCommandOutput,
} from '@aws-sdk/lib-dynamodb';

import { ConstructorArgs, IdOptions } from './types';
Expand Down Expand Up @@ -158,29 +159,12 @@ class KeyValueRepository {
return itemToSave;
}

async updatePartial(item: any) {
async updatePartial(item: any): Promise<Record<string, any>> {
validateHashKeyPropertyExists({ item, keyName: this.keyName });
const { revision: previousRevision } = item;
const itemToSave = setRepositoryModifiedProperties(item);
const key = createDynamoDbKey({ keyName: this.keyName, keyValue: itemToSave[this.keyName] });
const updateInput: UpdateCommandInput = {
TableName: this.tableName,
Key: key,
ConditionExpression: 'attribute_exists(#key) AND #revision = :prevRev',
UpdateExpression: this.updateExpressionsBuilder.buildUpdateExpression(itemToSave),
ExpressionAttributeNames: {
'#key': this.keyName,
'#revision': 'revision',
...this.updateExpressionsBuilder.buildExpressionNames(itemToSave),
},
ExpressionAttributeValues: {
':prevRev': previousRevision,
...this.updateExpressionsBuilder.buildExpressionValues(itemToSave),
},
ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
};
const updateInput = this.buildUpdateCommandInput(item);
let result: UpdateCommandOutput;
try {
await this.docClient.send(new UpdateCommand(updateInput));
result = await this.docClient.send(new UpdateCommand(updateInput));
} catch (err: any) {
if (err.name === 'ConditionalCheckFailedException') {
const { Item } = err;
Expand All @@ -189,19 +173,43 @@ class KeyValueRepository {
}
if (
isRevisionConflict({
expectedRevision: previousRevision,
expectedRevision: item.revision,
actualRevision: Item?.revision?.N,
})
) {
throw Conflict(
`Conflict: Item in DB has revision [${Item?.revision?.N}]. You are using revision [${previousRevision}]`,
`Conflict: Item in DB has revision [${Item?.revision?.N}]. You are using revision [${item.revision}]`,
);
}
}
throw err;
}
return result.Attributes || {};
}

return itemToSave;
private buildUpdateCommandInput(item: any): UpdateCommandInput {
const { revision: previousRevision } = item;
const itemToSave = setRepositoryModifiedPropertiesForPartialUpdate(item);
const key = createDynamoDbKey({ keyName: this.keyName, keyValue: itemToSave[this.keyName] });
const updateInput: UpdateCommandInput = {
TableName: this.tableName,
Key: key,
ConditionExpression: 'attribute_exists(#key) AND #revision = :prevRev',
UpdateExpression: this.updateExpressionsBuilder.buildUpdateExpression(itemToSave),
ExpressionAttributeNames: {
'#key': this.keyName,
'#revision': 'revision',
...this.updateExpressionsBuilder.buildExpressionNames(itemToSave),
},
ExpressionAttributeValues: {
':prevRev': previousRevision,
...this.updateExpressionsBuilder.buildExpressionValues(itemToSave),
},
ReturnValues: 'ALL_NEW',
ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
};

return updateInput;
}
}

Expand All @@ -228,4 +236,13 @@ const setRepositoryModifiedProperties = (item: any) => {
return returnItem;
};

const setRepositoryModifiedPropertiesForPartialUpdate = (item: any) => {
const returnItem = { ...item };
delete returnItem.createdAt;
returnItem.updatedAt = new Date().toISOString();
returnItem.revision = item.revision + 1;

return returnItem;
};

export default KeyValueRepository;
2 changes: 1 addition & 1 deletion test/updatePartial.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('When updating an item', () => {
expect(result.field1).toEqual(newField1);
});

it.skip('should maintain original createdAt value', async () => {
it('should maintain original createdAt value', async () => {
// ARRANGE
const item = createTestKeyValueItem();
const originalCreatedAt = item.createdAt;
Expand Down

0 comments on commit be1d242

Please sign in to comment.