@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (136) hide show
  1. package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
  2. package/dist/common/src/util/entities.d.ts +22 -0
  3. package/dist/common/src/util/relations.d.ts +14 -4
  4. package/dist/common/src/util/resolutions.d.ts +1 -1
  5. package/dist/index.es.js +1254 -591
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +1254 -591
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
  10. package/dist/server-postgresql/src/auth/services.d.ts +7 -3
  11. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
  12. package/dist/server-postgresql/src/connection.d.ts +34 -1
  13. package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
  14. package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
  15. package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
  16. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  17. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  18. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
  19. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  20. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
  21. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
  22. package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
  23. package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
  24. package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
  25. package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
  26. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
  27. package/dist/types/src/controllers/auth.d.ts +2 -0
  28. package/dist/types/src/controllers/client.d.ts +119 -7
  29. package/dist/types/src/controllers/collection_registry.d.ts +4 -3
  30. package/dist/types/src/controllers/customization_controller.d.ts +7 -1
  31. package/dist/types/src/controllers/data.d.ts +34 -7
  32. package/dist/types/src/controllers/data_driver.d.ts +20 -28
  33. package/dist/types/src/controllers/database_admin.d.ts +2 -2
  34. package/dist/types/src/controllers/email.d.ts +34 -0
  35. package/dist/types/src/controllers/index.d.ts +1 -0
  36. package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
  37. package/dist/types/src/controllers/navigation.d.ts +5 -5
  38. package/dist/types/src/controllers/registry.d.ts +6 -3
  39. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
  40. package/dist/types/src/controllers/storage.d.ts +24 -26
  41. package/dist/types/src/rebase_context.d.ts +8 -4
  42. package/dist/types/src/types/backend.d.ts +4 -1
  43. package/dist/types/src/types/builders.d.ts +5 -4
  44. package/dist/types/src/types/chips.d.ts +1 -1
  45. package/dist/types/src/types/collections.d.ts +169 -125
  46. package/dist/types/src/types/cron.d.ts +102 -0
  47. package/dist/types/src/types/data_source.d.ts +1 -1
  48. package/dist/types/src/types/entity_actions.d.ts +8 -8
  49. package/dist/types/src/types/entity_callbacks.d.ts +15 -15
  50. package/dist/types/src/types/entity_link_builder.d.ts +1 -1
  51. package/dist/types/src/types/entity_overrides.d.ts +2 -1
  52. package/dist/types/src/types/entity_views.d.ts +8 -8
  53. package/dist/types/src/types/export_import.d.ts +3 -3
  54. package/dist/types/src/types/index.d.ts +1 -0
  55. package/dist/types/src/types/plugins.d.ts +72 -18
  56. package/dist/types/src/types/properties.d.ts +118 -33
  57. package/dist/types/src/types/relations.d.ts +1 -1
  58. package/dist/types/src/types/slots.d.ts +30 -6
  59. package/dist/types/src/types/translations.d.ts +44 -0
  60. package/dist/types/src/types/user_management_delegate.d.ts +1 -0
  61. package/drizzle-test/0000_woozy_junta.sql +6 -0
  62. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  63. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  64. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  65. package/drizzle-test/meta/0000_snapshot.json +47 -0
  66. package/drizzle-test/meta/0001_snapshot.json +48 -0
  67. package/drizzle-test/meta/0002_snapshot.json +38 -0
  68. package/drizzle-test/meta/0003_snapshot.json +48 -0
  69. package/drizzle-test/meta/_journal.json +34 -0
  70. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  71. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  72. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  73. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  74. package/drizzle-test-out/meta/_journal.json +20 -0
  75. package/drizzle.test.config.ts +10 -0
  76. package/package.json +88 -89
  77. package/scratch.ts +41 -0
  78. package/src/PostgresBackendDriver.ts +63 -79
  79. package/src/PostgresBootstrapper.ts +7 -8
  80. package/src/auth/ensure-tables.ts +158 -86
  81. package/src/auth/services.ts +109 -50
  82. package/src/cli.ts +259 -16
  83. package/src/collections/PostgresCollectionRegistry.ts +6 -6
  84. package/src/connection.ts +70 -48
  85. package/src/data-transformer.ts +155 -116
  86. package/src/databasePoolManager.ts +6 -5
  87. package/src/history/HistoryService.ts +3 -12
  88. package/src/interfaces.ts +3 -3
  89. package/src/schema/auth-schema.ts +26 -3
  90. package/src/schema/doctor-cli.ts +47 -0
  91. package/src/schema/doctor.ts +595 -0
  92. package/src/schema/generate-drizzle-schema-logic.ts +204 -57
  93. package/src/schema/generate-drizzle-schema.ts +6 -6
  94. package/src/schema/test-schema.ts +11 -0
  95. package/src/services/BranchService.ts +5 -5
  96. package/src/services/EntityFetchService.ts +317 -188
  97. package/src/services/EntityPersistService.ts +15 -17
  98. package/src/services/RelationService.ts +299 -37
  99. package/src/services/entity-helpers.ts +39 -13
  100. package/src/services/entityService.ts +11 -9
  101. package/src/services/realtimeService.ts +58 -29
  102. package/src/utils/drizzle-conditions.ts +25 -24
  103. package/src/websocket.ts +52 -21
  104. package/test/auth-services.test.ts +131 -39
  105. package/test/batch-many-to-many-regression.test.ts +573 -0
  106. package/test/branchService.test.ts +22 -12
  107. package/test/data-transformer-hardening.test.ts +417 -0
  108. package/test/data-transformer.test.ts +175 -0
  109. package/test/doctor.test.ts +182 -0
  110. package/test/entityService.errors.test.ts +31 -16
  111. package/test/entityService.relations.test.ts +155 -59
  112. package/test/entityService.subcollection-search.test.ts +107 -57
  113. package/test/entityService.test.ts +105 -47
  114. package/test/generate-drizzle-schema.test.ts +262 -69
  115. package/test/historyService.test.ts +31 -16
  116. package/test/n-plus-one-regression.test.ts +314 -0
  117. package/test/postgresDataDriver.test.ts +260 -168
  118. package/test/realtimeService.test.ts +70 -39
  119. package/test/relation-pipeline-gaps.test.ts +637 -0
  120. package/test/relations.test.ts +492 -39
  121. package/test-drizzle-bug.ts +18 -0
  122. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  123. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  124. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  125. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  126. package/test-drizzle-out/meta/_journal.json +20 -0
  127. package/test-drizzle-prompt.sh +2 -0
  128. package/test-policy-prompt.sh +3 -0
  129. package/test-programmatic.ts +30 -0
  130. package/test-programmatic2.ts +59 -0
  131. package/test-schema-no-policies.ts +12 -0
  132. package/test_drizzle_mock.js +2 -2
  133. package/test_find_changed.mjs +3 -1
  134. package/test_hash.js +14 -0
  135. package/tsconfig.json +1 -1
  136. package/vite.config.ts +5 -5
