dzql 0.1.0-alpha.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.
@@ -0,0 +1,141 @@
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
+
6
+ // Re-export commonly used utilities
7
+ export { sql, db } from "./db.js";
8
+ export { metaRoute } from "./meta-route.js";
9
+ export { createMCPRoute } from "./mcp.js";
10
+
11
+ export function createServer(options = {}) {
12
+ const {
13
+ port = process.env.PORT || 3000,
14
+ customApi = {},
15
+ routes = {},
16
+ staticPath = null // No default static path - applications should specify
17
+ } = options;
18
+
19
+ // Merge default API with custom API
20
+ const api = { ...defaultApi, ...customApi };
21
+
22
+ // Create WebSocket event handlers
23
+ const { broadcast, ...websocketHandlers } = createWebSocketHandlers({
24
+ customHandlers: api,
25
+ });
26
+
27
+ // Setup NOTIFY listeners for real-time events
28
+ setupListeners((event) => {
29
+ // Handle single dzql event with filtering
30
+ const { notify_users, ...eventData } = event;
31
+
32
+ // Create JSON-RPC notification
33
+ const message = JSON.stringify({
34
+ jsonrpc: "2.0",
35
+ method: `${event.table}:${event.op}`, // e.g., "venues:update"
36
+ params: eventData,
37
+ });
38
+
39
+ // Filter based on notify_users (null = broadcast to all)
40
+ if (notify_users && notify_users.length > 0) {
41
+ // Send to specific users only
42
+ notifyLogger.debug(`Broadcasting ${event.table}:${event.op} to ${notify_users.length} users`);
43
+ broadcast(message, notify_users);
44
+ } else {
45
+ // Send to all connected users
46
+ notifyLogger.debug(`Broadcasting ${event.table}:${event.op} to all users`);
47
+ broadcast(message);
48
+ }
49
+ });
50
+
51
+ routes['/health'] = () => new Response("OK", { status: 200 });
52
+
53
+ // Create and start the Bun server
54
+ const server = Bun.serve({
55
+ port,
56
+ routes,
57
+ async fetch(req, server) {
58
+ const url = new URL(req.url);
59
+
60
+ // WebSocket upgrade path
61
+ if (url.pathname === "/ws") {
62
+ // Extract token from Authorization header or query param
63
+ const auth_header = req.headers.get("Authorization");
64
+ const token =
65
+ auth_header?.replace("Bearer ", "") || url.searchParams.get("token");
66
+
67
+ let user_data = null;
68
+
69
+ // Verify JWT if provided
70
+ if (token) {
71
+ const payload = await verify_jwt_token(token);
72
+ if (payload) {
73
+ user_data = {
74
+ user_id: payload.user_id,
75
+ email: payload.email,
76
+ };
77
+ }
78
+ }
79
+
80
+ // Upgrade to WebSocket (allow anonymous for login/register)
81
+ const success = server.upgrade(req, {
82
+ data: user_data || {},
83
+ });
84
+
85
+ if (success) return undefined;
86
+ return new Response("WebSocket upgrade failed", { status: 400 });
87
+ }
88
+
89
+ // Static file serving (only if staticPath is configured)
90
+ if (staticPath) {
91
+ let filePath = url.pathname;
92
+
93
+ // Default to index.html for root or directory requests
94
+ if (!filePath || filePath === "/") {
95
+ filePath = "/index.html";
96
+ }
97
+
98
+ const file = Bun.file(`${staticPath}${filePath}`);
99
+ if (await file.exists()) {
100
+ return new Response(file);
101
+ }
102
+ }
103
+
104
+ return new Response("Not Found", { status: 404 });
105
+ },
106
+
107
+ websocket: websocketHandlers,
108
+ });
109
+
110
+ serverLogger.info(`🚀 DZQL server started`);
111
+ serverLogger.info(` HTTP: http://localhost:${port}`);
112
+ serverLogger.info(` WebSocket: ws://localhost:${port}/ws`);
113
+ serverLogger.info(` Environment: ${process.env.NODE_ENV || "development"}`);
114
+ serverLogger.info(` WS Ping Interval: ${process.env.WS_PING_INTERVAL || 30000}ms (Heroku safe: <55s)`);
115
+
116
+ // Add graceful shutdown handling
117
+ const shutdown = async () => {
118
+ serverLogger.info("Shutting down DZQL server...");
119
+ await closeConnections();
120
+ serverLogger.info("Server shutdown complete");
121
+ };
122
+
123
+ // Return server instance with utilities
124
+ return {
125
+ port,
126
+ server,
127
+ shutdown,
128
+ broadcast
129
+ };
130
+ }
131
+
132
+ // If this file is run directly (not imported), start the server
133
+ if (import.meta.main) {
134
+ const server = createServer();
135
+
136
+ // Graceful shutdown
137
+ process.on("SIGINT", async () => {
138
+ await server.shutdown();
139
+ process.exit(0);
140
+ });
141
+ }
@@ -0,0 +1,246 @@
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
+ // Parse LOG_CATEGORIES from environment
16
+ // Format: "ws:debug,db:trace,auth:info" or "*:debug" for all
17
+ const parseCategories = () => {
18
+ const categories = {};
19
+ const envCategories = process.env.LOG_CATEGORIES || "";
20
+
21
+ if (!envCategories) {
22
+ // Default settings for development vs production
23
+ if (process.env.NODE_ENV === "production") {
24
+ categories["*"] = LOG_LEVELS.WARN;
25
+ } else if (process.env.NODE_ENV === "test") {
26
+ categories["*"] = LOG_LEVELS.ERROR;
27
+ } else {
28
+ // Development defaults
29
+ categories["*"] = LOG_LEVELS[DEFAULT_LEVEL] ?? LOG_LEVELS.INFO;
30
+ }
31
+ return categories;
32
+ }
33
+
34
+ // Parse category:level pairs
35
+ envCategories.split(",").forEach((pair) => {
36
+ const [category, level] = pair.trim().split(":");
37
+ if (category && level) {
38
+ const levelValue = LOG_LEVELS[level.toUpperCase()];
39
+ if (levelValue !== undefined) {
40
+ categories[category] = levelValue;
41
+ }
42
+ }
43
+ });
44
+
45
+ // Set default for non-specified categories
46
+ if (!categories["*"]) {
47
+ categories["*"] = LOG_LEVELS[DEFAULT_LEVEL] ?? LOG_LEVELS.INFO;
48
+ }
49
+
50
+ return categories;
51
+ };
52
+
53
+ // Initialize categories
54
+ let logCategories = parseCategories();
55
+
56
+ // Colors for terminal output
57
+ const colors = {
58
+ reset: "\x1b[0m",
59
+ bright: "\x1b[1m",
60
+ dim: "\x1b[2m",
61
+ red: "\x1b[31m",
62
+ green: "\x1b[32m",
63
+ yellow: "\x1b[33m",
64
+ blue: "\x1b[34m",
65
+ magenta: "\x1b[35m",
66
+ cyan: "\x1b[36m",
67
+ white: "\x1b[37m",
68
+ gray: "\x1b[90m",
69
+ };
70
+
71
+ // Level colors
72
+ const levelColors = {
73
+ ERROR: colors.red,
74
+ WARN: colors.yellow,
75
+ INFO: colors.blue,
76
+ DEBUG: colors.cyan,
77
+ TRACE: colors.gray,
78
+ };
79
+
80
+ // Category colors
81
+ const categoryColors = {
82
+ ws: colors.green,
83
+ db: colors.magenta,
84
+ auth: colors.yellow,
85
+ api: colors.cyan,
86
+ server: colors.blue,
87
+ notify: colors.magenta,
88
+ };
89
+
90
+ // Format timestamp
91
+ const timestamp = () => {
92
+ const now = new Date();
93
+ return now.toISOString().replace("T", " ").slice(0, -5);
94
+ };
95
+
96
+ // Check if logging is enabled for category and level
97
+ const shouldLog = (category, level) => {
98
+ const categoryLevel = logCategories[category] ?? logCategories["*"] ?? LOG_LEVELS.INFO;
99
+ return LOG_LEVELS[level] <= categoryLevel;
100
+ };
101
+
102
+ // Format log message with colors
103
+ const formatMessage = (category, level, message, ...args) => {
104
+ const useColors = process.env.NO_COLOR !== "1" && process.env.NODE_ENV !== "test";
105
+
106
+ if (useColors) {
107
+ const catColor = categoryColors[category] || colors.white;
108
+ const lvlColor = levelColors[level];
109
+
110
+ return [
111
+ `${colors.gray}${timestamp()}${colors.reset}`,
112
+ `${lvlColor}[${level}]${colors.reset}`,
113
+ `${catColor}[${category}]${colors.reset}`,
114
+ message,
115
+ ...args,
116
+ ];
117
+ } else {
118
+ return [
119
+ timestamp(),
120
+ `[${level}]`,
121
+ `[${category}]`,
122
+ message,
123
+ ...args,
124
+ ];
125
+ }
126
+ };
127
+
128
+ // Create logger for a specific category
129
+ export const createLogger = (category) => {
130
+ return {
131
+ error: (message, ...args) => {
132
+ if (shouldLog(category, "ERROR")) {
133
+ console.error(...formatMessage(category, "ERROR", message, ...args));
134
+ }
135
+ },
136
+ warn: (message, ...args) => {
137
+ if (shouldLog(category, "WARN")) {
138
+ console.warn(...formatMessage(category, "WARN", message, ...args));
139
+ }
140
+ },
141
+ info: (message, ...args) => {
142
+ if (shouldLog(category, "INFO")) {
143
+ console.log(...formatMessage(category, "INFO", message, ...args));
144
+ }
145
+ },
146
+ debug: (message, ...args) => {
147
+ if (shouldLog(category, "DEBUG")) {
148
+ console.log(...formatMessage(category, "DEBUG", message, ...args));
149
+ }
150
+ },
151
+ trace: (message, ...args) => {
152
+ if (shouldLog(category, "TRACE")) {
153
+ console.log(...formatMessage(category, "TRACE", message, ...args));
154
+ }
155
+ },
156
+ // Special method for request/response logging
157
+ request: (method, params) => {
158
+ if (shouldLog(category, "DEBUG")) {
159
+ console.log(...formatMessage(
160
+ category,
161
+ "DEBUG",
162
+ `→ ${method}`,
163
+ params ? JSON.stringify(params) : "no params"
164
+ ));
165
+ }
166
+ },
167
+ response: (method, result, duration) => {
168
+ if (shouldLog(category, "DEBUG")) {
169
+ const resultStr = result === undefined
170
+ ? "void"
171
+ : typeof result === "object"
172
+ ? `${JSON.stringify(result).slice(0, 100)}...`
173
+ : result;
174
+ console.log(...formatMessage(
175
+ category,
176
+ "DEBUG",
177
+ `← ${method} (${duration}ms)`,
178
+ resultStr
179
+ ));
180
+ }
181
+ },
182
+ };
183
+ };
184
+
185
+ // Pre-configured loggers for common categories
186
+ export const wsLogger = createLogger("ws");
187
+ export const dbLogger = createLogger("db");
188
+ export const authLogger = createLogger("auth");
189
+ export const serverLogger = createLogger("server");
190
+ export const notifyLogger = createLogger("notify");
191
+
192
+ // Reload configuration (useful for runtime changes)
193
+ export const reloadConfig = () => {
194
+ logCategories = parseCategories();
195
+ };
196
+
197
+ // Get current configuration
198
+ export const getConfig = () => {
199
+ return {
200
+ categories: logCategories,
201
+ levels: LOG_LEVELS,
202
+ };
203
+ };
204
+
205
+ // Middleware for timing operations
206
+ export const timed = async (category, operation, fn) => {
207
+ const logger = createLogger(category);
208
+ const start = Date.now();
209
+
210
+ try {
211
+ logger.trace(`Starting ${operation}`);
212
+ const result = await fn();
213
+ const duration = Date.now() - start;
214
+ logger.debug(`Completed ${operation} in ${duration}ms`);
215
+ return result;
216
+ } catch (error) {
217
+ const duration = Date.now() - start;
218
+ logger.error(`Failed ${operation} after ${duration}ms:`, error.message);
219
+ throw error;
220
+ }
221
+ };
222
+
223
+ // Log configuration on startup (only in development)
224
+ if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test") {
225
+ const config = getConfig();
226
+ console.log(colors.bright + "=== Logger Configuration ===" + colors.reset);
227
+ console.log("Categories:", config.categories);
228
+ console.log("Available levels:", Object.keys(config.levels).join(", "));
229
+ console.log("");
230
+ console.log("Set LOG_CATEGORIES env var to configure:");
231
+ console.log(' Example: LOG_CATEGORIES="ws:debug,db:trace,auth:info"');
232
+ console.log(' Or use "*:debug" to set all categories to debug');
233
+ console.log(colors.bright + "===========================" + colors.reset + "\n");
234
+ }
235
+
236
+ export default {
237
+ createLogger,
238
+ wsLogger,
239
+ dbLogger,
240
+ authLogger,
241
+ serverLogger,
242
+ notifyLogger,
243
+ reloadConfig,
244
+ getConfig,
245
+ timed,
246
+ };