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
JavaScript
"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