Concepts fondamentaux

Le modèle mental derrière tout

Archestack repose sur un petit nombre de concepts fondamentaux que l'on retrouve partout dans le produit. Chacun existe pour résoudre un problème que vous auriez sinon résolu à la main, encore et encore, de manière légèrement différente à chaque fois. Passer quinze minutes ici vous fera gagner des heures de tâtonnements. Chaque section ci-dessous introduit un concept, propose un exemple concret et signale les erreurs courantes des nouveaux utilisateurs.

Le Schema est la source de vérité

Tout commence avec le Schema, une description JSON de chaque table, colonne, relation et index de votre application. Vous n'écrivez pas de SQL à la main. Vous concevez les tables dans le Schema Designer visuel, et Archestack convertit cela en vraies tables PostgreSQL quand vous déployez.

Pourquoi il existe

Un ERP traditionnel est un enchevêtrement de tables qui s'est développé de façon organique pendant une décennie. Archestack inverse le modèle : le schéma est un artefact versionné que vous éditez explicitement, comme du code source. Tous les autres outils de la plateforme, Business Entities, Pages, Events, Scripts, lisent depuis le schéma. Si une colonne n'existe pas dans le schéma, aucun autre outil n'en a connaissance.

Comment cela fonctionne en pratique

  • Auto-save vs deploy. Éditer dans Schema Designer enregistre automatiquement vos modifications (un indicateur apparaît en bas à gauche). L'enregistrement ne modifie pas la base de données, cela n'a lieu que lorsque vous créez un déploiement depuis Database Deployments et cliquez sur Deploy.
  • Le SQL généré est éditable. L'onglet Generated SQL du déploiement affiche le SQL de migration produit par la plateforme, CREATE TABLE, ALTER TABLE, etc. Vous pouvez le relire avant de déployer, et vous pouvez l'éditer directement si la version auto-générée n'est pas tout à fait correcte.
  • Hooks pré/post. Un déploiement embarque des scripts de pré-déploiement et de post-déploiement (PostgreSQL), utiles pour remplir une nouvelle colonne NOT NULL ou reconstruire un index dans un ordre contrôlé.
  • L'historique est permanent. Chaque déploiement est journalisé avec son statut (Draft / Executing / Succeeded / Failed) et le SQL exécuté.

Exemple concret

Vous ajoutez une colonne priority_score à deal. Dans Schema Designer, cela revient à sélectionner la table, ouvrir l'onglet Columns, taper le nom dans le champ "column_name", choisir INTEGER dans la liste déroulante Type, cliquer sur Add Column. L'indicateur d'auto-sauvegarde affiche brièvement "Auto saving..." puis "Saved". Cliquez ensuite sur Deploy dans la barre d'outils, vous arrivez sur une nouvelle page de configuration de déploiement. Ouvrez l'onglet Generated SQL :

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

Cliquez sur Deploy en haut à droite, terminé. La colonne existe désormais. Tant que vous ne l'ajoutez pas aussi au Business Entity deal (concept suivant), aucune page ni event ne la verra. Cette séparation est intentionnelle : les changements de schéma sont peu coûteux, les exposer constitue l'étape suivante, délibérée.

Pièges

  • Renommer une colonne la supprime puis la recrée. Le générateur de plan ne peut pas deviner votre intention. Si vous renommez, modifiez le SQL dans l'onglet Generated SQL pour utiliser RENAME COLUMN, ou faites un déploiement en plusieurs étapes : ajoutez la nouvelle colonne, écrivez un post-script pour copier les données, puis supprimez l'ancienne colonne dans un déploiement de suivi.
  • Ajouter une colonne NOT NULL à une table non vide échouera. Fournissez une valeur par défaut, ou ajoutez-la nullable, remplissez, puis passez-la en NOT NULL dans un second déploiement.
  • Les clés étrangères contraignent le comportement de suppression. Utilisez la liste déroulante On Delete sur chaque relation de façon réfléchie, les choix sont CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION.
  • Le snake_case est la convention. Le champ "Table name" du Schema Designer le rappelle ("Lowercase with underscores recommended"). Les outils fonctionnent aussi avec le PascalCase, mais tous les exemples de code de ce site supposent du snake_case pour les noms de tables et de colonnes.

