Buscar K
Aparência
Aparência
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.
TABLE_NOT_FOUND. Veja Cadastros no BI.'[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.Todos devolvem uma Promise e lançam WritebackError quando falham.
| Método | Devolve | O 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 null | Um 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 ficou | Sem id em dados, cria. Com id, atualiza. Veja Gravar |
app.writeback.delete(tabela, id) | true | Exclui 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.
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âmetro | Formato | Padrão |
|---|---|---|
where | Objeto: 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 deles | sem ordem |
limit | Número de registros, no máximo 1000 | 100 |
offset | Quantos registros pular | 0 |
Operadores da lista: =, !=, >, <, >=, <=, between (com value2), like, not_like, is_null e not_null. Os dois últimos não levam value.
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.
// 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.
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.
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.
Todo erro é um WritebackError, com estes campos:
| Campo | O que é |
|---|---|
code | O código da tabela abaixo. Não muda entre versões |
message | Texto pronto para mostrar, no idioma de quem está vendo (pt-BR, en ou es) |
params | Detalhe do erro, como { action } em FORBIDDEN e { field } em UNKNOWN_FIELD |
fields | Só 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 |
code | Quando acontece | O que o Widget faz |
|---|---|---|
NO_SESSION | Não há usuário logado, como em link público | Mostra message e não oferece ação de gravar |
TABLE_NOT_FOUND | O nome não é um Cadastro desta Aplicação | Corrija o nome no código. params.table traz o nome pedido |
AMBIGUOUS_TABLE | Dois Cadastros da Aplicação têm o mesmo nome | Peça a quem monta a Aplicação para renomear um deles |
FORBIDDEN | A liberação da pessoa não permite a ação, ou o registro está fora das linhas dela | Esconde as ações e mostra message. params.action diz qual ação |
VALIDATION | Tipo errado, campo obrigatório vazio, valor repetido numa restrição de unicidade, chave externa que não existe ou regra do Cadastro | Mostra fields ao lado de cada campo. Sem fields (por exemplo, operador inválido em where), mostra message |
CONFLICT | Alguém gravou o registro depois da sua leitura | Recarrega a lista com find e avisa com message. A pessoa refaz a ação sobre o dado novo |
NOT_FOUND | O id não existe ou foi excluído | Recarrega a lista e mostra message |
UNKNOWN_FIELD | Campo inexistente ou escondido em where, orderBy ou save. params.field traz o nome | Corrija o nome no código |
LIMIT | O Cadastro chegou ao limite de registros | Mostra message. O limite é do contrato; veja Recursos, limites e cobrança |
NETWORK | Qualquer outra falha: conexão, erro interno do servidor | Mostra 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.
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.
<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.
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.app.user é null), app.writeback responde NO_SESSION e nada é lido nem gravado. Veja Usuário.onMount nem no corpo do script. Veja Pense na exportação e no alerta.