Podstawowe koncepcje

Model mentalny, który stoi za wszystkim

Archestack ma niewielką liczbę podstawowych koncepcji, które pojawiają się wszędzie w produkcie. Każda z nich istnieje po to, żeby rozwiązać problem, który inaczej rozwiązywałbyś ręcznie, w kółko, za każdym razem trochę inaczej. Piętnaście minut spędzonych tutaj zaoszczędzi Ci godziny klikania na ślepo. Każda sekcja poniżej wprowadza jedną koncepcję, pokazuje konkretny przykład i wskazuje błędy, które popełniają nowi użytkownicy.

Schema to źródło prawdy

Wszystko zaczyna się od Schema, opisu JSON każdej tabeli, kolumny, relacji i indeksu w Twojej aplikacji. Nie piszesz SQL ręcznie. Projektujesz tabele wizualnie w Schema Designer, a Archestack przy wdrożeniu konwertuje to na prawdziwe tabele PostgreSQL.

Dlaczego istnieje

Tradycyjny ERP to gąszcz tabel, który rósł organicznie przez dekadę. Archestack odwraca model: schema to wersjonowany artefakt, który edytujesz świadomie, jak kod źródłowy. Każde inne narzędzie platformy, Business Entities, Pages, Events, Scripts, czyta ze schemy. Jeśli kolumny nie ma w schemie, żadne inne narzędzie o niej nie wie.

Jak to działa w praktyce

  • Auto-save vs deploy. Edycja w Schema Designer automatycznie zapisuje Twoje zmiany (wskaźnik pojawia się w lewym dolnym rogu). Zapis nie zmienia bazy danych, to dzieje się dopiero wtedy, gdy utworzysz wdrożenie w Database Deployments i klikniesz Deploy.
  • Generated SQL jest edytowalny. Karta Generated SQL wdrożenia pokazuje migrację SQL wygenerowaną przez platformę, CREATE TABLE, ALTER TABLE itd. Możesz ją przeczytać przed wdrożeniem i możesz ją bezpośrednio edytować, jeśli automatycznie wygenerowana wersja nie pasuje Ci do końca.
  • Hooki pre/post. Wdrożenie niesie ze sobą skrypty pre-deployment i post-deployment (PostgreSQL), przydatne do uzupełnienia danych dla nowej kolumny NOT NULL albo do odbudowy indeksu w kontrolowanej kolejności.
  • Historia jest trwała. Każde wdrożenie jest zalogowane wraz ze statusem (Draft / Executing / Succeeded / Failed) oraz uruchomionym SQL-em.

Konkretny przykład

Dodajesz kolumnę priority_score do deal. W Schema Designer to zaznaczenie tabeli, otwarcie karty Columns, wpisanie nazwy w polu "column_name", wybranie INTEGER z rozwijanej listy Type, kliknięcie Add Column. Wskaźnik auto-save mignie "Auto saving..." i przeskoczy na "Saved". Następnie kliknij Deploy na pasku narzędzi, lądujesz na świeżej stronie konfiguracji wdrożenia. Otwórz kartę Generated SQL:

ALTER TABLE "deal" ADD COLUMN "priority_score" INTEGER;

Kliknij Deploy w prawym górnym rogu i gotowe. Kolumna już istnieje. Dopóki nie dodasz jej również do Business Entity deal (następna koncepcja), żadna strona ani event jej nie zobaczy. To rozdzielenie jest celowe: zmiany w schemie są tanie, ich ujawnienie to świadomy następny krok.

Pułapki

  • Zmiana nazwy kolumny powoduje drop + recreate. Generator planu nie czyta w myślach. Jeśli zmieniasz nazwę, edytuj SQL na karcie Generated SQL, używając RENAME COLUMN, albo wykonaj wieloetapowe wdrożenie: dodaj nową kolumnę, napisz post-script, który skopiuje dane, a potem usuń starą kolumnę w kolejnym wdrożeniu.
  • Dodanie kolumny NOT NULL do niepustej tabeli się nie powiedzie. Albo podaj default, albo dodaj ją jako nullable, uzupełnij dane, a potem zmień na NOT NULL w drugim wdrożeniu.
  • Klucze obce ograniczają zachowanie przy usuwaniu. Używaj rozwijanej listy On Delete w każdej relacji świadomie, opcje to CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION.
  • Snake_case to konwencja. Pole "Table name" w Schema Designer wprost to sugeruje ("Lowercase with underscores recommended"). Narzędzia działają też z PascalCase, ale każdy przykład kodu w tym serwisie zakłada snake_case dla nazw tabel i kolumn.