Les Business Entities sont la vue organisée

Une table brute (customer) est rarement ce qu'un utilisateur final veut voir. Il veut "customer avec le total de sa dernière commande" ou "vehicle avec le nom et le téléphone du mécanicien assigné". Un Business Entity (BE) est une vue organisée d'une table source, augmentée de colonnes jointes depuis des tables liées. Les pages se construisent sur des BE, pas sur des tables brutes.

Pourquoi il existe

Deux raisons. D'abord, joindre les tables dans chaque page serait répétitif et incohérent, différentes pages joindraient les mêmes tables de manière légèrement différente et le comportement finirait par diverger. Le BE centralise la logique de jointure. Ensuite, on veut souvent plusieurs "vues" de la même table source pour des publics différents : la vue commerciale de customer montre le chiffre d'affaires et le dernier contact ; la vue support montre les tickets et les manquements de SLA. Différentes pages référencent différents BE, tous adossés à la même ligne dans customer.

Anatomie

  • Master Table - la table sur laquelle le BE est ancré. Chaque BE en a exactement une. La clé primaire du BE est celle de la table maître.
  • Label Column - une colonne dont la valeur identifie un enregistrement dans les listes et les sélecteurs (typiquement name, title ou similaire). Choisie à la création du BE.
  • Joins - ajoutées via le bouton Add Join. Chaque jointure a un type (INNER / LEFT / RIGHT), une table cible, des colonnes source/cible et une liste des colonnes cibles à exposer.
  • Colonnes agrégées - activez Aggregate Mode sur une jointure, puis choisissez une fonction d'agrégation par colonne : COUNT, SUM, AVG, MIN, MAX, COUNT DISTINCT. "Nombre de contacts ouverts sur cette Company" est une colonne agrégée typique.

Exemple concret

Table maître : vehicle. Colonnes natives exposées : vin, make, model, year. Jointure vers customer via vehicle.owner_id → customer.id, exposant customer.full_name sous owner_name. Seconde jointure en mode agrégat : nombre de lignes work_ordervehicle_id correspond et status != 'Closed', exposé sous open_work_order_count.

Une page liée à ce BE affiche une seule ligne de grille qui se présente comme un fait à plat alors qu'elle tire de trois tables, et elle le fait de manière cohérente sur l'ensemble de la plateforme.

Pièges

  • Les BE ne sont pas des frontières d'écriture par défaut. L'enregistrement depuis un formulaire lié à un BE écrit dans la table maître. Les colonnes jointes sont en général en lecture seule, pour éditer owner_name dans l'exemple ci-dessus, naviguez vers le Customer.
  • Utilisez Run Preview sans modération. Le bouton Run Preview dans l'éditeur de BE exécute la configuration de jointure et affiche de vraies lignes. Si une colonne de jointure revient vide, votre relation ou votre mappage de colonne est mal configuré.
  • N'allez pas trop en profondeur. Un BE qui joint trois tables de profondeur devient lent sur de gros jeux de données. Si vous vous retrouvez à joindre quatre ou cinq tables, c'est un signe qu'il vous faut plutôt un Script Module ou une vue base de données.

Les pages se configurent, ne se codent pas

Une Page est une UI d'exécution construite à partir d'un ou plusieurs Business Entities. Vous les configurez dans le Page Editor en définissant un nom, une route (par exemple /companies), un binding Business Entity et un switch Published. La plateforme génère automatiquement des sections à partir des colonnes du BE ; vous affinez la disposition à partir de là.