@@ -11,10 +11,31 @@ import { getTableName } from "@rebasepro/common";
11
11
  * `PostgresCollectionRegistry` instance — there is no global singleton.
12
12
  */
13
13
 
14
+ /**
15
+ * Interface for Drizzle column metadata introspection.
16
+ * Replaces unsafe `as unknown as Record<string, unknown>` double-cast chains.
17
+ */
18
+ export interface DrizzleColumnMeta {
19
+ columnType?: string;
20
+ dataType?: string;
21
+ primary?: boolean;
22
+ }
23
+
24
+ /** Safely extract Drizzle column metadata from a column object. */
25
+ export function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta {
26
+ const raw = col as unknown as Record<string | symbol, unknown>;
27
+ return {
28
+ columnType: typeof raw.columnType === "string" ? raw.columnType : undefined,
29
+ dataType: typeof raw.dataType === "string" ? raw.dataType : undefined,
30
+ primary: typeof raw.primary === "boolean" ? raw.primary : undefined
31
+ };
32
+ }
33
+
14
34
  export function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): EntityCollection {
15
35
  const collection = registry.getCollectionByPath(collectionPath);
16
36
  if (!collection) {
17
- throw new Error(`Collection not found: ${collectionPath}`);
37
+ const registered = registry.getCollections().map(c => c.slug).join(", ");
38
+ throw new Error(`Collection not found: ${collectionPath}. Registered collections: [${registered}]`);
18
39
  }
19
40
  return collection;
20
41
  }
