seeds klasörü konum değiştirildi

This commit is contained in:
Sedat ÖZTÜRK 2026-10-05 15:58:59 +03:00
parent da98513c42
commit 10abf9c08d
455 changed files with 2079 additions and 3067 deletions

View file

@ -23,7 +23,7 @@ Companion documents (this file wins on conflict):
| `dotnet.instructions.md` | Binding code standard for everything under `api/` |
| `list.instructions.md` | Step-by-step procedure for adding a module / list through seeders |
| `../../README.md` | Technical map: what exists, where it lives, how it is wired |
| `../../configs/seeds/host/docs/modules/{Module}.md` | **How a generated module works**: discovery answers, approved recipe, screens, formulas, status rules, permissions, FAQ. Read it first when asked how a module/component behaves; write or update it with every module/component change (`lowcode.instructions.md` §0.13). Index: `README.md` §10.5 |
| `../../configs/seeds/docs/modules/{Module}.md` | **How a generated module works**: discovery answers, approved recipe, screens, formulas, status rules, permissions, FAQ. Read it first when asked how a module/component behaves; write or update it with every module/component change (`lowcode.instructions.md` §0.13). Index: `README.md` §10.5 |
| `../../CLAUDE.md` | Operating rules and standing defaults for AI agents in this repo |
---
@ -665,7 +665,7 @@ Platform-owned screens, menus, permissions and language texts. Changing them req
a deploy. AI must learn and teach implementation flow primarily from these assets:
- ListFormSeeder_Saas.cs / ListFormSeeder_Administration.cs / ListFormSeeder_Hr.cs / ListFormSeeder_{Modul}.cs
- configs/seeds/host/data/App.Languages.{Language,LanguageText}.json — languages and texts (ListForm seed data)
- configs/seeds/data/App.Languages.{Language,LanguageText}.json — languages and texts (ListForm seed data)
- WizardDataSeeder.cs / CrudDataSeeder.cs — the seeders that read
surface B below
- ListFormSeedDataApplier, CrudEndpointSeedApplier (Domain/Data/) — the single home of the apply
@ -678,11 +678,14 @@ a deploy. AI must learn and teach implementation flow primarily from these asset
**B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through
`SeedPathResolver`. The Wizard, the Custom Components screen / Visual Designer, the CRUD
Endpoint Manager and every ListForm screen with a `SeedFilePath` write here; the
seeders above restore them when the database is recreated. Scope folders mirror the CDN layout:
seeders above restore them when the database is recreated. There is a single folder and no
tenant scope: the host and every tenant with its own database are seeded from the same files
(`SeedFileScope`; a tenant sharing the host database is skipped — the host seed already wrote
there). Files are written only in host context; a tenant's record never reaches a seed file.
```
configs/seeds/host/{wizard,data,crud,sql/{object,execute},postgres/{object,execute}}
configs/seeds/tenants/{tenantId}/… same layout, tenant scope
configs/seeds/{wizard,data,crud,docs,sql/{object,execute},postgres/{object,execute}}
configs/seeds/{sql,postgres}/host/{object,execute} host database only (e.g. scripts over HangFire)
configs/seeds/.imports/{importId}/… wizard import staging + backup (not scanned)
```
@ -722,7 +725,7 @@ AI must prioritize metadata-based field behavior instead of suggesting custom Re
### 31.3 Step 3 - Add menu and route mapping
From the `configs/seeds/host/data/App.Menus.*.json` patterns, AI must define both:
From the `configs/seeds/data/App.Menus.*.json` patterns, AI must define both:
- `App.Menus.Routes.json` row:
- `Id`, `Key`, `Path`, `ComponentPath`, `RouteType`, `Authority`
@ -739,7 +742,7 @@ AI must ensure menu URL and route path point to same screen contract.
### 31.4 Step 4 - Add permissions
From the `configs/seeds/host/data/Abp.Identity.Permission*.json` patterns, AI must define:
From the `configs/seeds/data/Abp.Identity.Permission*.json` patterns, AI must define:
- Permission group (if missing)
- Permission definitions for feature root and actions
@ -759,7 +762,7 @@ AI must never propose menu/route without permission contract.
### 31.5 Step 5 - Add settings/integration dependencies
From the `configs/seeds/host/data/App.SettingDefinitions.json` / `App.Definitions.AiBot.json` patterns, AI must define required settings when feature depends on external services:
From the `configs/seeds/data/App.SettingDefinitions.json` / `App.Definitions.AiBot.json` patterns, AI must define required settings when feature depends on external services:
- Sender credentials and endpoints (sms/mail/whatsapp/rocket)
- AiBot integration endpoint and activation state
@ -907,8 +910,8 @@ Display names are read from the language key whose name equals the group name.
### 32.5 Wizard export/import is a package, not a file
Exporting a wizard produces a zip whose paths are relative to the scope folder
(`host` / `tenants/{tenantId}`) and which carries every dependency:
Exporting a wizard produces a zip whose paths are relative to the seed folder and which carries
every dependency:
The closure is **recursive** (`WizardExportCollector`): a sub-form's wizard file is queued and its
own dependencies are collected under the same rules, so the archive stands on its own.

View file

@ -18,7 +18,7 @@ This document summarizes the rules, standards, and step-by-step instructions for
- Table names: `{Modul}_T_{Entity}` (e.g., `Mrp_T_Order`)
- Menu/permission keys: `App.{Modul}`, `App.{Modul}.{Liste}`
- **Tenant Isolation:** All data, forms, and permissions must be tenant-aware.
- **Localization:** Every menu, list, and field must have a corresponding entry in `configs/seeds/host/data/App.Languages.LanguageText.json`.
- **Localization:** Every menu, list, and field must have a corresponding entry in `configs/seeds/data/App.Languages.LanguageText.json`.
- **No Redundant Code:** Avoid duplicating permission grants or seeding logic across modules.
- **SeedConsts Rule:** For every new module/entity, add a constant to `SeedConsts` (e.g., `public static class {Modul}` and `public const string {Liste}`) if it does not already exist. Never duplicate existing constants.
- **DeleteCommand Rule:** In all `ListFormSeeder_{Modul}.cs` files, the `DeleteCommand` property **must** use the `DefaultDeleteCommand` function, e.g., `DeleteCommand = DefaultDeleteCommand("{TabloAdı}")`. Never use a raw SQL string directly for `DeleteCommand`.
@ -27,10 +27,10 @@ This document summarizes the rules, standards, and step-by-step instructions for
## Special File Handling Rules
- **configs/seeds/host/data/App.Menus.*.json:**
- **configs/seeds/data/App.Menus.*.json:**
- Never add, remove or re-path a route in `App.Menus.Routes.json`. The only field set you may edit on an existing row is its `Seo*` columns (lowcode §0.9). Only `App.Menus.Module.json` and `App.Menus.Menu.json` can be changed for menu additions or updates. All menu additions must use the platform's configuration mechanisms or be added to those two files as required by platform design.
- **configs/seeds/host/data/App.Languages.LanguageText.json:**
- **configs/seeds/data/App.Languages.LanguageText.json:**
- When adding a new key, always check if the key already exists. Only add the key if it does not exist to prevent duplicates.
- Each key is two rows (`CultureName` `en` and `tr`). `Id` = UUID v5 (URL namespace) of `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}`; never a random GUID.
@ -39,11 +39,11 @@ This document summarizes the rules, standards, and step-by-step instructions for
## File-by-File Change Summary
### 1. configs/seeds/host/data/App.Menus.Menu.json
### 1. configs/seeds/data/App.Menus.Menu.json
- **{Modul} menu** added as a top-level menu with `ModuleId: {Modul}`; the module row goes into `App.Menus.Module.json`.
- **{Liste}** added as a child menu under {Modul}.
### 2. configs/seeds/host/data/Abp.Identity.Permissions.json
### 2. configs/seeds/data/Abp.Identity.Permissions.json
- **{Modul} permissions** grouped under `App.{Modul}`.
- **{Liste} permissions** nested under {Modul} group.
@ -74,11 +74,11 @@ This document summarizes the rules, standards, and step-by-step instructions for
### 1. Add a New Module (e.g., {Modul})
```
Yeni bir modül ekle (ör: {Modul}):
- configs/seeds/host/data/App.Menus.Menu.json'a kök menü, App.Menus.Module.json'a modül kaydı olarak ekle
- configs/seeds/host/data/Abp.Identity.PermissionGroups.json'da ayrı bir grup oluştur
- configs/seeds/data/App.Menus.Menu.json'a kök menü, App.Menus.Module.json'a modül kaydı olarak ekle
- configs/seeds/data/Abp.Identity.PermissionGroups.json'da ayrı bir grup oluştur
- ListFormSeeder_{Modul}.cs dosyası oluştur ve list-form seed'ini buraya taşı
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak adlandır
- configs/seeds/host/data/App.Languages.LanguageText.json dosyasına menü, liste ve alan çevirilerini ekle
- configs/seeds/data/App.Languages.LanguageText.json dosyasına menü, liste ve alan çevirilerini ekle
```
### 2. Add a New List Under a Module
@ -87,8 +87,8 @@ Yeni bir liste ekle (ör: {Liste}):
- {Modul} ana menüsünün altına ekle
- ListFormSeeder_{Modul}.cs dosyasına seed kodunu ekle
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak oluştur
- configs/seeds/host/data/Abp.Identity.Permissions.json'da ilgili izinleri {Modul} grubuna ekle
- configs/seeds/host/data/App.Languages.LanguageText.json dosyasına liste ve alan çevirilerini ekle
- configs/seeds/data/Abp.Identity.Permissions.json'da ilgili izinleri {Modul} grubuna ekle
- configs/seeds/data/App.Languages.LanguageText.json dosyasına liste ve alan çevirilerini ekle
```
### 3. Enforce Tenant Isolation and Full Audit
@ -101,7 +101,7 @@ Tablo ve list-form için tenant izolasyonu ve full-audit alanlarını ekle:
### 4. Add/Update Localization
```
Yeni menü, liste veya alan eklediğinde configs/seeds/host/data/App.Languages.LanguageText.json dosyasına (EN + TR satırı) şu şekilde ekle:
Yeni menü, liste veya alan eklediğinde configs/seeds/data/App.Languages.LanguageText.json dosyasına (EN + TR satırı) şu şekilde ekle:
- App.{Modul}
- App.{Modul}.{Liste}
- App.Listform.ListformField.{Alan}

View file

@ -13,7 +13,7 @@ sorusunda buraya bak.
**Kaynak:** Buradaki her tablo `api/src/Sozsoft.Platform.Application.Contracts/ListForms/**`
(DTO), `api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListForm{,Field}.cs` (entity),
`ui/src/views/{list,form}/**` (alanın gerçekten okunup okunmadığı) ve §12 için
`configs/seeds/host/data/App.Menus.*.json` + `ListFormSeeder_*.cs` dosyalarından çıkarılmıştır.
`configs/seeds/data/App.Menus.*.json` + `ListFormSeeder_*.cs` dosyalarından çıkarılmıştır.
Çelişki hâlinde **kod esastır**; bu dosya güncellenir.
**Ortak kurallar**
@ -224,6 +224,15 @@ eklenen join ve `WHERE` parçası (`SelectQueryManager`; `ServerWhere` token'lar
ile çözülür). **Satır bazlı veri kısıtının** kod yazmadan kurulduğu yer burasıdır; ListForm
editörünün **Kişiselleştirme** sekmesinden yazılır.
**İstemci filtresi sözleşmesi** (`list-form-select/select` `filter` parametresi; grid, rapor, widget ve MCP
`query_list` aynı yoldan geçer): yalnızca DevExtreme filtre dizisi kabul edilir — `["Alan","op",değer]`,
`["Alan",değer]`, `[koşul,"and"|"or",koşul]`, `["!",koşul]`, iç içe gruplar. Alan ekranın alanı ya da anahtar
alan olmalıdır (değilse koşul `1 = 1` olur), operatör `= <> < <= > >= contains notcontains startswith
endswith` dışındaysa ve JSON bozuksa istek reddedilir; değer her zaman parametredir. Filtre tek grup
olarak paranteze alınır, ardından kiracı/şube/OU koşulları `AND` ile eklenir. Toplam/grup alanları da
ekranın alanlarından, toplam türü `sum avg min max count`'tan seçilir. "Tüm kayıtları sil"
(`createDeleteQuery`) ekranın silme yetkisini ve `AllowAllDeleting`'i birlikte ister.
### 2.6 Filtre, arama ve gruplama panelleri
**`FilterRowJson`** (`GridFilterRowDto`): `Visible` (`false`), `ApplyFilter` (`auto` \| `onClick`),
@ -1263,7 +1272,6 @@ soneklerini alır.
| **Public › Sales Orders** | `App.Orders.SalesOrders` (alt form Sales Order Item) | `Sas_H_Order` + `Sas_H_OrderItem` | host | **Onay akışı + alt kalem + toplam + diyalog** birlikte kullanılan tam örnek (§12.7). |
| **Public › Blog Category / Posts** | `App.BlogManagement.Category` · `.Posts` | `Sas_H_BlogCategory` · `Sas_H_BlogPost` | host | Çok dilli içerik (`ContentTr`/`ContentEn`), slug, yayın durumu, sayaç alanları; public `/blog` bu tablolardan okur, yayınlanmış yazılar sitemap'e girer. |
| **Public › Demos** | `App.Demos` | `Sas_H_Demo` | host | Public `/demo` formundan gelen talepler — public form → tablo akışının örneği. |
| **Dev Requests** | `App.DevRequests` | `Sas_H_DevRequest` | host | **Kanban/Todo düzeninin** referansı (`DefaultLayout: todo`): `Status`, `Priority`, `SortOrder`, `Tags`, `Assignees`, `SubTasks`, `Timesheets`; detayda `Description` yanında `Analysis` sekmesi (`AnalysisExpr`). MCP geliştirme döngüsünün kuyruğudur (`Backlog` → `Analysis` MCP analiz eder, `Ready to Start`'a kullanıcı alır; `Ready to Start` → `Testing`, `Staging`'e kullanıcı alır; araç çubuğunda DB Migrate butonu vardır — `UiEvalService.ApiDbMigrate()`, önce onay sorar, `App.Setup.Migrate`; yalnızca `start_dev_request` başlatır; açıklamadaki base64 görseller MCP'ye resim içeriği olarak gider, analiz `save_dev_request_analysis` ile yazılır, üretim notu `append_dev_request_note` ile `Analysis`'e eklenir — `Description` talebin yazıldığı hâliyle kalır; `lowcode.instructions.md` §0.11–§0.12). |
| **Tenants** | `AbpTenantManagement.Tenants` | `AbpTenants` | host | Kiracı kaydı, bağlantı dizesi diyaloğu (`TenantsConnectionString`), alt form olarak şubeler; yazma `list-form-dynamic-api/tenant-*`. Ekranların `IsTenant` davranışının kaynağı. |
| **Branches** | `App.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar. |
| **Setting Definitions** | `App.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions` (`List` tipinde değer → etiket; etiket dil anahtarıdır, Ayarlar ekranı `::` önekiyle çevirir, anahtar yoksa metin olduğu gibi görünür), `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/App.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. |
@ -1271,6 +1279,7 @@ soneklerini alır.
| **Data Source** | `App.DataSource` | `Sas_H_DataSource` | host | `Code`, `DataSourceType`, `ConnectionString`. **İki tür kayıt bir arada:** elle tanımlanan host/harici bağlantılar ve `Code = tenant adı` olan otomatik tenant bağlantıları (Tenants ekranındaki bağlantı diyaloğu yönetir, elle dokunma). Varsayılan kaydı `HostDataSeeder` appsettings'teki bağlantı cümlesinden kurar. Çözüm kuralı `lowcode` §9.3.3. Bağlantı dizesi koda/JSON'a yazılmaz. |
| **Background Workers › Recurring Jobs / Jobs** | `App.BackgroundWorkers.RecurringJobs` · `.Jobs` | `Sas_H_BackgroundWorker` · `Sas_H_BackgroundWorker_JobFlow` | host | Cron'lu işler ve çalıştırma geçmişi (`Tenant`, `ExecutionDate`, `Duration`, `Status`, `FailureReason`, `ExceptionDetails`). `WorkerType`/`Scope` değerleri §11'de; her kapsam ayrı bir Hangfire işidir, `Tenant` kolonu hangi kiracıda çalıştığını gösterir. Hazır işler (oturum temizliği, bildirim, yedek, şema sözlüğü) `data/App.BackgroundWorkers.RecurringJobs.json`'dan gelir. Ayrıntı: `lowcode` §9.10, `README.md` §14. |
| **Menus › Module / Routes / Menu / Manager** | `App.Menus.Module` · `.Routes` · `.Menu` · `.Manager` (`/admin/menuManager`) | `Sas_H_Module` · `Sas_H_Route` · `Sas_H_Menu` | host | Menü ve route veritabanından gelir; üretilen ekranın menü kaydı buradan açılır. `Menu` ekranı ağaç düzeninin referansıdır. Seed: `data/App.Menus.{Routes,Module,Menu}.json` — `Routes` dosyasına yalnızca var olan satırın `Seo*` alanları için dokunulur. |
| **Developer Kit › Dev Requests** | `App.DevRequests` | `Sas_H_DevRequest` | host | **Kanban/Todo düzeninin** referansı (`DefaultLayout: todo`): `Status`, `Priority`, `SortOrder`, `Tags`, `Assignees`, `SubTasks`, `Timesheets`; detayda `Description` yanında `Analysis` sekmesi (`AnalysisExpr`). MCP geliştirme döngüsünün kuyruğudur (`Backlog` → `Analysis` MCP analiz eder, `Ready to Start`'a kullanıcı alır; `Ready to Start` → `Testing`, `Staging`'e kullanıcı alır; araç çubuğunda DB Migrate butonu vardır — `UiEvalService.ApiDbMigrate()`, önce onay sorar, `App.Setup.Migrate`; yalnızca `start_dev_request` başlatır; açıklamadaki base64 görseller MCP'ye resim içeriği olarak gider, analiz `save_dev_request_analysis` ile yazılır, üretim notu `append_dev_request_note` ile `Analysis`'e eklenir — `Description` talebin yazıldığı hâliyle kalır; `lowcode.instructions.md` §0.11–§0.12). |
| **Developer Kit › SQL Query Manager** | `App.SqlQueryManager` · `/admin/sqlQueryManager` | `Sas_H_CrudEndpoint` (üretilen CRUD uçları) | — | View/procedure/function/tablo tasarımı ve çalıştırma, tablodan CRUD endpoint üretimi (`.CrudEndpoints` yetkisi); karar sırasının 2. adımı buradan başlar. |
| **Developer Kit › Files** | `App.Files` · `/admin/files` | Blob depolama | tenant | Dosya yöneticisi. Ekranın dosya alanı için ayrı depolama kurma; dosya/görsel editörleri ve Intranet Documents bu altyapıyı kullanır. |
| **Developer Kit › Custom Endpoints** | `App.DeveloperKit.CustomEndpoints` | `Sas_H_CustomEndpoint` | host | `Name`, `Url`, `Method`, `DataSourceCode`, `Sql`, `ParametersJson` (`Static`/`Query`/`Path`/`Body`), `PermissionsJson` (`Global`/`Role`/`User`) — parametreli SQL'i yetkili bir HTTP ucuna çevirir. |
@ -1279,7 +1288,7 @@ soneklerini alır.
| **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
| **Developer Kit › Intranet Widgets** | `App.DeveloperKit.IntranetWidgets` | `Sas_T_IntranetWidget` | tenant | Intranet panosunda hangi widget'ın hangi kolonda (`Column`), hangi sırada (`Order`) ve hangi yetkiyle (`PermissionName`) çizileceği; `SourceType` `Builtin` (koddaki `Code`) ya da `Custom` (`ComponentName` ile bir custom component). Seed: `data/App.DeveloperKit.IntranetWidgets.json`. |
| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `AllowedTools` (boşsa tüm araçlar, doluysa yalnızca seçilenler), `MaxRows`. Yetkiler: ekran `App.DeveloperKit.McpServers`, erişim ve okuma `.Invoke`, her değişiklik `.Write` (kiracıda satır yazma, host'ta ayrıca tablo/ekran/bileşen geliştirme) — arkasındaki ekran C/U/D, Wizard ve Custom Components yetkileri ayrıca istenir. Kiracı veri okur ve yazar, geliştirme yapamaz (`TenantTools`: şema araçları, `query_list`, `run_select`, `insert_row`/`update_row`/`delete_row`); ham SQL `App.SqlQueryManager` ister; MCP yetki vermez. Kayıtlar `data/App.DeveloperKit.McpServers.json`'a senkronlanır. Ayrıntı: `README.md` §13.9. |
| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/{kapsam}/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran, veri aynası ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır; onay diyaloğu ekranın tablosunu sorar, seçilirse `WizardTableCleaner` tabloyu scripti ve crud dosyasıyla düşürür — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. |
| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran, veri aynası ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır; onay diyaloğu ekranın tablosunu sorar, seçilirse `WizardTableCleaner` tabloyu scripti ve crud dosyasıyla düşürür — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. |
| **Forum Management › Categories / Topics / Posts** | `App.ForumManagement.Categories` · `.Topics` · `.Posts` | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Forumun yönetim tarafı: kategori (seed `data/App.ForumManagement.Categories.json`), konu (kategori lookup'ı, sayaçlar, sabitle/kilitle/çözüldü bayrakları), gönderi (konu lookup'ı, kabul edilen yanıt). Kullanıcı tarafı Administration'daki `/admin/forum`. |
> Menü kodu ile yetki adı **her zaman aynı değildir**: Wizard Manager'da kod
@ -1310,7 +1319,7 @@ ekranları ABP tablolarıdır, Licence/Dev Request host tablolarını okur.
| **Identity › Permission Groups / Permissions** | `Abp.Identity.PermissionGroups` · `Abp.Identity.Permissions` | `AbpPermissionGroups` · `AbpPermissions` | Yetki ağacı (tree düzeni). Yeni ekranın yetkileri burada görünür; `Modules` kolonu yetkiyi modüllere bağlar (pipe ile çoklu), `Order` kardeşler arası sıradır. Seed: `data/Abp.Identity.Permission{Groups,s}.json`. |
| **Identity › Claim Type** | `App.IdentityManagement.ClaimType` | `AbpClaimTypes` | Kullanıcıya ek alan (claim) tanımı: `ValueType`, `Regex`, `Required`, `IsStatic`. |
| **Identity › Roles** | `AbpIdentity.Roles` | `AbpRoles` | Rol yönetimi + `RolesPermission` diyaloğu; yeni ekranın yetkisi role buradan verilir. Yazma `list-form-dynamic-api/role-*`. |
| **Identity › Users** | `AbpIdentity.Users` · detay `/admin/users/detail/:userId` | `AbpUsers` | Kullanıcı yönetimi + `UsersPermission` diyaloğu, **KPI widget'ları** ve `list-form-dynamic-api/user-*` ile devredilen yazma. Kullanıcı `WorkHour`, `DepartmentId`, `JobPositionId`, `Gender` (`Male`/`Female`, `App.StaticLookup.*`) ve `HomeAddress` alanlarını liste ve formda taşır; kişisel ve kimlik bilgileri (`IdentityNumber`, `BirthDate`, `EducationLevel`, `Nationality`, `SskNo`…) detay ekranındadır. Detay sekmeleri karışmasın diye gruplanmıştır: **General** (iletişim + ikinci e-posta/telefon, ek bilgi, avatar, askerlik `MilitaryStatus`/`MilitaryPostponeEndDate`, engellilik `IsDisabled`/`DisabilityRate`/`DisabilityReport` — rapor base64, 5 MB), **Identity** (nüfus cüzdanı, doğum ve aile, uyruk Türkiye değilse yabancı uyruk `Passport*`/`WorkPermit*`), **Work** (organizasyon `RegistrationNo`/`SskNo`/`ShiftId`/`PdksCardNo`, sözleşme `EmploymentType`/`ContractType`/`ContractEndDate`/`ProbationEndDate`/`SeniorityStartDate`, çıkış `TerminationDate`/`TerminationReasonId`/`TerminationNote`, sürücü belgesi `DrivingLicense*` — sınıflar `|` ile, iletişim kanalları; altında iş deneyimi), **Education & Skills** (en yüksek `EducationLevel` + eğitim/dil/yetenek/belge alt listeleri), **Family / Emergency**, **Financial** (`BankName`, `Iban`, `SalaryType`, `GrossSalary`, `SalaryCurrency` + yan haklar), Health, Permissions, Lockout, Claims. Deneme süresi işe girişten en fazla dört ay sonra biter (`UpdateUserAsync` ve ekran doğrular). Ücret alanları `UserInfoViewModel`'de taşınmaz — o model intranet doğum günü listesine de akar; `GET {id}/financial` / `PUT financial` uçlarından okunur, yetki detayınkiyle aynıdır (`AbpIdentity.Users.Update`). Aynı sebeple `DisabilityReport` yalnızca `GetByIdAsync`'te doldurulur. `ShiftId`, `TerminationReasonId`, `SalaryCurrency` seçenekleri ekranda `list-form-select/lookup` ile bu ListForm'un alan lookup'larından okunur (`useUserFieldLookup`). Detaydaki **Sağlık Kartı** sekmesi `BloodType` ile soy geçmişini (`{Mother,Father,Sibling,Child}HealthStatus` — `Alive`/`Deceased`, `App.StaticLookup.*` — ve `…HealthNote`) ve öz geçmişini (`ChronicDisease`, `TetanusVaccination`, `HepatitisVaccination`, `OtherVaccination`) taşır; sekme dört gruptur (Soy/Öz Geçmişi, Laboratuvar - Fizik Muayene `Lab*`/`Exam*`, Anamnez `Complaint*`/`Diagnosis*` + 3-10. sorular, Sonuç `FitForWork*`/`ShiftWorkFit`/`NightWorkFit`). Sonuçtaki hekim (`FitnessDoctorId`, `ShiftNightDoctorId`) wizard tablosu `Hr_T_Doctor`'dan `LookupQueryValues.TenantDoctorValues` ile seçilir; diploma ve işyeri hekimliği belgesi hekimden gösterilir, kullanıcıya yazılmaz. Bu alanların hepsi listede gizli alandır; arayüz `user-management/HealthCardTab.tsx`. Kullanıcı seçen her lookup bu bilgileri `AbpUsers`'tan okur ve kopya kolon aynı ad/tip/değer kümesiyle açılır (`lowcode.instructions.md` §0.12 madde 5). Yeni bir kullanıcı alanı = `PlatformConsts.AbpIdentity.User` + `AbpIdentityUserExtensions` + EF eşlemesi ve migration + `UserInfoViewModel` (detay/profil) + `CreateUpdateUserInput` ve `user-insert`/`user-update` (liste) + `ListFormSeeder_Administration` alanı. |
| **Identity › Users** | `AbpIdentity.Users` · detay `/admin/users/detail/:userId` | `AbpUsers` | Kullanıcı yönetimi + `UsersPermission` diyaloğu, **KPI widget'ları** ve `list-form-dynamic-api/user-*` ile devredilen yazma. Kullanıcı `WorkHour`, `DepartmentId`, `JobPositionId`, `Gender` (`Male`/`Female`, `App.StaticLookup.*`) ve `HomeAddress` alanlarını liste ve formda taşır; kişisel ve kimlik bilgileri (`IdentityNumber`, `BirthDate`, `EducationLevel`, `Nationality`, `SskNo`…) detay ekranındadır. Detay sekmeleri karışmasın diye gruplanmıştır: **General** (iletişim + ikinci e-posta/telefon, ek bilgi, avatar, askerlik `MilitaryStatus`/`MilitaryPostponeEndDate`, engellilik `IsDisabled`/`DisabilityRate`/`DisabilityReport` — rapor base64, 5 MB), **Identity** (nüfus cüzdanı, doğum ve aile, uyruk Türkiye değilse yabancı uyruk `Passport*`/`WorkPermit*`), **Work** (organizasyon `RegistrationNo`/`SskNo`/`ShiftId`/`PdksCardNo`, sözleşme `EmploymentType`/`ContractType`/`ContractEndDate`/`ProbationEndDate`/`SeniorityStartDate`, çıkış `TerminationDate`/`TerminationReasonId`/`TerminationNote`, sürücü belgesi `DrivingLicense*` — sınıflar `|` ile, iletişim kanalları; altında iş deneyimi), **Education & Skills** (en yüksek `EducationLevel` + eğitim/dil/yetenek/belge alt listeleri), **Family / Emergency**, **Financial** (`BankName`, `Iban`, `SalaryType`, `GrossSalary`, `SalaryCurrency` + yan haklar), Health, Permissions, Lockout, Claims. Deneme süresi işe girişten en fazla dört ay sonra biter (`UpdateUserAsync` ve ekran doğrular). Ücret alanları `UserInfoViewModel`'de taşınmaz — o model intranet doğum günü listesine de akar; `GET {id}/financial` / `PUT financial` uçlarından okunur, yetki detayınkiyle aynıdır (`AbpIdentity.Users.Update`). Aynı sebeple `DisabilityReport` yalnızca `GetByIdAsync`'te doldurulur. `ShiftId`, `TerminationReasonId`, `SalaryCurrency` seçenekleri ekranda `list-form-select/lookup` ile bu ListForm'un alan lookup'larından okunur (`useUserFieldLookup`). Detaydaki **Sağlık Kartı** sekmesi `BloodType` ile soy geçmişini (`{Mother,Father,Sibling,Child}HealthStatus` — `Alive`/`Deceased`, `App.StaticLookup.*` — ve `…HealthNote`) ve öz geçmişini (`ChronicDisease`, `TetanusVaccination`, `HepatitisVaccination`, `OtherVaccination`) taşır; sekme dört gruptur (Soy/Öz Geçmişi, Laboratuvar - Fizik Muayene `Lab*`/`Exam*`, Anamnez `Complaint*`/`Diagnosis*` + 3-10. sorular, Sonuç `FitForWork*`/`ShiftWorkFit`/`NightWorkFit`). Sonuçtaki hekim (`FitnessDoctorId`, `ShiftNightDoctorId`) wizard tablosu `Hr_T_Doctor`'dan `LookupQueryValues.TenantDoctorValues` ile seçilir; diploma ve işyeri hekimliği belgesi hekimden gösterilir, kullanıcıya yazılmaz. Bu alanların hepsi listede gizli alandır; arayüz `user-management/HealthCardTab.tsx`. Kullanıcı seçen her lookup bu bilgileri `AbpUsers`'tan okur ve kopya kolon aynı ad/tip/değer kümesiyle açılır (`lowcode.instructions.md` §0.12 madde 5). Yeni bir kullanıcı alanı = `PlatformConsts.AbpIdentity.User` + `AbpIdentityUserExtensions` + EF eşlemesi ve migration + `UserInfoViewModel` (detay/profil) + `CreateUpdateUserInput` ve `user-insert`/`user-update` (liste) + `ListFormSeeder_Administration` alanı + gerekiyorsa yönetici varsayılanı (`PlatformConsts.AbpIdentity.User.Admin*DefaultValue` + `PlatformIdentityDataSeeder`; host ve tenant Seed sekmesinin yöneticisi buradan oluşur). |
| **Identity › Users › Work Experience** | `App.IdentityManagement.UserWorkExperience` (menüsüz alt liste) | `Adm_T_UserWorkExperience` | Kullanıcının daha önce çalıştığı yerler: `CompanyName`, `SectorId` (Sektörler lookup'ı, `LookupQueryValues.TenantSectorValues`), `StartDate`, `EndDate`, `JobDescription` (HtmlEditor). Entity `IMultiTenant`; kullanıcı detayının Work sekmesinde `PlatformViewHost` ile `UserId` filtresiyle gömülüdür, yeni satır `UserId`'yi filtreden alır. Kendi yetkisi yoktur: okuma `AbpIdentity.Users`, ekleme/güncelleme/silme detay sayfasının yetkisi `AbpIdentity.Users.Update`. |
| **Identity › Users › Health Card lists** | `App.IdentityManagement.UserPeriodicExamination` · `.UserPatientProtocol` · `.UserSickLeave` (menüsüz alt listeler) | `Adm_T_UserPeriodicExamination` · `Adm_T_UserPatientProtocol` · `Adm_T_UserSickLeave` | Periyodik muayene (hekim `Hr_T_Doctor`'dan, diploma no seçimde `EditorScript.Copy` ile kopyalanır), hasta protokol defteri (`ProtocolType`/`ProtocolResult` statik lookup) ve istirahat listesi (`Days` tarihlerden `EditorScript.Days` ile hesaplanır). Entity'ler `IMultiTenant`; Sağlık Kartı sekmesinde (`HealthCardTab.tsx`) `UserId` filtresiyle gömülüdür. Yetki Work Experience ile aynıdır: okuma `AbpIdentity.Users`, ekleme/güncelleme/silme `AbpIdentity.Users.Update`. |
| **Identity › Users › Education & Skills lists** | `App.IdentityManagement.UserEducation` · `.UserLanguage` · `.UserSkill` · `.UserCertificate` (menüsüz alt listeler) | `Adm_T_UserEducation` · `Adm_T_UserLanguage` · `Adm_T_UserSkill` · `Adm_T_UserCertificate` | Eğitim geçmişi (`EducationLevel` kullanıcınınkiyle aynı değerler, `SchoolName`, `Department`, `StartDate`, `EndDate`, `IsGraduated`), yabancı dil (`SkillId` → `Languages` tipindeki `Hr_T_Skill`, `SkillLevelId` dilin tipine ait `Hr_T_SkillLevel`, cascade), yetenek (`SkillId` → `Languages` dışındaki tipler, `Level` 1-5 statik lookup, check constraint) ve yeterlilik belgesi (`CertificateId` → wizard tablosu `Hr_T_Certificate`, EF ilişkisi yok; `CertificateNo`, `IssuedBy`, `IssueDate`, `ExpiryDate`). Lookup'lar `LookupQueryValues.Tenant{LanguageSkill,SkillLevelBySkill,NonLanguageSkill,Certificate}Values`. `EducationSkillsTab.tsx`; yetki Work Experience ile aynıdır. |
@ -1395,9 +1404,9 @@ listesinde `Hr` varsa görür (modül listesi boş kiracıda süzme yoktur — `
| **Recruitment › Application** | `App.Hr.JobApplication` (alt formlar `App.Hr.Interview`, `App.Hr.JobOffer`) | `Hr_T_JobApplication` | tenant | Aday × ilan başvurusu (çift tekil). `Stage`: `Applied` → `Screening` → `Interview` → `Offer` → `Hired` / `Rejected`; **Todo (kanban) görünümü** aşama sütunlarıyla. Mülakat eklenince `Interview`, teklif eklenince `Offer`, teklif `Accepted` olunca `Hired`'a ilerler (after-command SQL, geri gitmez; insert sonrası komutlar yalnızca eklenen satıra `@Id` ile çalışır). `CandidateName`/`PostingTitle` kopyalanır (kanban başlığı). Ret sebebi yalnızca `Rejected` aşamasında düzenlenir. Alt form olduğu için widget yok. Wizard. |
| **Recruitment › Interview** | `App.Hr.Interview` (alt form `App.Hr.InterviewScore`) | `Hr_T_Interview` · `Hr_T_InterviewScore` | tenant | Mülakat planlama: tür, başlangıç/bitiş (**Scheduler** görünümü), mülakatçı, durum, öneri. Kayıt eklenince mülakatçıya `IseAlimMulakatAtandi` bildirimi SQL ile yazılır (tipin `Custom` kuralları kanalı belirler, metin `App.Hr.Interview.Notification.*`). Puan alt formu kriter başına 1–5; `OverallScore` = Σ(puan × ağırlık) / Σ ağırlık, başvurunun `AverageScore`'u mülakatların ortalaması. Wizard. |
| **Recruitment › Job Offer** | `App.Hr.JobOffer` | `Hr_T_JobOffer` | tenant | Teklif: `OfferNo` (`TKL-…`), başvuru seçilince pozisyon bilgileri ilandan `copy` ile dolar, brüt ücret + para birimi, yan haklar (`Hr_T_BenefitType` kodları), `Status` (`Draft`/`Sent`/`Accepted`/`Declined`/`Expired`, yanıt tarihi otomatik). İç onay **Grid Workflow** (Kadro Talebi ile aynı akış). Onaylı satırda **Teklif Mektubu** butonu (`DynamicReport`, `Raporlar` kategorisi). Wizard. |
| **Performance Management** | `App.Hr.Performance` dalı (yalnız İK): `PerformancePeriod`, `PerformanceReview`, `PerformanceCalibration`, `DevelopmentPlan`; tanım `App.Hr.PerformanceRating` (İK › Tanımlar). Çalışana ait `MyPerformanceReview`, `TeamPerformanceReview`, `My360Feedback` menüsüzdür; Intranet › Dashboard'daki custom widget'lardan (`MyPerformanceWidget`, `TeamPerformanceWidget`, `My360FeedbackWidget`) açılır | `Hr_T_PerformanceRating` · `Hr_T_PerformancePeriod` · `Hr_T_PerformancePeriodSkill` · `Hr_T_PerformanceReview` · `Hr_T_PerformanceGoal` · `Hr_T_PerformanceCompetency` · `Hr_T_PerformanceFeedback` · `Hr_T_PerformanceFeedbackScore` · `Hr_T_DevelopmentPlan` | tenant | Dönem → değerlendirme (hedef KPI/OKR + yetkinlik `Hr_T_Skill`/`Hr_T_SkillLevel` + isteğe bağlı 360) → kalibrasyon → gelişim planı. **Aynı tabloya persona ekranları**: İK (tümü), çalışan (`EmployeeId = '@USERID'`), yönetici (`ManagerId = '@USERID'`), 360 değerlendiricisi (`ReviewerId = '@USERID'`); kilitli alanlar İK ekranının yetkisine bağlıdır (sunucu düşürür), satır sahipliği ve aşama kuralı `Hr_S_PerformanceGuard` (before-command). Hesap ve durum geçişi `Hr_S_PerformanceReviewRecalc` / `Hr_S_PerformanceReviewAfterSave`; tamamlanan değerlendirme `Adm_T_UserSkill`'e yazar. Widget'lar veriyi ekranın kendi sorgusundan (`list-form-select/select`) okur. Çalışma şekli ve kararlar: `configs/seeds/host/docs/modules/HrPerformance.md`. Wizard + Custom Component (pano kutusu). |
| **Leave Management** | `App.Hr.Leave` dalı (yalnız İK): `LeaveRequest` (Grid Workflow: Müdür Onayı `@ManagerUserNames` → İK Onayı; üst pozisyonu olmayan çalışanda müdür adımı `RequiresManagerApproval` karşılaştırmasıyla atlanır), `LeaveBalance` (alt formlar `LeaveBalanceAdjustment`, `LeaveBalanceYear`). Çalışana ait `MyLeaveRequest`, `MyLeaveBalance` ve müdüre ait `TeamLeaveRequest` menüsüzdür; Intranet › Dashboard'daki `MyLeaveWidget` / `TeamLeaveWidget`'tan açılır; kullanıcı detayında **İzinler** sekmesi | `Hr_T_LeaveRequest` · `Hr_T_LeaveBalanceAdjustment` · görünümler `Hr_V_LeaveBalanceYear` · `Hr_V_LeaveSummary` | tenant | Gün sayısı `Hr_F_LeaveWorkDays` (Ayarlar › İnsan Kaynakları › İzin çalışma günleri + `Hr_T_WorkCalendar`); bakiye İş Kanunu md.53 kademeleri (ayarlardan) + devir + düzeltme; müdür = departmanın en üst pozisyonunun sahibi. Doğrulama, müdür çözümü, rapor ↔ `Adm_T_UserSickLeave` eşlemesi ve bildirim `Hr_S_LeaveRequestAfterSave`; satır koruması `Hr_S_LeaveRequestGuard`; Sağlık Kartı tarafı `Hr_S_LeaveSickLeaveSync`. SQL'den ayar okuma `Hr_F_HrSetting` (kiracı → genel → varsayılan). Çalışma şekli: `configs/seeds/host/docs/modules/HrLeave.md`. Wizard + Custom Component + küçük UI/motor eki. |
| **Timesheet & Attendance** | `App.Hr.Attendance` dalı (yalnız İK): `AttendanceLog` (ham okutma; Excel import), `Timesheet` (çalışan × gün; düzenlenen gün `IsManual`), `TimesheetMonthly` (salt okunur aylık özet, alt form `Timesheet` `Id → PeriodKey`), `TimesheetPeriod` (ay yeniden hesap / kapanış). Çalışana ait `MyTimesheet` (+ alt form `MyTimesheetDay`) menüsüzdür; Intranet › Dashboard'daki `MyTimesheetWidget`'tan açılır; kullanıcı detayında **Puantaj** sekmesi | `Hr_T_AttendanceLog` · `Hr_T_Timesheet` · `Hr_T_TimesheetPeriod` · görünümler `Hr_V_AttendanceShift` · `Hr_V_TimesheetMonthly` | tenant | Hesap `Hr_S_TimesheetCalculate` (gün tipi: rapor > ücretsiz izin > `Hr_T_WorkCalendar` tatili > hafta tatili (İzin çalışma günü ayarları) > ücretli izin > okutma > eğitim katılımı (`Training`) > varsayılan çalışma / devamsız; izinli günde `LeaveTypeId`, eğitim gününde `TrainingId`; mola `MAX(vardiya, okutulan)`; geç/erken vardiya toleransı aşılırsa tamamı; fazla mesai haftalık 45 saati aşan kısım + tatil çalışması). Kişi eşlemesi `AbpUsers.PdksCardNo` / `RegistrationNo`; kartı olan çalışan ilk PDKS okutmasından itibaren okutmayla, diğerleri elle (kayıtsız gün `IsAssumed`) takip edilir. Tetikler: hareket kaydı `Hr_S_AttendanceProcess`, PDKS Custom Endpoint `Hr_S_AttendanceImport` (`/hr/attendance/`), dönem `Hr_S_TimesheetPeriodAfterSave`, gece işi `Hr_S_TimesheetNightly`, izin/rapor prosedürleri, `Hr_S_TrainingSync`, `Hr_S_WorkCalendarAfterSave`. Modüller arası çakışma kuralları (raporlu gün devamsız değildir, rapora denk gelen izin günü bakiyeye döner, kapalı aya izin/rapor yazılmaz) `HrAttendance.md` §7.1'dedir. Ayarlar `App.HrSettings.Attendance.*`. Çalışma şekli: `configs/seeds/host/docs/modules/HrAttendance.md`. Wizard + Custom Component + Custom Endpoint + zamanlanmış iş + küçük UI eki. |
| **Performance Management** | `App.Hr.Performance` dalı (yalnız İK): `PerformancePeriod`, `PerformanceReview`, `PerformanceCalibration`, `DevelopmentPlan`; tanım `App.Hr.PerformanceRating` (İK › Tanımlar). Çalışana ait `MyPerformanceReview`, `TeamPerformanceReview`, `My360Feedback` menüsüzdür; Intranet › Dashboard'daki custom widget'lardan (`MyPerformanceWidget`, `TeamPerformanceWidget`, `My360FeedbackWidget`) açılır | `Hr_T_PerformanceRating` · `Hr_T_PerformancePeriod` · `Hr_T_PerformancePeriodSkill` · `Hr_T_PerformanceReview` · `Hr_T_PerformanceGoal` · `Hr_T_PerformanceCompetency` · `Hr_T_PerformanceFeedback` · `Hr_T_PerformanceFeedbackScore` · `Hr_T_DevelopmentPlan` | tenant | Dönem → değerlendirme (hedef KPI/OKR + yetkinlik `Hr_T_Skill`/`Hr_T_SkillLevel` + isteğe bağlı 360) → kalibrasyon → gelişim planı. **Aynı tabloya persona ekranları**: İK (tümü), çalışan (`EmployeeId = '@USERID'`), yönetici (`ManagerId = '@USERID'`), 360 değerlendiricisi (`ReviewerId = '@USERID'`); kilitli alanlar İK ekranının yetkisine bağlıdır (sunucu düşürür), satır sahipliği ve aşama kuralı `Hr_S_PerformanceGuard` (before-command). Hesap ve durum geçişi `Hr_S_PerformanceReviewRecalc` / `Hr_S_PerformanceReviewAfterSave`; tamamlanan değerlendirme `Adm_T_UserSkill`'e yazar. Widget'lar veriyi ekranın kendi sorgusundan (`list-form-select/select`) okur. Çalışma şekli ve kararlar: `configs/seeds/docs/modules/HrPerformance.md`. Wizard + Custom Component (pano kutusu). |
| **Leave Management** | `App.Hr.Leave` dalı (yalnız İK): `LeaveRequest` (Grid Workflow: Müdür Onayı `@ManagerUserNames` → İK Onayı; üst pozisyonu olmayan çalışanda müdür adımı `RequiresManagerApproval` karşılaştırmasıyla atlanır), `LeaveBalance` (alt formlar `LeaveBalanceAdjustment`, `LeaveBalanceYear`). Çalışana ait `MyLeaveRequest`, `MyLeaveBalance` ve müdüre ait `TeamLeaveRequest` menüsüzdür; Intranet › Dashboard'daki `MyLeaveWidget` / `TeamLeaveWidget`'tan açılır; kullanıcı detayında **İzinler** sekmesi | `Hr_T_LeaveRequest` · `Hr_T_LeaveBalanceAdjustment` · görünümler `Hr_V_LeaveBalanceYear` · `Hr_V_LeaveSummary` | tenant | Gün sayısı `Hr_F_LeaveWorkDays` (Ayarlar › İnsan Kaynakları › İzin çalışma günleri + `Hr_T_WorkCalendar`); bakiye İş Kanunu md.53 kademeleri (ayarlardan) + devir + düzeltme; müdür = departmanın en üst pozisyonunun sahibi. Doğrulama, müdür çözümü, rapor ↔ `Adm_T_UserSickLeave` eşlemesi ve bildirim `Hr_S_LeaveRequestAfterSave`; satır koruması `Hr_S_LeaveRequestGuard`; Sağlık Kartı tarafı `Hr_S_LeaveSickLeaveSync`. SQL'den ayar okuma `Hr_F_HrSetting` (kiracı → genel → varsayılan). Çalışma şekli: `configs/seeds/docs/modules/HrLeave.md`. Wizard + Custom Component + küçük UI/motor eki. |
| **Timesheet & Attendance** | `App.Hr.Attendance` dalı (yalnız İK): `AttendanceLog` (ham okutma; Excel import), `Timesheet` (çalışan × gün; düzenlenen gün `IsManual`), `TimesheetMonthly` (salt okunur aylık özet, alt form `Timesheet` `Id → PeriodKey`), `TimesheetPeriod` (ay yeniden hesap / kapanış). Çalışana ait `MyTimesheet` (+ alt form `MyTimesheetDay`) menüsüzdür; Intranet › Dashboard'daki `MyTimesheetWidget`'tan açılır; kullanıcı detayında **Puantaj** sekmesi | `Hr_T_AttendanceLog` · `Hr_T_Timesheet` · `Hr_T_TimesheetPeriod` · görünümler `Hr_V_AttendanceShift` · `Hr_V_TimesheetMonthly` | tenant | Hesap `Hr_S_TimesheetCalculate` (gün tipi: rapor > ücretsiz izin > `Hr_T_WorkCalendar` tatili > hafta tatili (İzin çalışma günü ayarları) > ücretli izin > okutma > eğitim katılımı (`Training`) > varsayılan çalışma / devamsız; izinli günde `LeaveTypeId`, eğitim gününde `TrainingId`; mola `MAX(vardiya, okutulan)`; geç/erken vardiya toleransı aşılırsa tamamı; fazla mesai haftalık 45 saati aşan kısım + tatil çalışması). Kişi eşlemesi `AbpUsers.PdksCardNo` / `RegistrationNo`; kartı olan çalışan ilk PDKS okutmasından itibaren okutmayla, diğerleri elle (kayıtsız gün `IsAssumed`) takip edilir. Tetikler: hareket kaydı `Hr_S_AttendanceProcess`, PDKS Custom Endpoint `Hr_S_AttendanceImport` (`/hr/attendance/`), dönem `Hr_S_TimesheetPeriodAfterSave`, gece işi `Hr_S_TimesheetNightly`, izin/rapor prosedürleri, `Hr_S_TrainingSync`, `Hr_S_WorkCalendarAfterSave`. Modüller arası çakışma kuralları (raporlu gün devamsız değildir, rapora denk gelen izin günü bakiyeye döner, kapalı aya izin/rapor yazılmaz) `HrAttendance.md` §7.1'dedir. Ayarlar `App.HrSettings.Attendance.*`. Çalışma şekli: `configs/seeds/docs/modules/HrAttendance.md`. Wizard + Custom Component + Custom Endpoint + zamanlanmış iş + küçük UI eki. |
| **Org Chart** | `App.Hr.OrgChart` · `/admin/organization` | `Hr_T_Department` · `Hr_T_JobPosition` | tenant | Departman/ünvan verisinin organizasyon şeması görünümü (özel React sayfası `hr/OrgChart`, veri `api/app/org-chart`). Menü ve route aynı yetkiyi ister. |
| **Intranet › Announcement** | `App.Hr.Intranet.Announcement` (alt formlar Like/Comment) | `Hr_T_Announcement` · `Hr_T_AnnouncementLike` · `Hr_T_AnnouncementComment` | tenant | Duyuru + beğeni + yorum: **ana kayıt / alt etkileşim** kalıbının hazır örneği. |
| **Intranet › Survey** | `App.Hr.Intranet.Survey` (alt form Question → Option; Response → Answer) | `Hr_T_Survey` · `Hr_T_SurveyQuestion` · `Hr_T_SurveyQuestionOption` · `Hr_T_SurveyResponse` · `Hr_T_SurveyAnswer` | tenant | Anket motoru; soru–seçenek–yanıt zinciri **iki seviyeli alt form** ihtiyacının referansıdır. |
@ -1408,15 +1417,15 @@ listesinde `Hr` varsa görür (modül listesi boş kiracıda süzme yoktur — `
**Şube bilgisi:** `Department.BranchId` ve `JobPosition.BranchId` (`Guid`, **zorunlu** — kolon `NOT NULL`, formda
zorunlu alan; değer `Sas_T_Branch.Id`) şubeyi taşır; ekranlar `IsBranch = false`'tur (satırlar kullanıcının şube atamasına göre süzülmez, şube
sütunu veridir). Host varsayılan şubesinin kimliği sabittir
(`PlatformConsts.Branches.BranchIdDefaultValue`; `PlatformIdentityDataSeeder` /
`TenantIdentityDataSeeder` host şubesi için bu kimliği kullanır, kiracı şubeleri yeni GUID alır);
(`PlatformConsts.Branches.BranchIdDefaultValue`; `PlatformIdentityDataSeeder` host şubesi için bu
kimliği kullanır, kiracı şubeleri yeni GUID alır);
seed edilen departman ve pozisyon satırları `BranchId` ile bu şubeye bağlanır. Yeni seed satırı
eklerken `BranchId` de yazılır.
### 12.5 ListForm olmayan (özel React) ekranlar
Bu ekranlar `Sas_H_Route` kayıtlarıyla yönetilir (seed:
`configs/seeds/host/data/App.Menus.Routes.json`). Yenisini eklemek **kod yazmak demektir** — karar
`configs/seeds/data/App.Menus.Routes.json`). Yenisini eklemek **kod yazmak demektir** — karar
sırasının son adımıdır ve çerçeve dışıdır (onay ister, `lowcode` §0.8). Rotalı bir sayfa ihtiyacının
çerçeve içi karşılığı `RoutePath` taşıyan bir custom component'tir.
@ -1515,9 +1524,9 @@ alanları da doldurur (`lowcode` §0.9).
| "izin türü, masraf türü, yan hak türü, vardiya, resmi tatil / çalışma takvimi" | İK › Definitions · İK › Work Calendar · İK › Shift | Var olan `Hr_T_WorkCalendar` / `Hr_T_LeaveType` / `Hr_T_ExpenseType` / `Hr_T_BenefitType` / `Hr_T_Shift` tablosuna lookup bağla; yenisini üretme |
| "eğitim / katılımcı, araç / şoför / araç masrafı / araç çıkış-giriş, olay günlüğü, disiplin / tutanak / savunma / ihtar" | İK › Trainings · İK › Vehicles · İK › Event Log · İK › Disciplinary Case | Var olan `Hr_T_Training*` / `Hr_T_Vehicle*` / `Hr_T_Driver*` / `Hr_T_EventLog` / `Hr_T_Disciplinary*` ekranını genişlet ya da lookup bağla; paralel tablo üretme |
| "işe alım, kadro talebi, ilan, aday / CV, mülakat, teklif" | İK › Recruitment | Var olan `Hr_T_StaffRequest` / `Hr_T_JobPosting` / `Hr_T_Candidate` / `Hr_T_JobApplication` / `Hr_T_Interview` / `Hr_T_JobOffer` ekranını genişlet; personel kullanıcısını BT elle açar, aday kaydına `UserId` ile bağlanır |
| "performans değerlendirme, hedef / KPI / OKR, yetkinlik değerlendirmesi, 360, kalibrasyon, gelişim planı" | İK › Performance Management | Var olan `Hr_T_Performance*` / `Hr_T_DevelopmentPlan` ekranını genişlet; yeni persona ekranı aynı tabloya `DefaultFilter` + `Hr_S_PerformanceGuard` ile açılır (`configs/seeds/host/docs/modules/HrPerformance.md`) |
| "izin talebi, izin onayı, izin bakiyesi, yıllık izin hakkı, rapor/istirahat izni" | İK › Leave Management | Var olan `Hr_T_LeaveRequest` ekranını genişlet; persona ekranı aynı tabloya `DefaultFilter` + `Hr_S_LeaveRequestGuard` ile açılır, bakiye `Hr_V_LeaveSummary`'den okunur (`configs/seeds/host/docs/modules/HrLeave.md`) |
| "puantaj, devam takibi, PDKS, giriş-çıkış, geç kalma, fazla mesai, devamsızlık" | İK › Timesheet & Attendance | Var olan `Hr_T_AttendanceLog` / `Hr_T_Timesheet` ekranlarını genişlet; hesap `Hr_S_TimesheetCalculate`'te tek yerdedir, kural değişikliği oraya yazılır (`configs/seeds/host/docs/modules/HrAttendance.md`) |
| "performans değerlendirme, hedef / KPI / OKR, yetkinlik değerlendirmesi, 360, kalibrasyon, gelişim planı" | İK › Performance Management | Var olan `Hr_T_Performance*` / `Hr_T_DevelopmentPlan` ekranını genişlet; yeni persona ekranı aynı tabloya `DefaultFilter` + `Hr_S_PerformanceGuard` ile açılır (`configs/seeds/docs/modules/HrPerformance.md`) |
| "izin talebi, izin onayı, izin bakiyesi, yıllık izin hakkı, rapor/istirahat izni" | İK › Leave Management | Var olan `Hr_T_LeaveRequest` ekranını genişlet; persona ekranı aynı tabloya `DefaultFilter` + `Hr_S_LeaveRequestGuard` ile açılır, bakiye `Hr_V_LeaveSummary`'den okunur (`configs/seeds/docs/modules/HrLeave.md`) |
| "puantaj, devam takibi, PDKS, giriş-çıkış, geç kalma, fazla mesai, devamsızlık" | İK › Timesheet & Attendance | Var olan `Hr_T_AttendanceLog` / `Hr_T_Timesheet` ekranlarını genişlet; hesap `Hr_S_TimesheetCalculate`'te tek yerdedir, kural değişikliği oraya yazılır (`configs/seeds/docs/modules/HrAttendance.md`) |
| "yapay zekâ cevaplasın" | Saas › Definitions › AiBot | Bot tanımı; `/admin/ai` bu tanımı kullanır |
### 12.7 Canlı referans uygulamalar

View file

@ -33,7 +33,7 @@ Bir istek geldiğinde çekirdek koda dokunmak değil, `configs/seeds/` altına u
Kullanıcı "şöyle bir ekran/form/süreç istiyorum" dediğinde **uygulamayı kullanıyormuş gibi**
davran: Wizard'ın, Custom Components ekranının ve SQL Query Manager'ın diske yazacağı dosyaların
aynılarını `configs/seeds/host/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur.
aynılarını `configs/seeds/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur.
Talep iki biçimde gelir ve ikisi de aynı hatta girer:
@ -47,10 +47,11 @@ ve onay, en son dosyalar.**
### 0.1 Yazılacak klasörler
```
configs/seeds/host/
configs/seeds/
├── sql/object/{Nesne}.sql ← tablo / view / fonksiyon / prosedür (SQL Server)
├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon)
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
├── {sql|postgres}/host/object|execute/… ← yalnızca host veritabanında çalışan script (ör. HangFire'a dayanan view)
├── crud/{EntityName}.json ← tablonun REST uçları
├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları (List yolunda sunucu yazar, §4.1)
├── docs/modules/{Modül}.md ← modül dokümanı; iskelet + envanteri deploy yazar (§0.13)
@ -60,7 +61,8 @@ configs/seeds/host/
│ data/App.DeveloperKit.CustomComponents/{Id}.json
```
Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır.
Tenant'a özel seed klasörü yoktur: host ve kendi veritabanı olan her tenant aynı dosyalarla
kurulur; host veritabanını paylaşan tenant'ta dosyalar yeniden uygulanmaz (`SeedFileScope`).
### 0.2 Üretim sırası — bu sırayla yaz, bu sırayla çalışır
@ -487,7 +489,7 @@ ya da ziyaretçiye açık yeni bir fiziksel sayfa ürettiğinde.
açıklama, paylaşım görseli, Twitter hesabı, diller, robots yönergesi, Google doğrulaması,
sosyal hesaplar, varsayılan anahtar kelimeler (`DefaultKeywords`), sitemap anahtarı (`SitemapEnabled`) ve
sitemap dışı yollar (`SitemapExcludePaths`) `App.SiteManagement.Seo.*` ayarlarındadır
(`configs/seeds/host/data/App.SettingDefinitions.json`). Bir değer bütün site için geçerliyse
(`configs/seeds/data/App.SettingDefinitions.json`). Bir değer bütün site için geçerliyse
oraya yazılır, her yola tekrarlanmaz. Site kökü (`/`) hiçbir kayıt taşımaz; doğrudan bu
varsayılanlardan beslenir.
@ -539,8 +541,8 @@ eklenen dosya) bu satırlar ekranla birlikte seed edilir; migrate çalıştığ
içerikli satır dosyada varsa (tekrar deploy) yeniden eklenmez.
2. DB Migrate'i bekleme. Migrate wizard seeder ile ekranı kurar, ardından `ListFormSeedDataApplier`
dosyadaki satırları ekler (anahtarı olmayanı ekler, var olana dokunmaz).
3. Migrate'ten sonra ekrandan girilen satırlar `ListFormSeedDataSynchronizer` ile aynı dosyaya yazılır
(kiracı bağlamında `tenants/{TenantId}/data/`).
3. Migrate'ten sonra host bağlamında ekrandan girilen satırlar `ListFormSeedDataSynchronizer` ile aynı
dosyaya yazılır; kiracı bağlamındaki kayıt dosyaya yansımaz.
Kurallar:
- Dosyayı elle yazma; `seedRows` alan adlarını ekranın alanlarına göre doğrular, bilinmeyen alanı reddeder.
@ -805,7 +807,7 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma.
ihtiyaç, §0.8, adıyla). Talebi daraltan geçici çözüm önerilmez; "Hayır" taşıyan talep yetenek
eklenene kadar `Ready to Start`'a alınmaz.
### 0.13 Modül dokümanı — `configs/seeds/{kapsam}/docs/modules/{Modül}.md`
### 0.13 Modül dokümanı — `configs/seeds/docs/modules/{Modül}.md`
Üretilen her modül kendi çalışma şeklini bir md dosyasında taşır; "bu ekran nasıl çalışıyor / şu
puan nereden geliyor" sorusu kod ve seed taranmadan bu dosyadan cevaplanır (MCP'de `get_module_doc`).
@ -813,9 +815,8 @@ Doküman **wizard'ın parçasıdır**: Wizard ekranından ya da MCP `deploy_wiza
modülde sunucu onu kendisi açar ve güncel tutar; anlatım bölümlerini üreten kişi/AI doldurur.
Wizard dışı (entity, özel React) modüller dokümanı elle yazar.
- **Yer:** kapsam seed klasörü — `configs/seeds/host/docs/modules/{Modül}.md` (kiracı kapsamında
`tenants/{tenantId}/docs/modules`). Sunucu yalnızca seed köküne yazabildiği için depo kökünde
değildir. Wizard export zip'i dokümanı taşır, import yazar (`docs/modules/*.md`).
- **Yer:** seed klasörü — `configs/seeds/docs/modules/{Modül}.md`. Sunucu yalnızca seed köküne
yazabildiği için depo kökünde değildir. Wizard export zip'i dokümanı taşır, import yazar (`docs/modules/*.md`).
- **Hangi wizard hangi dokümana** (`ModuleDocCatalog`; wizard sözleşmesine alan eklenmez):
1. Dokümanın ilk satırındaki beyan ekranı sahipleniyorsa o doküman:
`<!-- module-doc: claims=App.Hr.CandidateSource, App.Hr.RejectionReason -->` (başka dalda
@ -914,8 +915,7 @@ Tutarlılık zorunludur; menü, route, yetki ve dil anahtarı aynı kökten tür
| CRUD endpoint seed | `crud/{EntityName}.json` | `Mrp_T_Order.json` |
| Liste verisi seed | `data/{ListFormCode}.json` (tek dosya) ya da `data/{ListFormCode}/{anahtar}.json` (klasör) — §9.15 | `App.Definitions.Sequence.json` |
Kapsam klasörü: host bağlamında `configs/seeds/host/…`, tenant bağlamında
`configs/seeds/tenants/{tenantId}/…`. Çözüm her zaman `SeedPathResolver` üzerinden yapılır;
Seed klasörü tektir: `configs/seeds/…`. Çözüm her zaman `SeedPathResolver` üzerinden yapılır;
yol elle kurulmaz.
---
@ -1015,7 +1015,7 @@ Her editörün kendine özgü seçenekleri için §4.3.2.
## 4. Artefakt A — ListForm ekranı (Wizard seed dosyası)
Konum: `configs/seeds/{kapsam}/wizard/{WizardName}.json` (dosya adında tarih yoktur)
Konum: `configs/seeds/wizard/{WizardName}.json` (dosya adında tarih yoktur)
Okuyan: `WizardDataSeeder` (migrate) · Yazan: `ListFormWizardAppService` (deploy) ve `WizardSeedSynchronizer`
(ListForm editöründen kayıt) · Okuma/yazma her zaman `WizardSeedFileDto.FromSeedJson` /
`ToSeedJson` ile — düz `JsonSerializer` bloklu bölümleri düşürür.
@ -1046,7 +1046,8 @@ Kurulum **DB Migrate**'tedir (Wizard Manager'daki buton, `POST /api/setup/migrat
`WizardDataSeeder` ekrana özgü kayıtları kurar (veri kaynağı, ListForm, alanlar, iş akışı), ardından
`ListFormSeedDataApplier` menü/yetki/dil satırlarını ekler (eksik olanı ekler, var olana dokunmaz).
**Yetkiler hiçbir role otomatik atanmaz** — admin dahil; ekranı hangi rolün göreceği rol ekranından
seçilir.
seçilir. Tek istisna yeni kiracının yöneticisidir: ilk seed'de, o anda tanımlı olan kiracı tarafı
yetkilerin hepsini alır (`PlatformIdentityDataSeeder`).
Deploy'dan migrate'e kadar wizard'ın hiçbir kaydı veritabanında yoktur ve bu beklenen durumdur.
Yeni modül, kök/üst menü, izin grubu, yetki ya da dil anahtarı veritabanında bulunmuyorsa
@ -2419,7 +2420,7 @@ ardından `ListFormSeedDataApplier` menü/yetki/dil satırlarını kurar (§4.0)
6. Deploy + tamamlandı
Başarıda sihirbaz "Migrate bekliyor" bildirimiyle Wizard Manager'a döner (ekran henüz kurulu
değildir; DB Migrate ile kurulur) ve seed dosyası `configs/seeds/{kapsam}/wizard/` altına yazılmış
değildir; DB Migrate ile kurulur) ve seed dosyası `configs/seeds/wizard/` altına yazılmış
olur. **Düzenleme modu** Wizard Manager'dan `EditFileName` ile açılır: aynı adımlar dolu gelir,
kaydetmede dosyalar cevaptan yeniden yazılır; önceki sürümün kayıtları `Pending.Replaced`'a alınır,
veritabanına dokunulmaz — kaldırma ve yeniden kurulum DB Migrate'tedir (§4.0).
@ -2814,7 +2815,7 @@ ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**,
### 7.1 Seed dosyası
`configs/seeds/{kapsam}/data/App.DeveloperKit.CustomComponents/{Id}.json` — Custom Components
`configs/seeds/data/App.DeveloperKit.CustomComponents/{Id}.json` — Custom Components
ekranının veri aynası, **klasör modunda** (§9.15): her bileşen kendi dosyasındadır, dosya adı
satırın `Id`'sidir. Dosya yine liste verisi zarfını taşır; `Rows` tek satırdır. Listeden ve Visual
Designer'dan (`CustomComponentAppService`) yapılan her kayıt bu klasöre yansır.
@ -3514,7 +3515,7 @@ tabloları için yalnız `T`/`D` üretir.)
1. Üretilen SQL hedef veri kaynağında çalıştırılır (yeni tabloda `CREATE`, düzenleme modunda
**`ALTER` diff'i**).
2. Seed dosyası kaydedilir: her zaman **tam `CREATE TABLE` script'i** (diff değil),
`save-table-script` ucu ile `configs/seeds/{kapsam}/{sql|postgres}/object/{TabloAdı}.sql`
`save-table-script` ucu ile `configs/seeds/{sql|postgres}/object/{TabloAdı}.sql`
altına (`SeedPathResolver`). Dosya kaydı başarısız olsa deploy geri alınmaz — log'a düşer.
3. CRUD adımında işaretlendiyse endpoint'ler üretilir (`crud/{Entity}.json`); onay kutusu
**varsayılan kapalıdır**, Custom Component'in kullanacağı tabloda açılır.
@ -3575,7 +3576,7 @@ tasarımcıyı kaybettirir.
> **Kalıcılık — kritik ayrım:** diske otomatik seed yazan tek tasarımcı **tablo
> tasarımcısıdır**. View Designer'ın deploy'u, sorgu editöründen çalıştırılan
> `CREATE PROCEDURE`/`CREATE FUNCTION` ve her türlü elle DDL **yalnızca veritabanına** gider.
> Bu nesnelerin `configs/seeds/{kapsam}/{sql|postgres}/object/{Ad}.sql` dosyasını **sen
> Bu nesnelerin `configs/seeds/{sql|postgres}/object/{Ad}.sql` dosyasını **sen
> yazarsın** — prompt ile üretimde zaten dosyayı doğrudan yazdığın için bu senin doğal yolun;
> uygulama içinden üretim yapıldıysa dosyaya alınmamış nesne veritabanı sıfırlanınca kaybolur.
@ -3642,7 +3643,7 @@ Yeni bir entity/tablo `sql/object/` altına yazılır ve veritabanı `SqlDataSee
dosyalardan kurulur. Mekanik:
1. **Yalnızca aktif sağlayıcının klasörü çalışır.** `DefaultDatabaseProvider` SQL Server ise
`{kapsam}/sql/`, PostgreSQL ise `{kapsam}/postgres/`. İki dosya da yazmak taşınabilirlik
`sql/`, PostgreSQL ise `postgres/`. İki dosya da yazmak taşınabilirlik
içindir; aynı anda ikisi birden çalışmaz.
2. Sıra: `object/` → `execute/`, her klasörün içinde **dosya adına göre alfabetik**. Bir tablo
başka bir tabloya FK ile bağlıysa dosya adları o sırayı vermelidir (ör. `Hr_T_Employee.sql`
@ -3698,9 +3699,15 @@ konfigürasyon ekranlarını (9.9–9.14) ve liste verisi seed dosyasını (9.15
### 9.1 CRUD Endpoint seed dosyası
Konum: `configs/seeds/{kapsam}/crud/{EntityName}.json` · Okuyan: `CrudEndpointSeedApplier`
Konum: `configs/seeds/crud/{EntityName}.json` · Okuyan: `CrudEndpointSeedApplier`
(migrate'te `CrudDataSeeder`, import kapanışında `CompleteImport` üzerinden — kural tek yerdedir)
Çalışma zamanı sınırı (`CrudEndpointAppService` + `DynamicEntityManager`): uçlar **giriş ister**
(anonim/public sayfadan çağrılamaz). Tabloda
`TenantId` varsa okuma, güncelleme ve silme isteği yapanın kiracısıyla sınırlıdır (host için
`TenantId IS NULL`), ekleme `TenantId`'yi sunucudan yazar; istemcinin gönderdiği `Id`/`TenantId` yok sayılır.
`PasswordHash` ve `SecurityStamp` yanıtta dönmez ve yazılamaz.
```jsonc
{
"EntityName": "Mrp_T_Order",
@ -3815,7 +3822,7 @@ Endpoint **tanımlarını** düzenleme yetkileri ListForm ekranının
`App.DeveloperKit.CustomEndpoints{,.Create,.Update,.Delete}` yetkileridir — çağırma
yetkilerinden (`.Get/.Post/.Put/.Remove` + `PermissionsJson`) ayrıdır.
**Seed:** `configs/seeds/{kapsam}/data/App.DeveloperKit.CustomEndpoints.json` (§9.15). Ekranın
**Seed:** `configs/seeds/data/App.DeveloperKit.CustomEndpoints.json` (§9.15). Ekranın
`SeedSyncInsert/Update/Delete` bayrakları **açıktır**: ekrandan eklenen/değiştirilen/silinen kayıt
dosyaya yansır, migrate'te eksikse geri eklenir. Prompt ile üretimde satırı bu dosyaya yaz
(`Id` sabit GUID; `ParametersJson`/`PermissionsJson` kaçışlanmış string).
@ -4027,7 +4034,7 @@ Editör davranışları:
Kalıcılık kuralı (§8.2'deki uyarının özeti): **seed dosyasını otomatik yazan tek akış tablo
tasarımcısının deploy'udur.** View Designer deploy'u ve editörden çalıştırılan her DDL
(view/SP/function) yalnızca veritabanına gider; kalıcı olması için
`configs/seeds/{kapsam}/{sql|postgres}/{object|execute}/{ad}.sql` dosyası ayrıca yazılır.
`configs/seeds/{sql|postgres}/{object|execute}/{ad}.sql` dosyası ayrıca yazılır.
Prompt ile üretimde bu ayrım seni etkilemez — dosyayı zaten sen yazarsın (§0.2 sırası) —
ama "uygulamadan ürettim, seed'i var mı?" sorusunun cevabı budur.
@ -4630,7 +4637,7 @@ Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep fo
üretilecek dosya seti:
```
configs/seeds/host/
configs/seeds/
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
├── data/App.DeveloperKit.CustomComponents/{Id}.json ← yalnızca serbest yerleşim isteniyorsa
@ -4698,13 +4705,13 @@ tedarikçi lookup'ı + iki kademeli onay).
veri aynası ve `InsertedRecords` içindeki kullanılmayan kayıtlar gider; tablo yalnızca
onayda seçilirse (MCP: `dropTables: true`) scripti ve crud dosyasıyla düşer (§4.5).
Dosyayı diskten silmek kayıtları temizlemez.
- [ ] Modül dokümanı (`configs/seeds/{kapsam}/docs/modules/{Modül}.md`) yazıldı ya da güncellendi; analiz/kararlar,
- [ ] Modül dokümanı (`configs/seeds/docs/modules/{Modül}.md`) yazıldı ya da güncellendi; analiz/kararlar,
ekranlar, hesaplar ve yetkiler üretilen artefaktların gerçek adlarıyla yazılı, README §10.5
dizininde satırı var (§0.13).
Prompt ile üretim yaptıysan ek olarak:
- [ ] Dosyalar doğru kapsam klasöründe (`host/` ya da `tenants/{tenantId}/`).
- [ ] Dosyalar `configs/seeds/` altındaki doğru klasörde (§0.1).
- [ ] Üretim sırası doğru (§0.2) ve bağımlı wizard'ın `SeededAt` damgası daha büyük.
- [ ] Tablo/kolon adları uydurulmadı; ya var olan nesneden okundu ya da `sql/object/` altında
üretildi.

214
README.md
View file

@ -155,7 +155,6 @@ sozsoft-platform/
│ │ ├── Sozsoft.Platform.Application/ # Uygulama servisleri (iş mantığı)
│ │ ├── Sozsoft.Platform.EntityFrameworkCore/ # DbContext, migration'lar, dinamik veri katmanı
│ │ ├── Sozsoft.Platform.HttpApi/ # Controller'lar (conventional)
│ │ ├── Sozsoft.Platform.HttpApi.Client/ # HTTP istemci proxy'leri
│ │ ├── Sozsoft.Platform.HttpApi.Host/ # Host: Swagger, auth, Hangfire, SignalR, raporlama
│ │ └── Sozsoft.Platform.DbMigrator/ # Migration + seed uygulaması
│ ├── modules/ # Bağımsız ABP modülleri
@ -179,9 +178,8 @@ sozsoft-platform/
├── configs/
│ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler
│ ├── deployment/ # Dev/Prod compose dosyaları, nginx/redis/turn konfigürasyonu, deploy scriptleri
│ ├── seeds/ # Runtime'da düzenlenebilen seed'ler; CDN ile aynı kapsam düzeni
│ │ # host/{crud,custom,wizard,sql,postgres,docs/modules}
│ │ # tenants/{tenantId}/{crud,custom,wizard,sql,postgres}
│ ├── seeds/ # Runtime'da düzenlenebilen seed'ler; host ve tenant'lar için tek klasör
│ │ # {crud,wizard,data,docs/modules}
│ │ # {sql|postgres}/{object,execute}
│ └── ai/ # AI entegrasyonu (n8n akış tanımları, Ollama model kurulum notları)
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları)
@ -244,32 +242,50 @@ host ayarları, ülke/şehir/ilçe verileri ile seed edilmiş ListForm ekranlar
(kodla versiyonlanan sabit seed'ler `api/src/Sozsoft.Platform.DbMigrator/Migrations/`,
runtime'da düzenlenebilen seed'ler `configs/seeds/` altındadır — bkz. `App:SeedsPath`).
`configs/seeds` altında en üst seviye kapsamdır (CDN klasörleriyle aynı mantık): host
veritabanı için `host/`, her tenant için `tenants/{tenantId}/`. Kapsamın altında içerik
türüne göre `crud/`, `wizard/`, `data/`, `sql/`, `postgres/` klasörleri bulunur:
**Tek tenant seed'i:** Tenants ekranındaki bağlantı diyaloğunun Seed sekmesi
`POST /api/setup/tenants/{id}/seed` ucunu çağırır (`AbpTenantManagement.Tenants.Update`, yalnızca host
kullanıcısı). Uç DbMigrator'ü migrate ile aynı kilit ve SSE akışıyla çalıştırır; `TenantSeed:TenantId`,
`TenantSeed:AdminEmail`, `TenantSeed:AdminPassword` ortam değişkeniyle geçer (`TenantSeedOptions`) ve
`PlatformDbMigrationService` yalnızca o tenant'ın şema + seed + `execute` adımlarını yürütür. Loglar
DB Migrate'teki `DbMigrateLogPanel` ile gösterilir.
`configs/seeds` tek klasördür; tenant'a özel bir seed kapsamı yoktur. Tek bir `PlatformDbContext`
olduğu için kendi veritabanı olan tenant ekran tanımlarını, menüleri, yetkileri ve wizard
tablolarını kendi veritabanından okur. Bu yüzden host ve kendi veritabanı olan her tenant aynı
dosyalarla kurulur. Host veritabanını paylaşan (bağlantı cümlesi olmayan) tenant'ta dosyalar
yeniden uygulanmaz, çünkü host seed'i onları aynı veritabanına zaten yazmıştır. Karar
`SeedFileScope`'tadır: migrate kararı `DataSeedContext`'e yazar, dosya okuyan her seeder
(`SqlDataSeeder`, `CrudDataSeeder`, `WizardDataSeeder`, `ListFormSeedDataApplier`, `FinalSeeder`)
aynı kararı uygular. İçerik türüne göre klasörler:
```
configs/seeds/
host/
crud/ wizard/ data/
sql/ { object/ execute/ }
postgres/ { object/ execute/ }
tenants/
{tenantId}/
crud/ wizard/ data/
sql/ { object/ execute/ }
postgres/ { object/ execute/ }
crud/ wizard/ data/ docs/
sql/ { object/ execute/ host/{ object/ execute/ } }
postgres/ { object/ execute/ host/{ object/ execute/ } }
```
Dosyalar ürünün varsayılanıdır ve **yalnızca host bağlamında yazılır**. Tenant bağlamındaki bir
kayıt veritabanında kalır, seed dosyasına yansımaz; yansısaydı bir tenant'ın verisi bütün
veritabanlarına yayılırdı. Bunu `ListFormSeedDataSynchronizer` ve `WizardSeedSynchronizer`
uygular. Wizard, SQL Query Manager, CRUD Endpoint ve Custom Components yetkileri zaten yalnızca
host tarafındadır.
Bekleyen wizard kurulumu (`Pending`) dosyaları uygulayan **son** veritabanında kapanır. Tam
migrate'te bu, kendi veritabanı olan son tenant'tır; öyle bir tenant yoksa host'tur. Host'ta
kapansaydı, ekranın eski sürümü kurulu olan tenant ekranı "kurulu" sayıp güncellemezdi. Tek
tenant seed'i (Tenants › Seed) bayrağı kapatmaz.
Aktif veritabanı sağlayıcısına göre `sql/` (SQL Server) ya da `postgres/` okunur. Her
`.sql` dosyası amacını belirten bir klasörün altındadır; klasör köklerinde dosya bulunmaz:
| Klasör | Ne zaman çalışır |
| --- | --- |
| `{sql\|postgres}/object` | Seed sırasında nesneyi oluşturur/günceller. SqlTableDesigner ve SqlQueryManager, deploy ettiği script'i çalıştığı kapsamın bu klasörüne yazar (host bağlamı → `host/`, tenant seçiliyken → `tenants/{tenantId}/`). |
| `{sql\|postgres}/object` | Seed sırasında nesneyi oluşturur/günceller. SqlTableDesigner ve SqlQueryManager, deploy ettiği script'i bu klasöre yazar. |
| `{sql\|postgres}/execute` | Nesneyi oluşturur; ayrıca tüm migration ve seed'ler bittikten sonra `FinalSeeder` dosya adından türettiği procedure'ü çalıştırır (dosya adı = procedure adı, `schema.Procedure.sql` de olur). |
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` kendi kapsamındaki dosyaları okur. |
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları `ICrudEndpointSeedApplier` üzerinden uygular. Politika **damga karşılaştırmalıdır**: eksik endpoint her zaman eklenir, mevcut bir kayıt yalnızca dosyanın `GeneratedAt` damgası kaydın `LastModificationTime`/`CreationTime` değerinden **yeniyse** güncellenir ya da silinir; aksi halde kayıt korunur ve log'a drift uyarısı düşer. Böylece CRUD Endpoint Manager'dan yapılan bir revizyon depodan gelen eski bir dosya yüzünden migrate sırasında geri alınmaz. Damgası olmayan (eski biçimde yazılmış) dosya hiçbir zaman yeni sayılmaz. |
| `{sql\|postgres}/host/{object\|execute}` | Aynı iki kural, **yalnızca host veritabanında**. Tenant veritabanında karşılığı olmayan nesnelere dayanan script buraya konur — örneğin HangFire tabloları tenant migrate'inde de yalnızca host'a kurulduğu için `Sas_H_BackgroundWorker_JobFlow` view'ı buradadır. Sıra: `object` → `host/object` → `execute` → `host/execute`. SqlTableDesigner ve SqlQueryManager her zaman `object/`'e yazar; yalnızca host'ta çalışması gereken script File Manager'dan buraya taşınır. Wizard export/import bu klasörü de taşır. |
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` okur. |
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` dosyaları `ICrudEndpointSeedApplier` üzerinden uygular. Politika **damga karşılaştırmalıdır**: eksik endpoint her zaman eklenir, mevcut bir kayıt yalnızca dosyanın `GeneratedAt` damgası kaydın `LastModificationTime`/`CreationTime` değerinden **yeniyse** güncellenir ya da silinir; aksi halde kayıt korunur ve log'a drift uyarısı düşer. Böylece CRUD Endpoint Manager'dan yapılan bir revizyon depodan gelen eski bir dosya yüzünden migrate sırasında geri alınmaz. Damgası olmayan (eski biçimde yazılmış) dosya hiçbir zaman yeni sayılmaz. |
| `data` | Ekranlardan girilen listelerin veri aynası: tek dosya (`{ListFormCode}.json`) ya da satır başına dosya tutan klasör (`{ListFormCode}/{anahtar}.json`; klasör özyinelemeli taranır ve tamamı tek unit of work'te uygulanır). `ListForm.SeedFilePath` dolu olan ekranda `SeedSyncInsert/Update/Delete` bayraklarından işaretli olan işlemler dosyaya yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında bütün contributor'lar bittikten sonra `ListFormSeedDataApplier` dosyayı geri uygular: anahtarı veritabanında **olmayan** satırları ekler, var olanlara dokunmaz. Aynı uygulayıcı wizard import'unun kapanışında da (`CompleteImport`) yalnızca o import'ta yazılan dosyalar için, `overwriteExisting` açık olarak çalışır — orada var olan satır dosyadaki değerlerle güncellenir. |
Klasör kökünde kalmış eski `.sql` dosyaları geriye dönük uyumluluk için hâlâ işlenir,
@ -294,6 +310,16 @@ Seed edilen host yöneticisi ile `/login` üzerinden giriş yapılır (kullanıc
`PlatformIdentityDataSeeder` ile belirlenir). Giriş sonrası
`/admin/dashboard` açılır.
Yöneticinin ilk yetkileri, yöneticinin hiç yetkisi yokken bir kez atanır. Host yöneticisi kimlik
yetkileri (rol/kullanıcı) ve `Setup.Migrate` ile başlar. Kiracı yöneticisi ise host'taki tanım
tablosunda (`AbpPermissions`) kiracı tarafına açık bütün yetkileri alır. Sonradan deploy edilen
ekranların yetkileri migrate ile atanmaz.
Kendi veritabanı olan kiracıda yalnızca `IMultiTenant` varlıklar (kullanıcı, rol, yetki ataması)
o veritabanındadır. ListForm, menü ve yetki tanımı gibi kiracısız varlıkları ABP her zaman host'ta
okur. Kiracı tablolarının satırları, kiracı adıyla kayıtlı veri kaynağından okunup yazılır
(`DataSourceManager`).
---
## 6. Yapılandırma (Configuration)
@ -304,7 +330,8 @@ Seed edilen host yöneticisi ile `/login` üzerinden giriş yapılır (kullanıc
| --- | --- |
| `App:SelfUrl` | API'nin kendi adresi (OpenIddict authority ile uyumlu olmalı). |
| `App:ClientUrl` | React uygulamasının kök adresi (mail linkleri bu adresi kullanır). |
| `App:CorsOrigins` | Virgülle ayrılmış izinli origin listesi; wildcard subdomain destekli. |
| `App:CorsOrigins` | Virgülle ayrılmış izinli origin listesi; wildcard subdomain destekli (`https://*.alan.com`). |
| `App:BaseDomain` | Production'da ana alan adı ve tüm alt alan adları (yalnızca `https`), geliştirmede `dev.{alan}` CORS'a açılır. Tek politika `PlatformCorsOriginValidator`'dadır: origin bu kurala ya da `App:CorsOrigins`'e uyuyorsa izinlidir; geliştirmede `localhost`/`127.0.0.1`/`[::1]` her zaman açıktır. |
| `App:RedirectAllowedUrls` | OAuth yönlendirme için izinli adresler. |
| `App:CdnUrl` / `App:CdnPath` | Dosya servisinin adresi ve BLOB kök dizini. |
| `App:AttachmentsPath` | Mail kuyruğu eklerinin yazıldığı dizin. |
@ -372,7 +399,7 @@ Ekran davranışı büyük ölçüde JSON kolonlarında saklanır. Öne çıkanl
| `ColumnOptionJson`, `PagerOptionJson`, `SelectionJson`, `StateStoringJson` | Sütun, sayfalama, seçim ve durum saklama ayarları. |
| `Insert/Update/DeleteCommand` (+ `Before` / `After`) | CRUD komutları ve öncesi/sonrası çalışacak SQL kancaları. |
| `Insert/Update/DeleteServiceAddress` | Varsayılan olarak `list-form-data/*`; özel endpoint'e yönlendirilebilir. |
| `SeedFilePath` | Doluysa seed sırasında listenin verisi kapsam klasöründeki bu hedeften geri yüklenir: `.json` ile biten değer tek dosya (`data/{ListFormCode}.json`), uzantısız değer satır başına dosya tutan klasördür (`data/{ListFormCode}/{anahtar}.json`). |
| `SeedFilePath` | Doluysa seed sırasında listenin verisi seed klasöründeki bu hedeften geri yüklenir: `.json` ile biten değer tek dosya (`data/{ListFormCode}.json`), uzantısız değer satır başına dosya tutan klasördür (`data/{ListFormCode}/{anahtar}.json`). |
| `SeedSyncInsert` / `SeedSyncUpdate` / `SeedSyncDelete` | Ekranda yapılan hangi işlemin seed dosyasına yansıyacağı; kapalıysa o işlem dosyaya dokunmaz. |
| `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). |
| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. Alt form olarak üretilen ekrana (menüsüz ya da menülü) yazılmaz; menüsüz wizard'da deploy boşaltır, alt form sekmesinde çizilmez. |
@ -489,7 +516,7 @@ butonlar gösterilebilir.
5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`)
6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR)
7. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları
8. **Seed dosyası** — `configs/seeds/{host|tenants/{tenantId}}/wizard/{Ad}.json` (wizard cevapları + üretilen `ListForm`/`Fields` sözleşmesi)
8. **Seed dosyası** — `configs/seeds/wizard/{Ad}.json` (wizard cevapları + üretilen `ListForm`/`Fields` sözleşmesi)
9. **Doğrulama ve geri alma** notları
**Adım bileşenleri.** Adımlar numaralı dosya adları yerine işlevleriyle adlandırılır ve
@ -586,8 +613,8 @@ kontrol `ListFormWizardAppService` üzerindedir.
**Ekran tanımı (wizard seed paketi)**
Bir ekranın tanımı, bağımlılıklarıyla birlikte tek bir zip olarak taşınır. Zip içindeki yollar seed
kapsam klasörü (`host` / `tenants/{tenantId}`) kökü baz alınarak yazılır; arşiv başka bir ortamda
aynı klasör düzenine doğrudan açılabilir.
klasörünün kökü baz alınarak yazılır; arşiv başka bir ortamda aynı klasör düzenine doğrudan
açılabilir. Eski arşivlerdeki `host/` öneki import'ta yok sayılır.
Toplama **özyinelemelidir** (`WizardExportCollector`): alt formların wizard dosyaları kuyruğa
eklenir ve onların bağımlılıkları da aynı kurallarla toplanır. Böylece zip tek başına çalışır.
@ -601,7 +628,7 @@ paylaşılan dosyalar arşive bir kez, birleşik satırlarla girer ve import ar
| `data/App.DeveloperKit.CustomComponents/{Id}.json` | Custom yolunda bağlı component ve bağımlılıkları — yalnızca bu bileşenlerin satır dosyaları |
| `data/{ListFormCode}.json` ya da `data/{ListFormCode}/{anahtar}.json` | Ekranın `SeedFilePath` değeri doluysa kendi veri aynası (dosya ya da klasör) |
| `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları |
| `{sql\|postgres}/{object\|execute}/{nesne}.sql` | `SelectCommand`'a karşılık gelen SQL nesnesi, lookup sorgularının (`LookupDto.LookupQuery`) `FROM`/`JOIN` ile işaret ettiği nesneler ve Custom yolunda CRUD endpoint'lerinin dayandığı tablolar |
| `{sql\|postgres}/[host/]{object\|execute}/{nesne}.sql` | `SelectCommand`'a karşılık gelen SQL nesnesi, lookup sorgularının (`LookupDto.LookupQuery`) `FROM`/`JOIN` ile işaret ettiği nesneler ve Custom yolunda CRUD endpoint'lerinin dayandığı tablolar |
Script dosyaları sabit bir sağlayıcı/klasör kombinasyonundan **tahmin edilmez**: kapsam kökündeki
script klasörleri (`sql/object`, `postgres/execute` …) diskten taranır ve nesne adıyla eşleşen her
@ -757,7 +784,7 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar:
| **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. |
| **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. |
| **Dynamic Service** | `/admin/list/App.DeveloperKit.DynamicServices` | Liste bir ListForm ekranıdır (KPI şeridi, Swagger butonu, kart/ızgara görünümü); kod yazma listeden açılan `DynamicServiceEditor` diyalogundadır: Monaco editörü, Roslyn ile derleme (`TestCompile`), yayınlama (`Publish`) ve çalışan uygulamaya controller olarak kaydetme. Yetkiler: Create/Edit/Delete/Manage/TestCompile/Publish/ViewCode. |
| **Custom Component** | `/admin/list/App.DeveloperKit.CustomComponents` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste bir ListForm ekranıdır (KPI şeridi, ızgara/kart görünümü, grid'in kendi ekleme/düzenleme formu); satırdaki **Design** butonu `/admin/developerkit/components/design/{id}` tasarımcısını, **Open URL** butonu bileşenin `RoutePath`'ini açar. Kayıtlar `configs/seeds/host/data/App.DeveloperKit.CustomComponents/{Id}.json` klasörüne senkronlanır. Yetkiler `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`. |
| **Custom Component** | `/admin/list/App.DeveloperKit.CustomComponents` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste bir ListForm ekranıdır (KPI şeridi, ızgara/kart görünümü, grid'in kendi ekleme/düzenleme formu); satırdaki **Design** butonu `/admin/developerkit/components/design/{id}` tasarımcısını, **Open URL** butonu bileşenin `RoutePath`'ini açar. Kayıtlar `configs/seeds/data/App.DeveloperKit.CustomComponents/{Id}.json` klasörüne senkronlanır. Yetkiler `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`. |
| **Visual Designer** | `/admin/developerkit/components/design/:id` | Custom Component listesindeki **Design** butonundan açılır; sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). |
| **ListForm** | `/admin/list/App.Listforms.Listform` | Var olan ekranların ham tanım listesi; buradan `/admin/listform/edit/{kod}` sekmeli editörüne geçilir. |
| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Menüsüz alt formlar onları sekme olarak açan ekranın altında iç içe görünür; kendi menüsü olan ekran başka bir ekranın sekmesi olsa da kökte kalır (export kapanışı onu yine taşır). Wizard'ı üreten talep listede görünür ve kaydına bağlanır; talebin bütün wizard'ları tek arşivde export edilir (`export-dev-request`) ya da bağımlılık sırasıyla tek işlemde silinir (`delete-dev-request`; alt formlar önce, talebin ekranları birbirini kullanım saymaz, tablolar en sonda — `lowcode.instructions.md` §4.5). Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. |
@ -799,7 +826,7 @@ düşer ve kullanıcı Query Editor'dan devam eder (`onOpenInEditor`). PostgreSQ
> açılabilir. Hepsi `CrudEndpointDialog` bileşenini kullanır. Yetki `App.SqlQueryManager` altındaki
> `App.SqlQueryManager.CrudEndpoints`'tir; izin verilmemişse butonlar görünmez ve
> `crud-endpoint-generate` uçlarının tamamı (base CRUD metotları dahil) reddedilir.
> Üretilen endpoint'ler `configs/seeds/{host|tenants/{tenantId}}/crud/{EntityName}.json` olarak
> Üretilen endpoint'ler `configs/seeds/crud/{EntityName}.json` olarak
> seed dosyasına yazılır; `CrudDataSeeder` aynı dosyaları okuyup endpoint'leri geri yükler.
> **Tablo tasarımı varsayılanı:** Yeni tablo oluştururken tenant ve tam denetim (audit) kolonları
@ -817,7 +844,7 @@ tasarımcı dokümanı geri okunabilir. `sourceMode` alanı `visual` ya da `code
geçilen bir bileşen kanvasa geri dönmez.
**Seed senkronizasyonu.** Bileşenlerin seed kaynağı Custom Components ekranının veri klasörüdür:
`configs/seeds/{host|tenants/{tenantId}}/data/App.DeveloperKit.CustomComponents/{Id}.json` — her
`configs/seeds/data/App.DeveloperKit.CustomComponents/{Id}.json` — her
bileşen kendi dosyasındadır; bileşen kodu satırın içinde durduğu için tek dosya hızla büyürdü.
Bileşene yazan **iki yol** da bu klasöre işlenir (`ListFormSeedDataSynchronizer`): listeden yapılan
ekleme/güncelleme/silme ve Visual Designer'ın kaydı (`CustomComponentAppService`). Veritabanı
@ -882,7 +909,7 @@ tıklamak seçim yapar, sayfayı değiştirmez — tıklama yakalama evresinde d
**Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts`
`Select.componentAs` için saklanan adı (`ReactSelect`, `CreatableSelect`, `AsyncSelect`) gerçek
bileşene çevirir — kanvas ve üretilen bileşenin aynı adı aynı şekilde yorumlamasını sağlayan tek
nokta budur. Yeni bileşen `configs/seeds/host/data/App.DeveloperKit.CustomComponents/` klasöründe
nokta budur. Yeni bileşen `configs/seeds/data/App.DeveloperKit.CustomComponents/` klasöründe
kendi `{Id}.json` satır dosyası olarak başlar.
**Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler →
@ -946,15 +973,15 @@ sebebi ve değerlendirme kriteri tanımları İK › Tanımlar (`App.Hr.Definiti
kullanıcı (personel) kaydını BT elle açar; İK aday kaydını `UserId` ile o kullanıcıya bağlar. Ayrıntı:
`lowcode-reference.instructions.md` §12.4. **Performans Yönetimi** (`App.Hr.Performance`) wizard +
custom component ile üretilmiştir: İK menüsü şartları ve takibi taşır, çalışana ait listeler menüsüzdür ve
Intranet panosundaki widget'lardan açılır; çalışma şekli [HrPerformance.md](configs/seeds/host/docs/modules/HrPerformance.md).
Intranet panosundaki widget'lardan açılır; çalışma şekli [HrPerformance.md](configs/seeds/docs/modules/HrPerformance.md).
**İzin Yönetimi** (`App.Hr.Leave`) aynı kalıptadır (talep, Müdür → İK Grid Workflow onayı, İş Kanunu md.53 bakiyesi,
rapor ↔ Sağlık Kartı eşlemesi, kullanıcı detayında İzinler sekmesi); çalışma şekli [HrLeave.md](configs/seeds/host/docs/modules/HrLeave.md).
rapor ↔ Sağlık Kartı eşlemesi, kullanıcı detayında İzinler sekmesi); çalışma şekli [HrLeave.md](configs/seeds/docs/modules/HrLeave.md).
**Puantaj ve Devam Takibi** (`App.Hr.Attendance`) hareketlerden (PDKS ucu, Excel, elle) günlük ve aylık puantaj kurar;
izin, rapor, çalışma takvimi ve vardiyadan beslenir, kullanıcı detayında Puantaj sekmesi ve Intranet'te Puantajım widget'ı vardır;
çalışma şekli [HrAttendance.md](configs/seeds/host/docs/modules/HrAttendance.md).
çalışma şekli [HrAttendance.md](configs/seeds/docs/modules/HrAttendance.md).
**Yetki kodları** `PlatformConsts.AppCodes` içinde merkezî olarak tanımlıdır ve
`configs/seeds/host/data/Abp.Identity.Permissions.json` ile seed edilir (gruplar:
`configs/seeds/data/Abp.Identity.Permissions.json` ile seed edilir (gruplar:
`Abp.Identity.PermissionGroups.json` — `App.Saas` (1), `App.Administration` (2), `App.Hr` (3)).
Ana yetki kökleri: `App.Saas`, `App.Branches`,
`App.Definitions.*`, `App.Hr.*` (Intranet dahil, `App.Hr.Intranet.*`), `App.Restrictions.*`, `App.Languages.*`, `App.Listforms.*`,
@ -1012,7 +1039,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
| Modül | Kapsam |
| --- | --- |
| **Sozsoft.Languages** | Dil tanımları, dil anahtarları, çeviri metinleri; tenant bazlı lokalizasyon (`TenantLocalizationMiddleware`). |
| **Sozsoft.Languages** | Dil tanımları, dil anahtarları, çeviri metinleri; veritabanı tabanlı lokalizasyon (`DatabaseResourceLocalizer`). Çeviri okuma (public sayfalar dahil) ABP'nin `application-localization` ucundandır ve yetki istemez; dil/anahtar/metin yönetim uçları ile önbellek temizleme `App.Languages.Language*` / `App.Languages.LanguageText*` yetkilerine bağlıdır (`LanguagesPermissions`). Önbellek temizleme yalnızca uygulamanın anahtarlarını siler (`DistributedCacheCleaner`). |
| **Sozsoft.Settings** | Ayar tanımları, sağlayıcılar, şifreleme bayrakları ve ayar ekranı metadata'sı. |
| **Sozsoft.Notifications** | Bildirim tipleri, kuralları ve kullanıcı bildirimleri; kanal bazlı yönlendirme. |
| **Sozsoft.MailQueue** | Şablon (Scriban) tabanlı gövde üretimi, ek dosya yaşam döngüsü, kuyruk çalıştırma ve günlükleme. |
@ -1041,7 +1068,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
| Aktivite ve denetim kaydı | `/admin/activityLog`, `/admin/changeLog` | Kullanıcı hareketleri ve ABP audit log detayı. |
| Bildirimler | `/admin/profile/notification-settings`, bildirim ekranları | Kanal tercihleri ve bildirim oluşturma. |
| Forum | `/admin/forum` | Forum kullanımı; yönetim `App.ForumManagement` altındaki ListForm ekranlarıyla yapılır. |
| Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik; ayrıca İK modüllerinin çalışana ait özetleri (Performans Yönetimi: Performansım, Ekibimin Performansı, 360 Taleplerim — custom component). Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Sas_T_IntranetWidget` kaydından gelir; kayıtlar `configs/seeds/{kapsam}/data/App.DeveloperKit.IntranetWidgets.json` ile senkron tutulur. |
| Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik; ayrıca İK modüllerinin çalışana ait özetleri (Performans Yönetimi: Performansım, Ekibimin Performansı, 360 Taleplerim — custom component). Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Sas_T_IntranetWidget` kaydından gelir; kayıtlar `configs/seeds/data/App.DeveloperKit.IntranetWidgets.json` ile senkron tutulur. |
| Video oda | `/admin/videoroom/*` | Oda listesi, detay, planlama, katılımcı ve ekran paylaşımı. |
| Raporlar | `/admin/reports/:report/view|design/...` | DevExpress rapor görüntüleyici ve tasarımcı. `Raporlar` kategorisindeki şablon: `/admin/reports/DynamicReport/view/{ŞablonId}[/{ListFormCode}?recordId=...]` (§15). |
| AI asistanı | `/admin/ai` | n8n webhook üzerinden sohbet arayüzü. |
@ -1064,7 +1091,7 @@ react-icons adı, `Order` sıra, `IsDisabled` yayın anahtarı, `DisplayName` di
`[Authorize]` olduğu için menüler uygulama konfigürasyonunun `publicMenus` alanıyla taşınır
(`PlatformApplicationConfigurationContributor`), istemcide `usePublicMenu` okur. `Pub` modülü
yönetim navigasyonuna girmez (`abpConfig.model.ts` menüyü kurarken bu modülü eler; Menü Yönetimi
ekranı elemez, menüler oradan düzenlenir). Seed: `configs/seeds/host/data/App.Menus.Module.json` ve
ekranı elemez, menüler oradan düzenlenir). Seed: `configs/seeds/data/App.Menus.Module.json` ve
`App.Menus.Menu.json`.
**Sayfa.** Her public sayfa bir Custom Component'tir: `RoutePath` `/admin/` ile başlamadığı sürece
@ -1072,7 +1099,7 @@ bileşen otomatik olarak public route olur ve `PublicLayout` altında yayınlan
DeveloperKit > Custom Components (Visual Designer) üzerinden değişir. Aynı yolu bir fiziksel route
ile paylaşan custom component kazanır; karşılığı olan dosyası bulunmayan rota kaydı ise hiç
yüklenmez (`hasPhysicalComponent`) — seeder yalnızca eklediği için eski kayıtlar veritabanında
kalsa bile ekran patlamaz. Seed: `configs/seeds/host/data/App.DeveloperKit.CustomComponents/{Id}.json`;
kalsa bile ekran patlamaz. Seed: `configs/seeds/data/App.DeveloperKit.CustomComponents/{Id}.json`;
ekran klasör modundadır, her bileşen kendi satır dosyasındadır (dosya adı satırın `Id`'si).
Runtime kapsamı sınırlıdır: bileşen kodundaki `import` satırları derlemede temizlenir,
@ -1203,7 +1230,7 @@ Get/Save uçları ve DTO'ları, `TenantData.json` + `TenantDataSeeder`,
`App.{Home,About,Services,Contact}` yetkileri ve `.Design` alt yetkileri, `/admin/public/*/designer`
rota ve menüleri.
### 10.5. Modül dokümanları (`configs/seeds/host/docs/modules/`)
### 10.5. Modül dokümanları (`configs/seeds/docs/modules/`)
Konfigürasyonla üretilen her modülün çalışma şekli, keşif analizi ve kararları kendi dosyasındadır;
"bu ekran nasıl çalışıyor?" sorusunun cevabı oradadır (MCP'de `get_module_doc`). Dokümanlar seed
@ -1216,21 +1243,21 @@ başlığındaki ⓘ simgesi ise ekranın kendi ayrıntılı açıklamasını g
| Modül | Menü | Doküman |
| --- | --- | --- |
| Performans Yönetimi | İK › Performans Yönetimi (`App.Hr.Performance`) | [HrPerformance.md](configs/seeds/host/docs/modules/HrPerformance.md) |
| İşe Alım | İK › İşe Alım (`App.Hr.Recruitment`) | [HrRecruitment.md](configs/seeds/host/docs/modules/HrRecruitment.md) |
| İzin Yönetimi | İK › İzin Yönetimi (`App.Hr.Leave`) | [HrLeave.md](configs/seeds/host/docs/modules/HrLeave.md) |
| Puantaj ve Devam Takibi | İK › Puantaj ve Devam Takibi (`App.Hr.Attendance`) | [HrAttendance.md](configs/seeds/host/docs/modules/HrAttendance.md) |
| Eğitimler | İK › Eğitimler (`App.Hr.Trainings`) | [HrTrainings.md](configs/seeds/host/docs/modules/HrTrainings.md) |
| Araçlar | İK › Araçlar (`App.Hr.Vehicles`) | [HrVehicles.md](configs/seeds/host/docs/modules/HrVehicles.md) |
| Disiplin Dosyaları | İK › Disiplin Dosyaları (`App.Hr.DisciplinaryCase`) | [HrDisciplinaryCase.md](configs/seeds/host/docs/modules/HrDisciplinaryCase.md) |
| Tanımlamalar (dizin + genel tanımlar) | İK › Tanımlamalar (`App.Hr.Definitions`) | [HrDefinitions.md](configs/seeds/host/docs/modules/HrDefinitions.md) |
| Organizasyon ve Yetkinlik | İK › Departmanlar, Pozisyonlar, Yetenek Tipleri, Organizasyon Şeması | [HrOrganization.md](configs/seeds/host/docs/modules/HrOrganization.md) |
| Çalışma Takvimi ve Vardiyalar | İK › Çalışma Takvimi, Vardiyalar | [HrWorkCalendar.md](configs/seeds/host/docs/modules/HrWorkCalendar.md) |
| Sağlık Kartları | İK › Sağlık Kartları (`App.Hr.HealthCard`) | [HrHealthCard.md](configs/seeds/host/docs/modules/HrHealthCard.md) |
| Ziyaretçiler | İK › Ziyaretçiler (`App.Hr.Visitor`) | [HrVisitor.md](configs/seeds/host/docs/modules/HrVisitor.md) |
| Olay Günlüğü | İK › Olay Günlüğü (`App.Hr.EventLog`) | [HrEventLog.md](configs/seeds/host/docs/modules/HrEventLog.md) |
| Personel Kaydı (özlük, sözleşme, eğitim, aile, mali bilgiler) | Kimlik › Kullanıcılar › detay | [HrPersonnel.md](configs/seeds/host/docs/modules/HrPersonnel.md) |
| Intranet (pano, duyuru, anket, sosyal duvar, etkinlik, doküman, mesajlaşma) | İK › Intranet (`App.Hr.Intranet`) | [HrIntranet.md](configs/seeds/host/docs/modules/HrIntranet.md) |
| Performans Yönetimi | İK › Performans Yönetimi (`App.Hr.Performance`) | [HrPerformance.md](configs/seeds/docs/modules/HrPerformance.md) |
| İşe Alım | İK › İşe Alım (`App.Hr.Recruitment`) | [HrRecruitment.md](configs/seeds/docs/modules/HrRecruitment.md) |
| İzin Yönetimi | İK › İzin Yönetimi (`App.Hr.Leave`) | [HrLeave.md](configs/seeds/docs/modules/HrLeave.md) |
| Puantaj ve Devam Takibi | İK › Puantaj ve Devam Takibi (`App.Hr.Attendance`) | [HrAttendance.md](configs/seeds/docs/modules/HrAttendance.md) |
| Eğitimler | İK › Eğitimler (`App.Hr.Trainings`) | [HrTrainings.md](configs/seeds/docs/modules/HrTrainings.md) |
| Araçlar | İK › Araçlar (`App.Hr.Vehicles`) | [HrVehicles.md](configs/seeds/docs/modules/HrVehicles.md) |
| Disiplin Dosyaları | İK › Disiplin Dosyaları (`App.Hr.DisciplinaryCase`) | [HrDisciplinaryCase.md](configs/seeds/docs/modules/HrDisciplinaryCase.md) |
| Tanımlamalar (dizin + genel tanımlar) | İK › Tanımlamalar (`App.Hr.Definitions`) | [HrDefinitions.md](configs/seeds/docs/modules/HrDefinitions.md) |
| Organizasyon ve Yetkinlik | İK › Departmanlar, Pozisyonlar, Yetenek Tipleri, Organizasyon Şeması | [HrOrganization.md](configs/seeds/docs/modules/HrOrganization.md) |
| Çalışma Takvimi ve Vardiyalar | İK › Çalışma Takvimi, Vardiyalar | [HrWorkCalendar.md](configs/seeds/docs/modules/HrWorkCalendar.md) |
| Sağlık Kartları | İK › Sağlık Kartları (`App.Hr.HealthCard`) | [HrHealthCard.md](configs/seeds/docs/modules/HrHealthCard.md) |
| Ziyaretçiler | İK › Ziyaretçiler (`App.Hr.Visitor`) | [HrVisitor.md](configs/seeds/docs/modules/HrVisitor.md) |
| Olay Günlüğü | İK › Olay Günlüğü (`App.Hr.EventLog`) | [HrEventLog.md](configs/seeds/docs/modules/HrEventLog.md) |
| Personel Kaydı (özlük, sözleşme, eğitim, aile, mali bilgiler) | Kimlik › Kullanıcılar › detay | [HrPersonnel.md](configs/seeds/docs/modules/HrPersonnel.md) |
| Intranet (pano, duyuru, anket, sosyal duvar, etkinlik, doküman, mesajlaşma) | İK › Intranet (`App.Hr.Intranet`) | [HrIntranet.md](configs/seeds/docs/modules/HrIntranet.md) |
---
@ -1287,7 +1314,10 @@ başlığındaki ⓘ simgesi ise ekranın kendi ayrıntılı açıklamasını g
- **Şube (Branch):** Tenant altındaki fiziksel/örgütsel kırılım; kullanıcı–şube eşlemesi
`BranchUsers` ile tutulur, lisans limiti girişte kontrol edilir.
- **Organizasyon birimi:** ABP `OrganizationUnit` ağacı; ListForm'lar OU bazlı filtrelenebilir.
- Tenant bazlı lokalizasyon: `TenantLocalizationInitializer` + `TenantLocalizationMiddleware`.
- Lokalizasyon: isteğin kültürü (metin, tarih, sayı) `UseAbpRequestLocalization` ile UI'ın gönderdiği
`Accept-Language`'tan kurulur. `TenantLocalizationMiddleware` + `TenantLocalizationInitializer` istek dışı
iş parçacıklarının (Hangfire, worker) varsayılan kültürünü ve `IClock` tipini global `Abp.Localization.DefaultLanguage`
/ `Abp.Timing.TimeZone` ayarlarından tutar; değerler dakikada en fazla bir kez okunur ve yalnızca değişince yazılır.
---
@ -1386,11 +1416,12 @@ Agenda'nın üstündedir; Saas altında bildirim ekranı **yoktur**.
Tablolar kiracı kapsamındadır (`Adm_T_NotificationTemplate`, `Adm_T_NotificationType`,
`Adm_T_NotificationRule`, `Adm_T_Notification`; dördü de `IMultiTenant`). Her kiracı kendi türünü, kuralını ve kuyruğunu
görür; host kaydı (`TenantId = null`) yalnızca host oturumunda görünür. Yeni bir kiracı
oluşturulduğunda varsayılan tür ve kural kümesi o kiracıya kopyalanır
(`NotificationTenantDefaultsSeedContributor`): kaynak host kapsamındaki
oluşturulduğunda varsayılan tür ve kural kümesi o kiracıya kopyalanır. Kendi veritabanı olan
kiracı kümeyi seed dosyasından alır (`ListFormSeedDataApplier`). Host veritabanını paylaşan
kiracıya ise `NotificationTenantDefaultsSeedContributor` kopyalar: kaynak
`data/App.Notifications.Notification{Types,Rules}.json` dosyalarıdır, satırlar **yeni
kimliklerle** yazılır ve eşleşme ad üzerinden yapıldığı için ikinci çalıştırma bir şey eklemez;
şablonlar kopyalanmaz.
şablonlar kopyalanmaz. İki yol aynı kiracıda çalışmaz, yoksa türler iki kez yazılırdı.
**Bildirimler** ekranı (`App.Notifications.Notification`) hem kuyruk günlüğü hem gönderim
ekranıdır: liste kural motorunun ürettiklerini de gösterir, ekleme formu ise elle gönderimdir.
@ -1513,7 +1544,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. |
| `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. |
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`, ya da başlık metniyle). Bölüm önce istenen, sonra diğer belgede aranır; başlıkla eşleşmeyen terimde (örn. `dxImageUpload`) terimin geçtiği bölümler (en çok 15, `DevGuideSearchMatches`), hiç geçmiyorsa başlık listesi döner — hata atılmaz. Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. |
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. 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). |
@ -1524,7 +1555,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `add_dev_request_subtasks` | Talebin `SubTasks` listesine tamamlanmış satırlar ekler: model yalnızca başlığı verir, atanan kişi bağlanan kullanıcının adı (`userName`), bitiş tarihi bugün (`yyyy-MM-dd`) olarak sunucuda yazılır; var olan satırlar korunur. `.Write` + `App.DevRequests.Update` ister. |
| `append_dev_request_note` | Talebin `Analysis` kolonunun sonuna HTML ekler (üretim sonuç notu); `Description` talebin yazıldığı hâliyle kalır. İkisi de `update_row` ile yazılmaz — açıklama base64 görsel taşır. `.Write` + `App.DevRequests.Update` ister. |
| `list_components` / `get_component` / `save_component` / `delete_component` | Custom component'ler `ICustomComponentAppService` üzerinden; seed senkronu ve `DataSources` çözümü aynen çalışır. `save_component` ada göre ekler/günceller, tasarımcı belgesi (`Props.visualDesigner`, `sourceMode ≠ code`) taşıyan bileşene kod yazmayı reddeder — kod belgeden üretildiği için tasarımcıdaki ilk kayıtta ezilirdi. |
| `get_module_doc` / `save_module_doc` | Modül dokümanı (`configs/seeds/{kapsam}/docs/modules/{Modül}.md`, `IModuleDocAppService`). Ad yerine ekran kodu ya da wizard dosya adı verilebilir. `save_module_doc` elle yazılan bölümleri kaydeder, "Ek — Ekran envanteri"ni sunucu üretir. Talebi `update_row` ile `Testing`'e almak, talebin wizard'larının dokümanı tamamlanmadan (dosya var, `module-doc:todo` kalmamış) reddedilir. Okuma `App.Listforms.Wizard`, yazma ayrıca `Wizards.Update` ister. |
| `get_module_doc` / `save_module_doc` | Modül dokümanı (`configs/seeds/docs/modules/{Modül}.md`, `IModuleDocAppService`). Ad yerine ekran kodu ya da wizard dosya adı verilebilir. `save_module_doc` elle yazılan bölümleri kaydeder, "Ek — Ekran envanteri"ni sunucu üretir. Talebi `update_row` ile `Testing`'e almak, talebin wizard'larının dokümanı tamamlanmadan (dosya var, `module-doc:todo` kalmamış) reddedilir. Okuma `App.Listforms.Wizard`, yazma ayrıca `Wizards.Update` ister. |
| `list_report_templates` / `get_report_template` / `save_report_category` / `save_report_template` / `delete_report_template` | Rapor kategorileri ve `DynamicReport` şablonları (`McpReportAppService`). Okuma depodan, ekranın okuma yetkisiyle yapılır. Yazma `App.Reports.Categories` / `App.Reports.ReportTemplates` ekranlarının C/U/D sözleşmesinden (`IListFormDataAppService`) geçer; seed senkronu aynen çalışır. Kayıt ada göre ekler/günceller. `listFormCode` verilirse HTML'deki `{{AlanAdi}}` yer tutucuları o ekranın alanlarına göre doğrulanır ve cevapta satır butonu (`rowButton`) döner (§15). |
**Timesheets.** Talebin `Timesheets` listesini sunucu doldurur (`IMcpDevRequestTimesheetRecorder`):
@ -1858,17 +1889,20 @@ konfigürasyonu ile üretilir.
### 18.4. PWA ve sürüm yönetimi
- Kurulum iki kademelidir: **kabuk** (index.html + entry JS/CSS + eager vendor chunk'ları)
workbox precache manifest'inden gelir ve zorunludur; **bileşenler** (tüm lazy chunk'lar,
build'de üretilen `dist/warmup.json`) güncelleme penceresinde aynı ilerleme çubuğunda arka
planda indirilir. İkinci kademe best-effort'tur (inemeyen dosya kurulumu düşürmez, süre
bütçesi aşılırsa kalanlar runtime cache'e kalır); böylece deploy sonrası ilk gezinmede chunk
beklenmez ama yavaş bağlantıda kurulum timeout'a düşmez. DevExtreme temaları (~33 MB), font
ve görseller runtime cache'tedir.
- Service worker kurulumu yalnızca **kabuğu** indirir (index.html + entry JS/CSS + eager vendor
chunk'ları, workbox precache manifest'i); kabuk zorunludur. **Bileşen ısıtması** kurulumdan
ayrıdır ve yalnızca oturum açık kullanıcı içindir: store, application-configuration her
geldiğinde `protected` fiziksel rotalardan kullanıcının yetkili olduklarını
(`PermissionGuard` ile aynı ölçüt) `setWarmupRoutes` ile bildirir, uygulama `ENSURE_WARMUP`
mesajıyla bu rotaları worker'a yollar. Worker build'de üretilen `dist/warmup.json`'daki
`routes` eşlemesinden (view modülü → chunk + statik import zinciri + CSS) yalnızca o dosyaları
indirir. Ziyaretçi, public sayfa ve giriş ekranı hiçbir bileşen indirmez; ekranın içinden lazy
açılan parçalar (diyalog, editör) ve runtime bileşenleri ilk kullanımda runtime cache'e
alınır. Isıtma best-effort'tur (inemeyen dosya atlanır, süre bütçesi aşılırsa kalanlar
runtime cache'e kalır) ve her açılışta eksikleri kaldığı yerden tamamlar. DevExtreme temaları
(~33 MB), font ve görseller runtime cache'tedir.
- `registerType: 'prompt'` — yeni sürüm indirilir, indirme bitince otomatik devreye alınır
(`views/version/swRegistration.ts`, `sw.ts`, `AppUpdateDialog.tsx`).
- Isıtma yalnızca kuruluma bağlı değildir: uygulama açılışta `ENSURE_WARMUP` mesajı yollar,
service worker eksik kalan bileşenleri kaldığı yerden tamamlar.
- **Sürüm bilgisi backend'dedir.** `Sas_H_ChangeLog` tablosu tek kaynaktır; istemci sürüm
numarasını ve son sürümün notlarını `application-configuration` yanıtındaki
`extraProperties.changeLogs` alanından okur (`PlatformApplicationConfigurationContributor`),
@ -2068,7 +2102,7 @@ Yeni ABP modülü eklemek için:
İki ayrı seed yüzeyi vardır ve karıştırılmamalıdır.
**1. Depo içi (kod ile taşınan) seed'ler** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`.
Platformun kendi ekranları buradadır (dil metinleri `configs/seeds/host/data/App.Languages.*.json`); değişiklikleri
Platformun kendi ekranları buradadır (dil metinleri `configs/seeds/data/App.Languages.*.json`); değişiklikleri
derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar:
- `ListFormSeeder_Saas.cs`, `ListFormSeeder_Administration.cs`, `ListFormSeeder_Hr.cs` — menü
@ -2081,29 +2115,31 @@ derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek içi
**2. Runtime'da üretilen seed'ler** — `configs/seeds/`. Wizard, Custom Components ekranı/Visual
Designer, CRUD Endpoint Manager ve `SeedFilePath` tanımlı ListForm ekranları çalışma zamanında
burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar
geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver` üzerinden çözülür:
geri yüklenir. Klasör tektir (host ve kendi veritabanı olan her tenant aynı dosyalarla kurulur)
ve `SeedPathResolver` üzerinden çözülür:
```
configs/seeds/
├── host/ # host kapsamı
│ ├── wizard/ # {Ad}.json → WizardDataSeeder
│ ├── crud/ # {EntityName}.json → CrudEndpointSeedApplier
│ ├── data/ # {ListFormCode}.json ya da {ListFormCode}/{anahtar}.json → ListFormSeedDataApplier
│ │ # App.Menus.{Routes,Module,Menu}.json — route/modül/menü ağacı
│ │ # Abp.Identity.Permission{Groups,s}.json — yetki tanımları
│ │ # App.Definitions.{CountryGroup,Country,City,District}.json — adres zinciri
│ │ # App.Definitions.{Currency,ContactTitle,AiBot,TerminationReason}.json — para birimi, hitap, AI bot, SGK çıkış kodu
│ │ # App.Hr.{SkillType,SkillLevel,Skill}.json — İK yetkinlik tanımları
│ │ # App.Hr.{Department,JobPosition}.json — İK departman → pozisyon (BranchId ile şubeye bağlı)
│ │ # App.Hr.Intranet.*.json — İK › Intranet (duyuru, anket, sosyal akış, etkinlik)
│ │ # App.Hr.{CandidateSource,RejectionReason,EvaluationCriterion}.json — İK › Tanımlar altındaki işe alım tanımları
│ │ # App.Hr.PerformanceRating.json — İK › Tanımlar › Performans Dereceleri (5 kademeli ölçek)
│ │ # App.Notifications.Notification{Templates,Types,Rules}.json — bildirim şablonu, tipi → kuralı
│ │ # App.Languages.{Language,LanguageText}.json — dil → metin (eksik LanguageKey metinle üretilir)
│ │ # App.SettingDefinitions.json, App.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler
│ ├── sql/{object,execute}/ # .sql (SQL Server)
│ └── postgres/{object,execute}/
├── tenants/{tenantId}/… # aynı düzen, tenant kapsamı
├── wizard/ # {Ad}.json → WizardDataSeeder
├── crud/ # {EntityName}.json → CrudEndpointSeedApplier
├── docs/modules/ # {Modül}.md → ModuleDocManager (seed edilmez)
├── data/ # {ListFormCode}.json ya da {ListFormCode}/{anahtar}.json → ListFormSeedDataApplier
│ # App.Menus.{Routes,Module,Menu}.json — route/modül/menü ağacı
│ # Abp.Identity.Permission{Groups,s}.json — yetki tanımları
│ # App.Definitions.{CountryGroup,Country,City,District}.json — adres zinciri
│ # App.Definitions.{Currency,ContactTitle,AiBot,TerminationReason}.json — para birimi, hitap, AI bot, SGK çıkış kodu
│ # App.Hr.{SkillType,SkillLevel,Skill}.json — İK yetkinlik tanımları
│ # App.Hr.{Department,JobPosition}.json — İK departman → pozisyon (BranchId ile şubeye bağlı)
│ # App.Hr.Intranet.*.json — İK › Intranet (duyuru, anket, sosyal akış, etkinlik)
│ # App.Hr.{CandidateSource,RejectionReason,EvaluationCriterion}.json — İK › Tanımlar altındaki işe alım tanımları
│ # App.Hr.PerformanceRating.json — İK › Tanımlar › Performans Dereceleri (5 kademeli ölçek)
│ # App.Notifications.Notification{Templates,Types,Rules}.json — bildirim şablonu, tipi → kuralı
│ # App.Languages.{Language,LanguageText}.json — dil → metin (eksik LanguageKey metinle üretilir)
│ # App.SettingDefinitions.json, App.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler
├── sql/{object,execute}/ # .sql (SQL Server)
│ └── host/{object,execute}/ # yalnızca host veritabanında çalışır
├── postgres/{object,execute}/
│ └── host/{object,execute}/
└── .imports/{importId}/ # wizard import staging + backup (seeder taramalarına girmez)
```

View file

@ -55,7 +55,6 @@ COPY "src/Sozsoft.Platform.Domain/Sozsoft.Platform.Domain.csproj" "src/Sozsoft.P
COPY "src/Sozsoft.Platform.Domain.Shared/Sozsoft.Platform.Domain.Shared.csproj" "src/Sozsoft.Platform.Domain.Shared/"
COPY "src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj" "src/Sozsoft.Platform.EntityFrameworkCore/"
COPY "src/Sozsoft.Platform.HttpApi/Sozsoft.Platform.HttpApi.csproj" "src/Sozsoft.Platform.HttpApi/"
COPY "src/Sozsoft.Platform.HttpApi.Client/Sozsoft.Platform.HttpApi.Client.csproj" "src/Sozsoft.Platform.HttpApi.Client/"
COPY "src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj" "src/Sozsoft.Platform.HttpApi.Host/"
COPY "src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj" "src/Sozsoft.Platform.DbMigrator/"
COPY "test/Sozsoft.Platform.EntityFrameworkCore.Tests/Sozsoft.Platform.EntityFrameworkCore.Tests.csproj" "test/Sozsoft.Platform.EntityFrameworkCore.Tests/"

View file

@ -17,8 +17,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.Applicatio
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.HttpApi", "src\Sozsoft.Platform.HttpApi\Sozsoft.Platform.HttpApi.csproj", "{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.HttpApi.Client", "src\Sozsoft.Platform.HttpApi.Client\Sozsoft.Platform.HttpApi.Client.csproj", "{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.DbMigrator", "src\Sozsoft.Platform.DbMigrator\Sozsoft.Platform.DbMigrator.csproj", "{AA94D832-1CCC-4715-95A9-A483F23A1A5D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.HttpApi.Host", "src\Sozsoft.Platform.HttpApi.Host\Sozsoft.Platform.HttpApi.Host.csproj", "{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}"
@ -186,18 +184,6 @@ Global
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x64.Build.0 = Release|Any CPU
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x86.ActiveCfg = Release|Any CPU
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x86.Build.0 = Release|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x64.ActiveCfg = Debug|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x64.Build.0 = Debug|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x86.ActiveCfg = Debug|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x86.Build.0 = Debug|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.Build.0 = Release|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x64.ActiveCfg = Release|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x64.Build.0 = Release|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x86.ActiveCfg = Release|Any CPU
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x86.Build.0 = Release|Any CPU
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -593,7 +579,6 @@ Global
{42F719ED-8413-4895-B5B4-5AB56079BC66} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{520659C8-C734-4298-A3DA-B539DB9DFC0B} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{AA94D832-1CCC-4715-95A9-A483F23A1A5D} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{4D886EEF-D4CF-4879-A10D-249D762E4ED9} = {03E1C8DA-035E-4882-AF81-F392139FCF38}

View file

@ -1,22 +0,0 @@
using System.Threading.Tasks;
using Volo.Abp;
namespace Sozsoft.Languages;
[IntegrationService]
public interface ILanguageKeyIntegrationService
{
Task CreateKeysForSettingsAsync(
string NameKey,
string DescriptionKey,
string MainGroupKey,
string SubGroupKey
);
Task DeleteKeysForSettingsAsync(
string NameKey,
string DescriptionKey,
string MainGroupKey,
string SubGroupKey);
}

View file

@ -6,6 +6,25 @@ public class LanguagesPermissions
{
public const string GroupName = "Languages";
// Yetki tanımları seed'dedir (data/Abp.Identity.Permissions.json); burada yalnızca adları durur.
// Değerler PlatformConsts.AppCodes.Languages ile aynıdır — modül platforma referans vermez.
public static class Language
{
public const string Default = "App.Languages.Language";
public const string Create = Default + ".Create";
public const string Update = Default + ".Update";
public const string Delete = Default + ".Delete";
}
/// <summary>Dil anahtarları ayrı bir ekranda değil, dil metinleriyle birlikte yönetilir.</summary>
public static class LanguageText
{
public const string Default = "App.Languages.LanguageText";
public const string Create = Default + ".Create";
public const string Update = Default + ".Update";
public const string Delete = Default + ".Delete";
}
public static string[] GetAll()
{
return ReflectionHelper.GetPublicConstantsRecursively(typeof(LanguagesPermissions));

View file

@ -2,8 +2,10 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Localization;
using Sozsoft.Languages.Permissions;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
@ -12,6 +14,7 @@ using Volo.Abp.Validation;
namespace Sozsoft.Languages;
[Authorize]
public class LanguageAppService : CrudAppService<
Language,
LanguageDto,
@ -23,6 +26,11 @@ public class LanguageAppService : CrudAppService<
{
LocalizationResource = typeof(LanguagesResource);
ObjectMapperContext = typeof(LanguagesApplicationModule);
GetPolicyName = LanguagesPermissions.Language.Default;
GetListPolicyName = LanguagesPermissions.Language.Default;
CreatePolicyName = LanguagesPermissions.Language.Create;
UpdatePolicyName = LanguagesPermissions.Language.Update;
DeletePolicyName = LanguagesPermissions.Language.Delete;
}
public override async Task<LanguageDto> CreateAsync(LanguageDto input)
@ -40,6 +48,7 @@ public class LanguageAppService : CrudAppService<
return await base.CreateAsync(input);
}
[Authorize(LanguagesPermissions.Language.Default)]
public async Task<LanguageDto> GetLanguageByCultureNameAsync(string CultureName)
{
var item = await Repository.FirstOrDefaultAsync(a => a.CultureName == CultureName);

View file

@ -2,8 +2,10 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Localization;
using Sozsoft.Languages.Permissions;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
@ -11,6 +13,7 @@ using Volo.Abp.Validation;
namespace Sozsoft.Languages;
[Authorize]
public class LanguageKeyAppService : CrudAppService<
LanguageKey,
LanguageKeyDto,
@ -26,6 +29,11 @@ public class LanguageKeyAppService : CrudAppService<
{
LocalizationResource = typeof(LanguagesResource);
ObjectMapperContext = typeof(LanguagesApplicationModule);
GetPolicyName = LanguagesPermissions.LanguageText.Default;
GetListPolicyName = LanguagesPermissions.LanguageText.Default;
CreatePolicyName = LanguagesPermissions.LanguageText.Create;
UpdatePolicyName = LanguagesPermissions.LanguageText.Update;
DeletePolicyName = LanguagesPermissions.LanguageText.Delete;
_repository = repository;
}

View file

@ -1,64 +0,0 @@
using System;
using System.Threading.Tasks;
using Sozsoft.Languages.Entities;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Languages;
[IntegrationService]
public class LanguageKeyIntegrationService : ILanguageKeyIntegrationService, ITransientDependency
{
private readonly IRepository<LanguageKey, Guid> _repository;
public LanguageKeyIntegrationService(IRepository<LanguageKey, Guid> repository)
{
_repository = repository;
}
public async Task CreateKeysForSettingsAsync(
string NameKey,
string DescriptionKey,
string MainGroupKey,
string SubGroupKey
)
{
//NameKey
if (!await _repository.AnyAsync(a => a.Key == NameKey && a.ResourceName == LanguagesConsts.AppName))
{
await _repository.InsertAsync(new LanguageKey { Key = NameKey, ResourceName = LanguagesConsts.AppName });
}
//DescriptionKey
if (!await _repository.AnyAsync(a => a.Key == DescriptionKey && a.ResourceName == LanguagesConsts.AppName))
{
await _repository.InsertAsync(new LanguageKey { Key = DescriptionKey, ResourceName = LanguagesConsts.AppName });
}
//MainGroupKey
if (!await _repository.AnyAsync(a => a.Key == MainGroupKey && a.ResourceName == LanguagesConsts.AppName))
{
await _repository.InsertAsync(new LanguageKey { Key = MainGroupKey, ResourceName = LanguagesConsts.AppName });
}
//SubGroupKey
if (!await _repository.AnyAsync(a => a.Key == SubGroupKey && a.ResourceName == LanguagesConsts.AppName))
{
await _repository.InsertAsync(new LanguageKey { Key = SubGroupKey, ResourceName = LanguagesConsts.AppName });
}
}
public async Task DeleteKeysForSettingsAsync(
string NameKey,
string DescriptionKey,
string MainGroupKey,
string SubGroupKey)
{
await _repository.DeleteAsync(a => a.Key == NameKey && a.ResourceName == LanguagesConsts.AppName);
await _repository.DeleteAsync(a => a.Key == DescriptionKey && a.ResourceName == LanguagesConsts.AppName);
await _repository.DeleteAsync(a => a.Key == MainGroupKey && a.ResourceName == LanguagesConsts.AppName);
await _repository.DeleteAsync(a => a.Key == SubGroupKey && a.ResourceName == LanguagesConsts.AppName);
}
}

View file

@ -4,11 +4,12 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Sozsoft.Languages.Caching;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Localization;
using StackExchange.Redis;
using Sozsoft.Languages.Permissions;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
@ -19,6 +20,11 @@ using Volo.Abp.Validation;
namespace Sozsoft.Languages;
/// <summary>
/// Dil metinlerinin yönetim uçları. Çeviri okuma bu servisten geçmez — public sayfalar dahil bütün
/// istemciler metinleri ABP'nin application-localization ucundan (DatabaseResourceLocalizer) alır.
/// </summary>
[Authorize]
public class LanguageTextAppService : CrudAppService<
LanguageText,
LanguageTextDto,
@ -29,20 +35,25 @@ public class LanguageTextAppService : CrudAppService<
{
private readonly IRepository<LanguageText, Guid> _repositoryText;
private readonly IRepository<LanguageKey, Guid> _repositoryKey;
private readonly IConfiguration _configuration;
private readonly DistributedCacheCleaner _cacheCleaner;
public LanguageTextAppService(
IRepository<LanguageText, Guid> repositoryText,
IRepository<LanguageKey, Guid> repositoryKey,
IConfiguration configuration
DistributedCacheCleaner cacheCleaner
)
: base(repositoryText)
{
LocalizationResource = typeof(LanguagesResource);
ObjectMapperContext = typeof(LanguagesApplicationModule);
GetPolicyName = LanguagesPermissions.LanguageText.Default;
GetListPolicyName = LanguagesPermissions.LanguageText.Default;
CreatePolicyName = LanguagesPermissions.LanguageText.Create;
UpdatePolicyName = LanguagesPermissions.LanguageText.Update;
DeletePolicyName = LanguagesPermissions.LanguageText.Delete;
_repositoryText = repositoryText;
_repositoryKey = repositoryKey;
_configuration = configuration;
_cacheCleaner = cacheCleaner;
}
public override async Task<LanguageTextDto> CreateAsync(LanguageTextCreateUpdateDto input)
@ -63,6 +74,7 @@ public class LanguageTextAppService : CrudAppService<
return await base.CreateAsync(input);
}
[Authorize(LanguagesPermissions.LanguageText.Update)]
public async Task UpdateLanguageTextAsync(LanguageTextCreateUpdateDto input)
{
var entity = await _repositoryText.GetAsync(
@ -77,6 +89,7 @@ public class LanguageTextAppService : CrudAppService<
}
}
[Authorize(LanguagesPermissions.LanguageText.Default)]
public async Task<PagedResultDto<LanguageTextTranslatedDto>> GetLanguageTextTranslatedAsync(LanguageTextTranslatedRequestDto input)
{
var query = await _repositoryKey.GetQueryableAsync();
@ -109,6 +122,7 @@ public class LanguageTextAppService : CrudAppService<
return new PagedResultDto<LanguageTextTranslatedDto>(totalCount, dtoLanguageTextTranslate);
}
[Authorize(LanguagesPermissions.LanguageText.Default)]
public async Task<LanguageTextDto> GetLanguageTextAsync(LanguageTextFilteredDto input)
{
var entity = await _repositoryText.FirstOrDefaultAsync(a =>
@ -119,6 +133,7 @@ public class LanguageTextAppService : CrudAppService<
return ObjectMapper.Map<LanguageText, LanguageTextDto>(entity);
}
[Authorize(LanguagesPermissions.LanguageText.Default)]
public async Task<List<LanguageTextDto>> GetLanguageTextByCultureNameAsync(string CultureName)
{
var item = await _repositoryText.GetListAsync(a => a.CultureName == CultureName);
@ -130,11 +145,11 @@ public class LanguageTextAppService : CrudAppService<
return ObjectMapper.Map<List<LanguageText>, List<LanguageTextDto>>(item);
}
// Dil Metinleri listesindeki "Redis önbelleğini temizle" butonu da bu yetkiyle görünür.
[Authorize(LanguagesPermissions.LanguageText.Default)]
public async Task ClearRedisCacheAsync()
{
var redis = ConnectionMultiplexer.Connect(_configuration["Redis:Configuration"]!);
var db = redis.GetDatabase();
await db.ExecuteAsync("FLUSHALL");
await _cacheCleaner.ClearAsync();
}
protected virtual IQueryable<LanguageKey> ApplySorting(IQueryable<LanguageKey> query, LanguageTextTranslatedRequestDto input)

View file

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Sozsoft.Languages.Localization;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
namespace Sozsoft.Languages.Caching;
/// <summary>
/// Uygulamanın dağıtık önbelleğini boşaltır. <c>FLUSHALL</c> yerine yalnızca bu uygulamanın anahtarlarını
/// siler: Redis başka bir uygulama ya da ortamla paylaşılıyorsa onların verisine dokunulmaz.
/// Bağlantı süreç başına tektir; her çağrıda yeni bağlantı açmak bağlantı sızdırıyordu.
/// </summary>
public class DistributedCacheCleaner : ISingletonDependency, IDisposable
{
private const int BatchSize = 500;
private readonly Lazy<ConnectionMultiplexer> _connection;
private readonly string[] _keyPatterns;
private readonly IDatabaseResourceLocalizer _localizer;
public DistributedCacheCleaner(
IConfiguration configuration,
IOptions<AbpDistributedCacheOptions> cacheOptions,
IDatabaseResourceLocalizer localizer)
{
_localizer = localizer;
_connection = new Lazy<ConnectionMultiplexer>(
() => ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]!));
// ABP iki biçimde yazar: IDistributedCache<T> anahtarı [t:{kiracı},]c:{önbellek},k:{KeyPrefix}{anahtar},
// bellek içi tanım önbelleklerinin damgası ise doğrudan {KeyPrefix}_Abp...Stamp. Host "Platform",
// DbMigrator "Platform:" öneki kullanır; ':' atılınca desenler iki sürecin anahtarlarını da kapsar.
var prefix = (cacheOptions.Value.KeyPrefix ?? string.Empty).TrimEnd(':');
_keyPatterns = [$"{prefix}*", $"*,k:{prefix}*"];
}
/// <returns>Silinen anahtar sayısı.</returns>
public async Task<long> ClearAsync()
{
var connection = _connection.Value;
var database = connection.GetDatabase();
long removed = 0;
foreach (var endpoint in connection.GetEndPoints())
{
var server = connection.GetServer(endpoint);
if (server.IsReplica || !server.IsConnected)
{
continue;
}
foreach (var pattern in _keyPatterns)
{
var batch = new List<RedisKey>(BatchSize);
await foreach (var key in server.KeysAsync(database.Database, pattern, BatchSize))
{
batch.Add(key);
if (batch.Count == BatchSize)
{
removed += await DeleteAsync(database, batch);
batch.Clear();
}
}
removed += await DeleteAsync(database, batch);
}
}
// Dil sözlüğünün süreç içi kopyası Redis'te değildir; temizlik onu da kapsar.
_localizer.ClearLocalCache();
return removed;
}
// Anahtarlar tek tek silinir: çok anahtarlı DEL, cluster'da farklı slot'lara düşen anahtarları reddeder.
private static async Task<long> DeleteAsync(IDatabase database, List<RedisKey> keys)
{
var results = await Task.WhenAll(keys.Select(key => database.KeyDeleteAsync(key)));
return results.Count(deleted => deleted);
}
public void Dispose()
{
if (_connection.IsValueCreated)
{
_connection.Value.Dispose();
}
GC.SuppressFinalize(this);
}
}

View file

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Localization;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
@ -12,10 +13,12 @@ public class LanguageTextCacheItemInvalidator :
ITransientDependency
{
private readonly IDistributedCache<LanguageTextCacheItem> cache;
private readonly IDatabaseResourceLocalizer localizer;
public LanguageTextCacheItemInvalidator(IDistributedCache<LanguageTextCacheItem> cache)
public LanguageTextCacheItemInvalidator(IDistributedCache<LanguageTextCacheItem> cache, IDatabaseResourceLocalizer localizer)
{
this.cache = cache;
this.localizer = localizer;
}
public async Task HandleEventAsync(EntityChangedEventData<LanguageText> eventData)
@ -23,7 +26,7 @@ public class LanguageTextCacheItemInvalidator :
//distributedCacheOptions.Value.KeyPrefix
var key = LanguageTextCacheItem.CalculateCacheKey(eventData.Entity.ResourceName, eventData.Entity.CultureName);
await cache.RemoveAsync(key);
return;
localizer.ClearLocalCache();
}
}

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Sozsoft.Languages.Localization;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
@ -13,11 +14,14 @@ public class LanguageTextCacheItemInvalidatorDistributed :
ITransientDependency
{
private readonly IDistributedCache<LanguageTextCacheItem> cache;
private readonly IDatabaseResourceLocalizer localizer;
public LanguageTextCacheItemInvalidatorDistributed(
IDistributedCache<LanguageTextCacheItem> cache)
IDistributedCache<LanguageTextCacheItem> cache,
IDatabaseResourceLocalizer localizer)
{
this.cache = cache;
this.localizer = localizer;
}
public async Task HandleEventAsync(EntityCreatedEto<LanguageTextEto> eventData)
@ -39,6 +43,7 @@ public class LanguageTextCacheItemInvalidatorDistributed :
{
var key = LanguageTextCacheItem.CalculateCacheKey(eventData.ResourceName, eventData.CultureName);
await cache.RemoveAsync(key);
localizer.ClearLocalCache();
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -14,8 +15,14 @@ namespace Sozsoft.Languages.Localization;
public class DatabaseResourceLocalizer : IDatabaseResourceLocalizer, ISingletonDependency
{
// Her metin aramasında sözlüğün tamamı Redis'ten çekilip çözülüyordu. Süreç içi katman bunu önler.
// Kısa ömürlüdür: metinlerin bir kısmı ham SQL ile yazılır (olay üretmez) ve diğer API örnekleri ile
// DbMigrator ayrı süreçtir; onların temizliği buraya en geç bu süre sonunda yansır.
private static readonly long LocalCacheDurationMs = (long)TimeSpan.FromSeconds(30).TotalMilliseconds;
private readonly IRepository<LanguageText, Guid> languageTextRepository;
private readonly IDistributedCache<LanguageTextCacheItem> cache;
private readonly ConcurrentDictionary<string, (LanguageTextCacheItem Item, long ExpiresAt)> localCache = new();
public DatabaseResourceLocalizer(
IRepository<LanguageText, Guid> languageTextRepository,
@ -27,27 +34,52 @@ public class DatabaseResourceLocalizer : IDatabaseResourceLocalizer, ISingletonD
public LocalizedString GetOrNull(LocalizationResourceBase resource, string cultureName, string name)
{
var item = GetCacheItem(resource, cultureName);
var value = item.Dictionary?.GetOrDefault(name);
if (item == null || value == null)
{
return null;
}
return new LocalizedString(name, value);
var value = GetCacheItem(resource, cultureName)?.Dictionary?.GetOrDefault(name);
return value == null ? null : new LocalizedString(name, value);
}
/// <summary>Süreç içi katmanı boşaltır; dağıtık önbellek temizlendiğinde ve metin değiştiğinde çağrılır.</summary>
public void ClearLocalCache() => localCache.Clear();
protected LanguageTextCacheItem GetCacheItem(LocalizationResourceBase resource, string cultureName)
{
// culture, resource, key
var key = LanguageTextCacheItem.CalculateCacheKey(resource.ResourceName, cultureName);
return cache.GetOrAdd(key, () => CreateCacheItem(resource, cultureName));
if (TryGetLocal(key, out var local))
{
return local;
}
return SetLocal(key, cache.GetOrAdd(key, () => CreateCacheItem(resource, cultureName)));
}
private bool TryGetLocal(string key, out LanguageTextCacheItem item)
{
if (localCache.TryGetValue(key, out var entry) && entry.ExpiresAt > Environment.TickCount64)
{
item = entry.Item;
return true;
}
item = null;
return false;
}
private LanguageTextCacheItem SetLocal(string key, LanguageTextCacheItem item)
{
if (item != null)
{
localCache[key] = (item, Environment.TickCount64 + LocalCacheDurationMs);
}
return item;
}
public void Fill(LocalizationResourceBase resource, string cultureName, Dictionary<string, LocalizedString> dictionary)
{
var item = GetCacheItem(resource, cultureName);
foreach (var kvp in item.Dictionary)
foreach (var kvp in item?.Dictionary ?? [])
{
dictionary[kvp.Key] = new LocalizedString(kvp.Key, kvp.Value);
}
@ -67,9 +99,12 @@ public class DatabaseResourceLocalizer : IDatabaseResourceLocalizer, ISingletonD
public async Task FillAsync(LocalizationResourceBase resource, string cultureName, Dictionary<string, LocalizedString> dictionary)
{
var key = LanguageTextCacheItem.CalculateCacheKey(resource.ResourceName, cultureName);
var item = await cache.GetOrAddAsync(key, () => CreateCacheItemAsync(resource, cultureName));
if (!TryGetLocal(key, out var item))
{
item = SetLocal(key, await cache.GetOrAddAsync(key, () => CreateCacheItemAsync(resource, cultureName)));
}
foreach (var kvp in item.Dictionary)
foreach (var kvp in item?.Dictionary ?? [])
{
dictionary[kvp.Key] = new LocalizedString(kvp.Key, kvp.Value);
}

View file

@ -10,5 +10,6 @@ public interface IDatabaseResourceLocalizer
void Fill(LocalizationResourceBase resource, string cultureName, Dictionary<string, LocalizedString> dictionary);
Task FillAsync(LocalizationResourceBase resource, string cultureName, Dictionary<string, LocalizedString> dictionary);
LocalizedString GetOrNull(LocalizationResourceBase resource, string cultureName, string name);
void ClearLocalCache();
}

View file

@ -329,7 +329,7 @@ public class McpDevelopAppService(
if (listForm == null || string.IsNullOrWhiteSpace(relativePath) || SeedPathResolver.IsSeedFolderPath(relativePath))
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsNotAllowed]);
var path = SeedPathResolver.GetScopePath(configuration, CurrentTenant.Id, relativePath.Split('/'));
var path = SeedPathResolver.GetPath(configuration, relativePath.Split('/'));
var seedFile = File.Exists(path)
? JsonSerializer.Deserialize<ListFormSeedDataFileDto>(await File.ReadAllTextAsync(path), ListFormSeedDataFileDto.JsonOptions)
: null;

View file

@ -6,7 +6,7 @@ namespace Sozsoft.Mcp;
/// <summary>
/// Modulun paylasilan sozlesmesi. Kullaniciya gorunen metinler platformun kendi
/// <c>PlatformResource</c> kaynagindan (configs/seeds/host/data/App.Languages.LanguageText.json) okunur; modul ikinci bir dil
/// <c>PlatformResource</c> kaynagindan (configs/seeds/data/App.Languages.LanguageText.json) okunur; modul ikinci bir dil
/// kaynagi acmaz.
/// </summary>
[DependsOn(

View file

@ -1,7 +1,7 @@
namespace Sozsoft.Mcp;
/// <summary>
/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>configs/seeds/host/data/App.Languages.LanguageText.json</c>
/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>configs/seeds/data/App.Languages.LanguageText.json</c>
/// dosyasinda EN + TR olarak durur; koda gomulu metin kullanilmaz.
/// </summary>
public static class McpErrorCodes

View file

@ -265,14 +265,13 @@ public class McpTableCreator(
}
/// <summary>
/// Tasarimcinin <c>SaveTableScriptAsync</c> ile yazdigi yol: kapsam istegin kiracisini izler.
/// Tasarimcinin <c>SaveTableScriptAsync</c> ile yazdigi yol.
/// Dosyasiz tablo veritabani sifirlandiginda geri gelmezdi; yazilamazsa islem hata verir.
/// </summary>
private string SaveSeedScript(string tableName, string script)
{
var outputPath = SeedPathResolver.GetScopePath(
var outputPath = SeedPathResolver.GetPath(
configuration,
CurrentTenant.Id,
SeedPathResolver.GetProviderFolderName(isPostgreSql: false),
SeedPathResolver.ObjectFolder);
@ -281,7 +280,7 @@ public class McpTableCreator(
var filePath = Path.Combine(outputPath, $"{tableName}.sql");
File.WriteAllText(filePath, script);
return $"{SeedPathResolver.GetScopeFolderName(CurrentTenant.Id)}/{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{tableName}.sql";
return $"{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{tableName}.sql";
}
private void EnsureSafeIdentifier(string identifier)

View file

@ -118,44 +118,5 @@ public class SettingUiAppService : ApplicationService, ISettingUiAppService
}
}
}
/*
public override async Task<SettingDefinitionDto> CreateAsync(SettingDefinitionDto input)
{
await languageKeyIntegrationService.CreateKeysForSettingsAsync(
input.NameKey,
input.DescriptionKey,
input.MainGroupKey,
input.SubGroupKey);
return await base.CreateAsync(input);
}
public override async Task<SettingDefinitionDto> UpdateAsync(Guid id, SettingDefinitionDto input)
{
await languageKeyIntegrationService.CreateKeysForSettingsAsync(
input.NameKey,
input.DescriptionKey,
input.MainGroupKey,
input.SubGroupKey);
return await base.UpdateAsync(id, input);
}
public override async Task DeleteAsync(Guid id)
{
var key = await _repositorySettingDefinition.GetAsync(id);
if (key != null)
{
await languageKeyIntegrationService.DeleteKeysForSettingsAsync(
key.NameKey,
key.DescriptionKey,
key.MainGroupKey,
key.SubGroupKey);
}
await base.DeleteAsync(id);
}
*/
}

View file

@ -30,8 +30,8 @@ public interface ISqlObjectManagerAppService : IApplicationService
Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName);
/// <summary>
/// Saves the T-SQL script to {SeedsRoot}/{host|tenants/{tenantId}}/{sql|postgres}/object/{fileName}.sql
/// (repo: configs/seeds). The scope folder follows the current tenant: host context writes to host/.
/// Saves the T-SQL script to {SeedsRoot}/{sql|postgres}/object/{fileName}.sql
/// (repo: configs/seeds). The same file is seeded into the host and every tenant that has its own database.
/// Called automatically after a successful SqlTableDesigner deploy so the script can be re-seeded.
/// </summary>
Task SaveTableScriptAsync(SaveTableScriptDto input);

View file

@ -21,7 +21,7 @@ public class SqlQueryExecutionResultDto
/// <summary>
/// Input for saving a T-SQL script file to the seeds folder
/// ({App:SeedsPath}/{host|tenants/{tenantId}}/{sql|postgres}/object).
/// ({App:SeedsPath}/{sql|postgres}/object).
/// </summary>
public class SaveTableScriptDto
{
@ -38,7 +38,7 @@ public class SaveTableScriptDto
/// <summary>
/// Input for deleting seed files from the seeds folder
/// ({App:SeedsPath}/{host|tenants/{tenantId}}/{sql|postgres}/object).
/// ({App:SeedsPath}/{sql|postgres}/object).
/// </summary>
public class DeleteSqlDataFilesDto
{

View file

@ -960,23 +960,15 @@ FROM (
/// <summary>
/// Script dosyalarinin yazilacagi klasor. SqlDataSeeder ile ayni cozumleyiciyi kullanir
/// (App:SeedsPath -> configs/seeds), aksi halde deploy edilen nesne seed olarak geri okunmaz.
/// <para>
/// Yol, islemin yapildigi veritabani kapsamini izler: host (varsayilan) baglaminda
/// <c>host/{sql|postgres}/object</c>, bir tenant secilmisken
/// <c>tenants/{tenantId}/{sql|postgres}/object</c>. Boylece nesne, seed sirasinda
/// olusturuldugu veritabaninda yeniden olusturulur.
/// </para>
/// Dosya <c>{sql|postgres}/object</c> altindadir; nesne host'ta ve kendi veritabani olan her
/// tenant'ta seed sirasinda yeniden olusturulur.
/// </summary>
private string ResolveSqlDataOutputPath()
{
var providerDirectoryName = SeedPathResolver.GetProviderFolderName(
SettingsConsts.DefaultDatabaseProvider == SettingsConsts.DatabaseProvider.PostgreSql);
return SeedPathResolver.GetScopePath(
_configuration,
_currentTenant.Id,
providerDirectoryName,
SeedPathResolver.ObjectFolder);
return SeedPathResolver.GetPath(_configuration, providerDirectoryName, SeedPathResolver.ObjectFolder);
}
}

View file

@ -9,8 +9,6 @@ public class CreateUpdateTenantInput
public bool IsActive { get; set; }
public Guid? Id { get; set; }
public string AdminEmail { get; set; }
public string AdminPassword { get; set; }
public string ConnectionString { get; set; }
public string OrganizationName { get; set; }
public string Founder { get; set; }

View file

@ -32,7 +32,7 @@ public class GridOptionsEditDto : GridOptionsDto
set { SelectFieldsDefaultValueJson = JsonSerializer.Serialize(value); }
}
/// <summary>Kapsam klasorune gore goreli seed dosyasi yolu (ornegin <c>data/{ListFormCode}.json</c>).
/// <summary>Seed klasorune gore goreli seed dosyasi yolu (ornegin <c>data/{ListFormCode}.json</c>).
/// Dolu ise ekrandan yapilan kayit islemleri dosyaya yansitilir.</summary>
public string SeedFilePath { get; set; }

View file

@ -39,7 +39,7 @@ public class ModuleDocDto
/// <summary>Dosya adi, uzantisiz (<c>HrPerformance</c>).</summary>
public string Name { get; set; } = string.Empty;
/// <summary>Kapsam klasorune gore goreli yol (<c>docs/modules/HrPerformance.md</c>).</summary>
/// <summary>Seed klasorune gore goreli yol (<c>docs/modules/HrPerformance.md</c>).</summary>
public string Path { get; set; } = string.Empty;
public bool Exists { get; set; }

View file

@ -85,7 +85,7 @@ public class ListFormWizardDto
public bool ConfirmDelete { get; set; }
/// <summary>
/// Ekranin verisi kapsam klasorundeki <c>data/{ListFormCode}.json</c> dosyasina aynalanir
/// Ekranin verisi seed klasorundeki <c>data/{ListFormCode}.json</c> dosyasina aynalanir
/// (<c>SeedFilePath</c> + ekle/guncelle/sil senkronizasyonu). Istekle gelen ornek/demo satirlar
/// ekrandan girildiginde dosyaya yazilir ve migrate ile geri yuklenir.
/// </summary>

View file

@ -5,7 +5,7 @@ namespace Sozsoft.Platform.ListForms;
/// <summary>Export zip'ine girecek tek bir seed dosyasi.</summary>
public class WizardExportEntryDto
{
/// <summary>Kapsam klasorune goreli yol, ornegin <c>sql/object/Prj_T_Task.sql</c>.</summary>
/// <summary>Seed klasorune goreli yol, ornegin <c>sql/object/Prj_T_Task.sql</c>.</summary>
public string Path { get; set; }
/// <summary>Dosyanin arsive neden girdigi (wizard'in kendisi, alt form, lookup, veri...).</summary>
@ -27,9 +27,6 @@ public class WizardExportAnalysisDto
/// <summary>Indirilecek arsivin adi.</summary>
public string ArchiveName { get; set; }
/// <summary>Kapsam klasoru (<c>host</c> ya da <c>tenants/{tenantId}</c>).</summary>
public string ScopeFolder { get; set; }
public List<WizardExportEntryDto> Entries { get; set; } = [];
/// <summary>Bagimlilik olarak bulunan ama diskte karsiligi olmayan dosyalar.</summary>

View file

@ -5,11 +5,11 @@ namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Import edilen zip icindeki tek bir seed dosyasinin analiz sonucu.
/// Yol her zaman kapsam klasorune (host / tenants/{tenantId}) goreli tutulur.
/// Yol her zaman seed klasorune goreli tutulur.
/// </summary>
public class WizardImportEntryDto
{
/// <summary>Kapsam klasorune goreli yol, ornegin <c>crud/Orders.json</c>.</summary>
/// <summary>Seed klasorune goreli yol, ornegin <c>crud/Orders.json</c>.</summary>
public string Path { get; set; }
/// <summary>Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, data, sql, postgres).</summary>
@ -36,9 +36,6 @@ public class WizardImportAnalysisDto
/// <summary>Staging klasorunu ve rollback kaydini tanimlayan oturum kimligi.</summary>
public string ImportId { get; set; }
/// <summary>Dosyalarin yazilacagi kapsam klasoru (<c>host</c> ya da <c>tenants/{tenantId}</c>).</summary>
public string ScopeFolder { get; set; }
public List<WizardImportEntryDto> Entries { get; set; } = [];
/// <summary>Guvenlik/format nedeniyle atlanan zip girdileri icin aciklamalar.</summary>
@ -166,7 +163,7 @@ public class WizardImportCompleteResultDto
/// <summary>Import kapanisindaki tek bir adimin sonucu (SQL scripti ya da CRUD dosyasi).</summary>
public class WizardImportStepResultDto
{
/// <summary>Kapsam klasorune goreli yol.</summary>
/// <summary>Seed klasorune goreli yol.</summary>
public string Path { get; set; }
/// <summary>Adim basariliysa yapilan isi ozetleyen kisa metin.</summary>
@ -179,7 +176,7 @@ public class WizardImportStepResultDto
/// <summary>Tek bir veri seed dosyasinin veritabanina uygulanma sonucu.</summary>
public class WizardImportSeedApplyResultDto
{
/// <summary>Kapsam klasorune goreli yol.</summary>
/// <summary>Seed klasorune goreli yol.</summary>
public string Path { get; set; }
/// <summary>Veritabanina eklenen satir sayisi.</summary>

View file

@ -11,7 +11,7 @@ namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Wizard seed dosyası formatı.
/// WizardAppService tarafından Seeds/{host|tenants/{tenantId}}/wizard/{wizardName}.json olarak kaydedilir.
/// WizardAppService tarafından Seeds/wizard/{wizardName}.json olarak kaydedilir.
/// WizardDataSeeder (DbMigrator) bu dosyaları okuyarak veritabanını yeniden oluşturur.
/// </summary>
public class WizardSeedFileDto
@ -511,7 +511,7 @@ public class WizardDeleteTableDto
public WizardDeleteReasonEnum Reason { get; set; }
public List<WizardRecordRefDto> UsedBy { get; set; } = [];
/// <summary>Tabloyla birlikte silinecek seed dosyalari, kapsam klasorune gore goreli.</summary>
/// <summary>Tabloyla birlikte silinecek seed dosyalari, seed klasorune gore goreli.</summary>
public List<string> Files { get; set; } = [];
}

View file

@ -3,7 +3,7 @@
namespace Sozsoft.Platform.Permissions;
/// <summary>
/// Uygulama yetkileri kod içinde tanımlanmaz; <c>configs/seeds/host/data/Abp.Identity.Permissions.json</c>
/// Uygulama yetkileri kod içinde tanımlanmaz; <c>configs/seeds/data/Abp.Identity.Permissions.json</c>
/// dosyasından <c>ListFormSeedDataApplier</c> ile veritabanına seed edilir ve
/// oradan dinamik olarak yönetilir. Bu sağlayıcı yalnızca kullanılmayan
/// ABP modül gruplarını kaldırmak için vardır.

View file

@ -1,10 +0,0 @@
using System.Threading.Tasks;
using Sozsoft.Platform.ListForms.DynamicApi;
namespace Sozsoft.Platform.Tenants;
public interface IPlatformTenantAppService
{
Task PostSeedTenantDataAsync(CreateUpdateTenantInput input);
}

View file

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform;
using Sozsoft.Platform.Branchs;
using Sozsoft.Platform.Entities;
@ -18,10 +19,13 @@ public class BranchAppService : PlatformAppService
_branchSeedManager = branchSeedManager;
}
// Şubeler listesindeki BranchSeed komutu da bu yetkiyle görünür (ListFormSeeder_Saas).
[Authorize(PlatformConsts.AppCodes.Branches + ".Update")]
public async Task<BranchSeedResultDto> PostSeedAsync(Guid branchId)
{
// Branch kiracı filtresi taşımaz; kiracı kullanıcısı yalnızca kendi şubesini seed edebilir, host hepsini.
var branch = await _branchRepository.FirstOrDefaultAsync(x => x.Id == branchId);
if (branch == null)
if (branch == null || (CurrentTenant.Id.HasValue && branch.TenantId != CurrentTenant.Id))
return new BranchSeedResultDto { Success = false, Message = "Branch not found." };
using (CurrentTenant.Change(branch.TenantId))

View file

@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Entities;
using Volo.Abp.Application.Dtos;
@ -16,6 +17,23 @@ public class DataSourceAppService : CrudAppService<
{
public DataSourceAppService(IRepository<DataSource, Guid> repo) : base(repo)
{
// Okuma açık kalır: ListForm, Wizard ve SQL Query Manager açılır listeleri kodu ve türü okur.
CreatePolicyName = PlatformConsts.AppCodes.DataSource + ".Create";
UpdatePolicyName = PlatformConsts.AppCodes.DataSource + ".Update";
DeletePolicyName = PlatformConsts.AppCodes.DataSource + ".Delete";
}
protected override async Task<DataSourceDto> MapToGetOutputDtoAsync(DataSource entity)
=> WithoutSecret(await base.MapToGetOutputDtoAsync(entity));
protected override async Task<DataSourceDto> MapToGetListOutputDtoAsync(DataSource entity)
=> WithoutSecret(await base.MapToGetListOutputDtoAsync(entity));
// Bağlantı cümlesi veritabanı kimlik bilgisi taşır; hiçbir istemci ekranı onu okumaz.
private static DataSourceDto WithoutSecret(DataSourceDto dto)
{
dto.ConnectionString = null;
return dto;
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform.Domain.DeveloperKit;
@ -12,6 +13,11 @@ using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Application;
/// <summary>
/// Üretilmiş CRUD uçları ham tabloya yazar; bu yüzden giriş zorunludur ve veri isteği yapanın
/// kiracısıyla sınırlıdır (<see cref="IDynamicEntityManager"/>).
/// </summary>
[Authorize]
[RemoteService]
[Route("api/app/crudendpoint")]
public class CrudEndpointAppService : ApplicationService

View file

@ -203,7 +203,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
}
/// <summary>
/// Entity'nin guncel endpoint'lerini <c>{host|tenants/{tenantId}}/crud/{EntityName}.json</c>
/// Entity'nin guncel endpoint'lerini <c>crud/{EntityName}.json</c>
/// dosyasina yazar; endpoint kalmamissa dosyayi siler. Dosya islemi hatasi CRUD islemini
/// engellemez, yalnizca loglanir.
/// </summary>
@ -261,12 +261,12 @@ public class CrudEndpointGenerateAppService : CrudAppService<
}
/// <summary>
/// CRUD seed dosyalarinin yazilacagi klasor; kapsam aktif tenant'i izler
/// (host baglaminda <c>host/crud</c>, tenant baglaminda <c>tenants/{tenantId}/crud</c>).
/// CRUD seed dosyalarinin yazilacagi klasor (<c>crud/</c>); host ve kendi veritabani olan her
/// tenant ayni dosyalarla kurulur.
/// </summary>
private string ResolveCrudSeedOutputPath()
{
return SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id, SeedPathResolver.CrudFolder);
return SeedPathResolver.GetPath(_configuration, SeedPathResolver.CrudFolder);
}
/// <summary>Entity adindan guvenli bir dosya adi uretir; path traversal denemelerini reddeder.</summary>

View file

@ -10,14 +10,13 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.DeveloperKit;
using Volo.Abp.DependencyInjection;
namespace Sozsoft.Platform.DynamicServices;
/// <summary>
/// Dynamic C# kod derleme servisi
/// </summary>
public class DynamicServiceCompiler : ITransientDependency
public class DynamicServiceCompiler
{
private readonly ILogger<DynamicServiceCompiler> _logger;

View file

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using OpenIddict.Abstractions;
using Sozsoft.Platform.BlobStoring;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.Identity.Dto;
@ -79,6 +80,8 @@ public class PlatformIdentityAppService : ApplicationService
this.guidGenerator = guidGenerator;
}
// Kullanıcı detay ekranı (Users listesindeki Detail komutu) bu yetkiyle açılır.
[Authorize(IdentityPermissions.Users.Update)]
public async Task<UserInfoViewModel> GetByIdAsync(Guid UserId)
{
var user = await identityUserRepository.GetAsync(UserId);
@ -187,6 +190,7 @@ public class PlatformIdentityAppService : ApplicationService
}.MapIdentityExtraProperties(user);
}
[Authorize(IdentityPermissions.Users.ManageRoles)]
public async Task UpdatePermissionAsync(UserInfoViewModel UserInfo)
{
var roles = UserInfo.Roles.Where(r => r.IsAssigned).Select(r => r.Name).ToArray();
@ -202,10 +206,15 @@ public class PlatformIdentityAppService : ApplicationService
}
// 2. Yeni atamaları ekle
var assignedBranchIds = UserInfo.Branches
// Branch kiracı filtresi taşımaz: istemciden gelen kimlikler kiracının kendi şubelerine süzülür.
var requestedBranchIds = UserInfo.Branches
.Where(b => b.IsAssigned)
.Select(b => b.Id)
.ToList();
var assignedBranchIds = (await branchRepository.GetListAsync(
b => requestedBranchIds.Contains(b.Id) && b.TenantId == CurrentTenant.Id))
.Select(b => b.Id)
.ToList();
foreach (var branchId in assignedBranchIds)
{
@ -218,6 +227,7 @@ public class PlatformIdentityAppService : ApplicationService
}
}
[Authorize(IdentityPermissions.Users.Update)]
public async Task UpdateLockoutAsync(UserInfoViewModel UserInfo)
{
var user = await UserManager.GetByIdAsync(UserInfo.Id);
@ -255,6 +265,7 @@ public class PlatformIdentityAppService : ApplicationService
await UserManager.UpdateAsync(user);
}
[Authorize(IdentityPermissions.Users.Update)]
public async Task UpdateUserAsync(UserInfoViewModel UserInfo)
{
// 4857 s. Is Kanunu md. 15: deneme suresi en fazla iki ay, toplu sozlesmeyle dort ay.
@ -464,6 +475,8 @@ public class PlatformIdentityAppService : ApplicationService
return [.. list.OrderBy(p => p.Name)];
}
// Listelerdeki "Yetkili kullanıcılar" menüsü de bu yetkiyle görünür (useFilters).
[Authorize(PlatformConsts.AppCodes.Listforms.Listform + ".Update")]
public async Task<List<PermissionGrantedUsersDto>> GetPermissionGrantedUsersAsync(string permissionName)
{
if (string.IsNullOrWhiteSpace(permissionName))
@ -576,6 +589,7 @@ public class PlatformIdentityAppService : ApplicationService
}).ToList();
}
[Authorize(IdentityPermissions.Users.Update)]
public async Task CreateClaimUserAsync(UserClaimModel input)
{
var user = await identityUserRepository.GetAsync(input.UserId);
@ -583,6 +597,7 @@ public class PlatformIdentityAppService : ApplicationService
user.AddClaim(guidGenerator, new Claim(input.ClaimType, input.ClaimValue));
}
[Authorize(IdentityPermissions.Users.Update)]
public async Task DeleteClaimUser(Guid id, Guid userId)
{
var user = await identityUserRepository.GetAsync(userId);
@ -591,8 +606,12 @@ public class PlatformIdentityAppService : ApplicationService
user.Claims.Remove(claim);
}
[Authorize(SeedConsts.AppCodes.IdentityManagement.Sessions)]
public async Task KickUserAsync(Guid userId)
{
// OpenIddict token'ları kiracıya göre süzülmez; kullanıcının bu kiracıda olduğu önce doğrulanır.
_ = await identityUserRepository.GetAsync(userId, includeDetails: false);
using (CurrentTenant.Change(CurrentTenant.Id))
{
// 1. AbpSessions temizle

View file

@ -62,8 +62,9 @@ public enum SeedSyncOperation
}
/// <summary>
/// <c>ListForm.SeedFilePath</c> dolu olan ekranlarin verisi kapsam klasorunde aynalanir; veritabani
/// sifirlandiginda <c>ListFormSeedDataApplier</c> ayni yerden geri yukler.
/// <c>ListForm.SeedFilePath</c> dolu olan ekranlarin verisi seed klasorunde aynalanir; veritabani
/// sifirlandiginda <c>ListFormSeedDataApplier</c> ayni yerden geri yukler. Yalnizca host baglamindaki
/// kayitlar aynalanir; tenant baglamindaki kayit veritabaninda kalir.
/// Senkronizasyon satir bazlidir: yalnizca islem goren satir okunup karsiligina yazilir,
/// boylece her kayitta listenin tamami sorgulanmaz ve dosyadaki satir sirasi korunur.
/// <para>
@ -403,6 +404,11 @@ public class ListFormSeedDataSynchronizer(
Func<ListForm, SeedTarget, Task> action,
bool createTarget = true)
{
// Seed dosyalari urunun varsayilanidir ve host ile kendi veritabani olan her tenant'a kurulur;
// bir tenant'in kaydi dosyaya yazilsaydi o tenant'in verisi butun veritabanlarina yayilirdi.
if (currentTenant.Id != null)
return;
try
{
var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
@ -411,10 +417,7 @@ public class ListFormSeedDataSynchronizer(
var isFolder = SeedPathResolver.IsSeedFolderPath(listForm.SeedFilePath);
var fullPath = SeedPathResolver.GetScopePath(
configuration,
currentTenant.Id,
listForm.SeedFilePath.Replace('\\', '/').Trim('/').Split('/'));
var fullPath = SeedPathResolver.GetPath(configuration, listForm.SeedFilePath.Replace('\\', '/').Trim('/').Split('/'));
// Klasor ilk kayitta olusur; ekran, hedefi diskte hazir bulmak zorunda degildir.
if (createTarget)

View file

@ -20,6 +20,8 @@ namespace Sozsoft.Platform.ListForms.Select;
[Authorize]
public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppService
{
private static readonly JsonSerializerOptions CaseInsensitiveJson = new() { PropertyNameCaseInsensitive = true };
private readonly IRepository<ListForm, Guid> listFormRepository;
private readonly IRepository<ListFormField, Guid> listFormFieldRepository;
private readonly IRepository<ListFormWorkflow, string> listFormWorkflowRepository;
@ -148,6 +150,22 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
//kullaniciya ait ListForm verisini al
var listForm = await listFormManager.GetUserListForm(input.ListFormCode);
// "Tüm kayıtları sil" filtreye uyan satırları kalıcı siler: okuma yetkisi yetmez, ekranın
// silme yetkisi ve tasarımda açılmış AllowAllDeleting ayarı birlikte aranır.
if (input.CreateDeleteQuery)
{
var editingJson = listForm.EditingOptionJson;
var editing = string.IsNullOrWhiteSpace(editingJson)
? null
: JsonSerializer.Deserialize<GridEditingDto>(editingJson, CaseInsensitiveJson);
if (editing?.AllowAllDeleting != true
|| !await authManager.CanAccess(input.ListFormCode, AuthorizationTypeEnum.Delete))
{
throw new Volo.Abp.UserFriendlyException(L[PlatformConsts.AppErrorCodes.NoAuth]);
}
}
//kullaniciya ait ListFormField verilerini al
var fields = await listFormFieldManager.GetUserListFormFields(input.ListFormCode);
fields = fields.Where(a => a.CanRead).ToList();

View file

@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages;
using Sozsoft.Languages.Caching;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Data.Seeds;
@ -51,7 +51,7 @@ public class ListFormWizardAppService(
IRepository<Module, string> repoModule,
IRepository<DevRequest, Guid> repoDevRequest,
IConfiguration configuration,
LanguageTextAppService languageTextAppService,
DistributedCacheCleaner cacheCleaner,
IDynamicDataManager dynamicDataManager,
IListFormSeedDataApplier seedDataApplier,
ICrudEndpointSeedApplier crudEndpointSeedApplier,
@ -72,7 +72,7 @@ public class ListFormWizardAppService(
private readonly IRepository<Module, string> repoModule = repoModule;
private readonly IRepository<DevRequest, Guid> repoDevRequest = repoDevRequest;
private readonly IConfiguration _configuration = configuration;
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
private readonly DistributedCacheCleaner _cacheCleaner = cacheCleaner;
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
private readonly IListFormSeedDataApplier _seedDataApplier = seedDataApplier;
private readonly ICrudEndpointSeedApplier _crudEndpointSeedApplier = crudEndpointSeedApplier;
@ -1014,7 +1014,7 @@ public class ListFormWizardAppService(
}
var componentDependencies = fileByComponent.Count > 0
? await new WizardExportCollector(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id))
? await new WizardExportCollector(SeedPathResolver.GetRootPath(_configuration))
.ReadCustomComponentDependenciesAsync()
: [];
@ -1356,7 +1356,7 @@ public class ListFormWizardAppService(
// Bekleyen wizard'da veritabani degismedigi icin onbellekte gecersiz kalan bir sey yoktur.
if (!isPending)
await _languageTextAppService.ClearRedisCacheAsync();
await _cacheCleaner.ClearAsync();
await _moduleDocManager.RefreshAllAsync();
@ -1560,26 +1560,25 @@ public class ListFormWizardAppService(
}
}
/// <summary>Aktif kapsamin seed klasoru (<c>host</c> ya da <c>tenants/{tenantId}</c>).</summary>
/// <summary>Seed klasorunun tam yolu.</summary>
private string ResolveScopeRoot()
{
return Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
return Path.GetFullPath(SeedPathResolver.GetRootPath(_configuration));
}
/// <summary>
/// Wizard seed dosyalarinin yazilacagi wizard dizinini dondurur; kapsam aktif tenant'i izler
/// (host baglaminda <c>host/wizard</c>, tenant baglaminda <c>tenants/{tenantId}/wizard</c>).
/// Wizard seed dosyalarinin yazilacagi wizard dizinini (<c>wizard/</c>) dondurur.
/// Yol cozumu <see cref="SeedPathResolver"/> uzerinden seeder'lar ve File Manager ile ortaktir.
/// </summary>
private string ResolveWizardSeedOutputPath()
{
return SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id, SeedPathResolver.WizardFolder);
return SeedPathResolver.GetPath(_configuration, SeedPathResolver.WizardFolder);
}
/// <summary>
/// Wizard seed dosyasini ve tasinabilir olmasi icin gereken butun seed dosyalarini tek bir zip
/// icinde dondurur. Zip icindeki yollar seed kapsam klasoru (host / tenants/{tenantId}) koku baz
/// alinarak yazilir; dolayisiyla arsiv baska bir ortamda ayni klasor duzenine dogrudan acilabilir.
/// icinde dondurur. Zip icindeki yollar seed klasorunun koku baz alinarak
/// yazilir; dolayisiyla arsiv baska bir ortamda ayni klasor duzenine dogrudan acilabilir.
/// <para>
/// Bagimlilik kapanisi ozyinelemelidir (alt formlar, lookup nesneleri, ekran verisi, custom
/// component zinciri) — ayrinti icin <see cref="WizardExportCollector"/>. Cozulemeyen bir
@ -1678,7 +1677,6 @@ public class ListFormWizardAppService(
FileName = fileName,
WizardFiles = wizardFiles,
ArchiveName = archiveName,
ScopeFolder = SeedPathResolver.GetScopeFolderName(CurrentTenant.Id),
Entries = [.. items.Select(x => new WizardExportEntryDto
{
Path = x.Path,
@ -1695,7 +1693,7 @@ public class ListFormWizardAppService(
var seed = WizardSeedFileDto.FromSeedJson(await File.ReadAllTextAsync(filePath))
?? throw new UserFriendlyException("Dosya okunamadı.");
var collector = new WizardExportCollector(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
var collector = new WizardExportCollector(SeedPathResolver.GetRootPath(_configuration));
var items = await collector.CollectAsync([filePath]);
return (items, collector.Warnings, ArchiveName(seed.Wizard?.WizardName ?? Path.GetFileNameWithoutExtension(fileName), "Wizard"));
@ -1713,7 +1711,7 @@ public class ListFormWizardAppService(
var title = devRequest?.Title ?? files[0].Seed!.DevRequest.Title;
var fileNames = files.Select(f => f.FileName).ToList();
var collector = new WizardExportCollector(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
var collector = new WizardExportCollector(SeedPathResolver.GetRootPath(_configuration));
var items = await collector.CollectAsync([.. fileNames.Select(ResolveWizardSeedFilePath)]);
return (items, collector.Warnings, ArchiveName(title, "DevRequest"), fileNames);
@ -1757,7 +1755,7 @@ public class ListFormWizardAppService(
private const int ImportMaxEntryCount = 500;
/// <summary>Kapsam klasorunde import edilmesine izin verilen kok klasorler.</summary>
/// <summary>Seed klasorunde import edilmesine izin verilen kok klasorler.</summary>
private static readonly string[] ImportAllowedRootFolders =
[
SeedPathResolver.WizardFolder,
@ -1811,12 +1809,11 @@ public class ListFormWizardAppService(
var importId = GuidGenerator.Create().ToString("N");
var sessionPath = GetImportSessionPath(importId);
var stagedRoot = Path.Combine(sessionPath, ImportStagedFolder);
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(_configuration));
var result = new WizardImportAnalysisDto
{
ImportId = importId,
ScopeFolder = SeedPathResolver.GetScopeFolderName(CurrentTenant.Id),
DataSourceCode = SeedConsts.DataSources.DefaultCode,
};
@ -2165,7 +2162,7 @@ public class ListFormWizardAppService(
if (!File.Exists(stagedPath))
throw new UserFriendlyException($"Import oturumunda dosya bulunamadı: {relativePath}");
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(_configuration));
var targetPath = Path.GetFullPath(Path.Combine(scopeRoot, systemPath));
if (!targetPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
throw new UserFriendlyException("Geçersiz dosya yolu.");
@ -2247,7 +2244,7 @@ public class ListFormWizardAppService(
// burada da denetlenmezse bu uc dogrudan cagrilarak analiz karari atlanabilirdi.
EnsureImportNotBlocked(sessionPath);
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(_configuration));
var result = new WizardImportCompleteResultDto();
var providerFolder = SeedPathResolver.GetProviderFolderName(DefaultDatabaseProvider == DatabaseProvider.PostgreSql);
@ -2419,7 +2416,7 @@ public class ListFormWizardAppService(
}
/// <summary>
/// Kapsam klasorundeki bir seed scriptini calistirir. Script'ler varlik kontrollu yazilir
/// Seed klasorundeki bir seed scriptini calistirir. Script'ler varlik kontrollu yazilir
/// (nesne varsa olusturma atlanir), bu yuzden yeniden calistirilmalari guvenlidir. Hata tek
/// script ile sinirli kalir; kalan adimlar yurumeye devam eder ve sonuc kullaniciya raporlanir.
/// </summary>
@ -2533,7 +2530,7 @@ public class ListFormWizardAppService(
{
var sessionPath = GetExistingImportSessionPath(importId);
var logPath = Path.Combine(sessionPath, ImportAppliedLogFile);
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(_configuration));
if (File.Exists(logPath))
{
@ -2725,9 +2722,10 @@ public class ListFormWizardAppService(
}
/// <summary>
/// Zip girdisini kapsam klasorune goreli yola cevirir. Zip, kapsam klasorunun icinden
/// (<c>wizard/x.json</c>) ya da depo kokunden (<c>configs/seeds/host/wizard/x.json</c>)
/// alinmis olabilir; her iki durumda da izin verilen ilk kok klasorden itibaren okunur.
/// Zip girdisini seed klasorune goreli yola cevirir. Zip, seed klasorunun icinden
/// (<c>wizard/x.json</c>) ya da depo kokunden (<c>configs/seeds/wizard/x.json</c>) alinmis
/// olabilir; eski arsivler <c>host/</c> onekini de tasir. Her durumda izin verilen ilk kok
/// klasorden itibaren okunur.
/// Kural disi bir yol icin null doner.
/// </summary>
private static string? NormalizeImportEntryPath(string? entryPath)
@ -2750,7 +2748,8 @@ public class ListFormWizardAppService(
var isDocsFolder = segments[0].Equals(SeedPathResolver.DocsFolder, StringComparison.OrdinalIgnoreCase);
// sql/postgres altinda object|execute klasoru zorunludur; digerlerinde alt klasor yoktur.
// sql/postgres altinda object|execute klasoru zorunludur (yalnizca host'ta calisanlar icin
// host/object|execute); digerlerinde alt klasor yoktur.
if (isDocsFolder)
{
// Yalnizca modul dokumanlari: docs/modules/{Modul}.md.
@ -2759,9 +2758,16 @@ public class ListFormWizardAppService(
}
else if (isScriptFolder)
{
if (segments.Length != 3 || !ImportAllowedScriptFolders.Contains(segments[1], StringComparer.OrdinalIgnoreCase))
var isHostOnly = segments.Length == 4
&& segments[1].Equals(SeedPathResolver.HostOnlyFolder, StringComparison.OrdinalIgnoreCase);
var scriptFolder = isHostOnly ? segments[2] : segments[1];
if (segments.Length != (isHostOnly ? 4 : 3)
|| !ImportAllowedScriptFolders.Contains(scriptFolder, StringComparer.OrdinalIgnoreCase))
{
return null;
}
}
else if (isDataFolder)
{
// Veri ya tek dosyadadir (data/{ListFormCode}.json) ya da satir basina dosya tutan bir
@ -2856,7 +2862,7 @@ public class ListFormWizardAppService(
}
/// <summary>
/// Kapsam klasorune goreli yolu var olan bir dosyanin tam yoluna cevirir; kapsam disina cikan
/// Seed klasorune goreli yolu var olan bir dosyanin tam yoluna cevirir; kapsam disina cikan
/// ya da bulunmayan bir yol icin null doner.
/// </summary>
private static string? ResolveScopeFilePath(string scopeRoot, string relativePath)

View file

@ -10,7 +10,7 @@ using Sozsoft.Platform.Data.Seeds;
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Bir kapsam klasorundeki wizard dosyalarini ve modul dokumanlarini okur; her wizard'in hangi
/// Bir seed klasorundeki wizard dosyalarini ve modul dokumanlarini okur; her wizard'in hangi
/// dokumana ait oldugunu cozer. Kural, wizard sozlesmesine alan eklemeden menu agacindan cikar:
/// <list type="number">
/// <item>Bir dokuman ekranin kodunu basligindaki beyanla sahiplenmisse (<c>module-doc: claims=…</c>) o dokuman.</item>

View file

@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Data.Seeds;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.ListForms;
@ -20,12 +19,11 @@ namespace Sozsoft.Platform.ListForms;
/// </summary>
public class ModuleDocManager(
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<ModuleDocManager> logger) : ITransientDependency
{
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private string ScopeRoot => Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, currentTenant.Id));
private string ScopeRoot => Path.GetFullPath(SeedPathResolver.GetRootPath(configuration));
public Task<ModuleDocCatalog> LoadCatalogAsync() => ModuleDocCatalog.LoadAsync(ScopeRoot);

View file

@ -461,7 +461,7 @@ public sealed class WizardExportCollector(string scopeRoot)
}
/// <summary>
/// Kapsamdaki script klasorlerinin kapsam-goreli yollari (<c>sql/object</c>, <c>postgres/execute</c> …),
/// Script klasorlerinin seed-goreli yollari (<c>sql/object</c>, <c>postgres/execute</c>, <c>sql/host/object</c> …),
/// diskte gercekten var olanlar. Bir kez taranir; kapsam kokunun disina cikilmaz.
/// </summary>
private IReadOnlyList<string> ScriptFolders => scriptFolders ??= ScanScriptFolders();
@ -486,7 +486,21 @@ public sealed class WizardExportCollector(string scopeRoot)
}
foreach (var scriptPath in SafeEnumerateDirectories(providerPath))
folders.Add($"{provider}/{System.IO.Path.GetFileName(scriptPath)}");
{
var scriptFolder = System.IO.Path.GetFileName(scriptPath);
// {sql|postgres}/host yalnizca host'ta calisan scriptlerin kabidir; scriptler bir
// seviye altta, kendi object/execute klasorlerindedir.
if (scriptFolder.Equals(SeedPathResolver.HostOnlyFolder, StringComparison.OrdinalIgnoreCase))
{
foreach (var hostScriptPath in SafeEnumerateDirectories(scriptPath))
folders.Add($"{provider}/{scriptFolder}/{System.IO.Path.GetFileName(hostScriptPath)}");
continue;
}
folders.Add($"{provider}/{scriptFolder}");
}
}
return folders;

View file

@ -12,7 +12,6 @@ using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Extensions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement;
@ -58,7 +57,6 @@ public class WizardRecordCleaner(
ITenantRepository tenantRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<WizardRecordCleaner> logger) : ITransientDependency
{
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
@ -192,7 +190,7 @@ public class WizardRecordCleaner(
return;
}
var path = SeedPathResolver.GetScopePath(configuration, currentTenant.Id, relativePath.Split('/'));
var path = SeedPathResolver.GetPath(configuration, relativePath.Split('/'));
if (SeedPathResolver.IsSeedFolderPath(relativePath))
{
if (!Directory.Exists(path))

View file

@ -28,7 +28,7 @@ public sealed class WizardSeedRowStore
public static readonly string PermissionFile = SeedConsts.AbpIdentity.Permissions.Default;
public static readonly string LanguageTextFile = PlatformConsts.AppCodes.Languages.LanguageText;
// Sira, kapsam klasorundeki mevcut dosyalarla aynidir: once tanim, sonra ona referans veren satir.
// Sira, seed klasorundeki mevcut dosyalarla aynidir: once tanim, sonra ona referans veren satir.
private static readonly FileSpec[] Specs =
[
new(ModuleFile, "Id", 2),
@ -48,7 +48,7 @@ public sealed class WizardSeedRowStore
{
}
/// <summary>Kapsam klasorundeki dosyalari okur; olmayan dosya bos olarak baslar ve ilk yazimda olusur.</summary>
/// <summary>Seed klasorundeki dosyalari okur; olmayan dosya bos olarak baslar ve ilk yazimda olusur.</summary>
public static async Task<WizardSeedRowStore> LoadAsync(string scopeRoot)
{
var store = new WizardSeedRowStore();

View file

@ -44,7 +44,9 @@ public class WizardSeedSynchronizer(
public async Task SyncAsync(string listFormCode)
{
if (string.IsNullOrWhiteSpace(listFormCode))
// Wizard dosyasi host ile kendi veritabani olan her tenant'a kurulur; tenant'in ekran
// duzenlemesi dosyaya yazilsaydi butun veritabanlarina yayilirdi.
if (string.IsNullOrWhiteSpace(listFormCode) || currentTenant.Id != null)
return;
try
@ -89,13 +91,13 @@ public class WizardSeedSynchronizer(
}
/// <summary>
/// Kapsam klasorundeki wizard dosyalari icinde bu ListForm koduna ait olani bulur. Dosya adi
/// Seed klasorundeki wizard dosyalari icinde bu ListForm koduna ait olani bulur. Dosya adi
/// wizard adiyla ayni oldugu icin once dogrudan ad denenir; eski ya da elle adlandirilmis
/// dosyalar icin klasor taranir.
/// </summary>
private async Task<(string FilePath, WizardSeedFileDto Seed)?> FindSeedFileAsync(string listFormCode)
{
var wizardPath = SeedPathResolver.GetScopePath(configuration, currentTenant.Id, SeedPathResolver.WizardFolder);
var wizardPath = SeedPathResolver.GetPath(configuration, SeedPathResolver.WizardFolder);
if (!Directory.Exists(wizardPath))
return null;

View file

@ -12,7 +12,6 @@ using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.ListForms;
@ -38,7 +37,6 @@ public class WizardTableCleaner(
IRepository<CrudEndpoint, Guid> repoCrudEndpoint,
IDynamicDataManager dynamicDataManager,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<WizardTableCleaner> logger) : ITransientDependency
{
private const int MaxUsedBy = 20;
@ -353,7 +351,7 @@ public class WizardTableCleaner(
// ── Yardimcilar ────────────────────────────────────────────────────────────────
private string ScopeFile(string relativePath) =>
SeedPathResolver.GetScopePath(configuration, currentTenant.Id, relativePath.Split('/'));
SeedPathResolver.GetPath(configuration, relativePath.Split('/'));
/// <summary>Sema yazilmamissa null doner; varsayilan sema saglayiciya gore <see cref="ReadTableStateAsync"/>'ta atanir.</summary>
private static (string? SchemaName, string TableName) ParseTableName(string selectCommand)

View file

@ -5,7 +5,7 @@ using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages;
using Sozsoft.Languages.Caching;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Entities;
@ -32,7 +32,7 @@ public class MenuAppService : CrudAppService<
private readonly IRepository<LanguageText, Guid> _repositoryText;
private readonly ITenantRepository _tenantRepository;
private readonly IPermissionDefinitionRecordRepository _permissionRepository;
private readonly LanguageTextAppService _languageTextAppService;
private readonly DistributedCacheCleaner _cacheCleaner;
private readonly IRepository<Module, string> _moduleRepository;
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
@ -42,7 +42,7 @@ public class MenuAppService : CrudAppService<
IRepository<LanguageText, Guid> languageTextRepository,
ITenantRepository tenantRepository,
IPermissionDefinitionRecordRepository permissionRepository,
LanguageTextAppService languageTextAppService,
DistributedCacheCleaner cacheCleaner,
IRepository<Module, string> moduleRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer
) : base(menuRepository)
@ -52,7 +52,7 @@ public class MenuAppService : CrudAppService<
_repositoryText = languageTextRepository;
_tenantRepository = tenantRepository;
_permissionRepository = permissionRepository;
_languageTextAppService = languageTextAppService;
_cacheCleaner = cacheCleaner;
_moduleRepository = moduleRepository;
_seedDataSynchronizer = seedDataSynchronizer;
@ -379,7 +379,7 @@ public class MenuAppService : CrudAppService<
}
}
await _languageTextAppService.ClearRedisCacheAsync();
await _cacheCleaner.ClearAsync();
// Dil metinleri de kendi ekraninin seed dosyasinda yasar; menu adini runtime'da
// degistirmek DB sifirlandiginda geri gelmeli.

View file

@ -2,27 +2,21 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.ListForms.DynamicApi;
using Sozsoft.Platform.Localization;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Identity;
using Volo.Abp.TenantManagement;
namespace Sozsoft.Platform.Tenants;
[Authorize(TenantManagementPermissions.Tenants.Default)]
[Dependency(ReplaceServices = true)]
public class PlatformTenantAppService : TenantAppService, IPlatformTenantAppService
public class PlatformTenantAppService : TenantAppService
{
private readonly ITenantRepository tenantRepository;
private readonly IDistributedEventBus distributedEventBus;
public PlatformTenantAppService(
ITenantRepository tenantRepository,
ITenantManager tenantManager,
@ -30,9 +24,6 @@ public class PlatformTenantAppService : TenantAppService, IPlatformTenantAppServ
IDistributedEventBus distributedEventBus,
ILocalEventBus localEventBus) : base(tenantRepository, tenantManager, dataSeeder, distributedEventBus, localEventBus)
{
this.tenantRepository = tenantRepository;
this.distributedEventBus = distributedEventBus;
LocalizationResource = typeof(PlatformResource);
}
@ -93,23 +84,5 @@ public class PlatformTenantAppService : TenantAppService, IPlatformTenantAppServ
return dto;
}
[Authorize(TenantManagementPermissions.Tenants.Update)]
public async Task PostSeedTenantDataAsync(CreateUpdateTenantInput input)
{
var tenant = (input.Id.HasValue ? await tenantRepository.FindAsync(input.Id.Value) : null) ?? throw new EntityNotFoundException();
await distributedEventBus.PublishAsync(
new ApplyDatabaseMigrationsEto
{
TenantId = tenant.Id,
DatabaseName = tenant.Name,
Properties =
{
{ IdentityDataSeedContributor.AdminEmailPropertyName, input.AdminEmail },
{ IdentityDataSeedContributor.AdminPasswordPropertyName, input.AdminPassword },
{ IdentityDataSeedContributor.AdminUserNamePropertyName, input.AdminEmail }
}
});
}
}

View file

@ -6,7 +6,7 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// SqlQueryManager uzerinden uretilen CRUD endpoint'lerini
/// <c>{host|tenants/{tenantId}}/crud/*.json</c> dosyalarindan okuyarak veritabanina uygular.
/// <c>crud/*.json</c> dosyalarindan okuyarak veritabanina uygular.
/// <para>
/// <see cref="SqlDataSeeder"/> tablolari olusturduktan hemen sonra calisir; boylece endpoint'ler
/// her zaman dayandiklari tablolar hazir olduktan sonra yazilir. ABP'nin contributor sirasi

View file

@ -10,18 +10,19 @@ using Microsoft.Extensions.Logging;
using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Seeds/{host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki .sql dosyaları
/// iki aşamada işlenir:
/// 1) Migration/seed sırasında <see cref="SqlDataSeeder"/> tarafından, kapsam klasörüyle
/// (host/tenants/{tenantId}) aynı bağlamda çalıştırılır (nesne oluşturulur/güncellenir).
/// Seeds/{sql|postgres}/execute klasöründeki .sql dosyaları iki aşamada işlenir:
/// 1) Migration/seed sırasında <see cref="SqlDataSeeder"/> tarafından çalıştırılır (nesne
/// oluşturulur/güncellenir).
/// 2) Tüm migration ve seed işlemleri bittikten sonra bu sınıf, dosya adından türetilen stored
/// procedure'ü ilgili bağlamda (host ya da tenant) varlığı doğrulanarak çalıştırır.
/// procedure'ü çağıranın seçtiği bağlamda (host ya da kendi veritabanı olan tenant) varlığı
/// doğrulanarak çalıştırır. {sql|postgres}/host/execute yalnızca host bağlamında işlenir.
/// Dosya adı = procedure adı. İsteğe bağlı olarak "schema.Procedure.sql" formatı da desteklenir.
/// </summary>
public class FinalSeeder : IFinalSeeder, ITransientDependency
@ -39,44 +40,39 @@ public class FinalSeeder : IFinalSeeder, ITransientDependency
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConfiguration _configuration;
private readonly ICurrentTenant _currentTenant;
private readonly ILogger<FinalSeeder> _logger;
public FinalSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IUnitOfWorkManager unitOfWorkManager,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<FinalSeeder> logger)
{
_dbContextProvider = dbContextProvider;
_unitOfWorkManager = unitOfWorkManager;
_configuration = configuration;
_currentTenant = currentTenant;
_logger = logger;
}
public async Task ExecuteAsync(Guid? tenantId = null)
public async Task ExecuteAsync()
{
var directoryPath = GetScriptDirectoryPath(tenantId);
if (!Directory.Exists(directoryPath))
{
_logger.LogInformation("{Directory} directory not found, skipping FinalSeeder.", directoryPath);
return;
}
var sqlFiles = Directory.GetFiles(directoryPath, "*.sql")
.OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)
// Ortak procedure'ler once, host'a ozgu olanlar (host/execute) yalnizca host baglaminda sonra.
var sqlFiles = GetScriptDirectoryPaths()
.Where(Directory.Exists)
.SelectMany(path => Directory.GetFiles(path, "*.sql")
.OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase))
.ToArray();
if (sqlFiles.Length == 0)
{
_logger.LogInformation("No .sql files found in {Directory}, skipping FinalSeeder.", directoryPath);
_logger.LogInformation("No execute script found, skipping FinalSeeder.");
return;
}
_logger.LogInformation(
"FinalSeeder started for {Scope}. {Count} procedure(s) to be executed.",
tenantId == null ? "Host" : $"Tenant {tenantId}",
sqlFiles.Length);
_logger.LogInformation("FinalSeeder started. {Count} procedure(s) to be executed.", sqlFiles.Length);
var failed = new List<string>();
@ -133,12 +129,15 @@ public class FinalSeeder : IFinalSeeder, ITransientDependency
}
}
private string GetScriptDirectoryPath(Guid? tenantId)
private IEnumerable<string> GetScriptDirectoryPaths()
{
var providerDirectoryName = SeedPathResolver.GetProviderFolderName(
DefaultDatabaseProvider == DatabaseProvider.PostgreSql);
return SeedPathResolver.GetScopePath(_configuration, tenantId, providerDirectoryName, DirectoryName);
yield return SeedPathResolver.GetPath(_configuration, providerDirectoryName, DirectoryName);
if (_currentTenant.Id == null)
yield return SeedPathResolver.GetPath(_configuration, providerDirectoryName, SeedPathResolver.HostOnlyFolder, DirectoryName);
}
/// <summary>

View file

@ -14,7 +14,7 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Varsayilan veri kaynagini olusturur. Baglanti cumlesi ortamin appsettings dosyasindan gelir;
/// bu yuzden diger host verisi gibi <c>configs/seeds/host/data</c> altina yazilamaz.
/// bu yuzden diger platform verisi gibi <c>configs/seeds/data</c> altina yazilamaz.
/// </summary>
public class HostDataSeeder : IDataSeedContributor, ITransientDependency
{

View file

@ -40,7 +40,6 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
{
var configurationSection = _configuration.GetSection("OpenIddict:Applications");
var swaggerRootUrl = configurationSection["Platform_Swagger:RootUrl"]?.TrimEnd('/');
var utils = new ListFormSeeder_Utils(_listFormRepository, _listFormFieldRepository);
var listFormName = String.Empty;
#region Permission Group

View file

@ -5,14 +5,12 @@ using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Localization;
using Sozsoft.Languages.Languages;
using Sozsoft.Mcp;
using Sozsoft.Platform.Editors;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Permissions;
using Sozsoft.Platform.Queries;
using Volo.Abp.Data;
@ -31,28 +29,24 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
private readonly IRepository<ListFormField, Guid> _listFormFieldRepository;
private readonly IRepository<ListFormWorkflow, string> _listFormWorkflowRepository;
private readonly IConfiguration _configuration;
protected IStringLocalizer<PlatformResource> _localizer { get; }
public ListFormSeeder_Saas(
IRepository<ListForm, Guid> listFormRepository,
IRepository<ListFormField, Guid> listFormFieldRepository,
IRepository<ListFormWorkflow, string> listFormWorkflowRepository,
IConfiguration configuration,
IStringLocalizer<PlatformResource> localizer
IConfiguration configuration
)
{
_listFormRepository = listFormRepository;
_listFormFieldRepository = listFormFieldRepository;
_listFormWorkflowRepository = listFormWorkflowRepository;
_configuration = configuration;
_localizer = localizer;
}
public async Task SeedAsync(DataSeedContext context)
{
var configurationSection = _configuration.GetSection("OpenIddict:Applications");
var swaggerRootUrl = configurationSection["Platform_Swagger:RootUrl"]?.TrimEnd('/');
var utils = new ListFormSeeder_Utils(_listFormRepository, _listFormFieldRepository);
var listFormName = String.Empty;
#region Tenant

View file

@ -1,212 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Data.Seeds;
public class ListFormSeeder_Utils
{
private readonly IRepository<ListForm, Guid> _listFormRepository;
private readonly IRepository<ListFormField, Guid> _listFormFieldRepository;
public ListFormSeeder_Utils(
IRepository<ListForm, Guid> listFormRepository,
IRepository<ListFormField, Guid> listFormFieldRepository)
{
_listFormRepository = listFormRepository;
_listFormFieldRepository = listFormFieldRepository;
}
public static string GetDefaultFormCodes(string listFormCode) => "form-" + listFormCode;
public async Task CloneFormLayoutAsync(string listFormCode, string subFormsJson)
{
var newListFormCode = GetDefaultFormCodes(listFormCode);
var cloneForm = await CloneListFormWithFieldsAsync(listFormCode, newListFormCode);
if (cloneForm != null)
{
await SetSubFormsJsonAsync(cloneForm, subFormsJson);
}
else
{
Console.WriteLine($"ListForm with code {listFormCode} not found!");
}
}
public async Task<ListForm> CloneListFormWithFieldsAsync(string listFormCode, string newListFormCode, bool copyFormFields = true)
{
var listForm = await _listFormRepository.FirstOrDefaultAsync(f => f.ListFormCode == listFormCode);
if (listForm == null)
{
Console.WriteLine($"ListForm with code {listFormCode} not found!");
return null;
}
var targetForm = await _listFormRepository.FirstOrDefaultAsync(f => f.ListFormCode == newListFormCode);
if (targetForm == null)
{
var insertForm = new ListForm
{
ListFormCode = newListFormCode,
SubFormsJson = listForm.SubFormsJson,
WidgetsJson = listForm.WidgetsJson,
WorkflowJson = listForm.WorkflowJson,
ListFormType = listForm.ListFormType,
IsSubForm = listForm.IsSubForm,
ShowNote = listForm.ShowNote,
LayoutJson = listForm.LayoutJson,
CultureName = listForm.CultureName,
Name = listForm.Name,
Title = listForm.Title,
DataSourceCode = listForm.DataSourceCode,
IsTenant = listForm.IsTenant,
IsBranch = listForm.IsBranch,
IsOrganizationUnit = listForm.IsOrganizationUnit,
Description = listForm.Description,
SelectCommandType = listForm.SelectCommandType,
SelectCommand = listForm.SelectCommand,
KeyFieldName = listForm.KeyFieldName,
KeyFieldDbSourceType = listForm.KeyFieldDbSourceType,
DefaultFilter = listForm.DefaultFilter,
SortMode = listForm.SortMode,
PermissionJson = listForm.PermissionJson,
DeleteCommand = listForm.DeleteCommand,
DeleteFieldsDefaultValueJson = listForm.DeleteFieldsDefaultValueJson,
EditingOptionJson = listForm.EditingOptionJson,
EditingFormJson = listForm.EditingFormJson,
InsertFieldsDefaultValueJson = listForm.InsertFieldsDefaultValueJson,
FormFieldsDefaultValueJson = listForm.FormFieldsDefaultValueJson,
CommandColumnJson = listForm.CommandColumnJson,
InsertServiceAddress = listForm.InsertServiceAddress,
UpdateServiceAddress = listForm.UpdateServiceAddress,
FilterRowJson = listForm.FilterRowJson,
HeaderFilterJson = listForm.HeaderFilterJson,
SearchPanelJson = listForm.SearchPanelJson,
GroupPanelJson = listForm.GroupPanelJson,
SelectionJson = listForm.SelectionJson,
ColumnOptionJson = listForm.ColumnOptionJson,
PagerOptionJson = listForm.PagerOptionJson,
};
// insert cloned form
targetForm = await _listFormRepository.InsertAsync(insertForm, autoSave: true);
}
if (copyFormFields)
{
var targetFormFields = await _listFormFieldRepository.GetListAsync(f => f.ListFormCode == listFormCode);
if (targetFormFields != null && targetFormFields.Count > 0)
{
var clonedFields = new List<ListFormField>();
foreach (var f in targetFormFields)
{
var newField = new ListFormField
{
ListFormCode = newListFormCode,
CultureName = f.CultureName,
SourceDbType = f.SourceDbType,
FieldName = f.FieldName,
Width = f.Width,
ListOrderNo = f.ListOrderNo,
Visible = f.Visible,
IsActive = f.IsActive,
AllowSearch = f.AllowSearch,
LookupJson = f.LookupJson,
ColumnCustomizationJson = f.ColumnCustomizationJson,
PermissionJson = f.PermissionJson,
PivotSettingsJson = f.PivotSettingsJson,
ValidationRuleJson = f.ValidationRuleJson,
EditorOptions = f.EditorOptions,
SortIndex = f.SortIndex,
SortDirection = f.SortDirection,
};
clonedFields.Add(newField);
}
await _listFormFieldRepository.InsertManyAsync(clonedFields, autoSave: true);
}
}
return targetForm;
}
private async Task SetSubFormsJsonAsync(ListForm listForm, string subFormsJson)
{
listForm.ListFormType = ListFormTypeEnum.Form;
listForm.SubFormsJson = subFormsJson;
await _listFormRepository.UpdateAsync(listForm, autoSave: true);
//Eksik olan form alanlarını ekle
var subForms = JsonSerializer.Deserialize<List<SubFormDto>>(subFormsJson);
if (subForms != null)
{
foreach (var subForm in subForms)
{
foreach (var sf in subForm.Relation)
{
var parentFieldName = sf.ParentFieldName;
var dbType = sf.DbType;
await InsertListFormFieldAsync(
listForm.ListFormCode,
parentFieldName,
dbType
);
var childListFormCode = subForm.Code;
var childFieldName = sf.ChildFieldName;
await InsertListFormFieldAsync(
childListFormCode,
childFieldName,
dbType
);
}
}
}
}
private async Task InsertListFormFieldAsync(string listFormCode, string fieldName, DbType dbType)
{
var form = await _listFormRepository.FirstOrDefaultAsync(f => f.ListFormCode == listFormCode);
var fieldQuery = await _listFormFieldRepository.GetQueryableAsync();
var formField = await fieldQuery.FirstOrDefaultAsync(f => f.ListFormCode == listFormCode && f.FieldName == fieldName);
if (formField == null)
{
var listOrderNo = await fieldQuery.Where(f => f.ListFormCode == listFormCode).MaxAsync(a => a.ListOrderNo) ?? 0;
var field = new ListFormField
{
CultureName = PlatformConsts.DefaultLanguage,
ListFormCode = listFormCode,
SourceDbType = dbType,
FieldName = fieldName,
ListOrderNo = listOrderNo + 1,
Visible = false,
IsActive = true,
SortIndex = 0,
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = form.Name + ".Create",
R = form.Name,
U = form.Name + ".Update",
E = true,
I = false,
Deny = false
}),
};
await _listFormFieldRepository.InsertAsync(field, autoSave: true);
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
@ -7,6 +8,7 @@ using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.Permissions;
using Volo.Abp;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
@ -14,6 +16,7 @@ using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement;
using Volo.Abp.Uow;
using IdentityRole = Volo.Abp.Identity.IdentityRole;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
@ -23,6 +26,8 @@ namespace Sozsoft.Platform.Data.Seeds;
public class PlatformIdentityDataSeeder : IdentityDataSeeder
{
private readonly IPermissionGrantRepository _permissionGrantRepository;
private readonly IPermissionDefinitionRecordRepository _permissionDefinitionRecordRepository;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly ITenantRepository _tenantRepository;
private readonly IRepository<Branch, Guid> _branchRepository;
private readonly IRepository<BranchUsers, Guid> _branchUsersRepository;
@ -38,6 +43,8 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
IdentityUserManager userManager,
IdentityRoleManager roleManager,
IPermissionGrantRepository permissionGrantRepository,
IPermissionDefinitionRecordRepository permissionDefinitionRecordRepository,
IUnitOfWorkManager unitOfWorkManager,
ICurrentTenant currentTenant,
ITenantRepository tenantRepository,
IConfiguration configuration,
@ -45,6 +52,8 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
) : base(guidGenerator, roleRepository, userRepository, lookupNormalizer, userManager, roleManager, currentTenant, identityOptions)
{
this._permissionGrantRepository = permissionGrantRepository;
this._permissionDefinitionRecordRepository = permissionDefinitionRecordRepository;
this._unitOfWorkManager = unitOfWorkManager;
this._tenantRepository = tenantRepository;
this._branchRepository = branchRepository;
this._branchUsersRepository = branchUsersRepository;
@ -159,6 +168,13 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
adminUser.SetAvatar(GetAvatar(tenantId?.ToString(), adminUser.Id.ToString()));
adminUser.SetUserType(PlatformConsts.AbpIdentity.User.InternalUserTypeDefaultValue);
adminUser.SetGender(PlatformConsts.AbpIdentity.User.AdminGenderDefaultValue);
adminUser.SetMilitaryStatus(PlatformConsts.AbpIdentity.User.AdminMilitaryStatusDefaultValue);
adminUser.SetEmploymentType(PlatformConsts.AbpIdentity.User.AdminEmploymentTypeDefaultValue);
adminUser.SetContractType(PlatformConsts.AbpIdentity.User.AdminContractTypeDefaultValue);
adminUser.SetSalaryType(PlatformConsts.AbpIdentity.User.AdminSalaryTypeDefaultValue);
// İzin hakkı ve kıdem hesabı bu iki tarihten yürür; yönetici kurulum günü işe başlamış sayılır.
adminUser.SetHireDate(DateTime.Today);
adminUser.SetSeniorityStartDate(DateTime.Today);
(await UserManager.CreateAsync(adminUser, adminPassword, validatePassword: false)).CheckErrors();
result.CreatedAdminUser = true;
@ -180,7 +196,11 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
/*admin rolüne ait yetkiler otomatik atanıyor*/
var permissions = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
if (permissions.Count == 0)
if (permissions.Count == 0 && tenantId != null)
{
await GrantTenantPermissionsAsync(tenantId.Value);
}
else if (permissions.Count == 0)
{
await _permissionGrantRepository.InsertManyAsync(
[
@ -229,6 +249,43 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
}
}
/// <summary>
/// Yeni kiracının yöneticisi kiracı tarafına açık bütün yetkilerle başlar; yoksa menüdeki hiçbir
/// ekranı göremez ve rol ekranından yetki verecek kimse kalmaz. Yetkilerin çoğu seed dosyasından
/// gelen dinamik tanımlardır ve DbMigrator'ın yetki tanımı yöneticisinde yoktur
/// (<c>IsDynamicPermissionStoreEnabled</c> yalnızca API'dedir); bu yüzden ABP'nin
/// <c>PermissionDataSeedContributor</c>'ı onları atayamaz, liste host'taki tanım tablosundan okunur.
/// Yalnızca yöneticinin hiç yetkisi yokken çalışır: sonradan deploy edilen ekranların yetkileri
/// migrate ile kendiliğinden atanmaz, rol ekranından verilir.
/// </summary>
private async Task GrantTenantPermissionsAsync(Guid tenantId)
{
string[] permissionNames;
// Tanım tablosu host'tadır. Okuma, kiracının atamalarıyla aynı unit of work'te yapıldığında
// atamalar tenant veritabanı yerine host'a yazıldı; okuma bu yüzden kendi unit of work'ündedir.
using (CurrentTenant.Change(null))
using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
var records = await _permissionDefinitionRecordRepository.GetListAsync();
permissionNames = [.. records
.Where(a => a.MultiTenancySide.HasFlag(MultiTenancySides.Tenant))
.Where(a => string.IsNullOrWhiteSpace(a.Providers)
|| a.Providers.Split(',', StringSplitOptions.TrimEntries).Contains(RolePermissionValueProvider.ProviderName))
.Select(a => a.Name)];
await uow.CompleteAsync();
}
await _permissionGrantRepository.InsertManyAsync(
permissionNames.Select(name => new PermissionGrant(
GuidGenerator.Create(),
name,
RolePermissionValueProvider.ProviderName,
PlatformConsts.AbpIdentity.User.AdminRoleName,
tenantId)),
autoSave: true);
}
private string GetAvatar(string? tenantId, string? userId)
{
var tenantPart = !string.IsNullOrEmpty(tenantId)

View file

@ -17,23 +17,19 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// SqlTableDesigner ve SqlQueryManager üzerinden deploy edilen nesneleri
/// <c>{host|tenants/{tenantId}}/{sql|postgres}</c> klasöründeki .sql dosyalarından okuyarak
/// veritabanına uygular.
/// <c>{sql|postgres}</c> klasöründeki .sql dosyalarından okuyarak veritabanına uygular; host'ta ve
/// kendi veritabanı olan her tenant'ta aynı dosyalar çalışır (<see cref="SeedFileScope"/>).
/// Her dosya, tek bir nesneye (ve ilgili ALTER/INDEX script'lerine) ait varlık kontrolü içeren
/// batch'lerden oluşmalıdır. Veritabanı silinip yeniden oluşturulduğunda tüm scriptler yeniden çalışır.
/// <para>
/// Klasör düzeni — her .sql dosyası amacını belirten bir klasörün altındadır, kökte dosya durmaz:
/// <code>
/// host/ → host (varsayılan) veritabanı kapsamı
/// {sql|postgres}/
/// object/ → nesne oluşturulur/güncellenir. SqlTableDesigner ve
/// SqlQueryManager, host bağlamındayken buraya yazar.
/// SqlQueryManager buraya yazar.
/// execute/ → nesne oluşturulur, ayrıca tüm migration'lar bittikten sonra
/// <see cref="FinalSeeder"/> tarafından çalıştırılır.
/// tenants/{tenantId}/ → ilgili tenant veritabanı kapsamı
/// {sql|postgres}/
/// object/
/// execute/
/// host/{object,execute}/ → aynı kurallar, yalnızca host veritabanında.
/// </code>
/// Geriye dönük uyumluluk: klasör köklerinde kalmış eski .sql dosyaları da işlenir, ancak
/// uyarı ile loglanır — taşınmaları beklenir.
@ -68,21 +64,24 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
/// </summary>
public async Task SeedAsync(DataSeedContext context)
{
if (!SeedFileScope.IsEnabled(context))
return;
await SeedSqlObjectsAsync(context);
await _crudDataSeeder.SeedAsync(context);
}
private async Task SeedSqlObjectsAsync(DataSeedContext context)
{
var dataDirectoryName = $"{SeedPathResolver.GetScopeFolderName(context.TenantId)}/{GetProviderDirectoryName()}";
var sqlDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, GetProviderDirectoryName());
var dataDirectoryName = GetProviderDirectoryName();
var sqlDataPath = SeedPathResolver.GetPath(_configuration, dataDirectoryName);
if (!Directory.Exists(sqlDataPath))
{
_logger.LogInformation("Seeds/{DirectoryName} directory not found, skipping SqlDataSeeder.", dataDirectoryName);
return;
}
var sqlFiles = GetSqlFiles(sqlDataPath);
var sqlFiles = GetSqlFiles(sqlDataPath, includeHostOnly: context.TenantId == null);
if (sqlFiles.Length == 0)
{
@ -151,20 +150,32 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
return SeedPathResolver.GetProviderFolderName(DefaultDatabaseProvider == DatabaseProvider.PostgreSql);
}
private string[] GetSqlFiles(string providerDirectoryPath)
/// <summary>
/// Sira: ortak nesneler, host'a ozgu nesneler, ardindan execute scriptleri. Host'a ozgu script
/// ortak tablolara dayanabilir; execute scriptleri de nesnelerin kurulmus olmasini bekler.
/// </summary>
private string[] GetSqlFiles(string providerDirectoryPath, bool includeHostOnly)
{
var sqlFiles = new List<string>();
var hostOnlyPath = Path.Combine(providerDirectoryPath, SeedPathResolver.HostOnlyFolder);
// Klasor kokunde dosya beklenmez; eskiden kalanlar uyari ile islenir.
sqlFiles.AddRange(GetLegacyRootSqlFiles(providerDirectoryPath));
sqlFiles.AddRange(GetOrderedSqlFiles(Path.Combine(providerDirectoryPath, ObjectDirectoryName)));
// {sql|postgres}/host: tenant veritabaninda karsiligi olmayan nesnelere (HangFire) dayanir.
if (includeHostOnly)
sqlFiles.AddRange(GetOrderedSqlFiles(Path.Combine(hostOnlyPath, ObjectDirectoryName)));
// {sql|postgres}/execute: nesneler burada da oluşturulur, ayrıca tüm migration
// bittikten sonra FinalSeeder dosya adı üzerinden çalıştırır.
sqlFiles.AddRange(GetOrderedSqlFiles(
Path.Combine(providerDirectoryPath, FinalSeeder.DirectoryName)));
if (includeHostOnly)
sqlFiles.AddRange(GetOrderedSqlFiles(Path.Combine(hostOnlyPath, FinalSeeder.DirectoryName)));
return sqlFiles.ToArray();
}

View file

@ -18,8 +18,8 @@ using Volo.Abp.PermissionManagement;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Wizard ile uretilen ekranlari Seeds/{host|tenants/{tenantId}}/wizard/*.json dosyalarindan kurar
/// (kapsam, seed edilen veritabanini izler).
/// Wizard ile uretilen ekranlari Seeds/wizard/*.json dosyalarindan kurar; host'ta ve kendi
/// veritabani olan her tenant'ta ayni dosyalar kullanilir (<see cref="SeedFileScope"/>).
/// <para>
/// Deploy veritabanina yazmaz; bu seeder wizard'in kurulumunun tek yeridir. Dosya yalnizca ekrana
/// ozgu kayitlari tasir: veri kaynagi, ListForm, alanlar ve is akisi. Yetkiler hicbir role atanmaz;
@ -53,18 +53,20 @@ public class WizardDataSeeder(
public async Task SeedAsync(DataSeedContext context)
{
var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId);
var wizardDataPath = SeedPathResolver.GetScopePath(configuration, context.TenantId, SeedPathResolver.WizardFolder);
if (!SeedFileScope.IsEnabled(context))
return;
var wizardDataPath = SeedPathResolver.GetPath(configuration, SeedPathResolver.WizardFolder);
if (!Directory.Exists(wizardDataPath))
{
logger.LogInformation("Seeds/{Scope}/wizard directory not found, skipping.", scopeFolderName);
logger.LogInformation("Seeds/wizard directory not found, skipping.");
return;
}
var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json");
if (jsonFiles.Length == 0)
{
logger.LogInformation("No JSON files found in Seeds/{Scope}/wizard directory, skipping.", scopeFolderName);
logger.LogInformation("No JSON files found in Seeds/wizard directory, skipping.");
return;
}
@ -126,7 +128,7 @@ public class WizardDataSeeder(
await InstallAsync(seedFile);
if (seedFile.Pending != null)
if (seedFile.Pending != null && SeedFileScope.CompletesPending(context))
{
seedFile.Pending = null;
await File.WriteAllTextAsync(filePath, seedFile.ToSeedJson());

View file

@ -1,6 +1,6 @@
namespace Sozsoft.Platform.Enums;
/// <summary>Import edilen zip girdisinin hedef kapsam klasorundeki dosyayla karsilastirma sonucu.</summary>
/// <summary>Import edilen zip girdisinin hedef seed klasorundeki dosyayla karsilastirma sonucu.</summary>
public enum WizardImportEntryStatusEnum
{
/// <summary>Hedefte ayni yolda dosya yok; dogrudan kopyalanir.</summary>

View file

@ -51,7 +51,7 @@ public interface IListFormArtifact
string DeleteBeforeCommand { get; set; }
string DeleteAfterCommand { get; set; }
/// <summary>Kapsam klasorune gore goreli seed dosyasi yolu; dolu ise liste verisi bu dosyayla senkron tutulur.</summary>
/// <summary>Seed klasorune gore goreli seed dosyasi yolu; dolu ise liste verisi bu dosyayla senkron tutulur.</summary>
string SeedFilePath { get; set; }
/// <summary>Ekrandan eklenen kayit seed dosyasina da eklenir.</summary>

View file

@ -289,7 +289,12 @@ public static class PlatformConsts
public static readonly DateTime AdminBirthDateDefaultValue = new(1981, 8, 27);
public const string AdminBirthPlaceDefaultValue = "Kadıköy, İstanbul";
public const string AdminMaritalStatusDefaultValue = "Evli";
public const string AdminGenderDefaultValue = "Male";
public const string AdminGenderDefaultValue = GenderMale;
// Değerler Users ekranındaki statik lookup anahtarlarıdır (ListFormSeeder_Administration).
public const string AdminMilitaryStatusDefaultValue = "Done";
public const string AdminEmploymentTypeDefaultValue = "FullTime";
public const string AdminContractTypeDefaultValue = "Indefinite";
public const string AdminSalaryTypeDefaultValue = "Monthly";
// Cinsiyet dilden bagimsiz anahtarla saklanir; ekranda App.StaticLookup.* ile cevrilir.
public const string GenderMale = "Male";

View file

@ -24,7 +24,7 @@ public static class WizardConsts
/// <summary>Liste verisinin aynalandigi seed klasoru; SeedPathResolver ile ayni deger.</summary>
public const string SeedDataFolder = "data";
/// <summary>Ekranin varsayilan veri seed dosyasi, kapsam klasorune gore goreli.</summary>
/// <summary>Ekranin varsayilan veri seed dosyasi, seed klasorune gore goreli.</summary>
public static string SeedDataFilePath(string listFormCode) => $"{SeedDataFolder}/{listFormCode}.json";
public static string MenuUrl(string code) => $"/admin/list/{code}";

View file

@ -18,15 +18,15 @@ namespace Sozsoft.Platform.Data.Seeds;
public record CrudEndpointSeedApplyResult(string Path, int Inserted, int Updated, int Deleted, int Skipped, string Error = null);
/// <summary>
/// <c>{host|tenants/{tenantId}}/crud/{EntityName}.json</c> dosyalarini veritabanina uygular.
/// <c>crud/{EntityName}.json</c> dosyalarini verilen tenant baglamindaki veritabanina uygular.
/// </summary>
public interface ICrudEndpointSeedApplier
{
/// <summary>Kapsamdaki butun CRUD seed dosyalarini uygular (migrate/seed akisi).</summary>
/// <summary>Butun CRUD seed dosyalarini uygular (migrate/seed akisi).</summary>
Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyScopeAsync(Guid? tenantId);
/// <summary>
/// Kapsam klasorune goreli belirli dosyalari uygular; import gibi dosya bazli akislar butun
/// Seed klasorune goreli belirli dosyalari uygular; import gibi dosya bazli akislar butun
/// klasoru taramak yerine yalnizca yazilan dosyalari uygular.
/// </summary>
Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyFilesAsync(Guid? tenantId, IReadOnlyCollection<string> relativePaths);
@ -58,23 +58,22 @@ public class CrudEndpointSeedApplier(
public async Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyScopeAsync(Guid? tenantId)
{
var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId);
var crudPath = SeedPathResolver.GetScopePath(configuration, tenantId, SeedPathResolver.CrudFolder);
var crudPath = SeedPathResolver.GetPath(configuration, SeedPathResolver.CrudFolder);
if (!Directory.Exists(crudPath))
{
logger.LogInformation("Seeds/{Scope}/crud directory not found, skipping CrudEndpointSeedApplier.", scopeFolderName);
logger.LogInformation("Seeds/crud directory not found, skipping CrudEndpointSeedApplier.");
return [];
}
var jsonFiles = Directory.GetFiles(crudPath, "*.json").OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ToArray();
if (jsonFiles.Length == 0)
{
logger.LogInformation("No JSON files found in Seeds/{Scope}/crud directory, skipping CrudEndpointSeedApplier.", scopeFolderName);
logger.LogInformation("No JSON files found in Seeds/crud directory, skipping CrudEndpointSeedApplier.");
return [];
}
logger.LogInformation("CrudEndpointSeedApplier started for {Scope}. {Count} file(s) to be processed.", scopeFolderName, jsonFiles.Length);
logger.LogInformation("CrudEndpointSeedApplier started. {Count} file(s) to be processed.", jsonFiles.Length);
return await ApplyAsync(tenantId, jsonFiles);
}
@ -84,7 +83,7 @@ public class CrudEndpointSeedApplier(
if (relativePaths == null || relativePaths.Count == 0)
return [];
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId));
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(configuration));
var results = new List<CrudEndpointSeedApplyResult>();
var filePaths = new List<string>();

View file

@ -5,7 +5,7 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Bir entity icin uretilen CRUD endpoint'lerinin seed dosyasi.
/// SqlQueryManager endpoint uretince <c>{host|tenants/{tenantId}}/crud/{EntityName}.json</c>
/// SqlQueryManager endpoint uretince <c>crud/{EntityName}.json</c>
/// olarak yazilir; veritabani silinip yeniden olusturuldugunda
/// <c>CrudDataSeeder</c> ayni dosyalari okuyarak endpoint'leri geri yukler.
/// </summary>

View file

@ -3,7 +3,7 @@
/// <summary>
/// Component'in kullandigi tek bir API. <see cref="EntityName"/> dolu ise endpoint
/// CRUD Endpoint Manager tarafindan uretilmistir ve tanimi <see cref="SeedFile"/>
/// (kapsam klasorune gore <c>crud/{EntityName}.json</c>) dosyasindadir.
/// (seed klasorune gore <c>crud/{EntityName}.json</c>) dosyasindadir.
/// </summary>
public class CustomComponentSeedDataSourceDto
{
@ -23,6 +23,6 @@ public class CustomComponentSeedDataSourceDto
/// <summary>CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete).</summary>
public string? OperationType { get; set; }
/// <summary>Endpoint tanimini tasiyan seed dosyasinin kapsam klasorune gore yolu.</summary>
/// <summary>Endpoint tanimini tasiyan seed dosyasinin seed klasorune gore yolu.</summary>
public string? SeedFile { get; set; }
}

View file

@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Sozsoft.Platform.Data;
@ -9,8 +8,8 @@ namespace Sozsoft.Platform.Data;
/// </summary>
public interface IFinalSeeder
{
/// <param name="tenantId">
/// null ise host/, aksi halde tenants/{tenantId}/ kapsamındaki {sql|postgres}/execute klasörü işlenir.
/// </param>
Task ExecuteAsync(Guid? tenantId = null);
/// <summary>
/// {sql|postgres}/execute klasörünü geçerli bağlamın (host ya da tenant) veritabanında işler.
/// </summary>
Task ExecuteAsync();
}

View file

@ -23,7 +23,7 @@ using Volo.Abp.Uow;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Kapsam klasorundeki <c>data/*.json</c> dosyalarini okuyup satirlari ilgili ListForm'un
/// Seed klasorundeki <c>data/*.json</c> dosyalarini okuyup satirlari ilgili ListForm'un
/// tablosuna yazar.
/// </summary>
public interface IListFormSeedDataApplier
@ -32,7 +32,7 @@ public interface IListFormSeedDataApplier
Task ApplyAsync(Guid? tenantId);
/// <summary>
/// Kapsam klasorune goreli belirli veri seed dosyalarini uygular; import gibi dosya bazli
/// Seed klasorune goreli belirli veri seed dosyalarini uygular; import gibi dosya bazli
/// akislar butun klasoru taramak yerine yalnizca yazilan dosyalari uygular. Dosyalar
/// <c>ApplyAsync</c> ile ayni kurala gore siralanir.
/// </summary>
@ -105,7 +105,7 @@ public class ListFormSeedDataApplier(
if (relativePaths == null || relativePaths.Count == 0)
return [];
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId));
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(configuration));
var results = new List<ListFormSeedDataApplyResult>();
var filePaths = new List<string>();
@ -143,12 +143,11 @@ public class ListFormSeedDataApplier(
public async Task ApplyAsync(Guid? tenantId)
{
var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId);
var dataPath = SeedPathResolver.GetScopePath(configuration, tenantId, SeedPathResolver.DataFolder);
var dataPath = SeedPathResolver.GetPath(configuration, SeedPathResolver.DataFolder);
if (!Directory.Exists(dataPath))
{
logger.LogInformation("Seeds/{Scope}/data directory not found, skipping ListFormSeedDataApplier.", scopeFolderName);
logger.LogInformation("Seeds/data directory not found, skipping ListFormSeedDataApplier.");
return;
}
@ -159,13 +158,13 @@ public class ListFormSeedDataApplier(
var jsonFiles = Directory.GetFiles(dataPath, "*.json", SearchOption.AllDirectories);
if (jsonFiles.Length == 0)
{
logger.LogInformation("No JSON files found in Seeds/{Scope}/data directory, skipping ListFormSeedDataApplier.", scopeFolderName);
logger.LogInformation("No JSON files found in Seeds/data directory, skipping ListFormSeedDataApplier.");
return;
}
logger.LogInformation("ListFormSeedDataApplier started for {Scope}. {Count} file(s) to be processed.", scopeFolderName, jsonFiles.Length);
logger.LogInformation("ListFormSeedDataApplier started. {Count} file(s) to be processed.", jsonFiles.Length);
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId));
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(configuration));
using (currentTenant.Change(tenantId))
{
@ -179,7 +178,7 @@ public class ListFormSeedDataApplier(
}
}
logger.LogInformation("ListFormSeedDataApplier completed for {Scope}.", scopeFolderName);
logger.LogInformation("ListFormSeedDataApplier completed.");
}
/// <summary>
@ -249,7 +248,7 @@ public class ListFormSeedDataApplier(
/// <summary>
/// Tek seferde uygulanan bir ekranin dosyalari: tek dosya ya da satir klasorundeki butun satir
/// dosyalari. <see cref="Path"/> kapsam klasorune goreli hedeftir (dosya ya da klasor) ve
/// dosyalari. <see cref="Path"/> seed klasorune goreli hedeftir (dosya ya da klasor) ve
/// sonuclarda, loglarda bu gorunur.
/// </summary>
private sealed record SeedDataTarget(string Path, string[] FilePaths);

View file

@ -7,7 +7,7 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Bir ListForm ekranindan (Grid, Tree, TodoBoard vb.) girilen verinin seed dosyasi:
/// <c>{host|tenants/{tenantId}}/data/{ListFormCode}.json</c>.
/// <c>data/{ListFormCode}.json</c>.
/// <para>
/// Ekrandaki her insert/update/delete sonrasi ilgili satir dosyaya islenir; veritabani
/// sifirlandiginda <c>ListFormSeedDataApplier</c> ayni dosyayi okuyup satirlari geri yazar.

View file

@ -15,8 +15,9 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Bildirim tipleri ve varsayilan kurallari kiraci kapsamindadir: her kiraci kendi turunu ve
/// kuralini duzenler. Host kapsamindaki seed dosyalari varsayilan kumeyi tanimlar, bu contributor
/// da yeni kiraciya o kumenin bir kopyasini uretir.
/// kuralini duzenler. Seed dosyalari varsayilan kumeyi tanimlar; host ve kendi veritabani olan
/// kiraci onu dosyadan alir, bu contributor da host veritabanini paylasan yeni kiraciya o kumenin
/// bir kopyasini uretir.
/// <para>
/// Kopya satirlar yeni kimliklerle yazilir; dosyadaki kimlik host kaydinindir ve tum kiracilarda
/// tekrar edilemez. Eslesme ad uzerinden yapilir, bu yuzden ayni kiraci icin ikinci kez
@ -34,9 +35,10 @@ public class NotificationTenantDefaultsSeedContributor(
public async Task SeedAsync(DataSeedContext context)
{
// Host kapsami kendi dosyalarini ListFormSeedDataApplier ile uygular; burada yalnizca
// kiraci kopyasi uretilir.
if (context.TenantId == null)
// Seed dosyalarinin uygulandigi veritabani (host ve kendi veritabani olan kiraci) bu satirlari
// ListFormSeedDataApplier ile alir; burada yalnizca host veritabanini paylasan kiracinin kopyasi
// uretilir. Iki yol birden calissaydi ayri veritabanli kiracida tipler iki kez yazilirdi.
if (SeedFileScope.IsEnabled(context))
{
return;
}
@ -120,14 +122,13 @@ public class NotificationTenantDefaultsSeedContributor(
}
/// <summary>
/// Varsayilan kumeyi host kapsamindaki veri seed dosyasindan okur; ikinci bir varsayilan
/// Varsayilan kumeyi veri seed dosyasindan okur; ikinci bir varsayilan
/// listesi kodda tutulmaz. Dosya yoksa kiraci bos baslar, bu bir hata degildir.
/// </summary>
private List<JsonElement> ReadRows(string listFormCode)
{
var path = SeedPathResolver.GetScopePath(
var path = SeedPathResolver.GetPath(
configuration,
null,
SeedPathResolver.GetDataFilePath(listFormCode).Split('/'));
if (!File.Exists(path))

View file

@ -1,15 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Sozsoft.Languages.Caching;
using Sozsoft.Platform.Data.Seeds;
using StackExchange.Redis;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
@ -29,6 +26,7 @@ public class PlatformDbMigrationService : ITransientDependency
private readonly IConfiguration _configuration;
private readonly IEnumerable<IFinalSeeder> _finalSeeders;
private readonly IListFormSeedDataApplier _listFormSeedDataApplier;
private readonly DistributedCacheCleaner _cacheCleaner;
public PlatformDbMigrationService(
IDataSeeder dataSeeder,
@ -37,7 +35,8 @@ public class PlatformDbMigrationService : ITransientDependency
ICurrentTenant currentTenant,
IConfiguration configuration,
IEnumerable<IFinalSeeder> finalSeeders,
IListFormSeedDataApplier listFormSeedDataApplier)
IListFormSeedDataApplier listFormSeedDataApplier,
DistributedCacheCleaner cacheCleaner)
{
_dataSeeder = dataSeeder;
_dbSchemaMigrators = dbSchemaMigrators;
@ -46,6 +45,7 @@ public class PlatformDbMigrationService : ITransientDependency
_configuration = configuration;
_finalSeeders = finalSeeders;
_listFormSeedDataApplier = listFormSeedDataApplier;
_cacheCleaner = cacheCleaner;
Logger = NullLogger<PlatformDbMigrationService>.Instance;
}
@ -55,27 +55,32 @@ public class PlatformDbMigrationService : ITransientDependency
var seed = _configuration.GetValue("Seed", false);
Logger.LogInformation($"Migrating database. Seed: {seed}. Env: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}");
ClearCache();
await ClearCacheAsync();
var initialMigrationAdded = AddInitialMigrationIfNotExist();
if (initialMigrationAdded)
var tenantSeedId = _configuration.GetValue<Guid?>(TenantSeedOptions.TenantIdKey);
if (tenantSeedId.HasValue)
{
await SeedSingleTenantAsync(tenantSeedId.Value);
return;
}
Logger.LogInformation("Started database migrations...");
await MigrateDatabaseSchemaAsync();
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
// Bekleyen wizard kurulumu dosyaları uygulayan son veritabanında kapanır; host'ta kapansaydı
// ekranın eski sürümü kurulu olan ayrı veritabanlı tenant onu "kurulu" sayıp güncellemezdi.
var lastSeedFileTenant = tenants.LastOrDefault(SeedFileScope.AppliesTo);
if (seed)
{
await SeedDataAsync();
await SeedDataAsync(completesPending: lastSeedFileTenant == null);
}
Logger.LogInformation($"Successfully completed host database migrations.");
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var migratedDatabaseSchemas = new HashSet<string>();
foreach (var tenant in tenants)
{
@ -96,7 +101,7 @@ public class PlatformDbMigrationService : ITransientDependency
}
if (seed)
{
await SeedDataAsync(tenant);
await SeedDataAsync(tenant, completesPending: tenant == lastSeedFileTenant);
}
}
@ -105,29 +110,68 @@ public class PlatformDbMigrationService : ITransientDependency
Logger.LogInformation("Successfully completed all database migrations.");
await ExecuteFinalSeedersAsync(tenants);
await ExecuteFinalSeedersAsync(tenants, includeHost: true);
// Seed; menu, yetki ve dil metni ekler. Calisan uygulama bu arada eski halleri onbellege
// almis olabilir (ornegin wizard'in yeni menusu); migrate sonrasinda da temizlenir.
if (seed)
ClearCache();
await ClearCacheAsync();
Logger.LogInformation("You can safely end this process...");
}
private void ClearCache()
/// <summary>
/// Tenants ekranının "Seed" sekmesi: yalnızca verilen tenant'ın şemasını ve seed'ini
/// tam migrate'le aynı adımlarla çalıştırır. Yönetici bilgisi bu tenant için ekrandan gelir;
/// tam migrate'teki varsayılan yönetici kullanılmaz.
/// </summary>
private async Task SeedSingleTenantAsync(Guid tenantId)
{
using var redis = ConnectionMultiplexer.Connect(_configuration["Redis:Configuration"]!);
redis.GetDatabase().Execute("FLUSHALL");
var tenant = await _tenantRepository.FindAsync(tenantId, includeDetails: true)
?? throw new InvalidOperationException($"Tenant not found: {tenantId}");
var adminEmail = _configuration[TenantSeedOptions.AdminEmailKey];
var adminPassword = _configuration[TenantSeedOptions.AdminPasswordKey];
if (string.IsNullOrWhiteSpace(adminEmail) || string.IsNullOrWhiteSpace(adminPassword))
{
throw new InvalidOperationException("Admin email and password are required for tenant seed.");
}
Logger.LogInformation("Started {TenantName} tenant migration and seed...", tenant.Name);
using (_currentTenant.Change(tenant.Id))
{
if (tenant.ConnectionStrings.Any())
{
await MigrateDatabaseSchemaAsync(tenant);
}
// Host ve diğer tenant'lar bu çalışmada kurulmadığı için bekleyen wizard kurulumu açık kalır.
await SeedDataAsync(tenant, completesPending: false, adminEmail, adminPassword);
}
Logger.LogInformation("Successfully completed {TenantName} tenant database migrations.", tenant.Name);
await ExecuteFinalSeedersAsync([tenant], includeHost: false);
await ClearCacheAsync();
Logger.LogInformation("You can safely end this process...");
}
private async Task ClearCacheAsync()
{
var removed = await _cacheCleaner.ClearAsync();
Logger.LogInformation("Distributed cache cleared. {Count} key(s) removed.", removed);
}
/// <summary>
/// Tüm host ve tenant migration/seed adımları bittikten sonra,
/// {host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki script'lerin dosya adından
/// türeyen procedure'lerini ilgili bağlamda çalıştırır. İlgili tenant klasörü yoksa
/// o tenant adımı sessizce atlanır.
/// {sql|postgres}/execute klasöründeki script'lerin dosya adından türeyen procedure'lerini
/// host'ta ve kendi veritabanı olan her tenant'ta çalıştırır. Host veritabanını paylaşan tenant
/// atlanır; procedure o veritabanında host adımında zaten çalışmıştır.
/// </summary>
private async Task ExecuteFinalSeedersAsync(List<Tenant> tenants)
private async Task ExecuteFinalSeedersAsync(List<Tenant> tenants, bool includeHost)
{
if (!_finalSeeders.Any())
{
@ -136,9 +180,12 @@ public class PlatformDbMigrationService : ITransientDependency
}
Logger.LogInformation(
"Executing SQL scripts for host and {TenantCount} tenant(s)...",
"Executing SQL scripts for {Host}{TenantCount} tenant(s)...",
includeHost ? "host and " : string.Empty,
tenants.Count);
if (includeHost)
{
using (_currentTenant.Change(null))
{
foreach (var executor in _finalSeeders)
@ -148,8 +195,9 @@ public class PlatformDbMigrationService : ITransientDependency
Logger.LogInformation("Completed SQL scripts for host.");
}
}
}
foreach (var tenant in tenants)
foreach (var tenant in tenants.Where(SeedFileScope.AppliesTo))
{
using (_currentTenant.Change(tenant.Id))
{
@ -158,7 +206,7 @@ public class PlatformDbMigrationService : ITransientDependency
Logger.LogInformation(
"Running SQL scripts for {TenantName} tenant...",
tenant.Name);
await executor.ExecuteAsync(tenant.Id);
await executor.ExecuteAsync();
Logger.LogInformation(
"Completed SQL scripts for {TenantName} tenant.",
tenant.Name);
@ -180,129 +228,34 @@ public class PlatformDbMigrationService : ITransientDependency
}
}
private async Task SeedDataAsync(Tenant? tenant = null)
private async Task SeedDataAsync(
Tenant? tenant = null,
bool completesPending = true,
string adminEmail = PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue,
string adminPassword = PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
var applySeedFiles = SeedFileScope.AppliesTo(tenant);
if (!applySeedFiles)
{
Logger.LogInformation("{TenantName} tenant shares the host database; seed files are already applied there.", tenant!.Name);
}
await _dataSeeder.SeedAsync(new DataSeedContext(tenant?.Id)
.WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue)
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue)
.WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue)
.WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, adminEmail)
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, adminPassword)
.WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, adminEmail)
.WithProperty(SeedFileScope.PropertyName, applySeedFiles)
.WithProperty(SeedFileScope.CompletesPendingPropertyName, completesPending)
);
// Veri seed dosyalari ListForm tanimlarina ve tablolara dayandigi icin butun
// contributor'lar bittikten sonra uygulanir; ABP contributor sirasi garanti degildir.
if (applySeedFiles)
{
await _listFormSeedDataApplier.ApplyAsync(tenant?.Id);
}
private bool AddInitialMigrationIfNotExist()
{
try
{
if (!DbMigrationsProjectExists())
{
return false;
}
}
catch (Exception)
{
return false;
}
try
{
if (!MigrationsFolderExists())
{
AddInitialMigration();
return true;
}
else
{
return false;
}
}
catch (Exception e)
{
Logger.LogWarning("Couldn't determinate if any migrations exist : " + e.Message);
return false;
}
}
private bool DbMigrationsProjectExists()
{
var dbMigrationsProjectFolder = GetEntityFrameworkCoreProjectFolderPath();
return dbMigrationsProjectFolder != null;
}
private bool MigrationsFolderExists()
{
var dbMigrationsProjectFolder = GetEntityFrameworkCoreProjectFolderPath();
return dbMigrationsProjectFolder != null && Directory.Exists(Path.Combine(dbMigrationsProjectFolder, "Migrations"));
}
private void AddInitialMigration()
{
Logger.LogInformation("Creating initial migration...");
string argumentPrefix;
string fileName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
argumentPrefix = "-c";
fileName = "/bin/bash";
}
else
{
argumentPrefix = "/C";
fileName = "cmd.exe";
}
var procStartInfo = new ProcessStartInfo(fileName,
$"{argumentPrefix} \"abp create-migration-and-run-migrator \"{GetEntityFrameworkCoreProjectFolderPath()}\"\""
);
try
{
Process.Start(procStartInfo);
}
catch (Exception)
{
throw new Exception("Couldn't run ABP CLI...");
}
}
private string? GetEntityFrameworkCoreProjectFolderPath()
{
var slnDirectoryPath = GetSolutionDirectoryPath();
if (slnDirectoryPath == null)
{
throw new Exception("Solution folder not found!");
}
var srcDirectoryPath = Path.Combine(slnDirectoryPath, "src");
return Directory.GetDirectories(srcDirectoryPath)
.FirstOrDefault(d => d.EndsWith(".EntityFrameworkCore"));
}
private string? GetSolutionDirectoryPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (currentDirectory != null && Directory.GetParent(currentDirectory.FullName) != null)
{
currentDirectory = Directory.GetParent(currentDirectory.FullName);
if (currentDirectory != null && Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
{
return currentDirectory.FullName;
}
}
return null;
}
}

View file

@ -0,0 +1,40 @@
using System.Linq;
using Volo.Abp.Data;
using Volo.Abp.TenantManagement;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Seed klasorundeki dosyalarin (sql, crud, wizard, data, execute) bir seed calismasinda uygulanip
/// uygulanmayacagi. Host her zaman uygular; tenant yalnizca kendi veritabani varsa uygular. Baglanti
/// cumlesi olmayan tenant host veritabanini paylasir ve dosyalar o veritabanina host seed'inde zaten
/// yazilmistir; tekrar uygulamak toplu procedure'leri bosuna calistirir ve anahtarsiz satirlari
/// tenant kimligiyle kopyalar.
/// </summary>
public static class SeedFileScope
{
/// <summary><see cref="DataSeedContext"/> uzerinde kararin tasindigi ozellik.</summary>
public const string PropertyName = "Sozsoft.ApplySeedFiles";
/// <summary>Tenant icin karar: kendi veritabanini gosteren bir baglanti cumlesi var mi.</summary>
public static bool AppliesTo(Tenant? tenant) =>
tenant == null || tenant.ConnectionStrings.Any(a => !string.IsNullOrWhiteSpace(a.Value));
/// <summary>
/// Seed contributor'lari karari baglamdan okur. Ozellik yoksa (ornegin tenant olusturulurken
/// uygulamanin calistirdigi seed) yalnizca host uygular.
/// </summary>
public static bool IsEnabled(DataSeedContext context) =>
context.TenantId == null || context[PropertyName] is true;
/// <summary>
/// Bekleyen wizard kurulumunun (<c>Pending</c>) bu calismada dosyadan silinip silinmeyecegini
/// tasiyan ozellik. Ayni dosyalar birden cok veritabanina kuruldugu icin bayrak, kurulumu bekleyen
/// son veritabani da kurulduktan sonra kalkar.
/// </summary>
public const string CompletesPendingPropertyName = "Sozsoft.CompletesPendingWizards";
/// <summary>Ozellik yoksa tek veritabanli calisma sayilir ve bayrak silinir.</summary>
public static bool CompletesPending(DataSeedContext context) =>
context[CompletesPendingPropertyName] is not false;
}

View file

@ -18,22 +18,25 @@ namespace Sozsoft.Platform.Data.Seeds;
/// uzerinden de duzenlenebildigi icin yapilan degisiklik dogrudan host diskinde kalici olur.
/// </para>
/// <para>
/// Klasor duzeni CDN ile ayni kapsam mantigini izler — once kapsam, sonra icerik turu:
/// Tek klasor vardir; host ve kendi veritabani olan her tenant ayni dosyalarla kurulur. Tek bir
/// <c>PlatformDbContext</c> oldugu icin ayri veritabanli tenant ekran tanimlarini, menuleri ve yetkileri
/// kendi veritabanindan okur; tenant'a ozel bir seed kapsami yoktur. Paylasilan (host) veritabanindaki
/// tenant dosyalari yeniden uygulamaz (<see cref="SeedFileScope"/>). Dosyalar urunun varsayilanidir;
/// yalnizca host baglaminda yazilir, tenant'in verisi dosyaya yansimaz.
/// <code>
/// seeds/
/// host/ → host (varsayilan) veritabani kapsami
/// crud/
/// wizard/
/// data/
/// docs/
/// sql/ → SQL Server saglayicisi
/// object/ → nesne olusturulur/guncellenir
/// execute/ → nesne olusturulur, ayrica tum migration'lar bitince calistirilir
/// host/{object,execute}/ → ayni kurallar, yalnizca host veritabaninda
/// postgres/ → PostgreSQL saglayicisi
/// object/
/// execute/
/// tenants/
/// {tenantId}/ → her tenant kendi klasorunde
/// crud/ wizard/ data/ sql/{object,execute}/ postgres/{object,execute}/
/// host/{object,execute}/
/// </code>
/// </para>
/// <para>
@ -47,12 +50,6 @@ public static class SeedPathResolver
{
public const string ConfigurationKey = "App:SeedsPath";
/// <summary>Host veritabani kapsamindaki seed'lerin kok klasoru.</summary>
public const string HostFolder = "host";
/// <summary>Tenant kapsamindaki seed'lerin kok klasoru; altinda tenant Id'si ile klasorler bulunur.</summary>
public const string TenantsFolder = "tenants";
/// <summary>SQL Server saglayicisinin script klasoru.</summary>
public const string SqlFolder = "sql";
@ -65,6 +62,13 @@ public static class SeedPathResolver
/// <summary>Olusturulan, ayrica tum migration'lar bittikten sonra calistirilan scriptler.</summary>
public const string ExecuteFolder = "execute";
/// <summary>
/// Saglayici klasorunun altinda yalnizca host veritabaninda calisan scriptler
/// (<c>{sql|postgres}/host/{object,execute}</c>). Host'a ozgu nesnelere dayanan script buraya
/// yazilir; ornegin HangFire tablolari tenant migrate'inde de yalnizca host'a kurulur.
/// </summary>
public const string HostOnlyFolder = "host";
/// <summary>Wizard'in urettigi konfigurasyon dosyalari.</summary>
public const string WizardFolder = "wizard";
@ -75,7 +79,7 @@ public static class SeedPathResolver
public const string DataFolder = WizardConsts.SeedDataFolder;
/// <summary>
/// Kapsam klasorundeki dokumanlar. Modul dokumanlari <c>docs/modules/{Modul}.md</c> altindadir;
/// Seed klasorundeki dokumanlar. Modul dokumanlari <c>docs/modules/{Modul}.md</c> altindadir;
/// sunucu (wizard deploy, MCP) yalnizca seed kokune yazabildigi icin depo kokunde degil buradadir.
/// </summary>
public const string DocsFolder = "docs";
@ -84,7 +88,7 @@ public static class SeedPathResolver
public const string ModuleDocsFolder = "modules";
/// <summary>
/// Bir ListForm icin varsayilan veri seed dosyasinin kapsam klasorune gore goreli yolu. Bicim
/// Bir ListForm icin varsayilan veri seed dosyasinin seed klasorune gore goreli yolu. Bicim
/// Domain.Shared'dedir; wizard'in urettigi ekran (Application.Contracts) ayni yolu yazar.
/// </summary>
public static string GetDataFilePath(string listFormCode)
@ -93,7 +97,7 @@ public static class SeedPathResolver
}
/// <summary>
/// Bir ListForm icin satir basina dosya tutan veri seed klasorunun kapsam klasorune gore goreli
/// Bir ListForm icin satir basina dosya tutan veri seed klasorunun seed klasorune gore goreli
/// yolu. Cok satirli ya da satiri buyuk (bilesen kodu gibi) ekranlarda tek dosya her kayitta
/// bastan yazilir; klasor modunda yalnizca islem goren satirin dosyasina dokunulur.
/// </summary>
@ -168,38 +172,12 @@ public static class SeedPathResolver
return path;
}
/// <summary>
/// Kapsam klasorunun (host ya da ilgili tenant) altindaki yolu dondurur.
/// <paramref name="tenantId"/> null ise <c>host/</c>, aksi halde <c>tenants/{tenantId}/</c> kullanilir.
/// </summary>
public static string GetScopePath(IConfiguration? configuration, Guid? tenantId, params string[] subPaths)
{
return GetPath(configuration, BuildScopeSubPaths(tenantId, subPaths));
}
/// <summary>Kapsam klasorunun seed koküne gore goreli adi (<c>host</c> ya da <c>tenants/{tenantId}</c>).</summary>
public static string GetScopeFolderName(Guid? tenantId)
{
return tenantId == null
? HostFolder
: $"{TenantsFolder}/{tenantId.Value}";
}
/// <summary>Aktif veritabani saglayicisina karsilik gelen script klasorunun adi.</summary>
public static string GetProviderFolderName(bool isPostgreSql)
{
return isPostgreSql ? PostgresFolder : SqlFolder;
}
private static string[] BuildScopeSubPaths(Guid? tenantId, string[] subPaths)
{
var scopeSegments = tenantId == null
? new[] { HostFolder }
: new[] { TenantsFolder, tenantId.Value.ToString() };
return scopeSegments.Concat(subPaths ?? Array.Empty<string>()).ToArray();
}
/// <summary>Calisma dizininden yukari dogru cikarak depodaki configs/seeds klasorunu arar.</summary>
private static string? FindRepositorySeedsPath()
{

View file

@ -0,0 +1,15 @@
namespace Sozsoft.Platform.Data;
/// <summary>
/// Tek tenant seed'inin DbMigrator alt sürecine taşıdığı yapılandırma anahtarları.
/// Değerler komut satırı yerine ortam değişkeniyle geçer; yönetici parolası süreç listesinde görünmez.
/// </summary>
public static class TenantSeedOptions
{
public const string TenantIdKey = "TenantSeed:TenantId";
public const string AdminEmailKey = "TenantSeed:AdminEmail";
public const string AdminPasswordKey = "TenantSeed:AdminPassword";
/// <summary>Yapılandırma anahtarının ortam değişkeni karşılığı (<c>:</c> → <c>__</c>).</summary>
public static string ToEnvironmentVariable(string key) => key.Replace(":", "__");
}

View file

@ -98,7 +98,7 @@ public class ListForm : Entity<Guid>, IListFormArtifact
/// </summary>
public string DeleteServiceAddress { get; set; } = "list-form-data/delete";
/// <summary> Bu listenin verisinin aynalandigi seed dosyasinin kapsam klasorune gore goreli yolu
/// <summary> Bu listenin verisinin aynalandigi seed dosyasinin seed klasorune gore goreli yolu
/// (ornegin <c>data/App.DeveloperKit.IntranetWidgets.json</c>). Dolu ise ekrandan yapilan
/// seed sirasinda dosyadaki satirlar tabloya geri yuklenir. Ekranda yapilan islemlerin dosyaya
/// yansimasi asagidaki bayraklara baglidir.

View file

@ -4,11 +4,6 @@ namespace Sozsoft.Platform.Extensions;
public static class StringExtensions
{
public static string RemoveParentheses(this string str)
{
return str?.Replace("(", "").Replace(")", "");
}
public static string GetSegment(this string str, char segment, int index)
{
var start = str.NthIndexOf(segment, index);

View file

@ -1,8 +1,9 @@
using System;
using System;
using System.Threading.Tasks;
using Sozsoft.Platform.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Queries;
@ -15,10 +16,12 @@ public interface IDataSourceManager
public class DataSourceManager : DomainService, IDataSourceManager
{
private readonly IRepository<DataSource, Guid> repoDataSource;
private readonly ITenantStore tenantStore;
public DataSourceManager(IRepository<DataSource, Guid> repoDataSource)
public DataSourceManager(IRepository<DataSource, Guid> repoDataSource, ITenantStore tenantStore)
{
this.repoDataSource = repoDataSource;
this.tenantStore = tenantStore;
}
public async Task<string> GetConnectionStringAsync(bool isTenant, string dataSourceCode)
@ -30,10 +33,23 @@ public class DataSourceManager : DomainService, IDataSourceManager
public async Task<DataSource> GetDataSourceAsync(bool isTenant, string dataSourceCode)
{
var code = isTenant && CurrentTenant.Name is not null ? CurrentTenant.Name : dataSourceCode;
var tenantName = isTenant ? await GetTenantNameAsync() : null;
var code = tenantName ?? dataSourceCode;
var dataSource = await repoDataSource.FirstOrDefaultAsync(a => a.Code == code);
dataSource ??= await repoDataSource.FirstOrDefaultAsync(a => a.Code == dataSourceCode);
return dataSource;
}
}
/// <summary>
/// Kiracının veri kaynağı kiracı adıyla kayıtlıdır. İstek dışında (DbMigrator, seed applier,
/// arka plan işi) kiracı yalnızca kimliğiyle değiştirilir ve ad boş kalır; ad o zaman kiracı
/// kaydından okunur, yoksa kiracının satırları host veritabanına yazılırdı.
/// </summary>
private async Task<string> GetTenantNameAsync()
{
if (CurrentTenant.Name is not null || CurrentTenant.Id is not { } tenantId)
return CurrentTenant.Name;
return (await tenantStore.FindAsync(tenantId))?.Name;
}
}

View file

@ -212,28 +212,20 @@ public class QueryManager : PlatformDomainService, IQueryManager
}
else if (op == OperationEnum.Delete)
{
// todo: softdelete
string where = string.Empty;
if (parameters.Any())
{
where = string.Join(
" AND ",
parameters.Select(a => BuildWhereCondition(a.Key, a.Value, dataSourceType)).ToList());
}
else
// Silme her zaman anahtarla sınırlanır; diğer parametreler (TenantId gibi) yalnızca ek koşuldur.
// Önceden parametre varsa anahtar hiç eklenmiyordu: silme varsayılanı tanımsız bir ekranda sorgu
// "WHERE TenantId = @TenantId" olarak kalıp kiracının bütün satırlarını siliyordu.
if (!parameters.ContainsKey(listForm.KeyFieldName))
{
var val = QueryHelper.GetFormattedValue(listForm.KeyFieldDbSourceType, keys);
parameters.Add(
listForm.KeyFieldName,
val.GetType().IsArray ? val : new object[] { val });
where = dataSourceType switch
{
DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN @{listForm.KeyFieldName}",
DataSourceTypeEnum.Postgresql => $"\"{listForm.KeyFieldName}\" = ANY(@{listForm.KeyFieldName})",
_ => "1 = 0",
};
}
var where = string.Join(
" AND ",
parameters.Select(a => BuildWhereCondition(a.Key, a.Value, dataSourceType)));
sql = $"DELETE FROM \"{listForm.SelectCommand}\" WHERE {where}";
}
}

View file

@ -12,6 +12,7 @@ using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.ListForms;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.OrganizationUnits;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Settings;
@ -205,10 +206,15 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
for (int i = 0; i < summaries.Length; i++)
{
var summary = summaries[i].Split(' ');
var fieldName = summary[0];
var summmaryType = summary[1]; // asc, desc
fieldName = ConvertAliasToFieldName(fieldName, listFormFields);
if (fieldName == "undefined")
// Toplam türü SQL fonksiyon adı olarak yazılır; yalnızca bilinen beş tür kabul edilir.
if (summary.Length != 2 || !SummaryTypes.Contains(summary[1]))
{
continue;
}
var summmaryType = summary[1];
var fieldName = ConvertAliasToFieldName(summary[0], listFormFields);
if (fieldName == UnknownFieldName)
{
continue;
}
@ -294,9 +300,22 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
}
}
// Ad istemciden gelir (toplam, grup, filtre); ekranda olmayan bir ad SQL'e hiç yazılmaz.
if (filterName != KeyFieldName && !listFormFields.Any(a => a.FieldName == filterName))
{
return UnknownFieldName;
}
return string.Format(format, $"\"{TableName}\".\"{filterName}\"");
}
private const string UnknownFieldName = "undefined";
private static readonly HashSet<string> SummaryTypes = new(StringComparer.OrdinalIgnoreCase)
{
"sum", "avg", "min", "max", "count",
};
private string GetSelectString()
{
return $"SELECT {string.Join(',', SelectFields.Select(c => $"{c.SelectFieldName}"))} FROM {From} AS \"{TableName}\"";
@ -352,57 +371,18 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
var whereParts = new List<string>();
if (!string.IsNullOrEmpty(queryParams?.Filter))
{
var filters = queryParams.Filter.Replace('[', '(')
.Replace(']', ')')
.Replace("\"\"", "''") // (empty string) "" -> ''
.Replace("\"", "")
.Replace(",\"or\",", " or ")
.Replace(",\"and\",", " and ");
var filterItems = filters.Split(",").ToList();
if (filterItems
.Where(a => !a.Contains('!') && a != "and" && a != "or")
.ToList().Count % 3 == 0) //Eğer gelen veri 'and', 'or' veya '!' dışında kalan item adedi 3 ün katıysa
{
SelectQueryParameters = new Dictionary<string, object>();
filterParameterIndex = 0;
var forDeger = 3;
for (int i = 0; i < filterItems.Count; i += forDeger)
var clientFilter = BuildClientFilter(queryParams.Filter, listFormFields);
if (clientFilter != null)
{
if (filterItems[i].Contains('!'))
{
forDeger = 1;
whereParts.Add(filterItems[i].Replace("!", "NOT"));
// İstemci filtresi tek grup olarak paranteze alınır; aksi hâlde içindeki "or",
// ardından eklenen kiracı/şube/OU koşullarını devre dışı bırakırdı.
whereParts.Add($"({clientFilter})");
IsAppliedGridFilter = true;
}
else if (filterItems[i] == "and" || filterItems[i] == "or")
{
forDeger = 1;
whereParts.Add(filterItems[i]);
IsAppliedGridFilter = true;
}
else
{
forDeger = 3;
var fieldName = filterItems[i];
var op = filterItems[i + 1];
var fieldValue = filterItems[i + 2];
var filter = ParseFilter(fieldName, op, fieldValue, listFormFields, i);
if (filter != "")
{
whereParts.Add(filter);
IsAppliedGridFilter = true;
}
}
}
}
}
if (!listform.DefaultFilter.IsNullOrWhiteSpace())
@ -527,42 +507,188 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
return (value ?? string.Empty).Replace("'", "''");
}
/// <summary> FieldName operator Id seklinde olan filtreleri ayirir
private const int MaxFilterDepth = 32;
private static readonly HashSet<string> FilterOperators = new(StringComparer.OrdinalIgnoreCase)
{
"=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith",
};
private int filterParameterIndex;
/// <summary>
/// DevExtreme filtre dizisini (<c>["Alan","=",değer]</c>, <c>[koşul,"and",koşul]</c>, <c>["!",koşul]</c>)
/// ağaç olarak çözer. Alan adı ekranın alanlarından, operatör beyaz listeden gelir; değer her zaman
/// parametredir. Metin olarak işlenmez: önceki ayrıştırıcı virgül ve parantezle böldüğü için değerin bir
/// kısmı ham SQL olarak sorguya ekleniyordu.
/// </summary>
/// <param name="filter"></param>
/// <returns></returns>
private string ParseFilter(
string fieldName,
string op,
string fieldValue,
List<ListFormField> listFormFields,
int customKey)
private string BuildClientFilter(string filterJson, List<ListFormField> listFormFields)
{
string sqlPart = string.Empty;
if (string.IsNullOrWhiteSpace(fieldName) || string.IsNullOrWhiteSpace(op))
JsonElement root;
try
{
return sqlPart;
using var document = JsonDocument.Parse(filterJson);
root = document.RootElement.Clone();
}
catch (JsonException)
{
throw new UserFriendlyException("Invalid filter.");
}
if (root.ValueKind != JsonValueKind.Array)
{
throw new UserFriendlyException("Invalid filter.");
}
return BuildFilterNode(root, listFormFields, 0);
}
private string BuildFilterNode(JsonElement node, List<ListFormField> listFormFields, int depth)
{
if (depth > MaxFilterDepth)
{
throw new UserFriendlyException("Invalid filter.");
}
var items = node.EnumerateArray().ToList();
if (items.Count == 0)
{
return null;
}
if (items.Count == 2 && IsFilterToken(items[0], "!") && items[1].ValueKind == JsonValueKind.Array)
{
var negated = BuildFilterNode(items[1], listFormFields, depth + 1);
return negated == null ? null : $"NOT ({negated})";
}
if (IsFilterCondition(items))
{
return items.Count == 2
? BuildFilterCondition(items[0].GetString(), "=", items[1], listFormFields)
: BuildFilterCondition(items[0].GetString(), items[1].GetString(), items[2], listFormFields);
}
// Grup: terimler ve aralarındaki bağlaçlar. Bağlaçsız ardışık terimler "and" sayılır (DevExtreme
// kuralı). UI bazen koşulu gruba düz ekler (["A","=",1,"and",[...]]); bu üçlü de koşul olarak okunur.
var parts = new List<string>();
string connector = null;
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
string term;
if (IsFilterToken(item, "and") || IsFilterToken(item, "or"))
{
connector = item.GetString().ToUpperInvariant();
continue;
}
if (IsFilterToken(item, "!") && i + 1 < items.Count && items[i + 1].ValueKind == JsonValueKind.Array)
{
var negated = BuildFilterNode(items[++i], listFormFields, depth + 1);
term = negated == null ? null : $"NOT ({negated})";
}
else if (item.ValueKind == JsonValueKind.Array)
{
var group = BuildFilterNode(item, listFormFields, depth + 1);
term = group == null ? null : $"({group})";
}
else if (item.ValueKind == JsonValueKind.String
&& i + 2 < items.Count
&& items[i + 1].ValueKind == JsonValueKind.String)
{
term = BuildFilterCondition(item.GetString(), items[i + 1].GetString(), items[i + 2], listFormFields);
i += 2;
}
else
{
throw new UserFriendlyException("Invalid filter.");
}
if (term == null)
{
continue;
}
if (parts.Count > 0)
{
parts.Add(connector ?? "AND");
}
parts.Add(term);
connector = null;
}
return parts.Count == 0 ? null : string.Join(" ", parts);
}
private static bool IsFilterToken(JsonElement item, string token)
{
return item.ValueKind == JsonValueKind.String
&& string.Equals(item.GetString(), token, StringComparison.OrdinalIgnoreCase);
}
/// <summary><c>["Alan","op",değer]</c> ya da kısa biçim <c>["Alan",değer]</c>.</summary>
private static bool IsFilterCondition(List<JsonElement> items)
{
if (items[0].ValueKind != JsonValueKind.String
|| IsFilterToken(items[0], "!")
|| IsFilterToken(items[0], "and")
|| IsFilterToken(items[0], "or"))
{
return false;
}
return items.Count switch
{
2 => items[1].ValueKind != JsonValueKind.Array,
3 => items[1].ValueKind == JsonValueKind.String
&& !IsFilterToken(items[1], "and")
&& !IsFilterToken(items[1], "or"),
_ => false,
};
}
/// <summary>Tek koşulun SQL'i. Alan ekranda yoksa koşul etkisiz (<c>1 = 1</c>) yazılır.</summary>
private string BuildFilterCondition(string fieldName, string op, JsonElement value, List<ListFormField> listFormFields)
{
if (string.IsNullOrWhiteSpace(fieldName) || string.IsNullOrWhiteSpace(op))
{
return null;
}
var fieldValue = value.ValueKind switch
{
JsonValueKind.String => value.GetString(),
JsonValueKind.Number => value.GetRawText(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Null or JsonValueKind.Undefined => "null",
_ => throw new UserFriendlyException("Invalid filter."),
};
var fieldFilter = fieldName.Split('.');
var filterFieldName = fieldFilter.First().RemoveParentheses();
var filterFieldName = fieldFilter.First();
var filterGroupInterval = fieldFilter.Length > 1 ? fieldFilter.Last().ToLower() : null;
var filterParamKey = $"@{filterFieldName}{customKey}";
var filterParamKeyLower = filterParamKey;
var listFormField = listFormFields.FirstOrDefault(c => c.FieldName == filterFieldName);
// Anahtar alan her sorguya eklenir ama alan listesinde olmayabilir; grid satira odaklanirken
// filtreye anahtar kosulu yazar.
var isKeyField = listFormField == null && filterFieldName == KeyFieldName;
if (listFormField == null && !isKeyField)
{
// Kosul dusurulurse etrafindaki baglac ve parantezler yetim kalir (WHERE and (...)));
// yerine parantezleri koruyan etkisiz bir kosul yazilir.
var leading = fieldName[..^fieldName.TrimStart('(').Length];
var trailing = fieldValue[fieldValue.TrimEnd(')').Length..];
return $"{leading}1 = 1{trailing}";
return "1 = 1";
}
var filterOperator = op.RemoveParentheses();
var filterOperator = op;
if (!FilterOperators.Contains(filterOperator))
{
throw new UserFriendlyException($"'{op}' is not a supported filter operator.");
}
var filterParamKey = $"@{filterFieldName}{filterParameterIndex++}";
var filterParamKeyLower = filterParamKey;
// filterGroupInterval sayı olduğu için (örn OrderDate.Month), eğer bu gelirse sayıya parse ediyoruz,
// diğer durumlarda field tipine parse ediyoruz
DbType dbType = filterGroupInterval switch
@ -571,13 +697,12 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
_ => isKeyField ? KeyFieldDbSourceType : listFormField.SourceDbType
};
var filterValue = fieldValue.RemoveParentheses();
var filterValue = fieldValue;
if (filterValue == "null")
{
if (filterOperator == "=") filterOperator = "isblank";
if (filterOperator == "<>") filterOperator = "isnotblank";
}
var lastCharacters = fieldValue.Replace(filterValue, string.Empty); // Sadece parantezleri almak için
var filterSqlFieldName = ConvertAliasToFieldName(filterFieldName, listFormFields, filterGroupInterval);
if (filterOperator == "=" && DataSourceType == DataSourceTypeEnum.Postgresql && (
@ -588,7 +713,6 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
filterParamKeyLower = $"lower({filterParamKeyLower})";
filterValue = filterValue.Replace('I', 'ı');
}
filterSqlFieldName = fieldFilter.First().Replace(filterFieldName, filterSqlFieldName);
var sqlOperator = "";
string formatSql = "{0} {1} {2}";
@ -615,11 +739,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
else if (filterOperator == "isblank") formatSql = "{0} IS NULL";
else if (filterOperator == "isnotblank") formatSql = "{0} IS NOT NULL";
sqlPart = string.Format(formatSql, filterSqlFieldName, sqlOperator, filterParamKeyLower);
if (lastCharacters.Any())
{
sqlPart += lastCharacters;
}
var sqlPart = string.Format(formatSql, filterSqlFieldName, sqlOperator, filterParamKeyLower);
if (formatSql.Contains("{2}"))
{
@ -743,9 +863,18 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
var isExpanded = items.Length > 2 && items[2] == "true";
var groupInterval = items.Length > 3 ? validGroupIntervals.Contains(items[3]) ? items[3] : null : null;
var fieldName = ConvertAliasToFieldName(field, listFormFields, groupInterval);// Alias li bir alan ise gercek alani bul
if (fieldName == UnknownFieldName)
{
continue;
}
GroupTuples.Add((i, fieldName, $"Group_{i}", sort, isExpanded, groupInterval));
}
if (GroupTuples.Count == 0)
{
return null;
}
// gruplanan bir alan icin grup satirinda summary bilgisinin gorunmesi icin gerekli veri yapisidir,
// grup sorgusu ile birlikte calistirilir.
// Gelen veri yapisi: FieldName SummaryType|FieldName SummaryType
@ -768,6 +897,10 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
continue;
}
var fieldName = ConvertAliasToFieldName(field, listFormFields);// Alias li bir alan ise gercek alani bul
if (fieldName == UnknownFieldName)
{
continue;
}
GroupSummaryTuples.Add((i, fieldName, $"Summary_{i}", summaryType));
}
}

View file

@ -1,192 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Guids;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement;
using Volo.Abp.Uow;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Migrations;
public class DatabaseMigrationEventHandler :
IDistributedEventHandler<ApplyDatabaseMigrationsEto>,
ITransientDependency
{
protected string DatabaseName { get; }
public ICurrentTenant CurrentTenant { get; }
public IUnitOfWorkManager UnitOfWorkManager { get; }
public ITenantStore TenantStore { get; }
public IAbpDistributedLock DistributedLock { get; }
public IDataSeeder DataSeeder { get; }
public IOptions<IdentityOptions> IdentityOptions { get; }
public IIdentityUserRepository UserRepository { get; }
public IIdentityRoleRepository RoleRepository { get; }
public ILookupNormalizer LookupNormalizer { get; }
public IGuidGenerator GuidGenerator { get; }
public IdentityUserManager UserManager { get; }
public IdentityRoleManager RoleManager { get; }
public IPermissionGrantRepository PermissionGrantRepository { get; }
public ITenantRepository TenantRepository { get; }
protected ILogger<DatabaseMigrationEventHandler> Logger { get; }
public DatabaseMigrationEventHandler(
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
ITenantStore tenantStore,
IAbpDistributedLock distributedLock,
IDistributedEventBus distributedEventBus,
ILoggerFactory loggerFactory,
IDataSeeder dataSeeder,
IOptions<IdentityOptions> identityOptions,
IIdentityUserRepository userRepository,
IIdentityRoleRepository roleRepository,
ILookupNormalizer lookupNormalizer,
IGuidGenerator guidGenerator,
IdentityUserManager userManager,
IdentityRoleManager roleManager,
IPermissionGrantRepository permissionGrantRepository,
ITenantRepository tenantRepository)
{
DatabaseName = DefaultDatabaseProvider;
CurrentTenant = currentTenant;
UnitOfWorkManager = unitOfWorkManager;
TenantStore = tenantStore;
DistributedLock = distributedLock;
DataSeeder = dataSeeder;
IdentityOptions = identityOptions;
UserRepository = userRepository;
RoleRepository = roleRepository;
LookupNormalizer = lookupNormalizer;
GuidGenerator = guidGenerator;
UserManager = userManager;
RoleManager = roleManager;
PermissionGrantRepository = permissionGrantRepository;
TenantRepository = tenantRepository;
Logger = loggerFactory.CreateLogger<DatabaseMigrationEventHandler>();
}
public async Task HandleEventAsync(ApplyDatabaseMigrationsEto eventData)
{
try
{
var schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.TenantId);
if (schemaMigrated)
{
await SeedTenantDataAsync(eventData);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error while seeding tenant data");
throw;
//await HandleErrorOnApplyDatabaseMigrationAsync(eventData, ex);
}
}
private async Task<bool> MigrateDatabaseSchemaAsync(Guid? tenantId)
{
var result = false;
using (CurrentTenant.Change(tenantId))
{
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
async Task<IAsyncDisposable> WaitForDistributedLockAsync(PlatformDbContext dbContext)
{
var distributedLockHandle = await DistributedLock.TryAcquireAsync(
"DatabaseMigrationEventHandler_" +
dbContext.Database.GetConnectionString()!.ToUpperInvariant().ToMd5(),
TimeSpan.FromSeconds(60)
);
if (distributedLockHandle == null)
{
throw new AbpException($"Distributed lock could not be acquired for database migration event handler: {DatabaseName}.");
}
return distributedLockHandle;
}
async Task<bool> MigrateDatabaseSchemaWithDbContextAsync()
{
var dbContext = await uow.ServiceProvider
.GetRequiredService<IDbContextProvider<PlatformDbContext>>()
.GetDbContextAsync();
await using (await WaitForDistributedLockAsync(dbContext))
{
if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
{
await dbContext.Database.MigrateAsync();
return true;
}
}
return false;
}
if (tenantId == null)
{
//Migrating the host database
Logger.LogInformation($"Migrating database of host. Database Name = {DatabaseName}");
result = await MigrateDatabaseSchemaWithDbContextAsync();
}
else
{
var tenantConfiguration = await TenantStore.FindAsync(tenantId.Value);
if (!tenantConfiguration!.ConnectionStrings!.Default.IsNullOrWhiteSpace() ||
!tenantConfiguration.ConnectionStrings.GetOrDefault(DatabaseName).IsNullOrWhiteSpace())
{
//Migrating the tenant database (only if tenant has a separate database)
Logger.LogInformation($"Migrating separate database of tenant. Database Name = {DatabaseName}, TenantId = {tenantId}");
result = await MigrateDatabaseSchemaWithDbContextAsync();
}
else
{
result = true;
}
}
await uow.CompleteAsync();
}
}
return result;
}
private async Task SeedTenantDataAsync(ApplyDatabaseMigrationsEto eventData)
{
using (CurrentTenant.Change(eventData.TenantId))
{
await DataSeeder.SeedAsync(
new DataSeedContext(eventData.TenantId)
.WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, eventData.Properties[IdentityDataSeedContributor.AdminEmailPropertyName])
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, eventData.Properties[IdentityDataSeedContributor.AdminPasswordPropertyName])
.WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, eventData.Properties[IdentityDataSeedContributor.AdminUserNamePropertyName])
);
}
}
}

View file

@ -9,15 +9,31 @@ using Sozsoft.Platform.Domain.DeveloperKit;
using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy;
public class DynamicEntityManager : IDynamicEntityManager
{
private const string TenantIdColumn = "TenantId";
/// <summary>
/// Kimlik bilgisi taşıyan kolonlar: istemciye hiç dönmez ve istemciden yazılamaz.
/// Parola ve güvenlik damgası yalnızca kimlik yöneticisinden (UserManager) geçer.
/// </summary>
private static readonly HashSet<string> SecretColumns = new(StringComparer.OrdinalIgnoreCase)
{
"PasswordHash",
"SecurityStamp",
};
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly ICurrentTenant _currentTenant;
public DynamicEntityManager(
IDbContextProvider<PlatformDbContext> dbContextProvider)
IDbContextProvider<PlatformDbContext> dbContextProvider,
ICurrentTenant currentTenant)
{
_dbContextProvider = dbContextProvider;
_currentTenant = currentTenant;
}
public async Task<List<object>?> GetEntityListAsync(
@ -35,6 +51,10 @@ public class DynamicEntityManager : IDynamicEntityManager
conditions.Add($"({idDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {idDeletedColumn} IS NULL)");
var parameters = new List<object>();
var tenantCondition = await BuildTenantConditionAsync(entityName, isPostgreSql, parameters);
if (tenantCondition != null)
conditions.Add(tenantCondition);
if (filters is { Count: > 0 })
{
var columns = await GetColumnNamesAsync(entityName);
@ -49,7 +69,7 @@ public class DynamicEntityManager : IDynamicEntityManager
var query = conditions.Count > 0
? $"SELECT * FROM {tableName} WHERE {string.Join(" AND ", conditions)}"
: $"SELECT * FROM {tableName}";
return await ExecuteRawQueryAsync(query, parameters);
return RemoveSecretColumns(await ExecuteRawQueryAsync(query, parameters));
}
/// <summary>
@ -196,11 +216,18 @@ public class DynamicEntityManager : IDynamicEntityManager
var idColumn = QuoteIdentifier("Id", isPostgreSql);
var isDeletedColumn = QuoteIdentifier("IsDeleted", isPostgreSql);
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var query = hasIsDeleted
? $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}' AND ({isDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {isDeletedColumn} IS NULL)"
: $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}'";
var result = await ExecuteRawQueryAsync(query);
return result?.FirstOrDefault();
var parameters = new List<object>();
var conditions = new List<string> { $"{idColumn} = {AddParameter(parameters, id)}" };
if (hasIsDeleted)
conditions.Add($"({isDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {isDeletedColumn} IS NULL)");
var tenantCondition = await BuildTenantConditionAsync(entityName, isPostgreSql, parameters);
if (tenantCondition != null)
conditions.Add(tenantCondition);
var query = $"SELECT * FROM {tableName} WHERE {string.Join(" AND ", conditions)}";
return RemoveSecretColumns(await ExecuteRawQueryAsync(query, parameters)).FirstOrDefault();
}
public async Task<object?> CreateEntityAsync(string entityName, JsonElement data)
@ -218,9 +245,16 @@ public class DynamicEntityManager : IDynamicEntityManager
if (hasCreationTime) { columns.Add(QuoteIdentifier("CreationTime", isPostgreSql)); values.Add(UtcNowExpression(isPostgreSql)); }
if (hasIsDeleted) { columns.Add(QuoteIdentifier("IsDeleted", isPostgreSql)); values.Add(FalseLiteral(isPostgreSql)); }
// Kayıt her zaman isteği yapanın kiracısına yazılır; istemcinin gönderdiği TenantId yok sayılır.
if (_currentTenant.Id.HasValue && await ColumnExistsAsync(entityName, TenantIdColumn))
{
columns.Add(QuoteIdentifier(TenantIdColumn, isPostgreSql));
values.Add(AddParameter(parameters, _currentTenant.Id.Value));
}
foreach (var prop in data.EnumerateObject())
{
if (prop.NameEquals("id") || prop.NameEquals("Id"))
if (IsProtectedColumn(prop.Name))
continue;
columns.Add(QuoteIdentifier(prop.Name, isPostgreSql));
@ -251,7 +285,7 @@ public class DynamicEntityManager : IDynamicEntityManager
foreach (var prop in data.EnumerateObject())
{
if (prop.NameEquals("id") || prop.NameEquals("Id"))
if (IsProtectedColumn(prop.Name))
continue;
setParts.Add($"{QuoteIdentifier(prop.Name, isPostgreSql)} = {AddParameter(parameters, ToSqlParameterValue(prop.Value))}");
@ -300,6 +334,44 @@ public class DynamicEntityManager : IDynamicEntityManager
}
}
/// <summary>
/// Kiracı kolonu olan tabloda yalnızca isteği yapanın kiracısının satırları görünür (host için
/// <c>TenantId IS NULL</c>). Güncelleme ve silme önce <see cref="GetEntityByIdAsync"/> ile satırı
/// bulduğu için aynı sınır onlara da uygulanır.
/// </summary>
private async Task<string?> BuildTenantConditionAsync(string entityName, bool isPostgreSql, List<object> parameters)
{
if (!await ColumnExistsAsync(entityName, TenantIdColumn))
return null;
var column = QuoteIdentifier(TenantIdColumn, isPostgreSql);
return _currentTenant.Id.HasValue
? $"{column} = {AddParameter(parameters, _currentTenant.Id.Value)}"
: $"{column} IS NULL";
}
/// <summary>Anahtar, kiracı ve kimlik bilgisi kolonları istemci verisinden yazılmaz.</summary>
private static bool IsProtectedColumn(string columnName)
{
return columnName.Equals("Id", StringComparison.OrdinalIgnoreCase)
|| columnName.Equals(TenantIdColumn, StringComparison.OrdinalIgnoreCase)
|| SecretColumns.Contains(columnName);
}
private static List<object> RemoveSecretColumns(List<object> rows)
{
foreach (var row in rows)
{
if (row is not Dictionary<string, object?> item)
continue;
foreach (var key in item.Keys.Where(SecretColumns.Contains).ToList())
item.Remove(key);
}
return rows;
}
private async Task<bool> ColumnExistsAsync(string tableName, string columnName)
{
var dbContext = await _dbContextProvider.GetDbContextAsync();

View file

@ -1,197 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.TenantManagement;
using IdentityRole = Volo.Abp.Identity.IdentityRole;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
namespace Sozsoft.Platform.Data.Seeds;
[Dependency(ReplaceServices = true)]
public class TenantIdentityDataSeeder : IdentityDataSeeder
{
private readonly IRepository<Branch, Guid> _branchRepository;
private readonly IRepository<BranchUsers, Guid> _branchUsersRepository;
private readonly ITenantRepository _tenantRepository;
private readonly IConfiguration _configuration;
public TenantIdentityDataSeeder(
IGuidGenerator guidGenerator,
IIdentityRoleRepository roleRepository,
IIdentityUserRepository userRepository,
IRepository<Branch, Guid> branchRepository,
IRepository<BranchUsers, Guid> branchUsersRepository,
ILookupNormalizer lookupNormalizer,
IdentityUserManager userManager,
IdentityRoleManager roleManager,
ICurrentTenant currentTenant,
ITenantRepository tenantRepository,
IConfiguration configuration,
IOptions<IdentityOptions> identityOptions
) : base(guidGenerator, roleRepository, userRepository, lookupNormalizer, userManager, roleManager, currentTenant, identityOptions)
{
this._branchRepository = branchRepository;
this._branchUsersRepository = branchUsersRepository;
this._tenantRepository = tenantRepository;
this._configuration = configuration;
}
public override async Task<IdentityDataSeedResult> SeedAsync(string adminEmail, string adminPassword, Guid? tenantId = null, string adminUserName = null)
{
Check.NotNullOrWhiteSpace(adminEmail, nameof(adminEmail));
Check.NotNullOrWhiteSpace(adminPassword, nameof(adminPassword));
using (CurrentTenant.Change(tenantId))
{
await IdentityOptions.SetAsync();
var result = new IdentityDataSeedResult();
var branchCode = "";
var branchName = "";
if (tenantId == null && !CurrentTenant.IsAvailable)
{
branchCode = PlatformConsts.Branches.BranchCode;
branchName = PlatformConsts.Branches.BranchName;
}
else
{
var tenant = await _tenantRepository.FindAsync(tenantId ?? CurrentTenant.Id.Value);
if (tenant != null)
{
branchCode = tenant.Name;
branchName = tenant.GetOrganizationName();
}
}
//Default Branch otomatik olarak oluşturuluyor.
var defaultBranch = await _branchRepository.FirstOrDefaultAsync(b => b.Code == branchCode);
if (defaultBranch == null)
{
var branchId = tenantId == null && !CurrentTenant.IsAvailable
? PlatformConsts.Branches.BranchIdDefaultValue
: GuidGenerator.Create();
defaultBranch = await _branchRepository.InsertAsync(new Branch(branchId)
{
Code = branchCode,
Name = branchName,
VknTckn = PlatformConsts.Branches.BranchVknTckn,
TaxOffice = PlatformConsts.Branches.BranchTaxOffice,
MobileNumber = PlatformConsts.Branches.BranchMobileNumber,
Country = PlatformConsts.Branches.BranchCountry,
City = PlatformConsts.Branches.BranchCity,
District = PlatformConsts.Branches.BranchDistrict,
Township = PlatformConsts.Branches.BranchTownship,
Email = PlatformConsts.Branches.BranchEmail,
Website = PlatformConsts.Branches.BranchWebsite,
IsActive = PlatformConsts.Branches.BranchIsActive,
TenantId = tenantId,
}, autoSave: true);
}
var adminRole = await RoleRepository.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName(PlatformConsts.AbpIdentity.User.AdminRoleName));
if (adminRole is null)
{
adminRole = new IdentityRole(
GuidGenerator.Create(),
PlatformConsts.AbpIdentity.User.AdminRoleName,
tenantId
)
{
IsStatic = true,
IsPublic = true
};
(await RoleManager.CreateAsync(adminRole)).CheckErrors();
result.CreatedAdminRole = true;
}
adminUserName ??= PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue;
var adminUser = await UserRepository.FindByNormalizedUserNameAsync(
LookupNormalizer.NormalizeName(adminUserName)
);
if (adminUser is null)
{
adminUser = new IdentityUser(
GuidGenerator.Create(),
adminUserName,
adminEmail,
tenantId
)
{
Name = PlatformConsts.AbpIdentity.User.AdminNameDefaultValue,
Surname = PlatformConsts.AbpIdentity.User.AdminSurNameDefaultValue,
};
adminUser.SetIsVerified(true);
adminUser.SetEmailConfirmed(true);
adminUser.SetRocketUsername(PlatformConsts.AbpIdentity.User.AdminRocketUsernameDefaultValue);
adminUser.SetPhoneNumber(PlatformConsts.AbpIdentity.User.AdminPhoneNumberDefaultValue, adminUser.PhoneNumberConfirmed);
adminUser.SetWorkHour(PlatformConsts.AbpIdentity.User.AdminWorkHourDefaultValue);
adminUser.SetNationality(PlatformConsts.AbpIdentity.User.AdminNationalityDefaultValue);
adminUser.SetBloodType(PlatformConsts.AbpIdentity.User.AdminBloodTypeDefaultValue);
adminUser.SetEducationLevel(PlatformConsts.AbpIdentity.User.AdminEducationLevelDefaultValue);
adminUser.SetHomeAddress(PlatformConsts.AbpIdentity.User.AdminHomeAddressDefaultValue);
adminUser.SetBirthDate(PlatformConsts.AbpIdentity.User.AdminBirthDateDefaultValue);
adminUser.SetBirthPlace(PlatformConsts.AbpIdentity.User.AdminBirthPlaceDefaultValue);
adminUser.SetMaritalStatus(PlatformConsts.AbpIdentity.User.AdminMaritalStatusDefaultValue);
adminUser.SetDepartmentId(PlatformConsts.AbpIdentity.User.AdminDepartmentIdDefaultValue);
adminUser.SetJobPositionId(PlatformConsts.AbpIdentity.User.AdminJobPositionIdDefaultValue);
adminUser.SetAvatar(GetAvatar(tenantId?.ToString(), adminUser.Id.ToString()));
adminUser.SetUserType(PlatformConsts.AbpIdentity.User.InternalUserTypeDefaultValue);
adminUser.SetGender(PlatformConsts.AbpIdentity.User.AdminGenderDefaultValue);
(await UserManager.CreateAsync(adminUser, adminPassword, validatePassword: false)).CheckErrors();
result.CreatedAdminUser = true;
(await UserManager.AddToRoleAsync(adminUser, PlatformConsts.AbpIdentity.User.AdminRoleName)).CheckErrors();
// BranchUsers tablosuna admin kullanicisi ekleniyor.
var defaultBranchUsers = await _branchUsersRepository.FirstOrDefaultAsync(bu => bu.UserId == adminUser.Id && bu.BranchId == defaultBranch.Id);
if (defaultBranchUsers is null)
{
await _branchUsersRepository.InsertAsync(new BranchUsers
{
BranchId = defaultBranch.Id,
UserId = adminUser.Id,
TenantId = tenantId
}, autoSave: true);
}
}
// Grant all permission kısmı Abp tarafından otomatik çalıştırılıyor.
// Ayrıca bende çalıştırınca hata veriyor.
// Hem DbMigrator hem de burada IdentityDataSeeder var.
// Aynı Assembly'de birden fazla IdentityDataSeeder olabilir
// DbMigrator çalışınca kendi IdentityDataSeeder'ını çalıştırıyor.
// Tenant seed edilince bu IdentityDataSeeder çalışıyor.
// See: Volo.Abp.PermissionManagement.PermissionDataSeedContributor
return result;
}
}
private string GetAvatar(string? tenantId, string? userId)
{
var tenantPart = !string.IsNullOrEmpty(tenantId)
? $"tenants/{tenantId}"
: "host";
var baseUrl = _configuration["App:CdnUrl"]?.TrimEnd('/');
return $"{baseUrl}/{tenantPart}/avatar/{userId ?? "default"}.jpg";
}
}

View file

@ -8,7 +8,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.BulkExtensions" Version="10.0.1" />
<ProjectReference Include="..\..\modules\Sozsoft.Languages\Sozsoft.Languages.EntityFrameworkCore\Sozsoft.Languages.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Sozsoft.Notifications\Sozsoft.Notifications.EntityFrameworkCore\Sozsoft.Notifications.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Sozsoft.Settings\Sozsoft.Settings.EntityFrameworkCore\Sozsoft.Settings.EntityFrameworkCore.csproj" />
@ -18,7 +17,6 @@
<PackageReference Include="Volo.Abp.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Volo.Abp.EntityFrameworkCore.PostgreSql" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Include="Oracle.EntityFrameworkCore" Version="10.23.26200" />
<PackageReference Include="Volo.Abp.PermissionManagement.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Volo.Abp.Identity.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Volo.Abp.BackgroundJobs.EntityFrameworkCore" Version="10.0.0" />

View file

@ -1,39 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Account;
using Volo.Abp.FeatureManagement;
using Volo.Abp.Identity;
using Volo.Abp.Modularity;
using Volo.Abp.PermissionManagement;
using Volo.Abp.SettingManagement;
using Volo.Abp.TenantManagement;
using Volo.Abp.VirtualFileSystem;
namespace Sozsoft.Platform;
[DependsOn(
typeof(PlatformApplicationContractsModule),
typeof(AbpAccountHttpApiClientModule),
typeof(AbpIdentityHttpApiClientModule),
typeof(AbpPermissionManagementHttpApiClientModule),
typeof(AbpTenantManagementHttpApiClientModule),
typeof(AbpFeatureManagementHttpApiClientModule),
typeof(AbpSettingManagementHttpApiClientModule)
)]
public class PlatformHttpApiClientModule : AbpModule
{
public const string RemoteServiceName = "Default";
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(
typeof(PlatformApplicationContractsModule).Assembly,
RemoteServiceName
);
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<PlatformHttpApiClientModule>();
});
}
}

View file

@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Sozsoft.Platform</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Sozsoft.Platform.Application.Contracts\Sozsoft.Platform.Application.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Account.HttpApi.Client" Version="10.0.0" />
<PackageReference Include="Volo.Abp.Identity.HttpApi.Client" Version="10.0.0" />
<PackageReference Include="Volo.Abp.PermissionManagement.HttpApi.Client" Version="10.0.0" />
<PackageReference Include="Volo.Abp.TenantManagement.HttpApi.Client" Version="10.0.0" />
<PackageReference Include="Volo.Abp.FeatureManagement.HttpApi.Client" Version="10.0.0" />
<PackageReference Include="Volo.Abp.SettingManagement.HttpApi.Client" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="**\*generate-proxy.json" />
<Content Remove="**\*generate-proxy.json" />
</ItemGroup>
</Project>

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -10,12 +11,14 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Data;
using Sozsoft.Platform.DbStartup;
using Sozsoft.Platform.EntityFrameworkCore;
using Sozsoft.Platform.Permissions;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy;
using Volo.Abp.TenantManagement;
using Volo.Abp.Uow;
namespace Sozsoft.Platform.Controllers;
@ -46,6 +49,7 @@ public class SetupController : ControllerBase
private readonly ICurrentTenant _currentTenant;
private readonly IAbpDistributedLock _distributedLock;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly ITenantRepository _tenantRepository;
private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer;
private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer;
private readonly ILogger<SetupController> _logger;
@ -58,6 +62,7 @@ public class SetupController : ControllerBase
ICurrentTenant currentTenant,
IAbpDistributedLock distributedLock,
IUnitOfWorkManager unitOfWorkManager,
ITenantRepository tenantRepository,
BackgroundWorkerInitializer backgroundWorkerInitializer,
AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer,
ILogger<SetupController> logger)
@ -69,6 +74,7 @@ public class SetupController : ControllerBase
_currentTenant = currentTenant;
_distributedLock = distributedLock;
_unitOfWorkManager = unitOfWorkManager;
_tenantRepository = tenantRepository;
_backgroundWorkerInitializer = backgroundWorkerInitializer;
_abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer;
_logger = logger;
@ -108,12 +114,65 @@ public class SetupController : ControllerBase
if (!authorized)
{
_logger.LogWarning("Database migration request denied: {Reason}", denialReason);
Response.StatusCode = StatusCodes.Status403Forbidden;
Response.ContentType = "text/plain; charset=utf-8";
await Response.WriteAsync(denialReason);
await WritePlainTextAsync(StatusCodes.Status403Forbidden, denialReason);
return;
}
_logger.LogWarning(
"Database migration triggered via /api/setup/migrate by user {UserId}.",
User?.Identity?.Name ?? "(setup password)");
await RunMigratorAsync(environment: null, reloadWorkers: true);
}
/// <summary>
/// Tenants ekranının Seed sekmesi: yalnızca bu tenant için migration + seed çalıştırır ve
/// logları <see cref="MigrateAsync"/> ile aynı SSE biçiminde akıtır. Yönetici e-posta/parolası
/// DbMigrator'a ortam değişkeniyle geçer (<see cref="TenantSeedOptions"/>).
/// </summary>
[HttpPost("tenants/{id:guid}/seed")]
public async Task SeedTenantAsync(Guid id, [FromBody] SeedTenantInput input)
{
var (authorized, denialReason) = await AuthorizeTenantSeedRequestAsync();
if (!authorized)
{
_logger.LogWarning("Tenant seed request denied: {Reason}", denialReason);
await WritePlainTextAsync(StatusCodes.Status403Forbidden, denialReason);
return;
}
if (string.IsNullOrWhiteSpace(input.AdminEmail) || string.IsNullOrWhiteSpace(input.AdminPassword))
{
await WritePlainTextAsync(StatusCodes.Status400BadRequest, "Admin email and password are required.");
return;
}
var tenant = await _tenantRepository.FindAsync(id, includeDetails: false);
if (tenant is null)
{
await WritePlainTextAsync(StatusCodes.Status404NotFound, $"Tenant not found: {id}");
return;
}
_logger.LogWarning(
"Tenant seed triggered for {TenantName} ({TenantId}) by user {UserId}.",
tenant.Name, tenant.Id, User?.Identity?.Name);
await RunMigratorAsync(new Dictionary<string, string>
{
[TenantSeedOptions.ToEnvironmentVariable(TenantSeedOptions.TenantIdKey)] = tenant.Id.ToString(),
[TenantSeedOptions.ToEnvironmentVariable(TenantSeedOptions.AdminEmailKey)] = input.AdminEmail.Trim(),
[TenantSeedOptions.ToEnvironmentVariable(TenantSeedOptions.AdminPasswordKey)] = input.AdminPassword,
}, reloadWorkers: false);
}
/// <summary>
/// DbMigrator'ü tek kilit altında çalıştırır; migrate ile tenant seed aynı anda yürümez.
/// Worker tanımları host verisidir; tek tenant seed'i onlara dokunmadığı için
/// <paramref name="reloadWorkers"/> yalnızca tam migrate'te açıktır.
/// </summary>
private async Task RunMigratorAsync(IReadOnlyDictionary<string, string>? environment, bool reloadWorkers)
{
var stream = new SetupEventStream(Response);
await stream.StartAsync();
@ -130,13 +189,9 @@ public class SetupController : ControllerBase
return;
}
_logger.LogWarning(
"Database migration triggered via /api/setup/migrate by user {UserId}.",
User?.Identity?.Name ?? "(setup password)");
// ApplicationStopping kullanılıyor: istemci sekmesini kapatsa da migration tamamlanır.
var outcome = await SetupMigrationRunner.ExecuteAsync(
_configuration, _env.ContentRootPath, stream, _lifetime.ApplicationStopping);
_configuration, _env.ContentRootPath, stream, _lifetime.ApplicationStopping, environment);
if (outcome != MigrationOutcome.Succeeded)
{
@ -144,6 +199,13 @@ public class SetupController : ControllerBase
return;
}
if (!reloadWorkers)
{
await stream.SuccessAsync("Migration and seed completed successfully.");
await stream.DoneAsync("Completed.");
return;
}
await stream.InfoAsync("Migration and seed completed successfully.");
// Yeni/değişen tanımlar Hangfire'a yansısın diye worker'lar tazelenir.
@ -165,6 +227,13 @@ public class SetupController : ControllerBase
await stream.DoneAsync("Completed.");
}
private async Task WritePlainTextAsync(int statusCode, string message)
{
Response.StatusCode = statusCode;
Response.ContentType = "text/plain; charset=utf-8";
await Response.WriteAsync(message);
}
// Yetkilendirme
/// <summary>
@ -215,6 +284,27 @@ public class SetupController : ControllerBase
: (false, $"The '{PlatformPermissions.Setup.Migrate}' permission is required.");
}
/// <summary>
/// Tenant seed'i tenant yönetimi işidir: host kullanıcısı + tenant güncelleme yetkisi.
/// Setup parolası bu uçta geçerli değildir; veritabanı ayakta olmadan tenant da yoktur.
/// </summary>
private async Task<(bool Authorized, string Reason)> AuthorizeTenantSeedRequestAsync()
{
if (User?.Identity?.IsAuthenticated != true)
{
return (false, "Authentication is required to seed tenant data.");
}
if (_currentTenant.Id is not null)
{
return (false, "Tenant data can only be seeded by a host user.");
}
return (await _authorizationService.AuthorizeAsync(User, TenantManagementPermissions.Tenants.Update)).Succeeded
? (true, string.Empty)
: (false, $"The '{TenantManagementPermissions.Tenants.Update}' permission is required.");
}
private async Task<bool> CanMigrateAsync()
{
if (User?.Identity?.IsAuthenticated != true)

View file

@ -0,0 +1,11 @@
namespace Sozsoft.Platform.DbStartup;
/// <summary>
/// /api/setup/tenants/{id}/seed isteği: tenant'ın yönetici hesabı seed sırasında bu bilgilerle kurulur.
/// </summary>
public sealed class SeedTenantInput
{
public string AdminEmail { get; init; } = string.Empty;
public string AdminPassword { get; init; } = string.Empty;
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
@ -42,11 +43,15 @@ public static class SetupMigrationRunner
/// uygulamanın <c>ApplicationStopping</c> token'ı olmalıdır. Aksi halde kullanıcı sekmeyi
/// kapattığında migration yarıda kesilir ve veritabanı tutarsız kalır.
/// </param>
/// <param name="environment">
/// Alt sürece ek ortam değişkenleri (ör. tek tenant seed'i). Gizli değer taşıyabilir; loglanmaz.
/// </param>
public static async Task<MigrationOutcome> ExecuteAsync(
IConfiguration configuration,
string contentRootPath,
SetupEventStream stream,
CancellationToken processCancellationToken)
CancellationToken processCancellationToken,
IReadOnlyDictionary<string, string>? environment = null)
{
if (!await ExecutionGate.WaitAsync(0, CancellationToken.None))
{
@ -57,7 +62,7 @@ public static class SetupMigrationRunner
try
{
return await ExecuteCoreAsync(configuration, contentRootPath, stream, processCancellationToken);
return await ExecuteCoreAsync(configuration, contentRootPath, stream, processCancellationToken, environment);
}
finally
{
@ -69,7 +74,8 @@ public static class SetupMigrationRunner
IConfiguration configuration,
string contentRootPath,
SetupEventStream stream,
CancellationToken processCancellationToken)
CancellationToken processCancellationToken,
IReadOnlyDictionary<string, string>? environment)
{
await stream.InfoAsync("Database migration and seeding are being initiated...");
@ -112,6 +118,14 @@ public static class SetupMigrationRunner
process.StartInfo.Environment[SeedsPathEnvironmentVariable] =
SeedPathResolver.GetRootPath(configuration);
if (environment is not null)
{
foreach (var (name, value) in environment)
{
process.StartInfo.Environment[name] = value;
}
}
process.Start();
async Task PumpAsync(StreamReader reader, string level)

View file

@ -10,14 +10,13 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.DependencyInjection;
namespace Sozsoft.Platform.DynamicServices;
/// <summary>
/// Dinamik assembly'leri runtime'da DI ve MVC sistemine kaydeden background service
/// </summary>
public class DynamicAssemblyRegistrationService : BackgroundService, ITransientDependency
public class DynamicAssemblyRegistrationService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<DynamicAssemblyRegistrationService> _logger;

View file

@ -9,14 +9,14 @@ using Sozsoft.Platform.Identity;
using Sozsoft.Platform.Public;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
using Volo.Abp.Identity;
using Volo.Abp.Users;
namespace Sozsoft.Platform.Extensions;
public class PlatformApplicationConfigurationContributor : IApplicationConfigurationContributor, ITransientDependency
public class PlatformApplicationConfigurationContributor : IApplicationConfigurationContributor
{
public async Task ContributeAsync(ApplicationConfigurationContributorContext context)
{
@ -30,13 +30,13 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
// version.json isteği yoktur). Yalnızca EN SON sürüm taşınır; tam liste
// ChangeLog ekranının kendi endpoint'inden (/api/app/change-log) gelir,
// aksi halde her config isteğinde bütün changelog gönderilirdi.
// Her sayfa açılışında çalışır; yayınlanmış bütün sürüm notları yerine yalnızca en yenisi okunur.
var changeLogRepository = context.ServiceProvider.GetRequiredService<IRepository<ChangeLog, Guid>>();
var changeLogs = await changeLogRepository.GetListAsync(x => x.IsPublished);
var latest = changeLogs
var asyncExecuter = context.ServiceProvider.GetRequiredService<IAsyncQueryableExecuter>();
var latest = await asyncExecuter.FirstOrDefaultAsync((await changeLogRepository.GetQueryableAsync())
.Where(note => note.IsPublished)
.OrderByDescending(note => note.ReleaseDate)
.ThenByDescending(note => note.Version)
.FirstOrDefault();
.ThenByDescending(note => note.Version));
context.ApplicationConfiguration.SetProperty("changeLogs", new
{
@ -85,11 +85,16 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
if (currentUser.Id.HasValue)
{
var sessionRepository = context.ServiceProvider.GetRequiredService<IIdentitySessionRepository>();
var sessions = await sessionRepository.GetListAsync(userId: currentUser.Id.Value);
var claimSessionId = currentUser.FindClaim(PlatformClaimTypes.SessionId.Key)?.Value;
var sessionId = sessions.Exists(session => session.SessionId == claimSessionId)
// Olağan yol token'daki oturumun hâlâ var olmasıdır; kullanıcının bütün oturumları yalnızca
// o bulunamazsa (en son açılanı seçmek için) okunur.
var claimSession = claimSessionId == null ? null : await sessionRepository.FindAsync(claimSessionId);
var sessionId = claimSession?.UserId == currentUser.Id.Value
? claimSessionId
: sessions.OrderByDescending(session => session.SignedIn).FirstOrDefault()?.SessionId;
: (await sessionRepository.GetListAsync(userId: currentUser.Id.Value))
.OrderByDescending(session => session.SignedIn)
.FirstOrDefault()?.SessionId;
context.ApplicationConfiguration.CurrentUser.SessionId = sessionId;
context.ApplicationConfiguration.SetProperty("currentUserSessionId", sessionId);

View file

@ -0,0 +1,106 @@
using System;
using System.Linq;
namespace Sozsoft.Platform;
/// <summary>
/// Uygulamanın tek CORS politikasının origin kararı. Önceden iki ayrı politika (Program.cs'teki
/// "Dynamic" ve modüldeki varsayılan) art arda çalışıyordu; bir origin ikisinden birine uyuyorsa
/// izinlidir, böylece iki kural kümesi de aynen geçerli kalır.
/// </summary>
public sealed class PlatformCorsOriginValidator
{
private static readonly string[] LoopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
private readonly bool _isProduction;
private readonly string? _baseDomain;
private readonly string[] _configuredOrigins;
/// <param name="corsOrigins"><c>App:CorsOrigins</c>: virgülle ayrılmış, <c>https://*.alan.com</c> joker alt alan adı destekli.</param>
public PlatformCorsOriginValidator(bool isProduction, string? baseDomain, string? corsOrigins)
{
_isProduction = isProduction;
_baseDomain = string.IsNullOrWhiteSpace(baseDomain) ? null : baseDomain.Trim().ToLowerInvariant();
_configuredOrigins = (corsOrigins ?? string.Empty)
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(o => o.TrimEnd('/'))
.ToArray();
}
public bool IsAllowed(string origin)
{
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri))
{
return false;
}
return IsAllowedByEnvironmentRule(uri) || IsAllowedByConfiguredOrigins(origin, uri);
}
/// <summary>Eski "Dynamic" politikası: ortama göre ana alan adı, geliştirme makinesi ve listedeki host'lar.</summary>
private bool IsAllowedByEnvironmentRule(Uri uri)
{
var scheme = uri.Scheme.ToLowerInvariant();
var host = uri.Host.ToLowerInvariant();
if (_isProduction)
{
if (scheme != Uri.UriSchemeHttps)
{
return false;
}
if (_baseDomain is not null && (host == _baseDomain || host.EndsWith("." + _baseDomain, StringComparison.Ordinal)))
{
return true;
}
return _configuredOrigins.Any(o =>
Uri.TryCreate(o, UriKind.Absolute, out var allowed)
&& allowed.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
&& allowed.Host.Equals(host, StringComparison.OrdinalIgnoreCase));
}
if ((scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp) && LoopbackHosts.Contains(host))
{
return true;
}
if (_baseDomain is not null && host == $"dev.{_baseDomain}")
{
return true;
}
return _configuredOrigins.Any(o =>
Uri.TryCreate(o, UriKind.Absolute, out var allowed)
&& allowed.Host.Equals(host, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Eski varsayılan politika: <c>WithOrigins(App:CorsOrigins)</c> +
/// <c>SetIsOriginAllowedToAllowWildcardSubdomains</c>. Joker eşleşme ASP.NET Core'un kuralıyla
/// aynıdır: şema ve port aynı, host <c>.alan</c> ile biter.
/// </summary>
private bool IsAllowedByConfiguredOrigins(string origin, Uri uri)
{
foreach (var allowed in _configuredOrigins)
{
if (allowed.Contains("*.", StringComparison.Ordinal))
{
if (Uri.TryCreate(allowed.Replace("*.", string.Empty, StringComparison.Ordinal), UriKind.Absolute, out var domain)
&& uri.Scheme == domain.Scheme
&& uri.Port == domain.Port
&& uri.Host.EndsWith("." + domain.Host, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
else if (allowed.Equals(origin, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}

View file

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -134,7 +133,6 @@ public class PlatformHttpApiHostModule : AbpModule
//Tenant bazında lokalizasyon ayarları için
//TenantLocalization Middleware kaydı
context.Services.AddTransient<TenantLocalizationMiddleware>();
context.Services.AddTransient<TenantLocalizationInitializer>();
ConfigureAuthentication(context);
ConfigureBundles();
@ -258,18 +256,19 @@ public class PlatformHttpApiHostModule : AbpModule
private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration)
{
var originValidator = new PlatformCorsOriginValidator(
context.Services.GetHostingEnvironment().IsProduction(),
configuration["App:BaseDomain"],
configuration["App:CorsOrigins"]);
context.Services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder
.WithOrigins(configuration["App:CorsOrigins"]?
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray() ?? Array.Empty<string>())
.SetIsOriginAllowed(originValidator.IsAllowed)
.WithAbpExposedHeaders()
.WithExposedHeaders(["X-Correlation-Id"])
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
@ -335,9 +334,6 @@ public class PlatformHttpApiHostModule : AbpModule
{
options.KeyPrefix = PlatformConsts.AppName;
});
Configure<RedisCacheOptions>(options =>
{
});
}
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
@ -483,6 +479,10 @@ public class PlatformHttpApiHostModule : AbpModule
// Setup delegate for dynamic service unregistration
DynamicServiceCompiler.NotifyAssemblyUnregistration = DynamicAssemblyRegistrationService.RequestAssemblyUnregistration;
// Preflight'ı ve hata yanıtlarını da kapsasın diye boru hattının en başında; endpoint'e özel
// [EnableCors] kullanılmadığı için routing'i beklemesi gerekmez.
app.UseCors();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
@ -500,7 +500,6 @@ public class PlatformHttpApiHostModule : AbpModule
app.UseResponseCompression();
app.MapAbpStaticAssets();
app.UseRouting();
app.UseCors();
// Not: /api/setup/application-status artık SetupController tarafından karşılanıyor.
// Buradaki kopya middleware kaldırıldı — durum mantığı tek yerde (DatabaseReadinessChecker).

View file

@ -10,7 +10,6 @@ using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Sinks.PostgreSQL;
using Sozsoft.Platform.DbStartup;
using Sozsoft.Platform.DynamicServices;
using Sozsoft.Platform.Enums;
using static Sozsoft.Settings.SettingsConsts;
@ -111,78 +110,9 @@ public class Program
.UseSerilog();
await builder.AddApplicationAsync<PlatformHttpApiHostModule>();
var baseDomain = builder.Configuration["App:BaseDomain"]?.Trim();
var extraOrigins = (builder.Configuration["App:CorsOrigins"] ?? "")
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
builder.Services.AddCors(options =>
{
options.AddPolicy("Dynamic", policy =>
{
policy
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetIsOriginAllowed(origin =>
{
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri))
return false;
var scheme = uri.Scheme.ToLowerInvariant();
var host = uri.Host.ToLowerInvariant();
if (builder.Environment.IsProduction()) //IsProduction
{
if (scheme != "https") return false;
if (!string.IsNullOrWhiteSpace(baseDomain))
{
var bd = baseDomain.ToLowerInvariant();
if (host == bd || host.EndsWith("." + bd))
return true;
}
foreach (var o in extraOrigins)
{
if (Uri.TryCreate(o, UriKind.Absolute, out var eo)
&& eo.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
&& eo.Host.Equals(host, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
else //IsDevelopment
{
if (scheme == "https" && (host == "localhost" || host == "127.0.0.1" || host == "[::1]")
|| (scheme == "http" && (host == "localhost" || host == "127.0.0.1" || host == "[::1]"))
|| (!string.IsNullOrWhiteSpace(baseDomain) && (host == $"dev.{baseDomain.ToLowerInvariant()}")))
return true;
foreach (var o in extraOrigins)
{
if (Uri.TryCreate(o, UriKind.Absolute, out var eo)
&& eo.Host.Equals(host, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
});
});
});
var app = builder.Build();
app.UseDevExpressControls();
app.UseCors("Dynamic");
// Dynamic Assembly Registration Delegate Setup
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
DynamicServiceCompiler.NotifyAssemblyUnregistration = DynamicAssemblyRegistrationService.RequestAssemblyUnregistration;
await app.InitializeApplicationAsync();
await app.RunAsync();
return 0;

View file

@ -27,7 +27,6 @@
<PackageReference Include="Hangfire.PostgreSql" Version="1.20.10" />
<PackageReference Include="Hangfire.SqlServer" Version="1.8.21" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.PostgreSQL" Version="2.3.0" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="8.2.0" />

View file

@ -1,55 +1,93 @@
using System;
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Settings;
using Volo.Abp.Localization;
using Volo.Abp.SettingManagement;
using Volo.Abp.Timing;
namespace Sozsoft.Platform.Localization;
/// <summary>
/// Sürecin varsayılan kültürünü ve <see cref="IClock"/> tipini dil/saat dilimi ayarlarına göre tutar.
///
/// İsteğin kültürü (tarih, sayı, metin) burada belirlenmez: <c>UseAbpRequestLocalization</c> onu her
/// istekte UI'ın gönderdiği <c>Accept-Language</c>'tan kurar. Buradaki değerler istek dışında çalışan
/// iş parçacıkları (Hangfire işleri, worker'lar) içindir ve süreç genelinde tektir; bu yüzden istekteki
/// kiracı ya da kullanıcı değil, global ayar okunur. Aksi hâlde eşzamanlı istekler birbirinin değerini ezerdi.
/// </summary>
public class TenantLocalizationInitializer : ITransientDependency
{
private readonly ISettingProvider _settingProvider;
// Ayar değişikliği yeniden başlatma gerektirmesin ama her istek ayar okumasın.
private static readonly long RefreshIntervalMs = (long)TimeSpan.FromMinutes(1).TotalMilliseconds;
private static long _nextRefreshAt;
private readonly ISettingManager _settingManager;
private readonly IOptions<AbpClockOptions> _clockOptions;
public TenantLocalizationInitializer(
ISettingProvider settingProvider,
ISettingManager settingManager,
IOptions<AbpClockOptions> clockOptions)
{
_settingProvider = settingProvider;
_settingManager = settingManager;
_clockOptions = clockOptions;
}
public async Task ApplyTenantSettingsAsync()
{
// Dil ayarı
var lang = await _settingProvider.GetOrNullAsync("Abp.Localization.DefaultLanguage") ?? "en";
var now = Environment.TickCount64;
var due = Interlocked.Read(ref _nextRefreshAt);
if (now < due || Interlocked.CompareExchange(ref _nextRefreshAt, now + RefreshIntervalMs, due) != due)
{
return;
}
var language = await _settingManager.GetOrNullGlobalAsync(LocalizationSettingNames.DefaultLanguage) ?? "en";
var timeZoneId = await _settingManager.GetOrNullGlobalAsync(TimingSettingNames.TimeZone) ?? "UTC";
ApplyCulture(language);
ApplyClockKind(timeZoneId);
}
private static void ApplyCulture(string language)
{
CultureInfo culture;
try
{
var culture = new CultureInfo(lang);
culture = new CultureInfo(language);
}
catch (CultureNotFoundException)
{
culture = CultureInfo.InvariantCulture;
}
if (CultureInfo.DefaultThreadCurrentCulture?.Name != culture.Name)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
}
catch
{
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
}
// Saat tipi (UTC / Local)
var timeZoneId = await _settingProvider.GetOrNullAsync("Abp.Timing.TimeZone") ?? "UTC";
private void ApplyClockKind(string timeZoneId)
{
DateTimeKind kind;
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
_clockOptions.Value.Kind = tz.Id.Contains("UTC", StringComparison.OrdinalIgnoreCase)
var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
kind = timeZone.Id.Contains("UTC", StringComparison.OrdinalIgnoreCase)
? DateTimeKind.Utc
: DateTimeKind.Local;
}
catch
catch (Exception ex) when (ex is TimeZoneNotFoundException or InvalidTimeZoneException)
{
_clockOptions.Value.Kind = DateTimeKind.Utc;
kind = DateTimeKind.Utc;
}
if (_clockOptions.Value.Kind != kind)
{
_clockOptions.Value.Kind = kind;
}
}
}

View file

@ -93,14 +93,13 @@ public class DynamicDataRepository_Tests : PlatformEntityFrameworkCoreTestBase
}
catch (Exception e) when (e.Message == "Invalid object name 'Test'.")
{
await unitOfWorkManager.Current.RollbackAsync();
// WithUnitOfWorkAsync içinde her zaman açık bir unit of work vardır.
if (unitOfWorkManager.Current is { } unitOfWork)
{
await unitOfWork.RollbackAsync();
}
}
});
//var user = await repo.QueryAsync<string>("select Name from Test where Id = 3", ConnectionString);
//var user2 = await repo.QueryAsync<string>("select Name from Test2 where Id = 3", ConnectionString2);
//user.FirstOrDefault().ShouldBeNull();
//user2.FirstOrDefault().ShouldBeNull();
}
public override void Dispose()

View file

@ -111,7 +111,7 @@ These sections encode expectations the user will **not** repeat in every prompt.
| `api/` kod standardı (sürüm, nullable, ölü kod, modern C#, ABP katmanları) | `.github/instructions/dotnet.instructions.md` |
| Modül/liste ekleme prosedürü, seeder dosya sorumlulukları | `.github/instructions/list.instructions.md` |
| Ne nerede yaşıyor, hangi ekran ne yapıyor | `README.md` |
| Bir modülün nasıl çalıştığı, analizi ve kararları | `configs/seeds/host/docs/modules/{Modül}.md` (dizin: `README.md` §10.5) |
| Bir modülün nasıl çalıştığı, analizi ve kararları | `configs/seeds/docs/modules/{Modül}.md` (dizin: `README.md` §10.5) |
| Yetki kodları | `PlatformConsts.AppCodes` (backend) + `constants/permission.constant.ts` (UI) |
| Seed yolları | `SeedPathResolver` |
@ -184,7 +184,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
Alt form ekranının menü/yetki/düzenleme kuralları (menüsüz alt formun yetkisi ana ekranın altında,
`Form` sekmesi tek kayıt + `AllowAdding`/`AllowUpdating` açık): `lowcode.instructions.md` §4.6.
14. **Modül dokümanı** → MCP ya da wizard ile oluşturulan her modül çalışma şeklini
`configs/seeds/{kapsam}/docs/modules/{Modül}.md` dosyasında taşır: keşif cevapları ve onaylanan
`configs/seeds/docs/modules/{Modül}.md` dosyasında taşır: keşif cevapları ve onaylanan
tarif (analiz), ekranlar, veri modeli, hesaplar/durum geçişleri, yetki dağılımı, entegrasyonlar,
sık sorulanlar. Dosyayı ve "Ek — Ekran envanteri"ni deploy kendisi açar/yeniler; anlatım
bölümlerini (`module-doc:todo`) üreten doldurur — MCP'de `save_module_doc`, Claude Code'da dosyaya
@ -226,7 +226,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
sözleşmesi düzdür, dönüşüm `WizardSeedFileDto.ToSeedJson` / `FromSeedJson` içindedir. **Wizard
seed dosyası her zaman bu iki metotla okunup yazılır**; düz `JsonSerializer` çağrısı bloklu
bölümleri sessizce düşürür.
- **Wizard ile üretilen ekran** → seed dosyası (`configs/seeds/{kapsam}/wizard/{WizardName}.json`)
- **Wizard ile üretilen ekran** → seed dosyası (`configs/seeds/wizard/{WizardName}.json`)
wizard cevaplarının yanında ekranın `GridOptionsEditDto` (`ListForm`) ve alanların
`ColumnFormatEditDto` (`Fields`) sözleşmesini taşır. ListForm detayından yapılan her kayıt
(`ListFormsAppService`, `ListFormFieldsAppService`, `ListFormJsonRowAppService`) bu bölümleri
@ -291,7 +291,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
- **MCP ile demo veri** → geliştirme isteği örnek satır taşıyorsa (prompt'taki tablo/liste, ekran
görüntüsü) ekran `deploy_wizard`'a `SeedData: true` ile gider (`SeedFilePath = data/{ListFormCode}.json`
+ `SeedSync*` açık, `ListFormWizardDto.BuildListForm`) ve satırlar aynı çağrıda `seedRows` ile
`configs/seeds/host/data/{ListFormCode}.json` dosyasına yazılır (`McpDevelopAppService`; alan adları
`configs/seeds/data/{ListFormCode}.json` dosyasına yazılır (`McpDevelopAppService`; alan adları
ekrana göre doğrulanır, `Guid` anahtarı sunucu üretir). Migrate ekranı kurar, ardından satırları
ekler; MCP migrate'i beklemez. Talep döngüsü `Testing`'de biter, `Staging`'e kullanıcı alır. Dosya elle yazılmaz, görselden okunamayan değer uydurulmaz.
`SeedData` wizard ekranında sorulmaz ama düzenlemede korunur (`RefreshFrom` + UI modeli).
@ -340,8 +340,9 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
Administration › Bildirimler altındadır. Ayrı bir manuel gönderim tablosu yoktur: **Bildirimler**
ekranının ekleme formu elle gönderimdir, uç alıcı × kanal açılımını kuyruğa yazar
(`list-form-dynamic-api/notification-insert`). Yeni kiracıya varsayılan tür/kural kümesi
kopyalanır (`NotificationTenantDefaultsSeedContributor`); kaynak host kapsamındaki seed
dosyalarıdır, satırlar yeni kimliklerle yazılır. Kiracı verisine dokunan zamanlanmış işin
gelir: kendi veritabanı olan kiracı seed dosyasından alır, host veritabanını paylaşana
`NotificationTenantDefaultsSeedContributor` aynı dosyalardan yeni kimliklerle kopyalar; iki yol
aynı kiracıda çalışmaz. Kiracı verisine dokunan zamanlanmış işin
`Scope` alanı `Tenant` ya da `Host ve Kiracılar` olmalıdır — `Host` kapsamında çalışan bir iş
kiracı satırlarını göremez (`WorkerScopeEnum`, `PlatformBackgroundWorker`).
- **Bildirim bayrakları** → sıra değişmez: `IsSent` (iletildi) olmadan `IsRead` yazılmaz, `IsRead`
@ -365,7 +366,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
yazıları) `SeoAppService` içindedir; yeni bir public kaynak türü eklenirse toplayıcıya da
eklenir, yoksa sayfa sitemap'te görünmez. Yeni bir meta alanı = iki entity + `SeoRouteDto` +
`SeoFields` (seeder) + `SeoHead` birlikte.
- **Yeni bir dil anahtarı** → `configs/seeds/host/data/App.Languages.LanguageText.json` (EN + TR iki satır;
- **Yeni bir dil anahtarı** → `configs/seeds/data/App.Languages.LanguageText.json` (EN + TR iki satır;
`Id` = `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}` adından URL isim alanlı v5 GUID); eklemeden önce anahtarın zaten var olup
olmadığını kontrol et.
- **Custom component sınıfları** → bileşen kodu veritabanında yaşar, Tailwind derlemede yalnızca
@ -419,8 +420,9 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
olmasıdır: tabloya dair bir bulgu üretirken `TableName` (+ tablo hedefte yoksa `TableMissing`)
doldurulur, istemci tablo tasarımcısını buna göre açar.
- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete`
bayraklarından işaretli olan işlemler kapsam klasöründeki hedefe yansır
(`ListFormSeedDataSynchronizer`). Hedef iki biçimdedir ve ayrım uzantıdadır: `.json` ile biten
bayraklarından işaretli olan işlemler seed klasöründeki hedefe yansır
(`ListFormSeedDataSynchronizer`); yalnızca host bağlamındaki kayıt yansır, kiracının kaydı
veritabanında kalır. Hedef iki biçimdedir ve ayrım uzantıdadır: `.json` ile biten
değer **tek dosyadır** (`data/{ListFormCode}.json`, bütün satırlar orada), uzantısız değer
**klasördür** (`data/{ListFormCode}/{anahtar}.json`, her satır kendi dosyasında) ve klasör yoksa
ilk kayıtta oluşturulur. Klasör modu satırı büyük ya da sık yazılan ekranlar içindir
@ -434,6 +436,21 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
eklenir, varsa dokunulmaz; import'ta `overwriteExisting` açıktır: var olan kayıt dosyadaki
değerlerle güncellenir ve soft delete izleri (`IsDeleted`/`DeletionTime`/`DeleterId`) temizlenir. Satırı veritabanından okuyan senkronizasyon açık UoW varsa commit sonrasında kendi UoW'sinde çalışır (EF ile yazan çağıran, ör. `MenuAppService`, aksi hâlde eski değeri dosyaya yazardı); yalnızca dosyaya dokunan `RemoveRowsByFieldAsync` hemen çalışır. Yeni bir kayıt yolu (yeni endpoint, toplu işlem, tipli AppService) eklenirse
senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar.
- **Seed kapsamı** → `configs/seeds/` tek klasördür; `host/` ya da `tenants/{tenantId}/` yoktur.
Host ve kendi veritabanı olan her tenant aynı dosyalarla kurulur, host veritabanını paylaşan
tenant'ta dosyalar yeniden uygulanmaz (`SeedFileScope`; migrate kararı `DataSeedContext`'e yazar).
Dosyayı okuyan yeni bir seeder `SeedFileScope.IsEnabled` ile başlar. Dosyalar yalnızca host
bağlamında yazılır; dosyaya yazan yeni bir yol tenant bağlamında yazmaz. Bekleyen wizard kurulumu
(`Pending`) dosyaları uygulayan son veritabanında kapanır (`SeedFileScope.CompletesPending`).
Tek istisna `{sql|postgres}/host/{object,execute}`: yalnızca host'ta çalışır; tenant veritabanında
karşılığı olmayan nesneye (HangFire tabloları) dayanan script buraya konur, tablo önekine bakılmaz.
Kendi veritabanı olan tenant'ta EF ile yalnızca `IMultiTenant` varlıklar o veritabanına gider;
ListForm, menü ve yetki tanımını ABP her zaman host'ta okur. Kiracı tablosunun satırları kiracı
adıyla kayıtlı `DataSource` üzerinden gider. Kiracı yalnızca kimliğiyle değiştirildiğinde ad boş
kalır; bu durumda adı `DataSourceManager` kiracı kaydından okur. Yeni tenant yöneticisi ilk
seed'de kiracı tarafı bütün yetkileri alır (`PlatformIdentityDataSeeder`). Bunun nedeni,
DbMigrator'da dinamik yetki deposunun kapalı olmasıdır: ABP'nin yetki seed'i seed dosyasından
gelen yetkileri göremez.
- **Seed dosyası yazmak** → veri dosyası `ListFormSeedDataFileDto.Serialize`, wizard dosyası
`WizardSeedFileDto.ToSeedJson` ile yazılır; düz `JsonSerializer.Serialize` + `File.WriteAllText`
kullanılmaz. System.Text.Json emojiyi her zaman vekil çift kaçışına çevirir; dosya baştan
@ -453,7 +470,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
satırları + route `Authority`. Yetki `Modules` değeri: Saas/Administration `Sas|Adm`, İK `Sas|Hr`
(`Sas` host, `Adm`/`Hr` kiracı görünürlüğü). İK departman/pozisyon `BranchId` zorunludur; seed
satırı host şubesine (`PlatformConsts.Branches.BranchIdDefaultValue`) bağlanır.
- **Menü ağacı ve yetkiler** → `configs/seeds/host/data/App.Menus.{Routes,Module,Menu}.json` ve
- **Menü ağacı ve yetkiler** → `configs/seeds/data/App.Menus.{Routes,Module,Menu}.json` ve
`Abp.Identity.Permission{Groups,s}.json`; platformun kendi menü/yetki kayıtları da diğer liste
verisiyle aynı mekanizmadan (`ListFormSeedDataApplier`) geçer, ayrı bir seeder yoktur. `Order`
alanı bağımlılık sırasını kurar: route → modül → menü, yetki grubu → yetki, ülke grubu → ülke →
@ -497,7 +514,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
tipler düz metin aramasında referanssız görünür.
- Yorum satırına alınmış kod bloğu bırakılmaz.
- Modül/ekran/bileşen ekleyen ya da davranışını değiştiren iş modül dokümanını
(`configs/seeds/{kapsam}/docs/modules/{Modül}.md`) yazar veya günceller; liste ekranının açıklaması
(`configs/seeds/docs/modules/{Modül}.md`) yazar veya günceller; liste ekranının açıklaması
ayrıntılıdır (standing default 14–15).
- `api/` değişikliğinden sonra: `dotnet build -p:EnforceCodeStyleInBuild=true` — 0 hata, yeni
`IDE00xx` yok.

Some files were not shown because too many files have changed in this diff Show more