@nodefony/mongoose 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +97 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +90 -0
- package/dist/nodefony/config/config.js +57 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/entity/sessionEntity.js +63 -0
- package/dist/nodefony/entity/tokenEntity.js +184 -0
- package/dist/nodefony/entity/userEntity.js +106 -0
- package/dist/nodefony/entity/webAuthnCredentialEntity.js +97 -0
- package/dist/nodefony/entity/webhookEndpointEntity.js +109 -0
- package/dist/nodefony/interfaces/IMongooseConfig.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/registerStores.js +89 -0
- package/dist/nodefony/service/MongooseService.js +95 -0
- package/dist/nodefony/src/MongooseTokenStore.js +237 -0
- package/dist/nodefony/src/MongooseUserRepository.js +205 -0
- package/dist/nodefony/src/MongooseWebAuthnCredentialStore.js +144 -0
- package/dist/nodefony/src/MongooseWebhookStore.js +181 -0
- package/dist/nodefony/src/SessionStorage.js +241 -0
- package/dist/nodefony/src/mongoOrder.js +49 -0
- package/dist/nodefony/src/orm-core/MongooseOrm.js +440 -0
- package/dist/nodefony/src/orm-core/MongooseRepository.js +300 -0
- package/dist/nodefony/src/orm-core/MongooseTransaction.js +53 -0
- package/dist/nodefony/src/orm-core/index.js +4 -0
- package/dist/types/index.d.ts +74 -0
- package/dist/types/nodefony/config/config.d.ts +21 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +26 -0
- package/dist/types/nodefony/entity/sessionEntity.d.ts +42 -0
- package/dist/types/nodefony/entity/tokenEntity.d.ts +59 -0
- package/dist/types/nodefony/entity/userEntity.d.ts +54 -0
- package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +61 -0
- package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +62 -0
- package/dist/types/nodefony/interfaces/IMongooseConfig.d.ts +17 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/registerStores.d.ts +37 -0
- package/dist/types/nodefony/service/MongooseService.d.ts +48 -0
- package/dist/types/nodefony/src/MongooseTokenStore.d.ts +126 -0
- package/dist/types/nodefony/src/MongooseUserRepository.d.ts +82 -0
- package/dist/types/nodefony/src/MongooseWebAuthnCredentialStore.d.ts +52 -0
- package/dist/types/nodefony/src/MongooseWebhookStore.d.ts +72 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +63 -0
- package/dist/types/nodefony/src/mongoOrder.d.ts +40 -0
- package/dist/types/nodefony/src/orm-core/MongooseOrm.d.ts +132 -0
- package/dist/types/nodefony/src/orm-core/MongooseRepository.d.ts +51 -0
- package/dist/types/nodefony/src/orm-core/MongooseTransaction.d.ts +37 -0
- package/dist/types/nodefony/src/orm-core/index.d.ts +9 -0
- package/docs/configuration.md +776 -0
- package/docs/index.md +881 -0
- package/package.json +97 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { SESSION_CONNECTOR } from "../entity/sessionEntity.js";
|
|
2
|
+
import { assertPageQuery, pickOrder, renameOrderFields } from "nodefony";
|
|
3
|
+
import { ormRegistry, paginate } from "@nodefony/orm-core";
|
|
4
|
+
import { SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_SORTABLE_FIELDS, SessionsService } from "@nodefony/http";
|
|
5
|
+
//#region nodefony/src/SessionStorage.ts
|
|
6
|
+
/**
|
|
7
|
+
* Stockage de session **Mongoose** (driver NoSQL), branché sur `@nodefony/orm-core`.
|
|
8
|
+
*
|
|
9
|
+
* Implémente le contrat {@link ISessionStorage} consommé par le `SessionsService`
|
|
10
|
+
* de `@nodefony/http` — store de session portable. Persiste via le repository
|
|
11
|
+
* orm-core de l'entité `session` (connecteur `nodefony`, modèle compilé au boot
|
|
12
|
+
* par `MongooseOrm`). Logique **identique** au store Drizzle (timestamps en ms,
|
|
13
|
+
* GC via l'opérateur riche portable `$lt`) — la portabilité du contrat orm-core.
|
|
14
|
+
*/
|
|
15
|
+
var SessionStorage = class SessionStorage {
|
|
16
|
+
manager;
|
|
17
|
+
idleTimeoutS;
|
|
18
|
+
absoluteTimeoutS;
|
|
19
|
+
/**
|
|
20
|
+
* Même vocabulaire public que les autres backends — le tri part dans le
|
|
21
|
+
* `sort()` Mongo, où il ne coûte qu'un index.
|
|
22
|
+
*/
|
|
23
|
+
sortableFields = SESSION_SORTABLE_FIELDS;
|
|
24
|
+
constructor(manager) {
|
|
25
|
+
this.manager = manager;
|
|
26
|
+
this.idleTimeoutS = manager.options.idleTimeoutS;
|
|
27
|
+
this.absoluteTimeoutS = manager.options.absoluteTimeoutS;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Repository de l'entité session, ou `null` si l'ORM n'est pas (ou plus)
|
|
31
|
+
* connecté.
|
|
32
|
+
*
|
|
33
|
+
* Pendant le shutdown du kernel, `MongooseService` déconnecte l'ORM alors que
|
|
34
|
+
* des requêtes peuvent encore être en vol (firewall → `startSession`). On
|
|
35
|
+
* renvoie `null` et chaque opération dégrade gracieusement (session non
|
|
36
|
+
* persistée le temps de l'arrêt) plutôt que de jeter (500 + `unhandledRejection`
|
|
37
|
+
* via le GC fire-and-forget). Une table absente sur un ORM **connecté** (vraie
|
|
38
|
+
* misconfig) jette toujours via `getRepository`.
|
|
39
|
+
*/
|
|
40
|
+
#repo() {
|
|
41
|
+
const orm = ormRegistry.get(SESSION_CONNECTOR);
|
|
42
|
+
if (!orm.isConnected()) return null;
|
|
43
|
+
return orm.getRepository("session");
|
|
44
|
+
}
|
|
45
|
+
async read(id) {
|
|
46
|
+
const criteria = { session_id: id };
|
|
47
|
+
const repo = this.#repo();
|
|
48
|
+
if (!repo) return {};
|
|
49
|
+
const row = await repo.findOne(criteria);
|
|
50
|
+
if (!row) return {};
|
|
51
|
+
return {
|
|
52
|
+
Attributes: row.Attributes ?? {},
|
|
53
|
+
metaBag: row.metaBag ?? {},
|
|
54
|
+
flashBag: row.flashBag ?? {},
|
|
55
|
+
user: row.user ?? "",
|
|
56
|
+
createdAt: new Date(row.createdAt),
|
|
57
|
+
updatedAt: new Date(row.updatedAt)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async start(id) {
|
|
61
|
+
return this.read(id);
|
|
62
|
+
}
|
|
63
|
+
async write(id, data) {
|
|
64
|
+
const serialize = data;
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const repo = this.#repo();
|
|
67
|
+
if (!repo) return {
|
|
68
|
+
...serialize,
|
|
69
|
+
createdAt: new Date(now),
|
|
70
|
+
updatedAt: new Date(now)
|
|
71
|
+
};
|
|
72
|
+
const fields = {
|
|
73
|
+
Attributes: serialize.Attributes,
|
|
74
|
+
flashBag: serialize.flashBag,
|
|
75
|
+
metaBag: serialize.metaBag,
|
|
76
|
+
user: serialize.user || null,
|
|
77
|
+
updatedAt: now
|
|
78
|
+
};
|
|
79
|
+
const row = await repo.upsert({ session_id: id }, fields, { createdAt: now });
|
|
80
|
+
return {
|
|
81
|
+
...serialize,
|
|
82
|
+
createdAt: new Date(row.createdAt),
|
|
83
|
+
updatedAt: new Date(now)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async open() {
|
|
87
|
+
await this.gc();
|
|
88
|
+
const repo = this.#repo();
|
|
89
|
+
if (!repo) return 0;
|
|
90
|
+
const count = await repo.count();
|
|
91
|
+
this.manager.log(`MONGOOSE SESSIONS STORAGE ==> COUNT SESSIONS : ${count}`, "INFO");
|
|
92
|
+
return count;
|
|
93
|
+
}
|
|
94
|
+
close() {
|
|
95
|
+
this.gc();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
async destroy(id) {
|
|
99
|
+
const criteria = { session_id: id };
|
|
100
|
+
const repo = this.#repo();
|
|
101
|
+
if (!repo) return true;
|
|
102
|
+
await repo.delete(criteria);
|
|
103
|
+
this.manager.log(`MONGOOSE DESTROY SESSION ID : ${id}`, "DEBUG");
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
async gc(idleSeconds, absoluteSeconds) {
|
|
107
|
+
const repo = this.#repo();
|
|
108
|
+
if (!repo) return;
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
const idleCutoff = now - (idleSeconds ?? this.idleTimeoutS) * 1e3;
|
|
111
|
+
let deleted = await repo.delete({ updatedAt: { $lt: idleCutoff } });
|
|
112
|
+
const absoluteS = absoluteSeconds ?? this.absoluteTimeoutS;
|
|
113
|
+
if (absoluteS > 0) deleted += await repo.delete({ createdAt: { $lt: now - absoluteS * 1e3 } });
|
|
114
|
+
if (deleted > 0) this.manager.log(`MONGOOSE SESSIONS GC ==> ${deleted} DELETED`, "DEBUG");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Prolonge l'idle d'une session (timeout glissant) : `updateOne updatedAt = now`
|
|
118
|
+
* sur `session_id` — SANS réécrire le blob (touch NIST/OWASP). N'affecte pas
|
|
119
|
+
* `createdAt` (= borne absolute). ORM déconnecté → no-op ; ligne absente →
|
|
120
|
+
* no-op silencieux. Parité avec le store Drizzle.
|
|
121
|
+
*/
|
|
122
|
+
async touch(id) {
|
|
123
|
+
const repo = this.#repo();
|
|
124
|
+
if (!repo) return;
|
|
125
|
+
await repo.updateOne({ session_id: id }, { updatedAt: Date.now() });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Énumération admin (capacité optionnelle d'`ISessionStorage`) — `find` projeté,
|
|
129
|
+
* filtrable par `user`. **Redaction par construction** : seuls `user`/`metaBag`/
|
|
130
|
+
* timestamps sortent de la base ; `Attributes`/`flashBag` (potentiellement
|
|
131
|
+
* sensibles) restent en base. ORM déconnecté → `[]`. Strictement identique au
|
|
132
|
+
* store Drizzle (parité du contrat orm-core).
|
|
133
|
+
*/
|
|
134
|
+
async listAll(filter) {
|
|
135
|
+
const repo = this.#repo();
|
|
136
|
+
if (!repo) return [];
|
|
137
|
+
return (filter?.user !== void 0 ? await repo.find({ user: filter.user }) : await repo.find()).map((row) => SessionStorage.#toRecord(row));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Projette un document en {@link ISessionRecord} **redacté** : `Attributes` et
|
|
141
|
+
* `flashBag` restent en base, seuls `user`/`metaBag`/horodatages sortent. Une
|
|
142
|
+
* session anonyme est stockée `user = null` (cf `write`) et ressort en chaîne
|
|
143
|
+
* vide — la représentation du « pas d'utilisateur » appartient au backend.
|
|
144
|
+
*/
|
|
145
|
+
static #toRecord(row) {
|
|
146
|
+
return {
|
|
147
|
+
id: row.session_id,
|
|
148
|
+
data: {
|
|
149
|
+
Attributes: {},
|
|
150
|
+
flashBag: {},
|
|
151
|
+
metaBag: row.metaBag ?? {},
|
|
152
|
+
user: row.user ?? "",
|
|
153
|
+
createdAt: new Date(row.createdAt),
|
|
154
|
+
updatedAt: new Date(row.updatedAt)
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Traduit les filtres du contrat en `Criteria` orm-core — **portables** (égalité
|
|
160
|
+
* + test de nullité), donc poussés dans le `find` Mongo et jamais ré-appliqués
|
|
161
|
+
* en mémoire. Source unique du périmètre de {@link listPage},
|
|
162
|
+
* {@link countSessions} et {@link countDistinctUsers}.
|
|
163
|
+
*
|
|
164
|
+
* Les deux filtres portent la **même** propriété (`authenticated` signifie
|
|
165
|
+
* « `user` non nul »). Quand ils se contredisent — un identifiant nommé qu'on
|
|
166
|
+
* demande anonyme, ou l'inverse — la réponse honnête est l'ensemble vide, et
|
|
167
|
+
* c'est ce que dit `null`. Parité stricte avec le store Drizzle et le store
|
|
168
|
+
* mémoire, qui appliquent les deux conditions.
|
|
169
|
+
*
|
|
170
|
+
* @returns `undefined` (aucun filtre), le critère, ou **`null`** si les
|
|
171
|
+
* filtres sont contradictoires.
|
|
172
|
+
*/
|
|
173
|
+
static #criteria(query) {
|
|
174
|
+
if (!query) return void 0;
|
|
175
|
+
const { user, authenticated } = query;
|
|
176
|
+
if (user !== void 0) {
|
|
177
|
+
const anonymous = user === "";
|
|
178
|
+
if (authenticated !== void 0 && authenticated === anonymous) return null;
|
|
179
|
+
return { user: anonymous ? { $null: true } : user };
|
|
180
|
+
}
|
|
181
|
+
if (authenticated !== void 0) return { user: { $null: !authenticated } };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Pagination **native** `skip/limit` + `countDocuments` (helper `paginate`
|
|
185
|
+
* orm-core) : une page = une requête bornée, quel que soit le volume en base.
|
|
186
|
+
* Ordre `updatedAt` DESC départagé par `session_id` — déterministe à horodatage
|
|
187
|
+
* égal. ORM déconnecté → page vide. Parité stricte avec le store Drizzle.
|
|
188
|
+
*/
|
|
189
|
+
async listPage(query) {
|
|
190
|
+
assertPageQuery(query, "offset");
|
|
191
|
+
const repo = this.#repo();
|
|
192
|
+
if (!repo) return {
|
|
193
|
+
items: [],
|
|
194
|
+
total: query.withTotal === false ? void 0 : 0,
|
|
195
|
+
limit: query.limit,
|
|
196
|
+
offset: query.offset ?? 0,
|
|
197
|
+
hasNext: false
|
|
198
|
+
};
|
|
199
|
+
const criteria = SessionStorage.#criteria(query);
|
|
200
|
+
if (criteria === null) return {
|
|
201
|
+
items: [],
|
|
202
|
+
total: query.withTotal === false ? void 0 : 0,
|
|
203
|
+
limit: query.limit,
|
|
204
|
+
offset: query.offset ?? 0,
|
|
205
|
+
hasNext: false
|
|
206
|
+
};
|
|
207
|
+
const page = await paginate(repo, {
|
|
208
|
+
criteria,
|
|
209
|
+
limit: query.limit,
|
|
210
|
+
offset: query.offset,
|
|
211
|
+
withTotal: query.withTotal,
|
|
212
|
+
order: renameOrderFields(pickOrder(query.order, this.sortableFields, SESSION_DEFAULT_ORDER), SESSION_COLUMN_ALIASES)
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
...page,
|
|
216
|
+
items: page.items.map((row) => SessionStorage.#toRecord(row))
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** `countDocuments` natif filtré — aucun document matérialisé. Déconnecté → 0. */
|
|
220
|
+
async countSessions(query) {
|
|
221
|
+
const repo = this.#repo();
|
|
222
|
+
if (!repo) return 0;
|
|
223
|
+
const criteria = SessionStorage.#criteria(query);
|
|
224
|
+
return criteria === null ? 0 : repo.count(criteria);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Utilisateurs distincts, agrégés côté serveur. Les sessions anonymes portent
|
|
228
|
+
* un `user` nul ou absent : l'agrégation les écarte, elles ne comptent donc
|
|
229
|
+
* pas pour un utilisateur. Déconnecté → 0 (même dégradation que
|
|
230
|
+
* {@link countSessions}).
|
|
231
|
+
*/
|
|
232
|
+
async countDistinctUsers(query) {
|
|
233
|
+
const repo = this.#repo();
|
|
234
|
+
if (!repo) return 0;
|
|
235
|
+
const criteria = SessionStorage.#criteria(query);
|
|
236
|
+
return criteria === null ? 0 : repo.countDistinct("user", criteria);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
SessionsService.registerStorage("mongoose", SessionStorage);
|
|
240
|
+
//#endregion
|
|
241
|
+
export { SessionStorage as default };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { pickOrder, renameOrderFields } from "nodefony";
|
|
2
|
+
//#region nodefony/src/mongoOrder.ts
|
|
3
|
+
/**
|
|
4
|
+
* **La** table d'alias du dialecte Mongo — `id` public → `_id` au repos.
|
|
5
|
+
*
|
|
6
|
+
* Elle vit ici, chez l'adapter qui possède la convention, et non dans le
|
|
7
|
+
* vocabulaire de chaque ressource : ce n'est pas une propriété des jetons ni des
|
|
8
|
+
* endpoints, c'est une propriété de **Mongo**, où la clé primaire s'appelle
|
|
9
|
+
* `_id` et où aucun champ `id` n'existe au repos (`id` n'est qu'un virtuel de
|
|
10
|
+
* lecture). Trois copies de cette même règle vivaient auparavant dans trois
|
|
11
|
+
* fichiers de vocabulaire, une par ressource.
|
|
12
|
+
*
|
|
13
|
+
* Ce que la traduction évite : Mongo ne se plaint **pas** d'un tri sur un champ
|
|
14
|
+
* absent — il rend les documents dans un ordre arbitraire. Sans elle, un
|
|
15
|
+
* `?order=id` serait donc silencieusement inerte sur Mongo et correct partout
|
|
16
|
+
* ailleurs, et l'écart ne se verrait qu'en production, chez un tiers.
|
|
17
|
+
*/
|
|
18
|
+
const MONGO_ORDER_ALIASES = { id: "_id" };
|
|
19
|
+
/**
|
|
20
|
+
* Borne un `order` reçu à ce que le store DÉCLARE savoir trier, puis l'exprime
|
|
21
|
+
* dans le schéma Mongo — les deux gestes que tout store Mongo doit faire, dans
|
|
22
|
+
* cet ordre, avant de descendre le tri au driver.
|
|
23
|
+
*
|
|
24
|
+
* L'ordre compte : filtrer d'abord (sur le vocabulaire **public**, celui que le
|
|
25
|
+
* store annonce), traduire ensuite. L'inverse comparerait des noms de colonnes à
|
|
26
|
+
* une liste de noms publics, et laisserait passer ce qu'elle est censée refuser.
|
|
27
|
+
*
|
|
28
|
+
* @param order - l'ordre demandé, en vocabulaire public.
|
|
29
|
+
* @param sortable - ce que le store déclare ({@link ISortableSource}).
|
|
30
|
+
* @param fallback - l'ordre contractuel appliqué à défaut, en vocabulaire public.
|
|
31
|
+
* @returns l'ordre prêt pour `.sort()` / `paginate()`, en noms de champs Mongo.
|
|
32
|
+
*/
|
|
33
|
+
function mongoOrder(order, sortable, fallback) {
|
|
34
|
+
return renameOrderFields(pickOrder(order, sortable ?? [], fallback), MONGO_ORDER_ALIASES);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Forme `{ champ: 1 | -1 }` attendue par `Model.sort()` — le dernier maillon,
|
|
38
|
+
* pour les stores qui parlent au driver Mongo directement plutôt que par
|
|
39
|
+
* `paginate()`.
|
|
40
|
+
*
|
|
41
|
+
* @param order - un ordre déjà borné et traduit (sortie de {@link mongoOrder}).
|
|
42
|
+
*/
|
|
43
|
+
function toMongoSort(order) {
|
|
44
|
+
const sort = {};
|
|
45
|
+
for (const [field, dir] of order) sort[field] = dir === "DESC" ? -1 : 1;
|
|
46
|
+
return sort;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { MONGO_ORDER_ALIASES, mongoOrder, toMongoSort };
|