@@ -28,16 +49,17 @@ export function getTableForCollection(collection: EntityCollection, registry: Po
28
49
  return table;
29
50
  }
30
51
 
31
- export function getPrimaryKeys(collection: EntityCollection, registry: PostgresCollectionRegistry): { fieldName: string; type: "string" | "number" }[] {
52
+ export function getPrimaryKeys(collection: EntityCollection, registry: PostgresCollectionRegistry): { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] {
32
53
  const table = getTableForCollection(collection, registry);
33
54
 
34
55
  // Fallback to explicitly defined isId properties
35
56
  if (collection.properties) {
36
57
  const idProps = Object.entries(collection.properties)
37
- .filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as unknown as Record<string, unknown>).isId))
58
+ .filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as { isId?: unknown }).isId))
38
59
  .map(([key, prop]) => ({
39
60
  fieldName: key,
40
- type: prop.type === "number" ? "number" as const : "string" as const
61
+ type: prop.type === "number" ? "number" as const : "string" as const,
62
+ isUUID: (prop as { isId?: unknown }).isId === "uuid"
41
63
  }));
42
64
 
43
65
  if (idProps.length > 0) {
@@ -46,12 +68,14 @@ export function getPrimaryKeys(collection: EntityCollection, registry: PostgresC
46
68
  }
47
69
 
48
70
  // Otherwise infer from Drizzle schema
49
- const keys: { fieldName: string; type: "string" | "number" }[] = [];
71
+ const keys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] = [];
50
72
  for (const [key, colRaw] of Object.entries(table)) {
51
73
  const col = colRaw as AnyPgColumn;
52
74
  if (col && typeof col === "object" && "primary" in col && col.primary) {
53
- const type = col.dataType === "number" || (col as unknown as Record<string, unknown>).columnType === "PgSerial" || (col as unknown as Record<string, unknown>).columnType === "PgInteger" ? "number" : "string";
54
- keys.push({ fieldName: key, type });
75
+ const meta = getColumnMeta(col);
76
+ const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
77
+ const isUUID = meta.columnType === "PgUUID";
78
+ keys.push({ fieldName: key, type, isUUID });
55
79
  }
56
80
  }
57
81
 
@@ -59,14 +83,16 @@ export function getPrimaryKeys(collection: EntityCollection, registry: PostgresC
59
83
  // This maintains backwards compatibility
60
84
  if (keys.length === 0 && "id" in table) {
61
85
  const idCol = table["id" as keyof typeof table] as AnyPgColumn;
62
- const type = idCol.dataType === "number" || (idCol as unknown as Record<string, unknown>).columnType === "PgSerial" || (idCol as unknown as Record<string, unknown>).columnType === "PgInteger" ? "number" : "string";
63
- keys.push({ fieldName: "id", type });
86
+ const idMeta = getColumnMeta(idCol);
87
+ const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
88
+ const isUUID = idMeta.columnType === "PgUUID";
89
+ keys.push({ fieldName: "id", type, isUUID });
64
90
  }
65
91
 
66
92
  return keys;
67
93
  }
68
94
 
