Mcp problemlerini giderildi, Wizard silme işlemi değiştirildi.

This commit is contained in:
Sedat ÖZTÜRK 2026-09-24 16:29:53 +03:00
parent dd6759f46c
commit 9a14be4ee4
29 changed files with 1010 additions and 60 deletions

View file

@ -1265,7 +1265,7 @@ soneklerini alır.
| **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
| **Developer Kit › Intranet Widgets** | `App.DeveloperKit.IntranetWidgets` | `Sas_T_IntranetWidget` | tenant | Intranet panosunda hangi widget'ın hangi kolonda (`Column`), hangi sırada (`Order`) ve hangi yetkiyle (`PermissionName`) çizileceği; `SourceType` `Builtin` (koddaki `Code`) ya da `Custom` (`ComponentName` ile bir custom component). Seed: `data/App.DeveloperKit.IntranetWidgets.json`. |
| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `AllowedTools` (boşsa tüm araçlar, doluysa yalnızca seçilenler), `MaxRows`. Yetkiler: ekran `App.DeveloperKit.McpServers`, erişim ve okuma `.Invoke`, her değişiklik `.Write` (kiracıda satır yazma, host'ta ayrıca tablo/ekran/bileşen geliştirme) — arkasındaki ekran C/U/D, Wizard ve Custom Components yetkileri ayrıca istenir. Kiracı veri okur ve yazar, geliştirme yapamaz (`TenantTools`: şema araçları, `query_list`, `run_select`, `insert_row`/`update_row`/`delete_row`); ham SQL `App.SqlQueryManager` ister; MCP yetki vermez. Kayıtlar `data/App.DeveloperKit.McpServers.json`'a senkronlanır. Ayrıntı: `README.md` §13.9. |
| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/{kapsam}/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. |
| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/{kapsam}/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran, veri aynası ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır; onay diyaloğu ekranın tablosunu sorar, seçilirse `WizardTableCleaner` tabloyu scripti ve crud dosyasıyla düşürür — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. |
| **Forum Management › Categories / Topics / Posts** | `App.ForumManagement.Categories` · `.Topics` · `.Posts` | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Forumun yönetim tarafı: kategori (seed `data/App.ForumManagement.Categories.json`), konu (kategori lookup'ı, sayaçlar, sabitle/kilitle/çözüldü bayrakları), gönderi (konu lookup'ı, kabul edilen yanıt). Kullanıcı tarafı Administration'daki `/admin/forum`. |
> Menü kodu ile yetki adı **her zaman aynı değildir**: Wizard Manager'da kod

View file

@ -504,10 +504,11 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz.
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
| `deploy_wizard` | Wizard cevabından ekranı, menüyü, yetkileri, dil anahtarlarını ve seed dosyasını üretir | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). İstek demo veri taşıyorsa `SeedData: true` (§0.10.1) |
| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler | Bağımlı wizard varsa reddeder (§4.5) |
| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) |
| `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) |
| `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + resim içerikleri) | Yarım kalan talebe dönmek için; `App.DevRequests` ister |
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler | Sonuç notu için; `Description`'ı `update_row` ile yazma (§0.11) |
| `add_dev_request_subtasks` | `SubTasks` listesine tamamlanmış satırlar ekler | Atanan = bağlanan kullanıcı, bitiş = bugün (sunucu yazar); `SubTasks`'ı `update_row` ile yazma (§0.11) |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | `passed` yalnızca engelleyici bir kontrol kalırsa `false`; `seed_closure` engellemez ama rapora yazılır (§0.11) |
| `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) |
@ -585,8 +586,10 @@ talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `St
| 5b. Kaldı | `update_row` → `In Progress` + sonuç kaydı; düzelt ve 4'e dön ya da bırak | In Progress |
**Sonuç kaydı** şema değiştirmeden mevcut alanlara yazılır:
- `SubTasks` — her üretilen artefakt bir satır: `{ "id": "<guid>", "title": "deploy_wizard: Mrp.Orders.json", "assignee": "", "dueDate": null, "completed": true }`.
Var olan satırları koru, sona ekle (alan JSON dizisi metnidir).
- `SubTasks` — her üretilen artefakt bir satır, `add_dev_request_subtasks` ile yalnızca başlık
verilerek (`"deploy_wizard: Mrp.Orders.json"`). Araç satırı tamamlanmış yazar, atanan kişiyi
bağlanan kullanıcı, bitiş tarihini bugün yapar ve var olan satırları korur. `SubTasks`
`update_row` ile yazılmaz.
- `Description` — `append_dev_request_note` ile sonuna `<h4>MCP</h4>` başlığıyla `verify_wizard`
özeti: geçen/kalan kontroller, `seed_closure` uyarıları. `Description` hiçbir zaman `update_row`
ile yazılmaz — base64 görselleri taşır ve yeniden yazmak onları siler.
@ -1476,6 +1479,23 @@ modül, veri kaynağı ve admin dışındaki rollerin yetki atamaları korunur
kullanılırlar; silinip üretilseler menü sırası ve rol atamaları kaybolurdu. Wizard'ın yarattığı
modül `InsertedRecords.ModuleIds` ile izlenir.
Silme yolunda ekranın veri aynası (`ListForm.SeedFilePath` → `data/{ListFormCode}.json` ya da
klasörü) ekranla birlikte silinir; ekransız uygulanamaz.
**Ekranın tablosu** wizard'ın ürettiği bir kayıt değildir (tasarımcı ya da `create_table` ile
ayrıca kurulur, `InsertedRecords`'ta yoktur) ve **kendiliğinden silinmez**. Silme onayı önce
`DeleteAnalysis` ile adayı sorar (`WizardTableCleaner`): yalnızca `Table` tipli ekranın tablosu
aday olur ve iki koşulla seçilebilir — kapsamdaki `{sql|postgres}/object/{Tablo}.sql` bir
`CREATE TABLE` scriptidir (platform tabloları bu klasörde durmaz; scriptsiz tablo `NotManaged`) ve
tabloyu başka bir ekran (sorgu/C/U/D komutu ya da bu ekranı alt form olarak kullanan üst ekran),
Custom Endpoint SQL'i ya da custom component veri kaynağı kullanmaz (`InUse`). Kullanıcı onay
diyaloğunda tabloyu seçerse (`WizardDeleteInput.DropTables`) kayıt temizliğinden **sonra**
`DROP TABLE` çalışır, ardından tablonun CRUD endpoint'leri ve seed dosyaları
(`{sql|postgres}/object/{Tablo}.sql`, `crud/{Tablo}.json`) silinir. Düşürme hatası (ör. yabancı
anahtar) silmeyi durdurmaz: tablo ve scripti yerinde kalır, rapor `Failed` + mesaj gösterir.
Tablo düşürmek `App.SqlQueryManager` yetkisi ister; wizard silme yetkisi tek başına yetmez.
Düzenleme (`Redeploy`) yolu tabloya hiç dokunmaz.
### 4.6 Alt formlar ve widget'lar
**`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar:
@ -4057,9 +4077,10 @@ tedarikçi lookup'ı + iki kademeli onay).
Endpoint…) ekranın `data/` dosyasına yazıldı; `SeedFilePath`'i olmayan ekranın kaydı için
`sql/execute/` altında geri getirici prosedür var (§9.14).
- [ ] Export zip'i başka bir ortamda açıldığında ekran ayağa kalkıyor.
- [ ] Geri alma yolu yazılı: Wizard Manager'dan ya da MCP `delete_wizard` ile sil → dosya ve
`InsertedRecords` içindeki kullanılmayan kayıtlar gider (§4.5). Dosyayı diskten silmek
kayıtları temizlemez.
- [ ] Geri alma yolu yazılı: Wizard Manager'dan ya da MCP `delete_wizard` ile sil → dosya,
veri aynası ve `InsertedRecords` içindeki kullanılmayan kayıtlar gider; tablo yalnızca
onayda seçilirse (MCP: `dropTables: true`) scripti ve crud dosyasıyla düşer (§4.5).
Dosyayı diskten silmek kayıtları temizlemez.
Prompt ile üretim yaptıysan ek olarak:

View file

@ -1358,7 +1358,11 @@ ardışık düzeninden sonra bağlanır ve açıkça bearer şemasına bağlıd
sayfasına 302 ile değil, 401 ile döner); kiracı token talebinden ya da `__tenant` başlığından
çözülür. Taşıyıcı **durumsuz** kiptedir
(`HttpServerSessionMode.Stateless`): her istek kendi ABP kapsamında koşar, ters vekil
arkasında oturum yapışkanlığı gerekmez.
arkasında oturum yapışkanlığı gerekmez. `/mcp` istek düzeyindeki unit of work'ün **dışındadır**
(`AbpAspNetCoreUnitOfWorkOptions.IgnoredUrls`): cevap SSE ile aktığı için istek UoW'si sonuç
istemciye gittikten sonra commit edilir ve bağlantı kapanınca geri alınırdı — araç başarılı
görünür, EF kayıtları (ListForm, menü, yetki) kaybolur, DDL ve seed dosyası kalırdı. Her MCP
servis çağrısı kendi UoW'sini sonuç dönmeden tamamlar.
**Kiracı bazlı sözleşme.** `Sas_H_McpServer` tablosundaki kayıt (ekran:
`App.DeveloperKit.McpServers`) hangi araçların yayınlandığını ve sorguların hangi satır tavanıyla koşacağını
@ -1382,7 +1386,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| Araç | İş |
| --- | --- |
| `server_info` | Bağlantının kiracısı, açık araçlar, veri sözlüğünün tazelik bilgisi. Her zaman açıktır. |
| `server_info` | Bağlantının bağlamı (`scope`: `host` / `tenant`; kiracı adı `server.tenantName`), açık araçlar, veri sözlüğünün tazelik bilgisi. Sözlüğün `dictionaryKey` alanı (host'ta `Sozsoft`) sözlüğün yazıldığı anahtardır, kiracı bağlamını göstermez. Her zaman açıktır. |
| `list_lists` | Listeler: kod, başlık, açıklama, arkasındaki tablo. |
| `describe_list` | Bir listenin sütunları: alan adı, görünen başlık, SQL tipi, lookup ilişkisi, `isSensitive`. |
| `search_schema` | Liste ve sütun adları/başlıkları üzerinde aksan ve büyük/küçük harf duyarsız arama. |
@ -1395,10 +1399,11 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. `replaceFileName` doluysa güncellemedir (eski dosya ve ürettiği kayıtlar silinip yeniden üretilir). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — ardından satırlar `insert_row` ile eklenir; `ListFormSeedDataSynchronizer` onları `configs/seeds/host/data/` altına yazar, migrate ekranla birlikte geri yükler (`lowcode.instructions.md` §0.10.1). |
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: yalnızca `InsertedRecords`, bağımlı wizard varsa reddedilir. |
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). `App.DevRequests` geliştirme döngüsünün test adımıdır (`lowcode.instructions.md` §0.11). |
| `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. |
| `get_dev_request` | Talebi durumuna dokunmadan, `start_dev_request` ile aynı biçimde (metin + resim içerikleri) okur; yarım kalan talebe dönmek için. `App.DevRequests` ister. |
| `add_dev_request_subtasks` | Talebin `SubTasks` listesine tamamlanmış satırlar ekler: model yalnızca başlığı verir, atanan kişi bağlanan kullanıcının adı (`userName`), bitiş tarihi bugün (`yyyy-MM-dd`) olarak sunucuda yazılır; var olan satırlar korunur. `.Write` + `App.DevRequests.Update` ister. |
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler, var olan metne ve görsellere dokunmaz. Açıklama base64 görsel taşıdığı için `update_row` ile yeniden yazılmaz. `.Write` + `App.DevRequests.Update` ister. |
**Prompt'lar** (`McpPromptNames`, `McpDevelopPrompts`). İstemcide komut olarak görünür; yalnızca
@ -1446,7 +1451,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| --- | --- | --- | --- |
| `App.DeveloperKit.McpServers` | Host, kiracı | MCP Servers ekranını görme (`.Create/.Update/.Delete/.Export/.Import` ekran işlemleri) | `/mcp` ucuna erişim vermez |
| `App.DeveloperKit.McpServers.Invoke` | Host, kiracı | `/mcp` bağlantısı ve bütün okuma araçları: `server_info`, `list_lists`, `describe_list`, `search_schema`, `query_list`; host'ta ayrıca `get_dev_guide`, `list_wizards`, `get_wizard`, `verify_wizard`, `list_components`, `get_component` | Hiçbir şey yazmaz. Okuma da ekran yetkisine bağlıdır: `R` yetkisi olmayan liste görünmez, sorgulanamaz. Ham SQL (`run_select`, `list_queries`, `run_query`) ayrıca `App.SqlQueryManager` ister |
| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `start_dev_request`, `append_dev_request_note` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz |
| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `start_dev_request`, `append_dev_request_note`, `add_dev_request_subtasks` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz |
**Araç → gereken yetkiler** (Invoke hepsinde aranır, tabloda tekrar yazılmadı)
@ -1461,7 +1466,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| `list_wizards`, `get_wizard` | — | — | `App.Listforms.Wizard` |
| `verify_wizard` | — | — | `App.Listforms.Wizard`; seed kapanışı için `.Export` (yoksa o kontrol atlanır, doğrulama düşmez) |
| `deploy_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Create` (yeni) / `.Update` (`replaceFileName`) |
| `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete` |
| `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete`; `dropTables` için ayrıca `App.SqlQueryManager` |
| `create_table` | — | `.Write` | `App.SqlQueryManager` |
| `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` |
| `save_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Create` (yeni) / `.Update` |
@ -1469,6 +1474,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| `start_dev_request` | — | `.Write` | `App.DevRequests.Update` |
| `get_dev_request` | — | — | `App.DevRequests` |
| `append_dev_request_note` | — | `.Write` | `App.DevRequests.Update` |
| `add_dev_request_subtasks` | — | `.Write` | `App.DevRequests.Update` |
Kaynak: MCP kapısı `McpToolNames.RequiredPermissions`, kiracı kümesi `McpToolNames.TenantTools`,
arkadaki servisin attribute ile istediği yetkiler `McpToolNames.ServicePermissions`. `Invoke` her MCP

