@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
@@ -1,7 +1,21 @@
1
+ import { MONGO_ORDER_ALIASES } from "../mongoOrder.js";
2
+ import { writeCount } from "../writeCount.js";
1
3
  import { RequestContext, redactSecrets } from "nodefony";
2
- import { UnknownCriteriaField, assertOrderOption, isFieldOperators, isUpdateOperators, likePatternToRegExp, queryFlowMonitor } from "@nodefony/orm-core";
4
+ import { ReferencedEntityError, UnknownCriteriaField, assertOrderOption, isFieldOperators, isUpdateOperators, likePatternToRegExp, queryFlowMonitor } from "@nodefony/orm-core";
3
5
  //#region nodefony/src/orm-core/MongooseRepository.ts
4
6
  /**
7
+ * Clé Mongo d'un champ de TRI — `id` public → `_id` au repos.
8
+ *
9
+ * Mongo ne se plaint pas d'un tri sur un champ absent : il rend les documents
10
+ * dans un ordre arbitraire. `id` n'est qu'un virtuel de lecture ; sans cette
11
+ * traduction, `order: [["id", "DESC"]]` — le départage du tri par défaut de
12
+ * tout CRUD généré — serait silencieusement inerte ici et correct en SQL. Même
13
+ * table que les stores Mongo ({@link MONGO_ORDER_ALIASES}).
14
+ */
15
+ function sortKey(field) {
16
+ return MONGO_ORDER_ALIASES[field] ?? field;
17
+ }
18
+ /**
5
19
  * Repository portable (contrat {@link IRepository}) au-dessus d'un modèle Mongoose.
6
20
  *
7
21
  * Démontre la portabilité du contrat sur un **store documentaire hétérogène** :
@@ -19,15 +33,29 @@ var MongooseRepository = class MongooseRepository {
19
33
  #session;
20
34
  /** Connecteur ORM (clé du registre) — tag des métriques de flux. */
21
35
  #connector;
36
+ /** Champs qui référencent cette entité — `null` si personne ne la vise (cas courant, coût nul). */
37
+ #referrers;
22
38
  /**
23
39
  * @param model - modèle Mongoose compilé.
24
40
  * @param connector - nom de la connexion (clé du registre) — défaut `"nodefony"`.
25
41
  * @param session - session transactionnelle à laquelle lier les ops (ou `null`).
42
+ * @param referrers - champs d'autres entités qui référencent celle-ci (index
43
+ * inverse de {@link MongooseOrm}) ; `null` quand aucune ne la vise.
26
44
  */
27
- constructor(model, connector = "nodefony", session = null) {
45
+ constructor(model, connector = "nodefony", session = null, referrers = null) {
28
46
  this.#model = model;
29
47
  this.#connector = connector;
30
48
  this.#session = session;
49
+ this.#referrers = referrers;
50
+ }
51
+ /**
52
+ * Options de session pour les méthodes dont le type refuse `session: null`
53
+ * (`updateMany`, `deleteMany`, `countDocuments`…) : la clé est ABSENTE hors
54
+ * transaction. Un objet neuf par appel, comme le littéral qu'il remplace —
55
+ * Mongoose peut enrichir l'objet d'options qu'il reçoit.
56
+ */
57
+ #sessionOptions() {
58
+ return this.#session === null ? {} : { session: this.#session };
31
59
  }
