Aller au contenu

Guide — Interroger et modifier la base de données via des scripts Prisma jetables

Contexte : technique utilisée par Claude Code (session du 2026-07-10) pour répondre à des questions ponctuelles sur les données ProjetESM (compter des enfants, retrouver un compte utilisateur, réinitialiser un mot de passe...) sans passer par l'API HTTP ni par un client SQL externe.


Principe

Un script TypeScript jetable qui : 1. charge les variables d'environnement du backend (BackEnd/.env, notamment DATABASE_URL), 2. instancie PrismaClient (le client généré à partir de prisma/schema.prisma), 3. exécute une ou plusieurs requêtes, 4. affiche le résultat en console, 5. est supprimé juste après (fichier _tmp_*.ts, jamais commité).

Tout se lance depuis le dossier BackEnd/ du projet :

cd /home/kanmaber/projets/ProjetESM/BackEnd


Template de base

cat > prisma/_tmp_ma_requete.ts <<'EOF'
import { config } from 'dotenv';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
config({ path: path.resolve(__dirname, '../.env') });

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function main() {
  // 👉 requête ici
}

main().catch(console.error).finally(() => prisma.$disconnect());
EOF
npx tsx prisma/_tmp_ma_requete.ts
rm -f prisma/_tmp_ma_requete.ts

Pourquoi ce chemin précis : - Le fichier doit être sous BackEnd/ (ex: BackEnd/prisma/) pour que import { PrismaClient } from '@prisma/client' résolve vers le client généré dans BackEnd/node_modules. - config({ path: ... }) pointe explicitement vers BackEnd/.env — sans ça, Prisma ne connaît pas DATABASE_URL et échoue. - Le heredoc <<'EOF' (avec guillemets autour du premier EOF) empêche le shell d'interpréter les caractères spéciaux/accents du script — cf. règle projet "sed échoue sur l'UTF-8, utiliser des outils qui respectent l'encodage". - Si erreur @prisma/client did not initialize yet : lancer d'abord npx prisma generate.

Exécution exacte utilisée en session (comptage des maîtres coraniques par commune) :

cat > prisma/_tmp_count_maitres.ts <<'EOF'
import { config } from 'dotenv';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
config({ path: path.resolve(__dirname, '../.env') });

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function main() {
  const total = await prisma.maitreCoranique.count();
  const parCommune = await prisma.maitreCoranique.groupBy({
    by: ['communeCode'],
    _count: true,
  });
  console.log('TOTAL maîtres coraniques:', total);
  console.log('Par commune:', JSON.stringify(parCommune, null, 2));
}

main().catch(console.error).finally(() => prisma.$disconnect());
EOF
npx tsx prisma/_tmp_count_maitres.ts
rm -f prisma/_tmp_count_maitres.ts

Les noms de modèles (enfant, utilisateur, maitreCoranique, accompagnement...) et leurs champs correspondent exactement à ceux de BackEnd/prisma/schema.prisma — c'est la référence à consulter pour savoir ce qui est disponible sur chaque table.


LECTURE — exemples

Compter des enregistrements

await prisma.enfant.count();
await prisma.enfant.count({ where: { communeCode: 'KANDI' } });
await prisma.enfant.count({ where: { statutValidation: 'EN_ATTENTE' } });

Grouper / statistiques

await prisma.enfant.groupBy({
  by: ['statutValidation'],
  _count: true,
});

await prisma.enfant.groupBy({
  by: ['communeCode', 'genre'],
  _count: true,
});

Chercher un enregistrement précis

// Par identifiant unique (email, id...)
await prisma.utilisateur.findUnique({
  where: { email: 'x@y.com' },
  select: { id: true, email: true, actif: true, communeCode: true },
});

// Par critère non-unique (retourne le premier trouvé, ou null)
await prisma.enfant.findFirst({
  where: { prenom: 'A', nom: 'A' },
});

Lister avec filtres et relations

await prisma.enfant.findMany({
  where: { communeCode: 'KANDI', statutValidation: 'EN_ATTENTE' },
  select: {
    id: true,
    prenom: true,
    nom: true,
    idUtilisateur: true,
    utilisateur: { select: { email: true } }, // relation incluse
  },
  orderBy: { createdAt: 'desc' },
  take: 20, // limiter le nombre de résultats
});

