# Collections et enregistrements

> Les vingt-six collections d'enregistrements, l'enveloppe de snapshot qu'elles partagent, et comment parcourir une collection.

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

Une **collection**, c'est un type d'enregistrement : `temperature-records`,
`cooling`, `deliveries`, et vingt-trois autres. Chacune se lit de la
même façon, à travers les mêmes quatre endpoints, et chaque enregistrement
revient dans la même enveloppe : les champs autour de `data` qui disent quand il
a été saisi et s'il existe encore.

Chaque collection est une autorisation distincte. Une clé lit
`temperature-records` parce que quelqu'un a accordé `temperature-records:read`,
et rien d'autre ne vient avec.

## Ce que vous pouvez lire

```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
```

Commencez par le premier. Il liste **uniquement les collections qui ont été
accordées à votre clé**, chacune avec le scope qui l'ouvre, vous n'avez donc
jamais à deviner :

```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
    }
  ]
}
```

Un `data` vide n'est pas une erreur et pas une panne : c'est une clé sans aucune
autorisation sur les collections. La plupart des clés émises avant que cette
surface n'existe sont exactement dans ce cas, et l'élargir tient en
[un e-mail](https://developers.backresto.com/fr/docs/access).

## L'enveloppe

Chaque enregistrement de chaque collection porte les mêmes champs extérieurs.
Seul `data` change de forme.

| Champ | Type | Signification |
| --- | --- | --- |
| `id` | string | L'identifiant de l'enregistrement, stable et unique au sein de la collection. |
| `restaurantId` | string | Le restaurant auquel il appartient. Reprend le chemin. |
| `collection` | string | La collection d'où il vient. Reprend le chemin. |
| `data` | object \| absent | L'enregistrement lui-même. Un tombstone est le seul cas où il peut manquer, considérez-le donc comme présent partout ailleurs. |
| `deleted` | boolean | `true` signifie un tombstone : l'enregistrement a été supprimé dans l'application. |
| `capturedAt` | string | Quand l'appareil a consigné le changement, en millisecondes depuis l'epoch sous forme de chaîne de caractères. |
| `receivedAt` | string | Quand cette API l'a reçu. Postérieur à `capturedAt`, parfois de plusieurs heures. |
| `sequence` | string | Une position croissante de façon monotone dans le flux des changements. Comparez-en deux ; ne faites pas d'arithmétique sur une seule. |
| `mutationId` | string | L'identifiant du changement qui a produit cet état. Clé d'idempotence de notre côté ; clé de déduplication du vôtre. |
| `sourceVersion` | string \| null | Le marqueur de version de l'enregistrement d'origine, quand il en a un. |
| `payloadVersion` | number | Toujours `1` aujourd'hui. Il augmente si le sens de `data` vient à changer. |
| `stateKind` | string | Toujours `received-shadow-snapshot`. Lisez la section suivante. |

`capturedAt` et `receivedAt` comptent tous les deux. Une tablette dans une
chambre froide sans réseau enregistre à 09:00 et téléverse à 14:00 ; ordonner
votre propre pipeline sur `receivedAt` le garde correct, et faire vos rapports
sur `capturedAt` les garde justes.

## Ce qu'est un snapshot, et ce qu'il n'est pas

`stateKind` dit `received-shadow-snapshot`, et la formulation est choisie avec
soin.

- **C'est l'état actuel d'un enregistrement**, pas un journal de chaque
  modification. Lisez un enregistrement deux fois et vous obtenez son état du
  moment, les deux fois.
- **C'est ce que nous avons reçu**, pas ce que le restaurant détient. Un
  appareil qui n'a jamais téléversé un changement, c'est un enregistrement que
  cette API n'a jamais vu.
- **Ce n'est pas une reprise de l'historique.** Une collection commence pour un
  restaurant le jour où la capture est activée pour lui. Les enregistrements
  créés avant sont dans l'application, pas ici. Les [livraisons](https://developers.backresto.com/fr/docs/deliveries)
  sont la seule exception : ces deux collections portent l'historique que
  l'application détenait encore à leur ouverture.

Un enregistrement absent est donc réellement ambigu : jamais saisi, ou saisi et
pas encore arrivé. N'en tirez pas un chiffre qui se lit comme un audit, et voyez
[la note sur l'exhaustivité](https://developers.backresto.com/fr/docs/support) avant de communiquer un décompte à
qui que ce soit.

Les tombstones sont le seul cas où l'absence est sans ambiguïté.
`deleted: true` sans `data` signifie que l'enregistrement a existé et a été
supprimé, et c'est la seule façon d'apprendre que quelque chose a disparu.

## Combien de temps un enregistrement reste ici

Les enregistrements opérationnels restent ici **90 jours**, comptés depuis le
moment où ils ont eu lieu en cuisine (`data.timestamp`), pas depuis leur
réception. Ensuite, une tâche nocturne les déplace dans une archive froide
verrouillée, conservée cinq ans, et ils disparaissent de toutes les lectures :
listes, enregistrement unique et fichiers attachés.

| Conservés 90 jours | Conservés pendant toute la vie du compte |
| --- | --- |
| `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` |

**Un enregistrement archivé n'est pas une tombstone**, et c'est la seule exception à la règle des tombstones ci-dessus. Il part sans
`deleted: true`, parce que personne ne l'a supprimé : il a été déplacé. Si vous
tenez une copie, gardez ce que vous avez déjà, et ne traitez jamais un
enregistrement absent d'un parcours comme une suppression. Au-delà de 90 jours,
votre copie fait foi ; un contrôle qui a besoin de notre archive passe par le
[support](https://developers.backresto.com/fr/docs/support).

## Les vingt-six collections

Chacune prend `<collection>:read` comme scope (`cooling` exige
`cooling:read`, et ainsi de suite). Elles sont documentées par domaine, champ
par champ, sur les pages ci-dessous.

| Page | Collections |
| --- | --- |
| [Températures](https://developers.backresto.com/fr/docs/temperatures) | `temperature-records`, `temperature-readings`, `cooking-temperature-records` |
| [Livraisons](https://developers.backresto.com/fr/docs/deliveries) | `deliveries`, `delivery-pictures` |
| [Étiquettes de traçabilité](https://developers.backresto.com/fr/docs/traceability-labels) | `traceability-labels` |
| [Nettoyage](https://developers.backresto.com/fr/docs/cleaning) | `cleaning-tasks`, `cleaning-task-records`, `cleaning-task-pictures` |
| [Friteuses](https://developers.backresto.com/fr/docs/fryers) | `fryer-equipment`, `fryer-checks` |
| [Refroidissement et procédés alimentaires](https://developers.backresto.com/fr/docs/food-processes) | `cooling`, `freezing`, `reheating`, `transport` |
| [Analyses de surface](https://developers.backresto.com/fr/docs/surface-analyses) | `surface-analyses` |
| [Documents](https://developers.backresto.com/fr/docs/drive-files) | `drive-files` |
| [Catalogue](https://developers.backresto.com/fr/docs/catalogue) | `suppliers`, `products`, `preparations` |
| [Équipements et personnes](https://developers.backresto.com/fr/docs/facilities) | `restaurants`, `users`, `areas`, `equipment`, `sensors`, `cooking-equipment` |

Les températures d'une livraison sont toujours en degrés Celsius : un relevé
pris en Fahrenheit est converti à son arrivée, et chaque
`temperatureRecords[].unit` vaut `"C"`.

Commencez par **Équipements et personnes** si vous lisez l'une des autres : ce
sont les seuils des équipements et les zones qui transforment une simple mesure
en quelque chose que vous pouvez juger.

Le [document OpenAPI](https://api.backresto.com/openapi.json) porte les mêmes
formes, généré à partir du service en fonctionnement. Quand une page d'ici et
ce document divergent, c'est le document qui a raison et nous avons une page à
corriger.

## Parcourir une collection

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

| Paramètre | Obligatoire | Règles |
| --- | --- | --- |
| `limit` | non | De 1 à 100. 50 par défaut. |
| `cursor` | non | Le `nextCursor` de la page précédente, tel quel. |

Il n'y a pas de plage de temps : vous parcourez une collection, pas une fenêtre
de celle-ci. Pour lire une semaine de livraisons, parcourez `deliveries` et
gardez les enregistrements dont le `data.timestamp` tombe dans cette semaine.

**Un parcours est un snapshot cohérent.** La première page fixe la position
dans le flux, et chaque page suivante est servie à cette même position. Les
enregistrements écrits pendant que vous paginez ne décalent pas les pages sous
vos pieds et n'apparaissent pas en cours de parcours : vous les verrez au
parcours suivant. Le tri se fait sur `sequence`, croissant.

Le curseur est lié au restaurant, à la collection **et à la limite**. Changez la
taille de page en cours de parcours et il est rejeté : choisissez une limite,
gardez-la pour tout le parcours.

```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);
}
```

### Maintenir une copie à jour

Il n'y a pas de paramètre `since` aujourd'hui. Une actualisation, c'est un
nouveau parcours, et vous réconciliez avec ce que vous détenez déjà :

- **Indexez sur `id`** au sein d'une collection, et écrasez quand le `sequence`
  que vous recevez est supérieur à celui que vous aviez stocké.
- **Respectez les tombstones.** `deleted: true` est l'instruction de
  suppression ; l'appliquer est la seule façon d'empêcher votre copie de
  diverger.
- **Parcourez à une heure raisonnable.** Une collection entière à `limit=100`,
  c'est une poignée de requêtes pour un seul restaurant, et le
  [budget](https://developers.backresto.com/fr/docs/rate-limits) est de 300 par minute, mais plusieurs centaines
  de restaurants sur la même minute de cron, c'est un pic qui vous appartient.

Si un curseur incrémental changeait ce que vous pouvez construire, dites-le.
C'est un petit changement sur un flux déjà ordonné. S'il n'existe pas, c'est
que personne n'en a encore eu besoin.

## Fichiers attachés

Quatre collections portent un fichier : `cleaning-task-pictures`,
`delivery-pictures`, `traceability-labels` et `drive-files`. L'enregistrement le nomme dans
`data.asset` ; les octets viennent de l'endpoint des fichiers.

```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"
    }
  ]
}
```

Il répond par une liste d'URL signées, valables **quinze minutes** chacune, et
toutes les URL d'une même réponse partagent la même expiration. Chacune porte sa
propre autorisation, si bien que le téléchargement ne demande aucun en-tête
`Authorization`, ce qui veut aussi dire que le lien est en lui-même un
identifiant au porteur : pendant quinze minutes, quiconque le détient peut lire
ce fichier. Deux choses méritent d'être connues.

**Ce sont des fichiers, pas seulement des photos.** Une preuve de nettoyage ou une
photographie de livraison est une image, mais une étiquette de traçabilité ou un document de l'espace
documentaire peut être un PDF, un CSV, un tableur ou un document Word. Lisez
`contentType` plutôt que de supposer une image, et ne renommez pas tout en
`.jpg` à l'entrée.

**`sha256` est là pour vous éviter du travail.** C'est l'empreinte des octets :
si elle correspond à quelque chose que vous avez déjà stocké, vous n'avez pas
besoin de le retélécharger.

Seuls apparaissent les fichiers dont le téléversement s'est achevé et a été
vérifié : un fichier encore en transit est absent plutôt que cassé, et c'est
également vrai une fois l'enregistrement parent supprimé.

Le reste des conseils mérite d'être répété parce que l'échec est silencieux :
récupérez les octets maintenant, stockez les octets plutôt que le lien, ne
mettez jamais l'URL là où elle survivra à son objet (un e-mail, un ticket, un
cache de longue durée), et rappelez l'endpoint quand vous avez besoin d'une URL
fraîche. C'est peu coûteux, et cela revérifie l'autorisation.

## Les échecs que vous rencontrerez

**`403`** : votre clé atteint le restaurant, mais pas avec le scope de cette
collection. L'endpoint des collections est le moyen peu coûteux de savoir
lesquelles elle détient bel et bien.

**`404`** : aucune autorisation pour ce restaurant, ou pas d'enregistrement de
ce nom. Les deux sont volontairement indiscernables.

**`400`** : une collection hors des vingt-six ci-dessus, ou un curseur qui
n'appartient pas à ce restaurant, cette collection et cette limite.

Les trois sont des [documents problème](https://developers.backresto.com/fr/docs/errors) avec un `requestId`.
