refacto: error and error codes in services

This commit is contained in:
Julien Froidefond
2025-02-25 06:39:19 +01:00
parent d4871d1afb
commit 4b710cbac2
16 changed files with 389 additions and 125 deletions

View File

@@ -1,11 +1,11 @@
import mongoose from "mongoose";
import { ERROR_CODES } from "../constants/errorCodes";
import { AppError } from "../utils/errors";
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error(
"Veuillez définir la variable d'environnement MONGODB_URI dans votre fichier .env"
);
throw new AppError(ERROR_CODES.MONGODB.MISSING_URI);
}
interface MongooseCache {
@@ -42,7 +42,7 @@ async function connectDB(): Promise<typeof mongoose> {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
throw new AppError(ERROR_CODES.MONGODB.CONNECTION_FAILED, {}, e);
}
return cached.conn;

View File

@@ -2,6 +2,8 @@ import { cookies } from "next/headers";
import connectDB from "@/lib/mongodb";
import { UserModel } from "@/lib/models/user.model";
import bcrypt from "bcrypt";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
interface UserData {
id: string;
@@ -18,13 +20,13 @@ export class AuthServerService {
//check if password is strong
if (!AuthServerService.isPasswordStrong(password)) {
throw new Error("PASSWORD_NOT_STRONG");
throw new AppError(ERROR_CODES.AUTH.PASSWORD_NOT_STRONG);
}
// Check if user already exists
const existingUser = await UserModel.findOne({ email: email.toLowerCase() });
if (existingUser) {
throw new Error("EMAIL_EXISTS");
throw new AppError(ERROR_CODES.AUTH.EMAIL_EXISTS);
}
// Hash password
@@ -98,13 +100,15 @@ export class AuthServerService {
await connectDB();
const user = await UserModel.findOne({ email: email.toLowerCase() });
if (!user) {
throw new Error("INVALID_CREDENTIALS");
throw new AppError(ERROR_CODES.AUTH.INVALID_CREDENTIALS);
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new Error("INVALID_CREDENTIALS");
throw new AppError(ERROR_CODES.AUTH.INVALID_CREDENTIALS);
}
const userData: UserData = {

View File

@@ -2,6 +2,8 @@ import { AuthConfig } from "@/types/auth";
import { getServerCacheService } from "./server-cache.service";
import { ConfigDBService } from "./config-db.service";
import { DebugService } from "./debug.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
// Types de cache disponibles
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
@@ -25,13 +27,13 @@ export abstract class BaseApiService {
};
} catch (error) {
console.error("Erreur lors de la récupération de la configuration:", error);
throw new Error("Configuration Komga non trouvée");
throw new AppError(ERROR_CODES.KOMGA.MISSING_CONFIG, {}, error);
}
}
protected static getAuthHeaders(config: AuthConfig): Headers {
if (!config.authHeader) {
throw new Error("Credentials Komga manquants");
throw new AppError(ERROR_CODES.KOMGA.MISSING_CREDENTIALS);
}
return new Headers({
@@ -56,16 +58,6 @@ export abstract class BaseApiService {
}
}
protected static handleError(error: unknown, defaultMessage: string): never {
console.error("API Error:", error);
if (error instanceof Error) {
throw error;
}
throw new Error(defaultMessage);
}
protected static buildUrl(
config: AuthConfig,
path: string,
@@ -114,7 +106,10 @@ export abstract class BaseApiService {
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
status: response.status,
statusText: response.statusText,
});
}
return options.isImage ? response : response.json();

View File

@@ -2,6 +2,8 @@ import { BaseApiService } from "./base-api.service";
import { KomgaBook } from "@/types/komga";
import { ImageService } from "./image.service";
import { PreferencesService } from "./preferences.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
export class BookService extends BaseApiService {
static async getBook(bookId: string): Promise<{ book: KomgaBook; pages: number[] }> {
@@ -25,7 +27,7 @@ export class BookService extends BaseApiService {
"BOOKS"
);
} catch (error) {
return this.handleError(error, "Impossible de récupérer le tome");
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
}
}
@@ -47,10 +49,13 @@ export class BookService extends BaseApiService {
});
if (!response.ok) {
throw new Error("Erreur lors de la mise à jour de la progression");
throw new AppError(ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR);
}
} catch (error) {
return this.handleError(error, "Impossible de mettre à jour la progression");
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR, {}, error);
}
}
@@ -67,10 +72,13 @@ export class BookService extends BaseApiService {
});
if (!response.ok) {
throw new Error("Erreur lors de la suppression de la progression");
throw new AppError(ERROR_CODES.BOOK.PROGRESS_DELETE_ERROR);
}
} catch (error) {
return this.handleError(error, "Impossible de supprimer la progression");
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.BOOK.PROGRESS_DELETE_ERROR, {}, error);
}
}
@@ -88,7 +96,7 @@ export class BookService extends BaseApiService {
},
});
} catch (error) {
throw this.handleError(error, "Impossible de récupérer la page");
throw new AppError(ERROR_CODES.BOOK.PAGES_FETCH_ERROR, {}, error);
}
}
@@ -111,7 +119,7 @@ export class BookService extends BaseApiService {
// Sinon, récupérer la première page
return this.getPage(bookId, 1);
} catch (error) {
throw this.handleError(error, "Impossible de récupérer la couverture");
throw new AppError(ERROR_CODES.BOOK.PAGES_FETCH_ERROR, {}, error);
}
}
@@ -135,7 +143,7 @@ export class BookService extends BaseApiService {
},
});
} catch (error) {
throw this.handleError(error, "Impossible de récupérer la miniature de la page");
throw new AppError(ERROR_CODES.BOOK.PAGES_FETCH_ERROR, {}, error);
}
}

