Modèle de données

Les quatre tables du module, les migrations idempotentes, les règles de slug et de visibilité.

Le module crée jusqu'à quatre tables, toutes préfixées lrvo_posts_. Elles sont créées et migrées par npx lrvo-db init, qui appelle le createTables exporté par le module. Voir lrvo-db.

Création sensible à la configuration

Toutes les tables ne sont pas créées systématiquement :

TableCréée quand
lrvo_posts_postToujours.
lrvo_posts_taxonomy_termAu moins un type déclare un champ taxonomy.
lrvo_posts_post_taxonomyIdem.
lrvo_posts_relationAu moins un type déclare un champ relation.

Ajouter une taxonomie ou une relation à un type existant impose donc de relancer npx lrvo-db init. La commande est idempotente : elle saute les tables déjà présentes et n'altère aucune donnée.

lrvo_posts_post

Une seule table pour tous les types de contenu. Ce qui les distingue est la valeur de post_type et la forme du JSON de data.

ColonneTypeRôle
idSERIAL PRIMARY KEYIdentifiant.
post_typeVARCHAR(100) NOT NULLClé du type déclaré.
slugVARCHAR(500) NOT NULLSegment d'URL, unique par type.
dataJSONB NOT NULL DEFAULT '{}'Valeurs des champs déclarés, plus template et seo.
visibilityVARCHAR(20) NOT NULL DEFAULT 'public'public ou private.
parent_idINTEGER REFERENCES lrvo_posts_post(id) ON DELETE SET NULLParent hiérarchique.
sort_orderINTEGER NOT NULL DEFAULT 0Position dans la fratrie.
created_atTIMESTAMPTZ DEFAULT CURRENT_TIMESTAMPModifiable par un champ created.
updated_atTIMESTAMPTZ DEFAULT CURRENT_TIMESTAMPÉcrit à chaque mise à jour.

Contrainte d'unicité : UNIQUE(post_type, slug).

Index : post_type, (post_type, slug), parent_id, (post_type, parent_id, sort_order), et un index GIN sur data qui rend interrogeables les valeurs des champs déclarés.

Ce qui vit dans data et ce qui n'y vit pas

Les champs de type slug, taxonomy, relation, parent, created, updated et visibility pilotent une colonne ou une table dédiée. Ils ne sont jamais écrits dans data.

Tous les autres champs déclarés y sont écrits sous leur nom, avec null quand la valeur est absente. Deux clés s'y ajoutent sans être déclarables :

  • template, l'identifiant du template de rendu, default en l'absence de choix ;
  • seo, l'objet de métadonnées, forcé à un objet vide si la valeur soumise n'en est pas un.

Seules les valeurs saisies sont stockées dans seo. La méta description et l'image de partage dérivées du contenu n'y sont jamais écrites : elles sont recalculées à chaque rendu depuis les champs du post. Voir Repli SEO automatique.

À la lecture, le module aplatit data au premier niveau de l'objet post tout en conservant l'objet brut sous _data. Un post lu porte donc post.title et post._data.title.

Les champs média dans data

Un champ image ou thumbnail est écrit comme l'identifiant du média, ou null. Un champ gallery est écrit comme un tableau ordonné d'identifiants, vide plutôt que null quand la galerie ne contient rien. C'est ce tableau qui porte l'ordre des images : la table de références du module Médias n'en garde pas trace.

Jamais d'URL ni de nom de fichier en base. À la lecture, chaque champ média est enrichi en { id, filename, alt_text, caption, mime_type } (un tableau de ces objets pour une galerie, dans le même ordre), en une seule requête pour tous les médias de tous les posts lus. Le texte alternatif et la légende sont donc toujours relus depuis la fiche du média, et une correction s'y propage partout.

Une référence dont le média a disparu apparaît comme { id, filename: null } au lieu de faire tomber la lecture.

lrvo_posts_taxonomy_term

ColonneTypeRôle
idSERIAL PRIMARY KEYIdentifiant.
post_typeVARCHAR(100) NOT NULLType auquel le terme appartient.
taxonomyVARCHAR(100) NOT NULLClé de la taxonomie, soit le nom du champ.
titleVARCHAR(255) NOT NULLLibellé du terme.
descriptionTEXTFacultative.
is_activeBOOLEAN DEFAULT trueUn terme inactif disparaît des sélecteurs et de la route publique.
created_atTIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
updated_atTIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP

Index sur (post_type, taxonomy) et sur is_active.

Aucune contrainte d'unicité sur le titre. Deux termes homonymes dans la même taxonomie sont acceptés par la base.

lrvo_posts_post_taxonomy

Table de liaison, plusieurs à plusieurs.

