dzql 0.1.0-alpha.3 → 0.1.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.
- package/CLAUDE.md +931 -0
- package/GETTING_STARTED.md +2 -0
- package/README.md +19 -483
- package/REFERENCE.md +891 -0
- package/package.json +4 -4
- package/src/client/ws.js +186 -0
- package/src/server/db.js +73 -0
- package/src/server/index.js +71 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "PostgreSQL-powered framework with zero boilerplate CRUD operations and real-time WebSocket synchronization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server/index.js",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"src/database/migrations/**/*.sql",
|
|
16
16
|
"README.md",
|
|
17
17
|
"GETTING_STARTED.md",
|
|
18
|
+
"REFERENCE.md",
|
|
19
|
+
"CLAUDE.md",
|
|
18
20
|
"LICENSE"
|
|
19
21
|
],
|
|
20
22
|
"scripts": {
|
|
@@ -25,7 +27,6 @@
|
|
|
25
27
|
"jose": "^6.1.0",
|
|
26
28
|
"postgres": "^3.4.7"
|
|
27
29
|
},
|
|
28
|
-
|
|
29
30
|
"keywords": [
|
|
30
31
|
"postgresql",
|
|
31
32
|
"postgres",
|
|
@@ -61,7 +62,6 @@
|
|
|
61
62
|
"bun": ">=1.0.0"
|
|
62
63
|
},
|
|
63
64
|
"publishConfig": {
|
|
64
|
-
"access": "public"
|
|
65
|
-
"tag": "alpha"
|
|
65
|
+
"access": "public"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/client/ws.js
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket manager for DZQL client-side real-time communication
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - WebSocket connection management with auto-reconnect
|
|
6
|
+
* - JSON-RPC 2.0 protocol for API calls
|
|
7
|
+
* - Proxy-based API matching server-side db.api pattern
|
|
8
|
+
* - Real-time broadcast event handling
|
|
9
|
+
* - Automatic JWT authentication
|
|
10
|
+
*
|
|
11
|
+
* @class WebSocketManager
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Basic usage
|
|
15
|
+
* import { WebSocketManager } from 'dzql/client';
|
|
16
|
+
*
|
|
17
|
+
* const ws = new WebSocketManager();
|
|
18
|
+
* await ws.connect('ws://localhost:3000/ws');
|
|
19
|
+
*
|
|
20
|
+
* // Login
|
|
21
|
+
* const session = await ws.api.login_user({
|
|
22
|
+
* email: 'user@example.com',
|
|
23
|
+
* password: 'password123'
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // CRUD operations
|
|
27
|
+
* const venue = await ws.api.get.venues({ id: 1 });
|
|
28
|
+
* const created = await ws.api.save.venues({ name: 'New Venue' });
|
|
29
|
+
*
|
|
30
|
+
* // Listen to real-time updates
|
|
31
|
+
* ws.onBroadcast((method, params) => {
|
|
32
|
+
* console.log(`Event: ${method}`, params);
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Advanced search
|
|
37
|
+
* const results = await ws.api.search.venues({
|
|
38
|
+
* filters: {
|
|
39
|
+
* city: 'New York',
|
|
40
|
+
* capacity: { gte: 1000 },
|
|
41
|
+
* _search: 'garden'
|
|
42
|
+
* },
|
|
43
|
+
* sort: { field: 'name', order: 'asc' },
|
|
44
|
+
* page: 1,
|
|
45
|
+
* limit: 25
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
1
48
|
// Pure WebSocket manager class (no React dependencies)
|
|
2
49
|
class WebSocketManager {
|
|
50
|
+
/**
|
|
51
|
+
* Create a WebSocketManager instance
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} [options={}] - Configuration options
|
|
54
|
+
* @param {number} [options.maxReconnectAttempts=5] - Maximum reconnection attempts before giving up
|
|
55
|
+
*/
|
|
3
56
|
constructor(options = {}) {
|
|
4
57
|
this.ws = null;
|
|
5
58
|
this.messageId = 0;
|
|
@@ -22,6 +75,62 @@ class WebSocketManager {
|
|
|
22
75
|
search: this.createEntityProxy("search"),
|
|
23
76
|
};
|
|
24
77
|
|
|
78
|
+
/**
|
|
79
|
+
* API proxy for calling DZQL operations and custom functions
|
|
80
|
+
*
|
|
81
|
+
* @member {Object} api
|
|
82
|
+
* @memberof WebSocketManager
|
|
83
|
+
*
|
|
84
|
+
* @property {Object} get - Get single record by primary key
|
|
85
|
+
* @property {Object} save - Create or update record (upsert)
|
|
86
|
+
* @property {Object} delete - Delete record by primary key
|
|
87
|
+
* @property {Object} lookup - Autocomplete lookup by label field
|
|
88
|
+
* @property {Object} search - Advanced search with filters
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Get operation
|
|
92
|
+
* const venue = await ws.api.get.venues({ id: 1 });
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Save operation (create)
|
|
96
|
+
* const created = await ws.api.save.venues({
|
|
97
|
+
* name: 'New Venue',
|
|
98
|
+
* org_id: 3
|
|
99
|
+
* });
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* // Save operation (update)
|
|
103
|
+
* const updated = await ws.api.save.venues({
|
|
104
|
+
* id: 1,
|
|
105
|
+
* name: 'Updated Name'
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // Delete operation
|
|
110
|
+
* await ws.api.delete.venues({ id: 1 });
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // Lookup for autocomplete
|
|
114
|
+
* const results = await ws.api.lookup.venues({ p_filter: 'garden' });
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // Search with filters
|
|
118
|
+
* const results = await ws.api.search.venues({
|
|
119
|
+
* filters: {
|
|
120
|
+
* city: 'New York',
|
|
121
|
+
* capacity: { gte: 1000, lt: 5000 },
|
|
122
|
+
* name: { ilike: '%garden%' },
|
|
123
|
+
* _search: 'madison'
|
|
124
|
+
* },
|
|
125
|
+
* sort: { field: 'name', order: 'asc' },
|
|
126
|
+
* page: 1,
|
|
127
|
+
* limit: 25
|
|
128
|
+
* });
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* // Call custom function
|
|
132
|
+
* const result = await ws.api.myCustomFunction({ param: 'value' });
|
|
133
|
+
*/
|
|
25
134
|
this.api = new Proxy(dzqlOps, {
|
|
26
135
|
get: (target, prop) => {
|
|
27
136
|
// Return cached DZQL operation if it exists
|
|
@@ -102,6 +211,31 @@ class WebSocketManager {
|
|
|
102
211
|
);
|
|
103
212
|
}
|
|
104
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Connect to DZQL WebSocket server
|
|
216
|
+
*
|
|
217
|
+
* Automatically detects environment (browser vs Node.js) and constructs WebSocket URL.
|
|
218
|
+
* If JWT token exists in localStorage, automatically includes it in connection.
|
|
219
|
+
*
|
|
220
|
+
* @param {string|null} [url=null] - WebSocket URL (auto-detected if not provided)
|
|
221
|
+
* Browser: ws://current-host/ws or wss://current-host/ws
|
|
222
|
+
* Node.js: ws://localhost:3000/ws
|
|
223
|
+
* @param {number} [timeout=5000] - Connection timeout in milliseconds
|
|
224
|
+
*
|
|
225
|
+
* @returns {Promise<void>} Resolves when connected, rejects on timeout or error
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* // Auto-detect URL (browser)
|
|
229
|
+
* await ws.connect();
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* // Explicit URL
|
|
233
|
+
* await ws.connect('ws://localhost:3000/ws');
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Custom timeout
|
|
237
|
+
* await ws.connect(null, 10000); // 10 second timeout
|
|
238
|
+
*/
|
|
105
239
|
connect(url = null, timeout = 5000) {
|
|
106
240
|
return new Promise((resolve, reject) => {
|
|
107
241
|
let wsUrl;
|
|
@@ -214,6 +348,14 @@ class WebSocketManager {
|
|
|
214
348
|
}
|
|
215
349
|
}
|
|
216
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Call a method via JSON-RPC over WebSocket
|
|
353
|
+
*
|
|
354
|
+
* @private
|
|
355
|
+
* @param {string} method - Method name (e.g., 'login_user' or 'dzql.get.venues')
|
|
356
|
+
* @param {Object} [params={}] - Method parameters
|
|
357
|
+
* @returns {Promise<*>} Resolves with method result, rejects on error
|
|
358
|
+
*/
|
|
217
359
|
call(method, params = {}) {
|
|
218
360
|
return new Promise((resolve, reject) => {
|
|
219
361
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -234,6 +376,50 @@ class WebSocketManager {
|
|
|
234
376
|
});
|
|
235
377
|
}
|
|
236
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Register callback for real-time broadcast events
|
|
381
|
+
*
|
|
382
|
+
* Broadcasts are sent when data changes (insert/update/delete operations).
|
|
383
|
+
* Method format: "{table}:{operation}" (e.g., "venues:update")
|
|
384
|
+
*
|
|
385
|
+
* @param {Function} callback - Callback function (method, params) => void
|
|
386
|
+
* @returns {Function} Cleanup function to remove the callback
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* // Listen to all broadcasts
|
|
390
|
+
* ws.onBroadcast((method, params) => {
|
|
391
|
+
* console.log(`Event: ${method}`, params);
|
|
392
|
+
* });
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* // Listen to specific table events
|
|
396
|
+
* ws.onBroadcast((method, params) => {
|
|
397
|
+
* if (method === 'venues:update') {
|
|
398
|
+
* console.log('Venue updated:', params.after);
|
|
399
|
+
* }
|
|
400
|
+
* });
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* // With cleanup
|
|
404
|
+
* const cleanup = ws.onBroadcast((method, params) => {
|
|
405
|
+
* console.log(method, params);
|
|
406
|
+
* });
|
|
407
|
+
*
|
|
408
|
+
* // Later: stop listening
|
|
409
|
+
* cleanup();
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* // Event structure
|
|
413
|
+
* {
|
|
414
|
+
* table: 'venues',
|
|
415
|
+
* op: 'insert', // 'insert', 'update', or 'delete'
|
|
416
|
+
* pk: { id: 1 },
|
|
417
|
+
* before: null, // Old values (null for insert)
|
|
418
|
+
* after: { ... }, // New values (null for delete)
|
|
419
|
+
* user_id: 123,
|
|
420
|
+
* at: '2025-01-15T10:30:00Z'
|
|
421
|
+
* }
|
|
422
|
+
*/
|
|
237
423
|
onBroadcast(callback) {
|
|
238
424
|
this.broadcastCallbacks.add(callback);
|
|
239
425
|
return () => this.broadcastCallbacks.delete(callback);
|
package/src/server/db.js
CHANGED
|
@@ -190,6 +190,79 @@ function createEntityProxy(operation) {
|
|
|
190
190
|
);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* DZQL database API
|
|
195
|
+
*
|
|
196
|
+
* Provides server-side access to DZQL operations and custom PostgreSQL functions.
|
|
197
|
+
* All operations require explicit userId parameter (unlike client API which auto-injects).
|
|
198
|
+
*
|
|
199
|
+
* @namespace db.api
|
|
200
|
+
*
|
|
201
|
+
* @property {Object} get - Get single record by primary key
|
|
202
|
+
* @property {Object} save - Create or update record (upsert)
|
|
203
|
+
* @property {Object} delete - Delete record by primary key
|
|
204
|
+
* @property {Object} lookup - Autocomplete lookup by label field
|
|
205
|
+
* @property {Object} search - Advanced search with filters, pagination, sorting
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* // Get a single venue
|
|
209
|
+
* const venue = await db.api.get.venues({ id: 1 }, userId);
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* // Create a new venue
|
|
213
|
+
* const venue = await db.api.save.venues({
|
|
214
|
+
* name: 'Madison Square Garden',
|
|
215
|
+
* org_id: 3,
|
|
216
|
+
* address: '4 Pennsylvania Plaza, New York'
|
|
217
|
+
* }, userId);
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* // Update existing venue
|
|
221
|
+
* const updated = await db.api.save.venues({
|
|
222
|
+
* id: 1,
|
|
223
|
+
* name: 'Updated Name'
|
|
224
|
+
* }, userId);
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* // Delete venue
|
|
228
|
+
* await db.api.delete.venues({ id: 1 }, userId);
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* // Lookup for autocomplete
|
|
232
|
+
* const results = await db.api.lookup.venues({
|
|
233
|
+
* p_filter: 'garden' // Searches label field
|
|
234
|
+
* }, userId);
|
|
235
|
+
* // Returns: [{ value: 1, label: 'Madison Square Garden' }, ...]
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* // Advanced search with filters
|
|
239
|
+
* const results = await db.api.search.venues({
|
|
240
|
+
* p_filters: {
|
|
241
|
+
* city: 'New York',
|
|
242
|
+
* capacity: { gte: 1000 },
|
|
243
|
+
* _search: 'garden' // Full-text search
|
|
244
|
+
* },
|
|
245
|
+
* p_sort: { field: 'name', order: 'asc' },
|
|
246
|
+
* p_page: 1,
|
|
247
|
+
* p_limit: 25
|
|
248
|
+
* }, userId);
|
|
249
|
+
* // Returns: { data: [...], total: 100, page: 1, limit: 25 }
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* // Call custom PostgreSQL function
|
|
253
|
+
* const stats = await db.api.myCustomFunction({ param1: 'value' }, userId);
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* // Call auth functions (no userId required)
|
|
257
|
+
* const user = await db.api.register_user({
|
|
258
|
+
* email: 'user@example.com',
|
|
259
|
+
* password: 'secure123'
|
|
260
|
+
* });
|
|
261
|
+
* const session = await db.api.login_user({
|
|
262
|
+
* email: 'user@example.com',
|
|
263
|
+
* password: 'secure123'
|
|
264
|
+
* });
|
|
265
|
+
*/
|
|
193
266
|
// DZQL database API proxy with custom function support
|
|
194
267
|
export const db = {
|
|
195
268
|
api: new Proxy(
|
package/src/server/index.js
CHANGED
|
@@ -8,6 +8,77 @@ export { sql, db } from "./db.js";
|
|
|
8
8
|
export { metaRoute } from "./meta-route.js";
|
|
9
9
|
export { createMCPRoute } from "./mcp.js";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Create a DZQL server with WebSocket support, real-time updates, and automatic CRUD operations
|
|
13
|
+
*
|
|
14
|
+
* Sets up a Bun server with:
|
|
15
|
+
* - WebSocket endpoint at /ws for real-time communication
|
|
16
|
+
* - JSON-RPC 2.0 protocol for API calls
|
|
17
|
+
* - PostgreSQL NOTIFY/LISTEN for real-time broadcasts
|
|
18
|
+
* - Automatic JWT authentication
|
|
19
|
+
* - Health check endpoint at /health
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} [options={}] - Server configuration options
|
|
22
|
+
* @param {number} [options.port=3000] - Port number to listen on (or process.env.PORT)
|
|
23
|
+
* @param {Object} [options.customApi={}] - Custom Bun functions to expose via WebSocket API
|
|
24
|
+
* Each function receives (userId, params) and can return any JSON-serializable value
|
|
25
|
+
* @param {Object} [options.routes={}] - Additional HTTP routes as { path: handlerFunction }
|
|
26
|
+
* @param {string|null} [options.staticPath=null] - Path to static files directory for serving
|
|
27
|
+
* @param {Function} [options.onReady=null] - Callback invoked after server initialization
|
|
28
|
+
* Receives { broadcast, routes } to allow dynamic route setup
|
|
29
|
+
*
|
|
30
|
+
* @returns {Object} Server instance with the following properties:
|
|
31
|
+
* @returns {number} .port - The port number the server is listening on
|
|
32
|
+
* @returns {Object} .server - The underlying Bun.Server instance
|
|
33
|
+
* @returns {Function} .shutdown - Async function to gracefully shutdown server and close DB connections
|
|
34
|
+
* @returns {Function} .broadcast - Function to send messages to connected WebSocket clients
|
|
35
|
+
* Signature: broadcast(message: string, userIds?: number[])
|
|
36
|
+
* If userIds provided, sends only to those users; otherwise broadcasts to all authenticated users
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Basic server
|
|
40
|
+
* import { createServer } from 'dzql';
|
|
41
|
+
*
|
|
42
|
+
* const server = createServer({ port: 3000 });
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Server with custom API functions
|
|
46
|
+
* import { createServer, db } from 'dzql';
|
|
47
|
+
*
|
|
48
|
+
* const server = createServer({
|
|
49
|
+
* port: 3000,
|
|
50
|
+
* customApi: {
|
|
51
|
+
* async getVenueStats(userId, params) {
|
|
52
|
+
* const { venueId } = params;
|
|
53
|
+
* return db.api.get.venues({ id: venueId }, userId);
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // Client can call: await ws.api.getVenueStats({ venueId: 1 })
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Server with static files and custom routes
|
|
62
|
+
* const server = createServer({
|
|
63
|
+
* port: 3000,
|
|
64
|
+
* staticPath: './public',
|
|
65
|
+
* routes: {
|
|
66
|
+
* '/api/health': () => new Response(JSON.stringify({ status: 'ok' }))
|
|
67
|
+
* }
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // Server with onReady callback for dynamic setup
|
|
72
|
+
* const server = createServer({
|
|
73
|
+
* onReady: ({ broadcast, routes }) => {
|
|
74
|
+
* // Add routes dynamically
|
|
75
|
+
* routes['/api/notify'] = (req) => {
|
|
76
|
+
* broadcast(JSON.stringify({ method: 'alert', params: { msg: 'Hello!' } }));
|
|
77
|
+
* return new Response('Sent');
|
|
78
|
+
* };
|
|
79
|
+
* }
|
|
80
|
+
* });
|
|
81
|
+
*/
|
|
11
82
|
export function createServer(options = {}) {
|
|
12
83
|
const {
|
|
13
84
|
port = process.env.PORT || 3000,
|