301 lines
11 KiB
C#
301 lines
11 KiB
C#
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<CrudMigration, Guid> _migrationRepository;
|
|
private readonly IRepository<CustomEntity, Guid> _entityRepository;
|
|
private readonly IRepository<CustomEntity, Guid> _fieldRepository;
|
|
private readonly IApiMigrationRepository _customSqlExecutor;
|
|
|
|
public CrudMigrationAppService(
|
|
IRepository<CrudMigration, Guid> migrationRepository,
|
|
IRepository<CustomEntity, Guid> entityRepository,
|
|
IRepository<CustomEntity, Guid> fieldRepository,
|
|
IApiMigrationRepository customSqlExecutor
|
|
) : base(migrationRepository)
|
|
{
|
|
_migrationRepository = migrationRepository;
|
|
_entityRepository = entityRepository;
|
|
_fieldRepository = fieldRepository;
|
|
_customSqlExecutor = customSqlExecutor;
|
|
}
|
|
|
|
public virtual async Task<CrudMigrationDto> 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<CrudMigration, CrudMigrationDto>(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<List<CrudMigrationDto>> GetPendingMigrationsAsync()
|
|
{
|
|
var queryable = await _migrationRepository.GetQueryableAsync();
|
|
var pending = await queryable
|
|
.Where(m => m.Status == "pending")
|
|
.OrderBy(m => m.CreationTime)
|
|
.ToListAsync();
|
|
|
|
return ObjectMapper.Map<List<CrudMigration>, List<CrudMigrationDto>>(pending);
|
|
}
|
|
|
|
|
|
public virtual async Task<CrudMigrationDto> 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<CrudMigration, CrudMigrationDto>(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<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;
|
|
}
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|