diff --git a/.github/instructions/list.instructions.md b/.github/instructions/list.instructions.md index 3e65f7d..0022381 100644 --- a/.github/instructions/list.instructions.md +++ b/.github/instructions/list.instructions.md @@ -49,7 +49,7 @@ This document summarizes the rules, standards, and step-by-step instructions for - **Created** new seeder file for {Modul}. - **Seeds** {Liste} list-form, referencing `{Modul}_T_{Entity}`. -### 5. SqlTables.sql +### 5. SqlObjects.sql - **Renamed** {Liste} table to `{Modul}_T_{Entity}`. - **Updated** all related constraints and references. @@ -72,7 +72,7 @@ Yeni bir modül ekle (ör: {Modul}): - MenusData.json'a kök menü olarak ekle - PermissionsData.json'da ayrı bir PermissionGroup oluştur - ListFormSeeder_{Modul}.cs dosyası oluştur ve list-form seed'ini buraya taşı -- SqlTables.sql'de tabloyu {Modul}_T_{Entity} olarak adlandır +- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak adlandır - LanguagesData.json'a menü, liste ve alan çevirilerini ekle ``` @@ -81,7 +81,7 @@ Yeni bir modül ekle (ör: {Modul}): Yeni bir liste ekle (ör: {Liste}): - {Modul} ana menüsünün altına ekle - ListFormSeeder_{Modul}.cs dosyasına seed kodunu ekle -- SqlTables.sql'de tabloyu {Modul}_T_{Entity} olarak oluştur +- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak oluştur - PermissionsData.json'da ilgili izinleri {Modul} grubuna ekle - LanguagesData.json'a liste ve alan çevirilerini ekle ``` diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs index adef427..b2e35bf 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs @@ -27,4 +27,16 @@ public interface ISqlObjectManagerAppService : IApplicationService /// Gets the SQL definition/body of a native SQL Server object (Stored Procedure, View, or Function) /// Task GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName); + + /// + /// Saves the T-SQL script to Seeds/SqlData/{fileName}.sql in the DbMigrator project. + /// Called automatically after a successful SqlTableDesigner deploy so the script can be re-seeded. + /// + Task SaveTableScriptAsync(SaveTableScriptDto input); + + /// + /// Deletes matching SQL seed files from Seeds/SqlData when objects are dropped from the UI. + /// Non-existing files are ignored. + /// + Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input); } diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs index e180eaf..83f4417 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs @@ -18,3 +18,31 @@ public class SqlQueryExecutionResultDto public long ExecutionTimeMs { get; set; } public Dictionary Metadata { get; set; } } + +/// +/// Input for saving a T-SQL script file to DbMigrator Seeds/SqlData. +/// +public class SaveTableScriptDto +{ + /// + /// File name without extension (e.g. "Adm_T_Behavior"). Must not contain path separators. + /// + public string FileName { get; set; } + + /// + /// The T-SQL script content (CREATE TABLE / ALTER TABLE etc.). + /// + public string SqlScript { get; set; } +} + +/// +/// Input for deleting seed files from DbMigrator Seeds/SqlData. +/// +public class DeleteSqlDataFilesDto +{ + /// + /// File names without extension (e.g. "Adm_T_Behavior"). + /// Any invalid or unsafe values are ignored. + /// + public List FileNames { get; set; } = new(); +} diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs index 8d40c06..afa4112 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs @@ -1,13 +1,18 @@ using System; using System.Collections.Generic; +using System.IO; 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 Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Volo.Abp.Application.Services; using Volo.Abp.MultiTenancy; +using System.Text.RegularExpressions; namespace Sozsoft.SqlQueryManager.Application; @@ -22,17 +27,23 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA private readonly ISqlTemplateProvider _templateProvider; private readonly ICurrentTenant _currentTenant; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IHostEnvironment _hostEnvironment; + private readonly ILogger _logger; public SqlObjectManagerAppService( ISqlExecutorService sqlExecutorService, ISqlTemplateProvider templateProvider, ICurrentTenant currentTenant, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IHostEnvironment hostEnvironment, + ILogger logger) { _sqlExecutorService = sqlExecutorService; _templateProvider = templateProvider; _currentTenant = currentTenant; _httpContextAccessor = httpContextAccessor; + _hostEnvironment = hostEnvironment; + _logger = logger; } private string GetTenantFromHeader() @@ -155,11 +166,37 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA public async Task ExecuteQueryAsync(ExecuteSqlQueryDto input) { ValidateTenantAccess(); - var result = await _sqlExecutorService.ExecuteQueryAsync( - input.QueryText, - input.DataSourceCode, - input.Parameters); - return MapExecutionResult(result); + + // Split on GO batch separators (SQL Server SSMS convention — not valid T-SQL) + var batches = Regex.Split(input.QueryText ?? string.Empty, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase) + .Select(b => b.Trim()) + .Where(b => !string.IsNullOrWhiteSpace(b)) + .ToList(); + + if (batches.Count <= 1) + { + // Single batch — original path + var result = await _sqlExecutorService.ExecuteQueryAsync( + input.QueryText, + input.DataSourceCode, + input.Parameters); + return MapExecutionResult(result); + } + + // Multiple batches — execute sequentially, return last meaningful result + SqlExecutionResult lastResult = null; + foreach (var batch in batches) + { + lastResult = await _sqlExecutorService.ExecuteQueryAsync( + batch, + input.DataSourceCode, + input.Parameters); + + if (!lastResult.Success) + return MapExecutionResult(lastResult); + } + + return MapExecutionResult(lastResult!); } public async Task GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName) @@ -251,4 +288,98 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA Metadata = result.Metadata }; } + + public Task SaveTableScriptAsync(SaveTableScriptDto input) + { + // Security: reject any path traversal attempts + if (string.IsNullOrWhiteSpace(input?.FileName) || + input.FileName.Contains('/') || + input.FileName.Contains('\\') || + input.FileName.Contains("..")) + { + throw new Volo.Abp.UserFriendlyException("Invalid file name."); + } + + try + { + var outputPath = ResolveSqlDataOutputPath(); + Directory.CreateDirectory(outputPath); + + var safeFileName = string.Concat(input.FileName.Trim().Split(Path.GetInvalidFileNameChars())); + var filePath = Path.Combine(outputPath, $"{safeFileName}.sql"); + File.WriteAllText(filePath, input.SqlScript); + + _logger.LogInformation("SQL seed file saved: {FilePath}", filePath); + } + catch (Exception ex) + { + // File save failure does not block the deploy + _logger.LogError(ex, "Failed to save SQL seed file: {Message}", ex.Message); + } + + return Task.CompletedTask; + } + + [HttpPost("api/app/sql-object-manager/delete-sql-data-files")] + public Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input) + { + if (input?.FileNames == null || input.FileNames.Count == 0) + return Task.CompletedTask; + + try + { + var outputPath = ResolveSqlDataOutputPath(); + if (!Directory.Exists(outputPath)) + return Task.CompletedTask; + + foreach (var rawName in input.FileNames.Distinct()) + { + if (string.IsNullOrWhiteSpace(rawName)) + continue; + + // Security: reject any path traversal attempts + if (rawName.Contains('/') || rawName.Contains('\\') || rawName.Contains("..")) + continue; + + var safeFileName = string.Concat(rawName.Trim().Split(Path.GetInvalidFileNameChars())); + if (string.IsNullOrWhiteSpace(safeFileName)) + continue; + + var filePath = Path.Combine(outputPath, $"{safeFileName}.sql"); + if (!File.Exists(filePath)) + continue; + + File.Delete(filePath); + _logger.LogInformation("SQL seed file deleted: {FilePath}", filePath); + } + } + catch (Exception ex) + { + // File delete failure does not block drop operation + _logger.LogError(ex, "Failed to delete SQL seed file(s): {Message}", ex.Message); + } + + return Task.CompletedTask; + } + + private string ResolveSqlDataOutputPath() + { + const string dbMigratorName = "Sozsoft.Platform.DbMigrator"; + var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath); + + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds"); + if (Directory.Exists(candidate)) + return Path.Combine(candidate, "SqlData"); + + candidate = Path.Combine(dir.FullName, dbMigratorName, "Seeds"); + if (Directory.Exists(candidate)) + return Path.Combine(candidate, "SqlData"); + + dir = dir.Parent; + } + + return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", "SqlData"); + } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_Behavior.sql b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_Behavior.sql new file mode 100644 index 0000000..50db692 --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_Behavior.sql @@ -0,0 +1,32 @@ +IF OBJECT_ID(N'[dbo].[Adm_T_Behavior]', 'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Adm_T_Behavior]( + [Id] [nvarchar](128) NOT NULL, + [TenantId] [uniqueidentifier] NULL, + [Name] [nvarchar](128) NOT NULL, + [CreationTime] [datetime2](7) NOT NULL, + [CreatorId] [uniqueidentifier] NULL, + [LastModificationTime] [datetime2](7) NULL, + [LastModifierId] [uniqueidentifier] NULL, + [IsDeleted] [bit] NOT NULL, + [DeleterId] [uniqueidentifier] NULL, + [DeletionTime] [datetime2](7) NULL, + CONSTRAINT [PK_Adm_T_Behavior] PRIMARY KEY CLUSTERED + ( + [Id] ASC + )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] + ) ON [PRIMARY] +END +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.default_constraints dc + JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id + WHERE dc.parent_object_id = OBJECT_ID(N'[dbo].[Adm_T_Behavior]') + AND c.name = N'IsDeleted' +) +BEGIN + ALTER TABLE [dbo].[Adm_T_Behavior] ADD DEFAULT (CONVERT([bit],(0))) FOR [IsDeleted] +END +GO \ No newline at end of file diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_DatabaseBackup.sql b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_DatabaseBackup.sql new file mode 100644 index 0000000..d1ef0fa --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_DatabaseBackup.sql @@ -0,0 +1,19 @@ +CREATE OR ALTER PROCEDURE [dbo].[Adm_T_DatabaseBackup] +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @SQL NVARCHAR(MAX) = N''; + + SELECT @SQL = @SQL + N' +BACKUP DATABASE ' + QUOTENAME(name) + N' +TO DISK = N''/var/opt/mssql/backup/' ++ name + N'_' + CONVERT(VARCHAR(8), GETDATE(), 112) + N'.bak'' +WITH INIT;' + FROM sys.databases + WHERE name NOT IN ('master','model','msdb','tempdb') + AND state_desc = 'ONLINE'; + + EXEC sp_executesql @SQL; +END +GO \ No newline at end of file diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_Department.sql b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_Department.sql new file mode 100644 index 0000000..f79aaaa --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/Adm_T_Department.sql @@ -0,0 +1,23 @@ +IF OBJECT_ID(N'[dbo].[Adm_T_Department]', 'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Adm_T_Department] + ( + [Id] uniqueidentifier NOT NULL DEFAULT NEWID(), + [CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(), + [CreatorId] uniqueidentifier NULL, + [LastModificationTime] datetime2 NULL, + [LastModifierId] uniqueidentifier NULL, + [IsDeleted] bit NOT NULL DEFAULT 0, + [DeletionTime] datetime2 NULL, + [DeleterId] uniqueidentifier NULL, + [TenantId] uniqueidentifier NULL, + [BranchId] uniqueidentifier NULL, + [Name] nvarchar(64) NOT NULL, + [ParentId] uniqueidentifier NULL, + CONSTRAINT [PK_Adm_T_Department] PRIMARY KEY CLUSTERED + ( + [Id] ASC + )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] + ) ON [PRIMARY] +END +GO \ No newline at end of file diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlDataSeeder.cs new file mode 100644 index 0000000..6049e92 --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlDataSeeder.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Sozsoft.Platform.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EntityFrameworkCore; + +namespace Sozsoft.Platform.Data.Seeds; + +/// +/// SqlTableDesigner üzerinden deploy edilen tabloları Seeds/SqlData/*.sql dosyalarından okuyarak veritabanına uygular. +/// Her dosya, tek bir tabloya (veya ilgili ALTER/INDEX script'lerine) ait IF OBJECT_ID kontrolü içeren T-SQL batch'leri içermelidir. +/// Veritabanı silinip yeniden oluşturulduğunda bu seeder tüm tablo scriptlerini yeniden çalıştırır. +/// +public class SqlDataSeeder : IDataSeedContributor, ITransientDependency +{ + private readonly IDbContextProvider _dbContextProvider; + private readonly ILogger _logger; + + public SqlDataSeeder( + IDbContextProvider dbContextProvider, + ILogger logger) + { + _dbContextProvider = dbContextProvider; + _logger = logger; + } + + public async Task SeedAsync(DataSeedContext context) + { + var sqlDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", "SqlData"); + if (!Directory.Exists(sqlDataPath)) + { + _logger.LogInformation("Seeds/SqlData directory not found, skipping SqlDataSeeder."); + return; + } + + var sqlFiles = Directory.GetFiles(sqlDataPath, "*.sql") + .OrderBy(f => Path.GetFileName(f)) + .ToArray(); + + if (sqlFiles.Length == 0) + { + _logger.LogInformation("No .sql files found in Seeds/SqlData directory, skipping SqlDataSeeder."); + return; + } + + _logger.LogInformation("SqlDataSeeder started. {Count} file(s) to be processed.", sqlFiles.Length); + + var dbContext = await _dbContextProvider.GetDbContextAsync(); + + foreach (var filePath in sqlFiles) + { + var fileName = Path.GetFileName(filePath); + try + { + var sqlContent = await File.ReadAllTextAsync(filePath); + + // Split by GO batch separators (SQL Server convention) + var batches = Regex.Split(sqlContent, @"^\s*GO\s*$", + RegexOptions.Multiline | RegexOptions.IgnoreCase); + + foreach (var batch in batches) + { + var sql = batch.Trim(); + if (string.IsNullOrWhiteSpace(sql)) + continue; + + try + { + var (action, objectName, objectType) = ExtractSqlInfo(sql); + + if (!string.IsNullOrEmpty(objectName)) + _logger.LogInformation("Executing: {Action} {ObjectType} {ObjectName}", action, objectType, objectName); + else + _logger.LogInformation("Executing SQL batch from {FileName}", fileName); + + await dbContext.Database.ExecuteSqlRawAsync(sql); + } + catch (Exception ex) + { + _logger.LogError(ex, "SQL batch failed in file {FileName}: {Sql}", fileName, sql); + throw; + } + } + } + catch (Exception ex) when (ex is not InvalidOperationException) + { + _logger.LogError(ex, "Failed to process SQL seed file: {FileName}", fileName); + throw; + } + } + + _logger.LogInformation("SqlDataSeeder completed. {Count} file(s) processed.", sqlFiles.Length); + } + + private static (string Action, string? ObjectName, string? ObjectType) ExtractSqlInfo(string sql) + { + var patterns = new[] + { + (@"CREATE\s+TABLE\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "TABLE"), + (@"ALTER\s+TABLE\s+(\[?\w+\]?\.?\[?\w+\]?)", "ALTER", "TABLE"), + (@"CREATE\s+VIEW\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "VIEW"), + (@"CREATE\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "PROCEDURE"), + (@"ALTER\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "ALTER", "PROCEDURE"), + (@"CREATE\s+OR\s+ALTER\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE/ALTER", "PROCEDURE"), + (@"CREATE\s+FUNCTION\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "FUNCTION"), + (@"CREATE\s+(UNIQUE\s+)?INDEX\s+(\[?\w+\]?)", "CREATE", "INDEX"), + (@"CREATE\s+TRIGGER\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "TRIGGER"), + (@"DROP\s+TABLE\s+(\[?\w+\]?\.?\[?\w+\]?)", "DROP", "TABLE"), + (@"DROP\s+VIEW\s+(\[?\w+\]?\.?\[?\w+\]?)", "DROP", "VIEW"), + (@"DROP\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "DROP", "PROCEDURE"), + }; + + foreach (var (pattern, action, type) in patterns) + { + var match = Regex.Match(sql, pattern, RegexOptions.IgnoreCase); + if (match.Success) + { + var captured = match.Groups[match.Groups.Count - 1].Value; + return (action, captured, type); + } + } + + return ("UNKNOWN", null, null); + } +} diff --git a/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj b/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj index e0b243c..0f670dd 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj +++ b/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj @@ -81,6 +81,10 @@ PreserveNewest Always + + PreserveNewest + Always + diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/SqlTables.sql b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/SqlTables.sql deleted file mode 100644 index a8b4b91..0000000 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/SqlTables.sql +++ /dev/null @@ -1,76 +0,0 @@ -IF OBJECT_ID(N'[dbo].[Adm_T_Behavior]', 'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Adm_T_Behavior]( - [Id] [nvarchar](128) NOT NULL, - [TenantId] [uniqueidentifier] NULL, - [Name] [nvarchar](128) NOT NULL, - [CreationTime] [datetime2](7) NOT NULL, - [CreatorId] [uniqueidentifier] NULL, - [LastModificationTime] [datetime2](7) NULL, - [LastModifierId] [uniqueidentifier] NULL, - [IsDeleted] [bit] NOT NULL, - [DeleterId] [uniqueidentifier] NULL, - [DeletionTime] [datetime2](7) NULL, - CONSTRAINT [PK_Adm_T_Behavior] PRIMARY KEY CLUSTERED - ( - [Id] ASC - )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] - ) ON [PRIMARY] -END -GO - -IF NOT EXISTS ( - SELECT 1 - FROM sys.default_constraints dc - JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id - WHERE dc.parent_object_id = OBJECT_ID(N'[dbo].[Adm_T_Behavior]') - AND c.name = N'IsDeleted' -) -BEGIN - ALTER TABLE [dbo].[Adm_T_Behavior] ADD DEFAULT (CONVERT([bit],(0))) FOR [IsDeleted] -END -GO - -CREATE OR ALTER PROCEDURE [dbo].[Adm_T_DatabaseBackup] -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @SQL NVARCHAR(MAX) = N''; - - SELECT @SQL = @SQL + N' -BACKUP DATABASE ' + QUOTENAME(name) + N' -TO DISK = N''/var/opt/mssql/backup/' -+ name + N'_' + CONVERT(VARCHAR(8), GETDATE(), 112) + N'.bak'' -WITH INIT;' - FROM sys.databases - WHERE name NOT IN ('master','model','msdb','tempdb') - AND state_desc = 'ONLINE'; - - EXEC sp_executesql @SQL; -END -GO - -IF OBJECT_ID(N'[dbo].[Adm_T_Department]', 'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Adm_T_Department] - ( - [Id] uniqueidentifier NOT NULL DEFAULT NEWID(), - [CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(), - [CreatorId] uniqueidentifier NULL, - [LastModificationTime] datetime2 NULL, - [LastModifierId] uniqueidentifier NULL, - [IsDeleted] bit NOT NULL DEFAULT 0, - [DeletionTime] datetime2 NULL, - [DeleterId] uniqueidentifier NULL, - [TenantId] uniqueidentifier NULL, - [BranchId] uniqueidentifier NULL, - [Name] nvarchar(64) NOT NULL, - [ParentId] uniqueidentifier NULL, - CONSTRAINT [PK_Adm_T_Department] PRIMARY KEY CLUSTERED - ( - [Id] ASC - )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] - ) ON [PRIMARY] -END -GO diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/SqlTablesSeeder.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/SqlTablesSeeder.cs deleted file mode 100644 index 2fed0bb..0000000 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/SqlTablesSeeder.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Sozsoft.Platform.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EntityFrameworkCore; - -namespace Sozsoft.Platform.Data.Seeds; - -/// -/// Sql tables seeder for creating or updating database tables based on the current entity definitions. This seeder ensures that the database schema is in sync with the application's data model, allowing for smooth migrations and updates without manual intervention. -/// -public class SqlTablesSeeder : IDataSeedContributor, ITransientDependency -{ - private readonly IDbContextProvider _dbContextProvider; - private readonly ILogger _logger; - - public SqlTablesSeeder( - IDbContextProvider dbContextProvider, - ILogger logger) - { - _dbContextProvider = dbContextProvider; - _logger = logger; - } - - public async Task SeedAsync(DataSeedContext context) - { - var assemblyLocation = Path.GetDirectoryName(typeof(SqlTablesSeeder).Assembly.Location)!; - var sqlFilePath = Path.Combine(assemblyLocation, "Seeds", "SqlTables.sql"); - - if (!File.Exists(sqlFilePath)) - { - _logger.LogWarning("SqlTables.sql file not found at {Path}. Skipping SQL table seeding.", sqlFilePath); - return; - } - - var sqlContent = await File.ReadAllTextAsync(sqlFilePath); - - // Split by GO statements (SQL Server batch separator) - var batches = Regex.Split(sqlContent, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - var dbContext = await _dbContextProvider.GetDbContextAsync(); - - _logger.LogInformation("Starting database script seeding..."); - - foreach (var batch in batches) - { - var sql = batch.Trim(); - if (string.IsNullOrWhiteSpace(sql)) - continue; - - try - { - var (action, objectName, objectType) = ExtractSqlInfo(sql); - - if (!string.IsNullOrEmpty(objectName)) - { - _logger.LogInformation( - "Executing SQL: {Action} {ObjectType} {ObjectName}", - action, - objectType, - objectName); - } - else - { - _logger.LogInformation("Executing SQL batch (object not detected)"); - } - - await dbContext.Database.ExecuteSqlRawAsync(sql); - } - catch (Exception ex) - { - _logger.LogError(ex, "SQL batch failed: {Sql}", sql); - throw; - } - } - - _logger.LogInformation("Database script seeding completed successfully."); - } - - private (string Action, string? ObjectName, string? ObjectType) ExtractSqlInfo(string sql) - { - var patterns = new[] - { - (@"CREATE\s+TABLE\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "TABLE"), - (@"ALTER\s+TABLE\s+(\[?\w+\]?\.?\[?\w+\]?)", "ALTER", "TABLE"), - - (@"CREATE\s+VIEW\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "VIEW"), - - (@"CREATE\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "PROCEDURE"), - (@"ALTER\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "ALTER", "PROCEDURE"), - (@"CREATE\s+OR\s+ALTER\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE/ALTER", "PROCEDURE"), - - (@"CREATE\s+FUNCTION\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "FUNCTION"), - - (@"CREATE\s+INDEX\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "INDEX"), - - (@"CREATE\s+TRIGGER\s+(\[?\w+\]?\.?\[?\w+\]?)", "CREATE", "TRIGGER"), - - (@"DROP\s+TABLE\s+(\[?\w+\]?\.?\[?\w+\]?)", "DROP", "TABLE"), - (@"DROP\s+VIEW\s+(\[?\w+\]?\.?\[?\w+\]?)", "DROP", "VIEW"), - (@"DROP\s+PROCEDURE\s+(\[?\w+\]?\.?\[?\w+\]?)", "DROP", "PROCEDURE") - }; - - foreach (var (pattern, action, type) in patterns) - { - var match = Regex.Match(sql, pattern, RegexOptions.IgnoreCase); - if (match.Success) - { - return (action, match.Groups[1].Value, type); - } - } - - return ("UNKNOWN", null, null); - } -} - diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj b/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj index 0ee77f8..6ad3fdf 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj @@ -21,11 +21,6 @@ PreserveNewest Always - - - PreserveNewest - Always - diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs index 3183582..b7e0b70 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs @@ -59,8 +59,8 @@ public class SetupController : ControllerBase var migratorPath = _configuration["Setup:MigratorPath"] ?? Path.GetFullPath(Path.Combine(_env.ContentRootPath, "..", "Sozsoft.Platform.DbMigrator")); - await Send("info", "Veritabanı migration ve seed başlatılıyor..."); - await Send("info", $"Migrator yolu: {migratorPath}"); + await Send("info", "Database migration and seeding are being initiated..."); + await Send("info", $"Migrator path: {migratorPath}"); var extraArgs = _configuration["Setup:MigratorArgs"] ?? "--Seed=true"; diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs index ddcc013..68f3879 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs @@ -152,8 +152,8 @@ internal static class SetupAppRunner var migratorPath = cfg["Setup:MigratorPath"] ?? Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "Sozsoft.Platform.DbMigrator")); - await Send("info", "Veritabanı migration ve seed başlatılıyor..."); - await Send("info", $"Migrator yolu: {migratorPath}"); + await Send("info", "Database migration and seeding are being initiated..."); + await Send("info", $"Migrator path: {migratorPath}"); var extraArgs = cfg["Setup:MigratorArgs"] ?? "--Seed=true"; diff --git a/ui/src/services/sql-query-manager.service.ts b/ui/src/services/sql-query-manager.service.ts index 0f25dc9..5fac552 100644 --- a/ui/src/services/sql-query-manager.service.ts +++ b/ui/src/services/sql-query-manager.service.ts @@ -48,6 +48,26 @@ export class SqlObjectManagerService { }, { apiName: this.apiName, ...config }, ) + + saveTableScript = (input: { fileName: string; sqlScript: string }, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-object-manager/save-table-script', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + deleteSqlDataFiles = (input: { fileNames: string[] }, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-object-manager/delete-sql-data-files', + data: input, + }, + { apiName: this.apiName, ...config }, + ) } export const sqlObjectManagerService = new SqlObjectManagerService() diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx index a3024dd..3569954 100644 --- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx @@ -251,7 +251,7 @@ const WizardFileManager = () => { >

