Skip to content

Commit

Permalink
add support for delete item command
Browse files Browse the repository at this point in the history
  • Loading branch information
mindler-olli committed Mar 19, 2024
1 parent d50633b commit 8a1adb5
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/nodes/deleteNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ExpressionNode } from "./expressionNode";
import { KeysNode } from "./keysNode";
import { ReturnValuesNode } from "./returnValuesNode";
import { TableNode } from "./tableNode";

export type DeleteNode = {
readonly kind: "DeleteNode";
readonly table: TableNode;
readonly conditionExpression: ExpressionNode;
readonly returnValues?: ReturnValuesNode;
readonly keys?: KeysNode;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`DeleteItemQueryBuilder > handles a simple delete query 1`] = `
{
"dataTimestamp": 2,
"userId": "1",
}
`;
50 changes: 50 additions & 0 deletions src/queryBuilders/deleteItemQueryBuilder.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { DDB } from "../../test/testFixture";
import { getDDBClientFor, startDDBTestContainer } from "../../test/testUtil";
import { Tsynamo } from "./../index";

describe("DeleteItemQueryBuilder", () => {
let tsynamoClient: Tsynamo<DDB>;

beforeAll(async () => {
const testContainer = await startDDBTestContainer();

tsynamoClient = new Tsynamo<DDB>({
ddbClient: await getDDBClientFor(testContainer),
});
});

it("handles a simple delete query", async () => {
await tsynamoClient
.putItem("myTable")
.item({
userId: "1",
dataTimestamp: 2,
})
.execute();

const itemBeforeDeletion = await tsynamoClient
.getItemFrom("myTable")
.keys({ userId: "1", dataTimestamp: 2 })
.execute();

expect(itemBeforeDeletion).toBeDefined();

const deleteResponse = await tsynamoClient
.deleteItem("myTable")
.keys({
userId: "1",
dataTimestamp: 2,
})
.returnValues("ALL_OLD")
.execute();

expect(deleteResponse).toMatchSnapshot();

const itemAfterDeletion = await tsynamoClient
.getItemFrom("myTable")
.keys({ userId: "1", dataTimestamp: 2 })
.execute();

expect(itemAfterDeletion).toBeUndefined();
});
});
195 changes: 195 additions & 0 deletions src/queryBuilders/deleteItemQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { DeleteNode } from "../nodes/deleteNode";
import { ReturnValuesOptions } from "../nodes/returnValuesNode";
import { QueryCompiler } from "../queryCompiler";
import {
ExecuteOutput,
ObjectKeyPaths,
PickPk,
PickSkRequired,
} from "../typeHelpers";
import { preventAwait } from "../util/preventAwait";
import {
AttributeBeginsWithExprArg,
AttributeBetweenExprArg,
AttributeContainsExprArg,
AttributeFuncExprArg,
BuilderExprArg,
ComparatorExprArg,
ExprArgs,
ExpressionBuilder,
NotExprArg,
} from "./expressionBuilder";

export interface DeleteItemQueryBuilderInterface<
DDB,
Table extends keyof DDB,
O
> {
// conditionExpression
conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: ComparatorExprArg<DDB, Table, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O>;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

execute(): Promise<ExecuteOutput<O>[] | undefined>;
}

