Base de données
Initialiser le schéma, comprendre les tables créées et écrire ses premières requêtes.
Tout accès à la base passe par core/database. Cette page initialise le schéma et montre comment lire et écrire.
Vérifier la connexion
npx lrvo-db test ◆ Testing database connection...
✓ Database connection successfulEn cas d'échec, vérifier LRVO_DATABASE_URL et la politique TLS. Voir Configuration.
Créer les tables
npx lrvo-db init ◆ Initializing database...
✓ DB ready — 7 tables created, 0 skippedLa commande charge les modules installés, enregistre leurs permissions, applique le seed, puis appelle le createTables() de chacun.
Elle est idempotente : une table existante est comptée dans skipped, pas recréée. La relancer après chaque npm install @lrvo/module-X est donc sans risque, et c'est ce qu'il faut faire.
La base ne contient encore aucun compte. Au premier npm run dev, /auth affiche le formulaire de première configuration qui crée l'administrateur. Voir Créer le compte administrateur.
Tables du core
| Table | Contenu |
|---|---|
lrvo_auth_users | Comptes utilisateurs. |
lrvo_auth_accounts | Identifiants par provider. |
lrvo_auth_sessions | Sessions actives, avec IP et user-agent. |
lrvo_auth_verifications | Tokens de vérification, réinitialisation, changement d'adresse, invitation. |
lrvo_auth_roles | Rôles. |
lrvo_auth_role_permissions | Permissions par rôle. |
lrvo_auth_user_roles | Rôles par utilisateur. |
Chaque module ajoute les siennes, préfixées lrvo_<module>_.
Repartir de zéro
npx lrvo-db dropSupprime toutes les tables, core et modules compris, après confirmation explicite. Réservé au développement : il n'y a ni sauvegarde ni retour en arrière.
Lire et écrire
Helpers CRUD
Pour les opérations courantes.
import { create, find, findById, findOne, updateById, deleteById, count, exists }
from '@lrvo/core/database';
const post = await create('lrvo_blog_posts', { title, slug, content });
const posts = await find('lrvo_blog_posts', { published: true }, { orderBy: 'created_at DESC', limit: 10 });
const one = await findById('lrvo_blog_posts', id);
const bySlug = await findOne('lrvo_blog_posts', { slug });
const total = await count('lrvo_blog_posts', { published: true });Tout est paramétré, y compris les noms de tables et de colonnes.
Liste blanche
Dès que les données viennent d'une requête HTTP, passer allowedColumns.
const post = await create('lrvo_blog_posts', body, {
allowedColumns: ['title', 'slug', 'content'],
});Toute clé absente de la liste lève. Sans elle, un champ non prévu envoyé par le client (author_id, published) serait écrit tel quel.
Requêtes libres
import { query, queryOne, queryAll } from '@lrvo/core/database';
const rows = await queryAll(
'SELECT p.*, u.name AS author FROM lrvo_blog_posts p JOIN lrvo_auth_users u ON u.id = p.author_id WHERE p.published = $1',
[true],
);Toujours des paramètres $1, jamais de concaténation.
Transactions
import { transaction } from '@lrvo/core/database';
const post = await transaction(async (client) => {
const inserted = await client.query(
'INSERT INTO lrvo_blog_posts (title) VALUES ($1) RETURNING *',
[title],
);
await client.query(
'INSERT INTO lrvo_blog_tags (post_id, tag) VALUES ($1, $2)',
[inserted.rows[0].id, tag],
);
return inserted.rows[0];
});Rollback automatique si le callback lève. À utiliser dès que deux écritures doivent réussir ou échouer ensemble.
Gérer les erreurs
import { DatabaseError } from '@lrvo/core/database';
import { apiError } from '@lrvo/core/api';
try {
await create('lrvo_blog_posts', { slug });
} catch (error) {
if (error instanceof DatabaseError && error.code === '23505') {
return apiError('Bad Request', 'Ce slug est déjà utilisé. Choisir un autre.');
}
throw error;
}| Code | Signification |
|---|---|
23505 | Contrainte UNIQUE violée. |
23503 | Contrainte FOREIGN KEY violée. |
23502 | Contrainte NOT NULL violée. |
DatabaseError n'expose jamais de nom de table ni de fragment SQL : ces détails restent dans les journaux serveur.
Ajouter ses propres tables
Chaque module possède ses tables et leur cycle de vie. createTables() crée le schéma initial, de façon idempotente :
import { query, tableExists } from '@lrvo/core/database';
const TABLES = [
{ name: 'lrvo_blog_posts', sql: `CREATE TABLE lrvo_blog_posts (…)` },
];
export async function createTables() {
const created = [], skipped = [];
for (const t of TABLES) {
if (await tableExists(t.name)) { skipped.push(t.name); continue; }
await query(t.sql);
created.push(t.name);
}
return { created, skipped };
}Une fois ce schéma déployé, createTables() ne peut plus rien faire évoluer : il court-circuite sur tableExists(). Ajouter une colonne, poser une contrainte ou convertir des données passe par une migration versionnée, déclarée dans un tableau migrations :
export const migrations = [
{
id: '001-add-excerpt',
description: 'Ajoute la colonne excerpt.',
up: async (ctx) => {
await ctx.sql(`ALTER TABLE lrvo_blog_posts ADD COLUMN excerpt TEXT`);
},
},
];npx lrvo-db migrate applique ce qui manque, une transaction par migration, et retient ce qui a été fait dans lrvo_migrations. Voir Écrire une migration et Créer un module.
Garde-fous
update et deleteWhere exigent au moins une condition. Un UPDATE ou un DELETE sans WHERE lève au lieu de balayer la table.
Le pool est plafonné à 20 connexions. Une requête lente tenue trop longtemps épuise le pool et bloque tout le reste : préférer plusieurs requêtes courtes à une transaction longue.
Ne jamais instancier un client pg ailleurs. Le pool, la politique TLS et l'opacité des erreurs sont centralisés ici.
Étape suivante
Authentification crée le compte administrateur et protège une route.