Business Entities to wyselekcjonowany widok

Surowa tabela (customer) rzadko jest tym, co chce zobaczyć końcowy użytkownik. On chce "customer z sumą ostatniego zamówienia" albo "vehicle z nazwiskiem i numerem telefonu przypisanego mechanika". Business Entity (BE) to wyselekcjonowany widok jednej tabeli źródłowej plus dołączone kolumny z powiązanych tabel. Strony budujesz w oparciu o BE, nie o surowe tabele.

Dlaczego istnieje

Dwa powody. Po pierwsze, łączenie tabel na każdej stronie byłoby powtarzalne i niespójne, różne strony łączyłyby te same tabele trochę inaczej, a zachowanie zaczęłoby dryfować. BE centralizuje logikę łączenia. Po drugie, często chcesz mieć wiele "widoków" na tę samą tabelę źródłową dla różnych odbiorców: widok sprzedażowy customer pokazuje przychody i ostatni kontakt; widok supportu pokazuje tickety i naruszenia SLA. Różne strony odwołują się do różnych BE, wszystkie oparte na tym samym wierszu w customer.

Anatomia

  • Master Table - tabela, na której BE jest zakotwiczone. Każde BE ma dokładnie jedną. Klucz główny BE to klucz główny master table.
  • Label Column - kolumna, której wartość identyfikuje rekord na listach i w pickerach (zazwyczaj name, title lub podobna). Wybierasz ją przy tworzeniu BE.
  • Joins - dodajesz je przyciskiem Add Join. Każdy join ma typ złączenia (INNER / LEFT / RIGHT), tabelę docelową, kolumny source/target oraz listę kolumn docelowych do wystawienia.
  • Aggregated columns - włącz Aggregate Mode na joinie, a następnie wybierz funkcję agregującą dla każdej kolumny: COUNT, SUM, AVG, MIN, MAX, COUNT DISTINCT. "Liczba otwartych kontaktów na tej firmie" to typowa kolumna agregowana.

Konkretny przykład

Master table: vehicle. Wystawione kolumny natywne: vin, make, model, year. Join do customer przez vehicle.owner_id -> customer.id, wystawia customer.full_name jako owner_name. Drugi join w trybie agregacji: count wierszy work_order, gdzie vehicle_id pasuje i status != 'Closed', wystawiony jako open_work_order_count.

Strona oparta o to BE renderuje pojedynczy wiersz siatki, który wygląda jak jeden płaski fakt, mimo że pobiera dane z trzech tabel, i robi to spójnie na całej platformie.

Pułapki

  • BE domyślnie nie są granicami zapisu. Zapis z formularza powiązanego z BE trafia do master table. Kolumny dołączone (joined) są zwykle tylko do odczytu, żeby edytować owner_name z przykładu powyżej, przejdź do Customer.
  • Używaj Run Preview bez ograniczeń. Przycisk Run Preview w edytorze BE uruchamia konfigurację join i pokazuje prawdziwe wiersze. Jeśli kolumna join wraca pusta, Twoja relacja albo mapowanie kolumn są źle skonfigurowane.
  • Nie schodź zbyt głęboko. BE łączące trzy tabele w głąb zaczyna spowalniać na dużych zbiorach danych. Jeśli łapiesz się na łączeniu czterech albo pięciu tabel, to znak, że chcesz raczej własnego Script Module albo widoku bazy danych.

Strony się konfiguruje, a nie koduje

Page to interfejs uruchomieniowy zbudowany z jednego albo wielu Business Entities. Konfigurujesz je w Page Editor, ustawiając nazwę, route (np. /companies), powiązanie z Business Entity oraz przełącznik Published. Platforma automatycznie generuje sekcje na podstawie kolumn BE, a Ty stamtąd dostrajasz układ.

