erp-platform/api/src/Kurs.Platform.Application/DeveloperKit/CrudMigrationAppService.cs

302 lines
11 KiB
C#
Raw Normal View History

2025-08-11 06:34:44 +00:00
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;
2025-11-05 09:02:16 +00:00
public class CrudMigrationAppService : CrudAppService<
CrudMigration,
CrudMigrationDto,
2025-08-11 06:34:44 +00:00
Guid,
PagedAndSortedResultRequestDto,
2025-11-05 09:02:16 +00:00
CreateUpdateCrudMigrationDto>, ICrudMigrationAppService
2025-08-11 06:34:44 +00:00
{
2025-11-05 09:02:16 +00:00
private readonly IRepository<CrudMigration, Guid> _migrationRepository;
2025-08-11 06:34:44 +00:00
private readonly IRepository<CustomEntity, Guid> _entityRepository;
private readonly IRepository<CustomEntity, Guid> _fieldRepository;
private readonly IApiMigrationRepository _customSqlExecutor;
2025-11-05 09:02:16 +00:00
public CrudMigrationAppService(
IRepository<CrudMigration, Guid> migrationRepository,
2025-08-11 06:34:44 +00:00
IRepository<CustomEntity, Guid> entityRepository,
IRepository<CustomEntity, Guid> fieldRepository,
IApiMigrationRepository customSqlExecutor
) : base(migrationRepository)
{
_migrationRepository = migrationRepository;
_entityRepository = entityRepository;
_fieldRepository = fieldRepository;
_customSqlExecutor = customSqlExecutor;
}
2025-11-05 09:02:16 +00:00
public virtual async Task<CrudMigrationDto> ApplyMigrationAsync(Guid id)
2025-08-11 06:34:44 +00:00
{
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);
2025-11-05 09:02:16 +00:00
return ObjectMapper.Map<CrudMigration, CrudMigrationDto>(migration);
2025-08-11 06:34:44 +00:00
}
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;
}
}
2025-11-05 09:02:16 +00:00
public virtual async Task<List<CrudMigrationDto>> GetPendingMigrationsAsync()
2025-08-11 06:34:44 +00:00
{
var queryable = await _migrationRepository.GetQueryableAsync();
var pending = await queryable
.Where(m => m.Status == "pending")
.OrderBy(m => m.CreationTime)
.ToListAsync();
2025-11-05 09:02:16 +00:00
return ObjectMapper.Map<List<CrudMigration>, List<CrudMigrationDto>>(pending);
2025-08-11 06:34:44 +00:00
}
2025-11-05 09:02:16 +00:00
public virtual async Task<CrudMigrationDto> GenerateMigrationAsync(Guid entityId)
2025-08-11 06:34:44 +00:00
{
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");
2025-11-05 14:52:01 +00:00
// 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();
2025-08-11 06:34:44 +00:00
2025-11-05 14:52:01 +00:00
// 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);
2025-08-11 06:34:44 +00:00
2025-11-05 09:02:16 +00:00
var migration = new CrudMigration
2025-08-11 06:34:44 +00:00
{
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);
2025-11-05 09:02:16 +00:00
return ObjectMapper.Map<CrudMigration, CrudMigrationDto>(migration);
2025-08-11 06:34:44 +00:00
}
2025-11-05 14:52:01 +00:00
private string GenerateCreateTableScript(CustomEntity entity)
2025-08-11 06:34:44 +00:00
{
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(",");
}
2025-11-05 14:52:01 +00:00
// Add TenantId if entity is multi-tenant
if (entity.IsMultiTenant)
2025-08-11 06:34:44 +00:00
{
2025-11-05 14:52:01 +00:00
sb.AppendLine(" [TenantId] UNIQUEIDENTIFIER NULL,");
2025-08-11 06:34:44 +00:00
}
2025-11-05 14:52:01 +00:00
// Add Full Audited Entity fields (includes both audit and soft delete)
if (entity.IsFullAuditedEntity)
2025-08-11 06:34:44 +00:00
{
2025-11-05 14:52:01 +00:00
sb.AppendLine(" [CreationTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,");
sb.AppendLine(" [CreatorId] UNIQUEIDENTIFIER NULL,");
sb.AppendLine(" [LastModificationTime] DATETIME2 NULL,");
sb.AppendLine(" [LastModifierId] UNIQUEIDENTIFIER NULL,");
2025-08-11 06:34:44 +00:00
sb.AppendLine(" [IsDeleted] BIT DEFAULT 0 NOT NULL,");
sb.AppendLine(" [DeleterId] UNIQUEIDENTIFIER NULL,");
2025-11-05 14:52:01 +00:00
sb.AppendLine(" [DeletionTime] DATETIME2 NULL,");
2025-08-11 06:34:44 +00:00
}
// Remove last comma
var script = sb.ToString().TrimEnd(',', '\r', '\n');
script += "\n);\n";
return script;
}
2025-11-05 14:52:01 +00:00
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<string> ExtractFieldNamesFromCreateScript(string sqlScript)
{
var fieldNames = new HashSet<string>(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;
}
2025-08-11 06:34:44 +00:00
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
};
}
}