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.truesignifica 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 varde topo dá um erro de sintaxe num script, por isso feche uma ligação você mesmo, numfinally, como faz o exemplo. - Um valor lido de
Entityédynamic, e um método de extensão como oQueryAsyncdo 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
OmniCoreouArchestack, 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ção | Por omissão | O que faz |
|---|---|---|
Enabled | true | Desligado, 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. |
Sources | nuget.org | De 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, Password | Nenhum | Um feed com nome, para um que precise de credenciais. O URL nunca leva um nome de utilizador nem uma palavra-passe. |
Feeds:<name>:Patterns | Nenhum | Os 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>:AllowInsecureConnections | false | Deixa passar um feed em http:// simples. |
Directory | Uma pasta nos dados locais da aplicação | Onde 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, MaxExtractedBytes | 100 MB, 1 GB | O maior pacote NuGet que um restauro descarrega, e o máximo que um pacote NuGet pode ocupar depois de descompactado. |
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages | 500 MB, 2 GB, 150 | O máximo que um restauro descarrega, descompacta e conta em pacotes NuGet, tudo junto. |
MaxCacheBytes | 10 GB | Acima 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. |
MinFreeDiskBytes | 1 GB | Com menos espaço livre em disco do que isto, um restauro não começa, e um download que deixaria menos é parado. |
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds | 300, 120, 5000 | Quanto 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. |
AllowNativeAssets | true | Desligado, um pacote NuGet que traga bibliotecas nativas é recusado, e nenhuma é alguma vez carregada. |
AuditRefuseLevel | Vazio | low, 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. |
CertificateRevocationMode | online | online 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, devolveapplication/pdf. Usado pelo painel de pré-visualização do editor.POST /api/v1/pdf-templates/generate/{name}- renderiza por nome, devolveapplication/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- ExpandoObjectdynamic. Registo atual. Aceda a campos comEntity.column_name. MutarEntitynum trigger Before* persiste as alterações como parte da escrita em curso.OldEntity- ExpandoObjectdynamic. 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 devolveTask<object?>.Pdf-ScriptPdfHelper. Renderizar um PDF template guardado pelo nome:await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() })devolvebyte[];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 umOcrExtraction.await Ocr.ExtractBase64Async(string)aceita a mesma imagem codificada em base64 (um prefixodata: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 umDocumentCrop.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. Devolvedynamic?(ounullse não encontrar).await Db.CreateAsync(string table, object values)- inserir.valuespode ser um objeto anónimo ou umDictionary<string, object?>. Dispara triggers Before/After Create. Devolveint(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()- devolveList<dynamic>.await ...FirstAsync()- devolvedynamic?(primeira correspondência ou null). Temporariamente define o limite para 1.await ...CountAsync()- devolveint.
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 fazemCreateAsync,UpdateAsynceDeleteAsync, 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,ExecuteDeletee escritas por SQL direto são recusados. - Tipos. Um
timestamp with time zoneé lido como umDateTimeem 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 comDb.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. DevolveTask<dynamic?>, por isso o resultado lê-se sem cast, incluindo um tipo anónimo. Os argumentos lidos deEntitysã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. DevolveTask<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;ReadTextAsynclê texto UTF-8;ReadDataUriAsyncdevolvedata:image/png;base64,…para uma imagem num PDF Template ou num email.WriteAsync(path, bytes)eWriteTextAsynccriam 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 temPath,Name,IsFolder,Size,ModifiedUtceContentType.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 linhaText,Confidencee uma bounding box:X,Y,Width,Height, maisRighteBottom. 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 quandoFoundé 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 viaDataTable.Compute()
Não existe {{ user.email }} nem outro token com âmbito de utilizador.