# Collections & Datensätze

> Die sechsundzwanzig Datensatz-Collections, der Snapshot-Umschlag, den sie teilen, und wie Sie eine durchlaufen.

Source: https://developers.backresto.com/de/docs/collections

Eine **Collection** ist ein Datensatztyp: `temperature-records`, `cooling`,
`deliveries` und dreiundzwanzig weitere. Jede wird auf dieselbe Weise
gelesen, über dieselben vier Endpunkte, und jeder Datensatz kommt im selben
Umschlag zurück: den Feldern um `data` herum, die sagen, wann er erfasst wurde
und ob es ihn noch gibt.

Jede Collection ist eine eigene Zuweisung. Ein Schlüssel liest
`temperature-records`, weil jemand `temperature-records:read` zugewiesen hat,
und nichts sonst kommt damit.

## Was Sie lesen können

```http
GET /v1/restaurants/{restaurantId}/collections
GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=…&cursor=…
GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}
GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}/assets
```

Beginnen Sie mit dem ersten. Er listet **nur die Collections, die Ihrem
Schlüssel zugewiesen wurden**, jede mit dem Scope, der sie geöffnet hat. Sie
müssen also nie raten:

```json
{
  "data": [
    {
      "collection": "temperature-records",
      "readScope": "temperature-records:read",
      "description": "Received shadow snapshots for temperature-records; not complete primary-store history.",
      "stateKind": "received-shadow-snapshot",
      "payloadVersion": 1
    }
  ]
}
```

