Skip to content

Commit 902aa5d

Browse files
feat: added branch to handle conditional check failure
1 parent a51ab39 commit 902aa5d

File tree

2 files changed

+84
-6
lines changed

2 files changed

+84
-6
lines changed

packages/idempotency/src/persistence/DynamoDbPersistenceLayer.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-empty-function */
2-
import { DynamoDB } from '@aws-sdk/client-dynamodb';
2+
import { DynamoDB, DynamoDBServiceException } from '@aws-sdk/client-dynamodb';
33
import { DynamoDBDocument, GetCommandOutput } from '@aws-sdk/lib-dynamodb';
4-
import { IdempotencyItemNotFoundError } from '../Exceptions';
4+
import { IdempotencyItemAlreadyExistsError, IdempotencyItemNotFoundError } from '../Exceptions';
55
import { IdempotencyRecordStatus } from '../types/IdempotencyRecordStatus';
66
import { IdempotencyRecord, PersistenceLayer } from './PersistenceLayer';
77

@@ -46,7 +46,15 @@ class DynamoDBPersistenceLayer extends PersistenceLayer {
4646
const idempotencyKeyExpired = '#expiry < :now';
4747
const notInProgress = 'NOT #status = :inprogress';
4848
const conditionalExpression = `${idempotencyKeyDoesNotExist} OR ${idempotencyKeyExpired} OR ${notInProgress}`;
49-
await table.put({ TableName: this.tableName, Item: item, ExpressionAttributeNames: { '#id': this.key_attr, '#expiry': this.expiry_attr, '#status': this.status_attr }, ExpressionAttributeValues: { ':now': Date.now(), ':inprogress': IdempotencyRecordStatus.INPROGRESS }, ConditionExpression: conditionalExpression });
49+
try {
50+
await table.put({ TableName: this.tableName, Item: item, ExpressionAttributeNames: { '#id': this.key_attr, '#expiry': this.expiry_attr, '#status': this.status_attr }, ExpressionAttributeValues: { ':now': Date.now(), ':inprogress': IdempotencyRecordStatus.INPROGRESS }, ConditionExpression: conditionalExpression });
51+
} catch (e){
52+
if ((e as DynamoDBServiceException).name === 'ConditionalCheckFailedException'){
53+
throw new IdempotencyItemAlreadyExistsError();
54+
}
55+
56+
throw e;
57+
}
5058
}
5159

