UNPKG

@moicky/dynamodb

Version:

Contains a collection of convenience functions for working with AWS DynamoDB

244 lines (243 loc) 9.87 kB
"use strict"; 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;