Coleções e registos
As vinte e seis coleções de registos, o envelope de snapshot que partilham, e como percorrer uma.
Uma coleção é um tipo de registo: temperature-records, cooling,
deliveries e outros vinte e três. Todas se leem da mesma maneira,
através dos mesmos quatro endpoints, e todos os registos voltam no mesmo
envelope: os campos à volta de data que dizem quando foi capturado e se ainda
existe.
Cada coleção é uma concessão à parte. Uma chave lê temperature-records porque
alguém concedeu temperature-records:read, e mais nada vem com isso.
O que pode ler
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
Comece pelo primeiro. Ele lista apenas as coleções que foram concedidas à sua chave, cada uma com o scope que a abriu, para que nunca tenha de adivinhar:
{
"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
}
]
}
Um data vazio não é um erro nem uma falha de serviço: é uma chave sem
concessões de coleções. A maioria das chaves emitidas antes de esta superfície
existir está exatamente nesse estado, e alargar uma é um email.
O envelope
Todos os registos de todas as coleções trazem os mesmos campos exteriores. Só o
data muda de formato.
| Campo | Tipo | Significado |
|---|---|---|
id | string | O identificador do registo, estável e único dentro da coleção. |
restaurantId | string | O restaurante a que pertence. Repete o caminho. |
collection | string | De que coleção veio. Repete o caminho. |
data | object | ausente | O registo em si. Um tombstone é o único caso em que pode faltar, por isso trate-o como presente em todo o resto. |
deleted | boolean | true significa um tombstone: o registo foi apagado na aplicação. |
capturedAt | string | Quando o dispositivo registou a alteração, em milissegundos desde a época Unix, em forma de cadeia de caracteres. |
receivedAt | string | Quando esta API o recebeu. Posterior a capturedAt, às vezes por horas. |
sequence | string | Uma posição monotonicamente crescente no fluxo de alterações. Compare duas; não faça aritmética com uma. |
mutationId | string | O identificador da alteração que produziu este estado. Chave de idempotência do nosso lado; chave de desduplicação do seu. |
sourceVersion | string | null | O carimbo de versão do registo de origem, quando ele tem um. |
payloadVersion | number | De momento é sempre 1. Sobe se o significado de data alguma vez mudar. |
stateKind | string | Sempre received-shadow-snapshot. Leia a secção seguinte. |
capturedAt e receivedAt contam os dois. Um tablet numa câmara frigorífica
sem sinal regista às 09:00 e carrega às 14:00; ordenar o seu próprio
processamento por receivedAt mantém-no correto, e fazer os relatórios por
capturedAt mantém-no verdadeiro.
O que um snapshot é, e o que não é
O stateKind diz received-shadow-snapshot, e a formulação é cuidadosa.
- É o estado atual de um registo, não um histórico de todas as edições. Leia um registo duas vezes e recebe, nas duas, o aspeto que ele tem agora.
- É o que nós recebemos, não o que o restaurante tem. Um dispositivo que nunca carregou uma alteração significa um registo que esta API nunca viu.
- Não é uma recuperação de histórico. Uma coleção começa, para um restaurante, no dia em que a captura é ativada para ele. Os registos criados antes disso estão na aplicação e não aqui. As entregas são a única exceção: essas duas coleções trazem o histórico que a aplicação ainda tinha quando foram abertas.
Por isso, um registo ausente é genuinamente ambíguo: nunca foi registado, ou foi registado e ainda não chegou. Não construa em cima disso um número que se leia como uma auditoria, e veja a nota sobre a completude antes de comunicar uma contagem a quem quer que seja.
Os tombstones são o único caso em que a ausência não é ambígua.
deleted: true sem data significa que o registo existiu e foi apagado, e é a
única forma de ficar a saber que alguma coisa desapareceu.
Quanto tempo os registos ficam aqui
Os registos operacionais ficam aqui 90 dias, contados a partir do momento em
que aconteceram na cozinha (data.timestamp), não de quando os recebemos.
Depois disso, uma tarefa noturna move-os para um arquivo frio bloqueado,
conservado durante cinco anos, e deixam de aparecer em qualquer leitura: listas,
registo individual e respetivos ficheiros.
| Conservados 90 dias | Conservados durante toda a vida da conta |
|---|---|
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 |
Um registo arquivado não é um tombstone, e é a única exceção à regra dos tombstones acima. Sai sem deleted: true, porque
ninguém o apagou: foi movido. Se mantém uma cópia, guarde o que já tem e nunca
trate como eliminação um registo que falta num percurso. Para tudo o que tenha
mais de 90 dias, a sua cópia é a fonte; uma inspeção que precise do nosso
arquivo passa pelo suporte.
As vinte e seis coleções
Cada uma delas tem <collection>:read como scope: cooling precisa de
cooling:read, e assim por diante. Estão documentadas por domínio, campo a
campo, nas páginas abaixo.
| Página | Coleções |
|---|---|
| Temperaturas | temperature-records, temperature-readings, cooking-temperature-records |
| Entregas | deliveries, delivery-pictures |
| Etiquetas de rastreabilidade | traceability-labels |
| Limpeza | cleaning-tasks, cleaning-task-records, cleaning-task-pictures |
| Fritadeiras | fryer-equipment, fryer-checks |
| Arrefecimento e processos alimentares | cooling, freezing, reheating, transport |
| Análises de superfície | surface-analyses |
| Documentos | drive-files |
| Catálogo | suppliers, products, preparations |
| Instalações e pessoas | restaurants, users, areas, equipment, sensors, cooking-equipment |
As temperaturas dentro de uma entrega estão sempre em graus Celsius: uma
leitura feita em Fahrenheit é convertida quando chega, e todos os
temperatureRecords[].unit dizem "C".
Comece por Instalações e pessoas se for ler qualquer uma das outras: são os limiares dos equipamentos e as zonas que transformam uma medição nua em algo que consegue julgar.
O documento OpenAPI traz os mesmos formatos, gerados a partir do serviço em execução. Quando uma página daqui e esse documento discordarem, o documento é que está certo e nós temos uma página para corrigir.
Percorrer uma coleção
GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=100
| Parâmetro | Obrigatório | Regras |
|---|---|---|
limit | não | De 1 a 100. Por omissão, 50. |
cursor | não | O nextCursor da página anterior, tal e qual. |
Não há intervalo de tempo: percorre-se uma coleção, não uma janela dela. Para
ler uma semana de entregas, percorra deliveries e guarde os registos cujo
data.timestamp cai nessa semana.
Um percurso é um snapshot coerente. A primeira página fixa a posição no
fluxo, e todas as páginas seguintes são respondidas a partir dessa mesma
posição. Os registos escritos enquanto está a paginar não deslocam as páginas
debaixo de si e não aparecem a meio do percurso. Vê-os no percurso seguinte. A
ordenação é por sequence, ascendente.
O cursor está ligado ao restaurante, à coleção e ao limite. Mudar o tamanho da página a meio do percurso é rejeitado: escolha um limite e mantenha-o durante todo o percurso.
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);
}
Manter uma cópia atualizada
De momento não há parâmetro since. Uma atualização é outro percurso, e
reconcilia-se com o que já tem:
- Indexe pelo
iddentro de uma coleção, e substitua quando osequenceque recebe for maior do que o que guardou. - Respeite os tombstones.
deleted: trueé a instrução de apagar; aplicá-la é a única forma de a sua cópia deixar de divergir. - Percorra a uma hora sensata. Uma coleção inteira com
limit=100são poucos pedidos para um único restaurante, e o orçamento é de 300 por minuto, mas várias centenas de restaurantes no mesmo minuto do cron são um pico que é seu.
Se um cursor incremental mudasse aquilo que consegue construir, diga-o. É uma alteração pequena num fluxo que já está ordenado: a razão para não existir é que ainda ninguém precisou dele.
Ficheiros
Quatro coleções trazem um ficheiro: cleaning-task-pictures,
delivery-pictures, traceability-labels e drive-files. O registo indica-o em data.asset; os
bytes vêm do endpoint assets.
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 com uma lista de URL assinados, cada um válido durante quinze
minutos, e todos os URL de uma mesma resposta partilham a mesma expiração.
Cada um leva a sua própria autorização, por isso a transferência não precisa de
qualquer cabeçalho Authorization, o que também significa que a ligação é, ela
própria, uma credencial ao portador: durante quinze minutos, qualquer pessoa
que a tenha consegue ler esse ficheiro. Vale a pena saber duas coisas.
Isto são ficheiros, não só imagens. Uma prova de limpeza ou uma fotografia
de entrega é uma imagem, mas uma etiqueta de rastreabilidade ou um ficheiro do drive pode ser um PDF, um
CSV, uma folha de cálculo ou um documento Word. Leia o contentType em vez de
assumir que é uma imagem, e não renomeie tudo para .jpg à entrada.
O sha256 está ali para lhe poupar trabalho. É o resumo criptográfico dos
bytes: se corresponder a alguma coisa que já guardou, não precisa de a voltar a
transferir.
Só aparecem os ficheiros cujo carregamento terminou e foi verificado: um que ainda vem a caminho está ausente, não avariado, e o mesmo acontece assim que o registo a que pertence é apagado.
O resto dos conselhos vale a pena repetir, porque a falha é silenciosa: vá buscar os bytes agora, guarde os bytes e não a ligação, nunca ponha o URL num sítio onde sobreviva ao seu propósito (um email, um pedido de suporte, uma cache de longa duração), e volte a chamar o endpoint quando precisar de um URL novo. É barato, e volta a verificar a concessão.
As falhas que vai encontrar
403: a sua chave alcança o restaurante, mas não com o scope dessa
coleção. O endpoint das coleções é a forma barata de descobrir quais é que ela
tem.
404: nenhuma concessão para esse restaurante, ou esse registo não existe.
Os dois casos são indistinguíveis de propósito.
400: uma coleção fora das vinte e seis acima, ou um cursor que não
pertence a este restaurante, a esta coleção e a este limite.
Os três são documentos de problema com um requestId.