@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,1120 @@
1
+ /**
2
+ * MongoDB DataDriver Delegate
3
+ *
4
+ * Implements the DataDriver interface for Rebase frontend integration.
5
+ * This is the main entry point for Rebase to interact with MongoDB.
6
+ */
7
+
8
+ import { Db } from "mongodb";
9
+ import {
10
+ DataDriver,
11
+ DeleteProps,
12
+ Entity,
13
+ CollectionConfig,
14
+ FetchCollectionProps,
15
+ FetchOneProps,
16
+ ListenCollectionProps,
17
+ ListenOneProps,
18
+ SaveProps,
19
+ RebaseCallContext,
20
+ CollectionRegistryInterface,
21
+ User,
22
+ RebaseClient,
23
+ RebaseData,
24
+ RebaseSdkData,
25
+ SecurityRule
26
+ } from "@rebasepro/types";
27
+ import { MongoDataService } from "../db/MongoDataService";
28
+ import { MongoRealtimeService } from "./MongoRealtimeService";
29
+ import { MongoHistoryService } from "./MongoHistoryService";
30
+ import { buildPropertyCallbacks, updateDateAutoValues, buildSdkData, checkOperation } from "@rebasepro/common";
31
+ import { mergeDeep } from "@rebasepro/utils";
32
+ import { Filter, Document } from "mongodb";
33
+ import { ApiError } from "@rebasepro/server";
34
+ import { MongoConditionBuilder } from "../db/MongoConditionBuilder";
35
+ import { logger } from "@rebasepro/server";
36
+
37
+ /**
38
+ * MongoDB DataDriver Delegate
39
+ *
40
+ * Implements the DataDriver interface for Rebase.
41
+ * Provides all data operations needed by the Rebase frontend.
42
+ */
43
+ export class MongoDriver implements DataDriver {
44
+ key = "mongodb";
45
+ initialised = true;
46
+
47
+ private dataService: MongoDataService;
48
+ private realtimeService: MongoRealtimeService;
49
+ public historyService: MongoHistoryService;
50
+ public user?: User;
51
+ public data: RebaseSdkData;
52
+ public client?: RebaseClient;
53
+
54
+ constructor(
55
+ private db: Db,
56
+ realtimeService?: MongoRealtimeService,
57
+ historyService?: MongoHistoryService,
58
+ public readonly registry?: CollectionRegistryInterface,
59
+ user?: User
60
+ ) {
61
+ this.dataService = new MongoDataService(db);
62
+ this.realtimeService = realtimeService ?? new MongoRealtimeService(db);
63
+ this.historyService = historyService ?? new MongoHistoryService(db);
64
+ this.user = user;
65
+ this.data = buildSdkData(this);
66
+ this.realtimeService.setDataDriver(this);
67
+ }
68
+
69
+ /**
70
+ * Get the current timestamp
71
+ */
72
+ currentTime(): Date {
73
+ return new Date();
74
+ }
75
+
76
+ /**
77
+ * Resolve a collection's callbacks and property callbacks from the registry.
78
+ * Used by AuthenticatedMongoDriver to apply callbacks after RLS filtering.
79
+ */
80
+ resolveCollectionCallbacks<M extends Record<string, unknown>>(
81
+ collection: CollectionConfig<M> | undefined,
82
+ path: string
83
+ ) {
84
+ if (!collection && !path) return { collection: undefined,
85
+ callbacks: undefined,
86
+ globalCallbacks: undefined,
87
+ propertyCallbacks: undefined };
88
+ const registryCollection = this.registry?.getCollectionByPath(path);
89
+ const resolvedCollection = registryCollection
90
+ ? ({ ...collection,
91
+ ...registryCollection } as CollectionConfig<M>)
92
+ : (collection as CollectionConfig<M>);
93
+
94
+ const callbacks = resolvedCollection?.callbacks;
95
+ const globalCallbacks = this.registry?.getGlobalCallbacks();
96
+ const properties = resolvedCollection?.properties;
97
+ let propertyCallbacks;
98
+ if (properties) {
99
+ propertyCallbacks = buildPropertyCallbacks(properties);
100
+ }
101
+ return {
102
+ collection: resolvedCollection,
103
+ callbacks,
104
+ globalCallbacks,
105
+ propertyCallbacks
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Fetch a collection of rows
111
+ */
112
+ async fetchCollection<M extends Record<string, any>>({
113
+ path,
114
+ collection,
115
+ filter,
116
+ limit,
117
+ startAfter,
118
+ orderBy,
119
+ searchString,
120
+ order
121
+ }: FetchCollectionProps<M>): Promise<Record<string, unknown>[]> {
122
+ const rows = await this.dataService.fetchCollection<M>(path, {
123
+ filter,
124
+ limit,
125
+ startAfter,
126
+ orderBy,
127
+ order,
128
+ searchString,
129
+ collection: collection as CollectionConfig
130
+ });
131
+
132
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
133
+
134
+ if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
135
+ const contextForCallback = {
136
+ user: this.user,
137
+ driver: this,
138
+ data: this.data,
139
+ client: this.client,
140
+ storageSource: this.client?.storage
141
+ } as unknown as RebaseCallContext; // Backend context
142
+ return Promise.all(rows.map(async (row) => {
143
+ let fetched = row;
144
+ if (globalCallbacks?.afterRead) {
145
+ fetched = await globalCallbacks.afterRead({
146
+ collection: resolvedCollection as CollectionConfig<M>,
147
+ path,
148
+ row: fetched,
149
+ context: contextForCallback
150
+ }) ?? fetched;
151
+ }
152
+ if (callbacks?.afterRead) {
153
+ fetched = await callbacks.afterRead({
154
+ collection: resolvedCollection as CollectionConfig<M>,
155
+ path,
156
+ row: fetched,
157
+ context: contextForCallback
158
+ }) ?? fetched;
159
+ }
160
+ if (propertyCallbacks?.afterRead) {
161
+ fetched = await propertyCallbacks.afterRead({
162
+ collection: resolvedCollection as CollectionConfig<M>,
163
+ path,
164
+ row: fetched,
165
+ context: contextForCallback
166
+ }) ?? fetched;
167
+ }
168
+ return fetched;
169
+ }));
170
+ }
171
+
172
+ return rows;
173
+ }
174
+
175
+ /**
176
+ * Listen to collection changes
177
+ */
178
+ listenCollection<M extends Record<string, any>>({
179
+ path,
180
+ collection,
181
+ filter,
182
+ limit,
183
+ startAfter,
184
+ orderBy,
185
+ searchString,
186
+ order,
187
+ onUpdate,
188
+ onError
189
+ }: ListenCollectionProps<M>): () => void {
190
+ const subscriptionId = this.generateSubscriptionId();
191
+
192
+ const callback = (rows: Record<string, unknown>[]) => {
193
+ try {
194
+ onUpdate(rows);
195
+ } catch (error) {
196
+ logger.error("Error in collection update callback", { error: error });
197
+ if (onError) {
198
+ onError(error instanceof Error ? error : new Error(String(error)));
199
+ }
200
+ }
201
+ };
202
+
203
+ this.realtimeService.subscribeToCollection(
204
+ subscriptionId,
205
+ {
206
+ clientId: "driver",
207
+ path,
208
+ filter,
209
+ orderBy,
210
+ order,
211
+ limit,
212
+ startAfter,
213
+ searchString
214
+ },
215
+ callback
216
+ );
217
+
218
+ // Return unsubscribe function
219
+ return () => {
220
+ this.realtimeService.unsubscribe(subscriptionId);
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Fetch a single row
226
+ */
227
+ async fetchOne<M extends Record<string, any>>({
228
+ path,
229
+ id,
230
+ databaseId,
231
+ collection
232
+ }: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
233
+ let row = await this.dataService.fetchOne<M>(path, id, databaseId);
234
+
235
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
236
+
237
+ if (row && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
238
+ const contextForCallback = {
239
+ user: this.user,
240
+ driver: this,
241
+ data: this.data,
242
+ client: this.client,
243
+ storageSource: this.client?.storage
244
+ } as unknown as RebaseCallContext; // Backend context
245
+ let processedRow: Record<string, unknown> = row;
246
+ if (globalCallbacks?.afterRead) {
247
+ processedRow = await globalCallbacks.afterRead({
248
+ collection: resolvedCollection as CollectionConfig<M>,
249
+ path,
250
+ row: processedRow,
251
+ context: contextForCallback
252
+ }) ?? processedRow;
253
+ }
254
+ if (callbacks?.afterRead) {
255
+ processedRow = await callbacks.afterRead({
256
+ collection: resolvedCollection as CollectionConfig<M>,
257
+ path,
258
+ row: processedRow,
259
+ context: contextForCallback
260
+ }) ?? processedRow;
261
+ }
262
+ if (propertyCallbacks?.afterRead) {
263
+ processedRow = await propertyCallbacks.afterRead({
264
+ collection: resolvedCollection as CollectionConfig<M>,
265
+ path,
266
+ row: processedRow,
267
+ context: contextForCallback
268
+ }) ?? processedRow;
269
+ }
270
+ row = processedRow;
271
+ }
272
+
273
+ return row;
274
+ }
275
+
276
+ /**
277
+ * Listen to row changes
278
+ */
279
+ listenOne<M extends Record<string, any>>({
280
+ path,
281
+ id,
282
+ collection,
283
+ onUpdate,
284
+ onError
285
+ }: ListenOneProps<M>): () => void {
286
+ const subscriptionId = this.generateSubscriptionId();
287
+
288
+ const callback = (row: Record<string, unknown> | null) => {
289
+ try {
290
+ onUpdate(row);
291
+ } catch (error) {
292
+ logger.error("Error in row update callback", { error: error });
293
+ if (onError) {
294
+ onError(error instanceof Error ? error : new Error(String(error)));
295
+ }
296
+ }
297
+ };
298
+
299
+ this.realtimeService.subscribeToOne(
300
+ subscriptionId,
301
+ {
302
+ clientId: "driver",
303
+ path,
304
+ id
305
+ },
306
+ callback
307
+ );
308
+
309
+ // Return unsubscribe function
310
+ return () => {
311
+ this.realtimeService.unsubscribe(subscriptionId);
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Save an row (create or update)
317
+ */
318
+ async save<M extends Record<string, any>>({
319
+ path,
320
+ id,
321
+ values,
322
+ collection,
323
+ status
324
+ }: SaveProps<M>): Promise<Record<string, unknown>> {
325
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
326
+
327
+ let updatedValues = values;
328
+ const contextForCallback = {
329
+ user: this.user,
330
+ driver: this,
331
+ data: this.data,
332
+ client: this.client,
333
+ storageSource: this.client?.storage
334
+ } as unknown as RebaseCallContext;
335
+
336
+ // Fetch previous values for callbacks AND history recording
337
+ let previousValuesForHistory: Partial<M> | undefined;
338
+ if (status === "existing" && id) {
339
+ const existing = await this.dataService.fetchOne<M>(path, id, resolvedCollection?.databaseId);
340
+ if (existing) {
341
+ const { id: _existingId, ...existingValues } = existing;
342
+ previousValuesForHistory = existingValues as Partial<M>;
343
+ }
344
+ }
345
+
346
+ if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
347
+ if (globalCallbacks?.beforeSave) {
348
+ const result = await globalCallbacks.beforeSave({
349
+ collection: resolvedCollection as CollectionConfig<M>,
350
+ path,
351
+ id,
352
+ values: updatedValues,
353
+ previousValues: previousValuesForHistory,
354
+ status,
355
+ context: contextForCallback
356
+ });
357
+ if (result) updatedValues = mergeDeep(updatedValues, result);
358
+ }
359
+
360
+ if (callbacks?.beforeSave) {
361
+ const result = await callbacks.beforeSave({
362
+ collection: resolvedCollection as CollectionConfig<M>,
363
+ path,
364
+ id,
365
+ values: updatedValues,
366
+ previousValues: previousValuesForHistory,
367
+ status,
368
+ context: contextForCallback
369
+ });
370
+ if (result) updatedValues = mergeDeep(updatedValues, result);
371
+ }
372
+
373
+ if (propertyCallbacks?.beforeSave) {
374
+ const result = await propertyCallbacks.beforeSave({
375
+ collection: resolvedCollection as CollectionConfig<M>,
376
+ path,
377
+ id,
378
+ values: updatedValues,
379
+ previousValues: previousValuesForHistory,
380
+ status,
381
+ context: contextForCallback
382
+ });
383
+ if (result) updatedValues = mergeDeep(updatedValues, result);
384
+ }
385
+ }
386
+
387
+ // Apply autoValue timestamps (on_create / on_update) at the application layer.
388
+ if (resolvedCollection?.properties) {
389
+ updatedValues = updateDateAutoValues({
390
+ inputValues: updatedValues,
391
+ properties: resolvedCollection.properties,
392
+ status: status ?? "new",
393
+ timestampNowValue: new Date()
394
+ });
395
+ }
396
+
397
+ try {
398
+ let savedRow = await this.dataService.save<M>(
399
+ path,
400
+ updatedValues,
401
+ id,
402
+ resolvedCollection?.databaseId
403
+ );
404
+
405
+ if (savedRow && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
406
+ if (globalCallbacks?.afterRead) {
407
+ savedRow = await globalCallbacks.afterRead({
408
+ collection: resolvedCollection as CollectionConfig<M>,
409
+ path,
410
+ row: savedRow,
411
+ context: contextForCallback
412
+ }) ?? savedRow;
413
+ }
414
+ if (callbacks?.afterRead) {
415
+ savedRow = await callbacks.afterRead({
416
+ collection: resolvedCollection as CollectionConfig<M>,
417
+ path,
418
+ row: savedRow,
419
+ context: contextForCallback
420
+ }) ?? savedRow;
421
+ }
422
+ if (propertyCallbacks?.afterRead) {
423
+ savedRow = await propertyCallbacks.afterRead({
424
+ collection: resolvedCollection as CollectionConfig<M>,
425
+ path,
426
+ row: savedRow,
427
+ context: contextForCallback
428
+ }) ?? savedRow;
429
+ }
430
+ }
431
+
432
+ const savedId = savedRow.id as string | number;
433
+ const { id: _savedId, ...savedValues } = savedRow;
434
+
435
+ if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
436
+ if (globalCallbacks?.afterSave) {
437
+ await globalCallbacks.afterSave({
438
+ collection: resolvedCollection as CollectionConfig<M>,
439
+ path,
440
+ id: savedId,
441
+ values: savedValues,
442
+ previousValues: previousValuesForHistory,
443
+ status,
444
+ context: contextForCallback
445
+ });
446
+ }
447
+ if (callbacks?.afterSave) {
448
+ await callbacks.afterSave({
449
+ collection: resolvedCollection as CollectionConfig<M>,
450
+ path,
451
+ id: savedId,
452
+ values: savedValues as Partial<M>,
453
+ previousValues: previousValuesForHistory,
454
+ status,
455
+ context: contextForCallback
456
+ });
457
+ }
458
+ if (propertyCallbacks?.afterSave) {
459
+ await propertyCallbacks.afterSave({
460
+ collection: resolvedCollection as CollectionConfig<M>,
461
+ path,
462
+ id: savedId,
463
+ values: savedValues,
464
+ previousValues: previousValuesForHistory,
465
+ status,
466
+ context: contextForCallback
467
+ });
468
+ }
469
+ }
470
+
471
+ // Record row history (fire-and-forget, never blocks the save)
472
+ if (this.historyService && resolvedCollection?.history) {
473
+ this.historyService.recordHistory({
474
+ tableName: path,
475
+ id: savedId.toString(),
476
+ action: status === "new" ? "create" : "update",
477
+ values: savedValues as Record<string, unknown>,
478
+ previousValues: previousValuesForHistory as Record<string, unknown> | undefined,
479
+ updatedBy: this.user?.uid
480
+ }).catch(err => {
481
+ logger.error(`Failed to record history for ${path}/${savedId}`, { error: err });
482
+ });
483
+ }
484
+
485
+ // Notify real-time subscribers
486
+ await this.realtimeService.notifyUpdate(
487
+ path,
488
+ savedId.toString(),
489
+ savedRow
490
+ );
491
+
492
+ return savedRow;
493
+ } catch (error) {
494
+ if (callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
495
+ if (callbacks?.afterSaveError) {
496
+ await callbacks.afterSaveError({
497
+ collection: resolvedCollection as CollectionConfig<M>,
498
+ path,
499
+ id: id || "unknown",
500
+ values: updatedValues,
501
+ previousValues: undefined,
502
+ status,
503
+ context: contextForCallback
504
+ });
505
+ }
506
+ if (propertyCallbacks?.afterSaveError) {
507
+ await propertyCallbacks.afterSaveError({
508
+ collection: resolvedCollection as CollectionConfig<M>,
509
+ path,
510
+ id: id || "unknown",
511
+ values: updatedValues,
512
+ previousValues: undefined,
513
+ status,
514
+ context: contextForCallback
515
+ });
516
+ }
517
+ }
518
+ throw error;
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Delete an row
524
+ */
525
+ async delete<M extends Record<string, any>>({
526
+ row,
527
+ collection
528
+ }: DeleteProps<M>): Promise<void> {
529
+ const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
530
+
531
+ const callbackRow: Record<string, unknown> = { id: row.id, ...(row.values ?? {}) };
532
+
533
+ const contextForCallback = {
534
+ user: this.user,
535
+ driver: this,
536
+ data: this.data,
537
+ client: this.client,
538
+ storageSource: this.client?.storage
539
+ } as unknown as RebaseCallContext;
540
+
541
+ if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
542
+ let preventDefault = false;
543
+ if (globalCallbacks?.beforeDelete) {
544
+ const result = await globalCallbacks.beforeDelete({
545
+ collection: resolvedCollection as CollectionConfig<M>,
546
+ path: row.path,
547
+ id: row.id,
548
+ row: callbackRow,
549
+ context: contextForCallback
550
+ });
551
+ if (result === false) {
552
+ preventDefault = true;
553
+ }
554
+ }
555
+ if (callbacks?.beforeDelete) {
556
+ const result = await callbacks.beforeDelete({
557
+ collection: resolvedCollection as CollectionConfig<M>,
558
+ path: row.path,
559
+ id: row.id,
560
+ row: callbackRow,
561
+ context: contextForCallback
562
+ });
563
+ if (result === false) {
564
+ preventDefault = true;
565
+ }
566
+ }
567
+ if (propertyCallbacks?.beforeDelete) {
568
+ const result = await propertyCallbacks.beforeDelete({
569
+ collection: resolvedCollection as CollectionConfig<M>,
570
+ path: row.path,
571
+ id: row.id,
572
+ row: callbackRow,
573
+ context: contextForCallback
574
+ });
575
+ if (result === false) {
576
+ preventDefault = true;
577
+ }
578
+ }
579
+ if (preventDefault) {
580
+ return;
581
+ }
582
+ }
583
+
584
+ await this.dataService.delete(row.path, row.id);
585
+
586
+ if (globalCallbacks?.afterDelete || callbacks?.afterDelete || propertyCallbacks?.afterDelete) {
587
+ if (globalCallbacks?.afterDelete) {
588
+ await globalCallbacks.afterDelete({
589
+ collection: resolvedCollection as CollectionConfig<M>,
590
+ path: row.path,
591
+ id: row.id,
592
+ row: callbackRow,
593
+ context: contextForCallback
594
+ });
595
+ }
596
+ if (callbacks?.afterDelete) {
597
+ await callbacks.afterDelete({
598
+ collection: resolvedCollection as CollectionConfig<M>,
599
+ path: row.path,
600
+ id: row.id,
601
+ row: callbackRow,
602
+ context: contextForCallback
603
+ });
604
+ }
605
+ if (propertyCallbacks?.afterDelete) {
606
+ await propertyCallbacks.afterDelete({
607
+ collection: resolvedCollection as CollectionConfig<M>,
608
+ path: row.path,
609
+ id: row.id,
610
+ row: callbackRow,
611
+ context: contextForCallback
612
+ });
613
+ }
614
+ }
615
+
616
+ // Record history
617
+ if (this.historyService && resolvedCollection?.history) {
618
+ this.historyService.recordHistory({
619
+ action: "delete",
620
+ id: String(row.id),
621
+ tableName: row.path,
622
+ previousValues: row.values,
623
+ updatedBy: this.user?.uid
624
+ }).catch(err => {
625
+ logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
626
+ });
627
+ }
628
+
629
+ // Notify subscribers of the deletion
630
+ await this.realtimeService.notifyUpdate(row.path, String(row.id), null);
631
+ }
632
+
633
+ /**
634
+ * Check if a field value is unique
635
+ */
636
+ async checkUniqueField(
637
+ path: string,
638
+ name: string,
639
+ value: any,
640
+ id?: string,
641
+ collection?: CollectionConfig
642
+ ): Promise<boolean> {
643
+ return this.dataService.checkUniqueField(path, name, value, id);
644
+ }
645
+
646
+ /**
647
+ * Generate a new row ID
648
+ */
649
+ generateId(path: string, collection?: CollectionConfig): string {
650
+ return this.dataService.generateId();
651
+ }
652
+
653
+ /**
654
+ * Count rows in a collection
655
+ */
656
+ async count<M extends Record<string, any>>({
657
+ path,
658
+ collection,
659
+ filter
660
+ }: FetchCollectionProps<M>): Promise<number> {
661
+ return this.dataService.count<M>(path, { filter });
662
+ }
663
+
664
+ /**
665
+ * Generate a unique subscription ID
666
+ */
667
+ private generateSubscriptionId(): string {
668
+ return `mongo_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
669
+ }
670
+
671
+ /**
672
+ * Check if the delegate is ready
673
+ */
674
+ isReady(): boolean {
675
+ return this.initialised;
676
+ }
677
+
678
+ /**
679
+ * Get the underlying row service for direct access
680
+ */
681
+ getDataService(): MongoDataService {
682
+ return this.dataService;
683
+ }
684
+
685
+ /**
686
+ * Get the underlying realtime service for direct access
687
+ */
688
+ getRealtimeService(): MongoRealtimeService {
689
+ return this.realtimeService;
690
+ }
691
+
692
+ /**
693
+ * Scope the MongoDriver with an authenticated user context
694
+ */
695
+ async withAuth(user: User): Promise<DataDriver> {
696
+ return new AuthenticatedMongoDriver(this, user);
697
+ }
698
+ }
699
+
700
+ export class AuthenticatedMongoDriver implements DataDriver {
701
+ key = "mongodb";
702
+ initialised = true;
703
+ public user: User;
704
+ public data: RebaseSdkData;
705
+
706
+ constructor(public delegate: MongoDriver, user: User) {
707
+ this.user = user;
708
+ this.data = buildSdkData(this);
709
+ }
710
+
711
+ currentTime(): Date {
712
+ return this.delegate.currentTime();
713
+ }
714
+
715
+ async fetchCollection<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]> {
716
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
717
+ const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
718
+ if (rlsFilter === null) {
719
+ return [];
720
+ }
721
+
722
+ const userQuery = MongoConditionBuilder.buildQuery({
723
+ filter: props.filter,
724
+ searchString: props.searchString,
725
+ properties: resolvedCollection?.properties
726
+ });
727
+
728
+ const combinedQuery = Object.keys(rlsFilter).length > 0
729
+ ? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
730
+ : userQuery;
731
+
732
+ const originalService = this.delegate.getDataService();
733
+ const rows = await originalService.fetchCollection<M>(props.path, {
734
+ ...props,
735
+ rawQuery: combinedQuery,
736
+ collection: resolvedCollection
737
+ });
738
+
739
+ const { callbacks, globalCallbacks, propertyCallbacks } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
740
+
741
+ if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
742
+ const contextForCallback = {
743
+ user: this.user,
744
+ driver: this,
745
+ data: this.data,
746
+ client: this.delegate.client,
747
+ storageSource: this.delegate.client?.storage
748
+ } as unknown as RebaseCallContext;
749
+ return Promise.all(rows.map(async (row) => {
750
+ let fetched = row;
751
+ if (globalCallbacks?.afterRead) {
752
+ fetched = await globalCallbacks.afterRead({
753
+ collection: resolvedCollection as CollectionConfig<M>,
754
+ path: props.path,
755
+ row: fetched,
756
+ context: contextForCallback
757
+ }) ?? fetched;
758
+ }
759
+ if (callbacks?.afterRead) {
760
+ fetched = await callbacks.afterRead({
761
+ collection: resolvedCollection as CollectionConfig<M>,
762
+ path: props.path,
763
+ row: fetched,
764
+ context: contextForCallback
765
+ }) ?? fetched;
766
+ }
767
+ if (propertyCallbacks?.afterRead) {
768
+ fetched = await propertyCallbacks.afterRead({
769
+ collection: resolvedCollection as CollectionConfig<M>,
770
+ path: props.path,
771
+ row: fetched,
772
+ context: contextForCallback
773
+ }) ?? fetched;
774
+ }
775
+ return fetched;
776
+ }));
777
+ }
778
+
779
+ return rows;
780
+ }
781
+
782
+ listenCollection<M extends Record<string, any>>(props: ListenCollectionProps<M>): () => void {
783
+ const unsubscribe = this.delegate.listenCollection(props);
784
+ const authContext = { userId: this.user.uid,
785
+ roles: this.user.roles ?? [] };
786
+ const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
787
+ const lastEntry = Array.from(subscriptions.entries()).pop();
788
+ const lastSub = lastEntry?.[1];
789
+ if (lastSub && lastSub.config.clientId === "driver") {
790
+ lastSub.authContext = authContext;
791
+ }
792
+ return unsubscribe;
793
+ }
794
+
795
+ async fetchOne<M extends Record<string, any>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
796
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
797
+ const row = await this.delegate.fetchOne(props);
798
+ if (row) {
799
+ const authorized = checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(row, props.path), "select", { onUnknown: "deny" });
800
+ if (!authorized) {
801
+ return undefined;
802
+ }
803
+ }
804
+ return row;
805
+ }
806
+
807
+ listenOne<M extends Record<string, any>>(props: ListenOneProps<M>): () => void {
808
+ const unsubscribe = this.delegate.listenOne(props);
809
+ const authContext = { userId: this.user.uid,
810
+ roles: this.user.roles ?? [] };
811
+ const subscriptions = this.delegate.getRealtimeService().getSubscriptions();
812
+ const lastEntry = Array.from(subscriptions.entries()).pop();
813
+ const lastSub = lastEntry?.[1];
814
+ if (lastSub && lastSub.config.clientId === "driver") {
815
+ lastSub.authContext = authContext;
816
+ }
817
+ return unsubscribe;
818
+ }
819
+
820
+ async save<M extends Record<string, any>>(props: SaveProps<M>): Promise<Record<string, unknown>> {
821
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
822
+
823
+ if (props.status === "existing" && props.id) {
824
+ const existing = await this.delegate.fetchOne({ path: props.path,
825
+ id: props.id,
826
+ collection: resolvedCollection });
827
+ if (!existing || !checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(existing, props.path), "update", { onUnknown: "deny" })) {
828
+ throw ApiError.forbidden("Forbidden");
829
+ }
830
+ } else {
831
+ const tempEntity = { id: props.id || "new",
832
+ path: props.path,
833
+ values: props.values } as Entity;
834
+ if (!checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, tempEntity, "insert", { onUnknown: "deny" })) {
835
+ throw ApiError.forbidden("Forbidden");
836
+ }
837
+ }
838
+
839
+ const saved = await this.delegate.save({
840
+ ...props,
841
+ collection: resolvedCollection
842
+ });
843
+
844
+ // After save / withCheck rules verification
845
+ if (!checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(saved, props.path), props.status === "existing" ? "update" : "insert", { onUnknown: "deny" })) {
846
+ throw ApiError.forbidden("Forbidden");
847
+ }
848
+
849
+ return saved;
850
+ }
851
+
852
+ async delete<M extends Record<string, any>>(props: DeleteProps<M>): Promise<void> {
853
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.row.path);
854
+
855
+ const existing = await this.delegate.fetchOne({ path: props.row.path,
856
+ id: props.row.id,
857
+ collection: resolvedCollection });
858
+ if (!existing || !checkOperation(resolvedCollection as CollectionConfig, { user: this.user }, rowToEntityForCheck(existing, props.row.path), "delete", { onUnknown: "deny" })) {
859
+ throw ApiError.forbidden("Forbidden");
860
+ }
861
+
862
+ return this.delegate.delete(props);
863
+ }
864
+
865
+ async checkUniqueField(
866
+ path: string,
867
+ name: string,
868
+ value: any,
869
+ id?: string,
870
+ collection?: CollectionConfig
871
+ ): Promise<boolean> {
872
+ return this.delegate.checkUniqueField(path, name, value, id, collection);
873
+ }
874
+
875
+ generateId(path: string, collection?: CollectionConfig): string {
876
+ return this.delegate.generateId(path, collection);
877
+ }
878
+
879
+ async count<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<number> {
880
+ const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
881
+ const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
882
+ if (rlsFilter === null) {
883
+ return 0;
884
+ }
885
+
886
+ const userQuery = MongoConditionBuilder.buildQuery({
887
+ filter: props.filter,
888
+ searchString: props.searchString,
889
+ properties: resolvedCollection?.properties
890
+ });
891
+
892
+ const combinedQuery = Object.keys(rlsFilter).length > 0
893
+ ? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
894
+ : userQuery;
895
+
896
+ const originalService = this.delegate.getDataService();
897
+ return originalService.count(props.path, {
898
+ ...props,
899
+ rawQuery: combinedQuery
900
+ });
901
+ }
902
+
903
+ isReady(): boolean {
904
+ return this.delegate.isReady();
905
+ }
906
+ }
907
+
908
+ /**
909
+ * Wrap a flat row into the Entity shape expected by `checkOperation`,
910
+ * which evaluates security rules against `row.values`.
911
+ */
912
+ function rowToEntityForCheck(row: Record<string, unknown>, path: string): Entity {
913
+ return {
914
+ id: row.id as string | number,
915
+ path,
916
+ values: row
917
+ };
918
+ }
919
+
920
+ function getMongoFilterForSQL(sqlString: string, user: User): Filter<Document> | null {
921
+ let cleanedSQL = sqlString.trim();
922
+ while (cleanedSQL.startsWith("(") && cleanedSQL.endsWith(")")) {
923
+ let openCount = 0;
924
+ let isEnclosing = true;
925
+ for (let i = 0; i < cleanedSQL.length - 1; i++) {
926
+ if (cleanedSQL[i] === "(") openCount++;
927
+ else if (cleanedSQL[i] === ")") openCount--;
928
+ if (openCount === 0) {
929
+ isEnclosing = false;
930
+ break;
931
+ }
932
+ }
933
+ if (isEnclosing) {
934
+ cleanedSQL = cleanedSQL.substring(1, cleanedSQL.length - 1).trim();
935
+ } else {
936
+ break;
937
+ }
938
+ }
939
+
940
+ const splitByTopLevel = (str: string, delimiter: string) => {
941
+ const parts: string[] = [];
942
+ let current = "";
943
+ let openCount = 0;
944
+ let i = 0;
945
+ while (i < str.length) {
946
+ if (str[i] === "(") openCount++;
947
+ else if (str[i] === ")") openCount--;
948
+
949
+ if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
950
+ parts.push(current);
951
+ current = "";
952
+ i += delimiter.length;
953
+ } else {
954
+ current += str[i];
955
+ i++;
956
+ }
957
+ }
958
+ parts.push(current);
959
+ return parts;
960
+ };
961
+
962
+ const orParts = splitByTopLevel(cleanedSQL, " OR ");
963
+ if (orParts.length > 1) {
964
+ const subFilters = orParts.map(part => getMongoFilterForSQL(part, user)).filter(f => f !== null) as Filter<Document>[];
965
+ if (subFilters.length === 0) return null;
966
+ if (subFilters.length === 1) return subFilters[0];
967
+ return { $or: subFilters } as Filter<Document>;
968
+ }
969
+
970
+ const andParts = splitByTopLevel(cleanedSQL, " AND ");
971
+ if (andParts.length > 1) {
972
+ const subFilters = andParts.map(part => getMongoFilterForSQL(part, user)).filter(f => f !== null) as Filter<Document>[];
973
+ if (subFilters.length === 0) return null;
974
+ if (subFilters.length === 1) return subFilters[0];
975
+ return { $and: subFilters } as Filter<Document>;
976
+ }
977
+
978
+ const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
979
+ if (roleIntersectMatch && roleIntersectMatch[1]) {
980
+ const requiredRoles = roleIntersectMatch[1].split(",").map(r => r.trim().replace(/'/g, ""));
981
+ const userRoles = user.roles || [];
982
+ const matches = requiredRoles.some(r => userRoles.includes(r));
983
+ return matches ? {} : { _id: { $exists: false } };
984
+ }
985
+
986
+ const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
987
+ if (roleContainMatch && roleContainMatch[1]) {
988
+ const requiredRoles = roleContainMatch[1].split(",").map(r => r.trim().replace(/'/g, ""));
989
+ const userRoles = user.roles || [];
990
+ const matches = requiredRoles.every(r => userRoles.includes(r));
991
+ return matches ? {} : { _id: { $exists: false } };
992
+ }
993
+
994
+ const pattern1 = new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
995
+ const pattern2 = new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
996
+
997
+ const match1 = cleanedSQL.match(pattern1);
998
+ if (match1 && match1[1]) {
999
+ return { [match1[1]]: user.uid };
1000
+ }
1001
+
1002
+ const match2 = cleanedSQL.match(pattern2);
1003
+ if (match2 && match2[1]) {
1004
+ return { [match2[1]]: user.uid };
1005
+ }
1006
+
1007
+ const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
1008
+ if (simpleEqualityMatch) {
1009
+ const field = simpleEqualityMatch[1];
1010
+ const operator = simpleEqualityMatch[2];
1011
+ const value = simpleEqualityMatch[3];
1012
+ if (operator === "=") return { [field]: value };
1013
+ if (operator === "!=") return { [field]: { $ne: value } };
1014
+ }
1015
+
1016
+ return {};
1017
+ }
1018
+
1019
+ function getMongoFilterForRule(rule: SecurityRule, user: User): Filter<Document> | null {
1020
+ if (rule.access === "public") return {};
1021
+
1022
+ const filters: Filter<Document>[] = [];
1023
+
1024
+ if (rule.ownerField) {
1025
+ filters.push({ [rule.ownerField]: user.uid });
1026
+ }
1027
+
1028
+ if (rule.using) {
1029
+ const f = getMongoFilterForSQL(rule.using, user);
1030
+ if (f) filters.push(f);
1031
+ }
1032
+
1033
+ if (rule.withCheck) {
1034
+ const f = getMongoFilterForSQL(rule.withCheck, user);
1035
+ if (f) filters.push(f);
1036
+ }
1037
+
1038
+ if (filters.length === 0) return {};
1039
+ if (filters.length === 1) return filters[0];
1040
+ return { $and: filters } as Filter<Document>;
1041
+ }
1042
+
1043
+ function buildMongoFilterFromSecurityRules<M extends Record<string, any>>(
1044
+ collection: CollectionConfig<M> | undefined,
1045
+ user: User,
1046
+ targetOperation: "select" | "insert" | "update" | "delete"
1047
+ ): Filter<Document> | null {
1048
+ if (!collection || !collection.securityRules || collection.securityRules.length === 0) {
1049
+ return {};
1050
+ }
1051
+
1052
+ const applicableRules = collection.securityRules.filter((r: SecurityRule) =>
1053
+ r.operation === targetOperation ||
1054
+ r.operation === "all" ||
1055
+ r.operations?.includes(targetOperation) ||
1056
+ r.operations?.includes("all")
1057
+ );
1058
+
1059
+ if (applicableRules.length === 0) {
1060
+ return null;
1061
+ }
1062
+
1063
+ const userRoleIds = user.roles ?? [];
1064
+ const userRoles = [...userRoleIds, "public"];
1065
+ const roleApplicableRules = applicableRules.filter((rule: SecurityRule) => {
1066
+ if (!rule.roles || rule.roles.length === 0) return true;
1067
+ return rule.roles.some((r: string) => userRoles.includes(r));
1068
+ });
1069
+
1070
+ if (roleApplicableRules.length === 0) {
1071
+ return null;
1072
+ }
1073
+
1074
+ const permissiveFilters: Filter<Document>[] = [];
1075
+ const restrictiveFilters: Filter<Document>[] = [];
1076
+
1077
+ for (const rule of roleApplicableRules) {
1078
+ const mode = rule.mode || "permissive";
1079
+ const filter = getMongoFilterForRule(rule, user);
1080
+ if (filter === null) {
1081
+ if (mode === "restrictive") {
1082
+ return null;
1083
+ }
1084
+ continue;
1085
+ }
1086
+
1087
+ if (mode === "restrictive") {
1088
+ restrictiveFilters.push(filter);
1089
+ } else {
1090
+ permissiveFilters.push(filter);
1091
+ }
1092
+ }
1093
+
1094
+ const finalAnds: Filter<Document>[] = [];
1095
+
1096
+ if (permissiveFilters.length > 0) {
1097
+ const hasAlwaysTruePermissive = permissiveFilters.some(f => Object.keys(f).length === 0);
1098
+ if (!hasAlwaysTruePermissive) {
1099
+ if (permissiveFilters.length === 1) {
1100
+ finalAnds.push(permissiveFilters[0]);
1101
+ } else {
1102
+ finalAnds.push({ $or: permissiveFilters } as Filter<Document>);
1103
+ }
1104
+ }
1105
+ } else {
1106
+ return null;
1107
+ }
1108
+
1109
+ if (restrictiveFilters.length > 0) {
1110
+ for (const rf of restrictiveFilters) {
1111
+ if (Object.keys(rf).length > 0) {
1112
+ finalAnds.push(rf);
1113
+ }
1114
+ }
1115
+ }
1116
+
1117
+ if (finalAnds.length === 0) return {};
1118
+ if (finalAnds.length === 1) return finalAnds[0];
1119
+ return { $and: finalAnds } as Filter<Document>;
1120
+ }