@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

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 (196) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +58 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +22 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +192 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,518 @@
1
+ import { RealtimeService } from "./services/realtimeService";
2
+ import { PostgresBackendDriver } from "./PostgresBackendDriver";
3
+ import { DataDriver, DeleteEntityProps, FetchCollectionProps, FetchEntityProps, SaveEntityProps, TableMetadata, BranchInfo, isSQLAdmin, isSchemaAdmin } from "@rebasepro/types";
4
+ import { WebSocketServer, WebSocket } from "ws";
5
+ import { Server } from "http";
6
+ import { inspect } from "util";
7
+ // @ts-ignore
8
+ import { extractUserFromToken, AccessTokenPayload } from "@rebasepro/server-core";
9
+ // @ts-ignore
10
+ import { AuthConfig } from "@rebasepro/server-core";
11
+
12
+ interface ClientSession {
13
+ ws: WebSocket;
14
+ user?: AccessTokenPayload;
15
+ authenticated: boolean;
16
+ /** Sliding window message counter for rate limiting */
17
+ messageCount: number;
18
+ messageWindowStart: number;
19
+ }
20
+
21
+ const clientSessions = new Map<string, ClientSession>();
22
+
23
+ /** Maximum messages per client per window */
24
+ const WS_RATE_LIMIT = 2000;
25
+ /** Rate limit window in milliseconds (60 seconds) */
26
+ const WS_RATE_WINDOW_MS = 60_000;
27
+
28
+ /** Admin-only WebSocket message types */
29
+ const ADMIN_ONLY_TYPES = new Set([
30
+ "EXECUTE_SQL",
31
+ "FETCH_DATABASES",
32
+ "FETCH_ROLES",
33
+ "FETCH_UNMAPPED_TABLES",
34
+ "FETCH_TABLE_METADATA",
35
+ "FETCH_CURRENT_DATABASE",
36
+ "CREATE_BRANCH",
37
+ "DELETE_BRANCH",
38
+ "LIST_BRANCHES"
39
+ ]);
40
+
41
+ /**
42
+ * Check if the current session belongs to an admin user.
43
+ */
44
+ function isAdminSession(session: ClientSession | undefined): boolean {
45
+ if (!session?.user?.roles) return false;
46
+ return session.user.roles.some((r: unknown) => {
47
+ if (typeof r === "string") return r === "admin";
48
+ if (r && typeof r === "object" && "isAdmin" in r) return (r as { isAdmin: boolean }).isAdmin;
49
+ if (r && typeof r === "object" && "id" in r) return (r as { id: string }).id === "admin";
50
+ return false;
51
+ });
52
+ }
53
+
54
+ export function createPostgresWebSocket(
55
+ server: Server,
56
+ realtimeService: RealtimeService,
57
+ driver: PostgresBackendDriver,
58
+ authConfig?: AuthConfig
59
+ ) {
60
+ const isProduction = process.env.NODE_ENV === "production";
61
+ /** Debug logger that is suppressed in production to prevent PII/data leaks */
62
+ const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };
63
+ const wss = new WebSocketServer({ server });
64
+
65
+ // Handle errors on the WSS so that EADDRINUSE from the underlying HTTP
66
+ // server doesn't surface as an unhandled 'error' event and crash the
67
+ // process. The dev-mode `listenWithPortRetry` utility handles retry
68
+ // logic on the HTTP server side — we just need the WSS not to throw.
69
+ wss.on("error", (err: NodeJS.ErrnoException) => {
70
+ if (err.code === "EADDRINUSE") {
71
+ // Silently absorbed — listenWithPortRetry will retry the next port
72
+ return;
73
+ }
74
+ console.error("❌ [WebSocket Server] Error:", err);
75
+ });
76
+
77
+ const requireAuth = authConfig?.requireAuth !== false && authConfig?.jwtSecret;
78
+
79
+ wss.on("connection", (ws) => {
80
+ const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
81
+ wsDebug(`WebSocket client connected: ${clientId}`);
82
+
83
+ // Initialize client session
84
+ clientSessions.set(clientId, { ws,
85
+ authenticated: !requireAuth,
86
+ messageCount: 0,
87
+ messageWindowStart: Date.now() });
88
+ realtimeService.addClient(clientId, ws);
89
+
90
+ ws.on("close", () => {
91
+ wsDebug(`WebSocket client disconnected: ${clientId}`);
92
+ clientSessions.delete(clientId);
93
+ });
94
+
95
+ // Route all messages through RealtimeService for unified handling
96
+ ws.on("message", async (message) => {
97
+ let requestId: string | undefined;
98
+ try {
99
+ const {
100
+ type,
101
+ payload,
102
+ requestId: reqId
103
+ } = JSON.parse(message.toString());
104
+ requestId = reqId; // Capture requestId for use in catch block
105
+
106
+ wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : "");
107
+
108
+ // Handle authentication first
109
+ // Helper: send a canonical error frame
110
+ const sendError = (errType: "ERROR" | "AUTH_ERROR", code: string, msg: string) => {
111
+ ws.send(JSON.stringify({
112
+ type: errType,
113
+ requestId,
114
+ payload: { error: { message: msg,
115
+ code } }
116
+ }));
117
+ };
118
+
119
+ if (type === "AUTHENTICATE") {
120
+ const { token } = payload || {};
121
+ if (!token) {
122
+ sendError("AUTH_ERROR", "INVALID_INPUT", "Token is required");
123
+ return;
124
+ }
125
+
126
+ const user = extractUserFromToken(token);
127
+ if (user) {
128
+ const session = clientSessions.get(clientId);
129
+ if (session) {
130
+ session.user = user;
131
+ session.authenticated = true;
132
+ }
133
+ wsDebug(`[WS] replying AUTH_SUCCESS for requestId ${requestId}`);
134
+ ws.send(JSON.stringify({
135
+ type: "AUTH_SUCCESS",
136
+ requestId,
137
+ payload: { userId: user.userId,
138
+ roles: user.roles }
139
+ }));
140
+ wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${user.userId}`);
141
+ } else {
142
+ wsDebug(`[WS] replying AUTH_ERROR for requestId ${requestId} (invalid token)`);
143
+ sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
144
+ }
145
+ return;
146
+ }
147
+
148
+ // Check authentication for protected operations
149
+ if (requireAuth) {
150
+ const session = clientSessions.get(clientId);
151
+ if (!session?.authenticated) {
152
+ sendError("ERROR", "UNAUTHORIZED", "Authentication required");
153
+ return;
154
+ }
155
+ }
156
+
157
+ // Rate limiting: reject if client exceeds message limit
158
+ {
159
+ const session = clientSessions.get(clientId);
160
+ if (session) {
161
+ const now = Date.now();
162
+ if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {
163
+ session.messageCount = 0;
164
+ session.messageWindowStart = now;
165
+ }
166
+ session.messageCount++;
167
+ if (session.messageCount > WS_RATE_LIMIT) {
168
+ sendError("ERROR", "RATE_LIMITED", "Too many requests. Please slow down.");
169
+ return;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Admin-only operations require admin role
175
+ if (ADMIN_ONLY_TYPES.has(type)) {
176
+ const session = clientSessions.get(clientId);
177
+ if (!isAdminSession(session)) {
178
+ sendError("ERROR", "FORBIDDEN", "Admin access required for this operation");
179
+ return;
180
+ }
181
+ }
182
+
183
+ // Helper to get correctly scoped delegate for the current request
184
+ const getScopedDelegate = async (): Promise<DataDriver> => {
185
+ const session = clientSessions.get(clientId);
186
+ if (session?.user && "withAuth" in driver && typeof (driver as unknown as Record<string, unknown>).withAuth === "function") {
187
+ try {
188
+ // Map AccessTokenPayload back to User interface for withAuth (roles are already string IDs from JWT)
189
+ const userForAuth: Record<string, unknown> = {
190
+ uid: session.user.userId,
191
+ roles: session.user.roles ?? []
192
+ };
193
+ return await (driver as unknown as { withAuth: (user: Record<string, unknown>) => Promise<DataDriver> }).withAuth(userForAuth);
194
+ } catch (e) {
195
+ console.error("Failed to create authenticated delegate for WS request", e);
196
+ return driver;
197
+ }
198
+ }
199
+ return driver;
200
+ };
201
+
202
+ switch (type) {
203
+ case "FETCH_COLLECTION": {
204
+ wsDebug("📋 [WebSocket Server] Processing FETCH_COLLECTION request");
205
+ const request: FetchCollectionProps = payload;
206
+ const delegate = await getScopedDelegate();
207
+ const entities = await delegate.fetchCollection(request);
208
+ wsDebug("📋 [WebSocket Server] FETCH_COLLECTION result - entities count:", entities.length);
209
+ const response = {
210
+ type: "FETCH_COLLECTION_SUCCESS",
211
+ payload: { entities },
212
+ requestId
213
+ };
214
+ wsDebug("📋 [WebSocket Server] Sending FETCH_COLLECTION_SUCCESS response");
215
+ ws.send(JSON.stringify(response));
216
+ }
217
+ break;
218
+
219
+ case "FETCH_ENTITY": {
220
+ wsDebug("📄 [WebSocket Server] Processing FETCH_ENTITY request");
221
+ const request: FetchEntityProps = payload;
222
+ const delegate = await getScopedDelegate();
223
+ const entity = await delegate.fetchEntity(request);
224
+ wsDebug("📄 [WebSocket Server] FETCH_ENTITY result:", entity);
225
+ const response = {
226
+ type: "FETCH_ENTITY_SUCCESS",
227
+ payload: { entity },
228
+ requestId
229
+ };
230
+ wsDebug("📄 [WebSocket Server] Sending FETCH_ENTITY_SUCCESS response");
231
+ ws.send(JSON.stringify(response));
232
+ }
233
+ break;
234
+
235
+ case "SAVE_ENTITY": {
236
+ wsDebug("💾 [WebSocket Server] Processing SAVE_ENTITY request");
237
+ const request: SaveEntityProps = payload;
238
+ wsDebug("💾 [WebSocket Server] Saving entity with request:", inspect(request, { depth: null,
239
+ colors: true }));
240
+ const delegate = await getScopedDelegate();
241
+ const entity = await delegate.saveEntity(request);
242
+ wsDebug("💾 [WebSocket Server] SAVE_ENTITY result:", inspect(entity, { depth: null,
243
+ colors: true }));
244
+ const response = {
245
+ type: "SAVE_ENTITY_SUCCESS",
246
+ payload: { entity },
247
+ requestId
248
+ };
249
+ wsDebug("💾 [WebSocket Server] Sending SAVE_ENTITY_SUCCESS response");
250
+ ws.send(JSON.stringify(response));
251
+ }
252
+ break;
253
+
254
+ case "DELETE_ENTITY": {
255
+ wsDebug("🗑️ [WebSocket Server] Processing DELETE_ENTITY request");
256
+ const request: DeleteEntityProps = payload;
257
+ wsDebug("🗑️ [WebSocket Server] Deleting entity:", request.entity);
258
+ const delegate = await getScopedDelegate();
259
+ await delegate.deleteEntity(request);
260
+ wsDebug("🗑️ [WebSocket Server] DELETE_ENTITY completed successfully");
261
+ const response = {
262
+ type: "DELETE_ENTITY_SUCCESS",
263
+ payload: { success: true },
264
+ requestId
265
+ };
266
+ wsDebug("🗑️ [WebSocket Server] Sending DELETE_ENTITY_SUCCESS response");
267
+ ws.send(JSON.stringify(response));
268
+ }
269
+ break;
270
+
271
+ case "CHECK_UNIQUE_FIELD": {
272
+ wsDebug("🔍 [WebSocket Server] Processing CHECK_UNIQUE_FIELD request");
273
+ const {
274
+ path,
275
+ name,
276
+ value,
277
+ entityId,
278
+ collection
279
+ } = payload;
280
+ const delegate = await getScopedDelegate();
281
+ const isUnique = await delegate.checkUniqueField(path, name, value, entityId, collection);
282
+ wsDebug("🔍 [WebSocket Server] CHECK_UNIQUE_FIELD result:", isUnique);
283
+ const response = {
284
+ type: "CHECK_UNIQUE_FIELD_SUCCESS",
285
+ payload: { isUnique },
286
+ requestId
287
+ };
288
+ wsDebug("🔍 [WebSocket Server] Sending CHECK_UNIQUE_FIELD_SUCCESS response");
289
+ ws.send(JSON.stringify(response));
290
+ }
291
+ break;
292
+
293
+
294
+ case "COUNT_ENTITIES": {
295
+ const request: FetchCollectionProps = payload;
296
+ const delegate = await getScopedDelegate();
297
+ const count = await delegate.countEntities!(request);
298
+ const response = {
299
+ type: "COUNT_ENTITIES_SUCCESS",
300
+ payload: { count },
301
+ requestId
302
+ };
303
+ ws.send(JSON.stringify(response));
304
+ }
305
+ break;
306
+
307
+ case "EXECUTE_SQL": {
308
+ const { sql, options } = payload;
309
+ const delegate = await getScopedDelegate();
310
+ const admin = delegate.admin;
311
+ if (!isSQLAdmin(admin)) {
312
+ sendError("ERROR", "NOT_SUPPORTED", "SQL execution is not available for this driver.");
313
+ break;
314
+ }
315
+ const result = await admin.executeSql(sql, options);
316
+ if (process.env.NODE_ENV !== "production") {
317
+ wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : "non-array"} rows.`);
318
+ }
319
+ const response = {
320
+ type: "EXECUTE_SQL_SUCCESS",
321
+ payload: { result },
322
+ requestId
323
+ };
324
+ ws.send(JSON.stringify(response));
325
+ }
326
+ break;
327
+
328
+ case "FETCH_DATABASES": {
329
+ wsDebug("📚 [WebSocket Server] Processing FETCH_DATABASES request");
330
+ const delegate = await getScopedDelegate();
331
+ const admin = delegate.admin;
332
+ let databases: string[] = [];
333
+ if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) {
334
+ databases = await admin.fetchAvailableDatabases();
335
+ }
336
+ wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);
337
+ const response = {
338
+ type: "FETCH_DATABASES_SUCCESS",
339
+ payload: { databases },
340
+ requestId
341
+ };
342
+ ws.send(JSON.stringify(response));
343
+ }
344
+ break;
345
+
346
+ case "FETCH_ROLES": {
347
+ wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
348
+ const delegate = await getScopedDelegate();
349
+ const admin = delegate.admin;
350
+ let roles: string[] = [];
351
+ if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
352
+ roles = await admin.fetchAvailableRoles();
353
+ }
354
+ wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} roles.`);
355
+ const response = {
356
+ type: "FETCH_ROLES_SUCCESS",
357
+ payload: { roles },
358
+ requestId
359
+ };
360
+ ws.send(JSON.stringify(response));
361
+ }
362
+ break;
363
+
364
+ case "FETCH_CURRENT_DATABASE": {
365
+ wsDebug("📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request");
366
+ const delegate = await getScopedDelegate();
367
+ const admin = delegate.admin;
368
+ let database: string | undefined = undefined;
369
+ if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) {
370
+ database = await admin.fetchCurrentDatabase();
371
+ }
372
+ const response = {
373
+ type: "FETCH_CURRENT_DATABASE_SUCCESS",
374
+ payload: { database },
375
+ requestId
376
+ };
377
+ ws.send(JSON.stringify(response));
378
+ }
379
+ break;
380
+
381
+ case "FETCH_UNMAPPED_TABLES": {
382
+ wsDebug("📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request");
383
+ const delegate = await getScopedDelegate();
384
+ const admin = delegate.admin;
385
+ let tables: string[] = [];
386
+ if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) {
387
+ tables = await admin.fetchUnmappedTables(payload?.mappedPaths);
388
+ }
389
+ wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);
390
+ const response = {
391
+ type: "FETCH_UNMAPPED_TABLES_SUCCESS",
392
+ payload: { tables },
393
+ requestId
394
+ };
395
+ ws.send(JSON.stringify(response));
396
+ }
397
+ break;
398
+
399
+ case "FETCH_TABLE_METADATA": {
400
+ wsDebug("📋 [WebSocket Server] Processing FETCH_TABLE_METADATA request");
401
+ const { tableName } = payload;
402
+ const delegate = await getScopedDelegate();
403
+ const admin = delegate.admin;
404
+ let metadata: TableMetadata | undefined;
405
+ if (isSchemaAdmin(admin) && admin.fetchTableMetadata) {
406
+ metadata = await admin.fetchTableMetadata(tableName) as TableMetadata;
407
+ }
408
+ wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);
409
+ const response = {
410
+ type: "FETCH_TABLE_METADATA_SUCCESS",
411
+ payload: { metadata },
412
+ requestId
413
+ };
414
+ ws.send(JSON.stringify(response));
415
+ }
416
+ break;
417
+
418
+ case "CREATE_BRANCH": {
419
+ wsDebug("🌿 [WebSocket Server] Processing CREATE_BRANCH request");
420
+ const { name, options } = payload;
421
+ const delegate = await getScopedDelegate();
422
+ if (!delegate.admin?.createBranch) {
423
+ sendError("ERROR", "NOT_SUPPORTED", "Database branching is not available. Configure adminConnectionString.");
424
+ break;
425
+ }
426
+ const branch: BranchInfo = await delegate.admin.createBranch(name, options);
427
+ wsDebug(`🌿 [WebSocket Server] Branch created: ${branch.name}`);
428
+ const response = {
429
+ type: "CREATE_BRANCH_SUCCESS",
430
+ payload: { branch },
431
+ requestId
432
+ };
433
+ ws.send(JSON.stringify(response));
434
+ }
435
+ break;
436
+
437
+ case "DELETE_BRANCH": {
438
+ wsDebug("🗑️ [WebSocket Server] Processing DELETE_BRANCH request");
439
+ const { name: branchName } = payload;
440
+ const delegate = await getScopedDelegate();
441
+ if (!delegate.admin?.deleteBranch) {
442
+ sendError("ERROR", "NOT_SUPPORTED", "Database branching is not available.");
443
+ break;
444
+ }
445
+ await delegate.admin.deleteBranch(branchName);
446
+ wsDebug(`🗑️ [WebSocket Server] Branch deleted: ${branchName}`);
447
+ const response = {
448
+ type: "DELETE_BRANCH_SUCCESS",
449
+ payload: { success: true },
450
+ requestId
451
+ };
452
+ ws.send(JSON.stringify(response));
453
+ }
454
+ break;
455
+
456
+ case "LIST_BRANCHES": {
457
+ wsDebug("🌿 [WebSocket Server] Processing LIST_BRANCHES request");
458
+ const delegate = await getScopedDelegate();
459
+ let branches: BranchInfo[] = [];
460
+ if (delegate.admin?.listBranches) {
461
+ branches = await delegate.admin.listBranches();
462
+ }
463
+ wsDebug(`🌿 [WebSocket Server] Listed ${branches.length} branches.`);
464
+ const response = {
465
+ type: "LIST_BRANCHES_SUCCESS",
466
+ payload: { branches },
467
+ requestId
468
+ };
469
+ ws.send(JSON.stringify(response));
470
+ }
471
+ break;
472
+
473
+ // Route subscription messages to RealtimeService
474
+ case "subscribe_collection":
475
+ case "subscribe_entity":
476
+ case "unsubscribe": {
477
+ wsDebug("🔄 [WebSocket Server] Routing subscription message to RealtimeService:", type);
478
+ // Attach auth context from the WS session so RLS-aware refetches work
479
+ const session = clientSessions.get(clientId);
480
+ const authContext = session?.user
481
+ ? { userId: session.user.userId,
482
+ roles: session.user.roles ?? [] }
483
+ : undefined;
484
+ // Let RealtimeService handle these messages
485
+ await realtimeService.handleClientMessage(clientId, {
486
+ type,
487
+ payload,
488
+ subscriptionId: payload?.subscriptionId
489
+ }, authContext);
490
+ break;
491
+ }
492
+
493
+ default:
494
+ console.error("❌ [WebSocket Server] Unknown message type:", type);
495
+ }
496
+ } catch (error: unknown) {
497
+ console.error("💥 [WebSocket Server] Error handling message:", error);
498
+ if (error instanceof Error) {
499
+ console.error("Stack trace:", error.stack);
500
+ }
501
+ const errorMessage = process.env.NODE_ENV === "production"
502
+ ? "An unexpected error occurred"
503
+ : (error instanceof Error ? error.message : "An unexpected error occurred");
504
+ const errorResponse = {
505
+ type: "ERROR",
506
+ requestId,
507
+ payload: {
508
+ error: {
509
+ message: errorMessage,
510
+ code: "INTERNAL_ERROR"
511
+ }
512
+ }
513
+ };
514
+ ws.send(JSON.stringify(errorResponse));
515
+ }
516
+ });
517
+ });
518
+ }