Appearance
BDD-118 — Création d'une source depuis un formulaire API
1. User Story
En tant qu'administrateur, je veux pouvoir créer une source à partir des données d'un formulaire récupérées via une connexion API, afin d'exploiter ces données comme n'importe quelle autre source dans l'application.
2. Critères d'acceptation
| # | Critère | Statut |
|---|---|---|
| 1 | L'utilisateur peut créer une nouvelle source basée sur les données d'un formulaire API | ✅ |
| 2 | La nouvelle source est visible et exploitable comme les autres sources (liste, détail, export) | ✅ |
| 3 | En cas d'erreur lors de la création : un message explicite est affiché et la source n'est pas créée | ✅ |
Détail fonctionnel des critères
Création depuis un formulaire API
- Deux points d'entrée :
- depuis
/connections/:uuid/browse(après sélection d'un formulaire) → bouton « Créer une source » qui pré-remplit l'assistant (connectionUuid,formUuid,formTitle) et amène directement à l'étape Nommer. - depuis
/sources/create→ toggle Depuis un fichier / Depuis une API à l'étape 1 ; en mode API : sélection d'une connexionACTIVEpuis d'un formulaire.
- depuis
- Étapes du wizard identiques aux sources fichier : choix de la source → nom + nom de table → preview + typage des colonnes → import.
- Deux points d'entrée :
Source exploitable
- La source apparaît dans
/sourcesavec un badge d'origine « API Formulaire » (vs « Fichier »). - Détail (
/sources/:uuid), export CSV / XLSX, suppression : comportement identique aux sources document. - Le bouton Recharger est désactivé pour les sources
API_FORMavec un tooltip explicite (rechargement API hors périmètre — ticket suivant).
- La source apparaît dans
Erreurs explicites
- Connexion ou formulaire introuvable / supprimé → message dédié.
- Connexion non supportée (autre que
FORMULAIRES) → exception métier 400. - Formulaire sans colonnes exploitables →
SourceApiFormNoColumnsException. - Clé API révoquée → message déjà géré par
BDD-117(markConnectionAsError). - Nom de table déjà utilisé → bannière d'avertissement (remplacement explicite).
- En cas d'échec après preview, la source n'est pas persistée (transaction métier dans
createSource).
3. Périmètre fonctionnel
Base de données
| Fichier | Rôle |
|---|---|
sql/tool_14_add_api_form_sources.sql | Migration : enum SourceType (DOCUMENT / API_FORM), document_id nullable, colonnes api_connection_id, form_uuid, form_title, source_type, index |
back/prisma/schema.prisma | Enum SourceType, sources.document_id nullable, relation sources ↔ api_connections, colonnes API |
Backend
| Fichier | Rôle |
|---|---|
back/src/api/sources/sources.controller.ts | POST /sources unifié (discriminant source), GET /sources/table-check (document ou connexion), GET /sources/api-preview |
back/src/api/sources/sources.service.ts | createSource unifiée (helpers buildDocumentSourceContext / buildApiFormSourceContext), checkTable unifiée, getApiFormPreview, blocage reloadSource sur API_FORM |
back/src/api/sources/dto/source.dto.ts | CreateSourceDto unifié (source: 'document' | 'api') avec @ValidateIf ; SourceResponseDto enrichi (sourceType, connectionUuid, connectionName, formUuid, formTitle) |
back/src/api/sources/source.presenter.ts | Mapping document nullable + relation api_connection |
back/src/api/sources/sources.module.ts | Import de ConnectionsModule (injection ConnectionsService + ConnectionBrowseService) |
back/src/api/connections/connections.module.ts | Export de ConnectionBrowseService pour réutilisation par SourcesModule |
back/src/exceptions/source.exceptions.ts | SourceApiFormNoColumnsException, SourceApiReloadNotSupportedException |
Frontend
| Fichier | Rôle |
|---|---|
front/src/pages/admin/sources/AdminSourceCreatePage.vue | Wizard 3 étapes, toggle document/API, pré-remplissage via query, branche preview/submit selon le mode |
front/src/pages/admin/sources/AdminSourcesPage.vue | Badge d'origine (Fichier / API Formulaire), bouton Recharger désactivé pour API_FORM, contexte super-admin enrichi (connexion → formulaire) |
front/src/pages/connections/ConnectionBrowsePage.vue | Bouton « Créer une source » dans la barre de métriques, redirige vers /sources/create?connectionUuid=…&formUuid=…&formTitle=… |
front/src/stores/source-store.js | createSource unifiée, checkTableName({ tableName, documentUuid? | connectionUuid? }), getApiSourcePreview |
front/src/stores/connection-browse-store.js | Action resetForms (réinitialisation lors d'un changement de connexion) |
4. Endpoints API
| Méthode | Chemin | Guards | Description |
|---|---|---|---|
POST | /sources | AuthGuard, AdminGuard | Crée une source (source: 'document' ou 'api') |
GET | /sources/table-check | AuthGuard, AdminGuard | Vérifie disponibilité du nom de table (documentUuid ou connectionUuid exclusifs) |
GET | /sources/api-preview | AuthGuard, AdminGuard | Aperçu typé d'un formulaire API (20 premières lignes) |
Corps POST /sources — mode document
json
{
"source": "document",
"name": "RH Effectifs 2026",
"tableName": "rh_effectifs_2026",
"documentUuid": "uuid-doc",
"columns": [
{ "name": "nom", "selectedType": "text" },
{ "name": "naissance", "selectedType": "date", "dateFormat": "DD/MM/YYYY" }
]
}Corps POST /sources — mode API
json
{
"source": "api",
"name": "Inscriptions 2026",
"tableName": "inscriptions_2026",
"connectionUuid": "uuid-connexion",
"formUuid": "uuid-formulaire",
"formTitle": "Inscriptions 2026",
"columns": [
{ "name": "email", "selectedType": "text" }
]
}Réponse POST /sources (extrait, mode API)
json
{
"uuid": "...",
"name": "Inscriptions 2026",
"tableName": "inscriptions_2026",
"schemaName": "schema_aaaaaaaa...",
"rowCount": 42,
"status": "ACTIVE",
"sourceType": "API_FORM",
"documentUuid": null,
"folderUuid": null,
"connectionUuid": "...",
"connectionName": "API Formulaires prod",
"formUuid": "...",
"formTitle": "Inscriptions 2026",
"enterpriseName": "Acme Corp"
}Erreurs courantes
| Code | Cas |
|---|---|
400 | Body invalide (source manquant, documentUuid/connectionUuid incohérent), nom de table invalide, formulaire sans colonnes exploitables, connexion non supportée |
400 | POST /sources/:uuid/reload sur une source API_FORM (SourceApiReloadNotSupportedException) |
401 | Jeton absent / invalide |
403 | Rôle non admin |
404 | Document, connexion ou formulaire introuvable (ou hors périmètre entreprise) |
5. Règles métier implémentées
- Périmètre entreprise
administrateur: création limitée aux documents / connexions de sonenterprise_id.super-admin: voit toutes les entreprises (logique inchangée vs sources fichier).
- Modèle unifié :
sources.source_type(DOCUMENTpar défaut) ;document_idnullable ;api_connection_id,form_uuid,form_titlerenseignés uniquement enAPI_FORM.- Une source est rattachée à exactement une origine (XOR enforced par les helpers de service).
- Création en mode API :
- Récupération des réponses via
ConnectionBrowseService.loadFormResponses(clé API déchiffrée côté serveur, jamais exposée). - Seules les connexions de type
FORMULAIRESACTIVEsont éligibles. - Réutilisation du typage de colonnes existant (text / number / date / boolean) avec correction manuelle possible avant import.
- Nom de table contraint au regex
^[a-z][a-z0-9_]{0,62}$(idem sources fichier).
- Récupération des réponses via
- Rechargement :
POST /sources/:uuid/reloadsur une sourceAPI_FORM→ 400SourceApiReloadNotSupportedException(bouton désactivé côté front).- Le rechargement API sera traité dans un ticket dédié (BDD-119).
- Suppression : aucun changement, hard delete côté table métier (la connexion d'origine peut être supprimée — la source survit, l'origine devient orpheline).
6. Migration
À exécuter côté base tool après merge :
bash
psql "$DATABASE_URL_TOOL" -f sql/tool_14_add_api_form_sources.sql
yarn --cwd back db:generateLa migration :
- crée l'enum
SourceType, - ajoute
source_type,api_connection_id,form_uuid,form_titleàsources, - rend
document_idnullable, - ajoute les index
idx_sources_api_connection_idetidx_sources_source_type, - ajoute la FK
api_connection_id → api_connections.id.
Les sources existantes restent en source_type = 'DOCUMENT'.
7. Points de vérification
- Depuis
/connections/:uuid/browse: sélectionner un formulaire chargé → bouton « Créer une source » visible → arrive sur/sources/createétape Nommer pré-remplie (connexion + formulaire affichés en récapitulatif). - Depuis
/sources/create: toggle Depuis une API → liste des connexionsACTIVEchargée → sélection d'une connexion → liste des formulaires chargée. - Création complète mode API : aperçu typé, ajustement type / format de date, import, redirection
/sources, notification « Source "X" créée — N lignes chargées ». - Liste
/sources: badge API Formulaire sur la nouvelle source ; bouton Recharger désactivé avec tooltip « Rechargement API bientôt disponible ». - Détail
/sources/:uuid: affichage des données, export CSV / XLSX fonctionnels. - Suppression d'une connexion API : la source
API_FORMrattachée reste exploitable (lecture seule de la donnée déjà chargée). - Tentative de rechargement (
POST /sources/:uuid/reload) via API directe sur une sourceAPI_FORM→ 400 avec message explicite. - Erreurs : formulaire introuvable, clé API révoquée, nom de table invalide → messages clairs côté front, pas d'enregistrement partiel.
8. Tests associés
Backend (unitaires)
back/src/api/sources/sources.service.spec.tscreateSourcemode document : happy path, document introuvable, périmètre entreprise.createSourcemode API : happy path,connectionUuidmanquant (400), connexion type non supporté, connexion introuvable, formulaire sans colonnes (SourceApiFormNoColumnsException),formTitlevide accepté.checkTablemode document et mode API (XORdocumentUuid/connectionUuid).reloadSource→SourceApiReloadNotSupportedExceptionsisource_type === 'API_FORM'.getApiFormPreview: inférence des colonnes, limite à 20 lignes, connexion non supportée, formulaire sans colonnes.
Backend (e2e)
back/test/e2e/sources-controller.e2e-spec.tsPOST /sourcesmode document : 400 (sourcemanquant), 401, 403, 404 document inexistant, 201 happy path.POST /sourcesmode API (à compléter avec mockConnectionBrowseService) : 400 (formulaire vide), 404 connexion inconnue, 201.GET /sources/table-checkavecdocumentUuidet avecconnectionUuid.
Frontend (unitaires)
front/src/__tests__/stores/source-store.spec.jscreateSourcedocument & API (signature unifiée).checkTableNameavecdocumentUuid, avecconnectionUuid, sans UUID.getApiSourcePreview(avec / sansformTitle).
front/src/__tests__/stores/connection-browse-store.spec.js- Action
resetForms.
- Action
front/src/__tests__/pages/admin/sources/AdminSourceCreatePage.spec.js- Mode document inchangé (super-admin / admin,
checkTableName,getSourcePreview). - Bascule en mode API via le toggle → chargement des connexions, sélection →
resetForms+fetchForms. - Pré-remplissage depuis la query string → étape 2 directe,
fetchConnectionappelé. checkTableNameavecconnectionUuid,getApiSourcePreviewau passage à l'étape 3.submit: payloadsource: 'document'vssource: 'api'.
- Mode document inchangé (super-admin / admin,
front/src/__tests__/pages/admin/sources/AdminSourcesPage.spec.js- Badge
Fichier/API Formulaire. - Bouton Recharger désactivé pour
API_FORM, actif pourDOCUMENT. - Super-admin : contexte connexion → formulaire pour les sources API.
- Badge
front/src/__tests__/pages/connections/ConnectionBrowsePage.spec.js- Bouton Créer une source redirige vers
/sources/createavecconnectionUuid,formUuid,formTitle. formTitleomis si titre vide.
- Bouton Créer une source redirige vers
Frontend (e2e Playwright)
front/e2e/pages/connections/connections.spec.js- Bouton Créer une source → redirection avec les bons query params.
- Bouton absent tant qu'aucun formulaire n'est sélectionné.
9. Notes d'implémentation
- Unification volontaire des endpoints (
POST /sources,GET /sources/table-check) via un discriminantsource— évite la divergence entre les deux flux (preview, typage, validation, persistance partagés). - Le toggle à l'étape 1 a été préféré à des routes séparées (
/sources/create/documentvs/sources/create/api) pour conserver un seul wizard et une seule logique côté front. - Le bouton « Créer une source » sur
ConnectionBrowsePageréutilise le style des actions de la barre de métriques (icônetable_chart, tooltip) — il n'apparaît qu'après chargement des réponses, garantissant qu'on a un jeu de données valide à importer. - Le presenter
toSourceResponserenvoie systématiquement les deux blocs d'origine (documentUuid/folderUuidouconnectionUuid/formUuid/formTitle), avecnullpour le bloc non applicable — permet aux pages front de gérer les deux types sans branche conditionnelle au niveau de l'appel.
10. Hors périmètre (ticket suivant : BDD-119)
- Rechargement d'une source
API_FORM(refetch des réponses depuis Formulaires). - Ré-attribution d'une source à une autre connexion / formulaire après suppression.
- Synchronisation automatique périodique des sources API (cron / webhook).
- Rotation de clé API sur connexion existante.
