@nodefony/mongoose 10.0.0-alpha.8 → 10.0.0-beta.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 (44) hide show
  1. package/README.md +32 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +13 -7
  5. package/dist/nodefony/config/defineModuleConfig.js +4 -3
  6. package/dist/nodefony/entity/auditEventEntity.js +124 -0
  7. package/dist/nodefony/entity/idempotencyEntity.js +96 -0
  8. package/dist/nodefony/entity/sessionEntity.js +1 -1
  9. package/dist/nodefony/entity/totpSecretEntity.js +103 -0
  10. package/dist/nodefony/registerStores.js +42 -5
  11. package/dist/nodefony/service/MongooseService.js +15 -5
  12. package/dist/nodefony/src/MongooseAuditStore.js +215 -0
  13. package/dist/nodefony/src/MongooseIdempotencyStore.js +264 -0
  14. package/dist/nodefony/src/MongooseTokenStore.js +60 -17
  15. package/dist/nodefony/src/MongooseTotpSecretStore.js +151 -0
  16. package/dist/nodefony/src/MongooseUserRepository.js +1 -1
  17. package/dist/nodefony/src/MongooseWebAuthnCredentialStore.js +13 -1
  18. package/dist/nodefony/src/MongooseWebhookStore.js +14 -2
  19. package/dist/nodefony/src/SessionStorage.js +8 -0
  20. package/dist/nodefony/src/orm-core/MongooseOrm.js +68 -22
  21. package/dist/nodefony/src/orm-core/MongooseRepository.js +147 -25
  22. package/dist/nodefony/src/orm-core/MongooseTransaction.js +19 -6
  23. package/dist/nodefony/src/writeCount.js +17 -0
  24. package/dist/types/index.d.ts +9 -0
  25. package/dist/types/nodefony/entity/auditEventEntity.d.ts +76 -0
  26. package/dist/types/nodefony/entity/idempotencyEntity.d.ts +75 -0
  27. package/dist/types/nodefony/entity/totpSecretEntity.d.ts +75 -0
  28. package/dist/types/nodefony/registerStores.d.ts +6 -3
  29. package/dist/types/nodefony/service/MongooseService.d.ts +2 -2
  30. package/dist/types/nodefony/src/MongooseAuditStore.d.ts +70 -0
  31. package/dist/types/nodefony/src/MongooseIdempotencyStore.d.ts +121 -0
  32. package/dist/types/nodefony/src/MongooseTokenStore.d.ts +8 -1
  33. package/dist/types/nodefony/src/MongooseTotpSecretStore.d.ts +63 -0
  34. package/dist/types/nodefony/src/MongooseUserRepository.d.ts +1 -1
  35. package/dist/types/nodefony/src/MongooseWebAuthnCredentialStore.d.ts +6 -0
  36. package/dist/types/nodefony/src/MongooseWebhookStore.d.ts +6 -0
  37. package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
  38. package/dist/types/nodefony/src/orm-core/MongooseOrm.d.ts +10 -3
  39. package/dist/types/nodefony/src/orm-core/MongooseRepository.d.ts +22 -1
  40. package/dist/types/nodefony/src/orm-core/MongooseTransaction.d.ts +12 -4
  41. package/dist/types/nodefony/src/writeCount.d.ts +12 -0
  42. package/docs/configuration.md +27 -27
  43. package/docs/index.md +133 -82
  44. package/package.json +23 -19
package/docs/index.md CHANGED
@@ -192,11 +192,11 @@ des connexions, la compilation des schémas, et présente le tout derrière le c
192
192
 
193
193
  Nodefony ne demande pas à chaque backend de tout savoir faire. **Chaque adapter déclare ce qu'il
194
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
195
+ à chaud (`readAdapterManifest()` (`KernelAdminApi.ts:93`)). Rien n'est curaté dans le cœur : la
196
196
  source de vérité, c'est l'adapter lui-même — ce qui vaut aussi pour un adapter tiers.
197
197
 
198
- Ce que `@nodefony/mongoose` déclare : `session`, `user`, `tokens`, `passkeys`, `webhooks`, avec la
199
- nature `durable`. Comparé aux deux autres adapters officiels :
198
+ Ce que `@nodefony/mongoose` déclare : les huit briques — `session`, `user`, `tokens`, `passkeys`,
199
+ `totp`, `audit`, `webhooks`, `idempotency` — avec la nature `durable`. Comparé aux deux autres adapters officiels :
200
200
 
201
201
  <!-- prettier-ignore -->
202
202
  | Brique | `@nodefony/drizzle` (SQL) | `@nodefony/mongoose` (Mongo) | `@nodefony/redis` (cache) |
@@ -206,24 +206,16 @@ nature `durable`. Comparé aux deux autres adapters officiels :
206
206
  | `tokens` | ✅ | ✅ | ✅ |
207
207
  | `passkeys` | ✅ | ✅ | ✅ |
208
208
  | `webhooks` | ✅ | ✅ | — |
