Appearance
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ère | Statut |
|---|---|---|
| 1 | Depuis /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 | ✅ |
| 2 | Si 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é) | ✅ |
| 3 | Le 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) | ✅ |
| 5 | Les dates Excel sentinelles (1899-12-30, epoch Unix, 0) sont aussi traitées comme NULL sur les colonnes date | ✅ |
| 6 | La 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]) | ✅ |
| 7 | Le parsing d'une colonne date essaie en cascade tous les formats reconnus, avec priorité au dateFormat fourni par le DTO côté UI | ✅ |
| 8 | Les 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… | ✅ |
| 9 | La 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
| Fichier | Rôle |
|---|---|
back/src/api/sources/services/date-parse.util.ts | Nouveau. 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.ts | Nouveau. 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.ts | Nouveau. 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.ts | Dé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.ts | normalizeTypedRows 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.ts | loadTable / insertRows passent toute valeur via coerceSqlCellValue → NULL 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.ts | Normalisation des en-têtes API (normalizeParsedHeaders) + formatCell retourne null pour les chaînes vides |
Frontend — Création de source
| Fichier | Rôle |
|---|---|
front/src/pages/admin/sources/AdminSourceCreatePage.vue | Toggle 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.vue | Suppression du fallback ad-hoc colonne_sans_nom (normalisation faite en amont). Affichage — pour les cellules null |
front/src/stores/source-store.js | Propagation de apiResourceType, cardUuid, cardTitle dans createSource et getApiSourcePreview |
front/src/stores/connection-browse-store.js | fetchCards, 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 dupublicationStatus).
- 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.xxxZproche 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. Retournenullsi 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
daten'est parsable par aucun format candidat,validateTypedColumnslève uneBadRequestException(« 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
- en-tête vide /
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.vuen'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 :
apiResourceTypeest obligatoire en mode API et doit être cohérent avec le coupleformUuid/cardUuid(un seul des deux est attendu). - Connexion
FORMULAIRESonly (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 leapiResourceTypedemandé. - Date
NULLvs erreur :- valeur vide sur colonne
date→NULLSQL (jamais d'erreur, jamais d'epoch). - valeur non vide non parsable sur colonne
date→400 BadRequest(sécurise l'utilisateur contre un mapping incorrect).
- valeur vide sur colonne
- Préservation du
0numérique :coerceSqlCellValue('number', 0)retourne"0"; seul le0sur colonnedateest traité comme placeholder. - Headers : la normalisation est idempotente — la rejouer ne change rien (
Sans titre 1resteSans titre 1). - Stringification :
stringifyCellValuedistinguestring/number/boolean/bigint/symbol/objectpour é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
FORMULAIRESdé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-inputintégré auq-select. - Le payload
POST /sourcescontient bienapiResourceTypeet soitformUuid(+formTitle) soitcardUuid(+cardTitle).
Dates
- Importer un CSV avec une colonne
date_naissancecontenant1990-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_naissancepartiellement vide ("",null,1970-01-01T01:00:00Z) : ces lignes sontNULLen base, pas d'erreur 400. - Importer un CSV avec une vraie valeur incorrecte (
abc) sur une colonnedate→ 400 avec message « Format de date invalide… ». GET /sources/:uuid/data: lesNULLsortent ennulldans 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.ts— 38 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.ts— 22 tests :isBlankCellValue,isPlaceholderDateValue(epoch, Excel1899-12-30,1970-01-01T00:xx),coerceSqlCellValue(préservation du0numérique),stringifyCellValue(string, number, boolean, bigint, symbol, objet, null, undefined).back/src/api/sources/services/parsed-headers.util.spec.ts— 18 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, mappingISO_8601 → YYYY-MM-DDetDD.MM.YYYY → DD-MM-YYYY.back/src/api/sources/services/schema-table.service.spec.ts—loadTableforceNULLsur cellules placeholder date ;insertRowsforceNULLsur chaînes vides ;0numérique préservé.back/src/api/sources/sources.service.spec.ts— préservationnullsur colonnes date, parsing en cascade, 400 sur valeur non parsable, normalisation des en-têtes lors d'uncreateSourcemode 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,loadCardSubmissionscouvert.
Backend (e2e)
back/test/e2e/connections-controller.e2e-spec.ts— refonte des tokens E2E (sub / enterprise dédiés, générationbeforeEach, 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), affichageSans titre Nbrut, fallback—pournull, génération de colonnes par défaut si la propcolumnsest vide.front/src/__tests__/pages/admin/sources/AdminSourceCreatePage.spec.js— toggle Formulaires/Fiches conditionnel,fetchCardsdéclenché au switch, filtrage / tri super-admin, payloadapiResourceType,step1Valid = falsesi type connexion nonFORMULAIRES.
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é).
- bascule via le toggle déclenche
front/e2e/pages/connections/connection-create.spec.js— nouveau fichier, 3 tests pourConnectionCreatePage.vue(happy path, clé API absente, erreur 400).front/e2e/pages/connections/connection-browse.spec.js— nouveau fichier, 3 tests pourConnectionBrowsePage.vue: redirection/sources/create?formUuid=…&formTitle=…, bouton « Créer une source » masqué sans sélection, redirectioncardUuid=…&cardTitle=…(mode Fiches).front/e2e/pages/connections/connections.spec.js— recentré surConnectionsPage.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 entrefile-parser.service,sources.service,schema-table.serviceetconnection-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/_3niSans 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 colonnedatedoit pouvoir être filtrée / triée comme une date. - E2E
connections-controller: la refonte du setup (sub / enterprise dédiés + générationbeforeEach) 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
dateFormatreste 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).
- création d'une source à partir d'une connexion d'un type autre que
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.