ColonneTypeRôle
idSERIAL PRIMARY KEY
post_idINTEGER NOT NULL REFERENCES lrvo_posts_post(id) ON DELETE CASCADE
taxonomyVARCHAR(100) NOT NULLRedondant avec le terme, sert au filtrage direct.
term_idINTEGER NOT NULL REFERENCES lrvo_posts_taxonomy_term(id) ON DELETE CASCADE

Contrainte : UNIQUE(post_id, taxonomy, term_id).

Supprimer un post ou un terme retire les liaisons correspondantes par cascade. La suppression d'un terme encore utilisé est cependant refusée en amont, par le module. Voir Taxonomies et relations.

lrvo_posts_relation

ColonneTypeRôle
idSERIAL PRIMARY KEY
post_idINTEGER NOT NULL REFERENCES lrvo_posts_post(id) ON DELETE CASCADEPost source.
field_nameVARCHAR(100) NOT NULLNom du champ relation qui porte le lien.
related_post_idINTEGER NOT NULL REFERENCES lrvo_posts_post(id) ON DELETE CASCADEPost cible.
sort_orderINTEGER DEFAULT 0Position dans la sélection.

Contrainte : UNIQUE(post_id, field_name, related_post_id). Un même post ne peut donc pas être lié deux fois par le même champ.

sort_order reçoit la position dans le tableau soumis, ce qui rend l'ordre de sélection significatif et durable.

Règles de slug

Le slug est dérivé du champ slug s'il est renseigné, du champ title sinon, et de l'identifiant du post en dernier recours, sous la forme post-<id>.

Ce dernier recours sert surtout aux types qui ne déclarent pas de champ title : ils n'ont aucune source textuelle dont tirer une URL. L'identifiant est réservé sur la séquence de la table avant l'insertion, ce qui permet de construire le slug définitif en une seule écriture. Le résultat reste modifiable à la main dans le formulaire.

La normalisation met en minuscules, retire les accents, remplace les espaces par des tirets, puis supprime purement tout caractère hors [a-z0-9-]. Les barres obliques, les points et les soulignés disparaissent donc au lieu d'être convertis. Une suite de tirets est ensuite réduite à un seul, et les tirets de tête et de queue sont retirés. Un titre entièrement composé de caractères non latins produit un slug vide, ramené lui aussi à post-<id>.

L'unicité est vérifiée sur le couple (type de post, slug). En cas de collision, le module suffixe -2, puis -3, jusqu'à trouver une valeur libre.

Il n'existe aucune liste de slugs réservés. Contrairement à @lrvo/module-pages, rien n'empêche un slug admin ou api, parce que l'URL publique d'un post est toujours préfixée par son type et ne peut donc pas entrer en collision avec une route du projet.

À la mise à jour, le slug n'est recalculé que si le champ est explicitement soumis. Modifier le titre d'un post publié ne change donc jamais son URL.

Visibilité

La colonne accepte public ou private. Toute autre valeur est rejetée avec Invalid visibility.

Le filtrage est appliqué à trois endroits, indépendamment :

  • les routes publiques de liste et de détail ne renvoient que les posts publics ;
  • le résolveur de rendu public refuse un post non public ;
  • la résolution d'une URL imbriquée exige que chaque maillon de la chaîne soit public.

Un parent privé masque donc toute sa descendance, même publiée.

Migrations

Trois migrations idempotentes sont exécutées à chaque lrvo-db init, y compris sur une base déjà à jour.

MigrationEffet
Colonne visibilityADD COLUMN IF NOT EXISTS visibility VARCHAR(20) NOT NULL DEFAULT 'public'.
Colonne parent_idAjoute la clé étrangère auto-référencée et son index.
Colonne sort_orderAjoute la colonne et l'index (post_type, parent_id, sort_order).

Les deux dernières sont neutres pour un projet qui n'utilise ni hiérarchie ni ordre manuel : la colonne reste vide ou à zéro.

Migration de l'ancien schéma de catégories

Une quatrième migration se déclenche uniquement si la table lrvo_posts_category est encore présente. Elle transpose chaque catégorie en terme de taxonomie, reporte les classements des posts dans la table de liaison, supprime la colonne category_id, puis supprime l'ancienne table.

La taxonomie de destination est la première taxonomie déclarée sur le type concerné, ou categories si le type n'en déclare aucune. Un projet issu de cet ancien schéma a donc intérêt à déclarer ses taxonomies dans le bon ordre avant de lancer la migration.

Notes

  • dropTables supprime les quatre tables plus l'ancienne table de catégories, dans un ordre qui respecte les clés étrangères.
  • Supprimer un post détache d'abord ses références média, parce que la table de références du module Médias interdit la suppression d'un média encore lié. Les fichiers eux-mêmes sont conservés.
  • Retirer un type de la configuration ne supprime aucune ligne. Les posts de ce type deviennent simplement invisibles de l'admin et du rendu public.

Sur cette page