SqlData ve WizardFileManager güncellemeleri
This commit is contained in:
parent
10bf95da66
commit
e01875b7c9
19 changed files with 486 additions and 220 deletions
6
.github/instructions/list.instructions.md
vendored
6
.github/instructions/list.instructions.md
vendored
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
131
api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlDataSeeder.cs
Normal file
131
api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlDataSeeder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue