@rebasepro/server-mongo 0.21.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,26 +9,26 @@ pnpm add @rebasepro/server-mongo
9
9
  ```
10
10
 
11
11
  ESM-only: `"type": "module"` with no CommonJS build, so it is loaded with
12
- `import`. `require()` of it resolves only on Node 22.12+, which supports
13
- `require(esm)`.
12
+ `import`. It needs Node `>=22.22.0` (its `engines` floor), where `require()`
13
+ of it resolves too: Node has supported `require(esm)` since 22.12.
14
14
 
15
15
  ## What This Package Does
16
16
 
17
- Implements the Rebase `BackendBootstrapper` and backend interfaces for MongoDB. Provides a complete data driver, change-stream-based realtime, snapshot history, auth repositories, and WebSocket support. Plug it into `@rebasepro/server` via `createMongoBootstrapper()`, or use the standalone `createMongoBackend()` factory for direct access.
17
+ Implements the Rebase `BackendBootstrapper` and backend interfaces for MongoDB. Provides a complete data driver, change-stream-based realtime, entity history, auth repositories, and WebSocket support. Plug it into `@rebasepro/server` via `createMongoBootstrapper()`, or use the standalone `createMongoBackend()` factory for direct access.
18
18
 
19
19
  ## Key Exports
20
20
 
21
21
  | Export | Description |
22
22
  |--------|-------------|
23
23
  | `createMongoBootstrapper(config)` | Creates a `BackendBootstrapper` for use with `initializeRebaseBackend({ bootstrappers: [...] })`. |
24
- | `createMongoBackend(config)` | Standalone factory — returns a `MongoBackendInstance` with driver, snapshot service, realtime, admin, and lifecycle methods. |
24
+ | `createMongoBackend(config)` | Standalone factory — returns a `MongoBackendInstance` with driver, data service, realtime, admin, and lifecycle methods. |
25
25
  | `createMongoDelegate(db)` | Convenience factory for just the `MongoDriver` (DataDriver). |
26
26
  | `createMongoRealtimeService(db)` | Creates a MongoDB change-stream-based realtime provider. |
27
- | `createMongoSnapshotRepository(db)` | Creates an `SnapshotRepository` for direct CRUD. |
27
+ | `createMongoEntityRepository(db)` | Creates a `DataRepository` for direct CRUD. |
28
28
  | `createMongoDBConnection(url, dbName)` | Connects to MongoDB and returns a `MongoDBConnection` wrapper. |
29
29
  | `MongoDBConnection` | `DatabaseConnection` implementation wrapping `MongoClient` + `Db`. |
30
30
  | `MongoDriver` | The `DataDriver` implementation for MongoDB. |
31
- | `MongoSnapshotService` | Low-level snapshot CRUD service. |
31
+ | `MongoDataService` | Low-level entity CRUD service (the `DataRepository` implementation). |
32
32
  | `MongoRealtimeService` | Change-stream-based `RealtimeProvider`. |
33
33
  | `MongoCollectionRegistry` | In-memory collection registry. |
34
34
  | `isMongoBackendConfig(config)` | Type guard for `MongoBackendConfig`. |
@@ -74,11 +74,11 @@ const backend = createMongoBackend({
74
74
  });
75
75
 
76
76
  // Use directly
77
- const health = await backend.healthCheck();
78
- const snapshots = await backend.snapshotService.fetchCollection("users", {});
77
+ const health = await backend.healthCheck?.();
78
+ const users = await backend.dataService.fetchCollection("users", {});
79
79
 
80
80
  // Cleanup
81
- await backend.destroy();
81
+ await backend.destroy?.();
82
82
  ```
83
83
 
84
84
  ## Related Packages
package/dist/index.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { MongoClient, ObjectId } from "mongodb";
2
2
  import { ANONYMOUS_USER_ID, EntityReference, ListLimitError, getCollectionDataPath, isAnonymousUid, resolveClientListLimit } from "@rebasepro/types";
3
3
  import { buildPropertyCallbacks, buildSdkData, callbackRefusal, checkOperation, normalizeDriverOrderBy, normalizeEmail, requireCallbackClient, securityRuleToConditions, toCallbackError, toFilterTuples, updateDateAutoValues } from "@rebasepro/common";
4
- import { ApiError, logger } from "@rebasepro/server";
4
+ import { ApiError, declaredErrorAnswer, logger } from "@rebasepro/server";
5
5
  import { mergeDeep } from "@rebasepro/utils";
6
6
  //#region \0rolldown/runtime.js
7
7
  var __defProp = Object.defineProperty;
@@ -511,6 +511,44 @@ var MongoDataService = class {
511
511
  * Provides real-time subscriptions to collection and row changes.
512
512
  */
513
513
  /**
514
+ * The answer a failed fetch chose for its caller, when it chose one.
515
+ *
516
+ * A deliberate 4xx — the server's `ApiError`, or the `RebaseApiError` a
517
+ * collection callback throws because a collection file cannot import the
518
+ * server — was decided about this caller, and its message and code are written
519
+ * for them. It is recognised by `declaredErrorAnswer`, the predicate REST and
520
+ * this socket's request frames use. A declared 5xx is not one: its message is
521
+ * about the server, and the Postgres subscription path masks it too.
522
+ */
523
+ var refusalOf = (error) => {
524
+ const answer = declaredErrorAnswer(error);
525
+ return answer && answer.status >= 400 && answer.status < 500 ? answer : void 0;
526
+ };
527
+ /**
528
+ * The frame that tells a subscriber its fetch failed, addressed by the
529
+ * subscription id the client keys its listeners on — the shape of the
530
+ * `INVALID_LIMIT` refusal below.
531
+ *
532
+ * A refusal keeps its message, code and details. Anything else is a fault
533
+ * whose text can name internals (hosts, collections, a document's values), so
534
+ * the subscriber reads a generic message, the same words the Postgres path
535
+ * uses, and the full error goes to the log.
536
+ */
537
+ function subscriptionErrorFrame(subscriptionId, path, error) {
538
+ const refusal = refusalOf(error);
539
+ const message = refusal?.message ?? `Could not load data for "${path ?? ""}". Check server logs for details.`;
540
+ return {
541
+ type: "ERROR",
542
+ subscriptionId,
543
+ payload: { error: {
544
+ message,
545
+ code: refusal?.code ?? "INTERNAL_ERROR",
546
+ ...refusal?.details !== void 0 && { details: refusal.details }
547
+ } },
548
+ error: message
549
+ };
550
+ }
551
+ /**
514
552
  * The query half of a subscription config — everything except who is watching.
515
553
  *
516
554
  * Spread into the re-fetch rather than re-listed field by field. Re-listing is
@@ -564,20 +602,53 @@ var MongoRealtimeService = class {
564
602
  * fetch behind it is the newest thing known about the row, so it must also
565
603
  * be the thing that closes the door on an older fetch still in flight —
566
604
  * otherwise the deleted row reappears a moment after it vanished.
605
+ *
606
+ * `mayReportFailure()` is the same check for reporting this delivery's
607
+ * failure, except that it also answers yes to the delivery that already
608
+ * claimed the slot. That is the send itself failing (the socket closure's
609
+ * `JSON.stringify` on a row that will not serialise) after the check passed
610
+ * and before anything reached the subscriber. Same rule as the Postgres
611
+ * service's `beginDelivery`.
567
612
  */
568
613
  beginDelivery(subscriptionId, subscription) {
569
614
  const seq = ++subscription.started;
570
- return () => {
615
+ const canDeliver = () => {
571
616
  if (this.subscriptions.get(subscriptionId) !== subscription) return false;
572
617
  if (seq <= subscription.delivered) return false;
573
618
  subscription.delivered = seq;
574
619
  return true;
575
620
  };
621
+ const mayReportFailure = () => canDeliver() || this.subscriptions.get(subscriptionId) === subscription && subscription.delivered === seq;
622
+ return Object.assign(canDeliver, { mayReportFailure });
623
+ }
624
+ /**
625
+ * Tell the subscriber a fetch failed, through the slot the rows would have
626
+ * used.
627
+ *
628
+ * The slot matters as much for an error as for rows. Without it, a fetch
629
+ * that a newer delivery has overtaken would mark a view showing current
630
+ * data as failed, and one whose subscription was cancelled or replaced
631
+ * under the same id would fail a different subscription's view.
632
+ */
633
+ reportFetchFailure(subscriptionId, subscription, canDeliver, error) {
634
+ const target = subscription.type === "single" ? "row" : "collection";
635
+ const refusal = refusalOf(error);
636
+ if (refusal) {
637
+ const line = `[API ${refusal.status} ${refusal.code}] fetching ${target} for subscription ${subscriptionId}: ${refusal.message}`;
638
+ if (refusal.expected) logger.debug(line);
639
+ else logger.warn(`⚠️ ${line}`);
640
+ } else logger.error(`Error fetching ${target} for subscription ${subscriptionId}`, { error });
641
+ if (!subscription.onError || !canDeliver.mayReportFailure()) return;
642
+ try {
643
+ subscription.onError(error);
644
+ } catch (reportError) {
645
+ logger.error(`Could not report a failed fetch to subscription ${subscriptionId}`, { error: reportError });
646
+ }
576
647
  }
577
648
  /**
578
649
  * Subscribe to collection changes
579
650
  */
580
- subscribeToCollection(subscriptionId, config, callback) {
651
+ subscribeToCollection(subscriptionId, config, callback, onError) {
581
652
  this.unsubscribe(subscriptionId);
582
653
  const collectionName = this.getCollectionName(config.path);
583
654
  const collection = this.db.collection(collectionName);
@@ -595,6 +666,7 @@ var MongoRealtimeService = class {
595
666
  config,
596
667
  changeStream,
597
668
  callback,
669
+ onError,
598
670
  started: 0,
599
671
  delivered: 0
600
672
  };
@@ -612,6 +684,7 @@ var MongoRealtimeService = class {
612
684
  type: "collection",
613
685
  config,
614
686
  callback,
687
+ onError,
615
688
  started: 0,
616
689
  delivered: 0
617
690
  };
@@ -635,7 +708,7 @@ var MongoRealtimeService = class {
635
708
  });
636
709
  if (callback && canDeliver()) callback(rows);
637
710
  } catch (error) {
638
- logger.error(`Error fetching collection for subscription ${subscriptionId}`, { error });
711
+ this.reportFetchFailure(subscriptionId, subscription, canDeliver, error);
639
712
  }
640
713
  }
641
714
  /**
@@ -648,14 +721,19 @@ var MongoRealtimeService = class {
648
721
  if (!this.driver) throw new Error("MongoRealtimeService has no data driver — subscriptions cannot be authorized");
649
722
  const user = {
650
723
  uid: authContext?.uid ?? ANONYMOUS_USER_ID,
651
- roles: authContext?.roles ?? []
724
+ roles: authContext?.roles ?? [],
725
+ isAnonymous: authContext?.isAnonymous === true,
726
+ displayName: null,
727
+ email: null,
728
+ photoURL: null,
729
+ providerId: "realtime"
652
730
  };
653
731
  return this.driver.withAuth(user);
654
732
  }
655
733
  /**
656
734
  * Subscribe to single row changes
657
735
  */
658
- subscribeToOne(subscriptionId, config, callback) {
736
+ subscribeToOne(subscriptionId, config, callback, onError) {
659
737
  this.unsubscribe(subscriptionId);
660
738
  const collectionName = this.getCollectionName(config.path);
661
739
  const collection = this.db.collection(collectionName);
@@ -675,6 +753,7 @@ var MongoRealtimeService = class {
675
753
  config,
676
754
  changeStream,
677
755
  callback,
756
+ onError,
678
757
  started: 0,
679
758
  delivered: 0
680
759
  };
@@ -695,6 +774,7 @@ var MongoRealtimeService = class {
695
774
  type: "single",
696
775
  config,
697
776
  callback,
777
+ onError,
698
778
  started: 0,
699
779
  delivered: 0
700
780
  };
@@ -718,7 +798,7 @@ var MongoRealtimeService = class {
718
798
  });
719
799
  if (callback && canDeliver()) callback(row || null);
720
800
  } catch (error) {
721
- logger.error(`Error fetching row for subscription ${subscriptionId}`, { error });
801
+ this.reportFetchFailure(subscriptionId, subscription, canDeliver, error);
722
802
  }
723
803
  }
