The toExpressionAttributeValues function in the FindOptions class does not correctly handle scenarios where the same field is used multiple times in the filter expression.
This results in a single alias being generated for multiple occurrences of the same field, causing validation errors when constructing the DynamoDB query.
For example
Given the following filter expression:
options: FindOptions = {
filter: "created_date < '2020-01-01T00:00:00.000Z'" AND "created_date < '2025-05-05T05:05:05.000Z'"
};
toFilterExpression generates the following:
created_date < :created_date AND #created_date > :created_date
toExpressionAttributeValues generates the following:
{
':created_date': { S: '2025-05-05T05:05:05.000Z' } // Overwrites previous value for :status
}
Expected Behavior:
The toExpressionAttributeValues function should generate unique aliases for each occurrence of the same field in the filter expression, e.g.:
{
":created_date0": { S: "2020-01-01T00:00:00.000Z" },
":created_date1": { S: "2025-05-05T05:05:05.000Z" }
}
Actual Behavior:
The function generates a single alias for the field, resulting in conflicts and overwriting of values:
{
":status": { S: "inactive" } // Last value overwrites previous value
}
Proposed Fix:
Update the toExpressionAttributeValues and toFilterExpression function to dynamically generate unique aliases for each field in the filter expression.
1. Updated toExpressionAttributeValues
static toExpressionAttributeValues(findOptions) {
const values = {};
let aliasCounter = 0; // Counter for unique aliases
if ((0, DynamoObjectHelper_1.isNotEmpty)(findOptions.where)) {
const keys = Object.keys(findOptions.where);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (findOptions.where[key] instanceof typeorm_1.FindOperator) {
if (findOptions.where[key].type === 'and') {
findOptions.where[key].value.forEach((operator, i) => {
values[`:${(0, DynamoTextHelper_1.poundToUnderscore)(key)}${i}`] =
(0, util_dynamodb_1.marshall)(operator.value);
});
} else {
values[`:${(0, DynamoTextHelper_1.poundToUnderscore)(key)}`] =
(0, util_dynamodb_1.marshall)(findOptions.where[key].value);
}
} else {
values[`:${(0, DynamoTextHelper_1.poundToUnderscore)(key)}`] =
(0, util_dynamodb_1.marshall)(findOptions.where[key]);
}
}
}
if (findOptions.filter) {
const expressions = findOptions.filter.split(/ and | or /gi).map(expression => expression.trim());
expressions.forEach(expression => {
expression = containsToAttributeValues(expression, values);
if (!expression.toLowerCase().includes('contains(')) {
const parts = (0, property_parser_1.splitOperators)(expression);
if (parts.length === 2) {
const name = parts[0].trim();
const value = parts[1].trim();
// Generate unique alias
const uniqueAlias = `:${(0, DynamoTextHelper_1.poundToUnderscore)(name)}${aliasCounter++}`;
values[uniqueAlias] = (0, util_dynamodb_1.marshall)(removeLeadingAndTrailingQuotes(value));
} else {
throw Error(`Failed to convert filter to ExpressionAttributeValues: ${findOptions.filter}`);
}
}
});
}
return common_utils_1.commonUtils.isNotEmpty(values) ? values : undefined;
}
2. Update toFilterExpression
Ensure the filter string replaces static placeholders with dynamically generated aliases.
static toFilterExpression(options) {
if (options.filter) {
const expressions = options.filter.split(/ and | or /gi); // Split by AND/OR
const connectors = options.filter.match(/ and | or /gi) || []; // Extract AND/OR operators
let aliasCounter = 0; // Counter for unique aliases
const processedExpressions = expressions.map(expression => {
let processedExpression = containsToFilterExpression(expression.trim());
if (!expression.toLowerCase().includes('contains(')) {
const parts = (0, property_parser_1.splitOperators)(expression.trim());
if (parts.length === 2) {
const name = parts[0].trim();
const value = parts[1].trim();
// Update FilterExpression for new unique alias
const uniqueAlias = `:${(0, DynamoTextHelper_1.poundToUnderscore)(name)}${aliasCounter++}`;
processedExpression = processedExpression.replace(name, `#${(0, DynamoTextHelper_1.poundToUnderscore)(name)}`);
processedExpression = processedExpression.replace(value, uniqueAlias);
processedExpression = processedExpression.replace(/['"]/g, ''); // Remove quotes
} else {
throw Error(`Failed to convert filter to FilterExpression: ${options.filter}`);
}
}
return processedExpression;
});
// Combine processed expressions with connectors
let finalFilterExpression = processedExpressions[0];
for (let i = 1; i < processedExpressions.length; i++) {
finalFilterExpression += ` ${connectors[i - 1].trim()} ${processedExpressions[i]}`;
}
return finalFilterExpression;
}
return undefined;
}
Thank you for reviewing this issue. It was identified and drafted with the assistance of ChatGPT. If you have any further questions or need clarification, feel free to reach out!
The
toExpressionAttributeValuesfunction in theFindOptionsclass does not correctly handle scenarios where the same field is used multiple times in the filter expression.This results in a single alias being generated for multiple occurrences of the same field, causing validation errors when constructing the DynamoDB query.
For example
Given the following filter expression:
toFilterExpressiongenerates the following:toExpressionAttributeValuesgenerates the following:Expected Behavior:
The
toExpressionAttributeValuesfunction should generate unique aliases for each occurrence of the same field in the filter expression, e.g.:Actual Behavior:
The function generates a single alias for the field, resulting in conflicts and overwriting of values:
Proposed Fix:
Update the
toExpressionAttributeValuesandtoFilterExpressionfunction to dynamically generate unique aliases for each field in the filter expression.1. Updated
toExpressionAttributeValues2. Update
toFilterExpressionEnsure the filter string replaces static placeholders with dynamically generated aliases.
Thank you for reviewing this issue. It was identified and drafted with the assistance of ChatGPT. If you have any further questions or need clarification, feel free to reach out!