UNPKG

dynamodb-toolbox

Version:

Lightweight and type-safe query builder for DynamoDB and TypeScript.

146 lines (145 loc) 5.91 kB
import { DynamoDBToolboxError } from '../../../../errors/index.js'; import { ConditionParser } from '../../../../schema/actions/parseCondition/index.js'; import { BinarySchema } from '../../../../schema/binary/schema.js'; import { ItemSchema } from '../../../../schema/item/schema.js'; import { NumberSchema } from '../../../../schema/number/schema.js'; import { StringSchema } from '../../../../schema/string/schema.js'; import { Table } from '../../../../table/index.js'; import { pick } from '../../../../utils/pick.js'; import { isArray } from '../../../../utils/validation/isArray.js'; import { isObject } from '../../../../utils/validation/isObject.js'; const queryOperatorSet = new Set([ 'eq', 'gt', 'gte', 'lt', 'lte', 'between', 'beginsWith' ]); const getIndexKeySchema = (key) => { switch (key.type) { case 'number': return new NumberSchema({ big: true }); case 'string': return new StringSchema({}); case 'binary': return new BinarySchema({}); } }; export const parseQuery = (table, query) => { const queryIndex = getQueryIndex(table, query); const { partitionKeys, sortKeys } = flattenQuerySchema(table, queryIndex); const { partition, range } = query; const isPartitionArray = isArray(partition); const hasRange = range !== undefined; const isRangeArray = hasRange && isArray(range); if (!(queryIndex instanceof Table) && queryIndex.type === 'global') { if ('partitionKeys' in queryIndex && !isPartitionArray) { throw new DynamoDBToolboxError('queryCommand.invalidPartition', { message: 'Invalid query partition. Expected: Array.', payload: { partition } }); } if (hasRange && 'sortKeys' in queryIndex && !isRangeArray) { throw new DynamoDBToolboxError('queryCommand.invalidRange', { message: 'Invalid query range. Expected: Array.', payload: { range } }); } } const partitions = isPartitionArray ? partition : [partition]; if (partitions.length !== partitionKeys.length) { throw new DynamoDBToolboxError('queryCommand.invalidPartition', { message: `Invalid number of query partitions. Expected: ${partitionKeys.length}.`, payload: { partition: partitions } }); } const ranges = hasRange ? (isRangeArray ? range : [range]) : []; if (ranges.length > sortKeys.length) { throw new DynamoDBToolboxError('queryCommand.invalidRange', { message: `Invalid number of query ranges. Expected: Less than or equal to ${sortKeys.length}.`, payload: { range: ranges } }); } const keyConditions = []; let partitionIndex = 0; for (const partition of partitions) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion keyConditions.push({ attr: partitionKeys[partitionIndex].name, eq: partition }); partitionIndex++; } if (hasRange) { let hasRangeObject = false; let rangeIndex = 0; for (const range of ranges) { if (hasRangeObject) { throw new DynamoDBToolboxError('queryCommand.invalidRange', { message: 'Invalid query range: Range object must be provided last.', payload: { range } }); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const sortKey = sortKeys[rangeIndex]; if (isObject(range)) { keyConditions.push({ attr: sortKey.name, ...pick(range, ...queryOperatorSet) }); hasRangeObject = true; } else { keyConditions.push({ attr: sortKey.name, eq: range }); } rangeIndex++; } } const indexSchema = new ItemSchema(Object.fromEntries([...partitionKeys, ...sortKeys].map(key => [key.name, getIndexKeySchema(key)]))); const conditionParser = new ConditionParser(indexSchema); const { ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues } = conditionParser.parse({ and: keyConditions }, { expressionId: '0' }); return { KeyConditionExpression: ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues }; }; const getQueryIndex = (table, { index }) => { if (index === undefined) { return table; } const indexSchema = table.indexes[index]; if (indexSchema === undefined) { const indexes = Object.keys(table.indexes); const hasIndex = indexes.length > 0; throw new DynamoDBToolboxError('queryCommand.invalidIndex', { message: `Unknown index: ${index}. ${hasIndex ? ` Expected one of: ${indexes.join(', ')}.` : ''}`, payload: { received: index, ...(hasIndex ? { expected: indexes } : {}) } }); } return indexSchema; }; const flattenQuerySchema = (table, queryIndex) => { var _a; if (queryIndex instanceof Table) { return { partitionKeys: [queryIndex.partitionKey], sortKeys: queryIndex.sortKey ? [queryIndex.sortKey] : [] }; } switch (queryIndex.type) { case 'global': return { partitionKeys: (_a = queryIndex.partitionKeys) !== null && _a !== void 0 ? _a : [queryIndex.partitionKey], sortKeys: queryIndex.sortKeys ? queryIndex.sortKeys : queryIndex.sortKey ? [queryIndex.sortKey] : [] }; case 'local': return { partitionKeys: [table.partitionKey], sortKeys: [queryIndex.sortKey] }; } };