Modèle balisé `sql` (sûr contre l'injection)
sql est un modèle balisé (tagged template) emprunté à Bun.SQL : il produit une paire { text, params } sûre par construction. Chaque valeur interpolée {value} devient un paramètre lié — elle n'est jamais concaténée dans le SQL. Les identifiants passent par sql.id() ; la composition par fragments imbriqués, sql.join et sql.if. Tout est du JS pur (identique sur V8 et QuickJS) ; seul { text, params } traverse vers Rust.
Astuce
L'injection est impossible par construction : une valeur interpolée finit toujours dans
params, jamais dans text. Regardez "1; DROP TABLE users;--" ci-dessous — la charge utile reste un seul paramètre lié, inerte. Et grâce au seam de dialecte (SQLite ? par défaut), la même API fonctionnera sur Postgres ($1..$N) le jour où bext aura un client PG, sans changer une ligne de vos requêtes.Compilé en direct
Chaque exemple appelle .compile() pendant le rendu de cette page — aucune base de données n'est touchée. La colonne de gauche est la source ; celle de droite, le { text, params } réellement produit.
Scalaires → paramètres liés
const q = sql`SELECT * FROM users WHERE id = ${42} AND name = ${"o'brien"}`;
q.compile();{"text":"SELECT * FROM users WHERE id = ? AND name = ?","params":[42,"o'brien"]}Tentative d'injection → paramètre inerte
const evil = "1; DROP TABLE users;--";
const q = sql`SELECT * FROM t WHERE id = ${evil}`;
q.compile(); // the injection stays a single bound param{"text":"SELECT * FROM t WHERE id = ?","params":["1; DROP TABLE users;--"]}Composition via sql.join
const filters = [sql`active = ${true}`, sql`age > ${18}`];
const q = sql`SELECT * FROM t WHERE ${sql.join(filters, " AND ")}`;
q.compile(); // nested fragments share one param counter{"text":"SELECT * FROM t WHERE active = ? AND age > ?","params":[true,18]}Identifiant + tableau IN (…)
const q = sql`SELECT * FROM ${sql.id("user table")} WHERE id IN (${[1, 2, 3]})`;
q.compile(); // identifier quoted; array expands to ?, ?, ?{"text":"SELECT * FROM \"user table\" WHERE id IN (?, ?, ?)","params":[1,2,3]}// src/app/.../page.tsx — injection-safe `sql` tagged template (Bun.SQL steal).
// One ergonomic tagged template → an injection-safe { text, params } pair.
import { sql } from "@bext-stack/framework";
// Interpolated values become BOUND PARAMS — never concatenated into SQL:
const q = sql`SELECT * FROM users WHERE id = ${id} AND name = ${name}`;
q.compile();
// → { text: "SELECT * FROM users WHERE id = ? AND name = ?", params: [id, name] }
// Identifiers (table/column names) — quoted, not parameterised:
sql`SELECT * FROM ${sql.id("my table")}`;
// Arrays expand to ?, ?, ? for IN (...):
sql`SELECT * FROM t WHERE id IN (${[1, 2, 3]})`;
// Composition — nested fragments, sql.if, sql.join share one param counter:
const filters = [sql`active = ${true}`, sql`age > ${minAge}`];
sql`SELECT * FROM t WHERE ${sql.join(filters, " AND ")}`;
sql`SELECT * FROM t ${sql.if(name, sql`WHERE name = ${name}`)}`;
// Same query, run for real over the in-process SQLite bridge:
import { database } from "@bext-stack/framework";
const db = database(".bext/data/app.db");
const rows = db.all(sql`SELECT * FROM users WHERE id = ${id}`);
// Dialect seam — SQLite (?) by default, Postgres ($1..$N) when bext gets a PG client:
import { POSTGRES_DIALECT } from "@bext-stack/framework";
q.compile(POSTGRES_DIALECT); // → { text: "... id = $1 ...", params: [...] }