Anatomia

  • Visual tab - edytor układu WYSIWYG. Sekcje, pola, karty, przyciski akcji.
  • Overview tab - konfiguracja listy/siatki: które kolumny się pojawiają, domyślne sortowanie, filtry.
  • Create tab - formularz nowego rekordu. Często różni się nieco od formularza szczegółów (mniej pól, inne wartości domyślne).
  • Entities tab - dodatkowe powiązania BE (strona może odwoływać się do więcej niż jednego BE dla kart i powiązanych siatek).
  • Events tab - event triggers o zasięgu strony.
  • JSON tab - surowy JSON dla użytkowników zaawansowanych.

Typy widgetów pól

Dla każdego pola w formularzu szczegółów wybierasz Type z kontrolki Select. Faktyczne opcje:

  • Text - jednoliniowy input
  • Textarea - wieloliniowy input
  • Number - input numeryczny
  • Date - picker daty
  • Select - rozwijana lista (użyj jej dla pól klucza obcego i kolumn typu enum; dla pól FK ustaw autocomplete Entity na referencjonowane BE, by użytkownicy wybierali po etykiecie)
  • Checkbox - boolean
  • Email - input tekstowy z walidacją emaila

Dla każdego pola konfigurujesz dodatkowo Label, Placeholder, Required, Read Only i Span (szerokość kolumny siatki).

Karty i powiązane siatki

Formularz szczegółów strony obsługuje karty. Użyj przycisku Add Tab, żeby dodać kartę. Wewnątrz karty możesz dodać sekcję RelatedGrid, która pokazuje wiersze z innego BE filtrowane przez join (np. wszystkie wiersze contact, gdzie company_id = id aktualnej firmy). W ten sposób bez ani jednej linii kodu budujesz strony master-detail "Company -> Contacts / Deals".

Publikacja

Każda strona ma Published Switch w nagłówku u góry. OFF = draft (tylko Ty, w edytorze, widzisz swoje zmiany); ON = opublikowana strona pojawia się pod Published Pages w pasku bocznym i to jest to, co widzą końcowi użytkownicy, gdy wchodzą na jej route.

Frontend Templates: gdy "konfiguruj, nie koduj" przestaje wystarczać

Czasem skonfigurowane szablony to za mało, chcesz własny widget, nietypowy układ albo konkretną wizualizację. Frontend Templates pozwalają napisać fragment TSX, który staje się dostępny w edytorze stron jako komponent wielokrotnego użytku. Są pakowalne i żyją obok reszty Twojej konfiguracji. Zobacz Reference -> Frontend Templates.

Pułapki

  • Zapomniałeś przełączyć switcha Published? Strona nie pojawi się w pasku bocznym, a końcowi użytkownicy wchodzący na jej route zobaczą 404. Łatwo to przeoczyć, bo edytor wprost tego nie wymusza.
  • Szerokość ma znaczenie. Jeśli Twoje BE ma 25 kolumn, siatka będzie nieużywalna na laptopie. Ukryj kolumny o niskiej wartości; nadal da się je odpytać, po prostu nie są wyświetlane.
  • Karty wywołują osobne zapytania. Strona z pięcioma kartami wysyła dodatkowe zapytania, gdy otworzysz wiersz szczegółów. Zwykle nie ma problemu, czasem to kwestia wydajności na dużych zbiorach danych.

Business Events sprawiają, że dane reagują

Business Event obserwuje Business Entity pod kątem zmian i uruchamia akcje, gdy spełnione są jego warunki. To reguły "gdy dzieje się X, zrób Y", które zamieniają pasywną bazę danych w działającą aplikację.

Momenty wyzwalania triggerów

Faktyczne opcje przy tworzeniu eventu (możesz zaznaczyć więcej niż jedną):

  • BeforeCreate - odpala się przed zapisem nowego rekordu. Skrypt może mutować Entity, a zmiany zostaną utrwalone.
  • BeforeUpdate - odpala się przed zapisem istniejącego rekordu. To samo zachowanie mutacji.
  • AfterCreate - odpala się po zatwierdzeniu nowego rekordu.
  • AfterUpdate - odpala się po zatwierdzeniu istniejącego rekordu.
  • BeforeDelete - odpala się przed usunięciem rekordu.
  • InitialValue - odpala się przy otwarciu nowego formularza; ustawia domyślne wartości pól.
  • OnSchedule - odpalany przez Quartz wg harmonogramu cron (zobacz Scheduled Events).
  • Manual - odpalany jawnie przez akcję użytkownika z przycisku na stronie.

