| |
| |
| |
| |
| class FilterExpressionBuilder { |
| |
| |
| |
| |
| |
| constructor() { |
| this.names = {}; |
| this.values = {}; |
| this.nameCount = 0; |
| this.valueCount = 0; |
| } |
| |
| |
| generateName(fieldName) { |
| const key = `#n${++this.nameCount}`; |
| this.names[key] = fieldName; |
| return key; |
| } |
| |
| |
| generateValue(value) { |
| const key = `:v${++this.valueCount}`; |
| this.values[key] = value; |
| return key; |
| } |
| |
| |
| convertValue(value, model, fieldName) { |
| const field = model.fields[fieldName]; |
| return field.toDy(value); |
| } |
| |
| |
| buildComparison(fieldName, operator, value, model) { |
| const nameKey = this.generateName(fieldName); |
| const convertedValue = this.convertValue(value, model, fieldName); |
| |
| switch (operator) { |
| case "$eq": |
| const valueKey = this.generateValue(convertedValue); |
| return `${nameKey} = ${valueKey}`; |
| case "$ne": |
| return `${nameKey} <> ${this.generateValue(convertedValue)}`; |
| case "$gt": |
| return `${nameKey} > ${this.generateValue(convertedValue)}`; |
| case "$gte": |
| return `${nameKey} >= ${this.generateValue(convertedValue)}`; |
| case "$lt": |
| return `${nameKey} < ${this.generateValue(convertedValue)}`; |
| case "$lte": |
| return `${nameKey} <= ${this.generateValue(convertedValue)}`; |
| case "$contains": |
| return `contains(${nameKey}, ${this.generateValue(convertedValue)})`; |
| case "$beginsWith": |
| return `begins_with(${nameKey}, ${this.generateValue(convertedValue)})`; |
| case "$in": |
| if (!Array.isArray(convertedValue)) { |
| throw new Error("$in operator requires an array value"); |
| } |
| const valueKeys = convertedValue.map((v) => this.generateValue(v)); |
| return `${nameKey} IN (${valueKeys.join(", ")})`; |
| case "$exists": |
| if (typeof value !== "boolean") { |
| throw new Error("$exists operator requires a boolean value"); |
| } |
| return value |
| ? `attribute_exists(${nameKey})` |
| : `attribute_not_exists(${nameKey})`; |
| default: |
| throw new Error(`Unsupported operator: ${operator}`); |
| } |
| } |
| |
| |
| buildFieldExpression(fieldName, condition, model) { |
| if (condition === null) { |
| const nameKey = this.generateName(fieldName); |
| return `attribute_not_exists(${nameKey})`; |
| } |
| |
| if (typeof condition !== "object" || condition instanceof Date) { |
| return this.buildComparison(fieldName, "$eq", condition, model); |
| } |
| |
| const operators = Object.keys(condition); |
| if (operators.length === 0) { |
| throw new Error(`Empty condition object for field: ${fieldName}`); |
| } |
| |
| const expressions = operators.map((operator) => { |
| return this.buildComparison( |
| fieldName, |
| operator, |
| condition[operator], |
| model, |
| ); |
| }); |
| |
| return expressions.join(" AND "); |
| } |
| |
| |
| buildLogicalExpression(operator, conditions, model) { |
| if (!Array.isArray(conditions)) { |
| throw new Error(`${operator} requires an array of conditions`); |
| } |
| |
| const expressions = conditions.map((condition) => { |
| return this.buildFilterExpression(condition, model); |
| }); |
| |
| const joinOperator = operator === "$and" ? " AND " : " OR "; |
| return `(${expressions.join(joinOperator)})`; |
| } |
| |
| |
| buildFilterExpression(filter, model) { |
| if (!filter || Object.keys(filter).length === 0) { |
| return null; |
| } |
| |
| const expressions = []; |
| |
| for (const [key, value] of Object.entries(filter)) { |
| if (key === "$and" || key === "$or") { |
| expressions.push(this.buildLogicalExpression(key, value, model)); |
| } else if (key === "$not") { |
| const innerExpr = this.buildFilterExpression(value, model); |
| expressions.push(`NOT ${innerExpr}`); |
| } else { |
| expressions.push(this.buildFieldExpression(key, value, model)); |
| } |
| } |
| |
| return expressions.join(" AND "); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| build(filter, model) { |
| |
| this.validateFields(filter, model); |
| |
| const filterExpression = this.buildFilterExpression(filter, model); |
| |
| if (!filterExpression) { |
| return null; |
| } |
| |
| return { |
| FilterExpression: filterExpression, |
| ExpressionAttributeNames: this.names, |
| ExpressionAttributeValues: this.values, |
| }; |
| } |
| |
| |
| validateFields(filter, model) { |
| const validateObject = (obj) => { |
| for (const [key, value] of Object.entries(obj)) { |
| |
| if (["$and", "$or", "$not"].includes(key)) { |
| if (Array.isArray(value)) { |
| value.forEach(validateObject); |
| } else { |
| validateObject(value); |
| } |
| continue; |
| } |
| |
| |
| if (!model.fields[key]) { |
| throw new Error(`Unknown field in filter: ${key}`); |
| } |
| |
| |
| if ( |
| value && |
| typeof value === "object" && |
| !Array.isArray(value) && |
| !(value instanceof Date) |
| ) { |
| const operators = Object.keys(value); |
| operators.forEach((op) => { |
| if ( |
| ![ |
| "$eq", |
| "$ne", |
| "$gt", |
| "$gte", |
| "$lt", |
| "$lte", |
| "$in", |
| "$contains", |
| "$beginsWith", |
| "$exists", |
| ].includes(op) |
| ) { |
| throw new Error(`Invalid operator ${op} for field ${key}`); |
| } |
| }); |
| } |
| } |
| }; |
| |
| validateObject(filter); |
| } |
| } |
| |
| module.exports = { FilterExpressionBuilder }; |