@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
@@ -13,6 +13,29 @@ import {
13
13
  } from "./entity-helpers";
14
14
  import { parseDataFromServer } from "../data-transformer";
15
15
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
16
+ import { logger } from "@rebasepro/server-core";
17
+
18
+ /**
19
+ * Typed wrapper for Drizzle dynamic query innerJoin.
20
+ * Drizzle's `$dynamic()` queries lose the `innerJoin` method from
21
+ * their static type, but it exists at runtime. This helper bridges
22
+ * the gap with a single confined cast.
23
+ */
24
+ function applyDynamicJoin<T>(query: T, joinTable: PgTable, condition: SQL): T {
25
+ return (query as unknown as { innerJoin(t: PgTable, c: SQL): T }).innerJoin(joinTable, condition) as T;
26
+ }
27
+
28
+ /**
29
+ * Typed wrapper for DrizzleConditionBuilder.buildRelationQuery on dynamic queries.
30
+ * The method returns a widened generic that doesn't reassign cleanly;
31
+ * this helper confines the cast.
32
+ */
33
+ function applyDynamicRelationQuery<T>(
34
+ query: T,
35
+ ...args: Parameters<typeof DrizzleConditionBuilder.buildRelationQuery>
36
+ ): T {
37
+ return DrizzleConditionBuilder.buildRelationQuery(...args) as unknown as T;
38
+ }
16
39
 
