Banco de dados (CRUD)

API CRUD centralizada (/data) + dicionário de metadados (/dictionary). Endpoint único para todas as operações com modo structured (recomendado) ou raw.

Banco de dados (CRUD)

API centralizada para acesso ao banco de dados da Plataforma Keeptor. Dois endpoints:

  • /data: operações CRUD genéricas (SELECT/INSERT/UPDATE/DELETE/UPSERT) com 2 modos: structured (recomendado) e raw
  • /dictionary: retorna metadados de uma entidade (cabeçalhos de tabelas dinâmicas, chaves estrangeiras como objetos aninhados, suporte a i18n)

Modos do /data

🔷 Modo structured (recomendado)

Use para operações comuns. Mais seguro (parametrizado) e fácil de usar:

  • SELECT: buscar dados com filtros, joins, paginação, ordenação
  • INSERT: inserir um ou mais registros (data como array)
  • UPDATE: atualizar registros com filters (data como objeto)
  • UPSERT: inserir ou atualizar com onConflict declarado
  • DELETE: remover com filters

Gravação em lote (INSERT e UPSERT): até 1.000 registros por requisição. A requisição é atômica (grava tudo ou nada): se um registro for recusado, nenhum é gravado e a resposta 400 explica o motivo. Mais de 1.000 registros: divida em várias requisições (cada uma atômica por si). No UPSERT só-chave (todas as colunas enviadas estão no onConflict, como num vínculo entre dois cadastros), os registros que já existem são ignorados: data.rows/data.affected trazem só os inseridos e data.ignored, quantos já existiam.

Operadores de filtro: =, <>, >, >=, <, <=, like, ilike, in, not in, is, is not. Não existe != nem IS NULL (usar <> e is + value: null).

Marcadores "$me" e "$now". Alguns valores de filtro são resolvidos pela própria API no momento da consulta, a partir da sessão do token. A mesma requisição (ou visão salva) serve qualquer usuário e qualquer dia, e nem o id do usuário nem a data calculada no aplicativo trafegam.

MarcadorSignificaOperadores aceitos
"$me"o usuário logado= e como item de uma lista in (pode misturar com ids). Com like/ilike é texto comum de busca
"$now"agora=, <>, >, >=, <, <= (comparação de datas)
"$now(-24 hours)", "$now(+1 day)"agora mais ou menos um intervalo: sinal obrigatório, número de 1 a 9999 (para year, de 1 a 100), unidade minute, hour, day, week, month ou year (singular ou plural)os mesmos de "$now"
{ "column": "assigned_user_id", "operator": "=", "value": "$me" }
{ "column": "assigned_user_id", "operator": "in", "value": ["$me", 4] }
{ "column": "last_message_at", "operator": ">=", "value": "$now(-24 hours)" }
  • Valem em filters (também de UPDATE e DELETE) e nas condições de joins e having. Em data (gravação) não têm efeito especial.
  • Marcador com operador não aceito, in sem lista ou forma inválida ("$me(1)", "$now(-24 horas)", "$now(-0 hours)", "$now(24 hours)", "$now(-101 years)"): 400 e error.key = core.facets.invalid_condition (params.field, params.operator).
  • Só a forma exata (minúsculas, sem espaços nas pontas). "$ME" é texto comum.

⚙️ Modo raw (avançado)

Aceita SQL parametrizado direto (apenas SELECT). Use somente quando o modo structured não cobrir o caso. Sempre valide o payload contra o schema OpenAPI antes de enviar.

Nomes em vez de códigos: resolução de chave estrangeira

Uma consulta comum devolve city_id: 3, e a tela precisa exibir Belo Horizonte. Em vez de buscar o nome de cada cidade em uma chamada separada (uma por linha, ou uma por identificador distinto), declare no próprio select quais chaves estrangeiras quer resolver:

{
  "mode": "structured",
  "operation": "select",
  "tables": ["kpt_shared_con"],
  "columns": ["contact_id", "contact_name", "city_id", "cargo_id"],
  "resolve": [
    { "column": "city_id" },
    { "column": "city_id.state_id" },
    { "column": "cargo_id", "as": "cargo_descricao" }
  ],
  "locale": "pt"
}

A resposta chega achatada, na mesma linha e na mesma chamada de rede:

{
  "contact_id": 7,
  "contact_name": "Contato Exemplo",
  "city_id": 1,
  "city_name": "São Paulo",
  "state_name": "São Paulo",
  "cargo_id": 13,
  "cargo_descricao": "Analista"
}

O que vale saber:

  • É opcional. Sem o campo resolve, nada muda: mesma consulta, mesma resposta de sempre.
  • Você não descreve o caminho. Nada de tabela de destino, chave física ou condição de junção: só o nome da coluna. Quem descobre isso é a plataforma, que também garante o isolamento por organização.
  • O identificador continua lá. city_id permanece com o mesmo nome, tipo e valor; o rótulo vem como uma chave irmã. Nada que já consumia a resposta quebra.
  • A chave do rótulo é a coluna de exibição do destino (city_id → city_name). Use as para renomear.
  • Vários níveis. "city_id.state_id" traz o estado da cidade do contato. O limite é 5 níveis.
  • Ordenar e filtrar pelo rótulo funciona: use a chave de saída (city_name) em order e filters, como qualquer outra coluna.
  • locale traduz o rótulo (pt, en, es) quando a entidade de destino tem descrição traduzida. A chave de dados não muda entre idiomas, só o valor.
  • Nomes repetidos são recusados, não sobrescritos. Se duas resoluções produzirem a mesma chave de saída, a consulta não é executada e a resposta é 400. Para corrigir, dê outro nome a uma delas com as.
  • Limites: vale apenas na operação select, e não pode ser combinado com joins explícitos, porque é justamente o trabalho que ele substitui.