/**
* @todo support ConditionExpression
*/
export class DeleteItemQueryBuilder<
DDB,
Table extends keyof DDB,
O extends DDB[Table]
> implements DeleteItemQueryBuilderInterface<DDB, Table, O>
{
readonly #props: DeleteItemQueryBuilderProps;

constructor(props: DeleteItemQueryBuilderProps) {
this.#props = props;
}

conditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: ExprArgs<DDB, Table, O, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
const eB = new ExpressionBuilder<DDB, Table, O>({
node: { ...this.#props.node.conditionExpression },
});

const expressionNode = eB.expression(...args)._getNode();

return new DeleteItemQueryBuilder<DDB, Table, O>({
...this.#props,
node: {
...this.#props.node,
conditionExpression: expressionNode,
},
});
}

orConditionExpression<Key extends ObjectKeyPaths<DDB[Table]>>(
...args: ExprArgs<DDB, Table, O, Key>
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
const eB = new ExpressionBuilder<DDB, Table, O>({
node: { ...this.#props.node.conditionExpression },
});

const expressionNode = eB.orExpression(...args)._getNode();

return new DeleteItemQueryBuilder<DDB, Table, O>({
...this.#props,
node: {
...this.#props.node,
conditionExpression: expressionNode,
},
});
}

returnValues(
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
): DeleteItemQueryBuilderInterface<DDB, Table, O> {
return new DeleteItemQueryBuilder<DDB, Table, O>({
...this.#props,
node: {
...this.#props.node,
returnValues: {
kind: "ReturnValuesNode",
option,
},
},
});
}

keys<Keys extends PickPk<DDB[Table]> & PickSkRequired<DDB[Table]>>(
keys: Keys
) {
return new DeleteItemQueryBuilder<DDB, Table, O>({
...this.#props,
node: {
...this.#props.node,
keys: {
kind: "KeysNode",
keys,
},
},
});
}

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

preventAwait(
DeleteItemQueryBuilder,
"Don't await DeleteItemQueryBuilder instances directly. To execute the query you need to call the `execute` method"
);

interface DeleteItemQueryBuilderProps {
readonly node: DeleteNode;
readonly ddbClient: DynamoDBDocumentClient;
readonly queryCompiler: QueryCompiler;
}
2 changes: 1 addition & 1 deletion src/queryBuilders/putItemQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class PutItemQueryBuilder<
}

returnValues(
option: ReturnValuesOptions
option: Extract<ReturnValuesOptions, "NONE" | "ALL_OLD">
): PutItemQueryBuilderInterface<DDB, Table, O> {
return new PutItemQueryBuilder<DDB, Table, O>({
...this.#props,
Expand Down
52 changes: 50 additions & 2 deletions src/queryCompiler/queryCompiler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { GetCommand, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
import {
DeleteCommand,
GetCommand,
PutCommand,
QueryCommand,
} from "@aws-sdk/lib-dynamodb";
import { ExpressionJoinTypeNode } from "../nodes/expressionJoinTypeNode";
import { ExpressionNode } from "../nodes/expressionNode";
import { GetNode } from "../nodes/getNode";
Expand All @@ -11,19 +16,23 @@ import {
} from "./compilerUtil";
import { AttributesNode } from "../nodes/attributesNode";
import { PutNode } from "../nodes/putNode";
import { DeleteNode } from "../nodes/deleteNode";

export class QueryCompiler {
compile(rootNode: QueryNode): QueryCommand;
compile(rootNode: GetNode): GetCommand;
compile(rootNode: PutNode): PutCommand;
compile(rootNode: QueryNode | GetNode | PutNode) {
compile(rootNode: DeleteNode): DeleteCommand;
compile(rootNode: QueryNode | GetNode | PutNode | DeleteNode) {
switch (rootNode.kind) {
case "GetNode":
return this.compileGetNode(rootNode);
case "QueryNode":
return this.compileQueryNode(rootNode);
case "PutNode":
return this.compilePutNode(rootNode);
case "DeleteNode":
return this.compileDeleteNode(rootNode);
}
}

Expand Down Expand Up @@ -140,6 +149,45 @@ export class QueryCompiler {
});
}

compileDeleteNode(deleteNode: DeleteNode) {
const {
table: tableNode,
returnValues: returnValuesNode,
keys: keysNode,
conditionExpression: conditionExpressionNode,
} = deleteNode;

const attributeNames = new Map();
const filterExpressionAttributeValues = new Map();

const compiledConditionExpression = this.compileExpression(
conditionExpressionNode,
filterExpressionAttributeValues,
attributeNames
);

return new DeleteCommand({
TableName: tableNode.table,
Key: keysNode?.keys,
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))
Expand Down
27 changes: 27 additions & 0 deletions src/queryCreator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { DeleteItemQueryBuilder } from "./queryBuilders/deleteItemQueryBuilder";
import { GetQueryBuilder } from "./queryBuilders/getItemQueryBuilder";
import {
PutItemQueryBuilder,
Expand Down Expand Up @@ -92,6 +93,32 @@ export class QueryCreator<DDB> {
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/DeleteItemCommand/
*/
deleteItem<Table extends keyof DDB & string>(
table: Table
): DeleteItemQueryBuilder<DDB, Table, DDB[Table]> {
return new DeleteItemQueryBuilder<DDB, Table, DDB[Table]>({
node: {
kind: "DeleteNode",
table: {
kind: "TableNode",
table,
},
conditionExpression: {
kind: "ExpressionNode",
expressions: [],
},
},
ddbClient: this.#props.ddbClient,
queryCompiler: this.#props.queryCompiler,
});
}
}

export interface QueryCreatorProps {
Expand Down

0 comments on commit 8a1adb5

Please sign in to comment.