# Booster databáze

> Čtení a zápis záznamů databáze booster panelu z vlastního serveru pomocí API klíče.

Booster panel je malá webová aplikace vytvořená v AYETO Booster Studiu. Panel může mít
vlastní databázi a endpointy na této stránce umožňují vašemu serveru tuto databázi
číst a měnit pomocí API klíče: importovat do panelu data, synchronizovat je s jiným
systémem nebo zpracovávat, co zadali uživatelé panelu.

Všechny endpointy jsou pod `/api/v3/booster/database/private/`. Samotné panely se ke
své databázi dostávají samostatnou přístupovou cestou, kterou tato stránka nepopisuje.

## Základní pojmy

### Panely a jejich databáze

Každý panel má nejvýše jednu databázi. Existuje, když je nastavení panelu **Přístup
k databázi** (detaily panelu v aplikaci) **Veřejná databáze** nebo **Soukromá
databáze**; s volbou **Bez databáze** odpovídá každý endpoint na této stránce `403`.
Přes toto API lze používat veřejný i soukromý režim. Databáze se vytvoří při prvním
použití a smaže se spolu s panelem.

Každý požadavek uvádí panel v hlavičce `panel-id`. ID panelu je zobrazeno, s tlačítkem
pro zkopírování, v horní části detailů panelu v aplikaci.

### Kolekce a záznamy

Databáze obsahuje **kolekce** a kolekce obsahuje **záznamy**. Kolekce není třeba
vytvářet: kolekce existuje, jakmile má nějaký záznam. Názvy kolekcí smějí obsahovat
jen písmena, číslice a podtržítka (`orders`, `contact_requests`).

Záznam má právě dvě pole:

| Pole | Typ | Popis |
|---|---|---|
| `key` | UUID | Identifikátor záznamu, jedinečný napříč databázemi všech panelů (nejen v rámci jeho kolekce). |
| `value` | libovolný | Data, která jste uložili. Přijímá se jakákoli hodnota JSON; pokud chcete filtrovat nebo řadit podle jejích polí, použijte objekt. |

Záznamy nenesou žádná další metadata: odpovědi neobsahují název kolekce, časová
razítka ani autora. Pokud cokoli z toho potřebujete, uložte to do `value`.

Endpointy, které pracují s jedním záznamem (`get`, `update`, `delete` a jejich dávkové
varianty), ho adresují jen pomocí `key`; kolekce se vezme z uloženého záznamu.
Endpointy, které pracují s celou kolekcí (`create`, `find`, `count`, zámky), přijímají
název kolekce v těle.

```json
{
  "key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70",
  "value": {
    "customer": {"name": "Jana Nováková", "email": "jana@example.com"},
    "status": "open",
    "priority": 3,
    "created_at": "2026-10-01T09:15:00Z"
  }
}
```

### Kdo má přístup k datům panelu

Požadavky se provádějí jménem uživatele, kterému API klíč patří. Tento uživatel musí:

- panel vlastnit (být jeho tvůrcem nebo členem skupiny, která ho vlastní, například
  administrátorů organizace, která panel dostala od partnera), nebo
- mít panel sdílený, přímo nebo prostřednictvím skupiny, do které patří.

