diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md index 0dc387d..e4a811c 100644 --- a/soneta-programming/SKILL.md +++ b/soneta-programming/SKILL.md @@ -13,6 +13,8 @@ description: > Skill zawiera dokumentację fundamentalnych klas logiki biznesowej platformy enova365/Soneta Enterprise. Klasy te stanowią podstawę mapowania obiektowo-relacyjnego (ORM) i są niezbędne do tworzenia kodu i dodatków. +> **Code review / refaktoring:** po napisaniu nowego kodu biznesowego oraz po każdym refaktoringu, **zawsze** zweryfikuj go względem [references/safe-code.md](references/safe-code.md). Ten sam dokument służy jako lista kontrolna do review PR-ów. + ## Mapa skilla SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzorce. Po szczegóły konkretnego tematu sięgaj do referencji: @@ -32,6 +34,7 @@ SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzo | ViewInfo - definicja widoków list (folderów), CreateView, klasa Params, powiązanie z viewform.xml | [references/viewinfo.md](references/viewinfo.md) | | Cechy (Features) - tabela Features, typy cech, dostęp typowany/nietypowany, bindowanie w form.xml | [references/features.md](references/features.md) | | Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) | +| **Zasady bezpiecznego kodu biznesowego — checklist do review i refaktoringu** | [references/safe-code.md](references/safe-code.md) | | Skanowanie pól obiektu biznesowego z DLL (Roslyn MetadataReference) | [references/scan-props.md](references/scan-props.md) | | Inwentaryzacja modułów i tabel (`*Module` / `*Row` / `*Table`) z DLL | [references/scan-modules.md](references/scan-modules.md) | diff --git a/soneta-programming/references/safe-code.md b/soneta-programming/references/safe-code.md new file mode 100644 index 0000000..a56cc94 --- /dev/null +++ b/soneta-programming/references/safe-code.md @@ -0,0 +1,393 @@ +# Safe Code - Zasady bezpiecznego kodu biznesowego Soneta + +Lista zasad, które należy weryfikować **po każdym refaktoringu** oraz **przy każdym code review / PR** kodu biznesowego enova365 / Soneta Enterprise. Każda zasada jest sformułowana jako kontrolne pytanie — odpowiedź "nie" oznacza realne ryzyko (utrata danych, niespójność, race condition, błędna logika). + +## Spis treści + +- [1. Sesje i transakcje](#1-sesje-i-transakcje) +- [2. Mieszanie sesji i obiektów](#2-mieszanie-sesji-i-obiektów) +- [3. Thread-safety](#3-thread-safety) +- [4. Optimistic locking i konflikty](#4-optimistic-locking-i-konflikty) +- [5. Walidacja danych](#5-walidacja-danych) +- [6. Filtrowanie po stronie serwera](#6-filtrowanie-po-stronie-serwera) +- [7. Kod biznesowy vs UI](#7-kod-biznesowy-vs-ui) +- [8. ExecuteConfig - dane konfiguracyjne](#8-executeconfig---dane-konfiguracyjne) +- [9. Obsługa wyjątków i rollback](#9-obsługa-wyjątków-i-rollback) +- [10. Typy biznesowe](#10-typy-biznesowe) +- [11. Tłumaczenia i komunikaty](#11-tłumaczenia-i-komunikaty) +- [12. Logowanie](#12-logowanie) +- [13. Wydajność jako bezpieczeństwo](#13-wydajność-jako-bezpieczeństwo) +- [14. Czystość API publicznego](#14-czystość-api-publicznego) +- [15. Code review checklist (TL;DR)](#15-code-review-checklist-tldr) + +--- + +## 1. Sesje i transakcje + +### 1.1 Każda sesja w `using` + +Sesja implementuje `IDisposable` — bez `using` zostanie ona w pamięci do GC, blokując zasoby i potencjalnie powodując nieskończony wzrost zużycia pamięci. + +```csharp +// ŹLE +var session = login.CreateSession(readOnly: false, config: false, name: "Op"); +// ... brak Dispose + +// DOBRZE +using (var session = login.CreateSession(readOnly: false, config: false, name: "Op")) { + // ... +} +``` + +--- + +## 2. Mieszanie sesji i obiektów + +### 2.1 Nigdy nie używaj `Row` z innej sesji bezpośrednio + +Każdy obiekt biznesowy żyje tylko w swojej sesji. Przekazanie go do innej sesji powoduje nieprzewidywalne zachowanie (stary stan, niespójne klucze). + +```csharp +// ŹLE +faktura.Kontrahent = kontrahentZInnejSesji; + +// DOBRZE +faktura.Kontrahent = session.Get(kontrahentZInnejSesji); +``` + +### 2.2 Nie zwracaj obiektów sesyjnych poza `using` + +```csharp +// ŹLE - Towar po wyjściu z using jest "martwy" +public Towar PobierzTowar(Login login, string kod) { + using (var session = login.CreateSession(true, false, "Pobierz")) { + return session.GetTowary().Towary.WgKodu[kod]; + } +} +``` + +Zwracaj DTO / prymitywy, albo niech wywołujący kontroluje cykl życia sesji. + +--- + +## 3. Thread-safety + +### 3.1 Lista współdzielonych vs niewspółdzielonych + +| Można używać jednocześnie w wielu wątkach | Nie wolno używać jednocześnie w wielu wątkach | +|-------------------------------------------|------------------------------------------------| +| `BusApplication`, `Database`, `Login` | `Session`, `Module`, `Table`, `Row`, `Context` | + +### 3.2 Singleton / statyczne pola - bez sesyjnych referencji + +Statyczne pole trzymające `Session`, `Row`, `Table` to klasyczna pułapka — pierwsza sesja "zawłaszcza" pole i kolejne wątki widzą cudze dane. + +--- + +## 4. Optimistic locking i konflikty + +### 4.1 Konflikty wykrywane w `Save()` + +Soneta używa optimistic concurrency. Konflikt edycji (ktoś inny zapisał ten sam rekord) wybucha jako wyjątek z `session.Save()`. Jeśli kod łapie generyczny `Exception` i kontynuuje, użytkownik dostaje "zapisano" mimo że dane nie poszły do bazy. + +```csharp +try { + session.Save(); +} +catch (RowConflictException ex) { + // świadoma obsługa: refresh + powtórzenie lub eskalacja do użytkownika +} +``` + +### 4.2 Nie ignoruj wyjątku z `Save()` + +`Save()` zgłaszający wyjątek = **nie zapisano**. Każdy taki przypadek musi mieć dedykowaną obsługę (retry, log, komunikat). Połknięcie wyjątku = utrata danych. + +--- + +## 5. Walidacja danych + +### 5.1 Walidacja przed `transaction.Commit()` + +Wyjątek po `Commit()` ale przed `Save()` nie wycofuje zmian z bieżącej sesji — tylko z bazy. Następne `Save()` w tej samej sesji może je przepuścić niezauważenie. Walidacja musi rzucić wyjątek **przed** `Commit()` lub w event-handlerze `SavingRow`. + +### 5.2 Komunikaty walidacyjne przez `Translate` + +```csharp +throw new RowException(this, "Pole {0} jest wymagane".Translate(), nameof(Nazwa)); +``` + +--- + +## 6. Filtrowanie po stronie serwera + +### 6.1 Nie iteruj całej tabeli z `if` w pamięci + +```csharp +// ŹLE - ściąga całą tabelę +foreach (Towar t in tm.Towary.WgKodu) { + if (t.Aktywny && t.Cena > 100) { ... } +} + +// DOBRZE - warunek wykonany przez serwer +foreach (Towar t in tm.Towary[(Towar t) => t.Aktywny && t.Cena > 100]) { ... } +``` + +Szczegóły wyrażeń `RowCondition` (LINQ-like, predykaty, kompozycja) — patrz [rowcondition.md](rowcondition.md). + +### 6.2 Nie używaj `View` w kodzie biznesowym + +`View` należy do warstwy UI. W kodzie biznesowym używaj `SubTable[condition]`. Szczegóły — patrz [viewinfo.md](viewinfo.md). + +### 6.3 Nie wczytuj całych tabel kartotekowych ani operacyjnych + +Tabele dzielą się na trzy kategorie pod kątem rozmiaru i bezpieczeństwa pełnego skanu: + +| Kategoria | Przykłady | Pełny skan? | +|-----------|-----------|-------------| +| **Konfiguracyjne** (stałe, niewielkie) | `Jednostki`, `DefinicjeDokumentow`, `Stawki VAT` | **Dozwolony** | +| **Kartotekowe** (rosną z biznesem) | `Towary`, `Kontrahenci`, `Pracownicy` | **Zabroniony** bez filtra | +| **Operacyjne** (rosną w czasie, guided) | `DokHandlowe`, `Zapisy`, `WyplatyElementy` | **Zabroniony** — wymagany zakres czasowy | + +**Reguła dla tabel operacyjnych guided** (zawierających daty): dostęp prawie zawsze musi być ograniczony do określonego okresu. + +```csharp +// ŹLE - pobiera wszystkie dokumenty od początku istnienia bazy +foreach (DokumentHandlowy d in hm.DokHandlowe.WgNumeru) { ... } + +// DOBRZE - zakres czasowy +var od = new Date(2026, 1, 1); +var doD = Date.Today; +foreach (DokumentHandlowy d in hm.DokHandlowe[(DokumentHandlowy d) => d.Data >= od && d.Data <= doD]) { ... } +``` + +**Reguła dla danych operacyjnych nie-guided** (pozycje, składniki, zapisy szczegółowe): używaj ich w zakresie obiektu guided root (np. pozycje konkretnego dokumentu, składniki konkretnej wypłaty). Jeśli musisz iterować poprzecznie, **dodaj filtr czasowy analogiczny do tabel guided**. + +```csharp +// DOBRZE - w zakresie roota +foreach (PozycjaDokHandlowego p in faktura.Pozycje) { ... } + +// ŹLE - iteracja po wszystkich pozycjach historycznych +foreach (PozycjaDokHandlowego p in hm.PozycjeDokHan.WgDokument) { ... } + +// AKCEPTOWALNE - poprzeczna iteracja z filtrem czasowym (przez root) +var od = new Date(2026, 1, 1); +foreach (DokumentHandlowy d in hm.DokHandlowe.WgDaty[d => d.Data >= od]) { + foreach (var p in d.Pozycje) { ... } +} +``` + +--- + +## 7. Kod biznesowy vs UI + +### 7.1 Brak referencji do UI w kodzie biznesowym + +Kod, który będzie wywoływany z workerów, schedulera, importu, API — **nie może** używać: +- `View`, `ViewInfo` (zamiast tego: `SubTable[condition]`) +- `IsReadOnlyXxx`, `IsVisibleXxx`, `IsEnabledXxx`, `GetListXxx`, `GetAppearanceXxx` +- okien dialogowych, `MessageBox`, `IUIServices` + +### 7.2 Nie sprawdzaj `AccessRight` w logice biznesowej + +```csharp +// ŹLE - logika biznesowa nie ma znać uprawnień +if (Table.AccessRight == AccessRights.Denied) return; +``` + +Prawa dostępu są warstwą UI / autoryzacji. Logika biznesowa zakłada poprawne wywołanie; egzekucja praw jest gdzie indziej. + +--- + +## 8. ExecuteConfig - dane konfiguracyjne + +### 8.1 Nie zwracaj obiektów sesyjnych z `ExecuteConfig` + +`ExecuteConfig` korzysta z sesji współdzielonej między wątkami. Obiekt zwrócony na zewnątrz może być w międzyczasie unieważniony. + +```csharp +// ŹLE +return login.ExecuteConfig(s => s.GetHandel().DefDokHandlowych.WgSymbolu["ZK"]); + +// DOBRZE - zwracamy prymityw / kopię +return login.ExecuteConfig(s => s.GetHandel().DefDokHandlowych.WgSymbolu["ZK"]?.Kategoria); +``` + +### 8.2 Nie używaj `Session.ConfigSession` — używaj `Session.ExecuteConfig()` + +`Session.ConfigSession` daje bezpośredni uchwyt do sesji konfiguracyjnej, którego cykl życia nie jest kontrolowany przez wywołującego — łatwo o wyciek obiektów sesyjnych poza wątek docelowy lub o korzystanie z nieaktualnego stanu. + +```csharp +// ŹLE +var def = session.ConfigSession.GetHandel().DefDokHandlowych.WgSymbolu["ZK"]; + +// DOBRZE +var kategoria = session.ExecuteConfig(s => + s.GetHandel().DefDokHandlowych.WgSymbolu["ZK"]?.Kategoria); +``` + +`ExecuteConfig` ogranicza zasięg sesji konfiguracyjnej do bloku lambda i wymusza zwracanie prymitywów (patrz §8.1). + +--- + +## 9. Obsługa wyjątków i rollback + +### 9.1 Nie łap `Exception` bez konkretu + +Generyczne `catch (Exception) { /* log */ }` ukrywa konflikty, błędy walidacji, brak praw, błędy I/O. Każdy z nich wymaga innej reakcji. Łap konkretne typy: `RowConflictException`, `RowException`, `BusException`, `LoginException`. + +### 9.2 Nie rzucaj nieokreślonego `Exception` + +`throw new Exception("...")` zmusza wywołującego do łapania `Exception` ogólnie, co (patrz §9.1) tłumi sygnały o realnej przyczynie błędu. Rzucaj **typowane** wyjątki, dobrane do warstwy: + +| Sytuacja | Wyjątek | +|----------|---------| +| Naruszenie reguły biznesowej powiązane z rekordem | `RowException(row, "komunikat".Translate())` | +| Ogólny błąd logiki biznesowej (bez konkretnego wiersza) | `BusException` | +| Nieprawidłowy stan obiektu / nieprawidłowe wywołanie API | `InvalidOperationException` | +| Nieprawidłowy argument | `ArgumentException`, `ArgumentNullException`, `ArgumentOutOfRangeException` | +| Funkcja jeszcze nie obsłużona | `NotSupportedException`, `NotImplementedException` | + +```csharp +// ŹLE +throw new Exception("Brak definicji dokumentu"); + +// DOBRZE - błąd biznesowy bez konkretnego wiersza +throw new BusException("Brak definicji dokumentu {0}".TranslateFormat(symbol)); + +// DOBRZE - błąd walidacyjny związany z wierszem +throw new RowException(faktura, "Pole {0} jest wymagane".Translate(), nameof(faktura.Kontrahent)); + +// DOBRZE - nieprawidłowy stan / niepoprawne wywołanie +throw new InvalidOperationException("Worker wymaga otwartej transakcji edycyjnej"); +``` + +--- + +## 10. Typy biznesowe + +### 10.1 Pieniądze, ilości, daty - dedykowane typy + +| Pojęcie | Typ | NIE używaj | +|---------|-----|------------| +| Kwota walutowa | `Currency`, `DoubleCy` | `decimal`, `double` (gubi walutę) | +| Ilość z jednostką | `Quantity` | `decimal`, `int` (gubi jednostkę) | +| Data biznesowa | `Soneta.Types.Date` | `DateTime` (gubi semantykę "tylko data") | +| Procent | `Percent` | `double` | + +### 10.2 `Date.Today` zamiast `DateTime.Today` + +`Date.Today` honoruje datę biznesową aplikacji (np. zamknięty miesiąc). `DateTime.Today` zwraca datę systemową. + +--- + +## 11. Tłumaczenia i komunikaty + +### 11.1 Każdy łańcuch widoczny dla użytkownika przez `.Translate()` + +```csharp +throw new RowException(this, "Pole jest wymagane".Translate()); +throw new RowException(this, "Pole {0} jest wymagane".TranslateFormat(nameof(Nazwa))); +``` + +### 11.2 Stałe konfiguracyjne nie przez `Translate` + +`Translate` jest dla tekstu UI / komunikatów. Klucze, symbole, kody nie powinny być tłumaczone — to dane techniczne. + +### 11.3 `[TranslateIgnore]` dla pól nie do tłumaczenia + +Szczegóły mechanizmów tłumaczeń — patrz [translations-logging.md](translations-logging.md). + +--- + +## 12. Logowanie + +### 12.1 Nie loguj danych wrażliwych (PII, hasła, tokeny) + +PESEL, NIP w nadmiarze, hasła, klucze API nie trafiają do logów. + +--- + +## 13. Wydajność jako bezpieczeństwo + +Wolny kod = timeouty = nieskończone retry = blokady / data corruption. + +### 13.1 Pętla po tabeli w transakcji edycyjnej - krótka + +Długa transakcja blokuje innych użytkowników na poziomie optimistic-lock (większe szanse na konflikt). Operacja > 30 sekund powinna być dzielona na paczki. + +### 13.2 Filtrowanie serwerowe zamiast `.ToList().Where(...)` + +`ToList()` materializuje całą tabelę do pamięci. To anti-pattern w kodzie biznesowym. + +--- + +## 14. Czystość API publicznego + +### 14.1 Publiczne metody dokumentują kontrakt sesyjny + +Metoda przyjmująca `Login` zarządza sesją sama. Metoda przyjmująca `Session` / `ISessionable` deleguje zarządzanie do wywołującego. **Nie mieszaj** — albo metoda otwiera własną sesję, albo dostaje ją z zewnątrz. + +### 14.2 Brak `static` metod modyfikujących stan biznesowy + +`static void DodajTowar(string kod)` → skąd `Login`? Globalne pole? Pułapka thread-safety. Zawsze przekazuj `Login` / `Session` jawnie. + +### 14.3 Nazewnictwo zgodne z konwencją projektu + +| Element | Konwencja | +|---------|-----------| +| Parametr `bool` | nazwany (np. `readOnly: true`) | +| Logika biznesowa | nazwy polskie (`Towar`, `Faktura`) | +| Klasy systemowe | nazwy angielskie (`Session`, `Module`) | + +--- + +## 15. Code review checklist (TL;DR) + +Do szybkiej weryfikacji PR-a / refaktoringu: + +**Sesje i obiekty sesyjne** +- [ ] Każda sesja w `using` (§1.1) +- [ ] Obiekty z innych sesji przepuszczone przez `session.Get(...)` (§2.1) +- [ ] Brak zwracania obiektów sesyjnych poza `using` (§2.2) +- [ ] Brak współdzielenia `Session`/`Row`/`Table`/`Module`/`Context` między wątkami (§3.1) +- [ ] Brak statycznych pól trzymających obiekty sesyjne (§3.2) + +**Konflikty i wyjątki** +- [ ] `RowConflictException` z `Save()` ma dedykowaną obsługę (§4.1) +- [ ] Brak ignorowania wyjątku z `Save()` (§4.2) +- [ ] Brak `catch (Exception)` bez konkretnego typu (§9.1) +- [ ] Brak `throw new Exception(...)` — używane typowane (`RowException`, `BusException`, `InvalidOperationException`, …) (§9.2) + +**Walidacja** +- [ ] Walidacja rzuca wyjątek **przed** `Commit()` lub w `SavingRow` (§5.1) +- [ ] Komunikaty błędów przez `.Translate()` (§5.2, §11.1) + +**Dane** +- [ ] Filtrowanie przez `SubTable[condition]`, nie pętle z `if` w pamięci (§6.1) +- [ ] Brak `View` w kodzie biznesowym (§6.2) +- [ ] Brak pełnego skanu tabel kartotekowych; tabele operacyjne guided z zakresem czasowym; nie-guided w zakresie roota (§6.3) +- [ ] Brak referencji do UI (`IsXxx`, `GetXxx`, `MessageBox`, `IUIServices`) w kodzie biznesowym (§7.1) +- [ ] Brak sprawdzania `AccessRight` w logice biznesowej (§7.2) +- [ ] Z `ExecuteConfig` wracają tylko prymitywy / kopie, nie obiekty sesyjne (§8.1) +- [ ] Brak `Session.ConfigSession` — dostęp do konfiguracji przez `Session.ExecuteConfig()` (§8.2) + +**Typy** +- [ ] Pieniądze: `Currency`/`DoubleCy` z walutą (§10.1) +- [ ] Ilości: `Quantity` z jednostką (§10.1) +- [ ] Daty: `Soneta.Types.Date`, `Date.Today` (§10.1, §10.2) + +**Tłumaczenia i logowanie** +- [ ] Łańcuchy widoczne dla użytkownika przez `.Translate()` (§11.1) +- [ ] Klucze / symbole / kody bez `Translate` (§11.2) +- [ ] Brak PII / haseł / tokenów w logach (§12.1) + +**Wydajność** +- [ ] Krótkie transakcje edycyjne, duże operacje dzielone na paczki (§13.1) +- [ ] Brak `.ToList().Where(...)` w miejsce filtra serwerowego (§13.2) + +**API i konwencje** +- [ ] Spójny kontrakt sesyjny — `Login` albo `Session`, nie mieszane (§14.1) +- [ ] Brak `static` metod modyfikujących stan biznesowy (§14.2) +- [ ] Parametry `bool` nazwane (§14.3) +- [ ] Nazewnictwo: logika biznesowa po polsku, klasy systemowe po angielsku (§14.3) diff --git a/soneta-programming/references/viewinfo.md b/soneta-programming/references/viewinfo.md index 73a9d0f..5eeac4d 100644 --- a/soneta-programming/references/viewinfo.md +++ b/soneta-programming/references/viewinfo.md @@ -98,7 +98,7 @@ Logikę ViewInfo wprowadzasz przez handlery eventów dziedziczonych z `Soneta.Bu ```csharp public FakturyViewInfo() { - NewRowTable = "Faktura".TranslateIgnore(); + NewRowTable = "Faktura"; ResourceName = "Faktury"; InitContext += FakturyViewInfo_InitContext; @@ -252,7 +252,10 @@ Pełen opis `RowCondition` (rodzaje, `Exists`, `Or`/`And`, użycie w `SubTable`) ViewInfo i jego `Params` przez składnię `{NazwaViewInfo+Params.Nazwa}`: ```xml - + +