Entités

Une application PRISM dispose de deux façons de travailler avec des données relationnelles :

  1. defineEntity : un générateur d'écrans d'administration piloté par configuration. Il associe une entité à un CRUD complet, adossé aux procédures de la Platform API.
  2. Requêtes SQL : le tagged template sql du framework, pour les requêtes que vous écrivez vous-même.

defineEntity

Note

defineEntity fait partie du kit @bext-stack/manage, et non du framework (@bext-stack/framework). C'est la brique des écrans de console et d'administration.

defineEntity associe une entité à des écrans liste / détail / création / édition et au CRUD correspondant, adossés aux procédures tRPC de la Platform API.

Les procédures de lecture sont typées : list.proc et getById.proc n'acceptent que des noms du catalogue de procédures du kit, donc une faute de frappe y est une erreur de compilation. Le type de la réponse de list.proc est aussi transmis aux adaptateurs rows et total. Les noms de crud.create, crud.update et crud.delete sont de simples chaînes : vérifiez-les vous-même.

Configuration

Les lectures et les écritures passent par le kit, qui appelle lui-même la Platform API : vous n'avez pas de client à écrire. L'adresse de l'API, le tenant et le site viennent de configureManage(...), appelé une seule fois dans un module que chaque route importe pour son effet de bord :

// src/lib/manage-setup.ts
import { configureManage } from "@bext-stack/manage/config";

configureManage({
  appId: "<app-id>",
  cmBase: "…",
  host: "…",
  origin: "…",
  tenantId: "…",
  siteId: "…",
});
Option Rôle
appId L'identifiant de votre application.
cmBase L'adresse de base de la Platform API.
host Le nom d'hôte du site administré, envoyé à l'API pour qu'elle résolve le bon tenant et le bon site.
origin L'origine publique de host, utilisée pour le contrôle d'origine à la connexion.
tenantId, siteId Le tenant et le site que ces écrans administrent.
loginPath Facultatif. La page vers laquelle les écrans redirigent sans session. Par défaut /manage/login.
brandName Facultatif. Le nom affiché dans la barre latérale et le titre des pages.

Le ré-export mince

Pour publier une entité générée, ré-exportez le module d'entité du kit depuis un fichier de route, après l'import du module de configuration :

// src/app/manage/posts/page.tsx
import "../../../lib/manage-setup";
export { loader, default } from "@bext-stack/manage/entities/posts";

Une entité fournit un module par écran qu'elle possède (certaines n'ont qu'une liste) :

Module Écran Route type
@bext-stack/manage/entities/<entité> Liste …/page.tsx
@bext-stack/manage/entities/<entité>.detail Détail …/[id]/page.tsx
@bext-stack/manage/entities/<entité>.form Création et édition …/new/page.tsx, …/[id]/edit/page.tsx

Exemple : la base de connaissances du helpdesk

L'entité « articles » de la base de connaissances du helpdesk est une configuration defineEntity reliée à ces procédures :

Procédure Usage
helpdeskKb.getArticles Lister les articles (list.proc), filtrés par recherche, statut et visibilité
helpdeskKb.getArticleById Lire un article (getById.proc)
helpdeskKb.createArticle, helpdeskKb.updateArticle, helpdeskKb.deleteArticle Créer, modifier et supprimer (crud)
helpdeskKb.getCategories Remplir la liste des catégories du formulaire (formOptions)

Un extrait de cette configuration :

import { defineEntity } from "@bext-stack/manage/lib/entity";

export const helpdeskKbArticles = defineEntity({
  name: "article",
  namePlural: "articles",
  icon: "book-open",
  basePath: "/manage/helpdesk/knowledge-base",
  list: {
    proc: "helpdeskKb.getArticles",
    rows: (r) => (Array.isArray(r) ? r : []),
    total: (r) => (Array.isArray(r) ? r.length : 0),
    input: (q) => ({
      search: q.search || undefined,
      status: q.filters.status || undefined,
    }),
  },
  getById: { proc: "helpdeskKb.getArticleById" },
  crud: {
    create: "helpdeskKb.createArticle",
    update: "helpdeskKb.updateArticle",
    delete: "helpdeskKb.deleteArticle",
  },
  // colonnes, filtres et champs de formulaire omis
});

Voir Helpdesk.

Requêtes SQL

Pour écrire vos propres requêtes, le framework exporte un helper SQL en tagged template :

import { sql, database } from "@bext-stack/framework";

const db = database("data.sqlite");
const posts = db.all(sql`SELECT id, title FROM posts WHERE author_id = ${authorId}`);
Export Rôle
sql Construit une requête à partir d'un tagged template. sql.id(name) (identifiant entre guillemets doubles), sql.raw(text), sql.if(cond, frag) et sql.join(frags, sep) composent des fragments.
database(path) Lie un fichier de base SQLite. Retourne des fonctions synchrones : all (lignes, sous forme d'objets), get (première ligne ou null), run ({ changes, last_insert_rowid }) et compile ({ text, params }, sans exécution).
SQLITE_DIALECT, POSTGRES_DIALECT Styles de paramètres (? ou $1) du texte compilé, passés en second argument de database ou à la méthode compile d'une requête. SQLite par défaut ; l'exécution passe toujours par SQLite.

Un tableau interpolé devient une liste de paramètres, pratique pour IN (...). Les fragments se combinent sans perdre la numérotation des paramètres :

const rows = db.all(sql`
  SELECT id, title FROM ${sql.id("posts")}
  WHERE id IN (${ids})
  ${sql.if(authorId, sql`AND author_id = ${authorId}`)}
  ORDER BY created_at ${sql.raw(desc ? "DESC" : "ASC")}
`);

const conds = [sql`status = ${status}`, sql`author_id = ${authorId}`];
const count = db.get(sql`SELECT COUNT(*) AS n FROM posts WHERE ${sql.join(conds, " AND ")}`);

const { changes } = db.run(sql`UPDATE posts SET status = ${"archived"} WHERE id = ${id}`);
Note

Chaque valeur interpolée avec ${...} devient un paramètre lié : elle n'est jamais concaténée au texte SQL. Passez vos valeurs ainsi plutôt que de construire la chaîne de requête à la main. sql.raw insère du texte sans échappement : réservez-le à des valeurs fixes comme ASC / DESC. Quand sql.if reçoit une condition fausse, il n'insère rien : mettez le mot-clé (AND, WHERE) dans le fragment lui-même.

Le client createSdk offre aussi sdk.db.all, sdk.db.get et sdk.db.exec avec des paramètres ? (voir la Présentation du SDK). Ces appels sont asynchrones ; exec retourne { changes, lastInsertId }. Chaque paramètre est envoyé sous forme de chaîne : les nombres sont convertis, et null ou undefined deviennent une chaîne vide. Écrivez NULL dans le texte SQL quand il vous faut une vraie valeur nulle.

Et ensuite

Page Ce qu'elle couvre
Modèle de données Le schéma par tenant et les modèles clés
Helpdesk L'entité de la base de connaissances en contexte
Loaders & Actions Où s'exécutent les lectures et écritures
KV Store Stocker un petit état clé/valeur