32
60
  /**
33
61
  * Tap dev-only : mesure la durée d'une opération et alimente **deux** sondes
@@ -77,6 +105,13 @@ var MongooseRepository = class MongooseRepository {
77
105
  * (`$eq`/`$ne`) — en Mongo `null` matche aussi le champ **absent**, ce qui est
78
106
  * bien l'équivalent du `NULL` SQL (colonne sans valeur).
79
107
  *
108
+ * `$ne`/`$nin` ne matchent JAMAIS l'absence de valeur, comme en SQL (où
109
+ * `col <> 'x'` est inconnu, donc faux, sur un NULL). Natifs, ils la
110
+ * matcheraient : `{ note: { $ne: "x" } }` ramenait sous MongoDB les documents
111
+ * sans note, que PostgreSQL écarte — la même requête rendait plus de lignes en
112
+ * production MongoDB, sans un mot. `null` est donc ajouté à l'ensemble exclu.
113
+ * Un `$nin` VIDE reste neutre (SQL : aucune exclusion, NULL compris).
114
+ *
80
115
  * @throws Error si `$null` est combiné à `$eq`/`$ne` sur le même champ : les
81
116
  * deux viseraient la même clé Mongo et l'une écraserait l'autre **en
82
117
  * silence** (le contrat les déclare exclusifs — cf `FieldOperators.$null`).
@@ -90,6 +125,17 @@ var MongooseRepository = class MongooseRepository {
90
125
  out[target] = null;
91
126
  } else if ((key === "$eq" || key === "$ne") && key in out) throw new Error(`criteria: $null ne se combine pas avec ${key} sur le même champ (${this.#model.modelName})`);
92
127
  else out[key] = value;
128
+ const ne = ops.$ne;
129
+ const nin = ops.$nin;
130
+ if (ne !== void 0 && ne !== null || nin !== void 0 && nin.length > 0) {
131
+ const excluded = [...nin ?? []];
132
+ if (ne !== void 0 && ne !== null) {
133
+ excluded.push(ne);
134
+ delete out.$ne;
135
+ }
136
+ excluded.push(null);
137
+ out.$nin = excluded;
138
+ }
93
139
  return out;
94
140
  }
95
141
  /**
@@ -106,7 +152,7 @@ var MongooseRepository = class MongooseRepository {
106
152
  #resolveField(field) {
107
153
  const key = field === "id" ? "_id" : field;
108
154
  const paths = this.#model.schema.paths;
109
- if (key === "_id" || paths[key] !== void 0 || paths[key.split(".")[0]] !== void 0) return key;
155
+ if (key === "_id" || paths[key] !== void 0 || paths[key.split(".")[0] ?? key] !== void 0) return key;
110
156
  throw new UnknownCriteriaField(field, this.#model.modelName, Object.keys(paths));
111
157
  }
112
158
  /**
@@ -141,7 +187,7 @@ var MongooseRepository = class MongooseRepository {
141
187
  if (options?.relations?.length) query = query.populate(options.relations);
142
188
  if (options?.offset !== void 0) query = query.skip(options.offset);
143
189
  if (options?.limit !== void 0) query = query.limit(options.limit);
144
- if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [field, dir === "DESC" ? -1 : 1])));
190
+ if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [sortKey(field), dir === "DESC" ? -1 : 1])));
145
191
  return (await query.exec()).map((doc) => this.#plain(doc));
146
192
  }, (docs) => docs.length);
147
193
  }
@@ -152,21 +198,22 @@ var MongooseRepository = class MongooseRepository {
152
198
  let query = this.#model.findOne(filter);
153
199
  if (this.#session) query = query.session(this.#session);
154
200
  if (options?.relations?.length) query = query.populate(options.relations);
155
- if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [field, dir === "DESC" ? -1 : 1])));
201
+ if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [sortKey(field), dir === "DESC" ? -1 : 1])));
156
202
  const doc = await query.exec();
157
203
  return doc ? this.#plain(doc) : null;
158
204
  }, (doc) => doc ? 1 : 0);
159
205
  }
160
206
  async create(data) {
161
207
  return this.#prof(() => this.#descr("create"), async () => {
162
- const [doc] = await this.#model.create([data], { session: this.#session ?? void 0 });
208
+ const [doc] = await this.#model.create([data], { session: this.#session });
209
+ if (doc === void 0) throw new Error(`create: le pilote n'a rendu aucun document (${this.#model.modelName})`);
163
210
  return this.#plain(doc);
164
211
  }, () => 1);
165
212
  }
166
213
  async createMany(data) {
167
214
  if (data.length === 0) return [];
168
215
  return this.#prof(() => this.#descr("insertMany"), async () => {
169
- return (await this.#model.insertMany(data, { session: this.#session ?? void 0 })).map((doc) => this.#plain(doc));
216
+ return (await this.#model.insertMany(data, { session: this.#session })).map((doc) => this.#plain(doc));
170
217
  }, (rows) => rows.length);
171
218
  }
172
219
  async updateOne(criteria, data) {
@@ -174,7 +221,7 @@ var MongooseRepository = class MongooseRepository {
174
221
  return this.#prof(() => this.#descr("findOneAndUpdate", filter), async () => {
175
222
  const doc = await this.#model.findOneAndUpdate(filter, data, {
176
223
  returnDocument: "after",
177
- session: this.#session ?? void 0
224
+ session: this.#session
178
225
  }).exec();
179
226
  return doc ? this.#plain(doc) : null;
180
227
  }, (doc) => doc ? 1 : 0);
@@ -188,9 +235,8 @@ var MongooseRepository = class MongooseRepository {
188
235
  }, {
189
236
  upsert: true,
190
237
  returnDocument: "after",
191
- session: this.#session ?? void 0
238
+ session: this.#session
192
239
  }).exec();
193
- if (!doc) throw new Error("MongooseRepository.upsert: aucun document retourné");
194
240
  return this.#plain(doc);
195
241
  }, () => 1);
196
242
  }
@@ -224,7 +270,8 @@ var MongooseRepository = class MongooseRepository {
224
270
  async updateMany(criteria, data) {
225
271
  const filter = this.#filter(criteria);
226
272
  return this.#prof(() => this.#descr("updateMany", filter), async () => {
227
- return (await this.#model.updateMany(filter, data, { session: this.#session ?? void 0 })).modifiedCount ?? 0;
273
+ const res = await this.#model.updateMany(filter, data, this.#sessionOptions());
274
+ return writeCount(res.modifiedCount);
228
275
  }, (n) => n);
229
276
  }
230
277
  async increment(criteria, changes) {
@@ -232,33 +279,108 @@ var MongooseRepository = class MongooseRepository {
232
279
  return this.#prof(() => this.#descr("findOneAndUpdate", filter), async () => {
233
280
  const doc = await this.#model.findOneAndUpdate(filter, { $inc: changes }, {
234
281
  returnDocument: "after",
235
- session: this.#session ?? void 0
282
+ session: this.#session
236
283
  }).exec();
237
284
  return doc ? this.#plain(doc) : null;
238
285
  }, (doc) => doc ? 1 : 0);
239
286
  }
240
287
  async delete(criteria) {
241
- const filter = this.#filter(criteria);
242
- return this.#prof(() => this.#descr("deleteMany", filter), async () => {
243
- return (await this.#model.deleteMany(filter, { session: this.#session ?? void 0 })).deletedCount ?? 0;
288
+ let filter = this.#filter(criteria);
289
+ const ids = await this.#referencedTargets(filter, false);
290
+ if (ids !== null) {
291
+ if (ids.length === 0) return 0;
292
+ filter = { _id: { $in: ids } };
293
+ }
294
+ const removed = await this.#prof(() => this.#descr("deleteMany", filter), async () => {
295
+ const res = await this.#model.deleteMany(filter, this.#sessionOptions());
296
+ return writeCount(res.deletedCount);
244
297
  }, (n) => n);
298
+ if (ids !== null && removed > 0) await this.#releaseOptional(ids);
299
+ return removed;
245
300
  }
246
301
  async deleteOne(criteria) {
247
- const filter = this.#filter(criteria);
248
- return this.#prof(() => this.#descr("deleteOne", filter), async () => {
249
- return ((await this.#model.deleteOne(filter, { session: this.#session ?? void 0 })).deletedCount ?? 0) > 0;
302
+ let filter = this.#filter(criteria);
303
+ const ids = await this.#referencedTargets(filter, true);
304
+ if (ids !== null) {
305
+ if (ids.length === 0) return false;
306
+ filter = { _id: ids[0] };
307
+ }
308
+ const removed = await this.#prof(() => this.#descr("deleteOne", filter), async () => {
309
+ const res = await this.#model.deleteOne(filter, this.#sessionOptions());
310
+ return writeCount(res.deletedCount) > 0;
250
311
  }, (ok) => ok ? 1 : 0);
312
+ if (ids !== null && removed) await this.#releaseOptional(ids);
313
+ return removed;
251
314
  }
252
315
  async findOneAndDelete(criteria) {
253
- const filter = this.#filter(criteria);
254
- return this.#prof(() => this.#descr("findOneAndDelete", filter), async () => {
255
- const doc = await this.#model.findOneAndDelete(filter, { session: this.#session ?? void 0 }).exec();
256
- return doc ? this.#plain(doc) : null;
257
- }, (doc) => doc ? 1 : 0);
316
+ let filter = this.#filter(criteria);
317
+ const ids = await this.#referencedTargets(filter, true);
318
+ if (ids !== null) {
319
+ if (ids.length === 0) return null;
320
+ filter = { _id: ids[0] };
321
+ }
322
+ const doc = await this.#prof(() => this.#descr("findOneAndDelete", filter), async () => {
323
+ const found = await this.#model.findOneAndDelete(filter, { session: this.#session }).exec();
324
+ return found ? this.#plain(found) : null;
325
+ }, (d) => d ? 1 : 0);
326
+ if (ids !== null && doc !== null) await this.#releaseOptional(ids);
327
+ return doc;
328
+ }
329
+ /**
330
+ * Garde d'intégrité d'une suppression — le `restrict` que MongoDB ne tient pas.
331
+ *
332
+ * Rend `null` (et ne coûte rien) quand aucune entité ne référence celle-ci.
333
+ * Sinon, lit les `_id` que la suppression viserait, REFUSE si l'un d'eux est
334
+ * encore désigné par une référence obligatoire, et les rend : la suppression
335
+ * porte alors sur ces `_id` exacts, pas sur le filtre — ce qui a été contrôlé
336
+ * est ce qui part.
337
+ *
338
+ * ⚠️ Hors transaction, le contrôle et la suppression sont deux opérations : un
339
+ * enfant créé entre les deux n'est pas vu. Le SQL, lui, est atomique ; dans
340
+ * une transaction (`withTransaction`), les deux lisent la même session.
341
+ * ⚠️ Les `_id` visés sont tous chargés : une suppression en MASSE garde sa
342
+ * règle « un seul parent retenu bloque le lot », au prix d'une mémoire
343
+ * proportionnelle au lot. Le CRUD généré supprime par `id`, donc un à un.
344
+ *
345
+ * @param filter - filtre Mongo déjà traduit.
346
+ * @param single - vrai pour `deleteOne`/`findOneAndDelete` (un seul document).
347
+ * @returns les `_id` visés, ou `null` si l'entité n'est référencée par personne.
348
+ * @throws ReferencedEntityError si une référence obligatoire désigne l'un d'eux.
349
+ */
350
+ async #referencedTargets(filter, single) {
351
+ const referrers = this.#referrers;
352
+ if (referrers === null) return null;
353
+ const session = this.#session;
354
+ const query = this.#model.find(filter, { _id: 1 }, { session }).lean();
355
+ if (single) query.limit(1);
356
+ const docs = await query.exec();
357
+ if (docs.length === 0) return [];
358
+ const ids = docs.map((d) => d._id);
359
+ for (const referrer of referrers) {
360
+ if (!referrer.required) continue;
361
+ if (await referrer.model.exists(referrer.model === this.#model ? {
362
+ [referrer.path]: { $in: ids },
363
+ _id: { $nin: ids }
364
+ } : { [referrer.path]: { $in: ids } }).session(session ?? null).exec() !== null) throw new ReferencedEntityError(this.#model.modelName, referrer.model.modelName, referrer.path);
365
+ }
366
+ return ids;
367
+ }
368
+ /**
369
+ * Remet à `null` les références FACULTATIVES aux documents supprimés — le
370
+ * `set null` du SQL généré : l'enfant survit à son parent, sans pointer dans
371
+ * le vide.
372
+ *
373
+ * @param ids - `_id` des documents supprimés.
374
+ */
375
+ async #releaseOptional(ids) {
376
+ for (const referrer of this.#referrers ?? []) {
377
+ if (referrer.required) continue;
378
+ await referrer.model.updateMany({ [referrer.path]: { $in: ids } }, { $set: { [referrer.path]: null } }, this.#sessionOptions());
379
+ }
258
380
  }