69
- export function parseIdValues(idValue: string | number, primaryKeys: { fieldName: string; type: "string" | "number" }[]): Record<string, string | number> {
95
+ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[]): Record<string, string | number> {
70
96
  const result: Record<string, string | number> = {};
71
97
 
72
98
  if (primaryKeys.length === 0) {
@@ -75,7 +101,7 @@ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName
75
101
 
76
102
  if (primaryKeys.length === 1) {
77
103
  const pk = primaryKeys[0];
78
- if (pk.type === "number") {
104
+ if (pk.type === "number" && !pk.isUUID) {
79
105
  const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
80
106
  if (isNaN(parsed)) {
81
107
  throw new Error(`Invalid numeric ID: ${idValue}`);
@@ -96,7 +122,7 @@ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName
96
122
  for (let i = 0; i < primaryKeys.length; i++) {
97
123
  const pk = primaryKeys[i];
98
124
  const val = parts[i];
99
- if (pk.type === "number") {
125
+ if (pk.type === "number" && !pk.isUUID) {
100
126
  const parsed = parseInt(val, 10);
101
127
  if (isNaN(parsed)) {
102
128
  throw new Error(`Invalid numeric ID component: ${val}`);
@@ -110,7 +136,7 @@ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName
110
136
  return result;
111
137
  }
112
138
 
113
- export function buildCompositeId(values: Record<string, any>, primaryKeys: { fieldName: string; type: "string" | "number" }[]): string {
139
+ export function buildCompositeId(values: Record<string, unknown>, primaryKeys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[]): string {
114
140
  if (primaryKeys.length === 0) {
115
141
  return "";
116
142
  }
@@ -19,13 +19,13 @@ export * from "../interfaces";
19
19
 
20
20
  /**
21
21
  * EntityService - Facade for entity operations.
22
- *
22
+ *
23
23
  * This class provides a unified API for entity CRUD operations by delegating
24
24
  * to specialized services:
25
25
  * - EntityFetchService: Read operations (fetch, search, count)
26
26
  * - EntityPersistService: Write operations (save, delete)
27
27
  * - RelationService: Relation operations (fetch related, update relations)
28
- *
28
+ *
29
29
  * Implements the EntityRepository interface for database abstraction.
30
30
  */
31
31
  export class EntityService implements EntityRepository {
@@ -44,7 +44,7 @@ export class EntityService implements EntityRepository {
44
44
  /**
45
45
  * Fetch a single entity by ID
46
46
  */
47
- async fetchEntity<M extends Record<string, any>>(
47
+ async fetchEntity<M extends Record<string, unknown>>(
48
48
  collectionPath: string,
49
49
  entityId: string | number,
50
50
  databaseId?: string
@@ -55,13 +55,14 @@ export class EntityService implements EntityRepository {
55
55
  /**
56
56
  * Fetch a collection of entities with optional filtering, ordering, and pagination
57
57
  */
58
- async fetchCollection<M extends Record<string, any>>(
58
+ async fetchCollection<M extends Record<string, unknown>>(
59
59
  collectionPath: string,
60
60
  options: {
61
61
  filter?: FilterValues<Extract<keyof M, string>>;
62
62
  orderBy?: string;
63
63
  order?: "desc" | "asc";
64
64
  limit?: number;
65
+ offset?: number;
65
66
  startAfter?: Record<string, unknown>;
66
67
  searchString?: string;
67
68
  databaseId?: string;
@@ -73,7 +74,7 @@ export class EntityService implements EntityRepository {
73
74
  /**
74
75
  * Search entities by text
75
76
  */
76
- async searchEntities<M extends Record<string, any>>(
77
+ async searchEntities<M extends Record<string, unknown>>(
77
78
  collectionPath: string,
78
79
  searchString: string,
79
80
  options: {
@@ -90,10 +91,11 @@ export class EntityService implements EntityRepository {
90
91
  /**
91
92
  * Count entities in a collection
92
93
  */
93
- async countEntities<M extends Record<string, any>>(
94
+ async countEntities<M extends Record<string, unknown>>(
94
95
  collectionPath: string,
95
96
  options: {
96
97
  filter?: FilterValues<Extract<keyof M, string>>;
98
+ searchString?: string;
97
99
  databaseId?: string;
98
100
  } = {}
99
101
  ): Promise<number> {
@@ -116,7 +118,7 @@ export class EntityService implements EntityRepository {
116
118
  /**
117
119
  * Fetch entities related to a parent entity
118
120
  */
119
- async fetchRelatedEntities<M extends Record<string, any>>(
121
+ async fetchRelatedEntities<M extends Record<string, unknown>>(
120
122
  parentCollectionPath: string,
121
123
  parentEntityId: string | number,
122
124
  relationKey: string,
@@ -145,7 +147,7 @@ export class EntityService implements EntityRepository {
145
147
  /**
146
148
  * Save an entity (create or update)
147
149
  */
148
- async saveEntity<M extends Record<string, any>>(
150
+ async saveEntity<M extends Record<string, unknown>>(
149
151
  collectionPath: string,
150
152
  values: Partial<M>,
151
153
  entityId?: string | number,
@@ -177,7 +179,7 @@ export class EntityService implements EntityRepository {
177
179
  const result = await this.db.execute(sql.raw(sqlText));
178
180
  const rows = result.rows;
179
181
  if (process.env.NODE_ENV !== "production") {
180
- console.debug(`SQL executed successfully. Returned ${Array.isArray(rows) ? rows.length : 'non-array'} rows.`);
182
+ console.debug(`SQL executed successfully. Returned ${Array.isArray(rows) ? rows.length : "non-array"} rows.`);
181
183
  }
182
184
  return rows as Record<string, unknown>[];
183
185
  }
@@ -4,7 +4,7 @@ import { Client as PgClient } from "pg";
4
4
  import { randomUUID } from "crypto";
5
5
  import { EntityService } from "./entityService";
6
6
 
7
- import { Entity, FetchCollectionProps, ListenCollectionProps, ListenEntityProps, DataDriver, CollectionUpdateMessage, EntityUpdateMessage, CollectionEntityPatchMessage, WebSocketMessage } from "@rebasepro/types";
7
+ import { Entity, FetchCollectionProps, ListenCollectionProps, ListenEntityProps, DataDriver, CollectionUpdateMessage, EntityUpdateMessage, CollectionEntityPatchMessage, WebSocketMessage, FilterValues, EntityCollection, RebaseCallContext } 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, EntitySubscriptionConfig } from "../interfaces";
@@ -32,7 +32,7 @@ type RealTimeListenEntityProps = ListenEntityProps & { subscriptionId: string };
32
32
  /**
33
33
  * PostgreSQL-specific realtime service.
34
34
  * Handles WebSocket connections and subscriptions for real-time entity updates.
35
- *
35
+ *
36
36
  * Implements the RealtimeProvider interface for database abstraction.
37
37
  */
38
38
  export class RealtimeService extends EventEmitter implements RealtimeProvider {
@@ -50,6 +50,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
50
50
  orderBy?: string;
51
51
  order?: "desc" | "asc";
52
52
  limit?: number;
53
+ offset?: number;
53
54
  startAfter?: Record<string, unknown>;
54
55
  databaseId?: string;
55
56
  searchString?: string;
@@ -111,6 +112,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
111
112
  orderBy?: string;
112
113
  order?: "desc" | "asc";
113
114
  limit?: number;
115
+ offset?: number;
114
116
  startAfter?: Record<string, unknown>;
115
117
  databaseId?: string;
116
118
  searchString?: string;
@@ -253,6 +255,16 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
253
255
  const subscriptionId = request.subscriptionId;
254
256
 
255
257
  try {
258
+ // Early validation: ensure the requested collection exists in the registry
259
+ const collection = this.registry.getCollectionByPath(request.path);
260
+ if (!collection) {
261
+ const registered = this.registry.getCollections().map(c => c.slug).join(", ");
262
+ const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
263
+ console.error(`[RealtimeService] ${msg}`);
264
+ this.sendError(clientId, msg, subscriptionId);
265
+ return;
266
+ }
267
+
256
268
  // Store subscription with full request parameters and auth context for RLS
257
269
  this._subscriptions.set(subscriptionId, {
258
270
  clientId,
@@ -273,7 +285,6 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
273
285
  // Send initial data
274
286
  let entities;
275
287
  if (this.driver) {
276
- const collection = this.registry.getCollectionByPath(request.path);
277
288
  entities = await this.driver.fetchCollection({
278
289
  path: request.path,
279
290
  collection: collection,
@@ -307,6 +318,16 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
307
318
  const subscriptionId = request.subscriptionId;
308
319
 
309
320
  try {
321
+ // Early validation: ensure the requested collection exists in the registry
322
+ const collection = this.registry.getCollectionByPath(request.path);
323
+ if (!collection) {
324
+ const registered = this.registry.getCollections().map(c => c.slug).join(", ");
325
+ const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
326
+ console.error(`[RealtimeService] ${msg}`);
327
+ this.sendError(clientId, msg, subscriptionId);
328
+ return;
329
+ }
330
+
310
331
  // Store subscription in memory with auth context for RLS
311
332
  this._subscriptions.set(subscriptionId, {
312
333
  clientId,
@@ -319,7 +340,6 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
319
340
  // Send initial data
320
341
  let entity;
321
342
  if (this.driver) {
322
- const collection = this.registry.getCollectionByPath(request.path);
323
343
  entity = await this.driver.fetchEntity({
324
344
  path: request.path,
325
345
  entityId: request.entityId,
@@ -424,7 +444,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
424
444
  try {
425
445
  if (subscription.type === "entity" && notifyPath === originalPath) {
426
446
  // Send entity update directly (only for exact path matches)
427
- if (entity && (entity as any).values?._rebase_invalidated) {
447
+ if (entity && (entity as unknown as Record<string, unknown>).values && ((entity as unknown as Record<string, unknown>).values as Record<string, unknown>)?._rebase_invalidated) {
428
448
  this.debouncedEntityRefetch(subscriptionId, notifyPath, entityId, subscription);
429
449
  } else {
430
450
  this.sendEntityUpdate(subscription.clientId, subscriptionId, entity);
@@ -432,7 +452,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
432
452
  } else if (subscription.type === "collection" && subscription.collectionRequest) {
433
453
  // Phase 1: Send instant entity-level patch (no DB query)
434
454
  // This gives immediate cross-tab feedback
435
- if (!entity || !(entity as any).values?._rebase_invalidated) {
455
+ if (!entity || !((entity as unknown as Record<string, unknown>).values && ((entity as unknown as Record<string, unknown>).values as Record<string, unknown>)?._rebase_invalidated)) {
436
456
  this.sendCollectionEntityPatch(subscription.clientId, subscriptionId, entityId, entity);
437
457
  }
438
458
 
@@ -453,7 +473,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
453
473
  if (!callback) continue;
454
474
 
455
475
  if (subscription.type === "entity" && notifyPath === originalPath) {
456
- if (entity && (entity as any).values?._rebase_invalidated) {
476
+ if (entity && (entity as unknown as Record<string, unknown>).values && ((entity as unknown as Record<string, unknown>).values as Record<string, unknown>)?._rebase_invalidated) {
457
477
  this.debouncedEntityDriverRefetch(subscriptionId, notifyPath, entityId, subscription, callback);
458
478
  } else {
459
479
  // Call the callback directly with the entity (only for exact path matches)
@@ -476,7 +496,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
476
496
  private debouncedCollectionRefetch(
477
497
  subscriptionId: string,
478
498
  notifyPath: string,
479
- subscription: { clientId: string; collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext }
499
+ 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 }
480
500
  ) {
481
501
  const timerKey = `ws_${subscriptionId}`;
482
502
  const existing = this.refetchTimers.get(timerKey);
@@ -502,7 +522,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
502
522
  private debouncedDriverRefetch(
503
523
  subscriptionId: string,
504
524
  notifyPath: string,
505
- subscription: { collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext },
525
+ 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 },
506
526
  callback: (data: Entity[] | Entity | null) => void
507
527
  ) {
508
528
  const timerKey = `drv_${subscriptionId}`;
@@ -528,7 +548,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
528
548
  */
529
549
  private async fetchCollectionWithAuth(
530
550
  notifyPath: string,
531
- collectionRequest: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string },
551
+ collectionRequest: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string },
532
552
  authContext?: SubscriptionAuthContext
533
553
  ): Promise<Entity[]> {
534
554
  if (this.driver) {
@@ -540,6 +560,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
540
560
  orderBy: collectionRequest.orderBy,
541
561
  order: collectionRequest.order,
542
562
  limit: collectionRequest.limit,
563
+ offset: collectionRequest.offset,
543
564
  startAfter: collectionRequest.startAfter,
544
565
  searchString: collectionRequest.searchString
545
566
  });
@@ -549,7 +570,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
549
570
  return await this.db.transaction(async (tx) => {
550
571
  await tx.execute(drizzleSql`SELECT set_config('app.user_id', ${authContext.userId}, true)`);
551
572
  await tx.execute(drizzleSql`SELECT set_config('app.user_roles', ${authContext.roles.join(",")}, true)`);
552
- await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId, roles: authContext.roles })}, true)`);
573
+ await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId,
574
+ roles: authContext.roles })}, true)`);
553
575
  const txEntityService = new EntityService(tx, this.registry);
554
576
  let fetchedEntities;
555
577
  if (collectionRequest.searchString) {
@@ -557,7 +579,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
557
579
  notifyPath,
558
580
  collectionRequest.searchString,
559
581
  {
560
- filter: collectionRequest.filter as import("@rebasepro/types").FilterValues<string>,
582
+ filter: collectionRequest.filter as FilterValues<string>,
561
583
  orderBy: collectionRequest.orderBy,
562
584
  order: collectionRequest.order,
563
585
  limit: collectionRequest.limit,
@@ -566,10 +588,11 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
566
588
  );
567
589
  } else {
568
590
  fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
569
- filter: collectionRequest.filter as import("@rebasepro/types").FilterValues<string>,
591
+ filter: collectionRequest.filter as FilterValues<string>,
570
592
  orderBy: collectionRequest.orderBy,
571
593
  order: collectionRequest.order,
572
594
  limit: collectionRequest.limit,
595
+ offset: collectionRequest.offset,
573
596
  startAfter: collectionRequest.startAfter,
574
597
  databaseId: collectionRequest.databaseId
575
598
  });
@@ -578,18 +601,20 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
578
601
  // Re-apply `afterRead` lifecycle hooks to ensure consistent data structures
579
602
  // between the initial driver fetch and this RLS-bound refetch.
580
603
  const registryCollection = this.registry.getCollectionByPath(notifyPath);
581
- const resolvedCollection = collection ? { ...collection, ...registryCollection } as import("@rebasepro/types").EntityCollection : registryCollection as import("@rebasepro/types").EntityCollection;
582
-
604
+ const resolvedCollection = collection ? { ...collection,
605
+ ...registryCollection } as EntityCollection : registryCollection as EntityCollection;
606
+
583
607
  const callbacks = resolvedCollection?.callbacks;
584
608
  const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;
585
609
 
586
610
  if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
587
611
  const contextForCallback = {
588
- user: { uid: authContext.userId, roles: authContext.roles },
612
+ user: { uid: authContext.userId,
613
+ roles: authContext.roles },
589
614
  driver: this.driver,
590
- data: this.driver ? (this.driver as any).data : undefined
591
- } as any;
592
-
615
+ data: this.driver ? (this.driver as unknown as Record<string, unknown>).data : undefined
616
+ } as unknown as RebaseCallContext;
617
+
593
618
  return await Promise.all(fetchedEntities.map(async (entity) => {
594
619
  let processedEntity = entity;
595
620
  if (callbacks?.afterRead) {
@@ -625,7 +650,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
625
650
  notifyPath,
626
651
  collectionRequest.searchString,
627
652
  {
628
- filter: collectionRequest.filter as import("@rebasepro/types").FilterValues<string>,
653
+ filter: collectionRequest.filter as FilterValues<string>,
629
654
  orderBy: collectionRequest.orderBy,
630
655
  order: collectionRequest.order,
631
656
  limit: collectionRequest.limit,
@@ -634,10 +659,11 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
634
659
  );
635
660
  }
636
661
  return await this.entityService.fetchCollection(notifyPath, {
637
- filter: collectionRequest.filter as import("@rebasepro/types").FilterValues<string>,
662
+ filter: collectionRequest.filter as FilterValues<string>,
638
663
  orderBy: collectionRequest.orderBy,
639
664
  order: collectionRequest.order,
640
665
  limit: collectionRequest.limit,
666
+ offset: collectionRequest.offset,
641
667
  startAfter: collectionRequest.startAfter,
642
668
  databaseId: collectionRequest.databaseId
643
669
  });
@@ -677,7 +703,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
677
703
  notifyPath: string,
678
704
  entityId: string,
679
705
  subscription: { clientId: string; authContext?: SubscriptionAuthContext },
680
- callback: (data: any) => void
706
+ callback: (data: Entity[] | Entity | null) => void
681
707
  ) {
682
708
  const timerKey = `drve_${subscriptionId}`;
683
709
  const existing = this.refetchTimers.get(timerKey);
@@ -716,24 +742,27 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
716
742
  return await this.db.transaction(async (tx) => {
717
743
  await tx.execute(drizzleSql`SELECT set_config('app.user_id', ${authContext.userId}, true)`);
718
744
  await tx.execute(drizzleSql`SELECT set_config('app.user_roles', ${authContext.roles.join(",")}, true)`);
719
- await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId, roles: authContext.roles })}, true)`);
745
+ await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId,
746
+ roles: authContext.roles })}, true)`);
720
747
  const txEntityService = new EntityService(tx, this.registry);
721
748
  let processedEntity = await txEntityService.fetchEntity(notifyPath, entityId, collection?.databaseId);
722
749
 
723
750
  if (processedEntity) {
724
751
  const registryCollection = this.registry.getCollectionByPath(notifyPath);
725
- const resolvedCollection = collection ? { ...collection, ...registryCollection } as import("@rebasepro/types").EntityCollection : registryCollection as import("@rebasepro/types").EntityCollection;
726
-
752
+ const resolvedCollection = collection ? { ...collection,
753
+ ...registryCollection } as EntityCollection : registryCollection as EntityCollection;
754
+
727
755
  const callbacks = resolvedCollection?.callbacks;
728
756
  const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;
729
757
 
730
758
  if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
731
759
  const contextForCallback = {
732
- user: { uid: authContext.userId, roles: authContext.roles },
760
+ user: { uid: authContext.userId,
761
+ roles: authContext.roles },
733
762
  driver: this.driver,
734
- data: this.driver ? (this.driver as any).data : undefined
735
- } as any;
736
-
763
+ data: this.driver ? (this.driver as unknown as Record<string, unknown>).data : undefined
764
+ } as unknown as RebaseCallContext;
765
+
737
766
  if (callbacks?.afterRead) {
738
767
  processedEntity = await callbacks.afterRead({
739
768
  collection: resolvedCollection,