UNPKG

@moicky/dynamodb

Version:

Contains a collection of convenience functions for working with AWS DynamoDB

542 lines (430 loc) 15.4 kB
# @moicky/dynamodb ![](https://img.shields.io/github/actions/workflow/status/moicky/dynamodb/npm-publish.yml?label=build) ![](https://img.shields.io/github/actions/workflow/status/moicky/dynamodb/run-tests.yml?label=tests) ## Description Contains convenience functions for all major dynamodb operations. Requires very little code to interact with items from aws dynamodb. Uses **aws sdk v3** and fixes several issues: - 🎁 Will **automatically marshall and unmarshall** items - 📦 Will **group items into batches** to avoid aws limits and improve performance - ⏱ Will **automatically** add `createdAt` and `updatedAt` attributes on all items to track their most recent create/update operation timestamp. Example value: `Date.now() -> 1685138436000` - 🔄 Will **retry** `getItems`, `deleteItems` **up to 3 times** on unprocessed items and `queryAllItems` until finished - 🔒 When specifying an item using its keySchema, all additional attributes (apart from keySchema attributes from `initSchema` or `PK` & `SK` as default) will be removed to avoid errors - 👻 Will **use placeholders** to avoid colliding with [reserved words](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html) if applicable - 🌎 Supports globally defined default arguments for each operation ([example](#configuring-global-defaults)) - 🔨 Supports fixes for several issues with dynamodb ([example](#applying-fixes)) - 📖 Offers a convenient way to use pagination with queries ([example](#paginated-items)) - 🗂️ Supports **transactGetItems** & **transactWriteItems** ## Installation ```bash npm i @moicky/dynamodb ``` ## Setup Automatically grabs `DYNAMODB_TABLE` as an **environment variable** and assumes `PK` and `SK` as it's schema. Can be customized using `initSchema` with one or more tables: ```ts import { initSchema } from "@moicky/dynamodb"; // Should be called once at the start of the runtime before any operation is executed initSchema({ // first one will be used by default if no TableName is specified when calling functions [process.env.DEFAULT_TABLE]: { hash: "PK", range: "SK", }, [process.env.SECOND_TABLE]: { hash: "somePK", }, }); ``` ## Working with multiple tables Every function accepts `args` which can include a `TableName` property that specifies the table and uses the keySchema from `initSchema()` ```ts import { getItem, putItem, deleteItem } from "@moicky/dynamodb"; await putItem( { PK: "User/1", someSortKey: "Book/1", title: "The Great Gatsby", author: "F. Scott Fitzgerald", released: 1925, }, { TableName: process.env.SECOND_TABLE } ); const item = await getItem( { PK: "User/1", someSortKey: "Book/1" }, { TableName: process.env.SECOND_TABLE } ); await deleteItem(item, { TableName: process.env.SECOND_TABLE }); ``` ## Usage Examples ### Put Items Every put operation also adds the `createdAt` attribute with the current timestamp on each item. ```ts import { putItem, putItems } from "@moicky/dynamodb"; // Put single item into dynamodb await putItem({ PK: "User/1", SK: "Book/1", title: "The Great Gatsby", author: "F. Scott Fitzgerald", released: 1925, }); // Put multiple items into dynamodb await putItems([ { PK: "User/1", SK: "Book/1", title: "The Great Gatsby", author: "F. Scott Fitzgerald", released: 1925, }, // ... infinite more items (will be grouped into batches of 25 due to aws limit) ]); ``` ### Get Items ```ts import { getItem, getItems, getAllItems } from "@moicky/dynamodb"; // Passing more than just the key is possible, but will be removed to avoid errors // Get single item await getItem({ PK: "User/1", SK: "Book/1", title: "The Great Gatsby", // additional fields will be removed before sending author: "F. Scott Fitzgerald", released: 1925, }); // Get multiple items // Items will be grouped into batches of 100 and will be retried up to 3 times if there are unprocessed items // Will also only request each keySchema once, even if it is present multiple times in the array to improve performance await getItems([ { PK: "User/1", SK: "Book/1", title: "The Great Gatsby", // additional fields will be removed before sending author: "F. Scott Fitzgerald", released: 1925, }, // ... infinite more items (will be grouped into batches of 100 due to aws limit) and retried up to 3 times ]); // Retrieve all items using ScanCommand await getAllItems(); ``` ### Delete Items ```ts import { deleteItem, deleteItems } from "@moicky/dynamodb"; // Delete a single item await deleteItem({ PK: "User/1", SK: "Book/1", title: "The Great Gatsby", // additional fields will be removed before sending to avoid errors author: "F. Scott Fitzgerald", released: 1925, }); // Delete multiple items // Will only delete each keySchema once, even if it is present multiple times in the array to improve performance and avoid aws errors await deleteItems([ { PK: "User/1", SK: "Book/1" }, // ... infinite more items (will be grouped into batches of 25 due to aws limit) and retried up to 3 times ]); ``` ### Update Items Every update operation also upserts the `updatedAt` attribute with the current timestamp on each item. ```ts import { updateItem, removeAttributes } from "@moicky/dynamodb"; // Update the item and overwrite all supplied fields await updateItem( { PK: "User/1", SK: "Book/1" }, // reference to item { description: "A book about a rich guy", author: "F. Scott Fitzgerald" } // fields to update ); await updateItem( { PK: "User/1", SK: "Book/1" }, { released: 2000, maxReleased: 1950 }, // maxReleased will not be updated on the item, since it is referenced inside the ConditionExpression { ConditionExpression: "#released < :maxReleased" } ); const newItem = await updateItem( { PK: "User/1", SK: "Book/1" }, { released: 2000 }, { ReturnValues: "ALL_NEW" } ); console.log(newItem); // { "PK": "User/1", "SK": "Book/1", "released": 2000 } await removeAttributes({ PK: "User/1", SK: "Book/1" }, ["description"]); ``` ### Query Items ```ts import { query, queryItems, queryAllItems } from "@moicky/dynamodb"; // You HAVE TO use placeholders for the keyCondition & filterExpression: // Prefix the attributeNames with a hash (#) and the attributeValues with a colon (:) // Query only using keyCondition and retrieve complete response const booksResponse = await query("#PK = :PK and begins_with(#SK, :SK)", { PK: "User/1", SK: "Book/", }); // Query and retrieve unmarshalled items array const books = await queryItems("#PK = :PK and begins_with(#SK, :SK)", { PK: "User/1", SK: "Book/", }); // Query and retry until all items are retrieved (due to aws limit of 1MB per query) const allBooks = await queryAllItems("#PK = :PK and begins_with(#SK, :SK)", { PK: "User/1", SK: "Book/", }); // Query with filterExpression (also specifiy attributes inside the key object) const booksWithFilter = await queryAllItems( "#PK = :PK and begins_with(#SK, :SK)", // keyCondition { // definition for all attributes PK: "User/1", SK: "Book/", from: 1950, to: 2000, }, // additional args with filterExpression for example { FilterExpression: "#released BETWEEN :from AND :to" } ); ``` #### Paginated Items ```ts // Pagination const { items, hasNextPage, hasPreviousPage, currentPage } = await queryPaginatedItems( "#PK = :PK and begins_with(#SK, :SK)", { PK: "User/1", SK: "Book/" }, { pageSize: 100 } ); // items: The items on the current page. // currentPage: { number: 1, firstKey: { ... }, lastKey: { ... } } const { items: nextItems, currentPage: nextPage } = await queryPaginatedItems( "#PK = :PK and begins_with(#SK, :SK)", { PK: "User/1", SK: "Book/" }, { pageSize: 100, currentPage } // args.direction: 'next' or 'previous' ); // items: The items on the second page. // currentPage: { number: 2, firstKey: { ... }, lastKey: { ... } } ``` ### Miscellaneous ```ts import { itemExists, getAscendingId } from "@moicky/dynamodb"; // Check if an item exists using keySchema const exists = await itemExists({ PK: "User/1", SK: "Book/1" }); console.log(exists); // true or false // Generate ascending ID // Specify Partition-Key and optionally the Sort-Key. // Example Structure 1: PK: "User/1", SK: "{{ ASCENDING_ID }}" // Last item: { PK: "User/1", SK: "00000009" } const id1 = await getAscendingId({ PK: "User/1" }); console.log(id1); // "00000010" // Example Structure 2: PK: "User/1", SK: "Book/{{ ASCENDING_ID }}" // Last item: { PK: "User/1", SK: "Book/00000009" } const id2 = await getAscendingId({ PK: "User/1", SK: "Book/" }); console.log(id2); // "00000010" // Specify length of ID const id3 = await getAscendingId({ PK: "User/1", SK: "Book/", length: 4 }); console.log(id3); // "0010" // Example Structure 3: somePartitionKey: "User/1", SK: "Book/{{ ASCENDING_ID }}" // Last item: { somePartitionKey: "User/1", SK: "Book/00000009" } const id4 = await getAscendingId({ somePartitionKey: "User/1", SK: "Book/", }); console.log(id4); // "00000010" ``` ### TransactWriteItems ```ts import { transactWriteItems } from "@moicky/dynamodb"; // Perform a TransactWriteItems operation const response = await transactWriteItems([ { Put: { item: { PK: "User/1", SK: "Book/1", title: "The Great Gatsby", author: "F. Scott Fitzgerald", released: 1925, }, }, }, { Update: { key: { PK: "User/1", SK: "Book/1" }, updateData: { title: "The Great Gatsby - Updated" }, }, }, { Delete: { key: { PK: "User/1", SK: "Book/1" }, }, }, { ConditionCheck: { key: { PK: "User/1", SK: "Book/1" }, ConditionExpression: "#title = :title", conditionData: { title: "The Great Gatsby" }, }, }, ]); console.log(response); ``` ### TransactGetItems ```ts import { transactGetItems } from "@moicky/dynamodb"; // Perform a TransactGetItems operation const items = await transactGetItems([ { key: { PK: "User/1", SK: "Book/1" } }, { key: { PK: "User/1", SK: "Book/2" } }, { key: { PK: "User/1", SK: "Book/3" } }, ]); console.log(items); ``` ## Configuring global defaults Global defaults can be configured using the `initDefaults` function. This allows to provide but still override every property of the `args` parameter. Should be called before any DynamoDB operations are performed. ```ts import { initDefaultArguments, getItem } from "@moicky/dynamodb"; // This example enables consistent reads for all DynamoDB operations which support it. initDefaultArguments({ getItem: { ConsistentRead: true }, getAllItems: { ConsistentRead: true }, itemExists: { ConsistentRead: true }, query: { ConsistentRead: true }, queryItems: { ConsistentRead: true }, queryAllItems: { ConsistentRead: true }, }); // It is still possible to override any arguments when calling a function const itemWithoutConsistentRead = await getItem( { PK: "User/1", SK: "Book/001" }, { ConsistentRead: false } ); ``` ## Applying fixes Arguments which are passed to [marshall](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/interfaces/_aws_sdk_util_dynamodb.marshallOptions.html) and [unmarshall](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/interfaces/_aws_sdk_util_dynamodb.unmarshallOptions.html) from `@aws-sdk/util-dynamodb` can be configured using ```ts import { initFixes } from "@moicky/dynamodb"; initFixes({ marshallOptions: { removeUndefinedValues: true, }, unmarshallOptions: { wrapNumbers: true, }, }); ``` When using `GlobalSecondaryIndexes`, DynamoDb does not support using `ConsistantRead`. This is fixed by default (`ConsistantRead` is turned off) and can be configured using: ```ts import { initFixes } from "@moicky/dynamodb"; initFixes({ disableConsistantReadWhenUsingIndexes: { enabled: true, // default, // Won't disable ConsistantRead if IndexName is specified here. // This works because DynamoDB supports ConsistantRead on LocalSecondaryIndexes stillUseOnLocalIndexes: ["localIndexName1", "localIndexName2"], }, }); ``` ## What are the benefits and why should I use it? Generally it makes it easier to interact with the dynamodb from AWS. Here are some before and after examples using the new aws-sdk v3: ### Put ```js const demoItem = { PK: "User/1", SK: "Book/1", title: "The Great Gatsby", author: "F. Scott Fitzgerald", released: 1925, }; // Without helpers: import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; const client = new DynamoDBClient({ region: process.env.AWS_REGION, }); const newItem = await client .send( new PutItemCommand({ TableName: process.env.DYNAMODB_TABLE, Item: marshall(demoItem), ReturnValues: "ALL_NEW", }) ) .then((result) => unmarshall(result.Attributes)); // With helpers: import { putItem } from "@moicky/dynamodb"; const newItem = await putItem(demoItem, { ReturnValues: "ALL_NEW" }); ``` ### Query ```js // Without helpers: import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; const client = new DynamoDBClient({ region: process.env.AWS_REGION, }); const results = await client .send( new QueryCommand({ TableName: process.env.DYNAMODB_TABLE, KeyConditionExpression: "#PK = :PK and begins_with(#SK, :SK)", ExpressionAttributeNames: { "#PK": "PK", "#SK": "SK", }, ExpressionAttributeValues: { ":PK": marshall("User/1"), ":SK": marshall("Book/"), }, }) ) .then((result) => result.Items.map((item) => unmarshall(item))); // With helpers import { queryItems } from "@moicky/dynamodb"; const results = await queryItems("#PK = :PK and begins_with(#SK, :SK)", { PK: "User/1", SK: "Book/", }); ``` ### Update ```js // Without helpers import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; const client = new DynamoDBClient({ region: process.env.AWS_REGION, }); const result = await client .send( new UpdateItemCommand({ TableName: process.env.DYNAMODB_TABLE, Key: marshall({ PK: "User/1", SK: "Book/1" }), UpdateExpression: "SET #released = :released, #title = :title", ExpressionAttributeNames: { "#released": "released", "#title": "title", }, ExpressionAttributeValues: marshall({ ":released": 2000, ":title": "New Title", }), ReturnValues: "ALL_NEW", }) ) .then((result) => unmarshall(result.Attributes)); // With helpers import { updateItem } from "@moicky/dynamodb"; const result = await updateItem( { PK: "User/1", SK: "Book/1" }, { released: 2000, title: "New Title" }, { ReturnValues: "ALL_NEW" } ); ``` ## Tests ### Setup Requires environment variables to be present for the tests to successfully connect to dynamodb tables. You can find a list of required environment variables here: [.env.template](.env.template) They can be obtained using the **template.yml** which can be deployed on aws using: ```bash sam deploy ``` Will then return the table-names as the output of the template ### Execution Finally executing all tests: ```bash npm run test ```