Anatomie

  • Onglet Visual - l'éditeur de disposition WYSIWYG. Sections, champs, onglets, boutons d'action.
  • Onglet Overview - configuration de la liste/grille : quelles colonnes apparaissent, tri par défaut, filtres.
  • Onglet Create - le formulaire de création. Diffère souvent légèrement du formulaire de détail (moins de champs, autres valeurs par défaut).
  • Onglet Entities - bindings BE supplémentaires (la page peut référencer plus d'un BE pour les onglets et les grilles liées).
  • Onglet Events - event triggers à portée de page.
  • Onglet JSON - JSON brut pour les utilisateurs avancés.

Types de widget de champ

Pour chaque champ du formulaire de détail, vous choisissez un Type dans un contrôle Select. Les options disponibles :

  • Text - saisie sur une ligne
  • Textarea - saisie multi-lignes
  • Number - saisie numérique
  • Date - sélecteur de date
  • Select - liste déroulante (à utiliser pour les champs de clé étrangère et les colonnes de type enum ; pour les champs FK, réglez l'autocomplete Entity sur le BE référencé pour que les utilisateurs choisissent par libellé)
  • Checkbox - booléen
  • Email - saisie texte avec validation d'email

Pour chaque champ, vous configurez également Label, Placeholder, Required, Read Only et Span (largeur de colonne de la grille).

Onglets et grilles liées

Le formulaire de détail d'une page prend en charge des onglets. Utilisez le bouton Add Tab pour en ajouter un. À l'intérieur d'un onglet, vous pouvez ajouter une section RelatedGrid qui affiche les lignes d'un autre BE filtrées par une jointure (par exemple toutes les lignes contactcompany_id = id de la company courante). C'est ainsi que l'on construit des pages maître-détail "Company → Contacts / Deals" sans aucune ligne de code.

Publication

Chaque page a un switch Published dans l'en-tête supérieur. OFF = brouillon (vous seul, dans l'éditeur, voyez vos modifications) ; ON = la page publiée apparaît sous Published Pages dans la barre latérale, et c'est ce que les utilisateurs finaux voient quand ils naviguent vers sa route.

Frontend Templates : quand "configurer, ne pas coder" ne suffit pas

Parfois, les templates configurés ne suffisent pas, vous voulez un widget personnalisé, une disposition inhabituelle ou une visualisation spécifique. Les Frontend Templates vous permettent d'écrire un morceau de TSX qui devient disponible dans le Page Editor comme composant réutilisable. Ils sont packageables et coexistent avec le reste de votre configuration. Voir Reference → Frontend Templates.

Pièges

  • Oublié de basculer le switch Published ? La page n'apparaîtra pas dans la barre latérale et les utilisateurs finaux naviguant vers sa route verront un 404. Facile à manquer car l'éditeur ne l'impose pas visiblement.
  • La largeur compte. Si votre BE a 25 colonnes, la grille sera inutilisable sur un laptop. Masquez les colonnes à faible valeur ; elles restent interrogeables, simplement non affichées.
  • Les onglets déclenchent des requêtes séparées. Une page avec cinq onglets fait des requêtes supplémentaires lorsque vous ouvrez une ligne de détail. Généralement acceptable, parfois problématique en performance sur de gros jeux de données.

Les Business Events font réagir les données

Un Business Event surveille un Business Entity à la recherche de changements et exécute des actions quand ses conditions sont satisfaites. Ce sont les règles "quand X arrive, fais Y" qui transforment une base de données passive en application opérationnelle.

Moments de déclenchement

Les options disponibles lorsque vous créez un event (vous pouvez en cocher plusieurs) :

  • BeforeCreate - se déclenche avant qu'un nouvel enregistrement ne soit sauvegardé. Le script peut muter Entity et les modifications sont persistées.
  • BeforeUpdate - se déclenche avant qu'un enregistrement existant ne soit sauvegardé. Même comportement de mutation.
  • AfterCreate - se déclenche après qu'un nouvel enregistrement a été validé.
  • AfterUpdate - se déclenche après qu'un enregistrement existant a été validé.
  • BeforeDelete - se déclenche avant qu'un enregistrement ne soit supprimé.
  • InitialValue - se déclenche à l'ouverture d'un nouveau formulaire ; définit les valeurs de champ par défaut.
  • OnSchedule - déclenché par Quartz sur un calendrier cron (voir Scheduled Events).
  • Manual - déclenché explicitement par un bouton d'action utilisateur sur une page.

Types d'action

Les types d'action effectifs (RuleActionKind) :

  • ExecuteScript - exécute un script C#. L'action la plus flexible ; c'est celle que vous utiliserez pour définir un champ, faire un calcul, appeler une API, n'importe quoi de sur-mesure. Le script reçoit Entity (mutez-le pour modifier l'enregistrement), OldEntity, Log, Db, Modules.
  • Validate - exécute un script C# qui retourne bool. true signifie que la validation échoue et la sauvegarde est bloquée avec le message d'erreur configuré. Met optionnellement en évidence certaines colonnes.
  • BlockOperation - arrête net l'opération avec un message. Pas de script.
  • CreateEntity - insère un enregistrement dans un autre BE. Les valeurs de champ prennent en charge les expressions de template.
  • UpdateEntity - met à jour les enregistrements d'un autre BE correspondant à un filtre de condition.
  • DeleteEntity - supprime les enregistrements correspondant à une condition (refuse de se déclencher sans condition, par sécurité).
  • SendEmail, SendWebhook, PublishEvent - définis dans le schéma comme améliorations futures ; actuellement journalisés et ignorés.

Expressions de template

La configuration des actions accepte des expressions de template entre accolades {{ ... }}. Les jetons disponibles :

  • {{ Entity.column_name }} - champ de l'enregistrement courant ({{ Entity.id }} fonctionne aussi)
  • {{ OldEntity.column_name }} - valeur précédente (triggers d'update uniquement)
  • {{ now() }} ou {{ getdate() }} - date-heure UTC ISO 8601
  • {{ today() }} - chaîne date
  • {{ guid() }} ou {{ newid() }} - GUID frais
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}
  • Agrégats dans un contexte de jointure : {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Arithmétique : {{ Entity.quantity * Entity.price }} - évaluée après substitution

Note : il n'y a pas de jeton {{ user.email }}, les scripts et les expressions de template n'ont pas accès à l'utilisateur courant. Si vous avez besoin de l'identité utilisateur dans une règle, stockez-la sur l'enregistrement au moment de l'insertion via le front-end et relisez-la depuis là.

Exemples concrets

  • Normaliser un champ à l'enregistrement. Trigger : Before Update sur Deal. Pas de conditions. Action : Execute Script avec Entity.title = ((string)Entity.title)?.Trim(); pour retirer les espaces superflus. (Vous n'avez pas besoin de règle pour created_at / updated_at / created_by / updated_by, la plateforme les estampille automatiquement.)
  • Bloquer les deals de faible montant marqués Won. Trigger : Before Update sur Deal. Condition : stage = 'Won' AND amount < 100. Action : Block Operation avec le message "Deals under €100 can't be marked Won, log them as Lost or delete them."
  • Créer une Note quand un Customer est signalé. Trigger : After Update sur Customer. Condition : flagged = true. Action : Create Entity sur note avec title = "Customer flagged at {{ now() }}", body = "Auto-generated for {{ Entity.full_name }}".

Simulez avant d'enregistrer

L'éditeur de trigger possède un onglet Simulate avec un bouton Run Simulation. Choisissez un enregistrement réel et la plateforme exécute les conditions et les actions dessus sans persister les modifications, les opérations d'écriture s'exécutent dans une transaction qui est ensuite annulée (rollback). Le panneau de sortie indique quelles conditions ont correspondu et ce qu'aurait fait chaque action. Utilisez-le avant d'activer un trigger sur des données de production.

Pièges

  • Triggers récursifs. Un trigger After Update qui utilise Update Entity pour mettre à jour le même enregistrement se redéclenchera. Utilisez plutôt Before Update + Execute Script + Entity.field = ... : cela mute l'écriture en cours au lieu d'en démarrer une nouvelle.
  • L'ordre compte entre les triggers. Plusieurs triggers sur le même event se déclenchent par ordre de priorité. Si le comportement dépend de l'ordre, définissez explicitement les priorités.
  • Les events désactivés apparaissent toujours dans la liste. Le switch Enabled est distinct de la configuration du trigger, vérifiez qu'il est activé avant de tester.

Les Script Modules vous offrent une porte de sortie

Pour tout ce qui ne peut pas s'exprimer comme une simple condition + action, calculer une remise complexe, appeler une API externe, générer un slug, faire une opération base de données en plusieurs étapes, vous écrivez un Script Module : un petit morceau de C# (compilé à l'exécution par Roslyn) que vous pouvez appeler depuis un Business Event, depuis un Scheduled Event ou depuis le front-end.

