Concetti chiave

Il modello mentale dietro a tutto

Archestack ha un piccolo numero di concetti chiave che si ripresentano ovunque nel prodotto. Ognuno esiste per risolvere un problema che altrimenti risolveresti a mano, più e più volte, in modi leggermente diversi. Dedicare quindici minuti qui ti farà risparmiare ore passate a girarci intorno. Ogni sezione qui sotto introduce un concetto, ti dà un esempio pratico e segnala gli errori tipici dei nuovi utenti.

Lo Schema è la fonte di verità

Tutto parte dallo Schema, una descrizione JSON di ogni tabella, colonna, relazione e indice della tua applicazione. Non scrivi SQL a mano. Progetti le tabelle nello Schema Designer visuale, e Archestack le converte in vere tabelle PostgreSQL quando fai il deploy.

Perché esiste

Un ERP tradizionale è una giungla di tabelle cresciuta in modo organico per un decennio. Archestack capovolge il modello: lo schema è un artefatto versionato che modifichi esplicitamente, come fosse codice sorgente. Ogni altro strumento della piattaforma, Business Entities, Pages, Events, Scripts, legge dallo schema. Se una colonna non esiste nello schema, nessun altro strumento la conosce.

Come funziona in pratica

  • Auto-save vs deploy. Modificare in Schema Designer salva automaticamente le tue modifiche (un indicatore compare in basso a sinistra). Salvare non cambia il database, questo succede solo quando crei un deployment da Database Deployments e fai clic su Deploy.
  • L'SQL generato è modificabile. La tab Generated SQL del deployment mostra l'SQL di migration prodotto dalla piattaforma, CREATE TABLE, ALTER TABLE, ecc. Puoi leggerlo prima di fare il deploy e puoi modificarlo direttamente se la versione auto-generata non è proprio quella giusta.
  • Hook pre/post. Un deployment porta con sé script pre-deployment e post-deployment (PostgreSQL), utili per fare il backfill di una nuova colonna NOT NULL o per ricostruire un indice in un ordine controllato.
  • La storia è permanente. Ogni deployment viene registrato con il suo stato (Draft / Executing / Succeeded / Failed) e l'SQL eseguito.

Esempio pratico

Aggiungi una colonna priority_score a deal. In Schema Designer significa selezionare la tabella, aprire la tab Columns, digitare il nome nel campo "column_name", scegliere INTEGER dal dropdown Type e fare clic su Add Column. L'indicatore di auto-save mostra brevemente "Auto saving..." poi "Saved". A questo punto fai clic su Deploy nella toolbar, arrivi su una pagina di configurazione deployment nuova. Apri la tab Generated SQL:

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

Fai clic su Deploy in alto a destra, fatto. La colonna ora esiste. Finché non la aggiungi anche alla Business Entity deal (concetto successivo), nessuna pagina o event la vedrà. Quella separazione è voluta: i cambi di schema costano poco, esporli è il passo successivo, fatto in modo deliberato.

Trappole

  • Rinominare una colonna equivale a un drop + ricreazione. Il plan generator non legge nel pensiero. Se rinomini, modifica l'SQL nella tab Generated SQL per usare RENAME COLUMN, oppure fai un deployment multi-step: aggiungi la nuova colonna, scrivi un post-script che copi i dati, poi elimina la vecchia colonna in un deployment successivo.
  • Aggiungere una colonna NOT NULL a una tabella non vuota fallisce. Fornisci un default, oppure aggiungila come nullable, fai il backfill, poi alterala a NOT NULL in un secondo deployment.
  • Le foreign key vincolano il comportamento di delete. Usa il dropdown On Delete su ogni relazione in modo deliberato: le opzioni sono CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION.
  • Snake_case è la convenzione. Il campo "Table name" di Schema Designer lo suggerisce ("Lowercase with underscores recommended"). Gli strumenti funzionano anche con PascalCase, ma ogni esempio di codice in questo sito assume snake_case per i nomi di tabelle e colonne.

Le Business Entities sono la vista curata

Una tabella raw (customer) raramente è quello che un utente finale vuole vedere. Vuole "il cliente con il totale del suo ultimo ordine" o "il veicolo con il nome e il telefono del meccanico assegnato". Una Business Entity (BE) è una vista curata di una tabella sorgente più le colonne ottenute in join da tabelle correlate. Le pagine sono costruite sulle BE, non sulle tabelle raw.

Perché esistono

