Collezioni e record

I ventiquattro tipi di record oltre alle consegne — l'involucro di istantanea che condividono, che cos'è un'istantanea e come percorrerne una.

Le consegne sono una risorsa curata, modellata a mano perché l'accettazione merci è stata la prima cosa che qualcuno ha chiesto. Le collezioni sono tutto il resto del registro: temperature, cicli di raffreddamento, pulizie, controlli delle friggitrici, etichette, ventiquattro in tutto, servite attraverso una sola forma generica.

Dove l'endpoint delle consegne le dà un oggetto progettato, una collezione le dà il record così come lo tiene l'app, avvolto in un involucro che le dice quando è stato acquisito e se esiste ancora. Il compromesso è deliberato: è ciò che permette a un nuovo modulo di raggiungere questa API la settimana in cui viene rilasciato anziché il trimestre successivo.

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 l'ambito 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.

CampoTipoSignificato
idstringaL'identificatore del record, stabile e univoco all'interno della collezione.
restaurantIdstringaIl ristorante a cui appartiene. Ripete il percorso.
collectionstringaLa collezione da cui proviene. Ripete il percorso.
dataoggetto | assenteIl record stesso. Un tombstone è l'unico caso in cui può mancare, quindi lo consideri presente in ogni altro caso.
deletedbooleanotrue significa un tombstone: il record è stato cancellato nell'app.
capturedAtstringaQuando il dispositivo ha registrato la modifica, millisecondi dall'epoch come stringa.
receivedAtstringaQuando questa API l'ha ricevuta. Successivo a capturedAt, a volte di ore.
sequencestringaUna posizione monotonamente crescente nel flusso delle modifiche. Ne confronti due; non ci faccia aritmetica sopra.
mutationIdstringaL'identificatore della modifica che ha prodotto questo stato. Chiave di idempotenza dalla nostra parte; chiave di deduplicazione dalla sua.
sourceVersionstringa | nullIl marcatore di versione del record di origine, quando ne ha uno.
payloadVersionnumeroAttualmente sempre 1. Aumenta se il significato di data dovesse mai cambiare.
stateKindstringaSempre 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 su receivedAt la mantiene corretta, e fare i report su capturedAt la mantiene veritiera.

Che cos'è un'istantanea, 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.

Quindi un record assente è davvero ambiguo: mai registrato, oppure registrato e non ancora arrivato. Non ci costruisca sopra un dato che si legge 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.

Le ventiquattro collezioni

Ognuna di queste richiede <collection>:read come ambito — cooling richiede cooling:read, e così via lungo l'elenco.

CollezioneChe cos'è un record
restaurantsLa sede stessa: nome, indirizzo, giorni di chiusura, abbonamento e impostazioni.
usersGli account del personale che registra i controlli, e quali moduli usano.
areasLe zone in cui un ristorante è suddiviso — cucina, cella frigorifera, bar.
equipmentFrigoriferi, congelatori e unità della catena del freddo, con le loro soglie min/max.
sensorsLe sonde wireless, per indirizzo MAC, e l'apparecchiatura che ciascuna sorveglia.
temperature-recordsUna temperatura rilevata da una persona su un'apparecchiatura, con il turno ed eventuali azioni correttive.
temperature-readingsUna temperatura riportata da un sensore per conto proprio, senza presidio.
suppliersChi consegna, con i recapiti e il numero di conto.
productsI prodotti ricevuti e utilizzati.
preparationsLe preparazioni interne, con durata di conservazione e allergeni.
cleaning-tasksIl piano di pulizia: ogni compito, la sua area, la sua ricorrenza, se richiede una fotografia.
cleaning-task-recordsUn compito di pulizia effettivamente svolto — quando, e da chi.
cleaning-task-picturesLa fotografia che lo dimostra. Porta con sé un file; veda la sezione sugli asset qui sotto.
coolingUn ciclo di raffreddamento: prodotto, temperatura iniziale e finale, ora di inizio e di fine.
freezingUn'operazione di congelamento, stessa forma del raffreddamento.
reheatingUn'operazione di riscaldamento, stessa forma del raffreddamento.
transportUn prodotto trasportato, con luogo, ora e temperatura di partenza e di arrivo.
fryer-equipmentLe friggitrici.
fryer-checksUn controllo della qualità dell'olio e cosa è stato deciso — filtrato, cambiato, lasciato.
cooking-equipmentForni e unità di cottura, con le loro soglie.
cooking-temperature-recordsUna temperatura di cottura, rilevata da una persona, su un'unità di cottura.
surface-analysesUn tampone di superficie: cosa è stato analizzato, se ha superato la prova, il piano d'azione in caso contrario.
traceability-labelsUn'etichetta di tracciabilità stampata. Porta con sé un file; veda la sezione sugli asset qui sotto.
drive-filesUn documento archiviato nel drive del ristorante. Porta con sé un file; veda la sezione sugli asset qui sotto.

Le strutture campo per campo di ogni oggetto data sono nel documento OpenAPI, che è generato dal servizio in esecuzione — è l'unica descrizione che non può divergere da ciò che è distribuito.

Percorrere una collezione

GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=100
ParametroObbligatorioRegole
limitnoDa 1 a 100. Il valore predefinito è 50.
cursornoIl nextCursor della pagina precedente, invariato.

Qui non c'è alcun intervallo di tempo, a differenza delle consegne: si percorre una collezione, non una sua finestra.

Un percorso è un'istantanea 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 id come chiave all'interno di una collezione, e sovrascriva quando il sequence che 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 cambierebbe 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

Tre collezioni portano con sé un file: cleaning-task-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 più o meno come le fotografie delle consegne: un elenco di URL firmati, ciascuno valido per quindici minuti, ciascuno con la propria autorizzazione, così il download non richiede alcun header. Due differenze vale la pena conoscerle.

Questi sono file, non solo immagini. Una prova di pulizia è una fotografia, 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 è identico, e vale la pena ripeterlo perché l'errore è silenzioso: scarichi i byte adesso, conservi i byte anziché il collegamento, e richiami l'endpoint quando le serve un URL nuovo.

Gli errori che incontrerà

403 — la sua chiave raggiunge il ristorante ma non con l'ambito di quella collezione. L'endpoint delle collezioni è il modo economico per scoprire quali ambiti 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 ventiquattro 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.

Ultimo aggiornamento 2026-09-19.