From 47c9ce20c555abd0154e9cc692f407e780af578c Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 18 Apr 2024 13:35:41 +0300 Subject: [PATCH 01/15] intial work for supporting transactions --- src/nodes/TransactItemNode.ts | 6 +++ src/nodes/transactionNode.ts | 6 +++ ...ransactionBuilder.integration.test.ts.snap | 14 +++++ src/queryBuilders/putItemQueryBuilder.ts | 49 ++++++++++-------- .../transactionBuilder.integration.test.ts | 43 ++++++++++++++++ src/queryBuilders/transactionBuilder.ts | 44 ++++++++++++++++ src/queryCompiler/queryCompiler.ts | 51 ++++++++++++++++--- src/queryCreator.ts | 36 ++++++++----- 8 files changed, 211 insertions(+), 38 deletions(-) create mode 100644 src/nodes/TransactItemNode.ts create mode 100644 src/nodes/transactionNode.ts create mode 100644 src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap create mode 100644 src/queryBuilders/transactionBuilder.integration.test.ts create mode 100644 src/queryBuilders/transactionBuilder.ts diff --git a/src/nodes/TransactItemNode.ts b/src/nodes/TransactItemNode.ts new file mode 100644 index 0000000..ec9822b --- /dev/null +++ b/src/nodes/TransactItemNode.ts @@ -0,0 +1,6 @@ +import { PutNode } from "./putNode"; + +export type TransactItemNode = { + readonly kind: "TransactItemNode"; + readonly Put?: PutNode; +}; diff --git a/src/nodes/transactionNode.ts b/src/nodes/transactionNode.ts new file mode 100644 index 0000000..b9438fb --- /dev/null +++ b/src/nodes/transactionNode.ts @@ -0,0 +1,6 @@ +import { TransactItemNode } from "./TransactItemNode"; + +export type TransactionNode = { + readonly kind: "TransactionNode"; + readonly transactItems: TransactItemNode[]; +}; diff --git a/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap new file mode 100644 index 0000000..3463d1e --- /dev/null +++ b/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap @@ -0,0 +1,14 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`TransactionBuilder > handles puts 1`] = ` +[ + { + "dataTimestamp": 1, + "userId": "9999", + }, + { + "dataTimestamp": 2, + "userId": "9999", + }, +] +`; diff --git a/src/queryBuilders/putItemQueryBuilder.ts b/src/queryBuilders/putItemQueryBuilder.ts index 2ddbb42..06fa3c4 100644 --- a/src/queryBuilders/putItemQueryBuilder.ts +++ b/src/queryBuilders/putItemQueryBuilder.ts @@ -16,52 +16,56 @@ import { NotExprArg, } from "./expressionBuilder"; -export interface PutItemQueryBuilderInterface { +export interface PutItemQueryBuilderInterface< + DDB, + Table extends keyof DDB, + O extends DDB[Table] +> { // conditionExpression conditionExpression>( ...args: ComparatorExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; conditionExpression>( ...args: AttributeFuncExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; conditionExpression>( ...args: AttributeBeginsWithExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; conditionExpression>( ...args: AttributeContainsExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; conditionExpression>( ...args: AttributeBetweenExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; conditionExpression>( ...args: NotExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; conditionExpression>( ...args: BuilderExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; // orConditionExpression orConditionExpression>( ...args: ComparatorExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; orConditionExpression>( ...args: AttributeFuncExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; orConditionExpression>( ...args: AttributeBeginsWithExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; orConditionExpression>( ...args: AttributeContainsExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; orConditionExpression>( ...args: AttributeBetweenExprArg @@ -69,19 +73,19 @@ export interface PutItemQueryBuilderInterface { orConditionExpression>( ...args: NotExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; orConditionExpression>( ...args: BuilderExprArg - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; returnValues( option: Extract - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; item>( item: Item - ): PutItemQueryBuilderInterface; + ): PutItemQueryBuilder; compile(): PutCommand; execute(): Promise[] | undefined>; @@ -101,7 +105,7 @@ export class PutItemQueryBuilder< conditionExpression>( ...args: ExprArgs - ): PutItemQueryBuilderInterface { + ): PutItemQueryBuilder { const eB = new ExpressionBuilder({ node: { ...this.#props.node.conditionExpression }, }); @@ -119,7 +123,7 @@ export class PutItemQueryBuilder< orConditionExpression>( ...args: ExprArgs - ): PutItemQueryBuilderInterface { + ): PutItemQueryBuilder { const eB = new ExpressionBuilder({ node: { ...this.#props.node.conditionExpression }, }); @@ -137,7 +141,7 @@ export class PutItemQueryBuilder< item>( item: Item - ): PutItemQueryBuilderInterface { + ): PutItemQueryBuilder { return new PutItemQueryBuilder({ ...this.#props, node: { @@ -152,7 +156,7 @@ export class PutItemQueryBuilder< returnValues( option: Extract - ): PutItemQueryBuilderInterface { + ): PutItemQueryBuilder { return new PutItemQueryBuilder({ ...this.#props, node: { @@ -168,11 +172,16 @@ export class PutItemQueryBuilder< compile = (): PutCommand => { return this.#props.queryCompiler.compile(this.#props.node); }; + execute = async (): Promise[] | undefined> => { const putCommand = this.compile(); const data = await this.#props.ddbClient.send(putCommand); return data.Attributes as any; }; + + public get node() { + return this.#props.node; + } } preventAwait( diff --git a/src/queryBuilders/transactionBuilder.integration.test.ts b/src/queryBuilders/transactionBuilder.integration.test.ts new file mode 100644 index 0000000..bf59af4 --- /dev/null +++ b/src/queryBuilders/transactionBuilder.integration.test.ts @@ -0,0 +1,43 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { DDB } from "../../test/testFixture"; +import { getDDBClientFor, startDDBTestContainer } from "../../test/testUtil"; +import { Tsynamo } from "../index"; + +describe("TransactionBuilder", () => { + let tsynamoClient: Tsynamo; + let ddbClient: DynamoDBDocumentClient; + + beforeAll(async () => { + const testContainer = await startDDBTestContainer(); + ddbClient = await getDDBClientFor(testContainer); + + tsynamoClient = new Tsynamo({ + ddbClient, + }); + }); + + it("handles puts", async () => { + const trx = tsynamoClient.createTransaction(); + + trx.addItem({ + Put: tsynamoClient + .putItem("myTable") + .item({ userId: "9999", dataTimestamp: 1 }), + }); + + trx.addItem({ + Put: tsynamoClient + .putItem("myTable") + .item({ userId: "9999", dataTimestamp: 2 }), + }); + + await trx.execute(); + + const result = await tsynamoClient + .query("myTable") + .keyCondition("userId", "=", "9999") + .execute(); + + expect(result).toMatchSnapshot(); + }); +}); diff --git a/src/queryBuilders/transactionBuilder.ts b/src/queryBuilders/transactionBuilder.ts new file mode 100644 index 0000000..f276d8d --- /dev/null +++ b/src/queryBuilders/transactionBuilder.ts @@ -0,0 +1,44 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { TransactionNode } from "../nodes/transactionNode"; +import { QueryCompiler } from "../queryCompiler"; +import { PutItemQueryBuilder } from "./putItemQueryBuilder"; + +export interface TransactionBuilderInterface { + /** + * TODO: Update, Put, Delete, ConditionCheck + */ + addItem(item: { Put?: PutItemQueryBuilder }): void; + + execute(): Promise; +} + +export class TransactionBuilder + implements TransactionBuilderInterface +{ + readonly #props: TransactionBuilderProps; + + constructor(props: TransactionBuilderProps) { + this.#props = props; + } + + addItem(item: { Put?: PutItemQueryBuilder }) { + this.#props.node.transactItems.push({ + kind: "TransactItemNode", + Put: item.Put?.node, + }); + } + + async execute() { + const transactionCommand = this.#props.queryCompiler.compile( + this.#props.node + ); + + await this.#props.ddbClient.send(transactionCommand); + } +} + +interface TransactionBuilderProps { + readonly node: TransactionNode; + readonly ddbClient: DynamoDBDocumentClient; + readonly queryCompiler: QueryCompiler; +} diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index 1f14087..eea9c1c 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -1,20 +1,27 @@ +import { TransactWriteItem } from "@aws-sdk/client-dynamodb"; import { DeleteCommand, GetCommand, PutCommand, + PutCommandInput, QueryCommand, + TransactWriteCommand, UpdateCommand, } from "@aws-sdk/lib-dynamodb"; +import { AddUpdateExpression } from "../nodes/addUpdateExpression"; import { AttributesNode } from "../nodes/attributesNode"; import { DeleteNode } from "../nodes/deleteNode"; +import { DeleteUpdateExpression } from "../nodes/deleteUpdateExpression"; import { ExpressionJoinTypeNode } from "../nodes/expressionJoinTypeNode"; import { ExpressionNode } from "../nodes/expressionNode"; import { GetNode } from "../nodes/getNode"; import { KeyConditionNode } from "../nodes/keyConditionNode"; import { PutNode } from "../nodes/putNode"; import { QueryNode } from "../nodes/queryNode"; +import { RemoveUpdateExpression } from "../nodes/removeUpdateExpression"; import { SetUpdateExpression } from "../nodes/setUpdateExpression"; import { SetUpdateExpressionFunction } from "../nodes/setUpdateExpressionFunction"; +import { TransactionNode } from "../nodes/transactionNode"; import { UpdateExpression } from "../nodes/updateExpression"; import { UpdateNode } from "../nodes/updateNode"; import { @@ -22,9 +29,6 @@ import { getExpressionAttributeNameFrom, mergeObjectIntoMap, } from "./compilerUtil"; -import { RemoveUpdateExpression } from "../nodes/removeUpdateExpression"; -import { AddUpdateExpression } from "../nodes/addUpdateExpression"; -import { DeleteUpdateExpression } from "../nodes/deleteUpdateExpression"; export class QueryCompiler { compile(rootNode: QueryNode): QueryCommand; @@ -32,7 +36,16 @@ export class QueryCompiler { compile(rootNode: PutNode): PutCommand; compile(rootNode: DeleteNode): DeleteCommand; compile(rootNode: UpdateNode): UpdateCommand; - compile(rootNode: QueryNode | GetNode | PutNode | DeleteNode | UpdateNode) { + compile(rootNode: TransactionNode): TransactWriteCommand; + compile( + rootNode: + | QueryNode + | GetNode + | PutNode + | DeleteNode + | UpdateNode + | TransactionNode + ) { switch (rootNode.kind) { case "GetNode": return this.compileGetNode(rootNode); @@ -44,6 +57,8 @@ export class QueryCompiler { return this.compileDeleteNode(rootNode); case "UpdateNode": return this.compileUpdateNode(rootNode); + case "TransactionNode": + return this.compileTransactionNode(rootNode); } } @@ -122,6 +137,10 @@ export class QueryCompiler { } compilePutNode(putNode: PutNode) { + return new PutCommand(this.compilePutCmdInput(putNode)); + } + + compilePutCmdInput(putNode: PutNode): PutCommandInput { const { table: tableNode, item: itemNode, @@ -138,7 +157,7 @@ export class QueryCompiler { attributeNames ); - return new PutCommand({ + return { TableName: tableNode.table, Item: itemNode?.item, ReturnValues: returnValuesNode?.option, @@ -157,7 +176,7 @@ export class QueryCompiler { ...Object.fromEntries(attributeNames), } : undefined, - }); + }; } compileDeleteNode(deleteNode: DeleteNode) { @@ -252,6 +271,26 @@ export class QueryCompiler { }); } + compileTransactionNode(transactionNode: TransactionNode) { + const TransactItems = transactionNode.transactItems.map((item) => { + const compiledTransactItem: TransactWriteItem = {}; + + if (item.Put) { + compiledTransactItem.Put = this.compilePutCmdInput(item.Put); + } + + return compiledTransactItem; + }); + + console.log("WUT", { + TransactItems: TransactItems.map((x) => x.Put), + }); + + return new TransactWriteCommand({ + TransactItems: TransactItems, + }); + } + compileAttributeNamesNode(node?: AttributesNode) { const ProjectionExpression = node?.attributes .map((att) => getExpressionAttributeNameFrom(att)) diff --git a/src/queryCreator.ts b/src/queryCreator.ts index ced2865..ec698cf 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -1,16 +1,11 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { DeleteItemQueryBuilder } from "./queryBuilders/deleteItemQueryBuilder"; import { GetQueryBuilder } from "./queryBuilders/getItemQueryBuilder"; -import { - PutItemQueryBuilder, - PutItemQueryBuilderInterface, -} from "./queryBuilders/putItemQueryBuilder"; -import { - QueryQueryBuilder, - QueryQueryBuilderInterface, -} from "./queryBuilders/queryQueryBuilder"; -import { QueryCompiler } from "./queryCompiler"; +import { PutItemQueryBuilder } from "./queryBuilders/putItemQueryBuilder"; +import { QueryQueryBuilder } from "./queryBuilders/queryQueryBuilder"; +import { TransactionBuilder } from "./queryBuilders/transactionBuilder"; import { UpdateItemQueryBuilder } from "./queryBuilders/updateItemQueryBuilder"; +import { QueryCompiler } from "./queryCompiler"; export class QueryCreator { readonly #props: QueryCreatorProps; @@ -50,7 +45,7 @@ export class QueryCreator { */ query( table: Table - ): QueryQueryBuilderInterface { + ): QueryQueryBuilder { return new QueryQueryBuilder({ node: { kind: "QueryNode", @@ -77,7 +72,7 @@ export class QueryCreator { */ putItem
( table: Table - ): PutItemQueryBuilderInterface { + ): PutItemQueryBuilder { return new PutItemQueryBuilder({ node: { kind: "PutNode", @@ -146,13 +141,30 @@ export class QueryCreator { setUpdateExpressions: [], removeUpdateExpressions: [], addUpdateExpressions: [], - deleteUpdateExpressions: [] + deleteUpdateExpressions: [], }, }, ddbClient: this.#props.ddbClient, queryCompiler: this.#props.queryCompiler, }); } + + /** + * Returns a builder that can be used to group many different operations + * together and execute them in a transaction. + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/TransactWriteItemsCommand/ + */ + createTransaction() { + return new TransactionBuilder({ + node: { + kind: "TransactionNode", + transactItems: [], + }, + ddbClient: this.#props.ddbClient, + queryCompiler: this.#props.queryCompiler, + }); + } } export interface QueryCreatorProps { From ba663e1b7d1403dd76467cf1adcc1aa546c181c0 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 18 Apr 2024 14:10:21 +0300 Subject: [PATCH 02/15] add support for delete operation in transaction --- src/nodes/TransactItemNode.ts | 2 + src/queryBuilders/deleteItemQueryBuilder.ts | 49 +++++++------ .../transactionBuilder.integration.test.ts | 70 +++++++++++++++++++ src/queryBuilders/transactionBuilder.ts | 14 +++- src/queryCompiler/queryCompiler.ts | 17 +++-- 5 files changed, 121 insertions(+), 31 deletions(-) diff --git a/src/nodes/TransactItemNode.ts b/src/nodes/TransactItemNode.ts index ec9822b..bee2064 100644 --- a/src/nodes/TransactItemNode.ts +++ b/src/nodes/TransactItemNode.ts @@ -1,6 +1,8 @@ +import { DeleteNode } from "./deleteNode"; import { PutNode } from "./putNode"; export type TransactItemNode = { readonly kind: "TransactItemNode"; readonly Put?: PutNode; + readonly Delete?: DeleteNode; }; diff --git a/src/queryBuilders/deleteItemQueryBuilder.ts b/src/queryBuilders/deleteItemQueryBuilder.ts index 50dd957..e74aa81 100644 --- a/src/queryBuilders/deleteItemQueryBuilder.ts +++ b/src/queryBuilders/deleteItemQueryBuilder.ts @@ -24,77 +24,77 @@ import { export interface DeleteItemQueryBuilderInterface< DDB, Table extends keyof DDB, - O + O extends DDB[Table] > { // conditionExpression conditionExpression>( ...args: ComparatorExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; conditionExpression>( ...args: AttributeFuncExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; conditionExpression>( ...args: AttributeBeginsWithExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; conditionExpression>( ...args: AttributeContainsExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; conditionExpression>( ...args: AttributeBetweenExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; conditionExpression>( ...args: NotExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; conditionExpression>( ...args: BuilderExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; // orConditionExpression orConditionExpression>( ...args: ComparatorExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; orConditionExpression>( ...args: AttributeFuncExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; orConditionExpression>( ...args: AttributeBeginsWithExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; orConditionExpression>( ...args: AttributeContainsExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; orConditionExpression>( ...args: AttributeBetweenExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; orConditionExpression>( ...args: NotExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; orConditionExpression>( ...args: BuilderExprArg - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; returnValues( option: Extract - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; returnValuesOnConditionCheckFailure( option: Extract - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; keys & PickSkRequired>( pk: Keys - ): DeleteItemQueryBuilderInterface; + ): DeleteItemQueryBuilder; compile(): DeleteCommand; execute(): Promise[] | undefined>; @@ -117,7 +117,7 @@ export class DeleteItemQueryBuilder< conditionExpression>( ...args: ExprArgs - ): DeleteItemQueryBuilderInterface { + ): DeleteItemQueryBuilder { const eB = new ExpressionBuilder({ node: { ...this.#props.node.conditionExpression }, }); @@ -135,7 +135,7 @@ export class DeleteItemQueryBuilder< orConditionExpression>( ...args: ExprArgs - ): DeleteItemQueryBuilderInterface { + ): DeleteItemQueryBuilder { const eB = new ExpressionBuilder({ node: { ...this.#props.node.conditionExpression }, }); @@ -153,7 +153,7 @@ export class DeleteItemQueryBuilder< returnValues( option: Extract - ): DeleteItemQueryBuilderInterface { + ): DeleteItemQueryBuilder { return new DeleteItemQueryBuilder({ ...this.#props, node: { @@ -168,7 +168,7 @@ export class DeleteItemQueryBuilder< returnValuesOnConditionCheckFailure( option: Extract - ): DeleteItemQueryBuilderInterface { + ): DeleteItemQueryBuilder { return new DeleteItemQueryBuilder({ ...this.#props, node: { @@ -199,11 +199,16 @@ export class DeleteItemQueryBuilder< compile = (): DeleteCommand => { return this.#props.queryCompiler.compile(this.#props.node); }; + execute = async (): Promise[] | 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( diff --git a/src/queryBuilders/transactionBuilder.integration.test.ts b/src/queryBuilders/transactionBuilder.integration.test.ts index bf59af4..0bb04eb 100644 --- a/src/queryBuilders/transactionBuilder.integration.test.ts +++ b/src/queryBuilders/transactionBuilder.integration.test.ts @@ -40,4 +40,74 @@ describe("TransactionBuilder", () => { expect(result).toMatchSnapshot(); }); + + it("handles deletes", async () => { + await tsynamoClient + .putItem("myTable") + .item({ userId: "9999", dataTimestamp: 1 }) + .execute(); + + await tsynamoClient + .putItem("myOtherTable") + .item({ userId: "9999", stringTimestamp: "123" }) + .execute(); + + let foundItem: unknown = await tsynamoClient + .getItem("myTable") + .keys({ + userId: "9999", + dataTimestamp: 1, + }) + .execute(); + + expect(foundItem).toBeDefined(); + + foundItem = await tsynamoClient + .getItem("myOtherTable") + .keys({ + userId: "9999", + stringTimestamp: "123", + }) + .execute(); + + expect(foundItem).toBeDefined(); + + const trx = tsynamoClient.createTransaction(); + + trx.addItem({ + Delete: tsynamoClient.deleteItem("myTable").keys({ + userId: "9999", + dataTimestamp: 1, + }), + }); + + trx.addItem({ + Delete: tsynamoClient.deleteItem("myOtherTable").keys({ + userId: "9999", + stringTimestamp: "123", + }), + }); + + await trx.execute(); + + foundItem = await tsynamoClient + .getItem("myTable") + .keys({ + userId: "9999", + dataTimestamp: 1, + }) + .execute(); + + expect(foundItem).toBeUndefined(); + + foundItem = await tsynamoClient + .getItem("myOtherTable") + .keys({ + userId: "9999", + stringTimestamp: "9999", + }) + .execute(); + + expect(foundItem).toBeUndefined(); + }); }); diff --git a/src/queryBuilders/transactionBuilder.ts b/src/queryBuilders/transactionBuilder.ts index f276d8d..22cb9d7 100644 --- a/src/queryBuilders/transactionBuilder.ts +++ b/src/queryBuilders/transactionBuilder.ts @@ -2,12 +2,16 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { TransactionNode } from "../nodes/transactionNode"; import { QueryCompiler } from "../queryCompiler"; import { PutItemQueryBuilder } from "./putItemQueryBuilder"; +import { DeleteItemQueryBuilder } from "./deleteItemQueryBuilder"; export interface TransactionBuilderInterface { /** - * TODO: Update, Put, Delete, ConditionCheck + * TODO: Update, ConditionCheck */ - addItem(item: { Put?: PutItemQueryBuilder }): void; + addItem(item: { + Put?: PutItemQueryBuilder; + Delete?: DeleteItemQueryBuilder; + }): void; execute(): Promise; } @@ -21,10 +25,14 @@ export class TransactionBuilder this.#props = props; } - addItem(item: { Put?: PutItemQueryBuilder }) { + addItem(item: { + Put?: PutItemQueryBuilder; + Delete?: DeleteItemQueryBuilder; + }) { this.#props.node.transactItems.push({ kind: "TransactItemNode", Put: item.Put?.node, + Delete: item.Delete?.node, }); } diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index eea9c1c..56ee044 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -1,6 +1,7 @@ import { TransactWriteItem } from "@aws-sdk/client-dynamodb"; import { DeleteCommand, + DeleteCommandInput, GetCommand, PutCommand, PutCommandInput, @@ -180,6 +181,10 @@ export class QueryCompiler { } compileDeleteNode(deleteNode: DeleteNode) { + return new DeleteCommand(this.compileDeleteCmdInput(deleteNode)); + } + + compileDeleteCmdInput(deleteNode: DeleteNode): DeleteCommandInput { const { table: tableNode, returnValues: returnValuesNode, @@ -198,7 +203,7 @@ export class QueryCompiler { attributeNames ); - return new DeleteCommand({ + return { TableName: tableNode.table, Key: keysNode?.keys, ReturnValues: returnValuesNode?.option, @@ -219,7 +224,7 @@ export class QueryCompiler { ...Object.fromEntries(attributeNames), } : undefined, - }); + }; } compileUpdateNode(updateNode: UpdateNode) { @@ -279,11 +284,11 @@ export class QueryCompiler { compiledTransactItem.Put = this.compilePutCmdInput(item.Put); } - return compiledTransactItem; - }); + if (item.Delete) { + compiledTransactItem.Delete = this.compileDeleteCmdInput(item.Delete); + } - console.log("WUT", { - TransactItems: TransactItems.map((x) => x.Put), + return compiledTransactItem; }); return new TransactWriteCommand({ From 1e57bb1ca0c193e9d62d45facd73e478c1397d41 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 18 Apr 2024 14:28:13 +0300 Subject: [PATCH 03/15] add support for UPDATE operation in transaction --- src/nodes/TransactItemNode.ts | 2 + ...ransactionBuilder.integration.test.ts.snap | 32 ++++++++++ .../transactionBuilder.integration.test.ts | 41 +++++++++++- src/queryBuilders/transactionBuilder.ts | 6 +- src/queryBuilders/updateItemQueryBuilder.ts | 64 ++++++++++--------- src/queryCompiler/queryCompiler.ts | 16 ++++- 6 files changed, 125 insertions(+), 36 deletions(-) diff --git a/src/nodes/TransactItemNode.ts b/src/nodes/TransactItemNode.ts index bee2064..d8fce78 100644 --- a/src/nodes/TransactItemNode.ts +++ b/src/nodes/TransactItemNode.ts @@ -1,8 +1,10 @@ import { DeleteNode } from "./deleteNode"; import { PutNode } from "./putNode"; +import { UpdateNode } from "./updateNode"; export type TransactItemNode = { readonly kind: "TransactItemNode"; readonly Put?: PutNode; readonly Delete?: DeleteNode; + readonly Update?: UpdateNode; }; diff --git a/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap index 3463d1e..2dee0bd 100644 --- a/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap +++ b/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap @@ -12,3 +12,35 @@ exports[`TransactionBuilder > handles puts 1`] = ` }, ] `; + +exports[`TransactionBuilder > handles transaction with puts 1`] = ` +[ + { + "dataTimestamp": 1, + "userId": "9999", + }, + { + "dataTimestamp": 2, + "userId": "9999", + }, +] +`; + +exports[`TransactionBuilder > handles transaction with updates 1`] = ` +[ + { + "dataTimestamp": 1, + "someBoolean": true, + "userId": "9999", + }, + { + "dataTimestamp": 2, + "tags": [ + "a", + "b", + "c", + ], + "userId": "9999", + }, +] +`; diff --git a/src/queryBuilders/transactionBuilder.integration.test.ts b/src/queryBuilders/transactionBuilder.integration.test.ts index 0bb04eb..506a418 100644 --- a/src/queryBuilders/transactionBuilder.integration.test.ts +++ b/src/queryBuilders/transactionBuilder.integration.test.ts @@ -16,7 +16,7 @@ describe("TransactionBuilder", () => { }); }); - it("handles puts", async () => { + it("handles transaction with puts", async () => { const trx = tsynamoClient.createTransaction(); trx.addItem({ @@ -41,7 +41,7 @@ describe("TransactionBuilder", () => { expect(result).toMatchSnapshot(); }); - it("handles deletes", async () => { + it("handles transaction with deletes", async () => { await tsynamoClient .putItem("myTable") .item({ userId: "9999", dataTimestamp: 1 }) @@ -110,4 +110,41 @@ describe("TransactionBuilder", () => { expect(foundItem).toBeUndefined(); }); + + it("handles transaction with updates", async () => { + await tsynamoClient + .putItem("myTable") + .item({ userId: "1", dataTimestamp: 1 }) + .execute(); + + await tsynamoClient + .putItem("myTable") + .item({ userId: "1", dataTimestamp: 2 }) + .execute(); + + const trx = tsynamoClient.createTransaction(); + + trx.addItem({ + Update: tsynamoClient + .updateItem("myTable") + .keys({ userId: "9999", dataTimestamp: 1 }) + .set("someBoolean", "=", true), + }); + + trx.addItem({ + Update: tsynamoClient + .updateItem("myTable") + .keys({ userId: "9999", dataTimestamp: 2 }) + .set("tags", "=", ["a", "b", "c"]), + }); + + await trx.execute(); + + const result = await tsynamoClient + .query("myTable") + .keyCondition("userId", "=", "9999") + .execute(); + + expect(result).toMatchSnapshot(); + }); }); diff --git a/src/queryBuilders/transactionBuilder.ts b/src/queryBuilders/transactionBuilder.ts index 22cb9d7..b11823b 100644 --- a/src/queryBuilders/transactionBuilder.ts +++ b/src/queryBuilders/transactionBuilder.ts @@ -3,14 +3,16 @@ import { TransactionNode } from "../nodes/transactionNode"; import { QueryCompiler } from "../queryCompiler"; import { PutItemQueryBuilder } from "./putItemQueryBuilder"; import { DeleteItemQueryBuilder } from "./deleteItemQueryBuilder"; +import { UpdateItemQueryBuilder } from "./updateItemQueryBuilder"; export interface TransactionBuilderInterface { /** - * TODO: Update, ConditionCheck + * TODO: ConditionCheck */ addItem(item: { Put?: PutItemQueryBuilder; Delete?: DeleteItemQueryBuilder; + Update?: UpdateItemQueryBuilder; }): void; execute(): Promise; @@ -28,11 +30,13 @@ export class TransactionBuilder addItem(item: { Put?: PutItemQueryBuilder; Delete?: DeleteItemQueryBuilder; + Update?: UpdateItemQueryBuilder; }) { this.#props.node.transactItems.push({ kind: "TransactItemNode", Put: item.Put?.node, Delete: item.Delete?.node, + Update: item.Update?.node, }); } diff --git a/src/queryBuilders/updateItemQueryBuilder.ts b/src/queryBuilders/updateItemQueryBuilder.ts index 6579aa7..bd64e3a 100644 --- a/src/queryBuilders/updateItemQueryBuilder.ts +++ b/src/queryBuilders/updateItemQueryBuilder.ts @@ -31,71 +31,71 @@ import { SetUpdateExpressionFunctionQueryBuilder } from "./setUpdateExpressionFu export interface UpdateItemQueryBuilderInterface< DDB, Table extends keyof DDB, - O + O extends DDB[Table] > { // conditionExpression conditionExpression>( ...args: ComparatorExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; conditionExpression>( ...args: AttributeFuncExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; conditionExpression>( ...args: AttributeBeginsWithExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; conditionExpression>( ...args: AttributeContainsExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; conditionExpression>( ...args: AttributeBetweenExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; conditionExpression>( ...args: NotExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; conditionExpression>( ...args: BuilderExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; // orConditionExpression orConditionExpression>( ...args: ComparatorExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; orConditionExpression>( ...args: AttributeFuncExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; orConditionExpression>( ...args: AttributeBeginsWithExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; orConditionExpression>( ...args: AttributeContainsExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; orConditionExpression>( ...args: AttributeBetweenExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; orConditionExpression>( ...args: NotExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; orConditionExpression>( ...args: BuilderExprArg - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; set>>( key: Key, operand: UpdateExpressionOperands, value: StripKeys> - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; set>>( key: Key, @@ -103,7 +103,7 @@ export interface UpdateItemQueryBuilderInterface< value: ( builder: SetUpdateExpressionFunctionQueryBuilder ) => SetUpdateExpressionFunction - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; set>>( key: Key, @@ -111,16 +111,16 @@ export interface UpdateItemQueryBuilderInterface< value: ( builder: SetUpdateExpressionFunctionQueryBuilder ) => [SetUpdateExpressionFunction, number] - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; keys & PickSkRequired>( pk: Keys - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; // TODO: Make it possible to delete a whole object, and not just nested keys remove>>( attribute: Key - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; add< Key extends ObjectKeyPaths< @@ -129,7 +129,7 @@ export interface UpdateItemQueryBuilderInterface< >( attribute: Key, value: StripKeys> - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; delete< Key extends ObjectKeyPaths< @@ -138,11 +138,11 @@ export interface UpdateItemQueryBuilderInterface< >( attribute: Key, value: StripKeys> - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; returnValues( option: ReturnValuesOptions - ): UpdateItemQueryBuilderInterface; + ): UpdateItemQueryBuilder; compile(): UpdateCommand; execute(): Promise[] | undefined>; @@ -162,7 +162,7 @@ export class UpdateItemQueryBuilder< conditionExpression>( ...args: ExprArgs - ): UpdateItemQueryBuilderInterface { + ): UpdateItemQueryBuilder { const eB = new ExpressionBuilder({ node: { ...this.#props.node.conditionExpression }, }); @@ -180,7 +180,7 @@ export class UpdateItemQueryBuilder< orConditionExpression>( ...args: ExprArgs - ): UpdateItemQueryBuilderInterface { + ): UpdateItemQueryBuilder { const eB = new ExpressionBuilder({ node: { ...this.#props.node.conditionExpression }, }); @@ -225,7 +225,7 @@ export class UpdateItemQueryBuilder< > ) => [SetUpdateExpressionFunction, number] ] - ): UpdateItemQueryBuilderInterface { + ): UpdateItemQueryBuilder { const [key, operand, right] = args; if (typeof right === "function") { @@ -312,7 +312,7 @@ export class UpdateItemQueryBuilder< remove>>( attribute: Key - ): UpdateItemQueryBuilderInterface { + ): UpdateItemQueryBuilder { return new UpdateItemQueryBuilder({ ...this.#props, node: { @@ -332,7 +332,7 @@ export class UpdateItemQueryBuilder< add>>( attribute: Key, value: StripKeys> - ): UpdateItemQueryBuilderInterface { + ): UpdateItemQueryBuilder { return new UpdateItemQueryBuilder({ ...this.#props, node: { @@ -355,7 +355,7 @@ export class UpdateItemQueryBuilder< >( attribute: Key, value: StripKeys> - ): UpdateItemQueryBuilderInterface { + ): UpdateItemQueryBuilder { return new UpdateItemQueryBuilder({ ...this.#props, node: { @@ -375,7 +375,7 @@ export class UpdateItemQueryBuilder< returnValues( option: ReturnValuesOptions - ): UpdateItemQueryBuilderInterface { + ): UpdateItemQueryBuilder { return new UpdateItemQueryBuilder({ ...this.#props, node: { @@ -412,6 +412,10 @@ export class UpdateItemQueryBuilder< const data = await this.#props.ddbClient.send(putCommand); return data.Attributes as any; }; + + public get node() { + return this.#props.node; + } } preventAwait( diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index 56ee044..7355a48 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -1,4 +1,4 @@ -import { TransactWriteItem } from "@aws-sdk/client-dynamodb"; +import { TransactWriteItem, Update } from "@aws-sdk/client-dynamodb"; import { DeleteCommand, DeleteCommandInput, @@ -228,6 +228,10 @@ export class QueryCompiler { } compileUpdateNode(updateNode: UpdateNode) { + return new UpdateCommand(this.compileUpdateCmdInput(updateNode)); + } + + compileUpdateCmdInput(updateNode: UpdateNode) { const { table: tableNode, conditionExpression: conditionExpressionNode, @@ -251,7 +255,7 @@ export class QueryCompiler { attributeNames ); - return new UpdateCommand({ + return { TableName: tableNode.table, Key: keysNode?.keys, ReturnValues: returnValuesNode?.option, @@ -273,7 +277,7 @@ export class QueryCompiler { ...Object.fromEntries(attributeNames), } : undefined, - }); + }; } compileTransactionNode(transactionNode: TransactionNode) { @@ -288,6 +292,12 @@ export class QueryCompiler { compiledTransactItem.Delete = this.compileDeleteCmdInput(item.Delete); } + if (item.Update) { + compiledTransactItem.Update = this.compileUpdateCmdInput( + item.Update + ) as Update; + } + return compiledTransactItem; }); From dfc5a0b54ca43afb64b3900c6121f234c2f81cf6 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 18 Apr 2024 14:38:32 +0300 Subject: [PATCH 04/15] refactor transaction -> writeTransaction --- src/nodes/TransactItemNode.ts | 4 ++-- src/nodes/transactionNode.ts | 8 +++---- ...ansactionBuilder.integration.test.ts.snap} | 13 ----------- ...iteTransactionBuilder.integration.test.ts} | 6 ++--- ...nBuilder.ts => writeTransactionBuilder.ts} | 22 +++++++++---------- src/queryCompiler/queryCompiler.ts | 12 +++++----- src/queryCreator.ts | 10 ++++----- 7 files changed, 31 insertions(+), 44 deletions(-) rename src/queryBuilders/__snapshots__/{transactionBuilder.integration.test.ts.snap => writeTransactionBuilder.integration.test.ts.snap} (75%) rename src/queryBuilders/{transactionBuilder.integration.test.ts => writeTransactionBuilder.integration.test.ts} (95%) rename src/queryBuilders/{transactionBuilder.ts => writeTransactionBuilder.ts} (70%) diff --git a/src/nodes/TransactItemNode.ts b/src/nodes/TransactItemNode.ts index d8fce78..c58f4db 100644 --- a/src/nodes/TransactItemNode.ts +++ b/src/nodes/TransactItemNode.ts @@ -2,8 +2,8 @@ import { DeleteNode } from "./deleteNode"; import { PutNode } from "./putNode"; import { UpdateNode } from "./updateNode"; -export type TransactItemNode = { - readonly kind: "TransactItemNode"; +export type TransactWriteItemNode = { + readonly kind: "TransactWriteItemNode"; readonly Put?: PutNode; readonly Delete?: DeleteNode; readonly Update?: UpdateNode; diff --git a/src/nodes/transactionNode.ts b/src/nodes/transactionNode.ts index b9438fb..8d2a6ce 100644 --- a/src/nodes/transactionNode.ts +++ b/src/nodes/transactionNode.ts @@ -1,6 +1,6 @@ -import { TransactItemNode } from "./TransactItemNode"; +import { TransactWriteItemNode } from "./TransactItemNode"; -export type TransactionNode = { - readonly kind: "TransactionNode"; - readonly transactItems: TransactItemNode[]; +export type WriteTransactionNode = { + readonly kind: "WriteTransactionNode"; + readonly transactWriteItems: TransactWriteItemNode[]; }; diff --git a/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap similarity index 75% rename from src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap rename to src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap index 2dee0bd..af28f37 100644 --- a/src/queryBuilders/__snapshots__/transactionBuilder.integration.test.ts.snap +++ b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap @@ -1,18 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`TransactionBuilder > handles puts 1`] = ` -[ - { - "dataTimestamp": 1, - "userId": "9999", - }, - { - "dataTimestamp": 2, - "userId": "9999", - }, -] -`; - exports[`TransactionBuilder > handles transaction with puts 1`] = ` [ { diff --git a/src/queryBuilders/transactionBuilder.integration.test.ts b/src/queryBuilders/writeTransactionBuilder.integration.test.ts similarity index 95% rename from src/queryBuilders/transactionBuilder.integration.test.ts rename to src/queryBuilders/writeTransactionBuilder.integration.test.ts index 506a418..ae14375 100644 --- a/src/queryBuilders/transactionBuilder.integration.test.ts +++ b/src/queryBuilders/writeTransactionBuilder.integration.test.ts @@ -17,7 +17,7 @@ describe("TransactionBuilder", () => { }); it("handles transaction with puts", async () => { - const trx = tsynamoClient.createTransaction(); + const trx = tsynamoClient.createWriteTransaction(); trx.addItem({ Put: tsynamoClient @@ -72,7 +72,7 @@ describe("TransactionBuilder", () => { expect(foundItem).toBeDefined(); - const trx = tsynamoClient.createTransaction(); + const trx = tsynamoClient.createWriteTransaction(); trx.addItem({ Delete: tsynamoClient.deleteItem("myTable").keys({ @@ -122,7 +122,7 @@ describe("TransactionBuilder", () => { .item({ userId: "1", dataTimestamp: 2 }) .execute(); - const trx = tsynamoClient.createTransaction(); + const trx = tsynamoClient.createWriteTransaction(); trx.addItem({ Update: tsynamoClient diff --git a/src/queryBuilders/transactionBuilder.ts b/src/queryBuilders/writeTransactionBuilder.ts similarity index 70% rename from src/queryBuilders/transactionBuilder.ts rename to src/queryBuilders/writeTransactionBuilder.ts index b11823b..6a2c3df 100644 --- a/src/queryBuilders/transactionBuilder.ts +++ b/src/queryBuilders/writeTransactionBuilder.ts @@ -1,11 +1,11 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; -import { TransactionNode } from "../nodes/transactionNode"; +import { WriteTransactionNode } from "../nodes/transactionNode"; import { QueryCompiler } from "../queryCompiler"; -import { PutItemQueryBuilder } from "./putItemQueryBuilder"; import { DeleteItemQueryBuilder } from "./deleteItemQueryBuilder"; +import { PutItemQueryBuilder } from "./putItemQueryBuilder"; import { UpdateItemQueryBuilder } from "./updateItemQueryBuilder"; -export interface TransactionBuilderInterface { +export interface WriteTransactionBuilderInterface { /** * TODO: ConditionCheck */ @@ -18,12 +18,12 @@ export interface TransactionBuilderInterface { execute(): Promise; } -export class TransactionBuilder - implements TransactionBuilderInterface +export class WriteTransactionBuilder + implements WriteTransactionBuilderInterface { - readonly #props: TransactionBuilderProps; + readonly #props: WriteTransactionBuilderProps; - constructor(props: TransactionBuilderProps) { + constructor(props: WriteTransactionBuilderProps) { this.#props = props; } @@ -32,8 +32,8 @@ export class TransactionBuilder Delete?: DeleteItemQueryBuilder; Update?: UpdateItemQueryBuilder; }) { - this.#props.node.transactItems.push({ - kind: "TransactItemNode", + this.#props.node.transactWriteItems.push({ + kind: "TransactWriteItemNode", Put: item.Put?.node, Delete: item.Delete?.node, Update: item.Update?.node, @@ -49,8 +49,8 @@ export class TransactionBuilder } } -interface TransactionBuilderProps { - readonly node: TransactionNode; +interface WriteTransactionBuilderProps { + readonly node: WriteTransactionNode; readonly ddbClient: DynamoDBDocumentClient; readonly queryCompiler: QueryCompiler; } diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index 7355a48..7061261 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -22,7 +22,7 @@ import { QueryNode } from "../nodes/queryNode"; import { RemoveUpdateExpression } from "../nodes/removeUpdateExpression"; import { SetUpdateExpression } from "../nodes/setUpdateExpression"; import { SetUpdateExpressionFunction } from "../nodes/setUpdateExpressionFunction"; -import { TransactionNode } from "../nodes/transactionNode"; +import { WriteTransactionNode } from "../nodes/transactionNode"; import { UpdateExpression } from "../nodes/updateExpression"; import { UpdateNode } from "../nodes/updateNode"; import { @@ -37,7 +37,7 @@ export class QueryCompiler { compile(rootNode: PutNode): PutCommand; compile(rootNode: DeleteNode): DeleteCommand; compile(rootNode: UpdateNode): UpdateCommand; - compile(rootNode: TransactionNode): TransactWriteCommand; + compile(rootNode: WriteTransactionNode): TransactWriteCommand; compile( rootNode: | QueryNode @@ -45,7 +45,7 @@ export class QueryCompiler { | PutNode | DeleteNode | UpdateNode - | TransactionNode + | WriteTransactionNode ) { switch (rootNode.kind) { case "GetNode": @@ -58,7 +58,7 @@ export class QueryCompiler { return this.compileDeleteNode(rootNode); case "UpdateNode": return this.compileUpdateNode(rootNode); - case "TransactionNode": + case "WriteTransactionNode": return this.compileTransactionNode(rootNode); } } @@ -280,8 +280,8 @@ export class QueryCompiler { }; } - compileTransactionNode(transactionNode: TransactionNode) { - const TransactItems = transactionNode.transactItems.map((item) => { + compileTransactionNode(transactionNode: WriteTransactionNode) { + const TransactItems = transactionNode.transactWriteItems.map((item) => { const compiledTransactItem: TransactWriteItem = {}; if (item.Put) { diff --git a/src/queryCreator.ts b/src/queryCreator.ts index ec698cf..140dd89 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -3,8 +3,8 @@ import { DeleteItemQueryBuilder } from "./queryBuilders/deleteItemQueryBuilder"; import { GetQueryBuilder } from "./queryBuilders/getItemQueryBuilder"; import { PutItemQueryBuilder } from "./queryBuilders/putItemQueryBuilder"; import { QueryQueryBuilder } from "./queryBuilders/queryQueryBuilder"; -import { TransactionBuilder } from "./queryBuilders/transactionBuilder"; import { UpdateItemQueryBuilder } from "./queryBuilders/updateItemQueryBuilder"; +import { WriteTransactionBuilder } from "./queryBuilders/writeTransactionBuilder"; import { QueryCompiler } from "./queryCompiler"; export class QueryCreator { @@ -155,11 +155,11 @@ export class QueryCreator { * * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/TransactWriteItemsCommand/ */ - createTransaction() { - return new TransactionBuilder({ + createWriteTransaction() { + return new WriteTransactionBuilder({ node: { - kind: "TransactionNode", - transactItems: [], + kind: "WriteTransactionNode", + transactWriteItems: [], }, ddbClient: this.#props.ddbClient, queryCompiler: this.#props.queryCompiler, From 5a48f589a881d7bdd1410bb71861227e1483eba9 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 18 Apr 2024 14:39:00 +0300 Subject: [PATCH 05/15] update comment --- src/queryCreator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/queryCreator.ts b/src/queryCreator.ts index 140dd89..cb47597 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -150,8 +150,8 @@ export class QueryCreator { } /** - * Returns a builder that can be used to group many different operations - * together and execute them in a transaction. + * Returns a builder that can be used to group many different write + * operations together and execute them in a transaction. * * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/TransactWriteItemsCommand/ */ From e1a616bac6a9425375a6b7d8b9a37d37c5e4384b Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Fri, 19 Apr 2024 15:30:32 +0300 Subject: [PATCH 06/15] initial work for readTransactions --- src/nodes/readTransactionNode.ts | 6 +++ src/nodes/transactGetItemNode.ts | 6 +++ ...ctItemNode.ts => transactWriteItemNode.ts} | 0 ...sactionNode.ts => writeTransactionNode.ts} | 2 +- ...ransactionBuilder.integration.test.ts.snap | 4 +- src/queryBuilders/getItemQueryBuilder.ts | 19 +++++--- ...readTransactionBuilder.integration.test.ts | 32 +++++++++++++ src/queryBuilders/readTransactionBuilder.ts | 48 +++++++++++++++++++ ...riteTransactionBuilder.integration.test.ts | 2 +- src/queryBuilders/writeTransactionBuilder.ts | 2 +- src/queryCompiler/queryCompiler.ts | 43 ++++++++++++++--- src/queryCreator.ts | 18 +++++++ 12 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 src/nodes/readTransactionNode.ts create mode 100644 src/nodes/transactGetItemNode.ts rename src/nodes/{TransactItemNode.ts => transactWriteItemNode.ts} (100%) rename src/nodes/{transactionNode.ts => writeTransactionNode.ts} (67%) create mode 100644 src/queryBuilders/readTransactionBuilder.integration.test.ts create mode 100644 src/queryBuilders/readTransactionBuilder.ts diff --git a/src/nodes/readTransactionNode.ts b/src/nodes/readTransactionNode.ts new file mode 100644 index 0000000..9ed1dd2 --- /dev/null +++ b/src/nodes/readTransactionNode.ts @@ -0,0 +1,6 @@ +import { TransactGetItemNode } from "./transactGetItemNode"; + +export type ReadTransactionNode = { + readonly kind: "ReadTransactionNode"; + readonly transactGetItems: TransactGetItemNode[]; +}; diff --git a/src/nodes/transactGetItemNode.ts b/src/nodes/transactGetItemNode.ts new file mode 100644 index 0000000..d128b15 --- /dev/null +++ b/src/nodes/transactGetItemNode.ts @@ -0,0 +1,6 @@ +import { GetNode } from "./getNode"; + +export type TransactGetItemNode = { + readonly kind: "TransactGetItemNode"; + readonly Get: GetNode; +}; diff --git a/src/nodes/TransactItemNode.ts b/src/nodes/transactWriteItemNode.ts similarity index 100% rename from src/nodes/TransactItemNode.ts rename to src/nodes/transactWriteItemNode.ts diff --git a/src/nodes/transactionNode.ts b/src/nodes/writeTransactionNode.ts similarity index 67% rename from src/nodes/transactionNode.ts rename to src/nodes/writeTransactionNode.ts index 8d2a6ce..60488dc 100644 --- a/src/nodes/transactionNode.ts +++ b/src/nodes/writeTransactionNode.ts @@ -1,4 +1,4 @@ -import { TransactWriteItemNode } from "./TransactItemNode"; +import { TransactWriteItemNode } from "./transactWriteItemNode"; export type WriteTransactionNode = { readonly kind: "WriteTransactionNode"; diff --git a/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap index af28f37..108e098 100644 --- a/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap +++ b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`TransactionBuilder > handles transaction with puts 1`] = ` +exports[`WriteTransactionBuilder > handles transaction with puts 1`] = ` [ { "dataTimestamp": 1, @@ -13,7 +13,7 @@ exports[`TransactionBuilder > handles transaction with puts 1`] = ` ] `; -exports[`TransactionBuilder > handles transaction with updates 1`] = ` +exports[`WriteTransactionBuilder > handles transaction with updates 1`] = ` [ { "dataTimestamp": 1, diff --git a/src/queryBuilders/getItemQueryBuilder.ts b/src/queryBuilders/getItemQueryBuilder.ts index 2935428..a2eceae 100644 --- a/src/queryBuilders/getItemQueryBuilder.ts +++ b/src/queryBuilders/getItemQueryBuilder.ts @@ -13,19 +13,19 @@ import { preventAwait } from "../util/preventAwait"; export interface GetQueryBuilderInterface { keys & PickSkRequired>( pk: Keys - ): GetQueryBuilderInterface; + ): GetQueryBuilder; - consistentRead(enabled: boolean): GetQueryBuilderInterface; + consistentRead(enabled: boolean): GetQueryBuilder; attributes[] & string[]>( attributes: A - ): GetQueryBuilderInterface>; + ): GetQueryBuilder>; compile(): GetCommand; execute(): Promise | undefined>; } -export class GetQueryBuilder +export class GetQueryBuilder implements GetQueryBuilderInterface { readonly #props: GetQueryBuilderProps; @@ -36,7 +36,7 @@ export class GetQueryBuilder keys & PickSkRequired>( keys: Keys - ) { + ): GetQueryBuilder { return new GetQueryBuilder({ ...this.#props, node: { @@ -49,7 +49,7 @@ export class GetQueryBuilder }); } - consistentRead(enabled: boolean): GetQueryBuilderInterface { + consistentRead(enabled: boolean): GetQueryBuilder { return new GetQueryBuilder({ ...this.#props, node: { @@ -64,7 +64,7 @@ export class GetQueryBuilder attributes[] & string[]>( attributes: A - ): GetQueryBuilderInterface> { + ): GetQueryBuilder> { return new GetQueryBuilder({ ...this.#props, node: { @@ -80,11 +80,16 @@ export class GetQueryBuilder compile(): GetCommand { return this.#props.queryCompiler.compile(this.#props.node); } + execute = async (): Promise | undefined> => { const command = this.compile(); const item = await this.#props.ddbClient.send(command); return (item.Item as ExecuteOutput) ?? undefined; }; + + public get node() { + return this.#props.node; + } } preventAwait( diff --git a/src/queryBuilders/readTransactionBuilder.integration.test.ts b/src/queryBuilders/readTransactionBuilder.integration.test.ts new file mode 100644 index 0000000..b4b227f --- /dev/null +++ b/src/queryBuilders/readTransactionBuilder.integration.test.ts @@ -0,0 +1,32 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { DDB } from "../../test/testFixture"; +import { getDDBClientFor, startDDBTestContainer } from "../../test/testUtil"; +import { Tsynamo } from "../index"; + +describe("ReadTransactionBuilder", () => { + let tsynamoClient: Tsynamo; + let ddbClient: DynamoDBDocumentClient; + + beforeAll(async () => { + const testContainer = await startDDBTestContainer(); + ddbClient = await getDDBClientFor(testContainer); + + tsynamoClient = new Tsynamo({ + ddbClient, + }); + }); + + it("handles transaction with gets", async () => { + const trx = tsynamoClient.createReadTransaction(); + + trx.addItem({ + Get: tsynamoClient.getItem("myTable").keys({ + userId: "123", + dataTimestamp: 222, + }), + }); + + const result = await trx.execute(); + console.log("result", result); + }); +}); diff --git a/src/queryBuilders/readTransactionBuilder.ts b/src/queryBuilders/readTransactionBuilder.ts new file mode 100644 index 0000000..395c33a --- /dev/null +++ b/src/queryBuilders/readTransactionBuilder.ts @@ -0,0 +1,48 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { ReadTransactionNode } from "../nodes/readTransactionNode"; +import { QueryCompiler } from "../queryCompiler"; +import { GetQueryBuilder } from "./getItemQueryBuilder"; + +/** + * @todo What to show as return type? + */ +export interface ReadTransactionBuilderInterface { + addItem
(item: { + Get: GetQueryBuilder; + }): void; + + execute(): Promise; +} + +export class ReadTransactionBuilder + implements ReadTransactionBuilderInterface +{ + readonly #props: ReadTransactionBuilderProps; + + constructor(props: ReadTransactionBuilderProps) { + this.#props = props; + } + + addItem(item: { Get: GetQueryBuilder }) { + this.#props.node.transactGetItems.push({ + kind: "TransactGetItemNode", + Get: item.Get.node, + }); + } + + async execute() { + const transactionCommand = this.#props.queryCompiler.compile( + this.#props.node + ); + + return ( + await this.#props.ddbClient.send(transactionCommand) + ).Responses?.map((o) => o.Item) as unknown[]; + } +} + +interface ReadTransactionBuilderProps { + readonly node: ReadTransactionNode; + readonly ddbClient: DynamoDBDocumentClient; + readonly queryCompiler: QueryCompiler; +} diff --git a/src/queryBuilders/writeTransactionBuilder.integration.test.ts b/src/queryBuilders/writeTransactionBuilder.integration.test.ts index ae14375..1d84582 100644 --- a/src/queryBuilders/writeTransactionBuilder.integration.test.ts +++ b/src/queryBuilders/writeTransactionBuilder.integration.test.ts @@ -3,7 +3,7 @@ import { DDB } from "../../test/testFixture"; import { getDDBClientFor, startDDBTestContainer } from "../../test/testUtil"; import { Tsynamo } from "../index"; -describe("TransactionBuilder", () => { +describe("WriteTransactionBuilder", () => { let tsynamoClient: Tsynamo; let ddbClient: DynamoDBDocumentClient; diff --git a/src/queryBuilders/writeTransactionBuilder.ts b/src/queryBuilders/writeTransactionBuilder.ts index 6a2c3df..7e56863 100644 --- a/src/queryBuilders/writeTransactionBuilder.ts +++ b/src/queryBuilders/writeTransactionBuilder.ts @@ -1,5 +1,5 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; -import { WriteTransactionNode } from "../nodes/transactionNode"; +import { WriteTransactionNode } from "../nodes/writeTransactionNode"; import { QueryCompiler } from "../queryCompiler"; import { DeleteItemQueryBuilder } from "./deleteItemQueryBuilder"; import { PutItemQueryBuilder } from "./putItemQueryBuilder"; diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index 7061261..f599ad6 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -1,11 +1,17 @@ -import { TransactWriteItem, Update } from "@aws-sdk/client-dynamodb"; +import { + TransactGetItem, + TransactWriteItem, + Update, +} from "@aws-sdk/client-dynamodb"; import { DeleteCommand, DeleteCommandInput, GetCommand, + GetCommandInput, PutCommand, PutCommandInput, QueryCommand, + TransactGetCommand, TransactWriteCommand, UpdateCommand, } from "@aws-sdk/lib-dynamodb"; @@ -19,12 +25,13 @@ import { GetNode } from "../nodes/getNode"; import { KeyConditionNode } from "../nodes/keyConditionNode"; import { PutNode } from "../nodes/putNode"; import { QueryNode } from "../nodes/queryNode"; +import { ReadTransactionNode } from "../nodes/readTransactionNode"; import { RemoveUpdateExpression } from "../nodes/removeUpdateExpression"; import { SetUpdateExpression } from "../nodes/setUpdateExpression"; import { SetUpdateExpressionFunction } from "../nodes/setUpdateExpressionFunction"; -import { WriteTransactionNode } from "../nodes/transactionNode"; import { UpdateExpression } from "../nodes/updateExpression"; import { UpdateNode } from "../nodes/updateNode"; +import { WriteTransactionNode } from "../nodes/writeTransactionNode"; import { getAttributeNameFrom, getExpressionAttributeNameFrom, @@ -38,6 +45,7 @@ export class QueryCompiler { compile(rootNode: DeleteNode): DeleteCommand; compile(rootNode: UpdateNode): UpdateCommand; compile(rootNode: WriteTransactionNode): TransactWriteCommand; + compile(rootNode: ReadTransactionNode): TransactGetCommand; compile( rootNode: | QueryNode @@ -46,6 +54,7 @@ export class QueryCompiler { | DeleteNode | UpdateNode | WriteTransactionNode + | ReadTransactionNode ) { switch (rootNode.kind) { case "GetNode": @@ -59,11 +68,17 @@ export class QueryCompiler { case "UpdateNode": return this.compileUpdateNode(rootNode); case "WriteTransactionNode": - return this.compileTransactionNode(rootNode); + return this.compileWriteTransactionNode(rootNode); + case "ReadTransactionNode": + return this.compileReadTransactionNode(rootNode); } } compileGetNode(getNode: GetNode): GetCommand { + return new GetCommand(this.compileGetCmdInput(getNode)); + } + + compileGetCmdInput(getNode: GetNode): GetCommandInput { const { table: tableNode, keys: keysNode, @@ -74,13 +89,13 @@ export class QueryCompiler { const { ProjectionExpression, ExpressionAttributeNames } = this.compileAttributeNamesNode(attributesNode); - return new GetCommand({ + return { TableName: tableNode.table, Key: keysNode?.keys, ConsistentRead: consistentReadNode?.enabled, ProjectionExpression, ExpressionAttributeNames, - }); + }; } compileQueryNode(queryNode: QueryNode): QueryCommand { @@ -280,7 +295,7 @@ export class QueryCompiler { }; } - compileTransactionNode(transactionNode: WriteTransactionNode) { + compileWriteTransactionNode(transactionNode: WriteTransactionNode) { const TransactItems = transactionNode.transactWriteItems.map((item) => { const compiledTransactItem: TransactWriteItem = {}; @@ -306,6 +321,22 @@ export class QueryCompiler { }); } + compileReadTransactionNode(transactionNode: ReadTransactionNode) { + const TransactItems = transactionNode.transactGetItems.map((item) => { + const compiledGet = this.compileGetCmdInput(item.Get); + + const compiledTransactItem: TransactGetItem = { + Get: compiledGet, + }; + + return compiledTransactItem; + }); + + return new TransactGetCommand({ + TransactItems: TransactItems, + }); + } + compileAttributeNamesNode(node?: AttributesNode) { const ProjectionExpression = node?.attributes .map((att) => getExpressionAttributeNameFrom(att)) diff --git a/src/queryCreator.ts b/src/queryCreator.ts index cb47597..50029a6 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -3,6 +3,7 @@ import { DeleteItemQueryBuilder } from "./queryBuilders/deleteItemQueryBuilder"; import { GetQueryBuilder } from "./queryBuilders/getItemQueryBuilder"; import { PutItemQueryBuilder } from "./queryBuilders/putItemQueryBuilder"; import { QueryQueryBuilder } from "./queryBuilders/queryQueryBuilder"; +import { ReadTransactionBuilder } from "./queryBuilders/readTransactionBuilder"; import { UpdateItemQueryBuilder } from "./queryBuilders/updateItemQueryBuilder"; import { WriteTransactionBuilder } from "./queryBuilders/writeTransactionBuilder"; import { QueryCompiler } from "./queryCompiler"; @@ -165,6 +166,23 @@ export class QueryCreator { queryCompiler: this.#props.queryCompiler, }); } + + /** + * Returns a builder that can be used to group many different get + * operations together and execute them in a transaction. + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/TransactGetItemsCommand/ + */ + createReadTransaction() { + return new ReadTransactionBuilder({ + node: { + kind: "ReadTransactionNode", + transactGetItems: [], + }, + ddbClient: this.#props.ddbClient, + queryCompiler: this.#props.queryCompiler, + }); + } } export interface QueryCreatorProps { From ca1dd011941acd60124f5cfa225437b660a4546f Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 30 May 2024 14:32:02 +0300 Subject: [PATCH 07/15] fix test --- src/queryBuilders/readTransactionBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queryBuilders/readTransactionBuilder.ts b/src/queryBuilders/readTransactionBuilder.ts index 395c33a..28fcfb4 100644 --- a/src/queryBuilders/readTransactionBuilder.ts +++ b/src/queryBuilders/readTransactionBuilder.ts @@ -14,7 +14,7 @@ export interface ReadTransactionBuilderInterface { execute(): Promise; } -export class ReadTransactionBuilder +export class ReadTransactionBuilder implements ReadTransactionBuilderInterface { readonly #props: ReadTransactionBuilderProps; From fff584aa905e0e6c7d6ad048022df4b2f395bfa9 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 30 May 2024 15:21:00 +0300 Subject: [PATCH 08/15] make it possible to retrieve types of the returned items when doing a read transaction request --- ...ransactionBuilder.integration.test.ts.snap | 25 +++++++++++++++++++ ...readTransactionBuilder.integration.test.ts | 19 +++++++++++++- src/queryBuilders/readTransactionBuilder.ts | 23 ++++++++++++----- src/typeHelpers.ts | 17 +++++++++++++ 4 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 src/queryBuilders/__snapshots__/readTransactionBuilder.integration.test.ts.snap diff --git a/src/queryBuilders/__snapshots__/readTransactionBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/readTransactionBuilder.integration.test.ts.snap new file mode 100644 index 0000000..4977c04 --- /dev/null +++ b/src/queryBuilders/__snapshots__/readTransactionBuilder.integration.test.ts.snap @@ -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, + ], +] +`; diff --git a/src/queryBuilders/readTransactionBuilder.integration.test.ts b/src/queryBuilders/readTransactionBuilder.integration.test.ts index b4b227f..7dc3081 100644 --- a/src/queryBuilders/readTransactionBuilder.integration.test.ts +++ b/src/queryBuilders/readTransactionBuilder.integration.test.ts @@ -26,7 +26,24 @@ describe("ReadTransactionBuilder", () => { }), }); + trx.addItem({ + Get: tsynamoClient + .getItem("myOtherTable") + .keys({ + userId: "123", + stringTimestamp: "111", + }) + .attributes(["userId"]), + }); + + trx.addItem({ + Get: tsynamoClient.getItem("myTable").keys({ + userId: "1111111", + dataTimestamp: 2222, + }), + }); + const result = await trx.execute(); - console.log("result", result); + expect(result).toMatchSnapshot(); }); }); diff --git a/src/queryBuilders/readTransactionBuilder.ts b/src/queryBuilders/readTransactionBuilder.ts index 28fcfb4..0362ed0 100644 --- a/src/queryBuilders/readTransactionBuilder.ts +++ b/src/queryBuilders/readTransactionBuilder.ts @@ -1,43 +1,54 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { ReadTransactionNode } from "../nodes/readTransactionNode"; import { QueryCompiler } from "../queryCompiler"; +import { AllTuples } from "../typeHelpers"; import { GetQueryBuilder } from "./getItemQueryBuilder"; -/** - * @todo What to show as return type? - */ export interface ReadTransactionBuilderInterface { addItem
(item: { Get: GetQueryBuilder; }): void; - execute(): Promise; + /** + * The return value is an array of tuples, where the first item + * tells the name of the table, and the right item is the result + * item itself (or undefined if not found). The table can be used + * for discriminated union to determine the actual type of the result + * item. + */ + execute(): Promise[]>; } export class ReadTransactionBuilder implements ReadTransactionBuilderInterface { readonly #props: ReadTransactionBuilderProps; + resultTables: string[] = []; constructor(props: ReadTransactionBuilderProps) { this.#props = props; } addItem(item: { Get: GetQueryBuilder }) { + this.resultTables.push(item.Get.node.table.table); + this.#props.node.transactGetItems.push({ kind: "TransactGetItemNode", Get: item.Get.node, }); } - async execute() { + async execute(): Promise[]> { const transactionCommand = this.#props.queryCompiler.compile( this.#props.node ); return ( await this.#props.ddbClient.send(transactionCommand) - ).Responses?.map((o) => o.Item) as unknown[]; + ).Responses?.map((o, i) => [ + this.resultTables[i], + o.Item, + ]) as AllTuples[]; } } diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts index 1edbb2e..b87775c 100644 --- a/src/typeHelpers.ts +++ b/src/typeHelpers.ts @@ -247,3 +247,20 @@ export type ObjectFullPaths = never : // Leaf value reached, don't return anything never; + +/** + * Creates a tuple where the first item is the name of the table + * and the second item is the schema of the table. + */ +type DependentTuple = [ + Table, + ExecuteOutput | undefined +]; + +/** + * Generates a type that can be used for discriminated union + * when returning data from multiple different tables. + */ +export type AllTuples = { + [Table in keyof DDB]: DependentTuple; +}[keyof DDB]; From bf2994d293edfbca25ac39b45c147172590e9694 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 30 May 2024 15:39:55 +0300 Subject: [PATCH 09/15] add test for a write transaction with failing conditions --- ...ransactionBuilder.integration.test.ts.snap | 2 ++ ...riteTransactionBuilder.integration.test.ts | 23 +++++++++++++++++++ src/queryBuilders/writeTransactionBuilder.ts | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap index 108e098..686d43b 100644 --- a/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap +++ b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap @@ -1,5 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`WriteTransactionBuilder > handles transaction with failing conditions 1`] = `[TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed]]`; + exports[`WriteTransactionBuilder > handles transaction with puts 1`] = ` [ { diff --git a/src/queryBuilders/writeTransactionBuilder.integration.test.ts b/src/queryBuilders/writeTransactionBuilder.integration.test.ts index 1d84582..40b6e59 100644 --- a/src/queryBuilders/writeTransactionBuilder.integration.test.ts +++ b/src/queryBuilders/writeTransactionBuilder.integration.test.ts @@ -147,4 +147,27 @@ describe("WriteTransactionBuilder", () => { expect(result).toMatchSnapshot(); }); + + it("handles transaction with failing conditions", async () => { + // Create a conflicting entry + await tsynamoClient + .putItem("myTable") + .item({ userId: "1", dataTimestamp: 2 }) + .execute(); + + const trx = tsynamoClient.createWriteTransaction(); + + trx.addItem({ + Put: tsynamoClient + .putItem("myTable") + .item({ + userId: "1", + dataTimestamp: 2, + someBoolean: true, + }) + .conditionExpression("userId", "attribute_not_exists"), + }); + + expect(trx.execute()).rejects.toMatchSnapshot(); + }); }); diff --git a/src/queryBuilders/writeTransactionBuilder.ts b/src/queryBuilders/writeTransactionBuilder.ts index 7e56863..573b199 100644 --- a/src/queryBuilders/writeTransactionBuilder.ts +++ b/src/queryBuilders/writeTransactionBuilder.ts @@ -7,7 +7,7 @@ import { UpdateItemQueryBuilder } from "./updateItemQueryBuilder"; export interface WriteTransactionBuilderInterface { /** - * TODO: ConditionCheck + * @todo add support for ConditionCheck items */ addItem(item: { Put?: PutItemQueryBuilder; From fcbf17c2f54c1fae7dde06fdbb6235de8cb47244 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 30 May 2024 16:02:56 +0300 Subject: [PATCH 10/15] add documentation for transactions --- README.md | 78 +++++++++++++++++++- src/queryBuilders/writeTransactionBuilder.ts | 4 + 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1403600..abd89f8 100644 --- a/README.md +++ b/README.md @@ -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) @@ -72,6 +72,7 @@ export interface DDB { }; } ``` + > [!TIP] > Notice that you can have multiple tables in the DDB schema. Nested attributes are supported too. @@ -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" @@ -282,6 +284,80 @@ 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 + +```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. + +#### 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 +result.forEach(([table, item]) => { + // note that item can be undefined + if (table === "myTable") { + // item's type is DDB["myTable"] + // ... + } else if (table === "myOtherTable") { + // item's type is DDB["myOtherTable"] + // ... + } +}); +``` + ## Contributors

diff --git a/src/queryBuilders/writeTransactionBuilder.ts b/src/queryBuilders/writeTransactionBuilder.ts index 573b199..b9ae358 100644 --- a/src/queryBuilders/writeTransactionBuilder.ts +++ b/src/queryBuilders/writeTransactionBuilder.ts @@ -15,6 +15,10 @@ export interface WriteTransactionBuilderInterface { Update?: UpdateItemQueryBuilder; }): void; + /** + * @todo add support for ClientRequestToken + */ + execute(): Promise; } From 77075e35ee0a11e1316f56b3f823cdd4160dc527 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Thu, 30 May 2024 16:03:33 +0300 Subject: [PATCH 11/15] fix comment --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abd89f8..7359e5e 100644 --- a/README.md +++ b/README.md @@ -346,8 +346,8 @@ const result = await trx.execute(); Then, one can loop through the result items as so: ```ts +// note that item can be undefined result.forEach(([table, item]) => { - // note that item can be undefined if (table === "myTable") { // item's type is DDB["myTable"] // ... From 6757874b944f9b7b6ceae765fda9c493f5ec625e Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Fri, 31 May 2024 10:45:57 +0300 Subject: [PATCH 12/15] add support for ClientRequestToken when doing write transactions --- src/nodes/writeTransactionNode.ts | 1 + ...ransactionBuilder.integration.test.ts.snap | 8 +++-- ...riteTransactionBuilder.integration.test.ts | 31 ++++++++++++++++--- src/queryBuilders/writeTransactionBuilder.ts | 4 --- src/queryCompiler/queryCompiler.ts | 1 + src/queryCreator.ts | 9 +++++- test/testUtil.ts | 2 +- 7 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/nodes/writeTransactionNode.ts b/src/nodes/writeTransactionNode.ts index 60488dc..b33074d 100644 --- a/src/nodes/writeTransactionNode.ts +++ b/src/nodes/writeTransactionNode.ts @@ -3,4 +3,5 @@ import { TransactWriteItemNode } from "./transactWriteItemNode"; export type WriteTransactionNode = { readonly kind: "WriteTransactionNode"; readonly transactWriteItems: TransactWriteItemNode[]; + readonly clientRequestToken?: string; }; diff --git a/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap index 686d43b..2f841ea 100644 --- a/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap +++ b/src/queryBuilders/__snapshots__/writeTransactionBuilder.integration.test.ts.snap @@ -1,8 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`WriteTransactionBuilder > handles transaction with failing conditions 1`] = `[TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed]]`; +exports[`WriteTransactionBuilder > handles a transaction with a client request token 1`] = `[IdempotentParameterMismatchException: UnknownError]`; -exports[`WriteTransactionBuilder > handles transaction with puts 1`] = ` +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, @@ -15,7 +17,7 @@ exports[`WriteTransactionBuilder > handles transaction with puts 1`] = ` ] `; -exports[`WriteTransactionBuilder > handles transaction with updates 1`] = ` +exports[`WriteTransactionBuilder > handles a transaction with updates 1`] = ` [ { "dataTimestamp": 1, diff --git a/src/queryBuilders/writeTransactionBuilder.integration.test.ts b/src/queryBuilders/writeTransactionBuilder.integration.test.ts index 40b6e59..c21c7f9 100644 --- a/src/queryBuilders/writeTransactionBuilder.integration.test.ts +++ b/src/queryBuilders/writeTransactionBuilder.integration.test.ts @@ -16,7 +16,7 @@ describe("WriteTransactionBuilder", () => { }); }); - it("handles transaction with puts", async () => { + it("handles a transaction with puts", async () => { const trx = tsynamoClient.createWriteTransaction(); trx.addItem({ @@ -41,7 +41,7 @@ describe("WriteTransactionBuilder", () => { expect(result).toMatchSnapshot(); }); - it("handles transaction with deletes", async () => { + it("handles a transaction with deletes", async () => { await tsynamoClient .putItem("myTable") .item({ userId: "9999", dataTimestamp: 1 }) @@ -111,7 +111,7 @@ describe("WriteTransactionBuilder", () => { expect(foundItem).toBeUndefined(); }); - it("handles transaction with updates", async () => { + it("handles a transaction with updates", async () => { await tsynamoClient .putItem("myTable") .item({ userId: "1", dataTimestamp: 1 }) @@ -148,7 +148,7 @@ describe("WriteTransactionBuilder", () => { expect(result).toMatchSnapshot(); }); - it("handles transaction with failing conditions", async () => { + it("handles a transaction with failing conditions", async () => { // Create a conflicting entry await tsynamoClient .putItem("myTable") @@ -170,4 +170,27 @@ describe("WriteTransactionBuilder", () => { expect(trx.execute()).rejects.toMatchSnapshot(); }); + + it("handles a transaction with a client request token", async () => { + const trx = tsynamoClient.createWriteTransaction("hello token"); + + trx.addItem({ + Put: tsynamoClient.putItem("myTable").item({ + userId: "1", + dataTimestamp: 2, + }), + }); + + await trx.execute(); + + trx.addItem({ + Put: tsynamoClient.putItem("myTable").item({ + userId: "1", + dataTimestamp: 2, + someBoolean: true, + }), + }); + + expect(trx.execute()).rejects.toMatchSnapshot(); + }); }); diff --git a/src/queryBuilders/writeTransactionBuilder.ts b/src/queryBuilders/writeTransactionBuilder.ts index b9ae358..573b199 100644 --- a/src/queryBuilders/writeTransactionBuilder.ts +++ b/src/queryBuilders/writeTransactionBuilder.ts @@ -15,10 +15,6 @@ export interface WriteTransactionBuilderInterface { Update?: UpdateItemQueryBuilder; }): void; - /** - * @todo add support for ClientRequestToken - */ - execute(): Promise; } diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index f599ad6..9500536 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -318,6 +318,7 @@ export class QueryCompiler { return new TransactWriteCommand({ TransactItems: TransactItems, + ClientRequestToken: transactionNode.clientRequestToken, }); } diff --git a/src/queryCreator.ts b/src/queryCreator.ts index 50029a6..dfc8982 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -154,13 +154,20 @@ export class QueryCreator { * Returns a builder that can be used to group many different write * operations together and execute them in a transaction. * + * @param clientRequestToken + * From AWS documentation: Providing a ClientRequestToken makes the call to TransactWriteItems idempotent, meaning that multiple identical calls have the same effect as one single call. + * Although multiple identical calls using the same client request token produce the same result on the server (no side effects), the responses to the calls might not be the same. If the ReturnConsumedCapacity parameter is set, then the initial TransactWriteItems call returns the amount of write capacity units consumed in making the changes. Subsequent TransactWriteItems calls with the same client token return the number of read capacity units consumed in reading the item. + * A client request token is valid for 10 minutes after the first request that uses it is completed. After 10 minutes, any request with the same client token is treated as a new request. Do not resubmit the same request with the same client token for more than 10 minutes, or the result might not be idempotent. + * If you submit a request with the same client token but a change in other parameters within the 10-minute idempotency window, DynamoDB returns an IdempotentParameterMismatch exception. + * * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/TransactWriteItemsCommand/ */ - createWriteTransaction() { + createWriteTransaction(clientRequestToken?: string) { return new WriteTransactionBuilder({ node: { kind: "WriteTransactionNode", transactWriteItems: [], + clientRequestToken, }, ddbClient: this.#props.ddbClient, queryCompiler: this.#props.queryCompiler, diff --git a/test/testUtil.ts b/test/testUtil.ts index 4242dea..c56d5af 100644 --- a/test/testUtil.ts +++ b/test/testUtil.ts @@ -5,7 +5,7 @@ import { GenericContainer, StartedTestContainer } from "testcontainers"; const DDB_PORT = 8000 as const; export const startDDBTestContainer = async () => { - return new GenericContainer("amazon/dynamodb-local") + return new GenericContainer("amazon/dynamodb-local:2.5.0") .withReuse() .withExposedPorts(DDB_PORT) .start(); From b65d609945bf180ac998bba5b95730099c26e999 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Fri, 31 May 2024 13:56:58 +0300 Subject: [PATCH 13/15] 0.0.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 815009d..45f3530 100644 --- a/package.json +++ b/package.json @@ -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", From 5d48a28a96243151d86a021c6cac1a3268cbe6b7 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Fri, 31 May 2024 14:01:43 +0300 Subject: [PATCH 14/15] update readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 7359e5e..25cad01 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,8 @@ One can also utilise [DynamoDB Transaction](https://docs.aws.amazon.com/amazondy #### 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(); @@ -319,6 +321,9 @@ 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. From ee8cd3043afac6162b5e2e7f414b6f199a5a6b86 Mon Sep 17 00:00:00 2001 From: Olli Warro Date: Fri, 31 May 2024 14:02:04 +0300 Subject: [PATCH 15/15] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25cad01..02b8e31 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ const result = await trx.execute(); Then, one can loop through the result items as so: ```ts -// note that item can be undefined +// 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"]