Due motivi. Primo, fare join di tabelle in ogni pagina sarebbe ripetitivo e incoerente: pagine diverse farebbero il join delle stesse tabelle in modi leggermente diversi e il comportamento divergerebbe. La BE centralizza la logica del join. Secondo, spesso vuoi più "viste" della stessa tabella sorgente per pubblici diversi: la vista sales di customer mostra i ricavi e l'ultimo contatto; la vista support mostra i ticket e gli SLA violati. Pagine diverse referenziano BE diverse, tutte basate sulla stessa riga in customer.

Anatomia

  • Master Table - la tabella su cui la BE è ancorata. Ogni BE ne ha esattamente una. La primary key della BE è la primary key della master table.
  • Label Column - una colonna il cui valore identifica un record nelle liste e nei picker (tipicamente name, title o simile). Si sceglie quando crei la BE.
  • Joins - si aggiungono tramite il pulsante Add Join. Ogni join ha un tipo (INNER / LEFT / RIGHT), una tabella target, colonne source/target e l'elenco delle colonne target da esporre.
  • Colonne aggregate - attiva Aggregate Mode su un join, poi scegli una funzione aggregata per colonna: COUNT, SUM, AVG, MIN, MAX, COUNT DISTINCT. "Numero di contatti aperti su questa Company" è una tipica colonna aggregata.

Esempio pratico

Master table: vehicle. Colonne native esposte: vin, make, model, year. Join a customer via vehicle.owner_id -> customer.id, che espone customer.full_name come owner_name. Secondo join in modalità aggregata: conteggio delle righe work_order dove vehicle_id corrisponde e status != 'Closed', esposto come open_work_order_count.

Una pagina collegata a questa BE renderizza una singola riga di griglia che sembra un dato piatto, anche se viene letta da tre tabelle, e lo fa in modo coerente in tutta la piattaforma.

Trappole

  • Di default le BE non sono confini di scrittura. Salvare da un form collegato a una BE scrive sulla master table. Le colonne ottenute in join sono di solito read-only: per modificare owner_name nell'esempio sopra, vai al Customer.
  • Usa Run Preview senza farti problemi. Il pulsante Run Preview nell'editor BE esegue la configurazione dei join e mostra righe reali. Se una colonna ottenuta in join torna vuota, la relazione o il mapping delle colonne è configurato male.
  • Non spingerti troppo in profondità. Una BE che fa join di tre tabelle inizia a diventare lenta su dataset grandi. Se ti ritrovi a fare join di quattro o cinque tabelle, è il segnale che ti serve uno Script Module custom o una database view.

Le pagine si configurano, non si scrivono

Una Page è una UI runtime costruita a partire da una o più Business Entities. La configuri nel Page Editor impostando un nome, una route (es. /companies), un binding a una Business Entity e un toggle Published. La piattaforma auto-genera le sezioni dalle colonne della BE; tu raffini il layout da lì.

Anatomia

  • Tab Visual - l'editor di layout WYSIWYG. Sezioni, campi, tab, pulsanti azione.
  • Tab Overview - configurazione lista/griglia: quali colonne compaiono, ordinamento di default, filtri.
  • Tab Create - il form di nuovo record. Spesso differisce leggermente dal form di dettaglio (meno campi, default diversi).
  • Tab Entities - binding BE aggiuntivi (la pagina può referenziare più di una BE per tab e griglie correlate).
  • Tab Events - event trigger con scope di pagina.
  • Tab JSON - JSON raw per power user.

Tipi di widget di campo

Per ogni campo nel form di dettaglio scegli un Type da un controllo Select. Le opzioni effettive:

  • Text - input a riga singola
  • Textarea - input multi-riga
  • Number - input numerico
  • Date - selettore di data
  • Select - dropdown (usalo per i campi foreign key e per le colonne enum; per i campi FK, imposta l'autocomplete Entity sulla BE referenziata così gli utenti scelgono per label)
  • Checkbox - booleano
  • Email - input testo con validazione email

Per ogni campo configuri anche Label, Placeholder, Required, Read Only e Span (larghezza della colonna nella griglia).

Tab e griglie correlate

Il form di dettaglio di una pagina supporta le tab. Usa il pulsante Add Tab per aggiungerne una. Dentro una tab puoi aggiungere una sezione RelatedGrid che mostra righe di un'altra BE filtrate da un join (es. tutte le righe contact dove company_id = current company's id). È così che costruisci pagine master-detail "Company -> Contacts / Deals" senza scrivere codice.

Pubblicazione

