import { eq, desc, and, like, gte, lte, or, sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import mysql from "mysql2/promise"; import { InsertUser, users, localUsers, veilleItems, aapItems, appSettings, importLogs, InsertLocalUser, ideas, InsertIdea, rssFeeds, rssSettings, type InsertRssFeed, type InsertRssSettings, type RssFeed, type RssSettings, } from "../drizzle/schema"; import { ENV } from "./_core/env"; // eslint-disable-next-line @typescript-eslint/no-explicit-any let _db: any = null; export async function getDb() { if (!_db && process.env.DATABASE_URL) { try { const pool = mysql.createPool({ uri: process.env.DATABASE_URL, waitForConnections: true, connectionLimit: 10, enableKeepAlive: true, }); _db = drizzle(pool); } catch (error) { console.warn("[Database] Failed to connect:", error); _db = null; } } return _db; } // ─── Users (Manus OAuth) ───────────────────────────────────────────────────── 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) { console.warn("[Database] Cannot upsert user: database not available"); return; } const values: InsertUser = { openId: user.openId }; const updateSet: Record = {}; const textFields = ["name", "email", "loginMethod"] as const; for (const field of textFields) { const value = user[field]; if (value === undefined) continue; 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]; } // ─── Local Users ───────────────────────────────────────────────────────────── export async function getLocalUsers() { const db = await getDb(); if (!db) return []; return db .select({ id: localUsers.id, name: localUsers.name, username: localUsers.username, email: localUsers.email, role: localUsers.role, isActive: localUsers.isActive, createdAt: localUsers.createdAt, lastSignedIn: localUsers.lastSignedIn, }) .from(localUsers) .orderBy(desc(localUsers.createdAt)); } export async function createLocalUser(data: Omit) { const db = await getDb(); if (!db) throw new Error("DB unavailable"); await db.insert(localUsers).values(data); } export async function updateLocalUser(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("DB unavailable"); await db.update(localUsers).set(data).where(eq(localUsers.id, id)); } export async function deleteLocalUser(id: number) { const db = await getDb(); if (!db) throw new Error("DB unavailable"); await db.delete(localUsers).where(eq(localUsers.id, id)); } // ─── Veille Items ───────────────────────────────────────────────────────────── export interface VeilleFilters { typeVeille?: string; categorie?: string; niveau?: string; territoire?: string; search?: string; dateFrom?: Date; dateTo?: Date; page?: number; pageSize?: number; } export async function getVeilleItems(filters: VeilleFilters = {}) { const db = await getDb(); if (!db) return { items: [], total: 0 }; const { page = 1, pageSize = 50, ...f } = filters; const offset = (page - 1) * pageSize; const conditions = []; if (f.typeVeille) conditions.push(eq(veilleItems.typeVeille, f.typeVeille as "reglementaire" | "concurrentielle" | "technologique" | "generale")); if (f.categorie) conditions.push(like(veilleItems.categorie, `%${f.categorie}%`)); if (f.niveau) conditions.push(like(veilleItems.niveau, `%${f.niveau}%`)); if (f.territoire) conditions.push(like(veilleItems.territoire, `%${f.territoire}%`)); if (f.search) { conditions.push( or( like(veilleItems.titre, `%${f.search}%`), like(veilleItems.resume, `%${f.search}%`) ) ); } if (f.dateFrom) conditions.push(gte(veilleItems.datePublication, f.dateFrom)); if (f.dateTo) conditions.push(lte(veilleItems.datePublication, f.dateTo)); const where = conditions.length > 0 ? and(...conditions) : undefined; const [items, countResult] = await Promise.all([ db .select() .from(veilleItems) .where(where) .orderBy(desc(veilleItems.datePublication), desc(veilleItems.importedAt)) .limit(pageSize) .offset(offset), db .select({ count: sql`count(*)` }) .from(veilleItems) .where(where), ]); return { items, total: Number(countResult[0]?.count ?? 0) }; } export async function getVeilleDistinctValues() { const db = await getDb(); if (!db) return { categories: [], niveaux: [], territoires: [] }; const [cats, niveaux, territoires] = await Promise.all([ db.selectDistinct({ value: veilleItems.categorie }).from(veilleItems).where(sql`${veilleItems.categorie} IS NOT NULL`), db.selectDistinct({ value: veilleItems.niveau }).from(veilleItems).where(sql`${veilleItems.niveau} IS NOT NULL`), db.selectDistinct({ value: veilleItems.territoire }).from(veilleItems).where(sql`${veilleItems.territoire} IS NOT NULL`), ]); return { categories: cats.map((r) => r.value!).filter(Boolean).sort(), niveaux: niveaux.map((r) => r.value!).filter(Boolean).sort(), territoires: territoires.map((r) => r.value!).filter(Boolean).sort(), }; } // ─── AAP Items ──────────────────────────────────────────────────────────────── export interface AapFilters { categorie?: string; region?: string; departement?: string; search?: string; dateFrom?: Date; dateTo?: Date; clotureFrom?: Date; clotureTo?: Date; page?: number; pageSize?: number; } export async function getAapItems(filters: AapFilters = {}) { const db = await getDb(); if (!db) return { items: [], total: 0 }; const { page = 1, pageSize = 50, ...f } = filters; const offset = (page - 1) * pageSize; const conditions = []; if (f.categorie) conditions.push(eq(aapItems.categorie, f.categorie as "Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre")); if (f.region) conditions.push(like(aapItems.region, `%${f.region}%`)); if (f.departement) conditions.push(like(aapItems.departement, `%${f.departement}%`)); if (f.search) conditions.push(like(aapItems.titre, `%${f.search}%`)); if (f.dateFrom) conditions.push(gte(aapItems.datePublication, f.dateFrom)); if (f.dateTo) conditions.push(lte(aapItems.datePublication, f.dateTo)); if (f.clotureFrom) conditions.push(gte(aapItems.dateCloture, f.clotureFrom)); if (f.clotureTo) conditions.push(lte(aapItems.dateCloture, f.clotureTo)); const where = conditions.length > 0 ? and(...conditions) : undefined; const [items, countResult] = await Promise.all([ db .select() .from(aapItems) .where(where) .orderBy(desc(aapItems.datePublication), desc(aapItems.importedAt)) .limit(pageSize) .offset(offset), db .select({ count: sql`count(*)` }) .from(aapItems) .where(where), ]); return { items, total: Number(countResult[0]?.count ?? 0) }; } export async function getAapDistinctValues() { const db = await getDb(); if (!db) return { regions: [], departements: [] }; const [regions, departements] = await Promise.all([ db.selectDistinct({ value: aapItems.region }).from(aapItems).where(sql`${aapItems.region} IS NOT NULL`), db.selectDistinct({ value: aapItems.departement }).from(aapItems).where(sql`${aapItems.departement} IS NOT NULL`), ]); return { regions: regions.map((r) => r.value!).filter(Boolean).sort(), departements: departements.map((r) => r.value!).filter(Boolean).sort(), }; } // ─── App Settings ───────────────────────────────────────────────────────────── export async function getSetting(key: string): Promise { const db = await getDb(); if (!db) return null; const rows = await db.select().from(appSettings).where(eq(appSettings.key, key)).limit(1); return rows[0]?.value ?? null; } export async function getAllSettings(): Promise> { const db = await getDb(); if (!db) return {}; const rows = await db.select().from(appSettings); const map: Record = {}; for (const r of rows) { if (r.key && r.value !== null && r.value !== undefined) map[r.key] = r.value; } return map; } export async function setSetting(key: string, value: string): Promise { const db = await getDb(); if (!db) throw new Error("DB unavailable"); await db .insert(appSettings) .values({ key, value }) .onDuplicateKeyUpdate({ set: { value } }); } export async function setSettings(settings: Record): Promise { const db = await getDb(); if (!db) throw new Error("DB unavailable"); for (const [key, value] of Object.entries(settings)) { await db .insert(appSettings) .values({ key, value }) .onDuplicateKeyUpdate({ set: { value } }); } } // ─── Import Logs ────────────────────────────────────────────────────────────── export async function getImportLogs(limit = 50) { const db = await getDb(); if (!db) return []; return db .select() .from(importLogs) .orderBy(desc(importLogs.startedAt)) .limit(limit); } export async function getImportStats() { const db = await getDb(); if (!db) return { totalVeille: 0, totalAap: 0, lastImport: null, total: 0, success: 0, errors: 0, totalNewRows: 0 }; const [veilleCount, aapCount, lastLog, allLogs] = await Promise.all([ db.select({ count: sql`count(*)` }).from(veilleItems), db.select({ count: sql`count(*)` }).from(aapItems), db.select().from(importLogs).orderBy(desc(importLogs.startedAt)).limit(1), db.select().from(importLogs), ]); const total = allLogs.length; const success = allLogs.filter(l => l.status === 'success').length; const errors = allLogs.filter(l => l.status === 'error').length; const totalNewRows = allLogs.reduce((sum, l) => sum + (l.newRows ?? 0), 0); return { totalVeille: Number(veilleCount[0]?.count ?? 0), totalAap: Number(aapCount[0]?.count ?? 0), lastImport: lastLog[0] ?? null, total, success, errors, totalNewRows, }; } // ─── Boîte à idées ──────────────────────────────────────────────────────────── export async function createIdea(data: InsertIdea) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.insert(ideas).values(data); } export async function getAllIdeas() { const db = await getDb(); if (!db) return []; return db.select().from(ideas).orderBy(desc(ideas.createdAt)); } export async function getIdeasByUser(userId: number) { const db = await getDb(); if (!db) return []; return db.select().from(ideas).where(eq(ideas.userId, userId)).orderBy(desc(ideas.createdAt)); } export async function repondreIdea( id: number, reponseAdmin: string, reponduPar: string, statut: "ouvert" | "en_cours" | "resolu" | "ferme" ) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(ideas).set({ reponseAdmin, reponduPar, reponduAt: new Date(), statut, }).where(eq(ideas.id, id)); } export async function updateIdeaStatut(id: number, statut: "ouvert" | "en_cours" | "resolu" | "ferme") { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(ideas).set({ statut }).where(eq(ideas.id, id)); } // ─── Flux RSS ──────────────────────────────────────────────────────────────────────────────────── export async function getRssFeeds(): Promise { const db = await getDb(); if (!db) return []; return db.select().from(rssFeeds).orderBy(rssFeeds.name); } export async function getRssFeedById(id: number): Promise { const db = await getDb(); if (!db) return null; const rows = await db.select().from(rssFeeds).where(eq(rssFeeds.id, id)).limit(1); return rows[0] ?? null; } export async function createRssFeed(data: Omit): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(rssFeeds).values(data); return (result[0] as any).insertId as number; } export async function updateRssFeed(id: number, data: Partial>): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(rssFeeds).set(data).where(eq(rssFeeds.id, id)); } export async function deleteRssFeed(id: number): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(rssFeeds).where(eq(rssFeeds.id, id)); } export async function getRssSettings(): Promise { const db = await getDb(); if (!db) return null; const rows = await db.select().from(rssSettings).limit(1); if (rows.length > 0) return rows[0]; // Créer les paramètres par défaut si inexistants await db.insert(rssSettings).values({ fetchIntervalMinutes: 360, scheduledTime: "06:00", fetchMode: "scheduled", autoFetchEnabled: true, }); const newRows = await db.select().from(rssSettings).limit(1); return newRows[0] ?? null; } export async function saveRssSettings(data: Partial>): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const existing = await db.select().from(rssSettings).limit(1); if (existing.length > 0) { await db.update(rssSettings).set(data).where(eq(rssSettings.id, existing[0].id)); } else { await db.insert(rssSettings).values({ fetchIntervalMinutes: 360, scheduledTime: "06:00", fetchMode: "scheduled", autoFetchEnabled: true, ...data, }); } } // ─── Purge ─────────────────────────────────────────────────────────────────── export async function purgeVeilleItems(): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.delete(veilleItems); return (result as any).affectedRows ?? 0; } export async function purgeAapItems(): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.delete(aapItems); return (result as any).affectedRows ?? 0; }