Thèmes / centres d'intérêt
Catalogue de thèmes éditorialisé en back-office (≈ 50 entrées) parmi lesquels chaque utilisateur en choisit 5 à 10 lors de l’onboarding. Sert ensuite à recouper le feed (médias / établissements taggés sur les mêmes thèmes).
Modèle de données
Section titled “Modèle de données”topic— entrée du catalogue.idBINARY(16),slugVARCHAR(64) UNIQUE (identité publique stable),icon,position(ordre d’affichage),is_active(un thème retiré du catalogue reste référencé par les utilisateurs qui l’avaient coché — il est juste invisible dans le picker).topic_translation—(topic_id, locale)→label,description. Résolution avec double LEFT JOIN côté repo : locale demandée +SupportedLocales::DEFAULT. Lelabelretombe sur le slug si aucune traduction n’existe.user_topic—(user_id, topic_id)PK composite, index inverse(topic_id, user_id)pour les futures requêtes “utilisateurs intéressés par le thème X”. FK ON DELETE CASCADE des deux côtés.
Identité côté API
Section titled “Identité côté API”L’identifiant JSON:API d’une ressource topics est le slug, pas l’UUID hex. L’UUID interne reste exposé en attribut internalId pour le debug / cross-référence outils admin uniquement.
Variables d’environnement
Section titled “Variables d’environnement”| Variable | Défaut | Effet |
|---|---|---|
TOPIC_MAX_RESULTS | 100 | Cap serveur sur /api/topics (meta.truncated = true + meta.maxResults si atteint). |
USER_TOPICS_MIN | 5 | Nombre minimum de thèmes dans un PUT /api/users/me/topics. En-dessous → 422 userTopic.tooFew. |
USER_TOPICS_MAX | 10 | Nombre maximum de thèmes. Au-dessus → 422 userTopic.tooMany. |
Codes d’erreur userTopic.*
Section titled “Codes d’erreur userTopic.*”Tous les guards du setMine produisent un 422 avec un meta.code stable côté front :
meta.code | Détail |
|---|---|
userTopic.payloadInvalid | Le tableau slugs est absent ou n’est pas une liste de strings. |
userTopic.tooFew | Moins de USER_TOPICS_MIN slugs envoyés. |
userTopic.tooMany | Plus de USER_TOPICS_MAX slugs envoyés. |
userTopic.duplicates | Au moins un slug envoyé deux fois (échos dans meta.unknownSlugs). |
userTopic.unknownSlugs | Au moins un slug n’est pas dans le catalogue actif (idem). |
GET /api/topics
Section titled “GET /api/topics”Catalogue public des thèmes actifs, localisé. Aucun paramètre de query, aucune pagination (le catalogue est borné par produit). Tri systématique (position, id) — c’est le même ordre que GET /api/users/me/topics, donc le picker d’onboarding peut cocher la sélection courante par simple correspondance.
- Auth : aucune
- Action : ListTopicsAction
- Réponse
200:
{ "data": [ { "type": "topics", "id": "outdoor", "attributes": { "slug": "outdoor", "label": "Plein air", "description": "Randonnée, kayak, escalade…", "icon": "mountain", "position": 10, "isActive": true, "internalId": "0190f4b5-1c2a-7a3d-b3f1-1e4a8b2c0011", "createdAt": "2026-06-10T12:00:00+00:00", "updatedAt": null } } ], "links": { "self": "https://api.example/api/topics" }, "meta": { "total": 42 }}- Cap serveur atteint (
meta.total === TOPIC_MAX_RESULTS) : la réponse ajoutemeta.truncated = trueetmeta.maxResults = <cap>.
GET /api/users/me/topics
Section titled “GET /api/users/me/topics”Sélection courante de l’utilisateur authentifié (de 0 à USER_TOPICS_MAX entrées). Avant onboarding, retourne un tableau vide. La réponse inclut les thèmes que le BO a depuis désactivés (isActive = false) — la sélection historique reste lisible, le front peut les griser ou les filtrer côté UI.
- Auth : requise
- Action : GetMyTopicsAction
- Réponse
200: identique àGET /api/topics(même forme, mêmes attributs, même tri(position, id)).
PUT /api/users/me/topics
Section titled “PUT /api/users/me/topics”Remplace toute la sélection de l’utilisateur. Pas d’add / remove granulaires : le picker UX est “coche les thèmes voulus, sauvegarde la liste entière”. L’opération est atomique (DELETE + INSERT en une transaction) — pas de fenêtre où l’utilisateur se retrouve sans sélection.
- Auth : requise
- Action : SetMyTopicsAction
- Corps accepté : forme aplatie OU forme JSON:API.
// flat{ "slugs": ["outdoor", "gastronomie", "famille", "culture", "sport"] }
// JSON:API{ "data": { "attributes": { "slugs": ["outdoor", "gastronomie", "famille", "culture", "sport"] } } }-
Réponse
200: la sélection persistée, hydratée dans la locale active — même forme queGET /api/users/me/topics. Le front peut donc remplacer son état local par cette réponse sans GET de suivi. -
Pipeline de validation (l’ordre fixe quel
meta.codesort en premier) :- Shape — chaque entrée doit être une string non vide après trim ;
- Duplicates — détectés avant la cardinalité, pour qu’un envoi
["outdoor","outdoor","outdoor"]retourneuserTopic.duplicatesplutôt queuserTopic.tooFew; - Cardinalité —
count(slugs) ∈ [USER_TOPICS_MIN, USER_TOPICS_MAX]; - Catalogue — chaque slug doit pointer un
topicis_active = 1. Les inactifs / inconnus sont énumérés dansmeta.unknownSlugs; - Persistance — DELETE intégral des
user_topicdu user puis INSERT multi-VALUES, le tout dans une transaction unique (pattern tx-join : rejoint la tx en cours si une est ouverte par un middleware).
-
Réponses d’erreur : toutes en
422avecsource.pointer = "/data/attributes/slugs"et unmeta.codeparmi la table plus haut. Exemple :
{ "errors": [{ "status": "422", "title": "Unknown slugs", "detail": "One or more slugs are not in the active catalogue.", "source": { "pointer": "/data/attributes/slugs" }, "meta": { "code": "userTopic.unknownSlugs", "unknownSlugs": ["foo", "bar"] } }]}