@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.
- package/README.md +69 -89
- package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
- package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
- package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
- package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
- package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
- package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
- package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
- package/dist/index.es.js +10174 -11184
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +10735 -11462
- package/dist/index.umd.js.map +1 -1
- package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
- package/dist/types.d.ts +3 -0
- package/dist/utils/pg-error-utils.d.ts +55 -0
- package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
- package/package.json +24 -21
- package/src/PostgresAdapter.ts +9 -10
- package/src/PostgresBackendDriver.ts +135 -122
- package/src/PostgresBootstrapper.ts +90 -16
- package/src/auth/ensure-tables.ts +28 -5
- package/src/auth/services.ts +56 -45
- package/src/cli.ts +140 -110
- package/src/collections/PostgresCollectionRegistry.ts +7 -0
- package/src/connection.ts +11 -6
- package/src/data-transformer.ts +73 -109
- package/src/databasePoolManager.ts +5 -3
- package/src/history/HistoryService.ts +3 -2
- package/src/history/ensure-history-table.ts +5 -4
- package/src/schema/auth-schema.ts +1 -2
- package/src/schema/doctor-cli.ts +2 -1
- package/src/schema/doctor.ts +40 -37
- package/src/schema/generate-drizzle-schema-logic.ts +56 -18
- package/src/schema/generate-drizzle-schema.ts +11 -11
- package/src/schema/introspect-db-inference.ts +25 -25
- package/src/schema/introspect-db-logic.ts +38 -38
- package/src/schema/introspect-db.ts +28 -27
- package/src/services/BranchService.ts +14 -0
- package/src/services/EntityFetchService.ts +28 -25
- package/src/services/EntityPersistService.ts +11 -124
- package/src/services/RelationService.ts +57 -37
- package/src/services/entity-helpers.ts +6 -2
- package/src/services/realtimeService.ts +45 -32
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +31 -15
- package/src/utils/pg-error-utils.ts +211 -0
- package/src/websocket.ts +51 -33
- package/test/auth-services.test.ts +36 -19
- package/test/batch-many-to-many-regression.test.ts +119 -39
- package/test/data-transformer-hardening.test.ts +67 -33
- package/test/data-transformer.test.ts +4 -2
- package/test/doctor.test.ts +10 -5
- package/test/drizzle-conditions.test.ts +59 -6
- package/test/generate-drizzle-schema.test.ts +65 -40
- package/test/introspect-db-generation.test.ts +179 -81
- package/test/introspect-db-utils.test.ts +92 -37
- package/test/mocks/chalk.cjs +7 -0
- package/test/pg-error-utils.test.ts +221 -0
- package/test/postgresDataDriver.test.ts +14 -5
- package/test/property-ordering.test.ts +126 -79
- package/test/realtimeService.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +84 -36
- package/test/relations.test.ts +247 -0
- package/test/unmapped-tables-safety.test.ts +14 -6
- package/test/websocket.test.ts +1 -1
- package/tsconfig.json +5 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -6
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -536
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -941
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1181
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/drizzle.test.config.ts +0 -10
- /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
- /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
- /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
- /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
- /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
- /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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1160
|
+
logger.warn(`Parent source column '${parentSourceColName}' not found for joinPath relation '${relation.relationName}'`);
|
|
1141
1161
|
continue;
|
|
1142
1162
|
}
|
|
1143
1163
|
if (!targetFKCol) {
|
|
1144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1288
|
+
logger.info(`Created junction table entry for many-to-many relation '${relationKey}': ${JSON.stringify(junctionData)}`);
|
|
1269
1289
|
} catch (error) {
|
|
1270
|
-
|
|
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,
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
this.sendError(subscription.clientId,
|
|
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
|
-
|
|
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
|
-
|
|
555
|
-
this.sendError(subscription.clientId,
|
|
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
|
-
|
|
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",
|
|
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,
|
|
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,
|
|
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
|
-
|
|
729
|
-
this.sendError(subscription.clientId,
|
|
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
|
-
|
|
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",
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1235
|
+
logger.error("❌ [RealtimeService] Failed to connect LISTEN client", { error: err });
|
|
1223
1236
|
this.scheduleReconnect();
|
|
1224
1237
|
}
|
|
1225
1238
|
}
|
package/src/types.ts
ADDED