# 14 - Databaze Naprogramovano a overeno proti Postgresu 16. Zatim se do databaze ukladaji **konektory**, tedy pristupove udaje k sluzbam. Zbytek je v pameti procesu, poradi dalsich kroku je na konci. ## Databaze je volitelna, ale rezimy jsou oddelene Aplikace jede ve dvou rezimech a rozdil se resi **na jednom miste**, v `src/data/connectorStore.ts`. Nikde jinde se nezjistuje, jestli databaze je - kdyby se to rozlezlo po kodu, jedno misto by se zapomnelo. | Rezim | Kdy | Prezije restart | | ---------- | --------------------------------------- | --------------- | | `postgres` | je `DATABASE_URL` **i** `SECRETS_KEY` | ano | | `memory` | jinak | ne | Chybejici databaze **nesmi shodit start**. Container, ktery nenastartuje, je pro AppFactory nefunkcni sluzba (AGENTS.md). Misto toho se do logu napise, proc se jede v pameti, a portal to ukaze na strance Konektory. Databaze potrebuje **oboji**. Bez klice by se pristupove udaje ukladaly v plaintextu, a to je horsi nez ztratit je pri restartu - tabulku vidi kazda zaloha a kazdy dump pri ladeni. Stejne tak: kdyz jsou migrace nastavene, ale selzou, jede se dal v pameti. Psat do rozbiteho schematu je horsi nez neukladat. ## Promenne | Promenna | K cemu | | ------------------- | ---------------------------------------------------------- | | `DATABASE_URL` | `postgres://uzivatel:heslo@host:5432/csbot` | | `SECRETS_KEY` | klic pro sifrovani pristupovych udaju, **secret** | | `DATABASE_POOL_MAX` | kolik spojeni si vezme jedna instance, vychozi 10 | | `DATABASE_SSL` | `true` u spravovanych databazi, ktere vyzaduji TLS | `SECRETS_KEY` ma byt nahodny retezec, ne heslo: ```bash node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" ``` **Klic se nesmi ztratit ani zmenit bez prevodu dat.** Bez nej se ulozene udaje nerozsifruji. Nic se nerozbije, jen se konektory chovaji jako nevyplnene a v logu je napsane proc - udaje se pak zadaji znovu. ## Lokalni spusteni ```bash docker run -d --name csbot-pg -p 5433:5432 \ -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=csbot postgres:16-alpine export DATABASE_URL="postgres://postgres:devpass@127.0.0.1:5433/csbot" export SECRETS_KEY="$(node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))")" npm run dev ``` Migrace se pousti samy pri startu. Kontrola, ze to jede z databaze: ```bash curl -s http://localhost:3000/health/ready ``` Bez promennych `npm run dev` funguje dal, jen v pameti. ## Migrace Soubory `src/db/migrations/*.sql`, v abecednim poradi, kazdy jednou. Co uz proslo, je v tabulce `schema_migrations`. Dve veci, na kterych to stoji: - **Poradovy zamek.** Pri rolling deployi startuje vic instanci naraz a bez `pg_advisory_lock` by migrace pustily vsechny. - **Jeden soubor je jedna transakce.** Pri chybe se z nej neuplatni nic, takze nevznikne rozdelane schema, o kterem nikdo nevi. **Migrace se nikdy neupravuji zpetne.** Uz projely u nekoho jineho, takze zmena souboru znamena dve rozdilna schemata se stejnym cislem. Oprava je vzdy novy soubor. ## Sifrovani pristupovych udaju `src/db/secretBox.ts`, AES-256-GCM. ```json { "v": 1, "iv": "...", "tag": "...", "data": "..." } ``` - **GCM**, ne CBC: sifruje a zaroven overuje, ze s daty nikdo nehybal. - **Nahodne IV** pro kazdou hodnotu, aby dve stejne hodnoty nedaly stejnou sifru. - **`v` je verze klice.** Vymena klice pak znamena precist starym a zapsat novym, ne zahodit vsechna napojeni. - **Nerozsifrovatelna hodnota nepada.** Jeden rozbity konektor nesmi shodit seznam vsech ostatnich, takze se chova jako nevyplneny a zaloguje se to. Sifruji se vsechna pole, i necitliva. Je to jednodussi nez rozhodovat u kazdeho zvlast a nic to nestoji. ## Schema `connectors` (migrace `001_connectors.sql`): | Sloupec | Poznamka | | ------------- | ------------------------------------------------- | | `tenant_id` | povinne, index zacina jim | | `service_id` | odkaz do katalogu v kodu, ne do tabulky | | `secrets` | JSONB se sifrovanymi hodnotami, nikdy plaintext | | `is_default` | jediny vychozi na firmu a sluzbu, hlida index | **Sluzby v databazi nejsou.** Jsou to definice, ktere delame my, a repo je u nich zdroj pravdy kvuli code review a historii v gitu. Rucne upraveny radek v produkci nikdo za tri mesice nedohleda. Duvody jsou v [12-sluzby-a-konektory.md](12-sluzby-a-konektory.md). Vychozi konektor hlida **castecny unikatni index**, ne jen kod: ```sql CREATE UNIQUE INDEX connectors_one_default_idx ON connectors (tenant_id, service_id) WHERE is_default; ``` Bez nej by dva soubezne zapisy udelaly dva vychozi a krok bez vybraneho konektoru by si vybiral podle nahody. ## Dvere k oddelene databazi `dbFor(tenantId)` dnes vraci vzdy tentyz pool. Je to zamerny sev: jednou prijde klient, ktery bude chtit vlastni databazi nebo bude delat tricet procent provozu, a presun ma byt konfigurace, ne prepisovani dotazu. Podminka je **nikdy nespojovat dotazem dva klienty**, coz uz vynucuje povinny argument `tenantIds` v ulozistich. Podrobnosti v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md). ## Health | Endpoint | Zavisi na DB | K cemu | | --------------- | ------------ | ----------------------------------------- | | `/health` | ne | liveness, AppFactory podle nej restartuje | | `/health/ready` | ano | readiness, vraci 503 pri nedostupne DB | `/health` **nesmi** na databazi zavisel. Kratky vypadek DB by jinak vedl k restartovani containeru, coz nic nespravi. Vysledek pingu se par sekund cachuje, aby monitoring nedelal dotaz pri kazdem pingu. ## Pool a jedno pravidlo `DATABASE_POOL_MAX` je vychozi 10 a je to zamerne malo. **Worker nesmi drzet spojeni po dobu volani ciziho API** - volani do iDokladu trva 300 ms a pri stovce soubeznych kroku by to bylo sto obsazenych spojeni. Se spravnym poradim (odeber ulohu, uvolni spojeni, volej, zapis) staci par. Az bude instanci vic, prijde PgBouncer v transakcnim rezimu. **Pozor: v nem nefunguje `LISTEN/NOTIFY`**, na kterem ma stat sbernice udalosti pro SSE. Ta pak potrebuje prime spojeni mimo PgBouncer. ## Co bylo overeno Proti Postgresu 16 v kontejneru: | Co | Vysledek | | ----------------------------------------------------- | -------- | | Migrace projedou a zapisou se do `schema_migrations` | ano | | Udaje jsou v tabulce sifrovane, plaintext nikde | ano | | Konektor prezije restart procesu | ano | | Se spravnym klicem se udaje rozsifruji | ano | | Se spatnym klicem se chovaji jako nevyplnene a loguje se | ano | | `PATCH` bez tajneho pole tajne pole nesmaze | ano | | Prepnuti vychoziho konektoru | ano | | Smazani vychoziho preda priznak zbylemu | ano | | Bez `DATABASE_URL` jede pametovy rezim a rekne to | ano | ## Co chybi | Chybi | Poznamka | | ---------------------------- | ----------------------------------------------------- | | Automatizace v databazi | dalsi na rade, je to to, co si clovek nastavi | | Rozlozeni dashboardu | male a samostatne, hned po automatizacich | | Tickety a incidenty | naposled, dnes je generuje simulace | | Uzivatele, firmy, resitele | v prototypu je to spis konfigurace nez data | | Sbernice udalosti pres LISTEN/NOTIFY | dnes `EventEmitter` v pameti jedne instance | | Vymena klice (rotace) | `v` je pripravene, prevod dat napsany neni | | Retence a partitionovani | az u tabulek behu, viz dokument 10 |