Collections et enregistrements

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

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

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 :

{
  "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.

L'enveloppe

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

ChampTypeSignification
idstringL'identifiant de l'enregistrement, stable et unique au sein de la collection.
restaurantIdstringLe restaurant auquel il appartient. Reprend le chemin.
collectionstringLa collection d'où il vient. Reprend le chemin.
dataobject | absentL'enregistrement lui-même. Un tombstone est le seul cas où il peut manquer, considérez-le donc comme présent partout ailleurs.
deletedbooleantrue signifie un tombstone : l'enregistrement a été supprimé dans l'application.
capturedAtstringQuand l'appareil a consigné le changement, en millisecondes depuis l'epoch sous forme de chaîne de caractères.
receivedAtstringQuand cette API l'a reçu. Postérieur à capturedAt, parfois de plusieurs heures.
sequencestringUne position croissante de façon monotone dans le flux des changements. Comparez-en deux ; ne faites pas d'arithmétique sur une seule.
mutationIdstringL'identifiant du changement qui a produit cet état. Clé d'idempotence de notre côté ; clé de déduplication du vôtre.
sourceVersionstring | nullLe marqueur de version de l'enregistrement d'origine, quand il en a un.
payloadVersionnumberToujours 1 aujourd'hui. Il augmente si le sens de data vient à changer.
stateKindstringToujours 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 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é 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 joursConservé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-picturesrestaurants, 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.

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.

PageCollections
Températurestemperature-records, temperature-readings, cooking-temperature-records
Livraisonsdeliveries, delivery-pictures
Étiquettes de traçabilitétraceability-labels
Nettoyagecleaning-tasks, cleaning-task-records, cleaning-task-pictures
Friteusesfryer-equipment, fryer-checks
Refroidissement et procédés alimentairescooling, freezing, reheating, transport
Analyses de surfacesurface-analyses
Documentsdrive-files
Cataloguesuppliers, products, preparations
Équipements et personnesrestaurants, 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 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

GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=100
ParamètreObligatoireRègles
limitnonDe 1 à 100. 50 par défaut.
cursornonLe 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.

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

GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}/assets
{
  "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 avec un requestId.

Dernière mise à jour 2026-09-29.