using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Sozsoft.SqlQueryManager.Application.Contracts; using Sozsoft.SqlQueryManager.Domain.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Volo.Abp.Application.Services; using Volo.Abp.MultiTenancy; namespace Sozsoft.SqlQueryManager.Application; /// /// Executes T-SQL against configured data sources and exposes database metadata. /// Does not persist SQL objects (queries, procedures, views, functions) to its own tables. /// [Authorize("App.SqlQueryManager")] public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService { private readonly ISqlExecutorService _sqlExecutorService; private readonly ISqlTemplateProvider _templateProvider; private readonly ICurrentTenant _currentTenant; private readonly IHttpContextAccessor _httpContextAccessor; public SqlObjectManagerAppService( ISqlExecutorService sqlExecutorService, ISqlTemplateProvider templateProvider, ICurrentTenant currentTenant, IHttpContextAccessor httpContextAccessor) { _sqlExecutorService = sqlExecutorService; _templateProvider = templateProvider; _currentTenant = currentTenant; _httpContextAccessor = httpContextAccessor; } private string GetTenantFromHeader() { return _httpContextAccessor.HttpContext? .Request? .Headers["__tenant"] .FirstOrDefault(); } private void ValidateTenantAccess() { var headerTenant = GetTenantFromHeader(); var currentTenantName = _currentTenant.Name; if (_currentTenant.IsAvailable) { if (headerTenant != currentTenantName) { throw new Volo.Abp.UserFriendlyException($"Tenant mismatch. Header tenant '{headerTenant}' does not match current tenant '{currentTenantName}'."); } } } public async Task GetAllObjectsAsync(string dataSourceCode) { ValidateTenantAccess(); var result = new SqlObjectExplorerDto(); result.Tables = await GetTablesAsync(dataSourceCode); result.Views = await GetNativeObjectsAsync(dataSourceCode, "V"); result.StoredProcedures = await GetNativeObjectsAsync(dataSourceCode, "P"); result.Functions = await GetNativeObjectsAsync(dataSourceCode, "FN", "IF", "TF"); result.Templates = _templateProvider.GetAvailableQueryTemplates() .Select(t => new SqlTemplateDto { Type = t.Type, Name = t.Name, Description = t.Description, Template = _templateProvider.GetQueryTemplate(t.Type) }) .ToList(); return result; } private async Task> GetNativeObjectsAsync(string dataSourceCode, params string[] objectTypes) { var typeList = string.Join(",", objectTypes.Select(t => $"'{t}'")); var query = $@" SELECT SCHEMA_NAME(o.schema_id) AS SchemaName, o.name AS ObjectName FROM sys.objects o WHERE o.type IN ({typeList}) AND o.is_ms_shipped = 0 ORDER BY SCHEMA_NAME(o.schema_id), o.name"; var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var objects = new List(); if (result.Success && result.Data != null) { foreach (var row in result.Data) { var dict = row as IDictionary; if (dict != null) { objects.Add(new SqlNativeObjectDto { SchemaName = dict["SchemaName"]?.ToString() ?? "dbo", ObjectName = dict["ObjectName"]?.ToString() ?? "" }); } } } return objects; } private async Task> GetTablesAsync(string dataSourceCode) { var query = @" SELECT SCHEMA_NAME(t.schema_id) AS SchemaName, t.name AS TableName FROM sys.tables t WHERE t.is_ms_shipped = 0 ORDER BY SCHEMA_NAME(t.schema_id), t.name"; var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var tables = new List(); if (result.Success && result.Data != null) { foreach (var row in result.Data) { var dict = row as IDictionary; if (dict != null) { tables.Add(new DatabaseTableDto { SchemaName = dict["SchemaName"]?.ToString() ?? "dbo", TableName = dict["TableName"]?.ToString() ?? "" }); } } } return tables; } public async Task ExecuteQueryAsync(ExecuteSqlQueryDto input) { ValidateTenantAccess(); var result = await _sqlExecutorService.ExecuteQueryAsync( input.QueryText, input.DataSourceCode, input.Parameters); return MapExecutionResult(result); } public async Task GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName) { ValidateTenantAccess(); var query = @" SELECT OBJECT_DEFINITION(OBJECT_ID(@ObjectName)) AS Definition"; var fullObjectName = $"[{schemaName}].[{objectName}]"; var result = await _sqlExecutorService.ExecuteQueryAsync( query.Replace("@ObjectName", $"'{fullObjectName}'"), dataSourceCode); if (result.Success && result.Data != null) { var dataList = result.Data.ToList(); if (dataList.Count > 0) { var row = dataList[0] as IDictionary; if (row != null && row.ContainsKey("Definition")) { var definition = row["Definition"]?.ToString() ?? string.Empty; // Replace first CREATE keyword with ALTER so the user edits, not recreates definition = System.Text.RegularExpressions.Regex.Replace( definition, @"^\s*CREATE\s+", "ALTER ", System.Text.RegularExpressions.RegexOptions.IgnoreCase); return definition; } } } return string.Empty; } public async Task> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName) { ValidateTenantAccess(); var query = $@" SELECT c.name AS ColumnName, TYPE_NAME(c.user_type_id) AS DataType, c.is_nullable AS IsNullable, c.max_length AS MaxLength FROM sys.columns c INNER JOIN sys.tables t ON c.object_id = t.object_id INNER JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = '{schemaName}' AND t.name = '{tableName}' ORDER BY c.column_id"; var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var columns = new List(); if (result.Success && result.Data != null) { foreach (var row in result.Data) { var dict = row as IDictionary; if (dict != null) { columns.Add(new DatabaseColumnDto { ColumnName = dict["ColumnName"]?.ToString() ?? "", DataType = dict["DataType"]?.ToString() ?? "", IsNullable = dict["IsNullable"] is bool b && b, MaxLength = dict["MaxLength"] != null ? int.Parse(dict["MaxLength"].ToString()) : null }); } } } return columns; } private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result) { return new SqlQueryExecutionResultDto { Success = result.Success, Message = result.Message, Data = result.Data, RowsAffected = result.RowsAffected, ExecutionTimeMs = result.ExecutionTimeMs, Metadata = result.Metadata }; } }