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.
| 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 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 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.
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 | temperature-records, temperature-readings, cooking-temperature-records |
| Livraisons | deliveries, delivery-pictures |
| Étiquettes de traçabilité | traceability-labels |
| Nettoyage | cleaning-tasks, cleaning-task-records, cleaning-task-pictures |
| Friteuses | fryer-equipment, fryer-checks |
| Refroidissement et procédés alimentaires | cooling, freezing, reheating, transport |
| Analyses de surface | surface-analyses |
| Documents | drive-files |
| Catalogue | suppliers, products, preparations |
| Équipements et personnes | 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 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è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.
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
idau sein d'une collection, et écrasez quand lesequenceque vous recevez est supérieur à celui que vous aviez stocké. - Respectez les tombstones.
deleted: trueest 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.