259
381
  async count(criteria) {
260
382
  const filter = this.#filter(criteria);
261
- return this.#prof(() => this.#descr("countDocuments", filter), () => this.#model.countDocuments(filter, { session: this.#session ?? void 0 }));
383
+ return this.#prof(() => this.#descr("countDocuments", filter), () => this.#model.countDocuments(filter, this.#sessionOptions()));
262
384
  }
263
385
  /**
264
386
  * `COUNT(DISTINCT …)` en agrégation — `$group` puis `$count`, donc la
@@ -293,7 +415,7 @@ var MongooseRepository = class MongooseRepository {
293
415
  });
294
416
  }
295
417
  withTransaction(tx) {
296
- return new MongooseRepository(this.#model, this.#connector, tx.getNative());
418
+ return new MongooseRepository(this.#model, this.#connector, tx.getNative(), this.#referrers);
297
419
  }
298
420
  };
299
421
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { SavepointNotSupportedError } from "@nodefony/orm-core";
1
2
  //#region nodefony/src/orm-core/MongooseTransaction.ts
2
3
  /**
3
4
  * Adapte une `ClientSession` MongoDB au contrat portable {@link ITransaction}.
@@ -7,8 +8,8 @@
7
8
  * MongoDB **exigent un replica set** (un standalone ne les supporte pas).
8
9
  *
9
10
  * MongoDB n'a **pas de savepoints** : {@link MongooseTransaction.savepoint} /
10
- * {@link MongooseTransaction.rollbackTo} sont des no-op documentés (limite du
11
- * driver, prévue par le contrat).
11
+ * {@link MongooseTransaction.rollbackTo} les REFUSENT (`SavepointNotSupportedError`).
12
+ * Un no-op laissait croire un rollback partiel qui n'annulait rien.
12
13
  */