17
40
  /**
18
41
  * Service for handling all relation-related operations.
@@ -108,8 +131,7 @@ export class RelationService {
108
131
  throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
109
132
  }
110
133
 
111
- // @ts-expect-error Drizzle mutates base query generic on innerJoin
112
- query = query.innerJoin(joinTable, eq(fromCol, toCol));
134
+ query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
113
135
  currentTable = joinTable;
114
136
  }
115
137
 
@@ -167,8 +189,8 @@ export class RelationService {
167
189
  }
168
190
 
169
191
  // Use unified relation query builder
170
- // @ts-expect-error buildRelationQuery uses dynamic queries
171
- query = DrizzleConditionBuilder.buildRelationQuery(
192
+ query = applyDynamicRelationQuery(
193
+ query,
172
194
  query,
173
195
  relation,
174
196
  parsedParentId,
@@ -316,8 +338,7 @@ export class RelationService {
316
338
  throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
317
339
  }
318
340
 
319
- // @ts-expect-error Drizzle mutates base query generic on innerJoin
320
- query = query.innerJoin(joinTable, eq(fromCol, toCol));
341
+ query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
321
342
  currentTable = joinTable;
322
343
  }
323
344
 
@@ -419,8 +440,8 @@ export class RelationService {
419
440
  let query = this.db.select().from(targetTable).$dynamic();
420
441
 
421
442
  // Build the relation query with ALL parent IDs
422
- // @ts-expect-error buildRelationQuery uses dynamic queries
423
- query = DrizzleConditionBuilder.buildRelationQuery(
443
+ query = applyDynamicRelationQuery(
444
+ query,
424
445
  query,
425
446
  relation,
426
447
  parsedParentIds, // Pass array instead of single ID
@@ -513,8 +534,7 @@ export class RelationService {
513
534
  const toCol = joinTable[toColName as keyof typeof joinTable] as AnyPgColumn;
514
535
  if (!fromCol || !toCol) throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
515
536
 
516
- // @ts-expect-error Drizzle mutates base query generic on innerJoin
517
- query = query.innerJoin(joinTable, eq(fromCol, toCol));
537
+ query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
518
538
  currentTable = joinTable;
519
539
  }
520
540
 
@@ -549,7 +569,7 @@ export class RelationService {
549
569
  if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
550
570
  const junctionTable = this.registry.getTable(relation.through.table);
551
571
  if (!junctionTable) {
552
- console.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
572
+ logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
553
573
  return new Map();
554
574
  }
555
575
 
@@ -557,7 +577,7 @@ export class RelationService {
557
577
  const targetJunctionCol = junctionTable[relation.through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
558
578
 
559
579
  if (!sourceJunctionCol || !targetJunctionCol) {
560
- console.warn(`[batchFetchRelatedEntitiesMany] Junction columns not found in '${relation.through.table}'`);
580
+ logger.warn(`[batchFetchRelatedEntitiesMany] Junction columns not found in '${relation.through.table}'`);
561
581
  return new Map();
562
582
  }
563
583
 
@@ -597,8 +617,8 @@ export class RelationService {
597
617
  // Handle FK-based relations (one-to-many inverse)
598
618
  let query = this.db.select().from(targetTable).$dynamic();
599
619
 
600
- // @ts-expect-error buildRelationQuery uses dynamic queries
601
- query = DrizzleConditionBuilder.buildRelationQuery(
620
+ query = applyDynamicRelationQuery(
621
+ query,
602
622
  query,
603
623
  relation,
604
624
  parsedParentIds,
@@ -710,7 +730,7 @@ export class RelationService {
710
730
  }
711
731
 
712
732
  if (!junctionTable || !sourceJunctionColumn || !targetJunctionColumn) {
713
- console.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
733
+ logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
714
734
  continue;
715
735
  }
716
736
 
@@ -740,7 +760,7 @@ export class RelationService {
740
760
  // Handle many-to-many relations with junction table using 'through' property
741
761
  const junctionTable = this.registry.getTable(relation.through.table);
742
762
  if (!junctionTable) {
743
- console.warn(`Junction table '${relation.through.table}' not found for relation '${key}' in collection '${collection.slug}'`);
763
+ logger.warn(`Junction table '${relation.through.table}' not found for relation '${key}' in collection '${collection.slug}'`);
744
764
  continue;
745
765
  }
746
766
 
@@ -748,7 +768,7 @@ export class RelationService {
748
768
  const targetJunctionColumn = junctionTable[relation.through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
749
769
 
750
770
  if (!sourceJunctionColumn || !targetJunctionColumn) {
751
- console.warn(`Junction columns not found for relation '${key}'`);
771
+ logger.warn(`Junction columns not found for relation '${key}'`);
752
772
  continue;
753
773
  }
754
774
 
@@ -777,7 +797,7 @@ export class RelationService {
777
797
  } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
778
798
  // Inverse M2M relations should be saved from the owning side.
779
799
  // The owning collection manages the junction table rows.
780
- console.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
800
+ logger.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
781
801
  } else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
782
802
  // Handle one-to-many (inverse) by updating target FK to point to parent
783
803
  const targetTable = getTableForCollection(targetCollection, this.registry);
@@ -787,7 +807,7 @@ export class RelationService {
787
807
  const fkCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
788
808
 
789
809
  if (!fkCol || !targetIdCol) {
790
- console.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
810
+ logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
791
811
  continue;
792
812
  }
793
813
 
@@ -817,7 +837,7 @@ export class RelationService {
817
837
  .where(eq(fkCol, parsedParentId));
818
838
  }
819
839
  } else {
820
- console.warn(`Many relation '${key}' in collection '${collection.slug}' lacks write configuration and will be skipped during save.`);
840
+ logger.warn(`Many relation '${key}' in collection '${collection.slug}' lacks write configuration and will be skipped during save.`);
821
841
  }
822
842
  }
823
843
  }
@@ -895,13 +915,13 @@ export class RelationService {
895
915
 
896
916
  // Handle simple inverse relations
897
917
  if (!relation.foreignKeyOnTarget) {
898
- console.warn(`Inverse relation '${relation.relationName}' is missing foreignKeyOnTarget property. Skipping.`);
918
+ logger.warn(`Inverse relation '${relation.relationName}' is missing foreignKeyOnTarget property. Skipping.`);
899
919
  continue;
900
920
  }
901
921
 
902
922
  const foreignKeyColumn = targetTable[relation.foreignKeyOnTarget! as keyof typeof targetTable] as AnyPgColumn;
903
923
  if (!foreignKeyColumn) {
904
- console.warn(`Foreign key column '${relation.foreignKeyOnTarget}' not found in target table for relation '${relation.relationName}'`);
924
+ logger.warn(`Foreign key column '${relation.foreignKeyOnTarget}' not found in target table for relation '${relation.relationName}'`);
905
925
  continue;
906
926
  }
907
927
 
@@ -931,7 +951,7 @@ export class RelationService {
931
951
  .where(eq(targetIdField, parsedNewTargetId));
932
952
  }
933
953
  } catch (e) {
934
- console.warn(`Failed to update inverse relation '${relation.relationName}':`, e);
954
+ logger.warn(`Failed to update inverse relation '${relation.relationName}'`, { error: e });
935
955
  }
936
956
  }
937
957
  }
@@ -949,7 +969,7 @@ export class RelationService {
949
969
  ) {
950
970
  try {
951
971
  if (!relation.joinPath || relation.joinPath.length === 0) {
952
- console.warn(`Inverse relation '${relation.relationName}' missing joinPath`);
972
+ logger.warn(`Inverse relation '${relation.relationName}' missing joinPath`);
953
973
  return;
954
974
  }
955
975
 
@@ -967,7 +987,7 @@ export class RelationService {
967
987
  const junctionTable = this.registry.getTable(junctionTableName);
968
988
 
969
989
  if (!junctionTable) {
970
- console.warn(`Junction table '${junctionTableName}' not found for inverse joinPath relation '${relation.relationName}'`);
990
+ logger.warn(`Junction table '${junctionTableName}' not found for inverse joinPath relation '${relation.relationName}'`);
971
991
  return;
972
992
  }
973
993
 
@@ -996,7 +1016,7 @@ export class RelationService {
996
1016
  }
997
1017
 
998
1018
  if (!sourceJunctionColumn || !targetJunctionColumn) {
999
- console.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
1019
+ logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
1000
1020
  return;
1001
1021
  }
1002
1022
 
@@ -1041,7 +1061,7 @@ export class RelationService {
1041
1061
  }
1042
1062
  }
1043
1063
  } catch (error) {
1044
- console.error(`Failed to update inverse joinPath relation '${relation.relationName}':`, error);
1064
+ logger.error(`Failed to update inverse joinPath relation '${relation.relationName}'`, { error: error });
1045
1065
  throw error;
1046
1066
  }
1047
1067
  }
@@ -1061,7 +1081,7 @@ export class RelationService {
1061
1081
  try {
1062
1082
  const junctionTable = this.registry.getTable(junctionInfo.table);
1063
1083
  if (!junctionTable) {
1064
- console.warn(`Junction table '${junctionInfo.table}' not found for many-to-many inverse relation '${relation.relationName}'`);
1084
+ logger.warn(`Junction table '${junctionInfo.table}' not found for many-to-many inverse relation '${relation.relationName}'`);
1065
1085
  return;
1066
1086
  }
1067
1087
 
@@ -1069,7 +1089,7 @@ export class RelationService {
1069
1089
  const targetJunctionColumn = junctionTable[junctionInfo.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
1070
1090
 
1071
1091
  if (!sourceJunctionColumn || !targetJunctionColumn) {
1072
- console.warn(`Junction columns not found for relation '${relation.relationName}'`);
1092
+ logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
1073
1093
  return;
1074
1094
  }
1075
1095
 
@@ -1098,7 +1118,7 @@ export class RelationService {
1098
1118
  }
1099
1119
  }
1100
1120
  } catch (error) {
1101
- console.error(`Failed to update many-to-many inverse relation '${relation.relationName}':`, error);
1121
+ logger.error(`Failed to update many-to-many inverse relation '${relation.relationName}'`, { error: error });
1102
1122
  throw error;
1103
1123
  }
1104
1124
  }
@@ -1137,11 +1157,11 @@ export class RelationService {
1137
1157
  const targetFKCol = targetTable[targetFKColName as keyof typeof targetTable] as AnyPgColumn;
1138
1158
 
1139
1159
  if (!parentSourceCol) {
1140
- console.warn(`Parent source column '${parentSourceColName}' not found for joinPath relation '${relation.relationName}'`);
1160
+ logger.warn(`Parent source column '${parentSourceColName}' not found for joinPath relation '${relation.relationName}'`);
1141
1161
  continue;
1142
1162
  }
1143
1163
  if (!targetFKCol) {
1144
- console.warn(`Target FK column '${targetFKColName}' not found for joinPath relation '${relation.relationName}'`);
1164
+ logger.warn(`Target FK column '${targetFKColName}' not found for joinPath relation '${relation.relationName}'`);
1145
1165
  continue;
1146
1166
  }
1147
1167
 
@@ -1174,7 +1194,7 @@ export class RelationService {
1174
1194
  .set({ [targetFKColName]: null })
1175
1195
  .where(eq(targetFKCol, String(parentFKValue)));
1176
1196
  } else {
1177
- console.warn(`Cannot set joinPath relation '${relation.relationName}' because parent FK value is null/undefined`);
1197
+ logger.warn(`Cannot set joinPath relation '${relation.relationName}' because parent FK value is null/undefined`);
1178
1198
  continue;
1179
1199
  }
1180
1200
 
@@ -1239,7 +1259,7 @@ parentSourceColName };
1239
1259
  try {
1240
1260
  const junctionTable = this.registry.getTable(relation.through!.table);
1241
1261
  if (!junctionTable) {
1242
- console.warn(`Junction table '${relation.through!.table}' not found for relation '${relationKey}'`);
1262
+ logger.warn(`Junction table '${relation.through!.table}' not found for relation '${relationKey}'`);
1243
1263
  return;
1244
1264
  }
1245
1265
 
@@ -1247,7 +1267,7 @@ parentSourceColName };
1247
1267
  const targetJunctionColumn = junctionTable[relation.through!.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
1248
1268
 
1249
1269
  if (!sourceJunctionColumn || !targetJunctionColumn) {
1250
- console.warn(`Junction columns not found for relation '${relationKey}'`);
1270
+ logger.warn(`Junction columns not found for relation '${relationKey}'`);
1251
1271
  return;
1252
1272
  }
1253
1273
 
@@ -1265,9 +1285,9 @@ parentSourceColName };
1265
1285
 
1266
1286
  await tx.insert(junctionTable).values(junctionData);
1267
1287
 
1268
- console.log(`Created junction table entry for many-to-many relation '${relationKey}': ${JSON.stringify(junctionData)}`);
1288
+ logger.info(`Created junction table entry for many-to-many relation '${relationKey}': ${JSON.stringify(junctionData)}`);
1269
1289
  } catch (error) {
1270
- console.error(`Failed to create junction table entry for relation '${relationKey}':`, error);
1290
+ logger.error(`Failed to create junction table entry for relation '${relationKey}'`, { error: error });
1271
1291
  throw error;
1272
1292
  }
1273
1293
  }
@@ -75,7 +75,9 @@ export function getPrimaryKeys(collection: EntityCollection, registry: PostgresC
75
75
  const meta = getColumnMeta(col);
76
76
  const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
77
77
  const isUUID = meta.columnType === "PgUUID";
78
- keys.push({ fieldName: key, type, isUUID });
78
+ keys.push({ fieldName: key,
79
+ type,
80
+ isUUID });
79
81
  }
80
82
  }
81
83
 
@@ -86,7 +88,9 @@ export function getPrimaryKeys(collection: EntityCollection, registry: PostgresC
86
88
  const idMeta = getColumnMeta(idCol);
87
89
  const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
88
90
  const isUUID = idMeta.columnType === "PgUUID";
89
- keys.push({ fieldName: "id", type, isUUID });
91
+ keys.push({ fieldName: "id",
92
+ type,
93
+ isUUID });
90
94
  }
91
95
 
92
96
  return keys;
@@ -10,6 +10,8 @@ import { sql as drizzleSql } from "drizzle-orm";
10
10
  import { RealtimeProvider, CollectionSubscriptionConfig, EntitySubscriptionConfig } from "../interfaces";
11
11
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
12
12
  import { buildPropertyCallbacks } from "@rebasepro/common";
13
+ import { logger } from "@rebasepro/server-core";
14
+ import { sanitizeErrorForClient } from "../utils/pg-error-utils";
13
15
 
14
16
  /** Channel name used for Postgres LISTEN/NOTIFY cross-instance realtime. */
