@nodefony/mongoose 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +97 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +90 -0
  6. package/dist/nodefony/config/config.js +57 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/entity/sessionEntity.js +63 -0
  9. package/dist/nodefony/entity/tokenEntity.js +184 -0
  10. package/dist/nodefony/entity/userEntity.js +106 -0
  11. package/dist/nodefony/entity/webAuthnCredentialEntity.js +97 -0
  12. package/dist/nodefony/entity/webhookEndpointEntity.js +109 -0
  13. package/dist/nodefony/interfaces/IMongooseConfig.js +1 -0
  14. package/dist/nodefony/interfaces/index.js +1 -0
  15. package/dist/nodefony/registerStores.js +89 -0
  16. package/dist/nodefony/service/MongooseService.js +95 -0
  17. package/dist/nodefony/src/MongooseTokenStore.js +237 -0
  18. package/dist/nodefony/src/MongooseUserRepository.js +205 -0
  19. package/dist/nodefony/src/MongooseWebAuthnCredentialStore.js +144 -0
  20. package/dist/nodefony/src/MongooseWebhookStore.js +181 -0
  21. package/dist/nodefony/src/SessionStorage.js +241 -0
  22. package/dist/nodefony/src/mongoOrder.js +49 -0
  23. package/dist/nodefony/src/orm-core/MongooseOrm.js +440 -0
  24. package/dist/nodefony/src/orm-core/MongooseRepository.js +300 -0
  25. package/dist/nodefony/src/orm-core/MongooseTransaction.js +53 -0
  26. package/dist/nodefony/src/orm-core/index.js +4 -0
  27. package/dist/types/index.d.ts +74 -0
  28. package/dist/types/nodefony/config/config.d.ts +21 -0
  29. package/dist/types/nodefony/config/defineModuleConfig.d.ts +26 -0
  30. package/dist/types/nodefony/entity/sessionEntity.d.ts +42 -0
  31. package/dist/types/nodefony/entity/tokenEntity.d.ts +59 -0
  32. package/dist/types/nodefony/entity/userEntity.d.ts +54 -0
  33. package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +61 -0
  34. package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +62 -0
  35. package/dist/types/nodefony/interfaces/IMongooseConfig.d.ts +17 -0
  36. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  37. package/dist/types/nodefony/registerStores.d.ts +37 -0
  38. package/dist/types/nodefony/service/MongooseService.d.ts +48 -0
  39. package/dist/types/nodefony/src/MongooseTokenStore.d.ts +126 -0
  40. package/dist/types/nodefony/src/MongooseUserRepository.d.ts +82 -0
  41. package/dist/types/nodefony/src/MongooseWebAuthnCredentialStore.d.ts +52 -0
  42. package/dist/types/nodefony/src/MongooseWebhookStore.d.ts +72 -0
  43. package/dist/types/nodefony/src/SessionStorage.d.ts +63 -0
  44. package/dist/types/nodefony/src/mongoOrder.d.ts +40 -0
  45. package/dist/types/nodefony/src/orm-core/MongooseOrm.d.ts +132 -0
  46. package/dist/types/nodefony/src/orm-core/MongooseRepository.d.ts +51 -0
  47. package/dist/types/nodefony/src/orm-core/MongooseTransaction.d.ts +37 -0
  48. package/dist/types/nodefony/src/orm-core/index.d.ts +9 -0
  49. package/docs/configuration.md +776 -0
  50. package/docs/index.md +881 -0
  51. package/package.json +97 -0
