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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.1.0-alpha.3",
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(
@@ -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,