Skip to content

Bug: toExpressionAttributeValues Does Not Generate Unique Aliases for Duplicate Fields in Filter Expressions #13

@alkaline1024

Description

@alkaline1024

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions