Skip to content

Commit

Permalink
Start work on partial update builder, adding int tests
Browse files Browse the repository at this point in the history
  • Loading branch information
SethO committed Oct 28, 2023
1 parent a4f3fb3 commit 9f568e3
Show file tree
Hide file tree
Showing 6 changed files with 404 additions and 93 deletions.
63 changes: 58 additions & 5 deletions lib/keyValueRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import {
ScanCommand,
ScanCommandInput,
ScanCommandOutput,
UpdateCommand,
UpdateCommandInput,
} from '@aws-sdk/lib-dynamodb';

import { ConstructorArgs, IdOptions } from './types';
import keyValueRepoConstructor from './validator';
import { createCursor, parseCursor, createId } from './utils';
import { createCursor, parseCursor, createId, createDynamoDbKey } from './utils';
import { UpdateExpressionsBuilder } from './updateExpressionBuilder';

class KeyValueRepository {
private tableName: string;
Expand All @@ -23,6 +26,8 @@ class KeyValueRepository {

private docClient: DynamoDBDocumentClient;

private updateExpressionsBuilder: UpdateExpressionsBuilder;

/**
* Create a HashKey Repository
* @param {Object} param - The constructor parameter
Expand All @@ -39,10 +44,11 @@ class KeyValueRepository {
this.keyName = keyName;
this.idOptions = idOptions;
this.docClient = documentClient;
this.updateExpressionsBuilder = new UpdateExpressionsBuilder(keyName);
}

async get(hashKey: string) {
const key = { [this.keyName]: hashKey };
const key = createDynamoDbKey({ keyName: this.keyName, keyValue: hashKey });
const getParams = {
TableName: this.tableName,
Key: key,
Expand Down Expand Up @@ -85,7 +91,7 @@ class KeyValueRepository {
}

async remove(hashKey: string) {
const key = { [this.keyName]: hashKey };
const key = createDynamoDbKey({ keyName: this.keyName, keyValue: hashKey });
const deleteParams = {
TableName: this.tableName,
Key: key,
Expand All @@ -112,14 +118,15 @@ class KeyValueRepository {

async update(item: any) {
validateHashKeyPropertyExists({ item, keyName: this.keyName });
const itemToSave = setRepositoryModifiedProperties(item);
const { revision: previousRevision } = item;
const itemToSave = setRepositoryModifiedProperties(item);
const putParams: PutCommandInput = {
TableName: this.tableName,
Item: itemToSave,
ConditionExpression: 'attribute_exists(#key) AND revision = :prevRev',
ConditionExpression: 'attribute_exists(#key) AND #revision = :prevRev',
ExpressionAttributeNames: {
'#key': this.keyName,
'#revision': 'revision',
},
ExpressionAttributeValues: {
':prevRev': previousRevision,
Expand Down Expand Up @@ -150,6 +157,52 @@ class KeyValueRepository {

return itemToSave;
}

async updatePartial(item: 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',
};
try {
await this.docClient.send(new UpdateCommand(updateInput));
} 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;
}
}

const isNotFoundConflict = (itemFromError?: any) => !itemFromError;
Expand Down
95 changes: 69 additions & 26 deletions lib/updateExpressionBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,77 @@
export type ExpressionBuilderResult = {
UpdateExpression: string;
ExpressionAttributeNames: any;
ExpressionAttributeValues: any;
updateExpression: string;
expressionAttributeNames: { [key: string]: string };
expressionAttributeValues: { [key: string]: any };
};
export const updateExpressionBuilder = (item: any): ExpressionBuilderResult => ({
UpdateExpression: internalUpdateBuilder(item),
ExpressionAttributeNames: internalNameBuilder(item),
ExpressionAttributeValues: internalValueBuilder(item),
});

const internalUpdateBuilder = (item: any): string => {
const itemKeys = Object.keys(item);
export class UpdateExpressionsBuilder {
private keyName: string;

return `SET ${itemKeys.map((k, index) => `#field_${index} = :value_${index}`).join(', ')}`;
};
constructor(keyName: string) {
this.keyName = keyName;
}

const internalNameBuilder = (item: any): any => {
const itemKeys = Object.keys(item);
buildExpressionNames(item: any): { [key: string]: string } {
const keylessItem = this.removeKey(item);
const itemKeys = Object.keys(keylessItem);

return itemKeys.reduce(
(accumulator, k, index) => ({ ...accumulator, [`#field_${index}`]: k }),
{},
);
};
return itemKeys.reduce(
(accumulator, k, index) => ({ ...accumulator, [`#prop_${index}`]: k }),
{},
);
}

const internalValueBuilder = (item: any): any => {
const itemKeys = Object.keys(item);
buildExpressionValues(item: any): { [key: string]: any } {
const keylessItem = this.removeKey(item);
const itemKeys = Object.keys(keylessItem);

return itemKeys.reduce(
(accumulator, k, index) => ({ ...accumulator, [`:value_${index}`]: item[k] }),
{},
);
};
return itemKeys.reduce(
(accumulator, k, index) => ({ ...accumulator, [`:value_${index}`]: item[k] }),
{},
);
}

buildUpdateExpression(item: any) {
const keylessItem = this.removeKey(item);
const itemKeys = Object.keys(keylessItem);

return `SET ${itemKeys.map((k, index) => `#prop_${index} = :value_${index}`).join(', ')}`;
}

private removeKey(item: any) {
const itemCopy = { ...item };
delete itemCopy[this.keyName];

return itemCopy;
}
}

// export const updateExpressionBuilder = (item: any): ExpressionBuilderResult => ({
// updateExpression: internalUpdateBuilder(item),
// expressionAttributeNames: internalNameBuilder(item),
// expressionAttributeValues: internalValueBuilder(item),
// });

// const internalUpdateBuilder = (item: any): string => {
// const itemKeys = Object.keys(item);

// return `SET ${itemKeys.map((k, index) => `#prop_${index} = :value_${index}`).join(', ')}`;
// };

// const internalNameBuilder = (item: any): any => {
// const itemKeys = Object.keys(item);

// return itemKeys.reduce(
// (accumulator, k, index) => ({ ...accumulator, [`#prop_${index}`]: k }),
// {},
// );
// };

// const internalValueBuilder = (item: any): any => {
// const itemKeys = Object.keys(item);

// return itemKeys.reduce(
// (accumulator, k, index) => ({ ...accumulator, [`:value_${index}`]: item[k] }),
// {},
// );
// };
13 changes: 10 additions & 3 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ulid } from "ulid";
import { ulid } from 'ulid';
import { BadRequest } from 'http-errors';

export const createCursor = (lastEvaluatedKey: Record<string, any> | string) => (Buffer.from(JSON.stringify(lastEvaluatedKey)).toString('base64'));
export const createCursor = (lastEvaluatedKey: Record<string, any> | string) =>
Buffer.from(JSON.stringify(lastEvaluatedKey)).toString('base64');

export const parseCursor = (cursor: string) => {
let result;
Expand All @@ -13,4 +14,10 @@ export const parseCursor = (cursor: string) => {
return result;
};

export const createId = async ({ prefix = '' } = {}) => (`${prefix}${ulid()}`);
export const createId = async ({ prefix = '' } = {}) => `${prefix}${ulid()}`;

export const createDynamoDbKey = (input: { keyName: string; keyValue: any }) => {
const { keyName, keyValue } = input;

return { [keyName]: keyValue };
};
3 changes: 2 additions & 1 deletion test/update.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ describe('When updating an item', () => {
expect(result.field1).toEqual(newField1);
});

it('should maintain original createdAt value', async () => {
it.skip('should maintain original createdAt value', async () => {
// ARRANGE
const item = createTestKeyValueItem();
const originalCreatedAt = item.createdAt;
const key = await insertHashKeyItem(item);
testKeys.push(key);
const repo = new KeyValueRepository({ tableName: TableName, keyName: KeyName, documentClient });
item.createdAt = faker.date.past().toISOString();

// ACT
const result = await repo.update(item);
Expand Down
Loading

0 comments on commit 9f568e3

Please sign in to comment.