@rebasepro/server-postgresql 0.4.0 → 0.6.0

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 (168) hide show
  1. package/README.md +69 -89
  2. package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
  3. package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
  4. package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
  5. package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
  6. package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
  7. package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
  8. package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
  9. package/dist/index.es.js +10174 -11184
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +10735 -11462
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
  14. package/dist/types.d.ts +3 -0
  15. package/dist/utils/pg-error-utils.d.ts +55 -0
  16. package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
  17. package/package.json +24 -21
  18. package/src/PostgresAdapter.ts +9 -10
  19. package/src/PostgresBackendDriver.ts +135 -122
  20. package/src/PostgresBootstrapper.ts +90 -16
  21. package/src/auth/ensure-tables.ts +28 -5
  22. package/src/auth/services.ts +56 -45
  23. package/src/cli.ts +140 -110
  24. package/src/collections/PostgresCollectionRegistry.ts +7 -0
  25. package/src/connection.ts +11 -6
  26. package/src/data-transformer.ts +73 -109
  27. package/src/databasePoolManager.ts +5 -3
  28. package/src/history/HistoryService.ts +3 -2
  29. package/src/history/ensure-history-table.ts +5 -4
  30. package/src/schema/auth-schema.ts +1 -2
  31. package/src/schema/doctor-cli.ts +2 -1
  32. package/src/schema/doctor.ts +40 -37
  33. package/src/schema/generate-drizzle-schema-logic.ts +56 -18
  34. package/src/schema/generate-drizzle-schema.ts +11 -11
  35. package/src/schema/introspect-db-inference.ts +25 -25
  36. package/src/schema/introspect-db-logic.ts +38 -38
  37. package/src/schema/introspect-db.ts +28 -27
  38. package/src/services/BranchService.ts +14 -0
  39. package/src/services/EntityFetchService.ts +28 -25
  40. package/src/services/EntityPersistService.ts +11 -124
  41. package/src/services/RelationService.ts +57 -37
  42. package/src/services/entity-helpers.ts +6 -2
  43. package/src/services/realtimeService.ts +45 -32
  44. package/src/types.ts +4 -0
  45. package/src/utils/drizzle-conditions.ts +31 -15
  46. package/src/utils/pg-error-utils.ts +211 -0
  47. package/src/websocket.ts +51 -33
  48. package/test/auth-services.test.ts +36 -19
  49. package/test/batch-many-to-many-regression.test.ts +119 -39
  50. package/test/data-transformer-hardening.test.ts +67 -33
  51. package/test/data-transformer.test.ts +4 -2
  52. package/test/doctor.test.ts +10 -5
  53. package/test/drizzle-conditions.test.ts +59 -6
  54. package/test/generate-drizzle-schema.test.ts +65 -40
  55. package/test/introspect-db-generation.test.ts +179 -81
  56. package/test/introspect-db-utils.test.ts +92 -37
  57. package/test/mocks/chalk.cjs +7 -0
  58. package/test/pg-error-utils.test.ts +221 -0
  59. package/test/postgresDataDriver.test.ts +14 -5
  60. package/test/property-ordering.test.ts +126 -79
  61. package/test/realtimeService.test.ts +6 -2
  62. package/test/relation-pipeline-gaps.test.ts +84 -36
  63. package/test/relations.test.ts +247 -0
  64. package/test/unmapped-tables-safety.test.ts +14 -6
  65. package/test/websocket.test.ts +1 -1
  66. package/tsconfig.json +5 -0
  67. package/tsconfig.prod.json +3 -0
  68. package/vite.config.ts +5 -5
  69. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  70. package/dist/common/src/collections/default-collections.d.ts +0 -9
  71. package/dist/common/src/collections/index.d.ts +0 -2
  72. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  73. package/dist/common/src/data/query_builder.d.ts +0 -55
  74. package/dist/common/src/index.d.ts +0 -4
  75. package/dist/common/src/util/builders.d.ts +0 -57
  76. package/dist/common/src/util/callbacks.d.ts +0 -6
  77. package/dist/common/src/util/collections.d.ts +0 -11
  78. package/dist/common/src/util/common.d.ts +0 -2
  79. package/dist/common/src/util/conditions.d.ts +0 -26
  80. package/dist/common/src/util/entities.d.ts +0 -58
  81. package/dist/common/src/util/enums.d.ts +0 -3
  82. package/dist/common/src/util/index.d.ts +0 -16
  83. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  84. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  85. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  86. package/dist/common/src/util/paths.d.ts +0 -14
  87. package/dist/common/src/util/permissions.d.ts +0 -6
  88. package/dist/common/src/util/references.d.ts +0 -2
  89. package/dist/common/src/util/relations.d.ts +0 -22
  90. package/dist/common/src/util/resolutions.d.ts +0 -72
  91. package/dist/common/src/util/storage.d.ts +0 -24
  92. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  93. package/dist/types/src/controllers/auth.d.ts +0 -104
  94. package/dist/types/src/controllers/client.d.ts +0 -168
  95. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  96. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  97. package/dist/types/src/controllers/data.d.ts +0 -207
  98. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  99. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  100. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  101. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  102. package/dist/types/src/controllers/email.d.ts +0 -36
  103. package/dist/types/src/controllers/index.d.ts +0 -18
  104. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  105. package/dist/types/src/controllers/navigation.d.ts +0 -225
  106. package/dist/types/src/controllers/registry.d.ts +0 -63
  107. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  108. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  109. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  110. package/dist/types/src/controllers/storage.d.ts +0 -171
  111. package/dist/types/src/index.d.ts +0 -4
  112. package/dist/types/src/rebase_context.d.ts +0 -122
  113. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  114. package/dist/types/src/types/backend.d.ts +0 -536
  115. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  116. package/dist/types/src/types/builders.d.ts +0 -15
  117. package/dist/types/src/types/chips.d.ts +0 -5
  118. package/dist/types/src/types/collections.d.ts +0 -941
  119. package/dist/types/src/types/component_ref.d.ts +0 -47
  120. package/dist/types/src/types/cron.d.ts +0 -102
  121. package/dist/types/src/types/data_source.d.ts +0 -64
  122. package/dist/types/src/types/database_adapter.d.ts +0 -94
  123. package/dist/types/src/types/entities.d.ts +0 -145
  124. package/dist/types/src/types/entity_actions.d.ts +0 -104
  125. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  126. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  127. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  128. package/dist/types/src/types/entity_views.d.ts +0 -87
  129. package/dist/types/src/types/export_import.d.ts +0 -21
  130. package/dist/types/src/types/formex.d.ts +0 -40
  131. package/dist/types/src/types/index.d.ts +0 -28
  132. package/dist/types/src/types/locales.d.ts +0 -4
  133. package/dist/types/src/types/modify_collections.d.ts +0 -5
  134. package/dist/types/src/types/plugins.d.ts +0 -282
  135. package/dist/types/src/types/properties.d.ts +0 -1181
  136. package/dist/types/src/types/property_config.d.ts +0 -74
  137. package/dist/types/src/types/relations.d.ts +0 -336
  138. package/dist/types/src/types/slots.d.ts +0 -262
  139. package/dist/types/src/types/translations.d.ts +0 -900
  140. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  141. package/dist/types/src/types/websockets.d.ts +0 -78
  142. package/dist/types/src/users/index.d.ts +0 -1
  143. package/dist/types/src/users/user.d.ts +0 -50
  144. package/drizzle.test.config.ts +0 -10
  145. /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
  146. /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
  147. /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
  148. /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
  149. /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
  150. /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
  151. /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
  152. /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
  153. /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
  154. /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
  155. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
  156. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
  157. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
  158. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
  159. /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
  160. /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
  161. /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
  162. /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
  163. /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
  164. /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
  165. /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
  166. /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
  167. /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
  168. /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
