diff --git a/README.md b/README.md index b8a69b59..53325ddf 100644 --- a/README.md +++ b/README.md @@ -443,13 +443,27 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar: | Araç | Route | Ne işe yarar | | --- | --- | --- | -| **SQL Query Manager** | `/admin/sql-manager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, nesne özellikleri ve **tablo tasarımcısı**. | -| **Custom Endpoint** | `/admin/developerkit/endpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. | -| **CRUD Endpoint** | `/admin/developerkit/entities` | Tablo tanımından tam CRUD endpoint kümesi üretimi. | +| **SQL Query Manager** | `/admin/sql-manager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı** ve **CRUD endpoint yönetimi**. | +| **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. | +| **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. | | **Dynamic Service** | `/admin/developerkit/dynamic-services` | C# kodunu tarayıcıda yazıp Roslyn ile derleme (`TestCompile`), yayınlama (`Publish`) ve çalışan uygulamaya controller olarak kaydetme. Yetkiler: Create/Edit/Delete/Manage/TestCompile/Publish/ViewCode. | | **Custom Component** | `/admin/developerkit/components` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. | | **Visual Designer** | Component editörü içinde | Sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). | +> **CRUD Endpoint, SQL Query Manager ile birleştirildi.** Ayrı `/admin/developerkit/endpoints` +> ekranı ve menüsü kaldırıldı. Endpoint üretme, aktif/pasif etme, test etme ve silme işlemleri +> artık iki yerden yapılır: SQL Query Manager nesne gezgininde tablo satırının **CRUD Endpoints** +> aksiyonu (ve seçili tablolar için toolbar'daki toplu üretim butonu), ayrıca ListForm +> sihirbazının **Adım 2 – Veri Kaynağı** ekranındaki aynı diyalog. Ayrıca **tablo +> tasarımcısında deploy öncesi bir adım** vardır: endpoint üretilip üretilmeyeceği ve +> hangi operasyonların (GetList/GetById/Create/Update/Delete) aktif başlayacağı seçilir +> ("tümünü seç" destekli); seçilmeyenler pasif olarak kaydedilir, sonradan diyalogdan +> açılabilir. Hepsi `CrudEndpointDialog` bileşenini kullanır. Yetki `App.SqlQueryManager` altındaki +> `App.SqlQueryManager.CrudEndpoints`'tir; izin verilmemişse butonlar görünmez ve +> `crud-endpoint-generate` uçlarının tamamı (base CRUD metotları dahil) reddedilir. +> Üretilen endpoint'ler `configs/seeds/{host|tenants/{tenantId}}/crud/{EntityName}.json` olarak +> seed dosyasına yazılır; `CrudDataSeeder` aynı dosyaları okuyup endpoint'leri geri yükler. + > **Tablo tasarımı varsayılanı:** Yeni tablo oluştururken tenant ve tam denetim (audit) kolonları > varsayılan olarak eklenir: `Id`, `TenantId`, `CreationTime`, `CreatorId`, `LastModificationTime`, > `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`. Kullanıcı açıkça istemedikçe bu @@ -484,6 +498,7 @@ Route.Authority ──┘ └► Permission `PermissionsData.json` ile seed edilir. Ana gruplar: `App.Saas`, `App.Branches`, `App.Intranet.*`, `App.Definitions.*`, `App.Restrictions.*`, `App.Languages.*`, `App.Listforms.*`, `App.Notifications.*`, `App.BackgroundWorkers.*`, `App.Menus.*`, `App.DeveloperKit.*`, +`App.SqlQueryManager.*`, `App.Orders.*`, `App.BlogManagement.*`, `App.IdentityManagement.*`, `App.Reports.*`, `App.Administration`, `App.Setting`, `App.Setup.Migrate`. diff --git a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs index 5c34888e..25b02069 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs @@ -12,9 +12,13 @@ public interface ICrudEndpointAppService : ICrudAppService< PagedAndSortedResultRequestDto, CreateUpdateCrudEndpointDto> { - Task> GetActiveEndpointsAsync(); Task> GetEndpointsByEntityAsync(string entityName); Task ToggleAsync(Guid id); - Task> GenerateCrudEndpointsAsync(string entityName); + /// + /// Aktif baslatilacak operasyonlarin virgulle ayrilmis listesi + /// (GetList, GetById, Create, Update, Delete). Bos birakilirsa hepsi aktif olur. + /// + Task> GenerateCrudEndpointsAsync(string entityName, string operationTypes = null); + Task DeleteByEntityAsync(string entityName); } diff --git a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs index 598db8e4..5a1c1677 100644 --- a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs +++ b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.Entities; using Volo.Abp.Application.Dtos; @@ -8,6 +12,8 @@ using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Sozsoft.Platform; namespace Platform.Api.Application; @@ -19,22 +25,31 @@ public class CrudEndpointGenerateAppService : CrudAppService< PagedAndSortedResultRequestDto, CreateUpdateCrudEndpointDto>, ICrudEndpointAppService { + private static readonly JsonSerializerOptions SeedJsonWriteOptions = new() + { + WriteIndented = true + }; + + private readonly IConfiguration _configuration; + public CrudEndpointGenerateAppService( - IRepository repository) + IRepository repository, + IConfiguration configuration) : base(repository) { - } + _configuration = configuration; - [HttpGet("api/app/crud-endpoint-generate/active-endpoints")] - [Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)] - public virtual async Task> GetActiveEndpointsAsync() - { - var endpoints = await Repository.GetListAsync(x => x.IsActive); - return await MapToGetListOutputDtosAsync(endpoints); + // CrudAppService'ten miras alinan GetList/Get/Create/Update/Delete de + // ayni izne baglanir; aksi halde base metotlar izinsiz cagrilabilir. + GetPolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints; + GetListPolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints; + CreatePolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints; + UpdatePolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints; + DeletePolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints; } [HttpGet("api/app/crud-endpoint-generate/endpoints-by-entity/{entityName}")] - [Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)] + [Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)] public virtual async Task> GetEndpointsByEntityAsync(string entityName) { var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName); @@ -42,19 +57,34 @@ public class CrudEndpointGenerateAppService : CrudAppService< } [HttpPost("api/app/crud-endpoint-generate/{id}/toggle")] - [Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)] + [Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)] public virtual async Task ToggleAsync(Guid id) { var endpoint = await Repository.GetAsync(id); endpoint.IsActive = !endpoint.IsActive; await Repository.UpdateAsync(endpoint, autoSave: true); + + await SyncSeedFileAsync(endpoint.EntityName); + return ObjectMapper.Map(endpoint); } [HttpPost("api/app/crud-endpoint-generate/generate-crud-endpoints/{entityName}")] - [Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)] - public virtual async Task> GenerateCrudEndpointsAsync(string entityName) + [Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)] + public virtual async Task> GenerateCrudEndpointsAsync( + string entityName, + [FromQuery] string operationTypes = null) { + // Bes endpoint her zaman uretilir; operationTypes verilmisse yalnizca + // listedekiler aktif baslar, digerleri pasif olarak kaydedilir ve + // kullanici diyalogdan sonradan acabilir. Bos/null ise hepsi aktiftir. + var activeOperations = operationTypes + ?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + bool IsActiveOperation(string operationType) => + activeOperations == null || activeOperations.Count == 0 || activeOperations.Contains(operationType); + // CRUD endpointleri oluştur var endpoints = new List { @@ -63,7 +93,7 @@ public class CrudEndpointGenerateAppService : CrudAppService< Method = "GET", Path = $"/api/app/crudendpoint/{entityName}", OperationType = "GetList", - IsActive = true, + IsActive = IsActiveOperation("GetList"), CsharpCode = GenerateGetAllCode(entityName) }, new() { @@ -71,7 +101,7 @@ public class CrudEndpointGenerateAppService : CrudAppService< Method = "GET", Path = $"/api/app/crudendpoint/{entityName}/{{id}}", OperationType = "GetById", - IsActive = true, + IsActive = IsActiveOperation("GetById"), CsharpCode = GenerateGetByIdCode(entityName) }, new() { @@ -79,7 +109,7 @@ public class CrudEndpointGenerateAppService : CrudAppService< Method = "POST", Path = $"/api/app/crudendpoint/{entityName}", OperationType = "Create", - IsActive = true, + IsActive = IsActiveOperation("Create"), CsharpCode = GenerateCreateCode(entityName) }, new() { @@ -87,7 +117,7 @@ public class CrudEndpointGenerateAppService : CrudAppService< Method = "PUT", Path = $"/api/app/crudendpoint/{entityName}/{{id}}", OperationType = "Update", - IsActive = true, + IsActive = IsActiveOperation("Update"), CsharpCode = GenerateUpdateCode(entityName) }, new() { @@ -95,7 +125,7 @@ public class CrudEndpointGenerateAppService : CrudAppService< Method = "DELETE", Path = $"/api/app/crudendpoint/{entityName}/{{id}}", OperationType = "Delete", - IsActive = true, + IsActive = IsActiveOperation("Delete"), CsharpCode = GenerateDeleteCode(entityName) } }; @@ -109,6 +139,8 @@ public class CrudEndpointGenerateAppService : CrudAppService< // Yeni endpointleri ekle await Repository.InsertManyAsync(endpoints, autoSave: true); + await SyncSeedFileAsync(entityName); + var result = ObjectMapper.Map, List>(endpoints); return new PagedResultDto @@ -118,6 +150,128 @@ public class CrudEndpointGenerateAppService : CrudAppService< }; } + /// Bir entity'nin tum CRUD endpoint'lerini ve seed dosyasini kaldirir. + [HttpDelete("api/app/crud-endpoint-generate/by-entity/{entityName}")] + [Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)] + public virtual async Task DeleteByEntityAsync(string entityName) + { + var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName); + if (endpoints.Count > 0) + { + await Repository.DeleteManyAsync(endpoints, autoSave: true); + } + + await SyncSeedFileAsync(entityName); + } + + public override async Task DeleteAsync(Guid id) + { + var endpoint = await Repository.FindAsync(id); + await base.DeleteAsync(id); + + if (endpoint != null) + { + await SyncSeedFileAsync(endpoint.EntityName); + } + } + + public override async Task CreateAsync(CreateUpdateCrudEndpointDto input) + { + var result = await base.CreateAsync(input); + await SyncSeedFileAsync(result.EntityName); + return result; + } + + public override async Task UpdateAsync(Guid id, CreateUpdateCrudEndpointDto input) + { + var result = await base.UpdateAsync(id, input); + await SyncSeedFileAsync(result.EntityName); + return result; + } + + /// + /// Entity'nin guncel endpoint'lerini {host|tenants/{tenantId}}/crud/{EntityName}.json + /// dosyasina yazar; endpoint kalmamissa dosyayi siler. Dosya islemi hatasi CRUD islemini + /// engellemez, yalnizca loglanir. + /// + private async Task SyncSeedFileAsync(string entityName) + { + var safeFileName = GetSafeFileName(entityName); + if (safeFileName == null) + { + return; + } + + try + { + var outputPath = ResolveCrudSeedOutputPath(); + var filePath = Path.Combine(outputPath, $"{safeFileName}.json"); + + var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName); + + if (endpoints.Count == 0) + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + Logger.LogInformation("CRUD seed file deleted: {FilePath}", filePath); + } + + return; + } + + Directory.CreateDirectory(outputPath); + + var seedFile = new CrudEndpointSeedFileDto + { + EntityName = entityName, + GeneratedAt = Clock.Now.ToUniversalTime(), + Endpoints = endpoints + .OrderBy(x => x.OperationType) + .Select(x => new CrudEndpointSeedItemDto + { + Method = x.Method, + Path = x.Path, + OperationType = x.OperationType, + IsActive = x.IsActive + }) + .ToList() + }; + + await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, SeedJsonWriteOptions)); + Logger.LogInformation("CRUD seed file saved: {FilePath}", filePath); + } + catch (Exception ex) + { + Logger.LogError(ex, "CRUD seed file could not be synchronized for {EntityName}: {Message}", entityName, ex.Message); + } + } + + /// + /// CRUD seed dosyalarinin yazilacagi klasor; kapsam aktif tenant'i izler + /// (host baglaminda host/crud, tenant baglaminda tenants/{tenantId}/crud). + /// + private string ResolveCrudSeedOutputPath() + { + return SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id, SeedPathResolver.CrudFolder); + } + + /// Entity adindan guvenli bir dosya adi uretir; path traversal denemelerini reddeder. + private static string? GetSafeFileName(string entityName) + { + var trimmed = entityName?.Trim(); + if (string.IsNullOrWhiteSpace(trimmed) || + trimmed.Contains('/') || + trimmed.Contains('\\') || + trimmed.Contains("..")) + { + return null; + } + + var safeFileName = string.Concat(trimmed.Split(Path.GetInvalidFileNameChars())); + return string.IsNullOrWhiteSpace(safeFileName) ? null : safeFileName; + } + private string GenerateGetAllCode(string entityName) { return $@"[HttpGet] diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/CrudDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/CrudDataSeeder.cs new file mode 100644 index 00000000..ccd8e3bf --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/CrudDataSeeder.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Sozsoft.Platform.Entities; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Sozsoft.Platform.Data.Seeds; + +/// +/// SqlQueryManager uzerinden uretilen CRUD endpoint'lerini +/// {host|tenants/{tenantId}}/crud/*.json dosyalarindan okuyarak veritabanina uygular. +/// +/// tablolari olusturduktan hemen sonra calisir; boylece endpoint'ler +/// her zaman dayandiklari tablolar hazir olduktan sonra yazilir. ABP'nin contributor sirasi +/// garanti edilmedigi icin bilincli olarak degildir, +/// SqlDataSeeder tarafindan dogrudan cagrilir. +/// +/// Her dosya bir entity'ye aittir ve o entity'nin endpoint listesinin tamamini icerir: +/// dosyadaki liste kaynak kabul edilir, veritabanindaki eski kayitlar dosyaya gore guncellenir. +/// +public class CrudDataSeeder : ITransientDependency +{ + private static readonly JsonSerializerOptions JsonReadOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private readonly IRepository _repository; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public CrudDataSeeder( + IRepository repository, + IConfiguration configuration, + ILogger logger) + { + _repository = repository; + _configuration = configuration; + _logger = logger; + } + + public async Task SeedAsync(DataSeedContext context) + { + var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId); + var crudDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.CrudFolder); + + if (!Directory.Exists(crudDataPath)) + { + _logger.LogInformation("Seeds/{Scope}/crud directory not found, skipping CrudDataSeeder.", scopeFolderName); + return; + } + + var jsonFiles = Directory.GetFiles(crudDataPath, "*.json").OrderBy(Path.GetFileName).ToArray(); + if (jsonFiles.Length == 0) + { + _logger.LogInformation("No JSON files found in Seeds/{Scope}/crud directory, skipping CrudDataSeeder.", scopeFolderName); + return; + } + + _logger.LogInformation("CrudDataSeeder started. {Count} file(s) to be processed.", jsonFiles.Length); + + foreach (var filePath in jsonFiles) + { + var fileName = Path.GetFileName(filePath); + try + { + var json = await File.ReadAllTextAsync(filePath); + var seedFile = JsonSerializer.Deserialize(json, JsonReadOptions); + + var entityName = string.IsNullOrWhiteSpace(seedFile?.EntityName) + ? Path.GetFileNameWithoutExtension(filePath) + : seedFile!.EntityName.Trim(); + + if (seedFile?.Endpoints == null || seedFile.Endpoints.Count == 0) + { + _logger.LogWarning("[{File}] No endpoint found, skipped.", fileName); + continue; + } + + await ApplyAsync(entityName, seedFile.Endpoints); + _logger.LogInformation("[{File}] '{EntityName}' applied with {Count} endpoint(s).", fileName, entityName, seedFile.Endpoints.Count); + } + catch (Exception ex) + { + // Tek bir dosyanin hatasi diger endpoint'lerin seed edilmesini engellemez. + _logger.LogError(ex, "Failed to process CRUD seed file: {FileName}", fileName); + } + } + + _logger.LogInformation("CrudDataSeeder completed. {Count} file(s) processed.", jsonFiles.Length); + } + + private async Task ApplyAsync(string entityName, List endpoints) + { + var existing = await _repository.GetListAsync(x => x.EntityName == entityName); + + foreach (var item in endpoints) + { + if (string.IsNullOrWhiteSpace(item.OperationType)) + { + continue; + } + + var current = existing.FirstOrDefault(x => + string.Equals(x.OperationType, item.OperationType, StringComparison.OrdinalIgnoreCase)); + + if (current == null) + { + // CsharpCode seed dosyasinda tutulmaz; endpoint'in calismasi icin gerekli degildir, + // gerektiginde SqlQueryManager'daki "Yeniden Olustur" ile uretilir. + await _repository.InsertAsync(new CrudEndpoint + { + EntityName = entityName, + Method = item.Method, + Path = item.Path, + OperationType = item.OperationType, + IsActive = item.IsActive + }, autoSave: true); + continue; + } + + current.Method = item.Method; + current.Path = item.Path; + current.IsActive = item.IsActive; + await _repository.UpdateAsync(current, autoSave: true); + } + + // Dosyada artik yer almayan operasyonlar kaldirilir; dosya tek kaynaktir. + var seededOperations = endpoints + .Select(x => x.OperationType) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var obsolete = existing.Where(x => !seededOperations.Contains(x.OperationType)).ToList(); + if (obsolete.Count > 0) + { + await _repository.DeleteManyAsync(obsolete, autoSave: true); + } + } +} diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index f9ba6613..51a71671 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -638,7 +638,7 @@ }, { "resourceName": "Platform", - "key": "App.DeveloperKit.CrudEndpoints", + "key": "App.SqlQueryManager.CrudEndpoints", "en": "Crud Endpoints", "tr": "Crud Uç Noktaları" }, @@ -9084,12 +9084,6 @@ "en": "Apply Migration", "tr": "Geçişi Uygula" }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitMigration.GenerateCrudEndpoints", - "en": "Generate Crud Endpoints", - "tr": "Crud Uç Noktalarını Oluştur" - }, { "resourceName": "Platform", "key": "App.DeveloperKitMigration.NoSQLScript", @@ -16719,8 +16713,8 @@ { "resourceName": "Platform", "key": "App.SqlQueryManager.OpenFileManager", - "en": "Seed Files", - "tr": "Seed Dosyaları" + "en": "File Manager", + "tr": "File Manager" }, { "resourceName": "Platform", @@ -16842,6 +16836,96 @@ "en": "T-SQL Preview", "tr": "T-SQL Önizleme" }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.GenerateCrudEndpointsAfterDeploy", + "en": "Generate CRUD endpoints after deploy", + "tr": "Deploy sonrası CRUD endpoint'lerini oluştur" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.GenerateCrudEndpointsHint", + "en": "Endpoints are created for this entity right after the table is deployed", + "tr": "Endpoint'ler tablo deploy edildikten hemen sonra bu varlık için üretilir" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.SelectCrudOperations", + "en": "Operations to enable", + "tr": "Açılacak operasyonlar" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.SelectAtLeastOneCrudOperation", + "en": "Select at least one operation or turn generation off", + "tr": "En az bir operasyon seçin ya da üretimi kapatın" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.CrudEndpointsInactiveNote", + "en": "Unselected operations are still created but stay inactive; you can enable them later from the CRUD Endpoints dialog.", + "tr": "Seçilmeyen operasyonlar yine oluşturulur ancak pasif kalır; daha sonra CRUD Endpoint diyaloğundan açabilirsiniz." + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.ClearSelection", + "en": "Clear selection", + "tr": "Seçimi temizle" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.DropSelected", + "en": "Drop selected", + "tr": "Seçilenleri sil" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.DropObjects", + "en": "Drop objects", + "tr": "Nesneleri sil" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.DropObjectsWarning", + "en": "The following objects will be permanently dropped:", + "tr": "Aşağıdaki nesneler kalıcı olarak silinecek:" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.Drop", + "en": "Drop", + "tr": "Sil" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.CrudOperationGetList", + "en": "List records", + "tr": "Kayıtları listeler" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.CrudOperationGetById", + "en": "Get single record", + "tr": "Tek kayıt getirir" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.CrudOperationCreate", + "en": "Create record", + "tr": "Kayıt oluşturur" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.CrudOperationUpdate", + "en": "Update record", + "tr": "Kayıt günceller" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.CrudOperationDelete", + "en": "Delete record", + "tr": "Kayıt siler" + }, { "resourceName": "Platform", "key": "App.SqlQueryManager.TableDesigner", @@ -17166,84 +17250,6 @@ "en": "Table could not be deployed.", "tr": "Tablo deploy edilemedi." }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.TotalTables", - "en": "Total Tables", - "tr": "Toplam Tablo" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.TotalTablesDescription", - "en": "Number of total tables", - "tr": "Toplam tabloların sayısı" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.EndpointInstalled", - "en": "Endpoint Installed", - "tr": "Endpoint Kurulu" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.EndpointInstalledDescription", - "en": "Number of installed endpoints", - "tr": "Kurulu endpointlerin sayısı" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.ActiveEndpoint", - "en": "Active Endpoint", - "tr": "Aktif Endpoint" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.ActiveEndpointDescription", - "en": "Number of active endpoints", - "tr": "Aktif endpointlerin sayısı" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.DataSourceDescription", - "en": "Data source of the CRUD endpoints", - "tr": "CRUD endpointlerinin veri kaynağı" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.SearchTable", - "en": "Search table...", - "tr": "Tablo ara..." - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.FilterWith", - "en": "WITH", - "tr": "VAR" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.FilterWithout", - "en": "WITHOUT", - "tr": "YOK" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.SelectDataSource", - "en": "Select a data source", - "tr": "Veri kaynağı seçin" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.SelectTablePrompt", - "en": "Select a table from the left", - "tr": "Soldan bir tablo seçin" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.SelectTableDescription", - "en": "You can manage CRUD endpoints for the selected table", - "tr": "Seçilen tablo için CRUD endpointlerini yönetebilirsiniz" - }, { "resourceName": "Platform", "key": "App.DeveloperKitCrudEndpoints.DeleteAll", @@ -17262,6 +17268,24 @@ "en": "Create CRUD Endpoint", "tr": "CRUD Endpoint Oluştur" }, + { + "resourceName": "Platform", + "key": "App.Platform.SelectOrOpenHint", + "en": "Click to select, double-click to open in the editor", + "tr": "Seçmek için tıklayın, editörde açmak için çift tıklayın" + }, + { + "resourceName": "Platform", + "key": "App.Platform.InvalidJson", + "en": "Request body is not valid JSON", + "tr": "İstek gövdesi geçerli bir JSON değil" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitCrudEndpoints.CreateCrudEndpointHint", + "en": "Generates CRUD endpoints for the selected tables", + "tr": "Seçili tablolar için CRUD endpointlerini oluşturur" + }, { "resourceName": "Platform", "key": "App.DeveloperKitCrudEndpoints.NoEndpointsYet", @@ -17310,24 +17334,12 @@ "en": "Test", "tr": "Test Et" }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.CsharpCode", - "en": "C# Code", - "tr": "C# Kodu" - }, { "resourceName": "Platform", "key": "App.DeveloperKitCrudEndpoints.InactiveCount", "en": "inactive", "tr": "devre dışı" }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitCrudEndpoints.EndpointSummary", - "en": "5 endpoints: GetList, GetById, Create, Update, Delete", - "tr": "5 endpoint: GetList, GetById, Create, Update, Delete" - }, { "resourceName": "Platform", "key": "App.DeveloperKitDynamicServices.SuccessfulDescription", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenuDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenuDataSeeder.cs index 54f77720..323e814a 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenuDataSeeder.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenuDataSeeder.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; using Sozsoft.Platform.Entities; diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json index 8dcfd9dd..b5ed9898 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json @@ -336,14 +336,6 @@ "routeType": "protected", "authority": ["App.SqlQueryManager"] }, - { - "key": "admin.developerkit.endpoints", - "path": "/admin/developerkit/endpoints", - "componentType": "normal", - "componentPath": "@/views/developerKit/CrudEndpointManager", - "routeType": "protected", - "authority": ["App.DeveloperKit.CrudEndpoints"] - }, { "key": "admin.developerkit.dynamic-services", "path": "/admin/developerkit/dynamic-services", @@ -956,16 +948,6 @@ "RequiredPermissionName": "App.SqlQueryManager", "IsDisabled": false }, - { - "ParentCode": "App.DeveloperKit", - "Code": "App.DeveloperKit.CrudEndpoints", - "DisplayName": "App.DeveloperKit.CrudEndpoints", - "Order": 2, - "Url": "/admin/developerkit/endpoints", - "Icon": "FcOrgUnit", - "RequiredPermissionName": "App.DeveloperKit.CrudEndpoints", - "IsDisabled": false - }, { "ParentCode": "App.DeveloperKit", "Code": "App.DeveloperKit.CustomEndpoints", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json index 2a05004f..2b23359d 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json @@ -2374,9 +2374,9 @@ }, { "GroupName": "App.Saas", - "Name": "App.DeveloperKit.CrudEndpoints", - "ParentName": "App.DeveloperKit", - "DisplayName": "App.DeveloperKit.CrudEndpoints", + "Name": "App.SqlQueryManager.CrudEndpoints", + "ParentName": "App.SqlQueryManager", + "DisplayName": "App.SqlQueryManager.CrudEndpoints", "IsEnabled": true, "MultiTenancySide": 2, "MenuGroup": "Erp|Kurs" diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/SqlDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/SqlDataSeeder.cs index 0203d08d..6c276b67 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/SqlDataSeeder.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/SqlDataSeeder.cs @@ -46,19 +46,33 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency private readonly IDbContextProvider _dbContextProvider; private readonly IConfiguration _configuration; + private readonly CrudDataSeeder _crudDataSeeder; private readonly ILogger _logger; public SqlDataSeeder( IDbContextProvider dbContextProvider, IConfiguration configuration, + CrudDataSeeder crudDataSeeder, ILogger logger) { _dbContextProvider = dbContextProvider; _configuration = configuration; + _crudDataSeeder = crudDataSeeder; _logger = logger; } + /// + /// Once SQL nesneleri uygulanir, ardindan ayni kapsam icin + /// calisir — CRUD endpoint'leri dayandiklari + /// tablolardan sonra olusturulmus olur. + /// public async Task SeedAsync(DataSeedContext context) + { + await SeedSqlObjectsAsync(context); + await _crudDataSeeder.SeedAsync(context); + } + + private async Task SeedSqlObjectsAsync(DataSeedContext context) { var dataDirectoryName = $"{SeedPathResolver.GetScopeFolderName(context.TenantId)}/{GetProviderDirectoryName()}"; var sqlDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, GetProviderDirectoryName()); diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index aa2d72cc..cc39205f 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -486,8 +486,6 @@ public static class PlatformConsts public const string Put = CustomEndpoints + ".Put"; public const string Remove = CustomEndpoints + ".Remove"; - public const string CrudEndpoints = Default + ".CrudEndpoints"; - public static class DynamicServices { public const string DynamicService = Default + ".DynamicServices"; @@ -502,6 +500,13 @@ public static class PlatformConsts } } + public static class SqlQueryManagers + { + public const string Default = Prefix.App + ".SqlQueryManager"; + + public const string CrudEndpoints = Default + ".CrudEndpoints"; + } + //Web Site public const string Home = Prefix.App + ".Home"; public const string About = Prefix.App + ".About"; diff --git a/api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs b/api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs new file mode 100644 index 00000000..526755db --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace Sozsoft.Platform.Data.Seeds; + +/// +/// Bir entity icin uretilen CRUD endpoint'lerinin seed dosyasi. +/// SqlQueryManager endpoint uretince {host|tenants/{tenantId}}/crud/{EntityName}.json +/// olarak yazilir; veritabani silinip yeniden olusturuldugunda +/// CrudDataSeeder ayni dosyalari okuyarak endpoint'leri geri yukler. +/// +public class CrudEndpointSeedFileDto +{ + /// PascalCase entity adi; dosya adi ile ayni olmasi beklenir. + public string EntityName { get; set; } = string.Empty; + + /// Dosyanin en son yazildigi an (UTC) — yalnizca bilgi amaclidir. + public DateTime GeneratedAt { get; set; } + + public List Endpoints { get; set; } = []; +} + +/// +/// Endpoint'in seed edilebilir tanimi. Uretilen C# kodu bilincli olarak dosyada tutulmaz: +/// entity adi ve operasyondan yeniden turetilebilir bir ciktidir, dosyayi gereksiz sisirir. +/// +public class CrudEndpointSeedItemDto +{ + public string Method { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string OperationType { get; set; } = string.Empty; + public bool IsActive { get; set; } = true; +} diff --git a/ui/eslint.config.js b/ui/eslint.config.js index 4f98893b..22b35e00 100644 --- a/ui/eslint.config.js +++ b/ui/eslint.config.js @@ -75,6 +75,11 @@ export default [ ...tsPlugin.configs.recommended.rules, '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', + // TypeScript zaten tanimsiz tanimlayicilari yakalar; ESLint'in kapsam + // analizi ise `EventListener` / `NotificationOptions` gibi yalnizca tip + // olarak var olan DOM global'lerini bilmedigi icin yanlis alarm veriyor. + // typescript-eslint'in de onerdigi ayar. + 'no-undef': 'off', }, }, prettier, diff --git a/ui/src/components/shared/DbMigrateLogPanel.tsx b/ui/src/components/shared/DbMigrateLogPanel.tsx index 8420cb32..1dddbb1e 100644 --- a/ui/src/components/shared/DbMigrateLogPanel.tsx +++ b/ui/src/components/shared/DbMigrateLogPanel.tsx @@ -49,12 +49,17 @@ function DbMigrateLogPanel({ onClose }: DbMigrateLogPanelProps) {
DB Migration Logs + {/* + Panel her iki temada da koyu; buton varsayilan olarak acik temayi + izledigi icin renkler burada sabitlenir (aksi halde hover'da beyaz + zemin uzerine beyaz yazi kaliyordu). + */} {done && ( diff --git a/ui/src/proxy/developerKit/models.ts b/ui/src/proxy/developerKit/models.ts index a955de68..4d1808dc 100644 --- a/ui/src/proxy/developerKit/models.ts +++ b/ui/src/proxy/developerKit/models.ts @@ -12,16 +12,6 @@ export interface CrudEndpoint { lastModificationTime?: string } -export interface CreateUpdateCrudEndpointDto { - tenantId: string - entityName: string - method: 'GET' | 'POST' | 'PUT' | 'DELETE' - path: string - operationType: string - csharpCode: string - isActive: boolean -} - export interface CustomComponent { id: string tenantId?: string diff --git a/ui/src/routes/route.constant.ts b/ui/src/routes/route.constant.ts index 9454b654..59f6ef14 100644 --- a/ui/src/routes/route.constant.ts +++ b/ui/src/routes/route.constant.ts @@ -40,8 +40,6 @@ export const ROUTES_ENUM = { entities: '/admin/developerkit/entities', entitiesNew: '/admin/developerkit/entities/new', entitiesEdit: '/admin/developerkit/entities/edit/:id', - endpoints: '/admin/developerkit/endpoints', - endpointsNew: '/admin/developerkit/endpoints/new', components: '/admin/developerkit/components', componentsEdit: '/admin/developerkit/components/edit/:id', dynamicServices: '/admin/developerkit/dynamic-services', diff --git a/ui/src/services/developerKit.service.ts b/ui/src/services/developerKit.service.ts index 6084ea96..458546fa 100644 --- a/ui/src/services/developerKit.service.ts +++ b/ui/src/services/developerKit.service.ts @@ -2,7 +2,6 @@ import { PagedResultDto } from '@/proxy' import apiService from './api.service' import { CrudEndpoint, - CreateUpdateCrudEndpointDto, CreateUpdateCustomComponentDto, CustomComponent, CustomComponentDto, @@ -68,14 +67,6 @@ class DeveloperKitService { } // Generated Endpoint endpoints - async getActiveGeneratedEndpoints(): Promise> { - const response = await apiService.fetchData>({ - url: '/api/app/crud-endpoint-generate/active-endpoints', - method: 'GET', - }) - return response.data - } - async toggleGeneratedEndpoint(id: string): Promise { const response = await apiService.fetchData({ url: `/api/app/crud-endpoint-generate/${id}/toggle`, @@ -84,14 +75,37 @@ class DeveloperKitService { return response.data } - async generateCrudEndpoints(entityName: string): Promise> { + /** + * @param operationTypes Aktif baslatilacak operasyonlar; verilmezse hepsi aktif uretilir. + * Listede olmayanlar pasif olarak kaydedilir. + */ + async generateCrudEndpoints( + entityName: string, + operationTypes?: string[], + ): Promise> { const response = await apiService.fetchData>({ url: `/api/app/crud-endpoint-generate/generate-crud-endpoints/${entityName}`, method: 'POST', + params: operationTypes?.length ? { operationTypes: operationTypes.join(',') } : undefined, }) return response.data } + async getEndpointsByEntity(entityName: string): Promise { + const response = await apiService.fetchData({ + url: `/api/app/crud-endpoint-generate/endpoints-by-entity/${entityName}`, + method: 'GET', + }) + return response.data + } + + async deleteGeneratedEndpointsByEntity(entityName: string): Promise { + await apiService.fetchData({ + url: `/api/app/crud-endpoint-generate/by-entity/${entityName}`, + method: 'DELETE', + }) + } + async getGeneratedListEndpoints(): Promise> { const response = await apiService.fetchData>({ url: '/api/app/crud-endpoint-generate', @@ -101,35 +115,6 @@ class DeveloperKitService { return response.data } - async getGeneratedEndpoint(id: string): Promise { - const response = await apiService.fetchData({ - url: `/api/app/crud-endpoint-generate/${id}`, - method: 'GET', - }) - return response.data - } - - async createGeneratedEndpoint(data: CreateUpdateCrudEndpointDto): Promise { - const response = await apiService.fetchData({ - url: '/api/app/crud-endpoint-generate', - method: 'POST', - data: data as any, - }) - return response.data - } - - async updateGeneratedEndpoint( - id: string, - endpoint: CreateUpdateCrudEndpointDto, - ): Promise { - const response = await apiService.fetchData({ - url: `/api/app/crud-endpoint-generate/${id}`, - method: 'PUT', - data: endpoint as any, - }) - return response.data - } - async deleteGeneratedEndpoint(id: string): Promise { await apiService.fetchData({ url: `/api/app/crud-endpoint-generate/${id}`, diff --git a/ui/src/views/admin/listForm/wizard/WizardStep2.tsx b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx index 6a1196a4..dec85eb5 100644 --- a/ui/src/views/admin/listForm/wizard/WizardStep2.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx @@ -5,7 +5,7 @@ import { SelectBoxOption } from '@/types/shared' import { Field, FieldProps, FormikErrors, FormikTouched, useFormikContext } from 'formik' import { useState } from 'react' import CreatableSelect from 'react-select/creatable' -import { FaArrowLeft, FaArrowRight, FaPlus, FaTable } from 'react-icons/fa' +import { FaArrowLeft, FaArrowRight, FaBolt, FaPlus, FaTable } from 'react-icons/fa' import { dbSourceTypeOptions, listFormDefaultLayoutOptions, @@ -14,6 +14,10 @@ import { } from '../edit/options' import { ListFormWizardDto } from '@/proxy/admin/wizard/models' import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog' +import CrudEndpointDialog, { + CRUD_ENDPOINT_PERMISSION, +} from '@/views/developerKit/CrudEndpointDialog' +import { usePermission } from '@/utils/hooks/usePermission' // ─── Props ──────────────────────────────────────────────────────────────────── @@ -77,6 +81,10 @@ const WizardStep2 = ({ schemaName: string tableName: string } | null>(null) + const [showCrudEndpointDialog, setShowCrudEndpointDialog] = useState(false) + const { checkPermission } = usePermission() + /** CRUD endpoint uretimi/yonetimi yalnizca bu izin verildiginde gorunur. */ + const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION) const formik = useFormikContext() const selectedTable = values.selectCommand @@ -330,6 +338,18 @@ const WizardStep2 = ({ > Design Table + {canManageCrudEndpoints && ( + + )}
) }} @@ -790,6 +810,17 @@ const WizardStep2 = ({ initialTableData={designTableData} onDeployed={handleTableDeployed} /> + + {/* Secili tablonun CRUD endpoint'leri: uretme, aktif/pasif ve silme */} + {showCrudEndpointDialog && selectedTable && canManageCrudEndpoints && ( + setShowCrudEndpointDialog(false)} + /> + )}
) } diff --git a/ui/src/views/developerKit/CrudEndpointDialog.tsx b/ui/src/views/developerKit/CrudEndpointDialog.tsx new file mode 100644 index 00000000..58f5f195 --- /dev/null +++ b/ui/src/views/developerKit/CrudEndpointDialog.tsx @@ -0,0 +1,855 @@ +import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' +import { + FaBolt, + FaSyncAlt, + FaToggleOn, + FaToggleOff, + FaTrash, + FaCopy, + FaChevronDown, + FaChevronRight, + FaPaperPlane, + FaCheckCircle, + FaExclamationCircle, + FaList, + FaSearch, + FaPlus, + FaPen, +} from 'react-icons/fa' +import { Button, Dialog, Notification, toast } from '@/components/ui' +import { ConfirmDialog } from '@/components/shared' +import { developerKitService } from '@/services/developerKit.service' +import { sqlObjectManagerService } from '@/services/sql-query-manager.service' +import apiService from '@/services/api.service' +import type { CrudEndpoint } from '@/proxy/developerKit/models' +import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' +import { useLocalization } from '@/utils/hooks/useLocalization' +import { usePermission } from '@/utils/hooks/usePermission' + +// ─── Paylasilan yardimcilar ─────────────────────────────────────────────────── +// Endpoint'leri ureten/gosteren tum ekranlar (SqlQueryManager, SqlObjectExplorer, +// SqlTableDesignerDialog, ListForm Wizard) bu dosyadaki export'lari kullanir. + +/** + * CRUD endpoint uretimi/yonetimi icin gereken izin. + * Backend tarafinda PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints ile ayni. + */ +export const CRUD_ENDPOINT_PERMISSION = 'App.SqlQueryManager.CrudEndpoints' + +/** Uretilen endpoint operasyonlari; backend ile ayni sirada ve adlarla. */ +export const CRUD_OPERATION_TYPES = [ + 'GetList', + 'GetById', + 'Create', + 'Update', + 'Delete', +] as const + +export type CrudOperationType = (typeof CRUD_OPERATION_TYPES)[number] + +/** + * Endpoint'lerde kullanilan entity adini turetir. + * + * Entity adi calisma zamaninda `DynamicEntityManager` tarafindan dogrudan tablo adi + * olarak SQL'e girer (`SELECT * FROM {entityName}`), bu yuzden tablo adi aynen korunur; + * yalnizca `dbo.` gibi sema oneki ayiklanir. Ad normalize edilirse (`Adm_D_Approval` → + * `AdmDApproval`) uretilen endpoint calisma aninda olmayan bir tabloyu sorgular. + */ +export const toEntityName = (tableName: string): string => + (tableName || '').replace(/^.*\./g, '').trim() + +export const METHOD_COLOR: Record = { + GET: 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800', + POST: 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800', + PUT: 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-800', + DELETE: + 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800', +} + +interface OperationMeta { + icon: ReactNode + /** Aktif endpoint rengi; pasif olanlar gri gosterilir. */ + color: string +} + +const OPERATION_META: Record = { + GetList: { icon: , color: 'text-blue-500' }, + GetById: { icon: , color: 'text-sky-500' }, + Create: { icon: , color: 'text-green-500' }, + Update: { icon: , color: 'text-amber-500' }, + Delete: { icon: , color: 'text-red-500' }, +} + +export const getOperationMeta = (operationType: string): OperationMeta => + OPERATION_META[operationType] ?? { icon: , color: 'text-gray-400' } + +/** Endpoint'leri CRUD_OPERATION_TYPES sirasina gore siralar; bilinmeyenler sona gider. */ +export const sortByCrudOperation = (endpoints: T[]): T[] => { + const order = (operationType: string) => { + const index = CRUD_OPERATION_TYPES.indexOf(operationType as CrudOperationType) + return index === -1 ? CRUD_OPERATION_TYPES.length : index + } + return [...endpoints].sort((a, b) => order(a.operationType) - order(b.operationType)) +} + +/** Endpoint listesini entity adina gore gruplar. */ +export const groupEndpointsByEntity = (endpoints: CrudEndpoint[]): Map => { + const map = new Map() + for (const endpoint of endpoints) { + const list = map.get(endpoint.entityName) + if (list) list.push(endpoint) + else map.set(endpoint.entityName, [endpoint]) + } + // Explorer rozetleri de diyalogla ayni sirada cizilir. + for (const [entityName, list] of map) { + map.set(entityName, sortByCrudOperation(list)) + } + return map +} + +export const getErrorMessage = (error: unknown, fallback: string): string => { + const response = ( + error as { response?: { data?: { error?: { message?: string }; message?: string } } } + )?.response + return ( + response?.data?.error?.message || + response?.data?.message || + (error instanceof Error ? error.message : undefined) || + fallback + ) +} + +interface CrudEndpointBadgesProps { + endpoints: CrudEndpoint[] + /** Ikon uzerindeki tooltip metnini uretir: (operationType, method, path, isActive) */ + buildTitle: (endpoint: CrudEndpoint) => string + onClick?: () => void +} + +/** + * Bir tablonun acilmis CRUD endpoint'lerini kucuk ikonlarla gosterir. + * Aktif olanlar renkli, pasif olanlar soluk gri cizilir; tooltip operasyonu aciklar. + */ +export const CrudEndpointBadges = ({ endpoints, buildTitle, onClick }: CrudEndpointBadgesProps) => { + if (endpoints.length === 0) return null + + return ( + { + e.stopPropagation() + onClick() + } + : undefined + } + > + {endpoints.map((endpoint) => { + const meta = getOperationMeta(endpoint.operationType) + return ( + + {meta.icon} + + ) + })} + + ) +} + +// ─── Dialog ─────────────────────────────────────────────────────────────────── + +interface CrudEndpointDialogProps { + isOpen: boolean + /** Endpoint'leri yonetilecek tablo; entity adi tablo adindan turetilir. */ + tableName: string + schemaName?: string + /** Verilirse istek govdesi tablonun gercek kolonlarindan uretilir. */ + dataSourceCode?: string | null + onClose: () => void + /** Endpoint listesi degistiginde cagrilir; cagiran taraf kendi state'ini tazeler. */ + onChanged?: (entityName: string, endpoints: CrudEndpoint[]) => void +} + +interface TestResult { + success: boolean + status: number + durationMs: number + payload: string +} + +interface EndpointParameter { + name: string + type: 'path' | 'query' + value: string +} + +/** Create istegine gonderilmemesi gereken ABP audit/sistem kolonlari. */ +const SYSTEM_COLUMNS = new Set( + [ + 'Id', + 'TenantId', + 'CreationTime', + 'CreatorId', + 'LastModificationTime', + 'LastModifierId', + 'IsDeleted', + 'DeleterId', + 'DeletionTime', + 'ConcurrencyStamp', + 'ExtraProperties', + ].map((name) => name.toLowerCase()), +) + +const EMPTY_GUID = '00000000-0000-0000-0000-000000000000' + +/** SQL veri tipine gore ornek govde degeri uretir. */ +const getSampleValue = (column: DatabaseColumnDto): unknown => { + const type = (column.dataType || '').toLowerCase() + + if (column.isNullable) return null + if (type.includes('guid') || type === 'uuid') return EMPTY_GUID + if (type.includes('bit') || type.includes('bool')) return false + if ( + type.includes('int') || + type.includes('decimal') || + type.includes('numeric') || + type.includes('money') || + type.includes('float') || + type.includes('real') || + type.includes('double') + ) { + return 0 + } + if (type.includes('date') || type.includes('time')) return new Date().toISOString() + return '' +} + +const GENERIC_BODY = JSON.stringify({ name: 'Sample', isActive: true }, null, 2) + +/** Endpoint'in path'indeki {placeholder} adlarini cikarir. */ +const getPathParameterNames = (path: string): string[] => + [...(path || '').matchAll(/\{(\w+)\}/g)].map((match) => match[1]) + +const needsBody = (endpoint: CrudEndpoint) => + endpoint.operationType === 'Create' || endpoint.operationType === 'Update' + +/** + * Tek bir tablonun CRUD endpoint'lerini yonetir: uretme/yeniden uretme, tekil + * aktif/pasif, tekil silme, tumunu silme ve endpoint'i parametreleriyle calistirip + * sonucunu gorme. SqlQueryManager ve ListForm Wizard ayni diyalogu kullanir. + */ +const CrudEndpointDialog = ({ + isOpen, + tableName, + schemaName, + dataSourceCode, + onClose, + onChanged, +}: CrudEndpointDialogProps) => { + const { translate } = useLocalization() + const { checkPermission } = usePermission() + const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION) + const entityName = toEntityName(tableName) + + const [endpoints, setEndpoints] = useState([]) + const [columns, setColumns] = useState([]) + const [loading, setLoading] = useState(false) + const [generating, setGenerating] = useState(false) + const [deletingAll, setDeletingAll] = useState(false) + const [busyId, setBusyId] = useState(null) + const [expandedId, setExpandedId] = useState(null) + const [confirmDeleteAll, setConfirmDeleteAll] = useState(false) + const [testingId, setTestingId] = useState(null) + const [testResults, setTestResults] = useState>({}) + const [parameterValues, setParameterValues] = useState>>({}) + const [requestBodies, setRequestBodies] = useState>({}) + + const pushError = useCallback((message: string) => { + toast.push( + + {message} + , + { placement: 'bottom-end' }, + ) + }, []) + + const pushSuccess = useCallback((message: string) => { + toast.push( + + {message} + , + { placement: 'bottom-end' }, + ) + }, []) + + const publish = useCallback( + (next: CrudEndpoint[]) => { + // Liste veritabani sirasiyla gelir; kullaniciya her zaman CRUD sirasinda gosterilir. + const sorted = sortByCrudOperation(next) + setEndpoints(sorted) + onChanged?.(entityName, sorted) + }, + [entityName, onChanged], + ) + + useEffect(() => { + if (!isOpen || !entityName) return + let cancelled = false + + setLoading(true) + setExpandedId(null) + setTestResults({}) + developerKitService + .getEndpointsByEntity(entityName) + .then((items) => { + if (!cancelled) setEndpoints(sortByCrudOperation(items)) + }) + .catch((err) => { + if (!cancelled) pushError(getErrorMessage(err, translate('::App.Platform.ErrorLoading'))) + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + + return () => { + cancelled = true + } + }, [isOpen, entityName, pushError, translate]) + + // Govde sablonu icin tablo kolonlari; datasource bilinmiyorsa genel sablon kullanilir. + useEffect(() => { + if (!isOpen || !dataSourceCode) { + setColumns([]) + return + } + let cancelled = false + + sqlObjectManagerService + .getTableColumns(dataSourceCode, schemaName || 'dbo', tableName) + .then((res) => { + if (!cancelled) setColumns(res.data || []) + }) + .catch(() => { + // Kolonlar okunamazsa tester genel govde sablonuyla calismaya devam eder. + if (!cancelled) setColumns([]) + }) + + return () => { + cancelled = true + } + }, [isOpen, dataSourceCode, schemaName, tableName]) + + const sampleBodies = useMemo(() => { + if (columns.length === 0) return { create: GENERIC_BODY, update: GENERIC_BODY } + + const build = (includeId: boolean) => { + const body: Record = {} + for (const column of columns) { + const isSystem = SYSTEM_COLUMNS.has(column.columnName.toLowerCase()) + const isId = column.columnName.toLowerCase() === 'id' + if (isSystem && !(includeId && isId)) continue + body[column.columnName] = isId ? EMPTY_GUID : getSampleValue(column) + } + return JSON.stringify(body, null, 2) + } + + return { create: build(false), update: build(true) } + }, [columns]) + + const getDefaultBody = useCallback( + (endpoint: CrudEndpoint) => + endpoint.operationType === 'Update' ? sampleBodies.update : sampleBodies.create, + [sampleBodies], + ) + + const getParameters = useCallback( + (endpoint: CrudEndpoint): EndpointParameter[] => { + const values = parameterValues[endpoint.id] + const valueOf = (name: string, fallback: string) => values?.[name] ?? fallback + + const pathParams: EndpointParameter[] = getPathParameterNames(endpoint.path).map((name) => ({ + name, + type: 'path', + value: valueOf(name, name.toLowerCase() === 'id' ? EMPTY_GUID : ''), + })) + + if (endpoint.operationType !== 'GetList') return pathParams + + return [ + ...pathParams, + { name: 'SkipCount', type: 'query', value: valueOf('SkipCount', '0') }, + { name: 'MaxResultCount', type: 'query', value: valueOf('MaxResultCount', '10') }, + { name: 'Sorting', type: 'query', value: valueOf('Sorting', '') }, + ] + }, + [parameterValues], + ) + + const handleGenerate = async () => { + setGenerating(true) + try { + const result = await developerKitService.generateCrudEndpoints(entityName) + publish(result.items || []) + pushSuccess(translate('::App.Platform.OperationCompleted')) + } catch (err) { + pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz'))) + } finally { + setGenerating(false) + } + } + + const handleDeleteAll = async () => { + setDeletingAll(true) + try { + await developerKitService.deleteGeneratedEndpointsByEntity(entityName) + publish([]) + pushSuccess(translate('::App.Platform.ObjectDeletedSuccessfully')) + } catch (err) { + pushError(getErrorMessage(err, translate('::App.Platform.ErrorDeleting'))) + } finally { + setDeletingAll(false) + } + } + + const handleToggle = async (endpoint: CrudEndpoint) => { + setBusyId(endpoint.id) + try { + const updated = await developerKitService.toggleGeneratedEndpoint(endpoint.id) + publish(endpoints.map((ep) => (ep.id === endpoint.id ? updated : ep))) + } catch (err) { + pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz'))) + } finally { + setBusyId(null) + } + } + + const handleDelete = async (endpoint: CrudEndpoint) => { + setBusyId(endpoint.id) + try { + await developerKitService.deleteGeneratedEndpoint(endpoint.id) + publish(endpoints.filter((ep) => ep.id !== endpoint.id)) + } catch (err) { + pushError(getErrorMessage(err, translate('::App.Platform.ErrorDeleting'))) + } finally { + setBusyId(null) + } + } + + /** + * Endpoint'i doldurulan parametre ve govde ile cagirir. Istek apiService uzerinden + * gider; Authorization, __tenant ve Accept-Language interceptor'lari uygulanir. + */ + const handleTest = async (endpoint: CrudEndpoint) => { + setTestingId(endpoint.id) + const startedAt = performance.now() + + try { + const parameters = getParameters(endpoint) + const pathValues = new Map( + parameters.filter((p) => p.type === 'path').map((p) => [p.name, p.value]), + ) + + const url = endpoint.path.replace(/\{(\w+)\}/g, (match, name: string) => { + const value = pathValues.get(name) + return value ? encodeURIComponent(value) : match + }) + + const params = Object.fromEntries( + parameters + .filter((p) => p.type === 'query' && p.value !== '') + .map((p) => [p.name, p.value]), + ) + + let data: unknown + if (needsBody(endpoint)) { + const raw = requestBodies[endpoint.id] ?? getDefaultBody(endpoint) + try { + data = JSON.parse(raw) + } catch { + setTestResults((prev) => ({ + ...prev, + [endpoint.id]: { + success: false, + status: 0, + durationMs: 0, + payload: translate('::App.Platform.InvalidJson'), + }, + })) + return + } + } + + const response = await apiService.fetchData({ + method: endpoint.method, + url, + params: Object.keys(params).length > 0 ? params : undefined, + data, + timeout: 15000, + }) + + setTestResults((prev) => ({ + ...prev, + [endpoint.id]: { + success: true, + status: response.status, + durationMs: Math.round(performance.now() - startedAt), + payload: JSON.stringify(response.data, null, 2), + }, + })) + } catch (error: unknown) { + const axiosError = error as { response?: { status?: number; data?: unknown }; message?: string } + setTestResults((prev) => ({ + ...prev, + [endpoint.id]: { + success: false, + status: axiosError.response?.status ?? 0, + durationMs: Math.round(performance.now() - startedAt), + payload: JSON.stringify(axiosError.response?.data ?? axiosError.message, null, 2), + }, + })) + } finally { + setTestingId(null) + } + } + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text) + pushSuccess(translate('::App.Platform.Copied')) + } catch (err) { + pushError(getErrorMessage(err, translate('::App.Platform.CopyFailed'))) + } + } + + const activeCount = endpoints.reduce((sum, ep) => sum + (ep.isActive ? 1 : 0), 0) + const busy = loading || generating || deletingAll + + // Izin yoksa dialog hic acilmaz; cagiran ekranlar da butonu gizler. + if (!canManageCrudEndpoints) return null + + return ( + + +
+
{translate('::App.SqlQueryManager.CrudEndpoints')}
+

+ {schemaName ? `${schemaName}.${tableName}` : tableName} + {' — '} + {entityName} +

+
+ +
+ {loading ? ( +
+ + {translate('::App.Platform.LoadingWithThreeDot')} +
+ ) : endpoints.length === 0 ? ( +
+ +

+ {translate('::App.DeveloperKitCrudEndpoints.NoEndpointsYet')} +

+

+ {translate('::App.DeveloperKitCrudEndpoints.ClickToCreate')} +

+
+ ) : ( +
+ {endpoints.map((ep) => { + const meta = getOperationMeta(ep.operationType) + const isExpanded = expandedId === ep.id + const parameters = isExpanded ? getParameters(ep) : [] + const testResult = testResults[ep.id] + const isTesting = testingId === ep.id + return ( +
+
+
+ + {isExpanded && ( +
+ {parameters.length > 0 && ( +
+

+ {translate('::App.DeveloperKitCrudEndpoints.Parameters')} +

+
+ {parameters.map((param) => ( +
+ + {param.name} + + + setParameterValues((prev) => ({ + ...prev, + [ep.id]: { ...prev[ep.id], [param.name]: e.target.value }, + })) + } + className="flex-1 rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-900 focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" + /> +
+ ))} +
+
+ )} + + {needsBody(ep) && ( +
+
+ + {translate('::App.DeveloperKitCrudEndpoints.RequestBody')} + + +
+