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.

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. Uma marca de eliminação é o único caso em que pode faltar, por isso trate-o como presente em todo o resto.
deletedbooleantrue significa uma marca de eliminação: 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 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çãoO que é um registo
restaurantsO estabelecimento em si: nome, morada, dias de encerramento, subscrição e definições.
usersAs contas do pessoal que faz os controlos, e que módulos utilizam.
areasAs zonas em que um restaurante está dividido — cozinha, câmara frigorífica, bar.
equipmentFrigoríficos, congeladores e equipamentos de cadeia de frio, com os seus limiares mínimo e máximo.
sensorsAs sondas sem fios, por endereço MAC, e o equipamento que cada uma vigia.
temperature-recordsUma temperatura que uma pessoa mediu num equipamento, com o turno e a eventual ação corretiva.
temperature-readingsUma temperatura que um sensor comunicou sozinho, sem ninguém presente.
suppliersQuem entrega, com os meios de contacto e o número de conta.
productsOs produtos recebidos e utilizados.
preparationsAs preparações feitas na casa, com prazo de validade e alergénios.
cleaning-tasksO plano de limpeza: cada tarefa, a sua zona, a sua recorrência, e se exige uma fotografia.
cleaning-task-recordsUma tarefa de limpeza efetivamente feita — quando, e por quem.
cleaning-task-picturesA fotografia que o prova. Traz um ficheiro; veja a secção dos ficheiros abaixo.
coolingUm ciclo de arrefecimento: produto, temperatura inicial e final, hora de início e de fim.
freezingUma operação de congelação, com o mesmo formato do arrefecimento.
reheatingUma operação de reaquecimento, com o mesmo formato do arrefecimento.
transportUm produto transportado, com local, hora e temperatura de partida e de chegada.
fryer-equipmentAs fritadeiras.
fryer-checksUm controlo da qualidade do óleo e o que foi decidido — filtrado, mudado, deixado como estava.
cooking-equipmentFornos e equipamentos de confeção, com os seus limiares.
cooking-temperature-recordsUma temperatura de confeção, medida por uma pessoa, num equipamento de confeção.
surface-analysesUma zaragatoa de superfície: o que foi testado, se passou, e o plano de ação se não passou.
traceability-labelsUma etiqueta de rastreabilidade impressa. Traz um ficheiro; veja a secção dos ficheiros abaixo.
drive-filesUm 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âmetroObrigatórioRegras
limitnãoDe 1 a 100. Por omissão, 50.
cursornãoO 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 id dentro de uma coleção, e substitua quando o sequence que 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=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

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.

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