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

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