History
Moduł History to wspólna historia zdarzeń firmy (od SDK 3.10.0). Historia to tylko dopisywany rejestr tego, co się wydarzyło z obiektami biznesowymi firmy: zamówienie zostało utworzone, status się zmienił, przesyłka została nadana. Wszystkie moduły piszą do tego samego rejestru, a użytkownik czyta go w Ustawienia → Historia, filtrując po module źródłowym i po module, którego zdarzenie dotyczy.
Nie jest to ani Log, ani Problems. Logi służą diagnostyce technicznej dla programisty. Problems to bieżąca lista rzeczy, które użytkownik musi naprawić — wpisy deduplikują się po problemKey i znikają po resolve. Historia jest niezmiennym śladem audytowym: wpisu nie da się zmienić ani skasować, a każde add dopisuje kolejny.
const { History } = require('@orbito/api-lib');
History.add({ code, desc, relations, userId? }) // -> { _id } albo null
History.list(filters?) // -> { items, pagination }
Parametry History.add:
| Parametr | Typ | Wymagany | Opis |
|---|---|---|---|
code | string | Tak | Kod zdarzenia nadawany przez moduł, np. ORDER_STATUS_CHANGED (1–100 znaków). Służy do filtrowania i diagnostyki — użytkownik czyta desc, nie ten kod |
desc | string | object | Tak | Gotowy opis zdarzenia dla użytkownika. Zwykły string (1–2000 znaków) albo mapa językowa { pl: '...', en: '...' } — klucze w formacie kodu języka (^[a-z]{2}(-[A-Z]{2})?$), każda wartość 1–2000 znaków, min. 1 klucz |
relations | array | Tak | Encje, których dotyczy zdarzenie — od 1 do 10 elementów |
relations[].module | string | Tak | Kod modułu będącego właścicielem encji, w formacie <vendor>/<nazwa>, np. core/orders (max 100 znaków) |
relations[].object | string | Tak | Typ encji w tym module, np. order, shipment (max 50 znaków, wzorzec ^[a-z][a-z0-9_-]*$) |
relations[].id | string | Tak | Identyfikator encji (1–100 znaków). String, a nie ObjectId — dzięki temu działają też identyfikatory z systemów zewnętrznych |
relations[].instanceId | string | Nie | Instalacja modułu będąca właścicielem encji (24 znaki hex), gdy samo id nie jest jednoznaczne w obrębie firmy (kilka instalacji tego samego modułu, identyfikatory zewnętrzne) |
userId | string | Nie | Kto wywołał zdarzenie (24 znaki hex). Bierzesz go z kontekstu wywołania (this.userId w handlerze metody); brak oznacza zdarzenie systemowe |
Historia nie ma odpowiednika update ani delete — poprawkę zapisujesz jako kolejne zdarzenie.
Zalecana forma desc — mapa językowa
Wpis niesie gotowy opis, bo moduł działa w osobnym kontenerze i może być zainstalowany w kilku wersjach naraz — ani rdzeń, ani panel nie mają jak wykonać jego deskryptora w czasie odczytu. Skoro opis zapisujesz raz i na zawsze, przekazuj go jako mapę { pl, en, ... }: zespół firmy bywa wielojęzyczny, a wszyscy widzą tę samą historię — podawaj oba języki. Rdzeń przechowuje mapę 1:1 — nie tłumaczy ani nie normalizuje jej zawartości — a panel pokazuje wersję w języku zalogowanego użytkownika, z fallbackiem locale → en → pl → pierwsza dostępna wartość (funkcja i18nText we froncie). Zwykły string wciąż działa i jest pokazywany każdemu tak samo.
Relacje i źródło wpisu
relations mówi, czego dotyczy zdarzenie, i może wskazywać kilka encji naraz: utworzenie przesyłki dotyczy i przesyłki, i zamówienia, dla którego powstała. Zapytanie „historia zamówienia X” odpowiada po trójce module + object + id, czyli po danych, które pytający już zna — dlatego każda relacja musi podać wszystkie trzy pola.
Nie przekazujesz informacji o module, instalacji ani o czasie zdarzenia: kod modułu, instancję (z nazwą do wyświetlenia) i znacznik at dopisuje rdzeń na podstawie tożsamości połączenia kontenera. Moduł nie może więc antydatować wpisu ani zapisać go w cudzym imieniu. userId przekaż z kontekstu wywołania — bez niego wpis jest zdarzeniem systemowym (cron, integracja przychodząca) i panel pokazuje przy nim „system”. Wyjątkiem jest akcja automatyzacji: jej pochodzenie dopisuje samo SDK — patrz Pochodzenie wpisu.
Metoda nigdy nie odrzuca Promise
Zapis historii jest efektem ubocznym operacji biznesowej i nie może jej wywrócić, dlatego add — tak samo jak Log i Problems — nie rzuca wyjątku. Błąd transportu lub walidacji jest logowany na console.error przy ORBITO_DEBUG=1, a metoda zwraca null. Nie opieraj więc logiki modułu na tym, że wpis powstał.
History.add
Dopisuje wpis do historii firmy. Zwraca { _id } nowego wpisu. Wołaj ją po tym, jak opisywana zmiana faktycznie się wydarzyła.
const { History } = require('@orbito/api-lib');
await History.add({
code: 'ORDER_STATUS_CHANGED',
desc: {
pl: `Status zmieniony z „${from.name}” na „${to.name}”`,
en: `Status changed from "${from.name}" to "${to.name}"`
},
relations: [{ module: 'core/orders', object: 'order', id: String(order._id) }],
userId: this.userId
});
Zdarzenie dotyczące kilku encji — przesyłka utworzona w core.shipping dla zamówienia z core.orders — podaje obie relacje, dzięki czemu pojawi się zarówno w historii przesyłki, jak i w historii zamówienia:
await History.add({
code: 'SHIPMENT_CREATED',
desc: {
pl: `Utworzono przesyłkę ${shipment.number} (${carrier.name})`,
en: `Shipment ${shipment.number} created (${carrier.name})`
},
relations: [
{ module: 'core/shipping', object: 'shipment', id: String(shipment._id) },
{ module: 'core/orders', object: 'order', id: String(shipment.orderId) }
],
userId: this.userId
});
Zdarzenie wywołane bez udziału użytkownika (cykliczne pobranie statusów, hook, automatyzacja) po prostu pomija userId — jeśli wywołanie wyszło z biegu automatyzacji, SDK samo dopisze do wpisu jego pochodzenie:
await History.add({
code: 'SHIPMENT_STATUS_CHANGED',
desc: {
pl: `Status przesyłki ${shipment.number} zmieniony na „${status}”`,
en: `Shipment ${shipment.number} status changed to "${status}"`
},
relations: [{ module: 'core/shipping', object: 'shipment', id: String(shipment._id) }]
});
Pochodzenie wpisu (automatyzacja)
(od SDK 4.3.0)
Wpis zapisany w wyniku akcji automatyzacji niesie pole origin — panel pokazuje wtedy w kolumnie użytkownika nazwę automatyzacji z linkiem do konkretnego biegu (oś czasu w Ustawienia → Automatyzacje → Biegi), a nie „system”. Ten sam link pojawia się w historii zamówienia w module zamówień.
origin: {
type: 'automation',
automationId: '66f0...', // _id automatyzacji
automationName: 'Wysyłka po opłaceniu', // nazwa w chwili biegu (snapshot)
runId: '66f1...:cron:2026-10-06T08:00:00.000Z' // klucz biegu (string, nie ObjectId)
}
origin jest null dla wpisów użytkownika i zdarzeń systemowych. automationName to migawka z chwili biegu — późniejsza zmiana nazwy albo usunięcie automatyzacji nie zmienia wpisu (historia jest niezmienna).
Moduł niczego nie przekazuje. History.add nie przyjmuje origin w parametrach (rdzeń wycina takie pole z payloadu). Pochodzenie dokleja samo SDK z kontekstu wywołania:
- w
executeakcji automatyzacji (Automation), - w handlerach metod i hooków osiągniętych z takiej akcji przez
Core.callModule/Core.callInstanceMethod/Core.execHook— także w innych modułach i dalej w łańcuchu, np. akcjacore/orderszmienia status → hook →core/shippingzapisuje wpis o przesyłce z tym samym biegiem.
Dzięki temu wystarczy podbić @orbito/api-lib do ^4.3.0 — kod modułu się nie zmienia. Wpis z biegu automatyzacji nie ma userId, nawet gdy bieg wyzwolił użytkownik: liczy się to, że akcję wykonała automatyzacja.
Starsze SDK
Moduł na SDK sprzed 4.3.0 zapisuje wpisy bez pochodzenia, także te z akcji automatyzacji — panel pokaże przy nich „system”. To samo dotyczy łańcucha: jeśli moduł pośredni (np. ten, który woła execHook) ma starsze SDK, pochodzenie nie przejdzie dalej.
History.list
(od SDK 3.12.0)
Czyta historię zdarzeń, stosując te same filtry co strona Ustawienia → Historia (GET /history). Wynik jest stronicowany i posortowany od najnowszych wpisów.
Parametry History.list — wszystkie opcjonalne, łączone przez AND; bez żadnego z nich wywołanie przechodzi po całej historii firmy:
| Parametr | Typ | Opis |
|---|---|---|
module, object, id | string | Filtr relacji — cała trójka albo żadna z nich. Dopasowuje wpisy, w których którakolwiek relacja odpowiada podanej trójce module + object + id |
instanceId | string | Zawęża filtr relacji do konkretnej instalacji (24 znaki hex). Sam, bez pełnej trójki module/object/id, jest odrzucany |
sourceModule | string | Tylko wpisy zapisane przez ten moduł |
relatedModule | string | Tylko wpisy z dowolną relacją do tego modułu |
code | string | Tylko wpisy o tym kodzie zdarzenia |
from, to | string | Date | Zakres dat at (włącznie) |
page | number | Numer strony, liczony od 1 (domyślnie 1) |
limit | number | Liczba wpisów na stronę — domyślnie 20, rdzeń przycina do 200 |
Zwraca { items, pagination }:
items[]— wpisy w kształcie{ _id, source: { moduleCode, instanceId, instanceName }, code, desc, relations, userId, user: { _id, firstname, lastname } | null, origin, at }.descirelationssą dokładnie takie, jak przekazane doadd;userto dane autora dociągnięte przez rdzeń (nulldla zdarzeń systemowych albo usuniętego użytkownika);originto pochodzenie wpisu albonull. Identyfikatory i daty przychodzą jako stringi (atto ISO string), bo wynik podróżuje jako JSON.pagination—{ page, limit, total }. Gdytotaljest większe niż liczba elementów witems, zwrócona strona nie obejmuje całej historii.
Zakres odczytu — cała historia firmy
list nie ogranicza się do encji własnego modułu ani do wpisów zapisanych przez własny moduł. Moduł może odczytać całą historię swojej firmy, np. zdarzenia core/shipping i core/invoicing dotyczące jednego z jego zamówień — wystarczy, że tamte moduły dodały do swojego wpisu relację do Twojej encji (tak jak w przykładzie z SHIPMENT_CREATED wyżej). Firmę wyznacza tożsamość połączenia, więc modułu nigdy nie da się użyć do odczytania historii cudzej firmy.
list odrzuca Promise przy błędzie
W odróżnieniu od add, list rzuca wyjątek, gdy rdzeń odrzuci zapytanie (np. niepełną trójkę relacji) albo gdy wywołanie się nie powiedzie. To odczyt danych biznesowych — wywołujący (np. widok modala) ma zobaczyć błąd, a nie pustą listę udającą brak historii.
Przykład — historia własnej encji razem ze zdarzeniami dopisanymi przez inne moduły:
// historia zamówienia, razem ze zdarzeniami core/shipping i core/invoicing,
// jeśli te dodały do swoich wpisów relację do tego zamówienia
const { items, pagination } = await History.list({
module: 'core/orders', object: 'order', id: String(order._id),
limit: 200
});
if (pagination.total > items.length) {
// pobrana strona nie obejmuje całej historii — widoczne są tylko najnowsze wpisy
}
Dobre praktyki
Opis pisz dla użytkownika i od razu po ludzku – z historii ma dać się przeczytać, co się stało, bez zaglądania do kodu modułu. Wartości, które się zmieniły, wpleć w zdanie; surowych danych zdarzenie nie przenosi.
// ❌ Nigdy
desc: 'ORDER_STATUS_CHANGED'
desc: JSON.stringify({ from, to })
// ✅ Zamiast tego
desc: { pl: `Status zmieniony z „${from.name}” na „${to.name}”`, en: `Status changed from "${from.name}" to "${to.name}"` }
Każdy code powinien znaczyć jedno – to po nim filtrujesz listę i rozpoznajesz zdarzenia swojego modułu. Używaj stałego zestawu kodów w UPPER_SNAKE_CASE (ORDER_STATUS_CHANGED, SHIPMENT_CREATED), a nie treści opisu przepisanej na kod.
Wskaż wszystkie encje, których zdarzenie dotyczy – jeśli operacja łączy dwa obiekty, podaj obie relacje. Wpis z jedną relacją nie pojawi się w historii tej drugiej encji, a historii nie da się później poprawić.
// ❌ Nigdy
relations: [{ module: 'core/shipping', object: 'shipment', id }] // utworzenie przesyłki dla zamówienia
// ✅ Zamiast tego
relations: [
{ module: 'core/shipping', object: 'shipment', id },
{ module: 'core/orders', object: 'order', id: orderId }
]
Zapisuj zdarzenia biznesowe, a nie kroki techniczne – historia jest dla użytkownika. Ponowienie zapytania do API, odświeżenie tokenu czy szczegóły odpowiedzi zewnętrznego serwisu zostaw Log, a sytuacje, które użytkownik musi naprawić — Problems.
Nie loguj zdarzenia, które się nie wydarzyło – add wołaj po udanym zapisie zmiany, nie przed. Historia jest niezmienna, więc wpisu o nieudanej operacji nie da się wycofać.
