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