-
Notifications
You must be signed in to change notification settings - Fork 22
Improves SQL parameter handling and query safety #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -407,86 +407,101 @@ class Sql { | |
| * @returns {String} - updated sql query | ||
| */ | ||
| addParameters({ query, request, parameters, forWhere = false }) { | ||
| if (!parameters) { | ||
| return query; | ||
| } | ||
| if (!parameters) return query; | ||
|
|
||
| const { buildParameterName, dataTypes, forceCaseInsensitive } = this; | ||
| const paramNames = Object.keys(parameters); | ||
| const whereClauses = []; | ||
| for (let index = 0, len = paramNames.length; index < len; index++) { | ||
| let paramName = paramNames[index]; | ||
| const props = parameters[paramName]; | ||
| if (props === undefined || (typeof props === 'number' && isNaN(props))) { | ||
| continue; | ||
| } | ||
| let operator = "="; | ||
| const paramNames = Object.keys(parameters); | ||
|
|
||
| for (let i = 0; i < paramNames.length; i++) { | ||
| const originalName = paramNames[i]; | ||
| let paramName = originalName; | ||
| let props = parameters[paramName]; | ||
|
|
||
| if (props === undefined || (typeof props === 'number' && isNaN(props))) continue; | ||
|
|
||
| let operator = '='; | ||
| let value = props; | ||
| let ignoreNull = true; | ||
| let fieldName = paramName; | ||
| let sqlType; | ||
| if (props !== undefined && props !== null && typeof props === 'object') { | ||
|
|
||
| // Extract object-style properties | ||
| if (props && typeof props === 'object' && props !== null) { | ||
| if (props.statement) { | ||
| whereClauses.push(props.statement); | ||
| continue; | ||
| } | ||
|
|
||
| operator = props.operator || operator; | ||
| value = props.value; | ||
| ignoreNull = props.ignoreNull !== false; | ||
| fieldName = props.fieldName || fieldName; | ||
| sqlType = props.sqlType; | ||
| } | ||
|
|
||
| if ((!isNullNotNullOperators.includes(operator)) && (value === undefined || (value === null && ignoreNull))) { | ||
| continue; | ||
| } | ||
| // Skip null/undefined values for normal operators | ||
| const isNullOp = isNullNotNullOperators.includes(operator); | ||
| if (!isNullOp && (value === undefined || (value === null && ignoreNull))) continue; | ||
|
|
||
| // Case-insensitive handling | ||
| if (forceCaseInsensitive) { | ||
| if (typeof value === 'string' || sqlType === dataTypes.string) { | ||
| value = value.toUpperCase(); | ||
| fieldName = `UPPER(${fieldName})`; | ||
| } | ||
| if (Array.isArray(value)) { | ||
| value = value.map(val => typeof val === 'string' ? val.toUpperCase() : val); | ||
| // Only apply UPPER() to fieldName if the array contains at least one string | ||
| const hasString = value.some(val => typeof val === 'string'); | ||
| if(hasString) { | ||
| fieldName = `UPPER(${fieldName})`; | ||
| } | ||
| } else if (Array.isArray(value)) { | ||
| const hasString = value.some(v => typeof v === 'string'); | ||
| if (hasString) fieldName = `UPPER(${fieldName})`; | ||
| value = value.map(v => typeof v === 'string' ? v.toUpperCase() : v); | ||
| } | ||
| } | ||
|
|
||
| if (paramName.indexOf('.') > -1) { | ||
| const parts = paramName.split('.'); | ||
| paramName = parts[parts.length - 1]; | ||
| } | ||
| let statement = `${fieldName} ${operator}${!isNullNotNullOperators.includes(operator) ? ` ${buildParameterName(paramName)}` : ''}`; | ||
| const isBetweenOperator = operator.toLowerCase() === 'between' || operator.toLowerCase() === 'not between'; | ||
| // Shorten param name if it's nested | ||
| const shortName = paramName.includes('.') ? paramName.split('.').pop() : paramName; | ||
|
|
||
| if (Array.isArray(value) || ["in", "not in"].includes(operator.toLowerCase())) { | ||
| const inResult = isBetweenOperator ? this.between({ request, fieldName, paramName, values: value, operator, sqlType: sqlType || dataTypes.integer }) : this.in({ request, fieldName, paramName, values: value, operator, sqlType: sqlType || dataTypes.integer }); | ||
| if (inResult.paramNames.length === 0) { | ||
| continue; | ||
| } | ||
| const isBetween = /between/i.test(operator); | ||
| const isInOperator = ['in', 'not in'].includes(operator.toLowerCase()); | ||
|
|
||
| let statement = `${fieldName} ${operator}${!isNullOp ? ` ${buildParameterName(shortName)}` : ''}`; | ||
|
|
||
| // Handle IN / BETWEEN operators | ||
| if (Array.isArray(value) || isInOperator) { | ||
| const fn = isBetween ? this.between : this.in; | ||
| const inResult = fn.call(this, { | ||
| request, | ||
| fieldName, | ||
| paramName: shortName, | ||
| values: value, | ||
| operator, | ||
| sqlType: sqlType || dataTypes.integer | ||
| }); | ||
|
|
||
| if (!inResult || !inResult.paramNames?.length) continue; | ||
| statement = inResult.statement; | ||
| if (query && !isBetweenOperator) { | ||
| // ability to replace @{FieldName} if we have embedded parameter in inner query | ||
| query = query.replaceAll(`${this.parameterPrefix}{${paramName}}`, inResult.paramNames.join(', ')); | ||
|
|
||
| // Prevent infinite replacements | ||
| const placeholder = `${this.parameterPrefix}{${shortName}}`; | ||
| const replacement = inResult.paramNames.join(', '); | ||
|
|
||
| if (typeof query === 'string' && !replacement.includes(placeholder) && !isBetween) { | ||
|
||
| query = query.replaceAll(placeholder, replacement); | ||
| } | ||
| } else { | ||
| if (sqlType !== undefined && sqlType !== null) { | ||
| request.input(paramName, sqlType, value); | ||
| // Add SQL parameter binding safely | ||
| if (sqlType != null) { | ||
| request.input(shortName, sqlType, value); | ||
| } else { | ||
| request.input(paramName, value); | ||
| request.input(shortName, value); | ||
| } | ||
| } | ||
|
|
||
| if (forWhere) { | ||
| whereClauses.push(statement); | ||
| } | ||
| if (forWhere) whereClauses.push(statement); | ||
| } | ||
| if (whereClauses.length > 0) { | ||
| query += " WHERE " + whereClauses.join(' AND '); | ||
|
|
||
| if (forWhere && whereClauses.length > 0) { | ||
| query += ' WHERE ' + whereClauses.join(' AND '); | ||
| } | ||
|
|
||
| return query; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable 'props' is of type date, object or regular expression, but it is compared to an expression of type null.