15
17
  const PG_NOTIFY_CHANNEL = "rebase_entity_changes";
@@ -218,7 +220,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
218
220
  });
219
221
 
220
222
  ws.on("error", (error) => {
221
- console.error("WebSocket error for client", clientId, error);
223
+ logger.error("WebSocket error for client", { detail: clientId, error });
222
224
  this.removeClient(clientId);
223
225
  });
224
226
  }
@@ -321,7 +323,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
321
323
  if (!collection) {
322
324
  const registered = this.registry.getCollections().map(c => c.slug).join(", ");
323
325
  const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
324
- console.error(`[RealtimeService] ${msg}`);
326
+ logger.error(`[RealtimeService] ${msg}`);
325
327
  this.sendError(clientId, msg, subscriptionId);
326
328
  return;
327
329
  }
@@ -360,7 +362,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
360
362
  this.sendCollectionUpdate(clientId, subscriptionId, entities);
361
363
 
362
364
  } catch (error) {
363
- this.sendError(clientId, `Failed to subscribe to collection: ${error}`, subscriptionId);
365
+ const sanitized = sanitizeErrorForClient(error, request.path);
366
+ this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
364
367
  }
365
368
  }
366
369
 
@@ -373,7 +376,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
373
376
  if (!collection) {
374
377
  const registered = this.registry.getCollections().map(c => c.slug).join(", ");
375
378
  const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
376
- console.error(`[RealtimeService] ${msg}`);
379
+ logger.error(`[RealtimeService] ${msg}`);
377
380
  this.sendError(clientId, msg, subscriptionId);
378
381
  return;
379
382
  }
