feat: initial commit - veille-reglementaire v1.0.0
This commit is contained in:
317
server/db.ts
Normal file
317
server/db.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
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,
|
||||
} 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,
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user