using Dapper; using GB5Shared.CriteriaHandler; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using Newtonsoft.Json; using System.Text.RegularExpressions; namespace GB5Shared.QueryExecutor; public static class QueryWithCriteriaExtensions { // Matches ORDER BY ... at the end of the text after {DYNAMIC_WHERE} has been removed. // Used to lift ORDER BY from inner subquery to outer query (SQL Server forbids // ORDER BY inside a subquery without TOP/OFFSET). private static readonly Regex _orderByRegex = new( @"(?i)\s*\bORDER\s+BY\b[\s\S]*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); /// /// Replaces {DYNAMIC_WHERE} in the SQL. /// When is non-empty the query is automatically /// wrapped in a derived table so that SELECT-list aliases (e.g. SKILLCODE AS Code) /// are visible in the outer WHERE clause. ORDER BY is extracted from the original /// query and moved to the outer SELECT so SQL Server's subquery restriction is respected. /// private static string ApplyCriteria(string sql, string whereClause) { if (string.IsNullOrEmpty(whereClause)) return sql.Replace("{DYNAMIC_WHERE}", string.Empty); int idx = sql.IndexOf("{DYNAMIC_WHERE}", StringComparison.OrdinalIgnoreCase); if (idx < 0) return sql; string before = sql[..idx].TrimEnd(); string after = sql[(idx + "{DYNAMIC_WHERE}".Length)..]; // Extract trailing ORDER BY so it can be placed on the outer query var orderByMatch = _orderByRegex.Match(after); string orderBy = orderByMatch.Success ? orderByMatch.Value : string.Empty; string middle = orderByMatch.Success ? after[..orderByMatch.Index] : after; string innerSql = (before + middle).TrimEnd().TrimEnd(';'); // Strip table-alias prefixes from criteria column references (e.g. "A.CODE" → "CODE") // so they resolve against the derived-table columns exposed by the inner SELECT aliases. string outerWhere = Regex.Replace(whereClause, @"\b[A-Za-z_]\w*\.", string.Empty); // ORDER BY moves to the outer query too, so its alias prefixes (e.g. "ti.INSTANCETITLE") // must be stripped the same way — the inner table aliases aren't visible outside __T. string outerOrderBy = Regex.Replace(orderBy, @"\b[A-Za-z_]\w*\.", string.Empty); return $"SELECT * FROM ({innerSql}) AS __T WHERE 1=1 {outerWhere}{outerOrderBy}"; } public static async Task QueryWithCriteriaAsync( this IQueryExecutor executor, LoginDTO loginDTO, string sql, CriteriaDTO criteriaDTO) { var existingParams = new DynamicParameters(); var (whereClause, parameters) = CriteriaBuilder.Build(criteriaDTO, existingParams, sql); var finalSql = ApplyCriteria(sql, whereClause); var result = await executor.QueryAsync(loginDTO, finalSql, parameters); return JsonConvert.SerializeObject(result); } public static async Task QueryWithCriteriaAsync( this IQueryExecutor executor, LoginDTO loginDTO, string sql, CriteriaDTO criteriaDTO, CancellationToken cancellationToken) { var existingParams = new DynamicParameters(); var (whereClause, parameters) = CriteriaBuilder.Build(criteriaDTO, existingParams, sql); var finalSql = ApplyCriteria(sql, whereClause); var result = await executor.QueryAsync(loginDTO, finalSql, parameters, cancellationToken: cancellationToken); return JsonConvert.SerializeObject(result); } public static async Task> QueryWithCriteriaAsync( this IQueryExecutor executor, LoginDTO loginDTO, string sql, object sqlParams, CriteriaDTO criteriaDTO, CancellationToken cancellationToken = default) { var parameters = new DynamicParameters(sqlParams); var (whereClause, finalParameters) = CriteriaBuilder.Build(criteriaDTO, parameters, sql); var finalSql = ApplyCriteria(sql, whereClause); return await executor.QueryAsync( loginDTO, finalSql, finalParameters, cancellationToken: cancellationToken); } // JSON-serializing counterpart to the IEnumerable overload above — same // static-param + criteria composition, for callers whose interface returns // the raw JSON string (e.g. dynamic select-list endpoints) instead of typed objects. public static async Task QueryWithCriteriaJsonAsync( this IQueryExecutor executor, LoginDTO loginDTO, string sql, object sqlParams, CriteriaDTO criteriaDTO, CancellationToken cancellationToken = default) { var parameters = new DynamicParameters(sqlParams); var (whereClause, finalParameters) = CriteriaBuilder.Build(criteriaDTO, parameters, sql); var finalSql = ApplyCriteria(sql, whereClause); var result = await executor.QueryAsync( loginDTO, finalSql, finalParameters, cancellationToken: cancellationToken); return JsonConvert.SerializeObject(result); } }