dzql 0.5.33 → 0.6.1
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/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +309 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +653 -0
- package/docs/project-setup.md +456 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +166 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
package/src/server/ws.js
DELETED
|
@@ -1,573 +0,0 @@
|
|
|
1
|
-
import { SignJWT, jwtVerify } from "jose";
|
|
2
|
-
import {
|
|
3
|
-
callAuthFunction,
|
|
4
|
-
callUserFunction,
|
|
5
|
-
getUserProfile,
|
|
6
|
-
db,
|
|
7
|
-
sql,
|
|
8
|
-
} from "./db.js";
|
|
9
|
-
import { wsLogger, authLogger } from "./logger.js";
|
|
10
|
-
import {
|
|
11
|
-
registerSubscription,
|
|
12
|
-
unregisterSubscription,
|
|
13
|
-
unregisterSubscriptionByParams,
|
|
14
|
-
removeConnectionSubscriptions,
|
|
15
|
-
getSubscribableMetadata,
|
|
16
|
-
cacheSubscribableMetadata
|
|
17
|
-
} from "./subscriptions.js";
|
|
18
|
-
|
|
19
|
-
// Environment configuration
|
|
20
|
-
const JWT_SECRET_STRING = process.env.JWT_SECRET;
|
|
21
|
-
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
|
|
22
|
-
|
|
23
|
-
// WebSocket ping/pong configuration (important for Heroku and other platforms)
|
|
24
|
-
// Heroku terminates WebSocket connections after 55 seconds of inactivity (H15 error)
|
|
25
|
-
// Default 30s interval keeps connections alive well within that limit
|
|
26
|
-
const WS_PING_INTERVAL = parseInt(process.env.WS_PING_INTERVAL || "30000", 10); // 30 seconds default
|
|
27
|
-
const WS_PING_TIMEOUT = parseInt(process.env.WS_PING_TIMEOUT || "5000", 10); // 5 seconds default
|
|
28
|
-
const WS_MAX_MESSAGE_SIZE = parseInt(process.env.WS_MAX_MESSAGE_SIZE || "1048576", 10); // 1MB default
|
|
29
|
-
|
|
30
|
-
// Validate JWT_SECRET in production
|
|
31
|
-
if (process.env.NODE_ENV === "production" && !JWT_SECRET_STRING) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
"JWT_SECRET environment variable is required in production. Generate one with: openssl rand -base64 32"
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Warn if using default secret in development
|
|
38
|
-
if (!JWT_SECRET_STRING && process.env.NODE_ENV !== "test") {
|
|
39
|
-
console.warn(
|
|
40
|
-
"⚠️ WARNING: Using default JWT secret. Set JWT_SECRET environment variable for security."
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const JWT_SECRET = new TextEncoder().encode(
|
|
45
|
-
JWT_SECRET_STRING || "dev-secret-at-least-32-chars-long!!"
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
// JWT helpers
|
|
49
|
-
export async function create_jwt(payload) {
|
|
50
|
-
return await new SignJWT(payload)
|
|
51
|
-
.setProtectedHeader({ alg: "HS256" })
|
|
52
|
-
.setIssuedAt()
|
|
53
|
-
.setExpirationTime(JWT_EXPIRES_IN)
|
|
54
|
-
.sign(JWT_SECRET);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function verify_jwt_token(token) {
|
|
58
|
-
try {
|
|
59
|
-
const { payload } = await jwtVerify(token, JWT_SECRET);
|
|
60
|
-
return payload;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// JSON-RPC helpers
|
|
67
|
-
export function create_rpc_response(id, result) {
|
|
68
|
-
return JSON.stringify({
|
|
69
|
-
jsonrpc: "2.0",
|
|
70
|
-
result,
|
|
71
|
-
id,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function create_rpc_error(id, code, message, data = null) {
|
|
76
|
-
return JSON.stringify({
|
|
77
|
-
jsonrpc: "2.0",
|
|
78
|
-
error: { code, message, data },
|
|
79
|
-
id,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// SID (Session ID) Promise Management for bidirectional client-server communication
|
|
84
|
-
// Used for async operations where server requests data from client
|
|
85
|
-
const sidPromises = new Map();
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Create a promise with a unique SID that can be resolved/rejected later
|
|
89
|
-
* @param {number} timeout - Timeout in milliseconds (default: 30000)
|
|
90
|
-
* @returns {Object} - { sid, promise }
|
|
91
|
-
*/
|
|
92
|
-
export function createSIDPromise(timeout = 30000) {
|
|
93
|
-
const sid = crypto.randomUUID();
|
|
94
|
-
|
|
95
|
-
const promise = new Promise((resolve, reject) => {
|
|
96
|
-
// Store resolve/reject functions
|
|
97
|
-
sidPromises.set(sid, { resolve, reject });
|
|
98
|
-
|
|
99
|
-
// Set timeout
|
|
100
|
-
const timer = setTimeout(() => {
|
|
101
|
-
if (sidPromises.has(sid)) {
|
|
102
|
-
sidPromises.delete(sid);
|
|
103
|
-
reject(new Error(`SID request timeout after ${timeout}ms`));
|
|
104
|
-
}
|
|
105
|
-
}, timeout);
|
|
106
|
-
|
|
107
|
-
// Store timer so it can be cleared on resolution
|
|
108
|
-
const entry = sidPromises.get(sid);
|
|
109
|
-
if (entry) {
|
|
110
|
-
entry.timer = timer;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
return { sid, promise };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Resolve a pending SID promise with a result
|
|
119
|
-
* @param {string} sid - The session ID
|
|
120
|
-
* @param {any} result - The result to resolve with
|
|
121
|
-
* @returns {boolean} - True if SID was found and resolved
|
|
122
|
-
*/
|
|
123
|
-
export function resolveSID(sid, result) {
|
|
124
|
-
const entry = sidPromises.get(sid);
|
|
125
|
-
if (!entry) {
|
|
126
|
-
wsLogger.warn(`Attempted to resolve unknown SID: ${sid}`);
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
clearTimeout(entry.timer);
|
|
131
|
-
sidPromises.delete(sid);
|
|
132
|
-
entry.resolve(result);
|
|
133
|
-
wsLogger.debug(`SID resolved: ${sid}`);
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Reject a pending SID promise with an error
|
|
139
|
-
* @param {string} sid - The session ID
|
|
140
|
-
* @param {Error|string} error - The error to reject with
|
|
141
|
-
* @returns {boolean} - True if SID was found and rejected
|
|
142
|
-
*/
|
|
143
|
-
export function rejectSID(sid, error) {
|
|
144
|
-
const entry = sidPromises.get(sid);
|
|
145
|
-
if (!entry) {
|
|
146
|
-
wsLogger.warn(`Attempted to reject unknown SID: ${sid}`);
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
clearTimeout(entry.timer);
|
|
151
|
-
sidPromises.delete(sid);
|
|
152
|
-
entry.reject(typeof error === 'string' ? new Error(error) : error);
|
|
153
|
-
wsLogger.debug(`SID rejected: ${sid}`);
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Create RPC handler function
|
|
158
|
-
export function createRPCHandler(customHandlers = {}) {
|
|
159
|
-
return async function handle_rpc(ws, message) {
|
|
160
|
-
let id = null;
|
|
161
|
-
let method = null;
|
|
162
|
-
const startTime = Date.now();
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const parsed = JSON.parse(message);
|
|
166
|
-
method = parsed.method;
|
|
167
|
-
const params = parsed.params;
|
|
168
|
-
id = parsed.id;
|
|
169
|
-
|
|
170
|
-
// Log incoming request
|
|
171
|
-
wsLogger.request(method, params);
|
|
172
|
-
|
|
173
|
-
// Handle SID responses from client (special internal method)
|
|
174
|
-
if (method === "_sid_response") {
|
|
175
|
-
const { sid, result, error } = params || {};
|
|
176
|
-
if (!sid) {
|
|
177
|
-
return create_rpc_error(id, -32602, "Missing sid parameter");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (error) {
|
|
181
|
-
rejectSID(sid, error);
|
|
182
|
-
} else {
|
|
183
|
-
resolveSID(sid, result);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return create_rpc_response(id, { success: true });
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Validate method doesn't start with underscore (private)
|
|
190
|
-
if (method.startsWith("_")) {
|
|
191
|
-
wsLogger.warn(`Blocked private function call: ${method}`);
|
|
192
|
-
return create_rpc_error(id, -32601, "Cannot call private functions");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Handle DZQL operations (require auth, identifiable by signature)
|
|
196
|
-
if (method.startsWith("dzql.")) {
|
|
197
|
-
if (!ws.data.user_id) {
|
|
198
|
-
return create_rpc_error(id, -32603, "Not authenticated");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const [, operation, entity] = method.split(".");
|
|
202
|
-
if (!operation || !entity) {
|
|
203
|
-
return create_rpc_error(
|
|
204
|
-
id,
|
|
205
|
-
-32602,
|
|
206
|
-
"Invalid DZQL method format. Use: dzql.operation.entity",
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
!["get", "save", "delete", "lookup", "search"].includes(operation)
|
|
212
|
-
) {
|
|
213
|
-
return create_rpc_error(
|
|
214
|
-
id,
|
|
215
|
-
-32602,
|
|
216
|
-
`Unknown DZQL operation: ${operation}`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
wsLogger.debug(`DZQL: Calling ${operation}.${entity} with params:`, JSON.stringify(params));
|
|
221
|
-
const result = await db.api[operation][entity](
|
|
222
|
-
params || {},
|
|
223
|
-
ws.data.user_id,
|
|
224
|
-
);
|
|
225
|
-
wsLogger.debug(`DZQL: ${operation}.${entity} returned successfully`);
|
|
226
|
-
return create_rpc_response(id, result);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Local API functions that don't require auth
|
|
230
|
-
if (method === "login_user") {
|
|
231
|
-
authLogger.debug(`Login attempt for: ${params.email}`);
|
|
232
|
-
const data = await callAuthFunction(
|
|
233
|
-
"login_user",
|
|
234
|
-
params.email,
|
|
235
|
-
params.password,
|
|
236
|
-
params.options || null,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
// On successful auth, set user_id on WebSocket connection
|
|
240
|
-
if (data && data.user_id) {
|
|
241
|
-
ws.data.user_id = data.user_id;
|
|
242
|
-
authLogger.info(`User logged in: ${params.email} (id: ${data.user_id})`);
|
|
243
|
-
|
|
244
|
-
// Create JWT token for client storage
|
|
245
|
-
const token = await create_jwt({
|
|
246
|
-
user_id: data.user_id,
|
|
247
|
-
email: data.email,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// Get full profile
|
|
251
|
-
const profile = await getUserProfile(data.user_id);
|
|
252
|
-
|
|
253
|
-
const result = {
|
|
254
|
-
user_id: data.user_id,
|
|
255
|
-
email: data.email,
|
|
256
|
-
token,
|
|
257
|
-
profile,
|
|
258
|
-
};
|
|
259
|
-
wsLogger.response(method, result, Date.now() - startTime);
|
|
260
|
-
return create_rpc_response(id, result);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
authLogger.warn(`Login failed for: ${params.email}`);
|
|
264
|
-
wsLogger.response(method, data, Date.now() - startTime);
|
|
265
|
-
return create_rpc_response(id, data);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (method === "register_user") {
|
|
269
|
-
authLogger.debug(`Registration attempt for: ${params.email}`);
|
|
270
|
-
const data = await callAuthFunction(
|
|
271
|
-
"register_user",
|
|
272
|
-
params.email,
|
|
273
|
-
params.password,
|
|
274
|
-
params.options || null,
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
// On successful registration, set user_id on WebSocket connection
|
|
278
|
-
if (data && data.user_id) {
|
|
279
|
-
ws.data.user_id = data.user_id;
|
|
280
|
-
authLogger.info(`User registered: ${params.email} (id: ${data.user_id})`);
|
|
281
|
-
|
|
282
|
-
// Create JWT token for client storage
|
|
283
|
-
const token = await create_jwt({
|
|
284
|
-
user_id: data.user_id,
|
|
285
|
-
email: data.email,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const result = {
|
|
289
|
-
user_id: data.user_id,
|
|
290
|
-
email: data.email,
|
|
291
|
-
token,
|
|
292
|
-
profile: data,
|
|
293
|
-
};
|
|
294
|
-
wsLogger.response(method, result, Date.now() - startTime);
|
|
295
|
-
return create_rpc_response(id, result);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
authLogger.warn(`Registration failed for: ${params.email}`);
|
|
299
|
-
wsLogger.response(method, data, Date.now() - startTime);
|
|
300
|
-
return create_rpc_response(id, data);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Everything else requires authentication
|
|
304
|
-
if (!ws.data.user_id) {
|
|
305
|
-
wsLogger.warn(`Unauthenticated request to: ${method}`);
|
|
306
|
-
return create_rpc_error(id, -32603, "Not authenticated");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Authenticated-only local functions
|
|
310
|
-
if (method === "logout") {
|
|
311
|
-
authLogger.info(`User logged out (id: ${ws.data.user_id})`);
|
|
312
|
-
ws.data.user_id = null;
|
|
313
|
-
const result = { success: true };
|
|
314
|
-
wsLogger.response(method, result, Date.now() - startTime);
|
|
315
|
-
return create_rpc_response(id, result);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// SUBSCRIPTION HANDLERS - Pattern match on method name
|
|
319
|
-
if (method.startsWith("subscribe_")) {
|
|
320
|
-
const subscribableName = method.replace("subscribe_", "");
|
|
321
|
-
wsLogger.debug(`Subscribe request: ${subscribableName}`, params);
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
// Execute initial query (this also checks permissions)
|
|
325
|
-
const queryResult = await sql.unsafe(
|
|
326
|
-
`SELECT get_${subscribableName}($1, $2) as result`,
|
|
327
|
-
[params, ws.data.user_id]
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
const queryData = queryResult[0]?.result;
|
|
331
|
-
|
|
332
|
-
// Check if compiled function returned embedded schema
|
|
333
|
-
// Compiled: { data, schema } | Interpreted: just the data object
|
|
334
|
-
let data, schema;
|
|
335
|
-
if (queryData && queryData.schema && queryData.data !== undefined) {
|
|
336
|
-
// Compiled mode - schema is embedded (includes scopeTables)
|
|
337
|
-
data = queryData.data;
|
|
338
|
-
schema = queryData.schema;
|
|
339
|
-
// Cache scopeTables for event filtering
|
|
340
|
-
if (schema.scopeTables) {
|
|
341
|
-
cacheSubscribableMetadata(subscribableName, {
|
|
342
|
-
scopeTables: schema.scopeTables,
|
|
343
|
-
pathMapping: schema.paths,
|
|
344
|
-
rootEntity: schema.root,
|
|
345
|
-
relations: {}
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
} else {
|
|
349
|
-
// Interpreted mode - fetch schema from metadata table
|
|
350
|
-
data = queryData;
|
|
351
|
-
const metadata = await getSubscribableMetadata(subscribableName, sql);
|
|
352
|
-
schema = {
|
|
353
|
-
root: metadata.rootEntity,
|
|
354
|
-
paths: metadata.pathMapping
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Register subscription in memory
|
|
359
|
-
const subscriptionId = registerSubscription(
|
|
360
|
-
subscribableName,
|
|
361
|
-
ws.data.user_id,
|
|
362
|
-
ws.data.connection_id,
|
|
363
|
-
params
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
// Build result with schema for client-side patching
|
|
367
|
-
const result = {
|
|
368
|
-
subscription_id: subscriptionId,
|
|
369
|
-
data,
|
|
370
|
-
schema
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
wsLogger.response(method, result, Date.now() - startTime);
|
|
374
|
-
return create_rpc_response(id, result);
|
|
375
|
-
} catch (error) {
|
|
376
|
-
wsLogger.error(`Subscribe failed for ${subscribableName}:`, error.message);
|
|
377
|
-
return create_rpc_error(id, -32603, error.message);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (method.startsWith("unsubscribe_")) {
|
|
382
|
-
const subscribableName = method.replace("unsubscribe_", "");
|
|
383
|
-
wsLogger.debug(`Unsubscribe request: ${subscribableName}`, params);
|
|
384
|
-
|
|
385
|
-
// Remove subscription by params
|
|
386
|
-
const removed = unregisterSubscriptionByParams(
|
|
387
|
-
subscribableName,
|
|
388
|
-
ws.data.connection_id,
|
|
389
|
-
params
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
const result = { success: removed };
|
|
393
|
-
wsLogger.response(method, result, Date.now() - startTime);
|
|
394
|
-
return create_rpc_response(id, result);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Check for custom handlers
|
|
398
|
-
if (customHandlers[method]) {
|
|
399
|
-
wsLogger.debug(`Calling custom handler: ${method}`);
|
|
400
|
-
const result = await customHandlers[method](ws.data.user_id, params);
|
|
401
|
-
wsLogger.response(method, result, Date.now() - startTime);
|
|
402
|
-
return create_rpc_response(id, result);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Call stored function with user_id as first parameter
|
|
406
|
-
wsLogger.debug(`Calling database function: ${method}`);
|
|
407
|
-
const result = await callUserFunction(method, ws.data.user_id, params);
|
|
408
|
-
wsLogger.response(method, result, Date.now() - startTime);
|
|
409
|
-
return create_rpc_response(id, result);
|
|
410
|
-
} catch (error) {
|
|
411
|
-
wsLogger.error(`RPC error in ${method}:`, error.message);
|
|
412
|
-
wsLogger.debug(`RPC error stack:`, error.stack);
|
|
413
|
-
|
|
414
|
-
// PostgreSQL error codes
|
|
415
|
-
if (error.code) {
|
|
416
|
-
wsLogger.debug(`Returning PostgreSQL error for id=${id}`);
|
|
417
|
-
return create_rpc_error(id, -32603, String(error), {
|
|
418
|
-
code: error.code,
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Generic error
|
|
423
|
-
wsLogger.debug(`Returning generic error for id=${id}: ${error.message}`);
|
|
424
|
-
return create_rpc_error(id, -32603, "Internal error", {
|
|
425
|
-
message: error.message,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Create WebSocket event handlers
|
|
432
|
-
export function createWebSocketHandlers(options = {}) {
|
|
433
|
-
const {
|
|
434
|
-
rpcHandler = null,
|
|
435
|
-
customHandlers = {},
|
|
436
|
-
onConnection = null,
|
|
437
|
-
onDisconnection = null,
|
|
438
|
-
} = options;
|
|
439
|
-
|
|
440
|
-
// Active WebSocket connections
|
|
441
|
-
const connections = new Map();
|
|
442
|
-
|
|
443
|
-
// Create RPC handler if not provided
|
|
444
|
-
const handler = rpcHandler || createRPCHandler(customHandlers);
|
|
445
|
-
|
|
446
|
-
// Create broadcaster function
|
|
447
|
-
const broadcast = createBroadcaster(connections);
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
connections,
|
|
451
|
-
broadcast,
|
|
452
|
-
|
|
453
|
-
// WebSocket configuration for Bun.serve
|
|
454
|
-
// These properties are required for proper ping/pong support (especially on Heroku)
|
|
455
|
-
perMessageDeflate: true,
|
|
456
|
-
maxPayloadLength: WS_MAX_MESSAGE_SIZE,
|
|
457
|
-
idleTimeout: WS_PING_INTERVAL / 1000, // Convert to seconds for Bun
|
|
458
|
-
closeOnBackpressureLimit: true, // Close connection if backpressure limit exceeded
|
|
459
|
-
|
|
460
|
-
// Connection opened
|
|
461
|
-
async open(ws) {
|
|
462
|
-
const id = crypto.randomUUID();
|
|
463
|
-
ws.data.connection_id = id;
|
|
464
|
-
connections.set(id, ws);
|
|
465
|
-
|
|
466
|
-
wsLogger.info(
|
|
467
|
-
`Connection opened: ${id.slice(0, 8)}...`,
|
|
468
|
-
ws.data.user_id ? `(user: ${ws.data.user_id})` : "(anonymous)",
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
// Get full profile if authenticated
|
|
472
|
-
let profile = null;
|
|
473
|
-
if (ws.data.user_id) {
|
|
474
|
-
try {
|
|
475
|
-
profile = await getUserProfile(ws.data.user_id);
|
|
476
|
-
} catch (error) {
|
|
477
|
-
wsLogger.error("Failed to load profile:", error.message);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Send welcome message as JSON-RPC method call
|
|
482
|
-
ws.send(
|
|
483
|
-
JSON.stringify({
|
|
484
|
-
jsonrpc: "2.0",
|
|
485
|
-
method: "connected",
|
|
486
|
-
params: {
|
|
487
|
-
connection_id: id,
|
|
488
|
-
authenticated: !!ws.data.user_id,
|
|
489
|
-
profile,
|
|
490
|
-
},
|
|
491
|
-
}),
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
// Call custom connection handler
|
|
495
|
-
if (onConnection) {
|
|
496
|
-
onConnection(ws, id);
|
|
497
|
-
}
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
// Message received
|
|
501
|
-
async message(ws, message) {
|
|
502
|
-
const response = await handler(ws, message);
|
|
503
|
-
ws.send(response);
|
|
504
|
-
},
|
|
505
|
-
|
|
506
|
-
// Connection closed
|
|
507
|
-
close(ws) {
|
|
508
|
-
const id = ws.data.connection_id;
|
|
509
|
-
connections.delete(id);
|
|
510
|
-
|
|
511
|
-
// Clean up all subscriptions for this connection
|
|
512
|
-
const removedCount = removeConnectionSubscriptions(id);
|
|
513
|
-
if (removedCount > 0) {
|
|
514
|
-
wsLogger.info(`Connection closed: ${id?.slice(0, 8)}... (${removedCount} subscriptions removed)`);
|
|
515
|
-
} else {
|
|
516
|
-
wsLogger.info(`Connection closed: ${id?.slice(0, 8)}...`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Call custom disconnection handler
|
|
520
|
-
if (onDisconnection) {
|
|
521
|
-
onDisconnection(ws, id);
|
|
522
|
-
}
|
|
523
|
-
},
|
|
524
|
-
|
|
525
|
-
// Error occurred
|
|
526
|
-
error(ws, error) {
|
|
527
|
-
wsLogger.error(`WebSocket error for ${ws.data.connection_id?.slice(0, 8)}...:`, error.message);
|
|
528
|
-
},
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Broadcast message to all authenticated connections or specific client_ids
|
|
533
|
-
export function createBroadcaster(connections) {
|
|
534
|
-
const broadcastToConnections = function(message, client_ids = null) {
|
|
535
|
-
if (client_ids && Array.isArray(client_ids)) {
|
|
536
|
-
// Send to specific user_ids
|
|
537
|
-
for (const [id, ws] of connections) {
|
|
538
|
-
if (ws.data.user_id && client_ids.includes(ws.data.user_id)) {
|
|
539
|
-
ws.send(message);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
} else {
|
|
543
|
-
// Send to all authenticated connections
|
|
544
|
-
for (const [id, ws] of connections) {
|
|
545
|
-
if (ws.data.user_id) {
|
|
546
|
-
ws.send(message);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
// Add helper function to send to a specific connection
|
|
553
|
-
broadcastToConnections.toConnection = function(connectionId, message) {
|
|
554
|
-
const ws = connections.get(connectionId);
|
|
555
|
-
if (ws && ws.readyState === 1) { // 1 = OPEN
|
|
556
|
-
ws.send(message);
|
|
557
|
-
return true;
|
|
558
|
-
}
|
|
559
|
-
return false;
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
return broadcastToConnections;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Legacy export for backward compatibility
|
|
566
|
-
export function broadcastToConnections(connections, message) {
|
|
567
|
-
// Send to all authenticated connections
|
|
568
|
-
for (const [id, ws] of connections) {
|
|
569
|
-
if (ws.data.user_id) {
|
|
570
|
-
ws.send(message);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|