Typy akcji

Faktyczne typy akcji (RuleActionKind):

  • ExecuteScript - uruchamia skrypt C#. Najbardziej elastyczna akcja; sięgasz po nią, gdy chcesz ustawić pole, zrobić obliczenie, wywołać API, cokolwiek niestandardowego. Skrypt dostaje Entity (mutuj go, by zmienić rekord), OldEntity, Log, Db, Modules.
  • Validate - uruchamia skrypt C# zwracający bool. true oznacza, że walidacja nie przeszła, a zapis jest blokowany skonfigurowanym komunikatem błędu. Opcjonalnie podświetla konkretne kolumny.
  • BlockOperation - twarde zatrzymanie operacji z komunikatem. Bez skryptu.
  • CreateEntity - wstawia rekord do innego BE. Wartości pól obsługują wyrażenia szablonów.
  • UpdateEntity - aktualizuje rekordy w innym BE pasujące do filtra warunków.
  • DeleteEntity - usuwa rekordy pasujące do warunku (bez warunku odmawia odpalenia, dla bezpieczeństwa).
  • SendEmail, SendWebhook, PublishEvent - zdefiniowane w schemie jako przyszłe rozszerzenia; aktualnie logowane i pomijane.

Wyrażenia szablonów

Konfiguracja akcji akceptuje wyrażenia szablonów w nawiasach {{ ... }}. Faktyczne tokeny:

  • {{ Entity.column_name }} - pole aktualnego rekordu ({{ Entity.id }} też działa)
  • {{ OldEntity.column_name }} - poprzednia wartość (tylko triggery update)
  • {{ now() }} albo {{ getdate() }} - datetime ISO 8601 UTC
  • {{ today() }} - string z datą
  • {{ guid() }} albo {{ newid() }} - świeży GUID
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}
  • Agregaty w kontekście join: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Arytmetyka: {{ Entity.quantity * Entity.price }} - obliczana po podstawieniu

Uwaga: nie ma tokenu {{ user.email }}, skrypty i wyrażenia szablonów nie mają dostępu do aktualnego użytkownika. Jeśli potrzebujesz tożsamości użytkownika w regule, zapisz ją w rekordzie przy wstawianiu po stronie frontendu i czytaj stamtąd.

Konkretne przykłady

  • Znormalizuj pole przy zapisie. Trigger: Before Update na Deal. Bez warunków. Akcja: Execute Script z Entity.title = ((string)Entity.title)?.Trim();, aby usunąć zbędne spacje. (Nie potrzebujesz reguły dla created_at / updated_at / created_by / updated_by, platforma stempluje je automatycznie.)
  • Zablokuj oznaczanie nisko-kwotowych deali jako Won. Trigger: Before Update na Deal. Warunek: stage = 'Won' AND amount < 100. Akcja: Block Operation z komunikatem "Deals under €100 can't be marked Won, log them as Lost or delete them."
  • Utwórz Note, gdy Customer zostanie oflagowany. Trigger: After Update na Customer. Warunek: flagged = true. Akcja: Create Entity na note z title = "Customer flagged at {{ now() }}", body = "Auto-generated for {{ Entity.full_name }}".

Symuluj, zanim zapiszesz

Edytor triggera ma kartę Simulate z przyciskiem Run Simulation. Wybierz prawdziwy rekord, a platforma uruchomi warunki i akcje na nim, bez utrwalania zmian, operacje zapisu wykonują się w transakcji, która zostaje cofnięta. Panel wyjścia pokazuje, które warunki pasowały i co zrobiłaby każda akcja. Korzystaj z tego, zanim włączysz trigger na danych produkcyjnych.

Pułapki

  • Triggery rekurencyjne. Trigger After Update, który używa Update Entity do aktualizacji tego samego rekordu, odpali się ponownie. Zamiast tego użyj Before Update + Execute Script + Entity.field = ..., to mutuje zapis w locie, zamiast rozpoczynać nowy.
  • Kolejność między triggerami ma znaczenie. Wiele triggerów na tym samym evencie odpala się w kolejności priorytetów. Jeśli zachowanie zależy od kolejności, ustaw priorytety jawnie.
  • Wyłączone eventy nadal widać na liście. Switch Enabled jest niezależny od konfiguracji triggera, sprawdź przed testowaniem, czy jest włączony.

