Referência

Cada ferramenta, num só sítio

Esta página é a referência prática para cada ferramenta da barra lateral do seu ambiente Archestack. Cada secção cobre o que a ferramenta faz, quando recorrer a ela, os passos para os fluxos de trabalho mais comuns e as armadilhas que apanham os utilizadores novos. A secção final (APIs invocáveis a partir de scripts) documenta a superfície exata que os scripts podem chamar.

Schema Designer

Um editor visual para o schema da base de dados, tabelas, colunas, tipos, relações, índices, que guarda automaticamente num documento JSON. O schema é a fonte da verdade para a sua aplicação; nenhuma outra ferramenta vê uma coluna enquanto não a adicionar aqui e fizer deploy.

Fluxo comum: na barra de ferramentas, clique em Add Table (botão de ícone). Abre-se um diálogo intitulado "Create New Table"; escreva um nome (o texto de ajuda lembra "Lowercase with underscores recommended"), clique em Create. A tabela aparece no canvas com uma chave primária SERIAL id gerada automaticamente. Selecione a tabela e o painel à direita abre no separador Columns. Adicione colunas a partir da caixa com contorno tracejado "Add column" em baixo (campo de nome, menu Type, clique em Add Column). Clique em cada coluna para expandir e ajustar os toggles (PK, NULL, UQ), Length e Default value.

Tipos de coluna disponíveis no menu Type: SERIAL, BIGSERIAL, INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, REAL, DOUBLE PRECISION, VARCHAR, CHAR, TEXT, BOOLEAN, DATE, TIME, TIMESTAMP, TIMESTAMPTZ, UUID, JSON, JSONB, BYTEA, INET, CIDR, MACADDR, MONEY, INTERVAL, mais tipos geométricos e de array. Length só é relevante para VARCHAR e CHAR.

As chaves estrangeiras vivem no separador Relations do painel à direita (não "Relationships"). Desça até "Add relationship": escolha a coluna FK na tabela atual, o tipo de relação (One-to-One / One-to-Many / Many-to-Many), a tabela de referência, a coluna de referência e clique em Add Relationship. Cada relação tem menus pendentes On Delete e On Update (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION).

Auto-save: o indicador no canto inferior esquerdo alterna entre "Auto saving…" e "Saved". Há também um botão Save na barra de ferramentas (só ativado quando há alterações pendentes) para guardar explicitamente.

Outros botões da barra de ferramentas: Add Group (contentores visuais para tabelas relacionadas, painel de propriedades intitulado "Group"), Add Text (anotações em forma livre, painel de propriedades intitulado "Text Annotation"), controlos de zoom, filtro de package e pesquisa de tabela.

Armadilhas: renomear uma coluna produz um plano destrutivo (drop + add), edite o SQL no separador Generated SQL do deployment para usar RENAME COLUMN se quiser preservar os dados. Adicionar uma coluna NOT NULL a uma tabela não vazia sem default falha, torne-a nullable, faça backfill, e só depois altere. O botão Deploy da barra de ferramentas leva-o a uma página de configuração de deployment, não diretamente para um deploy.

Database Deployments

Lista cada deployment com o seu estado (Draft / Executing / Succeeded / Failed) e corre novos. Rota: /database-deployments/overview.

Botões no canto superior direito: Refresh (recarrega a lista), New Deployment (cria um novo rascunho).

A partir do Schema Designer: o botão verde Deploy com ícone de foguetão na barra de ferramentas do Schema Designer leva-o a /database-deployments/configure/new, uma página de configuração de deployment nova.

Página de configuração de deployment: dois separadores.

  • Configuration - editores de scripts pre/post-deployment (PostgreSQL) com um botão Test Run em cada. O pre-script corre antes das alterações de schema; o post-script corre depois. Útil para backfills, reconstruções de índices ou limpezas de áreas de staging. Mais um toggle Force Deploy (Allow Data Loss) para operações destrutivas.
  • Generated SQL - mostra a migração que a plataforma produziu. O SQL é editável. Um botão Regenerate (ícone de refresh) corre o diff outra vez se tiver feito alterações de schema desde que abriu esta página.

Botões no topo: Back, Save (guarda a configuração como Draft), Deploy (corre o deployment imediatamente). O Deploy é o último que clica.

Armadilhas: os deployments não são automaticamente envolvidos numa transação, uma falha a meio deixa a base de dados num estado parcial. Para migrações arriscadas, envolva os seus statements em BEGIN; … COMMIT; no pre-script. O toggle Force Deploy salta as verificações de segurança; use-o de forma deliberada.

Business Entities

Vistas curadas construídas sobre uma master table mais joins de tabelas relacionadas. As páginas ligam-se a Business Entities, nunca a tabelas em bruto. Ver Conceitos → Business Entities para perceber o porquê.

Fluxo comum: clique em Create. Abre-se o editor em página inteira com os separadores Visual, JSON, Events. Defina Entity Name, Master Table (Autocomplete) e Label Column (a coluna cujo valor representa um registo em seletores). Guarde, a BE gera automaticamente uma lista de colunas nativas a partir da master table.

Adicionar joins: desça até ao cartão Join Configurator e clique em Add Join. Cada join abre como um Accordion com: Join Type (INNER / LEFT / RIGHT), From Table, To Table (Autocomplete agrupado por Database Tables / Third Party), From Column, To Column, menus opcionais Type Cast, e um seletor de colunas para indicar que colunas alvo expor.

Agregações: num join, ative Aggregate Mode. Para cada coluna escolhida escolhe depois uma Aggregate Function: COUNT / SUM / AVG / MIN / MAX / COUNT DISTINCT. "Número de contactos em aberto desta empresa" é o exemplo canónico.

Run Preview: o editor tem um botão Run Preview (com um ícone de play) que executa a configuração de joins completa e mostra linhas reais numa grelha de dados. Se uma coluna de join vier vazia, a coluna não foi marcada no seletor de colunas do join (ou a relação está mal configurada).

Várias BEs por master table: nada o impede de ter customer (sales view) e customer (support view) sobre a mesma master. Páginas diferentes, colunas adequadas ao público, a mesma linha subjacente.

Armadilhas: as colunas juntadas e agregadas são só de leitura. Para editar a etiqueta juntada, navegue até ao registo de origem. As colunas agregadas são recalculadas em cada query, ok para centenas de linhas, lento em milhões.

Page Editor

Configura a UI em runtime ligando uma Page a uma Business Entity. A plataforma gera automaticamente secções com base nas colunas da BE; afina a disposição a partir daí. Não há um "seletor de templates" separado, cada página parte da mesma baseline gerada e personaliza-a.

Fluxo comum: clique em Create. Preencha Page Name, Page Route (por exemplo /companies), escolha a Business Entity (Autocomplete). A página abre no editor com os separadores Visual, Overview, Create, Entities, Events, JSON. Ajuste a disposição gerada automaticamente, depois vire o interruptor Published no cabeçalho do topo para ON.

Tipos de widget de campo (o Select Type em cada campo): Text, Textarea, Number, Date, Select, Checkbox, Email. Use Select para campos de chave estrangeira e defina o autocomplete Entity do campo para a BE referenciada para que os utilizadores escolham por etiqueta.

Separadores no formulário de detalhe: clique em Add Tab no separador Visual. Dentro de um separador pode adicionar uma secção RelatedGrid ligada a outra BE filtrada pela PK do registo atual. Exemplo clássico: uma página Company com separadores "Contacts" e "Deals", cada um filtrado por company_id = id da empresa atual.

Botões de ação no Header inspector do formulário de detalhe. Clique em Add na secção Actions. Cada ação tem Label, Icon (Save / Delete / Add / Refresh / Download / Bolt / None), Variant (Contained / Outlined / Text), Color (Primary / Secondary / Error / Success / Warning / Info) e Steps (uma lista ordenada de: Save Record, Delete Record, Navigate, Business Events, Custom). Para correr um Business Event a partir de um botão, defina o tipo do passo como Business Events e escolha o evento pelo nome.

Publicação: o interruptor Published no cabeçalho do topo é o único controlo de publicação. ON = a página aparece em Published Pages na barra lateral e os utilizadores finais que naveguem para a sua rota veem-na. OFF = rascunho (só você no editor vê as suas alterações).

