using GB5Shared.DTO.Framework.Login;
using GB5Shared.QueryExecutor;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace PayRollDAL.CustomeCode.Employee
{
///
/// Resolves which addon columns are valid for a given entity (e.g. "EMPLOYEEADDON")
/// on the CURRENT tenant database, and builds a SQL SELECT fragment that can be
/// spliced into a larger query (e.g. GET_EMPLOYEE) at a token position.
///
/// SQL Server only (no dialect branching) per your requirement.
///
/// Resolution chain:
/// MENTITY (ENTITYCODE -> ENTITYID)
/// -> MADDONFIELDS (ENTITYID -> configured FIELDNAME list)
/// -> INFORMATION_SCHEMA.COLUMNS (validate FIELDNAME actually exists
/// physically on this tenant DB's table - the safeguard that prevents
/// the "basictest only has EMPLOYEEID" bug)
///
public class AddonColumnResolver : IAddonColumnResolver
{
private readonly IQueryExecutor _queryExecutor;
// cache key = "{ClientId}|{EntityCode}|{TableName}"
private static readonly ConcurrentDictionary> _fieldCache = new();
// Defense in depth: never splice anything into SQL text that isn't a plain
// identifier, even though we already intersect against real physical columns.
private static readonly Regex SafeIdentifier = new("^[A-Za-z0-9_]+$", RegexOptions.Compiled);
public AddonColumnResolver(IQueryExecutor queryExecutor)
{
_queryExecutor = queryExecutor;
}
///
/// Returns the validated, safe field names for the given entity/table on the
/// tenant database resolved from loginDTO. Cached, since MENTITY/MADDONFIELDS
/// metadata rarely changes.
///
public async Task> GetSafeFieldNamesAsync(
LoginDTO loginDTO, string entityCode, string tableName)
{
var cacheKey = $"{loginDTO.ClientId}|{entityCode}|{tableName}";
if (_fieldCache.TryGetValue(cacheKey, out var cached))
return cached;
// 1) ENTITYID for this entity code.
// TOP 1 guards against QuerySingleAsync throwing if ENTITYCODE
// somehow isn't unique - we just want one deterministic match.
var entityId = await _queryExecutor.QuerySingleAsync(
loginDTO,
"SELECT TOP 1 ENTITYID FROM MENTITY WHERE ENTITYCODE = @EntityCode",
new { EntityCode = entityCode });
if (entityId == null)
{
_fieldCache[cacheKey] = new List();
return _fieldCache[cacheKey];
}
// 2) Configured fields from MADDONFIELDS (only active ones, in admin-defined order)
var configuredFields = (await _queryExecutor.QueryAsync(
loginDTO,
@"SELECT FIELDNAME FROM MADDONFIELDS
WHERE ENTITYID = @EntityId AND STATUS = 1
ORDER BY SORTORDER",
new { EntityId = entityId })).ToList();
if (configuredFields.Count == 0)
{
_fieldCache[cacheKey] = new List();
return _fieldCache[cacheKey];
}
// 3) Physical columns that actually exist on THIS tenant DB's table.
var physicalColumns = (await _queryExecutor.QueryAsync(
loginDTO,
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName",
new { TableName = tableName }))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
// 4) Intersect configured fields with physical reality + identifier safety check
var safeFields = configuredFields
.Where(f => physicalColumns.Contains(f))
.Where(f => SafeIdentifier.IsMatch(f))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
_fieldCache[cacheKey] = safeFields;
return safeFields;
}
///
/// Builds the SQL fragment for the addon table, aliased with an "Addon_" prefix
/// so each field can be embedded into GET_EMPLOYEE without colliding with the
/// existing splitOn markers (AddonEmployeeId, AddressId, PermAddressId, ContactId).
/// Each item is comma-terminated so it can be inserted directly after
/// "empaddon.EMPLOYEEID AS AddonEmployeeId," with no special-casing for the
/// empty case.
///
public async Task<(string sqlFragment, List fieldNames)> BuildAddonSelectFragmentAsync(
LoginDTO loginDTO,
string entityCode,
string tableName,
string tableAlias)
{
var fields = await GetSafeFieldNamesAsync(loginDTO, entityCode, tableName);
if (fields.Count == 0)
return (string.Empty, fields);
var fragment = string.Join(
"\n ",
fields.Select(f => $"{tableAlias}.[{f}] AS Addon_{f},"));
return (fragment, fields);
}
///
/// Call if MENTITY/MADDONFIELDS metadata changes at runtime (rare) and you
/// need to force a re-read instead of serving cached values.
///
public void ClearCache() => _fieldCache.Clear();
}
public interface IAddonColumnResolver
{
Task> GetSafeFieldNamesAsync(LoginDTO loginDTO, string entityCode, string tableName);
Task<(string sqlFragment, List fieldNames)> BuildAddonSelectFragmentAsync(
LoginDTO loginDTO, string entityCode, string tableName, string tableAlias);
void ClearCache();
}
}