Ce à quoi les scripts ont accès

Chaque script reçoit ces variables globales (aucun autre nom n'existe dans la portée du script) :

  • Entity - l'enregistrement courant (dans les contextes de trigger), de type dynamic. Accédez aux champs avec Entity.column_name. Muter Entity dans un trigger Before* est la façon de "définir un champ".
  • OldEntity - l'enregistrement précédent (triggers d'update et de delete). Lecture seule.
  • Log - un ILogger pour les diagnostics. Log.LogInformation("...") écrit dans les logs serveur et dans l'entrée Event Log.
  • Db - le helper de base de données. Voir ci-dessous.
  • Modules - appeler d'autres Script Modules : await Modules.CallAsync("OtherModule", new Dictionary<string, object?> { ["param"] = value }).
  • Pdf - rend un PDF Template stocké par son nom : await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) retourne le byte[] rendu. Pdf.GenerateBase64Async(...) retourne le même payload encodé en base64.

Il n'y a pas de variable globale User, Http ni Email.

À quoi ressemble un 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 plupart des scripts font 5 à 30 lignes. Pas besoin d'expertise .NET, le contrôle de flux C# de base et le helper Db suffisent. IntelliSense est activé, et après une affectation depuis Db.GetAsync("customer", ...) l'éditeur connaît les colonnes de cette variable et propose des complétions.

