Conceitos centrais

O modelo mental por trás de tudo

O Archestack tem um pequeno número de conceitos centrais que aparecem em todo o produto. Cada um existe para resolver um problema que, de outra forma, resolveria à mão, várias vezes, de maneiras subtilmente diferentes. Passar aqui quinze minutos poupa-lhe horas de exploração. Cada secção abaixo apresenta um conceito, dá-lhe um exemplo prático e assinala os erros que os utilizadores novos cometem.

O Schema é a fonte de verdade

Tudo começa com o Schema, uma descrição JSON de cada tabela, coluna, relação e índice na sua aplicação. Não escreve SQL à mão. Desenha tabelas no Schema Designer visual e o Archestack converte isso em tabelas PostgreSQL reais quando faz deploy.

Porque existe

Um ERP tradicional é uma selva de tabelas que cresceu organicamente ao longo de uma década. O Archestack inverte o modelo: o schema é um artefacto versionado que edita explicitamente, tal como código-fonte. Todas as outras ferramentas da plataforma, Business Entities, Pages, Events, Scripts, leem a partir do schema. Se uma coluna não existir no schema, nenhuma outra ferramenta sabe dela.

Como funciona na prática

  • Auto-save vs deploy. Editar no Schema Designer guarda automaticamente as suas alterações (aparece um indicador no canto inferior esquerdo). Guardar não muda a base de dados, isso só acontece quando cria um deployment a partir de Database Deployments e clica em Deploy.
  • O Generated SQL é editável. O separador Generated SQL do deployment mostra o SQL de migração que a plataforma produziu, CREATE TABLE, ALTER TABLE, etc. Pode lê-lo antes de fazer deploy e pode editá-lo diretamente se a versão gerada automaticamente não estiver bem.
  • Hooks pre/post. Um deployment leva scripts de pre-deployment e post-deployment (PostgreSQL), úteis para preencher uma nova coluna NOT NULL ou reconstruir um índice numa ordem controlada.
  • O histórico é permanente. Cada deployment é registado com o seu estado (Draft / Executing / Succeeded / Failed) e o SQL que correu.

Exemplo prático

Imagine que adiciona uma coluna priority_score a deal. No Schema Designer isso significa selecionar a tabela, abrir o separador Columns, escrever o nome no campo "column_name", escolher INTEGER no menu Type e clicar em Add Column. O indicador de auto-save mostra brevemente "Auto saving…" e depois "Saved". A seguir, clique em Deploy na barra de ferramentas, vai parar a uma nova página de configuração de deployment. Abra o separador Generated SQL:

ALTER TABLE "deal" ADD COLUMN "priority_score" INTEGER;

Clique em Deploy no canto superior direito, pronto. A coluna existe agora. Enquanto não a adicionar também à Business Entity deal (próximo conceito), nenhuma página ou evento a verá. Essa separação é intencional: alterações de schema são baratas, expô-las é o próximo passo deliberado.

Armadilhas

  • Renomear uma coluna faz drop + recria. O gerador do plano não consegue ler a sua mente. Se renomear, edite o SQL no separador Generated SQL para usar RENAME COLUMN, ou faça um deployment em vários passos: adicionar a nova coluna, escrever um post-script para copiar dados, e largar a antiga num deployment seguinte.
  • Adicionar uma coluna NOT NULL a uma tabela não vazia vai falhar. Ou indica um default, ou adiciona-a como nullable, faz backfill, e só depois altera para NOT NULL num segundo deployment.
  • As chaves estrangeiras restringem o comportamento de delete. Use o menu On Delete em cada relação de forma deliberada, as opções são CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION.
  • Snake_case é a convenção. O campo "Table name" do Schema Designer até dá a dica ("Lowercase with underscores recommended"). As ferramentas também funcionam com PascalCase, mas todos os exemplos de código neste site assumem snake_case para nomes de tabela e coluna.

As Business Entities são a vista curada

Uma tabela em bruto (customer) raramente é o que um utilizador final quer ver. Ele quer "cliente com o total da sua última encomenda" ou "veículo com o nome e o telefone do mecânico atribuído". Uma Business Entity (BE) é uma vista curada de uma tabela de origem mais colunas juntadas a partir de tabelas relacionadas. As páginas são construídas sobre BEs, não sobre tabelas em bruto.

Porque existe