View file

@ -36,7 +36,12 @@ public class McpSchemaInfoDto
public DateTime? RefreshedAt { get; set; }
public int FormCount { get; set; }
public int FieldCount { get; set; }
public string? TenantName { get; set; }
/// <summary>
/// Sozlugun yazildigi anahtar (prosedurun <c>@TenantName</c>'i). Host'ta da dolu gelir;
/// istegin kiraci baglamini gostermez — o <c>server_info</c>'nun <c>scope</c> alanidir.
/// </summary>
public string? DictionaryKey { get; set; }
public string? DefinitionCultureName { get; set; }
public string? UiCultureName { get; set; }

View file

@ -25,7 +25,8 @@ public interface IMcpDevelopAppService : IApplicationService
/// <summary>Deploy edilen wizard'in ozeti.</summary>
Task<WizardFileInfoDto?> DeployWizardAsync(McpWizardDeployRequestDto input);
Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName);
/// <summary>Wizard'i siler; <paramref name="dropTables"/> acikken ekranin secilebilir tablosunu da dusurur.</summary>
Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName, bool dropTables);
/// <summary>Deploy edilmis ekrani veri yazmadan dogrular.</summary>
Task<McpVerifyResultDto> VerifyWizardAsync(string fileName);
@ -42,6 +43,12 @@ public interface IMcpDevelopAppService : IApplicationService
/// </summary>
Task AppendDevRequestNoteAsync(string request, string html);
/// <summary>
/// Talebin SubTasks listesine tamamlanmis satirlar ekler; atanan kisi cagiran kullanici,
/// bitis tarihi bugundur.
/// </summary>
Task AddDevRequestSubTasksAsync(string request, List<string> titles);
Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search);
Task<CustomComponentDto?> GetComponentAsync(string name);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
@ -49,6 +50,9 @@ public class McpDevelopAppService(
private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update";
/// <summary>Talep ekranindaki alt gorev tarih kutusunun okudugu bicim.</summary>
private const string SubTaskDateFormat = "yyyy-MM-dd";
/// <summary>verify_wizard cevabindaki kontrol adlari.</summary>
private static class VerifyChecks
{
@ -162,11 +166,22 @@ public class McpDevelopAppService(
return files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase));
}
public async Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName)
/// <summary>
/// Ekranin tablosu ancak <paramref name="dropTables"/> acikken ve Wizard Manager'daki onayla ayni
/// kosullarda (secilebilir aday) dusurulur; onayi kullanicidan almak istemcinin isidir.
/// </summary>
public async Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName, bool dropTables)
{
await EnsureToolAllowedAsync(McpToolNames.DeleteWizard);
return await wizardAppService.DeleteFile(fileName);
var input = new WizardDeleteInput { FileName = fileName };
if (dropTables)
{
var analysis = await wizardAppService.DeleteAnalysis(fileName);
input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)];
}
return await wizardAppService.DeleteFile(input);
}
/// <summary>
@ -280,6 +295,55 @@ public class McpDevelopAppService(
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
}
/// <summary>
/// Atanan kisi ve tarih modelin dikkatine birakilmaz: satiri yazan cagiran kullanicidir
/// (ekranin atanan listesi kullanici adiyla calisir), bitis tarihi yazildigi gundur.
/// Var olan satirlara dokunulmaz.
/// </summary>
public async Task AddDevRequestSubTasksAsync(string request, List<string> titles)
{
await EnsureToolAllowedAsync(McpToolNames.AddDevRequestSubTasks);
await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
var items = (titles ?? []).Select(title => title?.Trim()).Where(title => !string.IsNullOrEmpty(title)).ToList();
if (items.Count == 0)
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksRequired]);
}
var devRequest = await FindDevRequestAsync(request);
JsonArray subTasks;
try
{
subTasks = string.IsNullOrWhiteSpace(devRequest.SubTasks)
? []
: JsonNode.Parse(devRequest.SubTasks) as JsonArray ?? throw new JsonException();
}
catch (JsonException)
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksInvalid, devRequest.Title ?? devRequest.Id.ToString()]);
}
var assignee = CurrentUser.UserName ?? string.Empty;
var dueDate = Clock.Now.ToString(SubTaskDateFormat, CultureInfo.InvariantCulture);
foreach (var title in items)
{
subTasks.Add(new JsonObject
{
["id"] = GuidGenerator.Create().ToString(),
["title"] = title,
["assignee"] = assignee,
["dueDate"] = dueDate,
["completed"] = true
});
}
devRequest.SubTasks = subTasks.ToJsonString();
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
}
private static McpDevRequestDto ToDto(DevRequest devRequest)
{
var (description, images) = McpDevRequestImages.Extract(devRequest.Description);

View file

@ -15,8 +15,6 @@ namespace Sozsoft.Mcp;
/// </summary>
internal static class McpLoadOptions
{
private static readonly string Delimiter = PlatformConsts.MultiValueDelimiter.ToString();
public static string? ToSort(string? json)
=> Convert(json, item => $"{Selector(item)} {Direction(item)}");
@ -54,7 +52,7 @@ internal static class McpLoadOptions
.Select(map)
.ToList();
return parts.Count == 0 ? null : string.Join(Delimiter, parts);
return parts.Count == 0 ? null : string.Join(PlatformConsts.MultiValueDelimiter, parts);
}
private static string Selector(JsonElement item) => Text(item, "selector");

View file

@ -61,7 +61,7 @@ public class McpSchemaAppService(
return new McpSchemaInfoDto
{
Available = false,
TenantName = dictionary.ResolveTenantName(),
DictionaryKey = dictionary.ResolveTenantName(),
Message = L[McpErrorCodes.DictionaryNotFound, McpConsts.DictionaryProcedureName]
};
}
@ -73,7 +73,7 @@ public class McpSchemaAppService(
RefreshedAt = snapshot.RefreshedAt,
FormCount = snapshot.FormCount,
FieldCount = snapshot.FieldCount,
TenantName = snapshot.TenantName,
DictionaryKey = snapshot.TenantName,
DefinitionCultureName = snapshot.DefinitionCultureName,
UiCultureName = snapshot.UiCultureName
};

View file

@ -15,6 +15,13 @@ public static class McpConsts
/// </summary>
public const string DefaultDictionaryTenantName = "Sozsoft";
/// <summary><c>server_info</c> cevabindaki istek baglami.</summary>
public static class Scopes
{
public const string Host = "host";
public const string Tenant = "tenant";
}
/// <summary>Sunucu kaydinda satir tavani verilmediginde (ya da sifir oldugunda) uygulanan deger.</summary>
public const int DefaultMaxRows = 200;

View file

@ -50,6 +50,8 @@ public static class McpErrorCodes
public const string DevRequestAmbiguous = Prefix + "DevRequestAmbiguous";
public const string DevRequestNotReady = Prefix + "DevRequestNotReady";
public const string DevRequestNoteRequired = Prefix + "DevRequestNoteRequired";
public const string DevRequestSubTasksRequired = Prefix + "DevRequestSubTasksRequired";
public const string DevRequestSubTasksInvalid = Prefix + "DevRequestSubTasksInvalid";
public const string VisualDesignerComponent = Prefix + "VisualDesignerComponent";
public const string TableColumnsRequired = Prefix + "TableColumnsRequired";
public const string UnsupportedColumnType = Prefix + "UnsupportedColumnType";

View file

@ -32,6 +32,7 @@ public static class McpToolNames
public const string StartDevRequest = "start_dev_request";
public const string GetDevRequest = "get_dev_request";
public const string AppendDevRequestNote = "append_dev_request_note";
public const string AddDevRequestSubTasks = "add_dev_request_subtasks";
public const string ListComponents = "list_components";
public const string GetComponent = "get_component";
public const string SaveComponent = "save_component";
@ -54,6 +55,7 @@ public static class McpToolNames
StartDevRequest,
GetDevRequest,
AppendDevRequestNote,
AddDevRequestSubTasks,
ListComponents,
GetComponent,
SaveComponent,
@ -105,6 +107,7 @@ public static class McpToolNames
[DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[AppendDevRequestNote] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[AddDevRequestSubTasks] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[DeleteComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
};
@ -122,7 +125,7 @@ public static class McpToolNames
[GetWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[VerifyWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[DeployWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[DeleteWizard] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Delete],
[DeleteWizard] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Delete, PlatformConsts.AppCodes.SqlQueryManagers.Default],
};
/// <summary>

View file

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
@ -73,11 +73,13 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
=> await developAppService.DeployWizardAsync(new McpWizardDeployRequestDto { Wizard = wizard, ReplaceFileName = replaceFileName });
[McpServerTool(Name = McpToolNames.DeleteWizard, Destructive = true, Idempotent = true)]
[Description("Bir wizard'i ve yalnizca onun urettigi kayitlari siler; baska yerde kullanilan kayitlar korunur ve raporda gosterilir. Bagimli wizard'i olan bir wizard silinemez, once bagimlilar silinir.")]
[Description("Bir wizard'i ve yalnizca onun urettigi kayitlari siler; baska yerde kullanilan kayitlar korunur ve raporda gosterilir. Ekranin veri aynasi (data/{ListFormCode}.json) ekranla birlikte gider. Bagimli wizard'i olan bir wizard silinemez, once bagimlilar silinir. Ekranin tablosu varsayilan olarak kalir; dropTables true ise tablo DROP edilir, CRUD endpoint'leri ve sql/object + crud seed dosyalari da silinir. Tablo yalnizca create_table ya da tablo tasarimcisiyla olusturulmussa (sql/object scripti varsa) ve baska ekran, Custom Endpoint ya da bilesen kullanmiyorsa dusurulur; aksi halde raporda neden korundugu yazar. Tablo silmek geri alinamaz ve App.SqlQueryManager yetkisi ister: dropTables true gondermeden once kullaniciya tablonun ve icindeki verinin silinecegini soyle ve acik onay al.")]
public async Task<WizardDeleteResultDto> DeleteWizardAsync(
[Description("Silinecek wizard dosyasinin adi (list_wizards cikisindaki fileName).")]
string fileName)
=> await developAppService.DeleteWizardAsync(fileName);
string fileName,
[Description("Ekranin tablosu da dusurulsun mu. Yalnizca kullanici acikca onayladiysa true.")]
bool dropTables = false)
=> await developAppService.DeleteWizardAsync(fileName, dropTables);
[McpServerTool(Name = McpToolNames.VerifyWizard, ReadOnly = true)]
[Description("Deploy edilmis bir wizard'i veri yazmadan dogrular: menu kaydi (var mi, etkin mi, URL'si wizard'la ayni mi), menunun istedigi yetki tanimi, liste ekraninda ekranin sorgusu (cagiranin yetkisiyle tek satir), Custom yolunda bilesen (var mi, etkin mi, rotasi menuyle ayni mi) ve seed kapanisi (migrate ya da baska ortamda eksik kalacak bagimlilik). passed yalnizca blocking kontrollerden biri kalirsa false olur; seed_closure engellemez ama raporlanmalidir. deploy_wizard'dan sonra ve bir gelistirme talebini Staging durumuna almadan once cagir.")]
@ -112,6 +114,18 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
return "ok";
}
[McpServerTool(Name = McpToolNames.AddDevRequestSubTasks, Destructive = false, Idempotent = false)]
[Description("Talebin Sub Tasks listesine uretilen her artefakt icin tamamlanmis bir satir ekler; atanan kisi baglanan kullanici, bitis tarihi bugun olarak sunucuda yazilir, var olan satirlar korunur. SubTasks alanini update_row ile yazma.")]
public async Task<string> AddDevRequestSubTasksAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request,
[Description("Eklenecek satir basliklari, ornegin [\"create_table: Hr_T_Certificate\", \"deploy_wizard: Certificates.json\"].")]
List<string> titles)
{
await developAppService.AddDevRequestSubTasksAsync(request, titles);
return "ok";
}
/// <summary>
/// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu
/// bicimde gorur, base64 metni olarak goremez.
@ -122,7 +136,8 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
foreach (var image in devRequest.Images)
{
content.Add(ImageContentBlock.FromBytes(Convert.FromBase64String(image.Data), image.MimeType));
// Veri zaten base64; cozup yeniden kodlamak buyuk gorsellerde bosuna bellek harcar.
content.Add(new ImageContentBlock { Data = Encoding.UTF8.GetBytes(image.Data), MimeType = image.MimeType });
}
return content;