Quand recourir à un script

  • Calculs qui s'étalent sur plusieurs tables (la valeur à vie d'un customer, le total de l'historique de réparations d'un vehicle).
  • Appels d'APIs externes (géocodage, prestataires de paiement, passerelles SMS).
  • Opérations en masse déclenchées par un Scheduled Event (recalcul nocturne, digest hebdomadaire).
  • Retourner au front-end des données trop personnalisées pour un Business Entity.
  • Tout cas où vous enchaîneriez sinon six Business Events bout à bout, c'est en général le signe qu'il fallait un script.

Pièges

  • Le helper Db est asynchrone. Toujours await. L'oublier compile mais retourne une Task, pas les données.
  • Il n'y a pas de FirstOrDefaultAsync ni de SumAsync. Les terminaux sont ToListAsync(), FirstAsync(), CountAsync(). Pour une somme, récupérez les lignes et faites la somme en C#.
  • Where prend trois arguments - colonne, opérateur, valeur. Les opérateurs incluent =, !=, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL.
  • Update est un appel de premier niveau : await Db.UpdateAsync("table", id, new { field = value }), pas chaîné depuis un Where.
  • Les scripts d'essai s'exécutent dans le même sandbox que ceux de production. Ne supposez pas que "ce n'est qu'un essai" implique des limites plus souples, votre script peut tout à fait saturer Postgres.

Les Scheduled Events tournent sur une horloge

Un Scheduled Event est le même système d'Event Trigger avec le timing OnSchedule : une expression cron Quartz déclenche la règle de manière récurrente plutôt qu'en réponse à un changement de données. Utilisez-le pour des résumés nocturnes, des rafraîchissements de données quotidiens, des nettoyages hebdomadaires, des rapports mensuels.

Format cron