Ogni pagina ha uno Switch Published nell'header in alto. OFF = bozza (solo tu nell'editor vedi le tue modifiche); ON = la pagina pubblicata compare sotto Published Pages nella sidebar ed è ciò che gli utenti finali vedono quando navigano alla sua route.

Frontend Templates: quando "configura, non programmare" non basta

A volte i template configurati non bastano: vuoi un widget custom, un layout insolito o una visualizzazione specifica. I Frontend Templates ti permettono di scrivere un pezzo di TSX che diventa disponibile nel Page Editor come componente riutilizzabile. Sono pacchettizzabili e vivono insieme al resto della tua configurazione. Vedi Reference -> Frontend Templates.

Trappole

  • Hai dimenticato di spostare lo switch Published? La pagina non comparirà nella sidebar e gli utenti finali che navigano alla sua route vedranno un 404. Facile da non notare perché l'editor non lo impone visivamente.
  • La larghezza conta. Se la tua BE ha 25 colonne, la griglia sarà inutilizzabile su un laptop. Nascondi le colonne di basso valore; restano interrogabili, semplicemente non sono visualizzate.
  • Le tab attivano query separate. Una pagina con cinque tab fa query aggiuntive quando apri una riga di dettaglio. Di solito va bene, a volte può essere un problema di performance su dataset grandi.

I Business Events fanno reagire i dati

Un Business Event osserva una Business Entity in attesa di modifiche ed esegue action quando le sue condizioni sono soddisfatte. Sono le regole "quando succede X, fai Y" che trasformano un database passivo in un'applicazione funzionante.

Timing dei trigger

Le opzioni effettive quando crei un event (puoi spuntarne più di una):

  • BeforeCreate - scatta prima che un nuovo record sia salvato. Lo script può mutare Entity e le modifiche vengono persistite.
  • BeforeUpdate - scatta prima che un record esistente sia salvato. Stesso comportamento di mutazione.
  • AfterCreate - scatta dopo che un nuovo record è stato committato.
  • AfterUpdate - scatta dopo che un record esistente è stato committato.
  • BeforeDelete - scatta prima che un record sia eliminato.
  • InitialValue - scatta quando viene aperto un form nuovo; imposta i valori di default dei campi.
  • OnSchedule - fatto scattare da Quartz su una pianificazione cron (vedi Scheduled Events).
  • Manual - fatto scattare esplicitamente da un pulsante azione utente su una pagina.

Tipi di action

I tipi effettivi di action (RuleActionKind):

  • ExecuteScript - esegue uno script C#. L'action più flessibile; è quella che usi quando vuoi impostare un campo, fare un calcolo, chiamare un'API, qualsiasi cosa custom. Lo script riceve Entity (mutalo per cambiare il record), OldEntity, Log, Db, Modules.
  • Validate - esegue uno script C# che ritorna bool. true significa che la validazione fallisce e il salvataggio viene bloccato con il messaggio di errore configurato. Opzionalmente evidenzia colonne specifiche.
  • BlockOperation - blocca l'operazione in modo netto con un messaggio. Niente script.
  • CreateEntity - inserisce un record in un'altra BE. I valori dei campi supportano template expression.
  • UpdateEntity - aggiorna i record in un'altra BE che corrispondono a un filtro di condizione.
  • DeleteEntity - elimina i record che corrispondono a una condizione (rifiuta di scattare se manca, per sicurezza).
  • SendEmail, SendWebhook, PublishEvent - definite nello schema come miglioramenti futuri; al momento vengono solo loggate e saltate.

Template expression

La configurazione delle action accetta template expression tra parentesi {{ ... }}. I token effettivi:

  • {{ Entity.column_name }} - campo del record corrente (funziona anche {{ Entity.id }})
  • {{ OldEntity.column_name }} - valore precedente (solo trigger di update)
  • {{ now() }} o {{ getdate() }} - datetime UTC ISO 8601
  • {{ today() }} - stringa di data
  • {{ guid() }} o {{ newid() }} - GUID nuovo
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}
  • Aggregati dentro un contesto di join: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Aritmetica: {{ Entity.quantity * Entity.price }} - valutata dopo la sostituzione

Nota: non c'è un token {{ user.email }}, gli script e le template expression non hanno accesso all'utente corrente. Se ti serve l'identità utente in una regola, salvala sul record al momento dell'insert dal front end e leggila da lì.

