UNPKG

rawsql-ts

Version:

High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

802 lines 44.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqlParamInjector = void 0; const SelectQuery_1 = require("../models/SelectQuery"); const BinarySelectQuery_1 = require("../models/BinarySelectQuery"); const SelectableColumnCollector_1 = require("./SelectableColumnCollector"); const ValueComponent_1 = require("../models/ValueComponent"); const UpstreamSelectQueryFinder_1 = require("./UpstreamSelectQueryFinder"); const SelectQueryParser_1 = require("../parsers/SelectQueryParser"); const Clause_1 = require("../models/Clause"); /** * SqlParamInjector injects state parameters into a SelectQuery model, * creating WHERE conditions and setting parameter values. */ class SqlParamInjector { constructor(optionsOrResolver, options) { // Type-check to decide which argument was provided if (typeof optionsOrResolver === 'function') { this.tableColumnResolver = optionsOrResolver; this.options = options || {}; } else { this.tableColumnResolver = undefined; this.options = optionsOrResolver || {}; } } /** * Injects parameters as WHERE conditions into the given query model. * @param query The SelectQuery to modify * @param state A record of parameter names and values * @returns The modified SelectQuery * @throws Error when all parameters are undefined and allowAllUndefined is not set to true */ inject(query, state) { // Convert string query to SimpleSelectQuery using SelectQueryParser if needed if (typeof query === 'string') { query = SelectQueryParser_1.SelectQueryParser.parse(query); } // Pass tableColumnResolver to finder and collector const finder = new UpstreamSelectQueryFinder_1.UpstreamSelectQueryFinder(this.tableColumnResolver, this.options); const collector = new SelectableColumnCollector_1.SelectableColumnCollector(this.tableColumnResolver, false, // includeWildCard SelectableColumnCollector_1.DuplicateDetectionMode.FullName, // Use FullName to preserve JOIN table columns (u.id vs p.id) { upstream: true } // Enable upstream collection for qualified name resolution ); // Normalization is handled locally below. const normalize = (s) => this.options.ignoreCaseAndUnderscore ? s.toLowerCase().replace(/_/g, '') : s; const allowedOps = ['min', 'max', 'like', 'ilike', 'in', 'any', '=', '<', '>', '!=', '<>', '<=', '>=', 'or', 'and', 'column']; // Check if all parameters are undefined const stateValues = Object.values(state); const hasParameters = stateValues.length > 0; const allUndefined = hasParameters && stateValues.every(value => value === undefined); if (allUndefined && !this.options.allowAllUndefined) { throw new Error('All parameters are undefined. This would result in fetching all records. Use allowAllUndefined: true option to explicitly allow this behavior.'); } // Separate qualified and unqualified parameters for hybrid processing const qualifiedParams = []; const unqualifiedParams = []; for (const [name, stateValue] of Object.entries(state)) { // skip undefined values if (stateValue === undefined) continue; if (this.isQualifiedColumnName(name)) { qualifiedParams.push([name, stateValue]); } else { unqualifiedParams.push([name, stateValue]); } } // Process qualified parameters first for (const [name, stateValue] of qualifiedParams) { this.processStateParameter(name, stateValue, query, finder, collector, normalize, allowedOps, injectOrConditions, injectAndConditions, injectSimpleCondition, injectComplexConditions, validateOperators); } // Then process unqualified parameters, but only for columns that haven't been processed yet const processedQualifiedColumns = new Set(); for (const [qualifiedName, _] of qualifiedParams) { const parsed = this.parseQualifiedColumnName(qualifiedName); if (parsed) { // Track which table.column combinations have been processed processedQualifiedColumns.add(`${parsed.table.toLowerCase()}.${parsed.column.toLowerCase()}`); } } for (const [name, stateValue] of unqualifiedParams) { this.processUnqualifiedParameter(name, stateValue, query, finder, collector, normalize, allowedOps, injectOrConditions, injectAndConditions, injectSimpleCondition, injectComplexConditions, validateOperators, processedQualifiedColumns); } function injectAndConditions(q, baseName, andConditions, normalize, availableColumns) { // For AND conditions, we process each condition and add them all with AND logic for (let i = 0; i < andConditions.length; i++) { const andCondition = andConditions[i]; const columnName = andCondition.column || baseName; // Find the target column const entry = availableColumns.find(item => normalize(item.name) === normalize(columnName)); if (!entry) { throw new Error(`Column '${columnName}' not found in query for AND condition`); } const columnRef = entry.value; // Process each operator in the AND condition if ('=' in andCondition && andCondition['='] !== undefined) { const paramName = `${baseName}_and_${i}_eq`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition['=']); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "=", paramExpr)); } if ('min' in andCondition && andCondition.min !== undefined) { const paramName = `${baseName}_and_${i}_min`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition.min); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, ">=", paramExpr)); } if ('max' in andCondition && andCondition.max !== undefined) { const paramName = `${baseName}_and_${i}_max`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition.max); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "<=", paramExpr)); } if ('like' in andCondition && andCondition.like !== undefined) { const paramName = `${baseName}_and_${i}_like`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition.like); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "like", paramExpr)); } if ('ilike' in andCondition && andCondition.ilike !== undefined) { const paramName = `${baseName}_and_${i}_ilike`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition.ilike); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "ilike", paramExpr)); } if ('in' in andCondition && andCondition.in !== undefined) { const arr = andCondition.in; const prms = arr.map((v, j) => new ValueComponent_1.ParameterExpression(`${baseName}_and_${i}_in_${j}`, v)); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "in", new ValueComponent_1.ParenExpression(new ValueComponent_1.ValueList(prms)))); } if ('any' in andCondition && andCondition.any !== undefined) { const paramName = `${baseName}_and_${i}_any`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition.any); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "=", new ValueComponent_1.FunctionCall(null, "any", paramExpr, null))); } if ('<' in andCondition && andCondition['<'] !== undefined) { const paramName = `${baseName}_and_${i}_lt`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition['<']); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "<", paramExpr)); } if ('>' in andCondition && andCondition['>'] !== undefined) { const paramName = `${baseName}_and_${i}_gt`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition['>']); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, ">", paramExpr)); } if ('!=' in andCondition && andCondition['!='] !== undefined) { const paramName = `${baseName}_and_${i}_neq`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition['!=']); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "!=", paramExpr)); } if ('<>' in andCondition && andCondition['<>'] !== undefined) { const paramName = `${baseName}_and_${i}_ne`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition['<>']); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "<>", paramExpr)); } if ('<=' in andCondition && andCondition['<='] !== undefined) { const paramName = `${baseName}_and_${i}_le`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition['<=']); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "<=", paramExpr)); } if ('>=' in andCondition && andCondition['>='] !== undefined) { const paramName = `${baseName}_and_${i}_ge`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, andCondition['>=']); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, ">=", paramExpr)); } } } function injectOrConditions(q, baseName, orConditions, normalize, availableColumns) { const orExpressions = []; for (let i = 0; i < orConditions.length; i++) { const orCondition = orConditions[i]; const columnName = orCondition.column || baseName; // Find the target column const entry = availableColumns.find(item => normalize(item.name) === normalize(columnName)); if (!entry) { throw new Error(`Column '${columnName}' not found in query for OR condition`); } const columnRef = entry.value; // Create conditions for this OR branch const branchConditions = []; // Process each operator in the OR condition if ('=' in orCondition && orCondition['='] !== undefined) { const paramName = `${baseName}_or_${i}_eq`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition['=']); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "=", paramExpr)); } if ('min' in orCondition && orCondition.min !== undefined) { const paramName = `${baseName}_or_${i}_min`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition.min); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, ">=", paramExpr)); } if ('max' in orCondition && orCondition.max !== undefined) { const paramName = `${baseName}_or_${i}_max`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition.max); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<=", paramExpr)); } if ('like' in orCondition && orCondition.like !== undefined) { const paramName = `${baseName}_or_${i}_like`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition.like); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "like", paramExpr)); } if ('ilike' in orCondition && orCondition.ilike !== undefined) { const paramName = `${baseName}_or_${i}_ilike`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition.ilike); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "ilike", paramExpr)); } if ('in' in orCondition && orCondition.in !== undefined) { const arr = orCondition.in; const prms = arr.map((v, j) => new ValueComponent_1.ParameterExpression(`${baseName}_or_${i}_in_${j}`, v)); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "in", new ValueComponent_1.ParenExpression(new ValueComponent_1.ValueList(prms)))); } if ('any' in orCondition && orCondition.any !== undefined) { const paramName = `${baseName}_or_${i}_any`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition.any); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "=", new ValueComponent_1.FunctionCall(null, "any", paramExpr, null))); } if ('<' in orCondition && orCondition['<'] !== undefined) { const paramName = `${baseName}_or_${i}_lt`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition['<']); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<", paramExpr)); } if ('>' in orCondition && orCondition['>'] !== undefined) { const paramName = `${baseName}_or_${i}_gt`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition['>']); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, ">", paramExpr)); } if ('!=' in orCondition && orCondition['!='] !== undefined) { const paramName = `${baseName}_or_${i}_neq`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition['!=']); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "!=", paramExpr)); } if ('<>' in orCondition && orCondition['<>'] !== undefined) { const paramName = `${baseName}_or_${i}_ne`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition['<>']); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<>", paramExpr)); } if ('<=' in orCondition && orCondition['<='] !== undefined) { const paramName = `${baseName}_or_${i}_le`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition['<=']); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<=", paramExpr)); } if ('>=' in orCondition && orCondition['>='] !== undefined) { const paramName = `${baseName}_or_${i}_ge`; const paramExpr = new ValueComponent_1.ParameterExpression(paramName, orCondition['>=']); branchConditions.push(new ValueComponent_1.BinaryExpression(columnRef, ">=", paramExpr)); } // Combine branch conditions with AND if there are multiple if (branchConditions.length > 0) { let branchExpr = branchConditions[0]; for (let j = 1; j < branchConditions.length; j++) { branchExpr = new ValueComponent_1.BinaryExpression(branchExpr, "and", branchConditions[j]); } // Wrap in parentheses if multiple conditions within the OR branch if (branchConditions.length > 1) { orExpressions.push(new ValueComponent_1.ParenExpression(branchExpr)); } else { orExpressions.push(branchExpr); } } } // Combine OR expressions if (orExpressions.length > 0) { let finalOrExpr = orExpressions[0]; for (let i = 1; i < orExpressions.length; i++) { finalOrExpr = new ValueComponent_1.BinaryExpression(finalOrExpr, "or", orExpressions[i]); } // Wrap in parentheses and append to WHERE clause q.appendWhere(new ValueComponent_1.ParenExpression(finalOrExpr)); } } function validateOperators(stateValue, allowedOps, name) { Object.keys(stateValue).forEach(op => { if (!allowedOps.includes(op)) { throw new Error(`Unsupported operator '${op}' for state key '${name}'`); } }); } function injectSimpleCondition(q, columnRef, name, stateValue) { const paramExpr = new ValueComponent_1.ParameterExpression(name, stateValue); q.appendWhere(new ValueComponent_1.BinaryExpression(columnRef, "=", paramExpr)); } function injectComplexConditions(q, columnRef, name, stateValue) { const conditions = []; if ('=' in stateValue) { const paramEq = new ValueComponent_1.ParameterExpression(name, stateValue['=']); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "=", paramEq)); } if ('min' in stateValue) { const paramMin = new ValueComponent_1.ParameterExpression(name + "_min", stateValue.min); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, ">=", paramMin)); } if ('max' in stateValue) { const paramMax = new ValueComponent_1.ParameterExpression(name + "_max", stateValue.max); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<=", paramMax)); } if ('like' in stateValue) { const paramLike = new ValueComponent_1.ParameterExpression(name + "_like", stateValue.like); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "like", paramLike)); } if ('ilike' in stateValue) { const paramIlike = new ValueComponent_1.ParameterExpression(name + "_ilike", stateValue.ilike); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "ilike", paramIlike)); } if ('in' in stateValue) { const arr = stateValue['in']; const prms = arr.map((v, i) => new ValueComponent_1.ParameterExpression(`${name}_in_${i}`, v)); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "in", new ValueComponent_1.ParenExpression(new ValueComponent_1.ValueList(prms)))); } if ('any' in stateValue) { const paramAny = new ValueComponent_1.ParameterExpression(name + "_any", stateValue.any); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "=", new ValueComponent_1.FunctionCall(null, "any", paramAny, null))); } if ('<' in stateValue) { const paramLT = new ValueComponent_1.ParameterExpression(name + "_lt", stateValue['<']); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<", paramLT)); } if ('>' in stateValue) { const paramGT = new ValueComponent_1.ParameterExpression(name + "_gt", stateValue['>']); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, ">", paramGT)); } if ('!=' in stateValue) { const paramNEQ = new ValueComponent_1.ParameterExpression(name + "_neq", stateValue['!=']); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "!=", paramNEQ)); } if ('<>' in stateValue) { const paramNE = new ValueComponent_1.ParameterExpression(name + "_ne", stateValue['<>']); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<>", paramNE)); } if ('<=' in stateValue) { const paramLE = new ValueComponent_1.ParameterExpression(name + "_le", stateValue['<=']); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, "<=", paramLE)); } if ('>=' in stateValue) { const paramGE = new ValueComponent_1.ParameterExpression(name + "_ge", stateValue['>=']); conditions.push(new ValueComponent_1.BinaryExpression(columnRef, ">=", paramGE)); } // Combine conditions with AND and wrap in parentheses if multiple conditions for clarity if (conditions.length === 1) { // Single condition - no parentheses needed q.appendWhere(conditions[0]); } else if (conditions.length > 1) { // Multiple conditions - combine with AND and wrap in parentheses for clarity let combinedExpr = conditions[0]; for (let i = 1; i < conditions.length; i++) { combinedExpr = new ValueComponent_1.BinaryExpression(combinedExpr, "and", conditions[i]); } q.appendWhere(new ValueComponent_1.ParenExpression(combinedExpr)); } } return query; } /** * Type guard for OR conditions */ isOrCondition(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && 'or' in value; } /** * Type guard for AND conditions */ isAndCondition(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && 'and' in value; } /** * Type guard for explicit column mapping without OR */ isExplicitColumnMapping(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && 'column' in value && !('or' in value); } /** * Type guard for objects that need operator validation */ isValidatableObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; } /** * Parses a qualified column name (table.column) into its components * @param qualifiedName The qualified name (e.g., 'users.name' or 'name') * @returns Object with table and column parts, or null if invalid */ parseQualifiedColumnName(qualifiedName) { const parts = qualifiedName.split('.'); if (parts.length === 2 && parts[0].trim() && parts[1].trim()) { return { table: parts[0].trim(), column: parts[1].trim() }; } return null; } /** * Checks if a column name is qualified (contains a dot) */ isQualifiedColumnName(name) { return name.includes('.') && this.parseQualifiedColumnName(name) !== null; } /** * Sanitizes parameter names by replacing dots with underscores * This ensures qualified names like 'users.name' become 'users_name' as parameter names */ sanitizeParameterName(name) { return name.replace(/\./g, '_'); } /** * Type guard for column mapping presence */ hasColumnMapping(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && 'column' in value; } /** * Type guard for simple values (non-object conditions) */ isSimpleValue(value) { return value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Date; } /** * Processes a single state parameter */ processStateParameter(name, stateValue, query, finder, collector, normalize, allowedOps, injectOrConditions, injectAndConditions, injectSimpleCondition, injectComplexConditions, validateOperators) { // Handle OR conditions specially - they don't need the main column to exist if (this.isOrCondition(stateValue)) { const orConditions = stateValue.or; if (orConditions && orConditions.length > 0) { const targetQuery = this.findTargetQueryForLogicalCondition(finder, query, name, orConditions); const allColumns = this.getAllAvailableColumns(targetQuery, collector); injectOrConditions(targetQuery, name, orConditions, normalize, allColumns); return; } } // Handle AND conditions specially - they don't need the main column to exist if (this.isAndCondition(stateValue)) { const andConditions = stateValue.and; if (andConditions && andConditions.length > 0) { const targetQuery = this.findTargetQueryForLogicalCondition(finder, query, name, andConditions); const allColumns = this.getAllAvailableColumns(targetQuery, collector); injectAndConditions(targetQuery, name, andConditions, normalize, allColumns); return; } } // Handle explicit column mapping without OR if (this.isExplicitColumnMapping(stateValue)) { const explicitColumnName = stateValue.column; if (explicitColumnName) { const queries = finder.find(query, explicitColumnName); if (queries.length === 0) { throw new Error(`Explicit column '${explicitColumnName}' not found in query`); } for (const q of queries) { const allColumns = this.getAllAvailableColumns(q, collector); const entry = allColumns.find(item => normalize(item.name) === normalize(explicitColumnName)); if (!entry) { throw new Error(`Explicit column '${explicitColumnName}' not found in query`); } // if object, validate its keys if (this.isValidatableObject(stateValue)) { validateOperators(stateValue, allowedOps, name); } injectComplexConditions(q, entry.value, name, stateValue); } return; } } // Handle regular column conditions this.processRegularColumnCondition(name, stateValue, query, finder, collector, normalize, allowedOps, injectSimpleCondition, injectComplexConditions, validateOperators); } /** * Processes unqualified parameters, respecting qualified parameter overrides */ processUnqualifiedParameter(name, stateValue, query, finder, collector, normalize, allowedOps, injectOrConditions, injectAndConditions, injectSimpleCondition, injectComplexConditions, validateOperators, processedQualifiedColumns) { // Handle OR conditions specially - they don't need the main column to exist if (this.isOrCondition(stateValue)) { const orConditions = stateValue.or; if (orConditions && orConditions.length > 0) { const targetQuery = this.findTargetQueryForLogicalCondition(finder, query, name, orConditions); const allColumns = this.getAllAvailableColumns(targetQuery, collector); injectOrConditions(targetQuery, name, orConditions, normalize, allColumns); return; } } // Handle AND conditions specially - they don't need the main column to exist if (this.isAndCondition(stateValue)) { const andConditions = stateValue.and; if (andConditions && andConditions.length > 0) { const targetQuery = this.findTargetQueryForLogicalCondition(finder, query, name, andConditions); const allColumns = this.getAllAvailableColumns(targetQuery, collector); injectAndConditions(targetQuery, name, andConditions, normalize, allColumns); return; } } // Handle explicit column mapping without OR if (this.isExplicitColumnMapping(stateValue)) { const explicitColumnName = stateValue.column; if (explicitColumnName) { const queries = finder.find(query, explicitColumnName); if (queries.length === 0) { throw new Error(`Explicit column '${explicitColumnName}' not found in query`); } for (const q of queries) { const allColumns = this.getAllAvailableColumns(q, collector); const entry = allColumns.find(item => normalize(item.name) === normalize(explicitColumnName)); if (!entry) { throw new Error(`Explicit column '${explicitColumnName}' not found in query`); } // if object, validate its keys if (this.isValidatableObject(stateValue)) { validateOperators(stateValue, allowedOps, name); } injectComplexConditions(q, entry.value, name, stateValue); } return; } } // Find all queries that contain this unqualified column name const queries = finder.find(query, name); if (queries.length === 0) { // Ignore non-existent columns if option is enabled if (this.options.ignoreNonExistentColumns) { return; } throw new Error(`Column '${name}' not found in query`); } for (const q of queries) { const allColumns = this.getAllAvailableColumns(q, collector); const tableMapping = this.buildTableMapping(q); // Find all columns with this name const matchingColumns = allColumns.filter(item => normalize(item.name) === normalize(name)); for (const entry of matchingColumns) { // Check if this column has already been processed by a qualified parameter let skipColumn = false; // Get the table name for this column if (entry.value && typeof entry.value.getNamespace === 'function') { const namespace = entry.value.getNamespace(); if (namespace) { const realTableName = tableMapping.aliasToRealTable.get(namespace.toLowerCase()); if (realTableName) { const qualifiedKey = `${realTableName.toLowerCase()}.${name.toLowerCase()}`; if (processedQualifiedColumns.has(qualifiedKey)) { skipColumn = true; } } } } // Skip if this column was already processed by a qualified parameter if (skipColumn) { continue; } const columnRef = entry.value; // if object, validate its keys if (this.isValidatableObject(stateValue)) { validateOperators(stateValue, allowedOps, name); } // Handle explicit column mapping let targetColumn = columnRef; if (this.hasColumnMapping(stateValue)) { const explicitColumnName = stateValue.column; if (explicitColumnName) { const explicitEntry = allColumns.find(item => normalize(item.name) === normalize(explicitColumnName)); if (explicitEntry) { targetColumn = explicitEntry.value; } } } // Use the original parameter name for unqualified columns if (this.isSimpleValue(stateValue)) { injectSimpleCondition(q, targetColumn, name, stateValue); } else { injectComplexConditions(q, targetColumn, name, stateValue); } } } } /** * Processes regular column conditions (non-logical, non-explicit) */ processRegularColumnCondition(name, stateValue, query, finder, collector, normalize, allowedOps, injectSimpleCondition, injectComplexConditions, validateOperators) { // Check if this is a qualified column name (table.column) let searchColumnName = name; let targetTableName = undefined; if (this.isQualifiedColumnName(name)) { const parsed = this.parseQualifiedColumnName(name); if (parsed) { searchColumnName = parsed.column; targetTableName = parsed.table; } } const queries = finder.find(query, searchColumnName); if (queries.length === 0) { // Ignore non-existent columns if option is enabled if (this.options.ignoreNonExistentColumns) { return; } throw new Error(`Column '${searchColumnName}' not found in query`); } for (const q of queries) { const allColumns = this.getAllAvailableColumns(q, collector); // For qualified names, find the column with the specific table name let entry; if (targetTableName) { // Build table mapping for enhanced qualified name resolution const tableMapping = this.buildTableMapping(q); // Look for column with specific real table name only (no alias matching) entry = allColumns.find(item => { const normalizedColumnName = normalize(item.name) === normalize(searchColumnName); if (!normalizedColumnName) return false; // Check if the column has the target table name if (item.value && typeof item.value.getNamespace === 'function') { const namespace = item.value.getNamespace(); if (namespace) { const normalizedNamespace = normalize(namespace); const normalizedTargetTable = normalize(targetTableName); // Only check if targetTableName is a real table name that maps to this alias const realTableName = tableMapping.aliasToRealTable.get(normalizedNamespace); if (realTableName && normalize(realTableName) === normalizedTargetTable) { return true; } } } return false; }); if (!entry) { // If qualified name not found, try to be more lenient for backward compatibility if (this.options.ignoreNonExistentColumns) { continue; } // Check if the table exists at all const tableMapping = this.buildTableMapping(q); const hasRealTable = Array.from(tableMapping.realTableToAlias.keys()).some(realTable => normalize(realTable) === normalize(targetTableName)); const hasAliasTable = Array.from(tableMapping.aliasToRealTable.keys()).some(alias => normalize(alias) === normalize(targetTableName)); if (!hasRealTable && !hasAliasTable) { // Table doesn't exist at all throw new Error(`Column '${name}' (qualified as ${name}) not found in query`); } else if (hasAliasTable && !hasRealTable) { // It's an alias, not a real table name throw new Error(`Column '${name}' not found. Only real table names are allowed in qualified column references (e.g., 'users.name'), not aliases (e.g., 'u.name').`); } else { // Real table exists but column doesn't throw new Error(`Column '${name}' (qualified as ${name}) not found in query`); } } } else { // Unqualified name - find any matching column entry = allColumns.find(item => normalize(item.name) === normalize(searchColumnName)); if (!entry) { throw new Error(`Column '${searchColumnName}' not found in query`); } } const columnRef = entry.value; // if object, validate its keys if (this.isValidatableObject(stateValue)) { validateOperators(stateValue, allowedOps, name); } // Handle explicit column mapping let targetColumn = columnRef; if (this.hasColumnMapping(stateValue)) { const explicitColumnName = stateValue.column; if (explicitColumnName) { const explicitEntry = allColumns.find(item => normalize(item.name) === normalize(explicitColumnName)); if (explicitEntry) { targetColumn = explicitEntry.value; } } } // Use sanitized parameter name (replace dots with underscores) // This ensures qualified names like 'users.name' become 'users_name' as parameter names const parameterName = this.sanitizeParameterName(name); if (this.isSimpleValue(stateValue)) { injectSimpleCondition(q, targetColumn, parameterName, stateValue); } else { injectComplexConditions(q, targetColumn, parameterName, stateValue); } } } /** * Finds target query for logical conditions (AND/OR) */ findTargetQueryForLogicalCondition(finder, query, baseName, conditions) { const referencedColumns = conditions .map(cond => cond.column || baseName) .filter((col, index, arr) => arr.indexOf(col) === index); // unique columns for (const colName of referencedColumns) { const queries = finder.find(query, colName); if (queries.length > 0) { return queries[0]; } } const conditionType = conditions === conditions.or ? 'OR' : 'AND'; throw new Error(`None of the ${conditionType} condition columns [${referencedColumns.join(', ')}] found in query`); } /** * Collects all available columns from a query including CTE columns */ getAllAvailableColumns(query, collector) { const columns = collector.collect(query); const cteColumns = this.collectCTEColumns(query); return [...columns, ...cteColumns]; } /** * Collects column names and references from CTE definitions */ collectCTEColumns(query) { const cteColumns = []; if (query.withClause) { for (const cte of query.withClause.tables) { try { const columns = this.collectColumnsFromSelectQuery(cte.query); cteColumns.push(...columns); } catch (error) { // Log error but continue processing other CTEs console.warn(`Failed to collect columns from CTE '${cte.getSourceAliasName()}':`, error); } } } return cteColumns; } /** * Recursively collects columns from any SelectQuery type */ collectColumnsFromSelectQuery(query) { if (query instanceof SelectQuery_1.SimpleSelectQuery) { const collector = new SelectableColumnCollector_1.SelectableColumnCollector(this.tableColumnResolver, false, // includeWildCard SelectableColumnCollector_1.DuplicateDetectionMode.FullName, // Use FullName to preserve JOIN table columns { upstream: true } // Enable upstream collection for qualified name resolution ); return collector.collect(query); } else if (query instanceof BinarySelectQuery_1.BinarySelectQuery) { // For UNION/INTERSECT/EXCEPT, columns from left side are representative // since both sides must have matching column structure return this.collectColumnsFromSelectQuery(query.left); } return []; } /** * Builds a mapping between table aliases and real table names for enhanced qualified name resolution */ buildTableMapping(query) { const aliasToRealTable = new Map(); const realTableToAlias = new Map(); try { // Process FROM clause if (query.fromClause) { this.processSourceForMapping(query.fromClause.source, aliasToRealTable, realTableToAlias); // Process JOIN clauses if (query.fromClause.joins) { for (const join of query.fromClause.joins) { this.processSourceForMapping(join.source, aliasToRealTable, realTableToAlias); } } } // Process CTE definitions if (query.withClause) { for (const cte of query.withClause.tables) { const cteAlias = cte.getSourceAliasName(); if (cteAlias) { // For CTEs, the "real table name" is the CTE name itself aliasToRealTable.set(cteAlias.toLowerCase(), cteAlias); realTableToAlias.set(cteAlias.toLowerCase(), cteAlias); } } } } catch (error) { // Log warning but continue with empty mapping for safety console.warn('Failed to build table mapping:', error); } return { aliasToRealTable, realTableToAlias }; } /** * Helper method to process a single SourceExpression for table mapping */ processSourceForMapping(source, aliasToRealTable, realTableToAlias) { var _a, _b; try { if (source.datasource instanceof Clause_1.TableSource) { const realTableName = source.datasource.getSourceName(); const aliasName = ((_b = (_a = source.aliasExpression) === null || _a === void 0 ? void 0 : _a.table) === null || _b === void 0 ? void 0 : _b.name) || realTableName; if (realTableName && aliasName) { // Store mappings in lowercase for case-insensitive lookup aliasToRealTable.set(aliasName.toLowerCase(), realTableName); realTableToAlias.set(realTableName.toLowerCase(), aliasName); // Also store direct mapping if alias equals real table name if (aliasName === realTableName) { aliasToRealTable.set(realTableName.toLowerCase(), realTableName); } } } } catch (error) { // Log warning but continue processing other sources console.warn('Failed to process source for mapping:', error); } } } exports.SqlParamInjector = SqlParamInjector; //# sourceMappingURL=SqlParamInjector.js.map