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; /// /// 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. /// public class WizardDataSeeder : IDataSeedContributor, ITransientDependency { private readonly IRepository _repoLangKey; private readonly IRepository _repoLangText; private readonly IRepository _repoPermGroup; private readonly IRepository _repoPerm; private readonly IRepository _repoMenu; private readonly IRepository _repoModule; private readonly IRepository _repoDataSource; private readonly IRepository _repoListForm; private readonly IRepository _repoListFormField; private readonly IRepository _repoListFormWorkflow; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage; private readonly string _appName = PlatformConsts.AppName; public WizardDataSeeder( IRepository repoLangKey, IRepository repoLangText, IRepository repoPermGroup, IRepository repoPerm, IRepository repoMenu, IRepository repoModule, IRepository repoDataSource, IRepository repoListForm, IRepository repoListFormField, IRepository repoListFormWorkflow, IConfiguration configuration, ILogger 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) { _logger.LogInformation("[{File}] '{WizardName}' already exists, skipped.", 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 titleLangKey = $"{listFormCode}.Title"; var nameLangKey = code; var descLangKey = $"{listFormCode}.Desc"; 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 CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr); if (!isCustomComponent) { await CreateLangKeyAsync(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr); await CreateLangKeyAsync(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr); } // Permission Group // Menuye eklenmeyen wizard'da izin grubu sorulmaz; bos geldiginde varsayilan gruba dusar. var groupName = string.IsNullOrWhiteSpace(input.PermissionGroupName) ? AppName : input.PermissionGroupName.Trim(); input.PermissionGroupName = groupName; if (!await _repoPermGroup.AnyAsync(a => a.Name == groupName)) { await _repoPermGroup.InsertAsync( new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true); } // Grup adi ust menu koduyla ayniysa dil anahtari menu metinleriyle uretilir; aksi halde // gorunen adlar yazilir. CreateLangKeyAsync mevcut metinlerin uzerine yazmaz. if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase)) await EnsureLangKeyAsync(groupName); else await CreateLangKeyAsync(groupName, input.PermissionGroupDisplayNameEn?.Trim(), input.PermissionGroupDisplayNameTr?.Trim()); // 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(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; await CreateLangKeyAsync(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr); 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); } // 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); } 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); } } /// /// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden /// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir. /// 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; } /// /// Seed dosyasindaki modul kaydi yoksa olusturur; menunun ModuleId yabanci anahtari /// tanimsiz bir module bagli kalmasin diye menu yazilmadan once cagrilir. /// 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 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); } 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); } } private async Task EnsureLangKeyAsync(string key) { if (string.IsNullOrWhiteSpace(key)) return; if (!await _repoLangKey.AnyAsync(a => a.ResourceName == _appName && a.Key == key)) { await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true); } } }