@@ -1,14 +1,10 @@
1
- // import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
1
  import { EntityService } from "./services/entityService";
3
2
  import { BranchService } from "./services/BranchService";
4
3
  import { RealtimeService } from "./services/realtimeService";
5
4
  import { DatabasePoolManager } from "./databasePoolManager";
6
5
  import { DrizzleClient } from "./interfaces";
7
- import { User, RebaseClient } from "@rebasepro/types";
8
- import { sql as drizzleSql } from "drizzle-orm";
9
- import { buildPropertyCallbacks, updateDateAutoValues } from "@rebasepro/common";
10
- import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
11
6
  import {
7
+ DatabaseAdmin,
12
8
  DataDriver,
13
9
  DeleteEntityProps,
14
10
  Entity,
@@ -18,22 +14,23 @@ import {
18
14
  ListenCollectionProps,
19
15
  ListenEntityProps,
20
16
  RebaseCallContext,
21
- SaveEntityProps,
17
+ RebaseClient,
22
18
  RebaseData,
23
- TableMetadata,
19
+ RestFetchService,
20
+ SaveEntityProps,
24
21
  TableColumnInfo,
25
22
  TableForeignKeyInfo,
26
23
  TableJunctionInfo,
24
+ TableMetadata,
27
25
  TablePolicyInfo,
28
- SQLAdmin,
29
- SchemaAdmin,
30
- DatabaseAdmin,
31
- RestFetchService
26
+ User
32
27
  } from "@rebasepro/types";
33
- import { buildRebaseData } from "@rebasepro/common";
34
- // @ts-ignore
28
+ import { sql as drizzleSql } from "drizzle-orm";
29
+ import { buildPropertyCallbacks, buildRebaseData, updateDateAutoValues } from "@rebasepro/common";
30
+ import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
35
31
  import { HistoryService } from "./history/HistoryService";
36
32
  import { mergeDeep } from "@rebasepro/utils";
33
+ import { logger } from "@rebasepro/server-core";
37
34
 
38
35
  export class PostgresBackendDriver implements DataDriver {
39
36
  key = "postgres";
@@ -112,15 +109,28 @@ export class PostgresBackendDriver implements DataDriver {
112
109
  return this.entityService.getFetchService();
113
110
  }
114
111
 
112
+ private buildCallContext(): RebaseCallContext {
113
+ return {
114
+ user: this.user,
115
+ driver: this,
116
+ data: this.data,
117
+ client: this.client,
118
+ storageSource: this.client?.storage
119
+ } as unknown as RebaseCallContext;
120
+ }
115
121
 
116
122
  private resolveCollectionCallbacks<M extends Record<string, unknown>>(collection: EntityCollection<M> | undefined, path: string) {
117
- if (!collection && !path) return { collection: undefined,
118
- callbacks: undefined,
119
- propertyCallbacks: undefined };
123
+ if (!collection && !path) return {
124
+ collection: undefined,
125
+ callbacks: undefined,
126
+ propertyCallbacks: undefined
127
+ };
120
128
  const registryCollection = this.registry?.getCollectionByPath(path);
121
129
  const resolvedCollection = registryCollection
122
- ? { ...collection,
123
- ...registryCollection } as EntityCollection<M>
130
+ ? {
131
+ ...collection,
132
+ ...registryCollection
133
+ } as EntityCollection<M>
124
134
  : collection as EntityCollection<M>;
125
135
 
126
136
  const callbacks = resolvedCollection?.callbacks;
@@ -137,17 +147,17 @@ propertyCallbacks: undefined };
137
147
  }
138
148
 
139
149
  async fetchCollection<M extends Record<string, unknown>>({
140
- path,
141
- collection,
142
- filter,
143
- limit,
144
- offset,
145
- startAfter,
146
- orderBy,
147
- searchString,
148
- order,
149
- vectorSearch
150
- }: FetchCollectionProps<M>): Promise<Entity<M>[]> {
150
+ path,
151
+ collection,
152
+ filter,
153
+ limit,
154
+ offset,
155
+ startAfter,
156
+ orderBy,
157
+ searchString,
158
+ order,
159
+ vectorSearch
160
+ }: FetchCollectionProps<M>): Promise<Entity<M>[]> {
151
161
 
152
162
  const entities = await this.entityService.fetchCollection<M>(path, {
153
163
  filter,
@@ -161,16 +171,14 @@ propertyCallbacks: undefined };
161
171
  vectorSearch
162
172
  });