Duas razões. Primeiro, juntar tabelas em cada página seria repetitivo e inconsistente, páginas diferentes juntariam as mesmas tabelas de formas ligeiramente diferentes e o comportamento ia divergindo. A BE centraliza a lógica de junção. Segundo, muitas vezes quer várias "vistas" da mesma tabela de origem para públicos diferentes: a vista de vendas de customer mostra receita e último contacto; a vista de suporte mostra tickets e violações de SLA. Páginas diferentes referenciam BEs diferentes, todas suportadas pela mesma linha em customer.

Anatomia

  • Master Table - a tabela na qual a BE está ancorada. Cada BE tem exatamente uma. A chave primária da BE é a chave primária da master table.
  • Label Column - uma coluna cujo valor identifica um registo em listas e seletores (tipicamente name, title ou equivalente). Escolhida quando cria a BE.
  • Joins - adicionados a partir do botão Add Join. Cada join tem um tipo de junção (INNER / LEFT / RIGHT), uma tabela alvo, colunas de origem/alvo e uma lista de quais as colunas alvo a expor.
  • Colunas agregadas - ative Aggregate Mode num join e escolha depois uma função de agregação por coluna: COUNT, SUM, AVG, MIN, MAX, COUNT DISTINCT. "Número de contactos em aberto desta empresa" é uma coluna agregada típica.

Exemplo prático

Master table: vehicle. Colunas nativas expostas: vin, make, model, year. Join a customer via vehicle.owner_id → customer.id, expondo customer.full_name como owner_name. Segundo join em modo agregado: contagem de linhas de work_order em que vehicle_id coincide e status != 'Closed', exposto como open_work_order_count.

Uma página ligada a esta BE renderiza uma única linha numa grelha que parece um facto plano apesar de estar a ir buscar dados a três tabelas, e fá-lo de forma consistente em toda a plataforma.

Armadilhas

  • As BEs não são fronteiras de escrita por predefinição. Guardar a partir de um formulário ligado a uma BE escreve na master table. As colunas juntadas são geralmente só de leitura; para editar owner_name no exemplo acima, navegue até ao Customer.
  • Use Run Preview à vontade. O botão Run Preview no editor de BE executa a configuração de joins e mostra linhas reais. Se uma coluna de join vier vazia, é a sua relação ou o mapeamento de coluna que está mal configurado.
  • Não estique através de demasiados níveis. Uma BE que junta três tabelas em profundidade começa a ficar lenta em datasets grandes. Se se vir a juntar quatro ou cinco tabelas, é sinal de que queria um Script Module personalizado ou uma vista de base de dados.

As Pages são configuradas, não programadas

Uma Page é uma UI em runtime construída a partir de uma ou mais Business Entities. Configura-as no Page Editor definindo um nome, uma rota (por exemplo /companies), uma ligação a uma Business Entity e um toggle Published. A plataforma gera automaticamente secções a partir das colunas da BE; afina o layout a partir daí.

Anatomia

  • Separador Visual - o editor de layout WYSIWYG. Secções, campos, separadores, botões de ação.
  • Separador Overview - configuração de lista/grelha: que colunas aparecem, ordenação predefinida, filtros.
  • Separador Create - o formulário de novo registo. Muitas vezes difere ligeiramente do formulário de detalhe (menos campos, defaults diferentes).
  • Separador Entities - ligações adicionais a BEs (a página pode referenciar mais do que uma BE para separadores e grelhas relacionadas).
  • Separador Events - event triggers com âmbito de página.
  • Separador JSON - JSON em bruto para utilizadores avançados.

Tipos de widget de campo

Para cada campo no formulário de detalhe escolhe um Type num controlo Select. As opções reais:

  • Text - input de uma linha
  • Textarea - input multi-linha
  • Number - input numérico
  • Date - seletor de data
  • Select - menu pendente (use isto para campos de chave estrangeira e colunas do tipo enum; para campos FK, defina o autocomplete Entity para a BE referenciada para que os utilizadores escolham pela etiqueta)
  • Checkbox - booleano
  • Email - input de texto com validação de email

Para cada campo configura ainda Label, Placeholder, Required, Read Only e Span (largura em colunas da grelha).

Separadores e grelhas relacionadas

O formulário de detalhe de uma página suporta separadores. Use o botão Add Tab para adicionar um. Dentro de um separador pode adicionar uma secção RelatedGrid que mostra linhas de outra BE filtradas por um join (por exemplo, todas as linhas de contact em que company_id = id da empresa atual). É assim que constrói páginas master-detail "Company → Contacts / Deals" sem código nenhum.

Publicação

Cada página tem um Switch Published no cabeçalho do topo. OFF = rascunho (só você, dentro do editor, vê as suas alterações); ON = a página publicada aparece em Published Pages na barra lateral e é o que os utilizadores finais veem quando navegam para a sua rota.

