@rebasepro/server-mongo 0.21.1-canary.g8c5a265 → 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/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
  };
@@ -1766,11 +1863,19 @@ var AuthenticatedMongoDriver = class {
1766
1863
  listenCollection(props) {
1767
1864
  return this.delegate.listenCollection(props, this.authContext());
1768
1865
  }
1769
- /** 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
+ */
1770
1874
  authContext() {
1771
1875
  return {
1772
1876
  uid: this.user.uid,
1773
- roles: this.user.roles ?? []
1877
+ roles: this.user.roles ?? [],
1878
+ isAnonymous: this.user.isAnonymous === true
1774
1879
  };
1775
1880
  }
1776
1881
  /**
@@ -2969,7 +3074,7 @@ function createMongoBootstrapper(mongoConfig) {
2969
3074
  },
2970
3075
  mountRoutes() {},
2971
3076
  async initializeWebsockets(server, realtimeService, driver, config, authAdapter) {
2972
- const { createMongoWebSocket } = await import("./websocket-Cc1T8ETt.js");
3077
+ const { createMongoWebSocket } = await import("./websocket-C3iT6srl.js");
2973
3078
  createMongoWebSocket(server, realtimeService, driver, config, cachedAdmin, authAdapter);
2974
3079
  }
2975
3080
  };