163
173
 
164
- const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
174
+ const {
175
+ collection: resolvedCollection,
176
+ callbacks,
177
+ propertyCallbacks
178
+ } = this.resolveCollectionCallbacks(collection, path);
165
179
 
166
180
  if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
167
- const contextForCallback = {
168
- user: this.user,
169
- driver: this,
170
- data: this.data,
171
- client: this.client,
172
- storageSource: this.client?.storage
173
- } as unknown as RebaseCallContext; // Backend context
181
+ const contextForCallback = this.buildCallContext();
174
182
  return Promise.all(entities.map(async (entity) => {
175
183
  let fetched = entity;
176
184
  if (callbacks?.afterRead) {
@@ -197,18 +205,18 @@ propertyCallbacks: undefined };
197
205
  }
198
206
 
199
207
  listenCollection<M extends Record<string, unknown>>({
200
- path,
201
- collection,
202
- filter,
203
- limit,
204
- offset,
205
- startAfter,
206
- orderBy,
207
- searchString,
208
- order,
209
- onUpdate,
210
- onError
211
- }: ListenCollectionProps<M>): () => void {
208
+ path,
209
+ collection,
210
+ filter,
211
+ limit,
212
+ offset,
213
+ startAfter,
214
+ orderBy,
215
+ searchString,
216
+ order,
217
+ onUpdate,
218
+ onError
219
+ }: ListenCollectionProps<M>): () => void {
212
220
 
213
221
  const subscriptionId = this.generateSubscriptionId();
214
222
 
@@ -261,27 +269,25 @@ propertyCallbacks: undefined };
261
269
  }