View file

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.Threading.Tasks;
using ModelContextProtocol.Server;
using Sozsoft.Mcp.Dtos;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Mcp.Tools;
@ -12,17 +13,21 @@ namespace Sozsoft.Mcp.Tools;
/// ekranda gordugu adlarla ayni olur.
/// </summary>
[McpServerToolType]
public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService)
public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService, ICurrentTenant currentTenant)
{
[McpServerTool(Name = McpToolNames.ServerInfo, ReadOnly = true)]
[Description("Bu baglantinin kiracisini, acik araclari ve veri sozlugunun tazelik bilgisini dondurur. Baska bir arac beklendigi gibi calismiyorsa once bunu cagir.")]
[Description("Bu baglantinin baglamini (scope: host ya da tenant; kiraci adi server.tenantName), acik araclari ve veri sozlugunun tazelik bilgisini dondurur. Baska bir arac beklendigi gibi calismiyorsa once bunu cagir.")]
public async Task<McpServerInfoResult> ServerInfoAsync()
{
var config = await schemaAppService.GetServerConfigAsync();
var schema = await schemaAppService.GetSchemaInfoAsync();
var availableTools = await schemaAppService.GetAvailableToolsAsync();
return new McpServerInfoResult(config, schema, availableTools);
return new McpServerInfoResult(
currentTenant.IsAvailable ? McpConsts.Scopes.Tenant : McpConsts.Scopes.Host,
config,
schema,
availableTools);
}
[McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)]
@ -51,10 +56,12 @@ public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService)
=> await schemaAppService.SearchAsync(term, maxResults);
}
/// <param name="Scope">Istegin baglami: <c>host</c> ya da <c>tenant</c>. Gelistirme araclari yalnizca host'ta aciktir.</param>
/// <param name="Server">Istegin kiracisina dusen sunucu kaydi; yoksa null.</param>
/// <param name="Schema">Veri sozlugunun tazelik bilgisi.</param>
/// <param name="AvailableTools">Bu baglantida cagrilabilen araclar.</param>
public sealed record McpServerInfoResult(
string Scope,
McpServerConfigDto? Server,
McpSchemaInfoDto Schema,
IReadOnlyList<string> AvailableTools);

