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");
|
|
|
|
|
|
|
|
|
|
var fileName = $"{DateTime.UtcNow:yyyyMMddHHmmss}_Create{entity.Name}Table.sql";
|
|
|
|
|
|
|
|
|
|
var sqlScript = GenerateSqlScript(entity);
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GenerateSqlScript(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(",");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entity.HasAuditFields)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine(" [CreationTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,");
|
|
|
|
|
sb.AppendLine(" [CreatorId] UNIQUEIDENTIFIER NULL,");
|
|
|
|
|
sb.AppendLine(" [LastModificationTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,");
|
|
|
|
|
sb.AppendLine(" [LastModifierId] UNIQUEIDENTIFIER NULL,");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entity.HasSoftDelete)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine(" [IsDeleted] BIT DEFAULT 0 NOT NULL,");
|
|
|
|
|
sb.AppendLine(" [DeleterId] UNIQUEIDENTIFIER NULL,");
|
|
|
|
|
sb.AppendLine(" [DeletionTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove last comma
|
|
|
|
|
var script = sb.ToString().TrimEnd(',', '\r', '\n');
|
|
|
|
|
script += "\n);\n";
|
|
|
|
|
|
|
|
|
|
return script;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|