Skip to content

Gravar em Cadastro ​

Tipo
Como fazer
Para
Desenvolvedor
Na tela
DataViz › Aplicação › Editar › Dashboards › Editando Widget › Svelte › Editar Código

O objeto app de um Widget Svelte tem o app.writeback, que lê e grava registros de um Cadastro, a tabela que as pessoas preenchem dentro do Lumo. Com ele você monta uma tela de entrada de dado em Svelte: confirmar um alerta, anotar uma ocorrência, corrigir um registro. O mesmo vale para o componente paramétrico.

Antes de começar ​

  • O Cadastro precisa estar publicado e ser uma das tabelas da Aplicação onde o Widget está. Cadastro de fora da Aplicação responde TABLE_NOT_FOUND. Veja Cadastros no BI.
  • O que cada pessoa consegue fazer é decidido pela liberação dela no Cadastro: Ler, Criar, Atualizar e Excluir. O servidor confere a liberação em cada chamada. Veja Permissões de Cadastro e Acesso a dados.
  • Cadastro e campo se referenciam pelo nome: a tabela pelo nome que ela tem na Aplicação (o mesmo de '[Tabela]."COLUNA":SUM' no fetchMatrix) e o campo pelo nome interno (STATUS). Nunca use número de ID. O id do registro é um texto (uuid) que vem da leitura.

Métodos ​

Todos devolvem uma Promise e lançam WritebackError quando falham.

MétodoDevolveO que faz
app.writeback.listTables()[{ table, label, permissions }]Os Cadastros da Aplicação que a pessoa pode ler. table é o nome para passar aos outros métodos. permissions é { read, create, update, delete }
app.writeback.metadata(tabela){ table, label, fields, permissions, rowScope }Os campos e o que a pessoa pode fazer. Fica em cache enquanto o Widget está montado
app.writeback.find(tabela, consulta?){ rows, total }Lista registros. Veja Ler
app.writeback.findOne(tabela, id) ou (tabela, { where })registro ou nullUm registro pelo id, ou o primeiro que casar com o where. Não achar devolve null, sem erro
app.writeback.save(tabela, dados)o registro como ficouSem id em dados, cria. Com id, atualiza. Veja Gravar
app.writeback.delete(tabela, id)trueExclui o registro
app.refreshDashboard()Promise<void>Recarrega os outros Widgets do Dashboard. Veja Depois de gravar

Em metadata, cada item de fields tem name, label, type (number, date, string, external_key, image ou picklist) e required, e, quando existem, default, options, ref e granularity. Em options (lista de escolha), cada opção é { value, color? }. Campo escondido da pessoa pela liberação não aparece em fields. rowScope vale true quando a liberação restringe as linhas que a pessoa vê.

Um registro é um objeto com os campos no topo (row.STATUS), mais id e version. Esses dois nomes são reservados e não colidem com campo.

Use metadata antes de desenhar a tela: esconda o botão de excluir sem permissions.delete, transforme a lista de escolha em botões e marque os campos obrigatórios.

Ler ​

javascript
const r = await app.writeback.find('Ocorrências de Perda', {
  where: { STATUS: 'Pendente' },
  orderBy: ['DT:DESC'],
  limit: 50,
});
// r.rows  → [{ STATUS, DESCRICAO, DT, id, version }, ...]
// r.total → quantos registros casam
ParâmetroFormatoPadrão
whereObjeto: igualdade por campo, todas juntas. null como valor procura campo vazio. Ou lista de { field, op, value, value2? }todos os registros
orderBy'CAMPO' ou 'CAMPO:DESC', ou uma lista delessem ordem
limitNúmero de registros, no máximo 1000100
offsetQuantos registros pular0

Operadores da lista: =, !=, >, <, >=, <=, between (com value2), like, not_like, is_null e not_null. Os dois últimos não levam value.

javascript
await app.writeback.find('Metas', {
  where: [
    { field: 'VALOR', op: '>', value: 100 },
    { field: 'DATA', op: 'between', value: '2026-11-01', value2: '2026-11-30' },
  ],
});

Campo que não existe ou está escondido para a pessoa dá UNKNOWN_FIELD; o servidor nunca ignora o filtro e devolve tudo. Pelo mesmo motivo, where: { STATUS: undefined } é recusado com VALIDATION antes de ir ao servidor: o JSON descartaria a chave e o find traria todas as linhas. Use null para campo vazio.

Gravar ​

javascript
// cria
const novo = await app.writeback.save('Ocorrências de Perda', { DESCRICAO: 'Quebra no estoque', STATUS: 'Pendente' });

// atualiza, com a versão que você leu
const atual = await app.writeback.save('Ocorrências de Perda', { id: row.id, version: row.version, STATUS: 'Verificado' });

