style: préfixe tous les membres private/protected par _

Convention demandée par l'utilisateur : `emit` -> `_emit`, sur toutes les
classes du repo, pas seulement le nouveau code. `public` reste sans
préfixe.

- LoggerService (apps/api) : _minSeverity, _emit.
- ApiClient (apps/web) : _request (39 sites d'appel mis à jour).
- ErrorHandlerService (packages/error-tools) : _fromZodError,
  _fromHttpError, _fromUnknownError.
- ExpressServer (packages/express-tools) : _app, _registeredRoutes.

Aucun changement de comportement — pur renommage interne, aucune méthode
private/protected n'était appelée depuis l'extérieur de sa classe.

Vérifié : pnpm --filter api test (303/303), pnpm lint/build clean sur
tout le repo (apps/api, apps/web, packages/*).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-08-21 10:32:52 +02:00
parent 22582536a6
commit c5282ba4a7
4 changed files with 70 additions and 70 deletions

View file

@ -60,30 +60,30 @@ const CONSOLE_METHOD: Record<LogLevel, "debug" | "info" | "warn" | "error"> = {
/** Server-side operational logger — see the module doc comment for why this exists and what it's for. */
export class LoggerService {
/** Computed once at construction from `env.NODE_ENV` — see {@link minLevelFor}. */
private readonly minSeverity: number;
private readonly _minSeverity: number;
public constructor(nodeEnv: typeof env.NODE_ENV = env.NODE_ENV) {
this.minSeverity = LEVEL_SEVERITY[minLevelFor(nodeEnv)];
this._minSeverity = LEVEL_SEVERITY[minLevelFor(nodeEnv)];
}
public debug(message: string, meta?: LogMeta): void {
this.emit("debug", message, meta);
this._emit("debug", message, meta);
}
public info(message: string, meta?: LogMeta): void {
this.emit("info", message, meta);
this._emit("info", message, meta);
}
public warn(message: string, meta?: LogMeta): void {
this.emit("warn", message, meta);
this._emit("warn", message, meta);
}
public error(message: string, meta?: LogMeta): void {
this.emit("error", message, meta);
this._emit("error", message, meta);
}
private emit(level: LogLevel, message: string, meta?: LogMeta): void {
if (LEVEL_SEVERITY[level] < this.minSeverity) return;
private _emit(level: LogLevel, message: string, meta?: LogMeta): void {
if (LEVEL_SEVERITY[level] < this._minSeverity) return;
// `meta` spread first so a caller accidentally passing e.g. `{ message:
// ... }` in it can never shadow the line's own core fields.

View file

@ -71,7 +71,7 @@ export class ApiClient {
*
* @throws {ApiError} if the response status is not in the 2xx range.
*/
private async request<TResponseBody>(
private async _request<TResponseBody>(
path: string,
options: RequestInit = {},
): Promise<TResponseBody> {
@ -102,27 +102,27 @@ export class ApiClient {
/** Creates a profile and starts a session — no household yet, that's an optional step of the onboarding wizard. */
public signup(input: SignupInput): Promise<SafeUserProfile> {
return this.request("/auth/signup", { method: "POST", body: JSON.stringify(input) });
return this._request("/auth/signup", { method: "POST", body: JSON.stringify(input) });
}
/** Verifies credentials and starts a session. */
public login(input: LoginInput): Promise<SafeUserProfile> {
return this.request("/auth/login", { method: "POST", body: JSON.stringify(input) });
return this._request("/auth/login", { method: "POST", body: JSON.stringify(input) });
}
/** Ends the current session. */
public logout(): Promise<void> {
return this.request("/auth/logout", { method: "POST" });
return this._request("/auth/logout", { method: "POST" });
}
/** Fetches the currently authenticated profile — rejects with `NOT_AUTHENTICATED` if there's no session. */
public me(): Promise<SafeUserProfile> {
return this.request("/auth/me");
return this._request("/auth/me");
}
/** Permanently deletes the current profile, after re-verifying its password — rejects with `INVALID_CREDENTIALS` if it's wrong. */
public deleteAccount(password: string): Promise<void> {
return this.request("/auth/me", { method: "DELETE", body: JSON.stringify({ password }) });
return this._request("/auth/me", { method: "DELETE", body: JSON.stringify({ password }) });
}
/**
@ -131,7 +131,7 @@ export class ApiClient {
* there isn't one for that week yet.
*/
public getPlanningForWeek(date: string): Promise<PlanningView | null> {
return this.request(`/planning?date=${date}`);
return this._request(`/planning?date=${date}`);
}
/**
@ -142,37 +142,37 @@ export class ApiClient {
* caller).
*/
public addPlanningItem(input: AddPlanningItemInput): Promise<PlanningItemView> {
return this.request("/planning/items", { method: "POST", body: JSON.stringify(input) });
return this._request("/planning/items", { method: "POST", body: JSON.stringify(input) });
}
/** Removes one recipe from a planning slot — rejects with `PLANNING_ITEM_NOT_FOUND`. */
public removePlanningItem(id: number): Promise<void> {
return this.request(`/planning/items/${id}`, { method: "DELETE" });
return this._request(`/planning/items/${id}`, { method: "DELETE" });
}
/** Reference list of dietary regimes to pick from (signup wizard, `/foyer`). Public — no session required. */
public getDiets(): Promise<DietView[]> {
return this.request("/reference/diets");
return this._request("/reference/diets");
}
/** Reference list of selectable allergens (signup wizard, `/foyer`). Public — no session required. */
public getAllergies(): Promise<AllergyView[]> {
return this.request("/reference/allergies");
return this._request("/reference/allergies");
}
/** Reference list of ingredients, each resolved to its allergens — static, non-administrable (recipe form's ingredient picker). Public — no session required. */
public getIngredients(): Promise<IngredientView[]> {
return this.request("/reference/ingredients");
return this._request("/reference/ingredients");
}
/** Reference list of recipe ingredient units (g, kg, cuillère à soupe…) — static, non-administrable (recipe form's per-ingredient unit select). Public — no session required. */
public getUnits(): Promise<UnitView[]> {
return this.request("/reference/units");
return this._request("/reference/units");
}
/** Reference list of implemented recipe sources (onboarding wizard's source step, `/parametres/foyer`) — empty until a concrete source is registered. Public — no session required. */
public getSources(): Promise<SourceView[]> {
return this.request("/reference/sources");
return this._request("/reference/sources");
}
/** One page of `sourceKey`'s own catalog (recipe catalog's "Sources" tab), each item flagged with whether it's already been imported. Rejects with `SOURCE_NOT_FOUND` unless the viewer's household has this source enabled (`/parametres/foyer`). */
@ -184,12 +184,12 @@ export class ApiClient {
if (params.query) search.set("query", params.query);
if (params.cursor) search.set("cursor", params.cursor);
const queryString = search.toString();
return this.request(`/sources/${sourceKey}/browse${queryString ? `?${queryString}` : ""}`);
return this._request(`/sources/${sourceKey}/browse${queryString ? `?${queryString}` : ""}`);
}
/** Fully translates one not-yet-saved source item (ingredients/units/techniques resolved where possible) — nothing is persisted. Rejects with `RECIPE_NOT_FOUND` if the source couldn't fetch/parse it. */
public previewSourceItem(sourceKey: string, externalId: string): Promise<RecipeImportDraftView> {
return this.request(`/sources/${sourceKey}/preview/${encodeURIComponent(externalId)}`);
return this._request(`/sources/${sourceKey}/preview/${encodeURIComponent(externalId)}`);
}
/** Finalizes an import — `input` is a fully-resolved `CreateRecipeInput`, exactly like a manual `createRecipe()` call (the review screen, `ImportRecipePage`, is what makes sure of that before calling this). Rejects with `RECIPE_ALREADY_IMPORTED` if this item was imported since the preview was fetched. */
@ -198,7 +198,7 @@ export class ApiClient {
externalId: string,
input: CreateRecipeInput,
): Promise<RecipeView> {
return this.request(`/sources/${sourceKey}/import/${encodeURIComponent(externalId)}`, {
return this._request(`/sources/${sourceKey}/import/${encodeURIComponent(externalId)}`, {
method: "POST",
body: JSON.stringify(input),
});
@ -225,82 +225,82 @@ export class ApiClient {
if (filters.suitableForHousehold) params.set("suitableForHousehold", "true");
for (const id of filters.ingredientIds ?? []) params.append("ingredientIds", String(id));
for (const id of filters.dietIds ?? []) params.append("dietIds", String(id));
return this.request(`/recipes?${params.toString()}`);
return this._request(`/recipes?${params.toString()}`);
}
/** Fetches one recipe's full detail — rejects with `RECIPE_NOT_FOUND` if `id` doesn't match any recipe. */
public getRecipe(id: number): Promise<RecipeView> {
return this.request(`/recipes/${id}`);
return this._request(`/recipes/${id}`);
}
/** Adds a recipe to the catalog — rejects with `INGREDIENT_NOT_FOUND` if any `ingredientId` doesn't match a reference ingredient. */
public createRecipe(input: CreateRecipeInput): Promise<RecipeView> {
return this.request("/recipes", { method: "POST", body: JSON.stringify(input) });
return this._request("/recipes", { method: "POST", body: JSON.stringify(input) });
}
/** Replaces a recipe's full content (not a partial merge) — same rejections as {@link createRecipe}, plus `RECIPE_NOT_FOUND`. */
public updateRecipe(id: number, input: UpdateRecipeInput): Promise<RecipeView> {
return this.request(`/recipes/${id}`, { method: "PATCH", body: JSON.stringify(input) });
return this._request(`/recipes/${id}`, { method: "PATCH", body: JSON.stringify(input) });
}
/** Removes a recipe from the catalog outright — rejects with `RECIPE_IN_USE` if it's still referenced by a planning item. */
public deleteRecipe(id: number): Promise<void> {
return this.request(`/recipes/${id}`, { method: "DELETE" });
return this._request(`/recipes/${id}`, { method: "DELETE" });
}
/** Favorites a recipe for the current user — idempotent. */
public addFavoriteRecipe(id: number): Promise<void> {
return this.request(`/recipes/${id}/favorite`, { method: "POST" });
return this._request(`/recipes/${id}/favorite`, { method: "POST" });
}
/** Unfavorites a recipe for the current user — idempotent. */
public removeFavoriteRecipe(id: number): Promise<void> {
return this.request(`/recipes/${id}/favorite`, { method: "DELETE" });
return this._request(`/recipes/${id}/favorite`, { method: "DELETE" });
}
/** Fetches the current user's household (with its member list), or `null` if they don't have one yet. */
public getCurrentHouse(): Promise<HouseView | null> {
return this.request("/house/current");
return this._request("/house/current");
}
/** Renames the current user's household. */
public renameHouse(name: string): Promise<HouseView> {
return this.request("/house/current", { method: "PATCH", body: JSON.stringify({ name }) });
return this._request("/house/current", { method: "PATCH", body: JSON.stringify({ name }) });
}
/** Creates a new household, with the caller as its admin — rejects with `ALREADY_HAS_HOUSE` if they already belong to one. */
public createHouse(name: string): Promise<HouseView> {
return this.request("/house", { method: "POST", body: JSON.stringify({ name }) });
return this._request("/house", { method: "POST", body: JSON.stringify({ name }) });
}
/** Joins an existing household by invite code — rejects with `ALREADY_HAS_HOUSE`/`INVITE_CODE_NOT_FOUND`. */
public joinHouse(inviteCode: string): Promise<HouseView> {
return this.request("/house/join", { method: "POST", body: JSON.stringify({ inviteCode }) });
return this._request("/house/join", { method: "POST", body: JSON.stringify({ inviteCode }) });
}
/** Removes the current user from their household — hands off adminship or deletes the household if they were its last member (see the API's `house.service.ts`). */
public leaveHouse(): Promise<void> {
return this.request("/house/leave", { method: "POST" });
return this._request("/house/leave", { method: "POST" });
}
/** Deletes the current user's household outright — every member loses it. Admin-only. */
public deleteHouse(): Promise<void> {
return this.request("/house/current", { method: "DELETE" });
return this._request("/house/current", { method: "DELETE" });
}
/** Removes one specific member from the current user's household. Admin-only. */
public removeHouseMember(memberId: number): Promise<HouseView> {
return this.request(`/house/members/${memberId}`, { method: "DELETE" });
return this._request(`/house/members/${memberId}`, { method: "DELETE" });
}
/** Fetches the current user's household's enabled recipe-source ids — which sources show up in its recipe tabs. Rejects with `HOUSE_NOT_FOUND` if they have no household yet. */
public getHouseSourceIds(): Promise<number[]> {
return this.request("/house/current/sources");
return this._request("/house/current/sources");
}
/** Replaces the current user's household's full enabled-source selection (not a merge — send the complete list; empty hides every external source). Rejects with `HOUSE_NOT_FOUND`/`SOURCE_NOT_FOUND`. */
public updateHouseSourceIds(sourceIds: number[]): Promise<number[]> {
return this.request("/house/current/sources", {
return this._request("/house/current/sources", {
method: "PATCH",
body: JSON.stringify({ sourceIds }),
});
@ -308,17 +308,17 @@ export class ApiClient {
/** Sets (or clears, with `null`) the current user's dietary regime. */
public updateDiet(dietId: number | null): Promise<SafeUserProfile> {
return this.request("/profile/diet", { method: "PATCH", body: JSON.stringify({ dietId }) });
return this._request("/profile/diet", { method: "PATCH", body: JSON.stringify({ dietId }) });
}
/** Fetches the current user's selected allergen ids. */
public getAllergyIds(): Promise<number[]> {
return this.request("/profile/allergies");
return this._request("/profile/allergies");
}
/** Replaces the current user's full allergen selection (not a merge — send the complete list). */
public updateAllergyIds(allergyIds: number[]): Promise<number[]> {
return this.request("/profile/allergies", {
return this._request("/profile/allergies", {
method: "PATCH",
body: JSON.stringify({ allergyIds }),
});
@ -326,12 +326,12 @@ export class ApiClient {
/** Fetches the current user's personally disliked ingredient ids — a taste preference, distinct from `getAllergyIds` (medical). */
public getDislikedIngredientIds(): Promise<number[]> {
return this.request("/profile/disliked-ingredients");
return this._request("/profile/disliked-ingredients");
}
/** Replaces the current user's full disliked-ingredient selection (not a merge — send the complete list). */
public updateDislikedIngredientIds(dislikedIngredientIds: number[]): Promise<number[]> {
return this.request("/profile/disliked-ingredients", {
return this._request("/profile/disliked-ingredients", {
method: "PATCH",
body: JSON.stringify({ dislikedIngredientIds }),
});
@ -339,12 +339,12 @@ export class ApiClient {
/** Fetches the current user's personalization preferences — `theme` defaults to `"SYSTEM"` if never set. */
public getPreferences(): Promise<PreferencesView> {
return this.request("/preferences");
return this._request("/preferences");
}
/** Sets the current user's theme preference. */
public updatePreferences(theme: ThemePreference): Promise<PreferencesView> {
return this.request("/preferences", { method: "PATCH", body: JSON.stringify({ theme }) });
return this._request("/preferences", { method: "PATCH", body: JSON.stringify({ theme }) });
}
}

View file

@ -31,16 +31,16 @@ export class ErrorHandlerService {
*/
public handle(error: unknown): ErrorHandlingResult {
if (error instanceof ZodError) {
return this.fromZodError(error);
return this._fromZodError(error);
}
if (error instanceof HttpError) {
return this.fromHttpError(error);
return this._fromHttpError(error);
}
return this.fromUnknownError(error);
return this._fromUnknownError(error);
}
/** Request body/query failed schema validation — always a 400. */
private fromZodError(error: ZodError): ErrorHandlingResult {
private _fromZodError(error: ZodError): ErrorHandlingResult {
return {
status: 400,
body: {
@ -52,7 +52,7 @@ export class ErrorHandlerService {
}
/** Our own typed error — status/code were decided by whoever threw it. */
private fromHttpError(error: HttpError): ErrorHandlingResult {
private _fromHttpError(error: HttpError): ErrorHandlingResult {
return {
status: error.status,
body: { code: error.code, message: error.message },
@ -68,7 +68,7 @@ export class ErrorHandlerService {
* which sees every error including this exact "unrecognized" case
* before it ever reaches here).
*/
private fromUnknownError(_error: unknown): ErrorHandlingResult {
private _fromUnknownError(_error: unknown): ErrorHandlingResult {
return {
status: 500,
body: { code: ErrorCode.INTERNAL_ERROR, message: "Internal server error" },

View file

@ -31,17 +31,17 @@ export interface ExpressServerCoreOptions {
*/
export class ExpressServer {
/** The underlying Express application. */
private readonly app: Express;
private readonly _app: Express;
/** Tracks `"METHOD path"` keys already registered via {@link addRoute}, to warn instead of silently double-registering a route. */
private readonly registeredRoutes = new Set<string>();
private readonly _registeredRoutes = new Set<string>();
public constructor() {
this.app = express();
this._app = express();
}
/** The underlying Express application — needed by test tooling (e.g. supertest) that expects a raw `Express` instance. */
public get instance(): Express {
return this.app;
return this._app;
}
/**
@ -51,14 +51,14 @@ export class ExpressServer {
* any route.
*/
public setupCore(options: ExpressServerCoreOptions): void {
this.app.use(cors({ origin: options.corsOrigin, credentials: true }));
this.app.use(express.json());
this.app.use(cookieParser());
this._app.use(cors({ origin: options.corsOrigin, credentials: true }));
this._app.use(express.json());
this._app.use(cookieParser());
}
/** Registers a middleware that runs on every request (e.g. logging, a catch-all 404 handler). */
public addMiddleware(middleware: RequestHandler): void {
this.app.use(middleware);
this._app.use(middleware);
}
/**
@ -67,12 +67,12 @@ export class ExpressServer {
* error handler by its arity, and only the last matching one runs.
*/
public setErrorHandler(middleware: ErrorRequestHandler): void {
this.app.use(middleware);
this._app.use(middleware);
}
/** Mounts a whole `express.Router` under a base path (e.g. `mountRouter("/auth", authRouter)`). */
public mountRouter(basePath: string, router: Router): void {
this.app.use(basePath, router);
this._app.use(basePath, router);
}
/**
@ -88,8 +88,8 @@ export class ExpressServer {
* this, so `pnpm dev:web`'s own Vite dev server is unaffected.
*/
public serveStaticFrontend(distDir: string): void {
this.app.use(express.static(distDir));
this.app.get("*", (_req, res) => {
this._app.use(express.static(distDir));
this._app.get("*", (_req, res) => {
res.sendFile(path.join(distDir, "index.html"));
});
}
@ -101,16 +101,16 @@ export class ExpressServer {
*/
public addRoute(method: HttpMethod, path: string, ...handlers: RequestHandler[]): void {
const key = `${method.toUpperCase()} ${path}`;
if (this.registeredRoutes.has(key)) {
if (this._registeredRoutes.has(key)) {
console.warn(`[ExpressServer] Route already registered, skipping: ${key}`);
return;
}
this.registeredRoutes.add(key);
this.app[method](path, ...handlers);
this._registeredRoutes.add(key);
this._app[method](path, ...handlers);
}
/** Starts listening on the given port. `onListening` is called once the server is up (e.g. to log the URL). */
public listen(port: number, onListening?: () => void): void {
this.app.listen(port, onListening);
this._app.listen(port, onListening);
}
}