View File

@@ -3,6 +3,8 @@ import { KomgaConfig } from "@/lib/models/config.model";
import { TTLConfig } from "@/lib/models/ttl-config.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
interface User {
id: string;
@@ -29,66 +31,94 @@ export class ConfigDBService {
private static getCurrentUser(): User {
const user = AuthServerService.getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}
return user;
}
static async saveConfig(data: KomgaConfigData) {
const user = this.getCurrentUser();
await connectDB();
try {
const user = this.getCurrentUser();
await connectDB();
const authHeader = Buffer.from(`${data.username}:${data.password}`).toString("base64");
const authHeader = Buffer.from(`${data.username}:${data.password}`).toString("base64");
const config = await KomgaConfig.findOneAndUpdate(
{ userId: user.id },
{
userId: user.id,
url: data.url,
username: data.username,
// password: data.password,
authHeader,
},
{ upsert: true, new: true }
);
return config;
}
static async getConfig() {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("getConfig", async () => {
const config = await KomgaConfig.findOne({ userId: user.id });
return config;
});
}
static async getTTLConfig() {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("getTTLConfig", async () => {
const config = await TTLConfig.findOne({ userId: user.id });
return config;
});
}
static async saveTTLConfig(data: TTLConfigData) {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("saveTTLConfig", async () => {
const config = await TTLConfig.findOneAndUpdate(
const config = await KomgaConfig.findOneAndUpdate(
{ userId: user.id },
{
userId: user.id,
...data,
url: data.url,
username: data.username,
// password: data.password,
authHeader,
},
{ upsert: true, new: true }
);
return config;
});
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.CONFIG.SAVE_ERROR, {}, error);
}
}
static async getConfig() {
try {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("getConfig", async () => {
const config = await KomgaConfig.findOne({ userId: user.id });
return config;
});
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.CONFIG.FETCH_ERROR, {}, error);
}
}
static async getTTLConfig() {
try {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("getTTLConfig", async () => {
const config = await TTLConfig.findOne({ userId: user.id });
return config;
});
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.CONFIG.TTL_FETCH_ERROR, {}, error);
}
}
static async saveTTLConfig(data: TTLConfigData) {
try {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("saveTTLConfig", async () => {
const config = await TTLConfig.findOneAndUpdate(
{ userId: user.id },
{
userId: user.id,
...data,
},
{ upsert: true, new: true }
);
return config;
});
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.CONFIG.TTL_SAVE_ERROR, {}, error);
}
}
}

