Skip to content

BDD-127 — Correctifs création de source (API Fiches, dates, en-têtes vides)

1. User Story

En tant qu'administrateur, je veux créer une source à partir d'une Fiche d'une connexion API (en plus des Formulaires), afin de transformer toutes les soumissions accessibles via la connexion en table PostgreSQL sans repasser par la page « Connexions ».

Ce ticket regroupe trois correctifs complémentaires identifiés après [BDD-118] et [BDD-126] :

  • exposition des Fiches dans le wizard de création de source ;
  • dates : préservation des valeurs nulles + extension des formats reconnus ;
  • en-têtes vides ou en doublon : normalisation systématique au parsing pour éviter les collisions de colonnes.

2. Critères d'acceptance

#CritèreStatut
1Depuis /sources/create, mode Depuis une API, je peux choisir entre Formulaires et Fiches dès que la connexion sélectionnée est de type FORMULAIRES
2Si la connexion sélectionnée n'est pas de type FORMULAIRES, le toggle Formulaires/Fiches et le sélecteur suivant sont masqués (bandeau explicatif + Suivant désactivé)
3Le super-admin peut créer une source API depuis n'importe quelle entreprise (filtrage + tri du sélecteur de connexion par entreprise, parité avec le mode fichier)
4À la création d'une source, une cellule nulle / vide / "null" / "undefined" sur une colonne typée date reste NULL en base (plus de fallback 1970-01-01T01:00:00.000Z)
5Les dates Excel sentinelles (1899-12-30, epoch Unix, 0) sont aussi traitées comme NULL sur les colonnes date
6La détection automatique du format reconnaît ISO 8601 (avec / sans heure), YYYY-MM-DD, DD/MM/YYYY, MM/DD/YYYY, DD-MM-YYYY, MM-DD-YYYY, DD.MM.YYYY, YYYY/MM/DD, DD/MM/YY (et variantes avec heures HH:mm[:ss])
7Le parsing d'une colonne date essaie en cascade tous les formats reconnus, avec priorité au dateFormat fourni par le DTO côté UI
8Les en-têtes vides sont remplacés par Sans titre N (incrément stable, par ordre d'apparition) ; les doublons sont dédoublonnés en suffixant _2, _3
9La normalisation s'applique uniformément aux sources fichier (CSV / Excel / JSON) et API (formulaires / fiches) — les lignes sont remappées sur les en-têtes normalisés

3. Périmètre fonctionnel

Backend — Sources

FichierRôle
back/src/api/sources/services/date-parse.util.tsNouveau. Catalogue de formats (SOURCE_DATE_FORMAT_CANDIDATES), parsing strict (parseDateWithFormat), parsing en cascade (parseSourceDateValue), détection (isParseableAsSourceDate), inférence (inferBestDateFormat), mapping UI (toUiInferredDateFormat, coerceUiDateFormat, type UiSourceDateFormat)
back/src/api/sources/services/cell-value.util.tsNouveau. Helpers de coercition cellule : isBlankCellValue, isPlaceholderDateValue (placeholders Excel / epoch / 1970-01-01T00:xx), coerceSqlCellValue (force NULL selon le columnType), stringifyCellValue (sérialisation robuste, gère BigInt / Symbol / objets via JSON.stringify)
back/src/api/sources/services/parsed-headers.util.tsNouveau. normalizeParsedHeaders (Sans titre N + dédoublonnage), remapRowsToHeaders (remap des lignes sur les en-têtes normalisés), applyNormalizedHeaders (helper combiné)
back/src/api/sources/services/file-parser.service.tsDélègue la détection de dates et la normalisation des en-têtes aux nouveaux utilitaires. normalizeExcelValueCell neutralise les dates sentinelles
back/src/api/sources/sources.service.tsnormalizeTypedRows et buildParsedFromApiRows utilisent cell-value.util ; ajout de stringifySourceCellValue (fix linter [object Object]) ; validation stricte des dates sur les colonnes typées date (erreur 400 si valeur réellement non parsable)
back/src/api/sources/services/schema-table.service.tsloadTable / insertRows passent toute valeur via coerceSqlCellValueNULL SQL pour les cellules vides ou les placeholders de date (préservation du 0 numérique légitime)
back/src/api/connections/browse/connection-browse.service.tsNormalisation des en-têtes API (normalizeParsedHeaders) + formatCell retourne null pour les chaînes vides

Frontend — Création de source

FichierRôle
front/src/pages/admin/sources/AdminSourceCreatePage.vueToggle q-btn-toggle Formulaires / Fiches affiché uniquement si la connexion choisie est de type FORMULAIRES ; bandeau explicite sinon. Sélecteur de connexion API enrichi pour le super-admin (filtre q-select use-input par nom / entreprise, tri par entreprise puis par nom). Ajout du flux fetchCards et du payload `apiResourceType: 'form'
front/src/components/sources/SourcePreviewPanel.vueSuppression du fallback ad-hoc colonne_sans_nom (normalisation faite en amont). Affichage pour les cellules null
front/src/stores/source-store.jsPropagation de apiResourceType, cardUuid, cardTitle dans createSource et getApiSourcePreview
front/src/stores/connection-browse-store.jsfetchCards, resetCards, cardsLoading, cardsError (parité Formulaires / Fiches)

4. Détail des correctifs

A. Accès aux Fiches dans la création de source

  • Wizard étape 1, mode Depuis une API :
    • Tant qu'aucune connexion n'est sélectionnée, seul le sélecteur de connexion est affiché.
    • Connexion non FORMULAIRES : bandeau d'avertissement (q-banner) + bouton Suivant désactivé. Aucun sélecteur de ressource n'est affiché — la suite du parcours n'a pas de sens.
    • Connexion FORMULAIRES : affichage du toggle Formulaires / Fiches + sélecteur de ressource (use-input, filtrage local, affichage du publicationStatus).
  • Payload POST /sources (mode API) :
    json
    {
      "source": "api",
      "name": "Inscriptions 2026",
      "tableName": "inscriptions_2026",
      "connectionUuid": "<uuid>",
      "apiResourceType": "form" | "card",
      "formUuid": "<uuid>"  // si form
      "formTitle": "Inscriptions 2026",
      "cardUuid": "<uuid>"  // si card
      "cardTitle": "Fiches clients"
    }
  • Préview API : GET /sources/api-preview?connectionUuid=…&{form|card}Uuid=…&apiResourceType=….

B. Préservation des dates nulles + extension des formats

  • Placeholders nullifiés sur les colonnes typées date (coerceSqlCellValue('date')) :
    • null, undefined, chaîne vide après trim
    • chaînes "null" / "undefined" (insensibles à la casse)
    • 0, "0", "0.0", "0,0"
    • Date(0) (epoch Unix), 1970-01-01T00:00:00.xxxZ proche de l'epoch (≤ 2h, pour absorber les biais de timezone)
    • 1899-12-30 (epoch Excel Windows), 1900-01-01
  • Formats reconnus (SOURCE_DATE_FORMAT_CANDIDATES) :
    • ISO_8601 (avec heure, fuseau, millisecondes)
    • YYYY-MM-DD[ HH:mm[:ss]]
    • YYYY/MM/DD[ HH:mm[:ss]]
    • DD/MM/YYYY[ HH:mm[:ss]], DD/MM/YY[ HH:mm[:ss]]
    • MM/DD/YYYY[ HH:mm[:ss]]
    • DD-MM-YYYY[ HH:mm[:ss]], MM-DD-YYYY[ HH:mm[:ss]]
    • DD.MM.YYYY[ HH:mm[:ss]]
  • Parsing en cascade : parseSourceDateValue(value, preferredFormat?) essaie en priorité le format fourni par le DTO, puis chaque format candidat. Retourne null si aucun ne matche.
  • Inférence (inferBestDateFormat) : score sur l'échantillon ; un format n'est retenu que si ≥ 80 % des lignes non vides l'acceptent.
  • Mapping UI (toUiInferredDateFormat) : projection des 17 formats internes sur les 4 formats acceptés par le DTO front (DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD, DD-MM-YYYY).
  • Validation stricte : si une valeur non vide d'une colonne date n'est parsable par aucun format candidat, validateTypedColumns lève une BadRequestException (« Format de date invalide… »).

C. En-têtes vides et dédoublonnage

  • normalizeParsedHeaders(headers) :
    • en-tête vide / null / espaces uniquement → Sans titre 1, Sans titre 2, … (incrément stable par ordre d'apparition)
    • en-tête déjà rencontré → suffixe _2, _3… (premier conservé tel quel)
    • normalisation appliquée avant toute génération de nom de colonne SQL
  • remapRowsToHeaders(rows, originalHeaders, normalizedHeaders) : les lignes sont reprojetées sur les nouvelles clés. En cas de doublon réel (même clé d'origine pointant vers plusieurs en-têtes), la même valeur est dupliquée sur les colonnes normalisées (comportement conscient, documenté dans le test).
  • applyNormalizedHeaders(parsed) : helper combiné utilisé par CSV / Excel / JSON / API.
  • Côté UI : SourcePreviewPanel.vue n'a plus besoin de patcher l'affichage — la normalisation est faite côté serveur, donc cohérente entre l'aperçu, la création et le rechargement.

5. Règles métier implémentées

  • API XOR ressource : apiResourceType est obligatoire en mode API et doit être cohérent avec le couple formUuid / cardUuid (un seul des deux est attendu).
  • Connexion FORMULAIRES only (provisoire) : la création de source depuis une connexion d'un autre type est bloquée côté UI. Le back accepte techniquement la valeur mais retourne une erreur explicite si le type ne supporte pas le apiResourceType demandé.
  • Date NULL vs erreur :
    • valeur vide sur colonne dateNULL SQL (jamais d'erreur, jamais d'epoch).
    • valeur non vide non parsable sur colonne date400 BadRequest (sécurise l'utilisateur contre un mapping incorrect).
  • Préservation du 0 numérique : coerceSqlCellValue('number', 0) retourne "0" ; seul le 0 sur colonne date est traité comme placeholder.
  • Headers : la normalisation est idempotente — la rejouer ne change rien (Sans titre 1 reste Sans titre 1).
  • Stringification : stringifyCellValue distingue string / number / boolean / bigint / symbol / object pour éviter le [object Object] qui apparaissait sur certaines colonnes API renvoyant un objet (fichier joint, choix multiple).

6. Points de vérification

Création de source — mode API

  • Sur /sources/create :
    • Le toggle Depuis un fichier / Depuis une API affiche le sélecteur de connexion uniquement en mode API.
    • Une connexion FORMULAIRES débloque le toggle Formulaires / Fiches.
    • Une connexion d'un autre type masque le toggle et affiche le bandeau ; Suivant est désactivé.
    • Super-admin : la liste des connexions est triée par entreprise puis par nom, filtrable par le q-input intégré au q-select.
    • Le payload POST /sources contient bien apiResourceType et soit formUuid (+ formTitle) soit cardUuid (+ cardTitle).

Dates

  • Importer un CSV avec une colonne date_naissance contenant 1990-05-12, 12/05/1990, 1990/05/12, 12.05.1990, 1990-05-12T08:30:00Z : tous parsés en ISO 8601.
  • Importer un CSV avec une colonne date_naissance partiellement vide ("", null, 1970-01-01T01:00:00Z) : ces lignes sont NULL en base, pas d'erreur 400.
  • Importer un CSV avec une vraie valeur incorrecte (abc) sur une colonne date → 400 avec message « Format de date invalide… ».
  • GET /sources/:uuid/data : les NULL sortent en null dans le JSON (front affiche ).

En-têtes

  • Importer un CSV ";;A;A;Nom" → en-têtes finaux ["Sans titre 1", "Sans titre 2", "A", "A_2", "Nom"].
  • Importer un Excel avec une colonne vide entre deux colonnes nommées → même résultat (Sans titre N).
  • Importer une réponse API formulaires dont deux questions ont le même libellé → suffixe _2.

7. Tests associés

Backend (unitaires)

  • back/src/api/sources/services/date-parse.util.spec.ts38 tests : tous les formats supportés, parsing strict, cascade, inférence (seuil 80 %), mapping UI, normalisation d'années à 2 chiffres.
  • back/src/api/sources/services/cell-value.util.spec.ts22 tests : isBlankCellValue, isPlaceholderDateValue (epoch, Excel 1899-12-30, 1970-01-01T00:xx), coerceSqlCellValue (préservation du 0 numérique), stringifyCellValue (string, number, boolean, bigint, symbol, objet, null, undefined).
  • back/src/api/sources/services/parsed-headers.util.spec.ts18 tests : normalizeParsedHeaders (vides, doublons, ordre), remapRowsToHeaders, applyNormalizedHeaders.
  • back/src/api/sources/services/file-parser.service.spec.ts — mises à jour : CSV (uniques après PapaParse), Excel (Sans titre N), inférence ISO 8601 + DD.MM.YYYY, mapping ISO_8601 → YYYY-MM-DD et DD.MM.YYYY → DD-MM-YYYY.
  • back/src/api/sources/services/schema-table.service.spec.tsloadTable force NULL sur cellules placeholder date ; insertRows force NULL sur chaînes vides ; 0 numérique préservé.
  • back/src/api/sources/sources.service.spec.ts — préservation null sur colonnes date, parsing en cascade, 400 sur valeur non parsable, normalisation des en-têtes lors d'un createSource mode API.
  • back/src/api/connections/browse/connection-browse.service.spec.ts — en-têtes multiples vides → Sans titre N, doublons → _2, chaînes vides → null, loadCardSubmissions couvert.

Backend (e2e)

  • back/test/e2e/connections-controller.e2e-spec.ts — refonte des tokens E2E (sub / enterprise dédiés, génération beforeEach, seed enterprises) pour fiabiliser la suite (passage du flaky 401/403 sur l'external-client).

Frontend (unitaires)

  • front/src/__tests__/components/sources/SourcePreviewPanel.spec.js — loading (q-skeleton), erreur (q-banner), affichage Sans titre N brut, fallback pour null, génération de colonnes par défaut si la prop columns est vide.
  • front/src/__tests__/pages/admin/sources/AdminSourceCreatePage.spec.js — toggle Formulaires/Fiches conditionnel, fetchCards déclenché au switch, filtrage / tri super-admin, payload apiResourceType, step1Valid = false si type connexion non FORMULAIRES.

Frontend (e2e Playwright)

  • front/e2e/pages/admin/admin-sources.spec.js — 4 nouveaux tests mode API :
    • bascule via le toggle déclenche GET /connections ;
    • connexion non FORMULAIRES → bannière + Suivant désactivé + absence du toggle Formulaires/Fiches ;
    • bascule sur Fiches déclenche GET /connections/:uuid/cards ;
    • happy path création depuis un formulaire (payload { source: 'api', apiResourceType: 'form', connectionUuid, formUuid, name, tableName } vérifié).
  • front/e2e/pages/connections/connection-create.spec.jsnouveau fichier, 3 tests pour ConnectionCreatePage.vue (happy path, clé API absente, erreur 400).
  • front/e2e/pages/connections/connection-browse.spec.jsnouveau fichier, 3 tests pour ConnectionBrowsePage.vue : redirection /sources/create?formUuid=…&formTitle=…, bouton « Créer une source » masqué sans sélection, redirection cardUuid=…&cardTitle=… (mode Fiches).
  • front/e2e/pages/connections/connections.spec.js — recentré sur ConnectionsPage.vue (liste, clé API entreprise, menu, renommage, suppression). Les tests create / browse ont été déplacés vers les nouveaux fichiers ci-dessus.

8. Notes d'implémentation

  • Modularisation : extraction de trois utilitaires purs (date-parse.util, cell-value.util, parsed-headers.util) pour permettre un test isolé et une réutilisation entre file-parser.service, sources.service, schema-table.service et connection-browse.service.
  • Compatibilité PapaParse : la lib normalise déjà les doublons côté CSV. La normalisation custom intervient quand même pour homogénéiser CSV / Excel / JSON / API, car PapaParse n'aligne pas la convention _2 / _3 ni Sans titre N.
  • stringifyCellValue : ajouté pour silencier la règle ESLint @typescript-eslint/restrict-template-expressions ([object Object]) tout en garantissant une sérialisation déterministe sur les colonnes API qui peuvent renvoyer des objets (fichier joint, choix multiple).
  • Validation stricte vs tolérance : sur une colonne date, le ticket choisit explicitement de tolérer les vides (NULL) et de refuser les valeurs réellement non parsables. C'est cohérent avec la promesse de typage : une colonne date doit pouvoir être filtrée / triée comme une date.
  • E2E connections-controller : la refonte du setup (sub / enterprise dédiés + génération beforeEach) est un effet de bord du ticket — la suite était flaky en CI selon l'ordre d'exécution des suites.
  • Hors scope (volontairement) :
    • création d'une source à partir d'une connexion d'un type autre que FORMULAIRES (bandeau de blocage UI, le back n'a pas vocation à supporter ce cas dans ce ticket) ;
    • rotation de format de date après création (le dateFormat reste figé sur la colonne) — la cascade de parsing au reload reste tolérante mais le DTO reste sur les 4 formats UI ;
    • gestion d'en-têtes contenant uniquement des caractères de contrôle (rare et hors observation terrain).

9. Variables d'environnement

Aucune nouvelle variable. Toutes les modifications sont logiques (utilitaires + UI). Les variables existantes (FORMULAIRES_API_URL, CONSOLE_ADMIN_API_*, ENCRYPTION_KEY, FILE_SIZE_LIMIT) restent inchangées.