Config
Moduł Config to magazyn konfiguracji modułu przechowywany przez platformę Orbito (od SDK 2.3.0). Rozwiązuje dwa problemy:
- Przechowywanie ustawień — klucze API, adresy sklepów, mapowania statusów itd. bez tworzenia własnej kolekcji z jednym dokumentem typu
store_config. - Ekran ustawień bez pisania UI — moduł deklaruje bloki konfiguracyjne (
registerConfigBlocks), a panel Orbito sam renderuje z nich stronę konfiguracji z formularzami, walidacją, obsługą sekretów i własnymi przyciskami akcji (od SDK 3.3.0). Użytkownik znajdzie ją pod Ustawienia → Moduły → Konfiguruj.
const { Config } = require('@orbito/api-lib');
Izolacja konfiguracji
Konfiguracja jest izolowana per instalacja modułu — scope wynika z tożsamości połączenia, moduł nie ma dostępu do konfiguracji innych firm, innych modułów ani innych instalacji tego samego modułu (dwa sklepy tej samej firmy mają osobne klucze API). Wartości przeżywają wyłączenie/włączenie modułu oraz aktualizację wersji.
Odczyt i zapis wartości
| Metoda | Opis |
|---|---|
Config.get(key, defaultValue?) | Zwraca wartość klucza lub defaultValue, gdy klucz nie istnieje (zapisany null jest zwracany jako null) |
Config.set(key, value) | Zapisuje wartość (upsert). Dowolny typ serializowalny do BSON, max 64 KB |
Config.delete(key) | Usuwa klucz; zwraca true, jeśli istniał |
Config.getAll() | Zwraca wszystkie wartości scope'u jako obiekt { klucz: wartość } |
Config.forUser(userId) | Zwraca te same cztery metody w scope użytkownika |
const { Config } = require('@orbito/api-lib');
await Config.set('inpost.api_key', 'abc123');
let apiKey = await Config.get('inpost.api_key');
let mode = await Config.get('inpost.mode', 'sandbox'); // wartość domyślna
// ustawienia per użytkownik (userId zwykle z ctx.userId handlera metody)
await Config.forUser(ctx.userId).set('print_label', true);
let autoPrint = await Config.forUser(ctx.userId).get('print_label', false);
Scope globalny ≠ scope użytkownika
Scope globalny i scope'y per-użytkownik to całkowicie rozłączne przestrzenie kluczy — Config.getAll() nie zwraca wpisów zapisanych przez forUser i odwrotnie.
Większe dane → DB
Config służy do małych wartości (limit 64 KB na wartość). Większe dane trzymaj w DB.
registerConfigBlocks — ekran ustawień modułu
Config.registerConfigBlocks(blocks) deklaruje bloki ustawień modułu. Wywołaj ją raz, przy starcie modułu. Panel Orbito:
- pokaże przycisk Konfiguruj przy module (bez uruchamiania kontenera),
- wyrenderuje stronę konfiguracji z zakładką per blok,
- po zapisie przez użytkownika wywoła Twoją funkcję
validate, a gdy przejdzie — sam zapisze wartości do magazynu konfiguracji, - wyrenderuje zadeklarowane przez blok akcje jako przyciski przy formularzu.
const { Config } = require('@orbito/api-lib');
const { PropertyType, DisplayFormat } = Config;
await Config.registerConfigBlocks({
general: {
title: { pl: 'Połączenie z InPost', en: 'InPost connection' },
scope: 'global',
schema: async (actualConfig) => ({
apiKey: {
type: PropertyType.STRING,
format: DisplayFormat.SECRET,
name: { pl: 'Klucz API', en: 'API key' },
required: true
},
sandbox: {
type: PropertyType.BOOLEAN,
name: { pl: 'Tryb testowy (sandbox)', en: 'Sandbox mode' }
},
hint: {
type: PropertyType.INFO,
name: { pl: 'Klucz API znajdziesz w panelu InPost w zakładce Moje konto → API.' }
}
}),
validate: async (params, raport) => {
if (!params.apiKey) {
raport.addError('apiKey', 'Klucz API jest wymagany');
return;
}
const ok = await InpostApi.checkKey(params.apiKey, params.sandbox);
if (!ok) raport.addError('apiKey', 'Niepoprawny klucz API');
}
},
print: {
title: { pl: 'Preferencje wydruku', en: 'Print preferences' },
scope: 'user',
schema: {
autoPrint: {
type: PropertyType.BOOLEAN,
name: { pl: 'Drukuj etykietę automatycznie po utworzeniu' }
}
}
// validate jest opcjonalne — brak oznacza, że każdy zapis przechodzi
}
});
Klucz bloku jest prefiksem zmiennych
Wartości zapisane przez formularz lądują pod kluczami blok.pole. Blok general z polem apiKey odczytasz tak:
let apiKey = await Config.get('general.apiKey');
// blok scope: 'user' — czytaj w scope użytkownika z ctx
let autoPrint = await Config.forUser(ctx.userId).get('print.autoPrint', false);
Definicja bloku
| Pole | Typ | Opis |
|---|---|---|
title | object | string | Tytuł zakładki jako mapa język → tekst, np. { pl: '...', en: '...' }. en jest wymagane (język zapasowy). Sam string jest traktowany jako { en } |
scope | 'global' | 'user' | global (domyślne) — jeden zestaw wartości na firmę; user — każdy użytkownik edytuje własne wartości |
schema | object | function | Schema pól formularza (format niżej) albo async (actualConfig) => schema, gdy zestaw pól zależy od obecnej konfiguracji (np. dodatkowe pola dopiero po podaniu poprawnego klucza API) |
validate | function (opcjonalne) | async (params, raport) => void — wywoływane przez platformę przed zapisem; jakikolwiek błąd w raporcie odrzuca zapis |
actions | object | function (opcjonalne) | Przyciski akcji renderowane przy formularzu bloku — mapa actionKey → definicja albo async (actualConfig) => actions, gdy zestaw przycisków zależy od konfiguracji (od SDK 3.3.0) |
Format schemy (DeclarativeSchema)
Ten sam format co parametry akcji i triggerów automatyzacji — panel renderuje z niego kontrolki formularza. Każde pole:
fieldKey: {
type: PropertyType.STRING, // string | number | boolean | datetime | array | info
format: DisplayFormat.SECRET, // opcjonalna podpowiedź UI/walidacji
name: { pl: 'Etykieta pola' }, // string lub mapa język → tekst
description: { pl: 'Dodatkowy opis pod polem' }, // opcjonalne
required: true, // opcjonalne
options: { values: [ { id: 'a', label: 'A' } ] } // opcjonalny dropdown (statyczny lub dynamiczny)
}
Najważniejsze DisplayFormat: SECRET, EMAIL, URL, PHONE, TEXT (textarea), HTML, MARKDOWN, CRON, formaty liczbowe (INTEGER, POSITIVE_DECIMAL, ...) oraz DATE_ONLY/TIME_ONLY dla DATETIME.
PropertyType.INFO
Pole typu INFO to czysty tekst informacyjny w formularzu (treść w name/description) — nie ma wartości i nigdy nie jest zapisywane do konfiguracji. Używaj go na instrukcje typu „gdzie znaleźć klucz API".
Domyślnie description jest wyświetlany jako zwykły tekst — HTML od modułu nigdy nie jest interpretowany (znaczniki pokażą się dosłownie), a DisplayFormat.HTML w polu INFO nie działa. Aby uzyskać formatowanie, ustaw format: DisplayFormat.MARKDOWN.
Z DisplayFormat.MARKDOWN opis jest renderowany jako ograniczony markdown:
- akapity (pusta linia) i nowe linie,
**pogrubienie**,*kursywa*,`kod`,- nagłówki
#i listy-, - tabele: wiersz nagłówka, wiersz separatora
| --- | --- |i wiersze danych;\|oznacza dosłowną kreskę w komórce.
Treść jest escapowana przed renderowaniem, więc dane z zewnętrznych API można wstawiać bez ryzyka wstrzyknięcia HTML. Znak | w komórkach musisz jednak sam zamienić na \|, inaczej rozbije tabelę.
warehouses: {
type: PropertyType.INFO,
format: DisplayFormat.MARKDOWN,
name: { pl: 'Dostępne magazyny', en: 'Available warehouses' },
description: {
pl: '| ID | Nazwa |\n| --- | --- |\n| 1 | Magazyn główny |\n| 2 | Magazyn zwrotów |',
en: '| ID | Name |\n| --- | --- |\n| 1 | Main warehouse |\n| 2 | Returns warehouse |'
}
}
Sekrety (DisplayFormat.SECRET)
Pola oznaczone SECRET są traktowane szczególnie:
- zapisana wartość nigdy nie wraca do przeglądarki — użytkownik widzi tylko maskę
••••••••i informację, że wartość jest ustawiona, - puste pole SECRET przy zapisie oznacza „bez zmian" (obecna wartość zostaje),
- Twoja funkcja
validatezawsze dostaje kompletne dane — platforma podstawia aktualną wartość sekretu przed walidacją, więc np. test klucza API działa także wtedy, gdy użytkownik zmienił tylko inne pole.
Pola SECRET służą do danych użytkownika firmy (jego klucz API, hasło do konta). Klucze aplikacji autora modułu, wspólne dla wszystkich firm (np. clientSecret aplikacji marketplace'u), nie należą do konfiguracji — deklarujesz je jako sekrety aplikacji w module.json.
Walidacja — ValidationRaport
validate(params, raport) dostaje pełny obraz bloku po zapisie (stare + nowe wartości) oraz raport z jedną metodą:
raport.addError(fieldKey, 'Komunikat wyświetlany przy polu');
Zgłoszone błędy pokazują się użytkownikowi przy odpowiednich polach formularza, a zapis jest odrzucany w całości. Możesz w validate wykonywać operacje asynchroniczne (np. testowe wywołanie zewnętrznego API).
Akcje bloku (przyciski przy formularzu)
(od SDK 3.3.0)
Blok może wystawić actions — przyciski, które panel renderuje w stopce bloku, obok Zapisz. Kliknięcie uruchamia handler w module, a panel domyślnie pobiera blok ponownie, więc dynamiczna schema od razu pokazuje efekt akcji. Typowe zastosowania: „Dodaj nową regułę", „Testuj połączenie", „Synchronizuj teraz", „Rozłącz konto".
const { Config } = require('@orbito/api-lib');
const { ActionStyle } = Config;
await Config.registerConfigBlocks({
rules: {
title: { pl: 'Reguły wysyłki', en: 'Shipping rules' },
// schema dynamiczna — pola generowane per reguła zapisana w configu
schema: async (actualConfig) => buildRulesSchema(actualConfig),
actions: {
addRule: {
title: { pl: 'Dodaj nową regułę', en: 'Add new rule' },
style: ActionStyle.PRIMARY,
handler: async (actualConfig, ctx) => {
await addEmptyRule(actualConfig);
return { message: 'Dodano regułę' }; // success i needReload domyślnie true
}
},
clearRules: {
title: { pl: 'Usuń wszystkie reguły', en: 'Remove all rules' },
style: ActionStyle.DANGER,
confirm: { pl: 'Na pewno usunąć wszystkie reguły?', en: 'Really remove all rules?' },
handler: async () => { await clearRules(); } // brak `return` = sukces + reload
}
}
}
});
Definicja akcji
| Pole | Typ | Opis |
|---|---|---|
title | object | string | Etykieta przycisku jako mapa język → tekst; en jest wymagane. Sam string jest traktowany jako { en } |
handler | function (wymagane) | async (actualConfig, ctx) => result — kod wykonywany po kliknięciu |
style | ActionStyle (opcjonalne) | Podpowiedź wyglądu: ActionStyle.DEFAULT (domyślne), ActionStyle.PRIMARY, ActionStyle.DANGER |
confirm | object | string (opcjonalne) | Tekst pytania w oknie potwierdzenia; gdy podany, panel prosi o potwierdzenie przed uruchomieniem handlera |
Handler akcji
handler: async (actualConfig, ctx) => {
// actualConfig — ZAPISANE wartości bloku (te same, które dostaje dynamiczna schema)
// ctx.userId — id użytkownika, który kliknął przycisk (dostępne też jako this.userId)
return { success: true, message: 'Gotowe', needReload: true };
}
actualConfigto zapisane wartości bloku — dlascope: 'user'wartości klikającego użytkownika. Handler nie widzi niezapisanego stanu formularza, więc jeśli akcja ma działać na świeżych danych, użytkownik musi najpierw zapisać blok.ctxto{ userId }wywołującego; ten sam obiekt jest zbindowany jakothis(użyj zwykłegofunction, jeśli chcesz z tego korzystać — funkcja strzałkowa ignoruje bindowanie).- Wynik
{ success, message, needReload }— wszystkie pola opcjonalne:
| Pole | Domyślnie | Znaczenie |
|---|---|---|
success | true | false oznacza biznesowe niepowodzenie — panel pokaże komunikat błędu |
message | brak | Tekst pokazywany użytkownikowi jako powiadomienie |
needReload | true | Panel pobiera blok ponownie (nowa schema i wartości). Ustaw false, gdy akcja niczego nie zmienia |
- Brak
returnw handlerze = sukces z odświeżeniem bloku. - Wyjątek rzucony z handlera nie jest błędem protokołu — wraca jako
{ success: false, message: error.message }, a blok i tak się odświeża (akcja mogła coś zmienić, zanim rzuciła).
Dynamiczny zestaw akcji
actions może być funkcją konfiguracji — wtedy zestaw przycisków zależy od stanu modułu:
actions: async (actualConfig) => {
if (!actualConfig.apiKey) {
return {}; // brak klucza → żadnych przycisków
}
return actualConfig.connected
? { disconnect: { title: { pl: 'Rozłącz konto', en: 'Disconnect' }, style: ActionStyle.DANGER, handler: disconnect } }
: { connect: { title: { pl: 'Połącz konto', en: 'Connect' }, style: ActionStyle.PRIMARY, handler: connect } };
}
Zestaw jest rozwiązywany za każdym razem — i przy pobraniu schemy, i przy wykonaniu akcji. Akcji, której nie ma w aktualnym zestawie, nie da się wykonać, więc ukryty warunkowo przycisk nie zostanie wywołany „na skróty".
Kiedy błąd w definicji akcji wybuchnie
Akcje podane wprost (obiektem) są walidowane już przy registerConfigBlocks — literówka w style czy brak handler przerwie start modułu. Wariant funkcyjny jest z natury dynamiczny, więc waliduje się dopiero przy rozwiązaniu zestawu (otwarcie strony konfiguracji lub kliknięcie).
Akcja blokuje formularz
W trakcie wykonywania akcji panel wyłącza przycisk Zapisz i pozostałe akcje bloku (na klikniętym przycisku widać spinner). Długie operacje uruchamiaj asynchronicznie i tylko raportuj ich start, zamiast trzymać handler otwarty — pamiętaj o 30-sekundowym limicie wywołań RPC.
Zachowanie i dobre praktyki
Metadane a schema
Klucz, tytuł i scope bloku są zgłaszane do platformy przy każdym połączeniu (i automatycznie po reconnect — nie musisz nic robić). Schematy, validate i handlery akcji zostają w module — platforma pyta kontener o schemę dopiero, gdy użytkownik otworzy stronę konfiguracji. Metadane akcji (etykieta, styl, potwierdzenie) jadą razem ze schemą, a nie z metadanymi bloków — dzięki temu zestaw przycisków może być dynamiczny, a lista bloków i flaga „moduł ma konfigurację" są czytane z trwałego indeksu, więc działają też zanim kontener połączy się po restarcie rdzenia.
- Zapis jest merge, nie replace — pola nieobecne w żądaniu zachowują obecną wartość, a pola spoza schemy są ignorowane.
- Schema może być dynamiczna: funkcja
schema(actualConfig)dostaje aktualne wartości bloku (dlascope: 'user'— wartości edytującego użytkownika). - Nie duplikuj bloków w wielu wywołaniach —
registerConfigBlocksmożesz wywołać kilka razy, ale zestaw zgłoszony do platformy zawsze odzwierciedla sumę zarejestrowanych bloków. - Wartości z formularza czytaj zwykłym
Config.get('blok.pole')— także we własnych metodach, hookach i akcjach automatyzacji. - Akcja bloku to dobre miejsce na operacje, których nie da się wyrazić formularzem (test połączenia, dodanie pozycji do listy, ręczna synchronizacja) — ale sam zapis wartości zostaw przyciskowi Zapisz i funkcji
validate.
