Skip to content

Commit fd6a3d4

Browse files
authored
Merge pull request #28 from mindler-olli/main
intial work for supporting transactions
2 parents dc22939 + ee8cd30 commit fd6a3d4

20 files changed

Lines changed: 800 additions & 105 deletions

README.md

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ Tsynamo simplifies the DynamoDB API so that you don't have to write commands wit
3333
- [Put item](#put-item)
3434
- [Delete item](#delete-item)
3535
- [Update item](#update-item)
36+
- [Transactions](#transactions)
3637
- [Contributors](#contributors)
3738

38-
3939
## Requirements
4040

4141
- [@aws-sdk/client-dynamodb](https://www.npmjs.com/package/@aws-sdk/client-dynamodb)
@@ -72,6 +72,7 @@ export interface DDB {
7272
};
7373
}
7474
```
75+
7576
> [!TIP]
7677
> Notice that you can have multiple tables in the DDB schema. Nested attributes are supported too.
7778
@@ -196,6 +197,7 @@ await tsynamoClient
196197
.execute();
197198
```
198199

200+
> [!NOTE]
199201
> This would compile as the following FilterExpression:
200202
> `NOT eventType = "LOG_IN"`, i.e. return all events whose types is not "LOG_IN"
201203
@@ -282,6 +284,85 @@ await tsynamoClient
282284
.execute();
283285
```
284286

287+
### Transactions
288+
289+
One can also utilise [DynamoDB Transaction](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html) features using Tsynamo. You can perform operations to multiple tables in a single transaction command.
290+
291+
#### Write transaction
292+
293+
DynamoDB enables you to do multiple `Put`, `Update` and `Delete` in a single `WriteTransaction` command. One can also provide an optional `ClientRequestToken` to the transaction to ensure idempotency.
294+
295+
```ts
296+
const trx = tsynamoClient.createWriteTransaction();
297+
298+
trx.addItem({
299+
Put: tsynamoClient
300+
.putItem("myTable")
301+
.item({ userId: "313", dataTimestamp: 1 }),
302+
});
303+
304+
trx.addItem({
305+
Update: tsynamoClient
306+
.updateItem("myTable")
307+
.keys({ userId: "313", dataTimestamp: 2 })
308+
.set("tags", "=", ["a", "b", "c"]),
309+
});
310+
311+
trx.addItem({
312+
Delete: tsynamoClient.deleteItem("myTable").keys({
313+
userId: "313",
314+
dataTimestamp: 3,
315+
}),
316+
});
317+
318+
await trx.execute();
319+
```
320+
321+
> [!IMPORTANT]
322+
> When passing the items into the transaction using the tsynamoClient, do not execute the individual calls! Instead just pass in the query builder as the item.
323+
324+
> [!WARNING]
325+
> DynamoDB also supports doing `ConditionCheck` operations in the transaction, but Tsynamo does not yet support those.
326+
327+
#### Read transaction
328+
329+
Since the read transaction output can affect multiple tables, the resulting output is an array of tuples where the first item is the name of the table and the second item is the item itself (or `undefined` if the item was not found). This can be used as a discriminated union to determine the resulting item's type.
330+
331+
```ts
332+
const trx = tsynamoClient.createReadTransaction();
333+
334+
trx.addItem({
335+
Get: tsynamoClient.getItem("myTable").keys({
336+
userId: "123",
337+
dataTimestamp: 222,
338+
}),
339+
});
340+
341+
trx.addItem({
342+
Get: tsynamoClient.getItem("myOtherTable").keys({
343+
userId: "321",
344+
stringTimestamp: "222",
345+
}),
346+
});
347+
348+
const result = await trx.execute();
349+
```
350+
351+
Then, one can loop through the result items as so:
352+
353+
```ts
354+
// note that the items can be undefined if they were not found from DynamoDB
355+
result.forEach(([table, item]) => {
356+
if (table === "myTable") {
357+
// item's type is DDB["myTable"]
358+
// ...
359+
} else if (table === "myOtherTable") {
360+
// item's type is DDB["myOtherTable"]
361+
// ...
362+
}
363+
});
364+
```
365+
285366
## Contributors
286367

287368
<p>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tsynamo",
33
"author": "woltsu",
4-
"version": "0.0.9",
4+
"version": "0.0.10",
55
"description": "Typed query builder for DynamoDB",
66
"main": "dist/index.js",
77
"types": "dist/index.d.ts",

src/nodes/readTransactionNode.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { TransactGetItemNode } from "./transactGetItemNode";
2+
3+
export type ReadTransactionNode = {
4+
readonly kind: "ReadTransactionNode";
5+
readonly transactGetItems: TransactGetItemNode[];
6+
};

src/nodes/transactGetItemNode.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { GetNode } from "./getNode";
2+
3+
export type TransactGetItemNode = {
4+
readonly kind: "TransactGetItemNode";
5+
readonly Get: GetNode;
6+
};

src/nodes/transactWriteItemNode.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { DeleteNode } from "./deleteNode";
2+
import { PutNode } from "./putNode";
3+
import { UpdateNode } from "./updateNode";
4+
5+
export type TransactWriteItemNode = {
6+
readonly kind: "TransactWriteItemNode";
7+
readonly Put?: PutNode;
8+
readonly Delete?: DeleteNode;
9+
readonly Update?: UpdateNode;
10+
};

src/nodes/writeTransactionNode.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { TransactWriteItemNode } from "./transactWriteItemNode";
2+
3+
export type WriteTransactionNode = {
4+
readonly kind: "WriteTransactionNode";
5+
readonly transactWriteItems: TransactWriteItemNode[];
6+
readonly clientRequestToken?: string;
7+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`ReadTransactionBuilder > handles transaction with gets 1`] = `
4+
[
5+
[
6+
"myTable",
7+
{
8+
"dataTimestamp": 222,
9+
"someBoolean": true,
10+
"somethingElse": 2,
11+
"userId": "123",
12+
},
13+
],
14+
[
15+
"myOtherTable",
16+
{
17+
"userId": "123",
18+
},
19+
],
20+
[
21+
"myTable",
22+
undefined,
23+
],
24+
]
25+
`;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`WriteTransactionBuilder > handles a transaction with a client request token 1`] = `[IdempotentParameterMismatchException: UnknownError]`;
4+
5+
exports[`WriteTransactionBuilder > handles a transaction with failing conditions 1`] = `[TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed]]`;
6+
7+
exports[`WriteTransactionBuilder > handles a transaction with puts 1`] = `
8+
[
9+
{
10+
"dataTimestamp": 1,
11+
"userId": "9999",
12+
},
13+
{
14+
"dataTimestamp": 2,
15+
"userId": "9999",
16+
},
17+
]
18+
`;
19+
20+
exports[`WriteTransactionBuilder > handles a transaction with updates 1`] = `
21+
[
22+
{
23+
"dataTimestamp": 1,
24+
"someBoolean": true,
25+
"userId": "9999",
26+
},
27+
{
28+
"dataTimestamp": 2,
29+
"tags": [
30+
"a",
31+
"b",
32+
"c",
33+
],
34+
"userId": "9999",
35+
},
36+
]
37+
`;

src/queryBuilders/deleteItemQueryBuilder.ts

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,77 +24,77 @@ import {
2424
export interface DeleteItemQueryBuilderInterface<
2525
DDB,
2626
Table extends keyof DDB,
27-
O
27+
O extends DDB[Table]
2828
> {
2929
// conditionExpression
3030
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
3131
...args: ComparatorExprArg<DDB, Table, Key>
32-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
32+
): DeleteItemQueryBuilder<DDB, Table, O>;
3333

3434
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
3535
...args: AttributeFuncExprArg<Key>
36-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
36+
): DeleteItemQueryBuilder<DDB, Table, O>;
3737

3838
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
3939
...args: AttributeBeginsWithExprArg<Key>
40-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
40+
): DeleteItemQueryBuilder<DDB, Table, O>;
4141

4242
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
4343
...args: AttributeContainsExprArg<DDB, Table, Key>
44-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
44+
): DeleteItemQueryBuilder<DDB, Table, O>;
4545

4646
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
4747
...args: AttributeBetweenExprArg<DDB, Table, Key>
48-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
48+
): DeleteItemQueryBuilder<DDB, Table, O>;
4949

5050
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
5151
...args: NotExprArg<DDB, Table, Key>
52-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
52+
): DeleteItemQueryBuilder<DDB, Table, O>;
5353

5454
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
5555
...args: BuilderExprArg<DDB, Table, Key>
56-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
56+
): DeleteItemQueryBuilder<DDB, Table, O>;
5757

5858
// orConditionExpression
5959
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
6060
...args: ComparatorExprArg<DDB, Table, Key>
61-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
61+
): DeleteItemQueryBuilder<DDB, Table, O>;
6262

6363
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
6464
...args: AttributeFuncExprArg<Key>
65-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
65+
): DeleteItemQueryBuilder<DDB, Table, O>;
6666

6767
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
6868
...args: AttributeBeginsWithExprArg<Key>
69-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
69+
): DeleteItemQueryBuilder<DDB, Table, O>;
7070

7171
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
7272
...args: AttributeContainsExprArg<DDB, Table, Key>
73-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
73+
): DeleteItemQueryBuilder<DDB, Table, O>;
7474

7575
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
7676
...args: AttributeBetweenExprArg<DDB, Table, Key>
77-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
77+
): DeleteItemQueryBuilder<DDB, Table, O>;
7878

7979
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
8080
...args: NotExprArg<DDB, Table, Key>
81-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
81+
): DeleteItemQueryBuilder<DDB, Table, O>;
8282

8383
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
8484
...args: BuilderExprArg<DDB, Table, Key>
85-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
85+
): DeleteItemQueryBuilder<DDB, Table, O>;
8686

8787
returnValues(
8888
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
89-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
89+
): DeleteItemQueryBuilder<DDB, Table, O>;
9090

9191
returnValuesOnConditionCheckFailure(
9292
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
93-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
93+
): DeleteItemQueryBuilder<DDB, Table, O>;
9494

9595
keys<Keys extends PickPk<DDB[Table]> & PickSkRequired<DDB[Table]>>(
9696
pk: Keys
97-
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
97+
): DeleteItemQueryBuilder<DDB, Table, O>;
9898

9999
compile(): DeleteCommand;
100100
execute(): Promise<ExecuteOutput<O>[] | undefined>;
@@ -117,7 +117,7 @@ export class DeleteItemQueryBuilder<
117117

118118
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
119119
...args: ExprArgs<DDB, Table, O, Key>
120-
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
120+
): DeleteItemQueryBuilder<DDB, Table, O> {
121121
const eB = new ExpressionBuilder<DDB, Table, O>({
122122
node: { ...this.#props.node.conditionExpression },
123123
});
@@ -135,7 +135,7 @@ export class DeleteItemQueryBuilder<
135135

136136
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
137137
...args: ExprArgs<DDB, Table, O, Key>
138-
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
138+
): DeleteItemQueryBuilder<DDB, Table, O> {
139139
const eB = new ExpressionBuilder<DDB, Table, O>({
140140
node: { ...this.#props.node.conditionExpression },
141141
});
@@ -153,7 +153,7 @@ export class DeleteItemQueryBuilder<
153153

154154
returnValues(
155155
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
156-
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
156+
): DeleteItemQueryBuilder<DDB, Table, O> {
157157
return new DeleteItemQueryBuilder<DDB, Table, O>({
158158
...this.#props,
159159
node: {
@@ -168,7 +168,7 @@ export class DeleteItemQueryBuilder<
168168

169169
returnValuesOnConditionCheckFailure(
170170
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
171-
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
171+
): DeleteItemQueryBuilder<DDB, Table, O> {
172172
return new DeleteItemQueryBuilder<DDB, Table, O>({
173173
...this.#props,
174174
node: {
@@ -199,11 +199,16 @@ export class DeleteItemQueryBuilder<
199199
compile = (): DeleteCommand => {
200200
return this.#props.queryCompiler.compile(this.#props.node);
201201
};
202+
202203
execute = async (): Promise<ExecuteOutput<O>[] | undefined> => {
203204
const deleteCommand = this.compile();
204205
const data = await this.#props.ddbClient.send(deleteCommand);
205206
return data.Attributes as any;
206207
};
208+
209+
public get node() {
210+
return this.#props.node;
211+
}
207212
}
208213

209214
preventAwait(

0 commit comments

Comments
 (0)