Armadilhas: as colunas geradas automaticamente numa Page nova espelham a BE, se a BE mudar (adiciona um join), a Page não vai apanhar as novas colunas. Recarregue a página no editor ou adicione a coluna manualmente à secção. Atualmente não existe um tipo de passo de ação "Generate PDF", para ligar um PDF a um botão de página, exponha o PDF via um Script Module que chama o endpoint REST e use um passo Custom (ou chame o endpoint diretamente a partir de um Frontend Template).

Business Events

Regras que disparam em alterações de dados (ou num horário, ou manualmente) e correm ações quando as condições são satisfeitas. O mecanismo "quando X acontece, faz Y" da plataforma. Ver Conceitos → Business Events para o modelo.

Fluxo comum: clique em Create. Defina Rule Name, ative Enabled. Escolha Business Entity. Marque um ou mais Triggers: BeforeCreate / BeforeUpdate / AfterCreate / AfterUpdate / BeforeDelete / OnSchedule / Manual / InitialValue. Construa a árvore de condições (grupos AND/OR). Adicione uma ou mais ações no FlowCanvas. Guarde.

Tipos de ação (RuleActionKind):

  • Execute Script - corre um script C#. A ação mais flexível; recorra a ela sempre que quiser definir um campo, fazer um cálculo, chamar uma API, qualquer coisa personalizada. O script recebe Entity (mute-o em triggers Before* para alterar a escrita em curso), OldEntity, Log, Db, Modules.
  • Validate - um script 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 via errorColumns.
  • Block Operation - bloqueia duramente a operação com uma mensagem configurada. Sem script.
  • Create Entity - insere um registo noutra BE. Os valores dos campos suportam expressões de template como {{ Entity.column_name }} e {{ now() }}.
  • Update Entity - atualiza registos noutra BE que correspondam a um filtro de condição. Mesmas expressões de template.
  • Delete Entity - apaga registos que correspondam a uma condição. Recusa disparar sem uma condição (segurança).
  • Send Email / Send Webhook / Publish Event - definidos no schema como melhorias futuras; atualmente são registados e ignorados.

Padrão "definir um campo": não existe uma ação discreta Set Field. A forma de definir um campo é um Execute Script num trigger Before* que muta Entity. Exemplo: Entity.title = ((string)Entity.title)?.Trim();. A plataforma persiste o Entity modificado como parte da escrita em curso, sem query extra, sem risco de recursão.

Run Simulation: o separador Simulate do editor de trigger tem um seletor de registo e um botão Run Simulation. A plataforma corre as suas condições e ações contra o registo escolhido sem persistir alterações: as escritas são verificadas e registadas, mas nunca executadas, incluindo as de um script através de Db e Files, por isso nada chega à base de dados nem aos ficheiros do workspace e também não dispara nenhum outro Business Event. O email e as notificações não são enviados. Tudo o resto que um script faz por si mesmo é real: chamadas HTTP, System.IO e tudo o que um pacote NuGet faz, como uma ligação a outra base de dados. O painel de output mostra que condições corresponderam e o que cada ação teria feito. Use isto para apanhar configurações erradas antes de dados ao vivo lhes baterem.

Expressões de template na configuração de ações usam chavetas {{ … }}. Os tokens reais: {{ Entity.column }}, {{ OldEntity.column }}, {{ now() }} / {{ getdate() }}, {{ today() }}, {{ guid() }} / {{ newid() }}, {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}, agregações como {{ SUM(column) }} dentro de contextos de join, e aritmética pós-substituição como {{ Entity.quantity * Entity.price }}. Não existe um token {{ user.email }}.

Operadores de condição: Equals, NotEquals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, Like (= ILIKE), Contains (= ILIKE %value%), IsNull, IsNotNull, In (= ANY(...)).

Armadilhas: um trigger que muta o mesmo registo que vigia volta a disparar-se se usar Update Entity; use antes Before Update + Execute Script + Entity.field = …. Dois triggers no mesmo evento disparam por ordem de prioridade. Eventos desativados continuam a aparecer na lista, o toggle Enabled é separado da configuração do trigger.

Script Modules

Scripts C# reutilizáveis compilados em runtime pelo Roslyn. Recebem parâmetros, consultam a base de dados via o helper Db, devolvem um valor. Invocáveis a partir de Business Events, Scheduled Events e diretamente do front-end.

Fluxo comum: clique em Create. Abre-se o editor com os separadores Edit, Parameters, Test, JSON. Dê um nome ao módulo (PascalCase, por exemplo RecalcOpenRevenue). No separador Parameters, clique em Add por parâmetro (Name, Type, Required, Default Value). Opções de Type: string, int, decimal, double, bool, DateTime. De volta ao separador Edit, escreva C#. O IntelliSense está ligado.

Painel Test: o separador Test mostra os inputs de parâmetros à esquerda e um botão Run (ícone PlayArrow). Clique em Run; o lado direito mostra um indicador de sucesso ou erro, log de output e o valor de retorno (formatado em JSON).

O helper Db, ver a referência completa em APIs invocáveis a partir de scripts abaixo.

Chamar a partir de um Business Event: adicione uma ação Execute Script cujo corpo chame o módulo:

// The module is a method; its name and parameters are checked when the script compiles
await Modules.RecalcCompanyOpenDealValue(company_id: (int)Entity.company_id);

// Calling it by name still works
await Modules.CallAsync("RecalcCompanyOpenDealValue", new Dictionary<string, object?> {
    ["company_id"] = Entity.company_id
});

Chamar a partir de um Scheduled Event: crie um evento com o trigger On Schedule, defina o cron, adicione uma ação Execute Script com o mesmo corpo Modules.CallAsync.

Logging: cada invocação escreve uma entrada em Event Logs com tempo de execução, parâmetros e resultado (ou erro).

Armadilhas: faça sempre await nas chamadas Db, esquecer compila mas devolve um Task. Os returns passam por objeto (não são serializados em JSON pela framework); para consumo no front-end, devolva objetos anónimos com campos primitivos. Suggestion-on-commit-character está intencionalmente desativado no editor; use Tab para aceitar.

Shared Code

Código de que mais do que um script ou página precisa, guardado em ficheiros próprios. O nome de um ficheiro é o seu caminho, como Sales/OrderSummary.cs ou components/StatusChip.tsx, e a sua extensão diz o que ele é: .cs é C# que todos os scripts podem usar, enquanto .tsx, .ts, .jsx e .js são módulos que as páginas, os Frontend Templates e os blocos Code dos layouts importam. Os ficheiros partilhados pertencem a packages como qualquer outro artefacto.

Fluxo comum: na barra lateral, em Logic, abra Shared Code e clique em New file. Escreva o caminho; o editor começa o ficheiro com um exemplo do seu tipo. Escreva o código e clique em Save.

Os ficheiros C# são C# comum: namespaces, records, classes e helpers estáticos. Todos os scripts (Script Modules, Business Events, Scheduled Events, data scripts de PDF e testes) podem usar os seus tipos públicos. Um tipo sem namespace está disponível pelo seu nome; um tipo num namespace precisa de um using no script, ou de um global using em qualquer ficheiro partilhado, que passa então a aplicar-se a todos os scripts. Um ficheiro partilhado vê o modelo de dados tipado (Order, WorkspaceDb), mas não os globais de um script, por isso um helper que precise de Db ou de Log recebe-o como argumento.

// Shared code: Sales/OrderSummary.cs, ordinary C# every script can use
namespace Sales;

/// <summary>One order as the invoices and the dashboard report it.</summary>
public record OrderSummary(int Id, string Number, decimal Total);

public static class Vat
{
    public static decimal Add(decimal amount, decimal rate = 0.21m) =>
        Math.Round(amount * (1 + rate), 2);
}
// A script module using it
using Sales;

var open = await Db.Order.Where(o => o.Status == "Open").ToListAsync();
return open.Select(o => new OrderSummary(o.Id, o.Number, Vat.Add(o.Total ?? 0))).ToList();

Uma alteração guardada chega a todos os scripts na sua próxima execução. O editor verifica um ficheiro partilhado em conjunto com os outros ficheiros partilhados, e Go to Definition num tipo partilhado, dentro de um script, abre o ficheiro que o declara. Quando o código partilhado não compila, os scripts que o usam falham e indicam o ficheiro que está errado; os scripts que não o usam continuam a correr. Uma alteração de tabela que partiria um ficheiro partilhado é assinalada, com os scripts que parte, antes de o deployment correr.

