diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 9f38d394..86ba3299 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -68,7 +68,7 @@ kurulur; host veritabanını paylaşan tenant'ta dosyalar yeniden uygulanmaz (`S | # | Adım | Dosya | Neden bu sırada | | --- | --- | --- | --- | -| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablonun scripti `create_table` ile yazılır (biçim tasarımcıyla aynı; tablo DB Migrate'te kurulur); view MCP'den kurulamaz | +| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablonun scripti `create_table` ile yazılır (biçim tasarımcıyla aynı; tablo DB Migrate'te kurulur); view/procedure/function scripti `save_sql_object` ile iki sağlayıcı için yazılır | | 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar | | 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar | | 4 | Wizard | `wizard/{Ad}.json` + menü/yetki/dil satırları (`data/App.Menus.*`, `data/Abp.Identity.Permission*`, `data/App.Languages.LanguageText.json`) | Deploy (MCP `deploy_wizard` ya da wizard ekranı, §0.10) **yalnızca bu dosyaları yazar**; ekran, menü ve yetkiler DB Migrate ile kurulur (§4.0). Dosyaları sunucu yazar. Elle yazılan List dosyası `ListForm` + `Fields` taşımak zorundadır, yoksa seeder atlar (§4.1) | @@ -506,12 +506,22 @@ araçlarla üretir; sonuç ve seed dosyaları wizard ekranıyla birebir aynıdı `McpToolNames`'tedir; okuyanlar `App.DeveloperKit.McpServers.Invoke`, değiştirenler `.Write` ister ve kiracı kullanıcısına hiçbiri açılmaz. +**MCP geliştirme araçları yalnızca seed dosyası yazar** (wizard / data / crud JSON'u, `sql`+`postgres` +object scripti) — Wizard ekranıyla aynı kural. Veritabanına hiçbiri yazmaz; canlıya yansıma her zaman +kullanıcının çalıştırdığı DB Migrate iledir, model migrate'i beklemez. Kolon adı tahmin edilmez: +`list_lists` / `describe_list` / `search_schema` geliştiriciye (host + `App.Listforms.Wizard`) veri +sözlüğünde henüz olmayan kaynakları da döner — migrate bekleyen wizard ekranları (`sourceKind: pending`, +alanlar wizard dosyasının `Fields` bölümünden) ve yalnızca `create_table` scripti olan tablolar +(`sourceKind: pendingTable`, kolonlar scriptten). Lookup, widget, view ve `Query` SQL'i bu adlarla yazılır. + | Araç | Ne yapar | Not | | --- | --- | --- | | `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla (`4.3`) ya da başlık metniyle istenir; aynı numarayı iki başlık taşımamalıdır. Bölüm önce istenen belgede, sonra diğerinde aranır; başlık olmayan bir terim (`dxImageUpload`) terimin geçtiği bölümlerin listesini döndürür | -| `create_table` | Yeni tablonun `sql/object/{Tablo}.sql` ve PG eşi `postgres/object/{Tablo}.sql` dosyalarını yazar; veritabanında çalıştırmaz, tabloyu DB Migrate kurar (deploy kolonları o zamana kadar scriptten okur) | 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 | +| `create_table` | Yeni tablonun `sql/object/{Tablo}.sql` ve PG eşi `postgres/object/{Tablo}.sql` dosyalarını yazar; veritabanında çalıştırmaz, tabloyu DB Migrate kurar (deploy kolonları o zamana kadar scriptten okur) | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. `indexes` (`{columns, unique}` → `IX_`/`UQ_{Tablo}_{Kolonlar}`) ve `foreignKeys` (`{column, referencedTable, referencedColumn = Id, onDelete = NoAction|Cascade|SetNull}` → `FK_{Tablo}_{Kolon}`) aynı scripte varlık kontrollü yazılır. Migrate scriptleri dosya adı sırasıyla çalıştırdığı için FK'nin hedefi ya adı bu tablodan önce sıralanan bir seed tablosu ya da veritabanında kurulu bir platform tablosu olmalıdır (`ForeignKeyTableNotInstalledFirst`) | +| `save_sql_object` | View, procedure ya da function scriptini `sql/object/{Ad}.sql` ve `postgres/object/{Ad}.sql` dosyalarına yazar (`McpSqlObjectWriter`); veritabanında çalıştırmaz | `App.SqlQueryManager` ister. İki script de zorunludur ve aynı nesneyi tanımlar (ad scriptten okunur); başlık `CREATE OR ALTER` / `CREATE OR REPLACE`'e çevrilir, aynı adla tekrar yazmak günceller. Tablo scriptinin adını alamaz. Okuduğu tabloların adı view'in adından önce sıralanmalıdır (`Inv_T_…` → `Inv_V_…`). Birden çok tablodan beslenen ekranda `Query` yerine view (§8.2) | +| `generate_crud_endpoints` | Tablo/view için SQL Query Manager CRUD diyaloğuyla aynı endpoint kümesini yalnızca `crud/{Entity}.json` dosyasına yazar (`CrudEndpointSeedFileDto.Generate`); kayıtları DB Migrate ekler (`CrudEndpointSeedApplier`) | `App.SqlQueryManager.CrudEndpoints` ister. Custom component'in okuyup yazacağı tablo içindir; ListForm ekranı gerekmez. View/procedure/function'da yalnızca `GetList`/`GetById` | | `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ın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | 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). List ekranında `Widgets` dolu gelir — alt form olarak üretilen ekran hariç (`Widgets: []`) — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Satır/toolbar butonları `CommandColumns` (§4.9). Bağımlı lookup öğedeki `Cascade*` alanları (§4.3.4). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | +| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | 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). List ekranında `Widgets` dolu gelir — alt form olarak üretilen ekran hariç (`Widgets: []`) — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Satır/toolbar butonları `CommandColumns` (§4.9). Bağımlı lookup öğedeki `Cascade*` alanları (§4.3.4). Wizard cevabının üretmediği ekran ayarları (`FormFieldsDefaultValueDto` gibi varsayılanlar §4.3.5, `ValidationRuleDto`, `Format`, `Width`, `BandName`, özet/gruplama, `ExtraFilter`, komutlar, `PageSize` …) aynı JSON'un `ListForm` ve `Fields` bölümlerine `get_wizard` çıktısındaki adlarla yazılır: bölümler kısmi olabilir, `Fields` öğeleri `FieldName` ile eşleşir, `*Json` adları `*Dto` karşılığına çevrilir (`WizardScreenOverlay`). Güncellemede `get_wizard` çıktısı bütünüyle düzenlenip gönderilirse editör ayarları korunur; aynı ayarı Wizard bölümündeki cevap da söylüyorsa cevap kazanır. Kimlik alanları (kod, ad, başlık/açıklama anahtarı, yetki) wizard'dan gelir; tanınmayan ya da hesaplanan anahtar reddedilir. Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | | `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 + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister | @@ -1212,7 +1222,7 @@ saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldur | `MenuCode` | Menü kaydının kodu; yetki adlarının kökü de budur. | | `MenuUrl` | List'te `/admin/list/{MenuCode}` hesaplanır; Custom'da bileşenin `RoutePath`'i yazılır. | | `CustomComponentName` | Yalnızca Custom yolunda; bağlanacak bileşenin adı. | -| `EditFileName` | Yalnızca **istek girdisi** (MCP'de `replaceFileName`): doluysa güncellemedir — `.Update` yetkisi ister, `SeededAt` korunur, dosyalar cevaptan yeniden yazılır ve ekran migrate'te **cevaptan yeniden kurulur** (§4.0); ListForm editöründe yapılmış, wizard'da karşılığı olmayan ayarlar kaybolur. Seed dosyasına yazılmaz. | +| `EditFileName` | Yalnızca **istek girdisi** (MCP'de `replaceFileName`): doluysa güncellemedir — `.Update` yetkisi ister, `SeededAt` korunur, dosyalar cevaptan yeniden yazılır ve ekran migrate'te **cevaptan yeniden kurulur** (§4.0). Wizard ekranından yapılan güncellemede ListForm editöründe yapılmış, wizard'da karşılığı olmayan ayarlar kaybolur; MCP'de aynı çağrıya `ListForm`/`Fields` bölümleri (`get_wizard` çıktısı) verilirse korunur (§0.10). Seed dosyasına yazılmaz. | **Menü** @@ -1906,7 +1916,8 @@ deploy'dur). Değiştirmenin yolları: 2. **`EditFileName` ile yeniden deploy** (wizard ekranı ya da MCP `replaceFileName`) — menü, yetki grubu, veri kaynağı gibi wizard cevabına ait değişiklikler için. Dosyalar yeniden yazılır, ekran DB Migrate'te cevaptan yeniden kurulur (§4.0); editörde yapılmış, wizard'da karşılığı - olmayan ayarlar kaybolur. + olmayan ayarlar kaybolur — MCP'de `get_wizard` çıktısının `ListForm`/`Fields` bölümleri de + gönderilirse korunur (§0.10). 3. İlgili kayıtları elle sil, sonra seed'i yeniden çalıştır. > `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow` diff --git a/README.md b/README.md index 1116c845..24181c52 100644 --- a/README.md +++ b/README.md @@ -1550,7 +1550,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır. | Araç | İş | | --- | --- | | `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. | +| `list_lists` | Listeler: kod, başlık, açıklama, arkasındaki tablo. Host'ta `App.Listforms.Wizard` yetkisi olan geliştiriciye veri sözlüğünde henüz olmayan kaynaklar da döner: migrate bekleyen wizard ekranları (`sourceKind: pending`, alanlar wizard dosyasının `Fields` bölümünden) ve yalnızca seed scripti olan tablolar (`pendingTable`, kolonlar scriptten). `describe_list` ve `search_schema` aynı kümeyi kullanır; model lookup/widget/view SQL'ini bu adlarla yazar. | | `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. | | `query_list` | Listenin verisini ekranın kendi sorgusuyla (`GetSelectAsync`) okur: filtre, sıralama, gruplama, özet (DevExtreme biçimi). Sıralama/gruplama/özet JSON'u, arayüzün `Utils.ts`'te yaptığı gibi select ucunun metin biçimine (`Alan desc|Alan2 asc`) `McpLoadOptions` ile çevrilir; select ucu JSON'u tanımaz. Normal kullanıcının veri okuma yolu. | @@ -1559,9 +1559,11 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır. | `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. | | `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. | | `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`, ya da başlık metniyle). Bölüm önce istenen, sonra diğer belgede aranır; başlıkla eşleşmeyen terimde (örn. `dxImageUpload`) terimin geçtiği bölümler (en çok 15, `DevGuideSearchMatches`), hiç geçmiyorsa başlık listesi döner — hata atılmaz. Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. | -| `create_table` | Yeni tablonun scriptini SQL Query Manager tablo tasarımcısıyla aynı biçimde üretir (`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 `sql/object/{Tablo}.sql` dosyasına, PostgreSQL eşi (`PostgreSqlTableScript`) `postgres/object/{Tablo}.sql` dosyasına yazılır. Veritabanında çalıştırılmaz: tabloyu kullanıcının çalıştırdığı DB Migrate kurar; o zamana kadar `deploy_wizard` kolonları scriptten okur (`PostgreSqlTableScript.ReadSeedColumns`). Yalnızca oluşturur — tablo ya da scripti varsa reddeder. `.Write` + `App.SqlQueryManager` ister. | +| `create_table` | Yeni tablonun scriptini SQL Query Manager tablo tasarımcısıyla aynı biçimde üretir (`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. `indexes` ve `foreignKeys` ile index/unique/FK aynı scripte varlık kontrollü (tekrar çalışmaya dayanıklı) yazılır; FK'nin hedefi migrate'te önce kurulmalıdır — adı bu tablodan önce sıralanan bir seed tablosu ya da veritabanında kurulu bir platform tablosu (`ForeignKeyTableNotInstalledFirst`). Script `sql/object/{Tablo}.sql` dosyasına, PostgreSQL eşi (`PostgreSqlTableScript`) `postgres/object/{Tablo}.sql` dosyasına yazılır. Veritabanında çalıştırılmaz: tabloyu kullanıcının çalıştırdığı DB Migrate kurar; o zamana kadar `deploy_wizard` kolonları scriptten okur (`PostgreSqlTableScript.ReadSeedColumns`). Yalnızca oluşturur — tablo ya da scripti varsa reddeder. `.Write` + `App.SqlQueryManager` ister. | +| `save_sql_object` | View/procedure/function scriptini `sql/object/{Ad}.sql` ve `postgres/object/{Ad}.sql` dosyalarına yazar (`McpSqlObjectWriter`); veritabanında çalıştırılmaz, nesneyi DB Migrate kurar. Gövde diyalektler arasında güvenle çevrilemediği için iki script de istemciden gelir ve aynı nesneyi tanımlamalıdır (`SqlObjectInvalid`); ad scriptten okunur, başlık `CREATE OR ALTER` / `CREATE OR REPLACE`'e çevrilir. Tablo scriptinin üstüne yazmaz. `.Write` + `App.SqlQueryManager` ister. | +| `generate_crud_endpoints` | SQL Query Manager CRUD diyaloğuyla aynı endpoint kümesini (`CrudEndpointSeedFileDto.Generate` — diyaloğun servisi de bunu kullanır) yalnızca `crud/{Entity}.json` dosyasına yazar; kayıtları DB Migrate ekler (`CrudEndpointSeedApplier`). `.Write` + `App.SqlQueryManager.CrudEndpoints` 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. Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İ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 — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | +| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …, "ListForm": …, "Fields": […]}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromDeployJson`'dan geçer: Wizard bölümünde yazılan cevap korunur, yazılmayan ekran alanı `ListForm`/`Fields`'tan tamamlanır, iki bölüm de ekrana katman olarak uygulanır (`WizardScreenOverlay`, üç yönlü: bölümdeki değer wizard'ın kendi ürettiğiyle aynıysa cevap kazanır, farklıysa editör ayarıdır ve korunur; kimlik alanları wizard'dan gelir, tanınmayan/hesaplanan anahtar reddedilir). Böylece MCP, ListForm editörünün yapabildiği her ayarı wizard dosyasına yazar ve `replaceFileName` ile güncelleme editör ayarlarını silmez. Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İ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 — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | | `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). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alı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. Sonuç istemcinin 1 MB sınırına sığdırılır: büyük görseller küçültülüp JPEG'e çevrilir (`IMcpImageShrinker`), sığmayan gönderilmez ve metinde bildirilir. `.Write` + `App.DevRequests.Update` ister. | @@ -1575,7 +1577,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır. **Timesheets.** Talebin `Timesheets` listesini sunucu doldurur (`IMcpDevRequestTimesheetRecorder`): `get_dev_request` / `start_dev_request` talebi kullanıcının etkin talebi yapar (dağıtık önbellek, -`McpDevRequestActivityCacheItem`, 12 saat); sonraki her MCP yazma işlemi (`create_table`, +`McpDevRequestActivityCacheItem`, 12 saat); sonraki her MCP yazma işlemi (`create_table`, `save_sql_object`, `generate_crud_endpoints`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `save_report_category`, `save_report_template`, `delete_report_template`, `save_module_doc`, satır araçları, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks`) o talebe bir satır @@ -1634,7 +1636,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`, `get_dev_request`, `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`, `save_report_category`, `save_report_template`, `delete_report_template`, `save_module_doc`, `start_dev_request`, `save_dev_request_analysis`, `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 | +| `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`, `save_sql_object`, `generate_crud_endpoints`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `save_report_category`, `save_report_template`, `delete_report_template`, `save_module_doc`, `start_dev_request`, `save_dev_request_analysis`, `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ı) @@ -1650,7 +1652,8 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay | `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`; `dropTables` için ayrıca `App.SqlQueryManager` | -| `create_table` | — | `.Write` | `App.SqlQueryManager` | +| `create_table`, `save_sql_object` | — | `.Write` | `App.SqlQueryManager` | +| `generate_crud_endpoints` | — | `.Write` | `App.SqlQueryManager.CrudEndpoints` | | `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` | | `save_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Create` (yeni) / `.Update` | | `delete_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Delete` | diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs index 790c6136..35c78785 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs @@ -71,6 +71,32 @@ public class McpCreateTableRequestDto public bool FullAudited { get; set; } = true; public bool MultiTenant { get; set; } = true; + + public List Indexes { get; set; } = []; + + public List ForeignKeys { get; set; } = []; +} + +/// Index ya da unique kisit; ad sunucuda IX_/UQ_{Tablo}_{Kolonlar} olarak uretilir. +public class McpTableIndexDto +{ + public List Columns { get; set; } = []; + + public bool Unique { get; set; } +} + +/// FK; ad FK_{Tablo}_{Kolon}. +public class McpTableForeignKeyDto +{ + public string Column { get; set; } = string.Empty; + + public string ReferencedTable { get; set; } = string.Empty; + + /// Bos ise Id. + public string? ReferencedColumn { get; set; } + + /// NoAction (varsayilan), Cascade ya da SetNull. + public string? OnDelete { get; set; } } public class McpTableColumnDto @@ -99,6 +125,30 @@ public class McpCreateTableResultDto public string SeedFilePath { get; set; } = string.Empty; } +/// View, procedure ya da function scriptinin iki saglayici icin yazildigi seed dosyalari. +public class McpSqlObjectResultDto +{ + public string Name { get; set; } = string.Empty; + + /// VIEW, PROCEDURE ya da FUNCTION. + public string Kind { get; set; } = string.Empty; + + /// Seed kokune gore goreli yollar: sql/object/{Ad}.sql, postgres/object/{Ad}.sql. + public List SeedFilePaths { get; set; } = []; +} + +/// Yazilan crud/{Entity}.json dosyasi ve icindeki endpoint'ler. +public class McpCrudEndpointsResultDto +{ + public string EntityName { get; set; } = string.Empty; + + /// Seed kokune gore goreli yol. + public string SeedFilePath { get; set; } = string.Empty; + + /// {Method} {Path}; pasif baslayanlar isaretlidir. + public List Endpoints { get; set; } = []; +} + /// /// Deploy edilmis bir wizard'in salt-okur dogrulamasi. yalnizca /// bir kontrol kaldiginda false olur; engellemeyen diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs index 1f026c15..871c9165 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs @@ -63,7 +63,11 @@ public class McpListSummaryDto /// Tablo adi ya da SELECT metni — listenin gercek SQL kaynagi. public string? SqlSourceName { get; set; } - /// table, selectSql ya da unknown. + /// + /// table, selectSql ya da unknown; gelistiricide ayrica pending (wizard dosyasi + /// var, DB Migrate bekliyor) ve pendingTable (yalnizca seed scripti olan tablo). Kolon adlari + /// dogrudur; query_list migrate'ten once calismaz. + /// public string? SourceKind { get; set; } public string? KeyFieldName { get; set; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs index 40b379d8..4ec03c0d 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs @@ -18,6 +18,12 @@ public interface IMcpDevelopAppService : IApplicationService Task CreateTableAsync(McpCreateTableRequestDto input); + /// View/procedure/function scriptini iki saglayicinin seed klasorune yazar; veritabaninda calistirmaz. + Task SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript); + + /// Tablo ya da view icin CRUD endpoint'lerini crud/{Entity}.json seed dosyasina yazar; kaydi DB Migrate ekler. + Task GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType); + Task> GetWizardsAsync(string? search); Task GetWizardAsync(string fileName); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs index 6f5033eb..00e75bce 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs @@ -35,6 +35,7 @@ public class McpDevelopAppService( IMcpServerConfigProvider serverConfigProvider, IMcpDevGuide devGuide, IMcpTableCreator tableCreator, + IMcpSqlObjectWriter sqlObjectWriter, IListFormWizardAppService wizardAppService, ICustomComponentAppService componentAppService, IListFormSelectAppService listFormSelectAppService, @@ -164,7 +165,9 @@ public class McpDevelopAppService( [.. (input.Columns ?? []).Select(column => new McpTableColumn( column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))], input.FullAudited, - input.MultiTenant)); + input.MultiTenant, + [.. (input.Indexes ?? []).Select(index => new McpTableIndex(index.Columns ?? [], index.Unique))], + [.. (input.ForeignKeys ?? []).Select(fk => new McpTableForeignKey(fk.Column, fk.ReferencedTable, fk.ReferencedColumn, fk.OnDelete))])); await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName); @@ -176,6 +179,55 @@ public class McpDevelopAppService( }; } + /// + /// Tablo tasarimcisiyla ayni yetkiyi ister: SQL Query Manager'daki view/procedure/function + /// kaydinin karsiligidir. Nesneyi DB Migrate kurar. + /// + public async Task SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript) + { + await EnsureToolAllowedAsync(McpToolNames.SaveSqlObject); + await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.Default); + + var saved = sqlObjectWriter.Save(sqlServerScript, postgreSqlScript); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveSqlObject, saved.Name); + + return new McpSqlObjectResultDto + { + Name = saved.Name, + Kind = saved.Kind, + SeedFilePaths = [saved.SqlServerPath, saved.PostgreSqlPath] + }; + } + + /// + /// SQL Query Manager'daki CRUD endpoint diyaloguyla ayni endpoint kumesini + /// () yalnizca crud/{Entity}.json seed dosyasina + /// yazar; kayitlari DB Migrate ekler (CrudEndpointSeedApplier). Yetki diyalogunkiyle aynidir. + /// + public async Task GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType) + { + await EnsureToolAllowedAsync(McpToolNames.GenerateCrudEndpoints); + await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints); + + // Tasarimcidaki gibi sema oneki atilir: endpoint yolu yalnizca nesne adini tasir. + var entity = entityName?.Split('.')[^1].Trim('[', ']', '"', ' ') ?? string.Empty; + if (CrudEndpointSeedFileDto.SafeFileName(entity) is null) + { + throw new UserFriendlyException(L[McpErrorCodes.InvalidIdentifier, entityName ?? string.Empty]); + } + + var seedFile = CrudEndpointSeedFileDto.Generate(entity, operationTypes, objectType, Clock.Now.ToUniversalTime()); + seedFile.Write(configuration); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.GenerateCrudEndpoints, entity); + + return new McpCrudEndpointsResultDto + { + EntityName = entity, + SeedFilePath = $"{SeedPathResolver.CrudFolder}/{CrudEndpointSeedFileDto.SafeFileName(entity)}.json", + Endpoints = [.. seedFile.Endpoints.Select(e => $"{e.Method} {e.Path}{(e.IsActive ? string.Empty : " [inactive]")}")] + }; + } + public async Task> GetWizardsAsync(string? search) { await EnsureToolAllowedAsync(McpToolNames.ListWizards); @@ -542,7 +594,6 @@ public class McpDevelopAppService( throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]); } - var devRequest = await FindDevRequestAsync(request); if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis)) @@ -887,39 +938,21 @@ public class McpDevelopAppService( } /// - /// Wizard'i seed dosyasi biciminden ({"Wizard": {...}}) ya da duz cevaplardan okur. - /// Iki bicim de dosyanin okuma kuralindan gecer; boylece Menu/Language bloklari ve - /// WizardCode/ModuleId adlari dosyadakiyle ayni anlama gelir. + /// Wizard'i seed dosyasi biciminden ({"Wizard": {...}, "ListForm": {...}, "Fields": [...]}) + /// ya da duz cevaplardan okur; Menu/Language bloklari ve WizardCode/ModuleId adlari dosyadakiyle + /// ayni anlama gelir. ListForm/Fields bolumleri ekrana katman olarak uygulanir + /// (). /// private ListFormWizardDto ParseWizard(string? json) { - JsonObject? root; try { - root = string.IsNullOrWhiteSpace(json) ? null : JsonNode.Parse(json) as JsonObject; - } - catch (JsonException) - { - root = null; - } - - if (root is null) - { - throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); - } - - var seedJson = root.ContainsKey(nameof(WizardSeedFileDto.Wizard)) - ? root.ToJsonString() - : new JsonObject { [nameof(WizardSeedFileDto.Wizard)] = root }.ToJsonString(); - - try - { - return WizardSeedFileDto.FromSeedJson(seedJson)?.Wizard + return (string.IsNullOrWhiteSpace(json) ? null : WizardSeedFileDto.FromDeployJson(json)) ?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); } - catch (JsonException) + catch (JsonException ex) { - throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); + throw new UserFriendlyException(L[McpErrorCodes.InvalidWizardJson, ex.Message]); } } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs index bee9db5f..ec43e793 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs @@ -1,11 +1,17 @@ using System; using System.Collections.Generic; +using System.Data; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; +using Sozsoft.Platform.Data.Seeds; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.ListForms; using Volo.Abp.Authorization.Permissions; namespace Sozsoft.Mcp; @@ -20,9 +26,17 @@ public class McpSchemaAppService( IMcpServerConfigProvider serverConfigProvider, IMcpSchemaDictionary dictionary, IMcpListFormAccessFilter accessFilter, - IPermissionChecker permissionChecker) + IPermissionChecker permissionChecker, + IListFormWizardAppService wizardAppService, + IConfiguration configuration) : McpAppService(serverConfigProvider), IMcpSchemaAppService { + /// Wizard dosyasi olan ama sozlukte olmayan ekran (DB Migrate bekliyor). + private const string PendingSourceKind = "pending"; + + /// Yalnizca seed scripti olan, ekrani olmayan tablo. + private const string PendingTableSourceKind = "pendingTable"; + public async Task GetServerConfigAsync() => await ResolveServerConfigAsync(); @@ -168,7 +182,90 @@ public class McpSchemaAppService( var forms = await dictionary.GetListFormsAsync(); var readable = await accessFilter.GetReadableListFormCodesAsync(); - return [.. forms.Where(form => form.ListFormCode is not null && readable.Contains(form.ListFormCode))]; + List result = [.. forms.Where(form => form.ListFormCode is not null && readable.Contains(form.ListFormCode))]; + + // Gelistirici, sozlukte henuz olmayan (DB Migrate bekleyen ya da sozluk tazelenmemis) ekran ve + // tablolari da gorur; lookup, widget ve view SQL'i bunlarin gercek kolon adlariyla yazilir. + if (!CurrentTenant.IsAvailable && await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizard)) + result.AddRange(await GetSeedOnlySourcesAsync(forms)); + + return result; + } + + /// + /// Seed klasorunde olup sozlukte olmayan kaynaklar: wizard dosyalarinin ekranlari (alanlari + /// Fields bolumunden) ve yalnizca sql/object scripti olan tablolar (kolonlari scriptten). + /// pending / pendingTable olarak isaretlenir: + /// kolon adlari dogrudur ama query_list migrate'ten once calismaz. + /// + private async Task> GetSeedOnlySourcesAsync(IReadOnlyList known) + { + var codes = known.Select(form => form.ListFormCode).OfType().ToHashSet(StringComparer.OrdinalIgnoreCase); + var tables = known.Select(form => form.TableName).OfType().ToHashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + + foreach (var file in await wizardAppService.GetFiles()) + { + if (file.ComponentKind == WizardComponentKindEnum.Custom || codes.Contains(file.ListFormCode ?? string.Empty)) + continue; + + var seed = await wizardAppService.GetFile(file.FileName); + if (seed.ListForm is not { } listForm) + continue; + + var table = listForm.SelectCommandType == SelectCommandTypeEnum.Query ? null : listForm.SelectCommand?.Split('.')[^1].Trim('[', ']', '"'); + if (table is not null) + tables.Add(table); + + result.Add(new McpDictionaryListForm + { + ListFormCode = listForm.ListFormCode, + DisplayName = seed.Wizard?.TitleTr ?? seed.Wizard?.TitleEn, + Title = seed.Wizard?.TitleEn, + Description = seed.Wizard?.DescTr, + TableName = table, + SqlSourceName = listForm.SelectCommand, + SourceKind = PendingSourceKind, + KeyFieldName = listForm.KeyFieldName, + SearchText = string.Join(' ', listForm.ListFormCode, seed.Wizard?.TitleTr, seed.Wizard?.TitleEn, table), + Fields = [.. seed.Fields.Select(field => new McpDictionaryField + { + Field = field.FieldName, + DisplayName = field.CaptionName, + SqlDataType = field.SourceDbType.ToString(), + IsVisible = field.Visible, + IsBooleanColumn = field.SourceDbType == DbType.Boolean, + LookupValueColumn = string.IsNullOrWhiteSpace(field.LookupJson) ? null : field.LookupDto.ValueExpr, + LookupDisplayColumn = string.IsNullOrWhiteSpace(field.LookupJson) ? null : field.LookupDto.DisplayExpr, + IsDefaultSelectColumn = field.Visible + })] + }); + } + + var scriptFolder = SeedPathResolver.GetPath(configuration, SeedPathResolver.SqlFolder, SeedPathResolver.ObjectFolder); + foreach (var path in Directory.Exists(scriptFolder) ? Directory.GetFiles(scriptFolder, "*.sql") : []) + { + var table = Path.GetFileNameWithoutExtension(path); + if (tables.Contains(table)) + continue; + + var columns = PostgreSqlTableScript.ReadSeedColumns(configuration, table); + if (columns.Count == 0) + continue; + + result.Add(new McpDictionaryListForm + { + ListFormCode = table, + TableName = table, + SqlSourceName = table, + SourceKind = PendingTableSourceKind, + KeyFieldName = columns.ContainsKey("Id") ? "Id" : null, + SearchText = table, + Fields = [.. columns.Keys.Select(column => new McpDictionaryField { Field = column, IsVisible = true, IsDefaultSelectColumn = true })] + }); + } + + return result; } private static McpListSummaryDto ToSummary(McpDictionaryListForm form) diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs index 7566364e..f41cc062 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs @@ -78,5 +78,8 @@ public static class McpErrorCodes public const string InvalidColumnLength = Prefix + "InvalidColumnLength"; public const string InvalidDefaultValue = Prefix + "InvalidDefaultValue"; public const string DuplicateColumn = Prefix + "DuplicateColumn"; + public const string TableColumnNotFound = Prefix + "TableColumnNotFound"; + public const string ForeignKeyTableNotInstalledFirst = Prefix + "ForeignKeyTableNotInstalledFirst"; + public const string SqlObjectInvalid = Prefix + "SqlObjectInvalid"; public const string TableAlreadyExists = Prefix + "TableAlreadyExists"; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs index d7fa4bd7..383440d8 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs @@ -12,6 +12,8 @@ public static class McpTimesheetTexts public const string AppendNote = Prefix + "AppendNote"; public const string AddSubTasks = Prefix + "AddSubTasks"; public const string CreateTable = Prefix + "CreateTable"; + public const string SaveSqlObject = Prefix + "SaveSqlObject"; + public const string GenerateCrudEndpoints = Prefix + "GenerateCrudEndpoints"; public const string DeployWizard = Prefix + "DeployWizard"; public const string DeleteWizard = Prefix + "DeleteWizard"; public const string SaveComponent = Prefix + "SaveComponent"; diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs index 287ba648..7882d783 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs @@ -24,6 +24,8 @@ public static class McpToolNames public const string DeleteRow = "delete_row"; public const string GetDevGuide = "get_dev_guide"; public const string CreateTable = "create_table"; + public const string SaveSqlObject = "save_sql_object"; + public const string GenerateCrudEndpoints = "generate_crud_endpoints"; public const string ListWizards = "list_wizards"; public const string GetWizard = "get_wizard"; public const string DeployWizard = "deploy_wizard"; @@ -55,6 +57,8 @@ public static class McpToolNames [ GetDevGuide, CreateTable, + SaveSqlObject, + GenerateCrudEndpoints, ListWizards, GetWizard, DeployWizard, @@ -119,6 +123,8 @@ public static class McpToolNames [UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [CreateTable] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [SaveSqlObject] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [GenerateCrudEndpoints] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [DeployWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSqlObjectWriter.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSqlObjectWriter.cs new file mode 100644 index 00000000..9fdc097f --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSqlObjectWriter.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Localization; +using Sozsoft.Platform.Data.Seeds; +using Sozsoft.Platform.Localization; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace Sozsoft.Mcp.Services; + +/// +/// Ekranin veri kaynagi olan view, procedure ve fonksiyon scriptlerini SQL Query Manager'in yazdigi +/// yere (sql/object/{Ad}.sql) ve PostgreSQL esine (postgres/object/{Ad}.sql) yazar. +/// Govde diyalektler arasinda guvenle cevrilemedigi icin () iki +/// script de istemciden gelir; biri olmadan yazilmaz. +/// +/// Veritabaninda calistirilmaz: nesneyi, tablo gibi DB Migrate kurar (SqlDataSeeder). Nesnenin +/// adi SQL Server scriptinin basligindan okunur; basliklar tekrar calismaya dayanikli bicime cevrilir +/// (CREATE OR ALTER / CREATE OR REPLACE), ayni adla tekrar yazmak scripti gunceller. +/// +/// +public interface IMcpSqlObjectWriter +{ + McpSavedSqlObject Save(string sqlServerScript, string postgreSqlScript); +} + +public partial class McpSqlObjectWriter( + IConfiguration configuration, + IStringLocalizer localizer) : DomainService, IMcpSqlObjectWriter +{ + private const string SqlServerProvider = "SQL Server"; + private const string PostgreSqlProvider = "PostgreSQL"; + + public McpSavedSqlObject Save(string sqlServerScript, string postgreSqlScript) + { + var sqlServer = Canonicalize(sqlServerScript, SqlServerHeaderRegex(), "CREATE OR ALTER", SqlServerProvider); + var postgreSql = Canonicalize(postgreSqlScript, PostgreSqlHeaderRegex(), "CREATE OR REPLACE", PostgreSqlProvider); + + if (!string.Equals(sqlServer.Name, postgreSql.Name, StringComparison.OrdinalIgnoreCase)) + throw new UserFriendlyException(localizer[McpErrorCodes.SqlObjectInvalid, PostgreSqlProvider]); + + // Tablo scriptinin yerine view yazilirsa migrate tabloyu bir daha kuramazdi. + if (PostgreSqlTableScript.ReadSeedColumns(configuration, sqlServer.Name).Count > 0) + throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, sqlServer.Name]); + + var fileName = $"{sqlServer.Name}.sql"; + var sqlServerPath = Write(SeedPathResolver.SqlFolder, fileName, sqlServer.Script); + var postgreSqlPath = Write(SeedPathResolver.PostgresFolder, fileName, postgreSql.Script); + + return new McpSavedSqlObject(sqlServer.Name, sqlServer.Kind, sqlServerPath, postgreSqlPath); + } + + /// Scripti saglayicinin object klasorune yazar; seed kokune goreli yolu dondurur. + private string Write(string providerFolder, string fileName, string script) + { + var folder = SeedPathResolver.GetPath(configuration, providerFolder, SeedPathResolver.ObjectFolder); + Directory.CreateDirectory(folder); + File.WriteAllText(Path.Combine(folder, fileName), script); + + return $"{providerFolder}/{SeedPathResolver.ObjectFolder}/{fileName}"; + } + + /// + /// Script tek bir nesne tanimlamalidir; basligi tekrar calismaya dayanikli bicime cevrilir. + /// Yorumlar yalnizca aramada yok sayilir, dosyada kalir. + /// + private (string Script, string Name, string Kind) Canonicalize(string script, Regex headerRegex, string createClause, string provider) + { + var headers = string.IsNullOrWhiteSpace(script) ? null : headerRegex.Matches(CommentRegex().Replace(script, " ")); + if (headers is not { Count: 1 }) + throw new UserFriendlyException(localizer[McpErrorCodes.SqlObjectInvalid, provider]); + + var name = headers[0].Groups["name"].Value; + var kind = headers[0].Groups["kind"].Value.ToUpperInvariant() switch + { + "PROC" => "PROCEDURE", + var value => value + }; + + // Yorumdaki ornek baslik degismesin diye yalnizca nesnenin adini tasiyan ilk baslik cevrilir. + var replaced = false; + var canonical = headerRegex.Replace(script.Trim(), m => + { + if (replaced || !string.Equals(m.Groups["name"].Value, name, StringComparison.OrdinalIgnoreCase)) + return m.Value; + + replaced = true; + return $"{createClause} {kind} {m.Groups["qualified"].Value}"; + }); + + return (canonical + Environment.NewLine, name, kind); + } + + [GeneratedRegex(@"/\*.*?\*/|--[^\r\n]*", RegexOptions.Singleline)] + private static partial Regex CommentRegex(); + + [GeneratedRegex(@"\b(?:CREATE\s+OR\s+ALTER|CREATE|ALTER)\s+(?VIEW|PROCEDURE|PROC|FUNCTION)\s+(?(?:\[?dbo\]?\s*\.\s*)?\[?(?[A-Za-z_][A-Za-z0-9_]*)\]?)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex SqlServerHeaderRegex(); + + [GeneratedRegex(@"\bCREATE\s+(?:OR\s+REPLACE\s+)?(?VIEW|PROCEDURE|FUNCTION)\s+(?(?:""?public""?\s*\.\s*)?""?(?[A-Za-z_][A-Za-z0-9_]*)""?)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex PostgreSqlHeaderRegex(); +} + +/// VIEW, PROCEDURE ya da FUNCTION. +/// Seed kokune gore goreli yol. +/// Seed kokune gore goreli yol. +public sealed record McpSavedSqlObject(string Name, string Kind, string SqlServerPath, string PostgreSqlPath); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs index 9e423b7c..9a73ba3f 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs @@ -47,10 +47,6 @@ public class McpTableCreator( @"^[A-Za-z_][A-Za-z0-9_]{0,127}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); - /// - /// Varsayilan deger olarak kabul edilenler: sayi, metin sabiti ve tasarimcinin kendi - /// sablonlarinda kullandigi fonksiyonlar. Baska bir ifade script'e hic girmez. - /// /// /// Istemcinin SQL bicimiyle yazdigi tip: nvarchar(50), nvarchar (MAX), /// decimal(18, 4). Boyut tipten ayrilir; tip adi yine beyaz listeden gecer. @@ -62,6 +58,10 @@ public class McpTableCreator( /// Tasarimci decimal'i her zaman bu kesinlikle yazar (ToColumnLine). private const string DecimalSize = "18,4"; + /// + /// Varsayilan deger olarak kabul edilenler: sayi, metin sabiti ve tasarimcinin kendi + /// sablonlarinda kullandigi fonksiyonlar. Baska bir ifade script'e hic girmez. + /// private static readonly Regex SafeDefaultRegex = new( @"^(?:-?\d{1,18}(?:\.\d{1,18})?|N?'(?:[^']|'')*'|NEWID\(\)|GETDATE\(\)|GETUTCDATE\(\)|SYSUTCDATETIME\(\))$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); @@ -74,6 +74,16 @@ public class McpTableCreator( ]; // Sablon kolonlari tasarimcinin FULL_AUDIT_COLUMNS / TENANT_COLUMN tanimlariyla aynidir. + /// Tasarimcinin FK silme davranislari; anahtar istemcinin yazdigi, deger SQL karsiligidir. + private static readonly Dictionary ReferentialActions = new(StringComparer.OrdinalIgnoreCase) + { + ["NoAction"] = "NO ACTION", + ["Cascade"] = "CASCADE", + ["SetNull"] = "SET NULL", + }; + + private const string DefaultReferentialAction = "NoAction"; + private static readonly McpTableColumn IdColumn = new("Id", "uniqueidentifier", null, false, "NEWID()"); private static readonly McpTableColumn TenantColumn = new("TenantId", "uniqueidentifier", null, true, null); @@ -97,24 +107,34 @@ public class McpTableCreator( EnsureSafeIdentifier(tableName); var columns = BuildColumns(definition); + var indexes = BuildIndexes(tableName, definition.Indexes, columns); + var foreignKeys = BuildForeignKeys(definition.ForeignKeys, columns); var context = await queryExecutor.ResolveDataSourceAsync(); var isPostgreSql = context.DataSourceType == DataSourceTypeEnum.Postgresql; - var existing = await context.Repository.ExecuteScalarAsync( - isPostgreSql ? "SELECT to_regclass(@name)::text" : "SELECT OBJECT_ID(@name, 'U')", - context.ConnectionString, - new Dictionary { ["name"] = isPostgreSql ? $"public.\"{tableName}\"" : $"{SchemaName}.{tableName}" }); + async Task TableExistsAsync(string name) + { + var existing = await context.Repository.ExecuteScalarAsync( + isPostgreSql ? "SELECT to_regclass(@name)::text" : "SELECT OBJECT_ID(@name, 'U')", + context.ConnectionString, + new Dictionary { ["name"] = isPostgreSql ? $"public.\"{name}\"" : $"{SchemaName}.{name}" }); + + return existing is not null and not DBNull; + } // Henuz migrate edilmemis bir create_table da tabloyu sahiplenmis sayilir; ikinci cagri onun // scriptinin ustune yazmaz. - if (existing is not null and not DBNull || PostgreSqlTableScript.ReadSeedColumns(configuration, tableName).Count > 0) + if (await TableExistsAsync(tableName) || PostgreSqlTableScript.ReadSeedColumns(configuration, tableName).Count > 0) { throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, tableName]); } + foreach (var foreignKey in foreignKeys) + await EnsureReferenceInstalledFirstAsync(tableName, foreignKey, TableExistsAsync); + // Dosya biçimi tasarımcınınkiyle aynı T-SQL'dir; PostgreSQL eşi ondan üretilir. - var script = BuildScript(tableName, columns); + var script = BuildScript(tableName, columns, indexes, foreignKeys); if (!PostgreSqlTableScript.TryConvert(script, out _)) { throw new InvalidOperationException($"PostgreSQL script could not be generated for {tableName}."); @@ -161,6 +181,69 @@ public class McpTableCreator( return columns; } + /// Index ve unique kolonlari tablonun kolonlari arasinda olmalidir; ad tasarimcinin kalibindadir. + private List BuildIndexes(string tableName, IReadOnlyList indexes, List columns) + { + var result = new List(); + foreach (var index in indexes) + { + var indexColumns = index.Columns.Select(column => ResolveColumn(column, columns)).Distinct().ToList(); + var name = $"{(index.Unique ? "UQ" : "IX")}_{tableName}_{string.Join("_", indexColumns)}"; + + // Kolonsuz ya da ayni kolonlarla tekrar eden index uretilecek bir sey tasimaz. + if (indexColumns.Count > 0 && !result.Exists(existing => string.Equals(existing.Name, name, StringComparison.OrdinalIgnoreCase))) + result.Add(new McpTableIndex(indexColumns, index.Unique, name)); + } + + return result; + } + + private List BuildForeignKeys(IReadOnlyList foreignKeys, List columns) + { + var result = new List(); + foreach (var foreignKey in foreignKeys) + { + var column = ResolveColumn(foreignKey.Column, columns); + var referencedTable = (foreignKey.ReferencedTable ?? string.Empty).Trim(); + var referencedColumn = string.IsNullOrWhiteSpace(foreignKey.ReferencedColumn) ? IdColumn.Name : foreignKey.ReferencedColumn.Trim(); + EnsureSafeIdentifier(referencedTable); + EnsureSafeIdentifier(referencedColumn); + + // Tanimadigi davranis (Restrict gibi) SQL Server'in varsayilanina, NO ACTION'a duser. + var action = ReferentialActions.Keys.FirstOrDefault(key => string.Equals(key, foreignKey.OnDelete?.Trim(), StringComparison.OrdinalIgnoreCase)) + ?? DefaultReferentialAction; + + // Bir kolonun tek FK'si olur; ayni kolon icin tekrar eden tanimin ilki gecerlidir. + if (!result.Exists(existing => string.Equals(existing.Column, column, StringComparison.OrdinalIgnoreCase))) + result.Add(new McpTableForeignKey(column, referencedTable, referencedColumn, action)); + } + + return result; + } + + /// + /// Seed scriptleri DB Migrate'te dosya adi sirasiyla calisir (SqlDataSeeder). FK'nin + /// gosterdigi tablo bir seed scriptiyle kuruluyorsa o dosya bu tablonunkinden once gelmelidir; + /// scripti yoksa (EF ile kurulan platform tablosu) veritabaninda bulunmalidir. Aksi halde + /// sifirdan kurulumda migrate FK satirinda duserdi. + /// + private async Task EnsureReferenceInstalledFirstAsync(string tableName, McpTableForeignKey foreignKey, Func> tableExistsAsync) + { + var hasScript = PostgreSqlTableScript.ReadSeedColumns(configuration, foreignKey.ReferencedTable).Count > 0; + var installed = hasScript + ? Comparer.Default.Compare($"{foreignKey.ReferencedTable}.sql", $"{tableName}.sql") < 0 + : await tableExistsAsync(foreignKey.ReferencedTable); + + if (!installed) + { + throw new UserFriendlyException(localizer[McpErrorCodes.ForeignKeyTableNotInstalledFirst, foreignKey.Column, foreignKey.ReferencedTable, tableName]); + } + } + + private string ResolveColumn(string? name, List columns) => + columns.FirstOrDefault(column => string.Equals(column.Name, name?.Trim(), StringComparison.OrdinalIgnoreCase))?.Name + ?? throw new UserFriendlyException(localizer[McpErrorCodes.TableColumnNotFound, name ?? string.Empty]); + private McpTableColumn NormalizeColumn(McpTableColumn column) { var name = (column.Name ?? string.Empty).Trim(); @@ -230,7 +313,7 @@ public class McpTableCreator( return (type, maxLength); } - private static string BuildScript(string tableName, List columns) + private static string BuildScript(string tableName, List columns, List indexes, List foreignKeys) { var fullTableName = $"[{SchemaName}].[{tableName}]"; @@ -251,6 +334,42 @@ public class McpTableCreator( lines.Add("END"); lines.Add("GO"); + // Tasarimcinin unique/index/FK deyimleri; her biri varlik kontroluyle yazilir ki migrate + // scripti yeniden calistirdiginda dusmesin. + foreach (var index in indexes) + { + var columnList = string.Join(", ", index.Columns.Select(column => $"[{column}] ASC")); + lines.Add(string.Empty); + if (index.Unique) + { + lines.Add($"IF OBJECT_ID(N'[{SchemaName}].[{index.Name}]', 'UQ') IS NULL"); + lines.Add("BEGIN"); + lines.Add($" ALTER TABLE {fullTableName} ADD CONSTRAINT [{index.Name}] UNIQUE NONCLUSTERED ({columnList});"); + } + else + { + lines.Add($"IF INDEXPROPERTY(OBJECT_ID(N'{fullTableName}'), '{index.Name}', 'IndexID') IS NULL"); + lines.Add("BEGIN"); + lines.Add($" CREATE INDEX [{index.Name}] ON {fullTableName} ({columnList});"); + } + + lines.Add("END"); + lines.Add("GO"); + } + + foreach (var foreignKey in foreignKeys) + { + var name = $"FK_{tableName}_{foreignKey.Column}"; + lines.Add(string.Empty); + lines.Add($"IF OBJECT_ID(N'[{SchemaName}].[{name}]', 'F') IS NULL"); + lines.Add("BEGIN"); + lines.Add($" ALTER TABLE {fullTableName} ADD CONSTRAINT [{name}] FOREIGN KEY ([{foreignKey.Column}]) " + + $"REFERENCES [{SchemaName}].[{foreignKey.ReferencedTable}] ([{foreignKey.ReferencedColumn}]) " + + $"ON DELETE {ReferentialActions[foreignKey.OnDelete!]} ON UPDATE NO ACTION;"); + lines.Add("END"); + lines.Add("GO"); + } + return string.Join('\n', lines); } @@ -280,11 +399,21 @@ public class McpTableCreator( /// Tam audit kolonlari (olusturma/degistirme/silme izi) eklensin mi. /// TenantId kolonu eklensin mi. +/// Index ve unique kisitlari; ad sunucuda uretilir (IX_/UQ_{Tablo}_{Kolonlar}). +/// FK'ler; ad FK_{Tablo}_{Kolon}. public sealed record McpTableDefinition( string TableName, IReadOnlyList Columns, bool FullAudited, - bool MultiTenant); + bool MultiTenant, + IReadOnlyList Indexes, + IReadOnlyList ForeignKeys); + +public sealed record McpTableIndex(IReadOnlyList Columns, bool Unique, string? Name = null); + +/// Bos ise Id. +/// NoAction (varsayilan), Cascade ya da SetNull. +public sealed record McpTableForeignKey(string Column, string ReferencedTable, string? ReferencedColumn, string? OnDelete); /// Yalnizca nvarchar icin; bos ise tasarimcinin varsayilani (100). public sealed record McpTableColumn( diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs index 9c7e47c5..f4ffaa90 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs @@ -38,7 +38,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc => await developAppService.GetDevGuideAsync(document, section); [McpServerTool(Name = McpToolNames.CreateTable, Destructive = false, Idempotent = false)] - [Description("Ekranin baglanacagi yeni tablonun script'ini SQL Query Manager tablo tasarimcisiyla ayni bicimde uretir ve seed dosyasina (sql/object + postgres/object) yazar; veritabaninda calistirmaz, tabloyu kullanicinin calistirdigi DB Migrate kurar. DB Migrate'i bekleme ve calistirmaya calisma; deploy_wizard kolonlari script'ten okur. Id (uniqueidentifier, PK), TenantId ve tam audit kolonlari (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId) otomatik eklenir; columns icine yalnizca is kolonlarini yaz. Yalnizca yeni tablo icindir: tablo ya da script'i varsa reddeder, kolon ekleme/degistirme tablo tasarimcisindan yapilir. Ardindan deploy_wizard ile ekrani kur.")] + [Description("Ekranin baglanacagi yeni tablonun script'ini SQL Query Manager tablo tasarimcisiyla ayni bicimde uretir ve seed dosyasina (sql/object + postgres/object) yazar; veritabaninda calistirmaz, tabloyu kullanicinin calistirdigi DB Migrate kurar. DB Migrate'i bekleme ve calistirmaya calisma; deploy_wizard kolonlari script'ten okur. Id (uniqueidentifier, PK), TenantId ve tam audit kolonlari (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId) otomatik eklenir; columns icine yalnizca is kolonlarini yaz. indexes ile index/unique, foreignKeys ile FK kisitlari ayni scripte yazilir. Yalnizca yeni tablo icindir: tablo ya da script'i varsa reddeder, kolon ekleme/degistirme tablo tasarimcisindan yapilir. View/procedure/function icin save_sql_object kullan. Ardindan deploy_wizard ile ekrani kur.")] public async Task CreateTableAsync( [Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")] string tableName, @@ -47,15 +47,41 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc [Description("Tam audit kolonlari eklensin mi. Kullanici acikca istemedikce true birak.")] bool fullAudited = true, [Description("TenantId kolonu eklensin mi. Kullanici acikca istemedikce true birak.")] - bool multiTenant = true) + bool multiTenant = true, + [Description("Index ve unique kisitlari: [{\"columns\": [\"Code\"], \"unique\": true}]. Ad sunucuda IX_/UQ_{Tablo}_{Kolonlar} olarak uretilir. Kiraci tablosunda unique kisita TenantId'yi de ekle.")] + List? indexes = null, + [Description("FK'ler: [{\"column\": \"UomId\", \"referencedTable\": \"Adm_T_Uom\", \"referencedColumn\": \"Id\", \"onDelete\": \"NoAction\"}]. onDelete: NoAction (varsayilan), Cascade, SetNull. Migrate scriptleri dosya adi sirasiyla calistirdigi icin hedef tablo ya scripti bu tablonunkinden once siralanan bir seed tablosu ya da veritabaninda kurulu bir platform tablosu olmalidir; degilse reddedilir.")] + List? foreignKeys = null) => await developAppService.CreateTableAsync(new McpCreateTableRequestDto { TableName = tableName, Columns = columns, FullAudited = fullAudited, - MultiTenant = multiTenant + MultiTenant = multiTenant, + Indexes = indexes ?? [], + ForeignKeys = foreignKeys ?? [] }); + [McpServerTool(Name = McpToolNames.SaveSqlObject, Destructive = true, Idempotent = true)] + [Description("Ekranin veri kaynagi ya da lookup'i olacak view, stored procedure ya da function scriptini SQL Query Manager'in yazdigi yere (sql/object/{Ad}.sql) ve PostgreSQL esine (postgres/object/{Ad}.sql) yazar; ad scriptteki nesne adidir; veritabaninda calistirmaz, nesneyi kullanicinin calistirdigi DB Migrate kurar. Uygulama iki saglayicida calistigi icin iki script de zorunludur ve ayni nesneyi ayni parametre sirasiyla tanimlar (SQL Server: [dbo], PostgreSQL: public, ceviri kurallari get_dev_guide lowcode 8.3.1). Baslik tekrar calismaya dayanikli bicime cevrilir (CREATE OR ALTER / CREATE OR REPLACE); ayni adla tekrar cagrilirsa script guncellenir. Migrate object scriptlerini dosya adi sirasiyla calistirir: view'in okudugu tablolarin adi view'in adindan once siralanmalidir (Inv_T_Product → Inv_V_Product). Birden fazla tablodan beslenen ekranda Query (SelectCommandType 4) yerine bu aracla view yazip ekrani view'e (SelectCommandType 2) bagla. Tablo icin create_table kullan.")] + public async Task SaveSqlObjectAsync( + [Description("SQL Server scripti: tek bir CREATE [OR ALTER] VIEW/PROCEDURE/FUNCTION [dbo].[Ad] tanimi; GO ile biten batch'ler kullanilabilir. Dosya adi nesnenin adidir.")] + string sqlServerScript, + [Description("PostgreSQL scripti: ayni nesnenin CREATE OR REPLACE VIEW/FUNCTION/PROCEDURE public.\"Ad\" tanimi.")] + string postgreSqlScript) + => await developAppService.SaveSqlObjectAsync(sqlServerScript, postgreSqlScript); + + [McpServerTool(Name = McpToolNames.GenerateCrudEndpoints, Destructive = true, Idempotent = true)] + [Description("Bir tablo ya da view icin SQL Query Manager'daki CRUD endpoint diyaloguyla ayni REST endpoint'lerini (/api/app/crudendpoint/{entity}: GetList, GetById, Create, Update, Delete) yalnizca crud/{entity}.json seed dosyasina yazar; kayitlari DB Migrate ekler (migrate'i bekleme). Ayni entity icin tekrar cagrilirsa dosya yeniden yazilir. Custom component'in veri okuyup yazacagi tablo icin kullanilir; ListForm ekranlari bu endpoint'lere ihtiyac duymaz. View/procedure/function icin yalnizca GetList ve GetById uretilir. App.SqlQueryManager.CrudEndpoints yetkisi ister.")] + public async Task GenerateCrudEndpointsAsync( + [Description("Tablo ya da view adi, ornegin 'Inv_T_Product' (sema oneki atilir).")] + string entityName, + [Description("Aktif baslayacak operasyonlar, virgulle: GetList,GetById,Create,Update,Delete. Bos ise hepsi aktif; digerleri pasif kaydedilir.")] + string? operationTypes = null, + [Description("Nesne turu: table (varsayilan), view, procedure, function.")] + string? objectType = null) + => await developAppService.GenerateCrudEndpointsAsync(entityName, operationTypes, objectType); + [McpServerTool(Name = McpToolNames.ListWizards, ReadOnly = true)] [Description("Wizard File Manager'daki wizard dosyalarini listeler: dosya adi, ekran kodu, yol (List/Custom), menu yolu ve bagimli wizard'lar.")] public async Task> ListWizardsAsync( @@ -71,9 +97,9 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc => await developAppService.GetWizardAsync(fileName); [McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)] - [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] + [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir. Wizard cevaplarinin uretmedigi ekran ayarlari (FormFieldsDefaultValueDto/InsertFieldsDefaultValueDto gibi varsayilan degerler, ValidationRuleDto, Format, Alignment, Width, Visible, BandName, ozet/gruplama, ExtraFilter, Insert/Update/Delete komutlari, PageSize, CommandColumnDto …) ayni JSON'un ListForm ve Fields bolumlerine get_wizard ciktisindaki adlarla yazilir; bolumler kismi olabilir (yalnizca degisen anahtarlar), Fields ogeleri FieldName ile eslesir, *Json adlari (FormFieldsDefaultValueJson) *Dto karsiligina cevrilir. Guncellemede get_wizard ciktisini butunuyle degistirip gonder: tasarimci ayarlari korunur; Wizard bolumunde yazdigin cevap ayni ayarin ListForm'daki eski degerine ustun gelir. Kimlik alanlari (ListFormCode, Name, Title, Description, PermissionDto, FieldName) wizard'dan gelir. Fields'taki her alan Wizard Groups'ta da bulunmalidir; tanınmayan ya da hesaplanan bir anahtar reddedilir. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] public async Task DeployWizardAsync( - [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")] + [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}, \"ListForm\": {...}, \"Fields\": [...]}, get_wizard cikisi gibi) ya da duz wizard cevaplari. ListForm/Fields istege baglidir ve kismi olabilir. Enum alanlari sayi olarak yazilir.")] string wizard, [Description("Guncellemede yerine gecilecek dosyanin adi (list_wizards cikisindaki fileName). Yeni wizard icin bos birak.")] string? replaceFileName = null, diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs index 324dbbf4..ca3eec3b 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs @@ -31,7 +31,7 @@ public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService, ICurre } [McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)] - [Description("Kiracinin listelerini (ekranlarini) ad, baslik, aciklama ve arkasindaki tablo ile dondurur. Arama metni verilmezse tumu doner.")] + [Description("Kiracinin listelerini (ekranlarini) ad, baslik, aciklama ve arkasindaki tablo ile dondurur. Arama metni verilmezse tumu doner. Gelistiriciye (host, Wizard yetkisi) DB Migrate bekleyen wizard ekranlari (sourceKind pending) ve yalnizca seed scripti olan tablolar (pendingTable) da doner; lookup, widget, view ve Query SQL'ini yazarken kolon adlarini describe_list ile buradan al, tahmin etme.")] public async Task> ListListsAsync( [Description("Liste adi, basligi, aciklamasi ya da tablo adi icinde aranacak metin. Bos birakilabilir.")] string? search = null, diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs index 954bac8f..ae53afea 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -46,6 +46,14 @@ public class ListFormWizardDto [JsonIgnore] public Guid? DevRequestId { get; set; } + /// + /// Deploy girdisindeki ListForm/Fields bolumleri; wizard cevaplarindan uretilen + /// ekrana katman olarak uygulanir (). Yalnizca + /// sunucu ici girdidir, dosyaya yazilmaz: uygulanan ayarlar ListForm/Fields bolumlerinde kalir. + /// + [JsonIgnore] + public WizardScreenOverlay ScreenOverlay { get; set; } + /// /// Wizard'in ilk deploy edildigi an. Dosya adi tarih tasimadigi icin seeder dosyalari bu /// damgaya gore siralar; birbirine bagli wizard'lar (once Customers, sonra Tasks) uretildikleri diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardScreenOverlay.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardScreenOverlay.cs new file mode 100644 index 00000000..65e45026 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardScreenOverlay.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Sozsoft.Platform.ListForms; + +/// +/// Deploy girdisindeki ListForm/Fields bolumleri. Wizard cevaplari ekranin yalnizca bir +/// kismini uretir; varsayilan degerler, dogrulama kurallari, bicim, komutlar, ek filtre gibi ayarlar +/// ListForm detayinda (tasarimci) yapilir. Bu katman ayni ayarlari deploy'da seed dosyasinin +/// bicimiyle (get_wizard ciktisi) kabul eder; boylece MCP tasarimcinin yapabildigi her seyi +/// wizard dosyasina yazabilir ve yeniden deploy tasarimci ayarlarini silmez. +/// +/// Birlestirme uc yonludur: bolumdeki deger, bolumlerden geri kurulan wizard'in uretecegi degerle +/// () ayniysa katman sayilmaz ve wizard cevabi gecerli olur; farkliysa +/// tasarimci ayaridir ve uretilen ekranin ustune yazilir. Kimlik alanlari (kod, ad, dil anahtarli +/// baslik, yetki) her zaman wizard'dan gelir. +/// +/// +public class WizardScreenOverlay +{ + public JsonObject ListForm { get; set; } + + public List Fields { get; set; } = []; + + /// Bolumlerden geri kurulan wizard; uc yonlu birlestirmenin tabani. + public ListFormWizardDto Baseline { get; set; } + + /// Wizard'dan turedigi icin katmanla degistirilmeyen ekran alanlari. + private static readonly string[] ProtectedListFormKeys = + [ + nameof(GridOptionsEditDto.Id), nameof(GridOptionsEditDto.ListFormCode), nameof(GridOptionsEditDto.Name), + nameof(GridOptionsEditDto.Title), nameof(GridOptionsEditDto.Description), nameof(GridOptionsEditDto.CultureName), + nameof(GridOptionsEditDto.PermissionDto), nameof(GridOptionsEditDto.EditType), + nameof(GridOptionsEditDto.CreationTime), nameof(GridOptionsEditDto.CreatorId), + nameof(GridOptionsEditDto.LastModificationTime), nameof(GridOptionsEditDto.LastModifierId), + ]; + + private static readonly string[] ProtectedFieldKeys = + [ + nameof(ColumnFormatEditDto.Id), nameof(ColumnFormatEditDto.ListFormCode), nameof(ColumnFormatEditDto.FieldName), + nameof(ColumnFormatEditDto.CultureName), nameof(ColumnFormatEditDto.PermissionDto), nameof(ColumnFormatEditDto.EditType), + nameof(ColumnFormatEditDto.CreationTime), nameof(ColumnFormatEditDto.CreatorId), + nameof(ColumnFormatEditDto.LastModificationTime), nameof(ColumnFormatEditDto.LastModifierId), + ]; + + /// Wizard cevabindaki adla yazilan alan anahtarlari; alan sozlesmesindeki karsiliklari. + private static readonly Dictionary FieldAliases = new(StringComparer.OrdinalIgnoreCase) + { + [nameof(WizardColumnItemInputDto.EditorType)] = nameof(ColumnFormatEditDto.EditorType2), + [nameof(WizardColumnItemInputDto.DbSourceType)] = nameof(ColumnFormatEditDto.SourceDbType), + }; + + private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true }; + + /// + /// Katmani wizard'in urettigi ListForm'a uygular. ayni donusumun + /// wizard'i icin ciktisidir. + /// + public GridOptionsEditDto Apply(GridOptionsEditDto built, GridOptionsEditDto baseline, List errors) + { + if (ListForm == null) + return built; + + return Merge(built, ListForm, baseline, ProtectedListFormKeys, [], "ListForm", errors); + } + + /// + /// Katmani alanlara uygular; alanlar FieldName ile eslesir. Wizard gruplarinda olmayan bir + /// alan icin katman yazilamaz: alan kumesi gruplardan gelir. + /// + public List Apply(List built, List baseline, List errors) + { + if (Fields.Count == 0) + return built; + + var result = built.ToList(); + foreach (var overlay in Fields) + { + var fieldName = Text(overlay, nameof(ColumnFormatEditDto.FieldName)); + var index = result.FindIndex(f => string.Equals(f.FieldName, fieldName, StringComparison.OrdinalIgnoreCase)); + if (index < 0) + { + errors.Add($"Fields: '{fieldName}' alanı wizard gruplarında (Groups) yok; alanı bir gruba ekleyin ya da Fields'tan çıkarın."); + continue; + } + + var source = baseline.FirstOrDefault(f => string.Equals(f.FieldName, fieldName, StringComparison.OrdinalIgnoreCase)); + result[index] = Merge(result[index], overlay, source, ProtectedFieldKeys, FieldAliases, $"Fields[{fieldName}]", errors); + } + + return result; + } + + private static T Merge( + T built, + JsonObject overlay, + T baseline, + string[] protectedKeys, + Dictionary aliases, + string path, + List errors) + { + var target = JsonSerializer.SerializeToNode(built) as JsonObject ?? []; + var baseNode = baseline == null ? null : JsonSerializer.SerializeToNode(baseline) as JsonObject; + var writable = WritableProperties(typeof(T)); + var touched = new List(); + + foreach (var (rawKey, rawValue) in overlay) + { + var (key, value) = Resolve(target, rawKey, rawValue, aliases); + if (key == null) + { + errors.Add($"{path}: '{rawKey}' tanınmayan bir alan."); + continue; + } + + if (protectedKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) + continue; + + if (!writable.Contains(key)) + { + if (!JsonNode.DeepEquals(value, target[key])) + errors.Add($"{path}: '{rawKey}' hesaplanan bir alandır, yazılamaz."); + continue; + } + + var merged = MergeValue(target[key], value, baseNode?[key]); + if (merged.Changed) + { + target[key] = merged.Value; + touched.Add(key); + } + } + + // Ayni Json kolonunu besleyen iki ozellik olabilir (ExtraFilterDto / ExtraFilterEditDto); + // degisen anahtar sona alinir ki okurken son yazilan o olsun. + foreach (var key in touched.Distinct()) + { + target.Remove(key, out var value); + target[key] = value; + } + + return target.Deserialize(ReadOptions); + } + + /// + /// Nesneler alan alan birlestirilir; dizi ve tekil degerler tumden yazilir. Tabandaki degerle + /// ayni olan katman degeri wizard'in turettigi degeri ezmez. + /// + private static (bool Changed, JsonNode Value) MergeValue(JsonNode current, JsonNode overlay, JsonNode baseline) + { + if (baseline != null && JsonNode.DeepEquals(overlay, baseline)) + return (false, current); + + if (current is JsonObject currentObject && overlay is JsonObject overlayObject) + { + var result = (JsonObject)currentObject.DeepClone(); + var changed = false; + foreach (var (key, value) in overlayObject) + { + var existing = result.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Key ?? key; + var merged = MergeValue(result[existing], value, (baseline as JsonObject)?[existing]); + if (merged.Changed) + { + result[existing] = merged.Value; + changed = true; + } + } + + return (changed, result); + } + + return (true, overlay?.DeepClone()); + } + + /// + /// Anahtari sozlesmedeki ada cevirir: buyuk/kucuk harf duyarsiz, wizard adlari (EditorType) + /// ve rehberdeki kolon adlari (FormFieldsDefaultValueJson → FormFieldsDefaultValueDto). + /// Json kolonu metin olarak verilmisse cozulur. + /// + private static (string Key, JsonNode Value) Resolve(JsonObject target, string key, JsonNode value, Dictionary aliases) + { + if (aliases.TryGetValue(key, out var alias)) + key = alias; + + var found = Find(target, key); + if (found == null && key.EndsWith("Json", StringComparison.OrdinalIgnoreCase)) + { + found = Find(target, key[..^4] + "Dto"); + if (found != null && value is JsonValue text && text.TryGetValue(out var json)) + { + try + { + value = string.IsNullOrWhiteSpace(json) ? null : JsonNode.Parse(json); + } + catch (JsonException) + { + return (found, value); + } + } + } + + return (found, value?.DeepClone()); + } + + private static string Find(JsonObject node, string key) => + node.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Key; + + private static string Text(JsonObject node, string key) => + node.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Value is JsonValue value && + value.TryGetValue(out var text) ? text : null; + + private static HashSet WritableProperties(Type type) => + [ + .. type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite && p.SetMethod?.IsPublic == true && p.GetCustomAttribute() == null) + .Select(p => p.Name) + ]; +} diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs index eec297ef..33a1f138 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -285,23 +285,136 @@ public class WizardSeedFileDto { var node = JsonNode.Parse(json); if (node?[nameof(Wizard)] is JsonObject wizardNode) - { - FlattenSection(wizardNode, MenuSection, MenuKeys); - FlattenSection(wizardNode, LanguageSection, LanguageKeys); - - foreach (var (property, seedKey) in RenamedKeys) - Rename(wizardNode, seedKey, property); - - foreach (var item in ItemNodes(wizardNode)) - { - foreach (var (property, seedKey) in CaptionKeys) - Rename(item, seedKey, property); - } - } + FlattenWizard(wizardNode); return node.Deserialize(SeedReadOptions)?.Rehydrate(); } + /// + /// Deploy girdisini (MCP deploy_wizard) okur: seed dosyasi bicimi ({"Wizard": ..., "ListForm": ..., "Fields": [...]}) + /// ya da duz wizard cevaplari. 'dan farki, bolumlerin eksik ya da + /// kismi olabilmesidir: + /// + /// Wizard bolumunde yazilan cevap korunur; yazilmayan ekran alani (veri kaynagi, duzen, + /// alan editoru …) ListForm/Fields bolumunden tamamlanir. Boylece get_wizard ciktisina + /// yeni bir alan eklemek, Fields'ta karsiligi olmadigi icin sessizce dusmez. + /// ListForm/Fields bolumleri ekranin sozlesmesine katman olarak uygulanir + /// (): wizard cevaplarinin uretmedigi her ayar (varsayilan + /// degerler, dogrulama, bicim, komutlar …) deploy'da ekranda kalir. + /// + /// + public static ListFormWizardDto FromDeployJson(string json) + { + if (JsonNode.Parse(json) is not JsonObject root) + return null; + + if (!root.ContainsKey(nameof(Wizard))) + root = new JsonObject { [nameof(Wizard)] = root }; + + if (root[nameof(Wizard)] is not JsonObject wizardNode) + return null; + + FlattenWizard(wizardNode); + var wizard = wizardNode.Deserialize(SeedReadOptions); + if (wizard == null) + return null; + + var listForm = root[nameof(ListForm)] as JsonObject; + var fields = root[nameof(Fields)] as JsonArray; + if (listForm == null && fields == null) + return wizard; + + // Bolumlerden geri kurulan wizard hem eksik cevaplarin kaynagi hem de katmanin karsilastirma + // tabanidir: bolumde wizard'in zaten urettigi deger katman sayilmaz. + var sections = new JsonObject + { + [nameof(Wizard)] = wizardNode.DeepClone(), + [nameof(ListForm)] = listForm?.DeepClone() ?? new JsonObject(), + [nameof(Fields)] = fields?.DeepClone() ?? new JsonArray(), + }.Deserialize(SeedReadOptions).Rehydrate().Wizard; + + if (listForm != null) + FillMissing(wizard, sections, wizardNode, ListFormOwnedWizardKeys); + + if (fields != null) + FillMissingGroups(wizard, sections, wizardNode); + + wizard.ScreenOverlay = new WizardScreenOverlay + { + ListForm = listForm, + Fields = [.. (fields ?? []).OfType()], + Baseline = sections, + }; + + return wizard; + } + + /// Bolumde yazilmamis wizard alanlarini 'tan kopyalar. + private static void FillMissing(T target, T source, JsonObject written, IEnumerable keys) + { + foreach (var key in keys.Where(key => !ContainsKey(written, key))) + { + var property = typeof(T).GetProperty(key); + property?.SetValue(target, property.GetValue(source)); + } + } + + /// + /// Gruplar wizard bolumunde hic yoksa Fields'tan kurulur; varsa yazilan gruplar ve ogeler + /// esas alinir, ogenin yazilmamis editor/lookup alanlari ayni adli alandan tamamlanir. + /// + private static void FillMissingGroups(ListFormWizardDto wizard, ListFormWizardDto sections, JsonObject written) + { + if (written[nameof(ListFormWizardDto.Groups)] is not JsonArray groupNodes) + { + wizard.Groups = sections.Groups; + return; + } + + var sectionItems = sections.Groups + .SelectMany(g => g.Items) + .Where(i => !string.IsNullOrWhiteSpace(i.FieldName)) + .GroupBy(i => i.FieldName, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); + + for (var gi = 0; gi < wizard.Groups.Count && gi < groupNodes.Count; gi++) + { + if (groupNodes[gi] is not JsonObject groupNode) + continue; + + var group = wizard.Groups[gi]; + if (gi < sections.Groups.Count) + FillMissing(group, sections.Groups[gi], groupNode, FieldsOwnedGroupKeys); + + var itemNodes = groupNode[nameof(WizardColumnGroupInputDto.Items)] as JsonArray ?? []; + for (var ii = 0; ii < group.Items.Count && ii < itemNodes.Count; ii++) + { + var item = group.Items[ii]; + if (itemNodes[ii] is JsonObject itemNode && sectionItems.TryGetValue(item.FieldName ?? string.Empty, out var source)) + FillMissing(item, source, itemNode, FieldsOwnedItemKeys); + } + } + } + + private static bool ContainsKey(JsonObject node, string key) => + node.Any(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)); + + /// `Menu`/`Language` basliklarini duzlestirir ve dosyadaki adlari sozlesme adlarina cevirir. + private static void FlattenWizard(JsonObject wizardNode) + { + FlattenSection(wizardNode, MenuSection, MenuKeys); + FlattenSection(wizardNode, LanguageSection, LanguageKeys); + + foreach (var (property, seedKey) in RenamedKeys) + Rename(wizardNode, seedKey, property); + + foreach (var item in ItemNodes(wizardNode)) + { + foreach (var (property, seedKey) in CaptionKeys) + Rename(item, seedKey, property); + } + } + /// Wizard bolumundeki tum grup ogeleri; ad donusumu her ogeye ayni sekilde uygulanir. private static IEnumerable ItemNodes(JsonObject wizard) => (wizard[nameof(ListFormWizardDto.Groups)] as JsonArray ?? []) diff --git a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs index a7fe954f..27c91fee 100644 --- a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs +++ b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -25,11 +24,6 @@ public class CrudEndpointGenerateAppService : CrudAppService< PagedAndSortedResultRequestDto, CreateUpdateCrudEndpointDto>, ICrudEndpointAppService { - private static readonly JsonSerializerOptions SeedJsonWriteOptions = new() - { - WriteIndented = true - }; - private readonly IConfiguration _configuration; public CrudEndpointGenerateAppService( @@ -76,72 +70,27 @@ public class CrudEndpointGenerateAppService : CrudAppService< [FromQuery] string? operationTypes = null, [FromQuery] string? objectType = null) { - // Tablolar icin bes endpoint 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); - - // View / stored procedure / function yazma islemlerini desteklemez; - // bu nesneler icin yalnizca okuma endpoint'leri (GetList, GetById) uretilir. - var isReadOnlyObject = !string.IsNullOrWhiteSpace(objectType) - && !string.Equals(objectType.Trim(), "table", StringComparison.OrdinalIgnoreCase); - - // CRUD endpointleri oluştur - var endpoints = new List - { - new() { + // Endpoint kumesi (operasyonlar, yollar, aktiflik) seed dosyasiyla ayni kaynaktan gelir; + // MCP ayni kumeyi yalnizca dosyaya yazar. + var endpoints = CrudEndpointSeedFileDto.Generate(entityName, operationTypes, objectType, Clock.Now.ToUniversalTime()) + .Endpoints + .Select(item => new CrudEndpoint + { EntityName = entityName, - Method = "GET", - Path = $"/api/app/crudendpoint/{entityName}", - OperationType = "GetList", - IsActive = IsActiveOperation("GetList"), - CsharpCode = GenerateGetAllCode(entityName) - }, - new() { - EntityName = entityName, - Method = "GET", - Path = $"/api/app/crudendpoint/{entityName}/{{id}}", - OperationType = "GetById", - IsActive = IsActiveOperation("GetById"), - CsharpCode = GenerateGetByIdCode(entityName) - }, - new() { - EntityName = entityName, - Method = "POST", - Path = $"/api/app/crudendpoint/{entityName}", - OperationType = "Create", - IsActive = IsActiveOperation("Create"), - CsharpCode = GenerateCreateCode(entityName) - }, - new() { - EntityName = entityName, - Method = "PUT", - Path = $"/api/app/crudendpoint/{entityName}/{{id}}", - OperationType = "Update", - IsActive = IsActiveOperation("Update"), - CsharpCode = GenerateUpdateCode(entityName) - }, - new() { - EntityName = entityName, - Method = "DELETE", - Path = $"/api/app/crudendpoint/{entityName}/{{id}}", - OperationType = "Delete", - IsActive = IsActiveOperation("Delete"), - CsharpCode = GenerateDeleteCode(entityName) - } - }; - - if (isReadOnlyObject) - { - endpoints = endpoints - .Where(x => x.OperationType is "GetList" or "GetById") - .ToList(); - } + Method = item.Method, + Path = item.Path, + OperationType = item.OperationType, + IsActive = item.IsActive, + CsharpCode = item.OperationType switch + { + "GetList" => GenerateGetAllCode(entityName), + "GetById" => GenerateGetByIdCode(entityName), + "Create" => GenerateCreateCode(entityName), + "Update" => GenerateUpdateCode(entityName), + _ => GenerateDeleteCode(entityName) + } + }) + .ToList(); // Var olanları sil var existingEndpoints = await Repository @@ -209,7 +158,7 @@ public class CrudEndpointGenerateAppService : CrudAppService< /// private async Task SyncSeedFileAsync(string entityName) { - var safeFileName = GetSafeFileName(entityName); + var safeFileName = CrudEndpointSeedFileDto.SafeFileName(entityName); if (safeFileName == null) { return; @@ -217,13 +166,11 @@ public class CrudEndpointGenerateAppService : CrudAppService< try { - var outputPath = ResolveCrudSeedOutputPath(); - var filePath = Path.Combine(outputPath, $"{safeFileName}.json"); - var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName); if (endpoints.Count == 0) { + var filePath = Path.Combine(SeedPathResolver.GetPath(_configuration, SeedPathResolver.CrudFolder), $"{safeFileName}.json"); if (File.Exists(filePath)) { File.Delete(filePath); @@ -233,8 +180,6 @@ public class CrudEndpointGenerateAppService : CrudAppService< return; } - Directory.CreateDirectory(outputPath); - var seedFile = new CrudEndpointSeedFileDto { EntityName = entityName, @@ -251,8 +196,7 @@ public class CrudEndpointGenerateAppService : CrudAppService< .ToList() }; - await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, SeedJsonWriteOptions)); - Logger.LogInformation("CRUD seed file saved: {FilePath}", filePath); + Logger.LogInformation("CRUD seed file saved: {FilePath}", seedFile.Write(_configuration)); } catch (Exception ex) { @@ -260,31 +204,6 @@ public class CrudEndpointGenerateAppService : CrudAppService< } } - /// - /// CRUD seed dosyalarinin yazilacagi klasor (crud/); host ve kendi veritabani olan her - /// tenant ayni dosyalarla kurulur. - /// - private string ResolveCrudSeedOutputPath() - { - return SeedPathResolver.GetPath(_configuration, 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.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index d6af71b3..e09b9f98 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -314,7 +314,6 @@ public class ListFormWizardAppService( ? await IsQueryMainTableSoftDeletedAsync(input) : tableColumns.ContainsKey("IsDeleted"); isCreated = tableColumns.ContainsKey("CreatorId"); - ValidateImageColumnLengths(input, tableColumns); foreach (var item in input.Groups.SelectMany(g => g.Items)) item.CaptionName = await ResolveLanguageKeyCasingAsync(rows, item.CaptionName); listFormDto = input.BuildListForm(isDeleted, isCreated); @@ -325,6 +324,11 @@ public class ListFormWizardAppService( if (input.SeedData && !string.IsNullOrWhiteSpace(previous?.ListForm?.SeedFilePath)) listFormDto.SeedFilePath = previous.ListForm.SeedFilePath; + if (input.ScreenOverlay != null) + (listFormDto, fieldDtos) = await ApplyScreenOverlayAsync(input, listFormDto, fieldDtos, isDeleted, isCreated, rows); + + ValidateImageColumnLengths(input, tableColumns); + foreach (var item in input.Groups.SelectMany(g => g.Items)) await WriteLanguageRowsAsync(rows, item.CaptionName, item.EnglishCaption, item.TurkishCaption, owned, inserted); } @@ -344,6 +348,48 @@ public class ListFormWizardAppService( } } + /// + /// Deploy girdisindeki ListForm/Fields bolumlerini uretilen ekrana uygular + /// () ve wizard cevaplarini son sozlesmeyle esitler; sonraki + /// adimlar (dil satirlari, widget ve gorsel kolonu dogrulamasi) katmanin getirdigi degerleri gorur. + /// + private async Task<(GridOptionsEditDto ListForm, List Fields)> ApplyScreenOverlayAsync( + ListFormWizardDto input, + GridOptionsEditDto listForm, + List fields, + bool isDeleted, + bool isCreated, + WizardSeedRowStore rows) + { + var overlay = input.ScreenOverlay; + var errors = new List(); + + try + { + listForm = overlay.Apply(listForm, overlay.Baseline.BuildListForm(isDeleted, isCreated), errors); + fields = overlay.Apply(fields, overlay.Baseline.BuildFields(), errors); + } + catch (JsonException ex) + { + errors.Add(ex.Message); + } + + if (errors.Count > 0) + throw new UserFriendlyException($"ListForm/Fields bölümü uygulanamadı: {string.Join(" ", errors)}"); + + foreach (var field in fields) + field.CaptionName = await ResolveLanguageKeyCasingAsync(rows, field.CaptionName); + + // Menusuz ekranin widget'i cizilmez (Create); katman bu kurali asamaz. + if (!input.CreateMenu) + listForm.WidgetsJson = null; + + input.RefreshFrom(listForm, fields); + await ValidateWidgetLanguageKeysAsync(input, rows); + + return (listForm, fields); + } + /// /// Bekleyen kurulum: onceki surumun sahip oldugu kayitlar ve kurdugu ekran migrate'te yenilenmek /// uzere eklenir. Migrate edilmeden ust uste yapilan duzenlemeler ayni listeyi buyutur; ilk diff --git a/api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs b/api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs index 82b5d1da..62e852b0 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs @@ -1,5 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.Configuration; namespace Sozsoft.Platform.Data.Seeds; @@ -18,6 +22,72 @@ public class CrudEndpointSeedFileDto public DateTime GeneratedAt { get; set; } public List Endpoints { get; set; } = []; + + private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true }; + + /// + /// Bir entity'nin standart endpoint kumesi: tabloda bes operasyon, view/procedure/function'da + /// yalnizca okuma. verilmisse yalnizca listedekiler aktif baslar, + /// digerleri pasif yazilir. SQL Query Manager diyalogu ve MCP ayni kumeyi bu metottan uretir. + /// + public static CrudEndpointSeedFileDto Generate(string entityName, string operationTypes, string objectType, DateTime generatedAt) + { + var active = operationTypes? + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var readOnly = !string.IsNullOrWhiteSpace(objectType) && + !string.Equals(objectType.Trim(), "table", StringComparison.OrdinalIgnoreCase); + + (string Method, string Suffix, string Operation)[] operations = + [ + ("GET", string.Empty, "GetList"), + ("GET", "/{id}", "GetById"), + ("POST", string.Empty, "Create"), + ("PUT", "/{id}", "Update"), + ("DELETE", "/{id}", "Delete"), + ]; + + return new CrudEndpointSeedFileDto + { + EntityName = entityName, + GeneratedAt = generatedAt, + Endpoints = [.. operations + .Where(o => !readOnly || o.Operation is "GetList" or "GetById") + .Select(o => new CrudEndpointSeedItemDto + { + Method = o.Method, + Path = $"/api/app/crudendpoint/{entityName}{o.Suffix}", + OperationType = o.Operation, + IsActive = active == null || active.Count == 0 || active.Contains(o.Operation) + })] + }; + } + + /// + /// Entity adindan guvenli dosya adi; yol iceren (/, \, ..) ad reddedilir (null). + /// + public static string SafeFileName(string entityName) + { + var trimmed = entityName?.Trim(); + if (string.IsNullOrWhiteSpace(trimmed) || trimmed.Contains('/') || trimmed.Contains('\\') || trimmed.Contains("..")) + return null; + + var safe = string.Concat(trimmed.Split(Path.GetInvalidFileNameChars())); + return string.IsNullOrWhiteSpace(safe) ? null : safe; + } + + /// Dosyayi crud/{EntityName}.json olarak yazar ve tam yolunu dondurur. + public string Write(IConfiguration configuration) + { + var folder = SeedPathResolver.GetPath(configuration, SeedPathResolver.CrudFolder); + Directory.CreateDirectory(folder); + + var filePath = Path.Combine(folder, $"{SafeFileName(EntityName)}.json"); + File.WriteAllText(filePath, JsonSerializer.Serialize(this, WriteOptions)); + + return filePath; + } } /// diff --git a/claude.md b/claude.md index f85a9554..183e4a98 100644 --- a/claude.md +++ b/claude.md @@ -333,7 +333,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → `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. `Timesheets`'i tamamen sunucu yazar (`IMcpDevRequestTimesheetRecorder`): `get_dev_request`/`start_dev_request` talebi etkin talep yapar, - sonraki her üretim/veri yazma işlemi (`create_table`, `deploy_wizard`, `delete_wizard`, + sonraki her üretim/veri yazma işlemi (`create_table`, `save_sql_object`, `generate_crud_endpoints`, + `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `save_report_category`, `save_report_template`, `delete_report_template`, satır araçları, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks`) ona `App.Mcp.Timesheet.*` anahtarından Türkçe @@ -362,6 +363,17 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → istemcisi kararı tek yerdedir (`McpConsts.IsMcpClient`): `resource` parametresi yok sayılır ve token kullanıcı oturumuna bağlanmaz; token ucuna yeni bir oturum yolu eklenirse bu istisna korunur, yoksa connector web oturumunu düşürür. +- **MCP yalnızca seed dosyası yazar** → Wizard gibi: geliştirme araçları (`create_table`, + `save_sql_object`, `generate_crud_endpoints`, `deploy_wizard`) yalnızca `configs/seeds/` altına dosya + yazar, veritabanına DB Migrate yansıtır. Yeni bir geliştirme aracı veritabanına yazan AppService'i + çağırmaz, seed dosyasını üreten ortak metodu kullanır (ör. `CrudEndpointSeedFileDto.Generate/Write`; + diyaloğun servisi de aynı metottan geçer). Hata kodu yalnızca migrate'i bozacak girdi içindir; + düzeltilebilen girdi (bilinmeyen seçenek, boş/tekrar eden index) sessizce normalize edilir. + Kolon adları tahmin edilmez: şema araçları geliştiriciye migrate bekleyen wizard ekranlarını ve + seed-only tabloları da döner (`McpSchemaAppService.GetSeedOnlySourcesAsync`). `deploy_wizard` + girdisindeki `ListForm`/`Fields` bölümleri ekrana katman olarak uygulanır (`WizardSeedFileDto.FromDeployJson` + + `WizardScreenOverlay`); `ListFormWizardDto`'ya editörde karşılığı olan alan eklenirse katman onu + kendiliğinden taşır. - **MCP yetki vermez** → yeni bir MCP aracı kullanıcının uygulamadaki yetkisini aşamaz: liste verisi ekranın sorgusundan (`IListFormSelectAppService`/`IListFormDataAppService`) geçer; liste yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki diff --git a/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json b/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json index d1f51f79..7e9770ce 100644 --- a/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json +++ b/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json @@ -17,7 +17,7 @@ "Assignees": "system@sozsoft.com", "Priority": null, "Completed": false, - "SortOrder": 0, + "SortOrder": 1, "Timesheets": null, "SubTasks": null, "Analysis": null diff --git a/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json b/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json index 6d1b4980..8671c121 100644 --- a/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json +++ b/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json @@ -9,7 +9,7 @@ "TenantId": null, "UserName": "system@sozsoft.com", "RequestDate": null, - "Status": "Backlog", + "Status": "Ready to Start", "Title": "Inventory -> Tanımlamalar", "Description": "

