SqlData ve WizardFileManager güncellemeleri

This commit is contained in:
Sedat Öztürk 2026-05-03 22:50:51 +03:00
parent 10bf95da66
commit e01875b7c9
19 changed files with 486 additions and 220 deletions

View file

@ -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
```

View file

@ -27,4 +27,16 @@ public interface ISqlObjectManagerAppService : IApplicationService
/// Gets the SQL definition/body of a native SQL Server object (Stored Procedure, View, or Function)
/// </summary>
Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName);
/// <summary>
/// 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.
/// </summary>
Task SaveTableScriptAsync(SaveTableScriptDto input);
/// <summary>
/// Deletes matching SQL seed files from Seeds/SqlData when objects are dropped from the UI.
/// Non-existing files are ignored.
/// </summary>
Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input);
}

View file

@ -18,3 +18,31 @@ public class SqlQueryExecutionResultDto
public long ExecutionTimeMs { get; set; }
public Dictionary<string, object> Metadata { get; set; }
}
/// <summary>
/// Input for saving a T-SQL script file to DbMigrator Seeds/SqlData.
/// </summary>
public class SaveTableScriptDto
{
/// <summary>
/// File name without extension (e.g. "Adm_T_Behavior"). Must not contain path separators.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// The T-SQL script content (CREATE TABLE / ALTER TABLE etc.).
/// </summary>
public string SqlScript { get; set; }
}
/// <summary>
/// Input for deleting seed files from DbMigrator Seeds/SqlData.
/// </summary>
public class DeleteSqlDataFilesDto
{
/// <summary>
/// File names without extension (e.g. "Adm_T_Behavior").
/// Any invalid or unsafe values are ignored.
/// </summary>
public List<string> FileNames { get; set; } = new();
}

View file

@ -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<SqlObjectManagerAppService> _logger;
public SqlObjectManagerAppService(
ISqlExecutorService sqlExecutorService,
ISqlTemplateProvider templateProvider,
ICurrentTenant currentTenant,
IHttpContextAccessor httpContextAccessor)
IHttpContextAccessor httpContextAccessor,
IHostEnvironment hostEnvironment,
ILogger<SqlObjectManagerAppService> logger)
{
_sqlExecutorService = sqlExecutorService;
_templateProvider = templateProvider;
_currentTenant = currentTenant;
_httpContextAccessor = httpContextAccessor;
_hostEnvironment = hostEnvironment;
_logger = logger;
}
private string GetTenantFromHeader()
@ -155,6 +166,16 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
public async Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input)
{
ValidateTenantAccess();
// 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,
@ -162,6 +183,22 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
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<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName)
{
ValidateTenantAccess();
@ -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");
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
{
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly ILogger<SqlDataSeeder> _logger;
public SqlDataSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider,
ILogger<SqlDataSeeder> 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);
}
}

View file

@ -81,6 +81,10 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\SqlData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View file

@ -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

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
public class SqlTablesSeeder : IDataSeedContributor, ITransientDependency
{
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly ILogger<SqlTablesSeeder> _logger;
public SqlTablesSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider,
ILogger<SqlTablesSeeder> 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);
}
}

View file

@ -21,11 +21,6 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="Seeds\SqlTables.sql" />
<Content Include="Seeds\SqlTables.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View file

@ -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";

View file

@ -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";

View file

@ -48,6 +48,26 @@ export class SqlObjectManagerService {
},
{ apiName: this.apiName, ...config },
)
saveTableScript = (input: { fileName: string; sqlScript: string }, config?: Partial<Config>) =>
apiService.fetchData<void, { fileName: string; sqlScript: string }>(
{
method: 'POST',
url: '/api/app/sql-object-manager/save-table-script',
data: input,
},
{ apiName: this.apiName, ...config },
)
deleteSqlDataFiles = (input: { fileNames: string[] }, config?: Partial<Config>) =>
apiService.fetchData<void, { fileNames: string[] }>(
{
method: 'POST',
url: '/api/app/sql-object-manager/delete-sql-data-files',
data: input,
},
{ apiName: this.apiName, ...config },
)
}
export const sqlObjectManagerService = new SqlObjectManagerService()

View file

@ -251,7 +251,7 @@ const WizardFileManager = () => {
>
<p className="text-gray-600 dark:text-gray-400">
{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?'}
</p>
</ConfirmDialog>

View file

@ -260,6 +260,16 @@ const SqlObjectExplorer = ({
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 () => {
if (!dropConfirm || !dataSource) return
setDropping(true)
@ -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<string>()
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'

View file

@ -1049,7 +1049,7 @@ GO`,
}}
>
<p className="text-gray-600 dark:text-gray-400">
{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?'}
</p>
</ConfirmDialog>

View file

@ -1138,7 +1138,13 @@ const SqlTableDesignerDialog = ({
// ── Column operations ──────────────────────────────────────────────────────
const addColumn = () => setColumns((prev) => [...prev, createEmptyColumn()])
const [newRowId, setNewRowId] = useState<string | null>(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 = ({
</Notification>,
{ 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)
}
}}
/>
</div>
<div className="col-span-3">