View file

@ -9,7 +9,10 @@ public interface IListFormWizardAppService
Task Create(ListFormWizardDto input);
Task<List<WizardFileInfoDto>> GetFiles();
Task<WizardSeedFileDto> GetFile(string fileName);
Task<WizardDeleteResultDto> DeleteFile(string fileName);
Task<WizardDeleteResultDto> DeleteFile(WizardDeleteInput input);
/// <summary>Silme onayinda sorulacak tablolari ve tabloyla birlikte gidecek seed dosyalarini dondurur.</summary>
Task<WizardDeleteAnalysisDto> DeleteAnalysis(string fileName);
/// <summary>Izin gruplarini iki dilli gorunen adlariyla dondurur.</summary>
Task<List<WizardPermissionGroupDto>> GetPermissionGroups();

View file

@ -386,6 +386,9 @@ public class WizardDeleteLogItemDto
public WizardDeleteStatusEnum Status { get; set; }
public WizardDeleteReasonEnum Reason { get; set; }
public List<WizardRecordRefDto> UsedBy { get; set; } = [];
/// <summary><see cref="WizardDeleteReasonEnum.Failed"/> durumunda veritabaninin hata mesaji.</summary>
public string Message { get; set; }
}
/// <summary>Wizard silme raporu; silinen, korunan ve bulunamayan her kayit tek tek listelenir.</summary>
@ -396,6 +399,52 @@ public class WizardDeleteResultDto
public List<WizardDeleteLogItemDto> Items { get; set; } = [];
}
/// <summary>
/// Silme onayinda gosterilen, wizard'in ekranina bagli tablo. Tablo wizard'in urettigi bir kayit
/// degildir (<see cref="WizardInsertedRecordsDto"/> icinde yoktur); bu yuzden kendiliginden
/// silinmez, kullanici onay diyalogunda secerse dusurulur.
/// </summary>
public class WizardDeleteTableDto
{
public string DataSourceCode { get; set; }
public string SchemaName { get; set; }
public string TableName { get; set; }
/// <summary>Tablo veri kaynaginda var mi; yoksa yalnizca seed dosyalari silinir.</summary>
public bool Exists { get; set; }
public long? RowCount { get; set; }
/// <summary>Tablo secilebilir mi; degilse neden <see cref="Reason"/>, kullananlar <see cref="UsedBy"/>.</summary>
public bool CanDrop { get; set; }
public WizardDeleteReasonEnum Reason { get; set; }
public List<WizardRecordRefDto> UsedBy { get; set; } = [];
/// <summary>Tabloyla birlikte silinecek seed dosyalari, kapsam klasorune gore goreli.</summary>
public List<string> Files { get; set; } = [];
}
/// <summary>Silmeden once onay diyaloguna gelen analiz.</summary>
public class WizardDeleteAnalysisDto
{
public string FileName { get; set; }
public string WizardName { get; set; }
public List<WizardDeleteTableDto> Tables { get; set; } = [];
}
public class WizardDeleteInput
{
public string FileName { get; set; }
/// <summary>
/// Onay diyalogunda secilen tablolar (<see cref="WizardDeleteTableDto.TableName"/>). Yalnizca
/// analizde <see cref="WizardDeleteTableDto.CanDrop"/> olanlar dusurulur; bos ise hicbir tabloya
/// dokunulmaz.
/// </summary>
public List<string> DropTables { get; set; } = [];
}
/// <summary>Wizard seed dosyası özet bilgisi (yönetim listesi için).</summary>
public class WizardFileInfoDto
{

View file

@ -57,6 +57,7 @@ public class ListFormWizardAppService(
IListFormSeedDataApplier seedDataApplier,
ICrudEndpointSeedApplier crudEndpointSeedApplier,
WizardRecordCleaner recordCleaner,
WizardTableCleaner tableCleaner,
ILogger<ListFormWizardAppService> logger
) : PlatformAppService(), IListFormWizardAppService
{
@ -77,6 +78,7 @@ public class ListFormWizardAppService(
private readonly IListFormSeedDataApplier _seedDataApplier = seedDataApplier;
private readonly ICrudEndpointSeedApplier _crudEndpointSeedApplier = crudEndpointSeedApplier;
private readonly WizardRecordCleaner _recordCleaner = recordCleaner;
private readonly WizardTableCleaner _tableCleaner = tableCleaner;
private readonly ILogger<ListFormWizardAppService> logger = logger;
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
@ -776,10 +778,37 @@ public class ListFormWizardAppService(
[UnitOfWork]
[Authorize(AppCodes.Listforms.Wizards.Delete)]
public async Task<WizardDeleteResultDto> DeleteFile(string fileName)
[HttpPost("api/app/list-form-wizard/delete-file")]
public async Task<WizardDeleteResultDto> DeleteFile(WizardDeleteInput input)
{
await EnsureNoDependentWizardsAsync(fileName);
return await DeleteFileInternalAsync(fileName, WizardDeleteMode.Delete);
Check.NotNull(input, nameof(input));
var dropTables = input.DropTables?.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).ToList() ?? [];
// Tablo dusurmek SQL Query Manager'in isidir; wizard silme yetkisi tek basina tablo silmez.
if (dropTables.Count > 0)
await AuthorizationService.CheckAsync(AppCodes.SqlQueryManagers.Default);
await EnsureNoDependentWizardsAsync(input.FileName);
return await DeleteFileInternalAsync(input.FileName, WizardDeleteMode.Delete, dropTables);
}
/// <summary>
/// Silme onayinda sorulacak tablolari dondurur. Tablo wizard'in urettigi bir kayit olmadigi icin
/// kendiliginden silinmez; kullanici burada gordugu adaylardan secer (<see cref="WizardTableCleaner"/>).
/// </summary>
[Authorize(AppCodes.Listforms.Wizards.Delete)]
[HttpGet("api/app/list-form-wizard/delete-analysis")]
public async Task<WizardDeleteAnalysisDto> DeleteAnalysis(string fileName)
{
var seed = await GetFile(fileName);
return new WizardDeleteAnalysisDto
{
FileName = fileName,
WizardName = seed.Wizard?.WizardName ?? fileName,
Tables = await _tableCleaner.AnalyzeAsync(seed),
};
}
/// <summary>
@ -806,7 +835,7 @@ public class ListFormWizardAppService(
/// akibeti rapora ve loga yazilir (<see cref="WizardRecordCleaner"/>). Yetki kontrolu cagiran
/// uca aittir: silme yolunda <c>Delete</c>, duzenleme yolunda <c>Update</c> politikasi kullanilir.
/// </summary>
private async Task<WizardDeleteResultDto> DeleteFileInternalAsync(string fileName, WizardDeleteMode mode)
private async Task<WizardDeleteResultDto> DeleteFileInternalAsync(string fileName, WizardDeleteMode mode, List<string>? dropTables = null)
{
var filePath = ResolveWizardSeedFilePath(fileName);
@ -819,9 +848,21 @@ public class ListFormWizardAppService(
WizardName = seed?.Wizard?.WizardName ?? fileName,
};
// Tablolar ekran silinmeden once analiz edilir: kullanim kontrolu ekranin kendisini disarida
// birakir ve baglanti, veri kaynagi kaydi temizlenmeden once cozulur. Duzenleme yolunda
// tabloya hic dokunulmaz.
var tables = mode == WizardDeleteMode.Delete && seed != null
? await _tableCleaner.AnalyzeAsync(seed)
: [];
var connections = await _tableCleaner.ResolveConnectionsAsync(
tables.Where(t => dropTables?.Contains(t.TableName, StringComparer.OrdinalIgnoreCase) == true));
if (seed != null)
result.Items.AddRange(await _recordCleaner.CleanAsync(seed, mode));
if (tables.Count > 0)
await _tableCleaner.DropAsync(tables, dropTables ?? [], connections, result.Items);
File.Delete(filePath);
result.Items.Add(new WizardDeleteLogItemDto
{

View file

@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages.Entities;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Extensions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement;
@ -46,6 +50,8 @@ public class WizardRecordCleaner(
IRepository<BackgroundWorker, Guid> repoBackgroundWorker,
IRepository<IntranetWidget, Guid> repoIntranetWidget,
ITenantRepository tenantRepository,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<WizardRecordCleaner> logger) : ITransientDependency
{
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
@ -131,9 +137,46 @@ public class WizardRecordCleaner(
{
await repoListForm.DeleteAsync(listForm, autoSave: true);
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.Deleted);
if (mode == WizardDeleteMode.Delete)
DeleteSeedDataMirror(listForm.SeedFilePath, report);
}
}
/// <summary>
/// Ekranin veri aynasi (<c>ListForm.SeedFilePath</c>) ekransiz uygulanamaz; ekranla birlikte
/// gider. Duzenlemede ekran ayni yolla yeniden kurulacagi icin dokunulmaz.
/// </summary>
private void DeleteSeedDataMirror(string? seedFilePath, List<WizardDeleteLogItemDto> report)
{
var relativePath = seedFilePath?.Replace('\\', '/').Trim('/');
if (string.IsNullOrWhiteSpace(relativePath) ||
relativePath.Contains("..", StringComparison.Ordinal) ||
!relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase))
{
return;
}
var path = SeedPathResolver.GetScopePath(configuration, currentTenant.Id, relativePath.Split('/'));
if (SeedPathResolver.IsSeedFolderPath(relativePath))
{
if (!Directory.Exists(path))
return;
Directory.Delete(path, recursive: true);
}
else
{
if (!File.Exists(path))
return;
File.Delete(path);
}
Add(report, WizardRecordTypeEnum.SeedFile, relativePath, WizardDeleteStatusEnum.Deleted);
logger.LogInformation("Wizard seed data mirror deleted: {Path}", path);
}
private static string FieldName(ListFormField field) => $"{field.ListFormCode}.{field.FieldName}";
private static string CriteriaName(ListFormWorkflow criteria) =>

View file

@ -0,0 +1,373 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Wizard silinirken ekranin tablosunu ve tabloya bagli seed artefaktlarini (tablo scripti,
/// CRUD endpoint'leri ve <c>crud/{Tablo}.json</c>) kaldirir.
/// <para>
/// Tablo wizard'in urettigi bir kayit degildir; tasarimci ya da MCP <c>create_table</c> ile ayrica
/// olusturulur. Bu yuzden kendiliginden silinmez: once <see cref="AnalyzeAsync"/> adaylari dondurur,
/// kullanici onay diyalogunda secer, <see cref="DropAsync"/> yalnizca secilenleri dusurur.
/// </para>
/// <para>
/// Aday yalnizca <c>Table</c> tipli ekranin tablosudur ve iki kosulla secilebilir: tablonun
/// <c>{sql|postgres}/object/{Tablo}.sql</c> altinda <c>CREATE TABLE</c> scripti vardir (platform
/// tablolari bu klasorde durmaz; scriptsiz tablo elle ya da migration ile gelmistir) ve baska bir
/// ekran, Custom Endpoint ya da custom component tabloyu kullanmaz.
/// </para>
/// </summary>
public class WizardTableCleaner(
IRepository<ListForm, Guid> repoListForm,
IRepository<CustomEndpoint, Guid> repoCustomEndpoint,
IRepository<CustomComponent, Guid> repoCustomComponent,
IRepository<CrudEndpoint, Guid> repoCrudEndpoint,
IDynamicDataManager dynamicDataManager,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<WizardTableCleaner> logger) : ITransientDependency
{
private const int MaxUsedBy = 20;
public async Task<List<WizardDeleteTableDto>> AnalyzeAsync(WizardSeedFileDto seed)
{
var wizard = seed?.Wizard;
if (wizard == null || wizard.ComponentKind == WizardComponentKindEnum.Custom)
return [];
var listFormCode = wizard.ResolveListFormCode();
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
// Ekran tasarimcida degismis olabilir; kayit varsa esas odur.
var dataSourceCode = listForm?.DataSourceCode ?? wizard.DataSourceCode;
var commandType = listForm?.SelectCommandType ?? wizard.SelectCommandType;
var selectCommand = listForm?.SelectCommand ?? wizard.SelectCommand;
if (commandType != SelectCommandTypeEnum.Table ||
string.IsNullOrWhiteSpace(selectCommand) ||
string.IsNullOrWhiteSpace(dataSourceCode))
{
return [];
}
var (schemaName, tableName) = ParseTableName(selectCommand);
var table = new WizardDeleteTableDto
{
DataSourceCode = dataSourceCode,
SchemaName = schemaName,
TableName = tableName,
Files = FindSeedFiles(tableName),
};
await ReadTableStateAsync(table);
table.SchemaName ??= "dbo";
var usedBy = await FindUsagesAsync(table, listFormCode);
if (usedBy.Count > 0)
{
table.Reason = WizardDeleteReasonEnum.InUse;
table.UsedBy = [.. usedBy
.DistinctBy(u => (u.RecordType, u.Name?.ToLowerInvariant()))
.Take(MaxUsedBy)];
}
else if (!HasCreateTableScript(tableName))
{
table.Reason = WizardDeleteReasonEnum.NotManaged;
}
else
{
table.CanDrop = true;
}
return [table];
}
/// <summary>
/// Secilen tablolari dusurur ve tabloya bagli seed artefaktlarini siler; secilmeyen ya da
/// secilemeyen adaylar rapora nedeniyle yazilir. Veritabani hatasi silmeyi durdurmaz: tablo
/// yerinde kalir, hata raporda gosterilir ve tablonun scripti de korunur.
/// <para>
/// Kayit temizliginden sonra cagrilir ki temizlik hata verirse geri alinamaz bir dusurme
/// yapilmamis olsun. Temizlik veri kaynagi kaydini silmis olabilecegi icin baglantilar ondan
/// once <see cref="ResolveConnectionsAsync"/> ile alinir.
/// </para>
/// </summary>
public async Task DropAsync(
List<WizardDeleteTableDto> tables,
IReadOnlyCollection<string> selected,
Dictionary<string, TableConnection> connections,
List<WizardDeleteLogItemDto> report)
{
foreach (var table in tables)
{
var name = QualifiedName(table);
if (!table.CanDrop)
{
report.Add(new WizardDeleteLogItemDto
{
RecordType = WizardRecordTypeEnum.Table,
Name = name,
Status = WizardDeleteStatusEnum.Kept,
Reason = table.Reason,
UsedBy = table.UsedBy,
});
continue;
}
if (!selected.Contains(table.TableName, StringComparer.OrdinalIgnoreCase))
{
report.Add(new WizardDeleteLogItemDto
{
RecordType = WizardRecordTypeEnum.Table,
Name = name,
Status = WizardDeleteStatusEnum.Kept,
Reason = WizardDeleteReasonEnum.NotSelected,
});
continue;
}
if (table.Exists && !await TryDropTableAsync(table, connections.GetValueOrDefault(table.DataSourceCode), report))
continue;
if (!table.Exists)
report.Add(new WizardDeleteLogItemDto { RecordType = WizardRecordTypeEnum.Table, Name = name, Status = WizardDeleteStatusEnum.NotFound });
await DeleteCrudEndpointsAsync(table.TableName, report);
DeleteSeedFiles(table.Files, report);
}
}
/// <summary>Veri kaynagi kayitlari temizlenmeden once her tablonun baglantisini cozer.</summary>
public async Task<Dictionary<string, TableConnection>> ResolveConnectionsAsync(IEnumerable<WizardDeleteTableDto> tables)
{
var result = new Dictionary<string, TableConnection>(StringComparer.OrdinalIgnoreCase);
foreach (var code in tables.Where(t => t.CanDrop && t.Exists).Select(t => t.DataSourceCode).Distinct(StringComparer.OrdinalIgnoreCase))
{
var (repo, connectionString, dbType) = await dynamicDataManager.GetAsync(false, code);
result[code] = new TableConnection(repo, connectionString, dbType);
}
return result;
}
// ── Analiz ─────────────────────────────────────────────────────────────────────
private async Task ReadTableStateAsync(WizardDeleteTableDto table)
{
try
{
var (repo, connectionString, dbType) = await dynamicDataManager.GetAsync(false, table.DataSourceCode);
table.SchemaName ??= dbType == DataSourceTypeEnum.Postgresql ? "public" : "dbo";
var parameters = new Dictionary<string, object>
{
["schemaName"] = table.SchemaName,
["tableName"] = table.TableName,
};
var exists = await repo.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = @schemaName AND table_name = @tableName AND table_type = 'BASE TABLE'",
connectionString,
parameters);
table.Exists = exists > 0;
if (!table.Exists)
return;
// Tablo adi parametre olarak baglanamaz; yalnizca katalogda var oldugu dogrulanan ad
// tirnaklanarak kullanilir.
table.RowCount = await repo.ExecuteScalarAsync<long>(
$"SELECT COUNT(*) FROM {QuoteIdentifier(table.SchemaName, table.TableName, dbType)}",
connectionString);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Wizard table state could not be read: {DataSource}.{Table}", table.DataSourceCode, table.TableName);
}
}
private async Task<List<WizardRecordRefDto>> FindUsagesAsync(WizardDeleteTableDto table, string ownListFormCode)
{
var tableName = table.TableName;
var pattern = TablePattern(tableName);
var usedBy = new List<WizardRecordRefDto>();
// Ekran baska bir ekranin alt formuysa silinmez (WizardRecordCleaner); tablosu da kalir.
var quotedCode = $"\"{ownListFormCode}\"";
usedBy.AddRange((await repoListForm.GetListAsync(a =>
a.ListFormCode != ownListFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quotedCode)))
.Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
// Veritabani Contains ile on suzer, kelime siniri bellekte kontrol edilir; aksi halde
// "Tags" tablosu "TagsGroup" kullanan ekrani kullanan sayardi.
var listForms = await repoListForm.GetListAsync(a =>
a.ListFormCode != ownListFormCode &&
a.DataSourceCode == table.DataSourceCode &&
((a.SelectCommand != null && a.SelectCommand.Contains(tableName)) ||
(a.InsertCommand != null && a.InsertCommand.Contains(tableName)) ||
(a.UpdateCommand != null && a.UpdateCommand.Contains(tableName)) ||
(a.DeleteCommand != null && a.DeleteCommand.Contains(tableName))));
usedBy.AddRange(listForms
.Where(l => new[] { l.SelectCommand, l.InsertCommand, l.UpdateCommand, l.DeleteCommand }
.Any(c => c != null && pattern.IsMatch(c)))
.Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
var endpoints = await repoCustomEndpoint.GetListAsync(a =>
a.DataSourceCode == table.DataSourceCode && a.Sql != null && a.Sql.Contains(tableName));
usedBy.AddRange(endpoints
.Where(e => pattern.IsMatch(e.Sql))
.Select(e => Ref(WizardRecordTypeEnum.CustomEndpoint, e.Name)));
// Bilesenin veri kaynaklari CRUD endpoint'lerinin entity adini tasir (tablo adi).
var quotedTable = $"\"{tableName}\"";
usedBy.AddRange((await repoCustomComponent.GetListAsync(a => a.DataSources != null && a.DataSources.Contains(quotedTable)))
.Select(c => Ref(WizardRecordTypeEnum.CustomComponent, c.Name)));
return usedBy;
}
private static Regex TablePattern(string tableName) =>
new($@"(?<![\w]){Regex.Escape(tableName)}(?![\w])", RegexOptions.IgnoreCase);
private List<string> FindSeedFiles(string tableName)
{
var fileName = SeedPathResolver.GetRowFileName(tableName);
if (fileName == null)
return [];
var scriptName = Path.ChangeExtension(fileName, ".sql");
var candidates = new[]
{
$"{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{scriptName}",
$"{SeedPathResolver.PostgresFolder}/{SeedPathResolver.ObjectFolder}/{scriptName}",
$"{SeedPathResolver.CrudFolder}/{fileName}",
};
return [.. candidates.Where(path => File.Exists(ScopeFile(path)))];
}
private bool HasCreateTableScript(string tableName)
{
var scriptName = Path.ChangeExtension(SeedPathResolver.GetRowFileName(tableName) ?? string.Empty, ".sql");
foreach (var provider in new[] { SeedPathResolver.SqlFolder, SeedPathResolver.PostgresFolder })
{
var path = ScopeFile($"{provider}/{SeedPathResolver.ObjectFolder}/{scriptName}");
if (File.Exists(path) && Regex.IsMatch(File.ReadAllText(path), @"\bCREATE\s+TABLE\b", RegexOptions.IgnoreCase))
return true;
}
return false;
}
// ── Dusurme ────────────────────────────────────────────────────────────────────
private async Task<bool> TryDropTableAsync(WizardDeleteTableDto table, TableConnection? connection, List<WizardDeleteLogItemDto> report)
{
var name = QualifiedName(table);
try
{
if (connection == null)
throw new InvalidOperationException($"'{table.DataSourceCode}' veri kaynağı çözülemedi.");
await connection.Repository.ExecuteAsync(
$"DROP TABLE {QuoteIdentifier(table.SchemaName, table.TableName, connection.DataSourceType)}",
connection.ConnectionString);
report.Add(new WizardDeleteLogItemDto { RecordType = WizardRecordTypeEnum.Table, Name = name, Status = WizardDeleteStatusEnum.Deleted });
logger.LogInformation("Wizard table dropped: {DataSource}.{Table}", table.DataSourceCode, name);
return true;
}
catch (Exception ex)
{
// Yabanci anahtar gibi bir engel tabloyu yerinde birakir; scripti de korunur ki tablo
// ile seed klasoru birbirinden kopmasin.
logger.LogWarning(ex, "Wizard table could not be dropped: {DataSource}.{Table}", table.DataSourceCode, name);
report.Add(new WizardDeleteLogItemDto
{
RecordType = WizardRecordTypeEnum.Table,
Name = name,
Status = WizardDeleteStatusEnum.Kept,
Reason = WizardDeleteReasonEnum.Failed,
Message = ex.Message,
});
return false;
}
}
/// <summary>
/// Tablonun CRUD endpoint'leri tabloyla birlikte anlamsizlasir; SQL Explorer'daki tablo
/// dusurme akisiyla ayni kural.
/// </summary>
private async Task DeleteCrudEndpointsAsync(string tableName, List<WizardDeleteLogItemDto> report)
{
var endpoints = await repoCrudEndpoint.GetListAsync(a => a.EntityName == tableName);
if (endpoints.Count == 0)
return;
await repoCrudEndpoint.DeleteManyAsync(endpoints, autoSave: true);
foreach (var endpoint in endpoints)
{
report.Add(new WizardDeleteLogItemDto
{
RecordType = WizardRecordTypeEnum.CrudEndpoint,
Name = $"{endpoint.Method} {endpoint.Path}",
Status = WizardDeleteStatusEnum.Deleted,
});
}
}
private void DeleteSeedFiles(IEnumerable<string> files, List<WizardDeleteLogItemDto> report)
{
foreach (var file in files)
{
var path = ScopeFile(file);
if (!File.Exists(path))
continue;
File.Delete(path);
report.Add(new WizardDeleteLogItemDto { RecordType = WizardRecordTypeEnum.SeedFile, Name = file, Status = WizardDeleteStatusEnum.Deleted });
logger.LogInformation("Wizard table seed file deleted: {FilePath}", path);
}
}
// ── Yardimcilar ────────────────────────────────────────────────────────────────
private string ScopeFile(string relativePath) =>
SeedPathResolver.GetScopePath(configuration, currentTenant.Id, relativePath.Split('/'));
/// <summary>Sema yazilmamissa null doner; varsayilan sema saglayiciya gore <see cref="ReadTableStateAsync"/>'ta atanir.</summary>
private static (string? SchemaName, string TableName) ParseTableName(string selectCommand)
{
var parts = selectCommand.Split('.');
var schemaName = parts.Length > 1 ? parts[^2].Trim().Trim('"', '[', ']') : null;
return (schemaName, parts[^1].Trim().Trim('"', '[', ']'));
}
private static string QuoteIdentifier(string schemaName, string tableName, DataSourceTypeEnum dbType) =>
dbType == DataSourceTypeEnum.Postgresql
? $"\"{schemaName.Replace("\"", "\"\"")}\".\"{tableName.Replace("\"", "\"\"")}\""
: $"[{schemaName.Replace("]", "]]")}].[{tableName.Replace("]", "]]")}]";
private static string QualifiedName(WizardDeleteTableDto table) => $"{table.SchemaName}.{table.TableName}";
private static WizardRecordRefDto Ref(WizardRecordTypeEnum type, string name) => new() { RecordType = type, Name = name };
}
/// <summary>Tablonun dusurulecegi baglanti; veri kaynagi kaydi silinmeden once cozulur.</summary>
public sealed record TableConnection(IDynamicDataRepository Repository, string ConnectionString, DataSourceTypeEnum DataSourceType);

