@rebasepro/server-mongo 0.0.1-canary.4829d6e
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/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/MongoBootstrapper.d.ts +18 -0
- package/dist/MongoBootstrapper.d.ts.map +1 -0
- package/dist/auth/ensure-collections.d.ts +3 -0
- package/dist/auth/ensure-collections.d.ts.map +1 -0
- package/dist/auth/services.d.ts +156 -0
- package/dist/auth/services.d.ts.map +1 -0
- package/dist/connection.d.ts +35 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/db/MongoConditionBuilder.d.ts +64 -0
- package/dist/db/MongoConditionBuilder.d.ts.map +1 -0
- package/dist/db/MongoDataService.d.ts +101 -0
- package/dist/db/MongoDataService.d.ts.map +1 -0
- package/dist/ensure-collections-Bkx_O5CQ.js +94 -0
- package/dist/ensure-collections-Bkx_O5CQ.js.map +1 -0
- package/dist/ensure-history-collection-yajOt2dv.js +21 -0
- package/dist/ensure-history-collection-yajOt2dv.js.map +1 -0
- package/dist/factory.d.ts +151 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/history/ensure-history-collection.d.ts +3 -0
- package/dist/history/ensure-history-collection.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +2508 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2925 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/services/MongoDriver.d.ts +125 -0
- package/dist/services/MongoDriver.d.ts.map +1 -0
- package/dist/services/MongoHistoryService.d.ts +37 -0
- package/dist/services/MongoHistoryService.d.ts.map +1 -0
- package/dist/services/MongoRealtimeService.d.ts +103 -0
- package/dist/services/MongoRealtimeService.d.ts.map +1 -0
- package/dist/websocket-DQlwCHFq.js +281 -0
- package/dist/websocket-DQlwCHFq.js.map +1 -0
- package/dist/websocket.d.ts +7 -0
- package/dist/websocket.d.ts.map +1 -0
- package/package.json +81 -0
- package/src/MongoBootstrapper.ts +196 -0
- package/src/auth/ensure-collections.ts +105 -0
- package/src/auth/services.ts +732 -0
- package/src/connection.ts +60 -0
- package/src/db/MongoConditionBuilder.ts +224 -0
- package/src/db/MongoDataService.ts +368 -0
- package/src/factory.ts +305 -0
- package/src/history/ensure-history-collection.ts +22 -0
- package/src/index.ts +25 -0
- package/src/services/MongoDriver.ts +1120 -0
- package/src/services/MongoHistoryService.ts +181 -0
- package/src/services/MongoRealtimeService.ts +446 -0
- package/src/websocket.ts +297 -0
package/src/websocket.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User } from "@rebasepro/types";
|
|
2
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
3
|
+
import { Server } from "http";
|
|
4
|
+
import { inspect } from "util";
|
|
5
|
+
import type { AccessTokenPayload } from "@rebasepro/server";
|
|
6
|
+
import { extractUserFromToken } from "@rebasepro/server";
|
|
7
|
+
import type { RebaseAuthConfig } from "@rebasepro/server";
|
|
8
|
+
import { MongoRealtimeService } from "./services/MongoRealtimeService";
|
|
9
|
+
import { MongoDriver } from "./services/MongoDriver";
|
|
10
|
+
import { logger } from "@rebasepro/server";
|
|
11
|
+
|
|
12
|
+
interface DriverWithAuth extends DataDriver {
|
|
13
|
+
withAuth(user: Record<string, unknown>): Promise<DataDriver>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isDriverWithAuth(driver: DataDriver): driver is DriverWithAuth {
|
|
17
|
+
return "withAuth" in driver && typeof (driver as Record<string, unknown>).withAuth === "function";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ClientSession {
|
|
21
|
+
ws: WebSocket;
|
|
22
|
+
user?: AccessTokenPayload;
|
|
23
|
+
authenticated: boolean;
|
|
24
|
+
messageCount: number;
|
|
25
|
+
messageWindowStart: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const clientSessions = new Map<string, ClientSession>();
|
|
29
|
+
const WS_RATE_LIMIT = 2000;
|
|
30
|
+
const WS_RATE_WINDOW_MS = 60_000;
|
|
31
|
+
|
|
32
|
+
const ADMIN_ONLY_TYPES = new Set([
|
|
33
|
+
"EXECUTE_SQL",
|
|
34
|
+
"FETCH_DATABASES",
|
|
35
|
+
"FETCH_ROLES",
|
|
36
|
+
"FETCH_UNMAPPED_TABLES",
|
|
37
|
+
"FETCH_TABLE_METADATA",
|
|
38
|
+
"FETCH_CURRENT_DATABASE",
|
|
39
|
+
"CREATE_BRANCH",
|
|
40
|
+
"DELETE_BRANCH",
|
|
41
|
+
"LIST_BRANCHES"
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function isAdminSession(session: ClientSession | undefined): boolean {
|
|
45
|
+
if (!session?.user?.roles) return false;
|
|
46
|
+
return session.user.roles.includes("admin");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createMongoWebSocket(
|
|
50
|
+
server: Server,
|
|
51
|
+
realtimeService: MongoRealtimeService,
|
|
52
|
+
driver: MongoDriver,
|
|
53
|
+
authConfig?: RebaseAuthConfig,
|
|
54
|
+
admin?: DatabaseAdmin
|
|
55
|
+
) {
|
|
56
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
57
|
+
const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };
|
|
58
|
+
const wss = new WebSocketServer({ server });
|
|
59
|
+
|
|
60
|
+
wss.on("error", (err: NodeJS.ErrnoException) => {
|
|
61
|
+
if (err.code === "EADDRINUSE") return;
|
|
62
|
+
logger.error("❌ [WebSocket Server] Error", { error: err });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const requireAuth = authConfig?.requireAuth !== false && authConfig?.jwtSecret;
|
|
66
|
+
|
|
67
|
+
wss.on("connection", (ws) => {
|
|
68
|
+
const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
69
|
+
wsDebug(`WebSocket client connected: ${clientId}`);
|
|
70
|
+
|
|
71
|
+
clientSessions.set(clientId, { ws,
|
|
72
|
+
authenticated: !requireAuth,
|
|
73
|
+
messageCount: 0,
|
|
74
|
+
messageWindowStart: Date.now() });
|
|
75
|
+
realtimeService.addClient(clientId, ws);
|
|
76
|
+
|
|
77
|
+
ws.on("close", () => {
|
|
78
|
+
wsDebug(`WebSocket client disconnected: ${clientId}`);
|
|
79
|
+
clientSessions.delete(clientId);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
ws.on("message", async (message) => {
|
|
83
|
+
let requestId: string | undefined;
|
|
84
|
+
try {
|
|
85
|
+
const { type, payload, requestId: reqId } = JSON.parse(message.toString());
|
|
86
|
+
requestId = reqId;
|
|
87
|
+
|
|
88
|
+
wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : "");
|
|
89
|
+
|
|
90
|
+
const sendError = (errType: "ERROR" | "AUTH_ERROR", code: string, msg: string) => {
|
|
91
|
+
ws.send(JSON.stringify({ type: errType,
|
|
92
|
+
requestId,
|
|
93
|
+
payload: { error: { message: msg,
|
|
94
|
+
code } } }));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (type === "AUTHENTICATE") {
|
|
98
|
+
const { token } = payload || {};
|
|
99
|
+
if (!token) {
|
|
100
|
+
sendError("AUTH_ERROR", "INVALID_INPUT", "Token is required");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const user = extractUserFromToken(token);
|
|
105
|
+
if (user) {
|
|
106
|
+
const session = clientSessions.get(clientId);
|
|
107
|
+
if (session) {
|
|
108
|
+
session.user = user;
|
|
109
|
+
session.authenticated = true;
|
|
110
|
+
}
|
|
111
|
+
ws.send(JSON.stringify({ type: "AUTH_SUCCESS",
|
|
112
|
+
requestId,
|
|
113
|
+
payload: { userId: user.userId,
|
|
114
|
+
roles: user.roles } }));
|
|
115
|
+
} else {
|
|
116
|
+
sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (requireAuth) {
|
|
122
|
+
const session = clientSessions.get(clientId);
|
|
123
|
+
if (!session?.authenticated) {
|
|
124
|
+
sendError("ERROR", "UNAUTHORIZED", "Authentication required");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
const session = clientSessions.get(clientId);
|
|
131
|
+
if (session) {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {
|
|
134
|
+
session.messageCount = 0;
|
|
135
|
+
session.messageWindowStart = now;
|
|
136
|
+
}
|
|
137
|
+
session.messageCount++;
|
|
138
|
+
if (session.messageCount > WS_RATE_LIMIT) {
|
|
139
|
+
sendError("ERROR", "RATE_LIMITED", "Too many requests. Please slow down.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (ADMIN_ONLY_TYPES.has(type)) {
|
|
146
|
+
const session = clientSessions.get(clientId);
|
|
147
|
+
if (!isAdminSession(session)) {
|
|
148
|
+
sendError("ERROR", "FORBIDDEN", "Admin access required for this operation");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const getScopedDelegate = async (): Promise<DataDriver> => {
|
|
154
|
+
const session = clientSessions.get(clientId);
|
|
155
|
+
if (session?.user && isDriverWithAuth(driver)) {
|
|
156
|
+
try {
|
|
157
|
+
const userForAuth: User = {
|
|
158
|
+
uid: session.user.userId,
|
|
159
|
+
email: session.user.email ?? "",
|
|
160
|
+
displayName: session.user.displayName ?? "",
|
|
161
|
+
photoURL: session.user.photoURL ?? "",
|
|
162
|
+
providerId: "jwt",
|
|
163
|
+
isAnonymous: false,
|
|
164
|
+
roles: session.user.roles ?? []
|
|
165
|
+
};
|
|
166
|
+
return await driver.withAuth(userForAuth);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
logger.error("Failed to create authenticated delegate for WS request", { error: e });
|
|
169
|
+
return driver;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return driver;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
switch (type) {
|
|
176
|
+
case "FETCH_COLLECTION": {
|
|
177
|
+
const request: FetchCollectionProps = payload;
|
|
178
|
+
const delegate = await getScopedDelegate();
|
|
179
|
+
const rows = await delegate.fetchCollection(request);
|
|
180
|
+
ws.send(JSON.stringify({ type: "FETCH_COLLECTION_SUCCESS",
|
|
181
|
+
payload: { rows },
|
|
182
|
+
requestId }));
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "FETCH_ONE": {
|
|
186
|
+
const request: FetchOneProps = payload;
|
|
187
|
+
const delegate = await getScopedDelegate();
|
|
188
|
+
const row = await delegate.fetchOne(request);
|
|
189
|
+
ws.send(JSON.stringify({ type: "FETCH_ONE_SUCCESS",
|
|
190
|
+
payload: { row },
|
|
191
|
+
requestId }));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "SAVE": {
|
|
195
|
+
const request: SaveProps = payload;
|
|
196
|
+
const delegate = await getScopedDelegate();
|
|
197
|
+
const row = await delegate.save(request);
|
|
198
|
+
ws.send(JSON.stringify({ type: "SAVE_SUCCESS",
|
|
199
|
+
payload: { row },
|
|
200
|
+
requestId }));
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "DELETE": {
|
|
204
|
+
const request: DeleteProps = payload;
|
|
205
|
+
const delegate = await getScopedDelegate();
|
|
206
|
+
await delegate.delete(request);
|
|
207
|
+
ws.send(JSON.stringify({ type: "DELETE_SUCCESS",
|
|
208
|
+
payload: { success: true },
|
|
209
|
+
requestId }));
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case "CHECK_UNIQUE_FIELD": {
|
|
213
|
+
const { path, name, value, id, collection } = payload;
|
|
214
|
+
const delegate = await getScopedDelegate();
|
|
215
|
+
const isUnique = await delegate.checkUniqueField(path, name, value, id, collection);
|
|
216
|
+
ws.send(JSON.stringify({ type: "CHECK_UNIQUE_FIELD_SUCCESS",
|
|
217
|
+
payload: { isUnique },
|
|
218
|
+
requestId }));
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "COUNT": {
|
|
222
|
+
const request: FetchCollectionProps = payload;
|
|
223
|
+
const delegate = await getScopedDelegate();
|
|
224
|
+
const count = await delegate.count!(request);
|
|
225
|
+
ws.send(JSON.stringify({ type: "COUNT_SUCCESS",
|
|
226
|
+
payload: { count },
|
|
227
|
+
requestId }));
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "EXECUTE_SQL": {
|
|
231
|
+
const { sql, options } = payload;
|
|
232
|
+
if (admin && isDocumentAdmin(admin) && admin.executeAggregate) {
|
|
233
|
+
const result = await admin.executeAggregate(sql as Record<string, unknown>[]);
|
|
234
|
+
ws.send(JSON.stringify({ type: "EXECUTE_SQL_SUCCESS",
|
|
235
|
+
payload: { result },
|
|
236
|
+
requestId }));
|
|
237
|
+
} else {
|
|
238
|
+
ws.send(JSON.stringify({ type: "ERROR",
|
|
239
|
+
requestId,
|
|
240
|
+
payload: { error: { message: "SQL execution not supported for this driver",
|
|
241
|
+
code: "NOT_SUPPORTED" } } }));
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "FETCH_UNMAPPED_TABLES": {
|
|
246
|
+
if (admin && isSchemaAdmin(admin)) {
|
|
247
|
+
const tables = await admin.fetchUnmappedTables?.(payload?.mappedPaths) || [];
|
|
248
|
+
ws.send(JSON.stringify({ type: "FETCH_UNMAPPED_TABLES_SUCCESS",
|
|
249
|
+
payload: { tables },
|
|
250
|
+
requestId }));
|
|
251
|
+
} else {
|
|
252
|
+
ws.send(JSON.stringify({ type: "FETCH_UNMAPPED_TABLES_SUCCESS",
|
|
253
|
+
payload: { tables: [] },
|
|
254
|
+
requestId }));
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "FETCH_TABLE_METADATA": {
|
|
259
|
+
const { tableName } = payload;
|
|
260
|
+
if (admin && isSchemaAdmin(admin)) {
|
|
261
|
+
const metadata = await admin.fetchTableMetadata?.(tableName);
|
|
262
|
+
ws.send(JSON.stringify({ type: "FETCH_TABLE_METADATA_SUCCESS",
|
|
263
|
+
payload: { metadata },
|
|
264
|
+
requestId }));
|
|
265
|
+
} else {
|
|
266
|
+
ws.send(JSON.stringify({ type: "FETCH_TABLE_METADATA_SUCCESS",
|
|
267
|
+
payload: { metadata: null },
|
|
268
|
+
requestId }));
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case "subscribe_collection":
|
|
273
|
+
case "subscribe_one":
|
|
274
|
+
case "unsubscribe": {
|
|
275
|
+
const session = clientSessions.get(clientId);
|
|
276
|
+
const authContext = session?.user ? { userId: session.user.userId,
|
|
277
|
+
roles: session.user.roles ?? [] } : undefined;
|
|
278
|
+
await realtimeService.handleClientMessage(clientId, {
|
|
279
|
+
type,
|
|
280
|
+
payload,
|
|
281
|
+
subscriptionId: payload?.subscriptionId
|
|
282
|
+
}, authContext);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
default:
|
|
286
|
+
logger.error("❌ [WebSocket Server] Unknown message type", { detail: type });
|
|
287
|
+
}
|
|
288
|
+
} catch (error: unknown) {
|
|
289
|
+
const errorMessage = process.env.NODE_ENV === "production" ? "An unexpected error occurred" : (error instanceof Error ? error.message : "An unexpected error occurred");
|
|
290
|
+
ws.send(JSON.stringify({ type: "ERROR",
|
|
291
|
+
requestId,
|
|
292
|
+
payload: { error: { message: errorMessage,
|
|
293
|
+
code: "INTERNAL_ERROR" } } }));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|