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.
- package/package.json +65 -0
- package/src/client/ui-configs/sample-2.js +207 -0
- package/src/client/ui-loader.js +618 -0
- package/src/client/ui.js +990 -0
- package/src/client/ws.js +352 -0
- package/src/client.js +9 -0
- package/src/database/migrations/001_schema.sql +59 -0
- package/src/database/migrations/002_functions.sql +742 -0
- package/src/database/migrations/003_operations.sql +725 -0
- package/src/database/migrations/004_search.sql +505 -0
- package/src/database/migrations/005_entities.sql +511 -0
- package/src/database/migrations/006_auth.sql +83 -0
- package/src/database/migrations/007_events.sql +136 -0
- package/src/database/migrations/008_hello.sql +18 -0
- package/src/database/migrations/008a_meta.sql +165 -0
- package/src/index.js +19 -0
- package/src/server/api.js +9 -0
- package/src/server/db.js +261 -0
- package/src/server/index.js +141 -0
- package/src/server/logger.js +246 -0
- package/src/server/mcp.js +594 -0
- package/src/server/meta-route.js +251 -0
- package/src/server/ws.js +464 -0
|
@@ -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
|
+
};
|