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.

CampoTipoSignificado
idstringO identificador do registo, estável e único dentro da coleção.
restaurantIdstringO restaurante a que pertence. Repete o caminho.
collectionstringDe que coleção veio. Repete o caminho.
dataobject | ausenteO registo em si. Um tombstone é o único caso em que pode faltar, por isso trate-o como presente em todo o resto.
deletedbooleantrue significa um tombstone: o registo foi apagado na aplicação.
capturedAtstringQuando o dispositivo registou a alteração, em milissegundos desde a época Unix, em forma de cadeia de caracteres.
receivedAtstringQuando esta API o recebeu. Posterior a capturedAt, às vezes por horas.
sequencestringUma posição monotonicamente crescente no fluxo de alterações. Compare duas; não faça aritmética com uma.
mutationIdstringO identificador da alteração que produziu este estado. Chave de idempotência do nosso lado; chave de desduplicação do seu.
sourceVersionstring | nullO carimbo de versão do registo de origem, quando ele tem um.
payloadVersionnumberDe momento é sempre 1. Sobe se o significado de data alguma vez mudar.
stateKindstringSempre 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 diasConservados 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-picturesrestaurants, 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áginaColeções
Temperaturastemperature-records, temperature-readings, cooking-temperature-records
Entregasdeliveries, delivery-pictures
Etiquetas de rastreabilidadetraceability-labels
Limpezacleaning-tasks, cleaning-task-records, cleaning-task-pictures
Fritadeirasfryer-equipment, fryer-checks
Arrefecimento e processos alimentarescooling, freezing, reheating, transport
Análises de superfíciesurface-analyses
Documentosdrive-files
Catálogosuppliers, products, preparations
Instalações e pessoasrestaurants, 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âmetroObrigatórioRegras
limitnãoDe 1 a 100. Por omissão, 50.
cursornãoO 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 id dentro de uma coleção, e substitua quando o sequence que 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=100 sã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.

Última atualização em 2026-09-29.