Ürünler : Ürün listesi ve detayı aşağıdaki örneklendirilmiştir. Ürünler Satış, Masraflar, Satınalma olarak 3 seçeneği var. Satış seçerse sadece Satış siparişlerinde görünür. Satınalma seçerse Satınalınabilir anlamına gelir. Ürün Türü Mal veya Hizmet olabilir. Envanter Takibi yapılıp yapılmayacağı seçilebilir. Eğer takip edilecekse Seri No, Lot Numarası veya Miktara göre seklinden seçilecek.


Nitelikler ve Varyantlar seçeneğinde yazılabilecek değerler Nitelik olarak tanımlanmalı. İçerisindeki değerleri de tanımlanabilir. Nitelik seçilip değerleri birden fazla şekilde yapılabilir. Eğer nitelikli bir ürün tanımlanırsa o varyantlı ürün listeler de seçilecektir.


Satınalma kısmında o ürünü hangi tedarikçiden hangi bedele aldığımızı belirtiyoruz. Ürün ve reçetede kullanılan Birim sütunun Adm_T_Uom tablosu ile ilişkili olmalı. Adm_T_Uom tablosundaki Id bilgisi nvarchar(64)



Ürün listesinde ürün adı, resmi, Satış, Masraflar ve Satınalma sütunlarından oluşmalı. Ayrıca yukarıdaki Ürün lisesindeki sütunlar olabilir. Diğer bilgiler Ürün listesindeki Detay ksımında Tablar şeklinde resimdeki gibi görüntülenmeli. App.Definitions.Contact gibi olabilir.