724
804
  /**
@@ -787,7 +867,8 @@ var MongoRealtimeService = class {
787
867
  if (!ws) return;
788
868
  const authContext = _authContext ? {
789
869
  uid: _authContext.uid,
790
- roles: (_authContext.roles ?? []).map(String)
870
+ roles: (_authContext.roles ?? []).map(String),
871
+ isAnonymous: _authContext.isAnonymous === true
791
872
  } : void 0;
792
873
  switch (message.type) {
793
874
  case "subscribe_collection": {
@@ -829,6 +910,8 @@ var MongoRealtimeService = class {
829
910
  subscriptionId,
830
911
  rows
831
912
  }));
913
+ }, (error) => {
914
+ ws.send(JSON.stringify(subscriptionErrorFrame(subscriptionId, message.payload?.path, error)));
832
915
  });
833
916
  break;
834
917
  }
@@ -846,6 +929,8 @@ var MongoRealtimeService = class {
846
929
  subscriptionId,
847
930
  row
848
931
  }));
932
+ }, (error) => {
933
+ ws.send(JSON.stringify(subscriptionErrorFrame(subscriptionId, message.payload?.path, error)));
849
934
  });
850
935
  break;
851
936
  }
@@ -1239,6 +1324,18 @@ function callContext(driver, user, data, client) {
1239
1324
  };
1240
1325
  }
1241
1326
  /**
1327
+ * A listener's `onError`, in the shape the realtime service calls it with.
1328
+ *
1329
+ * Without it a failed fetch behind an in-process subscription was logged, and
1330
+ * the listener heard nothing and kept waiting for rows. The error arrives as
1331
+ * thrown, not masked the way a socket frame is: this is trusted server code,
1332
+ * so an `afterRead` refusal reaches it as the `RebaseApiError` it is.
1333
+ */
1334
+ function fetchErrorListener(onError) {
1335
+ if (!onError) return void 0;
1336
+ return (error) => onError(error instanceof Error ? error : new Error(String(error)));
1337
+ }
1338
+ /**
1242
1339
  * MongoDB DataDriver Delegate
1243
1340
  *
1244
1341
  * Implements the DataDriver interface for Rebase.
@@ -1360,7 +1457,7 @@ var MongoDriver = class {
1360
1457
  clientId: "driver",
1361
1458
  ...query,
1362
1459
  authContext
1363
- }, callback);
1460
+ }, callback, fetchErrorListener(onError));
1364
1461
  return () => {
1365
1462
  this.realtimeService.unsubscribe(subscriptionId);
1366
1463
  };
@@ -1414,7 +1511,7 @@ var MongoDriver = class {
1414
1511
  path,
1415
1512
  id,
1416
1513
  authContext
1417
- }, callback);
1514
+ }, callback, fetchErrorListener(onError));
1418
1515
  return () => {
1419
1516
  this.realtimeService.unsubscribe(subscriptionId);
1420
1517
  };
@@ -1570,9 +1667,15 @@ var MongoDriver = class {
1570
1667
  */
