Coleções e registos
Os vinte e quatro tipos de registo para além das entregas — o envelope de instantâneo que partilham, o que é um instantâneo, e como percorrer um.
As entregas são um recurso tratado à mão, moldado assim porque a receção de mercadorias foi a primeira coisa que alguém pediu. As coleções são o resto do registo: temperaturas, ciclos de arrefecimento, limpeza, controlos de fritadeiras, etiquetas — vinte e quatro delas, servidas através de um único formato genérico.
Onde o endpoint das entregas lhe dá um objeto desenhado, uma coleção dá-lhe o registo tal como a aplicação o guarda, envolvido num envelope que lhe diz quando foi capturado e se ainda existe. A troca é deliberada: é o que permite que um módulo novo chegue a esta API na semana em que é lançado, em vez de no trimestre seguinte.
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 âmbito 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. Uma marca de eliminação é o único caso em que pode faltar, por isso trate-o como presente em todo o resto. |
deleted | boolean | true significa uma marca de eliminação: 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 instantâneo é, 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.
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.
As marcas de eliminação 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.
As vinte e quatro coleções
Cada uma delas tem <collection>:read como âmbito — cooling precisa de
cooling:read, e assim por diante ao longo da lista.
| Coleção | O que é um registo |
|---|---|
restaurants | O estabelecimento em si: nome, morada, dias de encerramento, subscrição e definições. |
users | As contas do pessoal que faz os controlos, e que módulos utilizam. |
areas | As zonas em que um restaurante está dividido — cozinha, câmara frigorífica, bar. |
equipment | Frigoríficos, congeladores e equipamentos de cadeia de frio, com os seus limiares mínimo e máximo. |
sensors | As sondas sem fios, por endereço MAC, e o equipamento que cada uma vigia. |
temperature-records | Uma temperatura que uma pessoa mediu num equipamento, com o turno e a eventual ação corretiva. |
temperature-readings | Uma temperatura que um sensor comunicou sozinho, sem ninguém presente. |
suppliers | Quem entrega, com os meios de contacto e o número de conta. |
products | Os produtos recebidos e utilizados. |
preparations | As preparações feitas na casa, com prazo de validade e alergénios. |
cleaning-tasks | O plano de limpeza: cada tarefa, a sua zona, a sua recorrência, e se exige uma fotografia. |
cleaning-task-records | Uma tarefa de limpeza efetivamente feita — quando, e por quem. |
cleaning-task-pictures | A fotografia que o prova. Traz um ficheiro; veja a secção dos ficheiros abaixo. |
cooling | Um ciclo de arrefecimento: produto, temperatura inicial e final, hora de início e de fim. |
freezing | Uma operação de congelação, com o mesmo formato do arrefecimento. |
reheating | Uma operação de reaquecimento, com o mesmo formato do arrefecimento. |
transport | Um produto transportado, com local, hora e temperatura de partida e de chegada. |
fryer-equipment | As fritadeiras. |
fryer-checks | Um controlo da qualidade do óleo e o que foi decidido — filtrado, mudado, deixado como estava. |
cooking-equipment | Fornos e equipamentos de confeção, com os seus limiares. |
cooking-temperature-records | Uma temperatura de confeção, medida por uma pessoa, num equipamento de confeção. |
surface-analyses | Uma zaragatoa de superfície: o que foi testado, se passou, e o plano de ação se não passou. |
traceability-labels | Uma etiqueta de rastreabilidade impressa. Traz um ficheiro; veja a secção dos ficheiros abaixo. |
drive-files | Um documento arquivado no drive do restaurante. Traz um ficheiro; veja a secção dos ficheiros abaixo. |
Os formatos campo a campo de cada objeto data estão no
documento OpenAPI, que é gerado a
partir do serviço em execução — é a única descrição que não pode afastar-se
daquilo que está implantado.
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. |
Aqui não há intervalo de tempo, ao contrário das entregas: percorre-se uma coleção, não uma janela dela.
Um percurso é um instantâneo 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 você
reconcilia 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 as marcas de eliminação.
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
Três coleções trazem um ficheiro: cleaning-task-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 de forma muito parecida com as fotografias das entregas: uma lista de URL assinados, cada um válido durante quinze minutos, cada um a levar a sua própria autorização, por isso a transferência não precisa de qualquer cabeçalho. Vale a pena conhecer duas diferenças.
Isto são ficheiros, não só imagens. Uma prova de limpeza é uma fotografia,
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 é idêntico, e vale a pena repeti-lo porque a falha é silenciosa: vá buscar os bytes agora, guarde os bytes e não a ligação, e volte a chamar o endpoint quando precisar de um URL novo.
As falhas que vai encontrar
403 — a sua chave alcança o restaurante, mas não com o âmbito 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 quatro 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.