Nitelikler kısmı Nitelik kısmında seçilmeli ve Değerler birden fazla seçebilecek bir sütundan oluşmalı.




Reçeteler : Ürünlerin hammadde ve yarı mamülerin ve tüketim miktarlarının belirlendiği kısımdır. Ayrıca tanımlanan operasyonların tanımlandığı listedir. Hangi bileşenlerin hangi operasyon ile tüketildiği belirlenecektir. Ürün bazında da reçete yazılabilir. Her bir ürünün Varyantı bazında da reçete yazılabilri. Bom türü eklenmeyecek


İş Merkezi tanımlamaları : Inventory -> Tanımlamalar menüsünde yer alacaktır.İş Merkezi lsitesinde iş merkezi adı, etiketler, çalışma saatleri (Weekly working time limit (h) ile setting kısmından alınabilir.)İş Merkezi lsitesinde iş merkezi adı, etiketler, çalışma saatleri (Weekly working time limit (h) ile setting kısmından alınabilir.) İş merkezi lsitesinin detayında Genel Bİlgi, Ürün kapasitesi bilgilerini tab olarak göstermeli


Operasyonlar : Inventory -> Tanımlamalar menüsünde yer alacaktır.Operasyon Listesinde Operasyon Adı, İş Merkezi Adı, Varsayılan Süre sütunlarından oluşmalı. Detay butonu ile Açıklamalar listesinde yer almalı



