Modèle de données

Tables lrvo_forms_form et lrvo_forms_submission, structure des champs et des réglages, absence de migration.

Le module crée deux tables, toutes deux préfixées lrvo_forms_. Elles sont créées par npx lrvo-db init, qui appelle le createTables exporté par le module. Voir lrvo-db.

Le module n'a aucune migration de schéma. Les tables sont créées si elles sont absentes, et laissées strictement intactes sinon. Une évolution du schéma demanderait une intervention manuelle, ce qui n'a jamais été nécessaire jusqu'ici.

lrvo_forms_form

ColonneTypeRôle
idSERIAL PRIMARY KEYIdentifiant. C'est ce que stocke le bloc du BlockEditor.
slugVARCHAR(200) NOT NULL UNIQUEIdentifiant d'URL, utilisé par la page publique et par FormEmbed.
titleVARCHAR(500) NOT NULLTitre affiché.
descriptionTEXTFacultative, affichée sous le titre.
fieldsJSONB NOT NULL DEFAULT '[]'La liste ordonnée des champs.
settingsJSONB NOT NULL DEFAULT '{}'Les réglages du formulaire.
statusVARCHAR(20) NOT NULL DEFAULT 'active'active ou inactive.
created_atTIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
updated_atTIMESTAMPTZ DEFAULT CURRENT_TIMESTAMPÉcrit à chaque modification.

Index sur status.

Le statut n'a pas de contrainte de vérification en base : il est validé côté module, qui refuse toute valeur autre que active ou inactive. Une écriture directe en SQL pourrait donc y placer autre chose.

Slug

Dérivé du slug soumis ou, à défaut, du titre : minuscules, accents retirés, espaces en tirets, puis suppression de tout caractère hors [a-z0-9-].

L'unicité est globale. En cas de collision, le module suffixe -2, puis -3.

Le titre est obligatoire à la création comme à la mise à jour, où il ne peut pas être vidé.

Structure de fields

Un tableau d'objets, dans l'ordre de rendu. Chaque champ porte au minimum id, type, label, width, et name pour les types qui collectent une valeur.

Il n'existe aucun schéma de base sur ce JSON, et donc aucune contrainte d'intégrité. Un champ mal formé n'est pas détecté à l'écriture, il produira une anomalie au rendu.

Voir Champs pour la structure complète par type.

Structure de settings

Toujours normalisé à l'écriture : chaque branche absente est remplie avec sa valeur par défaut, de sorte qu'un formulaire lu porte toujours l'arbre complet.

CléDéfautContenu
submitLabelEnvoyerTexte du bouton.
success.modemessagemessage ou redirect. Toute autre valeur retombe sur message.
success.messageRemerciement génériqueAffiché après envoi.
success.redirectUrlvideDestination en mode redirection.
notify.enabledfalseNotification à l'équipe.
notify.recipients[]Adresses destinataires.
notify.subjectvideSujet, un défaut est calculé s'il est vide.
confirmation.enabledfalseAccusé de réception au visiteur.
confirmation.toFieldvideNom du champ courriel à utiliser.
confirmation.subjectvideSujet, un défaut est calculé s'il est vide.
confirmation.messagevideCorps personnalisé.
antiBot.minSeconds3Délai minimal de remplissage, jamais négatif.

lrvo_forms_submission

ColonneTypeRôle
idSERIAL PRIMARY KEYIdentifiant.
form_idINTEGER NOT NULL REFERENCES lrvo_forms_form(id) ON DELETE CASCADEFormulaire d'origine.
dataJSONB NOT NULL DEFAULT '{}'Réponses, indexées par nom de champ.
filesJSONB NOT NULL DEFAULT '[]'Métadonnées des pièces jointes.
metaJSONB NOT NULL DEFAULT '{}'Contexte de la requête.
statusVARCHAR(20) NOT NULL DEFAULT 'new'new, read ou spam.
created_atTIMESTAMPTZ DEFAULT CURRENT_TIMESTAMPDate de réception.

Index sur form_id, sur status, et sur le couple (form_id, status) qui est l'axe de filtrage de l'admin.

Il n'y a pas de colonne de modification. Une soumission n'est jamais éditée, seul son statut change.

data

Les clés sont les name des champs, pas leurs identifiants ni leurs libellés. Les valeurs sont des chaînes, sauf pour deux types : checkbox produit un booléen, checkboxes produit un tableau.

Renommer un champ après réception de soumissions rompt donc le lien avec les réponses déjà reçues, qui restent stockées sous l'ancienne clé.

files

Un tableau d'objets { fieldName, key, filename, size, contentType }. key est la clé de l'objet dans le stockage, de la forme forms/<identifiant du formulaire>/<nom unique>.

Le contenu du fichier n'est jamais en base. Il est servi uniquement par la route de téléchargement de l'admin, protégée par forms.view.

meta

{ ip, userAgent, referer }. L'adresse est lue dans x-forwarded-for, première entrée, puis dans x-real-ip. Chacune des trois vaut null quand l'en-tête correspondant est absent.

Elles sont prises telles quelles, sans la vérification LRVO_TRUST_PROXY appliquée à la clé de limitation de débit. Un appelant peut donc les falsifier : elles éclairent un diagnostic, elles ne prouvent rien.

Ces trois valeurs restent des données personnelles. En tenir compte dans la politique de rétention du projet.

Cycle de vie

ActionEffet en baseEffet au stockage
Soumission rejetée après téléversementAucune ligne créée.Aucun. Les fichiers déjà envoyés deviennent orphelins.
Suppression d'une soumissionLigne supprimée.Aucun. Les fichiers deviennent orphelins.
Suppression d'un formulaireFormulaire et soumissions supprimés en cascade.Aucun. Idem.
Duplication d'un formulaireNouveau formulaire, titre suffixé, slug ré-unicifié, statut inactive. Aucune soumission copiée.Aucun.

L'absence de nettoyage du stockage est à connaître : un projet qui reçoit beaucoup de pièces jointes accumule des objets sans référence. Prévoir une purge si le volume le justifie.

Notes

  • dropTables supprime les deux tables en cascade, dans l'ordre qui respecte la clé étrangère. Il ne supprime aucun fichier du stockage objet.
  • Aucune purge automatique n'existe sur les soumissions. Une politique de rétention est à mettre en place au niveau du projet.
  • Le commentaire d'en-tête du module mentionne une clé sourceUrl dans meta. Elle n'est jamais écrite, le tableau de cette page fait foi.

Sur cette page