5260
protected async _updateRecord(record: IdempotencyRecord): Promise<void> {

packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DeleteCommand, DynamoDBDocument, GetCommand, PutCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';
22
import { mockClient } from 'aws-sdk-client-mock';
33
import 'aws-sdk-client-mock-jest';
4-
import { IdempotencyItemNotFoundError } from '../../../src/Exceptions';
4+
import { IdempotencyItemAlreadyExistsError, IdempotencyItemNotFoundError } from '../../../src/Exceptions';
55
import { DynamoDBPersistenceLayer } from '../../../src/persistence/DynamoDbPersistenceLayer';
66
import { IdempotencyRecord } from '../../../src/persistence/IdempotencyRecord';
77
import { IdempotencyRecordStatus } from '../../../src/types/IdempotencyRecordStatus';
@@ -36,7 +36,7 @@ describe('Class: DynamoDbPersistenceLayer', () => {
3636
});
3737

3838
describe('Method: _putRecord', () => {
39-
test('when called with a record that succeeds condition, it puts record in dynamo table', () => {
39+
test('when called with a record that succeeds condition, it puts record in dynamo table', async () => {
4040
// Prepare
4141
const tableName = 'tableName';
4242
const persistenceLayer = new TestDynamoPersistenceLayer(tableName);
@@ -53,7 +53,7 @@ describe('Class: DynamoDbPersistenceLayer', () => {
5353
const dynamoClient = mockClient(DynamoDBDocument).on(PutCommand).resolves({});
5454

5555
// Act
56-
persistenceLayer._putRecord(record);
56+
await persistenceLayer._putRecord(record);
5757

5858
// Assess
5959
expect(dynamoClient).toReceiveCommandWith(PutCommand, {
@@ -64,6 +64,76 @@ describe('Class: DynamoDbPersistenceLayer', () => {
6464
ConditionExpression: 'attribute_not_exists(#id) OR #expiry < :now OR NOT #status = :inprogress'
6565
});
6666
});
67+
68+
test('when called with a record that fails condition, it throws IdempotencyItemAlreadyExistsError', async () => {
69+
// Prepare
70+
const tableName = 'tableName';
71+
const persistenceLayer = new TestDynamoPersistenceLayer(tableName);
72+
73+
const key = 'key';
74+
const status = IdempotencyRecordStatus.EXPIRED;
75+
const expiryTimestamp = 0;
76+
const inProgressExpiryTimestamp = 0;
77+
const record = new IdempotencyRecord(key, status, expiryTimestamp, inProgressExpiryTimestamp, undefined, undefined);
78+
79+
const currentDate = 1;
80+
jest.spyOn(Date, 'now').mockReturnValue(currentDate);
81+
82+
const dynamoClient = mockClient(DynamoDBDocument).on(PutCommand).rejects({ name: 'ConditionalCheckFailedException' });
83+
84+
// Act
85+
let error: unknown;
86+
try {
87+
await persistenceLayer._putRecord(record);
88+
} catch (e){
89+
error = e;
90+
}
91+
92+
// Assess
93+
expect(dynamoClient).toReceiveCommandWith(PutCommand, {
94+
TableName: tableName,
95+
Item: { 'id': key, 'expiration': expiryTimestamp, status: status },
96+
ExpressionAttributeNames: { '#id': 'id', '#expiry': 'expiration', '#status': 'status' },
97+
ExpressionAttributeValues: { ':now': currentDate, ':inprogress': IdempotencyRecordStatus.INPROGRESS },
98+
ConditionExpression: 'attribute_not_exists(#id) OR #expiry < :now OR NOT #status = :inprogress'
99+
});
100+
expect(error).toBeInstanceOf(IdempotencyItemAlreadyExistsError);
101+
});
102+
103+
test('when encountering an unknown error, it throws the causing error', async () => {
104+
// Prepare
105+
const tableName = 'tableName';
106+
const persistenceLayer = new TestDynamoPersistenceLayer(tableName);
107+
108+
const key = 'key';
109+
const status = IdempotencyRecordStatus.EXPIRED;
110+
const expiryTimestamp = 0;
111+
const inProgressExpiryTimestamp = 0;
112+
const record = new IdempotencyRecord(key, status, expiryTimestamp, inProgressExpiryTimestamp, undefined, undefined);
113+
114+
const currentDate = 1;
115+
jest.spyOn(Date, 'now').mockReturnValue(currentDate);
116+
117+
const dynamoClient = mockClient(DynamoDBDocument).on(PutCommand).rejects(new Error());
118+
119+
// Act
120+
let error: unknown;
121+
try {
122+
await persistenceLayer._putRecord(record);
123+
} catch (e){
124+
error = e;
125+
}
126+
127+
// Assess
128+
expect(dynamoClient).toReceiveCommandWith(PutCommand, {
129+
TableName: tableName,
130+
Item: { 'id': key, 'expiration': expiryTimestamp, status: status },
131+
ExpressionAttributeNames: { '#id': 'id', '#expiry': 'expiration', '#status': 'status' },
132+
ExpressionAttributeValues: { ':now': currentDate, ':inprogress': IdempotencyRecordStatus.INPROGRESS },
133+
ConditionExpression: 'attribute_not_exists(#id) OR #expiry < :now OR NOT #status = :inprogress'
134+
});
135+
expect(error).toBe(error);
136+
});
67137
});
68138

69139
describe('Method: _getRecord', () => {

0 commit comments

Comments
 (0)