@@ -397,7 +400,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
397
400
  this.sendEntityUpdate(clientId, subscriptionId, entity || null);
398
401
 
399
402
  } catch (error) {
400
- this.sendError(clientId, `Failed to subscribe to entity: ${request.path} ${request.entityId} ${error}`, subscriptionId);
403
+ const sanitized = sanitizeErrorForClient(error, request.path);
404
+ this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
401
405
  }
402
406
  }
403
407
 
@@ -441,7 +445,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
441
445
  try {
442
446
  await this.broadcastChange(path, entityId, databaseId);
443
447
  } catch (err) {
444
- console.error("❌ [RealtimeService] Failed to broadcast change via pg_notify:", err);
448
+ logger.error("❌ [RealtimeService] Failed to broadcast change via pg_notify", { error: err });
445
449
  }
446
450
  }
447
451
 
@@ -502,8 +506,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
502
506
  this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
503
507
  }
504
508
  } catch (error) {
505
- console.error(`❌ [RealtimeService] Error processing WebSocket subscription ${subscriptionId}:`, error);
506
- this.sendError(subscription.clientId, `Failed to process update for subscription ${subscriptionId}`, subscriptionId);
509
+ const sanitized = sanitizeErrorForClient(error, notifyPath);
510
+ this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
507
511
  }