209
- | `totp` | ✅ | — | — |
210
- | `audit` | ✅ | — | — |
211
- | `idempotency` | ✅ | — | ✅ |
209
+ | `totp` | ✅ | ✅ | — |
210
+ | `audit` | ✅ | ✅ | — |
211
+ | `idempotency` | ✅ | ✅ | ✅ |
212
212
  | Nature | durable | durable | cache |
213
213
 
214
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
- > 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.
215
+ > **Une application tourne entièrement sur MongoDB, sans charger `@nodefony/drizzle`.** Les huit
216
+ > briques y ont leur store, et ce n'est pas qu'une déclaration : `npm run test:all -- --mongo`
217
+ > démarre l'application sur MongoDB et rejoue toute la passe d'intégration, et chaque store joue le
218
+ > même banc de contrat que son jumeau SQL — cf [Parité des adaptateurs](../../../../../docs/guides/parite-des-adaptateurs.md).
227
219
  >
228
220
  > La colonne `redis` obéit à une autre logique : elle gagnera `totp` (au régime opt-in de ses jetons
229
221
  > et passkeys, jamais choisi par `auto`), mais pas `user`, `audit` ni `webhooks` — non parce que
@@ -233,7 +225,7 @@ nature `durable`. Comparé aux deux autres adapters officiels :
233
225
  ### Ce qui se passe quand tu ne choisis rien
234
226
 
235
227
  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
228
+ (`resolveAutoStore()` (`infra.ts:297`)) suit l'infrastructure **déclarée**, bornée aux backends
237
229
  réellement chargés :
238
230
 
239
231
  | Ta situation | Ce que `auto` choisit |
@@ -323,6 +315,9 @@ const ArticleEntity = defineEntity({
323
315
  module: "blog",
324
316
  schema: articleSchema,
325
317
  timestamps: true, // Mongoose gère createdAt / updatedAt
318
+ // Index COMPOSITE : un schéma plat n'en exprime que par champ. Sans lui, un
319
+ // tri sur deux champs parcourt la collection puis trie en mémoire.
320
+ indexes: [{ fields: { views: -1, createdAt: -1 } }], // « les plus lus, récents d'abord »
326
321
  });
327
322
 
328
323
  // ── 2. Le controller : le repository se demande au registre ─────────────────
@@ -362,6 +357,23 @@ class Blog extends Module {
362
357
  export default Blog;
363
358
  ```
364
359
 
360
+ ### Le raccourci — `nodefony create entity`
361
+
362
+ Dans une application MongoDB (`create app --database mongodb`), tout ce qui précède se génère,
363
+ avec la même grammaire de champs qu'en SQL :
364
+
365
+ ```bash
366
+ npx nodefony create entity Post title:string(120) body:text? views:int=0 author:ref:User
367
+ ```
368
+
369
+ La commande écrit le schéma Mongoose (`nodefony/entity/Post.ts`), les schémas de validation, un
370
+ service CRUD, un controller REST + WebSocket et les tests, puis câble `@entities([...])` et
371
+ `@controllers([...])`. L'entité se pose sur le connecteur `nodefony` ; une référence est un
372
+ `ObjectId` indexé, que le schéma d'entrée exige bien formé (24 caractères hexadécimaux, sinon 422).
373
+ Les options propres au SQL (`--table`, `--column-case`, `--id-name`, `--dialect`, `--id`,
374
+ `--index`, `--unique`) sont refusées en le disant ; l'entité `User` ne se régénère pas, elle
375
+ s'étend dans `nodefony/entity/User.ts`.
376
+
365
377
  ### 3. Ce qu'on observe
366
378
 
367
379
  ```bash
@@ -387,7 +399,7 @@ INFO mongoose Mongoose ORM "nodefony" connected (127.0.0.1:27017/blog)
387
399
 
388
400
  Une fois le driver chargé, les stores Mongo deviennent **sélectionnables par leur nom**, sans aucun
389
401
  câblage : le module les enregistre lui-même à son démarrage
390
- (`registerMongooseFrameworkStores()` (`registerStores.ts:94`)).
402
+ (`registerMongooseFrameworkStores()` (`registerStores.ts:126`)).
391
403
 
392
404
  ```typescript
393
405
  // nodefony.config.ts — sessions, jetons, passkeys et webhooks dans Mongo
@@ -472,7 +484,7 @@ sequenceDiagram
472
484
 
473
485
  L'ordre n'est pas cosmétique. Les entités sont déclarées à `onKernelRegister`, **strictement avant**
474
486
  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
487
+ (`MongooseOrm.onConnect()` (`MongooseOrm.ts:137`)), donc une entité déclarée trop tard n'existe tout
476
488
  simplement pas. C'est aussi pour ça que `@entities` s'exécute à la phase `onRegister` et non `onBoot`.
477
489
 
478
490
  Chaque connecteur ouvre une **connexion isolée** (`mongoose.createConnection`), pas le singleton
@@ -480,8 +492,8 @@ global de Mongoose : c'est ce qui permet à plusieurs bases — voire plusieurs
480
492
  le même processus.
481
493
 
482
494
  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:178`)). Le module se déclare **non critique**
