Skip to content

Commit

Permalink
Change update to partial
Browse files Browse the repository at this point in the history
  • Loading branch information
SethO committed Nov 9, 2023
1 parent 7e5897d commit 1758df9
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 242 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,33 @@ await getAllItems();
await myRepo.remove(id);
```

## Update

- Requires `dynamodb:UpdateItem`
- Honors revision check; it will only update if the revision on disk is the one you are updating. Will return a `409` if the revision has changed underneath you.
- Will perform a partial update if you don't pass in all properties. Think of this as a "patch" vs. a replacement update. The `key` and `revision` properties are always required.
- Returns the entire entity, including both new and unchanged properties

```js
const person = await myRepo.create({
name: 'Joe',
age: 28,
favoriteColor: 'blue',
});
// Full item update
person.favoriteColor = 'teal';
const newPerson1 = await myRepo.update(person);
console.log(newPerson1.favoriteColor); // 'teal'

// Partial update
const partial = {
favoriteColor: 'aquamarine',
key: person.key,
revision: newPerson1.revision,
};
const newPerson2 = await myRepo.update(partial);
console.log(newPerson2.favoriteColor); // 'aquamarine'
console.log(newPerson2.age); // 28
```

// More Coming Soon...
57 changes: 3 additions & 54 deletions lib/keyValueRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
DynamoDBDocumentClient,
GetCommand,
PutCommand,
PutCommandInput,
ScanCommand,
ScanCommandInput,
ScanCommandOutput,
Expand Down Expand Up @@ -117,49 +116,7 @@ class KeyValueRepository {
return itemToSave;
}

async update(item: any) {
this.validateRequiredProperties(item);
const { revision: previousRevision } = item;
const itemToSave = setRepositoryModifiedProperties(item);
const putParams: PutCommandInput = {
TableName: this.tableName,
Item: itemToSave,
ConditionExpression: 'attribute_exists(#key) AND #revision = :prevRev',
ExpressionAttributeNames: {
'#key': this.keyName,
'#revision': 'revision',
},
ExpressionAttributeValues: {
':prevRev': previousRevision,
},
ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
};
try {
await this.docClient.send(new PutCommand(putParams));
} catch (err: any) {
if (err.name === 'ConditionalCheckFailedException') {
const { Item } = err;
if (isNotFoundConflict(Item)) {
throw NotFound();
}
if (
isRevisionConflict({
expectedRevision: previousRevision,
actualRevision: Item?.revision?.N,
})
) {
throw Conflict(
`Conflict: Item in DB has revision [${Item?.revision?.N}]. You are using revision [${previousRevision}]`,
);
}
}
throw err;
}

return itemToSave;
}

async updatePartial(item: any): Promise<Record<string, any>> {
async update(item: any): Promise<Record<string, any>> {
this.validateRequiredProperties(item);
const updateInput = this.buildUpdateCommandInput(item);
let result: UpdateCommandOutput;
Expand Down Expand Up @@ -189,7 +146,7 @@ class KeyValueRepository {

private buildUpdateCommandInput(item: any): UpdateCommandInput {
const { revision: previousRevision } = item;
const itemToSave = setRepositoryModifiedPropertiesForPartialUpdate(item);
const itemToSave = setRepositoryModifiedPropertiesForUpdate(item);
const key = createDynamoDbKey({ keyName: this.keyName, keyValue: itemToSave[this.keyName] });
const updateInput: UpdateCommandInput = {
TableName: this.tableName,
Expand Down Expand Up @@ -230,15 +187,7 @@ const isRevisionConflict = (input: { expectedRevision: number; actualRevision: n
return expectedRevision !== actualRevision;
};

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

return returnItem;
};

const setRepositoryModifiedPropertiesForPartialUpdate = (item: any) => {
const setRepositoryModifiedPropertiesForUpdate = (item: any) => {
const returnItem = { ...item };
delete returnItem.createdAt;
returnItem.updatedAt = new Date().toISOString();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@setho/dynamodb-repository",
"version": "0.8.7",
"version": "0.9.0",
"description": "DynamoDB repository for hash-key and hash-key/range indexed tables. Designed for Lambda use. Handles nice-to-haves like created and updated timestamps and default id creation.",
"license": "MIT",
"repository": "setho/dynamodb-repository",
Expand Down
45 changes: 44 additions & 1 deletion test/update.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,32 @@ describe('When updating an item', () => {
const repo = new KeyValueRepository({ tableName: TableName, keyName: KeyName, documentClient });
const newField1 = faker.lorem.slug();

// ACT
await repo.update({ ...item, field1: newField1 });

// ASSERT
const itemInDb = await repo.get(key);
expect(itemInDb.field1).toEqual(newField1);
});

it('should return the entire object, including unchanged properties', async () => {
// ARRANGE
const item = createTestKeyValueItem();
const key = await insertHashKeyItem(item);
testKeys.push(key);
const repo = new KeyValueRepository({ tableName: TableName, keyName: KeyName, documentClient });
const newField1 = faker.lorem.slug();

// ACT
const result = await repo.update({ ...item, field1: newField1 });

// ASSERT
expect(result.map1).toEqual(item.map1);
expect(result.field1).not.toEqual(item.field1);
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 Expand Up @@ -158,4 +176,29 @@ describe('When updating an item', () => {
await expect(updateAction()).rejects.toThrow(`${item.revision}`);
});
});

describe('with a subset of properties', () => {
it('should return the entire object with the updated properties', async () => {
// ARRANGE
const item = createTestKeyValueItem();
const key = await insertHashKeyItem(item);
testKeys.push(key);
const itemWithoutField1: any = { ...item };
delete itemWithoutField1.field1;
const newMap = { field2: 'affirmative' };
itemWithoutField1.map1 = newMap;
const repo = new KeyValueRepository({
tableName: TableName,
keyName: KeyName,
documentClient,
});

// ACT
const result = await repo.update(itemWithoutField1);

// ASSERT
expect(result.map1).toEqual(newMap);
expect(result.field1).toEqual(item.field1);
});
});
});
186 changes: 0 additions & 186 deletions test/updatePartial.int.test.ts

This file was deleted.

0 comments on commit 1758df9

Please sign in to comment.