Données et migrations
Deux modules complémentaires donnent aux applications PRISM une véritable couche de données sur le SQLite en cours de bext :
@bext-stack/framework/migrate— un constructeur de schéma et un **suivi, exécuteur de migration idempotent **. C'est la pièce qui manquait à bext : au lieu deCREATE TABLE IF NOT EXISTSparesseux à chaque requête (ou un appel hors bande)db push), vous définissez des migrations ordonnées et un grand livre_bext_migrationsenregistre ce qui a été exécuté.@bext-stack/framework/orm— un modèle/générateur de requêtes typé Eloquent-lite. Sûr par construction (chaque valeur passe par le modèle balisésql) et il renvoie des objets typés réels - il compresse les lignes du tableau de position que les mains du pont renvoient, de sorte que vous ne touchez plus jamais.columns/.rows.
Quand l'utiliser#
- Toute application possédant des tables (pas seulement appelant les magasins SDK).
- Vous souhaitez que les modifications du schéma soient révisables, ordonnées et appliquées une seule fois.
- Vous souhaitez des lectures/écritures tapées sans écriture manuelle de SQL ni compression manuelle des lignes.
Pour une requête ponctuelle, le modèle balisé sql est toujours présent en dessous. Recherchez le modèle lorsque la table est un élément de première classe que votre application lit et écrit.
Migrations#
import { migrate, type Migration } from "@bext-stack/framework/migrate";
const migrations: Migration[] = [
{
id: "0001_create_notes", // unique, sortable — runs in id order
up: (db) => db.schema.createTable("notes", (t) => {
t.id(); // INTEGER PRIMARY KEY AUTOINCREMENT
t.text("body").notNull();
t.integer("author_id").references("users");
t.timestamps(); // created_at + updated_at (epoch ms)
}),
down: (db) => db.schema.dropTable("notes"),
},
];
migrate({ db: ".bext/data/app.db" }, migrations); // applies pending only — idempotent
migrate() peut être appelé en toute sécurité en haut d'un chargeur/action ou au démarrage : il crée le grand livre, puis exécute uniquement les migrations dont id n'est pas encore enregistré.
| Fonction | Fait |
|---|---|
migrate(source, migrations) |
appliquer les migrations en attente dans l'ordre id ; renvoie { applied, skipped } |
rollback(source, migrations, steps?) |
inverser le dernier steps (par défaut 1), le plus récent en premier, via chaque down |
migrationStatus(source, migrations) |
{ id, applied, appliedAt? }[] pour chaque migration |
source est { db } (un chemin SQLite → le pont natif) ou { executor } (voir Essai ).
Générateur de schéma#
createTable(name, build) et dropTable(name) renvoient des chaînes SQL (utilisées par
db.schema.* dans le cadre d'une migration ou de manière autonome). Chaîne de constructeurs de colonnes :
t.id(); // auto-increment PK
t.text("email").notNull().unique();
t.integer("age");
t.integer("org_id").references("orgs"); // FK → orgs(id)
t.real("price").default(0);
t.boolean("active").default(true); // stored INTEGER 0/1
t.json("meta"); // TEXT
t.text("created").defaultRaw("CURRENT_TIMESTAMP");
t.timestamps(); // created_at + updated_at
Modèles et requêtes#
import { defineModel } from "@bext-stack/framework/orm";
interface Note { id: number; body: string; author_id: number; created_at: number; updated_at: number }
const Notes = defineModel<Note>({ table: "notes", db: ".bext/data/app.db", timestamps: true });
Notes.create({ body: "hello", author_id: 1 }); // INSERT → returns the row (id + timestamps filled)
Notes.find(3); // by primary key, or null
Notes.all(); // every row
Notes.count();
Notes.where("author_id", "=", 1) // chainable query builder
.where("body", "like", "%draft%")
.orderBy("created_at", "desc")
.limit(20)
.all();
Notes.update(3, { body: "edited" }); // by PK; bumps updated_at when timestamps:true
Notes.delete(3);
Notes.query().whereIn("id", [1, 2, 3]).delete(); // bulk
| Membre | Retours |
|---|---|
create(data) |
la ligne insérée (relue par PK) |
find(id) / first() |
T | null |
all() / where(col, op, val) / query() |
lignes / un QueryBuilder<T> |
count() |
number |
update(id, data) / delete(id) |
nombre de personnes affectées |
raw(sql\…`)` |
trappe de secours, rangées tapées T |
Générateur de requêtes : .where(col, op, val), .whereIn(col, vals), .orderBy(col, dir),
.limit(n), .offset(n), puis .all() / .first() / .count() / .update(data) /
.delete(). Les opérateurs sont une liste verte fixe ( =, !=, <, <=, >, >=,
like, not like ); .toSql() affiche la requête compilée. Chaque valeur est un paramètre lié — Notes.where("body", "=", userInput) ne peut jamais injecter.
Essai#
L'exécution est connectable via un Executor, de sorte que le code de modèle/migration identique s'exécute dans l'isolat (pont natif par défaut) ou sur n'importe quel SQLite dans un test unitaire - par exemple. bun:sqlite :
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
const executor = {
rows: (text, params) => db.query(text).all(...params),
run: (text, params) => { const r = db.run(text, ...params); return { changes: r.changes, lastInsertRowid: Number(r.lastInsertRowid) }; },
};
migrate({ executor }, migrations);
const Notes = defineModel<Note>({ table: "notes", executor });
Essayez-le#
Le Démo SQLite CRUD en direct exécute une véritable migration et un modèle typé : ajoutez et supprimez des notes et regardez-les persister. Source dans sites/demo/src/app/examples/db-crud/page.tsx.
Voir aussi#
- Pont ( SQLite, etc.) — le
dbQuerynatif sur lequel se superposent. - Validation — valide l'entrée avant qu'elle n'atteigne une écriture de modèle.
- Boîte à outils de candidature — le reste de la couche d'application TypeScript.