Files
veille-reglementaire/server/db.ts

363 lines
13 KiB
TypeScript

import { eq, desc, and, like, gte, lte, or, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import {
InsertUser,
users,
localUsers,
veilleItems,
aapItems,
appSettings,
importLogs,
InsertLocalUser,
ideas,
InsertIdea,
} from "../drizzle/schema";
import { ENV } from "./_core/env";
let _db: ReturnType<typeof drizzle> | 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 (Manus OAuth) ─────────────────────────────────────────────────────
export async function upsertUser(user: InsertUser): Promise<void> {
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<string, unknown> = {};
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<InsertLocalUser, "id" | "createdAt" | "updatedAt">) {
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<InsertLocalUser>) {
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<number>`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<number>`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<string | null> {
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<Record<string, string>> {
const db = await getDb();
if (!db) return {};
const rows = await db.select().from(appSettings);
const map: Record<string, string> = {};
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<void> {
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<string, string>): Promise<void> {
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<number>`count(*)` }).from(veilleItems),
db.select({ count: sql<number>`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));
}