@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
@@ -0,0 +1,151 @@
1
+ import { TOTP_SECRET_ENTITY } from "../entity/totpSecretEntity.js";
2
+ import { assertPageQuery } from "nodefony";
3
+ import { paginate, searchCriteria } from "@nodefony/orm-core";
4
+ //#region nodefony/src/MongooseTotpSecretStore.ts
5
+ /**
6
+ * Store de secrets TOTP **Mongoose** (NoSQL) — implémentation documentaire d'
7
+ * {@link ITotpSecretStore} au-dessus d'un unique repository `@nodefony/orm-core`
8
+ * (`totp_secret`). Pendant documentaire de `DrizzleTotpSecretStore` : là où
9
+ * `MemoryTotpSecretStore` est volatile, ce store survit au redémarrage et se
10
+ * partage entre pods.
11
+ *
12
+ * **Modèle 1 secret / utilisateur** (clé = `userId`, portée par `_id`) → `save`
13
+ * est un upsert par clé primaire.
14
+ *
15
+ * **Approche B** : `@nodefony/security` n'est connu qu'en `import type` (0 dép
16
+ * runtime). L'entité (`registerTotpSecretEntity`) doit être enregistrée
17
+ * **avant** `orm.connect()` (le modèle est compilé au connect).
18
+ *
19
+ * **100 % portable** (aucune query native) — toutes les opérations passent par
20
+ * le contrat `IRepository`, comme son frère SQL : l'`userId` est cherché sous
21
+ * `id` (le contrat traduit `{ id }` → `{ _id }`), et le listing par `paginate()`.
22
+ *
23
+ * **`secretEnc` opaque** : le store persiste le secret DÉJÀ chiffré (AES-256-GCM
24
+ * côté service) — il ne déchiffre jamais, ne voit que des octets. Il ne sort
25
+ * JAMAIS par `listPage` (cf {@link ITotpEnrollmentSummary}).
26
+ */
27
+ var MongooseTotpSecretStore = class MongooseTotpSecretStore {
28
+ #repo;
29
+ /**
30
+ * @param repo - repository de la collection `totp_secret`.
31
+ */
32
+ constructor(repo) {
33
+ this.#repo = repo;
34
+ }
35
+ /** Connecteur ORM qui porte ce store — posé par {@link MongooseTotpSecretStore.from}. */
36
+ #connector;
37
+ /**
38
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
39
+ * registre des stores : la console rattache la brique à SON connecteur au
40
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
41
+ */
42
+ get connector() {
43
+ return this.#connector;
44
+ }
45
+ /**
46
+ * Construit le store depuis un {@link MongooseOrm} connecté. L'entité
47
+ * (`registerTotpSecretEntity`) doit avoir été enregistrée **avant**
48
+ * `orm.connect()`.
49
+ *
50
+ * @param orm - ORM Mongoose connecté hébergeant la collection du store.
51
+ */
52
+ static from(orm) {
53
+ const store = new MongooseTotpSecretStore(orm.getRepository(TOTP_SECRET_ENTITY));
54
+ store.#connector = orm.name;
55
+ return store;
56
+ }
57
+ /** Row plate → secret du contrat (recoveryCodes copié = mutable indépendant). */
58
+ #toSecret(row) {
59
+ return {
60
+ userId: row.userId,
61
+ secretEnc: row.secretEnc,
62
+ algorithm: row.algorithm,
63
+ digits: row.digits,
64
+ period: row.period,
65
+ recoveryCodes: [...row.recoveryCodes],
66
+ confirmedAt: row.confirmedAt,
67
+ lastUsedStep: row.lastUsedStep,
68
+ createdAt: row.createdAt,
69
+ lastUsedAt: row.lastUsedAt
70
+ };
71
+ }
72
+ async findByUser(userId) {
73
+ const row = await this.#repo.findOne({ id: userId });
74
+ return row ? this.#toSecret(row) : null;
75
+ }
76
+ async save(secret) {
77
+ await this.#repo.upsert({ id: secret.userId }, {
78
+ userId: secret.userId,
79
+ secretEnc: secret.secretEnc,
80
+ algorithm: secret.algorithm,
81
+ digits: secret.digits,
82
+ period: secret.period,
83
+ recoveryCodes: [...secret.recoveryCodes],
84
+ confirmedAt: secret.confirmedAt,
85
+ lastUsedStep: secret.lastUsedStep,
86
+ createdAt: secret.createdAt,
87
+ lastUsedAt: secret.lastUsedAt
88
+ });
89
+ }
90
+ async update(userId, patch) {
91
+ const set = {};
92
+ if (patch.confirmedAt !== void 0) set.confirmedAt = patch.confirmedAt;
93
+ if (patch.recoveryCodes !== void 0) set.recoveryCodes = [...patch.recoveryCodes];
94
+ if (patch.lastUsedStep !== void 0) set.lastUsedStep = patch.lastUsedStep;
95
+ if (patch.lastUsedAt !== void 0) set.lastUsedAt = patch.lastUsedAt;
96
+ if (Object.keys(set).length === 0) return;
97
+ await this.#repo.updateOne({ id: userId }, set);
98
+ }
99
+ async delete(userId) {
100
+ await this.#repo.delete({ id: userId });
101
+ }
102
+ /**
103
+ * Critère portable des filtres du listing. `confirmed` s'exprime en `$null`
104
+ * sur `confirmedAt` (pas de champ booléen dérivé à maintenir), `q` en `$like`
105
+ * **ancré à gauche** (`préfixe%`) — traduit en `$regex` ancré par le
106
+ * repository, donc servable par un index.
107
+ */
108
+ #listCriteria(query) {
109
+ const criteria = {};
110
+ if (query.confirmed !== void 0) criteria.confirmedAt = { $null: !query.confirmed };
111
+ Object.assign(criteria, searchCriteria(query.q, ["userId"]) ?? {});
112
+ return criteria;
113
+ }
114
+ /**
115
+ * {@inheritDoc ITotpSecretStore.listPage}
116
+ *
117
+ * 100 % portable : le helper `paginate()` d'orm-core (limite/décalage + total
118
+ * optionnel) sur un critère simple. La projection en vue d'enrôlement retire
119
+ * `secretEnc` et les condensats — ils ne franchissent jamais la frontière du
120
+ * store, quel que soit l'appelant.
121
+ */
122
+ async listPage(query) {
123
+ assertPageQuery(query, "offset");
124
+ const page = await paginate(this.#repo, {
125
+ criteria: this.#listCriteria(query),
126
+ limit: query.limit,
127
+ offset: query.offset,
128
+ withTotal: query.withTotal,
129
+ order: [["createdAt", "DESC"], ["userId", "ASC"]]
130
+ });
131
+ return {
132
+ ...page,
133
+ items: page.items.map((row) => ({
134
+ userId: row.userId,
135
+ algorithm: row.algorithm,
136
+ digits: row.digits,
137
+ period: row.period,
138
+ confirmedAt: row.confirmedAt,
139
+ createdAt: row.createdAt,
140
+ lastUsedAt: row.lastUsedAt,
141
+ recoveryCodesLeft: row.recoveryCodes.length
142
+ }))
143
+ };
144
+ }
145
+ /** {@inheritDoc ITotpSecretStore.countEnrollments} */
146
+ countEnrollments(query) {
147
+ return this.#repo.count(this.#listCriteria(query));
148
+ }
149
+ };
150
+ //#endregion
151
+ export { MongooseTotpSecretStore };
@@ -167,7 +167,7 @@ var MongooseUserRepository = class MongooseUserRepository {
167
167
  const offset = Math.max(0, Math.floor(query.offset ?? 0));
168
168
  const filter = this.#listFilter(query);
169
169
  const sort = toMongoSort(mongoOrder(query.order, this.sortableFields, USER_DEFAULT_ORDER));
170
- if (sort._id === void 0) sort._id = 1;
170
+ if (!Object.hasOwn(sort, "_id")) sort._id = 1;
171
171
  let cursor = this.#model.find(filter).sort(sort).skip(offset).limit(limit + 1);
172
172
  if (this.#session) cursor = cursor.session(this.#session);
173
173
  const docs = await cursor.exec();
@@ -25,6 +25,16 @@ var MongooseWebAuthnCredentialStore = class MongooseWebAuthnCredentialStore {
25
25
  constructor(repo) {
26
26
  this.#repo = repo;
27
27
  }
28
+ /** Connecteur ORM qui porte ce store — posé par {@link MongooseWebAuthnCredentialStore.from}. */
29
+ #connector;
30
+ /**
31
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
32
+ * registre des stores : la console rattache la brique à SON connecteur au
33
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
34
+ */
35
+ get connector() {
36
+ return this.#connector;
37
+ }
28
38
  /**
29
39
  * Construit le store depuis un {@link MongooseOrm} connecté. L'entité
30
40
  * (`registerWebAuthnCredentialEntity`) doit avoir été enregistrée **avant**
@@ -33,7 +43,9 @@ var MongooseWebAuthnCredentialStore = class MongooseWebAuthnCredentialStore {
33
43
  * @param orm - ORM Mongoose connecté hébergeant la collection du store.
34
44
  */
35
45
  static from(orm) {
36
- return new MongooseWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY));
46
+ const store = new MongooseWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY));
47
+ store.#connector = orm.name;
48
+ return store;
37
49
  }