508
512
  }
509
513
 
@@ -525,7 +529,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
525
529
  this.debouncedDriverRefetch(subscriptionId, notifyPath, subscription, callback);
526
530
  }
527
531
  } catch (error) {
528
- console.error(`❌ [RealtimeService] Error processing DataDriver subscription ${subscriptionId}:`, error);
532
+ logger.error(`❌ [RealtimeService] Error processing DataDriver subscription ${subscriptionId}`, { error: error });
529
533
  }
530
534
  }
531
535
  }
@@ -551,8 +555,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
551
555
  const entities = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest!, subscription.authContext);
552
556
  this.sendCollectionUpdate(subscription.clientId, subscriptionId, entities);
553
557
  } catch (error) {
554
- console.error(`❌ [RealtimeService] Error in debounced refetch for ${subscriptionId}:`, error);
555
- this.sendError(subscription.clientId, `Failed to process update for subscription ${subscriptionId}`, subscriptionId);
558
+ const sanitized = sanitizeErrorForClient(error, notifyPath);
559
+ this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
556
560
  }
557
561
  }, RealtimeService.REFETCH_DEBOUNCE_MS));
558
562
  }
@@ -577,7 +581,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
577
581
  const entities = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest!, subscription.authContext);
578
582
  callback(entities);
579
583
  } catch (error) {
580
- console.error(`❌ [RealtimeService] Error in debounced driver refetch for ${subscriptionId}:`, error);
584
+ logger.error(`❌ [RealtimeService] Error in debounced driver refetch for ${subscriptionId}`, { error: error });
581
585
  }
582
586
  }, RealtimeService.REFETCH_DEBOUNCE_MS));
583
587
  }
@@ -607,11 +611,13 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
607
611
  });
608
612
 
609
613
  // Always wrap in a transaction with session vars, defaulting to anonymous context if missing
610
- const activeAuth = authContext || { userId: "anon", roles: ["anon"] };
614
+ const activeAuth = authContext || { userId: "anon",
615
+ roles: ["anon"] };
611
616
  return await this.db.transaction(async (tx) => {
612
617
  await tx.execute(drizzleSql`SELECT set_config('app.user_id', ${activeAuth.userId}, true)`);
613
618
  await tx.execute(drizzleSql`SELECT set_config('app.user_roles', ${activeAuth.roles.join(",")}, true)`);
614
- await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: activeAuth.userId, roles: activeAuth.roles })}, true)`);
619
+ await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: activeAuth.userId,
620
+ roles: activeAuth.roles })}, true)`);
615
621
  const txEntityService = new EntityService(tx, this.registry);