Frontend Templates: quando "configure, don't code" não chega

Por vezes os templates configurados não chegam, quer um widget personalizado, um layout invulgar ou uma visualização específica. Os Frontend Templates permitem-lhe escrever um pedaço de TSX que fica disponível no editor de páginas como um componente reutilizável. São empacotáveis e vivem lado a lado com o resto da sua configuração. Ver Referência → Frontend Templates.

Armadilhas

  • Esqueceu-se de virar o Switch Published? A página não aparece na barra lateral e os utilizadores finais que naveguem para a sua rota veem um 404. É fácil escapar porque o editor não o impõe visivelmente.
  • A largura importa. Se a sua BE tem 25 colunas, a grelha vai ser inutilizável num portátil. Esconda colunas de baixo valor; continuam consultáveis, simplesmente não são apresentadas.
  • Separadores desencadeiam queries separadas. Uma página com cinco separadores faz queries adicionais quando abre uma linha de detalhe. Em geral não há problema, por vezes é uma preocupação de performance em datasets grandes.

Os Business Events fazem os dados reagir

Um Business Event observa uma Business Entity à procura de alterações e executa ações quando as suas condições são satisfeitas. São as regras "quando X acontece, faz Y" que transformam uma base de dados passiva numa aplicação funcional.

Timings de trigger

As opções reais quando cria um evento (pode marcar mais do que uma):

  • BeforeCreate - dispara antes de um novo registo ser guardado. O script pode mutar Entity e as alterações são persistidas.
  • BeforeUpdate - dispara antes de um registo existente ser guardado. Mesmo comportamento de mutação.
  • AfterCreate - dispara depois de um novo registo ter sido confirmado.
  • AfterUpdate - dispara depois de um registo existente ter sido confirmado.
  • BeforeDelete - dispara antes de um registo ser apagado.
  • InitialValue - dispara quando um formulário novo é aberto; define valores de campo predefinidos.
  • OnSchedule - disparado pelo Quartz num horário cron (ver Scheduled Events).
  • Manual - disparado explicitamente por um botão de ação do utilizador numa página.

Tipos de ação

Os tipos de ação reais (RuleActionKind):

  • ExecuteScript - corre um script C#. A ação mais flexível; é a que escolhe quando quer definir um campo, fazer um cálculo, chamar uma API, seja o que for à medida. O script recebe Entity (mute-o para alterar o registo), OldEntity, Log, Db, Modules.
  • Validate - corre um script C# que devolve bool. true significa que a validação falha e a gravação é bloqueada com a mensagem de erro configurada. Opcionalmente destaca colunas específicas.
  • BlockOperation - bloqueia a operação por completo com uma mensagem. Sem script.
  • CreateEntity - insere um registo noutra BE. Os valores dos campos suportam expressões de template.
  • UpdateEntity - atualiza registos noutra BE que correspondam a um filtro de condição.
  • DeleteEntity - apaga registos que correspondam a uma condição (recusa disparar sem uma, por segurança).
  • SendEmail, SendWebhook, PublishEvent - definidos no schema como melhorias futuras; atualmente são registados e ignorados.

Expressões de template

A configuração de ações aceita expressões de template em chavetas {{ … }}. Os tokens reais:

  • {{ Entity.column_name }} - campo do registo atual ({{ Entity.id }} também funciona)
  • {{ OldEntity.column_name }} - valor anterior (só em triggers de update)
  • {{ now() }} ou {{ getdate() }} - datetime UTC ISO 8601
  • {{ today() }} - string de data
  • {{ guid() }} ou {{ newid() }} - GUID novo
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}
  • Agregações dentro do contexto de um join: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Aritmética: {{ Entity.quantity * Entity.price }} - avaliada após substituição

Nota: não existe um token {{ user.email }}, os scripts e as expressões de template não têm acesso ao utilizador atual. Se precisar da identidade do utilizador numa regra, guarde-a no registo no momento da inserção via front-end e leia-a a partir daí.

Exemplos práticos

  • Normalizar um campo ao guardar. Trigger: Before Update em Deal. Sem condições. Ação: Execute Script com Entity.title = ((string)Entity.title)?.Trim(); para remover os espaços supérfluos. (Não precisa de uma regra para created_at / updated_at / created_by / updated_by, a plataforma carimba-os automaticamente.)
  • Impedir que deals de baixo valor sejam marcados como Won. Trigger: Before Update em Deal. Condição: stage = 'Won' AND amount < 100. Ação: Block Operation com a mensagem "Deals abaixo de €100 não podem ser marcados Won, registe-os como Lost ou apague-os."
  • Criar uma Note quando um Customer é sinalizado. Trigger: After Update em Customer. Condição: flagged = true. Ação: Create Entity em note com title = "Customer flagged at {{ now() }}", body = "Auto-generated for {{ Entity.full_name }}".