38
50
  /** Identité réelle d'un credential : `_id` fait foi, le virtuel `id` en repli. */
39
51
  #idOf(row) {
@@ -1,5 +1,5 @@
1
- import { WEBHOOK_ENDPOINT_ENTITY } from "../entity/webhookEndpointEntity.js";
2
1
  import { mongoOrder, toMongoSort } from "./mongoOrder.js";
2
+ import { WEBHOOK_ENDPOINT_ENTITY } from "../entity/webhookEndpointEntity.js";
3
3
  import { assertPageQuery, escapeRegExp } from "nodefony";
4
4
  import { WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS } from "@nodefony/security";
5
5
  //#region nodefony/src/MongooseWebhookStore.ts
@@ -41,6 +41,16 @@ var MongooseWebhookStore = class MongooseWebhookStore {
41
41
  this.#repo = repo;
42
42
  this.#model = model;
43
43
  }
44
+ /** Connecteur ORM qui porte ce store — posé par {@link MongooseWebhookStore.from}. */
45
+ #connector;
46
+ /**
47
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
48
+ * registre des stores : la console rattache la brique à SON connecteur au
49
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
50
+ */
51
+ get connector() {
52
+ return this.#connector;
53
+ }
44
54
  /**
45
55
  * Construit le store depuis un {@link MongooseOrm} connecté. L'entité
46
56
  * (`registerWebhookEndpointEntity`) doit avoir été enregistrée **avant**
@@ -50,7 +60,9 @@ var MongooseWebhookStore = class MongooseWebhookStore {
50
60
  */