Script Modules to Twoje wyjście awaryjne

Dla wszystkiego, czego nie da się wyrazić jako prosty warunek + akcja, liczenie złożonego rabatu, wywołanie zewnętrznego API, wygenerowanie sluga, wykonanie wieloetapowej operacji na bazie danych, piszesz Script Module: mały fragment C# (kompilowany w czasie wykonania przez Roslyn), który możesz wywołać z Business Event, ze Scheduled Event albo z frontendu.

Do czego skrypty mają dostęp

Każdy skrypt dostaje te globale (żadne inne nazwy nie istnieją w zakresie skryptu):

  • Entity - aktualny rekord (w kontekstach triggerów) jako dynamic. Do pól dostajesz się przez Entity.column_name. Mutowanie Entity w triggerze Before* to sposób na "ustawienie pola".
  • OldEntity - poprzedni rekord (triggery update i delete). Tylko do odczytu.
  • Log - ILogger do diagnostyki. Log.LogInformation("...") zapisuje do logów serwera i wpisu Event Log.
  • Db - helper bazy danych. Zobacz poniżej.
  • Modules - wywołuje inne Script Modules: await Modules.CallAsync("OtherModule", new Dictionary<string, object?> { ["param"] = value }).
  • Pdf - renderuje zapisany PDF Template po nazwie: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) zwraca wyrenderowany byte[]. Pdf.GenerateBase64Async(...) zwraca ten sam payload zakodowany w base64.

Nie ma globali User, Http ani Email.

Jak wygląda skrypt

// Parameters declared in the UI: customer_id (int)
var customer = await Db.GetAsync("customer", customer_id);
if (customer == null) return new { ok = false, error = "Customer not found" };

var orders = await Db.From("sales_order")
    .Where("customer_id", "=", customer_id)
    .Where("status", "=", "Closed")
    .ToListAsync();

decimal totalRevenue = 0;
foreach (var o in orders) totalRevenue += (decimal)(o.total ?? 0);

return new { ok = true, customer = customer.name, revenue = totalRevenue };

Większość skryptów ma 5-30 linii. Nie potrzebujesz biegłości w .NET, wystarczy podstawowa kontrola przepływu w C# i helper Db. IntelliSense jest włączony, a po przypisaniu wyniku z Db.GetAsync("customer", ...) edytor zna kolumny tej zmiennej i oferuje uzupełnienia.

Kiedy sięgać po skrypt

  • Obliczenia obejmujące wiele tabel (lifetime value klienta, suma historii napraw pojazdu).
  • Wywoływanie zewnętrznych API (geokodowanie, dostawcy płatności, bramki SMS).
  • Operacje hurtowe wyzwalane ze Scheduled Event (nocne przeliczanie, tygodniowy digest).
  • Zwracanie do frontendu danych zbyt niestandardowych dla Business Entity.
  • Wszędzie tam, gdzie alternatywą byłoby spięcie sześciu Business Events w łańcuch, zwykle to znak, że tak naprawdę chodziło Ci o skrypt.

Pułapki

  • Helper Db jest async. Zawsze await. Pominięcie się skompiluje, ale zwróci Task, a nie dane.
  • Nie ma FirstOrDefaultAsync ani SumAsync. Terminatory to ToListAsync(), FirstAsync(), CountAsync(). Żeby zsumować, pobierz wiersze i zsumuj w C#.
  • Where przyjmuje trzy argumenty - kolumnę, operator, wartość. Operatory obejmują =, !=, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL.
  • Update jest wywołaniem najwyższego poziomu: await Db.UpdateAsync("table", id, new { field = value }), nie łańcuchujesz go po Where.
  • Skrypty trial uruchamiają się w tym samym sandboxie co produkcja. Nie zakładaj, że "to tylko trial" oznacza luźniejsze limity, Twój skrypt i tak może obciążyć Postgresa.

Scheduled Events chodzą według zegara

Scheduled Event to ten sam system Event Trigger z timingiem OnSchedule, wyrażenie cron Quartza wyzwala regułę w nawracających odstępach, zamiast w odpowiedzi na zmianę danych. Używaj go do nocnych podsumowań, codziennego odświeżania danych, tygodniowych czyszczeń, miesięcznych raportów.

Format cron