262
270
 
263
271
  async fetchEntity<M extends Record<string, unknown>>({
264
- path,
265
- entityId,
266
- databaseId,
267
- collection
268
- }: FetchEntityProps<M>): Promise<Entity<M> | undefined> {
272
+ path,
273
+ entityId,
274
+ databaseId,
275
+ collection
276
+ }: FetchEntityProps<M>): Promise<Entity<M> | undefined> {
269
277
  let entity = await this.entityService.fetchEntity<M>(
270
278
  path,
271
279
  entityId,
272
280
  databaseId || collection?.databaseId
273
281
  );
274
282
 
275
- const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
283
+ const {
284
+ collection: resolvedCollection,
285
+ callbacks,
286
+ propertyCallbacks
287
+ } = this.resolveCollectionCallbacks(collection, path);
276
288
 
277
289
  if (entity && (callbacks?.afterRead || propertyCallbacks?.afterRead)) {
278
- const contextForCallback = {
279
- user: this.user,
280
- driver: this,
281
- data: this.data,
282
- client: this.client,
283
- storageSource: this.client?.storage
284
- } as unknown as RebaseCallContext; // Backend context
290
+ const contextForCallback = this.buildCallContext();
285
291
  if (callbacks?.afterRead) {
286
292
  entity = await callbacks.afterRead({
287
293
  collection: resolvedCollection as EntityCollection<M>,
@@ -304,12 +310,12 @@ propertyCallbacks: undefined };
304
310
  }
305
311
 
306
312
  listenEntity<M extends Record<string, unknown>>({
307
- path,
308
- entityId,
309
- collection,
310
- onUpdate,
311
- onError
312
- }: ListenEntityProps<M>): () => void {
313
+ path,
314
+ entityId,
315
+ collection,
316
+ onUpdate,
317
+ onError
318
+ }: ListenEntityProps<M>): () => void {
313
319
 
314
320
  const subscriptionId = this.generateSubscriptionId();
315
321
  const callbackWrapper = (entity: Entity<M> | null) => {
@@ -349,23 +355,21 @@ propertyCallbacks: undefined };
349
355
  }
350
356
 
351
357
  async saveEntity<M extends Record<string, unknown>>({
352
- path,
353
- entityId,
354
- values,
355
- collection,
356
- status
357
- }: SaveEntityProps<M>): Promise<Entity<M>> {
358
-
359
- const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
358
+ path,
359
+ entityId,
360
+ values,
361
+ collection,
362
+ status
363
+ }: SaveEntityProps<M>): Promise<Entity<M>> {
364
+
365
+ const {
366
+ collection: resolvedCollection,
367
+ callbacks,
368
+ propertyCallbacks
369
+ } = this.resolveCollectionCallbacks(collection, path);
360
370
 
361
371
  let updatedValues = values;
362
- const contextForCallback = {
363
- user: this.user,
364
- driver: this,
365
- data: this.data,
366
- client: this.client,
367
- storageSource: this.client?.storage
368
- } as unknown as RebaseCallContext;
372
+ const contextForCallback = this.buildCallContext();
369
373
 
370
374
  // Fetch previous values for callbacks AND history recording
371
375
  let previousValuesForHistory: Partial<Entity<M>["values"]> | undefined;
@@ -528,20 +532,18 @@ propertyCallbacks: undefined };
528
532
  }
529
533
 
530
534
  async deleteEntity<M extends Record<string, unknown>>({
531
- entity,
532
- collection
533
- }: DeleteEntityProps<M>): Promise<void> {
535
+ entity,
536
+ collection
537
+ }: DeleteEntityProps<M>): Promise<void> {
534
538
 
535
539
  // Resolve from backend registry to restore callbacks lost during WebSocket serialization
536
- const { collection: resolvedCollection, callbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, entity.path);
540
+ const {
541
+ collection: resolvedCollection,
542
+ callbacks,
543
+ propertyCallbacks
544
+ } = this.resolveCollectionCallbacks(collection, entity.path);
537
545
 
538
- const contextForCallback = {
539
- user: this.user,
540
- driver: this,
541
- data: this.data,
542
- client: this.client,
543
- storageSource: this.client?.storage
544
- } as unknown as RebaseCallContext;
546
+ const contextForCallback = this.buildCallContext();
545
547
 
546
548
  if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
547
549
  let preventDefault = false;
@@ -653,17 +655,18 @@ propertyCallbacks: undefined };
653
655
  );