Os módulos são importados de @/ seguido do seu caminho sem a extensão: components/StatusChip.tsx é import { StatusChip } from "@/components/StatusChip". Uma página também pode ter ficheiros próprios ao lado das suas vistas, adicionados com + por cima do seu editor de código e importados como ./OrderRow. Um módulo vê os mesmos globais que o código de páginas vê e exporta o que os outros usam. Uma vista que importa pode, em vez de terminar com return, exportar o seu componente como default. O código sem nenhum import nem export corre exatamente como sempre correu.

// Shared code: components/StatusChip.tsx, a module pages import
export function StatusChip({ status }: { status: string }) {
  const color = status === "Open" ? "warning" : status === "Paid" ? "success" : "default";
  return <Chip size="small" color={color} label={status} />;
}
// A page's overview, with a file of its own, OrderRow.tsx, beside it
import { StatusChip } from "@/components/StatusChip";
import { OrderRow } from "./OrderRow";

export default function Orders() {
  const [rows, setRows] = useState([]);
  useEffect(() => {
    backend.getBusinessEntityData("Order").then((r) => setRows(r.rows.items));
  }, []);
  return (
    <Stack spacing={1}>
      {rows.map((row) => (
        <OrderRow key={row.id} row={row}>
          <StatusChip status={row.orders_status} />
        </OrderRow>
      ))}
    </Stack>
  );
}

Os pacotes que o código pode importar são os que a aplicação já traz, e um import é o mesmo objeto que os globais da página contêm: react, @mui/material, @mui/x-date-pickers, dayjs, lucide-react, three, @react-three/fiber e @react-three/drei. Mais nenhum pacote está disponível.

Publicação: a aplicação publicada lê os módulos partilhados tal como a release atual os fixou, por isso uma edição chega aos utilizadores finais com a próxima publicação, tal como acontece com uma página. Um visitante sem sessão iniciada recebe apenas os módulos que as páginas públicas e os layouts públicos importam.

Armadilhas: o caminho é a identidade de um ficheiro em todos os espaços de trabalho, e os imports indicam-no, por isso renomear um ficheiro parte o que o importa até esses imports acompanharem a mudança. Os caminhos usam letras, dígitos, pontos, underscores e hífenes. O namespace Archestack está reservado. Um alias num global using (global using Money = decimal;) não é passado aos scripts.

NuGet Packages

Bibliotecas do NuGet para os scripts C# do workspace: um driver para outra base de dados, um leitor de CSV ou de Excel, um cliente para um serviço. Todos os scripts as podem usar (Script Modules, scripts de Business Events, Scheduled Events, data scripts de PDF, scripts de teste e verificações de código), tal como os ficheiros .cs partilhados. O editor de script e o VS Code completam automaticamente os seus tipos. Um workspace que não adicione nenhuma corre os seus scripts exatamente como antes.

Fluxo comum: na barra lateral, em Logic, abra NuGet Packages e clique em Add NuGet package. Pesquise o pacote NuGet, ou escreva o seu id exato quando a origem não permite pesquisas, e escolha uma versão exata: a estável mais recente vem pré-selecionada, e Include prerelease versions lista as outras. Em Belongs to, escolha aquilo a que pertence: um dos packages do workspace, ou Workspace only. Add descarrega-o primeiro no servidor, com as suas dependências. Quando isso falha, o diálogo diz porquê e nada é guardado; quando funciona, mostra os namespaces que ele traz.

Num package: um pacote NuGet que pertence a um package viaja com ele, na sua exportação e naquilo de que um pipeline faz deploy, e instalar um package lista, antes de alguma coisa mudar, os pacotes NuGet que ele acrescenta ou cuja versão sobe. Num repositório ficam em packages/<package>/nuget.json, um ficheiro escrito apenas quando o package declara algum, e o VS Code verifica-o enquanto escreve. Um push que altera o ficheiro descarrega-os primeiro, e é recusado, sem mudar nada, quando não podem ser restaurados.

{
  "packages": [
    { "id": "CsvHelper", "version": "33.0.1" },
    { "id": "Dapper", "version": "2.1.35" },
    { "id": "MySqlConnector", "version": "2.4.0" }
  ]
}

Usar um: um pacote NuGet traz tipos, nunca um global, e nada é importado automaticamente. Escreva um using para o seu namespace, que muitas vezes não é o id: a coluna Namespaces da página mostra os namespaces de cada pacote NuGet como linhas using para copiar. Devolva valores simples de um script, não os objetos da própria biblioteca.

// NuGet package: CsvHelper. Reads a price list an admin uploaded on the Files page.
using System.Globalization;
using System.IO;
using CsvHelper;

var text = await Files.ReadTextAsync("imports/prices.csv");
var csv = new CsvReader(new StringReader(text), CultureInfo.InvariantCulture);
var rows = new List<object>();
try
{
    csv.Read();
    csv.ReadHeader();
    while (csv.Read())
        rows.Add(new { Sku = csv.GetField("sku"), Price = csv.GetField<decimal>("price") });
}
finally
{
    csv.Dispose();
}

// Plain rows, not CsvHelper objects.
return rows;
// NuGet packages: MySqlConnector and Dapper. A script module that lists the open invoices in
// the old ERP's MySQL database. The connection string is a file an admin uploads on the Files
// page of each environment: a password written in a script travels to git, package exports
// and every environment.
using Dapper;
using MySqlConnector;

var connectionString = await Files.ReadTextAsync("config/erp-connection.txt");
var connection = new MySqlConnection(connectionString.Trim());
try
{
    await connection.OpenAsync();
    var invoices = await connection.QueryAsync(
        "SELECT number, customer_number, amount FROM invoices WHERE paid = 0 ORDER BY number");

    // Plain values, not driver objects.
    return invoices
        .Select(i => new { Number = (string)i.number, Customer = (string)i.customer_number, Amount = (decimal)i.amount })
        .ToList();
}
finally
{
    // A top-level using var does not parse in a script, so the connection is closed here.
    await connection.DisposeAsync();
}

Versões: uma declaração indica uma única versão exata, como 2.1.35, nunca um intervalo. Quando vários packages pedem versões diferentes do mesmo pacote NuGet, todos os scripts do workspace usam a mais alta. A coluna In use mostra que versão é essa e quem a pediu.

Que cópia corre: um workspace carrega a sua própria cópia de cada pacote NuGet, separada de todos os outros workspaces do servidor, também quando o servidor traz a mesma biblioteca para uso próprio. O próprio .NET e as bibliotecas que os scripts já têm, como System.Text.Json, logging, EF Core e Npgsql, continuam a ser a cópia do servidor. Um pacote NuGet que peça uma versão mais recente de uma delas recebe um aviso: corre sobre a cópia do servidor, e um método que só a versão mais recente tem falha quando é chamado.

Restauros: o primeiro script que precisa dos pacotes NuGet num servidor faz com que esse servidor os descarregue. Espera até cinco segundos e, quando descarregá-los demora mais, é-lhe pedido que corra outra vez daqui a pouco. Depois de uma alteração, o workspace restaura o novo conjunto. Até isso funcionar, os scripts mantêm o último conjunto que foi restaurado, por isso um restauro falhado nunca tira pacotes NuGet que funcionavam. O banner da página mostra o estado e os erros de um restauro falhado explicados. Depois de uma falha, o servidor tenta outra vez da primeira vez que um script precisa dos pacotes NuGet depois da hora que o banner indica, ou de imediato com Restore now. Resolved lista todos os pacotes NuGet que os scripts usam, incluindo as dependências.

Simulações e testes: uma simulação (o separador Test, Run Simulation) só retém o que passa pelo Archestack: as escritas através de Db e Files são simuladas e não são guardadas, e o email e as notificações não são enviados. Uma execução de testes faz as suas escritas através de Db na sua sandbox e retém o resto da mesma forma. Tudo o resto que o script faz por si mesmo é real: chamadas HTTP, System.IO e tudo o que um pacote NuGet faz, como uma ligação a outra base de dados, por isso uma escrita feita através de um driver acontece em cada execução. Aponte um script destes para uma base de dados de teste enquanto o experimenta.