Na atualização, mande só o id, a version e os campos que mudam.

Com id e version, o servidor atualiza só se ninguém gravou o registro depois da sua leitura; senão responde CONFLICT. Esse é o caminho a preferir. Com id e sem version, a última escrita vence, sem conferência: serve para "marcar o status e pronto", e sobrescreve uma edição que outra pessoa fez no meio. id que não é um uuid válido responde NOT_FOUND.

Campo de imagem recebe a foto como data-URI (data:image/jpeg;base64,...), em PNG, JPEG, WebP ou GIF, até 5 MB. O save sobe a imagem e grava o endereço dela no campo; o registro devolvido já traz o endereço. Reduza a foto antes de mandar (por exemplo, desenhando num canvas de até 1600 px): foto de celular inteira passa do limite. Endereço de imagem que já existe também é aceito.

js
const foto = await reduzir(arquivo);   // data:image/jpeg;base64,...
await app.writeback.save('Conferência dos Avisos', { COD_AVISO: aviso, SITUACAO: 'Conferido', COMPROVANTE: foto });

As regras do Cadastro valem como na grade: tipo do campo, campos obrigatórios, restrições de unicidade, chave externa e as validações em JavaScript do próprio Cadastro. save e delete esperam até 3 s a cópia do registro para o DW. Se ela demora mais, a gravação vale do mesmo jeito.

Depois de gravar ​

find e findOne leem o registro que acabou de ser gravado. O fetchMatrix lê o DW, que recebe a cópia instantes depois; um Widget que consulta o mesmo Cadastro pelo fetchMatrix pode mostrar o valor anterior por um momento. Veja Cadastros no BI.

app.refreshDashboard() recarrega os outros Widgets, para que o número do Dashboard acompanhe a gravação. O Widget que chamou não é recarregado, porque perderia o rascunho do formulário, o estado "salvando" e a mensagem de erro. Para atualizar a lista dele, chame find de novo. Chamar refreshDashboard é decisão do seu código: save e delete não o fazem sozinhos.

Erros e o que fazer ​

Todo erro é um WritebackError, com estes campos:

CampoO que é
codeO código da tabela abaixo. Não muda entre versões
messageTexto pronto para mostrar, no idioma de quem está vendo (pt-BR, en ou es)
paramsDetalhe do erro, como { action } em FORBIDDEN e { field } em UNKNOWN_FIELD
fieldsSó em VALIDATION: { CAMPO: mensagem }, uma mensagem por campo. A mensagem que o dono do Cadastro escreveu numa validação em JavaScript chega como ele escreveu
codeQuando aconteceO que o Widget faz
NO_SESSIONNão há usuário logado, como em link públicoMostra message e não oferece ação de gravar
TABLE_NOT_FOUNDO nome não é um Cadastro desta AplicaçãoCorrija o nome no código. params.table traz o nome pedido
AMBIGUOUS_TABLEDois Cadastros da Aplicação têm o mesmo nomePeça a quem monta a Aplicação para renomear um deles
FORBIDDENA liberação da pessoa não permite a ação, ou o registro está fora das linhas delaEsconde as ações e mostra message. params.action diz qual ação
VALIDATIONTipo errado, campo obrigatório vazio, valor repetido numa restrição de unicidade, chave externa que não existe ou regra do CadastroMostra fields ao lado de cada campo. Sem fields (por exemplo, operador inválido em where), mostra message
CONFLICTAlguém gravou o registro depois da sua leituraRecarrega a lista com find e avisa com message. A pessoa refaz a ação sobre o dado novo
NOT_FOUNDO id não existe ou foi excluídoRecarrega a lista e mostra message
UNKNOWN_FIELDCampo inexistente ou escondido em where, orderBy ou save. params.field traz o nomeCorrija o nome no código
LIMITO Cadastro chegou ao limite de registrosMostra message. O limite é do contrato; veja Recursos, limites e cobrança
NETWORKQualquer outra falha: conexão, erro interno do servidorMostra message e deixa a pessoa tentar de novo

Envolva toda chamada em try/catch. Uma exceção solta num onMount deixa o Widget em branco, e uma gravação que falhou sem aviso parece ter dado certo.

Exemplo completo ​

Lista as ocorrências pendentes e deixa a pessoa mudar o status de cada uma. Os botões só aparecem para quem pode atualizar, as opções de status vêm do Cadastro e nada é gravado ao montar.

