diff --git a/README.md b/README.md index f77dfea..f631ecd 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ Type-friendly DynamoDB query builder! Inspired by [Kysely](https://github.com/ky Usable with AWS SDK v3 `DynamoDBDocumentClient`. -> [!NOTE] -> Currently this is a POC and a WIP. Currently, `get-item` and `query` operations are -> supported, but I am planning to add support for the rest of the operations too. - ![](https://github.com/woltsu/tsynamo/blob/main/assets/demo.gif) # Installation @@ -163,11 +159,48 @@ await tsynamoClient > This would compile as the following FilterExpression: > `NOT eventType = "LOG_IN"`, i.e. return all events whose types is not "LOG_IN" -## Delete item +## Put item -WIP +### Simple put item -## Put item +```ts +await tsynamoClient + .putItem("myTable") + .item({ + userId: "123", + eventId: 313, + }) + .execute(); +``` + +### Put item with ConditionExpression + +```ts +await tsynamoClient + .putItem("myTable") + .item({ + userId: "123", + eventId: 313, + }) + .conditionExpression("userId", "attribute_not_exists") + .execute(); +``` + +### Put item with multiple ConditionExpressions + +```ts +await tsynamoClient + .putItem("myTable") + .item({ + userId: "123", + eventId: 313, + }) + .conditionExpression("userId", "attribute_not_exists") + .orConditionExpression("eventType", "begins_with", "LOG_") + .execute(); +``` + +## Delete item WIP @@ -180,8 +213,9 @@ WIP WIP # Contributors +

-

\ No newline at end of file +

diff --git a/package.json b/package.json index 9bb4a85..74d1490 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tsynamo", "author": "woltsu", - "version": "0.0.5", + "version": "0.0.6", "description": "Typed query builder for DynamoDB", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/nodes/expressionComparatorExpression.ts b/src/nodes/expressionComparatorExpression.ts new file mode 100644 index 0000000..5d64a72 --- /dev/null +++ b/src/nodes/expressionComparatorExpression.ts @@ -0,0 +1,8 @@ +import { ExpressionConditionComparators } from "./operands"; + +export type ExpressionComparatorExpressions = { + readonly kind: "ExpressionComparatorExpressions"; + readonly key: string; + readonly operation: ExpressionConditionComparators; + readonly value: unknown; +}; diff --git a/src/nodes/filterExpressionJoinTypeNode.ts b/src/nodes/expressionJoinTypeNode.ts similarity index 60% rename from src/nodes/filterExpressionJoinTypeNode.ts rename to src/nodes/expressionJoinTypeNode.ts index 97bfc0c..e589490 100644 --- a/src/nodes/filterExpressionJoinTypeNode.ts +++ b/src/nodes/expressionJoinTypeNode.ts @@ -3,18 +3,18 @@ import { AttributeNotExistsFunctionExpression } from "./attributeNotExistsFuncti import { BeginsWithFunctionExpression } from "./beginsWithFunctionExpression"; import { BetweenConditionExpression } from "./betweenConditionExpression"; import { ContainsFunctionExpression } from "./containsFunctionExpression"; -import { FilterExpressionComparatorExpressions } from "./filterExpressionComparatorExpression"; -import { FilterExpressionNode } from "./filterExpressionNode"; -import { FilterExpressionNotExpression } from "./filterExpressionNotExpression"; +import { ExpressionComparatorExpressions } from "./expressionComparatorExpression"; +import { ExpressionNode } from "./expressionNode"; +import { ExpressionNotExpression } from "./expressionNotExpression"; export type JoinType = "AND" | "OR"; -export type FilterExpressionJoinTypeNode = { - readonly kind: "FilterExpressionJoinTypeNode"; +export type ExpressionJoinTypeNode = { + readonly kind: "ExpressionJoinTypeNode"; readonly expr: - | FilterExpressionNode - | FilterExpressionComparatorExpressions - | FilterExpressionNotExpression + | ExpressionNode + | ExpressionComparatorExpressions + | ExpressionNotExpression | AttributeExistsFunctionExpression | AttributeNotExistsFunctionExpression | BetweenConditionExpression diff --git a/src/nodes/expressionNode.ts b/src/nodes/expressionNode.ts new file mode 100644 index 0000000..390e6c7 --- /dev/null +++ b/src/nodes/expressionNode.ts @@ -0,0 +1,6 @@ +import { ExpressionJoinTypeNode } from "./expressionJoinTypeNode"; + +export type ExpressionNode = { + readonly kind: "ExpressionNode"; + readonly expressions: ExpressionJoinTypeNode[]; +}; diff --git a/src/nodes/expressionNotExpression.ts b/src/nodes/expressionNotExpression.ts new file mode 100644 index 0000000..8325947 --- /dev/null +++ b/src/nodes/expressionNotExpression.ts @@ -0,0 +1,6 @@ +import { ExpressionNode } from "./expressionNode"; + +export type ExpressionNotExpression = { + readonly kind: "ExpressionNotExpression"; + readonly expr: ExpressionNode; +}; diff --git a/src/nodes/filterExpressionComparatorExpression.ts b/src/nodes/filterExpressionComparatorExpression.ts deleted file mode 100644 index 76f0099..0000000 --- a/src/nodes/filterExpressionComparatorExpression.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FilterConditionComparators } from "./operands"; - -export type FilterExpressionComparatorExpressions = { - readonly kind: "FilterExpressionComparatorExpressions"; - readonly key: string; - readonly operation: FilterConditionComparators; - readonly value: unknown; -}; diff --git a/src/nodes/filterExpressionNode.ts b/src/nodes/filterExpressionNode.ts deleted file mode 100644 index 8213903..0000000 --- a/src/nodes/filterExpressionNode.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FilterExpressionJoinTypeNode } from "./filterExpressionJoinTypeNode"; - -export type FilterExpressionNode = { - readonly kind: "FilterExpressionNode"; - readonly expressions: FilterExpressionJoinTypeNode[]; -}; diff --git a/src/nodes/filterExpressionNotExpression.ts b/src/nodes/filterExpressionNotExpression.ts deleted file mode 100644 index fd84716..0000000 --- a/src/nodes/filterExpressionNotExpression.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FilterExpressionNode } from "./filterExpressionNode"; - -export type FilterExpressionNotExpression = { - readonly kind: "FilterExpressionNotExpression"; - readonly expr: FilterExpressionNode; -}; diff --git a/src/nodes/itemNode.ts b/src/nodes/itemNode.ts new file mode 100644 index 0000000..6b27a92 --- /dev/null +++ b/src/nodes/itemNode.ts @@ -0,0 +1,4 @@ +export type ItemNode = { + readonly kind: "ItemNode"; + readonly item: Record; +}; diff --git a/src/nodes/operands.ts b/src/nodes/operands.ts index e8aec33..922a287 100644 --- a/src/nodes/operands.ts +++ b/src/nodes/operands.ts @@ -10,4 +10,4 @@ export type FunctionExpression = export type NotExpression = "NOT"; export type KeyConditionComparators = "=" | "<" | "<=" | ">" | ">="; -export type FilterConditionComparators = KeyConditionComparators | "<>"; +export type ExpressionConditionComparators = KeyConditionComparators | "<>"; diff --git a/src/nodes/putNode.ts b/src/nodes/putNode.ts new file mode 100644 index 0000000..c158089 --- /dev/null +++ b/src/nodes/putNode.ts @@ -0,0 +1,12 @@ +import { ExpressionNode } from "./expressionNode"; +import { ItemNode } from "./itemNode"; +import { ReturnValuesNode } from "./returnValuesNode"; +import { TableNode } from "./tableNode"; + +export type PutNode = { + readonly kind: "PutNode"; + readonly table: TableNode; + readonly conditionExpression: ExpressionNode; + readonly item?: ItemNode; + readonly returnValues?: ReturnValuesNode; +}; diff --git a/src/nodes/queryNode.ts b/src/nodes/queryNode.ts index ece9837..959e7f5 100644 --- a/src/nodes/queryNode.ts +++ b/src/nodes/queryNode.ts @@ -1,6 +1,6 @@ import { AttributesNode } from "./attributesNode"; import { ConsistentReadNode } from "./consistentReadNode"; -import { FilterExpressionNode } from "./filterExpressionNode"; +import { ExpressionNode } from "./expressionNode"; import { KeyConditionNode } from "./keyConditionNode"; import { LimitNode } from "./limitNode"; import { ScanIndexForwardNode } from "./scanIndexForwardNode"; @@ -10,7 +10,7 @@ export type QueryNode = { readonly kind: "QueryNode"; readonly table: TableNode; readonly keyConditions: KeyConditionNode[]; - readonly filterExpression: FilterExpressionNode; + readonly filterExpression: ExpressionNode; readonly consistentRead?: ConsistentReadNode; readonly scanIndexForward?: ScanIndexForwardNode; readonly limit?: LimitNode; diff --git a/src/nodes/returnValuesNode.ts b/src/nodes/returnValuesNode.ts new file mode 100644 index 0000000..d413ccd --- /dev/null +++ b/src/nodes/returnValuesNode.ts @@ -0,0 +1,11 @@ +export type ReturnValuesOptions = + | "NONE" + | "ALL_OLD" + | "UPDATED_OLD" + | "ALL_NEW" + | "UPDATED_NEW"; + +export type ReturnValuesNode = { + readonly kind: "ReturnValuesNode"; + readonly option: ReturnValuesOptions; +}; diff --git a/src/queryBuilders/__snapshots__/putItemQueryBuilder.integration.test.ts.snap b/src/queryBuilders/__snapshots__/putItemQueryBuilder.integration.test.ts.snap new file mode 100644 index 0000000..2925154 --- /dev/null +++ b/src/queryBuilders/__snapshots__/putItemQueryBuilder.integration.test.ts.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PutItemQueryBuilder > handles 'contains' ConditionExpression 1`] = ` +{ + "dataTimestamp": 212, + "tags": [ + "cats", + ], + "userId": "333", +} +`; diff --git a/src/queryBuilders/expressionBuilder.ts b/src/queryBuilders/expressionBuilder.ts new file mode 100644 index 0000000..2d7d963 --- /dev/null +++ b/src/queryBuilders/expressionBuilder.ts @@ -0,0 +1,404 @@ +import { AttributeExistsFunctionExpression } from "../nodes/attributeExistsFunctionExpression"; +import { AttributeNotExistsFunctionExpression } from "../nodes/attributeNotExistsFunctionExpression"; +import { + ExpressionJoinTypeNode, + JoinType, +} from "../nodes/expressionJoinTypeNode"; +import { ExpressionNode } from "../nodes/expressionNode"; +import { + BetweenExpression, + ExpressionConditionComparators, + FunctionExpression, + NotExpression, +} from "../nodes/operands"; +import { + GetFromPath, + ObjectKeyPaths, + PickNonKeys, + StripKeys, +} from "../typeHelpers"; + +export interface ExpressionBuilderInterface< + DDB, + Table extends keyof DDB, + O, + AllowKeys = false +> { + // expression + expression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: ComparatorExprArg + ): ExpressionBuilderInterface; + + expression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeFuncExprArg + ): ExpressionBuilderInterface; + + expression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeBeginsWithExprArg + ): ExpressionBuilderInterface; + + expression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeContainsExprArg + ): ExpressionBuilderInterface; + + expression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeBetweenExprArg + ): ExpressionBuilderInterface; + + expression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: NotExprArg + ): ExpressionBuilderInterface; + + expression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: BuilderExprArg + ): ExpressionBuilderInterface; + + // orExpression + orExpression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: ComparatorExprArg + ): ExpressionBuilderInterface; + + orExpression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeFuncExprArg + ): ExpressionBuilderInterface; + + orExpression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeBeginsWithExprArg + ): ExpressionBuilderInterface; + + orExpression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeContainsExprArg + ): ExpressionBuilderInterface; + + orExpression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: AttributeBetweenExprArg + ): ExpressionBuilderInterface; + + orExpression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: NotExprArg + ): ExpressionBuilderInterface; + + orExpression< + Key extends ObjectKeyPaths< + AllowKeys extends true ? DDB[Table] : PickNonKeys + > + >( + ...args: BuilderExprArg + ): ExpressionBuilderInterface; + + _getNode(): ExpressionNode; +} + +export type ComparatorExprArg = [ + key: Key, + operation: ExpressionConditionComparators, + value: StripKeys> +]; + +export type AttributeFuncExprArg = [ + key: Key, + func: Extract +]; + +export type AttributeBeginsWithExprArg = [ + key: Key, + func: Extract, + substr: string +]; + +export type AttributeContainsExprArg = [ + key: Key, + expr: Extract, + value: GetFromPath extends unknown[] + ? StripKeys>[number] + : never +]; + +export type AttributeBetweenExprArg = [ + key: Key, + expr: BetweenExpression, + left: StripKeys>, + right: StripKeys> +]; + +export type NotExprArg< + DDB, + Table extends keyof DDB, + O, + AllowKeysInExpression = true +> = [ + not: NotExpression, + builder: ( + qb: ExpressionBuilderInterface + ) => ExpressionBuilderInterface +]; + +export type BuilderExprArg< + DDB, + Table extends keyof DDB, + O, + AllowKeysInExpression = true +> = [ + builder: ( + qb: ExpressionBuilderInterface + ) => ExpressionBuilderInterface +]; + +export type ExprArgs< + DDB, + Table extends keyof DDB, + O, + Key, + AllowKeysInExpression = true +> = + | ComparatorExprArg + | AttributeFuncExprArg + | AttributeBeginsWithExprArg + | AttributeContainsExprArg + | AttributeBetweenExprArg + | BuilderExprArg + | NotExprArg; +export class ExpressionBuilder< + DDB, + Table extends keyof DDB, + O extends DDB[Table] +> implements ExpressionBuilderInterface +{ + readonly #props: ExpressionBuilderProps; + + constructor(props: ExpressionBuilderProps) { + this.#props = props; + } + + _expression>( + joinType: JoinType, + ...args: ExprArgs + ): ExpressionBuilderInterface { + if (args[1] === "begins_with") { + const [key, f, substr] = args; + + return new ExpressionBuilder({ + ...this.#props, + node: { + ...this.#props.node, + expressions: this.#props.node.expressions.concat({ + kind: "ExpressionJoinTypeNode", + expr: { + kind: "BeginsWithFunctionExpression", + key, + substr, + }, + joinType, + }), + }, + }); + } else if (args[1] === "contains") { + const [key, expr, value] = args; + + return new ExpressionBuilder({ + ...this.#props, + node: { + ...this.#props.node, + expressions: this.#props.node.expressions.concat({ + kind: "ExpressionJoinTypeNode", + expr: { + kind: "ContainsFunctionExpression", + key, + value, + }, + joinType, + }), + }, + }); + } else if ( + args[1] === "attribute_exists" || + args[1] === "attribute_not_exists" + ) { + const [key, func] = args; + let resultExpr: + | AttributeExistsFunctionExpression + | AttributeNotExistsFunctionExpression; + + if (func === "attribute_exists") { + resultExpr = { kind: "AttributeExistsFunctionExpression", key }; + } else { + resultExpr = { kind: "AttributeNotExistsFunctionExpression", key }; + } + + return new ExpressionBuilder({ + ...this.#props, + node: { + ...this.#props.node, + expressions: this.#props.node.expressions.concat({ + kind: "ExpressionJoinTypeNode", + expr: resultExpr, + joinType, + }), + }, + }); + } else if (args[1] === "BETWEEN") { + const [key, expr, left, right] = args; + + return new ExpressionBuilder({ + ...this.#props, + node: { + ...this.#props.node, + expressions: this.#props.node.expressions.concat({ + kind: "ExpressionJoinTypeNode", + expr: { + kind: "BetweenConditionExpression", + key, + left, + right, + }, + joinType, + }), + }, + }); + } else if ( + typeof args[0] !== "function" && + args[0] !== "NOT" && + typeof args[1] !== "function" && + args[1] !== undefined && + args[2] !== undefined + ) { + const [key, operation, value] = args; + + return new ExpressionBuilder({ + ...this.#props, + node: { + ...this.#props.node, + expressions: this.#props.node.expressions.concat({ + kind: "ExpressionJoinTypeNode", + joinType, + expr: { + kind: "ExpressionComparatorExpressions", + key, + operation, + value, + }, + }), + }, + }); + } else if (typeof args[0] === "function" || typeof args[1] === "function") { + let builder; + + if (typeof args[0] === "function") { + builder = args[0]; + } else if (typeof args[1] === "function") { + builder = args[1]; + } + + if (!builder) throw new Error("Could not find builder"); + + const qb = new ExpressionBuilder({ + ...this.#props, + node: { + expressions: [], + kind: "ExpressionNode", + }, + }); + + const result = builder(qb); + const expressionNode = result._getNode(); + + let resultNode: ExpressionJoinTypeNode = { + kind: "ExpressionJoinTypeNode", + expr: expressionNode, + joinType, + }; + + if (args[0] === "NOT") { + resultNode = { + ...resultNode, + expr: { + kind: "ExpressionNotExpression", + expr: expressionNode, + }, + }; + } + + return new ExpressionBuilder({ + ...this.#props, + node: { + ...this.#props.node, + expressions: this.#props.node.expressions.concat(resultNode), + }, + }); + } + + throw new Error("Invalid arguments given to expression builder"); + } + + expression>( + ...args: ExprArgs + ): ExpressionBuilderInterface { + return this._expression("AND", ...args); + } + + orExpression>( + ...args: ExprArgs + ): ExpressionBuilderInterface { + return this._expression("OR", ...args); + } + + _getNode() { + return this.#props.node; + } +} + +interface ExpressionBuilderProps { + readonly node: ExpressionNode; +} diff --git a/src/queryBuilders/putItemQueryBuilder.integration.test.ts b/src/queryBuilders/putItemQueryBuilder.integration.test.ts new file mode 100644 index 0000000..43a4d84 --- /dev/null +++ b/src/queryBuilders/putItemQueryBuilder.integration.test.ts @@ -0,0 +1,146 @@ +import { DDB } from "../../test/testFixture"; +import { getDDBClientFor, startDDBTestContainer } from "../../test/testUtil"; +import { Tsynamo } from "./../index"; + +describe("PutItemQueryBuilder", () => { + let tsynamoClient: Tsynamo; + + const itemToPut = { + userId: "333", + dataTimestamp: 222, + someBoolean: true, + }; + + beforeAll(async () => { + const testContainer = await startDDBTestContainer(); + + tsynamoClient = new Tsynamo({ + ddbClient: await getDDBClientFor(testContainer), + }); + }); + + it("handles a simple put query", async () => { + let result = await tsynamoClient + .getItemFrom("myTable") + .keys({ + userId: itemToPut.userId, + dataTimestamp: itemToPut.dataTimestamp, + }) + .execute(); + + expect(result).toBeUndefined(); + + await tsynamoClient.putItem("myTable").item(itemToPut).execute(); + + result = await tsynamoClient + .getItemFrom("myTable") + .keys({ + userId: "333", + dataTimestamp: 222, + }) + .execute(); + + expect(result).toBeDefined(); + expect(result).toEqual(itemToPut); + }); + + it("handles ReturnValues option", async () => { + let result = await tsynamoClient + .putItem("myTable") + .item(itemToPut) + .returnValues("ALL_OLD") + .execute(); + + expect(result).toBeUndefined(); + + result = await tsynamoClient + .putItem("myTable") + .item({ + ...itemToPut, + tags: ["kissa"], + }) + .returnValues("ALL_OLD") + .execute(); + + expect(result).toEqual(itemToPut); + }); + + it("handles an 'attribute_not_exists' ConditionExpression", async () => { + await tsynamoClient.putItem("myTable").item(itemToPut).execute(); + expect( + tsynamoClient + .putItem("myTable") + .item(itemToPut) + .conditionExpression("userId", "attribute_not_exists") + .execute() + ).rejects.toMatchInlineSnapshot( + `[ConditionalCheckFailedException: The conditional request failed]` + ); + }); + + it("handles 'contains' ConditionExpression", async () => { + await tsynamoClient + .putItem("myTable") + .item({ + userId: "333", + dataTimestamp: 212, + tags: ["cats"], + }) + .execute(); + + const oldValues = await tsynamoClient + .putItem("myTable") + .item({ + userId: "333", + dataTimestamp: 212, + tags: ["cats"], + }) + .conditionExpression("tags", "contains", "cats") + .returnValues("ALL_OLD") + .execute(); + + expect(oldValues).toBeTruthy(); + expect(oldValues).toMatchSnapshot(); + + expect( + tsynamoClient + .putItem("myTable") + .item({ + userId: "333", + dataTimestamp: 212, + }) + .conditionExpression("NOT", (qb) => + qb.expression("tags", "contains", "cats") + ) + .execute() + ).rejects.toMatchInlineSnapshot( + `[ConditionalCheckFailedException: The conditional request failed]` + ); + }); + + it("Handles nested ConditionExpressions", async () => { + await tsynamoClient + .putItem("myTable") + .item({ + userId: "333", + dataTimestamp: 212, + nested: { + nestedBoolean: false, + }, + }) + .execute(); + + expect( + tsynamoClient + .putItem("myTable") + .item({ + userId: "333", + dataTimestamp: 212, + }) + .conditionExpression("nested.nestedBoolean", "=", true) + .execute() + ).rejects.toMatchInlineSnapshot( + `[ConditionalCheckFailedException: The conditional request failed]` + ); + }); +}); diff --git a/src/queryBuilders/putItemQueryBuilder.ts b/src/queryBuilders/putItemQueryBuilder.ts new file mode 100644 index 0000000..ce4cbd0 --- /dev/null +++ b/src/queryBuilders/putItemQueryBuilder.ts @@ -0,0 +1,186 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { PutNode } from "../nodes/putNode"; +import { QueryCompiler } from "../queryCompiler"; +import { ExecuteOutput, ObjectKeyPaths, PickNonKeys } from "../typeHelpers"; +import { preventAwait } from "../util/preventAwait"; +import { ReturnValuesOptions } from "../nodes/returnValuesNode"; +import { + AttributeBeginsWithExprArg, + AttributeBetweenExprArg, + AttributeContainsExprArg, + AttributeFuncExprArg, + BuilderExprArg, + ComparatorExprArg, + ExprArgs, + ExpressionBuilder, + NotExprArg, +} from "./expressionBuilder"; + +export interface PutItemQueryBuilderInterface { + // conditionExpression + conditionExpression>( + ...args: ComparatorExprArg + ): PutItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeFuncExprArg + ): PutItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeBeginsWithExprArg + ): PutItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeContainsExprArg + ): PutItemQueryBuilderInterface; + + conditionExpression>( + ...args: AttributeBetweenExprArg + ): PutItemQueryBuilderInterface; + + conditionExpression>( + ...args: NotExprArg + ): PutItemQueryBuilderInterface; + + conditionExpression>( + ...args: BuilderExprArg + ): PutItemQueryBuilderInterface; + + // orConditionExpression + orConditionExpression>( + ...args: ComparatorExprArg + ): PutItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeFuncExprArg + ): PutItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeBeginsWithExprArg + ): PutItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeContainsExprArg + ): PutItemQueryBuilderInterface; + + orConditionExpression>( + ...args: AttributeBetweenExprArg + ): PutItemQueryBuilderInterface; + + orConditionExpression>( + ...args: NotExprArg + ): PutItemQueryBuilderInterface; + + orConditionExpression>( + ...args: BuilderExprArg + ): PutItemQueryBuilderInterface; + + returnValues( + option: Extract + ): PutItemQueryBuilderInterface; + + item>( + item: Item + ): PutItemQueryBuilderInterface; + + execute(): Promise[] | undefined>; +} + +/** + * @todo support ConditionExpression + */ +export class PutItemQueryBuilder< + DDB, + Table extends keyof DDB, + O extends DDB[Table] +> implements PutItemQueryBuilderInterface +{ + readonly #props: PutItemQueryBuilderProps; + + constructor(props: PutItemQueryBuilderProps) { + this.#props = props; + } + + conditionExpression>( + ...args: ExprArgs + ): PutItemQueryBuilderInterface { + const eB = new ExpressionBuilder({ + node: { ...this.#props.node.conditionExpression }, + }); + + const expressionNode = eB.expression(...args)._getNode(); + + return new PutItemQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + conditionExpression: expressionNode, + }, + }); + } + + orConditionExpression>( + ...args: ExprArgs + ): PutItemQueryBuilderInterface { + const eB = new ExpressionBuilder({ + node: { ...this.#props.node.conditionExpression }, + }); + + const expressionNode = eB.orExpression(...args)._getNode(); + + return new PutItemQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + conditionExpression: expressionNode, + }, + }); + } + + item>( + item: Item + ): PutItemQueryBuilderInterface { + return new PutItemQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + item: { + kind: "ItemNode", + item, + }, + }, + }); + } + + returnValues( + option: ReturnValuesOptions + ): PutItemQueryBuilderInterface { + return new PutItemQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + returnValues: { + kind: "ReturnValuesNode", + option, + }, + }, + }); + } + + execute = async (): Promise[] | undefined> => { + const putCommand = this.#props.queryCompiler.compile(this.#props.node); + const data = await this.#props.ddbClient.send(putCommand); + return data.Attributes as any; + }; +} + +preventAwait( + PutItemQueryBuilder, + "Don't await PutQueryBuilder instances directly. To execute the query you need to call the `execute` method" +); + +interface PutItemQueryBuilderProps { + readonly node: PutNode; + readonly ddbClient: DynamoDBDocumentClient; + readonly queryCompiler: QueryCompiler; +} diff --git a/src/queryBuilders/queryQueryBuilder.integration.test.ts b/src/queryBuilders/queryQueryBuilder.integration.test.ts index 212b128..df35423 100644 --- a/src/queryBuilders/queryQueryBuilder.integration.test.ts +++ b/src/queryBuilders/queryQueryBuilder.integration.test.ts @@ -48,6 +48,7 @@ describe("QueryQueryBuilder", () => { .query("myTable") .keyCondition("userId", "=", TEST_DATA[0].userId) .filterExpression("someBoolean", "=", true) + .filterExpression("someBoolean", "=", true) .execute(); expect(data?.length).toBe(2); @@ -74,8 +75,8 @@ describe("QueryQueryBuilder", () => { .filterExpression("somethingElse", "<", 2) .orFilterExpression((qb) => qb - .filterExpression("someBoolean", "=", true) - .filterExpression("somethingElse", "=", 2) + .expression("someBoolean", "=", true) + .expression("somethingElse", "=", 2) ) .execute(); @@ -86,9 +87,7 @@ describe("QueryQueryBuilder", () => { let data = await tsynamoClient .query("myTable") .keyCondition("userId", "=", "123") - .filterExpression("NOT", (qb) => - qb.filterExpression("someBoolean", "=", true) - ) + .filterExpression("NOT", (qb) => qb.expression("someBoolean", "=", true)) .execute(); expect(data).toMatchSnapshot(); @@ -97,9 +96,7 @@ describe("QueryQueryBuilder", () => { .query("myTable") .keyCondition("userId", "=", "123") .filterExpression("someBoolean", "=", true) - .orFilterExpression("NOT", (qb) => - qb.filterExpression("somethingElse", "=", 0) - ) + .orFilterExpression("NOT", (qb) => qb.expression("somethingElse", "=", 0)) .execute(); expect(data).toMatchSnapshot(); diff --git a/src/queryBuilders/queryQueryBuilder.ts b/src/queryBuilders/queryQueryBuilder.ts index 186cee9..dc29c5d 100644 --- a/src/queryBuilders/queryQueryBuilder.ts +++ b/src/queryBuilders/queryQueryBuilder.ts @@ -1,22 +1,13 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; -import { AttributeExistsFunctionExpression } from "../nodes/attributeExistsFunctionExpression"; -import { AttributeNotExistsFunctionExpression } from "../nodes/attributeNotExistsFunctionExpression"; -import { - FilterExpressionJoinTypeNode, - JoinType, -} from "../nodes/filterExpressionJoinTypeNode"; import { BetweenExpression, - FilterConditionComparators, FunctionExpression, KeyConditionComparators, - NotExpression, } from "../nodes/operands"; import { QueryNode } from "../nodes/queryNode"; import { QueryCompiler } from "../queryCompiler"; import { ExecuteOutput, - GetFromPath, ObjectFullPaths, ObjectKeyPaths, PickAllKeys, @@ -26,154 +17,99 @@ import { StripKeys, } from "../typeHelpers"; import { preventAwait } from "../util/preventAwait"; +import { + AttributeBeginsWithExprArg, + AttributeBetweenExprArg, + AttributeContainsExprArg, + AttributeFuncExprArg, + BuilderExprArg, + ComparatorExprArg, + ExprArgs, + ExpressionBuilder, + NotExprArg, +} from "./expressionBuilder"; export interface QueryQueryBuilderInterface { - execute(): Promise[] | undefined>; - - /** - * keyCondition methods - */ - keyCondition & string>( - key: Key, - expr: Key extends keyof PickSk - ? Extract - : never, - substr: string + // filterExpression + filterExpression>>( + ...args: ComparatorExprArg ): QueryQueryBuilderInterface; - keyCondition & string>( - key: Key, - expr: BetweenExpression, - left: StripKeys, - right: StripKeys + filterExpression>>( + ...args: AttributeFuncExprArg ): QueryQueryBuilderInterface; - keyCondition & string>( - key: Key, - operation: KeyConditionComparators, - val: StripKeys + filterExpression>>( + ...args: AttributeBeginsWithExprArg ): QueryQueryBuilderInterface; - /** - * filterExpression methods - * - * @todo Currently NOT FilterExpression returns operations as suggestions as well. - */ - - // Regular operand filterExpression>>( - key: Exclude, - operation: Key extends NotExpression ? never : FilterConditionComparators, - val: StripKeys> + ...args: AttributeContainsExprArg ): QueryQueryBuilderInterface; - // function expression for functions that only take path as param filterExpression>>( - key: Exclude, - func: Extract< - FunctionExpression, - "attribute_exists" | "attribute_not_exists" - > + ...args: AttributeBetweenExprArg ): QueryQueryBuilderInterface; - // BEGINS_WITH function expression filterExpression>>( - key: Key, - expr: Extract, - substr: string + ...args: NotExprArg ): QueryQueryBuilderInterface; - // CONTAINS function expression - filterExpression< - Key extends ObjectKeyPaths>, - Property extends GetFromPath & unknown[] - >( - key: Key, - expr: Extract, - value: StripKeys[number] + filterExpression>>( + ...args: BuilderExprArg ): QueryQueryBuilderInterface; - // BETWEEN expression - filterExpression>>( - key: Key, - expr: BetweenExpression, - left: StripKeys>, - right: StripKeys> + // orFilterExpression + orFilterExpression>>( + ...args: ComparatorExprArg ): QueryQueryBuilderInterface; - // NOT expression - filterExpression( - not: NotExpression, - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations + orFilterExpression>>( + ...args: AttributeFuncExprArg ): QueryQueryBuilderInterface; - // Nested expression - filterExpression( - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations + orFilterExpression>>( + ...args: AttributeBeginsWithExprArg ): QueryQueryBuilderInterface; - /** - * orFilterExpression methods - */ + orFilterExpression>>( + ...args: AttributeContainsExprArg + ): QueryQueryBuilderInterface; - // Regular operand orFilterExpression>>( - key: Key, - operation: FilterConditionComparators, - val: StripKeys> + ...args: AttributeBetweenExprArg ): QueryQueryBuilderInterface; - // function expression for functions that only take path as param orFilterExpression>>( - key: Exclude, - func: Extract< - FunctionExpression, - "attribute_exists" | "attribute_not_exists" - > + ...args: NotExprArg ): QueryQueryBuilderInterface; - // begins_with function expression orFilterExpression>>( - key: Key, - expr: Extract, - substr: string + ...args: BuilderExprArg ): QueryQueryBuilderInterface; - // CONTAINS function expression - orFilterExpression< - Key extends ObjectKeyPaths>, - Property extends GetFromPath & unknown[] - >( + /** + * keyCondition methods + */ + keyCondition & string>( key: Key, - expr: Extract, - value: StripKeys[number] + expr: Key extends keyof PickSk + ? Extract + : never, + substr: string ): QueryQueryBuilderInterface; - // BETWEEN expression - orFilterExpression>>( + keyCondition & string>( key: Key, expr: BetweenExpression, - left: StripKeys>, - right: StripKeys> - ): QueryQueryBuilderInterface; - - // NOT expression - orFilterExpression( - not: NotExpression, - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations + left: StripKeys, + right: StripKeys ): QueryQueryBuilderInterface; - // Nested expression - orFilterExpression( - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations + keyCondition & string>( + key: Key, + operation: KeyConditionComparators, + val: StripKeys ): QueryQueryBuilderInterface; limit(value: number): QueryQueryBuilderInterface; @@ -186,167 +122,9 @@ export interface QueryQueryBuilderInterface { attributes: A ): QueryQueryBuilderInterface>; - _getNode(): QueryNode; -} - -/** - * When we use a nested builder, this type is used to remove - * all the extra functions of the builder for DX improvement. - */ -export interface QueryQueryBuilderInterfaceWithOnlyFilterOperations< - DDB, - Table extends keyof DDB, - O -> { - /** - * filterExpression methods - */ - filterExpression>>( - key: Key, - operation: FilterConditionComparators, - val: StripKeys> - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - filterExpression>>( - key: Exclude, - func: Extract< - FunctionExpression, - "attribute_exists" | "attribute_not_exists" - > - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - filterExpression>>( - key: Key, - func: Extract, - substr: string - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - filterExpression< - Key extends ObjectKeyPaths>, - Property extends GetFromPath & unknown[] - >( - key: Key, - expr: Extract, - value: StripKeys[number] - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - filterExpression>>( - key: Key, - expr: BetweenExpression, - left: StripKeys>, - right: StripKeys> - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - filterExpression( - not: NotExpression, - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - filterExpression( - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - /** - * orFilterExpression methods - */ - orFilterExpression>>( - key: Key, - operation: FilterConditionComparators, - val: StripKeys> - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - orFilterExpression>>( - key: Exclude, - func: Extract< - FunctionExpression, - "attribute_exists" | "attribute_not_exists" - > - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - orFilterExpression>>( - key: Key, - func: Extract, - substr: string - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - orFilterExpression< - Key extends ObjectKeyPaths>, - Property extends GetFromPath & unknown[] - >( - key: Key, - expr: Extract, - value: StripKeys[number] - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - orFilterExpression>>( - key: Key, - expr: BetweenExpression, - left: StripKeys>, - right: StripKeys> - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - orFilterExpression( - not: NotExpression, - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - orFilterExpression( - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations - ): QueryQueryBuilderInterfaceWithOnlyFilterOperations; - - _getNode(): QueryNode; + execute(): Promise[] | undefined>; } -type FilterExprArgs< - DDB, - Table extends keyof DDB, - O, - Key extends ObjectKeyPaths> -> = - | [ - key: Key, - operation: FilterConditionComparators, - value: StripKeys> - ] - | [ - key: Exclude, - func: Extract< - FunctionExpression, - "attribute_exists" | "attribute_not_exists" - > - ] - | [key: Key, func: Extract, substr: string] - | [ - key: Key, - expr: Extract, - value: StripKeys> - ] - | [ - key: Key, - expr: BetweenExpression, - left: StripKeys>, - right: StripKeys> - ] - | [ - not: NotExpression, - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations - ] - | [ - builder: ( - qb: QueryQueryBuilderInterfaceWithOnlyFilterOperations - ) => QueryQueryBuilderInterfaceWithOnlyFilterOperations - ]; - /** * @todo support IndexName * @todo support ExclusiveStartKey @@ -363,7 +141,6 @@ export class QueryQueryBuilder< this.#props = props; } - // TODO: Add support for all operations from here: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.KeyConditionExpressions.html keyCondition & string>( ...args: | [ @@ -438,200 +215,40 @@ export class QueryQueryBuilder< } } - _filterExpression>>( - joinType: JoinType, - ...args: FilterExprArgs + filterExpression>>( + ...args: ExprArgs ): QueryQueryBuilderInterface { - if (args[1] === "begins_with") { - const [key, f, substr] = args; - - return new QueryQueryBuilder({ - ...this.#props, - node: { - ...this.#props.node, - filterExpression: { - ...this.#props.node.filterExpression, - expressions: this.#props.node.filterExpression.expressions.concat({ - kind: "FilterExpressionJoinTypeNode", - expr: { - kind: "BeginsWithFunctionExpression", - key, - substr, - }, - joinType, - }), - }, - }, - }); - } else if (args[1] === "contains") { - const [key, expr, value] = args; - - return new QueryQueryBuilder({ - ...this.#props, - node: { - ...this.#props.node, - filterExpression: { - ...this.#props.node.filterExpression, - expressions: this.#props.node.filterExpression.expressions.concat({ - kind: "FilterExpressionJoinTypeNode", - expr: { - kind: "ContainsFunctionExpression", - key, - value, - }, - joinType, - }), - }, - }, - }); - } else if ( - args[1] === "attribute_exists" || - args[1] === "attribute_not_exists" - ) { - const [key, func] = args; - let resultExpr: - | AttributeExistsFunctionExpression - | AttributeNotExistsFunctionExpression; - - if (func === "attribute_exists") { - resultExpr = { kind: "AttributeExistsFunctionExpression", key }; - } else { - resultExpr = { kind: "AttributeNotExistsFunctionExpression", key }; - } - - return new QueryQueryBuilder({ - ...this.#props, - node: { - ...this.#props.node, - filterExpression: { - ...this.#props.node.filterExpression, - expressions: this.#props.node.filterExpression.expressions.concat({ - kind: "FilterExpressionJoinTypeNode", - expr: resultExpr, - joinType, - }), - }, - }, - }); - } else if (args[1] === "BETWEEN") { - const [key, expr, left, right] = args; - - return new QueryQueryBuilder({ - ...this.#props, - node: { - ...this.#props.node, - filterExpression: { - ...this.#props.node.filterExpression, - expressions: this.#props.node.filterExpression.expressions.concat({ - kind: "FilterExpressionJoinTypeNode", - expr: { - kind: "BetweenConditionExpression", - key, - left, - right, - }, - joinType, - }), - }, - }, - }); - } else if ( - typeof args[0] !== "function" && - args[0] !== "NOT" && - typeof args[1] !== "function" && - args[1] !== undefined && - args[2] !== undefined - ) { - const [key, operation, value] = args; - - return new QueryQueryBuilder({ - ...this.#props, - node: { - ...this.#props.node, - filterExpression: { - ...this.#props.node.filterExpression, - expressions: this.#props.node.filterExpression.expressions.concat({ - kind: "FilterExpressionJoinTypeNode", - joinType, - expr: { - kind: "FilterExpressionComparatorExpressions", - key, - operation, - value, - }, - }), - }, - }, - }); - } else if (typeof args[0] === "function" || typeof args[1] === "function") { - let builder; - - if (typeof args[0] === "function") { - builder = args[0]; - } else if (typeof args[1] === "function") { - builder = args[1]; - } - - if (!builder) throw new Error("Could not find builder"); - - const qb = new QueryQueryBuilder({ - ...this.#props, - node: { - ...this.#props.node, - filterExpression: { - expressions: [], - kind: "FilterExpressionNode", - }, - }, - }); - - const result = builder(qb); - - const { filterExpression } = result._getNode(); - - let resultNode: FilterExpressionJoinTypeNode = { - kind: "FilterExpressionJoinTypeNode", - expr: filterExpression, - joinType, - }; - - if (args[0] === "NOT") { - resultNode = { - ...resultNode, - expr: { - kind: "FilterExpressionNotExpression", - expr: filterExpression, - }, - }; - } - - return new QueryQueryBuilder({ - ...this.#props, - node: { - ...this.#props.node, - filterExpression: { - ...this.#props.node.filterExpression, - expressions: - this.#props.node.filterExpression.expressions.concat(resultNode), - }, - }, - }); - } + const eB = new ExpressionBuilder({ + node: { ...this.#props.node.filterExpression }, + }); - throw new Error("Invalid arguments given to filterExpression"); - } + const expressionNode = eB.expression(...args)._getNode(); - // TODO: Add support for all operations from here: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Syntax - filterExpression>>( - ...args: FilterExprArgs - ): QueryQueryBuilderInterface { - return this._filterExpression("AND", ...args); + return new QueryQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + filterExpression: expressionNode, + }, + }); } orFilterExpression>>( - ...args: FilterExprArgs + ...args: ExprArgs ): QueryQueryBuilderInterface { - return this._filterExpression("OR", ...args); + const eB = new ExpressionBuilder({ + node: { ...this.#props.node.filterExpression }, + }); + + const expressionNode = eB.orExpression(...args)._getNode(); + + return new QueryQueryBuilder({ + ...this.#props, + node: { + ...this.#props.node, + filterExpression: expressionNode, + }, + }); } limit(value: number): QueryQueryBuilderInterface { @@ -647,10 +264,6 @@ export class QueryQueryBuilder< }); } - _getNode() { - return this.#props.node; - } - scanIndexForward( enabled: boolean ): QueryQueryBuilderInterface { diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts index d80c671..7df9777 100644 --- a/src/queryCompiler/queryCompiler.ts +++ b/src/queryCompiler/queryCompiler.ts @@ -1,6 +1,6 @@ -import { GetCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"; -import { FilterExpressionJoinTypeNode } from "../nodes/filterExpressionJoinTypeNode"; -import { FilterExpressionNode } from "../nodes/filterExpressionNode"; +import { GetCommand, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"; +import { ExpressionJoinTypeNode } from "../nodes/expressionJoinTypeNode"; +import { ExpressionNode } from "../nodes/expressionNode"; import { GetNode } from "../nodes/getNode"; import { KeyConditionNode } from "../nodes/keyConditionNode"; import { QueryNode } from "../nodes/queryNode"; @@ -10,16 +10,20 @@ import { mergeObjectIntoMap, } from "./compilerUtil"; import { AttributesNode } from "../nodes/attributesNode"; +import { PutNode } from "../nodes/putNode"; export class QueryCompiler { compile(rootNode: QueryNode): QueryCommand; compile(rootNode: GetNode): GetCommand; - compile(rootNode: QueryNode | GetNode) { + compile(rootNode: PutNode): PutCommand; + compile(rootNode: QueryNode | GetNode | PutNode) { switch (rootNode.kind) { case "GetNode": return this.compileGetNode(rootNode); case "QueryNode": return this.compileQueryNode(rootNode); + case "PutNode": + return this.compilePutNode(rootNode); } } @@ -64,7 +68,7 @@ export class QueryCompiler { attributeNames ); - const compiledFilterExpression = this.compileFilterExpression( + const compiledFilterExpression = this.compileExpression( filterExpressionNode, filterExpressionAttributeValues, attributeNames @@ -97,6 +101,45 @@ export class QueryCompiler { }); } + compilePutNode(putNode: PutNode) { + const { + table: tableNode, + item: itemNode, + returnValues: returnValuesNode, + conditionExpression: conditionExpressionNode, + } = putNode; + + const attributeNames = new Map(); + const filterExpressionAttributeValues = new Map(); + + const compiledConditionExpression = this.compileExpression( + conditionExpressionNode, + filterExpressionAttributeValues, + attributeNames + ); + + return new PutCommand({ + TableName: tableNode.table, + Item: itemNode?.item, + ReturnValues: returnValuesNode?.option, + ConditionExpression: compiledConditionExpression + ? compiledConditionExpression + : undefined, + ExpressionAttributeValues: + filterExpressionAttributeValues.size > 0 + ? { + ...Object.fromEntries(filterExpressionAttributeValues), + } + : undefined, + ExpressionAttributeNames: + attributeNames.size > 0 + ? { + ...Object.fromEntries(attributeNames), + } + : undefined, + }); + } + compileAttributeNamesNode(node?: AttributesNode) { const ProjectionExpression = node?.attributes .map((att) => getExpressionAttributeNameFrom(att)) @@ -133,8 +176,8 @@ export class QueryCompiler { }; } - compileFilterExpression = ( - expression: FilterExpressionNode, + compileExpression = ( + expression: ExpressionNode, filterExpressionAttributeValues: Map, attributeNames: Map ) => { @@ -156,7 +199,7 @@ export class QueryCompiler { }; compileFilterExpressionJoinNodes = ( - { expr }: FilterExpressionJoinTypeNode, + { expr }: ExpressionJoinTypeNode, filterExpressionAttributeValues: Map, attributeNames: Map ) => { @@ -175,9 +218,9 @@ export class QueryCompiler { } switch (expr.kind) { - case "FilterExpressionNode": { + case "ExpressionNode": { res += "("; - res += this.compileFilterExpression( + res += this.compileExpression( expr, filterExpressionAttributeValues, attributeNames @@ -186,15 +229,15 @@ export class QueryCompiler { break; } - case "FilterExpressionComparatorExpressions": { + case "ExpressionComparatorExpressions": { res += `${attributeName} ${expr.operation} ${attributeValue}`; filterExpressionAttributeValues.set(attributeValue, expr.value); break; } - case "FilterExpressionNotExpression": { + case "ExpressionNotExpression": { res += "NOT ("; - res += this.compileFilterExpression( + res += this.compileExpression( expr.expr, filterExpressionAttributeValues, attributeNames diff --git a/src/queryCreator.ts b/src/queryCreator.ts index 06c0664..d2a532c 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -1,5 +1,9 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { GetQueryBuilder } from "./queryBuilders/getItemQueryBuilder"; +import { + PutItemQueryBuilder, + PutItemQueryBuilderInterface, +} from "./queryBuilders/putItemQueryBuilder"; import { QueryQueryBuilder, QueryQueryBuilderInterface, @@ -36,6 +40,12 @@ export class QueryCreator { }); } + /** + * + * @param table Table to perform the query command to + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/QueryCommand/ + */ query( table: Table ): QueryQueryBuilderInterface { @@ -48,7 +58,33 @@ export class QueryCreator { }, keyConditions: [], filterExpression: { - kind: "FilterExpressionNode", + kind: "ExpressionNode", + expressions: [], + }, + }, + ddbClient: this.#props.ddbClient, + queryCompiler: this.#props.queryCompiler, + }); + } + + /** + * + * @param table Table to perform the put item command to + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/PutItemCommand/ + */ + putItem
( + table: Table + ): PutItemQueryBuilderInterface { + return new PutItemQueryBuilder({ + node: { + kind: "PutNode", + table: { + kind: "TableNode", + table, + }, + conditionExpression: { + kind: "ExpressionNode", expressions: [], }, }, diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts index c07e15e..65f86e1 100644 --- a/src/typeHelpers.ts +++ b/src/typeHelpers.ts @@ -1,3 +1,4 @@ +import { DDB } from "../test/testFixture"; import type { PartitionKey, SortKey } from "./ddbTypes"; /**