@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g3660bd5

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.
@@ -4,7 +4,7 @@ import { Client as PgClient } from "pg";
4
4
  import { randomUUID } from "crypto";
5
5
  import { DataService } from "./dataService";
6
6
 
7
- import { ANONYMOUS_USER_ID, FetchCollectionProps, ListenCollectionProps, ListenOneProps, DataDriver, CollectionUpdateMessage, SingleUpdateMessage, CollectionPatchMessage, WebSocketMessage, FilterValues, CollectionConfig, RebaseCallContext, resolveClientListLimit } from "@rebasepro/types";
7
+ import { ANONYMOUS_USER_ID, FetchCollectionProps, ListenCollectionProps, ListenOneProps, DataDriver, CollectionUpdateMessage, SingleUpdateMessage, CollectionPatchMessage, WebSocketMessage, FilterValues, LogicalCondition, CollectionConfig, RebaseCallContext, resolveClientListLimit } from "@rebasepro/types";
8
8
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
9
9
  import { sql as drizzleSql } from "drizzle-orm";
10
10
  import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces";
@@ -41,6 +41,28 @@ type RealTimeListenCollectionProps = ListenCollectionProps & {
41
41
  subscriptionId: string
42
42
  };
43
43
 
44
+ /**
45
+ * The narrowing a collection subscription was created with, kept so that every
46
+ * refetch answers the same query the initial fetch did.
47
+ *
48
+ * Named once because it used to be written out inline in five places, and a
49
+ * field missing from one of them is accepted over the wire and then silently
50
+ * ignored: `offset` was declared on the incoming props and never stored, so a
51
+ * live list on page three served page one, and `logical` was never stored
52
+ * either, so an `or(...)` subscription was pushed every row in the table.
53
+ */
54
+ type StoredCollectionRequest = {
55
+ filter?: Record<string, unknown>;
56
+ logical?: LogicalCondition;
57
+ orderBy?: string;
58
+ order?: "desc" | "asc";
59
+ limit?: number;
60
+ offset?: number;
61
+ startAfter?: Record<string, unknown>;
62
+ databaseId?: string;
63
+ searchString?: string;
64
+ };
65
+
44
66
  type RealTimeListenEntityProps = ListenOneProps & { subscriptionId: string };
45
67
 
46
68
  /**
@@ -119,16 +141,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
119
141
  path: string;
120
142
  id?: string | number;
121
143
  // Store full collection request parameters for proper refetching
122
- collectionRequest?: {
123
- filter?: Record<string, unknown>;
124
- orderBy?: string;
125
- order?: "desc" | "asc";
126
- limit?: number;
127
- offset?: number;
128
- startAfter?: Record<string, unknown>;
129
- databaseId?: string;
130
- searchString?: string;
131
- };
144
+ collectionRequest?: StoredCollectionRequest;
132
145
  // Auth context for RLS — when set, refetches run in a transaction
133
146
  // with set_config('app.uid', ...) / set_config('app.user_roles', ...)
134
147
  authContext?: SubscriptionAuthContext;
@@ -212,16 +225,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
212
225
  type: "collection" | "single";
213
226
  path: string;
214
227
  id?: string | number;
215
- collectionRequest?: {
216
- filter?: Record<string, unknown>;
217
- orderBy?: string;
218
- order?: "desc" | "asc";
219
- limit?: number;
220
- offset?: number;
221
- startAfter?: Record<string, unknown>;
222
- databaseId?: string;
223
- searchString?: string;
224
- };
228
+ collectionRequest?: StoredCollectionRequest;
225
229
  authContext?: SubscriptionAuthContext;
226
230
  }) {
227
231
  this.debugLog("📋 [RealtimeService] Registering DataDriver subscription:", subscriptionId, subscription.authContext ? "(with auth)" : "(no auth)");
@@ -448,9 +452,11 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
448
452
  path: request.path,
449
453
  collectionRequest: {
450
454
  filter: request.filter,
455
+ logical: request.logical,
451
456
  orderBy: request.orderBy,
452
457
  order: request.order,
453
458
  limit: boundedLimit,
459
+ offset: request.offset,
454
460
  startAfter: request.startAfter as Record<string, unknown> | undefined,
455
461
  databaseId: request.collection?.databaseId,
456
462
  searchString: request.searchString
@@ -458,17 +464,12 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
458
464
  authContext
459
465
  });
460
466
 
461
- // Send initial data
467
+ // Send initial data. Built from the request the subscription just
468
+ // stored, so the first answer and every refetch after it cannot
469
+ // describe different queries.
462
470
  const rows = await this.fetchCollectionWithAuth(
463
471
  request.path,
464
- {
465
- filter: request.filter,
466
- orderBy: request.orderBy,
467
- order: request.order,
468
- limit: boundedLimit,
469
- startAfter: request.startAfter as Record<string, unknown> | undefined,
470
- searchString: request.searchString
471
- },
472
+ this._subscriptions.get(subscriptionId)!.collectionRequest!,
472
473
  authContext
473
474
  );
474
475
 
@@ -679,7 +680,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
679
680
  private debouncedCollectionRefetch(
680
681
  subscriptionId: string,
681
682
  notifyPath: string,
682
- subscription: { clientId: string; collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext }
683
+ subscription: { clientId: string; collectionRequest?: StoredCollectionRequest; authContext?: SubscriptionAuthContext }
683
684
  ) {
684
685
  const timerKey = `ws_${subscriptionId}`;
685
686
  const existing = this.refetchTimers.get(timerKey);
@@ -705,7 +706,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
705
706
  private debouncedDriverRefetch(
706
707
  subscriptionId: string,
707
708
  notifyPath: string,
708
- subscription: { collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext },
709
+ subscription: { collectionRequest?: StoredCollectionRequest; authContext?: SubscriptionAuthContext },
709
710
  callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void
710
711
  ) {
711
712
  const timerKey = `drv_${subscriptionId}`;
@@ -731,7 +732,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
731
732
  */
732
733
  private async fetchCollectionWithAuth(
733
734
  notifyPath: string,
734
- collectionRequest: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string },
735
+ collectionRequest: StoredCollectionRequest,
735
736
  authContext?: SubscriptionAuthContext
736
737
  ): Promise<Record<string, unknown>[]> {
737
738
  if (this.driver) {
@@ -740,6 +741,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
740
741
  path: notifyPath,
741
742
  collection: collection,
742
743
  filter: collectionRequest.filter as FetchCollectionProps["filter"],
744
+ logical: collectionRequest.logical,
743
745
  orderBy: collectionRequest.orderBy,
744
746
  order: collectionRequest.order,
745
747
  limit: collectionRequest.limit,
@@ -772,6 +774,7 @@ roles: ["anon"] };
772
774
  } else {
773
775
  fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
774
776
  filter: collectionRequest.filter as FilterValues<string>,
777
+ logical: collectionRequest.logical,
775
778
  orderBy: collectionRequest.orderBy,
776
779
  order: collectionRequest.order,
777
780
  limit: collectionRequest.limit,