svelte
<script>
  import { onMount } from 'svelte';

  export let app;

  const TABELA = 'Ocorrências de Perda';
  const CAMPO_STATUS = 'STATUS';
  const PENDENTE = 'Pendente';

  let meta = null;
  let linhas = [];
  let carregando = true;
  let salvandoId = null;
  let erro = '';
  let erroDaLinha = {};

  $: podeAtualizar = !!app.user && !!meta?.permissions?.update;
  $: opcoes = (meta?.fields.find((f) => f.name === CAMPO_STATUS)?.options ?? [])
    .map((o) => o.value)
    .filter((v) => v !== PENDENTE);

  async function recarregar() {
    const r = await app.writeback.find(TABELA, {
      where: { [CAMPO_STATUS]: PENDENTE },
      orderBy: ['DT:DESC'],
      limit: 50,
    });
    linhas = r.rows;
  }

  // Só leitura. Gravar aqui rodaria também na exportação de PDF e imagem.
  onMount(async () => {
    try {
      meta = await app.writeback.metadata(TABELA);
      await recarregar();
    } catch (e) {
      erro = e?.message ?? String(e);
    }
    carregando = false;
  });

  async function mudarStatus(linha, status) {
    if (salvandoId) return;
    salvandoId = linha.id;
    erro = '';
    erroDaLinha = {};
    try {
      await app.writeback.save(TABELA, { id: linha.id, version: linha.version, [CAMPO_STATUS]: status });
      await recarregar();             // atualiza este Widget
      await app.refreshDashboard();   // atualiza os outros
    } catch (e) {
      if (e?.code === 'VALIDATION') {
        erroDaLinha = { [linha.id]: Object.values(e.fields ?? {}).join(' ') || e.message };
      } else if (e?.code === 'CONFLICT') {
        try { await recarregar(); } catch (_) { /* a mensagem abaixo já avisa */ }
        erro = e.message;
      } else if (e?.code === 'FORBIDDEN') {
        meta = { ...meta, permissions: { ...meta.permissions, update: false } };
        erro = e.message;
      } else {
        erro = e?.message ?? String(e);
      }
    } finally {
      salvandoId = null;
    }
  }
</script>

{#if carregando}
  <div class="flex h-full items-center justify-center text-slate-500 dark:text-navy-300">Carregando…</div>
{:else}
  <div class="w-full p-3 bg-white text-slate-800 dark:bg-navy-800 dark:text-navy-100">
    {#if erro}
      <p class="mb-2 text-sm text-error">{erro}</p>
    {/if}

    {#if linhas.length === 0 && !erro}
      <p class="text-sm text-slate-500 dark:text-navy-300">Nenhuma ocorrência pendente.</p>
    {/if}

    <ul class="divide-y divide-slate-200 dark:divide-navy-600">
      {#each linhas as linha (linha.id)}
        <li class="py-2">
          <div class="flex items-center justify-between gap-2">
            <span class="text-sm">{linha.DESCRICAO}</span>
            {#if podeAtualizar}
              <span class="flex gap-1">
                {#each opcoes as status}
                  <button
                    class="px-2 py-1 text-xs rounded border border-slate-200 bg-slate-100 text-slate-700 hover:bg-slate-200 disabled:opacity-50 dark:border-navy-600 dark:bg-navy-700 dark:text-navy-100"
                    disabled={salvandoId !== null}
                    on:click={() => mudarStatus(linha, status)}
                  >
                    {salvandoId === linha.id ? 'Salvando…' : status}
                  </button>
                {/each}
              </span>
            {/if}
          </div>
          {#if erroDaLinha[linha.id]}
            <small class="text-error">{erroDaLinha[linha.id]}</small>
          {/if}
        </li>
      {/each}
    </ul>
  </div>
{/if}

O Cadastro Ocorrências de Perda, com os campos STATUS (lista de escolha), DESCRICAO e DT, é fictício; troque pelos seus. Confira o nome e os campos do Cadastro antes de escrever o código. As regras de tema, carregando e vazio são as de Boas práticas.

Segurança ​

  • A liberação é a permissão de verdade. Esconder um botão com metadata é conforto de tela. Quem abre as ferramentas do navegador chama app.writeback por conta própria, e o servidor só aceita o que a liberação da pessoa permite. A exigência de o Cadastro estar na Aplicação delimita o que o Widget alcança, mas não protege: quem tem a liberação já grava pela tela de Cadastros.
  • Link público não grava. Sem sessão (app.user é null), app.writeback responde NO_SESSION e nada é lido nem gravado. Veja Usuário.
  • A exportação renderiza o Widget. Na imagem e no PDF, o código roda como a pessoa que disparou a exportação, e o alerta renderiza uma vez por envio. Grave só em resposta a um clique, nunca no onMount nem no corpo do script. Veja Pense na exportação e no alerta.

Relacionado ​