@rebasepro/server-mongo 0.0.1-canary.4829d6e

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 (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +86 -0
  3. package/dist/MongoBootstrapper.d.ts +18 -0
  4. package/dist/MongoBootstrapper.d.ts.map +1 -0
  5. package/dist/auth/ensure-collections.d.ts +3 -0
  6. package/dist/auth/ensure-collections.d.ts.map +1 -0
  7. package/dist/auth/services.d.ts +156 -0
  8. package/dist/auth/services.d.ts.map +1 -0
  9. package/dist/connection.d.ts +35 -0
  10. package/dist/connection.d.ts.map +1 -0
  11. package/dist/db/MongoConditionBuilder.d.ts +64 -0
  12. package/dist/db/MongoConditionBuilder.d.ts.map +1 -0
  13. package/dist/db/MongoDataService.d.ts +101 -0
  14. package/dist/db/MongoDataService.d.ts.map +1 -0
  15. package/dist/ensure-collections-Bkx_O5CQ.js +94 -0
  16. package/dist/ensure-collections-Bkx_O5CQ.js.map +1 -0
  17. package/dist/ensure-history-collection-yajOt2dv.js +21 -0
  18. package/dist/ensure-history-collection-yajOt2dv.js.map +1 -0
  19. package/dist/factory.d.ts +151 -0
  20. package/dist/factory.d.ts.map +1 -0
  21. package/dist/history/ensure-history-collection.d.ts +3 -0
  22. package/dist/history/ensure-history-collection.d.ts.map +1 -0
  23. package/dist/index.d.ts +18 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.es.js +2508 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +2925 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/services/MongoDriver.d.ts +125 -0
  30. package/dist/services/MongoDriver.d.ts.map +1 -0
  31. package/dist/services/MongoHistoryService.d.ts +37 -0
  32. package/dist/services/MongoHistoryService.d.ts.map +1 -0
  33. package/dist/services/MongoRealtimeService.d.ts +103 -0
  34. package/dist/services/MongoRealtimeService.d.ts.map +1 -0
  35. package/dist/websocket-DQlwCHFq.js +281 -0
  36. package/dist/websocket-DQlwCHFq.js.map +1 -0
  37. package/dist/websocket.d.ts +7 -0
  38. package/dist/websocket.d.ts.map +1 -0
  39. package/package.json +81 -0
  40. package/src/MongoBootstrapper.ts +196 -0
  41. package/src/auth/ensure-collections.ts +105 -0
  42. package/src/auth/services.ts +732 -0
  43. package/src/connection.ts +60 -0
  44. package/src/db/MongoConditionBuilder.ts +224 -0
  45. package/src/db/MongoDataService.ts +368 -0
  46. package/src/factory.ts +305 -0
  47. package/src/history/ensure-history-collection.ts +22 -0
  48. package/src/index.ts +25 -0
  49. package/src/services/MongoDriver.ts +1120 -0
  50. package/src/services/MongoHistoryService.ts +181 -0
  51. package/src/services/MongoRealtimeService.ts +446 -0
  52. package/src/websocket.ts +297 -0
@@ -0,0 +1,2508 @@
1
+ import { MongoClient, ObjectId } from "mongodb";
2
+ import { EntityReference } from "@rebasepro/types";
3
+ import { ApiError, logger } from "@rebasepro/server";
4
+ import { buildPropertyCallbacks, buildSdkData, checkOperation, updateDateAutoValues } from "@rebasepro/common";
5
+ import { mergeDeep } from "@rebasepro/utils";
6
+ //#region \0rolldown/runtime.js
7
+ var __defProp = Object.defineProperty;
8
+ var __exportAll = (all, no_symbols) => {
9
+ let target = {};
10
+ for (var name in all) __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
15
+ return target;
16
+ };
17
+ //#endregion
18
+ //#region src/connection.ts
19
+ /**
20
+ * MongoDB Connection
21
+ *
22
+ * Wraps MongoDB connection to implement the DatabaseConnection interface.
23
+ */
24
+ /**
25
+ * MongoDB database connection wrapper that implements DatabaseConnection interface.
26
+ */
27
+ var MongoDBConnection = class {
28
+ db;
29
+ client;
30
+ type = "mongodb";
31
+ constructor(db, client) {
32
+ this.db = db;
33
+ this.client = client;
34
+ }
35
+ get isConnected() {
36
+ try {
37
+ return this.client.topology?.isConnected?.() ?? false;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ async close() {
43
+ await this.client.close();
44
+ }
45
+ };
46
+ /**
47
+ * Create a MongoDB database connection from a connection string.
48
+ *
49
+ * @param connectionString - MongoDB connection string (e.g., mongodb://localhost:27017)
50
+ * @param databaseName - Name of the database to use
51
+ * @returns Promise resolving to MongoDBConnection
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const connection = await createMongoDBConnection(
56
+ * "mongodb://localhost:27017",
57
+ * "my_database"
58
+ * );
59
+ * ```
60
+ */
61
+ async function createMongoDBConnection(connectionString, databaseName) {
62
+ const client = new MongoClient(connectionString);
63
+ await client.connect();
64
+ return new MongoDBConnection(client.db(databaseName), client);
65
+ }
66
+ //#endregion
67
+ //#region src/db/MongoConditionBuilder.ts
68
+ /**
69
+ * Mapping from Rebase filter operators to MongoDB query operators
70
+ */
71
+ var REBASE_TO_MONGO_OP = {
72
+ "<": "$lt",
73
+ "<=": "$lte",
74
+ "==": "$eq",
75
+ "!=": "$ne",
76
+ ">=": "$gte",
77
+ ">": "$gt",
78
+ "array-contains": "$elemMatch",
79
+ "array-contains-any": "$in",
80
+ "in": "$in",
81
+ "not-in": "$nin"
82
+ };
83
+ function escapeRegExp$1(str) {
84
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85
+ }
86
+ /**
87
+ * Translate a SQL LIKE/ILIKE pattern into an anchored regular expression.
88
+ * `%` matches any sequence of characters, `_` matches a single character;
89
+ * every other character is matched literally.
90
+ */
91
+ function likePatternToRegExp(pattern, caseInsensitive) {
92
+ let body = "";
93
+ for (const ch of String(pattern)) if (ch === "%") body += ".*";
94
+ else if (ch === "_") body += ".";
95
+ else body += escapeRegExp$1(ch);
96
+ return new RegExp(`^${body}$`, caseInsensitive ? "i" : "");
97
+ }
98
+ /**
99
+ * MongoDB Condition Builder
100
+ *
101
+ * Provides static methods to translate Rebase filter conditions
102
+ * to MongoDB query filters.
103
+ */
104
+ var MongoConditionBuilder = class {
105
+ /**
106
+ * Build MongoDB filter conditions from Rebase FilterValues
107
+ *
108
+ * @param filter - Rebase filter values
109
+ * @returns Array of MongoDB filter objects
110
+ */
111
+ static buildFilterConditions(filter) {
112
+ if (!filter) return [];
113
+ const conditions = [];
114
+ for (const [field, filterParam] of Object.entries(filter)) {
115
+ if (!filterParam) continue;
116
+ const [op, value] = filterParam;
117
+ if (op === "is-null") {
118
+ conditions.push({ [field]: { $eq: null } });
119
+ continue;
120
+ }
121
+ if (op === "is-not-null") {
122
+ conditions.push({ [field]: { $ne: null } });
123
+ continue;
124
+ }
125
+ if (op === "like" || op === "ilike" || op === "not-like" || op === "not-ilike") {
126
+ const regex = likePatternToRegExp(value, op === "ilike" || op === "not-ilike");
127
+ const negated = op === "not-like" || op === "not-ilike";
128
+ conditions.push({ [field]: negated ? { $not: regex } : { $regex: regex } });
129
+ continue;
130
+ }
131
+ const mongoOp = REBASE_TO_MONGO_OP[op];
132
+ if (!mongoOp) {
133
+ logger.warn(`Unsupported filter operator: ${op}`);
134
+ continue;
135
+ }
136
+ if (op === "array-contains") conditions.push({ [field]: { $elemMatch: { $eq: value } } });
137
+ else conditions.push({ [field]: { [mongoOp]: value } });
138
+ }
139
+ return conditions;
140
+ }
141
+ /**
142
+ * Build search conditions for text search
143
+ *
144
+ * @param searchString - Text to search for
145
+ * @param properties - Properties to search in
146
+ * @returns Array of MongoDB filter objects for text search
147
+ */
148
+ static buildSearchConditions(searchString, properties) {
149
+ if (!searchString) return [];
150
+ const orConditions = [];
151
+ const escapedSearch = escapeRegExp$1(searchString);
152
+ const searchRegex = new RegExp(escapedSearch, "i");
153
+ for (const [key, prop] of Object.entries(properties)) if (prop?.dataType === "string" || typeof prop === "string") orConditions.push({ [key]: { $regex: searchRegex } });
154
+ if (orConditions.length === 0) return [{ $text: { $search: searchString } }];
155
+ return orConditions;
156
+ }
157
+ /**
158
+ * Combine multiple conditions with AND operator
159
+ *
160
+ * @param conditions - Array of filter conditions
161
+ * @returns Combined filter or undefined if empty
162
+ */
163
+ static combineConditionsWithAnd(conditions) {
164
+ if (conditions.length === 0) return void 0;
165
+ if (conditions.length === 1) return conditions[0];
166
+ return { $and: conditions };
167
+ }
168
+ /**
169
+ * Combine multiple conditions with OR operator
170
+ *
171
+ * @param conditions - Array of filter conditions
172
+ * @returns Combined filter or undefined if empty
173
+ */
174
+ static combineConditionsWithOr(conditions) {
175
+ if (conditions.length === 0) return void 0;
176
+ if (conditions.length === 1) return conditions[0];
177
+ return { $or: conditions };
178
+ }
179
+ /**
180
+ * Build a complete MongoDB query from Rebase options
181
+ *
182
+ * @param options - Rebase fetch options
183
+ * @returns MongoDB filter object
184
+ */
185
+ static buildQuery(options) {
186
+ const conditions = [];
187
+ if (options.filter) {
188
+ const filterConditions = this.buildFilterConditions(options.filter);
189
+ conditions.push(...filterConditions);
190
+ }
191
+ if (options.searchString && options.properties) {
192
+ const searchConditions = this.buildSearchConditions(options.searchString, options.properties);
193
+ if (searchConditions.length > 0) {
194
+ const searchFilter = this.combineConditionsWithOr(searchConditions);
195
+ if (searchFilter) conditions.push(searchFilter);
196
+ }
197
+ }
198
+ return this.combineConditionsWithAnd(conditions) ?? {};
199
+ }
200
+ /**
201
+ * Build MongoDB sort options from Rebase options
202
+ *
203
+ * @param orderBy - Field to order by
204
+ * @param order - Sort direction
205
+ * @returns MongoDB sort object
206
+ */
207
+ static buildSort(orderBy, order) {
208
+ if (!orderBy) return void 0;
209
+ return { [orderBy]: order === "desc" ? -1 : 1 };
210
+ }
211
+ };
212
+ //#endregion
213
+ //#region src/db/MongoDataService.ts
214
+ /**
215
+ * MongoDB Row Service
216
+ *
217
+ * Implements DataRepository interface for MongoDB.
218
+ * Provides all CRUD operations for rows.
219
+ */
220
+ /**
221
+ * MongoDB Row Service
222
+ *
223
+ * Implements the DataRepository interface for MongoDB.
224
+ * Provides all CRUD operations for rows stored in MongoDB collections.
225
+ */
226
+ var MongoDataService = class {
227
+ db;
228
+ constructor(db) {
229
+ this.db = db;
230
+ }
231
+ /**
232
+ * Get a MongoDB collection by its path
233
+ */
234
+ getCollection(collectionPath) {
235
+ const collectionName = collectionPath.replace(/\//g, "_");
236
+ return this.db.collection(collectionName);
237
+ }
238
+ /**
239
+ * Convert a string ID to ObjectId if it's a valid ObjectId string
240
+ */
241
+ toObjectId(id) {
242
+ if (typeof id === "string" && ObjectId.isValid(id) && id.length === 24) return new ObjectId(id);
243
+ return id;
244
+ }
245
+ /**
246
+ * Convert a MongoDB document to a flat row (`{ id, ...fields }`)
247
+ */
248
+ documentToRow(doc) {
249
+ const { _id, ...values } = doc;
250
+ return {
251
+ ...this.convertFromMongoValues(values),
252
+ id: _id.toString()
253
+ };
254
+ }
255
+ /**
256
+ * Convert values from MongoDB format to Rebase format
257
+ */
258
+ convertFromMongoValues(values) {
259
+ const result = {};
260
+ for (const [key, value] of Object.entries(values)) result[key] = this.convertFromMongoValue(value);
261
+ return result;
262
+ }
263
+ /**
264
+ * Convert a single value from MongoDB format
265
+ */
266
+ convertFromMongoValue(value) {
267
+ if (value === null || value === void 0) return value;
268
+ if (value instanceof ObjectId) return value.toString();
269
+ if (value instanceof Date) return value;
270
+ if (Array.isArray(value)) return value.map((v) => this.convertFromMongoValue(v));
271
+ if (typeof value === "object") {
272
+ const keys = Object.keys(value);
273
+ const isTagged = value.__type === "reference" && "id" in value && "path" in value;
274
+ const isLegacy = keys.length === 2 && keys.includes("id") && keys.includes("path");
275
+ if (isTagged || isLegacy) return new EntityReference({
276
+ id: value.id instanceof ObjectId ? value.id.toString() : String(value.id),
277
+ path: value.path,
278
+ driver: value.driver,
279
+ databaseId: value.databaseId
280
+ });
281
+ }
282
+ if (typeof value === "object") return this.convertFromMongoValues(value);
283
+ return value;
284
+ }
285
+ /**
286
+ * Convert values to MongoDB format for storage
287
+ */
288
+ convertToMongoValues(values) {
289
+ const result = {};
290
+ for (const [key, value] of Object.entries(values)) result[key] = this.convertToMongoValue(value);
291
+ return result;
292
+ }
293
+ /**
294
+ * Convert a single value to MongoDB format
295
+ */
296
+ convertToMongoValue(value) {
297
+ if (value === null || value === void 0) return value;
298
+ if (typeof value === "object" && value.isEntityReference?.()) {
299
+ const ref = {
300
+ __type: "reference",
301
+ id: ObjectId.isValid(value.id) ? new ObjectId(value.id) : value.id,
302
+ path: value.path
303
+ };
304
+ if (value.driver !== void 0) ref.driver = value.driver;
305
+ if (value.databaseId !== void 0) ref.databaseId = value.databaseId;
306
+ return ref;
307
+ }
308
+ if (value instanceof Date) return value;
309
+ if (Array.isArray(value)) return value.map((v) => this.convertToMongoValue(v));
310
+ if (typeof value === "object") return this.convertToMongoValues(value);
311
+ return value;
312
+ }
313
+ /**
314
+ * Fetch a single row by ID
315
+ */
316
+ async fetchOne(collectionPath, id, _databaseId) {
317
+ const collection = this.getCollection(collectionPath);
318
+ const objectId = this.toObjectId(id);
319
+ const doc = await collection.findOne({ _id: objectId });
320
+ if (!doc) return void 0;
321
+ return this.documentToRow(doc);
322
+ }
323
+ /**
324
+ * Fetch a collection of rows with optional filtering, ordering, and pagination
325
+ */
326
+ async fetchCollection(collectionPath, options = {}) {
327
+ const collection = this.getCollection(collectionPath);
328
+ const query = options.rawQuery ?? MongoConditionBuilder.buildQuery({
329
+ filter: options.filter,
330
+ searchString: options.searchString,
331
+ properties: options.collection?.properties ?? {}
332
+ });
333
+ const findOptions = {};
334
+ const sort = MongoConditionBuilder.buildSort(options.orderBy, options.order);
335
+ if (sort) findOptions.sort = sort;
336
+ if (options.limit) findOptions.limit = options.limit;
337
+ if (options.startAfter !== void 0) findOptions.skip = Number(options.startAfter);
338
+ return (await collection.find(query, findOptions).toArray()).map((doc) => this.documentToRow(doc));
339
+ }
340
+ /**
341
+ * Search rows by text
342
+ */
343
+ async searchRows(collectionPath, searchString, options = {}) {
344
+ return this.fetchCollection(collectionPath, {
345
+ ...options,
346
+ searchString
347
+ });
348
+ }
349
+ /**
350
+ * Count rows in a collection
351
+ */
352
+ async count(collectionPath, options = {}) {
353
+ const collection = this.getCollection(collectionPath);
354
+ const query = options.rawQuery ?? (options.filter ? MongoConditionBuilder.buildQuery({ filter: options.filter }) : {});
355
+ return collection.countDocuments(query);
356
+ }
357
+ /**
358
+ * Save an row (create or update)
359
+ */
360
+ async save(collectionPath, values, id, _databaseId) {
361
+ const collection = this.getCollection(collectionPath);
362
+ const mongoValues = this.convertToMongoValues(values);
363
+ if (id) {
364
+ const objectId = this.toObjectId(id);
365
+ await collection.updateOne({ _id: objectId }, { $set: mongoValues }, { upsert: true });
366
+ return {
367
+ ...values,
368
+ id: id.toString()
369
+ };
370
+ } else {
371
+ const newId = new ObjectId();
372
+ await collection.insertOne({
373
+ _id: newId,
374
+ ...mongoValues
375
+ });
376
+ return {
377
+ ...values,
378
+ id: newId.toString()
379
+ };
380
+ }
381
+ }
382
+ /**
383
+ * Delete an row by ID
384
+ */
385
+ async delete(collectionPath, id, _databaseId) {
386
+ const collection = this.getCollection(collectionPath);
387
+ const objectId = this.toObjectId(id);
388
+ if ((await collection.deleteOne({ _id: objectId })).deletedCount === 0) logger.warn(`Row ${id} not found in collection ${collectionPath}`);
389
+ }
390
+ /**
391
+ * Check if a field value is unique in a collection
392
+ */
393
+ async checkUniqueField(collectionPath, fieldName, value, excludeEntityId, _databaseId) {
394
+ const collection = this.getCollection(collectionPath);
395
+ const query = { [fieldName]: value };
396
+ if (excludeEntityId) query._id = { $ne: this.toObjectId(excludeEntityId) };
397
+ return await collection.countDocuments(query) === 0;
398
+ }
399
+ /**
400
+ * Generate a new row ID
401
+ */
402
+ generateId() {
403
+ return new ObjectId().toString();
404
+ }
405
+ };
406
+ //#endregion
407
+ //#region src/services/MongoRealtimeService.ts
408
+ /**
409
+ * MongoDB Realtime Service
410
+ *
411
+ * Implements RealtimeProvider interface using MongoDB Change Streams.
412
+ * Provides real-time subscriptions to collection and row changes.
413
+ */
414
+ /**
415
+ * MongoDB Realtime Service
416
+ *
417
+ * Implements real-time subscriptions using MongoDB Change Streams.
418
+ * Requires MongoDB replica set for change streams to work.
419
+ */
420
+ var MongoRealtimeService = class {
421
+ db;
422
+ subscriptions = /* @__PURE__ */ new Map();
423
+ clients = /* @__PURE__ */ new Map();
424
+ dataService;
425
+ driver;
426
+ constructor(db) {
427
+ this.db = db;
428
+ this.dataService = new MongoDataService(db);
429
+ }
430
+ setDataDriver(driver) {
431
+ this.driver = driver;
432
+ }
433
+ /**
434
+ * Get the collection name from a path
435
+ */
436
+ getCollectionName(path) {
437
+ return path.replace(/\//g, "_");
438
+ }
439
+ /**
440
+ * Subscribe to collection changes
441
+ */
442
+ subscribeToCollection(subscriptionId, config, callback) {
443
+ this.unsubscribe(subscriptionId);
444
+ const collectionName = this.getCollectionName(config.path);
445
+ const collection = this.db.collection(collectionName);
446
+ const pipeline = [];
447
+ pipeline.push({ $match: { operationType: { $in: [
448
+ "insert",
449
+ "update",
450
+ "replace",
451
+ "delete"
452
+ ] } } });
453
+ try {
454
+ const changeStream = collection.watch(pipeline, { fullDocument: "updateLookup" });
455
+ const subscription = {
456
+ type: "collection",
457
+ config,
458
+ changeStream,
459
+ callback,
460
+ authContext: config.authContext
461
+ };
462
+ this.subscriptions.set(subscriptionId, subscription);
463
+ this.fetchAndNotifyCollection(subscriptionId, config, callback);
464
+ changeStream.on("change", async (change) => {
465
+ await this.fetchAndNotifyCollection(subscriptionId, config, callback);
466
+ });
467
+ changeStream.on("error", (error) => {
468
+ logger.error(`Change stream error for subscription ${subscriptionId}`, { error });
469
+ });
470
+ } catch (error) {
471
+ logger.warn("Change streams not available, falling back to polling", { error });
472
+ const subscription = {
473
+ type: "collection",
474
+ config,
475
+ callback,
476
+ authContext: config.authContext
477
+ };
478
+ this.subscriptions.set(subscriptionId, subscription);
479
+ this.fetchAndNotifyCollection(subscriptionId, config, callback);
480
+ }
481
+ }
482
+ /**
483
+ * Fetch collection and notify callback
484
+ */
485
+ async fetchAndNotifyCollection(subscriptionId, config, callback) {
486
+ try {
487
+ let rows;
488
+ const registryCollection = this.driver?.registry?.getCollectionByPath(config.path);
489
+ if (config.authContext && this.driver) {
490
+ const mockUser = {
491
+ uid: config.authContext.userId,
492
+ roles: config.authContext.roles
493
+ };
494
+ rows = await (await this.driver.withAuth(mockUser)).fetchCollection({
495
+ path: config.path,
496
+ collection: registryCollection,
497
+ filter: config.filter,
498
+ orderBy: config.orderBy,
499
+ order: config.order,
500
+ limit: config.limit,
501
+ startAfter: config.startAfter,
502
+ searchString: config.searchString
503
+ });
504
+ } else rows = await this.dataService.fetchCollection(config.path, {
505
+ filter: config.filter,
506
+ orderBy: config.orderBy,
507
+ order: config.order,
508
+ limit: config.limit,
509
+ startAfter: config.startAfter,
510
+ searchString: config.searchString,
511
+ collection: registryCollection
512
+ });
513
+ if (callback) callback(rows);
514
+ } catch (error) {
515
+ logger.error(`Error fetching collection for subscription ${subscriptionId}`, { error });
516
+ }
517
+ }
518
+ /**
519
+ * Subscribe to single row changes
520
+ */
521
+ subscribeToOne(subscriptionId, config, callback) {
522
+ this.unsubscribe(subscriptionId);
523
+ const collectionName = this.getCollectionName(config.path);
524
+ const collection = this.db.collection(collectionName);
525
+ const pipeline = [{ $match: {
526
+ "documentKey._id": typeof config.id === "string" && ObjectId.isValid(config.id) ? new ObjectId(config.id) : config.id,
527
+ operationType: { $in: [
528
+ "insert",
529
+ "update",
530
+ "replace",
531
+ "delete"
532
+ ] }
533
+ } }];
534
+ try {
535
+ const changeStream = collection.watch(pipeline, { fullDocument: "updateLookup" });
536
+ const subscription = {
537
+ type: "single",
538
+ config,
539
+ changeStream,
540
+ callback,
541
+ authContext: config.authContext
542
+ };
543
+ this.subscriptions.set(subscriptionId, subscription);
544
+ this.fetchAndNotifyOne(subscriptionId, config, callback);
545
+ changeStream.on("change", async (change) => {
546
+ if (change.operationType === "delete") {
547
+ if (callback) callback(null);
548
+ } else await this.fetchAndNotifyOne(subscriptionId, config, callback);
549
+ });
550
+ changeStream.on("error", (error) => {
551
+ logger.error(`Change stream error for subscription ${subscriptionId}`, { error });
552
+ });
553
+ } catch (error) {
554
+ logger.warn("Change streams not available, falling back to polling", { error });
555
+ const subscription = {
556
+ type: "single",
557
+ config,
558
+ callback,
559
+ authContext: config.authContext
560
+ };
561
+ this.subscriptions.set(subscriptionId, subscription);
562
+ this.fetchAndNotifyOne(subscriptionId, config, callback);
563
+ }
564
+ }
565
+ /**
566
+ * Fetch row and notify callback
567
+ */
568
+ async fetchAndNotifyOne(subscriptionId, config, callback) {
569
+ try {
570
+ let row;
571
+ const registryCollection = this.driver?.registry?.getCollectionByPath(config.path);
572
+ if (config.authContext && this.driver) {
573
+ const mockUser = {
574
+ uid: config.authContext.userId,
575
+ roles: config.authContext.roles
576
+ };
577
+ row = await (await this.driver.withAuth(mockUser)).fetchOne({
578
+ path: config.path,
579
+ id: config.id,
580
+ collection: registryCollection
581
+ });
582
+ } else row = await this.dataService.fetchOne(config.path, config.id);
583
+ if (callback) callback(row || null);
584
+ } catch (error) {
585
+ logger.error(`Error fetching row for subscription ${subscriptionId}`, { error });
586
+ }
587
+ }
588
+ /**
589
+ * Unsubscribe from a subscription
590
+ */
591
+ unsubscribe(subscriptionId) {
592
+ const subscription = this.subscriptions.get(subscriptionId);
593
+ if (subscription) {
594
+ if (subscription.changeStream) subscription.changeStream.close().catch((err) => logger.error("Operation failed", { error: err }));
595
+ this.subscriptions.delete(subscriptionId);
596
+ }
597
+ }
598
+ /**
599
+ * Notify all relevant subscribers of an row update
600
+ * This is called after save/delete operations to push updates
601
+ */
602
+ async notifyUpdate(path, id, row, _databaseId) {
603
+ for (const [subscriptionId, subscription] of this.subscriptions) if (subscription.type === "single") {
604
+ const config = subscription.config;
605
+ if (config.path === path && config.id.toString() === id) {
606
+ if (subscription.callback) subscription.callback(row);
607
+ }
608
+ } else if (subscription.type === "collection") {
609
+ const config = subscription.config;
610
+ if (config.path === path) await this.fetchAndNotifyCollection(subscriptionId, config, subscription.callback);
611
+ }
612
+ }
613
+ /**
614
+ * Get all active subscriptions (for debugging)
615
+ */
616
+ getSubscriptions() {
617
+ return this.subscriptions;
618
+ }
619
+ /**
620
+ * Close all subscriptions
621
+ */
622
+ async closeAll() {
623
+ for (const [subscriptionId] of this.subscriptions) this.unsubscribe(subscriptionId);
624
+ }
625
+ /**
626
+ * Register a WebSocket client for real-time communication
627
+ */
628
+ addClient(clientId, ws) {
629
+ this.clients.set(clientId, ws);
630
+ ws.on("close", () => {
631
+ this.removeClient(clientId);
632
+ });
633
+ ws.on("error", (error) => {
634
+ logger.error("WebSocket error for client", {
635
+ detail: clientId,
636
+ error
637
+ });
638
+ this.removeClient(clientId);
639
+ });
640
+ }
641
+ /**
642
+ * Remove a WebSocket client and clean up its subscriptions
643
+ */
644
+ removeClient(clientId) {
645
+ this.clients.delete(clientId);
646
+ }
647
+ /**
648
+ * Handle an incoming WebSocket message for subscription management
649
+ */
650
+ async handleClientMessage(clientId, message, _authContext) {
651
+ const ws = this.clients.get(clientId);
652
+ if (!ws) return;
653
+ const authContext = _authContext ? {
654
+ userId: _authContext.userId,
655
+ roles: (_authContext.roles ?? []).map(String)
656
+ } : void 0;
657
+ switch (message.type) {
658
+ case "subscribe_collection": {
659
+ const subscriptionId = message.payload?.subscriptionId ?? message.subscriptionId;
660
+ if (!subscriptionId) return;
661
+ this.subscribeToCollection(subscriptionId, {
662
+ clientId,
663
+ path: message.payload?.path,
664
+ filter: message.payload?.filter,
665
+ orderBy: message.payload?.orderBy,
666
+ order: message.payload?.order,
667
+ limit: message.payload?.limit,
668
+ startAfter: message.payload?.startAfter,
669
+ searchString: message.payload?.searchString,
670
+ authContext
671
+ }, (rows) => {
672
+ ws.send(JSON.stringify({
673
+ type: "collection_update",
674
+ subscriptionId,
675
+ rows
676
+ }));
677
+ });
678
+ break;
679
+ }
680
+ case "subscribe_one": {
681
+ const subscriptionId = message.payload?.subscriptionId ?? message.subscriptionId;
682
+ if (!subscriptionId) return;
683
+ this.subscribeToOne(subscriptionId, {
684
+ clientId,
685
+ path: message.payload?.path,
686
+ id: message.payload?.id,
687
+ authContext
688
+ }, (row) => {
689
+ ws.send(JSON.stringify({
690
+ type: "single_update",
691
+ subscriptionId,
692
+ row
693
+ }));
694
+ });
695
+ break;
696
+ }
697
+ case "unsubscribe": {
698
+ const subscriptionId = message.payload?.subscriptionId ?? message.subscriptionId;
699
+ if (subscriptionId) this.unsubscribe(subscriptionId);
700
+ break;
701
+ }
702
+ }
703
+ }
704
+ };
705
+ //#endregion
706
+ //#region src/services/MongoHistoryService.ts
707
+ var MongoHistoryService_exports = /* @__PURE__ */ __exportAll({
708
+ MongoHistoryService: () => MongoHistoryService,
709
+ findChangedFields: () => findChangedFields
710
+ });
711
+ /**
712
+ * Deep equality without JSON.stringify.
713
+ * Handles primitives, arrays, Dates, and plain objects recursively.
714
+ */
715
+ function deepEqual(a, b) {
716
+ if (a === b) return true;
717
+ if (a == null || b == null) return false;
718
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
719
+ if (Array.isArray(a) && Array.isArray(b)) {
720
+ if (a.length !== b.length) return false;
721
+ return a.every((v, i) => deepEqual(v, b[i]));
722
+ }
723
+ if (typeof a === "object" && typeof b === "object") {
724
+ const aObj = a;
725
+ const bObj = b;
726
+ const aKeys = Object.keys(aObj);
727
+ const bKeys = Object.keys(bObj);
728
+ if (aKeys.length !== bKeys.length) return false;
729
+ return aKeys.every((k) => deepEqual(aObj[k], bObj[k]));
730
+ }
731
+ return false;
732
+ }
733
+ /**
734
+ * Shallow comparison to find top-level keys that changed between two objects.
735
+ */
736
+ function findChangedFields(oldValues, newValues) {
737
+ const changed = [];
738
+ const allKeys = new Set([...Object.keys(oldValues), ...Object.keys(newValues)]);
739
+ for (const key of allKeys) {
740
+ const oldVal = oldValues[key];
741
+ const newVal = newValues[key];
742
+ if (key.startsWith("__")) continue;
743
+ if (oldVal !== newVal) if (typeof oldVal === "object" && oldVal !== null && typeof newVal === "object" && newVal !== null) {
744
+ if (!deepEqual(oldVal, newVal)) changed.push(key);
745
+ } else changed.push(key);
746
+ }
747
+ return changed.length > 0 ? changed : null;
748
+ }
749
+ var DEFAULT_RETENTION = {
750
+ maxEntries: 200,
751
+ ttlDays: 90
752
+ };
753
+ var MongoHistoryService = class {
754
+ db;
755
+ retention;
756
+ constructor(db, retention) {
757
+ this.db = db;
758
+ this.retention = {
759
+ ...DEFAULT_RETENTION,
760
+ ...retention
761
+ };
762
+ }
763
+ async recordHistory(params) {
764
+ const { tableName, id, action, values, previousValues, updatedBy } = params;
765
+ const changedFields = previousValues && values ? findChangedFields(previousValues, values) : null;
766
+ if (action === "update" && (!changedFields || changedFields.length === 0)) return;
767
+ try {
768
+ const entry = {
769
+ id: new ObjectId().toString(),
770
+ table_name: tableName,
771
+ entity_id: String(id),
772
+ action,
773
+ changed_fields: changedFields,
774
+ values: values || null,
775
+ previous_values: previousValues || null,
776
+ updated_by: updatedBy || null,
777
+ updated_at: /* @__PURE__ */ new Date()
778
+ };
779
+ await this.db.collection("__rebase_history").insertOne(entry);
780
+ this.pruneHistory(String(id), tableName).catch((e) => {
781
+ logger.error(`[HistoryService] Failed to prune history for ${tableName}/${id}`, { error: e });
782
+ });
783
+ } catch (error) {
784
+ logger.error(`[HistoryService] Failed to record history for ${tableName}/${id}`, { error });
785
+ }
786
+ }
787
+ async pruneHistory(id, tableName) {
788
+ const collection = this.db.collection("__rebase_history");
789
+ const count = await collection.countDocuments({
790
+ entity_id: id,
791
+ table_name: tableName
792
+ });
793
+ if (count > this.retention.maxEntries) {
794
+ const toDelete = count - this.retention.maxEntries;
795
+ const oldestEntries = await collection.find({
796
+ entity_id: id,
797
+ table_name: tableName
798
+ }).sort({ updated_at: 1 }).limit(toDelete).toArray();
799
+ if (oldestEntries.length > 0) {
800
+ const idsToDelete = oldestEntries.map((entry) => entry._id);
801
+ await collection.deleteMany({ _id: { $in: idsToDelete } });
802
+ }
803
+ }
804
+ const cutoffDate = /* @__PURE__ */ new Date();
805
+ cutoffDate.setDate(cutoffDate.getDate() - this.retention.ttlDays);
806
+ await collection.deleteMany({
807
+ entity_id: id,
808
+ table_name: tableName,
809
+ updated_at: { $lt: cutoffDate }
810
+ });
811
+ }
812
+ };
813
+ //#endregion
814
+ //#region src/services/MongoDriver.ts
815
+ /**
816
+ * MongoDB DataDriver Delegate
817
+ *
818
+ * Implements the DataDriver interface for Rebase.
819
+ * Provides all data operations needed by the Rebase frontend.
820
+ */
821
+ var MongoDriver = class {
822
+ db;
823
+ registry;
824
+ key = "mongodb";
825
+ initialised = true;
826
+ dataService;
827
+ realtimeService;
828
+ historyService;
829
+ user;
830
+ data;
831
+ client;
832
+ constructor(db, realtimeService, historyService, registry, user) {
833
+ this.db = db;
834
+ this.registry = registry;
835
+ this.dataService = new MongoDataService(db);
836
+ this.realtimeService = realtimeService ?? new MongoRealtimeService(db);
837
+ this.historyService = historyService ?? new MongoHistoryService(db);
838
+ this.user = user;
839
+ this.data = buildSdkData(this);
840
+ this.realtimeService.setDataDriver(this);
841
+ }
842
+ /**
843
+ * Get the current timestamp
844
+ */
845
+ currentTime() {
846
+ return /* @__PURE__ */ new Date();
847
+ }
848
+ /**
849
+ * Resolve a collection's callbacks and property callbacks from the registry.
850
+ * Used by AuthenticatedMongoDriver to apply callbacks after RLS filtering.
851
+ */
852
+ resolveCollectionCallbacks(collection, path) {
853
+ if (!collection && !path) return {
854
+ collection: void 0,
855
+ callbacks: void 0,
856
+ globalCallbacks: void 0,
857
+ propertyCallbacks: void 0
858
+ };
859
+ const registryCollection = this.registry?.getCollectionByPath(path);
860
+ const resolvedCollection = registryCollection ? {
861
+ ...collection,
862
+ ...registryCollection
863
+ } : collection;
864
+ const callbacks = resolvedCollection?.callbacks;
865
+ const globalCallbacks = this.registry?.getGlobalCallbacks();
866
+ const properties = resolvedCollection?.properties;
867
+ let propertyCallbacks;
868
+ if (properties) propertyCallbacks = buildPropertyCallbacks(properties);
869
+ return {
870
+ collection: resolvedCollection,
871
+ callbacks,
872
+ globalCallbacks,
873
+ propertyCallbacks
874
+ };
875
+ }
876
+ /**
877
+ * Fetch a collection of rows
878
+ */
879
+ async fetchCollection({ path, collection, filter, limit, startAfter, orderBy, searchString, order }) {
880
+ const rows = await this.dataService.fetchCollection(path, {
881
+ filter,
882
+ limit,
883
+ startAfter,
884
+ orderBy,
885
+ order,
886
+ searchString,
887
+ collection
888
+ });
889
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
890
+ if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
891
+ const contextForCallback = {
892
+ user: this.user,
893
+ driver: this,
894
+ data: this.data,
895
+ client: this.client,
896
+ storageSource: this.client?.storage
897
+ };
898
+ return Promise.all(rows.map(async (row) => {
899
+ let fetched = row;
900
+ if (globalCallbacks?.afterRead) fetched = await globalCallbacks.afterRead({
901
+ collection: resolvedCollection,
902
+ path,
903
+ row: fetched,
904
+ context: contextForCallback
905
+ }) ?? fetched;
906
+ if (callbacks?.afterRead) fetched = await callbacks.afterRead({
907
+ collection: resolvedCollection,
908
+ path,
909
+ row: fetched,
910
+ context: contextForCallback
911
+ }) ?? fetched;
912
+ if (propertyCallbacks?.afterRead) fetched = await propertyCallbacks.afterRead({
913
+ collection: resolvedCollection,
914
+ path,
915
+ row: fetched,
916
+ context: contextForCallback
917
+ }) ?? fetched;
918
+ return fetched;
919
+ }));
920
+ }
921
+ return rows;
922
+ }
923
+ /**
924
+ * Listen to collection changes
925
+ */
926
+ listenCollection({ path, collection, filter, limit, startAfter, orderBy, searchString, order, onUpdate, onError }) {
927
+ const subscriptionId = this.generateSubscriptionId();
928
+ const callback = (rows) => {
929
+ try {
930
+ onUpdate(rows);
931
+ } catch (error) {
932
+ logger.error("Error in collection update callback", { error });
933
+ if (onError) onError(error instanceof Error ? error : new Error(String(error)));
934
+ }
935
+ };
936
+ this.realtimeService.subscribeToCollection(subscriptionId, {
937
+ clientId: "driver",
938
+ path,
939
+ filter,
940
+ orderBy,
941
+ order,
942
+ limit,
943
+ startAfter,
944
+ searchString
945
+ }, callback);
946
+ return () => {
947
+ this.realtimeService.unsubscribe(subscriptionId);
948
+ };
949
+ }
950
+ /**
951
+ * Fetch a single row
952
+ */
953
+ async fetchOne({ path, id, databaseId, collection }) {
954
+ let row = await this.dataService.fetchOne(path, id, databaseId);
955
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
956
+ if (row && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
957
+ const contextForCallback = {
958
+ user: this.user,
959
+ driver: this,
960
+ data: this.data,
961
+ client: this.client,
962
+ storageSource: this.client?.storage
963
+ };
964
+ let processedRow = row;
965
+ if (globalCallbacks?.afterRead) processedRow = await globalCallbacks.afterRead({
966
+ collection: resolvedCollection,
967
+ path,
968
+ row: processedRow,
969
+ context: contextForCallback
970
+ }) ?? processedRow;
971
+ if (callbacks?.afterRead) processedRow = await callbacks.afterRead({
972
+ collection: resolvedCollection,
973
+ path,
974
+ row: processedRow,
975
+ context: contextForCallback
976
+ }) ?? processedRow;
977
+ if (propertyCallbacks?.afterRead) processedRow = await propertyCallbacks.afterRead({
978
+ collection: resolvedCollection,
979
+ path,
980
+ row: processedRow,
981
+ context: contextForCallback
982
+ }) ?? processedRow;
983
+ row = processedRow;
984
+ }
985
+ return row;
986
+ }
987
+ /**
988
+ * Listen to row changes
989
+ */
990
+ listenOne({ path, id, collection, onUpdate, onError }) {
991
+ const subscriptionId = this.generateSubscriptionId();
992
+ const callback = (row) => {
993
+ try {
994
+ onUpdate(row);
995
+ } catch (error) {
996
+ logger.error("Error in row update callback", { error });
997
+ if (onError) onError(error instanceof Error ? error : new Error(String(error)));
998
+ }
999
+ };
1000
+ this.realtimeService.subscribeToOne(subscriptionId, {
1001
+ clientId: "driver",
1002
+ path,
1003
+ id
1004
+ }, callback);
1005
+ return () => {
1006
+ this.realtimeService.unsubscribe(subscriptionId);
1007
+ };
1008
+ }
1009
+ /**
1010
+ * Save an row (create or update)
1011
+ */
1012
+ async save({ path, id, values, collection, status }) {
1013
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
1014
+ let updatedValues = values;
1015
+ const contextForCallback = {
1016
+ user: this.user,
1017
+ driver: this,
1018
+ data: this.data,
1019
+ client: this.client,
1020
+ storageSource: this.client?.storage
1021
+ };
1022
+ let previousValuesForHistory;
1023
+ if (status === "existing" && id) {
1024
+ const existing = await this.dataService.fetchOne(path, id, resolvedCollection?.databaseId);
1025
+ if (existing) {
1026
+ const { id: _existingId, ...existingValues } = existing;
1027
+ previousValuesForHistory = existingValues;
1028
+ }
1029
+ }
1030
+ if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
1031
+ if (globalCallbacks?.beforeSave) {
1032
+ const result = await globalCallbacks.beforeSave({
1033
+ collection: resolvedCollection,
1034
+ path,
1035
+ id,
1036
+ values: updatedValues,
1037
+ previousValues: previousValuesForHistory,
1038
+ status,
1039
+ context: contextForCallback
1040
+ });
1041
+ if (result) updatedValues = mergeDeep(updatedValues, result);
1042
+ }
1043
+ if (callbacks?.beforeSave) {
1044
+ const result = await callbacks.beforeSave({
1045
+ collection: resolvedCollection,
1046
+ path,
1047
+ id,
1048
+ values: updatedValues,
1049
+ previousValues: previousValuesForHistory,
1050
+ status,
1051
+ context: contextForCallback
1052
+ });
1053
+ if (result) updatedValues = mergeDeep(updatedValues, result);
1054
+ }
1055
+ if (propertyCallbacks?.beforeSave) {
1056
+ const result = await propertyCallbacks.beforeSave({
1057
+ collection: resolvedCollection,
1058
+ path,
1059
+ id,
1060
+ values: updatedValues,
1061
+ previousValues: previousValuesForHistory,
1062
+ status,
1063
+ context: contextForCallback
1064
+ });
1065
+ if (result) updatedValues = mergeDeep(updatedValues, result);
1066
+ }
1067
+ }
1068
+ if (resolvedCollection?.properties) updatedValues = updateDateAutoValues({
1069
+ inputValues: updatedValues,
1070
+ properties: resolvedCollection.properties,
1071
+ status: status ?? "new",
1072
+ timestampNowValue: /* @__PURE__ */ new Date()
1073
+ });
1074
+ try {
1075
+ let savedRow = await this.dataService.save(path, updatedValues, id, resolvedCollection?.databaseId);
1076
+ if (savedRow && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
1077
+ if (globalCallbacks?.afterRead) savedRow = await globalCallbacks.afterRead({
1078
+ collection: resolvedCollection,
1079
+ path,
1080
+ row: savedRow,
1081
+ context: contextForCallback
1082
+ }) ?? savedRow;
1083
+ if (callbacks?.afterRead) savedRow = await callbacks.afterRead({
1084
+ collection: resolvedCollection,
1085
+ path,
1086
+ row: savedRow,
1087
+ context: contextForCallback
1088
+ }) ?? savedRow;
1089
+ if (propertyCallbacks?.afterRead) savedRow = await propertyCallbacks.afterRead({
1090
+ collection: resolvedCollection,
1091
+ path,
1092
+ row: savedRow,
1093
+ context: contextForCallback
1094
+ }) ?? savedRow;
1095
+ }
1096
+ const savedId = savedRow.id;
1097
+ const { id: _savedId, ...savedValues } = savedRow;
1098
+ if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
1099
+ if (globalCallbacks?.afterSave) await globalCallbacks.afterSave({
1100
+ collection: resolvedCollection,
1101
+ path,
1102
+ id: savedId,
1103
+ values: savedValues,
1104
+ previousValues: previousValuesForHistory,
1105
+ status,
1106
+ context: contextForCallback
1107
+ });
1108
+ if (callbacks?.afterSave) await callbacks.afterSave({
1109
+ collection: resolvedCollection,
1110
+ path,
1111
+ id: savedId,
1112
+ values: savedValues,
1113
+ previousValues: previousValuesForHistory,
1114
+ status,
1115
+ context: contextForCallback
1116
+ });
1117
+ if (propertyCallbacks?.afterSave) await propertyCallbacks.afterSave({
1118
+ collection: resolvedCollection,
1119
+ path,
1120
+ id: savedId,
1121
+ values: savedValues,
1122
+ previousValues: previousValuesForHistory,
1123
+ status,
1124
+ context: contextForCallback
1125
+ });
1126
+ }
1127
+ if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
1128
+ tableName: path,
1129
+ id: savedId.toString(),
1130
+ action: status === "new" ? "create" : "update",
1131
+ values: savedValues,
1132
+ previousValues: previousValuesForHistory,
1133
+ updatedBy: this.user?.uid
1134
+ }).catch((err) => {
1135
+ logger.error(`Failed to record history for ${path}/${savedId}`, { error: err });
1136
+ });
1137
+ await this.realtimeService.notifyUpdate(path, savedId.toString(), savedRow);
1138
+ return savedRow;
1139
+ } catch (error) {
1140
+ if (callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
1141
+ if (callbacks?.afterSaveError) await callbacks.afterSaveError({
1142
+ collection: resolvedCollection,
1143
+ path,
1144
+ id: id || "unknown",
1145
+ values: updatedValues,
1146
+ previousValues: void 0,
1147
+ status,
1148
+ context: contextForCallback
1149
+ });
1150
+ if (propertyCallbacks?.afterSaveError) await propertyCallbacks.afterSaveError({
1151
+ collection: resolvedCollection,
1152
+ path,
1153
+ id: id || "unknown",
1154
+ values: updatedValues,
1155
+ previousValues: void 0,
1156
+ status,
1157
+ context: contextForCallback
1158
+ });
1159
+ }
1160
+ throw error;
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Delete an row
1165
+ */
1166
+ async delete({ row, collection }) {
1167
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
1168
+ const callbackRow = {
1169
+ id: row.id,
1170
+ ...row.values ?? {}
1171
+ };
1172
+ const contextForCallback = {
1173
+ user: this.user,
1174
+ driver: this,
1175
+ data: this.data,
1176
+ client: this.client,
1177
+ storageSource: this.client?.storage
1178
+ };
1179
+ if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
1180
+ let preventDefault = false;
1181
+ if (globalCallbacks?.beforeDelete) {
1182
+ if (await globalCallbacks.beforeDelete({
1183
+ collection: resolvedCollection,
1184
+ path: row.path,
1185
+ id: row.id,
1186
+ row: callbackRow,
1187
+ context: contextForCallback
1188
+ }) === false) preventDefault = true;
1189
+ }
1190
+ if (callbacks?.beforeDelete) {
1191
+ if (await callbacks.beforeDelete({
1192
+ collection: resolvedCollection,
1193
+ path: row.path,
1194
+ id: row.id,
1195
+ row: callbackRow,
1196
+ context: contextForCallback
1197
+ }) === false) preventDefault = true;
1198
+ }
1199
+ if (propertyCallbacks?.beforeDelete) {
1200
+ if (await propertyCallbacks.beforeDelete({
1201
+ collection: resolvedCollection,
1202
+ path: row.path,
1203
+ id: row.id,
1204
+ row: callbackRow,
1205
+ context: contextForCallback
1206
+ }) === false) preventDefault = true;
1207
+ }
1208
+ if (preventDefault) return;
1209
+ }
1210
+ await this.dataService.delete(row.path, row.id);
1211
+ if (globalCallbacks?.afterDelete || callbacks?.afterDelete || propertyCallbacks?.afterDelete) {
1212
+ if (globalCallbacks?.afterDelete) await globalCallbacks.afterDelete({
1213
+ collection: resolvedCollection,
1214
+ path: row.path,
1215
+ id: row.id,
1216
+ row: callbackRow,
1217
+ context: contextForCallback
1218
+ });
1219
+ if (callbacks?.afterDelete) await callbacks.afterDelete({
1220
+ collection: resolvedCollection,
1221
+ path: row.path,
1222
+ id: row.id,
1223
+ row: callbackRow,
1224
+ context: contextForCallback
1225
+ });
1226
+ if (propertyCallbacks?.afterDelete) await propertyCallbacks.afterDelete({
1227
+ collection: resolvedCollection,
1228
+ path: row.path,
1229
+ id: row.id,
1230
+ row: callbackRow,
1231
+ context: contextForCallback
1232
+ });
1233
+ }
1234
+ if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
1235
+ action: "delete",
1236
+ id: String(row.id),
1237
+ tableName: row.path,
1238
+ previousValues: row.values,
1239
+ updatedBy: this.user?.uid
1240
+ }).catch((err) => {
1241
+ logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
1242
+ });
1243
+ await this.realtimeService.notifyUpdate(row.path, String(row.id), null);
1244
+ }
1245
+ /**
1246
+ * Check if a field value is unique
1247
+ */
1248
+ async checkUniqueField(path, name, value, id, collection) {
1249
+ return this.dataService.checkUniqueField(path, name, value, id);
1250
+ }
1251
+ /**
1252
+ * Generate a new row ID
1253
+ */
1254
+ generateId(path, collection) {
1255
+ return this.dataService.generateId();
1256
+ }
1257
+ /**
1258
+ * Count rows in a collection
1259
+ */
1260
+ async count({ path, collection, filter }) {
1261
+ return this.dataService.count(path, { filter });
1262
+ }
1263
+ /**
1264
+ * Generate a unique subscription ID
1265
+ */
1266
+ generateSubscriptionId() {
1267
+ return `mongo_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1268
+ }
1269
+ /**
1270
+ * Check if the delegate is ready
1271
+ */
1272
+ isReady() {
1273
+ return this.initialised;
1274
+ }
1275
+ /**
1276
+ * Get the underlying row service for direct access
1277
+ */
1278
+ getDataService() {
1279
+ return this.dataService;
1280
+ }
1281
+ /**
1282
+ * Get the underlying realtime service for direct access
1283
+ */
1284
+ getRealtimeService() {
1285
+ return this.realtimeService;
1286
+ }
1287
+ /**
1288
+ * Scope the MongoDriver with an authenticated user context
1289
+ */
1290
+ async withAuth(user) {
1291
+ return new AuthenticatedMongoDriver(this, user);
1292
+ }
1293
+ };
1294
+ var AuthenticatedMongoDriver = class {
1295
+ delegate;
1296
+ key = "mongodb";
1297
+ initialised = true;
1298
+ user;
1299
+ data;
1300
+ constructor(delegate, user) {
1301
+ this.delegate = delegate;
1302
+ this.user = user;
1303
+ this.data = buildSdkData(this);
1304
+ }
1305
+ currentTime() {
1306
+ return this.delegate.currentTime();
1307
+ }
1308
+ async fetchCollection(props) {
1309
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
1310
+ const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
1311
+ if (rlsFilter === null) return [];
1312
+ const userQuery = MongoConditionBuilder.buildQuery({
1313
+ filter: props.filter,
1314
+ searchString: props.searchString,
1315
+ properties: resolvedCollection?.properties
1316
+ });
1317
+ const combinedQuery = Object.keys(rlsFilter).length > 0 ? { $and: [userQuery, rlsFilter] } : userQuery;
1318
+ const rows = await this.delegate.getDataService().fetchCollection(props.path, {
1319
+ ...props,
1320
+ rawQuery: combinedQuery,
1321
+ collection: resolvedCollection
1322
+ });
1323
+ const { callbacks, globalCallbacks, propertyCallbacks } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
1324
+ if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
1325
+ const contextForCallback = {
1326
+ user: this.user,
1327
+ driver: this,
1328
+ data: this.data,
1329
+ client: this.delegate.client,
1330
+ storageSource: this.delegate.client?.storage
1331
+ };
1332
+ return Promise.all(rows.map(async (row) => {
1333
+ let fetched = row;
1334
+ if (globalCallbacks?.afterRead) fetched = await globalCallbacks.afterRead({
1335
+ collection: resolvedCollection,
1336
+ path: props.path,
1337
+ row: fetched,
1338
+ context: contextForCallback
1339
+ }) ?? fetched;
1340
+ if (callbacks?.afterRead) fetched = await callbacks.afterRead({
1341
+ collection: resolvedCollection,
1342
+ path: props.path,
1343
+ row: fetched,
1344
+ context: contextForCallback
1345
+ }) ?? fetched;
1346
+ if (propertyCallbacks?.afterRead) fetched = await propertyCallbacks.afterRead({
1347
+ collection: resolvedCollection,
1348
+ path: props.path,
1349
+ row: fetched,
1350
+ context: contextForCallback
1351
+ }) ?? fetched;
1352
+ return fetched;
1353
+ }));
1354
+ }
1355
+ return rows;
1356
+ }
1357
+ listenCollection(props) {
1358
+ const unsubscribe = this.delegate.listenCollection(props);
1359
+ const authContext = {
1360
+ userId: this.user.uid,
1361
+ roles: this.user.roles ?? []
1362
+ };
1363
+ const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
1364
+ const lastSub = Array.from(subscriptions.entries()).pop()?.[1];
1365
+ if (lastSub && lastSub.config.clientId === "driver") lastSub.authContext = authContext;
1366
+ return unsubscribe;
1367
+ }
1368
+ async fetchOne(props) {
1369
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
1370
+ const row = await this.delegate.fetchOne(props);
1371
+ if (row) {
1372
+ if (!checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(row, props.path), "select", { onUnknown: "deny" })) return;
1373
+ }
1374
+ return row;
1375
+ }
1376
+ listenOne(props) {
1377
+ const unsubscribe = this.delegate.listenOne(props);
1378
+ const authContext = {
1379
+ userId: this.user.uid,
1380
+ roles: this.user.roles ?? []
1381
+ };
1382
+ const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
1383
+ const lastSub = Array.from(subscriptions.entries()).pop()?.[1];
1384
+ if (lastSub && lastSub.config.clientId === "driver") lastSub.authContext = authContext;
1385
+ return unsubscribe;
1386
+ }
1387
+ async save(props) {
1388
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
1389
+ if (props.status === "existing" && props.id) {
1390
+ const existing = await this.delegate.fetchOne({
1391
+ path: props.path,
1392
+ id: props.id,
1393
+ collection: resolvedCollection
1394
+ });
1395
+ if (!existing || !checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(existing, props.path), "update", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
1396
+ } else {
1397
+ const tempEntity = {
1398
+ id: props.id || "new",
1399
+ path: props.path,
1400
+ values: props.values
1401
+ };
1402
+ if (!checkOperation(resolvedCollection, { user: this.user }, tempEntity, "insert", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
1403
+ }
1404
+ const saved = await this.delegate.save({
1405
+ ...props,
1406
+ collection: resolvedCollection
1407
+ });
1408
+ if (!checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(saved, props.path), props.status === "existing" ? "update" : "insert", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
1409
+ return saved;
1410
+ }
1411
+ async delete(props) {
1412
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.row.path);
1413
+ const existing = await this.delegate.fetchOne({
1414
+ path: props.row.path,
1415
+ id: props.row.id,
1416
+ collection: resolvedCollection
1417
+ });
1418
+ if (!existing || !checkOperation(resolvedCollection, { user: this.user }, rowToEntityForCheck(existing, props.row.path), "delete", { onUnknown: "deny" })) throw ApiError.forbidden("Forbidden");
1419
+ return this.delegate.delete(props);
1420
+ }
1421
+ async checkUniqueField(path, name, value, id, collection) {
1422
+ return this.delegate.checkUniqueField(path, name, value, id, collection);
1423
+ }
1424
+ generateId(path, collection) {
1425
+ return this.delegate.generateId(path, collection);
1426
+ }
1427
+ async count(props) {
1428
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
1429
+ const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
1430
+ if (rlsFilter === null) return 0;
1431
+ const userQuery = MongoConditionBuilder.buildQuery({
1432
+ filter: props.filter,
1433
+ searchString: props.searchString,
1434
+ properties: resolvedCollection?.properties
1435
+ });
1436
+ const combinedQuery = Object.keys(rlsFilter).length > 0 ? { $and: [userQuery, rlsFilter] } : userQuery;
1437
+ return this.delegate.getDataService().count(props.path, {
1438
+ ...props,
1439
+ rawQuery: combinedQuery
1440
+ });
1441
+ }
1442
+ isReady() {
1443
+ return this.delegate.isReady();
1444
+ }
1445
+ };
1446
+ /**
1447
+ * Wrap a flat row into the Entity shape expected by `checkOperation`,
1448
+ * which evaluates security rules against `row.values`.
1449
+ */
1450
+ function rowToEntityForCheck(row, path) {
1451
+ return {
1452
+ id: row.id,
1453
+ path,
1454
+ values: row
1455
+ };
1456
+ }
1457
+ function getMongoFilterForSQL(sqlString, user) {
1458
+ let cleanedSQL = sqlString.trim();
1459
+ while (cleanedSQL.startsWith("(") && cleanedSQL.endsWith(")")) {
1460
+ let openCount = 0;
1461
+ let isEnclosing = true;
1462
+ for (let i = 0; i < cleanedSQL.length - 1; i++) {
1463
+ if (cleanedSQL[i] === "(") openCount++;
1464
+ else if (cleanedSQL[i] === ")") openCount--;
1465
+ if (openCount === 0) {
1466
+ isEnclosing = false;
1467
+ break;
1468
+ }
1469
+ }
1470
+ if (isEnclosing) cleanedSQL = cleanedSQL.substring(1, cleanedSQL.length - 1).trim();
1471
+ else break;
1472
+ }
1473
+ const splitByTopLevel = (str, delimiter) => {
1474
+ const parts = [];
1475
+ let current = "";
1476
+ let openCount = 0;
1477
+ let i = 0;
1478
+ while (i < str.length) {
1479
+ if (str[i] === "(") openCount++;
1480
+ else if (str[i] === ")") openCount--;
1481
+ if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
1482
+ parts.push(current);
1483
+ current = "";
1484
+ i += delimiter.length;
1485
+ } else {
1486
+ current += str[i];
1487
+ i++;
1488
+ }
1489
+ }
1490
+ parts.push(current);
1491
+ return parts;
1492
+ };
1493
+ const orParts = splitByTopLevel(cleanedSQL, " OR ");
1494
+ if (orParts.length > 1) {
1495
+ const subFilters = orParts.map((part) => getMongoFilterForSQL(part, user)).filter((f) => f !== null);
1496
+ if (subFilters.length === 0) return null;
1497
+ if (subFilters.length === 1) return subFilters[0];
1498
+ return { $or: subFilters };
1499
+ }
1500
+ const andParts = splitByTopLevel(cleanedSQL, " AND ");
1501
+ if (andParts.length > 1) {
1502
+ const subFilters = andParts.map((part) => getMongoFilterForSQL(part, user)).filter((f) => f !== null);
1503
+ if (subFilters.length === 0) return null;
1504
+ if (subFilters.length === 1) return subFilters[0];
1505
+ return { $and: subFilters };
1506
+ }
1507
+ const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
1508
+ if (roleIntersectMatch && roleIntersectMatch[1]) {
1509
+ const requiredRoles = roleIntersectMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
1510
+ const userRoles = user.roles || [];
1511
+ return requiredRoles.some((r) => userRoles.includes(r)) ? {} : { _id: { $exists: false } };
1512
+ }
1513
+ const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
1514
+ if (roleContainMatch && roleContainMatch[1]) {
1515
+ const requiredRoles = roleContainMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
1516
+ const userRoles = user.roles || [];
1517
+ return requiredRoles.every((r) => userRoles.includes(r)) ? {} : { _id: { $exists: false } };
1518
+ }
1519
+ const pattern1 = /* @__PURE__ */ new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
1520
+ const pattern2 = /* @__PURE__ */ new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
1521
+ const match1 = cleanedSQL.match(pattern1);
1522
+ if (match1 && match1[1]) return { [match1[1]]: user.uid };
1523
+ const match2 = cleanedSQL.match(pattern2);
1524
+ if (match2 && match2[1]) return { [match2[1]]: user.uid };
1525
+ const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
1526
+ if (simpleEqualityMatch) {
1527
+ const field = simpleEqualityMatch[1];
1528
+ const operator = simpleEqualityMatch[2];
1529
+ const value = simpleEqualityMatch[3];
1530
+ if (operator === "=") return { [field]: value };
1531
+ if (operator === "!=") return { [field]: { $ne: value } };
1532
+ }
1533
+ return {};
1534
+ }
1535
+ function getMongoFilterForRule(rule, user) {
1536
+ if (rule.access === "public") return {};
1537
+ const filters = [];
1538
+ if (rule.ownerField) filters.push({ [rule.ownerField]: user.uid });
1539
+ if (rule.using) {
1540
+ const f = getMongoFilterForSQL(rule.using, user);
1541
+ if (f) filters.push(f);
1542
+ }
1543
+ if (rule.withCheck) {
1544
+ const f = getMongoFilterForSQL(rule.withCheck, user);
1545
+ if (f) filters.push(f);
1546
+ }
1547
+ if (filters.length === 0) return {};
1548
+ if (filters.length === 1) return filters[0];
1549
+ return { $and: filters };
1550
+ }
1551
+ function buildMongoFilterFromSecurityRules(collection, user, targetOperation) {
1552
+ if (!collection || !collection.securityRules || collection.securityRules.length === 0) return {};
1553
+ const applicableRules = collection.securityRules.filter((r) => r.operation === targetOperation || r.operation === "all" || r.operations?.includes(targetOperation) || r.operations?.includes("all"));
1554
+ if (applicableRules.length === 0) return null;
1555
+ const userRoles = [...user.roles ?? [], "public"];
1556
+ const roleApplicableRules = applicableRules.filter((rule) => {
1557
+ if (!rule.roles || rule.roles.length === 0) return true;
1558
+ return rule.roles.some((r) => userRoles.includes(r));
1559
+ });
1560
+ if (roleApplicableRules.length === 0) return null;
1561
+ const permissiveFilters = [];
1562
+ const restrictiveFilters = [];
1563
+ for (const rule of roleApplicableRules) {
1564
+ const mode = rule.mode || "permissive";
1565
+ const filter = getMongoFilterForRule(rule, user);
1566
+ if (filter === null) {
1567
+ if (mode === "restrictive") return null;
1568
+ continue;
1569
+ }
1570
+ if (mode === "restrictive") restrictiveFilters.push(filter);
1571
+ else permissiveFilters.push(filter);
1572
+ }
1573
+ const finalAnds = [];
1574
+ if (permissiveFilters.length > 0) {
1575
+ if (!permissiveFilters.some((f) => Object.keys(f).length === 0)) if (permissiveFilters.length === 1) finalAnds.push(permissiveFilters[0]);
1576
+ else finalAnds.push({ $or: permissiveFilters });
1577
+ } else return null;
1578
+ if (restrictiveFilters.length > 0) {
1579
+ for (const rf of restrictiveFilters) if (Object.keys(rf).length > 0) finalAnds.push(rf);
1580
+ }
1581
+ if (finalAnds.length === 0) return {};
1582
+ if (finalAnds.length === 1) return finalAnds[0];
1583
+ return { $and: finalAnds };
1584
+ }
1585
+ //#endregion
1586
+ //#region src/factory.ts
1587
+ /**
1588
+ * Simple in-memory collection registry for MongoDB.
1589
+ */
1590
+ var MongoCollectionRegistry = class {
1591
+ collections = /* @__PURE__ */ new Map();
1592
+ _globalCallbacks;
1593
+ /**
1594
+ * Register a collection
1595
+ */
1596
+ register(collection) {
1597
+ this.collections.set(collection.name, collection);
1598
+ }
1599
+ /**
1600
+ * Get a collection by its path
1601
+ */
1602
+ getCollectionByPath(path) {
1603
+ return this.collections.get(path);
1604
+ }
1605
+ /**
1606
+ * Get all registered collections
1607
+ */
1608
+ getCollections() {
1609
+ return Array.from(this.collections.values());
1610
+ }
1611
+ /**
1612
+ * Get the currently registered global callbacks, if any.
1613
+ */
1614
+ getGlobalCallbacks() {
1615
+ return this._globalCallbacks;
1616
+ }
1617
+ /**
1618
+ * Set global lifecycle callbacks that apply to every collection.
1619
+ */
1620
+ setGlobalCallbacks(callbacks) {
1621
+ this._globalCallbacks = callbacks;
1622
+ }
1623
+ };
1624
+ /**
1625
+ * Create a complete MongoDB backend instance.
1626
+ *
1627
+ * This factory function creates all the necessary services for a MongoDB backend:
1628
+ * - MongoDBConnection (database connection wrapper)
1629
+ * - MongoDataService (implements DataRepository)
1630
+ * - MongoRealtimeService (implements RealtimeProvider)
1631
+ * - MongoCollectionRegistry (implements CollectionRegistryInterface)
1632
+ * - MongoDriver (for Rebase integration)
1633
+ *
1634
+ * @example
1635
+ * ```typescript
1636
+ * import { createMongoBackend } from "@rebasepro/server-mongo";
1637
+ *
1638
+ * const client = new MongoClient("mongodb://localhost:27017");
1639
+ * await client.connect();
1640
+ * const db = client.db("my_database");
1641
+ *
1642
+ * const backend = createMongoBackend({
1643
+ * type: "mongodb",
1644
+ * connection: db,
1645
+ * client: client,
1646
+ * collections: myCollections
1647
+ * });
1648
+ *
1649
+ * // Use the backend
1650
+ * const rows = await backend.entityRepository.fetchCollection("users", {});
1651
+ * ```
1652
+ */
1653
+ function createMongoBackend(config) {
1654
+ const { connection: db, client, collections } = config;
1655
+ const collectionRegistry = new MongoCollectionRegistry();
1656
+ if (collections) collections.forEach((collection) => collectionRegistry.register(collection));
1657
+ const dataService = new MongoDataService(db);
1658
+ const realtimeService = new MongoRealtimeService(db);
1659
+ const driver = new MongoDriver(db, realtimeService, new MongoHistoryService(db, config.historyRetention), collectionRegistry);
1660
+ return {
1661
+ connection: new MongoDBConnection(db, client),
1662
+ entityRepository: dataService,
1663
+ realtimeProvider: realtimeService,
1664
+ collectionRegistry,
1665
+ admin: {
1666
+ async executeAggregate(pipeline) {
1667
+ const firstStage = pipeline[0];
1668
+ const collName = typeof firstStage.$from === "string" ? firstStage.$from : "__admin__";
1669
+ return await db.collection(collName).aggregate(pipeline).toArray();
1670
+ },
1671
+ async fetchCollectionStats(collectionName) {
1672
+ const stats = await db.command({ collStats: collectionName });
1673
+ return {
1674
+ count: stats.count,
1675
+ sizeBytes: stats.size
1676
+ };
1677
+ },
1678
+ async fetchUnmappedTables(mappedPaths) {
1679
+ const names = (await db.listCollections().toArray()).map((c) => c.name).filter((n) => !n.startsWith("system."));
1680
+ if (!mappedPaths || mappedPaths.length === 0) return names;
1681
+ const mappedSet = new Set(mappedPaths.map((p) => p.toLowerCase()));
1682
+ return names.filter((n) => !mappedSet.has(n.toLowerCase()));
1683
+ },
1684
+ async fetchTableMetadata(collectionName) {
1685
+ const sample = await db.collection(collectionName).findOne();
1686
+ if (!sample) return {
1687
+ columns: [],
1688
+ foreignKeys: [],
1689
+ junctions: [],
1690
+ policies: []
1691
+ };
1692
+ return {
1693
+ columns: Object.entries(sample).map(([key, value]) => ({
1694
+ column_name: key,
1695
+ data_type: typeof value,
1696
+ udt_name: typeof value,
1697
+ is_nullable: "YES",
1698
+ column_default: null,
1699
+ character_maximum_length: null
1700
+ })),
1701
+ foreignKeys: [],
1702
+ junctions: [],
1703
+ policies: []
1704
+ };
1705
+ }
1706
+ },
1707
+ async initialize() {},
1708
+ async healthCheck() {
1709
+ const start = Date.now();
1710
+ try {
1711
+ await db.command({ ping: 1 });
1712
+ return {
1713
+ healthy: true,
1714
+ latencyMs: Date.now() - start
1715
+ };
1716
+ } catch {
1717
+ return {
1718
+ healthy: false,
1719
+ latencyMs: Date.now() - start
1720
+ };
1721
+ }
1722
+ },
1723
+ async destroy() {
1724
+ await client.close();
1725
+ },
1726
+ db,
1727
+ client,
1728
+ driver,
1729
+ dataService,
1730
+ realtimeService
1731
+ };
1732
+ }
1733
+ /**
1734
+ * Create a MongoDB DataDriver.
1735
+ *
1736
+ * This is a convenience function when you only need the DataDriver
1737
+ * without the full backend instance.
1738
+ *
1739
+ * @example
1740
+ * ```typescript
1741
+ * import { createMongoDelegate } from "@rebasepro/server-mongo";
1742
+ *
1743
+ * const delegate = createMongoDelegate(db);
1744
+ * ```
1745
+ */
1746
+ function createMongoDelegate(db, realtimeService, historyService, registry) {
1747
+ return new MongoDriver(db, realtimeService ?? new MongoRealtimeService(db), historyService ?? new MongoHistoryService(db), registry);
1748
+ }
1749
+ /**
1750
+ * Create a RealtimeService for MongoDB.
1751
+ *
1752
+ * @example
1753
+ * ```typescript
1754
+ * import { createMongoRealtimeService } from "@rebasepro/server-mongo";
1755
+ *
1756
+ * const realtimeService = createMongoRealtimeService(db);
1757
+ * ```
1758
+ */
1759
+ function createMongoRealtimeService(db) {
1760
+ return new MongoRealtimeService(db);
1761
+ }
1762
+ /**
1763
+ * Create a MongoDB row repository.
1764
+ *
1765
+ * @example
1766
+ * ```typescript
1767
+ * import { createMongoEntityRepository } from "@rebasepro/server-mongo";
1768
+ *
1769
+ * const repository = createMongoEntityRepository(db);
1770
+ * const users = await repository.fetchCollection("users", {});
1771
+ * ```
1772
+ */
1773
+ function createMongoEntityRepository(db) {
1774
+ return new MongoDataService(db);
1775
+ }
1776
+ /**
1777
+ * Check if a backend config is for MongoDB.
1778
+ */
1779
+ function isMongoBackendConfig(config) {
1780
+ return config.type === "mongodb" && typeof config.connection !== "undefined" && typeof config.client !== "undefined";
1781
+ }
1782
+ /**
1783
+ * Check if a driver config is for MongoDB.
1784
+ */
1785
+ function isMongoDriverConfig(obj) {
1786
+ return typeof obj === "object" && obj !== null && "type" in obj && obj.type === "mongodb" && "connection" in obj && "client" in obj;
1787
+ }
1788
+ //#endregion
1789
+ //#region src/auth/services.ts
1790
+ function escapeRegExp(str) {
1791
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1792
+ }
1793
+ function toUser(doc) {
1794
+ return {
1795
+ id: doc._id || doc.id,
1796
+ email: doc.email,
1797
+ passwordHash: doc.passwordHash ?? null,
1798
+ displayName: doc.displayName ?? null,
1799
+ photoUrl: doc.photoUrl ?? null,
1800
+ emailVerified: doc.emailVerified ?? false,
1801
+ emailVerificationToken: doc.emailVerificationToken ?? null,
1802
+ emailVerificationSentAt: doc.emailVerificationSentAt ? new Date(doc.emailVerificationSentAt) : null,
1803
+ createdAt: new Date(doc.createdAt),
1804
+ updatedAt: new Date(doc.updatedAt)
1805
+ };
1806
+ }
1807
+ var MongoUserService = class {
1808
+ db;
1809
+ constructor(db) {
1810
+ this.db = db;
1811
+ }
1812
+ get collection() {
1813
+ return this.db.collection("rebase_users");
1814
+ }
1815
+ get identitiesCollection() {
1816
+ return this.db.collection("rebase_user_identities");
1817
+ }
1818
+ get userRolesCollection() {
1819
+ return this.db.collection("rebase_user_roles");
1820
+ }
1821
+ get rolesCollection() {
1822
+ return this.db.collection("rebase_roles");
1823
+ }
1824
+ async createUser(data) {
1825
+ const id = new ObjectId().toString();
1826
+ const now = /* @__PURE__ */ new Date();
1827
+ const doc = {
1828
+ _id: id,
1829
+ id,
1830
+ email: data.email.toLowerCase(),
1831
+ passwordHash: data.passwordHash ?? null,
1832
+ displayName: data.displayName ?? null,
1833
+ photoUrl: data.photoUrl ?? null,
1834
+ emailVerified: data.emailVerified ?? false,
1835
+ createdAt: now,
1836
+ updatedAt: now
1837
+ };
1838
+ await this.collection.insertOne(doc);
1839
+ return toUser(doc);
1840
+ }
1841
+ async getUserById(id) {
1842
+ const doc = await this.collection.findOne({ id });
1843
+ return doc ? toUser(doc) : null;
1844
+ }
1845
+ async getUserByEmail(email) {
1846
+ const doc = await this.collection.findOne({ email: email.toLowerCase() });
1847
+ return doc ? toUser(doc) : null;
1848
+ }
1849
+ async getUserByIdentity(provider, providerId) {
1850
+ const identity = await this.identitiesCollection.findOne({
1851
+ provider,
1852
+ providerId
1853
+ });
1854
+ if (!identity) return null;
1855
+ return this.getUserById(identity.userId);
1856
+ }
1857
+ async getUserIdentities(userId) {
1858
+ return (await this.identitiesCollection.find({ userId }).toArray()).map((doc) => ({
1859
+ id: doc.id,
1860
+ userId: doc.userId,
1861
+ provider: doc.provider,
1862
+ providerId: doc.providerId,
1863
+ profileData: doc.profileData ?? null,
1864
+ createdAt: new Date(doc.createdAt),
1865
+ updatedAt: new Date(doc.updatedAt)
1866
+ }));
1867
+ }
1868
+ async linkUserIdentity(userId, provider, providerId, profileData) {
1869
+ const now = /* @__PURE__ */ new Date();
1870
+ await this.identitiesCollection.updateOne({
1871
+ provider,
1872
+ providerId
1873
+ }, {
1874
+ $setOnInsert: {
1875
+ _id: new ObjectId().toString(),
1876
+ id: new ObjectId().toString(),
1877
+ userId,
1878
+ provider,
1879
+ providerId,
1880
+ createdAt: now
1881
+ },
1882
+ $set: {
1883
+ profileData: profileData ?? null,
1884
+ updatedAt: now
1885
+ }
1886
+ }, { upsert: true });
1887
+ }
1888
+ async updateUser(id, data) {
1889
+ const updateData = {
1890
+ ...data,
1891
+ updatedAt: /* @__PURE__ */ new Date()
1892
+ };
1893
+ if (typeof updateData.email === "string") updateData.email = updateData.email.toLowerCase();
1894
+ await this.collection.updateOne({ id }, { $set: updateData });
1895
+ return this.getUserById(id);
1896
+ }
1897
+ async deleteUser(id) {
1898
+ await this.collection.deleteOne({ id });
1899
+ await this.identitiesCollection.deleteMany({ userId: id });
1900
+ await this.userRolesCollection.deleteMany({ userId: id });
1901
+ }
1902
+ async listUsers() {
1903
+ return (await this.collection.find().toArray()).map(toUser);
1904
+ }
1905
+ async listUsersPaginated(options) {
1906
+ const limit = options?.limit ?? 25;
1907
+ const offset = options?.offset ?? 0;
1908
+ const search = options?.search?.trim() || "";
1909
+ const orderBy = options?.orderBy || "createdAt";
1910
+ const orderDir = options?.orderDir || "desc";
1911
+ const roleId = options?.roleId;
1912
+ const query = {};
1913
+ if (search) {
1914
+ const escapedSearch = escapeRegExp(search);
1915
+ query.$or = [{ email: {
1916
+ $regex: escapedSearch,
1917
+ $options: "i"
1918
+ } }, { displayName: {
1919
+ $regex: escapedSearch,
1920
+ $options: "i"
1921
+ } }];
1922
+ }
1923
+ if (roleId) query.id = { $in: (await this.userRolesCollection.find({ roleId }).toArray()).map((ur) => ur.userId) };
1924
+ const sort = {};
1925
+ sort[orderBy] = orderDir === "asc" ? 1 : -1;
1926
+ const total = await this.collection.countDocuments(query);
1927
+ return {
1928
+ users: (await this.collection.find(query).sort(sort).skip(offset).limit(limit).toArray()).map(toUser),
1929
+ total,
1930
+ limit,
1931
+ offset
1932
+ };
1933
+ }
1934
+ async updatePassword(id, passwordHash) {
1935
+ await this.collection.updateOne({ id }, { $set: {
1936
+ passwordHash,
1937
+ updatedAt: /* @__PURE__ */ new Date()
1938
+ } });
1939
+ }
1940
+ async setEmailVerified(id, verified) {
1941
+ await this.collection.updateOne({ id }, { $set: {
1942
+ emailVerified: verified,
1943
+ emailVerificationToken: null,
1944
+ updatedAt: /* @__PURE__ */ new Date()
1945
+ } });
1946
+ }
1947
+ async setVerificationToken(id, token) {
1948
+ await this.collection.updateOne({ id }, { $set: {
1949
+ emailVerificationToken: token,
1950
+ emailVerificationSentAt: token ? /* @__PURE__ */ new Date() : null,
1951
+ updatedAt: /* @__PURE__ */ new Date()
1952
+ } });
1953
+ }
1954
+ async getUserByVerificationToken(token) {
1955
+ const doc = await this.collection.findOne({ emailVerificationToken: token });
1956
+ return doc ? toUser(doc) : null;
1957
+ }
1958
+ async getUserRoles(userId) {
1959
+ const roleIds = (await this.userRolesCollection.find({ userId }).toArray()).map((ur) => ur.roleId);
1960
+ if (roleIds.length === 0) return [];
1961
+ return (await this.rolesCollection.find({ id: { $in: roleIds } }).toArray()).map((r) => ({
1962
+ id: r.id,
1963
+ name: r.name,
1964
+ isAdmin: r.isAdmin ?? false,
1965
+ defaultPermissions: r.defaultPermissions ?? null,
1966
+ collectionPermissions: r.collectionPermissions ?? null
1967
+ }));
1968
+ }
1969
+ async getUserRoleIds(userId) {
1970
+ return (await this.userRolesCollection.find({ userId }).toArray()).map((ur) => ur.roleId);
1971
+ }
1972
+ async setUserRoles(userId, roleIds) {
1973
+ await this.userRolesCollection.deleteMany({ userId });
1974
+ if (roleIds.length > 0) {
1975
+ const docs = roleIds.map((roleId) => ({
1976
+ _id: new ObjectId().toString(),
1977
+ userId,
1978
+ roleId
1979
+ }));
1980
+ await this.userRolesCollection.insertMany(docs);
1981
+ }
1982
+ }
1983
+ async assignDefaultRole(userId, roleId) {
1984
+ await this.userRolesCollection.updateOne({
1985
+ userId,
1986
+ roleId
1987
+ }, { $setOnInsert: {
1988
+ _id: new ObjectId().toString(),
1989
+ userId,
1990
+ roleId
1991
+ } }, { upsert: true });
1992
+ }
1993
+ async getUserWithRoles(userId) {
1994
+ const user = await this.getUserById(userId);
1995
+ if (!user) return null;
1996
+ return {
1997
+ user,
1998
+ roles: await this.getUserRoles(userId)
1999
+ };
2000
+ }
2001
+ };
2002
+ var MongoRoleService = class {
2003
+ db;
2004
+ constructor(db) {
2005
+ this.db = db;
2006
+ }
2007
+ get collection() {
2008
+ return this.db.collection("rebase_roles");
2009
+ }
2010
+ async getRoleById(id) {
2011
+ const doc = await this.collection.findOne({ id });
2012
+ if (!doc) return null;
2013
+ return {
2014
+ id: doc.id,
2015
+ name: doc.name,
2016
+ isAdmin: doc.isAdmin ?? false,
2017
+ defaultPermissions: doc.defaultPermissions ?? null,
2018
+ collectionPermissions: doc.collectionPermissions ?? null
2019
+ };
2020
+ }
2021
+ async listRoles() {
2022
+ return (await this.collection.find().sort({ name: 1 }).toArray()).map((doc) => ({
2023
+ id: doc.id,
2024
+ name: doc.name,
2025
+ isAdmin: doc.isAdmin ?? false,
2026
+ defaultPermissions: doc.defaultPermissions ?? null,
2027
+ collectionPermissions: doc.collectionPermissions ?? null
2028
+ }));
2029
+ }
2030
+ async createRole(data) {
2031
+ const doc = {
2032
+ _id: data.id,
2033
+ id: data.id,
2034
+ name: data.name,
2035
+ isAdmin: data.isAdmin ?? false,
2036
+ defaultPermissions: data.defaultPermissions ?? null,
2037
+ collectionPermissions: data.collectionPermissions ?? null
2038
+ };
2039
+ await this.collection.insertOne(doc);
2040
+ return { ...doc };
2041
+ }
2042
+ async updateRole(id, data) {
2043
+ await this.collection.updateOne({ id }, { $set: data });
2044
+ return this.getRoleById(id);
2045
+ }
2046
+ async deleteRole(id) {
2047
+ await this.collection.deleteOne({ id });
2048
+ await this.db.collection("rebase_user_roles").deleteMany({ roleId: id });
2049
+ }
2050
+ };
2051
+ var MongoRefreshTokenService = class {
2052
+ db;
2053
+ constructor(db) {
2054
+ this.db = db;
2055
+ }
2056
+ get collection() {
2057
+ return this.db.collection("rebase_refresh_tokens");
2058
+ }
2059
+ async createToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
2060
+ const safeUserAgent = userAgent || "";
2061
+ const safeIpAddress = ipAddress || "";
2062
+ await this.collection.deleteMany({
2063
+ userId,
2064
+ userAgent: safeUserAgent,
2065
+ ipAddress: safeIpAddress
2066
+ });
2067
+ await this.collection.insertOne({
2068
+ _id: new ObjectId().toString(),
2069
+ id: new ObjectId().toString(),
2070
+ userId,
2071
+ tokenHash,
2072
+ expiresAt,
2073
+ createdAt: /* @__PURE__ */ new Date(),
2074
+ userAgent: safeUserAgent,
2075
+ ipAddress: safeIpAddress
2076
+ });
2077
+ }
2078
+ async findByHash(tokenHash) {
2079
+ const doc = await this.collection.findOne({ tokenHash });
2080
+ if (!doc) return null;
2081
+ return {
2082
+ id: doc.id,
2083
+ userId: doc.userId,
2084
+ tokenHash: doc.tokenHash,
2085
+ expiresAt: new Date(doc.expiresAt),
2086
+ createdAt: new Date(doc.createdAt),
2087
+ userAgent: doc.userAgent,
2088
+ ipAddress: doc.ipAddress
2089
+ };
2090
+ }
2091
+ async deleteByHash(tokenHash) {
2092
+ await this.collection.deleteOne({ tokenHash });
2093
+ }
2094
+ async deleteAllForUser(userId) {
2095
+ await this.collection.deleteMany({ userId });
2096
+ }
2097
+ async listForUser(userId) {
2098
+ return (await this.collection.find({ userId }).sort({ createdAt: 1 }).toArray()).map((doc) => ({
2099
+ id: doc.id,
2100
+ userId: doc.userId,
2101
+ tokenHash: doc.tokenHash,
2102
+ expiresAt: new Date(doc.expiresAt),
2103
+ createdAt: new Date(doc.createdAt),
2104
+ userAgent: doc.userAgent,
2105
+ ipAddress: doc.ipAddress
2106
+ }));
2107
+ }
2108
+ async deleteById(id, userId) {
2109
+ await this.collection.deleteOne({
2110
+ id,
2111
+ userId
2112
+ });
2113
+ }
2114
+ };
2115
+ var MongoPasswordResetTokenService = class {
2116
+ db;
2117
+ constructor(db) {
2118
+ this.db = db;
2119
+ }
2120
+ get collection() {
2121
+ return this.db.collection("rebase_password_reset_tokens");
2122
+ }
2123
+ async createToken(userId, tokenHash, expiresAt) {
2124
+ await this.collection.deleteMany({
2125
+ userId,
2126
+ usedAt: null
2127
+ });
2128
+ await this.collection.insertOne({
2129
+ _id: new ObjectId().toString(),
2130
+ userId,
2131
+ tokenHash,
2132
+ expiresAt,
2133
+ usedAt: null
2134
+ });
2135
+ }
2136
+ async findValidByHash(tokenHash) {
2137
+ const doc = await this.collection.findOne({
2138
+ tokenHash,
2139
+ usedAt: null,
2140
+ expiresAt: { $gt: /* @__PURE__ */ new Date() }
2141
+ });
2142
+ if (!doc) return null;
2143
+ return {
2144
+ userId: doc.userId,
2145
+ expiresAt: new Date(doc.expiresAt)
2146
+ };
2147
+ }
2148
+ async markAsUsed(tokenHash) {
2149
+ await this.collection.updateOne({ tokenHash }, { $set: { usedAt: /* @__PURE__ */ new Date() } });
2150
+ }
2151
+ async deleteAllForUser(userId) {
2152
+ await this.collection.deleteMany({ userId });
2153
+ }
2154
+ async deleteExpired() {
2155
+ await this.collection.deleteMany({ expiresAt: { $lt: /* @__PURE__ */ new Date() } });
2156
+ }
2157
+ };
2158
+ var MongoTokenRepository = class {
2159
+ db;
2160
+ refreshTokenService;
2161
+ passwordResetTokenService;
2162
+ constructor(db) {
2163
+ this.db = db;
2164
+ this.refreshTokenService = new MongoRefreshTokenService(db);
2165
+ this.passwordResetTokenService = new MongoPasswordResetTokenService(db);
2166
+ }
2167
+ async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
2168
+ await this.refreshTokenService.createToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
2169
+ }
2170
+ async findRefreshTokenByHash(tokenHash) {
2171
+ return this.refreshTokenService.findByHash(tokenHash);
2172
+ }
2173
+ async deleteRefreshToken(tokenHash) {
2174
+ await this.refreshTokenService.deleteByHash(tokenHash);
2175
+ }
2176
+ async deleteAllRefreshTokensForUser(userId) {
2177
+ await this.refreshTokenService.deleteAllForUser(userId);
2178
+ }
2179
+ async listRefreshTokensForUser(userId) {
2180
+ return this.refreshTokenService.listForUser(userId);
2181
+ }
2182
+ async deleteRefreshTokenById(id, userId) {
2183
+ await this.refreshTokenService.deleteById(id, userId);
2184
+ }
2185
+ async createPasswordResetToken(userId, tokenHash, expiresAt) {
2186
+ await this.passwordResetTokenService.createToken(userId, tokenHash, expiresAt);
2187
+ }
2188
+ async findValidPasswordResetToken(tokenHash) {
2189
+ return this.passwordResetTokenService.findValidByHash(tokenHash);
2190
+ }
2191
+ async markPasswordResetTokenUsed(tokenHash) {
2192
+ await this.passwordResetTokenService.markAsUsed(tokenHash);
2193
+ }
2194
+ async deleteAllPasswordResetTokensForUser(userId) {
2195
+ await this.passwordResetTokenService.deleteAllForUser(userId);
2196
+ }
2197
+ async deleteExpiredTokens() {
2198
+ await this.passwordResetTokenService.deleteExpired();
2199
+ }
2200
+ async createMagicLinkToken(userId, tokenHash, expiresAt) {
2201
+ const col = this.db.collection("magic_link_tokens");
2202
+ await col.deleteMany({
2203
+ userId,
2204
+ usedAt: null
2205
+ });
2206
+ await col.insertOne({
2207
+ userId,
2208
+ tokenHash,
2209
+ expiresAt,
2210
+ usedAt: null,
2211
+ createdAt: /* @__PURE__ */ new Date()
2212
+ });
2213
+ }
2214
+ async findValidMagicLinkToken(tokenHash) {
2215
+ const doc = await this.db.collection("magic_link_tokens").findOne({
2216
+ tokenHash,
2217
+ usedAt: null,
2218
+ expiresAt: { $gt: /* @__PURE__ */ new Date() }
2219
+ });
2220
+ if (!doc) return null;
2221
+ return {
2222
+ userId: doc.userId,
2223
+ expiresAt: doc.expiresAt
2224
+ };
2225
+ }
2226
+ async markMagicLinkTokenUsed(tokenHash) {
2227
+ await this.db.collection("magic_link_tokens").updateOne({ tokenHash }, { $set: { usedAt: /* @__PURE__ */ new Date() } });
2228
+ }
2229
+ };
2230
+ var MongoAuthRepository = class {
2231
+ db;
2232
+ userService;
2233
+ roleService;
2234
+ tokenRepository;
2235
+ constructor(db) {
2236
+ this.db = db;
2237
+ this.userService = new MongoUserService(db);
2238
+ this.roleService = new MongoRoleService(db);
2239
+ this.tokenRepository = new MongoTokenRepository(db);
2240
+ }
2241
+ async createUser(data) {
2242
+ return this.userService.createUser(data);
2243
+ }
2244
+ async getUserById(id) {
2245
+ return this.userService.getUserById(id);
2246
+ }
2247
+ async getUserByEmail(email) {
2248
+ return this.userService.getUserByEmail(email);
2249
+ }
2250
+ async getUserByIdentity(provider, providerId) {
2251
+ return this.userService.getUserByIdentity(provider, providerId);
2252
+ }
2253
+ async getUserIdentities(userId) {
2254
+ return this.userService.getUserIdentities(userId);
2255
+ }
2256
+ async linkUserIdentity(userId, provider, providerId, profileData) {
2257
+ return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
2258
+ }
2259
+ async updateUser(id, data) {
2260
+ return this.userService.updateUser(id, data);
2261
+ }
2262
+ async deleteUser(id) {
2263
+ await this.userService.deleteUser(id);
2264
+ }
2265
+ async listUsers() {
2266
+ return this.userService.listUsers();
2267
+ }
2268
+ async listUsersPaginated(options) {
2269
+ return this.userService.listUsersPaginated(options);
2270
+ }
2271
+ async updatePassword(id, passwordHash) {
2272
+ await this.userService.updatePassword(id, passwordHash);
2273
+ }
2274
+ async setEmailVerified(id, verified) {
2275
+ await this.userService.setEmailVerified(id, verified);
2276
+ }
2277
+ async setVerificationToken(id, token) {
2278
+ await this.userService.setVerificationToken(id, token);
2279
+ }
2280
+ async getUserByVerificationToken(token) {
2281
+ return this.userService.getUserByVerificationToken(token);
2282
+ }
2283
+ async getUserRoles(userId) {
2284
+ return this.userService.getUserRoles(userId);
2285
+ }
2286
+ async getUserRoleIds(userId) {
2287
+ return this.userService.getUserRoleIds(userId);
2288
+ }
2289
+ async setUserRoles(userId, roleIds) {
2290
+ await this.userService.setUserRoles(userId, roleIds);
2291
+ }
2292
+ async assignDefaultRole(userId, roleId) {
2293
+ await this.userService.assignDefaultRole(userId, roleId);
2294
+ }
2295
+ async getUserWithRoles(userId) {
2296
+ return this.userService.getUserWithRoles(userId);
2297
+ }
2298
+ async getRoleById(id) {
2299
+ return this.roleService.getRoleById(id);
2300
+ }
2301
+ async listRoles() {
2302
+ return this.roleService.listRoles();
2303
+ }
2304
+ async createRole(data) {
2305
+ return this.roleService.createRole(data);
2306
+ }
2307
+ async updateRole(id, data) {
2308
+ return this.roleService.updateRole(id, data);
2309
+ }
2310
+ async deleteRole(id) {
2311
+ await this.roleService.deleteRole(id);
2312
+ }
2313
+ async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
2314
+ await this.tokenRepository.createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
2315
+ }
2316
+ async findRefreshTokenByHash(tokenHash) {
2317
+ return this.tokenRepository.findRefreshTokenByHash(tokenHash);
2318
+ }
2319
+ async deleteRefreshToken(tokenHash) {
2320
+ await this.tokenRepository.deleteRefreshToken(tokenHash);
2321
+ }
2322
+ async deleteAllRefreshTokensForUser(userId) {
2323
+ await this.tokenRepository.deleteAllRefreshTokensForUser(userId);
2324
+ }
2325
+ async listRefreshTokensForUser(userId) {
2326
+ return this.tokenRepository.listRefreshTokensForUser(userId);
2327
+ }
2328
+ async deleteRefreshTokenById(id, userId) {
2329
+ await this.tokenRepository.deleteRefreshTokenById(id, userId);
2330
+ }
2331
+ async createPasswordResetToken(userId, tokenHash, expiresAt) {
2332
+ await this.tokenRepository.createPasswordResetToken(userId, tokenHash, expiresAt);
2333
+ }
2334
+ async findValidPasswordResetToken(tokenHash) {
2335
+ return this.tokenRepository.findValidPasswordResetToken(tokenHash);
2336
+ }
2337
+ async markPasswordResetTokenUsed(tokenHash) {
2338
+ await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
2339
+ }
2340
+ async deleteAllPasswordResetTokensForUser(userId) {
2341
+ await this.tokenRepository.deleteAllPasswordResetTokensForUser(userId);
2342
+ }
2343
+ async deleteExpiredTokens() {
2344
+ await this.tokenRepository.deleteExpiredTokens();
2345
+ }
2346
+ async createMagicLinkToken(userId, tokenHash, expiresAt) {
2347
+ await this.tokenRepository.createMagicLinkToken(userId, tokenHash, expiresAt);
2348
+ }
2349
+ async findValidMagicLinkToken(tokenHash) {
2350
+ return this.tokenRepository.findValidMagicLinkToken(tokenHash);
2351
+ }
2352
+ async markMagicLinkTokenUsed(tokenHash) {
2353
+ await this.tokenRepository.markMagicLinkTokenUsed(tokenHash);
2354
+ }
2355
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
2356
+ throw new Error("MFA is not implemented for MongoDB");
2357
+ }
2358
+ async getMfaFactors(userId) {
2359
+ return [];
2360
+ }
2361
+ async getMfaFactorById(factorId) {
2362
+ return null;
2363
+ }
2364
+ async verifyMfaFactor(factorId) {
2365
+ throw new Error("MFA is not implemented for MongoDB");
2366
+ }
2367
+ async deleteMfaFactor(factorId, userId) {
2368
+ throw new Error("MFA is not implemented for MongoDB");
2369
+ }
2370
+ async createMfaChallenge(factorId, ipAddress) {
2371
+ throw new Error("MFA is not implemented for MongoDB");
2372
+ }
2373
+ async getMfaChallengeById(challengeId) {
2374
+ return null;
2375
+ }
2376
+ async verifyMfaChallenge(challengeId) {
2377
+ throw new Error("MFA is not implemented for MongoDB");
2378
+ }
2379
+ async createRecoveryCodes(userId, codeHashes) {
2380
+ throw new Error("MFA is not implemented for MongoDB");
2381
+ }
2382
+ async useRecoveryCode(userId, codeHash) {
2383
+ return false;
2384
+ }
2385
+ async getUnusedRecoveryCodeCount(userId) {
2386
+ return 0;
2387
+ }
2388
+ async deleteAllRecoveryCodes(userId) {}
2389
+ async hasVerifiedMfaFactors(userId) {
2390
+ return false;
2391
+ }
2392
+ };
2393
+ //#endregion
2394
+ //#region src/MongoBootstrapper.ts
2395
+ function createMongoBootstrapper(mongoConfig) {
2396
+ let cachedAdmin;
2397
+ return {
2398
+ type: "mongodb",
2399
+ async initializeDriver(config) {
2400
+ const { collections } = config;
2401
+ const registry = new MongoCollectionRegistry();
2402
+ if (collections) collections.forEach((collection) => registry.register(collection));
2403
+ const db = mongoConfig.connection;
2404
+ const client = mongoConfig.client;
2405
+ try {
2406
+ await db.command({ ping: 1 });
2407
+ } catch (err) {
2408
+ logger.error("❌ Failed to connect to MongoDB", { error: err });
2409
+ }
2410
+ const realtimeService = new MongoRealtimeService(db);
2411
+ const driver = new MongoDriver(db, realtimeService, void 0, registry);
2412
+ return {
2413
+ driver,
2414
+ realtimeProvider: realtimeService,
2415
+ collectionRegistry: registry,
2416
+ internals: {
2417
+ db,
2418
+ client,
2419
+ registry,
2420
+ realtimeService,
2421
+ driver
2422
+ }
2423
+ };
2424
+ },
2425
+ async initializeAuth(config, driverResult) {
2426
+ const db = driverResult.internals.db;
2427
+ const { ensureAuthCollectionsExist } = await import("./ensure-collections-Bkx_O5CQ.js");
2428
+ await ensureAuthCollectionsExist(db);
2429
+ const { createEmailService } = await import("@rebasepro/server");
2430
+ const authConfig = config;
2431
+ let emailService;
2432
+ if (authConfig?.email) emailService = createEmailService(authConfig.email);
2433
+ return {
2434
+ userService: new MongoUserService(db),
2435
+ roleService: new MongoRoleService(db),
2436
+ authRepository: new MongoAuthRepository(db),
2437
+ emailService
2438
+ };
2439
+ },
2440
+ async initializeHistory(config, driverResult) {
2441
+ if (!config) return void 0;
2442
+ const db = driverResult.internals.db;
2443
+ const { ensureHistoryCollectionExists } = await import("./ensure-history-collection-yajOt2dv.js");
2444
+ await ensureHistoryCollectionExists(db);
2445
+ const { MongoHistoryService } = await Promise.resolve().then(() => MongoHistoryService_exports);
2446
+ const retention = typeof config === "object" ? config.retention : void 0;
2447
+ return { historyService: new MongoHistoryService(db, retention ? { ttlDays: retention } : void 0) };
2448
+ },
2449
+ async initializeRealtime(_config, driverResult) {
2450
+ return driverResult.internals.realtimeService;
2451
+ },
2452
+ getAdmin(driverResult) {
2453
+ const db = driverResult.internals.db;
2454
+ const admin = {
2455
+ async executeAggregate(pipeline) {
2456
+ const collName = pipeline[0]?.$from ?? "__admin__";
2457
+ return await db.collection(collName).aggregate(pipeline).toArray();
2458
+ },
2459
+ async fetchCollectionStats(collectionName) {
2460
+ const stats = await db.command({ collStats: collectionName });
2461
+ return {
2462
+ count: stats.count,
2463
+ sizeBytes: stats.size
2464
+ };
2465
+ },
2466
+ async fetchUnmappedTables(mappedPaths) {
2467
+ const names = (await db.listCollections().toArray()).map((c) => c.name).filter((n) => !n.startsWith("system."));
2468
+ if (!mappedPaths || mappedPaths.length === 0) return names;
2469
+ const mappedSet = new Set(mappedPaths.map((p) => p.toLowerCase()));
2470
+ return names.filter((n) => !mappedSet.has(n.toLowerCase()));
2471
+ },
2472
+ async fetchTableMetadata(collectionName) {
2473
+ const sample = await db.collection(collectionName).findOne();
2474
+ if (!sample) return {
2475
+ columns: [],
2476
+ foreignKeys: [],
2477
+ junctions: [],
2478
+ policies: []
2479
+ };
2480
+ return {
2481
+ columns: Object.entries(sample).map(([key, value]) => ({
2482
+ column_name: key,
2483
+ data_type: typeof value,
2484
+ udt_name: typeof value,
2485
+ is_nullable: "YES",
2486
+ column_default: null,
2487
+ character_maximum_length: null
2488
+ })),
2489
+ foreignKeys: [],
2490
+ junctions: [],
2491
+ policies: []
2492
+ };
2493
+ }
2494
+ };
2495
+ cachedAdmin = admin;
2496
+ return admin;
2497
+ },
2498
+ mountRoutes() {},
2499
+ async initializeWebsockets(server, realtimeService, driver, config) {
2500
+ const { createMongoWebSocket } = await import("./websocket-DQlwCHFq.js");
2501
+ createMongoWebSocket(server, realtimeService, driver, config, cachedAdmin);
2502
+ }
2503
+ };
2504
+ }
2505
+ //#endregion
2506
+ export { AuthenticatedMongoDriver, MongoCollectionRegistry, MongoConditionBuilder, MongoDBConnection, MongoDataService, MongoDriver, MongoRealtimeService, createMongoBackend, createMongoBootstrapper, createMongoDBConnection, createMongoDelegate, createMongoEntityRepository, createMongoRealtimeService, isMongoBackendConfig, isMongoDriverConfig };
2507
+
2508
+ //# sourceMappingURL=index.es.js.map