using Microsoft.EntityFrameworkCore; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; using Volo.Abp.Application.Dtos; using System.Text; using Volo.Abp.Domain.Entities; using Kurs.Platform.Entities; using Kurs.Platform.DeveloperKit; using System; using System.Threading.Tasks; using System.Collections.Generic; using System.Linq; using Kurs.Platform.Domain.DeveloperKit; namespace Platform.Api.Application; public class CrudMigrationAppService : CrudAppService< CrudMigration, CrudMigrationDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateCrudMigrationDto>, ICrudMigrationAppService { private readonly IRepository _migrationRepository; private readonly IRepository _entityRepository; private readonly IRepository _fieldRepository; private readonly IApiMigrationRepository _customSqlExecutor; public CrudMigrationAppService( IRepository migrationRepository, IRepository entityRepository, IRepository fieldRepository, IApiMigrationRepository customSqlExecutor ) : base(migrationRepository) { _migrationRepository = migrationRepository; _entityRepository = entityRepository; _fieldRepository = fieldRepository; _customSqlExecutor = customSqlExecutor; } public virtual async Task ApplyMigrationAsync(Guid id) { var migration = await _migrationRepository.GetAsync(id); try { await _customSqlExecutor.ExecuteSqlAsync(migration.SqlScript); migration.Status = "applied"; migration.AppliedAt = DateTime.UtcNow; migration.ErrorMessage = null; await _migrationRepository.UpdateAsync(migration, autoSave: true); var entity = await _entityRepository.GetAsync(migration.EntityId); entity.MigrationStatus = "applied"; await _entityRepository.UpdateAsync(entity, autoSave: true); return ObjectMapper.Map(migration); } catch (Exception ex) { migration.Status = "failed"; migration.ErrorMessage = ex.Message; await _migrationRepository.UpdateAsync(migration, autoSave: true); var entity = await _entityRepository.GetAsync(migration.EntityId); entity.MigrationStatus = "failed"; await _entityRepository.UpdateAsync(entity, autoSave: true); throw; } } public virtual async Task> GetPendingMigrationsAsync() { var queryable = await _migrationRepository.GetQueryableAsync(); var pending = await queryable .Where(m => m.Status == "pending") .OrderBy(m => m.CreationTime) .ToListAsync(); return ObjectMapper.Map, List>(pending); } public virtual async Task GenerateMigrationAsync(Guid entityId) { var queryable = await _entityRepository.GetQueryableAsync(); var entity = await queryable .Include(x => x.Fields) .FirstOrDefaultAsync(x => x.Id == entityId); if (entity == null) throw new EntityNotFoundException($"CustomEntity with id {entityId} not found"); // Check if there are any previously applied migrations for this entity var migrationQueryable = await _migrationRepository.GetQueryableAsync(); var lastAppliedMigration = await migrationQueryable .Where(m => m.EntityId == entityId && m.Status == "applied") .OrderByDescending(m => m.CreationTime) .FirstOrDefaultAsync(); // Use "Update" if migrations were applied before, otherwise "Create" var operationType = lastAppliedMigration != null ? "Update" : "Create"; var fileName = $"{DateTime.UtcNow:yyyyMMddHHmmss}_{operationType}{entity.Name}Table.sql"; var sqlScript = lastAppliedMigration != null ? GenerateAlterTableScript(entity, lastAppliedMigration) : GenerateCreateTableScript(entity); var migration = new CrudMigration { EntityId = entityId, EntityName = entity.Name, FileName = fileName, SqlScript = sqlScript, Status = "pending" }; migration = await _migrationRepository.InsertAsync(migration, autoSave: true); entity.MigrationId = migration.Id; entity.MigrationStatus = "pending"; await _entityRepository.UpdateAsync(entity, autoSave: true); return ObjectMapper.Map(migration); } private string GenerateCreateTableScript(CustomEntity entity) { if (entity.Fields == null || !entity.Fields.Any()) throw new InvalidOperationException($"Entity '{entity.Name}' does not have any fields defined."); var sb = new StringBuilder(); sb.AppendLine($@"/* # Create {entity.DisplayName} table 1. New Table - [{entity.TableName}]"); foreach (var field in entity.Fields) { sb.AppendLine($" - [{field.Name}] ({field.Type}{(field.IsRequired ? ", required" : "")})"); } sb.AppendLine("*/"); sb.AppendLine($"\nIF OBJECT_ID(N'{entity.TableName}', N'U') IS NULL"); sb.AppendLine($"CREATE TABLE [{entity.TableName}] ("); sb.AppendLine(" [Id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),"); foreach (var field in entity.Fields) { var sqlType = GetSqlType(field.Type, field.MaxLength); sb.Append($" [{field.Name}] {sqlType}"); if (field.IsRequired) sb.Append(" NOT NULL"); if (!string.IsNullOrWhiteSpace(field.DefaultValue)) sb.Append($" DEFAULT {FormatDefaultValue(field.Type, field.DefaultValue)}"); sb.AppendLine(","); } // Add TenantId if entity is multi-tenant if (entity.IsMultiTenant) { sb.AppendLine(" [TenantId] UNIQUEIDENTIFIER NULL,"); } // Add Full Audited Entity fields (includes both audit and soft delete) if (entity.IsFullAuditedEntity) { sb.AppendLine(" [CreationTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,"); sb.AppendLine(" [CreatorId] UNIQUEIDENTIFIER NULL,"); sb.AppendLine(" [LastModificationTime] DATETIME2 NULL,"); sb.AppendLine(" [LastModifierId] UNIQUEIDENTIFIER NULL,"); sb.AppendLine(" [IsDeleted] BIT DEFAULT 0 NOT NULL,"); sb.AppendLine(" [DeleterId] UNIQUEIDENTIFIER NULL,"); sb.AppendLine(" [DeletionTime] DATETIME2 NULL,"); } // Remove last comma var script = sb.ToString().TrimEnd(',', '\r', '\n'); script += "\n);\n"; return script; } private string GenerateAlterTableScript(CustomEntity entity, CrudMigration lastAppliedMigration) { if (entity.Fields == null || !entity.Fields.Any()) throw new InvalidOperationException($"Entity '{entity.Name}' does not have any fields defined."); // Parse the last applied migration's SQL to understand what fields existed var existingFieldNames = ExtractFieldNamesFromCreateScript(lastAppliedMigration.SqlScript); var sb = new StringBuilder(); sb.AppendLine($@"/* # Update {entity.DisplayName} table 1. Alter Table - [{entity.TableName}] - Adding new fields or modifying existing ones"); sb.AppendLine("*/\n"); // Find new fields that need to be added var newFields = entity.Fields.Where(f => !existingFieldNames.Contains(f.Name)).ToList(); if (newFields.Any()) { foreach (var field in newFields) { var sqlType = GetSqlType(field.Type, field.MaxLength); sb.Append($"IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[{entity.TableName}]') AND name = '{field.Name}')\n"); sb.Append($"BEGIN\n"); sb.Append($" ALTER TABLE [{entity.TableName}] ADD [{field.Name}] {sqlType}"); if (field.IsRequired) sb.Append(" NOT NULL"); else sb.Append(" NULL"); if (!string.IsNullOrWhiteSpace(field.DefaultValue)) sb.Append($" DEFAULT {FormatDefaultValue(field.Type, field.DefaultValue)}"); sb.AppendLine(";"); sb.AppendLine($"END\n"); } } else { sb.AppendLine("-- No new fields to add"); } return sb.ToString(); } private HashSet ExtractFieldNamesFromCreateScript(string sqlScript) { var fieldNames = new HashSet(StringComparer.OrdinalIgnoreCase); // Simple parsing - extract field names between [ and ] var lines = sqlScript.Split('\n'); foreach (var line in lines) { var trimmedLine = line.Trim(); if (trimmedLine.StartsWith("[") && trimmedLine.Contains("]")) { var fieldName = trimmedLine.Substring(1, trimmedLine.IndexOf("]") - 1); if (fieldName != "Id" && fieldName != "TenantId" && fieldName != "CreationTime" && fieldName != "CreatorId" && fieldName != "LastModificationTime" && fieldName != "LastModifierId" && fieldName != "IsDeleted" && fieldName != "DeleterId" && fieldName != "DeletionTime") { fieldNames.Add(fieldName); } } } return fieldNames; } private string GetSqlType(string fieldType, int? maxLength) { return fieldType.ToLower() switch { "string" => maxLength.HasValue ? $"NVARCHAR({maxLength.Value})" : "NVARCHAR(MAX)", "number" => "INT", "decimal" => "DECIMAL(18, 2)", "boolean" => "BIT", "date" => "DATETIME2", "guid" => "UNIQUEIDENTIFIER", _ => "NVARCHAR(MAX)" }; } private string FormatDefaultValue(string fieldType, string defaultValue) { return fieldType.ToLower() switch { "string" => $"N'{defaultValue}'", "date" => $"'{defaultValue}'", // ISO format beklenir "guid" => $"'{defaultValue}'", "boolean" => (defaultValue.ToLower() == "true" || defaultValue == "1") ? "1" : "0", _ => defaultValue }; } }