View file

@ -16632,6 +16632,90 @@
"en": "Tenant",
"tr": "Kiracı"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardRecordType.Table",
"en": "Table",
"tr": "Tablo"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardRecordType.CrudEndpoint",
"en": "CRUD endpoint",
"tr": "CRUD endpoint"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardRecordType.CustomComponent",
"en": "Custom component",
"tr": "Custom component"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteReason.NotSelected",
"en": "Not selected for deletion",
"tr": "Silinmek üzere seçilmedi"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteReason.NotManaged",
"en": "Not created with the table designer or create_table; it cannot be dropped from here",
"tr": "Tablo tasarımcısı ya da create_table ile oluşturulmamış; buradan silinemez"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteReason.Failed",
"en": "Operation failed",
"tr": "İşlem başarısız"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteTables",
"en": "Screen table",
"tr": "Ekranın tablosu"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteTablesHint",
"en": "The table is kept unless you select it. A selected table is dropped together with its CRUD endpoints and seed files (sql/object, crud).",
"tr": "Seçmezseniz tablo veritabanında kalır. Seçilen tablo CRUD endpoint'leri ve seed dosyalarıyla (sql/object, crud) birlikte silinir."
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteTablesLoading",
"en": "Checking the screen table...",
"tr": "Ekranın tablosu kontrol ediliyor..."
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteTableRows",
"en": "{0} rows",
"tr": "{0} kayıt"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteTableMissing",
"en": "not in the database",
"tr": "veritabanında yok"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteTableWarning",
"en": "The table and all of its data will be permanently deleted. This cannot be undone. Files to be deleted:",
"tr": "Tablo ve içindeki tüm veriler kalıcı olarak silinecek. Bu işlem geri alınamaz. Silinecek dosyalar:"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteTableNoPermission",
"en": "Dropping a table requires the SQL Query Manager permission.",
"tr": "Tablo silmek için SQL Query Manager yetkisi gerekir."
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardDeleteWithTables",
"en": "Delete with table",
"tr": "Tabloyla birlikte sil"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WizardFileExport",
@ -28818,6 +28902,18 @@
"en": "The note to append cannot be empty.",
"tr": "Eklenecek not boş olamaz."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.DevRequestSubTasksRequired",
"en": "At least one sub task title is required.",
"tr": "En az bir alt görev başlığı gerekir."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.DevRequestSubTasksInvalid",
"en": "The Sub Tasks of development request \"{0}\" could not be read; fix it on the request screen first.",
"tr": "\"{0}\" geliştirme talebinin alt görevleri okunamadı; önce talep ekranından düzeltin."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VisualDesignerComponent",

