Collezioni e record
Le ventisei collezioni di record, l'involucro di snapshot che condividono e come percorrerne una.
Una collezione è un tipo di record: temperature-records, cooling,
deliveries e altri ventitré. Ognuna si legge allo stesso modo,
attraverso gli stessi quattro endpoint, e ogni record torna nello stesso
involucro: i campi attorno a data che dicono quando è stato acquisito e se
esiste ancora.
Ogni collezione è una concessione a sé. Una chiave legge
temperature-records perché qualcuno ha concesso temperature-records:read, e
con quella non arriva nient'altro.
Cosa può leggere
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
Cominci dal primo. Elenca solo le collezioni concesse alla sua chiave, ciascuna con lo scope che l'ha aperta, così non deve mai tirare a indovinare:
{
"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 vuoto non è un errore e non è un disservizio: è una chiave senza
concessioni su collezioni. La maggior parte delle chiavi emesse prima che questa
superficie esistesse si trova esattamente così, e ampliarne una è
una email.
L'involucro
Ogni record di ogni collezione porta gli stessi campi esterni. Cambia forma solo
data.
| Campo | Tipo | Significato |
|---|---|---|
id | string | L'identificatore del record, stabile e univoco all'interno della collezione. |
restaurantId | string | Il ristorante a cui appartiene. Ripete il percorso. |
collection | string | La collezione da cui proviene. Ripete il percorso. |
data | object | assente | Il record stesso. Un tombstone è l'unico caso in cui può mancare, quindi lo consideri presente in ogni altro caso. |
deleted | boolean | true significa un tombstone: il record è stato cancellato nell'app. |
capturedAt | string | Quando il dispositivo ha registrato la modifica, millisecondi dall'epoch come stringa. |
receivedAt | string | Quando questa API l'ha ricevuta. Successivo a capturedAt, a volte di ore. |
sequence | string | Una posizione monotonicamente crescente nel flusso delle modifiche. Ne confronti due; non ci faccia aritmetica sopra. |
mutationId | string | L'identificatore della modifica che ha prodotto questo stato. Chiave di idempotenza dalla nostra parte; chiave di deduplicazione dalla sua. |
sourceVersion | string | null | Il marcatore di versione del record di origine, quando ne ha uno. |
payloadVersion | number | Attualmente sempre 1. Aumenta se il significato di data dovesse mai cambiare. |
stateKind | string | Sempre received-shadow-snapshot. Legga la sezione successiva. |
capturedAt e receivedAt contano entrambi. Un tablet in una cella frigorifera
senza segnale registra alle 09:00 e carica alle 14:00; ordinare la sua pipeline
in base a receivedAt la mantiene corretta, e basare i report su capturedAt
la mantiene veritiera.
Che cos'è uno snapshot, e cosa non è
stateKind dice received-shadow-snapshot, e la formulazione è scelta con
cura.
- È lo stato attuale di un record, non un registro di ogni modifica. Legga un record due volte e ottiene com'è adesso, entrambe le volte.
- È ciò che abbiamo ricevuto, non ciò che il ristorante possiede. Un dispositivo che non ha mai caricato una modifica significa un record che questa API non ha mai visto.
- Non è un recupero dello storico. Una collezione comincia per un ristorante il giorno in cui l'acquisizione viene attivata per lui. I record creati prima sono nell'app e non qui. Le consegne sono l'unica eccezione: quelle due collezioni riportano lo storico che l'app conservava ancora quando sono state aperte.
Quindi un record assente è davvero ambiguo: mai registrato, oppure registrato e non ancora arrivato. Non ci costruisca sopra una cifra che si presenta come un audit, e veda la nota sulla completezza prima di comunicare un conteggio a chiunque.
I tombstone sono l'unico caso in cui l'assenza è priva di ambiguità.
deleted: true senza data significa che il record esisteva ed è stato
cancellato, ed è l'unico modo in cui viene a sapere che qualcosa è sparito.
Per quanto tempo restano i record
I record operativi restano qui 90 giorni, contati da quando sono avvenuti in
cucina (data.timestamp), non da quando li abbiamo ricevuti. Poi un job
notturno li sposta in un archivio a freddo bloccato, conservato cinque anni, e
spariscono da ogni lettura: elenchi, singolo record e relativi asset.
| Conservati 90 giorni | Conservati per tutta la vita dell'account |
|---|---|
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 record archiviato non è una tombstone, ed è l'unica eccezione alla regola delle tombstone qui sopra. Se ne va senza deleted: true,
perché nessuno l'ha cancellato: è stato spostato. Se tiene una copia, conservi
ciò che ha già e non tratti mai come cancellazione un record assente da un
percorso. Oltre i 90 giorni fa fede la sua copia; un controllo che ha bisogno
del nostro archivio passa dal supporto.
Le ventisei collezioni
Ognuna richiede <collection>:read come scope (cooling richiede
cooling:read, e così via). Sono documentate per dominio, campo per campo,
nelle pagine qui sotto.
| Pagina | Collezioni |
|---|---|
| Temperature | temperature-records, temperature-readings, cooking-temperature-records |
| Consegne | deliveries, delivery-pictures |
| Etichette di tracciabilità | traceability-labels |
| Pulizie | cleaning-tasks, cleaning-task-records, cleaning-task-pictures |
| Friggitrici | fryer-equipment, fryer-checks |
| Raffreddamento e processi alimentari | cooling, freezing, reheating, transport |
| Analisi di superficie | surface-analyses |
| Documenti | drive-files |
| Catalogo | suppliers, products, preparations |
| Strutture e persone | restaurants, users, areas, equipment, sensors, cooking-equipment |
Le temperature all'interno di una consegna sono sempre in Celsius: una lettura
rilevata in Fahrenheit viene convertita al suo arrivo, e ogni
temperatureRecords[].unit dice "C".
Cominci da Strutture e persone se sta leggendo una qualunque delle altre: le soglie delle apparecchiature e le aree sono ciò che trasforma una misurazione grezza in qualcosa che può giudicare.
Il documento OpenAPI riporta le stesse strutture, generate dal servizio in esecuzione. Quando una pagina qui e quel documento non concordano, ha ragione il documento e noi abbiamo una pagina da correggere.
Percorrere una collezione
GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=100
| Parametro | Obbligatorio | Regole |
|---|---|---|
limit | no | Da 1 a 100. Il valore predefinito è 50. |
cursor | no | Il nextCursor della pagina precedente, invariato. |
Non c'è alcun intervallo di tempo: si percorre una collezione, non una sua
finestra. Per leggere una settimana di consegne, percorra deliveries e tenga
i record il cui data.timestamp cade in quella settimana.
Un percorso è uno snapshot coerente. La prima pagina fissa la posizione nel
flusso, e ogni pagina successiva viene servita a partire da quella stessa
posizione. I record scritti mentre sta paginando non le spostano le pagine sotto
i piedi e non compaiono a metà percorso: li vedrà al percorso successivo.
L'ordinamento è per sequence, crescente.
Il cursore è legato al ristorante, alla collezione e al limit. Se cambia la dimensione della pagina a metà percorso viene rifiutato: scelga un limit e lo mantenga per tutto il percorso.
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);
}
Tenere aggiornata una copia
Oggi non esiste un parametro since. Un aggiornamento è un altro percorso, e lo
riconcilia con quello che già possiede:
- Usi
idcome chiave all'interno di una collezione, e sovrascriva quando ilsequenceche riceve è più alto di quello che ha memorizzato. - Rispetti i tombstone.
deleted: trueè l'istruzione di cancellazione; applicarla è l'unico modo perché la sua copia smetta di divergere. - Percorra a un'ora ragionevole. Una collezione intera a
limit=100è una manciata di richieste per un singolo ristorante, e il budget è di 300 al minuto, ma diverse centinaia di ristoranti sullo stesso minuto di cron sono un picco di cui risponde lei.
Se un cursore incrementale cambiasse quello che può realizzare, ce lo dica. È una modifica piccola su un flusso già ordinato: il motivo per cui non esiste è che finora non è servito a nessuno.
Asset
Quattro collezioni portano con sé un file: cleaning-task-pictures,
delivery-pictures, traceability-labels e drive-files. Il record lo indica in data.asset; i
byte arrivano dall'endpoint degli asset.
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"
}
]
}
Risponde con un elenco di URL firmati, ciascuno valido per quindici minuti,
e tutti gli URL di una stessa risposta condividono la stessa scadenza. Ciascuno
porta con sé la propria autorizzazione, così il download non richiede alcun
header Authorization, il che significa anche che il collegamento è di per sé
una credenziale al portatore: per quindici minuti chiunque lo possieda può
leggere quel file. Vale la pena sapere due cose.
Questi sono file, non solo immagini. Una prova di pulizia o una fotografia
di consegna è un'immagine, ma un'etichetta di tracciabilità o un file del drive può essere un PDF, un CSV,
un foglio di calcolo o un documento Word. Legga contentType anziché dare per
scontata un'immagine, e non rinomini tutto in .jpg mentre lo acquisisce.
sha256 è lì perché lei possa risparmiare lavoro. È il digest dei byte: se
corrisponde a qualcosa che ha già memorizzato, non serve scaricarlo di nuovo.
Compaiono solo gli asset il cui caricamento è terminato ed è stato verificato: uno ancora in transito è assente anziché rotto, e lo stesso vale una volta che il record a cui appartiene è stato cancellato.
Il resto dei consigli vale la pena ripeterlo perché l'errore è silenzioso: scarichi i byte adesso, conservi i byte anziché il collegamento, non metta mai l'URL dove possa sopravvivere al proprio scopo (una email, un ticket, una cache di lunga durata), e richiami l'endpoint quando le serve un URL nuovo. Costa poco, e ricontrolla la concessione.
Gli errori che incontrerà
403: la sua chiave raggiunge il ristorante ma non con lo scope di quella
collezione. L'endpoint delle collezioni è il modo economico per scoprire quali
scope ha davvero.
404: nessuna concessione per quel ristorante, oppure quel record non
esiste. I due casi sono indistinguibili di proposito.
400: una collezione al di fuori delle ventisei qui sopra, oppure un
cursore che non appartiene a questo ristorante, questa collezione e questo
limit.
Tutti e tre sono documenti di problema con un requestId.