`@biomejs/biome` passe de 1.9.4 à 2.5.9 (config migrée via `biome migrate
--write`) — nécessaire pour noFloatingPromises, une règle type-aware
apparue en 2.0 (nursery).
- noExplicitAny : déjà "recommended", actif depuis toujours, aucun changement.
- noConsole (biome.json) : bloque tout `console.*` sauf error/warn/info/
debug/table/assert — équivalent à "pas de console.log" sans interdire
les niveaux nommés (voir le nouveau log service dans le prochain commit,
qui centralise justement ces appels).
- noFloatingPromises (nursery) activé explicitement sous `rules.nursery`
sans avoir besoin d'activer le domaine "types" au sens large (ça aurait
aussi allumé des dizaines d'autres règles type-aware type
noUnresolvedImports/noUnnecessaryConditions, hors scope ici).
Le reste du diff, c'est soit du reformatage automatique (import sort, 2.x
ordonne différemment de 1.9.4 — `biome check --write --unsafe`), soit les
corrections des ~20 promesses flottantes que la nouvelle règle a fait
remonter :
- La plupart sont des `navigate(...)` non attendus (react-router v7 type
`navigate` en `void | Promise<void>`) — préfixés `void navigate(...)`,
aucun changement de comportement.
- Trois chargements initiaux en useEffect (OnboardingAllergensPage,
OnboardingDietPage, OnboardingHouseholdPage, HouseholdSettingsPage)
n'avaient jamais de `.catch()` du tout — ajouté (dégradation silencieuse
vers un état vide/par défaut, même raisonnement que le `.catch()` déjà
présent dans OnboardingSourcesPage).
- HouseholdSettingsPage : `loadHouse` était une fonction déclarée à chaque
render (donc une référence différente à chaque fois) utilisée comme
dépendance de useEffect ET passée en callback à des enfants — le
useEffect se re-déclenchait donc à chaque re-render provoqué par son
propre fetch, un vrai bug de boucle infinie de requêtes que
noFloatingPromises a fait remonter indirectement (via
useExhaustiveDependencies). Corrigé avec useCallback([]).
- RecipeDetailPanel : une clé de liste `${index}-...}` sur une liste
statique (draft.steps, sans id stable — DraftRecipeStepView n'en a pas)
— biome-ignore justifié, pas de bug réel.
- recipe.test.ts : variable `agent` non utilisée, retirée.
Vérifié : `pnpm --filter api test` (295/295), `pnpm lint` et `pnpm build`
clean sur tout le repo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import { HttpError } from "@batch-cooking/error-tools";
|
|
import { wrapAsyncHandler } from "@batch-cooking/express-tools";
|
|
import {
|
|
createRecipeSchema,
|
|
ErrorCode,
|
|
listRecipesSchema,
|
|
updateRecipeSchema,
|
|
} from "@batch-cooking/shared";
|
|
import { Router } from "express";
|
|
import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js";
|
|
import {
|
|
addFavorite,
|
|
createRecipe,
|
|
deleteRecipe,
|
|
getRecipe,
|
|
listRecipes,
|
|
removeFavorite,
|
|
updateRecipe,
|
|
} from "./recipe.service.js";
|
|
|
|
/** Router mounted at `/recipes` in app.ts. Every route requires a session — the catalog is shared across households, not public (same reasoning as `planning`/`house`: it's app content, not signup-time reference data). */
|
|
export const recipeRouter = Router();
|
|
|
|
/** Parses and validates an `:id` route param, shared by every route below that targets one recipe. */
|
|
function parseRecipeId(rawId: string | undefined): number {
|
|
const id = Number(rawId);
|
|
if (!Number.isInteger(id)) {
|
|
throw new HttpError(400, ErrorCode.VALIDATION_ERROR, "id must be an integer");
|
|
}
|
|
return id;
|
|
}
|
|
|
|
recipeRouter.get(
|
|
"/",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = listRecipesSchema.parse(req.query);
|
|
const { id: viewerId, houseId } = res.locals.userProfile;
|
|
res.status(200).json(
|
|
await listRecipes(viewerId, houseId, input.tab, {
|
|
search: input.search,
|
|
suitableForHousehold: input.suitableForHousehold,
|
|
ingredientIds: input.ingredientIds,
|
|
dietIds: input.dietIds,
|
|
}),
|
|
);
|
|
}),
|
|
);
|
|
|
|
recipeRouter.get(
|
|
"/:id",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const id = parseRecipeId(req.params.id);
|
|
const { id: viewerId, houseId } = res.locals.userProfile;
|
|
res.status(200).json(await getRecipe(id, viewerId, houseId));
|
|
}),
|
|
);
|
|
|
|
recipeRouter.post(
|
|
"/",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = createRecipeSchema.parse(req.body);
|
|
const { id: authorId, houseId } = res.locals.userProfile;
|
|
res.status(201).json(await createRecipe(input, authorId, houseId));
|
|
}),
|
|
);
|
|
|
|
recipeRouter.patch(
|
|
"/:id",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const id = parseRecipeId(req.params.id);
|
|
const input = updateRecipeSchema.parse(req.body);
|
|
const { id: viewerId, houseId } = res.locals.userProfile;
|
|
res.status(200).json(await updateRecipe(id, input, viewerId, houseId));
|
|
}),
|
|
);
|
|
|
|
recipeRouter.delete(
|
|
"/:id",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const id = parseRecipeId(req.params.id);
|
|
const { id: viewerId, houseId } = res.locals.userProfile;
|
|
await deleteRecipe(id, viewerId, houseId);
|
|
res.status(204).end();
|
|
}),
|
|
);
|
|
|
|
recipeRouter.post(
|
|
"/:id/favorite",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const id = parseRecipeId(req.params.id);
|
|
const { id: viewerId, houseId } = res.locals.userProfile;
|
|
await addFavorite(id, viewerId, houseId);
|
|
res.status(204).end();
|
|
}),
|
|
);
|
|
|
|
recipeRouter.delete(
|
|
"/:id/favorite",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const id = parseRecipeId(req.params.id);
|
|
await removeFavorite(id, res.locals.userProfile.id);
|
|
res.status(204).end();
|
|
}),
|
|
);
|