Simule antes de guardar

O editor de trigger tem um separador Simulate com um botão Run Simulation. Escolha um registo real e a plataforma corre as condições e as ações contra ele sem persistir alterações, as operações de escrita executam dentro de uma transação que é revertida. O painel de output mostra que condições foram satisfeitas e o que cada ação teria feito. Use-o antes de ativar um trigger em dados de produção.

Armadilhas

  • Triggers recursivos. Um trigger After Update que usa Update Entity para atualizar o mesmo registo vai disparar-se a si próprio. Use antes Before Update + Execute Script + Entity.field = …, isso muta a escrita em curso em vez de iniciar uma nova.
  • A ordem importa entre triggers. Vários triggers no mesmo evento disparam por ordem de prioridade. Se o comportamento depender da ordem, defina prioridades explicitamente.
  • Eventos desativados continuam a aparecer na lista. O Switch Enabled é independente da configuração do trigger, verifique se está ligado antes de testar.

Os Script Modules dão-lhe vias de escape

Para qualquer coisa que não possa ser expressa como uma simples condição + ação, calcular um desconto complexo, chamar uma API externa, gerar um slug, fazer uma operação de base de dados em vários passos, escreve-se um Script Module: um pequeno pedaço de C# (compilado em runtime pelo Roslyn) que pode chamar a partir de um Business Event, de um Scheduled Event ou do front-end.

A que têm acesso os scripts

Cada script recebe estes globais (não existem outros nomes no scope do script):

  • Entity - o registo atual (em contextos de trigger) como dynamic. Aceda aos campos com Entity.column_name. Mutar Entity num trigger Before* é a forma de "definir um campo".
  • OldEntity - o registo anterior (triggers de update e delete). Só de leitura.
  • Log - um ILogger para diagnóstico. Log.LogInformation("…") escreve nos logs do servidor e na entrada do Event Log.
  • Db - o helper de base de dados. Ver abaixo.
  • Modules - chamar outros Script Modules: await Modules.CallAsync("OtherModule", new Dictionary<string, object?> { ["param"] = value }).
  • Pdf - renderizar um PDF template guardado pelo nome: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) devolve o byte[] renderizado. Pdf.GenerateBase64Async(...) devolve o mesmo payload codificado em base64.

Não existe global User, Http ou Email.

Como é um script

// Parameters declared in the UI: customer_id (int)
var customer = await Db.GetAsync("customer", customer_id);
if (customer == null) return new { ok = false, error = "Customer not found" };

var orders = await Db.From("sales_order")
    .Where("customer_id", "=", customer_id)
    .Where("status", "=", "Closed")
    .ToListAsync();

decimal totalRevenue = 0;
foreach (var o in orders) totalRevenue += (decimal)(o.total ?? 0);

return new { ok = true, customer = customer.name, revenue = totalRevenue };

A maioria dos scripts tem 5-30 linhas. Não precisa de expertise em .NET, basta-lhe controlo de fluxo básico em C# e o helper Db. O IntelliSense está ligado e, depois de atribuir a partir de Db.GetAsync("customer", …), o editor sabe as colunas dessa variável e oferece completions.

Quando recorrer a um script

  • Cálculos que abrangem várias tabelas (o valor vitalício de um cliente, o total do histórico de reparações de um veículo).
  • Chamar APIs externas (geocodificação, fornecedores de pagamento, gateways de SMS).
  • Operações em massa desencadeadas por um Scheduled Event (recálculo noturno, resumo semanal).
  • Devolver dados ao front-end que são demasiado à medida para uma Business Entity.
  • Tudo aquilo em que de outra forma encadearia seis Business Events, geralmente um sinal de que queria um script.

Armadilhas

  • O helper Db é assíncrono. Faça sempre await. Esquecer-se compila mas devolve um Task, não os dados.
  • Não existe FirstOrDefaultAsync nem SumAsync. Os terminais são ToListAsync(), FirstAsync(), CountAsync(). Para uma soma, vá buscar as linhas e some em C#.
  • Where recebe três argumentos - coluna, operador, valor. Os operadores incluem =, !=, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL.
  • Update é uma chamada de topo: await Db.UpdateAsync("table", id, new { field = value }), não é encadeada a partir de um Where.
  • Os scripts de trial correm na mesma sandbox que os de produção. Não assuma que "é só um trial" significa limites mais frouxos, o seu script consegue na mesma martelar o Postgres.

