import { and, desc, eq, inArray, like, or, sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import { etablissements, InsertEtablissement, InsertMotCle, InsertPodcast, InsertUser, motsCles, podcastMotsCles, podcasts, users, } from "../drizzle/schema"; import { ENV } from "./_core/env"; let _db: ReturnType | null = null; export async function getDb() { if (!_db && process.env.DATABASE_URL) { try { _db = drizzle(process.env.DATABASE_URL); } catch (error) { console.warn("[Database] Failed to connect:", error); _db = null; } } return _db; } // ─── Users ───────────────────────────────────────────────────────────────────── export async function upsertUser(user: InsertUser): Promise { if (!user.openId) throw new Error("User openId is required for upsert"); const db = await getDb(); if (!db) return; const values: InsertUser = { openId: user.openId }; const updateSet: Record = {}; const textFields = ["name", "email", "loginMethod"] as const; textFields.forEach((field) => { const value = user[field]; if (value === undefined) return; const normalized = value ?? null; values[field] = normalized; updateSet[field] = normalized; }); if (user.lastSignedIn !== undefined) { values.lastSignedIn = user.lastSignedIn; updateSet.lastSignedIn = user.lastSignedIn; } if (user.role !== undefined) { values.role = user.role; updateSet.role = user.role; } else if (user.openId === ENV.ownerOpenId) { values.role = "admin"; updateSet.role = "admin"; } if (!values.lastSignedIn) values.lastSignedIn = new Date(); if (Object.keys(updateSet).length === 0) updateSet.lastSignedIn = new Date(); await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet }); } export async function getUserByOpenId(openId: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1); return result[0]; } export async function getAllUsers() { const db = await getDb(); if (!db) return []; return db.select().from(users).orderBy(desc(users.createdAt)); } export async function updateUserRole(userId: number, role: "user" | "admin") { const db = await getDb(); if (!db) return; // Protect immutable accounts const target = await db.select().from(users).where(eq(users.id, userId)).limit(1); if (target[0]?.immutable) throw new Error("Ce compte est immuable et ne peut pas être modifié"); await db.update(users).set({ role }).where(eq(users.id, userId)); } export async function getUserByUsername(username: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.username, username)).limit(1); return result[0]; } export async function createLocalUser(data: { username: string; passwordHash: string; name: string; role: "user" | "admin"; immutable?: boolean; openId: string; }) { const db = await getDb(); if (!db) throw new Error("DB not available"); await db.insert(users).values({ openId: data.openId, username: data.username, passwordHash: data.passwordHash, name: data.name, role: data.role, immutable: data.immutable ?? false, loginMethod: "local", lastSignedIn: new Date(), }).onDuplicateKeyUpdate({ set: { name: data.name, role: data.role }, }); } export async function updateUser( id: number, data: { name?: string; username?: string; passwordHash?: string; role?: "user" | "admin" } ) { const db = await getDb(); if (!db) throw new Error("DB not available"); const target = await db.select().from(users).where(eq(users.id, id)).limit(1); if (!target[0]) throw new Error("Utilisateur introuvable"); if (target[0].immutable) throw new Error("Ce compte est immuable et ne peut pas être modifié"); const updateData: Record = {}; if (data.name !== undefined) updateData.name = data.name; if (data.username !== undefined) updateData.username = data.username; if (data.passwordHash !== undefined) updateData.passwordHash = data.passwordHash; if (data.role !== undefined) updateData.role = data.role; if (Object.keys(updateData).length > 0) { await db.update(users).set(updateData).where(eq(users.id, id)); } } export async function deleteUser(id: number) { const db = await getDb(); if (!db) throw new Error("DB not available"); const target = await db.select().from(users).where(eq(users.id, id)).limit(1); if (!target[0]) throw new Error("Utilisateur introuvable"); if (target[0].immutable) throw new Error("Ce compte est immuable et ne peut pas être supprimé"); await db.delete(users).where(eq(users.id, id)); } export async function getUserById(id: number) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.id, id)).limit(1); return result[0]; } // ─── Établissements ──────────────────────────────────────────────────────────── export async function getAllEtablissements() { const db = await getDb(); if (!db) return []; return db.select().from(etablissements).orderBy(etablissements.nom); } export async function getEtablissementById(id: number) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(etablissements).where(eq(etablissements.id, id)).limit(1); return result[0]; } export async function createEtablissement(data: InsertEtablissement) { const db = await getDb(); if (!db) throw new Error("DB not available"); const result = await db.insert(etablissements).values(data); return result[0]; } export async function updateEtablissement(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("DB not available"); await db.update(etablissements).set(data).where(eq(etablissements.id, id)); } export async function deleteEtablissement(id: number) { const db = await getDb(); if (!db) throw new Error("DB not available"); await db.delete(etablissements).where(eq(etablissements.id, id)); } // ─── Mots-clés ───────────────────────────────────────────────────────────────── export async function getAllMotsCles() { const db = await getDb(); if (!db) return []; return db.select().from(motsCles).orderBy(motsCles.label); } export async function createMotCle(data: InsertMotCle) { const db = await getDb(); if (!db) throw new Error("DB not available"); await db.insert(motsCles).values(data); } export async function deleteMotCle(id: number) { const db = await getDb(); if (!db) throw new Error("DB not available"); await db.delete(motsCles).where(eq(motsCles.id, id)); } // ─── Podcasts ────────────────────────────────────────────────────────────────── export interface PodcastWithRelations { id: number; titre: string; resume: string; etablissementId: number; etablissementNom: string; audioUrl: string | null; audioKey: string | null; dureeSecondes: number | null; statut: "brouillon" | "publie"; auteurId: number | null; auteurNom: string | null; imageUrl: string | null; createdAt: Date; updatedAt: Date; motsCles: { id: number; label: string }[]; } export async function getPodcasts(opts?: { etablissementId?: number; motCleIds?: number[]; search?: string; statut?: "brouillon" | "publie"; auteurId?: number; }): Promise { const db = await getDb(); if (!db) return []; const conditions = []; if (opts?.etablissementId) conditions.push(eq(podcasts.etablissementId, opts.etablissementId)); if (opts?.statut) conditions.push(eq(podcasts.statut, opts.statut)); if (opts?.auteurId) conditions.push(eq(podcasts.auteurId, opts.auteurId)); if (opts?.search) { conditions.push( or( like(podcasts.titre, `%${opts.search}%`), like(podcasts.resume, `%${opts.search}%`) ) ); } const rows = await db .select({ id: podcasts.id, titre: podcasts.titre, resume: podcasts.resume, etablissementId: podcasts.etablissementId, etablissementNom: etablissements.nom, audioUrl: podcasts.audioUrl, audioKey: podcasts.audioKey, dureeSecondes: podcasts.dureeSecondes, statut: podcasts.statut, auteurId: podcasts.auteurId, auteurNom: users.name, imageUrl: podcasts.imageUrl, createdAt: podcasts.createdAt, updatedAt: podcasts.updatedAt, }) .from(podcasts) .leftJoin(etablissements, eq(podcasts.etablissementId, etablissements.id)) .leftJoin(users, eq(podcasts.auteurId, users.id)) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(desc(podcasts.createdAt)); // Charger les mots-clés pour chaque podcast const podcastIds = rows.map((r) => r.id); let keywordsMap: Map = new Map(); if (podcastIds.length > 0) { const kwRows = await db .select({ podcastId: podcastMotsCles.podcastId, motCleId: motsCles.id, label: motsCles.label, }) .from(podcastMotsCles) .innerJoin(motsCles, eq(podcastMotsCles.motCleId, motsCles.id)) .where(inArray(podcastMotsCles.podcastId, podcastIds)); for (const kw of kwRows) { if (!keywordsMap.has(kw.podcastId)) keywordsMap.set(kw.podcastId, []); keywordsMap.get(kw.podcastId)!.push({ id: kw.motCleId, label: kw.label }); } } let result = rows.map((r) => ({ ...r, etablissementNom: r.etablissementNom ?? "", auteurNom: r.auteurNom ?? null, motsCles: keywordsMap.get(r.id) ?? [], })); // Filtrer par mots-clés si demandé if (opts?.motCleIds && opts.motCleIds.length > 0) { result = result.filter((p) => opts.motCleIds!.some((id) => p.motsCles.some((mk) => mk.id === id)) ); } return result; } export async function getPodcastById(id: number): Promise { const results = await getPodcasts(); return results.find((p) => p.id === id); } export async function createPodcast( data: InsertPodcast, motCleIds: number[] ): Promise { const db = await getDb(); if (!db) throw new Error("DB not available"); const result = await db.insert(podcasts).values(data); const insertId = (result[0] as any).insertId as number; if (motCleIds.length > 0) { await db.insert(podcastMotsCles).values( motCleIds.map((motCleId) => ({ podcastId: insertId, motCleId })) ); } return insertId; } export async function updatePodcast( id: number, data: Partial, motCleIds?: number[] ) { const db = await getDb(); if (!db) throw new Error("DB not available"); await db.update(podcasts).set(data).where(eq(podcasts.id, id)); if (motCleIds !== undefined) { await db.delete(podcastMotsCles).where(eq(podcastMotsCles.podcastId, id)); if (motCleIds.length > 0) { await db.insert(podcastMotsCles).values( motCleIds.map((motCleId) => ({ podcastId: id, motCleId })) ); } } } export async function deletePodcast(id: number) { const db = await getDb(); if (!db) throw new Error("DB not available"); await db.delete(podcasts).where(eq(podcasts.id, id)); } export async function getPodcastStats() { const db = await getDb(); if (!db) return { total: 0, publies: 0, brouillons: 0, etablissements: 0 }; const [totalRow] = await db.select({ count: sql`count(*)` }).from(podcasts); const [publiesRow] = await db .select({ count: sql`count(*)` }) .from(podcasts) .where(eq(podcasts.statut, "publie")); const [etabRow] = await db.select({ count: sql`count(*)` }).from(etablissements); const total = Number(totalRow?.count ?? 0); const publies = Number(publiesRow?.count ?? 0); return { total, publies, brouillons: total - publies, etablissements: Number(etabRow?.count ?? 0), }; }