654
656
  }
655
657
 
656
-
657
658
  async countEntities<M extends Record<string, unknown>>({
658
- path,
659
- collection,
660
- filter,
661
- searchString
662
- }: FetchCollectionProps<M>): Promise<number> {
659
+ path,
660
+ collection,
661
+ filter,
662
+ searchString
663
+ }: FetchCollectionProps<M>): Promise<number> {
663
664
  return this.entityService.countEntities(
664
665
  path,
665
- { filter,
666
- searchString }
666
+ {
667
+ filter,
668
+ searchString
669
+ }
667
670
  );
668
671
  }
669
672
 
@@ -679,7 +682,10 @@ searchString }
679
682
  return this.poolManager.getDrizzle(databaseName);
680
683
  }
681
684
 
682
- async executeSql(sqlText: string, options?: { database?: string, role?: string }): Promise<Record<string, unknown>[]> {
685
+ async executeSql(sqlText: string, options?: {
686
+ database?: string,
687
+ role?: string
688
+ }): Promise<Record<string, unknown>[]> {
683
689
  if (!options?.database && !options?.role) {
684
690
  return this.entityService.executeSql(sqlText);
685
691
  }
@@ -704,7 +710,7 @@ searchString }
704
710
  }
705
711
 
706
712
  if (needsRoleSwitch && options?.role) {
707
- const safeRole = options.role.replace(/"/g, '""');
713
+ const safeRole = options.role.replace(/"/g, "\"\"");
708
714
  return await targetDb.transaction(async (tx) => {
709
715
  await tx.execute(drizzleSql.raw(`SET LOCAL ROLE "${safeRole}"`));
710
716
  const result = await tx.execute(drizzleSql.raw(sqlText));
@@ -817,7 +823,7 @@ searchString }
817
823
  `);
818
824
  junctionTables = new Set(junctionResult.map((r: Record<string, unknown>) => r.table_name as string));
819
825
  } catch (e) {
820
- console.warn("Could not detect junction tables:", e);
826
+ logger.warn("Could not detect junction tables", { error: e });
821
827
  }
822
828
 
823
829
  const filteredTables = allTables.filter(name => !junctionTables.has(name));
@@ -828,7 +834,6 @@ searchString }
828
834
  return filteredTables.filter((name: string) => !mappedSet.has(name.toLowerCase()));
829
835
  }
830
836
 
831
-
832
837
  /**
833
838
  * Fetch metadata for a given table from information_schema (columns, policies, constraints).
834
839
  */
@@ -972,31 +977,35 @@ export class AuthenticatedPostgresBackendDriver implements DataDriver {
972
977
  fetchCollectionForRest: async (collectionPath, options, include) => {
973
978
  return this.withTransaction(async (delegate) => {
974
979
  return delegate.restFetchService.fetchCollectionForRest(collectionPath, options, include);
975
- });
980
+ }, { accessMode: "read only" });
976
981
  },
977
982
  fetchEntityForRest: async (collectionPath, entityId, include, databaseId) => {
978
983
  return this.withTransaction(async (delegate) => {
979
984
  return delegate.restFetchService.fetchEntityForRest(collectionPath, entityId, include, databaseId);
980
- });
985
+ }, { accessMode: "read only" });
981
986
  }
982
987
  };
983
988
  }
984
989
 
985
990
  private async withTransaction<T>(
986
- operation: (delegate: PostgresBackendDriver) => Promise<T>
991
+ operation: (delegate: PostgresBackendDriver) => Promise<T>,
992
+ options?: {
993
+ accessMode?: "read only" | "read write";
994
+ isolationLevel?: "read uncommitted" | "read committed" | "repeatable read" | "serializable"
995
+ }
987
996
  ): Promise<T> {
988
997
  const pendingNotifications: PostgresBackendDriver["_pendingNotifications"] = [];
989
998
 
990
999
  const result = await this.delegate.db.transaction(async (tx) => {
991
1000
  let userId = this.user?.uid;
992
1001
  if (!userId) {
993
- console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
1002
+ logger.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object", { detail: this.user });
994
1003
  userId = "anonymous";
995
1004
  }
996
1005
 
997
1006
  const userRoles = this.user?.roles ?? [];
998
1007
  if (!this.user?.roles) {
999
- console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
1008
+ logger.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object", { detail: this.user });
1000
1009
  }
1001
1010
  const normalizedRoles = userRoles.map((r: unknown) =>
1002
1011
  typeof r === "string" ? r : (r as Record<string, unknown>)?.id ?? String(r)
@@ -1007,8 +1016,10 @@ export class AuthenticatedPostgresBackendDriver implements DataDriver {
1007
1016
  SELECT
1008
1017
  set_config('app.user_id', ${userId}, true),
1009
1018
  set_config('app.user_roles', ${rolesString}, true),
1010
- set_config('app.jwt', ${JSON.stringify({ sub: userId,
1011
- roles: userRoles })}, true)
1019
+ set_config('app.jwt', ${JSON.stringify({
1020
+ sub: userId,
1021
+ roles: userRoles
1022
+ })}, true)
1012
1023
  `);
1013
1024
 
1014
1025
  const txEntityService = new EntityService(tx, this.delegate.registry);
@@ -1020,7 +1031,7 @@ roles: userRoles })}, true)
1020
1031
  txDelegate.client = this.delegate.client;
