db.ts - Client IndexedDB
Localisation : frontend/src/lib/db.ts
Wrapper autour de IndexedDB pour le stockage local offline-first.
Vue d’ensemble
IndexedDB : Base de données NoSQL côté client du navigateur.
Capacité :
Chrome/Firefox : Illimitée (demande permission au-delà de 50 MB)
Safari : 1 GB maximum
Mobile : Variable selon appareil
Cas d’Usage :
✅ Stockage offline des données (buildings, owners, units, expenses)
✅ Queue de synchronisation (modifications en attente)
✅ Cache pour améliorer performance (éviter requêtes API répétées)
✅ Progressive Web App (PWA) fonctionnalité
Configuration Base de Données
const DB_NAME = "koprogo_db";
const DB_VERSION = 1;
Object Stores (équivalent tables SQL) :
users: Utilisateurs (cache profile)buildings: Immeublesowners: Copropriétairesunits: Lotsexpenses: Chargessync_queue: Queue de synchronisation
Schema :
interface SyncQueue {
id?: number; // Auto-incrémenté
action: "create" | "update" | "delete";
entity: string; // "buildings", "owners", etc.
data: any; // Payload JSON
timestamp: number; // Date.now()
synced: boolean; // false par défaut
}
Classe LocalDB
init()
Initialise la connexion IndexedDB et crée les object stores.
async init(): Promise<void> {
// Skip sur serveur (SSG/SSR)
if (typeof indexedDB === "undefined") {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Créer object stores
if (!db.objectStoreNames.contains("buildings")) {
db.createObjectStore("buildings", { keyPath: "id" });
}
// ...
};
});
}
onupgradeneeded : Appelé si DB_VERSION augmente, permet migrations.
Exemple d’utilisation :
import { localDB } from '../lib/db';
// Initialiser au démarrage app
await localDB.init();
Opérations CRUD Génériques
get<T>(storeName, id)
Récupère un élément par ID.
async get<T>(storeName: string, id: string): Promise<T | null>
Exemple :
const building = await localDB.get<Building>('buildings', buildingId);
getAll<T>(storeName)
Récupère tous les éléments d’un store.
async getAll<T>(storeName: string): Promise<T[]>
Exemple :
const buildings = await localDB.getAll<Building>('buildings');
put<T>(storeName, data)
Insère ou met à jour un élément.
async put<T>(storeName: string, data: T): Promise<void>
Exemple :
await localDB.put('buildings', {
id: '123',
name: 'Résidence du Parc',
// ...
});
⚠️ put() = INSERT OR UPDATE : Écrase si ID existe déjà.
delete(storeName, id)
Supprime un élément par ID.
async delete(storeName: string, id: string): Promise<void>
Exemple :
await localDB.delete('buildings', buildingId);
clear(storeName)
Vide complètement un object store.
async clear(storeName: string): Promise<void>
Exemple :
// Vider toutes les données au logout
await localDB.clear('buildings');
await localDB.clear('owners');
Opérations Sync Queue
addToSyncQueue(action, entity, data)
Ajoute une modification à la queue de synchronisation.
async addToSyncQueue(
action: "create" | "update" | "delete",
entity: string,
data: any
): Promise<void>
Exemple :
// Utilisateur crée un immeuble offline
await localDB.addToSyncQueue('create', 'buildings', {
name: 'Nouveau immeuble',
address: '123 Rue Example'
});
// Plus tard, quand online, sync.ts traitera cette queue
getSyncQueue()
Récupère tous les éléments de la queue.
async getSyncQueue(): Promise<SyncQueue[]>
Exemple :
const queue = await localDB.getSyncQueue();
const pendingItems = queue.filter(item => !item.synced);
console.log(`${pendingItems.length} modifications en attente`);
markSynced(id)
Marque un élément de la queue comme synchronisé.
async markSynced(id: number): Promise<void>
Exemple :
for (const item of queue) {
try {
await syncItemToBackend(item);
await localDB.markSynced(item.id!);
} catch (error) {
console.error('Sync failed:', error);
}
}
clearSyncedItems()
Supprime tous les éléments synchronisés de la queue.
async clearSyncedItems(): Promise<void>
Exemple :
// Après synchronisation complète
await localDB.clearSyncedItems();
Méthodes Spécifiques Entités
saveBuildings(buildings)
Sauvegarde plusieurs immeubles.
async saveBuildings(buildings: Building[]): Promise<void> {
for (const building of buildings) {
await this.put("buildings", building);
}
}
Exemple :
// Après fetch API
const response = await api.get('/buildings');
await localDB.saveBuildings(response.data);
getBuildings()
Récupère tous les immeubles.
async getBuildings(): Promise<Building[]>
Méthodes similaires :
saveOwners(owners)/getOwners()saveUnits(units)/getUnits()saveExpenses(expenses)/getExpenses()saveUser(user)/getUser(id)
Pattern d’Utilisation avec sync.ts
Workflow Offline-First :
// 1. Composant Svelte essaie API
async function loadBuildings() {
if (navigator.onLine) {
try {
// Essayer API d'abord
const response = await api.get('/buildings');
await localDB.saveBuildings(response.data);
return response.data;
} catch (error) {
console.log('API failed, using local cache');
}
}
// 2. Fallback sur IndexedDB
return await localDB.getBuildings();
}
Workflow Create Offline :
async function createBuilding(data: Partial<Building>) {
if (navigator.onLine) {
try {
// Online: POST direct
const building = await api.post('/buildings', data);
await localDB.put('buildings', building);
return building;
} catch (error) {
console.log('API unavailable, queueing...');
}
}
// Offline: Créer localement + queue
const tempId = `temp-${Date.now()}`;
const tempBuilding = { id: tempId, ...data } as Building;
await localDB.put('buildings', tempBuilding);
await localDB.addToSyncQueue('create', 'buildings', data);
return tempBuilding;
}
Debugging IndexedDB
Chrome DevTools :
Ouvrir DevTools (F12)
Onglet Application
Section IndexedDB → koprogo_db
Explorer object stores, inspecter données
Firefox DevTools :
Ouvrir DevTools (F12)
Onglet Storage
Section Indexed DB → koprogo_db
Console Debug :
// Afficher contenu complet
const buildings = await localDB.getBuildings();
console.table(buildings);
const queue = await localDB.getSyncQueue();
console.table(queue);
Vider données :
// Console browser
indexedDB.deleteDatabase('koprogo_db');
// Puis recharger page
Migrations Schema
Si besoin d’ajouter un object store ou index :
const DB_VERSION = 2; // Incrémenter version
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const oldVersion = event.oldVersion;
// Migration v1 → v2
if (oldVersion < 2) {
if (!db.objectStoreNames.contains("meetings")) {
db.createObjectStore("meetings", { keyPath: "id" });
}
}
};
⚠️ Incrémenter DB_VERSION : Déclenche onupgradeneeded.
Limitations IndexedDB
Pas de Relations :
IndexedDB est NoSQL, pas de JOIN. Nécessite récupérations multiples.
// Récupérer building + units associés const building = await localDB.get('buildings', buildingId); const allUnits = await localDB.getAll('units'); const buildingUnits = allUnits.filter(u => u.building_id === buildingId);
Pas de Requêtes Complexes :
Pas de WHERE, ORDER BY, GROUP BY natifs. Filtrer en JavaScript.
const expenses = await localDB.getAll('expenses'); const unpaidExpenses = expenses .filter(e => e.payment_status === 'Pending') .sort((a, b) => new Date(a.due_date) - new Date(b.due_date));
Performance avec Gros Volumes :
getAll() charge tout en mémoire. Pour > 10,000 items, utiliser cursor.
Pas de Full-Text Search :
Pas d’indexation texte. Pour recherche, utiliser bibliothèque externe (Fuse.js).
API Asynchrone Complexe :
Callbacks IDBRequest, pas de Promise native (wrapper requis).
Extensions Futures
Indexes :
Créer indexes pour requêtes performantes.
const store = db.createObjectStore("expenses", { keyPath: "id" }); store.createIndex("building_id", "building_id", { unique: false }); store.createIndex("payment_status", "payment_status", { unique: false });
Cursors :
Itérer gros datasets sans charger tout en mémoire.
const transaction = db.transaction("buildings", "readonly"); const store = transaction.objectStore("buildings"); const request = store.openCursor(); request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { console.log(cursor.value); cursor.continue(); } };
Compression :
Compresser données avant stockage (LZ-string).
import LZString from 'lz-string'; const compressed = LZString.compress(JSON.stringify(buildings)); await localDB.put('cache', { key: 'buildings', data: compressed });
Encryption :
Chiffrer données sensibles (crypto-js).
import CryptoJS from 'crypto-js'; const encrypted = CryptoJS.AES.encrypt( JSON.stringify(owner), 'secret-key' ).toString();
Tests IndexedDB
Mock IndexedDB :
// vitest.setup.ts
import 'fake-indexeddb/auto';
// tests/unit/db.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { localDB } from '../src/lib/db';
describe('localDB', () => {
beforeEach(async () => {
await localDB.init();
});
it('should save and retrieve building', async () => {
const building = {
id: '123',
name: 'Test Building',
address: '123 Main St'
};
await localDB.put('buildings', building);
const retrieved = await localDB.get('buildings', '123');
expect(retrieved).toEqual(building);
});
it('should queue offline modifications', async () => {
await localDB.addToSyncQueue('create', 'buildings', {
name: 'New Building'
});
const queue = await localDB.getSyncQueue();
expect(queue).toHaveLength(1);
expect(queue[0].action).toBe('create');
expect(queue[0].synced).toBe(false);
});
});
Sécurité
⚠️ Données Non Chiffrées :
IndexedDB stocke données en clair sur l’appareil.
Recommandations :
❌ Ne pas stocker mots de passe
❌ Ne pas stocker tokens JWT long terme
⚠️ Chiffrer données GDPR (emails, téléphones)
✅ Vider données au logout
// Logout
async function logout() {
await localDB.clear('users');
await localDB.clear('buildings');
await localDB.clear('owners');
await localDB.clear('units');
await localDB.clear('expenses');
await localDB.clear('sync_queue');
localStorage.removeItem('koprogo_token');
}
GDPR Compliance
Droit à l’Effacement :
async function deleteUserData(userId: string) {
// Supprimer toutes les données locales
indexedDB.deleteDatabase('koprogo_db');
// Appeler API backend
await api.delete(`/users/${userId}/gdpr-delete`);
}
Droit à la Portabilité :
async function exportUserData() {
const buildings = await localDB.getBuildings();
const owners = await localDB.getOwners();
const units = await localDB.getUnits();
const expenses = await localDB.getExpenses();
const data = { buildings, owners, units, expenses };
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'koprogo-data.json';
link.click();
}
Références
Sync Service :
frontend/src/lib/sync.tsTypes :
frontend/src/lib/types.tsAPI Client :
frontend/src/lib/api.tsMDN IndexedDB : https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API