Skip to main content
Glama
IBM
by IBM
parameterProcessor.ts32.6 kB
/** * @fileoverview Unified parameter processor for SQL parameter validation and binding * Combines validation, type conversion, and SQL parameter binding in a single streamlined interface * * @module src/utils/sql/parameterProcessor */ import { BindingValue } from "@ibm/mapepire-js"; import { logger } from "@/utils/internal/logger.js"; import { RequestContext, requestContextService, } from "@/utils/internal/requestContext.js"; import { ErrorHandler } from "@/utils/internal/errorHandler.js"; import { JsonRpcErrorCode, McpError } from "@/types-global/errors.js"; import { SqlToolParameter } from "../../schemas/index.js"; /** * Parameter processing mode */ export type ParameterMode = "named" | "positional" | "template" | "hybrid"; /** * Parameter processing options */ export interface ParameterProcessingOptions { /** Whether to include detailed logging */ detailedLogging?: boolean; /** Whether to validate parameter syntax */ validateSyntax?: boolean; /** Custom context for logging */ context?: RequestContext; /** Strict type validation */ strictTypeValidation?: boolean; } /** * Parameter processing result */ export interface ParameterProcessingResult { /** Processed SQL string with ? placeholders */ sql: string; /** Parameters in order for binding */ parameters: BindingValue[]; /** Parameter names that were found */ parameterNames: string[]; /** Parameters that were missing */ missingParameters: string[]; /** Detected parameter mode */ mode: ParameterMode; /** Processing statistics */ stats: { originalLength: number; processedLength: number; namedParametersFound: number; positionalParametersFound: number; parametersConverted: number; }; } /** * Parameter validation result */ export interface ValidationResult { /** Whether validation passed */ valid: boolean; /** Converted value (if valid) */ value?: BindingValue; /** Validation errors */ errors: string[]; /** Validation warnings */ warnings: string[]; } /** * Named parameter match */ interface NamedParameterMatch { fullMatch: string; paramName: string; position: number; } /** * Unified parameter processor for SQL parameter validation and binding * Handles validation, type conversion, and SQL parameter binding in one streamlined interface */ export class ParameterProcessor { /** * Process SQL statement with parameter validation and binding * @param sql - SQL statement with parameter placeholders * @param parameterValues - Parameter values by name * @param parameterDefinitions - YAML parameter definitions for validation * @param options - Processing options * @returns Processed SQL and parameter array with validation results */ static async process( sql: string, parameterValues: Record<string, unknown>, parameterDefinitions: SqlToolParameter[] = [], options: ParameterProcessingOptions = {}, ): Promise<ParameterProcessingResult> { const context = options.context || requestContextService.createRequestContext({ operation: "ProcessParameters", }); const { detailedLogging = false, validateSyntax = true, strictTypeValidation = true, } = options; return ErrorHandler.tryCatch( async () => { // Validate SQL syntax if requested if (validateSyntax) { this.validateSqlSyntax(sql); } // Step 1: Apply parameter validation and defaults const processedParameters = await this.validateAndProcessParameters( parameterValues, parameterDefinitions, context, ); // Step 2: Detect parameter mode and process SQL const mode = this.detectParameterMode(sql); if (mode === "template") { throw new McpError( JsonRpcErrorCode.ValidationError, "Template mode ({{param}}) is deprecated. Use named parameters (:param) or positional parameters (?) instead.", { sql: sql.substring(0, 100) + "..." }, ); } let result: ParameterProcessingResult; switch (mode) { case "named": result = await this.processNamedParameters( sql, processedParameters, context, strictTypeValidation, ); break; case "positional": result = await this.processPositionalParameters( sql, processedParameters, context, strictTypeValidation, ); break; case "hybrid": result = await this.processHybridParameters( sql, processedParameters, context, strictTypeValidation, ); break; default: // No parameters found result = { sql, parameters: [], parameterNames: [], missingParameters: [], mode: "positional", stats: { originalLength: sql.length, processedLength: sql.length, namedParametersFound: 0, positionalParametersFound: 0, parametersConverted: 0, }, }; } // Log processing details if (detailedLogging) { logger.debug( { ...context, ...result.stats, mode: result.mode, parameterNames: result.parameterNames, missingParameters: result.missingParameters.length > 0 ? result.missingParameters : undefined, }, "SQL parameters processed with detailed stats", ); } else { logger.debug( { ...context, mode: result.mode, parameterCount: result.parameters.length, parameterNames: result.parameterNames, }, "SQL parameters processed", ); } return result; }, { operation: "ProcessParameters", context, errorCode: JsonRpcErrorCode.ValidationError, }, ); } /** * Validate parameters and apply defaults according to YAML definitions * @param parameterValues - Raw parameter values * @param parameterDefinitions - YAML parameter definitions * @param context - Request context * @returns Validated and processed parameters */ private static async validateAndProcessParameters( parameterValues: Record<string, unknown>, parameterDefinitions: SqlToolParameter[], context: RequestContext, ): Promise<Record<string, unknown>> { if (parameterDefinitions.length === 0) { return parameterValues; } const processedParameters = { ...parameterValues }; const allErrors: string[] = []; const allWarnings: string[] = []; // Process each parameter definition for (const definition of parameterDefinitions) { const value = parameterValues[definition.name]; const validationResult = this.validateParameter(value, definition); if (!validationResult.valid) { allErrors.push(...validationResult.errors); } else { allWarnings.push(...validationResult.warnings); // Use the validated and converted value if (validationResult.value !== undefined) { processedParameters[definition.name] = validationResult.value; } } } // Report validation errors if (allErrors.length > 0) { throw new McpError( JsonRpcErrorCode.ValidationError, `Parameter validation failed: ${allErrors.join(", ")}`, { errors: allErrors }, ); } // Log warnings if (allWarnings.length > 0) { logger.warning( { ...context, warnings: allWarnings, }, "Parameter validation warnings", ); } return processedParameters; } /** * Validate and convert parameter value according to YAML definition * @param value - Input value to validate * @param definition - YAML parameter definition * @returns Validation result with converted value */ private static validateParameter( value: unknown, definition: SqlToolParameter, ): ValidationResult { const result: ValidationResult = { valid: true, errors: [], warnings: [], }; try { // Handle null/undefined values if (value === null || value === undefined) { if (definition.required && definition.default === undefined) { result.errors.push( `Parameter '${definition.name}' is required but not provided`, ); result.valid = false; return result; } if (definition.default !== undefined) { return this.validateParameter(definition.default, { ...definition, required: false, }); } // Allow null/undefined for non-required parameters result.value = ""; return result; } // Type-specific validation switch (definition.type) { case "string": result.value = this.validateStringParameter( value, definition, result, ); break; case "integer": result.value = this.validateIntegerParameter( value, definition, result, ); break; case "float": result.value = this.validateFloatParameter(value, definition, result); break; case "boolean": result.value = this.validateBooleanParameter( value, definition, result, ); break; case "array": result.value = this.validateArrayParameter(value, definition, result); break; default: result.errors.push(`Unsupported parameter type: ${definition.type}`); result.valid = false; } // Enum validation if (result.valid && definition.enum && definition.enum.length > 0) { this.validateEnumConstraint(result.value!, definition, result); } result.valid = result.errors.length === 0; return result; } catch (error) { result.errors.push( error instanceof Error ? error.message : String(error), ); result.valid = false; return result; } } /** * Validate string parameter */ private static validateStringParameter( value: unknown, definition: SqlToolParameter, result: ValidationResult, ): string { let stringValue: string; if (typeof value === "string") { stringValue = value; } else if (typeof value === "number" || typeof value === "boolean") { stringValue = String(value); result.warnings.push( `Parameter '${definition.name}' converted from ${typeof value} to string`, ); } else { result.errors.push( `Parameter '${definition.name}' must be a string, got ${typeof value}`, ); return ""; } // Length validation if ( definition.minLength !== undefined && stringValue.length < definition.minLength ) { result.errors.push( `Parameter '${definition.name}' must be at least ${definition.minLength} characters long`, ); } if ( definition.maxLength !== undefined && stringValue.length > definition.maxLength ) { result.errors.push( `Parameter '${definition.name}' must be at most ${definition.maxLength} characters long`, ); } // Pattern validation if (definition.pattern) { try { const regex = new RegExp(definition.pattern); if (!regex.test(stringValue)) { result.errors.push( `Parameter '${definition.name}' does not match required pattern: ${definition.pattern}`, ); } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { result.warnings.push( `Invalid pattern for parameter '${definition.name}': ${definition.pattern}`, ); } } return stringValue; } /** * Validate integer parameter */ private static validateIntegerParameter( value: unknown, definition: SqlToolParameter, result: ValidationResult, ): number { let numericValue: number; if (typeof value === "number") { if (!Number.isInteger(value)) { result.warnings.push( `Parameter '${definition.name}' is a float, converting to integer`, ); numericValue = Math.floor(value); } else { numericValue = value; } } else if (typeof value === "string") { const parsed = parseInt(value, 10); if (isNaN(parsed)) { result.errors.push( `Parameter '${definition.name}' must be a valid integer, got '${value}'`, ); return 0; } numericValue = parsed; result.warnings.push( `Parameter '${definition.name}' converted from string '${value}' to integer ${parsed}`, ); } else if (typeof value === "boolean") { numericValue = value ? 1 : 0; result.warnings.push( `Parameter '${definition.name}' converted from boolean to integer`, ); } else { result.errors.push( `Parameter '${definition.name}' must be an integer, got ${typeof value}`, ); return 0; } // Range validation if (definition.min !== undefined && numericValue < definition.min) { result.errors.push( `Parameter '${definition.name}' must be at least ${definition.min}`, ); } if (definition.max !== undefined && numericValue > definition.max) { result.errors.push( `Parameter '${definition.name}' must be at most ${definition.max}`, ); } return numericValue; } /** * Validate float parameter */ private static validateFloatParameter( value: unknown, definition: SqlToolParameter, result: ValidationResult, ): number { let numericValue: number; if (typeof value === "number") { numericValue = value; } else if (typeof value === "string") { const parsed = parseFloat(value); if (isNaN(parsed)) { result.errors.push( `Parameter '${definition.name}' must be a valid number, got '${value}'`, ); return 0; } numericValue = parsed; result.warnings.push( `Parameter '${definition.name}' converted from string '${value}' to float ${parsed}`, ); } else if (typeof value === "boolean") { numericValue = value ? 1.0 : 0.0; result.warnings.push( `Parameter '${definition.name}' converted from boolean to float`, ); } else { result.errors.push( `Parameter '${definition.name}' must be a number, got ${typeof value}`, ); return 0; } // Range validation if (definition.min !== undefined && numericValue < definition.min) { result.errors.push( `Parameter '${definition.name}' must be at least ${definition.min}`, ); } if (definition.max !== undefined && numericValue > definition.max) { result.errors.push( `Parameter '${definition.name}' must be at most ${definition.max}`, ); } return numericValue; } /** * Validate boolean parameter */ private static validateBooleanParameter( value: unknown, definition: SqlToolParameter, result: ValidationResult, ): number { let booleanValue: boolean; if (typeof value === "boolean") { booleanValue = value; } else if (typeof value === "string") { const lowerValue = value.toLowerCase(); if ( lowerValue === "true" || lowerValue === "1" || lowerValue === "yes" || lowerValue === "on" ) { booleanValue = true; } else if ( lowerValue === "false" || lowerValue === "0" || lowerValue === "no" || lowerValue === "off" ) { booleanValue = false; } else { result.errors.push( `Parameter '${definition.name}' must be a valid boolean, got '${value}'`, ); return 0; } result.warnings.push( `Parameter '${definition.name}' converted from string '${value}' to boolean`, ); } else if (typeof value === "number") { booleanValue = value !== 0; result.warnings.push( `Parameter '${definition.name}' converted from number ${value} to boolean`, ); } else { result.errors.push( `Parameter '${definition.name}' must be a boolean, got ${typeof value}`, ); return 0; } // Convert boolean to number for DB2 compatibility return booleanValue ? 1 : 0; } /** * Validate array parameter */ private static validateArrayParameter( value: unknown, definition: SqlToolParameter, result: ValidationResult, ): (string | number)[] { if (!Array.isArray(value)) { result.errors.push( `Parameter '${definition.name}' must be an array, got ${typeof value}`, ); return []; } // Length validation if ( definition.minLength !== undefined && value.length < definition.minLength ) { result.errors.push( `Parameter '${definition.name}' must have at least ${definition.minLength} items`, ); } if ( definition.maxLength !== undefined && value.length > definition.maxLength ) { result.errors.push( `Parameter '${definition.name}' must have at most ${definition.maxLength} items`, ); } // Validate each array item const itemType = definition.itemType || "string"; const convertedArray: (string | number)[] = []; for (let i = 0; i < value.length; i++) { const item = value[i]; const itemDefinition: SqlToolParameter = { name: `${definition.name}[${i}]`, type: itemType, required: true, }; const itemResult = this.validateParameter(item, itemDefinition); if (!itemResult.valid) { result.errors.push(...itemResult.errors); } else { result.warnings.push(...itemResult.warnings); convertedArray.push(itemResult.value as string | number); } } return convertedArray; } /** * Validate enum constraint */ private static validateEnumConstraint( value: BindingValue, definition: SqlToolParameter, result: ValidationResult, ): void { if (!definition.enum || definition.enum.length === 0) { return; } // Handle array values if (Array.isArray(value)) { for (const item of value) { if (!definition.enum.includes(item as string | number | boolean)) { result.errors.push( `Parameter '${definition.name}' array item '${item}' is not one of allowed values: ${definition.enum.join(", ")}`, ); } } } else { if (!definition.enum.includes(value as string | number | boolean)) { result.errors.push( `Parameter '${definition.name}' value '${value}' is not one of allowed values: ${definition.enum.join(", ")}`, ); } } } /** * Detect the parameter mode used in the SQL statement * @param sql - SQL statement to analyze * @returns Detected parameter mode */ private static detectParameterMode(sql: string): ParameterMode { const hasNamedParams = /:(\w+)/g.test(sql); const hasPositionalParams = /\?/g.test(sql); const hasTemplateParams = /\{\{(\w+)\}\}/g.test(sql); if (hasTemplateParams) { return "template"; } if (hasNamedParams && hasPositionalParams) { return "hybrid"; } if (hasNamedParams) { return "named"; } if (hasPositionalParams) { return "positional"; } return "positional"; // Default mode } /** * Process named parameters (:param) * @param sql - SQL statement with named parameters * @param parameterValues - Parameter values by name * @param context - Request context * @param strictTypeValidation - Whether to enforce strict type validation * @returns Processing result */ private static async processNamedParameters( sql: string, parameterValues: Record<string, unknown>, context: RequestContext, strictTypeValidation: boolean, ): Promise<ParameterProcessingResult> { const namedParamRegex = /:(\w+)/g; const matches: NamedParameterMatch[] = []; const parameterNames: string[] = []; const missingParameters: string[] = []; let match; // Find all named parameter matches while ((match = namedParamRegex.exec(sql)) !== null) { matches.push({ fullMatch: match[0], paramName: match[1]!, position: match.index!, }); if (!parameterNames.includes(match[1]!)) { parameterNames.push(match[1]!); } } // Convert parameters to BindingValue array in order of appearance const parameters: BindingValue[] = []; let processedSql = sql; let offset = 0; for (const paramMatch of matches) { const paramName = paramMatch.paramName; // Check if parameter exists in the parameter values if (!(paramName in parameterValues)) { missingParameters.push(paramName); logger.warning( { ...context, availableParams: Object.keys(parameterValues), missingParam: paramName, }, `Named parameter ':${paramName}' not found in parameter values`, ); continue; } const paramValue = parameterValues[paramName]; // Convert and validate parameter value const bindingValue = this.convertToBindingValue( paramValue, paramName, strictTypeValidation, ); // Handle array parameters - expand to multiple placeholders if (Array.isArray(bindingValue)) { // Add each array element as individual parameters for (const item of bindingValue) { parameters.push(item); } // Generate multiple ? placeholders for array (?, ?, ?) const placeholders = bindingValue.map(() => "?").join(", "); // Replace :param with (?, ?, ?) const replacePosition = paramMatch.position + offset; const beforeReplacement = processedSql.substring(0, replacePosition); const afterReplacement = processedSql.substring( replacePosition + paramMatch.fullMatch.length, ); processedSql = beforeReplacement + placeholders + afterReplacement; // Update offset for subsequent replacements offset += placeholders.length - paramMatch.fullMatch.length; logger.debug( { ...context, paramName, arrayLength: bindingValue.length, placeholders, }, `Expanded array parameter to ${bindingValue.length} placeholders`, ); } else { // Non-array parameter - standard single placeholder parameters.push(bindingValue); // Replace :param with ? placeholder const replacePosition = paramMatch.position + offset; const beforeReplacement = processedSql.substring(0, replacePosition); const afterReplacement = processedSql.substring( replacePosition + paramMatch.fullMatch.length, ); processedSql = beforeReplacement + "?" + afterReplacement; // Update offset for subsequent replacements offset += 1 - paramMatch.fullMatch.length; } } return { sql: processedSql, parameters, parameterNames, missingParameters, mode: "named", stats: { originalLength: sql.length, processedLength: processedSql.length, namedParametersFound: matches.length, positionalParametersFound: 0, parametersConverted: parameters.length, }, }; } /** * Process positional parameters (?) * @param sql - SQL statement with positional parameters * @param parameterValues - Parameter values by name or index * @param context - Request context * @param strictTypeValidation - Whether to enforce strict type validation * @returns Processing result */ private static async processPositionalParameters( sql: string, parameterValues: Record<string, unknown>, context: RequestContext, strictTypeValidation: boolean, ): Promise<ParameterProcessingResult> { const positionalParamRegex = /\?/g; const parameterCount = (sql.match(positionalParamRegex) || []).length; // Convert parameters to array in order const parameters: BindingValue[] = []; const parameterNames: string[] = []; const missingParameters: string[] = []; // Extract parameters using zero-based indexing for (let i = 0; i < parameterCount; i++) { let paramValue: unknown; let paramName: string; // Try zero-based index parameter (0, 1, 2, ...) if (parameterValues[i.toString()] !== undefined) { paramValue = parameterValues[i.toString()]; paramName = i.toString(); } // Take parameters in order of Object.keys else { const keys = Object.keys(parameterValues); if (i < keys.length) { paramName = keys[i]!; paramValue = parameterValues[paramName]; } else { paramName = `param_${i}`; paramValue = undefined; } } parameterNames.push(paramName); if (paramValue === undefined || paramValue === null) { missingParameters.push(paramName); logger.warning( { ...context, availableParams: Object.keys(parameterValues), missingIndex: i, }, `Positional parameter at index ${i} not found`, ); continue; } // Convert and validate parameter value const bindingValue = this.convertToBindingValue( paramValue, paramName, strictTypeValidation, ); parameters.push(bindingValue); } return { sql, parameters, parameterNames, missingParameters, mode: "positional", stats: { originalLength: sql.length, processedLength: sql.length, namedParametersFound: 0, positionalParametersFound: parameterCount, parametersConverted: parameters.length, }, }; } /** * Process hybrid parameters (mix of named and positional) * @param sql - SQL statement with mixed parameters * @param parameterValues - Parameter values by name * @param context - Request context * @param strictTypeValidation - Whether to enforce strict type validation * @returns Processing result */ private static async processHybridParameters( sql: string, parameterValues: Record<string, unknown>, context: RequestContext, strictTypeValidation: boolean, ): Promise<ParameterProcessingResult> { logger.warning( { ...context, sql: sql.substring(0, 100) + "...", }, "Hybrid parameter mode detected - processing named parameters first", ); // Process named parameters first, then handle any remaining positional ones const namedResult = await this.processNamedParameters( sql, parameterValues, context, strictTypeValidation, ); // If there are still ? placeholders, process them as positional const positionalParamCount = (namedResult.sql.match(/\?/g) || []).length; if (positionalParamCount > namedResult.parameters.length) { logger.debug( { ...context, remainingPositional: positionalParamCount - namedResult.parameters.length, }, "Processing remaining positional parameters in hybrid mode", ); } return { ...namedResult, mode: "hybrid", stats: { ...namedResult.stats, positionalParametersFound: positionalParamCount, }, }; } /** * Convert a parameter value to BindingValue * @param value - Parameter value to convert * @param paramName - Parameter name for error messages * @param strictTypeValidation - Whether to enforce strict type validation * @returns Converted BindingValue */ private static convertToBindingValue( value: unknown, paramName: string, strictTypeValidation: boolean, ): BindingValue { // BindingValue = string | number | (string | number)[] if (value === null || value === undefined) { if (strictTypeValidation) { throw new McpError( JsonRpcErrorCode.ValidationError, `Parameter '${paramName}' cannot be null or undefined`, { paramName, value }, ); } return ""; } // Handle arrays if (Array.isArray(value)) { const convertedArray = value.map((item, index) => { if (typeof item === "string" || typeof item === "number") { return item; } if (strictTypeValidation) { throw new McpError( JsonRpcErrorCode.ValidationError, `Array parameter '${paramName}[${index}]' must contain only strings or numbers`, { paramName, index, itemType: typeof item, itemValue: item }, ); } return String(item); }); return convertedArray; } // Handle primitives if (typeof value === "string" || typeof value === "number") { return value; } // Handle booleans if (typeof value === "boolean") { return value ? 1 : 0; // Convert boolean to number for DB2 } // Handle other types if (strictTypeValidation) { throw new McpError( JsonRpcErrorCode.ValidationError, `Parameter '${paramName}' must be string, number, boolean, or array of strings/numbers`, { paramName, type: typeof value, value }, ); } // Convert to string as fallback return String(value); } /** * Validate SQL statement syntax for parameter processing * @param sql - SQL statement to validate * @throws McpError if syntax is invalid */ private static validateSqlSyntax(sql: string): void { // Check for malformed named parameters const malformedNamedParams = sql.match(/:(\d)/g); if (malformedNamedParams) { throw new McpError( JsonRpcErrorCode.ValidationError, `Invalid named parameter syntax found: ${malformedNamedParams.join(", ")}. Named parameters must start with a letter.`, { malformedParams: malformedNamedParams }, ); } // Check for unmatched quotes that could affect parameter parsing const singleQuotes = (sql.match(/'/g) || []).length; const doubleQuotes = (sql.match(/"/g) || []).length; if (singleQuotes % 2 !== 0) { throw new McpError( JsonRpcErrorCode.ValidationError, "Unmatched single quotes in SQL statement", { singleQuoteCount: singleQuotes }, ); } if (doubleQuotes % 2 !== 0) { throw new McpError( JsonRpcErrorCode.ValidationError, "Unmatched double quotes in SQL statement", { doubleQuoteCount: doubleQuotes }, ); } } /** * Extract parameter names from SQL statement * @param sql - SQL statement to analyze * @returns Array of unique parameter names */ static extractParameterNames(sql: string): string[] { const namedParamRegex = /:(\w+)/g; const parameterNames: string[] = []; let match; while ((match = namedParamRegex.exec(sql)) !== null) { if (!parameterNames.includes(match[1]!)) { parameterNames.push(match[1]!); } } return parameterNames; } /** * Count positional parameters in SQL statement * @param sql - SQL statement to analyze * @returns Number of ? placeholders */ static countPositionalParameters(sql: string): number { return (sql.match(/\?/g) || []).length; } /** * Check if SQL statement has any parameters * @param sql - SQL statement to check * @returns True if statement contains parameters */ static hasParameters(sql: string): boolean { return ( this.detectParameterMode(sql) !== "positional" || this.countPositionalParameters(sql) > 0 ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IBM/ibmi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server