using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Localization;
using Volo.Abp;
using Volo.Abp.Domain.Services;
namespace Sozsoft.Mcp.Services;
///
/// Yeni tabloyu SQL Query Manager'daki tablo tasarimcisiyla ayni sozlesmeyle kurar: script
/// tasarimcinin generateCreateTableSql ciktisiyla ayni bicimdedir ve tasarimcinin deploy
/// sonrasi yazdigi yere ({kapsam}/sql/object/{Tablo}.sql) yazilir; boylece tablo
/// tasarimcida acilip duzenlenebilir ve veritabani sifirlandiginda seed ile geri gelir.
///
/// Yalnizca olusturma yapar. Var olan tabloya dokunulmaz — kolon ekleme/degistirme tasarimcinin
/// isidir ve sema degisikligi kurallarina (kayipsiz ekleme, degistirme/dusurme yok) tabidir.
/// Kolon tipi ve varsayilan deger serbest metin degil, beyaz listeden gecer; istemci SQL yazamaz.
///
///
public interface IMcpTableCreator
{
Task CreateAsync(McpTableDefinition definition);
}
public class McpTableCreator(
IMcpQueryExecutor queryExecutor,
IConfiguration configuration,
IStringLocalizer localizer) : DomainService, IMcpTableCreator
{
private const string SchemaName = "dbo";
private const int DefaultTextLength = 100;
private const int MaxTextLength = 4000;
private static readonly Regex SafeIdentifierRegex = new(
@"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
///
/// Varsayilan deger olarak kabul edilenler: sayi, metin sabiti ve tasarimcinin kendi
/// sablonlarinda kullandigi fonksiyonlar. Baska bir ifade script'e hic girmez.
///
private static readonly Regex SafeDefaultRegex = new(
@"^(?:-?\d{1,18}(?:\.\d{1,18})?|N?'(?:[^']|'')*'|NEWID\(\)|GETDATE\(\)|GETUTCDATE\(\)|SYSUTCDATETIME\(\))$",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
/// Tasarimcinin tip listesi (DATA_TYPES) ile aynidir.
private static readonly string[] SupportedTypes =
[
"nvarchar", "nvarchar(MAX)", "int", "bigint", "decimal", "float", "bit",
"datetime", "datetime2", "date", "uniqueidentifier", "money"
];
// Sablon kolonlari tasarimcinin FULL_AUDIT_COLUMNS / TENANT_COLUMN tanimlariyla aynidir.
private static readonly McpTableColumn IdColumn = new("Id", "uniqueidentifier", null, false, "NEWID()");
private static readonly McpTableColumn TenantColumn = new("TenantId", "uniqueidentifier", null, true, null);
private static readonly McpTableColumn[] AuditColumns =
[
new("CreationTime", "datetime2", null, false, "GETUTCDATE()"),
new("CreatorId", "uniqueidentifier", null, true, null),
new("LastModificationTime", "datetime2", null, true, null),
new("LastModifierId", "uniqueidentifier", null, true, null),
new("IsDeleted", "bit", null, false, "0"),
new("DeletionTime", "datetime2", null, true, null),
new("DeleterId", "uniqueidentifier", null, true, null)
];
public async Task CreateAsync(McpTableDefinition definition)
{
Check.NotNull(definition, nameof(definition));
var tableName = (definition.TableName ?? string.Empty).Trim();
EnsureSafeIdentifier(tableName);
var columns = BuildColumns(definition);
var context = await queryExecutor.ResolveDataSourceAsync();
if (context.DataSourceType != DataSourceTypeEnum.Mssql)
{
throw new UserFriendlyException(localizer[McpErrorCodes.TableProviderNotSupported, context.DataSourceType.ToString()]);
}
var existing = await context.Repository.ExecuteScalarAsync(
"SELECT OBJECT_ID(@name, 'U')",
context.ConnectionString,
new Dictionary { ["name"] = $"{SchemaName}.{tableName}" });
if (existing is not null)
{
throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, tableName]);
}
var script = BuildScript(tableName, columns);
// GO yalnizca SSMS ayiricidir; tek batch oldugu icin calistirirken atilir, dosyada kalir.
await context.Repository.ExecuteAsync(
string.Join('\n', script.Split('\n').Where(line => line.Trim() != "GO")),
context.ConnectionString);
var seedFilePath = SaveSeedScript(tableName, script);
return new McpCreatedTable(tableName, script, seedFilePath);
}
private List BuildColumns(McpTableDefinition definition)
{
if (definition.Columns.Count == 0)
{
throw new UserFriendlyException(localizer[McpErrorCodes.TableColumnsRequired]);
}
var userColumns = definition.Columns.Select(NormalizeColumn).ToList();
var columns = new List { IdColumn };
if (definition.MultiTenant)
{
columns.Add(TenantColumn);
}
columns.AddRange(userColumns);
if (definition.FullAudited)
{
columns.AddRange(AuditColumns);
}
var duplicate = columns
.GroupBy(column => column.Name, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(group => group.Count() > 1);
if (duplicate is not null)
{
throw new UserFriendlyException(localizer[McpErrorCodes.DuplicateColumn, duplicate.Key]);
}
return columns;
}
private McpTableColumn NormalizeColumn(McpTableColumn column)
{
var name = (column.Name ?? string.Empty).Trim();
EnsureSafeIdentifier(name);
var type = SupportedTypes.FirstOrDefault(candidate =>
string.Equals(candidate, column.Type?.Trim(), StringComparison.OrdinalIgnoreCase))
?? throw new UserFriendlyException(
localizer[McpErrorCodes.UnsupportedColumnType, column.Type ?? string.Empty, string.Join(", ", SupportedTypes)]);
int? maxLength = null;
if (type == "nvarchar")
{
maxLength = column.MaxLength ?? DefaultTextLength;
if (maxLength is < 1 or > MaxTextLength)
{
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidColumnLength, name, MaxTextLength]);
}
}
var defaultValue = string.IsNullOrWhiteSpace(column.DefaultValue) ? null : column.DefaultValue.Trim();
if (defaultValue is not null && !SafeDefaultRegex.IsMatch(defaultValue))
{
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidDefaultValue, name, defaultValue]);
}
return new McpTableColumn(name, type, maxLength, column.IsNullable, defaultValue);
}
private static string BuildScript(string tableName, List columns)
{
var fullTableName = $"[{SchemaName}].[{tableName}]";
var lines = new List
{
$"IF OBJECT_ID(N'{fullTableName}', 'U') IS NULL",
"BEGIN",
$" CREATE TABLE {fullTableName}",
" ("
};
lines.AddRange(columns.Select(column => ToColumnLine(column) + ","));
lines.Add($" CONSTRAINT [PK_{tableName}] PRIMARY KEY NONCLUSTERED");
lines.Add(" (");
lines.Add(" [Id] ASC");
lines.Add(" )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]");
lines.Add(" ) ON [PRIMARY]");
lines.Add("END");
lines.Add("GO");
return string.Join('\n', lines);
}
private static string ToColumnLine(McpTableColumn column)
{
var type = column.Type switch
{
"nvarchar" => $"nvarchar({column.MaxLength})",
"decimal" => "decimal(18, 4)",
_ => column.Type
};
var nullPart = column.IsNullable ? "NULL" : "NOT NULL";
var defaultPart = column.DefaultValue is null ? string.Empty : $" DEFAULT {column.DefaultValue}";
return $" [{column.Name}] {type} {nullPart}{defaultPart}";
}
///
/// Tasarimcinin SaveTableScriptAsync ile yazdigi yol: kapsam istegin kiracisini izler.
/// Dosyasiz tablo veritabani sifirlandiginda geri gelmezdi; yazilamazsa islem hata verir.
///
private string SaveSeedScript(string tableName, string script)
{
var outputPath = SeedPathResolver.GetScopePath(
configuration,
CurrentTenant.Id,
SeedPathResolver.GetProviderFolderName(isPostgreSql: false),
SeedPathResolver.ObjectFolder);
Directory.CreateDirectory(outputPath);
var filePath = Path.Combine(outputPath, $"{tableName}.sql");
File.WriteAllText(filePath, script);
return $"{SeedPathResolver.GetScopeFolderName(CurrentTenant.Id)}/{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{tableName}.sql";
}
private void EnsureSafeIdentifier(string identifier)
{
if (!SafeIdentifierRegex.IsMatch(identifier))
{
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidIdentifier, identifier]);
}
}
}
/// Tam audit kolonlari (olusturma/degistirme/silme izi) eklensin mi.
/// TenantId kolonu eklensin mi.
public sealed record McpTableDefinition(
string TableName,
IReadOnlyList Columns,
bool FullAudited,
bool MultiTenant);
/// Yalnizca nvarchar icin; bos ise tasarimcinin varsayilani (100).
public sealed record McpTableColumn(
string Name,
string Type,
int? MaxLength,
bool IsNullable,
string? DefaultValue);
/// Seed kokune gore goreli yol.
public sealed record McpCreatedTable(string TableName, string Script, string SeedFilePath);