/** * Schemas Hydra / API Platform 4. * * Important : API Platform 4 abandonne le prefixe `hydra:` dans les noms de * proprietes (compare a la version 3). Un GET /api/audit-logs renvoie : * { "@context": ..., "@id": ..., "@type": "...", * "member": [...], * "totalItems": 30, * "view": { "@id": ..., "@type": "...", "first": ..., "next": ..., ... } } * * En `application/json` (sans ld), API Platform retourne un simple tableau * plat sans ces metadonnees — on doit donc explicitement demander * `application/ld+json` (via l'option `headers: { Accept: ... }` de useApi) * pour avoir acces a la pagination. */ export interface HydraView { '@id'?: string '@type'?: string first?: string last?: string next?: string previous?: string } export interface HydraCollection { member: T[] totalItems: number view?: HydraView } export function extractHydraMembers(collection: HydraCollection): T[] { return collection.member ?? [] } /** * Une violation de contrainte API Platform (reponse 422). Le `propertyPath` * pointe le champ concerne, `message` est le libelle a afficher, `code` est le * code de contrainte Symfony (UUID stable, independant de la langue) — il sert * a surcharger un message back technique par une cle i18n (cf. * `VIOLATION_MESSAGE_I18N` / `resolveViolationMessage`). */ export interface ApiViolation { propertyPath: string message: string code: string } /** * Extrait les violations d'un payload d'erreur 422 d'API Platform 4. Supporte * les deux formats de negociation (`violations` ou `hydra:violations`) et * renvoie un tableau vide si le payload n'en contient pas d'exploitables. * * Utilise par useCategoryForm et tout futur composable de formulaire qui * doit mapper les violations serveur sur ses champs. */ export function extractApiViolations(data: unknown): ApiViolation[] { if (!data || typeof data !== 'object') return [] const record = data as Record const raw = record.violations ?? record['hydra:violations'] if (!Array.isArray(raw)) return [] const out: ApiViolation[] = [] for (const v of raw) { if (!v || typeof v !== 'object') continue const obj = v as Record out.push({ propertyPath: String(obj.propertyPath ?? ''), message: String(obj.message ?? ''), code: String(obj.code ?? ''), }) } return out } /** * Transforme un payload d'erreur 422 d'API Platform en dictionnaire * `{ propertyPath: message }`, directement consommable par la prop `:error` * des composants `Malio*` (le nom du champ cote front = le `propertyPath` * renvoye par le back). Fondation du mapping erreur→champ des formulaires : * utilise par `useFormErrors` (champs scalaires) et par les boucles de submit * de collections (erreur par ligne). * * Les violations sans `propertyPath` (erreur globale) sont ignorees ; en cas * de doublon de `propertyPath`, la derniere violation l'emporte. */ export function mapViolationsToRecord(data: unknown): Record { const out: Record = {} for (const v of extractApiViolations(data)) { if (v.propertyPath) out[v.propertyPath] = v.message } return out } /** * Surcharge i18n d'un message back par CODE de violation. * * La plupart des contraintes back portent deja un message FR explicite (ex. * `#[Assert\NotBlank(message: '...')]`) : on l'affiche tel quel. Mais certaines * 422 portent un message TECHNIQUE non montrable a l'utilisateur — typiquement * l'erreur de TYPE renvoyee par API Platform quand le back ne peut pas * denormaliser la valeur (date non parsable envoyee sur un champ * `DateTimeImmutable` : « Cette valeur doit être de type DateTimeImmutable|null. », * voire en anglais selon la negociation de langue). * * Plutot que de traduire/maquiller cote back, on surcharge ces messages cote * front via leur `code` de violation. Ce code est un UUID Symfony FIGE (contrat * de compatibilite : il ne change pas entre versions), donc bien plus robuste * qu'un match sur le texte du message (qui depend de la langue). La table * associe un code -> une cle i18n ; `resolveViolationMessage` l'applique. * * Limite a connaitre : le code de type-error est GENERIQUE (toute valeur de * mauvais type). Dans nos formulaires, seul un champ date saisi en texte libre * (MalioDate, qui forwarde la saisie brute invalide) le declenche, d'ou le * libelle « Date invalide ». Si un autre champ typé en saisie libre apparait, * affiner la resolution via `propertyPath` plutot que par code seul. */ export const VIOLATION_MESSAGE_I18N: Record = { // Symfony `Constraints\Type::INVALID_TYPE_ERROR` — valeur de mauvais type. 'ba785a8c-82cb-4283-967c-3cf342181b40': 'errors.validation.invalidDate', } /** * Resout le message a afficher pour une violation : si son `code` est surcharge * par `VIOLATION_MESSAGE_I18N`, renvoie la traduction de la cle associee ; * sinon, le message back tel quel (cas nominal). `t` est passe par l'appelant * (les utils sont purs, sans acces a useI18n). */ export function resolveViolationMessage(v: ApiViolation, t: (key: string) => string): string { const i18nKey = VIOLATION_MESSAGE_I18N[v.code] return i18nKey ? t(i18nKey) : v.message } /** * Extrait un message d'erreur lisible depuis un payload Hydra / JSON * d'erreur API Platform. Essaie les champs courants dans l'ordre : * `hydra:description` → `detail` → `description` → `message` → `error` → * `title` → `hydra:title`. Renvoie '' si rien d'exploitable. * * Si `data` est une string, la renvoie telle quelle (cas des erreurs * Symfony en text/plain ou des messages bruts). */ export function extractApiErrorMessage(data: unknown): string { if (typeof data === 'string') return data if (!data || typeof data !== 'object') return '' const record = data as Record return ( (record['hydra:description'] as string) ?? (record.detail as string) ?? (record.description as string) ?? (record.message as string) ?? (record.error as string) ?? (record.title as string) ?? (record['hydra:title'] as string) ?? '' ) }