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/index.js
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
import { createWebSocketHandlers, verify_jwt_token } from "./ws.js";
|
|
2
|
-
import { closeConnections, setupListeners, sql, db } from "./db.js";
|
|
3
|
-
import * as defaultApi from "./api.js";
|
|
4
|
-
import { serverLogger, notifyLogger } from "./logger.js";
|
|
5
|
-
import { getSubscriptionsBySubscribable, paramsMatch, getSubscribableScopeTables } from "./subscriptions.js";
|
|
6
|
-
|
|
7
|
-
// Re-export commonly used utilities
|
|
8
|
-
export { sql, db } from "./db.js";
|
|
9
|
-
export { metaRoute } from "./meta-route.js";
|
|
10
|
-
export { createMCPRoute } from "./mcp.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Process subscription updates when a database event occurs
|
|
14
|
-
* Forwards atomic events to affected subscriptions for client-side patching
|
|
15
|
-
* @param {Object} event - Database event {table, op, pk, data}
|
|
16
|
-
* @param {Function} broadcast - Broadcast function from WebSocket handlers
|
|
17
|
-
*/
|
|
18
|
-
async function processSubscriptionUpdates(event, broadcast) {
|
|
19
|
-
const { table, op, pk, data } = event;
|
|
20
|
-
|
|
21
|
-
// Get all active subscriptions grouped by subscribable
|
|
22
|
-
const subscriptionsByName = getSubscriptionsBySubscribable();
|
|
23
|
-
|
|
24
|
-
if (subscriptionsByName.size === 0) {
|
|
25
|
-
return; // No active subscriptions
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
notifyLogger.debug(`Checking ${subscriptionsByName.size} subscribable(s) for affected subscriptions`);
|
|
29
|
-
|
|
30
|
-
// For each unique subscribable, check if this event affects any subscriptions
|
|
31
|
-
for (const [subscribableName, subs] of subscriptionsByName.entries()) {
|
|
32
|
-
try {
|
|
33
|
-
// Check if this table is in scope for this subscribable
|
|
34
|
-
// This is an optimization to avoid calling _affected_documents for unrelated tables
|
|
35
|
-
const scopeTables = await getSubscribableScopeTables(subscribableName, sql);
|
|
36
|
-
if (scopeTables.length > 0 && !scopeTables.includes(table)) {
|
|
37
|
-
notifyLogger.debug(`Table ${table} not in scope for ${subscribableName}, skipping`);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Ask PostgreSQL which subscription instances are affected
|
|
42
|
-
// Pass (table, op, data) - the data contains pk and fields needed to resolve affected documents
|
|
43
|
-
const result = await sql.unsafe(
|
|
44
|
-
`SELECT ${subscribableName}_affected_documents($1, $2, $3) as affected`,
|
|
45
|
-
[table, op, data]
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const affectedParamSets = result[0]?.affected;
|
|
49
|
-
|
|
50
|
-
if (!affectedParamSets || affectedParamSets.length === 0) {
|
|
51
|
-
continue; // This subscribable not affected
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
notifyLogger.debug(`${subscribableName}: ${affectedParamSets.length} param set(s) affected by ${table}:${op}`);
|
|
55
|
-
|
|
56
|
-
// Match affected params to active subscriptions
|
|
57
|
-
for (const affectedParams of affectedParamSets) {
|
|
58
|
-
for (const sub of subs) {
|
|
59
|
-
// Check if this subscription matches the affected params
|
|
60
|
-
if (paramsMatch(sub.params, affectedParams)) {
|
|
61
|
-
try {
|
|
62
|
-
// Forward atomic event instead of re-querying the full document
|
|
63
|
-
// Client will apply the patch to their local copy
|
|
64
|
-
const message = JSON.stringify({
|
|
65
|
-
jsonrpc: "2.0",
|
|
66
|
-
method: "subscription:event",
|
|
67
|
-
params: {
|
|
68
|
-
subscription_id: sub.subscriptionId,
|
|
69
|
-
subscribable: subscribableName,
|
|
70
|
-
event: {
|
|
71
|
-
table,
|
|
72
|
-
op,
|
|
73
|
-
pk,
|
|
74
|
-
data
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const sent = broadcast.toConnection(sub.connection_id, message);
|
|
80
|
-
if (sent) {
|
|
81
|
-
notifyLogger.debug(`Sent atomic event to subscription ${sub.subscriptionId.slice(0, 8)}... (${table}:${op})`);
|
|
82
|
-
} else {
|
|
83
|
-
notifyLogger.warn(`Failed to send event to connection ${sub.connection_id.slice(0, 8)}...`);
|
|
84
|
-
}
|
|
85
|
-
} catch (error) {
|
|
86
|
-
notifyLogger.error(`Failed to send event to subscription ${sub.subscriptionId}:`, error.message);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch (error) {
|
|
92
|
-
// If the subscribable function doesn't exist, just skip
|
|
93
|
-
if (error.message && error.message.includes('does not exist')) {
|
|
94
|
-
notifyLogger.debug(`Subscribable ${subscribableName} functions not found, skipping`);
|
|
95
|
-
} else {
|
|
96
|
-
notifyLogger.error(`Error processing subscriptions for ${subscribableName}:`, error.message);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Create a DZQL server with WebSocket support, real-time updates, and automatic CRUD operations
|
|
104
|
-
*
|
|
105
|
-
* Sets up a Bun server with:
|
|
106
|
-
* - WebSocket endpoint at /ws for real-time communication
|
|
107
|
-
* - JSON-RPC 2.0 protocol for API calls
|
|
108
|
-
* - PostgreSQL NOTIFY/LISTEN for real-time broadcasts
|
|
109
|
-
* - Automatic JWT authentication
|
|
110
|
-
* - Health check endpoint at /health
|
|
111
|
-
*
|
|
112
|
-
* @param {Object} [options={}] - Server configuration options
|
|
113
|
-
* @param {number} [options.port=3000] - Port number to listen on (or process.env.PORT)
|
|
114
|
-
* @param {Object} [options.customApi={}] - Custom Bun functions to expose via WebSocket API
|
|
115
|
-
* Each function receives (userId, params) and can return any JSON-serializable value
|
|
116
|
-
* @param {Object} [options.routes={}] - Additional HTTP routes as { path: handlerFunction }
|
|
117
|
-
* @param {string|null} [options.staticPath=null] - Path to static files directory for serving
|
|
118
|
-
* @param {Function} [options.onReady=null] - Callback invoked after server initialization
|
|
119
|
-
* Receives { broadcast, routes } to allow dynamic route setup
|
|
120
|
-
*
|
|
121
|
-
* @returns {Object} Server instance with the following properties:
|
|
122
|
-
* @returns {number} .port - The port number the server is listening on
|
|
123
|
-
* @returns {Object} .server - The underlying Bun.Server instance
|
|
124
|
-
* @returns {Function} .shutdown - Async function to gracefully shutdown server and close DB connections
|
|
125
|
-
* @returns {Function} .broadcast - Function to send messages to connected WebSocket clients
|
|
126
|
-
* Signature: broadcast(message: string, userIds?: number[])
|
|
127
|
-
* If userIds provided, sends only to those users; otherwise broadcasts to all authenticated users
|
|
128
|
-
*
|
|
129
|
-
* @example
|
|
130
|
-
* // Basic server
|
|
131
|
-
* import { createServer } from 'dzql';
|
|
132
|
-
*
|
|
133
|
-
* const server = createServer({ port: 3000 });
|
|
134
|
-
*
|
|
135
|
-
* @example
|
|
136
|
-
* // Server with custom API functions
|
|
137
|
-
* import { createServer, db } from 'dzql';
|
|
138
|
-
*
|
|
139
|
-
* const server = createServer({
|
|
140
|
-
* port: 3000,
|
|
141
|
-
* customApi: {
|
|
142
|
-
* async getVenueStats(userId, params) {
|
|
143
|
-
* const { venueId } = params;
|
|
144
|
-
* return db.api.get.venues({ id: venueId }, userId);
|
|
145
|
-
* }
|
|
146
|
-
* }
|
|
147
|
-
* });
|
|
148
|
-
*
|
|
149
|
-
* // Client can call: await ws.api.getVenueStats({ venueId: 1 })
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* // Server with static files and custom routes
|
|
153
|
-
* const server = createServer({
|
|
154
|
-
* port: 3000,
|
|
155
|
-
* staticPath: './public',
|
|
156
|
-
* routes: {
|
|
157
|
-
* '/api/health': () => new Response(JSON.stringify({ status: 'ok' }))
|
|
158
|
-
* }
|
|
159
|
-
* });
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* // Server with onReady callback for dynamic setup
|
|
163
|
-
* const server = createServer({
|
|
164
|
-
* onReady: ({ broadcast, routes }) => {
|
|
165
|
-
* // Add routes dynamically
|
|
166
|
-
* routes['/api/notify'] = (req) => {
|
|
167
|
-
* broadcast(JSON.stringify({ method: 'alert', params: { msg: 'Hello!' } }));
|
|
168
|
-
* return new Response('Sent');
|
|
169
|
-
* };
|
|
170
|
-
* }
|
|
171
|
-
* });
|
|
172
|
-
*/
|
|
173
|
-
export function createServer(options = {}) {
|
|
174
|
-
const {
|
|
175
|
-
port = process.env.PORT || 3000,
|
|
176
|
-
customApi = {},
|
|
177
|
-
routes = {},
|
|
178
|
-
staticPath = null, // No default static path - applications should specify
|
|
179
|
-
onReady = null // Optional callback that receives { broadcast, server } after initialization
|
|
180
|
-
} = options;
|
|
181
|
-
|
|
182
|
-
// Merge default API with custom API
|
|
183
|
-
const api = { ...defaultApi, ...customApi };
|
|
184
|
-
|
|
185
|
-
// Create WebSocket event handlers
|
|
186
|
-
const { broadcast, ...websocketHandlers } = createWebSocketHandlers({
|
|
187
|
-
customHandlers: api,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Setup NOTIFY listeners for real-time events
|
|
191
|
-
setupListeners(async (event) => {
|
|
192
|
-
// Handle single dzql event with filtering
|
|
193
|
-
const { notify_users, ...eventData } = event;
|
|
194
|
-
|
|
195
|
-
// PATTERN 2: Need to Know notifications (existing)
|
|
196
|
-
// Create JSON-RPC notification
|
|
197
|
-
const message = JSON.stringify({
|
|
198
|
-
jsonrpc: "2.0",
|
|
199
|
-
method: `${event.table}:${event.op}`, // e.g., "venues:update"
|
|
200
|
-
params: eventData,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Filter based on notify_users (null = broadcast to all)
|
|
204
|
-
if (notify_users && notify_users.length > 0) {
|
|
205
|
-
// Send to specific users only
|
|
206
|
-
notifyLogger.debug(`Broadcasting ${event.table}:${event.op} to ${notify_users.length} users`);
|
|
207
|
-
broadcast(message, notify_users);
|
|
208
|
-
} else {
|
|
209
|
-
// Send to all connected users
|
|
210
|
-
notifyLogger.debug(`Broadcasting ${event.table}:${event.op} to all users`);
|
|
211
|
-
broadcast(message);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// PATTERN 1: Live Query subscriptions (new)
|
|
215
|
-
// Check if any subscriptions are affected by this event
|
|
216
|
-
await processSubscriptionUpdates(event, broadcast);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
routes['/health'] = () => new Response("OK", { status: 200 });
|
|
220
|
-
|
|
221
|
-
// Call onReady callback if provided to allow dynamic route setup
|
|
222
|
-
if (onReady && typeof onReady === 'function') {
|
|
223
|
-
const additionalRoutes = onReady({ broadcast, routes });
|
|
224
|
-
if (additionalRoutes && typeof additionalRoutes === 'object') {
|
|
225
|
-
Object.assign(routes, additionalRoutes);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Create and start the Bun server
|
|
230
|
-
const server = Bun.serve({
|
|
231
|
-
port,
|
|
232
|
-
routes,
|
|
233
|
-
async fetch(req, server) {
|
|
234
|
-
const url = new URL(req.url);
|
|
235
|
-
|
|
236
|
-
// WebSocket upgrade path
|
|
237
|
-
if (url.pathname === "/ws") {
|
|
238
|
-
// Extract token from Authorization header or query param
|
|
239
|
-
const auth_header = req.headers.get("Authorization");
|
|
240
|
-
const token =
|
|
241
|
-
auth_header?.replace("Bearer ", "") || url.searchParams.get("token");
|
|
242
|
-
|
|
243
|
-
let user_data = null;
|
|
244
|
-
|
|
245
|
-
// Verify JWT if provided
|
|
246
|
-
if (token) {
|
|
247
|
-
const payload = await verify_jwt_token(token);
|
|
248
|
-
if (payload) {
|
|
249
|
-
user_data = {
|
|
250
|
-
user_id: payload.user_id,
|
|
251
|
-
email: payload.email,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Upgrade to WebSocket (allow anonymous for login/register)
|
|
257
|
-
const success = server.upgrade(req, {
|
|
258
|
-
data: user_data || {},
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
if (success) return undefined;
|
|
262
|
-
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Static file serving (only if staticPath is configured)
|
|
266
|
-
if (staticPath) {
|
|
267
|
-
let filePath = url.pathname;
|
|
268
|
-
|
|
269
|
-
// Default to index.html for root or directory requests
|
|
270
|
-
if (!filePath || filePath === "/") {
|
|
271
|
-
filePath = "/index.html";
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const file = Bun.file(`${staticPath}${filePath}`);
|
|
275
|
-
if (await file.exists()) {
|
|
276
|
-
return new Response(file);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return new Response("Not Found", { status: 404 });
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
websocket: websocketHandlers,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
serverLogger.info(`🚀 DZQL server started`);
|
|
287
|
-
serverLogger.info(` HTTP: http://localhost:${port}`);
|
|
288
|
-
serverLogger.info(` WebSocket: ws://localhost:${port}/ws`);
|
|
289
|
-
serverLogger.info(` Environment: ${process.env.NODE_ENV || "development"}`);
|
|
290
|
-
serverLogger.info(` WS Ping Interval: ${process.env.WS_PING_INTERVAL || 30000}ms (Heroku safe: <55s)`);
|
|
291
|
-
|
|
292
|
-
// Add graceful shutdown handling
|
|
293
|
-
const shutdown = async () => {
|
|
294
|
-
serverLogger.info("Shutting down DZQL server...");
|
|
295
|
-
await closeConnections();
|
|
296
|
-
serverLogger.info("Server shutdown complete");
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Return server instance with utilities
|
|
300
|
-
return {
|
|
301
|
-
port,
|
|
302
|
-
server,
|
|
303
|
-
shutdown,
|
|
304
|
-
broadcast
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// If this file is run directly (not imported), start the server
|
|
309
|
-
if (import.meta.main) {
|
|
310
|
-
const server = createServer();
|
|
311
|
-
|
|
312
|
-
// Graceful shutdown
|
|
313
|
-
process.on("SIGINT", async () => {
|
|
314
|
-
await server.shutdown();
|
|
315
|
-
process.exit(0);
|
|
316
|
-
});
|
|
317
|
-
}
|
package/src/server/logger.js
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// logger.js - Flexible logging system with categories and levels
|
|
2
|
-
|
|
3
|
-
// Log levels
|
|
4
|
-
const LOG_LEVELS = {
|
|
5
|
-
ERROR: 0,
|
|
6
|
-
WARN: 1,
|
|
7
|
-
INFO: 2,
|
|
8
|
-
DEBUG: 3,
|
|
9
|
-
TRACE: 4,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// Default log level from environment or INFO
|
|
13
|
-
const DEFAULT_LEVEL = process.env.LOG_LEVEL?.toUpperCase() || "INFO";
|
|
14
|
-
|
|
15
|
-
// Detect if running in CLI context (invokej/tasks.js)
|
|
16
|
-
const isCliContext = () => {
|
|
17
|
-
// Check if main module contains 'tasks.js' or 'invokej'
|
|
18
|
-
const mainModule = process.argv[1] || '';
|
|
19
|
-
return mainModule.includes('tasks.js') ||
|
|
20
|
-
mainModule.includes('invokej') ||
|
|
21
|
-
mainModule.includes('invj');
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Parse LOG_CATEGORIES from environment
|
|
25
|
-
// Format: "ws:debug,db:trace,auth:info" or "*:debug" for all
|
|
26
|
-
const parseCategories = () => {
|
|
27
|
-
const categories = {};
|
|
28
|
-
const envCategories = process.env.LOG_CATEGORIES || "";
|
|
29
|
-
|
|
30
|
-
if (!envCategories) {
|
|
31
|
-
// Default settings for development vs production
|
|
32
|
-
// CLI context defaults to ERROR level unless explicitly configured
|
|
33
|
-
if (process.env.NODE_ENV === "production" || isCliContext()) {
|
|
34
|
-
categories["*"] = LOG_LEVELS.ERROR; // Only errors in production/CLI
|
|
35
|
-
} else if (process.env.NODE_ENV === "test") {
|
|
36
|
-
categories["*"] = LOG_LEVELS.ERROR;
|
|
37
|
-
} else {
|
|
38
|
-
// Development defaults
|
|
39
|
-
categories["*"] = LOG_LEVELS[DEFAULT_LEVEL] ?? LOG_LEVELS.INFO;
|
|
40
|
-
}
|
|
41
|
-
return categories;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Parse category:level pairs
|
|
45
|
-
envCategories.split(",").forEach((pair) => {
|
|
46
|
-
const [category, level] = pair.trim().split(":");
|
|
47
|
-
if (category && level) {
|
|
48
|
-
const levelValue = LOG_LEVELS[level.toUpperCase()];
|
|
49
|
-
if (levelValue !== undefined) {
|
|
50
|
-
categories[category] = levelValue;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Set default for non-specified categories
|
|
56
|
-
if (!categories["*"]) {
|
|
57
|
-
categories["*"] = LOG_LEVELS[DEFAULT_LEVEL] ?? LOG_LEVELS.INFO;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return categories;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Initialize categories
|
|
64
|
-
let logCategories = parseCategories();
|
|
65
|
-
|
|
66
|
-
// Colors for terminal output
|
|
67
|
-
const colors = {
|
|
68
|
-
reset: "\x1b[0m",
|
|
69
|
-
bright: "\x1b[1m",
|
|
70
|
-
dim: "\x1b[2m",
|
|
71
|
-
red: "\x1b[31m",
|
|
72
|
-
green: "\x1b[32m",
|
|
73
|
-
yellow: "\x1b[33m",
|
|
74
|
-
blue: "\x1b[34m",
|
|
75
|
-
magenta: "\x1b[35m",
|
|
76
|
-
cyan: "\x1b[36m",
|
|
77
|
-
white: "\x1b[37m",
|
|
78
|
-
gray: "\x1b[90m",
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Level colors
|
|
82
|
-
const levelColors = {
|
|
83
|
-
ERROR: colors.red,
|
|
84
|
-
WARN: colors.yellow,
|
|
85
|
-
INFO: colors.blue,
|
|
86
|
-
DEBUG: colors.cyan,
|
|
87
|
-
TRACE: colors.gray,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Category colors
|
|
91
|
-
const categoryColors = {
|
|
92
|
-
ws: colors.green,
|
|
93
|
-
db: colors.magenta,
|
|
94
|
-
auth: colors.yellow,
|
|
95
|
-
api: colors.cyan,
|
|
96
|
-
server: colors.blue,
|
|
97
|
-
notify: colors.magenta,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Format timestamp
|
|
101
|
-
const timestamp = () => {
|
|
102
|
-
const now = new Date();
|
|
103
|
-
return now.toISOString().replace("T", " ").slice(0, -5);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Check if logging is enabled for category and level
|
|
107
|
-
const shouldLog = (category, level) => {
|
|
108
|
-
const categoryLevel = logCategories[category] ?? logCategories["*"] ?? LOG_LEVELS.INFO;
|
|
109
|
-
return LOG_LEVELS[level] <= categoryLevel;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Format log message with colors
|
|
113
|
-
const formatMessage = (category, level, message, ...args) => {
|
|
114
|
-
const useColors = process.env.NO_COLOR !== "1" && process.env.NODE_ENV !== "test";
|
|
115
|
-
|
|
116
|
-
if (useColors) {
|
|
117
|
-
const catColor = categoryColors[category] || colors.white;
|
|
118
|
-
const lvlColor = levelColors[level];
|
|
119
|
-
|
|
120
|
-
return [
|
|
121
|
-
`${colors.gray}${timestamp()}${colors.reset}`,
|
|
122
|
-
`${lvlColor}[${level}]${colors.reset}`,
|
|
123
|
-
`${catColor}[${category}]${colors.reset}`,
|
|
124
|
-
message,
|
|
125
|
-
...args,
|
|
126
|
-
];
|
|
127
|
-
} else {
|
|
128
|
-
return [
|
|
129
|
-
timestamp(),
|
|
130
|
-
`[${level}]`,
|
|
131
|
-
`[${category}]`,
|
|
132
|
-
message,
|
|
133
|
-
...args,
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
// Create logger for a specific category
|
|
139
|
-
export const createLogger = (category) => {
|
|
140
|
-
return {
|
|
141
|
-
error: (message, ...args) => {
|
|
142
|
-
if (shouldLog(category, "ERROR")) {
|
|
143
|
-
console.error(...formatMessage(category, "ERROR", message, ...args));
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
warn: (message, ...args) => {
|
|
147
|
-
if (shouldLog(category, "WARN")) {
|
|
148
|
-
console.warn(...formatMessage(category, "WARN", message, ...args));
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
info: (message, ...args) => {
|
|
152
|
-
if (shouldLog(category, "INFO")) {
|
|
153
|
-
console.log(...formatMessage(category, "INFO", message, ...args));
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
debug: (message, ...args) => {
|
|
157
|
-
if (shouldLog(category, "DEBUG")) {
|
|
158
|
-
console.log(...formatMessage(category, "DEBUG", message, ...args));
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
trace: (message, ...args) => {
|
|
162
|
-
if (shouldLog(category, "TRACE")) {
|
|
163
|
-
console.log(...formatMessage(category, "TRACE", message, ...args));
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
// Special method for request/response logging
|
|
167
|
-
request: (method, params) => {
|
|
168
|
-
if (shouldLog(category, "DEBUG")) {
|
|
169
|
-
console.log(...formatMessage(
|
|
170
|
-
category,
|
|
171
|
-
"DEBUG",
|
|
172
|
-
`→ ${method}`,
|
|
173
|
-
params ? JSON.stringify(params) : "no params"
|
|
174
|
-
));
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
response: (method, result, duration) => {
|
|
178
|
-
if (shouldLog(category, "DEBUG")) {
|
|
179
|
-
const resultStr = result === undefined
|
|
180
|
-
? "void"
|
|
181
|
-
: typeof result === "object"
|
|
182
|
-
? `${JSON.stringify(result).slice(0, 100)}...`
|
|
183
|
-
: result;
|
|
184
|
-
console.log(...formatMessage(
|
|
185
|
-
category,
|
|
186
|
-
"DEBUG",
|
|
187
|
-
`← ${method} (${duration}ms)`,
|
|
188
|
-
resultStr
|
|
189
|
-
));
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// Pre-configured loggers for common categories
|
|
196
|
-
export const wsLogger = createLogger("ws");
|
|
197
|
-
export const dbLogger = createLogger("db");
|
|
198
|
-
export const authLogger = createLogger("auth");
|
|
199
|
-
export const serverLogger = createLogger("server");
|
|
200
|
-
export const notifyLogger = createLogger("notify");
|
|
201
|
-
|
|
202
|
-
// Reload configuration (useful for runtime changes)
|
|
203
|
-
export const reloadConfig = () => {
|
|
204
|
-
logCategories = parseCategories();
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// Get current configuration
|
|
208
|
-
export const getConfig = () => {
|
|
209
|
-
return {
|
|
210
|
-
categories: logCategories,
|
|
211
|
-
levels: LOG_LEVELS,
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Middleware for timing operations
|
|
216
|
-
export const timed = async (category, operation, fn) => {
|
|
217
|
-
const logger = createLogger(category);
|
|
218
|
-
const start = Date.now();
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
logger.trace(`Starting ${operation}`);
|
|
222
|
-
const result = await fn();
|
|
223
|
-
const duration = Date.now() - start;
|
|
224
|
-
logger.debug(`Completed ${operation} in ${duration}ms`);
|
|
225
|
-
return result;
|
|
226
|
-
} catch (error) {
|
|
227
|
-
const duration = Date.now() - start;
|
|
228
|
-
logger.error(`Failed ${operation} after ${duration}ms:`, error.message);
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// Log configuration on startup (only in development and when explicitly debugging)
|
|
234
|
-
// Suppress banner if LOG_CATEGORIES is not set (user doesn't care about logging config)
|
|
235
|
-
if (process.env.NODE_ENV !== "production" &&
|
|
236
|
-
process.env.NODE_ENV !== "test" &&
|
|
237
|
-
process.env.LOG_CATEGORIES) {
|
|
238
|
-
const config = getConfig();
|
|
239
|
-
console.log(colors.bright + "=== Logger Configuration ===" + colors.reset);
|
|
240
|
-
console.log("Categories:", config.categories);
|
|
241
|
-
console.log("Available levels:", Object.keys(config.levels).join(", "));
|
|
242
|
-
console.log("");
|
|
243
|
-
console.log("Set LOG_CATEGORIES env var to configure:");
|
|
244
|
-
console.log(' Example: LOG_CATEGORIES="ws:debug,db:trace,auth:info"');
|
|
245
|
-
console.log(' Or use "*:debug" to set all categories to debug');
|
|
246
|
-
console.log(colors.bright + "===========================" + colors.reset + "\n");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export default {
|
|
250
|
-
createLogger,
|
|
251
|
-
wsLogger,
|
|
252
|
-
dbLogger,
|
|
253
|
-
authLogger,
|
|
254
|
-
serverLogger,
|
|
255
|
-
notifyLogger,
|
|
256
|
-
reloadConfig,
|
|
257
|
-
getConfig,
|
|
258
|
-
timed,
|
|
259
|
-
};
|