@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
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { MongooseRepository } from "./MongooseRepository.js";
|
|
2
|
+
import { MongooseTransaction } from "./MongooseTransaction.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import mongoose from "mongoose";
|
|
5
|
+
import { Orm, entityRegistry } from "@nodefony/orm-core";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
//#region nodefony/src/orm-core/MongooseOrm.ts
|
|
9
|
+
/**
|
|
10
|
+
* Nom canonique d'un index à partir de sa définition (`{ identifier: 1 }` →
|
|
11
|
+
* `identifier_1`) — la convention de nommage de MongoDB lui-même, pour que le
|
|
12
|
+
* journal désigne l'index sous le nom qu'un exploitant lira dans la base.
|
|
13
|
+
*/
|
|
14
|
+
function indexName(definition) {
|
|
15
|
+
if (!definition || typeof definition !== "object") return String(definition);
|
|
16
|
+
return Object.entries(definition).map(([field, direction]) => `${field}_${String(direction)}`).join("_");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Adapter Mongoose **branché sur `@nodefony/orm-core`** (P5.4).
|
|
20
|
+
*
|
|
21
|
+
* 2ᵉ adapter, **hétérogène** au SQL : valide que le contrat enrichi
|
|
22
|
+
* (`relations`/`withTransaction`) est réellement portable sur un store
|
|
23
|
+
* documentaire. Distinct du service legacy `nodefony/service/orm.ts`.
|
|
24
|
+
*
|
|
25
|
+
* Spécificités MongoDB exposées par l'implémentation :
|
|
26
|
+
* - **connexion isolée** via `mongoose.createConnection` (pas le singleton global)
|
|
27
|
+
* → indispensable au multi-ORM (plusieurs connexions logiques) ;
|
|
28
|
+
* - relations sans clé étrangère SQL : `one-to-many` = **virtual populate**
|
|
29
|
+
* (réf ObjectId injectée sur l'enfant + virtuel sur le parent), `many-to-one`/
|
|
30
|
+
* `one-to-one` = champ réf sur la source. `many-to-many` → natif ;
|
|
31
|
+
* - transactions = **sessions** (requièrent un replica set).
|
|
32
|
+
*/
|
|
33
|
+
var MongooseOrm = class MongooseOrm extends Orm {
|
|
34
|
+
#connection = null;
|
|
35
|
+
/**
|
|
36
|
+
* Listeners de cycle de vie attachés à la connexion Mongoose — gardés pour
|
|
37
|
+
* pouvoir les DÉTACHER : `disconnect()` puis `connect()` sur le même ORM
|
|
38
|
+
* empilerait sinon un jeu de listeners par cycle (règle « pas de listener
|
|
39
|
+
* sans cleanup »). `null` tant qu'aucune connexion n'est ouverte.
|
|
40
|
+
*/
|
|
41
|
+
#lifecycle = null;
|
|
42
|
+
/**
|
|
43
|
+
* Mongoose traduit les signaux de topologie du driver MongoDB (SDAM) : il
|
|
44
|
+
* SAIT qu'un serveur est tombé, même sans le moindre trafic — d'où
|
|
45
|
+
* `"events"`. C'est la seule des trois familles d'adapters du dépôt qui
|
|
46
|
+
* dispose d'une surveillance de serveur indépendante des requêtes.
|
|
47
|
+
*/
|
|
48
|
+
get liveness() {
|
|
49
|
+
return "events";
|
|
50
|
+
}
|
|
51
|
+
#models = null;
|
|
52
|
+
#repositories = null;
|
|
53
|
+
#uri;
|
|
54
|
+
#options;
|
|
55
|
+
/**
|
|
56
|
+
* @param name - clé unique de l'ORM dans le `ormRegistry`.
|
|
57
|
+
* @param uri - URI de connexion MongoDB (replica set requis pour les tx).
|
|
58
|
+
* @param options - options de connexion Mongoose (auth, pool, timeouts).
|
|
59
|
+
*/
|
|
60
|
+
constructor(name, uri, options) {
|
|
61
|
+
super(name);
|
|
62
|
+
this.#uri = uri;
|
|
63
|
+
this.#options = options;
|
|
64
|
+
}
|
|
65
|
+
/** Entités enregistrées ciblant cet ORM. */
|
|
66
|
+
#ownEntities() {
|
|
67
|
+
return entityRegistry.list().filter((entity) => entity.connector === this.name);
|
|
68
|
+
}
|
|
69
|
+
/** FK déterministe camelCase `<entité>Id` (réf ObjectId côté enfant). */
|
|
70
|
+
#foreignKey(entityName) {
|
|
71
|
+
return `${entityName.charAt(0).toLowerCase()}${entityName.slice(1)}Id`;
|
|
72
|
+
}
|
|
73
|
+
async onConnect() {
|
|
74
|
+
if (this.#connection) {
|
|
75
|
+
this.#unwireLifecycle();
|
|
76
|
+
await this.#connection.close().catch(() => void 0);
|
|
77
|
+
this.#connection = null;
|
|
78
|
+
}
|
|
79
|
+
const connection = mongoose.createConnection(this.#uri, this.#healthyTimeouts());
|
|
80
|
+
await connection.asPromise();
|
|
81
|
+
this.#connection = connection;
|
|
82
|
+
this.#wireLifecycle(connection);
|
|
83
|
+
this.#models = Object.create(null);
|
|
84
|
+
const entities = this.#ownEntities();
|
|
85
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const entity of entities) schemas.set(entity.name, new mongoose.Schema(entity.schema, {
|
|
87
|
+
toObject: { virtuals: true },
|
|
88
|
+
toJSON: { virtuals: true },
|
|
89
|
+
timestamps: entity.timestamps ?? false
|
|
90
|
+
}));
|
|
91
|
+
for (const entity of entities) {
|
|
92
|
+
if (!entity.relations) continue;
|
|
93
|
+
const sourceSchema = schemas.get(entity.name);
|
|
94
|
+
for (const relation of entity.relations) {
|
|
95
|
+
const targetSchema = schemas.get(relation.target);
|
|
96
|
+
if (!sourceSchema || !targetSchema) throw new Error(`MongooseOrm "${this.name}": relation target "${relation.target}" (from "${entity.name}.${relation.field}") not registered for this ORM.`);
|
|
97
|
+
switch (relation.type) {
|
|
98
|
+
case "one-to-many": {
|
|
99
|
+
const fk = relation.foreignKey ?? this.#foreignKey(entity.name);
|
|
100
|
+
if (!targetSchema.path(fk)) targetSchema.add({ [fk]: {
|
|
101
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
102
|
+
ref: entity.name
|
|
103
|
+
} });
|
|
104
|
+
sourceSchema.virtual(relation.field, {
|
|
105
|
+
ref: relation.target,
|
|
106
|
+
localField: "_id",
|
|
107
|
+
foreignField: fk
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "many-to-one":
|
|
112
|
+
case "one-to-one": {
|
|
113
|
+
const fk = relation.foreignKey ?? relation.field;
|
|
114
|
+
if (!sourceSchema.path(fk)) sourceSchema.add({ [fk]: {
|
|
115
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
116
|
+
ref: relation.target
|
|
117
|
+
} });
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "many-to-many": throw new Error(`MongooseOrm "${this.name}": many-to-many ("${entity.name}.${relation.field}") non portable — déclarer via getNativeConnection().`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const entity of entities) {
|
|
125
|
+
const model = connection.model(entity.name, schemas.get(entity.name));
|
|
126
|
+
this.#models[entity.name] = model;
|
|
127
|
+
entity.model = model;
|
|
128
|
+
}
|
|
129
|
+
const audit = this.verifyIndexes();
|
|
130
|
+
this.#indexAudit = audit;
|
|
131
|
+
audit.catch(() => void 0);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Constat d'index en cours — la promesse de la passe lancée au `connect()`.
|
|
135
|
+
*
|
|
136
|
+
* Exposée pour que ce qui doit SAVOIR puisse attendre (un banc, une sonde
|
|
137
|
+
* d'administration, un futur point de disponibilité) sans que le démarrage,
|
|
138
|
+
* lui, ait à le faire.
|
|
139
|
+
*/
|
|
140
|
+
#indexAudit = null;
|
|
141
|
+
/** Constat d'index de la connexion courante, ou `null` hors connexion. */
|
|
142
|
+
get pendingIndexAudit() {
|
|
143
|
+
return this.#indexAudit;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Constate l'écart entre les index DÉCLARÉS par les schémas et ceux que la
|
|
147
|
+
* base porte réellement, et journalise tout manque en `CRITIC`.
|
|
148
|
+
*
|
|
149
|
+
* **Pourquoi c'est nécessaire.** Mongoose construit les index en tâche de
|
|
150
|
+
* fond à la compilation des modèles, et l'issue de cette construction n'était
|
|
151
|
+
* écoutée par personne. Or plusieurs de ces index portent des contraintes
|
|
152
|
+
* d'unicité dont dépend l'authentification. Reproduit sur un serveur réel :
|
|
153
|
+
* une collection portant déjà des doublons fait échouer la construction de
|
|
154
|
+
* l'index unique — et le process continue, code de sortie 0, **sans un seul
|
|
155
|
+
* message**, avec pour seul index `_id_`. La contrainte n'existe pas, et rien
|
|
156
|
+
* ne le dit : c'est la dégradation silencieuse que la doctrine interdit.
|
|
157
|
+
*
|
|
158
|
+
* **Pourquoi APRÈS `init()`.** Un `diffIndexes()` lancé aussitôt après la
|
|
159
|
+
* compilation annonce comme manquants des index dont la construction est
|
|
160
|
+
* simplement en cours — mesuré. `init()` est le point où la construction est
|
|
161
|
+
* terminée, en succès comme en échec ; c'est donc là, et pas avant, que
|
|
162
|
+
* l'écart veut dire quelque chose.
|
|
163
|
+
*
|
|
164
|
+
* **Pourquoi ce n'est pas attendu au démarrage.** Construire un index sur une
|
|
165
|
+
* grosse collection prend des minutes ; faire patienter le pod changerait son
|
|
166
|
+
* comportement bien au-delà de ce défaut. Le constat court donc en tâche de
|
|
167
|
+
* fond et parle dès qu'il sait — {@link MongooseOrm.pendingIndexAudit} permet
|
|
168
|
+
* de l'attendre quand il le faut.
|
|
169
|
+
*
|
|
170
|
+
* **Ce que cette méthode ne fait PAS** : réparer. `syncIndexes()` de mongoose
|
|
171
|
+
* SUPPRIME les index non déclarés — irréversible, et catastrophique sur une
|
|
172
|
+
* base qu'un exploitant a indexée à la main. Réparer reste un geste explicite.
|
|
173
|
+
*
|
|
174
|
+
* @returns un verdict par entité (vide si la connexion est déjà close).
|
|
175
|
+
*/
|
|
176
|
+
async verifyIndexes() {
|
|
177
|
+
const models = this.#models;
|
|
178
|
+
if (!models) return [];
|
|
179
|
+
const audits = [];
|
|
180
|
+
for (const [name, model] of Object.entries(models)) {
|
|
181
|
+
const audit = {
|
|
182
|
+
entity: name,
|
|
183
|
+
collection: model.collection?.name ?? name,
|
|
184
|
+
missing: [],
|
|
185
|
+
extra: []
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
await model.init();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
audit.error = error instanceof Error ? error.message : String(error);
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const diff = await model.diffIndexes();
|
|
194
|
+
audit.missing = diff.toCreate.map(indexName);
|
|
195
|
+
audit.extra = diff.toDrop.map(indexName);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (this.isConnected()) audit.error ??= error instanceof Error ? error.message : String(error);
|
|
198
|
+
else return audits;
|
|
199
|
+
}
|
|
200
|
+
this.#reportIndexAudit(audit);
|
|
201
|
+
audits.push(audit);
|
|
202
|
+
}
|
|
203
|
+
return audits;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Porte un verdict d'index au journal, à la hauteur de ce qu'il signifie.
|
|
207
|
+
*
|
|
208
|
+
* Un index manquant est un `CRITIC` : la contrainte que le code croit tenir
|
|
209
|
+
* n'est pas tenue. Un index en trop n'est qu'une information — il vient
|
|
210
|
+
* souvent d'un exploitant qui savait ce qu'il faisait, et rien ici ne le
|
|
211
|
+
* supprimera.
|
|
212
|
+
*/
|
|
213
|
+
#reportIndexAudit(audit) {
|
|
214
|
+
if (audit.error) this.log(`index de "${audit.entity}" (collection "${audit.collection}") : la base a refusé la construction — ${audit.error}`, "CRITIC");
|
|
215
|
+
if (audit.missing.length > 0) this.log(`index DÉCLARÉS mais ABSENTS de la collection "${audit.collection}" (entité "${audit.entity}") : ${audit.missing.join(", ")} — toute contrainte d'unicité qu'ils portent n'est PAS appliquée`, "CRITIC");
|
|
216
|
+
if (audit.extra.length > 0) this.log(`index présents en base et non déclarés sur "${audit.collection}" : ${audit.extra.join(", ")} — laissés en place`, "INFO");
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Délais d'attente **par défaut**, plus courts que ceux du driver.
|
|
220
|
+
*
|
|
221
|
+
* Le driver MongoDB attend **30 s** pour trouver un serveur utilisable
|
|
222
|
+
* (`serverSelectionTimeoutMS`) et autant pour établir une connexion
|
|
223
|
+
* (`connectTimeoutMS`) — vérifié au source, `connection_string.js`. Mesuré
|
|
224
|
+
* sur une base arrêtée : une requête PEND 30 s avant d'échouer. Pour une
|
|
225
|
+
* requête HTTP c'est absurde : le client a abandonné depuis longtemps, et
|
|
226
|
+
* le worker reste bloqué à attendre une base dont on sait déjà qu'elle ne
|
|
227
|
+
* répond pas.
|
|
228
|
+
*
|
|
229
|
+
* **5 s** est tenable parce que `retryReads`/`retryWrites` valent `true`
|
|
230
|
+
* par défaut : une opération qui échoue en sélection est RETENTÉE, ce qui
|
|
231
|
+
* porte la fenêtre effective à une dizaine de secondes — de quoi absorber
|
|
232
|
+
* l'élection d'un nouveau primaire sans faire attendre le client deux fois
|
|
233
|
+
* plus longtemps que nécessaire.
|
|
234
|
+
*
|
|
235
|
+
* `socketTimeoutMS` n'est délibérément PAS touché (le driver le laisse
|
|
236
|
+
* infini) : le borner ici tuerait les agrégations longues légitimes. Ce
|
|
237
|
+
* rôle revient à `timeoutMS` (CSOT), que seule l'application peut fixer en
|
|
238
|
+
* connaissance de ses opérations.
|
|
239
|
+
*
|
|
240
|
+
* 🔴 **Un choix EXPLICITE gagne toujours** — qu'il vienne des options ou de
|
|
241
|
+
* la chaîne de connexion. Poser un défaut n'autorise pas à écraser une
|
|
242
|
+
* intention : une URI qui porte `?serverSelectionTimeoutMS=20000` dit ce
|
|
243
|
+
* qu'elle veut, et l'objet d'options primerait silencieusement sur elle.
|
|
244
|
+
*/
|
|
245
|
+
#healthyTimeouts() {
|
|
246
|
+
const provided = this.#options ?? {};
|
|
247
|
+
const inUri = (key) => new RegExp(`[?&]${key}=`, "iu").test(this.#uri);
|
|
248
|
+
const defaults = {};
|
|
249
|
+
if (provided.serverSelectionTimeoutMS === void 0 && !inUri("serverSelectionTimeoutMS")) defaults.serverSelectionTimeoutMS = 5e3;
|
|
250
|
+
if (provided.connectTimeoutMS === void 0 && !inUri("connectTimeoutMS")) defaults.connectTimeoutMS = 5e3;
|
|
251
|
+
return {
|
|
252
|
+
...defaults,
|
|
253
|
+
...provided
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Traduit les événements du driver en signaux du contrat `orm-core`.
|
|
258
|
+
*
|
|
259
|
+
* Mongoose SAIT quand le serveur tombe — le setter de `readyState` émet
|
|
260
|
+
* l'état, et le driver câble `serverDescriptionChanged` (topologie simple)
|
|
261
|
+
* ou `topologyDescriptionChanged` (replica set : perte du primaire). Rien
|
|
262
|
+
* n'écoutait, d'où une santé ORM qui affirmait « connecté » pendant toute
|
|
263
|
+
* une coupure. `error` est écouté AUSSI parce qu'une `Connection` est un
|
|
264
|
+
* `EventEmitter` : sans auditeur, une erreur émise ferait tomber le process.
|
|
265
|
+
*/
|
|
266
|
+
#wireLifecycle(connection) {
|
|
267
|
+
const lost = (why) => () => this.connectionLost(why);
|
|
268
|
+
const listeners = [
|
|
269
|
+
["disconnected", lost("mongoose: disconnected")],
|
|
270
|
+
["close", lost("mongoose: close")],
|
|
271
|
+
["error", ((e) => this.connectionLost(`mongoose: ${e?.message ?? String(e)}`))],
|
|
272
|
+
["reconnected", () => this.connectionRestored()],
|
|
273
|
+
["connected", () => this.connectionRestored()]
|
|
274
|
+
];
|
|
275
|
+
for (const [event, handler] of listeners) connection.on(event, handler);
|
|
276
|
+
this.#lifecycle = listeners;
|
|
277
|
+
}
|
|
278
|
+
/** Détache les listeners de cycle de vie (anti-fuite, anti-empilement). */
|
|
279
|
+
#unwireLifecycle() {
|
|
280
|
+
const connection = this.#connection;
|
|
281
|
+
if (connection && this.#lifecycle) for (const [event, handler] of this.#lifecycle) connection.removeListener(event, handler);
|
|
282
|
+
this.#lifecycle = null;
|
|
283
|
+
}
|
|
284
|
+
async disconnect() {
|
|
285
|
+
this.alive = false;
|
|
286
|
+
this.stopHeartbeat();
|
|
287
|
+
this.#unwireLifecycle();
|
|
288
|
+
const connection = this.#connection;
|
|
289
|
+
if (connection) {
|
|
290
|
+
const sink = () => void 0;
|
|
291
|
+
connection.on("error", sink);
|
|
292
|
+
await connection.close();
|
|
293
|
+
}
|
|
294
|
+
this.#connection = null;
|
|
295
|
+
this.#models = null;
|
|
296
|
+
this.#repositories = null;
|
|
297
|
+
this.#indexAudit = null;
|
|
298
|
+
}
|
|
299
|
+
getRepository(name) {
|
|
300
|
+
const model = this.#models?.[name];
|
|
301
|
+
if (!model) throw new Error(`MongooseOrm "${this.name}": no entity model registered under "${name}".`);
|
|
302
|
+
if (this.#repositories === null) this.#repositories = Object.create(null);
|
|
303
|
+
let repository = this.#repositories[name];
|
|
304
|
+
if (repository === void 0) {
|
|
305
|
+
repository = new MongooseRepository(model, this.name);
|
|
306
|
+
this.#repositories[name] = repository;
|
|
307
|
+
}
|
|
308
|
+
return repository;
|
|
309
|
+
}
|
|
310
|
+
async transaction(work) {
|
|
311
|
+
const connection = this.#connection;
|
|
312
|
+
if (!connection) throw new Error(`MongooseOrm "${this.name}": not connected.`);
|
|
313
|
+
const session = await connection.startSession();
|
|
314
|
+
let result;
|
|
315
|
+
try {
|
|
316
|
+
await session.withTransaction(async () => {
|
|
317
|
+
result = await work(new MongooseTransaction(session));
|
|
318
|
+
});
|
|
319
|
+
} finally {
|
|
320
|
+
await session.endSession();
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
getNativeConnection() {
|
|
325
|
+
if (!this.#connection) throw new Error(`MongooseOrm "${this.name}": not connected.`);
|
|
326
|
+
return this.#connection;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Ping bas-coût : commande `{ ping: 1 }` sur la base native (`admin().command`)
|
|
330
|
+
* — round-trip réel vers MongoDB pour le diagnostic du data plane.
|
|
331
|
+
*
|
|
332
|
+
* @throws si la connexion (ou sa base native) n'est pas prête, ou si la base
|
|
333
|
+
* ne répond pas.
|
|
334
|
+
*/
|
|
335
|
+
async ping() {
|
|
336
|
+
const db = this.#connection?.db;
|
|
337
|
+
if (!db) throw new Error(`MongooseOrm "${this.name}": not connected.`);
|
|
338
|
+
await db.admin().command({ ping: 1 });
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Sonde Mongo (best-effort) : connexions du serveur (`serverStatus`) → pool.
|
|
342
|
+
* Round-trip réseau → uniquement pendant un abonnement actif. `{}` si indispo.
|
|
343
|
+
*
|
|
344
|
+
* @returns sonde `pool` + `extra`, ou `{}`.
|
|
345
|
+
*/
|
|
346
|
+
async probe() {
|
|
347
|
+
const db = this.#connection?.db;
|
|
348
|
+
if (!db) return {};
|
|
349
|
+
try {
|
|
350
|
+
const status = await db.admin().serverStatus();
|
|
351
|
+
const conn = status.connections;
|
|
352
|
+
return {
|
|
353
|
+
pool: {
|
|
354
|
+
borrowed: conn?.current,
|
|
355
|
+
available: conn?.available
|
|
356
|
+
},
|
|
357
|
+
extra: status.version ? { serverVersion: status.version } : {}
|
|
358
|
+
};
|
|
359
|
+
} catch {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Colonnes normalisées d'une entité depuis les `paths` du schéma Mongoose —
|
|
365
|
+
* alimente le graphe canonique / ERD / contexte IA. Pas de PK SQL : `_id` est
|
|
366
|
+
* la clé primaire implicite de tout document.
|
|
367
|
+
*
|
|
368
|
+
* @param name - nom logique de l'entité.
|
|
369
|
+
* @returns colonnes (`[]` si l'entité n'est pas connue de cet ORM).
|
|
370
|
+
*/
|
|
371
|
+
describeEntity(name) {
|
|
372
|
+
const model = this.#models?.[name];
|
|
373
|
+
if (!model) return [];
|
|
374
|
+
const paths = model.schema.paths;
|
|
375
|
+
return Object.entries(paths).map(([field, schemaType]) => ({
|
|
376
|
+
name: field,
|
|
377
|
+
type: schemaType.instance || "Mixed",
|
|
378
|
+
primaryKey: field === "_id",
|
|
379
|
+
nullable: field === "_id" ? false : schemaType.isRequired !== true,
|
|
380
|
+
unique: schemaType.options.unique === true
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Décrit la connexion : driver `mongodb` + cible (hôte:port/base, **sans
|
|
385
|
+
* credentials**) + version de la lib `mongoose`. Aucun secret n'est exposé
|
|
386
|
+
* dans le data plane (l'URI est nettoyée de tout `user:pass@`).
|
|
387
|
+
*
|
|
388
|
+
* @returns driver + cible nettoyée + version de l'ORM.
|
|
389
|
+
*/
|
|
390
|
+
describeConnection() {
|
|
391
|
+
return {
|
|
392
|
+
driver: "mongodb",
|
|
393
|
+
target: this.safeTarget(),
|
|
394
|
+
ormVersion: MongooseOrm.#ormVersion()
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Cible affichable de l'URI, **sans credentials** : `hôte:port/base`. Jamais
|
|
399
|
+
* de `user:pass@` (anti info-leak dans le data plane / les logs).
|
|
400
|
+
*/
|
|
401
|
+
safeTarget() {
|
|
402
|
+
try {
|
|
403
|
+
const url = new URL(this.#uri);
|
|
404
|
+
url.username = "";
|
|
405
|
+
url.password = "";
|
|
406
|
+
return `${url.host}${url.pathname}`;
|
|
407
|
+
} catch {
|
|
408
|
+
return this.#uri.replace(/^mongodb(\+srv)?:\/\//, "").replace(/^[^@/]*@/, "").replace(/\?.*$/, "");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** Version de la lib `mongoose` (résolue + cachée une seule fois). */
|
|
412
|
+
static #cachedOrmVersion;
|
|
413
|
+
static #ormVersion() {
|
|
414
|
+
if (MongooseOrm.#cachedOrmVersion === void 0) MongooseOrm.#cachedOrmVersion = MongooseOrm.#resolvePkgVersion("mongoose") ?? null;
|
|
415
|
+
return MongooseOrm.#cachedOrmVersion ?? void 0;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Version d'un package npm via son `package.json` (`createRequire` + remontée
|
|
419
|
+
* FS). `require("<pkg>/package.json")` direct échoue souvent : `exports` ne
|
|
420
|
+
* publie pas toujours `./package.json`.
|
|
421
|
+
*/
|
|
422
|
+
static #resolvePkgVersion(name) {
|
|
423
|
+
try {
|
|
424
|
+
const req = createRequire(import.meta.url);
|
|
425
|
+
let dir = path.dirname(req.resolve(name));
|
|
426
|
+
for (let i = 0; i < 8; i++) {
|
|
427
|
+
const pkgPath = path.join(dir, "package.json");
|
|
428
|
+
if (fs.existsSync(pkgPath)) {
|
|
429
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
430
|
+
if (pkg.name === name) return pkg.version;
|
|
431
|
+
}
|
|
432
|
+
const parent = path.dirname(dir);
|
|
433
|
+
if (parent === dir) break;
|
|
434
|
+
dir = parent;
|
|
435
|
+
}
|
|
436
|
+
} catch {}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
//#endregion
|
|
440
|
+
export { MongooseOrm };
|