dynamodb-toolbox
Version:
Lightweight and type-safe query builder for DynamoDB and TypeScript.
146 lines (145 loc) • 5.91 kB
JavaScript
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]
};
}
};