Erros do resolve. Todo erro de resolução responde 400 com error.code igual a KPT_FK_ERROR e error.field igual a resolve. O motivo exato vem em error.key (estável, use no seu código) e em error.params.reason:

error.keyparams.reasonQuando
core.fk.collision_resolveKPT_FK_COLLISIONDuas resoluções geram a mesma chave de saída (params.key)
core.fk.locale_invalidKPT_FK_LOCALE_INVALIDlocale fora de pt, en, es
core.fk.no_constraintKPT_FK_NO_CONSTRAINTA coluna não aponta para outro cadastro

Exemplo real (duas resoluções que geram entity_name):

{
  "success": false,
  "message": "Duas colunas resolvidas produzem a mesma chave de saída: \"entity_name\".",
  "error": {
    "type": "VALIDATION_ERROR",
    "code": "KPT_FK_ERROR",
    "message": "Duas colunas resolvidas produzem a mesma chave de saída: \"entity_name\".",
    "key": "core.fk.collision_resolve",
    "params": { "key": "entity_name", "path": "fk_entity_id", "reason": "KPT_FK_COLLISION" },
    "field": "resolve",
    "internal": "Rótulo de chave estrangeira: duas resoluções produzem o mesmo nome de saída; informe outro nome com \"as\". Params: key, path.",
    "fields": ["resolve"]
  }
}

Segurança e isolamento

  • Token obrigatório: todas as rotas exigem Authorization: Bearer <access_token>
  • Isolamento por organização: você só acessa dados da sua própria organização. Tentar referenciar dados de outra organização retorna 404 NOT_FOUND

i18n

Queries que dependem do /dictionary (catálogos traduzíveis) devem incluir o campo locale (pt, en, es) no payload.

Erros

Todo erro segue o envelope { "success": false, "message", "error": { "type", "code", "message" } }. message é o texto para exibir ao usuário.

Nos erros de regra de negócio, de permissão e nas validações da própria requisição (campo obrigatório, nome inválido, filtro mal formado, função não liberada), error traz também:

CampoPara que serve
keyChave estável do erro (ex.: access.denied). Não muda com o idioma nem quando o texto é revisto: use no seu código para reconhecer o erro
paramsValores usados no texto, separados (ex.: { "action": "delete", "entity": "kpt_shared_cct" })
fieldCampo do registro que causou o erro, para mostrar a mensagem embaixo dele (também em fields[0]). Pode ser null
internalDescrição da regra para o time técnico/suporte (em português). Exiba de forma discreta, se exibir

Nesses erros, message já vem no idioma do usuário logado (português, inglês ou espanhol). Se o usuário não escolheu idioma, vale o cabeçalho Accept-Language da requisição (pt-BR, en-US, es-ES); sem ele, ou sem tradução, português.

Os erros antes do login (token ausente, sessão expirada ou encerrada) saem em português, com key (ex.: gateway.auth.session_expired): traduza pela chave no seu app.

Com key, error.message é sempre o mesmo texto de message (nunca um detalhe técnico). Quando há um motivo técnico útil para diagnóstico, ele vem em error.details.reason; não o exiba ao usuário. error.code está sempre presente: o código do banco quando o erro veio dele (ex.: 23505, 42501, P0001), senão o mesmo valor de type.

No /dictionary: 400 com gateway.dictionary.entity_required (sem entity), 422 com core.entity.not_found (entidade inexistente, params.entity) e 500 com gateway.dictionary.load_failed.

HTTPerror.typeQuando
400VALIDATION_ERRORRequisição mal formada (campo obrigatório, nome de tabela/campo inválido, função não liberada)
400CONSTRAINT_ERROR / DATA_ERRORDado recusado pelo banco: duplicado, obrigatório, formato inválido. O texto não repete valores enviados nem dados do registro
401UNAUTHORIZEDSem token ou sessão encerrada/expirada
403ACCESS_DENIEDSem permissão para a operação
404NOT_FOUNDRegistro não existe ou não pertence à sua organização
422BUSINESS_RULEUma regra de negócio recusou a operação; message explica o motivo
500QUERY_ERROR / DATABASE_ERRORFalha inesperada. Tente de novo; se persistir, abra chamado com o horário

Exemplo (usuário com idioma inglês):

{
  "success": false,
  "message": "You do not have permission to delete Cost center.",
  "error": {
    "type": "ACCESS_DENIED",
    "code": "42501",
    "message": "You do not have permission to delete Cost center.",
    "key": "access.denied",
    "params": { "action": "delete", "entity": "kpt_shared_cct" },
    "field": null,
    "internal": "Acesso negado pela checagem de permissão: o usuário não tem a permissão exigida para essa ação nessa entidade. Params: action (create/read/update/delete), entity (entidade)."
  }
}