495
+ démarrage (`MongooseService.connectAll()` (`MongooseService.ts:104`)) et referme tout à l'arrêt
496
+ (`MongooseService.disconnectAll()` (`MongooseService.ts:210`)). Le module se déclare **non critique**
485
497
  (`Mongoose.critical` (`mongoose/index.ts:48`)) : une base injoignable ne tue pas le processus —
486
498
  l'application monte quand même, l'échec est journalisé, et c'est l'orchestrateur qui relèvera Mongo.
487
499
 
@@ -490,15 +502,15 @@ l'application monte quand même, l'échec est journalisé, et c'est l'orchestrat
490
502
  > indexées par `(connecteur, nom)` dans un registre **global au processus**. Si Drizzle (dont le
491
503
  > connecteur par défaut est `default`) et Mongoose tournaient ensemble avec le même nom, leurs deux
492
504
  > entités `session` entreraient en collision. Un nom distinct par driver règle le problème par
493
- > construction (`FRAMEWORK_CONNECTOR` (`registerStores.ts:49`)).
505
+ > construction (`FRAMEWORK_CONNECTOR` (`registerStores.ts:77`)).
494
506
 
495
507
  ### Le trajet d'une requête
496
508
 
497
509
  `repo.find({ views: { $gte: 10 } }, { limit: 20 })` traverse quatre étapes :
498
510
 
499
- 1. **Traduction du critère** (`MongooseRepository.#filter()` (`MongooseRepository.ts:184`)) : chaque
511
+ 1. **Traduction du critère** (`MongooseRepository.#filter()` (`MongooseRepository.ts:256`)) : chaque
500
512
  champ est résolu (`id` devient `_id`), chaque opérateur portable est converti.
501
- 2. **Validation du champ** (`MongooseRepository.#resolveField()` (`MongooseRepository.ts:162`)) : un
513
+ 2. **Validation du champ** (`MongooseRepository.#resolveField()` (`MongooseRepository.ts:222`)) : un
502
514
  champ absent du schéma lève `UnknownCriteriaField` — plutôt que de renvoyer zéro résultat sans
503
515
  rien dire, ce qui est la pire façon d'échouer.
