mcp ve wizard yetenekleri eşitlendi
This commit is contained in:
parent
3922c46539
commit
ce5d7764d1
25 changed files with 1125 additions and 184 deletions
21
.github/instructions/lowcode.instructions.md
vendored
21
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -68,7 +68,7 @@ kurulur; host veritabanını paylaşan tenant'ta dosyalar yeniden uygulanmaz (`S
|
||||||
|
|
||||||
| # | Adım | Dosya | Neden bu sırada |
|
| # | 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 |
|
| 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 |
|
| 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) |
|
| 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`
|
`McpToolNames`'tedir; okuyanlar `App.DeveloperKit.McpServers.Invoke`, değiştirenler `.Write`
|
||||||
ister ve kiracı kullanıcısına hiçbiri açılmaz.
|
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 |
|
| 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 |
|
| `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` |
|
| `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) |
|
| `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) |
|
| `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 |
|
| `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. |
|
| `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. |
|
| `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ı. |
|
| `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ü**
|
**Menü**
|
||||||
|
|
||||||
|
|
@ -1906,7 +1916,8 @@ deploy'dur). Değiştirmenin yolları:
|
||||||
2. **`EditFileName` ile yeniden deploy** (wizard ekranı ya da MCP `replaceFileName`) — menü,
|
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,
|
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ığı
|
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.
|
3. İlgili kayıtları elle sil, sonra seed'i yeniden çalıştır.
|
||||||
|
|
||||||
> `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow`
|
> `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow`
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -1550,7 +1550,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
|
||||||
| Araç | İş |
|
| 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. |
|
| `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`. |
|
| `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. |
|
| `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. |
|
| `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ı. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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). |
|
| `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). |
|
| `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. |
|
| `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`):
|
**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,
|
`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`,
|
`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_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
|
`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` | 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.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ı)
|
**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) |
|
| `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`) |
|
| `deploy_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Create` (yeni) / `.Update` (`replaceFileName`) |
|
||||||
| `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete`; `dropTables` için ayrıca `App.SqlQueryManager` |
|
| `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` |
|
| `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` |
|
||||||
| `save_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Create` (yeni) / `.Update` |
|
| `save_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Create` (yeni) / `.Update` |
|
||||||
| `delete_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Delete` |
|
| `delete_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Delete` |
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,32 @@ public class McpCreateTableRequestDto
|
||||||
public bool FullAudited { get; set; } = true;
|
public bool FullAudited { get; set; } = true;
|
||||||
|
|
||||||
public bool MultiTenant { get; set; } = true;
|
public bool MultiTenant { get; set; } = true;
|
||||||
|
|
||||||
|
public List<McpTableIndexDto> Indexes { get; set; } = [];
|
||||||
|
|
||||||
|
public List<McpTableForeignKeyDto> ForeignKeys { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Index ya da unique kisit; ad sunucuda <c>IX_/UQ_{Tablo}_{Kolonlar}</c> olarak uretilir.</summary>
|
||||||
|
public class McpTableIndexDto
|
||||||
|
{
|
||||||
|
public List<string> Columns { get; set; } = [];
|
||||||
|
|
||||||
|
public bool Unique { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>FK; ad <c>FK_{Tablo}_{Kolon}</c>.</summary>
|
||||||
|
public class McpTableForeignKeyDto
|
||||||
|
{
|
||||||
|
public string Column { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string ReferencedTable { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Bos ise <c>Id</c>.</summary>
|
||||||
|
public string? ReferencedColumn { get; set; }
|
||||||
|
|
||||||
|
/// <summary><c>NoAction</c> (varsayilan), <c>Cascade</c> ya da <c>SetNull</c>.</summary>
|
||||||
|
public string? OnDelete { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class McpTableColumnDto
|
public class McpTableColumnDto
|
||||||
|
|
@ -99,6 +125,30 @@ public class McpCreateTableResultDto
|
||||||
public string SeedFilePath { get; set; } = string.Empty;
|
public string SeedFilePath { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>View, procedure ya da function scriptinin iki saglayici icin yazildigi seed dosyalari.</summary>
|
||||||
|
public class McpSqlObjectResultDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary><c>VIEW</c>, <c>PROCEDURE</c> ya da <c>FUNCTION</c>.</summary>
|
||||||
|
public string Kind { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Seed kokune gore goreli yollar: <c>sql/object/{Ad}.sql</c>, <c>postgres/object/{Ad}.sql</c>.</summary>
|
||||||
|
public List<string> SeedFilePaths { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Yazilan <c>crud/{Entity}.json</c> dosyasi ve icindeki endpoint'ler.</summary>
|
||||||
|
public class McpCrudEndpointsResultDto
|
||||||
|
{
|
||||||
|
public string EntityName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Seed kokune gore goreli yol.</summary>
|
||||||
|
public string SeedFilePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary><c>{Method} {Path}</c>; pasif baslayanlar isaretlidir.</summary>
|
||||||
|
public List<string> Endpoints { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deploy edilmis bir wizard'in salt-okur dogrulamasi. <see cref="Passed"/> yalnizca
|
/// Deploy edilmis bir wizard'in salt-okur dogrulamasi. <see cref="Passed"/> yalnizca
|
||||||
/// <see cref="McpVerifyCheckDto.Blocking"/> bir kontrol kaldiginda false olur; engellemeyen
|
/// <see cref="McpVerifyCheckDto.Blocking"/> bir kontrol kaldiginda false olur; engellemeyen
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,11 @@ public class McpListSummaryDto
|
||||||
/// <summary>Tablo adi ya da SELECT metni — listenin gercek SQL kaynagi.</summary>
|
/// <summary>Tablo adi ya da SELECT metni — listenin gercek SQL kaynagi.</summary>
|
||||||
public string? SqlSourceName { get; set; }
|
public string? SqlSourceName { get; set; }
|
||||||
|
|
||||||
/// <summary><c>table</c>, <c>selectSql</c> ya da <c>unknown</c>.</summary>
|
/// <summary>
|
||||||
|
/// <c>table</c>, <c>selectSql</c> ya da <c>unknown</c>; gelistiricide ayrica <c>pending</c> (wizard dosyasi
|
||||||
|
/// var, DB Migrate bekliyor) ve <c>pendingTable</c> (yalnizca seed scripti olan tablo). Kolon adlari
|
||||||
|
/// dogrudur; query_list migrate'ten once calismaz.
|
||||||
|
/// </summary>
|
||||||
public string? SourceKind { get; set; }
|
public string? SourceKind { get; set; }
|
||||||
|
|
||||||
public string? KeyFieldName { get; set; }
|
public string? KeyFieldName { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ public interface IMcpDevelopAppService : IApplicationService
|
||||||
|
|
||||||
Task<McpCreateTableResultDto> CreateTableAsync(McpCreateTableRequestDto input);
|
Task<McpCreateTableResultDto> CreateTableAsync(McpCreateTableRequestDto input);
|
||||||
|
|
||||||
|
/// <summary>View/procedure/function scriptini iki saglayicinin seed klasorune yazar; veritabaninda calistirmaz.</summary>
|
||||||
|
Task<McpSqlObjectResultDto> SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript);
|
||||||
|
|
||||||
|
/// <summary>Tablo ya da view icin CRUD endpoint'lerini <c>crud/{Entity}.json</c> seed dosyasina yazar; kaydi DB Migrate ekler.</summary>
|
||||||
|
Task<McpCrudEndpointsResultDto> GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType);
|
||||||
|
|
||||||
Task<List<WizardFileInfoDto>> GetWizardsAsync(string? search);
|
Task<List<WizardFileInfoDto>> GetWizardsAsync(string? search);
|
||||||
|
|
||||||
Task<McpWizardFileDto> GetWizardAsync(string fileName);
|
Task<McpWizardFileDto> GetWizardAsync(string fileName);
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ public class McpDevelopAppService(
|
||||||
IMcpServerConfigProvider serverConfigProvider,
|
IMcpServerConfigProvider serverConfigProvider,
|
||||||
IMcpDevGuide devGuide,
|
IMcpDevGuide devGuide,
|
||||||
IMcpTableCreator tableCreator,
|
IMcpTableCreator tableCreator,
|
||||||
|
IMcpSqlObjectWriter sqlObjectWriter,
|
||||||
IListFormWizardAppService wizardAppService,
|
IListFormWizardAppService wizardAppService,
|
||||||
ICustomComponentAppService componentAppService,
|
ICustomComponentAppService componentAppService,
|
||||||
IListFormSelectAppService listFormSelectAppService,
|
IListFormSelectAppService listFormSelectAppService,
|
||||||
|
|
@ -164,7 +165,9 @@ public class McpDevelopAppService(
|
||||||
[.. (input.Columns ?? []).Select(column => new McpTableColumn(
|
[.. (input.Columns ?? []).Select(column => new McpTableColumn(
|
||||||
column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))],
|
column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))],
|
||||||
input.FullAudited,
|
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);
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName);
|
||||||
|
|
||||||
|
|
@ -176,6 +179,55 @@ public class McpDevelopAppService(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tablo tasarimcisiyla ayni yetkiyi ister: SQL Query Manager'daki view/procedure/function
|
||||||
|
/// kaydinin karsiligidir. Nesneyi DB Migrate kurar.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<McpSqlObjectResultDto> 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]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SQL Query Manager'daki CRUD endpoint diyaloguyla ayni endpoint kumesini
|
||||||
|
/// (<see cref="CrudEndpointSeedFileDto.Generate"/>) yalnizca <c>crud/{Entity}.json</c> seed dosyasina
|
||||||
|
/// yazar; kayitlari DB Migrate ekler (<c>CrudEndpointSeedApplier</c>). Yetki diyalogunkiyle aynidir.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<McpCrudEndpointsResultDto> 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<List<WizardFileInfoDto>> GetWizardsAsync(string? search)
|
public async Task<List<WizardFileInfoDto>> GetWizardsAsync(string? search)
|
||||||
{
|
{
|
||||||
await EnsureToolAllowedAsync(McpToolNames.ListWizards);
|
await EnsureToolAllowedAsync(McpToolNames.ListWizards);
|
||||||
|
|
@ -542,7 +594,6 @@ public class McpDevelopAppService(
|
||||||
throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]);
|
throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var devRequest = await FindDevRequestAsync(request);
|
var devRequest = await FindDevRequestAsync(request);
|
||||||
|
|
||||||
if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis))
|
if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis))
|
||||||
|
|
@ -887,39 +938,21 @@ public class McpDevelopAppService(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wizard'i seed dosyasi biciminden (<c>{"Wizard": {...}}</c>) ya da duz cevaplardan okur.
|
/// Wizard'i seed dosyasi biciminden (<c>{"Wizard": {...}, "ListForm": {...}, "Fields": [...]}</c>)
|
||||||
/// Iki bicim de dosyanin okuma kuralindan gecer; boylece Menu/Language bloklari ve
|
/// ya da duz cevaplardan okur; Menu/Language bloklari ve WizardCode/ModuleId adlari dosyadakiyle
|
||||||
/// WizardCode/ModuleId adlari dosyadakiyle ayni anlama gelir.
|
/// ayni anlama gelir. ListForm/Fields bolumleri ekrana katman olarak uygulanir
|
||||||
|
/// (<see cref="WizardSeedFileDto.FromDeployJson"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private ListFormWizardDto ParseWizard(string? json)
|
private ListFormWizardDto ParseWizard(string? json)
|
||||||
{
|
{
|
||||||
JsonObject? root;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
root = string.IsNullOrWhiteSpace(json) ? null : JsonNode.Parse(json) as JsonObject;
|
return (string.IsNullOrWhiteSpace(json) ? null : WizardSeedFileDto.FromDeployJson(json))
|
||||||
}
|
|
||||||
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
|
|
||||||
?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]);
|
?? 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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Sozsoft.Mcp.Dtos;
|
using Sozsoft.Mcp.Dtos;
|
||||||
using Sozsoft.Mcp.Services;
|
using Sozsoft.Mcp.Services;
|
||||||
using Sozsoft.Platform;
|
using Sozsoft.Platform;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Sozsoft.Platform.ListForms;
|
||||||
using Volo.Abp.Authorization.Permissions;
|
using Volo.Abp.Authorization.Permissions;
|
||||||
|
|
||||||
namespace Sozsoft.Mcp;
|
namespace Sozsoft.Mcp;
|
||||||
|
|
@ -20,9 +26,17 @@ public class McpSchemaAppService(
|
||||||
IMcpServerConfigProvider serverConfigProvider,
|
IMcpServerConfigProvider serverConfigProvider,
|
||||||
IMcpSchemaDictionary dictionary,
|
IMcpSchemaDictionary dictionary,
|
||||||
IMcpListFormAccessFilter accessFilter,
|
IMcpListFormAccessFilter accessFilter,
|
||||||
IPermissionChecker permissionChecker)
|
IPermissionChecker permissionChecker,
|
||||||
|
IListFormWizardAppService wizardAppService,
|
||||||
|
IConfiguration configuration)
|
||||||
: McpAppService(serverConfigProvider), IMcpSchemaAppService
|
: McpAppService(serverConfigProvider), IMcpSchemaAppService
|
||||||
{
|
{
|
||||||
|
/// <summary>Wizard dosyasi olan ama sozlukte olmayan ekran (DB Migrate bekliyor).</summary>
|
||||||
|
private const string PendingSourceKind = "pending";
|
||||||
|
|
||||||
|
/// <summary>Yalnizca seed scripti olan, ekrani olmayan tablo.</summary>
|
||||||
|
private const string PendingTableSourceKind = "pendingTable";
|
||||||
|
|
||||||
public async Task<McpServerConfigDto?> GetServerConfigAsync()
|
public async Task<McpServerConfigDto?> GetServerConfigAsync()
|
||||||
=> await ResolveServerConfigAsync();
|
=> await ResolveServerConfigAsync();
|
||||||
|
|
||||||
|
|
@ -168,7 +182,90 @@ public class McpSchemaAppService(
|
||||||
var forms = await dictionary.GetListFormsAsync();
|
var forms = await dictionary.GetListFormsAsync();
|
||||||
var readable = await accessFilter.GetReadableListFormCodesAsync();
|
var readable = await accessFilter.GetReadableListFormCodesAsync();
|
||||||
|
|
||||||
return [.. forms.Where(form => form.ListFormCode is not null && readable.Contains(form.ListFormCode))];
|
List<McpDictionaryListForm> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seed klasorunde olup sozlukte olmayan kaynaklar: wizard dosyalarinin ekranlari (alanlari
|
||||||
|
/// <c>Fields</c> bolumunden) ve yalnizca <c>sql/object</c> scripti olan tablolar (kolonlari scriptten).
|
||||||
|
/// <see cref="McpListSummaryDto.SourceKind"/> <c>pending</c> / <c>pendingTable</c> olarak isaretlenir:
|
||||||
|
/// kolon adlari dogrudur ama query_list migrate'ten once calismaz.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<McpDictionaryListForm>> GetSeedOnlySourcesAsync(IReadOnlyList<McpDictionaryListForm> known)
|
||||||
|
{
|
||||||
|
var codes = known.Select(form => form.ListFormCode).OfType<string>().ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var tables = known.Select(form => form.TableName).OfType<string>().ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var result = new List<McpDictionaryListForm>();
|
||||||
|
|
||||||
|
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)
|
private static McpListSummaryDto ToSummary(McpDictionaryListForm form)
|
||||||
|
|
|
||||||
|
|
@ -78,5 +78,8 @@ public static class McpErrorCodes
|
||||||
public const string InvalidColumnLength = Prefix + "InvalidColumnLength";
|
public const string InvalidColumnLength = Prefix + "InvalidColumnLength";
|
||||||
public const string InvalidDefaultValue = Prefix + "InvalidDefaultValue";
|
public const string InvalidDefaultValue = Prefix + "InvalidDefaultValue";
|
||||||
public const string DuplicateColumn = Prefix + "DuplicateColumn";
|
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";
|
public const string TableAlreadyExists = Prefix + "TableAlreadyExists";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ public static class McpTimesheetTexts
|
||||||
public const string AppendNote = Prefix + "AppendNote";
|
public const string AppendNote = Prefix + "AppendNote";
|
||||||
public const string AddSubTasks = Prefix + "AddSubTasks";
|
public const string AddSubTasks = Prefix + "AddSubTasks";
|
||||||
public const string CreateTable = Prefix + "CreateTable";
|
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 DeployWizard = Prefix + "DeployWizard";
|
||||||
public const string DeleteWizard = Prefix + "DeleteWizard";
|
public const string DeleteWizard = Prefix + "DeleteWizard";
|
||||||
public const string SaveComponent = Prefix + "SaveComponent";
|
public const string SaveComponent = Prefix + "SaveComponent";
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ public static class McpToolNames
|
||||||
public const string DeleteRow = "delete_row";
|
public const string DeleteRow = "delete_row";
|
||||||
public const string GetDevGuide = "get_dev_guide";
|
public const string GetDevGuide = "get_dev_guide";
|
||||||
public const string CreateTable = "create_table";
|
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 ListWizards = "list_wizards";
|
||||||
public const string GetWizard = "get_wizard";
|
public const string GetWizard = "get_wizard";
|
||||||
public const string DeployWizard = "deploy_wizard";
|
public const string DeployWizard = "deploy_wizard";
|
||||||
|
|
@ -55,6 +57,8 @@ public static class McpToolNames
|
||||||
[
|
[
|
||||||
GetDevGuide,
|
GetDevGuide,
|
||||||
CreateTable,
|
CreateTable,
|
||||||
|
SaveSqlObject,
|
||||||
|
GenerateCrudEndpoints,
|
||||||
ListWizards,
|
ListWizards,
|
||||||
GetWizard,
|
GetWizard,
|
||||||
DeployWizard,
|
DeployWizard,
|
||||||
|
|
@ -119,6 +123,8 @@ public static class McpToolNames
|
||||||
[UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
[UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
[DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
[DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
[CreateTable] = 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,
|
[DeployWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
[DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
[DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
[StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
[StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ekranin veri kaynagi olan view, procedure ve fonksiyon scriptlerini SQL Query Manager'in yazdigi
|
||||||
|
/// yere (<c>sql/object/{Ad}.sql</c>) ve PostgreSQL esine (<c>postgres/object/{Ad}.sql</c>) yazar.
|
||||||
|
/// Govde diyalektler arasinda guvenle cevrilemedigi icin (<see cref="PostgreSqlTableScript"/>) iki
|
||||||
|
/// script de istemciden gelir; biri olmadan yazilmaz.
|
||||||
|
/// <para>
|
||||||
|
/// Veritabaninda calistirilmaz: nesneyi, tablo gibi DB Migrate kurar (<c>SqlDataSeeder</c>). Nesnenin
|
||||||
|
/// adi SQL Server scriptinin basligindan okunur; basliklar tekrar calismaya dayanikli bicime cevrilir
|
||||||
|
/// (<c>CREATE OR ALTER</c> / <c>CREATE OR REPLACE</c>), ayni adla tekrar yazmak scripti gunceller.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpSqlObjectWriter
|
||||||
|
{
|
||||||
|
McpSavedSqlObject Save(string sqlServerScript, string postgreSqlScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class McpSqlObjectWriter(
|
||||||
|
IConfiguration configuration,
|
||||||
|
IStringLocalizer<PlatformResource> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Scripti saglayicinin <c>object</c> klasorune yazar; seed kokune goreli yolu dondurur.</summary>
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Script tek bir nesne tanimlamalidir; basligi tekrar calismaya dayanikli bicime cevrilir.
|
||||||
|
/// Yorumlar yalnizca aramada yok sayilir, dosyada kalir.
|
||||||
|
/// </summary>
|
||||||
|
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+(?<kind>VIEW|PROCEDURE|PROC|FUNCTION)\s+(?<qualified>(?:\[?dbo\]?\s*\.\s*)?\[?(?<name>[A-Za-z_][A-Za-z0-9_]*)\]?)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||||
|
private static partial Regex SqlServerHeaderRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\bCREATE\s+(?:OR\s+REPLACE\s+)?(?<kind>VIEW|PROCEDURE|FUNCTION)\s+(?<qualified>(?:""?public""?\s*\.\s*)?""?(?<name>[A-Za-z_][A-Za-z0-9_]*)""?)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||||
|
private static partial Regex PostgreSqlHeaderRegex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="Kind"><c>VIEW</c>, <c>PROCEDURE</c> ya da <c>FUNCTION</c>.</param>
|
||||||
|
/// <param name="SqlServerPath">Seed kokune gore goreli yol.</param>
|
||||||
|
/// <param name="PostgreSqlPath">Seed kokune gore goreli yol.</param>
|
||||||
|
public sealed record McpSavedSqlObject(string Name, string Kind, string SqlServerPath, string PostgreSqlPath);
|
||||||
|
|
@ -47,10 +47,6 @@ public class McpTableCreator(
|
||||||
@"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
|
@"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
|
||||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Varsayilan deger olarak kabul edilenler: sayi, metin sabiti ve tasarimcinin kendi
|
|
||||||
/// sablonlarinda kullandigi fonksiyonlar. Baska bir ifade script'e hic girmez.
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Istemcinin SQL bicimiyle yazdigi tip: <c>nvarchar(50)</c>, <c>nvarchar (MAX)</c>,
|
/// Istemcinin SQL bicimiyle yazdigi tip: <c>nvarchar(50)</c>, <c>nvarchar (MAX)</c>,
|
||||||
/// <c>decimal(18, 4)</c>. Boyut tipten ayrilir; tip adi yine beyaz listeden gecer.
|
/// <c>decimal(18, 4)</c>. Boyut tipten ayrilir; tip adi yine beyaz listeden gecer.
|
||||||
|
|
@ -62,6 +58,10 @@ public class McpTableCreator(
|
||||||
/// <summary>Tasarimci decimal'i her zaman bu kesinlikle yazar (<c>ToColumnLine</c>).</summary>
|
/// <summary>Tasarimci decimal'i her zaman bu kesinlikle yazar (<c>ToColumnLine</c>).</summary>
|
||||||
private const string DecimalSize = "18,4";
|
private const string DecimalSize = "18,4";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Varsayilan deger olarak kabul edilenler: sayi, metin sabiti ve tasarimcinin kendi
|
||||||
|
/// sablonlarinda kullandigi fonksiyonlar. Baska bir ifade script'e hic girmez.
|
||||||
|
/// </summary>
|
||||||
private static readonly Regex SafeDefaultRegex = new(
|
private static readonly Regex SafeDefaultRegex = new(
|
||||||
@"^(?:-?\d{1,18}(?:\.\d{1,18})?|N?'(?:[^']|'')*'|NEWID\(\)|GETDATE\(\)|GETUTCDATE\(\)|SYSUTCDATETIME\(\))$",
|
@"^(?:-?\d{1,18}(?:\.\d{1,18})?|N?'(?:[^']|'')*'|NEWID\(\)|GETDATE\(\)|GETUTCDATE\(\)|SYSUTCDATETIME\(\))$",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
@ -74,6 +74,16 @@ public class McpTableCreator(
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sablon kolonlari tasarimcinin FULL_AUDIT_COLUMNS / TENANT_COLUMN tanimlariyla aynidir.
|
// Sablon kolonlari tasarimcinin FULL_AUDIT_COLUMNS / TENANT_COLUMN tanimlariyla aynidir.
|
||||||
|
/// <summary>Tasarimcinin FK silme davranislari; anahtar istemcinin yazdigi, deger SQL karsiligidir.</summary>
|
||||||
|
private static readonly Dictionary<string, string> 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 IdColumn = new("Id", "uniqueidentifier", null, false, "NEWID()");
|
||||||
|
|
||||||
private static readonly McpTableColumn TenantColumn = new("TenantId", "uniqueidentifier", null, true, null);
|
private static readonly McpTableColumn TenantColumn = new("TenantId", "uniqueidentifier", null, true, null);
|
||||||
|
|
@ -97,24 +107,34 @@ public class McpTableCreator(
|
||||||
EnsureSafeIdentifier(tableName);
|
EnsureSafeIdentifier(tableName);
|
||||||
|
|
||||||
var columns = BuildColumns(definition);
|
var columns = BuildColumns(definition);
|
||||||
|
var indexes = BuildIndexes(tableName, definition.Indexes, columns);
|
||||||
|
var foreignKeys = BuildForeignKeys(definition.ForeignKeys, columns);
|
||||||
|
|
||||||
var context = await queryExecutor.ResolveDataSourceAsync();
|
var context = await queryExecutor.ResolveDataSourceAsync();
|
||||||
var isPostgreSql = context.DataSourceType == DataSourceTypeEnum.Postgresql;
|
var isPostgreSql = context.DataSourceType == DataSourceTypeEnum.Postgresql;
|
||||||
|
|
||||||
var existing = await context.Repository.ExecuteScalarAsync<object?>(
|
async Task<bool> TableExistsAsync(string name)
|
||||||
isPostgreSql ? "SELECT to_regclass(@name)::text" : "SELECT OBJECT_ID(@name, 'U')",
|
{
|
||||||
context.ConnectionString,
|
var existing = await context.Repository.ExecuteScalarAsync<object?>(
|
||||||
new Dictionary<string, object> { ["name"] = isPostgreSql ? $"public.\"{tableName}\"" : $"{SchemaName}.{tableName}" });
|
isPostgreSql ? "SELECT to_regclass(@name)::text" : "SELECT OBJECT_ID(@name, 'U')",
|
||||||
|
context.ConnectionString,
|
||||||
|
new Dictionary<string, object> { ["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
|
// Henuz migrate edilmemis bir create_table da tabloyu sahiplenmis sayilir; ikinci cagri onun
|
||||||
// scriptinin ustune yazmaz.
|
// 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]);
|
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.
|
// 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 _))
|
if (!PostgreSqlTableScript.TryConvert(script, out _))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"PostgreSQL script could not be generated for {tableName}.");
|
throw new InvalidOperationException($"PostgreSQL script could not be generated for {tableName}.");
|
||||||
|
|
@ -161,6 +181,69 @@ public class McpTableCreator(
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Index ve unique kolonlari tablonun kolonlari arasinda olmalidir; ad tasarimcinin kalibindadir.</summary>
|
||||||
|
private List<McpTableIndex> BuildIndexes(string tableName, IReadOnlyList<McpTableIndex> indexes, List<McpTableColumn> columns)
|
||||||
|
{
|
||||||
|
var result = new List<McpTableIndex>();
|
||||||
|
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<McpTableForeignKey> BuildForeignKeys(IReadOnlyList<McpTableForeignKey> foreignKeys, List<McpTableColumn> columns)
|
||||||
|
{
|
||||||
|
var result = new List<McpTableForeignKey>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seed scriptleri DB Migrate'te dosya adi sirasiyla calisir (<c>SqlDataSeeder</c>). 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.
|
||||||
|
/// </summary>
|
||||||
|
private async Task EnsureReferenceInstalledFirstAsync(string tableName, McpTableForeignKey foreignKey, Func<string, Task<bool>> tableExistsAsync)
|
||||||
|
{
|
||||||
|
var hasScript = PostgreSqlTableScript.ReadSeedColumns(configuration, foreignKey.ReferencedTable).Count > 0;
|
||||||
|
var installed = hasScript
|
||||||
|
? Comparer<string>.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<McpTableColumn> 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)
|
private McpTableColumn NormalizeColumn(McpTableColumn column)
|
||||||
{
|
{
|
||||||
var name = (column.Name ?? string.Empty).Trim();
|
var name = (column.Name ?? string.Empty).Trim();
|
||||||
|
|
@ -230,7 +313,7 @@ public class McpTableCreator(
|
||||||
return (type, maxLength);
|
return (type, maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildScript(string tableName, List<McpTableColumn> columns)
|
private static string BuildScript(string tableName, List<McpTableColumn> columns, List<McpTableIndex> indexes, List<McpTableForeignKey> foreignKeys)
|
||||||
{
|
{
|
||||||
var fullTableName = $"[{SchemaName}].[{tableName}]";
|
var fullTableName = $"[{SchemaName}].[{tableName}]";
|
||||||
|
|
||||||
|
|
@ -251,6 +334,42 @@ public class McpTableCreator(
|
||||||
lines.Add("END");
|
lines.Add("END");
|
||||||
lines.Add("GO");
|
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);
|
return string.Join('\n', lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,11 +399,21 @@ public class McpTableCreator(
|
||||||
|
|
||||||
/// <param name="FullAudited">Tam audit kolonlari (olusturma/degistirme/silme izi) eklensin mi.</param>
|
/// <param name="FullAudited">Tam audit kolonlari (olusturma/degistirme/silme izi) eklensin mi.</param>
|
||||||
/// <param name="MultiTenant"><c>TenantId</c> kolonu eklensin mi.</param>
|
/// <param name="MultiTenant"><c>TenantId</c> kolonu eklensin mi.</param>
|
||||||
|
/// <param name="Indexes">Index ve unique kisitlari; ad sunucuda uretilir (<c>IX_/UQ_{Tablo}_{Kolonlar}</c>).</param>
|
||||||
|
/// <param name="ForeignKeys">FK'ler; ad <c>FK_{Tablo}_{Kolon}</c>.</param>
|
||||||
public sealed record McpTableDefinition(
|
public sealed record McpTableDefinition(
|
||||||
string TableName,
|
string TableName,
|
||||||
IReadOnlyList<McpTableColumn> Columns,
|
IReadOnlyList<McpTableColumn> Columns,
|
||||||
bool FullAudited,
|
bool FullAudited,
|
||||||
bool MultiTenant);
|
bool MultiTenant,
|
||||||
|
IReadOnlyList<McpTableIndex> Indexes,
|
||||||
|
IReadOnlyList<McpTableForeignKey> ForeignKeys);
|
||||||
|
|
||||||
|
public sealed record McpTableIndex(IReadOnlyList<string> Columns, bool Unique, string? Name = null);
|
||||||
|
|
||||||
|
/// <param name="ReferencedColumn">Bos ise <c>Id</c>.</param>
|
||||||
|
/// <param name="OnDelete"><c>NoAction</c> (varsayilan), <c>Cascade</c> ya da <c>SetNull</c>.</param>
|
||||||
|
public sealed record McpTableForeignKey(string Column, string ReferencedTable, string? ReferencedColumn, string? OnDelete);
|
||||||
|
|
||||||
/// <param name="MaxLength">Yalnizca <c>nvarchar</c> icin; bos ise tasarimcinin varsayilani (100).</param>
|
/// <param name="MaxLength">Yalnizca <c>nvarchar</c> icin; bos ise tasarimcinin varsayilani (100).</param>
|
||||||
public sealed record McpTableColumn(
|
public sealed record McpTableColumn(
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
|
||||||
=> await developAppService.GetDevGuideAsync(document, section);
|
=> await developAppService.GetDevGuideAsync(document, section);
|
||||||
|
|
||||||
[McpServerTool(Name = McpToolNames.CreateTable, Destructive = false, Idempotent = false)]
|
[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<McpCreateTableResultDto> CreateTableAsync(
|
public async Task<McpCreateTableResultDto> CreateTableAsync(
|
||||||
[Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")]
|
[Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")]
|
||||||
string tableName,
|
string tableName,
|
||||||
|
|
@ -47,15 +47,41 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
|
||||||
[Description("Tam audit kolonlari eklensin mi. Kullanici acikca istemedikce true birak.")]
|
[Description("Tam audit kolonlari eklensin mi. Kullanici acikca istemedikce true birak.")]
|
||||||
bool fullAudited = true,
|
bool fullAudited = true,
|
||||||
[Description("TenantId kolonu eklensin mi. Kullanici acikca istemedikce true birak.")]
|
[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<McpTableIndexDto>? 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<McpTableForeignKeyDto>? foreignKeys = null)
|
||||||
=> await developAppService.CreateTableAsync(new McpCreateTableRequestDto
|
=> await developAppService.CreateTableAsync(new McpCreateTableRequestDto
|
||||||
{
|
{
|
||||||
TableName = tableName,
|
TableName = tableName,
|
||||||
Columns = columns,
|
Columns = columns,
|
||||||
FullAudited = fullAudited,
|
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<McpSqlObjectResultDto> 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<McpCrudEndpointsResultDto> 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)]
|
[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.")]
|
[Description("Wizard File Manager'daki wizard dosyalarini listeler: dosya adi, ekran kodu, yol (List/Custom), menu yolu ve bagimli wizard'lar.")]
|
||||||
public async Task<List<WizardFileInfoDto>> ListWizardsAsync(
|
public async Task<List<WizardFileInfoDto>> ListWizardsAsync(
|
||||||
|
|
@ -71,9 +97,9 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
|
||||||
=> await developAppService.GetWizardAsync(fileName);
|
=> await developAppService.GetWizardAsync(fileName);
|
||||||
|
|
||||||
[McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)]
|
[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<WizardFileInfoDto?> DeployWizardAsync(
|
public async Task<WizardFileInfoDto?> 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,
|
string wizard,
|
||||||
[Description("Guncellemede yerine gecilecek dosyanin adi (list_wizards cikisindaki fileName). Yeni wizard icin bos birak.")]
|
[Description("Guncellemede yerine gecilecek dosyanin adi (list_wizards cikisindaki fileName). Yeni wizard icin bos birak.")]
|
||||||
string? replaceFileName = null,
|
string? replaceFileName = null,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService, ICurre
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)]
|
[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<List<McpListSummaryDto>> ListListsAsync(
|
public async Task<List<McpListSummaryDto>> ListListsAsync(
|
||||||
[Description("Liste adi, basligi, aciklamasi ya da tablo adi icinde aranacak metin. Bos birakilabilir.")]
|
[Description("Liste adi, basligi, aciklamasi ya da tablo adi icinde aranacak metin. Bos birakilabilir.")]
|
||||||
string? search = null,
|
string? search = null,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,14 @@ public class ListFormWizardDto
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Guid? DevRequestId { get; set; }
|
public Guid? DevRequestId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deploy girdisindeki <c>ListForm</c>/<c>Fields</c> bolumleri; wizard cevaplarindan uretilen
|
||||||
|
/// ekrana katman olarak uygulanir (<see cref="WizardSeedFileDto.FromDeployJson"/>). Yalnizca
|
||||||
|
/// sunucu ici girdidir, dosyaya yazilmaz: uygulanan ayarlar ListForm/Fields bolumlerinde kalir.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public WizardScreenOverlay ScreenOverlay { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wizard'in ilk deploy edildigi an. Dosya adi tarih tasimadigi icin seeder dosyalari bu
|
/// 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
|
/// damgaya gore siralar; birbirine bagli wizard'lar (once Customers, sonra Tasks) uretildikleri
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deploy girdisindeki <c>ListForm</c>/<c>Fields</c> 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 (<c>get_wizard</c> ciktisi) kabul eder; boylece MCP tasarimcinin yapabildigi her seyi
|
||||||
|
/// wizard dosyasina yazabilir ve yeniden deploy tasarimci ayarlarini silmez.
|
||||||
|
/// <para>
|
||||||
|
/// Birlestirme uc yonludur: bolumdeki deger, bolumlerden geri kurulan wizard'in uretecegi degerle
|
||||||
|
/// (<see cref="Baseline"/>) 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.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class WizardScreenOverlay
|
||||||
|
{
|
||||||
|
public JsonObject ListForm { get; set; }
|
||||||
|
|
||||||
|
public List<JsonObject> Fields { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Bolumlerden geri kurulan wizard; uc yonlu birlestirmenin tabani.</summary>
|
||||||
|
public ListFormWizardDto Baseline { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Wizard'dan turedigi icin katmanla degistirilmeyen ekran alanlari.</summary>
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>Wizard cevabindaki adla yazilan alan anahtarlari; alan sozlesmesindeki karsiliklari.</summary>
|
||||||
|
private static readonly Dictionary<string, string> FieldAliases = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[nameof(WizardColumnItemInputDto.EditorType)] = nameof(ColumnFormatEditDto.EditorType2),
|
||||||
|
[nameof(WizardColumnItemInputDto.DbSourceType)] = nameof(ColumnFormatEditDto.SourceDbType),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Katmani wizard'in urettigi ListForm'a uygular. <paramref name="baseline"/> ayni donusumun
|
||||||
|
/// <see cref="Baseline"/> wizard'i icin ciktisidir.
|
||||||
|
/// </summary>
|
||||||
|
public GridOptionsEditDto Apply(GridOptionsEditDto built, GridOptionsEditDto baseline, List<string> errors)
|
||||||
|
{
|
||||||
|
if (ListForm == null)
|
||||||
|
return built;
|
||||||
|
|
||||||
|
return Merge(built, ListForm, baseline, ProtectedListFormKeys, [], "ListForm", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Katmani alanlara uygular; alanlar <c>FieldName</c> ile eslesir. Wizard gruplarinda olmayan bir
|
||||||
|
/// alan icin katman yazilamaz: alan kumesi gruplardan gelir.
|
||||||
|
/// </summary>
|
||||||
|
public List<ColumnFormatEditDto> Apply(List<ColumnFormatEditDto> built, List<ColumnFormatEditDto> baseline, List<string> 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>(
|
||||||
|
T built,
|
||||||
|
JsonObject overlay,
|
||||||
|
T baseline,
|
||||||
|
string[] protectedKeys,
|
||||||
|
Dictionary<string, string> aliases,
|
||||||
|
string path,
|
||||||
|
List<string> 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<string>();
|
||||||
|
|
||||||
|
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<T>(ReadOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nesneler alan alan birlestirilir; dizi ve tekil degerler tumden yazilir. Tabandaki degerle
|
||||||
|
/// ayni olan katman degeri wizard'in turettigi degeri ezmez.
|
||||||
|
/// </summary>
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anahtari sozlesmedeki ada cevirir: buyuk/kucuk harf duyarsiz, wizard adlari (<c>EditorType</c>)
|
||||||
|
/// ve rehberdeki kolon adlari (<c>FormFieldsDefaultValueJson</c> → <c>FormFieldsDefaultValueDto</c>).
|
||||||
|
/// Json kolonu metin olarak verilmisse cozulur.
|
||||||
|
/// </summary>
|
||||||
|
private static (string Key, JsonNode Value) Resolve(JsonObject target, string key, JsonNode value, Dictionary<string, string> 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<string>(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<string>(out var text) ? text : null;
|
||||||
|
|
||||||
|
private static HashSet<string> WritableProperties(Type type) =>
|
||||||
|
[
|
||||||
|
.. type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanWrite && p.SetMethod?.IsPublic == true && p.GetCustomAttribute<JsonIgnoreAttribute>() == null)
|
||||||
|
.Select(p => p.Name)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -285,23 +285,136 @@ public class WizardSeedFileDto
|
||||||
{
|
{
|
||||||
var node = JsonNode.Parse(json);
|
var node = JsonNode.Parse(json);
|
||||||
if (node?[nameof(Wizard)] is JsonObject wizardNode)
|
if (node?[nameof(Wizard)] is JsonObject wizardNode)
|
||||||
{
|
FlattenWizard(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.Deserialize<WizardSeedFileDto>(SeedReadOptions)?.Rehydrate();
|
return node.Deserialize<WizardSeedFileDto>(SeedReadOptions)?.Rehydrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deploy girdisini (MCP <c>deploy_wizard</c>) okur: seed dosyasi bicimi (<c>{"Wizard": ..., "ListForm": ..., "Fields": [...]}</c>)
|
||||||
|
/// ya da duz wizard cevaplari. <see cref="FromSeedJson"/>'dan farki, bolumlerin eksik ya da
|
||||||
|
/// kismi olabilmesidir:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Wizard bolumunde yazilan cevap korunur; yazilmayan ekran alani (veri kaynagi, duzen,
|
||||||
|
/// alan editoru …) <c>ListForm</c>/<c>Fields</c> bolumunden tamamlanir. Boylece get_wizard ciktisina
|
||||||
|
/// yeni bir alan eklemek, <c>Fields</c>'ta karsiligi olmadigi icin sessizce dusmez.</item>
|
||||||
|
/// <item><c>ListForm</c>/<c>Fields</c> bolumleri ekranin sozlesmesine katman olarak uygulanir
|
||||||
|
/// (<see cref="WizardScreenOverlay"/>): wizard cevaplarinin uretmedigi her ayar (varsayilan
|
||||||
|
/// degerler, dogrulama, bicim, komutlar …) deploy'da ekranda kalir.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
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<ListFormWizardDto>(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<WizardSeedFileDto>(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<JsonObject>()],
|
||||||
|
Baseline = sections,
|
||||||
|
};
|
||||||
|
|
||||||
|
return wizard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bolumde yazilmamis wizard alanlarini <paramref name="source"/>'tan kopyalar.</summary>
|
||||||
|
private static void FillMissing<T>(T target, T source, JsonObject written, IEnumerable<string> keys)
|
||||||
|
{
|
||||||
|
foreach (var key in keys.Where(key => !ContainsKey(written, key)))
|
||||||
|
{
|
||||||
|
var property = typeof(T).GetProperty(key);
|
||||||
|
property?.SetValue(target, property.GetValue(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gruplar wizard bolumunde hic yoksa <c>Fields</c>'tan kurulur; varsa yazilan gruplar ve ogeler
|
||||||
|
/// esas alinir, ogenin yazilmamis editor/lookup alanlari ayni adli alandan tamamlanir.
|
||||||
|
/// </summary>
|
||||||
|
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));
|
||||||
|
|
||||||
|
/// <summary>`Menu`/`Language` basliklarini duzlestirir ve dosyadaki adlari sozlesme adlarina cevirir.</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Wizard bolumundeki tum grup ogeleri; ad donusumu her ogeye ayni sekilde uygulanir.</summary>
|
/// <summary>Wizard bolumundeki tum grup ogeleri; ad donusumu her ogeye ayni sekilde uygulanir.</summary>
|
||||||
private static IEnumerable<JsonObject> ItemNodes(JsonObject wizard) =>
|
private static IEnumerable<JsonObject> ItemNodes(JsonObject wizard) =>
|
||||||
(wizard[nameof(ListFormWizardDto.Groups)] as JsonArray ?? [])
|
(wizard[nameof(ListFormWizardDto.Groups)] as JsonArray ?? [])
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
@ -25,11 +24,6 @@ public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
PagedAndSortedResultRequestDto,
|
PagedAndSortedResultRequestDto,
|
||||||
CreateUpdateCrudEndpointDto>, ICrudEndpointAppService
|
CreateUpdateCrudEndpointDto>, ICrudEndpointAppService
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions SeedJsonWriteOptions = new()
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
public CrudEndpointGenerateAppService(
|
public CrudEndpointGenerateAppService(
|
||||||
|
|
@ -76,72 +70,27 @@ public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
[FromQuery] string? operationTypes = null,
|
[FromQuery] string? operationTypes = null,
|
||||||
[FromQuery] string? objectType = null)
|
[FromQuery] string? objectType = null)
|
||||||
{
|
{
|
||||||
// Tablolar icin bes endpoint uretilir; operationTypes verilmisse yalnizca
|
// Endpoint kumesi (operasyonlar, yollar, aktiflik) seed dosyasiyla ayni kaynaktan gelir;
|
||||||
// listedekiler aktif baslar, digerleri pasif olarak kaydedilir ve
|
// MCP ayni kumeyi yalnizca dosyaya yazar.
|
||||||
// kullanici diyalogdan sonradan acabilir. Bos/null ise hepsi aktiftir.
|
var endpoints = CrudEndpointSeedFileDto.Generate(entityName, operationTypes, objectType, Clock.Now.ToUniversalTime())
|
||||||
var activeOperations = operationTypes
|
.Endpoints
|
||||||
?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
.Select(item => new CrudEndpoint
|
||||||
.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<CrudEndpoint>
|
|
||||||
{
|
|
||||||
new() {
|
|
||||||
EntityName = entityName,
|
EntityName = entityName,
|
||||||
Method = "GET",
|
Method = item.Method,
|
||||||
Path = $"/api/app/crudendpoint/{entityName}",
|
Path = item.Path,
|
||||||
OperationType = "GetList",
|
OperationType = item.OperationType,
|
||||||
IsActive = IsActiveOperation("GetList"),
|
IsActive = item.IsActive,
|
||||||
CsharpCode = GenerateGetAllCode(entityName)
|
CsharpCode = item.OperationType switch
|
||||||
},
|
{
|
||||||
new() {
|
"GetList" => GenerateGetAllCode(entityName),
|
||||||
EntityName = entityName,
|
"GetById" => GenerateGetByIdCode(entityName),
|
||||||
Method = "GET",
|
"Create" => GenerateCreateCode(entityName),
|
||||||
Path = $"/api/app/crudendpoint/{entityName}/{{id}}",
|
"Update" => GenerateUpdateCode(entityName),
|
||||||
OperationType = "GetById",
|
_ => GenerateDeleteCode(entityName)
|
||||||
IsActive = IsActiveOperation("GetById"),
|
}
|
||||||
CsharpCode = GenerateGetByIdCode(entityName)
|
})
|
||||||
},
|
.ToList();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Var olanları sil
|
// Var olanları sil
|
||||||
var existingEndpoints = await Repository
|
var existingEndpoints = await Repository
|
||||||
|
|
@ -209,7 +158,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SyncSeedFileAsync(string entityName)
|
private async Task SyncSeedFileAsync(string entityName)
|
||||||
{
|
{
|
||||||
var safeFileName = GetSafeFileName(entityName);
|
var safeFileName = CrudEndpointSeedFileDto.SafeFileName(entityName);
|
||||||
if (safeFileName == null)
|
if (safeFileName == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -217,13 +166,11 @@ public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var outputPath = ResolveCrudSeedOutputPath();
|
|
||||||
var filePath = Path.Combine(outputPath, $"{safeFileName}.json");
|
|
||||||
|
|
||||||
var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName);
|
var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName);
|
||||||
|
|
||||||
if (endpoints.Count == 0)
|
if (endpoints.Count == 0)
|
||||||
{
|
{
|
||||||
|
var filePath = Path.Combine(SeedPathResolver.GetPath(_configuration, SeedPathResolver.CrudFolder), $"{safeFileName}.json");
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
File.Delete(filePath);
|
File.Delete(filePath);
|
||||||
|
|
@ -233,8 +180,6 @@ public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(outputPath);
|
|
||||||
|
|
||||||
var seedFile = new CrudEndpointSeedFileDto
|
var seedFile = new CrudEndpointSeedFileDto
|
||||||
{
|
{
|
||||||
EntityName = entityName,
|
EntityName = entityName,
|
||||||
|
|
@ -251,8 +196,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, SeedJsonWriteOptions));
|
Logger.LogInformation("CRUD seed file saved: {FilePath}", seedFile.Write(_configuration));
|
||||||
Logger.LogInformation("CRUD seed file saved: {FilePath}", filePath);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -260,31 +204,6 @@ public class CrudEndpointGenerateAppService : CrudAppService<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// CRUD seed dosyalarinin yazilacagi klasor (<c>crud/</c>); host ve kendi veritabani olan her
|
|
||||||
/// tenant ayni dosyalarla kurulur.
|
|
||||||
/// </summary>
|
|
||||||
private string ResolveCrudSeedOutputPath()
|
|
||||||
{
|
|
||||||
return SeedPathResolver.GetPath(_configuration, SeedPathResolver.CrudFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Entity adindan guvenli bir dosya adi uretir; path traversal denemelerini reddeder.</summary>
|
|
||||||
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)
|
private string GenerateGetAllCode(string entityName)
|
||||||
{
|
{
|
||||||
return $@"[HttpGet]
|
return $@"[HttpGet]
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,6 @@ public class ListFormWizardAppService(
|
||||||
? await IsQueryMainTableSoftDeletedAsync(input)
|
? await IsQueryMainTableSoftDeletedAsync(input)
|
||||||
: tableColumns.ContainsKey("IsDeleted");
|
: tableColumns.ContainsKey("IsDeleted");
|
||||||
isCreated = tableColumns.ContainsKey("CreatorId");
|
isCreated = tableColumns.ContainsKey("CreatorId");
|
||||||
ValidateImageColumnLengths(input, tableColumns);
|
|
||||||
foreach (var item in input.Groups.SelectMany(g => g.Items))
|
foreach (var item in input.Groups.SelectMany(g => g.Items))
|
||||||
item.CaptionName = await ResolveLanguageKeyCasingAsync(rows, item.CaptionName);
|
item.CaptionName = await ResolveLanguageKeyCasingAsync(rows, item.CaptionName);
|
||||||
listFormDto = input.BuildListForm(isDeleted, isCreated);
|
listFormDto = input.BuildListForm(isDeleted, isCreated);
|
||||||
|
|
@ -325,6 +324,11 @@ public class ListFormWizardAppService(
|
||||||
if (input.SeedData && !string.IsNullOrWhiteSpace(previous?.ListForm?.SeedFilePath))
|
if (input.SeedData && !string.IsNullOrWhiteSpace(previous?.ListForm?.SeedFilePath))
|
||||||
listFormDto.SeedFilePath = 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))
|
foreach (var item in input.Groups.SelectMany(g => g.Items))
|
||||||
await WriteLanguageRowsAsync(rows, item.CaptionName, item.EnglishCaption, item.TurkishCaption, owned, inserted);
|
await WriteLanguageRowsAsync(rows, item.CaptionName, item.EnglishCaption, item.TurkishCaption, owned, inserted);
|
||||||
}
|
}
|
||||||
|
|
@ -344,6 +348,48 @@ public class ListFormWizardAppService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deploy girdisindeki ListForm/Fields bolumlerini uretilen ekrana uygular
|
||||||
|
/// (<see cref="WizardScreenOverlay"/>) ve wizard cevaplarini son sozlesmeyle esitler; sonraki
|
||||||
|
/// adimlar (dil satirlari, widget ve gorsel kolonu dogrulamasi) katmanin getirdigi degerleri gorur.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(GridOptionsEditDto ListForm, List<ColumnFormatEditDto> Fields)> ApplyScreenOverlayAsync(
|
||||||
|
ListFormWizardDto input,
|
||||||
|
GridOptionsEditDto listForm,
|
||||||
|
List<ColumnFormatEditDto> fields,
|
||||||
|
bool isDeleted,
|
||||||
|
bool isCreated,
|
||||||
|
WizardSeedRowStore rows)
|
||||||
|
{
|
||||||
|
var overlay = input.ScreenOverlay;
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bekleyen kurulum: onceki surumun sahip oldugu kayitlar ve kurdugu ekran migrate'te yenilenmek
|
/// 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
|
/// uzere eklenir. Migrate edilmeden ust uste yapilan duzenlemeler ayni listeyi buyutur; ilk
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Data.Seeds;
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
|
|
@ -18,6 +22,72 @@ public class CrudEndpointSeedFileDto
|
||||||
public DateTime GeneratedAt { get; set; }
|
public DateTime GeneratedAt { get; set; }
|
||||||
|
|
||||||
public List<CrudEndpointSeedItemDto> Endpoints { get; set; } = [];
|
public List<CrudEndpointSeedItemDto> Endpoints { get; set; } = [];
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir entity'nin standart endpoint kumesi: tabloda bes operasyon, view/procedure/function'da
|
||||||
|
/// yalnizca okuma. <paramref name="operationTypes"/> verilmisse yalnizca listedekiler aktif baslar,
|
||||||
|
/// digerleri pasif yazilir. SQL Query Manager diyalogu ve MCP ayni kumeyi bu metottan uretir.
|
||||||
|
/// </summary>
|
||||||
|
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)
|
||||||
|
})]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entity adindan guvenli dosya adi; yol iceren (<c>/</c>, <c>\</c>, <c>..</c>) ad reddedilir (null).
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Dosyayi <c>crud/{EntityName}.json</c> olarak yazar ve tam yolunu dondurur.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
14
claude.md
14
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 —
|
`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
|
sunucu doldurur, modele bırakılmaz. `Timesheets`'i tamamen sunucu yazar
|
||||||
(`IMcpDevRequestTimesheetRecorder`): `get_dev_request`/`start_dev_request` talebi etkin talep yapar,
|
(`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`,
|
`save_component`, `delete_component`, `save_report_category`, `save_report_template`,
|
||||||
`delete_report_template`, satır araçları, `save_dev_request_analysis`,
|
`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
|
`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
|
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
|
token kullanıcı oturumuna bağlanmaz; token ucuna yeni bir oturum yolu eklenirse bu istisna
|
||||||
korunur, yoksa connector web oturumunu düşürür.
|
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
|
- **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
|
ekranın sorgusundan (`IListFormSelectAppService`/`IListFormDataAppService`) geçer; liste
|
||||||
yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki
|
yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"Assignees": "system@sozsoft.com",
|
"Assignees": "system@sozsoft.com",
|
||||||
"Priority": null,
|
"Priority": null,
|
||||||
"Completed": false,
|
"Completed": false,
|
||||||
"SortOrder": 0,
|
"SortOrder": 1,
|
||||||
"Timesheets": null,
|
"Timesheets": null,
|
||||||
"SubTasks": null,
|
"SubTasks": null,
|
||||||
"Analysis": null
|
"Analysis": null
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -17,7 +17,7 @@
|
||||||
"Assignees": "system@sozsoft.com",
|
"Assignees": "system@sozsoft.com",
|
||||||
"Priority": "5",
|
"Priority": "5",
|
||||||
"Completed": false,
|
"Completed": false,
|
||||||
"SortOrder": 1,
|
"SortOrder": 2,
|
||||||
"Timesheets": null,
|
"Timesheets": null,
|
||||||
"SubTasks": null,
|
"SubTasks": null,
|
||||||
"Analysis": null
|
"Analysis": null
|
||||||
|
|
|
||||||
|
|
@ -69556,6 +69556,34 @@
|
||||||
"Key": "App.Mcp.Timesheet.CreateTable",
|
"Key": "App.Mcp.Timesheet.CreateTable",
|
||||||
"Value": "{0} tablosu oluşturuldu"
|
"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",
|
"Id": "c808ea33-665d-5b85-bca9-10b0b4fc407f",
|
||||||
"CultureName": "en",
|
"CultureName": "en",
|
||||||
|
|
@ -69934,6 +69962,48 @@
|
||||||
"Key": "App.Mcp.Error.TableAlreadyExists",
|
"Key": "App.Mcp.Error.TableAlreadyExists",
|
||||||
"Value": "\"{0}\" tablosu zaten var. Var olan tabloları SQL Query Manager tablo tasarımcısından değiştirin."
|
"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",
|
"Id": "d88ec341-a3cd-505f-b14a-2c6ce3f82de9",
|
||||||
"CultureName": "en",
|
"CultureName": "en",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue