@rebasepro/server-mongo 0.17.3 → 0.18.0

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 (50) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +4 -0
  3. package/dist/MongoBootstrapper.d.ts +0 -1
  4. package/dist/auth/ensure-collections.d.ts +0 -1
  5. package/dist/auth/services.d.ts +0 -1
  6. package/dist/connection.d.ts +0 -1
  7. package/dist/db/MongoConditionBuilder.d.ts +0 -1
  8. package/dist/db/MongoDataService.d.ts +0 -1
  9. package/dist/db/securityRuleFilter.d.ts +0 -1
  10. package/dist/factory.d.ts +0 -1
  11. package/dist/history/ensure-history-collection.d.ts +0 -1
  12. package/dist/index.d.ts +0 -1
  13. package/dist/index.es.js +83 -79
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/schema/plan-schema-change.d.ts +0 -1
  16. package/dist/services/MongoDriver.d.ts +0 -1
  17. package/dist/services/MongoHistoryService.d.ts +0 -1
  18. package/dist/services/MongoRealtimeService.d.ts +0 -1
  19. package/dist/websocket.d.ts +0 -1
  20. package/package.json +28 -24
  21. package/dist/MongoBootstrapper.d.ts.map +0 -1
  22. package/dist/auth/ensure-collections.d.ts.map +0 -1
  23. package/dist/auth/services.d.ts.map +0 -1
  24. package/dist/connection.d.ts.map +0 -1
  25. package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
  26. package/dist/db/MongoDataService.d.ts.map +0 -1
  27. package/dist/db/securityRuleFilter.d.ts.map +0 -1
  28. package/dist/factory.d.ts.map +0 -1
  29. package/dist/history/ensure-history-collection.d.ts.map +0 -1
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/schema/plan-schema-change.d.ts.map +0 -1
  32. package/dist/services/MongoDriver.d.ts.map +0 -1
  33. package/dist/services/MongoHistoryService.d.ts.map +0 -1
  34. package/dist/services/MongoRealtimeService.d.ts.map +0 -1
  35. package/dist/websocket.d.ts.map +0 -1
  36. package/src/MongoBootstrapper.ts +0 -204
  37. package/src/auth/ensure-collections.ts +0 -153
  38. package/src/auth/services.ts +0 -866
  39. package/src/connection.ts +0 -60
  40. package/src/db/MongoConditionBuilder.ts +0 -348
  41. package/src/db/MongoDataService.ts +0 -412
  42. package/src/db/securityRuleFilter.ts +0 -398
  43. package/src/factory.ts +0 -331
  44. package/src/history/ensure-history-collection.ts +0 -22
  45. package/src/index.ts +0 -25
  46. package/src/schema/plan-schema-change.ts +0 -159
  47. package/src/services/MongoDriver.ts +0 -950
  48. package/src/services/MongoHistoryService.ts +0 -186
  49. package/src/services/MongoRealtimeService.ts +0 -592
  50. package/src/websocket.ts +0 -387
