Skip to content

Commit

Permalink
Merge pull request #10 from lifeomic/condition-in-transaction
Browse files Browse the repository at this point in the history
feat: add conditionTransact
  • Loading branch information
aecorredor authored Aug 31, 2023
2 parents 3c404b5 + 728a2d4 commit cc8fd99
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 8 deletions.
47 changes: 46 additions & 1 deletion src/dynamo-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('DynamoTable', () => {
);
const transactionManager = new TransactionManager(dynamo.documentClient);

expect.assertions(6);
expect.assertions(9);

const user = await userTable.put({
id: 'user-1',
Expand Down Expand Up @@ -103,5 +103,50 @@ describe('DynamoTable', () => {
// still has the same account.
expect(user1?.account).toBe('account-2');
expect(user2?.account).toBe('account-1');

// Test that a transaction with an unmet condition does not make any
// changes.
await expect(async () => {
await transactionManager.run((transaction) => {
// The first two actions should not go through because the condition
// check will fail.
userTable.deleteTransact({ id: 'user-1' }, { transaction });

userTable.patchTransact(
{ id: 'user-2' },
{
set: {
account: 'account-3',
},
},
{ transaction },
);

// This is the operation that causes the transaction to fail.
userTable.conditionTransact(
{ id: 'user-1' },
{
// This condition causes a failure, since user 1 is assigned to
// account 2.
condition: { equals: { account: 'account-1' } },
transaction,
},
);
});
}).rejects.toThrow(
new Error(
'Transaction cancelled, please refer cancellation reasons for specific reasons [None, None, ConditionalCheckFailed]',
),
);

[user1, user2] = await Promise.all([
userTable.get({ id: 'user-1' }),
userTable.get({ id: 'user-2' }),
]);

// One more time, validate that the first user is still present, and that
// the second user still has the same account.
expect(user1?.account).toBe('account-2');
expect(user2?.account).toBe('account-1');
});
});
33 changes: 26 additions & 7 deletions src/dynamo-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
import { z } from 'zod';
import _pick from 'lodash/pick';
import retry from 'async-retry';
import { SetRequired } from 'type-fest';

import {
DynamoDBCondition,
Expand Down Expand Up @@ -80,16 +81,13 @@ export type PatchOptions<Item> = BaseWriteOptions<Item> & {
transaction?: undefined;
};

export type PatchOptionsTransact<Item> = BaseWriteOptions<Item> &
export type BaseWriteOptionsTransact<Item> = BaseWriteOptions<Item> &
BaseTransactOptions;

export type DeleteOptions<Item> = BaseWriteOptions<Item> & {
transaction?: undefined;
};

export type DeleteOptionsTransact<Item> = BaseWriteOptions<Item> &
BaseTransactOptions;

/* Types for particular methods */
export type QueryOptions = {
/** The maximum number of records to retrieve. */
Expand Down Expand Up @@ -175,7 +173,7 @@ export class DynamoTable<
patch: PatchObject<Schema>,
options?:
| PatchOptions<z.infer<Schema>>
| PatchOptionsTransact<z.infer<Schema>>,
| BaseWriteOptionsTransact<z.infer<Schema>>,
) {
return {
...serializeUpdate({
Expand Down Expand Up @@ -271,7 +269,7 @@ export class DynamoTable<

deleteTransact(
key: Required<CompleteKeyForIndex<z.infer<Schema>, Config['keys']>>,
options: DeleteOptionsTransact<z.infer<Schema>>,
options: BaseWriteOptionsTransact<z.infer<Schema>>,
): void {
options.transaction.addWrite({
Delete: this.getDelete(key, options),
Expand Down Expand Up @@ -431,13 +429,34 @@ export class DynamoTable<
patchTransact(
key: Required<CompleteKeyForIndex<z.infer<Schema>, Config['keys']>>,
patch: PatchObject<Schema>,
options: PatchOptionsTransact<z.infer<Schema>>,
options: BaseWriteOptionsTransact<z.infer<Schema>>,
): void {
options.transaction.addWrite({
Update: this.getPatch(key, patch, options),
});
}

conditionTransact(
key: Required<CompleteKeyForIndex<z.infer<Schema>, Config['keys']>>,
options: SetRequired<
BaseWriteOptionsTransact<z.infer<Schema>>,
'condition'
>,
): void {
const serializedCondition = serializeCondition(options.condition);

options.transaction.addWrite({
ConditionCheck: {
ConditionExpression: serializedCondition.ConditionExpression,
ExpressionAttributeNames: serializedCondition.ExpressionAttributeNames,
ExpressionAttributeValues:
serializedCondition.ExpressionAttributeValues,
Key: key,
TableName: this.config.tableName,
},
});
}

/**
* Performs a guaranteed "strict" update on the specified item. Guarantees
* that the modification described by the `calculate` function will be applied
Expand Down

0 comments on commit cc8fd99

Please sign in to comment.