51
61
  static from(orm) {
52
62
  const connection = orm.getNativeConnection();
53
- return new MongooseWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), connection.model(WEBHOOK_ENDPOINT_ENTITY));
63
+ const store = new MongooseWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), connection.model(WEBHOOK_ENDPOINT_ENTITY));
64
+ store.#connector = orm.name;
65
+ return store;
54
66
  }
55
67
  /** Identité réelle d'un endpoint : `_id` fait foi, le virtuel `id` en repli. */
56
68
  #idOf(row) {
@@ -27,6 +27,14 @@ var SessionStorage = class SessionStorage {
27
27
  this.absoluteTimeoutS = manager.options.absoluteTimeoutS;
28
28
  }
29
29
  /**
30
+ * Connecteur ORM qui porte les sessions — lu par `readStoreConnector` pour le
31
+ * registre des stores. `undefined` tant que l'ORM de ce connecteur n'est pas
32
+ * enregistré : on ne publie pas un rattachement que rien ne porte.
33
+ */
34
+ get connector() {
35
+ return ormRegistry.has("nodefony") ? SESSION_CONNECTOR : void 0;
36
+ }
37
+ /**
30
38
  * Repository de l'entité session, ou `null` si l'ORM n'est pas (ou plus)
31
39
  * connecté.
32
40
  *
@@ -56,9 +56,12 @@ var MongooseOrm = class MongooseOrm extends Orm {
56
56
  * @param name - clé unique de l'ORM dans le `ormRegistry`.
57
57
  * @param uri - URI de connexion MongoDB (replica set requis pour les tx).
58
58
  * @param options - options de connexion Mongoose (auth, pool, timeouts).
59
+ * @param container - container du service qui construit l'ORM : il porte le
60
+ * `syslog` du kernel. Sans lui, la perte et la reprise de connexion
61
+ * s'écrivent dans un journal que rien ne relie à la sortie du serveur.
59
62
  */