@@ -1,950 +0,0 @@
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
- SecurityOperation
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, PolicyClauses } 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 { assertSecurityRulesEnforceable, buildMongoFilterFromSecurityRules } from "../db/securityRuleFilter";
36
- import { logger } from "@rebasepro/server";
37
-
38
- /**
39
- * MongoDB DataDriver Delegate
40
- *
41
- * Implements the DataDriver interface for Rebase.
42
- * Provides all data operations needed by the Rebase frontend.
43
- */
44
- export class MongoDriver implements DataDriver {
45
- key = "mongodb";
46
- initialised = true;
47
-
48
- private dataService: MongoDataService;
49
- private realtimeService: MongoRealtimeService;
50
- public historyService: MongoHistoryService;
51
- public user?: User;
52
- public data: RebaseSdkData;
53
- public client?: RebaseClient;
54
-
55
- constructor(
56
- private db: Db,
57
- realtimeService?: MongoRealtimeService,
58
- historyService?: MongoHistoryService,
59
- public readonly registry?: CollectionRegistryInterface,
60
- user?: User
61
- ) {
62
- this.dataService = new MongoDataService(db);
63
- this.realtimeService = realtimeService ?? new MongoRealtimeService(db);
64
- this.historyService = historyService ?? new MongoHistoryService(db);
65
- this.user = user;
66
- this.data = buildSdkData(this);
67
- this.realtimeService.setDataDriver(this);
68
- }
69
-
70
- /**
71
- * Get the current timestamp
72
- */
73
- currentTime(): Date {
74
- return new Date();
75
- }
76
-
77
- /**
78
- * Resolve a collection's callbacks and property callbacks from the registry.
79
- * Used by AuthenticatedMongoDriver to apply callbacks after RLS filtering.
80
- */
81
- resolveCollectionCallbacks<M extends Record<string, unknown>>(
82
- collection: CollectionConfig<M> | undefined,
83
- path: string
84
- ) {
85
- if (!collection && !path) return { collection: undefined,
86
- callbacks: undefined,
87
- globalCallbacks: undefined,
88
- propertyCallbacks: undefined };
89
- const registryCollection = this.registry?.getCollectionByPath(path);
90
- const resolvedCollection = registryCollection
91
- ? ({ ...collection,
92
- ...registryCollection } as CollectionConfig<M>)
93
- : (collection as CollectionConfig<M>);
94
-
95
- const callbacks = resolvedCollection?.callbacks;
96
- const globalCallbacks = this.registry?.getGlobalCallbacks();
97
- const properties = resolvedCollection?.properties;
98
- let propertyCallbacks;
99
- if (properties) {
100
- propertyCallbacks = buildPropertyCallbacks(properties);
101
- }
102
- return {
103
- collection: resolvedCollection,
104
- callbacks,
105
- globalCallbacks,
106
- propertyCallbacks
107
- };
108
- }
109
-
110
- /**
111
- * Fetch a collection of rows
112
- */
113
- async fetchCollection<M extends Record<string, any>>(
114
- props: FetchCollectionProps<M>
115
- ): Promise<Record<string, unknown>[]> {
116
- // Forwarded whole rather than re-listed. The hand-written list here
117
- // named eight of the eleven fields `FetchCollectionProps` declares, so
118
- // `logical` and `offset` were accepted by every type-checked boundary
119
- // above and then dropped — an `or(...)` query ran unfiltered and
120
- // `?offset=` served page one.
121
- const { path, collection, ...query } = props;
122
- const rows = await this.dataService.fetchCollection<M>(path, {
123
- ...query,
124
- collection: collection as CollectionConfig
125
- });
126
-
127
- const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
128
-
129
- if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
130
- const contextForCallback = {
131
- user: this.user,
132
- driver: this,
133
- data: this.data,
134
- client: this.client,
135
- storageSource: this.client?.storage
136
- } as unknown as RebaseCallContext; // Backend context
137
- return Promise.all(rows.map(async (row) => {
138
- let fetched = row;
139
- if (globalCallbacks?.afterRead) {
140
- fetched = await globalCallbacks.afterRead({
141
- collection: resolvedCollection as CollectionConfig<M>,
142
- path,
143
- row: fetched,
144
- context: contextForCallback
145
- }) ?? fetched;
146
- }
147
- if (callbacks?.afterRead) {
148
- fetched = await callbacks.afterRead({
149
- collection: resolvedCollection as CollectionConfig<M>,
150
- path,
151
- row: fetched,
152
- context: contextForCallback
153
- }) ?? fetched;
154
- }
155
- if (propertyCallbacks?.afterRead) {
156
- fetched = await propertyCallbacks.afterRead({
157
- collection: resolvedCollection as CollectionConfig<M>,
158
- path,
159
- row: fetched,
160
- context: contextForCallback
161
- }) ?? fetched;
162
- }
163
- return fetched;
164
- }));
165
- }
166
-
167
- return rows;
168
- }
169
-
170
- /**
171
- * Listen to collection changes.
172
- *
173
- * `authContext` is not part of `ListenCollectionProps`; it is supplied by
174
- * {@link AuthenticatedMongoDriver}, which is the only caller that has one.
175
- * It has to travel *into* the subscription config, because that config is
176
- * what every re-fetch reads — the wrapper used to stamp the field on the
177
- * `Subscription` object instead, and nothing has ever read that one.
178
- */
179
- listenCollection<M extends Record<string, any>>(
180
- // `collection` is re-resolved from the registry on every re-fetch, and
181
- // `vectorSearch` is not a thing a subscription can do — the Postgres
182
- // service refuses it outright rather than run it once and never again.
183
- { onUpdate, onError, collection, vectorSearch, ...query }: ListenCollectionProps<M>,
184
- authContext?: { uid: string; roles: string[] }
185
- ): () => void {
186
- const subscriptionId = this.generateSubscriptionId();
187
-
188
- const callback = (rows: Record<string, unknown>[]) => {
189
- try {
190
- onUpdate(rows);
191
- } catch (error) {
192
- logger.error("Error in collection update callback", { error: error });
193
- if (onError) {
194
- onError(error instanceof Error ? error : new Error(String(error)));
195
- }
196
- }
197
- };
198
-
199
- // Forwarded whole rather than re-listed, for the reason `fetchCollection`
200
- // gives above: the hand-written list named seven of the eleven fields
201
- // `ListenCollectionProps` declares, so `logical` and `offset` were
202
- // accepted at every type-checked boundary and then dropped — an
203
- // `or(...)` subscription was pushed every row, and a subscription to
204
- // page two was pushed page one.
205
- this.realtimeService.subscribeToCollection(
206
- subscriptionId,
207
- { clientId: "driver", ...query, authContext },
208
- callback
209
- );
210
-
211
- // Return unsubscribe function
212
- return () => {
213
- this.realtimeService.unsubscribe(subscriptionId);
214
- };
215
- }
216
-
217
- /**
218
- * Fetch a single row
219
- */
220
- async fetchOne<M extends Record<string, any>>({
221
- path,
222
- id,
223
- databaseId,
224
- collection
225
- }: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
226
- let row = await this.dataService.fetchOne<M>(path, id, databaseId);
227
-
228
- const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
229
-
230
- if (row && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
231
- const contextForCallback = {
232
- user: this.user,
233
- driver: this,
234
- data: this.data,
235
- client: this.client,
236
- storageSource: this.client?.storage
237
- } as unknown as RebaseCallContext; // Backend context
238
- let processedRow: Record<string, unknown> = row;
239
- if (globalCallbacks?.afterRead) {
240
- processedRow = await globalCallbacks.afterRead({
241
- collection: resolvedCollection as CollectionConfig<M>,
242
- path,
243
- row: processedRow,
244
- context: contextForCallback
245
- }) ?? processedRow;
246
- }
247
- if (callbacks?.afterRead) {
248
- processedRow = await callbacks.afterRead({
249
- collection: resolvedCollection as CollectionConfig<M>,
250
- path,
251
- row: processedRow,
252
- context: contextForCallback
253
- }) ?? processedRow;
254
- }
255
- if (propertyCallbacks?.afterRead) {
256
- processedRow = await propertyCallbacks.afterRead({
257
- collection: resolvedCollection as CollectionConfig<M>,
258
- path,
259
- row: processedRow,
260
- context: contextForCallback
261
- }) ?? processedRow;
262
- }
263
- row = processedRow;
264
- }
265
-
266
- return row;
267
- }
268
-
269
- /**
270
- * Listen to row changes
271
- */
272
- listenOne<M extends Record<string, any>>({
273
- path,
274
- id,
275
- collection,
276
- onUpdate,
277
- onError
278
- }: ListenOneProps<M>, authContext?: { uid: string; roles: string[] }): () => void {
279
- const subscriptionId = this.generateSubscriptionId();
280
-
281
- const callback = (row: Record<string, unknown> | null) => {
282
- try {
283
- onUpdate(row);
284
- } catch (error) {
285
- logger.error("Error in row update callback", { error: error });
286
- if (onError) {
287
- onError(error instanceof Error ? error : new Error(String(error)));
288
- }
289
- }
290
- };
291
-
292
- this.realtimeService.subscribeToOne(
293
- subscriptionId,
294
- {
295
- clientId: "driver",
296
- path,
297
- id,
298
- authContext
299
- },
300
- callback
301
- );
302
-
303
- // Return unsubscribe function
304
- return () => {
305
- this.realtimeService.unsubscribe(subscriptionId);
306
- };
307
- }
308
-
309
- /**
310
- * Save an row (create or update)
311
- */
312
- async save<M extends Record<string, any>>({
313
- path,
314
- id,
315
- values,
316
- collection,
317
- status
318
- }: SaveProps<M>): Promise<Record<string, unknown>> {
319
- const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
320
-
321
- let updatedValues = values;
322
- const contextForCallback = {
323
- user: this.user,
324
- driver: this,
325
- data: this.data,
326
- client: this.client,
327
- storageSource: this.client?.storage
328
- } as unknown as RebaseCallContext;
329
-
330
- // Fetch previous values for callbacks AND history recording
331
- let previousValuesForHistory: Partial<M> | undefined;
332
- if (status === "existing" && id) {
333
- const existing = await this.dataService.fetchOne<M>(path, id, resolvedCollection?.databaseId);
334
- if (existing) {
335
- const { id: _existingId, ...existingValues } = existing;
336
- previousValuesForHistory = existingValues as Partial<M>;
337
- }
338
- }
339
-
340
- if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
341
- if (globalCallbacks?.beforeSave) {
342
- const result = await globalCallbacks.beforeSave({
343
- collection: resolvedCollection as CollectionConfig<M>,
344
- path,
345
- id,
346
- values: updatedValues,
347
- previousValues: previousValuesForHistory,
348
- status,
349
- context: contextForCallback
350
- });
351
- if (result) updatedValues = mergeDeep(updatedValues, result);
352
- }
353
-
354
- if (callbacks?.beforeSave) {
355
- const result = await callbacks.beforeSave({
356
- collection: resolvedCollection as CollectionConfig<M>,
357
- path,
358
- id,
359
- values: updatedValues,
360
- previousValues: previousValuesForHistory,
361
- status,
362
- context: contextForCallback
363
- });
364
- if (result) updatedValues = mergeDeep(updatedValues, result);
365
- }
366
-
367
- if (propertyCallbacks?.beforeSave) {
368
- const result = await propertyCallbacks.beforeSave({
369
- collection: resolvedCollection as CollectionConfig<M>,
370
- path,
371
- id,
372
- values: updatedValues,
373
- previousValues: previousValuesForHistory,
374
- status,
375
- context: contextForCallback
376
- });
377
- if (result) updatedValues = mergeDeep(updatedValues, result);
378
- }
379
- }
380
-
381
- // Apply autoValue timestamps (on_create / on_update) at the application layer.
382
- if (resolvedCollection?.properties) {
383
- updatedValues = updateDateAutoValues({
384
- inputValues: updatedValues,
385
- properties: resolvedCollection.properties,
386
- status: status ?? "new",
387
- timestampNowValue: new Date()
388
- });
389
- }
390
-
391
- try {
392
- let savedRow = await this.dataService.save<M>(
393
- path,
394
- updatedValues,
395
- id,
396
- resolvedCollection?.databaseId
397
- );
398
-
399
- if (savedRow && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
400
- if (globalCallbacks?.afterRead) {
401
- savedRow = await globalCallbacks.afterRead({
402
- collection: resolvedCollection as CollectionConfig<M>,
403
- path,
404
- row: savedRow,
405
- context: contextForCallback
406
- }) ?? savedRow;
407
- }
408
- if (callbacks?.afterRead) {
409
- savedRow = await callbacks.afterRead({
410
- collection: resolvedCollection as CollectionConfig<M>,
411
- path,
412
- row: savedRow,
413
- context: contextForCallback
414
- }) ?? savedRow;
415
- }
416
- if (propertyCallbacks?.afterRead) {
417
- savedRow = await propertyCallbacks.afterRead({
418
- collection: resolvedCollection as CollectionConfig<M>,
419
- path,
420
- row: savedRow,
421
- context: contextForCallback
422
- }) ?? savedRow;
423
- }
424
- }
425
-
426
- const savedId = savedRow.id as string | number;
427
- const { id: _savedId, ...savedValues } = savedRow;
428
-
429
- if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
430
- if (globalCallbacks?.afterSave) {
431
- await globalCallbacks.afterSave({
432
- collection: resolvedCollection as CollectionConfig<M>,
433
- path,
434
- id: savedId,
435
- values: savedValues,
436
- previousValues: previousValuesForHistory,
437
- status,
438
- context: contextForCallback
439
- });
440
- }
441
- if (callbacks?.afterSave) {
442
- await callbacks.afterSave({
443
- collection: resolvedCollection as CollectionConfig<M>,
444
- path,
445
- id: savedId,
446
- values: savedValues as Partial<M>,
447
- previousValues: previousValuesForHistory,
448
- status,
449
- context: contextForCallback
450
- });
451
- }
452
- if (propertyCallbacks?.afterSave) {
453
- await propertyCallbacks.afterSave({
454
- collection: resolvedCollection as CollectionConfig<M>,
455
- path,
456
- id: savedId,
457
- values: savedValues,
458
- previousValues: previousValuesForHistory,
459
- status,
460
- context: contextForCallback
461
- });
462
- }
463
- }
464
-
465
- // Record row history (fire-and-forget, never blocks the save)
466
- if (this.historyService && resolvedCollection?.history) {
467
- this.historyService.recordHistory({
468
- tableName: path,
469
- id: savedId.toString(),
470
- action: status === "new" ? "create" : "update",
471
- values: savedValues as Record<string, unknown>,
472
- previousValues: previousValuesForHistory as Record<string, unknown> | undefined,
473
- updatedBy: this.user?.uid
474
- }).catch(err => {
475
- logger.error(`Failed to record history for ${path}/${savedId}`, { error: err });
476
- });
477
- }
478
-
479
- // Notify real-time subscribers
480
- await this.realtimeService.notifyUpdate(
481
- path,
482
- savedId.toString(),
483
- savedRow
484
- );
485
-
486
- return savedRow;
487
- } catch (error) {
488
- if (callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
489
- if (callbacks?.afterSaveError) {
490
- await callbacks.afterSaveError({
491
- collection: resolvedCollection as CollectionConfig<M>,
492
- path,
493
- id: id || "unknown",
494
- values: updatedValues,
495
- previousValues: undefined,
496
- status,
497
- context: contextForCallback
498
- });
499
- }
500
- if (propertyCallbacks?.afterSaveError) {
501
- await propertyCallbacks.afterSaveError({
502
- collection: resolvedCollection as CollectionConfig<M>,
503
- path,
504
- id: id || "unknown",
505
- values: updatedValues,
506
- previousValues: undefined,
507
- status,
508
- context: contextForCallback
509
- });
510
- }
511
- }
512
- throw error;
513
- }
514
- }
515
-
516
- /**
517
- * Delete an row
518
- */
519
- async delete<M extends Record<string, any>>({
520
- row,
521
- collection
522
- }: DeleteProps<M>): Promise<void> {
523
- const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
524
-
525
- const callbackRow: Record<string, unknown> = { id: row.id, ...(row.values ?? {}) };
526
-
527
- const contextForCallback = {
528
- user: this.user,
529
- driver: this,
530
- data: this.data,
531
- client: this.client,
532
- storageSource: this.client?.storage
533
- } as unknown as RebaseCallContext;
534
-
535
- if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
536
- let preventDefault = false;
537
- if (globalCallbacks?.beforeDelete) {
538
- const result = await globalCallbacks.beforeDelete({
539
- collection: resolvedCollection as CollectionConfig<M>,
540
- path: row.path,
541
- id: row.id,
542
- row: callbackRow,
543
- context: contextForCallback
544
- });
545
- if (result === false) {
546
- preventDefault = true;
547
- }
548
- }
549
- if (callbacks?.beforeDelete) {
550
- const result = await callbacks.beforeDelete({
551
- collection: resolvedCollection as CollectionConfig<M>,
552
- path: row.path,
553
- id: row.id,
554
- row: callbackRow,
555
- context: contextForCallback
556
- });
557
- if (result === false) {
558
- preventDefault = true;
559
- }
560
- }
561
- if (propertyCallbacks?.beforeDelete) {
562
- const result = await propertyCallbacks.beforeDelete({
563
- collection: resolvedCollection as CollectionConfig<M>,
564
- path: row.path,
565
- id: row.id,
566
- row: callbackRow,
567
- context: contextForCallback
568
- });
569
- if (result === false) {
570
- preventDefault = true;
571
- }
572
- }
573
- if (preventDefault) {
574
- return;
575
- }
576
- }
577
-
578
- await this.dataService.delete(row.path, row.id);
579
-
580
- if (globalCallbacks?.afterDelete || callbacks?.afterDelete || propertyCallbacks?.afterDelete) {
581
- if (globalCallbacks?.afterDelete) {
582
- await globalCallbacks.afterDelete({
583
- collection: resolvedCollection as CollectionConfig<M>,
584
- path: row.path,
585
- id: row.id,
586
- row: callbackRow,
587
- context: contextForCallback
588
- });
589
- }
590
- if (callbacks?.afterDelete) {
591
- await callbacks.afterDelete({
592
- collection: resolvedCollection as CollectionConfig<M>,
593
- path: row.path,
594
- id: row.id,
595
- row: callbackRow,
596
- context: contextForCallback
597
- });
598
- }
599
- if (propertyCallbacks?.afterDelete) {
600
- await propertyCallbacks.afterDelete({
601
- collection: resolvedCollection as CollectionConfig<M>,
602
- path: row.path,
603
- id: row.id,
604
- row: callbackRow,
605
- context: contextForCallback
606
- });
607
- }
608
- }
609
-
610
- // Record history
611
- if (this.historyService && resolvedCollection?.history) {
612
- this.historyService.recordHistory({
613
- action: "delete",
614
- id: String(row.id),
615
- tableName: row.path,
616
- previousValues: row.values,
617
- updatedBy: this.user?.uid
618
- }).catch(err => {
619
- logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
620
- });
621
- }
622
-
623
- // Notify subscribers of the deletion
624
- await this.realtimeService.notifyUpdate(row.path, String(row.id), null);
625
- }
626
-
627
- /**
628
- * Check if a field value is unique
629
- */
630
- async checkUniqueField(
631
- path: string,
632
- name: string,
633
- value: any,
634
- id?: string,
635
- collection?: CollectionConfig
636
- ): Promise<boolean> {
637
- return this.dataService.checkUniqueField(path, name, value, id);
638
- }
639
-
640
- /**
641
- * Generate a new row ID
642
- */
643
- generateId(path: string, collection?: CollectionConfig): string {
644
- return this.dataService.generateId();
645
- }
646
-
647
- /**
648
- * Count rows in a collection
649
- */
650
- async count<M extends Record<string, any>>({
651
- path,
652
- collection,
653
- filter,
654
- logical,
655
- searchString
656
- }: FetchCollectionProps<M>): Promise<number> {
657
- // The same narrowing the listing gets, or the total describes a
658
- // different query than the rows it is reported beside.
659
- return this.dataService.count<M>(path, {
660
- filter,
661
- logical,
662
- searchString,
663
- collection: collection as CollectionConfig
664
- });
665
- }
666
-
667
- /**
668
- * Generate a unique subscription ID
669
- */
670
- private generateSubscriptionId(): string {
671
- return `mongo_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
672
- }
673
-
674
- /**
675
- * Check if the delegate is ready
676
- */
677
- isReady(): boolean {
678
- return this.initialised;
679
- }
680
-
681
- /**
682
- * Get the underlying row service for direct access
683
- */
684
- getDataService(): MongoDataService {
685
- return this.dataService;
686
- }
687
-
688
- /**
689
- * Get the underlying realtime service for direct access
690
- */
691
- getRealtimeService(): MongoRealtimeService {
692
- return this.realtimeService;
693
- }
694
-
695
- /**
696
- * Scope the MongoDriver with an authenticated user context
697
- */
698
- async withAuth(user: User): Promise<DataDriver> {
699
- return new AuthenticatedMongoDriver(this, user);
700
- }
701
- }
702
-
703
- export class AuthenticatedMongoDriver implements DataDriver {
704
- key = "mongodb";
705
- initialised = true;
706
- public user: User;
707
- public data: RebaseSdkData;
708
-
709
- constructor(public delegate: MongoDriver, user: User) {
710
- this.user = user;
711
- this.data = buildSdkData(this);
712
- }
713
-
714
- currentTime(): Date {
715
- return this.delegate.currentTime();
716
- }
717
-
718
- async fetchCollection<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]> {
719
- const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
720
- const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
721
- if (rlsFilter === null) {
722
- return [];
723
- }
724
-
725
- // `logical` belongs in this query, not in the props spread below: the
726
- // repository reads `rawQuery ?? buildQuery(...)`, so a `logical` that
727
- // travelled only in the spread was never consulted — and a dropped
728
- // `or(...)` group does not fail, it widens.
729
- const userQuery = MongoConditionBuilder.buildQuery({
730
- filter: props.filter,
731
- logical: props.logical,
732
- searchString: props.searchString,
733
- properties: resolvedCollection?.properties
734
- });
735
-
736
- const combinedQuery = Object.keys(rlsFilter).length > 0
737
- ? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
738
- : userQuery;
739
-
740
- const originalService = this.delegate.getDataService();
741
- const rows = await originalService.fetchCollection<M>(props.path, {
742
- ...props,
743
- rawQuery: combinedQuery,
744
- collection: resolvedCollection
745
- });
746
-
747
- const { callbacks, globalCallbacks, propertyCallbacks } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
748
-
749
- if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
750
- const contextForCallback = {
751
- user: this.user,
752
- driver: this,
753
- data: this.data,
754
- client: this.delegate.client,
755
- storageSource: this.delegate.client?.storage
756
- } as unknown as RebaseCallContext;
757
- return Promise.all(rows.map(async (row) => {
758
- let fetched = row;
759
- if (globalCallbacks?.afterRead) {
760
- fetched = await globalCallbacks.afterRead({
761
- collection: resolvedCollection as CollectionConfig<M>,
762
- path: props.path,
763
- row: fetched,
764
- context: contextForCallback
765
- }) ?? fetched;
766
- }
767
- if (callbacks?.afterRead) {
768
- fetched = await callbacks.afterRead({
769
- collection: resolvedCollection as CollectionConfig<M>,
770
- path: props.path,
771
- row: fetched,
772
- context: contextForCallback
773
- }) ?? fetched;
774
- }
775
- if (propertyCallbacks?.afterRead) {
776
- fetched = await propertyCallbacks.afterRead({
777
- collection: resolvedCollection as CollectionConfig<M>,
778
- path: props.path,
779
- row: fetched,
780
- context: contextForCallback
781
- }) ?? fetched;
782
- }
783
- return fetched;
784
- }));
785
- }
786
-
787
- return rows;
788
- }
789
-
790
- listenCollection<M extends Record<string, any>>(props: ListenCollectionProps<M>): () => void {
791
- // Handed to the subscription rather than stamped on it afterwards: the
792
- // config is what every re-fetch reads, and the stamp also landed after
793
- // the initial fetch had already been dispatched unfiltered.
794
- return this.delegate.listenCollection(props, this.authContext());
795
- }
796
-
797
- /** The acting user, in the shape the realtime subscriptions carry. */
798
- private authContext(): { uid: string; roles: string[] } {
799
- return { uid: this.user.uid,
800
- roles: this.user.roles ?? [] };
801
- }
802
-
803
- /**
804
- * Evaluate the collection's rules for one row, fail-closed.
805
- *
806
- * A path with no resolvable collection has no declared rules — the same
807
- * answer `buildMongoFilterFromSecurityRules` gives a listing on such a path,
808
- * so the two never disagree about whether this engine has row security.
809
- */
810
- private authorize(
811
- collection: CollectionConfig | undefined,
812
- entity: Entity,
813
- operation: SecurityOperation,
814
- clauses?: PolicyClauses
815
- ): boolean {
816
- if (!collection) return true;
817
- return checkOperation(collection, { user: this.user }, entity, operation, { onUnknown: "deny",
818
- clauses });
819
- }
820
-
821
- async fetchOne<M extends Record<string, any>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> {
822
- const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
823
- assertSecurityRulesEnforceable(resolvedCollection, "select");
824
- const row = await this.delegate.fetchOne(props);
825
- if (row && !this.authorize(resolvedCollection, rowToEntityForCheck(row, props.path), "select")) {
826
- return undefined;
827
- }
828
- return row;
829
- }
830
-
831
- listenOne<M extends Record<string, any>>(props: ListenOneProps<M>): () => void {
832
- return this.delegate.listenOne(props, this.authContext());
833
- }
834
-
835
- /**
836
- * Save, with both halves of the rule checked *before* the write.
837
- *
838
- * There is no transaction here, so a check that runs after
839
- * `delegate.save` cannot undo anything: the document is written, history is
840
- * recorded and subscribers have been notified by then, and a 403 at that
841
- * point only misleads the caller about what happened. Postgres evaluates
842
- * `WITH CHECK` inside the transaction; the closest this driver can get is to
843
- * evaluate it against the row as it *will* be, and refuse before writing.
844
- */
845
- async save<M extends Record<string, any>>(props: SaveProps<M>): Promise<Record<string, unknown>> {
846
- const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
847
-
848
- if (props.status === "existing" && props.id) {
849
- assertSecurityRulesEnforceable(resolvedCollection, "update");
850
- const existing = await this.delegate.fetchOne({ path: props.path,
851
- id: props.id,
852
- collection: resolvedCollection });
853
- // USING against the stored row, WITH CHECK against the row that
854
- // will replace it — the split Postgres makes, and the reason
855
- // `clauses` exists on `checkOperation`.
856
- const projected = rowToEntityForCheck({ ...existing,
857
- ...props.values,
858
- id: props.id }, props.path);
859
- if (!existing ||
860
- !this.authorize(resolvedCollection, rowToEntityForCheck(existing, props.path), "update", "using") ||
861
- !this.authorize(resolvedCollection, projected, "update", "withCheck")) {
862
- throw ApiError.forbidden("Forbidden");
863
- }
864
- } else {
865
- assertSecurityRulesEnforceable(resolvedCollection, "insert");
866
- const tempEntity = { id: props.id || "new",
867
- path: props.path,
868
- values: props.values } as Entity;
869
- if (!this.authorize(resolvedCollection, tempEntity, "insert")) {
870
- throw ApiError.forbidden("Forbidden");
871
- }
872
- }
873
-
874
- return this.delegate.save({
875
- ...props,
876
- collection: resolvedCollection
877
- });
878
- }
879
-
880
- async delete<M extends Record<string, any>>(props: DeleteProps<M>): Promise<void> {
881
- const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.row.path);
882
- assertSecurityRulesEnforceable(resolvedCollection, "delete");
883
-
884
- const existing = await this.delegate.fetchOne({ path: props.row.path,
885
- id: props.row.id,
886
- collection: resolvedCollection });
887
- if (!existing || !this.authorize(resolvedCollection, rowToEntityForCheck(existing, props.row.path), "delete")) {
888
- throw ApiError.forbidden("Forbidden");
889
- }
890
-
891
- return this.delegate.delete(props);
892
- }
893
-
894
- async checkUniqueField(
895
- path: string,
896
- name: string,
897
- value: any,
898
- id?: string,
899
- collection?: CollectionConfig
900
- ): Promise<boolean> {
901
- return this.delegate.checkUniqueField(path, name, value, id, collection);
902
- }
903
-
904
- generateId(path: string, collection?: CollectionConfig): string {
905
- return this.delegate.generateId(path, collection);
906
- }
907
-
908
- async count<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<number> {
909
- const { collection: resolvedCollection } = this.delegate.resolveCollectionCallbacks(props.collection, props.path);
910
- const rlsFilter = buildMongoFilterFromSecurityRules(resolvedCollection, this.user, "select");
911
- if (rlsFilter === null) {
912
- return 0;
913
- }
914
-
915
- // Narrowed by exactly what the listing is narrowed by — `logical`
916
- // included — or the total describes a different query than the rows.
917
- const userQuery = MongoConditionBuilder.buildQuery({
918
- filter: props.filter,
919
- logical: props.logical,
920
- searchString: props.searchString,
921
- properties: resolvedCollection?.properties
922
- });
923
-
924
- const combinedQuery = Object.keys(rlsFilter).length > 0
925
- ? ({ $and: [userQuery, rlsFilter] } as Filter<Document>)
926
- : userQuery;
927
-
928
- const originalService = this.delegate.getDataService();
929
- return originalService.count(props.path, {
930
- ...props,
931
- rawQuery: combinedQuery
932
- });
933
- }
934
-
935
- isReady(): boolean {
936
- return this.delegate.isReady();
937
- }
938
- }
939
-
940
- /**
941
- * Wrap a flat row into the Entity shape expected by `checkOperation`,
942
- * which evaluates security rules against `row.values`.
943
- */
944
- function rowToEntityForCheck(row: Record<string, unknown>, path: string): Entity {
945
- return {
946
- id: row.id as string | number,
947
- path,
948
- values: row
949
- };
950
- }