View File

@@ -3,6 +3,8 @@ import path from "path";
import { CacheType } from "./base-api.service";
import { AuthServerService } from "./auth-server.service";
import { PreferencesService } from "./preferences.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
interface RequestTiming {
url: string;
@@ -26,7 +28,7 @@ export class DebugService {
private static getCurrentUserId(): string {
const user = AuthServerService.getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}
return user.id;
}
@@ -73,7 +75,7 @@ export class DebugService {
await fs.writeFile(filePath, JSON.stringify(logs, null, 2));
} catch (error) {
// Ignore les erreurs de logging
// On ignore les erreurs de logging mais on les trace quand même
console.error("Erreur lors de l'enregistrement du log:", error);
}
}
@@ -84,7 +86,10 @@ export class DebugService {
const filePath = this.getLogFilePath(userId);
const content = await fs.readFile(filePath, "utf-8");
return JSON.parse(content);
} catch {
} catch (error) {
if (error instanceof AppError) {
throw error;
}
return [];
}
}
@@ -94,8 +99,11 @@ export class DebugService {
const userId = await this.getCurrentUserId();
const filePath = this.getLogFilePath(userId);
await fs.writeFile(filePath, "[]");
} catch {
// Ignore les erreurs si le fichier n'existe pas
} catch (error) {
if (error instanceof AppError) {
throw error;
}
// On ignore les autres erreurs si le fichier n'existe pas
}
}
@@ -136,8 +144,8 @@ export class DebugService {
await fs.writeFile(filePath, JSON.stringify(logs, null, 2));
} catch (error) {
// Ignore les erreurs de logging
console.error("Erreur lors de l'enregistrement du log de page:", error);
// On ignore les erreurs de logging mais on les trace quand même
console.error("Erreur lors de l'enregistrement du log de rendu:", error);
}
}

View File

@@ -2,6 +2,8 @@ import connectDB from "@/lib/mongodb";
import { FavoriteModel } from "@/lib/models/favorite.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
interface User {
id: string;
@@ -21,7 +23,7 @@ export class FavoriteService {
private static getCurrentUser(): User {
const user = AuthServerService.getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}
return user;
}
@@ -65,8 +67,7 @@ export class FavoriteService {
this.dispatchFavoritesChanged();
} catch (error) {
console.error("Erreur lors de l'ajout aux favoris:", error);
throw new Error("Erreur lors de l'ajout aux favoris");
throw new AppError(ERROR_CODES.FAVORITE.ADD_ERROR, {}, error);
}
}
@@ -87,8 +88,7 @@ export class FavoriteService {
this.dispatchFavoritesChanged();
} catch (error) {
console.error("Erreur lors de la suppression des favoris:", error);
throw new Error("Erreur lors de la suppression des favoris");
throw new AppError(ERROR_CODES.FAVORITE.DELETE_ERROR, {}, error);
}
}

View File

