@moicky/dynamodb
Version:
Contains a collection of convenience functions for working with AWS DynamoDB
244 lines (243 loc) • 9.87 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.queryPaginatedItems = exports.queryAllItems = exports.queryItems = exports.query = void 0;
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
const lib_1 = require("../lib");
/**
* The internal _query function that executes the QueryCommand with given conditions and arguments.
* @param keyCondition - The condition for the key in DynamoDB QueryCommand.
* @param key - Definitions for the attributes used in keyCondition and FilterExpression
* @param args - The additional arguments to override or specify for {@link QueryCommandInput}
* @returns A promise that resolves to the output of {@link QueryCommandOutput}
* @private
*/
async function _query(keyCondition, key, args = {}) {
args = (0, lib_1.withFixes)(args);
return (0, lib_1.getClient)().send(new client_dynamodb_1.QueryCommand({
KeyConditionExpression: keyCondition,
ExpressionAttributeValues: (0, lib_1.getAttributeValues)(key, {
attributesToGet: [
...(0, lib_1.getAttributesFromExpression)(keyCondition, ":"),
...(0, lib_1.getAttributesFromExpression)(args?.FilterExpression || "", ":"),
],
}),
ExpressionAttributeNames: (0, lib_1.getAttributeNames)(key, {
attributesToGet: [
...(0, lib_1.getAttributesFromExpression)(keyCondition),
...(0, lib_1.getAttributesFromExpression)(args?.FilterExpression || ""),
...(0, lib_1.getAttributesFromExpression)(args?.ProjectionExpression || ""),
],
}),
...args,
TableName: args?.TableName || (0, lib_1.getDefaultTable)(),
}));
}
/**
* Query a single item in a DynamoDB table using a key condition.
* @param keyCondition - The condition for the key in DynamoDB QueryCommand.
* @param key - Definitions for the attributes used in keyCondition and FilterExpression
* @param args - The additional arguments to override or specify for {@link QueryCommandInput}
* @returns A promise that resolves to the output of {@link QueryCommandOutput}
*
* @example
* Query a single item using a key condition
* ```javascript
* const booksResponse = await query("#PK = :PK and begins_with(#SK, :SK)", {
* PK: "User/1",
* SK: "Book/",
* });
* ```
*/
async function query(keyCondition, key, args) {
return _query(keyCondition, key, (0, lib_1.withDefaults)(args, "query"));
}
exports.query = query;
/**
* Query multiple items from the DynamoDB table using a key condition and unmarshalls the result.
* @param keyCondition - The condition for the key in DynamoDB QueryCommand.
* @param key - Definitions for the attributes used in keyCondition and FilterExpression
* @param args - The additional arguments to override or specify for {@link QueryCommandInput}
* @returns A promise that resolves to an array of unmarshalled items.
*
* @example
* Query multiple items using a key condition
* ```javascript
* const books = await queryItems("#PK = :PK and begins_with(#SK, :SK)", {
* PK: "User/1",
* SK: "Book/",
* });
* ```
*/
async function queryItems(keyCondition, key, args = {}) {
args = (0, lib_1.withDefaults)(args, "queryItems");
return _query(keyCondition, key, args).then((res) => (res?.Items || [])
.map((item) => item && (0, lib_1.unmarshallWithOptions)(item))
.filter(Boolean));
}
exports.queryItems = queryItems;
/**
* Query all items from the DynamoDB table using a key condition and unmarshalls the result.
* This function retries until all items are retrieved due to the AWS limit of 1MB per query.
* @param keyCondition - The condition for the key in DynamoDB QueryCommand.
* @param key - Definitions for the attributes used in keyCondition and FilterExpression
* @param args - The additional arguments to override or specify for {@link QueryCommandInput}
* @returns A promise that resolves to an array of unmarshalled items.
*
* @example
* Query all items using a key condition
* ```javascript
* const allBooks = await queryAllItems("#PK = :PK and begins_with(#SK, :SK)", {
* PK: "User/1",
* SK: "Book/",
* });
* 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" }
* );
* ```
*/
async function queryAllItems(keyCondition, key, args = {}) {
args = (0, lib_1.withDefaults)(args, "queryAllItems");
let data = await _query(keyCondition, key, args);
while (data.LastEvaluatedKey) {
if (!("Limit" in args) || data.Items.length < args?.Limit) {
let helper = await _query(keyCondition, key, {
...args,
ExclusiveStartKey: data.LastEvaluatedKey,
});
if (helper?.Items) {
data.Items.push(...helper.Items);
}
else {
break;
}
data.LastEvaluatedKey = helper.LastEvaluatedKey;
}
else {
break;
}
}
if (args.Limit && data.Items.length > args.Limit) {
data.Items = data.Items.slice(0, args.Limit);
}
return (data?.Items || [])
.map((item) => item && (0, lib_1.unmarshallWithOptions)(item))
.filter(Boolean);
}
exports.queryAllItems = queryAllItems;
/**
* Query items from the DynamoDB table using a key condition in a paginated manner.
* @param keyCondition - The condition for the key in DynamoDB QueryCommand.
* @param key - Definitions for the attributes used in keyCondition and FilterExpression
* @param args- The pagination arguments, including pageSize and direction. {@link PaginationArgs}
* @returns A promise that resolves to a {@link PaginationResult}.
* @example
* Query the first page of items using a key condition
* ```javascript
* 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 }
* );
* // items: The items on the second page.
* // currentPage: { number: 2, firstKey: { ... }, lastKey: { ... } }
* ```
*/
async function queryPaginatedItems(keyCondition, key, args) {
args = (0, lib_1.withDefaults)(args, "queryPaginatedItems");
const pageSize = args.pageSize;
const direction = args.direction || "next";
const currentPage = args.currentPage;
delete args.pageSize;
delete args.direction;
delete args.currentPage;
const queryArgs = {
...args,
Limit: pageSize,
ScanIndexForward: direction === "next",
};
let newPageNumber;
switch (direction) {
case "next":
if (currentPage?.lastKey) {
queryArgs.ExclusiveStartKey = (0, lib_1.marshallWithOptions)(currentPage.lastKey);
}
newPageNumber = (currentPage?.number || 0) + 1;
break;
case "previous":
if (currentPage?.firstKey) {
queryArgs.ExclusiveStartKey = (0, lib_1.marshallWithOptions)(currentPage.firstKey);
}
newPageNumber = (currentPage?.number || 2) - 1;
break;
}
let data = await _query(keyCondition, key, queryArgs);
// Assume schema for either given index or the table's schema
const keyAttributes = Object.keys(data.LastEvaluatedKey || queryArgs.ExclusiveStartKey || {});
while (data.LastEvaluatedKey) {
if (data.Items.length < pageSize) {
let helper = await _query(keyCondition, key, {
...queryArgs,
ExclusiveStartKey: data.LastEvaluatedKey,
});
if (helper?.Items) {
data.Items.push(...helper.Items);
}
else {
break;
}
data.LastEvaluatedKey = helper.LastEvaluatedKey;
}
else {
break;
}
}
let hasNextPage = direction === "previous" ||
// If pagination matches exactly with total items, dynamodb still returns a LastEvaluatedKey even tho next page is empty
// Therefore we check if the next page actually has items
(!!data.LastEvaluatedKey &&
(await _query(keyCondition, key, {
...queryArgs,
Limit: 1,
ExclusiveStartKey: data.LastEvaluatedKey,
}).then(({ Count }) => Count > 0)));
let hasPreviousPage = newPageNumber > 1;
data.Items = data.Items || [];
direction === "previous" && data.Items.reverse();
const applySchema = (item) => {
return keyAttributes.reduce((acc, key) => ({ ...acc, [key]: item[key] }), {});
};
const firstItem = data.Items?.[0];
const lastItem = data.Items?.[data.Items?.length - 1];
const firstKey = firstItem && (0, lib_1.unmarshallWithOptions)(applySchema(firstItem));
const lastKey = lastItem && (0, lib_1.unmarshallWithOptions)(applySchema(lastItem));
const items = data.Items.map((item) => item && (0, lib_1.unmarshallWithOptions)(item)).filter(Boolean);
return {
items,
hasPreviousPage,
hasNextPage,
currentPage: {
number: newPageNumber,
firstKey,
lastKey,
},
};
}
exports.queryPaginatedItems = queryPaginatedItems;