@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.
- package/LICENSE +544 -0
- package/README.md +97 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +90 -0
- package/dist/nodefony/config/config.js +57 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/entity/sessionEntity.js +63 -0
- package/dist/nodefony/entity/tokenEntity.js +184 -0
- package/dist/nodefony/entity/userEntity.js +106 -0
- package/dist/nodefony/entity/webAuthnCredentialEntity.js +97 -0
- package/dist/nodefony/entity/webhookEndpointEntity.js +109 -0
- package/dist/nodefony/interfaces/IMongooseConfig.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/registerStores.js +89 -0
- package/dist/nodefony/service/MongooseService.js +95 -0
- package/dist/nodefony/src/MongooseTokenStore.js +237 -0
- package/dist/nodefony/src/MongooseUserRepository.js +205 -0
- package/dist/nodefony/src/MongooseWebAuthnCredentialStore.js +144 -0
- package/dist/nodefony/src/MongooseWebhookStore.js +181 -0
- package/dist/nodefony/src/SessionStorage.js +241 -0
- package/dist/nodefony/src/mongoOrder.js +49 -0
- package/dist/nodefony/src/orm-core/MongooseOrm.js +440 -0
- package/dist/nodefony/src/orm-core/MongooseRepository.js +300 -0
- package/dist/nodefony/src/orm-core/MongooseTransaction.js +53 -0
- package/dist/nodefony/src/orm-core/index.js +4 -0
- package/dist/types/index.d.ts +74 -0
- package/dist/types/nodefony/config/config.d.ts +21 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +26 -0
- package/dist/types/nodefony/entity/sessionEntity.d.ts +42 -0
- package/dist/types/nodefony/entity/tokenEntity.d.ts +59 -0
- package/dist/types/nodefony/entity/userEntity.d.ts +54 -0
- package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +61 -0
- package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +62 -0
- package/dist/types/nodefony/interfaces/IMongooseConfig.d.ts +17 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/registerStores.d.ts +37 -0
- package/dist/types/nodefony/service/MongooseService.d.ts +48 -0
- package/dist/types/nodefony/src/MongooseTokenStore.d.ts +126 -0
- package/dist/types/nodefony/src/MongooseUserRepository.d.ts +82 -0
- package/dist/types/nodefony/src/MongooseWebAuthnCredentialStore.d.ts +52 -0
- package/dist/types/nodefony/src/MongooseWebhookStore.d.ts +72 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +63 -0
- package/dist/types/nodefony/src/mongoOrder.d.ts +40 -0
- package/dist/types/nodefony/src/orm-core/MongooseOrm.d.ts +132 -0
- package/dist/types/nodefony/src/orm-core/MongooseRepository.d.ts +51 -0
- package/dist/types/nodefony/src/orm-core/MongooseTransaction.d.ts +37 -0
- package/dist/types/nodefony/src/orm-core/index.d.ts +9 -0
- package/docs/configuration.md +776 -0
- package/docs/index.md +881 -0
- 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<T><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
|