616
622
  let fetchedEntities;
617
623
  if (collectionRequest.searchString) {
@@ -649,7 +655,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
649
655
 
650
656
  if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
651
657
  const contextForCallback = {
652
- user: { uid: activeAuth.userId, roles: activeAuth.roles },
658
+ user: { uid: activeAuth.userId,
659
+ roles: activeAuth.roles },
653
660
  driver: this.driver,
654
661
  data: (this.driver && "data" in this.driver) ? (this.driver as DataDriverWithData).data : undefined
655
662
  } as unknown as RebaseCallContext;
@@ -725,8 +732,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
725
732
  const entity = await this.fetchEntityWithAuth(notifyPath, entityId, subscription.authContext);
726
733
  this.sendEntityUpdate(subscription.clientId, subscriptionId, entity || null);
727
734
  } catch (error) {
728
- console.error(`❌ [RealtimeService] Error in debounced entity refetch for ${subscriptionId}:`, error);
729
- this.sendError(subscription.clientId, `Failed to process entity update for subscription ${subscriptionId}`, subscriptionId);
735
+ const sanitized = sanitizeErrorForClient(error, notifyPath);
736
+ this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
730
737
  }
731
738
  }, RealtimeService.REFETCH_DEBOUNCE_MS));
732
739
  }
@@ -752,7 +759,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
752
759
  const entity = await this.fetchEntityWithAuth(notifyPath, entityId, subscription.authContext);
753
760
  callback(entity || null);
754
761
  } catch (error) {
755
- console.error(`❌ [RealtimeService] Error in debounced entity driver refetch for ${subscriptionId}:`, error);
762
+ logger.error(`❌ [RealtimeService] Error in debounced entity driver refetch for ${subscriptionId}`, { error: error });
756
763
  }
757
764
  }, RealtimeService.REFETCH_DEBOUNCE_MS));
758
765
  }
@@ -774,11 +781,13 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
774
781
  });
775
782
 
776
783
  // Always wrap in a transaction with session vars, defaulting to anonymous context if missing
777
- const activeAuth = authContext || { userId: "anon", roles: ["anon"] };
784
+ const activeAuth = authContext || { userId: "anon",
785
+ roles: ["anon"] };
778
786
  return await this.db.transaction(async (tx) => {
779
787
  await tx.execute(drizzleSql`SELECT set_config('app.user_id', ${activeAuth.userId}, true)`);
780
788
  await tx.execute(drizzleSql`SELECT set_config('app.user_roles', ${activeAuth.roles.join(",")}, true)`);
781
- await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: activeAuth.userId, roles: activeAuth.roles })}, true)`);
789
+ await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: activeAuth.userId,
790
+ roles: activeAuth.roles })}, true)`);
782
791
  const txEntityService = new EntityService(tx, this.registry);
783
792
  let processedEntity = await txEntityService.fetchEntity(notifyPath, entityId, collection?.databaseId);
784
793
 
@@ -792,7 +801,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
792
801
 
793
802
  if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
794
803
  const contextForCallback = {
795
- user: { uid: activeAuth.userId, roles: activeAuth.roles },
804
+ user: { uid: activeAuth.userId,
805
+ roles: activeAuth.roles },
796
806
  driver: this.driver,
797
807
  data: (this.driver && "data" in this.driver) ? (this.driver as DataDriverWithData).data : undefined
798
808
  } as unknown as RebaseCallContext;
@@ -855,17 +865,19 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
855
865
  this.sendMessage(clientId, message);
856
866
  }
857
867
 
