Guide : creer un nouveau profil OPCO
Vue d'ensemble
Creer un profil pour un nouvel OPCO consiste a :
- Creer le dossier du profil
- Identifier les particularites de l'API
- Definir les transforms partages
- Definir chaque route
- Assembler le profil
- Tester
- Enregistrer dans le systeme
Etape 1 : creer le dossier
src/profiles/opco-xxx/
index.ts
shared-transforms.ts
get-dossiers.ts
post-dossiers.ts
...Convention : 1 fichier par route, un fichier pour les transforms partages.
Etape 2 : identifier les particularites
Avant de coder, tester l'API cible pour decouvrir ses divergences :
- Formats de date : ISO datetime ? YYYY-MM-DD ? DD/MM/YYYY ?
- Casse des champs : noms en majuscules ? camelCase ? snake_case ?
- Valeurs null : l'API accepte null ? plante ? ignore ?
- Enums : typos ? valeurs non documentees ? nommage incoherent ?
- Structure reponse : paginee ? wrappee ? array brut ?
- Types : string attendu pour des nombres ? boolean en 0/1 ?
- Multipart : quels endpoints ? quel nom de champ metadata ?
Comparer les reponses reelles avec la spec (quand elle existe). Le swagger n'est pas fiable -- tester en vrai.
Etape 3 : transforms partages
Definir les transforms reutilisables dans shared-transforms.ts :
import { t } from '../field-transform.js'
// Exemple : OPCO XXX veut les dates en DD/MM/YYYY
export const frenchDate = t.edit((v) => {
const [y, m, d] = String(v).split('-')
return `${d}/${m}/${y}`
})
// Exemple : OPCO XXX a ses propres codes d'etat
export const ETAT_MAPPING: Record<string, string> = {
'ACCEPTED': 'ENGAGE',
'PENDING': 'EN_COURS_INSTRUCTION',
}Etape 4 : definir les routes
Un fichier par route. Importer depuis ../dsl.js et depuis ./shared-transforms.js.
// post-dossiers.ts
import { defineRoute } from '../dsl.js'
import { frenchDate, ETAT_MAPPING } from './shared-transforms.js'
export const opcoXxxPostDossiers = defineRoute('/dossiers', 'POST', {
dest: '/api/v2/dossiers',
rateLimit: { maxPerMinute: 30 },
requestTransform: (t) => {
t.transform('contrat.dateDebut', frenchDate)
t.transform('contrat.dateFin', frenchDate)
t.stripNulls()
},
responseTransform: (t) => {
t.mapDeep('statut', ETAT_MAPPING)
},
})Pour les endpoints multipart :
export const opcoXxxPostFactures = defineRoute('/factures', 'POST', {
dest: {
endpoint: '/api/v2/factures',
format: 'multipart',
multipart: { metadata: 'donnees' },
},
})Pour les routes composites (multi-etapes) :
import { defineCompositeRoute } from '../dsl.js'
export const opcoXxxPostDossierComplet = defineCompositeRoute(
'/dossiers', 'POST',
[
{
dest: '/api/v2/dossiers',
responseTransform: (t) => { t.next('id') },
},
{
dest: '/api/v2/conventions',
requestTransform: (t) => {
t.transform('dossierId', t.previous())
},
},
],
)Etape 5 : assembler le profil
// index.ts
import { defineProfile } from '../dsl.js'
import { opcoXxxPostDossiers } from './post-dossiers.js'
// ... autres routes
export default defineProfile('opco-xxx', {
sync: { batchSize: 100 },
poll: { intervalMs: 600_000 },
routes: [
opcoXxxPostDossiers,
// ... autres routes
],
})Etape 6 : tester
Tests unitaires des transforms
Tester les FieldTransform isoles :
import { frenchDate } from '../opco-xxx/shared-transforms.js'
it('convertit YYYY-MM-DD en DD/MM/YYYY', () => {
expect(frenchDate.apply('2024-01-15')).toBe('15/01/2024')
})Tests d'integration des routes
Tester l'aller-retour defineRoute + applyOps :
import { opcoXxxPostDossiers } from '../opco-xxx/post-dossiers.js'
import { applyOps } from '../engine.js'
it('transforme la requete correctement', () => {
const input = { contrat: { dateDebut: '2024-01-15', dateFin: null } }
const result = applyOps(input, opcoXxxPostDossiers.requestOps)
expect(result.contrat.dateDebut).toBe('15/01/2024')
expect('dateFin' in result.contrat).toBe(false) // stripNulls
})Tester contre l'API reelle
Utiliser le profil mock (opco-mock/) comme modele pour les tests d'integration. Comparer les reponses reelles avec les reponses attendues.
Etape 7 : enregistrer
Le profil est charge automatiquement par le ProfileAdapter. Creer l'instance dans la configuration de l'environnement en passant les credentials :
import profile from './profiles/opco-xxx/index.js'
import { ProfileAdapter } from './profiles/adapter.js'
const adapter = new ProfileAdapter(profile, {
clientId: env.OPCO_XXX_CLIENT_ID,
clientSecret: env.OPCO_XXX_CLIENT_SECRET,
tokenUrl: 'https://api.opco-xxx.fr/oauth/token',
baseUrl: 'https://api.opco-xxx.fr',
})Checklist
- [ ] Dossier
src/profiles/opco-xxx/cree - [ ]
shared-transforms.tsavec les transforms reutilisables - [ ] 1 fichier par route
- [ ]
index.tsassemble le profil avecdefineProfile - [ ] Tests unitaires des transforms
- [ ] Tests d'integration des routes
- [ ] Test contre l'API reelle (ou mock)
- [ ] Pas de duplication (utiliser
shared-transforms.ts) - [ ]
stripNulls()si l'API ne tolere pas les null