Armadilhas:

  • Os pacotes NuGet precisam do acesso a dados tipado, tal como o C# partilhado. Num servidor que o desliga (Scripting:TypedData:Enabled), a página di-lo e não permite adicionar nenhum.
  • #r "nuget: Dapper, 2.1.35", a forma como outros executores de scripts carregam um, não funciona aqui. Adicione-o na página.
  • Uma biblioteca nativa que um pacote NuGet traga é carregada uma vez por processo do servidor, é partilhada pelos workspaces que usam o mesmo ficheiro e fica carregada até o servidor reiniciar.
  • Os pacotes NuGet Workspace only não são exportados com um package, não estão no repositório e os pipelines não fazem deploy deles. Dê um pacote NuGet de que uma aplicação precisa ao package dessa aplicação.
  • Um using var de topo dá um erro de sintaxe num script, por isso feche uma ligação você mesmo, num finally, como faz o exemplo.
  • Um valor lido de Entity é dynamic, e um método de extensão como o QueryAsync do Dapper não o pode receber como argumento: o script não compila (CS1973). Faça primeiro o cast: (string)Entity.customers_number.
  • Os pacotes NuGet que são ferramentas, templates ou MSBuild SDKs são recusados, tal como um que não tenha nada contra o qual os scripts possam compilar em .NET 8, ou um que estenda o próprio driver PostgreSQL do servidor ou o EF Core. Os ids chamados OmniCore ou Archestack, ou que comecem por um deles seguido de um ponto, pertencem à plataforma.

Para operadores: os pacotes NuGet estão ligados por omissão e sempre desligados num trial. As definições ficam em Scripting:NuGet, ou em variáveis de ambiente como Scripting__NuGet__Enabled. Cada servidor descarrega por si mesmo, por isso cada um precisa de chegar ao nuget.org ou aos feeds configurados aqui, através do proxy em https_proxy, http_proxy e no_proxy quando existe um. Dê a todos os servidores de uma instalação as mesmas origens.

DefiniçãoPor omissãoO que faz
EnabledtrueDesligado, nada é transferido e os scripts não carregam nenhum. A página não pode adicionar um; as declarações que uma importação ou um push trazem são mantidas, sem uso.
Sourcesnuget.orgDe onde vêm os pacotes NuGet, separados por ;: feeds pelo seu índice de serviço v3 (o URL que termina em index.json), ou pastas absolutas, de onde se copia e que são de confiança: os limites de download aplicam-se apenas aos feeds. O nuget.org só é usado quando nem esta definição nem Feeds indicam uma origem.
Feeds:<name>:Url, Username, PasswordNenhumUm feed com nome, para um que precise de credenciais. O URL nunca leva um nome de utilizador nem uma palavra-passe.
Feeds:<name>:PatternsNenhumOs ids que este feed serve, separados por ; (Contoso.*). Quando algum feed tem padrões, um id vem do feed cujo padrão lhe corresponde, e um feed sem padrões serve todo o resto.
Feeds:<name>:AllowInsecureConnectionsfalseDeixa passar um feed em http:// simples.
DirectoryUma pasta nos dados locais da aplicaçãoOnde este servidor guarda os pacotes NuGet que descarregou: uma pasta por servidor no seu disco local, nunca um volume que os servidores partilhem. Num contentor, dê-lhe um volume próprio desse servidor, ou cada contentor novo descarrega-os outra vez.
MaxPackageBytes, MaxExtractedBytes100 MB, 1 GBO maior pacote NuGet que um restauro descarrega, e o máximo que um pacote NuGet pode ocupar depois de descompactado.
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages500 MB, 2 GB, 150O máximo que um restauro descarrega, descompacta e conta em pacotes NuGet, tudo junto.
MaxCacheBytes10 GBAcima deste valor a pasta não aceita novos restauros, e os pacotes NuGet que este servidor não está a usar são removidos, começando pelos usados há mais tempo.
MinFreeDiskBytes1 GBCom menos espaço livre em disco do que isto, um restauro não começa, e um download que deixaria menos é parado.
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds300, 120, 5000Quanto tempo um restauro pode correr, quanto tempo adicionar um pacote NuGet espera pelo seu download, e quanto tempo um script espera pelo download deste servidor antes de lhe ser pedido que corra outra vez.
AllowNativeAssetstrueDesligado, um pacote NuGet que traga bibliotecas nativas é recusado, e nenhuma é alguma vez carregada.
AuditRefuseLevelVaziolow, moderate, high ou critical: um pacote NuGet com uma vulnerabilidade conhecida desse nível ou acima é recusado. Vazio não recusa nenhum, e as vulnerabilidades aparecem como avisos; qualquer outro valor faz falhar todos os restauros que adicionam um pacote NuGet.
CertificateRevocationModeonlineonline ou offline: como é verificada a revogação dos certificados de assinatura dos pacotes NuGet.

Frontend Templates

Fragmentos de UI reutilizáveis escritos em TSX que o editor de páginas pode largar numa página. Útil quando os templates configurados não chegam, gráficos à medida, disposições invulgares, integrações de widgets de terceiros.

Fluxo comum: clique em Create, dê um nome ao template, escreva um componente TSX no editor. O componente recebe props: record (a linha atual quando embutido num formulário de detalhe), refresh (refazer fetch dos dados) e alguns helpers. Guarde. Referencie o template a partir de um campo do Page Editor.

Empacotável: os templates fluem pelo sistema de Package como qualquer outro objeto de configuração, são trazidos em cascata quando uma página que os usa é adicionada a um package.

Armadilhas: o TSX não pode importar pacotes npm arbitrários: importa código partilhado de @/ e os poucos pacotes que a aplicação traz.

PDF Templates

HTML + template Scriban + um pequeno data script C# que renderiza para PDF (Chromium headless). Cada template é invocável pelo nome a partir de um script via REST ou diretamente a partir de um botão numa Page (com um pequeno adaptador, ver abaixo).

Fluxo comum: clique em + Create. O editor abre com campos de texto Name e Description no topo, cinco separadores do lado esquerdo (HTML Template, Data Script, Settings, Params, JSON), e um painel PDF Preview sempre ligado à direita. Os separadores Data Script e HTML Template abrem cada um um editor Monaco na metade esquerda; a pré-visualização re-renderiza quando guarda (botão verde Update no canto superior direito).

Declare parâmetros no separador Params via o botão + Add (Name, Type, Required, Default Value). A etiqueta do separador atualiza para Params (n) com a contagem. Formato de página / orientação / margens / escala / cabeçalho / rodapé vivem no separador Settings.

Forma do data script (ver tutorial PDF Invoices para um real):

var deal = await Db.GetAsync("deal", deal_id);
var lines = await Db.From("deal_line")
    .Where("deal_id", "=", deal_id).ToListAsync();

return new {
    InvoiceNumber = $"INV-{deal.id:D6}",
    Lines = lines.Select(l => new { l.description, l.quantity })
};

Forma do template HTML (Scriban, semelhante a Liquid/Handlebars):

<h1>{{ InvoiceNumber }}</h1>
<table>
  {{ for line in Lines }}
  <tr><td>{{ line.description }}</td><td>{{ line.quantity }}</td></tr>
  {{ end }}
</table>

Codificação HTML: cada valor {{ }} é codificado em HTML, por isso um valor de registo que contenha <b> aparece como o texto <b>. html.raw é o opt-out explícito, só para HTML de confiança: {{ notes | html.raw }}. Nunca o use em valores que os utilizadores finais possam escrever, porque isso coloca o HTML deles no documento renderizado. Só o template pode desativar a codificação, os dados não. html.escape e html.newline_to_br continuam a funcionar (newline_to_br escapa o texto mas mantém as tags <br />).

Painel de pré-visualização: sempre visível do lado direito do editor. Re-renderiza quando guarda (botão verde Update). Inclui uma coluna de thumbnails para PDFs multi-página e uma barra de ferramentas de visualizador integrada (zoom, rotação, download, imprimir). Os botões de ícone no canto superior direito do editor alternam a visibilidade do painel de pré-visualização e a edição em ecrã inteiro.

Chamá-lo a partir de fora do editor:

  • POST /api/v1/pdf-templates/{id}/preview - renderiza por ID, devolve application/pdf. Usado pelo painel de pré-visualização do editor.
  • POST /api/v1/pdf-templates/generate/{name} - renderiza por nome, devolve application/pdf.
  • POST /api/v1/pdf-templates/generate/{name}/base64 - igual mas devolve { "data": "<base64>" }.

