diff --git a/src/nodes/updateNode.ts b/src/nodes/updateNode.ts new file mode 100644 index 0000000..65965cb --- /dev/null +++ b/src/nodes/updateNode.ts @@ -0,0 +1,12 @@ +import { ExpressionNode } from "./expressionNode"; +import { ItemNode } from "./itemNode"; +import { ReturnValuesNode } from "./returnValuesNode"; +import { TableNode } from "./tableNode"; + +export type UpdateNode = { + readonly kind: "UpdateNode"; + readonly table: TableNode; + readonly conditionExpression: ExpressionNode; + readonly item?: ItemNode; + readonly returnValues?: ReturnValuesNode; +}; diff --git a/src/queryBuilders/updateItemQueryBuilder.integration.test.ts b/src/queryBuilders/updateItemQueryBuilder.integration.test.ts new file mode 100644 index 0000000..fb7a0f7 --- /dev/null +++ b/src/queryBuilders/updateItemQueryBuilder.integration.test.ts @@ -0,0 +1,17 @@ +import { DDB } from "../../test/testFixture"; +import { getDDBClientFor, startDDBTestContainer } from "../../test/testUtil"; +import { Tsynamo } from "./../index"; + +describe("UpdateItemQueryBuilder", () => { + let tsynamoClient: Tsynamo; + + beforeAll(async () => { + const testContainer = await startDDBTestContainer(); + + tsynamoClient = new Tsynamo({ + ddbClient: await getDDBClientFor(testContainer), + }); + }); + + it.todo("handles a simple update item query"); +}); diff --git a/src/queryBuilders/updateItemQueryBuilder.ts b/src/queryBuilders/updateItemQueryBuilder.ts new file mode 100644 index 0000000..1d71fa4 --- /dev/null +++ b/src/queryBuilders/updateItemQueryBuilder.ts @@ -0,0 +1,173 @@ +import { DynamoDBDocumentClient, UpdateCommand } from "@aws-sdk/lib-dynamodb"; +import { ReturnValuesOptions } from "../nodes/returnValuesNode"; +import { UpdateNode } from "../nodes/updateNode"; +import { QueryCompiler } from "../queryCompiler"; +import { ExecuteOutput, ObjectKeyPaths } from "../typeHelpers"; +import { preventAwait } from "../util/preventAwait"; +import { + AttributeBeginsWithExprArg, + AttributeBetweenExprArg, + AttributeContainsExprArg, + AttributeFuncExprArg, + BuilderExprArg, + ComparatorExprArg, + ExprArgs, + ExpressionBuilder, + NotExprArg, +} from "./expressionBuilder"; + +export interface UpdateItemQueryBuilderInterface< + DDB, + Table extends keyof DDB, + O +> { + // conditionExpression + conditionExpression>( + ...args: ComparatorExprArg + ): UpdateItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeFuncExprArg + ): UpdateItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeBeginsWithExprArg + ): UpdateItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeContainsExprArg + ): UpdateItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeBetweenExprArg + ): UpdateItemQueryBuilderInterface; + + conditionExpression>( + ...args: NotExprArg + ): UpdateItemQueryBuilderInterface; + + conditionExpression>( + ...args: BuilderExprArg + ): UpdateItemQueryBuilderInterface; + + // orConditionExpression + orConditionExpression>( + ...args: ComparatorExprArg + ): UpdateItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeFuncExprArg + ): UpdateItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeBeginsWithExprArg + ): UpdateItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeContainsExprArg + ): UpdateItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeBetweenExprArg + ): UpdateItemQueryBuilderInterface; + + orConditionExpression>( + ...args: NotExprArg + ): UpdateItemQueryBuilderInterface; + + orConditionExpression>( + ...args: BuilderExprArg + ): UpdateItemQueryBuilderInterface; + + returnValues( + option: ReturnValuesOptions + ): UpdateItemQueryBuilderInterface; + + compile(): UpdateCommand; + execute(): Promise[] | undefined>; +} + +export class UpdateItemQueryBuilder< + DDB, + Table extends keyof DDB, + O extends DDB[Table] +> implements UpdateItemQueryBuilderInterface +{ + readonly #props: UpdateItemQueryBuilderProps; + + constructor(props: UpdateItemQueryBuilderProps) { + this.#props = props; + } + + conditionExpression>( + ...args: ExprArgs + ): UpdateItemQueryBuilderInterface { + const eB = new ExpressionBuilder({ + node: { ...this.#props.node.conditionExpression }, + }); + + const expressionNode = eB.expression(...args)._getNode(); + + return new UpdateItemQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + conditionExpression: expressionNode, + }, + }); + } + + orConditionExpression>( + ...args: ExprArgs + ): UpdateItemQueryBuilderInterface { + const eB = new ExpressionBuilder({ + node: { ...this.#props.node.conditionExpression }, + }); + + const expressionNode = eB.orExpression(...args)._getNode(); + + return new UpdateItemQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + conditionExpression: expressionNode, + }, + }); + } + + returnValues( + option: ReturnValuesOptions + ): UpdateItemQueryBuilderInterface { + return new UpdateItemQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + returnValues: { + kind: "ReturnValuesNode", + option, + }, + }, + }); + } + + compile = (): UpdateCommand => { + 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; + }; +} + +preventAwait( + UpdateItemQueryBuilder, + "Don't await UpdateItemQueryBuilder instances directly. To execute the query you need to call the `execute` method" +); + +interface UpdateItemQueryBuilderProps { + readonly node: UpdateNode; + readonly ddbClient: DynamoDBDocumentClient; + readonly queryCompiler: QueryCompiler; +} diff --git a/src/queryCompiler/queryCompiler.test.ts b/src/queryCompiler/queryCompiler.test.ts index 2b5fb24..e61f75b 100644 --- a/src/queryCompiler/queryCompiler.test.ts +++ b/src/queryCompiler/queryCompiler.test.ts @@ -37,6 +37,7 @@ describe("QueryQueryBuilder", () => { expect(data).toMatchSnapshot(); }); + it("getItemQueryBuilder can be compiled", async () => { const data = await tsynamoClient .getItem("myTable") @@ -48,6 +49,7 @@ describe("QueryQueryBuilder", () => { expect(data).toMatchSnapshot(); }); + it("deleteItemQueryBuilder can be compiled", async () => { const data = await tsynamoClient .deleteItem("myTable") @@ -60,4 +62,6 @@ describe("QueryQueryBuilder", () => { expect(data).toMatchSnapshot(); }); + + it.todo("updateItemQueryBuilder can be compiled"); }); diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index 83c919c..b323f21 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -3,6 +3,7 @@ import { GetCommand, PutCommand, QueryCommand, + UpdateCommand, } from "@aws-sdk/lib-dynamodb"; import { ExpressionJoinTypeNode } from "../nodes/expressionJoinTypeNode"; import { ExpressionNode } from "../nodes/expressionNode"; @@ -17,13 +18,15 @@ import { import { AttributesNode } from "../nodes/attributesNode"; import { PutNode } from "../nodes/putNode"; import { DeleteNode } from "../nodes/deleteNode"; +import { UpdateNode } from "../nodes/updateNode"; export class QueryCompiler { compile(rootNode: QueryNode): QueryCommand; compile(rootNode: GetNode): GetCommand; compile(rootNode: PutNode): PutCommand; compile(rootNode: DeleteNode): DeleteCommand; - compile(rootNode: QueryNode | GetNode | PutNode | DeleteNode) { + compile(rootNode: UpdateNode): UpdateCommand; + compile(rootNode: QueryNode | GetNode | PutNode | DeleteNode | UpdateNode) { switch (rootNode.kind) { case "GetNode": return this.compileGetNode(rootNode); @@ -33,6 +36,8 @@ export class QueryCompiler { return this.compilePutNode(rootNode); case "DeleteNode": return this.compileDeleteNode(rootNode); + case "UpdateNode": + return this.compileUpdateNode(rootNode); } } @@ -192,6 +197,13 @@ export class QueryCompiler { }); } + compileUpdateNode(updateNode: UpdateNode) { + return new UpdateCommand({ + TableName: "TODO", + Key: {}, + }); + } + compileAttributeNamesNode(node?: AttributesNode) { const ProjectionExpression = node?.attributes .map((att) => getExpressionAttributeNameFrom(att)) diff --git a/src/queryCreator.ts b/src/queryCreator.ts index d3d191e..fc22949 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -10,6 +10,7 @@ import { QueryQueryBuilderInterface, } from "./queryBuilders/queryQueryBuilder"; import { QueryCompiler } from "./queryCompiler"; +import { UpdateItemQueryBuilder } from "./queryBuilders/updateItemQueryBuilder"; export class QueryCreator { readonly #props: QueryCreatorProps; @@ -119,6 +120,32 @@ export class QueryCreator { queryCompiler: this.#props.queryCompiler, }); } + + /** + * + * @param table Table to perform the update item command to + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/UpdateItemCommand/ + */ + updateItem( + table: Table + ): UpdateItemQueryBuilder { + return new UpdateItemQueryBuilder({ + node: { + kind: "UpdateNode", + table: { + kind: "TableNode", + table, + }, + conditionExpression: { + kind: "ExpressionNode", + expressions: [], + }, + }, + ddbClient: this.#props.ddbClient, + queryCompiler: this.#props.queryCompiler, + }); + } } export interface QueryCreatorProps {