504
516
  3. **Exécution** : la requête Mongoose est construite (session transactionnelle, `populate`, `skip`,
@@ -506,7 +518,7 @@ l'application monte quand même, l'échec est journalisé, et c'est l'orchestrat
506
518
  4. **Sérialisation** : chaque document sort en objet plat, **virtuels compris** — c'est là que `id`
507
519
  apparaît.
508
520
 
509
- Une sonde facultative encadre l'opération (`MongooseRepository.#prof()` (`MongooseRepository.ts:79`)) :
521
+ Une sonde facultative encadre l'opération (`MongooseRepository.#prof()` (`MongooseRepository.ts:118`)) :
510
522
  voir [Performance et mémoire](#-performance-et-mémoire).
511
523
 
512
524
  ## 🧰 Le repository portable — l'API que tu utilises vraiment
@@ -527,26 +539,28 @@ drivers. Les signatures exactes vivent dans le graphe généré
527
539
  | `delete` / `deleteOne` | supprimer | `deleteMany` / `deleteOne` |
528
540
  | `findOneAndDelete` | supprimer **et** récupérer le document | `findOneAndDelete` |
529
541
  | `count` / `exists` | compter / tester l'existence | `countDocuments` / `exists` |
542
+ | `countDistinct` | compter les valeurs distinctes et non nulles d'un champ | agrégation `$match` → `$group` → `$count` |
530
543
  | `withTransaction` | rejouer les mêmes opérations dans une transaction | ajoute la `session` à chaque opération |
531
544
 
532
545
  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
546
+ (`MongooseRepository.upsert()` (`MongooseRepository.ts:422`),
547
+ `MongooseRepository.increment()` (`MongooseRepository.ts:507`)) : un seul aller-retour, la
535
548
  comparaison est faite par le serveur. Ce n'est pas une optimisation cosmétique — c'est ce qui évite
536
549
  que deux requêtes simultanées lisent le même état et s'écrasent mutuellement.
537
550
 
538
551
  ### Les critères et leurs opérateurs
539
552
 
540
553
  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`)) :
554
+ natifs en Mongo. Deux méritent une explication (`MongooseRepository.#mongoOps()` (`MongooseRepository.ts:173`)) :
542
555
 
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 |
556
+ | Opérateur portable | Côté Mongo | Remarque |
557
+ | -------------------------- | ------------------------- | ------------------------------------------------------------------- |
558
+ | `$eq $gt $gte $lt $lte` | identiques | natifs |
559
+ | `$ne` / `$nin` | `$nin` + `null` | comme en SQL, jamais un champ absent (un `$nin` vide reste neutre) |
560
+ | `$in` | identique | natif |
561
+ | `$like: "ab%"` | `$regex` **ancrée** | motif SQL traduit (`sqlLikeToRegex()` (`MongooseRepository.ts:25`)) |
562
+ | `$null: true` / `false` | `$eq: null` / `$ne: null` | en Mongo, `null` couvre aussi le champ **absent** |
563
+ | `$max` / `$min` (écriture) | `$max` / `$min` natifs | l'équivalent du `GREATEST(col, ?)` SQL |
550
564
 
551
565
  ```typescript
552
566
  await articles.find({ tags: "nodefony" }); // tableau : appartenance native
@@ -575,6 +589,36 @@ Le chargement se demande à la lecture : `find(criteria, { relations: ["comments
575
589
  `populate`. Le refus du `many-to-many` est volontaire : il n'a pas de traduction unique en Mongo
576
590
  (tableau de références ? collection de liaison ?), et un choix imposé serait un mauvais choix.
577
591
 
592
+ **L'adapter tient la politique d'effacement du SQL généré.** Au `connect()`, il relève tout
593
+ champ `ObjectId` qui porte un `ref` vers une entité de la même connexion, et chaque suppression
594
+ d'un parent (`delete`, `deleteOne`, `findOneAndDelete`) la consulte :
595
+
596
+ | La référence est… | Supprimer le parent… |
597
+ | ------------------------------ | -------------------------------------------------------------------------- |
598
+ | obligatoire (`required: true`) | est **refusé** : `ReferencedEntityError`, que le rendu HTTP traduit en 409 |
599
+ | facultative | passe, et la référence de l'enfant est **remise à `null`** |
600
+
601
+ Une entité que personne ne référence ne paie rien. Une **auto-référence** (`Category.parent`) ne
602
+ retient pas un parent dont l'enfant part dans le même appel : l'arbre entier se supprime d'un coup.
603
+ Une référence **sans index** est signalée au `connect()` (`WARNING`, champ nommé) : sans lui,
604
+ chaque suppression du parent parcourt toute la collection de l'enfant. Quatre limites, à connaître :
605
+
606
+ - **Hors transaction, contrôle et suppression sont deux opérations** : un enfant créé entre les
607
+ deux n'est pas vu. Le SQL, lui, est atomique ; dans `withTransaction`, les deux partagent la
608
+ session.
609
+ - **Un tableau de références** (`[{ type: ObjectId, ref }]`) n'est pas gardé : retirer l'élément
610
+ n'a pas d'équivalent dans `restrict` / `set null`.
611
+ - **La suppression douce** (`deletedAt`) est une mise à jour : elle ne déclenche rien — en SQL
612
+ non plus.
613
+ - **La connexion native** (`getNativeConnection()`) contourne la garde, comme toute requête brute.
614
+ Et une suppression en masse charge tous les `_id` visés : c'est ce qui permet à un seul parent
615
+ retenu de bloquer le lot entier.
616
+
617
+ > [!WARNING]
618
+ > L'**insertion** n'est pas gardée : un enfant qui désigne un parent inexistant est accepté, et
619
+ > `populate` rend alors `null` à sa place. Le contrat d'entrée vérifie la forme de l'identifiant,
620
+ > pas l'existence du parent.
621
+
578
622
  ### Transactions
579
623
 
580
624
  ```typescript
@@ -586,22 +630,23 @@ await orm.transaction(async (tx) => {
586
630
  }); // commit si la fonction résout, annulation si elle échoue
587
631
  ```
588
632
 
589
- `MongooseOrm.transaction()` (`MongooseOrm.ts:483`) s'appuie sur les sessions Mongo « managées »
633
+ `MongooseOrm.transaction()` (`MongooseOrm.ts:571`) s'appuie sur les sessions Mongo « managées »
590
634
  (commit, annulation et **reprises** gérées par le driver).
591
635
 
592
636
  > [!IMPORTANT]
593
637
  > **Les transactions exigent un replica set.** Un serveur MongoDB isolé (`mongod` seul, l'installation
594
638
  > par défaut) ne les supporte pas. En développement, démarre un replica set à un nœud ; en production,
595
639
  > 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.
640
+ > intermédiaires n'existent pas en Mongo : `savepoint()` et `rollbackTo()` lèvent
641
+ > `SavepointNotSupportedError` — pour annuler, c'est la transaction entière qu'on abandonne.
597
642
 
598
643
  ### La trappe native — quand le contrat ne suffit plus
599
644
 
600
- `MongooseOrm.getNativeConnection()` (`MongooseOrm.ts:501`) rend la connexion Mongoose telle quelle :
645
+ `MongooseOrm.getNativeConnection()` (`MongooseOrm.ts:589`) rend la connexion Mongoose telle quelle :
601
646
  agrégations, `$or`, index, `$text`, `bulkWrite`, changements de flux. Le module lui-même s'en sert
602
647
  là où le contrat portable ne suffit pas — par exemple pour la recherche texte du listing des
603
648
  webhooks, qui a besoin d'un `$or` sur deux champs
604
- (`MongooseWebhookStore.#listFilter()` (`MongooseWebhookStore.ts:167`)).
649
+ (`MongooseWebhookStore.#listFilter()` (`MongooseWebhookStore.ts:195`)).
605
650
 
606
651
  C'est un **anti-blocage assumé** : le contrat portable couvre le quotidien, la trappe couvre le reste.
607
652
  Le code qui l'emprunte cesse d'être portable, et ça se voit — ce qui est exactement le but.
@@ -613,13 +658,16 @@ contrat est importé en `import type` (effacé à la compilation), et c'est le d
613
658
  auprès du module consommateur. Le module met en place tout ce câblage à son enregistrement, avant que
614
659
  la connexion ne s'ouvre.
615
660
 
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` |
661
+ | Store | Contrat | Comment le choisir | Collections |
662
+ | ------------ | ----------------------------- | ------------------------------------------------ | -------------------------------------------------- |
663
+ | Sessions | `ISessionStorage` (http) | `session: { store: "mongoose" }` | `session` |
664
+ | Utilisateurs | `IUserRepository` (user) | via `provisionUsers` de ton app | `User` |
665
+ | Jetons | `ITokenStore` (security) | `tokenStore: { store: "mongoose" }` | `access_token`, `denied_jti`, `subject_revocation` |
666
+ | Passkeys | `IWebAuthnCredentialStore` | `passkeys: { store: "mongoose" }` | `webauthn_credential` |
667
+ | Webhooks | `IWebhookStore` | `webhooks: { store: "mongoose" }` | `webhook_endpoint` |
668
+ | 2FA (TOTP) | `ITotpSecretStore` (security) | `totp: { store: "mongoose" }` | `totp_secret` |
669
+ | Audit | `IAuditStore` (security) | `audit: { store: "mongoose" }` | `audit_event` |
670
+ | Idempotence | `IIdempotencyStore` (cœur) | `idempotency: { store: "mongoose" }` (framework) | `idempotency_key` |
623
671
 
624
672
  L'auto-enregistrement peut être coupé (`frameworkEntities: false` (`config.ts:102`)) : le module
625
673
  devient alors un pur driver de données, sans schéma framework.
@@ -632,18 +680,18 @@ devient alors un pur driver de données, sans schéma framework.
632
680
 
633
681
  Trois comportements valent d'être connus :
634
682
 
635
- - **Purge à deux bornes** — `idleTimeoutS` et `absoluteTimeoutS` (`SessionStorage.gc()` (`SessionStorage.ts:156`)) :
683
+ - **Purge à deux bornes** — `idleTimeoutS` et `absoluteTimeoutS` (`SessionStorage.gc()` (`SessionStorage.ts:163`)) :
636
684
  l'inactivité (depuis la dernière activité) et l'âge absolu (depuis la création, **jamais prolongé** —
637
685
  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
686
+ - **Prolongation sans réécriture** (`SessionStorage.touch()` (`SessionStorage.ts:195`)) : rafraîchir
639
687
  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`)) :
688
+ - **Écran d'administration redacté par construction** (`SessionStorage.listPage()` (`SessionStorage.ts:286`)) :
641
689
  le contenu applicatif et les messages flash **ne sortent pas de la base**. Studio affiche qui est
642
690
  connecté, jamais ce qu'il y a dans sa session.
643
691
 
644
692
  Quand l'ORM n'est plus connecté — typiquement pendant l'arrêt du serveur, alors que des requêtes sont
645
693
  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
694
+ (`SessionStorage.#repo()` (`SessionStorage.ts:66`)). Une session non persistée le temps de l'arrêt
647
695
  vaut mieux qu'une erreur 500 et un rejet non capturé.
648
696
 
649
697
  ### Utilisateurs
@@ -651,15 +699,15 @@ vaut mieux qu'une erreur 500 et un rejet non capturé.
651
699
  `MongooseUserRepository` rend des objets `BaseUser` (avec leur comportement : rôles, actif, verrouillé),
652
700
  pas des documents nus. Deux recherches lui sont propres :
653
701
 
654
- - **par compte social lié** (`MongooseUserRepository.findBySocialProvider()` (`MongooseUserRepository.ts:256`)) :
702
+ - **par compte social lié** (`MongooseUserRepository.findBySocialProvider()` (`MongooseUserRepository.ts:238`)) :
655
703
  un `$elemMatch` sur un tableau libre de fournisseurs — le pendant Mongo du parcours JSON en SQL.
656
704
  C'est ce qui porte le motif « Shadow User » d'OAuth (un compte créé à la volée au premier login social),
657
705
  **sans colonne par fournisseur** : ajouter GitHub demain n'est pas une migration.
658
- - **listing paginé** (`MongooseUserRepository.listPage()` (`MongooseUserRepository.ts:299`)) : requête
706
+ - **listing paginé** (`MongooseUserRepository.listPage()` (`MongooseUserRepository.ts:281`)) : requête
659
707
  native bornée (`skip`/`limit + 1`), tri sur liste blanche, `_id` en départage. Une page est une page,
660
708
  jamais la collection entière rapatriée en mémoire.
661
709
 
662
- Le comptage des administrateurs actifs (`MongooseUserRepository.countActiveAdmins()` (`MongooseUserRepository.ts:354`))
710
+ Le comptage des administrateurs actifs (`MongooseUserRepository.countActiveAdmins()` (`MongooseUserRepository.ts:328`))
663
711
  compte côté serveur — c'est le garde-fou qui empêche de supprimer le dernier administrateur.
664
712
 
665
713
  ### Jetons
@@ -667,25 +715,25 @@ compte côté serveur — c'est le garde-fou qui empêche de supprimer le dernie
667
715
  Trois collections : les jetons eux-mêmes, la denylist de `jti`, les seuils de révocation par porteur.
668
716
  Deux invariants de sécurité sont tenus **par la requête**, pas par du code JavaScript entre deux appels :
669
717
 
670
- - **Révocation idempotente** (`MongooseTokenStore.revoke()` (`MongooseTokenStore.ts:215`)) : la
718
+ - **Révocation idempotente** (`MongooseTokenStore.revoke()` (`MongooseTokenStore.ts:275`)) : la
671
719
  condition « pas encore révoqué » est dans le filtre. Deux révocations simultanées ne se recouvrent
672
720
  pas ; la première date et la première raison sont conservées.
673
- - **Seuil monotone** (`MongooseTokenStore.revokeAllForSubject()` (`MongooseTokenStore.ts:297`)) : le
721
+ - **Seuil monotone** (`MongooseTokenStore.revokeAllForSubject()` (`MongooseTokenStore.ts:334`)) : le
674
722
  « déconnecte-moi de partout » utilise `$max`. Avec une lecture suivie d'une écriture, deux
675
723
  déconnexions simultanées pourraient reposer un seuil **plus ancien** — et des jetons révoqués
676
724
  redeviendraient valides. Ici, c'est structurellement impossible.
677
725
 
678
- La purge, bornée par `retentionRevokedMs` (`MongooseTokenStore.gc()` (`MongooseTokenStore.ts:313`)), s'appuie sur une particularité utile
726
+ La purge, bornée par `retentionRevokedMs` (`MongooseTokenStore.gc()` (`MongooseTokenStore.ts:356`)), s'appuie sur une particularité utile
679
727
  de Mongo : une comparaison numérique **ignore** les documents dont le champ est `null`. Les jetons sans
680
728
  expiration ne sont donc jamais balayés par erreur ; ils partent par une règle de rétention distincte.
681
729
 
682
730
  ### Passkeys
683
731
 
684
732
  Une collection, l'identifiant du credential en clé primaire. L'enregistrement passe par un `upsert`
685
- atomique (`MongooseWebAuthnCredentialStore.save()` (`MongooseWebAuthnCredentialStore.ts:94`)) : deux
733
+ atomique (`MongooseWebAuthnCredentialStore.save()` (`MongooseWebAuthnCredentialStore.ts:109`)) : deux
686
734
  enregistrements concurrents de la même passkey ne peuvent pas produire de collision de clé.
687
735
 
688
- Le listing d'administration (`MongooseWebAuthnCredentialStore.listPage()` (`MongooseWebAuthnCredentialStore.ts:158`))
736
+ Le listing d'administration (`MongooseWebAuthnCredentialStore.listPage()` (`MongooseWebAuthnCredentialStore.ts:173`))
689
737
  projette **sans la clé publique** — elle ne franchit jamais la frontière du store. La recherche libre
690
738
  est un **préfixe ancré**, pas une expression régulière fournie par l'appelant : une recherche
691
739
  utilisateur n'est jamais interprétée comme du code.
@@ -693,18 +741,18 @@ utilisateur n'est jamais interprétée comme du code.
693
741
  ### Webhooks
694
742
 
695
743
  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`))
744
+ redémarrage. Le listing paginé (`MongooseWebhookStore.listPage()` (`MongooseWebhookStore.ts:227`))
697
745
  lit `limit + 1` documents pour savoir s'il existe une page suivante — sans second comptage — et
698
746
  échappe les métacaractères de la recherche texte.
699
747
 
700
748
  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`))
749
+ listing paginé **refuse** de répondre (`MongooseWebhookStore.#nativeModel()` (`MongooseWebhookStore.ts:179`))
702
750
  au lieu de retomber sur un chargement complet de la collection. Une garantie silencieusement trahie
703
751
  serait pire qu'une erreur.
704
752
 
705
753
  ## 🗃 Ce qui est stocké
706
754
 
707
- Les cinq schémas portés par le module. Les collections sont créées à la volée par MongoDB — il n'y a
755
+ Les schémas portés par le module — dix collections. Les collections sont créées à la volée par MongoDB — il n'y a
708
756
  ni migration ni DDL à jouer, ce qui est l'un des vrais conforts du modèle documentaire.
709
757
 
710
758
  > ⚠️ **Le revers de ce confort : ce qui n'est pas déclaré est JETÉ, sans un mot.** Mongoose valide en
@@ -719,15 +767,18 @@ ni migration ni DDL à jouer, ce qui est l'un des vrais conforts du modèle docu
719
767
  > partagé (`USER_COLUMNS`), et le module en dérive un schéma Mongoose. Un champ métier que tu ajoutes
720
768
  > à ton utilisateur suit le même chemin — déclaré, donc écrit ; oublié, donc perdu en silence.
721
769
 
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) |
770
+ | Collection | Clé primaire (`_id`) | Contenu | Horodatages |
771
+ | --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------ | ------------------ |
772
+ | `session` | `ObjectId` (auto) | identifiant de session, contenu, messages flash, méta, utilisateur | nombres (ms) |
773
+ | `User` | `ObjectId` (auto) | identifiant, mot de passe haché, rôles, comptes sociaux, méta | gérés par Mongoose |
774
+ | `access_token` | le `jti` (texte) | type, porteur, périmètres, empreinte du secret, révocation | nombres (ms) |
775
+ | `denied_jti` | le `jti` (texte) | expiration | nombres (ms) |
776
+ | `subject_revocation` | le porteur (texte) | seuil `invalidBefore` | nombres (ms) |
777
+ | `webauthn_credential` | l'identifiant du credential | clé publique, compteur, transports, état de sauvegarde | nombres (ms) |
778
+ | `webhook_endpoint` | l'identifiant `wh_…` (texte) | URL, secret chiffré, événements, état des livraisons | nombres (ms) |
779
+ | `totp_secret` | l'identifiant de l'utilisateur (texte) | secret chiffré, algorithme, chiffres, période, codes de secours, dernier pas utilisé | nombres (ms) |
780
+ | `audit_event` | l'identifiant d'événement (texte) | catégorie, action, issue, acteur, ressource, raison, IP, requête | nombre (`ts`, ms) |
781
+ | `idempotency_key` | la clé d'idempotence (texte) | empreinte de la requête, état, réponse rejouée | expiration (ms) |
731
782
 
