@nodefony/mongoose 10.0.0-alpha.9 → 10.0.0-beta.2
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/README.md +13 -12
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +5 -5
- package/dist/nodefony/config/config.js +1 -1
- package/dist/nodefony/config/defineModuleConfig.js +4 -3
- package/dist/nodefony/entity/sessionEntity.js +1 -1
- package/dist/nodefony/registerStores.js +2 -2
- package/dist/nodefony/service/MongooseService.js +9 -2
- package/dist/nodefony/src/MongooseAuditStore.js +4 -2
- package/dist/nodefony/src/MongooseIdempotencyStore.js +10 -6
- package/dist/nodefony/src/MongooseTokenStore.js +1 -1
- package/dist/nodefony/src/MongooseUserRepository.js +2 -2
- package/dist/nodefony/src/MongooseWebhookStore.js +1 -1
- package/dist/nodefony/src/orm-core/MongooseOrm.js +7 -11
- package/dist/nodefony/src/orm-core/MongooseRepository.js +41 -33
- package/dist/nodefony/src/writeCount.js +17 -0
- package/dist/types/nodefony/service/MongooseService.d.ts +2 -2
- package/dist/types/nodefony/src/MongooseUserRepository.d.ts +1 -1
- package/dist/types/nodefony/src/writeCount.d.ts +12 -0
- package/docs/configuration.md +32 -34
- package/docs/index.md +46 -40
- package/package.json +17 -17
package/README.md
CHANGED
|
@@ -55,18 +55,19 @@ durables sont portées, aucune ne retombe sur un autre backend. C'est la propri
|
|
|
55
55
|
de choisir sa base — un backend durable est un chemin complet, ou il oblige à en rapatrier un
|
|
56
56
|
second pour une seule brique.
|
|
57
57
|
|
|
58
|
-
| Brique | Ce qu'elle range | Comment l'activer
|
|
59
|
-
| ------------ | ----------------------------------------- |
|
|
60
|
-
| Session | sessions HTTP | `use("@nodefony/http", { session: { store: "mongoose" } })`
|
|
61
|
-
| Utilisateurs | l'annuaire | `
|
|
62
|
-
| Jetons | rafraîchissement, clés d'API, révocations | `tokenStore: { store: "mongoose" }`
|
|
63
|
-
| Passkeys | credentials WebAuthn | `passkeys: { store: "mongoose" }`
|
|
64
|
-
| TOTP | secrets du second facteur | `totp: { store: "mongoose" }`
|
|
65
|
-
| Audit | journal de sécurité append-only | `audit: { store: "mongoose" }`
|
|
66
|
-
| Webhooks | endpoints notifiés | `webhooks: { store: "mongoose" }`
|
|
67
|
-
| Idempotence | dédoublonnage des mutations rejouées | `
|
|
68
|
-
|
|
69
|
-
Les
|
|
58
|
+
| Brique | Ce qu'elle range | Comment l'activer |
|
|
59
|
+
| ------------ | ----------------------------------------- | -------------------------------------------------------------------- |
|
|
60
|
+
| Session | sessions HTTP | `use("@nodefony/http", { session: { store: "mongoose" } })` |
|
|
61
|
+
| Utilisateurs | l'annuaire | `provisionUsers` → `MongooseUserRepository.from(orm)` |
|
|
62
|
+
| Jetons | rafraîchissement, clés d'API, révocations | `tokenStore: { store: "mongoose" }` |
|
|
63
|
+
| Passkeys | credentials WebAuthn | `passkeys: { store: "mongoose" }` |
|
|
64
|
+
| TOTP | secrets du second facteur | `totp: { store: "mongoose" }` |
|
|
65
|
+
| Audit | journal de sécurité append-only | `audit: { store: "mongoose" }` |
|
|
66
|
+
| Webhooks | endpoints notifiés | `webhooks: { store: "mongoose" }` |
|
|
67
|
+
| Idempotence | dédoublonnage des mutations rejouées | `use("@nodefony/framework", { idempotency: { store: "mongoose" } })` |
|
|
68
|
+
|
|
69
|
+
Les configurations des jetons, passkeys, TOTP, audit et webhooks vivent sous
|
|
70
|
+
`use("@nodefony/security", …)`.
|
|
70
71
|
**Rien d'autre à écrire** : charger le module suffit à rendre ces backends sélectionnables, et
|
|
71
72
|
`store: "auto"` (le défaut) les choisit tout seul dès qu'une infra Mongo est déclarée
|
|
72
73
|
(`NF_DATABASE_URL=mongodb://…`).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,8 @@ import { MongooseAuditStore } from "./nodefony/src/MongooseAuditStore.js";
|
|
|
19
19
|
import { MongooseTotpSecretStore } from "./nodefony/src/MongooseTotpSecretStore.js";
|
|
20
20
|
import { MongooseIdempotencyStore } from "./nodefony/src/MongooseIdempotencyStore.js";
|
|
21
21
|
import { FRAMEWORK_CONNECTOR, registerMongooseFrameworkStores } from "./nodefony/registerStores.js";
|
|
22
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
23
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
22
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
|
|
23
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
24
24
|
import sessionEntity_default, { SESSION_CONNECTOR, sessionSchema as schema } from "./nodefony/entity/sessionEntity.js";
|
|
25
25
|
import SessionStorage from "./nodefony/src/SessionStorage.js";
|
|
26
26
|
import { MongooseUserRepository } from "./nodefony/src/MongooseUserRepository.js";
|
|
@@ -63,10 +63,10 @@ let Mongoose = class Mongoose extends Module {
|
|
|
63
63
|
* propre avec messages clairs si la config est invalide (convention Zod).
|
|
64
64
|
*/
|
|
65
65
|
async onKernelRegister() {
|
|
66
|
-
const validated = defineMongooseConfig(this.options
|
|
66
|
+
const validated = defineMongooseConfig(this.options);
|
|
67
67
|
this.options = validated;
|
|
68
68
|
registerUserStore("mongoose");
|
|
69
|
-
if (validated.frameworkEntities
|
|
69
|
+
if (validated.frameworkEntities) {
|
|
70
70
|
const report = registerMongooseFrameworkStores();
|
|
71
71
|
if (report.appOwned.length) this.log(`schéma framework : entités déjà enregistrées par l'app [${report.appOwned.join(", ")}] — auto-register respecte l'app`, "DEBUG");
|
|
72
72
|
if (report.registered.length) this.log(`schéma framework déclaré sur "${FRAMEWORK_CONNECTOR}" : [${report.registered.join(", ")}]`, "DEBUG");
|
|
@@ -85,7 +85,7 @@ let Mongoose = class Mongoose extends Module {
|
|
|
85
85
|
wireOrmAdminPlane(this.kernel);
|
|
86
86
|
registerErrorAdapter("mongoose", {
|
|
87
87
|
isError: (e) => e instanceof mongoose.Error,
|
|
88
|
-
errorToString: (e) =>
|
|
88
|
+
errorToString: (e) => e instanceof Error ? e.message : String(e)
|
|
89
89
|
});
|
|
90
90
|
return this;
|
|
91
91
|
}
|
|
@@ -46,7 +46,7 @@ const connectorSchema = z.strictObject({
|
|
|
46
46
|
const mongooseConfigSchema = z.strictObject({
|
|
47
47
|
debug: z.boolean().default(false).describe("Active la trace Mongoose des requêtes (`mongoose.set('debug')`). Défaut false. Mettre true en dev pour voir chaque opération. Surchargé par l'env `NF_MONGODB_DEBUG` (1/true). Reco prod : false."),
|
|
48
48
|
connectors: z.record(z.string(), connectorSchema).default(() => ({ nodefony: connectorSchema.parse({}) })).describe("Connexions indexées par nom (= clé dans le `ormRegistry`). Défaut : un connecteur `nodefony` sur `localhost:27017/nodefony`. Le nom `nodefony` (≠ `default` de Drizzle) évite toute collision d'entité dans le `entityRegistry` (process-wide) si les deux ORM cohabitent."),
|
|
49
|
-
frameworkEntities: z.boolean().default(true).describe("Déclare le schéma framework sur le connecteur `nodefony` (tokens, webauthn, webhooks — modèles compilés au connect) et rend les stores correspondants sélectionnables par nom (`mongoose`).
|
|
49
|
+
frameworkEntities: z.boolean().default(true).describe("Déclare le schéma framework sur le connecteur `nodefony` (tokens, webauthn, totp, audit, webhooks, idempotence — modèles compilés au connect) et rend les stores correspondants sélectionnables par nom (`mongoose`). `false` = module data-only.")
|
|
50
50
|
}).describe("Configuration de @nodefony/mongoose.");
|
|
51
51
|
/**
|
|
52
52
|
* Défauts du module, matérialisés depuis le schéma (source unique). Toujours
|
|
@@ -18,10 +18,11 @@ import { z } from "zod";
|
|
|
18
18
|
function applyEnvOverrides(config) {
|
|
19
19
|
const env = process.env;
|
|
20
20
|
const database = resolveInfra(env).database;
|
|
21
|
-
const uri = env.MONGODB_URI ?? (database
|
|
21
|
+
const uri = env.MONGODB_URI ?? (database?.family === "mongo" ? database.url : void 0);
|
|
22
22
|
if (uri) {
|
|
23
|
-
const target = config.connectors
|
|
24
|
-
|
|
23
|
+
const target = Object.hasOwn(config.connectors, "nodefony") ? "nodefony" : Object.keys(config.connectors).at(0);
|
|
24
|
+
const connector = target === void 0 ? void 0 : config.connectors[target];
|
|
25
|
+
if (connector !== void 0) connector.uri = uri;
|
|
25
26
|
}
|
|
26
27
|
if (env.NF_MONGODB_DEBUG === "1" || env.NF_MONGODB_DEBUG === "true") config.debug = true;
|
|
27
28
|
return config;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
2
2
|
import { entity } from "@nodefony/orm-core";
|
|
3
3
|
//#region nodefony/entity/sessionEntity.ts
|
|
4
4
|
/** ORM cible du stockage de session (connecteur par défaut du module). */
|
|
@@ -83,7 +83,7 @@ function registerMongooseFrameworkStores() {
|
|
|
83
83
|
if (getTokenStoreFactory("mongoose")) return;
|
|
84
84
|
registerTokenStore("mongoose", (ctx) => {
|
|
85
85
|
const orm = resolveConnectedOrm(`tokenStore "mongoose"`);
|
|
86
|
-
const days = ctx
|
|
86
|
+
const days = ctx.config.tokenStore.retentionRevokedDays;
|
|
87
87
|
return MongooseTokenStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
|
|
88
88
|
});
|
|
89
89
|
});
|
|
@@ -99,7 +99,7 @@ function registerMongooseFrameworkStores() {
|
|
|
99
99
|
if (getAuditStoreFactory("mongoose")) return;
|
|
100
100
|
registerAuditStore("mongoose", (ctx) => {
|
|
101
101
|
const orm = resolveConnectedOrm(`audit.store "mongoose"`);
|
|
102
|
-
const days = ctx
|
|
102
|
+
const days = ctx.config.audit.retentionDays;
|
|
103
103
|
return MongooseAuditStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
|
|
104
104
|
});
|
|
105
105
|
});
|
|
@@ -21,7 +21,7 @@ var MongooseService = class MongooseService extends Service {
|
|
|
21
21
|
/** ORM connectés, indexés par nom de connecteur. */
|
|
22
22
|
#orms = /* @__PURE__ */ new Map();
|
|
23
23
|
constructor(module) {
|
|
24
|
-
super(serviceName, module.container, module.notificationsCenter, module.options
|
|
24
|
+
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
25
25
|
this.module = module;
|
|
26
26
|
this.module.hookKernel("onBoot", async () => {
|
|
27
27
|
queryFlowMonitor.setEnabled(resolveOrmFlowEnabled(this.kernel));
|
|
@@ -34,7 +34,14 @@ var MongooseService = class MongooseService extends Service {
|
|
|
34
34
|
await this.disconnectAll().catch(() => {});
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Config du module telle que le service la TROUVE.
|
|
39
|
+
*
|
|
40
|
+
* Validée par Zod au boot du kernel ; mais le service se construit aussi hors
|
|
41
|
+
* kernel (bancs, usage direct) avec une config partielle, voire absente — et
|
|
42
|
+
* « config absente → rien à connecter » est un contrat couvert par
|
|
43
|
+
* `MongooseService.test.ts`. Le type le dit, donc chaque lecture garde.
|
|
44
|
+
*/
|
|
38
45
|
#config() {
|
|
39
46
|
return this.module.config;
|
|
40
47
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { writeCount } from "./writeCount.js";
|
|
1
2
|
import { AUDIT_ENTITY_NAMES } from "../entity/auditEventEntity.js";
|
|
2
3
|
import { assertPageQuery } from "nodefony";
|
|
3
4
|
//#region nodefony/src/MongooseAuditStore.ts
|
|
@@ -133,7 +134,7 @@ var MongooseAuditStore = class MongooseAuditStore {
|
|
|
133
134
|
const hasNext = docs.length > limit;
|
|
134
135
|
const rows = (hasNext ? docs.slice(0, limit) : docs).map((doc) => doc.toObject({ virtuals: true }));
|
|
135
136
|
const items = rows.map((row) => this.#toEvent(row));
|
|
136
|
-
const last = rows
|
|
137
|
+
const last = rows.at(-1);
|
|
137
138
|
return {
|
|
138
139
|
items,
|
|
139
140
|
limit,
|
|
@@ -161,7 +162,8 @@ var MongooseAuditStore = class MongooseAuditStore {
|
|
|
161
162
|
const model = this.#resolveModel();
|
|
162
163
|
if (!model) return 0;
|
|
163
164
|
const threshold = now - this.#retentionMs;
|
|
164
|
-
|
|
165
|
+
const result = await model.deleteMany({ ts: { $lt: threshold } }).exec();
|
|
166
|
+
return writeCount(result.deletedCount);
|
|
165
167
|
}
|
|
166
168
|
/** Compose le filtre Mongo des critères AND ; `{}` si aucun (= tout). */
|
|
167
169
|
#buildFilter(filter) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { writeCount } from "./writeCount.js";
|
|
1
2
|
import { IDEMPOTENCY_ENTITY_NAME } from "../entity/idempotencyEntity.js";
|
|
2
3
|
import { assertPageQuery, escapeRegExp } from "nodefony";
|
|
3
4
|
//#region nodefony/src/MongooseIdempotencyStore.ts
|
|
@@ -164,22 +165,24 @@ var MongooseIdempotencyStore = class MongooseIdempotencyStore {
|
|
|
164
165
|
async complete(key, response) {
|
|
165
166
|
const model = this.#resolveModel();
|
|
166
167
|
if (!model) return;
|
|
167
|
-
|
|
168
|
+
const result = await model.updateOne({
|
|
168
169
|
_id: key,
|
|
169
170
|
state: "if"
|
|
170
171
|
}, { $set: {
|
|
171
172
|
state: "done",
|
|
172
173
|
response,
|
|
173
174
|
expiresAt: this.#now() + this.#ttlMs
|
|
174
|
-
} }).exec()
|
|
175
|
+
} }).exec();
|
|
176
|
+
if (writeCount(result.modifiedCount) > 0) this.#dec();
|
|
175
177
|
}
|
|
176
178
|
async abort(key) {
|
|
177
179
|
const model = this.#resolveModel();
|
|
178
180
|
if (!model) return;
|
|
179
|
-
|
|
181
|
+
const result = await model.deleteOne({
|
|
180
182
|
_id: key,
|
|
181
183
|
state: "if"
|
|
182
|
-
}).exec()
|
|
184
|
+
}).exec();
|
|
185
|
+
if (writeCount(result.deletedCount) > 0) this.#dec();
|
|
183
186
|
}
|
|
184
187
|
/**
|
|
185
188
|
* Purge les entrées mortes (`expiresAt <= now`) — supplée l'absence de TTL
|
|
@@ -193,7 +196,8 @@ var MongooseIdempotencyStore = class MongooseIdempotencyStore {
|
|
|
193
196
|
async gc(now = this.#now()) {
|
|
194
197
|
const model = this.#resolveModel();
|
|
195
198
|
if (!model) return 0;
|
|
196
|
-
|
|
199
|
+
const result = await model.deleteMany({ expiresAt: { $lte: now } }).exec();
|
|
200
|
+
return writeCount(result.deletedCount);
|
|
197
201
|
}
|
|
198
202
|
/**
|
|
199
203
|
* {@inheritDoc IIdempotencyStore.listPage}
|
|
@@ -242,7 +246,7 @@ var MongooseIdempotencyStore = class MongooseIdempotencyStore {
|
|
|
242
246
|
items: page.map((doc) => ({
|
|
243
247
|
key: doc._id,
|
|
244
248
|
state: doc.state === "if" ? "in-flight" : "done",
|
|
245
|
-
expiresAtMs:
|
|
249
|
+
expiresAtMs: doc.expiresAt,
|
|
246
250
|
hasResponse: doc.state !== "if"
|
|
247
251
|
})),
|
|
248
252
|
total,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mongoOrder } from "./mongoOrder.js";
|
|
2
1
|
import { TOKEN_ENTITY_NAMES } from "../entity/tokenEntity.js";
|
|
2
|
+
import { mongoOrder } from "./mongoOrder.js";
|
|
3
3
|
import { assertPageQuery } from "nodefony";
|
|
4
4
|
import { paginate } from "@nodefony/orm-core";
|
|
5
5
|
import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS, tokenStatusCriteria } from "@nodefony/security";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mongoOrder, toMongoSort } from "./mongoOrder.js";
|
|
2
1
|
import { DOCUMENT_USER_COLUMNS } from "../entity/userEntity.js";
|
|
2
|
+
import { mongoOrder, toMongoSort } from "./mongoOrder.js";
|
|
3
3
|
import { assertPageQuery, escapeRegExp } from "nodefony";
|
|
4
4
|
import { BaseUser, USER_DEFAULT_ORDER, USER_SORTABLE_FIELDS, assertUserContract, attachExtraColumns } from "@nodefony/user";
|
|
5
5
|
//#region nodefony/src/MongooseUserRepository.ts
|
|
@@ -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
|
|
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();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mongoOrder, toMongoSort } from "./mongoOrder.js";
|
|
2
1
|
import { WEBHOOK_ENDPOINT_ENTITY } from "../entity/webhookEndpointEntity.js";
|
|
2
|
+
import { mongoOrder, toMongoSort } from "./mongoOrder.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
|
|
@@ -107,7 +107,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
|
|
|
107
107
|
switch (relation.type) {
|
|
108
108
|
case "one-to-many": {
|
|
109
109
|
const fk = relation.foreignKey ?? this.#foreignKey(entity.name);
|
|
110
|
-
if (
|
|
110
|
+
if (targetSchema.path(fk) === void 0) targetSchema.add({ [fk]: {
|
|
111
111
|
type: mongoose.Schema.Types.ObjectId,
|
|
112
112
|
ref: entity.name
|
|
113
113
|
} });
|
|
@@ -121,7 +121,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
|
|
|
121
121
|
case "many-to-one":
|
|
122
122
|
case "one-to-one": {
|
|
123
123
|
const fk = relation.foreignKey ?? relation.field;
|
|
124
|
-
if (
|
|
124
|
+
if (sourceSchema.path(fk) === void 0) sourceSchema.add({ [fk]: {
|
|
125
125
|
type: mongoose.Schema.Types.ObjectId,
|
|
126
126
|
ref: relation.target
|
|
127
127
|
} });
|
|
@@ -191,7 +191,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
|
|
|
191
191
|
for (const [name, model] of Object.entries(models)) {
|
|
192
192
|
const audit = {
|
|
193
193
|
entity: name,
|
|
194
|
-
collection: model.collection
|
|
194
|
+
collection: model.collection.name,
|
|
195
195
|
missing: [],
|
|
196
196
|
extra: []
|
|
197
197
|
};
|
|
@@ -279,7 +279,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
|
|
|
279
279
|
const listeners = [
|
|
280
280
|
["disconnected", lost("mongoose: disconnected")],
|
|
281
281
|
["close", lost("mongoose: close")],
|
|
282
|
-
["error", (
|
|
282
|
+
["error", (e) => this.connectionLost(`mongoose: ${e instanceof Error ? e.message : String(e)}`)],
|
|
283
283
|
["reconnected", () => this.connectionRestored()],
|
|
284
284
|
["connected", () => this.connectionRestored()]
|
|
285
285
|
];
|
|
@@ -332,7 +332,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
|
|
|
332
332
|
model.schema.eachPath((fieldPath, type) => {
|
|
333
333
|
const ref = type.options?.ref;
|
|
334
334
|
if (fieldPath === "_id" || type.instance !== "ObjectId" || typeof ref !== "string" || models[ref] === void 0) return;
|
|
335
|
-
|
|
335
|
+
index ??= Object.create(null);
|
|
336
336
|
(index[ref] ??= []).push({
|
|
337
337
|
model,
|
|
338
338
|
path: fieldPath,
|
|
@@ -346,7 +346,7 @@ var MongooseOrm = class MongooseOrm extends Orm {
|
|
|
346
346
|
getRepository(name) {
|
|
347
347
|
const model = this.#models?.[name];
|
|
348
348
|
if (!model) throw new Error(`MongooseOrm "${this.name}": no entity model registered under "${name}".`);
|
|
349
|
-
|
|
349
|
+
this.#repositories ??= Object.create(null);
|
|
350
350
|
let repository = this.#repositories[name];
|
|
351
351
|
if (repository === void 0) {
|
|
352
352
|
repository = new MongooseRepository(model, this.name, null, this.#referrers?.[name] ?? null);
|
|
@@ -358,15 +358,11 @@ var MongooseOrm = class MongooseOrm extends Orm {
|
|
|
358
358
|
const connection = this.#connection;
|
|
359
359
|
if (!connection) throw new Error(`MongooseOrm "${this.name}": not connected.`);
|
|
360
360
|
const session = await connection.startSession();
|
|
361
|
-
let result;
|
|
362
361
|
try {
|
|
363
|
-
await session.withTransaction(
|
|
364
|
-
result = await work(new MongooseTransaction(session));
|
|
365
|
-
});
|
|
362
|
+
return await session.withTransaction(() => work(new MongooseTransaction(session)));
|
|
366
363
|
} finally {
|
|
367
364
|
await session.endSession();
|
|
368
365
|
}
|
|
369
|
-
return result;
|
|
370
366
|
}
|
|
371
367
|
getNativeConnection() {
|
|
372
368
|
if (!this.#connection) throw new Error(`MongooseOrm "${this.name}": not connected.`);
|
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeCount } from "../writeCount.js";
|
|
2
2
|
import { RequestContext, redactSecrets } from "nodefony";
|
|
3
3
|
import { ReferencedEntityError, UnknownCriteriaField, assertOrderOption, isFieldOperators, isUpdateOperators, likePatternToRegExp, queryFlowMonitor } from "@nodefony/orm-core";
|
|
4
4
|
//#region nodefony/src/orm-core/MongooseRepository.ts
|
|
5
5
|
/**
|
|
6
|
-
* Clé Mongo d'un champ de TRI — `id` public → `_id` au repos.
|
|
7
|
-
*
|
|
8
|
-
* Mongo ne se plaint pas d'un tri sur un champ absent : il rend les documents
|
|
9
|
-
* dans un ordre arbitraire. `id` n'est qu'un virtuel de lecture ; sans cette
|
|
10
|
-
* traduction, `order: [["id", "DESC"]]` — le départage du tri par défaut de
|
|
11
|
-
* tout CRUD généré — serait silencieusement inerte ici et correct en SQL. Même
|
|
12
|
-
* table que les stores Mongo ({@link MONGO_ORDER_ALIASES}).
|
|
13
|
-
*/
|
|
14
|
-
function sortKey(field) {
|
|
15
|
-
return MONGO_ORDER_ALIASES[field] ?? field;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
6
|
* Repository portable (contrat {@link IRepository}) au-dessus d'un modèle Mongoose.
|
|
19
7
|
*
|
|
20
8
|
* Démontre la portabilité du contrat sur un **store documentaire hétérogène** :
|
|
@@ -48,6 +36,15 @@ var MongooseRepository = class MongooseRepository {
|
|
|
48
36
|
this.#referrers = referrers;
|
|
49
37
|
}
|
|
50
38
|
/**
|
|
39
|
+
* Options de session pour les méthodes dont le type refuse `session: null`
|
|
40
|
+
* (`updateMany`, `deleteMany`, `countDocuments`…) : la clé est ABSENTE hors
|
|
41
|
+
* transaction. Un objet neuf par appel, comme le littéral qu'il remplace —
|
|
42
|
+
* Mongoose peut enrichir l'objet d'options qu'il reçoit.
|
|
43
|
+
*/
|
|
44
|
+
#sessionOptions() {
|
|
45
|
+
return this.#session === null ? {} : { session: this.#session };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
51
48
|
* Tap dev-only : mesure la durée d'une opération et alimente **deux** sondes
|
|
52
49
|
* complémentaires (sans surcoût quand les deux sont inactives — flags lus avant
|
|
53
50
|
* toute allocation, prod = coût nul) :
|
|
@@ -135,14 +132,18 @@ var MongooseRepository = class MongooseRepository {
|
|
|
135
132
|
* donnait 0 résultat silencieux — et divergeait de Drizzle qui, lui, renvoyait
|
|
136
133
|
* tout). Échoue tôt et pareil sur les deux drivers.
|
|
137
134
|
*
|
|
138
|
-
*
|
|
135
|
+
* Sert AUSSI au tri : Mongo ne se plaint pas d'un `sort` sur un champ absent
|
|
136
|
+
* — il rend un ordre arbitraire, là où Drizzle refuse. `id` (virtuel de
|
|
137
|
+
* lecture) y devient `_id`, le départage du tri par défaut d'un CRUD généré.
|
|
138
|
+
*
|
|
139
|
+
* @param field - clé brute du critère ou du tri.
|
|
139
140
|
* @returns la clé Mongo résolue.
|
|
140
141
|
* @throws UnknownCriteriaField si le champ n'existe pas sur le schéma.
|
|
141
142
|
*/
|
|
142
143
|
#resolveField(field) {
|
|
143
144
|
const key = field === "id" ? "_id" : field;
|
|
144
145
|
const paths = this.#model.schema.paths;
|
|
145
|
-
if (key === "_id" || paths
|
|
146
|
+
if (key === "_id" || Object.hasOwn(paths, key) || Object.hasOwn(paths, key.split(".")[0] ?? key)) return key;
|
|
146
147
|
throw new UnknownCriteriaField(field, this.#model.modelName, Object.keys(paths));
|
|
147
148
|
}
|
|
148
149
|
/**
|
|
@@ -155,8 +156,13 @@ var MongooseRepository = class MongooseRepository {
|
|
|
155
156
|
for (const [field, value] of Object.entries(criteria)) {
|
|
156
157
|
if (field === "$or") {
|
|
157
158
|
if (!Array.isArray(value)) throw new TypeError(`MongooseRepository(${this.#model.modelName}): $or attend un tableau de critères.`);
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
if (value.length === 0) {
|
|
160
|
+
out.$and = [{ _id: { $in: [] } }];
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const branches = value.map((branch) => this.#filter(branch));
|
|
164
|
+
if (branches.some((f) => Object.keys(f).length === 0)) continue;
|
|
165
|
+
out.$or = branches;
|
|
160
166
|
continue;
|
|
161
167
|
}
|
|
162
168
|
const key = this.#resolveField(field);
|
|
@@ -177,7 +183,7 @@ var MongooseRepository = class MongooseRepository {
|
|
|
177
183
|
if (options?.relations?.length) query = query.populate(options.relations);
|
|
178
184
|
if (options?.offset !== void 0) query = query.skip(options.offset);
|
|
179
185
|
if (options?.limit !== void 0) query = query.limit(options.limit);
|
|
180
|
-
if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [
|
|
186
|
+
if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [this.#resolveField(field), dir === "DESC" ? -1 : 1])));
|
|
181
187
|
return (await query.exec()).map((doc) => this.#plain(doc));
|
|
182
188
|
}, (docs) => docs.length);
|
|
183
189
|
}
|
|
@@ -188,21 +194,22 @@ var MongooseRepository = class MongooseRepository {
|
|
|
188
194
|
let query = this.#model.findOne(filter);
|
|
189
195
|
if (this.#session) query = query.session(this.#session);
|
|
190
196
|
if (options?.relations?.length) query = query.populate(options.relations);
|
|
191
|
-
if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [
|
|
197
|
+
if (options?.order?.length) query = query.sort(Object.fromEntries(options.order.map(([field, dir]) => [this.#resolveField(field), dir === "DESC" ? -1 : 1])));
|
|
192
198
|
const doc = await query.exec();
|
|
193
199
|
return doc ? this.#plain(doc) : null;
|
|
194
200
|
}, (doc) => doc ? 1 : 0);
|
|
195
201
|
}
|
|
196
202
|
async create(data) {
|
|
197
203
|
return this.#prof(() => this.#descr("create"), async () => {
|
|
198
|
-
const [doc] = await this.#model.create([data], { session: this.#session
|
|
204
|
+
const [doc] = await this.#model.create([data], { session: this.#session });
|
|
205
|
+
if (doc === void 0) throw new Error(`create: le pilote n'a rendu aucun document (${this.#model.modelName})`);
|
|
199
206
|
return this.#plain(doc);
|
|
200
207
|
}, () => 1);
|
|
201
208
|
}
|
|
202
209
|
async createMany(data) {
|
|
203
210
|
if (data.length === 0) return [];
|
|
204
211
|
return this.#prof(() => this.#descr("insertMany"), async () => {
|
|
205
|
-
return (await this.#model.insertMany(data, { session: this.#session
|
|
212
|
+
return (await this.#model.insertMany(data, { session: this.#session })).map((doc) => this.#plain(doc));
|
|
206
213
|
}, (rows) => rows.length);
|
|
207
214
|
}
|
|
208
215
|
async updateOne(criteria, data) {
|
|
@@ -210,7 +217,7 @@ var MongooseRepository = class MongooseRepository {
|
|
|
210
217
|
return this.#prof(() => this.#descr("findOneAndUpdate", filter), async () => {
|
|
211
218
|
const doc = await this.#model.findOneAndUpdate(filter, data, {
|
|
212
219
|
returnDocument: "after",
|
|
213
|
-
session: this.#session
|
|
220
|
+
session: this.#session
|
|
214
221
|
}).exec();
|
|
215
222
|
return doc ? this.#plain(doc) : null;
|
|
216
223
|
}, (doc) => doc ? 1 : 0);
|
|
@@ -224,9 +231,8 @@ var MongooseRepository = class MongooseRepository {
|
|
|
224
231
|
}, {
|
|
225
232
|
upsert: true,
|
|
226
233
|
returnDocument: "after",
|
|
227
|
-
session: this.#session
|
|
234
|
+
session: this.#session
|
|
228
235
|
}).exec();
|
|
229
|
-
if (!doc) throw new Error("MongooseRepository.upsert: aucun document retourné");
|
|
230
236
|
return this.#plain(doc);
|
|
231
237
|
}, () => 1);
|
|
232
238
|
}
|
|
@@ -260,7 +266,8 @@ var MongooseRepository = class MongooseRepository {
|
|
|
260
266
|
async updateMany(criteria, data) {
|
|
261
267
|
const filter = this.#filter(criteria);
|
|
262
268
|
return this.#prof(() => this.#descr("updateMany", filter), async () => {
|
|
263
|
-
|
|
269
|
+
const res = await this.#model.updateMany(filter, data, this.#sessionOptions());
|
|
270
|
+
return writeCount(res.modifiedCount);
|
|
264
271
|
}, (n) => n);
|
|
265
272
|
}
|
|
266
273
|
async increment(criteria, changes) {
|
|
@@ -268,7 +275,7 @@ var MongooseRepository = class MongooseRepository {
|
|
|
268
275
|
return this.#prof(() => this.#descr("findOneAndUpdate", filter), async () => {
|
|
269
276
|
const doc = await this.#model.findOneAndUpdate(filter, { $inc: changes }, {
|
|
270
277
|
returnDocument: "after",
|
|
271
|
-
session: this.#session
|
|
278
|
+
session: this.#session
|
|
272
279
|
}).exec();
|
|
273
280
|
return doc ? this.#plain(doc) : null;
|
|
274
281
|
}, (doc) => doc ? 1 : 0);
|
|
@@ -281,7 +288,8 @@ var MongooseRepository = class MongooseRepository {
|
|
|
281
288
|
filter = { _id: { $in: ids } };
|
|
282
289
|
}
|
|
283
290
|
const removed = await this.#prof(() => this.#descr("deleteMany", filter), async () => {
|
|
284
|
-
|
|
291
|
+
const res = await this.#model.deleteMany(filter, this.#sessionOptions());
|
|
292
|
+
return writeCount(res.deletedCount);
|
|
285
293
|
}, (n) => n);
|
|
286
294
|
if (ids !== null && removed > 0) await this.#releaseOptional(ids);
|
|
287
295
|
return removed;
|
|
@@ -294,7 +302,8 @@ var MongooseRepository = class MongooseRepository {
|
|
|
294
302
|
filter = { _id: ids[0] };
|
|
295
303
|
}
|
|
296
304
|
const removed = await this.#prof(() => this.#descr("deleteOne", filter), async () => {
|
|
297
|
-
|
|
305
|
+
const res = await this.#model.deleteOne(filter, this.#sessionOptions());
|
|
306
|
+
return writeCount(res.deletedCount) > 0;
|
|
298
307
|
}, (ok) => ok ? 1 : 0);
|
|
299
308
|
if (ids !== null && removed) await this.#releaseOptional(ids);
|
|
300
309
|
return removed;
|
|
@@ -307,7 +316,7 @@ var MongooseRepository = class MongooseRepository {
|
|
|
307
316
|
filter = { _id: ids[0] };
|
|
308
317
|
}
|
|
309
318
|
const doc = await this.#prof(() => this.#descr("findOneAndDelete", filter), async () => {
|
|
310
|
-
const found = await this.#model.findOneAndDelete(filter, { session: this.#session
|
|
319
|
+
const found = await this.#model.findOneAndDelete(filter, { session: this.#session }).exec();
|
|
311
320
|
return found ? this.#plain(found) : null;
|
|
312
321
|
}, (d) => d ? 1 : 0);
|
|
313
322
|
if (ids !== null && doc !== null) await this.#releaseOptional(ids);
|
|
@@ -337,7 +346,7 @@ var MongooseRepository = class MongooseRepository {
|
|
|
337
346
|
async #referencedTargets(filter, single) {
|
|
338
347
|
const referrers = this.#referrers;
|
|
339
348
|
if (referrers === null) return null;
|
|
340
|
-
const session = this.#session
|
|
349
|
+
const session = this.#session;
|
|
341
350
|
const query = this.#model.find(filter, { _id: 1 }, { session }).lean();
|
|
342
351
|
if (single) query.limit(1);
|
|
343
352
|
const docs = await query.exec();
|
|
@@ -360,15 +369,14 @@ var MongooseRepository = class MongooseRepository {
|
|
|
360
369
|
* @param ids - `_id` des documents supprimés.
|
|
361
370
|
*/
|
|
362
371
|
async #releaseOptional(ids) {
|
|
363
|
-
const session = this.#session ?? void 0;
|
|
364
372
|
for (const referrer of this.#referrers ?? []) {
|
|
365
373
|
if (referrer.required) continue;
|
|
366
|
-
await referrer.model.updateMany({ [referrer.path]: { $in: ids } }, { $set: { [referrer.path]: null } },
|
|
374
|
+
await referrer.model.updateMany({ [referrer.path]: { $in: ids } }, { $set: { [referrer.path]: null } }, this.#sessionOptions());
|
|
367
375
|
}
|
|
368
376
|
}
|
|
369
377
|
async count(criteria) {
|
|
370
378
|
const filter = this.#filter(criteria);
|
|
371
|
-
return this.#prof(() => this.#descr("countDocuments", filter), () => this.#model.countDocuments(filter,
|
|
379
|
+
return this.#prof(() => this.#descr("countDocuments", filter), () => this.#model.countDocuments(filter, this.#sessionOptions()));
|
|
372
380
|
}
|
|
373
381
|
/**
|
|
374
382
|
* `COUNT(DISTINCT …)` en agrégation — `$group` puis `$count`, donc la
|
|
@@ -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 };
|
|
@@ -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"`). */
|
|
@@ -56,7 +56,7 @@ export declare class MongooseUserRepository implements IUserRepository {
|
|
|
56
56
|
updateMany(criteria: Criteria<IPasswordAuthenticatedUser>, data: Partial<IPasswordAuthenticatedUser>): Promise<number>;
|
|
57
57
|
delete(criteria: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
|
|
58
58
|
count(criteria?: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
|
|
59
|
-
countDistinct(field: keyof IPasswordAuthenticatedUser
|
|
59
|
+
countDistinct(field: keyof IPasswordAuthenticatedUser, criteria?: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
|
|
60
60
|
withTransaction(tx: ITransaction): IUserRepository;
|
|
61
61
|
findByIdentifier(identifier: string): Promise<IPasswordAuthenticatedUser | null>;
|
|
62
62
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compte rendu par une écriture MongoDB (`deletedCount`, `modifiedCount`), 0 s'il manque.
|
|
3
|
+
*
|
|
4
|
+
* Le pilote le type `number`, mais le recopie de la réponse du serveur
|
|
5
|
+
* (`res.n`) : une écriture NON acquittée (`w: 0`) n'en reçoit aucune, et le
|
|
6
|
+
* compte arrive `undefined`. Sans ce repli, un `gc()` rendrait `undefined` et
|
|
7
|
+
* un `> 0` conclurait « rien écrit » pour une raison qui n'est pas la bonne.
|
|
8
|
+
*
|
|
9
|
+
* @param count - le compte tel que le pilote le rend.
|
|
10
|
+
* @returns le compte, ou 0 quand le serveur n'en a rendu aucun.
|
|
11
|
+
*/
|
|
12
|
+
export declare function writeCount(count: number | undefined): number;
|
package/docs/configuration.md
CHANGED
|
@@ -267,7 +267,7 @@ ainsi qu'une application lit une base métier et écrit dans une base d'archives
|
|
|
267
267
|
L'adresse se donne de deux façons, jamais les deux à la fois utilement :
|
|
268
268
|
|
|
269
269
|
- **En pièces détachées** — `host`, `port`, `dbname` : lisible, adapté au développement. Le service
|
|
270
|
-
les assemble en `mongodb://hôte:port/base` (`MongooseService.buildUri()` (`MongooseService.ts:
|
|
270
|
+
les assemble en `mongodb://hôte:port/base` (`MongooseService.buildUri()` (`MongooseService.ts:117`)).
|
|
271
271
|
- **En une URI** — `uri` (`config.ts:41`) : la seule forme capable d'exprimer un replica set, un
|
|
272
272
|
`mongodb+srv`, des options de requête. **Dès qu'`uri` est présent, les trois autres champs ne sont
|
|
273
273
|
plus lus du tout.**
|
|
@@ -295,7 +295,7 @@ publique** de ton application : le changer déplace des entités.
|
|
|
295
295
|
### `options` — ce que Mongoose sait faire et Nodefony ne re-décrit pas
|
|
296
296
|
|
|
297
297
|
`options` (`config.ts:86`) est transmis tel quel au driver
|
|
298
|
-
(`MongooseService.#connectOne()` (`MongooseService.ts:
|
|
298
|
+
(`MongooseService.#connectOne()` (`MongooseService.ts:156`)). On y met tout ce qui touche au
|
|
299
299
|
**transport** plutôt qu'à l'adresse : taille du pool, délais, TLS, identifiants séparés.
|
|
300
300
|
|
|
301
301
|
```typescript
|
|
@@ -320,7 +320,7 @@ description des options du driver.
|
|
|
320
320
|
|
|
321
321
|
### `debug` — voir passer les requêtes
|
|
322
322
|
|
|
323
|
-
`debug` (`config.ts:100`) active la trace intégrée de Mongoose (`connectAll()` (`MongooseService.ts:
|
|
323
|
+
`debug` (`config.ts:100`) active la trace intégrée de Mongoose (`connectAll()` (`MongooseService.ts:104`)).
|
|
324
324
|
Chaque opération part sur la sortie standard, avec sa collection, son filtre et ses champs.
|
|
325
325
|
|
|
326
326
|
C'est un **réglage de processus, pas de connecteur** : il vaut pour toutes les connexions à la fois.
|
|
@@ -334,10 +334,12 @@ C'est le champ le plus discret et le plus structurant (`frameworkEntities` (`con
|
|
|
334
334
|
défaut `true`, le module fait deux choses de plus qu'ouvrir des connexions, dès son enregistrement et
|
|
335
335
|
**avant** que la connexion ne s'ouvre (`Mongoose.onKernelRegister()` (`mongoose/index.ts:65`)) :
|
|
336
336
|
|
|
337
|
-
1. il **déclare les entités du framework** sur le connecteur `nodefony` — jetons, passkeys,
|
|
337
|
+
1. il **déclare les entités du framework** sur le connecteur `nodefony` — jetons, passkeys, TOTP,
|
|
338
|
+
audit, webhooks, idempotence —
|
|
338
339
|
pour que leurs modèles soient compilés au moment de la connexion ;
|
|
339
340
|
2. il **enregistre les fabriques** correspondantes dans les registres de `@nodefony/security`, ce qui
|
|
340
|
-
rend le nom `"mongoose"` sélectionnable dans `tokenStore`, `passkeys`, `webhooks`
|
|
341
|
+
rend le nom `"mongoose"` sélectionnable dans `tokenStore`, `passkeys`, `totp`, `audit`, `webhooks`
|
|
342
|
+
et `idempotency`
|
|
341
343
|
(`registerMongooseFrameworkStores()` (`registerStores.ts:126`)).
|
|
342
344
|
|
|
343
345
|
Le passer à `false` transforme le module en **pur driver de données** : tes entités à toi, rien
|
|
@@ -395,7 +397,7 @@ Trois précisions qui évitent les mauvaises surprises :
|
|
|
395
397
|
### L'override générique du framework
|
|
396
398
|
|
|
397
399
|
Toute clé de config d'un module se surcharge par `NF__<MODULE>__<CHEMIN>`, le double tiret bas
|
|
398
|
-
séparant les niveaux (`parseNfEnvOverrides()` (`envOverride.ts:
|
|
400
|
+
séparant les niveaux (`parseNfEnvOverrides()` (`envOverride.ts:92`)). Le segment de module est le nom
|
|
399
401
|
court : `MONGOOSE`.
|
|
400
402
|
|
|
401
403
|
```bash
|
|
@@ -405,7 +407,7 @@ NF__MONGOOSE__CONNECTORS__NODEFONY__DBNAME=recette # champ imbriqué
|
|
|
405
407
|
NF__MONGOOSE__CONNECTORS__NODEFONY__PORT=27018 # coercé en nombre
|
|
406
408
|
```
|
|
407
409
|
|
|
408
|
-
Ces overrides sont posés **avant** la validation Zod (`Kernel.applyEnvConfigOverrides()` (`Kernel.ts:
|
|
410
|
+
Ces overrides sont posés **avant** la validation Zod (`Kernel.applyEnvConfigOverrides()` (`Kernel.ts:1895`)) :
|
|
409
411
|
une valeur aberrante est donc rejetée comme si tu l'avais écrite dans ton fichier. C'est voulu — un
|
|
410
412
|
réglage d'environnement invalide doit casser aussi fort qu'un réglage de code.
|
|
411
413
|
|
|
@@ -436,7 +438,7 @@ De la plus faible à la plus forte priorité :
|
|
|
436
438
|
|
|
437
439
|
1. **Les défauts du schéma** — `localhost:27017/nodefony`, `debug: false`, `frameworkEntities: true`.
|
|
438
440
|
2. **Ta config d'app** — `use("@nodefony/mongoose", { … })`, fusionnée en profondeur sous les défauts
|
|
439
|
-
(`Kernel.loadModulesFromManifest()` (`Kernel.ts:
|
|
441
|
+
(`Kernel.loadModulesFromManifest()` (`Kernel.ts:1714`)).
|
|
440
442
|
3. **Un override venu d'un autre module** — la clé `module-mongoose` dans la config d'un module tiers.
|
|
441
443
|
4. **`NF__MONGOOSE__…`** — l'override générique d'environnement.
|
|
442
444
|
5. **La validation Zod** — types, bornes, défauts des champs restés absents.
|
|
@@ -549,9 +551,9 @@ use("@nodefony/mongoose", {
|
|
|
549
551
|
```
|
|
550
552
|
|
|
551
553
|
Les deux connexions s'ouvrent en série au démarrage, dans l'ordre de déclaration
|
|
552
|
-
(`connectAll()` (`MongooseService.ts:
|
|
553
|
-
(`disconnectAll()` (`MongooseService.ts:
|
|
554
|
-
(`getOrm()` (`MongooseService.ts:
|
|
554
|
+
(`connectAll()` (`MongooseService.ts:104`)), et se ferment toutes à l'arrêt
|
|
555
|
+
(`disconnectAll()` (`MongooseService.ts:210`)). Un service peut demander l'une ou l'autre par son nom
|
|
556
|
+
(`getOrm()` (`MongooseService.ts:218`)), mais l'usage courant reste le registre d'ORM.
|
|
555
557
|
|
|
556
558
|
## 🔐 Le secret de connexion
|
|
557
559
|
|
|
@@ -568,7 +570,7 @@ données d'un coup. La règle est donc sans nuance.
|
|
|
568
570
|
- **Les identifiants passés par `options`** (`user`, `pass`) suivent la même règle : ils viennent de
|
|
569
571
|
l'environnement, pas du fichier. Le framework rédige d'ailleurs la valeur de tout override
|
|
570
572
|
d'environnement dont le chemin ressemble à un secret, avant de le journaliser
|
|
571
|
-
(`pathLooksSecret()` (`envOverride.ts:
|
|
573
|
+
(`pathLooksSecret()` (`envOverride.ts:417`)).
|
|
572
574
|
|
|
573
575
|
> [!TIP]
|
|
574
576
|
> Vérifie ta redaction en une commande : démarre l'application et lis la ligne de connexion. Elle doit
|
|
@@ -653,7 +655,7 @@ Un port hors bornes, un type erroné, un champ vide : le démarrage s'arrête av
|
|
|
653
655
|
le champ**, pas une pile d'appels.
|
|
654
656
|
|
|
655
657
|
```
|
|
656
|
-
[@nodefony/mongoose]
|
|
658
|
+
[@nodefony/mongoose] configuration invalide — connectors.x.port : Too small: expected number to be >=1
|
|
657
659
|
```
|
|
658
660
|
|
|
659
661
|
Le message est assemblé à partir des remontées de validation, chemin de champ compris
|
|
@@ -665,24 +667,21 @@ coûteux à diagnostiquer qu'un serveur qui refuse de démarrer.
|
|
|
665
667
|
|
|
666
668
|
Le cas courant : la config est parfaite, mais Mongo n'est pas joignable — conteneur pas encore prêt,
|
|
667
669
|
réseau coupé, identifiants périmés. Le comportement **dépend de l'environnement**, arbitré par la
|
|
668
|
-
politique de boot du cœur (`Kernel.isBootErrorFatal()` (`Kernel.ts:
|
|
670
|
+
politique de boot du cœur (`Kernel.isBootErrorFatal()` (`Kernel.ts:3221`)) :
|
|
669
671
|
|
|
670
|
-
| Environnement
|
|
671
|
-
|
|
|
672
|
-
| Développement, test | `WARNING`, l'échec est agrégé au bilan de démarrage, **le serveur démarre quand même** |
|
|
673
|
-
|
|
|
672
|
+
| Environnement | Ce qui se passe |
|
|
673
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
674
|
+
| Développement, test **et production** | `WARNING`, l'échec est agrégé au bilan de démarrage, **le serveur démarre quand même** (module `critical = false`) |
|
|
675
|
+
| Toute configuration invalide | Fatale partout : une `BootConfigurationError` ne se répare pas en attendant |
|
|
674
676
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
tomber pour le relancer, et c'est le modèle cloud-native que le framework applique partout.
|
|
677
|
+
Un pod qui démarre sans sa base n'est pas pour autant mis en service à l'aveugle : c'est la sonde de
|
|
678
|
+
disponibilité (`/readyz`) qui dit à l'orchestrateur de ne pas lui envoyer de trafic.
|
|
678
679
|
|
|
679
680
|
> [!IMPORTANT]
|
|
680
|
-
> **Le module est déclaré non critique** (`Mongoose.critical` (`mongoose/index.ts:48`)), et
|
|
681
|
-
>
|
|
682
|
-
> par
|
|
683
|
-
>
|
|
684
|
-
> le démarrage. Si tu attends l'inverse — un serveur qui démarre sans sa base et se rattrape plus
|
|
685
|
-
> tard — ne compte pas dessus : prévois une sonde de disponibilité côté orchestrateur.
|
|
681
|
+
> **Le module est déclaré non critique** (`Mongoose.critical` (`mongoose/index.ts:48`)), et l'ouverture
|
|
682
|
+
> de la connexion est posée par `module.hookKernel("onBoot", …)` (`MongooseService.ts:59`), donc
|
|
683
|
+
> couverte par cette étiquette : même en production, une base injoignable n'interrompt pas le
|
|
684
|
+
> démarrage. Prévois une sonde de disponibilité côté orchestrateur.
|
|
686
685
|
|
|
687
686
|
### Pendant l'arrêt du serveur
|
|
688
687
|
|
|
@@ -697,7 +696,7 @@ vaut mieux qu'une erreur 500 et un rejet non capturé. À l'inverse, une entité
|
|
|
697
696
|
La configuration ne se contente pas d'être validée : elle se **montre**.
|
|
698
697
|
|
|
699
698
|
- Le module publie son schéma en JSON Schema (`Mongoose.configSchema()` (`mongoose/index.ts:55`) →
|
|
700
|
-
`mongooseConfigJsonSchema()` (`defineModuleConfig.ts:
|
|
699
|
+
`mongooseConfigJsonSchema()` (`defineModuleConfig.ts:78`)). C'est ce qui permet à Studio d'afficher
|
|
701
700
|
chaque clé avec son type, son défaut et son texte d'aide — sans qu'une seule ligne de description
|
|
702
701
|
soit recopiée quelque part.
|
|
703
702
|
- L'écran `/nodefony/config` montre la configuration **effective** après toutes les couches, et la
|
|
@@ -739,9 +738,8 @@ jamais figés ici.
|
|
|
739
738
|
bancs) : l'assemblage d'URI, l'ouverture et la fermeture des connexions, puis tout le reste du
|
|
740
739
|
module — contrat ORM, session, jetons, passkeys, webhooks, utilisateurs.
|
|
741
740
|
|
|
742
|
-
|
|
743
|
-
(`
|
|
744
|
-
dédiée `MONGODB_URI` en a un. Il n'y a pas non plus de test de charge ni de mesure mémoire dédiés au
|
|
741
|
+
La branche infra (`NF_DATABASE_URL` de famille mongo) et `MONGODB_URI` ont chacune un test unitaire
|
|
742
|
+
(`tests/unit/config.test.ts`). Ce qui **n'est pas** couvert, dit franchement : il n'y a pas de test de charge ni de mesure mémoire dédiés au
|
|
745
743
|
driver.
|
|
746
744
|
|
|
747
745
|
> [!WARNING]
|
|
@@ -753,9 +751,9 @@ driver.
|
|
|
753
751
|
> que la base était là : soit `NF_MONGO_TEST_URI` pointe un conteneur
|
|
754
752
|
> (`docker run -p 27017:27017 mongo:7`), soit le serveur en mémoire a démarré.
|
|
755
753
|
>
|
|
756
|
-
> Le catalogue des variables d'infrastructure du dépôt est `vitest.gates.ts`, à la racine. Ce module
|
|
757
|
-
>
|
|
758
|
-
>
|
|
754
|
+
> Le catalogue des variables d'infrastructure du dépôt est `vitest.gates.ts`, à la racine. Ce module y
|
|
755
|
+
> déclare sa porte `MONGO_GATE` : le rapporteur de fin de course nomme la cible Mongo non exercée, avec
|
|
756
|
+
> la commande exacte pour la monter.
|
|
759
757
|
|
|
760
758
|
Couverture : `npm run coverage` dans `@nodefony/mongoose`.
|
|
761
759
|
|
package/docs/index.md
CHANGED
|
@@ -30,8 +30,8 @@ coverageFiles: MongooseOrm.ts,MongooseRepository.ts,SessionStorage.ts,MongooseTo
|
|
|
30
30
|
|
|
31
31
|
> Le module qui fait parler ton application à **MongoDB**. Il ouvre les connexions au démarrage,
|
|
32
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
|
|
34
|
-
> Mongo — sessions, utilisateurs, jetons, passkeys, webhooks. Ce qu'il ne porte pas, il le dit :
|
|
33
|
+
> tourne sur Mongo ou sur SQL. Il fournit aussi huit **briques du framework** prêtes à l'emploi sur
|
|
34
|
+
> Mongo — sessions, utilisateurs, jetons, passkeys, TOTP, audit, webhooks, idempotence. Ce qu'il ne porte pas, il le dit :
|
|
35
35
|
> la couverture est **adaptée à la vocation de MongoDB**, jamais une course à la parité avec SQL.
|
|
36
36
|
|
|
37
37
|
📍 [Documentation](../../../../../docs/index.md) › **MongoDB (Mongoose)**
|
|
@@ -139,7 +139,7 @@ flowchart TD
|
|
|
139
139
|
ENT["Tes entités<br/>defineEntity + @entities"] --> MODEL
|
|
140
140
|
|
|
141
141
|
SESS["session"] -.->|store: mongoose| MR
|
|
142
|
-
TOK["tokens · passkeys · webhooks"] -.->|store: mongoose| MR
|
|
142
|
+
TOK["tokens · passkeys · totp · audit · webhooks · idempotence"] -.->|store: mongoose| MR
|
|
143
143
|
USR["users"] -.->|provisionUsers| MR
|
|
144
144
|
```
|
|
145
145
|
|
|
@@ -231,12 +231,12 @@ réellement chargés :
|
|
|
231
231
|
| Ta situation | Ce que `auto` choisit |
|
|
232
232
|
| ------------------------------------------------------- | -------------------------------------------------------- |
|
|
233
233
|
| `NF_DATABASE_URL=mongodb://…` + module mongoose chargé | `mongoose` — « infra database (mongodb) » |
|
|
234
|
-
|
|
|
234
|
+
| Une brique que le backend d'infra ne porte pas | repli annoncé (`memory`), avec la **raison** dans le log |
|
|
235
235
|
| Aucune infra déclarée, mongoose chargé (et pas drizzle) | `mongoose` — backend local persistant |
|
|
236
236
|
| `NF_REDIS_URL` déclaré, brique non durable (session…) | `redis` d'abord (le cache passe avant la base) |
|
|
237
237
|
|
|
238
238
|
Rien n'est jamais dégradé en silence : la raison du choix est journalisée. Et si tu nommes
|
|
239
|
-
**explicitement** un store qui n'existe pas (`audit: { store: "
|
|
239
|
+
**explicitement** un store qui n'existe pas (`audit: { store: "mongo" }`), l'échec est franc —
|
|
240
240
|
boot avorté en production, brique désactivée avec un log `CRITIC` en développement. Un store durable
|
|
241
241
|
ne retombe **jamais** en mémoire sans le dire.
|
|
242
242
|
|
|
@@ -469,7 +469,7 @@ sequenceDiagram
|
|
|
469
469
|
|
|
470
470
|
K->>M: onKernelRegister
|
|
471
471
|
M->>M: valider la config (Zod) + geler
|
|
472
|
-
M->>M: déclarer les entités framework<br/>(tokens · passkeys · webhooks)
|
|
472
|
+
M->>M: déclarer les entités framework<br/>(tokens · passkeys · totp · audit · webhooks · idempotence)
|
|
473
473
|
M->>M: déclarer "mongoose" comme backend utilisateur
|
|
474
474
|
K->>M: onKernelBoot
|
|
475
475
|
M->>M: monter le data plane ORM + l'adapter d'erreurs
|
|
@@ -492,8 +492,8 @@ global de Mongoose : c'est ce qui permet à plusieurs bases — voire plusieurs
|
|
|
492
492
|
le même processus.
|
|
493
493
|
|
|
494
494
|
Le service orchestre ce cycle de bout en bout : il ouvre une connexion par connecteur déclaré au
|
|
495
|
-
démarrage (`MongooseService.connectAll()` (`MongooseService.ts:
|
|
496
|
-
(`MongooseService.disconnectAll()` (`MongooseService.ts:
|
|
495
|
+
démarrage (`MongooseService.connectAll()` (`MongooseService.ts:104`)) et referme tout à l'arrêt
|
|
496
|
+
(`MongooseService.disconnectAll()` (`MongooseService.ts:210`)). Le module se déclare **non critique**
|
|
497
497
|
(`Mongoose.critical` (`mongoose/index.ts:48`)) : une base injoignable ne tue pas le processus —
|
|
498
498
|
l'application monte quand même, l'échec est journalisé, et c'est l'orchestrateur qui relèvera Mongo.
|
|
499
499
|
|
|
@@ -508,7 +508,7 @@ l'application monte quand même, l'échec est journalisé, et c'est l'orchestrat
|
|
|
508
508
|
|
|
509
509
|
`repo.find({ views: { $gte: 10 } }, { limit: 20 })` traverse quatre étapes :
|
|
510
510
|
|
|
511
|
-
1. **Traduction du critère** (`MongooseRepository.#filter()` (`MongooseRepository.ts:
|
|
511
|
+
1. **Traduction du critère** (`MongooseRepository.#filter()` (`MongooseRepository.ts:256`)) : chaque
|
|
512
512
|
champ est résolu (`id` devient `_id`), chaque opérateur portable est converti.
|
|
513
513
|
2. **Validation du champ** (`MongooseRepository.#resolveField()` (`MongooseRepository.ts:222`)) : un
|
|
514
514
|
champ absent du schéma lève `UnknownCriteriaField` — plutôt que de renvoyer zéro résultat sans
|
|
@@ -539,11 +539,12 @@ drivers. Les signatures exactes vivent dans le graphe généré
|
|
|
539
539
|
| `delete` / `deleteOne` | supprimer | `deleteMany` / `deleteOne` |
|
|
540
540
|
| `findOneAndDelete` | supprimer **et** récupérer le document | `findOneAndDelete` |
|
|
541
541
|
| `count` / `exists` | compter / tester l'existence | `countDocuments` / `exists` |
|
|
542
|
+
| `countDistinct` | compter les valeurs distinctes et non nulles d'un champ | agrégation `$match` → `$group` → `$count` |
|
|
542
543
|
| `withTransaction` | rejouer les mêmes opérations dans une transaction | ajoute la `session` à chaque opération |
|
|
543
544
|
|
|
544
545
|
Les écritures qui « lisent puis écrivent » sont **atomiques par construction**
|
|
545
|
-
(`MongooseRepository.upsert()` (`MongooseRepository.ts:
|
|
546
|
-
`MongooseRepository.increment()` (`MongooseRepository.ts:
|
|
546
|
+
(`MongooseRepository.upsert()` (`MongooseRepository.ts:422`),
|
|
547
|
+
`MongooseRepository.increment()` (`MongooseRepository.ts:507`)) : un seul aller-retour, la
|
|
547
548
|
comparaison est faite par le serveur. Ce n'est pas une optimisation cosmétique — c'est ce qui évite
|
|
548
549
|
que deux requêtes simultanées lisent le même état et s'écrasent mutuellement.
|
|
549
550
|
|
|
@@ -569,8 +570,8 @@ await articles.find({ publishedAt: { $null: true } }); // jamais publié
|
|
|
569
570
|
```
|
|
570
571
|
|
|
571
572
|
> [!WARNING]
|
|
572
|
-
> Un critère ne combine **qu'une condition par champ** (c'est un ET de champs
|
|
573
|
-
> agrégation, une recherche plein texte ou un index composé : passe par
|
|
573
|
+
> Un critère ne combine **qu'une condition par champ** (c'est un ET de champs ; la disjonction se
|
|
574
|
+
> dit `$or: [...]`). Pour une agrégation, une recherche plein texte ou un index composé : passe par
|
|
574
575
|
> [la trappe native](#la-trappe-native--quand-le-contrat-ne-suffit-plus). C'est prévu, pas subi.
|
|
575
576
|
|
|
576
577
|
### Relations — sans clé étrangère
|
|
@@ -657,13 +658,16 @@ contrat est importé en `import type` (effacé à la compilation), et c'est le d
|
|
|
657
658
|
auprès du module consommateur. Le module met en place tout ce câblage à son enregistrement, avant que
|
|
658
659
|
la connexion ne s'ouvre.
|
|
659
660
|
|
|
660
|
-
| Store | Contrat
|
|
661
|
-
| ------------ |
|
|
662
|
-
| Sessions | `ISessionStorage` (http)
|
|
663
|
-
| Utilisateurs | `IUserRepository` (user)
|
|
664
|
-
| Jetons | `ITokenStore` (security)
|
|
665
|
-
| Passkeys | `IWebAuthnCredentialStore`
|
|
666
|
-
| Webhooks | `IWebhookStore`
|
|
661
|
+
| Store | Contrat | Comment le choisir | Collections |
|
|
662
|
+
| ------------ | ----------------------------- | ------------------------------------------------ | -------------------------------------------------- |
|
|
663
|
+
| Sessions | `ISessionStorage` (http) | `session: { store: "mongoose" }` | `session` |
|
|
664
|
+
| Utilisateurs | `IUserRepository` (user) | via `provisionUsers` de ton app | `User` |
|
|
665
|
+
| Jetons | `ITokenStore` (security) | `tokenStore: { store: "mongoose" }` | `access_token`, `denied_jti`, `subject_revocation` |
|
|
666
|
+
| Passkeys | `IWebAuthnCredentialStore` | `passkeys: { store: "mongoose" }` | `webauthn_credential` |
|
|
667
|
+
| Webhooks | `IWebhookStore` | `webhooks: { store: "mongoose" }` | `webhook_endpoint` |
|
|
668
|
+
| 2FA (TOTP) | `ITotpSecretStore` (security) | `totp: { store: "mongoose" }` | `totp_secret` |
|
|
669
|
+
| Audit | `IAuditStore` (security) | `audit: { store: "mongoose" }` | `audit_event` |
|
|
670
|
+
| Idempotence | `IIdempotencyStore` (cœur) | `idempotency: { store: "mongoose" }` (framework) | `idempotency_key` |
|
|
667
671
|
|
|
668
672
|
L'auto-enregistrement peut être coupé (`frameworkEntities: false` (`config.ts:102`)) : le module
|
|
669
673
|
devient alors un pur driver de données, sans schéma framework.
|
|
@@ -676,7 +680,7 @@ devient alors un pur driver de données, sans schéma framework.
|
|
|
676
680
|
|
|
677
681
|
Trois comportements valent d'être connus :
|
|
678
682
|
|
|
679
|
-
- **Purge à deux bornes** — `idleTimeoutS` et `absoluteTimeoutS` (`SessionStorage.gc()` (`SessionStorage.ts:
|
|
683
|
+
- **Purge à deux bornes** — `idleTimeoutS` et `absoluteTimeoutS` (`SessionStorage.gc()` (`SessionStorage.ts:163`)) :
|
|
680
684
|
l'inactivité (depuis la dernière activité) et l'âge absolu (depuis la création, **jamais prolongé** —
|
|
681
685
|
la ré-authentification finit par être imposée, conformément aux recommandations NIST/OWASP).
|
|
682
686
|
- **Prolongation sans réécriture** (`SessionStorage.touch()` (`SessionStorage.ts:195`)) : rafraîchir
|
|
@@ -695,15 +699,15 @@ vaut mieux qu'une erreur 500 et un rejet non capturé.
|
|
|
695
699
|
`MongooseUserRepository` rend des objets `BaseUser` (avec leur comportement : rôles, actif, verrouillé),
|
|
696
700
|
pas des documents nus. Deux recherches lui sont propres :
|
|
697
701
|
|
|
698
|
-
- **par compte social lié** (`MongooseUserRepository.findBySocialProvider()` (`MongooseUserRepository.ts:
|
|
702
|
+
- **par compte social lié** (`MongooseUserRepository.findBySocialProvider()` (`MongooseUserRepository.ts:238`)) :
|
|
699
703
|
un `$elemMatch` sur un tableau libre de fournisseurs — le pendant Mongo du parcours JSON en SQL.
|
|
700
704
|
C'est ce qui porte le motif « Shadow User » d'OAuth (un compte créé à la volée au premier login social),
|
|
701
705
|
**sans colonne par fournisseur** : ajouter GitHub demain n'est pas une migration.
|
|
702
|
-
- **listing paginé** (`MongooseUserRepository.listPage()` (`MongooseUserRepository.ts:
|
|
706
|
+
- **listing paginé** (`MongooseUserRepository.listPage()` (`MongooseUserRepository.ts:281`)) : requête
|
|
703
707
|
native bornée (`skip`/`limit + 1`), tri sur liste blanche, `_id` en départage. Une page est une page,
|
|
704
708
|
jamais la collection entière rapatriée en mémoire.
|
|
705
709
|
|
|
706
|
-
Le comptage des administrateurs actifs (`MongooseUserRepository.countActiveAdmins()` (`MongooseUserRepository.ts:
|
|
710
|
+
Le comptage des administrateurs actifs (`MongooseUserRepository.countActiveAdmins()` (`MongooseUserRepository.ts:328`))
|
|
707
711
|
compte côté serveur — c'est le garde-fou qui empêche de supprimer le dernier administrateur.
|
|
708
712
|
|
|
709
713
|
### Jetons
|
|
@@ -719,7 +723,7 @@ Deux invariants de sécurité sont tenus **par la requête**, pas par du code Ja
|
|
|
719
723
|
déconnexions simultanées pourraient reposer un seuil **plus ancien** — et des jetons révoqués
|
|
720
724
|
redeviendraient valides. Ici, c'est structurellement impossible.
|
|
721
725
|
|
|
722
|
-
La purge, bornée par `retentionRevokedMs` (`MongooseTokenStore.gc()` (`MongooseTokenStore.ts:
|
|
726
|
+
La purge, bornée par `retentionRevokedMs` (`MongooseTokenStore.gc()` (`MongooseTokenStore.ts:356`)), s'appuie sur une particularité utile
|
|
723
727
|
de Mongo : une comparaison numérique **ignore** les documents dont le champ est `null`. Les jetons sans
|
|
724
728
|
expiration ne sont donc jamais balayés par erreur ; ils partent par une règle de rétention distincte.
|
|
725
729
|
|
|
@@ -748,7 +752,7 @@ serait pire qu'une erreur.
|
|
|
748
752
|
|
|
749
753
|
## 🗃 Ce qui est stocké
|
|
750
754
|
|
|
751
|
-
Les
|
|
755
|
+
Les schémas portés par le module — dix collections. Les collections sont créées à la volée par MongoDB — il n'y a
|
|
752
756
|
ni migration ni DDL à jouer, ce qui est l'un des vrais conforts du modèle documentaire.
|
|
753
757
|
|
|
754
758
|
> ⚠️ **Le revers de ce confort : ce qui n'est pas déclaré est JETÉ, sans un mot.** Mongoose valide en
|
|
@@ -763,15 +767,18 @@ ni migration ni DDL à jouer, ce qui est l'un des vrais conforts du modèle docu
|
|
|
763
767
|
> partagé (`USER_COLUMNS`), et le module en dérive un schéma Mongoose. Un champ métier que tu ajoutes
|
|
764
768
|
> à ton utilisateur suit le même chemin — déclaré, donc écrit ; oublié, donc perdu en silence.
|
|
765
769
|
|
|
766
|
-
| Collection | Clé primaire (`_id`)
|
|
767
|
-
| --------------------- |
|
|
768
|
-
| `session` | `ObjectId` (auto)
|
|
769
|
-
| `User` | `ObjectId` (auto)
|
|
770
|
-
| `access_token` | le `jti` (texte)
|
|
771
|
-
| `denied_jti` | le `jti` (texte)
|
|
772
|
-
| `subject_revocation` | le porteur (texte)
|
|
773
|
-
| `webauthn_credential` | l'identifiant du credential
|
|
774
|
-
| `webhook_endpoint` | l'identifiant `wh_…` (texte)
|
|
770
|
+
| Collection | Clé primaire (`_id`) | Contenu | Horodatages |
|
|
771
|
+
| --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------ | ------------------ |
|
|
772
|
+
| `session` | `ObjectId` (auto) | identifiant de session, contenu, messages flash, méta, utilisateur | nombres (ms) |
|
|
773
|
+
| `User` | `ObjectId` (auto) | identifiant, mot de passe haché, rôles, comptes sociaux, méta | gérés par Mongoose |
|
|
774
|
+
| `access_token` | le `jti` (texte) | type, porteur, périmètres, empreinte du secret, révocation | nombres (ms) |
|
|
775
|
+
| `denied_jti` | le `jti` (texte) | expiration | nombres (ms) |
|
|
776
|
+
| `subject_revocation` | le porteur (texte) | seuil `invalidBefore` | nombres (ms) |
|
|
777
|
+
| `webauthn_credential` | l'identifiant du credential | clé publique, compteur, transports, état de sauvegarde | nombres (ms) |
|
|
778
|
+
| `webhook_endpoint` | l'identifiant `wh_…` (texte) | URL, secret chiffré, événements, état des livraisons | nombres (ms) |
|
|
779
|
+
| `totp_secret` | l'identifiant de l'utilisateur (texte) | secret chiffré, algorithme, chiffres, période, codes de secours, dernier pas utilisé | nombres (ms) |
|
|
780
|
+
| `audit_event` | l'identifiant d'événement (texte) | catégorie, action, issue, acteur, ressource, raison, IP, requête | nombre (`ts`, ms) |
|
|
781
|
+
| `idempotency_key` | la clé d'idempotence (texte) | empreinte de la requête, état, réponse rejouée | expiration (ms) |
|
|
775
782
|
|
|
776
783
|
Deux choix structurants s'y lisent :
|
|
777
784
|
|
|
@@ -867,10 +874,10 @@ Le module suit la règle de fond du framework : **ce qui n'est pas observé ne c
|
|
|
867
874
|
| `ORM "nodefony" introuvable` au montage d'un store | `@nodefony/security` chargé **avant** `@nodefony/mongoose` | Mettre le driver **avant** dans `modules` (`registerStores.ts:64`) |
|
|
868
875
|
| `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` |
|
|
869
876
|
| `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 |
|
|
870
|
-
| 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) |
|
|
877
|
+
| 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) | `$or: [...]`, ou une requête native (agrégation) |
|
|
871
878
|
| `many-to-many non portable` | Refus explicite : pas de traduction unique en Mongo | Déclarer la relation via la connexion native |
|
|
872
879
|
| Les sessions disparaissent au redémarrage | `session.store` resté sur `memory` | `session: { store: "mongoose" }`, ou déclarer `NF_DATABASE_URL` et laisser `auto` |
|
|
873
|
-
| `audit store "
|
|
880
|
+
| `audit store "xxx" inconnu` — boot avorté en production | Nom de store inexistant, ou `frameworkEntities: false` | Corriger le nom, ou laisser `auto` |
|
|
874
881
|
| Les comptes ne survivent pas au redémarrage | `provisionUsers` toujours branché sur l'annuaire mémoire | Câbler `MongooseUserRepository.from(orm)` (`MongooseUserRepository.ts:89`) |
|
|
875
882
|
| Un champ écrit se relit `undefined`, sans aucune erreur | Il n'est pas dans le **schéma** : Mongoose est strict et l'écarte en silence | L'ajouter au schéma de l'entité — le type TypeScript seul ne suffit pas |
|
|
876
883
|
| Le premier `npm test` du module met une éternité | Le serveur Mongo de test télécharge son binaire (une seule fois) | Définir `NF_MONGO_TEST_URI` sur un conteneur Mongo |
|
|
@@ -901,9 +908,8 @@ couverts en amont, dans les modules qui possèdent les contrats.
|
|
|
901
908
|
> bien là : soit `NF_MONGO_TEST_URI` pointe sur un conteneur (`docker run -p 27017:27017 mongo:7`), soit
|
|
902
909
|
> le serveur en mémoire a démarré. Les bancs de transaction exigent en plus un **replica set**.
|
|
903
910
|
>
|
|
904
|
-
> Le catalogue des variables d'infrastructure du dépôt est `vitest.gates.ts`, à la racine. Ce module
|
|
905
|
-
>
|
|
906
|
-
> Redis affichent en fin de course ce qu'elles n'ont pas joué.
|
|
911
|
+
> Le catalogue des variables d'infrastructure du dépôt est `vitest.gates.ts`, à la racine. Ce module y
|
|
912
|
+
> déclare sa porte `MONGO_GATE` : le rapporteur de fin de course nomme la cible Mongo non exercée.
|
|
907
913
|
|
|
908
914
|
Couverture : `npm run coverage` dans `@nodefony/mongoose` (rapport lisible aussi dans Studio).
|
|
909
915
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/mongoose",
|
|
3
|
-
"version": "10.0.0-
|
|
3
|
+
"version": "10.0.0-beta.2",
|
|
4
4
|
"description": "Moteur MongoDB de Nodefony sur Mongoose : dépôts typés conformes au contrat orm-core",
|
|
5
5
|
"nodefony": {
|
|
6
6
|
"storeKind": "durable",
|
|
@@ -49,29 +49,29 @@
|
|
|
49
49
|
"esm"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"mongodb": "7.
|
|
53
|
-
"mongoose": "9.10.
|
|
52
|
+
"mongodb": "7.7.0",
|
|
53
|
+
"mongoose": "9.10.3",
|
|
54
54
|
"tslib": "2.8.1",
|
|
55
55
|
"zod": "^4.6.5"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@nodefony/framework": "^10.0.0-
|
|
59
|
-
"@nodefony/http": "^10.0.0-
|
|
60
|
-
"@nodefony/orm-core": "^10.0.0-
|
|
61
|
-
"@nodefony/security": "^10.0.0-
|
|
62
|
-
"@nodefony/user": "^10.0.0-
|
|
63
|
-
"@types/node": "26.6.
|
|
58
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
59
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
60
|
+
"@nodefony/orm-core": "^10.0.0-beta.2",
|
|
61
|
+
"@nodefony/security": "^10.0.0-beta.2",
|
|
62
|
+
"@nodefony/user": "^10.0.0-beta.2",
|
|
63
|
+
"@types/node": "26.6.3",
|
|
64
64
|
"mongodb-memory-server": "11.3.0",
|
|
65
|
-
"nodefony": "^10.0.0-
|
|
66
|
-
"vitest": "5.0.
|
|
65
|
+
"nodefony": "^10.0.0-beta.2",
|
|
66
|
+
"vitest": "5.0.3"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@nodefony/framework": "^10.0.0-
|
|
70
|
-
"@nodefony/http": "^10.0.0-
|
|
71
|
-
"@nodefony/orm-core": "^10.0.0-
|
|
72
|
-
"@nodefony/security": "^10.0.0-
|
|
73
|
-
"@nodefony/user": "^10.0.0-
|
|
74
|
-
"nodefony": "^10.0.0-
|
|
69
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
70
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
71
|
+
"@nodefony/orm-core": "^10.0.0-beta.2",
|
|
72
|
+
"@nodefony/security": "^10.0.0-beta.2",
|
|
73
|
+
"@nodefony/user": "^10.0.0-beta.2",
|
|
74
|
+
"nodefony": "^10.0.0-beta.2"
|
|
75
75
|
},
|
|
76
76
|
"repository": {
|
|
77
77
|
"type": "git",
|