View file

@ -19,10 +19,17 @@ public enum WizardRecordTypeEnum
DataSource = 9,
LanguageKey = 10,
/// <summary>Ekranin tablosu; yalnizca silme onayinda secildiyse dusurulur.</summary>
Table = 11,
/// <summary>Dusurulen tablonun CRUD endpoint'leri.</summary>
CrudEndpoint = 12,
CustomEndpoint = 20,
BackgroundWorker = 21,
IntranetWidget = 22,
Tenant = 23,
CustomComponent = 24,
}
/// <summary>Silme raporunda bir kaydin akibeti.</summary>
@ -47,4 +54,16 @@ public enum WizardDeleteReasonEnum
/// <summary>Duzenleme yolunda paylasilan kayit korunur; deploy onu yeniden kullanir.</summary>
Redeploy = 2,
/// <summary>Tablo silme onayinda secilmedi; tablo ve ona bagli seed dosyalari yerinde kalir.</summary>
NotSelected = 3,
/// <summary>
/// Tablo tasarimcida ya da MCP <c>create_table</c> ile olusturulmamis (seed scripti yok);
/// platform tablosu olabilecegi icin silme onayinda secilemez.
/// </summary>
NotManaged = 4,
/// <summary>Islem veritabaninda hata verdi; ayrinti raporun mesajindadir.</summary>
Failed = 5,
}