60
- constructor(name, uri, options) {
61
- super(name);
63
+ constructor(name, uri, options, container) {
64
+ super(name, container);
62
65
  this.#uri = uri;
63
66
  this.#options = options;
64
67
  }
@@ -83,11 +86,18 @@ var MongooseOrm = class MongooseOrm extends Orm {
83
86
  this.#models = Object.create(null);
84
87
  const entities = this.#ownEntities();
85
88
  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
- }));
89
+ for (const entity of entities) {
90
+ const schema = new mongoose.Schema(entity.schema, {
91
+ toObject: { virtuals: true },
92
+ toJSON: { virtuals: true },
93
+ timestamps: entity.timestamps ?? false
94
+ });
95
+ for (const index of entity.indexes ?? []) schema.index({ ...index.fields }, {
96
+ ...index.unique !== void 0 ? { unique: index.unique } : {},
97
+ ...index.name !== void 0 ? { name: index.name } : {}
98
+ });
99
+ schemas.set(entity.name, schema);
100
+ }
91
101
  for (const entity of entities) {
92
102
  if (!entity.relations) continue;
93
103
  const sourceSchema = schemas.get(entity.name);
@@ -97,7 +107,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
97
107
  switch (relation.type) {
98
108
  case "one-to-many": {
99
109
  const fk = relation.foreignKey ?? this.#foreignKey(entity.name);
100
- if (!targetSchema.path(fk)) targetSchema.add({ [fk]: {
110
+ if (targetSchema.path(fk) === void 0) targetSchema.add({ [fk]: {
101
111
  type: mongoose.Schema.Types.ObjectId,
102
112
  ref: entity.name
103
113
  } });
@@ -111,7 +121,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
111
121
  case "many-to-one":
112
122
  case "one-to-one": {
113
123
  const fk = relation.foreignKey ?? relation.field;
114
- if (!sourceSchema.path(fk)) sourceSchema.add({ [fk]: {
124
+ if (sourceSchema.path(fk) === void 0) sourceSchema.add({ [fk]: {
115
125
  type: mongoose.Schema.Types.ObjectId,
116
126
  ref: relation.target
117
127
  } });
@@ -126,6 +136,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
126
136
  this.#models[entity.name] = model;
127
137
  entity.model = model;
128
138
  }
139
+ this.#referrers = this.#indexReferrers(this.#models);
129
140
  const audit = this.verifyIndexes();
130
141
  this.#indexAudit = audit;
131
142
  audit.catch(() => void 0);
@@ -180,7 +191,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
180
191
  for (const [name, model] of Object.entries(models)) {
181
192
  const audit = {
182
193
  entity: name,
183
- collection: model.collection?.name ?? name,
194
+ collection: model.collection.name,
184
195
  missing: [],
185
196
  extra: []
186
197
  };
@@ -268,7 +279,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
268
279
  const listeners = [
269
280
  ["disconnected", lost("mongoose: disconnected")],
270
281
  ["close", lost("mongoose: close")],
271
- ["error", ((e) => this.connectionLost(`mongoose: ${e?.message ?? String(e)}`))],
282
+ ["error", (e) => this.connectionLost(`mongoose: ${e instanceof Error ? e.message : String(e)}`)],
272
283
  ["reconnected", () => this.connectionRestored()],
273
284
  ["connected", () => this.connectionRestored()]
274
285
  ];
@@ -294,15 +305,51 @@ var MongooseOrm = class MongooseOrm extends Orm {
294
305
  this.#connection = null;
295
306
  this.#models = null;
296
307
  this.#repositories = null;
308
+ this.#referrers = null;
297
309
  this.#indexAudit = null;
298
310
  }
311
+ /**
312
+ * Index inverse des références : pour chaque entité visée, les champs des
313
+ * autres entités qui la désignent. `null` quand aucune référence n'existe.
314
+ */
315
+ #referrers = null;
316
+ /**
317
+ * Bâtit l'index inverse des références à partir des schémas COMPILÉS.
318
+ *
319
+ * Tout champ `ObjectId` qui porte un `ref` vers une entité de cette connexion
320
+ * compte — qu'il vienne d'un schéma généré (`author: { type: ObjectId, ref }`)
321
+ * ou d'une relation déclarée (`relations`, que l'étape 2 traduit en champ).
322
+ * Un tableau de références n'y entre pas : son effacement (retirer l'élément)
323
+ * n'a pas d'équivalent dans la politique `restrict` / `set null` du SQL.
324
+ *
325
+ * @param models - modèles compilés de la connexion.
326
+ * @returns l'index, ou `null` si aucune entité n'en référence une autre.
327
+ */
328
+ #indexReferrers(models) {
329
+ let index = null;
330
+ for (const model of Object.values(models)) {
331
+ const leading = new Set(model.schema.indexes().map(([fields]) => Object.keys(fields)[0]));
332
+ model.schema.eachPath((fieldPath, type) => {
333
+ const ref = type.options?.ref;
334
+ if (fieldPath === "_id" || type.instance !== "ObjectId" || typeof ref !== "string" || models[ref] === void 0) return;
335
+ index ??= Object.create(null);
336
+ (index[ref] ??= []).push({
337
+ model,
338
+ path: fieldPath,
339
+ required: type.isRequired === true
340
+ });
341
+ if (!leading.has(fieldPath)) this.log(`référence "${model.modelName}.${fieldPath}" → "${ref}" sans index : chaque suppression d'un "${ref}" parcourra toute la collection "${model.collection.collectionName}" — ajouter \`index: true\``, "WARNING");
342
+ });
343
+ }
344
+ return index;
345
+ }
299
346
  getRepository(name) {
300
347
  const model = this.#models?.[name];
301
348
  if (!model) throw new Error(`MongooseOrm "${this.name}": no entity model registered under "${name}".`);
302
- if (this.#repositories === null) this.#repositories = Object.create(null);
349
+ this.#repositories ??= Object.create(null);
303
350
  let repository = this.#repositories[name];
304
351
  if (repository === void 0) {
305
- repository = new MongooseRepository(model, this.name);
352
+ repository = new MongooseRepository(model, this.name, null, this.#referrers?.[name] ?? null);
306
353
  this.#repositories[name] = repository;
307
354
  }
308
355
  return repository;
@@ -311,15 +358,11 @@ var MongooseOrm = class MongooseOrm extends Orm {
311
358
  const connection = this.#connection;
312
359
  if (!connection) throw new Error(`MongooseOrm "${this.name}": not connected.`);
313
360
  const session = await connection.startSession();
314
- let result;
315
361
  try {
316
- await session.withTransaction(async () => {
317
- result = await work(new MongooseTransaction(session));
318
- });
362
+ return await session.withTransaction(() => work(new MongooseTransaction(session)));
319
363
  } finally {
320
364
  await session.endSession();
321
365
  }
322
- return result;
323
366
  }
324
367
  getNativeConnection() {
325
368
  if (!this.#connection) throw new Error(`MongooseOrm "${this.name}": not connected.`);
@@ -362,8 +405,11 @@ var MongooseOrm = class MongooseOrm extends Orm {
362
405
  }
363
406
  /**
364
407
  * 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.
408
+ * alimente le graphe canonique / ERD / contexte IA. Rendues dans le
409
+ * vocabulaire du CONTRAT, pas du moteur : la clé primaire `_id` s'y nomme
410
+ * `id` (comme dans les critères et les lignes lues), et la clé de version
411
+ * interne `__v` n'y figure pas — un graphe qui changerait de noms selon
412
+ * l'adaptateur ne décrirait plus la même application.
367
413
  *
368
414
  * @param name - nom logique de l'entité.
369
415
  * @returns colonnes (`[]` si l'entité n'est pas connue de cet ORM).
@@ -372,8 +418,8 @@ var MongooseOrm = class MongooseOrm extends Orm {
372
418
  const model = this.#models?.[name];
373
419
  if (!model) return [];
374
420
  const paths = model.schema.paths;
375
- return Object.entries(paths).map(([field, schemaType]) => ({
376
- name: field,
421
+ return Object.entries(paths).filter(([field]) => field !== "__v").map(([field, schemaType]) => ({
422
+ name: field === "_id" ? "id" : field,
377
423
  type: schemaType.instance || "Mixed",
378
424
  primaryKey: field === "_id",
379
425
  nullable: field === "_id" ? false : schemaType.isRequired !== true,