sozsoft-platform/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs
2026-09-02 17:31:39 +03:00

558 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using static Sozsoft.Platform.PlatformConsts;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Wizard ile oluşturulan konfigürasyonları Seeds/{host|tenants/{tenantId}}/wizard/*.json
/// dosyalarından okuyarak veritabanına aktarır (kapsam, seed edilen veritabanını izler).
/// Wizard çalıştıktan sonra oluşturulan JSON dosyaları projeye commit edildikten sonra,
/// veritabanı silinip yeniden oluşturulduğunda bu seeder tüm wizard konfigürasyonlarını geri yükler.
/// </summary>
public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<LanguageKey, Guid> _repoLangKey;
private readonly IRepository<LanguageText, Guid> _repoLangText;
private readonly IRepository<PermissionGroupDefinitionRecord, Guid> _repoPermGroup;
private readonly IRepository<PermissionDefinitionRecord, Guid> _repoPerm;
private readonly IRepository<Menu, Guid> _repoMenu;
private readonly IRepository<Module, string> _repoModule;
private readonly IRepository<DataSource, Guid> _repoDataSource;
private readonly IRepository<ListForm, Guid> _repoListForm;
private readonly IRepository<ListFormField, Guid> _repoListFormField;
private readonly IRepository<ListFormWorkflow, string> _repoListFormWorkflow;
private readonly IConfiguration _configuration;
private readonly ILogger<WizardDataSeeder> _logger;
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
private readonly string _appName = PlatformConsts.AppName;
public WizardDataSeeder(
IRepository<LanguageKey, Guid> repoLangKey,
IRepository<LanguageText, Guid> repoLangText,
IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
IRepository<PermissionDefinitionRecord, Guid> repoPerm,
IRepository<Menu, Guid> repoMenu,
IRepository<Module, string> repoModule,
IRepository<DataSource, Guid> repoDataSource,
IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField,
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
IConfiguration configuration,
ILogger<WizardDataSeeder> logger)
{
_repoLangKey = repoLangKey;
_repoLangText = repoLangText;
_repoPermGroup = repoPermGroup;
_repoPerm = repoPerm;
_repoMenu = repoMenu;
_repoModule = repoModule;
_repoDataSource = repoDataSource;
_repoListForm = repoListForm;
_repoListFormField = repoListFormField;
_repoListFormWorkflow = repoListFormWorkflow;
_configuration = configuration;
_logger = logger;
}
public async Task SeedAsync(DataSeedContext context)
{
var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId);
var wizardDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.WizardFolder);
if (!Directory.Exists(wizardDataPath))
{
_logger.LogInformation("Seeds/{Scope}/wizard directory not found, skipping.", scopeFolderName);
return;
}
var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json");
if (jsonFiles.Length == 0)
{
_logger.LogInformation("No JSON files found in Seeds/{Scope}/wizard directory, skipping.", scopeFolderName);
return;
}
// Dosya adi tarih tasimadigi icin uygulama sirasi dosyanin kendi damgasindan gelir;
// birbirine bagli wizard'lar (once Customers, sonra Tasks) uretildikleri sirayla uygulanir.
var seedFiles = new List<(string FilePath, WizardSeedFileDto Seed)>();
foreach (var filePath in jsonFiles)
{
try
{
var seed = WizardSeedFileDto.FromSeedJson(await File.ReadAllTextAsync(filePath));
if (seed?.Wizard == null)
{
_logger.LogWarning("Invalid file skipped: {FilePath}", filePath);
continue;
}
seedFiles.Add((filePath, seed));
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error - {filePath}: {ex.Message}");
}
}
_logger.LogInformation("WizardDataSeeder started. {Count} files to be processed.", seedFiles.Count);
foreach (var (filePath, seedFile) in seedFiles
.OrderBy(x => x.Seed.Wizard.SeededAt)
.ThenBy(x => Path.GetFileName(x.FilePath), StringComparer.OrdinalIgnoreCase))
{
var fileName = Path.GetFileName(filePath);
try
{
var wizardName = seedFile.Wizard.WizardName?.Trim();
if (string.IsNullOrWhiteSpace(wizardName))
{
_logger.LogWarning("WizardName is empty, skipped: {FilePath}", filePath);
continue;
}
var isCustomComponent = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom;
if (!isCustomComponent && seedFile.ListForm == null)
{
_logger.LogWarning("[{File}] ListForm section is missing, skipped.", fileName);
continue;
}
await BackfillMenuParentModuleIdAsync(seedFile.Wizard);
// Zaten seeded mi kontrol et. Custom yolunda ListForm üretilmediği için
// idempotency menü kaydı üzerinden değerlendirilir.
// Menusuz Custom wizard'da ne ListForm ne de menu kaydi olustugu icin
// idempotency okuma permission'i uzerinden degerlendirilir.
var alreadySeeded = isCustomComponent
? seedFile.Wizard.CreateMenu
? await _repoMenu.AnyAsync(a => a.Code == seedFile.Wizard.ResolveMenuCode())
: await _repoPerm.AnyAsync(a => a.Name == seedFile.Wizard.ResolveMenuCode())
: await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode);
if (alreadySeeded)
{
// Ekran kurulu olsa da dil metinleri eksik kalmis olabilir: seed dosyasina yeni
// bir alan eklendiginde ya da ekran seeder disinda olustugunda sutun basliklari
// ham anahtar olarak gorunur. Mevcut metinlerin uzerine yazilmadigi icin bu
// tamamlama ekrani degistirmez.
await SeedLanguageTextsAsync(seedFile.Wizard, isCustomComponent);
_logger.LogInformation("[{File}] '{WizardName}' already exists, only missing language texts were created.", fileName, wizardName);
continue;
}
_logger.LogInformation("[{File}] '{WizardName}' is being applied...", fileName, wizardName);
await ApplyWizardSeedAsync(seedFile);
_logger.LogInformation("[{File}] '{WizardName}' has been successfully applied.", fileName, wizardName);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error - {filePath}: {ex.Message}");
}
}
}
private async Task ApplyWizardSeedAsync(WizardSeedFileDto seedFile)
{
var input = seedFile.Wizard;
input.Groups ??= [];
var code = input.ResolveMenuCode();
var listFormCode = input.ResolveListFormCode();
var nameLangKey = code;
var permCreateName = $"{code}.Create";
var permUpdateName = $"{code}.Update";
var permDeleteName = $"{code}.Delete";
var permExportName = $"{code}.Export";
var permImportName = $"{code}.Import";
var permNoteName = $"{code}.Note";
// Yol ayrımı: Custom yolunda ListForm ekosistemi üretilmez (AppService.Create ile aynı davranış).
var isCustomComponent = input.ComponentKind == WizardComponentKindEnum.Custom;
var customRoutePath = NormalizeSeedCustomRoutePath(input);
// Dil - Language Keys
await SeedLanguageTextsAsync(input, isCustomComponent);
// Permission Group - gorunen adin dil anahtari SeedLanguageTextsAsync icinde uretilir.
var groupName = ResolvePermissionGroupName(input);
input.PermissionGroupName = groupName;
if (!await _repoPermGroup.AnyAsync(a => a.Name == groupName))
{
await _repoPermGroup.InsertAsync(
new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true);
}
// Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et
var permQueryable = await _repoPerm.GetQueryableAsync();
var existingPerms = permQueryable.Where(a => a.GroupName == groupName).ToList();
// Yetkiler menuyle ayni modul filtresine tabidir; modul bos kalirsa rol ekraninda
// modul secimiyle filtrelenen listede gorunmez.
var permissionModules = input.MenuParentModuleId?.Trim();
var permRead = existingPerms.FirstOrDefault(a => a.Name == code);
permRead ??= await InsertPermissionAsync(groupName, code, null, nameLangKey, permissionModules);
// CRUD üçlüsü her iki yolda da üretilir: Custom component de Form
// üzerinden kayıt ekler, günceller ve siler (AppService.Create ile aynı davranış).
if (!existingPerms.Any(a => a.Name == permCreateName))
await InsertPermissionAsync(groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, permissionModules);
if (!existingPerms.Any(a => a.Name == permUpdateName))
await InsertPermissionAsync(groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, permissionModules);
if (!existingPerms.Any(a => a.Name == permDeleteName))
await InsertPermissionAsync(groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, permissionModules);
// Export/Import/Note yalnızca ListForm ekosisteminde karşılığı olan işlemlerdir.
if (!isCustomComponent)
{
if (!existingPerms.Any(a => a.Name == permExportName))
await InsertPermissionAsync(groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, permissionModules);
if (!existingPerms.Any(a => a.Name == permImportName))
await InsertPermissionAsync(groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, permissionModules);
if (!existingPerms.Any(a => a.Name == permNoteName))
await InsertPermissionAsync(groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, permissionModules);
}
// // Permission Grants - Admin role için, sadece eksik olanları ekle
// var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
// var existingGrantNames = new HashSet<string>(existingGrants.Select(g => g.Name));
// var grantsToInsert = new[]
// {
// permRead.Name, permCreate.Name, permUpdate.Name,
// permDelete.Name, permExport.Name, permImport.Name, permNote.Name
// }
// .Where(name => !existingGrantNames.Contains(name))
// .Select(name => new PermissionGrant(Guid.NewGuid(), name, "R", PlatformConsts.AbpIdentity.User.AdminRoleName))
// .ToList();
// if (grantsToInsert.Count > 0)
// {
// await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true);
// }
// Menunun acacagi adres; menu uretilmese de seed dosyasindaki degeri korunur.
input.MenuUrl = isCustomComponent
? customRoutePath
: !string.IsNullOrWhiteSpace(input.MenuUrl)
? input.MenuUrl.Trim()
: WizardConsts.MenuUrl(code);
// Menusuz wizard: menu (ve ust menu) kaydi hic uretilmez.
if (input.CreateMenu)
{
// Menu Parent
await EnsureModuleAsync(input.MenuParentModuleId);
var menuQueryable = await _repoMenu.GetQueryableAsync();
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
if (menuParent == null)
{
var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
? input.MenuParentIcon
: !string.IsNullOrWhiteSpace(input.MenuIcon)
? input.MenuIcon
: WizardConsts.MenuIcon;
menuParent = await _repoMenu.InsertAsync(new Menu
{
Code = input.MenuParentCode,
DisplayName = input.MenuParentCode,
IsDisabled = false,
ModuleId = input.MenuParentModuleId,
Icon = menuParentIcon,
Order = maxRootOrder + 1,
}, autoSave: true);
}
// Menu
var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0;
var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1;
input.MenuOrder = menuOrder;
var menuUrl = input.MenuUrl;
var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code);
if (existingMenu == null)
{
await _repoMenu.InsertAsync(new Menu
{
Code = code,
DisplayName = nameLangKey,
IsDisabled = false,
ParentCode = menuParent.Code,
Icon = input.MenuIcon ?? WizardConsts.MenuIcon,
Target = null,
ElementId = null,
CssClass = null,
Url = menuUrl,
RequiredPermissionName = permRead.Name,
Order = menuOrder,
ModuleId = menuParent.ModuleId,
}, autoSave: true);
}
else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl)
{
existingMenu.Order = menuOrder;
existingMenu.Url = menuUrl;
await _repoMenu.UpdateAsync(existingMenu, autoSave: true);
}
}
// Custom yolunda ListForm ekosistemi üretilmez.
if (isCustomComponent)
return;
// DataSource
var existingDataSource = await _repoDataSource.FirstOrDefaultAsync(a => a.Code == input.DataSourceCode);
if (existingDataSource == null)
{
await _repoDataSource.InsertAsync(new DataSource
{
Code = input.DataSourceCode,
DataSourceType = input.DataSourceConnectionString != null &&
input.DataSourceConnectionString.IndexOf("Server", StringComparison.OrdinalIgnoreCase) >= 0
? DataSourceTypeEnum.Mssql
: DataSourceTypeEnum.Postgresql,
ConnectionString = input.DataSourceConnectionString
}, autoSave: true);
}
// ListForm ve alanlari - varsa sil, seed dosyasindaki sozlesmeden yeniden kur
var existingListForm = await _repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode);
if (existingListForm != null)
{
await _repoListForm.DeleteAsync(existingListForm, autoSave: true);
}
var existingListFormFields = await _repoListFormField.GetListAsync(a => a.ListFormCode == input.ListFormCode);
if (existingListFormFields.Count > 0)
{
await _repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true);
}
var existingWorkflowCriteria = await _repoListFormWorkflow.GetListAsync(a => a.ListFormCode == input.ListFormCode);
if (existingWorkflowCriteria.Count > 0)
{
await _repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true);
}
// Ekran, seed dosyasinin tasidigi ListForm sozlesmesinden kurulur; boylece wizard
// sonrasinda tasarimci uzerinde yapilan degisiklikler de geri gelir.
var listForm = new ListForm();
seedFile.ListForm.ApplyTo(listForm);
await _repoListForm.InsertAsync(listForm, autoSave: true);
foreach (var fieldDto in seedFile.Fields)
{
var field = new ListFormField();
fieldDto.ApplyTo(field);
await _repoListFormField.InsertAsync(field, autoSave: true);
}
foreach (var criteria in seedFile.ListForm.WorkflowDto.Criteria)
{
if (string.IsNullOrWhiteSpace(criteria.Id))
{
_logger.LogWarning("Workflow criteria skipped because Id is empty. ListFormCode: {ListFormCode}, Title: {Title}", input.ListFormCode, criteria.Title);
continue;
}
await _repoListFormWorkflow.InsertAsync(new ListFormWorkflow(criteria.Id)
{
ListFormCode = string.IsNullOrWhiteSpace(criteria.ListFormCode) ? input.ListFormCode : criteria.ListFormCode,
Kind = criteria.Kind,
Title = criteria.Title,
CompareColumn = criteria.CompareColumn,
CompareOperator = criteria.CompareOperator,
CompareValue = criteria.CompareValue,
Approver = criteria.Approver,
NextOnStart = criteria.NextOnStart,
NextOnTrue = criteria.NextOnTrue,
NextOnFalse = criteria.NextOnFalse,
NextOnApprove = criteria.NextOnApprove,
NextOnReject = criteria.NextOnReject,
PositionX = criteria.PositionX,
PositionY = criteria.PositionY,
CompareOutcomesJson = JsonSerializer.Serialize(criteria.CompareOutcomes ?? []),
}, autoSave: true);
}
}
/// <summary>
/// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden
/// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir.
/// </summary>
private static string NormalizeSeedCustomRoutePath(ListFormWizardDto input)
{
if (input.ComponentKind != WizardComponentKindEnum.Custom)
return null;
var routePath = input.MenuUrl?.Trim();
if (string.IsNullOrWhiteSpace(routePath))
throw new InvalidOperationException(
$"'{input.WizardName}' custom component wizard'inda Route Path bos olamaz.");
return routePath.StartsWith('/') ? routePath : "/" + routePath;
}
/// <summary>
/// Seed dosyasindaki modul kaydi yoksa olusturur; menunun ModuleId yabanci anahtari
/// tanimsiz bir module bagli kalmasin diye menu yazilmadan once cagrilir.
/// </summary>
private async Task EnsureModuleAsync(string moduleId)
{
var cleaned = moduleId?.Trim();
if (string.IsNullOrWhiteSpace(cleaned))
return;
if (await _repoModule.AnyAsync(a => a.Id == cleaned))
return;
await _repoModule.InsertAsync(new Module(cleaned) { Name = cleaned }, autoSave: true);
_logger.LogInformation("Module '{ModuleId}' seed sirasinda olusturuldu.", cleaned);
}
private async Task<PermissionDefinitionRecord> InsertPermissionAsync(
string groupName,
string name,
string parentName,
string displayName,
string modules)
{
var permission = new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, name, parentName, displayName, true, MultiTenancySides.Both);
permission.SetModules(modules);
return await _repoPerm.InsertAsync(permission, autoSave: true);
}
private async Task BackfillMenuParentModuleIdAsync(ListFormWizardDto input)
{
if (string.IsNullOrWhiteSpace(input.MenuParentCode) ||
string.IsNullOrWhiteSpace(input.MenuParentModuleId))
return;
await EnsureModuleAsync(input.MenuParentModuleId);
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
if (menuParent == null || !string.IsNullOrWhiteSpace(menuParent.ModuleId))
return;
menuParent.ModuleId = input.MenuParentModuleId.Trim();
await _repoMenu.UpdateAsync(menuParent, autoSave: true);
_logger.LogInformation(
"Menu parent '{MenuParentCode}' ModuleId backfilled as '{ModuleId}'.",
input.MenuParentCode,
menuParent.ModuleId);
}
/// <summary>
/// Wizard'in urettigi ekranin dil metinleri: menu adi, ust menu adi, izin grubu gorunen adi,
/// ekran basligi/aciklamasi ve alan basliklari. Ekran kurulumundan bagimsiz calisir; boylece
/// zaten kurulu bir ekranda eksik kalan anahtarlar da her seed calismasinda tamamlanir.
/// </summary>
private async Task SeedLanguageTextsAsync(ListFormWizardDto input, bool isCustomComponent)
{
await CreateLangKeyAsync(input.ResolveMenuCode(), input.MenuEn, input.MenuTr);
// Ust menu ve izin grubu paylasilan kayitlardir: ilk wizard onlari uretmis olsa bile dil
// metinleri eksik kalabilir, bu yuzden menu/grup kaydinin varligindan bagimsiz yazilir.
if (input.CreateMenu)
await CreateLangKeyAsync(input.MenuParentCode, input.MenuParentEn, input.MenuParentTr);
var groupName = ResolvePermissionGroupName(input);
// Grup adi ust menu koduyla ayniysa anahtar tektir; menu adi ile izin grubu adi ayni
// metni gosterir, ayrica gorunen ad girilmediyse menu metinlerine duser.
if (!string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
{
await CreateLangKeyAsync(groupName, input.PermissionGroupEn?.Trim(), input.PermissionGroupTr?.Trim());
}
else
{
await CreateLangKeyAsync(groupName,
input.PermissionGroupEn?.Trim() ?? input.MenuParentEn,
input.PermissionGroupTr?.Trim() ?? input.MenuParentTr);
}
// Custom yolunda ListForm ekosistemi (baslik, aciklama, alan basliklari) uretilmez.
if (isCustomComponent)
return;
var listFormCode = input.ResolveListFormCode();
await CreateLangKeyAsync($"{listFormCode}.Title", input.TitleEn, input.TitleTr);
await CreateLangKeyAsync($"{listFormCode}.Desc", input.DescEn, input.DescTr);
// Alan basliklarinin iki dilli metinleri yalnizca wizard bolumunde tasinir.
foreach (var item in (input.Groups ?? []).SelectMany(g => g.Items ?? []))
{
await CreateLangKeyAsync(item.CaptionName, item.EnglishCaption, item.TurkishCaption);
}
}
/// <summary>
/// Yetkilerin toplanacagi izin grubu adi. Menuye eklenmeyen wizard'da grup sorulmaz; bos
/// geldiginde varsayilan uygulama grubuna duser.
/// </summary>
private static string ResolvePermissionGroupName(ListFormWizardDto input) =>
string.IsNullOrWhiteSpace(input.PermissionGroupName) ? AppName : input.PermissionGroupName.Trim();
private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
{
if (string.IsNullOrWhiteSpace(key)) return;
var langKey = await _repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == _appName && a.Key == key)
?? await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true);
var existingTexts = await _repoLangText.GetListAsync(a => a.ResourceName == _appName && a.Key == langKey.Key);
var existingEn = existingTexts.FirstOrDefault(a => a.CultureName == _cultureNameDefault);
if (existingEn == null)
{
await _repoLangText.InsertAsync(new LanguageText
{
ResourceName = _appName,
Key = langKey.Key,
CultureName = _cultureNameDefault,
Value = textEn ?? key
}, autoSave: true);
}
var existingTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr);
if (existingTr == null)
{
await _repoLangText.InsertAsync(new LanguageText
{
ResourceName = _appName,
Key = langKey.Key,
CultureName = LanguageCodes.Tr,
Value = textTr ?? key
}, autoSave: true);
}
}
}