Skripty konektoru: vykonna cast s manifestem a kontrolou parametru
Konektory dostaly vykonnou cast. Jeden skript je jeden soubor, ktery nese manifest (vstupni a vystupni parametry) i kod. Diky manifestu s nim umi pracovat strom automatizace, aniz by o kodu cokoliv vedel. Soubory jsou zamerne obycejny JavaScript, ne TypeScript. TypeScript by se musel prelozit a to je presne to otaceni, ktere tady nema byt. Registr sleduje cas zmeny souboru, takze uprava v portalu, rucni uprava souboru i novy soubor ve slozce funguji stejne a bez restartu. Pridano: - scripts/ se skripty konektoru, nazev souboru je zaroven ID operace - kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany. Chybejici povinny vystup je chyba skriptu, ne uzivatele - jinak by strom veril parametru, ktery nikdy nedosel - ctx predavany skriptu: http nad adresou napojeni, util, log, config, idempotencyKey, fail a retry. Skript nedostane pristupove udaje - rozliseni opakovatelne a koncove chyby. Runner nikdy nevyhodi vyjimku, vzdy vraci vysledek vcetne retryable - redakce tajnych hodnot pred zapisem do logu. Cizi API rado vraci prijaty token v chybove zprave a log ticketu vidi klient - napojeni z environment variables vcetne iDokladu - sest ukazkovych skriptu pro iDoklad proti skutecnemu API sluzby services.csbot.cz/apps/idoklad, kazdy na jiny vzor - stranka /dashboard/skripty: seznam, manifest, editor, zkusebni spusteni. Formular testu se sklada z manifestu, nepise se pro kazdy skript - endpointy /api/dashboard/scripts vcetne Swaggeru Zmeneno: - katalog konektoru uz neni jen staticky seznam. Akce ze skriptu se domeruji prekryvem v src/data/connectors.ts, takze se naraz objevi ve validaci stromu, ve vypoctu scope i v sablonach. Pri stejnem ID vyhrava skript - ConnectorOperation ma implementation a scriptId - ApiError na klientovi nese cele telo odpovedi a umi z nej vytahnout issues - Dockerfile kopiruje scripts/ do vysledneho image Ukladani nemuze rozbit fungujici skript: kod se nejdriv zapise do docasneho souboru, ten se nacte a overi, a az pak prepise puvodni. K tomu tri dokumenty navrhu dalsich kroku: 09 datove modely a prava, 10 runtime a rozpocet na 150 klientu, 11 popis skriptu konektoru. Overeno: npm run typecheck prochazi na serveru i webu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bbc2236c0d
commit
6f6b287d7e
@@ -30,6 +30,7 @@ React aplikaci ze slozky `dist/public`.
|
||||
| Kanaly WhatsApp, FB, Instagram | hotovo | vcetne vzorovych automatizaci na prijem |
|
||||
| Firmy a prava | hotovo | tri pohledy, uzivatel muze byt ve vic firmach |
|
||||
| Nastavitelny dashboard | hotovo | widgety, sirky a poradi, ulozene za uzivatele a firmu |
|
||||
| Skripty konektoru | hotovo | manifest, kontrola parametru, hot reload, iDoklad |
|
||||
| Sprava clenstvi z portalu | chybi | memberships jdou zmenit jen v kodu |
|
||||
| Bugs a wishes | chybi | vyvojarska agenda, samostatna evidence vedle ticketu |
|
||||
| Beh automatizaci | chybi | ulozeny strom se nevykonava, neni runtime |
|
||||
@@ -63,3 +64,12 @@ v [05-dashboard-a-builder.md](05-dashboard-a-builder.md).
|
||||
|
||||
Za rozmysleni stoji evidence bugs a wishes. Zamerne to nejsou tickety,
|
||||
duvod je v [06-tickety.md](06-tickety.md).
|
||||
|
||||
Prvni cast navrhu uz je hotova: vykonna cast konektoru, tedy skripty
|
||||
s manifestem a kontrolou parametru, viz [11-skripty-konektoru.md](11-skripty-konektoru.md).
|
||||
Runner je pripraveny, chybi nad nim fronta.
|
||||
|
||||
Zbytek navrhu je ve dvou souborech, oba jsou navrh k rozhodnuti, ne popis stavu:
|
||||
[09-navrh-rozsireni.md](09-navrh-rozsireni.md) pro datove modely a prava,
|
||||
[10-runtime-a-kapacita.md](10-runtime-a-kapacita.md) pro frontu, beh kroku
|
||||
a rozpocet na 150 klientu.
|
||||
|
||||
@@ -37,6 +37,11 @@ Vyzaduji `Authorization: Bearer <token>`:
|
||||
| POST | `/api/dashboard/tickets/:id/comment` |
|
||||
| GET | `/api/dashboard/incidents` |
|
||||
| GET | `/api/dashboard/connectors` |
|
||||
| GET | `/api/dashboard/scripts` |
|
||||
| GET | `/api/dashboard/scripts/:id` |
|
||||
| PUT | `/api/dashboard/scripts/:id` |
|
||||
| POST | `/api/dashboard/scripts/:id/test` |
|
||||
| POST | `/api/dashboard/scripts/reload` |
|
||||
| GET | `/api/dashboard/stream` |
|
||||
| GET | `/api/dashboard/automations` |
|
||||
| POST | `/api/dashboard/automations` |
|
||||
@@ -166,6 +171,25 @@ ticket zustane bez zakaznika i bez resitele a v logu je videt proc.
|
||||
Nevyplnena pole server doplni ukazkovou hodnotou. U akci s "resolved" se bez
|
||||
zadaneho id pouzije prvni nevyrizeny zaznam.
|
||||
|
||||
## Skripty konektoru
|
||||
|
||||
Popis modelu je v [11-skripty-konektoru.md](11-skripty-konektoru.md), tady jen API.
|
||||
|
||||
Cteni smi kazdy prihlaseny, protoze builder potrebuje vedet, co skript umi.
|
||||
Uprava, zkusebni spusteni a vynucene nacteni smi **jen spravce platformy** -
|
||||
uprava skriptu meni chovani vseho, co ho pouziva.
|
||||
|
||||
`GET /api/dashboard/scripts` vraci vedle manifestu i `problems` s rozbitymi
|
||||
skripty a `connections` se stavem napojeni. **Hodnoty pristupovych udaju se
|
||||
nevraci nikdy**, jen jmena chybejicich environment variables.
|
||||
|
||||
`PUT /api/dashboard/scripts/:id` kod nejdriv nacte a overi a az pak prepise
|
||||
soubor. Rozbita uprava vraci 400 s `issues` a puvodni skript dal funguje.
|
||||
|
||||
`POST /api/dashboard/scripts/:id/test` **vola opravdovou sluzbu**. Chyba skriptu
|
||||
neni chyba API, vraci se 200 a popis v `error` vcetne toho, jestli ma smysl
|
||||
zkusit to znovu.
|
||||
|
||||
## Pri pridani endpointu
|
||||
|
||||
Soucasne aktualizovat `src/openapi.ts` a tenhle soubor. Swagger musi odpovidat
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,410 @@
|
||||
# 10 - Runtime, vykonna cast a kapacita
|
||||
|
||||
Navrh, ne popis stavu. Runtime neexistuje, dnes se ulozeny strom nevykonava.
|
||||
Souvisejici navrh datovych modelu je v [09-navrh-rozsireni.md](09-navrh-rozsireni.md).
|
||||
|
||||
Tenhle soubor odpovida na tri veci: jak se vyhodnocuji kroky, jak se prijimaji
|
||||
udalosti a co to znamena pri 150 klientech.
|
||||
|
||||
## Nejdriv cisla, pak architektura
|
||||
|
||||
Zadani: 150 klientu, kazdy asi 5 systemu, z nich chodi radove desitky udalosti.
|
||||
To je 750 napojeni. "Desitky udalosti" ma dve cteni a **odpoved se mezi nimi
|
||||
podstatne lisi**, takze obe:
|
||||
|
||||
| Scenar | Desitky udalosti za | Udalosti/den | Kroku/den | Prumer | Spicka |
|
||||
| ------ | ------------------- | ------------ | ---------- | ------- | -------- |
|
||||
| A | den a system | 37 tisic | 375 tisic | 4 kr/s | 30-60/s |
|
||||
| B | hodinu a system | 450 tisic | 4,5 mil | 52 kr/s | 200-400/s|
|
||||
|
||||
Pocitano s 10 kroky na udalost, coz je stredni automatizace. Spicka vychazi
|
||||
z toho, ze provoz je v osmihodinovem okne a uvnitr nerovnomerny, tedy radove
|
||||
osmkrat nad prumerem.
|
||||
|
||||
**Planovat se musi na kroky, ne na udalosti.** Jedna udalost s trisetkrokovym
|
||||
stromem stoji tristakrat vic nez udalost s jednim krokem. Az bude runtime bezet,
|
||||
je metrika kroku za sekundu ta jedina, podle ktere se da neco rict.
|
||||
|
||||
### Co je a co neni uzke misto
|
||||
|
||||
| Vec | Scenar A | Scenar B |
|
||||
| ----------------------- | --------------- | ------------------------------ |
|
||||
| Fronta v Postgresu | par procent | zvladne, ale s davkovym odberem|
|
||||
| Soubezne HTTP volani | 15 soubezne | 90 soubezne, Node se nezapoti |
|
||||
| **Zapis `run_step`** | 22 GB/mesic | **270 GB/mesic, nutne zkratit**|
|
||||
| Limity cizich API | uzke misto | uzke misto |
|
||||
|
||||
Fronta nad Postgresem s `FOR UPDATE SKIP LOCKED` uklidne obslouzi radove
|
||||
200 az 500 uloh za sekundu na jednom uzlu, kdyz se odebira davkove. Scenar A
|
||||
je tedy nezajimavy a scenar B je v pohodlnem pasmu.
|
||||
|
||||
**Skutecne uzke misto je objem zapisu.** Radek `run_step` s vstupem a vystupem
|
||||
v JSONB ma realne 1 az 3 kB. Pri scenari B je to 9 GB denne, coz za pul roku
|
||||
nikdo neuklidi. Reseni je v sekci o retenci a je to jedina vec z celeho navrhu,
|
||||
kterou **nelze odlozit na potom**.
|
||||
|
||||
Druhe uzke misto je za nasimi hranicemi. iDoklad, WhatsApp ani ekonomicky system
|
||||
nesnesou desitky pozadavku za sekundu na jeden ucet. Skalovani workeru bez limitu
|
||||
za napojeni znamena jen rychleji dojit k odpovedi 429.
|
||||
|
||||
### Co pri teto velikosti nepotrebujeme
|
||||
|
||||
Rict to nahlas, aby se to nestavelo: **zadna Kafka, zadny Redis, zadny Kubernetes,
|
||||
zadne sharding.** 150 klientu je pro jeden Postgres a par procesu Node maly
|
||||
provoz. Usili patri do idempotence, spravedlnosti mezi klienty a pozorovatelnosti,
|
||||
ne do infrastruktury.
|
||||
|
||||
Odhad velikosti:
|
||||
|
||||
| Scenar | Postgres | Workeri | Kde to bezi |
|
||||
| ------ | ------------------ | --------------------------- | ----------- |
|
||||
| A | 4 vCPU, 16 GB | 2 procesy, 50 soubezne | jeden stroj |
|
||||
| B | 8-16 vCPU, 32 GB, NVMe | 4-6 procesu, 100 soubezne | dva stroje |
|
||||
|
||||
## Moznosti u databaze
|
||||
|
||||
| Varianta | Verdikt pri 150 klientech |
|
||||
| -------------------------------- | ------------------------------------------------ |
|
||||
| Jedno DB, `tenant_id` ve sloupci | **ano, tohle** |
|
||||
| Schema na klienta | ne: 150 x 20 tabulek je 3000 tabulek, migrace se stanou nespolehlivymi |
|
||||
| Databaze na klienta | ne, ale nechat si dvere otevrene |
|
||||
| Partitionovani podle klienta | ne, oddily by byly velikostne nesouvisle |
|
||||
| Partitionovani podle casu | **ano, u pripisovacich tabulek** |
|
||||
|
||||
### Dvere k oddelene databazi za par korun
|
||||
|
||||
Jednou prijde klient, ktery bude chtit vlastni databazi, nebo bude delat
|
||||
tricet procent provozu. Aby to pak nebyla prestavba, staci **jedna vec od zacatku**:
|
||||
pristup k poolu jen pres `dbFor(tenantId)`, i kdyz zpocatku vraci porad tentyz
|
||||
pool. K tomu **nikdy nespojovat dotazem dva klienty**, coz uz vynucuje povinny
|
||||
argument `tenantIds` v ulozistich.
|
||||
|
||||
Splneni tehle dvou podminek znamena, ze presun jednoho klienta do vlastni
|
||||
databaze je konfigurace, ne prepisovani dotazu.
|
||||
|
||||
### Pool a jedno pravidlo, na kterem to stoji nebo pada
|
||||
|
||||
**Worker nesmi drzet spojeni do databaze po dobu volani ciziho API.** Je to
|
||||
nejcastejsi zpusob, jak takovy system umre. Volani do iDokladu trva 300 ms.
|
||||
Kdyz drzi spojeni, znamena 90 soubeznych kroku 90 obsazenych spojeni a pool
|
||||
skonci.
|
||||
|
||||
Spravne poradi:
|
||||
|
||||
```
|
||||
transakce: odeber ulohu (2 ms) - spojeni drzim
|
||||
uvolni spojeni
|
||||
volani ciziho API (300 ms) - spojeni nedrzim
|
||||
transakce: zapis vysledek (2 ms) - spojeni drzim znovu
|
||||
```
|
||||
|
||||
Pri tomhle poradi staci na 90 soubeznych kroku 3 az 5 spojeni. Bez nej 90.
|
||||
|
||||
Az bude workeru vic, prijde PgBouncer v transakcnim rezimu. **Pozor: v transakcnim
|
||||
rezimu nefunguje `LISTEN/NOTIFY`**, a prave na nem ma podle
|
||||
[09-navrh-rozsireni.md](09-navrh-rozsireni.md) stat sbernice udalosti pro SSE.
|
||||
Ta potrebuje prime spojeni mimo PgBouncer. Zjistit to az pri nasazeni znamena
|
||||
rozbity zivy dashboard.
|
||||
|
||||
## Cesta udalosti: tri oddelene faze
|
||||
|
||||
```
|
||||
POST /webhook/:token -> event_inbox 202, rychle a hloupe
|
||||
|
|
||||
dispatcher udalost na N behu
|
||||
|
|
||||
worker krok po kroku
|
||||
```
|
||||
|
||||
Rozdeleni na tri faze neni akademicke. Kazda ma jinou vlastnost: prijem musi byt
|
||||
rychly, rozeslani musi byt idempotentni, vykonavani musi byt prerusitelne.
|
||||
|
||||
### 1. Prijem: rychle a hloupe
|
||||
|
||||
```sql
|
||||
event_inbox(id, tenant_id, token_id, source_kind, source_id,
|
||||
idempotency_key, payload jsonb, received_at,
|
||||
status, dispatched_at, cause_run_id, depth)
|
||||
|
||||
unique index on (token_id, idempotency_key)
|
||||
```
|
||||
|
||||
Webhook **nesmi vyhodnocovat strom**. Overi token, overi velikost, zapise jeden
|
||||
radek, vrati 202. Cil je p99 pod 50 ms.
|
||||
|
||||
Duvod je praktickeho razu: odesilatel pri timeoutu opakuje. Kdyz webhook ceka
|
||||
na iDoklad, pomaly iDoklad zpusobi, ze tataz objednavka prijde tri krat.
|
||||
|
||||
- **Idempotence pri prijmu.** Hlavicka `Idempotency-Key`, nebo hash tela, kdyz
|
||||
ji odesilatel neposila. Unikatni index nad `(token_id, idempotency_key)`
|
||||
v okne 24 hodin. Duplikat vrati 202 a stejne ID udalosti, ne chybu - pro
|
||||
odesilatele to je uspech, protoze jeho udalost je prijata.
|
||||
- **Limit za token.** Jeden rozbity klient ve smycce nesmi zaplnit inbox.
|
||||
- **Strop velikosti tela**, radove 256 kB, vetsi 413.
|
||||
- **Backpressure.** Kdyz hloubka fronty prekroci hranici, vracet 429 tokenum,
|
||||
ktere nejsou oznacene jako kriticke. Odesilatele 429 umi, na rozdil od tiche
|
||||
latence rostouci do minut.
|
||||
|
||||
### 2. Vlastni udalosti nechodi pres HTTP
|
||||
|
||||
Podle zadani budou udalosti vznikat volanim na webhook z jinych automatizaci.
|
||||
U cizich odesilatelu ano. **U nasich vlastnich ne.**
|
||||
|
||||
Volat vlastni HTTP endpoint na sebe pridava latenci, obsazuje spojeni a zaklada
|
||||
poruchu, ktera nemusi existovat. Vnitrni udalost zapise radek do `event_inbox`
|
||||
primo, prochazi tim samym dispatcherem a je v tom samem prehledu. Webhook zustava
|
||||
pro to, co prichazi zvenci.
|
||||
|
||||
### 3. Ochrana proti smycce musi projit skrz udalost
|
||||
|
||||
Tohle je nejvaznejsi dusledek toho, ze automatizace vyrabeji udalosti pro jine
|
||||
automatizace.
|
||||
|
||||
`depth` na behu chrani jen **uvnitr jednoho behu**. Kdyz automatizace A vyrobi
|
||||
udalost, ktera spusti B, a B vyrobi udalost, ktera spusti A, tak kazdy jednotlivy
|
||||
beh ma hloubku 1 a kontrola nikdy nezasahne. Smycka pobezi, dokud ji nekdo
|
||||
nevsimne na uctu za cizi API.
|
||||
|
||||
Proto `event_inbox` nese `cause_run_id` a `depth`, a plati:
|
||||
|
||||
```
|
||||
depth nove udalosti = depth behu, ktery ji vyrobil, + 1
|
||||
depth > 5 -> udalost se odmitne, zapise se do logu a upozorni se
|
||||
```
|
||||
|
||||
Bez tohohle jednoho sloupce je navrh z bodu 9 generator nekonecnych smycek.
|
||||
|
||||
### 4. Rozeslani
|
||||
|
||||
Dispatcher najde automatizace, ktere na dvojici klient a spoustec sedi, a zalozi
|
||||
beh pro kazdou.
|
||||
|
||||
- **Filtr na spousteci se vyhodnoti tady**, jeste pred zalozenim behu. Je to
|
||||
odpoved na otevrene rozhodnuti z konce [06-tickety.md](06-tickety.md) a pri
|
||||
tomhle objemu to neni kosmetika: beh, ktery hned skonci, stejne zaplati zapis
|
||||
do `run`, `run_step` i `ticket_trace`.
|
||||
- **Jedna transakce**: oznac udalost jako rozeslanou, zaloz behy, zaloz prvni
|
||||
ulohy. Bud vse, nebo nic.
|
||||
- **Unikatni index nad `(event_id, automation_id)`.** Kdyz dispatcher padne
|
||||
uprostred, opakovane rozeslani nezalozi druhy beh.
|
||||
|
||||
## Vykonna cast: co ridi prechod mezi kroky
|
||||
|
||||
Dve veci, oddelene.
|
||||
|
||||
**1. Cista funkce.** `next(tree, path, outputs): FlowPath | null` rozhodne, ktery
|
||||
krok je dalsi. Zadny stav, zadne IO, testovatelne. Podminka se vyhodnoti nad
|
||||
`outputs` a vybere vetev. Chuze po strome uz z poloviny existuje ve
|
||||
`web/src/lib/flow.ts` a `src/data/flowScope.ts`.
|
||||
|
||||
**2. Radek v tabulce.** Fakticky prubeh behu drzi zaznam ulohy v databazi,
|
||||
ne pamet procesu. **Nikdy `setTimeout`, nikdy dlouhy retez promisu, nikdy
|
||||
rekurze drzici cely beh.** Restart containeru je bezna vec a beh ho musi prezit.
|
||||
|
||||
```sql
|
||||
run(id, tenant_id, source_kind, source_id, source_version,
|
||||
event_id, trigger_type, status, depth, queue_key,
|
||||
started_at, finished_at, error)
|
||||
|
||||
run_step(id, run_id, path, step_id, attempt, status,
|
||||
input jsonb, output jsonb, error, started_at, finished_at)
|
||||
|
||||
job(id, run_id, tenant_id, next_path, run_after, attempts,
|
||||
queue_key, locked_by, locked_until, priority)
|
||||
```
|
||||
|
||||
`source_kind` a `source_version` rikaji, jestli beh patri automatizaci nebo akci
|
||||
a podle ktere verze jeji definice se ma dokoncit. To je to, co dela z cekaciho
|
||||
kroku fungujici vec: beh cekajici tyden dobehne podle stromu, ktery platil pri
|
||||
jeho spusteni.
|
||||
|
||||
`run_after` v tabulce `job` je zaroven **cele cekani z bodu 8**. Krok `wait` neni
|
||||
v runtimu vyjimka, je to obycejny krok, ktery misto volani sluzby nastavi
|
||||
`run_after` a skonci. Proto ten bod skoro nic nestoji.
|
||||
|
||||
### Smycka workeru
|
||||
|
||||
```
|
||||
1. transakce: odeber DAVKU uloh
|
||||
SELECT ... FROM job
|
||||
WHERE run_after <= now()
|
||||
AND (locked_until IS NULL OR locked_until < now())
|
||||
AND tenant_id <> ALL (:klienti_na_stropu)
|
||||
ORDER BY priority, run_after
|
||||
FOR UPDATE SKIP LOCKED LIMIT 25
|
||||
UPDATE job SET locked_by = :worker, locked_until = now() + interval '2 min'
|
||||
|
||||
2. uvolni spojeni, vykonej ulohy soubezne, kazda PRAVE JEDEN krok
|
||||
|
||||
3. za kazdou ulohu transakce: zapis vysledek kroku
|
||||
smaz hotovou ulohu
|
||||
zarad dalsi ulohu podle next()
|
||||
```
|
||||
|
||||
**Krok 3 v jedne transakci je cely trik.** Bud se zapise vysledek i dalsi uloha,
|
||||
nebo nic. Nikdy nevznikne beh, ktery ma hotovy krok a nema pokracovani, ani
|
||||
dvakrat zarazeny stejny krok.
|
||||
|
||||
**Davkovy odber je nejvetsi pacidlo na propustnost.** Po jedne uloze znamena pri
|
||||
300 krocich za sekundu 300 odberovych transakci za sekundu. Po dvaceti peti
|
||||
je jich dvanact. Je to jedna zmena `LIMIT` a nekolikanasobne mensi zatez.
|
||||
|
||||
`SKIP LOCKED` znamena, ze workeru muze byt libovolne mnoho a nepotrebuji
|
||||
koordinatora. Za zvazeni stoji `graphile-worker` - je to tentyz princip nad
|
||||
Postgresem, overeny provozem. Rucne az kdyz bude potreba spravedlnost podle
|
||||
`queue_key`, kterou hotova knihovna neresi.
|
||||
|
||||
### Spravedlnost mezi klienty
|
||||
|
||||
Ciste FIFO znamena, ze jeden vecerni import u jednoho klienta zastavi ostatnich
|
||||
149. Pri 150 klientech to neni hypoteza, je to otazka casu.
|
||||
|
||||
Prakticky pouzitelna verze je dvojice:
|
||||
|
||||
- **Semafor za klienta ve workeru**: nejvyse N soubeznych kroku na klienta.
|
||||
- **Odberovy dotaz preskoci klienty na stropu** (`tenant_id <> ALL (...)`).
|
||||
Seznam si worker drzi sam a je aktualni na jednu davku.
|
||||
|
||||
Presna spravedlnost cistym SQL je slozita a nevyplati se. Tohle je odhadem
|
||||
o dva rady jednodussi a rozdil nikdo nepozna.
|
||||
|
||||
K tomu dve dalsi hranice:
|
||||
|
||||
- **Limit a rychlostni strop za napojeni, ne za konektor.** Kvota je na uctu
|
||||
klienta v iDokladu, ne na tom, ze iDoklad existuje. Zetonovy kosik jednim
|
||||
`UPDATE ... RETURNING` nad radkem napojeni.
|
||||
- **Serializace nad jednim ticketem.** `queue_key = ticket:<id>` a jen jedna
|
||||
bezici uloha na klic. Bez toho dve automatizace prepisuji stav teze veci
|
||||
a poradi neni dane.
|
||||
|
||||
### Pady
|
||||
|
||||
- **Lease.** Worker padne uprostred kroku, `locked_until` vyprsi, ulohu si vezme
|
||||
jiny. Nic se neztrati.
|
||||
- **Kroky musi byt idempotentni.** Kazdy krok dostane
|
||||
`idempotencyKey = runId + ':' + path`, **stabilni pres vsechny pokusy**.
|
||||
Konektory, ktere umi `Idempotency-Key`, ho dostanou a druhy pokus nevystavi
|
||||
druhou fakturu. Klic za pokus by byl k nicemu, o tom to cele je.
|
||||
- **Retry.** Exponencialni backoff s jitterem, radove 5 pokusu. Rozlisit
|
||||
opakovatelne (timeout, spojeni, 429, 5xx) od koncovych (400, 401, 403,
|
||||
validace). Koncovou chybu neopakovat, jen se tim vypali kvota.
|
||||
- **Po vycerpani pokusu** dostane beh stav `failed`, zapise se do logu ticketu
|
||||
a vznikne udalost. Beh zustane a **lze ho pokracovat od padleho kroku**,
|
||||
protoze stav je per krok, ne per beh.
|
||||
- **Limit kroku na beh** a globalni timeout behu.
|
||||
- **Exactly-once neexistuje.** Cil je at-least-once plus idempotence. Kdo slibi
|
||||
exactly-once, jen jeste nenasel pripad, kdy to nedrzi.
|
||||
|
||||
## Kde bezi skripty
|
||||
|
||||
Skripty z bodu 9 jsou cisty prevod dat, ale i tak maji vlastni provozni pravidlo,
|
||||
a je dulezite:
|
||||
|
||||
**Skript nesmi bezet v hlavnim vlakne workeru.** Skript, ktery pocita
|
||||
pet set milisekund, zablokuje smycku udalosti a s ni **vsechny ostatni soubezne
|
||||
kroky toho workeru**. Jeden nepovedeny cyklus u jednoho klienta tim zastavi
|
||||
provoz vsech ostatnich, a v logu to vypada jako pomala cizi API.
|
||||
|
||||
Navrh:
|
||||
|
||||
- Bazen `worker_threads`, v kazdem `isolated-vm`. Radove tolik vlaken, kolik je
|
||||
jader.
|
||||
- Tvrdy timeout 50 az 200 ms a **zabiti vlakna** pri prekroceni, ne zdvorile
|
||||
preruseni. Prerusit smycku `while (true)` jinak nejde.
|
||||
- Zkompilovany skript se drzi v cache za verzi, kontext se po N spustenich zahodi
|
||||
kvuli unikum pameti.
|
||||
- Rezie kontextu je radove milisekunda, takze tisic skriptovych kroku za sekundu
|
||||
neni problem.
|
||||
|
||||
Kdyz nekdy bude potreba skript, ktery neco vola nebo dlouho pocita, nedostane
|
||||
vic pravomoci. Stane se **vlastni sluzbou v AppFactory** a v katalogu konektorem,
|
||||
ktery ji vola. Tim pro nej zacne platit retry, rate limit i audit jako pro kazdy
|
||||
jiny krok.
|
||||
|
||||
Skripty se nikdy nespousti v procesu API. Portal nesmi zpomalit kvuli tomu,
|
||||
ze nekdo ulozil spatny cyklus.
|
||||
|
||||
## Retence a objem, hned pri navrhu schematu
|
||||
|
||||
Tohle je jedina vec, ktera pri scenari B rozhoduje o tom, jestli to za pul roku
|
||||
jde provozovat.
|
||||
|
||||
**Zkracovani obsahu.** Plny vstup a vystup kroku se uklada jen u kroku, ktere
|
||||
selhaly, plus u male vzorku uspesnych. U ostatnich se uklada velikost, hash
|
||||
a prvnich radove 512 bajtu. Snizi to objem radove desetkrat a neztrati to nic,
|
||||
co by nekdo cetl - do uspesneho kroku se nikdo nechodi divat.
|
||||
|
||||
**Partitionovani po mesicich** u pripisovacich tabulek: `run_step`,
|
||||
`ticket_trace`, `event_inbox`, `audit`. Mazani stareho oddilu je pak
|
||||
`DROP TABLE`, ne `DELETE` bezici pres noc.
|
||||
|
||||
**Retence** podle toho, kdo to cte:
|
||||
|
||||
| Data | Jak dlouho |
|
||||
| --------------------- | ----------------- |
|
||||
| Vstupy a vystupy kroku| 30 dni |
|
||||
| Souhrn behu | 12 mesicu |
|
||||
| Log ticketu | 90 dni |
|
||||
| Audit | dele, dane pravni potrebou |
|
||||
|
||||
Cisla patri do nastaveni za klienta, protoze delsi retence je dobry duvod
|
||||
pro drazsi tarif.
|
||||
|
||||
## Co se monitoruje
|
||||
|
||||
Ne CPU. Ctyri veci, a kazda odpovida na jinou otazku:
|
||||
|
||||
| Metrika | Odpovida na |
|
||||
| ----------------------------------- | --------------------------------- |
|
||||
| Hloubka fronty | stiha se to |
|
||||
| **Vek nejstarsi pripravene ulohy** | je to zahlcene, nebo zaseknute |
|
||||
| Kroku za sekundu, p95 za konektor | kde to drhne |
|
||||
| Padle behy za hodinu, podil opakovani| co je rozbite |
|
||||
| Podil kroku za klienta | kdo je hlucny soused |
|
||||
| Zpozdeni inboxu (prijato az rozeslano) | stiha dispatcher |
|
||||
|
||||
Bez veku nejstarsi ulohy se neda odlisit "je hodne prace" od "nic se nedeje",
|
||||
a to jsou dva uplne jine problemy se stejnou hloubkou fronty.
|
||||
|
||||
## Kdy zmenit architekturu
|
||||
|
||||
Aby se to nemuselo rozhodovat dopredu. Do te doby plati navrh vyse.
|
||||
|
||||
| Signal | Co udelat |
|
||||
| ---------------------------------------- | ------------------------------------- |
|
||||
| Fronta zere nad 30 % CPU databaze | vetsi davky, pak fronta v Redisu (BullMQ) |
|
||||
| Zapisy `run_step` prevalcuji IO | zkratit obsah, vzorkovat, velka tela do objektoveho uloziste |
|
||||
| Jeden klient dela nad 30 % provozu | vlastni bazen workeru, pak vlastni databaze |
|
||||
| Fronta roste kazdy den ve spicce | pridat workery, jsou bezstavove |
|
||||
| Prevazuji chyby 429 z cizich API | limity za napojeni, pak vyjednat kvoty|
|
||||
| Cekajici behy jdou do stovek tisic | oddelena fronta pro dlouha cekani, aby nezdrzovala bezny odber |
|
||||
|
||||
## Jeden container, dve role
|
||||
|
||||
AppFactory nasazuje jednu aplikaci, takze worker nebude zvlastni sluzba.
|
||||
Rozdelit ho **procesne uvnitr image** pres `APP_ROLE=api|worker|both`
|
||||
s vychozim `both`. Az bude spicka takova, ze behy zpomaluji portal, nasadi se
|
||||
druha instance s `APP_ROLE=worker`. Zmena je jedna environment variable, zadny
|
||||
zasah do infrastruktury AppFactory.
|
||||
|
||||
Migrace pri startu potrebuji poradovy zamek (`pg_advisory_lock`), aby je pri
|
||||
soubeznem nasazeni nespustilo vic instanci najednou.
|
||||
|
||||
## Poradi, v jakem to stavet
|
||||
|
||||
1. `event_inbox`, webhook, idempotence pri prijmu, `depth` skrz udalost.
|
||||
Bez toho zbytek nema co zpracovavat a smycky jsou otevrene.
|
||||
2. `run`, `run_step`, `job`, `executeStep`, smycka po jedne uloze.
|
||||
Nejmensi verze, ktera vykona strom.
|
||||
3. Retry, lease, idempotency key ke konektorum.
|
||||
4. Dispatcher s filtrem na spousteci.
|
||||
5. Krok `wait` a prehled cekajicich behu.
|
||||
6. Davkovy odber, semafor za klienta, limity za napojeni.
|
||||
7. Zkracovani obsahu, partitionovani, retence.
|
||||
8. Bazen vlaken pro skripty.
|
||||
|
||||
Body 1 az 3 jsou nutne, aby vubec neco bezelo. Body 6 a 7 jsou to, co odlisuje
|
||||
scenar A od scenare B, a **daji se dodelat pozdeji bez prestavby** - vyzaduji ale,
|
||||
aby uz od zacatku existovaly sloupce `tenant_id` a `queue_key` v tabulce `job`
|
||||
a partitionovani u `run_step`. Pridat oddily do nejvetsi tabulky v systemu az
|
||||
potom je ta jedina cast, ktera by opravdu bolela.
|
||||
@@ -0,0 +1,315 @@
|
||||
# 11 - Skripty konektoru
|
||||
|
||||
Tohle uz neni navrh, je to naprogramovane. Navrh, ze ktereho to vzniklo, je
|
||||
v [09-navrh-rozsireni.md](09-navrh-rozsireni.md), bod 9.
|
||||
|
||||
## Co to je
|
||||
|
||||
Skript je **vykonna cast konektoru**. Jeden soubor, ktery nese dve veci:
|
||||
|
||||
- **manifest** - jak se operace jmenuje, co potrebuje na vstupu, co vraci na vystupu,
|
||||
- **kod** - co se ma opravdu udelat.
|
||||
|
||||
Diky manifestu s nim umi pracovat strom automatizace, aniz by o kodu cokoliv
|
||||
vedel. Builder z manifestu vykresli pole kroku a podminka za krokem se muze
|
||||
zeptat na jeho vystupy.
|
||||
|
||||
## Kde to je
|
||||
|
||||
```
|
||||
scripts/ soubory skriptu, obycejny JavaScript
|
||||
_sablona.js sablona ke zkopirovani (podtrzitko = nenacita se)
|
||||
idoklad.get-issued-invoice.js
|
||||
...
|
||||
src/scripts/types.ts co je skript, zod schema manifestu
|
||||
src/scripts/values.ts kontrola vstupu a vystupu
|
||||
src/scripts/util.ts pomocne funkce pro skripty, redakce tajemstvi
|
||||
src/scripts/connections.ts kam se vola a cim se to autorizuje
|
||||
src/scripts/http.ts HTTP klient predany skriptu
|
||||
src/scripts/manifest.ts overeni manifestu, prevod na operaci katalogu
|
||||
src/scripts/registry.ts nacitani ze souboru, hot reload, ukladani
|
||||
src/scripts/runner.ts spusteni jednoho skriptu
|
||||
src/routes/scripts.ts API
|
||||
web/src/pages/dashboard/Scripts.tsx stranka /dashboard/skripty
|
||||
```
|
||||
|
||||
## Nic se neotaci
|
||||
|
||||
Soubory jsou zamerne **obycejny JavaScript, ne TypeScript**. TypeScript by se
|
||||
musel prelozit, a to je presne to otaceni, ktere tady nema byt.
|
||||
|
||||
Registr si drzi cas zmeny souboru a pri zmene ho nacte znovu. Prohledava
|
||||
nejvyse jednou za sekundu, takze cteni katalogu neznamena stat na kazdy dotaz.
|
||||
|
||||
Uprava tedy funguje trema cestami a vzdy stejne:
|
||||
|
||||
| Kudy | Co se stane |
|
||||
| -------------------------------- | -------------------------------------------- |
|
||||
| Editor v portalu | ulozi soubor, registr ho nacte hned |
|
||||
| Rucni uprava souboru na serveru | registr si zmeny vsimne pri dalsim dotazu |
|
||||
| Novy soubor ve slozce | objevi se jako nova operace v katalogu |
|
||||
|
||||
`POST /api/dashboard/scripts/reload` to jen vynuti hned, bez cekani.
|
||||
|
||||
## Nazev souboru je ID
|
||||
|
||||
Soubor se jmenuje `<konektor>.<operace>.js` a `manifest.id` musi byt stejne.
|
||||
Nesoulad je chyba, ne varovani - jinak by se skript ulozil pod jednim jmenem
|
||||
a nacetl pod druhym.
|
||||
|
||||
```
|
||||
scripts/idoklad.get-issued-invoice.js
|
||||
\_____/ \________________/
|
||||
konektor operace
|
||||
```
|
||||
|
||||
Z ID se dopocita, do ktereho konektoru operace patri, takze se to nepise
|
||||
dvakrat. Konektor **musi existovat** v `src/data/connectors.ts`, jinak se skript
|
||||
ohlasi jako problem.
|
||||
|
||||
## Manifest
|
||||
|
||||
```js
|
||||
export const manifest = {
|
||||
id: 'idoklad.get-issued-invoice',
|
||||
name: 'Získat vydanou fakturu',
|
||||
description: 'Načte vydanou fakturu z iDokladu podle jejího ID.',
|
||||
timeoutMs: 15000, // nepovinne
|
||||
inputs: [ /* ScriptField */ ],
|
||||
outputs: [ /* ScriptField */ ],
|
||||
};
|
||||
```
|
||||
|
||||
Parametr je pro vstup i vystup **tentyz tvar**. Kontrola je pak jedna funkce,
|
||||
ne dve skoro stejne, ktere by se casem rozesly.
|
||||
|
||||
| Klic | K cemu |
|
||||
| ----------- | ------------------------------------------------------------- |
|
||||
| `id` | pouziva se v sablonach jako `{{id}}`, jen pismena a podtrzitka |
|
||||
| `label` | co vidi uzivatel v builderu |
|
||||
| `type` | `string`, `number`, `boolean`, `date` |
|
||||
| `required` | u vstupu: bez hodnoty se skript nespusti. U vystupu: musi ho vratit |
|
||||
| `hint` | napoveda pod polem |
|
||||
| `options` | vyber z hodnot, jina neprojde |
|
||||
| `pattern` | dalsi kontrola regularnim vyrazem (jen `string`) |
|
||||
| `multiline` | pole na vic radku (jen `string`) |
|
||||
| `default` | dosadi se, kdyz hodnota chybi a parametr neni povinny |
|
||||
|
||||
Schema manifestu je `.strict()`. Preklep v nazvu klice (`outputFileds`) se ohlasi,
|
||||
ne tise ignoruje.
|
||||
|
||||
## Kontrola vstupu a vystupu
|
||||
|
||||
Poradi je vzdy stejne: **overit vstup, spustit, overit vystup**.
|
||||
|
||||
Overeni vystupu neni pridavek. Bez nej by strom veril parametru, ktery nikdy
|
||||
nedosel, a podminka za krokem by se rozhodovala podle `undefined`.
|
||||
|
||||
Pravidla:
|
||||
|
||||
- povinny parametr bez hodnoty je chyba, ne prazdny retezec,
|
||||
- nepovinny parametr bez hodnoty dostane `default`, jinak `null`,
|
||||
- hodnota se prevede na deklarovany typ, kdyz to jde bez hadani. Ceska
|
||||
desetinna carka projde, `"ano"` u typu boolean taky,
|
||||
- parametr, ktery v manifestu neni, se zahodi a zaloguje. Stejne jako u webhooku:
|
||||
odesilatele posilaji i vlastni data a odmitat je by rozbijelo integrace,
|
||||
- chyby se vraci **vsechny najednou**, ne jen prvni.
|
||||
|
||||
Chybejici povinny vystup je chyba **skriptu**, ne uzivatele, a hlasi se jinym
|
||||
druhem (`output`).
|
||||
|
||||
## Co skript ma a co nema
|
||||
|
||||
Skript ma jen `ctx`. Zadny import, zadny pristup na sit mimo `ctx.http`
|
||||
a **zadne pristupove udaje**.
|
||||
|
||||
```js
|
||||
export async function run(inputs, ctx) { /* ... */ }
|
||||
```
|
||||
|
||||
| Na kontextu | K cemu |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| `ctx.http` | `get`, `post`, `patch`, `put`, `del` nad adresou napojeni |
|
||||
| `ctx.util` | pomocne funkce, viz nize |
|
||||
| `ctx.log` | radek do logu behu, vzdy zredigovany a zkraceny |
|
||||
| `ctx.config` | necitliva cast nastaveni napojeni |
|
||||
| `ctx.idempotencyKey` | stabilni pres vsechny pokusy tehoz kroku |
|
||||
| `ctx.fail` | koncova chyba, neopakuje se |
|
||||
| `ctx.retry` | docasna chyba, ma smysl zkusit znovu |
|
||||
|
||||
Adresu i autorizacni hlavicky doplnuje runtime podle napojeni. Skript rika
|
||||
`GET /issued-invoices/12` a nic vic. Duvod je v bodu 9 navrhu: kdyby skript
|
||||
znal tajemstvi, staci jeden `ctx.log` a je v logu, ktery vidi klient.
|
||||
|
||||
### Pomocne funkce
|
||||
|
||||
Cizi API vraci pokazde jinak. iDoklad pouziva velka pocatecni pismena a nekde
|
||||
obaluje odpoved do `Data`. Bez tehle sady by to kazdy skript resil znovu a jeden
|
||||
z nich by to resil spatne.
|
||||
|
||||
| Funkce | Co dela |
|
||||
| -------------------------- | -------------------------------------------------- |
|
||||
| `unwrap(body)` | rozbali `{ Data: x }` i `{ data: x }` |
|
||||
| `pick(obj, ...names)` | prvni existujici pole bez ohledu na velikost pismen |
|
||||
| `first(value)` | prvni prvek pole, nebo null |
|
||||
| `text`, `num`, `bool`, `date` | prevody s fallbackem |
|
||||
| `round(value, decimals)` | zaokrouhleni, uctuje se v halerich |
|
||||
| `need(value, label)` | vrati hodnotu, nebo skonci citelnou chybou |
|
||||
|
||||
## Chyby: opakovatelne a koncove
|
||||
|
||||
Rozdeleni je to podstatne. Timeout nebo 503 ma smysl zkusit znovu, spatny vstup
|
||||
nebo 403 ne - opakovat koncovou chybu jen vypali kvotu u cizi sluzby.
|
||||
|
||||
| Druh | Kdy | Opakovat |
|
||||
| ------------ | ------------------------------------------ | -------- |
|
||||
| `not_found` | skript neexistuje | ne |
|
||||
| `config` | chybi pristupove udaje, 401, 403 | ne |
|
||||
| `validation` | vstup neprosel kontrolou | ne |
|
||||
| `output` | skript nevratil deklarovany vystup | ne |
|
||||
| `terminal` | 400, 404, jina koncova odpoved sluzby | ne |
|
||||
| `retryable` | 408, 429, 5xx, chyba spojeni | ano |
|
||||
| `timeout` | skript nedobehl v limitu | ano |
|
||||
| `internal` | neocekavana vyjimka ve skriptu | ne |
|
||||
|
||||
Runner **nikdy nevyhodi vyjimku**. Vzdy vrati vysledek s `ok`, `outputs`, `logs`,
|
||||
`durationMs`, `httpCalls` a pripadne `error` vcetne `retryable`. Az bude
|
||||
existovat runtime automatizaci, bude tohle jeho jediny vstupni bod na kroku.
|
||||
|
||||
## Napojeni
|
||||
|
||||
Zatim jedno napojeni na konektor, sestavene z environment variables. Cilovy stav
|
||||
je napojeni za firmu v databazi, viz bod 9 navrhu. Az to bude, prepise se vnitrek
|
||||
`resolveConnection` a nic dalsiho.
|
||||
|
||||
| Promenna | K cemu |
|
||||
| --------------------------- | --------------------------------------------------- |
|
||||
| `SERVICES_BASE_URL` | zaklad adres, vychozi `https://services.csbot.cz/apps` |
|
||||
| `<KONEKTOR>_BASE_URL` | presmerovani jednoho konektoru |
|
||||
| `IDOKLAD_CLIENT_ID` | povinne pro iDoklad, jde do `X-ClientId` |
|
||||
| `IDOKLAD_CLIENT_SECRET` | povinne pro iDoklad, jde do `X-ClientSecret` |
|
||||
| `IDOKLAD_APPLICATION_ID` | jen partnerske aplikace |
|
||||
| `SCRIPTS_DIR` | jina slozka se skripty |
|
||||
| `SCRIPT_TIMEOUT_MS` | vychozi strop na beh, 15000 |
|
||||
| `SCRIPT_MAX_RESPONSE_BYTES` | strop na velikost odpovedi, 1000000 |
|
||||
| `ALLOW_PRIVATE_TARGETS` | povoli volani na localhost, **jen pro lokalni vyvoj** |
|
||||
|
||||
Autorizace konektoru je popsana v `authSpecs` v `src/scripts/connections.ts`.
|
||||
Novy konektor s pristupovymi udaji znamena jeden zaznam v teto tabulce.
|
||||
|
||||
**Hodnoty se z API nikdy nevraci.** `GET /api/dashboard/scripts` posila jen jmena
|
||||
chybejicich promennych a jmena vyplnenych hlavicek, nikdy hodnoty (AGENTS.md).
|
||||
|
||||
## Redakce tajemstvi
|
||||
|
||||
Cizi API rado vraci prijaty token v chybove zprave. Log ticketu ukazuje, co
|
||||
sluzba vratila, a zobrazuje se klientovi. Proto vsechno, co jde do logu nebo do
|
||||
chyby, projde nahradou znamych tajnych hodnot za hvezdicky.
|
||||
|
||||
Neni to volitelne dolazeni, je to soucast zapisu.
|
||||
|
||||
## Bezpecnostni hranice a co jeste chybi
|
||||
|
||||
Skripty ve slozce jsou **nase**, prosly gitem a code review. Bezi proto v procesu
|
||||
serveru, ne v sandboxu. Plati pro ne:
|
||||
|
||||
- **nemaji sit mimo `ctx.http`** a v nem nesmi mirit do vnitrni site
|
||||
(`ALLOW_PRIVATE_TARGETS` je jen pro vyvoj),
|
||||
- **nedostanou pristupove udaje**,
|
||||
- timeout je hlidany pres `AbortSignal`, takze prerusi cekani na sit.
|
||||
|
||||
Co to **neresi**: skript s `while (true)` timeout nezastavi. Runner vrati chybu,
|
||||
ale smycka bezi dal a blokuje hlavni vlakno. U nasich skriptu je to prijatelne,
|
||||
u zakaznickych ne - ti musi bezet v izolovanem enginu ve vlastnim vlakne.
|
||||
Podrobnosti v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md), sekce
|
||||
o skriptech.
|
||||
|
||||
## Napojeni do katalogu
|
||||
|
||||
Skript se domeri do katalogu konektoru jako akce s `implementation: 'script'`
|
||||
a `scriptId`. Kdyz nese ID operace, ktera uz v katalogu je, **skript vyhrava** -
|
||||
staticky zapis je popis toho, co umime, skript je to, co se opravdu stane.
|
||||
|
||||
Prekryv drzi `src/data/connectors.ts` (`setScriptActions`, `actionsFor`).
|
||||
Je to zamerne tam, protoze vsechno ostatni se uz pta pres `findOperation`.
|
||||
Tim se skripty naraz objevi ve validaci stromu, ve vypoctu toho, co je v kterem
|
||||
kroku videt, i v sablonach - bez toho, aby se to psalo trikrat.
|
||||
|
||||
V portalu jsou operace se skriptem oznacene ikonou v katalogu konektoru.
|
||||
|
||||
## API
|
||||
|
||||
| Metoda | Cesta | Kdo smi |
|
||||
| ------ | ----------------------------------------- | ---------------- |
|
||||
| GET | `/api/dashboard/scripts` | prihlaseny |
|
||||
| GET | `/api/dashboard/scripts/:id` | prihlaseny |
|
||||
| PUT | `/api/dashboard/scripts/:id` | spravce platformy |
|
||||
| POST | `/api/dashboard/scripts/:id/test` | spravce platformy |
|
||||
| POST | `/api/dashboard/scripts/reload` | spravce platformy |
|
||||
|
||||
Cteni smi kazdy prihlaseny - builder potrebuje vedet, co skript umi. Uprava meni
|
||||
chovani vseho, co skript pouziva, takze to neni pravo vedle prava zakladat tickety.
|
||||
|
||||
**Ukladani je bezpecne proti rozbiti.** Nejdriv se kod zapise do docasneho
|
||||
souboru, ten se nacte a overi, a az pak prepise puvodni. Rozbita uprava se
|
||||
neulozi a skript, ktery fungoval, funguje dal. Co presne nesedi, prijde
|
||||
v `issues`.
|
||||
|
||||
**Zkusebni spusteni vola opravdovou sluzbu.** Vystavena faktura opravdu vznikne.
|
||||
Zamerne: test, ktery volani predstira, nerekne nic o tom, jestli skript funguje.
|
||||
Portal na to upozornuje nad tlacitkem.
|
||||
|
||||
## Ukazkove skripty pro iDoklad
|
||||
|
||||
Postavene proti skutecnemu API sluzby na `https://services.csbot.cz/apps/idoklad`.
|
||||
Kazdy ukazuje jiny vzor, at je z ceho vychazet.
|
||||
|
||||
| Skript | Vzor |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| `idoklad.get-issued-invoice` | jedno volani a prevod odpovedi |
|
||||
| `idoklad.find-issued-invoice` | predvalidace: nenalezeno **neni** chyba |
|
||||
| `idoklad.find-contact` | vlastni kontrola vstupu (aspon jedno z dvojice) |
|
||||
| `idoklad.create-issued-invoice` | dve volani, vzor z `/default` a prepis jen znamych poli |
|
||||
| `idoklad.register-payment` | akce, ktera meni stav, plus idempotence |
|
||||
| `idoklad.send-invoice-email` | odpoved nic nevraci, vystup se sklada ze vstupu |
|
||||
|
||||
### Proc se u zakladani bere vzor z `/default`
|
||||
|
||||
iDoklad u faktury vyzaduje pole, ktera nikdo rucne vyplnovat nechce
|
||||
(`documentSerialNumber`, `isEet`, `isIncomeTax`). Skript proto nejdriv vezme
|
||||
predvyplneny vzor z `GET /issued-invoices/default` a prepise v nem **jen to,
|
||||
cemu rozumime**.
|
||||
|
||||
Kdyby se telo skladalo od nuly, rozbila by ho kazda zmena povinnych poli na
|
||||
strane iDokladu.
|
||||
|
||||
### Idempotence
|
||||
|
||||
Kazde volani nese hlavicku `Idempotency-Key` s hodnotou `ctx.idempotencyKey`.
|
||||
Klic je pro tentyz krok **stabilni pres vsechny pokusy**, takze druhy pokus
|
||||
po timeoutu nevystavi druhou fakturu. Klic za pokus by byl k nicemu, o tom to
|
||||
cele je.
|
||||
|
||||
## Jak pridat skript
|
||||
|
||||
1. Zkopirovat `scripts/_sablona.js` na `<konektor>.<operace>.js`.
|
||||
2. Srovnat `manifest.id` s nazvem souboru.
|
||||
3. Vyplnit `inputs` a `outputs`.
|
||||
4. Napsat `run`.
|
||||
5. Kdyz konektor jeste neni v `src/data/connectors.ts`, pridat ho.
|
||||
6. Kdyz potrebuje pristupove udaje, pridat zaznam do `authSpecs`
|
||||
v `src/scripts/connections.ts`.
|
||||
|
||||
Katalog, builder i stranka skriptu si ho vezmou samy. Nic se nerestartuje.
|
||||
|
||||
## Co chybi
|
||||
|
||||
| Chybi | Poznamka |
|
||||
| ---------------------------- | --------------------------------------------------- |
|
||||
| Napojeni za firmu | zatim jedno na konektor z environment variables |
|
||||
| Skripty od zakazniku | potrebuji sandbox a vlastni vlakno, viz vyse |
|
||||
| Verzovani skriptu | uprava prepise soubor, historie je jen v gitu |
|
||||
| Vykonavani ze stromu | runner je hotovy, ale runtime automatizaci neni |
|
||||
| Skripty jako spoustece | zatim jen akce, spoustec potrebuje runtime |
|
||||
| Metriky pro widgety | manifest to zatim nezna, viz bod 4 navrhu |
|
||||
| Ulozeni uprav mimo git | portal zapisuje do souboru v containeru, redeploy je vrati |
|
||||
@@ -2,6 +2,70 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-08-12 - skripty konektoru
|
||||
|
||||
Naprogramovana vykonna cast konektoru. Popis je
|
||||
v [11-skripty-konektoru.md](11-skripty-konektoru.md).
|
||||
|
||||
### Pridano
|
||||
|
||||
- `scripts/` se skripty konektoru. Jeden soubor nese manifest (vstupni a vystupni
|
||||
parametry) i kod. Obycejny JavaScript, aby se nemusel prekladat.
|
||||
- Hot reload podle casu zmeny souboru. Uprava v portalu i rucni uprava souboru
|
||||
se projevi bez restartu.
|
||||
- Kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany.
|
||||
Chybejici povinny vystup je chyba skriptu, ne uzivatele.
|
||||
- `ctx` predavany skriptu: `http` nad adresou napojeni, `util`, `log`, `config`,
|
||||
`idempotencyKey`, `fail` a `retry`. Skript nedostane pristupove udaje.
|
||||
- Rozliseni opakovatelne a koncove chyby. Runner nikdy nevyhodi vyjimku, vzdy
|
||||
vraci vysledek vcetne `retryable`.
|
||||
- Redakce tajnych hodnot pred zapisem do logu.
|
||||
- Napojeni z environment variables (`src/scripts/connections.ts`) vcetne iDokladu.
|
||||
- Sest ukazkovych skriptu pro iDoklad proti skutecnemu API sluzby
|
||||
`services.csbot.cz/apps/idoklad`, kazdy na jiny vzor.
|
||||
- Stranka `/dashboard/skripty`: seznam, manifest, editor, zkusebni spusteni.
|
||||
Formular testu se sklada z manifestu, nepise se pro kazdy skript.
|
||||
- Endpointy `/api/dashboard/scripts`, `/:id`, `PUT /:id`, `/:id/test` a `/reload`.
|
||||
Vse ve Swaggeru.
|
||||
|
||||
### Zmeneno
|
||||
|
||||
- Katalog konektoru uz neni jen staticky seznam. Akce ze skriptu se domeruji
|
||||
prekryvem v `src/data/connectors.ts`, takze se naraz objevi ve validaci stromu,
|
||||
ve vypoctu scope i v sablonach. Pri stejnem ID operace vyhrava skript.
|
||||
- `ConnectorOperation` ma `implementation` a `scriptId`. Katalog v portalu operace
|
||||
se skriptem oznacuje ikonou.
|
||||
- `ApiError` na klientovi nese cele telo odpovedi a umi z nej vytahnout `issues`.
|
||||
- Dockerfile kopiruje `scripts/` do vysledneho image.
|
||||
|
||||
### Vedome neudelano
|
||||
|
||||
Skripty bezi v procesu serveru, ne v sandboxu. Jsou nase a prosly gitem.
|
||||
Zakaznicke skripty budou potrebovat izolovany engine ve vlastnim vlakne, duvod
|
||||
je v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md).
|
||||
|
||||
Ulozeni z portalu zapisuje do souboru v containeru. Bez trvaleho svazku ho
|
||||
redeploy vrati na verzi z gitu.
|
||||
|
||||
## 2026-08-12 - navrhy
|
||||
|
||||
Pridany [09-navrh-rozsireni.md](09-navrh-rozsireni.md)
|
||||
a [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md).
|
||||
|
||||
09 popisuje datove modely: akce navazane na typ nebo tag ticketu s telem jako
|
||||
operaci, vlastnim stromem nebo skriptem, typy a tagy ticketu, role a prava jako
|
||||
data misto unionu, zalozky a zpristupneni konektoru za firmu, konektory rozdelene
|
||||
na definici, zpristupneni a napojeni, cekaci krok, sablony zprav, vlastni widgety
|
||||
se seskupovanim a prevod na Postgres. Soucasti je kontrola navrhu proti celemu
|
||||
prikladu se dvema firmami jednoho cloveka.
|
||||
|
||||
10 popisuje vykonnou cast: cestu udalosti od webhooku pres inbox a dispatcher
|
||||
k workeru, frontu v Postgresu se `SKIP LOCKED`, davkovy odber, spravedlnost mezi
|
||||
klienty, idempotenci, retence a rozpocet na 150 klientu ve dvou scenarich objemu.
|
||||
|
||||
Nic z toho neni naprogramovane, oba dokumenty jsou navrh k rozhodnuti.
|
||||
Kod se nemenil.
|
||||
|
||||
## 2026-08-03
|
||||
|
||||
Tickety predelane na plnohodnotny konektor. Prestavaji byt polozkou v seznamu
|
||||
|
||||
Reference in New Issue
Block a user