@@ -2,6 +2,8 @@ import { BaseApiService } from "./base-api.service";
import { KomgaBook, KomgaSeries } from "@/types/komga";
import { LibraryResponse } from "@/types/library";
import { getServerCacheService } from "./server-cache.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
interface HomeData {
ongoing: KomgaSeries[];
@@ -78,15 +80,19 @@ export class HomeService extends BaseApiService {
latestSeries: latestSeries.content || [],
};
} catch (error) {
return this.handleError(error, "Impossible de récupérer les données de la page d'accueil");
throw new AppError(ERROR_CODES.HOME.FETCH_ERROR, {}, error);
}
}
static async invalidateHomeCache(): Promise<void> {
const cacheService = await getServerCacheService();
await cacheService.delete("home-ongoing");
await cacheService.delete("home-recently-read");
await cacheService.delete("home-on-deck");
await cacheService.delete("home-latest-series");
try {
const cacheService = await getServerCacheService();
await cacheService.delete("home-ongoing");
await cacheService.delete("home-recently-read");
await cacheService.delete("home-on-deck");
await cacheService.delete("home-latest-series");
} catch (error) {
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
}
}
}

View File

@@ -1,4 +1,6 @@
import { BaseApiService } from "./base-api.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
interface ImageResponse {
buffer: Buffer;
@@ -27,7 +29,7 @@ export class ImageService extends BaseApiService {
);
} catch (error) {
console.error("Erreur lors de la récupération de l'image:", error);
return this.handleError(error, "Impossible de récupérer l'image");
throw new AppError(ERROR_CODES.IMAGE.FETCH_ERROR, {}, error);
}
}

View File

@@ -2,6 +2,8 @@ import { BaseApiService } from "./base-api.service";
import { Library, LibraryResponse } from "@/types/library";
import { Series } from "@/types/series";
import { getServerCacheService } from "./server-cache.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
export class LibraryService extends BaseApiService {
static async getLibraries(): Promise<Library[]> {
@@ -12,17 +14,24 @@ export class LibraryService extends BaseApiService {
"LIBRARIES"
);
} catch (error) {
return this.handleError(error, "Impossible de récupérer les bibliothèques");
throw new AppError(ERROR_CODES.LIBRARY.FETCH_ERROR, {}, error);
}
}
static async getLibrary(libraryId: string): Promise<Library> {
const libraries = await this.getLibraries();
const library = libraries.find((library) => library.id === libraryId);
if (!library) {
throw new Error(`Bibliothèque ${libraryId} non trouvée`);
try {
const libraries = await this.getLibraries();
const library = libraries.find((library) => library.id === libraryId);
if (!library) {
throw new AppError(ERROR_CODES.LIBRARY.NOT_FOUND, { libraryId });
}
return library;
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.LIBRARY.FETCH_ERROR, {}, error);
}
return library;
}
static async getAllLibrarySeries(libraryId: string): Promise<Series[]> {
@@ -60,7 +69,7 @@ export class LibraryService extends BaseApiService {
return response.content;
} catch (error) {
return this.handleError(error, "Impossible de récupérer toutes les séries");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
@@ -130,12 +139,17 @@ export class LibraryService extends BaseApiService {
totalPages,
};
} catch (error) {
return this.handleError(error, "Impossible de récupérer les séries");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
static async invalidateLibrarySeriesCache(libraryId: string): Promise<void> {
const cacheService = await getServerCacheService();
await cacheService.delete(`library-${libraryId}-all-series`);
try {
const cacheService = await getServerCacheService();
const cacheKey = `library-${libraryId}-all-series`;
await cacheService.delete(cacheKey);
} catch (error) {
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
}
}
}

View File

@@ -1,5 +1,7 @@
import { PreferencesModel } from "@/lib/models/preferences.model";
import { AuthServerService } from "./auth-server.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
interface User {
id: string;
@@ -24,7 +26,7 @@ export class PreferencesService {
static getCurrentUser(): User {
const user = AuthServerService.getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}
return user;
}
@@ -41,8 +43,10 @@ export class PreferencesService {
...preferences.toObject(),
};
} catch (error) {
console.error("Error getting preferences:", error);
return defaultPreferences;
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.PREFERENCES.FETCH_ERROR, {}, error);
}
}
@@ -61,8 +65,10 @@ export class PreferencesService {
};
return result;
} catch (error) {
console.error("Error updating preferences:", error);
throw error;
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.PREFERENCES.UPDATE_ERROR, {}, error);
}
}
}