ÉCRITURE — création, modification, suppression

⚠️ Ces opérations touchent la vraie base de données, pas un environnement isolé. Avant de les lancer : - vérifier qu'on cible bien le bon enregistrement (id ou critère précis, jamais un where trop large sur updateMany/deleteMany sans avoir d'abord fait un findMany de vérification), toujours regarder le résultat, jamais deviner) ; - pour deleteMany, updateMany ou toute suppression, s'assurer que le destinataire de la commande (Bernard) en confirme l'usage — ce ne sont pas des opérations à lancer par réflexe.

Créer un enregistrement

async function main() {
  const nouveauMaitre = await prisma.maitreCoranique.create({
    data: {
      nom: 'Dupont',
      prenom: 'Jean',
      communeCode: 'KANDI',
      idUtilisateur: 'cmqjhrhs30001nh5zrri51xf9', // id d'un utilisateur existant
    },
  });
  console.log('Créé:', nouveauMaitre);
}

Modifier un enregistrement

async function main() {
  const misAJour = await prisma.enfant.update({
    where: { id: 'cmrcnze2f0001u3cwjt5340bm' },
    data: { communeCode: 'NDALI' },
  });
  console.log('Modifié:', misAJour);
}

Exemple réel utilisé en session (réinitialisation de mot de passe, avec bcryptjs pour respecter le hachage attendu par ServiceHashage) :

import bcrypt from 'bcryptjs';
// ...
async function main() {
  const email = 'flow.test.backup@projetesm.test';
  const hash = await bcrypt.hash('TestFlow1234!', 10); // 10 rounds = même config que le backend

  await prisma.utilisateur.update({
    where: { email },
    data: { motDePasse: hash, loginAttempts: 0, lockedUntil: null },
  });
}

Supprimer un enregistrement

async function main() {
  // Toujours vérifier AVANT de supprimer
  const cible = await prisma.enfant.findUnique({ where: { id: 'xxx' } });
  console.log('À supprimer:', cible);

  if (cible) {
    await prisma.enfant.delete({ where: { id: 'xxx' } });
    console.log('Supprimé.');
  }
}

upsert (créer si absent, modifier si présent)

await prisma.profilPermission.upsert({
  where: { profilId_permissionId: { profilId: 'xxx', permissionId: 'yyy' } },
  update: {},
  create: { profilId: 'xxx', permissionId: 'yyy' },
});

Transaction (plusieurs opérations, tout ou rien)

await prisma.$transaction([
  prisma.enfant.update({ where: { id: 'xxx' }, data: { statutValidation: 'VALIDE' } }),
  prisma.mesureIndicateur.create({ data: { /* ... */ } }),
]);

Règles de prudence

  • Lecture (count, findMany, findUnique, groupBy) : sans risque, à utiliser librement.
  • Écriture (create, update, upsert) : vérifier l'identifiant/critère avant d'exécuter, relire le résultat affiché.
  • Suppression (delete, deleteMany) ou modification en masse (updateMany) : action difficile à annuler → toujours confirmer avec Bernard avant de lancer, jamais par automatisme.
  • Ne jamais coller le contenu de DATABASE_URL (mot de passe de la base) dans une sortie affichée ou un fichier commité — le script charge .env en interne sans l'afficher.
  • Toujours supprimer le script _tmp_*.ts après usage : ce ne sont pas des scripts métier destinés à rester dans le dépôt (contrairement à prisma/create-comptes-test.ts, prisma/list-comptes-test.ts, etc. qui sont des utilitaires réutilisables et commités).

Repères utiles

  • Schéma complet des tables et champs : BackEnd/prisma/schema.prisma
  • Règle de hachage des mots de passe : BackEnd/src/Modules/Auth/infrastructure/services/ServiceHashage.ts (bcrypt, 10 rounds)
  • Règle métier de longueur minimale de mot de passe (12 caractères) : BackEnd/src/Modules/Auth/infrastructure/services/validation/ServiceValidationAuth.ts (CONFIG_AUTH_DEFAUT.longueurMinMotDePasse)
  • Règle d'autorisation de modification des données de terrain (peutModifier) : BackEnd/src/shared/infrastructure/services/ServiceAutorisationDonnees.ts