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.
Files changed (142) 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 +309 -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 +653 -0
  20. package/docs/project-setup.md +456 -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 +166 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/runtime/auth.ts +39 -0
  42. package/src/runtime/db.ts +33 -0
  43. package/src/runtime/errors.ts +51 -0
  44. package/src/runtime/index.ts +98 -0
  45. package/src/runtime/js_functions.ts +63 -0
  46. package/src/runtime/manifest_loader.ts +29 -0
  47. package/src/runtime/namespace.ts +483 -0
  48. package/src/runtime/server.ts +87 -0
  49. package/src/runtime/ws.ts +197 -0
  50. package/src/shared/ir.ts +197 -0
  51. package/tests/client.test.ts +38 -0
  52. package/tests/codegen.test.ts +71 -0
  53. package/tests/compiler.test.ts +45 -0
  54. package/tests/graph_rules.test.ts +173 -0
  55. package/tests/integration/db.test.ts +174 -0
  56. package/tests/integration/e2e.test.ts +65 -0
  57. package/tests/integration/features.test.ts +922 -0
  58. package/tests/integration/full_stack.test.ts +262 -0
  59. package/tests/integration/setup.ts +45 -0
  60. package/tests/ir.test.ts +32 -0
  61. package/tests/namespace.test.ts +395 -0
  62. package/tests/permissions.test.ts +55 -0
  63. package/tests/pinia.test.ts +48 -0
  64. package/tests/realtime.test.ts +22 -0
  65. package/tests/runtime.test.ts +80 -0
  66. package/tests/subscribable_gen.test.ts +72 -0
  67. package/tests/subscribable_reactivity.test.ts +258 -0
  68. package/tests/venues_gen.test.ts +25 -0
  69. package/tsconfig.json +20 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/README.md +0 -90
  72. package/bin/cli.js +0 -727
  73. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  74. package/docs/compiler/CODING_STANDARDS.md +0 -415
  75. package/docs/compiler/COMPARISON.md +0 -673
  76. package/docs/compiler/QUICKSTART.md +0 -326
  77. package/docs/compiler/README.md +0 -134
  78. package/docs/examples/README.md +0 -38
  79. package/docs/examples/blog.sql +0 -160
  80. package/docs/examples/venue-detail-simple.sql +0 -8
  81. package/docs/examples/venue-detail-subscribable.sql +0 -45
  82. package/docs/for-ai/claude-guide.md +0 -1210
  83. package/docs/getting-started/quickstart.md +0 -125
  84. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  85. package/docs/getting-started/tutorial.md +0 -1104
  86. package/docs/guides/atomic-updates.md +0 -299
  87. package/docs/guides/client-stores.md +0 -730
  88. package/docs/guides/composite-primary-keys.md +0 -158
  89. package/docs/guides/custom-functions.md +0 -362
  90. package/docs/guides/drop-semantics.md +0 -554
  91. package/docs/guides/field-defaults.md +0 -240
  92. package/docs/guides/interpreter-vs-compiler.md +0 -237
  93. package/docs/guides/many-to-many.md +0 -929
  94. package/docs/guides/subscriptions.md +0 -537
  95. package/docs/reference/api.md +0 -1373
  96. package/docs/reference/client.md +0 -224
  97. package/src/client/stores/index.js +0 -8
  98. package/src/client/stores/useAppStore.js +0 -285
  99. package/src/client/stores/useWsStore.js +0 -289
  100. package/src/client/ws.js +0 -762
  101. package/src/compiler/cli/compile-example.js +0 -33
  102. package/src/compiler/cli/compile-subscribable.js +0 -43
  103. package/src/compiler/cli/debug-compile.js +0 -44
  104. package/src/compiler/cli/debug-parse.js +0 -26
  105. package/src/compiler/cli/debug-path-parser.js +0 -18
  106. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  107. package/src/compiler/cli/index.js +0 -174
  108. package/src/compiler/codegen/auth-codegen.js +0 -153
  109. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  110. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  111. package/src/compiler/codegen/notification-codegen.js +0 -232
  112. package/src/compiler/codegen/operation-codegen.js +0 -1382
  113. package/src/compiler/codegen/permission-codegen.js +0 -318
  114. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  115. package/src/compiler/compiler.js +0 -371
  116. package/src/compiler/index.js +0 -11
  117. package/src/compiler/parser/entity-parser.js +0 -440
  118. package/src/compiler/parser/path-parser.js +0 -290
  119. package/src/compiler/parser/subscribable-parser.js +0 -244
  120. package/src/database/dzql-core.sql +0 -161
  121. package/src/database/migrations/001_schema.sql +0 -60
  122. package/src/database/migrations/002_functions.sql +0 -890
  123. package/src/database/migrations/003_operations.sql +0 -1135
  124. package/src/database/migrations/004_search.sql +0 -581
  125. package/src/database/migrations/005_entities.sql +0 -730
  126. package/src/database/migrations/006_auth.sql +0 -94
  127. package/src/database/migrations/007_events.sql +0 -133
  128. package/src/database/migrations/008_hello.sql +0 -18
  129. package/src/database/migrations/008a_meta.sql +0 -172
  130. package/src/database/migrations/009_subscriptions.sql +0 -240
  131. package/src/database/migrations/010_atomic_updates.sql +0 -157
  132. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  133. package/src/index.js +0 -40
  134. package/src/server/api.js +0 -9
  135. package/src/server/db.js +0 -442
  136. package/src/server/index.js +0 -317
  137. package/src/server/logger.js +0 -259
  138. package/src/server/mcp.js +0 -594
  139. package/src/server/meta-route.js +0 -251
  140. package/src/server/namespace.js +0 -292
  141. package/src/server/subscriptions.js +0 -351
  142. 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
- };