Passe os valores dos parâmetros no body JSON. Auth é o token Bearer padrão.

Limitação de trial: tanto o endpoint generate como o endpoint {id}/preview (usado pelo painel de pré-visualização do editor) devolvem 403 em modo trial, a renderização de PDF está totalmente bloqueada. Pode continuar a autorar o template (data script, HTML, definições, parâmetros) e guardá-lo; só não verá um PDF renderizado até estar num ambiente pago.

Nota sobre renderização invocável por script: o contexto de scripting C# expõe sim um global Pdf, portanto um Script Module renderiza um template guardado diretamente com await Pdf.GenerateAsync("invoice", ...) ou Pdf.GenerateBase64Async(...). Não é preciso nenhuma ida e volta de HttpClient ao endpoint generate. (O autocomplete do editor de script ainda anuncia Pdf.RenderAsync e um objeto Templates; nenhum dos dois nomes existe em runtime.)

Armadilhas: Scriban escapa HTML por defeito. Quebras de página são por CSS, page-break-before: always numa secção. As fontes disponíveis no servidor de renderização estão limitadas a fontes do sistema mais as famílias Noto e Liberation, embuta a sua própria via base64 @font-face se precisar de uma fonte de marca.

Imagens, fontes e folhas de estilo: o renderizador só carrega recursos de endereços http(s) públicos, mais URLs data: e blob:. Endereços internos e de rede privada (localhost, 10.x, 192.168.x, nomes de serviços de contentores, etc.) são bloqueados, por isso uma imagem servida por um serviço interno não aparece no PDF. Use um URL público ou um URL data:.

Scheduled Events

O mesmo sistema de Event Trigger que Business Events, com o trigger On Schedule marcado. Use-o para recálculos noturnos, resumos semanais, limpezas periódicas, relatórios mensais.

Fluxo comum: crie um Business Event com o trigger On Schedule (em vez de, ou em adição aos, triggers de alteração de dados). Configure a expressão cron. Adicione uma ação Execute Script cujo corpo invoque um Script Module via Modules.CallAsync(...).

Expressões cron: 6 ou 7 campos no estilo Quartz, ver Conceitos → Scheduled Events para padrões comuns. As horas são hora do servidor (UTC no stack de produção).

Armadilhas: um job de longa duração que ultrapassa o seu intervalo cron não duplica, o Quartz não dispara uma segunda instância do mesmo job em simultâneo, o segundo tick é saltado. Execuções falhadas não tentam de novo automaticamente; construa o retry no script.

Packages

Bundles exportáveis de objetos de configuração (BEs, páginas, eventos, scripts, templates, schema) com dependências em cascata. Adicionar uma Page a um package traz automaticamente tudo o que essa página referencia.

Fluxo comum (export): clique em Create, nomeie e versione o package. Adicione os objetos de topo que lhe interessam, geralmente um punhado de páginas. Abra o painel de linked-items para ver a lista em cascata (o que o package vai realmente conter). Desmarque qualquer coisa que queira omitir. Decida se quer incluir dados (toggle na exportação). Clique em Export, obtém um ZIP.

Fluxo comum (import): no ambiente de destino, abra Packages, clique em Import, faça upload do ZIP. A plataforma mostra o que vai ser adicionado ou atualizado. Deltas de schema não são auto-aplicados, se o package referencia uma coluna que não existe no destino, a importação falha.

Armadilhas: os identificadores são por nome, não por ID numérico. Uma BE chamada "customer" na origem liga-se a uma BE chamada "customer" no destino, renomeie de qualquer lado e a ligação parte.

No git: a extensão do VS Code traz um package para uma pasta como um ficheiro por artefacto, e envia a pasta de volta, para que um package possa ser revisto e versionado como qualquer outro código.

Third-Party Data Connections

Ligações a bases de dados externas (Postgres, SQL Server, MySQL, outras) que importam tabelas num horário. As tabelas importadas comportam-se como tabelas nativas, aparecem no Schema Designer e podem ser referenciadas por Business Entities, Pages e Scripts.

Fluxo comum: crie uma ligação (connection string, test, save). Liste as tabelas remotas disponíveis; escolha quais importar. Para cada tabela importada, defina o modo de sincronização (Full / Delta) e o trigger de sincronização (cron / manual / intervalo fixo).

Espelhados, não federados: os dados importados vivem na sua própria omnicore-db. As leituras são rápidas (Postgres local). Compromisso: desatualização entre sincronizações.

Colunas geridas pela app: marque algumas colunas como "geridas pelo Archestack", não serão sobrescritas por sincronizações futuras. Útil quando quer anotar dados de fornecedor com as suas próprias flags de estado.

Armadilhas: a primeira sincronização de uma tabela grande pode demorar. As sincronizações Delta exigem uma coluna que o conector possa usar como watermark, tipicamente um timestamp modified_at.

AI Assistant

Um assistente de chat integrado na aplicação que tanto responde a perguntas sobre os seus dados como altera o seu ambiente. Pergunte "quantas ordens de trabalho abertas por filial este mês?" e ele consulta e responde. Peça "adiciona uma coluna de estado às marcações de serviço e uma página para as listar" e, se for administrador, ele constrói.

Traga o seu próprio modelo. O Archestack não inclui qualquer LLM nem paga inferência. Um administrador adiciona um fornecedor no ecrã AI Configuration: uma base URL, um nome de modelo e uma chave de API. Funciona tudo o que fale a API chat-completions da OpenAI (ChatGPT, DeepSeek, OpenRouter, Groq, Together, um Ollama ou vLLM self-hosted), e a API nativa da Anthropic é suportada diretamente. Pode configurar vários e escolher um por conversa.

Tudo o que faz passa pelo MCP Server. O assistente não tem uma porta das traseiras própria para a plataforma, chama as mesmas ferramentas que qualquer cliente MCP externo, com a sua identidade. Os limites que definir lá também se aplicam a ele, e cada ação fica no mesmo registo de auditoria. Ver MCP Server abaixo.

Fluxo comum: abra o assistente a partir da barra lateral ou da bolha flutuante. Pergunte em uma ou duas frases. Ele lê o seu schema atual antes de propor o que quer que seja, diz o que vai alterar e espera pela sua confirmação. Cada chamada a uma ferramenta aparece por baixo da resposta, para ver o que ele realmente fez.

Funciona bem para: perguntas pontuais sobre os seus dados, funcionalidades em forma de CRUD, automações simples, extensões a entidades existentes.

Menos bem para: regras de negócio não óbvias e fluxos de trabalho complexos em vários passos. Trate o que ele constrói como o PR de um júnior.

Armadilhas: trabalha sobre dados em produção e não há undo, por isso faça um backup antes de algo arriscado. Os seus prompts e o que as ferramentas devolvem são enviados para o fornecedor que configurou, sob os termos dele; aponte-o a um modelo self-hosted se isso importar. Conversas longas só repetem os turnos mais recentes, por isso um chat deixado aberto o dia todo não se lembra do início. E repare nos dois interruptores: com o MCP Server parado o assistente ainda fala, mas não consegue ler nem alterar nada.

MCP Server

Cada ambiente pode expor-se através do Model Context Protocol, para que um cliente de IA fora do Archestack, Claude Desktop, Claude Code, ChatGPT deep research, um IDE ou um script seu, possa trabalhar diretamente com ele. É também sobre isto que corre o AI Assistant integrado.

Desligado por omissão, e as ferramentas que alteram a forma como a aplicação é construída continuam desligadas mesmo depois de o iniciar. É deliberado.

Três grupos de ferramentas. Read cobre listar e descrever entidades, consultar linhas e ler o schema, páginas, eventos, script modules, packages, translations e event logs. Write é criar, atualizar e apagar registos, e segue o mesmo caminho da interface, por isso os seus Business Events disparam exatamente como para uma pessoa que carrega em Guardar. Admin é o grupo que permite a um modelo construir a própria aplicação: entidades, schema, páginas, eventos, script modules, translations, packages e os pacotes NuGet que os scripts usam.

Quatro portões decidem se uma chamada passa: o servidor está a correr, o grupo é permitido, aquela ferramenta específica está ativa, e o token traz o scope. Uma ferramenta bloqueada por qualquer um deles é invisível para o cliente, em vez de recusada depois.