1571
1668
  async delete({ row, collection }) {
1572
1669
  const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
1670
+ const stored = await this.fetchOne({
1671
+ path: row.path,
1672
+ id: row.id,
1673
+ collection: resolvedCollection
1674
+ });
1675
+ if (!stored) throw ApiError.notFound(`No row "${row.id}" in "${row.path}" to delete.`);
1573
1676
  const callbackRow = {
1574
1677
  id: row.id,
1575
- ...row.values ?? {}
1678
+ ...stored
1576
1679
  };
1577
1680
  const contextForCallback = callContext(this, this.user, this.data, this.client);
1578
1681
  try {
@@ -1638,7 +1741,7 @@ var MongoDriver = class {
1638
1741
  action: "delete",
1639
1742
  id: String(row.id),
1640
1743
  tableName: row.path,
1641
- previousValues: row.values,
1744
+ previousValues: stored,
1642
1745
  updatedBy: this.user?.uid
1643
1746
  }).catch((err) => {
1644
1747
  logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
@@ -1760,11 +1863,19 @@ var AuthenticatedMongoDriver = class {
1760
1863
  listenCollection(props) {
1761
1864
  return this.delegate.listenCollection(props, this.authContext());
1762
1865
  }
1763
- /** The acting user, in the shape the realtime subscriptions carry. */
1866
+ /**
1867
+ * The acting user, in the shape the realtime subscriptions carry.
1868
+ *
1869
+ * `isAnonymous` included: a guest has a real uid, and without the flag
1870
+ * every fetch a listener's subscription makes reads as an account, so
1871
+ * `policy.registered()` handed a guest's listener what it withholds from a
1872
+ * guest's `fetchCollection` on this same driver.
1873
+ */
1764
1874
  authContext() {
1765
1875
  return {
1766
1876
  uid: this.user.uid,
1767
- roles: this.user.roles ?? []
1877
+ roles: this.user.roles ?? [],
1878
+ isAnonymous: this.user.isAnonymous === true
1768
1879
  };
1769
1880
  }
1770
1881
  /**
@@ -2963,7 +3074,7 @@ function createMongoBootstrapper(mongoConfig) {
2963
3074
  },
2964
3075
  mountRoutes() {},
2965
3076
  async initializeWebsockets(server, realtimeService, driver, config, authAdapter) {
2966
- const { createMongoWebSocket } = await import("./websocket-B3LiQfFN.js");
3077
+ const { createMongoWebSocket } = await import("./websocket-C3iT6srl.js");
2967
3078
  createMongoWebSocket(server, realtimeService, driver, config, cachedAdmin, authAdapter);
2968
3079
  }
2969
3080
  };