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