732
783
  Deux choix structurants s'y lisent :
733
784
 
@@ -740,7 +791,7 @@ natif.
740
791
  **Les horodatages sont des nombres, pas des dates.** Les contrats du framework portent des `number`
741
792
  (millisecondes depuis l'époque) : les stocker tels quels garde la logique de purge **strictement
742
793
  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.
794
+ (`createUserEntity()` (`userEntity.ts:104`)), parce que son contrat porte des dates.
744
795
 
745
796
  Le contrat expose partout `id: string`, jamais un `ObjectId` : le champ virtuel `id` est activé à la
746
797
  sérialisation, sur toutes les entités compilées par l'adapter.
@@ -757,7 +808,7 @@ heures plus tard.
757
808
  | `debug` | trace toutes les opérations Mongoose (développement) | `false` |
758
809
  | `frameworkEntities` | déclare le schéma framework et rend ses stores sélectionnables | `true` |
759
810
 
760
- Les valeurs font foi dans le schéma (`mongooseConfigSchema` (`config.ts:83`)) ; la surcharge par
811
+ Les valeurs font foi dans le schéma (`mongooseConfigSchema` (`config.ts:98`)) ; la surcharge par
761
812
  l'environnement est appliquée **après** la validation
762
813
  (`applyEnvOverrides()` (`defineModuleConfig.ts:22`)), ce qui garde le schéma pur et publiable en
763
814
  JSON Schema pour Studio.
@@ -785,15 +836,15 @@ Schema). Le module fournit les sondes correspondantes :
785
836
 