Os Scheduled Events correm a horas certas

Um Scheduled Event é o mesmo sistema de Event Trigger com o timing OnSchedule, uma expressão cron do Quartz dispara a regra de forma recorrente em vez de em resposta a uma alteração de dados. Use-o para resumos noturnos, atualizações diárias de dados, limpezas semanais, relatórios mensais.

Formato cron

Cron de 6 ou 7 campos no estilo Quartz (second minute hour day-of-month month day-of-week [year]). Alguns padrões comuns:

  • 0 0 3 * * ? - todos os dias às 03:00.
  • 0 0 9 ? * MON-FRI - dias úteis às 09:00.
  • 0 */15 * * * ? - a cada 15 minutos.
  • 0 0 0 1 * ? - primeiro de cada mês à meia-noite.

As horas são hora do servidor (UTC no stack de produção). A página Scheduled Events mostra a próxima hora de disparo para que possa confirmar que faz sentido antes de guardar.

Armadilhas

  • Jobs longos seguram o seu slot. Se um job demora 25 minutos e o cron é a cada 15 minutos, a próxima execução é saltada, o Quartz não dispara duas instâncias do mesmo job em simultâneo.
  • Execuções falhadas não voltam a tentar. Ficam registadas em Event Logs com a exceção. Adicione lógica de retry explícita ao seu script se precisar.

Os Packages agrupam configuração para promoção

Depois de construir algo, um conjunto de tabelas, BEs, páginas, eventos e scripts, vai querer movê-lo do seu trial para um ambiente real, ou partilhá-lo com outro parceiro. Um Package é um ZIP de objetos de configuração selecionados (com cascata: ao trazer uma página, traz automaticamente a sua BE, que traz as suas tabelas) que pode exportar e importar.

A cascata, explicada

Adiciona os objetos de topo que lhe interessam (geralmente páginas). O construtor do package percorre o grafo de dependências e adiciona tudo o que esses objetos precisam: BEs às quais as páginas se ligam, tabelas que as BEs referenciam, Frontend Templates que as páginas embebem, Script Modules que os eventos chamam. Vê a lista completa em cascata antes de exportar e pode desmarcar qualquer coisa que queira omitir.

Enviar dados-semente

Os packages podem opcionalmente incluir dados de linha, útil para enviar tabelas de lookup predefinidas (países, moedas, enums de estado) ou dados de demonstração. Marque Include data na exportação. Na importação, as linhas são fundidas (upsert) por chave primária.

Armadilhas

  • Deltas de schema não fazem auto-deploy na importação. Se o ambiente de importação tiver tabelas em falta, a importação falha. Faça deploy do schema primeiro, depois importe o package.
  • Os identificadores são por nome, não por ID. Uma BE chamada "customer" no ambiente de origem liga-se a uma BE chamada "customer" no destino, não ao mesmo ID numérico. Renomear de qualquer um dos lados quebra a ligação.

As Business Units controlam quem vê o quê

Para setups multi-tenant ou multi-equipa, as Business Units são grupos do Keycloak que podem ser atribuídos a recursos específicos. Um utilizador na Business Unit "Garagem A" só consegue ver Pages, BEs e Triggers atribuídos à Garagem A.

Como funciona

Cada utilizador pertence a uma ou mais BUs (geridas em User Management e Business Units). O front-end acompanha a BU ativa do utilizador e define o cabeçalho HTTP X-Business-Unit em cada pedido. O backend usa esse cabeçalho para filtrar os endpoints de listagem, só voltam os recursos atribuídos à BU ativa.

Quando usar (e quando não)

  • Use quando equipas ou clientes diferentes precisam da sua própria fatia da mesma plataforma, os mecânicos da Garagem A não devem ver as work orders da Garagem B.
  • Não use como sistema de permissões. As BUs filtram a visibilidade, não a autorização. As roles (admin / owner / editor / user) tratam de quem está autorizado a fazer o quê.
  • Não use para segurança ao nível da linha dentro de um único tenant. Isso é trabalho para filtros na Business Entity, não para uma BU.

Para onde isto segue a seguir

Com estes conceitos na mão, o resto da documentação segue naturalmente. O tutorial first-app usa todos os conceitos desta página. A Referência vai um nível mais fundo em cada ferramenta, o que clicar, o que ter em atenção, quando recorrer.