@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.
Files changed (51) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +97 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +90 -0
  6. package/dist/nodefony/config/config.js +57 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/entity/sessionEntity.js +63 -0
  9. package/dist/nodefony/entity/tokenEntity.js +184 -0
  10. package/dist/nodefony/entity/userEntity.js +106 -0
  11. package/dist/nodefony/entity/webAuthnCredentialEntity.js +97 -0
  12. package/dist/nodefony/entity/webhookEndpointEntity.js +109 -0
  13. package/dist/nodefony/interfaces/IMongooseConfig.js +1 -0
  14. package/dist/nodefony/interfaces/index.js +1 -0
  15. package/dist/nodefony/registerStores.js +89 -0
  16. package/dist/nodefony/service/MongooseService.js +95 -0
  17. package/dist/nodefony/src/MongooseTokenStore.js +237 -0
  18. package/dist/nodefony/src/MongooseUserRepository.js +205 -0
  19. package/dist/nodefony/src/MongooseWebAuthnCredentialStore.js +144 -0
  20. package/dist/nodefony/src/MongooseWebhookStore.js +181 -0
  21. package/dist/nodefony/src/SessionStorage.js +241 -0
  22. package/dist/nodefony/src/mongoOrder.js +49 -0
  23. package/dist/nodefony/src/orm-core/MongooseOrm.js +440 -0
  24. package/dist/nodefony/src/orm-core/MongooseRepository.js +300 -0
  25. package/dist/nodefony/src/orm-core/MongooseTransaction.js +53 -0
  26. package/dist/nodefony/src/orm-core/index.js +4 -0
  27. package/dist/types/index.d.ts +74 -0
  28. package/dist/types/nodefony/config/config.d.ts +21 -0
  29. package/dist/types/nodefony/config/defineModuleConfig.d.ts +26 -0
  30. package/dist/types/nodefony/entity/sessionEntity.d.ts +42 -0
  31. package/dist/types/nodefony/entity/tokenEntity.d.ts +59 -0
  32. package/dist/types/nodefony/entity/userEntity.d.ts +54 -0
  33. package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +61 -0
  34. package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +62 -0
  35. package/dist/types/nodefony/interfaces/IMongooseConfig.d.ts +17 -0
  36. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  37. package/dist/types/nodefony/registerStores.d.ts +37 -0
  38. package/dist/types/nodefony/service/MongooseService.d.ts +48 -0
  39. package/dist/types/nodefony/src/MongooseTokenStore.d.ts +126 -0
  40. package/dist/types/nodefony/src/MongooseUserRepository.d.ts +82 -0
  41. package/dist/types/nodefony/src/MongooseWebAuthnCredentialStore.d.ts +52 -0
  42. package/dist/types/nodefony/src/MongooseWebhookStore.d.ts +72 -0
  43. package/dist/types/nodefony/src/SessionStorage.d.ts +63 -0
  44. package/dist/types/nodefony/src/mongoOrder.d.ts +40 -0
  45. package/dist/types/nodefony/src/orm-core/MongooseOrm.d.ts +132 -0
  46. package/dist/types/nodefony/src/orm-core/MongooseRepository.d.ts +51 -0
  47. package/dist/types/nodefony/src/orm-core/MongooseTransaction.d.ts +37 -0
  48. package/dist/types/nodefony/src/orm-core/index.d.ts +9 -0
  49. package/docs/configuration.md +776 -0
  50. package/docs/index.md +881 -0
  51. package/package.json +97 -0