Cron w stylu Quartza, 6 albo 7 pól (second minute hour day-of-month month day-of-week [year]). Kilka typowych wzorców:

  • 0 0 3 * * ? - codziennie o 03:00.
  • 0 0 9 ? * MON-FRI - w dni robocze o 09:00.
  • 0 */15 * * * ? - co 15 minut.
  • 0 0 0 1 * ? - pierwszy dzień każdego miesiąca o północy.

Czasy są w czasie serwera (UTC na stacku produkcyjnym). Strona Scheduled Events pokazuje najbliższy moment odpalenia, dzięki czemu możesz to zweryfikować przed zapisem.

Pułapki

  • Długo trwające zadania trzymają swój slot. Jeśli zadanie trwa 25 minut, a cron jest co 15 minut, następny przebieg zostanie pominięty, Quartz nie odpali dwóch instancji tego samego zadania równolegle.
  • Niepowodzone uruchomienia nie są ponawiane. Lądują w Event Logs z wyjątkiem. Jeśli potrzebujesz ponowień, dodaj jawną logikę retry do skryptu.

Packages pakują konfigurację do promocji

Gdy już coś zbudujesz, zestaw tabel, BE, stron, eventów i skryptów, będziesz chciał przenieść to z triala na prawdziwe środowisko albo udostępnić innemu partnerowi. Package to ZIP wybranych obiektów konfiguracji (z kaskadowaniem: dodanie strony automatycznie ciągnie jej BE, które ciąga jej tabele), który możesz eksportować i importować.

Jak działa kaskadowanie

Dodajesz obiekty najwyższego poziomu, na których Ci zależy (zwykle strony). Builder package'a przechodzi graf zależności i dociąga wszystko, czego te obiekty potrzebują: BE, do których strony się odwołują, tabele, do których odwołują się BE, Frontend Templates osadzone na stronach, Script Modules wywoływane przez eventy. Pełną kaskadową listę widzisz przed eksportem i możesz odznaczyć to, co chcesz pominąć.

Wysyłanie danych seed

Packages mogą opcjonalnie zawierać dane wierszowe, przydatne do wysyłania domyślnych tabel lookup (kraje, waluty, enumeracje statusów) albo danych demo. Zaznacz Include data przy eksporcie. Przy imporcie wiersze są upsertowane po kluczu głównym.

Pułapki

  • Delty schemy nie wdrażają się automatycznie przy imporcie. Jeśli środowisku docelowemu brakuje tabel, import się wywali. Najpierw wdróż schemę, potem importuj package.
  • Identyfikatory są po nazwie, nie po ID. BE o nazwie "customer" w środowisku źródłowym wiąże się z BE o nazwie "customer" w docelowym, a nie z tym samym numerycznym ID. Zmiana nazwy po którejkolwiek stronie zrywa powiązanie.

Business Units kontrolują, kto co widzi

Dla setupów multi-tenant albo multi-team Business Units to grupy Keycloaka, które można przypisywać do konkretnych zasobów. Użytkownik w Business Unit "Garage A" widzi tylko Pages, BE i triggery przypisane do Garage A.

Jak to działa

Każdy użytkownik należy do jednej albo wielu BU (zarządzanych w User Management i Business Units). Frontend śledzi aktywne BU użytkownika i ustawia nagłówek HTTP X-Business-Unit przy każdym requeście. Backend używa tego nagłówka do filtrowania endpointów list, wracają tylko zasoby przypisane do aktywnego BU.

Kiedy używać (a kiedy nie)

  • Używaj, gdy różne zespoły albo różni klienci potrzebują własnego wycinka tej samej platformy, mechanicy z Garage A nie powinni widzieć work orderów Garage B.
  • Nie używaj jako systemu uprawnień. BU filtrują widoczność, a nie autoryzację. Role (admin / owner / editor / user) obsługują, komu wolno co robić.
  • Nie używaj do bezpieczeństwa na poziomie wierszy w obrębie jednego tenanta. To zadanie dla filtrów na Business Entity, nie BU.

Co dalej

Z tymi koncepcjami w głowie reszta dokumentacji układa się naturalnie. Tutorial first-app używa każdej koncepcji z tej strony. Reference wchodzi o poziom głębiej w każde narzędzie: co kliknąć, na co uważać, kiedy po nie sięgać.