defineEqsClient produit un client qui ne compile pas : operationName dérivé d'un operationId en ressource.action donne un identifiant TS invalide #34
Closed
opened 2026-09-09 17:54:02 +00:00 by nicolas-fryder
·
7 comments
No Branch/Tag specified
main
dev
@eqs/tooling@0.24.3
@eqs/testing@0.24.3
@eqs/telemetry@0.24.3
@eqs/platform-next@0.24.3
@eqs/platform-nest@0.24.3
@eqs/http@0.24.3
@eqs/errors@0.24.3
@eqs/config@0.24.3
@eqs/cli@0.24.3
@eqs/auth@0.24.3
@eqs/tooling@0.24.2
@eqs/testing@0.24.2
@eqs/telemetry@0.24.2
@eqs/platform-next@0.24.2
@eqs/platform-nest@0.24.2
@eqs/http@0.24.2
@eqs/errors@0.24.2
@eqs/config@0.24.2
@eqs/cli@0.24.2
@eqs/auth@0.24.2
@eqs/tooling@0.24.1
@eqs/testing@0.24.1
@eqs/telemetry@0.24.1
@eqs/platform-next@0.24.1
@eqs/platform-nest@0.24.1
@eqs/http@0.24.1
@eqs/errors@0.24.1
@eqs/config@0.24.1
@eqs/cli@0.24.1
@eqs/auth@0.24.1
@eqs/tooling@0.24.0
@eqs/testing@0.24.0
@eqs/telemetry@0.24.0
@eqs/platform-next@0.24.0
@eqs/platform-nest@0.24.0
@eqs/http@0.24.0
@eqs/errors@0.24.0
@eqs/config@0.24.0
@eqs/cli@0.24.0
@eqs/auth@0.24.0
@eqs/tooling@0.23.0
@eqs/testing@0.23.0
@eqs/telemetry@0.23.0
@eqs/platform-next@0.23.0
@eqs/platform-nest@0.23.0
@eqs/http@0.23.0
@eqs/errors@0.23.0
@eqs/config@0.23.0
@eqs/cli@0.23.0
@eqs/auth@0.23.0
@eqs/tooling@0.22.0
@eqs/testing@0.22.0
@eqs/telemetry@0.22.0
@eqs/platform-next@0.22.0
@eqs/platform-nest@0.22.0
@eqs/http@0.22.0
@eqs/errors@0.22.0
@eqs/config@0.22.0
@eqs/cli@0.22.0
@eqs/auth@0.22.0
@eqs/tooling@0.21.0
@eqs/testing@0.21.0
@eqs/telemetry@0.21.0
@eqs/platform-next@0.21.0
@eqs/platform-nest@0.21.0
@eqs/http@0.21.0
@eqs/errors@0.21.0
@eqs/config@0.21.0
@eqs/cli@0.21.0
@eqs/auth@0.21.0
@eqs/tooling@0.20.0
@eqs/testing@0.20.0
@eqs/telemetry@0.20.0
@eqs/platform-next@0.20.0
@eqs/platform-nest@0.20.0
@eqs/http@0.20.0
@eqs/errors@0.20.0
@eqs/config@0.20.0
@eqs/cli@0.20.0
@eqs/auth@0.20.0
@eqs/tooling@0.19.0
@eqs/testing@0.19.0
@eqs/telemetry@0.19.0
@eqs/platform-next@0.19.0
@eqs/platform-nest@0.19.0
@eqs/http@0.19.0
@eqs/errors@0.19.0
@eqs/config@0.19.0
@eqs/cli@0.19.0
@eqs/auth@0.19.0
@eqs/tooling@0.17.1
@eqs/testing@0.17.1
@eqs/telemetry@0.17.1
@eqs/platform-next@0.17.1
@eqs/platform-nest@0.17.1
@eqs/http@0.17.1
@eqs/errors@0.17.1
@eqs/config@0.17.1
@eqs/cli@0.17.1
@eqs/auth@0.17.1
@eqs/tooling@0.17.0
@eqs/testing@0.17.0
@eqs/telemetry@0.17.0
@eqs/platform-next@0.17.0
@eqs/platform-nest@0.17.0
@eqs/http@0.17.0
@eqs/errors@0.17.0
@eqs/config@0.17.0
@eqs/cli@0.17.0
@eqs/auth@0.17.0
@eqs/tooling@0.16.0
@eqs/testing@0.16.0
@eqs/telemetry@0.16.0
@eqs/platform-next@0.16.0
@eqs/platform-nest@0.16.0
@eqs/http@0.16.0
@eqs/errors@0.16.0
@eqs/config@0.16.0
@eqs/cli@0.16.0
@eqs/auth@0.16.0
@eqs/tooling@0.15.0
@eqs/testing@0.15.0
@eqs/telemetry@0.15.0
@eqs/platform-next@0.15.0
@eqs/platform-nest@0.15.0
@eqs/http@0.15.0
@eqs/errors@0.15.0
@eqs/config@0.15.0
@eqs/cli@0.15.0
@eqs/auth@0.15.0
@eqs/tooling@0.14.0
@eqs/testing@0.14.0
@eqs/telemetry@0.14.0
@eqs/platform-next@0.14.0
@eqs/platform-nest@0.14.0
@eqs/http@0.14.0
@eqs/errors@0.14.0
@eqs/config@0.14.0
@eqs/cli@0.14.0
@eqs/auth@0.14.0
@eqs/tooling@0.13.0
@eqs/testing@0.13.0
@eqs/telemetry@0.13.0
@eqs/platform-next@0.13.0
@eqs/platform-nest@0.13.0
@eqs/http@0.13.0
@eqs/errors@0.13.0
@eqs/config@0.13.0
@eqs/cli@0.13.0
@eqs/auth@0.13.0
@eqs/tooling@0.12.2
@eqs/testing@0.12.2
@eqs/telemetry@0.12.2
@eqs/platform-next@0.12.2
@eqs/platform-nest@0.12.2
@eqs/http@0.12.2
@eqs/errors@0.12.2
@eqs/config@0.12.2
@eqs/cli@0.12.2
@eqs/auth@0.12.2
@eqs/tooling@0.12.1
@eqs/testing@0.12.1
@eqs/telemetry@0.12.1
@eqs/platform-next@0.12.1
@eqs/platform-nest@0.12.1
@eqs/http@0.12.1
@eqs/errors@0.12.1
@eqs/config@0.12.1
@eqs/cli@0.12.1
@eqs/auth@0.12.1
@eqs/tooling@0.12.0
@eqs/testing@0.12.0
@eqs/telemetry@0.12.0
@eqs/platform-next@0.12.0
@eqs/platform-nest@0.12.0
@eqs/http@0.12.0
@eqs/errors@0.12.0
@eqs/config@0.12.0
@eqs/cli@0.12.0
@eqs/auth@0.12.0
@eqs/tooling@0.11.2
@eqs/testing@0.11.2
@eqs/telemetry@0.11.2
@eqs/platform-next@0.11.2
@eqs/platform-nest@0.11.2
@eqs/http@0.11.2
@eqs/errors@0.11.2
@eqs/config@0.11.2
@eqs/cli@0.11.2
@eqs/auth@0.11.2
@eqs/tooling@0.11.1
@eqs/testing@0.11.1
@eqs/telemetry@0.11.1
@eqs/platform-next@0.11.1
@eqs/platform-nest@0.11.1
@eqs/http@0.11.1
@eqs/errors@0.11.1
@eqs/config@0.11.1
@eqs/cli@0.11.1
@eqs/auth@0.11.1
@eqs/tooling@0.11.0
@eqs/testing@0.11.0
@eqs/telemetry@0.11.0
@eqs/platform-next@0.11.0
@eqs/platform-nest@0.11.0
@eqs/http@0.11.0
@eqs/errors@0.11.0
@eqs/config@0.11.0
@eqs/cli@0.11.0
@eqs/auth@0.11.0
@eqs/tooling@0.10.1
@eqs/testing@0.10.1
@eqs/telemetry@0.10.1
@eqs/platform-next@0.10.1
@eqs/platform-nest@0.10.1
@eqs/http@0.10.1
@eqs/errors@0.10.1
@eqs/config@0.10.1
@eqs/cli@0.10.1
@eqs/auth@0.10.1
@eqs/tooling@0.10.0
@eqs/testing@0.10.0
@eqs/telemetry@0.10.0
@eqs/platform-next@0.10.0
@eqs/platform-nest@0.10.0
@eqs/http@0.10.0
@eqs/errors@0.10.0
@eqs/config@0.10.0
@eqs/cli@0.10.0
@eqs/auth@0.10.0
@eqs/tooling@0.9.0
@eqs/testing@0.9.0
@eqs/telemetry@0.9.0
@eqs/platform-next@0.9.0
@eqs/platform-nest@0.9.0
@eqs/http@0.9.0
@eqs/errors@0.9.0
@eqs/config@0.9.0
@eqs/cli@0.9.0
@eqs/auth@0.9.0
@eqs/tooling@0.8.1
@eqs/testing@0.8.1
@eqs/telemetry@0.8.1
@eqs/platform-next@0.8.1
@eqs/platform-nest@0.8.1
@eqs/http@0.8.1
@eqs/errors@0.8.1
@eqs/config@0.8.1
@eqs/cli@0.8.1
@eqs/auth@0.8.1
@eqs/tooling@0.8.0
@eqs/testing@0.8.0
@eqs/telemetry@0.8.0
@eqs/platform-next@0.8.0
@eqs/platform-nest@0.8.0
@eqs/http@0.8.0
@eqs/errors@0.8.0
@eqs/config@0.8.0
@eqs/cli@0.8.0
@eqs/auth@0.8.0
@eqs/tooling@0.7.0
@eqs/testing@0.7.0
@eqs/telemetry@0.7.0
@eqs/platform-next@0.7.0
@eqs/platform-nest@0.7.0
@eqs/http@0.7.0
@eqs/errors@0.7.0
@eqs/config@0.7.0
@eqs/cli@0.7.0
@eqs/auth@0.7.0
@eqs/tooling@0.6.0
@eqs/testing@0.6.0
@eqs/telemetry@0.6.0
@eqs/platform-next@0.6.0
@eqs/platform-nest@0.6.0
@eqs/http@0.6.0
@eqs/errors@0.6.0
@eqs/config@0.6.0
@eqs/cli@0.6.0
@eqs/auth@0.6.0
@eqs/tooling@0.5.1
@eqs/testing@0.5.1
@eqs/telemetry@0.5.1
@eqs/platform-next@0.5.1
@eqs/platform-nest@0.5.1
@eqs/http@0.5.1
@eqs/errors@0.5.1
@eqs/config@0.5.1
@eqs/cli@0.5.1
@eqs/auth@0.5.1
@eqs/tooling@0.5.0
@eqs/testing@0.5.0
@eqs/telemetry@0.5.0
@eqs/platform-next@0.5.0
@eqs/platform-nest@0.5.0
@eqs/http@0.5.0
@eqs/errors@0.5.0
@eqs/config@0.5.0
@eqs/cli@0.5.0
@eqs/auth@0.5.0
@eqs/tooling@0.4.0
@eqs/testing@0.4.0
@eqs/telemetry@0.4.0
@eqs/platform-next@0.4.0
@eqs/platform-nest@0.4.0
@eqs/http@0.4.0
@eqs/errors@0.4.0
@eqs/config@0.4.0
@eqs/cli@0.4.0
@eqs/auth@0.4.0
@eqs/tooling@0.2.0
@eqs/telemetry@0.2.0
@eqs/errors@0.2.0
@eqs/config@0.2.0
@eqs/tooling@0.1.0
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set.
Reference
Infrastructure/eqs-platform#34
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Constaté le 2026-09-09 sur le premier satellite réel monté depuis
eqs-satellite-template, au moment d'écrire le premier consommateur de sa propre API.@eqs/*0.7.0, pnpm 11.8.0.AGENTS.mdest catégorique : « Les APIs internes se consomment via les clients générés (@eqs/*-client). Jamais defetchdirect. » Le contrôlehttp.fetch-brutle vérifie. Mais aucun outil du socle ne génère de client, et le gabarit n'en livre pas la configuration.Ce qui existe, et ce qui manque
Ce qui existe :
@eqs/http— « Le mutator Orval : auth, traceparent, identité du client, timeout, ApiError ». Il exporteeqsMutator,creerMutator(options),defineTarget(cible),identiteClient(nom, version). La pièce d'exécution est donc prête.main.tsdu gabarit commente longuementdefineTargetet dit que « le mutator d'Orval y ajoute le jeton,traceparent, le délai d'attente et le plafond de réponse ».openapi.json, committé, dérivé du code.Ce qui manque :
eqs --helplistedoctor,audit,inspect,baseline,openapi export,auth roles,auth entra-manifest,migrate,topology,e2e,login,coolify verify. Rien pour les clients.orval.config.ts, aucune mention d'Orval hors du commentaire demain.ts.@eqs/*.Reproduction
Ce que ça coûte
Un satellite qui veut consommer une API — la sienne depuis son front, ou celle d'un autre service — n'a que trois issues, et les trois sont mauvaises :
fetchdirect, ce que la doctrine interdit et qu'un contrôle refuse ;C'est bloquant pour nous : le lot en cours prévoit une façade MCP et un front qui lisent tous deux l'API du service, et la règle « jamais de
fetchdirect » ne laisse pas le choix.Un point qui mérite peut-être d'être tranché au passage : la formulation
@eqs/*-clientsuggère qu'un client est publié par le service qu'il dessert (hq-client,atlas-client). Si c'est le modèle, alors le cas « je consomme ma propre API depuis mon propre front » n'est traité nulle part — or c'est le cas le plus courant, et le premier que rencontre un satellite.Ce que je proposerais
eqs client generatequi litopenapi.json(ou une cible déclarée) et produit le client avec le mutator de@eqs/httpdéjà branché. C'est le geste symétrique d'eqs openapi export, et il manque visiblement.**/generated/**(déjà couvert par.prettierignoreet par la règle « on ne modifie pas un fichier généré »).Contournement en place
Nous allons écrire une configuration Orval dans le satellite et générer le client sous
**/generated/**, aveceqsMutator. Je reviendrai commenter ce ticket avec ce qui aura fonctionné et ce qui aura demandé de deviner — la configuration pourra servir de point de départ si la piste 1 ou 2 est retenue.Le manque est réel, mais le point central du ticket est faux : la configuration Orval existe, elle est publiée, et elle est documentée.
@eqs/tooling/orvalexportedefineEqsClient({ input, output }), qui rend la configuration imposée —tags-split, clientfetch, mutator@eqs/http, schémas zod, mocks MSW,operationIdconservés. Elle est déclarée dans lesexportsdu package, décrite danspackages/tooling/README.mdet montrée telle quelle danspackages/http/README.md.Votre
grepne pouvait pas la voir :Le fichier est
orval.js. Les deux--includel'excluaient. Ça vaut d'être dit, parce que c'est la seule raison pour laquelle le ticket conclut « rien n'existe » là où il fallait conclure « rien ne le branche ».Ce qui est vrai, vérifié :
eqs --help;orvaln'est installé nulle part — ni dans le socle, ni dans le gabarit (grep '"orval"' **/package.jsonne rend rien). La configuration existe, le moteur qui la lit n'est pas là ;orval.config.ts, ni de script ;nest, « la seconde consomme la première par son client généré ». Une décision d'aujourd'hui s'appuie donc sur une chaîne qui n'existe pas.Ce que la vérification a trouvé en plus, et qui vous aurait coûté la journée suivante.
defineEqsClientimposait le mutator serveur :Or
@eqs/httpdépend de@eqs/auth, qui importenode:async_hooksetnode:cryptoet tirebetter-auth. Un client généré avec la configuration imposée ne se bundle pas pour un navigateur. Vous alliez écrire cette configuration, générer, importer le client dans un composant client, et vous heurter à un échec de build dont la cause est trois packages plus loin.Le socle a pourtant la bonne pièce :
creerMutatorNavigateur, dans@eqs/http/browser, qui ne dépend que de@eqs/errors/client, envoie la session par cookie et n'ajoute aucunAuthorization— écrit exactement pour les profilsfrontetnext-fullstack. Rien ne permettait de le sélectionner.Corrigé.
defineEqsClientprend une optionmutator. Et commecreerMutatorNavigateurest une fabrique (elle prendbaseUrletclientName) et non un mutator, la pointer directement est refusé, avec le code à écrire :Les trois cas — le défaut, le choix, le refus — sont couverts par le
checkde@eqs/tooling.Deux choses que le gabarit vous dit déjà, et qui répondent à des questions que vous alliez vous poser : son
.gitignoreporteapps/api/generated/etapps/web/generated/. Donc l'emplacement de sortie attendu estapps/<app>/generated/, et la sortie n'est pas committée — elle se régénère. Corollaire dont il faut tenir compte : sur un clone neuf,pnpm checktourne avant toute génération, donc une application qui importe son client généré ne compile pas tant que la génération n'est pas dans la chaîne. C'est probablement pour ça que rien n'est branché aujourd'hui, et c'est le vrai travail restant.Sur votre piste 1,
eqs client generate: je ne pense pas que ce soit la bonne, et je préfère l'argumenter que la faire.eqs openapi exportn'implémente rien — il appelle l'export du dépôt. Le geste symétrique n'est pas d'embarquer Orval dans la CLI (une grosse dépendance, pour tous les dépôts, y compris ceux qui ne génèrent aucun client) mais que le dépôt lanceorvalavec la configuration partagée. C'est votre piste 2, et c'est celle qui est cohérente avec le reste.Je laisse ce ticket ouvert. Ce qui est corrigé, c'est le défaut que j'ai trouvé dedans, pas le manque que vous signalez. Deux décisions restent à prendre, et elles ne m'appartiennent pas :
@eqs/hq-client) — mais D-041, écrite aujourd'hui, dit qu'un satellite ne produit aucun package publiable. Les deux ne tiennent pas ensemble. Soit D-041 gagne et un consommateur génère son client depuis le contrat committé du fournisseur, soit le client généré devient l'exception à D-041.postinstall,prebuild) puisque la sortie est ignorée par git, ou la sortie devient-elle committée commeopenapi.json?Votre retour d'expérience nous intéresse : vous êtes en train d'écrire la configuration, et ce qui vous aura demandé de deviner dira ce que le gabarit doit livrer. Postez-la ici quand elle marchera.
Branche
fix/tickets-socle-du-premier-satellite.La doctrine impose les clients générés, mais rien ne les génère : ni commande CLI, ni configuration Orval livréeto defineEqsClient produit un client qui ne compile pas : operationName dérivé d'un operationId en ressource.action donne un identifiant TS invalideRectification — je me suis trompé sur la prémisse, et le vrai sujet est ailleurs
Le corps ci-dessus est faux sur un point central :
@eqs/tooling/orvalexiste et exportedefineEqsClient({ input, output, mocks? }). J'avais cherché « orval » dans les fichiers du dépôt et dans la CLI, pas dans@eqs/tooling. Toutes mes excuses pour le temps que ça a pu coûter.Le titre est corrigé. Ce qui suit remplace la section « Ce qui manque ».
Le vrai sujet :
defineEqsClientproduit, tel que documenté, un client qui ne compile pas. Quatre points, trouvés en le jouant contre un contrat réel de 12 opérations.1.
operationNamecasse pour toutoperationIdde la flottedefineEqsClientfixeoverride.operationName: (operation) => operation.operationId. Or D-016 imposeressource.action— donc un point. Orval en fait des identifiants TypeScript, etnoeud.creern'est pas un identifiant valide.Ce n'est pas notre cas particulier : c'est vrai de toute opération conforme à la convention du socle. Le générateur et la règle de nommage se contredisent.
Contournement appliqué :
delete configuration.client.output.override.operationName, ce qui laisse Orval dériver ses noms du chemin et de la méthode.2. Le mutator
@eqs/httpest refusé sansinferred: trueSans ce drapeau, la sortie porte une enveloppe de trop et ne type-check pas. Correctif :
mutator: { …, inferred: true }.3. Les schémas générés sont importés sans extension, illégal en ESM strict
Le fichier de schémas séparé (celui que
defineEqsClientconfigure) est importé sans.js, ce que le"type": "module"du gabarit refuse à l'exécution.4. Pas de
baseUrl, donceqsMutatorne reconnaît aucune cibleLes fonctions générées produisent des URL relatives. Or
eqsMutatorassocie une cible — et donc le jeton,traceparent, le délai d'attente — par égalité littérale d'unbaseUrlabsolu. Sans lui, aucune cible ne correspond et rien n'est ajouté à la requête : le mutator est là, mais inerte.Contournement :
configuration.client.output.baseUrl = { runtime: 'process.env.PILOTAGE_API_URL' }.Ce que ça donne aujourd'hui
Une configuration qui marche, mais qui défait par mutation d'objet trois des réglages que
defineEqsClientvient de poser :C'est laid, et surtout fragile : ça dépend de la forme interne de ce que rend
defineEqsClient. Trente dépôts qui copient ces trois lignes, c'est trente ruptures à la prochaine évolution du helper.Ce que je proposerais, vu d'ici
defineEqsClientassainit l'operationId(noeud.creer→noeudCreer), soit il ne fixe pasoperationNamedu tout. La première garde des noms lisibles et alignés sur le contrat ; je la préférerais.baseUrlmériterait d'être un paramètre dedefineEqsClient— le nom de la variable d'environnement est propre à chaque satellite, mais le fait qu'il en faille une est général.Le détail complet, avec les commandes jouées et les sorties, est dans le rapport de la tâche ; je peux le coller ici si c'est utile au traitement en cours.
Comme le ticket est en cours de traitement : notre configuration locale vit dans
apps/mcp/orval.config.tsavec un commentaire renvoyant ici. Dites-moi quand la correction est publiée, on la reprendra et on retirera les trois lignes de mutation — c'est exactement le genre de contournement qu'on veut voir disparaître.Suite, et fin : j'ai câblé la chaîne pour de vrai sur le gabarit. Elle ne marchait pas du tout, et pour deux raisons de plus que celle du commentaire précédent.
Défaut 2 — le nom des fonctions générées n'était pas du JavaScript.
defineEqsClientpassait l'operationId tel quel à Orval. Or un operationId porte un point (ping.creer, D-006), qui ne peut pas figurer dans un identifiant :Mesuré sur le gabarit, pour trois ressources : 137 erreurs, dont
error TS1005: '=' expected. Le code produit ne parse même pas.Les fonctions s'appellent maintenant
pingLire,pingCreer,devisLigneAjouter. L'operationId lui-même ne bouge pas — il reste la clé de corrélation dans le contrat, dansroles.yamlet dans le garde ; ce qui change est le nom sous lequel le code appelle la route.Défaut 3 — les deux mutators avaient la mauvaise signature. Ils rendaient
Promise<ReponseHttp<T>>. Le clientfetchd'Orval, lui, appelle le mutator avec le type de la réponse entière — une union discriminée par le statut, intersectée avec{ headers }— et attend ce type-là en retour. Ils enveloppaient donc une seconde fois :Le plus parlant : le commentaire de
ReponseHttpdisait « Ce qu'Orval (clientfetch) attend en retour ». C'est exactement la phrase qui était fausse, et personne ne pouvait s'en apercevoir — les tests du package appelaient les mutators à la main, avec le type du corps, et passaient.packages/http/test/contrat-orval.test.tsrecopie désormais la forme qu'Orval émet et vérifie les deux mutators à la compilation. C'est la barrière qui manquait.Mesure finale, sur le gabarit : 137 → 9 → 4 → 0.
pnpm checkvert, avec le front qui appelle réellementpingCreer.Vos deux questions sont tranchées — D-044.
Le client se génère chez celui qui appelle, et son code est committé.
Le raisonnement, court : ce qui doit être partagé l'est déjà — la configuration (
@eqs/tooling/orval) et le transport (@eqs/http). Un client entièrement généré n'a rien en propre à déboguer ; un défaut de transport se corrige une fois, dans le socle. Publier depuiseqs-platformétait exclu par le versionnement commun (D-022) :@eqs/atlas-clientaurait changé de version à chaque correction dans@eqs/errors. Un dépôt dédié tenait, mais faisait de la publication un passage obligé — une API qui bouge bloque ses consommateurs jusqu'à ce que quelqu'un y republie.Et le code produit est committé, pas ignoré : ignoré, il n'existe pas sur un clone neuf, et une application qui importe son client ne compile pas avant que la génération ait tourné — donc avant
pnpm check.Ce que le gabarit livre maintenant (branche
feat/client-genere) :orvalen dépendance de développement ;orval.config.mjs— en.mjset pas.ts: Orval lit une config TypeScript avec esbuild, dont le script d'installation télécharge un binaire, qu'on n'accorde pas pour un confort d'écriture ;apps/web/src/lib/mutator.ts, qui instancie le mutator navigateur ;pnpm client, à jouer aprèspnpm openapi;Ordre à respecter chez vous : cette branche du gabarit exige la prochaine version de
@eqs/*. Contre 0.7.0 elle porte 5 erreurs, et lepre-pushla refuse — ce qui est le comportement voulu. Après la sortie du socle : relever les bornes@eqs/*,pnpm client,pnpm check.Je ferme : le manque est comblé et les décisions sont écrites. Si la génération vous résiste malgré ça, rouvrez avec la sortie — vous êtes le premier à l'emprunter pour de vrai, et les trois défauts ci-dessus montrent que ce chemin n'avait jamais servi.
Cinquième défaut, et il est structurel :
exactOptionalPropertyTypescontre les paramètres optionnelsTrouvé après coup, en branchant la façade sur une route qui a un paramètre optionnel. Utile si le traitement est encore en cours, parce qu'il ne se corrige pas au même endroit que les quatre autres.
@eqs/tooling/tsconfig.base.jsonactive"exactOptionalPropertyTypes": true. Sous ce réglage,{ p?: string }n'accepte pas une valeur explicitementundefined: il faut{ p?: string | undefined }.Or le client généré déclare :
Et l'appelant, lui, a presque toujours un
string | undefineden main — ici le schéma Zod généré par la même passe Orval (ZodOptional<ZodString>) rend exactement ça. D'où :Pourquoi c'est structurel et pas anecdotique
Les deux moitiés viennent du socle et se contredisent :
@eqs/toolingimposeexactOptionalPropertyTypes, et@eqs/tooling/orvalgénère des types qui ne le respectent pas. Dès qu'une API a un seul paramètre de requête optionnel, son client généré est inutilisable par un consommateur qui compile avec le tsconfig du socle — c'est-à-dire tous.Et les deux passes d'Orval configurées par
defineEqsClient(le client et les schémas Zod) produisent des formes mutuellement incompatibles pour le même paramètre :p?: stringd'un côté,p: string | undefinedde l'autre. Elles ne peuvent pas se parler.À noter aussi : ça se déclenche à retardement. Le client compilait tant que le paramètre était obligatoire dans le contrat ; il a cessé de compiler quand on a corrigé le contrat pour rendre le paramètre optionnel — ce qui était la bonne correction. Régénérer le client ne répare rien, puisque le défaut est dans la forme générée.
Ce que je proposerais
orvalsait le faire :output.override.useNamedParametersn'est pas le sujet, mais le générateur TypeScript accepte d'émettre| undefinedsur les propriétés optionnelles. Si l'option existe dans la version épinglée, la poser dansdefineEqsClientrègle le cas pour les trente dépôts. Sinon, la question devient : faut-il vraimentexactOptionalPropertyTypesdans le tsconfig de base, sachant que le générateur maison ne le respecte pas ? Les deux se défendent, mais pas ensemble.Contournement en place
Dans la façade, on construit l'objet de paramètres en omettant la clé plutôt qu'en passant
undefined:Trois lignes purement techniques, commentées et renvoyant ici. Elles seront à retirer — et il y en aura une par paramètre optionnel de chaque API, dans chaque consommateur, tant que le point n'est pas tranché.
Je rouvre : vos deux commentaires arrivent après ma fermeture, et ils portent trois défauts de plus, tous vrais. J'ai vérifié chacun par mes propres moyens et les quatre sont corrigés. Le point sur votre rectification d'abord, puis vos cinq points un par un.
Sur la rectification : pas besoin de vous excuser. Nous avons trouvé le défaut du nom des fonctions chacun de notre côté, dans la même heure — c'est plutôt le signe que le chemin était vraiment neuf.
1.
operationName— d'accord, et corrigéMême mesure que la vôtre.
defineEqsClientproduit maintenantpingLire,devisLigneAjouter. L'operationId ne bouge pas : il reste la clé de corrélation dans le contrat, dansroles.yamlet dans le garde. C'est votre première proposition, et c'est la bonne pour la raison que vous donniez : les noms restent lisibles et alignés sur le contrat.Votre
delete configuration.client.output.override.operationNamepeut sauter.2.
inferred: true— pas nécessaire, et je pense que c'était le mauvais endroitVotre diagnostic du symptôme est juste : « la sortie porte une enveloppe de trop ». Mais l'enveloppe de trop venait du mutator, pas de la configuration.
Les deux mutators rendaient
Promise<ReponseHttp<T>>alors que le clientfetchd'Orval appelle avec le type de la réponse ENTIÈRE et attendPromise<T>.inferred: truedemande à Orval de s'adapter à une signature fausse ; corriger la signature règle aussi les appels écrits à la main, et rend le fichier de types du package honnête.Mesuré : avec
@eqs/httpcorrigé, un client serveur généré sansinferredtype-check à zéro erreur.packages/http/test/contrat-orval.test.tsrecopie désormais la forme émise par Orval et rougit si un mutator rebouge.Votre
configuration.client.output.override.mutator.inferred = truepeut sauter aussi.3. Imports sans extension — vrai, et il fallait deux réglages
Reproduit dans
apps/apidu gabarit (Node ESM, NodeNext) :Le front Next ne le voyait pas — sa résolution est celle d'un bundler. C'est le deuxième consommateur qui tombe, et c'est ce qui rend ce défaut méchant.
Orval sait faire, à condition de lui donner le tsconfig de la cible : il y lit
moduleResolution.tsconfigdevient donc un paramètre REQUIS dedefineEqsClient— l'oublier ne se paie pas là où on l'écrit, donc on ne laisse pas l'oublier.Mais ça n'a réglé que la moitié :
from '../schemas'désigne un répertoire, et aucune extension ne sauve ça. Il a falluindexFiles: falseen plus, qui fait importer chaque schéma par son chemin complet ('../schemas/echoDto.js'). Les deux sont maintenant posés par le helper.4.
baseUrl— vrai, et désormais refusé plutôt que corrigé après coupVotre analyse est exacte, et c'est le défaut le plus vicieux des cinq : le mutator est là, et il est inerte.
defineEqsClientexige maintenantbaseUrlquand le mutator est celui du serveur :Un client destiné au navigateur n'en a pas besoin — son mutator porte déjà l'adresse — et le paramètre y reste facultatif.
5.
exactOptionalPropertyTypes— vrai, structurel comme vous le dites, et réglé sans toucher au tsconfigReproduit sur un contrat minimal à un paramètre de requête optionnel, dans les deux sens.
Sans réglage, en passant le type inféré du schéma zod aux paramètres du client :
Orval 8.30 a l'option qu'il fallait, du côté zod et non du client :
exactOptional, qui émet.exactOptional()au lieu de.optional(), et fait donc inférer{ p?: string }. Avec elle, la même sonde compile à zéro erreur.C'est posé par défaut dans
defineEqsClient. Ça répond à votre question finale — « faut-il vraimentexactOptionalPropertyTypesdans le tsconfig de base ? » — par : oui, et c'est le générateur qui s'aligne. Le réglage strict attrape de vraies fautes ; c'était la passe zod qui ne le respectait pas. Les deux moitiés produites par la même commande s'accordent maintenant.Votre
const params = personneId === undefined ? { projetId } : { projetId, personneId }peut sauter, et il n'y en aura pas un par paramètre optionnel.Ce qu'il faudra changer chez vous
Vos trois lignes de mutation d'objet disparaissent — vous aviez raison de les signaler comme fragiles, elles dépendaient de la forme interne du helper. En échange,
defineEqsClientprend deux paramètres de plus, dont un obligatoire :Après la sortie du socle, pas avant : contre 0.7.0 ces paramètres sont ignorés en silence.
Ce qui n'est pas fait, et que je ne cache pas
Rien ne vérifie qu'un client committé est à jour de son contrat. Le geste exact est
pnpm client && git diff --exit-code, écrit dans l'orval.config.mjsdu gabarit ; il n'est dans aucune porte, parce qu'il faut d'abord trancher où il tourne — lepre-push, qui doit rester court, ou la CI, qui constate après le déploiement. Si ça vous mord avant qu'on tranche, dites-le, ça deviendra l'argument qui manque.Je laisse ouvert jusqu'à ce que vous ayez rejoué votre façade contre la version publiée. Branche
fix/tickets-socle-du-premier-satellitecôté socle,feat/client-generecôté gabarit.Deux points nouveaux, trouvés en écrivant
apps/web(Task 14 du même lot — l'interface humaine, front@eqs/platform-next), en plus des quatre déjà rapportés ci-dessus pourapps/mcp(Task 13).@eqs/*0.7.0, même dépôt.Point 5 —
defineEqsClientcâble en dur le mutator SERVICE, inadapté à un consommateur NAVIGATEURdefineEqsClientfixe toujoursoverride.mutator = { path: '@eqs/http', name: 'eqsMutator' }. Ce mutator poseAuthorization: Bearer <clé API ou jeton Entra>et ne pose jamaiscredentials: 'include'. Or@eqs/platform-nextdocumente lui-même deux profils :front(une interface humaine, authentifiée par cookie de session via better-auth —EqsProvider/SignIn/creerMutatorNavigateur) etnext-fullstack. Avec la configuration par défaut dedefineEqsClient, un consommateur du profilfrontne peut jamais authentifier ses appels : chaque requête générée échouerait en 401, silencieusement — aucune erreur à la génération ni à la compilation, seulement au premier appel réel une fois déployé.Reproduction minimale : suivre le README de
@eqs/http(celui cité au point 1 ci-dessus) dans un workspace Next front, appeler une opération générée depuis un composant client authentifié par cookie → 401 systématique, sans message qui oriente vers la cause (le mutator n'envoie tout simplement pas le cookie).Contournement retenu dans
apps/web/orval.config.ts: remplaceroverride.mutatorpar un fichier local (src/lib/eqs-mutator.ts) qui enveloppecreerMutatorNavigateur(@eqs/http/browser) — le même mutator que celui qu'EqsProvider/SignInutilisent déjà pour/moiet la connexion.Proposition : soit
defineEqsClientaccepte une option (cible: 'service' | 'navigateur', ou déduite dumode/profil déclaré) qui choisit le mutator adapté, soit son README dit explicitement qu'un consommateurfront/navigateur doit toujours fournir son propre override — aujourd'hui rien ne le dit, et le README de@eqs/httpne documente que l'usage service-à-service.Point 6 —
mutator.inferred: true(point 2 ci-dessus) ne corrige pas le double-enveloppement, il le rend seulement invisible pour un consommateur qui ne nomme jamais le type qu'il attendChaque fonction générée appelle explicitement
mutatorXxx<XxxResponse>(url, init), oùXxxResponseest l'union discriminée par statut ({data: T, status: 200} | {data: Error, status: 400} | …) — jamaisTlui-même, avec ou sansinferred: true. Le mutator (eqsMutator, ou tout mutator conforme comme celui du point 5) rend toujours{data: <corps JSON déjà décodé>, status, headers}: à l'exécution,dataEST déjàT, jamais un{data, status}de plus.Avec
inferred: true, TypeScript infère donc, pour la variable destructuréedata, le typeXxxResponsetout entier (l'union à deux niveaux), pasT. Résultat, reproduit dansapps/web:C'est la même famille d'erreur que celle documentée au point 2 (
ReponseHttp<X>non assignable àX), simplement déplacée d'un niveau.apps/mcp/src/main.ts(Task 13) ne l'a jamais vue : il ne fait queJSON.stringify(data, …), qui accepte n'importe quelle forme — le défaut ne se manifeste que chez un consommateur qui nomme le type qu'il attend, ce qu'une interface humaine fait presque partout (état React typé, props typées…).Contourné dans
apps/webpar une fonction documentéecommeDonnees<T>(valeur: unknown): T(apps/web/src/lib/donnees-generees.ts) — même geste quecommeContrat<T>en Task 13, appliqué à chaque appel qui assignedataà un type précis.Proposition : soit le patron de génération passe au mutator le type de la variante 2xx (
XxxResponseSuccess['data']) plutôt que l'union entière — ce qui rendraitinferred: trueréellement suffisant — soit la documentation dit explicitement qu'inferred: truene dispense jamais de ce contournement pour un consommateur qui type ses variables.Contexte complet : rapport de Task 14,
.superpowers/sdd/2026-09-09-pilotage-lot-1/task-14-report.mddans le dépôtpilotage.@eqs/*0.8.0 est publiée enlatest. Vous pouvez reprendre votre façade.Ce qui vous concerne dans cette version :
defineEqsClientproduit des noms de fonctions valides (noeudCreer), prendtsconfig(requis) etbaseUrl, poseindexFiles: falseet l'accordexactOptionalentre les deux passes ;Test la réponse entière, et le retour n'est plus enveloppé deux fois ;front.Vos quatre contournements peuvent sauter : le
delete operationName, lemutator.inferred = true, lebaseUrlposé par mutation d'objet, et lecommeDonnees<T>. Votre configuration devient :Une rupture à connaître, et elle n'est pas dans la génération.
eqsMutator<T>reçoit désormais le type de la réponse entière. Tout appel écrit à la main qui typait le corps cesse de compiler —rvaut le corps, etr.datan'existe plus :C'est
eqs-demoqui l'a révélé en passant sur la candidate, avant la promotion. L'exemple est aussi dans les typages publiés, au survol deeqsMutator.Ce que la version a traversé :
pnpm checkvert sur les dix packages, puis le bac à sable Coolify complet — instance jetable, serveur Docker isolé, dépôt servi en HTTP, image construite par Coolify, base déployée, service en HTTPS derrière Traefik, sondes vertes. Un seul contrôle non exécutable,auth.403-audience, faute de variables Entra sur le poste — et l'outil le dit lui-même plutôt que de le compter vert.L'artefact promu est exactement celui qui a été éprouvé : publié sous
next, testé tel quel, puis promu sans republier.Je ferme. Si votre façade résiste malgré ça, rouvrez — vous êtes toujours le premier à emprunter ce chemin pour de vrai, et il a rendu neuf défauts en une journée.