Cron Quartz à 6 ou 7 champs (seconde minute heure jour-du-mois mois jour-de-semaine [année]). Quelques motifs courants :

  • 0 0 3 * * ? - tous les jours à 03:00.
  • 0 0 9 ? * MON-FRI - en semaine à 09:00.
  • 0 */15 * * * ? - toutes les 15 minutes.
  • 0 0 0 1 * ? - le premier de chaque mois à minuit.

Les heures sont à l'heure serveur (UTC sur la stack de production). La page Scheduled Events affiche la prochaine exécution pour que vous puissiez vérifier avant d'enregistrer.

Pièges

  • Les jobs longs gardent leur créneau. Si un job prend 25 minutes et que le cron est toutes les 15 minutes, l'exécution suivante est sautée : Quartz ne lancera pas deux instances du même job simultanément.
  • Les exécutions échouées ne sont pas réessayées. Elles sont journalisées dans Event Logs avec l'exception. Ajoutez une logique de réessai explicite à votre script si vous en avez besoin.

Les Packages regroupent la configuration pour la promotion

Une fois que vous avez construit quelque chose, un ensemble de tables, BE, pages, events et scripts, vous voudrez le déplacer de votre essai vers un environnement réel, ou le partager avec un autre partenaire. Un Package est un ZIP d'objets de configuration sélectionnés (avec cascade : ajouter une page tire automatiquement son BE, qui tire ses tables) que vous pouvez exporter et importer.

La cascade expliquée

Vous ajoutez les objets de haut niveau qui vous intéressent (généralement des pages). Le constructeur de package parcourt le graphe de dépendances et ajoute tout ce dont ces objets ont besoin : les BE auxquels les pages se lient, les tables que les BE référencent, les Frontend Templates que les pages embarquent, les Script Modules que les events appellent. Vous voyez la liste cascadée complète avant l'export et pouvez décocher ce que vous voulez omettre.

Livrer des données de seed

Les packages peuvent optionnellement inclure des données de ligne, utile pour livrer des tables de référence par défaut (pays, devises, enums de statut) ou des données de démo. Cochez Include data à l'export. À l'import, les lignes sont upsertées par clé primaire.

Pièges

  • Les deltas de schéma ne se déploient pas automatiquement à l'import. Si l'environnement d'import n'a pas les tables, l'import échoue. Déployez d'abord le schéma, puis importez le package.
  • Les identifiants sont par nom, pas par ID. Un BE appelé "customer" dans l'environnement source se lie à un BE appelé "customer" dans la destination, pas au même ID numérique. Un renommage d'un côté ou de l'autre casse le lien.

Les Business Units contrôlent qui voit quoi

Pour les configurations multi-tenant ou multi-équipes, les Business Units sont des groupes Keycloak qui peuvent être assignés à des ressources spécifiques. Un utilisateur de la Business Unit "Garage A" ne peut voir que les Pages, BE et Triggers assignés à Garage A.

Comment cela fonctionne

Chaque utilisateur appartient à une ou plusieurs BU (gérées dans User Management et Business Units). Le front-end suit la BU active de l'utilisateur et positionne l'en-tête HTTP X-Business-Unit sur chaque requête. Le back-end utilise cet en-tête pour filtrer les endpoints de liste, seules les ressources assignées à la BU active sont renvoyées.

Quand l'utiliser (et quand l'éviter)

  • Utilisez-les quand différentes équipes ou clients ont besoin de leur propre tranche de la même plateforme, les mécaniciens du Garage A ne devraient pas voir les ordres de travail du Garage B.
  • Ne les utilisez pas comme système de permissions. Les BU filtrent la visibilité, pas l'autorisation. Les rôles (admin / owner / editor / user) gèrent qui a le droit de faire quoi.
  • Ne les utilisez pas pour la sécurité au niveau ligne à l'intérieur d'un seul tenant. C'est le rôle des filtres sur le Business Entity, pas celui d'une BU.

Où cela mène ensuite

Avec ces concepts en main, le reste de la documentation suit naturellement. Le tutoriel de première application utilise chaque concept de cette page. La Reference va un niveau plus profond dans chaque outil, sur quoi cliquer, à quoi faire attention, quand y recourir.