Eğer nitelikli bir ürün tanımlanırsa o zaman Varyantlı liste oluşur ve tüm sistem varyantlı seçeneker çalışır. Eğer nitelik tanımlamazsa ana ürün seçilecektir.

Ürün reçetesine genel bakış raporu bu ERP ssitemlerdeki BOM ile aynı. Reçetelerin malzemelerinde recursive şekilde iç içe kullanılan diğer malzemeler ve miktarların hesaplanması gerekiyor. Bu ksıım Tree Layout olarak sunulmalı.




", "DueDate": "2026-10-06T21:02:00", @@ -17,10 +17,10 @@ "Assignees": "system@sozsoft.com", "Priority": "5", "Completed": false, - "SortOrder": 2, - "Timesheets": null, + "SortOrder": 0, + "Timesheets": "[{\"id\":\"09379560-7e9f-fa43-dfd3-3a2429dc2afe\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.0167}]", "SubTasks": null, - "Analysis": "" + "Analysis": "

1. Amaç

\n

Envanter ve üretim modülünün ana verisini kurmak: ürün kartı (satış/masraf/satınalma bayrakları, tür, envanter takibi, nitelik–varyant, tedarikçi fiyatları), ölçü birimi bağlı reçeteler (bileşen + operasyon, ürün ya da varyant bazında), iş merkezleri, operasyonlar ve çok seviyeli reçete patlatma raporu. Kullanıcı: stok, satınalma ve üretim planlama ekipleri.

\n\n

2. Görsel analizi

\n
    \n
  • [image-1] Ürün listesi (grid): favori yıldız, Ürün Adı, İç Referans, Satış Fiyatı, Maliyet, Stokta, Öngörülen, Birim. Arama + filtre çubuğu.
  • \n
  • [image-2] Nitelik formu: Nitelik Adı, Görünüm Türü (Radio/Haplar/Seçme/Renk/Çoklu onay/Görsel), Varyant Oluşturma (Hemen/Dinamik/Asla); alt sekme \"Nitelik Değerleri\" liste: Değer, Serbest metin (bit), Varsayılan Ekstra Fiyat. Demo: renk → kırmızı, mavi.
  • \n
  • [image-3] Ürün formu – Genel Bilgi: başlıkta Satış / Masraflar / Satınalma onay kutuları + resim; Ürün Türü (Mallar/Hizmet/Kombo), Envanter Takibi (bit) + takip yöntemi (Seri No'ya göre / Lotlara göre / Miktara göre), Satış Fiyatı + birim, Satış Vergileri, Maliyet, Satınalma Vergileri, Kategori, Referans, Barkod, GTİP, Firma; İç Notlar.
  • \n
  • [image-4] Nitelik & Varyantlar sekmesi: satır listesi Nitelik (seçim) + Değerler (çoklu).
  • \n
  • [image-5] Satınalma sekmesi: Tedarikçi, Miktar, Birim, Birim Fiyat, Kur, Teslim Süresi; altta Kontrol Kuralı (Sipariş edilen / Teslim alınan miktarlar), Satınalma Açıklaması.
  • \n
  • [image-6] Stok sekmesi: Sorumlu, Ağırlık (kg), Hacim (m³), GTİP Kodu, Malların Menşei; Lot/Seri bazında değerleme, Özel Lot/Seri; Alım / Teslimat / İç transfer açıklamaları.
  • \n
  • [image-7] Muhasebe sekmesi: Gelir Hesabı, Gider Hesabı.
  • \n
  • [image-8..10] Reçete formu: Ürün, Ürün Varyantı, Miktar + birim, Referans, (BoM Türü – talep gereği eklenmez), Firma. Sekmeler: Bileşenler (Komponent, Miktar, Birim, Operasyonda Tüketilenler), Operasyonlar (Operasyon, İş Merkezi, Süre dk), Diğer (Üretim Hazırlığı, Esnek Tüketim, Üretim Akış Zamanı, Sipariş Hazırlama Günleri, Parti Büyüklüğü).
  • \n
  • [image-11] İş merkezi formu: İş Merkezi Adı, Etiket, Alternatif İş Merkezleri, Kod, Çalışma Saatleri; sekmeler Genel Bilgi (Zaman Verimliliği %, OEE Hedefi %, Kurulum/Temizlik Zamanı dk, Saat Başına Maliyet iş merkezi/çalışan, Açıklama), Ürün Kapasitesi.
  • \n
  • [image-12] Operasyon formu: Operasyon adı, İş Merkezi, Varyantlara Uygula, Maliyete Dayalı (Gerçek/Teorik), Süre Hesaplama (Sabit/Hesaplanmış), Varsayılan Süre (dk); sekme Açıklamalar (Başlık, Tip, Referans).
  • \n
  • [image-13] Ürün Varyantları listesi: Ürün adı + varyant değerleri (renk/malzeme/boyut etiketleri), Satış Fiyatı, Maliyet, Birim. 12 satır (2×2×3 kartezyen).
  • \n
  • [image-14] Reçeteye Genel Bakış: ürün → bileşenler (girintili), Operasyonlar grubu; Miktar, Birim, Maliyet kolonları.
  • \n
\n\n

3. Çözüm yolu

\n

Tüm tanım ve kart ekranları Wizard List + alt formlar (Contact kartındaki gibi; tek kaydı düzenleyen sekmeler aynı tabloya bağlı Form alt formu). Varyant listesi ve reçete patlatma raporu Query kaynaklı Wizard List (SelectCommandType: 4): varyantlar nitelik satırlarından kartezyen üretilir, rapor özyinelemeli CTE ile çok seviyeli patlatılır ve Tree görünümünde sunulur. Kalıcı varyant tablosu ve SP gerekmez; tüm adımlar create_table + deploy_wizard ile yapılır.

\n\n

4. Menü yeri

\n

Envanter › Tanımlamalar › {Ekran}

\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SeviyeKodAd (TR / EN)İkonDurum
KökApp.InventoryEnvanter / InventoryFcPackageYeni
AraApp.Inventory.DefinitionsTanımlamalar / DefinitionsFcSettingsYeni
EkranApp.Inventory.ProductCategoryÜrün Kategorileri / Product CategoriesFcFolderYeni
EkranApp.Inventory.AttributeNitelikler / AttributesFcListYeni
EkranApp.Inventory.WorkCenterTagİş Merkezi Etiketleri / Work Center TagsFcBookmarkYeni
EkranApp.Inventory.WorkCenterİş Merkezleri / Work CentersFcFactoryYeni
EkranApp.Inventory.OperationOperasyonlar / OperationsFcProcessYeni
EkranApp.Inventory.ProductÜrünler / ProductsFcShopYeni
EkranApp.Inventory.ProductVariantÜrün Varyantları / Product VariantsFcGridYeni
EkranApp.Inventory.BomReçeteler / Bills of MaterialsFcDocumentYeni
EkranApp.Inventory.BomOverviewReçeteye Genel Bakış / BoM OverviewFcTreeStructureYeni
\n

ModuleId: Inventory · İzin grubu: App.Inventory (Yeni) · Route: /admin/list/{ListFormCode}. Kök veya Tanımlamalar dalı menü seed'inde aynı adla varsa deploy var olana bağlar.

\n

Yetki: App.Inventory › App.Inventory.Definitions (Yeni) › {ListFormCode}; alt formlar kendi listelerinin altında (ör. … › App.Inventory.Product › App.Inventory.ProductSupplier).

\n\n

5. Veri modeli

\n

Tümü kiracı (TenantId) + audit + soft delete (IsDeleted) kolonlu, anahtar Id uniqueidentifier. Birim kolonları UomId nvarchar(64) → Adm_T_Uom.Id; tedarikçi Adm_T_Contact; para birimi Sas_H_Currency.

\n

Inv_T_ProductCategory — Tanım tablosu: Evet (görselde Kategori seçimi). Name nvarchar(128) zorunlu, ParentId uniqueidentifier null (→ kendisi), IsActive bit NOT NULL DEFAULT 1.

\n

Inv_T_Attribute — Tanım tablosu: Evet (talep: \"Nitelik olarak tanımlanmalı\"). Name nvarchar(128) zorunlu, DisplayType nvarchar(16) zorunlu (Radio/Pills/Select/Color/Multi/Image, varsayılan Radio), VariantCreation nvarchar(16) zorunlu (Always/Dynamic/Never, varsayılan Always), IsActive bit.

\n

Inv_T_AttributeValue — Tanım tablosu: Evet. AttributeId uniqueidentifier zorunlu, Name nvarchar(128) zorunlu, IsCustom bit (Serbest metin) DEFAULT 0, ExtraPrice decimal(18,2) DEFAULT 0, ColorCode nvarchar(16) null, SortOrder int, IsActive bit.

\n

Inv_T_WorkCenterTag — Tanım: Evet. Name nvarchar(64) zorunlu, Color nvarchar(16), IsActive bit.

\n

Inv_T_WorkCenter — Tanım: Evet (operasyon ve reçetede lookup kaynağı). Name nvarchar(128) zorunlu, Code nvarchar(32), Tags nvarchar(1000) (dxTagBox, | ayrımlı Inv_T_WorkCenterTag Id), AlternativeWorkCenterIds nvarchar(2000) (dxTagBox), WeeklyWorkingHours decimal(5,2) zorunlu, TimeEfficiency decimal(5,2) DEFAULT 100, OeeTarget decimal(5,2) DEFAULT 90, SetupTimeMin decimal(9,2) DEFAULT 0, CleanupTimeMin decimal(9,2) DEFAULT 0, CostPerHour decimal(18,2) DEFAULT 0, EmployeeCostPerHour decimal(18,2) DEFAULT 0, Description nvarchar(MAX), IsActive bit.

\n

Inv_T_WorkCenterCapacity — Tanım: Hayır. WorkCenterId zorunlu, ProductId uniqueidentifier zorunlu, UomId nvarchar(64), Capacity decimal(18,4) zorunlu, SetupTimeMin decimal(9,2), CleanupTimeMin decimal(9,2).

\n

Inv_T_Operation — Tanım: Evet (reçete operasyonlarının kaynağı). Name nvarchar(128) zorunlu, WorkCenterId zorunlu, ApplyOnVariants nvarchar(4000) (dxTagBox, AttributeValue Id), CostBasis nvarchar(16) (Actual/Theoretical, varsayılan Actual), DurationMode nvarchar(16) (Fixed/Computed, varsayılan Fixed), DefaultDurationMin decimal(9,2) DEFAULT 60, IsActive bit.

\n

Inv_T_OperationInstruction — Tanım: Hayır. OperationId zorunlu, Title nvarchar(256) zorunlu, InstructionType nvarchar(16) (Text/Pdf/Image/Link), Reference nvarchar(1000), Note nvarchar(MAX), SortOrder int.

\n

Inv_T_Product — Tanım: Evet (tüm modüllerin ürün kaynağı). Name nvarchar(256) zorunlu, Image nvarchar(MAX) (dxImageUpload), IsFavorite bit DEFAULT 0, CanBeSold bit DEFAULT 1, CanBeExpensed bit DEFAULT 0, CanBePurchased bit DEFAULT 1, ProductType nvarchar(16) zorunlu (Goods/Service, varsayılan Goods), TrackInventory bit DEFAULT 0, TrackingMethod nvarchar(16) null (Serial/Lot/Quantity), InternalReference nvarchar(64), Barcode nvarchar(64), GtipCode nvarchar(32), CategoryId uniqueidentifier, UomId nvarchar(64) zorunlu, PurchaseUomId nvarchar(64), SalesPrice decimal(18,4) DEFAULT 0, Cost decimal(18,4) DEFAULT 0, SalesTaxRate decimal(5,2) DEFAULT 20, PurchaseTaxRate decimal(5,2) DEFAULT 20, InternalNotes nvarchar(MAX), ResponsibleUserId uniqueidentifier (→ AbpUsers), Weight decimal(18,4), Volume decimal(18,4), OriginCountryId (→ Sas_H_Country), LotValuation bit DEFAULT 0, CustomLotSerial nvarchar(64), ReceiptNote / DeliveryNote / InternalTransferNote nvarchar(MAX), PurchaseControlPolicy nvarchar(16) (Ordered/Received, varsayılan Received), PurchaseDescription nvarchar(MAX), IncomeAccount nvarchar(64), ExpenseAccount nvarchar(64), IsActive bit.

\n

Inv_T_ProductAttributeLine — Tanım: Hayır. ProductId zorunlu, AttributeId zorunlu, ValueIds nvarchar(4000) zorunlu (dxTagBox, | ayrımlı AttributeValue Id; seçili niteliğe göre süzülür). Benzersizlik: (ProductId, AttributeId).

\n

Inv_T_ProductSupplier — Tanım: Hayır. ProductId zorunlu, ContactId zorunlu (→ Adm_T_Contact), MinQuantity decimal(18,4) DEFAULT 1, UomId nvarchar(64), Price decimal(18,4) zorunlu, CurrencyId (→ Sas_H_Currency), LeadTimeDays int DEFAULT 0, VariantKey nvarchar(450) null.

\n

Inv_T_Bom — Tanım: Evet (bileşen olarak alt reçete aranır). ProductId zorunlu, VariantKey nvarchar(450) null (boşsa tüm varyantlar), Quantity decimal(18,4) DEFAULT 1 zorunlu, UomId nvarchar(64) zorunlu, Reference nvarchar(64), ReadyToProduce nvarchar(16) (AllAvailable/FirstOperation), FlexibleConsumption nvarchar(16) (Allowed/Warning/Blocked, varsayılan Warning), ProductionLeadDays int DEFAULT 0, PreparationDays int DEFAULT 0, BatchSize decimal(18,4) null, IsActive bit. BoM Türü kolonu açılmaz (talep).

\n

Inv_T_BomComponent — Tanım: Hayır. BomId zorunlu, ProductId zorunlu, VariantKey nvarchar(450) null, Quantity decimal(18,4) zorunlu, UomId nvarchar(64) zorunlu, BomOperationId uniqueidentifier null (Operasyonda Tüketilenler), SortOrder int.

\n

Inv_T_BomOperation — Tanım: Hayır. BomId zorunlu, OperationId zorunlu, WorkCenterId zorunlu (operasyondan dolar), DurationMin decimal(9,2) zorunlu, SortOrder int.

\n

Varyant anahtarı: varyant kalıcı tabloda tutulmaz. VariantKey = ürünün nitelik satırlarındaki seçili değer Id'lerinin sıralı | birleşimi; aynı kombinasyon her zaman aynı anahtarı üretir. Query kaynakları (T‑SQL):

\n
    \n
  • App.Inventory.ProductVariant (TableName \"Q\", KeyFieldName Key = ProductId + ':' + VariantKey, nvarchar): nitelik satırlarını STRING_SPLIT(ValueIds,'|') ile açıp özyinelemeli CTE ile kartezyen üretir; kolonlar ProductId, ProductName, VariantKey, VariantName (değer adları virgülle), SalesPrice + Σ ExtraPrice, Cost, UomId, InternalReference. Nitelik satırı olmayan ürün tek satır (VariantKey boş). Ana tablo Inv_T_Product IsDeleted seçilir.
  • \n
  • App.Inventory.BomOverview (KeyFieldName NodeKey nvarchar(900)): aktif Inv_T_Bom reçetelerinden başlayan özyinelemeli CTE; her bileşen için o ürün(+varyant, yoksa ürün geneli) reçetesi varsa alt seviyeye iner. Miktar = üst miktar × bileşen miktarı ÷ reçete miktarı; Maliyet = miktar × Product.Cost; operasyonlar her seviyede \"Operasyonlar\" düğümü altında DurationMin × (miktar ÷ reçete miktarı) ve WorkCenter.CostPerHour ile. Döngüye karşı OPTION (MAXRECURSION 20) ve yol (Path) içinde aynı BomId tekrarı engellenir. Kolonlar NodeKey, ParentKey, Level, NodeType (Product/Component/OperationGroup/Operation), Name, Quantity, UomName, Cost.
  • \n
\n\n

6. Alt form ağacı

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SekmeHedef ListFormCodeTabTypeMenüİlişkiEkle / Düzenle / SilYetki yolu
Nitelik DeğerleriApp.Inventory.AttributeValueListMenüsüzId → AttributeId✓ / ✓ / ✓… › Attribute › AttributeValue
İş Merkezi › Genel BilgiApp.Inventory.WorkCenterGeneral (Inv_T_WorkCenter)FormMenüsüzId → Id✓ / ✓ / –… › WorkCenter › WorkCenterGeneral
İş Merkezi › Ürün KapasitesiApp.Inventory.WorkCenterCapacityListMenüsüzId → WorkCenterId✓ / ✓ / ✓… › WorkCenter › WorkCenterCapacity
Operasyon › AçıklamalarApp.Inventory.OperationInstructionListMenüsüzId → OperationId✓ / ✓ / ✓… › Operation › OperationInstruction
Ürün › Genel BilgiApp.Inventory.ProductGeneral (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductGeneral
Ürün › Nitelik & VaryantlarApp.Inventory.ProductAttributeLineListMenüsüzId → ProductId✓ / ✓ / ✓… › Product › ProductAttributeLine
Ürün › VaryantlarApp.Inventory.ProductVariantListMenülüId → ProductId– / – / –kendi dalı
Ürün › SatınalmaApp.Inventory.ProductSupplierListMenüsüzId → ProductId✓ / ✓ / ✓… › Product › ProductSupplier
Ürün › StokApp.Inventory.ProductStock (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductStock
Ürün › MuhasebeApp.Inventory.ProductAccounting (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductAccounting
Ürün › ReçetelerApp.Inventory.BomListMenülüId → ProductId✓ / ✓ / ✓kendi dalı
Reçete › BileşenlerApp.Inventory.BomComponentListMenüsüzId → BomId✓ / ✓ / ✓… › Bom › BomComponent
Reçete › OperasyonlarApp.Inventory.BomOperationListMenüsüzId → BomId✓ / ✓ / ✓… › Bom › BomOperation
Reçete › DiğerApp.Inventory.BomOther (Inv_T_Bom)FormMenüsüzId → Id✓ / ✓ / –… › Bom › BomOther
Reçete › Genel BakışApp.Inventory.BomOverviewListMenülüId → BomId– / – / –kendi dalı
\n\n

7. Etkilenen kısımlar

\n
    \n
  • Yeni kök menü Envanter, ara menü Tanımlamalar, izin grubu App.Inventory, 9 menülü ekran + 12 menüsüz alt form; her listede C/R/U/D/Export/Import/Note yetkileri (Query ekranlarında yalnız R/Export).
  • \n
  • Dil anahtarları (TR/EN) her ekran, alan ve statik seçenek için.
  • \n
  • Modül dokümanı docs/modules/App.Inventory.md deploy ile açılır.
  • \n
  • Var olan ekranlara dokunulmaz; Adm_T_Uom, Adm_T_Contact, Sas_H_Currency, Sas_H_Country yalnız lookup kaynağıdır. Sipariş ekranları ürünü CanBePurchased/CanBeSold ile süzecek; bu talepte sipariş ekranı yoktur.
  • \n
\n\n

8. Görünümler ve davranış

\n
    \n
  • Grid/Card/Pivot/Chart açık (standart). BomOverview varsayılan Tree (KeyExpr NodeKey, ParentIdExpr ParentKey, AutoExpandAll true), Grid de açık.
  • \n
  • Ürün listesi kolonları: Image, Name, CanBeSold, CanBeExpensed, CanBePurchased, InternalReference, SalesPrice, Cost, UomId, CategoryId, ProductType. Stokta/Öngörülen kolonları stok hareket tablosu olmadığından açılmaz.
  • \n
  • Ürün ana formu: Name, Image (dxImageUpload), Satış/Masraflar/Satınalma onay kutuları, ProductType (dxRadioGroup), TrackInventory, TrackingMethod (dxRadioGroup), UomId, SalesPrice, Cost, CategoryId. Diğer alanlar sekmelerde.
  • \n
  • EditorScript: TrackInventory = false iken TrackingMethod gizli ve null; ProductType = Service iken TrackInventory false ve kilitli. Varsayılan TrackingMethod = Quantity.
  • \n
  • ProductAttributeLine.ValueIds lookup'ı seçili AttributeId'ye göre cascade süzülür (dxTagBox).
  • \n
  • Bom.VariantKey ve BomComponent.VariantKey: lookup App.Inventory.ProductVariant sorgusundan, seçili ProductId'ye göre cascade; ürünün nitelik satırı yoksa boş kalır (ana ürün). BomComponent.BomOperationId aynı reçetenin operasyonlarıyla süzülür. BomOperation.OperationId seçilince WorkCenterId ve DurationMin operasyondan dolar.
  • \n
  • Product.UomId seçilince PurchaseUomId boşsa aynı değeri alır; BomComponent.UomId ürünün UomId'sinden dolar.
  • \n
  • WorkCenter.WeeklyWorkingHours varsayılanı (FormFieldsDefaultValueJson, DbQuery): SELECT COALESCE(MAX(TRY_CAST(Value AS decimal(5,2))),40) FROM AbpSettings WHERE Name LIKE '%WeeklyWorkingTimeLimit%'.
  • \n
  • Doğrulamalar: Quantity/Capacity/Price > 0; OEE ve verimlilik 0–100; TrackInventory = true iken TrackingMethod zorunlu.
  • \n
  • Widget'lar yalnız ana ekranlarda: Ürünler (toplam, satılabilir, satınalınabilir, hizmet), Reçeteler (toplam, varyant bazlı), İş Merkezleri (toplam, aktif). Alt formlarda widget yok.
  • \n
\n\n

9. Kararlar

\n
    \n
  • Ara menü kodu App.Inventory.Definitions (Satınalmadaki App.Prc.Definitions kalıbı).
  • \n
  • Ürün Kategorileri ve İş Merkezi Etiketleri tanım ekranları eklendi — görsellerdeki Kategori ve Etiket alanlarının lookup kaynağı.
  • \n
  • Ürün Türü yalnız Mal / Hizmet (talep); Kombo açılmaz.
  • \n
  • Vergiler vergi tablosu olmadığından oran (decimal, varsayılan %20) olarak tutulur (onay).
  • \n
  • Muhasebe hesapları hesap planı tablosu olmadığından serbest metin kod (nvarchar(64)) (onay).
  • \n
  • Varyantlar kalıcı tabloya yazılmaz; deterministik VariantKey ile Query ekranından türetilir. Silinen kombinasyonu kullanan reçete satırı reçete listesinde kırmızı biçimlendirilir (ColumnStyling) (onay).
  • \n
  • \"Varyant Oluşturma = Asla\" olan nitelik kartezyene katılmaz; Dinamik ve Hemen aynı davranır (onay).
  • \n
  • Çalışma saati ayardan varsayılan olarak okunur, kayıt bazında değiştirilebilir; ayar yoksa 40 (onay).
  • \n
  • Reçete patlatmada alt reçete seçimi: önce bileşenin VariantKey'ine uyan, yoksa VariantKey boş reçete; birden fazla aktif reçete varsa en eski oluşturulan.
  • \n
  • Demo veri görsellerden: nitelikler renk (kırmızı, mavi), malzeme (çelik, tahta), boyut (40x40, 80x100, 10x30); ürünler \"(40X40mm) Mamül Sepeti Üst Geçme Kepi\", \"(K.M.G) AUTONİCS PR12-4DN Sensör\", \"(K.M.G) 2A Cam Sigorta (5*20)\"; iş merkezi \"ram makinası\"; operasyon \"Apre\" (60 dk); reçete 1 adet kep → 20 adet sensör.
  • \n
\n\n

10. Üretim planı

\n
    \n
  1. create_table: Inv_T_ProductCategory
  2. \n
  3. create_table: Inv_T_Attribute
  4. \n
  5. create_table: Inv_T_AttributeValue
  6. \n
  7. create_table: Inv_T_WorkCenterTag
  8. \n
  9. create_table: Inv_T_WorkCenter
  10. \n
  11. create_table: Inv_T_WorkCenterCapacity
  12. \n
  13. create_table: Inv_T_Operation
  14. \n
  15. create_table: Inv_T_OperationInstruction
  16. \n
  17. create_table: Inv_T_Product
  18. \n
  19. create_table: Inv_T_ProductAttributeLine
  20. \n
  21. create_table: Inv_T_ProductSupplier
  22. \n
  23. create_table: Inv_T_Bom
  24. \n
  25. create_table: Inv_T_BomComponent
  26. \n
  27. create_table: Inv_T_BomOperation
  28. \n
  29. deploy_wizard: ProductCategory.json
  30. \n
  31. deploy_wizard: AttributeValue.json
  32. \n
  33. deploy_wizard: Attribute.json
  34. \n
  35. deploy_wizard: WorkCenterTag.json
  36. \n
  37. deploy_wizard: WorkCenterGeneral.json
  38. \n
  39. deploy_wizard: WorkCenterCapacity.json
  40. \n
  41. deploy_wizard: WorkCenter.json
  42. \n
  43. deploy_wizard: OperationInstruction.json
  44. \n
  45. deploy_wizard: Operation.json
  46. \n
  47. deploy_wizard: ProductVariant.json
  48. \n
  49. deploy_wizard: BomComponent.json
  50. \n
  51. deploy_wizard: BomOperation.json
  52. \n
  53. deploy_wizard: BomOther.json
  54. \n
  55. deploy_wizard: BomOverview.json
  56. \n
  57. deploy_wizard: Bom.json
  58. \n
  59. deploy_wizard: ProductGeneral.json
  60. \n
  61. deploy_wizard: ProductAttributeLine.json
  62. \n
  63. deploy_wizard: ProductSupplier.json
  64. \n
  65. deploy_wizard: ProductStock.json
  66. \n
  67. deploy_wizard: ProductAccounting.json
  68. \n
  69. deploy_wizard: Product.json
  70. \n
\n\n

11. Üretime hazır

\n

Evet

" } ] } diff --git a/configs/seeds/data/App.DevRequests/94639b65-1bb7-454a-ae73-3e1c5c9fe23a.json b/configs/seeds/data/App.DevRequests/94639b65-1bb7-454a-ae73-3e1c5c9fe23a.json index 913a55ee..b49fc57f 100644 --- a/configs/seeds/data/App.DevRequests/94639b65-1bb7-454a-ae73-3e1c5c9fe23a.json +++ b/configs/seeds/data/App.DevRequests/94639b65-1bb7-454a-ae73-3e1c5c9fe23a.json @@ -17,7 +17,7 @@ "Assignees": "system@sozsoft.com", "Priority": "5", "Completed": false, - "SortOrder": 1, + "SortOrder": 2, "Timesheets": null, "SubTasks": null, "Analysis": null diff --git a/configs/seeds/data/App.Languages.LanguageText.json b/configs/seeds/data/App.Languages.LanguageText.json index ffdf7b09..0509a514 100644 --- a/configs/seeds/data/App.Languages.LanguageText.json +++ b/configs/seeds/data/App.Languages.LanguageText.json @@ -69556,6 +69556,34 @@ "Key": "App.Mcp.Timesheet.CreateTable", "Value": "{0} tablosu oluşturuldu" }, + { + "Id": "efffc4d3-1034-5a57-930b-ad59c903be76", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveSqlObject", + "Value": "SQL object {0} saved" + }, + { + "Id": "9248dc0c-94a8-59ab-96d1-cac7035857a9", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveSqlObject", + "Value": "{0} SQL nesnesi kaydedildi" + }, + { + "Id": "893dfae1-5727-5e05-bd63-413d121bfc08", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.GenerateCrudEndpoints", + "Value": "CRUD endpoints for {0} generated" + }, + { + "Id": "9fb6c735-88e5-570d-ab8c-ad66b2ce4f8c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.GenerateCrudEndpoints", + "Value": "{0} için CRUD endpoint'leri üretildi" + }, { "Id": "c808ea33-665d-5b85-bca9-10b0b4fc407f", "CultureName": "en", @@ -69934,6 +69962,48 @@ "Key": "App.Mcp.Error.TableAlreadyExists", "Value": "\"{0}\" tablosu zaten var. Var olan tabloları SQL Query Manager tablo tasarımcısından değiştirin." }, + { + "Id": "570cb82e-157e-5473-b173-99db3bc2fbc2", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.TableColumnNotFound", + "Value": "Column \"{0}\" is not in the table. Index and foreign key columns must be among the table's columns." + }, + { + "Id": "66fa738d-2d12-5dd4-97bd-b441cd25cadc", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.TableColumnNotFound", + "Value": "\"{0}\" kolonu tabloda yok. Index ve FK kolonları tablonun kolonları arasında olmalıdır." + }, + { + "Id": "65bd2bec-309a-5b6e-be6f-6dd6cd00bbdc", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ForeignKeyTableNotInstalledFirst", + "Value": "The foreign key on \"{0}\" points to \"{1}\", which DB Migrate would not have created before \"{2}\". Seed scripts run in file name order: the referenced table must be a seed table whose name sorts before \"{2}\", or a platform table that already exists in the database. Rename the table or leave the foreign key out." + }, + { + "Id": "04476134-076d-54bc-9fc6-86d3713f5da6", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ForeignKeyTableNotInstalledFirst", + "Value": "\"{0}\" kolonundaki FK \"{1}\" tablosunu gösteriyor; DB Migrate bu tabloyu \"{2}\" tablosundan önce kurmuş olmaz. Seed scriptleri dosya adı sırasıyla çalışır: hedef tablo adı \"{2}\" adından önce sıralanan bir seed tablosu ya da veritabanında zaten kurulu bir platform tablosu olmalıdır. Tablo adını değiştirin ya da FK'yi yazmayın." + }, + { + "Id": "e17aea0e-0529-585e-8f5f-6a71c88a2483", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.SqlObjectInvalid", + "Value": "The {0} script must define exactly one view, procedure or function (CREATE [OR ALTER|OR REPLACE] VIEW/PROCEDURE/FUNCTION) and the SQL Server and PostgreSQL scripts must define the same object. Both scripts are required." + }, + { + "Id": "79c9dd83-97aa-51e3-9842-d00a2e21744a", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.SqlObjectInvalid", + "Value": "{0} scripti tek bir view, procedure ya da function tanımlamalıdır (CREATE [OR ALTER|OR REPLACE] VIEW/PROCEDURE/FUNCTION); SQL Server ve PostgreSQL scriptleri aynı nesneyi tanımlamalıdır. İki script de zorunludur." + }, { "Id": "d88ec341-a3cd-505f-b14a-2c6ce3f82de9", "CultureName": "en",