Skip to content

Commit 2103e1c

Browse files
committed
improve transactions typing, chaining & merge multiple update item operations
1 parent 8ff8123 commit 2103e1c

6 files changed

Lines changed: 117 additions & 11 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@moicky/dynamodb",
3-
"version": "3.0.5",
3+
"version": "3.1.0",
44
"main": "dist/index.js",
55
"types": "dist/index.d.ts",
66
"description": "Contains a collection of convenience functions for working with AWS DynamoDB",

src/transactions/index.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class Transaction {
5050
private updatedAt: any;
5151
private operations: { [itemKey: string]: ItemOperation } = {};
5252
private shouldSplitTransactions: boolean;
53+
private hasBeenExecuted: boolean = false;
5354

5455
constructor({
5556
tableName,
@@ -79,6 +80,12 @@ export class Transaction {
7980
item: WithoutReferences<T>,
8081
args?: CreateOperation["args"]
8182
) {
83+
if (this.hasBeenExecuted) {
84+
throw new Error(
85+
"[@moicky/dynamodb]: Transaction has already been executed"
86+
);
87+
}
88+
8289
const itemKey = this.getItemKey(item, args?.TableName);
8390

8491
const createOperation: CreateOperation = {
@@ -92,15 +99,30 @@ export class Transaction {
9299
return new CreateOperations<T>(createOperation, this);
93100
}
94101
update<T extends DynamoDBItemKey>(
95-
item: OnlyKey<T>,
102+
item: T extends any ? OnlyKey<T> : T,
96103
args?: UpdateOperation["args"]
97104
) {
105+
if (this.hasBeenExecuted) {
106+
throw new Error(
107+
"[@moicky/dynamodb]: Transaction has already been executed"
108+
);
109+
}
110+
98111
const itemKey = this.getItemKey(item, args?.TableName);
99112

113+
const actions: UpdateAction[] = [
114+
{ _type: "set", values: { updatedAt: this.updatedAt } },
115+
];
116+
117+
const existingOperation = this.operations[itemKey];
118+
if (existingOperation && existingOperation._type === "update") {
119+
actions.push(...existingOperation.actions);
120+
}
121+
100122
const updateOperation: UpdateOperation = {
101123
_type: "update",
102124
item,
103-
actions: [{ _type: "set", values: { updatedAt: this.updatedAt } }],
125+
actions,
104126
args: { TableName: this.tableName, ...args },
105127
};
106128

@@ -109,19 +131,33 @@ export class Transaction {
109131
return new UpdateOperations<T>(updateOperation, this);
110132
}
111133
delete(item: DynamoDBItemKey, args?: DeleteOperation["args"]) {
134+
if (this.hasBeenExecuted) {
135+
throw new Error(
136+
"[@moicky/dynamodb]: Transaction has already been executed"
137+
);
138+
}
139+
112140
const itemKey = this.getItemKey(item, args?.TableName);
113141

114142
this.operations[itemKey] = {
115143
_type: "delete",
116144
item,
117145
args: { TableName: this.tableName, ...args },
118146
};
147+
148+
return this;
119149
}
120150
addConditionFor<T extends DynamoDBItemKey>(
121151
item: T,
122152
args?: Partial<ConditionOperation["args"]>
123153
) {
124-
return new ConditionOperations<T>(this.operations, item, {
154+
if (this.hasBeenExecuted) {
155+
throw new Error(
156+
"[@moicky/dynamodb]: Transaction has already been executed"
157+
);
158+
}
159+
160+
return new ConditionOperations<T>(this, this.operations, item, {
125161
TableName: this.tableName,
126162
...args,
127163
});
@@ -234,6 +270,16 @@ export class Transaction {
234270

235271
return new Promise<Record<string, ResponseItem[]>>(
236272
async (resolve, reject) => {
273+
if (this.hasBeenExecuted) {
274+
reject(
275+
new Error(
276+
"[@moicky/dynamodb]: Transaction has already been executed"
277+
)
278+
);
279+
}
280+
281+
this.hasBeenExecuted = true;
282+
237283
const operations = Object.values(this.operations);
238284

239285
if (

src/transactions/operations.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TransactWriteItemsCommandInput } from "@aws-sdk/client-dynamodb";
12
import { Transaction } from ".";
23
import {
34
getAttributeNames,
@@ -39,7 +40,7 @@ export class CreateOperations<U extends ItemWithKey> {
3940

4041
const refData =
4142
ref instanceof Set
42-
? [...ref].map((references) =>
43+
? Array.from(ref).map((references) =>
4344
createReference({ references, ...refArgs }, this.transaction)
4445
)
4546
: createReference({ references: ref, ...refArgs }, this.transaction);
@@ -55,6 +56,12 @@ export class CreateOperations<U extends ItemWithKey> {
5556
}, this.operation.item);
5657
});
5758
}
59+
60+
execute(
61+
args?: Partial<Omit<TransactWriteItemsCommandInput, "TransactItems">>
62+
) {
63+
return this.transaction.execute(args);
64+
}
5865
}
5966

6067
export class UpdateOperations<U extends DynamoDBItem> {
@@ -77,7 +84,7 @@ export class UpdateOperations<U extends DynamoDBItem> {
7784
values: {
7885
[attributeName]:
7986
ref instanceof Set
80-
? [...ref].map((references) =>
87+
? Array.from(ref).map((references) =>
8188
createReference({ references, ...refArgs }, this.transaction)
8289
)
8390
: createReference(
@@ -147,10 +154,17 @@ export class UpdateOperations<U extends DynamoDBItem> {
147154
}, {}),
148155
};
149156
}
157+
158+
execute(
159+
args?: Partial<Omit<TransactWriteItemsCommandInput, "TransactItems">>
160+
) {
161+
return this.transaction.execute(args);
162+
}
150163
}
151164

152165
export class ConditionOperations<U extends DynamoDBItem> {
153166
constructor(
167+
private transaction: Transaction,
154168
private operations: Transaction["operations"],
155169
private item: DynamoDBItemKey,
156170
private args: Partial<ConditionOperation["args"]> & { TableName: string }
@@ -181,6 +195,12 @@ export class ConditionOperations<U extends DynamoDBItem> {
181195
},
182196
};
183197
}
198+
199+
execute(
200+
args?: Partial<Omit<TransactWriteItemsCommandInput, "TransactItems">>
201+
) {
202+
return this.transaction.execute(args);
203+
}
184204
}
185205

186206
const arraysToSets = <T extends DynamoDBItemKey>(

src/transactions/references/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,15 @@ const getRefsToResolve = (item: any) => {
4848
for (const value of Object.values<DynamoDBReference>(item)) {
4949
if (typeof value === "object") {
5050
if (value instanceof Set || Array.isArray(value)) {
51-
refs.push(...[...value].map((v) => getRefsToResolve(v)).flat());
51+
refs.push(
52+
...Array.from(value)
53+
.map((v) => getRefsToResolve(v))
54+
.flat()
55+
);
5256
} else if (value?._type === "dynamodb:reference") {
5357
refs.push(value?._target);
5458
} else {
55-
refs.push(...getRefsToResolve(value));
59+
refs.push(...getRefsToResolve(value).flat());
5660
}
5761
}
5862
}
@@ -69,7 +73,7 @@ const injectRefs = (item: any, refs: Record<string, ItemWithKey>) => {
6973
for (const [key, value] of Object.entries<DynamoDBReference>(item)) {
7074
if (typeof value === "object") {
7175
if (value instanceof Set || Array.isArray(value)) {
72-
item[key] = new Set([...value].map((v) => injectRefs(v, refs)));
76+
item[key] = new Set(Array.from(value).map((v) => injectRefs(v, refs)));
7377
} else if (value?._type === "dynamodb:reference") {
7478
item[key] = refs[itemToStringKey(value._target)];
7579
} else {

test/transactions/index.test.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
require("dotenv/config");
22

3-
const { ConditionalCheckFailedException } = require("@aws-sdk/client-dynamodb");
43
const {
54
Transaction,
65
getItems,
@@ -525,4 +524,23 @@ describe("Transaction operations", () => {
525524
const newItem = await getItem(item, { ConsistentRead: true });
526525
expect(newItem.nested.number).toEqual(3);
527526
});
527+
528+
it("should merge update operations for the same item", async () => {
529+
const item = generateItem("9003");
530+
await putItem(item);
531+
532+
const transaction = new Transaction();
533+
transaction.update(item).set({ title: "Test" });
534+
transaction
535+
.update(item)
536+
.set({ author: "Test Author" })
537+
.adjustNumber({ stars: 1 });
538+
539+
await transaction.execute();
540+
541+
const newItem = await getItem(item, { ConsistentRead: true });
542+
expect(newItem.title).toEqual("Test");
543+
expect(newItem.author).toEqual("Test Author");
544+
expect(newItem.stars).toEqual(item.stars + 1);
545+
});
528546
});

test/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import "dotenv/config";
2+
13
import { PutItemCommandOutput } from "@aws-sdk/client-dynamodb";
24
import {
35
DynamoDBItem,
@@ -10,8 +12,9 @@ import {
1012
queryPaginatedItems,
1113
removeAttributes,
1214
transactGetItems,
15+
Transaction,
1316
updateItem,
14-
} from "../dist";
17+
} from "../src";
1518

1619
type DemoItem = {
1720
PK: `User/${string}`;
@@ -142,3 +145,18 @@ async function playground() {
142145
const simpleTypedItems = await getItems<DemoItem>([a, a]);
143146
const [simpleTypedItem1, simpleTypedItem2] = simpleTypedItems;
144147
}
148+
149+
async function transactionPlayground() {
150+
type TxnItem = {
151+
PK: `User/${string}`;
152+
SK: string;
153+
stars: number;
154+
otherProp: string;
155+
};
156+
157+
const res = await new Transaction()
158+
.update<TxnItem>({ PK: "User/1", SK: "Book/1" })
159+
.set({ otherProp: "test" })
160+
.adjustNumber({ stars: 1 })
161+
.execute();
162+
}

0 commit comments

Comments
 (0)