Colecciones y registros
Las veintiséis colecciones de registros, el sobre de snapshot que comparten y cómo recorrer una.
Una colección es un tipo de registro: temperature-records, cooling,
deliveries y otros veintitrés. Todas se leen igual, a través de los
mismos cuatro endpoints, y todos los registros vuelven en el mismo sobre: los
campos que rodean a data y que dicen cuándo se capturó y si sigue existiendo.
Cada colección es una concesión aparte. Una clave lee temperature-records
porque alguien concedió temperature-records:read, y con eso no viene nada
más.
Qué puede leer
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
Empiece por el primero. Lista solo las colecciones que se le concedieron a su clave, cada una con el scope que la abre, para que nunca tenga que adivinar:
{
"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 vacío no es un error ni una caída: es una clave sin ninguna concesión
de colección. La mayoría de las claves emitidas antes de que existiera esta
superficie están exactamente así, y ampliar una es un correo
electrónico.
El sobre
Todos los registros de todas las colecciones llevan los mismos campos
exteriores. Lo único que cambia de forma es data.
| Campo | Tipo | Significado |
|---|---|---|
id | string | El identificador del registro, estable y único dentro de la colección. |
restaurantId | string | El restaurante al que pertenece. Refleja la ruta. |
collection | string | De qué colección viene. Refleja la ruta. |
data | object | ausente | El registro en sí. Un tombstone es el único caso en que puede faltar, así que dé por hecho que está presente en todos los demás. |
deleted | boolean | true significa un tombstone: el registro se borró en la aplicación. |
capturedAt | string | Cuándo registró el cambio el dispositivo, en milisegundos desde la época Unix como cadena. |
receivedAt | string | Cuándo lo recibió esta API. Posterior a capturedAt, a veces por horas. |
sequence | string | Una posición monótonamente creciente en el flujo de cambios. Compare dos; no haga aritmética con una. |
mutationId | string | El identificador del cambio que produjo este estado. Clave de idempotencia por nuestra parte; clave de deduplicación por la suya. |
sourceVersion | string | null | La marca de versión del registro de origen, cuando la tiene. |
payloadVersion | number | Ahora mismo siempre 1. Sube si alguna vez cambia el significado de data. |
stateKind | string | Siempre received-shadow-snapshot. Lea la sección siguiente. |
capturedAt y receivedAt importan los dos. Una tableta en una cámara
frigorífica sin cobertura registra a las 09:00 y sube a las 14:00; ordenar su
propio pipeline por receivedAt lo mantiene correcto, e informar sobre
capturedAt lo mantiene veraz.
Qué es un snapshot y qué no
stateKind dice received-shadow-snapshot, y la formulación está medida.
- Es el estado actual de un registro, no un historial de cada edición. Lea un registro dos veces y obtendrá el aspecto que tiene ahora, las dos veces.
- Es lo que hemos recibido, no lo que tiene el restaurante. Un dispositivo que nunca subió un cambio significa un registro que esta API no ha visto nunca.
- No es una carga del histórico. Una colección empieza para un restaurante el día en que se le activa la captura. Los registros creados antes están en la aplicación y no aquí. Las entregas son la única excepción: esas dos colecciones llevan el historial que la aplicación todavía conservaba cuando se abrieron.
Así que un registro ausente es genuinamente ambiguo: nunca se registró, o se registró y todavía no ha llegado. No construya encima una cifra que se lea como una auditoría, y consulte la nota sobre exhaustividad antes de dar un recuento a nadie.
Los tombstones son el único caso en que la ausencia no es ambigua.
deleted: true sin data significa que el registro existió y se borró, y es la
única manera de enterarse de que algo ha desaparecido.
Cuánto tiempo se quedan los registros
Los registros operativos se quedan aquí 90 días, contados desde el momento
en que ocurrieron en la cocina (data.timestamp), no desde que los recibimos.
Después, un proceso nocturno los traslada a un archivo en frío bloqueado, que se
conserva cinco años, y dejan de aparecer en cualquier lectura: listas, registro
individual y sus archivos adjuntos.
| Se conservan 90 días | Se conservan durante toda la vida de la cuenta |
|---|---|
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 registro archivado no es un tombstone, y es la única excepción a la regla de los tombstones de arriba. Se va sin deleted: true, porque
nadie lo borró: se trasladó. Si mantiene una copia, conserve lo que ya tiene y
no trate nunca como un borrado un registro que falta en un recorrido. Para todo
lo que tenga más de 90 días, su copia es la fuente; una inspección que necesite
nuestro archivo pasa por soporte.
Las veintiséis colecciones
Cada una tiene <collection>:read como scope: cooling necesita
cooling:read, y así sucesivamente. Están documentadas por dominio, campo a
campo, en las páginas siguientes.
| Página | Colecciones |
|---|---|
| Temperaturas | temperature-records, temperature-readings, cooking-temperature-records |
| Entregas | deliveries, delivery-pictures |
| Etiquetas de trazabilidad | traceability-labels |
| Limpieza | cleaning-tasks, cleaning-task-records, cleaning-task-pictures |
| Freidoras | fryer-equipment, fryer-checks |
| Enfriamiento y procesos alimentarios | cooling, freezing, reheating, transport |
| Análisis de superficie | surface-analyses |
| Documentos | drive-files |
| Catálogo | suppliers, products, preparations |
| Instalaciones y personas | restaurants, users, areas, equipment, sensors, cooking-equipment |
Las temperaturas de una entrega están siempre en Celsius: una lectura tomada en
Fahrenheit se convierte al llegar, y todo temperatureRecords[].unit dice
"C".
Empiece por Instalaciones y personas si va a leer cualquiera de las otras: los umbrales de los equipos y las zonas son lo que convierte una medición a secas en algo que se puede juzgar.
El documento OpenAPI lleva las mismas formas, generadas a partir del servicio en ejecución: cuando una página de aquí y ese documento no coinciden, el documento tiene razón y nosotros tenemos una página que arreglar.
Recorrer una colección
GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=100
| Parámetro | Obligatorio | Reglas |
|---|---|---|
limit | no | De 1 a 100. Por defecto, 50. |
cursor | no | El nextCursor de la página anterior, tal cual. |
Aquí no hay rango de tiempo: usted recorre una colección, no una ventana de
ella. Para leer una semana de entregas, recorra deliveries y quédese con los
registros cuyo data.timestamp caiga en esa semana.
Un recorrido es un snapshot coherente. La primera página fija la posición
del flujo, y todas las páginas siguientes se responden en esa misma posición. Los
registros escritos mientras pagina no le mueven las páginas bajo los pies y no
aparecen a mitad del recorrido: los verá en el siguiente. El orden es por
sequence, ascendente.
El cursor está ligado al restaurante, a la colección y al límite. Cambie el tamaño de página a mitad del recorrido y se rechaza: elija un límite y consérvelo durante todo el recorrido.
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);
}
Mantener una copia al día
Hoy no hay parámetro since. Una actualización es otro recorrido, y usted
concilia con lo que ya tiene:
- Indexe por
iddentro de una colección, y sobrescriba cuando elsequenceque reciba sea mayor que el que guardó. - Respete los tombstones.
deleted: truees la instrucción de borrado; aplicarla es la única manera de que su copia deje de divergir. - Recorra a una hora sensata. Una colección entera con
limit=100son un puñado de peticiones para un solo restaurante, y el presupuesto es de 300 por minuto; pero varios cientos de restaurantes en el mismo minuto de cron son un pico del que responde usted.
Si un cursor incremental cambiaría lo que puede construir, díganoslo. Es un cambio pequeño sobre un flujo que ya está ordenado: la razón de que no exista es que nadie lo ha necesitado todavía.
Archivos adjuntos
Cuatro colecciones llevan un archivo: cleaning-task-pictures,
delivery-pictures, traceability-labels y drive-files. El registro lo nombra en data.asset;
los bytes salen del endpoint de archivos adjuntos.
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"
}
]
}
Responde con una lista de URL firmadas, cada una válida durante quince
minutos, y todas las URL de una misma respuesta comparten la misma caducidad.
Cada una lleva su propia autorización, de modo que la descarga no necesita
cabecera Authorization, lo que también significa que el enlace es en sí mismo
una credencial al portador: durante quince minutos, cualquiera que lo tenga
puede leer ese archivo. Conviene saber dos cosas.
Son archivos, no solo fotos. Una prueba de limpieza o una fotografía de
entrega es una imagen, pero una etiqueta de trazabilidad o un archivo del drive puede ser un PDF, un CSV,
una hoja de cálculo o un documento de Word: lea contentType en lugar de
suponer una imagen, y no renombre todo a .jpg al entrar.
sha256 está ahí para ahorrarle trabajo. Es el resumen criptográfico de los
bytes: si coincide con algo que ya guardó, no necesita volver a descargarlo.
Solo aparecen los archivos cuya subida ha terminado y se ha verificado: uno todavía en tránsito está ausente, no roto, y lo mismo ocurre en cuanto se borra el registro al que pertenece.
El resto del consejo vale la pena repetirlo porque el fallo es silencioso: descargue los bytes ahora, guarde los bytes en lugar del enlace, no ponga nunca la URL donde sobreviva a su propósito (un correo, un ticket, una caché de larga duración), y vuelva a llamar al endpoint cuando necesite una URL nueva. Es barato, y vuelve a comprobar la concesión.
Los fallos con los que se topará
403: su clave alcanza el restaurante, pero no con el scope de esa
colección. El endpoint de colecciones es la vía barata de averiguar cuáles sí
tiene.
404: ninguna concesión para ese restaurante, o ese registro no existe.
Ambos casos son indistinguibles a propósito.
400: una colección fuera de las veintiséis de arriba, o un cursor que
no pertenece a este restaurante, colección y límite.
Los tres son documentos de problema con un requestId.