View file

@ -52,6 +52,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.AspNetCore.Uow;
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.BlobStoring;
@ -86,6 +87,8 @@ namespace Sozsoft.Platform;
)]
public class PlatformHttpApiHostModule : AbpModule
{
private const string McpEndpointPath = "/mcp";
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
@ -151,6 +154,7 @@ public class PlatformHttpApiHostModule : AbpModule
context.Services.AddHostedService<Extensions.ChangeLogStartupService>();
ConfigureBlobStoring(configuration);
ConfigureAuditing();
ConfigureUnitOfWork();
context.Services.AddResponseCompression(options =>
{
@ -413,6 +417,20 @@ public class PlatformHttpApiHostModule : AbpModule
});
}
/// <summary>
/// MCP cevabi SSE ile akar: istek duzeyindeki unit of work arac sonucu istemciye gittikten sonra
/// commit edilir ve istemci baglantiyi kapattiginda (RequestAborted) geri alinir. Arac basarili
/// gorunur ama EF kayitlari kaybolur; DDL ve seed dosyasi ise kalir. Uc bu yuzden istek
/// UoW'sinin disindadir; her servis cagrisi kendi UoW'sini sonuc donmeden tamamlar.
/// </summary>
private void ConfigureUnitOfWork()
{
Configure<AbpAspNetCoreUnitOfWorkOptions>(options =>
{
options.IgnoredUrls.Add(McpEndpointPath);
});
}
private void ConfigureDynamicServices(ServiceConfigurationContext context)
{
// Dynamic AppService Background Service
@ -567,14 +585,15 @@ public class PlatformHttpApiHostModule : AbpModule
endpoints.MapHub<MessengerHub>("/messengerhub");
endpoints.MapHub<NotificationHub>("/notificationhub");
// MCP (Model Context Protocol) ucu. Kimlik dogrulama, kiraci cozumu ve unit of work
// yukaridaki ardisik duzenden gelir. Uc yalnizca kimlik ister; Invoke her MCP
// servisinin sinif duzeyindeki [Authorize] attribute'unda aranir. Uca konsaydi yetkisiz
// MCP (Model Context Protocol) ucu. Kimlik dogrulama ve kiraci cozumu yukaridaki
// ardisik duzenden gelir; istek UoW'si bu uca uygulanmaz (ConfigureUnitOfWork). Uc
// yalnizca kimlik ister; Invoke her MCP servisinin sinif duzeyindeki [Authorize]
// attribute'unda aranir. Uca konsaydi yetkisiz
// kullanici nedeni belirtilmeyen bir 403 alirdi; servis duzeyinde reddedildiginde
// istemci eksik yetkiyi adiyla gorur (McpToolErrorTranslator). Sema acikca bearer'a
// baglanir: varsayilan cerez semasi token'siz istegi giris sayfasina 302 ile
// yonlendirir, MCP istemcisi 401 bekler.
endpoints.MapMcp("/mcp").RequireAuthorization(new AuthorizeAttribute
endpoints.MapMcp(McpEndpointPath).RequireAuthorization(new AuthorizeAttribute
{
AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme
});

View file

@ -210,6 +210,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
yazma ekranın C/U/D sözleşmesinden geçtiği için kiracıya açıktır; ekran yetkisinden geçmeden
yazabilen araç (procedure, Custom Endpoint) ve geliştirme araçları (`DevelopTools`) `TenantTools`'a
eklenmez. Geliştirme işlemi yine Wizard / Custom Components AppService'inden geçer.
**`/mcp` istek düzeyindeki unit of work'ün dışındadır** (`AbpAspNetCoreUnitOfWorkOptions.IgnoredUrls`):
cevap SSE ile aktığı için istek UoW'si sonuç istemciye gittikten sonra commit edilir ve bağlantı
kapanınca geri alınır — araç başarılı görünür, EF kayıtları kaybolur. Her MCP servis çağrısı
kendi UoW'sini sonuç dönmeden tamamlar; bu istisna kaldırılmaz.
Kapı iki yerdedir: `AddListToolsFilter` listeyi süzer, `McpAppService.EnsureToolAllowedAsync`
çağrıyı reddeder — süzme tek başına yetki sınırı değildir, ikisi birlikte yazılır. `Invoke`
uçta değil her MCP servisinin sınıf düzeyindeki `[Authorize]`'dadır; yeni bir MCP AppService'i de
@ -235,7 +239,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
kalır, hiçbir yere yüklenmez. `start_dev_request` / `get_dev_request` onu metinden ayıklar
(`McpDevRequestImages`) ve MCP resim içeriği (`ImageContentBlock`) olarak döndürür; metinde
`[image-N]` kalır. Açıklama bu yüzden `update_row` ile yeniden yazılmaz, not
`append_dev_request_note` ile eklenir.
`append_dev_request_note` ile eklenir. `SubTasks` de `add_dev_request_subtasks` ile yazılır:
atanan kişi bağlanan kullanıcı, bitiş tarihi bugün — sunucu doldurur, modele bırakılmaz.
- **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn`
sözlüğünden okunur (`IMcpSchemaDictionary`). Sözlüğün alan adları
`dbo.Sas_H_ListFormFieldsDictionaryJson` prosedürünün ürettiği JSON ile birebir aynıdır;

View file

@ -17,6 +17,8 @@ export const WIZARD_PERMISSION = {
DELETE: 'App.Listforms.Wizard.Delete',
EXPORT: 'App.Listforms.Wizard.Export',
IMPORT: 'App.Listforms.Wizard.Import',
/** Wizard silinirken ekranın tablosunu düşürmek SQL Query Manager yetkisi ister. */
DROP_TABLE: 'App.SqlQueryManager',
} as const
/**

View file

@ -158,10 +158,13 @@ export enum WizardRecordTypeEnum {
Module = 8,
DataSource = 9,
LanguageKey = 10,
Table = 11,
CrudEndpoint = 12,
CustomEndpoint = 20,
BackgroundWorker = 21,
IntranetWidget = 22,
Tenant = 23,
CustomComponent = 24,
}
export enum WizardDeleteStatusEnum {
@ -174,6 +177,9 @@ export enum WizardDeleteReasonEnum {
None = 0,
InUse = 1,
Redeploy = 2,
NotSelected = 3,
NotManaged = 4,
Failed = 5,
}
export interface WizardRecordRefDto {
@ -187,6 +193,8 @@ export interface WizardDeleteLogItemDto {
status: WizardDeleteStatusEnum
reason: WizardDeleteReasonEnum
usedBy: WizardRecordRefDto[]
/** `Failed` durumunda veritabanının hata mesajı. */
message?: string
}
/** Silinen, korunan ve bulunamayan her kayıt tek tek listelenir. */
@ -196,6 +204,31 @@ export interface WizardDeleteResultDto {
items: WizardDeleteLogItemDto[]
}
/** Silme onayında sorulan, ekranın tablosu; yalnızca `canDrop` olan seçilebilir. */
export interface WizardDeleteTableDto {
dataSourceCode: string
schemaName: string
tableName: string
exists: boolean
rowCount?: number
canDrop: boolean
reason: WizardDeleteReasonEnum
usedBy: WizardRecordRefDto[]
/** Tabloyla birlikte silinecek seed dosyaları (kapsam klasörüne göre). */
files: string[]
}
export interface WizardDeleteAnalysisDto {
fileName: string
wizardName: string
tables: WizardDeleteTableDto[]
}
export interface WizardDeleteInput {
fileName: string
dropTables: string[]
}
export interface WizardInsertedRecordsDto {
languageKeys: string[]
permissionGroupNames: string[]

View file

@ -1,6 +1,8 @@
import {
ListFormWizardDto,
WizardExportAnalysisDto,
WizardDeleteAnalysisDto,
WizardDeleteInput,
WizardDeleteResultDto,
WizardFileInfoDto,
WizardImportAnalysisDto,
@ -38,13 +40,21 @@ export const getWizardFile = (fileName: string) =>
params: { fileName },
})
export const deleteWizardFile = (fileName: string) =>
apiService.fetchData<WizardDeleteResultDto>({
method: 'DELETE',
url: `/api/app/list-form-wizard/file`,
/** Silme onayında sorulacak tabloları getirir; hiçbir şeye dokunmaz. */
export const getWizardDeleteAnalysis = (fileName: string) =>
apiService.fetchData<WizardDeleteAnalysisDto>({
method: 'GET',
url: `/api/app/list-form-wizard/delete-analysis`,
params: { fileName },
})
export const deleteWizardFile = (input: WizardDeleteInput) =>
apiService.fetchData<WizardDeleteResultDto, WizardDeleteInput>({
method: 'POST',
url: `/api/app/list-form-wizard/delete-file`,
data: input,
})
/**
* Indirmeden once arsivin tam icerigini ve cozulemeyen bagimliliklarini getirir; export ile
* ayni toplamayi kullanir, dolayisiyla onizlemedeki liste indirilen zip ile birebir aynidir.

View file

@ -72,7 +72,7 @@ function JsonRowOpDialogEditForm({
<Dialog.Body>
<p>{translate('::App.Platform.DeleteConfirmation')}</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button type="button" variant="plain" onClick={handleClose}>
{translate('::App.Platform.Cancel')}
</Button>
@ -149,7 +149,7 @@ function JsonRowOpDialogEditForm({
<Field name="colSpan" type="number" component={Input} />
</FormItem>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button type="button" variant="plain" onClick={handleClose}>
{translate('::App.Platform.Cancel')}
</Button>

View file

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import classNames from 'classnames'
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
import { Button, Checkbox, Dialog, Input, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
import Loading from '@/components/shared/Loading'
import PageTitle from '@/components/shared/PageTitle'
@ -32,6 +32,7 @@ import {
deleteWizardFile,
downloadBlob,
exportWizardFile,
getWizardDeleteAnalysis,
getWizardExportAnalysis,
getWizardFiles,
} from '@/services/wizard.service'
@ -45,6 +46,7 @@ import {
WizardDeleteReasonEnum,
WizardDeleteResultDto,
WizardDeleteStatusEnum,
WizardDeleteTableDto,
WizardExportAnalysisDto,
WizardFileInfoDto,
WizardRecordTypeEnum,
@ -61,6 +63,10 @@ const VIEW_STATE_CODE = 'developerkit-wizardmanager'
interface ConfirmState {
fileName: string
wizardName: string
/** Ekranın tablosu; analiz gelene kadar null. */
tables: WizardDeleteTableDto[] | null
/** Onayda seçilen, düşürülecek tablolar. */
dropTables: string[]
}
const DELETE_STATUS_ORDER = [
@ -186,6 +192,7 @@ const WizardFileManager = () => {
const canDelete = checkPermission(WIZARD_PERMISSION.DELETE)
const canExport = checkPermission(WIZARD_PERMISSION.EXPORT)
const canImport = checkPermission(WIZARD_PERMISSION.IMPORT)
const canDropTable = checkPermission(WIZARD_PERMISSION.DROP_TABLE)
const menuPathText = useCallback(
(f: WizardFileInfoDto) => (f.menuPath ?? []).map((key) => translate('::' + key)).join(' › '),
@ -312,12 +319,45 @@ const WizardFileManager = () => {
if (file) setImportFile(file)
}
/**
* Onay açılırken ekranın tablosu sorulur. Tablo wizard'ın ürettiği bir kayıt olmadığı için
* kendiliğinden silinmez; kullanıcı burada seçerse düşürülür. Analiz alınamazsa silme yine
* yapılabilir, yalnızca tablo sorulmaz.
*/
const openDeleteConfirm = async (f: WizardFileInfoDto) => {
const base = { fileName: f.fileName, wizardName: f.wizardName || f.fileName, dropTables: [] }
setConfirm({ ...base, tables: null })
try {
const res = await getWizardDeleteAnalysis(f.fileName)
setConfirm((prev) =>
prev?.fileName === f.fileName ? { ...prev, tables: res.data.tables ?? [] } : prev,
)
} catch {
setConfirm((prev) => (prev?.fileName === f.fileName ? { ...prev, tables: [] } : prev))
}
}
const toggleDropTable = (tableName: string, checked: boolean) =>
setConfirm((prev) =>
prev
? {
...prev,
dropTables: checked
? [...prev.dropTables, tableName]
: prev.dropTables.filter((t) => t !== tableName),
}
: prev,
)
const handleDeleteConfirm = async () => {
if (!confirm) return
setDeletingFile(confirm.fileName)
setConfirm(null)
try {
const res = await deleteWizardFile(confirm.fileName)
const res = await deleteWizardFile({
fileName: confirm.fileName,
dropTables: confirm.dropTables,
})
setDeleteReport(res.data)
toast.push(
<Notification type="success" duration={3000}>
@ -359,6 +399,58 @@ const WizardFileManager = () => {
.filter((x): x is WizardFileInfoDto => !!x)
.map((x) => x.wizardName || x.fileName)
/** Seçilemeyen tablo nedeniyle gösterilir; seçilen tablo için geri alınamaz uyarısı çıkar. */
const renderDropTable = (table: WizardDeleteTableDto, selected: string[]) => {
const checked = selected.includes(table.tableName)
return (
<div
key={`${table.dataSourceCode}:${table.tableName}`}
className="mt-3 border-t border-gray-100 pt-3 first-of-type:border-t-0 dark:border-gray-800"
>
<Checkbox
checked={checked}
disabled={!table.canDrop || !canDropTable}
onChange={(value: boolean) => toggleDropTable(table.tableName, value)}
>
<span className="font-mono text-sm">
{table.schemaName}.{table.tableName}
</span>
<span className="ml-2 text-xs text-gray-500">
{table.dataSourceCode} ·{' '}
{table.exists
? translate('::App.ListForm.WizardDeleteTableRows', { 0: table.rowCount ?? '?' })
: translate('::App.ListForm.WizardDeleteTableMissing')}
</span>
</Checkbox>
{!table.canDrop && (
<div className="ml-6 mt-1 text-xs text-amber-700 dark:text-amber-300">
{translate(`::App.ListForm.WizardDeleteReason.${WizardDeleteReasonEnum[table.reason]}`)}
{table.usedBy.map((ref) => (
<div key={`${ref.recordType}:${ref.name}`} className="break-all text-gray-500">
{recordTypeText(ref.recordType)}: <span className="font-mono">{ref.name}</span>
</div>
))}
</div>
)}
{table.canDrop && !canDropTable && (
<div className="ml-6 mt-1 text-xs text-amber-700 dark:text-amber-300">
{translate('::App.ListForm.WizardDeleteTableNoPermission')}
</div>
)}
{checked && (
<div className="ml-6 mt-1 text-xs text-red-600 dark:text-red-400">
{translate('::App.ListForm.WizardDeleteTableWarning')}
{table.files.map((file) => (
<div key={file} className="break-all font-mono text-gray-500">
{file}
</div>
))}
</div>
)}
</div>
)
}
const renderActions = (f: WizardFileInfoDto, className: string) => (
<div className={className}>
{!f.hasInsertedRecords && (
@ -425,9 +517,7 @@ const WizardFileManager = () => {
loading={deletingFile === f.fileName}
icon={<FaTrash className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
}
onClick={() => openDeleteConfirm(f)}
/>
)}
</div>
@ -835,13 +925,40 @@ const WizardFileManager = () => {
</p>
</div>
</div>
{confirm?.tables === null && (
<p className="mt-4 text-xs text-gray-500">
{translate('::App.ListForm.WizardDeleteTablesLoading')}
</p>
)}
{!!confirm?.tables?.length && (
<div className="mt-4 rounded border border-gray-200 p-3 dark:border-gray-700">
<p className="text-sm font-medium text-gray-800 dark:text-gray-200">
{translate('::App.ListForm.WizardDeleteTables')}
</p>
<p className="mt-0.5 text-xs text-gray-500">
{translate('::App.ListForm.WizardDeleteTablesHint')}
</p>
{confirm.tables.map((table) => renderDropTable(table, confirm.dropTables))}
</div>
)}
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button size="sm" variant="plain" type="button" onClick={() => setConfirm(null)}>
{translate('::App.Platform.Cancel')}
</Button>
<Button size="sm" variant="solid" type="button" onClick={handleDeleteConfirm}>
{translate('::App.Platform.Delete')}
<Button
size="sm"
variant="solid"
type="button"
color={confirm?.dropTables.length ? 'red-600' : undefined}
disabled={confirm?.tables === null}
onClick={handleDeleteConfirm}
>
{translate(
confirm?.dropTables.length
? '::App.ListForm.WizardDeleteWithTables'
: '::App.Platform.Delete',
)}
</Button>
</Dialog.Footer>
</Dialog>
@ -916,6 +1033,11 @@ const WizardFileManager = () => {
)}
</div>
)}
{item.message && (
<div className="break-all text-red-600 dark:text-red-400">
{item.message}
</div>
)}
{item.usedBy.map((ref) => (
<div key={`${ref.recordType}:${ref.name}`} className="break-all">
{recordTypeText(ref.recordType)}:{' '}
@ -929,7 +1051,7 @@ const WizardFileManager = () => {
</table>
</div>
</Dialog.Body>
<Dialog.Footer className="flex justify-end">
<Dialog.Footer className="flex justify-end mt-2">
<Button size="sm" variant="solid" type="button" onClick={() => setDeleteReport(null)}>
{translate('::App.Platform.Close')}
</Button>
@ -977,7 +1099,7 @@ const WizardFileManager = () => {
</div>
</div>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button size="sm" variant="plain" type="button" onClick={() => setExportPreview(null)}>
{translate('::App.Platform.Cancel')}
</Button>

View file

@ -445,7 +445,7 @@ function WizardStepSubForms({
/>
</FormItem>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button
size="sm"
type="button"
@ -473,7 +473,7 @@ function WizardStepSubForms({
<h5>{translate('::App.Platform.Delete')}</h5>
<p>{translate('::App.Platform.DeleteConfirmation')}</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button type="button" variant="plain" onClick={() => setDeleteIndex(null)}>
{translate('::App.Platform.Cancel')}
</Button>

View file

@ -320,7 +320,7 @@ function WizardStepWidgets({ widgets, translate, onChange, onBack, onNext }: Pro
<Field name="onClick" component={Input} />
</FormItem>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button type="button" variant="plain" onClick={() => setDialogIndex(null)}>
{translate('::App.Platform.Cancel')}
</Button>
@ -343,7 +343,7 @@ function WizardStepWidgets({ widgets, translate, onChange, onBack, onNext }: Pro
<h5>{translate('::App.Platform.Delete')}</h5>
<p>{translate('::App.Platform.DeleteConfirmation')}</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Dialog.Footer className="flex justify-end gap-2 mt-2">
<Button type="button" variant="plain" onClick={() => setDeleteIndex(null)}>
{translate('::App.Platform.Cancel')}
</Button>