Esempi pratici

  • Normalizzare un campo al salvataggio. Trigger: Before Update su Deal. Nessuna condizione. Action: Execute Script con Entity.title = ((string)Entity.title)?.Trim(); per rimuovere gli spazi superflui. (Non ti serve una regola per created_at / updated_at / created_by / updated_by, la piattaforma li marca automaticamente.)
  • Impedisci che i deal di importo basso vengano marcati come Won. Trigger: Before Update su Deal. Condizione: stage = 'Won' AND amount < 100. Action: Block Operation con messaggio "I Deals sotto i 100 euro non possono essere marcati Won, registrali come Lost o eliminali."
  • Crea una Note quando un Customer viene flaggato. Trigger: After Update su Customer. Condizione: flagged = true. Action: Create Entity su note con title = "Customer flagged at {{ now() }}", body = "Auto-generated for {{ Entity.full_name }}".

Simula prima di salvare

L'editor dei trigger ha una tab Simulate con un pulsante etichettato Run Simulation. Scegli un record reale e la piattaforma esegue le condizioni e le action contro di esso senza persistere modifiche: le operazioni di scrittura girano in una transazione che viene fatta rollback. Il pannello di output mostra quali condizioni hanno fatto match e cosa avrebbe fatto ogni action. Usalo prima di abilitare un trigger sui dati di produzione.

Trappole

  • Trigger ricorsivi. Un trigger After Update che usa Update Entity per aggiornare lo stesso record si rilancerà da solo. Usa invece Before Update + Execute Script + Entity.field = ..., che muta la scrittura in corso invece di iniziarne una nuova.
  • L'ordine conta tra i trigger. Più trigger sullo stesso event scattano in ordine di priorità. Se il comportamento dipende dall'ordine, imposta le priorità in modo esplicito.
  • Gli event disabilitati compaiono comunque nella lista. Lo Switch Enabled è separato dalla config del trigger, verifica che sia su ON prima di testare.

Gli Script Modules ti danno vie d'uscita

Per qualsiasi cosa che non si possa esprimere come una semplice condizione + action, calcolare uno sconto complesso, chiamare un'API esterna, generare uno slug, fare un'operazione database multi-step, scrivi uno Script Module: un piccolo pezzo di C# (compilato a runtime da Roslyn) che puoi chiamare da un Business Event, da uno Scheduled Event o dal front end.

A cosa hanno accesso gli script

Ogni script riceve questi globali (nessun altro nome esiste nello scope dello script):

  • Entity - il record corrente (nei contesti di trigger) come dynamic. Accedi ai campi con Entity.column_name. Mutare Entity in un trigger Before* è il modo in cui "imposti un campo".
  • OldEntity - il record precedente (trigger di update e delete). Read-only.
  • Log - un ILogger per la diagnostica. Log.LogInformation("...") scrive sui log del server e sulla voce dell'Event Log.
  • Db - l'helper del database. Vedi sotto.
  • Modules - chiama altri Script Modules: await Modules.CallAsync("OtherModule", new Dictionary<string, object?> { ["param"] = value }).
  • Pdf - renderizza un PDF Template salvato per nome: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) ritorna il byte[] renderizzato. Pdf.GenerateBase64Async(...) ritorna lo stesso payload codificato in base64.

Non c'è un globale User, Http o Email.

Com'è fatto uno script

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

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

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

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

La maggior parte degli script sta tra le 5 e le 30 righe. Non ti serve esperienza .NET, ti serve il flusso di controllo di base del C# e l'helper Db. IntelliSense è attivo e, dopo aver assegnato da Db.GetAsync("customer", ...), l'editor conosce le colonne di quella variabile e ti offre i completamenti.

Quando usare uno script

  • Calcoli che attraversano più tabelle (il valore a vita di un customer, il totale dello storico riparazioni di un veicolo).
  • Chiamate ad API esterne (geocoding, payment provider, gateway SMS).
  • Operazioni bulk attivate da uno Scheduled Event (ricalcolo notturno, digest settimanale).
  • Restituire al front end dati troppo custom per una Business Entity.
  • Qualsiasi cosa per cui altrimenti concateneresti sei Business Events, di solito è il segnale che volevi uno script.

Trappole

  • L'helper Db è async. Sempre await. Dimenticarlo compila ma ti ritorna un Task, non i dati.
  • Non esiste FirstOrDefaultAsyncSumAsync. I terminali sono ToListAsync(), FirstAsync(), CountAsync(). Per una somma, recupera le righe e fai la somma in C#.
  • Where prende tre argomenti - colonna, operatore, valore. Gli operatori includono =, !=, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL.
  • Update è una chiamata di primo livello: await Db.UpdateAsync("table", id, new { field = value }), non concatenata a un Where.
  • Gli script del trial girano nella stessa sandbox della produzione. Non dare per scontato che "tanto è solo un trial" voglia dire limiti più larghi, il tuo script può comunque martellare Postgres.

