@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

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 (196) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +58 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +22 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +192 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,1008 @@
1
+ // import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { EntityService } from "./services/entityService";
3
+ import { BranchService } from "./services/BranchService";
4
+ import { RealtimeService } from "./services/realtimeService";
5
+ import { DatabasePoolManager } from "./databasePoolManager";
6
+ import { DrizzleClient } from "./interfaces";
7
+ import { User } from "@rebasepro/types";
8
+ import { sql as drizzleSql } from "drizzle-orm";
9
+ import { buildPropertyCallbacks } from "@rebasepro/common";
10
+ import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
11
+ import {
12
+ DataDriver,
13
+ DeleteEntityProps,
14
+ Entity,
15
+ EntityCollection,
16
+ FetchCollectionProps,
17
+ FetchEntityProps,
18
+ ListenCollectionProps,
19
+ ListenEntityProps,
20
+ RebaseCallContext,
21
+ SaveEntityProps,
22
+ RebaseData,
23
+ TableMetadata,
24
+ TableColumnInfo,
25
+ TableForeignKeyInfo,
26
+ TableJunctionInfo,
27
+ TablePolicyInfo,
28
+ SQLAdmin,
29
+ SchemaAdmin,
30
+ DatabaseAdmin
31
+ } from "@rebasepro/types";
32
+ import { buildRebaseData } from "@rebasepro/common";
33
+ // @ts-ignore
34
+ import { HistoryService } from "./history/HistoryService";
35
+ import { mergeDeep } from "@rebasepro/utils";
36
+
37
+ export class PostgresBackendDriver implements DataDriver {
38
+ key = "postgres";
39
+ initialised = true;
40
+
41
+ public entityService: EntityService;
42
+ public realtimeService: RealtimeService;
43
+ public historyService?: HistoryService;
44
+ public branchService?: BranchService;
45
+ public user?: User;
46
+ public data: RebaseData;
47
+
48
+ /**
49
+ * When true, realtime notifications are deferred until after the
50
+ * wrapping transaction commits. Set by `withAuth` → `withTransaction`.
51
+ */
52
+ _deferNotifications = false;
53
+ _pendingNotifications: Array<{
54
+ path: string;
55
+ entityId: string;
56
+ entity: Entity | null;
57
+ databaseId?: string;
58
+ }> = [];
59
+
60
+ constructor(
61
+ public db: DrizzleClient,
62
+ realtimeService: RealtimeService,
63
+ public readonly registry: PostgresCollectionRegistry,
64
+ user?: User,
65
+ public poolManager?: DatabasePoolManager,
66
+ historyService?: HistoryService
67
+ ) {
68
+ this.entityService = new EntityService(db, registry);
69
+ this.realtimeService = realtimeService;
70
+ this.historyService = historyService;
71
+ this.user = user;
72
+ this.data = buildRebaseData(this);
73
+
74
+ // Initialize BranchService when adminConnectionString is configured
75
+ if (poolManager) {
76
+ this.branchService = new BranchService(db, poolManager);
77
+ }
78
+
79
+ }
80
+
81
+ /**
82
+ * Typed admin capabilities (SQLAdmin + SchemaAdmin + BranchAdmin).
83
+ * Implemented as a getter so method references are resolved at call-time,
84
+ * allowing test spies applied after construction to take effect.
85
+ */
86
+ get admin(): DatabaseAdmin {
87
+ return {
88
+ executeSql: (...args: Parameters<NonNullable<DatabaseAdmin["executeSql"]>>) => this.executeSql(...args),
89
+ fetchAvailableDatabases: () => this.fetchAvailableDatabases(),
90
+ fetchAvailableRoles: () => this.fetchAvailableRoles(),
91
+ fetchCurrentDatabase: () => this.fetchCurrentDatabase(),
92
+ fetchUnmappedTables: (...args: Parameters<NonNullable<DatabaseAdmin["fetchUnmappedTables"]>>) => this.fetchUnmappedTables(...args),
93
+ fetchTableMetadata: (...args: Parameters<NonNullable<DatabaseAdmin["fetchTableMetadata"]>>) => this.fetchTableMetadata(...args),
94
+ // Branch operations (only available when poolManager is configured)
95
+ ...(this.branchService ? {
96
+ createBranch: this.branchService.createBranch.bind(this.branchService),
97
+ deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
98
+ listBranches: this.branchService.listBranches.bind(this.branchService),
99
+ getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
100
+ } : {})
101
+ };
102
+ }
103
+
104
+
105
+ private resolveCollectionCallbacks<M extends Record<string, unknown>>(collection: EntityCollection<M> | undefined, path: string) {
106
+ if (!collection && !path) return { collection: undefined,
107
+ callbacks: undefined,
108
+ propertyCallbacks: undefined };
109
+ const registryCollection = this.registry.getCollectionByPath(path);
110
+ const resolvedCollection = registryCollection
111
+ ? { ...collection,
112
+ ...registryCollection } as EntityCollection<M>
113
+ : collection as EntityCollection<M>;
114
+
115
+ const callbacks = resolvedCollection?.callbacks;
116
+ const properties = resolvedCollection?.properties;
117
+ let propertyCallbacks;
118
+ if (properties) {
119
+ propertyCallbacks = buildPropertyCallbacks(properties);
120
+ }
121
+ return {
122
+ collection: resolvedCollection,
123
+ callbacks,
124
+ propertyCallbacks
125
+ };
126
+ }
127
+
128
+ async fetchCollection<M extends Record<string, unknown>>({
129
+ path,
130
+ collection,
131
+ filter,
132
+ limit,
133
+ offset,
134
+ startAfter,
135
+ orderBy,
136
+ searchString,
137
+ order
138
+ }: FetchCollectionProps<M>): Promise<Entity<M>[]> {
139
+
140
+ const entities = await this.entityService.fetchCollection<M>(path, {
141
+ filter,
142
+ orderBy,
143
+ order,
144
+ limit,
145
+ offset,
146
+ startAfter: startAfter as Record<string, unknown> | undefined,
147
+ databaseId: collection?.databaseId,
148
+ searchString
149
+ });
150
+
151
+ const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
152
+
153
+ if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
154
+ const contextForCallback = {
155
+ user: this.user,
156
+ driver: this,
157
+ data: this.data
158
+ } as unknown as RebaseCallContext; // Backend context
159
+ return Promise.all(entities.map(async (entity) => {
160
+ let fetched = entity;
161
+ if (callbacks?.afterRead) {
162
+ fetched = await callbacks.afterRead({
163
+ collection: resolvedCollection as EntityCollection<M>,
164
+ path,
165
+ entity: fetched,
166
+ context: contextForCallback
167
+ }) ?? fetched;
168
+ }
169
+ if (propertyCallbacks?.afterRead) {
170
+ fetched = await propertyCallbacks.afterRead({
171
+ collection: resolvedCollection as EntityCollection<M>,
172
+ path,
173
+ entity: fetched,
174
+ context: contextForCallback
175
+ }) as Entity<M> ?? fetched;
176
+ }
177
+ return fetched;
178
+ }));
179
+ }
180
+
181
+ return entities;
182
+ }
183
+
184
+ listenCollection<M extends Record<string, unknown>>({
185
+ path,
186
+ collection,
187
+ filter,
188
+ limit,
189
+ offset,
190
+ startAfter,
191
+ orderBy,
192
+ searchString,
193
+ order,
194
+ onUpdate,
195
+ onError
196
+ }: ListenCollectionProps<M>): () => void {
197
+
198
+ const subscriptionId = this.generateSubscriptionId();
199
+
200
+ // Type-adapter wrapper: RealtimeService expects a union callback signature
201
+ const callbackWrapper = (entities: Entity<M>[]) => {
202
+ onUpdate(entities);
203
+ };
204
+
205
+ // Store the subscription in RealtimeService properly using the new public method
206
+ this.realtimeService.registerDataDriverSubscription(subscriptionId, {
207
+ clientId: "driver",
208
+ type: "collection" as const,
209
+ path,
210
+ collectionRequest: {
211
+ filter,
212
+ orderBy,
213
+ order,
214
+ limit,
215
+ offset,
216
+ startAfter: startAfter as Record<string, unknown> | undefined,
217
+ databaseId: collection?.databaseId,
218
+ searchString
219
+ }
220
+ });
221
+
222
+ // Store the callback for this subscription
223
+ this.realtimeService.addSubscriptionCallback(subscriptionId, callbackWrapper as (data: Entity | Entity[] | null) => void);
224
+
225
+ // Send initial data immediately
226
+ this.fetchCollection({
227
+ path: path,
228
+ collection,
229
+ filter,
230
+ limit,
231
+ offset,
232
+ startAfter,
233
+ orderBy,
234
+ searchString,
235
+ order
236
+ }).then(entities => {
237
+ callbackWrapper(entities);
238
+ }).catch(error => {
239
+ if (onError) onError(error);
240
+ });
241
+
242
+ return () => {
243
+ this.realtimeService.removeSubscriptionCallback(subscriptionId);
244
+ this.realtimeService.subscriptions.delete(subscriptionId);
245
+ };
246
+ }
247
+
248
+ async fetchEntity<M extends Record<string, unknown>>({
249
+ path,
250
+ entityId,
251
+ databaseId,
252
+ collection
253
+ }: FetchEntityProps<M>): Promise<Entity<M> | undefined> {
254
+ let entity = await this.entityService.fetchEntity<M>(
255
+ path,
256
+ entityId,
257
+ databaseId || collection?.databaseId
258
+ );
259
+
260
+ const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
261
+
262
+ if (entity && (callbacks?.afterRead || propertyCallbacks?.afterRead)) {
263
+ const contextForCallback = {
264
+ user: this.user,
265
+ driver: this,
266
+ data: this.data
267
+ } as unknown as RebaseCallContext; // Backend context
268
+ if (callbacks?.afterRead) {
269
+ entity = await callbacks.afterRead({
270
+ collection: resolvedCollection as EntityCollection<M>,
271
+ path,
272
+ entity,
273
+ context: contextForCallback
274
+ }) ?? entity;
275
+ }
276
+ if (propertyCallbacks?.afterRead) {
277
+ entity = await propertyCallbacks.afterRead({
278
+ collection: resolvedCollection as EntityCollection<M>,
279
+ path,
280
+ entity,
281
+ context: contextForCallback
282
+ }) as Entity<M> ?? entity;
283
+ }
284
+ }
285
+
286
+ return entity;
287
+ }
288
+
289
+ listenEntity<M extends Record<string, unknown>>({
290
+ path,
291
+ entityId,
292
+ collection,
293
+ onUpdate,
294
+ onError
295
+ }: ListenEntityProps<M>): () => void {
296
+
297
+ const subscriptionId = this.generateSubscriptionId();
298
+ const callbackWrapper = (entity: Entity<M> | null) => {
299
+ if (entity)
300
+ onUpdate(entity);
301
+ };
302
+
303
+ // Register the subscription with the RealtimeService
304
+ this.realtimeService.registerDataDriverSubscription(subscriptionId, {
305
+ clientId: "driver",
306
+ type: "entity" as const,
307
+ path,
308
+ entityId
309
+ });
310
+
311
+ // Store the callback for this subscription
312
+ this.realtimeService.addSubscriptionCallback(subscriptionId, callbackWrapper as (data: Entity | Entity[] | null) => void);
313
+
314
+ // Fetch initial data
315
+ this.fetchEntity({
316
+ path,
317
+ entityId,
318
+ collection
319
+ })
320
+ .then(entity => {
321
+ if (entity) onUpdate(entity);
322
+ })
323
+ .catch(error => {
324
+ if (onError) onError(error as Error);
325
+ });
326
+
327
+ // Return the unsubscribe function
328
+ return () => {
329
+ this.realtimeService.removeSubscriptionCallback(subscriptionId);
330
+ this.realtimeService.subscriptions.delete(subscriptionId);
331
+ };
332
+ }
333
+
334
+ async saveEntity<M extends Record<string, unknown>>({
335
+ path,
336
+ entityId,
337
+ values,
338
+ collection,
339
+ status
340
+ }: SaveEntityProps<M>): Promise<Entity<M>> {
341
+
342
+ const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
343
+
344
+ let updatedValues = values;
345
+ const contextForCallback = {
346
+ user: this.user,
347
+ driver: this,
348
+ data: this.data
349
+ } as unknown as RebaseCallContext;
350
+
351
+ // Fetch previous values for callbacks AND history recording
352
+ let previousValuesForHistory: Partial<Entity<M>["values"]> | undefined;
353
+ if (status === "existing" && entityId) {
354
+ const existing = await this.entityService.fetchEntity<M>(path, entityId, resolvedCollection?.databaseId);
355
+ if (existing) {
356
+ previousValuesForHistory = existing.values as Partial<Entity<M>["values"]>;
357
+ }
358
+ }
359
+
360
+ if (callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
361
+ if (callbacks?.beforeSave) {
362
+ const result = await callbacks.beforeSave({
363
+ collection: resolvedCollection as EntityCollection<M>,
364
+ path,
365
+ entityId,
366
+ values: updatedValues,
367
+ previousValues: previousValuesForHistory,
368
+ status,
369
+ context: contextForCallback
370
+ });
371
+ if (result) updatedValues = mergeDeep(updatedValues, result);
372
+ }
373
+
374
+ if (propertyCallbacks?.beforeSave) {
375
+ const result = await propertyCallbacks.beforeSave({
376
+ collection: resolvedCollection as EntityCollection<M>,
377
+ path,
378
+ entityId,
379
+ values: updatedValues,
380
+ previousValues: previousValuesForHistory,
381
+ status,
382
+ context: contextForCallback
383
+ });
384
+ if (result) updatedValues = mergeDeep(updatedValues, result);
385
+ }
386
+
387
+ }
388
+
389
+ try {
390
+ let savedEntity = await this.entityService.saveEntity<M>(
391
+ path,
392
+ updatedValues,
393
+ entityId,
394
+ resolvedCollection?.databaseId
395
+ );
396
+
397
+ if (savedEntity && (callbacks?.afterRead || propertyCallbacks?.afterRead)) {
398
+ if (callbacks?.afterRead) {
399
+ savedEntity = await callbacks.afterRead({
400
+ collection: resolvedCollection as EntityCollection<M>,
401
+ path,
402
+ entity: savedEntity,
403
+ context: contextForCallback
404
+ }) ?? savedEntity;
405
+ }
406
+ if (propertyCallbacks?.afterRead) {
407
+ savedEntity = await propertyCallbacks.afterRead({
408
+ collection: resolvedCollection as EntityCollection<M>,
409
+ path,
410
+ entity: savedEntity,
411
+ context: contextForCallback
412
+ }) as Entity<M> ?? savedEntity;
413
+ }
414
+ }
415
+
416
+ if (callbacks?.afterSave || propertyCallbacks?.afterSave) {
417
+ if (callbacks?.afterSave) {
418
+ await callbacks.afterSave({
419
+ collection: resolvedCollection as EntityCollection<M>,
420
+ path,
421
+ entityId: savedEntity.id,
422
+ values: savedEntity.values,
423
+ previousValues: previousValuesForHistory,
424
+ status,
425
+ context: contextForCallback
426
+ });
427
+ }
428
+ if (propertyCallbacks?.afterSave) {
429
+ await propertyCallbacks.afterSave({
430
+ collection: resolvedCollection as EntityCollection<M>,
431
+ path,
432
+ entityId: savedEntity.id,
433
+ values: savedEntity.values,
434
+ previousValues: previousValuesForHistory,
435
+ status,
436
+ context: contextForCallback
437
+ });
438
+ }
439
+ }
440
+
441
+ // Record entity history (fire-and-forget, never blocks the save)
442
+ if (this.historyService && resolvedCollection?.history) {
443
+ this.historyService.recordHistory({
444
+ tableName: path,
445
+ entityId: savedEntity.id.toString(),
446
+ action: status === "new" ? "create" : "update",
447
+ values: savedEntity.values as Record<string, unknown>,
448
+ previousValues: previousValuesForHistory as Record<string, unknown> | undefined,
449
+ updatedBy: this.user?.uid
450
+ });
451
+ }
452
+
453
+ // Notify real-time subscribers (deferred if inside a transaction)
454
+ if (this._deferNotifications) {
455
+ this._pendingNotifications.push({
456
+ path,
457
+ entityId: savedEntity.id.toString(),
458
+ entity: savedEntity,
459
+ databaseId: resolvedCollection?.databaseId
460
+ });
461
+ } else {
462
+ await this.realtimeService.notifyEntityUpdate(
463
+ path,
464
+ savedEntity.id.toString(),
465
+ savedEntity,
466
+ resolvedCollection?.databaseId
467
+ );
468
+ }
469
+
470
+ return savedEntity;
471
+ } catch (error) {
472
+ if (callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
473
+ if (callbacks?.afterSaveError) {
474
+ await callbacks.afterSaveError({
475
+ collection: resolvedCollection as EntityCollection<M>,
476
+ path,
477
+ entityId: entityId || "unknown",
478
+ values: updatedValues,
479
+ previousValues: undefined,
480
+ status,
481
+ context: contextForCallback
482
+ });
483
+ }
484
+ if (propertyCallbacks?.afterSaveError) {
485
+ await propertyCallbacks.afterSaveError({
486
+ collection: resolvedCollection as EntityCollection<M>,
487
+ path,
488
+ entityId: entityId || "unknown",
489
+ values: updatedValues,
490
+ previousValues: undefined,
491
+ status,
492
+ context: contextForCallback
493
+ });
494
+ }
495
+ }
496
+ throw error;
497
+ }
498
+ }
499
+
500
+ async deleteEntity<M extends Record<string, unknown>>({
501
+ entity,
502
+ collection
503
+ }: DeleteEntityProps<M>): Promise<void> {
504
+
505
+ // Resolve from backend registry to restore callbacks lost during WebSocket serialization
506
+ const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, entity.path);
507
+
508
+ const contextForCallback = {
509
+ user: this.user,
510
+ driver: this,
511
+ data: this.data
512
+ } as unknown as RebaseCallContext;
513
+
514
+ if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
515
+ if (callbacks?.beforeDelete) {
516
+ await callbacks.beforeDelete({
517
+ collection: resolvedCollection as EntityCollection<M>,
518
+ path: entity.path,
519
+ entityId: entity.id,
520
+ entity,
521
+ context: contextForCallback
522
+ });
523
+ }
524
+ if (propertyCallbacks?.beforeDelete) {
525
+ await propertyCallbacks.beforeDelete({
526
+ collection: resolvedCollection as EntityCollection<M>,
527
+ path: entity.path,
528
+ entityId: entity.id,
529
+ entity,
530
+ context: contextForCallback
531
+ });
532
+ }
533
+ }
534
+
535
+ await this.entityService.deleteEntity(
536
+ entity.path,
537
+ entity.id,
538
+ entity.databaseId || resolvedCollection?.databaseId
539
+ );
540
+
541
+ if (callbacks?.afterDelete || propertyCallbacks?.afterDelete) {
542
+ if (callbacks?.afterDelete) {
543
+ await callbacks.afterDelete({
544
+ collection: resolvedCollection as EntityCollection<M>,
545
+ path: entity.path,
546
+ entityId: entity.id,
547
+ entity,
548
+ context: contextForCallback
549
+ });
550
+ }
551
+ if (propertyCallbacks?.afterDelete) {
552
+ await propertyCallbacks.afterDelete({
553
+ collection: resolvedCollection as EntityCollection<M>,
554
+ path: entity.path,
555
+ entityId: entity.id,
556
+ entity,
557
+ context: contextForCallback
558
+ });
559
+ }
560
+ }
561
+
562
+ // Record delete history (fire-and-forget)
563
+ if (this.historyService && resolvedCollection?.history) {
564
+ this.historyService.recordHistory({
565
+ tableName: entity.path,
566
+ entityId: entity.id.toString(),
567
+ action: "delete",
568
+ values: entity.values as Record<string, unknown>,
569
+ updatedBy: this.user?.uid
570
+ });
571
+ }
572
+
573
+ // Notify real-time subscribers (deferred if inside a transaction)
574
+ if (this._deferNotifications) {
575
+ this._pendingNotifications.push({
576
+ path: entity.path,
577
+ entityId: entity.id.toString(),
578
+ entity: null,
579
+ databaseId: entity.databaseId || resolvedCollection?.databaseId
580
+ });
581
+ } else {
582
+ await this.realtimeService.notifyEntityUpdate(
583
+ entity.path,
584
+ entity.id.toString(),
585
+ null,
586
+ entity.databaseId || resolvedCollection?.databaseId
587
+ );
588
+ }
589
+
590
+ }
591
+
592
+ async checkUniqueField(
593
+ path: string,
594
+ name: string,
595
+ value: unknown,
596
+ entityId?: string,
597
+ collection?: EntityCollection
598
+ ): Promise<boolean> {
599
+ return this.entityService.checkUniqueField(
600
+ path,
601
+ name,
602
+ value,
603
+ entityId,
604
+ collection?.databaseId
605
+ );
606
+ }
607
+
608
+
609
+ async countEntities<M extends Record<string, unknown>>({
610
+ path,
611
+ collection,
612
+ filter,
613
+ searchString
614
+ }: FetchCollectionProps<M>): Promise<number> {
615
+ return this.entityService.countEntities(
616
+ path,
617
+ { filter,
618
+ searchString }
619
+ );
620
+ }
621
+
622
+ private getTargetDb(databaseName?: string): DrizzleClient {
623
+ if (!databaseName || databaseName === this.poolManager?.defaultDatabaseName) {
624
+ return this.db;
625
+ }
626
+ if (!this.poolManager) {
627
+ throw new Error(
628
+ "Cross-database execution requires adminConnectionString to be configured in the backend."
629
+ );
630
+ }
631
+ return this.poolManager.getDrizzle(databaseName);
632
+ }
633
+
634
+ async executeSql(sqlText: string, options?: { database?: string, role?: string }): Promise<Record<string, unknown>[]> {
635
+ if (!options?.database && !options?.role) {
636
+ return this.entityService.executeSql(sqlText);
637
+ }
638
+
639
+ const targetDb = this.getTargetDb(options?.database);
640
+
641
+ try {
642
+ if (options?.role) {
643
+ const safeRole = options.role.replace(/"/g, '""');
644
+ return await targetDb.transaction(async (tx) => {
645
+ await tx.execute(drizzleSql.raw(`SET LOCAL ROLE "${safeRole}"`));
646
+ const result = await tx.execute(drizzleSql.raw(sqlText));
647
+ return result.rows as Record<string, unknown>[];
648
+ });
649
+ }
650
+
651
+ const result = await targetDb.execute(drizzleSql.raw(sqlText));
652
+ return result.rows as Record<string, unknown>[];
653
+ } catch (error: unknown) {
654
+ const msg = error instanceof Error ? error.message : String(error);
655
+ // Provide a user-friendly message for connection/auth errors
656
+ if (msg.includes("pg_hba.conf") || msg.includes("no encryption") || msg.includes("connection refused")) {
657
+ const dbName = options?.database || "unknown";
658
+ throw new Error(`Cannot connect to database "${dbName}": the server rejected the connection. This database may require SSL or is not accessible from this host.`);
659
+ }
660
+ throw error;
661
+ }
662
+ }
663
+
664
+ async fetchAvailableDatabases(): Promise<string[]> {
665
+ // Exclude template databases, Cloud SQL internal databases, and the default 'postgres' system db
666
+ const result = await this.executeSql(
667
+ `SELECT datname FROM pg_database
668
+ WHERE datistemplate = false
669
+ AND datname NOT IN ('postgres', 'cloudsqladmin', '_cloudsqladmin')
670
+ ORDER BY datname;`
671
+ );
672
+ const databases = result.map((r: Record<string, unknown>) => r.datname as string);
673
+ // Ensure the current connected database is always first in the list
674
+ const currentDb = this.poolManager?.defaultDatabaseName;
675
+ if (currentDb && !databases.includes(currentDb)) {
676
+ databases.unshift(currentDb);
677
+ } else if (currentDb) {
678
+ // Move it to the front
679
+ const idx = databases.indexOf(currentDb);
680
+ if (idx > 0) {
681
+ databases.splice(idx, 1);
682
+ databases.unshift(currentDb);
683
+ }
684
+ }
685
+ return databases;
686
+ }
687
+
688
+ async fetchAvailableRoles(): Promise<string[]> {
689
+ const result = await this.executeSql("SELECT rolname FROM pg_roles;");
690
+ return result.map((r: Record<string, unknown>) => r.rolname as string);
691
+ }
692
+
693
+ async fetchCurrentDatabase(): Promise<string | undefined> {
694
+ return this.poolManager?.defaultDatabaseName;
695
+ }
696
+
697
+ /**
698
+ * Fetch public tables that are not yet mapped to a collection.
699
+ * Excludes internal tables (_rebase_*, _auth_*, auth tables, etc.)
700
+ * and junction/connection tables used for many-to-many relations.
701
+ */
702
+ async fetchUnmappedTables(mappedPaths?: string[]): Promise<string[]> {
703
+ const result = await this.executeSql(`
704
+ SELECT table_name
705
+ FROM information_schema.tables
706
+ WHERE table_schema = 'public'
707
+ AND table_type = 'BASE TABLE'
708
+ ORDER BY table_name;
709
+ `);
710
+
711
+ const internalPrefixes = ["_rebase_", "_auth_"];
712
+ const internalExact = [
713
+ "users", "roles", "user_roles", "refresh_tokens",
714
+ "password_reset_tokens", "email_verification_tokens"
715
+ ];
716
+
717
+ const allTables = result
718
+ .map((r: Record<string, unknown>) => r.table_name as string)
719
+ .filter((name: string) => {
720
+ if (internalPrefixes.some(prefix => name.startsWith(prefix))) return false;
721
+ if (internalExact.includes(name)) return false;
722
+ return true;
723
+ });
724
+
725
+ // Detect junction tables: tables where every column is part of a foreign key.
726
+ // These are typically many-to-many connection tables and shouldn't be suggested.
727
+ let junctionTables = new Set<string>();
728
+ try {
729
+ const junctionResult = await this.executeSql(`
730
+ SELECT t.table_name
731
+ FROM information_schema.tables t
732
+ WHERE t.table_schema = 'public'
733
+ AND t.table_type = 'BASE TABLE'
734
+ AND NOT EXISTS (
735
+ -- Find columns that are NOT part of any foreign key
736
+ SELECT 1
737
+ FROM information_schema.columns c
738
+ WHERE c.table_schema = t.table_schema
739
+ AND c.table_name = t.table_name
740
+ AND c.column_name NOT IN (
741
+ SELECT kcu.column_name
742
+ FROM information_schema.key_column_usage kcu
743
+ JOIN information_schema.table_constraints tc
744
+ ON tc.constraint_name = kcu.constraint_name
745
+ AND tc.table_schema = kcu.table_schema
746
+ WHERE tc.constraint_type = 'FOREIGN KEY'
747
+ AND kcu.table_schema = t.table_schema
748
+ AND kcu.table_name = t.table_name
749
+ )
750
+ );
751
+ `);
752
+ junctionTables = new Set(junctionResult.map((r: Record<string, unknown>) => r.table_name as string));
753
+ } catch (e) {
754
+ console.warn("Could not detect junction tables:", e);
755
+ }
756
+
757
+ const filteredTables = allTables.filter(name => !junctionTables.has(name));
758
+
759
+ if (!mappedPaths || mappedPaths.length === 0) return filteredTables;
760
+
761
+ const mappedSet = new Set(mappedPaths.map(p => p.toLowerCase()));
762
+ return filteredTables.filter((name: string) => !mappedSet.has(name.toLowerCase()));
763
+ }
764
+
765
+
766
+ /**
767
+ * Fetch metadata for a given table from information_schema (columns, policies, constraints).
768
+ */
769
+ async fetchTableMetadata(tableName: string): Promise<TableMetadata> {
770
+ // Sanitize table name as defense-in-depth (parameterized below)
771
+ const safeName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
772
+
773
+ // 1. Fetch Columns
774
+ const result = await this.db.execute(drizzleSql`
775
+ SELECT column_name, data_type, udt_name, is_nullable, column_default, character_maximum_length
776
+ FROM information_schema.columns
777
+ WHERE table_schema = 'public'
778
+ AND table_name = ${safeName}
779
+ ORDER BY ordinal_position
780
+ `);
781
+ const columns = result.rows as Record<string, unknown>[];
782
+
783
+ // Also fetch enum values for any USER-DEFINED columns
784
+ const enumColumns = columns.filter((c) => c.data_type === "USER-DEFINED");
785
+ if (enumColumns.length > 0) {
786
+ for (const col of enumColumns) {
787
+ try {
788
+ const enumResult = await this.db.execute(drizzleSql`
789
+ SELECT e.enumlabel
790
+ FROM pg_type t
791
+ JOIN pg_enum e ON t.oid = e.enumtypid
792
+ WHERE t.typname = ${col.udt_name as string}
793
+ ORDER BY e.enumsortorder
794
+ `);
795
+ col.enum_values = (enumResult.rows as Record<string, unknown>[]).map(e => e.enumlabel);
796
+ } catch {
797
+ col.enum_values = [];
798
+ }
799
+ }
800
+ }
801
+ const typedColumns = columns as unknown as TableColumnInfo[];
802
+
803
+ // 2. Fetch Foreign Keys
804
+ const fkResult = await this.db.execute(drizzleSql`
805
+ SELECT
806
+ kcu.column_name as column_name,
807
+ ccu.table_name AS foreign_table_name,
808
+ ccu.column_name AS foreign_column_name
809
+ FROM
810
+ information_schema.table_constraints AS tc
811
+ JOIN information_schema.key_column_usage AS kcu
812
+ ON tc.constraint_name = kcu.constraint_name
813
+ AND tc.table_schema = kcu.table_schema
814
+ JOIN information_schema.constraint_column_usage AS ccu
815
+ ON ccu.constraint_name = tc.constraint_name
816
+ AND ccu.table_schema = tc.table_schema
817
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = ${safeName};
818
+ `);
819
+ const foreignKeys = fkResult.rows as unknown as TableForeignKeyInfo[];
820
+
821
+ // 3. Fetch Junction Tables (Many-to-Many)
822
+ // A simple junction table is one that has foreign keys to our table and other tables
823
+ const junctionsResult = await this.db.execute(drizzleSql`
824
+ SELECT
825
+ tc1.table_name as junction_table_name,
826
+ kcu1.column_name as source_column_name,
827
+ ccu2.table_name as target_table_name,
828
+ kcu2.column_name as target_column_name
829
+ FROM information_schema.table_constraints tc1
830
+ JOIN information_schema.key_column_usage kcu1 ON tc1.constraint_name = kcu1.constraint_name
831
+ JOIN information_schema.constraint_column_usage ccu1 ON ccu1.constraint_name = tc1.constraint_name
832
+ JOIN information_schema.table_constraints tc2 ON tc1.table_name = tc2.table_name AND tc2.constraint_type = 'FOREIGN KEY'
833
+ JOIN information_schema.key_column_usage kcu2 ON tc2.constraint_name = kcu2.constraint_name
834
+ JOIN information_schema.constraint_column_usage ccu2 ON ccu2.constraint_name = tc2.constraint_name
835
+ WHERE tc1.constraint_type = 'FOREIGN KEY'
836
+ AND ccu1.table_name = ${safeName}
837
+ AND ccu2.table_name != ${safeName};
838
+ `);
839
+ const junctions = junctionsResult.rows as unknown as TableJunctionInfo[];
840
+
841
+ // 4. Fetch RLS Policies
842
+ const policiesResult = await this.db.execute(drizzleSql`
843
+ SELECT
844
+ polname as policy_name,
845
+ polcmd as cmd,
846
+ polroles::regrole[]::text[] as roles,
847
+ pg_get_expr(polqual, polrelid) as qual,
848
+ pg_get_expr(polwithcheck, polrelid) as with_check
849
+ FROM pg_policy
850
+ WHERE polrelid = (SELECT oid FROM pg_class WHERE relname = ${safeName} AND relnamespace = 'public'::regnamespace);
851
+ `);
852
+ const policies = policiesResult.rows as unknown as TablePolicyInfo[];
853
+
854
+ return {
855
+ columns: typedColumns,
856
+ foreignKeys,
857
+ junctions,
858
+ policies
859
+ };
860
+ }
861
+
862
+ private generateSubscriptionId(): string {
863
+ return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
864
+ }
865
+
866
+ /**
867
+ * Create a new delegate instance with authenticated context.
868
+ * Starts a transaction and sets the current_user_id and current_user_roles
869
+ * configuration parameters for PostgreSQL Row Level Security.
870
+ */
871
+ async withAuth(user: User): Promise<DataDriver> {
872
+ return new AuthenticatedPostgresBackendDriver(this, user);
873
+ }
874
+ }
875
+
876
+ export class AuthenticatedPostgresBackendDriver implements DataDriver {
877
+ key = "postgres";
878
+ initialised = true;
879
+
880
+ public user: User;
881
+ public data: RebaseData;
882
+
883
+ constructor(
884
+ public delegate: PostgresBackendDriver,
885
+ user: User
886
+ ) {
887
+ this.user = user;
888
+ this.data = buildRebaseData(this);
889
+
890
+ // Delegate admin ops to the base driver (no RLS wrapping for admin)
891
+ this.admin = delegate.admin;
892
+ }
893
+
894
+ /**
895
+ * Typed admin capabilities — delegates to the base driver.
896
+ */
897
+ admin: DatabaseAdmin;
898
+
899
+ private async withTransaction<T>(
900
+ operation: (delegate: PostgresBackendDriver) => Promise<T>
901
+ ): Promise<T> {
902
+ const pendingNotifications: PostgresBackendDriver["_pendingNotifications"] = [];
903
+
904
+ const result = await this.delegate.db.transaction(async (tx) => {
905
+ let userId = this.user?.uid;
906
+ if (!userId) {
907
+ console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
908
+ userId = "anonymous";
909
+ }
910
+
911
+ const userRoles = this.user?.roles ?? [];
912
+ if (!this.user?.roles) {
913
+ console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
914
+ }
915
+ const normalizedRoles = userRoles.map((r: unknown) =>
916
+ typeof r === "string" ? r : (r as Record<string, unknown>)?.id ?? String(r)
917
+ );
918
+ const rolesString = normalizedRoles.join(",");
919
+
920
+ await tx.execute(drizzleSql`
921
+ SELECT
922
+ set_config('app.user_id', ${userId}, true),
923
+ set_config('app.user_roles', ${rolesString}, true),
924
+ set_config('app.jwt', ${JSON.stringify({ sub: userId,
925
+ roles: userRoles })}, true)
926
+ `);
927
+
928
+ const txEntityService = new EntityService(tx, this.delegate.registry);
929
+ const txDelegate = new PostgresBackendDriver(tx, this.delegate.realtimeService, this.delegate.registry, this.user, this.delegate.poolManager, this.delegate.historyService);
930
+
931
+ txDelegate.entityService = txEntityService;
932
+ txDelegate._deferNotifications = true;
933
+ txDelegate._pendingNotifications = pendingNotifications;
934
+
935
+ return await operation(txDelegate);
936
+ });
937
+
938
+ for (const notification of pendingNotifications) {
939
+ try {
940
+ await this.delegate.realtimeService.notifyEntityUpdate(
941
+ notification.path,
942
+ notification.entityId,
943
+ notification.entity,
944
+ notification.databaseId
945
+ );
946
+ } catch (e) {
947
+ console.error("[DataDriver] Error flushing deferred notification:", e);
948
+ }
949
+ }
950
+
951
+ return result;
952
+ }
953
+
954
+ async fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Entity<M>[]> {
955
+ return this.withTransaction((delegate) => delegate.fetchCollection(props));
956
+ }
957
+
958
+ /**
959
+ * Injects the authenticated user's context into the most recently
960
+ * registered realtime subscription so RLS-aware polling can apply.
961
+ */
962
+ private injectAuthContext(unsubscribe: () => void): () => void {
963
+ const authContext = { userId: this.user?.uid || "anonymous",
964
+ roles: this.user?.roles ?? [] };
965
+ const entries = Array.from(this.delegate.realtimeService.subscriptions.entries());
966
+ const lastEntry = entries[entries.length - 1];
967
+ const lastSub = lastEntry?.[1] as Record<string, unknown> | undefined;
968
+ if (lastSub && lastSub.clientId === "driver") {
969
+ lastSub.authContext = authContext;
970
+ }
971
+ return unsubscribe;
972
+ }
973
+
974
+ listenCollection<M extends Record<string, unknown>>(props: ListenCollectionProps<M>): () => void {
975
+ return this.injectAuthContext(this.delegate.listenCollection(props));
976
+ }
977
+
978
+ async fetchEntity<M extends Record<string, unknown>>(props: FetchEntityProps<M>): Promise<Entity<M> | undefined> {
979
+ return this.withTransaction((delegate) => delegate.fetchEntity(props));
980
+ }
981
+
982
+ listenEntity<M extends Record<string, unknown>>(props: ListenEntityProps<M>): () => void {
983
+ return this.injectAuthContext(this.delegate.listenEntity(props));
984
+ }
985
+
986
+ async saveEntity<M extends Record<string, unknown>>(props: SaveEntityProps<M>): Promise<Entity<M>> {
987
+ return this.withTransaction((delegate) => delegate.saveEntity(props));
988
+ }
989
+
990
+ async deleteEntity<M extends Record<string, unknown>>(props: DeleteEntityProps<M>): Promise<void> {
991
+ return this.withTransaction((delegate) => delegate.deleteEntity(props));
992
+ }
993
+
994
+ async checkUniqueField(
995
+ path: string,
996
+ name: string,
997
+ value: unknown,
998
+ entityId?: string,
999
+ collection?: EntityCollection
1000
+ ): Promise<boolean> {
1001
+ return this.withTransaction((delegate) => delegate.checkUniqueField(path, name, value, entityId, collection));
1002
+ }
1003
+
1004
+ async countEntities<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number> {
1005
+ return this.withTransaction((delegate) => delegate.countEntities(props));
1006
+ }
1007
+
1008
+ }