858
- private sendError(clientId: string, error: string, subscriptionId?: string) {
859
- console.error("Error handling collection subscription:", error);
868
+ private sendError(clientId: string, error: string, subscriptionId?: string, code?: string) {
860
869
  const message = {
861
870
  type: "error" as const,
862
871
  subscriptionId,
872
+ payload: {
873
+ error: code ? { message: error, code } : error
874
+ },
863
875
  error
864
876
  };
865
877
  this.sendMessage(clientId, message);
866
878
  }
867
879
 
868
- private sendMessage(clientId: string, message: CollectionUpdateMessage | EntityUpdateMessage | CollectionEntityPatchMessage | { type: string; subscriptionId?: string; error?: string }) {
880
+ private sendMessage(clientId: string, message: CollectionUpdateMessage | EntityUpdateMessage | CollectionEntityPatchMessage | { type: string; subscriptionId?: string; error?: string; payload?: unknown }) {
869
881
  const client = this.clients.get(clientId);
870
882
  if (client && client.readyState === WebSocket.OPEN) {
871
883
  client.send(JSON.stringify(message));
@@ -953,7 +965,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
953
965
  }
954
966
 
955
967
  const channelPresence = this.presence.get(channel)!;
956
- channelPresence.set(clientId, { state, lastSeen: Date.now() });
968
+ channelPresence.set(clientId, { state,
969
+ lastSeen: Date.now() });
957
970
 
958
971
  // Broadcast join / state update to channel
959
972
  this.broadcastPresenceDiff(channel, { [clientId]: state }, {});
@@ -1102,7 +1115,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
1102
1115
  */
1103
1116
  async startListening(connectionString: string): Promise<void> {
1104
1117
  if (this.broadcasting) {
1105
- console.warn("⚠️ [RealtimeService] startListening called but already listening. Ignoring.");
1118
+ logger.warn("⚠️ [RealtimeService] startListening called but already listening. Ignoring.");
1106
1119
  return;
1107
1120
  }
1108
1121
 
@@ -1111,7 +1124,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
1111
1124
  // works correctly if the initial connection attempt fails.
1112
1125
  this.broadcasting = true;
1113
1126
  await this.connectListenClient();
1114
- console.log(`📡 [RealtimeService] Cross-instance realtime enabled (instanceId: ${this.instanceId})`);
1127
+ logger.info(`📡 [RealtimeService] Cross-instance realtime enabled (instanceId: ${this.instanceId})`);
1115
1128
  }
1116
1129
 
1117
1130
  /**
@@ -1129,7 +1142,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
1129
1142
  } catch { /* ignore close errors */ }
1130
1143
  this.listenClient = undefined;
1131
1144
  }
1132
- console.log("📡 [RealtimeService] Cross-instance realtime disabled.");
1145
+ logger.info("📡 [RealtimeService] Cross-instance realtime disabled.");
1133
1146
  }
1134
1147
 
1135
1148
  /**
@@ -1156,13 +1169,13 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
1156
1169
  const client = new PgClient({ connectionString: this.listenConnectionString });
1157
1170
 
1158
1171
  client.on("error", (err) => {
1159
- console.error("❌ [RealtimeService] LISTEN client error:", err.message);
1172
+ logger.error("❌ [RealtimeService] LISTEN client error", { detail: err.message });
1160
1173
  this.scheduleReconnect();
1161
1174
  });
1162
1175
 
1163
1176
  client.on("end", () => {
1164
1177
  if (this.broadcasting) {
1165
- console.warn("⚠️ [RealtimeService] LISTEN client disconnected unexpectedly.");
1178
+ logger.warn("⚠️ [RealtimeService] LISTEN client disconnected unexpectedly.");
1166
1179
  this.scheduleReconnect();
1167
1180
  }
1168
1181
  });
@@ -1209,7 +1222,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
1209
1222
  // Trigger local fan-out with broadcast=false to avoid re-broadcasting
1210
1223
  await this.notifyEntityUpdate(p, eid, entity, db ?? undefined, false);
1211
1224
  } catch (err) {
1212
- console.error("❌ [RealtimeService] Error processing cross-instance notification:", err);
1225
+ logger.error("❌ [RealtimeService] Error processing cross-instance notification", { error: err });
1213
1226
  }
1214
1227
  });
1215
1228
 
@@ -1219,7 +1232,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
1219
1232
 
1220
1233
  this.debugLog(`📡 [RealtimeService] LISTEN client connected on channel "${PG_NOTIFY_CHANNEL}"`);
1221
1234
  } catch (err) {
1222
- console.error("❌ [RealtimeService] Failed to connect LISTEN client:", err);
1235
+ logger.error("❌ [RealtimeService] Failed to connect LISTEN client", { error: err });
1223
1236
  this.scheduleReconnect();
1224
1237
  }
1225
1238
  }
package/src/types.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
2
+
3
+ /** Drizzle PgTable with column access by name. Runtime Drizzle tables satisfy this shape. */
4
+ export type RebasePgTable = PgTable & Record<string, AnyPgColumn>;