Patterns Prisma ORM utilisés dans Lumira V2 — requêtes, transactions, schema navigation, et bonnes pratiques de performance.
Install
npx skillscat add tachfineamnay/lumirav2/prisma-database-patterns Install via the SkillsCat registry.
SKILL.md
Prisma & Database Patterns
Context
- ORM : Prisma 5+
- Base de données : PostgreSQL
- Package :
@packages/database - Schema :
packages/database/prisma/schema.prisma - Client : Généré dans
node_modules/.prisma/client
Import standard
// Dans un service NestJS
import { prisma } from '@packages/database';
// Ou via injection (si PrismaService est fourni)
constructor(private readonly prisma: PrismaService) {}Modèles principaux
| Modèle | Description |
|---|---|
User |
Compte utilisateur (client ou expert) |
UserProfile |
Données onboarding (birth, photos, context) |
Order |
Commande avec statut et contenu généré |
Product |
Catalogue des offres (initié → intégrale) |
Insight |
Carte d'insight AI pour un utilisateur |
SpiritualPath |
Plan 7 jours de l'utilisateur |
PathStep |
Étape individuelle du spiritual path |
ChatSession |
Session de chat avec Oracle Lumira |
Message |
Message individuel dans un ChatSession |
Notification |
Notification utilisateur |
AkashicRecord |
Registre akashique (niveau 4) |
Requêtes courantes
Trouver un user avec son profil
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
profile: true,
orders: {
where: { status: 'COMPLETED' },
orderBy: { createdAt: 'desc' },
take: 5,
},
},
});Filtrer les commandes
const orders = await prisma.order.findMany({
where: {
userId,
status: { in: ['PAID', 'PROCESSING', 'AWAITING_VALIDATION'] },
},
include: {
product: true,
user: { select: { email: true, id: true } },
},
orderBy: { createdAt: 'desc' },
});Pagination
const { page = 1, limit = 20 } = dto;
const [items, total] = await Promise.all([
prisma.order.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.order.count({ where: filters }),
]);
return { items, total, page, totalPages: Math.ceil(total / limit) };Transactions
Pour les opérations atomiques
// Après paiement réussi : créer order + profil en transaction
const result = await prisma.$transaction(async (tx) => {
const order = await tx.order.create({
data: {
userId,
productId,
status: 'PAID',
stripeSessionId,
},
});
// Met à jour le profil avec les données d'onboarding
await tx.userProfile.upsert({
where: { userId },
create: { userId, ...profileData },
update: { ...profileData },
});
return order;
});Transaction interactive (long)
// Pour les workflows complexes avec timeout
const result = await prisma.$transaction(
async (tx) => {
// Génération AI + sauvegarde peut prendre >5s
const reading = await vertexOracle.generate(data);
await tx.order.update({
where: { id: orderId },
data: {
status: 'COMPLETED',
generatedContent: reading,
},
});
// Créer les insights individuels
await tx.insight.createMany({
data: reading.insights.map((i) => ({
userId,
orderId,
...i,
})),
});
},
{ timeout: 30000 } // 30s pour les workflows AI
);Upsert Pattern
Utilisé pour UserProfile (1 seul par user) :
const profile = await prisma.userProfile.upsert({
where: { userId },
create: {
userId,
birthDate: dto.birthDate,
birthPlace: dto.birthPlace,
specificQuestion: dto.specificQuestion,
},
update: {
birthDate: dto.birthDate,
specificQuestion: dto.specificQuestion,
// Mise à jour partielle possible
},
});Select — Optimisation des requêtes
Toujours sélectionner uniquement les champs nécessaires pour éviter les données sensibles et améliorer les performances :
// ✅ Sélection ciblée
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
firstName: true,
role: true,
// Pas de password, pas de refreshToken
},
});
// ❌ Éviter (expose tous les champs)
const users = await prisma.user.findMany();Relations imbriquées
// Commande avec tout le contenu nécessaire pour le Sanctuaire
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
product: true,
user: {
include: { profile: true },
},
insights: {
orderBy: { category: 'asc' },
},
spiritualPath: {
include: { steps: { orderBy: { dayNumber: 'asc' } } },
},
},
});Migrations
# Créer et appliquer une migration
pnpm db:migrate
# Push schéma sans migration (dev rapide)
pnpm db:push
# Régénérer le client après changement de schéma
pnpm db:generate
# Interface Prisma Studio
pnpm db:studio
# Seed de données de test
pnpm db:seedGestion des erreurs Prisma
import { Prisma } from '@packages/database';
import { ConflictException, NotFoundException } from '@nestjs/common';
async function createUser(data: CreateUserDto) {
try {
return await prisma.user.create({ data });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2002') {
throw new ConflictException('Email déjà utilisé');
}
if (e.code === 'P2025') {
throw new NotFoundException('Enregistrement introuvable');
}
}
throw e;
}
}Codes d'erreur Prisma fréquents
| Code | Signification |
|---|---|
P2002 |
Violation contrainte unique |
P2025 |
Enregistrement non trouvé |
P2003 |
Violation clé étrangère |
P2016 |
Interprétation requête erreur |
Best Practices
| ✅ DO | ❌ DON'T |
|---|---|
Utiliser select pour limiter les données renvoyées |
Retourner des findMany() sans select sur des modèles sensibles |
Wrapper les opérations multi-tables en $transaction |
Faire des opérations liées sans transaction |
| Gérer les erreurs Prisma avec codes spécifiques | Laisser filtrer les erreurs Prisma brutes |
Utiliser upsert pour les modèles 1-to-1 |
delete + create séparément |
| Indexer les champs de filtre fréquents dans le schema | Ignorer les performances de requête |
Fermer Prisma proprement dans les tests (prisma.$disconnect()) |
Laisser des connexions ouvertes |