dzql 0.5.32 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +293 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +641 -0
  20. package/docs/project-setup.md +432 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. package/src/server/ws.js +0 -573
@@ -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
- }
@@ -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
- };