786
837
  | Sonde | Ce qu'elle renvoie |
787
838
  | ---------------------- | --------------------------------------------------------------------------- |
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`) |
839
+ | `ping()` | un aller-retour réel vers la base (`MongooseOrm.ts:603`) |
840
+ | `probe()` | les connexions du serveur et sa version (`MongooseOrm.ts:617`) |
841
+ | `describeEntity()` | les champs d'une entité, depuis le schéma compilé (`MongooseOrm.ts:649`) |
842
+ | `describeConnection()` | le pilote, la cible et la version de la bibliothèque (`MongooseOrm.ts:676`) |
792
843
 
793
844
  > [!IMPORTANT]
794
845
  > **Aucun identifiant ne sort jamais.** La cible affichée est nettoyée de tout `utilisateur:mot de
795
846
  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
847
+ > (`MongooseOrm.safeTarget()` (`MongooseOrm.ts:688`)), y compris pour les URI multi-hôtes que
797
848
  > l'analyseur d'URL standard ne sait pas découper.
798
849
 
799
850
  ## ⚡ Performance et mémoire
@@ -803,10 +854,10 @@ Le module suit la règle de fond du framework : **ce qui n'est pas observé ne c
803
854
  - **Instrumentation à coût nul hors observation.** Chaque opération peut alimenter deux sondes (le
804
855
  profil par requête de la barre de debug, et le flux agrégé). Les deux drapeaux sont lus **avant**