Gli Scheduled Events girano a orologio

Uno Scheduled Event è lo stesso sistema di Event Trigger con il timing OnSchedule: un'espressione cron in stile Quartz fa scattare la regola su base ricorrente invece che in risposta a un cambio di dati. Usalo per riassunti notturni, refresh giornalieri dei dati, pulizie settimanali, report mensili.

Formato cron

Cron in stile Quartz a 6 o 7 campi (second minute hour day-of-month month day-of-week [year]). Qualche pattern comune:

  • 0 0 3 * * ? - ogni giorno alle 03:00.
  • 0 0 9 ? * MON-FRI - giorni feriali alle 09:00.
  • 0 */15 * * * ? - ogni 15 minuti.
  • 0 0 0 1 * ? - il primo di ogni mese a mezzanotte.

Gli orari sono ora del server (UTC sullo stack di produzione). La pagina Scheduled Events mostra la prossima ora di scatto così puoi fare un sanity-check prima di salvare.

Trappole

  • I job lunghi tengono occupato il loro slot. Se un job impiega 25 minuti e il cron è ogni 15 minuti, l'esecuzione successiva viene saltata, Quartz non fa scattare due istanze dello stesso job in concorrenza.
  • Le esecuzioni fallite non riprovano. Loggano su Event Logs con l'eccezione. Aggiungi logica di retry esplicita al tuo script se ne hai bisogno.

I Packages impacchettano la configurazione per la promozione

Una volta costruito qualcosa, un set di tabelle, BE, pagine, event e script, vorrai spostarlo dal tuo trial a un ambiente reale, o condividerlo con un altro partner. Un Package è uno ZIP di oggetti di configurazione selezionati (con cascading: prendere una pagina porta automaticamente la sua BE, che porta le sue tabelle) che puoi esportare e importare.

Il cascading spiegato

Aggiungi gli oggetti di livello superiore che ti interessano (di solito le pagine). Il package builder percorre il grafo delle dipendenze e aggiunge tutto ciò che quegli oggetti richiedono: le BE a cui le pagine si collegano, le tabelle che le BE referenziano, i Frontend Templates che le pagine incorporano, gli Script Modules che gli event chiamano. Vedi la lista completa con il cascading prima di esportare e puoi togliere la spunta a qualsiasi cosa tu voglia omettere.

Spedire i seed data

I Packages possono opzionalmente includere dati di righe, utile per spedire tabelle di lookup di default (paesi, valute, enum di stato) o dati demo. Spunta Include data in fase di export. In fase di import le righe vengono fatte upsert per primary key.

Trappole

  • I delta di schema non si deployano automaticamente all'import. Se all'ambiente che importa mancano delle tabelle, l'import fallisce. Fai prima il deploy dello schema, poi importa il package.
  • Gli identificatori sono per nome, non per ID. Una BE chiamata "customer" nell'ambiente sorgente si collega a una BE chiamata "customer" nella destinazione, non allo stesso ID numerico. Rinomine da una delle due parti rompono il link.

Le Business Units controllano chi vede cosa

Per setup multi-tenant o multi-team, le Business Units sono gruppi Keycloak che possono essere assegnati a risorse specifiche. Un utente nella Business Unit "Garage A" può vedere solo Pages, BE e Trigger assegnati a Garage A.

Come funziona

Ogni utente appartiene a una o più BU (gestite in User Management e Business Units). Il front end tiene traccia della BU attiva dell'utente e imposta l'header HTTP X-Business-Unit su ogni richiesta. Il backend usa quell'header per filtrare gli endpoint di lista: tornano solo le risorse assegnate alla BU attiva.

Quando usarle (e quando no)

  • Usale quando team o clienti diversi hanno bisogno della loro fetta della stessa piattaforma: i meccanici di Garage A non dovrebbero vedere i work order di Garage B.
  • Non usarle come sistema di permessi. Le BU filtrano la visibilità, non l'autorizzazione. I ruoli (admin / owner / editor / user) gestiscono chi può fare cosa.
  • Non usarle per la sicurezza a livello di riga all'interno di un singolo tenant. Quello è un compito per i filtri sulla Business Entity, non per una BU.

Dove si va da qui

Con questi concetti in mano, il resto della documentazione viene di conseguenza. Il tutorial first-app usa ogni concetto di questa pagina. La Reference va un livello più in profondità su ogni strumento: cosa cliccare, a cosa stare attenti, quando usarlo.