using GB5Shared.DTO.Framework.Login;
using SwBLL.ClientDatabase;
using SwBLL.DbServer;
using SwBLL.Provisioning;
using SwDAL.DTO.DmlScript;
using SwDAL.Enums;
using System.Text.Json;
namespace SwBLL.DmlScript;
///
/// Executes parameterized DML (INSERT / UPDATE / DELETE) against a client's target database.
///
/// Security rules:
/// - Column names from RowValues/WhereValues are ALWAYS bracket-quoted: [{col.Replace("]","")}]
/// - Values are ALWAYS bound as Dapper parameters — never concatenated into SQL.
/// - Never injects another module's DAL — uses IDbServerBLL and IClientDatabaseBLL.
///
public class DmlScriptBLL : IDmlScriptBLL
{
private readonly IClientDatabaseBLL _clientDatabaseBLL;
private readonly IDbServerBLL _dbServerBLL;
private readonly ITargetDbExecutor _targetDbExecutor;
public DmlScriptBLL(
IClientDatabaseBLL clientDatabaseBLL,
IDbServerBLL dbServerBLL,
ITargetDbExecutor targetDbExecutor)
{
_clientDatabaseBLL = clientDatabaseBLL;
_dbServerBLL = dbServerBLL;
_targetDbExecutor = targetDbExecutor;
}
public async Task ExecuteDml(
DmlExecutionRequestDTO request, LoginDTO loginDTO, CancellationToken ct)
{
if (request == null) throw new ArgumentNullException(nameof(request));
if (string.IsNullOrWhiteSpace(request.TableName))
throw new ArgumentException("TableName is required.", nameof(request));
var clientDb = await _clientDatabaseBLL.GetById(request.ClientDatabaseId, loginDTO, ct)
?? throw new InvalidOperationException($"Client database {request.ClientDatabaseId} not found.");
var server = await _dbServerBLL.GetByIdWithCredentials(clientDb.DbServerId, loginDTO, ct)
?? throw new InvalidOperationException($"DB server {clientDb.DbServerId} not found.");
// Data-only change, no schema rights needed — uses the least-privilege _app contained user.
string connStr = await _targetDbExecutor
.BuildConnectionStringForRoleAsync(server, clientDb, ClientDbLoginRole.App, ct)
.ConfigureAwait(false);
string qualTable = $"[{EscapeBracket(request.SchemaName)}].[{EscapeBracket(request.TableName)}]";
var (sql, parm) = BuildDml(request, qualTable);
int rows = await _targetDbExecutor.ExecuteDmlAsync(connStr, sql, parm, ct).ConfigureAwait(false);
return new DmlExecutionResultDTO
{
RowsAffected = rows,
Success = rows > 0,
Message = rows > 0 ? $"{rows} row(s) affected." : "No rows affected."
};
}
// ── SQL builder ───────────────────────────────────────────────────────
private static (string Sql, Dictionary Params) BuildDml(
DmlExecutionRequestDTO req, string qualTable)
{
var p = new Dictionary();
return req.Operation switch
{
DmlOperation.Insert => BuildInsert(req, qualTable, p),
DmlOperation.Update => BuildUpdate(req, qualTable, p),
DmlOperation.Delete => BuildDelete(req, qualTable, p),
_ => throw new ArgumentOutOfRangeException(nameof(req.Operation))
};
}
private static (string, Dictionary) BuildInsert(
DmlExecutionRequestDTO req, string qualTable, Dictionary p)
{
if (req.RowValues.Count == 0)
throw new ArgumentException("RowValues must not be empty for INSERT.");
var cols = new List();
var vals = new List();
int idx = 0;
foreach (var kv in req.RowValues)
{
string paramName = $"@p{idx++}";
cols.Add($"[{EscapeBracket(kv.Key)}]");
vals.Add(paramName);
p[paramName] = NormalizeValue(kv.Value);
}
string sql = $"INSERT INTO {qualTable} ({string.Join(", ", cols)}) VALUES ({string.Join(", ", vals)})";
return (sql, p);
}
private static (string, Dictionary) BuildUpdate(
DmlExecutionRequestDTO req, string qualTable, Dictionary p)
{
if (req.RowValues.Count == 0)
throw new ArgumentException("RowValues must not be empty for UPDATE.");
if (req.WhereValues.Count == 0)
throw new ArgumentException("WhereValues must not be empty for UPDATE — unbounded update rejected.");
var setClauses = new List();
var whereClauses = new List();
int idx = 0;
foreach (var kv in req.RowValues)
{
string paramName = $"@p{idx++}";
setClauses.Add($"[{EscapeBracket(kv.Key)}] = {paramName}");
p[paramName] = kv.Value;
}
foreach (var kv in req.WhereValues)
{
string paramName = $"@w{idx++}";
whereClauses.Add($"[{EscapeBracket(kv.Key)}] = {paramName}");
p[paramName] = kv.Value;
}
string sql = $"UPDATE {qualTable} SET {string.Join(", ", setClauses)} WHERE {string.Join(" AND ", whereClauses)}";
return (sql, p);
}
private static (string, Dictionary) BuildDelete(
DmlExecutionRequestDTO req, string qualTable, Dictionary p)
{
if (req.WhereValues.Count == 0)
throw new ArgumentException("WhereValues must not be empty for DELETE — unbounded delete rejected.");
var whereClauses = new List();
int idx = 0;
foreach (var kv in req.WhereValues)
{
string paramName = $"@w{idx++}";
whereClauses.Add($"[{EscapeBracket(kv.Key)}] = {paramName}");
p[paramName] = kv.Value;
}
string sql = $"DELETE FROM {qualTable} WHERE {string.Join(" AND ", whereClauses)}";
return (sql, p);
}
/// Strips bracket characters to prevent bracket-injection in column names.
private static string EscapeBracket(string name) => name.Replace("[", "").Replace("]", "");
private static object? NormalizeValue(object? value)
{
if (value is JsonElement je)
{
switch (je.ValueKind)
{
case JsonValueKind.String:
return je.GetString();
case JsonValueKind.Number:
if (je.TryGetInt32(out int intVal))
return intVal;
if (je.TryGetInt64(out long longVal))
return longVal;
if (je.TryGetDecimal(out decimal decVal))
return decVal;
return je.GetDouble();
case JsonValueKind.True:
return true;
case JsonValueKind.False:
return false;
case JsonValueKind.Null:
return null;
default:
return je.ToString();
}
}
return value;
}
}