1021
1032
 
1022
1033
  return await operation(txDelegate);
1023
- });
1034
+ }, options);
1024
1035
 
1025
1036
  for (const notification of pendingNotifications) {
1026
1037
  try {
@@ -1031,7 +1042,7 @@ roles: userRoles })}, true)
1031
1042
  notification.databaseId
1032
1043
  );
1033
1044
  } catch (e) {
1034
- console.error("[DataDriver] Error flushing deferred notification:", e);
1045
+ logger.error("[DataDriver] Error flushing deferred notification", { error: e });
1035
1046
  }
1036
1047
  }
1037
1048
 
@@ -1039,7 +1050,7 @@ roles: userRoles })}, true)
1039
1050
  }
1040
1051
 
1041
1052
  async fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Entity<M>[]> {
1042
- return this.withTransaction((delegate) => delegate.fetchCollection(props));
1053
+ return this.withTransaction((delegate) => delegate.fetchCollection(props), { accessMode: "read only" });
1043
1054
  }
1044
1055
 
1045
1056
  /**
@@ -1047,8 +1058,10 @@ roles: userRoles })}, true)
1047
1058
  * registered realtime subscription so RLS-aware polling can apply.
1048
1059
  */
1049
1060
  private injectAuthContext(unsubscribe: () => void): () => void {
1050
- const authContext = { userId: this.user?.uid || "anonymous",
1051
- roles: this.user?.roles ?? [] };
1061
+ const authContext = {
1062
+ userId: this.user?.uid || "anonymous",
1063
+ roles: this.user?.roles ?? []
1064
+ };
1052
1065
  const entries = Array.from(this.delegate.realtimeService.subscriptions.entries());
1053
1066
  const lastEntry = entries[entries.length - 1];
1054
1067
  const lastSub = lastEntry?.[1] as Record<string, unknown> | undefined;
@@ -1063,7 +1076,7 @@ roles: this.user?.roles ?? [] };
1063
1076
  }
1064
1077
 
1065
1078
  async fetchEntity<M extends Record<string, unknown>>(props: FetchEntityProps<M>): Promise<Entity<M> | undefined> {
1066
- return this.withTransaction((delegate) => delegate.fetchEntity(props));
1079
+ return this.withTransaction((delegate) => delegate.fetchEntity(props), { accessMode: "read only" });
1067
1080
  }
1068
1081
 
1069
1082
  listenEntity<M extends Record<string, unknown>>(props: ListenEntityProps<M>): () => void {
@@ -1079,7 +1092,7 @@ roles: this.user?.roles ?? [] };
1079
1092
  }
1080
1093
 
1081
1094
  async deleteAll(path: string): Promise<void> {
1082
- return this.delegate.deleteAll(path);
1095
+ return this.withTransaction((delegate) => delegate.deleteAll(path));
1083
1096
  }
1084
1097
 
1085
1098
  async checkUniqueField(
@@ -1089,11 +1102,11 @@ roles: this.user?.roles ?? [] };
1089
1102
  entityId?: string,
1090
1103
  collection?: EntityCollection
