@ftschopp/dynatable-core 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +19 -2
- package/dist/builders/update/create-update-builder.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.js +25 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/utils/model-utils.d.ts +9 -3
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +15 -7
- package/package.json +19 -2
- package/eslint.config.mjs +0 -4
- package/jest.config.js +0 -11
- package/src/builders/README.md +0 -339
- package/src/builders/batch-get/README.md +0 -43
- package/src/builders/batch-get/create-batch-get-builder.test.ts +0 -193
- package/src/builders/batch-get/create-batch-get-builder.ts +0 -108
- package/src/builders/batch-get/index.ts +0 -2
- package/src/builders/batch-get/types.ts +0 -28
- package/src/builders/batch-write/README.md +0 -204
- package/src/builders/batch-write/create-batch-write-builder.test.ts +0 -173
- package/src/builders/batch-write/create-batch-write-builder.ts +0 -49
- package/src/builders/batch-write/index.ts +0 -2
- package/src/builders/batch-write/types.ts +0 -33
- package/src/builders/delete/create-delete-builder.test.ts +0 -294
- package/src/builders/delete/create-delete-builder.ts +0 -100
- package/src/builders/delete/index.ts +0 -2
- package/src/builders/delete/types.ts +0 -17
- package/src/builders/get/create-get-builder.test.ts +0 -272
- package/src/builders/get/create-get-builder.ts +0 -117
- package/src/builders/get/index.ts +0 -2
- package/src/builders/get/types.ts +0 -39
- package/src/builders/index.ts +0 -14
- package/src/builders/put/create-put-builder.test.ts +0 -213
- package/src/builders/put/create-put-builder.ts +0 -160
- package/src/builders/put/index.ts +0 -2
- package/src/builders/put/types.ts +0 -24
- package/src/builders/query/create-query-builder.test.ts +0 -770
- package/src/builders/query/create-query-builder.ts +0 -455
- package/src/builders/query/index.ts +0 -2
- package/src/builders/query/types.ts +0 -109
- package/src/builders/scan/create-scan-builder.test.ts +0 -416
- package/src/builders/scan/create-scan-builder.ts +0 -265
- package/src/builders/scan/index.ts +0 -2
- package/src/builders/scan/types.ts +0 -88
- package/src/builders/shared/conditions.ts +0 -58
- package/src/builders/shared/index.ts +0 -4
- package/src/builders/shared/operators.test.ts +0 -270
- package/src/builders/shared/operators.ts +0 -208
- package/src/builders/shared/projection.ts +0 -28
- package/src/builders/shared/types.ts +0 -101
- package/src/builders/transact-get/README.md +0 -167
- package/src/builders/transact-get/create-transact-get-builder.test.ts +0 -239
- package/src/builders/transact-get/create-transact-get-builder.ts +0 -67
- package/src/builders/transact-get/index.ts +0 -2
- package/src/builders/transact-get/types.ts +0 -31
- package/src/builders/transact-write/README.md +0 -202
- package/src/builders/transact-write/create-transact-write-builder.test.ts +0 -288
- package/src/builders/transact-write/create-transact-write-builder.ts +0 -124
- package/src/builders/transact-write/index.ts +0 -2
- package/src/builders/transact-write/types.ts +0 -71
- package/src/builders/update/create-update-builder.test.ts +0 -573
- package/src/builders/update/create-update-builder.ts +0 -375
- package/src/builders/update/index.ts +0 -2
- package/src/builders/update/types.ts +0 -63
- package/src/core/types.test.ts +0 -641
- package/src/core/types.ts +0 -366
- package/src/entity/create-entity-api.ts +0 -224
- package/src/entity/index.ts +0 -19
- package/src/entity/middleware/factories.ts +0 -15
- package/src/entity/middleware/types.ts +0 -7
- package/src/entity/middleware/with-middleware.ts +0 -37
- package/src/entity/types.ts +0 -79
- package/src/entity/validation/key-validation.ts +0 -34
- package/src/index.ts +0 -23
- package/src/table.ts +0 -116
- package/src/utils/dynamodb-logger.test.ts +0 -246
- package/src/utils/dynamodb-logger.ts +0 -175
- package/src/utils/model-utils.test.ts +0 -452
- package/src/utils/model-utils.ts +0 -188
- package/src/utils/zod-utils.test.ts +0 -363
- package/src/utils/zod-utils.ts +0 -56
- package/tests/integration/instagram-clone.integration.test.ts +0 -1019
- package/tests/integration/pagination-timestamps.integration.test.ts +0 -374
- package/tests/integration/transactions.integration.test.ts +0 -528
- package/tsconfig.json +0 -12
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
# TransactWrite - Atomic Multi-Item Writes
|
|
2
|
-
|
|
3
|
-
TransactWrite provides atomic multi-item write operations in DynamoDB. All operations in a transaction either succeed together or fail together.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Functional API**: Immutable builders using pure functions
|
|
8
|
-
- **Type-safe**: Full TypeScript support with AWS SDK types
|
|
9
|
-
- **Composable**: Chain operations fluently
|
|
10
|
-
- **Idempotent**: Support for client request tokens
|
|
11
|
-
- **Flexible**: Accepts both entity builder outputs and raw DynamoDB parameters
|
|
12
|
-
|
|
13
|
-
## Usage
|
|
14
|
-
|
|
15
|
-
### Basic Example
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
import { createTransactWriteBuilder } from '@repo/core';
|
|
19
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
20
|
-
|
|
21
|
-
const client = new DynamoDBClient({});
|
|
22
|
-
|
|
23
|
-
await createTransactWriteBuilder(client).addPut(putParams).addUpdate(updateParams).execute();
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### With Table API
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
const table = new Table({
|
|
30
|
-
name: 'MyTable',
|
|
31
|
-
client: new DynamoDBClient({}),
|
|
32
|
-
schema: MySchema,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Like a photo atomically
|
|
36
|
-
await table
|
|
37
|
-
.transactWrite()
|
|
38
|
-
.addPut(
|
|
39
|
-
table.entities.Like.put({
|
|
40
|
-
photoId: '123',
|
|
41
|
-
likingUsername: 'alice',
|
|
42
|
-
})
|
|
43
|
-
.ifNotExists()
|
|
44
|
-
.dbParams()
|
|
45
|
-
)
|
|
46
|
-
.addUpdate(
|
|
47
|
-
table.entities.Photo.update({
|
|
48
|
-
username: 'bob',
|
|
49
|
-
photoId: '123',
|
|
50
|
-
})
|
|
51
|
-
.add('likesCount', 1)
|
|
52
|
-
.dbParams()
|
|
53
|
-
)
|
|
54
|
-
.execute();
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Real-World Examples
|
|
58
|
-
|
|
59
|
-
### Follow a User
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
await table
|
|
63
|
-
.transactWrite()
|
|
64
|
-
.addPut(
|
|
65
|
-
table.entities.Follow.put({
|
|
66
|
-
followedUsername: 'alice',
|
|
67
|
-
followingUsername: 'bob',
|
|
68
|
-
})
|
|
69
|
-
.ifNotExists()
|
|
70
|
-
.dbParams()
|
|
71
|
-
)
|
|
72
|
-
.addUpdate(table.entities.User.update({ username: 'alice' }).add('followerCount', 1).dbParams())
|
|
73
|
-
.addUpdate(table.entities.User.update({ username: 'bob' }).add('followingCount', 1).dbParams())
|
|
74
|
-
.execute();
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Comment on Photo
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
await table
|
|
81
|
-
.transactWrite()
|
|
82
|
-
.addPut(
|
|
83
|
-
table.entities.Comment.put({
|
|
84
|
-
photoId: '123',
|
|
85
|
-
commentingUsername: 'charlie',
|
|
86
|
-
content: 'Great photo!',
|
|
87
|
-
}).dbParams()
|
|
88
|
-
)
|
|
89
|
-
.addUpdate(
|
|
90
|
-
table.entities.Photo.update({
|
|
91
|
-
username: 'alice',
|
|
92
|
-
photoId: '123',
|
|
93
|
-
})
|
|
94
|
-
.add('commentCount', 1)
|
|
95
|
-
.dbParams()
|
|
96
|
-
)
|
|
97
|
-
.execute();
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Delete with Safety Check
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// Only delete if photo has no engagement
|
|
104
|
-
await table
|
|
105
|
-
.transactWrite()
|
|
106
|
-
.addDelete(
|
|
107
|
-
table.entities.Photo.delete({
|
|
108
|
-
username: 'alice',
|
|
109
|
-
photoId: '123',
|
|
110
|
-
})
|
|
111
|
-
.where((attr, op) => op.and(op.eq(attr.likesCount, 0), op.eq(attr.commentCount, 0)))
|
|
112
|
-
.dbParams()
|
|
113
|
-
)
|
|
114
|
-
.execute();
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Condition Check
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
// Create user only if admin exists
|
|
121
|
-
await table
|
|
122
|
-
.transactWrite()
|
|
123
|
-
.addPut(
|
|
124
|
-
table.entities.User.put({
|
|
125
|
-
username: 'newuser',
|
|
126
|
-
name: 'New User',
|
|
127
|
-
})
|
|
128
|
-
.ifNotExists()
|
|
129
|
-
.dbParams()
|
|
130
|
-
)
|
|
131
|
-
.addConditionCheck({
|
|
132
|
-
TableName: 'MyTable',
|
|
133
|
-
Key: { pk: 'USER#admin', sk: 'USER#admin' },
|
|
134
|
-
ConditionExpression: 'attribute_exists(#pk)',
|
|
135
|
-
ExpressionAttributeNames: { '#pk': 'pk' },
|
|
136
|
-
})
|
|
137
|
-
.execute();
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Idempotency
|
|
141
|
-
|
|
142
|
-
Use client request tokens for idempotent operations:
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
await table.transactWrite().addPut(params).withClientRequestToken('unique-id-12345').execute();
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Type Safety
|
|
149
|
-
|
|
150
|
-
All operations are fully typed using AWS SDK types:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
import type {
|
|
154
|
-
TransactPutParams,
|
|
155
|
-
TransactUpdateParams,
|
|
156
|
-
TransactDeleteParams,
|
|
157
|
-
TransactConditionCheckParams,
|
|
158
|
-
} from '@ftschopp/dynatable-core';
|
|
159
|
-
|
|
160
|
-
// addPut accepts PutCommandInput from @aws-sdk/lib-dynamodb
|
|
161
|
-
const putParams: TransactPutParams = {
|
|
162
|
-
TableName: 'MyTable',
|
|
163
|
-
Item: { pk: 'USER#123', sk: 'USER#123', name: 'John' },
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// TypeScript validates all parameters at compile time
|
|
167
|
-
table.transactWrite().addPut(putParams).execute();
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Using Entity Builders
|
|
171
|
-
|
|
172
|
-
Entity builders automatically provide correctly typed parameters via `.dbParams()`:
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
// The entity builder's dbParams() returns PutCommandInput
|
|
176
|
-
const params = table.entities.User.put({ username: 'john' }).ifNotExists().dbParams();
|
|
177
|
-
// params is typed as PutCommandInput
|
|
178
|
-
|
|
179
|
-
// Pass it to the transaction builder
|
|
180
|
-
table.transactWrite().addPut(params).execute();
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Functional Design
|
|
184
|
-
|
|
185
|
-
The builder is immutable - each method returns a new instance:
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
const builder1 = table.transactWrite();
|
|
189
|
-
const builder2 = builder1.addPut(params1);
|
|
190
|
-
const builder3 = builder2.addPut(params2);
|
|
191
|
-
|
|
192
|
-
builder1.dbParams().TransactItems.length; // 0
|
|
193
|
-
builder2.dbParams().TransactItems.length; // 1
|
|
194
|
-
builder3.dbParams().TransactItems.length; // 2
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Limitations
|
|
198
|
-
|
|
199
|
-
- Maximum **100 items** per transaction
|
|
200
|
-
- Maximum **4 MB** total size
|
|
201
|
-
- Single table only
|
|
202
|
-
- May fail with `TransactionCanceledException` if conditions fail
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
-
import { createTransactWriteBuilder } from './create-transact-write-builder';
|
|
3
|
-
|
|
4
|
-
const client = new DynamoDBClient({});
|
|
5
|
-
|
|
6
|
-
describe('TransactWrite Builder - Functional API', () => {
|
|
7
|
-
test('should create empty transaction', () => {
|
|
8
|
-
const params = createTransactWriteBuilder(client).dbParams();
|
|
9
|
-
|
|
10
|
-
expect(params).toEqual({
|
|
11
|
-
TransactItems: [],
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('should add single Put operation', () => {
|
|
16
|
-
const putParams = {
|
|
17
|
-
TableName: 'InstagramClone',
|
|
18
|
-
Item: {
|
|
19
|
-
pk: 'USER#alice',
|
|
20
|
-
sk: 'USER#alice',
|
|
21
|
-
username: 'alice',
|
|
22
|
-
name: 'Alice Smith',
|
|
23
|
-
followerCount: 0,
|
|
24
|
-
followingCount: 0,
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const params = createTransactWriteBuilder(client).addPut(putParams).dbParams();
|
|
29
|
-
|
|
30
|
-
expect(params).toEqual({
|
|
31
|
-
TransactItems: [{ Put: putParams }],
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('should add single Update operation', () => {
|
|
36
|
-
const updateParams = {
|
|
37
|
-
TableName: 'InstagramClone',
|
|
38
|
-
Key: { pk: 'USER#alice', sk: 'USER#alice' },
|
|
39
|
-
UpdateExpression: 'ADD #followerCount :followerCount_0',
|
|
40
|
-
ExpressionAttributeNames: { '#followerCount': 'followerCount' },
|
|
41
|
-
ExpressionAttributeValues: { ':followerCount_0': 1 },
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const params = createTransactWriteBuilder(client).addUpdate(updateParams).dbParams();
|
|
45
|
-
|
|
46
|
-
expect(params).toEqual({
|
|
47
|
-
TransactItems: [{ Update: updateParams }],
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('should add single Delete operation', () => {
|
|
52
|
-
const deleteParams = {
|
|
53
|
-
TableName: 'InstagramClone',
|
|
54
|
-
Key: { pk: 'USER#alice', sk: 'USER#alice' },
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const params = createTransactWriteBuilder(client).addDelete(deleteParams).dbParams();
|
|
58
|
-
|
|
59
|
-
expect(params).toEqual({
|
|
60
|
-
TransactItems: [{ Delete: deleteParams }],
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('should add ConditionCheck operation', () => {
|
|
65
|
-
const conditionCheckParams = {
|
|
66
|
-
TableName: 'InstagramClone',
|
|
67
|
-
Key: { pk: 'USER#admin', sk: 'USER#admin' },
|
|
68
|
-
ConditionExpression: 'attribute_exists(#username)',
|
|
69
|
-
ExpressionAttributeNames: { '#username': 'username' },
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const params = createTransactWriteBuilder(client)
|
|
73
|
-
.addConditionCheck(conditionCheckParams)
|
|
74
|
-
.dbParams();
|
|
75
|
-
|
|
76
|
-
expect(params).toEqual({
|
|
77
|
-
TransactItems: [{ ConditionCheck: conditionCheckParams }],
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('should chain multiple operations - Like a Photo', () => {
|
|
82
|
-
// Like a Photo: Create Like + Increment Photo.likesCount
|
|
83
|
-
const putLikeParams = {
|
|
84
|
-
TableName: 'InstagramClone',
|
|
85
|
-
Item: {
|
|
86
|
-
pk: 'PL#photo123',
|
|
87
|
-
sk: 'LIKE#juanca',
|
|
88
|
-
photoId: 'photo123',
|
|
89
|
-
likingUsername: 'juanca',
|
|
90
|
-
likeId: '01K16ZP43BRX67DG50SHGZ11DS',
|
|
91
|
-
},
|
|
92
|
-
ConditionExpression: '(attribute_not_exists(#PK)) AND (attribute_not_exists(#SK))',
|
|
93
|
-
ExpressionAttributeNames: { '#PK': 'pk', '#SK': 'sk' },
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const updatePhotoParams = {
|
|
97
|
-
TableName: 'InstagramClone',
|
|
98
|
-
Key: { pk: 'UP#alice', sk: 'PHOTO#photo123' },
|
|
99
|
-
UpdateExpression: 'ADD #likesCount :likesCount_0',
|
|
100
|
-
ExpressionAttributeNames: { '#likesCount': 'likesCount' },
|
|
101
|
-
ExpressionAttributeValues: { ':likesCount_0': 1 },
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const params = createTransactWriteBuilder(client)
|
|
105
|
-
.addPut(putLikeParams)
|
|
106
|
-
.addUpdate(updatePhotoParams)
|
|
107
|
-
.dbParams();
|
|
108
|
-
|
|
109
|
-
expect(params).toEqual({
|
|
110
|
-
TransactItems: [{ Put: putLikeParams }, { Update: updatePhotoParams }],
|
|
111
|
-
});
|
|
112
|
-
expect(params.TransactItems).toHaveLength(2);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('should chain multiple operations - Follow a User', () => {
|
|
116
|
-
// Follow: Create Follow + Update both user counts
|
|
117
|
-
const putFollowParams = {
|
|
118
|
-
TableName: 'InstagramClone',
|
|
119
|
-
Item: {
|
|
120
|
-
pk: 'FOLLOW#alice',
|
|
121
|
-
sk: 'FOLLOW#bob',
|
|
122
|
-
followedUsername: 'alice',
|
|
123
|
-
followingUsername: 'bob',
|
|
124
|
-
},
|
|
125
|
-
ConditionExpression: '(attribute_not_exists(#PK)) AND (attribute_not_exists(#SK))',
|
|
126
|
-
ExpressionAttributeNames: { '#PK': 'pk', '#SK': 'sk' },
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const updateFollowedParams = {
|
|
130
|
-
TableName: 'InstagramClone',
|
|
131
|
-
Key: { pk: 'USER#alice', sk: 'USER#alice' },
|
|
132
|
-
UpdateExpression: 'ADD #followerCount :followerCount_0',
|
|
133
|
-
ExpressionAttributeNames: { '#followerCount': 'followerCount' },
|
|
134
|
-
ExpressionAttributeValues: { ':followerCount_0': 1 },
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const updateFollowingParams = {
|
|
138
|
-
TableName: 'InstagramClone',
|
|
139
|
-
Key: { pk: 'USER#bob', sk: 'USER#bob' },
|
|
140
|
-
UpdateExpression: 'ADD #followingCount :followingCount_0',
|
|
141
|
-
ExpressionAttributeNames: { '#followingCount': 'followingCount' },
|
|
142
|
-
ExpressionAttributeValues: { ':followingCount_0': 1 },
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const params = createTransactWriteBuilder(client)
|
|
146
|
-
.addPut(putFollowParams)
|
|
147
|
-
.addUpdate(updateFollowedParams)
|
|
148
|
-
.addUpdate(updateFollowingParams)
|
|
149
|
-
.dbParams();
|
|
150
|
-
|
|
151
|
-
expect(params.TransactItems).toHaveLength(3);
|
|
152
|
-
expect(params.TransactItems[0]).toEqual({ Put: putFollowParams });
|
|
153
|
-
expect(params.TransactItems[1]).toEqual({ Update: updateFollowedParams });
|
|
154
|
-
expect(params.TransactItems[2]).toEqual({ Update: updateFollowingParams });
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('should chain multiple operations - Comment on Photo', () => {
|
|
158
|
-
const putCommentParams = {
|
|
159
|
-
TableName: 'InstagramClone',
|
|
160
|
-
Item: {
|
|
161
|
-
pk: 'PC#photo123',
|
|
162
|
-
sk: 'COMMENT#comment456',
|
|
163
|
-
photoId: 'photo123',
|
|
164
|
-
commentId: 'comment456',
|
|
165
|
-
commentingUsername: 'bob',
|
|
166
|
-
content: 'Great photo!',
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const updatePhotoParams = {
|
|
171
|
-
TableName: 'InstagramClone',
|
|
172
|
-
Key: { pk: 'UP#alice', sk: 'PHOTO#photo123' },
|
|
173
|
-
UpdateExpression: 'ADD #commentCount :commentCount_0',
|
|
174
|
-
ExpressionAttributeNames: { '#commentCount': 'commentCount' },
|
|
175
|
-
ExpressionAttributeValues: { ':commentCount_0': 1 },
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const params = createTransactWriteBuilder(client)
|
|
179
|
-
.addPut(putCommentParams)
|
|
180
|
-
.addUpdate(updatePhotoParams)
|
|
181
|
-
.dbParams();
|
|
182
|
-
|
|
183
|
-
expect(params.TransactItems).toHaveLength(2);
|
|
184
|
-
expect(params.TransactItems[0]).toEqual({ Put: putCommentParams });
|
|
185
|
-
expect(params.TransactItems[1]).toEqual({ Update: updatePhotoParams });
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test('should support client request token for idempotency', () => {
|
|
189
|
-
const putParams = {
|
|
190
|
-
TableName: 'InstagramClone',
|
|
191
|
-
Item: { pk: 'USER#alice', sk: 'USER#alice', username: 'alice' },
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const params = createTransactWriteBuilder(client)
|
|
195
|
-
.addPut(putParams)
|
|
196
|
-
.withClientRequestToken('my-unique-token-123')
|
|
197
|
-
.dbParams();
|
|
198
|
-
|
|
199
|
-
expect(params).toEqual({
|
|
200
|
-
TransactItems: [{ Put: putParams }],
|
|
201
|
-
ClientRequestToken: 'my-unique-token-123',
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('should preserve immutability - original builder unchanged', () => {
|
|
206
|
-
const builder1 = createTransactWriteBuilder(client);
|
|
207
|
-
const putParams = {
|
|
208
|
-
TableName: 'InstagramClone',
|
|
209
|
-
Item: { pk: 'USER#alice', sk: 'USER#alice' },
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const builder2 = builder1.addPut(putParams);
|
|
213
|
-
const builder3 = builder2.withClientRequestToken('token');
|
|
214
|
-
|
|
215
|
-
// Each builder should be independent
|
|
216
|
-
expect(builder1.dbParams().TransactItems).toHaveLength(0);
|
|
217
|
-
expect(builder2.dbParams().TransactItems).toHaveLength(1);
|
|
218
|
-
expect(builder3.dbParams().ClientRequestToken).toBe('token');
|
|
219
|
-
expect(builder2.dbParams().ClientRequestToken).toBeUndefined();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('should handle complex transaction - Delete Photo only if no engagement', () => {
|
|
223
|
-
const deletePhotoParams = {
|
|
224
|
-
TableName: 'InstagramClone',
|
|
225
|
-
Key: { pk: 'UP#alice', sk: 'PHOTO#photo123' },
|
|
226
|
-
ConditionExpression: '(#likesCount = :likesCount_0) AND (#commentCount = :commentCount_1)',
|
|
227
|
-
ExpressionAttributeNames: {
|
|
228
|
-
'#likesCount': 'likesCount',
|
|
229
|
-
'#commentCount': 'commentCount',
|
|
230
|
-
},
|
|
231
|
-
ExpressionAttributeValues: {
|
|
232
|
-
':likesCount_0': 0,
|
|
233
|
-
':commentCount_1': 0,
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const params = createTransactWriteBuilder(client).addDelete(deletePhotoParams).dbParams();
|
|
238
|
-
|
|
239
|
-
expect(params.TransactItems).toHaveLength(1);
|
|
240
|
-
expect(params.TransactItems[0]).toEqual({ Delete: deletePhotoParams });
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('should handle max operations (100 items limit)', () => {
|
|
244
|
-
let builder = createTransactWriteBuilder(client);
|
|
245
|
-
|
|
246
|
-
// Add 100 Put operations (DynamoDB limit)
|
|
247
|
-
for (let i = 0; i < 100; i++) {
|
|
248
|
-
builder = builder.addPut({
|
|
249
|
-
TableName: 'InstagramClone',
|
|
250
|
-
Item: { pk: `USER#user${i}`, sk: `USER#user${i}` },
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const params = builder.dbParams();
|
|
255
|
-
expect(params.TransactItems).toHaveLength(100);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test('should mix all operation types', () => {
|
|
259
|
-
const params = createTransactWriteBuilder(client)
|
|
260
|
-
.addPut({
|
|
261
|
-
TableName: 'InstagramClone',
|
|
262
|
-
Item: { pk: 'USER#alice', sk: 'USER#alice' },
|
|
263
|
-
})
|
|
264
|
-
.addUpdate({
|
|
265
|
-
TableName: 'InstagramClone',
|
|
266
|
-
Key: { pk: 'USER#bob', sk: 'USER#bob' },
|
|
267
|
-
UpdateExpression: 'ADD #count :val',
|
|
268
|
-
ExpressionAttributeNames: { '#count': 'followerCount' },
|
|
269
|
-
ExpressionAttributeValues: { ':val': 1 },
|
|
270
|
-
})
|
|
271
|
-
.addDelete({
|
|
272
|
-
TableName: 'InstagramClone',
|
|
273
|
-
Key: { pk: 'PHOTO#old', sk: 'PHOTO#old' },
|
|
274
|
-
})
|
|
275
|
-
.addConditionCheck({
|
|
276
|
-
TableName: 'InstagramClone',
|
|
277
|
-
Key: { pk: 'USER#admin', sk: 'USER#admin' },
|
|
278
|
-
ConditionExpression: 'attribute_exists(pk)',
|
|
279
|
-
})
|
|
280
|
-
.dbParams();
|
|
281
|
-
|
|
282
|
-
expect(params.TransactItems).toHaveLength(4);
|
|
283
|
-
expect(params.TransactItems[0]).toHaveProperty('Put');
|
|
284
|
-
expect(params.TransactItems[1]).toHaveProperty('Update');
|
|
285
|
-
expect(params.TransactItems[2]).toHaveProperty('Delete');
|
|
286
|
-
expect(params.TransactItems[3]).toHaveProperty('ConditionCheck');
|
|
287
|
-
});
|
|
288
|
-
});
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
-
import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
|
|
3
|
-
import {
|
|
4
|
-
TransactWriteBuilder,
|
|
5
|
-
TransactWriteState,
|
|
6
|
-
TransactPutParams,
|
|
7
|
-
TransactUpdateParams,
|
|
8
|
-
TransactDeleteParams,
|
|
9
|
-
TransactConditionCheckParams,
|
|
10
|
-
} from './types';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Creates the initial state for a TransactWrite builder
|
|
14
|
-
*/
|
|
15
|
-
const createInitialState = (client: DynamoDBClient): TransactWriteState => ({
|
|
16
|
-
client,
|
|
17
|
-
items: [],
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Adds a Put operation to the transaction
|
|
22
|
-
*/
|
|
23
|
-
const addPutItem =
|
|
24
|
-
(state: TransactWriteState) =>
|
|
25
|
-
(params: TransactPutParams): TransactWriteState => ({
|
|
26
|
-
...state,
|
|
27
|
-
items: [...state.items, { Put: params }],
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Adds an Update operation to the transaction
|
|
32
|
-
*/
|
|
33
|
-
const addUpdateItem =
|
|
34
|
-
(state: TransactWriteState) =>
|
|
35
|
-
(params: TransactUpdateParams): TransactWriteState => ({
|
|
36
|
-
...state,
|
|
37
|
-
items: [...state.items, { Update: params }],
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Adds a Delete operation to the transaction
|
|
42
|
-
*/
|
|
43
|
-
const addDeleteItem =
|
|
44
|
-
(state: TransactWriteState) =>
|
|
45
|
-
(params: TransactDeleteParams): TransactWriteState => ({
|
|
46
|
-
...state,
|
|
47
|
-
items: [...state.items, { Delete: params }],
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Adds a ConditionCheck operation to the transaction
|
|
52
|
-
*/
|
|
53
|
-
const addConditionCheckItem =
|
|
54
|
-
(state: TransactWriteState) =>
|
|
55
|
-
(params: TransactConditionCheckParams): TransactWriteState => ({
|
|
56
|
-
...state,
|
|
57
|
-
items: [...state.items, { ConditionCheck: params }],
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Sets the client request token for idempotency
|
|
62
|
-
*/
|
|
63
|
-
const setClientRequestToken =
|
|
64
|
-
(state: TransactWriteState) =>
|
|
65
|
-
(token: string): TransactWriteState => ({
|
|
66
|
-
...state,
|
|
67
|
-
clientRequestToken: token,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Converts the builder state to DynamoDB parameters
|
|
72
|
-
*/
|
|
73
|
-
const toDbParams = (state: TransactWriteState): ReturnType<TransactWriteBuilder['dbParams']> => {
|
|
74
|
-
const params: ReturnType<TransactWriteBuilder['dbParams']> = {
|
|
75
|
-
TransactItems: [...state.items],
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
if (state.clientRequestToken) {
|
|
79
|
-
params.ClientRequestToken = state.clientRequestToken;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return params;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Executes the transaction
|
|
87
|
-
*/
|
|
88
|
-
const execute = async (state: TransactWriteState) => {
|
|
89
|
-
const params = toDbParams(state);
|
|
90
|
-
const command = new TransactWriteCommand(params);
|
|
91
|
-
return await state.client.send(command);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Creates a builder from the current state
|
|
96
|
-
*/
|
|
97
|
-
const createBuilder = (state: TransactWriteState): TransactWriteBuilder => ({
|
|
98
|
-
addPut: (params) => createBuilder(addPutItem(state)(params)),
|
|
99
|
-
addUpdate: (params) => createBuilder(addUpdateItem(state)(params)),
|
|
100
|
-
addDelete: (params) => createBuilder(addDeleteItem(state)(params)),
|
|
101
|
-
addConditionCheck: (params) => createBuilder(addConditionCheckItem(state)(params)),
|
|
102
|
-
withClientRequestToken: (token) => createBuilder(setClientRequestToken(state)(token)),
|
|
103
|
-
dbParams: () => toDbParams(state),
|
|
104
|
-
execute: () => execute(state),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Creates a new TransactWrite builder
|
|
109
|
-
*
|
|
110
|
-
* @param client - DynamoDB client instance
|
|
111
|
-
* @returns A TransactWriteBuilder
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* ```typescript
|
|
115
|
-
* const result = await createTransactWriteBuilder(client)
|
|
116
|
-
* .addPut(putParams)
|
|
117
|
-
* .addUpdate(updateParams)
|
|
118
|
-
* .execute();
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
export const createTransactWriteBuilder = (client: DynamoDBClient): TransactWriteBuilder => {
|
|
122
|
-
const initialState = createInitialState(client);
|
|
123
|
-
return createBuilder(initialState);
|
|
124
|
-
};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
-
import type {
|
|
4
|
-
TransactWriteCommandInput,
|
|
5
|
-
TransactWriteCommandOutput,
|
|
6
|
-
PutCommandInput,
|
|
7
|
-
UpdateCommandInput,
|
|
8
|
-
DeleteCommandInput,
|
|
9
|
-
} from '@aws-sdk/lib-dynamodb';
|
|
10
|
-
|
|
11
|
-
// Extract types from AWS SDK's TransactWriteCommandInput
|
|
12
|
-
type TransactWriteItems = NonNullable<TransactWriteCommandInput['TransactItems']>;
|
|
13
|
-
type StrictTransactPut = NonNullable<TransactWriteItems[number]['Put']>;
|
|
14
|
-
type StrictTransactUpdate = NonNullable<TransactWriteItems[number]['Update']>;
|
|
15
|
-
type StrictTransactDelete = NonNullable<TransactWriteItems[number]['Delete']>;
|
|
16
|
-
type StrictTransactConditionCheck = NonNullable<TransactWriteItems[number]['ConditionCheck']>;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Parameters for a Put operation within a transaction
|
|
20
|
-
* Accepts both regular PutCommandInput and strict transaction Put params
|
|
21
|
-
*/
|
|
22
|
-
export type TransactPutParams = PutCommandInput | StrictTransactPut;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Parameters for an Update operation within a transaction
|
|
26
|
-
* Accepts both regular UpdateCommandInput and strict transaction Update params
|
|
27
|
-
*/
|
|
28
|
-
export type TransactUpdateParams = UpdateCommandInput | StrictTransactUpdate;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Parameters for a Delete operation within a transaction
|
|
32
|
-
* Accepts both regular DeleteCommandInput and strict transaction Delete params
|
|
33
|
-
*/
|
|
34
|
-
export type TransactDeleteParams = DeleteCommandInput | StrictTransactDelete;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Parameters for a ConditionCheck operation within a transaction
|
|
38
|
-
*/
|
|
39
|
-
export type TransactConditionCheckParams = StrictTransactConditionCheck;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Represents a single item in a TransactWrite operation
|
|
43
|
-
* Using any internally to allow flexibility between CommandInput and strict transaction types
|
|
44
|
-
*/
|
|
45
|
-
export type TransactWriteItem =
|
|
46
|
-
| { Put: any }
|
|
47
|
-
| { Update: any }
|
|
48
|
-
| { Delete: any }
|
|
49
|
-
| { ConditionCheck: any };
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* State for the TransactWrite builder
|
|
53
|
-
*/
|
|
54
|
-
export type TransactWriteState = {
|
|
55
|
-
readonly client: DynamoDBClient;
|
|
56
|
-
readonly items: readonly TransactWriteItem[];
|
|
57
|
-
readonly clientRequestToken?: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* TransactWrite builder interface
|
|
62
|
-
*/
|
|
63
|
-
export type TransactWriteBuilder = {
|
|
64
|
-
readonly addPut: (params: TransactPutParams) => TransactWriteBuilder;
|
|
65
|
-
readonly addUpdate: (params: TransactUpdateParams) => TransactWriteBuilder;
|
|
66
|
-
readonly addDelete: (params: TransactDeleteParams) => TransactWriteBuilder;
|
|
67
|
-
readonly addConditionCheck: (params: TransactConditionCheckParams) => TransactWriteBuilder;
|
|
68
|
-
readonly withClientRequestToken: (token: string) => TransactWriteBuilder;
|
|
69
|
-
readonly dbParams: () => TransactWriteCommandInput;
|
|
70
|
-
readonly execute: () => Promise<TransactWriteCommandOutput>;
|
|
71
|
-
};
|