Tokens. Emita um por cliente no ecrã MCP Server, com os scopes de que realmente precisa e uma validade opcional. O segredo é mostrado uma única vez, na criação, e só é guardado um hash, por isso um token perdido roda-se em vez de se recuperar. Dê a cada integração o seu próprio token: mantém o registo de atividade legível, e revogar um não perturba os outros.

Registo de atividade. Cada invocação é registada, incluindo as recusadas e porquê. É aqui que verifica o que uma integração tem realmente andado a fazer.

Armadilhas: um token com o scope admin é tratado como administrador completo por tudo o resto, por isso distribua-os com parcimónia. "Allow token in URL" existe para clientes que não conseguem enviar um cabeçalho Authorization, mas um URL acaba em logs e capturas de ecrã muito mais facilmente do que um cabeçalho; deixe desligado a menos que precise. E num ambiente de produção com dados reais, deixe o grupo admin desligado.

Object Browser

Vista em bruto de cada tabela nativa e importada, adicionar, editar, apagar linhas diretamente. Inclui uma importação CSV para carregamento em massa.

Quando recorrer:

  • Carregar em massa dados de referência antes das páginas existirem (países, moedas, enums de estado).
  • Depuração, ver o que está realmente numa tabela quando uma página se comporta mal.
  • Edições rápidas de administração a dados ainda não expostos via uma Page.
  • Importação CSV: escolha uma tabela, faça upload de um ficheiro, mapeie as colunas aos campos, pré-visualize, confirme.

Armadilhas: o Object Browser dispara Business Events (passa pelo mesmo caminho de escrita do EntityService que o front-end). A exceção é SQL direto via Database Deployments, isso contorna tudo.

Dependency Graph

Um mapa só de leitura de como os objetos de configuração no seu ambiente se referenciam uns aos outros. Oito tipos de recurso aparecem como nós, coloridos por tipo: Business Entities, Pages, Event Triggers, Script Modules, PDF Templates, Frontend Templates, Scheduled Events e Packages. Uma aresta entre dois nós significa que um recurso depende do outro. Recorra a ele para responder às perguntas "o que se partiria se eu apagar isto?" e "há algo sem uso aqui?".

O que significa uma aresta: uma Page ligada a uma Business Entity, um Event Trigger registado numa Business Entity, um passo de ação de Page que dispara um Event Trigger, um script que chama outro Script Module via Modules.CallAsync("Name"), um script que renderiza um PDF Template, um Scheduled Event que dispara um Event Trigger, ou um Package que contém um recurso. É uma vista de design-time, reflete o que a sua configuração liga em conjunto, não o tráfego de runtime ao vivo.

Dois tipos de problema são feitos para saltar à vista:

  • Loose ends (órfãos) - nós que nada referencia e que não referenciam nada. São reunidos numa banda "Loose ends" com borda vermelha no fundo do canvas. Um Script Module que ninguém chama; uma Page ligada a nada.
  • Dangling references - desenhadas como arestas tracejadas vermelhas. A referência existe na configuração mas o seu alvo não pode ser encontrado, por exemplo um passo de ação de Page que ainda aponta para um Event Trigger apagado, ou um script que chama um Script Module por um nome que já não existe.

Fluxo comum: abra Dependency Graph a partir da secção Tools da sidebar. O painel esquerdo tem uma pesquisa por nome, um seletor Layout (Left to right, Top to bottom, By type, Force-directed), os interruptores Group by package, Show loose ends e Show edges, e uma checklist por tipo. Clique num nó para entrar em modo de foco: esse nó e os seus vizinhos diretos permanecem brilhantes enquanto o resto do grafo escurece, para que possa traçar exatamente o que um recurso toca. Clique no fundo do canvas para limpar o foco. Cada nó traz um pequeno botão de abrir em novo separador que salta diretamente para o editor desse recurso. A linha de estatísticas no topo reporta o número de nós, arestas, órfãos e dangling references.

Group by package: envolve os membros de cada package numa caixa tracejada etiquetada, para que possa ver um package como uma unidade e detetar os recursos que ele puxou por cascata.

Armadilhas: o grafo é construído a pedido a partir do estado atual, use o botão de refresh depois de alterar recursos noutro lado. As arestas derivadas de scripts são encontradas ao varrer os corpos de script à procura da forma de chamada literal Modules.CallAsync("Name"), um module invocado através de um nome calculado não mostrará uma aresta. Uma dangling reference vale sempre a pena corrigir; um órfão muitas vezes não, um Script Module acabado de criar conta como órfão até que algo o chame.

Event Logs

O rasto de auditoria de tudo o que a plataforma correu, Business Events, Script Modules, Scheduled Events, sincronizações de terceiros.

Fluxo comum: abra Event Logs, defina Status como Failed, clique numa entrada. O painel de detalhe mostra o payload (estado do registo na altura, parâmetros passados), a exceção com stack trace e o timing.

Logs e Charts: a página tem dois separadores que partilham uma mesma linha de filtros. Logs é a tabela acima. Charts é um painel operacional sobre o mesmo filtro: o volume ao longo do tempo empilhado por estado, uma tendência da taxa de sucesso, uma repartição por source, os events que mais falham e os mais lentos, a duração média e máxima ao longo do tempo, uma repartição por tipo de trigger, e um mapa de calor de dia da semana por hora do dia, com os totais principais no topo. Ambos os separadores partilham um intervalo de datas De / Até (com as predefinições Últimas 1h / 24h / 7d); o separador Charts escolhe automaticamente um intervalo de tempo adequado ao período e pode atualizar-se em intervalos para monitorização em direto.

Notificações em tempo real: o ícone de sino na barra superior da aplicação admin mostra falhas não lidas via SignalR.

Armadilhas: o log pode crescer muito. As entradas antigas ficam guardadas para sempre por defeito; se o disco for uma preocupação, configure um Scheduled Event que poda entradas mais antigas que N dias.

User Management

Criar, convidar e gerir utilizadores; atribuir roles do realm. Envolve a API REST de administração do Keycloak.

Roles:

  • admin - tudo; pode gerir todas as Business Units, utilizadores, roles, definições de sistema.
  • owner - gere as suas próprias Business Units, pode editar páginas de administração e de runtime.
  • editor - pode ver e editar páginas de runtime, mas não alterar configuração de schema/BE/eventos.
  • user - só-de-leitura em páginas de runtime.

Armadilhas: despromover um utilizador de admin a meio da sessão não o expulsa, a alteração aplica-se no próximo refresh de token (tipicamente dentro de um minuto).

Business Units

Grupos suportados pelo Keycloak que filtram a visibilidade em recursos individuais. Ver Conceitos → Business Units para o modelo.

Fluxo comum: crie uma BU. Convide membros por email e escolha a role. Atribua recursos indo a cada BE / Page / Trigger ou via o painel de linked-items da BU.

Atribuições em cascata: quando atribui uma Page a uma BU, a atribuição desce em cascata para a BE a que a página se liga.

Armadilhas: a BU ativa de um utilizador é definida no seletor de BU da barra superior e persiste em localStorage. Os utilizadores novos partem da sua primeira BU; se uma página de listagem está suspeitosamente vazia, verifique qual BU está ativa.

Database Backups

pg_dump noturno das três bases de dados, mais um ZIP dos ficheiros do espaço de trabalho. Disparo manual / upload / restauro a partir da UI.

Fluxo comum: Trigger now → Download → Upload → Restore. O horário predefinido é noturno às 03:00 UTC.

Limitação de trial: os Backups estão desativados no ambiente de trial.

Branding

Alterar o nome de apresentação da aplicação, logótipo (claro + escuro), cores, tipos de letra e o resto do tema Material UI, aplicado em runtime, sem rebuild.

Fluxo comum: defina o nome da app, faça upload de um logótipo claro e de um logótipo escuro (PNG/JPG/SVG, ≤ 10 MB), escolha as cores da marca no separador Theme. Guarde: a sua sessão mostra-o de imediato, as dos outros no próximo carregamento da página.