{translate('::App.DbMigrate.ConfirmMessage') || - 'Veritabanı migration işlemini başlatmak istediğinize emin misiniz?'} + 'Are you sure you want to start the database migration process?'}

diff --git a/ui/src/views/developerKit/SqlObjectExplorer.tsx b/ui/src/views/developerKit/SqlObjectExplorer.tsx index 9ce2d7a..eb0d4c3 100644 --- a/ui/src/views/developerKit/SqlObjectExplorer.tsx +++ b/ui/src/views/developerKit/SqlObjectExplorer.tsx @@ -257,7 +257,17 @@ const SqlObjectExplorer = ({ node.folder === 'views' ? 'VIEW' : node.folder === 'procedures' ? 'PROCEDURE' : 'FUNCTION' - return `DROP ${keyword} ${obj.fullName ?? `[${obj.schemaName}].[${obj.objectName}]`};` + return `DROP ${keyword} ${obj.fullName ?? `[${obj.schemaName}].[${obj. objectName}]`};` + } + + const getSqlDataFileCandidates = (node: TreeNode): string[] => { + if (node.folder === 'tables') { + const t = node.data as DatabaseTableDto + return [t.tableName, `${t.schemaName}_${t.tableName}`] + } + + const obj = node.data as SqlNativeObjectDto + return [obj.objectName, `${obj.schemaName}_${obj.objectName}`] } const handleDrop = async () => { @@ -268,6 +278,17 @@ const SqlObjectExplorer = ({ queryText: buildDropSql(dropConfirm.node), dataSourceCode: dataSource, }) + + // If a matching seed file exists under DbMigrator/Seeds/SqlData, delete it too. + try { + const fileNames = [...new Set(getSqlDataFileCandidates(dropConfirm.node).filter(Boolean))] + if (fileNames.length > 0) { + await sqlObjectManagerService.deleteSqlDataFiles({ fileNames }) + } + } catch { + // Non-blocking: object drop succeeded even if seed file cleanup fails. + } + setDropConfirm(null) loadObjects() } catch (err: any) { @@ -336,6 +357,22 @@ const SqlObjectExplorer = ({ const filteredTree = filterTree(treeData, filterText) + useEffect(() => { + if (filterText.trim()) { + const allIds = new Set() + const collect = (nodes: TreeNode[]) => { + for (const node of nodes) { + allIds.add(node.id) + if (node.children?.length) collect(node.children) + } + } + collect(filteredTree) + setExpandedNodes(allIds) + } else { + setExpandedNodes(new Set(['root'])) + } + }, [filterText]) + // Context menu items per folder const ctxNode = contextMenu.node const isTableObj = ctxNode?.type === 'object' && ctxNode.folder === 'tables' diff --git a/ui/src/views/developerKit/SqlQueryManager.tsx b/ui/src/views/developerKit/SqlQueryManager.tsx index dd7b2eb..2170eff 100644 --- a/ui/src/views/developerKit/SqlQueryManager.tsx +++ b/ui/src/views/developerKit/SqlQueryManager.tsx @@ -1049,7 +1049,7 @@ GO`, }} >

- {translate('::App.DbMigrate.ConfirmMessage') || 'Veritabanı migration işlemini başlatmak istediğinize emin misiniz?'} + {translate('::App.DbMigrate.ConfirmMessage') || 'Are you sure you want to start the database migration process?'}

diff --git a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx index 74f396b..08661bf 100644 --- a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx +++ b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx @@ -1138,7 +1138,13 @@ const SqlTableDesignerDialog = ({ // ── Column operations ────────────────────────────────────────────────────── - const addColumn = () => setColumns((prev) => [...prev, createEmptyColumn()]) + const [newRowId, setNewRowId] = useState(null) + + const addColumn = () => { + const col = createEmptyColumn() + setColumns((prev) => [...prev, col]) + setNewRowId(col.id) + } const clearAllColumns = () => setColumns([createEmptyColumn()]) @@ -1516,8 +1522,15 @@ const SqlTableDesignerDialog = ({ } setIsDeploying(true) try { + // Strip GO batch separators — they are SSMS-only and not valid T-SQL + const executableSql = generatedSql + .split(/^\s*GO\s*$/im) + .map((b) => b.trim()) + .filter(Boolean) + .join('\n') + const result = await sqlObjectManagerService.executeQuery({ - queryText: generatedSql, + queryText: executableSql, dataSourceCode: dataSource, }) if (result.data.success) { @@ -1528,6 +1541,18 @@ const SqlTableDesignerDialog = ({ , { placement: 'top-center' }, ) + // Save seed file to DbMigrator/Seeds/SqlData after successful deploy. + // Always use the full CREATE TABLE script (not the ALTER diff) so the seed file + // can recreate the table from scratch when the database is wiped. + try { + const seedSql = generateCreateTableSql(columns, settings, relationships, indexes) + await sqlObjectManagerService.saveTableScript({ + fileName: deployedTable, + sqlScript: seedSql, + }) + } catch { + // Non-blocking: seed file save failure does not affect deploy success + } onDeployed?.() handleClose() } else { @@ -1731,6 +1756,12 @@ const SqlTableDesignerDialog = ({ placeholder={translate('::App.SqlQueryManager.ColumnNamePlaceholder')} value={col.columnName} onChange={(e) => updateColumn(col.id, 'columnName', e.target.value)} + ref={(el) => { + if (el && col.id === newRowId) { + el.focus() + setNewRowId(null) + } + }} />