View File

@@ -5,6 +5,8 @@ import { BookService } from "./book.service";
import { ImageService } from "./image.service";
import { PreferencesService } from "./preferences.service";
import { getServerCacheService } from "./server-cache.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
export class SeriesService extends BaseApiService {
static async getSeries(seriesId: string): Promise<KomgaSeries> {
@@ -15,13 +17,17 @@ export class SeriesService extends BaseApiService {
"SERIES"
);
} catch (error) {
return this.handleError(error, "Impossible de récupérer la série");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
static async invalidateSeriesCache(seriesId: string): Promise<void> {
const cacheService = await getServerCacheService();
await cacheService.delete(`series-${seriesId}`);
try {
const cacheService = await getServerCacheService();
await cacheService.delete(`series-${seriesId}`);
} catch (error) {
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
}
}
static async getAllSeriesBooks(seriesId: string): Promise<KomgaBook[]> {
@@ -57,9 +63,16 @@ export class SeriesService extends BaseApiService {
"BOOKS"
);
if (!response.content.length) {
throw new AppError(ERROR_CODES.SERIES.NO_BOOKS_FOUND);
}
return response.content;
} catch (error) {
return this.handleError(error, "Impossible de récupérer tous les tomes");
if (error instanceof AppError) {
throw error;
}
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
@@ -122,7 +135,7 @@ export class SeriesService extends BaseApiService {
totalPages,
};
} catch (error) {
return this.handleError(error, "Impossible de récupérer les tomes");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
@@ -141,7 +154,7 @@ export class SeriesService extends BaseApiService {
params: { page: "0", size: "1" },
});
if (!data.content || data.content.length === 0) {
throw new Error("Aucun livre trouvé dans la série");
throw new AppError(ERROR_CODES.SERIES.NO_BOOKS_FOUND);
}
return data.content[0].id;
@@ -150,7 +163,7 @@ export class SeriesService extends BaseApiService {
);
} catch (error) {
console.error("Erreur lors de la récupération du premier livre:", error);
return this.handleError(error, "Impossible de récupérer le premier livre");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
@@ -175,7 +188,7 @@ export class SeriesService extends BaseApiService {
const response = await BookService.getPage(firstBookId, 1);
return response;
} catch (error) {
throw this.handleError(error, "Impossible de récupérer la couverture");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
@@ -189,7 +202,7 @@ export class SeriesService extends BaseApiService {
const series = await Promise.all(seriesPromises);
return series.filter(Boolean);
} catch (error) {
return this.handleError(error, "Impossible de récupérer les séries");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
}

View File

@@ -1,6 +1,8 @@
import { BaseApiService } from "./base-api.service";
import { AuthConfig } from "@/types/auth";
import { KomgaLibrary } from "@/types/komga";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
export class TestService extends BaseApiService {
static async testConnection(config: AuthConfig): Promise<{ libraries: KomgaLibrary[] }> {
@@ -12,19 +14,20 @@ export class TestService extends BaseApiService {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Erreur lors du test de connexion");
throw new AppError(ERROR_CODES.KOMGA.CONNECTION_ERROR, { message: errorData.message });
}
const libraries = await response.json();
return { libraries };
} catch (error) {
console.error("Erreur lors du test de connexion:", error);
if (error instanceof Error && error.message.includes("fetch")) {
throw new Error(
"Impossible de se connecter au serveur. Vérifiez l'URL et que le serveur est accessible."
);
if (error instanceof AppError) {
throw error;
}
throw error instanceof Error ? error : new Error("Erreur lors du test de connexion");
if (error instanceof Error && error.message.includes("fetch")) {
throw new AppError(ERROR_CODES.KOMGA.SERVER_UNREACHABLE);
}
throw new AppError(ERROR_CODES.KOMGA.CONNECTION_ERROR, {}, error);
}
}
}