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/` | | `dotnet.instructions.md` | Binding code standard for everything under `api/` |
| `list.instructions.md` | Step-by-step procedure for adding a module / list through seeders | | `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 | | `../../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 | | `../../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: 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 - 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 - WizardDataSeeder.cs / CrudDataSeeder.cs — the seeders that read
surface B below surface B below
- ListFormSeedDataApplier, CrudEndpointSeedApplier (Domain/Data/) — the single home of the apply - 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 **B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through
`SeedPathResolver`. The Wizard, the Custom Components screen / Visual Designer, the CRUD `SeedPathResolver`. The Wizard, the Custom Components screen / Visual Designer, the CRUD
Endpoint Manager and every ListForm screen with a `SeedFilePath` write here; the 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/{wizard,data,crud,docs,sql/{object,execute},postgres/{object,execute}}
configs/seeds/tenants/{tenantId}/… same layout, tenant scope 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) 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 ### 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: - `App.Menus.Routes.json` row:
- `Id`, `Key`, `Path`, `ComponentPath`, `RouteType`, `Authority` - `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 ### 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 group (if missing)
- Permission definitions for feature root and actions - 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 ### 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) - Sender credentials and endpoints (sms/mail/whatsapp/rocket)
- AiBot integration endpoint and activation state - 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 ### 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 Exporting a wizard produces a zip whose paths are relative to the seed folder and which carries
(`host` / `tenants/{tenantId}`) and which carries every dependency: every dependency:
The closure is **recursive** (`WizardExportCollector`): a sub-form's wizard file is queued and its 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. 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`) - Table names: `{Modul}_T_{Entity}` (e.g., `Mrp_T_Order`)
- Menu/permission keys: `App.{Modul}`, `App.{Modul}.{Liste}` - Menu/permission keys: `App.{Modul}`, `App.{Modul}.{Liste}`
- **Tenant Isolation:** All data, forms, and permissions must be tenant-aware. - **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. - **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. - **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`. - **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 ## 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. - 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. - 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. - 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 ## 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`. - **{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}. - **{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}`. - **{Modul} permissions** grouped under `App.{Modul}`.
- **{Liste} permissions** nested under {Modul} group. - **{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}) ### 1. Add a New Module (e.g., {Modul})
``` ```
Yeni bir modül ekle (ör: {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/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/Abp.Identity.PermissionGroups.json'da ayrı bir grup oluştur
- ListFormSeeder_{Modul}.cs dosyası oluştur ve list-form seed'ini buraya taşı - ListFormSeeder_{Modul}.cs dosyası oluştur ve list-form seed'ini buraya taşı
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak adlandır - 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 ### 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 - {Modul} ana menüsünün altına ekle
- ListFormSeeder_{Modul}.cs dosyasına seed kodunu ekle - ListFormSeeder_{Modul}.cs dosyasına seed kodunu ekle
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak oluştur - 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/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/App.Languages.LanguageText.json dosyasına liste ve alan çevirilerini ekle
``` ```
### 3. Enforce Tenant Isolation and Full Audit ### 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 ### 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}
- App.{Modul}.{Liste} - App.{Modul}.{Liste}
- App.Listform.ListformField.{Alan} - App.Listform.ListformField.{Alan}

View file

@ -13,7 +13,7 @@ sorusunda buraya bak.
**Kaynak:** Buradaki her tablo `api/src/Sozsoft.Platform.Application.Contracts/ListForms/**` **Kaynak:** Buradaki her tablo `api/src/Sozsoft.Platform.Application.Contracts/ListForms/**`
(DTO), `api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListForm{,Field}.cs` (entity), (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 `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. Çelişki hâlinde **kod esastır**; bu dosya güncellenir.
**Ortak kurallar** **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 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. 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 ### 2.6 Filtre, arama ve gruplama panelleri
**`FilterRowJson`** (`GridFilterRowDto`): `Visible` (`false`), `ApplyFilter` (`auto` \| `onClick`), **`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 › 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 › 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. | | **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ğı. | | **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. | | **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. | | **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. | | **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. | | **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. | | **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 › 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 › 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. | | **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 › 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 › 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 › 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`. | | **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 > 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 › 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 › 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 › 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 › 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 › 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. | | **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 › 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 › 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. | | **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). | | **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/host/docs/modules/HrLeave.md`. Wizard + Custom Component + küçük UI/motor eki. | | **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/host/docs/modules/HrAttendance.md`. Wizard + Custom Component + Custom Endpoint + zamanlanmış iş + küçük UI 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. | | **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 › 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. | | **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 **Ş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 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 sütunu veridir). Host varsayılan şubesinin kimliği sabittir
(`PlatformConsts.Branches.BranchIdDefaultValue`; `PlatformIdentityDataSeeder` / (`PlatformConsts.Branches.BranchIdDefaultValue`; `PlatformIdentityDataSeeder` host şubesi için bu
`TenantIdentityDataSeeder` host şubesi için bu kimliği kullanır, kiracı şubeleri yeni GUID alır); 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ı seed edilen departman ve pozisyon satırları `BranchId` ile bu şubeye bağlanır. Yeni seed satırı
eklerken `BranchId` de yazılır. eklerken `BranchId` de yazılır.
### 12.5 ListForm olmayan (özel React) ekranlar ### 12.5 ListForm olmayan (özel React) ekranlar
Bu ekranlar `Sas_H_Route` kayıtlarıyla yönetilir (seed: 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 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. ç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 | | "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 | | "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 | | "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`) | | "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/host/docs/modules/HrLeave.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/host/docs/modules/HrAttendance.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 | | "yapay zekâ cevaplasın" | Saas › Definitions › AiBot | Bot tanımı; `/admin/ai` bu tanımı kullanır |
### 12.7 Canlı referans uygulamalar ### 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** 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 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: 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 ### 0.1 Yazılacak klasörler
``` ```
configs/seeds/host/ configs/seeds/
├── sql/object/{Nesne}.sql ← tablo / view / fonksiyon / prosedür (SQL Server) ├── sql/object/{Nesne}.sql ← tablo / view / fonksiyon / prosedür (SQL Server)
├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon) ├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon)
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise) ├── 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ı ├── crud/{EntityName}.json ← tablonun REST uçları
├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları (List yolunda sunucu yazar, §4.1) ├── 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) ├── 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 │ 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 ### 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ı, 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 sosyal hesaplar, varsayılan anahtar kelimeler (`DefaultKeywords`), sitemap anahtarı (`SitemapEnabled`) ve
sitemap dışı yollar (`SitemapExcludePaths`) `App.SiteManagement.Seo.*` ayarlarındadır 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 oraya yazılır, her yola tekrarlanmaz. Site kökü (`/`) hiçbir kayıt taşımaz; doğrudan bu
varsayılanlardan beslenir. 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. içerikli satır dosyada varsa (tekrar deploy) yeniden eklenmez.
2. DB Migrate'i bekleme. Migrate wizard seeder ile ekranı kurar, ardından `ListFormSeedDataApplier` 2. DB Migrate'i bekleme. Migrate wizard seeder ile ekranı kurar, ardından `ListFormSeedDataApplier`
dosyadaki satırları ekler (anahtarı olmayanı ekler, var olana dokunmaz). 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 3. Migrate'ten sonra host bağlamında ekrandan girilen satırlar `ListFormSeedDataSynchronizer` ile aynı
(kiracı bağlamında `tenants/{TenantId}/data/`). dosyaya yazılır; kiracı bağlamındaki kayıt dosyaya yansımaz.
Kurallar: Kurallar:
- Dosyayı elle yazma; `seedRows` alan adlarını ekranın alanlarına göre doğrular, bilinmeyen alanı reddeder. - 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 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. 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 Ü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`). 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. 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. 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 - **Yer:** seed klasörü — `configs/seeds/docs/modules/{Modül}.md`. Sunucu yalnızca seed köküne
`tenants/{tenantId}/docs/modules`). Sunucu yalnızca seed köküne yazabildiği için depo kökünde yazabildiği için depo kökünde değildir. Wizard export zip'i dokümanı taşır, import yazar (`docs/modules/*.md`).
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): - **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: 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 `<!-- 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` | | 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` | | 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 Seed klasörü tektir: `configs/seeds/…`. Çözüm her zaman `SeedPathResolver` üzerinden yapılır;
`configs/seeds/tenants/{tenantId}/…`. Çözüm her zaman `SeedPathResolver` üzerinden yapılır;
yol elle kurulmaz. 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ı) ## 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` Okuyan: `WizardDataSeeder` (migrate) · Yazan: `ListFormWizardAppService` (deploy) ve `WizardSeedSynchronizer`
(ListForm editöründen kayıt) · Okuma/yazma her zaman `WizardSeedFileDto.FromSeedJson` / (ListForm editöründen kayıt) · Okuma/yazma her zaman `WizardSeedFileDto.FromSeedJson` /
`ToSeedJson` ile — düz `JsonSerializer` bloklu bölümleri düşürür. `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 `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). `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 **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. 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 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ı 6. Deploy + tamamlandı
Başarıda sihirbaz "Migrate bekliyor" bildirimiyle Wizard Manager'a döner (ekran henüz kurulu 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, 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, 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). 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ı ### 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ı 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 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. 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 1. Üretilen SQL hedef veri kaynağında çalıştırılır (yeni tabloda `CREATE`, düzenleme modunda
**`ALTER` diff'i**). **`ALTER` diff'i**).
2. Seed dosyası kaydedilir: her zaman **tam `CREATE TABLE` script'i** (diff değil), 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. 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 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. **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 > **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 > 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. > `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; > 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. > 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: dosyalardan kurulur. Mekanik:
1. **Yalnızca aktif sağlayıcının klasörü çalışır.** `DefaultDatabaseProvider` SQL Server ise 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. 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 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` 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ı ### 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) (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 ```jsonc
{ {
"EntityName": "Mrp_T_Order", "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 `App.DeveloperKit.CustomEndpoints{,.Create,.Update,.Delete}` yetkileridir — çağırma
yetkilerinden (`.Get/.Post/.Put/.Remove` + `PermissionsJson`) ayrıdır. 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 `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 dosyaya yansır, migrate'te eksikse geri eklenir. Prompt ile üretimde satırı bu dosyaya yaz
(`Id` sabit GUID; `ParametersJson`/`PermissionsJson` kaçışlanmış string). (`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 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 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 (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ı) — 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. 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: üretilecek dosya seti:
``` ```
configs/seeds/host/ configs/seeds/
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…) ├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir ├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
├── data/App.DeveloperKit.CustomComponents/{Id}.json ← yalnızca serbest yerleşim isteniyorsa ├── 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 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). onayda seçilirse (MCP: `dropTables: true`) scripti ve crud dosyasıyla düşer (§4.5).
Dosyayı diskten silmek kayıtları temizlemez. 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 ekranlar, hesaplar ve yetkiler üretilen artefaktların gerçek adlarıyla yazılı, README §10.5
dizininde satırı var (§0.13). dizininde satırı var (§0.13).
Prompt ile üretim yaptıysan ek olarak: 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. - [ ] Ü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 - [ ] Tablo/kolon adları uydurulmadı; ya var olan nesneden okundu ya da `sql/object/` altında
üretildi. üretildi.

214
README.md
View file

@ -155,7 +155,6 @@ sozsoft-platform/
│ │ ├── Sozsoft.Platform.Application/ # Uygulama servisleri (iş mantığı) │ │ ├── Sozsoft.Platform.Application/ # Uygulama servisleri (iş mantığı)
│ │ ├── Sozsoft.Platform.EntityFrameworkCore/ # DbContext, migration'lar, dinamik veri katmanı │ │ ├── Sozsoft.Platform.EntityFrameworkCore/ # DbContext, migration'lar, dinamik veri katmanı
│ │ ├── Sozsoft.Platform.HttpApi/ # Controller'lar (conventional) │ │ ├── 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.HttpApi.Host/ # Host: Swagger, auth, Hangfire, SignalR, raporlama
│ │ └── Sozsoft.Platform.DbMigrator/ # Migration + seed uygulaması │ │ └── Sozsoft.Platform.DbMigrator/ # Migration + seed uygulaması
│ ├── modules/ # Bağımsız ABP modülleri │ ├── modules/ # Bağımsız ABP modülleri
@ -179,9 +178,8 @@ sozsoft-platform/
├── configs/ ├── configs/
│ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler │ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler
│ ├── deployment/ # Dev/Prod compose dosyaları, nginx/redis/turn konfigürasyonu, deploy scriptleri │ ├── 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 │ ├── seeds/ # Runtime'da düzenlenebilen seed'ler; host ve tenant'lar için tek klasör
│ │ # host/{crud,custom,wizard,sql,postgres,docs/modules} │ │ # {crud,wizard,data,docs/modules}
│ │ # tenants/{tenantId}/{crud,custom,wizard,sql,postgres}
│ │ # {sql|postgres}/{object,execute} │ │ # {sql|postgres}/{object,execute}
│ └── ai/ # AI entegrasyonu (n8n akış tanımları, Ollama model kurulum notları) │ └── ai/ # AI entegrasyonu (n8n akış tanımları, Ollama model kurulum notları)
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları) ├── .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/`, (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`). 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 **Tek tenant seed'i:** Tenants ekranındaki bağlantı diyaloğunun Seed sekmesi
veritabanı için `host/`, her tenant için `tenants/{tenantId}/`. Kapsamın altında içerik `POST /api/setup/tenants/{id}/seed` ucunu çağırır (`AbpTenantManagement.Tenants.Update`, yalnızca host
türüne göre `crud/`, `wizard/`, `data/`, `sql/`, `postgres/` klasörleri bulunur: 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/ configs/seeds/
host/ crud/ wizard/ data/ docs/
crud/ wizard/ data/ sql/ { object/ execute/ host/{ object/ execute/ } }
sql/ { object/ execute/ } postgres/ { object/ execute/ host/{ object/ execute/ } }
postgres/ { object/ execute/ }
tenants/
{tenantId}/
crud/ wizard/ data/
sql/ { object/ execute/ }
postgres/ { 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 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: `.sql` dosyası amacını belirten bir klasörün altındadır; klasör köklerinde dosya bulunmaz:
| Klasör | Ne zaman çalışır | | 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). | | `{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. | | `{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. |
| `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. | | `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. | | `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, 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ı `PlatformIdentityDataSeeder` ile belirlenir). Giriş sonrası
`/admin/dashboard` açılır. `/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) ## 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: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: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:RedirectAllowedUrls` | OAuth yönlendirme için izinli adresler. |
| `App:CdnUrl` / `App:CdnPath` | Dosya servisinin adresi ve BLOB kök dizini. | | `App:CdnUrl` / `App:CdnPath` | Dosya servisinin adresi ve BLOB kök dizini. |
| `App:AttachmentsPath` | Mail kuyruğu eklerinin yazıldığı dizin. | | `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ı. | | `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/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. | | `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. | | `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). | | `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. | | `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`) 5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`)
6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR) 6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR)
7. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları 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ı 9. **Doğrulama ve geri alma** notları
**Adım bileşenleri.** Adımlar numaralı dosya adları yerine işlevleriyle adlandırılır ve **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)** **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 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 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
aynı klasör düzenine doğrudan açılabilir. açılabilir. Eski arşivlerdeki `host/` öneki import'ta yok sayılır.
Toplama **özyinelemelidir** (`WizardExportCollector`): alt formların wizard dosyaları kuyruğa 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. 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/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) | | `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ı | | `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 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 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ı. | | **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. | | **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. | | **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`). | | **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. | | **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}`. | | **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 > 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 > `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. > `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. > 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ı > **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. 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: **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ş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 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ı 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` **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 `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 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. kendi `{Id}.json` satır dosyası olarak başlar.
**Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler → **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ı: 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 + `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 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, **İ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; **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; 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 **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)). `Abp.Identity.PermissionGroups.json` — `App.Saas` (1), `App.Administration` (2), `App.Hr` (3)).
Ana yetki kökleri: `App.Saas`, `App.Branches`, Ana yetki kökleri: `App.Saas`, `App.Branches`,
`App.Definitions.*`, `App.Hr.*` (Intranet dahil, `App.Hr.Intranet.*`), `App.Restrictions.*`, `App.Languages.*`, `App.Listforms.*`, `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 | | 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.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.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. | | **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ı. | | 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. | | 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. | | 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ı. | | 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). | | 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ü. | | 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 `[Authorize]` olduğu için menüler uygulama konfigürasyonunun `publicMenus` alanıyla taşınır
(`PlatformApplicationConfigurationContributor`), istemcide `usePublicMenu` okur. `Pub` modülü (`PlatformApplicationConfigurationContributor`), istemcide `usePublicMenu` okur. `Pub` modülü
yönetim navigasyonuna girmez (`abpConfig.model.ts` menüyü kurarken bu modülü eler; Menü Yönetimi 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`. `App.Menus.Menu.json`.
**Sayfa.** Her public sayfa bir Custom Component'tir: `RoutePath` `/admin/` ile başlamadığı sürece **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 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ç 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 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). 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, 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` `App.{Home,About,Services,Contact}` yetkileri ve `.Design` alt yetkileri, `/admin/public/*/designer`
rota ve menüleri. 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; 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 "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 | | Modül | Menü | Doküman |
| --- | --- | --- | | --- | --- | --- |
| Performans Yönetimi | İK › Performans Yönetimi (`App.Hr.Performance`) | [HrPerformance.md](configs/seeds/host/docs/modules/HrPerformance.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/host/docs/modules/HrRecruitment.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/host/docs/modules/HrLeave.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/host/docs/modules/HrAttendance.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/host/docs/modules/HrTrainings.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/host/docs/modules/HrVehicles.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/host/docs/modules/HrDisciplinaryCase.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/host/docs/modules/HrDefinitions.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/host/docs/modules/HrOrganization.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/host/docs/modules/HrWorkCalendar.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/host/docs/modules/HrHealthCard.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/host/docs/modules/HrVisitor.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/host/docs/modules/HrEventLog.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/host/docs/modules/HrPersonnel.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/host/docs/modules/HrIntranet.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 - **Ş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. `BranchUsers` ile tutulur, lisans limiti girişte kontrol edilir.
- **Organizasyon birimi:** ABP `OrganizationUnit` ağacı; ListForm'lar OU bazlı filtrelenebilir. - **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`, 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 `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ı 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 oluşturulduğunda varsayılan tür ve kural kümesi o kiracıya kopyalanır. Kendi veritabanı olan
(`NotificationTenantDefaultsSeedContributor`): kaynak host kapsamındaki 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 `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; 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 **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. 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ı. | | `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. |
| `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. | | `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. |
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`, ya da başlık metniyle). Bölüm önce istenen, sonra diğer belgede aranır; başlıkla eşleşmeyen terimde (örn. `dxImageUpload`) terimin geçtiği bölümler (en çok 15, `DevGuideSearchMatches`), hiç geçmiyorsa başlık listesi döner — hata atılmaz. Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. | | `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`, ya da başlık metniyle). Bölüm önce istenen, sonra diğer belgede aranır; başlıkla eşleşmeyen terimde (örn. `dxImageUpload`) terimin geçtiği bölümler (en çok 15, `DevGuideSearchMatches`), hiç geçmiyorsa başlık listesi döner — hata atılmaz. Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. |
| `create_table` | Yeni 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. | | `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | | `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) 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). | | `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. | | `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. | | `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. | | `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). | | `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`): **Timesheets.** Talebin `Timesheets` listesini sunucu doldurur (`IMcpDevRequestTimesheetRecorder`):
@ -1858,17 +1889,20 @@ konfigürasyonu ile üretilir.
### 18.4. PWA ve sürüm yönetimi ### 18.4. PWA ve sürüm yönetimi
- Kurulum iki kademelidir: **kabuk** (index.html + entry JS/CSS + eager vendor chunk'ları) - Service worker kurulumu yalnızca **kabuğu** indirir (index.html + entry JS/CSS + eager vendor
workbox precache manifest'inden gelir ve zorunludur; **bileşenler** (tüm lazy chunk'lar, chunk'ları, workbox precache manifest'i); kabuk zorunludur. **Bileşen ısıtması** kurulumdan
build'de üretilen `dist/warmup.json`) güncelleme penceresinde aynı ilerleme çubuğunda arka ayrıdır ve yalnızca oturum açık kullanıcı içindir: store, application-configuration her
planda indirilir. İkinci kademe best-effort'tur (inemeyen dosya kurulumu düşürmez, süre geldiğinde `protected` fiziksel rotalardan kullanıcının yetkili olduklarını
bütçesi aşılırsa kalanlar runtime cache'e kalır); böylece deploy sonrası ilk gezinmede chunk (`PermissionGuard` ile aynı ölçüt) `setWarmupRoutes` ile bildirir, uygulama `ENSURE_WARMUP`
beklenmez ama yavaş bağlantıda kurulum timeout'a düşmez. DevExtreme temaları (~33 MB), font mesajıyla bu rotaları worker'a yollar. Worker build'de üretilen `dist/warmup.json`'daki
ve görseller runtime cache'tedir. `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 - `registerType: 'prompt'` — yeni sürüm indirilir, indirme bitince otomatik devreye alınır
(`views/version/swRegistration.ts`, `sw.ts`, `AppUpdateDialog.tsx`). (`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 - **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 numarasını ve son sürümün notlarını `application-configuration` yanıtındaki
`extraProperties.changeLogs` alanından okur (`PlatformApplicationConfigurationContributor`), `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. İ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/`. **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: 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ü - `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 **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 Designer, CRUD Endpoint Manager ve `SeedFilePath` tanımlı ListForm ekranları çalışma zamanında
burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar 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/ configs/seeds/
├── host/ # host kapsamı ├── wizard/ # {Ad}.json → WizardDataSeeder
│ ├── wizard/ # {Ad}.json → WizardDataSeeder ├── crud/ # {EntityName}.json → CrudEndpointSeedApplier
│ ├── crud/ # {EntityName}.json → CrudEndpointSeedApplier ├── docs/modules/ # {Modül}.md → ModuleDocManager (seed edilmez)
│ ├── data/ # {ListFormCode}.json ya da {ListFormCode}/{anahtar}.json → ListFormSeedDataApplier ├── data/ # {ListFormCode}.json ya da {ListFormCode}/{anahtar}.json → ListFormSeedDataApplier
│ │ # App.Menus.{Routes,Module,Menu}.json — route/modül/menü ağacı │ # App.Menus.{Routes,Module,Menu}.json — route/modül/menü ağacı
│ │ # Abp.Identity.Permission{Groups,s}.json — yetki tanımları │ # Abp.Identity.Permission{Groups,s}.json — yetki tanımları
│ │ # App.Definitions.{CountryGroup,Country,City,District}.json — adres zinciri │ # 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.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.{SkillType,SkillLevel,Skill}.json — İK yetkinlik tanımları
│ │ # App.Hr.{Department,JobPosition}.json — İK departman → pozisyon (BranchId ile şubeye bağlı) │ # 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.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.{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.Hr.PerformanceRating.json — İK › Tanımlar › Performans Dereceleri (5 kademeli ölçek)
│ │ # App.Notifications.Notification{Templates,Types,Rules}.json — bildirim şablonu, tipi → kuralı │ # App.Notifications.Notification{Templates,Types,Rules}.json — bildirim şablonu, tipi → kuralı
│ │ # App.Languages.{Language,LanguageText}.json — dil → metin (eksik LanguageKey metinle üretilir) │ # App.Languages.{Language,LanguageText}.json — dil → metin (eksik LanguageKey metinle üretilir)
│ │ # App.SettingDefinitions.json, App.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler │ # App.SettingDefinitions.json, App.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler
│ ├── sql/{object,execute}/ # .sql (SQL Server) ├── sql/{object,execute}/ # .sql (SQL Server)
│ └── postgres/{object,execute}/ │ └── host/{object,execute}/ # yalnızca host veritabanında çalışır
├── tenants/{tenantId}/… # aynı düzen, tenant kapsamı ├── postgres/{object,execute}/
│ └── host/{object,execute}/
└── .imports/{importId}/ # wizard import staging + backup (seeder taramalarına girmez) └── .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.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.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/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.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 "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/" 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.HttpApi", "src\Sozsoft.Platform.HttpApi\Sozsoft.Platform.HttpApi.csproj", "{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.HttpApi", "src\Sozsoft.Platform.HttpApi\Sozsoft.Platform.HttpApi.csproj", "{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sozsoft.Platform.DbMigrator", "src\Sozsoft.Platform.DbMigrator\Sozsoft.Platform.DbMigrator.csproj", "{AA94D832-1CCC-4715-95A9-A483F23A1A5D}"
EndProject 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}" 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|x64.Build.0 = Release|Any CPU
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x86.ActiveCfg = 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 {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.ActiveCfg = Debug|Any CPU
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.Build.0 = 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 {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} {42F719ED-8413-4895-B5B4-5AB56079BC66} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{520659C8-C734-4298-A3DA-B539DB9DFC0B} = {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} {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} {AA94D832-1CCC-4715-95A9-A483F23A1A5D} = {CA9AC87F-097E-4F15-8393-4BC07735A5B0}
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D} = {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} {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"; 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() public static string[] GetAll()
{ {
return ReflectionHelper.GetPublicConstantsRecursively(typeof(LanguagesPermissions)); return ReflectionHelper.GetPublicConstantsRecursively(typeof(LanguagesPermissions));

View file

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

View file

@ -2,8 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Languages.Entities; using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Localization; using Sozsoft.Languages.Localization;
using Sozsoft.Languages.Permissions;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
@ -11,6 +13,7 @@ using Volo.Abp.Validation;
namespace Sozsoft.Languages; namespace Sozsoft.Languages;
[Authorize]
public class LanguageKeyAppService : CrudAppService< public class LanguageKeyAppService : CrudAppService<
LanguageKey, LanguageKey,
LanguageKeyDto, LanguageKeyDto,
@ -26,6 +29,11 @@ public class LanguageKeyAppService : CrudAppService<
{ {
LocalizationResource = typeof(LanguagesResource); LocalizationResource = typeof(LanguagesResource);
ObjectMapperContext = typeof(LanguagesApplicationModule); 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; _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;
using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Sozsoft.Languages.Caching;
using Sozsoft.Languages.Entities; using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Localization; using Sozsoft.Languages.Localization;
using StackExchange.Redis; using Sozsoft.Languages.Permissions;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
@ -19,6 +20,11 @@ using Volo.Abp.Validation;
namespace Sozsoft.Languages; 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< public class LanguageTextAppService : CrudAppService<
LanguageText, LanguageText,
LanguageTextDto, LanguageTextDto,
@ -29,20 +35,25 @@ public class LanguageTextAppService : CrudAppService<
{ {
private readonly IRepository<LanguageText, Guid> _repositoryText; private readonly IRepository<LanguageText, Guid> _repositoryText;
private readonly IRepository<LanguageKey, Guid> _repositoryKey; private readonly IRepository<LanguageKey, Guid> _repositoryKey;
private readonly IConfiguration _configuration; private readonly DistributedCacheCleaner _cacheCleaner;
public LanguageTextAppService( public LanguageTextAppService(
IRepository<LanguageText, Guid> repositoryText, IRepository<LanguageText, Guid> repositoryText,
IRepository<LanguageKey, Guid> repositoryKey, IRepository<LanguageKey, Guid> repositoryKey,
IConfiguration configuration DistributedCacheCleaner cacheCleaner
) )
: base(repositoryText) : base(repositoryText)
{ {
LocalizationResource = typeof(LanguagesResource); LocalizationResource = typeof(LanguagesResource);
ObjectMapperContext = typeof(LanguagesApplicationModule); 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; _repositoryText = repositoryText;
_repositoryKey = repositoryKey; _repositoryKey = repositoryKey;
_configuration = configuration; _cacheCleaner = cacheCleaner;
} }
public override async Task<LanguageTextDto> CreateAsync(LanguageTextCreateUpdateDto input) public override async Task<LanguageTextDto> CreateAsync(LanguageTextCreateUpdateDto input)
@ -63,6 +74,7 @@ public class LanguageTextAppService : CrudAppService<
return await base.CreateAsync(input); return await base.CreateAsync(input);
} }
[Authorize(LanguagesPermissions.LanguageText.Update)]
public async Task UpdateLanguageTextAsync(LanguageTextCreateUpdateDto input) public async Task UpdateLanguageTextAsync(LanguageTextCreateUpdateDto input)
{ {
var entity = await _repositoryText.GetAsync( 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) public async Task<PagedResultDto<LanguageTextTranslatedDto>> GetLanguageTextTranslatedAsync(LanguageTextTranslatedRequestDto input)
{ {
var query = await _repositoryKey.GetQueryableAsync(); var query = await _repositoryKey.GetQueryableAsync();
@ -109,6 +122,7 @@ public class LanguageTextAppService : CrudAppService<
return new PagedResultDto<LanguageTextTranslatedDto>(totalCount, dtoLanguageTextTranslate); return new PagedResultDto<LanguageTextTranslatedDto>(totalCount, dtoLanguageTextTranslate);
} }
[Authorize(LanguagesPermissions.LanguageText.Default)]
public async Task<LanguageTextDto> GetLanguageTextAsync(LanguageTextFilteredDto input) public async Task<LanguageTextDto> GetLanguageTextAsync(LanguageTextFilteredDto input)
{ {
var entity = await _repositoryText.FirstOrDefaultAsync(a => var entity = await _repositoryText.FirstOrDefaultAsync(a =>
@ -119,6 +133,7 @@ public class LanguageTextAppService : CrudAppService<
return ObjectMapper.Map<LanguageText, LanguageTextDto>(entity); return ObjectMapper.Map<LanguageText, LanguageTextDto>(entity);
} }
[Authorize(LanguagesPermissions.LanguageText.Default)]
public async Task<List<LanguageTextDto>> GetLanguageTextByCultureNameAsync(string CultureName) public async Task<List<LanguageTextDto>> GetLanguageTextByCultureNameAsync(string CultureName)
{ {
var item = await _repositoryText.GetListAsync(a => a.CultureName == 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); 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() public async Task ClearRedisCacheAsync()
{ {
var redis = ConnectionMultiplexer.Connect(_configuration["Redis:Configuration"]!); await _cacheCleaner.ClearAsync();
var db = redis.GetDatabase();
await db.ExecuteAsync("FLUSHALL");
} }
protected virtual IQueryable<LanguageKey> ApplySorting(IQueryable<LanguageKey> query, LanguageTextTranslatedRequestDto input) 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 System.Threading.Tasks;
using Sozsoft.Languages.Entities; using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Localization;
using Volo.Abp.Caching; using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Entities.Events;
@ -12,10 +13,12 @@ public class LanguageTextCacheItemInvalidator :
ITransientDependency ITransientDependency
{ {
private readonly IDistributedCache<LanguageTextCacheItem> cache; 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.cache = cache;
this.localizer = localizer;
} }
public async Task HandleEventAsync(EntityChangedEventData<LanguageText> eventData) public async Task HandleEventAsync(EntityChangedEventData<LanguageText> eventData)
@ -23,7 +26,7 @@ public class LanguageTextCacheItemInvalidator :
//distributedCacheOptions.Value.KeyPrefix //distributedCacheOptions.Value.KeyPrefix
var key = LanguageTextCacheItem.CalculateCacheKey(eventData.Entity.ResourceName, eventData.Entity.CultureName); var key = LanguageTextCacheItem.CalculateCacheKey(eventData.Entity.ResourceName, eventData.Entity.CultureName);
await cache.RemoveAsync(key); await cache.RemoveAsync(key);
return; localizer.ClearLocalCache();
} }
} }

View file

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

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -14,8 +15,14 @@ namespace Sozsoft.Languages.Localization;
public class DatabaseResourceLocalizer : IDatabaseResourceLocalizer, ISingletonDependency 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 IRepository<LanguageText, Guid> languageTextRepository;
private readonly IDistributedCache<LanguageTextCacheItem> cache; private readonly IDistributedCache<LanguageTextCacheItem> cache;
private readonly ConcurrentDictionary<string, (LanguageTextCacheItem Item, long ExpiresAt)> localCache = new();
public DatabaseResourceLocalizer( public DatabaseResourceLocalizer(
IRepository<LanguageText, Guid> languageTextRepository, IRepository<LanguageText, Guid> languageTextRepository,
@ -27,27 +34,52 @@ public class DatabaseResourceLocalizer : IDatabaseResourceLocalizer, ISingletonD
public LocalizedString GetOrNull(LocalizationResourceBase resource, string cultureName, string name) public LocalizedString GetOrNull(LocalizationResourceBase resource, string cultureName, string name)
{ {
var item = GetCacheItem(resource, cultureName); var value = GetCacheItem(resource, cultureName)?.Dictionary?.GetOrDefault(name);
var value = item.Dictionary?.GetOrDefault(name); return value == null ? null : new LocalizedString(name, value);
if (item == null || value == null)
{
return null;
}
return 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) protected LanguageTextCacheItem GetCacheItem(LocalizationResourceBase resource, string cultureName)
{ {
// culture, resource, key // culture, resource, key
var key = LanguageTextCacheItem.CalculateCacheKey(resource.ResourceName, cultureName); 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) public void Fill(LocalizationResourceBase resource, string cultureName, Dictionary<string, LocalizedString> dictionary)
{ {
var item = GetCacheItem(resource, cultureName); 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); 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) public async Task FillAsync(LocalizationResourceBase resource, string cultureName, Dictionary<string, LocalizedString> dictionary)
{ {
var key = LanguageTextCacheItem.CalculateCacheKey(resource.ResourceName, cultureName); 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); 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); void Fill(LocalizationResourceBase resource, string cultureName, Dictionary<string, LocalizedString> dictionary);
Task FillAsync(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); 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)) if (listForm == null || string.IsNullOrWhiteSpace(relativePath) || SeedPathResolver.IsSeedFolderPath(relativePath))
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsNotAllowed]); 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) var seedFile = File.Exists(path)
? JsonSerializer.Deserialize<ListFormSeedDataFileDto>(await File.ReadAllTextAsync(path), ListFormSeedDataFileDto.JsonOptions) ? JsonSerializer.Deserialize<ListFormSeedDataFileDto>(await File.ReadAllTextAsync(path), ListFormSeedDataFileDto.JsonOptions)
: null; : null;

View file

@ -6,7 +6,7 @@ namespace Sozsoft.Mcp;
/// <summary> /// <summary>
/// Modulun paylasilan sozlesmesi. Kullaniciya gorunen metinler platformun kendi /// 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. /// kaynagi acmaz.
/// </summary> /// </summary>
[DependsOn( [DependsOn(

View file

@ -1,7 +1,7 @@
namespace Sozsoft.Mcp; namespace Sozsoft.Mcp;
/// <summary> /// <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. /// dosyasinda EN + TR olarak durur; koda gomulu metin kullanilmaz.
/// </summary> /// </summary>
public static class McpErrorCodes public static class McpErrorCodes

View file

@ -265,14 +265,13 @@ public class McpTableCreator(
} }
/// <summary> /// <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. /// Dosyasiz tablo veritabani sifirlandiginda geri gelmezdi; yazilamazsa islem hata verir.
/// </summary> /// </summary>
private string SaveSeedScript(string tableName, string script) private string SaveSeedScript(string tableName, string script)
{ {
var outputPath = SeedPathResolver.GetScopePath( var outputPath = SeedPathResolver.GetPath(
configuration, configuration,
CurrentTenant.Id,
SeedPathResolver.GetProviderFolderName(isPostgreSql: false), SeedPathResolver.GetProviderFolderName(isPostgreSql: false),
SeedPathResolver.ObjectFolder); SeedPathResolver.ObjectFolder);
@ -281,7 +280,7 @@ public class McpTableCreator(
var filePath = Path.Combine(outputPath, $"{tableName}.sql"); var filePath = Path.Combine(outputPath, $"{tableName}.sql");
File.WriteAllText(filePath, script); 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) 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); Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName);
/// <summary> /// <summary>
/// Saves the T-SQL script to {SeedsRoot}/{host|tenants/{tenantId}}/{sql|postgres}/object/{fileName}.sql /// Saves the T-SQL script to {SeedsRoot}/{sql|postgres}/object/{fileName}.sql
/// (repo: configs/seeds). The scope folder follows the current tenant: host context writes to host/. /// (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. /// Called automatically after a successful SqlTableDesigner deploy so the script can be re-seeded.
/// </summary> /// </summary>
Task SaveTableScriptAsync(SaveTableScriptDto input); Task SaveTableScriptAsync(SaveTableScriptDto input);

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ public class GridOptionsEditDto : GridOptionsDto
set { SelectFieldsDefaultValueJson = JsonSerializer.Serialize(value); } 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> /// Dolu ise ekrandan yapilan kayit islemleri dosyaya yansitilir.</summary>
public string SeedFilePath { get; set; } public string SeedFilePath { get; set; }

View file

@ -39,7 +39,7 @@ public class ModuleDocDto
/// <summary>Dosya adi, uzantisiz (<c>HrPerformance</c>).</summary> /// <summary>Dosya adi, uzantisiz (<c>HrPerformance</c>).</summary>
public string Name { get; set; } = string.Empty; 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 string Path { get; set; } = string.Empty;
public bool Exists { get; set; } public bool Exists { get; set; }

View file

@ -85,7 +85,7 @@ public class ListFormWizardDto
public bool ConfirmDelete { get; set; } public bool ConfirmDelete { get; set; }
/// <summary> /// <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 /// (<c>SeedFilePath</c> + ekle/guncelle/sil senkronizasyonu). Istekle gelen ornek/demo satirlar
/// ekrandan girildiginde dosyaya yazilir ve migrate ile geri yuklenir. /// ekrandan girildiginde dosyaya yazilir ve migrate ile geri yuklenir.
/// </summary> /// </summary>

View file

@ -5,7 +5,7 @@ namespace Sozsoft.Platform.ListForms;
/// <summary>Export zip'ine girecek tek bir seed dosyasi.</summary> /// <summary>Export zip'ine girecek tek bir seed dosyasi.</summary>
public class WizardExportEntryDto 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; } public string Path { get; set; }
/// <summary>Dosyanin arsive neden girdigi (wizard'in kendisi, alt form, lookup, veri...).</summary> /// <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> /// <summary>Indirilecek arsivin adi.</summary>
public string ArchiveName { get; set; } 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; } = []; public List<WizardExportEntryDto> Entries { get; set; } = [];
/// <summary>Bagimlilik olarak bulunan ama diskte karsiligi olmayan dosyalar.</summary> /// <summary>Bagimlilik olarak bulunan ama diskte karsiligi olmayan dosyalar.</summary>

View file

@ -5,11 +5,11 @@ namespace Sozsoft.Platform.ListForms;
/// <summary> /// <summary>
/// Import edilen zip icindeki tek bir seed dosyasinin analiz sonucu. /// 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> /// </summary>
public class WizardImportEntryDto 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; } public string Path { get; set; }
/// <summary>Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, data, sql, postgres).</summary> /// <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> /// <summary>Staging klasorunu ve rollback kaydini tanimlayan oturum kimligi.</summary>
public string ImportId { get; set; } 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; } = []; public List<WizardImportEntryDto> Entries { get; set; } = [];
/// <summary>Guvenlik/format nedeniyle atlanan zip girdileri icin aciklamalar.</summary> /// <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> /// <summary>Import kapanisindaki tek bir adimin sonucu (SQL scripti ya da CRUD dosyasi).</summary>
public class WizardImportStepResultDto public class WizardImportStepResultDto
{ {
/// <summary>Kapsam klasorune goreli yol.</summary> /// <summary>Seed klasorune goreli yol.</summary>
public string Path { get; set; } public string Path { get; set; }
/// <summary>Adim basariliysa yapilan isi ozetleyen kisa metin.</summary> /// <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> /// <summary>Tek bir veri seed dosyasinin veritabanina uygulanma sonucu.</summary>
public class WizardImportSeedApplyResultDto public class WizardImportSeedApplyResultDto
{ {
/// <summary>Kapsam klasorune goreli yol.</summary> /// <summary>Seed klasorune goreli yol.</summary>
public string Path { get; set; } public string Path { get; set; }
/// <summary>Veritabanina eklenen satir sayisi.</summary> /// <summary>Veritabanina eklenen satir sayisi.</summary>

View file

@ -11,7 +11,7 @@ namespace Sozsoft.Platform.ListForms;
/// <summary> /// <summary>
/// Wizard seed dosyası formatı. /// 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. /// WizardDataSeeder (DbMigrator) bu dosyaları okuyarak veritabanını yeniden oluşturur.
/// </summary> /// </summary>
public class WizardSeedFileDto public class WizardSeedFileDto
@ -511,7 +511,7 @@ public class WizardDeleteTableDto
public WizardDeleteReasonEnum Reason { get; set; } public WizardDeleteReasonEnum Reason { get; set; }
public List<WizardRecordRefDto> UsedBy { 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; } = []; public List<string> Files { get; set; } = [];
} }

View file

@ -3,7 +3,7 @@
namespace Sozsoft.Platform.Permissions; namespace Sozsoft.Platform.Permissions;
/// <summary> /// <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 /// dosyasından <c>ListFormSeedDataApplier</c> ile veritabanına seed edilir ve
/// oradan dinamik olarak yönetilir. Bu sağlayıcı yalnızca kullanılmayan /// oradan dinamik olarak yönetilir. Bu sağlayıcı yalnızca kullanılmayan
/// ABP modül gruplarını kaldırmak için vardır. /// 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform; using Sozsoft.Platform;
using Sozsoft.Platform.Branchs; using Sozsoft.Platform.Branchs;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
@ -18,10 +19,13 @@ public class BranchAppService : PlatformAppService
_branchSeedManager = branchSeedManager; _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) 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); 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." }; return new BranchSeedResultDto { Success = false, Message = "Branch not found." };
using (CurrentTenant.Change(branch.TenantId)) using (CurrentTenant.Change(branch.TenantId))

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
@ -16,6 +17,23 @@ public class DataSourceAppService : CrudAppService<
{ {
public DataSourceAppService(IRepository<DataSource, Guid> repo) : base(repo) 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.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform.Domain.DeveloperKit; using Sozsoft.Platform.Domain.DeveloperKit;
@ -12,6 +13,11 @@ using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Application; 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] [RemoteService]
[Route("api/app/crudendpoint")] [Route("api/app/crudendpoint")]
public class CrudEndpointAppService : ApplicationService public class CrudEndpointAppService : ApplicationService

View file

@ -203,7 +203,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
} }
/// <summary> /// <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 /// dosyasina yazar; endpoint kalmamissa dosyayi siler. Dosya islemi hatasi CRUD islemini
/// engellemez, yalnizca loglanir. /// engellemez, yalnizca loglanir.
/// </summary> /// </summary>
@ -261,12 +261,12 @@ public class CrudEndpointGenerateAppService : CrudAppService<
} }
/// <summary> /// <summary>
/// CRUD seed dosyalarinin yazilacagi klasor; kapsam aktif tenant'i izler /// CRUD seed dosyalarinin yazilacagi klasor (<c>crud/</c>); host ve kendi veritabani olan her
/// (host baglaminda <c>host/crud</c>, tenant baglaminda <c>tenants/{tenantId}/crud</c>). /// tenant ayni dosyalarla kurulur.
/// </summary> /// </summary>
private string ResolveCrudSeedOutputPath() 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> /// <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.CodeAnalysis.CSharp;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.DeveloperKit;
using Volo.Abp.DependencyInjection;
namespace Sozsoft.Platform.DynamicServices; namespace Sozsoft.Platform.DynamicServices;
/// <summary> /// <summary>
/// Dynamic C# kod derleme servisi /// Dynamic C# kod derleme servisi
/// </summary> /// </summary>
public class DynamicServiceCompiler : ITransientDependency public class DynamicServiceCompiler
{ {
private readonly ILogger<DynamicServiceCompiler> _logger; private readonly ILogger<DynamicServiceCompiler> _logger;

View file

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using Sozsoft.Platform.BlobStoring; using Sozsoft.Platform.BlobStoring;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions; using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.Identity.Dto; using Sozsoft.Platform.Identity.Dto;
@ -79,6 +80,8 @@ public class PlatformIdentityAppService : ApplicationService
this.guidGenerator = guidGenerator; 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) public async Task<UserInfoViewModel> GetByIdAsync(Guid UserId)
{ {
var user = await identityUserRepository.GetAsync(UserId); var user = await identityUserRepository.GetAsync(UserId);
@ -187,6 +190,7 @@ public class PlatformIdentityAppService : ApplicationService
}.MapIdentityExtraProperties(user); }.MapIdentityExtraProperties(user);
} }
[Authorize(IdentityPermissions.Users.ManageRoles)]
public async Task UpdatePermissionAsync(UserInfoViewModel UserInfo) public async Task UpdatePermissionAsync(UserInfoViewModel UserInfo)
{ {
var roles = UserInfo.Roles.Where(r => r.IsAssigned).Select(r => r.Name).ToArray(); 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 // 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) .Where(b => b.IsAssigned)
.Select(b => b.Id) .Select(b => b.Id)
.ToList(); .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) foreach (var branchId in assignedBranchIds)
{ {
@ -218,6 +227,7 @@ public class PlatformIdentityAppService : ApplicationService
} }
} }
[Authorize(IdentityPermissions.Users.Update)]
public async Task UpdateLockoutAsync(UserInfoViewModel UserInfo) public async Task UpdateLockoutAsync(UserInfoViewModel UserInfo)
{ {
var user = await UserManager.GetByIdAsync(UserInfo.Id); var user = await UserManager.GetByIdAsync(UserInfo.Id);
@ -255,6 +265,7 @@ public class PlatformIdentityAppService : ApplicationService
await UserManager.UpdateAsync(user); await UserManager.UpdateAsync(user);
} }
[Authorize(IdentityPermissions.Users.Update)]
public async Task UpdateUserAsync(UserInfoViewModel UserInfo) public async Task UpdateUserAsync(UserInfoViewModel UserInfo)
{ {
// 4857 s. Is Kanunu md. 15: deneme suresi en fazla iki ay, toplu sozlesmeyle dort ay. // 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)]; 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) public async Task<List<PermissionGrantedUsersDto>> GetPermissionGrantedUsersAsync(string permissionName)
{ {
if (string.IsNullOrWhiteSpace(permissionName)) if (string.IsNullOrWhiteSpace(permissionName))
@ -576,6 +589,7 @@ public class PlatformIdentityAppService : ApplicationService
}).ToList(); }).ToList();
} }
[Authorize(IdentityPermissions.Users.Update)]
public async Task CreateClaimUserAsync(UserClaimModel input) public async Task CreateClaimUserAsync(UserClaimModel input)
{ {
var user = await identityUserRepository.GetAsync(input.UserId); var user = await identityUserRepository.GetAsync(input.UserId);
@ -583,6 +597,7 @@ public class PlatformIdentityAppService : ApplicationService
user.AddClaim(guidGenerator, new Claim(input.ClaimType, input.ClaimValue)); user.AddClaim(guidGenerator, new Claim(input.ClaimType, input.ClaimValue));
} }
[Authorize(IdentityPermissions.Users.Update)]
public async Task DeleteClaimUser(Guid id, Guid userId) public async Task DeleteClaimUser(Guid id, Guid userId)
{ {
var user = await identityUserRepository.GetAsync(userId); var user = await identityUserRepository.GetAsync(userId);
@ -591,8 +606,12 @@ public class PlatformIdentityAppService : ApplicationService
user.Claims.Remove(claim); user.Claims.Remove(claim);
} }
[Authorize(SeedConsts.AppCodes.IdentityManagement.Sessions)]
public async Task KickUserAsync(Guid userId) 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)) using (CurrentTenant.Change(CurrentTenant.Id))
{ {
// 1. AbpSessions temizle // 1. AbpSessions temizle

View file

@ -62,8 +62,9 @@ public enum SeedSyncOperation
} }
/// <summary> /// <summary>
/// <c>ListForm.SeedFilePath</c> dolu olan ekranlarin verisi kapsam klasorunde aynalanir; veritabani /// <c>ListForm.SeedFilePath</c> dolu olan ekranlarin verisi seed klasorunde aynalanir; veritabani
/// sifirlandiginda <c>ListFormSeedDataApplier</c> ayni yerden geri yukler. /// 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, /// Senkronizasyon satir bazlidir: yalnizca islem goren satir okunup karsiligina yazilir,
/// boylece her kayitta listenin tamami sorgulanmaz ve dosyadaki satir sirasi korunur. /// boylece her kayitta listenin tamami sorgulanmaz ve dosyadaki satir sirasi korunur.
/// <para> /// <para>
@ -403,6 +404,11 @@ public class ListFormSeedDataSynchronizer(
Func<ListForm, SeedTarget, Task> action, Func<ListForm, SeedTarget, Task> action,
bool createTarget = true) 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 try
{ {
var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
@ -411,10 +417,7 @@ public class ListFormSeedDataSynchronizer(
var isFolder = SeedPathResolver.IsSeedFolderPath(listForm.SeedFilePath); var isFolder = SeedPathResolver.IsSeedFolderPath(listForm.SeedFilePath);
var fullPath = SeedPathResolver.GetScopePath( var fullPath = SeedPathResolver.GetPath(configuration, listForm.SeedFilePath.Replace('\\', '/').Trim('/').Split('/'));
configuration,
currentTenant.Id,
listForm.SeedFilePath.Replace('\\', '/').Trim('/').Split('/'));
// Klasor ilk kayitta olusur; ekran, hedefi diskte hazir bulmak zorunda degildir. // Klasor ilk kayitta olusur; ekran, hedefi diskte hazir bulmak zorunda degildir.
if (createTarget) if (createTarget)

View file

@ -20,6 +20,8 @@ namespace Sozsoft.Platform.ListForms.Select;
[Authorize] [Authorize]
public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppService public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppService
{ {
private static readonly JsonSerializerOptions CaseInsensitiveJson = new() { PropertyNameCaseInsensitive = true };
private readonly IRepository<ListForm, Guid> listFormRepository; private readonly IRepository<ListForm, Guid> listFormRepository;
private readonly IRepository<ListFormField, Guid> listFormFieldRepository; private readonly IRepository<ListFormField, Guid> listFormFieldRepository;
private readonly IRepository<ListFormWorkflow, string> listFormWorkflowRepository; private readonly IRepository<ListFormWorkflow, string> listFormWorkflowRepository;
@ -148,6 +150,22 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
//kullaniciya ait ListForm verisini al //kullaniciya ait ListForm verisini al
var listForm = await listFormManager.GetUserListForm(input.ListFormCode); 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 //kullaniciya ait ListFormField verilerini al
var fields = await listFormFieldManager.GetUserListFormFields(input.ListFormCode); var fields = await listFormFieldManager.GetUserListFormFields(input.ListFormCode);
fields = fields.Where(a => a.CanRead).ToList(); fields = fields.Where(a => a.CanRead).ToList();

View file

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

View file

@ -10,7 +10,7 @@ using Sozsoft.Platform.Data.Seeds;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
/// <summary> /// <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: /// dokumana ait oldugunu cozer. Kural, wizard sozlesmesine alan eklemeden menu agacindan cikar:
/// <list type="number"> /// <list type="number">
/// <item>Bir dokuman ekranin kodunu basligindaki beyanla sahiplenmisse (<c>module-doc: claims=…</c>) o dokuman.</item> /// <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 Sozsoft.Platform.Data.Seeds;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
@ -20,12 +19,11 @@ namespace Sozsoft.Platform.ListForms;
/// </summary> /// </summary>
public class ModuleDocManager( public class ModuleDocManager(
IConfiguration configuration, IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<ModuleDocManager> logger) : ITransientDependency ILogger<ModuleDocManager> logger) : ITransientDependency
{ {
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); 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); public Task<ModuleDocCatalog> LoadCatalogAsync() => ModuleDocCatalog.LoadAsync(ScopeRoot);

View file

@ -461,7 +461,7 @@ public sealed class WizardExportCollector(string scopeRoot)
} }
/// <summary> /// <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. /// diskte gercekten var olanlar. Bir kez taranir; kapsam kokunun disina cikilmaz.
/// </summary> /// </summary>
private IReadOnlyList<string> ScriptFolders => scriptFolders ??= ScanScriptFolders(); private IReadOnlyList<string> ScriptFolders => scriptFolders ??= ScanScriptFolders();
@ -486,7 +486,21 @@ public sealed class WizardExportCollector(string scopeRoot)
} }
foreach (var scriptPath in SafeEnumerateDirectories(providerPath)) 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; return folders;

View file

@ -12,7 +12,6 @@ using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Extensions; using Sozsoft.Platform.Extensions;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement; using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement; using Volo.Abp.TenantManagement;
@ -58,7 +57,6 @@ public class WizardRecordCleaner(
ITenantRepository tenantRepository, ITenantRepository tenantRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer, IListFormSeedDataSynchronizer seedDataSynchronizer,
IConfiguration configuration, IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<WizardRecordCleaner> logger) : ITransientDependency ILogger<WizardRecordCleaner> logger) : ITransientDependency
{ {
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary> /// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
@ -192,7 +190,7 @@ public class WizardRecordCleaner(
return; return;
} }
var path = SeedPathResolver.GetScopePath(configuration, currentTenant.Id, relativePath.Split('/')); var path = SeedPathResolver.GetPath(configuration, relativePath.Split('/'));
if (SeedPathResolver.IsSeedFolderPath(relativePath)) if (SeedPathResolver.IsSeedFolderPath(relativePath))
{ {
if (!Directory.Exists(path)) 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 PermissionFile = SeedConsts.AbpIdentity.Permissions.Default;
public static readonly string LanguageTextFile = PlatformConsts.AppCodes.Languages.LanguageText; 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 = private static readonly FileSpec[] Specs =
[ [
new(ModuleFile, "Id", 2), 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) public static async Task<WizardSeedRowStore> LoadAsync(string scopeRoot)
{ {
var store = new WizardSeedRowStore(); var store = new WizardSeedRowStore();

View file

@ -44,7 +44,9 @@ public class WizardSeedSynchronizer(
public async Task SyncAsync(string listFormCode) 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; return;
try try
@ -89,13 +91,13 @@ public class WizardSeedSynchronizer(
} }
/// <summary> /// <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 /// wizard adiyla ayni oldugu icin once dogrudan ad denenir; eski ya da elle adlandirilmis
/// dosyalar icin klasor taranir. /// dosyalar icin klasor taranir.
/// </summary> /// </summary>
private async Task<(string FilePath, WizardSeedFileDto Seed)?> FindSeedFileAsync(string listFormCode) 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)) if (!Directory.Exists(wizardPath))
return null; return null;

View file

@ -12,7 +12,6 @@ using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
@ -38,7 +37,6 @@ public class WizardTableCleaner(
IRepository<CrudEndpoint, Guid> repoCrudEndpoint, IRepository<CrudEndpoint, Guid> repoCrudEndpoint,
IDynamicDataManager dynamicDataManager, IDynamicDataManager dynamicDataManager,
IConfiguration configuration, IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<WizardTableCleaner> logger) : ITransientDependency ILogger<WizardTableCleaner> logger) : ITransientDependency
{ {
private const int MaxUsedBy = 20; private const int MaxUsedBy = 20;
@ -353,7 +351,7 @@ public class WizardTableCleaner(
// ── Yardimcilar ──────────────────────────────────────────────────────────────── // ── Yardimcilar ────────────────────────────────────────────────────────────────
private string ScopeFile(string relativePath) => 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> /// <summary>Sema yazilmamissa null doner; varsayilan sema saglayiciya gore <see cref="ReadTableStateAsync"/>'ta atanir.</summary>
private static (string? SchemaName, string TableName) ParseTableName(string selectCommand) 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sozsoft.Languages; using Sozsoft.Languages.Caching;
using Sozsoft.Languages.Entities; using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Languages; using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
@ -32,7 +32,7 @@ public class MenuAppService : CrudAppService<
private readonly IRepository<LanguageText, Guid> _repositoryText; private readonly IRepository<LanguageText, Guid> _repositoryText;
private readonly ITenantRepository _tenantRepository; private readonly ITenantRepository _tenantRepository;
private readonly IPermissionDefinitionRecordRepository _permissionRepository; private readonly IPermissionDefinitionRecordRepository _permissionRepository;
private readonly LanguageTextAppService _languageTextAppService; private readonly DistributedCacheCleaner _cacheCleaner;
private readonly IRepository<Module, string> _moduleRepository; private readonly IRepository<Module, string> _moduleRepository;
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer; private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
@ -42,7 +42,7 @@ public class MenuAppService : CrudAppService<
IRepository<LanguageText, Guid> languageTextRepository, IRepository<LanguageText, Guid> languageTextRepository,
ITenantRepository tenantRepository, ITenantRepository tenantRepository,
IPermissionDefinitionRecordRepository permissionRepository, IPermissionDefinitionRecordRepository permissionRepository,
LanguageTextAppService languageTextAppService, DistributedCacheCleaner cacheCleaner,
IRepository<Module, string> moduleRepository, IRepository<Module, string> moduleRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer IListFormSeedDataSynchronizer seedDataSynchronizer
) : base(menuRepository) ) : base(menuRepository)
@ -52,7 +52,7 @@ public class MenuAppService : CrudAppService<
_repositoryText = languageTextRepository; _repositoryText = languageTextRepository;
_tenantRepository = tenantRepository; _tenantRepository = tenantRepository;
_permissionRepository = permissionRepository; _permissionRepository = permissionRepository;
_languageTextAppService = languageTextAppService; _cacheCleaner = cacheCleaner;
_moduleRepository = moduleRepository; _moduleRepository = moduleRepository;
_seedDataSynchronizer = seedDataSynchronizer; _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 // Dil metinleri de kendi ekraninin seed dosyasinda yasar; menu adini runtime'da
// degistirmek DB sifirlandiginda geri gelmeli. // degistirmek DB sifirlandiginda geri gelmeli.

View file

@ -2,27 +2,21 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Extensions; using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.ListForms.DynamicApi;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local; using Volo.Abp.EventBus.Local;
using Volo.Abp.Identity;
using Volo.Abp.TenantManagement; using Volo.Abp.TenantManagement;
namespace Sozsoft.Platform.Tenants; namespace Sozsoft.Platform.Tenants;
[Authorize(TenantManagementPermissions.Tenants.Default)] [Authorize(TenantManagementPermissions.Tenants.Default)]
[Dependency(ReplaceServices = true)] [Dependency(ReplaceServices = true)]
public class PlatformTenantAppService : TenantAppService, IPlatformTenantAppService public class PlatformTenantAppService : TenantAppService
{ {
private readonly ITenantRepository tenantRepository;
private readonly IDistributedEventBus distributedEventBus;
public PlatformTenantAppService( public PlatformTenantAppService(
ITenantRepository tenantRepository, ITenantRepository tenantRepository,
ITenantManager tenantManager, ITenantManager tenantManager,
@ -30,9 +24,6 @@ public class PlatformTenantAppService : TenantAppService, IPlatformTenantAppServ
IDistributedEventBus distributedEventBus, IDistributedEventBus distributedEventBus,
ILocalEventBus localEventBus) : base(tenantRepository, tenantManager, dataSeeder, distributedEventBus, localEventBus) ILocalEventBus localEventBus) : base(tenantRepository, tenantManager, dataSeeder, distributedEventBus, localEventBus)
{ {
this.tenantRepository = tenantRepository;
this.distributedEventBus = distributedEventBus;
LocalizationResource = typeof(PlatformResource); LocalizationResource = typeof(PlatformResource);
} }
@ -93,23 +84,5 @@ public class PlatformTenantAppService : TenantAppService, IPlatformTenantAppServ
return dto; 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> /// <summary>
/// SqlQueryManager uzerinden uretilen CRUD endpoint'lerini /// 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> /// <para>
/// <see cref="SqlDataSeeder"/> tablolari olusturduktan hemen sonra calisir; boylece endpoint'ler /// <see cref="SqlDataSeeder"/> tablolari olusturduktan hemen sonra calisir; boylece endpoint'ler
/// her zaman dayandiklari tablolar hazir olduktan sonra yazilir. ABP'nin contributor sirasi /// 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 Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using static Sozsoft.Settings.SettingsConsts; using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Data.Seeds; namespace Sozsoft.Platform.Data.Seeds;
/// <summary> /// <summary>
/// Seeds/{host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki .sql dosyaları /// Seeds/{sql|postgres}/execute klasöründeki .sql dosyaları iki aşamada işlenir:
/// iki aşamada işlenir: /// 1) Migration/seed sırasında <see cref="SqlDataSeeder"/> tarafından çalıştırılır (nesne
/// 1) Migration/seed sırasında <see cref="SqlDataSeeder"/> tarafından, kapsam klasörüyle /// oluşturulur/güncellenir).
/// (host/tenants/{tenantId}) aynı bağlamda ç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 /// 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. /// Dosya adı = procedure adı. İsteğe bağlı olarak "schema.Procedure.sql" formatı da desteklenir.
/// </summary> /// </summary>
public class FinalSeeder : IFinalSeeder, ITransientDependency public class FinalSeeder : IFinalSeeder, ITransientDependency
@ -39,44 +40,39 @@ public class FinalSeeder : IFinalSeeder, ITransientDependency
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider; private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly ICurrentTenant _currentTenant;
private readonly ILogger<FinalSeeder> _logger; private readonly ILogger<FinalSeeder> _logger;
public FinalSeeder( public FinalSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider, IDbContextProvider<PlatformDbContext> dbContextProvider,
IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkManager unitOfWorkManager,
IConfiguration configuration, IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<FinalSeeder> logger) ILogger<FinalSeeder> logger)
{ {
_dbContextProvider = dbContextProvider; _dbContextProvider = dbContextProvider;
_unitOfWorkManager = unitOfWorkManager; _unitOfWorkManager = unitOfWorkManager;
_configuration = configuration; _configuration = configuration;
_currentTenant = currentTenant;
_logger = logger; _logger = logger;
} }
public async Task ExecuteAsync(Guid? tenantId = null) public async Task ExecuteAsync()
{ {
var directoryPath = GetScriptDirectoryPath(tenantId); // Ortak procedure'ler once, host'a ozgu olanlar (host/execute) yalnizca host baglaminda sonra.
var sqlFiles = GetScriptDirectoryPaths()
if (!Directory.Exists(directoryPath)) .Where(Directory.Exists)
{ .SelectMany(path => Directory.GetFiles(path, "*.sql")
_logger.LogInformation("{Directory} directory not found, skipping FinalSeeder.", directoryPath); .OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase))
return;
}
var sqlFiles = Directory.GetFiles(directoryPath, "*.sql")
.OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)
.ToArray(); .ToArray();
if (sqlFiles.Length == 0) if (sqlFiles.Length == 0)
{ {
_logger.LogInformation("No .sql files found in {Directory}, skipping FinalSeeder.", directoryPath); _logger.LogInformation("No execute script found, skipping FinalSeeder.");
return; return;
} }
_logger.LogInformation( _logger.LogInformation("FinalSeeder started. {Count} procedure(s) to be executed.", sqlFiles.Length);
"FinalSeeder started for {Scope}. {Count} procedure(s) to be executed.",
tenantId == null ? "Host" : $"Tenant {tenantId}",
sqlFiles.Length);
var failed = new List<string>(); 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( var providerDirectoryName = SeedPathResolver.GetProviderFolderName(
DefaultDatabaseProvider == DatabaseProvider.PostgreSql); 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> /// <summary>

View file

@ -14,7 +14,7 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary> /// <summary>
/// Varsayilan veri kaynagini olusturur. Baglanti cumlesi ortamin appsettings dosyasindan gelir; /// 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> /// </summary>
public class HostDataSeeder : IDataSeedContributor, ITransientDependency public class HostDataSeeder : IDataSeedContributor, ITransientDependency
{ {

View file

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

View file

@ -5,14 +5,12 @@ using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Localization;
using Sozsoft.Languages.Languages; using Sozsoft.Languages.Languages;
using Sozsoft.Mcp; using Sozsoft.Mcp;
using Sozsoft.Platform.Editors; using Sozsoft.Platform.Editors;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms; using Sozsoft.Platform.ListForms;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Permissions; using Sozsoft.Platform.Permissions;
using Sozsoft.Platform.Queries; using Sozsoft.Platform.Queries;
using Volo.Abp.Data; using Volo.Abp.Data;
@ -31,28 +29,24 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
private readonly IRepository<ListFormField, Guid> _listFormFieldRepository; private readonly IRepository<ListFormField, Guid> _listFormFieldRepository;
private readonly IRepository<ListFormWorkflow, string> _listFormWorkflowRepository; private readonly IRepository<ListFormWorkflow, string> _listFormWorkflowRepository;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
protected IStringLocalizer<PlatformResource> _localizer { get; }
public ListFormSeeder_Saas( public ListFormSeeder_Saas(
IRepository<ListForm, Guid> listFormRepository, IRepository<ListForm, Guid> listFormRepository,
IRepository<ListFormField, Guid> listFormFieldRepository, IRepository<ListFormField, Guid> listFormFieldRepository,
IRepository<ListFormWorkflow, string> listFormWorkflowRepository, IRepository<ListFormWorkflow, string> listFormWorkflowRepository,
IConfiguration configuration, IConfiguration configuration
IStringLocalizer<PlatformResource> localizer
) )
{ {
_listFormRepository = listFormRepository; _listFormRepository = listFormRepository;
_listFormFieldRepository = listFormFieldRepository; _listFormFieldRepository = listFormFieldRepository;
_listFormWorkflowRepository = listFormWorkflowRepository; _listFormWorkflowRepository = listFormWorkflowRepository;
_configuration = configuration; _configuration = configuration;
_localizer = localizer;
} }
public async Task SeedAsync(DataSeedContext context) public async Task SeedAsync(DataSeedContext context)
{ {
var configurationSection = _configuration.GetSection("OpenIddict:Applications"); var configurationSection = _configuration.GetSection("OpenIddict:Applications");
var swaggerRootUrl = configurationSection["Platform_Swagger:RootUrl"]?.TrimEnd('/'); var swaggerRootUrl = configurationSection["Platform_Swagger:RootUrl"]?.TrimEnd('/');
var utils = new ListFormSeeder_Utils(_listFormRepository, _listFormFieldRepository);
var listFormName = String.Empty; var listFormName = String.Empty;
#region Tenant #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;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -7,6 +8,7 @@ using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions; using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.Permissions; using Sozsoft.Platform.Permissions;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids; using Volo.Abp.Guids;
@ -14,6 +16,7 @@ using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement; using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement; using Volo.Abp.TenantManagement;
using Volo.Abp.Uow;
using IdentityRole = Volo.Abp.Identity.IdentityRole; using IdentityRole = Volo.Abp.Identity.IdentityRole;
using IdentityUser = Volo.Abp.Identity.IdentityUser; using IdentityUser = Volo.Abp.Identity.IdentityUser;
@ -23,6 +26,8 @@ namespace Sozsoft.Platform.Data.Seeds;
public class PlatformIdentityDataSeeder : IdentityDataSeeder public class PlatformIdentityDataSeeder : IdentityDataSeeder
{ {
private readonly IPermissionGrantRepository _permissionGrantRepository; private readonly IPermissionGrantRepository _permissionGrantRepository;
private readonly IPermissionDefinitionRecordRepository _permissionDefinitionRecordRepository;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly ITenantRepository _tenantRepository; private readonly ITenantRepository _tenantRepository;
private readonly IRepository<Branch, Guid> _branchRepository; private readonly IRepository<Branch, Guid> _branchRepository;
private readonly IRepository<BranchUsers, Guid> _branchUsersRepository; private readonly IRepository<BranchUsers, Guid> _branchUsersRepository;
@ -38,6 +43,8 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
IdentityUserManager userManager, IdentityUserManager userManager,
IdentityRoleManager roleManager, IdentityRoleManager roleManager,
IPermissionGrantRepository permissionGrantRepository, IPermissionGrantRepository permissionGrantRepository,
IPermissionDefinitionRecordRepository permissionDefinitionRecordRepository,
IUnitOfWorkManager unitOfWorkManager,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
ITenantRepository tenantRepository, ITenantRepository tenantRepository,
IConfiguration configuration, IConfiguration configuration,
@ -45,6 +52,8 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
) : base(guidGenerator, roleRepository, userRepository, lookupNormalizer, userManager, roleManager, currentTenant, identityOptions) ) : base(guidGenerator, roleRepository, userRepository, lookupNormalizer, userManager, roleManager, currentTenant, identityOptions)
{ {
this._permissionGrantRepository = permissionGrantRepository; this._permissionGrantRepository = permissionGrantRepository;
this._permissionDefinitionRecordRepository = permissionDefinitionRecordRepository;
this._unitOfWorkManager = unitOfWorkManager;
this._tenantRepository = tenantRepository; this._tenantRepository = tenantRepository;
this._branchRepository = branchRepository; this._branchRepository = branchRepository;
this._branchUsersRepository = branchUsersRepository; this._branchUsersRepository = branchUsersRepository;
@ -159,6 +168,13 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
adminUser.SetAvatar(GetAvatar(tenantId?.ToString(), adminUser.Id.ToString())); adminUser.SetAvatar(GetAvatar(tenantId?.ToString(), adminUser.Id.ToString()));
adminUser.SetUserType(PlatformConsts.AbpIdentity.User.InternalUserTypeDefaultValue); adminUser.SetUserType(PlatformConsts.AbpIdentity.User.InternalUserTypeDefaultValue);
adminUser.SetGender(PlatformConsts.AbpIdentity.User.AdminGenderDefaultValue); 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(); (await UserManager.CreateAsync(adminUser, adminPassword, validatePassword: false)).CheckErrors();
result.CreatedAdminUser = true; result.CreatedAdminUser = true;
@ -180,7 +196,11 @@ public class PlatformIdentityDataSeeder : IdentityDataSeeder
/*admin rolüne ait yetkiler otomatik atanıyor*/ /*admin rolüne ait yetkiler otomatik atanıyor*/
var permissions = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName); 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( 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) private string GetAvatar(string? tenantId, string? userId)
{ {
var tenantPart = !string.IsNullOrEmpty(tenantId) var tenantPart = !string.IsNullOrEmpty(tenantId)

View file

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

View file

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

View file

@ -1,6 +1,6 @@
namespace Sozsoft.Platform.Enums; 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 public enum WizardImportEntryStatusEnum
{ {
/// <summary>Hedefte ayni yolda dosya yok; dogrudan kopyalanir.</summary> /// <summary>Hedefte ayni yolda dosya yok; dogrudan kopyalanir.</summary>

View file

@ -51,7 +51,7 @@ public interface IListFormArtifact
string DeleteBeforeCommand { get; set; } string DeleteBeforeCommand { get; set; }
string DeleteAfterCommand { 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; } string SeedFilePath { get; set; }
/// <summary>Ekrandan eklenen kayit seed dosyasina da eklenir.</summary> /// <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 static readonly DateTime AdminBirthDateDefaultValue = new(1981, 8, 27);
public const string AdminBirthPlaceDefaultValue = "Kadıköy, İstanbul"; public const string AdminBirthPlaceDefaultValue = "Kadıköy, İstanbul";
public const string AdminMaritalStatusDefaultValue = "Evli"; 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. // Cinsiyet dilden bagimsiz anahtarla saklanir; ekranda App.StaticLookup.* ile cevrilir.
public const string GenderMale = "Male"; 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> /// <summary>Liste verisinin aynalandigi seed klasoru; SeedPathResolver ile ayni deger.</summary>
public const string SeedDataFolder = "data"; 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 SeedDataFilePath(string listFormCode) => $"{SeedDataFolder}/{listFormCode}.json";
public static string MenuUrl(string code) => $"/admin/list/{code}"; 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); public record CrudEndpointSeedApplyResult(string Path, int Inserted, int Updated, int Deleted, int Skipped, string Error = null);
/// <summary> /// <summary>
/// <c>{host|tenants/{tenantId}}/crud/{EntityName}.json</c> dosyalarini veritabanina uygular. /// <c>crud/{EntityName}.json</c> dosyalarini verilen tenant baglamindaki veritabanina uygular.
/// </summary> /// </summary>
public interface ICrudEndpointSeedApplier 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); Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyScopeAsync(Guid? tenantId);
/// <summary> /// <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. /// klasoru taramak yerine yalnizca yazilan dosyalari uygular.
/// </summary> /// </summary>
Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyFilesAsync(Guid? tenantId, IReadOnlyCollection<string> relativePaths); Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyFilesAsync(Guid? tenantId, IReadOnlyCollection<string> relativePaths);
@ -58,23 +58,22 @@ public class CrudEndpointSeedApplier(
public async Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyScopeAsync(Guid? tenantId) public async Task<IReadOnlyList<CrudEndpointSeedApplyResult>> ApplyScopeAsync(Guid? tenantId)
{ {
var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId); var crudPath = SeedPathResolver.GetPath(configuration, SeedPathResolver.CrudFolder);
var crudPath = SeedPathResolver.GetScopePath(configuration, tenantId, SeedPathResolver.CrudFolder);
if (!Directory.Exists(crudPath)) 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 []; return [];
} }
var jsonFiles = Directory.GetFiles(crudPath, "*.json").OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ToArray(); var jsonFiles = Directory.GetFiles(crudPath, "*.json").OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ToArray();
if (jsonFiles.Length == 0) 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 []; 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); return await ApplyAsync(tenantId, jsonFiles);
} }
@ -84,7 +83,7 @@ public class CrudEndpointSeedApplier(
if (relativePaths == null || relativePaths.Count == 0) if (relativePaths == null || relativePaths.Count == 0)
return []; return [];
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId)); var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(configuration));
var results = new List<CrudEndpointSeedApplyResult>(); var results = new List<CrudEndpointSeedApplyResult>();
var filePaths = new List<string>(); var filePaths = new List<string>();

View file

@ -5,7 +5,7 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary> /// <summary>
/// Bir entity icin uretilen CRUD endpoint'lerinin seed dosyasi. /// 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 /// olarak yazilir; veritabani silinip yeniden olusturuldugunda
/// <c>CrudDataSeeder</c> ayni dosyalari okuyarak endpoint'leri geri yukler. /// <c>CrudDataSeeder</c> ayni dosyalari okuyarak endpoint'leri geri yukler.
/// </summary> /// </summary>

View file

@ -3,7 +3,7 @@
/// <summary> /// <summary>
/// Component'in kullandigi tek bir API. <see cref="EntityName"/> dolu ise endpoint /// Component'in kullandigi tek bir API. <see cref="EntityName"/> dolu ise endpoint
/// CRUD Endpoint Manager tarafindan uretilmistir ve tanimi <see cref="SeedFile"/> /// 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> /// </summary>
public class CustomComponentSeedDataSourceDto public class CustomComponentSeedDataSourceDto
{ {
@ -23,6 +23,6 @@ public class CustomComponentSeedDataSourceDto
/// <summary>CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete).</summary> /// <summary>CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete).</summary>
public string? OperationType { get; set; } 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; } 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; namespace Sozsoft.Platform.Data;
@ -9,8 +8,8 @@ namespace Sozsoft.Platform.Data;
/// </summary> /// </summary>
public interface IFinalSeeder public interface IFinalSeeder
{ {
/// <param name="tenantId"> /// <summary>
/// null ise host/, aksi halde tenants/{tenantId}/ kapsamındaki {sql|postgres}/execute klasörü işlenir. /// {sql|postgres}/execute klasörünü geçerli bağlamın (host ya da tenant) veritabanında işler.
/// </param> /// </summary>
Task ExecuteAsync(Guid? tenantId = null); Task ExecuteAsync();
} }

View file

@ -23,7 +23,7 @@ using Volo.Abp.Uow;
namespace Sozsoft.Platform.Data.Seeds; namespace Sozsoft.Platform.Data.Seeds;
/// <summary> /// <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. /// tablosuna yazar.
/// </summary> /// </summary>
public interface IListFormSeedDataApplier public interface IListFormSeedDataApplier
@ -32,7 +32,7 @@ public interface IListFormSeedDataApplier
Task ApplyAsync(Guid? tenantId); Task ApplyAsync(Guid? tenantId);
/// <summary> /// <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 /// akislar butun klasoru taramak yerine yalnizca yazilan dosyalari uygular. Dosyalar
/// <c>ApplyAsync</c> ile ayni kurala gore siralanir. /// <c>ApplyAsync</c> ile ayni kurala gore siralanir.
/// </summary> /// </summary>
@ -105,7 +105,7 @@ public class ListFormSeedDataApplier(
if (relativePaths == null || relativePaths.Count == 0) if (relativePaths == null || relativePaths.Count == 0)
return []; return [];
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId)); var scopeRoot = Path.GetFullPath(SeedPathResolver.GetRootPath(configuration));
var results = new List<ListFormSeedDataApplyResult>(); var results = new List<ListFormSeedDataApplyResult>();
var filePaths = new List<string>(); var filePaths = new List<string>();
@ -143,12 +143,11 @@ public class ListFormSeedDataApplier(
public async Task ApplyAsync(Guid? tenantId) public async Task ApplyAsync(Guid? tenantId)
{ {
var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId); var dataPath = SeedPathResolver.GetPath(configuration, SeedPathResolver.DataFolder);
var dataPath = SeedPathResolver.GetScopePath(configuration, tenantId, SeedPathResolver.DataFolder);
if (!Directory.Exists(dataPath)) 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; return;
} }
@ -159,13 +158,13 @@ public class ListFormSeedDataApplier(
var jsonFiles = Directory.GetFiles(dataPath, "*.json", SearchOption.AllDirectories); var jsonFiles = Directory.GetFiles(dataPath, "*.json", SearchOption.AllDirectories);
if (jsonFiles.Length == 0) 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; 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)) using (currentTenant.Change(tenantId))
{ {
@ -179,7 +178,7 @@ public class ListFormSeedDataApplier(
} }
} }
logger.LogInformation("ListFormSeedDataApplier completed for {Scope}.", scopeFolderName); logger.LogInformation("ListFormSeedDataApplier completed.");
} }
/// <summary> /// <summary>
@ -249,7 +248,7 @@ public class ListFormSeedDataApplier(
/// <summary> /// <summary>
/// Tek seferde uygulanan bir ekranin dosyalari: tek dosya ya da satir klasorundeki butun satir /// 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. /// sonuclarda, loglarda bu gorunur.
/// </summary> /// </summary>
private sealed record SeedDataTarget(string Path, string[] FilePaths); private sealed record SeedDataTarget(string Path, string[] FilePaths);

View file

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

View file

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

View file

@ -1,15 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Sozsoft.Languages.Caching;
using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.Data.Seeds;
using StackExchange.Redis;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity; using Volo.Abp.Identity;
@ -29,6 +26,7 @@ public class PlatformDbMigrationService : ITransientDependency
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IEnumerable<IFinalSeeder> _finalSeeders; private readonly IEnumerable<IFinalSeeder> _finalSeeders;
private readonly IListFormSeedDataApplier _listFormSeedDataApplier; private readonly IListFormSeedDataApplier _listFormSeedDataApplier;
private readonly DistributedCacheCleaner _cacheCleaner;
public PlatformDbMigrationService( public PlatformDbMigrationService(
IDataSeeder dataSeeder, IDataSeeder dataSeeder,
@ -37,7 +35,8 @@ public class PlatformDbMigrationService : ITransientDependency
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IConfiguration configuration, IConfiguration configuration,
IEnumerable<IFinalSeeder> finalSeeders, IEnumerable<IFinalSeeder> finalSeeders,
IListFormSeedDataApplier listFormSeedDataApplier) IListFormSeedDataApplier listFormSeedDataApplier,
DistributedCacheCleaner cacheCleaner)
{ {
_dataSeeder = dataSeeder; _dataSeeder = dataSeeder;
_dbSchemaMigrators = dbSchemaMigrators; _dbSchemaMigrators = dbSchemaMigrators;
@ -46,6 +45,7 @@ public class PlatformDbMigrationService : ITransientDependency
_configuration = configuration; _configuration = configuration;
_finalSeeders = finalSeeders; _finalSeeders = finalSeeders;
_listFormSeedDataApplier = listFormSeedDataApplier; _listFormSeedDataApplier = listFormSeedDataApplier;
_cacheCleaner = cacheCleaner;
Logger = NullLogger<PlatformDbMigrationService>.Instance; Logger = NullLogger<PlatformDbMigrationService>.Instance;
} }
@ -55,27 +55,32 @@ public class PlatformDbMigrationService : ITransientDependency
var seed = _configuration.GetValue("Seed", false); var seed = _configuration.GetValue("Seed", false);
Logger.LogInformation($"Migrating database. Seed: {seed}. Env: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}"); Logger.LogInformation($"Migrating database. Seed: {seed}. Env: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}");
ClearCache(); await ClearCacheAsync();
var initialMigrationAdded = AddInitialMigrationIfNotExist(); var tenantSeedId = _configuration.GetValue<Guid?>(TenantSeedOptions.TenantIdKey);
if (tenantSeedId.HasValue)
if (initialMigrationAdded)
{ {
await SeedSingleTenantAsync(tenantSeedId.Value);
return; return;
} }
Logger.LogInformation("Started database migrations..."); Logger.LogInformation("Started database migrations...");
await MigrateDatabaseSchemaAsync(); 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) if (seed)
{ {
await SeedDataAsync(); await SeedDataAsync(completesPending: lastSeedFileTenant == null);
} }
Logger.LogInformation($"Successfully completed host database migrations."); Logger.LogInformation($"Successfully completed host database migrations.");
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var migratedDatabaseSchemas = new HashSet<string>(); var migratedDatabaseSchemas = new HashSet<string>();
foreach (var tenant in tenants) foreach (var tenant in tenants)
{ {
@ -96,7 +101,7 @@ public class PlatformDbMigrationService : ITransientDependency
} }
if (seed) 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."); 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 // 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. // almis olabilir (ornegin wizard'in yeni menusu); migrate sonrasinda da temizlenir.
if (seed) if (seed)
ClearCache(); await ClearCacheAsync();
Logger.LogInformation("You can safely end this process..."); 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"]!); var tenant = await _tenantRepository.FindAsync(tenantId, includeDetails: true)
redis.GetDatabase().Execute("FLUSHALL"); ?? 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> /// <summary>
/// Tüm host ve tenant migration/seed adımları bittikten sonra, /// 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 /// {sql|postgres}/execute klasöründeki script'lerin dosya adından türeyen procedure'lerini
/// türeyen procedure'lerini ilgili bağlamda çalıştırır. İlgili tenant klasörü yoksa /// host'ta ve kendi veritabanı olan her tenant'ta çalıştırır. Host veritabanını paylaşan tenant
/// o tenant adımı sessizce atlanır. /// atlanır; procedure o veritabanında host adımında zaten çalışmıştır.
/// </summary> /// </summary>
private async Task ExecuteFinalSeedersAsync(List<Tenant> tenants) private async Task ExecuteFinalSeedersAsync(List<Tenant> tenants, bool includeHost)
{ {
if (!_finalSeeders.Any()) if (!_finalSeeders.Any())
{ {
@ -136,20 +180,24 @@ public class PlatformDbMigrationService : ITransientDependency
} }
Logger.LogInformation( 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); tenants.Count);
using (_currentTenant.Change(null)) if (includeHost)
{ {
foreach (var executor in _finalSeeders) using (_currentTenant.Change(null))
{ {
Logger.LogInformation("Running SQL scripts for host..."); foreach (var executor in _finalSeeders)
await executor.ExecuteAsync(); {
Logger.LogInformation("Completed SQL scripts for host."); Logger.LogInformation("Running SQL scripts for host...");
await executor.ExecuteAsync();
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)) using (_currentTenant.Change(tenant.Id))
{ {
@ -158,7 +206,7 @@ public class PlatformDbMigrationService : ITransientDependency
Logger.LogInformation( Logger.LogInformation(
"Running SQL scripts for {TenantName} tenant...", "Running SQL scripts for {TenantName} tenant...",
tenant.Name); tenant.Name);
await executor.ExecuteAsync(tenant.Id); await executor.ExecuteAsync();
Logger.LogInformation( Logger.LogInformation(
"Completed SQL scripts for {TenantName} tenant.", "Completed SQL scripts for {TenantName} tenant.",
tenant.Name); 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..."); 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) await _dataSeeder.SeedAsync(new DataSeedContext(tenant?.Id)
.WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue) .WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, adminEmail)
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue) .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, adminPassword)
.WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue) .WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, adminEmail)
.WithProperty(SeedFileScope.PropertyName, applySeedFiles)
.WithProperty(SeedFileScope.CompletesPendingPropertyName, completesPending)
); );
// Veri seed dosyalari ListForm tanimlarina ve tablolara dayandigi icin butun // Veri seed dosyalari ListForm tanimlarina ve tablolara dayandigi icin butun
// contributor'lar bittikten sonra uygulanir; ABP contributor sirasi garanti degildir. // contributor'lar bittikten sonra uygulanir; ABP contributor sirasi garanti degildir.
await _listFormSeedDataApplier.ApplyAsync(tenant?.Id); if (applySeedFiles)
}
private bool AddInitialMigrationIfNotExist()
{
try
{ {
if (!DbMigrationsProjectExists()) await _listFormSeedDataApplier.ApplyAsync(tenant?.Id);
{
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. /// uzerinden de duzenlenebildigi icin yapilan degisiklik dogrudan host diskinde kalici olur.
/// </para> /// </para>
/// <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> /// <code>
/// seeds/ /// seeds/
/// host/ → host (varsayilan) veritabani kapsami /// crud/
/// crud/ /// wizard/
/// wizard/ /// data/
/// data/ /// docs/
/// sql/ → SQL Server saglayicisi /// sql/ → SQL Server saglayicisi
/// object/ → nesne olusturulur/guncellenir /// object/ → nesne olusturulur/guncellenir
/// execute/ → nesne olusturulur, ayrica tum migration'lar bitince calistirilir /// execute/ → nesne olusturulur, ayrica tum migration'lar bitince calistirilir
/// postgres/ → PostgreSQL saglayicisi /// host/{object,execute}/ → ayni kurallar, yalnizca host veritabaninda
/// object/ /// postgres/ → PostgreSQL saglayicisi
/// execute/ /// object/
/// tenants/ /// execute/
/// {tenantId}/ → her tenant kendi klasorunde /// host/{object,execute}/
/// crud/ wizard/ data/ sql/{object,execute}/ postgres/{object,execute}/
/// </code> /// </code>
/// </para> /// </para>
/// <para> /// <para>
@ -47,12 +50,6 @@ public static class SeedPathResolver
{ {
public const string ConfigurationKey = "App:SeedsPath"; 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> /// <summary>SQL Server saglayicisinin script klasoru.</summary>
public const string SqlFolder = "sql"; public const string SqlFolder = "sql";
@ -65,6 +62,13 @@ public static class SeedPathResolver
/// <summary>Olusturulan, ayrica tum migration'lar bittikten sonra calistirilan scriptler.</summary> /// <summary>Olusturulan, ayrica tum migration'lar bittikten sonra calistirilan scriptler.</summary>
public const string ExecuteFolder = "execute"; 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> /// <summary>Wizard'in urettigi konfigurasyon dosyalari.</summary>
public const string WizardFolder = "wizard"; public const string WizardFolder = "wizard";
@ -75,7 +79,7 @@ public static class SeedPathResolver
public const string DataFolder = WizardConsts.SeedDataFolder; public const string DataFolder = WizardConsts.SeedDataFolder;
/// <summary> /// <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. /// sunucu (wizard deploy, MCP) yalnizca seed kokune yazabildigi icin depo kokunde degil buradadir.
/// </summary> /// </summary>
public const string DocsFolder = "docs"; public const string DocsFolder = "docs";
@ -84,7 +88,7 @@ public static class SeedPathResolver
public const string ModuleDocsFolder = "modules"; public const string ModuleDocsFolder = "modules";
/// <summary> /// <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. /// Domain.Shared'dedir; wizard'in urettigi ekran (Application.Contracts) ayni yolu yazar.
/// </summary> /// </summary>
public static string GetDataFilePath(string listFormCode) public static string GetDataFilePath(string listFormCode)
@ -93,7 +97,7 @@ public static class SeedPathResolver
} }
/// <summary> /// <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 /// 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. /// bastan yazilir; klasor modunda yalnizca islem goren satirin dosyasina dokunulur.
/// </summary> /// </summary>
@ -168,38 +172,12 @@ public static class SeedPathResolver
return path; 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> /// <summary>Aktif veritabani saglayicisina karsilik gelen script klasorunun adi.</summary>
public static string GetProviderFolderName(bool isPostgreSql) public static string GetProviderFolderName(bool isPostgreSql)
{ {
return isPostgreSql ? PostgresFolder : SqlFolder; 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> /// <summary>Calisma dizininden yukari dogru cikarak depodaki configs/seeds klasorunu arar.</summary>
private static string? FindRepositorySeedsPath() 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> /// </summary>
public string DeleteServiceAddress { get; set; } = "list-form-data/delete"; 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 /// (ornegin <c>data/App.DeveloperKit.IntranetWidgets.json</c>). Dolu ise ekrandan yapilan
/// seed sirasinda dosyadaki satirlar tabloya geri yuklenir. Ekranda yapilan islemlerin dosyaya /// seed sirasinda dosyadaki satirlar tabloya geri yuklenir. Ekranda yapilan islemlerin dosyaya
/// yansimasi asagidaki bayraklara baglidir. /// yansimasi asagidaki bayraklara baglidir.

View file

@ -4,11 +4,6 @@ namespace Sozsoft.Platform.Extensions;
public static class StringExtensions 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) public static string GetSegment(this string str, char segment, int index)
{ {
var start = str.NthIndexOf(segment, index); var start = str.NthIndexOf(segment, index);

View file

@ -1,8 +1,9 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Queries; namespace Sozsoft.Platform.Queries;
@ -15,10 +16,12 @@ public interface IDataSourceManager
public class DataSourceManager : DomainService, IDataSourceManager public class DataSourceManager : DomainService, IDataSourceManager
{ {
private readonly IRepository<DataSource, Guid> repoDataSource; 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.repoDataSource = repoDataSource;
this.tenantStore = tenantStore;
} }
public async Task<string> GetConnectionStringAsync(bool isTenant, string dataSourceCode) 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) 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); var dataSource = await repoDataSource.FirstOrDefaultAsync(a => a.Code == code);
dataSource ??= await repoDataSource.FirstOrDefaultAsync(a => a.Code == dataSourceCode); dataSource ??= await repoDataSource.FirstOrDefaultAsync(a => a.Code == dataSourceCode);
return dataSource; 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) else if (op == OperationEnum.Delete)
{ {
// todo: softdelete // Silme her zaman anahtarla sınırlanır; diğer parametreler (TenantId gibi) yalnızca ek koşuldur.
string where = string.Empty; // Önceden parametre varsa anahtar hiç eklenmiyordu: silme varsayılanı tanımsız bir ekranda sorgu
if (parameters.Any()) // "WHERE TenantId = @TenantId" olarak kalıp kiracının bütün satırlarını siliyordu.
{ if (!parameters.ContainsKey(listForm.KeyFieldName))
where = string.Join(
" AND ",
parameters.Select(a => BuildWhereCondition(a.Key, a.Value, dataSourceType)).ToList());
}
else
{ {
var val = QueryHelper.GetFormattedValue(listForm.KeyFieldDbSourceType, keys); var val = QueryHelper.GetFormattedValue(listForm.KeyFieldDbSourceType, keys);
parameters.Add( parameters.Add(
listForm.KeyFieldName, listForm.KeyFieldName,
val.GetType().IsArray ? val : new object[] { val }); 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}"; sql = $"DELETE FROM \"{listForm.SelectCommand}\" WHERE {where}";
} }
} }

View file

@ -12,6 +12,7 @@ using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.ListForms; using Sozsoft.Platform.ListForms;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
using Sozsoft.Platform.OrganizationUnits; using Sozsoft.Platform.OrganizationUnits;
using Volo.Abp;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.ObjectMapping; using Volo.Abp.ObjectMapping;
using Volo.Abp.Settings; using Volo.Abp.Settings;
@ -205,10 +206,15 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
for (int i = 0; i < summaries.Length; i++) for (int i = 0; i < summaries.Length; i++)
{ {
var summary = summaries[i].Split(' '); var summary = summaries[i].Split(' ');
var fieldName = summary[0]; // Toplam türü SQL fonksiyon adı olarak yazılır; yalnızca bilinen beş tür kabul edilir.
var summmaryType = summary[1]; // asc, desc if (summary.Length != 2 || !SummaryTypes.Contains(summary[1]))
fieldName = ConvertAliasToFieldName(fieldName, listFormFields); {
if (fieldName == "undefined") continue;
}
var summmaryType = summary[1];
var fieldName = ConvertAliasToFieldName(summary[0], listFormFields);
if (fieldName == UnknownFieldName)
{ {
continue; 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}\""); 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() private string GetSelectString()
{ {
return $"SELECT {string.Join(',', SelectFields.Select(c => $"{c.SelectFieldName}"))} FROM {From} AS \"{TableName}\""; return $"SELECT {string.Join(',', SelectFields.Select(c => $"{c.SelectFieldName}"))} FROM {From} AS \"{TableName}\"";
@ -353,55 +372,16 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
if (!string.IsNullOrEmpty(queryParams?.Filter)) if (!string.IsNullOrEmpty(queryParams?.Filter))
{ {
var filters = queryParams.Filter.Replace('[', '(') SelectQueryParameters = new Dictionary<string, object>();
.Replace(']', ')') filterParameterIndex = 0;
.Replace("\"\"", "''") // (empty string) "" -> ''
.Replace("\"", "")
.Replace(",\"or\",", " or ")
.Replace(",\"and\",", " and ");
var filterItems = filters.Split(",").ToList(); var clientFilter = BuildClientFilter(queryParams.Filter, listFormFields);
if (filterItems if (clientFilter != null)
.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>(); // İ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ı.
var forDeger = 3; whereParts.Add($"({clientFilter})");
IsAppliedGridFilter = true;
for (int i = 0; i < filterItems.Count; i += forDeger)
{
if (filterItems[i].Contains('!'))
{
forDeger = 1;
whereParts.Add(filterItems[i].Replace("!", "NOT"));
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;
}
}
}
} }
} }
@ -527,42 +507,188 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
return (value ?? string.Empty).Replace("'", "''"); return (value ?? string.Empty).Replace("'", "''");
} }
/// <summary> FieldName operator Id seklinde olan filtreleri ayirir private const int MaxFilterDepth = 32;
/// </summary>
/// <param name="filter"></param> private static readonly HashSet<string> FilterOperators = new(StringComparer.OrdinalIgnoreCase)
/// <returns></returns>
private string ParseFilter(
string fieldName,
string op,
string fieldValue,
List<ListFormField> listFormFields,
int customKey)
{ {
string sqlPart = string.Empty; "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith",
if (string.IsNullOrWhiteSpace(fieldName) || string.IsNullOrWhiteSpace(op)) };
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>
private string BuildClientFilter(string filterJson, List<ListFormField> listFormFields)
{
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 fieldFilter = fieldName.Split('.');
var filterFieldName = fieldFilter.First().RemoveParentheses(); var filterFieldName = fieldFilter.First();
var filterGroupInterval = fieldFilter.Length > 1 ? fieldFilter.Last().ToLower() : null; var filterGroupInterval = fieldFilter.Length > 1 ? fieldFilter.Last().ToLower() : null;
var filterParamKey = $"@{filterFieldName}{customKey}";
var filterParamKeyLower = filterParamKey;
var listFormField = listFormFields.FirstOrDefault(c => c.FieldName == filterFieldName); var listFormField = listFormFields.FirstOrDefault(c => c.FieldName == filterFieldName);
// Anahtar alan her sorguya eklenir ama alan listesinde olmayabilir; grid satira odaklanirken // Anahtar alan her sorguya eklenir ama alan listesinde olmayabilir; grid satira odaklanirken
// filtreye anahtar kosulu yazar. // filtreye anahtar kosulu yazar.
var isKeyField = listFormField == null && filterFieldName == KeyFieldName; var isKeyField = listFormField == null && filterFieldName == KeyFieldName;
if (listFormField == null && !isKeyField) if (listFormField == null && !isKeyField)
{ {
// Kosul dusurulurse etrafindaki baglac ve parantezler yetim kalir (WHERE and (...))); return "1 = 1";
// yerine parantezleri koruyan etkisiz bir kosul yazilir.
var leading = fieldName[..^fieldName.TrimStart('(').Length];
var trailing = fieldValue[fieldValue.TrimEnd(')').Length..];
return $"{leading}1 = 1{trailing}";
} }
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, // filterGroupInterval sayı olduğu için (örn OrderDate.Month), eğer bu gelirse sayıya parse ediyoruz,
// diğer durumlarda field tipine parse ediyoruz // diğer durumlarda field tipine parse ediyoruz
DbType dbType = filterGroupInterval switch DbType dbType = filterGroupInterval switch
@ -571,13 +697,12 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
_ => isKeyField ? KeyFieldDbSourceType : listFormField.SourceDbType _ => isKeyField ? KeyFieldDbSourceType : listFormField.SourceDbType
}; };
var filterValue = fieldValue.RemoveParentheses(); var filterValue = fieldValue;
if (filterValue == "null") if (filterValue == "null")
{ {
if (filterOperator == "=") filterOperator = "isblank"; if (filterOperator == "=") filterOperator = "isblank";
if (filterOperator == "<>") filterOperator = "isnotblank"; if (filterOperator == "<>") filterOperator = "isnotblank";
} }
var lastCharacters = fieldValue.Replace(filterValue, string.Empty); // Sadece parantezleri almak için
var filterSqlFieldName = ConvertAliasToFieldName(filterFieldName, listFormFields, filterGroupInterval); var filterSqlFieldName = ConvertAliasToFieldName(filterFieldName, listFormFields, filterGroupInterval);
if (filterOperator == "=" && DataSourceType == DataSourceTypeEnum.Postgresql && ( if (filterOperator == "=" && DataSourceType == DataSourceTypeEnum.Postgresql && (
@ -588,7 +713,6 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
filterParamKeyLower = $"lower({filterParamKeyLower})"; filterParamKeyLower = $"lower({filterParamKeyLower})";
filterValue = filterValue.Replace('I', 'ı'); filterValue = filterValue.Replace('I', 'ı');
} }
filterSqlFieldName = fieldFilter.First().Replace(filterFieldName, filterSqlFieldName);
var sqlOperator = ""; var sqlOperator = "";
string formatSql = "{0} {1} {2}"; 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 == "isblank") formatSql = "{0} IS NULL";
else if (filterOperator == "isnotblank") formatSql = "{0} IS NOT NULL"; else if (filterOperator == "isnotblank") formatSql = "{0} IS NOT NULL";
sqlPart = string.Format(formatSql, filterSqlFieldName, sqlOperator, filterParamKeyLower); var sqlPart = string.Format(formatSql, filterSqlFieldName, sqlOperator, filterParamKeyLower);
if (lastCharacters.Any())
{
sqlPart += lastCharacters;
}
if (formatSql.Contains("{2}")) if (formatSql.Contains("{2}"))
{ {
@ -743,9 +863,18 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
var isExpanded = items.Length > 2 && items[2] == "true"; var isExpanded = items.Length > 2 && items[2] == "true";
var groupInterval = items.Length > 3 ? validGroupIntervals.Contains(items[3]) ? items[3] : null : null; 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 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)); 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, // gruplanan bir alan icin grup satirinda summary bilgisinin gorunmesi icin gerekli veri yapisidir,
// grup sorgusu ile birlikte calistirilir. // grup sorgusu ile birlikte calistirilir.
// Gelen veri yapisi: FieldName SummaryType|FieldName SummaryType // Gelen veri yapisi: FieldName SummaryType|FieldName SummaryType
@ -768,6 +897,10 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
continue; continue;
} }
var fieldName = ConvertAliasToFieldName(field, listFormFields);// Alias li bir alan ise gercek alani bul var fieldName = ConvertAliasToFieldName(field, listFormFields);// Alias li bir alan ise gercek alani bul
if (fieldName == UnknownFieldName)
{
continue;
}
GroupSummaryTuples.Add((i, fieldName, $"Summary_{i}", summaryType)); 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 Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy;
public class DynamicEntityManager : IDynamicEntityManager 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 IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly ICurrentTenant _currentTenant;
public DynamicEntityManager( public DynamicEntityManager(
IDbContextProvider<PlatformDbContext> dbContextProvider) IDbContextProvider<PlatformDbContext> dbContextProvider,
ICurrentTenant currentTenant)
{ {
_dbContextProvider = dbContextProvider; _dbContextProvider = dbContextProvider;
_currentTenant = currentTenant;
} }
public async Task<List<object>?> GetEntityListAsync( public async Task<List<object>?> GetEntityListAsync(
@ -35,6 +51,10 @@ public class DynamicEntityManager : IDynamicEntityManager
conditions.Add($"({idDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {idDeletedColumn} IS NULL)"); conditions.Add($"({idDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {idDeletedColumn} IS NULL)");
var parameters = new List<object>(); var parameters = new List<object>();
var tenantCondition = await BuildTenantConditionAsync(entityName, isPostgreSql, parameters);
if (tenantCondition != null)
conditions.Add(tenantCondition);
if (filters is { Count: > 0 }) if (filters is { Count: > 0 })
{ {
var columns = await GetColumnNamesAsync(entityName); var columns = await GetColumnNamesAsync(entityName);
@ -49,7 +69,7 @@ public class DynamicEntityManager : IDynamicEntityManager
var query = conditions.Count > 0 var query = conditions.Count > 0
? $"SELECT * FROM {tableName} WHERE {string.Join(" AND ", conditions)}" ? $"SELECT * FROM {tableName} WHERE {string.Join(" AND ", conditions)}"
: $"SELECT * FROM {tableName}"; : $"SELECT * FROM {tableName}";
return await ExecuteRawQueryAsync(query, parameters); return RemoveSecretColumns(await ExecuteRawQueryAsync(query, parameters));
} }
/// <summary> /// <summary>
@ -196,11 +216,18 @@ public class DynamicEntityManager : IDynamicEntityManager
var idColumn = QuoteIdentifier("Id", isPostgreSql); var idColumn = QuoteIdentifier("Id", isPostgreSql);
var isDeletedColumn = QuoteIdentifier("IsDeleted", isPostgreSql); var isDeletedColumn = QuoteIdentifier("IsDeleted", isPostgreSql);
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted"); var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var query = hasIsDeleted
? $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}' AND ({isDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {isDeletedColumn} IS NULL)" var parameters = new List<object>();
: $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}'"; var conditions = new List<string> { $"{idColumn} = {AddParameter(parameters, id)}" };
var result = await ExecuteRawQueryAsync(query); if (hasIsDeleted)
return result?.FirstOrDefault(); 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) 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 (hasCreationTime) { columns.Add(QuoteIdentifier("CreationTime", isPostgreSql)); values.Add(UtcNowExpression(isPostgreSql)); }
if (hasIsDeleted) { columns.Add(QuoteIdentifier("IsDeleted", isPostgreSql)); values.Add(FalseLiteral(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()) foreach (var prop in data.EnumerateObject())
{ {
if (prop.NameEquals("id") || prop.NameEquals("Id")) if (IsProtectedColumn(prop.Name))
continue; continue;
columns.Add(QuoteIdentifier(prop.Name, isPostgreSql)); columns.Add(QuoteIdentifier(prop.Name, isPostgreSql));
@ -251,7 +285,7 @@ public class DynamicEntityManager : IDynamicEntityManager
foreach (var prop in data.EnumerateObject()) foreach (var prop in data.EnumerateObject())
{ {
if (prop.NameEquals("id") || prop.NameEquals("Id")) if (IsProtectedColumn(prop.Name))
continue; continue;
setParts.Add($"{QuoteIdentifier(prop.Name, isPostgreSql)} = {AddParameter(parameters, ToSqlParameterValue(prop.Value))}"); 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) private async Task<bool> ColumnExistsAsync(string tableName, string columnName)
{ {
var dbContext = await _dbContextProvider.GetDbContextAsync(); 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> </PropertyGroup>
<ItemGroup> <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.Languages\Sozsoft.Languages.EntityFrameworkCore\Sozsoft.Languages.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Sozsoft.Notifications\Sozsoft.Notifications.EntityFrameworkCore\Sozsoft.Notifications.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" /> <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.SqlServer" Version="10.0.0" />
<PackageReference Include="Volo.Abp.EntityFrameworkCore.PostgreSql" 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="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.PermissionManagement.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Volo.Abp.Identity.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" /> <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;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -10,12 +11,14 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Data;
using Sozsoft.Platform.DbStartup; using Sozsoft.Platform.DbStartup;
using Sozsoft.Platform.EntityFrameworkCore; using Sozsoft.Platform.EntityFrameworkCore;
using Sozsoft.Platform.Permissions; using Sozsoft.Platform.Permissions;
using Volo.Abp.DistributedLocking; using Volo.Abp.DistributedLocking;
using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.TenantManagement;
using Volo.Abp.Uow; using Volo.Abp.Uow;
namespace Sozsoft.Platform.Controllers; namespace Sozsoft.Platform.Controllers;
@ -46,6 +49,7 @@ public class SetupController : ControllerBase
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly IAbpDistributedLock _distributedLock; private readonly IAbpDistributedLock _distributedLock;
private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly ITenantRepository _tenantRepository;
private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer; private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer;
private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer; private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer;
private readonly ILogger<SetupController> _logger; private readonly ILogger<SetupController> _logger;
@ -58,6 +62,7 @@ public class SetupController : ControllerBase
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IAbpDistributedLock distributedLock, IAbpDistributedLock distributedLock,
IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkManager unitOfWorkManager,
ITenantRepository tenantRepository,
BackgroundWorkerInitializer backgroundWorkerInitializer, BackgroundWorkerInitializer backgroundWorkerInitializer,
AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer, AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer,
ILogger<SetupController> logger) ILogger<SetupController> logger)
@ -69,6 +74,7 @@ public class SetupController : ControllerBase
_currentTenant = currentTenant; _currentTenant = currentTenant;
_distributedLock = distributedLock; _distributedLock = distributedLock;
_unitOfWorkManager = unitOfWorkManager; _unitOfWorkManager = unitOfWorkManager;
_tenantRepository = tenantRepository;
_backgroundWorkerInitializer = backgroundWorkerInitializer; _backgroundWorkerInitializer = backgroundWorkerInitializer;
_abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer; _abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer;
_logger = logger; _logger = logger;
@ -108,12 +114,65 @@ public class SetupController : ControllerBase
if (!authorized) if (!authorized)
{ {
_logger.LogWarning("Database migration request denied: {Reason}", denialReason); _logger.LogWarning("Database migration request denied: {Reason}", denialReason);
Response.StatusCode = StatusCodes.Status403Forbidden; await WritePlainTextAsync(StatusCodes.Status403Forbidden, denialReason);
Response.ContentType = "text/plain; charset=utf-8";
await Response.WriteAsync(denialReason);
return; 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); var stream = new SetupEventStream(Response);
await stream.StartAsync(); await stream.StartAsync();
@ -130,13 +189,9 @@ public class SetupController : ControllerBase
return; 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. // ApplicationStopping kullanılıyor: istemci sekmesini kapatsa da migration tamamlanır.
var outcome = await SetupMigrationRunner.ExecuteAsync( var outcome = await SetupMigrationRunner.ExecuteAsync(
_configuration, _env.ContentRootPath, stream, _lifetime.ApplicationStopping); _configuration, _env.ContentRootPath, stream, _lifetime.ApplicationStopping, environment);
if (outcome != MigrationOutcome.Succeeded) if (outcome != MigrationOutcome.Succeeded)
{ {
@ -144,6 +199,13 @@ public class SetupController : ControllerBase
return; return;
} }
if (!reloadWorkers)
{
await stream.SuccessAsync("Migration and seed completed successfully.");
await stream.DoneAsync("Completed.");
return;
}
await stream.InfoAsync("Migration and seed completed successfully."); await stream.InfoAsync("Migration and seed completed successfully.");
// Yeni/değişen tanımlar Hangfire'a yansısın diye worker'lar tazelenir. // 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."); 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 // Yetkilendirme
/// <summary> /// <summary>
@ -215,6 +284,27 @@ public class SetupController : ControllerBase
: (false, $"The '{PlatformPermissions.Setup.Migrate}' permission is required."); : (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() private async Task<bool> CanMigrateAsync()
{ {
if (User?.Identity?.IsAuthenticated != true) 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;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; 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 /// 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. /// kapattığında migration yarıda kesilir ve veritabanı tutarsız kalır.
/// </param> /// </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( public static async Task<MigrationOutcome> ExecuteAsync(
IConfiguration configuration, IConfiguration configuration,
string contentRootPath, string contentRootPath,
SetupEventStream stream, SetupEventStream stream,
CancellationToken processCancellationToken) CancellationToken processCancellationToken,
IReadOnlyDictionary<string, string>? environment = null)
{ {
if (!await ExecutionGate.WaitAsync(0, CancellationToken.None)) if (!await ExecutionGate.WaitAsync(0, CancellationToken.None))
{ {
@ -57,7 +62,7 @@ public static class SetupMigrationRunner
try try
{ {
return await ExecuteCoreAsync(configuration, contentRootPath, stream, processCancellationToken); return await ExecuteCoreAsync(configuration, contentRootPath, stream, processCancellationToken, environment);
} }
finally finally
{ {
@ -69,7 +74,8 @@ public static class SetupMigrationRunner
IConfiguration configuration, IConfiguration configuration,
string contentRootPath, string contentRootPath,
SetupEventStream stream, SetupEventStream stream,
CancellationToken processCancellationToken) CancellationToken processCancellationToken,
IReadOnlyDictionary<string, string>? environment)
{ {
await stream.InfoAsync("Database migration and seeding are being initiated..."); await stream.InfoAsync("Database migration and seeding are being initiated...");
@ -112,6 +118,14 @@ public static class SetupMigrationRunner
process.StartInfo.Environment[SeedsPathEnvironmentVariable] = process.StartInfo.Environment[SeedsPathEnvironmentVariable] =
SeedPathResolver.GetRootPath(configuration); SeedPathResolver.GetRootPath(configuration);
if (environment is not null)
{
foreach (var (name, value) in environment)
{
process.StartInfo.Environment[name] = value;
}
}
process.Start(); process.Start();
async Task PumpAsync(StreamReader reader, string level) async Task PumpAsync(StreamReader reader, string level)

View file

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

View file

@ -9,14 +9,14 @@ using Sozsoft.Platform.Identity;
using Sozsoft.Platform.Public; using Sozsoft.Platform.Public;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.Users; using Volo.Abp.Users;
namespace Sozsoft.Platform.Extensions; namespace Sozsoft.Platform.Extensions;
public class PlatformApplicationConfigurationContributor : IApplicationConfigurationContributor, ITransientDependency public class PlatformApplicationConfigurationContributor : IApplicationConfigurationContributor
{ {
public async Task ContributeAsync(ApplicationConfigurationContributorContext context) 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 // 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, // ChangeLog ekranının kendi endpoint'inden (/api/app/change-log) gelir,
// aksi halde her config isteğinde bütün changelog gönderilirdi. // 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 changeLogRepository = context.ServiceProvider.GetRequiredService<IRepository<ChangeLog, Guid>>();
var changeLogs = await changeLogRepository.GetListAsync(x => x.IsPublished); var asyncExecuter = context.ServiceProvider.GetRequiredService<IAsyncQueryableExecuter>();
var latest = await asyncExecuter.FirstOrDefaultAsync((await changeLogRepository.GetQueryableAsync())
var latest = changeLogs .Where(note => note.IsPublished)
.OrderByDescending(note => note.ReleaseDate) .OrderByDescending(note => note.ReleaseDate)
.ThenByDescending(note => note.Version) .ThenByDescending(note => note.Version));
.FirstOrDefault();
context.ApplicationConfiguration.SetProperty("changeLogs", new context.ApplicationConfiguration.SetProperty("changeLogs", new
{ {
@ -85,11 +85,16 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
if (currentUser.Id.HasValue) if (currentUser.Id.HasValue)
{ {
var sessionRepository = context.ServiceProvider.GetRequiredService<IIdentitySessionRepository>(); var sessionRepository = context.ServiceProvider.GetRequiredService<IIdentitySessionRepository>();
var sessions = await sessionRepository.GetListAsync(userId: currentUser.Id.Value);
var claimSessionId = currentUser.FindClaim(PlatformClaimTypes.SessionId.Key)?.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 ? 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.CurrentUser.SessionId = sessionId;
context.ApplicationConfiguration.SetProperty("currentUserSessionId", 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.Identity;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
@ -134,7 +133,6 @@ public class PlatformHttpApiHostModule : AbpModule
//Tenant bazında lokalizasyon ayarları için //Tenant bazında lokalizasyon ayarları için
//TenantLocalization Middleware kaydı //TenantLocalization Middleware kaydı
context.Services.AddTransient<TenantLocalizationMiddleware>(); context.Services.AddTransient<TenantLocalizationMiddleware>();
context.Services.AddTransient<TenantLocalizationInitializer>();
ConfigureAuthentication(context); ConfigureAuthentication(context);
ConfigureBundles(); ConfigureBundles();
@ -258,18 +256,19 @@ public class PlatformHttpApiHostModule : AbpModule
private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration) 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 => context.Services.AddCors(options =>
{ {
options.AddDefaultPolicy(builder => options.AddDefaultPolicy(builder =>
{ {
builder builder
.WithOrigins(configuration["App:CorsOrigins"]? .SetIsOriginAllowed(originValidator.IsAllowed)
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray() ?? Array.Empty<string>())
.WithAbpExposedHeaders() .WithAbpExposedHeaders()
.WithExposedHeaders(["X-Correlation-Id"]) .WithExposedHeaders(["X-Correlation-Id"])
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod() .AllowAnyMethod()
.AllowCredentials(); .AllowCredentials();
@ -335,9 +334,6 @@ public class PlatformHttpApiHostModule : AbpModule
{ {
options.KeyPrefix = PlatformConsts.AppName; options.KeyPrefix = PlatformConsts.AppName;
}); });
Configure<RedisCacheOptions>(options =>
{
});
} }
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
@ -483,6 +479,10 @@ public class PlatformHttpApiHostModule : AbpModule
// Setup delegate for dynamic service unregistration // Setup delegate for dynamic service unregistration
DynamicServiceCompiler.NotifyAssemblyUnregistration = DynamicAssemblyRegistrationService.RequestAssemblyUnregistration; 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()) if (env.IsDevelopment())
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
@ -500,7 +500,6 @@ public class PlatformHttpApiHostModule : AbpModule
app.UseResponseCompression(); app.UseResponseCompression();
app.MapAbpStaticAssets(); app.MapAbpStaticAssets();
app.UseRouting(); app.UseRouting();
app.UseCors();
// Not: /api/setup/application-status artık SetupController tarafından karşılanıyor. // Not: /api/setup/application-status artık SetupController tarafından karşılanıyor.
// Buradaki kopya middleware kaldırıldı — durum mantığı tek yerde (DatabaseReadinessChecker). // 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;
using Serilog.Sinks.PostgreSQL; using Serilog.Sinks.PostgreSQL;
using Sozsoft.Platform.DbStartup; using Sozsoft.Platform.DbStartup;
using Sozsoft.Platform.DynamicServices;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using static Sozsoft.Settings.SettingsConsts; using static Sozsoft.Settings.SettingsConsts;
@ -111,78 +110,9 @@ public class Program
.UseSerilog(); .UseSerilog();
await builder.AddApplicationAsync<PlatformHttpApiHostModule>(); 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(); var app = builder.Build();
app.UseDevExpressControls(); app.UseDevExpressControls();
app.UseCors("Dynamic");
// Dynamic Assembly Registration Delegate Setup
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
DynamicServiceCompiler.NotifyAssemblyUnregistration = DynamicAssemblyRegistrationService.RequestAssemblyUnregistration;
await app.InitializeApplicationAsync(); await app.InitializeApplicationAsync();
await app.RunAsync(); await app.RunAsync();
return 0; return 0;

View file

@ -27,7 +27,6 @@
<PackageReference Include="Hangfire.PostgreSql" Version="1.20.10" /> <PackageReference Include="Hangfire.PostgreSql" Version="1.20.10" />
<PackageReference Include="Hangfire.SqlServer" Version="1.8.21" /> <PackageReference Include="Hangfire.SqlServer" Version="1.8.21" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <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.Settings.Configuration" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.PostgreSQL" Version="2.3.0" /> <PackageReference Include="Serilog.Sinks.PostgreSQL" Version="2.3.0" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="8.2.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.Globalization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Settings; using Volo.Abp.Localization;
using Volo.Abp.SettingManagement;
using Volo.Abp.Timing; using Volo.Abp.Timing;
namespace Sozsoft.Platform.Localization; 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 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; private readonly IOptions<AbpClockOptions> _clockOptions;
public TenantLocalizationInitializer( public TenantLocalizationInitializer(
ISettingProvider settingProvider, ISettingManager settingManager,
IOptions<AbpClockOptions> clockOptions) IOptions<AbpClockOptions> clockOptions)
{ {
_settingProvider = settingProvider; _settingManager = settingManager;
_clockOptions = clockOptions; _clockOptions = clockOptions;
} }
public async Task ApplyTenantSettingsAsync() public async Task ApplyTenantSettingsAsync()
{ {
// Dil ayarı var now = Environment.TickCount64;
var lang = await _settingProvider.GetOrNullAsync("Abp.Localization.DefaultLanguage") ?? "en"; 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 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.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture; CultureInfo.DefaultThreadCurrentUICulture = culture;
} }
catch }
{
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
}
// Saat tipi (UTC / Local) private void ApplyClockKind(string timeZoneId)
var timeZoneId = await _settingProvider.GetOrNullAsync("Abp.Timing.TimeZone") ?? "UTC"; {
DateTimeKind kind;
try try
{ {
var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
_clockOptions.Value.Kind = tz.Id.Contains("UTC", StringComparison.OrdinalIgnoreCase) kind = timeZone.Id.Contains("UTC", StringComparison.OrdinalIgnoreCase)
? DateTimeKind.Utc ? DateTimeKind.Utc
: DateTimeKind.Local; : 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'.") 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() 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` | | `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` | | 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` | | 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) | | Yetki kodları | `PlatformConsts.AppCodes` (backend) + `constants/permission.constant.ts` (UI) |
| Seed yolları | `SeedPathResolver` | | 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, 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. `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 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, 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 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 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 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 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. 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 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 `ColumnFormatEditDto` (`Fields`) sözleşmesini taşır. ListForm detayından yapılan her kayıt
(`ListFormsAppService`, `ListFormFieldsAppService`, `ListFormJsonRowAppService`) bu bölümleri (`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 - **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` 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 + `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ı 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. 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). `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** 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 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 (`list-form-dynamic-api/notification-insert`). Yeni kiracıya varsayılan tür/kural kümesi
kopyalanır (`NotificationTenantDefaultsSeedContributor`); kaynak host kapsamındaki seed gelir: kendi veritabanı olan kiracı seed dosyasından alır, host veritabanını paylaşana
dosyalarıdır, satırlar yeni kimliklerle yazılır. Kiracı verisine dokunan zamanlanmış işin `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ş `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`). kiracı satırlarını göremez (`WorkerScopeEnum`, `PlatformBackgroundWorker`).
- **Bildirim bayrakları** → sıra değişmez: `IsSent` (iletildi) olmadan `IsRead` yazılmaz, `IsRead` - **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 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` + eklenir, yoksa sayfa sitemap'te görünmez. Yeni bir meta alanı = iki entity + `SeoRouteDto` +
`SeoFields` (seeder) + `SeoHead` birlikte. `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 `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. olmadığını kontrol et.
- **Custom component sınıfları** → bileşen kodu veritabanında yaşar, Tailwind derlemede yalnızca - **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`) 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. doldurulur, istemci tablo tasarımcısını buna göre açar.
- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete` - **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete`
bayraklarından işaretli olan işlemler kapsam klasöründeki hedefe yansır bayraklarından işaretli olan işlemler seed klasöründeki hedefe yansır
(`ListFormSeedDataSynchronizer`). Hedef iki biçimdedir ve ayrım uzantıdadır: `.json` ile biten (`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 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 **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 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 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 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. 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ı - **Seed dosyası yazmak** → veri dosyası `ListFormSeedDataFileDto.Serialize`, wizard dosyası
`WizardSeedFileDto.ToSeedJson` ile yazılır; düz `JsonSerializer.Serialize` + `File.WriteAllText` `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 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` 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 (`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. 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 `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` 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 → 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. tipler düz metin aramasında referanssız görünür.
- Yorum satırına alınmış kod bloğu bırakılmaz. - 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ı - 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). ayrıntılıdır (standing default 14–15).
- `api/` değişikliğinden sonra: `dotnet build -p:EnforceCodeStyleInBuild=true` — 0 hata, yeni - `api/` değişikliğinden sonra: `dotnet build -p:EnforceCodeStyleInBuild=true` — 0 hata, yeni
`IDE00xx` yok. `IDE00xx` yok.

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