Ogni strumento, in un solo posto
Questa pagina è il reference pratico per ogni strumento nella sidebar del tuo ambiente Archestack. Ogni sezione copre cosa fa lo strumento, quando lo useresti, i passi per i workflow più comuni e le trappole in cui inciampano i nuovi utenti. La sezione finale (API chiamabili dagli script) documenta l'esatta superficie che gli script possono chiamare.
Schema Designer
Un editor visuale per lo schema del database, tabelle, colonne, tipi, relazioni, indici, che si salva automaticamente su un documento JSON. Lo schema è la fonte di verità per la tua applicazione; nessun altro strumento vede una colonna finché non la aggiungi qui e ne fai il deploy.
Workflow comune: nella toolbar, fai clic su Add Table (pulsante
icona). Si apre un dialog intitolato "Create New Table"; digita un nome (il testo di aiuto
ti ricorda "Lowercase with underscores recommended"), fai clic su Create. La
tabella compare sul canvas con una primary key SERIAL id auto-generata. Seleziona
la tabella, il pannello destro si apre sulla tab Columns. Aggiungi colonne dal
box "Add column" con bordo tratteggiato in basso (campo nome, dropdown Type, fai clic su
Add Column). Fai clic su ogni colonna per espanderla e regolare i toggle
(PK, NULL, UQ), Length e Default value.
Tipi di colonna disponibili nel dropdown 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, più i tipi geometrici e array. La Length è rilevante solo per VARCHAR e CHAR.
Le foreign key stanno nella tab Relations del pannello destro (non "Relationships"). Scorri fino a "Add relationship": scegli la colonna FK sulla tabella corrente, il tipo di relazione (One-to-One / One-to-Many / Many-to-Many), la tabella di riferimento, la colonna di riferimento, poi fai clic su Add Relationship. Ogni relazione ha i dropdown On Delete e On Update (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION).
Auto-save: l'indicatore in basso a sinistra oscilla tra "Auto saving..." e "Saved". C'è anche un pulsante Save nella toolbar (abilitato solo quando ci sono modifiche in attesa) per i salvataggi espliciti.
Altri pulsanti della toolbar: Add Group (container visuali per tabelle correlate, pannello proprietà intitolato "Group"), Add Text (annotazioni libere, pannello proprietà intitolato "Text Annotation"), controlli di zoom, filtro package, ricerca tabelle.
Trappole: rinominare una colonna produce un piano distruttivo (drop + add):
modifica l'SQL nella tab Generated SQL del deployment per usare RENAME COLUMN
se vuoi preservare i dati. Aggiungere una colonna NOT NULL a una tabella non vuota senza
un default fallisce, rendila nullable, fai il backfill, poi altera. Il pulsante toolbar
Deploy ti porta su una pagina di configurazione del deployment, non direttamente nel deploy.
Database Deployments
Elenca ogni deployment con il suo stato (Draft / Executing / Succeeded / Failed) e ne lancia
di nuovi. Route: /database-deployments/overview.
Pulsanti in alto a destra: Refresh (ricarica la lista), New Deployment (crea una nuova bozza).
Da Schema Designer: il pulsante verde Deploy con icona razzo nella
toolbar di Schema Designer ti porta a /database-deployments/configure/new, una
pagina di configurazione deployment nuova.
Pagina di configurazione del deployment: due tab.
- Configuration - editor per gli script pre/post deployment (PostgreSQL) ciascuno con un pulsante Test Run. Il pre-script gira prima dei cambi di schema; il post-script dopo. Utili per backfill, ricostruzione di indici o pulizia di staging area. Più un toggle Force Deploy (Allow Data Loss) per operazioni distruttive.
- Generated SQL - mostra la migration prodotta dalla piattaforma. L'SQL è modificabile. Un pulsante Regenerate (icona refresh) riesegue il diff se hai fatto cambi di schema da quando hai aperto questa pagina.
Pulsanti in alto: Back, Save (salva la configurazione come Draft), Deploy (esegue il deployment immediatamente). Deploy è quello che clicchi alla fine.
Trappole: i deployment non vengono automaticamente avvolti in una transazione,
un fallimento a metà strada lascia il database in uno stato parziale. Per le migration
rischiose, avvolgi le tue istruzioni in BEGIN; ... COMMIT; nel pre-script. Il
toggle Force Deploy salta i controlli di sicurezza; usalo in modo deliberato.
Business Entities
Viste curate costruite su una master table più join da tabelle correlate. Le pagine si collegano alle Business Entities, mai a tabelle raw. Vedi Concetti -> Business Entities per il perché.
Workflow comune: fai clic su Create. Si apre l'editor a tutta pagina con le tab Visual, JSON, Events. Imposta Entity Name, Master Table (Autocomplete), e Label Column (la colonna il cui valore rappresenta un record nei picker). Salva, la BE auto-genera una lista di colonne native dalla master table.
Aggiungere join: scorri fino alla card del Join Configurator e fai clic su Add Join. Ogni join si apre come un Accordion con: Join Type (INNER / LEFT / RIGHT), From Table, To Table (Autocomplete raggruppato per Database Tables / Third Party), From Column, To Column, dropdown opzionali Type Cast, e un picker di colonne per quali colonne target esporre.
Aggregazioni: su un join, sposta Aggregate Mode. Per ogni colonna scelta poi scegli una Aggregate Function: COUNT / SUM / AVG / MIN / MAX / COUNT DISTINCT. "Numero di contatti aperti su questa Company" è l'esempio canonico.
Run Preview: l'editor ha un pulsante Run Preview (con un'icona play) che esegue la configurazione completa dei join e mostra righe reali in una griglia dati. Se una colonna joined torna vuota, la colonna non era spuntata nel picker delle colonne del join (o la relazione è mal configurata).
Più BE per master table: niente ti impedisce di avere
customer (sales view) e customer (support view) sulla stessa
master. Pagine diverse, colonne appropriate per pubblici diversi, stessa riga sottostante.
Trappole: le colonne joined e aggregate sono read-only. Per modificare l'etichetta joined, vai al record sorgente. Le colonne aggregate vengono ricalcolate a ogni query, va bene per centinaia di righe, lente su milioni.
Page Editor
Configura la UI runtime collegando una Page a una Business Entity. La piattaforma auto-genera le sezioni in base alle colonne della BE; tu raffini il layout da lì. Non c'è un "template picker" separato, ogni pagina parte dalla stessa baseline generata e tu la personalizzi.
Workflow comune: fai clic su Create. Compila Page Name,
Page Route (es. /companies), scegli la Business Entity
(Autocomplete). La pagina si apre nell'editor con le tab Visual, Overview,
Create, Entities, Events, JSON. Aggiusta il layout
auto-generato, poi sposta lo Switch Published nell'header in alto su ON.
Tipi di widget di campo (il Select Type su ogni campo): Text,
Textarea, Number, Date, Select, Checkbox, Email. Usa Select per i campi
foreign key e imposta l'autocomplete Entity del campo sulla BE referenziata così
gli utenti scelgono per label.
Tab nel form di dettaglio: fai clic su Add Tab nella tab Visual.
Dentro una tab puoi aggiungere una sezione RelatedGrid collegata a un'altra BE
filtrata per PK del record corrente. Esempio classico: una pagina Company con tab
"Contacts" e "Deals", ciascuna filtrata per company_id = current company's id.
Pulsanti azione nell'inspector Header del form di dettaglio. Fai clic su Add nella sezione Actions. Ogni azione ha Label, Icon (Save / Delete / Add / Refresh / Download / Bolt / None), Variant (Contained / Outlined / Text), Color (Primary / Secondary / Error / Success / Warning / Info), e Steps (una lista ordinata di: Save Record, Delete Record, Navigate, Business Events, Custom). Per eseguire un Business Event da un pulsante, imposta il tipo di step su Business Events e scegli l'event per nome.
Pubblicazione: lo Switch Published nell'header in alto è l'unico controllo di pubblicazione. ON = la pagina appare sotto Published Pages nella sidebar e gli utenti finali che navigano alla sua route la vedono. OFF = bozza (solo tu nell'editor vedi le tue modifiche).
Trappole: le colonne auto-generate su una Page fresca rispecchiano la BE, se la BE cambia (aggiungi un join), la Page non prenderà automaticamente le nuove colonne. Ricarica la pagina nell'editor o aggiungi la colonna manualmente alla sezione. Attualmente non c'è un tipo di step azione "Generate PDF", per collegare un PDF a un pulsante di pagina, esponi il PDF tramite uno Script Module che chiama l'endpoint REST e usa uno step Custom (oppure chiama l'endpoint direttamente da un Frontend Template).
Business Events
Regole che scattano sui cambi di dati (o su pianificazione, o manualmente) ed eseguono action quando le condizioni sono soddisfatte. Il meccanismo "quando succede X, fai Y" della piattaforma. Vedi Concetti -> Business Events per il modello.
Workflow comune: fai clic su Create. Imposta Rule Name, sposta Enabled. Scegli Business Entity. Spunta uno o più Triggers: BeforeCreate / BeforeUpdate / AfterCreate / AfterUpdate / BeforeDelete / OnSchedule / Manual / InitialValue. Costruisci l'albero di condizioni (gruppi AND/OR). Aggiungi una o più action nel FlowCanvas. Salva.
Tipi di action (RuleActionKind):
- Execute Script - esegue uno script C#. L'action più flessibile; usala ogni volta che vuoi impostare un campo, fare un calcolo, chiamare un'API, qualsiasi cosa custom. Lo script riceve
Entity(mutalo su trigger Before* per cambiare la scrittura in corso),OldEntity,Log,Db,Modules. - Validate - uno script che ritorna
bool.truesignifica che la validazione fallisce e il salvataggio è bloccato con il messaggio di errore configurato. Opzionalmente evidenzia colonne specifiche tramite errorColumns. - Block Operation - ferma duramente l'operazione con un messaggio configurato. Niente script.
- Create Entity - inserisce un record in un'altra BE. I valori dei campi supportano template expression come
{{ Entity.column_name }}e{{ now() }}. - Update Entity - aggiorna record in un'altra BE che matchano un filtro di condizione. Stesse template expression.
- Delete Entity - elimina record che matchano una condizione. Rifiuta di scattare senza una condizione (sicurezza).
- Send Email / Send Webhook / Publish Event - definite nello schema come miglioramenti futuri; attualmente loggate e saltate.
Pattern "imposta un campo": non c'è un'action discreta Set Field.
Il modo per impostare un campo è un Execute Script su un trigger Before* che muta
Entity. Esempio: Entity.title = ((string)Entity.title)?.Trim();. La
piattaforma persiste l'Entity modificato come parte della scrittura in corso, nessuna query
extra, nessun rischio di ricorsione.
Run Simulation: la tab Simulate dell'editor di trigger ha un
picker di record e un pulsante Run Simulation. La piattaforma esegue le tue
condizioni e action contro il record scelto senza persistere modifiche: le scritture vengono
controllate e registrate, ma mai eseguite, comprese quelle di uno script tramite Db
e Files, quindi niente arriva al database o ai file del workspace e non scatta
nessun altro Business Event. Mail e notifiche non vengono inviate. Tutto il resto che uno script
fa da sé, invece, è reale: le chiamate HTTP, System.IO e tutto ciò che fa un
pacchetto NuGet, come una connessione a un altro database. Il
pannello di output mostra quali condizioni hanno matchato e cosa avrebbe fatto ogni action.
Usalo per beccare configurazioni sbagliate prima che i dati live le incontrino.
Template expression nella configurazione delle action usano parentesi
{{ ... }}. I token effettivi:
{{ Entity.column }},
{{ OldEntity.column }},
{{ now() }} / {{ getdate() }},
{{ today() }},
{{ guid() }} / {{ newid() }},
{{ year() }},
{{ month() }},
{{ day() }},
{{ timestamp() }},
aggregati come {{ SUM(column) }} dentro contesti di join,
e aritmetica post-sostituzione come {{ Entity.quantity * Entity.price }}.
Non c'è un token {{ user.email }}.
Operatori di condizione: Equals, NotEquals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, Like (= ILIKE), Contains (= ILIKE %value%), IsNull, IsNotNull, In (= ANY(...)).
Trappole: un trigger che muta lo stesso record che osserva si rilancerà
da solo se usi Update Entity; usa invece Before Update +
Execute Script + Entity.field = .... Due trigger sullo stesso event
scattano in ordine di priorità. Gli event disabilitati appaiono comunque nella lista, il
toggle Enabled è separato dalla config del trigger.
Script Modules
Script C# riutilizzabili compilati a runtime da Roslyn. Prendono parametri, interrogano il
database tramite l'helper Db, ritornano un valore. Chiamabili da Business
Events, Scheduled Events, e direttamente dal front end.
Workflow comune: fai clic su Create. L'editor si apre con le tab
Edit, Parameters, Test, JSON. Nomina il modulo
(PascalCase, es. RecalcOpenRevenue). Nella tab Parameters, fai clic
su Add per parametro (Name, Type, Required, Default Value). Opzioni di Type:
string, int, decimal, double, bool, DateTime. Torna alla tab Edit, scrivi C#.
IntelliSense è attivo.
Pannello Test: la tab Test mostra gli input dei parametri a sinistra e un pulsante Run (icona PlayArrow). Fai clic su Run; il lato destro mostra un indicatore di successo o errore, log di output, e il valore di ritorno (formattato JSON).
L'helper Db, vedi il reference completo in API chiamabili dagli script sotto.
Chiamare da un Business Event: aggiungi un'action Execute Script il cui corpo chiama il modulo:
// 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
}); Chiamare da uno Scheduled Event: crea un event con il trigger
On Schedule, imposta il cron, aggiungi un'action Execute Script con lo
stesso corpo Modules.CallAsync.
Logging: ogni invocazione scrive una voce in Event Logs con tempo di esecuzione, parametri, e risultato (o errore).
Trappole: sempre await le chiamate Db, dimenticarlo compila
ma ritorna un Task. I ritorni sono passati per oggetto (non serializzati JSON dal
framework); per il consumo front-end, ritorna oggetti anonimi con campi primitivi. La
suggestion-on-commit-character è disabilitata di proposito nell'editor; usa Tab per accettare.
Shared Code
Codice che serve a più di uno script o di una pagina, tenuto in file a sé. Il nome di un file è
il suo percorso, come Sales/OrderSummary.cs o components/StatusChip.tsx,
e la sua estensione dice cos'è: .cs è C# che ogni script può usare, mentre
.tsx, .ts, .jsx e .js sono moduli importati
dalle pagine, dai Frontend Templates e dai blocchi Code dei layout. I file condivisi appartengono
ai package come qualsiasi altro artefatto.
Workflow comune: nella sidebar, sotto Logic, apri Shared Code e fai clic su New file. Digita il percorso; l'editor precompila il file con un esempio del suo tipo. Scrivi il codice e fai clic su Save.
I file C# sono C# ordinario: namespace, record, classi e helper statici. Ogni
script (Script Modules, Business Events, Scheduled Events, data script dei PDF e test) può usarne
i tipi pubblici. Un tipo senza namespace è disponibile con il suo nome; uno in un namespace
richiede un using nello script, oppure un global using in un qualsiasi
file condiviso, che vale poi per ogni script. Un file condiviso vede il
modello dati tipizzato (Order, WorkspaceDb) ma
non i globali di uno script, quindi un helper che ha bisogno di Db o di
Log lo riceve come argomento.
// 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(); Una modifica salvata raggiunge ogni script alla sua prossima esecuzione. L'editor controlla un file condiviso insieme agli altri file condivisi, e Go to Definition su un tipo condiviso in uno script apre il file che lo dichiara. Quando il codice condiviso non compila, gli script che lo usano falliscono e indicano il file che contiene l'errore; quelli che non lo usano continuano a girare. Una modifica a una tabella che romperebbe un file condiviso viene segnalata, insieme agli script che rompe, prima che il deployment venga eseguito.
I moduli si importano da @/ seguito dal loro percorso senza
l'estensione: components/StatusChip.tsx diventa
import { StatusChip } from "@/components/StatusChip". Una pagina può anche
avere file propri accanto alle sue viste, aggiunti con + sopra il suo editor di codice e
importati come ./OrderRow. Un modulo vede gli stessi globali del codice delle pagine
ed esporta ciò che gli altri file usano. Una vista che importa può terminare esportando il suo
componente come default, invece che con return. Il codice senza import
né export gira esattamente come ha sempre fatto.
// 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>
);
}
I pacchetti che il codice può importare sono quelli che l'app include già, e un import è lo
stesso oggetto che contengono i globali della pagina: react,
@mui/material, @mui/x-date-pickers, dayjs,
lucide-react, three, @react-three/fiber e
@react-three/drei. Qualsiasi altro pacchetto non c'è.
Pubblicazione: l'app pubblicata legge i moduli condivisi come li ha fissati la release corrente, quindi una modifica raggiunge gli utenti finali con la prossima pubblicazione, come succede per una pagina. Un visitatore che non ha effettuato l'accesso riceve solo i moduli importati dalle pagine pubbliche e dai layout pubblici.
Trappole: il percorso è l'identità di un file in ogni workspace, e gli import vi
fanno riferimento, quindi rinominare un file rompe ciò che lo importa finché gli import non
vengono aggiornati. I percorsi usano lettere, cifre, punti, underscore e trattini. Il namespace
Archestack è riservato. Un alias in un global using
(global using Money = decimal;) non viene passato agli script.
NuGet Packages
Librerie da NuGet per gli script C# del workspace: un driver per un altro database, un lettore di
CSV o di Excel, un client per un servizio. Ogni script può usarle (Script Modules, script dei
Business Events, Scheduled Events, data script dei PDF, script dei test e controlli in codice), e
anche i file .cs condivisi. L'editor di script e VS Code ne completano i tipi. Un
workspace che non ne aggiunge nessuno esegue i suoi script esattamente come prima.
Workflow comune: nella sidebar, sotto Logic, apri NuGet Packages e fai clic su Add NuGet package. Cercalo, oppure digita il suo id esatto quando la sorgente non supporta la ricerca, e scegli una versione esatta: l'ultima stabile è preselezionata, e Include prerelease versions elenca le altre. In Belongs to scegli a cosa appartiene: uno dei package del workspace, oppure Workspace only. Add lo scarica prima sul server, insieme alle sue dipendenze. Quando non ci riesce, il dialog dice perché e non viene salvato niente; quando funziona, mostra i namespace che porta.
In un package: un pacchetto NuGet che appartiene a un package viaggia con esso,
nella sua esportazione e in ciò di cui una pipeline fa il deploy, e l'installazione di un package
elenca, prima che cambi qualcosa, i pacchetti NuGet che aggiunge o di cui alza la versione. In un
repository stanno in packages/<package>/nuget.json, scritto solo quando il
package ne dichiara qualcuno, e VS Code controlla il file mentre scrivi. Un push che modifica il
file li scarica prima, e viene rifiutato, senza cambiare niente, quando non si possono ripristinare.
{
"packages": [
{ "id": "CsvHelper", "version": "33.0.1" },
{ "id": "Dapper", "version": "2.1.35" },
{ "id": "MySqlConnector", "version": "2.4.0" }
]
} Usarne uno: un pacchetto NuGet porta tipi, mai un globale, e niente viene
importato al posto tuo. Scrivi un using per il suo namespace, che spesso non è l'id:
la colonna Namespaces della pagina mostra i namespace di ogni pacchetto NuGet come righe
using da copiare. Da uno script ritorna valori semplici, non gli oggetti propri della
libreria.
// 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();
} Versioni: una dichiarazione indica una sola versione esatta, come
2.1.35, mai un intervallo. Quando più package chiedono versioni diverse dello stesso
pacchetto NuGet, ogni script del workspace usa la più alta. La colonna In use mostra qual
è e chi l'ha chiesta.
Quale copia gira: un workspace carica una copia propria di ogni pacchetto NuGet,
tenuta separata da ogni altro workspace sul server, anche quando il server contiene la stessa
libreria per sé. Resta invece la copia del server quella di .NET stesso e delle librerie che gli
script hanno già, come System.Text.Json, il logging, EF Core e Npgsql. Un pacchetto
NuGet che chiede una versione più recente di una di queste riceve un avviso: gira sulla copia del
server, e un metodo che esiste solo nella versione più recente fallisce quando viene chiamato.
Ripristini: il primo script che ha bisogno dei pacchetti NuGet su un server li fa scaricare a quel server. Aspetta fino a cinque secondi e, quando il download dura di più, gli viene chiesto di girare di nuovo tra un momento. Dopo una modifica, il workspace ripristina il nuovo insieme. Finché non ci riesce, gli script tengono l'ultimo insieme ripristinato, così un ripristino fallito non toglie mai pacchetti NuGet che funzionavano. Il banner della pagina mostra lo stato e gli errori di un ripristino fallito con la loro spiegazione. Dopo un fallimento il server riprova la prima volta che uno script ha bisogno dei pacchetti NuGet dopo l'ora indicata dal banner, oppure subito con Restore now. Resolved elenca ogni pacchetto NuGet usato dagli script, dipendenze comprese.
Simulazioni e test: una simulazione (la tab Test, Run Simulation)
blocca solo ciò che passa da Archestack: le scritture tramite Db e Files
vengono simulate e non salvate, e mail e notifiche non vengono inviate. Un'esecuzione di test fa
le sue scritture tramite Db nella sua sandbox e blocca il resto allo stesso modo.
Tutto il resto che lo script fa da sé, invece, è reale: le chiamate HTTP, System.IO e
tutto ciò che fa un pacchetto NuGet, come una connessione a un altro database, quindi una
scrittura tramite un driver avviene a ogni esecuzione. Mentre provi uno script del genere,
puntalo a un database di test.
Trappole:
- I pacchetti NuGet richiedono l'accesso ai dati tipizzato, come il C# condiviso. Su un server che lo disattiva (
Scripting:TypedData:Enabled), la pagina lo dice e non permette di aggiungerne. #r "nuget: Dapper, 2.1.35", il modo in cui altri esecutori di script ne caricano uno, qui non funziona. Aggiungilo dalla pagina.- Una libreria nativa portata da un pacchetto NuGet viene caricata una volta per processo del server, è condivisa dai workspace che usano lo stesso file e resta caricata finché il server non si riavvia.
- I pacchetti NuGet Workspace only non vengono esportati con un package, non stanno nel repository e le pipeline non ne fanno il deploy. Assegna un pacchetto NuGet di cui un'app ha bisogno al package di quell'app.
- In uno script un
using vardi primo livello non viene accettato dal parser, quindi chiudi tu la connessione, in unfinally, come fa l'esempio. - Un valore letto da
Entityèdynamic, e un metodo di estensione comeQueryAsyncdi Dapper non può riceverlo come argomento: lo script non compila (CS1973). Fanne prima il cast:(string)Entity.customers_number. - I pacchetti NuGet che sono strumenti, template o SDK MSBuild vengono rifiutati, e così anche uno che su .NET 8 non offre niente su cui gli script possano compilare, o uno che estende il driver PostgreSQL proprio del server o EF Core. Gli id che si chiamano
OmniCoreoArchestack, o che iniziano con uno dei due seguito da un punto, sono riservati alla piattaforma.
Per chi gestisce i server: i pacchetti NuGet sono attivi per impostazione
predefinita e sempre disattivati in un trial. Le impostazioni stanno sotto
Scripting:NuGet, oppure nelle variabili d'ambiente
Scripting__NuGet__Enabled e così via. Ogni server scarica per conto proprio, quindi
ognuno deve raggiungere nuget.org o i feed configurati qui, tramite il proxy in
https_proxy, http_proxy e no_proxy quando ce n'è uno. Dai a
tutti i server di un'installazione le stesse sorgenti.
| Impostazione | Predefinito | Cosa fa |
|---|---|---|
Enabled | true | Se disattivato, non si scarica nulla e gli script non ne caricano nessuno. La pagina non può aggiungerne; le dichiarazioni portate da un'importazione o da un push restano, inutilizzate. |
Sources | nuget.org | Da dove arrivano i pacchetti NuGet, separati da ;: feed indicati dal loro service index v3 (l'URL che finisce con index.json), oppure cartelle assolute, da cui i pacchetti vengono copiati e che sono considerate attendibili: i limiti di download valgono solo per i feed. nuget.org viene usato solo quando né questa impostazione né Feeds indicano una sorgente. |
Feeds:<name>:Url, Username, Password | Nessuno | Un feed con un nome, per uno che richiede credenziali. L'URL non contiene mai un nome utente o una password. |
Feeds:<name>:Patterns | Nessuno | Gli id che questo feed serve, separati da ; (Contoso.*). Quando un feed qualsiasi ha dei pattern, un id arriva dal feed il cui pattern gli corrisponde, e un feed senza pattern serve tutto il resto. |
Feeds:<name>:AllowInsecureConnections | false | Lascia passare un feed in semplice http://. |
Directory | Una cartella nei dati applicativi locali | Dove questo server tiene i pacchetti NuGet che ha scaricato: una cartella per server sul suo disco locale, mai un volume condiviso tra server. In un container, dagli un volume proprio di quel server, altrimenti ogni nuovo container li scarica di nuovo. |
MaxPackageBytes, MaxExtractedBytes | 100 MB, 1 GB | Il pacchetto NuGet più grande che un ripristino scarica, e lo spazio massimo che un pacchetto NuGet può occupare una volta estratto. |
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages | 500 MB, 2 GB, 150 | Il massimo che un ripristino scarica, estrae e conta in pacchetti NuGet, tutti insieme. |
MaxCacheBytes | 10 GB | Oltre questa soglia la cartella non accetta nuovi ripristini, e i pacchetti NuGet che questo server non sta usando vengono rimossi, a partire da quelli usati meno di recente. |
MinFreeDiskBytes | 1 GB | Con meno spazio libero su disco di così un ripristino non parte, e un download che ne lascerebbe meno viene interrotto. |
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds | 300, 120, 5000 | Quanto può durare un ripristino, quanto l'aggiunta di un pacchetto NuGet aspetta il suo download, e quanto uno script aspetta il download di questo server prima che gli venga chiesto di girare di nuovo. |
AllowNativeAssets | true | Se disattivato, un pacchetto NuGet che contiene librerie native viene rifiutato, e nessuna viene mai caricata. |
AuditRefuseLevel | Vuoto | low, moderate, high o critical: un pacchetto NuGet con una vulnerabilità nota di quel livello o superiore viene rifiutato. Vuoto non ne rifiuta nessuno, e le vulnerabilità compaiono come avvisi; qualsiasi altro valore fa fallire ogni ripristino che aggiunge un pacchetto NuGet. |
CertificateRevocationMode | online | online o offline: come viene controllata la revoca dei certificati di firma dei pacchetti NuGet. |
Frontend Templates
Frammenti UI riutilizzabili scritti in TSX che il page editor può inserire in una pagina. Utili quando i template configurati non bastano, grafici su misura, layout insoliti, integrazioni con widget di terze parti.
Workflow comune: fai clic su Create, nomina il template, scrivi
un componente TSX nell'editor. Il componente riceve props: record (la riga
corrente quando incorporato in un form di dettaglio), refresh (ri-recupera i
dati), e qualche helper. Salva. Referenzia il template da un campo del Page Editor.
Pacchettizzabile: i template fluiscono attraverso il sistema Package come qualsiasi altro oggetto config, vengono inclusi in cascading quando una pagina che li usa viene aggiunta a un package.
Trappole: il TSX non può importare pacchetti npm arbitrari: importa il
codice condiviso da @/ e i pochi pacchetti che l'app include.
PDF Templates
HTML + template Scriban + un piccolo data script C# che renderizza in un PDF (Chromium headless). Ogni template è invocabile per nome da uno script via REST o direttamente da un pulsante su una Page (con un piccolo adattatore, vedi sotto).
Workflow comune: fai clic su + Create. L'editor si apre con i campi testo Name e Description in alto, cinque tab sul lato sinistro (HTML Template, Data Script, Settings, Params, JSON), e un pannello PDF Preview sempre attivo sulla destra. Le tab Data Script e HTML Template aprono ciascuna un editor Monaco sulla metà sinistra; l'anteprima si rigenera quando salvi (pulsante verde Update in alto a destra).
Dichiara i parametri nella tab Params tramite il pulsante + Add (Name, Type, Required, Default Value). L'etichetta della tab si aggiorna a Params (n) con il conteggio. Page format / orientation / margins / scale / header / footer vivono nella tab Settings.
Forma del data script (vedi tutorial PDF Invoices per uno reale):
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 del template HTML (Scriban, simile a Liquid/Handlebars):
<h1>{{ InvoiceNumber }}</h1>
<table>
{{ for line in Lines }}
<tr><td>{{ line.description }}</td><td>{{ line.quantity }}</td></tr>
{{ end }}
</table> Encoding HTML: ogni valore {{ }} viene
codificato in HTML, quindi un valore di un record che contiene <b> viene
stampato come il testo <b>. html.raw è l'opt-out esplicito,
solo per HTML fidato: {{ notes | html.raw }}. Non usarlo mai
su valori che gli utenti finali possono digitare, perché così il loro markup finisce nel
documento renderizzato. Solo il template può disattivare l'encoding, i dati no.
html.escape e html.newline_to_br continuano a funzionare
(newline_to_br fa l'escape del testo ma mantiene i tag <br />).
Pannello di anteprima: sempre visibile sul lato destro dell'editor. Si rigenera quando salvi (pulsante verde Update). Include una colonna di thumbnail per i PDF multi-pagina e una toolbar di viewer integrata (zoom, ruota, download, stampa). I pulsanti icona in alto a destra dell'editor alternano la visibilità del pannello di anteprima e l'editing a tutto schermo.
Chiamarlo da fuori dall'editor:
POST /api/v1/pdf-templates/{id}/preview- renderizza per ID, ritornaapplication/pdf. Usato dal pannello di anteprima dell'editor.POST /api/v1/pdf-templates/generate/{name}- renderizza per nome, ritornaapplication/pdf.POST /api/v1/pdf-templates/generate/{name}/base64- stesso ma ritorna{ "data": "<base64>" }.
Passa i valori dei parametri nel body JSON. L'auth è il Bearer token standard.
Limitazione del trial: sia l'endpoint generate sia l'endpoint
{id}/preview (usato dal pannello di anteprima dell'editor) ritornano
403 nella modalità trial, il rendering PDF è interamente disabilitato. Puoi comunque
creare il template (data script, HTML, settings, parametri) e salvarlo; semplicemente non
vedrai un PDF renderizzato finché non sei su un ambiente a pagamento.
Nota sul rendering chiamabile da script: il contesto di scripting C# espone
eccome un globale Pdf, quindi uno Script Module renderizza un template salvato
direttamente con await Pdf.GenerateAsync("invoice", ...) o
Pdf.GenerateBase64Async(...). Non serve alcun giro con HttpClient
verso l'endpoint generate. (L'autocomplete dell'editor di script reclamizza ancora
Pdf.RenderAsync e un oggetto Templates; nessuno dei due nomi esiste a
runtime.)
Trappole: Scriban fa l'escape dell'HTML di default. I page break sono
guidati dal CSS, page-break-before: always su una sezione. I font disponibili
sul server di rendering sono limitati ai font di sistema più le famiglie Noto e Liberation,
incorpora i tuoi via @font-face base64 se ti serve un font di brand.
Immagini, font e fogli di stile: il renderer carica risorse solo da
indirizzi http(s) pubblici, più gli URL data: e blob:.
Gli indirizzi interni e di rete privata (localhost, 10.x,
192.168.x, nomi dei servizi dei container e così via) sono bloccati, quindi
un'immagine servita da un servizio interno non comparirà nel PDF. Usa un URL pubblico o un
URL data:.
Scheduled Events
Lo stesso sistema di Event Trigger dei Business Events, con il trigger On Schedule spuntato. Usalo per ricalcoli notturni, digest settimanali, pulizie periodiche, report mensili.
Workflow comune: crea un Business Event con il trigger On Schedule
(invece di, o in aggiunta ai, trigger di cambio dati). Configura l'espressione cron.
Aggiungi un'action Execute Script il cui corpo invoca uno Script Module via
Modules.CallAsync(...).
Espressioni cron: in stile Quartz a 6 o 7 campi, vedi Concetti -> Scheduled Events per i pattern comuni. Gli orari sono il server time (UTC sullo stack di produzione).
Trappole: un job lungo che eccede il suo intervallo cron non si raddoppia, Quartz non fa scattare una seconda istanza dello stesso job in concorrenza, il secondo tick viene saltato. Le esecuzioni fallite non riprovano automaticamente; costruisci il retry nello script.
Packages
Bundle esportabili di oggetti di configurazione (BE, pagine, event, script, template, schema) con dipendenze in cascading. Aggiungere una Page a un package porta automaticamente tutto ciò che quella pagina referenzia.
Workflow comune (export): fai clic su Create, nomina e versiona il package. Aggiungi gli oggetti di livello superiore che ti interessano, di solito una manciata di pagine. Apri il pannello degli item collegati per vedere la lista in cascading (cosa conterrà effettivamente il package). Despunta qualsiasi cosa tu voglia omettere. Decidi se includere i dati (toggle all'export). Fai clic su Export, ricevi uno ZIP.
Workflow comune (import): sull'ambiente di destinazione, apri Packages, fai clic su Import, carica lo ZIP. La piattaforma mostra cosa sarà aggiunto o aggiornato. I delta di schema non sono auto-applicati, se il package referenzia una colonna che non esiste sulla destinazione, l'import fallisce.
Trappole: gli identificatori sono per nome, non per ID numerico. Una BE chiamata "customer" sulla sorgente si collega a una BE chiamata "customer" sulla destinazione, rinomina da una delle due parti e il link si rompe.
In git: l'estensione VS Code scarica un package in una cartella, un file per artefatto, e rimanda indietro la cartella, così un package si può rivedere e versionare come qualsiasi altro codice.
Third-Party Data Connections
Connessioni a database esterni (Postgres, SQL Server, MySQL, altri) che importano tabelle su pianificazione. Le tabelle importate si comportano come tabelle native, appaiono in Schema Designer e possono essere referenziate da Business Entities, Pages, e Scripts.
Workflow comune: crea una connessione (stringa di connessione, test, salva). Elenca le tabelle remote disponibili; scegli quali importare. Per ogni tabella importata, imposta la modalità di sync (Full / Delta) e il trigger di sync (cron / manuale / intervallo fisso).
In mirror, non federata: i dati importati vivono nel tuo
omnicore-db. Le letture sono veloci (Postgres locale). Compromesso:
obsolescenza tra i sync.
Colonne gestite dall'app: marca alcune colonne come "managed by Archestack", non saranno sovrascritte dai sync futuri. Utile quando vuoi annotare dati del vendor con i tuoi flag di stato.
Trappole: il primo sync di una tabella grande può richiedere un po' di
tempo. I sync delta richiedono una colonna che il connettore può usare come watermark, di
solito un timestamp modified_at.
AI Assistant
Un assistente chat integrato nell'app che può sia rispondere a domande sui tuoi dati sia modificare il tuo ambiente. Chiedi "quanti ordini di lavoro aperti per sede questo mese?" e interroga e risponde. Chiedi "aggiungi una colonna stato agli appuntamenti di servizio e una pagina per elencarli" e, se sei amministratore, lo costruisce.
Porta il tuo modello. Archestack non include alcun LLM e non paga alcuna inferenza. Un amministratore aggiunge un provider nella schermata AI Configuration: una base URL, un nome di modello e una chiave API. Funziona tutto ciò che parla l'API chat-completions di OpenAI (ChatGPT, DeepSeek, OpenRouter, Groq, Together, un Ollama o vLLM self-hosted), e l'API nativa di Anthropic è supportata direttamente. Puoi configurarne diversi e sceglierne uno per conversazione.
Tutto ciò che fa passa dall'MCP Server. L'assistente non ha una porta di servizio privata verso la piattaforma, chiama gli stessi tool di qualsiasi client MCP esterno, con la tua identità. I limiti che imposti lì valgono quindi anche per lui, e ogni azione finisce nello stesso audit log. Vedi MCP Server qui sotto.
Workflow comune: apri l'assistente dalla sidebar o dalla bolla fluttuante. Chiedi in una o due frasi. Legge il tuo schema attuale prima di proporre qualcosa, dice cosa sta per cambiare e aspetta la tua conferma. Ogni chiamata a un tool appare sotto la risposta, così vedi cosa ha fatto davvero.
Funziona bene per: domande estemporanee sui tuoi dati, funzionalità in forma CRUD, automazioni semplici, estensioni di entity esistenti.
Meno bene per: regole di business non ovvie e workflow multi-step complessi. Tratta ciò che costruisce come la PR di uno junior dev.
Trappole: lavora su dati di produzione e non c'è undo, quindi fai un backup prima di qualcosa di rischioso. I tuoi prompt e ciò che i tool restituiscono vengono inviati al provider che hai configurato, secondo le sue condizioni; puntalo su un modello self-hosted se la cosa conta. Le conversazioni lunghe ripropongono solo i turni più recenti, quindi una chat lasciata aperta tutto il giorno non ne ricorda l'inizio. E nota i due interruttori: con l'MCP Server fermo l'assistente può ancora parlare, ma non può leggere né cambiare nulla.
MCP Server
Ogni ambiente può esporsi tramite il Model Context Protocol, così che un client AI esterno ad Archestack, Claude Desktop, Claude Code, ChatGPT deep research, un IDE o un tuo script, possa lavorarci direttamente. È anche ciò su cui gira l'AI Assistant integrato.
Spento di default, e i tool che cambiano come è costruita l'applicazione restano spenti anche dopo averlo avviato. È deliberato.
Tre gruppi di tool. Read copre l'elenco e la descrizione delle entity, l'interrogazione delle righe e la lettura di schema, pagine, event, script modules, packages, translations ed event logs. Write è creare, aggiornare ed eliminare record, e passa dallo stesso percorso della UI, quindi i tuoi Business Events scattano esattamente come per una persona che clicca Salva. Admin è il gruppo che permette a un modello di costruire l'applicazione stessa: entity, schema, pagine, event, script modules, translations, packages e i pacchetti NuGet usati dagli script.
Quattro cancelli decidono se una chiamata va a segno: il server è avviato, il gruppo è consentito, quello specifico tool è abilitato, e il token porta lo scope. Un tool bloccato da uno qualsiasi di questi è invisibile al client, invece di essere rifiutato dopo.
Token. Emettine uno per client nella schermata MCP Server, con gli scope che gli servono davvero e una scadenza facoltativa. Il segreto viene mostrato una sola volta, alla creazione, e viene memorizzato solo un hash: un token perso si ruota, non si recupera. Dai a ogni integrazione il proprio token: mantiene leggibile l'activity log, e revocarne uno non disturba gli altri.
Activity log. Ogni invocazione viene registrata, comprese quelle rifiutate e il motivo. È qui che controlli cosa ha fatto davvero un'integrazione.
Trappole: un token con lo scope admin viene trattato come amministratore a tutti gli effetti da tutto il resto, quindi distribuiscili con parsimonia. "Allow token in URL" esiste per i client che non possono inviare un header Authorization, ma un URL finisce nei log e negli screenshot molto più facilmente di un header: lascialo spento se non ti serve. E su un ambiente di produzione con dati reali, lascia spento il gruppo admin.
Object Browser
Vista raw di ogni tabella nativa e importata, aggiungi, modifica, elimina righe direttamente. Include un import CSV per caricamenti bulk.
Quando usarlo:
- Caricamento bulk di dati di riferimento prima che le pagine esistano (paesi, valute, enum di stato).
- Debugging, vedere a occhio cosa c'è effettivamente in una tabella quando una pagina si comporta male.
- Edit admin veloci a dati non ancora esposti tramite una Page.
- Import CSV: scegli una tabella, carica un file, mappa le colonne ai campi, anteprima, commit.
Trappole: Object Browser fa scattare i Business Events (passa attraverso lo stesso write path di EntityService del front end). L'eccezione è l'SQL diretto via Database Deployments, quello bypassa tutto.
Dependency Graph
Una mappa in sola lettura di come gli oggetti di configurazione nel tuo ambiente si referenziano a vicenda. Otto tipi di risorsa appaiono come nodi, colorati per tipo: Business Entities, Pages, Event Triggers, Script Modules, PDF Templates, Frontend Templates, Scheduled Events e Packages. Un arco tra due nodi significa che una risorsa dipende dall'altra. Usalo per rispondere alle domande "cosa si romperebbe se elimino questo?" e "c'è qualcosa di inutilizzato qui?".
Cosa significa un arco: una Page collegata a una Business Entity, un Event
Trigger registrato su una Business Entity, uno step di azione di Page che fa scattare un Event
Trigger, uno script che chiama un altro Script Module tramite
Modules.CallAsync("Name"), uno script che renderizza un PDF Template, uno
Scheduled Event che fa scattare un Event Trigger, o un Package che contiene una risorsa. È una
vista di design-time, riflette ciò che la tua configurazione collega insieme, non il
traffico runtime dal vivo.
Due tipi di problema vengono fatti risaltare visivamente:
- Loose ends (orfani) - nodi che niente referenzia e che non referenziano niente. Vengono raccolti in una banda "Loose ends" bordata di rosso in fondo al canvas. Uno Script Module che nessuno chiama; una Page collegata a niente.
- Dangling references - disegnate come archi tratteggiati rossi. Il riferimento esiste nella configurazione ma il suo target non può essere trovato, per esempio uno step di azione di Page che punta ancora a un Event Trigger eliminato, o uno script che chiama uno Script Module con un nome che non esiste più.
Workflow comune: apri Dependency Graph dalla sezione Tools della sidebar. Il pannello di sinistra ha una ricerca per nome, un selettore Layout (Left to right, Top to bottom, By type, Force-directed), gli interruttori Group by package, Show loose ends e Show edges, e una checklist per tipo. Clicca su un nodo per entrare in modalità focus: quel nodo e i suoi vicini diretti restano luminosi mentre il resto del grafo si attenua, così puoi tracciare esattamente cosa tocca una risorsa. Clicca sullo sfondo del canvas per cancellare il focus. Ogni nodo porta un piccolo pulsante di apertura in una nuova scheda che salta direttamente all'editor di quella risorsa. La riga di statistiche in alto riporta il numero di nodi, archi, orfani e dangling references.
Group by package: avvolge i membri di ogni package in un riquadro tratteggiato etichettato, così puoi vedere un package come un'unità e individuare le risorse che ha tirato dentro per cascata.
Trappole: il grafo viene costruito su richiesta a partire dallo stato
corrente, usa il pulsante di refresh dopo aver modificato risorse altrove. Gli archi derivati
dagli script vengono trovati scansionando i corpi degli script per la forma di chiamata
letterale Modules.CallAsync("Name"), un module invocato tramite un nome calcolato
non mostrerà un arco. Una dangling reference vale sempre la pena di essere corretta; un orfano
spesso no, uno Script Module nuovo di zecca conta come orfano finché qualcosa non lo chiama.
Event Logs
L'audit trail per tutto ciò che la piattaforma ha eseguito, Business Events, Script Modules, Scheduled Events, sync di terze parti.
Workflow comune: apri Event Logs, imposta Status su Failed, fai clic su una voce. Il pannello di dettaglio mostra il payload (stato del record al momento, parametri passati), l'eccezione con stack trace, e i tempi.
Logs e Charts: la pagina ha due schede che condividono un'unica riga di filtri. Logs è la tabella qui sopra. Charts è una dashboard operativa sullo stesso filtro: il volume nel tempo impilato per stato, un andamento del tasso di successo, una ripartizione per source, gli events che falliscono di più e i più lenti, la durata media e massima nel tempo, una ripartizione per tipo di trigger, e una heatmap giorno della settimana per ora del giorno, con i totali principali in alto. Entrambe le schede condividono un intervallo di date Da / A (con i preset Ultime 1h / 24h / 7d); la scheda Charts sceglie automaticamente un intervallo di tempo adatto al periodo e può aggiornarsi a cadenza regolare per il monitoraggio in tempo reale.
Notifiche in tempo reale: l'icona campanella nella barra in alto dell'app admin emerge i failure non letti via SignalR.
Trappole: il log può crescere molto. Le voci vecchie sono tenute per sempre di default; se il disco è un problema, configura uno Scheduled Event che pota le voci più vecchie di N giorni.
User Management
Crea, invita e gestisce utenti; assegna ruoli realm. Fa da wrapper all'API REST admin di Keycloak.
Ruoli:
- admin - tutto; può gestire tutte le Business Units, utenti, ruoli, impostazioni di sistema.
- owner - gestisce le proprie Business Units, può modificare le pagine di amministrazione e di runtime.
- editor - può vedere e modificare le pagine di runtime, ma non cambiare la configurazione di schema/BE/event.
- user - read-only sulle pagine di runtime.
Trappole: declassare un utente da admin a metà sessione non lo butta fuori, il cambiamento si applica al prossimo refresh del token (tipicamente entro un minuto).
Business Units
Gruppi basati su Keycloak che controllano la visibilità sulle singole risorse. Vedi Concetti -> Business Units per il modello.
Workflow comune: crea una BU. Invita i membri per email e scegli il ruolo. Assegna le risorse andando su ogni BE / Page / Trigger oppure tramite il pannello degli item collegati della BU.
Assegnazioni in cascading: quando assegni una Page a una BU, l'assegnazione si propaga alla BE a cui la pagina si collega.
Trappole: la BU attiva di un utente è impostata nello switcher
BU della barra in alto e persiste in localStorage. I nuovi utenti partono
dalla loro prima BU; se una pagina lista è sospettosamente vuota, controlla quale BU è
attiva.
Database Backups
pg_dump notturno di tutti e tre i database, più uno ZIP dei
file del workspace. Trigger manuale / upload / restore dalla UI.
Workflow comune: Trigger now -> Download -> Upload -> Restore. Lo scheduling di default è notturno alle 03:00 UTC.
Limitazione del trial: i Backup sono disabilitati nell'ambiente trial.
Branding
Cambia il display name dell'app, il logo (chiaro + scuro), i colori, i font e il resto del tema Material UI, applicato a runtime, niente rebuild.
Workflow comune: imposta il nome dell'app, carica un logo chiaro e uno scuro (PNG/JPG/SVG, ≤ 10 MB), scegli i colori del brand nella scheda Theme. Salva: la tua sessione lo mostra subito, quelle degli altri al prossimo caricamento della pagina.
Tema: la scheda Theme è un editor completo di temi Material UI con anteprima
dal vivo in modalità chiara e scura: i colori del brand e ogni colore della palette per
modalità, i font (qualsiasi Google Font) e ogni stile di testo, il raggio degli angoli, la
spaziatura, le ombre, la velocità delle animazioni, lo stile di pulsanti, card e campi, i valori
predefiniti dei componenti e i loro override in JSON. Controlla il contrasto, parte da preset,
importa un tema da un'altra app Material UI ed esporta questo come codice. Show on the whole
app prova una bozza su tutte le schermate prima di salvare. Un workspace che non cambia mai
il suo tema mantiene esattamente l'aspetto integrato. In un repository il tema è la chiave
theme di workspace/branding.json.
Trappole: il logo è anche l'icona della scheda del browser e l'icona che un telefono mostra per l'app nella schermata home. La schermata home vuole un logo PNG, JPG o WebP; con un SVG mostra l'icona di Archestack.
Analytics
Google Analytics sull'app pubblicata, configurato nella pagina Analytics (Configuration). Il builder non viene mai tracciato.
Flusso tipico: incolla l'ID di misurazione GA4 (G-XXXXXXXXXX, in Amministrazione, Stream di dati di Google Analytics), spunta Track visits to the published app e salva. Ask visitors for consent first è attivo per impostazione predefinita: il tag di Google si carica solo dopo che un visitatore ha accettato il banner, come chiedono le regole UE sui cookie. Il testo del banner, i suoi pulsanti e un link alla tua informativa sulla privacy possono essere i tuoi.
Eventi dal codice di pagina: una volta caricato il tag, il codice di pagina può inviare eventi propri, come window.gtag?.("event", "sign_up"). Non viene inviato nulla finché il tracciamento è spento o il visitatore ha rifiutato.
Translations
Editor key/value per le stringhe UI dell'app. Aggiungi nuovi locale, sovrascrivi le label di default per cliente.
Workflow comune: scegli un locale, cerca una chiave, digita un nuovo valore, salva. Aggiorna l'app admin.
Trappole: le traduzioni sono caricate una volta per sessione; gli utenti su una tab vecchia non vedono i cambiamenti finché non aggiornano. Alcune label (intestazioni di colonna dalle BE) vengono dalla config della BE, non dalle traduzioni.
API chiamabili dagli script
L'esatta superficie che gli script possono chiamare. Verificata contro il sorgente, ogni metodo qui sotto esiste davvero.
Globali
Ogni script riceve queste variabili di livello superiore (nessun altro nome esiste nello scope dello script):
Entity-dynamicExpandoObject. Record corrente. Accedi ai campi conEntity.column_name. MutareEntityin un trigger Before* persiste le modifiche come parte della scrittura in corso.OldEntity-dynamicExpandoObject. Record precedente (trigger di update + delete). Read-only.Log-ILogger.Log.LogInformation("..."),Log.LogWarning(...),Log.LogError(...). Scrive sui log server e sulla voce dell'Event Log.Db-DbHelper. Accesso al database: un set tipizzato per ogni Business Entity, e gli helper non tipizzati (vedi sotto).Modules- chiama altri Script Modules. Ogni modulo è un metodo con i propri parametri:await Modules.OrdersOf(customer: "Acme"), controllato quando lo script viene compilato; il risultato èdynamic.await Modules.CallAsync("OrdersOf", new Dictionary<string, object?> { ["customer"] = "Acme" })chiama ancora un modulo per nome e ritornaTask<object?>.Pdf-ScriptPdfHelper. Renderizza un PDF Template salvato per nome:await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() })ritornabyte[];await Pdf.GenerateBase64Async(...)ritorna lo stesso payload codificato in base64. I parametri sono inoltrati al data script del template come variabili locali tipo stringa.Ocr-ScriptOcrHelper. Legge il testo stampato da un'immagine, in-process, senza servizio esterno e senza API key:await Ocr.ExtractAsync(byte[] image)ritorna unOcrExtraction.await Ocr.ExtractBase64Async(string)prende la stessa immagine codificata in base64 (un prefissodata:image/png;base64,è tollerato),await Ocr.ExtractTextAsync(byte[])ritorna solo il testo.Doc-ScriptDocHelper. Trova il documento in una foto e lo appiattisce:await Doc.CropAsync(byte[] image)ritorna unDocumentCrop.await Doc.CropBase64Async(string)prende una cattura in base64.Files-ScriptFilesHelper. La cartella di file propria del workspace, quella gestita dalla pagina Files:await Files.WriteAsync("invoices/1042.pdf", bytes),await Files.ReadAsync(path),ListAsync(folder),MoveAsync,DeleteAsync. Vedi Files.
Non c'è User, Http, Email,
Templates, o nessun altro globale.
Db - metodi single-row + di scrittura
Tutti async, tutti sull'oggetto di livello superiore Db:
await Db.GetAsync(string table, object id)- prende una riga per primary key. Ritornadynamic?(onullse non trovata).await Db.CreateAsync(string table, object values)- insert.valuespuò essere un oggetto anonimo o unDictionary<string, object?>. Fa scattare i trigger Before/After Create. Ritornaint(il nuovo ID).await Db.UpdateAsync(string table, object id, object values)- update per PK. Fa scattare i trigger Before/After Update.await Db.DeleteAsync(string table, object id)- delete per PK. Fa scattare il trigger Before Delete.
Db - query builder fluente
Parti con Db.From(table). Concatena filtri, poi un terminale:
.Where(string column, string op, object? value = null)- aggiunge una clausola WHERE. Operatori:=,!=/<>,>,>=,<,<=,LIKE,ILIKE,IN,IS NULL,IS NOT NULL. Concatena più chiamate.Where(...)per AND..OrderBy(string column, bool desc = false)- imposta ORDER BY. Solo uno consentito per query..Limit(int limit)- massimo di righe. Default è 1000.
Terminali (ciascuno esegue la query):
await ...ToListAsync()- ritornaList<dynamic>.await ...FirstAsync()- ritornadynamic?(primo match o null). Imposta temporaneamente il limit a 1.await ...CountAsync()- ritornaint.
Non c'è SumAsync, MaxAsync,
FirstOrDefaultAsync, o SingleAsync su Db.From. Per le
aggregazioni, recupera con ToListAsync() e riduci in C#, oppure usa i set
tipizzati qui sotto, che li hanno tutti.
Esempio pratico
// 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 come set tipizzati
Ogni Business Entity è anche un set tipizzato su Db, con il nome dell'entity
senza gli spazi: un'entity chiamata Service Order è Db.ServiceOrder.
Sotto c'è EF Core, quindi funziona tutto LINQ (Where, Include,
SumAsync, GroupBy, proiezioni), con una proprietà di navigazione per
ogni foreign key e per ogni join di una Business Entity. Le colonne sono proprietà PascalCase
con i loro tipi reali: planned_date è PlannedDate, un 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; - Nomi. Una tabella su cui non è costruita nessuna Business Entity riceve un set con il nome della tabella (
bank_accountèDb.BankAccount). Una colonna foreign key senza il suffisso_idè la proprietà di navigazione:customer_idèCustomer, e l'altro lato è il plurale,Customer.Orders. - Salvataggio.
await Db.SaveChangesAsync()scrive ogni riga aggiunta, modificata o rimossa come fannoCreateAsync,UpdateAsynceDeleteAsync, quindi i Business Events scattano e le colonne di audit vengono marcate, e una riga modificata invia solo le colonne cambiate. Non è un'unica transazione: le righe salvate prima di un errore restano salvate. Le scritture conExecuteUpdate,ExecuteDeleteo SQL raw vengono rifiutate. - Tipi. Un
timestamp with time zonesi legge come unDateTimeUTC, e anche un valore confrontato con esso deve essere UTC:DateTime.UtcNow. Le colonne di un tipo che il modello non mappa (array, enum, tipi geometrici) vengono omesse; leggile conDb.From. - Sempre aggiornato. Il modello viene ricostruito ogni volta che cambia una tabella o una Business Entity, quindi una colonna rimossa è un errore di compilazione sulla sua riga invece di un errore a runtime. Un deployment in sospeso elenca, prima che tu lo esegua, gli script tipizzati che smetterebbero di compilare.
Db.From e i metodi qui sopra continuano a funzionare accanto ai set tipizzati, e
l'editor di script e VS Code completano i set, le colonne e le proprietà di navigazione.
Modalità simulazione
Durante la simulazione del trigger (il pulsante Run Simulation su un Business
Event) e nella tab Test di uno Script Module, Db.SimulationMode è true.
Le scritture (CreateAsync, UpdateAsync, DeleteAsync e
SaveChangesAsync) controllano la tabella, la riga e i valori, registrano quello
che avrebbero fatto e non scrivono niente, quindi non scatta nemmeno alcun Business Event. Le
letture vedono i dati reali. Anche scritture, spostamenti ed eliminazioni tramite
Files vengono simulati, e mail e notifiche vengono registrate,
non inviate. È questo che rende Run Simulation sicuro da usare contro dati reali. Tutto
il resto che lo script fa da sé, invece, è reale: le chiamate HTTP, System.IO e
tutto ciò che fa un pacchetto NuGet, come una connessione a un
altro database.
Modules
await Modules.OrdersOf(customer: "Acme")- ogni Script Module è un metodo chiamato come il modulo senza spazi ("Orders of" diventaModules.OrdersOf), che prende i parametri che dichiara con i loro tipi, prima quelli obbligatori; un parametro opzionale omesso riceve il suo valore predefinito. Un nome scritto male o un valore del tipo sbagliato è un errore di compilazione sulla sua riga. RitornaTask<dynamic?>, quindi leggi il risultato senza cast, anche un tipo anonimo. Gli argomenti letti daEntitysono dynamic: fanne il cast ((int)Entity.company_id) perché la chiamata venga controllata in compilazione.await Modules.CallAsync(string moduleName, Dictionary<string, object?>? parameters = null)- chiama un altro Script Module per nome. I parametri diventano variabili tipizzate nello script del modulo chiamato. RitornaTask<object?>(qualunque cosa il modulo abbia ritornato).
Files - la cartella del workspace
Ogni workspace ha sul server una cartella propria, gestita dalla pagina Files
(Operations). Lì arrivano i caricamenti, e anche tutto ciò che uno script conserva: una fattura
generata, un'esportazione, un modello addestrato. Niente di tutto questo è salvato in un database,
e nessun percorso raggiunge la cartella di un altro workspace. I percorsi sono relativi e usano la
barra normale; un percorso con .., un'unità o un collegamento viene rifiutato.
// 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)ritorna i byte e lancia un'eccezione se il file non esiste;ReadTextAsynclegge testo UTF-8;ReadDataUriAsyncritornadata:image/png;base64,…per un'immagine in un PDF Template o in una mail.WriteAsync(path, bytes)eWriteTextAsynccreano le cartelle e sostituiscono un file esistente. Una scrittura riesce per intero o per niente.ListAsync(folder, recursive)elenca prima le cartelle; ogni voce haPath,Name,IsFolder,Size,ModifiedUtceContentType.MoveAsync(from, to)non sovrascrive mai;DeleteAsync(path)elimina un file, o una cartella con tutto il suo contenuto.PathOf(path)è il percorso assoluto su disco, per una libreria che ne vuole uno.
In una prova a vuoto (il pulsante Test) e in un'esecuzione dei test, scritture, spostamenti ed
eliminazioni non raggiungono mai la cartella: l'esecuzione li tiene per sé, quindi rileggere un
file appena scritto funziona, e li elenca insieme a ciò che ha registrato. La cartella
.archestack contiene i file propri della piattaforma (loghi, icone dei pacchetti,
allegati delle mail): gli script possono leggerla, mai scriverla. Un backup include la cartella
come archivio workspace-files accanto ai dump dei database.
Ocr e Doc - leggere una foto
Entrambi girano dentro il processo dell'API. Nessun servizio OCR da deployare, nessuna chiave da configurare, nessun costo per pagina.
OcrExtraction (quello che ritorna Ocr):
Text- ogni riga riconosciuta unita da newline, in ordine di lettura.Lines- per rigaText,Confidencee un bounding box:X,Y,Width,Height, piùRighteBottom. Pixel dell'immagine sorgente, origine in alto a sinistra.Confidence- media delle confidenze per riga, da 0 a 1.ElapsedMs,IsEmpty.
Usa i box. Leggere un documento significa localizzare un'etichetta e poi leggere ciò che le sta
accanto, cosa che il solo testo non può esprimere: trova la riga che contiene "Totale", poi
prendi la riga il cui intervallo Y la sovrappone e la cui X supera il
suo Right.
DocumentCrop (quello che ritorna Doc):
Found- se un contorno è stato davvero rilevato. Controllalo per primo.Image- byte PNG della pagina appiattita, o i byte originali intatti quandoFoundè false. Una foto mai ritagliata si legge comunque; una ritagliata sul quadrilatero sbagliato no, quindi il rilevatore preferisce rinunciare piuttosto che tirare a indovinare.Corners- il contorno nella foto sorgente, alto-sinistra, alto-destra, basso-destra, basso-sinistra.Width,Height,ElapsedMs.
I due si compongono nell'ordine ovvio, e poiché un ritaglio fallito restituisce l'originale,
concatenarli fa sempre qualcosa: var crop = await Doc.CropAsync(photo); var scan = await
Ocr.ExtractAsync(crop.Image);
Tre limiti da considerare in fase di progetto. Il modello incluso è PP-OCRv5
latin, quindi alfabeti occidentali e cifre, non CJK. Non è un lettore di PDF, una pagina PDF va
prima rasterizzata in immagine. E questo è OCR, non comprensione del documento: trasformare le
righe in fornitore, numero fattura e totale è logica che scrivi tu, e a volte sbaglierà, quindi
controlla line.Confidence e manda le letture basse a una persona invece di
scriverle dritte in tabella.
Attenzione al timeout dello script. Una pagina intera richiede un secondo o due e la prima
chiamata nel processo aggiunge circa un secondo per caricare i modelli. Il timeout di dieci
secondi non interrompe un await, quindi uno script che legge più pagine non ritorna
niente invece di fallire presto. Fai il lavoro multipagina in uno Scheduled Event, una pagina per
esecuzione.
Template expression (action dei Business Event)
La configurazione delle action usa parentesi {{ ... }}. La lista completa dei token validi:
{{ Entity.column_name }}/{{ Entity.id }}{{ OldEntity.column_name }}{{ now() }}/{{ getdate() }}- datetime UTC ISO 8601{{ today() }}- stringa di data{{ guid() }}/{{ newid() }}- GUID fresco{{ year() }},{{ month() }},{{ day() }},{{ timestamp() }}- secondi Unix{{ empty() }},{{ null() }}- Aggregati dentro un contesto di join:
{{ SUM(column) }},{{ AVG() }},{{ COUNT() }},{{ MIN() }},{{ MAX() }} - Aritmetica:
{{ Entity.quantity * Entity.price }}- valutata post-sostituzione viaDataTable.Compute()
Non c'è un {{ user.email }} o altro token di scope utente.