Ein leeres `data` ist kein Fehler und keine Störung: Es ist ein Schlüssel ohne
Collection-Zuweisungen. Die meisten Schlüssel, die ausgestellt wurden, bevor es
diese Oberfläche gab, haben genau das, und einen zu erweitern ist
[eine E-Mail](https://developers.backresto.com/de/docs/access).

## Der Umschlag

Jeder Datensatz in jeder Collection trägt dieselben äußeren Felder. Nur `data`
ändert seine Struktur.

| Feld | Typ | Bedeutung |
| --- | --- | --- |
| `id` | string | Die Kennung des Datensatzes, stabil und eindeutig innerhalb der Collection. |
| `restaurantId` | string | Das Restaurant, zu dem er gehört. Spiegelt den Pfad wider. |
| `collection` | string | Aus welcher Collection er stammt. Spiegelt den Pfad wider. |
| `data` | object \| fehlt | Der Datensatz selbst. Ein Tombstone ist der einzige Fall, in dem es fehlen kann; überall sonst dürfen Sie es als vorhanden behandeln. |
| `deleted` | boolean | `true` bedeutet einen Tombstone: Der Datensatz wurde in der App gelöscht. |
| `capturedAt` | string | Wann das Gerät die Änderung erfasst hat, als String mit Millisekunden seit Epoch. |
| `receivedAt` | string | Wann diese API sie empfangen hat. Später als `capturedAt`, manchmal um Stunden. |
| `sequence` | string | Eine monoton steigende Position im Strom der Änderungen. Vergleichen Sie zwei davon; rechnen Sie nicht mit einer. |
| `mutationId` | string | Die Kennung der Änderung, die diesen Zustand erzeugt hat. Auf unserer Seite ein Idempotenzschlüssel, auf Ihrer ein Schlüssel zur Deduplizierung. |
| `sourceVersion` | string \| null | Der Versionsstempel des ursprünglichen Datensatzes, sofern er einen hat. |
| `payloadVersion` | number | Derzeit immer `1`. Er steigt, falls sich die Bedeutung von `data` je ändert. |
| `stateKind` | string | Immer `received-shadow-snapshot`. Lesen Sie den nächsten Abschnitt. |

`capturedAt` und `receivedAt` zählen beide. Ein Tablet in einem Kühlraum ohne
Empfang erfasst um 09:00 Uhr und lädt um 14:00 Uhr hoch; Ihre eigene Pipeline
nach `receivedAt` zu ordnen hält sie korrekt, und nach `capturedAt` zu berichten
hält sie wahr.

## Was ein Snapshot ist und was nicht

`stateKind` sagt `received-shadow-snapshot`, und die Formulierung ist mit
Bedacht gewählt.

- **Es ist der aktuelle Zustand eines Datensatzes**, kein Protokoll jeder
  Änderung. Lesen Sie einen Datensatz zweimal, und Sie bekommen beide Male, wie
  er jetzt aussieht.
- **Es ist, was wir empfangen haben**, nicht, was das Restaurant hält. Ein
  Gerät, das eine Änderung nie hochgeladen hat, heißt: ein Datensatz, den diese
  API nie gesehen hat.
- **Es ist kein Backfill.** Eine Collection beginnt für ein Restaurant an dem
  Tag, an dem die Erfassung dafür eingeschaltet wird. Datensätze, die davor
  entstanden sind, liegen in der App und nicht hier. [Lieferungen](https://developers.backresto.com/de/docs/deliveries)
  sind die einzige Ausnahme: Diese beiden Collections tragen die Historie, die
  die App noch hielt, als sie geöffnet wurden.

Ein fehlender Datensatz ist also wirklich mehrdeutig: nie erfasst, oder erfasst
und noch nicht angekommen. Bauen Sie darauf keine Zahl, die sich wie ein Audit
liest, und lesen Sie [die Anmerkung zur Vollständigkeit](https://developers.backresto.com/de/docs/support), bevor
Sie irgendjemandem eine Anzahl nennen.

Tombstones sind der eine Fall, in dem Abwesenheit eindeutig ist. `deleted: true`
ohne `data` heißt, dass der Datensatz existiert hat und gelöscht wurde, und es
ist der einzige Weg, auf dem Sie erfahren, dass etwas verschwunden ist.

## Wie lange Datensätze hier bleiben

Betriebliche Datensätze bleiben hier **90 Tage**, gezählt ab dem Zeitpunkt, an
dem sie in der Küche entstanden sind (`data.timestamp`), nicht ab ihrem Eingang
bei uns. Danach verschiebt ein nächtlicher Job sie in ein gesperrtes Kaltarchiv,
das fünf Jahre aufbewahrt wird, und sie erscheinen in keinem Lesezugriff mehr:
weder in Listen noch als einzelner Datensatz noch mit ihren Assets.

| 90 Tage aufbewahrt | Für die gesamte Lebensdauer des Kontos aufbewahrt |
| --- | --- |
| `temperature-readings`, `temperature-records`, `cooking-temperature-records`, `cleaning-task-records`, `cleaning-task-pictures`, `cooling`, `freezing`, `reheating`, `transport`, `fryer-checks`, `surface-analyses`, `traceability-labels`, `deliveries`, `delivery-pictures` | `restaurants`, `users`, `areas`, `equipment`, `sensors`, `cooking-equipment`, `suppliers`, `products`, `preparations`, `cleaning-tasks`, `fryer-equipment`, `drive-files` |

**Ein archivierter Datensatz ist kein Tombstone**, und er ist die einzige Ausnahme von der Tombstone-Regel oben. Er verschwindet ohne
`deleted: true`, weil ihn niemand gelöscht hat: Er wurde verschoben. Wenn Sie
eine Kopie führen, behalten Sie, was Sie bereits haben, und werten Sie einen in
einem Durchlauf fehlenden Datensatz nie als Löschung. Für alles, was älter als
90 Tage ist, ist Ihre Kopie die Quelle; eine Prüfung, die unser Archiv braucht,
läuft über den [Support](https://developers.backresto.com/de/docs/support).

## Die sechsundzwanzig Collections

Jede nimmt `<collection>:read` als Scope (`cooling` braucht `cooling:read`, und
so weiter). Sie sind nach Themengebiet dokumentiert, Feld für Feld, auf den
Seiten unten.

| Seite | Collections |
| --- | --- |
| [Temperaturen](https://developers.backresto.com/de/docs/temperatures) | `temperature-records`, `temperature-readings`, `cooking-temperature-records` |
| [Lieferungen](https://developers.backresto.com/de/docs/deliveries) | `deliveries`, `delivery-pictures` |
| [Rückverfolgbarkeitsetiketten](https://developers.backresto.com/de/docs/traceability-labels) | `traceability-labels` |
| [Reinigung](https://developers.backresto.com/de/docs/cleaning) | `cleaning-tasks`, `cleaning-task-records`, `cleaning-task-pictures` |
| [Fritteusen](https://developers.backresto.com/de/docs/fryers) | `fryer-equipment`, `fryer-checks` |
| [Kühlen & Lebensmittelprozesse](https://developers.backresto.com/de/docs/food-processes) | `cooling`, `freezing`, `reheating`, `transport` |
| [Oberflächenanalysen](https://developers.backresto.com/de/docs/surface-analyses) | `surface-analyses` |
| [Dokumente](https://developers.backresto.com/de/docs/drive-files) | `drive-files` |
| [Katalog](https://developers.backresto.com/de/docs/catalogue) | `suppliers`, `products`, `preparations` |
| [Standort & Personen](https://developers.backresto.com/de/docs/facilities) | `restaurants`, `users`, `areas`, `equipment`, `sensors`, `cooking-equipment` |

Temperaturen in einer Lieferung sind immer in Celsius: Eine in Fahrenheit
genommene Messung wird beim Eingang umgerechnet, und jedes
`temperatureRecords[].unit` sagt `"C"`.

Beginnen Sie mit **Standort & Personen**, wenn Sie eine der anderen Seiten
lesen: Die Schwellen der Geräte und die Bereiche sind das, was aus einer nackten
Messung etwas macht, das Sie beurteilen können.

Das [OpenAPI-Dokument](https://api.backresto.com/openapi.json) trägt dieselben
Strukturen, erzeugt aus dem laufenden Dienst. Wenn eine Seite hier und jenes
Dokument sich widersprechen, hat das Dokument recht und wir haben eine Seite zu
korrigieren.

## Eine Collection durchlaufen

```http
GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=100
```

| Parameter | Pflicht | Regeln |
| --- | --- | --- |
| `limit` | nein | 1 bis 100. Standard ist 50. |
| `cursor` | nein | Der `nextCursor` der vorherigen Seite, wortgleich. |

Hier gibt es keinen Zeitraum: Sie durchlaufen eine Collection, nicht ein
Fenster daraus. Um eine Woche an Lieferungen zu lesen, durchlaufen Sie
`deliveries` und behalten die Datensätze, deren `data.timestamp` in diese Woche
fällt.

**Ein Durchlauf ist ein konsistenter Snapshot.** Die erste Seite fixiert die
Position im Strom, und jede spätere Seite wird zu genau dieser Position
beantwortet. Datensätze, die geschrieben werden, während Sie blättern,
verschieben die Seiten unter Ihnen nicht und tauchen mitten im Durchlauf nicht
auf: Sie sehen sie beim nächsten Durchlauf. Sortiert wird aufsteigend nach
`sequence`.

Der Cursor ist an das Restaurant, die Collection **und das Limit** gebunden.
Ändern Sie die Seitengröße mitten im Durchlauf, wird sie abgelehnt: Wählen Sie
ein Limit und behalten Sie es für den ganzen Durchlauf.

```js
async function* records({ apiKey, restaurantId, collection }) {
  const base = `https://api.backresto.com/v1/restaurants/${restaurantId}/collections/${collection}/records`;
  let cursor = null;

  do {
    const url = new URL(base);
    url.searchParams.set('limit', '100');
    if (cursor !== null) {
      url.searchParams.set('cursor', cursor);
    }

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` }
    });
    if (!response.ok) {
      throw new Error(`BackResto ${response.status}`);
    }

    const page = await response.json();
    yield* page.data;
    cursor = page.nextCursor;
  } while (cursor !== null);
}
```

### Eine Kopie aktuell halten

Es gibt heute keinen `since`-Parameter. Eine Auffrischung ist ein weiterer
Durchlauf, und Sie gleichen ihn mit dem ab, was Sie schon halten:

- **Nutzen Sie `id` als Schlüssel** innerhalb einer Collection und überschreiben
  Sie, wenn die `sequence`, die Sie bekommen, höher ist als die gespeicherte.
- **Werten Sie Tombstones aus.** `deleted: true` ist die Anweisung zu löschen;
  sie anzuwenden ist der einzige Weg, auf dem Ihre Kopie aufhört
  auseinanderzulaufen.
- **Durchlaufen Sie zu einer vernünftigen Stunde.** Eine vollständige Collection
  mit `limit=100` ist für ein einzelnes Restaurant eine Handvoll Anfragen, und
  das [Budget](https://developers.backresto.com/de/docs/rate-limits) sind 300 pro Minute, aber mehrere hundert
  Restaurants in derselben Cron-Minute sind eine Spitze, die Ihnen gehört.

Wenn ein inkrementeller Cursor ändern würde, was Sie bauen können, sagen Sie es.
Es ist eine kleine Änderung an einem Strom, der ohnehin geordnet ist. Der
Grund, warum es ihn nicht gibt, ist, dass ihn bisher niemand gebraucht hat.

## Assets

Vier Collections tragen eine Datei: `cleaning-task-pictures`,
`delivery-pictures`, `traceability-labels` und `drive-files`. Der Datensatz benennt sie in
`data.asset`; die Bytes kommen vom Assets-Endpunkt.

```http
GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}/assets
```

```json
{
  "data": [
    {
      "id": "a17c93be4f02",
      "contentType": "application/pdf",
      "byteLength": 184320,
      "sha256": "9f2a…",
      "uploadedAt": "1789000000000",
      "url": "https://…?X-Amz-Signature=…",
      "urlExpiresAt": "1789000900000"
    }
  ]
}
```

Er antwortet mit einer Liste signierter URLs, jede **fünfzehn Minuten** gültig,
und jede URL einer Antwort hat denselben Ablaufzeitpunkt. Jede trägt ihre eigene
Autorisierung, der Download braucht also keinen `Authorization`-Header, was
zugleich heißt, dass der Link selbst ein Bearer-Credential ist: Fünfzehn Minuten
lang kann jeder, der ihn hat, diese Datei lesen. Zwei Dinge sollte man wissen.

**Das sind Dateien, nicht nur Bilder.** Ein Reinigungsbeleg oder ein Lieferfoto
ist ein Bild, aber
ein Rückverfolgbarkeitsetikett oder eine Drive-Datei kann ein PDF, eine CSV,
eine Tabelle oder ein Word-Dokument sein. Lesen Sie `contentType`, statt ein
Bild anzunehmen, und benennen Sie beim Import nicht alles in `.jpg` um.

**`sha256` ist da, damit Sie sich Arbeit sparen können.** Es ist der Digest der
Bytes: Passt er zu etwas, das Sie schon gespeichert haben, müssen Sie es nicht
erneut herunterladen.

Es erscheinen nur Assets, deren Upload abgeschlossen und geprüft ist: Eines,
das noch unterwegs ist, fehlt schlicht, statt defekt zu erscheinen, und dasselbe
gilt, sobald der übergeordnete Datensatz gelöscht ist.

Den Rest des Rats zu wiederholen lohnt sich, weil der Fehlschlag leise ist:
Holen Sie die Bytes jetzt, speichern Sie die Bytes statt des Links, legen Sie
die URL nie irgendwohin, wo sie ihren Zweck überlebt (eine E-Mail, ein Ticket,
ein langlebiger Cache), und rufen Sie den Endpunkt erneut auf, wenn Sie eine
frische URL brauchen. Das ist billig, und dabei wird die Zuweisung erneut
geprüft.

## Die Fehler, die Ihnen begegnen werden

**`403`**: Ihr Schlüssel erreicht das Restaurant, aber nicht mit dem Scope
dieser Collection. Der Collections-Endpunkt ist der billige Weg herauszufinden,
welche Scopes er hat.

**`404`**: überhaupt keine Zuweisung für dieses Restaurant, oder es gibt diesen
Datensatz nicht. Beides ist absichtlich nicht unterscheidbar.

**`400`**: eine Collection außerhalb der sechsundzwanzig oben, oder ein Cursor,
der nicht zu diesem Restaurant, dieser Collection und diesem Limit gehört.

Alle drei sind [Problemdokumente](https://developers.backresto.com/de/docs/errors) mit einer `requestId`.