805
856
  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
857
+ (`MongooseRepository.#prof()` (`MongooseRepository.ts:118`)). En production, le chemin est celui d'un
807
858
  appel direct.
808
859
  - **Repositories alloués à la demande.** Le cache est créé au premier accès, pas à la connexion
809
- (`MongooseOrm.getRepository()` (`MongooseOrm.ts:465`)).
860
+ (`MongooseOrm.getRepository()` (`MongooseOrm.ts:548`)).
810
861
  - **Un aller-retour par écriture.** Les opérations « lire puis écrire » sont exprimées en une seule
811
862
  requête atomique — moins de latence _et_ pas de course.
812
863
  - **Le comptage reste côté serveur.** Les listings paginés lisent `limit + 1` documents pour savoir
@@ -827,7 +878,7 @@ Le module suit la règle de fond du framework : **ce qui n'est pas observé ne c
827
878
  | `many-to-many non portable` | Refus explicite : pas de traduction unique en Mongo | Déclarer la relation via la connexion native |
828
879
  | Les sessions disparaissent au redémarrage | `session.store` resté sur `memory` | `session: { store: "mongoose" }`, ou déclarer `NF_DATABASE_URL` et laisser `auto` |
829
880
  | `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`) |
881
+ | Les comptes ne survivent pas au redémarrage | `provisionUsers` toujours branché sur l'annuaire mémoire | Câbler `MongooseUserRepository.from(orm)` (`MongooseUserRepository.ts:89`) |
831
882
  | 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