Tema: o separador Theme é um editor completo de temas Material UI com pré-visualização ao vivo em modo claro e escuro: as cores da marca e cada cor da paleta por modo, os tipos de letra (qualquer Google Font) e cada estilo de texto, o raio dos cantos, o espaçamento, as sombras, a velocidade das animações, o estilo de botões, cartões e campos, os valores por omissão dos componentes e os seus overrides em JSON. Verifica o contraste, parte de predefinições, importa um tema de outra app Material UI e exporta este como código. Show on the whole app experimenta um rascunho em todos os ecrãs antes de guardar. Um workspace que nunca muda o seu tema mantém exatamente o aspeto incorporado. Num repositório, o tema é a chave theme de workspace/branding.json.

Armadilhas: o logótipo é também o ícone do separador do navegador e o ícone que um telemóvel mostra para a aplicação no ecrã principal. O ecrã principal precisa de um logótipo PNG, JPG ou WebP; com um SVG mostra o ícone do Archestack.

Analytics

Google Analytics na aplicação publicada, configurado na página Analytics (Configuration). O builder nunca é rastreado.

Fluxo habitual: cole o ID de medição GA4 (G-XXXXXXXXXX, em Administrador, Streams de dados no Google Analytics), marque Track visits to the published app e guarde. Ask visitors for consent first está ligado por omissão: a etiqueta do Google só carrega depois de um visitante aceitar o banner, como exigem as regras de cookies da UE. O texto do banner, os botões e uma ligação para a sua política de privacidade podem ser os seus.

Eventos a partir do código da página: assim que a etiqueta carregar, o código da página pode enviar eventos próprios, como window.gtag?.("event", "sign_up"). Nada é enviado enquanto o rastreio estiver desligado ou o visitante tiver recusado.

Translations

Editor chave/valor para as strings de UI da aplicação. Adicione novas localidades, sobreponha etiquetas predefinidas por cliente.

Fluxo comum: escolha uma localidade, pesquise uma chave, escreva um valor novo, guarde. Recarregue a aplicação admin.

Armadilhas: as traduções são carregadas uma vez por sessão; utilizadores numa aba antiga não veem alterações até refrescarem. Algumas etiquetas (cabeçalhos de coluna das BEs) vêm da configuração da BE, não das traduções.


APIs invocáveis a partir de scripts

A superfície exata que os scripts podem chamar. Verificada contra o código-fonte, todos os métodos abaixo existem mesmo.

Globais

Cada script recebe estas variáveis de topo (não existem outros nomes no scope do script):

  • Entity - ExpandoObject dynamic. Registo atual. Aceda a campos com Entity.column_name. Mutar Entity num trigger Before* persiste as alterações como parte da escrita em curso.
  • OldEntity - ExpandoObject dynamic. Registo anterior (triggers de update + delete). Só-de-leitura.
  • Log - ILogger. Log.LogInformation("…"), Log.LogWarning(…), Log.LogError(…). Escreve nos logs do servidor e na entrada do Event Log.
  • Db - DbHelper. Acesso à base de dados: um conjunto tipado por Business Entity, e os helpers não tipados (ver abaixo).
  • Modules - chamar outros Script Modules. Cada módulo é um método com os seus próprios parâmetros: await Modules.OrdersOf(customer: "Acme"), verificado quando o script é compilado; o resultado é dynamic. await Modules.CallAsync("OrdersOf", new Dictionary<string, object?> { ["customer"] = "Acme" }) continua a chamar um módulo pelo nome e devolve Task<object?>.
  • Pdf - ScriptPdfHelper. Renderizar um PDF template guardado pelo nome: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) devolve byte[]; await Pdf.GenerateBase64Async(…) devolve o mesmo payload codificado em base64. Os parâmetros são reencaminhados para o data script do template como locais tipados em string.
  • Ocr - ScriptOcrHelper. Lê o texto impresso de uma imagem, no próprio processo, sem serviço externo e sem API key: await Ocr.ExtractAsync(byte[] image) devolve um OcrExtraction. await Ocr.ExtractBase64Async(string) aceita a mesma imagem codificada em base64 (um prefixo data:image/png;base64, é tolerado), await Ocr.ExtractTextAsync(byte[]) devolve apenas o texto.
  • Doc - ScriptDocHelper. Encontra o documento numa foto e aplana-o: await Doc.CropAsync(byte[] image) devolve um DocumentCrop. await Doc.CropBase64Async(string) aceita uma captura em base64.
  • Files - ScriptFilesHelper. A pasta de ficheiros própria do workspace, a que a página Files gere: await Files.WriteAsync("invoices/1042.pdf", bytes), await Files.ReadAsync(path), ListAsync(folder), MoveAsync, DeleteAsync. Ver Files.

Não existe global User, Http, Email, Templates, ou qualquer outro.

Db - métodos de linha única + de escrita

Todos assíncronos, todos no objeto de topo Db:

  • await Db.GetAsync(string table, object id) - obter uma linha por chave primária. Devolve dynamic? (ou null se não encontrar).
  • await Db.CreateAsync(string table, object values) - inserir. values pode ser um objeto anónimo ou um Dictionary<string, object?>. Dispara triggers Before/After Create. Devolve int (o novo ID).
  • await Db.UpdateAsync(string table, object id, object values) - atualizar por PK. Dispara triggers Before/After Update.
  • await Db.DeleteAsync(string table, object id) - apagar por PK. Dispara trigger Before Delete.

Db - construtor fluente de queries

Comece com Db.From(table). Encadeie filtros, depois um terminal:

  • .Where(string column, string op, object? value = null) - adicionar uma cláusula WHERE. Operadores: =, != / <>, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL. Encadeie várias chamadas .Where(...) para AND.
  • .OrderBy(string column, bool desc = false) - definir ORDER BY. Apenas um permitido por query.
  • .Limit(int limit) - máximo de linhas. Predefinição é 1000.

Terminais (cada um corre a query):

  • await ...ToListAsync() - devolve List<dynamic>.
  • await ...FirstAsync() - devolve dynamic? (primeira correspondência ou null). Temporariamente define o limite para 1.
  • await ...CountAsync() - devolve int.

Não existe SumAsync, MaxAsync, FirstOrDefaultAsync ou SingleAsync em Db.From. Para agregações, vá buscar com ToListAsync() e reduza em C#, ou use os conjuntos tipados abaixo, que os têm todos.

Exemplo trabalhado

// Get a row by ID, query a list, calculate a sum, update.
var order = await Db.GetAsync("orders", 42);
if (order == null) return false;

var items = await Db.From("order_items")
    .Where("order_id", "=", order.id)
    .OrderBy("id")
    .ToListAsync();

decimal totalAmount = 0;
foreach (var item in items) {
    totalAmount += (decimal)(item.amount ?? 0);
}

await Db.UpdateAsync("orders", 42, new {
    total = totalAmount,
    updated_at = DateTime.UtcNow
});

return true;

Db - Business Entities como conjuntos tipados

Cada Business Entity é também um conjunto tipado em Db, com o nome da entidade sem os espaços: uma entidade chamada Service Order é Db.ServiceOrder. Por baixo é o EF Core, por isso todo o LINQ funciona (Where, Include, SumAsync, GroupBy, projeções), com uma navegação para cada chave estrangeira e para cada join de Business Entity. As colunas são propriedades em PascalCase com os seus tipos reais: planned_date é PlannedDate, um DateOnly.

// Every business entity is a typed set on Db, named after the entity.
var since = new DateOnly(2026, 1, 1);
var open = await Db.Order
    .Where(o => o.Status == "Open" && o.OrderDate >= since)
    .Include(o => o.OrderItems)
    .OrderBy(o => o.OrderDate)
    .ToListAsync();

foreach (var order in open)
    order.Total = order.OrderItems.Sum(i => i.Amount ?? 0);

var customer = await Db.Customer.SingleAsync(c => c.Email == "ada@example.com");
Db.Order.Add(new Order { Customer = customer, Status = "Open" });

