✨ Type-friendly DynamoDB query builder! ✨
Inspired by Kysely
Tsynamo simplifies the DynamoDB API so that you don't have to write commands with raw expressions and hassle with the attribute names and values. Moreover, Tsynamo makes sure you use correct types in your DynamoDB expressions, and the queries are nicer to write with autocompletion!
[!WARNING]
Tsynamo is still an early stage project, please post issues if you notice something missing from the API!
Available in NPM.
npm i tsynamo
pnpm install tsynamo
yarn add tsynamo
[!NOTE] You can also try it out at Tsynamo Playground
import { PartitionKey, SortKey } from "tsynamo";
export interface DDB {
UserEvents: {
userId: PartitionKey<string>;
eventId: SortKey<number>;
eventType: string;
userAuthenticated: boolean;
};
}
[!TIP] Notice that you can have multiple tables in the DDB schema. Nested attributes are supported too.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
const ddbClient = DynamoDBDocumentClient.from(
new DynamoDBClient({
/* Configure client... */
})
);
[!IMPORTANT] The document client must come from @aws-sdk/lib-dynamodb!
const tsynamoClient = new Tsynamo<DDB>({
ddbClient: dynamoDbDocumentClient,
});
await tsynamoClient
.getItem("UserEvents")
.keys({
userId: "123",
eventId: 222,
})
.attributes(["userId"])
.execute();
await tsynamoClient
.query("UserEvents")
.keyCondition("userId", "=", "123")
.execute();
await tsynamoClient
.query("UserEvents")
.keyCondition("userId", "=", "123")
.keyCondition("eventId", "<", 1000)
.execute();
await tsynamoClient
.query("UserEvents")
.keyCondition("userId", "=", "123")
.filterExpression("eventType", "=", "LOG_IN_EVENT")
.execute();
await tsynamoClient
.query("UserEvents")
.keyCondition("userId", "=", "123")
.filterExpression("eventType", "begins_with", "LOG")
.execute();
await tsynamoClient
.query("UserEvents")
.keyCondition("userId", "=", "123")
.filterExpression("eventType", "begins_with", "LOG_IN")
.orFilterExpression("eventType", "begins_with", "SIGN_IN")
.execute();
await tsynamoClient
.query("UserEvents")
.keyCondition("userId", "=", "123")
.filterExpression("eventType", "=", "LOG_IN")
.orFilterExpression((qb) =>
qb
.filterExpression("eventType", "=", "UNAUTHORIZED_ACCESS")
.filterExpression("userAuthenticated", "=", true)
)
.orFilterExpression("eventType", "begins_with", "SIGN_IN")
.execute();
[!NOTE] This would compile as the following FilterExpression:
eventType = "LOG_IN" OR (eventType = "UNAUTHORIZED_ACCESS" AND userAuthenticated = true
)
await tsynamoClient
.query("UserEvents")
.keyCondition("userId", "=", "123")
.filterExpression("NOT", (qb) =>
qb.filterExpression("eventType", "=", "LOG_IN")
)
.execute();
[!NOTE] This would compile as the following FilterExpression:
NOT eventType = "LOG_IN"
, i.e. return all events whose types is not "LOG_IN"
await tsynamoClient
.putItem("myTable")
.item({
userId: "123",
eventId: 313,
})
.execute();
await tsynamoClient
.putItem("myTable")
.item({
userId: "123",
eventId: 313,
})
.conditionExpression("userId", "attribute_not_exists")
.execute();
await tsynamoClient
.putItem("myTable")
.item({
userId: "123",
eventId: 313,
})
.conditionExpression("userId", "attribute_not_exists")
.orConditionExpression("eventType", "begins_with", "LOG_")
.execute();
await tsynamoClient
.deleteItem("myTable")
.keys({
userId: "123",
eventId: 313,
})
.execute();
await tsynamoClient
.deleteItem("myTable")
.keys({
userId: "123",
eventId: 313,
})
.conditionExpression("eventType", "attribute_not_exists")
.execute();
await tsynamoClient
.updateItem("myTable")
.keys({ userId: "1", dataTimestamp: 2 })
.set("nested.nestedBoolean", "=", true)
.remove("nested.nestedString")
.add("somethingElse", 10)
.add("someSet", new Set(["4", "5"]))
.delete("nested.nestedSet", new Set(["4", "5"]))
.conditionExpression("somethingElse", ">", 0)
.execute();
One can also utilise DynamoDB Transaction features using Tsynamo. You can perform operations to multiple tables in a single transaction command.
DynamoDB enables you to do multiple Put
, Update
and Delete
in a single WriteTransaction
command. One can also provide an optional ClientRequestToken
to the transaction to ensure idempotency.
const trx = tsynamoClient.createWriteTransaction();
trx.addItem({
Put: tsynamoClient
.putItem("myTable")
.item({ userId: "313", dataTimestamp: 1 }),
});
trx.addItem({
Update: tsynamoClient
.updateItem("myTable")
.keys({ userId: "313", dataTimestamp: 2 })
.set("tags", "=", ["a", "b", "c"]),
});
trx.addItem({
Delete: tsynamoClient.deleteItem("myTable").keys({
userId: "313",
dataTimestamp: 3,
}),
});
await trx.execute();
[!IMPORTANT] When passing the items into the transaction using the tsynamoClient, do not execute the individual calls! Instead just pass in the query builder as the item.
[!WARNING]
DynamoDB also supports doingConditionCheck
operations in the transaction, but Tsynamo does not yet support those.
Since the read transaction output can affect multiple tables, the resulting output is an array of tuples where the first item is the name of the table and the second item is the item itself (or undefined
if the item was not found). This can be used as a discriminated union to determine the resulting item's type.
const trx = tsynamoClient.createReadTransaction();
trx.addItem({
Get: tsynamoClient.getItem("myTable").keys({
userId: "123",
dataTimestamp: 222,
}),
});
trx.addItem({
Get: tsynamoClient.getItem("myOtherTable").keys({
userId: "321",
stringTimestamp: "222",
}),
});
const result = await trx.execute();
Then, one can loop through the result items as so:
// note that the items can be undefined if they were not found from DynamoDB
result.forEach(([table, item]) => {
if (table === "myTable") {
// item's type is DDB["myTable"]
// ...
} else if (table === "myOtherTable") {
// item's type is DDB["myOtherTable"]
// ...
}
});