@rebasepro/server-postgresql 0.5.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 (165) hide show
  1. package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
  2. package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
  3. package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
  4. package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
  5. package/dist/index.es.js +10168 -11145
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +10735 -11429
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
  10. package/dist/utils/pg-error-utils.d.ts +55 -0
  11. package/package.json +24 -21
  12. package/src/PostgresAdapter.ts +9 -10
  13. package/src/PostgresBackendDriver.ts +134 -121
  14. package/src/PostgresBootstrapper.ts +86 -13
  15. package/src/auth/ensure-tables.ts +28 -5
  16. package/src/auth/services.ts +28 -18
  17. package/src/cli.ts +99 -96
  18. package/src/collections/PostgresCollectionRegistry.ts +7 -0
  19. package/src/connection.ts +11 -6
  20. package/src/data-transformer.ts +16 -14
  21. package/src/databasePoolManager.ts +3 -2
  22. package/src/history/HistoryService.ts +3 -2
  23. package/src/history/ensure-history-table.ts +5 -4
  24. package/src/schema/auth-schema.ts +1 -2
  25. package/src/schema/doctor-cli.ts +2 -1
  26. package/src/schema/doctor.ts +40 -37
  27. package/src/schema/generate-drizzle-schema-logic.ts +56 -18
  28. package/src/schema/generate-drizzle-schema.ts +11 -11
  29. package/src/schema/introspect-db-inference.ts +25 -25
  30. package/src/schema/introspect-db-logic.ts +38 -38
  31. package/src/schema/introspect-db.ts +28 -27
  32. package/src/services/BranchService.ts +14 -0
  33. package/src/services/EntityFetchService.ts +28 -25
  34. package/src/services/EntityPersistService.ts +11 -141
  35. package/src/services/RelationService.ts +57 -37
  36. package/src/services/entity-helpers.ts +6 -2
  37. package/src/services/realtimeService.ts +45 -32
  38. package/src/utils/drizzle-conditions.ts +31 -15
  39. package/src/utils/pg-error-utils.ts +211 -0
  40. package/src/websocket.ts +15 -12
  41. package/test/auth-services.test.ts +36 -19
  42. package/test/batch-many-to-many-regression.test.ts +119 -39
  43. package/test/data-transformer-hardening.test.ts +67 -33
  44. package/test/data-transformer.test.ts +4 -2
  45. package/test/doctor.test.ts +10 -5
  46. package/test/drizzle-conditions.test.ts +59 -6
  47. package/test/generate-drizzle-schema.test.ts +65 -40
  48. package/test/introspect-db-generation.test.ts +179 -81
  49. package/test/introspect-db-utils.test.ts +92 -37
  50. package/test/mocks/chalk.cjs +7 -0
  51. package/test/pg-error-utils.test.ts +221 -0
  52. package/test/postgresDataDriver.test.ts +14 -5
  53. package/test/property-ordering.test.ts +126 -79
  54. package/test/realtimeService.test.ts +6 -2
  55. package/test/relation-pipeline-gaps.test.ts +84 -36
  56. package/test/relations.test.ts +247 -0
  57. package/test/unmapped-tables-safety.test.ts +14 -6
  58. package/test/websocket.test.ts +1 -1
  59. package/tsconfig.json +5 -0
  60. package/tsconfig.prod.json +3 -0
  61. package/vite.config.ts +5 -5
  62. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  63. package/dist/common/src/collections/default-collections.d.ts +0 -9
  64. package/dist/common/src/collections/index.d.ts +0 -2
  65. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  66. package/dist/common/src/data/query_builder.d.ts +0 -55
  67. package/dist/common/src/index.d.ts +0 -4
  68. package/dist/common/src/util/builders.d.ts +0 -57
  69. package/dist/common/src/util/callbacks.d.ts +0 -6
  70. package/dist/common/src/util/collections.d.ts +0 -11
  71. package/dist/common/src/util/common.d.ts +0 -2
  72. package/dist/common/src/util/conditions.d.ts +0 -26
  73. package/dist/common/src/util/entities.d.ts +0 -58
  74. package/dist/common/src/util/enums.d.ts +0 -3
  75. package/dist/common/src/util/index.d.ts +0 -16
  76. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  77. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  78. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  79. package/dist/common/src/util/paths.d.ts +0 -14
  80. package/dist/common/src/util/permissions.d.ts +0 -14
  81. package/dist/common/src/util/references.d.ts +0 -2
  82. package/dist/common/src/util/relations.d.ts +0 -22
  83. package/dist/common/src/util/resolutions.d.ts +0 -72
  84. package/dist/common/src/util/storage.d.ts +0 -24
  85. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  86. package/dist/types/src/controllers/auth.d.ts +0 -104
  87. package/dist/types/src/controllers/client.d.ts +0 -168
  88. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  89. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  90. package/dist/types/src/controllers/data.d.ts +0 -207
  91. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  92. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  93. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  94. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  95. package/dist/types/src/controllers/email.d.ts +0 -36
  96. package/dist/types/src/controllers/index.d.ts +0 -18
  97. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  98. package/dist/types/src/controllers/navigation.d.ts +0 -225
  99. package/dist/types/src/controllers/registry.d.ts +0 -63
  100. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  101. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  102. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  103. package/dist/types/src/controllers/storage.d.ts +0 -171
  104. package/dist/types/src/index.d.ts +0 -4
  105. package/dist/types/src/rebase_context.d.ts +0 -122
  106. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  107. package/dist/types/src/types/backend.d.ts +0 -571
  108. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  109. package/dist/types/src/types/builders.d.ts +0 -15
  110. package/dist/types/src/types/chips.d.ts +0 -5
  111. package/dist/types/src/types/collections.d.ts +0 -961
  112. package/dist/types/src/types/component_ref.d.ts +0 -47
  113. package/dist/types/src/types/cron.d.ts +0 -102
  114. package/dist/types/src/types/data_source.d.ts +0 -64
  115. package/dist/types/src/types/database_adapter.d.ts +0 -94
  116. package/dist/types/src/types/entities.d.ts +0 -145
  117. package/dist/types/src/types/entity_actions.d.ts +0 -104
  118. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  119. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  120. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  121. package/dist/types/src/types/entity_views.d.ts +0 -87
  122. package/dist/types/src/types/export_import.d.ts +0 -21
  123. package/dist/types/src/types/formex.d.ts +0 -40
  124. package/dist/types/src/types/index.d.ts +0 -28
  125. package/dist/types/src/types/locales.d.ts +0 -4
  126. package/dist/types/src/types/modify_collections.d.ts +0 -5
  127. package/dist/types/src/types/plugins.d.ts +0 -282
  128. package/dist/types/src/types/properties.d.ts +0 -1173
  129. package/dist/types/src/types/property_config.d.ts +0 -74
  130. package/dist/types/src/types/relations.d.ts +0 -336
  131. package/dist/types/src/types/slots.d.ts +0 -262
  132. package/dist/types/src/types/translations.d.ts +0 -900
  133. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  134. package/dist/types/src/types/websockets.d.ts +0 -78
  135. package/dist/types/src/users/index.d.ts +0 -1
  136. package/dist/types/src/users/user.d.ts +0 -50
  137. /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
  138. /package/dist/{server-postgresql/src/auth → auth}/services.d.ts +0 -0
  139. /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
  140. /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
  141. /package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -0
  142. /package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +0 -0
  143. /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
  144. /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
  145. /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
  146. /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
  147. /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
  148. /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
  149. /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
  150. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
  151. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
  152. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
  153. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
  154. /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
  155. /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
  156. /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
  157. /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
  158. /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
  159. /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
  160. /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
  161. /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
  162. /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
  163. /package/dist/{server-postgresql/src/types.d.ts → types.d.ts} +0 -0
  164. /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
  165. /package/dist/{server-postgresql/src/websocket.d.ts → websocket.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 {
@@ -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
  }
@@ -16,7 +16,8 @@ import {
16
16
  RealtimeProvider,
17
17
  type DataDriver,
18
18
  type AuthAdapter,
19
- EntityCollection
19
+ EntityCollection,
20
+ PostgresCollection
20
21
  } from "@rebasepro/types";
21
22
  import { PostgresBackendDriver } from "./PostgresBackendDriver";
22
23
  import { RealtimeService } from "./services/realtimeService";
@@ -24,28 +25,33 @@ import { DatabasePoolManager } from "./databasePoolManager";
24
25
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
25
26
  import {
26
27
  createAuthRoutes,
27
- createAdminRoutes,
28
28
  requireAuth,
29
29
  requireAdmin,
30
30
  logger
31
- // @ts-ignore
32
31
  } from "@rebasepro/server-core";
33
32
  import { ensureAuthTablesExist } from "./auth/ensure-tables";
34
33
  import { UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
35
34
  import { createAuthSchema } from "./schema/auth-schema";
36
35
 
37
- // @ts-ignore
38
36
  import { createEmailService, type EmailConfig, type EmailService } from "@rebasepro/server-core";
39
- // @ts-ignore
40
37
  import { createHistoryRoutes } from "@rebasepro/server-core";
41
38
  import { HistoryService } from "./history/HistoryService";
42
39
  import { ensureHistoryTableExists } from "./history/ensure-history-table";
43
- // @ts-ignore
44
- import type { AuthConfig, PostgresDriverConfig, HistoryConfig } from "@rebasepro/server-core";
45
40
  import type { Hono } from "hono";
46
- // @ts-ignore
47
41
  import type { HonoEnv } from "@rebasepro/server-core";
48
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
+
49
55
  /**
50
56
  * Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
51
57
  * and re-uses in subsequent lifecycle hooks.
@@ -108,9 +114,10 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
108
114
  ...(pgConfig.schema?.relations || {})
109
115
  };
110
116
  const { drizzle: createDrizzle } = await import("drizzle-orm/node-postgres");
111
- const rawClient = ("$client" in pgConfig.connection
112
- ? (pgConfig.connection as Record<string, unknown>).$client
113
- : 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;
114
121
  const schemaAwareDb = createDrizzle(rawClient, { schema: mergedSchema });
115
122
 
116
123
  // Verify connection
@@ -163,6 +170,72 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
163
170
  }
164
171
  }
165
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
+
166
239
  const internals: PostgresDriverInternals = {
167
240
  db: schemaAwareDb,
168
241
  readDb,
@@ -231,7 +304,7 @@ authRepository };
231
304
  },
232
305
 
233
306
  async initializeHistory(config: unknown, driverResult: InitializedDriver): Promise<{ historyService: HistoryService } | undefined> {
234
- const historyConfig = config as HistoryConfig | boolean | undefined;
307
+ const historyConfig = config as { retention?: number } | boolean | undefined;
235
308
  if (!historyConfig) return undefined;
236
309
 
237
310
  const internals = driverResult.internals as PostgresDriverInternals;
@@ -267,7 +340,7 @@ authRepository };
267
340
  server as import("http").Server,
268
341
  realtimeService as RealtimeService,
269
342
  driver as PostgresBackendDriver,
270
- config as AuthConfig,
343
+ config as { requireAuth?: boolean },
271
344
  adapter as AuthAdapter | undefined
272
345
  );
273
346
  }