package/docs/index.md ADDED
@@ -0,0 +1,881 @@
1
+ ---
2
+ title: "@nodefony/mongoose — le driver MongoDB"
3
+ navTitle: "@nodefony/mongoose"
4
+ lang: fr
5
+ module: "@nodefony/mongoose"
6
+ topic: mongoose
7
+ section: "Données"
8
+ audience: [developer]
9
+ tags:
10
+ [
11
+ orm,
12
+ mongoose,
13
+ mongodb,
14
+ nosql,
15
+ document,
16
+ session,
17
+ repository,
18
+ transaction,
19
+ stores,
20
+ ]
21
+ version: "doc"
22
+ status: stable
23
+ updated: 2026-07-19
24
+ source: "src/packages/@nodefony/mongoose/docs/index.md"
25
+ coverageModule: mongoose
26
+ coverageFiles: MongooseOrm.ts,MongooseRepository.ts,SessionStorage.ts,MongooseTokenStore.ts,MongooseWebAuthnCredentialStore.ts,MongooseWebhookStore.ts,MongooseUserRepository.ts
27
+ ---
28
+
29
+ # @nodefony/mongoose — le driver MongoDB
30
+
31
+ > Le module qui fait parler ton application à **MongoDB**. Il ouvre les connexions au démarrage,
32
+ > compile tes schémas en modèles, et te rend des **repositories portables** : le même code métier
33
+ > tourne sur Mongo ou sur SQL. Il fournit aussi cinq **briques du framework** prêtes à l'emploi sur
34
+ > Mongo — sessions, utilisateurs, jetons, passkeys, webhooks. Ce qu'il ne porte pas, il le dit :
35
+ > la couverture est **adaptée à la vocation de MongoDB**, jamais une course à la parité avec SQL.
36
+
37
+ 📍 [Documentation](../../../../../docs/index.md) › **MongoDB (Mongoose)**
38
+
39
+ ## 🧭 Par où commencer
40
+
41
+ Quatre parcours, selon ce que tu viens faire. L'ordre compte : chaque étape suppose la précédente.
42
+
43
+ **Je démarre une application documentaire** — ma donnée métier est faite de documents.
44
+
45
+ 1. [🚀 Démarrage rapide](#-démarrage-rapide) — la config, une entité, un repository, une requête.
46
+ Copie-colle, ça tourne.
47
+ 2. [Configuration](./configuration.md) — l'URI, les connecteurs nommés, la surcharge par
48
+ l'environnement. C'est là que vit le secret de connexion.
49
+ 3. [🧰 Le repository portable](#-le-repository-portable--lapi-que-tu-utilises-vraiment) — critères,
50
+ opérateurs, tri, pagination.
51
+ 4. [`@nodefony/orm-core`](../../orm-core/docs/index.md) — le contrat commun à tous les drivers, si
52
+ tu veux comprendre ce qui est portable et ce qui ne l'est pas.
53
+
54
+ **Je branche les briques du framework sur Mongo** — sessions, comptes, jetons, passkeys, webhooks.
55
+
56
+ 1. [🧭 Couverture adaptée](#-la-vision-nodefony--une-couverture-adaptée-pas-une-course-à-la-parité) —
57
+ **lis ça d'abord** : ce que Mongo porte, ce qu'il ne porte pas, et pourquoi ce n'est pas un manque.
58
+ 2. [🔐 Les stores fournis](#-les-stores-fournis) — un par brique, avec sa clé de sélection.
59
+ 3. [Sessions HTTP](../../http/docs/session.md) et [jetons](../../security/docs/tokens.md) —
60
+ le contrat côté consommateur ; cette page en donne l'implémentation Mongo.
61
+ 4. [⚠️ Pièges](#-pièges-symptôme--cause--correction) — l'ordre de chargement des modules se paie cher
62
+ quand on le découvre en production.
63
+
64
+ **J'exploite MongoDB à fond** — au-delà du CRUD portable.
65
+
66
+ 1. [🏗️ Architecture interne](#-architecture-interne--du-boot-à-la-requête) — ce qui se passe au boot,
67
+ et le trajet exact d'une requête.
68
+ 2. [Relations et transactions](#relations--sans-clé-étrangère) — populate, virtuels, replica set.
69
+ 3. [La trappe native](#la-trappe-native--quand-le-contrat-ne-suffit-plus) — agrégations, `$or`,
70
+ index : tout ce que le contrat portable ne couvre pas volontairement.
71
+
72
+ **Je passe en production / je supervise.**
73
+
74
+ 1. [Configuration](./configuration.md) — `MONGODB_URI` / `NF_DATABASE_URL`, pool, TLS.
75
+ 2. [📡 Observabilité Studio](#-observabilité--studio) — écrans ORM, Stores, Bases, et le data plane.
76
+ 3. [⚡ Performance et mémoire](#-performance-et-mémoire) — ce que coûte (ou pas) l'instrumentation.
77
+ 4. [🧪 Tests](#-tests-et-couverture) — et surtout **ce qu'un test vert ne prouve pas** ici.
78
+
79
+ ## 🗂️ Le module en un coup d'œil
80
+
81
+ Le tableau pour choisir en cinq secondes ; les cards en dessous pour le détail.
82
+
83
+ | Brique | Ce qu'elle fait | Tu t'en sers quand… |
84
+ | ------------------------------------- | ------------------------------------------------------- | ------------------------------------------ |
85
+ | [`configuration`](./configuration.md) | URI, connecteurs nommés, options, surcharge par env | tu branches une vraie base |
86
+ | `MongooseService` | ouvre les connexions au boot, les ferme à l'arrêt | jamais directement — il travaille pour toi |
87
+ | `MongooseOrm` | compile tes entités en modèles, expose les sondes | tu veux la connexion native ou un ping |
88
+ | `MongooseRepository` | le CRUD portable (critères, tri, pagination, relations) | **tout le temps** — c'est ton API données |
89
+ | `SessionStorage` | les sessions HTTP/WS persistées dans Mongo | tu veux des sessions qui survivent au pod |
90
+ | `MongooseUserRepository` | l'annuaire des comptes (identité, rôles, OAuth) | tes utilisateurs vivent dans Mongo |
91
+ | `MongooseTokenStore` | jetons, clés d'API, denylist, révocation en masse | tu émets des JWT ou des PAT |
92
+ | `MongooseWebAuthnCredentialStore` | les passkeys enregistrées | tu actives WebAuthn |
93
+ | `MongooseWebhookStore` | le registre durable des endpoints webhook | tu notifies des systèmes tiers |
94
+
95
+ ```nodefony-cards
96
+ [
97
+ { "icon": "⚙️", "title": "configuration", "href": "configuration.md",
98
+ "desc": "Le seul fichier que tu écris vraiment : une URI complète (Atlas, replica set) ou les composants `host`/`port`/`dbname`, plus les options de pool et d'authentification. Le secret ne vit jamais dans le dépôt — il arrive par l'environnement.",
99
+ "meta": "commence par sa section « Forme », puis la table des champs" },
100
+ { "icon": "🔌", "title": "MongooseService", "href": "#-architecture-interne--du-boot-à-la-requête",
101
+ "desc": "Le service bootable : il instancie un ORM par connecteur déclaré, le connecte au démarrage et referme tout à l'arrêt. Le module est déclaré non critique — une base injoignable ne tue pas le processus.",
102
+ "meta": "jamais directement — il travaille pour toi" },
103
+ { "icon": "🧩", "title": "MongooseOrm", "href": "#-architecture-interne--du-boot-à-la-requête",
104
+ "desc": "L'adapter du contrat commun : une connexion isolée (jamais le singleton global de Mongoose), les entités compilées en modèles, les relations traduites en références `ObjectId` + `populate`, et les sondes qu'affiche Studio.",
105
+ "meta": "pour la connexion native ou une transaction" },
106
+ { "icon": "🧰", "title": "MongooseRepository", "href": "#-le-repository-portable--lapi-que-tu-utilises-vraiment",
107
+ "desc": "L'objet que tu manipules au quotidien : `find`, `create`, `upsert`, `updateOne`, `increment`, `count`, `exists`, plus la liaison transactionnelle. Il traduit `id` en `_id`, les opérateurs portables en opérateurs Mongo, et refuse un champ inconnu au lieu de rendre zéro résultat en silence.",
108
+ "meta": "tout le temps — c'est ton API données" },
109
+ { "icon": "🗝️", "title": "SessionStorage", "href": "#sessions",
110
+ "desc": "Les sessions HTTP et WebSocket persistées dans Mongo, auto-enregistrées sous le nom `mongoose` auprès du service de sessions de `@nodefony/http`.",
111
+ "meta": "sélection : session.store = mongoose" },
112
+ { "icon": "👤", "title": "MongooseUserRepository", "href": "#utilisateurs",
113
+ "desc": "L'annuaire des comptes : identifiants, comptes sociaux liés, listing paginé. Il rend des objets `BaseUser` avec leur comportement, pas des documents nus.",
114
+ "meta": "câblé par ton application, dans son provisionUsers" },
115
+ { "icon": "🎫", "title": "MongooseTokenStore", "href": "#jetons",
116
+ "desc": "Jetons, clés d'API, denylist de `jti` et seuils de révocation par porteur — trois collections, dont les invariants de sécurité sont tenus par la requête elle-même.",
117
+ "meta": "sélection : tokenStore.store = mongoose" },
118
+ { "icon": "🔑", "title": "MongooseWebAuthnCredentialStore", "href": "#passkeys",
119
+ "desc": "Les passkeys enrôlées : une collection, la clé du credential en clé primaire, un enregistrement atomique et un listing d'administration qui ne sort jamais la clé publique.",
120
+ "meta": "sélection : passkeys.store = mongoose" },
121
+ { "icon": "🪝", "title": "MongooseWebhookStore", "href": "#webhooks",
122
+ "desc": "Le registre durable des destinations à notifier : il survit au redémarrage, contrairement au store mémoire, et pagine sans second comptage.",
123
+ "meta": "sélection : webhooks.store = mongoose" }
124
+ ]
125
+ ```
126
+
127
+ ## 🧠 Le schéma général
128
+
129
+ ```mermaid
130
+ flowchart TD
131
+ APP["Ton application<br/>controllers, services"] --> REPO["IRepository&lt;T&gt;<br/>contrat portable orm-core"]
132
+ REPO --> MR["MongooseRepository<br/>id → _id · $like → $regex"]
133
+ MR --> MODEL["Modèle Mongoose<br/>compilé au boot"]
134
+ MODEL --> CNX["Connexion isolée<br/>mongoose.createConnection"]
135
+ CNX --> DB[("MongoDB")]
136
+
137
+ CFG["nodefony.config.ts<br/>use('@nodefony/mongoose', …)"] --> SVC["MongooseService<br/>1 ORM par connecteur"]
138
+ SVC --> CNX
139
+ ENT["Tes entités<br/>defineEntity + @entities"] --> MODEL
140
+
141
+ SESS["session"] -.->|store: mongoose| MR
142
+ TOK["tokens · passkeys · webhooks"] -.->|store: mongoose| MR
143
+ USR["users"] -.->|provisionUsers| MR
144
+ ```
145
+
146
+ Place dans le graphe de dépendances : `@nodefony/mongoose` s'appuie sur
147
+ [`@nodefony/orm-core`](../../orm-core/docs/index.md) (les contrats), et **fournit** des
148
+ implémentations à [`@nodefony/http`](../../http/docs/index.md) (sessions),
149
+ [`@nodefony/security`](../../security/docs/index.md) (jetons, passkeys, webhooks) et
150
+ [`@nodefony/user`](../../user/docs/index.md) (comptes) — sans jamais dépendre d'eux au **runtime** :
151
+ ces modules ne sont connus qu'en `import type`, et c'est le driver qui se **déclare** à eux.
152
+
153
+ ## 📖 Lexique
154
+
155
+ | Terme | Sens |
156
+ | ------------------ | ----------------------------------------------------------------------------------------------------- |
157
+ | MongoDB | Base **documentaire** : elle range des documents (JSON) dans des collections, sans schéma imposé. |
158
+ | Mongoose | La bibliothèque Node.js qui pose un **schéma** et une validation par-dessus MongoDB. |
159
+ | Document | Un enregistrement (l'équivalent d'une ligne SQL), mais imbriqué et de forme libre. |
160
+ | Collection | Un ensemble de documents (l'équivalent d'une table). |
161
+ | `_id` / `ObjectId` | La clé primaire implicite de tout document Mongo ; `ObjectId` est son type par défaut. |
162
+ | Virtuel | Un champ calculé, absent de la base, exposé à la sérialisation (ici : `id`, la forme texte de `_id`). |
163
+ | `populate` | Le remplacement d'une référence par le document visé — le pendant Mongo d'une jointure. |
164
+ | Replica set | Un groupe de serveurs Mongo répliqués. **Obligatoire** pour les transactions. |
165
+ | Connecteur | Une connexion **nommée** déclarée en config (`nodefony` par défaut) ; sa clé sert de nom d'ORM. |
166
+ | Repository | L'objet qui lit et écrit une entité, avec une API identique sur tous les drivers. |
167
+ | Store (brique) | L'implémentation d'une brique du framework (session, jetons…) sur un backend donné. |
168
+ | Upsert | « écris, et crée si ça n'existe pas » — en une seule opération atomique. |
169
+ | PAT | Personal Access Token : une clé d'API opaque, révocable côté serveur. |
170
+ | `jti` | L'identifiant unique d'un JWT — ce qu'on met en denylist pour le révoquer. |
171
+ | GC | Garbage collector : ici, la purge périodique des enregistrements expirés. |
172
+
173
+ ## ❓ Qu'est-ce que c'est ?
174
+
175
+ **MongoDB range des documents, pas des lignes.** Une commande, avec ses articles, son adresse de
176
+ livraison et son historique de statuts, tient dans **un seul document** — là où le SQL éclaterait la
177
+ même chose en quatre tables reliées par des clés étrangères. Pas de schéma imposé par la base : la
178
+ forme des données vit dans le code, et elle peut varier d'un document à l'autre.
179
+
180
+ C'est le bon outil quand la donnée est **hétérogène** (chaque objet a ses propres champs), quand elle
181
+ est **imbriquée** (on lit et écrit le tout d'un bloc), ou quand la forme **change souvent** — un
182
+ champ ajouté ne demande aucune migration. C'est le mauvais outil quand tu as besoin de jointures
183
+ arbitraires entre entités indépendantes, ou de contraintes relationnelles fortes.
184
+
185
+ **Mongoose** est la bibliothèque Node.js standard pour parler à MongoDB. Elle ajoute ce que la base
186
+ n'impose pas : un **schéma** déclaré, la validation, les champs calculés, les références entre
187
+ documents. `@nodefony/mongoose` est l'adaptation de Mongoose au framework : il gère le cycle de vie
188
+ des connexions, la compilation des schémas, et présente le tout derrière le contrat portable de
189
+ [`@nodefony/orm-core`](../../orm-core/docs/index.md).
190
+
191
+ ## 🧭 La vision Nodefony — une couverture adaptée, pas une course à la parité
192
+
193
+ Nodefony ne demande pas à chaque backend de tout savoir faire. **Chaque adapter déclare ce qu'il
194
+ implémente**, dans son `package.json` (clé `nodefony.stores`), et le framework lit cette déclaration
195
+ à chaud (`readAdapterManifest()` (`KernelAdminApi.ts:84`)). Rien n'est curaté dans le cœur : la
196
+ source de vérité, c'est l'adapter lui-même — ce qui vaut aussi pour un adapter tiers.
197
+
198
+ Ce que `@nodefony/mongoose` déclare : `session`, `user`, `tokens`, `passkeys`, `webhooks`, avec la
199
+ nature `durable`. Comparé aux deux autres adapters officiels :
200
+
201
+ <!-- prettier-ignore -->
202
+ | Brique | `@nodefony/drizzle` (SQL) | `@nodefony/mongoose` (Mongo) | `@nodefony/redis` (cache) |
203
+ | --- | :---: | :---: | :---: |
204
+ | `session` | ✅ | ✅ | ✅ |
205
+ | `user` | ✅ | ✅ | — |
206
+ | `tokens` | ✅ | ✅ | ✅ |
207
+ | `passkeys` | ✅ | ✅ | ✅ |
208
+ | `webhooks` | ✅ | ✅ | — |
209
+ | `totp` | ✅ | — | — |
210
+ | `audit` | ✅ | — | — |
211
+ | `idempotency` | ✅ | — | ✅ |
212
+ | Nature | durable | durable | cache |
213
+
214
+ > [!IMPORTANT]
215
+ > **Ces trois cases vides sont un manque, et il sera comblé.** L'objectif est qu'une application
216
+ > puisse tourner **entièrement sur MongoDB, sans charger `@nodefony/drizzle`** — donc mongoose à 8/8
217
+ > (`MIGRATION_STATUS.md`, P7.11). Le raisonnement « ces briques-là appellent d'autres propriétés »
218
+ > décrit une préférence technique, pas ce que vit l'utilisateur : **tu choisis une base de données, tu
219
+ > ne choisis pas de perdre le 2FA, la traçabilité ou la déduplication.**
220
+ >
221
+ > En attendant, ces trois briques se résolvent ailleurs **et te le disent** (repli annoncé au boot,
222
+ > avertissement en production) — mais mesure ce que le repli coûte : secrets TOTP perdus au
223
+ > redémarrage (utilisateurs verrouillés hors de leur second facteur), journal d'audit volatil,
224
+ > idempotence sans effet entre pods. La parade immédiate tient en une ligne : charger
225
+ > `@nodefony/drizzle` à côté de Mongo, **même en SQLite local** — les deux modules cohabitent, chaque
226
+ > brique choisit son store.
227
+ >
228
+ > La colonne `redis` obéit à une autre logique : elle gagnera `totp` (au régime opt-in de ses jetons
229
+ > et passkeys, jamais choisi par `auto`), mais pas `user`, `audit` ni `webhooks` — non parce que
230
+ > Redis serait « un cache », mais parce que ces données croissent sans borne, se conservent longtemps
231
+ > et se consultent. Ça, c'est un choix.
232
+
233
+ ### Ce qui se passe quand tu ne choisis rien
234
+
235
+ Chaque brique a une clé `store` dont le défaut est `"auto"`. La résolution
236
+ (`resolveAutoStore()` (`infra.ts:241`)) suit l'infrastructure **déclarée**, bornée aux backends
237
+ réellement chargés :
238
+
239
+ | Ta situation | Ce que `auto` choisit |
240
+ | ------------------------------------------------------- | -------------------------------------------------------- |
241
+ | `NF_DATABASE_URL=mongodb://…` + module mongoose chargé | `mongoose` — « infra database (mongodb) » |
242
+ | Idem, mais pour une brique que mongoose ne porte pas | repli annoncé (`memory`), avec la **raison** dans le log |
243
+ | Aucune infra déclarée, mongoose chargé (et pas drizzle) | `mongoose` — backend local persistant |
244
+ | `NF_REDIS_URL` déclaré, brique non durable (session…) | `redis` d'abord (le cache passe avant la base) |
245
+
246
+ Rien n'est jamais dégradé en silence : la raison du choix est journalisée. Et si tu nommes
247
+ **explicitement** un store qui n'existe pas (`audit: { store: "mongoose" }`), l'échec est franc —
248
+ boot avorté en production, brique désactivée avec un log `CRITIC` en développement. Un store durable
249
+ ne retombe **jamais** en mémoire sans le dire.
250
+
251
+ ## 🚀 Démarrage rapide
252
+
253
+ Vu d'une application créée par `nodefony create app` : déclarer la base, décrire une entité, la lire.
254
+ Trois étapes, dans l'ordre.
255
+
256
+ ### 1. Déclarer la base
257
+
258
+ ```typescript
259
+ // nodefony.config.ts — le manifeste des modules de l'app
260
+ export default defineConfig(() => ({
261
+ modules: [
262
+ // Le driver AVANT les modules qui consomment ses stores (security, http) :
263
+ // les fabriques de store exigent un ORM déjà connecté.
264
+ use("@nodefony/mongoose", {
265
+ connectors: {
266
+ // `nodefony` = le connecteur par défaut du module (≠ `default` de Drizzle).
267
+ nodefony: { host: "127.0.0.1", port: 27017, dbname: "blog" },
268
+ },
269
+ }),
270
+ "@nodefony/http",
271
+ "@nodefony/framework",
272
+ ],
273
+ }));
274
+ ```
275
+
276
+ En production, tu ne touches pas à ce fichier : `MONGODB_URI` (ou `NF_DATABASE_URL`) surcharge l'URI
277
+ du connecteur primaire — c'est là que vit le secret. Détail : [Configuration](./configuration.md).
278
+
279
+ ### 2. Décrire l'entité, l'inscrire, l'utiliser
280
+
281
+ Un module minimal tient dans un fichier : le schéma, le type de ligne, le controller, et le module qui
282
+ inscrit les deux. Dans une vraie application, ces trois blocs vivent dans `entity/`, `controllers/` et
283
+ `index.ts` — mais l'ordre logique, lui, ne change pas.
284
+
285
+ ```typescript
286
+ // nodefony/index.ts d'un module « blog » — complet, compile tel quel
287
+ import { Module, Kernel } from "nodefony";
288
+ import { defineEntity, entities, ormRegistry } from "@nodefony/orm-core";
289
+ import type { IRepository } from "@nodefony/orm-core";
290
+ import type { SchemaDefinition } from "mongoose";
291
+ import {
292
+ controller,
293
+ controllers,
294
+ Controller,
295
+ Get,
296
+ Post,
297
+ Body,
298
+ } from "@nodefony/framework";
299
+
300
+ // ── 1. Le schéma Mongoose : ce que contient un article ──────────────────────
301
+ const articleSchema: SchemaDefinition = {
302
+ title: { type: String, required: true },
303
+ slug: { type: String, index: true, unique: true },
304
+ tags: { type: [String], default: [] },
305
+ views: { type: Number, default: 0 },
306
+ };
307
+
308
+ /** La forme plate rendue par le repository — `id` est le virtuel de `_id`. */
309
+ interface ArticleRow {
310
+ id: string;
311
+ title: string;
312
+ slug: string;
313
+ tags: string[];
314
+ views: number;
315
+ createdAt: Date;
316
+ updatedAt: Date;
317
+ }
318
+
319
+ // `defineEntity` ne fait qu'attacher un type : aucun effet de bord, aucune
320
+ // inscription. C'est le décorateur `@entities` du module qui inscrit.
321
+ const ArticleEntity = defineEntity({
322
+ name: "Article",
323
+ module: "blog",
324
+ schema: articleSchema,
325
+ timestamps: true, // Mongoose gère createdAt / updatedAt
326
+ });
327
+
328
+ // ── 2. Le controller : le repository se demande au registre ─────────────────
329
+ @controller("/api/articles")
330
+ class ArticleController extends Controller {
331
+ #articles(): IRepository<ArticleRow> {
332
+ return ormRegistry.get("nodefony").getRepository<ArticleRow>("Article");
333
+ }
334
+
335
+ @Get("/")
336
+ async list() {
337
+ // Critères portables : opérateurs `$`, tri, pagination — traduits en Mongo.
338
+ const items = await this.#articles().find(
339
+ { views: { $gte: 10 } },
340
+ { limit: 20, order: [["views", "DESC"]] },
341
+ );
342
+ return this.renderJson({ items });
343
+ }
344
+
345
+ @Post("/")
346
+ async create(@Body() body: { title: string; slug: string }) {
347
+ const created = await this.#articles().create(body);
348
+ return this.renderJson(created, 201);
349
+ }
350
+ }
351
+
352
+ // ── 3. Le module : `@entities` inscrit à la phase `onRegister`, AVANT que
353
+ // l'ORM ne se connecte — c'est ce qui garantit que le modèle est compilé.
354
+ @entities([ArticleEntity], { connector: "nodefony" })
355
+ @controllers([ArticleController])
356
+ class Blog extends Module {
357
+ constructor(kernel: Kernel) {
358
+ super("blog", kernel, import.meta.url, {});
359
+ }
360
+ }
361
+
362
+ export default Blog;
363
+ ```
364
+
365
+ ### 3. Ce qu'on observe
366
+
367
+ ```bash
368
+ # Création
369
+ curl -s -X POST http://localhost:5151/api/articles \
370
+ -H 'Content-Type: application/json' \
371
+ -d '{"title":"Premier","slug":"premier"}'
372
+ # {"id":"66a1…c3","title":"Premier","slug":"premier","tags":[],"views":0, …}
373
+ # ▲ `id` est le virtuel de `_id` : le contrat `id: string` est tenu, sans ObjectId qui fuit.
374
+
375
+ # Lecture filtrée
376
+ curl -s 'http://localhost:5151/api/articles'
377
+ # {"items":[ … ]}
378
+ ```
379
+
380
+ Au démarrage, le journal du serveur annonce la connexion — **sans jamais les identifiants** :
381
+
382
+ ```
383
+ INFO mongoose Mongoose ORM "nodefony" connected (127.0.0.1:27017/blog)
384
+ ```
385
+
386
+ ### Brancher les briques du framework
387
+
388
+ Une fois le driver chargé, les stores Mongo deviennent **sélectionnables par leur nom**, sans aucun
389
+ câblage : le module les enregistre lui-même à son démarrage
390
+ (`registerMongooseFrameworkStores()` (`registerStores.ts:94`)).
391
+
392
+ ```typescript
393
+ // nodefony.config.ts — sessions, jetons, passkeys et webhooks dans Mongo
394
+ export default defineConfig(() => ({
395
+ modules: [
396
+ use("@nodefony/mongoose", {
397
+ connectors: { nodefony: { uri: "mongodb://127.0.0.1:27017/app" } },
398
+ }),
399
+ use("@nodefony/http", { session: { store: "mongoose" } }),
400
+ use("@nodefony/security", {
401
+ tokenStore: { store: "mongoose" },
402
+ passkeys: { store: "mongoose" },
403
+ webhooks: { store: "mongoose" },
404
+ }),
405
+ "@nodefony/framework",
406
+ ],
407
+ }));
408
+ ```
409
+
410
+ > [!TIP]
411
+ > Si `NF_DATABASE_URL` pointe déjà sur `mongodb://…`, tu peux **tout laisser en `auto`** (le défaut) :
412
+ > chaque brique portée par Mongo s'y résout d'elle-même, et les autres se replient en l'annonçant.
413
+
414
+ ### Brancher l'annuaire utilisateurs
415
+
416
+ Le dépôt d'utilisateurs n'est pas choisi par une clé de config : c'est **ton application** qui le
417
+ pose, dans le `provisionUsers` généré par `nodefony create app`. Une seule ligne change.
418
+
419
+ ```typescript
420
+ // nodefony/security/provisionUsers.ts (extrait — variante Mongo)
421
+ import type { Module } from "nodefony";
422
+ import { ormRegistry } from "@nodefony/orm-core";
423
+ import { UserService } from "@nodefony/user";
424
+ import type { IPasswordEncoder } from "@nodefony/user";
425
+ import { MongooseUserRepository } from "@nodefony/mongoose";
426
+ import type { MongooseOrm } from "@nodefony/mongoose";
427
+
428
+ export async function provisionUsers(module: Module): Promise<void> {
429
+ const container = module.container;
430
+ if (!container || container.has("users")) return;
431
+
432
+ // Posé par @nodefony/security ; son absence = échec franc, pas de repli muet.
433
+ const encoder = container.get<IPasswordEncoder>("passwordEncoder");
434
+ if (!encoder) {
435
+ throw new Error("provisionUsers : @nodefony/security n'est pas chargé");
436
+ }
437
+
438
+ const orm = ormRegistry.get("nodefony") as MongooseOrm;
439
+ container.set(
440
+ "users",
441
+ new UserService(MongooseUserRepository.from(orm), encoder),
442
+ );
443
+ }
444
+ ```
445
+
446
+ ## 🏗️ Architecture interne — du boot à la requête
447
+
448
+ ### Ce qui se passe au démarrage
449
+
450
+ ```mermaid
451
+ sequenceDiagram
452
+ participant K as Kernel
453
+ participant M as Module Mongoose
454
+ participant S as MongooseService
455
+ participant O as MongooseOrm
456
+ participant DB as MongoDB
457
+
458
+ K->>M: onKernelRegister
459
+ M->>M: valider la config (Zod) + geler
460
+ M->>M: déclarer les entités framework<br/>(tokens · passkeys · webhooks)
461
+ M->>M: déclarer "mongoose" comme backend utilisateur
462
+ K->>M: onKernelBoot
463
+ M->>M: monter le data plane ORM + l'adapter d'erreurs
464
+ K->>S: onBoot
465
+ S->>O: new MongooseOrm(nom, uri, options)
466
+ O->>DB: createConnection (isolée)
467
+ O->>O: compiler schémas → relations → modèles
468
+ O-->>S: connecté (s'inscrit dans ormRegistry)
469
+ K->>S: onTerminate
470
+ S->>O: disconnect (toutes les connexions)
471
+ ```
472
+
473
+ L'ordre n'est pas cosmétique. Les entités sont déclarées à `onKernelRegister`, **strictement avant**
474
+ le `connect` de `onBoot` : les modèles sont compilés à la connexion
475
+ (`MongooseOrm.onConnect()` (`MongooseOrm.ts:74`)), donc une entité déclarée trop tard n'existe tout
476
+ simplement pas. C'est aussi pour ça que `@entities` s'exécute à la phase `onRegister` et non `onBoot`.
477
+
478
+ Chaque connecteur ouvre une **connexion isolée** (`mongoose.createConnection`), pas le singleton
479
+ global de Mongoose : c'est ce qui permet à plusieurs bases — voire plusieurs ORM — de cohabiter dans
480
+ le même processus.
481
+
482
+ Le service orchestre ce cycle de bout en bout : il ouvre une connexion par connecteur déclaré au
483
+ démarrage (`MongooseService.connectAll()` (`MongooseService.ts:63`)) et referme tout à l'arrêt
484
+ (`MongooseService.disconnectAll()` (`MongooseService.ts:134`)). Le module se déclare **non critique**
485
+ (`Mongoose.critical` (`mongoose/index.ts:48`)) : une base injoignable ne tue pas le processus —
486
+ l'application monte quand même, l'échec est journalisé, et c'est l'orchestrateur qui relèvera Mongo.
487
+
488
+ > [!NOTE]
489
+ > **Pourquoi le connecteur par défaut s'appelle `nodefony` et pas `default`.** Les entités sont
490
+ > indexées par `(connecteur, nom)` dans un registre **global au processus**. Si Drizzle (dont le
491
+ > connecteur par défaut est `default`) et Mongoose tournaient ensemble avec le même nom, leurs deux
492
+ > entités `session` entreraient en collision. Un nom distinct par driver règle le problème par
493
+ > construction (`FRAMEWORK_CONNECTOR` (`registerStores.ts:49`)).
494
+
495
+ ### Le trajet d'une requête
496
+
497
+ `repo.find({ views: { $gte: 10 } }, { limit: 20 })` traverse quatre étapes :
498
+
499
+ 1. **Traduction du critère** (`MongooseRepository.#filter()` (`MongooseRepository.ts:184`)) : chaque
500
+ champ est résolu (`id` devient `_id`), chaque opérateur portable est converti.
501
+ 2. **Validation du champ** (`MongooseRepository.#resolveField()` (`MongooseRepository.ts:162`)) : un
502
+ champ absent du schéma lève `UnknownCriteriaField` — plutôt que de renvoyer zéro résultat sans
503
+ rien dire, ce qui est la pire façon d'échouer.
504
+ 3. **Exécution** : la requête Mongoose est construite (session transactionnelle, `populate`, `skip`,
505
+ `limit`, `sort`), puis exécutée.
506
+ 4. **Sérialisation** : chaque document sort en objet plat, **virtuels compris** — c'est là que `id`
507
+ apparaît.
508
+
509
+ Une sonde facultative encadre l'opération (`MongooseRepository.#prof()` (`MongooseRepository.ts:79`)) :
510
+ voir [Performance et mémoire](#-performance-et-mémoire).
511
+
512
+ ## 🧰 Le repository portable — l'API que tu utilises vraiment
513
+
514
+ Toutes les opérations sont typées par ta ligne d'entité et disponibles à l'identique sur les autres
515
+ drivers. Les signatures exactes vivent dans le graphe généré
516
+ (`jq '.symbols.MongooseRepository' .ai/symbols.json`) — jamais recopiées ici, elles divergeraient.
517
+
518
+ <!-- prettier-ignore -->
519
+ | Opération | Ce que ça fait | Traduction Mongo |
520
+ | --- | --- | --- |
521
+ | `find` / `findOne` | lire, avec tri, pagination, relations | `find` / `findOne` (+ `populate`, `skip`, `sort`) |
522
+ | `create` / `createMany` | insérer | `create` / `insertMany` |
523
+ | `updateOne` | modifier et **rendre le document à jour** | `findOneAndUpdate` atomique, 1 aller-retour |
524
+ | `upsert` | écrire, créer si absent | `findOneAndUpdate` avec `upsert` + `$setOnInsert` |
525
+ | `increment` | ajouter un delta à un compteur | `$inc` côté serveur — pas de lecture-écriture |
526
+ | `updateMany` | modifier en masse | `updateMany` |
527
+ | `delete` / `deleteOne` | supprimer | `deleteMany` / `deleteOne` |
528
+ | `findOneAndDelete` | supprimer **et** récupérer le document | `findOneAndDelete` |
529
+ | `count` / `exists` | compter / tester l'existence | `countDocuments` / `exists` |
530
+ | `withTransaction` | rejouer les mêmes opérations dans une transaction | ajoute la `session` à chaque opération |
531
+
532
+ Les écritures qui « lisent puis écrivent » sont **atomiques par construction**
533
+ (`MongooseRepository.upsert()` (`MongooseRepository.ts:343`),
534
+ `MongooseRepository.increment()` (`MongooseRepository.ts:429`)) : un seul aller-retour, la
535
+ comparaison est faite par le serveur. Ce n'est pas une optimisation cosmétique — c'est ce qui évite
536
+ que deux requêtes simultanées lisent le même état et s'écrasent mutuellement.
537
+
538
+ ### Les critères et leurs opérateurs
539
+
540
+ Les opérateurs portables sont ceux d'[`orm-core`](../../orm-core/docs/index.md), et la plupart sont
541
+ natifs en Mongo. Deux méritent une explication (`MongooseRepository.#mongoOps()` (`MongooseRepository.ts:127`)) :
542
+
543
+ | Opérateur portable | Côté Mongo | Remarque |
544
+ | --------------------------- | ------------------------- | ------------------------------------------------------------------- |
545
+ | `$eq $ne $gt $gte $lt $lte` | identiques | natifs |
546
+ | `$in` / `$nin` | identiques | natifs |
547
+ | `$like: "ab%"` | `$regex` **ancrée** | motif SQL traduit (`sqlLikeToRegex()` (`MongooseRepository.ts:25`)) |
548
+ | `$null: true` / `false` | `$eq: null` / `$ne: null` | en Mongo, `null` couvre aussi le champ **absent** |
549
+ | `$max` / `$min` (écriture) | `$max` / `$min` natifs | l'équivalent du `GREATEST(col, ?)` SQL |
550
+
551
+ ```typescript
552
+ await articles.find({ tags: "nodefony" }); // tableau : appartenance native
553
+ await articles.find({ views: { $gte: 10, $lt: 100 } }); // plusieurs opérateurs = ET
554
+ await articles.find({ slug: { $like: "guide-%" } }); // motif SQL → regex ancrée
555
+ await articles.find({ publishedAt: { $null: true } }); // jamais publié
556
+ ```
557
+
558
+ > [!WARNING]
559
+ > Un critère ne combine **qu'une condition par champ** (c'est un ET de champs). Pour un `$or`, une
560
+ > agrégation, une recherche plein texte ou un index composé : passe par
561
+ > [la trappe native](#la-trappe-native--quand-le-contrat-ne-suffit-plus). C'est prévu, pas subi.
562
+
563
+ ### Relations — sans clé étrangère
564
+
565
+ MongoDB n'a pas de clé étrangère. L'adapter traduit les relations déclarées en **références
566
+ `ObjectId`** plus, quand il faut, un champ virtuel :
567
+
568
+ | Relation | Ce que fait l'adapter |
569
+ | ---------------------------- | ------------------------------------------------------------------- |
570
+ | `one-to-many` | référence posée sur l'**enfant** + virtuel `populate` sur le parent |
571
+ | `many-to-one` / `one-to-one` | champ de référence posé sur la **source** |
572
+ | `many-to-many` | **refusé explicitement** — à déclarer via la connexion native |
573
+
574
+ Le chargement se demande à la lecture : `find(criteria, { relations: ["comments"] })` devient un
575
+ `populate`. Le refus du `many-to-many` est volontaire : il n'a pas de traduction unique en Mongo
576
+ (tableau de références ? collection de liaison ?), et un choix imposé serait un mauvais choix.
577
+
578
+ ### Transactions
579
+
580
+ ```typescript
581
+ await orm.transaction(async (tx) => {
582
+ const orders = repo.withTransaction(tx);
583
+ const stock = stockRepo.withTransaction(tx);
584
+ await orders.create({ ref: "A-1" });
585
+ await stock.increment({ sku: "X" }, { quantity: -1 });
586
+ }); // commit si la fonction résout, annulation si elle échoue
587
+ ```
588
+
589
+ `MongooseOrm.transaction()` (`MongooseOrm.ts:483`) s'appuie sur les sessions Mongo « managées »
590
+ (commit, annulation et **reprises** gérées par le driver).
591
+
592
+ > [!IMPORTANT]
593
+ > **Les transactions exigent un replica set.** Un serveur MongoDB isolé (`mongod` seul, l'installation
594
+ > par défaut) ne les supporte pas. En développement, démarre un replica set à un nœud ; en production,
595
+ > Atlas et la plupart des services managés en fournissent un d'office. Les points de sauvegarde
596
+ > intermédiaires (`savepoint`) n'existent pas en Mongo : ce sont des opérations neutres.
597
+
598
+ ### La trappe native — quand le contrat ne suffit plus
599
+
600
+ `MongooseOrm.getNativeConnection()` (`MongooseOrm.ts:501`) rend la connexion Mongoose telle quelle :
601
+ agrégations, `$or`, index, `$text`, `bulkWrite`, changements de flux. Le module lui-même s'en sert
602
+ là où le contrat portable ne suffit pas — par exemple pour la recherche texte du listing des
603
+ webhooks, qui a besoin d'un `$or` sur deux champs
604
+ (`MongooseWebhookStore.#listFilter()` (`MongooseWebhookStore.ts:167`)).
605
+
606
+ C'est un **anti-blocage assumé** : le contrat portable couvre le quotidien, la trappe couvre le reste.
607
+ Le code qui l'emprunte cesse d'être portable, et ça se voit — ce qui est exactement le but.
608
+
609
+ ## 🔐 Les stores fournis
610
+
611
+ Chaque store implémente le contrat d'un autre module, mais **sans dépendre de lui au runtime** : le
612
+ contrat est importé en `import type` (effacé à la compilation), et c'est le driver qui s'annonce
613
+ auprès du module consommateur. Le module met en place tout ce câblage à son enregistrement, avant que
614
+ la connexion ne s'ouvre.
615
+
616
+ | Store | Contrat | Comment le choisir | Collections |
617
+ | ------------ | -------------------------- | ----------------------------------- | -------------------------------------------------- |
618
+ | Sessions | `ISessionStorage` (http) | `session: { store: "mongoose" }` | `session` |
619
+ | Utilisateurs | `IUserRepository` (user) | via `provisionUsers` de ton app | `User` |
620
+ | Jetons | `ITokenStore` (security) | `tokenStore: { store: "mongoose" }` | `access_token`, `denied_jti`, `subject_revocation` |
621
+ | Passkeys | `IWebAuthnCredentialStore` | `passkeys: { store: "mongoose" }` | `webauthn_credential` |
622
+ | Webhooks | `IWebhookStore` | `webhooks: { store: "mongoose" }` | `webhook_endpoint` |
623
+
624
+ L'auto-enregistrement peut être coupé (`frameworkEntities: false` (`config.ts:102`)) : le module
625
+ devient alors un pur driver de données, sans schéma framework.
626
+
627
+ ### Sessions
628
+
629
+ `SessionStorage` s'enregistre sous le nom `mongoose` auprès du service de sessions de
630
+ [`@nodefony/http`](../../http/docs/session.md) — l'inverse de ce qu'on attendrait, et c'est le point :
631
+ `http` ne connaît aucun ORM, ce sont les ORM qui se déclarent.
632
+
633
+ Trois comportements valent d'être connus :
634
+
635
+ - **Purge à deux bornes** — `idleTimeoutS` et `absoluteTimeoutS` (`SessionStorage.gc()` (`SessionStorage.ts:156`)) :
636
+ l'inactivité (depuis la dernière activité) et l'âge absolu (depuis la création, **jamais prolongé** —
637
+ la ré-authentification finit par être imposée, conformément aux recommandations NIST/OWASP).
638
+ - **Prolongation sans réécriture** (`SessionStorage.touch()` (`SessionStorage.ts:174`)) : rafraîchir
639
+ l'activité ne réécrit pas le contenu de la session, juste son horodatage.
640
+ - **Écran d'administration redacté par construction** (`SessionStorage.listPage()` (`SessionStorage.ts:277`)) :
641
+ le contenu applicatif et les messages flash **ne sortent pas de la base**. Studio affiche qui est
642
+ connecté, jamais ce qu'il y a dans sa session.
643
+
644
+ Quand l'ORM n'est plus connecté — typiquement pendant l'arrêt du serveur, alors que des requêtes sont
645
+ encore en vol — le store dégrade **gracieusement** au lieu de lever une exception
646
+ (`SessionStorage.#repo()` (`SessionStorage.ts:45`)). Une session non persistée le temps de l'arrêt
647
+ vaut mieux qu'une erreur 500 et un rejet non capturé.
648
+
649
+ ### Utilisateurs
650
+
651
+ `MongooseUserRepository` rend des objets `BaseUser` (avec leur comportement : rôles, actif, verrouillé),
652
+ pas des documents nus. Deux recherches lui sont propres :
653
+
654
+ - **par compte social lié** (`MongooseUserRepository.findBySocialProvider()` (`MongooseUserRepository.ts:256`)) :
655
+ un `$elemMatch` sur un tableau libre de fournisseurs — le pendant Mongo du parcours JSON en SQL.
656
+ C'est ce qui porte le motif « Shadow User » d'OAuth (un compte créé à la volée au premier login social),
657
+ **sans colonne par fournisseur** : ajouter GitHub demain n'est pas une migration.
658
+ - **listing paginé** (`MongooseUserRepository.listPage()` (`MongooseUserRepository.ts:299`)) : requête
659
+ native bornée (`skip`/`limit + 1`), tri sur liste blanche, `_id` en départage. Une page est une page,
660
+ jamais la collection entière rapatriée en mémoire.
661
+
662
+ Le comptage des administrateurs actifs (`MongooseUserRepository.countActiveAdmins()` (`MongooseUserRepository.ts:328`))
663
+ compte côté serveur — c'est le garde-fou qui empêche de supprimer le dernier administrateur.
664
+
665
+ ### Jetons
666
+
667
+ Trois collections : les jetons eux-mêmes, la denylist de `jti`, les seuils de révocation par porteur.
668
+ Deux invariants de sécurité sont tenus **par la requête**, pas par du code JavaScript entre deux appels :
669
+
670
+ - **Révocation idempotente** (`MongooseTokenStore.revoke()` (`MongooseTokenStore.ts:215`)) : la
671
+ condition « pas encore révoqué » est dans le filtre. Deux révocations simultanées ne se recouvrent
672
+ pas ; la première date et la première raison sont conservées.
673
+ - **Seuil monotone** (`MongooseTokenStore.revokeAllForSubject()` (`MongooseTokenStore.ts:297`)) : le
674
+ « déconnecte-moi de partout » utilise `$max`. Avec une lecture suivie d'une écriture, deux
675
+ déconnexions simultanées pourraient reposer un seuil **plus ancien** — et des jetons révoqués
676
+ redeviendraient valides. Ici, c'est structurellement impossible.
677
+
678
+ La purge, bornée par `retentionRevokedMs` (`MongooseTokenStore.gc()` (`MongooseTokenStore.ts:313`)), s'appuie sur une particularité utile
679
+ de Mongo : une comparaison numérique **ignore** les documents dont le champ est `null`. Les jetons sans
680
+ expiration ne sont donc jamais balayés par erreur ; ils partent par une règle de rétention distincte.
681
+
682
+ ### Passkeys
683
+
684
+ Une collection, l'identifiant du credential en clé primaire. L'enregistrement passe par un `upsert`
685
+ atomique (`MongooseWebAuthnCredentialStore.save()` (`MongooseWebAuthnCredentialStore.ts:94`)) : deux
686
+ enregistrements concurrents de la même passkey ne peuvent pas produire de collision de clé.
687
+
688
+ Le listing d'administration (`MongooseWebAuthnCredentialStore.listPage()` (`MongooseWebAuthnCredentialStore.ts:158`))
689
+ projette **sans la clé publique** — elle ne franchit jamais la frontière du store. La recherche libre
690
+ est un **préfixe ancré**, pas une expression régulière fournie par l'appelant : une recherche
691
+ utilisateur n'est jamais interprétée comme du code.
692
+
693
+ ### Webhooks
694
+
695
+ Registre **durable** des destinations à notifier, par opposition au store mémoire qui disparaît au
696
+ redémarrage. Le listing paginé (`MongooseWebhookStore.listPage()` (`MongooseWebhookStore.ts:188`))
697
+ lit `limit + 1` documents pour savoir s'il existe une page suivante — sans second comptage — et
698
+ échappe les métacaractères de la recherche texte.
699
+
700
+ Détail révélateur de la doctrine du framework : si le store est construit sans modèle natif, le
701
+ listing paginé **refuse** de répondre (`MongooseWebhookStore.#nativeModel()` (`MongooseWebhookStore.ts:151`))
702
+ au lieu de retomber sur un chargement complet de la collection. Une garantie silencieusement trahie
703
+ serait pire qu'une erreur.
704
+
705
+ ## 🗃 Ce qui est stocké
706
+
707
+ Les cinq schémas portés par le module. Les collections sont créées à la volée par MongoDB — il n'y a
708
+ ni migration ni DDL à jouer, ce qui est l'un des vrais conforts du modèle documentaire.
709
+
710
+ > ⚠️ **Le revers de ce confort : ce qui n'est pas déclaré est JETÉ, sans un mot.** Mongoose valide en
711
+ > mode strict par défaut, et un champ absent du schéma n'est pas refusé à l'écriture — il est
712
+ > silencieusement écarté, puis relu comme `undefined`. Là où une base SQL t'arrête sur « colonne
713
+ > inconnue », Mongo te rend un document amputé qui a l'air normal. Donc : **un champ que tu ajoutes à
714
+ > une entité doit être ajouté à son SCHÉMA**, et pas seulement au type TypeScript qui te dit qu'il
715
+ > existe. C'est le pendant documentaire de la migration : tu n'as rien à jouer sur la base, mais tu
716
+ > as toujours une déclaration à tenir à jour.
717
+ >
718
+ > Cela vaut pour l'entité `User`, que ton application possède : ses colonnes viennent du contrat
719
+ > partagé (`USER_COLUMNS`), et le module en dérive un schéma Mongoose. Un champ métier que tu ajoutes
720
+ > à ton utilisateur suit le même chemin — déclaré, donc écrit ; oublié, donc perdu en silence.
721
+
722
+ | Collection | Clé primaire (`_id`) | Contenu | Horodatages |
723
+ | --------------------- | ---------------------------- | ------------------------------------------------------------------ | ------------------ |
724
+ | `session` | `ObjectId` (auto) | identifiant de session, contenu, messages flash, méta, utilisateur | nombres (ms) |
725
+ | `User` | `ObjectId` (auto) | identifiant, mot de passe haché, rôles, comptes sociaux, méta | gérés par Mongoose |
726
+ | `access_token` | le `jti` (texte) | type, porteur, périmètres, empreinte du secret, révocation | nombres (ms) |
727
+ | `denied_jti` | le `jti` (texte) | expiration | nombres (ms) |
728
+ | `subject_revocation` | le porteur (texte) | seuil `invalidBefore` | nombres (ms) |
729
+ | `webauthn_credential` | l'identifiant du credential | clé publique, compteur, transports, état de sauvegarde | nombres (ms) |
730
+ | `webhook_endpoint` | l'identifiant `wh_…` (texte) | URL, secret chiffré, événements, état des livraisons | nombres (ms) |
731
+
732
+ Deux choix structurants s'y lisent :
733
+
734
+ **La clé naturelle prend la place de `_id`.** Pour les jetons, les passkeys et les webhooks,
735
+ l'identifiant vient de l'appelant (un `jti`, un identifiant d'authentificateur, un `wh_…`) : il est
736
+ posé **en clé primaire** (`accessTokenSchema` (`tokenEntity.ts:22`)) plutôt que dupliqué dans un champ
737
+ indexé à côté d'un `ObjectId` inutile. Gratuit : l'unicité, et l'éligibilité à un index d'expiration
738
+ natif.
739
+
740
+ **Les horodatages sont des nombres, pas des dates.** Les contrats du framework portent des `number`
741
+ (millisecondes depuis l'époque) : les stocker tels quels garde la logique de purge **strictement
742
+ identique** à celle de l'adapter SQL. Seule l'entité `User` utilise la gestion automatique de Mongoose
743
+ (`createUserEntity()` (`userEntity.ts:90`)), parce que son contrat porte des dates.
744
+
745
+ Le contrat expose partout `id: string`, jamais un `ObjectId` : le champ virtuel `id` est activé à la
746
+ sérialisation, sur toutes les entités compilées par l'adapter.
747
+
748
+ ## ⚙️ Configuration
749
+
750
+ Un point d'entrée : `use("@nodefony/mongoose", { … })`, validé par Zod au démarrage — une config
751
+ invalide fait échouer le boot avec un message précis, plutôt qu'un `undefined` qui explose trois
752
+ heures plus tard.
753
+
754
+ | Clé | Rôle | Défaut |
755
+ | ------------------- | -------------------------------------------------------------- | --------------------------------------- |
756
+ | `connectors` | les connexions nommées (la clé est le nom de l'ORM) | `nodefony` → `localhost:27017/nodefony` |
757
+ | `debug` | trace toutes les opérations Mongoose (développement) | `false` |
758
+ | `frameworkEntities` | déclare le schéma framework et rend ses stores sélectionnables | `true` |
759
+
760
+ Les valeurs font foi dans le schéma (`mongooseConfigSchema` (`config.ts:83`)) ; la surcharge par
761
+ l'environnement est appliquée **après** la validation
762
+ (`applyEnvOverrides()` (`defineModuleConfig.ts:22`)), ce qui garde le schéma pur et publiable en
763
+ JSON Schema pour Studio.
764
+
765
+ ➡️ **Tout le détail — champs, variables d'environnement, sécurité des identifiants — est dans
766
+ [Configuration](./configuration.md).** Voir aussi le
767
+ [guide de configuration transverse](../../../../../docs/guides/configuration.md).
768
+
769
+ ## 📡 Observabilité — Studio
770
+
771
+ Le module alimente le plan d'administration ORM, monté par
772
+ `wireOrmAdminPlane()` (`ormWiring.ts:31`) — appelé par **chaque** driver, ce qui garantit qu'une
773
+ application uniquement Mongo a un Studio ORM aussi vivant qu'une application SQL.
774
+
775
+ | Écran Studio | Ce que tu y vois |
776
+ | ---------------------- | ------------------------------------------------------------------- |
777
+ | `/nodefony/orm` | connecteurs, état, nombre d'entités, flux des requêtes |
778
+ | `/nodefony/orm-entity` | une entité : ses champs, ses types, sa clé primaire |
779
+ | `/nodefony/databases` | les connexions et leur santé |
780
+ | `/nodefony/stores` | chaque brique × son backend résolu, **et pourquoi** il a été retenu |
781
+
782
+ Côté données, le plan d'administration expose `/nodefony/orm/api/*` : `orms`, `entities`,
783
+ `entity/{name}`, `graph`, `counts`, `connection/health`, `flow`, `export/{format}` (DBML ou JSON
784
+ Schema). Le module fournit les sondes correspondantes :
785
+
786
+ | Sonde | Ce qu'elle renvoie |
787
+ | ---------------------- | --------------------------------------------------------------------------- |
788
+ | `ping()` | un aller-retour réel vers la base (`MongooseOrm.ts:515`) |
789
+ | `probe()` | les connexions du serveur et sa version (`MongooseOrm.ts:529`) |
790
+ | `describeEntity()` | les champs d'une entité, depuis le schéma compilé (`MongooseOrm.ts:558`) |
791
+ | `describeConnection()` | le pilote, la cible et la version de la bibliothèque (`MongooseOrm.ts:583`) |
792
+
793
+ > [!IMPORTANT]
794
+ > **Aucun identifiant ne sort jamais.** La cible affichée est nettoyée de tout `utilisateur:mot de
795
+ passe@` avant d'atteindre le plan d'administration ou les journaux
796
+ > (`MongooseOrm.safeTarget()` (`MongooseOrm.ts:432`)), y compris pour les URI multi-hôtes que
797
+ > l'analyseur d'URL standard ne sait pas découper.
798
+
799
+ ## ⚡ Performance et mémoire
800
+
801
+ Le module suit la règle de fond du framework : **ce qui n'est pas observé ne coûte rien**.
802
+
803
+ - **Instrumentation à coût nul hors observation.** Chaque opération peut alimenter deux sondes (le
804
+ profil par requête de la barre de debug, et le flux agrégé). Les deux drapeaux sont lus **avant**
805
+ toute allocation, et la description de la requête n'est construite que si l'on regarde
806
+ (`MongooseRepository.#prof()` (`MongooseRepository.ts:79`)). En production, le chemin est celui d'un
807
+ appel direct.
808
+ - **Repositories alloués à la demande.** Le cache est créé au premier accès, pas à la connexion
809
+ (`MongooseOrm.getRepository()` (`MongooseOrm.ts:465`)).
810
+ - **Un aller-retour par écriture.** Les opérations « lire puis écrire » sont exprimées en une seule
811
+ requête atomique — moins de latence _et_ pas de course.
812
+ - **Le comptage reste côté serveur.** Les listings paginés lisent `limit + 1` documents pour savoir
813
+ s'il y a une suite ; les compteurs passent par `countDocuments`, jamais par la longueur d'un tableau
814
+ rapatrié.
815
+ - **La version de la bibliothèque est résolue une fois** puis mémorisée — le plan d'administration
816
+ peut être interrogé en boucle sans toucher au système de fichiers.
817
+
818
+ ## ⚠️ Pièges (symptôme → cause → correction)
819
+
820
+ | Symptôme | Cause | Correction |
821
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
822
+ | `Transaction numbers are only allowed on a replica set…` | Serveur Mongo isolé : pas de transactions | Démarrer un replica set (même à un nœud) ou utiliser un service managé |
823
+ | `ORM "nodefony" introuvable` au montage d'un store | `@nodefony/security` chargé **avant** `@nodefony/mongoose` | Mettre le driver **avant** dans `modules` (`registerStores.ts:64`) |
824
+ | `no entity model registered under "X"` | Entité déclarée après la connexion (les modèles sont compilés au `connect`) | Déclarer via `@entities` (phase `onRegister`), jamais à `onBoot` |
825
+ | `UnknownCriteriaField` sur un champ pourtant présent en base | Le champ n'est pas dans le **schéma** déclaré | L'ajouter au schéma, ou passer par la connexion native |
826
+ | Un critère renvoie tout au lieu de filtrer | Deux conditions posées sur le **même champ** (le critère est un ET de champs) | Passer par une requête native (`$or`, agrégation) |
827
+ | `many-to-many non portable` | Refus explicite : pas de traduction unique en Mongo | Déclarer la relation via la connexion native |
828
+ | Les sessions disparaissent au redémarrage | `session.store` resté sur `memory` | `session: { store: "mongoose" }`, ou déclarer `NF_DATABASE_URL` et laisser `auto` |
829
+ | `audit store "mongoose" inconnu` — boot avorté en production | Brique **non portée** par Mongo, sélectionnée explicitement | Laisser `auto` (repli annoncé) ou choisir un backend qui la porte |
830
+ | Les comptes ne survivent pas au redémarrage | `provisionUsers` toujours branché sur l'annuaire mémoire | Câbler `MongooseUserRepository.from(orm)` (`MongooseUserRepository.ts:74`) |
831
+ | Un champ écrit se relit `undefined`, sans aucune erreur | Il n'est pas dans le **schéma** : Mongoose est strict et l'écarte en silence | L'ajouter au schéma de l'entité — le type TypeScript seul ne suffit pas |
832
+ | Le premier `npm test` du module met une éternité | Le serveur Mongo de test télécharge son binaire (une seule fois) | Définir `NF_MONGO_TEST_URI` sur un conteneur Mongo |
833
+
834
+ ## 🧪 Tests et couverture
835
+
836
+ Le module est couvert par deux familles, dont les compteurs exacts sont **recomptés à chaque
837
+ génération** (jamais figés dans ce texte) :
838
+
839
+ - **unitaires** — la validation de configuration (schéma Zod, surcharge par environnement) et
840
+ l'assemblage d'URI. Ce sont les seuls tests qui tournent **sans base** ;
841
+ - **intégration, sur un vrai `mongod`** — le contrat `orm-core` (CRUD, relations, transactions), les
842
+ opérations avancées, le store de sessions, les stores de jetons, de passkeys et de webhooks,
843
+ l'adapter utilisateur, et le service de connexion ;
844
+ - **bancs de contrat partagés** — la pagination des utilisateurs, des jetons et des passkeys rejoue
845
+ ici les **mêmes assertions** que le store mémoire et les trois dialectes SQL. C'est la seule preuve
846
+ sérieuse de portabilité : un seul jeu d'assertions, plusieurs backends.
847
+
848
+ Ce qui manque, dit franchement : **pas de test de charge ni de mesure mémoire dédiés** à ce module
849
+ (contrairement à l'adapter SQL), et **pas de test d'attaque** propre au driver — les vecteurs sont
850
+ couverts en amont, dans les modules qui possèdent les contrats.
851
+
852
+ > [!WARNING]
853
+ > **Un « tout vert » ne prouve pas ce qu'on croit ici.** L'immense majorité des cas exige un serveur
854
+ > MongoDB. Sans lui, l'infrastructure de test fournit une URI nulle, chaque suite se met en
855
+ > `describe.skipIf`… **et un test sauté compte comme vert.** La suite passe alors en n'ayant
856
+ > réellement exercé que la configuration. Avant de conclure « ça marche », vérifie que la base était
857
+ > bien là : soit `NF_MONGO_TEST_URI` pointe sur un conteneur (`docker run -p 27017:27017 mongo:7`), soit
858
+ > le serveur en mémoire a démarré. Les bancs de transaction exigent en plus un **replica set**.
859
+ >
860
+ > Le catalogue des variables d'infrastructure du dépôt est `vitest.gates.ts`, à la racine. Ce module
861
+ > n'y déclare pas encore sa porte : ses sauts sont donc **silencieux**, alors que les suites SQL et
862
+ > Redis affichent en fin de course ce qu'elles n'ont pas joué.
863
+
864
+ Couverture : `npm run coverage` dans `@nodefony/mongoose` (rapport lisible aussi dans Studio).
865
+
866
+ ## 🔗 Pour aller plus loin
867
+
868
+ - ⬆️ **Retour** : [Toute la documentation](../../../../../docs/index.md) ·
869
+ [Par où démarrer](../../../../../docs/demarrer.md)
870
+ - 📄 **Page sœur** : [Configuration du module](./configuration.md)
871
+ - 🧭 **Le socle** : [`@nodefony/orm-core`](../../orm-core/docs/index.md) — les contrats portables ·
872
+ [tutoriel : créer une entité](../../orm-core/docs/tutorial-entity.md)
873
+ - 🔄 **L'autre driver** : [`@nodefony/drizzle`](../../drizzle/docs/index.md) — l'adapter SQL de
874
+ référence · [`@nodefony/redis`](../../redis/docs/index.md) — le backend de cache
875
+ - 🔌 **Les modules servis** : [sessions HTTP](../../http/docs/session.md) ·
876
+ [jetons](../../security/docs/tokens.md) · [passkeys](../../security/docs/webauthn.md) ·
877
+ [webhooks](../../security/docs/webhooks.md) · [utilisateurs](../../user/docs/index.md)
878
+ - 🏛️ **Transverse** : [guide de la persistance](../../../../../docs/guides/persistence.md) ·
879
+ [stockage de session](../../../../../docs/guides/session-storage.md) ·
880
+ [ADR-0003 — l'abstraction multi-ORM](../../../../../docs/adr/0003-orm-core-abstraction-repository-multi-orm.md)
881
+ - 📖 [Lexique général](../../../../../docs/lexique.md) du framework