// Writes every changed, added and removed row; business events fire as usual.
await Db.SaveChangesAsync();
return open.Count;
  • Nomes. Uma tabela sobre a qual não está construída nenhuma Business Entity recebe um conjunto com o nome da tabela (bank_account é Db.BankAccount). Uma coluna de chave estrangeira sem o seu sufixo _id é a navegação: customer_id é Customer, e o outro lado é o plural, Customer.Orders.
  • Gravação. await Db.SaveChangesAsync() escreve cada linha adicionada, alterada ou removida tal como o fazem CreateAsync, UpdateAsync e DeleteAsync, por isso os Business Events disparam e as colunas de auditoria são carimbadas, e uma linha alterada envia apenas as colunas que mudaram. Não é uma única transação: as linhas guardadas antes de uma falha continuam guardadas. ExecuteUpdate, ExecuteDelete e escritas por SQL direto são recusados.
  • Tipos. Um timestamp with time zone é lido como um DateTime em UTC, e um valor comparado com ele também tem de estar em UTC: DateTime.UtcNow. As colunas de um tipo que o modelo não mapeia (arrays, enums, tipos geométricos) ficam de fora; leia-as com Db.From.
  • Sempre atualizado. O modelo é reconstruído sempre que uma tabela ou uma Business Entity muda, por isso uma coluna removida é um erro de compilação na sua linha em vez de uma falha em runtime. Um deployment pendente lista, antes de o correr, os scripts tipados que deixariam de compilar.

Db.From e os métodos acima continuam a funcionar ao lado dos conjuntos tipados, e o editor de script e o VS Code completam automaticamente os conjuntos, as colunas e as navegações.

Modo de simulação

Durante a simulação de trigger (o botão Run Simulation num Business Event) e no separador Test de um Script Module, Db.SimulationMode é true. As escritas (CreateAsync, UpdateAsync, DeleteAsync e SaveChangesAsync) verificam a tabela, a linha e os valores, registam o que teriam feito e não escrevem nada, por isso também não dispara nenhum Business Event. As leituras veem os dados reais. As escritas, movimentos e eliminações através de Files também são simulados, e o email e as notificações são registados, não enviados. É isto que torna Run Simulation seguro para usar contra dados reais. Tudo o resto que o script faz por si mesmo é real: chamadas HTTP, System.IO e tudo o que um pacote NuGet faz, como uma ligação a outra base de dados.

Modules

  • await Modules.OrdersOf(customer: "Acme") - cada Script Module é um método com o nome do módulo sem espaços ("Orders of" é Modules.OrdersOf), que recebe os parâmetros que declara com os seus tipos, primeiro os obrigatórios; um parâmetro opcional omitido recebe o seu valor por omissão. Um nome mal escrito ou um valor do tipo errado é um erro de compilação na sua linha. Devolve Task<dynamic?>, por isso o resultado lê-se sem cast, incluindo um tipo anónimo. Os argumentos lidos de Entity são dynamic: faça o cast ((int)Entity.company_id) para que a chamada seja verificada na compilação.
  • await Modules.CallAsync(string moduleName, Dictionary<string, object?>? parameters = null) - chamar outro Script Module pelo nome. Os parâmetros tornam-se variáveis tipadas no script do módulo chamado. Devolve Task<object?> (o que quer que o módulo tenha devolvido).

Files - a pasta do workspace

Cada workspace tem no servidor uma pasta própria, gerida na página Files (Operations). Os carregamentos chegam lá, tal como tudo o que um script guarda: uma fatura gerada, uma exportação, um modelo treinado. Nada disso é guardado numa base de dados, e nenhum caminho chega à pasta de outro workspace. Os caminhos são relativos e usam barras normais; um caminho com .., uma unidade ou uma ligação é recusado.

// A generated invoice, kept in the workspace's own folder.
var pdf = await Pdf.GenerateAsync("Invoice", new Dictionary<string, string> { ["id"] = Entity.orders_id.ToString() });
var path = $"invoices/{DateTime.UtcNow:yyyy}/{Entity.orders_number}.pdf";
await Files.WriteAsync(path, pdf);

// Read it back, or anything an admin uploaded on the Files page.
var rates = await Files.ReadTextAsync("config/vat-rates.json");
foreach (var file in await Files.ListAsync("invoices", recursive: true))
    Log.LogInformation("{Path} is {Size} bytes", file.Path, file.Size);

return path;
  • ReadAsync(path) devolve os bytes e lança uma exceção quando o ficheiro não existe; ReadTextAsync lê texto UTF-8; ReadDataUriAsync devolve data:image/png;base64,… para uma imagem num PDF Template ou num email.
  • WriteAsync(path, bytes) e WriteTextAsync criam as pastas e substituem um ficheiro existente. Uma escrita conclui-se por inteiro ou não acontece.
  • ListAsync(folder, recursive) lista primeiro as pastas; cada entrada tem Path, Name, IsFolder, Size, ModifiedUtc e ContentType.
  • MoveAsync(from, to) nunca substitui; DeleteAsync(path) apaga um ficheiro, ou uma pasta com tudo o que contém.
  • PathOf(path) é o caminho absoluto no disco, para uma biblioteca que precise de um.

Numa execução de ensaio (o botão Test) e numa execução de testes, escritas, movimentos e eliminações nunca chegam à pasta: a execução guarda-os para si, por isso ler um ficheiro acabado de escrever funciona, e lista-os com o que registou. A pasta .archestack contém os ficheiros próprios da plataforma (logótipos, ícones de pacotes, anexos de email): os scripts podem lê-la, nunca escrevê-la. Uma cópia de segurança inclui a pasta como um arquivo workspace-files ao lado dos dumps das bases de dados.

Ocr e Doc - ler uma foto

Ambos correm dentro do processo da API. Não há serviço de OCR para instalar, nem chave para configurar, nem custo por página.

OcrExtraction (o que Ocr devolve):

  • Text - cada linha reconhecida junta por newlines, por ordem de leitura.
  • Lines - por linha Text, Confidence e uma bounding box: X, Y, Width, Height, mais Right e Bottom. Píxeis da imagem de origem, origem no canto superior esquerdo.
  • Confidence - média das confianças por linha, 0 a 1. ElapsedMs, IsEmpty.

Use as caixas. Ler um documento é localizar uma etiqueta e depois ler o que está ao lado, coisa que o texto sozinho não exprime: encontre a linha que contém "Total" e depois pegue na linha cujo intervalo Y a sobrepõe e cujo X ultrapassa o seu Right.

DocumentCrop (o que Doc devolve):

  • Found - se foi mesmo detetado um contorno. Verifique isto primeiro.
  • Image - bytes PNG da página aplanada, ou os bytes originais intactos quando Found é false. Uma foto que nunca foi recortada continua a ler-se; uma recortada no quadrilátero errado não, por isso o detetor prefere recusar a adivinhar.
  • Corners - o contorno na foto de origem, superior-esquerdo, superior-direito, inferior-direito, inferior-esquerdo. Width, Height, ElapsedMs.

Os dois compõem-se pela ordem óbvia e, como um recorte falhado devolve o original, encadeá-los faz sempre alguma coisa: var crop = await Doc.CropAsync(photo); var scan = await Ocr.ExtractAsync(crop.Image);

Três limites a desenhar de antemão. O modelo incluído é PP-OCRv5 latin, ou seja alfabetos ocidentais e dígitos, não CJK. Não é um leitor de PDF, uma página PDF tem de ser rasterizada para imagem primeiro. E isto é OCR, não compreensão de documentos: transformar linhas em fornecedor, número de fatura e total é lógica que você escreve, e às vezes vai estar errada, por isso verifique line.Confidence e encaminhe as leituras fracas para uma pessoa em vez de as escrever diretamente numa tabela.

Atenção ao timeout do script. Uma página completa demora um ou dois segundos e a primeira chamada do processo acrescenta cerca de um segundo a carregar os modelos. O timeout de dez segundos não interrompe um await, por isso um script que lê várias páginas não devolve nada em vez de falhar cedo. Faça o trabalho multipágina num Scheduled Event, uma página por execução.

Expressões de template (ações de Business Event)

A configuração de ações usa chavetas {{ … }}. A lista completa de tokens válidos:

  • {{ Entity.column_name }} / {{ Entity.id }}
  • {{ OldEntity.column_name }}
  • {{ now() }} / {{ getdate() }} - datetime UTC ISO 8601
  • {{ today() }} - string de data
  • {{ guid() }} / {{ newid() }} - GUID novo
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }} - segundos Unix
  • {{ empty() }}, {{ null() }}
  • Agregações dentro de um contexto de join: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Aritmética: {{ Entity.quantity * Entity.price }} - avaliada após substituição via DataTable.Compute()

Não existe {{ user.email }} nem outro token com âmbito de utilizador.