@@ -0,0 +1,205 @@
1
+ import { mongoOrder, toMongoSort } from "./mongoOrder.js";
2
+ import { DOCUMENT_USER_COLUMNS } from "../entity/userEntity.js";
3
+ import { assertPageQuery, escapeRegExp } from "nodefony";
4
+ import { BaseUser, USER_DEFAULT_ORDER, USER_SORTABLE_FIELDS, assertUserContract, attachExtraColumns } from "@nodefony/user";
5
+ //#region nodefony/src/MongooseUserRepository.ts
6
+ /**
7
+ * Ce que le MOTEUR ajoute à chaque document, et qui n'appartient à personne
8
+ * d'autre : la clé primaire brute et la clé de version. Le contrat utilisateur
9
+ * ne peut pas les connaître — il ignore qu'il existe un Mongo — donc c'est ce
10
+ * dépôt qui les tait, au lieu de les laisser passer pour des champs métier.
11
+ */
12
+ const MONGOOSE_INTERNAL_KEYS = /* @__PURE__ */ new Set(["_id", "__v"]);
13
+ /**
14
+ * Adapter Mongoose du contrat {@link IUserRepository} — persistance NoSQL de
15
+ * l'utilisateur (P5.8), pendant documentaire de `DrizzleUserRepository` (P5.9).
16
+ *
17
+ * Décore le repository portable (`IRepository<UserRow>` de {@link MongooseOrm}) de
18
+ * deux responsabilités propres à l'utilisateur :
19
+ * - **mapping document ↔ `BaseUser`** : les consommateurs reçoivent le comportement
20
+ * (`hasRole`/`isActive`/`isLocked`), pas un document nu ;
21
+ * - **finders métier** : `findByIdentifier` (lookup unique) et
22
+ * `findBySocialProvider` (scan du tableau `socialProviders` via `$elemMatch` —
23
+ * équivalent Mongo du `json_each` SQL de Drizzle, pattern Shadow User OAuth).
24
+ *
25
+ * Le credential (`password`) transite par cette frontière — attendu : le repository
26
+ * **est** la frontière de persistance du hash (cf `IUserRepository`).
27
+ */
28
+ var MongooseUserRepository = class MongooseUserRepository {
29
+ /**
30
+ * Même vocabulaire public que les autres repositories — ici le tri part
31
+ * dans la requête, où il ne coûte qu'un index.
32
+ */
33
+ sortableFields = USER_SORTABLE_FIELDS;
34
+ #base;
35
+ #model;
36
+ #session;
37
+ /**
38
+ * @param base - repository portable sur l'entité `User` (CRUD + criteria).
39
+ * @param model - modèle Mongoose natif `User` (pour le scan `$elemMatch`).
40
+ * @param session - session transactionnelle liée aux ops natives, ou `null`.
41
+ */
42
+ constructor(base, model, session = null) {
43
+ this.#base = base;
44
+ this.#model = model;
45
+ this.#session = session;
46
+ }
47
+ /**
48
+ * Construit le repository utilisateur depuis un {@link MongooseOrm} connecté.
49
+ * L'entité `User` doit avoir été enregistrée (cf `registerUserEntity`) **avant**
50
+ * `orm.connect()` (le modèle est compilé au connect).
51
+ *
52
+ * @param orm - ORM Mongoose connecté.
53
+ * @returns le repository utilisateur prêt à l'emploi.
54
+ */
55
+ static from(orm) {
56
+ const model = orm.getNativeConnection().model("User");
57
+ assertUserContract(Object.keys(model.schema.paths), `L'entité « User » de cette application (connecteur « ${orm.name} », mongoose)`, DOCUMENT_USER_COLUMNS);
58
+ return new MongooseUserRepository(orm.getRepository("User"), model);
59
+ }
60
+ /** Mappe une ligne plate en `BaseUser` (comportement + champs anti-migration). */
61
+ #toUser(row) {
62
+ const user = new BaseUser({
63
+ id: row.id,
64
+ identifier: row.identifier,
65
+ roles: row.roles,
66
+ password: row.password,
67
+ enabled: row.enabled,
68
+ locked: row.locked,
69
+ currentRole: row.currentRole,
70
+ socialProviders: row.socialProviders,
71
+ metadata: row.metadata
72
+ });
73
+ return attachExtraColumns(user, row, MONGOOSE_INTERNAL_KEYS);
74
+ }
75
+ async find(criteria, options) {
76
+ return (await this.#base.find(criteria, options)).map((row) => this.#toUser(row));
77
+ }
78
+ async findOne(criteria, options) {
79
+ const row = await this.#base.findOne(criteria, options);
80
+ return row ? this.#toUser(row) : null;
81
+ }
82
+ async create(data) {
83
+ const row = await this.#base.create(data);
84
+ return this.#toUser(row);
85
+ }
86
+ async updateOne(criteria, data) {
87
+ const row = await this.#base.updateOne(criteria, data);
88
+ return row ? this.#toUser(row) : null;
89
+ }
90
+ async upsert(criteria, update, insertOnly) {
91
+ const row = await this.#base.upsert(criteria, update, insertOnly);
92
+ return this.#toUser(row);
93
+ }
94
+ async createMany(data) {
95
+ return (await this.#base.createMany(data)).map((row) => this.#toUser(row));
96
+ }
97
+ exists(criteria) {
98
+ return this.#base.exists(criteria);
99
+ }
100
+ deleteOne(criteria) {
101
+ return this.#base.deleteOne(criteria);
102
+ }
103
+ async findOneAndDelete(criteria) {
104
+ const row = await this.#base.findOneAndDelete(criteria);
105
+ return row ? this.#toUser(row) : null;
106
+ }
107
+ async increment(criteria, changes) {
108
+ const row = await this.#base.increment(criteria, changes);
109
+ return row ? this.#toUser(row) : null;
110
+ }
111
+ updateMany(criteria, data) {
112
+ return this.#base.updateMany(criteria, data);
113
+ }
114
+ delete(criteria) {
115
+ return this.#base.delete(criteria);
116
+ }
117
+ count(criteria) {
118
+ return this.#base.count(criteria);
119
+ }
120
+ countDistinct(field, criteria) {
121
+ return this.#base.countDistinct(field, criteria);
122
+ }
123
+ withTransaction(tx) {
124
+ return new MongooseUserRepository(this.#base.withTransaction(tx), this.#model, tx.getNative());
125
+ }
126
+ findByIdentifier(identifier) {
127
+ return this.findOne({ identifier });
128
+ }
129
+ /**
130
+ * Recherche par compte externe lié — scanne le tableau `socialProviders` via
131
+ * `$elemMatch` (1 requête, équivalent Mongo du `json_each` Drizzle). `null` si
132
+ * aucun lien. Liée à la session transactionnelle courante le cas échéant.
133
+ */
134
+ async findBySocialProvider(provider, providerId) {
135
+ let query = this.#model.findOne({ socialProviders: { $elemMatch: {
136
+ provider,
137
+ providerId
138
+ } } });
139
+ if (this.#session) query = query.session(this.#session);
140
+ const doc = await query.exec();
141
+ return doc ? this.#toUser(doc.toObject({ virtuals: true })) : null;
142
+ }
143
+ /** Construit le filtre Mongo natif des filtres de listing (role/enabled/q). */
144
+ #listFilter(query) {
145
+ const filter = {};
146
+ if (query.role !== void 0) filter.roles = query.role;
147
+ if (query.enabled !== void 0) filter.enabled = query.enabled;
148
+ if (query.locked !== void 0) filter.locked = query.locked;
149
+ if (query.hasSocial !== void 0) filter["socialProviders.0"] = { $exists: query.hasSocial };
150
+ if (query.q !== void 0 && query.q.length > 0) filter.identifier = {
151
+ $regex: escapeRegExp(query.q),
152
+ $options: "i"
153
+ };
154
+ return filter;
155
+ }
156
+ /**
157
+ * {@inheritDoc IUserRepository.listPage}
158
+ *
159
+ * Query native Mongo : `find(filter).sort().skip().limit(limit + 1)` — le store
160
+ * ne renvoie qu'une page (jamais de matérialisation complète). `roles: role` =
161
+ * containment de tableau natif, `$regex/i` = sous-chaîne insensible casse.
162
+ * `_id` en tiebreaker de tri (pagination offset déterministe).
163
+ */
164
+ async listPage(query) {
165
+ assertPageQuery(query, "offset");
166
+ const limit = Math.max(1, Math.floor(query.limit));
167
+ const offset = Math.max(0, Math.floor(query.offset ?? 0));
168
+ const filter = this.#listFilter(query);
169
+ const sort = toMongoSort(mongoOrder(query.order, this.sortableFields, USER_DEFAULT_ORDER));
170
+ if (sort._id === void 0) sort._id = 1;
171
+ let cursor = this.#model.find(filter).sort(sort).skip(offset).limit(limit + 1);
172
+ if (this.#session) cursor = cursor.session(this.#session);
173
+ const docs = await cursor.exec();
174
+ const hasNext = docs.length > limit;
175
+ const items = (hasNext ? docs.slice(0, limit) : docs).map((doc) => this.#toUser(doc.toObject({ virtuals: true })));
176
+ let total;
177
+ if (query.withTotal !== false) {
178
+ let countQuery = this.#model.countDocuments(filter);
179
+ if (this.#session) countQuery = countQuery.session(this.#session);
180
+ total = await countQuery.exec();
181
+ }
182
+ return {
183
+ items,
184
+ total,
185
+ limit,
186
+ offset,
187
+ hasNext
188
+ };
189
+ }
190
+ /** {@inheritDoc IUserRepository.countActiveAdmins} */
191
+ /** {@inheritDoc IUserRepository.countUsers} */
192
+ countUsers(query) {
193
+ return this.#model.countDocuments(this.#listFilter(query)).exec();
194
+ }
195
+ async countActiveAdmins(adminRole) {
196
+ let countQuery = this.#model.countDocuments({
197
+ enabled: true,
198
+ roles: adminRole
199
+ });
200
+ if (this.#session) countQuery = countQuery.session(this.#session);
201
+ return countQuery.exec();
202
+ }
203
+ };
204
+ //#endregion
205
+ export { MongooseUserRepository };
@@ -0,0 +1,144 @@
1
+ import { WEBAUTHN_CREDENTIAL_ENTITY } from "../entity/webAuthnCredentialEntity.js";
2
+ import { assertPageQuery } from "nodefony";
3
+ import { paginate } from "@nodefony/orm-core";
4
+ //#region nodefony/src/MongooseWebAuthnCredentialStore.ts
5
+ /**
6
+ * Store de credentials WebAuthn **Mongoose** (NoSQL) — implémentation d'
7
+ * {@link IWebAuthnCredentialStore} au-dessus d'un unique repository
8
+ * `@nodefony/orm-core` (`webauthn_credential`). Pendant documentaire de
9
+ * `DrizzleWebAuthnCredentialStore`.
10
+ *
11
+ * **Approche B** : `@nodefony/security` n'est connu qu'en `import type` (0 dép
12
+ * runtime). C'est l'application qui enregistre la fabrique
13
+ * (`registerWebAuthnStore("mongoose", …)`) et l'entité
14
+ * (`registerWebAuthnCredentialEntity(orm)` avant `orm.connect()`).
15
+ *
16
+ * **Spécificité Mongo** : la clé naturelle (credentialId) est portée par `_id`
17
+ * (cf {@link webAuthnCredentialSchema}). Le contrat traduit `{ id }` → `{ _id }`,
18
+ * donc les lookups passent par le champ `id` ; les **écritures** posent
19
+ * explicitement `_id` (Mongo ne génère pas notre credentialId). Le mapping
20
+ * `Row ↔ IWebAuthnCredential` normalise `nickname` (`null` → omis).
21
+ */
22
+ var MongooseWebAuthnCredentialStore = class MongooseWebAuthnCredentialStore {
23
+ #repo;
24
+ /** @param repo - repository de la collection `webauthn_credential`. */
25
+ constructor(repo) {
26
+ this.#repo = repo;
27
+ }
28
+ /**
29
+ * Construit le store depuis un {@link MongooseOrm} connecté. L'entité
30
+ * (`registerWebAuthnCredentialEntity`) doit avoir été enregistrée **avant**
31
+ * `connect()`.
32
+ *
33
+ * @param orm - ORM Mongoose connecté hébergeant la collection du store.
34
+ */
35
+ static from(orm) {
36
+ return new MongooseWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY));
37
+ }
38
+ /** Identité réelle d'un credential : `_id` fait foi, le virtuel `id` en repli. */
39
+ #idOf(row) {
40
+ return row._id ?? row.id;
41
+ }
42
+ /** Row plate → credential du contrat (`nickname?` omis si `null`/absent). */
43
+ #toCredential(row) {
44
+ return {
45
+ id: this.#idOf(row),
46
+ userId: row.userId,
47
+ publicKey: row.publicKey,
48
+ signCount: row.signCount,
49
+ transports: row.transports,
50
+ backupEligible: row.backupEligible,
51
+ backupState: row.backupState,
52
+ uvInitialized: row.uvInitialized,
53
+ createdAt: row.createdAt,
54
+ lastUsedAt: row.lastUsedAt,
55
+ ...row.nickname != null ? { nickname: row.nickname } : {}
56
+ };
57
+ }
58
+ async findById(credentialId) {
59
+ const row = await this.#repo.findOne({ id: credentialId });
60
+ return row ? this.#toCredential(row) : null;
61
+ }
62
+ async findByUser(userId) {
63
+ return (await this.#repo.find({ userId })).map((row) => this.#toCredential(row));
64
+ }
65
+ /** `countDocuments` natif — jamais un `find().length` (le plafond ne charge rien). */
66
+ countByUser(userId) {
67
+ return this.#repo.count({ userId });
68
+ }
69
+ async save(credential) {
70
+ const data = {
71
+ userId: credential.userId,
72
+ publicKey: credential.publicKey,
73
+ signCount: credential.signCount,
74
+ transports: [...credential.transports],
75
+ backupEligible: credential.backupEligible,
76
+ backupState: credential.backupState,
77
+ uvInitialized: credential.uvInitialized,
78
+ nickname: credential.nickname ?? null,
79
+ createdAt: credential.createdAt,
80
+ lastUsedAt: credential.lastUsedAt
81
+ };
82
+ await this.#repo.upsert({ id: credential.id }, data);
83
+ }
84
+ async update(credentialId, patch) {
85
+ await this.#repo.updateOne({ id: credentialId }, {
86
+ signCount: patch.signCount,
87
+ backupState: patch.backupState,
88
+ uvInitialized: patch.uvInitialized,
89
+ lastUsedAt: patch.lastUsedAt
90
+ });
91
+ }
92
+ async delete(credentialId) {
93
+ await this.#repo.delete({ id: credentialId });
94
+ }
95
+ /**
96
+ * Critères du listing admin. `q` = PRÉFIXE d'`userId` (`$like 'x%'`, traduit en
97
+ * ancrage `^` côté Mongo), jamais une recherche non ancrée.
98
+ */
99
+ #listCriteria(query) {
100
+ const criteria = {};
101
+ if (query.userId !== void 0) criteria.userId = query.userId;
102
+ else if (query.q !== void 0 && query.q.length > 0) criteria.userId = { $like: `${query.q.replace(/[\\%_]/g, (c) => "\\" + c)}%` };
103
+ if (query.backedUp !== void 0) criteria.backupState = query.backedUp;
104
+ return criteria;
105
+ }
106
+ /**
107
+ * {@inheritDoc IWebAuthnCredentialStore.listPage}
108
+ *
109
+ * Même helper `paginate()` que l'adapter SQL (`skip`/`limit` + `countDocuments`).
110
+ * La projection retire `publicKey` — elle ne franchit jamais la frontière du store.
111
+ * ⚠️ L'identité vient de `_id` (`#idOf`), pas du champ `id` de la row.
112
+ */
113
+ async listPage(query) {
114
+ assertPageQuery(query, "offset");
115
+ const page = await paginate(this.#repo, {
116
+ criteria: this.#listCriteria(query),
117
+ limit: query.limit,
118
+ offset: query.offset,
119
+ withTotal: query.withTotal,
120
+ order: [["createdAt", "DESC"], ["_id", "ASC"]]
121
+ });
122
+ return {
123
+ ...page,
124
+ items: page.items.map((row) => ({
125
+ id: this.#idOf(row),
126
+ userId: row.userId,
127
+ transports: row.transports,
128
+ backupEligible: row.backupEligible,
129
+ backupState: row.backupState,
130
+ uvInitialized: row.uvInitialized,
131
+ signCount: row.signCount,
132
+ createdAt: row.createdAt,
133
+ lastUsedAt: row.lastUsedAt,
134
+ ...row.nickname != null ? { nickname: row.nickname } : {}
135
+ }))
136
+ };
137
+ }
138
+ /** {@inheritDoc IWebAuthnCredentialStore.countCredentials} */
139
+ countCredentials(query) {
140
+ return this.#repo.count(this.#listCriteria(query));
141
+ }
142
+ };
143
+ //#endregion
144
+ export { MongooseWebAuthnCredentialStore };
@@ -0,0 +1,181 @@
1
+ import { WEBHOOK_ENDPOINT_ENTITY } from "../entity/webhookEndpointEntity.js";
2
+ import { mongoOrder, toMongoSort } from "./mongoOrder.js";
3
+ import { assertPageQuery, escapeRegExp } from "nodefony";
4
+ import { WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS } from "@nodefony/security";
5
+ //#region nodefony/src/MongooseWebhookStore.ts
6
+ /**
7
+ * Store d'endpoints webhook **Mongoose** (NoSQL) — implémentation d'
8
+ * {@link IWebhookStore} au-dessus d'un unique repository `@nodefony/orm-core`
9
+ * (`webhook_endpoint`). Pendant documentaire de `DrizzleWebhookStore` ; registre
10
+ * DURABLE des endpoints (survit au redémarrage, ≠ `MemoryWebhookStore`).
11
+ *
12
+ * **Approche B** : `@nodefony/security` n'est connu qu'en `import type` (0 dép
13
+ * runtime). C'est l'application qui enregistre la fabrique
14
+ * (`registerWebhookStore("mongoose", …)`) et l'entité
15
+ * (`registerWebhookEndpointEntity(orm)` avant `orm.connect()`).
16
+ *
17
+ * **Spécificité Mongo** : la clé naturelle (`wh_<random>`) est portée par `_id`
18
+ * (cf {@link webhookEndpointSchema}). Le contrat traduit `{ id }` → `{ _id }`,
19
+ * donc les lookups passent par `id` ; les **écritures** posent explicitement
20
+ * `_id` (Mongo ne génère pas notre id). Mapping `Row ↔ IWebhookEndpoint` :
21
+ * `IWebhookEndpoint` est déjà « plat tout `| null` », seuls les champs JSON
22
+ * `events`/`metadata` sont copiés défensivement.
23
+ */
24
+ var MongooseWebhookStore = class MongooseWebhookStore {
25
+ /**
26
+ * {@inheritDoc IWebhookStore.sortableFields}
27
+ *
28
+ * Capacité pleine : le vocabulaire public entier est trié par Mongo, `id`
29
+ * compris — traduit en `_id` au moment de la requête.
30
+ */
31
+ sortableFields = WEBHOOK_SORTABLE_FIELDS;
32
+ #repo;
33
+ #model;
34
+ /**
35
+ * @param repo - repository de la collection `webhook_endpoint`.
36
+ * @param model - modèle Mongoose natif, requis par le seul listing paginé
37
+ * (recherche `q` = `$or` sur deux champs, hors `Criteria` AND-only).
38
+ * `null` = `listPage` refuse plutôt que de tout charger en silence.
39
+ */
40
+ constructor(repo, model = null) {
41
+ this.#repo = repo;
42
+ this.#model = model;
43
+ }
44
+ /**
45
+ * Construit le store depuis un {@link MongooseOrm} connecté. L'entité
46
+ * (`registerWebhookEndpointEntity`) doit avoir été enregistrée **avant**
47
+ * `connect()`.
48
+ *
49
+ * @param orm - ORM Mongoose connecté hébergeant la collection du store.
50
+ */
51
+ static from(orm) {
52
+ const connection = orm.getNativeConnection();
53
+ return new MongooseWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), connection.model(WEBHOOK_ENDPOINT_ENTITY));
54
+ }
55
+ /** Identité réelle d'un endpoint : `_id` fait foi, le virtuel `id` en repli. */
56
+ #idOf(row) {
57
+ return row._id ?? row.id;
58
+ }
59
+ /** Row plate → endpoint du contrat (sans `_id`/`__v` ; `events` en `readonly`). */
60
+ #toEndpoint(row) {
61
+ return {
62
+ id: this.#idOf(row),
63
+ url: row.url,
64
+ secretEnc: row.secretEnc,
65
+ events: row.events,
66
+ enabled: row.enabled,
67
+ description: row.description,
68
+ tenantId: row.tenantId,
69
+ createdBy: row.createdBy,
70
+ createdAt: row.createdAt,
71
+ updatedAt: row.updatedAt,
72
+ lastDeliveryAt: row.lastDeliveryAt,
73
+ lastDeliveryStatus: row.lastDeliveryStatus,
74
+ lastDeliveryError: row.lastDeliveryError,
75
+ failureCount: row.failureCount,
76
+ metadata: row.metadata
77
+ };
78
+ }
79
+ async save(endpoint) {
80
+ const data = {
81
+ url: endpoint.url,
82
+ secretEnc: endpoint.secretEnc,
83
+ events: [...endpoint.events],
84
+ enabled: endpoint.enabled,
85
+ description: endpoint.description,
86
+ tenantId: endpoint.tenantId,
87
+ createdBy: endpoint.createdBy,
88
+ createdAt: endpoint.createdAt,
89
+ updatedAt: endpoint.updatedAt,
90
+ lastDeliveryAt: endpoint.lastDeliveryAt,
91
+ lastDeliveryStatus: endpoint.lastDeliveryStatus,
92
+ lastDeliveryError: endpoint.lastDeliveryError,
93
+ failureCount: endpoint.failureCount,
94
+ metadata: { ...endpoint.metadata }
95
+ };
96
+ await this.#repo.upsert({ id: endpoint.id }, data);
97
+ }
98
+ async findById(id) {
99
+ const row = await this.#repo.findOne({ id });
100
+ return row ? this.#toEndpoint(row) : null;
101
+ }
102
+ async update(id, patch) {
103
+ const { events, metadata, ...rest } = patch;
104
+ const data = { ...rest };
105
+ if (events !== void 0) data.events = [...events];
106
+ if (metadata !== void 0) data.metadata = { ...metadata };
107
+ await this.#repo.updateOne({ id }, data);
108
+ }
109
+ async delete(id) {
110
+ await this.#repo.delete({ id });
111
+ }
112
+ async listAll() {
113
+ return (await this.#repo.find({})).map((row) => this.#toEndpoint(row));
114
+ }
115
+ /**
116
+ * Modèle natif ou erreur explicite — un `listPage` qui retomberait sur un
117
+ * `find({})` complet trahirait silencieusement la garantie du contrat.
118
+ */
119
+ #nativeModel() {
120
+ if (this.#model === null) throw new Error("MongooseWebhookStore: listing paginé indisponible (store construit sans modèle natif). Utiliser MongooseWebhookStore.from(orm).");
121
+ return this.#model;
122
+ }
123
+ /**
124
+ * Filtre Mongo des filtres du listing. `events: <event>` = **containment de
125
+ * tableau natif** (Mongo matche un scalaire contre chaque élément) ; `q` =
126
+ * `$or` de deux `$regex/i` — le `$or` sort du `Criteria` AND-only, d'où la
127
+ * query native.
128
+ */
129
+ #listFilter(query) {
130
+ const filter = {};
131
+ if (query.enabled !== void 0) filter.enabled = query.enabled;
132
+ if (query.failing !== void 0) filter.failureCount = query.failing ? { $gt: 0 } : 0;
133
+ if (query.event !== void 0) filter.events = query.event;
134
+ if (query.q !== void 0 && query.q.length > 0) {
135
+ const needle = escapeRegExp(query.q);
136
+ filter.$or = [{ url: {
137
+ $regex: needle,
138
+ $options: "i"
139
+ } }, { description: {
140
+ $regex: needle,
141
+ $options: "i"
142
+ } }];
143
+ }
144
+ return filter;
145
+ }
146
+ /**
147
+ * {@inheritDoc IWebhookStore.listPage}
148
+ *
149
+ * Query native : `find(filter).sort(…).skip().limit(limit+1)` — une page,
150
+ * jamais la collection. Le `limit + 1` donne `hasNext` sans compter.
151
+ *
152
+ * Le tri demandé est **traduit** avant de descendre : au repos, l'endpoint n'a
153
+ * pas de champ `id` (l'identifiant EST le `_id`), et Mongo ne se plaint pas
154
+ * d'un tri sur un champ absent — il rend un ordre arbitraire. Sans traduction,
155
+ * un `?order=id` serait donc inerte ici et correct partout ailleurs. À défaut
156
+ * d'`order`, l'ordre par défaut `createdAt DESC, _id ASC` reste déterministe.
157
+ */
158
+ async listPage(query) {
159
+ assertPageQuery(query, "offset");
160
+ const limit = Math.max(1, Math.floor(query.limit));
161
+ const offset = Math.max(0, Math.floor(query.offset ?? 0));
162
+ const model = this.#nativeModel();
163
+ const filter = this.#listFilter(query);
164
+ const order = mongoOrder(query.order, this.sortableFields, WEBHOOK_DEFAULT_ORDER);
165
+ const docs = await model.find(filter).sort(toMongoSort(order)).skip(offset).limit(limit + 1).exec();
166
+ const hasNext = docs.length > limit;
167
+ return {
168
+ items: (hasNext ? docs.slice(0, limit) : docs).map((doc) => this.#toEndpoint(doc.toObject({ virtuals: true }))),
169
+ total: query.withTotal === false ? void 0 : await model.countDocuments(filter).exec(),
170
+ limit,
171
+ offset,
172
+ hasNext
173
+ };
174
+ }
175
+ /** {@inheritDoc IWebhookStore.countEndpoints} */
176
+ async countEndpoints(query) {
177
+ return this.#nativeModel().countDocuments(this.#listFilter(query)).exec();
178
+ }
179
+ };
180
+ //#endregion
181
+ export { MongooseWebhookStore };