1091
1104
  ): Promise<boolean> {
1092
- return this.withTransaction((delegate) => delegate.checkUniqueField(path, name, value, entityId, collection));
1105
+ return this.withTransaction((delegate) => delegate.checkUniqueField(path, name, value, entityId, collection), { accessMode: "read only" });
1093
1106
  }
1094
1107
 
1095
1108
  async countEntities<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number> {
1096
- return this.withTransaction((delegate) => delegate.countEntities(props));
1109
+ return this.withTransaction((delegate) => delegate.countEntities(props), { accessMode: "read only" });
1097
1110
  }
1098
1111
 
1099
1112
  }
@@ -6,7 +6,8 @@
6
6
 
7
7
  import { getTableName, isTable, Relations, sql, Table } from "drizzle-orm";
8
8
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
9
- import { PgEnum, PgTable, getTableConfig, AnyPgColumn } from "drizzle-orm/pg-core";
9
+ import { PgEnum, PgTable, getTableConfig } from "drizzle-orm/pg-core";
10
+ import type { RebasePgTable } from "./types";
10
11
  import {
11
12
  BackendBootstrapper,
12
13
  InitializedDriver,
@@ -15,7 +16,8 @@ import {
15
16
  RealtimeProvider,
16
17
  type DataDriver,
17
18
  type AuthAdapter,
18
- EntityCollection
19
+ EntityCollection,
20
+ PostgresCollection
19
21
  } from "@rebasepro/types";
20
22
  import { PostgresBackendDriver } from "./PostgresBackendDriver";
21
23
  import { RealtimeService } from "./services/realtimeService";
@@ -23,28 +25,33 @@ import { DatabasePoolManager } from "./databasePoolManager";
23
25
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
24
26
  import {
25
27
  createAuthRoutes,
26
- createAdminRoutes,
27
28
  requireAuth,
28
29
  requireAdmin,
29
30
  logger
30
- // @ts-ignore
31
31
  } from "@rebasepro/server-core";
32
32
  import { ensureAuthTablesExist } from "./auth/ensure-tables";
33
33
  import { UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
34
34
  import { createAuthSchema } from "./schema/auth-schema";
35
35
 
36
- // @ts-ignore
37
36
  import { createEmailService, type EmailConfig, type EmailService } from "@rebasepro/server-core";
38
- // @ts-ignore
39
37
  import { createHistoryRoutes } from "@rebasepro/server-core";
40
38
  import { HistoryService } from "./history/HistoryService";
41
39
  import { ensureHistoryTableExists } from "./history/ensure-history-table";
42
- // @ts-ignore
43
- import type { AuthConfig, PostgresDriverConfig, HistoryConfig } from "@rebasepro/server-core";
44
40
  import type { Hono } from "hono";
45
- // @ts-ignore
46
41
  import type { HonoEnv } from "@rebasepro/server-core";
47
42
 
43
+ export interface PostgresDriverConfig {
44
+ connectionString?: string;
45
+ adminConnectionString?: string;
46
+ readConnectionString?: string;
47
+ connection?: unknown;
48
+ schema?: {
49
+ tables?: Record<string, unknown>;
50
+ enums?: Record<string, unknown>;
51
+ relations?: Record<string, unknown>;
52
+ };
53
+ }
54
+
48
55
  /**
49
56
  * Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
50
57
  * and re-uses in subsequent lifecycle hooks.
@@ -107,9 +114,10 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
107
114
  ...(pgConfig.schema?.relations || {})
108
115
  };
109
116
  const { drizzle: createDrizzle } = await import("drizzle-orm/node-postgres");
110
- const rawClient = ("$client" in pgConfig.connection
111
- ? (pgConfig.connection as Record<string, unknown>).$client
112
- : pgConfig.connection) as import("pg").Pool;
117
+ const connection = pgConfig.connection;
118
+ const rawClient = (connection && typeof connection === "object" && "$client" in connection
119
+ ? (connection as Record<string, unknown>).$client
120
+ : connection) as import("pg").Pool;
113
121
  const schemaAwareDb = createDrizzle(rawClient, { schema: mergedSchema });
114
122
 
115
123
  // Verify connection
@@ -162,6 +170,72 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
162
170
  }
163
171
  }
164
172
 
173
+ // ── Startup Schema Validation ────────────────────────────────────
174
+ // One-directional: only checks collections → DB (extra DB tables
175
+ // that aren't mapped to collections are perfectly fine).
176
+ try {
177
+ const registeredCollections = registry.getCollections();
178
+ if (registeredCollections.length > 0) {
179
+ const schemasToCheck = Array.from(new Set(
180
+ registeredCollections.map(c => "schema" in c && c.schema ? c.schema : "public")
181
+ ));
182
+ const schemasList = schemasToCheck.map(s => `'${s}'`).join(",");
183
+ const result = await schemaAwareDb.execute(sql.raw(`
184
+ SELECT table_name, table_schema
185
+ FROM information_schema.tables
186
+ WHERE table_schema IN (${schemasList})
187
+ AND table_type = 'BASE TABLE'
188
+ `));
189
+ const dbTables = new Set(
190
+ (result.rows as Array<{ table_name: string; table_schema: string }>).map(r =>
191
+ r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
192
+ )
193
+ );
194
+ const missing: Array<{ slug: string; table: string }> = [];
195
+ for (const col of registeredCollections) {
196
+ const schemaName = "schema" in col && col.schema ? col.schema : "public";
197
+ const tableName = registry.hasTableForCollection(
198
+ col.table ?? col.slug
199
+ )
200
+ ? (col.table ?? col.slug)
201
+ : col.slug;
202
+ // Resolve the actual table name the registry stored
203
+ const resolvedTable = registry.getTableNames().find((k) =>
204
+ k === tableName ||
205
+ k === col.slug
206
+ );
207
+ const checkName = resolvedTable ?? tableName;
208
+ const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
209
+ if (!dbTables.has(fullCheckName)) {
210
+ missing.push({ slug: col.slug,
211
+ table: checkName });
212
+ }
213
+ }
214
+ if (missing.length > 0) {
215
+ const lines = missing.map(
216
+ m => ` • collection "${m.slug}" → table "${m.table}"`
217
+ );
218
+ logger.warn([
219
+ "",
220
+ "┌──────────────────────────────────────────────────────────────┐",
221
+ "│ ⚠️ SCHEMA DRIFT — Missing tables in database │",
222
+ "├──────────────────────────────────────────────────────────────┤",
223
+ ...lines.map(l => `│ ${l.padEnd(60)}│`),
224
+ "├──────────────────────────────────────────────────────────────┤",
225
+ "│ Run one of: │",
226
+ "│ pnpm db:push (dev — fast, no migration files) │",
227
+ "│ pnpm db:migrate (prod — creates migration files) │",
228
+ "└──────────────────────────────────────────────────────────────┘",
229
+ ""
230
+ ].join("\n"));
231
+ }
232
+ }
233
+ } catch (err) {
234
+ logger.warn("⚠️ Startup schema validation could not run", {
235
+ error: err instanceof Error ? err.message : String(err)
236
+ });
237
+ }
238
+
165
239
  const internals: PostgresDriverInternals = {
166
240
  db: schemaAwareDb,
167
241
  readDb,
@@ -207,7 +281,7 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
207
281
  : authCollection.slug)
208
282
  : undefined;
209
283
  const usersTable = tableName
210
- ? registry.getTable(tableName) as (PgTable & Record<string, AnyPgColumn>) | undefined
284
+ ? registry.getTable(tableName) as RebasePgTable | undefined
211
285
  : undefined;
212
286
 
213
287
  let usersSchemaName = "rebase";
@@ -217,7 +291,7 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
217
291
 
218
292
  const authTables = createAuthSchema(usersSchemaName) as unknown as AuthSchemaTables;
219
293
  if (usersTable) {
220
- authTables.users = usersTable as unknown as PgTable & Record<string, AnyPgColumn>;
294
+ authTables.users = usersTable as RebasePgTable;
221
295
  }
222
296
 
223
297
  const userService = new UserService(db, authTables);
@@ -230,7 +304,7 @@ authRepository };
230
304
  },
231
305
 
232
306
  async initializeHistory(config: unknown, driverResult: InitializedDriver): Promise<{ historyService: HistoryService } | undefined> {
233
- const historyConfig = config as HistoryConfig | boolean | undefined;
307
+ const historyConfig = config as { retention?: number } | boolean | undefined;
234
308
  if (!historyConfig) return undefined;
235
309
 
236
310
  const internals = driverResult.internals as PostgresDriverInternals;
@@ -266,7 +340,7 @@ authRepository };
266
340
  server as import("http").Server,
267
341
  realtimeService as RealtimeService,
268
342
  driver as PostgresBackendDriver,
269
- config as AuthConfig,
343
+ config as { requireAuth?: boolean },
270
344
  adapter as AuthAdapter | undefined
271
345
  );
272
346
  }