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:
parent
22582536a6
commit
c5282ba4a7
4 changed files with 70 additions and 70 deletions
|
|
@ -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. */
|
/** Server-side operational logger — see the module doc comment for why this exists and what it's for. */
|
||||||
export class LoggerService {
|
export class LoggerService {
|
||||||
/** Computed once at construction from `env.NODE_ENV` — see {@link minLevelFor}. */
|
/** 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) {
|
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 {
|
public debug(message: string, meta?: LogMeta): void {
|
||||||
this.emit("debug", message, meta);
|
this._emit("debug", message, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public info(message: string, meta?: LogMeta): void {
|
public info(message: string, meta?: LogMeta): void {
|
||||||
this.emit("info", message, meta);
|
this._emit("info", message, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public warn(message: string, meta?: LogMeta): void {
|
public warn(message: string, meta?: LogMeta): void {
|
||||||
this.emit("warn", message, meta);
|
this._emit("warn", message, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public error(message: string, meta?: LogMeta): void {
|
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 {
|
private _emit(level: LogLevel, message: string, meta?: LogMeta): void {
|
||||||
if (LEVEL_SEVERITY[level] < this.minSeverity) return;
|
if (LEVEL_SEVERITY[level] < this._minSeverity) return;
|
||||||
|
|
||||||
// `meta` spread first so a caller accidentally passing e.g. `{ message:
|
// `meta` spread first so a caller accidentally passing e.g. `{ message:
|
||||||
// ... }` in it can never shadow the line's own core fields.
|
// ... }` in it can never shadow the line's own core fields.
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export class ApiClient {
|
||||||
*
|
*
|
||||||
* @throws {ApiError} if the response status is not in the 2xx range.
|
* @throws {ApiError} if the response status is not in the 2xx range.
|
||||||
*/
|
*/
|
||||||
private async request<TResponseBody>(
|
private async _request<TResponseBody>(
|
||||||
path: string,
|
path: string,
|
||||||
options: RequestInit = {},
|
options: RequestInit = {},
|
||||||
): Promise<TResponseBody> {
|
): 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. */
|
/** 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> {
|
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. */
|
/** Verifies credentials and starts a session. */
|
||||||
public login(input: LoginInput): Promise<SafeUserProfile> {
|
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. */
|
/** Ends the current session. */
|
||||||
public logout(): Promise<void> {
|
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. */
|
/** Fetches the currently authenticated profile — rejects with `NOT_AUTHENTICATED` if there's no session. */
|
||||||
public me(): Promise<SafeUserProfile> {
|
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. */
|
/** Permanently deletes the current profile, after re-verifying its password — rejects with `INVALID_CREDENTIALS` if it's wrong. */
|
||||||
public deleteAccount(password: string): Promise<void> {
|
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.
|
* there isn't one for that week yet.
|
||||||
*/
|
*/
|
||||||
public getPlanningForWeek(date: string): Promise<PlanningView | null> {
|
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).
|
* caller).
|
||||||
*/
|
*/
|
||||||
public addPlanningItem(input: AddPlanningItemInput): Promise<PlanningItemView> {
|
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`. */
|
/** Removes one recipe from a planning slot — rejects with `PLANNING_ITEM_NOT_FOUND`. */
|
||||||
public removePlanningItem(id: number): Promise<void> {
|
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. */
|
/** Reference list of dietary regimes to pick from (signup wizard, `/foyer`). Public — no session required. */
|
||||||
public getDiets(): Promise<DietView[]> {
|
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. */
|
/** Reference list of selectable allergens (signup wizard, `/foyer`). Public — no session required. */
|
||||||
public getAllergies(): Promise<AllergyView[]> {
|
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. */
|
/** 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[]> {
|
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. */
|
/** 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[]> {
|
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. */
|
/** 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[]> {
|
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`). */
|
/** 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.query) search.set("query", params.query);
|
||||||
if (params.cursor) search.set("cursor", params.cursor);
|
if (params.cursor) search.set("cursor", params.cursor);
|
||||||
const queryString = search.toString();
|
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. */
|
/** 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> {
|
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. */
|
/** 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,
|
externalId: string,
|
||||||
input: CreateRecipeInput,
|
input: CreateRecipeInput,
|
||||||
): Promise<RecipeView> {
|
): Promise<RecipeView> {
|
||||||
return this.request(`/sources/${sourceKey}/import/${encodeURIComponent(externalId)}`, {
|
return this._request(`/sources/${sourceKey}/import/${encodeURIComponent(externalId)}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
});
|
});
|
||||||
|
|
@ -225,82 +225,82 @@ export class ApiClient {
|
||||||
if (filters.suitableForHousehold) params.set("suitableForHousehold", "true");
|
if (filters.suitableForHousehold) params.set("suitableForHousehold", "true");
|
||||||
for (const id of filters.ingredientIds ?? []) params.append("ingredientIds", String(id));
|
for (const id of filters.ingredientIds ?? []) params.append("ingredientIds", String(id));
|
||||||
for (const id of filters.dietIds ?? []) params.append("dietIds", 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. */
|
/** Fetches one recipe's full detail — rejects with `RECIPE_NOT_FOUND` if `id` doesn't match any recipe. */
|
||||||
public getRecipe(id: number): Promise<RecipeView> {
|
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. */
|
/** 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> {
|
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`. */
|
/** 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> {
|
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. */
|
/** 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> {
|
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. */
|
/** Favorites a recipe for the current user — idempotent. */
|
||||||
public addFavoriteRecipe(id: number): Promise<void> {
|
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. */
|
/** Unfavorites a recipe for the current user — idempotent. */
|
||||||
public removeFavoriteRecipe(id: number): Promise<void> {
|
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. */
|
/** Fetches the current user's household (with its member list), or `null` if they don't have one yet. */
|
||||||
public getCurrentHouse(): Promise<HouseView | null> {
|
public getCurrentHouse(): Promise<HouseView | null> {
|
||||||
return this.request("/house/current");
|
return this._request("/house/current");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renames the current user's household. */
|
/** Renames the current user's household. */
|
||||||
public renameHouse(name: string): Promise<HouseView> {
|
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. */
|
/** 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> {
|
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`. */
|
/** Joins an existing household by invite code — rejects with `ALREADY_HAS_HOUSE`/`INVITE_CODE_NOT_FOUND`. */
|
||||||
public joinHouse(inviteCode: string): Promise<HouseView> {
|
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`). */
|
/** 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> {
|
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. */
|
/** Deletes the current user's household outright — every member loses it. Admin-only. */
|
||||||
public deleteHouse(): Promise<void> {
|
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. */
|
/** Removes one specific member from the current user's household. Admin-only. */
|
||||||
public removeHouseMember(memberId: number): Promise<HouseView> {
|
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. */
|
/** 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[]> {
|
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`. */
|
/** 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[]> {
|
public updateHouseSourceIds(sourceIds: number[]): Promise<number[]> {
|
||||||
return this.request("/house/current/sources", {
|
return this._request("/house/current/sources", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ sourceIds }),
|
body: JSON.stringify({ sourceIds }),
|
||||||
});
|
});
|
||||||
|
|
@ -308,17 +308,17 @@ export class ApiClient {
|
||||||
|
|
||||||
/** Sets (or clears, with `null`) the current user's dietary regime. */
|
/** Sets (or clears, with `null`) the current user's dietary regime. */
|
||||||
public updateDiet(dietId: number | null): Promise<SafeUserProfile> {
|
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. */
|
/** Fetches the current user's selected allergen ids. */
|
||||||
public getAllergyIds(): Promise<number[]> {
|
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). */
|
/** Replaces the current user's full allergen selection (not a merge — send the complete list). */
|
||||||
public updateAllergyIds(allergyIds: number[]): Promise<number[]> {
|
public updateAllergyIds(allergyIds: number[]): Promise<number[]> {
|
||||||
return this.request("/profile/allergies", {
|
return this._request("/profile/allergies", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ allergyIds }),
|
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). */
|
/** Fetches the current user's personally disliked ingredient ids — a taste preference, distinct from `getAllergyIds` (medical). */
|
||||||
public getDislikedIngredientIds(): Promise<number[]> {
|
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). */
|
/** Replaces the current user's full disliked-ingredient selection (not a merge — send the complete list). */
|
||||||
public updateDislikedIngredientIds(dislikedIngredientIds: number[]): Promise<number[]> {
|
public updateDislikedIngredientIds(dislikedIngredientIds: number[]): Promise<number[]> {
|
||||||
return this.request("/profile/disliked-ingredients", {
|
return this._request("/profile/disliked-ingredients", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ dislikedIngredientIds }),
|
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. */
|
/** Fetches the current user's personalization preferences — `theme` defaults to `"SYSTEM"` if never set. */
|
||||||
public getPreferences(): Promise<PreferencesView> {
|
public getPreferences(): Promise<PreferencesView> {
|
||||||
return this.request("/preferences");
|
return this._request("/preferences");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the current user's theme preference. */
|
/** Sets the current user's theme preference. */
|
||||||
public updatePreferences(theme: ThemePreference): Promise<PreferencesView> {
|
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 }) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,16 @@ export class ErrorHandlerService {
|
||||||
*/
|
*/
|
||||||
public handle(error: unknown): ErrorHandlingResult {
|
public handle(error: unknown): ErrorHandlingResult {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
return this.fromZodError(error);
|
return this._fromZodError(error);
|
||||||
}
|
}
|
||||||
if (error instanceof HttpError) {
|
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. */
|
/** Request body/query failed schema validation — always a 400. */
|
||||||
private fromZodError(error: ZodError): ErrorHandlingResult {
|
private _fromZodError(error: ZodError): ErrorHandlingResult {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
|
|
@ -52,7 +52,7 @@ export class ErrorHandlerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Our own typed error — status/code were decided by whoever threw it. */
|
/** Our own typed error — status/code were decided by whoever threw it. */
|
||||||
private fromHttpError(error: HttpError): ErrorHandlingResult {
|
private _fromHttpError(error: HttpError): ErrorHandlingResult {
|
||||||
return {
|
return {
|
||||||
status: error.status,
|
status: error.status,
|
||||||
body: { code: error.code, message: error.message },
|
body: { code: error.code, message: error.message },
|
||||||
|
|
@ -68,7 +68,7 @@ export class ErrorHandlerService {
|
||||||
* which sees every error — including this exact "unrecognized" case —
|
* which sees every error — including this exact "unrecognized" case —
|
||||||
* before it ever reaches here).
|
* before it ever reaches here).
|
||||||
*/
|
*/
|
||||||
private fromUnknownError(_error: unknown): ErrorHandlingResult {
|
private _fromUnknownError(_error: unknown): ErrorHandlingResult {
|
||||||
return {
|
return {
|
||||||
status: 500,
|
status: 500,
|
||||||
body: { code: ErrorCode.INTERNAL_ERROR, message: "Internal server error" },
|
body: { code: ErrorCode.INTERNAL_ERROR, message: "Internal server error" },
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,17 @@ export interface ExpressServerCoreOptions {
|
||||||
*/
|
*/
|
||||||
export class ExpressServer {
|
export class ExpressServer {
|
||||||
/** The underlying Express application. */
|
/** 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. */
|
/** 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() {
|
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. */
|
/** The underlying Express application — needed by test tooling (e.g. supertest) that expects a raw `Express` instance. */
|
||||||
public get instance(): Express {
|
public get instance(): Express {
|
||||||
return this.app;
|
return this._app;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -51,14 +51,14 @@ export class ExpressServer {
|
||||||
* any route.
|
* any route.
|
||||||
*/
|
*/
|
||||||
public setupCore(options: ExpressServerCoreOptions): void {
|
public setupCore(options: ExpressServerCoreOptions): void {
|
||||||
this.app.use(cors({ origin: options.corsOrigin, credentials: true }));
|
this._app.use(cors({ origin: options.corsOrigin, credentials: true }));
|
||||||
this.app.use(express.json());
|
this._app.use(express.json());
|
||||||
this.app.use(cookieParser());
|
this._app.use(cookieParser());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registers a middleware that runs on every request (e.g. logging, a catch-all 404 handler). */
|
/** Registers a middleware that runs on every request (e.g. logging, a catch-all 404 handler). */
|
||||||
public addMiddleware(middleware: RequestHandler): void {
|
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.
|
* error handler by its arity, and only the last matching one runs.
|
||||||
*/
|
*/
|
||||||
public setErrorHandler(middleware: ErrorRequestHandler): void {
|
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)`). */
|
/** Mounts a whole `express.Router` under a base path (e.g. `mountRouter("/auth", authRouter)`). */
|
||||||
public mountRouter(basePath: string, router: Router): void {
|
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.
|
* this, so `pnpm dev:web`'s own Vite dev server is unaffected.
|
||||||
*/
|
*/
|
||||||
public serveStaticFrontend(distDir: string): void {
|
public serveStaticFrontend(distDir: string): void {
|
||||||
this.app.use(express.static(distDir));
|
this._app.use(express.static(distDir));
|
||||||
this.app.get("*", (_req, res) => {
|
this._app.get("*", (_req, res) => {
|
||||||
res.sendFile(path.join(distDir, "index.html"));
|
res.sendFile(path.join(distDir, "index.html"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -101,16 +101,16 @@ export class ExpressServer {
|
||||||
*/
|
*/
|
||||||
public addRoute(method: HttpMethod, path: string, ...handlers: RequestHandler[]): void {
|
public addRoute(method: HttpMethod, path: string, ...handlers: RequestHandler[]): void {
|
||||||
const key = `${method.toUpperCase()} ${path}`;
|
const key = `${method.toUpperCase()} ${path}`;
|
||||||
if (this.registeredRoutes.has(key)) {
|
if (this._registeredRoutes.has(key)) {
|
||||||
console.warn(`[ExpressServer] Route already registered, skipping: ${key}`);
|
console.warn(`[ExpressServer] Route already registered, skipping: ${key}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.registeredRoutes.add(key);
|
this._registeredRoutes.add(key);
|
||||||
this.app[method](path, ...handlers);
|
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). */
|
/** 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 {
|
public listen(port: number, onListening?: () => void): void {
|
||||||
this.app.listen(port, onListening);
|
this._app.listen(port, onListening);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue