Skip to content

Commit

Permalink
Merge pull request #28 from mindler-olli/main
Browse files Browse the repository at this point in the history
intial work for supporting transactions
  • Loading branch information
woltsu authored May 31, 2024
2 parents dc22939 + ee8cd30 commit fd6a3d4
Show file tree
Hide file tree
Showing 20 changed files with 800 additions and 105 deletions.
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ Tsynamo simplifies the DynamoDB API so that you don't have to write commands wit
- [Put item](#put-item)
- [Delete item](#delete-item)
- [Update item](#update-item)
- [Transactions](#transactions)
- [Contributors](#contributors)


## Requirements

- [@aws-sdk/client-dynamodb](https://www.npmjs.com/package/@aws-sdk/client-dynamodb)
Expand Down Expand Up @@ -72,6 +72,7 @@ export interface DDB {
};
}
```

> [!TIP]
> Notice that you can have multiple tables in the DDB schema. Nested attributes are supported too.
Expand Down Expand Up @@ -196,6 +197,7 @@ await tsynamoClient
.execute();
```

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

### Transactions

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.

#### Write transaction

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.

```ts
const trx = tsynamoClient.createWriteTransaction();

trx.addItem({
Put: tsynamoClient
.putItem("myTable")
.item({ userId: "313", dataTimestamp: 1 }),
});

trx.addItem({
Update: tsynamoClient
.updateItem("myTable")
.keys({ userId: "313", dataTimestamp: 2 })
.set("tags", "=", ["a", "b", "c"]),
});

trx.addItem({
Delete: tsynamoClient.deleteItem("myTable").keys({
userId: "313",
dataTimestamp: 3,
}),
});

await trx.execute();
```

> [!IMPORTANT]
> 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.
> [!WARNING]
> DynamoDB also supports doing `ConditionCheck` operations in the transaction, but Tsynamo does not yet support those.
#### Read transaction

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.

```ts
const trx = tsynamoClient.createReadTransaction();

trx.addItem({
Get: tsynamoClient.getItem("myTable").keys({
userId: "123",
dataTimestamp: 222,
}),
});

trx.addItem({
Get: tsynamoClient.getItem("myOtherTable").keys({
userId: "321",
stringTimestamp: "222",
}),
});

const result = await trx.execute();
```

Then, one can loop through the result items as so:

```ts
// note that the items can be undefined if they were not found from DynamoDB
result.forEach(([table, item]) => {
if (table === "myTable") {
// item's type is DDB["myTable"]
// ...
} else if (table === "myOtherTable") {
// item's type is DDB["myOtherTable"]
// ...
}
});
```

## Contributors

<p>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tsynamo",
"author": "woltsu",
"version": "0.0.9",
"version": "0.0.10",
"description": "Typed query builder for DynamoDB",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
6 changes: 6 additions & 0 deletions src/nodes/readTransactionNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TransactGetItemNode } from "./transactGetItemNode";

export type ReadTransactionNode = {
readonly kind: "ReadTransactionNode";
readonly transactGetItems: TransactGetItemNode[];
};
6 changes: 6 additions & 0 deletions src/nodes/transactGetItemNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { GetNode } from "./getNode";

export type TransactGetItemNode = {
readonly kind: "TransactGetItemNode";
readonly Get: GetNode;
};
10 changes: 10 additions & 0 deletions src/nodes/transactWriteItemNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DeleteNode } from "./deleteNode";
import { PutNode } from "./putNode";
import { UpdateNode } from "./updateNode";

export type TransactWriteItemNode = {
readonly kind: "TransactWriteItemNode";
readonly Put?: PutNode;
readonly Delete?: DeleteNode;
readonly Update?: UpdateNode;
};
7 changes: 7 additions & 0 deletions src/nodes/writeTransactionNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TransactWriteItemNode } from "./transactWriteItemNode";

export type WriteTransactionNode = {
readonly kind: "WriteTransactionNode";
readonly transactWriteItems: TransactWriteItemNode[];
readonly clientRequestToken?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`ReadTransactionBuilder > handles transaction with gets 1`] = `
[
[
"myTable",
{
"dataTimestamp": 222,
"someBoolean": true,
"somethingElse": 2,
"userId": "123",
},
],
[
"myOtherTable",
{
"userId": "123",
},
],
[
"myTable",
undefined,
],
]
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`WriteTransactionBuilder > handles a transaction with a client request token 1`] = `[IdempotentParameterMismatchException: UnknownError]`;

exports[`WriteTransactionBuilder > handles a transaction with failing conditions 1`] = `[TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed]]`;

exports[`WriteTransactionBuilder > handles a transaction with puts 1`] = `
[
{
"dataTimestamp": 1,
"userId": "9999",
},
{
"dataTimestamp": 2,
"userId": "9999",
},
]
`;

exports[`WriteTransactionBuilder > handles a transaction with updates 1`] = `
[
{
"dataTimestamp": 1,
"someBoolean": true,
"userId": "9999",
},
{
"dataTimestamp": 2,
"tags": [
"a",
"b",
"c",
],
"userId": "9999",
},
]
`;
49 changes: 27 additions & 22 deletions src/queryBuilders/deleteItemQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,77 +24,77 @@ import {
export interface DeleteItemQueryBuilderInterface<
DDB,
Table extends keyof DDB,
O
O extends DDB[Table]
> {
// conditionExpression
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: ComparatorExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeFuncExprArg<Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeBeginsWithExprArg<Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeContainsExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeBetweenExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: NotExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: BuilderExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

// orConditionExpression
orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: ComparatorExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeFuncExprArg<Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeBeginsWithExprArg<Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeContainsExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: AttributeBetweenExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: NotExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: BuilderExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

returnValues(
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

returnValuesOnConditionCheckFailure(
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

keys<Keys extends PickPk<DDB[Table]> & PickSkRequired<DDB[Table]>>(
pk: Keys
): DeleteItemQueryBuilderInterface<DDB, Table, O>;
): DeleteItemQueryBuilder<DDB, Table, O>;

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

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

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

returnValues(
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
): DeleteItemQueryBuilder<DDB, Table, O> {
return new DeleteItemQueryBuilder<DDB, Table, O>({
...this.#props,
node: {
Expand All @@ -168,7 +168,7 @@ export class DeleteItemQueryBuilder<

returnValuesOnConditionCheckFailure(
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
): DeleteItemQueryBuilder<DDB, Table, O> {
return new DeleteItemQueryBuilder<DDB, Table, O>({
...this.#props,
node: {
Expand Down Expand Up @@ -199,11 +199,16 @@ export class DeleteItemQueryBuilder<
compile = (): DeleteCommand => {
return this.#props.queryCompiler.compile(this.#props.node);
};

execute = async (): Promise<ExecuteOutput<O>[] | undefined> => {
const deleteCommand = this.compile();
const data = await this.#props.ddbClient.send(deleteCommand);
return data.Attributes as any;
};

public get node() {
return this.#props.node;
}
}

preventAwait(
Expand Down
Loading

0 comments on commit fd6a3d4

Please sign in to comment.