883
  | 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
884
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/mongoose",
3
- "version": "10.0.0-alpha.8",
3
+ "version": "10.0.0-beta.1",
4
4
  "description": "Moteur MongoDB de Nodefony sur Mongoose : dépôts typés conformes au contrat orm-core",
5
5
  "nodefony": {
6
6
  "storeKind": "durable",
@@ -9,7 +9,10 @@
9
9
  "user",
10
10
  "tokens",
11
11
  "passkeys",
12
- "webhooks"
12
+ "totp",
13
+ "audit",
14
+ "webhooks",
15
+ "idempotency"
13
16
  ]
14
17
  },
15
18
  "contributors": [],
@@ -25,9 +28,9 @@
25
28
  },
26
29
  "scripts": {
27
30
  "start": "node dist/index.js",
28
- "build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
31
+ "build": "rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
29
32
  "dev": "rolldown -c rolldown.config.ts --watch",
30
- "clean": "rimraf dist",
33
+ "clean": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
31
34
  "test": "vitest run",
32
35
  "coverage": "vitest run --coverage",
33
36
  "typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json"
@@ -47,27 +50,28 @@
47
50
  ],
48
51
  "dependencies": {
49
52
  "mongodb": "7.6.0",
50
- "mongoose": "9.10.1",
53
+ "mongoose": "9.10.2",
51
54
  "tslib": "2.8.1",
52
55
  "zod": "^4.6.5"
53
56
  },
54
57
  "devDependencies": {
55
- "@nodefony/http": "^10.0.0-alpha.8",
56
- "@nodefony/orm-core": "^10.0.0-alpha.8",
57
- "@nodefony/security": "^10.0.0-alpha.8",
58
- "@nodefony/user": "^10.0.0-alpha.8",
59
- "@types/node": "26.6.2",
60
- "mongodb-memory-server": "11.2.0",
61
- "nodefony": "^10.0.0-alpha.8",
62
- "rimraf": "6.1.3",
63
- "vitest": "5.0.1"
58
+ "@nodefony/framework": "^10.0.0-beta.1",
59
+ "@nodefony/http": "^10.0.0-beta.1",
60
+ "@nodefony/orm-core": "^10.0.0-beta.1",
61
+ "@nodefony/security": "^10.0.0-beta.1",
62
+ "@nodefony/user": "^10.0.0-beta.1",
63
+ "@types/node": "26.6.3",
64
+ "mongodb-memory-server": "11.3.0",
65
+ "nodefony": "^10.0.0-beta.1",
66
+ "vitest": "5.0.2"
64
67
  },
65
68
  "peerDependencies": {
66
- "@nodefony/http": "^10.0.0-alpha.8",
67
- "@nodefony/orm-core": "^10.0.0-alpha.8",
68
- "@nodefony/security": "^10.0.0-alpha.8",
69
- "@nodefony/user": "^10.0.0-alpha.8",
70
- "nodefony": "^10.0.0-alpha.8"
69
+ "@nodefony/framework": "^10.0.0-beta.1",
70
+ "@nodefony/http": "^10.0.0-beta.1",
71
+ "@nodefony/orm-core": "^10.0.0-beta.1",
72
+ "@nodefony/security": "^10.0.0-beta.1",
73
+ "@nodefony/user": "^10.0.0-beta.1",
74
+ "nodefony": "^10.0.0-beta.1"
71
75
  },
72
76
  "repository": {
73
77
  "type": "git",