Vlastníci a uživatelé, se kterými je panel sdílený (pro čtení nebo pro zápis), mohou
databázi číst i do ní zapisovat, v obou režimech databáze: zápis do databáze se
počítá jako používání panelu, ne jeho úprava (změna samotného panelu dál vyžaduje
sdílení pro zápis). Členství v organizaci panelu samo o sobě přístup nedává, stejně
jako administrátorské účty AYETO. API klíč musí také nést odpovídající
[rozsah](authentication.md#rozsahy):

| Rozsah | Endpointy |
|---|---|
| `ayeto.booster.database` | `get`, `find`, `count` |
| `ayeto.booster.database.write` | `create`, `create-many`, `update`, `update-many`, `delete`, `delete-many`, `lock/acquire`, `lock/release` |

Rozsah pro zápis nezahrnuje rozsah pro čtení; klíč, který čte i zapisuje, potřebuje
oba.

Zápisy provedené přes toto API jsou zápisy do databáze panelu jako jakékoli jiné:
spouštějí workflow se spouštěčem booster záznamu na daném panelu a spouštějí moduly
panelu pro webhooky a e-mailová oznámení.

### Pravidla kolekcí

Panel může definovat pravidla kolekcí, která určují, co smějí dělat anonymní
návštěvníci panelu. Pravidla platí jen v režimu **veřejné databáze**; panel
v soukromém režimu je ignoruje a každý endpoint na této stránce funguje na všech jeho
kolekcích.

U panelu ve veřejném režimu je pro požadavky na této stránce důležitá jen jedna část
pravidel:

- Operace nastavená pro kolekci (nebo pro pravidlo `*`, které ji pokrývá) na **none**
  je zakázaná pro všechny, včetně tohoto API. Požadavek selže s `403`.
- Každá jiná úroveň (public, secret, private) požadavky API povoluje.
- Kolekce bez odpovídajícího pravidla je pro API plně přístupná.

Omezení zápisu z pravidla (validační schéma, maximální velikost hodnoty, maximální
počet záznamů) se na zápisy provedené přes toto API **nepoužijí**. Data validujte na
své straně, než je zapíšete.

### Limity

| Limit | Hodnota |
|---|---|
| Záznamy vrácené jedním `find` | při stránkování až 1000 (`limit_to` je nejvýše `1000`, `limit_from` menší než `1000`); bez poslaných limitů všechny odpovídající záznamy |
| Podmínky filtru v jednom dotazu | 1000, včetně podmínek uvnitř `AND` / `OR` |
| Vnoření `AND` / `OR` | 100 úrovní |
| Hodnoty v jedné podmínce `in` | 100 |
| Řetězcová hodnota filtru | 1000 znaků |
| Doba platnosti zámku | více než 0,1 s, výchozí 3600 s |

Dávkové endpointy nemají pevný maximální počet položek, ale každá položka se zapisuje
zvlášť (viz [Vytvoření více záznamů](#vytvoreni-vice-zaznamu)); udržujte dávky
v rozsahu několika set položek, aby požadavek skončil s rezervou v rámci vašeho
časového limitu HTTP.

## Dotazy na záznamy

`find` a `count` přijímají volitelný objekt `params`. Jeho pole se řídí obecnými
[konvencemi pro dotazy](conventions.md#dotazy-na-seznamy) s rozdíly uvedenými zde.

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `filters` | pole | ne | Podmínky, které musí platit všechny. Viz [Filtry](conventions.md#filtry) a níže. |
| `sort_key` | řetězec | ne | Pole, podle kterého se řadí: `key` nebo `value.<field>`. |
| `sort_order` | celé číslo | ne | `0` vzestupně (výchozí, když je nastaveno `sort_key`), `1` sestupně. |
| `limit_from` | celé číslo | ne | Index prvního vraceného záznamu, od `0`, menší než `1000`. |
| `limit_to` | celé číslo | ne | Index za posledním vraceným záznamem (ne počet), nejvýše `1000`. Když se pošle jen `limit_from`, je to `limit_from + 100` (nejvýše `1000`). |

### Pole, podle kterých lze filtrovat a řadit

Přijímají se jen dva druhy názvů polí:

- `key`, klíč záznamu (porovnávejte ho s řetězcem UUID).
- `value.<path>`, pole uvnitř hodnoty záznamu, s tečkami pro vnořené objekty:
  `value.status`, `value.customer.email`.

Jakýkoli jiný název selže s `422`. Záznamy, jejichž hodnota pole nemá (nebo není
objektem), prostě neodpovídají.

Podmínka je `[field, operator, value]` s operátory `==`, `!=`, `<`, `>`, `<=`, `>=`,
`regex` (nikdy nerozlišuje velikost písmen) a `in` (hodnota je seznam, z něhož může
odpovídat kterákoli položka). Podmínky lze kombinovat pomocí `{"AND": [...]}`
a `{"OR": [...]}`; každá z těchto skupin potřebuje aspoň dvě podmínky a lze je
vnořovat.

```json
{
  "collection": "tickets",
  "params": {
    "filters": [
      ["value.status", "in", ["open", "waiting"]],
      {"OR": [
        ["value.priority", ">=", 3],
        ["value.customer.email", "regex", "@example\\.com$"]
      ]}
    ],
    "sort_key": "value.created_at",
    "sort_order": 1,
    "limit_from": 0,
    "limit_to": 50
  }
}
```

Na co myslet při filtrování podle hodnot:

- **Typy se musí shodovat.** `["value.priority", ">=", 3]` neodpovídá uloženému
  `"priority": "3"`. Čísla ukládejte jako čísla a data jako řetězce ISO 8601 nebo jako
  čísla, aby je `<` a `>` porovnávaly správně.
- **Řetězce, které vypadají jako UUID, zůstávají řetězci** v podmínkách `value.*`, takže
  UUID, které jste uložili jako text, se najde jako text.
- **Znaky `<`, `>`, `"` a `'` v řetězcové hodnotě filtru** se před porovnáním
  escapují, takže podmínka na text, který je obsahuje, neodpovídá. Řetězcová hodnota
  filtru také nesmí začínat na `$`.

### Řazení a stránkování

Bez `sort_key` přicházejí záznamy od nejnovějších. S `sort_key` se řadí podle tohoto
pole.

`limit_from` a `limit_to` jsou pozice ve výsledku, takže druhá stránka po 50 je
`"limit_from": 50, "limit_to": 100`. Protože `limit_to` nemůže přesáhnout `1000`,
stránkování pomocí limitů dosáhne jen na prvních 1000 odpovídajících záznamů. Chcete-li
pokračovat dál, stránkujte podle hodnoty: řaďte podle pole a filtrujte podle poslední
hodnoty, kterou jste dostali, například `["value.created_at", "<", "2026-09-30T12:00:00Z"]`
s `"sort_key": "value.created_at", "sort_order": 1`.

Pokud nepošlete ani `limit_from`, ani `limit_to`, `find` vrátí **všechny** odpovídající
záznamy. Používejte to jen u kolekcí, o kterých víte, že jsou malé.

`count` použije filtry a ignoruje řazení a stránkování: vrací celkový počet
odpovídajících záznamů.

## Společné hlavičky požadavků a chyby

Každý endpoint na této stránce potřebuje tyto hlavičky:

| Hlavička | Popis |
|---|---|
| `uni-api-key` | Váš API klíč. Viz [Autentizace](authentication.md). |
| `panel-id` | ID panelu, k jehož databázi přistupujete. |
| `Content-Type` | `application/json` |

Chyby jsou JSON ve tvaru `{"detail": "..."}` (viz [Chyby](conventions.md#chyby)).
Tyto mohou přijít z každého endpointu:

| Stav | `detail` | Příčina |
|---|---|---|
| `401` | `API key not provided` / `API key is invalid` | Klíč je prázdný, neznámý, vypnutý nebo mu chybí rozsah endpointu. |
| `403` | `permission denied` | Panel nevlastníte a ani s vámi není sdílený. |
| `403` | `database access is not allowed for this panel` | Přístup k databázi panelu je nastaven na **Bez databáze**. |
| `403` | `operation '<operation>' is disabled for collection '<collection>'` | Pravidlo kolekce panelu ve veřejném režimu nastavuje tuto operaci na **none**. Operace je `create`, `read`, `update`, `delete`, `count` nebo `lock`. |
| `404` | `panel not found` | Panel s tímto ID neexistuje, nebo je panel vypnutý. |
| `422` | chyba validace | Hlavička `panel-id` chybí nebo není UUID, nebo tělo neodpovídá modelu požadavku. |
| `422` | `Collection name can only contain alphanumeric characters and underscores` | Neplatný název kolekce (`Collection cannot be empty` u prázdného). |
| `429` | zpráva limitu požadavků | Příliš mnoho požadavků; počkejte dobu uvedenou v hlavičce `Retry-After`. Viz [Limity požadavků](conventions.md#limity-pozadavku). |
| `500` | chyba serveru | Neočekávané selhání; zkuste to později. |

## Vyhledání záznamů

Vrací záznamy kolekce, které odpovídají dotazu.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/find` |
| Rozsah | `ayeto.booster.database` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `collection` | řetězec | ano | Název kolekce. |
| `params` | objekt | ne | Filtry, řazení a stránkování; viz [Dotazy na záznamy](#dotazy-na-zaznamy). Bez něj se vrátí všechny záznamy kolekce, od nejnovějších. |

### Odpověď

Pole [záznamů](#kolekce-a-zaznamy) (`key`, `value`). Neznámá nebo prázdná kolekce
vrací `[]`.

### Chyby

| Stav | `detail` | Příčina |
|---|---|---|
| `422` | `Field '<field>' is not allowed for filtering or sorting` | Filtr nebo `sort_key` uvádí něco jiného než `key` nebo `value.<path>`. |
| `422` | `Each filter condition must be a list of [field, operator, value]` | Chybně utvořená podmínka. |
| `422` | `Operator '<operator>' is not allowed` | Neznámý operátor. |
| `422` | `'<AND/OR>' operator requires a list of at least 2 conditions` | Hodnota `AND` / `OR` není seznam aspoň dvou podmínek. |
| `422` | `Logical filter must have exactly one key ('AND' or 'OR')` | Chybně utvořená logická podmínka, např. `{}` nebo objekt s několika klíči. |
| `422` | `limit_from must be lower than 1000` | `limit_from` 1000 nebo více (stránkování dosáhne jen na prvních 1000 záznamů). |
| `422` | `Maximum number of filter conditions is 1000 (including conditions inside AND/OR)` | Příliš mnoho podmínek. |
| `422` | `sort_order must be 0 (ASC) or 1 (DESC)` | Neplatné `sort_order`. |
| `422` | `limit_to cannot exceed 1000` | `limit_to` nad 1000. |
| `422` | `limit_from cannot be greater than limit_to` | Obrácený rozsah. |

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/find" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{
    "collection": "tickets",
    "params": {
      "filters": [["value.status", "==", "open"]],
      "sort_key": "value.priority",
      "sort_order": 1,
      "limit_from": 0,
      "limit_to": 20
    }
  }'
```

```json
[
  {
    "key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70",
    "value": {
      "customer": {"name": "Jana Nováková", "email": "jana@example.com"},
      "status": "open",
      "priority": 3,
      "created_at": "2026-10-01T09:15:00Z"
    }
  },
  {
    "key": "c9f0f895-fb98-4b91-8f2a-6d3e5c1b0a47",
    "value": {
      "customer": {"name": "Petr Svoboda", "email": "petr@example.org"},
      "status": "open",
      "priority": 1,
      "created_at": "2026-10-01T11:40:00Z"
    }
  }
]
```

## Počet záznamů

Vrací počet záznamů kolekce, které odpovídají filtrům.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/count` |
| Rozsah | `ayeto.booster.database` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `collection` | řetězec | ano | Název kolekce. |
| `params` | objekt | ne | Stejný objekt jako u [Vyhledání záznamů](#vyhledani-zaznamu). Výsledek ovlivňují jen `filters`; řazení a stránkování se validují, ale ignorují. |

### Odpověď

Celé číslo.

### Chyby

Chyby dotazu z [Vyhledání záznamů](#vyhledani-zaznamu).

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/count" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{"collection": "tickets", "params": {"filters": [["value.status", "==", "open"]]}}'
```

```json
42
```

## Čtení záznamu

Vrací jeden záznam podle jeho klíče.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/get` |
| Rozsah | `ayeto.booster.database` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `key` | UUID | ano | Klíč záznamu. |

### Odpověď

[Záznam](#kolekce-a-zaznamy).

### Chyby

| Stav | `detail` | Příčina |
|---|---|---|
| `404` | `Database record not found` | Databáze tohoto panelu nemá záznam s tímto klíčem. |

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/get" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{"key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70"}'
```

```json
{
  "key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70",
  "value": {
    "customer": {"name": "Jana Nováková", "email": "jana@example.com"},
    "status": "open",
    "priority": 3,
    "created_at": "2026-10-01T09:15:00Z"
  }
}
```

## Vytvoření záznamu

Přidá záznam do kolekce.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/create` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `collection` | řetězec | ano | Název kolekce (písmena, číslice, podtržítka). Kolekce vznikne s prvním záznamem. |
| `value` | libovolný | ne | Data k uložení. Vynechání znamená `null`. |
| `key` | UUID | ne | Klíč nového záznamu. Když se vynechá, vygeneruje se; nesmí ho používat žádný existující záznam žádného panelu (jinak `422`). |

### Odpověď

Vytvořený [záznam](#kolekce-a-zaznamy), včetně jeho `key`.

### Chyby

| Stav | `detail` | Příčina |
|---|---|---|
| `422` | `a record with key '<key>' already exists` | Klíč, který jste poslali, používá existující záznam (tohoto nebo jiného panelu). |
| `500` | `error creating database record` | Záznam se nepodařilo uložit (výjimečně, když dva požadavky vytvořily stejný klíč ve stejném okamžiku). |

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/create" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{
    "collection": "tickets",
    "value": {
      "customer": {"name": "Jana Nováková", "email": "jana@example.com"},
      "status": "open",
      "priority": 3,
      "created_at": "2026-10-01T09:15:00Z"
    }
  }'
```

```json
{
  "key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70",
  "value": {
    "customer": {"name": "Jana Nováková", "email": "jana@example.com"},
    "status": "open",
    "priority": 3,
    "created_at": "2026-10-01T09:15:00Z"
  }
}
```

## Vytvoření více záznamů

Přidá několik záznamů, případně do různých kolekcí.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/create-many` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

Tělo je **pole** JSON objektů stejného tvaru jako u
[Vytvoření záznamu](#vytvoreni-zaznamu) (`collection`, `value`, volitelný `key`).

Položky se zapisují jedna po druhé, v pořadí. Dávka není atomická: pokud položka
selže, požadavek vrátí chybu, záznamy před ní zůstanou vytvořené a položky za ní se
nezapíšou. Položka, jejíž `key` už existuje, zastaví dávku s `422`. Před opakováním
neúspěšné dávky zkontrolujte, co se zapsalo, nebo posílejte vlastní klíče, abyste
poznali, které položky existují.

### Odpověď

Pole vytvořených záznamů v pořadí požadavku.

### Chyby

Chyby [Vytvoření záznamu](#vytvoreni-zaznamu) pro první položku, která selhala.

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/create-many" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '[
    {"collection": "products", "key": "0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30", "value": {"sku": "MUG-01", "name": "Mug", "quantity": 12}},
    {"collection": "products", "key": "5a9d3f2e-8c1b-4a6e-b7d0-3e2f1c9a8b54", "value": {"sku": "TEE-M", "name": "T-shirt M", "quantity": 40}}
  ]'
```

```json
[
  {"key": "0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30", "value": {"sku": "MUG-01", "name": "Mug", "quantity": 12}},
  {"key": "5a9d3f2e-8c1b-4a6e-b7d0-3e2f1c9a8b54", "value": {"sku": "TEE-M", "name": "T-shirt M", "quantity": 40}}
]
```

## Úprava záznamu

Nahradí hodnotu záznamu.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/update` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `key` | UUID | ano | Klíč záznamu. |
| `value` | libovolný | ne | Nová hodnota. Uloženou hodnotu nahradí úplně; pole, která vynecháte, se odstraní. Vynechání znamená `null`. |

Chcete-li změnit jediné pole, přečtěte záznam, pole změňte a pošlete zpět celou
hodnotu. Pokud mohou stejný záznam ve stejnou chvíli zapisovat i jiní klienti, dělejte
to pod [zámkem](#zamky).

### Odpověď

Upravený [záznam](#kolekce-a-zaznamy).

### Chyby

| Stav | `detail` | Příčina |
|---|---|---|
| `404` | `Database record not found` | Databáze tohoto panelu nemá záznam s tímto klíčem. |

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/update" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70",
    "value": {
      "customer": {"name": "Jana Nováková", "email": "jana@example.com"},
      "status": "closed",
      "priority": 3,
      "created_at": "2026-10-01T09:15:00Z"
    }
  }'
```

```json
{
  "key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70",
  "value": {
    "customer": {"name": "Jana Nováková", "email": "jana@example.com"},
    "status": "closed",
    "priority": 3,
    "created_at": "2026-10-01T09:15:00Z"
  }
}
```

## Úprava více záznamů

Nahradí hodnoty několika záznamů.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/update-many` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

Tělo je **pole** JSON objektů stejného tvaru jako u
[Úpravy záznamu](#uprava-zaznamu) (`key`, `value`). Položky se zapisují jedna po druhé,
v pořadí, a dávka není atomická: položka, která selže, požadavek zastaví a položky
před ní zůstanou upravené.

### Odpověď

Pole upravených záznamů v pořadí požadavku.

### Chyby

Chyby [Úpravy záznamu](#uprava-zaznamu) pro první položku, která selhala.

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/update-many" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '[
    {"key": "0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30", "value": {"sku": "MUG-01", "name": "Mug", "quantity": 10}},
    {"key": "5a9d3f2e-8c1b-4a6e-b7d0-3e2f1c9a8b54", "value": {"sku": "TEE-M", "name": "T-shirt M", "quantity": 37}}
  ]'
```

```json
[
  {"key": "0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30", "value": {"sku": "MUG-01", "name": "Mug", "quantity": 10}},
  {"key": "5a9d3f2e-8c1b-4a6e-b7d0-3e2f1c9a8b54", "value": {"sku": "TEE-M", "name": "T-shirt M", "quantity": 37}}
]
```

## Smazání záznamu

Smaže jeden záznam.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/delete` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `key` | UUID | ano | Klíč záznamu. |

### Odpověď

`null`.

### Chyby

| Stav | `detail` | Příčina |
|---|---|---|
| `404` | `Database record not found` | Databáze tohoto panelu nemá záznam s tímto klíčem (i když už byl smazán). |

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/delete" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{"key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70"}'
```

## Smazání více záznamů

Smaže několik záznamů.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/delete-many` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

Tělo je **pole** JSON objektů s polem `key`, jako u
[Smazání záznamu](#smazani-zaznamu). Záznamy se mažou jeden po druhém, v pořadí;
položka, která selže (například klíč, který neexistuje), požadavek zastaví a záznamy
před ní zůstanou smazané.

### Odpověď

`null`.

### Chyby

Chyby [Smazání záznamu](#smazani-zaznamu) pro první položku, která selhala.

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/delete-many" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '[
    {"key": "0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30"},
    {"key": "5a9d3f2e-8c1b-4a6e-b7d0-3e2f1c9a8b54"}
  ]'
```

## Zámky

Zámky umožňují několika klientům (vašim serverům, samotnému panelu) střídat se nad
stejnými daty. Zámek má dobu platnosti (TTL) a je jednoho ze dvou druhů:

- **Zámek záznamu**: `collection` a `key`. Použijte skutečnou kolekci záznamu; zámek
  vzatý pod jiným názvem kolekce záznam nechrání.
- **Zámek kolekce**: `collection` bez `key`.

Zámky jsou **doporučující** (advisory): server je nekontroluje. `get`, `find`, `count`,
`create`, `update`, `delete` a jejich dávkové varianty projdou bez ohledu na to, zda je
záznam nebo kolekce zamčená. Zámek jen přiměje ostatní volání `lock/acquire` na stejný
zámek čekat, takže data chrání jen tehdy, když si ho každý klient, který je zapisuje,
nejdřív vezme; koordinace je na panelu a vašem kódu.

- **Zámky záznamů a kolekcí jsou nezávislé.** Držení zámku kolekce nikomu nebrání vzít
  si zámek záznamu v ní, a naopak.
- **Zámek může uvolnit kdokoli.** `release` uvolní zámek bez ohledu na to, kdo ho vzal,
  a druhé `acquire` zámku, který už držíte, čeká stejně jako u kohokoli jiného. Zámek
  uvolňujte jen z kódu, který ho získal.
- **Zámky vypršejí.** Každý zámek se automaticky uvolní po uplynutí svého `ttl`, i když
  nebyl nikdy uvolněn. Zvolte TTL delší, než trvá vaše práce, ale dost krátké na to,
  aby spadlý klient neblokoval ostatní dlouho.

Zamykání je operace `lock` v [pravidlech kolekcí](#pravidla-kolekci); oba endpointy
potřebují rozsah pro zápis a stejný přístup k panelu jako zápis.

## Získání zámku

Vezme zámek záznamu nebo kolekce a počká, dokud nebude volný.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/lock/acquire` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `collection` | řetězec | ano | Název kolekce. |
| `key` | UUID | ne | Klíč záznamu. Vynechte ho, chcete-li vzít zámek kolekce. |
| `acquire_timeout` | číslo | ne | Kolik sekund na zámek čekat. `0` selže okamžitě, když je zámek obsazený. Když se vynechá, požadavek čeká tak dlouho, jak je potřeba, nejvýše po zbývající TTL současného držitele; vždy pošlete hodnotu kratší, než je váš časový limit HTTP. |
| `ttl` | číslo | ne | Počet sekund, po kterých se zámek automaticky uvolní. Musí být větší než `0.1`. Výchozí `3600`. |

### Odpověď

`null`, jakmile je zámek váš.

### Chyby

| Stav | `detail` | Příčina |
|---|---|---|
| `423` | `Failed to acquire lock for collection '<collection>' and key '<key>'` | Zámek zůstal obsazený po dobu `acquire_timeout` sekund (`<key>` je u zámku kolekce `None`). Zkuste to později. |
| `422` | chyba validace | `ttl` je `0.1` nebo méně. |

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/lock/acquire" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{"collection": "products", "key": "0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30", "acquire_timeout": 10, "ttl": 30}'
```

## Uvolnění zámku

Uvolní zámek záznamu nebo kolekce bez ohledu na to, kdo ho drží.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/database/private/lock/release` |
| Rozsah | `ayeto.booster.database.write` |
| Limit požadavků | [default](conventions.md#limity-pozadavku) |

### Požadavek

| Pole | Typ | Povinné | Popis |
|---|---|---|---|
| `collection` | řetězec | ano | Název kolekce, jak byl poslán do `lock/acquire`. |
| `key` | UUID | ne | Klíč záznamu, jak byl poslán do `lock/acquire`. U zámku kolekce ho vynechte. |

### Odpověď

`null`. Uvolnění zámku, který není obsazený (nebo už vypršel), také uspěje.

### Příklady

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/database/private/lock/release" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{"collection": "products", "key": "0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30"}'
```

### Čtení, úprava a zápis pod zámkem

`update` nahrazuje celou hodnotu, takže dva klienti, kteří každý přečtou záznam,
změní ho a zapíšou zpět, se mohou navzájem přepsat. Vezměte zámek záznamu kolem celé
této sekvence a uvolněte ho v bloku `finally`; totéž musí dělat každý klient, který
záznam zapisuje:

```python
import os
import time

import requests

BASE_URL = "https://ayeto.ai/api/v3/booster/database/private"
HEADERS = {
    "uni-api-key": os.environ["AYETO_API_KEY"],
    "panel-id": "3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90",
}


def call(path: str, body):
    response = requests.post(f"{BASE_URL}/{path}", json=body, headers=HEADERS, timeout=30)
    response.raise_for_status()
    return response.json()


def change_stock(key: str, delta: int, attempts: int = 3) -> dict:
    """Add `delta` to the quantity of a product record without losing concurrent changes."""
    lock = {"collection": "products", "key": key}
    for attempt in range(attempts):
        try:
            # wait at most 10 s; the lock frees itself after 30 s if this process dies
            call("lock/acquire", {**lock, "acquire_timeout": 10, "ttl": 30})
            break
        except requests.HTTPError as e:
            if e.response.status_code != 423 or attempt == attempts - 1:
                raise
            time.sleep(2)

    try:
        record = call("get", {"key": key})
        value = record["value"]
        value["quantity"] = value.get("quantity", 0) + delta
        return call("update", {"key": key, "value": value})
    finally:
        call("lock/release", lock)


print(change_stock("0b6e8c1a-5d2f-4e3b-9a7c-1f4d6e8a2b30", -2))
```
