Checkpoint: Migration complète vers authentification locale (JWT + bcrypt) : schéma DB (10 tables), routes tRPC (auth, users, etablissements, inventaire, capex, opex, parametres), page de login avec branding Itinova/Santinova, protection des routes, onglets Établissements et Utilisateurs branchés sur tRPC, import inventaire/établissements/utilisateurs via tRPC, 14 tests Vitest passants.
This commit is contained in:
310
server/db.ts
Normal file
310
server/db.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import {
|
||||
capexLignes,
|
||||
etablissements,
|
||||
InsertCapexLigne,
|
||||
InsertEtablissement,
|
||||
InsertOpexMontantEtab,
|
||||
InsertOpexPoste,
|
||||
InsertUser,
|
||||
inventaireMeta,
|
||||
inventairePostes,
|
||||
opexMontantsEtab,
|
||||
opexPostes,
|
||||
opexValidated,
|
||||
parametresApp,
|
||||
userEtablissements,
|
||||
users,
|
||||
} from "../drizzle/schema";
|
||||
|
||||
let _db: ReturnType<typeof drizzle> | null = null;
|
||||
|
||||
// Lazily create the drizzle instance so local tooling can run without a DB.
|
||||
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 getUserByLogin(login: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
const result = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.login, login))
|
||||
.limit(1);
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
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.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
export async function createUser(user: InsertUser) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const [result] = await db.insert(users).values(user);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateUser(id: number, data: Partial<InsertUser>) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.update(users).set(data).where(eq(users.id, id));
|
||||
}
|
||||
|
||||
export async function listUsers() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(users);
|
||||
}
|
||||
|
||||
export async function deleteUser(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.delete(users).where(eq(users.id, id));
|
||||
}
|
||||
|
||||
export async function updateLastSignedIn(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db
|
||||
.update(users)
|
||||
.set({ lastSignedIn: new Date() })
|
||||
.where(eq(users.id, id));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ÉTABLISSEMENTS
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function listEtablissements() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(etablissements);
|
||||
}
|
||||
|
||||
export async function upsertEtablissement(etab: InsertEtablissement) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db
|
||||
.insert(etablissements)
|
||||
.values(etab)
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
nom: etab.nom,
|
||||
groupe: etab.groupe,
|
||||
ville: etab.ville,
|
||||
actif: etab.actif,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteEtablissement(code: string) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.delete(etablissements).where(eq(etablissements.code, code));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// PARAMÈTRES
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getParametres() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(parametresApp);
|
||||
}
|
||||
|
||||
export async function setParametre(cle: string, valeur: string) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db
|
||||
.insert(parametresApp)
|
||||
.values({ cle, valeur })
|
||||
.onDuplicateKeyUpdate({ set: { valeur } });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// OPEX
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getOpexPostes(annee: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(opexPostes)
|
||||
.where(eq(opexPostes.annee, annee));
|
||||
}
|
||||
|
||||
export async function upsertOpexPoste(poste: InsertOpexPoste) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
if (poste.id) {
|
||||
await db
|
||||
.update(opexPostes)
|
||||
.set(poste)
|
||||
.where(eq(opexPostes.id, poste.id));
|
||||
} else {
|
||||
await db.insert(opexPostes).values(poste);
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertOpexPostes(postes: InsertOpexPoste[]) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
if (postes.length === 0) return;
|
||||
await db.insert(opexPostes).values(postes);
|
||||
}
|
||||
|
||||
export async function getOpexMontantsEtab(annee: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(opexMontantsEtab)
|
||||
.where(eq(opexMontantsEtab.annee, annee));
|
||||
}
|
||||
|
||||
export async function setOpexMontantEtab(data: InsertOpexMontantEtab) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db
|
||||
.insert(opexMontantsEtab)
|
||||
.values(data)
|
||||
.onDuplicateKeyUpdate({ set: { montant: data.montant } });
|
||||
}
|
||||
|
||||
export async function insertOpexMontantsEtab(rows: InsertOpexMontantEtab[]) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
if (rows.length === 0) return;
|
||||
// Insert par batch de 100
|
||||
for (let i = 0; i < rows.length; i += 100) {
|
||||
await db.insert(opexMontantsEtab).values(rows.slice(i, i + 100));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOpexValidated(annee: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
const result = await db
|
||||
.select()
|
||||
.from(opexValidated)
|
||||
.where(eq(opexValidated.annee, annee))
|
||||
.limit(1);
|
||||
return result.length > 0 ? result[0] : null;
|
||||
}
|
||||
|
||||
export async function setOpexValidated(annee: number, userId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db
|
||||
.insert(opexValidated)
|
||||
.values({ annee, validatedBy: userId })
|
||||
.onDuplicateKeyUpdate({ set: { validatedAt: new Date() } });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// INVENTAIRE PC
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getInventaire(annee: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(inventairePostes)
|
||||
.where(eq(inventairePostes.annee, annee));
|
||||
}
|
||||
|
||||
export async function getInventaireMeta(annee: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
const result = await db
|
||||
.select()
|
||||
.from(inventaireMeta)
|
||||
.where(eq(inventaireMeta.annee, annee))
|
||||
.limit(1);
|
||||
return result.length > 0 ? result[0] : null;
|
||||
}
|
||||
|
||||
export async function importInventaire(
|
||||
annee: number,
|
||||
postes: typeof inventairePostes.$inferInsert[],
|
||||
meta: { filename: string; nbEtablissements: number; nbFixes: number; nbPortables: number }
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
// Supprimer l'inventaire existant pour cette année
|
||||
await db.delete(inventairePostes).where(eq(inventairePostes.annee, annee));
|
||||
// Insérer les nouveaux postes par batch
|
||||
if (postes.length > 0) {
|
||||
for (let i = 0; i < postes.length; i += 200) {
|
||||
await db.insert(inventairePostes).values(postes.slice(i, i + 200));
|
||||
}
|
||||
}
|
||||
// Upsert meta
|
||||
await db
|
||||
.insert(inventaireMeta)
|
||||
.values({ annee, ...meta })
|
||||
.onDuplicateKeyUpdate({ set: { ...meta, dateImport: new Date() } });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// CAPEX
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getCapexLignes(annee: number, etablissementCode: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(capexLignes)
|
||||
.where(
|
||||
and(
|
||||
eq(capexLignes.annee, annee),
|
||||
eq(capexLignes.etablissementCode, etablissementCode)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveCapexLignes(
|
||||
annee: number,
|
||||
etablissementCode: string,
|
||||
lignes: { cle: string; montant: string | null }[]
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
for (const ligne of lignes) {
|
||||
await db
|
||||
.insert(capexLignes)
|
||||
.values({ annee, etablissementCode, cle: ligne.cle, montant: ligne.montant })
|
||||
.onDuplicateKeyUpdate({ set: { montant: ligne.montant } });
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertCapexLignes(rows: InsertCapexLigne[]) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
if (rows.length === 0) return;
|
||||
for (let i = 0; i < rows.length; i += 100) {
|
||||
await db.insert(capexLignes).values(rows.slice(i, i + 100));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user