13
14
  var MongooseTransaction = class {
14
15
  #session;
@@ -40,10 +41,22 @@ var MongooseTransaction = class {
40
41
  this.#done = true;
41
42
  await this.#session.abortTransaction();
42
43
  }
43
- /** No-op : MongoDB ne gère pas les savepoints. */
44
- async savepoint(_name) {}
45
- /** No-op : MongoDB ne gère pas les savepoints. */
46
- async rollbackTo(_name) {}
44
+ /**
45
+ * Refusé : MongoDB n'a pas de savepoints.
46
+ *
47
+ * @throws SavepointNotSupportedError toujours.
48
+ */
49
+ async savepoint(_name) {
50
+ throw new SavepointNotSupportedError("mongodb", "savepoint");
51
+ }
52
+ /**
53
+ * Refusé : MongoDB n'a pas de savepoints.
54
+ *
55
+ * @throws SavepointNotSupportedError toujours.
56
+ */
57
+ async rollbackTo(_name) {
58
+ throw new SavepointNotSupportedError("mongodb", "rollbackTo");
59
+ }
47
60
  /** Expose la `ClientSession` native (trappe bas niveau). */
48
61
  getNative() {
49
62
  return this.#session;
@@ -0,0 +1,17 @@
1
+ //#region nodefony/src/writeCount.ts
2
+ /**
3
+ * Compte rendu par une écriture MongoDB (`deletedCount`, `modifiedCount`), 0 s'il manque.
4
+ *
5
+ * Le pilote le type `number`, mais le recopie de la réponse du serveur
6
+ * (`res.n`) : une écriture NON acquittée (`w: 0`) n'en reçoit aucune, et le
7
+ * compte arrive `undefined`. Sans ce repli, un `gc()` rendrait `undefined` et
8
+ * un `> 0` conclurait « rien écrit » pour une raison qui n'est pas la bonne.
9
+ *
10
+ * @param count - le compte tel que le pilote le rend.
11
+ * @returns le compte, ou 0 quand le serveur n'en a rendu aucun.
12
+ */
13
+ function writeCount(count) {
14
+ return count ?? 0;
15
+ }
16
+ //#endregion
17
+ export { writeCount };
@@ -70,5 +70,14 @@ export { MongooseWebAuthnCredentialStore } from "./nodefony/src/MongooseWebAuthn
70
70
  export { webhookEndpointSchema, createWebhookEndpointEntity, registerWebhookEndpointEntity, WEBHOOK_ENDPOINT_ENTITY, } from "./nodefony/entity/webhookEndpointEntity.js";
71
71
  export type { WebhookEndpointRow } from "./nodefony/entity/webhookEndpointEntity.js";
72
72
  export { MongooseWebhookStore } from "./nodefony/src/MongooseWebhookStore.js";
73
+ export { auditEventSchema, createAuditEntities, registerAuditEntities, AUDIT_ENTITY_NAMES, } from "./nodefony/entity/auditEventEntity.js";
74
+ export type { AuditEventRow } from "./nodefony/entity/auditEventEntity.js";
75
+ export { MongooseAuditStore } from "./nodefony/src/MongooseAuditStore.js";
76
+ export { totpSecretSchema, createTotpSecretEntity, registerTotpSecretEntity, TOTP_SECRET_ENTITY, } from "./nodefony/entity/totpSecretEntity.js";
77
+ export type { TotpSecretRow } from "./nodefony/entity/totpSecretEntity.js";
78
+ export { MongooseTotpSecretStore } from "./nodefony/src/MongooseTotpSecretStore.js";
79
+ export { idempotencyKeySchema, createIdempotencyEntities, registerIdempotencyEntities, IDEMPOTENCY_ENTITY_NAME, } from "./nodefony/entity/idempotencyEntity.js";
80
+ export type { IdempotencyKeyRow } from "./nodefony/entity/idempotencyEntity.js";
81
+ export { MongooseIdempotencyStore } from "./nodefony/src/MongooseIdempotencyStore.js";
73
82
  export { registerMongooseFrameworkStores, FRAMEWORK_CONNECTOR, } from "./nodefony/registerStores.js";
74
83
  export type { IFrameworkStoresReport } from "./nodefony/registerStores.js";
@@ -0,0 +1,76 @@
1
+ import type { SchemaDefinition } from "mongoose";
2
+ import type { IEntity } from "@nodefony/orm-core";
3
+ import type { AuditCategory, AuditOutcome, IAuditEventFlags } from "@nodefony/security";
4
+ /**
5
+ * Schéma Mongoose du **journal d'audit** `@nodefony/security` — pendant
6
+ * documentaire de la table Drizzle `audit_event`, implémentation NoSQL d'
7
+ * `IAuditStore` (append-only, tamper-evident).
8
+ *
9
+ * ⚠️ **`_id` = clé naturelle (String), PAS un ObjectId auto** : l'`id` d'un
10
+ * événement est posé par l'`AuditService` (corrélation + curseur de pagination)
11
+ * → on force `_id: String`, l'id EST la clé primaire. Même convention que
12
+ * `webhookEndpointEntity` et `tokenEntity`.
13
+ *
14
+ * ⚠️ **Horodatage = `Number` (epoch ms), `timestamps:false`** : `IAuditEvent.ts`
15
+ * porte un `number` (`Date.now()`), pas une `Date` — et c'est ce champ qui porte
16
+ * l'ordre du journal.
17
+ *
18
+ * **Index** — mêmes axes que la variante SQL, et pour la même raison (filtrage
19
+ * de la console d'audit) ; perf de lecture seule, jamais de sémantique.
20
+ *
21
+ * L'ORDRE TOTAL du store (`ts DESC, _id DESC`) est porté par un index
22
+ * COMPOSITE déclaré sur l'entité (`indexes`, cf `createAuditEntities`) : un
23
+ * `SchemaDefinition` plat n'exprime que des index par champ, et sans lui la
24
+ * requête du journal parcourait toute la collection puis triait en mémoire
25
+ * (plan `COLLSCAN` + `SORT`, mesuré). C'est lui qui garantit, au coût d'un
26
+ * parcours d'index, qu'aucun événement ne se répète ni ne se perd d'une page à
27
+ * l'autre — `audit-store.test.ts` en lit le plan d'exécution.
28
+ */
29
+ export declare const auditEventSchema: SchemaDefinition;
30
+ /**
31
+ * Forme **plate** d'une ligne du journal renvoyée par le repository Mongoose —
32
+ * `id` = virtuel (= `_id` = l'id d'événement). Miroir d'`IAuditEvent` avec les
33
+ * champs optionnels rendus explicitement `| null` (le document Mongo les porte
34
+ * à `null`, jamais absents). `MongooseAuditStore` mappe `Row ↔ IAuditEvent`.
35
+ */
36
+ export interface AuditEventRow {
37
+ id: string;
38
+ ts: number;
39
+ category: AuditCategory;
40
+ action: string;
41
+ outcome: AuditOutcome;
42
+ actor: string | null;
43
+ resource: string | null;
44
+ reason: string | null;
45
+ ip: string | null;
46
+ userAgent: string | null;
47
+ requestId: string | null;
48
+ flags: IAuditEventFlags | null;
49
+ metadata: Record<string, unknown> | null;
50
+ }
51
+ /** Noms logiques des entités du store (clés de lookup `getRepository`). */
52
+ export declare const AUDIT_ENTITY_NAMES: {
53
+ readonly events: "audit_event";
54
+ };
55
+ /**
56
+ * Construit les descripteurs d'entités du journal d'audit pour un ORM nommé.
57
+ *
58
+ * Le `connector` est **dynamique** (nom du connecteur de l'app, ex. `"nodefony"`) :
59
+ * le schéma est statique mais sa liaison à un ORM dépend de la config → pas
60
+ * d'`@entity` figé. Pas de dialecte (NoSQL) : le schéma est portable par
61
+ * construction. À enregistrer **avant** `orm.connect()` (modèle compilé au connect).
62
+ *
63
+ * `module: "security"` → la collection est regroupée sous @nodefony/security dans
64
+ * l'ERD Studio (l'audit est une feature security, hébergée par l'ORM).
65
+ *
66
+ * @param connector - clé de l'ORM cible dans le `ormRegistry`.
67
+ * @returns les descripteurs {@link IEntity} du journal d'audit.
68
+ */
69
+ export declare function createAuditEntities(connector: string): IEntity[];
70
+ /**
71
+ * Enregistre les entités du journal d'audit dans le `entityRegistry` pour un ORM
72
+ * donné. À appeler **avant** `orm.connect()` (le modèle est compilé au connect).
73
+ *
74
+ * @param connector - nom de la connexion cible (clé du registre).
75
+ */
76
+ export declare function registerAuditEntities(connector: string): void;
@@ -0,0 +1,75 @@
1
+ import type { SchemaDefinition } from "mongoose";
2
+ import type { IEntity } from "@nodefony/orm-core";
3
+ import type { IdempotentResponse } from "nodefony";
4
+ /**
5
+ * Schéma Mongoose du **store d'idempotence** — pendant documentaire de la table
6
+ * Drizzle `idempotency_key`, implémentation NoSQL d'`IIdempotencyStore` (contrat
7
+ * au CORE) pour dédoublonner les mutations rejouées PARTAGÉ cross-pod, là où le
8
+ * store mémoire par défaut reste affine à un pod.
9
+ *
10
+ * **Pourquoi MongoDB plutôt que Redis** : un cluster qui possède déjà une base
11
+ * Mongo mais pas de Redis obtient la dédup cross-pod sans nouvelle infra — le
12
+ * même argument qui fonde la variante SQL.
13
+ *
14
+ * ⚠️ **`_id` = la clé d'idempotence (String)** : c'est la contrainte d'unicité de
15
+ * la clé primaire qui porte la **réservation atomique** de `begin`
16
+ * (`findOneAndUpdate(… , {upsert:true})` → `E11000` pour le perdant). Faire
17
+ * porter l'unicité par un index secondaire serait un faux ami : Mongoose
18
+ * construit ses index en tâche de fond, donc la contrainte n'existerait pas
19
+ * pendant la fenêtre de construction, et deux `begin` concurrents renverraient
20
+ * tous deux `fresh` — exactement le double-effet que ce store existe pour
21
+ * empêcher. Sur `_id`, la contrainte existe dès le premier document.
22
+ *
23
+ * Le champ `key` est **dupliqué** à côté de `_id` : c'est lui que le vocabulaire
24
+ * public filtre (`q` = préfixe de clé) et que `listPage` rend. Les deux sont
25
+ * écrits ensemble, jamais séparément.
26
+ *
27
+ * ⚠️ **PAS de TTL index natif** (≠ `expireAfterSeconds`) : l'échéance d'une
28
+ * entrée n'est pas une durée fixe depuis sa création — c'est d'abord un **bail**
29
+ * *in-flight* (`now + lease`), puis une **rétention** (`now + ttl`) posée à la
30
+ * complétion. Un TTL index effacerait selon une règle qui ignore cette bascule.
31
+ * `expiresAt` est donc un `Number` (epoch ms) indexé, purgé par le `gc()`
32
+ * applicatif — même contrepartie que la variante SQL.
33
+ *
34
+ * `state` : `if` = réservation *in-flight* ; `done` = réponse mémorisée
35
+ * (rejouée en `replayed`). `response` = la réponse mémorisée, `null` tant
36
+ * qu'*in-flight* ; elle ne sort JAMAIS par `listPage` (anti-IDOR sur le cache).
37
+ */
38
+ export declare const idempotencyKeySchema: SchemaDefinition;
39
+ /**
40
+ * Forme plate d'une ligne du store d'idempotence telle que rendue par le
41
+ * repository Mongoose. Identique aux champs de {@link idempotencyKeySchema}
42
+ * → zéro mapping store ↔ entité.
43
+ */
44
+ export interface IdempotencyKeyRow {
45
+ key: string;
46
+ fingerprint: string;
47
+ state: "if" | "done";
48
+ response: IdempotentResponse | null;
49
+ expiresAt: number;
50
+ }
51
+ /** Nom logique de l'entité (clé de lookup `getRepository` / `entityRegistry`). */
52
+ export declare const IDEMPOTENCY_ENTITY_NAME: "idempotency_key";
53
+ /**
54
+ * Construit le descripteur d'entité du store d'idempotence pour un ORM nommé.
55
+ *
56
+ * Le `connector` est **dynamique** (nom du connecteur de l'app, ex. `"nodefony"`) :
57
+ * le schéma est statique mais sa liaison à un ORM dépend de la config → pas
58
+ * d'`@entity` figé. À enregistrer **avant** `orm.connect()` (le modèle est
59
+ * compilé au connect).
60
+ *
61
+ * `module: "framework"` → la collection est regroupée sous @nodefony/framework
62
+ * dans l'ERD Studio (l'idempotence est une feature framework — data plane admin
63
+ * + `@Idempotent` — simplement hébergée par l'ORM).
64
+ *
65
+ * @param connector - clé de l'ORM cible dans le `ormRegistry`.
66
+ * @returns les descripteurs {@link IEntity} du store d'idempotence.
67
+ */
68
+ export declare function createIdempotencyEntities(connector: string): IEntity[];
69
+ /**
70
+ * Enregistre l'entité du store d'idempotence dans le `entityRegistry` pour un
71
+ * ORM donné. À appeler **avant** `orm.connect()`.
72
+ *
73
+ * @param connector - nom de la connexion cible (clé du registre).
74
+ */
75
+ export declare function registerIdempotencyEntities(connector: string): void;
@@ -0,0 +1,75 @@
1
+ import type { SchemaDefinition } from "mongoose";
2
+ import type { IEntity } from "@nodefony/orm-core";
3
+ /**
4
+ * Algorithme HMAC du code TOTP (RFC 6238) — union littérale (miroir du
5
+ * `TotpAlgorithm` de `@nodefony/security`, non ré-exporté à la racine). Écrite en
6
+ * clair plutôt que dérivée d'`ITotpSecret["algorithm"]` : sinon le `.d.ts` généré
7
+ * référencerait un chemin interne du package security (non portable, TS2883).
8
+ */
9
+ type TotpAlgorithm = "SHA1" | "SHA256" | "SHA512";
10
+ /**
11
+ * Schéma Mongoose du **store de secrets TOTP** `@nodefony/security` (2FA) —
12
+ * pendant documentaire de la table Drizzle `totp_secret`, implémentation NoSQL
13
+ * d'`ITotpSecretStore`.
14
+ *
15
+ * ⚠️ **`_id` = `userId` (String)** : le modèle est **1 secret / utilisateur**, donc
16
+ * la clé naturelle EST l'utilisateur — `save` devient un upsert par `_id`, et
17
+ * l'unicité est portée par la clé primaire elle-même (aucun index à construire
18
+ * en tâche de fond, donc aucune fenêtre où la contrainte n'existerait pas).
19
+ *
20
+ * Le champ `userId` est **dupliqué** à côté de `_id` : c'est lui que le
21
+ * vocabulaire public trie et filtre (`q` = préfixe d'`userId`), et un document
22
+ * Mongo n'expose pas `_id` sous un autre nom. Les deux sont écrits ensemble par
23
+ * le store, jamais séparément.
24
+ *
25
+ * ⚠️ **`secretEnc` = secret déjà CHIFFRÉ** (AES-256-GCM par le service détenteur
26
+ * de la clé) : le store ne voit que des octets opaques, jamais le secret `K` en
27
+ * clair. Il ne sort JAMAIS par `listPage` (cf `ITotpEnrollmentSummary`).
28
+ *
29
+ * ⚠️ **Horodatages = `Number` (epoch ms), `timestamps:false`** — sauf
30
+ * `lastUsedStep`, qui est une **tranche `T`** RFC 6238 (`floor(epochSeconds /
31
+ * period)`), pas un horodatage.
32
+ *
33
+ * `recoveryCodes` = tableau de condensats `sha256` des codes NON consommés ; le
34
+ * store les réécrit en bloc (jamais de mutation partielle in-place).
35
+ */
36
+ export declare const totpSecretSchema: SchemaDefinition;
37
+ /**
38
+ * Forme **plate** d'une ligne de secret renvoyée par le repository Mongoose.
39
+ * Structure identique à `ITotpSecret` (tout `| null` déjà présent côté contrat)
40
+ * → mapping quasi-identité dans `MongooseTotpSecretStore`.
41
+ */
42
+ export interface TotpSecretRow {
43
+ userId: string;
44
+ secretEnc: string;
45
+ algorithm: TotpAlgorithm;
46
+ digits: number;
47
+ period: number;
48
+ recoveryCodes: string[];
49
+ confirmedAt: number | null;
50
+ lastUsedStep: number | null;
51
+ createdAt: number;
52
+ lastUsedAt: number | null;
53
+ }
54
+ /** Nom logique de l'entité (clé de lookup `getRepository`). */
55
+ export declare const TOTP_SECRET_ENTITY = "totp_secret";
56
+ /**
57
+ * Construit le descripteur d'entité du store de secrets TOTP pour un ORM nommé.
58
+ *
59
+ * Le `connector` est **dynamique** (nom du connecteur de l'app, ex. `"nodefony"`) :
60
+ * le schéma est statique mais sa liaison à un ORM dépend de la config → pas
61
+ * d'`@entity` figé. À enregistrer **avant** `orm.connect()` (le modèle est
62
+ * compilé au connect). `module: "security"` → regroupé sous @nodefony/security
63
+ * dans l'ERD Studio.
64
+ *
65
+ * @param connector - clé de l'ORM cible dans le `ormRegistry`.
66
+ */
67
+ export declare function createTotpSecretEntity(connector: string): IEntity;
68
+ /**
69
+ * Enregistre l'entité du store de secrets TOTP dans le `entityRegistry` pour un
70
+ * ORM donné. À appeler **avant** `orm.connect()`.
71
+ *
72
+ * @param connector - nom de la connexion cible (clé du registre).
73
+ */
74
+ export declare function registerTotpSecretEntity(connector: string): void;
75
+ export {};
@@ -7,9 +7,12 @@
7
7
  * modèles sont compilés à la connexion). Pas de dialecte (NoSQL) : les schémas
8
8
  * sont portables par construction.
9
9
  *
10
- * Couverture PARTIELLE assumée : session (auto via `@entity`), tokens, webauthn,
11
- * webhooks. PAS d'implémentation mongoose pour l'audit ni l'idempotence — les
12
- * sélectionner sur mongoose échoue franc (« store inconnu »), jamais en silence.
10
+ * Couverture : session (auto via `@entity`), utilisateurs, tokens, webauthn,
11
+ * webhooks, audit, TOTP et idempotence — soit **la totalité des briques
12
+ * durables**. C'est la propriété que Mongo doit tenir : une application doit
13
+ * pouvoir tourner SANS aucun backend SQL, et un backend durable est un chemin
14
+ * complet ou n'en est pas un. Une brique qui manquerait se sélectionnerait en
15
+ * échec franc (« store inconnu »), jamais en silence.
13
16
  *
14
17
  * Mêmes garde-fous que Drizzle : entité `has`-guarded (l'app garde la main),
15
18
  * fabrique `get`-guarded (premier-arrivé-premier-servi).
@@ -28,7 +28,7 @@ declare class MongooseService extends Service {
28
28
  */
29
29
  connectAll(connect?: boolean): Promise<void>;
30
30
  /** Assemble l'URI de connexion à partir de la config (`uri` ou composants). */
31
- static buildUri(cfg: IMongooseConnectorConfig): string;
31
+ static buildUri(cfg: Partial<IMongooseConnectorConfig>): string;
32
32
  /**
33
33
  * Options de connexion Mongoose d'un connecteur.
34
34
  *
@@ -45,7 +45,7 @@ declare class MongooseService extends Service {
45
45
  * @param cfg - configuration validée du connecteur.
46
46
  * @returns les options à passer à la connexion, ou `undefined` s'il n'y en a aucune.
47
47
  */
48
- static buildConnectOptions(cfg: IMongooseConnectorConfig): ConnectOptions | undefined;
48
+ static buildConnectOptions(cfg: Partial<IMongooseConnectorConfig>): ConnectOptions | undefined;
49
49
  /** Ferme toutes les connexions. */
50
50
  disconnectAll(): Promise<void>;
51
51
  /** Retourne l'ORM Mongoose d'un connecteur (défaut : `"nodefony"`). */